@object-ui/data-objectstack 16.0.0 → 17.1.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 CHANGED
@@ -1,5 +1,716 @@
1
1
  # @object-ui/data-objectstack
2
2
 
3
+ ## 17.1.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 9b773f9: fix(analytics): a missing analytics capability no longer renders as an empty KPI — objectstack#3891
8
+
9
+ The framework retired its degraded in-kernel analytics fallback (objectstack#3891):
10
+ it dropped the caller's RLS/tenant scope and ignored the contract filter, so it
11
+ answered `200` with over-broad numbers. `@objectstack/service-analytics` is now
12
+ the only implementation, and a deployment without it answers `404` on
13
+ `/analytics/query` (objectstack#4019 stops mounting the routes) or `501` on
14
+ `/analytics/dataset/query`.
15
+
16
+ Three things were wrong on this side of that boundary:
17
+
18
+ **① A KPI on such a deployment rendered a confident zero.** `aggregate()`'s
19
+ `catch` promises a client-side fallback, and the fallback is correct — but the
20
+ adapter never got there for the most likely failure. It now classifies the
21
+ failure (`classifyAnalyticsFailure`) instead of treating every error alike:
22
+ capability-absent (404/501) degrades to a client-side aggregate over a
23
+ **server-scoped** `find()` — same rows, same filter, RLS still applied — and
24
+ says so **once per adapter** in the console, naming the package to install,
25
+ rather than once per widget or not at all.
26
+
27
+ **② A rejected query was answered with plausible numbers.** The framework
28
+ validates `/analytics/query` at the entry now (objectstack#4010), so a `400
29
+ VALIDATION_FAILED` means _this adapter_ sent an off-contract body. Degrading
30
+ there would bury our own bug behind output from a different code path — the
31
+ misdirection objectstack#3878 documented. It now throws
32
+ `AnalyticsQueryRejectedError` and never falls back. Transient failures (5xx,
33
+ network) degrade exactly as before.
34
+
35
+ **③ The dataset preview blamed the author for a missing capability.**
36
+ `queryDataset` mapped `501`/`404` to `Dataset query failed: 501 Not Implemented
37
+ — …`; it now throws the typed `AnalyticsNotInstalledError`
38
+ (`code: 'ANALYTICS_NOT_INSTALLED'`) with a message a UI can render verbatim, and
39
+ `DatasetPreview` shows it as a "analytics capability not installed" empty state
40
+ instead of a red error banner. A real compile error (e.g. "relationship not
41
+ declared in include") keeps its server detail and its banner.
42
+
43
+ New exports from `@object-ui/data-objectstack`: `AnalyticsNotInstalledError`,
44
+ `AnalyticsQueryRejectedError`, `isAnalyticsNotInstalledError`,
45
+ `classifyAnalyticsFailure`.
46
+
47
+ - 1cf0de7: fix(detail): finish the approval-lock story, and warn on silently stripped fields (framework#3794)
48
+
49
+ The Console reported record writability wrong in both directions during an
50
+ approval, so a user had nothing to go on: what they _could_ edit said "locked",
51
+ and what they _couldn't_ said "updated successfully".
52
+
53
+ **The lock band told the truth; the Edit button did not.** objectui#2902 split
54
+ the band into "in approval · editable" vs locked, but the header **Edit** CTA
55
+ still keyed off nothing at all — on a genuinely locked record it stayed live, so
56
+ the user opened the form, filled a screen, and got `RECORD_LOCKED` back on Save.
57
+ It is now `disabled` on a locked record: visible-but-off, with the band beside it
58
+ saying why. This is the LOCK, not the mere presence of an approval — a
59
+ `lockRecord: false` node keeps Edit live, which is the point of that setting.
60
+
61
+ **And the band could still re-lock itself.** `DetailView` OR-ed the record's own
62
+ `approval_status` mirror into `isLocked` unconditionally. That mirror is written
63
+ on submit by any flow configuring an `approvalStatusField`, _regardless of_
64
+ `lockRecord` — so on a `lockRecord: false` node the host correctly resolved "not
65
+ locked" from the request's `lock_record` while the mirror dragged the band back
66
+ to "Locked for approval", with the pencils live and saves landing underneath it.
67
+ The host is now authoritative whenever it threads `approvalPending`; the mirror
68
+ is consulted only for bare/legacy `DetailView` hosts that thread nothing, where
69
+ it still reads as locked (no node granularity — the safe direction).
70
+
71
+ Recall's tooltip no longer promises to unlock a record the node never locked
72
+ (`detail.cancelApprovalTooltipUnlocked`).
73
+
74
+ **Silently stripped fields now surface on the record form's save path.** The
75
+ adapter emitted a write-warning for `create`/`update` responses carrying
76
+ `droppedFields`, but not for `batchTransaction` — which is how the record form
77
+ saves a master-detail record, i.e. the one surface where a user actually edits a
78
+ `readonlyWhen`-locked field. `batchTransaction` now emits one warning per event,
79
+ resolving each back to its operation via the response's `index`.
80
+
81
+ The toast itself was hardcoded English and called every strip "read-only". It is
82
+ now localized (`detail.writeStripped*`, ten locales) and worded by reason:
83
+ `readonly_when` says the field is not editable _in this record's current state_,
84
+ which is what actually happened — the field is editable in other states and the
85
+ form rendered it as an ordinary input, so "read-only" sent the user hunting for a
86
+ permission problem that does not exist.
87
+
88
+ **And it stopped crying wolf.** `createObjectStackUserStateAdapter` hand-stamped
89
+ the server-managed `updated_at` on every recents/favorites write, which the
90
+ server strips and reports — so the console popped "Some fields were not saved"
91
+ about a field no user ever touched, on page loads, drowning the signal the toast
92
+ exists for. It no longer sends the column; the server stamps it anyway.
93
+
94
+ - 0ded602: fix(form): a server rejection that names fields now marks those fields (objectstack#3896)
95
+
96
+ The server has always said which field it rejected. `@objectstack/objectql`'s
97
+ validators throw `VALIDATION_FAILED` with `fields[]` — one entry per offending
98
+ field, each with a human `message` — and both the REST layer and the runtime
99
+ dispatcher serve that as a 400 with the entries intact.
100
+
101
+ Every form dropped them. The submit handler caught the rejection, ran the
102
+ message through `extractWriteErrorMessage`, and showed **one undirected toast**:
103
+ the user was told something was wrong but not _what_, on a surface that already
104
+ knows how to mark an input — and already does exactly that for client-side
105
+ validation. On a long form the offending field was often off-screen, so "创建"
106
+ appeared to do nothing.
107
+
108
+ **Now the two failures behave identically, because they share one
109
+ implementation.** The per-field marking, the toast naming the fields, and the
110
+ scroll-and-focus of the first offender (#2793) were extracted from the
111
+ client-side invalid handler; the server path calls the same function. As far as
112
+ the person filling in the form is concerned these are the same event — only the
113
+ referee differs.
114
+
115
+ Three layers, each of which was dropping the detail:
116
+
117
+ - **`@object-ui/react`** — new `extractFieldErrors(err)` (exported alongside
118
+ `extractWriteErrorMessage` / `isPermissionError`) normalises the three shapes
119
+ the error can arrive in: a typed `ValidationError` from the ObjectStack
120
+ adapter, the raw `@objectstack/client` error (whose `details` falls back to the
121
+ whole response body, which is where `fields[]` lands), and a hand-rolled error
122
+ carrying `fields` directly — the server duck-types that shape identically, so
123
+ the client must not be pickier than the server. Entries with no usable `field`
124
+ are **dropped rather than guessed at**: marking an innocent input is worse than
125
+ the generic toast.
126
+ - **`@object-ui/data-objectstack`** — `normaliseClientError` now maps a 400
127
+ `VALIDATION_FAILED` onto the `ValidationError` class that has sat in
128
+ `errors.ts` since the package was written, exported and **never once
129
+ constructed**. Its `validationErrors: Array<{ field, message }>` shape was
130
+ already exactly right. `create` also now normalises at all: only `update` did,
131
+ so a rejected insert reached callers as the raw client error — and a create is
132
+ the path that most often trips required-field validation.
133
+ - **`@object-ui/components`** — the form renderer maps the entries onto
134
+ `form.setError` and takes over the failure, **but only when every rejected
135
+ field has a visible input to carry it**. If the server also rejected something
136
+ the form does not render, it falls through to the banner, whose top-level
137
+ message concatenates every field's reason — so the part the user cannot see
138
+ inline is still said out loud instead of silently dropped.
139
+
140
+ This also removes the need for the client-side predicate mirroring added in
141
+ #2962: a form no longer has to guess what the server will reject in order to
142
+ warn about it beforehand, and mirrored predicates drift.
143
+
144
+ Non-field failures (403 / permission denials / anything without `fields[]`) take
145
+ exactly the path they took before.
146
+
147
+ ### Patch Changes
148
+
149
+ - 4952edf: fix(errors): error-code branches survive the framework's ADR-0112 rename — objectstack#3841
150
+
151
+ Framework ADR-0112 renamed the whole `error.code` vocabulary from lowercase
152
+ `snake_case` to `SCREAMING_SNAKE` (`destructive_change` → `DESTRUCTIVE_CHANGE`).
153
+ Eleven places compared `err.code` against the old spelling with `===`, so against
154
+ a swept server they simply stopped matching — and nothing threw. The affordance
155
+ each branch guards just vanished and the user got the generic error toast instead:
156
+
157
+ - the destructive-change confirm dialog (resource editor, permission matrix)
158
+ - the "create a writable package first" hint
159
+ - field-scoped validation issues on embedded item saves
160
+ - the all-or-nothing publish summary naming the causal item
161
+ - unknown-object tolerance in the app header and in record search
162
+ - the marketplace's local-install messages for conflict / auth / unavailable
163
+ - `isNotFoundError` in the data layer
164
+
165
+ `RECORD_NOT_FOUND` had already been renamed a release earlier, so that branch was
166
+ already dead before this fix.
167
+
168
+ New `errorCodeIs` / `errorCodeIsAnyOf` in `@object-ui/types` compare
169
+ case-insensitively, so the console keeps working against servers on either side
170
+ of the rename — the console ships separately from the server it talks to. Every
171
+ call site now passes the catalog (SCREAMING) spelling, and `error-code.ts` is the
172
+ single file to delete once no supported server emits the old vocabulary.
173
+
174
+ - 7f0252e: fix(list,data-objectstack,types): exporting a searched list no longer downloads the unsearched superset
175
+
176
+ The server-streamed export mirrored the view's `filter` and `sort`, and the
177
+ code comment claimed that made the file match the screen:
178
+
179
+ > Mirrors the active view's filter + sort so the exported file matches what the
180
+ > user sees.
181
+
182
+ It mirrored one half. There was no way to carry the term a user had typed into
183
+ the search box — `ExportDownloadRequest` had no field for one — so exporting
184
+ during a search produced **more rows than the list showed**, in a file that
185
+ looks authoritative, with nothing indicating the difference. The client-side
186
+ fallback was always correct (it serializes the already-searched `data`); only
187
+ the server path was wrong, and it is the one that handles xlsx.
188
+
189
+ Same family as a dropped filter (objectstack#3948, objectstack#4181): a
190
+ plausible answer that is quietly broader than the one asked for.
191
+
192
+ - `ExportDownloadRequest` gains `search` / `searchFields`.
193
+ - `ObjectStackAdapter.exportDownload` sends them as `search=` / `searchFields=`,
194
+ trimming the term and omitting both when it is blank (`searchFields` alone
195
+ means nothing).
196
+ - `ListView` passes the active `searchTerm` and the view's `searchableFields`,
197
+ and both are now in the export callback's dependency array — a stale closure
198
+ would export the wrong row set.
199
+
200
+ Requires a server with objectstack#4230. Older servers ignore unknown query
201
+ params on this route, so they keep today's behaviour rather than erroring.
202
+
203
+ **Also: the filter merge is no longer written twice.** The three filter sources
204
+ (view filter, filter-panel group, per-field user filters) were merged by
205
+ verbatim copies in the data fetch and in the export — two copies that must
206
+ agree, deciding respectively what the user _sees_ and what they _download_.
207
+ Both now call `buildEffectiveFilter`. This is a pure extraction: the copies did
208
+ agree, and the four parity tests added for it pass against the old code too.
209
+ They exist to keep it that way — the adapter's duplicated filter-shape check
210
+ had already drifted apart unnoticed (#3072).
211
+
212
+ - 7d35010: fix(data-objectstack): a view's own filter no longer disappears when the user adds one
213
+
214
+ `ObjectStackAdapter` translated object-form filter entries
215
+ (`[{ field, operator, value }, ...]`) only at the **top level** of a `$filter`.
216
+ The moment a list has both a stored view filter and a user filter, it builds
217
+
218
+ ```js
219
+ [
220
+ "and",
221
+ [{ field: "stage", operator: "eq", value: "won" }],
222
+ [["amount", ">", 1]],
223
+ ];
224
+ ```
225
+
226
+ whose head is the string `and`, so the old check called the whole thing
227
+ "already AST" and shipped the rules untranslated. Both server answers to that
228
+ are wrong:
229
+
230
+ ```js
231
+ isFilterAST(above); // false — a bare rule object is not an AST child
232
+ parseFilterAST(above); // { amount: { $gt: 1 } } ← `stage = won` is GONE
233
+ ```
234
+
235
+ Since objectstack#4121 the `isFilterAST` gate turns it into a **400 and the
236
+ list fails to load**. Before it — or anywhere `parseFilterAST` is reached
237
+ without that gate — **the view's own condition is dropped without a word** and
238
+ the list returns records the view exists to exclude.
239
+
240
+ Translation is now recursive through `and`/`or` nodes and legacy flat child
241
+ arrays, so the shape reaches the server as a valid AST
242
+ (`{$and: [{stage: 'won'}, {amount: {$gt: 1}}]}`).
243
+
244
+ Three related fixes in the same code:
245
+
246
+ - **An untranslatable entry is now an error, not an omission.** Entries that
247
+ failed to translate were dropped, and dropping one conjunct of an `and`
248
+ returns a _superset_ of the rows asked for — dropping the last one sent no
249
+ `filter=` at all, so the whole table came back. `find()` now throws
250
+ `MalformedFilterError`, carrying `code: 'INVALID_FILTER'` / `httpStatus: 400`
251
+ so a failed list renders "the filter is malformed" rather than "check your
252
+ connection". A rule with a blank `field` passes `ViewFilterRuleSchema`
253
+ (`z.string()` admits `''`), so this is reachable from real stored metadata.
254
+ A _mixed_ array (`[{ field, operator, value }, ['amount', '>', 1]]`) now
255
+ keeps both halves instead of dropping the tuple — that case was a lost
256
+ condition, not a malformed one.
257
+ - **The two `find()` routes can no longer disagree.** The "is this object
258
+ form?" test existed twice — once in `translateFilterToAST`, once inline in
259
+ `convertQueryParams` — and the copies had already drifted: the inline one
260
+ omitted a `!== null` guard, so a `$filter` of `[null]` threw a `TypeError` on
261
+ the plain route while the same value was handled on the `$expand` route. One
262
+ definition now serves both.
263
+ - **Dropped an unreachable `entry.name` fallback.** `objectFilterEntryToAST`
264
+ read `entry.field ?? entry.name` while the shape check keyed on `field`
265
+ alone, so the `name` half was dead from the commit that introduced it. The
266
+ spec agrees it is not a real shape — `ViewFilterRuleSchema.field` is
267
+ required, so such a rule cannot be saved as view metadata.
268
+
269
+ Refs objectstack#3948, objectstack#4121, #2945
270
+
271
+ - c4d7b20: fix(view,list,core): a view's filter no longer disappears, or arrives as a predicate on columns that don't exist
272
+
273
+ Sweeping the other `$filter` producers after #3078 turned up two live defects in
274
+ `ObjectView`, which fetches its own data for calendar / kanban / gallery /
275
+ timeline (grid delegates to `ObjectGrid`).
276
+
277
+ **1. An object filter was dropped, and only for non-grid views.**
278
+ `table.defaultFilters` is declared `Record<string, any>`, and the merge tested
279
+ `baseFilter.length > 0` — `undefined > 0` for an object. So the filter vanished
280
+ and the view returned **every record**. `ObjectGrid` assigns the same value
281
+ straight to `params.$filter`, so one view definition filtered correctly as a
282
+ grid and returned everything as a calendar.
283
+
284
+ **2. Rule objects were spread into the `and`, not wrapped.**
285
+ `['and', ...baseFilter, ...userFilter]` is only correct when the source is an
286
+ array of AST nodes. `activeView.filter` is a spec `ViewFilterRule[]`, so
287
+ spreading put bare rule objects where the AST expects nodes:
288
+
289
+ ```js
290
+ isFilterAST([
291
+ "and",
292
+ { field: "stage", operator: "eq", value: "won" },
293
+ ["owner", "=", "me"],
294
+ ]);
295
+ // false → 400 since objectstack#4121
296
+ parseFilterAST(same);
297
+ // {$and:[{field:'stage',operator:'eq',value:'won'}, {owner:'me'}]}
298
+ ```
299
+
300
+ That second line is a predicate over three columns named `field`, `operator`
301
+ and `value` — which don't exist.
302
+
303
+ > **Correction.** The first version of this note said the spread was "reachable
304
+ > whenever a view with a filter meets a user filter value". That was wrong for
305
+ > `ObjectView`: the branch required a non-empty user filter, and nothing ever
306
+ > wrote the state it was built from, so it could never run. The shape is
307
+ > genuinely broken — a live server answers it with a 400 — and the adapter-level
308
+ > defence added alongside is still warranted for any producer that emits it, but
309
+ > **this particular site was dead code, not a live defect.** Defect 1 above was
310
+ > live: it sat on the always-taken path. The dead machinery behind the wrong
311
+ > claim is removed in a follow-up.
312
+
313
+ New in `@object-ui/core`: `toFilterNode` normalizes one source (rule array / AST
314
+ / MongoDB object) and `mergeFilterNodes` combines sources as siblings under one
315
+ `and`. `ObjectView` and `ListView.buildEffectiveFilter` both use them, so the
316
+ three filter shapes are reconciled in one place instead of by hand at each
317
+ renderer.
318
+
319
+ `ObjectStackAdapter` also now translates a bare rule object sitting directly
320
+ under a logical node — the chokepoint defence for any producer still emitting
321
+ the spread shape. Only rule-_shaped_ objects are touched; a child with no
322
+ `field` is a genuine MongoDB condition and passes through untouched.
323
+
324
+ **Correcting a comment shipped in #3078.** `buildEffectiveFilter` documented the
325
+ dropped-object case as unreachable, "nothing in this repo produces one for a
326
+ list view". That was wrong: `ObjectView` passes `mergedFilters` straight into
327
+ that schema's `filter`, and its last fallback is `table.defaultFilters`. The
328
+ case is now handled rather than explained away.
329
+
330
+ Verified with 19 tests across the four packages; reverting each source file
331
+ fails the ones that cover it. Emitted filters are asserted against the spec's
332
+ own `isFilterAST` / `parseFilterAST`, including an executable pin on what the
333
+ old spread shape produced.
334
+
335
+ - ad0183a: fix(data-objectstack,core): an object filter no longer depends on whether the query expands a lookup
336
+
337
+ #3072 single-sourced the ARRAY branch of the adapter's two `find()` routes. The
338
+ object branch was left as it was: `convertQueryParams` converted a MongoDB-style
339
+ filter to AST while `translateFilterToAST` returned it verbatim — so the same
340
+ `$filter` went out in two formats, decided by whether the query happened to
341
+ expand a lookup.
342
+
343
+ Measured across 21 operator shapes, **four diverged**. Most of the gap turned
344
+ out to be harmless — `{$and: […]}` survives the plain route as a
345
+ `['$and','=',[…]]` comparison that `parseFilterAST` reads back as a real `$and`,
346
+ and `$exists` vs `$null` is a difference the server treats identically. Two were
347
+ not harmless:
348
+
349
+ - **The unknown-operator guard only ran on one route.** `convertFiltersToAST`
350
+ throws on an unrecognised operator, with a comment saying it does so "to avoid
351
+ silent failure" — but the expanded route never called it, so a typo'd operator
352
+ threw on a plain read and shipped silently whenever a lookup was expanded.
353
+ - **`$regex` was silently rewritten to `contains`.** `$regex: 'a.c'` matches
354
+ "abc"; `contains 'a.c'` matches only those three literal characters. That is a
355
+ _different question_, not a weaker version of the same one, and neither result
356
+ looks wrong on screen. The rewrite sat behind a `console.warn`, which is not
357
+ an error channel in a deployed app — and the function's own unknown-operator
358
+ message never listed `$regex` among the supported set. The spec has no
359
+ `$regex` (`FILTER_OPERATORS`, `data/filter.zod.ts`), so there is nothing to
360
+ translate it into: it is now refused, the same treatment the neighbouring
361
+ unknown operator already got. Nothing in the repo depended on the conversion.
362
+
363
+ Both refusals now throw `FilterOperatorError`, carrying `code: 'INVALID_FILTER'`
364
+ / `httpStatus: 400`. The pre-existing unknown-operator throw was a bare `Error`,
365
+ which `classifyLoadError` classifies as a network fault — so a malformed filter
366
+ told the user to check their connection (#3066), the one thing it definitely
367
+ was not.
368
+
369
+ - a17ef09: fix(data-objectstack): a string `$orderby` reaches the server as a sort instead of a list of character indices — #3106
370
+
371
+ `QueryParams['$orderby']` declares four shapes — `string`, `string[]`,
372
+ `SortNode[]`, `Record<field, direction>`. Both of this adapter's `find()` routes
373
+ (`convertQueryParams` for a plain read, `rawFindWithPopulate` for one carrying
374
+ `$expand`/`$search`) carried their own copy of the fold that serializes it, and
375
+ both copies handled the same three. The bare string fell through to the
376
+ `Record` branch, where `Object.entries('name asc')` enumerates the string's
377
+ character indices — so the request went out as `sort=0,1,2,3,4,5,6,7`.
378
+
379
+ Since `objectstack#4226` the server refuses a sort it cannot read
380
+ (`400 INVALID_SORT`) rather than dropping it silently, so this was not a
381
+ degraded ordering but a list that failed to load outright — and `"${field}
382
+ ${order}"` is exactly the shape `ObjectGrid` builds from its view metadata's
383
+ `sort`, making every standalone grid with a configured sort a broken one.
384
+
385
+ Both routes now share one exported `serializeOrderBy`, for the same reason the
386
+ filter path already shares one: two copies of a fold can only agree by
387
+ inspection, and these two did not.
388
+
389
+ - Updated dependencies [62311b6]
390
+ - Updated dependencies [9e7349e]
391
+ - Updated dependencies [8864971]
392
+ - Updated dependencies [b41f401]
393
+ - Updated dependencies [19e9fa0]
394
+ - Updated dependencies [95b7214]
395
+ - Updated dependencies [7d9734d]
396
+ - Updated dependencies [6ae818e]
397
+ - Updated dependencies [746dd00]
398
+ - Updated dependencies [aebfa4f]
399
+ - Updated dependencies [38ca8be]
400
+ - Updated dependencies [4952edf]
401
+ - Updated dependencies [7f0252e]
402
+ - Updated dependencies [c4d7b20]
403
+ - Updated dependencies [7639a61]
404
+ - Updated dependencies [94e63ef]
405
+ - Updated dependencies [02aef0c]
406
+ - Updated dependencies [6f29aa5]
407
+ - Updated dependencies [c4db402]
408
+ - Updated dependencies [5319bf1]
409
+ - Updated dependencies [49e5671]
410
+ - Updated dependencies [b5b97e2]
411
+ - Updated dependencies [f59f2c1]
412
+ - Updated dependencies [4874117]
413
+ - Updated dependencies [ad0183a]
414
+ - Updated dependencies [ce08d55]
415
+ - Updated dependencies [aa1240a]
416
+ - Updated dependencies [2374a49]
417
+ - Updated dependencies [390c071]
418
+ - Updated dependencies [d10f526]
419
+ - Updated dependencies [2d5d594]
420
+ - Updated dependencies [ea7f477]
421
+ - Updated dependencies [7f23cd0]
422
+ - Updated dependencies [24e0e0a]
423
+ - Updated dependencies [3a6cf24]
424
+ - Updated dependencies [aa35561]
425
+ - Updated dependencies [03bd53b]
426
+ - Updated dependencies [3c1f321]
427
+ - Updated dependencies [a045a32]
428
+ - Updated dependencies [912496d]
429
+ - Updated dependencies [9867281]
430
+ - @object-ui/core@17.1.0
431
+ - @object-ui/types@17.1.0
432
+
433
+ ## 17.0.0
434
+
435
+ ### Minor Changes
436
+
437
+ - d62fb1f: feat(app-shell): toast when a save silently dropped read-only fields (framework #3431/#3455)
438
+
439
+ The framework now reports fields it LEGALLY stripped from a write (a non-system
440
+ caller can't seed a `readonly` field, a `readonlyWhen` predicate locked it, …)
441
+ via a `droppedFields` payload on the create/update response. Previously the
442
+ console discarded it: a value the user typed into a locked field just vanished on
443
+ save with a success toast and no explanation.
444
+
445
+ - **data-objectstack:** `ObjectStackAdapter` now emits a `WriteWarningEvent`
446
+ after a create/update whose response carried `droppedFields`, exposed through a
447
+ new `onWriteWarning(cb)` subscription (mirrors the existing `onMutation` bus).
448
+ Reads the field structurally, so an older client or a backend that never drops
449
+ is a no-op. New exported types: `WriteWarningEvent`, `WriteWarningListener`,
450
+ `DroppedFieldsEvent`.
451
+ - **app-shell:** `AdapterProvider` subscribes and raises a `toast.warning`
452
+ ("Some fields were not saved — the read-only field … could not be changed"),
453
+ so the strip is visible instead of silent. The write itself still succeeded;
454
+ status/behaviour are unchanged.
455
+
456
+ ### Patch Changes
457
+
458
+ - 8ecf5a6: Command palette (⌘K) now surfaces record search hits from the platform's global
459
+ search endpoint (`GET /api/v1/search`).
460
+
461
+ Previously the palette only ran a per-object `find({ $search })` fanout (the
462
+ metadata-driven ADR-0061 search), which misses records that only the global
463
+ search index knows about — so typing a well-known record name returned no
464
+ records even though `/api/v1/search` served them. `ObjectStackAdapter` now
465
+ exposes a `searchAll(query, { limit, objects })` method that calls the unified
466
+ endpoint, `useRecordSearch` prefers it when present (falling back to the fanout
467
+ otherwise), and the palette renders the resulting record hits grouped by object.
468
+
469
+ - 6e8fd3c: fix(charts): a fieldless `count` aggregate keyed its value column `undefined`, so the chart plotted nothing (framework#3701)
470
+
471
+ framework#3701 pinned down what an OBJECT-bound chart aggregate names its result
472
+ columns — the raw field names it was given (`groupBy` for the category, `field`
473
+ for the value; no `sum_`-style decoration, unlike a dataset measure), plus the
474
+ literal `count` when a `count` omits `field`, which is the alias the engine
475
+ projects `COUNT(*)` under. `os validate` now lints page sources against that
476
+ convention, so the paths that build these rows have to honour it exactly.
477
+
478
+ Three of the four did. The odd one out was `count` — the one function that may
479
+ legitimately omit `field` — because every row builder read `params.field`
480
+ directly:
481
+
482
+ - `aggregateRecords` / `ObjectDataSource.aggregateClientSide` emitted
483
+ `{ [groupBy]: key, [undefined]: value }`, i.e. a column literally named
484
+ `undefined` that no axis binding could ever name;
485
+ - the legacy analytics path was worse: it remapped the server's `count` measure
486
+ onto `params.field` and **deleted** the original key, so the value the server
487
+ did return was thrown away before the chart saw it.
488
+
489
+ All of them now resolve the column through one helper (`aggregateValueKey`) so a
490
+ fieldless count lands under `count`, matching the framework contract. The
491
+ comparison-overlay column is derived from the same key (`count__comparison`
492
+ instead of `undefined__comparison`), and `aggregate.field` is typed optional to
493
+ match the spec's `ChartAggregateSchema`. Charts that name a field are unchanged.
494
+
495
+ - Updated dependencies [1767124]
496
+ - Updated dependencies [8ecf5a6]
497
+ - Updated dependencies [7b35e4b]
498
+ - Updated dependencies [e16ed2d]
499
+ - Updated dependencies [f9bbddb]
500
+ - Updated dependencies [dfd3705]
501
+ - Updated dependencies [2735de6]
502
+ - Updated dependencies [6dee2cb]
503
+ - Updated dependencies [c7cff19]
504
+ - Updated dependencies [cd09a7b]
505
+ - Updated dependencies [f1abf0e]
506
+ - Updated dependencies [f05b84e]
507
+ - Updated dependencies [662bdf9]
508
+ - Updated dependencies [059a052]
509
+ - Updated dependencies [53642d4]
510
+ - Updated dependencies [8aae006]
511
+ - Updated dependencies [d147a13]
512
+ - @object-ui/types@17.0.0
513
+ - @object-ui/core@17.0.0
514
+
515
+ ## 16.1.0
516
+
517
+ ### Minor Changes
518
+
519
+ - 8c1e415: feat(data-objectstack): gate the non-atomic batch fallback on the discovery `transactionalBatch` capability (#2693)
520
+
521
+ `ObjectStackAdapter.batchTransaction` now negotiates atomic cross-object batch
522
+ **declaratively** instead of only probing at runtime. At `connect()` the adapter
523
+ reads `capabilities.transactionalBatch` from `GET /api/v1/discovery`
524
+ (framework #3298 — `declared === enforced`; the server advertises `true` only
525
+ when the `/batch` route is mounted _and_ the runtime engine can honour a
526
+ transaction):
527
+
528
+ - **Declared `true`** — the adapter TRUSTS server atomicity: it calls `/batch`
529
+ and surfaces any failure (including `404`/`405`/`501`) as a real error. No
530
+ runtime probe, no non-atomic client-side compensation.
531
+ - **Declared `false`, or absent** (backend predates #3298) — the legacy path is
532
+ unchanged: probe `/batch` and, on `404`/`405`/`501`, fall back to the
533
+ non-atomic `emulateBatchTransaction`. Keeping this avoids regressing older
534
+ backends from "saves, less safe" to "no save path" (#2679 compat constraint).
535
+
536
+ Both the hierarchical wire shape (`{ transactionalBatch: { enabled: true } }`)
537
+ and the flat form the client SDK normalizes to (`{ transactionalBatch: true }`)
538
+ are accepted. `@object-ui/core`'s generic `emulateBatchTransaction` /
539
+ `runBatchTransaction` are untouched and remain the fallback for adapters with no
540
+ server-side transaction (`ValueDataSource`, `MockDataSource`, …).
541
+
542
+ Docs: the adapter README and the data-source guide now document the capability
543
+ table and the minimum-backend note — atomic cross-object saves are guaranteed
544
+ only against backends advertising the capability (framework #3298 / #1604).
545
+
546
+ Picks up #2679 acceptance item 4; unblocked by framework#3298 (merged).
547
+
548
+ - 62b9ab5: feat(data): unify master-detail saves behind `DataSource.batchTransaction`, isolate the non-atomic fallback in the adapter (#2679)
549
+
550
+ Master-detail saves (`MasterDetailForm`, `LineItemsPanel`) now always persist
551
+ through `dataSource.batchTransaction(operations)` — one ordered cross-object
552
+ operation list, with `{ $ref: <op index> }` linking a child to a parent created
553
+ in the same batch. The form no longer contains any client-side orchestration or
554
+ best-effort compensation-delete; that atomicity anti-pattern is gone from the UI
555
+ layer (framework #1604 / framework ADR-0034 item 4).
556
+
557
+ - **`@object-ui/types`** — `batchTransaction?` is now a first-class (optional)
558
+ method on the `DataSource` contract, typed via `BatchTransactionOperation` /
559
+ `BatchRef`. Replaces the previous `(dataSource as any).batchTransaction`
560
+ method-sniffing.
561
+ - **`@object-ui/core`** — new `emulateBatchTransaction(dataSource, operations)`
562
+ (sequential writes, `$ref` resolution, best-effort reverse-order compensation)
563
+ and `runBatchTransaction(dataSource, operations)` (prefers the adapter's method,
564
+ emulates otherwise). `ApiDataSource` / `ValueDataSource` implement
565
+ `batchTransaction` via the emulation.
566
+ - **`@object-ui/data-objectstack`** — `ObjectStackAdapter.batchTransaction` uses
567
+ the server's atomic `POST /api/v1/batch`, prefers the typed
568
+ `client.data.batchTransaction` SDK method when the installed client exposes it,
569
+ and degrades to the client-side emulation ONLY when the endpoint is missing
570
+ (404/405) or the runtime can't do transactions (501). Real errors (400/401/403/
571
+ 409/500) still surface. This is the isolated, tested home of the non-atomic
572
+ fallback.
573
+ - **`@object-ui/plugin-form`** — removed `applyDetail` / `createMany` /
574
+ `ApplyDetailResult` from `masterDetailTx.ts`; `MasterDetailForm` and
575
+ `LineItemsPanel` build ops and call `runBatchTransaction`. `LineItemsPanel`
576
+ saves are now atomic on a capable backend, with the rollup folded into the same
577
+ batch.
578
+
579
+ No behavior change on a current ObjectStack backend (it has `/api/v1/batch`);
580
+ older/limited backends keep a working — now clearly non-atomic — save path.
581
+
582
+ ### Patch Changes
583
+
584
+ - 8b8b744: chore(deps): align `@objectstack/formula` / `lint` / `client` to `^15.1.1`
585
+
586
+ These three were still pinned to `^14.6.0` while `@objectstack/spec` was already
587
+ `^15.1.1` — a version skew from the v15 upgrade (formula/lint/client publish in
588
+ lockstep with spec, and their own 15.0.0 entries are pure dependency bumps, so
589
+ this is alignment, not a behavioral migration).
590
+
591
+ Practical effect: the client-side field-rule evaluation
592
+ (`visibleWhen`/`readonlyWhen`/`requiredWhen` via `fieldRules.ts`, which delegates
593
+ to `@objectstack/formula`'s `ExpressionEngine`) now tracks the 15.x engine — and
594
+ will pick up the framework's `dateField == today()` equality fix
595
+ (objectstack-ai/objectstack#3205) automatically at the next 15.x release via the
596
+ caret range. Renderer/action `visible`/`disabled` predicates are unaffected (they
597
+ use the home-grown JS evaluator — tracked separately in #2661).
598
+
599
+ - 7cf4051: chore(deps): align every `@objectstack/*` dependency to `^16.0.0-rc.0`
600
+
601
+ Bumps `@objectstack/spec` / `client` / `formula` / `lint` from `^15.1.1` to the
602
+ `16.0.0-rc.0` pre-release across the workspace (root + `apps/console` +
603
+ `apps/site` + all consuming packages). ObjectUI's own packages are already on
604
+ major 16, so this closes the 15↔16 skew between ObjectUI and the `@objectstack`
605
+ contract libraries (which publish in lockstep with `spec`).
606
+
607
+ This is a dependency alignment, not a behavioral migration: the full workspace
608
+ build (43/43) and the `@objectstack`-consuming package test suites
609
+ (`core` / `app-shell` / `data-objectstack` / `plugin-form` / `types`) are green
610
+ against `16.0.0-rc.0` with no source changes required.
611
+
612
+ Practical effect: `@objectstack/client@16.0.0-rc.0` now ships
613
+ `data.batchTransaction` (framework #3271), so `ObjectStackAdapter`'s feature
614
+ detect (`typeof client.data.batchTransaction === 'function'`) routes
615
+ master-detail cross-object saves through the typed SDK method instead of the
616
+ raw `fetch('/api/v1/batch')` fallback — realizing the "verify SDK path" half of
617
+ #2694. The raw-fetch branch stays as a defensive fallback (removal tracked in
618
+ #2694).
619
+
620
+ - 0ea5036: refactor(data-objectstack): route `batchTransaction` through the client SDK only, drop the raw-fetch branch
621
+
622
+ `@objectstack/client@^16` (framework #3271, the current ObjectUI dependency
623
+ floor) ships `data.batchTransaction`, so `ObjectStackAdapter.batchTransaction`
624
+ now calls the typed SDK method directly. The transitional hand-rolled
625
+ `fetch('/api/v1/batch')` branch — a feature-detect shim kept while the SDK
626
+ method was unreleased — is removed (#2694). Per AGENTS.md §7, adapter data
627
+ always flows through `@objectstack/client`, never a raw `fetch`.
628
+
629
+ No behavior change: the SDK still drives the server's atomic `POST /api/v1/batch`,
630
+ one `MutationEvent` is emitted per committed op (no double-fire), and the adapter
631
+ still degrades to the non-atomic `emulateBatchTransaction` when this backend lacks
632
+ the endpoint (404/405) or its runtime can't do transactions (501). Every other
633
+ status still surfaces to the caller.
634
+
635
+ - 549c67d: chore(lint): clear the mechanical baseline lint errors so these packages' lint gates protect them again
636
+
637
+ Extends the fields/core cleanup from #2709 (objectui#2713). These eight package
638
+ lints were red at baseline on `main`, so their per-package `lint` gate could not
639
+ catch new violations of the same class. Cleared every **error** (no behavior
640
+ change; warnings are out of scope):
641
+
642
+ - **`no-useless-catch`** (`data-objectstack`) — unwrapped five try/catch blocks
643
+ whose `catch` only re-threw; errors still propagate identically.
644
+ - **`preserve-caught-error`** (`cli`, `data-objectstack`, `react`) — the caught
645
+ error's message is inlined into the thrown `Error`; a scoped disable with a
646
+ justifying comment carries each one, because these packages target ES2020
647
+ whose lib types the 1-arg `Error` constructor only (so `{ cause }` won't
648
+ compile) — same reasoning as the core case in #2709.
649
+ - **`prefer-const`** (`plugin-calendar`, `plugin-map`) — `let`→`const` for
650
+ never-reassigned bindings.
651
+ - **`no-empty-object-type`** (`plugin-designer`) — empty extend-only interfaces
652
+ → equivalent `type` aliases.
653
+ - **`no-useless-assignment`** (`react`) — dropped a dead initializer that both
654
+ branches overwrite before it is read.
655
+ - **`no-require-imports`** (`plugin-calendar`, `plugin-timeline` tests) —
656
+ hoisted `vi.mock` factories now use an `async` factory with
657
+ `await import('react')` instead of `require('react')`.
658
+ - **stale `eslint-disable` directive** (`plugin-markdown`) — removed a
659
+ `react/no-danger` disable whose plugin is not loaded in the flat config (an
660
+ unknown-rule reference that ESLint v10 reports as an error); the rationale is
661
+ kept as a plain comment.
662
+
663
+ - 29c6040: fix(app-shell): redo the record-list "Add View" create flow — empty-name 405, invisible drafts, canonical naming
664
+
665
+ Rebuilds the record-list "Add View" / "Save as view" create path so a
666
+ runtime-created view has one canonical identity and is actually verifiable
667
+ before publish (supersedes #2754; fixes #2767).
668
+
669
+ - **Unified identity (P1).** New `viewEnvelope(objectName, spec, { name, label })`
670
+ seam in `runtime-metadata-persistence.ts` emits the canonical ViewItem
671
+ (`{ name: '<object>.<key>', object, viewKind: 'list', label, config }` with
672
+ `config.data = { provider: 'object', object }`), mirroring the Studio
673
+ `anchors.ts:createBuildBody`. The **qualified** name is passed as BOTH the
674
+ `PUT /meta/view/:name` URL segment and `body.name`, so the `sys_metadata`
675
+ row key, the ViewTabBar tab id, and the body identity all agree and the
676
+ draft → read → publish loop resolves. `ObjectView` and `ObjectDataPage` both
677
+ call the single helper — the duplicated envelope block is gone (P6).
678
+ - **Empty-name guards (405).** `MetadataClient.save()` and
679
+ `createRuntimeMetadata()` throw a clear contextual error instead of emitting
680
+ `PUT /meta/view/` (empty `:name`, server 405).
681
+ - **Draft visibility (P2/P3/P4).** `DataSource.listViews(objectName, { previewDrafts })`:
682
+ in draft-preview mode the `ObjectStackAdapter` makes a **single**
683
+ `MetadataClient.withPreviewDrafts(true).list('view')` request and uses the
684
+ server's already-overlaid list (draft wins by name, `_draft` tagged) —
685
+ replacing, not appending, so a draft that edits a published view can't
686
+ double-tab. No hand-rolled `fetch` of metadata routes at the adapter layer.
687
+ After a create in normal mode the console navigates to the new view with
688
+ `?preview=draft`, so the DraftPreviewBar is visible and Publish is one click.
689
+ - **CJK-aware naming (P5).** `CreateViewDialog` gains an editable machine-name
690
+ field, prefilled via `slugify(label)` for Latin labels and required (submit
691
+ disabled) when slugify yields empty for non-Latin labels — no more silent
692
+ random `task_grid_mrsyt56j` names. New `console.objectView.viewName*` keys
693
+ (en/zh).
694
+
695
+ - Updated dependencies [1c8935a]
696
+ - Updated dependencies [8b8b744]
697
+ - Updated dependencies [7cf4051]
698
+ - Updated dependencies [2e7d7f0]
699
+ - Updated dependencies [94d4876]
700
+ - Updated dependencies [2b17339]
701
+ - Updated dependencies [31b77d4]
702
+ - Updated dependencies [6d4fbe6]
703
+ - Updated dependencies [0a3710b]
704
+ - Updated dependencies [62b9ab5]
705
+ - Updated dependencies [1629313]
706
+ - Updated dependencies [29c6040]
707
+ - Updated dependencies [faebac3]
708
+ - Updated dependencies [2331ac9]
709
+ - Updated dependencies [199fa83]
710
+ - Updated dependencies [eee4ded]
711
+ - @object-ui/core@16.1.0
712
+ - @object-ui/types@16.1.0
713
+
3
714
  ## 16.0.0
4
715
 
5
716
  ### Patch Changes