Features
Saved Views
Vextro provides two layers of view persistence for collection list pages: cookie-based views stored per-browser, and database-backed views that are permanent and optionally shareable across all admin users.
How persistence layers work
| Layer | Scope | Survives browser close | Shareable |
|---|---|---|---|
Cookie (vextro-views) | Per-browser | Yes (1 year) | No |
Database (SavedView) | Per-user or team-wide | Yes (permanent) | Yes (isShared: true) |
Both layers store the same logical state: visible columns, active filters, sort order, search query, and an optional layout preference (list or grid).
When a user visits a collection list page, Vextro resolves the active view by checking the cookie first, then falling back to the database default for that collection.
Cookie-based views
The vextro-views cookie holds all temporary view configurations for every collection in a single JSON blob, keyed by collection slug. It is set with a 1-year max-age and SameSite=Lax, and is capped at approximately 3,800 bytes. When the cookie would overflow, Vextro automatically prunes non-active views from collections other than the currently-visited one.
Cookie structure
type ViewsCookie = Record<string, CollectionViewState>;
type CollectionViewState = {
a: string | null; // active view name
v: Record<string, SavedViewDef>; // named views
};
type SavedViewDef = {
c: string[]; // visible column field names
s?: string[]; // searchable fields (defaults to c)
q?: string; // serialized URL query string (filters/sort/search)
d?: boolean; // is default view
l?: "list" | "grid"; // layout preference
}; Client-side helper functions
These functions are exported from vextro/lib/viewsCookie and run only in the browser:
| Function | Description |
|---|---|
saveView(slug, name, def) | Create or update a named view for a collection |
setActiveView(slug, name | null) | Set the currently-active view |
deleteView(slug, name) | Remove a named view |
renameView(slug, oldName, newName) | Rename a view |
toggleDefault(slug, name) | Toggle the default flag (clears other defaults first) |
setCollectionLayout(slug, layout) | Persist "list" or "grid" layout preference |
Parse-only functions work on both server and client:
| Function | Description |
|---|---|
parseViewsCookie(raw) | Parse the raw cookie string into a ViewsCookie object |
getActiveView(data, slug) | Return the active SavedViewDef for a collection |
getActiveViewName(data, slug) | Return the active view name string |
getDefaultView(data, slug) | Return the default { name, view } for a collection |
getCollectionViews(data, slug) | Return all named views for a collection |
getCollectionLayout(data, slug) | Return the persisted layout preference |
Database-backed saved views
Database views are stored in the Vextro component and surfaced through the admin module API. They are permanent, survive cookie clearing, and can be shared with all admin users.
SavedView type
type SavedView = {
_id: string;
_creationTime: number;
name: string;
collectionSlug: string;
query?: {
filters?: Array<{ field: string; operator: string; value?: unknown }>;
sort?: Array<{ field: string; direction: "asc" | "desc" }>;
search?: string;
pageSize?: number;
};
columns?: Array<string>;
isDefault?: boolean;
isShared?: boolean;
createdBy: string; // user ID string
updatedAt: number; // Unix timestamp
}; Sharing views
Setting isShared: true makes a view visible to all admin users who have read access to the collection — not just the user who created it.
// Create a shared "Active items" view for all admins
await api.admin.createSavedView({
collectionSlug: "products",
name: "Active items",
query: {
filters: [{ field: "status", operator: "eq", value: "active" }],
sort: [{ field: "createdAt", direction: "desc" }],
},
columns: ["title", "status", "price", "createdAt"],
isShared: true,
}); Default views
Setting isDefault: true makes a view the landing configuration when a user first navigates to the collection list. Only one view should be default at a time — updating a view with isDefault: true is the caller's responsibility to manage.
await api.admin.createSavedView({
collectionSlug: "posts",
name: "Recent drafts",
query: {
filters: [{ field: "status", operator: "eq", value: "draft" }],
sort: [{ field: "updatedAt", direction: "desc" }],
},
isDefault: true,
isShared: true,
}); Admin module API
The following functions are available on every generated admin module. They require the caller to be authenticated as an admin user with read access (requireAdminRead).
listSavedViews
Returns all saved views for a collection visible to the current user — both the user's own views and any shared views.
const views = await api.admin.listSavedViews({
collectionSlug: "posts",
});
// Returns: SavedView[] createSavedView
Creates a new saved view and returns the new view's ID.
const viewId = await api.admin.createSavedView({
collectionSlug: "posts",
name: "Published this week",
query: {
filters: [{ field: "status", operator: "eq", value: "published" }],
sort: [{ field: "publishedAt", direction: "desc" }],
pageSize: 25,
},
columns: ["title", "status", "publishedAt"],
isShared: false,
isDefault: false,
});
// Returns: string (view ID) | Argument | Type | Description |
|---|---|---|
collectionSlug | string | * Collection the view belongs to |
name | string | * Display name for the view |
query | object | Filters, sort, search, and pageSize |
columns | string[] | Ordered list of visible column field names |
isShared | boolean | Make visible to all admin users (default false) |
isDefault | boolean | Use as the landing view for the collection (default false) |
updateSavedView
Partially updates an existing saved view by ID.
await api.admin.updateSavedView({
id: viewId,
patch: {
name: "Published this month",
isShared: true,
},
}); | Patch field | Type | Description |
|---|---|---|
name | string | New display name |
query | object | Replacement query config |
columns | string[] | Replacement column list |
isShared | boolean | Share or unshare the view |
isDefault | boolean | Set or unset as default |
deleteSavedView
Permanently deletes a saved view by ID.
await api.admin.deleteSavedView({ id: viewId });
createSavedView triggers a cache invalidation for the collection so list pages reflect the new view immediately. updateSavedView and deleteSavedView do not invalidate the cache — the Convex query subscription will update reactively.
Choosing between cookie and database views
Use cookie views when:
- Users want quick personal presets that don't need to persist after clearing cookies
- Layout preference (
listvsgrid) only needs to be per-device
Use database views when:
- A team needs a standardized "starting point" view for a collection
- An admin wants to share a complex filter/sort configuration with colleagues
- Views should survive across devices or browsers for the same user
Both layers can coexist. The admin UI can allow users to "promote" a cookie view to a database view by reading the cookie state and calling createSavedView with the same configuration.