@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/CHANGELOG.md +5149 -0
- package/README.md +398 -60
- package/dist/ListEmptyState.d.ts +72 -0
- package/dist/ListEmptyState.d.ts.map +1 -0
- package/dist/ListView.d.ts +220 -29
- package/dist/ListView.d.ts.map +1 -1
- package/dist/ListViewBlock.d.ts +1 -1
- package/dist/ListViewBlock.d.ts.map +1 -1
- package/dist/ListViewCompact.d.ts +77 -0
- package/dist/ListViewCompact.d.ts.map +1 -0
- package/dist/ObjectGallery.d.ts +35 -23
- package/dist/ObjectGallery.d.ts.map +1 -1
- package/dist/UserFilters.d.ts +14 -1
- package/dist/UserFilters.d.ts.map +1 -1
- package/dist/ViewSwitcher.d.ts +37 -7
- package/dist/ViewSwitcher.d.ts.map +1 -1
- package/dist/calendarWindow.d.ts +102 -0
- package/dist/calendarWindow.d.ts.map +1 -0
- package/dist/components/TabBar.d.ts.map +1 -1
- package/dist/components/ViewSettingsPopover.d.ts +53 -16
- package/dist/components/ViewSettingsPopover.d.ts.map +1 -1
- package/dist/index.d.ts +14 -14
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2491 -1529
- package/dist/index.umd.cjs +2 -2
- package/package.json +23 -22
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,
|
|
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
|
-
|
|
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
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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
|
|
186
|
-
per-element data source (`PageComponentSchema.dataSource`,
|
|
187
|
-
`ElementDataSourceSchema`) instead of
|
|
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")
|
|
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
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
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"}
|