| title | Plugin View |
|---|
import { InteractiveDemo } from '@/app/components/InteractiveDemo'; import { PluginLoader } from '@/app/components/PluginLoader';
Unified view component for ObjectQL objects with automatic form and grid generation.
npm install @object-ui/plugin-view<PluginLoader plugins={['view']}>
Each preview below is an object-view node drawn by this plugin — the JSON
in the Code tab is the whole example, and the rows on screen came out of a
find() call, not out of that JSON. The records are served by the docs site's
demo data source, because dataSource is not a schema key: it is the prop the
registered renderer pulls off SchemaRendererProvider context (see
Schema API below), so in your own app these same nodes read
whatever object your data source serves. That fixture answers search and sort
but not filters, which is the only reason showFilters: false is authored
here.
- Automatic Views - Generate views from ObjectQL schemas
- Form Generation - Auto-generate forms from object definitions
- Grid Generation - Auto-generate data grids
- CRUD Operations - Built-in create, read, update, delete
- Field Mapping - Automatic field type detection
- ObjectQL Integration - Native ObjectStack support
The keys below are the ones ObjectView actually reads off the schema node
(packages/plugin-view/src/ObjectView.tsx). The node is typed by
ObjectViewSchema in @object-ui/types — objectName and type are its only
required keys, and every example on this page carries the annotation, so a
missing objectName fails to compile rather than rendering an empty view.
import type { ObjectViewSchema } from '@object-ui/types';
const shape: ObjectViewSchema = {
type: 'object-view',
objectName: 'users', // required — the ObjectQL object name
title: 'Users',
description: 'Everyone with an account',
// --- List surface ---
defaultViewType: 'grid', // grid | kanban | gallery | calendar | timeline | gantt | map
listViews: { all: { label: 'All Users', columns: ['name', 'email'] } }, // named views; each needs `label` and `columns`
defaultListView: 'all',
table: { columns: ['name', 'email'] }, // grid configuration (see below)
// --- Record surface (create / edit / read) ---
layout: 'drawer', // drawer | modal | page
form: { showSubmit: true }, // form configuration (see below)
navigation: { mode: 'drawer' }, // row-click behaviour
onNavigate: (recordId, mode) => {}, // required by layout/navigation 'page'
// --- Toolbar ---
showSearch: true,
showFilters: true,
showSort: true,
showCreate: true,
showViewSwitcher: false, // default false
allowCreateView: false,
// --- Built-in CRUD toggles ---
operations: { create: true, read: true, update: true, delete: true },
};The object name key is objectName. There is no object, viewMode,
fields, mode, recordId, fieldConfig, nestedFields, tabs,
enableDelete, filters or searchable key on this node — none of them is a
declared member of ObjectViewSchema, and none is read anywhere in
packages/plugin-view/src. Because type: 'object-view' is registered, a
node built from those keys still resolves to a renderer; it just never receives
an objectName, and the component's data effects are all guarded on it
(ObjectView.tsx's if (!dataSource || !schema.objectName) return; guards), so
the result is a silent empty view rather than an error.
Three structural facts about this node:
dataSourceis not a schema key. It is a required prop ofObjectViewProps(ObjectView.tsx:146). Pass it to<ObjectView>directly, or let the registered renderer pull it offSchemaRendererProvidercontext. PuttingdataSourceinside the schema object does nothing.- There is no
viewMode, and no per-recordmode/recordId. The list type isdefaultViewType(pluslistViews/defaultListView); create, edit and read are internal states of one record surface, opened by the toolbar's create button and by row actions and rendered as a drawer, a modal or a page according tolayout. AccordinglyObjectViewSchema['form']omitsmode— the component sets it. - There are no
onCreate/onUpdate/onDeletecallbacks. The component performs mutations itself through thedataSource. What you can author isoperations(booleans that enable or disable each built-in) andonNavigate(recordId, mode), which hands off to your router when the record surface is a page.
table carries grid configuration and form carries form configuration, but
ObjectView forwards a fixed set of keys from each rather than passing the
object through:
| Sub-config | Keys ObjectView forwards |
|---|---|
table |
read by name: columns, fields, title, filter, sort, pagination, pageSize, selection, selectable, operations, className; handed to the grid as written: editable, singleClickEdit, frozenColumns, rowHeight, resizable, reorderableColumns, searchableFields, showSearch, showPagination, showColumnTypeIcons, conditionalFormatting, rowActions, bulkActions, bulkActionDefs, exportOptions, grouping, aggregations, rowColor, label |
form |
fields, customFields, sections, groups, layout, columns, title, description, subforms, buttons, defaults, initialValues, readOnly, showSubmit, submitText, showCancel, cancelText, showReset, className |
table declares exactly these keys (objectui#10976). Any other grid key —
emptyState, showFilters, description, keyboardNavigation, the record
source (data, staticData), the row click (navigation, onNavigate: write
those on the object-view node), node-level keys such as id, hidden or
style, and the legacy aliases batchActions / resizableColumns (write
bulkActions / resizable) — has nothing to act on in the grid the view
draws, so TypeScript rejects it and the validator refuses it by name. Anything
else you put in form is ignored.
The keys handed to the grid as written reach the grid the registered renderer
draws. Where the active named view declares the same member (see below), the
named view wins and table is the fallback. A host that supplies
renderListView composes its own list and takes columns, fields, filter
and sort from table, as before.
The forwarding is literal, key by key, spread across three sites in
ObjectView.tsx — the non-grid data fetch, the gridSchema memo, and the
mergedFilters / mergedSort pair handed to a host-supplied list renderer —
so the keys below behave the same on every rendering path.
table.defaultFiltersis retired (objectui#12030). It was the legacy spelling oftable.filter, the same rule array.@objectstack/spec18 refusesobject-grid'sdefaultFiltersby name (a retired-key tombstone, objectstack#11509), and neitherObjectViewnorObjectGridreads it any more: a view that still carries it renders unfiltered. Rename the key tofilter; the value does not change.
table.defaultSortis retired (objectui#5861). It was the legacy single-entry spelling oftable.sort. The installed@objectstack/specprotocol refusesobject-grid'sdefaultSortby name (a retired-key tombstone), andObjectViewno longer reads it on any of the three paths above (nor doesObjectGrid): a view that still carries it renders unsorted. Rename the key tosortand wrap the value in an array —defaultSort: { field: 'name', order: 'asc' }becomessort: [{ field: 'name', order: 'asc' }].
Two of the forwarded table keys are pairs — a canonical ObjectGridSchema
key and the @deprecated legacy spelling it replaced. Both work; write the
canonical one. (Two more pairs, filter / defaultFilters and sort /
defaultSort, used to be listed here; their legacy halves are retired — see
the notes above.)
| write this (canonical) | not this (legacy alias — still works) |
|---|---|
pagination: { pageSize, pageSizeOptions? } |
pageSize: number |
selection: { type: 'single' | 'multiple' | 'none' } |
selectable: boolean | 'single' | 'multiple' |
Before objectui#5102, pagination / selection / filter / sort had no
read point at all in this file: an author who wrote the canonical shape
ObjectGridSchema's own JSDoc recommends got a view that compiled, read
correctly, and silently did nothing. That is fixed — the legacy spellings on
the right are simply no longer the ones to reach for.
When a key is written both ways, the canonical spelling wins — that is
ObjectGrid's own existing resolution for the paging pairs
(schema.pagination?.pageSize || schema.pageSize; if (schema.selection?.type) … else if (schema.selectable !== undefined)), and
ObjectView defers to it by forwarding both slots rather than re-resolving
the pair itself. table.filter reaches the grid in its one filter slot
(objectui#11880), unless it lowers to nothing (absent, [] or {}), so its
export carries the filter in force and a change to it returns the grid to
page 1:
import type { ObjectViewSchema } from '@object-ui/types';
const bothSpellingsWritten: ObjectViewSchema = {
type: 'object-view',
objectName: 'products',
table: {
pagination: { pageSize: 10 }, // wins
pageSize: 50, // ignored while `pagination` is present
},
};filter / sort have one more tier ahead of table entirely, and it
predates this change: an active named view's own filter / sort
(listViews.<name>.filter / .sort) always outranks anything written on
table. In order, highest first: the active named view's filter/sort,
then table.filter/table.sort. Neither has a legacy tier: table.defaultFilters
and table.defaultSort are retired. If you never write listViews, that
first tier never applies.
Whichever tier wins, its filter goes through resolveFilterPlaceholders from
@object-ui/core before it leaves ObjectView (objectui#10506), on all three
paths: the query a non-grid view issues, the grid, and a host's
renderListView. So the spec's context tokens ({current_user_id},
{current_org_id}) and the relative-date macros ({today},
{current_quarter_start}, …) arrive as real values. The user and organization
come from the nearest FilterScopeProvider (@object-ui/react), which the
console shell mounts; with none mounted a context token is left as written and
the resolver warns. The filter is never widened: the ObjectStack server resolves
the literal token for a signed-in request and refuses the request otherwise
(the REST face answers 401 at its auth gate before the filter is read, and
where a guest context reaches the engine the resolver answers 400), and a
backend with no resolver of its own matches no record.
pagination and selection have no such tier, and no effect outside the
grid: defaultViewType: 'kanban' | 'gallery' | 'calendar' | 'timeline' | 'gantt' | 'map' don't page or multi-select, so ObjectView never forwards
either spelling to those renderers.
columns is the one forwarded table key that is not part of this
canonical/legacy story, and its gap is still open: it is forwarded on the grid
path only (the row above, and the section below), so on a non-grid
defaultViewType the field list still comes from table.fields
(objectui#5269).
import '@object-ui/plugin-view';That single import is the whole of registration — there is no components map to
iterate over. Importing the entry runs the five ComponentRegistry.register(...)
calls in packages/plugin-view/src/index.tsx, which claim exactly these schema
types:
| Namespaced key | Bare-name fallback | Renderer behind it |
|---|---|---|
plugin-view:object-view |
object-view |
ObjectViewRenderer — list plus integrated create / edit |
plugin-view:view:simple |
view:simple |
SimpleViewRenderer — container-only view |
view:view-switcher |
view-switcher |
ViewSwitcher |
view:filter-ui |
filter-ui |
FilterUI |
view:sort-ui |
sort-ui |
SortUI |
Both spellings resolve here: ComponentRegistry.register publishes
namespace:type and, unless the call passes skipFallback: true, the bare
type as a back-compat fallback (ComponentRegistry.register in
packages/core/src/registry/Registry.ts, its !meta?.skipFallback branch). No
call in this package sets skipFallback. Note the
namespaces are not uniform — object-view and view:simple register under
plugin-view, the three control components under view.
The bare view alias of object-view is RETIRED (objectui#10859 batch 8): it
named the same renderer as object-view, and objectui validate refused it at
type. Author object-view.
The shared-view-link node key is RETIRED too (objectui#10859 batch 8, phase 2b):
no schema produced it, and objectui validate refused it at type.
SharedViewLink is still exported; mount the component directly.
ObjectViewRenderer and SimpleViewRenderer are internal wrappers and are not
exported: the wrapper resolves dataSource from the renderer context and hands
the schema to ObjectView, which takes dataSource as a required prop rather
than as a schema key.
To serve one of this package's components under a key of your own, register the exported component — that is what a manual registration is here:
import { ComponentRegistry } from '@object-ui/core';
import { ViewSwitcher } from '@object-ui/plugin-view';
ComponentRegistry.register('my-switcher', ViewSwitcher, { namespace: 'my-app' });ObjectView, ViewSwitcher, FilterUI, SortUI, SharedViewLink,
ViewTabBar and ManageViewsDialog are on the package's export surface,
alongside the derivation helpers (deriveRecordSurface, deriveFieldOptions,
toFilterGroup, toSortItems, …) and the component prop types
(ObjectViewProps, ViewSwitcherProps, …). There is no aggregate map among
them, and no schema types — the authored object-view node is typed by
@object-ui/types.
The list is always rendered. defaultViewType picks which renderer draws it,
and table configures the grid:
import type { ObjectViewSchema } from '@object-ui/types';
const userDirectory: ObjectViewSchema = {
type: 'object-view',
objectName: 'users',
defaultViewType: 'grid',
table: {
columns: ['name', 'email', 'role', 'created_at'],
sort: [{ field: 'created_at', order: 'desc' }],
},
};Non-grid types (kanban, gallery, calendar, timeline, gantt, map) are
rendered through SchemaRenderer, so @object-ui/react and the matching plugin
must be installed for those. On that path the field list comes from
table.fields (or from the active named view's columns), not from
table.columns.
Create and edit share one record surface. layout chooses where it opens and
form configures what it contains — there is no separate "form view" node and
no authored mode:
import type { ObjectViewSchema } from '@object-ui/types';
const userForm: ObjectViewSchema = {
type: 'object-view',
objectName: 'users',
layout: 'drawer', // drawer | modal | page
form: {
fields: ['name', 'email', 'role'],
submitText: 'Save user',
showCancel: true,
},
};When layout is omitted, the surface is derived from how heavy the object is
(deriveRecordSurface): a field-heavy object opens as a page, a light one as a
drawer, and mobile always pages. A page is handed to onNavigate; with none to
hand it to, as on the registered object-view renderer, whose JSON schema
cannot carry a function, the page falls back to the drawer for create, edit and
read.
Reading a record is the same surface in its read state, reached by clicking a
row — there is no recordId to author, because the click chooses the record.
navigation.mode decides how it opens, and onNavigate is what hands a
page-mode record off to your router:
{/* doc-snippet: fragment — the navigation callback hands off to the host's own router */}
import type { ObjectViewSchema } from '@object-ui/types';
const userDetail: ObjectViewSchema = {
type: 'object-view',
objectName: 'users',
layout: 'page',
navigation: { mode: 'page' }, // none | drawer | modal | page | split | popover | new_window
onNavigate: (recordId, mode) => {
// mode is 'view' or 'edit'
router.push(`/users/${recordId}${mode === 'edit' ? '/edit' : ''}`);
},
};Without an onNavigate handler, page mode has nowhere to send the user, so
keep the two together: a page layout without one falls back to the drawer,
and a row click under navigation: { mode: 'page' } opens nothing.
navigation: { mode: 'none' } (or preventNavigation)
makes rows inert. An active named view (listViews, under Read/List) that
declares its own navigation replaces this one, as a whole, while it is shown.
Under split or popover, which open only beside a record the user picked, the
New button's create form opens on the layout surface instead: the modal for
layout: 'modal', the drawer otherwise, whether the mode is this node's or an
active named view's. A page layout with onNavigate still hands New to your
router.
With no host onRowClick, a Cmd/Ctrl-click or middle-click on a row opens the
record as a full page in a new browser tab. Inert rows stay inert: mode: 'none',
preventNavigation, and operations.read: false without a navigation config
ignore a modifier click too.
All four operations are built in and run against the dataSource prop. You do
not wire handlers for them — you switch them on or off with operations, and
the show* flags control whether the matching toolbar affordance is visible.
operations.create enables record creation; showCreate shows the button. Both
default to on, and the button also needs the listNew row of the
affordance-to-grant map (@object-ui/core) to allow it — the object's policy,
the effective API operation set and the caller's create grant — exactly as
the console's list pages read it. With no permission provider mounted that grant
reads open. The new-record form opens on the surface a drawer or modal
navigation names, and on the layout surface otherwise, under split and
popover too (see Opening a record):
import type { ObjectViewSchema } from '@object-ui/types';
const productCreate: ObjectViewSchema = {
type: 'object-view',
objectName: 'products',
showCreate: true,
operations: { create: true },
layout: 'drawer',
form: { fields: ['name', 'price', 'category'] },
};With a page surface, authored or derived, creation calls
onNavigate('new', 'edit'), whatever the navigation, so the host route owns the
form. With no onNavigate, the page has nowhere to route, so it falls back to
the drawer.
Search, filter and sort are toolbar toggles; the column set, filter, sort and
page size live in table:
import type { ObjectViewSchema } from '@object-ui/types';
const productList: ObjectViewSchema = {
type: 'object-view',
objectName: 'products',
defaultViewType: 'grid',
showSearch: true,
showFilters: true,
showSort: true,
table: {
columns: ['name', 'price', 'category'],
filter: [{ field: 'category', operator: 'equals', value: 'electronics' }],
pagination: { pageSize: 25 },
},
};Saved views are listViews, keyed by view name, with defaultListView
selecting which opens first:
import type { ObjectViewSchema } from '@object-ui/types';
const productViews: ObjectViewSchema = {
type: 'object-view',
objectName: 'products',
listViews: {
all: { label: 'All Products', type: 'grid', columns: ['name', 'price'] },
cheap: {
label: 'Under 100',
type: 'grid',
columns: ['name', 'price'],
filter: [{ field: 'price', operator: 'less_than', value: 100 }],
},
},
defaultListView: 'all',
};On the registered renderer. An authored object-view node, and the Studio's
view preview, draws a grid named view through ObjectGrid. Ten grid members the
protocol declares under the same name on a named view and on object-grid —
pagination, selection, rowHeight, resizable, searchableFields,
conditionalFormatting, rowActions, bulkActions, bulkActionDefs and
exportOptions — come from the active named view first, and fall back to
the same key on table (pagination and selection since objectui#10885, the
rest since objectui#10976). A named view's
hiddenFields removes those fields from the columns the grid draws, when a
column list is declared, and its fieldOrder then orders the columns that
remain, the way ListView orders them on a host's renderListView. Its
inlineEdit turns on in-cell editing, and only where the object grants inline
edit to the user. Its navigation replaces the node's navigation as a whole:
the row click, and the surface and width of the record, create and edit forms,
follow the active named view, on this path, and on a host's renderListView
when the host wires the onRowClick it is handed and passes ObjectView no
onRowClick of its own. Under a named split or popover, the create form
opens on the layout surface, as under Opening a record. label and data,
also declared on both, are not
handed to the grid on this path: the named view's label is already the tab's
text, and data waits on objectui#10971, because ListView and ObjectGrid
pick different objects for it.
src/__tests__/ObjectView.routeTwoNamedGridMembers-10885.test.tsx pins the ten
grid members and hiddenFields; ObjectView.namedViewNavigation-10885.test.tsx
and ObjectView.namedViewInlineEdit-10885.test.tsx pin navigation and
inlineEdit, and @object-ui/app-shell's
objectViewRouteParity.fieldOrder-10885.test.tsx pins fieldOrder against
ListView.
On a host's renderListView. A host that composes ObjectView with both
listViews and its own renderListView receives a list-view node for the
active view. For the list members the protocol declares on a named view — list
chrome (description, compactToolbar, allowPrinting, showRecordCount,
sharing, aria, emptyState), record actions (addRecord, inlineEdit,
rowActions, bulkActions, bulkActionDefs, exportOptions), grid
presentation (rowHeight, pagination, selection, resizable,
hiddenFields, conditionalFormatting), and search, filter and navigation
(searchableFields, filterableFields, userFilters, navigation) — that
node takes the value from the active named view first, ahead of the host's
views entry (objectui#10758).
src/__tests__/ObjectView.namedViewProtocolKeys-8980.test.tsx pins each family.
Inline editing on that node is on by default. Both keys below are the
author's permission. ListView reads the node's userActions.editInline with
the spec's default, on since the v18 ruling (objectstack#22605), so a view that
declares neither key offers the inline-edit toggle (objectui#12086), and
userActions.editInline: false is how a view withdraws it. When the node
carries no editInline of its own, its inlineEdit stands in for it
(objectui#5144). The named view's keys win over the host views entry's, as
for every member above:
| The view declares | Inline-edit toggle | Grid opens in edit mode |
|---|---|---|
| neither key | offered | no |
inlineEdit: true |
offered | yes |
inlineEdit: false |
not offered | no |
userActions.editInline: true |
offered | only with inlineEdit: true |
userActions.editInline: false |
not offered | no, whatever inlineEdit says |
The toggle switches the edit mode for the session and reports it through
ListView's onInlineEditChange. The console stores nothing for it: each load
opens in the mode the view's inlineEdit declares, so a user's choice is not
remembered across loads. A view where an earlier console stored
inlineEdit: false still reads off; that data is not migrated, and declaring
userActions.editInline: true brings the toggle back. A list whose records are
read-only by nature declares userActions.editInline: false. Neither key opens
editing past the object's own editability or the user's permission to update
the object.
Editing is reached from a row's edit action, and operations.update is what
gates it. The edited record is chosen by the click, never by an authored
recordId:
import type { ObjectViewSchema } from '@object-ui/types';
const productEdit: ObjectViewSchema = {
type: 'object-view',
objectName: 'products',
operations: { update: true },
layout: 'modal',
form: { fields: ['name', 'price'], submitText: 'Update' },
};Under a page surface this becomes onNavigate(recordId, 'edit'); a page with
no onNavigate falls back to the drawer.
operations.delete enables both the per-row delete and bulk delete. There is no
enableDelete key and no onDelete callback:
import type { ObjectViewSchema } from '@object-ui/types';
const productNoDelete: ObjectViewSchema = {
type: 'object-view',
objectName: 'products',
operations: { create: true, read: true, update: true, delete: false },
};There is no fieldConfig key. Labels, types, requiredness and validation come
from the object's own metadata, which the view reads through the dataSource —
that is what makes the view "automatic". What the schema node chooses is
which fields appear and how they are grouped:
import type { ObjectViewSchema } from '@object-ui/types';
const userFields: ObjectViewSchema = {
type: 'object-view',
objectName: 'users',
table: {
columns: ['name', 'email', 'role'], // grid columns
},
form: {
fields: ['name', 'email', 'role'], // flat field list, or use sections
sections: [
{ label: 'Identity', fields: ['name', 'email'] },
{ label: 'Access', fields: ['role'] },
],
columns: 2,
},
};To override a field's rendering beyond what the object metadata says, use
form.customFields (full field definitions) rather than a per-field patch on
the view node.
There is no nestedFields key. A child collection is declared as a subform
on the record form, which is where an order's line items belong:
import type { ObjectViewSchema } from '@object-ui/types';
const orderView: ObjectViewSchema = {
type: 'object-view',
objectName: 'orders',
layout: 'page',
form: {
fields: ['order_number', 'customer', 'total'],
subforms: [
{
childObject: 'order_items',
title: 'Line items',
columns: [{ name: 'product' }, { name: 'quantity' }, { name: 'price' }],
},
],
},
};Only childObject is required — the relationship field and the grid columns are
derived from the child object's metadata unless you override them
(relationshipField, columns). A column is @objectstack/spec's
InlineGridColumn: an object keyed by name, never a bare field name. One that
declares no type takes its label, type and the rest from the child field;
an undeclared column key is refused by the validator.
There is no tabs key, and form.layout has no tabbed value
(vertical | horizontal). The tab strip this package ships is
the saved-view tab bar: declare the views and render <ViewTabBar> (or let
a host such as @object-ui/app-shell do it):
import type { ObjectViewSchema } from '@object-ui/types';
const userTabs: ObjectViewSchema = {
type: 'object-view',
objectName: 'users',
showViewSwitcher: true,
allowCreateView: true,
listViews: {
active: { label: 'Active', type: 'grid', columns: ['name', 'email'] },
admins: {
label: 'Admins',
type: 'grid',
columns: ['name', 'email'],
filter: [{ field: 'role', operator: 'equals', value: 'admin' }],
},
},
defaultListView: 'active',
};To group a form's fields instead, use form.sections as shown under "Field
Configuration".
The adapter is the dataSource prop, not part of the schema:
import { createObjectStackAdapter } from '@object-ui/data-objectstack';
import { ObjectView } from '@object-ui/plugin-view';
import type { ObjectViewSchema } from '@object-ui/types';
const dataSource = createObjectStackAdapter({
baseUrl: 'https://api.example.com',
token: 'your-auth-token',
});
const contactView: ObjectViewSchema = {
type: 'object-view',
objectName: 'contacts',
defaultViewType: 'grid',
showSearch: true,
showSort: true,
table: {
columns: ['first_name', 'last_name', 'email', 'company'],
pagination: { pageSize: 25 },
},
};
<ObjectView schema={contactView} dataSource={dataSource} />;Rendering the same node through the registry instead (type: 'object-view' in a
larger schema tree) works because ObjectViewRenderer reads the dataSource
off SchemaRendererProvider context — again, not off the schema.
This package's type export surface is the component *Props types plus the
record-surface and field-option types listed under "Registering a component
under your own key" — it ships no schema types. The authored
type: 'object-view' node is typed by @object-ui/types, which
@object-ui/plugin-view imports without re-exporting, so import it from there:
Import from @object-ui/types |
What it types |
|---|---|
ObjectViewSchema |
the whole type: 'object-view' node — objectName (required), title, description, layout, defaultViewType, listViews, defaultListView, navigation, table, form, searchableFields, filterableFields, the show* flags, operations, onNavigate, allowCreateView, viewActions (viewTabBar is retired — objectui#7779 — and refused by name) |
NamedListView |
the former type of a listViews entry, still exported. Since objectui#7928 an entry is @objectstack/spec's ObjectListViewSchema (NonNullable<ObjectViewSchema['listViews']>[string]): toolbar toggles go under userActions (search, sort, filter, group, hideFields, rowHeight, rowColor), showDescription under appearance, and view-kind config in the top-level kanban / calendar / … block, never a legacy options bag |
ViewNavigationConfig |
navigation — row/item click behaviour |
ViewTabBarConfig |
the config prop of ViewTabBar — tab-bar UX (inline add, overflow, indicators), composed by the host; not an object-view node key |
import type { ObjectViewSchema } from '@object-ui/types';
const userView: ObjectViewSchema = {
type: 'object-view',
objectName: 'users',
defaultViewType: 'grid',
// Displayed columns are grid configuration, inherited from ObjectGridSchema.
table: { columns: ['name', 'email', 'role'] },
};MIT