@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
@@ -21,6 +21,8 @@
21
21
  * The helpers are intentionally protocol-agnostic — any caller can
22
22
  * synthesize an `event` object and rely on the same defaults / templating.
23
23
  */
24
+ import { parseFilterAST } from '@objectstack/spec/data';
25
+ import { FilterOperatorError, mergeFilterNodes } from './filter-converter.js';
24
26
  const TEMPLATE = /\$\{event\.([a-zA-Z0-9_]+)\}/g;
25
27
  /**
26
28
  * Substitute `${event.x}` placeholders in a string with values from the
@@ -137,3 +139,94 @@ export function isDrillEnabled(config) {
137
139
  return false;
138
140
  return config.enabled !== false;
139
141
  }
142
+ /**
143
+ * Compose a widget's OWN filter with the filter a drill click derived, into the
144
+ * one filter the drilled list is scoped by.
145
+ *
146
+ * ## The rule: `widget.filter ∧ drill.filter`
147
+ *
148
+ * The two are independent filter SOURCES and a drill must satisfy BOTH. The
149
+ * widget's filter is what scopes the chart; the click context only says WHICH
150
+ * bucket of that scope the user asked to see. So a drill may narrow the widget's
151
+ * scope and may never widen it — which makes the composition a conjunction, not
152
+ * a merge and emphatically not a spread.
153
+ *
154
+ * ⛔ The rule is NOT invented here. It is the contract {@link mergeFilterNodes}
155
+ * already states — "combine filter sources under a single `and`, each as its OWN
156
+ * child" — the sink every other multi-source filter in this repo goes through
157
+ * (`ObjectView`, `RelatedList`, `LineItemsPanel`, `RecordPickerDialog`,
158
+ * `ElementDataSourceGate`, `buildEffectiveFilter`). This function only applies
159
+ * it at the drill seam and names it, so the answer is in one place rather than
160
+ * re-derived per widget.
161
+ *
162
+ * ## Why the two arms needed a sink at all (objectui#8944)
163
+ *
164
+ * `ObjectChartSchema.filter` admits BOTH a spec `FilterArray`
165
+ * (`[['stage','=','won']]`) and the ObjectQL `$filter` object
166
+ * (`{ close_date: { $gte } }`), because both are read — both are forwarded
167
+ * verbatim to `ds.aggregate` / `ds.find`. The drill seam used to compose them by
168
+ * SPREADING the widget's filter into an object literal, which is correct for the
169
+ * object arm and silent nonsense for the array arm: spreading `[['stage','=',
170
+ * 'won']]` yields the index key `{ '0': ['stage','=','won'] }`, so the widget's
171
+ * own conditions were replaced by a key the query layer ignores and the drilled
172
+ * list showed rows the chart itself was scoped to exclude. `toFilterNode` (via
173
+ * `mergeFilterNodes`) already lowers all three shapes in circulation, so routing
174
+ * the pair through it is what makes the array arm survive.
175
+ *
176
+ * ## Why the result is lowered back to the object dialect
177
+ *
178
+ * {@link mergeFilterNodes} answers in the ObjectQL AST
179
+ * (`['and', <widget>, <drill>]`). Both drill sinks take the `FilterCondition`
180
+ * OBJECT dialect instead — the drawer hands the value to `object-data-table`'s
181
+ * `filter` (which becomes `$filter`), and `DrillNavigationContext.openRecordList`
182
+ * declares `Record<string, unknown>` and serializes it to `filter[...]` URL
183
+ * params. `parseFilterAST` is the spec's single lowering sink between the two
184
+ * dialects, so it is what converts, rather than a second local translation.
185
+ *
186
+ * ⭐ A lone surviving source lowers back to exactly the flat object the spread
187
+ * produced (`{ stage: 'won' }`), so a chart with no filter of its own drills
188
+ * identically to before; only a genuinely composed pair gains the `$and`.
189
+ *
190
+ * Returns `undefined` when neither source carries anything, so callers can omit
191
+ * the key rather than send an empty filter.
192
+ *
193
+ * ⚠️ Refusals are the sink's, not this seam's: `mergeFilterNodes` rejects a
194
+ * comparand the wire would also reject (a bare array on `=`, a `RegExp`) with
195
+ * the `INVALID_FILTER` / 400 envelope. Such a filter already fails the widget's
196
+ * OWN query for the same reason, so the drill and the chart now agree instead of
197
+ * the drill quietly sending something the chart could not.
198
+ *
199
+ * ## One refusal type out of this seam (objectui#10789)
200
+ *
201
+ * The seam has TWO refusing steps, and they used to throw two different
202
+ * things. `mergeFilterNodes` throws a {@link FilterOperatorError}. The spec's
203
+ * `parseFilterAST`, which judges comparand shapes the converter passes
204
+ * through, throws a plain `Error` that carries the same envelope as own
205
+ * properties: `code: 'INVALID_FILTER'` and `status: 400`. An array-dialect
206
+ * widget filter holds no rule objects, so the converter never judges it, and
207
+ * `[['stage', 'in', 'won']]` (a scalar on a list operator) is refused only
208
+ * there. So is the object spelling `{ stage: { $in: 'won' } }`.
209
+ *
210
+ * Every caller catches the refusal as a `FilterOperatorError`: the chart,
211
+ * pivot and dataset drills report it and open nothing (never a filter-less
212
+ * drill). So the spec's refusal is re-raised here AS one, with its message
213
+ * verbatim and the same `INVALID_FILTER` / 400 envelope, rather than taught to
214
+ * each caller as a second shape. Only an error whose `code` is
215
+ * `INVALID_FILTER` is re-raised; anything else from `parseFilterAST` is a
216
+ * defect rather than a statement about the author's filter, and passes
217
+ * through untouched.
218
+ */
219
+ export function composeDrillFilter(widgetFilter, drillFilter) {
220
+ const merged = mergeFilterNodes(widgetFilter, drillFilter);
221
+ try {
222
+ // `FilterCondition` is the spec's object-dialect filter; the drill sinks
223
+ // type the same value as `Record<string, unknown>`, and this is the one
224
+ // seam where the two names meet.
225
+ return parseFilterAST(merged);
226
+ }
227
+ catch (error) {
228
+ if (error?.code !== 'INVALID_FILTER')
229
+ throw error;
230
+ throw new FilterOperatorError(error.message);
231
+ }
232
+ }
@@ -0,0 +1,96 @@
1
+ /**
2
+ * ObjectUI — the shared emptiness floor
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
+ * THE floor under "is this value empty" (objectui#8496 — director seat,
10
+ * decision batch #86, 2026-09-08, option B).
11
+ *
12
+ * Exactly four members, and it never grows past them:
13
+ *
14
+ * `null` · `undefined` · the empty string · the empty array
15
+ *
16
+ * ## What a floor IS, and what it is not
17
+ *
18
+ * It is the WEAKEST claim the surfaces below it can all make — not the answer
19
+ * any one of them gives. A caller does one of two things with it, and both are
20
+ * legitimate:
21
+ *
22
+ * - **extends** it — `isEmptyValue(v) || <its own clause>` — when its surface
23
+ * calls MORE things empty (a grid trims whitespace; a boolean column calls
24
+ * every non-boolean empty);
25
+ * - **declines a member** in the open — `isEmptyValue(v) && !Array.isArray(v)`
26
+ * — when its surface has MEASURED that member to be a value there (a `json`
27
+ * cell draws the two-character literal `[]` on purpose, objectui#8474).
28
+ *
29
+ * What is NOT legitimate is a sixth private re-spelling of these four members.
30
+ * That is the defect this function exists to close: `plugin-detail`'s
31
+ * `hasCellValue`, `RelatedList`'s `isValueEmpty`, `ObjectGallery`'s and
32
+ * `ObjectKanban`'s inline guards and the guard idioms across
33
+ * `@object-ui/fields`' cell renderers each grew their own copy, and
34
+ * objectui#8481 was the THIRD rediscovery of the same hole — objectui#8474 and
35
+ * objectui#8459 had each closed it at their own door first. A copy that agrees
36
+ * today stops agreeing; one definition cannot.
37
+ *
38
+ * ## ⛔ The floor never grows past those four members
39
+ *
40
+ * The ruling fixed the member list, and every candidate fifth member is a
41
+ * measured disagreement rather than an oversight:
42
+ *
43
+ * - **whitespace-only strings.** `' '` is EMPTY on `record:details` and in
44
+ * `RelatedList` (objectui#8350 measured the damage a blank cell does there)
45
+ * and a VALUE on the gallery, the kanban and the shared renderers. Both are
46
+ * right for their surface, so the trim is an EXTENSION, not a member.
47
+ * - **`{}`.** Measured as a VALUE and pinned (objectui#8474): a populated or
48
+ * empty object literal is handed to a type-aware renderer that draws it, and
49
+ * the shape that would sweep it in — `Object.keys(v).length === 0` — is also
50
+ * true of `new Date(0)`, of a populated `Map`, of a populated `Set` and of
51
+ * any class instance whose state sits behind getters.
52
+ * - **`0` / `false`.** Values everywhere. `BooleanCellRenderer` keeping
53
+ * `false` a value is the pinned case (objectui#8582).
54
+ *
55
+ * ## Why `@object-ui/core` and not `@object-ui/types`
56
+ *
57
+ * It is a runtime predicate, not a protocol type, so it belongs in the engine.
58
+ * The ruling made that conditional on a measurement — `@object-ui/fields` is
59
+ * the lowest consumer, and if it did not already depend on `core` the floor
60
+ * would have had to fall back to `types`. Measured on the implementing branch:
61
+ * `@object-ui/fields`' `package.json` lists `@object-ui/core` in
62
+ * `dependencies`, and its barrel already imports from it. No new dependency
63
+ * edge is created by this file, in either direction — `core` reaches no
64
+ * consumer, which is why exporting the helper from `@object-ui/fields` instead
65
+ * (option C) was refused: the gallery and the kanban would then import a
66
+ * `fields` helper to decide whether to call a `fields` renderer.
67
+ *
68
+ * ## "Empty" is two questions; this floor answers the half both share
69
+ *
70
+ * objectui#8496's later evidence (comment 5603203484) measured that the word
71
+ * has split in two on this codebase: SCALAR-MISSING (`EmptyValue`, the em-dash
72
+ * affordance whose accessible name is fixed) and COLLECTION-EMPTY
73
+ * (`EmptyDescription`, an author's own sentence). The floor serves both and
74
+ * does not have to choose: its four members ARE two scalar-missing members,
75
+ * one blank scalar and one empty collection, and no call site asks a boolean to
76
+ * tell those apart — each one knows statically which affordance it is drawing.
77
+ * ⛔ So this function is deliberately NOT the place to grow a second axis. Which
78
+ * COMPONENT states the emptiness is a different question, carried by
79
+ * objectui#8570 / objectui#8526 / objectui#8507.
80
+ *
81
+ * ## Readers
82
+ *
83
+ * `isOptionGroupGated` / `isValueStillOffered` here in `core` (this function's
84
+ * origin: it was written privately in `evaluator/optionRules.ts`, byte-for-byte
85
+ * these four members, before the ruling promoted it); `hasCellValue` and
86
+ * `RelatedList.isValueEmpty` in `@object-ui/plugin-detail`; `ObjectGallery`'s
87
+ * card-field row filter; `ObjectKanban`'s card-field loop; and the cell-renderer
88
+ * guards in `@object-ui/fields`.
89
+ *
90
+ * The extensions and the declensions are pinned — the assertion that each
91
+ * surface still answers DIFFERENTLY from the floor, not merely that the floor
92
+ * works — in `__tests__/emptiness-floor-8496.test.ts` here and in
93
+ * `emptinessFloorExtensions-8496.test.tsx` in `@object-ui/fields` and
94
+ * `@object-ui/plugin-detail`.
95
+ */
96
+ export declare function isEmptyValue(value: unknown): boolean;
@@ -0,0 +1,101 @@
1
+ /**
2
+ * ObjectUI — the shared emptiness floor
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
+ * THE floor under "is this value empty" (objectui#8496 — director seat,
10
+ * decision batch #86, 2026-09-08, option B).
11
+ *
12
+ * Exactly four members, and it never grows past them:
13
+ *
14
+ * `null` · `undefined` · the empty string · the empty array
15
+ *
16
+ * ## What a floor IS, and what it is not
17
+ *
18
+ * It is the WEAKEST claim the surfaces below it can all make — not the answer
19
+ * any one of them gives. A caller does one of two things with it, and both are
20
+ * legitimate:
21
+ *
22
+ * - **extends** it — `isEmptyValue(v) || <its own clause>` — when its surface
23
+ * calls MORE things empty (a grid trims whitespace; a boolean column calls
24
+ * every non-boolean empty);
25
+ * - **declines a member** in the open — `isEmptyValue(v) && !Array.isArray(v)`
26
+ * — when its surface has MEASURED that member to be a value there (a `json`
27
+ * cell draws the two-character literal `[]` on purpose, objectui#8474).
28
+ *
29
+ * What is NOT legitimate is a sixth private re-spelling of these four members.
30
+ * That is the defect this function exists to close: `plugin-detail`'s
31
+ * `hasCellValue`, `RelatedList`'s `isValueEmpty`, `ObjectGallery`'s and
32
+ * `ObjectKanban`'s inline guards and the guard idioms across
33
+ * `@object-ui/fields`' cell renderers each grew their own copy, and
34
+ * objectui#8481 was the THIRD rediscovery of the same hole — objectui#8474 and
35
+ * objectui#8459 had each closed it at their own door first. A copy that agrees
36
+ * today stops agreeing; one definition cannot.
37
+ *
38
+ * ## ⛔ The floor never grows past those four members
39
+ *
40
+ * The ruling fixed the member list, and every candidate fifth member is a
41
+ * measured disagreement rather than an oversight:
42
+ *
43
+ * - **whitespace-only strings.** `' '` is EMPTY on `record:details` and in
44
+ * `RelatedList` (objectui#8350 measured the damage a blank cell does there)
45
+ * and a VALUE on the gallery, the kanban and the shared renderers. Both are
46
+ * right for their surface, so the trim is an EXTENSION, not a member.
47
+ * - **`{}`.** Measured as a VALUE and pinned (objectui#8474): a populated or
48
+ * empty object literal is handed to a type-aware renderer that draws it, and
49
+ * the shape that would sweep it in — `Object.keys(v).length === 0` — is also
50
+ * true of `new Date(0)`, of a populated `Map`, of a populated `Set` and of
51
+ * any class instance whose state sits behind getters.
52
+ * - **`0` / `false`.** Values everywhere. `BooleanCellRenderer` keeping
53
+ * `false` a value is the pinned case (objectui#8582).
54
+ *
55
+ * ## Why `@object-ui/core` and not `@object-ui/types`
56
+ *
57
+ * It is a runtime predicate, not a protocol type, so it belongs in the engine.
58
+ * The ruling made that conditional on a measurement — `@object-ui/fields` is
59
+ * the lowest consumer, and if it did not already depend on `core` the floor
60
+ * would have had to fall back to `types`. Measured on the implementing branch:
61
+ * `@object-ui/fields`' `package.json` lists `@object-ui/core` in
62
+ * `dependencies`, and its barrel already imports from it. No new dependency
63
+ * edge is created by this file, in either direction — `core` reaches no
64
+ * consumer, which is why exporting the helper from `@object-ui/fields` instead
65
+ * (option C) was refused: the gallery and the kanban would then import a
66
+ * `fields` helper to decide whether to call a `fields` renderer.
67
+ *
68
+ * ## "Empty" is two questions; this floor answers the half both share
69
+ *
70
+ * objectui#8496's later evidence (comment 5603203484) measured that the word
71
+ * has split in two on this codebase: SCALAR-MISSING (`EmptyValue`, the em-dash
72
+ * affordance whose accessible name is fixed) and COLLECTION-EMPTY
73
+ * (`EmptyDescription`, an author's own sentence). The floor serves both and
74
+ * does not have to choose: its four members ARE two scalar-missing members,
75
+ * one blank scalar and one empty collection, and no call site asks a boolean to
76
+ * tell those apart — each one knows statically which affordance it is drawing.
77
+ * ⛔ So this function is deliberately NOT the place to grow a second axis. Which
78
+ * COMPONENT states the emptiness is a different question, carried by
79
+ * objectui#8570 / objectui#8526 / objectui#8507.
80
+ *
81
+ * ## Readers
82
+ *
83
+ * `isOptionGroupGated` / `isValueStillOffered` here in `core` (this function's
84
+ * origin: it was written privately in `evaluator/optionRules.ts`, byte-for-byte
85
+ * these four members, before the ruling promoted it); `hasCellValue` and
86
+ * `RelatedList.isValueEmpty` in `@object-ui/plugin-detail`; `ObjectGallery`'s
87
+ * card-field row filter; `ObjectKanban`'s card-field loop; and the cell-renderer
88
+ * guards in `@object-ui/fields`.
89
+ *
90
+ * The extensions and the declensions are pinned — the assertion that each
91
+ * surface still answers DIFFERENTLY from the floor, not merely that the floor
92
+ * works — in `__tests__/emptiness-floor-8496.test.ts` here and in
93
+ * `emptinessFloorExtensions-8496.test.tsx` in `@object-ui/fields` and
94
+ * `@object-ui/plugin-detail`.
95
+ */
96
+ export function isEmptyValue(value) {
97
+ return (value === undefined ||
98
+ value === null ||
99
+ value === '' ||
100
+ (Array.isArray(value) && value.length === 0));
101
+ }
@@ -7,21 +7,181 @@
7
7
  */
8
8
  /**
9
9
  * Relational ("reference-bearing") field types whose stored value is a foreign
10
- * key into another object — and which therefore benefit from `$expand` so a
11
- * list / grid / detail cell can render the related record's display name
12
- * instead of a bare id placeholder ("—").
13
- *
14
- * This mirrors the form layer's `DATA_SOURCE_FIELD_TYPES`
15
- * (`lookup` / `master_detail` / `tree`) and additionally covers `user` — a
16
- * lookup specialised to `sys_user`. The server resolves `user` through the
17
- * same expand path as `lookup` / `master_detail` (it carries the same
18
- * `reference` + id storage), so a `user` column that is NOT requested for
19
- * expansion comes back as a raw user id and renders as "—". Keeping the set
20
- * here as the single source of truth is what closes that gap.
10
+ * key into another object — and which therefore benefit from `$expand`, so a
11
+ * response carries the related record instead of only its foreign key.
12
+ *
13
+ * `user` is a lookup specialised to `sys_user`: it carries the same `reference`
14
+ * + id storage and the server resolves it through the same expand path as
15
+ * `lookup` / `master_detail`, so a `user` column that is NOT requested for
16
+ * expansion comes back as a raw user id (objectui#2032). What a CELL then
17
+ * prints for that id is deliberately not stated here — see the next section.
18
+ *
19
+ * ## No rendering claim is made here, on purpose — objectui#8693
20
+ *
21
+ * Both paragraphs above used to make one: the `user` sentence ended "…and
22
+ * renders as '—'", and the opening one called the unexpanded cell "a bare id
23
+ * placeholder ('—')". Measured on `da5e4f69e` by resolving each family through
24
+ * `getCellRenderer` and rendering three unexpanded primitives:
25
+ *
26
+ * | unexpanded value | `lookup` / `master_detail` / `tree` | `user` |
27
+ * |----------------------|-------------------------------------|--------------------|
28
+ * | `'u_1'` | `u_1`, plain truncated span | `u_1`, same span |
29
+ * | `'01HQZX9K2M4N6P8R'` | muted `—` | the raw ULID |
30
+ * | `'Ada Lovelace'` | `Ada Lovelace`, plain span | the raw string |
31
+ *
32
+ * Wrong twice over. `user` never drew the em-dash at all — `UserCellRenderer`
33
+ * has no resolver, no `useLookupName`, no `isLikelyOpaqueId`. And the three
34
+ * families that do draw it draw it ONLY for values `isLikelyOpaqueId` accepts;
35
+ * that qualifier is the entire content of the behaviour and the sentence never
36
+ * carried it, so a reader was told a human-readable id would degrade visibly
37
+ * when it printed as if it were a name — the defect objectui#8434 was for.
38
+ *
39
+ * It is not replaced by a corrected sentence, because a corrected sentence
40
+ * could not be kept true HERE. `@object-ui/core` does not depend on
41
+ * `@object-ui/fields` in `dependencies`, `peerDependencies` or
42
+ * `devDependencies` — the arrow runs the other way — so no test that can reach
43
+ * this file can render the cell it would describe, and nothing in this repo
44
+ * compares a docblock to rendered output. A replacement would be checked by
45
+ * whatever checked the last one: nobody. Its shelf life was measured too —
46
+ * while objectui#8693 was open, PR #8698 (objectui#8434) sat in the merge
47
+ * queue rewriting the `user` column of that very table: on its head
48
+ * `3692f7218` all three primitives render an unresolved-reference affordance
49
+ * that KEEPS the raw value. A corrected sentence would have been stale before
50
+ * it landed. The other three families are byte-identical on both trees.
51
+ *
52
+ * The storage claim is the half this set actually depends on, and that half IS
53
+ * checkable here (`__tests__/expand-fields.test.ts`, plus the identity pins
54
+ * below). How an unexpanded reference prints belongs next to the renderers in
55
+ * `@object-ui/fields`, where a test can fail.
21
56
  *
22
57
  * Note on `tree`: a self-referencing hierarchy field is a reference too, so it
23
58
  * belongs in this set; whether the backend materialises the expanded object
24
59
  * for it is a server concern — requesting it is harmless and forward-compatible.
60
+ *
61
+ * ## One family, many consumers — and NO reliable count of them
62
+ *
63
+ * This section used to open "One family, four consumers" and enumerate four.
64
+ * That count was hand-kept, and hand-kept counts of this table are exactly what
65
+ * keeps going wrong: it was already stale by two when objectui#5692 measured it
66
+ * (`paramToField` in `app-shell` had joined with objectui#5312, and
67
+ * `ListView`'s relational-sort rule reads the set directly), and objectui#5312's
68
+ * claim to have converted "the LAST private copy" was false by two MORE — see
69
+ * the falsification note at the end. Read the list below as the lineage of the
70
+ * conversions, not as a census; the mechanical fact is the identity pins.
71
+ *
72
+ * Re-measured for objectui#8693 on `da5e4f69e`: stale again, by FOUR. The
73
+ * commit that corrected the "fourth and last" claim (objectui#5874 / #6064)
74
+ * converged four more private copies of its own and never added them here —
75
+ * the last four bullets below. Re-derive before trusting it; the recipe is at
76
+ * the end of the list.
77
+ *
78
+ * This is the reference-bearing FAMILY, not the `$expand` builder's private
79
+ * list, and these concerns read it under several different words:
80
+ *
81
+ * - `$expand` construction — `buildExpandFields` below ("expandable");
82
+ * - predicate-record projection — `predicate-record.ts` ("relational");
83
+ * - the object form's data-source wiring — `needsDataSourceWiring` in
84
+ * `packages/components/src/renderers/form/form.tsx`, which decides which
85
+ * registered widget gets `dataSource` / `dependentValues` /
86
+ * `dependsOnLabels` threaded to it;
87
+ * - the grid's bulk-action dialog — `widgetNeedsDataSource` in
88
+ * `packages/plugin-grid/src/components/bulkParamToField.ts`, which decides
89
+ * which param widget is handed the grid's `DataSource` and which param field
90
+ * shape carries `reference` / `displayField`;
91
+ * - the action-param dialog — `paramToField` in
92
+ * `packages/app-shell/src/utils/paramToField.ts`, which decides which param
93
+ * carries a reference target (objectui#5312);
94
+ * - the list view's relational-sort rule — `ListView.tsx`, which will not offer
95
+ * a server-side sort on a field whose stored value is a foreign key;
96
+ * - the dashboard table's `$expand` whitelist — `computeLookupExpand` in
97
+ * `packages/plugin-dashboard/src/ObjectDataTable.tsx` (objectui#5692);
98
+ * - the dashboard's relation/link test — `isLookupType` in
99
+ * `packages/plugin-dashboard/src/recordFields.tsx` (objectui#5692);
100
+ * - the action-param RESOLVER (one step before `paramToField`) —
101
+ * `packages/app-shell/src/utils/resolveActionParams.ts` (objectui#5874);
102
+ * - the detail header's relation test — `HeaderHighlight.tsx` in
103
+ * `packages/plugin-detail` (objectui#5874);
104
+ * - the detail drawer's relation test — `RecordDetailDrawer.tsx` in
105
+ * `packages/plugin-detail`, reading through `isExpandableFieldType`
106
+ * (objectui#5874);
107
+ * - the kanban board's `$expand` — `ObjectKanban.tsx` in
108
+ * `packages/plugin-kanban`, reading through `buildExpandFields`
109
+ * (objectui#5874).
110
+ *
111
+ * ⚠️ Re-derive rather than trust. One command, from the repo root, over the
112
+ * whole tree with NO pathspec. (Narrowing it to `packages/` + a `*` segment +
113
+ * `/src` matches ZERO files, and would drop `apps/` besides. The two-character
114
+ * sequence is spelled out in words here because writing it literally would end
115
+ * this comment.)
116
+ *
117
+ * git grep -lE 'EXPANDABLE_FIELD_TYPES|isExpandableFieldType|buildExpandFields'
118
+ *
119
+ * 96 files on `da5e4f69e`. Drop tests, changesets and changelogs and the
120
+ * production readers fall into three radii, only the first of which the list
121
+ * above is about: this file plus 8 more naming the set directly, plus 4 more
122
+ * calling `isExpandableFieldType`, plus 13 more calling `buildExpandFields`.
123
+ * The sweep was controlled by planting a reader under `apps/console/src/` and
124
+ * confirming the same command found it — `apps/` holds no reader today, and a
125
+ * zero-hit region is otherwise indistinguishable from a blind spot.
126
+ *
127
+ * The third one used to be a second hand-maintained copy, and this comment used
128
+ * to claim the set "mirrors the form layer's `DATA_SOURCE_FIELD_TYPES`
129
+ * (`lookup` / `master_detail` / `tree`)". That was true for 15 days. The form's
130
+ * copy then gained `capability-multiselect` (objectui#2403) and, the same day,
131
+ * `object-ref` / `filter-condition` / `recipient-picker` (objectui#2421), after
132
+ * which the two sets were not in a subset relation in EITHER direction — `user`
133
+ * only here, three picker names only there — and no gate could say so
134
+ * (objectui#4790). The form now DERIVES from this set instead of restating it:
135
+ *
136
+ * form's data-source rule == EXPANDABLE_FIELD_TYPES
137
+ * + three widget-hint-only picker names
138
+ *
139
+ * The extension is one-directional and cannot fold back into this set:
140
+ * `object-ref` / `filter-condition` / `recipient-picker` are widget HINTS, never
141
+ * declarable field types (`widgetHintOnly: true` in
142
+ * `app-shell/src/utils/paramValueShape.ts`), so no object schema can produce a
143
+ * field whose `type` is one of them.
144
+ *
145
+ * The fourth was a hand-maintained copy too, one member set removed from BOTH
146
+ * of the above (`lookup` / `master_detail` / `user` — no `tree`, no pickers, and
147
+ * for a while a fifth spelling, `owner`, that objectui#4814 retired). It now
148
+ * derives from this set with NO extension, because the bulk dialog's params
149
+ * cannot produce the three widget-hint names (objectui#4815):
150
+ *
151
+ * bulk dialog's data-source rule == EXPANDABLE_FIELD_TYPES
152
+ *
153
+ * Neither consumer copies the set — both call `.has()` on THIS object, and both
154
+ * carry an identity pin (a spy on this `has`) so a member-identical private copy
155
+ * fails rather than quietly re-forking the table.
156
+ *
157
+ * ## The "LAST private copy" claim was false — objectui#5692
158
+ *
159
+ * objectui#5312 converted `paramToField` (`app-shell`) and recorded it as the
160
+ * fourth and LAST private copy of this rule. `packages/plugin-dashboard` held
161
+ * two more the whole time — `LOOKUP_TYPES` in `recordFields.tsx` and an inline
162
+ * disjunction inside `computeLookupExpand` in `ObjectDataTable.tsx` — which
163
+ * predate that sweep and were outside its file surface, so nothing contradicted
164
+ * the claim. Both now derive from this set with NO extension and carry the same
165
+ * identity pin.
166
+ *
167
+ * Those two were NOT member-identical to this set, in either direction: they
168
+ * lacked `tree` and carried a fifth spelling, `reference`. Converging them was
169
+ * therefore a behaviour change in two directions, and the direction that could
170
+ * have widened THIS set was settled by measurement rather than by preference:
171
+ * `reference` is absent from `@objectstack/spec`'s closed `FieldType`
172
+ * vocabulary and is refused by `FieldSchema.safeParse` — measured with `lookup`
173
+ * / `master_detail` / `user` / `tree` as live controls and the retired `owner`
174
+ * plus a nonsense spelling as dead ones — so no spec-compliant object schema can
175
+ * declare a field whose stored type is `reference`, and the dashboard's copies
176
+ * were carrying a dead spelling, not a member this set was missing. This set is
177
+ * unchanged by objectui#5692; the two dashboard faces simply stopped answering
178
+ * for a type no producer can emit.
179
+ *
180
+ * Stated so it reads as a decision rather than a surprise: **adding a member
181
+ * here also grants that type the form's data-source wiring.** That is the
182
+ * intended coupling — a type whose stored value is a foreign key needs a
183
+ * `DataSource` to resolve that key on the read path (expand) and on the edit
184
+ * path (the picker) alike.
25
185
  */
26
186
  export declare const EXPANDABLE_FIELD_TYPES: ReadonlySet<string>;
27
187
  /**