@object-ui/plugin-list 17.3.0 → 17.6.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 +944 -0
- package/README.md +65 -19
- package/dist/ListView.d.ts +138 -6
- package/dist/ListView.d.ts.map +1 -1
- package/dist/ListViewBlock.d.ts +69 -0
- package/dist/ListViewBlock.d.ts.map +1 -0
- package/dist/UserFilters.d.ts.map +1 -1
- package/dist/ViewSwitcher.d.ts.map +1 -1
- package/dist/components/TabBar.d.ts.map +1 -1
- package/dist/components/ViewSettingsPopover.d.ts.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1437 -1230
- package/dist/index.umd.cjs +3 -3
- package/package.json +22 -22
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,943 @@
|
|
|
1
1
|
# @object-ui/plugin-list
|
|
2
2
|
|
|
3
|
+
## 17.6.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 0a73b51: `ObjectView` and `ListView` now flatten a view's `map` block through a
|
|
8
|
+
whitelist instead of spreading the whole (untyped) block to the top level.
|
|
9
|
+
|
|
10
|
+
Both `case 'map'` flatteners used to build the `object-map` schema with
|
|
11
|
+
`...(options.map || {})` — a raw spread of an untyped bag
|
|
12
|
+
(`NamedListView.options?: Record<string, any>`), so any key an author wrote in
|
|
13
|
+
the `map` block reached the top level unfiltered. `ObjectMap`'s own
|
|
14
|
+
`FlatMapConfigKeys = Omit<ObjectMapConfig, 'style'>` declares `style` OUT of
|
|
15
|
+
this flat form (`style` is also `BaseSchema.style`, inline CSS legal on every
|
|
16
|
+
node), so the two disagreed about the same shape. `style` was the live
|
|
17
|
+
specimen: `map: { style: '<url>' }` reached the top level as a CSS-shaped
|
|
18
|
+
`style` key it was never supposed to carry.
|
|
19
|
+
|
|
20
|
+
Behavior narrowing, stated because it changes what reaches the flattened
|
|
21
|
+
schema: a `map` block key that is not one of `ObjectMapConfig`'s declared
|
|
22
|
+
flat keys (`latitudeField` / `longitudeField` / `locationField` / `titleField`
|
|
23
|
+
/ `descriptionField` / `zoom` / `center`) — including `style` — no longer
|
|
24
|
+
reaches the top level of the flattened `object-map` schema. This closes a gap
|
|
25
|
+
rather than removing working behavior: the pinned strict spec view schemas
|
|
26
|
+
accept no `map` block at all today, so no author-facing surface could reach
|
|
27
|
+
this path, and `ObjectMap` already stopped reading a top-level `style` as a
|
|
28
|
+
map style (a dev warning names the correct spelling instead).
|
|
29
|
+
|
|
30
|
+
The whitelist is DERIVED from `ObjectMapConfigSchema` (`@object-ui/types/zod`)
|
|
31
|
+
rather than hand-listed, so the flatteners and the declaration cannot drift
|
|
32
|
+
apart again — a key added to (or removed from) the schema reaches both
|
|
33
|
+
flatteners without a second edit. `ObjectMap`'s own `FLAT_MAP_CONFIG_KEYS` is
|
|
34
|
+
derived from the same schema for the same reason.
|
|
35
|
+
- f1d4748: Remove the retired `striped` / `bordered` / `virtualScroll` list-view surface
|
|
36
|
+
|
|
37
|
+
objectstack#7176 retired `list.striped`, `list.bordered` and `list.virtualScroll`
|
|
38
|
+
from the spec after measuring every objectui reader as pass-through: each one
|
|
39
|
+
copied the key onward and no renderer ever applied it. objectui stops declaring,
|
|
40
|
+
typing and forwarding them.
|
|
41
|
+
|
|
42
|
+
Off the chain: the `@objectstack/spec` list-view bridge in `@object-ui/react`,
|
|
43
|
+
`ListView`'s child-view props in `@object-ui/plugin-list`, both `ObjectView`
|
|
44
|
+
relays (`@object-ui/plugin-view` and `@object-ui/app-shell`), the `ObjectGridSchema`
|
|
45
|
+
and `NamedListView` declarations in `@object-ui/types` (interface and zod),
|
|
46
|
+
`ObjectGrid.component.yml` in `@object-ui/components`, and the page-block
|
|
47
|
+
inspector's `striped` / `bordered` toggles in the metadata-admin designer.
|
|
48
|
+
|
|
49
|
+
Behaviour is unchanged: nothing read these keys, so nothing rendered differently
|
|
50
|
+
for them. Stored view metadata that still carries one keeps validating — the keys
|
|
51
|
+
are simply no longer relayed. `ListViewSchema` continues to take the spec's
|
|
52
|
+
list-view fields by reference, so the protocol's own retirement tombstones
|
|
53
|
+
arrive with the next `@objectstack/spec` bump and reject the keys at the
|
|
54
|
+
authoring boundary. Restoring any of the three as live surface requires an
|
|
55
|
+
implementation card filed first, per the ruling.
|
|
56
|
+
- b1119ec: Project every declared `recordIdField`, and refuse an action that names no record
|
|
57
|
+
|
|
58
|
+
objectstack#8018. An `api` action declaring `recordIdParam` identifies the record
|
|
59
|
+
it acts on by a row field — `recordIdField`, default `id`. The grid built
|
|
60
|
+
`$select` from the listView columns, `id` and the predicate refs only, so an
|
|
61
|
+
action keyed on any other field asked the server for everything except the key
|
|
62
|
+
naming its own record. The row arrived without it, and the injection was skipped
|
|
63
|
+
silently: the request went out anyway, minus the parameter. A backend reading a
|
|
64
|
+
missing selector as "match nothing" then answers success for having changed
|
|
65
|
+
nothing, so a record-scoped mutation reports success and does nothing.
|
|
66
|
+
|
|
67
|
+
Two independent repairs, both in this change:
|
|
68
|
+
|
|
69
|
+
- **Projection.** `listViewPredicates` (`@object-ui/core`) now also harvests
|
|
70
|
+
`recordIdField` from `rowActionDefs`, `bulkActionDefs` and the object's
|
|
71
|
+
`actions`, spelled as a synthetic `record.<name>` so the one existing harvester
|
|
72
|
+
handles it. Both projection builders — `ObjectGrid` and `ListView` — read that
|
|
73
|
+
function, so both gain the key with no call-site change. The existing guards
|
|
74
|
+
still apply: a name the object does not declare, or one that is not a bare
|
|
75
|
+
identifier, is dropped rather than put in `$select`, because an unknown key
|
|
76
|
+
there is not ignored by every backend.
|
|
77
|
+
- **Loud failure.** New `resolveRecordIdParamSeed` (`@object-ui/core`) is the one
|
|
78
|
+
definition of "can this row identify the record?". `useConsoleActionRuntime`'s
|
|
79
|
+
api handler now refuses the dispatch — `{ success: false, error }`, before the
|
|
80
|
+
request — when the row lacks the key, or holds `null` for it. The two refusals
|
|
81
|
+
are worded differently because they point at different repairs: an absent key
|
|
82
|
+
is a projection or read-visibility problem, a null value is a data one. Falsy
|
|
83
|
+
real values (`0`, `''`, `false`) are values and still dispatch.
|
|
84
|
+
|
|
85
|
+
The second half is what closes the class rather than the common case: a row can
|
|
86
|
+
lack the key for reasons projection cannot fix — a server-side read mask that
|
|
87
|
+
strips the field regardless of `$select`, a partial payload, a field the
|
|
88
|
+
principal cannot read.
|
|
89
|
+
|
|
90
|
+
Behaviour change worth noting: an action that previously dispatched an
|
|
91
|
+
under-specified request now fails visibly instead. That is the point — the old
|
|
92
|
+
path could not report the failure it was causing.
|
|
93
|
+
|
|
94
|
+
### Patch Changes
|
|
95
|
+
|
|
96
|
+
- 516663d: RecordAttachmentsPanel no longer offers a Retry for an api-disabled `sys_attachment` read.
|
|
97
|
+
|
|
98
|
+
`OBJECT_API_DISABLED` (404, `enable.apiEnabled: false`) and its sibling
|
|
99
|
+
`OBJECT_API_METHOD_NOT_ALLOWED` (405, the operation is absent from
|
|
100
|
+
`enable.apiMethods`) are pure functions of the object's metadata — no user, no
|
|
101
|
+
session, no request body — so every retry of every persona re-fetches the
|
|
102
|
+
identical refusal. Before this change both landed in `RecordAttachmentsPanel`'s
|
|
103
|
+
`unavailable` state and offered a Retry that was guaranteed to change nothing,
|
|
104
|
+
the same wrong advice `ListView`'s error panel already stops giving for list
|
|
105
|
+
reads.
|
|
106
|
+
|
|
107
|
+
The panel gains a fifth status, `api-unavailable`: no Retry button, and honest
|
|
108
|
+
copy ("The attachments list is not available on this object.", new
|
|
109
|
+
`detail.attachmentsApiUnavailable` key in all ten locale packs) instead of
|
|
110
|
+
"We couldn't load the attachments for this record." The pre-existing `denied`
|
|
111
|
+
(authorization) and `unavailable` (network/5xx/expired-session) states and
|
|
112
|
+
their affordances are unchanged.
|
|
113
|
+
|
|
114
|
+
`ListView.classifyLoadError` — the classifier that already separated this case
|
|
115
|
+
into its own `api-disabled` kind for list views — is lifted out of
|
|
116
|
+
`packages/plugin-list/src/ListView.tsx`'s module scope into
|
|
117
|
+
`@object-ui/react` (`classifyLoadError`, `LoadErrorKind`), so both surfaces
|
|
118
|
+
consume one classification instead of `RecordAttachmentsPanel` re-deriving it.
|
|
119
|
+
`ListView`'s own behavior is unchanged — it now imports the function it
|
|
120
|
+
previously defined locally. The classifier delegates its api-disabled check to
|
|
121
|
+
`isApiAccessDeniedError` (`@object-ui/data-objectstack`), removing a second,
|
|
122
|
+
independently-maintained copy of the same code list.
|
|
123
|
+
- a1609a6: Console list filters: a `between` range is submitted only when both bounds are filled, and six operator labels stop rendering as raw i18n keys.
|
|
124
|
+
|
|
125
|
+
Two defects in the list-view filter panel (objectstack#8815), both in the Console
|
|
126
|
+
render layer, with no workaround available downstream.
|
|
127
|
+
|
|
128
|
+
**A half-filled range no longer refuses the whole view.** Picking a date column
|
|
129
|
+
and 「介于」 draws two inputs — that part landed in objectui#3958 — but typing
|
|
130
|
+
only one bound produced `["2024-01-01", ""]`, and both write paths read "is this
|
|
131
|
+
row filled in?" with one shape-blind predicate (`null` / `''` / empty array).
|
|
132
|
+
An array of length 2 passed it, so the empty bound went to the server, which
|
|
133
|
+
refuses the query outright (`400 INVALID_FILTER`): the list showed
|
|
134
|
+
「该视图的查询被拒绝」 and the filters the user had already applied stopped
|
|
135
|
+
applying too. The saved-view fold persisted the same half-range, so the refusal
|
|
136
|
+
came back on every later read of that view, for every user of it.
|
|
137
|
+
|
|
138
|
+
The spec cannot intercept this — `ViewFilterRuleSchema` accepts
|
|
139
|
+
`["2024-01-01", ""]` because it counts the two slots rather than what is in
|
|
140
|
+
them, while refusing a scalar or a one-element array. Authoring validation is
|
|
141
|
+
therefore green on exactly the shape that fails at query time, which makes not
|
|
142
|
+
emitting it the producer's job. `@object-ui/components` now exports
|
|
143
|
+
`isFilterValueComplete(operator, value)` — arity-aware, so a `pair` row needs
|
|
144
|
+
both bounds — and the two consumers that had each kept a copy of the old
|
|
145
|
+
predicate (`plugin-list`'s `convertFilterGroupToAST`, `app-shell`'s
|
|
146
|
+
`foldFilterGroupToSpecRules`) read it instead. A half-filled range is now
|
|
147
|
+
dropped exactly as a half-typed `equals` row already was: no filter, rather than
|
|
148
|
+
a filter the server will reject. Bounds of `0` and `false` stay real bounds.
|
|
149
|
+
|
|
150
|
+
**Six operator labels are translated in all ten locale packs.**
|
|
151
|
+
`startsWith`, `endsWith`, `isNull`, `isNotNull`, `exists` and `notExists` were
|
|
152
|
+
missing from every pack, so i18next resolved them to the raw key and the dropdown
|
|
153
|
+
showed `filterBuilder.operators.isNull` beside translated entries. The
|
|
154
|
+
component's own defaults table could not cover it: that table serves only the
|
|
155
|
+
no-provider path, and the Console mounts a provider. The report named four —
|
|
156
|
+
a `date` column's bucket offers the four nullness operators; a `text` column
|
|
157
|
+
showed all six.
|
|
158
|
+
|
|
159
|
+
Because the label key is built dynamically (`t(\`filterBuilder.operators.${op}\`)`),
|
|
160
|
+
no existing gate could see the gap: the call-site checker classifies a template
|
|
161
|
+
key as `missing-prefix` and only asks whether the prefix resolves, and
|
|
162
|
+
cross-pack parity is satisfied when all ten packs are missing a key together.
|
|
163
|
+
A new parity test pins the packs against `FILTER_BUILDER_OPERATORS` in both
|
|
164
|
+
directions, so an operator added to the dropdown now fails loudly until every
|
|
165
|
+
pack labels it.
|
|
166
|
+
- c1ef923: Grid and related-list column headers no longer offer a sort on a `formula` column.
|
|
167
|
+
|
|
168
|
+
A `formula` value is computed on read: no driver materialises a column for it, so
|
|
169
|
+
a server `$orderby` naming one has nothing to order by. That sort never worked.
|
|
170
|
+
Until objectstack#6994 the platform did not say so — the response carried the very
|
|
171
|
+
values it had been asked to order by, out of order, under a `200`, with ascending
|
|
172
|
+
and descending byte-identical on a real SQL driver — and it now answers
|
|
173
|
+
`400 INVALID_SORT`. So the header was wrong before the platform's refusal and is
|
|
174
|
+
wrong after it, for the same reason: it offers a sort that cannot be performed.
|
|
175
|
+
|
|
176
|
+
`ObjectGrid` withheld the affordance only from reference-bearing columns
|
|
177
|
+
(objectui#3096). Unmaterialized types are a SECOND reason a server sort is
|
|
178
|
+
impossible, not a different mechanism, so it now reads both — and so do the two
|
|
179
|
+
sort entry points of a related list (the embedded table's headers and the
|
|
180
|
+
sort-button row a `data-list` card keeps), which each derived that rule
|
|
181
|
+
separately.
|
|
182
|
+
|
|
183
|
+
Client-side sorting is deliberately unchanged. There the rows are all in the
|
|
184
|
+
browser and the formula value is the one the server hydrated on read, so ordering
|
|
185
|
+
by what the cell shows is honest — the same split the relational carve-out makes.
|
|
186
|
+
A sort DECLARED in view metadata is also unchanged: it still goes out and is still
|
|
187
|
+
refused by name, because silently dropping an author's declaration would hide the
|
|
188
|
+
authoring error instead of surfacing it (the toolbar's sort picker keeps such a
|
|
189
|
+
field listed for exactly that reason — it is the only way to remove it).
|
|
190
|
+
|
|
191
|
+
The membership — `formula` alone — moved out of a private set in `ListView` into
|
|
192
|
+
`@object-ui/core` (`UNMATERIALIZED_FIELD_TYPES` / `isUnmaterializedFieldType`),
|
|
193
|
+
bound to `@objectstack/spec`'s own storage predicate so the renderer cannot drift
|
|
194
|
+
from what the drivers actually store. It is deliberately narrower than the spec's
|
|
195
|
+
write contract `COMPUTED_VALUE_TYPES`: a `summary` and an `autonumber` each get a
|
|
196
|
+
real maintained column and sort correctly, and withholding their headers would
|
|
197
|
+
have broken two affordances that work.
|
|
198
|
+
- 616a2a5: The list filter builder no longer offers `Is set` / `Is not set`, which its query dialects cannot express.
|
|
199
|
+
|
|
200
|
+
**User-visible before/after.** The operator dropdown in the list toolbar's
|
|
201
|
+
filter popover — and in the Studio view/tab/page filter inspectors, the dataset
|
|
202
|
+
inspector and the generic `filter` config field — loses two rows: **"Is set"**
|
|
203
|
+
and **"Is not set"**. The sharing-rule criteria builder (`FilterConditionField`)
|
|
204
|
+
keeps both, unchanged. `Is null` / `Is not null` and `Is empty` / `Is not empty`
|
|
205
|
+
are untouched everywhere and remain the way to filter on a missing value from
|
|
206
|
+
the list.
|
|
207
|
+
|
|
208
|
+
Nothing that worked stops working. Every save path behind those two rows was
|
|
209
|
+
already broken, in three different ways depending on the surface:
|
|
210
|
+
|
|
211
|
+
- **Live grid** — `ListView.mapOperator` had no row for either id, so its
|
|
212
|
+
`default:` arm returned the id verbatim and the query went out as
|
|
213
|
+
`['name', 'exists', 'x']`. `exists` is not a member of the spec's
|
|
214
|
+
`VALID_AST_OPERATORS`, so `isFilterAST()` rejects the shape: an unfiltered
|
|
215
|
+
read or a 400, never the filter the user asked for.
|
|
216
|
+
- **Save as view** — `foldFilterGroupToSpecRules` normalizes through the spec's
|
|
217
|
+
`normalizeFilterOperator`, which does not know the pair, and
|
|
218
|
+
`ViewFilterRuleSchema`'s enum then refuses the rule.
|
|
219
|
+
- **Dataset inspector** — `groupToCondition` has no row either and drops the
|
|
220
|
+
condition silently, so the filter simply never applied.
|
|
221
|
+
|
|
222
|
+
**Why withheld rather than mapped.** Measured on `@objectstack/spec`
|
|
223
|
+
17.0.0-rc.6: neither `VIEW_FILTER_OPERATORS` nor `VALID_AST_OPERATORS` contains
|
|
224
|
+
an existence operator, under any spelling — both sets have zero members matching
|
|
225
|
+
`/exist/`. Only the MongoDB-style `FieldOperatorsSchema` criteria carries
|
|
226
|
+
`$exists`, and that is precisely the dialect `FilterConditionField` writes, so
|
|
227
|
+
the pair moves behind the existing `OPT_IN_OPERATORS` gate and that widget opts
|
|
228
|
+
in. Collapsing them onto `isNotNull` / `isNull` was rejected: the builder
|
|
229
|
+
already draws those as their own rows, the round trip is lossy (a saved
|
|
230
|
+
`exists` reads back as `isNotNull`), and the spec's own note records `$exists` =
|
|
231
|
+
has-value as still unsettled across drivers — `driver-memory`'s live mingo path
|
|
232
|
+
and `driver-mongodb` read key-presence.
|
|
233
|
+
|
|
234
|
+
**The class is now closed by an assertion, not by discipline.** objectui's three
|
|
235
|
+
existing operator-parity guards all sweep spec vocabulary → objectui; none asked
|
|
236
|
+
whether an id the dropdown draws is an id the consumer can persist, which is the
|
|
237
|
+
direction that broke. `plugin-list`'s new
|
|
238
|
+
`list-offered-operator-expressible-parity.test.ts` forces the set the list
|
|
239
|
+
toolbar offers to **equal** the set its two dialects can express, in both
|
|
240
|
+
directions — so an unexpressible operator cannot be offered, and an operator
|
|
241
|
+
that becomes expressible upstream cannot stay needlessly withheld.
|
|
242
|
+
- dfc6975: Related-list "+ New" now honours `userActions.create` predicates, and the grid
|
|
243
|
+
toolbar's inline-edit affordance is gated on `update` permission (objectui#4646,
|
|
244
|
+
objectui#4647).
|
|
245
|
+
|
|
246
|
+
Two declared-but-unenforced gaps on the same toolbar surface.
|
|
247
|
+
|
|
248
|
+
**#4646 — `createPredicates` had a producer and no consumer.**
|
|
249
|
+
`@objectstack/spec@17.0.0` widened `userActions.create` to
|
|
250
|
+
`z.union([z.boolean(), RowCrudActionOverrideSchema])`, so `resolveCrudAffordances`
|
|
251
|
+
emits `createPredicates` — and nothing in objectui read them, against roughly
|
|
252
|
+
fifteen consumption sites apiece for `editPredicates` / `deletePredicates`. The
|
|
253
|
+
symptom: a parent record entering a frozen state correctly greyed its children's
|
|
254
|
+
row Edit/Delete while the related list's "+ New" stayed fully live, so the user
|
|
255
|
+
filled in the whole child form to earn a server 409. The related-list toolbar now
|
|
256
|
+
evaluates `visibleWhen` / `disabledWhen` **once against the host parent record**,
|
|
257
|
+
per the spec docblock's binding for this key, on top of the existing
|
|
258
|
+
`o.create ∧ can(child, 'create')` check. `visibleWhen` hides "+ New" and fails
|
|
259
|
+
CLOSED; `disabledWhen` greys it and fails SOFT — the same evaluator, fail
|
|
260
|
+
directions and hidden-vs-disabled split the record header already uses for
|
|
261
|
+
edit/delete (objectui#4419 / PR #4515). A bare-boolean `userActions.create` is
|
|
262
|
+
untouched: with no predicates there is nothing to evaluate.
|
|
263
|
+
|
|
264
|
+
**#4647 — the inline-edit toggle was the one ungated affordance on its toolbar.**
|
|
265
|
+
It rendered on "grid view ∧ the host wired `onInlineEditChange` ∧ not the compact
|
|
266
|
+
toolbar", and every host wires that callback unconditionally. New and Import are
|
|
267
|
+
hidden for an account without the grant and the bulk-delete entry on the same
|
|
268
|
+
toolbar ANDs `can(obj, 'delete')`, but a read-only principal could flip inline
|
|
269
|
+
edit, modify cells and press "Save all" to earn a server 403. It is now gated on
|
|
270
|
+
the object's resolved edit affordance ∧ `can(object, 'update')`, mirroring that
|
|
271
|
+
bulk-delete gate. The gate is applied at all three sites that carry this
|
|
272
|
+
affordance — the wide toolbar's toggle, the compact toolbar's settings-popover
|
|
273
|
+
entry (which previously had no gate at all, not even the callback), and the
|
|
274
|
+
`editable` mode handed to the grid, so a stored view carrying `inlineEdit: true`
|
|
275
|
+
can no longer drop a read-only principal into editable cells with no toggle to
|
|
276
|
+
press.
|
|
277
|
+
|
|
278
|
+
`ListViewSchema.userActions.editInline` is also consumed now: an explicit `false`
|
|
279
|
+
withholds the affordance wholesale, which authors previously could not do.
|
|
280
|
+
|
|
281
|
+
**Behaviour change for read-only users, stated plainly.** Where the UI used to
|
|
282
|
+
offer inline editing and let the server refuse it, it now declines to offer the
|
|
283
|
+
entry point at all. No data access changes — the server gate was and remains the
|
|
284
|
+
enforcement boundary; this only stops the UI walking users into round-trips
|
|
285
|
+
guaranteed to fail. Accounts *with* the grant see no change, and hosts with no
|
|
286
|
+
`PermissionProvider` mounted (standalone embeds, the Studio designer) keep
|
|
287
|
+
today's behaviour, since `can()` answers `true` there by design.
|
|
288
|
+
|
|
289
|
+
One deliberate non-change: the absent case of `userActions.editInline` defers to
|
|
290
|
+
the host's existing `inlineEdit` channel rather than enforcing the spec's
|
|
291
|
+
`.default(false)`. Enforcing that default would remove the toggle from every
|
|
292
|
+
stored console list view in one release, since nothing folds a legacy key into
|
|
293
|
+
`editInline` and no existing view declares it. This follows the rule the
|
|
294
|
+
surrounding toolbar-flag block already states for itself — defaults chosen to
|
|
295
|
+
match what the flags have always done. `InterfaceListPage`, the key's other
|
|
296
|
+
consumer, reads the absent case as OFF, because the ADR-0047 interface page has
|
|
297
|
+
no such host channel to defer to.
|
|
298
|
+
- 138ab04: Fix a list filter that silently applied nothing when the first thing you picked was **Is null** or **Is not null**.
|
|
299
|
+
|
|
300
|
+
Adding a filter seeds the row with no value, and changing the operator keeps it that way — which is correct for an operator that takes no value, since the panel draws no input for one. The live grid read that row as unfinished and dropped it: the filter appeared in the panel, the query went out without it, and every record came back with no error to explain it. Only `Is empty` / `Is not empty` were exempt; the builder renders six operators value-less.
|
|
301
|
+
|
|
302
|
+
Which operators are complete without a value now has a single owner — `VALUELESS_FILTER_BUILDER_OPERATORS`, exported from `@object-ui/components` beside the code that decides it. The live grid and the saved-view fold both read it instead of keeping their own lists, so the two halves of one interaction can no longer disagree about whether a row is finished.
|
|
303
|
+
- Updated dependencies [88085e3]
|
|
304
|
+
- Updated dependencies [69251bf]
|
|
305
|
+
- Updated dependencies [57e668f]
|
|
306
|
+
- Updated dependencies [516663d]
|
|
307
|
+
- Updated dependencies [41ac1b7]
|
|
308
|
+
- Updated dependencies [1eaf0a1]
|
|
309
|
+
- Updated dependencies [a09bc33]
|
|
310
|
+
- Updated dependencies [460c4d0]
|
|
311
|
+
- Updated dependencies [0ae27f7]
|
|
312
|
+
- Updated dependencies [2533ec5]
|
|
313
|
+
- Updated dependencies [78c0f9a]
|
|
314
|
+
- Updated dependencies [bbe8b86]
|
|
315
|
+
- Updated dependencies [8477be5]
|
|
316
|
+
- Updated dependencies [279fb13]
|
|
317
|
+
- Updated dependencies [2e82ab2]
|
|
318
|
+
- Updated dependencies [ad07b65]
|
|
319
|
+
- Updated dependencies [41f498b]
|
|
320
|
+
- Updated dependencies [ef0d150]
|
|
321
|
+
- Updated dependencies [f34226e]
|
|
322
|
+
- Updated dependencies [564b605]
|
|
323
|
+
- Updated dependencies [e1d4251]
|
|
324
|
+
- Updated dependencies [40d3a33]
|
|
325
|
+
- Updated dependencies [8b9dc62]
|
|
326
|
+
- Updated dependencies [1184192]
|
|
327
|
+
- Updated dependencies [a2a9747]
|
|
328
|
+
- Updated dependencies [65e88e6]
|
|
329
|
+
- Updated dependencies [a1609a6]
|
|
330
|
+
- Updated dependencies [53f23bc]
|
|
331
|
+
- Updated dependencies [c4533dc]
|
|
332
|
+
- Updated dependencies [be60815]
|
|
333
|
+
- Updated dependencies [37f6844]
|
|
334
|
+
- Updated dependencies [93de4f6]
|
|
335
|
+
- Updated dependencies [2b50261]
|
|
336
|
+
- Updated dependencies [384f30d]
|
|
337
|
+
- Updated dependencies [ac600e5]
|
|
338
|
+
- Updated dependencies [97fba31]
|
|
339
|
+
- Updated dependencies [232f61a]
|
|
340
|
+
- Updated dependencies [d374caf]
|
|
341
|
+
- Updated dependencies [5673576]
|
|
342
|
+
- Updated dependencies [c1ef923]
|
|
343
|
+
- Updated dependencies [911ceaa]
|
|
344
|
+
- Updated dependencies [98eab36]
|
|
345
|
+
- Updated dependencies [af5e292]
|
|
346
|
+
- Updated dependencies [3fbbea1]
|
|
347
|
+
- Updated dependencies [0bffb18]
|
|
348
|
+
- Updated dependencies [800f455]
|
|
349
|
+
- Updated dependencies [5458414]
|
|
350
|
+
- Updated dependencies [3241559]
|
|
351
|
+
- Updated dependencies [7f96b10]
|
|
352
|
+
- Updated dependencies [167ec42]
|
|
353
|
+
- Updated dependencies [616a2a5]
|
|
354
|
+
- Updated dependencies [6c68b13]
|
|
355
|
+
- Updated dependencies [0046d8f]
|
|
356
|
+
- Updated dependencies [f1d4748]
|
|
357
|
+
- Updated dependencies [bea374e]
|
|
358
|
+
- Updated dependencies [b1119ec]
|
|
359
|
+
- Updated dependencies [5607092]
|
|
360
|
+
- Updated dependencies [9f23d2b]
|
|
361
|
+
- Updated dependencies [578e025]
|
|
362
|
+
- Updated dependencies [af025ee]
|
|
363
|
+
- Updated dependencies [d109a4d]
|
|
364
|
+
- Updated dependencies [598c89a]
|
|
365
|
+
- Updated dependencies [4a0bd17]
|
|
366
|
+
- Updated dependencies [b8b9af4]
|
|
367
|
+
- Updated dependencies [d8b9259]
|
|
368
|
+
- Updated dependencies [31676be]
|
|
369
|
+
- Updated dependencies [8c0d52e]
|
|
370
|
+
- Updated dependencies [aff10e2]
|
|
371
|
+
- Updated dependencies [70a774b]
|
|
372
|
+
- Updated dependencies [9ce096f]
|
|
373
|
+
- Updated dependencies [e05db88]
|
|
374
|
+
- Updated dependencies [7458a41]
|
|
375
|
+
- Updated dependencies [ad13d63]
|
|
376
|
+
- Updated dependencies [5ffcc14]
|
|
377
|
+
- Updated dependencies [d971e51]
|
|
378
|
+
- Updated dependencies [97abb24]
|
|
379
|
+
- Updated dependencies [deb157a]
|
|
380
|
+
- Updated dependencies [9c60144]
|
|
381
|
+
- Updated dependencies [e7747f1]
|
|
382
|
+
- Updated dependencies [d2ce342]
|
|
383
|
+
- Updated dependencies [9695da7]
|
|
384
|
+
- Updated dependencies [ac2f332]
|
|
385
|
+
- Updated dependencies [a777058]
|
|
386
|
+
- Updated dependencies [75444e3]
|
|
387
|
+
- Updated dependencies [58b8346]
|
|
388
|
+
- Updated dependencies [2d0bd16]
|
|
389
|
+
- Updated dependencies [a9e17b4]
|
|
390
|
+
- Updated dependencies [b8ce7dc]
|
|
391
|
+
- Updated dependencies [dad51e5]
|
|
392
|
+
- Updated dependencies [1c9c342]
|
|
393
|
+
- Updated dependencies [787c738]
|
|
394
|
+
- Updated dependencies [8396656]
|
|
395
|
+
- Updated dependencies [dbbd38a]
|
|
396
|
+
- Updated dependencies [61556dc]
|
|
397
|
+
- Updated dependencies [8871c14]
|
|
398
|
+
- Updated dependencies [93fe362]
|
|
399
|
+
- Updated dependencies [dfc6975]
|
|
400
|
+
- Updated dependencies [3cf4de0]
|
|
401
|
+
- Updated dependencies [c9dc811]
|
|
402
|
+
- Updated dependencies [144ef9b]
|
|
403
|
+
- Updated dependencies [138ab04]
|
|
404
|
+
- Updated dependencies [a0b9e91]
|
|
405
|
+
- Updated dependencies [99bd015]
|
|
406
|
+
- Updated dependencies [21e4585]
|
|
407
|
+
- @object-ui/types@17.6.0
|
|
408
|
+
- @object-ui/fields@17.6.0
|
|
409
|
+
- @object-ui/i18n@17.6.0
|
|
410
|
+
- @object-ui/react@17.6.0
|
|
411
|
+
- @object-ui/components@17.6.0
|
|
412
|
+
- @object-ui/core@17.6.0
|
|
413
|
+
- @object-ui/permissions@17.6.0
|
|
414
|
+
- @object-ui/mobile@17.6.0
|
|
415
|
+
|
|
416
|
+
## 17.5.0
|
|
417
|
+
|
|
418
|
+
### Minor Changes
|
|
419
|
+
|
|
420
|
+
- 7084f7d: `DashboardRenderer` and `ListView` serve the props they declare — the index signature stops erasing them
|
|
421
|
+
|
|
422
|
+
Both components declared a full props interface and neither was enforced. A `[key: string]: any` on `DashboardRendererProps` and `ListViewProps` puts `string` into `keyof Props`, so `'ref' extends keyof Props` is always true and React's `PropsWithoutRef` takes its `Omit` branch — and `Omit` over a type carrying a string index signature keeps only the index signature. Every declared property was dropped from the resolved type, on both sides: the render function received `{ [x: string]: any }` (so even `schema` was `any` inside the component), and every JSX call site was unchecked. Measured on the pre-fix source, `keyof ComponentProps<typeof DashboardRenderer>` was `string | number` and `ComponentProps<typeof DashboardRenderer>['onWidgetClick']` was `any`, while the interface went on declaring `(widgetId: string | null) => void`. `ListView` measured identically for `onRowClick`. This is objectui#4422 / PR #4438's trap in the two packages that issue left unswept.
|
|
423
|
+
|
|
424
|
+
Graded **minor, not major**: the interfaces have always DECLARED these props; the index signature erased them from the resolved type. Restoring what the interface documents is a FIX to the published contract, not a contract break — no documented capability is removed, and `any`-typed accidental passthrough was never the documented surface. Nothing in either package's README or docs endorses relying on it.
|
|
425
|
+
|
|
426
|
+
The props each component genuinely reads but never declared are now declared by name, at the type each one lands on: `dataSource` on both, plus `onAddRecord` / `onBulkAction` / `onPageSizeChange` / `onEdit` / `onDelete` / `onBulkDelete` on `ListView`. `DashboardRenderer`'s DOM pass-through keys are derived from `toDomProps`' whitelist constant itself, so the declaration and the runtime filter cannot drift — the "declare it and forward it by name" direction `@object-ui/core`'s `dom-props` doctrine asks for, rather than reopening the spread.
|
|
427
|
+
|
|
428
|
+
Type-only: the emitted JS for both packages is byte-identical before and after (verified by sha256 on `dist/index.js` and `dist/index.umd.cjs`), and both packages' runtime suites are untouched and green.
|
|
429
|
+
|
|
430
|
+
Three latent defects the erasure had been hiding are fixed with it, each surfaced by the repo-wide type-check: `DashboardWithConfig` typed its widget-select handler `(widgetId: string)` while `DashboardRenderer` calls `onWidgetClick(null)` to deselect; `InterfaceListPage` built a list schema whose `viewType` was a bare `string`; and `StudioDesignSurface` forwarded a `refreshKey` prop that no component in the chain declares or reads, so it was silently dropped. Per-package structural guards now pin the shape in both packages, covering the public `forwardRef` that takes its props whole — the spelling objectui#4438's `schema`-destructuring scan could not see.
|
|
431
|
+
|
|
432
|
+
- fe52a04: `rowHeightToDensityMode` answers only for the five spec row heights — the coerce-to-`comfortable` fallback is gone
|
|
433
|
+
|
|
434
|
+
Two surfaces narrow a list view's `rowHeight` onto the renderer's three-step
|
|
435
|
+
density vocabulary, and since objectui#4352 they answered differently for the
|
|
436
|
+
same off-spec input: `@object-ui/react`'s spec bridge declined to answer, while
|
|
437
|
+
`@object-ui/core`'s `rowHeightToDensityMode` rehabilitated anything unknown into
|
|
438
|
+
`comfortable`. One metadata-driven system, two answers for one input
|
|
439
|
+
(objectui#4440).
|
|
440
|
+
|
|
441
|
+
The strict answer wins, per AGENTS.md #0.1: a renderer-side rehabilitation of
|
|
442
|
+
off-spec metadata is a second de-facto contract, and one strict contract beats N
|
|
443
|
+
dialects — a bad `rowHeight` gets fixed at the producer, where the schema already
|
|
444
|
+
rejects it. The five mappings themselves are untouched (`compact`/`short` →
|
|
445
|
+
`compact`, `medium` → `comfortable`, `tall`/`extra_tall` → `spacious`), and the
|
|
446
|
+
table keeps its `Record< RowHeight, … >` typing, so a row height added upstream
|
|
447
|
+
still fails the build here.
|
|
448
|
+
|
|
449
|
+
**Breaking semantics, deliberately graded `minor`** (this repo never publishes
|
|
450
|
+
`major` — its major tracks `@objectstack`). Two things change:
|
|
451
|
+
|
|
452
|
+
- **Published type.** `rowHeightToDensityMode` is exported from
|
|
453
|
+
`@object-ui/core`, and its return widens from `DensityMode` to
|
|
454
|
+
`DensityMode | undefined`. A host assigning the result straight into a
|
|
455
|
+
`DensityMode` now has to say what an off-spec row height should mean to it.
|
|
456
|
+
- **Rendered output, for input the spec already rejects.** `ListView` — the one
|
|
457
|
+
in-repo caller — used to render an off-spec `rowHeight` one step looser than an
|
|
458
|
+
ABSENT one (`comfortable`, 40px rows, vs `compact`, 32px). It now renders it
|
|
459
|
+
exactly like an absent one, `compact`, which is also `ObjectGrid`'s own default.
|
|
460
|
+
A sweep of this repo, the `objectstack` example apps and one downstream app
|
|
461
|
+
found zero authored off-spec values, and the legacy `densityMode` alias cannot
|
|
462
|
+
produce one (`DENSITY_MODE_TO_ROW_HEIGHT` is typed
|
|
463
|
+
`Record< DensityMode, RowHeight >`).
|
|
464
|
+
|
|
465
|
+
Also closed while retiring the branch: the lookup guarded membership with `in`,
|
|
466
|
+
which walks the prototype chain, so `rowHeight: 'toString'` returned
|
|
467
|
+
`Object.prototype.toString` — a function — from something typed `DensityMode`. It
|
|
468
|
+
is an own-property check now.
|
|
469
|
+
|
|
470
|
+
### Patch Changes
|
|
471
|
+
|
|
472
|
+
- ae10a01: Console chrome reaches the bundle — the list switcher, the aggregate footer, the dialog a11y fallbacks and the whole Settings namespace screen stop being English on non-English consoles
|
|
473
|
+
|
|
474
|
+
Six strings on the two screens a user looks at most were hardcoded English literals rather than bundle lookups, so they stayed English on every non-English console with nothing an app could author to change them. They are not object, field, view or action labels — no key in `TranslationData` reaches them — while the console's own bundle already ships zh-CN, ja-JP, es-ES, de, fr, pt, ru, ko and ar and translates hundreds of neighbouring strings. Omissions from an otherwise complete bundle, not a missing capability.
|
|
475
|
+
|
|
476
|
+
**Two of the six needed no new keys at all, which is the more interesting half.** The list-view mode switcher named its nine visualizations from a private `VIEW_LABELS` table while `console.objectView.viewType*` — the same nine words — had been resolved through the bundle by the create-view picker for months; the switcher now reads those keys, so the picker's 「画廊」 and the switcher's 「画廊」 cannot drift apart in nine languages. The create/edit dialog's close button is the remainder of a fix that already landed: objectstack#5505 routed the `sr-only` close label through `common.close` for the two Shadcn-synced primitives, but `MobileDialogContent` is a hand-written wrapper outside that regeneration zone with its own close button, and it is exactly what `ModalForm` renders — so the dialog the report measured was the one place still announcing "Close" in English.
|
|
477
|
+
|
|
478
|
+
The aggregate footer is the one the original report singled out: the **number** was already locale-formatted and the **prefix** was a hardcoded `Avg: ` / `Sum: `. All eleven aggregation kinds now take their prefix from `grid.summary.*`, and the label/value join is its own key rather than a `': '` baked into the renderer — the separator is translatable content, so zh sets a fullwidth colon and fr the French space-before-colon. The numbers are untouched. The form dialog's `sr-only` description fallback joins the packs too; it is clipped, not visible, so the only way an app could displace it was to author a `description` and thereby put a visible subtitle on every dialog.
|
|
479
|
+
|
|
480
|
+
**The Settings namespace screen converts as one unit.** `SettingsView` routed zero framing copy through i18n — save/failure toasts, the env-lock and crypto refusals, the load-error card, the empty-route state, the navigation buttons, the unsaved-changes save bar — while its immediate sibling `SettingsHub`, in the same directory, resolved everything through `t('console.settingsHub.*')`. A zh-CN admin read correctly translated field labels sitting inside an English save bar, because `useSettingsLabel` translates a namespace's authored content but reaches none of the chrome around it. All of it now resolves through a `console.settingsView.*` namespace placed beside the hub's, including the crypto-refusal strings that objectui#4579 deliberately left in English rather than leave one translated string among a dozen literals.
|
|
481
|
+
|
|
482
|
+
The save-bar counter was an English plural rule executing in every locale (`change` plus an `s` when the count exceeds one). It is now a real i18next plural family — base key plus `_one` and `_other` in all ten packs — not the `(s)` spelling translated nine ways. The base key is the load-bearing part: i18next asks `Intl.PluralRules` for the one suffix a language needs and, finding no such slot, falls back to English, so without it Russian would read English at counts 2-20 and Arabic at 2-99. Russian and Arabic take the "noun: {count}" form their packs already use for this exact reason, and the counter is verified rendering in-language at 1, 2 and 5.
|
|
483
|
+
|
|
484
|
+
The Beta badge reuses the hub's existing key rather than minting a twin, and the refusal messages interpolate their subject through the bundle instead of concatenating a translated word onto an English prefix.
|
|
485
|
+
|
|
486
|
+
- bb58d1d: i18n: the two search placeholders become pack values, and four values the packs served in English get translated
|
|
487
|
+
|
|
488
|
+
**objectui#4375** — `ListView` and `LookupField` built their search placeholder as
|
|
489
|
+
`t(key) + '...'`, so the ellipsis was a literal concatenated in code: it stayed ASCII
|
|
490
|
+
in all ten locales on screens where objectui#3878 had converged everything else on
|
|
491
|
+
U+2026, and no pack could opt out of it (sharpest in `ar`, where a left-to-right run
|
|
492
|
+
was appended to right-to-left text). Both now read `table.search`, which is already
|
|
493
|
+
the repo's search-input placeholder key — `data-table`, `RecordPickerDialog` and
|
|
494
|
+
`PeoplePicker` render it too — and is translated with the right ellipsis in all ten
|
|
495
|
+
packs. No new keys.
|
|
496
|
+
|
|
497
|
+
**objectui#4376** — `list.loading` served the English `Loading records…` in eight of
|
|
498
|
+
the nine translation packs (`zh` alone had translated it); `designer.undo` and
|
|
499
|
+
`designer.redo` were English in all nine; `appDesigner.snakeCaseHint` in `ko`, `pt`,
|
|
500
|
+
`ru` and `ar`. All translated, reusing each pack's own established vocabulary. A new
|
|
501
|
+
pin (`untranslated-identity-4376.test.ts`) fails on any value byte-identical to `en`
|
|
502
|
+
inside a non-Latin pack unless the key is on an explicit 22-entry allowlist.
|
|
503
|
+
|
|
504
|
+
- bb68488: An inline per-locale label now renders its locale's string at the thirteen read sites the `@objectstack/spec` 17.0.0-rc.6 bump exposed
|
|
505
|
+
|
|
506
|
+
rc.6 widened `I18nLabel` from `string` to `string | Record<string, string>`, so an author may write `label: { en: 'Owner', 'zh-CN': '负责人' }` anywhere the spec accepts a display label. PR #4169 repaired eight such sites; these thirteen were invisible to it because the five packages involved build through vite/rolldown, so `turbo run build` never type-checks their sources — only `turbo run type-check` does. All thirteen are now resolved through a shared resolver against a real locale, and `turbo run type-check` is 78/78 with zero errors.
|
|
507
|
+
|
|
508
|
+
| package | what an author can now write and see |
|
|
509
|
+
| ----------------------------- | --------------------------------------------------------------------------------------- |
|
|
510
|
+
| `@object-ui/layout` | `NavigationArea.label` — the sidebar area switcher's button and its tooltip |
|
|
511
|
+
| `@object-ui/plugin-list` | `ViewTab.label` — the inline pill row, and the mobile dropdown's trigger and menu items |
|
|
512
|
+
| `@object-ui/plugin-dashboard` | `DashboardWidget.title` — the widget card heading and its `title` attribute |
|
|
513
|
+
| `@object-ui/plugin-designer` | `DashboardWidget.title` — the widget card and the preview tile |
|
|
514
|
+
| `@object-ui/app-shell` | `ActionParam.label` **and** each `ActionParam.options[].label` |
|
|
515
|
+
|
|
516
|
+
**Patch, not minor, in every case: no public surface changes meaning.** Every entry above is a read site that previously could only be reached with a value the type system rejected, so no caller's working code changes behaviour. `@object-ui/app-shell` is the only package with an exported-type change and it is purely additive on the authoring side — `RawActionParam.label` and `RawActionParam.options[].label` widen to `I18nLabel` (they accept strictly more), `ResolveActionParamsContext` gains an optional `locale`, and the new `RawActionParamOption` names the authoring shape that was previously spelled with the resolved one. What `resolveActionParams` **emits** is unchanged: `ActionParamDef.label` and its options' labels are still plain `string`s.
|
|
517
|
+
|
|
518
|
+
Two consequences worth knowing:
|
|
519
|
+
|
|
520
|
+
- **The dashboard designer's title input is deliberately read-only for a map-valued title.** Resolving a per-locale map into a single-line input and writing `e.target.value` back would collapse every other locale on the first keystroke, so the write is guarded and an inline map survives an unrelated edit-and-save round trip untouched — the same conservative branch #4169 took for `DashboardWidgetInspector`. What Studio should actually offer for authoring a per-locale label is objectui#4163 part 2, which is unclaimed and pending design.
|
|
521
|
+
- **`@object-ui/layout` resolves at the spec's `en` default, not the viewer's language.** That package carries no i18n dependency by design (its whole i18n story is injection), and `AppSchemaRendererProps` exposes no locale to thread. The choice and what would change it are documented at the call site.
|
|
522
|
+
|
|
523
|
+
- 297534b: Align 43 inline `defaultValue` strings with the `en` pack, and make the call-site gate enforce it (objectui#3810)
|
|
524
|
+
|
|
525
|
+
`t(key, { defaultValue: 'English text' })` only renders that text when i18next
|
|
526
|
+
**misses** the key. Where the key exists in `packages/i18n/src/locales/en.ts` the
|
|
527
|
+
pack value always wins, so the inline string is dead code — and 43 of those dead
|
|
528
|
+
strings said something different from the sentence users actually read.
|
|
529
|
+
|
|
530
|
+
`scripts/check-i18n-call-site-keys.mjs` (objectui#3530) now compares the two
|
|
531
|
+
whenever a call site carries a literal `defaultValue` for a key `en` defines, and
|
|
532
|
+
fails on any byte of difference. It is a hard rule with **no baseline**: the
|
|
533
|
+
repo-wide census measured 43 sites in 19 files out of 851 literal inline defaults,
|
|
534
|
+
and all 43 are aligned here, so there is no debt for a ratchet to hold. A
|
|
535
|
+
`defaultValue` on a key that is _not_ yet in `en` stays legal — that transition
|
|
536
|
+
runs for months (objectui#3546) and belongs to the existing `missing-key` rule,
|
|
537
|
+
which keeps reporting it alone.
|
|
538
|
+
|
|
539
|
+
Every fix moved the CALL SITE to the pack's wording. `en.ts` is untouched: its
|
|
540
|
+
values are what users read today, and changing one would oblige the same change in
|
|
541
|
+
the nine other packs (`scripts/check-i18n-en-drift.mjs`, objectui#3650). Six of the
|
|
542
|
+
43 differed only in an ellipsis (`...` against U+2026) — invisible in review, which
|
|
543
|
+
is how they survived three i18n gates that are each blind to this class by
|
|
544
|
+
construction.
|
|
545
|
+
|
|
546
|
+
The visible effect is confined to hosts that render these components with **no**
|
|
547
|
+
`I18nProvider` and no initialised i18next instance. There, react-i18next's
|
|
548
|
+
not-ready `t` returns the `defaultValue`, so the inline string was the rendered
|
|
549
|
+
one; it now matches what a provider-backed app has always shown. Inside the
|
|
550
|
+
console — provider mounted — nothing users see changes. The clearest converging
|
|
551
|
+
examples: the workspaces screen was written as "Organizations" at nine call sites
|
|
552
|
+
while every user has been reading "Workspaces"; the forgot-password success line
|
|
553
|
+
was written as "If an account exists, a reset link has been sent." while the pack
|
|
554
|
+
asserts "We've sent a password reset link to {{email}}."
|
|
555
|
+
|
|
556
|
+
- f8595a0: A list emptied by the view's own filter says "no records match", instead of inviting you to create your first record
|
|
557
|
+
|
|
558
|
+
`ListView`'s empty state distinguishes "filtered to empty" from "truly empty (first run)", but the view's own declared `filter` did not count toward that decision — only the search term, the user-filter conditions and the toolbar's live filter group did. A view that returns nothing _because it is filtered_ therefore rendered the first-run copy over an object full of records.
|
|
559
|
+
|
|
560
|
+
That is a small string, and it cost real triage time. In objectui#4155 a stored overlay filter had emptied a list, and the screen said "no data yet / create your first record" — so the report read as data loss or a permission problem, and the investigation went to the data and permission layers rather than to the view layer where the defect was. The same misread is available without any bug at all: a perfectly healthy view declaring `status not_in [archived, deleted]` over an object whose rows are all archived told the user the object was empty.
|
|
561
|
+
|
|
562
|
+
The base `filter` now counts as an active query, in both at-rest shapes (an array of conditions, and the Mongo-style object form). No new copy — this only routes to the `list.noMatches` / `list.noMatchesMessage` strings that already exist in all ten locales, so there are no new keys to translate. An author-supplied `emptyState.title` / `emptyState.message` still wins over both branches, unchanged.
|
|
563
|
+
|
|
564
|
+
- 33c32bf: List sort: the picker stops borrowing the filter whitelist, and a header click is no longer a one-way door out of the view's declared sort
|
|
565
|
+
|
|
566
|
+
`filterableFields` was applied to the single field set both toolbar builders read, so a whitelist authored for _filtering_ silently became the _sort_ whitelist too. A view could declare a two-level default sort — `plan_start_date` then `name` — and get a sort panel that offered neither field and rendered both of its rows blank: the declared sort worked on load and could then be neither reproduced nor modified, and there was no way to express "sortable but not offered as a filter condition" short of widening the filter builder as collateral. The whitelist now narrows the filter builder alone, which is the contract it was written for; the sort picker starts from every field the view can name and applies its own sortability rules.
|
|
567
|
+
|
|
568
|
+
Those rules are about what the sort can honestly reach, so a second one joins the existing relational exclusion: a `formula` field is withheld. It has no materialised column, so ordering by one is refused by the server outright (objectstack `UNMATERIALIZED_SORT_TYPES`) — and it matters here precisely because the base set widened, since a formula field previously reached the picker only if someone had whitelisted it. The exclusion is `formula` alone and deliberately not the spec's `COMPUTED_VALUE_TYPES`: `summary` and `autonumber` are computed too, each gets a real maintained column, and both order correctly. Either rule keeps its existing escape hatch — a field the current sort already uses stays listed, which for a formula field is the only way to remove the offending row.
|
|
569
|
+
|
|
570
|
+
One consequence worth naming: the hint explaining the relational omission used to be gated by the same whitelist. A view whitelisting only `status` showed a near-empty sort picker and no word about why; the withheld relational field now reaches the rule that withholds it, so the explanation appears with it.
|
|
571
|
+
|
|
572
|
+
The second half is the way back. One column-header click replaces the whole sort array, so a view shipping a multi-level default lost it for the rest of the session — the declared `sort` behaved as an initial value only, recoverable just by reloading the page. The sort panel gains a **Reset to view default** control that restores the declared array whole: multi-level, in declared order, not merely cleared. It reads the view's declared sort through the same resolver the initial render already uses, so there is one answer to "what did this view declare". It is disabled while the active sort already matches that default, and absent entirely for a view that declares no sort — there is no default to return to, and clearing the sort under that label would be a second, differently-named way to do what removing the rows already does. The header click's own semantics are unchanged: it still replaces the array, it just no longer does so irreversibly.
|
|
573
|
+
|
|
574
|
+
- 7ffd616: fix(plugin-list): ListView hands the child grid the query behind the window it passes down
|
|
575
|
+
|
|
576
|
+
ListView owns the fetch on the external-pagination path — it holds the filter,
|
|
577
|
+
the search term and the sort, and it is the side that calls `dataSource.find`.
|
|
578
|
+
The grid it hands the window to has a cross-page "select all N matching"
|
|
579
|
+
escalation that RE-ISSUES that query to collect the whole match set, and with
|
|
580
|
+
nothing handed down it replayed its own never-written params ref and so asked the
|
|
581
|
+
server for the entire object, feeding unmatched records to bulk delete.
|
|
582
|
+
|
|
583
|
+
The params object is now hoisted out of the `find` call — one object, one query,
|
|
584
|
+
no reconstruction that could drift from what was actually asked — recorded past
|
|
585
|
+
the stale-request guard so it is always the query that produced the rows on
|
|
586
|
+
screen, and forwarded as `findParams` in the same handoff block as `rowCount`,
|
|
587
|
+
`page` and `onPageChange`. No public API of `ListView` changes.
|
|
588
|
+
|
|
589
|
+
- 37cd8e4: `list-view` now reads its `dataSource` binding through the shared `ElementDataSourceGate` instead of a private copy of the precedence table
|
|
590
|
+
|
|
591
|
+
objectstack#5576 landed the per-element `dataSource` binding on `list-view` by
|
|
592
|
+
writing the precedence table — binding keys override the component's, a `view` is
|
|
593
|
+
only a baseline, `filter` AND-combines rather than replaces, an authored-but-empty
|
|
594
|
+
`columns` counts as unauthored, the row cap lands on `pagination.pageSize`, and
|
|
595
|
+
`viewType` is taken only when the component declared none — inline in
|
|
596
|
+
`ListViewBlock`. objectstack#6953 then needed the same table for the other eight
|
|
597
|
+
object-bound blocks and lifted it into `@object-ui/react`
|
|
598
|
+
(`useElementDataSourceSchema` / `ElementDataSourceGate`), deliberately not
|
|
599
|
+
touching `ListViewBlock`: refactoring already-merged code inside a wiring PR
|
|
600
|
+
would have been an out-of-scope regression surface.
|
|
601
|
+
|
|
602
|
+
That left one table with two implementations — `list-view` on the private copy,
|
|
603
|
+
every other block on the shared one. Nothing was wrong for a user today; the risk
|
|
604
|
+
is the next person to change the rules changing one side, which is how the spec's
|
|
605
|
+
"_additional_ filter criteria" becomes two dialects and a per-element filter
|
|
606
|
+
quietly starts replacing a saved view's instead of narrowing it.
|
|
607
|
+
|
|
608
|
+
`ListViewBlock` now contributes only what is genuinely its own — the names of the
|
|
609
|
+
keys `ListView` reads:
|
|
610
|
+
|
|
611
|
+
```ts
|
|
612
|
+
const LIST_VIEW_DATA_SOURCE: ElementDataSourceMapping = {
|
|
613
|
+
columns: true,
|
|
614
|
+
filter: true,
|
|
615
|
+
sort: true,
|
|
616
|
+
limit: "pagination.pageSize",
|
|
617
|
+
viewType: true,
|
|
618
|
+
};
|
|
619
|
+
```
|
|
620
|
+
|
|
621
|
+
and the ~45-line `useMemo` mapping block is deleted, along with the block's
|
|
622
|
+
hand-rolled error and loading panels (the shared
|
|
623
|
+
`ElementDataSourceErrorPanel` / `ElementDataSourceLoadingPanel` render with the
|
|
624
|
+
`list-view` testId prefix, so `list-view-datasource-error` and
|
|
625
|
+
`list-view-resolving-view` are unchanged down to the byte, and the error heading
|
|
626
|
+
is passed through as `errorTitle`).
|
|
627
|
+
|
|
628
|
+
**No behaviour changes in either direction**, and that is the acceptance
|
|
629
|
+
criterion rather than a hoped-for outcome: objectstack#5576's entire suite passes
|
|
630
|
+
untouched, with no assertion edited — had any single case needed adapting, the
|
|
631
|
+
two implementations would have been proven to disagree, which is a defect to
|
|
632
|
+
re-grade rather than a refactor detail to absorb. New pins cover the mapping
|
|
633
|
+
table key by key at the block/gate seam, plus the shared loading panel, which
|
|
634
|
+
neither implementation had ever asserted.
|
|
635
|
+
|
|
636
|
+
- e076fd5: Inline-edit toggle reads "Edit fields" without an I18nProvider, matching every locale pack
|
|
637
|
+
|
|
638
|
+
`DETAIL_DEFAULT_TRANSLATIONS` said `Edit fields inline` where all ten packs say
|
|
639
|
+
`Edit fields`, so `InlineEditSaveBar`'s toggle announced two different names for one
|
|
640
|
+
control — the map's on provider-less hosts (standalone embeds, the preview gallery),
|
|
641
|
+
the pack's in the console. The pack wins; the map row now mirrors it byte for byte.
|
|
642
|
+
|
|
643
|
+
The three ungated defaults maps (`plugin-detail`, `plugin-list`, `plugin-designer`) are
|
|
644
|
+
now compared key-by-key against the `en` pack by a new gate, generalizing the
|
|
645
|
+
collaboration-only precedent from objectui#3440. `LIST_DEFAULT_TRANSLATIONS` and
|
|
646
|
+
`DESIGNER_DEFAULT_TRANSLATIONS` are exported for it, as `DETAIL_DEFAULT_TRANSLATIONS`
|
|
647
|
+
and `COLLAB_DEFAULT_TRANSLATIONS` already were.
|
|
648
|
+
|
|
649
|
+
- a84385b: `NavigationConfig.mode` is optional — the type now says what the hook does
|
|
650
|
+
|
|
651
|
+
`@object-ui/react` published a `NavigationConfig` that required `mode`, in front of a `useNavigationOverlay` that has always defaulted it. The declaration took the spec's authored config, `Omit`ted `mode`, and re-added it as `NonNullable< … >`; 140 lines below, the hook read `navigation?.mode ?? 'page'`. The type was strictly stricter than the implementation it fronted, and `'page'` is meaningful behaviour rather than a placeholder.
|
|
652
|
+
|
|
653
|
+
The spec never asked for that. `NavigationConfigSchema` declares `mode: NavigationModeSchema.default('page')`, and a `.default()` lands on the authoring side as `| undefined` — so `navigation: { view: 'summary_view' }` is legal authored metadata that lets the mode default. `@object-ui/types` already re-exported the spec's own `NavigationConfig` unchanged, which meant one monorepo shipped two published types of the same name that disagreed about whether `mode` could be omitted.
|
|
654
|
+
|
|
655
|
+
The alias is now the spec's authored config verbatim, with no divergence of its own:
|
|
656
|
+
|
|
657
|
+
```ts
|
|
658
|
+
export type NavigationConfig = SpecAuthoredInput<
|
|
659
|
+
typeof NavigationConfigSchema
|
|
660
|
+
>;
|
|
661
|
+
```
|
|
662
|
+
|
|
663
|
+
The cost of the old spelling was paid by callers. `ListView` carried `schema.navigation as NavigationConfig | undefined` for no reason except to get a valid spec-shaped value past the declaration; that assertion is deleted here, not replaced. A type in front of an implementation must not be stricter than the implementation — when it is, every caller pays in casts, and a cast is exactly the renderer-side workaround that belongs back at the producer.
|
|
664
|
+
|
|
665
|
+
**Nothing changes at runtime.** `navigation?.mode ?? 'page'` is untouched, and the default is now pinned as observable behaviour (`useNavigationOverlay.modeDefault.test.tsx`) rather than only as a comment — the explicit modes, the `preventNavigation` and `none` short-circuits, the `onRowClick` priority, and the Cmd/Ctrl/middle-click and `new_window` branches are all pinned alongside it.
|
|
666
|
+
|
|
667
|
+
**Why minor rather than patch**, from the measured `.d.ts`. Optional-izing a property is looser for writers and narrower for readers, so the grade turns on which role the published surface actually plays. In this package `NavigationConfig` occurs only in input positions — `useNavigationOverlay`'s `navigation?:` option and `resolveOverlayWidth`'s parameter — and never in a return type; the package consumes these values and never hands one back. For consumers the change is therefore purely permissive: every call that compiled before still compiles, and spec-shaped configs that previously needed an assertion now compile without one. That gained input shape is a real capability rather than an internal repair, which is more than a patch describes. The reader-side narrowing is real but secondary: code that imports the bare type, annotates its own value with it and reads `.mode` now sees `NavigationMode | undefined`. The in-repo census found exactly one such importer — `ListView` — and it imported the type only to write the assertion this change removes.
|
|
668
|
+
|
|
669
|
+
- dad805d: Six i18n keys no longer render as raw key strings on hosts with no `I18nProvider` (objectui#4396)
|
|
670
|
+
|
|
671
|
+
`detail.saving`, `list.resetSortToDefault`, `appDesigner.widgetProperties`, `appDesigner.addWidget`, `appDesigner.modeEdit` and `common.delete` were read through `createSafeTranslation` without a row in their hook's defaults table and without an inline `defaultValue` at the call site — the only two fallbacks that path has. On a provider-less host (standalone embedding, the preview gallery, host apps that never mount a provider) `fallbackT` therefore returned the key itself, so users saw `detail.saving` in the inline-edit save button, `list.resetSortToDefault` on the sort popover's reset control, `appDesigner.widgetProperties` as the dashboard inspector heading, `appDesigner.addWidget` as its toolbar label, `appDesigner.modeEdit` as a button's accessible name, and `common.delete` on the designer's destructive confirm.
|
|
672
|
+
|
|
673
|
+
Each key now has a row in its consumer hook's defaults table, byte-identical to the `en` pack value. No pack was edited, no key added, no call site changed.
|
|
674
|
+
|
|
675
|
+
- 7f1cb33: List sort: the relational hint stops recommending a formula field, the one type the server refuses to sort by
|
|
676
|
+
|
|
677
|
+
The Sort panel withholds columns that link to another record and explains why, and the last sentence of that explanation named the remedy: _add a formula field holding it_. A formula field is exactly what the platform will not order by. The server keeps `UNMATERIALIZED_SORT_TYPES = new Set(['formula'])` and, since objectstack#6994, a sort naming one is a hard `400 INVALID_SORT` — before that it degraded silently, returning every row with `asc` and `desc` byte-identical. So an author who read the hint, followed it, and built a formula field arrived at a refusal; and since #4243 withheld formula fields from this very picker, at a field the panel does not offer either. Two doors, opposite advice, for one problem.
|
|
678
|
+
|
|
679
|
+
The remedy sentence now names a **stored, denormalised field — written when the source changes** — and rules the formula field out in as many words: it is virtual, no column is stored for it, and the server refuses to sort by one. That is deliberately the server's own vocabulary rather than a third phrasing of the same fact: objectstack#6924 and objectstack#6994 settled on one wording across the refusal doors so an author refused twice is not sent two different ways, and this is the UI door of that same set. The first half of the hint — why relation columns are withheld at all — is unchanged.
|
|
680
|
+
|
|
681
|
+
All ten locale packs move together, as `check:i18n-drift` requires of any `en` edit. The same sentence also lives in `plugin-list`'s provider-less fallback table, which is what renders when the component is used outside an `I18nProvider`; it is updated to match `en` byte for byte, because a pack-only reword would have left the retired advice on exactly the surface this fixes.
|
|
682
|
+
|
|
683
|
+
- 2e3b0c0: fix(list): an `OBJECT_API_DISABLED` list request renders an honest cannot-work state instead of the empty state
|
|
684
|
+
|
|
685
|
+
A list pointed at an object whose `enable` block withholds the API rendered its ordinary
|
|
686
|
+
empty state, so _"this page cannot work, and never could"_ reached the user as _"you have no
|
|
687
|
+
records"_ (objectui#4408). The reported instance — `Setup › Advanced › Signing Keys`, whose
|
|
688
|
+
`sys_jwks` declares `enable.apiEnabled: false` — could not load for any persona and said so
|
|
689
|
+
to nobody. That is also why the upstream defect objectstack#7544 survived review for its
|
|
690
|
+
whole life: a merely unpopulated page invites nobody to click through.
|
|
691
|
+
|
|
692
|
+
The masking had two halves, in two packages, and neither package could see the other:
|
|
693
|
+
|
|
694
|
+
- **`@object-ui/data-objectstack`** (minor — see the grading note below) — `find()` degraded
|
|
695
|
+
**every** 404 into `{ data: [], total: 0 }` and memoised the resource, so the denial arrived
|
|
696
|
+
at the surface as a successful empty result, indistinguishable from a genuinely empty
|
|
697
|
+
object. The two `enable`-block denials are now let through instead: `OBJECT_API_DISABLED`
|
|
698
|
+
(404) and `OBJECT_API_METHOD_NOT_ALLOWED` (405). The memo skips them too — absorbing one
|
|
699
|
+
would have pinned the object to "empty" for the rest of the session.
|
|
700
|
+
- **`@object-ui/plugin-list`** — the load-error panel gained an `api-disabled` kind. The 405
|
|
701
|
+
half was never swallowed, so it already reached this panel, but classified as `network`:
|
|
702
|
+
_"check your connection and try again"_ for a condition no retry can change. It now says
|
|
703
|
+
the object is not exposed through the API, that this is a setting on the object rather than
|
|
704
|
+
a permission, and it offers **no Retry** button, because every retry re-fetches the
|
|
705
|
+
identical refusal.
|
|
706
|
+
|
|
707
|
+
Both denials are pure functions of the object's metadata — no user, no permission, no
|
|
708
|
+
context — so neither is transient or per-user, which is exactly the case where a silent empty
|
|
709
|
+
state is most misleading. Discrimination is on the ADR-0112 `code`, never the status: a
|
|
710
|
+
missing collection, a missing record and a disabled object are all 404.
|
|
711
|
+
|
|
712
|
+
**A genuinely empty object still renders the ordinary empty state**, and a backend without an
|
|
713
|
+
optional collection still degrades to empty — pinned in both directions, at the adapter, at
|
|
714
|
+
the view, and once end-to-end over a real adapter and a real `ListView`.
|
|
715
|
+
|
|
716
|
+
Also closes a code-propagation gap on the same path: `find()`'s raw `$expand`/`$search`
|
|
717
|
+
branch bypasses `@objectstack/client` and hand-rolled its own error, stamping only `status`.
|
|
718
|
+
It now carries the ADR-0112 envelope (`code` + `httpStatus`), so a denial arriving on the
|
|
719
|
+
branch a list takes whenever it expands a lookup or runs a search is no longer anonymous.
|
|
720
|
+
|
|
721
|
+
New strings: `list.loadErrorApiDisabledTitle` / `list.loadErrorApiDisabledMessage`, in the
|
|
722
|
+
`en` pack and mirrored in the list defaults map.
|
|
723
|
+
|
|
724
|
+
## Grading note — why `@object-ui/data-objectstack` is **minor** and not patch
|
|
725
|
+
|
|
726
|
+
Two independent reasons, either of which is sufficient under this repo's precedent
|
|
727
|
+
(objectui#4403 / #4177, and #4485's grading of `@object-ui/core`'s `toDomProps` lift):
|
|
728
|
+
|
|
729
|
+
1. **The emitted `.d.ts` grows two NEW exports.** `isApiAccessDeniedError(error: unknown):
|
|
730
|
+
boolean` and `API_ACCESS_DENIED_CODES` (the readonly tuple
|
|
731
|
+
`['OBJECT_API_DISABLED', 'OBJECT_API_METHOD_NOT_ALLOWED']`) are added to the package's
|
|
732
|
+
public surface. Additive surface growth is minor.
|
|
733
|
+
2. **Observable behaviour on a published API moves.** `ObjectStackDataSource.find()` now
|
|
734
|
+
**REJECTS** for the two `enable`-block denial codes where it previously **RESOLVED** with
|
|
735
|
+
`{ data: [], total: 0 }`. No signature changed and nothing was removed, but a caller that
|
|
736
|
+
relied on those two codes arriving as a successful empty result now receives a rejected
|
|
737
|
+
promise carrying `code` + `httpStatus`, and must handle it.
|
|
738
|
+
|
|
739
|
+
Deliberately unchanged, and still resolving to an empty result exactly as before: a bare 404
|
|
740
|
+
with no code, `OBJECT_NOT_FOUND` (still memoised) and `RECORD_NOT_FOUND`. The behaviour move
|
|
741
|
+
is scoped to the two denial codes named above and to nothing else.
|
|
742
|
+
|
|
743
|
+
Not major: this follows AGENTS.md's version-alignment rule — objectui's major tracks
|
|
744
|
+
`@objectstack`'s, so this repo's own breaking semantics are declared as minor with the change
|
|
745
|
+
described in the body, which is what this note is.
|
|
746
|
+
|
|
747
|
+
- 31ab1ac: fix(print): `window.print()` produces a usable page, and the Print buttons say what they do
|
|
748
|
+
|
|
749
|
+
The list, report and dashboard Print controls were bare `window.print()` calls with no
|
|
750
|
+
print stylesheet, so the browser printed the whole console — sidebar, top bar, chat rail,
|
|
751
|
+
toasts — with the data table clipped to a single viewport. With no label to the contrary
|
|
752
|
+
they were being accepted against "export to PDF" requirements, which they have never been.
|
|
753
|
+
|
|
754
|
+
- `@object-ui/app-shell/styles.css` gains a shared `@media print` block: it hides the shell
|
|
755
|
+
chrome, prints the active content area full-width, releases the viewport-height flex chain
|
|
756
|
+
so long tables paginate instead of clipping, repeats table headers on every sheet, and
|
|
757
|
+
neutralises dark mode (which otherwise prints white-on-white). One sheet serves list,
|
|
758
|
+
report and dashboard.
|
|
759
|
+
- The list and report Print buttons carry a tooltip and accessible name stating that they
|
|
760
|
+
open the browser's own print dialog and are not a PDF export (new `common.printDialogHint`,
|
|
761
|
+
translated in all ten locale packs).
|
|
762
|
+
- The dashboard's `export_dashboard_pdf` action no longer toasts "Preparing PDF export…" —
|
|
763
|
+
it names the print dialog it actually opens (`dashboardActions.pdfPreparing` is replaced by
|
|
764
|
+
`dashboardActions.printDialogOpening`).
|
|
765
|
+
|
|
766
|
+
No control was removed and no headless detection was added. A real print/PDF primitive
|
|
767
|
+
remains out of scope (`objectstack-ai/objectstack#1301`, closed NOT_PLANNED).
|
|
768
|
+
|
|
769
|
+
- ff84b05: Stop the report config panel being titled "Title", and the view-settings colour section "Color"
|
|
770
|
+
|
|
771
|
+
Two call sites asked for a key whose value was written for a different slot, so the rendered copy was wrong (objectui#4118, surfaced by objectui#3810's census).
|
|
772
|
+
|
|
773
|
+
`ReportConfigPanel` used `report.editor.title` for both its heading and the accessible name of its `role="complementary"` landmark. That key is the label of the report's Title _field_ — `report.editor.titlePlaceholder` ('e.g. Pipeline by Quarter') sits directly under it in the pack. So the panel was headed "Title", and a screen reader announced a complementary region named "Title", which says nothing about what the region is. A new `report.editor.panelTitle` ('Edit report' — what the call site's own dead fallback said before objectui#3810 aligned it to the pack) now names the panel, in all ten locale packs.
|
|
774
|
+
|
|
775
|
+
`ViewSettingsPopover`'s colour section used `list.color`. On the wide toolbar `ListView` already uses both keys correctly for the two slots of this one feature: the compact `Paintbrush` button is `list.color` ('Color') and the panel it opens is headed `list.rowColor` ('Row Color'). This popover is that same panel on the collapsed/`compactToolbar` surface, so it now takes `list.rowColor` — an existing key, no pack change.
|
|
776
|
+
|
|
777
|
+
No `en` value of an existing key changed; `scripts/check-i18n-en-drift.mjs` reports 0 en values changed, 1 key added.
|
|
778
|
+
|
|
779
|
+
## 17.4.0
|
|
780
|
+
|
|
781
|
+
### Minor Changes
|
|
782
|
+
|
|
783
|
+
- bd863fe: fix(timeline): the timeline binds to the date axis the view actually declares (#3129)
|
|
784
|
+
|
|
785
|
+
A view whose date axis is bound under `calendar` was **offered** the Timeline
|
|
786
|
+
visualization and then bucketed every record into "No date" — while the calendar
|
|
787
|
+
rendered the very same field correctly. Two read-sites disagreed about what
|
|
788
|
+
counts as a timeline binding:
|
|
789
|
+
|
|
790
|
+
- `ListView`'s capability gate accepted `options.calendar.startDateField` as a
|
|
791
|
+
timeline-resolvable axis; the render branch never read calendar config at all,
|
|
792
|
+
so it fell through to its `created_at` last resort.
|
|
793
|
+
- `app-shell`'s object page emitted `startDateField: 'due_date'` into
|
|
794
|
+
`options.timeline` for **every** object view, declared or not. Downstream that
|
|
795
|
+
is indistinguishable from a real binding, and because it is always present it
|
|
796
|
+
shadowed the fallback entirely.
|
|
797
|
+
|
|
798
|
+
`ListView` now resolves the axis once — `resolveTimelineDateBinding`, consumed by
|
|
799
|
+
the capability gate and the render branch alike, reading spec key before legacy
|
|
800
|
+
alias and `timeline` before `calendar` in both nestings — and the object page
|
|
801
|
+
forwards only what the view declared. A declared `timeline.startDateField` still
|
|
802
|
+
wins wherever both appear, and a view that declares no date axis anywhere keeps
|
|
803
|
+
the historical `created_at` fallback.
|
|
804
|
+
|
|
805
|
+
Observable rendering change (records move out of "No date" into real date
|
|
806
|
+
buckets), hence `minor`.
|
|
807
|
+
|
|
808
|
+
### Patch Changes
|
|
809
|
+
|
|
810
|
+
- e06810e: `PageComponentSchema.dataSource` is now consumed instead of discarded — a
|
|
811
|
+
`list-view` page component can reference a **saved view by name** for the first
|
|
812
|
+
time, and writing the binding no longer breaks the component
|
|
813
|
+
(objectstack#5576).
|
|
814
|
+
|
|
815
|
+
The spec declares a per-element data binding on every page component —
|
|
816
|
+
`dataSource: { object, view?, filter?, sort?, limit? }` — and objectui read none
|
|
817
|
+
of it. `ViewDataProvider.resolveElementDataSource` forwarded
|
|
818
|
+
`filter`/`sort`/`limit` and dropped `view` entirely, and had no caller outside its
|
|
819
|
+
own test; nothing mapped `object` onto the `objectName` a list actually reads. So
|
|
820
|
+
"reference a saved view by name" was published, validated and inert, and every
|
|
821
|
+
page that wanted a saved view's columns/filter/sort had to inline a second copy of
|
|
822
|
+
them — the drift the binding exists to remove.
|
|
823
|
+
|
|
824
|
+
Writing the binding also **broke** the block, for a reason unrelated to `view`:
|
|
825
|
+
`SchemaRenderer` spread the schema's `dataSource` metadata onto the component as a
|
|
826
|
+
React prop, and that is the prop name the host uses to inject the data-source
|
|
827
|
+
ADAPTER. The plain `{ object, view }` object shadowed the adapter, so the first
|
|
828
|
+
`dataSource.find(…)` threw `dataSource.find is not a function` and `list-view`
|
|
829
|
+
rendered "Couldn't load records" — a spec-compliant component failing next to
|
|
830
|
+
identical ones that omitted the binding.
|
|
831
|
+
|
|
832
|
+
- `@object-ui/react` — `SchemaRenderer` no longer spreads `schema.dataSource` as a
|
|
833
|
+
prop (it is metadata, like `visibleWhen`); renderers read it off `schema`. An
|
|
834
|
+
explicit React `dataSource` prop is unaffected. New
|
|
835
|
+
`useElementDataSource(schema, dataSource?)` hook resolves a binding, fetching
|
|
836
|
+
the named saved view from the object definition's `listViews` and the metadata
|
|
837
|
+
overlay's `listViews()`.
|
|
838
|
+
- `@object-ui/core` — new `isElementDataSourceConfig` / `collectSavedViews` /
|
|
839
|
+
`resolveSavedView` / `composeElementDataSource`, and `resolveElementDataSource`
|
|
840
|
+
now honours `view` through an optional `DataFetcher.fetchViews`, reporting an
|
|
841
|
+
unresolvable view as an error instead of silently returning every record.
|
|
842
|
+
`resolveViewId` moved here from `@object-ui/app-shell` (re-exported there) so
|
|
843
|
+
one matcher serves both the object page and a page component.
|
|
844
|
+
- `@object-ui/plugin-list` — `list-view` maps the binding onto the props
|
|
845
|
+
`ListView` reads. `dataSource.*` keys are authoritative, view-supplied values
|
|
846
|
+
are a baseline the component's own keys override, and `filter` AND-combines at
|
|
847
|
+
every level (the spec calls the binding's filter "additional criteria"), so a
|
|
848
|
+
binding can narrow a saved view but never widen it. A `view` name that does not
|
|
849
|
+
resolve renders a configuration error naming the object's actual views and
|
|
850
|
+
issues no query — it never falls back to the object's default view, because that
|
|
851
|
+
turns a typo into a silently wider answer.
|
|
852
|
+
|
|
853
|
+
- aeb8424: List row Edit/Delete, bulk delete and related-list CRUD now run the caller's own permission, not just the object's API exposure (objectui#4096)
|
|
854
|
+
|
|
855
|
+
The row kebab's built-in Edit/Delete rendered for every account, including ones
|
|
856
|
+
the server answers `403 PERMISSION_DENIED` on. Clicking Edit opened a fully
|
|
857
|
+
prefilled dialog that could only fail on save; Delete — a destructive entry —
|
|
858
|
+
sat one click away from users who could never perform it.
|
|
859
|
+
|
|
860
|
+
The gate intersected the object's resolved CRUD affordance with the server's
|
|
861
|
+
effective API operation set (`/me/permissions` `apiOperations`, objectui#3720),
|
|
862
|
+
and nothing else. `apiOperations` is the object's **API exposure surface** —
|
|
863
|
+
"which verbs does this object publish" — and the spec's own describe text says
|
|
864
|
+
so. It is principal-independent: the report measured two accounts with opposite
|
|
865
|
+
`allowEdit`, 30 shared objects, and **30/30 identical** `apiOperations`. A gate
|
|
866
|
+
made only of object-scoped layers therefore fails OPEN for every unprivileged
|
|
867
|
+
caller, which is why the same screen carried three different answers to "may
|
|
868
|
+
this user write this object": the toolbar's New was correctly hidden
|
|
869
|
+
(`affordances.create && can(obj, 'create')`), the record header's Edit/Delete
|
|
870
|
+
were correctly hidden (per-record write probe), and the row kebab was not.
|
|
871
|
+
|
|
872
|
+
Four surfaces now AND the principal's own verdict — `can(obj, 'update' |
|
|
873
|
+
'delete')`, i.e. `/me/permissions` `allowEdit` / `allowDelete`, the toolbar's
|
|
874
|
+
source — on top of the layers they already had:
|
|
875
|
+
|
|
876
|
+
- the grid row kebab's built-in Edit/Delete (`resolveRowCrudAffordances` gained
|
|
877
|
+
`permissionUpdate` / `permissionDelete`, filled at the `ObjectGrid` call site);
|
|
878
|
+
- the grid's bulk-delete bar, which rides the same object-level delete verdict,
|
|
879
|
+
so the row gate and the more destructive bulk entry move together;
|
|
880
|
+
- the non-grid (kanban / calendar / gallery) bulk bar `ListView` renders itself;
|
|
881
|
+
- the related-list Create/Edit/Delete on a child object
|
|
882
|
+
(`RelatedRecordActionsBridge`), which had the same object-only gate.
|
|
883
|
+
|
|
884
|
+
**This is a tightening of the intersection, not a swap.** Every existing layer
|
|
885
|
+
stays: the ADR-0103 lifecycle bucket, `userActions.edit` / `delete`, and
|
|
886
|
+
`apiOperations`. A permission grant cannot re-open what any of them closed, and
|
|
887
|
+
none of them survives a permission denial.
|
|
888
|
+
|
|
889
|
+
Fail-open is preserved where it is the deliberate contract: `usePermissions()`
|
|
890
|
+
with no `PermissionProvider` answers `can: () => true`, so standalone embeds and
|
|
891
|
+
hosts that ship no permission source keep their Edit/Delete exactly as before.
|
|
892
|
+
Under `MePermissionsProvider` the semantics are the toolbar's, unchanged and now
|
|
893
|
+
shared: an authenticated principal whose object is absent from
|
|
894
|
+
`/me/permissions.objects` resolves fail-closed (objectui#2926 ④), an anonymous
|
|
895
|
+
session keeps the permissive default, and children never render while the
|
|
896
|
+
permission set is loading. Per-key absence is still permissive — an object entry
|
|
897
|
+
without `allowEdit` reads as allowed.
|
|
898
|
+
|
|
899
|
+
Server-side enforcement was already hard (403, DB unchanged), so this closes a
|
|
900
|
+
UI-affordance gap rather than an authorization hole.
|
|
901
|
+
|
|
902
|
+
- cb5e32d: `UserFilters` preset tab buttons no longer submit an enclosing form; all six buttons declare `type="button"`
|
|
903
|
+
|
|
904
|
+
An HTML `<button>` defaults to `type="submit"` inside a `<form>`, so a preset
|
|
905
|
+
filter tab (`filter-tab-*`, tabs mode) submitted the enclosing form on every
|
|
906
|
+
click. The three buttons objectstack#6952 named now declare `type="button"`
|
|
907
|
+
explicitly — the dropdown chip trigger (`filter-badge-*`), the overflow trigger
|
|
908
|
+
(`user-filters-more`) and the preset tab — joining the session-tab buttons that
|
|
909
|
+
objectstack#5236 already declared it on.
|
|
910
|
+
|
|
911
|
+
Only one of the three was actually at risk, and the difference is measured
|
|
912
|
+
rather than assumed. The chip and the overflow trigger are
|
|
913
|
+
`PopoverTrigger asChild` children, and Radix's `PopoverTrigger` renders
|
|
914
|
+
`Primitive.button type="button"`; its Slot merges that onto a child declaring no
|
|
915
|
+
`type` of its own, so both already rendered as `button`. Reverting the change
|
|
916
|
+
confirms it: those two keep reading `button`, the plain preset tab button reads
|
|
917
|
+
`null`. For the two triggers this therefore moves a contract out of an upstream
|
|
918
|
+
implementation detail and into local source — the same reasoning objectui#3344
|
|
919
|
+
wrote onto the Combobox trigger — while the preset tab is a real fix.
|
|
920
|
+
|
|
921
|
+
Dormant rather than live: the only mount point today is `ListView`'s toolbar,
|
|
922
|
+
which is not inside a form, so no shipped screen submitted anything. The new
|
|
923
|
+
tests pin every rendered `UserFilters` button, in both modes, so a future button
|
|
924
|
+
cannot land at the submit default and an upstream Radix change surfaces in this
|
|
925
|
+
package's tests instead of in a user's form.
|
|
926
|
+
|
|
927
|
+
The in-file comment claiming "a Radix trigger keeps the HTML default of `submit`"
|
|
928
|
+
is corrected in passing — it is the inaccuracy that propagated into
|
|
929
|
+
objectstack#6952's premise.
|
|
930
|
+
|
|
931
|
+
- cf5be4e: `userFilters` tabs: the `allowAddTab` button now adds a tab instead of doing nothing (objectstack#5236)
|
|
932
|
+
|
|
933
|
+
The affordance `allowAddTab` renders had hover styling and `title="Add filter tab"` but no `onClick`, and `TabFilters` took no add-tab callback at all — a control that looked fully clickable and did nothing, which disguises "not implemented" as "a bug where clicking does nothing". That mattered more once objectstack#5073 promoted `allowAddTab` into the spec's `UserFiltersSchema`: the key became discoverable through JSON Schema, the Studio SchemaForm and the reference docs, so an author writing `allowAddTab: true` gets a declaration the runtime did not honour.
|
|
934
|
+
|
|
935
|
+
Clicking it now opens a small naming popover (the same Popover primitive the filter chips and the "More" overflow already use). Confirming a name adds a tab to the same bar as the presets, carrying a snapshot of the conditions applied at that moment, and selects it. Session tabs also carry a remove affordance; authored presets deliberately do not, since those are metadata. Removing the active session tab re-selects the author's default with the same precedence the initial mount uses, so the bar is never left with no active tab while the removed tab's conditions stay applied.
|
|
936
|
+
|
|
937
|
+
The new tab is **session-scoped, held in component state** — no `sys_metadata` write, no API call, no web storage, per ADR-0047 ("an end user's filter choices are session-scoped and never become metadata"). `sessionStorage` was available and deliberately not used: `UserFilters` receives no object or view identity, so any storage key it could invent would be shared by every list in the browser tab, surfacing one list's ad-hoc tabs on another's bar. Persistence beyond the mount, if ever wanted, belongs to the host that already owns the session channel for filter selections (`onSelectionsChange` mirrored into `uf_*` URL params) and can key it by view. The synthetic tab id is reported through `onSelectionsChange` like any other tab switch, so a host mirroring it into the URL hands it back on the next mount, where the existing id check finds no such tab and falls back to the author's default.
|
|
938
|
+
|
|
939
|
+
No public API change: `UserFiltersProps` is untouched, and `allowAddTab: false` / an omitted `allowAddTab` still render no affordance at all.
|
|
940
|
+
|
|
3
941
|
## 17.3.0
|
|
4
942
|
|
|
5
943
|
### Patch Changes
|
|
@@ -19,6 +957,7 @@
|
|
|
19
957
|
stayed invisible.
|
|
20
958
|
|
|
21
959
|
## What changed
|
|
960
|
+
|
|
22
961
|
- Both reads now share one `resolveCoverUrl`, so the "does anything have a
|
|
23
962
|
cover?" predicate and the per-card render can no longer disagree — that
|
|
24
963
|
disagreement is what collapsed the area for records that did have a cover.
|
|
@@ -1383,6 +2322,7 @@ colorField, scale }` arrived with only `titleField` and an axis pinned to the
|
|
|
1383
2322
|
platform primitive for "no records / no data" states. Two new props
|
|
1384
2323
|
keep it flexible enough to absorb the hand-rolled variants that lived
|
|
1385
2324
|
in `plugin-list`, `plugin-kanban`, and `plugin-dashboard`:
|
|
2325
|
+
|
|
1386
2326
|
- `showIcon?: boolean` — drops the icon container entirely. Used by the
|
|
1387
2327
|
kanban board-level empty banner, which is a status banner rather than
|
|
1388
2328
|
a true empty-state.
|
|
@@ -1391,6 +2331,7 @@ colorField, scale }` arrived with only `titleField` and an axis pinned to the
|
|
|
1391
2331
|
empty state, which uses a large standalone glyph).
|
|
1392
2332
|
|
|
1393
2333
|
Adopters:
|
|
2334
|
+
|
|
1394
2335
|
- `plugin-list` (`ListView` grid empty-state) — preserves the existing
|
|
1395
2336
|
large icon, title, message, add-record button and `data-testid`s,
|
|
1396
2337
|
but delegates the structural markup to `DataEmptyState`.
|
|
@@ -1467,6 +2408,7 @@ colorField, scale }` arrived with only `titleField` and an axis pinned to the
|
|
|
1467
2408
|
chrome on phones: title + 1 primary action, plus content. We were
|
|
1468
2409
|
shipping ~5 rows of toolbars + chips + tabs above the data. This commit
|
|
1469
2410
|
hides the desktop-only chrome at the `<sm` breakpoint:
|
|
2411
|
+
|
|
1470
2412
|
- **ListView**: TabBar (view switcher), UserFilters chip row, quick-filters
|
|
1471
2413
|
chip row, Sort button, list-scoped Search popover, and the
|
|
1472
2414
|
(newly-added) mobile-only ViewSettingsPopover gear are all hidden on
|
|
@@ -1488,6 +2430,7 @@ colorField, scale }` arrived with only `titleField` and an axis pinned to the
|
|
|
1488
2430
|
|
|
1489
2431
|
Refactor mobile object-view layout to match the Airtable Interface
|
|
1490
2432
|
pattern:
|
|
2433
|
+
|
|
1491
2434
|
- **AppHeader**: the mobile topbar's static page label is now a
|
|
1492
2435
|
view-switcher dropdown (`<viewName> ▾`). Tapping opens a list of
|
|
1493
2436
|
available views with icons + active-state checkmark. Falls back to
|
|
@@ -1618,6 +2561,7 @@ colorField, scale }` arrived with only `titleField` and an axis pinned to the
|
|
|
1618
2561
|
Library builds (vite lib mode) now externalize every non-relative import instead of bundling third-party CJS dependencies into the published dist. This avoids inlined `require("react")` / `require("react-dom")` calls that cause `Calling \`require\` for "react" in an environment that doesn't expose the \`require\` function` runtime errors when consumer apps re-bundle the published dist.
|
|
1619
2562
|
|
|
1620
2563
|
Specifically fixes:
|
|
2564
|
+
|
|
1621
2565
|
- `@object-ui/plugin-dashboard` no longer inlines `react-grid-layout` (and its transitive `react-draggable` / `react-resizable` CJS bundles). `react-grid-layout` is now declared as a peer dependency so consumers install a single ESM-friendly copy.
|
|
1622
2566
|
- `@object-ui/components`, `@object-ui/plugin-calendar`, `@object-ui/plugin-charts`, `@object-ui/plugin-designer` no longer inline `react-i18next` / `i18next` / `use-sync-external-store` CJS shims.
|
|
1623
2567
|
- All plugin packages now use a unified `external: (id) => !/^[./]/.test(id) && !id.startsWith(__dirname)` rule, ensuring future additions of CJS deps are automatically externalized.
|