@object-ui/core 17.6.0 → 18.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (147) hide show
  1. package/CHANGELOG.md +7260 -0
  2. package/README.md +228 -14
  3. package/dist/actions/ActionEngine.d.ts +4 -2
  4. package/dist/actions/ActionEngine.js +13 -8
  5. package/dist/actions/ActionRunner.d.ts +473 -69
  6. package/dist/actions/ActionRunner.js +792 -125
  7. package/dist/actions/TransactionManager.d.ts +7 -2
  8. package/dist/actions/UndoManager.d.ts +8 -13
  9. package/dist/actions/actionKeys.d.ts +9 -7
  10. package/dist/actions/actionKeys.js +67 -14
  11. package/dist/actions/actionResponse.js +2 -2
  12. package/dist/actions/index.d.ts +2 -0
  13. package/dist/actions/index.js +2 -0
  14. package/dist/actions/navigationTarget.d.ts +41 -0
  15. package/dist/actions/navigationTarget.js +20 -0
  16. package/dist/actions/recordDelete.d.ts +154 -0
  17. package/dist/actions/recordDelete.js +110 -0
  18. package/dist/adapters/ApiDataSource.d.ts +8 -4
  19. package/dist/adapters/ApiDataSource.js +19 -7
  20. package/dist/adapters/ValueDataSource.d.ts +6 -1
  21. package/dist/adapters/ValueDataSource.js +1090 -98
  22. package/dist/adapters/batchTransaction.d.ts +1 -1
  23. package/dist/adapters/batchTransaction.js +1 -1
  24. package/dist/adapters/resolveDataSource.d.ts +9 -6
  25. package/dist/adapters/resolveDataSource.js +9 -12
  26. package/dist/builder/schema-builder.d.ts +38 -81
  27. package/dist/builder/schema-builder.js +33 -152
  28. package/dist/data-scope/ViewDataProvider.d.ts +8 -25
  29. package/dist/data-scope/ViewDataProvider.js +27 -54
  30. package/dist/data-scope/element-data-source.d.ts +261 -7
  31. package/dist/data-scope/element-data-source.js +342 -52
  32. package/dist/data-scope/index.d.ts +9 -4
  33. package/dist/data-scope/index.js +9 -4
  34. package/dist/evaluator/ExpressionEvaluator.d.ts +86 -0
  35. package/dist/evaluator/ExpressionEvaluator.js +184 -12
  36. package/dist/evaluator/FormulaFunctions.d.ts +0 -10
  37. package/dist/evaluator/FormulaFunctions.js +166 -27
  38. package/dist/evaluator/deadlineSettled.d.ts +40 -0
  39. package/dist/evaluator/deadlineSettled.js +45 -0
  40. package/dist/evaluator/declaredPredicate.d.ts +93 -9
  41. package/dist/evaluator/declaredPredicate.js +117 -16
  42. package/dist/evaluator/fieldRules.d.ts +88 -4
  43. package/dist/evaluator/fieldRules.js +208 -31
  44. package/dist/evaluator/index.d.ts +2 -0
  45. package/dist/evaluator/index.js +2 -0
  46. package/dist/evaluator/listConditional.d.ts +103 -34
  47. package/dist/evaluator/listConditional.js +127 -50
  48. package/dist/evaluator/optionRules.d.ts +77 -6
  49. package/dist/evaluator/optionRules.js +87 -10
  50. package/dist/evaluator/predicateInput.d.ts +15 -4
  51. package/dist/evaluator/predicateInput.js +35 -2
  52. package/dist/evaluator/rowPredicateCanon.d.ts +7 -18
  53. package/dist/evaluator/rowPredicateCanon.js +65 -115
  54. package/dist/evaluator/subjectPermissions.d.ts +105 -0
  55. package/dist/evaluator/subjectPermissions.js +86 -0
  56. package/dist/evaluator/unevaluablePredicate.d.ts +57 -0
  57. package/dist/evaluator/unevaluablePredicate.js +70 -0
  58. package/dist/index.d.ts +15 -0
  59. package/dist/index.js +83 -2
  60. package/dist/protocols/index.d.ts +0 -1
  61. package/dist/protocols/index.js +9 -1
  62. package/dist/registry/Registry.d.ts +382 -34
  63. package/dist/registry/Registry.js +247 -12
  64. package/dist/registry/WidgetRegistry.js +7 -3
  65. package/dist/registry/html-tier-intrinsics.d.ts +90 -0
  66. package/dist/registry/html-tier-intrinsics.js +110 -0
  67. package/dist/registry/public-blocks.d.ts +8 -0
  68. package/dist/registry/public-blocks.js +57 -0
  69. package/dist/theme/ThemeEngine.js +1 -1
  70. package/dist/utils/affordanceGrants.d.ts +371 -0
  71. package/dist/utils/affordanceGrants.js +252 -0
  72. package/dist/utils/chart-category-key.d.ts +82 -0
  73. package/dist/utils/chart-category-key.js +84 -0
  74. package/dist/utils/chart-measure-key.d.ts +64 -0
  75. package/dist/utils/chart-measure-key.js +65 -0
  76. package/dist/utils/chart-presentation.d.ts +95 -112
  77. package/dist/utils/chart-presentation.js +169 -104
  78. package/dist/utils/chart-series.d.ts +165 -5
  79. package/dist/utils/chart-series.js +123 -18
  80. package/dist/utils/column-sortability.d.ts +157 -0
  81. package/dist/utils/column-sortability.js +217 -0
  82. package/dist/utils/dashboard-filters.d.ts +38 -6
  83. package/dist/utils/dashboard-filters.js +107 -61
  84. package/dist/utils/dataset-format.d.ts +211 -30
  85. package/dist/utils/dataset-format.js +389 -46
  86. package/dist/utils/date-display.d.ts +346 -0
  87. package/dist/utils/date-display.js +644 -0
  88. package/dist/utils/date-macros.js +7 -0
  89. package/dist/utils/drill-down.d.ts +78 -0
  90. package/dist/utils/drill-down.js +93 -0
  91. package/dist/utils/emptiness.d.ts +96 -0
  92. package/dist/utils/emptiness.js +101 -0
  93. package/dist/utils/expand-fields.d.ts +81 -9
  94. package/dist/utils/expand-fields.js +81 -9
  95. package/dist/utils/extract-records.d.ts +65 -5
  96. package/dist/utils/extract-records.js +65 -8
  97. package/dist/utils/filter-converter.d.ts +158 -6
  98. package/dist/utils/filter-converter.js +1098 -24
  99. package/dist/utils/filter-tokens.d.ts +49 -5
  100. package/dist/utils/filter-tokens.js +105 -33
  101. package/dist/utils/grouping-fields.d.ts +61 -0
  102. package/dist/utils/grouping-fields.js +84 -0
  103. package/dist/utils/managedBy.d.ts +3 -3
  104. package/dist/utils/managedBy.js +5 -5
  105. package/dist/utils/native-date-value.d.ts +48 -0
  106. package/dist/utils/native-date-value.js +167 -0
  107. package/dist/utils/non-grid-row-ceiling.d.ts +143 -0
  108. package/dist/utils/non-grid-row-ceiling.js +141 -0
  109. package/dist/utils/normalize-list-view.d.ts +135 -11
  110. package/dist/utils/normalize-list-view.js +243 -64
  111. package/dist/utils/number-display.d.ts +63 -14
  112. package/dist/utils/number-display.js +34 -12
  113. package/dist/utils/object-aggregate-query.d.ts +113 -0
  114. package/dist/utils/object-aggregate-query.js +50 -0
  115. package/dist/utils/parent-scope.d.ts +123 -0
  116. package/dist/utils/parent-scope.js +140 -0
  117. package/dist/utils/predicate-fields.d.ts +45 -12
  118. package/dist/utils/predicate-fields.js +141 -12
  119. package/dist/utils/record-color.d.ts +75 -0
  120. package/dist/utils/record-color.js +65 -0
  121. package/dist/utils/record-source.d.ts +298 -0
  122. package/dist/utils/record-source.js +331 -0
  123. package/dist/utils/record-title.d.ts +148 -8
  124. package/dist/utils/record-title.js +220 -26
  125. package/dist/utils/reference-keys.d.ts +21 -22
  126. package/dist/utils/reference-keys.js +532 -29
  127. package/dist/utils/sort-query.d.ts +115 -15
  128. package/dist/utils/sort-query.js +240 -13
  129. package/dist/utils/text-comparand.d.ts +92 -0
  130. package/dist/utils/text-comparand.js +99 -0
  131. package/dist/utils/without-denied-fields.d.ts +49 -0
  132. package/dist/utils/without-denied-fields.js +52 -0
  133. package/dist/validation/index.d.ts +8 -4
  134. package/dist/validation/index.js +8 -4
  135. package/dist/validation/schema-validator.d.ts +1 -1
  136. package/dist/validation/schema-validator.js +225 -83
  137. package/dist/validation/server-owned-value.d.ts +5 -1
  138. package/dist/validation/server-owned-value.js +5 -1
  139. package/dist/validation/validators/object-validation-engine.d.ts +1 -1
  140. package/dist/validation/validators/object-validation-engine.js +14 -3
  141. package/package.json +8 -9
  142. package/dist/data-scope/DataScopeManager.d.ts +0 -127
  143. package/dist/data-scope/DataScopeManager.js +0 -229
  144. package/dist/protocols/ResponsiveProtocol.d.ts +0 -73
  145. package/dist/protocols/ResponsiveProtocol.js +0 -158
  146. package/dist/validation/validation-engine.d.ts +0 -88
  147. package/dist/validation/validation-engine.js +0 -434
@@ -73,8 +73,21 @@ const isRecord = (v) => !!v && typeof v === 'object' && !Array.isArray(v);
73
73
  * available to users in the view toolbar", and already carries `rowHeight` —
74
74
  * objectui's `showDensity` under its spec name. `group` / `hideFields` /
75
75
  * `rowColor` are the same kind of toggle and are named after the config key
76
- * they gate (`grouping`, `hiddenFields`, `rowColor`), pending promotion into
77
- * `UserActionsConfigSchema` upstream.
76
+ * they gate (`grouping`, `hiddenFields`, `rowColor`).
77
+ *
78
+ * That promotion has LANDED (objectui#5435, ruled Option A 2026-08-22):
79
+ * `UserActionsConfigSchema` declares all three from `@objectstack/spec@17.3.0`,
80
+ * and the spec's declaring docblock names this card. Every key this table emits
81
+ * is therefore authorable, which
82
+ * `__tests__/normalize-list-view.foldOutputAuthorable-5435.test.ts` pins with a
83
+ * firing control. ⛔ Do not read the three as legacy-only spellings awaiting
84
+ * retirement — the protocol declares them, so removing them here would leave
85
+ * objectui narrower than the protocol.
86
+ *
87
+ * ⛔ `inlineEdit` → `editInline` is NOT a row here, although it lands in the same
88
+ * block (objectui#5144). Every row of this table is deleted after it folds;
89
+ * `inlineEdit` must survive its fold, because it is also the MODE `ListView`
90
+ * opens the grid in. See {@link foldsInlineEditIntoEditInline}.
78
91
  */
79
92
  const SHOW_FLAG_TO_USER_ACTION = {
80
93
  showSearch: 'search',
@@ -85,6 +98,43 @@ const SHOW_FLAG_TO_USER_ACTION = {
85
98
  showHideFields: 'hideFields',
86
99
  showColor: 'rowColor',
87
100
  };
101
+ /**
102
+ * Whether a view's `inlineEdit` folds into `userActions.editInline`
103
+ * (objectui#5144, the maintainer's B-fold).
104
+ *
105
+ * The spec declares `userActions.editInline` with `.default(true)` since the
106
+ * maintainer's v18 ruling (objectstack#22605): a list is editable in place by
107
+ * default, and `editInline: false` is the opt-out. It declares the view's
108
+ * `inlineEdit` as the same kind of permission ("allow inline editing"). Stored
109
+ * views carry `inlineEdit`, not `editInline`: authored views declare it, and
110
+ * the console's list toolbar used to write it. Folding it here puts both keys
111
+ * on one vocabulary, so `ListView` can read `editInline` with the spec default
112
+ * and a view's own `inlineEdit` still decides it when `editInline` is absent.
113
+ *
114
+ * The console's toolbar no longer writes `inlineEdit` (objectui#5144, ruling
115
+ * E). It persisted a user's edit mode into this permission key, so switching
116
+ * the toggle off stored `inlineEdit: false` and the fold then read the view as
117
+ * not offering inline editing. The toggle is session state now.
118
+ *
119
+ * Two rules, each the same as a fold above:
120
+ * - The value carries over: `inlineEdit: true` folds to `editInline: true`,
121
+ * and `inlineEdit: false` to `editInline: false`.
122
+ * - An explicit `userActions.editInline` wins. It is the spec key, so a
123
+ * stored `inlineEdit` only fills the gap.
124
+ *
125
+ * One departure, the `data` → `objectName` one: `inlineEdit` is KEPT. The other
126
+ * folds delete because the legacy key has one meaning and one home.
127
+ * `inlineEdit` has a second use. It is the spec's own `ListView.inlineEdit`,
128
+ * and `ListView` seeds the grid's edit MODE from it on each load. The toolbar
129
+ * toggle then flips that mode for the session. `editInline` decides whether
130
+ * the toggle is offered at all. Deleting `inlineEdit` would open every such
131
+ * view out of edit mode.
132
+ */
133
+ function foldsInlineEditIntoEditInline(s) {
134
+ if (typeof s.inlineEdit !== 'boolean')
135
+ return false;
136
+ return !(isRecord(s.userActions) && typeof s.userActions.editInline === 'boolean');
137
+ }
88
138
  /**
89
139
  * Legacy `sharing.visibility` → the spec's `ViewSharing.type`. The spec models
90
140
  * two ownership kinds; objectui's four-value audience enum collapses onto them:
@@ -102,31 +152,136 @@ const ARIA_KEY_ALIASES = {
102
152
  describedBy: 'ariaDescribedBy',
103
153
  };
104
154
  /**
105
- * Per-view-type config aliases → the spec key each one aliases (#2890, the
106
- * phase-3 carry-over).
155
+ * The view kinds `ListView` actually draws.
107
156
  *
108
- * Phase 3 derived `kanban` / `gallery` / `timeline` from the spec configs but
109
- * kept the pre-#2231 objectui spellings declared alongside the spec keys so
110
- * stored view metadata would keep validating
111
- * (`packages/types/src/zod/objectql.zod.ts`, each marked `@deprecated legacy
112
- * alias for the spec's X`). Folding them here is what lets a read-site stop
113
- * carrying its own `canonical || legacy` pair — the same move A1–A5 made for
114
- * the top-level vocabulary.
157
+ * A total `Record<…, true>` over {@link ListViewVisualization}, for the same
158
+ * reason the density maps above are `Record<RowHeight, …>`: a kind added to the
159
+ * union fails the build HERE instead of silently staying unreadable authored
160
+ * input.
115
161
  *
116
- * `calendar` is deliberately absent: its one local key, `defaultView`, is not
117
- * an alias of anything. It has no spec counterpart at all, so it wants
118
- * PROMOTION upstream, not a rename — folding it would delete an authored value
119
- * with nowhere to put it.
162
+ * ⚠️ That promise was FALSE between `@objectstack/spec@17.3.0` and `ca3942729`.
163
+ * `ViewType` was a hand-written copy of the spec's list, so this map was total
164
+ * over the copy and compiled green while the spec's `page` had nowhere to land.
165
+ * Both faces are derived now, and the two totality structures below close the
166
+ * other half of the union, so every member of the spec's list is accounted for
167
+ * by a structure that fails the build rather than by a reader's memory.
120
168
  *
121
- * Note `kanban.columns` is the spec's "fields shown on each card", NOT the
122
- * table columns that the same word names at the ListView top level. The fold
123
- * is scoped to the nested config, so the two never meet.
169
+ * `hasOwnProperty`, not `in` — same trap as {@link rowHeightToDensityMode}:
170
+ * `in` walks the prototype chain, so `'toString'` would read as a view kind.
124
171
  */
125
- const PER_VIEW_CONFIG_ALIASES = {
126
- kanban: { groupField: 'groupByField', cardFields: 'columns' },
127
- gallery: { imageField: 'coverField' },
128
- timeline: { dateField: 'startDateField' },
172
+ const LIST_VIEW_KINDS = {
173
+ grid: true,
174
+ kanban: true,
175
+ gallery: true,
176
+ calendar: true,
177
+ timeline: true,
178
+ gantt: true,
179
+ map: true,
180
+ chart: true,
181
+ tree: true,
129
182
  };
183
+ /**
184
+ * Every member of {@link ViewType} that {@link LIST_VIEW_KINDS} does NOT carry,
185
+ * with the reason — and, where the answer is "the author authored something
186
+ * valid and will get a grid", the sentence to say out loud.
187
+ *
188
+ * The two tables together still partition the derived union — a member the spec
189
+ * ADDS must be classed as drawable or explained HERE, and until it is the build
190
+ * is red — but the red lands on {@link LIST_VIEW_KINDS} alone, because a kind
191
+ * neither table classes is not extracted into {@link UndrawableViewKind} and so
192
+ * stays inside {@link ListViewVisualization}, whose record is total. A key
193
+ * MISSPELLED here fails the same way, for the same reason: the kind it was
194
+ * meant to class is not extracted either. Both directions are pinned in
195
+ * `__tests__/normalize-list-view.undrawableBothLegs-9880.test.ts` rather than
196
+ * asserted in this sentence.
197
+ *
198
+ * ⛔ NOT annotated `Record<Exclude<ViewType, ListViewVisualization>, …>`, and
199
+ * the `satisfies` is not a style choice (objectui#9880). An annotation makes
200
+ * the literal EXACT in BOTH directions: a kind the spec adds is a missing key
201
+ * (wanted) and a kind the spec RETIRES is an EXCESS PROPERTY — a TS2353 that
202
+ * demands a deletion this repository cannot make while it resolves a published
203
+ * spec which still carries the kind (as it did with `page` through 17.4.0).
204
+ * `satisfies` keeps the value constraint
205
+ * (every reason is a `string` or `null`) and drops the exactness, which is what
206
+ * lets one spelling compile against both the pinned published spec and one
207
+ * built from objectstack `main`.
208
+ *
209
+ * `null` means silence is correct — these are objectui view CATEGORIES, never
210
+ * authored as a list-view kind:
211
+ * - `list` is the view CATEGORY, not a kind — it already folds to `grid`.
212
+ * - `detail` is a different renderer (`plugin-detail`), never a ListView case.
213
+ *
214
+ * `page` had a row here until objectui#11073. objectstack#17063 removed
215
+ * `type: 'page'` from the list-view enum, and the row answered for it while this
216
+ * repository still resolved a spec that published the kind. `@objectstack/spec`
217
+ * 17.5.0 carries the removal: both published `@object-ui/types` faces refuse
218
+ * `page`, `Extract` no longer extracted the row, and the residual pins that held
219
+ * it (`normalize-list-view.pageResidual-8429.test.ts`) were converted, so it was
220
+ * deleted as it said it would be. A stored `page` view now degrades like any
221
+ * kind outside the vocabulary.
222
+ */
223
+ const UNDRAWABLE_VIEW_KINDS = {
224
+ list: null,
225
+ detail: null,
226
+ };
227
+ /** One warning per undrawable kind per process — see {@link warnUndrawableViewKind}. */
228
+ const warnedUndrawableKinds = new Set();
229
+ /**
230
+ * Fail-open is **loud**, not silent — the convention this repo already applies
231
+ * to unevaluable predicates (`../evaluator/fieldRules.ts`, objectstack#5149).
232
+ *
233
+ * The bug `ca3942729` records is not only that `type: 'page'` degrades to a
234
+ * grid; it is that it degrades IDENTICALLY to a typo. Measured on `5505aec`,
235
+ * `specType: 'page'` and `specType: 'nonsense'` both left `viewType: 'grid'`
236
+ * with no error, no warning and no console line — and `@object-ui/types`'
237
+ * published validator ACCEPTS `viewType: 'page'` while refusing the typo. An
238
+ * author who writes a spec-valid view and is silently handed a different one
239
+ * has no way to tell that from a working grid, so the degrade says so once.
240
+ *
241
+ * Once per kind per process, and never for the two objectui categories: this
242
+ * runs inside a normalizer that a render can call on every keystroke, and the
243
+ * point is one line, not a scrolling wall.
244
+ */
245
+ function warnUndrawableViewKind(kind) {
246
+ const reason = Object.prototype.hasOwnProperty.call(UNDRAWABLE_VIEW_KINDS, kind)
247
+ ? UNDRAWABLE_VIEW_KINDS[kind]
248
+ : null;
249
+ if (reason === null || warnedUndrawableKinds.has(kind))
250
+ return;
251
+ warnedUndrawableKinds.add(kind);
252
+ console.warn(`[object-ui] This build accepts a ${JSON.stringify(kind)} list view but cannot draw one: ` +
253
+ `${reason}. The view falls back to a grid. ` +
254
+ 'Author a visualization this renderer draws, or render the page through the surface that owns it.');
255
+ }
256
+ /**
257
+ * Whether `value` names a visualization `ListView` draws.
258
+ *
259
+ * Exported so a read site cannot restate the membership question as its own
260
+ * literal array: the gate and the seam must answer one question, which is the
261
+ * rule this file's own `availableViews` notes already argue for (objectui#5042,
262
+ * objectui#7544, and now `ca3942729`).
263
+ */
264
+ export function isListViewVisualization(value) {
265
+ return typeof value === 'string' && Object.prototype.hasOwnProperty.call(LIST_VIEW_KINDS, value);
266
+ }
267
+ /**
268
+ * The author's view kind, or `undefined` when the value names no kind ListView
269
+ * draws. An unrecognized kind is deliberately left unresolved rather than
270
+ * written through to `viewType`: the caller's own `'grid'` default is a more
271
+ * honest answer than a `viewType` no branch matches (the same call
272
+ * `normalizeChartSchema` makes for a chart family it cannot draw).
273
+ *
274
+ * A kind the SPEC allows but this renderer cannot draw takes the same fallback
275
+ * and additionally warns — see {@link warnUndrawableViewKind}.
276
+ */
277
+ function readListViewKind(value) {
278
+ if (typeof value !== 'string')
279
+ return undefined;
280
+ if (isListViewVisualization(value))
281
+ return value;
282
+ warnUndrawableViewKind(value);
283
+ return undefined;
284
+ }
130
285
  /**
131
286
  * ObjectUI's `list-view` node historically used a different vocabulary from
132
287
  * `@objectstack/spec` for the same concepts (`fields` where the spec says
@@ -168,6 +323,12 @@ const PER_VIEW_CONFIG_ALIASES = {
168
323
  * stays absent, because the defaults are per-toggle (search/sort/filter/
169
324
  * rowHeight/group default ON, hideFields/rowColor default OFF) and belong to
170
325
  * the renderer, not to the vocabulary bridge.
326
+ * - the view's `inlineEdit` → `userActions.editInline` (objectui#5144), with
327
+ * the value carried over and an explicit `editInline` winning. `inlineEdit`
328
+ * stays on the result, because `ListView` seeds the grid's edit mode from
329
+ * it. See {@link foldsInlineEditIntoEditInline}. An absent pair stays
330
+ * absent here as well; `ListView` reads that as on, the spec's default
331
+ * (objectstack#22605).
171
332
  * - `aria: { label, describedBy }` → the spec's `AriaProps`
172
333
  * (`{ ariaLabel, ariaDescribedBy }`), and `sharing: { visibility, enabled }`
173
334
  * → the spec's `ViewSharing` (`{ type }`) — #2890 scope A step 5. `aria.live`
@@ -179,10 +340,34 @@ const PER_VIEW_CONFIG_ALIASES = {
179
340
  * so objectui's field is typed from the spec but used as something else.
180
341
  * That mismatch is real and out of scope here; converting formats inside a
181
342
  * vocabulary fold would change what reaches the data source.
343
+ * - `data: { provider: 'object', object }` → `objectName` (#7477, step 6 of
344
+ * #2890). This is the spelling the published `react-blocks` contract
345
+ * recommends and the one `@objectstack/spec`'s `ViewDataSchema` declares;
346
+ * ListView read it at ZERO sites, so a page bound that way validated green
347
+ * upstream and rendered an empty list here with no diagnostic. The `object`
348
+ * provider is a `strictObject` carrying exactly `{ provider, object }`, so
349
+ * `objectName` captures all of it. Two deliberate departures from the folds
350
+ * above, both narrowing:
351
+ * · an already-present `objectName` WINS — this fold only fills a gap, it
352
+ * never re-points a binding that already resolves;
353
+ * · `data` is NOT deleted. Every other fold deletes because the legacy key
354
+ * has one meaning and one home; `data` has more providers than
355
+ * `object`, `value` is read live in `ListView`, and the whole block is FORWARDED to child
356
+ * views (the gantt branch), whose own `getDataConfig` reads `data` before
357
+ * `objectName`. Deleting it for one provider would rewrite what a child
358
+ * resolves. Nothing in `ListView` reads `data.provider === 'object'`, so
359
+ * keeping it cannot create a second de-facto contract inside the renderer.
182
360
  * - `viewType`: a missing kind, or the view CATEGORY `'list'` that AI-authored
183
361
  * metadata stores and hosts forward verbatim, becomes the renderable `'grid'`
184
362
  * — otherwise it reaches the renderer's typeless default branch and shows as
185
- * a red "Unknown component type" box.
363
+ * a red "Unknown component type" box. Before that default applies, the
364
+ * AUTHOR's kind is read (#7477): `specType` — the slot
365
+ * `components/renderers/layout/react-page.tsx` parks a react-tier `type` in
366
+ * when the SDUI envelope claims the `type` key (ADR-0078) — and then a bare
367
+ * `type` when it names a kind ListView draws, which the component
368
+ * discriminator (`'list-view'`) never does. Same two legs, same order, as
369
+ * `normalizeChartSchema`'s chart-family read. An explicit `viewType` still
370
+ * wins: this leg only fills the gap that used to resolve to `'grid'`.
186
371
  * - each `columns` entry's IDENTITY — `name` / `fieldName` → the spec's `field`
187
372
  * (#3104). This one MIRRORS instead of deleting, for the reason given on
188
373
  * {@link normalizeColumnIdentities}: `columns` entries cross the package
@@ -190,16 +375,14 @@ const PER_VIEW_CONFIG_ALIASES = {
190
375
  * breaking change with no inventory. Runs AFTER the `fields` → `columns` fold
191
376
  * above, so a view that spells its column list the legacy way still gets its
192
377
  * entries canonicalized.
193
- * - the four PER-VIEW-TYPE config aliases phase 3 carried over
194
- * ({@link PER_VIEW_CONFIG_ALIASES}): `kanban.groupField` → `groupByField`,
195
- * `kanban.cardFields` → `columns`, `gallery.imageField` → `coverField`,
196
- * `timeline.dateField` → `startDateField`. One read-site changes behaviour
197
- * as a result, and it is a CORRECTION of the same inverted precedence A2
198
- * fixed for `densityMode`: `ListView`'s kanban adapter resolves
199
- * `cardFields || columns`, i.e. legacy over canonical, so a config carrying
200
- * BOTH used to render the legacy value. After the fold the canonical
201
- * `columns` reaches it. Every other reader of these four was already
202
- * canonical-first and is unaffected.
378
+ *
379
+ * No longer folded: the four PER-VIEW-TYPE config aliases phase 3 carried over
380
+ * (`kanban.groupField` → `groupByField`, `kanban.cardFields` → `columns`,
381
+ * `gallery.imageField` → `coverField`, `timeline.dateField` →
382
+ * `startDateField`). objectui#6152 round 14 retired that fold with every
383
+ * renderer-side reader of the four: each list-view door refuses them by name
384
+ * (rounds 11 and 12), and a stored block that still carries one passes through
385
+ * here untouched and binds nothing downstream.
203
386
  */
204
387
  export function normalizeListViewSchema(schema) {
205
388
  if (!schema || typeof schema !== 'object')
@@ -208,16 +391,32 @@ export function normalizeListViewSchema(schema) {
208
391
  const legacyFields = s.fields;
209
392
  const foldColumns = Array.isArray(legacyFields);
210
393
  const legacyDensity = s.densityMode;
211
- const foldRowHeight = typeof legacyDensity === 'string' && legacyDensity in DENSITY_MODE_TO_ROW_HEIGHT;
394
+ // `hasOwnProperty`, not `in` — same trap as {@link rowHeightToDensityMode},
395
+ // in the fold direction: `in` walks the prototype chain, so `'toString'` used
396
+ // to fold into `rowHeight` as `Object.prototype.toString`, a FUNCTION, and
397
+ // drop the key (objectui#10868). An inherited key is an unrecognized density.
398
+ const foldRowHeight = typeof legacyDensity === 'string' &&
399
+ Object.prototype.hasOwnProperty.call(DENSITY_MODE_TO_ROW_HEIGHT, legacyDensity);
212
400
  const legacyFilters = s.filters;
213
401
  const foldFilter = Array.isArray(legacyFilters);
214
402
  const legacyFlags = Object.keys(SHOW_FLAG_TO_USER_ACTION).filter((k) => typeof s[k] === 'boolean');
403
+ const foldInlineEdit = foldsInlineEditIntoEditInline(s);
215
404
  const foldDescription = typeof s.showDescription === 'boolean';
216
405
  const aria = isRecord(s.aria) ? s.aria : undefined;
217
406
  const foldAria = !!aria && Object.keys(ARIA_KEY_ALIASES).some((k) => aria[k] !== undefined);
218
407
  const sharing = isRecord(s.sharing) ? s.sharing : undefined;
219
408
  const foldSharing = !!sharing && (sharing.visibility !== undefined || sharing.enabled !== undefined);
409
+ // `data: { provider: 'object', object }` → `objectName` (#7477). Gap-fill
410
+ // only: a non-empty `objectName` already on the schema wins, so this can
411
+ // never re-point a binding that resolves today.
412
+ const dataConfig = isRecord(s.data) ? s.data : undefined;
413
+ const dataObjectName = dataConfig?.provider === 'object' && typeof dataConfig.object === 'string' && dataConfig.object
414
+ ? dataConfig.object
415
+ : undefined;
416
+ const foldObjectName = dataObjectName !== undefined && !(typeof s.objectName === 'string' && s.objectName);
220
417
  const viewType = s.viewType;
418
+ // The author's kind, read before the `'grid'` default applies (#7477).
419
+ const authoredViewKind = readListViewKind(s.specType) ?? readListViewKind(s.type);
221
420
  const defaultViewKind = !viewType || viewType === 'list';
222
421
  // The columns array the identity fold will see — mirroring the `foldColumns`
223
422
  // precedence below (canonical `columns` wins; otherwise the legacy `fields`
@@ -231,22 +430,9 @@ export function normalizeListViewSchema(schema) {
231
430
  : undefined;
232
431
  const foldedColumns = normalizeColumnIdentities(columnsSource);
233
432
  const foldColumnIdentity = foldedColumns !== columnsSource;
234
- // Per-view-type config aliases (#2890 phase-3 carry-over). Collected before
235
- // the early return so an otherwise-canonical view carrying only a nested
236
- // legacy key still folds — and so a view carrying none still returns by
237
- // reference.
238
- const perViewFolds = Object.entries(PER_VIEW_CONFIG_ALIASES)
239
- .map(([viewKey, aliases]) => {
240
- const cfg = isRecord(s[viewKey]) ? s[viewKey] : undefined;
241
- if (!cfg)
242
- return undefined;
243
- const legacyKeys = Object.keys(aliases).filter((legacy) => cfg[legacy] !== undefined);
244
- return legacyKeys.length ? { viewKey, aliases, cfg, legacyKeys } : undefined;
245
- })
246
- .filter((entry) => entry !== undefined);
247
- if (!foldColumns && !foldRowHeight && !foldFilter && !legacyFlags.length &&
433
+ if (!foldColumns && !foldRowHeight && !foldFilter && !legacyFlags.length && !foldInlineEdit &&
248
434
  !foldDescription && !foldAria && !foldSharing && !defaultViewKind &&
249
- !foldColumnIdentity && !perViewFolds.length) {
435
+ !foldColumnIdentity && !foldObjectName) {
250
436
  return schema;
251
437
  }
252
438
  const next = { ...s };
@@ -270,7 +456,7 @@ export function normalizeListViewSchema(schema) {
270
456
  next.filter = legacyFilters;
271
457
  delete next.filters;
272
458
  }
273
- if (legacyFlags.length) {
459
+ if (legacyFlags.length || foldInlineEdit) {
274
460
  const ua = { ...(isRecord(next.userActions) ? next.userActions : {}) };
275
461
  for (const flag of legacyFlags) {
276
462
  const key = SHOW_FLAG_TO_USER_ACTION[flag];
@@ -278,6 +464,10 @@ export function normalizeListViewSchema(schema) {
278
464
  ua[key] = s[flag];
279
465
  delete next[flag];
280
466
  }
467
+ // objectui#5144. Gap-fill only, and `inlineEdit` is not deleted: see
468
+ // `foldsInlineEditIntoEditInline`.
469
+ if (foldInlineEdit)
470
+ ua.editInline = s.inlineEdit;
281
471
  next.userActions = ua;
282
472
  }
283
473
  if (foldDescription) {
@@ -320,20 +510,9 @@ export function normalizeListViewSchema(schema) {
320
510
  delete nextSharing.enabled;
321
511
  next.sharing = nextSharing;
322
512
  }
323
- for (const { viewKey, aliases, cfg, legacyKeys } of perViewFolds) {
324
- const nextCfg = { ...cfg };
325
- for (const legacy of legacyKeys) {
326
- const canonical = aliases[legacy];
327
- // Same one-directional shape as every fold above: the canonical key wins
328
- // when both are present, and the legacy key is REMOVED so a missed
329
- // read-site fails loudly instead of quietly taking the legacy path.
330
- if (nextCfg[canonical] === undefined)
331
- nextCfg[canonical] = cfg[legacy];
332
- delete nextCfg[legacy];
333
- }
334
- next[viewKey] = nextCfg;
335
- }
513
+ if (foldObjectName)
514
+ next.objectName = dataObjectName;
336
515
  if (defaultViewKind)
337
- next.viewType = 'grid';
516
+ next.viewType = authoredViewKind ?? 'grid';
338
517
  return next;
339
518
  }
@@ -20,11 +20,12 @@
20
20
  * grouped and pointed decimals the US way; and
21
21
  * 2. `useGrouping` was never set, so a four-digit YEAR stored as
22
22
  * `Field.number({ scale: 0 })` rendered as `2,026` — in every locale, with
23
- * no field property able to turn it off.
23
+ * no field property able to turn it off. (That property now exists: the
24
+ * spec's `FieldSchema.useGrouping`, read here since objectui#11026.)
24
25
  *
25
- * Both policies now live here and nowhere else. Call sites bring the value and
26
- * the display width; they do not bring a locale default and they do not decide
27
- * grouping.
26
+ * Both policies now live here and nowhere else. Call sites bring the value, the
27
+ * display width and whatever the FIELD declared (`scale`, `useGrouping`); they
28
+ * do not bring a locale default and they do not decide grouping.
28
29
  *
29
30
  * ── Why this module lives in `@object-ui/core` (objectui#4576) ──
30
31
  * It used to live in `@object-ui/i18n`, and that home cost the repo a real
@@ -57,6 +58,22 @@ export interface DisplayNumberFormatOptions {
57
58
  * for a non-React caller that has no locale in hand. It never means `en-US`:
58
59
  * assuming US conventions for the whole world is the defect this module was
59
60
  * created to remove.
61
+ *
62
+ * ── The caller this rule governs (objectui#10098) ──
63
+ * The `undefined` rule above is written for a NON-REACT display caller with
64
+ * no tag in hand: a plain formatter, an exporter, engine code — anything that
65
+ * cannot call a hook. For that caller the runtime default is the viewer's
66
+ * own environment, and the viewer's own environment is the honest locale for
67
+ * a user-facing display. A malformed tag ends in the same place, whoever the
68
+ * caller: {@link formatDisplayNumber} retries without it rather than
69
+ * throwing, so the number renders in the runtime default instead of taking
70
+ * the cell down.
71
+ *
72
+ * A React renderer is governed by a different rule, and the two do not
73
+ * contradict each other. It passes what `useDisplayLocale` returns, and that
74
+ * hook — `@object-ui/i18n`, `packages/i18n/src/useDisplayLocale.ts` — ends
75
+ * its provider chain on the concrete `'en'`, for determinism, when the chain
76
+ * yields no tag at all. That hook's docblock names this one in return.
60
77
  */
61
78
  locale?: string;
62
79
  /**
@@ -88,14 +105,40 @@ export interface DisplayNumberFormatOptions {
88
105
  *
89
106
  * ⚠️ INTERIM DEFAULT (objectui#4033, PM ruling 2026-08-11). Suppressing
90
107
  * grouping for every scale-0 number is a transitional policy with a known,
91
- * accepted cost: a large scale-0 COUNT loses its separators too. It is the
92
- * better trade only until the spec gains an authorable presentation hint
93
- * (`useGrouping` / `displayFormat` — being specified separately, contract-first,
94
- * in the objectstack repo). When that hint lands it OVERRIDES this default,
95
- * and this heuristic should be reduced to the fallback for fields that
96
- * declare nothing.
108
+ * accepted cost: a large scale-0 COUNT loses its separators too. The
109
+ * authorable hint it was waiting for has landed as
110
+ * {@link DisplayNumberFormatOptions.useGrouping} (objectui#11026): an
111
+ * authored boolean OVERRIDES this default, and the heuristic is now only the
112
+ * fallback for a field that declares nothing.
97
113
  */
98
114
  scale?: number;
115
+ /**
116
+ * The FIELD's authored `useGrouping` — `FieldSchema.useGrouping` in
117
+ * `@objectstack/spec`, the author's digit-grouping hint — and nothing else.
118
+ * Like `scale` it is a POLICY input: pass it only when a field declaration
119
+ * actually said so (objectui#11026).
120
+ *
121
+ * Three-valued, and the spec's own reading of each value is the one this
122
+ * option implements:
123
+ *
124
+ * - `undefined` — the author made no judgement, so the renderer decides:
125
+ * the `scale`/`currency` heuristic in {@link shouldGroupDisplayNumber},
126
+ * unchanged.
127
+ * - `false` — the author's explicit opt-out (a year, an ID, a zip code).
128
+ * Never grouped, whatever `scale` or `currency` would have suggested.
129
+ * - `true` — the author pins grouping ON, overriding the heuristic the
130
+ * other way (a scale-0 count that should read `2,026`).
131
+ *
132
+ * The spec maps the key 1:1 onto `Intl.NumberFormat`'s `useGrouping`, so an
133
+ * authored `true` reaches `Intl` as `true`, which means "always". That is
134
+ * deliberately NOT what the heuristic's own "group" answer does: it omits
135
+ * the key and keeps the locale's "auto" (see the `Intl` note inside
136
+ * {@link formatDisplayNumber}). The two differ where a locale leaves a
137
+ * four-digit number alone under "auto" (es-ES and pl-PL render `1234`); an
138
+ * author who pinned grouping on gets `1.234` / `1 234` there, because that
139
+ * is what they declared.
140
+ */
141
+ useGrouping?: boolean;
99
142
  /**
100
143
  * How the number relates to a percentage — the ONE place that distinction is
101
144
  * expressed, because getting it wrong is the whole of objectui#4576.
@@ -137,11 +180,17 @@ export interface DisplayNumberFormatOptions {
137
180
  * The grouping policy, alone and testable: does this number get thousands
138
181
  * separators?
139
182
  *
140
- * @param scale the field's declared `scale`, or `undefined` when the caller
141
- * has no field declaration behind it
142
- * @param currency ISO 4217 code when the number is money
183
+ * This is the ONE place the decision is made. An authored `useGrouping`
184
+ * answers it outright; only when the field declared nothing does the
185
+ * `scale`/`currency` heuristic below get a say (objectui#11026).
186
+ *
187
+ * @param scale the field's declared `scale`, or `undefined` when the
188
+ * caller has no field declaration behind it
189
+ * @param currency ISO 4217 code when the number is money
190
+ * @param useGrouping the field's authored `useGrouping`, or `undefined` when
191
+ * the author declared none
143
192
  */
144
- export declare function shouldGroupDisplayNumber(scale?: number, currency?: string): boolean;
193
+ export declare function shouldGroupDisplayNumber(scale?: number, currency?: string, useGrouping?: boolean): boolean;
145
194
  /**
146
195
  * Format a number for DISPLAY, in the active locale, under the grouping policy
147
196
  * above.
@@ -9,11 +9,23 @@
9
9
  * The grouping policy, alone and testable: does this number get thousands
10
10
  * separators?
11
11
  *
12
- * @param scale the field's declared `scale`, or `undefined` when the caller
13
- * has no field declaration behind it
14
- * @param currency ISO 4217 code when the number is money
12
+ * This is the ONE place the decision is made. An authored `useGrouping`
13
+ * answers it outright; only when the field declared nothing does the
14
+ * `scale`/`currency` heuristic below get a say (objectui#11026).
15
+ *
16
+ * @param scale the field's declared `scale`, or `undefined` when the
17
+ * caller has no field declaration behind it
18
+ * @param currency ISO 4217 code when the number is money
19
+ * @param useGrouping the field's authored `useGrouping`, or `undefined` when
20
+ * the author declared none
15
21
  */
16
- export function shouldGroupDisplayNumber(scale, currency) {
22
+ export function shouldGroupDisplayNumber(scale, currency, useGrouping) {
23
+ // The author's explicit declaration beats every guess below, money included:
24
+ // the spec reads `false` as "never grouped, regardless of what the
25
+ // renderer's heuristic would have guessed", and `true` as the same override
26
+ // in the other direction.
27
+ if (typeof useGrouping === 'boolean')
28
+ return useGrouping;
17
29
  // Money always groups — including money whose currency code could not be
18
30
  // resolved, which still renders as an amount (just without a symbol).
19
31
  if (currency)
@@ -32,7 +44,7 @@ export function shouldGroupDisplayNumber(scale, currency) {
32
44
  * malformed tag from a tenant config must never take a grid cell down.
33
45
  */
34
46
  export function formatDisplayNumber(value, options = {}) {
35
- const { locale, currency, scale, style, ...passthrough } = options;
47
+ const { locale, currency, scale, useGrouping, style, ...passthrough } = options;
36
48
  const intlOptions = { ...passthrough };
37
49
  if (style === 'percentPoints') {
38
50
  // The locale's percent convention WITHOUT `style: 'percent'`'s ×100 — see
@@ -50,15 +62,25 @@ export function formatDisplayNumber(value, options = {}) {
50
62
  delete intlOptions.unit;
51
63
  delete intlOptions.unitDisplay;
52
64
  }
53
- // ⚠️ Set `useGrouping` ONLY to suppress. `useGrouping: true` is NOT the same
54
- // as omitting the key: `true` means "always", while omitting it means "auto"
55
- // (and "min2" under compact notation), which is the locale's own preference.
56
- // Measured — for 1234: es-ES "auto" → `1234` but "always" → `1.234`; pl-PL
57
- // "auto" → `1234` but "always" → `1 234`. Writing `true` here would silently
58
- // override those locales' conventions in the name of preserving en-US output.
59
- if (!shouldGroupDisplayNumber(scale, currency)) {
65
+ // ⚠️ Never write `useGrouping: true` on the renderer's own behalf.
66
+ // `useGrouping: true` is NOT the same as omitting the key: `true` means
67
+ // "always", while omitting it means "auto" (and "min2" under compact
68
+ // notation), which is the locale's own preference. Measured — for 1234:
69
+ // es-ES "auto" → `1234` but "always" → `1.234`; pl-PL "auto" → `1234` but
70
+ // "always" → `1 234`. Writing `true` for a field that declared nothing would
71
+ // silently override those locales' conventions in the name of preserving
72
+ // en-US output.
73
+ //
74
+ // The ONE `true` this function writes is the author's own (objectui#11026):
75
+ // `FieldSchema.useGrouping` maps 1:1 onto this `Intl` option, and an author
76
+ // who pinned grouping on asked for "always". The decision itself is made
77
+ // once, above, by `shouldGroupDisplayNumber`; this only spells it for `Intl`.
78
+ if (!shouldGroupDisplayNumber(scale, currency, useGrouping)) {
60
79
  intlOptions.useGrouping = false;
61
80
  }
81
+ else if (useGrouping === true) {
82
+ intlOptions.useGrouping = true;
83
+ }
62
84
  try {
63
85
  return new Intl.NumberFormat(locale, intlOptions).format(value);
64
86
  }