Views Module Technical Specifications
Save filtered list views per-user, build analytics dashboards from any collection, enforce per-user defaults, and govern access via collection-level rules — wired into every collection's auto-generated CRUD.
System Metrics
Saved view loads
< 50ms (stored configuration, not computed)
Default enforcement
Atomic, race-condition-free — never more than one default at a time
Access checks
Index-optimized lookups by owner, by collection, and by default status
Dashboard layout
Opaque JSON, validated as an object on write — no server-side widget resolution
Filter operators
30+ via QueryBuilder — equals, gte, lt, in, not_in, contains, contains_any, etc.
Architecture Overview
The Views module has two concerns:
- Saved Views — Filter, sort, column, grouping, and view-type configurations for any collection
- Dashboard Workspaces — Grid-based widget layouts assembled from any module's widgets
Both are built on Aeion's standard auto-generated CRUD for their underlying data, plus a small set of view-specific API routes layered on top — set-default, and listing views scoped to a particular collection.
Because both saved views and dashboards ride on the platform's standard collection infrastructure rather than a bespoke storage layer, they inherit tenant isolation, access control, indexing, backup/restore, and audit history for free — the same guarantees every other collection in Aeion OS gets.
Data Model
| Field | Purpose |
|---|---|
| `name` | Display name shown in the Saved Views dropdown |
| `collectionSlug` | Which collection this view applies to (e.g., `deals`, `orders`) |
| `filter` | The filter configuration, using Aeion's standard operator syntax |
| `sort` | Sort field and direction |
| `columns` | Ordered list of visible columns |
| `viewType` | `list`, `kanban`, `calendar`, `gallery`, or `gantt` |
| `grouping` | Group-by field and aggregation rules |
| `conditionalFormatting` | Color-coding rules |
| `isDefault` | Whether this is the current user's default view for the collection |
| `isShared` | Whether teammates can see and apply this view |
| `createdBy` | The view's owner |
Auto-Default Enforcement
Saved Views — One Default Per Collection Per User
Whenever you mark a saved view as your default, Aeion automatically unsets your previous default for that same collection as part of the same save — there's no separate cleanup step and no window where two views both claim to be the default.
What this prevents: You have 3 saved views for the "deals" collection. You click "Set as default" on a fourth. Without this enforcement, you could end up with 4 defaults — ambiguous which one loads. With it, the 4th becomes default and the previous 3 are automatically unset.
Race condition handling: If two browser tabs simultaneously set different views as default, the result is still deterministic — whichever write commits last wins, and it's guaranteed to be the only default. No manual locking or conflict resolution required on your end.
Dashboards — One Default Per User
Dashboards work the same way, except the default is scoped per-user across all dashboards rather than per-collection — you have one default home dashboard, not one per data source.
Access Control
Saved views and dashboards use identical, straightforward access rules:
- Read: your own views, plus anything shared with the team
- Create: any authenticated user
- Update / Delete: the owner, or a tenant admin
Shared views are read-only for non-owners — other users see them but can't edit or delete them. They can, however, duplicate a shared view into their own copy and modify that freely.
Admin override: Admins can update or delete any view in their tenant — used for cleanup when users leave the organization.
API Routes
All view-specific routes require authentication and supplement the standard auto-generated CRUD with view-specific operations.
GET /views/for-collection/:slug
Lists saved views for a specific collection — the requesting user's own views plus any shared views, sorted by creation date.
Use case: Opening a collection fetches saved views for that collection and populates the Saved Views dropdown with "Your views" and "Shared views."
PATCH /views/:id/set-default
Sets a view as default and automatically unsets any previous default for that collection. The request is rejected with a 404 if the view doesn't exist and a 403 if you don't own it — you can only set your own views as default.
Dashboard CRUD Routes
Same pattern for dashboards:
GET /views/dashboards/list— user's own + sharedPOST /views/dashboards— createPATCH /views/dashboards/:id— update (owner or admin)DELETE /views/dashboards/:id— delete (owner or admin)
Frontend Behavior
The Saved Views control appears in the toolbar of every collection in the admin. It handles:
- Applying a view — instantly swaps filter, sort, columns, view type, grouping, and conditional formatting
- Saving current state — captures the working configuration as a new named view
- Rename / Delete / Set Default — available inline for any view you own
- Sharing — toggling "Shared" makes a view visible to the rest of the tenant
States your team will recognize:
- Default view selected: star icon on the default view, loads automatically on open
- Unsaved custom state: an "Unsaved Changes" indicator with an active Save button
- Shared views: shown with a "shared" badge; non-owner views are read-only
- Rename mode: inline edit field replaces the view name
- Empty state: "No saved views yet" with a prominent "Save Current View" action
Every field of a saved view — its filter, sort, columns, view type, grouping, and conditional formatting — is captured on save and restored exactly when the view is applied again, including from other devices and sessions.
Request/Response Shape
Every Saved Views API request accepts and validates the same fields you can set in the UI:
typescript
{
name: string; // required
collectionSlug: string; // required
viewType?: "list" | "kanban" | "calendar" | "gallery" | "gantt";
filter?: Record<string, unknown>;
sort?: string;
columns?: string[];
grouping?: Record<string, unknown>;
conditionalFormatting?: Record<string, unknown>;
isDefault?: boolean;
isShared?: boolean;
}
The same shape applies to dashboards, substituting layout for the view-specific fields. Validation happens on every write, so malformed filters or unknown view types are rejected with a clear error rather than silently stored.
Dashboard Layout Storage
A dashboard's layout is stored as a single JSON field — the Views module doesn't lock it to a specific grid or widget-reference schema. Every write is still validated as a JSON object, but the shape of positions, sizing, and per-widget configuration is left to whatever's building on top of it.
Why schema-less: locking layout to one rigid interface would mean a migration every time a new widget type or grid convention ships. Leaving it schema-less means the storage layer never has to change — only the code that reads and writes it does. This is the same trade-off Saved Views makes with filter, grouping, and conditionalFormatting, which are also stored as unstructured JSON.
Filter Operators Reference
| Operator | Example | Meaning |
|---|---|---|
| `equals` | `{ status: { equals: "active" } }` | Exact match |
| `not_equals` | `{ status: { not_equals: "archived" } }` | Not equal |
| `contains` | `{ name: { contains: "acme" } }` | Case-insensitive substring |
| `starts_with` | `{ email: { starts_with: "admin@" } }` | Prefix match |
| `ends_with` | `{ email: { ends_with: "@acme.com" } }` | Suffix match |
| `gt` / `gte` | `{ value: { gte: 50000 } }` | Greater than(equal) |
| `lt` / `lte` | `{ closeDate: { lt: "2026-07-01" } }` | Less than(equal) |
| `in` | `{ stage: { in: ["Negotiation", "Proposal"] } }` | Match any in array |
| `not_in` | `{ status: { not_in: ["Archived"] } }` | Exclude array values |
| `exists` | `{ archivedAt: { exists: true } }` | Field is/isn't null |
| `contains_any` | `{ tags: { contains_any: ["enterprise"] } }` | Array contains any of the values |
| `or` | `{ or: [{ a: 1 }, { b: 2 }] }` | OR group |
Architecture Decisions
Storage
Standard platform collections — Views are data, not logic — inherits tenancy, backup, and access control for free
Default enforcement
Atomic auto-unset on write — Single source of truth — never more than one default at a time, no race conditions
Access control
Owner-or-shared read, owner-or-admin write — Simple, predictable sharing model across every collection
Layout storage
Flexible, schema-less widget configuration — New widget types and layouts ship without migrations
Frontend state
Optimistic UI updates with automatic rollback — Views and dashboards feel instant, even before the server confirms
View types
5 values: list/kanban/calendar/gallery/gantt — Covers the large majority of data exploration needs
Column storage
Ordered list of field names — Order-preserving, easy to export and reapply