@object-ui/core 17.5.0 → 17.7.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 (157) hide show
  1. package/CHANGELOG.md +7404 -0
  2. package/README.md +127 -26
  3. package/dist/actions/ActionEngine.d.ts +5 -3
  4. package/dist/actions/ActionEngine.js +14 -10
  5. package/dist/actions/ActionRunner.d.ts +363 -30
  6. package/dist/actions/ActionRunner.js +544 -58
  7. package/dist/actions/TransactionManager.d.ts +7 -2
  8. package/dist/actions/actionKeys.d.ts +75 -13
  9. package/dist/actions/actionKeys.js +131 -16
  10. package/dist/actions/actionResponse.js +3 -3
  11. package/dist/actions/index.d.ts +2 -0
  12. package/dist/actions/index.js +2 -0
  13. package/dist/actions/recordDelete.d.ts +118 -0
  14. package/dist/actions/recordDelete.js +112 -0
  15. package/dist/actions/recordIdParam.d.ts +77 -0
  16. package/dist/actions/recordIdParam.js +37 -0
  17. package/dist/actions/serverActionHandler.d.ts +1 -1
  18. package/dist/actions/serverActionHandler.js +1 -1
  19. package/dist/adapters/ValueDataSource.d.ts +6 -1
  20. package/dist/adapters/ValueDataSource.js +1090 -98
  21. package/dist/adapters/batchTransaction.d.ts +1 -1
  22. package/dist/adapters/batchTransaction.js +1 -1
  23. package/dist/builder/schema-builder.d.ts +38 -81
  24. package/dist/builder/schema-builder.js +33 -152
  25. package/dist/data-scope/ViewDataProvider.d.ts +1 -1
  26. package/dist/data-scope/ViewDataProvider.js +21 -1
  27. package/dist/data-scope/element-data-source.d.ts +261 -7
  28. package/dist/data-scope/element-data-source.js +342 -52
  29. package/dist/data-scope/index.d.ts +9 -4
  30. package/dist/data-scope/index.js +9 -4
  31. package/dist/evaluator/ExpressionEvaluator.d.ts +128 -1
  32. package/dist/evaluator/ExpressionEvaluator.js +226 -13
  33. package/dist/evaluator/FormulaFunctions.d.ts +0 -10
  34. package/dist/evaluator/FormulaFunctions.js +166 -27
  35. package/dist/evaluator/SafeExpressionParser.js +0 -4
  36. package/dist/evaluator/declaredPredicate.d.ts +93 -9
  37. package/dist/evaluator/declaredPredicate.js +118 -17
  38. package/dist/evaluator/fieldRules.d.ts +107 -4
  39. package/dist/evaluator/fieldRules.js +231 -35
  40. package/dist/evaluator/index.d.ts +2 -0
  41. package/dist/evaluator/index.js +2 -0
  42. package/dist/evaluator/listConditional.d.ts +125 -9
  43. package/dist/evaluator/listConditional.js +167 -26
  44. package/dist/evaluator/optionRules.d.ts +126 -0
  45. package/dist/evaluator/optionRules.js +141 -4
  46. package/dist/evaluator/predicateInput.d.ts +15 -4
  47. package/dist/evaluator/predicateInput.js +35 -2
  48. package/dist/evaluator/rowPredicateCanon.d.ts +56 -0
  49. package/dist/evaluator/rowPredicateCanon.js +158 -0
  50. package/dist/evaluator/subjectPermissions.d.ts +76 -0
  51. package/dist/evaluator/subjectPermissions.js +44 -0
  52. package/dist/evaluator/unevaluablePredicate.d.ts +57 -0
  53. package/dist/evaluator/unevaluablePredicate.js +70 -0
  54. package/dist/index.d.ts +18 -0
  55. package/dist/index.js +97 -2
  56. package/dist/protocols/index.d.ts +0 -1
  57. package/dist/protocols/index.js +9 -1
  58. package/dist/registry/Registry.d.ts +397 -48
  59. package/dist/registry/Registry.js +263 -11
  60. package/dist/registry/WidgetRegistry.js +7 -3
  61. package/dist/registry/html-tier-intrinsics.d.ts +90 -0
  62. package/dist/registry/html-tier-intrinsics.js +110 -0
  63. package/dist/registry/public-blocks.d.ts +8 -0
  64. package/dist/registry/public-blocks.js +57 -0
  65. package/dist/theme/ThemeEngine.js +1 -1
  66. package/dist/theme/index.d.ts +1 -1
  67. package/dist/theme/index.js +1 -1
  68. package/dist/types/index.d.ts +92 -13
  69. package/dist/utils/chart-category-key.d.ts +82 -0
  70. package/dist/utils/chart-category-key.js +84 -0
  71. package/dist/utils/chart-measure-key.d.ts +64 -0
  72. package/dist/utils/chart-measure-key.js +65 -0
  73. package/dist/utils/chart-presentation.d.ts +188 -0
  74. package/dist/utils/chart-presentation.js +345 -0
  75. package/dist/utils/chart-series.d.ts +271 -31
  76. package/dist/utils/chart-series.js +478 -55
  77. package/dist/utils/column-identity.d.ts +43 -0
  78. package/dist/utils/column-identity.js +47 -1
  79. package/dist/utils/column-sortability.d.ts +157 -0
  80. package/dist/utils/column-sortability.js +217 -0
  81. package/dist/utils/dashboard-filters.d.ts +38 -6
  82. package/dist/utils/dashboard-filters.js +107 -61
  83. package/dist/utils/dataset-format.d.ts +211 -30
  84. package/dist/utils/dataset-format.js +407 -52
  85. package/dist/utils/date-display.d.ts +354 -0
  86. package/dist/utils/date-display.js +463 -0
  87. package/dist/utils/date-macros.js +7 -0
  88. package/dist/utils/debug.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 +171 -11
  94. package/dist/utils/expand-fields.js +171 -11
  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 +152 -4
  98. package/dist/utils/filter-converter.js +1092 -22
  99. package/dist/utils/filter-tokens.d.ts +49 -5
  100. package/dist/utils/filter-tokens.js +105 -34
  101. package/dist/utils/freeze-schema.d.ts +38 -1
  102. package/dist/utils/freeze-schema.js +10 -0
  103. package/dist/utils/grouping-fields.d.ts +61 -0
  104. package/dist/utils/grouping-fields.js +84 -0
  105. package/dist/utils/humanize-label.d.ts +60 -0
  106. package/dist/utils/humanize-label.js +62 -0
  107. package/dist/utils/managedBy.d.ts +30 -5
  108. package/dist/utils/managedBy.js +5 -5
  109. package/dist/utils/native-date-value.d.ts +48 -0
  110. package/dist/utils/native-date-value.js +167 -0
  111. package/dist/utils/non-grid-row-ceiling.d.ts +143 -0
  112. package/dist/utils/non-grid-row-ceiling.js +141 -0
  113. package/dist/utils/normalize-list-view.d.ts +131 -1
  114. package/dist/utils/normalize-list-view.js +248 -6
  115. package/dist/utils/number-display.d.ts +70 -17
  116. package/dist/utils/number-display.js +34 -12
  117. package/dist/utils/object-aggregate-query.d.ts +113 -0
  118. package/dist/utils/object-aggregate-query.js +50 -0
  119. package/dist/utils/parent-scope.d.ts +123 -0
  120. package/dist/utils/parent-scope.js +140 -0
  121. package/dist/utils/predicate-fields.d.ts +62 -4
  122. package/dist/utils/predicate-fields.js +169 -4
  123. package/dist/utils/record-color.d.ts +75 -0
  124. package/dist/utils/record-color.js +65 -0
  125. package/dist/utils/record-source.d.ts +298 -0
  126. package/dist/utils/record-source.js +331 -0
  127. package/dist/utils/record-title.d.ts +148 -8
  128. package/dist/utils/record-title.js +220 -26
  129. package/dist/utils/reference-keys.d.ts +21 -22
  130. package/dist/utils/reference-keys.js +532 -29
  131. package/dist/utils/retired-field-types.d.ts +90 -0
  132. package/dist/utils/retired-field-types.js +124 -0
  133. package/dist/utils/sort-query.d.ts +128 -26
  134. package/dist/utils/sort-query.js +240 -13
  135. package/dist/utils/text-comparand.d.ts +92 -0
  136. package/dist/utils/text-comparand.js +99 -0
  137. package/dist/utils/unmaterialized-fields.d.ts +52 -0
  138. package/dist/utils/unmaterialized-fields.js +57 -0
  139. package/dist/utils/without-denied-fields.d.ts +49 -0
  140. package/dist/utils/without-denied-fields.js +52 -0
  141. package/dist/validation/index.d.ts +9 -4
  142. package/dist/validation/index.js +9 -4
  143. package/dist/validation/schema-validator.d.ts +1 -1
  144. package/dist/validation/schema-validator.js +305 -82
  145. package/dist/validation/server-owned-value.d.ts +62 -0
  146. package/dist/validation/server-owned-value.js +99 -0
  147. package/dist/validation/validators/object-validation-engine.d.ts +1 -1
  148. package/dist/validation/validators/object-validation-engine.js +14 -5
  149. package/package.json +8 -9
  150. package/dist/__benchmarks__/core.bench.d.ts +0 -8
  151. package/dist/__benchmarks__/core.bench.js +0 -53
  152. package/dist/data-scope/DataScopeManager.d.ts +0 -127
  153. package/dist/data-scope/DataScopeManager.js +0 -229
  154. package/dist/protocols/ResponsiveProtocol.d.ts +0 -73
  155. package/dist/protocols/ResponsiveProtocol.js +0 -158
  156. package/dist/validation/validation-engine.d.ts +0 -88
  157. package/dist/validation/validation-engine.js +0 -434
@@ -30,6 +30,15 @@
30
30
  * more column and no extra round trip. (A relation that IS displayed still gets
31
31
  * expanded for its label, and `toPredicateRecord` collapses it back for the
32
32
  * predicate.)
33
+ *
34
+ * Since objectstack#8018 the same reader also carries the non-predicate row keys
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
39
+ * "predicate" spelling because the mechanism is identical (harvest a name, gate
40
+ * it against the declared fields, add it to `$select`); see
41
+ * {@link listViewPredicates} for what is in the set and why.
33
42
  */
34
43
  /**
35
44
  * The columns the framework provisions on EVERY business object
@@ -74,6 +83,81 @@ export function isProjectableField(name, declaredFields) {
74
83
  * record-header path), so this covers the documented spelling.
75
84
  */
76
85
  const RECORD_REF = /\b(?:record|data)\.([A-Za-z_][A-Za-z0-9_]*)/g;
86
+ /**
87
+ * A whole bare identifier — what a field name is, and the only shape that can be
88
+ * spelled as `record.<name>` without the harvester's regex reading a PREFIX of it
89
+ * as the name (`record.not a field` would otherwise yield `not`). Used to gate
90
+ * the non-predicate names folded into {@link listViewPredicates}, so a malformed
91
+ * declaration contributes nothing instead of contributing a plausible wrong name.
92
+ */
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/app-shell`) 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`);
129
+ * - each param's COLLECTED key, `name ?? field`: the key the param dialog
130
+ * stores its answer under (`paramName` in `app-shell`'s
131
+ * `resolveActionParams`). This is the opposite precedence to
132
+ * {@link defaultFromRowKey} on purpose: that one asks which row key a value
133
+ * is READ from, this one asks which key a value is WRITTEN under, and the
134
+ * capture looks the written key up on the row;
135
+ * - `bodyExtra`: the static body the console's generic `api` handler merges
136
+ * over the params before it writes (`useConsoleActionRuntime`).
137
+ *
138
+ * `recordId` is left out: both writers read it as the record's address and
139
+ * strip it from the written set, so no capture ever looks it up.
140
+ *
141
+ * A key the harvest cannot make the row carry (undeclared, or a field the
142
+ * principal may not read) is not rescued here. The capture refuses to invent a
143
+ * prior value for it instead, and offers no Undo.
144
+ */
145
+ function undoableWrittenKeys(def) {
146
+ const keys = [];
147
+ for (const bag of [def.patch, def.bodyExtra]) {
148
+ if (bag && typeof bag === 'object' && !Array.isArray(bag))
149
+ keys.push(...Object.keys(bag));
150
+ }
151
+ if (Array.isArray(def.params)) {
152
+ for (const param of def.params) {
153
+ if (!param || typeof param !== 'object')
154
+ continue;
155
+ const p = param;
156
+ keys.push(p.name ?? p.field);
157
+ }
158
+ }
159
+ return keys.filter((key) => key !== 'recordId');
160
+ }
77
161
  /** Pull the CEL source out of any of the shapes a predicate is authored in. */
78
162
  function predicateSource(pred) {
79
163
  if (typeof pred === 'string')
@@ -117,13 +201,62 @@ export function collectPredicateFieldRefs(predicates) {
117
201
  return out;
118
202
  }
119
203
  /**
120
- * Every predicate a list-ish view carries, flattened for
204
+ * Every record-field reference a list-ish view carries, flattened for
121
205
  * {@link collectPredicateFieldRefs}.
122
206
  *
123
207
  * One reader so the projection cannot fall behind the surfaces: each entry here
124
- * is a place a predicate is actually evaluated against a row — conditional
125
- * formatting, the row kebab (custom defs AND the built-in Edit/Delete
126
- * overrides), the selection bar, and the object's declared actions.
208
+ * is a place a row field is actually READ — conditional formatting, the row
209
+ * kebab (custom defs AND the built-in Edit/Delete overrides), the selection bar,
210
+ * and the object's declared actions.
211
+ *
212
+ * Most entries are predicates. Five are not, and all are spelled as a synthetic
213
+ * `record.<name>` so the one harvester handles them: conditional formatting's
214
+ * native `{ field, operator, value }` shape, an action's `recordIdField`
215
+ * (objectstack#8018), the field each `defaultFromRow` param seeds from, each
216
+ * `{field}` token of the action's `target` (both objectui#10277), and the fields
217
+ * an `undoable` action writes (objectui#10404). The
218
+ * `recordIdField` case is the same *class* of bug the predicate harvest exists
219
+ * to close, one surface over — the action runtime reads
220
+ * `rowRecord[action.recordIdField]` to seed `recordIdParam`, so a key outside the
221
+ * listView columns arrived absent and the injection was silently skipped, which
222
+ * turns a record-scoped mutation into one that names no record while still
223
+ * reporting success. The default (`id`) is already projected unconditionally, so
224
+ * only an explicit declaration adds anything here.
225
+ *
226
+ * The last two are the same class again, and they were silent in the same way.
227
+ * `resolveActionParam` seeds a `defaultFromRow` param only when the row HAS the
228
+ * key (an own-property check), so an absent key opened the param dialog with the
229
+ * field blank; and the console's `api` handler fills a `{field}` token from the
230
+ * row, so an absent key put an empty path segment into the URL. The key a param
231
+ * seeds from is `field ?? name` — the runtime's precedence, not the param's
232
+ * payload name — and the token grammar is the handler's own. Neither is
233
+ * narrowed by the action's `type`, for the asymmetry stated below: an extra
234
+ * column costs bytes.
235
+ *
236
+ * The written fields are the same class once more, and the worst of them,
237
+ * because what went wrong was a write. The Undo capture of an `undoable` update
238
+ * reads each written field's prior value off the row; on a projected row a
239
+ * written field no column shows was absent, the capture recorded `null`, and
240
+ * Undo then wrote `null` over the stored value it existed to restore. The
241
+ * written keys are `patch`, each param's collected key and `bodyExtra` (see
242
+ * `undoableWrittenKeys`). This one IS narrowed, to `undoable`, and not for
243
+ * cost: the capture is the only reader of a written field's prior value, and it
244
+ * runs only under `undoable`, so without it the row reads none of these keys.
245
+ *
246
+ * A `recordIdField`, param key or written key that is not a bare identifier is
247
+ * dropped here
248
+ * (a `target` token is one by construction of its grammar), and a name the
249
+ * object does not declare is dropped by the caller's `isProjectableField`
250
+ * guard. Both drops are the safe direction: such a name is not a column
251
+ * anywhere, and an unknown key in `$select` is not ignored by every backend.
252
+ * The identifier gate is not decoration — without it the harvester's regex would
253
+ * read a PREFIX of a malformed name (`record.not a field` → `not`) and
254
+ * contribute a plausible wrong field instead of nothing.
255
+ *
256
+ * ⛔ Harvesting is not a read grant. Every name here is a CANDIDATE: each
257
+ * consumer still passes it through its own field-level read filter before it
258
+ * reaches `$select`, so a field the principal may not read is not requested
259
+ * because an action names it.
127
260
  *
128
261
  * `objectActions` is deliberately the object's WHOLE action set rather than
129
262
  * only the ones this view promotes. Narrowing it would mean re-running the
@@ -151,6 +284,38 @@ export function listViewPredicates(view) {
151
284
  continue;
152
285
  const d = def;
153
286
  preds.push(d.visible, d.disabled);
287
+ // The row key this action identifies its record by — a field the runtime
288
+ // READS off the row, so the projection owes it exactly as it owes a
289
+ // predicate's operands. Spelled as a `record.` reference so the one
290
+ // harvester handles it (see the doc above).
291
+ if (typeof d.recordIdField === 'string' && BARE_IDENTIFIER.test(d.recordIdField)) {
292
+ preds.push(`record.${d.recordIdField}`);
293
+ }
294
+ // A `defaultFromRow` param's seed field and the `{field}` tokens of the
295
+ // action's `target` (objectui#10277) — two more row keys the runtime
296
+ // READS, by the same rule and the same identifier gate.
297
+ if (Array.isArray(d.params)) {
298
+ for (const param of d.params) {
299
+ const key = defaultFromRowKey(param);
300
+ if (typeof key === 'string' && BARE_IDENTIFIER.test(key))
301
+ preds.push(`record.${key}`);
302
+ }
303
+ }
304
+ if (typeof d.target === 'string') {
305
+ TARGET_ROW_TOKEN.lastIndex = 0;
306
+ for (let m = TARGET_ROW_TOKEN.exec(d.target); m; m = TARGET_ROW_TOKEN.exec(d.target)) {
307
+ preds.push(`record.${m[1]}`);
308
+ }
309
+ }
310
+ // The fields an `undoable` action writes (objectui#10404): its Undo
311
+ // capture reads each one's prior value off the row. Same rule, same
312
+ // identifier gate.
313
+ if (d.undoable) {
314
+ for (const key of undoableWrittenKeys(d)) {
315
+ if (typeof key === 'string' && BARE_IDENTIFIER.test(key))
316
+ preds.push(`record.${key}`);
317
+ }
318
+ }
154
319
  }
155
320
  }
156
321
  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`/`api`-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;