@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
@@ -0,0 +1,217 @@
1
+ /**
2
+ * ObjectUI — served per-column sortability projection (consumer side)
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 consumer half of objectstack#10235 (maintainer ruling A, 2026-08-23):
10
+ * the platform serves an explicit per-column sortability signal, and a grid
11
+ * reads THAT to decide whether a header offers a sort click — it never
12
+ * re-derives "virtual ⇒ unsortable" from the field's `type`.
13
+ *
14
+ * ## What the platform serves
15
+ *
16
+ * `GET /api/v1/meta/object/:name` answers `GetMetaItemResponseSchema`, whose
17
+ * ENVELOPE (never the document under `item` — `FieldSchema` is a
18
+ * `strictObject`, so the key must stay un-authorable) carries an optional
19
+ * `sortability` key when the served type is `object`, on every serving branch
20
+ * including the cached one:
21
+ *
22
+ * ```jsonc
23
+ * { "type": "object", "name": "crm_opportunity", "item": { … },
24
+ * "sortability": { "fields": {
25
+ * "amount": { "sortable": true },
26
+ * "expected_revenue": { "sortable": false, "reason": "virtual-type" },
27
+ * "remote_anchor": { "sortable": true, "caveat": "unprovisioned-anchor" }
28
+ * } } }
29
+ * ```
30
+ *
31
+ * Shape verified against the MERGED upstream change, not against a description
32
+ * of it: `@objectstack/spec`'s `ObjectSortabilitySchema` /
33
+ * `FieldSortabilitySchema` and `GetMetaItemResponseSchema.sortability`.
34
+ *
35
+ * ## The contract, and the asymmetry that makes it easy to get backwards
36
+ *
37
+ * Offer a sort affordance on a column **iff the projection has an entry for
38
+ * the column's field name AND that entry says `sortable: true`**.
39
+ *
40
+ * ABSENCE OF AN ENTRY MEANS "no platform sort behind this name" — never
41
+ * "assume sortable". The projection's domain is exactly the served field map
42
+ * plus the always-provisioned `id`, so an unknown name, a dotted path
43
+ * (`account.name`) and an unprovisioned audit column all arrive as absence,
44
+ * and all three are refused by the runtime doors. A `!== false` test — the
45
+ * spelling every other optional flag in this repo uses — answers `true` for
46
+ * absence and so gets exactly that family backwards. {@link isPlatformSortableField}
47
+ * is the only spelling of this test; do not inline a comparison at a call site.
48
+ *
49
+ * ## Present-vs-absent PROJECTION is a different question
50
+ *
51
+ * `readObjectSortability` returns `undefined` when the served metadata carried
52
+ * no `sortability` key at all — a backend older than the upstream change, an
53
+ * inline/mock data source, a fixture. That is NOT "every column is unsortable":
54
+ * it is "no signal was served", and the caller decides what it did before the
55
+ * signal existed. The two are deliberately different types (`undefined` vs an
56
+ * empty `fields` map) so a caller cannot conflate them by accident.
57
+ *
58
+ * ## `caveat: 'unprovisioned-anchor'` is NOT a refusal
59
+ *
60
+ * Platform-injected anchors on ADR-0015 `external` objects come back
61
+ * `sortable: true, caveat: 'unprovisioned-anchor'`: the runtime ACCEPTS the
62
+ * sort, but silently drops it (`asc` === `desc` under a `200`) when the remote
63
+ * table carries no such column — a degradation the platform cannot prove
64
+ * either way at serve time. This module treats the caveat as advisory and the
65
+ * `sortable` verdict as the enforcement fact, so a caveated column keeps its
66
+ * click. Refusing what the platform does not refuse would recreate
67
+ * declared-≠-enforced drift in mirror image; whether to badge or withhold on
68
+ * the caveat is an open affordance question, not something to settle here.
69
+ */
70
+ /**
71
+ * The served shape is the SPEC's, re-exported rather than restated.
72
+ *
73
+ * `FIELD_UNSORTABLE_VIRTUAL_TYPE` / `FIELD_SORTABLE_UNPROVISIONED_ANCHOR` are
74
+ * the two string literals the projection can carry, and `FieldSortability` /
75
+ * `ObjectSortability` are the wire types themselves (`z.input` of the
76
+ * schemas the REST layer answers with). Re-exporting is the point of a
77
+ * contract-first consumer: a local declaration under a spec export's name is
78
+ * read by the next agent as the spec's own definition, and then drifts from
79
+ * it — which is what `check:spec-symbols` guards, and what it caught here.
80
+ */
81
+ export { FIELD_UNSORTABLE_VIRTUAL_TYPE, FIELD_SORTABLE_UNPROVISIONED_ANCHOR, } from '@objectstack/spec/api';
82
+ /**
83
+ * Where the projection rides on the client-side object schema.
84
+ *
85
+ * A GLOBAL-REGISTRY SYMBOL, deliberately — the same carrier shape
86
+ * `INFLIGHT_GET_REGISTRY_KEY` uses in `@object-ui/types`. Three properties
87
+ * this signal needs and a string key would not give it:
88
+ *
89
+ * - `JSON.stringify` drops symbol-keyed properties, so a schema object that
90
+ * is ever round-tripped back at a metadata WRITE endpoint cannot carry the
91
+ * projection into a document the server parses `strict` and rejects by
92
+ * name. The upstream change kept the key off `item` for exactly that
93
+ * reason; stamping a string key here would undo it one repo away.
94
+ * - `Object.keys` / spread / `for…in` do not see it, so every existing reader
95
+ * that enumerates the schema (column generators, diffing, form builders)
96
+ * is unchanged by its presence.
97
+ * - `Symbol.for` is the cross-realm registry, so two copies of this module
98
+ * (a workspace build and a published one in the same graph) agree on the
99
+ * key rather than silently reading past each other.
100
+ */
101
+ export const OBJECT_SORTABILITY_KEY = Symbol.for('objectui.objectSortability');
102
+ /** Narrow one served entry, tolerating an unparsed / hand-built record. */
103
+ function asFieldSortability(value) {
104
+ if (!value || typeof value !== 'object' || Array.isArray(value))
105
+ return undefined;
106
+ const sortable = value.sortable;
107
+ if (typeof sortable !== 'boolean')
108
+ return undefined;
109
+ const reason = value.reason;
110
+ const caveat = value.caveat;
111
+ // `reason` / `caveat` are typed as closed literal unions by the spec, and
112
+ // this is unparsed input off the wire. Preserve whatever string arrived
113
+ // rather than dropping an annotation a NEWER platform added — the verdict
114
+ // this module acts on is `sortable`, and an unrecognised advisory string is
115
+ // strictly better carried than silently erased. The assertion is the
116
+ // boundary cast that admission implies, and it is confined to this function.
117
+ return {
118
+ sortable,
119
+ ...(typeof reason === 'string' ? { reason } : {}),
120
+ ...(typeof caveat === 'string' ? { caveat } : {}),
121
+ };
122
+ }
123
+ /**
124
+ * Normalize a served `sortability` envelope value into the projection, or
125
+ * `undefined` when the envelope carried nothing usable.
126
+ *
127
+ * Tolerant on purpose (the same contract the upstream resolver carries): the
128
+ * adapter hands this whatever came back over the wire, unparsed.
129
+ */
130
+ export function normalizeObjectSortability(served) {
131
+ if (!served || typeof served !== 'object' || Array.isArray(served))
132
+ return undefined;
133
+ const rawFields = served.fields;
134
+ if (!rawFields || typeof rawFields !== 'object' || Array.isArray(rawFields))
135
+ return undefined;
136
+ const fields = {};
137
+ for (const [name, entry] of Object.entries(rawFields)) {
138
+ const verdict = asFieldSortability(entry);
139
+ if (verdict)
140
+ fields[name] = verdict;
141
+ }
142
+ return { fields };
143
+ }
144
+ /**
145
+ * Stamp the served projection onto the client-side object schema, under
146
+ * {@link OBJECT_SORTABILITY_KEY}.
147
+ *
148
+ * Non-enumerable and non-writable-by-accident: the property is defined rather
149
+ * than assigned so it never shows up in a spread of the schema and cannot be
150
+ * clobbered by one. Returns the same object it was handed (the adapter caches
151
+ * that instance), and is a no-op when there is nothing usable to attach — so
152
+ * an older backend leaves the schema exactly as it found it rather than
153
+ * stamping an empty projection that would read as "nothing is sortable".
154
+ */
155
+ export function attachObjectSortability(schema, served) {
156
+ const projection = normalizeObjectSortability(served);
157
+ if (!projection || !schema || typeof schema !== 'object')
158
+ return schema;
159
+ Object.defineProperty(schema, OBJECT_SORTABILITY_KEY, {
160
+ value: projection,
161
+ enumerable: false,
162
+ configurable: true,
163
+ writable: true,
164
+ });
165
+ return schema;
166
+ }
167
+ /**
168
+ * Read the served projection off a client-side object schema.
169
+ *
170
+ * `undefined` means NO SIGNAL WAS SERVED — not "nothing is sortable". See the
171
+ * module docblock: callers must branch on this before applying the contract.
172
+ */
173
+ export function readObjectSortability(schema) {
174
+ if (!schema || typeof schema !== 'object')
175
+ return undefined;
176
+ const carried = schema[OBJECT_SORTABILITY_KEY];
177
+ if (!carried || typeof carried !== 'object')
178
+ return undefined;
179
+ const fields = carried.fields;
180
+ if (!fields || typeof fields !== 'object' || Array.isArray(fields))
181
+ return undefined;
182
+ return carried;
183
+ }
184
+ /**
185
+ * THE contract test: does the platform offer a sort over this field name?
186
+ *
187
+ * `true` iff the projection has an entry for `fieldName` and that entry says
188
+ * `sortable: true`. Every other case — no entry, `sortable: false`, a
189
+ * malformed entry — answers `false`, because absence means "no platform sort
190
+ * behind this name".
191
+ *
192
+ * `sortability` is REQUIRED and non-optional on purpose: a caller holding
193
+ * `undefined` (no signal served) has a different question to answer and must
194
+ * not reach this function with it. See {@link readObjectSortability}.
195
+ */
196
+ export function isPlatformSortableField(sortability, fieldName) {
197
+ if (typeof fieldName !== 'string' || fieldName.length === 0)
198
+ return false;
199
+ const entry = sortability.fields?.[fieldName];
200
+ return asFieldSortability(entry)?.sortable === true;
201
+ }
202
+ /**
203
+ * Drop from a sort list every entry the platform will not order by.
204
+ *
205
+ * The RESTORE leg of the same contract: withholding the header click stops a
206
+ * fresh unsortable sort from being created, but a sort persisted BEFORE the
207
+ * signal existed is already in stored view state, and replaying it would keep
208
+ * a refused `$orderby` alive and re-persist it on the next unrelated edit.
209
+ * Filtering the list at the seam the grid both renders from and emits through
210
+ * means an already-stored entry is inert and is dropped the first time
211
+ * anything writes the sort back.
212
+ */
213
+ export function filterPlatformSortableSort(sort, sortability) {
214
+ if (!Array.isArray(sort))
215
+ return [];
216
+ return sort.filter((item) => isPlatformSortableField(sortability, item?.field));
217
+ }
@@ -27,6 +27,31 @@ export interface DashboardFilterDef {
27
27
  name: string;
28
28
  /** Default target field when a widget declares no explicit binding. */
29
29
  field: string;
30
+ /**
31
+ * The object `field` lives on, carried through from
32
+ * @objectstack/spec's `GlobalFilterSchema.object` (`061f5e829`).
33
+ *
34
+ * The spec's own describe text for that key states the contract: "Object
35
+ * whose `fields.<object>.<field>` translation-bundle entry resolves this
36
+ * filter's field label and option labels". It was shipped on the spec side
37
+ * and never reached a consumer here, because this builder names the keys it
38
+ * copies and this one was not among them — so the value could not reach a
39
+ * renderer even in principle.
40
+ *
41
+ * ⚠️ NOT `optionsFrom.object`, which the spec separates deliberately: that
42
+ * one names the object dynamic OPTIONS are fetched from and may differ
43
+ * (filtering `opportunity` by `owner` with options sourced from `user`),
44
+ * while this one names the object whose translation bundle is keyed by
45
+ * `field`. Reading the former for label resolution resolves against the
46
+ * wrong object for exactly that filter.
47
+ *
48
+ * Like {@link DashboardFilterDef.label}, this module carries it through
49
+ * UNRESOLVED — `@object-ui/core` is locale-free by design and the bundle
50
+ * read belongs to the render side. Omitted entirely when the author declared
51
+ * none, so a dashboard that never names an object resolves to a def with the
52
+ * same key set it always had.
53
+ */
54
+ object?: string;
30
55
  /**
31
56
  * Display label, in @objectstack/spec's `I18nLabel` vocabulary — a plain
32
57
  * string, or an inline per-locale map (`{ en: 'Owner', 'zh-CN': '负责人' }`).
@@ -49,10 +74,11 @@ export interface DashboardFilterDef {
49
74
  * `resolveDashboardFilterDefs` — consumers always see the object form.
50
75
  *
51
76
  * The canonical authoring form is @objectstack/spec's `{ value, label }`
52
- * pair, and it is the ONLY one the platform accepts at publish. A bare-string
53
- * shorthand in a STORED document is still lifted here, with a deprecation
54
- * warning, on the objectstack#7917 retirement schedule — see
55
- * `normalizeFilterOptions`. Do not author a new one.
77
+ * pair, and it is the ONLY one the platform accepts at publish. The
78
+ * bare-string shorthand a STORED document may still carry is NOT lifted
79
+ * (objectui#4356, retired on the objectstack#7917 option-② schedule): such
80
+ * a member yields no option, and `normalizeFilterOptions` says so once. Do
81
+ * not author one; rewrite `"X"` as `{ "value": "X", "label": "X" }`.
56
82
  *
57
83
  * The PAIR SHAPE is normalized; the label's own vocabulary is not. `label`
58
84
  * is `I18nLabel` in `GlobalFilterSchema.options[]` too, and it reaches the
@@ -99,12 +125,18 @@ export interface DateRangeValue {
99
125
  * protect.
100
126
  *
101
127
  * What stays local is the BOUNDS table above, which is the other half of
102
- * #4614's design ("the two vocabularies live one import apart and neither
128
+ * objectstack-ai/objectstack#4614's design ("the two vocabularies live one import apart and neither
103
129
  * restates the other's grammar"): the spec owns which presets exist, this
104
130
  * module owns what each one resolves to in date-macro tokens.
105
131
  */
106
132
  export { DATE_RANGE_PRESETS };
107
- /** Reset the shorthand-options warn-once memo. Exported for tests. */
133
+ /**
134
+ * Reset the dropped-shorthand warn-once memo. Exported for tests.
135
+ *
136
+ * Kept through the objectui#4356 retirement: the memo it clears outlived the
137
+ * deprecation warning, because the warning that replaced it (a member was
138
+ * DROPPED, not lifted) dedupes on the same key for the same render-path reason.
139
+ */
108
140
  export declare function resetDashboardFilterWarnings(): void;
109
141
  /**
110
142
  * Normalize a dashboard schema's filter declarations into a flat list of
@@ -6,12 +6,39 @@
6
6
  * LICENSE file in the root directory of this source tree.
7
7
  */
8
8
  import { liftLegacyGlobalFilterDefault } from '@object-ui/types';
9
- import { DATE_RANGE_PRESETS } from '@objectstack/spec/ui';
9
+ import { DATE_RANGE_PRESETS, DashboardSchema as SpecDashboardSchema } from '@objectstack/spec/ui';
10
10
  import { resolveDateMacros } from './date-macros.js';
11
11
  /** Reserved filter name for the dashboard's built-in date range. */
12
12
  export const DATE_RANGE_FILTER_NAME = 'dateRange';
13
13
  /** Default target field for the built-in date range filter. */
14
14
  const DATE_RANGE_DEFAULT_FIELD = 'created_at';
15
+ /**
16
+ * The preset an authored `dateRange` resolves to when it omits `defaultRange`
17
+ * (objectui#10339) — READ FROM THE SPEC, never restated here.
18
+ *
19
+ * `@objectstack/spec` declares `DashboardSchema.dateRange.defaultRange` with a
20
+ * `.default(...)`, so the platform's parse of `dateRange: { field: 'created_at' }`
21
+ * carries that preset, and a renderer that treated the omission as "no filter"
22
+ * showed an UNFILTERED dashboard for a document the protocol says is filtered
23
+ * (#7759 ruling 5617465269, rule 1: the read site implements every arm the spec
24
+ * declares). The value is derived by parsing an EMPTY `dateRange` element
25
+ * through the spec's own schema: the only keys that come back are the ones the
26
+ * spec supplies by default, so this reads the spec's answer rather than a
27
+ * hand-copied string that could drift from it. `allowCustomRange` is the
28
+ * sibling default and needs no such read — it is consumed as `!== false`,
29
+ * which already is its spec default.
30
+ *
31
+ * Computed on first use and cached: `DashboardSchema` is a lazy schema, and
32
+ * forcing its construction at module load would cost every importer of
33
+ * `@object-ui/core` for a value only a dashboard with a bare `dateRange` needs.
34
+ */
35
+ let specDateRangeDefault;
36
+ function specDefaultDateRangePreset() {
37
+ if (specDateRangeDefault === undefined) {
38
+ specDateRangeDefault = SpecDashboardSchema.shape.dateRange.parse({})?.defaultRange;
39
+ }
40
+ return specDateRangeDefault;
41
+ }
15
42
  /**
16
43
  * Date-range presets → date-macro token bounds. Tokens stay symbolic in the
17
44
  * generated condition; every widget renderer resolves them at query time via
@@ -72,7 +99,7 @@ function presetBounds(preset) {
72
99
  * protect.
73
100
  *
74
101
  * What stays local is the BOUNDS table above, which is the other half of
75
- * #4614's design ("the two vocabularies live one import apart and neither
102
+ * objectstack-ai/objectstack#4614's design ("the two vocabularies live one import apart and neither
76
103
  * restates the other's grammar"): the spec owns which presets exist, this
77
104
  * module owns what each one resolves to in date-macro tokens.
78
105
  */
@@ -111,13 +138,13 @@ function warnDateFilter(message) {
111
138
  console.warn(`[dashboard-filters] ${message}`);
112
139
  }
113
140
  /**
114
- * Dev-mode gate, matching `actions/actionKeys.ts` — a deprecation warning that
115
- * floods a production console is a warning that gets muted.
141
+ * Dev-mode gate, matching `actions/actionKeys.ts` — a warning that floods a
142
+ * production console is a warning that gets muted.
116
143
  */
117
144
  const isDev = () => globalThis.process?.env?.NODE_ENV !==
118
145
  'production';
119
146
  /**
120
- * Warn-once memo for the bare-string `options` shorthand (objectui#4356).
147
+ * Warn-once memo for the DROPPED bare-string `options` members (objectui#4356).
121
148
  *
122
149
  * Keyed by filter NAME **and** the offending values, deliberately — the same
123
150
  * reasoning `warnOnUnknownActionKeys` records for its own memo. Keying on the
@@ -132,7 +159,13 @@ const isDev = () => globalThis.process?.env?.NODE_ENV !==
132
159
  * flood the dedupe exists to prevent.
133
160
  */
134
161
  const warnedShorthandOptions = new Set();
135
- /** Reset the shorthand-options warn-once memo. Exported for tests. */
162
+ /**
163
+ * Reset the dropped-shorthand warn-once memo. Exported for tests.
164
+ *
165
+ * Kept through the objectui#4356 retirement: the memo it clears outlived the
166
+ * deprecation warning, because the warning that replaced it (a member was
167
+ * DROPPED, not lifted) dedupes on the same key for the same render-path reason.
168
+ */
136
169
  export function resetDashboardFilterWarnings() {
137
170
  warnedShorthandOptions.clear();
138
171
  }
@@ -252,45 +285,40 @@ function normalizeDateDefault(type, defaultValue) {
252
285
  }
253
286
  /**
254
287
  * Normalize a filter's static `options` declaration to `{ value, label }`
255
- * pairs. The @objectstack/spec `GlobalFilterSchema.options` form is
256
- * `{ value, label }` objects; the bare-string shorthand (`options: ['EMEA', …]`)
257
- * is still lifted, but is DEPRECATED and now says so out loud. Rendering an
258
- * un-normalized option crashes React — this is the single place both shapes
259
- * converge.
288
+ * pairs — the @objectstack/spec `GlobalFilterSchema.options` form, and the
289
+ * only form this function emits. Rendering an un-normalized option crashes
290
+ * React, so every member that reaches a renderer passes through here.
260
291
  *
261
- * ## The shorthand's deprecation (objectui#4356, objectstack#7917)
292
+ * ## The bare-string shorthand is RETIRED (objectui#4356, objectstack#7917)
262
293
  *
263
294
  * Maintainer ruling of 2026-08-12 on objectstack#7917, verbatim 「7917 ②」:
264
- * option ② — **the spec stays strict; the runtime bare-string lift retires
265
- * behind a deprecation window sized by a stored-dashboard survey.** So a
266
- * document spelling `options: ['EMEA']` renders here and is refused the moment
267
- * it reaches the platform's validation — the "one strict contract beats N
268
- * dialects" divergence AGENTS.md #0.1 names, with the renderer's tolerance
269
- * acting as a second de-facto contract.
270
- *
271
- * This is the WARN half of that window (Phase 1). The lift itself is unchanged
272
- * and remains mechanically lossless (`'EMEA'` → `{ value: 'EMEA', label:
273
- * 'EMEA' }`), because stored dashboards carry the shorthand and dropping it
274
- * silently would turn a rendering filter into an empty one. Removal (Phase 2)
275
- * is scheduled on objectstack#7917, earliest one minor release after this ships
276
- * and not before the live-tenant channel has actually been queried.
277
- *
278
- * The warning is not decoration: a silent lift can never be retired, because
279
- * nothing would ever show that the last shorthand document is gone (ADR-0078 —
280
- * nothing silently inert). It is the same reasoning `liftLegacyFilterDeclaration`
281
- * records above, for the sibling alias.
282
- *
283
- * Phase 0 shipped in the same PR: objectui's own docs, its `plugin-dashboard`
284
- * README and its schema-catalog corpus stopped TEACHING the shorthand, so the
285
- * stored population is no longer growing while this warning asks authors to
286
- * migrate. Warning authors while the docs still taught the form would have been
287
- * a contradiction users report as a bug.
295
+ * option ② — **the spec stays strict; the runtime bare-string lift retires.**
296
+ * The window closed on the maintainer's 2026-09-02 ruling, verbatim
297
+ * 「objectstack#7917 不考虑现有数据」: Phase 2 proceeds on release cadence
298
+ * alone, with no live-tenant survey and no migration entry.
299
+ *
300
+ * So `options: ['EMEA']` — which the Phase 1 build (PR #4601) lifted to
301
+ * `{ value: 'EMEA', label: 'EMEA' }` under a deprecation warning — now yields
302
+ * NO option. A member that is not an object is skipped, exactly as a nullish
303
+ * member or an object with no `value` already was: a shorthand-only filter
304
+ * resolves with no `options`, and a mixed array keeps only its object members.
305
+ * The runtime reads the document the way the spec does, which is the end
306
+ * state option ② named — one strict contract, no renderer dialect beside it
307
+ * (AGENTS.md #0.1). ⛔ No lift, alias, fallback or migration returns here.
308
+ *
309
+ * The skip is not silent. `warnDroppedShorthandOptions` names the filter, the
310
+ * dropped members and the rewrite, once per filter per session — ADR-0078 §4:
311
+ * where the runtime skips an instance it emits a dev-mode diagnostic rather
312
+ * than swallowing it, and a STORED dashboard is the one document no author-time
313
+ * gate ever re-reads, so an empty select with nothing in the console would read
314
+ * as "no data". What that warning must NOT do is promise a lift: the Phase 1
315
+ * text said "still lifted here", and that sentence retired with the arm.
288
316
  *
289
317
  * ## What is normalized, and what is deliberately NOT (objectui#4032 / #4163)
290
318
  *
291
- * The PAIR SHAPE is normalized (`value` stringified, a bare string lifted to a
292
- * pair). The LABEL's authoring vocabulary is carried through untouched, because
293
- * `label` is `I18nLabel` — a string OR an inline per-locale map.
319
+ * The PAIR SHAPE is normalized (`value` stringified). The LABEL's authoring
320
+ * vocabulary is carried through untouched, because `label` is `I18nLabel` — a
321
+ * string OR an inline per-locale map.
294
322
  *
295
323
  * This line used to read:
296
324
  *
@@ -312,8 +340,8 @@ function normalizeFilterOptions(options, filterName) {
312
340
  if (!Array.isArray(options) || options.length === 0)
313
341
  return undefined;
314
342
  const normalized = [];
315
- /** Every bare-string member, in authored order — one warning names them all. */
316
- const shorthand = [];
343
+ /** Every non-object member, in authored order — one warning names them all. */
344
+ const dropped = [];
317
345
  for (const o of options) {
318
346
  if (o === null || o === undefined)
319
347
  continue;
@@ -329,39 +357,48 @@ function normalizeFilterOptions(options, filterName) {
329
357
  });
330
358
  }
331
359
  else {
332
- shorthand.push(String(o));
333
- normalized.push({ value: String(o), label: String(o) });
360
+ // The retired shorthand arm (objectui#4356): nothing is pushed for this
361
+ // member. It is recorded for the warning and yields no option.
362
+ dropped.push(o);
334
363
  }
335
364
  }
336
- if (shorthand.length > 0)
337
- warnShorthandOptions(filterName, shorthand);
365
+ if (dropped.length > 0)
366
+ warnDroppedShorthandOptions(filterName, dropped);
338
367
  return normalized.length > 0 ? normalized : undefined;
339
368
  }
340
369
  /**
341
- * Say the deprecated shorthand out loud — once per offending filter per
342
- * session, naming the filter and printing the canonical replacement.
370
+ * Say the dropped members out loud — once per offending filter per session,
371
+ * naming the filter, the members that yielded no option, and the rewrite.
343
372
  *
344
- * Collected per FILTER rather than per option: a filter declaring
373
+ * Collected per FILTER rather than per member: a filter declaring
345
374
  * `['EMEA', 'APAC', 'AMER']` is one authoring mistake in one place, so it earns
346
375
  * one warning carrying all three values, not three warnings the author has to
347
376
  * reassemble. A MIXED array (`[{ value: 'won', … }, 'lost']`) names only the
348
- * bare members, which are the ones that need rewriting — partial migrations
349
- * happen and a warning that re-reports the already-canonical members is noise.
377
+ * dropped members — the object members rendered, and re-reporting them is noise.
378
+ *
379
+ * Dev-mode only and memoised, exactly as the Phase 1 deprecation warning was:
380
+ * `resolveDashboardFilterDefs` runs on every dashboard render, and a warning
381
+ * that floods a console is a warning that gets muted. What changed is the
382
+ * sentence — this one reports a member that was DROPPED and promises no lift.
350
383
  */
351
- function warnShorthandOptions(filterName, shorthand) {
384
+ function warnDroppedShorthandOptions(filterName, dropped) {
352
385
  if (!isDev())
353
386
  return;
354
- const memo = `${filterName}:${shorthand.join(',')}`;
387
+ // A string is quoted so `"42"` and `42` stay distinguishable; anything else is
388
+ // spelled as itself — `String()` rather than `JSON.stringify()`, because a
389
+ // programmatic caller can hand this a value JSON cannot carry.
390
+ const spelled = dropped.map((v) => (typeof v === 'string' ? JSON.stringify(v) : String(v)));
391
+ const memo = `${filterName}:${spelled.join(',')}`;
355
392
  if (warnedShorthandOptions.has(memo))
356
393
  return;
357
394
  warnedShorthandOptions.add(memo);
358
- const canonical = shorthand.map((v) => `{ value: ${JSON.stringify(v)}, label: ${JSON.stringify(v)} }`).join(', ');
359
- warnDateFilter(`filter "${filterName}": \`options\` carries the bare-string shorthand ` +
360
- `(${shorthand.map((v) => JSON.stringify(v)).join(', ')}), which @objectstack/spec's ` +
361
- `\`GlobalFilterSchema\` REFUSES at publish — a dashboard authored this way renders here ` +
362
- `and is rejected the moment it reaches the platform (objectui#4356). Rewrite the stored ` +
363
- `dashboard to the canonical pair form: [${canonical}]. Still lifted here for already-` +
364
- `persisted dashboards; the lift is removed on the objectstack#7917 schedule.`);
395
+ const canonical = dropped
396
+ .map((v) => `{ value: ${JSON.stringify(String(v))}, label: ${JSON.stringify(String(v))} }`)
397
+ .join(', ');
398
+ warnDateFilter(`filter "${filterName}": \`options\` members ${spelled.join(', ')} are not \`{ value, label }\` ` +
399
+ `objects and were DROPPED — the bare-string shorthand is no longer lifted (objectui#4356), so the ` +
400
+ `filter renders without them. Rewrite the stored dashboard to @objectstack/spec's pair form: ` +
401
+ `[${canonical}].`);
365
402
  }
366
403
  /**
367
404
  * Normalize a dashboard schema's filter declarations into a flat list of
@@ -372,7 +409,11 @@ function warnShorthandOptions(filterName, shorthand) {
372
409
  export function resolveDashboardFilterDefs(schema) {
373
410
  const byName = new Map();
374
411
  if (schema.dateRange) {
375
- const preset = schema.dateRange.defaultRange;
412
+ // An omitted `defaultRange` takes the spec's declared default (#10339); an
413
+ // explicit value — `'custom'` included — is used exactly as authored.
414
+ const preset = schema.dateRange.defaultRange === undefined
415
+ ? specDefaultDateRangePreset()
416
+ : schema.dateRange.defaultRange;
376
417
  byName.set(DATE_RANGE_FILTER_NAME, {
377
418
  name: DATE_RANGE_FILTER_NAME,
378
419
  field: schema.dateRange.field || DATE_RANGE_DEFAULT_FIELD,
@@ -398,9 +439,14 @@ export function resolveDashboardFilterDefs(schema) {
398
439
  byName.set(name, {
399
440
  name,
400
441
  field: f.field,
442
+ // Spread rather than assigned (`061f5e829`): every filter authored
443
+ // before this key existed keeps the exact key set it resolved to, so
444
+ // `'object' in def` stays the discriminator the render side branches on
445
+ // and no consumer sees a new own-property carrying `undefined`.
446
+ ...(typeof f.object === 'string' && f.object ? { object: f.object } : {}),
401
447
  label: f.label,
402
448
  type,
403
- // `name` is the identifying context the deprecation warning needs, and
449
+ // `name` is the identifying context the dropped-member warning needs, and
404
450
  // the local above already resolved it — nothing new is threaded through a
405
451
  // public signature for it. `normalizeFilterOptions` is module-private, so
406
452
  // widening ITS parameter list is not a contract move.