Features Apograph CMS on GitHub

Saved views plugin

Saved views plugin @apograph/content-server

Naming the slice of a collection an editor returns to, and sharing it.

Documents 0.5.2 Updated Edit this page Report a problem

On this page

Saved list views: the named slice of a collection an editor returns to. It is the one plugin in this catalogue that is not a second package — it is a second ServerPlugin entry exported from @apograph/content-server, and its admin half is part of @apograph/content-admin.

Why it is a separate entry rather than part of content

ServerPlugin.migrations carries exactly one descriptor, and content’s is already spoken for: it points at the host’s generated collection tables, the ones drizzle.config.ts diffs out of src/content/index.ts. This feature has fixed tables of its own, so they ride their own entry with their own tracker, __drizzle_migrations_content_views.

Install

Nothing to install. Both halves ship in packages a deployment already has.

Register

// apps/server/src/plugins.ts
import { ContentPlugin, ContentViewsPlugin } from '@apograph/content-server';

const content = ContentPlugin({ types: contentTypes, migrations: {} });

// …after identity and workspaces, and after `content` itself:
ContentViewsPlugin({ content });

The admin side needs no registration — ContentPlugin() from @apograph/content-admin already renders the view switcher above the records table.

Placement is load-bearing twice over

saved_views carries foreign keys into identity’s users and workspaces’ workspaces, and migrations run in plugin-list order with nothing declaring that dependency — so it must come after both. It also takes content by value, for the registry that resolves a view’s content:<typeName> scope.

What a view stores

A view mirrors the URL parameters the records page already owns rather than re-modelling them, so it round-trips through the same code path a hand-edited link does.

KeyWhat it holds
filterThe raw ?filter= JSON string
sortThe ?sort= spec — updatedAt, -updatedAt
pageSizeRows per page
columnsVisible column ids, in display order
extraSlot-owned list params, such as i18n’s ?locale=

extra is an opaque string map on purpose. Its keys come from RECORDS_TOOLBAR_SLOT.listParamKeys at runtime, so naming them in the contract would break the next plugin’s parameters silently.

Two things are absent by decision. search is a one-off question rather than a property of the slice, and page is a reading position — so a view always opens on the first page.

Visibility, and the one permission

A view is private or workspace. The controller requires content:read for the whole surface — you cannot save a view over a list you cannot read — and sharing additionally requires views:share, checked per write rather than per class.

That split is the point. A private view is a personal bookmark over content the person can already see, so every role can make one. A shared view becomes navigation for the whole workspace, which is an editorial decision: contributors hold views:share, viewers do not, and viewers are not blocked from anything by its absence.

isDefault is per user, not per workspace. Marking a shared view as your default does not make it anyone else’s.

Routes

MethodPathNeeds
GET/api/viewscontent:read
POST/api/viewscontent:read, plus views:share to share
PATCH/api/views/:idcontent:read, plus views:share to share
DELETE/api/views/:idcontent:read
PUT/api/views/:id/defaultcontent:read
DELETE/api/views/:id/defaultcontent:read

Scope

A view belongs to a list, named by a scope key. Today that is content:<typeName> and nothing else: the DTO refuses any other prefix, so a typo cannot quietly create a view nothing will ever read. users:members is the shape the key was chosen for, if the switcher reaches the other list pages.

Limits

LimitValue
Name length80
Scope key length128
Columns per view100
Column id length128
extra keys20
extra key or value length255
Views per user, per workspace and scope100

Names are unique per owner within a workspace and scope — two people may each have a view called “Needs a cover”, and both may share it.

Configuration

It takes no configuration and reads no environment variables.