Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions content/docs/ai/skills-reference.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ ObjectStack ships **9 domain-specific skills**. Each is self-contained — an AI
| # | Skill | Domain | Path | What it covers |
| :--- | :--- | :--- | :--- | :--- |
| 1 | [Platform](#platform) | `platform` | `skills/objectstack-platform/` | Bootstrap, configure, extend, and operate ObjectStack runtimes. Covers project setup (`defineStack`, drivers, adapters, scaffolding), plugin and service development (PluginContext, DI, kernel hooks like `kernel:ready` and `data:*`), and operations (CLI commands, migrations, deployment, test harnesses via LiteKernel). |
| 2 | [Data](#data) | `data` | `skills/objectstack-data/` | Design ObjectStack data schemas — objects, fields, field conditional rules, relationships, validations, indexes, lifecycle hooks, permissions, row-level security — and the seed definitions (`defineSeed()`) that load fixtures and reference data alongside them. |
| 2 | [Data](#data) | `data` | `skills/objectstack-data/` | Design ObjectStack data schemas — objects, fields, field conditional rules, relationships, validations, indexes, lifecycle hooks, permissions, row-level security — and the seed datasets (`defineDataset()`) that load fixtures and reference data alongside them. |
| 3 | [Query](#query) | `query` | `skills/objectstack-query/` | Construct ObjectQL queries — filters, sorting, pagination, aggregation, joins/expansion, window functions, and full-text search. |
| 4 | [UI](#ui) | `ui` | `skills/objectstack-ui/` | Author ObjectStack UI metadata — Views (list/form/kanban/calendar/gantt), Apps (navigation), Pages (structured plus the HTML and React source-authoring tiers, ADR-0080/0081), Dashboards, Reports, Charts, Actions, and package Docs (`src/docs/*.md`). |
| 5 | [Automation](#automation) | `automation` | `skills/objectstack-automation/` | Design ObjectStack automation — Flows (visual logic), Workflows (declarative rules), Triggers, Approvals, scheduled jobs, and webhooks. |
Expand Down Expand Up @@ -75,7 +75,7 @@ Do not use for data schema design (see objectstack-data) or query patterns (see

**Domain** `data` · **Path** `skills/objectstack-data/`

Design ObjectStack data schemas — objects, fields, field conditional rules, relationships, validations, indexes, lifecycle hooks, permissions, row-level security — and the seed definitions (`defineSeed()`) that load fixtures and reference data alongside them.
Design ObjectStack data schemas — objects, fields, field conditional rules, relationships, validations, indexes, lifecycle hooks, permissions, row-level security — and the seed datasets (`defineDataset()`) that load fixtures and reference data alongside them.

Use when the user is creating or modifying `*.object.ts` / `*.seed.ts` files, picking field types, modelling relationships, writing `beforeInsert`/`afterUpdate` hooks, configuring per-object access control, or authoring bootstrap / demo data. Use for `visibleWhen` / `readonlyWhen` / `requiredWhen` rules that belong on fields.

Expand Down Expand Up @@ -147,7 +147,7 @@ Do not use for general LLM prompting questions unrelated to ObjectStack metadata

Design the server-side API surface that an ObjectStack runtime exposes — REST/GraphQL endpoints, auth providers, realtime channels, error envelopes, batch/versioning contracts.

Use when the user is adding `ApiEndpoint` entries to the `apis:` array in `defineStack()`, configuring auth providers, defining custom routes, or extending the REST/GraphQL generator.
Use when the user is adding `*.endpoint.ts`, configuring auth providers, defining custom routes, or extending the REST/GraphQL generator.

Do not use for: consuming an ObjectStack API from a client (that is just standard HTTP — no skill needed); the auto-generated CRUD endpoints (those follow from objectstack-data); request-side query syntax (see objectstack-query). CEL expressions in route guards or auth predicates: load objectstack-formula alongside.

Expand Down
16 changes: 14 additions & 2 deletions content/docs/references/ui/view.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ Migrated to shared/http.zod.ts. Re-exported here for backward compatibility.
## TypeScript Usage

```typescript
import { AddRecordConfig, AppearanceConfig, CalendarConfig, ColumnSummary, GalleryConfig, GanttConfig, GanttQuickFilter, GroupingConfig, GroupingField, KanbanConfig, ListChartConfig, ListColumn, NavigationConfig, NavigationMode, PaginationConfig, RowColorConfig, RowHeight, SelectionConfig, TimelineConfig, TreeConfig, UserActionsConfig, UserFilterField, UserFilters, ViewData, ViewFilterRule, ViewItemName, ViewKind, ViewScope, ViewSharing, ViewTab, VisualizationType } from '@objectstack/spec/ui';
import type { AddRecordConfig, AppearanceConfig, CalendarConfig, ColumnSummary, GalleryConfig, GanttConfig, GanttQuickFilter, GroupingConfig, GroupingField, KanbanConfig, ListChartConfig, ListColumn, NavigationConfig, NavigationMode, PaginationConfig, RowColorConfig, RowHeight, SelectionConfig, TimelineConfig, TreeConfig, UserActionsConfig, UserFilterField, UserFilters, ViewData, ViewFilterRule, ViewItemName, ViewKind, ViewScope, ViewSharing, ViewTab, VisualizationType } from '@objectstack/spec/ui';
import { AddRecordConfig, AppearanceConfig, CalendarConfig, ColumnSummary, GalleryConfig, GanttConfig, GanttQuickFilter, GroupingConfig, GroupingField, KanbanConfig, ListChartConfig, ListColumn, NavigationConfig, NavigationMode, ObjectUserFilters, PaginationConfig, RowColorConfig, RowHeight, SelectionConfig, TimelineConfig, TreeConfig, UserActionsConfig, UserFilterField, UserFilters, ViewData, ViewFilterRule, ViewItemName, ViewKind, ViewScope, ViewSharing, ViewTab, VisualizationType } from '@objectstack/spec/ui';
import type { AddRecordConfig, AppearanceConfig, CalendarConfig, ColumnSummary, GalleryConfig, GanttConfig, GanttQuickFilter, GroupingConfig, GroupingField, KanbanConfig, ListChartConfig, ListColumn, NavigationConfig, NavigationMode, ObjectUserFilters, PaginationConfig, RowColorConfig, RowHeight, SelectionConfig, TimelineConfig, TreeConfig, UserActionsConfig, UserFilterField, UserFilters, ViewData, ViewFilterRule, ViewItemName, ViewKind, ViewScope, ViewSharing, ViewTab, VisualizationType } from '@objectstack/spec/ui';

// Validate data
const result = AddRecordConfig.parse(data);
Expand Down Expand Up @@ -255,6 +255,18 @@ List chart view configuration
* `none`


---

## ObjectUserFilters

### Properties

| Property | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| **element** | `Enum<'dropdown' \| 'toggle'>` | ✅ | Filter control style on object views: "dropdown" (per-field value chips). "toggle" is deprecated. "tabs" is page-only — use `listViews` for named presets. |
| **fields** | `Object[]` | optional | Fields exposed as quick filters (dropdown/toggle elements) |


---

## PaginationConfig
Expand Down
14 changes: 14 additions & 0 deletions docs/adr/0047-object-ui-run-modes.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,20 @@
> and the page editor edits them in place. `sourceView` is deprecated and kept
> only as a runtime back-compat fallback. TL;DR #2 below no longer holds; the
> rest of the ADR (two run modes, userFilters, visualization whitelist) stands.
>
> **Amendment (2026-07-07) — `dropdown` user filters are allowed on object list
> views again (framework #2679 / objectui #2338).** The phase-4 rollout had
> blanket-suppressed *all* `userFilters` on object list views ("views" mode):
> `ObjectListViewSchema` omitted the field, the `validate` list-view-mode rule
> errored on it, and objectui's `ObjectView` forced `userFilters: undefined` at
> render. That over-corrected against TL;DR #5 (data mode is *supposed* to expose
> quick filters). The narrowed rule: an object list view MAY carry a `dropdown`
> (per-field value-chip) `userFilters`; only the `tabs` preset style stays
> page-only, because an object view's saved-view `ViewTabBar` already owns the
> tab-bar role and a `tabs` user-filter would render a second, conflicting bar
> (need named presets on an object → use `listViews`). Enforced by a new
> `ObjectUserFiltersSchema` (element narrowed to `dropdown`/`toggle`, `tabs`/
> `showAllRecords` omitted), which `ObjectListViewSchema` now `.extend`s back in.
**Deciders**: ObjectStack Protocol Architects
**Builds on**: [ADR-0005](./0005-metadata-customization-overlay.md) (one Zod source per type, org overlay), [ADR-0017](./0017-object-has-many-view.md) (independent view entities, `viewKind`), [ADR-0019](./0019-app-as-consumer-unit.md) (App is the consumer-facing unit — navigation decides what users see), [ADR-0027](./0027-metadata-authoring-lifecycle.md) (draft · publish lifecycle), [ADR-0033](./0033-ai-assisted-metadata-authoring.md) (**AI is the long-term author of metadata — the design center this ADR inherits**)
**Consumers**: `@objectstack/spec` (view + page Zod schemas), `@objectstack/objectql` (registry validation/diagnostics), `../objectui` (console `ObjectView` / `PageView`, `plugin-list`), framework templates (`hotcrm`, `app-showcase`), the `objectstack-ui` authoring skill
Expand Down
11 changes: 11 additions & 0 deletions examples/app-showcase/src/ui/views/project.view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,17 @@ export const ProjectViews = defineView({
label: 'All Projects',
type: 'grid',
data,
// Airtable-style quick-filter chips on the DEFAULT object list view
// (ADR-0047 amendment, framework #2679 / objectui #2338). `dropdown` value
// chips are allowed on object views; `tabs` presets stay page-only (they'd
// collide with the saved-view ViewTabBar — use `listViews` for those).
userFilters: {
element: 'dropdown',
fields: [
{ field: 'status' },
{ field: 'health', showCount: true },
],
},
columns: [
{ field: 'name' },
{ field: 'account' },
Expand Down
38 changes: 28 additions & 10 deletions packages/lint/src/validate-list-view-mode.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { describe, it, expect } from 'vitest';
import { validateListViewMode, LIST_VIEW_FILTERS_IN_VIEWS_MODE } from './validate-list-view-mode.js';

describe('validateListViewMode (ADR-0053 views-mode guardrail)', () => {
describe('validateListViewMode (ADR-0047 views-mode guardrail)', () => {
it('passes a clean stack — object listViews without page-only filters', () => {
const findings = validateListViewMode({
objects: [
Expand All @@ -11,10 +11,19 @@ describe('validateListViewMode (ADR-0053 views-mode guardrail)', () => {
expect(findings).toHaveLength(0);
});

it('flags userFilters on an object built-in list view, with location + hint', () => {
it('allows a dropdown userFilters on an object built-in list view', () => {
const findings = validateListViewMode({
objects: [
{ name: 'task', listViews: { tabular: { label: 'Tabular', userFilters: { element: 'dropdown' } } } },
{ name: 'task', listViews: { tabular: { label: 'Tabular', userFilters: { element: 'dropdown', fields: [{ field: 'status' }] } } } },
],
});
expect(findings).toHaveLength(0);
});

it('flags a tabs userFilters on an object built-in list view, with location + hint', () => {
const findings = validateListViewMode({
objects: [
{ name: 'task', listViews: { tabular: { label: 'Tabular', userFilters: { element: 'tabs' } } } },
],
});
expect(findings).toHaveLength(1);
Expand All @@ -25,7 +34,17 @@ describe('validateListViewMode (ADR-0053 views-mode guardrail)', () => {
});
expect(findings[0].where).toContain('task');
expect(findings[0].message).toContain('views');
expect(findings[0].hint).toContain('filters');
expect(findings[0].hint).toContain('listViews');
});

it('flags a userFilters that carries a tabs array even without element: "tabs"', () => {
const findings = validateListViewMode({
objects: [
{ name: 'task', listViews: { t: { userFilters: { tabs: [{ label: 'Mine', filter: [] }] } } } },
],
});
expect(findings).toHaveLength(1);
expect(findings[0].path).toBe('objects[0].listViews.t.userFilters');
});

it('flags quickFilters too', () => {
Expand All @@ -38,7 +57,7 @@ describe('validateListViewMode (ADR-0053 views-mode guardrail)', () => {
expect(findings[0].path).toBe('objects[0].listViews.all.quickFilters');
});

it('flags userFilters on a defineView default list AND named listViews', () => {
it('flags a tabs userFilters on a defineView default list but allows a dropdown named listView', () => {
const findings = validateListViewMode({
views: [
{
Expand All @@ -48,15 +67,14 @@ describe('validateListViewMode (ADR-0053 views-mode guardrail)', () => {
},
],
});
expect(findings).toHaveLength(2);
const paths = findings.map((f) => f.path).sort();
expect(paths).toEqual(['views[0].list.userFilters', 'views[0].listViews.mine.userFilters']);
expect(findings.every((f) => f.where.includes('task'))).toBe(true);
expect(findings).toHaveLength(1);
expect(findings[0].path).toBe('views[0].list.userFilters');
expect(findings[0].where).toContain('task');
});

it('handles the name-keyed map form of objects', () => {
const findings = validateListViewMode({
objects: { task: { listViews: { t: { userFilters: { element: 'dropdown' } } } } },
objects: { task: { listViews: { t: { userFilters: { element: 'tabs' } } } } },
});
expect(findings).toHaveLength(1);
expect(findings[0].where).toContain('task');
Expand Down
75 changes: 50 additions & 25 deletions packages/lint/src/validate-list-view-mode.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,24 @@
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
//
// Build-time guardrail for ADR-0053 list-view navigation modes.
// Build-time guardrail for ADR-0047 list-view navigation modes.
//
// A pure `(stack) => Finding[]` rule (ADR-0019), run from `os validate` and
// reusable by AI authoring. It catches the "wrong context" authoring mistake
// the type system alone cannot surface at author time: `userFilters` /
// `quickFilters` placed on an object list view ("views" mode — where the
// ViewTabBar is the only nav control), where they are silently dropped. Those
// controls belong to a page list (InterfaceListPage, "filters" mode) only.
// the type system alone cannot surface at author time on an object list view
// ("views" mode — where the ViewTabBar owns the tab-bar role):
// - `quickFilters` — never valid on an object list view;
// - `userFilters` with `element: 'tabs'` (or carrying `tabs`) — the tab-bar
// preset style is page-only; it would collide with the ViewTabBar.
// A `dropdown` (value-chip) `userFilters` IS allowed on object views since the
// ADR-0047 amendment (framework #2679 / objectui #2338) and is NOT flagged.
//
// Runs PRE-parse (on the normalizeStackInput output, before the
// ObjectStackDefinition parse): the object-list schema (ObjectListViewSchema)
// OMITS `userFilters`, so a post-parse stack has already had the field
// stripped and this rule would never see it. The layering is deliberate —
// tsc rejects it at author time, the schema strips it at runtime (no throw,
// back-compat), and this rule reports it at `os validate` with a fix hint.
// See objectui #2219 / #2220 and ADR-0053 phase 4.
// narrows `userFilters` to ObjectUserFiltersSchema (dropdown/toggle only), so a
// post-parse stack has already had a `tabs` user-filter stripped and this rule
// would never see it. The layering is deliberate — tsc rejects it at author
// time, the schema strips it at runtime (no throw, back-compat), and this rule
// reports it at `os validate` with a fix hint. See objectui #2338 and ADR-0047.

export type ListViewModeSeverity = 'error' | 'warning';

Expand All @@ -35,9 +38,6 @@ export const LIST_VIEW_FILTERS_IN_VIEWS_MODE = 'list-view-filters-in-views-mode'

type AnyRec = Record<string, unknown>;

/** Page filters-mode controls that must not appear on an object list view. */
const FORBIDDEN_FIELDS = ['userFilters', 'quickFilters'] as const;

/** Coerce an array-or-name-keyed-map collection to an array (name injected). */
function asArray(v: unknown): AnyRec[] {
if (Array.isArray(v)) return v as AnyRec[];
Expand All @@ -50,7 +50,7 @@ function asArray(v: unknown): AnyRec[] {
return [];
}

/** Emit a finding for each forbidden field present on a single list-view def. */
/** Emit a finding for each wrong-context filter control on a single list-view def. */
function scanView(
view: unknown,
where: string,
Expand All @@ -59,21 +59,45 @@ function scanView(
): void {
if (!view || typeof view !== 'object') return;
const rec = view as AnyRec;
for (const field of FORBIDDEN_FIELDS) {
if (rec[field] == null) continue;

// `quickFilters` is never valid on an object list view.
if (rec.quickFilters != null) {
out.push({
severity: 'error',
rule: LIST_VIEW_FILTERS_IN_VIEWS_MODE,
where,
path: `${path}.${field}`,
path: `${path}.quickFilters`,
message:
`\`${field}\` is a page filters-mode control and is ignored on an object ` +
`list view ("views" mode) — the ViewTabBar is the only nav control here.`,
'`quickFilters` is a page filters-mode control and is ignored on an object ' +
'list view ("views" mode) — the ViewTabBar owns nav here.',
hint:
`Move \`${field}\` to a page list (InterfaceListPage, "filters" mode), or ` +
`remove it. See ADR-0053.`,
'Move `quickFilters` to a page list (InterfaceListPage, "filters" mode), or ' +
'remove it. See ADR-0047.',
});
}

// `userFilters` is allowed on object views ONLY as `dropdown` (value chips).
// The `tabs` preset style — or any `userFilters` carrying `tabs` — collides
// with the ViewTabBar and stays page-only.
const uf = rec.userFilters;
if (uf && typeof uf === 'object') {
const ufRec = uf as AnyRec;
if (ufRec.element === 'tabs' || ufRec.tabs != null) {
out.push({
severity: 'error',
rule: LIST_VIEW_FILTERS_IN_VIEWS_MODE,
where,
path: `${path}.userFilters`,
message:
'`userFilters` with `element: "tabs"` is page-only and is ignored on an ' +
'object list view ("views" mode) — it would collide with the ViewTabBar.',
hint:
'Use `listViews` for named presets on an object (each becomes a segmented ' +
'tab), switch to `element: "dropdown"` for value chips, or move the `tabs` ' +
'filter to a page list (InterfaceListPage, "filters" mode). See ADR-0047.',
});
}
}
}

/** Scan a `listViews` record (name → list-view def). */
Expand All @@ -95,10 +119,11 @@ function scanListViews(
}

/**
* Flag ADR-0053 "views" mode violations: `userFilters` / `quickFilters` on an
* object's built-in named views or a `defineView` default `list` / named
* `listViews`. Returns the list of findings (empty = clean). Caller decides how
* to surface / whether to fail the build.
* Flag ADR-0047 "views" mode violations on an object's built-in named views or a
* `defineView` default `list` / named `listViews`: `quickFilters`, or a `tabs`
* `userFilters`. A `dropdown` `userFilters` is allowed and not flagged. Returns
* the list of findings (empty = clean). Caller decides how to surface / whether
* to fail the build.
*
* Feed the PRE-parse stack (normalizeStackInput output) — see file header.
*/
Expand Down
1 change: 1 addition & 0 deletions packages/spec/api-surface.json
Original file line number Diff line number Diff line change
Expand Up @@ -3251,6 +3251,7 @@
"ObjectListViewSchema (const)",
"ObjectNavItem (type)",
"ObjectNavItemSchema (const)",
"ObjectUserFiltersSchema (const)",
"OfflineCacheConfig (type)",
"OfflineCacheConfigSchema (const)",
"OfflineConfig (type)",
Expand Down
Loading