@object-ui/core 17.5.0 → 17.6.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 (70) hide show
  1. package/CHANGELOG.md +794 -0
  2. package/README.md +36 -22
  3. package/dist/actions/ActionEngine.d.ts +1 -1
  4. package/dist/actions/ActionEngine.js +1 -2
  5. package/dist/actions/ActionRunner.d.ts +82 -10
  6. package/dist/actions/ActionRunner.js +128 -22
  7. package/dist/actions/actionKeys.d.ts +68 -8
  8. package/dist/actions/actionKeys.js +81 -9
  9. package/dist/actions/actionResponse.js +1 -1
  10. package/dist/actions/index.d.ts +1 -0
  11. package/dist/actions/index.js +1 -0
  12. package/dist/actions/recordIdParam.d.ts +77 -0
  13. package/dist/actions/recordIdParam.js +37 -0
  14. package/dist/actions/serverActionHandler.d.ts +1 -1
  15. package/dist/actions/serverActionHandler.js +1 -1
  16. package/dist/evaluator/ExpressionEvaluator.d.ts +42 -1
  17. package/dist/evaluator/ExpressionEvaluator.js +42 -1
  18. package/dist/evaluator/SafeExpressionParser.js +0 -4
  19. package/dist/evaluator/declaredPredicate.js +1 -1
  20. package/dist/evaluator/fieldRules.d.ts +19 -0
  21. package/dist/evaluator/fieldRules.js +30 -6
  22. package/dist/evaluator/index.d.ts +1 -0
  23. package/dist/evaluator/index.js +1 -0
  24. package/dist/evaluator/listConditional.d.ts +49 -2
  25. package/dist/evaluator/listConditional.js +70 -6
  26. package/dist/evaluator/optionRules.d.ts +55 -0
  27. package/dist/evaluator/optionRules.js +60 -0
  28. package/dist/evaluator/rowPredicateCanon.d.ts +67 -0
  29. package/dist/evaluator/rowPredicateCanon.js +208 -0
  30. package/dist/index.d.ts +4 -0
  31. package/dist/index.js +17 -0
  32. package/dist/registry/Registry.d.ts +41 -19
  33. package/dist/theme/index.d.ts +1 -1
  34. package/dist/theme/index.js +1 -1
  35. package/dist/types/index.d.ts +92 -13
  36. package/dist/utils/chart-presentation.d.ts +205 -0
  37. package/dist/utils/chart-presentation.js +280 -0
  38. package/dist/utils/chart-series.d.ts +140 -26
  39. package/dist/utils/chart-series.js +380 -39
  40. package/dist/utils/column-identity.d.ts +43 -0
  41. package/dist/utils/column-identity.js +47 -1
  42. package/dist/utils/dataset-format.js +18 -6
  43. package/dist/utils/debug.js +7 -0
  44. package/dist/utils/expand-fields.d.ts +97 -7
  45. package/dist/utils/expand-fields.js +97 -7
  46. package/dist/utils/filter-tokens.js +0 -1
  47. package/dist/utils/freeze-schema.d.ts +38 -1
  48. package/dist/utils/freeze-schema.js +10 -0
  49. package/dist/utils/humanize-label.d.ts +60 -0
  50. package/dist/utils/humanize-label.js +62 -0
  51. package/dist/utils/managedBy.d.ts +27 -2
  52. package/dist/utils/normalize-list-view.d.ts +10 -0
  53. package/dist/utils/normalize-list-view.js +63 -1
  54. package/dist/utils/number-display.d.ts +7 -3
  55. package/dist/utils/predicate-fields.d.ts +29 -4
  56. package/dist/utils/predicate-fields.js +44 -4
  57. package/dist/utils/retired-field-types.d.ts +90 -0
  58. package/dist/utils/retired-field-types.js +124 -0
  59. package/dist/utils/sort-query.d.ts +12 -10
  60. package/dist/utils/unmaterialized-fields.d.ts +52 -0
  61. package/dist/utils/unmaterialized-fields.js +57 -0
  62. package/dist/validation/index.d.ts +1 -0
  63. package/dist/validation/index.js +1 -0
  64. package/dist/validation/schema-validator.js +105 -0
  65. package/dist/validation/server-owned-value.d.ts +62 -0
  66. package/dist/validation/server-owned-value.js +99 -0
  67. package/dist/validation/validators/object-validation-engine.js +0 -2
  68. package/package.json +4 -4
  69. package/dist/__benchmarks__/core.bench.d.ts +0 -8
  70. package/dist/__benchmarks__/core.bench.js +0 -53
@@ -92,9 +92,15 @@ export function debugLog(category, message, data) {
92
92
  if (!isDebugEnabled())
93
93
  return;
94
94
  if (data !== undefined) {
95
+ // objectui#4029 — this IS the repo's debug channel (opt-in via
96
+ // globalThis.OBJECTUI_DEBUG, gated by isDebugEnabled() above), not a
97
+ // leaked module-scope log; no-console's blanket ban would otherwise
98
+ // block the function whose entire job is to console.log.
99
+ // eslint-disable-next-line no-console
95
100
  console.log(`[ObjectUI Debug][${category}] ${message}`, data);
96
101
  }
97
102
  else {
103
+ // eslint-disable-next-line no-console -- see comment above
98
104
  console.log(`[ObjectUI Debug][${category}] ${message}`);
99
105
  }
100
106
  }
@@ -116,6 +122,7 @@ export function debugTimeEnd(label) {
116
122
  const start = timers.get(label);
117
123
  if (start !== undefined) {
118
124
  const elapsed = (performance.now() - start).toFixed(2);
125
+ // eslint-disable-next-line no-console -- objectui#4029, see debugLog above
119
126
  console.log(`[ObjectUI Debug][perf] ${label}: ${elapsed}ms`);
120
127
  timers.delete(label);
121
128
  }
@@ -11,17 +11,107 @@
11
11
  * list / grid / detail cell can render the related record's display name
12
12
  * instead of a bare id placeholder ("—").
13
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.
14
+ * `user` is a lookup specialised to `sys_user`: it carries the same `reference`
15
+ * + id storage and the server resolves it through the same expand path as
16
+ * `lookup` / `master_detail`, so a `user` column that is NOT requested for
17
+ * expansion comes back as a raw user id and renders as "—" (objectui#2032).
21
18
  *
22
19
  * Note on `tree`: a self-referencing hierarchy field is a reference too, so it
23
20
  * belongs in this set; whether the backend materialises the expanded object
24
21
  * for it is a server concern — requesting it is harmless and forward-compatible.
22
+ *
23
+ * ## One family, many consumers — and NO reliable count of them
24
+ *
25
+ * This section used to open "One family, four consumers" and enumerate four.
26
+ * That count was hand-kept, and hand-kept counts of this table are exactly what
27
+ * keeps going wrong: it was already stale by two when objectui#5692 measured it
28
+ * (`paramToField` in `app-shell` had joined with objectui#5312, and
29
+ * `ListView`'s relational-sort rule reads the set directly), and objectui#5312's
30
+ * claim to have converted "the LAST private copy" was false by two MORE — see
31
+ * the falsification note at the end. Read the list below as the lineage of the
32
+ * conversions, not as a census; the mechanical fact is the identity pins.
33
+ *
34
+ * This is the reference-bearing FAMILY, not the `$expand` builder's private
35
+ * list, and these concerns read it under several different words:
36
+ *
37
+ * - `$expand` construction — `buildExpandFields` below ("expandable");
38
+ * - predicate-record projection — `predicate-record.ts` ("relational");
39
+ * - the object form's data-source wiring — `needsDataSourceWiring` in
40
+ * `packages/components/src/renderers/form/form.tsx`, which decides which
41
+ * registered widget gets `dataSource` / `dependentValues` /
42
+ * `dependsOnLabels` threaded to it;
43
+ * - the grid's bulk-action dialog — `widgetNeedsDataSource` in
44
+ * `packages/plugin-grid/src/components/bulkParamToField.ts`, which decides
45
+ * which param widget is handed the grid's `DataSource` and which param field
46
+ * shape carries `reference_to` / `display_field`;
47
+ * - the action-param dialog — `paramToField` in
48
+ * `packages/app-shell/src/utils/paramToField.ts`, which decides which param
49
+ * carries a reference target (objectui#5312);
50
+ * - the list view's relational-sort rule — `ListView.tsx`, which will not offer
51
+ * a server-side sort on a field whose stored value is a foreign key;
52
+ * - the dashboard table's `$expand` whitelist — `computeLookupExpand` in
53
+ * `packages/plugin-dashboard/src/ObjectDataTable.tsx` (objectui#5692);
54
+ * - the dashboard's relation/link test — `isLookupType` in
55
+ * `packages/plugin-dashboard/src/recordFields.tsx` (objectui#5692).
56
+ *
57
+ * The third one used to be a second hand-maintained copy, and this comment used
58
+ * to claim the set "mirrors the form layer's `DATA_SOURCE_FIELD_TYPES`
59
+ * (`lookup` / `master_detail` / `tree`)". That was true for 15 days. The form's
60
+ * copy then gained `capability-multiselect` (objectui#2403) and, the same day,
61
+ * `object-ref` / `filter-condition` / `recipient-picker` (objectui#2421), after
62
+ * which the two sets were not in a subset relation in EITHER direction — `user`
63
+ * only here, three picker names only there — and no gate could say so
64
+ * (objectui#4790). The form now DERIVES from this set instead of restating it:
65
+ *
66
+ * form's data-source rule == EXPANDABLE_FIELD_TYPES
67
+ * + three widget-hint-only picker names
68
+ *
69
+ * The extension is one-directional and cannot fold back into this set:
70
+ * `object-ref` / `filter-condition` / `recipient-picker` are widget HINTS, never
71
+ * declarable field types (`widgetHintOnly: true` in
72
+ * `app-shell/src/utils/paramValueShape.ts`), so no object schema can produce a
73
+ * field whose `type` is one of them.
74
+ *
75
+ * The fourth was a hand-maintained copy too, one member set removed from BOTH
76
+ * of the above (`lookup` / `master_detail` / `user` — no `tree`, no pickers, and
77
+ * for a while a fifth spelling, `owner`, that objectui#4814 retired). It now
78
+ * derives from this set with NO extension, because the bulk dialog's params
79
+ * cannot produce the three widget-hint names (objectui#4815):
80
+ *
81
+ * bulk dialog's data-source rule == EXPANDABLE_FIELD_TYPES
82
+ *
83
+ * Neither consumer copies the set — both call `.has()` on THIS object, and both
84
+ * carry an identity pin (a spy on this `has`) so a member-identical private copy
85
+ * fails rather than quietly re-forking the table.
86
+ *
87
+ * ## The "LAST private copy" claim was false — objectui#5692
88
+ *
89
+ * objectui#5312 converted `paramToField` (`app-shell`) and recorded it as the
90
+ * fourth and LAST private copy of this rule. `packages/plugin-dashboard` held
91
+ * two more the whole time — `LOOKUP_TYPES` in `recordFields.tsx` and an inline
92
+ * disjunction inside `computeLookupExpand` in `ObjectDataTable.tsx` — which
93
+ * predate that sweep and were outside its file surface, so nothing contradicted
94
+ * the claim. Both now derive from this set with NO extension and carry the same
95
+ * identity pin.
96
+ *
97
+ * Those two were NOT member-identical to this set, in either direction: they
98
+ * lacked `tree` and carried a fifth spelling, `reference`. Converging them was
99
+ * therefore a behaviour change in two directions, and the direction that could
100
+ * have widened THIS set was settled by measurement rather than by preference:
101
+ * `reference` is absent from `@objectstack/spec`'s closed `FieldType`
102
+ * vocabulary and is refused by `FieldSchema.safeParse` — measured with `lookup`
103
+ * / `master_detail` / `user` / `tree` as live controls and the retired `owner`
104
+ * plus a nonsense spelling as dead ones — so no spec-compliant object schema can
105
+ * declare a field whose stored type is `reference`, and the dashboard's copies
106
+ * were carrying a dead spelling, not a member this set was missing. This set is
107
+ * unchanged by objectui#5692; the two dashboard faces simply stopped answering
108
+ * for a type no producer can emit.
109
+ *
110
+ * Stated so it reads as a decision rather than a surprise: **adding a member
111
+ * here also grants that type the form's data-source wiring.** That is the
112
+ * intended coupling — a type whose stored value is a foreign key needs a
113
+ * `DataSource` to resolve that key on the read path (expand) and on the edit
114
+ * path (the picker) alike.
25
115
  */
26
116
  export declare const EXPANDABLE_FIELD_TYPES: ReadonlySet<string>;
27
117
  /**
@@ -12,17 +12,107 @@ import { columnIdentity } from './column-identity.js';
12
12
  * list / grid / detail cell can render the related record's display name
13
13
  * instead of a bare id placeholder ("—").
14
14
  *
15
- * This mirrors the form layer's `DATA_SOURCE_FIELD_TYPES`
16
- * (`lookup` / `master_detail` / `tree`) and additionally covers `user` — a
17
- * lookup specialised to `sys_user`. The server resolves `user` through the
18
- * same expand path as `lookup` / `master_detail` (it carries the same
19
- * `reference` + id storage), so a `user` column that is NOT requested for
20
- * expansion comes back as a raw user id and renders as "—". Keeping the set
21
- * here as the single source of truth is what closes that gap.
15
+ * `user` is a lookup specialised to `sys_user`: it carries the same `reference`
16
+ * + id storage and the server resolves it through the same expand path as
17
+ * `lookup` / `master_detail`, so a `user` column that is NOT requested for
18
+ * expansion comes back as a raw user id and renders as "—" (objectui#2032).
22
19
  *
23
20
  * Note on `tree`: a self-referencing hierarchy field is a reference too, so it
24
21
  * belongs in this set; whether the backend materialises the expanded object
25
22
  * for it is a server concern — requesting it is harmless and forward-compatible.
23
+ *
24
+ * ## One family, many consumers — and NO reliable count of them
25
+ *
26
+ * This section used to open "One family, four consumers" and enumerate four.
27
+ * That count was hand-kept, and hand-kept counts of this table are exactly what
28
+ * keeps going wrong: it was already stale by two when objectui#5692 measured it
29
+ * (`paramToField` in `app-shell` had joined with objectui#5312, and
30
+ * `ListView`'s relational-sort rule reads the set directly), and objectui#5312's
31
+ * claim to have converted "the LAST private copy" was false by two MORE — see
32
+ * the falsification note at the end. Read the list below as the lineage of the
33
+ * conversions, not as a census; the mechanical fact is the identity pins.
34
+ *
35
+ * This is the reference-bearing FAMILY, not the `$expand` builder's private
36
+ * list, and these concerns read it under several different words:
37
+ *
38
+ * - `$expand` construction — `buildExpandFields` below ("expandable");
39
+ * - predicate-record projection — `predicate-record.ts` ("relational");
40
+ * - the object form's data-source wiring — `needsDataSourceWiring` in
41
+ * `packages/components/src/renderers/form/form.tsx`, which decides which
42
+ * registered widget gets `dataSource` / `dependentValues` /
43
+ * `dependsOnLabels` threaded to it;
44
+ * - the grid's bulk-action dialog — `widgetNeedsDataSource` in
45
+ * `packages/plugin-grid/src/components/bulkParamToField.ts`, which decides
46
+ * which param widget is handed the grid's `DataSource` and which param field
47
+ * shape carries `reference_to` / `display_field`;
48
+ * - the action-param dialog — `paramToField` in
49
+ * `packages/app-shell/src/utils/paramToField.ts`, which decides which param
50
+ * carries a reference target (objectui#5312);
51
+ * - the list view's relational-sort rule — `ListView.tsx`, which will not offer
52
+ * a server-side sort on a field whose stored value is a foreign key;
53
+ * - the dashboard table's `$expand` whitelist — `computeLookupExpand` in
54
+ * `packages/plugin-dashboard/src/ObjectDataTable.tsx` (objectui#5692);
55
+ * - the dashboard's relation/link test — `isLookupType` in
56
+ * `packages/plugin-dashboard/src/recordFields.tsx` (objectui#5692).
57
+ *
58
+ * The third one used to be a second hand-maintained copy, and this comment used
59
+ * to claim the set "mirrors the form layer's `DATA_SOURCE_FIELD_TYPES`
60
+ * (`lookup` / `master_detail` / `tree`)". That was true for 15 days. The form's
61
+ * copy then gained `capability-multiselect` (objectui#2403) and, the same day,
62
+ * `object-ref` / `filter-condition` / `recipient-picker` (objectui#2421), after
63
+ * which the two sets were not in a subset relation in EITHER direction — `user`
64
+ * only here, three picker names only there — and no gate could say so
65
+ * (objectui#4790). The form now DERIVES from this set instead of restating it:
66
+ *
67
+ * form's data-source rule == EXPANDABLE_FIELD_TYPES
68
+ * + three widget-hint-only picker names
69
+ *
70
+ * The extension is one-directional and cannot fold back into this set:
71
+ * `object-ref` / `filter-condition` / `recipient-picker` are widget HINTS, never
72
+ * declarable field types (`widgetHintOnly: true` in
73
+ * `app-shell/src/utils/paramValueShape.ts`), so no object schema can produce a
74
+ * field whose `type` is one of them.
75
+ *
76
+ * The fourth was a hand-maintained copy too, one member set removed from BOTH
77
+ * of the above (`lookup` / `master_detail` / `user` — no `tree`, no pickers, and
78
+ * for a while a fifth spelling, `owner`, that objectui#4814 retired). It now
79
+ * derives from this set with NO extension, because the bulk dialog's params
80
+ * cannot produce the three widget-hint names (objectui#4815):
81
+ *
82
+ * bulk dialog's data-source rule == EXPANDABLE_FIELD_TYPES
83
+ *
84
+ * Neither consumer copies the set — both call `.has()` on THIS object, and both
85
+ * carry an identity pin (a spy on this `has`) so a member-identical private copy
86
+ * fails rather than quietly re-forking the table.
87
+ *
88
+ * ## The "LAST private copy" claim was false — objectui#5692
89
+ *
90
+ * objectui#5312 converted `paramToField` (`app-shell`) and recorded it as the
91
+ * fourth and LAST private copy of this rule. `packages/plugin-dashboard` held
92
+ * two more the whole time — `LOOKUP_TYPES` in `recordFields.tsx` and an inline
93
+ * disjunction inside `computeLookupExpand` in `ObjectDataTable.tsx` — which
94
+ * predate that sweep and were outside its file surface, so nothing contradicted
95
+ * the claim. Both now derive from this set with NO extension and carry the same
96
+ * identity pin.
97
+ *
98
+ * Those two were NOT member-identical to this set, in either direction: they
99
+ * lacked `tree` and carried a fifth spelling, `reference`. Converging them was
100
+ * therefore a behaviour change in two directions, and the direction that could
101
+ * have widened THIS set was settled by measurement rather than by preference:
102
+ * `reference` is absent from `@objectstack/spec`'s closed `FieldType`
103
+ * vocabulary and is refused by `FieldSchema.safeParse` — measured with `lookup`
104
+ * / `master_detail` / `user` / `tree` as live controls and the retired `owner`
105
+ * plus a nonsense spelling as dead ones — so no spec-compliant object schema can
106
+ * declare a field whose stored type is `reference`, and the dashboard's copies
107
+ * were carrying a dead spelling, not a member this set was missing. This set is
108
+ * unchanged by objectui#5692; the two dashboard faces simply stopped answering
109
+ * for a type no producer can emit.
110
+ *
111
+ * Stated so it reads as a decision rather than a surprise: **adding a member
112
+ * here also grants that type the form's data-source wiring.** That is the
113
+ * intended coupling — a type whose stored value is a foreign key needs a
114
+ * `DataSource` to resolve that key on the read path (expand) and on the edit
115
+ * path (the picker) alike.
26
116
  */
27
117
  export const EXPANDABLE_FIELD_TYPES = new Set([
28
118
  'lookup',
@@ -116,7 +116,6 @@ export function resolveContextTokens(filter, scope = {}) {
116
116
  ? () => { }
117
117
  : scope.onUnresolved ??
118
118
  ((message) => {
119
- // eslint-disable-next-line no-console
120
119
  console.warn(`[object-ui] ${message}`);
121
120
  });
122
121
  const values = {
@@ -36,6 +36,33 @@ export declare const SYSTEM_VIEW_MARKER: unique symbol;
36
36
  export type DeepReadonly<T> = T extends (...args: any[]) => any ? T : T extends ReadonlyArray<infer U> ? ReadonlyArray<DeepReadonly<U>> : T extends object ? {
37
37
  readonly [K in keyof T]: DeepReadonly<T[K]>;
38
38
  } : T;
39
+ /**
40
+ * The inverse of `DeepReadonly<T>`: recursively strip `readonly` at every depth.
41
+ *
42
+ * Named `DeepMutable` rather than `Mutable` for the same reason the type above
43
+ * is not called `Readonly` — TS' built-in `Readonly<T>` is SHALLOW, so a bare
44
+ * `Mutable` would understate what this does by exactly the depth gap that made
45
+ * `cloneAsOverride` wrong in the first place. A type whose name misreports its
46
+ * own reach is the defect this pair exists to close, not a naming preference.
47
+ *
48
+ * Symmetric with `DeepReadonly` arm for arm, deliberately including its two
49
+ * limits: a tuple widens to an array (`DeepReadonly` widens it the same way, so
50
+ * an inverse that preserved tuples would not be an inverse), and `Date`,
51
+ * `RegExp`, `Map` and `Set` are mapped as plain objects even though
52
+ * `isFreezableObject` skips them at runtime.
53
+ *
54
+ * One thing it deliberately does NOT do: drop the `SYSTEM_VIEW_MARKER` key.
55
+ * `cloneAsOverride` never copies the symbol, so a clone's marker is always
56
+ * absent — but the key is declared optional (`?: true`), and carrying it
57
+ * therefore states "may be absent", which is true. Excluding it needs
58
+ * `Exclude<keyof T, typeof SYSTEM_VIEW_MARKER>`, a NON-homomorphic mapped type
59
+ * that drops the `?` modifier from every other property and turns optional keys
60
+ * required — a strictly worse type, and a real break for callers, traded for
61
+ * removing a key that already reads as optional.
62
+ */
63
+ export type DeepMutable<T> = T extends (...args: any[]) => any ? T : T extends ReadonlyArray<infer U> ? DeepMutable<U>[] : T extends object ? {
64
+ -readonly [K in keyof T]: DeepMutable<T[K]>;
65
+ } : T;
39
66
  /**
40
67
  * A schema that has been frozen by `defineSystemView()`. The marker symbol is
41
68
  * non-enumerable and therefore invisible to consumers, but its presence
@@ -87,8 +114,18 @@ export declare function isSystemView(value: unknown): boolean;
87
114
  * Produce a deep, mutable clone of a System View so callers can apply
88
115
  * Tenant/User overrides without touching the source schema.
89
116
  *
117
+ * Returns `DeepMutable<T>`, not `T`. The declaration used to hand back the
118
+ * input type unchanged, which meant cloning a `SystemView<S>` returned
119
+ * something still typed deep-readonly even though the implementation had always
120
+ * produced a plain mutable object — so the override flow this function exists
121
+ * for, `draft.columns.push(...)`, did not type-check (TS2339). The return type
122
+ * now relaxes TOWARD what the runtime already does, never away from it, which
123
+ * is why the change adds capability to a caller's type rather than removing it.
124
+ *
90
125
  * Implementation note: uses `structuredClone` when available (Node 17+, all
91
126
  * evergreen browsers) and falls back to a JSON round-trip. The marker
92
127
  * symbol is intentionally NOT copied — the clone is no longer a System View.
128
+ * Neither path can copy it by accident: `structuredClone` ignores symbol keys,
129
+ * and `JSON.stringify` never sees them.
93
130
  */
94
- export declare function cloneAsOverride<T>(view: T): T;
131
+ export declare function cloneAsOverride<T>(view: T): DeepMutable<T>;
@@ -140,9 +140,19 @@ export function isSystemView(value) {
140
140
  * Produce a deep, mutable clone of a System View so callers can apply
141
141
  * Tenant/User overrides without touching the source schema.
142
142
  *
143
+ * Returns `DeepMutable<T>`, not `T`. The declaration used to hand back the
144
+ * input type unchanged, which meant cloning a `SystemView<S>` returned
145
+ * something still typed deep-readonly even though the implementation had always
146
+ * produced a plain mutable object — so the override flow this function exists
147
+ * for, `draft.columns.push(...)`, did not type-check (TS2339). The return type
148
+ * now relaxes TOWARD what the runtime already does, never away from it, which
149
+ * is why the change adds capability to a caller's type rather than removing it.
150
+ *
143
151
  * Implementation note: uses `structuredClone` when available (Node 17+, all
144
152
  * evergreen browsers) and falls back to a JSON round-trip. The marker
145
153
  * symbol is intentionally NOT copied — the clone is no longer a System View.
154
+ * Neither path can copy it by accident: `structuredClone` ignores symbol keys,
155
+ * and `JSON.stringify` never sees them.
146
156
  */
147
157
  export function cloneAsOverride(view) {
148
158
  if (view == null || typeof view !== 'object')
@@ -0,0 +1,60 @@
1
+ /**
2
+ * ObjectUI — value-fallback label prettifier
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
+ * humanize-label — the ONE home for the VALUE fallback: a stored value becomes
10
+ * a display string when nothing else resolves it (objectui#5444).
11
+ *
12
+ * Two byte-identical copies preceded this module: `@object-ui/fields` (read by
13
+ * `plugin-grid`, `plugin-gantt`, `plugin-detail`, and by the field renderers in
14
+ * that package itself) and a deliberate local copy in `plugin-charts`'
15
+ * `ObjectChart.tsx`, whose comment said it existed "to avoid a dependency on
16
+ * `@object-ui/fields`". `@object-ui/core` is the shared ancestor both packages
17
+ * already depend on, so the convention gets a single home without a new
18
+ * dependency edge and without React reaching core — objectui#4389's ruling for
19
+ * this family: core-canonical logic, plugins consume; never core importing
20
+ * react. Both former sites now re-export THIS function, so there is one
21
+ * implementation and two doorways.
22
+ *
23
+ * ## Why this is NOT `humanizeFieldKey`, and why the two are kept apart
24
+ *
25
+ * The renderer carries a second prettifier — `humanizeFieldKey` in
26
+ * `@object-ui/plugin-dashboard`, the single home for the KEY fallback (a field
27
+ * key becomes a column header). It is a DIFFERENT convention, not a stale copy
28
+ * of this one: it additionally splits camelCase.
29
+ *
30
+ * input humanizeFieldKey humanizeLabel (here)
31
+ * ────────────────── ────────────────── ──────────────────
32
+ * needs_analysis Needs Analysis Needs Analysis
33
+ * NeedsAnalysis Needs Analysis NeedsAnalysis <- differ
34
+ * unitPrice Unit Price UnitPrice <- differ
35
+ * BestCase Best Case BestCase <- differ
36
+ * lost-to-competitor Lost-To-Competitor Lost To Competitor <- differ
37
+ *
38
+ * They are fed different inputs, which is why their outputs may differ. A field
39
+ * KEY is authored in the codebase and is expected to carry a machine spelling,
40
+ * so splitting camelCase recovers words its author meant. A stored VALUE is
41
+ * arbitrary tenant data — a picklist member, an object name, a code from
42
+ * another system — where a mid-token capital is not reliably a word boundary,
43
+ * and splitting it rewrites what the tenant wrote (`McDonald` -> `Mc Donald`).
44
+ * Note also that the two conventions do not nest: on the last row above each
45
+ * leaves alone the separator the other rewrites, so "converge" would have to
46
+ * pick a winner per input class rather than adopt one side wholesale.
47
+ *
48
+ * Kept distinct DELIBERATELY, and this paragraph is the write-down whose
49
+ * absence made the split read as drift. Converging them is a decision, not a
50
+ * refactor: this function is read by grid, gantt, detail and charts, so any
51
+ * change to what it RETURNS moves rendered output in four packages at once.
52
+ * That change is explicitly out of scope for the convergence that created this
53
+ * module; it needs its own card.
54
+ *
55
+ * Examples:
56
+ * "in_progress" -> "In Progress"
57
+ * "high-priority" -> "High Priority"
58
+ * "active" -> "Active"
59
+ */
60
+ export declare function humanizeLabel(value: string): string;
@@ -0,0 +1,62 @@
1
+ /**
2
+ * ObjectUI — value-fallback label prettifier
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
+ * humanize-label — the ONE home for the VALUE fallback: a stored value becomes
10
+ * a display string when nothing else resolves it (objectui#5444).
11
+ *
12
+ * Two byte-identical copies preceded this module: `@object-ui/fields` (read by
13
+ * `plugin-grid`, `plugin-gantt`, `plugin-detail`, and by the field renderers in
14
+ * that package itself) and a deliberate local copy in `plugin-charts`'
15
+ * `ObjectChart.tsx`, whose comment said it existed "to avoid a dependency on
16
+ * `@object-ui/fields`". `@object-ui/core` is the shared ancestor both packages
17
+ * already depend on, so the convention gets a single home without a new
18
+ * dependency edge and without React reaching core — objectui#4389's ruling for
19
+ * this family: core-canonical logic, plugins consume; never core importing
20
+ * react. Both former sites now re-export THIS function, so there is one
21
+ * implementation and two doorways.
22
+ *
23
+ * ## Why this is NOT `humanizeFieldKey`, and why the two are kept apart
24
+ *
25
+ * The renderer carries a second prettifier — `humanizeFieldKey` in
26
+ * `@object-ui/plugin-dashboard`, the single home for the KEY fallback (a field
27
+ * key becomes a column header). It is a DIFFERENT convention, not a stale copy
28
+ * of this one: it additionally splits camelCase.
29
+ *
30
+ * input humanizeFieldKey humanizeLabel (here)
31
+ * ────────────────── ────────────────── ──────────────────
32
+ * needs_analysis Needs Analysis Needs Analysis
33
+ * NeedsAnalysis Needs Analysis NeedsAnalysis <- differ
34
+ * unitPrice Unit Price UnitPrice <- differ
35
+ * BestCase Best Case BestCase <- differ
36
+ * lost-to-competitor Lost-To-Competitor Lost To Competitor <- differ
37
+ *
38
+ * They are fed different inputs, which is why their outputs may differ. A field
39
+ * KEY is authored in the codebase and is expected to carry a machine spelling,
40
+ * so splitting camelCase recovers words its author meant. A stored VALUE is
41
+ * arbitrary tenant data — a picklist member, an object name, a code from
42
+ * another system — where a mid-token capital is not reliably a word boundary,
43
+ * and splitting it rewrites what the tenant wrote (`McDonald` -> `Mc Donald`).
44
+ * Note also that the two conventions do not nest: on the last row above each
45
+ * leaves alone the separator the other rewrites, so "converge" would have to
46
+ * pick a winner per input class rather than adopt one side wholesale.
47
+ *
48
+ * Kept distinct DELIBERATELY, and this paragraph is the write-down whose
49
+ * absence made the split read as drift. Converging them is a decision, not a
50
+ * refactor: this function is read by grid, gantt, detail and charts, so any
51
+ * change to what it RETURNS moves rendered output in four packages at once.
52
+ * That change is explicitly out of scope for the convergence that created this
53
+ * module; it needs its own card.
54
+ *
55
+ * Examples:
56
+ * "in_progress" -> "In Progress"
57
+ * "high-priority" -> "High Priority"
58
+ * "active" -> "Active"
59
+ */
60
+ export function humanizeLabel(value) {
61
+ return value.replace(/[_-]/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
62
+ }
@@ -51,8 +51,33 @@ export type UserActionOverride = boolean | {
51
51
  disabledWhen?: RowCrudPredicates['disabledWhen'];
52
52
  };
53
53
  export interface UserActionsOverride {
54
- create?: boolean;
55
- import?: boolean;
54
+ /**
55
+ * Bare boolean or the object form. Widened from `boolean` in objectui#4646:
56
+ * `@objectstack/spec@17.0.0` types this key
57
+ * `z.union([z.boolean(), RowCrudActionOverrideSchema])`, and the spec's
58
+ * resolver emits the parsed predicates as `CrudAffordances.createPredicates`.
59
+ * The local type said `boolean` — so an author writing the shape the spec
60
+ * accepts, and that the related-list toolbar now honours
61
+ * (`RelatedRecordActionsBridge`, objectui#4646), was rejected by objectui's
62
+ * own type while the runtime handled it.
63
+ */
64
+ create?: UserActionOverride;
65
+ /**
66
+ * Bare boolean or the object form — the SAME union as `create` above, which
67
+ * is how `@objectstack/spec@17.0.0` types the two toolbar keys
68
+ * (`z.union([z.boolean(), RowCrudActionOverrideSchema])` for each), and its
69
+ * resolver emits `CrudAffordances.importPredicates` beside
70
+ * `createPredicates`.
71
+ *
72
+ * Widened in objectui#5142, deliberately IN THE SAME change that gave the
73
+ * object-list toolbar's Import button a consumer for those predicates
74
+ * (`ObjectView`). objectui#4646 left this key narrow on purpose and said why:
75
+ * widening a type ahead of its consumer re-declares the inert-metadata defect
76
+ * — objectui would have *claimed* to accept the object form while still
77
+ * dropping the predicate. Type and consumer travel together, so the
78
+ * declaration stays an honest statement of what this renderer honours.
79
+ */
80
+ import?: UserActionOverride;
56
81
  edit?: UserActionOverride;
57
82
  delete?: UserActionOverride;
58
83
  exportCsv?: boolean;
@@ -125,5 +125,15 @@ export declare function rowHeightToDensityMode(rowHeight: unknown): DensityMode
125
125
  * breaking change with no inventory. Runs AFTER the `fields` → `columns` fold
126
126
  * above, so a view that spells its column list the legacy way still gets its
127
127
  * entries canonicalized.
128
+ * - the four PER-VIEW-TYPE config aliases phase 3 carried over
129
+ * ({@link PER_VIEW_CONFIG_ALIASES}): `kanban.groupField` → `groupByField`,
130
+ * `kanban.cardFields` → `columns`, `gallery.imageField` → `coverField`,
131
+ * `timeline.dateField` → `startDateField`. One read-site changes behaviour
132
+ * as a result, and it is a CORRECTION of the same inverted precedence A2
133
+ * fixed for `densityMode`: `ListView`'s kanban adapter resolves
134
+ * `cardFields || columns`, i.e. legacy over canonical, so a config carrying
135
+ * BOTH used to render the legacy value. After the fold the canonical
136
+ * `columns` reaches it. Every other reader of these four was already
137
+ * canonical-first and is unaffected.
128
138
  */
129
139
  export declare function normalizeListViewSchema<T>(schema: T): T;
@@ -101,6 +101,32 @@ const ARIA_KEY_ALIASES = {
101
101
  label: 'ariaLabel',
102
102
  describedBy: 'ariaDescribedBy',
103
103
  };
104
+ /**
105
+ * Per-view-type config aliases → the spec key each one aliases (#2890, the
106
+ * phase-3 carry-over).
107
+ *
108
+ * Phase 3 derived `kanban` / `gallery` / `timeline` from the spec configs but
109
+ * kept the pre-#2231 objectui spellings declared alongside the spec keys so
110
+ * stored view metadata would keep validating
111
+ * (`packages/types/src/zod/objectql.zod.ts`, each marked `@deprecated legacy
112
+ * alias for the spec's X`). Folding them here is what lets a read-site stop
113
+ * carrying its own `canonical || legacy` pair — the same move A1–A5 made for
114
+ * the top-level vocabulary.
115
+ *
116
+ * `calendar` is deliberately absent: its one local key, `defaultView`, is not
117
+ * an alias of anything. It has no spec counterpart at all, so it wants
118
+ * PROMOTION upstream, not a rename — folding it would delete an authored value
119
+ * with nowhere to put it.
120
+ *
121
+ * Note `kanban.columns` is the spec's "fields shown on each card", NOT the
122
+ * table columns that the same word names at the ListView top level. The fold
123
+ * is scoped to the nested config, so the two never meet.
124
+ */
125
+ const PER_VIEW_CONFIG_ALIASES = {
126
+ kanban: { groupField: 'groupByField', cardFields: 'columns' },
127
+ gallery: { imageField: 'coverField' },
128
+ timeline: { dateField: 'startDateField' },
129
+ };
104
130
  /**
105
131
  * ObjectUI's `list-view` node historically used a different vocabulary from
106
132
  * `@objectstack/spec` for the same concepts (`fields` where the spec says
@@ -164,6 +190,16 @@ const ARIA_KEY_ALIASES = {
164
190
  * breaking change with no inventory. Runs AFTER the `fields` → `columns` fold
165
191
  * above, so a view that spells its column list the legacy way still gets its
166
192
  * entries canonicalized.
193
+ * - the four PER-VIEW-TYPE config aliases phase 3 carried over
194
+ * ({@link PER_VIEW_CONFIG_ALIASES}): `kanban.groupField` → `groupByField`,
195
+ * `kanban.cardFields` → `columns`, `gallery.imageField` → `coverField`,
196
+ * `timeline.dateField` → `startDateField`. One read-site changes behaviour
197
+ * as a result, and it is a CORRECTION of the same inverted precedence A2
198
+ * fixed for `densityMode`: `ListView`'s kanban adapter resolves
199
+ * `cardFields || columns`, i.e. legacy over canonical, so a config carrying
200
+ * BOTH used to render the legacy value. After the fold the canonical
201
+ * `columns` reaches it. Every other reader of these four was already
202
+ * canonical-first and is unaffected.
167
203
  */
168
204
  export function normalizeListViewSchema(schema) {
169
205
  if (!schema || typeof schema !== 'object')
@@ -195,9 +231,22 @@ export function normalizeListViewSchema(schema) {
195
231
  : undefined;
196
232
  const foldedColumns = normalizeColumnIdentities(columnsSource);
197
233
  const foldColumnIdentity = foldedColumns !== columnsSource;
234
+ // Per-view-type config aliases (#2890 phase-3 carry-over). Collected before
235
+ // the early return so an otherwise-canonical view carrying only a nested
236
+ // legacy key still folds — and so a view carrying none still returns by
237
+ // reference.
238
+ const perViewFolds = Object.entries(PER_VIEW_CONFIG_ALIASES)
239
+ .map(([viewKey, aliases]) => {
240
+ const cfg = isRecord(s[viewKey]) ? s[viewKey] : undefined;
241
+ if (!cfg)
242
+ return undefined;
243
+ const legacyKeys = Object.keys(aliases).filter((legacy) => cfg[legacy] !== undefined);
244
+ return legacyKeys.length ? { viewKey, aliases, cfg, legacyKeys } : undefined;
245
+ })
246
+ .filter((entry) => entry !== undefined);
198
247
  if (!foldColumns && !foldRowHeight && !foldFilter && !legacyFlags.length &&
199
248
  !foldDescription && !foldAria && !foldSharing && !defaultViewKind &&
200
- !foldColumnIdentity) {
249
+ !foldColumnIdentity && !perViewFolds.length) {
201
250
  return schema;
202
251
  }
203
252
  const next = { ...s };
@@ -271,6 +320,19 @@ export function normalizeListViewSchema(schema) {
271
320
  delete nextSharing.enabled;
272
321
  next.sharing = nextSharing;
273
322
  }
323
+ for (const { viewKey, aliases, cfg, legacyKeys } of perViewFolds) {
324
+ const nextCfg = { ...cfg };
325
+ for (const legacy of legacyKeys) {
326
+ const canonical = aliases[legacy];
327
+ // Same one-directional shape as every fold above: the canonical key wins
328
+ // when both are present, and the legacy key is REMOVED so a missed
329
+ // read-site fails loudly instead of quietly taking the legacy path.
330
+ if (nextCfg[canonical] === undefined)
331
+ nextCfg[canonical] = cfg[legacy];
332
+ delete nextCfg[legacy];
333
+ }
334
+ next[viewKey] = nextCfg;
335
+ }
274
336
  if (defaultViewKind)
275
337
  next.viewType = 'grid';
276
338
  return next;
@@ -111,9 +111,13 @@ export interface DisplayNumberFormatOptions {
111
111
  * repo's runner, `80.175` percentage points formatted to 2 decimals renders
112
112
  * `80.18%` when formatted directly and `80.17%` after the divide-and-remultiply
113
113
  * — 27,581 of 1,200,013 ordinary-magnitude en-US forms move, plus every
114
- * value at the top of the double range (`MAX_SAFE_INTEGER`, `1e23`). A caller
115
- * holding percentage points must therefore NOT reach for `'percent'`; that is
116
- * exactly the trap this option removes.
114
+ * value at the top of the double range (`MAX_SAFE_INTEGER`, `1e23`). That
115
+ * count is objectui#4576's tie-dense grid — 0.005 steps to 2,000, precisions
116
+ * 0/1/2 — measured on `formatDisplayNumber` / `formatMeasure`'s call shape.
117
+ * objectui#4590 re-measured the same route through `formatPercent` and
118
+ * reports 27,577 of 1,200,003: a different form set, not a correction of this
119
+ * one. A caller holding percentage points must therefore NOT reach for
120
+ * `'percent'`; that is exactly the trap this option removes.
117
121
  *
118
122
  * `'percentPoints'` is implemented with `Intl`'s `style: 'unit'` /
119
123
  * `unit: 'percent'`, which was measured to produce a BYTE-IDENTICAL percent