@object-ui/core 17.6.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 (134) hide show
  1. package/CHANGELOG.md +6610 -0
  2. package/README.md +101 -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 +288 -27
  6. package/dist/actions/ActionRunner.js +443 -63
  7. package/dist/actions/TransactionManager.d.ts +7 -2
  8. package/dist/actions/actionKeys.d.ts +9 -7
  9. package/dist/actions/actionKeys.js +57 -14
  10. package/dist/actions/actionResponse.js +2 -2
  11. package/dist/actions/index.d.ts +1 -0
  12. package/dist/actions/index.js +1 -0
  13. package/dist/actions/recordDelete.d.ts +118 -0
  14. package/dist/actions/recordDelete.js +112 -0
  15. package/dist/adapters/ValueDataSource.d.ts +6 -1
  16. package/dist/adapters/ValueDataSource.js +1090 -98
  17. package/dist/adapters/batchTransaction.d.ts +1 -1
  18. package/dist/adapters/batchTransaction.js +1 -1
  19. package/dist/builder/schema-builder.d.ts +38 -81
  20. package/dist/builder/schema-builder.js +33 -152
  21. package/dist/data-scope/ViewDataProvider.d.ts +1 -1
  22. package/dist/data-scope/ViewDataProvider.js +21 -1
  23. package/dist/data-scope/element-data-source.d.ts +261 -7
  24. package/dist/data-scope/element-data-source.js +342 -52
  25. package/dist/data-scope/index.d.ts +9 -4
  26. package/dist/data-scope/index.js +9 -4
  27. package/dist/evaluator/ExpressionEvaluator.d.ts +86 -0
  28. package/dist/evaluator/ExpressionEvaluator.js +184 -12
  29. package/dist/evaluator/FormulaFunctions.d.ts +0 -10
  30. package/dist/evaluator/FormulaFunctions.js +166 -27
  31. package/dist/evaluator/declaredPredicate.d.ts +93 -9
  32. package/dist/evaluator/declaredPredicate.js +117 -16
  33. package/dist/evaluator/fieldRules.d.ts +88 -4
  34. package/dist/evaluator/fieldRules.js +203 -31
  35. package/dist/evaluator/index.d.ts +1 -0
  36. package/dist/evaluator/index.js +1 -0
  37. package/dist/evaluator/listConditional.d.ts +103 -34
  38. package/dist/evaluator/listConditional.js +127 -50
  39. package/dist/evaluator/optionRules.d.ts +77 -6
  40. package/dist/evaluator/optionRules.js +87 -10
  41. package/dist/evaluator/predicateInput.d.ts +15 -4
  42. package/dist/evaluator/predicateInput.js +35 -2
  43. package/dist/evaluator/rowPredicateCanon.d.ts +7 -18
  44. package/dist/evaluator/rowPredicateCanon.js +65 -115
  45. package/dist/evaluator/subjectPermissions.d.ts +76 -0
  46. package/dist/evaluator/subjectPermissions.js +44 -0
  47. package/dist/evaluator/unevaluablePredicate.d.ts +57 -0
  48. package/dist/evaluator/unevaluablePredicate.js +70 -0
  49. package/dist/index.d.ts +14 -0
  50. package/dist/index.js +80 -2
  51. package/dist/protocols/index.d.ts +0 -1
  52. package/dist/protocols/index.js +9 -1
  53. package/dist/registry/Registry.d.ts +358 -31
  54. package/dist/registry/Registry.js +263 -11
  55. package/dist/registry/WidgetRegistry.js +7 -3
  56. package/dist/registry/html-tier-intrinsics.d.ts +90 -0
  57. package/dist/registry/html-tier-intrinsics.js +110 -0
  58. package/dist/registry/public-blocks.d.ts +8 -0
  59. package/dist/registry/public-blocks.js +57 -0
  60. package/dist/theme/ThemeEngine.js +1 -1
  61. package/dist/utils/chart-category-key.d.ts +82 -0
  62. package/dist/utils/chart-category-key.js +84 -0
  63. package/dist/utils/chart-measure-key.d.ts +64 -0
  64. package/dist/utils/chart-measure-key.js +65 -0
  65. package/dist/utils/chart-presentation.d.ts +95 -112
  66. package/dist/utils/chart-presentation.js +169 -104
  67. package/dist/utils/chart-series.d.ts +131 -5
  68. package/dist/utils/chart-series.js +98 -16
  69. package/dist/utils/column-sortability.d.ts +157 -0
  70. package/dist/utils/column-sortability.js +217 -0
  71. package/dist/utils/dashboard-filters.d.ts +38 -6
  72. package/dist/utils/dashboard-filters.js +107 -61
  73. package/dist/utils/dataset-format.d.ts +211 -30
  74. package/dist/utils/dataset-format.js +389 -46
  75. package/dist/utils/date-display.d.ts +354 -0
  76. package/dist/utils/date-display.js +463 -0
  77. package/dist/utils/date-macros.js +7 -0
  78. package/dist/utils/drill-down.d.ts +78 -0
  79. package/dist/utils/drill-down.js +93 -0
  80. package/dist/utils/emptiness.d.ts +96 -0
  81. package/dist/utils/emptiness.js +101 -0
  82. package/dist/utils/expand-fields.d.ts +76 -6
  83. package/dist/utils/expand-fields.js +76 -6
  84. package/dist/utils/extract-records.d.ts +65 -5
  85. package/dist/utils/extract-records.js +65 -8
  86. package/dist/utils/filter-converter.d.ts +152 -4
  87. package/dist/utils/filter-converter.js +1092 -22
  88. package/dist/utils/filter-tokens.d.ts +49 -5
  89. package/dist/utils/filter-tokens.js +105 -33
  90. package/dist/utils/grouping-fields.d.ts +61 -0
  91. package/dist/utils/grouping-fields.js +84 -0
  92. package/dist/utils/managedBy.d.ts +3 -3
  93. package/dist/utils/managedBy.js +5 -5
  94. package/dist/utils/native-date-value.d.ts +48 -0
  95. package/dist/utils/native-date-value.js +167 -0
  96. package/dist/utils/non-grid-row-ceiling.d.ts +143 -0
  97. package/dist/utils/non-grid-row-ceiling.js +141 -0
  98. package/dist/utils/normalize-list-view.d.ts +121 -1
  99. package/dist/utils/normalize-list-view.js +186 -6
  100. package/dist/utils/number-display.d.ts +63 -14
  101. package/dist/utils/number-display.js +34 -12
  102. package/dist/utils/object-aggregate-query.d.ts +113 -0
  103. package/dist/utils/object-aggregate-query.js +50 -0
  104. package/dist/utils/parent-scope.d.ts +123 -0
  105. package/dist/utils/parent-scope.js +140 -0
  106. package/dist/utils/predicate-fields.d.ts +45 -12
  107. package/dist/utils/predicate-fields.js +137 -12
  108. package/dist/utils/record-color.d.ts +75 -0
  109. package/dist/utils/record-color.js +65 -0
  110. package/dist/utils/record-source.d.ts +298 -0
  111. package/dist/utils/record-source.js +331 -0
  112. package/dist/utils/record-title.d.ts +148 -8
  113. package/dist/utils/record-title.js +220 -26
  114. package/dist/utils/reference-keys.d.ts +21 -22
  115. package/dist/utils/reference-keys.js +532 -29
  116. package/dist/utils/sort-query.d.ts +115 -15
  117. package/dist/utils/sort-query.js +240 -13
  118. package/dist/utils/text-comparand.d.ts +92 -0
  119. package/dist/utils/text-comparand.js +99 -0
  120. package/dist/utils/without-denied-fields.d.ts +49 -0
  121. package/dist/utils/without-denied-fields.js +52 -0
  122. package/dist/validation/index.d.ts +8 -4
  123. package/dist/validation/index.js +8 -4
  124. package/dist/validation/schema-validator.d.ts +1 -1
  125. package/dist/validation/schema-validator.js +200 -82
  126. package/dist/validation/validators/object-validation-engine.d.ts +1 -1
  127. package/dist/validation/validators/object-validation-engine.js +14 -3
  128. package/package.json +8 -9
  129. package/dist/data-scope/DataScopeManager.d.ts +0 -127
  130. package/dist/data-scope/DataScopeManager.js +0 -229
  131. package/dist/protocols/ResponsiveProtocol.d.ts +0 -73
  132. package/dist/protocols/ResponsiveProtocol.js +0 -158
  133. package/dist/validation/validation-engine.d.ts +0 -88
  134. package/dist/validation/validation-engine.js +0 -434
@@ -0,0 +1,157 @@
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
+ export type { FieldSortability, ObjectSortability } from '@objectstack/spec/api';
83
+ import type { ObjectSortability } from '@objectstack/spec/api';
84
+ /**
85
+ * Where the projection rides on the client-side object schema.
86
+ *
87
+ * A GLOBAL-REGISTRY SYMBOL, deliberately — the same carrier shape
88
+ * `INFLIGHT_GET_REGISTRY_KEY` uses in `@object-ui/types`. Three properties
89
+ * this signal needs and a string key would not give it:
90
+ *
91
+ * - `JSON.stringify` drops symbol-keyed properties, so a schema object that
92
+ * is ever round-tripped back at a metadata WRITE endpoint cannot carry the
93
+ * projection into a document the server parses `strict` and rejects by
94
+ * name. The upstream change kept the key off `item` for exactly that
95
+ * reason; stamping a string key here would undo it one repo away.
96
+ * - `Object.keys` / spread / `for…in` do not see it, so every existing reader
97
+ * that enumerates the schema (column generators, diffing, form builders)
98
+ * is unchanged by its presence.
99
+ * - `Symbol.for` is the cross-realm registry, so two copies of this module
100
+ * (a workspace build and a published one in the same graph) agree on the
101
+ * key rather than silently reading past each other.
102
+ */
103
+ export declare const OBJECT_SORTABILITY_KEY: unique symbol;
104
+ /**
105
+ * Normalize a served `sortability` envelope value into the projection, or
106
+ * `undefined` when the envelope carried nothing usable.
107
+ *
108
+ * Tolerant on purpose (the same contract the upstream resolver carries): the
109
+ * adapter hands this whatever came back over the wire, unparsed.
110
+ */
111
+ export declare function normalizeObjectSortability(served: unknown): ObjectSortability | undefined;
112
+ /**
113
+ * Stamp the served projection onto the client-side object schema, under
114
+ * {@link OBJECT_SORTABILITY_KEY}.
115
+ *
116
+ * Non-enumerable and non-writable-by-accident: the property is defined rather
117
+ * than assigned so it never shows up in a spread of the schema and cannot be
118
+ * clobbered by one. Returns the same object it was handed (the adapter caches
119
+ * that instance), and is a no-op when there is nothing usable to attach — so
120
+ * an older backend leaves the schema exactly as it found it rather than
121
+ * stamping an empty projection that would read as "nothing is sortable".
122
+ */
123
+ export declare function attachObjectSortability<T>(schema: T, served: unknown): T;
124
+ /**
125
+ * Read the served projection off a client-side object schema.
126
+ *
127
+ * `undefined` means NO SIGNAL WAS SERVED — not "nothing is sortable". See the
128
+ * module docblock: callers must branch on this before applying the contract.
129
+ */
130
+ export declare function readObjectSortability(schema: unknown): ObjectSortability | undefined;
131
+ /**
132
+ * THE contract test: does the platform offer a sort over this field name?
133
+ *
134
+ * `true` iff the projection has an entry for `fieldName` and that entry says
135
+ * `sortable: true`. Every other case — no entry, `sortable: false`, a
136
+ * malformed entry — answers `false`, because absence means "no platform sort
137
+ * behind this name".
138
+ *
139
+ * `sortability` is REQUIRED and non-optional on purpose: a caller holding
140
+ * `undefined` (no signal served) has a different question to answer and must
141
+ * not reach this function with it. See {@link readObjectSortability}.
142
+ */
143
+ export declare function isPlatformSortableField(sortability: ObjectSortability, fieldName: string | undefined): boolean;
144
+ /**
145
+ * Drop from a sort list every entry the platform will not order by.
146
+ *
147
+ * The RESTORE leg of the same contract: withholding the header click stops a
148
+ * fresh unsortable sort from being created, but a sort persisted BEFORE the
149
+ * signal existed is already in stored view state, and replaying it would keep
150
+ * a refused `$orderby` alive and re-persist it on the next unrelated edit.
151
+ * Filtering the list at the seam the grid both renders from and emits through
152
+ * means an already-stored entry is inert and is dropped the first time
153
+ * anything writes the sort back.
154
+ */
155
+ export declare function filterPlatformSortableSort<T extends {
156
+ field?: string;
157
+ }>(sort: readonly T[] | undefined, sortability: ObjectSortability): T[];
@@ -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