diff --git a/.changeset/plugin-view-readme-real-schema-keys.md b/.changeset/plugin-view-readme-real-schema-keys.md new file mode 100644 index 000000000..77fead282 --- /dev/null +++ b/.changeset/plugin-view-readme-real-schema-keys.md @@ -0,0 +1,40 @@ +--- +'@object-ui/plugin-view': patch +--- + +The plugin-view README now documents the keys `ObjectView` actually reads, so a +copied example renders instead of coming up empty. + +Every untyped schema literal in the README was written against a key vocabulary +`ObjectViewSchema` does not declare and `ObjectView` does not read. The object +name was spelled `object` — the real key is `objectName`, and it is the only +required key besides `type` — so a copied example left the component with no +object to query. Three "view modes" were organized around a `viewMode` key that +exists nowhere, and `fields`, `mode`, `recordId`, `fieldConfig`, `nestedFields`, +`tabs`, `searchable`, `sortable`, `filters` and `enableDelete` were documented +the same way. None of it failed loudly: `ObjectViewSchema` extends a base schema +carrying a `[key: string]: any` index signature, so excess-property checking is +defeated on this type, and the blocks carried no type annotation to trip even +the one assertion that does bite. + +The thirteen affected blocks are rewritten against the declared surface, each +one measured against the renderer before being written: `defaultViewType` (plus +`listViews` / `defaultListView`) for the list type, `layout` with its +drawer/modal/page record surface for what the README called form and detail +views, `table` and `form` for grid and form configuration, `operations` +booleans and `onNavigate` in place of the `onCreate` / `onUpdate` / `onDelete` / +`onSubmit` callbacks that were never part of this contract, and the `show*` +toolbar toggles. Examples now carry `ObjectViewSchema` annotations, which makes +a missing `objectName` a compile error in all fifteen of them. + +Three structural facts are stated outright rather than left to be inferred: +`dataSource` is a required prop of `ObjectViewProps` and not a schema key, so +putting it in the schema does nothing; create/edit/read are internal states of +one record surface rather than authored modes, which is why `ObjectViewSchema` +omits `mode` from its `form` block; and `ObjectView` forwards a fixed list of +keys out of `table` and `form` rather than passing those objects through, so the +README now names exactly which ones — including that page size is `table.pageSize` +on this path, the spelling the component forwards. + +The `ViewSwitcher`, `FilterUI` and `SortUI` sections are untouched: their keys +were checked against the registered `inputs` and already matched. diff --git a/packages/plugin-view/README.md b/packages/plugin-view/README.md index 954e55c63..e9bcf7c51 100644 --- a/packages/plugin-view/README.md +++ b/packages/plugin-view/README.md @@ -26,15 +26,20 @@ pnpm add @object-ui/plugin-view ```typescript // In your app entry point (e.g., App.tsx or main.tsx) import '@object-ui/plugin-view'; +import type { ObjectViewSchema } from '@object-ui/types'; // Now you can use view types in your schemas -const schema = { +const schema: ObjectViewSchema = { type: 'object-view', - object: 'users', - viewMode: 'grid' + objectName: 'users', // required — the ObjectQL object name + defaultViewType: 'grid', }; ``` +The object name key is **`objectName`**, and it is the only required key besides +`type`. There is no `object`, `viewMode`, `fields`, `mode` or `recordId` key on +this node — see "Schema API" below for the keys `ObjectView` actually reads. + ### What the side-effect import registers Registration is *only* a side effect of importing the package — the single @@ -130,22 +135,85 @@ ComponentRegistry.register('my-switcher', ViewSwitcher, { ### ObjectView -Unified view component for ObjectQL objects: +Unified view component for ObjectQL objects. The keys below are the ones +`ObjectView` reads off the schema node (`src/ObjectView.tsx`); every example in +this README is typed with `ObjectViewSchema` from `@object-ui/types`, so a +missing `objectName` fails to compile. ```typescript -{ +import type { ObjectViewSchema } from '@object-ui/types'; + +const shape: ObjectViewSchema = { type: 'object-view', - object: string, // ObjectQL object name - viewMode?: 'grid' | 'form' | 'detail', - fields?: string[], // Fields to display - dataSource?: DataSource, - onCreate?: (data) => void, - onUpdate?: (id, data) => void, - onDelete?: (id) => void, - className?: string -} + objectName: 'users', // required — 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' } }, // named views; each needs a `label` + 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, + viewActions: [{ type: 'share' }], + + // --- Built-in CRUD toggles --- + operations: { create: true, read: true, update: true, delete: true }, +}; ``` +Three structural facts this component's schema does **not** work the way an +older version of this README claimed: + +- **`dataSource` is not a schema key.** It is a **required prop** of + `ObjectViewProps` (`src/ObjectView.tsx`). Pass it to `` directly, + or let the registered renderer pull it off `SchemaRendererProvider` context. + Putting `dataSource` inside the schema object does nothing. +- **There is no `viewMode`, and no per-record `mode` / `recordId`.** The list + type is `defaultViewType` (plus `listViews` / `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 to `layout`. Accordingly `ObjectViewSchema['form']` omits + `mode` — the component sets it. +- **There are no `onCreate` / `onUpdate` / `onDelete` / `onSubmit` callbacks.** + The component performs mutations itself through the `dataSource`. What you + can author is `operations` (booleans that enable or disable each built-in) + and `onNavigate(recordId, mode)`, which hands off to your router when the + record surface is a page. + +#### `table` and `form` sub-configuration + +`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. Anything else you put in them is ignored: + +| Sub-config | Keys `ObjectView` forwards | +| --- | --- | +| `table` | `columns`, `fields`, `title`, `description`, `defaultFilters`, `defaultSort`, `pageSize`, `selectable`, `operations`, `className` | +| `form` | `fields`, `customFields`, `sections`, `groups`, `layout`, `columns`, `title`, `description`, `subforms`, `buttons`, `defaults`, `initialValues`, `readOnly`, `showSubmit`, `submitText`, `showCancel`, `cancelText`, `showReset`, `className` | + +Note that several of the forwarded `table` keys are the ones `ObjectGridSchema` +marks legacy — `fields`, `pageSize`, `selectable`, `defaultFilters` and +`defaultSort` each have a newer counterpart there (`columns`, `pagination`, +`selection`, `filter`, `sort`). `ObjectView` forwards the legacy spellings, so +on an `object-view` node those are the ones that take effect; `columns` is the +exception, forwarded alongside `fields` and preferred here. Shapes follow +`ObjectGridSchema`: `defaultSort` is a single `{ field, order }` object and +`defaultFilters` is a plain `Record` of field to value. + ### ViewSwitcher Toggle between multiple view configurations: @@ -205,111 +273,165 @@ Configure sorting with dropdowns or buttons: ## Examples -### Grid View +### Choosing the list type -Display objects in a data grid: +The list is always rendered; `defaultViewType` picks which renderer draws it, +and `table` configures the grid: ```typescript -const schema = { +const schema: ObjectViewSchema = { type: 'object-view', - object: 'users', - viewMode: 'grid', - fields: ['name', 'email', 'role', 'created_at'], - dataSource: myDataSource + objectName: 'users', + defaultViewType: 'grid', + table: { + columns: ['name', 'email', 'role', 'created_at'], + defaultSort: { field: 'created_at', order: 'desc' }, + }, }; ``` -### Form View +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. -Create or edit objects with a form: +### Configuring the record form + +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`: ```typescript -const schema = { +const schema: ObjectViewSchema = { type: 'object-view', - object: 'users', - viewMode: 'form', - mode: 'create', - fields: ['name', 'email', 'role'], - onSubmit: (data) => { - console.log('Form submitted:', data); - } + objectName: 'users', + layout: 'drawer', // drawer | modal | page + form: { + fields: ['name', 'email', 'role'], + submitText: 'Save user', + showCancel: true, + }, }; ``` -### Detail View +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. + +### Opening a record -Display a single object's details: +Reading a record is the same surface in its read state, reached by clicking a +row. `navigation.mode` decides how, and `onNavigate` is what hands a page-mode +record off to your router: ```typescript -const schema = { +const schema: ObjectViewSchema = { type: 'object-view', - object: 'users', - viewMode: 'detail', - recordId: '123', - fields: ['name', 'email', 'role', 'bio', 'created_at'] + 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. `navigation: { mode: 'none' }` (or `preventNavigation`) +makes rows inert. + ## CRUD Operations +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 +`show*` controls whether the matching toolbar affordance is visible. + ### Create +`operations.create` enables record creation; `showCreate` shows the button. +Both default to on, and the new-record form opens on the `layout` surface: + ```typescript -const schema = { +const schema: ObjectViewSchema = { type: 'object-view', - object: 'products', - viewMode: 'form', - mode: 'create', - onCreate: async (data) => { - const newProduct = await dataSource.create('products', data); - console.log('Created:', newProduct); - } + objectName: 'products', + showCreate: true, + operations: { create: true }, + layout: 'drawer', + form: { fields: ['name', 'price', 'category'] }, }; ``` +With `layout: 'page'`, creation calls `onNavigate('new', 'edit')` instead of +opening a drawer, so the host route owns the form. + ### Read/List +Search, filter and sort are toolbar toggles; column set, default filter, +default sort and page size live in `table`: + +```typescript +const schema: ObjectViewSchema = { + type: 'object-view', + objectName: 'products', + defaultViewType: 'grid', + showSearch: true, + showFilters: true, + showSort: true, + table: { + columns: ['name', 'price', 'category'], + defaultFilters: { category: 'electronics' }, + pageSize: 25, + }, +}; +``` + +Saved views are `listViews`, keyed by view name, with `defaultListView` +selecting which opens first: + ```typescript -const schema = { +const schema: ObjectViewSchema = { type: 'object-view', - object: 'products', - viewMode: 'grid', - pagination: true, - searchable: true, - filters: { - category: 'electronics' - } + objectName: 'products', + listViews: { + all: { label: 'All Products', type: 'grid', columns: ['name', 'price'] }, + cheap: { + label: 'Under 100', + type: 'grid', + filter: [{ field: 'price', operator: 'lessThan', value: 100 }], + }, + }, + defaultListView: 'all', }; ``` ### Update +Editing is reached from a row's edit action; `operations.update` is what gates +it. The edited record is chosen by the click, never by an authored `recordId`: + ```typescript -const schema = { +const schema: ObjectViewSchema = { type: 'object-view', - object: 'products', - viewMode: 'form', - mode: 'edit', - recordId: '123', - onUpdate: async (id, data) => { - await dataSource.update('products', id, data); - console.log('Updated product:', id); - } + objectName: 'products', + operations: { update: true }, + layout: 'modal', + form: { fields: ['name', 'price'], submitText: 'Update' }, }; ``` +Under `layout: 'page'` this becomes `onNavigate(recordId, 'edit')`. + ### Delete +`operations.delete` enables both the per-row delete and bulk delete; there is +no `enableDelete` key and no `onDelete` callback: + ```typescript -const schema = { +const schema: ObjectViewSchema = { type: 'object-view', - object: 'products', - viewMode: 'grid', - enableDelete: true, - onDelete: async (id) => { - await dataSource.delete('products', id); - console.log('Deleted product:', id); - } + objectName: 'products', + operations: { create: true, read: true, update: true, delete: false }, }; ``` @@ -317,99 +439,123 @@ const schema = { The plugin works seamlessly with ObjectStack: +The adapter is the `dataSource` **prop**, not part of the schema: + ```typescript 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' + token: 'your-auth-token', }); -const schema = { +const schema: ObjectViewSchema = { type: 'object-view', - object: 'contacts', - viewMode: 'grid', - dataSource, - fields: ['first_name', 'last_name', 'email', 'company'], - searchable: true, - sortable: true, - pagination: { - pageSize: 25 - } + objectName: 'contacts', + defaultViewType: 'grid', + showSearch: true, + showSort: true, + table: { + columns: ['first_name', 'last_name', 'email', 'company'], + pageSize: 25, + }, }; + +; ``` +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. + ## Field Configuration -Customize field display and behavior: +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: ```typescript -const schema = { +const schema: ObjectViewSchema = { type: 'object-view', - object: 'users', - viewMode: 'form', - fieldConfig: { - name: { - label: 'Full Name', - required: true, - placeholder: 'Enter name' - }, - email: { - label: 'Email Address', - type: 'email', - required: true, - validation: [ - { type: 'email', message: 'Invalid email format' } - ] - }, - role: { - label: 'User Role', - type: 'select', - options: [ - { label: 'Admin', value: 'admin' }, - { label: 'User', value: 'user' }, - { label: 'Guest', value: 'guest' } - ] - } - } + 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. + ## Advanced Features -### Nested Objects +### Child records (master-detail) + +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: ```typescript -const schema = { +const schema: ObjectViewSchema = { type: 'object-view', - object: 'orders', - viewMode: 'detail', - fields: ['order_number', 'customer.name', 'items', 'total'], - nestedFields: { - items: { - type: 'object-grid', - object: 'order_items', - fields: ['product.name', 'quantity', 'price'] - } - } + objectName: 'orders', + layout: 'page', + form: { + fields: ['order_number', 'customer', 'total'], + subforms: [ + { + childObject: 'order_items', + title: 'Line items', + columns: ['product', 'quantity', 'price'], + }, + ], + }, }; ``` -### Tabs View +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`). + +### View tabs + +There is no `tabs` key, and `form.layout` has no tabbed value +(`vertical | horizontal | inline | grid`). The tab strip this package ships is +the **saved-view** tab bar: declare the views and render `` (or let +a host such as `@object-ui/app-shell` do it): ```typescript -const schema = { +const schema: ObjectViewSchema = { type: 'object-view', - object: 'users', - viewMode: 'tabs', - tabs: [ - { label: 'Details', fields: ['name', 'email', 'bio'] }, - { label: 'Settings', fields: ['theme', 'notifications', 'timezone'] }, - { label: 'Activity', type: 'object-grid', object: 'user_activities' } - ] + objectName: 'users', + showViewSwitcher: true, + allowCreateView: true, + listViews: { + active: { label: 'Active', type: 'grid', columns: ['name', 'email'] }, + admins: { + label: 'Admins', + type: 'grid', + filter: [{ field: 'role', operator: 'equals', value: 'admin' }], + }, + }, + defaultListView: 'active', }; ``` +To group a *form's* fields instead, use `form.sections` as shown under "Field +Configuration". + ## TypeScript Support This package's type export surface is the seven `*Props` types plus the