@object-ui/core 17.3.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 (99) hide show
  1. package/CHANGELOG.md +2018 -0
  2. package/README.md +38 -34
  3. package/dist/actions/ActionEngine.d.ts +43 -26
  4. package/dist/actions/ActionEngine.js +67 -54
  5. package/dist/actions/ActionRunner.d.ts +281 -31
  6. package/dist/actions/ActionRunner.js +458 -36
  7. package/dist/actions/actionKeys.d.ts +163 -27
  8. package/dist/actions/actionKeys.js +238 -27
  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/data-scope/ViewDataProvider.d.ts +37 -12
  17. package/dist/data-scope/ViewDataProvider.js +83 -5
  18. package/dist/data-scope/element-data-source.d.ts +113 -0
  19. package/dist/data-scope/element-data-source.js +216 -0
  20. package/dist/data-scope/index.d.ts +2 -1
  21. package/dist/data-scope/index.js +1 -0
  22. package/dist/evaluator/ExpressionEvaluator.d.ts +42 -1
  23. package/dist/evaluator/ExpressionEvaluator.js +42 -1
  24. package/dist/evaluator/SafeExpressionParser.js +0 -4
  25. package/dist/evaluator/declaredPredicate.d.ts +119 -0
  26. package/dist/evaluator/declaredPredicate.js +152 -0
  27. package/dist/evaluator/fieldRules.d.ts +35 -0
  28. package/dist/evaluator/fieldRules.js +79 -20
  29. package/dist/evaluator/index.d.ts +2 -0
  30. package/dist/evaluator/index.js +2 -0
  31. package/dist/evaluator/listConditional.d.ts +54 -4
  32. package/dist/evaluator/listConditional.js +125 -19
  33. package/dist/evaluator/optionRules.d.ts +55 -0
  34. package/dist/evaluator/optionRules.js +60 -0
  35. package/dist/evaluator/predicateInput.d.ts +51 -3
  36. package/dist/evaluator/predicateInput.js +62 -5
  37. package/dist/evaluator/rowPredicateCanon.d.ts +67 -0
  38. package/dist/evaluator/rowPredicateCanon.js +208 -0
  39. package/dist/index.d.ts +8 -1
  40. package/dist/index.js +31 -1
  41. package/dist/protocols/DndProtocol.d.ts +42 -1
  42. package/dist/protocols/KeyboardProtocol.d.ts +31 -1
  43. package/dist/protocols/SharingProtocol.d.ts +11 -1
  44. package/dist/registry/Registry.d.ts +62 -16
  45. package/dist/theme/ThemeEngine.d.ts +0 -8
  46. package/dist/theme/ThemeEngine.js +22 -103
  47. package/dist/theme/index.d.ts +1 -1
  48. package/dist/theme/index.js +7 -1
  49. package/dist/types/index.d.ts +125 -12
  50. package/dist/utils/chart-presentation.d.ts +205 -0
  51. package/dist/utils/chart-presentation.js +280 -0
  52. package/dist/utils/chart-series.d.ts +390 -23
  53. package/dist/utils/chart-series.js +776 -32
  54. package/dist/utils/column-identity.d.ts +43 -0
  55. package/dist/utils/column-identity.js +47 -1
  56. package/dist/utils/compare-to.d.ts +40 -13
  57. package/dist/utils/compare-to.js +4 -32
  58. package/dist/utils/dashboard-filters.d.ts +54 -8
  59. package/dist/utils/dashboard-filters.js +261 -12
  60. package/dist/utils/dataset-format.d.ts +73 -14
  61. package/dist/utils/dataset-format.js +142 -9
  62. package/dist/utils/dataset-pivot.d.ts +28 -7
  63. package/dist/utils/dataset-pivot.js +27 -6
  64. package/dist/utils/debug.js +7 -0
  65. package/dist/utils/dom-props.d.ts +137 -0
  66. package/dist/utils/dom-props.js +153 -0
  67. package/dist/utils/expand-fields.d.ts +97 -7
  68. package/dist/utils/expand-fields.js +97 -7
  69. package/dist/utils/filter-tokens.js +0 -1
  70. package/dist/utils/freeze-schema.d.ts +38 -1
  71. package/dist/utils/freeze-schema.js +10 -0
  72. package/dist/utils/humanize-label.d.ts +60 -0
  73. package/dist/utils/humanize-label.js +62 -0
  74. package/dist/utils/managedBy.d.ts +27 -2
  75. package/dist/utils/normalize-list-view.d.ts +36 -6
  76. package/dist/utils/normalize-list-view.js +93 -9
  77. package/dist/utils/number-display.d.ts +155 -0
  78. package/dist/utils/number-display.js +71 -0
  79. package/dist/utils/predicate-fields.d.ts +29 -4
  80. package/dist/utils/predicate-fields.js +44 -4
  81. package/dist/utils/resolve-view-id.d.ts +31 -0
  82. package/dist/utils/resolve-view-id.js +45 -0
  83. package/dist/utils/retired-field-types.d.ts +90 -0
  84. package/dist/utils/retired-field-types.js +124 -0
  85. package/dist/utils/sort-query.d.ts +57 -0
  86. package/dist/utils/sort-query.js +35 -0
  87. package/dist/utils/unmaterialized-fields.d.ts +52 -0
  88. package/dist/utils/unmaterialized-fields.js +57 -0
  89. package/dist/validation/index.d.ts +1 -0
  90. package/dist/validation/index.js +1 -0
  91. package/dist/validation/schema-validator.js +105 -0
  92. package/dist/validation/server-owned-value.d.ts +62 -0
  93. package/dist/validation/server-owned-value.js +99 -0
  94. package/dist/validation/validators/object-validation-engine.js +0 -2
  95. package/package.json +5 -5
  96. package/dist/__benchmarks__/core.bench.d.ts +0 -8
  97. package/dist/__benchmarks__/core.bench.js +0 -53
  98. package/dist/utils/merge-views-into-objects.d.ts +0 -19
  99. package/dist/utils/merge-views-into-objects.js +0 -39
@@ -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;
@@ -35,13 +35,33 @@ export declare const DENSITY_MODE_TO_ROW_HEIGHT: Record<DensityMode, RowHeight>;
35
35
  */
36
36
  export declare const ROW_HEIGHT_TO_DENSITY_MODE: Record<RowHeight, DensityMode>;
37
37
  /**
38
- * Tolerant reader for {@link ROW_HEIGHT_TO_DENSITY_MODE}. View metadata is
39
- * user-authored, so `rowHeight` is not guaranteed to be one of the five spec
40
- * values — an unknown one lands on `comfortable` rather than rendering an
41
- * undefined density. Keeping the fallback here means every surface collapses
42
- * the same way.
38
+ * Runtime reader for {@link ROW_HEIGHT_TO_DENSITY_MODE}: the spec's five row
39
+ * heights narrowed onto the renderer's three densities, and **nothing else**.
40
+ *
41
+ * The parameter stays `unknown` because this is the boundary user-authored view
42
+ * metadata actually crosses — `ListViewSchema.rowHeight` is statically a
43
+ * `RowHeight`, but the value arrives from stored view definitions TypeScript
44
+ * never saw (`ObjectView`: `viewDef.rowHeight ?? listSchema.rowHeight`). The
45
+ * type-level half of the guarantee is the `Record<RowHeight, …>` on the table
46
+ * above, which fails the build when the spec grows a row height.
47
+ *
48
+ * An off-spec value gets NO density (objectui#4440). It used to be coerced to
49
+ * `comfortable`, which is the opposite of what `@object-ui/react`'s spec bridge
50
+ * answers for the same string after objectui#4352 — one metadata-driven system
51
+ * holding two answers for one input. AGENTS.md #0.1 decides which one survives:
52
+ * a renderer-side rehabilitation of off-spec metadata is a second de-facto
53
+ * contract, and one strict contract beats N. The producer is where a bad
54
+ * `rowHeight` gets fixed.
55
+ *
56
+ * Callers apply their own "nothing was said" default to `undefined`, so an
57
+ * off-spec row height now renders exactly like an absent one — `'compact'` in
58
+ * both `ListView` and `ObjectGrid`.
59
+ *
60
+ * `hasOwnProperty`, not `in`: `in` walks the prototype chain, so `'toString'`
61
+ * used to come back as `Object.prototype.toString` — a FUNCTION returned from
62
+ * something typed `DensityMode`.
43
63
  */
44
- export declare function rowHeightToDensityMode(rowHeight: unknown): DensityMode;
64
+ export declare function rowHeightToDensityMode(rowHeight: unknown): DensityMode | undefined;
45
65
  /**
46
66
  * ObjectUI's `list-view` node historically used a different vocabulary from
47
67
  * `@objectstack/spec` for the same concepts (`fields` where the spec says
@@ -105,5 +125,15 @@ export declare function rowHeightToDensityMode(rowHeight: unknown): DensityMode;
105
125
  * breaking change with no inventory. Runs AFTER the `fields` → `columns` fold
106
126
  * above, so a view that spells its column list the legacy way still gets its
107
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.
108
138
  */
109
139
  export declare function normalizeListViewSchema<T>(schema: T): T;