@object-ui/plugin-list 17.6.0 → 18.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -4,7 +4,8 @@ ListView plugin for ObjectUI - A unified view component with view type switching
4
4
 
5
5
  ## Features
6
6
 
7
- - **View Type Switching**: Switch between Grid, List, Kanban, Calendar, and Chart views
7
+ - **View Type Switching**: Switch between Grid, Kanban, Gallery, Calendar,
8
+ Timeline, Gantt, Map, Chart and Tree views
8
9
  - **View Persistence**: Automatically saves user's view preference
9
10
  - **Integrated Search**: Full-text search across records
10
11
  - **Filtering**: Advanced filter UI (expandable filter panel)
@@ -19,6 +20,15 @@ The toolbar and cell renderers are tuned for low visual noise on dense tables:
19
20
  - **Unified toolbar row**: view tabs (`schema.tabs`), user filters and tool
20
21
  buttons share a single bordered row. The previous stacked rows (`tabs` /
21
22
  `description` / `toolbar`) are collapsed into one separator line.
23
+ - **Four tool buttons by default**: the right side of the row is Filter, Sort,
24
+ Display and Search. **Display** holds the rest — a Layout row (the
25
+ visualization switcher, when the list offers more than one), Group, Row
26
+ color, Hide fields, Density, a session-only Row numbers switch and an Edit
27
+ inline switch, with Reset to view default, Refresh and Export in its footer.
28
+ Its trigger counts the settings that differ from the view's own. Each
29
+ control saves what its own toolbar button saves; the panel stores nothing
30
+ of its own. A view that sets `compactToolbar: false` gets one toolbar
31
+ button per control instead (objectui#12423).
22
32
  - **Flat user-filter pills**: `userFilters` (dropdown mode) render as ghost
23
33
  text + count. Active state is shown via `text-foreground font-medium`
24
34
  rather than a filled / bordered pill.
@@ -66,8 +76,12 @@ function ContactsView() {
66
76
  Group rows in grid/gallery views by one or more fields. Two equivalent shapes
67
77
  are supported on the schema:
68
78
 
79
+ Spec-compliant: a structured `GroupingConfig` (multi-level, with per-field
80
+ options).
81
+
69
82
  ```tsx
70
- // Spec-compliant: structured GroupingConfig (multi-level + per-field options)
83
+ import { ListView } from '@object-ui/plugin-list';
84
+
71
85
  <ListView
72
86
  schema={{
73
87
  type: 'list-view',
@@ -82,9 +96,15 @@ are supported on the schema:
82
96
  },
83
97
  }}
84
98
  />
99
+ ```
100
+
101
+ Shorthand: a single field name, the shape the visual view-config UI emits. It is
102
+ normalized internally into the `GroupingConfig` above — an alternative to the
103
+ block before it, never a second view rendered beside it.
104
+
105
+ ```tsx
106
+ import { ListView } from '@object-ui/plugin-list';
85
107
 
86
- // Shorthand: a single field name (used by the visual view-config UI).
87
- // Internally normalized into the GroupingConfig above.
88
108
  <ListView
89
109
  schema={{
90
110
  type: 'list-view',
@@ -97,39 +117,77 @@ are supported on the schema:
97
117
  ```
98
118
 
99
119
  When both are present, `grouping` wins. End users can also add or remove
100
- grouping fields at runtime via the Group toolbar button.
120
+ grouping fields at runtime via the Group toolbar button. Every such edit, in
121
+ the Display panel's Group section or in the wide toolbar's Group panel
122
+ (Clear included), fires `onGroupingChange` with the spec `GroupingConfig`, or
123
+ with `undefined` when the grouping is cleared. A changed `schema.grouping` that
124
+ the list re-reads does not fire it: that value came from the host
125
+ (objectui#11860).
126
+
127
+ A grouped **grid** is grouped on the server: over a data source that answers
128
+ the group header query (`dataSource.queryGroupHeaders`), `ListView` hands the
129
+ grid its own fetch, and the group set, every group count and each group's
130
+ rows come from the query (see *Grouping is server-side* in the
131
+ `@object-ui/plugin-grid` README). A toolbar search is handed to that grid too,
132
+ with the view's `searchableFields`: the grid puts the term on the group header
133
+ query and on every group's row query, so only the groups holding matches are
134
+ shown and each count is its matching rows (objectui#11021).
135
+
136
+ Over a data source that declares no `queryGroupHeaders`, `ListView` does not
137
+ fetch a window and group it — every count would be a page slice, and groups
138
+ past the window would be missing. It shows an error naming
139
+ `queryGroupHeaders` in place of the grid instead, fetches no rows, and keeps
140
+ the toolbar, so removing the grouping there lifts it (objectui#10881). Rows
141
+ handed in whole (`data` as an array, or `{ provider: 'value', items }`) are
142
+ still grouped in the browser, exactly.
101
143
 
102
144
  ### With Multiple View Types
103
145
 
146
+ Each view type's configuration is a block of that name at the top level of the
147
+ schema: `kanban`, `calendar`, `gallery`, `timeline`, `gantt`, `map`, `chart`,
148
+ `tree`. Each block is `@objectstack/spec`'s own list-view block, and an unknown
149
+ key in it is refused by name.
150
+
104
151
  ```tsx
152
+ import { ListView } from '@object-ui/plugin-list';
153
+
105
154
  <ListView
106
155
  schema={{
107
156
  type: 'list-view',
108
157
  objectName: 'deals',
109
158
  viewType: 'kanban',
110
159
  columns: ['name', 'amount', 'stage', 'close_date'],
111
- options: {
112
- kanban: {
113
- groupField: 'stage',
114
- titleField: 'name',
115
- },
116
- calendar: {
117
- startDateField: 'close_date',
118
- titleField: 'name',
119
- },
120
- chart: {
121
- chartType: 'bar',
122
- xAxisField: 'stage',
123
- yAxisFields: ['amount'],
124
- }
125
- }
160
+ kanban: {
161
+ groupByField: 'stage',
162
+ columns: ['name', 'amount'],
163
+ titleField: 'name',
164
+ },
165
+ calendar: {
166
+ startDateField: 'close_date',
167
+ titleField: 'name',
168
+ },
169
+ // A chart binds a semantic-layer dataset and selects its dimensions and
170
+ // measures by name (ADR-0021).
171
+ chart: {
172
+ chartType: 'bar',
173
+ dataset: 'deal_pipeline',
174
+ dimensions: ['stage'],
175
+ values: ['total_amount'],
176
+ },
126
177
  }}
127
178
  />
128
179
  ```
129
180
 
181
+ A stored view may also carry the same blocks in a legacy `options` bag
182
+ (`options.kanban`, …), which the platform's view write door judges key by key
183
+ with the same block schemas. `ListView` reads it under the top-level block, which
184
+ wins per key. Author the top-level blocks.
185
+
130
186
  ### With Callbacks
131
187
 
132
188
  ```tsx
189
+ import { ListView } from '@object-ui/plugin-list';
190
+
133
191
  <ListView
134
192
  schema={{
135
193
  type: 'list-view',
@@ -140,52 +198,290 @@ grouping fields at runtime via the Group toolbar button.
140
198
  onSearchChange={(search) => console.log('Search:', search)}
141
199
  onSortChange={(sort) => console.log('Sort:', sort)}
142
200
  onFilterChange={(filters) => console.log('Filters:', filters)}
201
+ onGroupingChange={(grouping) => console.log('Grouping:', grouping)}
202
+ />
203
+ ```
204
+
205
+ ### Host slot: `toolbarLeading`
206
+
207
+ A host can draw its own node at the start of the list toolbar row, before the
208
+ user-filter chips, with the `toolbarLeading` prop. The node sits in a
209
+ `min-w-0 flex-1` wrapper, so it gets the width the right-hand controls leave.
210
+ This is a React prop for the host app, not a schema key: list metadata cannot
211
+ set it. Without it the toolbar row is unchanged. The console uses it to put its
212
+ saved-view tabs on the same row as Filter, Sort, Display and Search.
213
+
214
+ ```tsx
215
+ import { ListView } from '@object-ui/plugin-list';
216
+
217
+ function MyViewTabs() {
218
+ return <nav aria-label="Views">…</nav>;
219
+ }
220
+
221
+ <ListView
222
+ schema={{ type: 'list-view', objectName: 'tasks', columns: ['title', 'status'] }}
223
+ toolbarLeading={<MyViewTabs />}
224
+ />
225
+ ```
226
+
227
+ ### Host prop: `viewDefaults`
228
+
229
+ A host that stores the user's own hidden fields or density and lays that
230
+ preference over `hiddenFields` / `rowHeight` in `schema` passes the view's own
231
+ values in `viewDefaults`. The Display control's trigger count and its "Reset to
232
+ view default" then compare these two settings against the view, not against
233
+ `schema`, and the reset writes the view's values back through the doors the
234
+ controls write through (`onHiddenFieldsChange`, `schema.onDensityChange`), so
235
+ the stored preference follows. This is a React prop for the host app, not a
236
+ schema key: list metadata cannot set it.
237
+
238
+ When it is passed, its members are the whole baseline: an absent `hiddenFields`
239
+ means the view hides nothing, and an absent `rowHeight` means the default
240
+ density. Without it nothing changes: `schema` is taken as the view's own. The
241
+ console passes it on the object list page, where it persists both settings per
242
+ user.
243
+
244
+ ```tsx
245
+ import { ListView } from '@object-ui/plugin-list';
246
+
247
+ // The view hides `amount`; the user has since hidden `stage` as well.
248
+ <ListView
249
+ schema={{ type: 'list-view', objectName: 'deals', hiddenFields: ['amount', 'stage'] }}
250
+ viewDefaults={{ hiddenFields: ['amount'] }}
251
+ onHiddenFieldsChange={(hidden) => console.log('Hidden:', hidden)}
143
252
  />
144
253
  ```
145
254
 
146
255
  ## Schema
147
256
 
148
- The ListView component accepts a `ListViewSchema`:
257
+ The ListView component accepts a `ListViewSchema`, exported from
258
+ `@object-ui/types`. That type is derived from the package's zod schema — which
259
+ itself derives from `@objectstack/spec` — so it cannot drift from the protocol.
260
+ This page therefore annotates an example *against* the shipped type instead of
261
+ restating it as a second hand-written interface: every key below is re-checked
262
+ on every commit, and a shape the type stops accepting fails here rather than
263
+ misleading a reader.
149
264
 
150
265
  ```typescript
151
- interface ListViewSchema {
152
- type: 'list-view';
153
- objectName: string;
154
- viewType?: 'grid' | 'kanban' | 'calendar' | 'gantt' | 'map' | 'chart';
155
- /** Spec-canonical column list. The legacy `fields` alias is still accepted
156
- * on input (stored view metadata carries it) and folded into `columns` by
157
- * `normalizeListViewSchema` — but nothing reads it, so emit `columns`. */
158
- columns?: string[] | ListColumn[];
159
- filters?: Array<any[] | string>;
160
- sort?: Array<{ field: string; order: 'asc' | 'desc' }>;
161
- options?: {
162
- grid?: Record<string, any>;
163
- list?: Record<string, any>;
164
- kanban?: {
165
- groupField: string;
166
- titleField?: string;
167
- cardFields?: string[];
168
- };
169
- calendar?: {
170
- startDateField: string;
171
- endDateField?: string;
172
- titleField: string;
173
- };
174
- chart?: {
175
- chartType: 'bar' | 'line' | 'pie' | 'area';
176
- xAxisField: string;
177
- yAxisFields: string[];
178
- };
179
- };
266
+ import type { ListViewSchema } from '@object-ui/types';
267
+
268
+ const view: ListViewSchema = {
269
+ type: 'list-view',
270
+ objectName: 'tasks',
271
+ viewType: 'grid',
272
+ // Spec-canonical column list. The legacy `fields` alias is still accepted on
273
+ // input (stored view metadata carries it) and folded into `columns` by
274
+ // `normalizeListViewSchema` — but nothing reads it, so emit `columns`.
275
+ columns: ['title', 'status', 'assignee'],
276
+ filters: [['status', '=', 'open']],
277
+ sort: [{ field: 'title', order: 'asc' }],
278
+ // One block per view type, at the top level. A grid has no block of its own:
279
+ // its settings are the top-level keys above.
280
+ kanban: { groupByField: 'status', columns: ['assignee'], titleField: 'title' },
281
+ calendar: { startDateField: 'due_date', titleField: 'title' },
282
+ chart: { chartType: 'bar', dataset: 'task_status', dimensions: ['status'], values: ['total_amount'] },
283
+ };
284
+
285
+ // `columns` also accepts ListColumn objects in place of the field-name strings.
286
+ const richColumns: ListViewSchema['columns'] = [
287
+ { field: 'title', label: 'Title', width: 200 },
288
+ ];
289
+
290
+ // The view-type vocabulary, written as a record keyed by the shipped union so
291
+ // this list cannot go stale: a value added to or removed from
292
+ // ListViewSchema['viewType'] fails this block.
293
+ const viewTypes: Record<NonNullable<ListViewSchema['viewType']>, string> = {
294
+ grid: 'Rows and columns',
295
+ kanban: 'Cards grouped into columns',
296
+ gallery: 'Card grid',
297
+ calendar: 'Records on a month / week calendar',
298
+ timeline: 'Records bucketed on a date axis',
299
+ gantt: 'Bars over a project timeline',
300
+ map: 'Records at their geographic coordinates',
301
+ chart: 'Aggregated bar / line / pie / area chart',
302
+ tree: 'Hierarchical parent-child rows',
303
+ };
304
+
305
+ export { view, richColumns, viewTypes };
306
+ ```
307
+
308
+ ## Empty state
309
+
310
+ When the `grid` or `gallery` visualization has no record to show, the view
311
+ draws its `emptyState` (the spec's `EmptyState`): a Lucide `icon`, a `title`, a
312
+ `message` and one `link: { label, url }` the user follows, such as the how-to
313
+ page that explains what belongs in the list (objectui#12270). A member left out
314
+ keeps the localized default copy; `title`, `message` and the link's `label`
315
+ each take a string or an inline locale map, resolved against the display
316
+ locale.
317
+
318
+ The link is followed as a `type: 'url'` action through the action runner, so it
319
+ opens what such an action opens: an in-app path (`/…` or `./…`) in place, an
320
+ `http(s)://` address as the runner opens an external target, and `mailto:` in
321
+ place. The runner refuses anything else at the click and reports it. The link
322
+ is drawn beside the add-record action when the view's `addRecord` block offers
323
+ one; the two are separate, and either, both or neither may be drawn.
324
+
325
+ ```typescript
326
+ import type { ListViewSchema } from '@object-ui/types';
327
+
328
+ const emptyTasks: ListViewSchema = {
329
+ type: 'list-view',
330
+ objectName: 'tasks',
331
+ viewType: 'grid',
332
+ columns: ['title'],
333
+ emptyState: {
334
+ title: 'No open tasks',
335
+ message: 'Tasks assigned to you appear here.',
336
+ icon: 'inbox',
337
+ link: { label: 'How tasks are assigned', url: '/docs/tasks' },
338
+ },
339
+ };
340
+
341
+ export { emptyTasks };
342
+ ```
343
+
344
+ A list page (`interfaceConfig` of a `type: 'list'` page) may declare its own
345
+ `emptyState` of the same shape. The page's own block wins, whole, over the one
346
+ its source view declares, on every visualization; the source view's is the
347
+ fallback when the page declares none.
348
+
349
+ ## Page size and fetch batch
350
+
351
+ A list view asks the server for one window of records. When
352
+ `pagination.pageSize` is not declared, what that window is depends on whether
353
+ the view pages:
354
+
355
+ - **The grid view pages on the server.** The window is one page, and the
356
+ grid's pager turns it. Its size is the default `@objectstack/spec` declares
357
+ for `pagination.pageSize`; the list reads it from the spec rather than
358
+ keeping a number of its own.
359
+ - **Every other view does not page** — kanban, gallery and the rest, and a
360
+ grouped grid. The window is one fetch batch of **100** records, and records
361
+ past it are not reachable; the record-count bar says so when the batch comes
362
+ back full. It is a fetch size, not a page size, so it does not follow the
363
+ spec's display default.
364
+ - **The calendar view fetches the days it shows.** With a start date bound
365
+ (`calendar.startDateField`), the window is the calendar's visible days on
366
+ that field (and on `calendar.endDateField` when one is bound, so a span that
367
+ runs into the month is fetched), plus the records with no start date, which
368
+ the calendar lists as unscheduled. The list walks that window in steps of the
369
+ same fetch batch of 100 until it is exhausted, so every record of the month
370
+ is drawn, and stops at the platform's non-grid ceiling of 2,000 records with
371
+ a note under the calendar naming both numbers. Moving to a month the fetched
372
+ window does not cover fetches that month. The calendar reports the days it
373
+ draws through its `onVisibleRangeChange` callback (objectui#12081).
374
+
375
+ A declared `pagination.pageSize` sizes the window on every view but the
376
+ calendar, as before. With no declared size, switching between the grid view and
377
+ another view changes the window, so the list fetches again.
378
+
379
+ ## On a page — `data` and `type`
380
+
381
+ A `list-view` node on a page is written in `@objectstack/spec`'s list view
382
+ spelling: `data` says where its records come from, and `type` says which kind of
383
+ list it is. On a metadata page both sit in the node's `properties`. The node's
384
+ own `type` stays `list-view`, because it is the key the page renderer looks the
385
+ component up by:
386
+
387
+ ```json
388
+ {
389
+ "type": "list-view",
390
+ "properties": {
391
+ "data": { "provider": "object", "object": "deal" },
392
+ "columns": ["name", "amount"],
393
+ "kanban": { "groupByField": "stage" }
394
+ }
395
+ }
396
+ ```
397
+
398
+ That node draws a grid of `deal` records: without `properties.type` the list is
399
+ a grid, the spec's default kind. Add `"type": "kanban"` to the same `properties`
400
+ and it draws a kanban over `deal`, grouped by `stage`. A `type` that names no
401
+ kind this list draws is a grid too. `data` may also hand the list its rows
402
+ (`{ "provider": "value", "items": [...] }`).
403
+
404
+ A page node's own `objectName` and `viewType` are **not read**. The page
405
+ renderer removes them before the list sees the node, and logs one warning per key
406
+ naming what to write instead. A node that names its object only as `objectName`
407
+ therefore names no object, and the list draws a hint in its place: "No object
408
+ named: set data.object or dataSource.object." Converting page nodes stored in
409
+ the old spelling is the platform's job (an ADR-0087 conversion in
410
+ `@objectstack/spec`); this renderer does not read them back. `ListView`'s own
411
+ React props (the `ListViewSchema` examples above) are a separate surface and
412
+ still spell the two `objectName` and `viewType`.
413
+
414
+ `objectui validate` judges a page `list-view` node in this spelling. Its record
415
+ source is `properties.data` or a bound `dataSource.object`, and a node with
416
+ neither is refused at `properties.data`. `objectName` and `viewType` are refused
417
+ by name, on the node or in `properties`, each naming what to write instead
418
+ (`properties.data`, `properties.type`). A `properties.type` that names no kind a
419
+ list view draws is refused, and so is a list view prop such as `columns` written
420
+ on the node itself rather than in `properties`.
421
+
422
+ ## On a page — `requiredPermissions`
423
+
424
+ One more key of the spec's list view row acts on a page `list-view` node. It
425
+ sits in `properties`:
426
+
427
+ ```json
428
+ {
429
+ "type": "list-view",
430
+ "properties": {
431
+ "data": { "provider": "object", "object": "deal" },
432
+ "columns": ["name", "amount"],
433
+ "requiredPermissions": ["sales.view_pipeline"]
434
+ }
435
+ }
436
+ ```
437
+
438
+ **`requiredPermissions`** names the capabilities the viewer must **all** hold,
439
+ the names permission sets grant through `systemPermissions`. It gates the node
440
+ the way it gates the record blocks (`record:details`, `record:highlights`,
441
+ `record:related_list`). A viewer missing one sees the notice "Insufficient
442
+ permissions to view list." (`role="status"`) in place of the list, and no
443
+ records are queried. An empty list, or no key, is no gate. A permission
444
+ provider that reports no capabilities at all leaves the list shown; the server
445
+ stays the authority. Only the page node reads this key: the object list page
446
+ draws `ListView` itself, and the server gates the views it serves.
447
+
448
+ `addRecord` draws an Add button. `position` places it: `"top"` (the toolbar),
449
+ `"bottom"` or `"both"`. `mode` and `formView` are not read.
450
+
451
+ The spec row declares `enabled` default `true` and `position` default
452
+ `"bottom"`. The renderer does not apply those defaults: it draws the button only
453
+ for an explicit `enabled: true`, and places an absent `position` at the top.
454
+ objectui#13099 owns that gap; until it lands, write both keys.
455
+
456
+ ```json
457
+ {
458
+ "type": "list-view",
459
+ "properties": {
460
+ "data": { "provider": "object", "object": "deal" },
461
+ "addRecord": { "enabled": true, "position": "top" }
462
+ }
180
463
  }
181
464
  ```
182
465
 
466
+ On a page node the click calls the `onCreate` a `RelatedRecordActionsContext`
467
+ provider (`@object-ui/react`) serves for the listed object, resolved with
468
+ `{ objectName }` and no `relationshipField`. The console's page host serves the
469
+ create the object list's New opens: the record-form overlay, or the full-page
470
+ create route for an object whose `editMode` is `page`. With no such provider,
471
+ for a viewer who may not create, or for a create the object greys, the button
472
+ is not drawn, rather than drawn with no action. A host that passes `ListView`'s
473
+ own `onAddRecord` prop keeps it; the console's object list passes its header
474
+ New's create.
475
+
476
+ `sharing` does nothing on a page node yet: it draws a Share button with no
477
+ action, because the spec declares none for it. It is not a published input.
478
+
183
479
  ## Page binding — `dataSource` (referencing a saved view by name)
184
480
 
185
- On a metadata page, a `list-view` component can bind its data through the spec's
186
- per-element data source (`PageComponentSchema.dataSource`,
187
- `ElementDataSourceSchema`) instead of spelling `objectName` and inlining the
188
- view's configuration:
481
+ On a metadata page, a `list-view` component can also bind its data through the
482
+ spec's per-element data source (`PageComponentSchema.dataSource`,
483
+ `ElementDataSourceSchema`) instead of naming its object in `data` and inlining
484
+ the view's configuration. A bound `dataSource.object` outranks `data`:
189
485
 
190
486
  ```json
191
487
  {
@@ -205,7 +501,8 @@ copy of a view's configuration in sync with the view itself. Both the short key
205
501
  this placement, and they beat the component's own same-named key. Values that
206
502
  come from the named view are a *baseline*: a key written on the component itself
207
503
  is more specific than the view it points at, so the component's key wins
208
- (an empty `columns: []` counts as "not authored"). `filter` is the exception —
504
+ (an empty `columns: []` counts as "not authored"), and a kind written as
505
+ `properties.type` beats the view's kind. `filter` is the exception —
209
506
  the spec calls `dataSource.filter` "additional filter criteria", so the
210
507
  component's filter, the view's filter and the binding's filter all AND together.
211
508
  A binding can narrow what the view selects, never widen it.
@@ -216,6 +513,48 @@ actual views, and issues no query. It deliberately does not fall back to the
216
513
  object's default view: that would turn a typo into a silently *wider* answer on a
217
514
  page that still looks like it works.
218
515
 
516
+ ## Compact posture in a narrow page region — `compact`
517
+
518
+ `ListViewProps` has an optional host prop, `compact`. It is a React prop, not a
519
+ schema key: nothing in `@objectstack/spec` declares a posture, and an author
520
+ never writes one. The `list-view` page block sets it. It reads
521
+ `usePageRegionPosture()` from `@object-ui/components` and passes
522
+ `compact` when the answer is `'compact'`, which means the page region the block
523
+ sits in is narrower than half the page (an `aside` or `sidebar` rail beside
524
+ `main`).
525
+
526
+ With `compact`, the list draws:
527
+
528
+ - a header with the list's title (its `label`, else the object's label) and
529
+ the record count;
530
+ - one row per record: the view's first visible column, with the second visible
531
+ column muted on a line below; a click on a row opens the record the way a grid
532
+ row does.
533
+
534
+ It does not draw the toolbar, the view tabs, the `#` row-number column, the
535
+ pager or the record-count bar.
536
+
537
+ The prop applies only to a paged grid, the view whose rows `ListView` fetches
538
+ a page of. A board, a calendar or a grouped grid keeps its own rendering. When
539
+ `compact` is absent (wide regions, unknown widths, the object list page and
540
+ every other host), the list renders exactly as before.
541
+
542
+ ### "View all"
543
+
544
+ Under its rows the compact list draws a "View all" link (`list.viewAll`) to the
545
+ rest of the list. The host builds the destination: the `list-view` block asks
546
+ the host's `listHref` / `openList` on `RelatedRecordActionsContext`
547
+ (`@object-ui/react`) for the object it reads, naming the saved view the block
548
+ binds with `dataSource.view` when it binds one. Without a bound view the link
549
+ goes to the object's own list. The block's own `filter`, `columns` and `sort`
550
+ are never handed over, so they never reach the URL; the header count stays the
551
+ answer to what the block shows.
552
+
553
+ The link is a real anchor: a plain left click navigates in the app through
554
+ `openList`, and a modifier click (a new tab) follows the href. A host that
555
+ publishes no `listHref`, or answers `null`, gets no link. An empty list draws
556
+ no link either.
557
+
219
558
  ## Sorting (and why relational columns are not offered)
220
559
 
221
560
  The toolbar sort becomes a server `$orderby` on the **flat field name**, so the
@@ -251,11 +590,10 @@ the loaded rows. So on that path a relational or formula column carries no
251
590
  clickable header either (`objectui#3950`); offering one would have been the same
252
591
  illusion through a different control.
253
592
 
254
- Where the sort really does stay in the browser — inline `data`, or the grouped
255
- view, which holds every row it groups — both kinds of header stay live and order
256
- by the value the cell shows: the resolved label for a relational column, the
257
- server-hydrated result for a formula one (see `getSortValue` in
258
- `@object-ui/core`).
593
+ Where the sort really does stay in the browser — inline `data` — both kinds of
594
+ header stay live and order by the value the cell shows: the resolved label for
595
+ a relational column, the server-hydrated result for a formula one (see
596
+ `getSortValue` in `@object-ui/core`).
259
597
 
260
598
  ## View Persistence
261
599
 
@@ -0,0 +1,72 @@
1
+ import { LucideIcon } from 'lucide-react';
2
+ import { ListViewSchema } from '@object-ui/types';
3
+ /**
4
+ * ObjectUI
5
+ * Copyright (c) 2024-present ObjectStack Inc.
6
+ *
7
+ * This source code is licensed under the MIT license found in the
8
+ * LICENSE file in the root directory of this source tree.
9
+ */
10
+ /**
11
+ * The empty state a list draws when it has no records (objectui#12217).
12
+ *
13
+ * ONE component, drawn by the two surfaces that show a list of records as
14
+ * rows or cards: `ListView` for its `grid` and `gallery` visualizations, and
15
+ * `ObjectGallery` when it is rendered without a `ListView` around it (an
16
+ * authored `object-gallery` node, `ObjectView`'s own non-grid path). It used to
17
+ * live inline in `ListView`'s grid branch, and `ObjectGallery` drew its own
18
+ * hard-coded English line instead, ignoring both the view's authored
19
+ * `emptyState` and the locale.
20
+ *
21
+ * Module-internal: not exported from the package barrel. The copy is the
22
+ * `list.*` keys the grid already read; ⛔ no second string table. Each caller
23
+ * hands over the translation function and display locale it already holds, the
24
+ * authored members it has (an authored icon NAME already resolved through the
25
+ * icon seam), and what it knows about the query that came back empty; a caller
26
+ * that has no search box or user filters (a standalone gallery) says `false`.
27
+ */
28
+ import * as React from 'react';
29
+ /**
30
+ * Whether a DECLARED filter (the view's own `filter`, or a standalone node's)
31
+ * narrows the query at all: a non-empty condition array or a non-empty
32
+ * filter-node object. An absent filter, `[]` and `{}` narrow nothing.
33
+ */
34
+ export declare function hasDeclaredFilter(filter: unknown): boolean;
35
+ /** The authored `emptyState` (the spec's `EmptyStateSchema`). */
36
+ type AuthoredEmptyState = NonNullable<ListViewSchema['emptyState']>;
37
+ export interface ListEmptyStateProps {
38
+ /** The authored `emptyState.title` (`I18nLabel`), resolved here against `displayLocale`. */
39
+ title?: AuthoredEmptyState['title'];
40
+ /** The authored `emptyState.message` (`I18nLabel`), resolved here against `displayLocale`. */
41
+ message?: AuthoredEmptyState['message'];
42
+ /**
43
+ * The authored `emptyState.icon`, already resolved by the caller through the
44
+ * ONE icon seam (`resolveIcon`, objectui#5935); `null` or absent draws the
45
+ * `Inbox` glyph.
46
+ */
47
+ icon?: LucideIcon | null;
48
+ /**
49
+ * The authored `emptyState.link`, `{ label, url }` (objectui#12270), drawn by
50
+ * the one shared `EmptyStateLink` beside the add-record action. The spec
51
+ * keeps the two apart: the link is the empty state's own next step, the
52
+ * add-record entry point is the view's `addRecord` block, so either, both or
53
+ * neither may be drawn.
54
+ */
55
+ link?: AuthoredEmptyState['link'];
56
+ /**
57
+ * The USER applied a query: the search box, the user-filter chips, the
58
+ * filter panel. Only then is the user told to adjust or clear their filters.
59
+ */
60
+ hasUserQuery: boolean;
61
+ /** The declared filter narrows the query (see {@link hasDeclaredFilter}). */
62
+ hasBaseFilter: boolean;
63
+ /** Draws the add-record action when supplied; omitted, no action is drawn. */
64
+ onAddRecord?: () => void;
65
+ /** The caller's `list.*` translation function. */
66
+ t: (key: string) => string;
67
+ /** The locale an authored `I18nLabel` map is resolved against. */
68
+ displayLocale: string;
69
+ }
70
+ export declare function ListEmptyState({ title: authoredTitle, message: authoredMessage, icon, link, hasUserQuery, hasBaseFilter, onAddRecord, t, displayLocale, }: ListEmptyStateProps): React.JSX.Element;
71
+ export {};
72
+ //# sourceMappingURL=ListEmptyState.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ListEmptyState.d.ts","sourceRoot":"","sources":["../src/ListEmptyState.tsx"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAE/B,OAAO,EAAe,KAAK,UAAU,EAAE,MAAM,cAAc,CAAC;AAC5D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAKvD;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,OAAO,GAAG,OAAO,CAI1D;AAED,iEAAiE;AACjE,KAAK,kBAAkB,GAAG,WAAW,CAAC,cAAc,CAAC,YAAY,CAAC,CAAC,CAAC;AAEpE,MAAM,WAAW,mBAAmB;IAClC,4FAA4F;IAC5F,KAAK,CAAC,EAAE,kBAAkB,CAAC,OAAO,CAAC,CAAC;IACpC,8FAA8F;IAC9F,OAAO,CAAC,EAAE,kBAAkB,CAAC,SAAS,CAAC,CAAC;IACxC;;;;OAIG;IACH,IAAI,CAAC,EAAE,UAAU,GAAG,IAAI,CAAC;IACzB;;;;;;OAMG;IACH,IAAI,CAAC,EAAE,kBAAkB,CAAC,MAAM,CAAC,CAAC;IAClC;;;OAGG;IACH,YAAY,EAAE,OAAO,CAAC;IACtB,6EAA6E;IAC7E,aAAa,EAAE,OAAO,CAAC;IACvB,8EAA8E;IAC9E,WAAW,CAAC,EAAE,MAAM,IAAI,CAAC;IACzB,kDAAkD;IAClD,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,MAAM,CAAC;IAC3B,kEAAkE;IAClE,aAAa,EAAE,MAAM,CAAC;CACvB;AAED,wBAAgB,cAAc,CAAC,EAC7B,KAAK,EAAE,aAAa,EACpB,OAAO,EAAE,eAAe,EACxB,IAAI,EACJ,IAAI,EACJ,YAAY,EACZ,aAAa,EACb,WAAW,EACX,CAAC,EACD,aAAa,GACd,EAAE,mBAAmB,qBAgErB"}