@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
@@ -32,7 +32,10 @@
32
32
  * predicate.)
33
33
  *
34
34
  * Since objectstack#8018 the same reader also carries the non-predicate row keys
35
- * a view's actions read — today just `recordIdField`. The name kept the
35
+ * a view's actions read: `recordIdField`, since objectui#10277 the field a
36
+ * `defaultFromRow` param seeds from and the `{field}` tokens of an action's
37
+ * `target`, and since objectui#10404 the fields an `undoable` action writes,
38
+ * whose prior values the Undo capture reads off the row. The name kept the
36
39
  * "predicate" spelling because the mechanism is identical (harvest a name, gate
37
40
  * it against the declared fields, add it to `$select`); see
38
41
  * {@link listViewPredicates} for what is in the set and why.
@@ -88,6 +91,77 @@ const RECORD_REF = /\b(?:record|data)\.([A-Za-z_][A-Za-z0-9_]*)/g;
88
91
  * declaration contributes nothing instead of contributing a plausible wrong name.
89
92
  */
90
93
  const BARE_IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]*$/;
94
+ /**
95
+ * A `{field}` token in an action's `target` — the grammar the console's `api`
96
+ * handler (`useConsoleActionRuntime` in `@object-ui/app-shell`) substitutes from
97
+ * the row record, spelled exactly as that handler spells it, so the harvest
98
+ * reads the same tokens it fills. The capture is a bare identifier by
99
+ * construction: a dotted path (`{owner.name}`), an expression (`{a + b}`) and
100
+ * the runner's own `${param.X}` / `${ctx.X}` scopes all fall outside it and
101
+ * match nothing here — this grammar does not read them off the row, so they owe
102
+ * the projection nothing.
103
+ */
104
+ const TARGET_ROW_TOKEN = /\{([a-z_][a-z0-9_]*)\}/gi;
105
+ /**
106
+ * The row key a `defaultFromRow` param seeds its value from — `field` wins,
107
+ * because row data is keyed by object field; `name` is the fallback. The same
108
+ * precedence `resolveActionParam` (`@object-ui/fields`) reads, which is the
109
+ * point: the projection must carry the key the runtime looks up, not the
110
+ * param's payload name. `remove_team_member` seeds `teamId` from `team_id`.
111
+ */
112
+ function defaultFromRowKey(param) {
113
+ if (!param || typeof param !== 'object')
114
+ return undefined;
115
+ const p = param;
116
+ if (!p.defaultFromRow)
117
+ return undefined;
118
+ return p.field ?? p.name;
119
+ }
120
+ /**
121
+ * The keys an `undoable` action WRITES, which are the keys its Undo capture
122
+ * reads off the row (objectui#10404). Returned unfiltered; the caller applies
123
+ * the identifier gate.
124
+ *
125
+ * Each source is the one a writer actually merges into the written set:
126
+ *
127
+ * - `patch`: the static field values of `operation: 'update'`, merged under the
128
+ * collected params (`ActionRunner`'s `executeUpdateOperation`). Every def
129
+ * this harvest reads is a definition its writer applies whole (a view's
130
+ * resolved row def, a `bulkActionDefs` entry, an object's action), never a
131
+ * page action row, so this is the declared `patch` the write applies: the
132
+ * one the runner's Undo snapshot reads too (objectui#13022);
133
+ * - each param's COLLECTED key, `name ?? field`: the key the param dialog
134
+ * stores its answer under (`paramName` in `app-shell`'s
135
+ * `resolveActionParams`). This is the opposite precedence to
136
+ * {@link defaultFromRowKey} on purpose: that one asks which row key a value
137
+ * is READ from, this one asks which key a value is WRITTEN under, and the
138
+ * capture looks the written key up on the row;
139
+ * - `bodyExtra`: the static body the console's generic `api` handler merges
140
+ * over the params before it writes (`useConsoleActionRuntime`).
141
+ *
142
+ * `recordId` is left out: both writers read it as the record's address and
143
+ * strip it from the written set, so no capture ever looks it up.
144
+ *
145
+ * A key the harvest cannot make the row carry (undeclared, or a field the
146
+ * principal may not read) is not rescued here. The capture refuses to invent a
147
+ * prior value for it instead, and offers no Undo.
148
+ */
149
+ function undoableWrittenKeys(def) {
150
+ const keys = [];
151
+ for (const bag of [def.patch, def.bodyExtra]) {
152
+ if (bag && typeof bag === 'object' && !Array.isArray(bag))
153
+ keys.push(...Object.keys(bag));
154
+ }
155
+ if (Array.isArray(def.params)) {
156
+ for (const param of def.params) {
157
+ if (!param || typeof param !== 'object')
158
+ continue;
159
+ const p = param;
160
+ keys.push(p.name ?? p.field);
161
+ }
162
+ }
163
+ return keys.filter((key) => key !== 'recordId');
164
+ }
91
165
  /** Pull the CEL source out of any of the shapes a predicate is authored in. */
92
166
  function predicateSource(pred) {
93
167
  if (typeof pred === 'string')
@@ -139,24 +213,54 @@ export function collectPredicateFieldRefs(predicates) {
139
213
  * kebab (custom defs AND the built-in Edit/Delete overrides), the selection bar,
140
214
  * and the object's declared actions.
141
215
  *
142
- * Most entries are predicates. Two are not, and both are spelled as a synthetic
216
+ * Most entries are predicates. Five are not, and all are spelled as a synthetic
143
217
  * `record.<name>` so the one harvester handles them: conditional formatting's
144
- * native `{ field, operator, value }` shape, and an action's `recordIdField`
145
- * (objectstack#8018). The `recordIdField` case is the same *class* of bug the
146
- * predicate harvest exists to close, one surface over — the action runtime reads
218
+ * native `{ field, operator, value }` shape, an action's `recordIdField`
219
+ * (objectstack#8018), the field each `defaultFromRow` param seeds from, each
220
+ * `{field}` token of the action's `target` (both objectui#10277), and the fields
221
+ * an `undoable` action writes (objectui#10404). The
222
+ * `recordIdField` case is the same *class* of bug the predicate harvest exists
223
+ * to close, one surface over — the action runtime reads
147
224
  * `rowRecord[action.recordIdField]` to seed `recordIdParam`, so a key outside the
148
225
  * listView columns arrived absent and the injection was silently skipped, which
149
226
  * turns a record-scoped mutation into one that names no record while still
150
227
  * reporting success. The default (`id`) is already projected unconditionally, so
151
228
  * only an explicit declaration adds anything here.
152
229
  *
153
- * A `recordIdField` that is not a bare identifier is dropped here, and one naming
154
- * a field the object does not declare is dropped by the caller's
155
- * `isProjectableField` guard. Both drops are the safe direction: such a name is
156
- * not a column anywhere, and an unknown key in `$select` is not ignored by every
157
- * backend. The identifier gate is not decoration — without it the harvester's
158
- * regex would read a PREFIX of a malformed name (`record.not a field` → `not`)
159
- * and contribute a plausible wrong field instead of nothing.
230
+ * The last two are the same class again, and they were silent in the same way.
231
+ * `resolveActionParam` seeds a `defaultFromRow` param only when the row HAS the
232
+ * key (an own-property check), so an absent key opened the param dialog with the
233
+ * field blank; and the console's `api` handler fills a `{field}` token from the
234
+ * row, so an absent key put an empty path segment into the URL. The key a param
235
+ * seeds from is `field ?? name` — the runtime's precedence, not the param's
236
+ * payload name — and the token grammar is the handler's own. Neither is
237
+ * narrowed by the action's `type`, for the asymmetry stated below: an extra
238
+ * column costs bytes.
239
+ *
240
+ * The written fields are the same class once more, and the worst of them,
241
+ * because what went wrong was a write. The Undo capture of an `undoable` update
242
+ * reads each written field's prior value off the row; on a projected row a
243
+ * written field no column shows was absent, the capture recorded `null`, and
244
+ * Undo then wrote `null` over the stored value it existed to restore. The
245
+ * written keys are `patch`, each param's collected key and `bodyExtra` (see
246
+ * `undoableWrittenKeys`). This one IS narrowed, to `undoable`, and not for
247
+ * cost: the capture is the only reader of a written field's prior value, and it
248
+ * runs only under `undoable`, so without it the row reads none of these keys.
249
+ *
250
+ * A `recordIdField`, param key or written key that is not a bare identifier is
251
+ * dropped here
252
+ * (a `target` token is one by construction of its grammar), and a name the
253
+ * object does not declare is dropped by the caller's `isProjectableField`
254
+ * guard. Both drops are the safe direction: such a name is not a column
255
+ * anywhere, and an unknown key in `$select` is not ignored by every backend.
256
+ * The identifier gate is not decoration — without it the harvester's regex would
257
+ * read a PREFIX of a malformed name (`record.not a field` → `not`) and
258
+ * contribute a plausible wrong field instead of nothing.
259
+ *
260
+ * ⛔ Harvesting is not a read grant. Every name here is a CANDIDATE: each
261
+ * consumer still passes it through its own field-level read filter before it
262
+ * reaches `$select`, so a field the principal may not read is not requested
263
+ * because an action names it.
160
264
  *
161
265
  * `objectActions` is deliberately the object's WHOLE action set rather than
162
266
  * only the ones this view promotes. Narrowing it would mean re-running the
@@ -191,6 +295,31 @@ export function listViewPredicates(view) {
191
295
  if (typeof d.recordIdField === 'string' && BARE_IDENTIFIER.test(d.recordIdField)) {
192
296
  preds.push(`record.${d.recordIdField}`);
193
297
  }
298
+ // A `defaultFromRow` param's seed field and the `{field}` tokens of the
299
+ // action's `target` (objectui#10277) — two more row keys the runtime
300
+ // READS, by the same rule and the same identifier gate.
301
+ if (Array.isArray(d.params)) {
302
+ for (const param of d.params) {
303
+ const key = defaultFromRowKey(param);
304
+ if (typeof key === 'string' && BARE_IDENTIFIER.test(key))
305
+ preds.push(`record.${key}`);
306
+ }
307
+ }
308
+ if (typeof d.target === 'string') {
309
+ TARGET_ROW_TOKEN.lastIndex = 0;
310
+ for (let m = TARGET_ROW_TOKEN.exec(d.target); m; m = TARGET_ROW_TOKEN.exec(d.target)) {
311
+ preds.push(`record.${m[1]}`);
312
+ }
313
+ }
314
+ // The fields an `undoable` action writes (objectui#10404): its Undo
315
+ // capture reads each one's prior value off the row. Same rule, same
316
+ // identifier gate.
317
+ if (d.undoable) {
318
+ for (const key of undoableWrittenKeys(d)) {
319
+ if (typeof key === 'string' && BARE_IDENTIFIER.test(key))
320
+ preds.push(`record.${key}`);
321
+ }
322
+ }
194
323
  }
195
324
  }
196
325
  for (const override of Object.values(view.userActions ?? {})) {
@@ -0,0 +1,75 @@
1
+ /**
2
+ * ObjectUI
3
+ * Copyright (c) 2024-present ObjectStack Inc.
4
+ *
5
+ * This source code is licensed under the MIT license found in the
6
+ * LICENSE file in the root directory of this source tree.
7
+ */
8
+ /**
9
+ * Resolve the colour a record's value stands for, off the field's own metadata.
10
+ *
11
+ * ## Why this is shared (objectui#7243)
12
+ *
13
+ * `colorField` is one authored key — `gantt.colorField`, `calendar.colorField`,
14
+ * `timeline.colorField` — and it used to mean three different things:
15
+ *
16
+ * - `plugin-timeline` resolved the field's option `color` (correct);
17
+ * - `plugin-calendar` passed the raw value on to a deterministic hash, so the
18
+ * colour the author declared on the option was never consulted;
19
+ * - `plugin-gantt` passed the raw value straight into `backgroundColor`, so
20
+ * `colorField: 'status'` emitted `backgroundColor: "open"` — not a colour.
21
+ * The browser dropped the declaration and every bar rendered identically,
22
+ * which made DECLARING the documented key strictly worse than omitting it.
23
+ *
24
+ * The timeline's resolver is lifted here verbatim so all three answer the same
25
+ * question the same way. What lives here is the part that is genuinely common —
26
+ * the two rungs that depend only on the field metadata and the stored value:
27
+ *
28
+ * 1. the field's own option `color` for that value;
29
+ * 2. the value itself when it already IS a CSS colour literal.
30
+ *
31
+ * The LAST rung deliberately stays with each caller, because each one has a
32
+ * different right answer for "a value that is neither": the gantt derives a
33
+ * semantic token hex (a bar must be painted), the calendar hashes onto its
34
+ * theme-aware 8-stop class palette (a soft tint, not a solid fill), and the
35
+ * timeline draws its default marker variant. Pulling those together would be a
36
+ * repaint of existing views, not a bug fix.
37
+ *
38
+ * ## Keyed by option `value` only
39
+ *
40
+ * Deliberately NOT `buildOptionColorMap` (`./chart-series.ts`), which keys by
41
+ * value AND display label: that one reads dataset ROWS, where the server may
42
+ * have resolved a select dimension to its label. These callers read RECORDS,
43
+ * where `record[colorField]` is the stored value. Keying by label as well would
44
+ * colour a record whose stored value happens to collide with another option's
45
+ * label — a tolerance nothing here needs, and one the timeline never had.
46
+ */
47
+ /** One entry of a select-like field's `options`, as far as colour is concerned. */
48
+ export interface FieldColorOption {
49
+ value?: unknown;
50
+ color?: unknown;
51
+ }
52
+ /** The slice of a field definition this resolver reads. */
53
+ export interface FieldColorDefinition {
54
+ options?: unknown;
55
+ }
56
+ /** Does this string already name a colour the DOM can paint? */
57
+ export declare function isColorLiteral(value: string): boolean;
58
+ /**
59
+ * Build a `value -> option colour` lookup from a field definition's `options`.
60
+ * Returns an empty map for a field with no options, so callers need no guard.
61
+ */
62
+ export declare function buildFieldColorMap(fieldDef: FieldColorDefinition | undefined | null): Record<string, string>;
63
+ /**
64
+ * Build the per-field colour resolver: rung 1 (option colour) then rung 2 (the
65
+ * value is already a colour literal), else `undefined` so the caller runs its
66
+ * own last rung.
67
+ *
68
+ * A factory rather than a plain function because callers resolve one field
69
+ * across every row of a view — the option map is built once, not per record.
70
+ *
71
+ * @param fieldDef `objectSchema.fields[colorField]`, or `undefined` when the
72
+ * view has no schema in hand (an inline-data or `api`-provider view). With no
73
+ * options, rung 1 is a no-op and rung 2 still answers for literal colours.
74
+ */
75
+ export declare function createFieldColorResolver(fieldDef: FieldColorDefinition | undefined | null): (value: unknown) => string | undefined;
@@ -0,0 +1,65 @@
1
+ /**
2
+ * ObjectUI
3
+ * Copyright (c) 2024-present ObjectStack Inc.
4
+ *
5
+ * This source code is licensed under the MIT license found in the
6
+ * LICENSE file in the root directory of this source tree.
7
+ */
8
+ /**
9
+ * Colour literals the renderers accept as-is.
10
+ *
11
+ * Hex accepts 3, 6 and 8 digits. The timeline's private regex accepted 3 and 6;
12
+ * `plugin-calendar`'s accepted 3, 6 and 8 — two in-repo spellings of the same
13
+ * question, and the narrow one is the only one under which a VALID CSS colour
14
+ * (`#rrggbbaa`) could fall past this rung and be replaced by a derived colour.
15
+ * Widening can only turn "not recognised" into "the author's colour"; it can
16
+ * never turn a colour into something else. So the wider spelling wins.
17
+ */
18
+ const HEX_COLOR_RE = /^#([0-9a-f]{3}|[0-9a-f]{6}|[0-9a-f]{8})$/i;
19
+ /** Does this string already name a colour the DOM can paint? */
20
+ export function isColorLiteral(value) {
21
+ return HEX_COLOR_RE.test(value) || value.startsWith('rgb') || value.startsWith('hsl');
22
+ }
23
+ /**
24
+ * Build a `value -> option colour` lookup from a field definition's `options`.
25
+ * Returns an empty map for a field with no options, so callers need no guard.
26
+ */
27
+ export function buildFieldColorMap(fieldDef) {
28
+ const options = fieldDef?.options;
29
+ if (!Array.isArray(options))
30
+ return {};
31
+ const map = {};
32
+ for (const opt of options) {
33
+ if (!opt || typeof opt !== 'object')
34
+ continue;
35
+ if (opt.value == null)
36
+ continue;
37
+ if (typeof opt.color === 'string' && opt.color)
38
+ map[String(opt.value)] = opt.color;
39
+ }
40
+ return map;
41
+ }
42
+ /**
43
+ * Build the per-field colour resolver: rung 1 (option colour) then rung 2 (the
44
+ * value is already a colour literal), else `undefined` so the caller runs its
45
+ * own last rung.
46
+ *
47
+ * A factory rather than a plain function because callers resolve one field
48
+ * across every row of a view — the option map is built once, not per record.
49
+ *
50
+ * @param fieldDef `objectSchema.fields[colorField]`, or `undefined` when the
51
+ * view has no schema in hand (an inline-data or `api`-provider view). With no
52
+ * options, rung 1 is a no-op and rung 2 still answers for literal colours.
53
+ */
54
+ export function createFieldColorResolver(fieldDef) {
55
+ const colorByValue = buildFieldColorMap(fieldDef);
56
+ return (value) => {
57
+ if (value == null || value === '')
58
+ return undefined;
59
+ const key = String(value);
60
+ const optionColor = colorByValue[key];
61
+ if (optionColor)
62
+ return optionColor;
63
+ return isColorLiteral(key) ? key : undefined;
64
+ };
65
+ }
@@ -0,0 +1,298 @@
1
+ /**
2
+ * ObjectUI — the shared record-source readers: the ruled three-rung ladder
3
+ * (`resolveRecordSourceConfig`) and the object-name it resolves to
4
+ * (`resolveRecordSourceObjectName`)
5
+ * Copyright (c) 2024-present ObjectStack Inc.
6
+ *
7
+ * This source code is licensed under the MIT license found in the
8
+ * LICENSE file in the root directory of this source tree.
9
+ */
10
+ import type { ViewData } from '@object-ui/types';
11
+ /**
12
+ * The object a view block is bound to, resolved ONCE for the whole renderer
13
+ * (`b041b9c0c`).
14
+ *
15
+ * ## The question this answers, and the one it does not
16
+ *
17
+ * There are TWO separately-ruled precedence questions about `objectName`, and
18
+ * they only look like one question when a single reader is asked to answer
19
+ * both:
20
+ *
21
+ * 1. **Which object does this block RESOLVE, at render time, when it carries
22
+ * more than one binding?** — the published three-rung record-source ladder
23
+ * (`data`, then `staticData`, then `objectName`), declared on both faces of
24
+ * the contract (`ObjectMapSchema.objectName` / `ObjectGanttSchema.objectName`
25
+ * in `@object-ui/types`, and the `.describe` on their zod twins — the map
26
+ * twin's reads *"objectName — the THIRD record source `getDataConfig`
27
+ * resolves, after `data` and `staticData`"*, naming `ObjectMap.tsx`'s local
28
+ * wrapper; the gantt twin names `resolveRecordSourceConfig` itself), ruled
29
+ * 2026-09-02 (landed as `77cb489b4`) and pinned by
30
+ * `objectql-record-source-refinement-6939.test.ts`. **That is this
31
+ * function.**
32
+ * 2. **How does `objectName` get POPULATED when it is absent?** — the
33
+ * authoring-time gap-fill in `normalizeListViewSchema` (objectui#7477,
34
+ * ruling B, landed as `00d2fa682`), where an `objectName` already on the schema WINS
35
+ * and the `data` block only fills a gap: *"it can never re-point a binding
36
+ * that already resolves."*
37
+ *
38
+ * The two are NOT merged and neither is re-pointed at the other. Merging them
39
+ * would override a standing maintainer ruling in whichever direction the merged
40
+ * reader happened to pick: at the sites below the binding that already resolves
41
+ * IS `data.object`, so ruling B's own words argue for keeping rung 1 as it is.
42
+ *
43
+ * ## Why `staticData` does not appear here
44
+ *
45
+ * The ladder's second rung wraps inline rows as `{ provider: 'value', items }`,
46
+ * which names no object at all. So for the object-NAME question the three-rung
47
+ * ladder reduces to two rungs — the resolved config's object when it names one,
48
+ * else the schema's own `objectName`, which is what a `value`-backed block
49
+ * still needs for metadata reads, i18n field labels and permission verdicts.
50
+ * Callers pass the ALREADY-RESOLVED config (their
51
+ * {@link resolveRecordSourceConfig} output, direct or through a local wrapper),
52
+ * so rung ordering is settled before this function is reached.
53
+ *
54
+ * ## No lenient rung was added (AGENTS.md #0.1)
55
+ *
56
+ * `ViewDataSchema`'s `object` provider is a `strictObject` carrying exactly
57
+ * `{ provider, object }` with `object` REQUIRED, so `{ provider: 'object' }`
58
+ * without an `object` is off-contract. This reader does not coerce that shape
59
+ * back to `objectName`; the two call sites that used to (`ObjectGrid`'s
60
+ * `'object' in dataConfig` test and `ObjectTree`'s header `?? schema.objectName`
61
+ * tail) keep their own tail at the site, so the collapse changes nothing they
62
+ * resolve today while the shared rung stays contract-strict.
63
+ *
64
+ * @param schema - The block's schema; only `objectName` is read.
65
+ * @param dataConfig - The RESOLVED data config — the caller's own
66
+ * {@link resolveRecordSourceConfig} output, `null` when nothing is bound.
67
+ * @returns The bound object's name, or `undefined` when neither the resolved
68
+ * config nor the schema names one.
69
+ *
70
+ * @example
71
+ * ```ts
72
+ * const dataConfig = useMemo(() => resolveRecordSourceConfig(schema, 'view-data'), [schema]);
73
+ * const objectName = resolveRecordSourceObjectName(schema, dataConfig);
74
+ * ```
75
+ */
76
+ export declare function resolveRecordSourceObjectName(schema: {
77
+ objectName?: string;
78
+ } | null | undefined, dataConfig: {
79
+ provider?: string;
80
+ object?: string;
81
+ } | null | undefined): string | undefined;
82
+ /**
83
+ * Which arm of `data` a block's PUBLISHED row declares — the only shape rung 1
84
+ * of the ladder honours for that block (objectui#8348).
85
+ *
86
+ * Maintainer ruling, decision batch #83 (2026-09-08), verbatim: 「8348 以协议为准」
87
+ * — the contract decides. A renderer honours the `data` spelling its block's
88
+ * published row declares and no other: what `os validate` and the save gate
89
+ * refuse, the renderer refuses too.
90
+ *
91
+ * - `'view-data'` — the row is the spec's `ViewData` discriminated union, four
92
+ * strict OBJECT arms on `provider`. A bare array under `data` is NOT a record
93
+ * source for such a block.
94
+ * - `'array'` — the row is `z.array(...)`: an array of PRE-FETCHED RECORDS. The
95
+ * `{ provider, items }` config object is NOT a record source for such a
96
+ * block.
97
+ * - `'undeclared'` — no published face declares a `data` row for the block at
98
+ * all, so neither arm of the ruling reaches it and rung 1 keeps its pre-8348
99
+ * verbatim behaviour. ⛔ NOT a tolerance to copy: it is the honest answer for
100
+ * a block the ruling does not decide, and it is reported rather than guessed.
101
+ * No ladder block passes it any more: `object-tree`, the last one that did,
102
+ * gained a published row and moved to `'view-data'` (objectui#8348, ruling
103
+ * batch #136 item 3, Q1-C). It stays the answer
104
+ * {@link recordSourceDataArmForType} gives a type it does not list.
105
+ *
106
+ * The arm is passed BY THE CALL SITE rather than looked up from `schema.type`
107
+ * on purpose. Most of these renderers are registered twice — `object-grid`
108
+ * and the `view:grid` alias `grid`, `object-calendar` and `calendar`, and so on
109
+ * (the bare `gantt` and `map` keys are retired, objectui#8008 / objectui#10393)
110
+ * — so a node reaches the same component under either spelling, and a table
111
+ * keyed by `type` would answer for one tag and silently miss the other. A
112
+ * REQUIRED parameter makes the arm a compile-time obligation at each of the
113
+ * five sites instead.
114
+ */
115
+ export type RecordSourceDataArm = 'view-data' | 'array' | 'undeclared';
116
+ /**
117
+ * The authored `data` shape a block ON THAT ARM may declare — the DECLARATION
118
+ * half of the same per-block ruling {@link authoredDataIsOnTheDeclaredArm}
119
+ * decides at RUNTIME (`ab856ed30`).
120
+ *
121
+ * ## The contradiction this closes
122
+ *
123
+ * {@link resolveRecordSourceConfig} used to declare its `data` parameter as a
124
+ * flat `ViewData`, while its own docblock, its REQUIRED `dataArm` parameter and
125
+ * its rung-1 predicate all say the honoured shape DIFFERS PER BLOCK — and
126
+ * `ViewData` is a discriminated union over four strict OBJECT arms with no
127
+ * array member. So the signature refused the one block whose published row IS
128
+ * the array arm, and the arm parameter that exists to make the row a
129
+ * compile-time obligation was the one thing the compiler ignored. The
130
+ * contradiction was held shut at `ObjectCalendar.tsx`'s call site by an
131
+ * `as ViewData | undefined` cast whose own comment reported it as an upstream
132
+ * defect rather than a licence.
133
+ *
134
+ * ⛔ The repair is NOT a union of the arms. `ViewData | unknown[]` would let a
135
+ * `'view-data'` site declare a bare array and a `'array'` site declare a
136
+ * provider block — each of them the shape that block's row REFUSES, each of
137
+ * them compiling — which is the repo-wide default {@link RecordSourceDataArm}'s
138
+ * docblock refuses by name: *"a default is how the second de-facto contract got
139
+ * in"* (AGENTS.md #0.1).
140
+ *
141
+ * - `'view-data'` — the spec's `ViewData`. UNCHANGED, and that matters: this
142
+ * is exactly what the flat declaration said, and it was right for the three
143
+ * blocks on this arm, so nothing moves for `object-grid`, `object-map` or
144
+ * `object-gantt`.
145
+ * - `'array'` — `unknown[]`, the shape `object-calendar`'s published row
146
+ * declares (`ComponentPropsMap['object-calendar'].data` is
147
+ * `z.array(z.unknown()).optional()`, *"Pre-fetched records"*).
148
+ * - `'undeclared'` — `unknown`. No published face declares a `data` row for
149
+ * such a block, so rung 1 keeps its pre-8348 verbatim behaviour and honours
150
+ * ANY truthy value. Anything narrower here would be a rule nobody has ruled.
151
+ *
152
+ * ⚠️ RUNTIME IS UNMOVED. This type is erased; {@link authoredDataIsOnTheDeclaredArm}
153
+ * still takes `unknown` and still decides rung 1 on the value's KIND, so every
154
+ * runtime verdict — including the off-arm ones — is byte-for-byte what it was.
155
+ * `record-source-config.behaviourNeutrality-7632.test.ts` passes its fixtures
156
+ * through `as any` precisely so it keeps measuring that, whatever the
157
+ * declaration says.
158
+ *
159
+ * ⚠️ DISTRIBUTIVE on purpose, and that is why this is a conditional type rather
160
+ * than a set of overloads. A caller holding a `RecordSourceDataArm`-typed
161
+ * VARIABLE instead of a literal gets `ViewData | unknown[] | unknown`, i.e.
162
+ * `unknown` — the honest answer when the arm is not decided at compile time.
163
+ * The same caller would match NONE of three literal-armed overloads, which
164
+ * would turn a working call into an error for no contract reason.
165
+ */
166
+ export type AuthoredRecordSourceData<Arm extends RecordSourceDataArm> = Arm extends 'view-data' ? ViewData : Arm extends 'array' ? unknown[] : unknown;
167
+ /**
168
+ * The block's record source, resolved from the ruled three-rung ladder
169
+ * (`ce2aaefe1`), with rung 1 judged against the block's own published `data`
170
+ * row (objectui#8348).
171
+ *
172
+ * ## The ruled contract this is the ONE implementation of
173
+ *
174
+ * `data`, then `staticData`, then `objectName` — declared on both faces of the
175
+ * published contract and pinned by
176
+ * `objectql-record-source-refinement-6939.test.ts`. Quoted here from the
177
+ * `ObjectMapSchema` faces, whose `getDataConfig` is `ObjectMap.tsx`'s local
178
+ * wrapper around this function; the gantt and calendar faces name
179
+ * `resolveRecordSourceConfig` instead (objectui#9618):
180
+ *
181
+ * 1. **`data`** — *"Data source configuration. Read FIRST by `getDataConfig`"*,
182
+ * honoured ONLY on the arm `dataArm` names. Returned verbatim, so a config
183
+ * on the declared arm reaches the caller exactly as the author wrote it.
184
+ * 2. **`staticData`** — *"Inline records — read SECOND by `getDataConfig`,
185
+ * wrapped into a `{ provider: value }` config"*.
186
+ * 3. **`objectName`** — *"the THIRD record source `getDataConfig` resolves,
187
+ * after `data` and `staticData`"*, folded to `{ provider: 'object' }`.
188
+ *
189
+ * `null` when none of the three is present — the same "nothing is drawn" signal
190
+ * the zod `requireRecordSource` refinement is written against.
191
+ *
192
+ * This is the PRODUCER whose output {@link resolveRecordSourceObjectName} (the
193
+ * reader `b041b9c0c` published) consumes; that function's docblock describes the same
194
+ * ladder from the consuming end. Five plugins — calendar, gantt, grid, map and
195
+ * tree — each carried a hand-copy of this ladder with no gate holding them
196
+ * together, which is the AGENTS.md #0.1 drift class: a change to the ruled
197
+ * order had five edit sites and nothing noticed a missed one.
198
+ *
199
+ * ## Rung 1 is judged against the block's own row (objectui#8348)
200
+ *
201
+ * ⛔ This docblock used to state, as a fact about the whole ladder, that *"an
202
+ * array under `data` cannot be published"*. That is true of the blocks whose row
203
+ * is `ViewData` and FALSE of `object-calendar`, whose published row
204
+ * (`ComponentPropsMap['object-calendar'].data` on `@objectstack/spec` 17.4.0) is
205
+ * `z.array(z.unknown()).optional()` — *"Pre-fetched records — skips the internal
206
+ * fetch"*. Both directions of that disagreement were live at once: the renderers
207
+ * honoured the `{ provider, items }` object on a block whose row refuses it by
208
+ * kind, and `ObjectGrid` / `ObjectMap` lifted a bare array on blocks whose row
209
+ * refuses THAT by kind. Decision batch #83 settled it — the row decides — and
210
+ * `dataArm` is where each block says which row it has.
211
+ *
212
+ * JUDGED, per block, against that block's `ComponentPropsMap` row in the
213
+ * INSTALLED `@objectstack/spec` — and ⛔ the verdicts are not written down here
214
+ * as facts, because a spec release can move a row (AGENTS.md #9). Each pin
215
+ * below derives the arm from the row at test time and compares it with what the
216
+ * block passes, so a moved row turns it red:
217
+ *
218
+ * - `object-grid` — `'view-data'`; `gridBareArrayDataRefused-8348.test.tsx`,
219
+ * its "the spec row is what all of the above is judged against" test. The
220
+ * row's own description names the refusal: *"the bare-array shortcut is
221
+ * refused — see migration `object-grid-data-view-data-converged`"*.
222
+ * - `object-calendar` — `'array'`; the test of the same name in
223
+ * `ObjectCalendar.dataObjectArmRefused-8348.test.tsx`. The registration
224
+ * publishes the same arm (`{ name: 'data', type: 'array' }`).
225
+ * - `object-map`, `object-gantt`, `object-tree` — `'view-data'`; one
226
+ * `dataArmSpecRow-8348` pin per block, in each plugin's own tests. These
227
+ * three rows are what ruling batch #136 item 3 (Q1-C) had the protocol
228
+ * gain; until they were installable, map and gantt were judged through this
229
+ * repo's own `ObjectMapSchema.data` / `ObjectGanttSchema.data` and tree
230
+ * passed `'undeclared'`. The tree's row declares `data`, so the ruling's
231
+ * 「协议没有的能力删」 clause did not fire for it: it keeps the object arm and
232
+ * loses only the bare-array spelling. Its `staticData` rung is declared by
233
+ * the same row and is unchanged.
234
+ *
235
+ * ## The DECLARATION follows the arm too (`ab856ed30`)
236
+ *
237
+ * The `data` member below is {@link AuthoredRecordSourceData}`<Arm>`, not a flat
238
+ * `ViewData`: the arm decides what a site may DECLARE exactly as it decides
239
+ * what rung 1 HONOURS. Read that type's docblock for why it is arm-indexed
240
+ * rather than a union of the arms, and for the reading that says nothing at
241
+ * runtime moved.
242
+ *
243
+ * ## No lenient rung was added (AGENTS.md #0.1)
244
+ *
245
+ * **Null tolerance** stays out: all five copies dereference `schema` unguarded
246
+ * and would throw on `null`; no site passes one, so no `?.` was added.
247
+ *
248
+ * `ObjectCalendar`'s copy guarded with `'data' in schema && schema.data`. That
249
+ * `in` test was a TYPESCRIPT narrowing device, not a behavioural one: when the
250
+ * property is absent the read yields `undefined`, which is falsy either way, so
251
+ * the guard could never change which rung is taken. The optional-property
252
+ * parameter below admits a schema that declares neither key directly, which is
253
+ * why the guard is gone rather than flattened away. The equivalence itself is
254
+ * pinned, not argued, by `CALENDAR_IN_GUARD` in
255
+ * `record-source-config.behaviourNeutrality-7632.test.ts`.
256
+ *
257
+ * ⛔ This passage used to explain that guard by the PROPS UNION the calendar
258
+ * plugin once carried — `ObjectGridSchema | CalendarSchema`, whose second
259
+ * member declared neither `data` nor `staticData`. objectui#8651 RETIRED that
260
+ * union (`ObjectCalendarComponentProps.schema` is the published
261
+ * `ObjectCalendarSchema` today), so the sentence explained the guard by a type
262
+ * no reader could look up. Rewritten rather than deleted: the guard's history
263
+ * is still why this parameter's members are optional.
264
+ *
265
+ * @param schema - The block's schema; only `data`, `staticData` and
266
+ * `objectName` are read. Its `data` member is typed by the arm — see
267
+ * {@link AuthoredRecordSourceData}.
268
+ * @param dataArm - The arm the CALLING BLOCK's published `data` row declares.
269
+ * Required: there is no repo-wide default, because the answer differs per
270
+ * block and a default is how the second de-facto contract got in.
271
+ * @returns The resolved data config, or `null` when nothing is bound.
272
+ *
273
+ * @example
274
+ * ```ts
275
+ * const dataConfig = useMemo(() => resolveRecordSourceConfig(schema, 'view-data'), [schema]);
276
+ * const objectName = resolveRecordSourceObjectName(schema, dataConfig);
277
+ * ```
278
+ */
279
+ export declare function resolveRecordSourceConfig<Arm extends RecordSourceDataArm>(schema: {
280
+ objectName?: string;
281
+ data?: AuthoredRecordSourceData<Arm>;
282
+ staticData?: any[];
283
+ }, dataArm: Arm): ViewData | null;
284
+ /**
285
+ * Which `data` arm the block registered under `type` declares.
286
+ *
287
+ * `'undeclared'` for every type not listed above — the same verdict the ladder
288
+ * gives a block no published face declares a `data` row for, and the same
289
+ * behaviour those types have today. That default is deliberately the
290
+ * NO-CHANGE direction: a caller acting on this reading (today:
291
+ * `SchemaRenderer`'s props spread) leaves an unlisted type exactly as it was,
292
+ * so a block that has never been measured is never silently re-decided by this
293
+ * table's mere existence.
294
+ *
295
+ * @param type - The node's registry key, either spelling.
296
+ * @returns The arm that type's published `data` row declares.
297
+ */
298
+ export declare function recordSourceDataArmForType(type: string | null | undefined): RecordSourceDataArm;