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.
| Key | What it holds |
|---|---|
filter | The raw ?filter= JSON string |
sort | The ?sort= spec — updatedAt, -updatedAt |
pageSize | Rows per page |
columns | Visible column ids, in display order |
extra | Slot-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
| Method | Path | Needs |
|---|---|---|
GET | /api/views | content:read |
POST | /api/views | content:read, plus views:share to share |
PATCH | /api/views/:id | content:read, plus views:share to share |
DELETE | /api/views/:id | content:read |
PUT | /api/views/:id/default | content:read |
DELETE | /api/views/:id/default | content: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
| Limit | Value |
|---|---|
| Name length | 80 |
| Scope key length | 128 |
| Columns per view | 100 |
| Column id length | 128 |
extra keys | 20 |
extra key or value length | 255 |
| Views per user, per workspace and scope | 100 |
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.