@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
@@ -22,8 +22,12 @@
22
22
  * (`nameField`) to canonical and de-prioritizes the legacy render-only
23
23
  * `titleFormat` template:
24
24
  *
25
+ * 0. `options.titleField` — the caller's own DECLARED view-level pointer
26
+ * (gallery / calendar / gantt / map / kanban / timeline config).
25
27
  * 1. `objectDef.nameField` — the canonical record-title pointer (ADR-0079);
26
- * a field the server can return and query, if present & non-empty.
28
+ * a field the server can return and query, if present & non-empty. With
29
+ * the undeclared `objectDef.titleField` read gone (objectui#6531) this is
30
+ * the top of the OBJECT-level ladder, exactly as ADR-0079 states.
27
31
  * 2. `objectDef.displayNameField` (alias `NAME_FIELD_KEY`) — deprecated alias
28
32
  * of `nameField`, kept for back-compat.
29
33
  * 3. `objectDef.titleFormat` — LEGACY render-only template, rendered via
@@ -38,6 +42,7 @@
38
42
  * app-shell, the field widgets, and every view plugin — exactly like
39
43
  * `percentDisplayValue` / `formatMeasure`.
40
44
  */
45
+ import { isSystemManagedField } from '@object-ui/types';
41
46
  // Sentinel marking an empty-placeholder position inside a titleFormat render,
42
47
  // so adjacent separators can be stripped in a second pass.
43
48
  //
@@ -138,10 +143,20 @@ function displayNameOfEmbeddedObject(o) {
138
143
  * supported. Dotted paths (`{account.name}`) walk `$expanded` lookup objects; a
139
144
  * placeholder that resolves to a whole object is reduced to its display name.
140
145
  *
141
- * Empty placeholders (missing / null / empty fields) are stripped along with
142
- * any orphan separator they leave behind, so `"{full_name} - {company}"`
143
- * evaluated against `{ company: "Acme" }` yields `"Acme"`, not `" - Acme"`.
144
- * Returns an empty string when no placeholder resolved.
146
+ * Empty placeholders (missing / null / empty / whitespace-only fields) are
147
+ * stripped along with any orphan separator they leave behind, so
148
+ * `"{full_name} - {company}"` evaluated against `{ company: "Acme" }` yields
149
+ * `"Acme"`, not `" - Acme"`. Returns an empty string when no placeholder
150
+ * resolved.
151
+ *
152
+ * What counts as "empty" is not decided here (objectui#10446). Each
153
+ * placeholder's value goes through {@link displayValueOf}, the body of
154
+ * {@link recordDisplayValueAt}: trim, then empty. This function used to test
155
+ * the raw value against `null` / `undefined` / `''` on its own, so a
156
+ * whitespace-only value counted as RESOLVED. The later whitespace collapse
157
+ * then left its separator dangling: `{contract_no} - {name}` with a blank
158
+ * `name` rendered `HT-2026-003 -`, while every value-keyed rung of
159
+ * {@link getRecordDisplayName} judged the same value empty.
145
160
  */
146
161
  export function formatTitleTemplate(titleFormat, record) {
147
162
  let template = typeof titleFormat === 'string'
@@ -163,14 +178,11 @@ export function formatTitleTemplate(titleFormat, record) {
163
178
  break;
164
179
  value = value[p];
165
180
  }
166
- if (value && typeof value === 'object') {
167
- value = displayNameOfEmbeddedObject(value);
168
- }
169
- if (value === null || value === undefined || value === '') {
181
+ const shown = displayValueOf(value);
182
+ if (shown === undefined)
170
183
  return EMPTY_TOKEN;
171
- }
172
184
  anyResolved = true;
173
- return String(value);
185
+ return shown;
174
186
  });
175
187
  if (!anyResolved)
176
188
  return '';
@@ -283,13 +295,132 @@ export function deriveTitleField(objectDef) {
283
295
  deriveTitleFieldCache.set(objectDef, derived);
284
296
  return derived;
285
297
  }
298
+ /**
299
+ * The declared name-field pointer, read exactly as {@link getRecordDisplayName}
300
+ * reads it: the canonical `nameField` (ADR-0079 Phase 2), then its deprecated
301
+ * `displayNameField` / `NAME_FIELD_KEY` aliases.
302
+ *
303
+ * One spelling, every reader. Inside this module that is the value-space
304
+ * resolver and the name-space {@link resolveNameField}. Re-typing the `??`
305
+ * chain at another reader is how two readers drift into disagreeing about
306
+ * which field titles an object. No new alias is read here; this is the
307
+ * existing ladder, extracted.
308
+ *
309
+ * Exported (objectui#9436) for callers that must rank the DECLARED pointer on
310
+ * its own, apart from the type-aware derivation that {@link resolveNameField}
311
+ * folds in after it. The record-page H1 needs exactly that: ADR-0079 puts the
312
+ * declared pointer (steps 1+2) above the legacy `titleFormat` (step 3), and the
313
+ * derivation (step 4) below it, so a caller that interleaves its own
314
+ * `titleFormat` rendering has to read the two halves separately. Read the value
315
+ * as `recordDisplayValueAt(record, declaredNameField(objectDef))`, which is how
316
+ * {@link getRecordDisplayName} reads steps 1+2.
317
+ *
318
+ * Returns the pointer exactly as declared, or `undefined` when none is. It
319
+ * never derives: an object that declares nothing answers `undefined` even when
320
+ * its `fields` would derive a title field.
321
+ */
322
+ export function declaredNameField(objectDef) {
323
+ return objectDef?.nameField ?? objectDef?.displayNameField ?? objectDef?.NAME_FIELD_KEY;
324
+ }
325
+ /**
326
+ * WHICH FIELD titles this object — the name-space twin of
327
+ * {@link getRecordDisplayName}, which answers what that field SAYS on one
328
+ * record. Same ladder, minus the rungs that only exist per record:
329
+ *
330
+ * 1+2. the declared pointer ({@link declaredNameField});
331
+ * 4. else the type-aware derivation ({@link deriveTitleField}).
332
+ *
333
+ * The two omissions are deliberate, not oversights. `options.titleField`
334
+ * (step 0) belongs to one view, not to the object. `titleFormat` (step 3) is a
335
+ * render-only template, not a field name — there is no column, no sort key and
336
+ * no `$select` entry to be had from it, which is the same reason ADR-0079
337
+ * demoted it. A caller that wants a title for a `titleFormat`-only object wants
338
+ * {@link getRecordDisplayName}, per record.
339
+ *
340
+ * Deliberately does NOT require the answer to exist in `objectDef.fields`: in
341
+ * value-space the declared pointer is read off the RECORD, so demanding a field
342
+ * def here would make the two resolvers disagree. A caller that needs a
343
+ * RENDERABLE column checks presence itself — see {@link leadWithNameField}.
344
+ *
345
+ * Returns `undefined` when nothing declares or derives a name field.
346
+ */
347
+ export function resolveNameField(objectDef) {
348
+ const declared = declaredNameField(objectDef);
349
+ if (typeof declared === 'string' && declared.length > 0)
350
+ return declared;
351
+ return deriveTitleField(objectDef);
352
+ }
353
+ /**
354
+ * Put the object's name field at the FRONT of a **synthesized** default column
355
+ * list, so a list no author ever configured still lets you tell its rows apart.
356
+ *
357
+ * Why this exists (objectui#7245): the three faces that synthesize default list
358
+ * columns each took a declared `highlightFields` verbatim. `highlightFields` is
359
+ * the ADR-0085 "most important fields" role, and its canonical consumer — the
360
+ * detail-page highlight strip — deliberately *excludes* the title field,
361
+ * because the strip sits directly under the page H1 that already shows it. So
362
+ * well-authored metadata routinely omits the name from `highlightFields`: the
363
+ * showcase `showcase_account` declares `["status", "industry", "annual_revenue"]`
364
+ * against `nameField: "name"`, and its default grid rendered 14 rows with no
365
+ * name column and nothing to distinguish them.
366
+ *
367
+ * A list has no H1 to lean on, so the same role needs the opposite treatment
368
+ * here. This is not a new convention: `deriveLookupColumns` in `@object-ui/fields`
369
+ * already leads its picker columns with the display field and filters it out of
370
+ * the declared list — the record-picker equivalent of this problem, solved the
371
+ * same way. This makes the list faces agree with it.
372
+ *
373
+ * MOVES the name field rather than only prepending a missing one: an author who
374
+ * lists it third still gets it first, which is what "the column that identifies
375
+ * the row" means. Order among the remaining columns is untouched.
376
+ *
377
+ * NOT for author-declared column lists. A view that declares `columns` said what
378
+ * it wants; overriding that would be exactly the renderer-side second-guessing
379
+ * AGENTS.md Commandment #0.1 rules out. Every caller passes a list it
380
+ * synthesized itself.
381
+ *
382
+ * Three cases return `columns` untouched, each guarding a rule that already
383
+ * governs these column lists:
384
+ *
385
+ * - **No field def** (nothing resolved, or the pointer names a field the object
386
+ * does not carry) — a synthesized list must never fabricate a column the
387
+ * object cannot render.
388
+ * - **`hidden: true`** — the author said don't show this field. That outranks
389
+ * the fact that it also titles the record; every one of these faces already
390
+ * drops hidden fields from its own walk.
391
+ * - **DERIVED onto a system-managed field.** {@link deriveTitleField} filters
392
+ * by TYPE only, so on an object with no name-ish business field it can land
393
+ * on a framework-injected column — and leading a default list with a raw id
394
+ * is precisely the regression objectui#2702 / #2777 fixed. A *declared*
395
+ * `nameField` is exempt: `sys_migration` really does point at `id`, and an
396
+ * author's explicit designation is not a heuristic misfire.
397
+ */
398
+ export function leadWithNameField(objectDef, columns) {
399
+ const declared = declaredNameField(objectDef);
400
+ const isDeclared = typeof declared === 'string' && declared.length > 0;
401
+ const lead = isDeclared ? declared : deriveTitleField(objectDef);
402
+ const def = lead ? objectDef?.fields?.[lead] : undefined;
403
+ if (!lead || !def)
404
+ return columns;
405
+ if (def.hidden)
406
+ return columns;
407
+ if (!isDeclared && isSystemManagedField(lead, def))
408
+ return columns;
409
+ return [lead, ...columns.filter((c) => c !== lead)];
410
+ }
286
411
  /**
287
412
  * Standard name-ish keys probed directly on a *record* as a last resort before
288
413
  * the `Record #<id>` floor — used when `objectDef.fields` is absent so
289
- * {@link deriveTitleField} can't run (loosely-typed metadata, lightweight search
290
- * candidates that carry only `name`/`titleField`, etc.). Mirrors the historical
414
+ * {@link deriveTitleField} can't run (loosely-typed metadata, the lightweight
415
+ * object stub `useRecordSearch` synthesizes for a hit whose object is not in
416
+ * the app's metadata — `{ name: objectName }` — etc.). Mirrors the historical
291
417
  * hard-coded fallback chain the divergent resolvers used, so removing them is
292
418
  * non-regressive. Ordered by priority.
419
+ *
420
+ * objectui#6531 rewrote the parenthetical above: it used to say those search
421
+ * candidates "carry only `name`/`titleField`", which read as evidence that some
422
+ * producer puts `titleField` on an object-shaped payload. The census found
423
+ * none, and the spec's object schema rejects the key outright.
293
424
  */
294
425
  const NAME_ISH_RECORD_KEYS = [
295
426
  'name',
@@ -301,11 +432,57 @@ const NAME_ISH_RECORD_KEYS = [
301
432
  'subject',
302
433
  'label',
303
434
  ];
304
- /** Read a non-empty string-ish value off a record at `field`, else undefined. */
305
- function valueAt(record, field) {
435
+ /**
436
+ * Read a non-empty string-ish display value off a record at `field`, else
437
+ * `undefined`.
438
+ *
439
+ * THE definition of "this record has a value here" — the question every
440
+ * value-keyed rung of {@link getRecordDisplayName} asks, and the reason a
441
+ * whitespace-only field is not a title.
442
+ *
443
+ * ## Why this is exported (objectui#8350)
444
+ *
445
+ * A second reader existed. `record:details`' dedupe ladder in
446
+ * `@object-ui/plugin-detail` asks the same question about the same record for
447
+ * the same field, to decide which body-grid row duplicates the page H1 — and it
448
+ * asked it with a hand-written raw `undefined` / `null` / `''` test, which a
449
+ * whitespace-only value passes and this one does not. So for a record whose
450
+ * title field held only spaces the two halves disagreed about whether that
451
+ * field had a value at all: the H1 walked on to the next rung, while the grid
452
+ * hid the row anyway — a field vanished to deduplicate against a heading that
453
+ * never showed it. The ladder now calls THIS. One authority, not two
454
+ * implementations that agree today.
455
+ *
456
+ * ⛔ Do not re-spell this test at a call site, and do not "just add a trim"
457
+ * there: three things beyond the trim are part of the definition, and a second
458
+ * implementation drifts on each of them independently —
459
+ * - an expanded/embedded reference object resolves through its Salesforce-
460
+ * style display chain and is EMPTY when that yields nothing (a bare
461
+ * `{ id }` lookup payload is not a title);
462
+ * - non-strings are stringified, so `0` and `false` ARE values, not blanks;
463
+ * - `null` and `undefined` are empty, and only those two.
464
+ *
465
+ * The rule itself is {@link displayValueOf}, which {@link formatTitleTemplate}
466
+ * applies to each placeholder value (objectui#10446). So a `titleFormat`
467
+ * placeholder is empty exactly when this function would call its field empty.
468
+ */
469
+ export function recordDisplayValueAt(record, field) {
306
470
  if (!field || !record || typeof record !== 'object')
307
471
  return undefined;
308
- let v = record[field];
472
+ return displayValueOf(record[field]);
473
+ }
474
+ /**
475
+ * The value half of {@link recordDisplayValueAt}: one already-read value in,
476
+ * its display string or `undefined` (empty) out. Trim, then empty.
477
+ *
478
+ * Module-private on purpose. It exists so the two readers in this file that
479
+ * judge a single value, `recordDisplayValueAt` and each placeholder of
480
+ * {@link formatTitleTemplate}, run ONE implementation of that judgement rather
481
+ * than two that agree today (objectui#10446). Read its full definition on
482
+ * {@link recordDisplayValueAt}.
483
+ */
484
+ function displayValueOf(value) {
485
+ let v = value;
309
486
  if (v && typeof v === 'object')
310
487
  v = displayNameOfEmbeddedObject(v);
311
488
  if (v === null || v === undefined)
@@ -318,9 +495,10 @@ function valueAt(record, field) {
318
495
  *
319
496
  * Precedence (ADR-0079 Phase 2 makes the object's declared field canonical and
320
497
  * de-prioritizes the legacy `titleFormat` template):
321
- * 0. `options.titleField`, then `objectDef.titleField` (when provided &
322
- * non-empty on the record) — lets a view / object keep its author-chosen
323
- * title field.
498
+ * 0. `options.titleField` (when provided & non-empty on the record) — lets
499
+ * a view keep its author-chosen title field. The object-level
500
+ * `objectDef.titleField` leg was removed in objectui#6531: the spec's
501
+ * object schema rejects that key, so nothing could produce it.
324
502
  * 1. `objectDef.nameField` — the NEW canonical record-title pointer
325
503
  * (ADR-0079). A field name the server can return and query.
326
504
  * 2. `objectDef.displayNameField` / `objectDef.NAME_FIELD_KEY` — the
@@ -340,9 +518,23 @@ function valueAt(record, field) {
340
518
  * @param record The record data.
341
519
  */
342
520
  export function getRecordDisplayName(objectDef, record, options) {
343
- // 0. Explicit title field — caller option first, then the object's own
344
- // `titleField` hint — if it actually resolves on the record.
345
- const explicit = valueAt(record, options?.titleField) ?? valueAt(record, objectDef?.titleField);
521
+ // 0. Explicit title field — the caller's option, if it actually resolves on
522
+ // the record. This is a DECLARED key: every view config that offers a
523
+ // title pointer (`ui/CalendarConfig`, `ui/GalleryConfig`, `ui/GanttConfig`,
524
+ // `ui/ListMapConfig`, `ui/ObjectKanbanProps`, `ui/TimelineConfig`) declares
525
+ // `titleField`, and its author chose it for THIS view, so it outranks the
526
+ // object's own pointer by design.
527
+ //
528
+ // An object-level `objectDef.titleField` used to be consulted here too, as
529
+ // a second `??` leg. Removed in objectui#6531: `@objectstack/spec`'s object
530
+ // schema is a `strictObject` that REJECTS `titleField` with
531
+ // `unrecognized_keys` — the same code a nonsense key gets — so no
532
+ // spec-compliant producer can ship it, and a census across both repos
533
+ // (metadata, every `getObjectSchema` implementation, the lookup-chip and
534
+ // search-candidate paths) found none that does. Reading it was a
535
+ // consumer-side alias for an undeclared key (AGENTS.md Commandment #0.1),
536
+ // ranked ABOVE the `nameField` ADR-0079 Phase 2 made canonical.
537
+ const explicit = recordDisplayValueAt(record, options?.titleField);
346
538
  if (explicit)
347
539
  return explicit;
348
540
  // 1+2. Declared name field — the canonical `nameField` (ADR-0079 Phase 2),
@@ -350,7 +542,9 @@ export function getRecordDisplayName(objectDef, record, options) {
350
542
  // explicitly-declared field now wins over the legacy `titleFormat`
351
543
  // template (step 3): an object that sets `nameField` resolves via it even
352
544
  // if it also carries a stale `titleFormat`.
353
- const declared = valueAt(record, objectDef?.nameField ?? objectDef?.displayNameField ?? objectDef?.NAME_FIELD_KEY);
545
+ // The `??` chain itself lives in {@link declaredNameField} so the
546
+ // name-space {@link resolveNameField} reads the identical ladder.
547
+ const declared = recordDisplayValueAt(record, declaredNameField(objectDef));
354
548
  if (declared)
355
549
  return declared;
356
550
  // 3. titleFormat (LEGACY, render-only template). DEPRECATED by ADR-0079: the
@@ -366,7 +560,7 @@ export function getRecordDisplayName(objectDef, record, options) {
366
560
  }
367
561
  // 4. Type-aware derivation from the object's fields.
368
562
  const derivedField = deriveTitleField(objectDef);
369
- const derived = valueAt(record, derivedField);
563
+ const derived = recordDisplayValueAt(record, derivedField);
370
564
  if (derived)
371
565
  return derived;
372
566
  // 4b. Name-ish keys on the record itself — the safety net for when the
@@ -377,7 +571,7 @@ export function getRecordDisplayName(objectDef, record, options) {
377
571
  if (options?.deriveFromRecordKeys !== false && record && typeof record === 'object') {
378
572
  // (i) exact standard keys (name, full_name, title, …).
379
573
  for (const key of NAME_ISH_RECORD_KEYS) {
380
- const v = valueAt(record, key);
574
+ const v = recordDisplayValueAt(record, key);
381
575
  if (v)
382
576
  return v;
383
577
  }
@@ -392,7 +586,7 @@ export function getRecordDisplayName(objectDef, record, options) {
392
586
  if (k === 'id' || k === '_id' || k.endsWith('_id') || k.startsWith('_'))
393
587
  continue;
394
588
  if (k.endsWith('_name') || k.endsWith('_title') || k.startsWith('name_')) {
395
- const v = valueAt(record, key);
589
+ const v = recordDisplayValueAt(record, key);
396
590
  if (v)
397
591
  return v;
398
592
  }
@@ -1,36 +1,35 @@
1
1
  /**
2
- * ObjectUI — reference-key canonicalization
2
+ * ObjectUI — object-schema key canonicalization at ingestion
3
3
  * Copyright (c) 2024-present ObjectStack Inc.
4
4
  *
5
5
  * This source code is licensed under the MIT license found in the
6
6
  * LICENSE file in the root directory of this source tree.
7
- */
8
- /**
9
- * Backend object schemas follow the ObjectStack convention and name a
10
- * relational field's target object `reference`
11
- * (e.g. `{ type: 'lookup', reference: 'showcase_account' }`), while ObjectUI's
12
- * types — and most in-repo consumers — historically read `reference_to`
13
- * (some legacy configs also carry camelCase `referenceTo`). A consumer that
14
- * reads only one key silently loses the relation under the other convention:
15
- * the exact bug HeaderHighlight had (#2407 / PR #2587), where a served
16
- * `reference`-keyed lookup rendered a raw id.
17
7
  *
18
- * `normalizeFieldReferenceKeys` stamps BOTH snake_case keys onto the field
19
- * definition whenever any of the three spellings is present, so downstream
20
- * reads work regardless of which single key they check. It mutates the field
21
- * in place — the ObjectStack adapter caches the schema object and re-serves
22
- * it, so the one pass must stick — and is idempotent. Keys that are already
23
- * set are never overwritten.
8
+ * ⚠️ The FILE and its exports still say `reference`; the pass no longer does
9
+ * only that. The `reference` / `reference_to` pair was the first tenant, and
10
+ * objectui#7650 added the retired-dialect arm below, which folds ANY undeclared
11
+ * snake twin of a declared `FieldSchema` key. Renaming the exports would be a
12
+ * published-export change across ~20 in-repo citations and belongs to its own
13
+ * card; the two arms are marked so the name cannot mislead a reader who got
14
+ * here from a grep.
24
15
  */
25
- export declare function normalizeFieldReferenceKeys<T>(fieldDef: T): T;
16
+ /** Reset the warn-once memos. Exported for tests. */
17
+ export declare function resetReferenceKeyWarnings(): void;
18
+ export declare function normalizeFieldReferenceKeys<T>(fieldDef: T, fieldName?: string, objectName?: string): T;
26
19
  /**
27
20
  * Apply {@link normalizeFieldReferenceKeys} to every field of an object
28
- * schema. Accepts both field-container shapes the metadata API serves —
29
- * a `name → def` map or an array of defs — and tolerates anything else by
30
- * returning the input untouched. Mutates in place; idempotent.
21
+ * schema — BOTH arms: the `reference` fold and the retired dialect. Accepts
22
+ * both field-container shapes the metadata API serves — a `name → def` map or
23
+ * an array of defs — and tolerates anything else by returning the input
24
+ * untouched. Mutates in place; idempotent.
31
25
  *
32
26
  * This is meant to run at the choke point where object schemas enter the
33
27
  * client (`ObjectStackAdapter.getObjectSchema`, the app-shell metadata
34
- * provider) so per-consumer dual-key fallbacks can't drift.
28
+ * provider), so no consumer needs a dual-key fallback of its own.
29
+ *
30
+ * Field NAMES and the OBJECT name are forwarded so the dev-mode warning above
31
+ * can name the whole producer site rather than half of it: the map form keys
32
+ * the def by name, the array form carries it as `def.name`, and the object name
33
+ * comes off `schema.name`.
35
34
  */
36
35
  export declare function normalizeSchemaReferenceKeys<T>(schema: T): T;