@object-ui/core 17.5.0 → 17.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (157) hide show
  1. package/CHANGELOG.md +7404 -0
  2. package/README.md +127 -26
  3. package/dist/actions/ActionEngine.d.ts +5 -3
  4. package/dist/actions/ActionEngine.js +14 -10
  5. package/dist/actions/ActionRunner.d.ts +363 -30
  6. package/dist/actions/ActionRunner.js +544 -58
  7. package/dist/actions/TransactionManager.d.ts +7 -2
  8. package/dist/actions/actionKeys.d.ts +75 -13
  9. package/dist/actions/actionKeys.js +131 -16
  10. package/dist/actions/actionResponse.js +3 -3
  11. package/dist/actions/index.d.ts +2 -0
  12. package/dist/actions/index.js +2 -0
  13. package/dist/actions/recordDelete.d.ts +118 -0
  14. package/dist/actions/recordDelete.js +112 -0
  15. package/dist/actions/recordIdParam.d.ts +77 -0
  16. package/dist/actions/recordIdParam.js +37 -0
  17. package/dist/actions/serverActionHandler.d.ts +1 -1
  18. package/dist/actions/serverActionHandler.js +1 -1
  19. package/dist/adapters/ValueDataSource.d.ts +6 -1
  20. package/dist/adapters/ValueDataSource.js +1090 -98
  21. package/dist/adapters/batchTransaction.d.ts +1 -1
  22. package/dist/adapters/batchTransaction.js +1 -1
  23. package/dist/builder/schema-builder.d.ts +38 -81
  24. package/dist/builder/schema-builder.js +33 -152
  25. package/dist/data-scope/ViewDataProvider.d.ts +1 -1
  26. package/dist/data-scope/ViewDataProvider.js +21 -1
  27. package/dist/data-scope/element-data-source.d.ts +261 -7
  28. package/dist/data-scope/element-data-source.js +342 -52
  29. package/dist/data-scope/index.d.ts +9 -4
  30. package/dist/data-scope/index.js +9 -4
  31. package/dist/evaluator/ExpressionEvaluator.d.ts +128 -1
  32. package/dist/evaluator/ExpressionEvaluator.js +226 -13
  33. package/dist/evaluator/FormulaFunctions.d.ts +0 -10
  34. package/dist/evaluator/FormulaFunctions.js +166 -27
  35. package/dist/evaluator/SafeExpressionParser.js +0 -4
  36. package/dist/evaluator/declaredPredicate.d.ts +93 -9
  37. package/dist/evaluator/declaredPredicate.js +118 -17
  38. package/dist/evaluator/fieldRules.d.ts +107 -4
  39. package/dist/evaluator/fieldRules.js +231 -35
  40. package/dist/evaluator/index.d.ts +2 -0
  41. package/dist/evaluator/index.js +2 -0
  42. package/dist/evaluator/listConditional.d.ts +125 -9
  43. package/dist/evaluator/listConditional.js +167 -26
  44. package/dist/evaluator/optionRules.d.ts +126 -0
  45. package/dist/evaluator/optionRules.js +141 -4
  46. package/dist/evaluator/predicateInput.d.ts +15 -4
  47. package/dist/evaluator/predicateInput.js +35 -2
  48. package/dist/evaluator/rowPredicateCanon.d.ts +56 -0
  49. package/dist/evaluator/rowPredicateCanon.js +158 -0
  50. package/dist/evaluator/subjectPermissions.d.ts +76 -0
  51. package/dist/evaluator/subjectPermissions.js +44 -0
  52. package/dist/evaluator/unevaluablePredicate.d.ts +57 -0
  53. package/dist/evaluator/unevaluablePredicate.js +70 -0
  54. package/dist/index.d.ts +18 -0
  55. package/dist/index.js +97 -2
  56. package/dist/protocols/index.d.ts +0 -1
  57. package/dist/protocols/index.js +9 -1
  58. package/dist/registry/Registry.d.ts +397 -48
  59. package/dist/registry/Registry.js +263 -11
  60. package/dist/registry/WidgetRegistry.js +7 -3
  61. package/dist/registry/html-tier-intrinsics.d.ts +90 -0
  62. package/dist/registry/html-tier-intrinsics.js +110 -0
  63. package/dist/registry/public-blocks.d.ts +8 -0
  64. package/dist/registry/public-blocks.js +57 -0
  65. package/dist/theme/ThemeEngine.js +1 -1
  66. package/dist/theme/index.d.ts +1 -1
  67. package/dist/theme/index.js +1 -1
  68. package/dist/types/index.d.ts +92 -13
  69. package/dist/utils/chart-category-key.d.ts +82 -0
  70. package/dist/utils/chart-category-key.js +84 -0
  71. package/dist/utils/chart-measure-key.d.ts +64 -0
  72. package/dist/utils/chart-measure-key.js +65 -0
  73. package/dist/utils/chart-presentation.d.ts +188 -0
  74. package/dist/utils/chart-presentation.js +345 -0
  75. package/dist/utils/chart-series.d.ts +271 -31
  76. package/dist/utils/chart-series.js +478 -55
  77. package/dist/utils/column-identity.d.ts +43 -0
  78. package/dist/utils/column-identity.js +47 -1
  79. package/dist/utils/column-sortability.d.ts +157 -0
  80. package/dist/utils/column-sortability.js +217 -0
  81. package/dist/utils/dashboard-filters.d.ts +38 -6
  82. package/dist/utils/dashboard-filters.js +107 -61
  83. package/dist/utils/dataset-format.d.ts +211 -30
  84. package/dist/utils/dataset-format.js +407 -52
  85. package/dist/utils/date-display.d.ts +354 -0
  86. package/dist/utils/date-display.js +463 -0
  87. package/dist/utils/date-macros.js +7 -0
  88. package/dist/utils/debug.js +7 -0
  89. package/dist/utils/drill-down.d.ts +78 -0
  90. package/dist/utils/drill-down.js +93 -0
  91. package/dist/utils/emptiness.d.ts +96 -0
  92. package/dist/utils/emptiness.js +101 -0
  93. package/dist/utils/expand-fields.d.ts +171 -11
  94. package/dist/utils/expand-fields.js +171 -11
  95. package/dist/utils/extract-records.d.ts +65 -5
  96. package/dist/utils/extract-records.js +65 -8
  97. package/dist/utils/filter-converter.d.ts +152 -4
  98. package/dist/utils/filter-converter.js +1092 -22
  99. package/dist/utils/filter-tokens.d.ts +49 -5
  100. package/dist/utils/filter-tokens.js +105 -34
  101. package/dist/utils/freeze-schema.d.ts +38 -1
  102. package/dist/utils/freeze-schema.js +10 -0
  103. package/dist/utils/grouping-fields.d.ts +61 -0
  104. package/dist/utils/grouping-fields.js +84 -0
  105. package/dist/utils/humanize-label.d.ts +60 -0
  106. package/dist/utils/humanize-label.js +62 -0
  107. package/dist/utils/managedBy.d.ts +30 -5
  108. package/dist/utils/managedBy.js +5 -5
  109. package/dist/utils/native-date-value.d.ts +48 -0
  110. package/dist/utils/native-date-value.js +167 -0
  111. package/dist/utils/non-grid-row-ceiling.d.ts +143 -0
  112. package/dist/utils/non-grid-row-ceiling.js +141 -0
  113. package/dist/utils/normalize-list-view.d.ts +131 -1
  114. package/dist/utils/normalize-list-view.js +248 -6
  115. package/dist/utils/number-display.d.ts +70 -17
  116. package/dist/utils/number-display.js +34 -12
  117. package/dist/utils/object-aggregate-query.d.ts +113 -0
  118. package/dist/utils/object-aggregate-query.js +50 -0
  119. package/dist/utils/parent-scope.d.ts +123 -0
  120. package/dist/utils/parent-scope.js +140 -0
  121. package/dist/utils/predicate-fields.d.ts +62 -4
  122. package/dist/utils/predicate-fields.js +169 -4
  123. package/dist/utils/record-color.d.ts +75 -0
  124. package/dist/utils/record-color.js +65 -0
  125. package/dist/utils/record-source.d.ts +298 -0
  126. package/dist/utils/record-source.js +331 -0
  127. package/dist/utils/record-title.d.ts +148 -8
  128. package/dist/utils/record-title.js +220 -26
  129. package/dist/utils/reference-keys.d.ts +21 -22
  130. package/dist/utils/reference-keys.js +532 -29
  131. package/dist/utils/retired-field-types.d.ts +90 -0
  132. package/dist/utils/retired-field-types.js +124 -0
  133. package/dist/utils/sort-query.d.ts +128 -26
  134. package/dist/utils/sort-query.js +240 -13
  135. package/dist/utils/text-comparand.d.ts +92 -0
  136. package/dist/utils/text-comparand.js +99 -0
  137. package/dist/utils/unmaterialized-fields.d.ts +52 -0
  138. package/dist/utils/unmaterialized-fields.js +57 -0
  139. package/dist/utils/without-denied-fields.d.ts +49 -0
  140. package/dist/utils/without-denied-fields.js +52 -0
  141. package/dist/validation/index.d.ts +9 -4
  142. package/dist/validation/index.js +9 -4
  143. package/dist/validation/schema-validator.d.ts +1 -1
  144. package/dist/validation/schema-validator.js +305 -82
  145. package/dist/validation/server-owned-value.d.ts +62 -0
  146. package/dist/validation/server-owned-value.js +99 -0
  147. package/dist/validation/validators/object-validation-engine.d.ts +1 -1
  148. package/dist/validation/validators/object-validation-engine.js +14 -5
  149. package/package.json +8 -9
  150. package/dist/__benchmarks__/core.bench.d.ts +0 -8
  151. package/dist/__benchmarks__/core.bench.js +0 -53
  152. package/dist/data-scope/DataScopeManager.d.ts +0 -127
  153. package/dist/data-scope/DataScopeManager.js +0 -229
  154. package/dist/protocols/ResponsiveProtocol.d.ts +0 -73
  155. package/dist/protocols/ResponsiveProtocol.js +0 -158
  156. package/dist/validation/validation-engine.d.ts +0 -88
  157. package/dist/validation/validation-engine.js +0 -434
@@ -19,7 +19,7 @@
19
19
  *
20
20
  * What is genuinely objectui's and keeps a local name:
21
21
  * {@link resolveEffectiveCrudAffordances} — the spec's object-level matrix
22
- * INTERSECTED with the server-resolved effective API operation set (#3391), so
22
+ * INTERSECTED with the server-resolved effective API operation set (objectstack#3391), so
23
23
  * the UI never offers a button the server would 405. The spec's function has
24
24
  * no such notion; naming ours after it was the misdescription this burn-down
25
25
  * removes.
@@ -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;
@@ -100,7 +125,7 @@ export declare function userActionPredicates(v: UserActionOverride | undefined |
100
125
  *
101
126
  * @param obj The object schema (managedBy bucket + userActions overrides).
102
127
  * @param effectiveApiOperations OPTIONAL server-resolved effective API operation
103
- * set for this object (from `/me/permissions` `apiOperations`, #3391). When
128
+ * set for this object (from `/me/permissions` `apiOperations`, objectstack#3391). When
104
129
  * provided, each affordance bit is ANDed with the corresponding API operation
105
130
  * (create/import→create/import, edit→update, delete→delete, exportCsv→export),
106
131
  * so the UI never shows a button the server would 405. Passing `undefined`
@@ -138,7 +163,7 @@ export declare function isSystemWritable(obj: SchemaLike | null | undefined): bo
138
163
  * object-level editability, so only the resolved boolean matters here.)
139
164
  *
140
165
  * @param effectiveApiOperations OPTIONAL server-resolved effective API operation
141
- * set for this object (`/me/permissions` `apiOperations`, #3391/#3546). When
166
+ * set for this object (`/me/permissions` `apiOperations`, objectstack#3391/objectstack#3546). When
142
167
  * provided, inline-edit is additionally ANDed with the server allowing
143
168
  * `update`, so a record page never offers double-click/pencil editing the
144
169
  * server would 405. `undefined` (old backend / unrestricted) leaves the
@@ -19,7 +19,7 @@
19
19
  *
20
20
  * What is genuinely objectui's and keeps a local name:
21
21
  * {@link resolveEffectiveCrudAffordances} — the spec's object-level matrix
22
- * INTERSECTED with the server-resolved effective API operation set (#3391), so
22
+ * INTERSECTED with the server-resolved effective API operation set (objectstack#3391), so
23
23
  * the UI never offers a button the server would 405. The spec's function has
24
24
  * no such notion; naming ours after it was the misdescription this burn-down
25
25
  * removes.
@@ -73,7 +73,7 @@ export function userActionPredicates(v) {
73
73
  }
74
74
  /**
75
75
  * The affordance-bit → server API-operation mapping used to intersect the
76
- * UI-intent affordances with the server's effective API operation set (#3391).
76
+ * UI-intent affordances with the server's effective API operation set (objectstack#3391).
77
77
  * This is the objectui end of the framework's `API_METHOD_DERIVATION` contract:
78
78
  * the button predicate is `affordance(op) ∧ effective.api(op)`.
79
79
  */
@@ -97,7 +97,7 @@ const AFFORDANCE_TO_API_OPERATION = {
97
97
  *
98
98
  * @param obj The object schema (managedBy bucket + userActions overrides).
99
99
  * @param effectiveApiOperations OPTIONAL server-resolved effective API operation
100
- * set for this object (from `/me/permissions` `apiOperations`, #3391). When
100
+ * set for this object (from `/me/permissions` `apiOperations`, objectstack#3391). When
101
101
  * provided, each affordance bit is ANDed with the corresponding API operation
102
102
  * (create/import→create/import, edit→update, delete→delete, exportCsv→export),
103
103
  * so the UI never shows a button the server would 405. Passing `undefined`
@@ -113,7 +113,7 @@ export function resolveEffectiveCrudAffordances(obj, effectiveApiOperations) {
113
113
  managedBy: obj?.managedBy ?? undefined,
114
114
  userActions: (obj?.userActions ?? undefined),
115
115
  });
116
- // [#3391] Intersect with the server's effective API operations when present.
116
+ // [objectstack#3391] Intersect with the server's effective API operations when present.
117
117
  // The frontend consumes the effective set the server hands down; it never
118
118
  // reads the raw `apiMethods` nor re-derives.
119
119
  if (Array.isArray(effectiveApiOperations)) {
@@ -159,7 +159,7 @@ export function isSystemWritable(obj) {
159
159
  * object-level editability, so only the resolved boolean matters here.)
160
160
  *
161
161
  * @param effectiveApiOperations OPTIONAL server-resolved effective API operation
162
- * set for this object (`/me/permissions` `apiOperations`, #3391/#3546). When
162
+ * set for this object (`/me/permissions` `apiOperations`, objectstack#3391/objectstack#3546). When
163
163
  * provided, inline-edit is additionally ANDed with the server allowing
164
164
  * `update`, so a record page never offers double-click/pencil editing the
165
165
  * server would 405. `undefined` (old backend / unrestricted) leaves the
@@ -0,0 +1,48 @@
1
+ /**
2
+ * ObjectUI
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
+ * Stored value → `<input type="date">` value (`YYYY-MM-DD`).
10
+ *
11
+ * A leading `YYYY-MM-DD` is passed through VERBATIM rather than re-parsed: a
12
+ * date-only string is parsed as UTC midnight by `new Date()`, so reading local
13
+ * calendar components back out of it shifts the day by one everywhere west of
14
+ * Greenwich. `2026-06-17` must stay June 17th in every zone.
15
+ */
16
+ export declare function toDateInputValue(value: unknown): string;
17
+ /**
18
+ * Stored value → `<input type="datetime-local">` value (`YYYY-MM-DDTHH:mm`),
19
+ * expressed in the viewer's local wall clock — the same instant the detail
20
+ * page shows, so opening the editor never appears to move the time.
21
+ *
22
+ * A value that is ALREADY a zone-less local datetime is truncated to minutes
23
+ * in place instead of being round-tripped through `Date`, which would
24
+ * reinterpret it and then re-render it, twice risking an off-by-a-zone shift.
25
+ */
26
+ export declare function toDateTimeInputValue(value: unknown): string;
27
+ /**
28
+ * `<input type="datetime-local">` value → stored value (ISO-8601, UTC).
29
+ *
30
+ * The control hands us a zone-less local wall clock; `new Date()` reads that
31
+ * form as local time, which is the inverse of what `toDateTimeInputValue`
32
+ * wrote — the pair is what keeps read and write on one basis.
33
+ *
34
+ * An unparseable string is returned untouched rather than blanked: dropping a
35
+ * value the user can see in the box would be a data loss dressed up as a fix.
36
+ */
37
+ export declare function fromDateTimeInputValue(value: string): string;
38
+ /**
39
+ * True when a stored value is written on a calendar day that does not exist
40
+ * (`2026-02-30`, `2026-02-30T10:00:00Z`, `2026-02-30T10:00+08:00`) — the day
41
+ * read AS WRITTEN from the leading `YYYY-MM-DD`, by the one judgement the read
42
+ * faces use (`isRealCalendarDate`). A parsed `Date` always names a real day,
43
+ * so the check can only be made before any conversion.
44
+ *
45
+ * Anything without a leading `YYYY-MM-DD` (a `Date`, a number, free text) is
46
+ * not this case and answers `false`.
47
+ */
48
+ export declare function isImpossibleStoredDay(value: unknown): boolean;
@@ -0,0 +1,167 @@
1
+ /**
2
+ * ObjectUI
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
+ * native-date-value - the ONE adapter set between a stored date / datetime
10
+ * value and the native date/time controls (objectui#3127).
11
+ *
12
+ * ## Why it lives in `core` (objectui#10625)
13
+ *
14
+ * It was written in `@object-ui/fields` (`widgets/nativeDateValue.ts`), and
15
+ * `@object-ui/components`' data table could not import it from there:
16
+ * `fields` depends on `components`, so the reverse import is a package cycle.
17
+ * The table therefore kept private copies with no impossible-day check, and a
18
+ * stored `2026-02-30T10:00:00Z` showed as March 2nd in its inline editor and
19
+ * could be written back as that day. The functions move DOWN into the
20
+ * React-free engine, next to the judgement they share (`isRealCalendarDate`
21
+ * in `date-display.ts`), and `fields` re-exports them unchanged: one home,
22
+ * nothing to drift. `date-display.ts`'s header is the same remedy applied to
23
+ * the display side.
24
+ *
25
+ * ## The defect these close
26
+ *
27
+ * `<input type="date">` accepts exactly `YYYY-MM-DD` and
28
+ * `<input type="datetime-local">` exactly `YYYY-MM-DDTHH:mm[:ss]` — a **local
29
+ * wall clock with no zone suffix**. Anything else is not "coerced" or
30
+ * "warned about": the browser SILENTLY rejects it. The attribute still lands
31
+ * in the DOM, so the value looks present to anyone inspecting markup, while
32
+ * `input.value` reads back `""` and the control paints its empty placeholder:
33
+ *
34
+ * ```js
35
+ * el.getAttribute('value') // "2026-08-16T00:33:00.000Z" ← the record's value
36
+ * el.value // "" ← what the user sees
37
+ * ```
38
+ *
39
+ * Records come back from the API as full ISO-8601 (`…T00:33:00.000Z`) — which
40
+ * is precisely one of those rejected shapes. So EVERY datetime field in an
41
+ * edit dialog rendered blank while holding a real value, and users read that
42
+ * as "the date was lost" and retyped it (which is the only way the display bug
43
+ * could turn into a data change).
44
+ *
45
+ * ## Why a round trip, and not just a formatter
46
+ *
47
+ * Formatting on the way IN without converting on the way OUT would be the
48
+ * worse bug: the control would then be read as UTC but written back as a naive
49
+ * local string, so a user in UTC+8 who picked "08:33" would store 08:33Z and
50
+ * see 16:33 on the detail page. Read basis and write basis must be the same
51
+ * one, so `toDateTimeInputValue` / `fromDateTimeInputValue` are a pair and are
52
+ * used as a pair.
53
+ *
54
+ * ## Shape kept in form state
55
+ *
56
+ * `datetime` round-trips through ISO — the shape the API hands us and the one
57
+ * display/format code already assumes. `date` stays a plain `YYYY-MM-DD`
58
+ * string. Untouched fields are never re-emitted, so a form that opens and
59
+ * closes without an edit still submits nothing for them.
60
+ *
61
+ * ## A stored day that does not exist (objectui#10474)
62
+ *
63
+ * The engine's date parse accepts a day of `01`-`31` for every month and rolls
64
+ * the surplus forward, so `new Date('2026-02-30T10:00:00Z')` is March 2nd. The
65
+ * READ faces refuse such a value through the shared judgement
66
+ * (`isRealCalendarDate`, objectui#10026 / objectui#10301); this pair used to
67
+ * reach the engine first, so the editor showed `2026-03-02T10:00` for it and a
68
+ * save could write that day back. `isImpossibleStoredDay` asks the same
69
+ * judgement of the day AS WRITTEN, before any `new Date(...)`, in every
70
+ * spelling (`Z`, offset, zone-less, date-only).
71
+ *
72
+ * The control cannot show the stored string instead: measured in Chromium, a
73
+ * `datetime-local` (and a `date`) sanitises a nonexistent day to `""`, in the
74
+ * zone-less spelling too. So `toDateTimeInputValue` hands the control `""` and
75
+ * the WIDGET names the stored string beside it with an invalid marker — an
76
+ * empty control alone would be objectui#3127's silent blank again. Nothing is
77
+ * written until the user picks a new value.
78
+ */
79
+ import { isRealCalendarDate } from './date-display.js';
80
+ function pad2(n) {
81
+ return String(n).padStart(2, '0');
82
+ }
83
+ /**
84
+ * Stored value → `<input type="date">` value (`YYYY-MM-DD`).
85
+ *
86
+ * A leading `YYYY-MM-DD` is passed through VERBATIM rather than re-parsed: a
87
+ * date-only string is parsed as UTC midnight by `new Date()`, so reading local
88
+ * calendar components back out of it shifts the day by one everywhere west of
89
+ * Greenwich. `2026-06-17` must stay June 17th in every zone.
90
+ */
91
+ export function toDateInputValue(value) {
92
+ if (value == null || value === '')
93
+ return '';
94
+ if (typeof value === 'string') {
95
+ const m = value.match(/^(\d{4}-\d{2}-\d{2})/);
96
+ if (m)
97
+ return m[1];
98
+ }
99
+ const d = value instanceof Date ? value : new Date(String(value));
100
+ if (Number.isNaN(d.getTime()))
101
+ return '';
102
+ return `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())}`;
103
+ }
104
+ /**
105
+ * Stored value → `<input type="datetime-local">` value (`YYYY-MM-DDTHH:mm`),
106
+ * expressed in the viewer's local wall clock — the same instant the detail
107
+ * page shows, so opening the editor never appears to move the time.
108
+ *
109
+ * A value that is ALREADY a zone-less local datetime is truncated to minutes
110
+ * in place instead of being round-tripped through `Date`, which would
111
+ * reinterpret it and then re-render it, twice risking an off-by-a-zone shift.
112
+ */
113
+ export function toDateTimeInputValue(value) {
114
+ if (value == null || value === '')
115
+ return '';
116
+ // Judged before any `new Date(...)`, which would roll it (objectui#10474).
117
+ if (isImpossibleStoredDay(value))
118
+ return '';
119
+ if (typeof value === 'string') {
120
+ const local = value.match(/^(\d{4}-\d{2}-\d{2})T(\d{2}:\d{2})(?::\d{2}(?:\.\d+)?)?$/);
121
+ if (local)
122
+ return `${local[1]}T${local[2]}`;
123
+ }
124
+ const d = value instanceof Date ? value : new Date(String(value));
125
+ if (Number.isNaN(d.getTime()))
126
+ return '';
127
+ return (`${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())}` +
128
+ `T${pad2(d.getHours())}:${pad2(d.getMinutes())}`);
129
+ }
130
+ /**
131
+ * `<input type="datetime-local">` value → stored value (ISO-8601, UTC).
132
+ *
133
+ * The control hands us a zone-less local wall clock; `new Date()` reads that
134
+ * form as local time, which is the inverse of what `toDateTimeInputValue`
135
+ * wrote — the pair is what keeps read and write on one basis.
136
+ *
137
+ * An unparseable string is returned untouched rather than blanked: dropping a
138
+ * value the user can see in the box would be a data loss dressed up as a fix.
139
+ */
140
+ export function fromDateTimeInputValue(value) {
141
+ if (!value)
142
+ return '';
143
+ // A nonexistent day is returned untouched, like an unparseable string: the
144
+ // engine would re-emit it as the rolled day (objectui#10474).
145
+ if (isImpossibleStoredDay(value))
146
+ return value;
147
+ const d = new Date(value);
148
+ if (Number.isNaN(d.getTime()))
149
+ return value;
150
+ return d.toISOString();
151
+ }
152
+ /**
153
+ * True when a stored value is written on a calendar day that does not exist
154
+ * (`2026-02-30`, `2026-02-30T10:00:00Z`, `2026-02-30T10:00+08:00`) — the day
155
+ * read AS WRITTEN from the leading `YYYY-MM-DD`, by the one judgement the read
156
+ * faces use (`isRealCalendarDate`). A parsed `Date` always names a real day,
157
+ * so the check can only be made before any conversion.
158
+ *
159
+ * Anything without a leading `YYYY-MM-DD` (a `Date`, a number, free text) is
160
+ * not this case and answers `false`.
161
+ */
162
+ export function isImpossibleStoredDay(value) {
163
+ if (typeof value !== 'string')
164
+ return false;
165
+ const m = value.match(/^(\d{4}-\d{2}-\d{2})(?:$|T)/);
166
+ return m != null && !isRealCalendarDate(m[1]);
167
+ }
@@ -0,0 +1,143 @@
1
+ /**
2
+ * ObjectUI
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 platform's hard row ceiling for a NON-GRID visualisation — gantt,
10
+ * calendar, map and tree (objectui#7210, maintainer ruling a′, 2026-09-02).
11
+ *
12
+ * ## Why this lives in `@object-ui/core` (objectui#7508, ruling A′)
13
+ *
14
+ * The four views live in four packages, so whatever they share is public by
15
+ * construction: every package here has a single entry and no subpath export.
16
+ * The non-React half of the mechanism — this constant, the query it sends and
17
+ * {@link applyNonGridRowCeiling} — wraps this package's own
18
+ * `extractRecords`, and every one of the four already imports core helpers,
19
+ * so this is its home. `@object-ui/react` re-exports the three names it
20
+ * carried before, and keeps only the React half (`NonGridRowCeilingNote`).
21
+ *
22
+ * ## What was ruled, and what was rejected
23
+ *
24
+ * A non-grid visualisation may fetch the whole FILTERED result set, because a
25
+ * truthful range or layout needs all of it: a gantt cannot compute
26
+ * `min(start) → max(end)` from one page, and a map cannot fit a camera to
27
+ * markers it never received. What it may not do is fetch it UNBOUNDED — at
28
+ * 100k scheduled rows that is the whole table into the browser, with no knob
29
+ * reachable from view metadata, since these four requests never carried a
30
+ * `$top` for `pagination.pageSize` to set.
31
+ *
32
+ * So the fetch carries a ceiling, and the ceiling is a NAMED CONSTANT IN THE
33
+ * RENDERER — not an authorable view key. Three alternatives were considered
34
+ * and are not what landed:
35
+ *
36
+ * - a documentation note only — still the whole table into the browser;
37
+ * - truncate at `pagination.pageSize` — SILENT truncation, and it caps a
38
+ * complete schedule at one page;
39
+ * - an authorable `maxRows` key — a new permanent key every author must set.
40
+ *
41
+ * ⛔ The direction the ruling names as dangerous is SILENT truncation: a
42
+ * cut-off schedule still looks like a schedule, a cut-off map still looks like
43
+ * a map. Nothing on screen distinguishes "these are all the records" from
44
+ * "these are the first 2,000 of 40,000" unless the view says so. That is why
45
+ * `NonGridRowCeilingNote` (`@object-ui/react`) is not decoration — crossing
46
+ * this ceiling quietly is the defect, and the note is the fix.
47
+ *
48
+ * ## Why 2,000, measured
49
+ *
50
+ * The ruling sets ONE constant across the four views, chosen after measuring
51
+ * them, so the binding view decides it. Measured in this repo's jsdom lane
52
+ * (`@testing-library/react`, real child views, inline `value` provider, mount
53
+ * to settled paint), DOM elements materialised and mount duration:
54
+ *
55
+ * | rows | gantt | calendar | map | tree |
56
+ * |------:|-----------:|----------:|---------------:|----------------:|
57
+ * | 250 | 726 · 314ms | 415 · 231ms | 478 · 152ms | 1,306 · 326ms |
58
+ * | 1,000 | 726 · 226ms | 415 · 235ms | 1,512 · 299ms | 5,206 · 1,025ms |
59
+ * | 2,000 | 726 · 484ms | 415 · 237ms | 2,770 · 1,250ms | 10,406 · 2,720ms |
60
+ * | 4,000 | 726 · 420ms | 415 · 211ms | 3,020 · 648ms | 20,806 · 3,105ms |
61
+ * | 8,000 | — | — | — | 41,606 · 7,597ms |
62
+ *
63
+ * Three of the four hold their DOM flat as rows grow, and for structural
64
+ * reasons that will not change: the gantt VIRTUALISES its task list and
65
+ * timeline window, the calendar month grid draws at most four events per day
66
+ * cell, and the map auto-clusters above 100 markers. Their cost in rows is the
67
+ * O(n) transform, not the DOM.
68
+ *
69
+ * `ObjectTree` is the outlier and therefore the constraint: it flattens every
70
+ * expanded node into the document, measured at a strictly linear **5.2 DOM
71
+ * elements per record**, with no virtualisation anywhere on that path.
72
+ *
73
+ * The budget applied to that: keep the WORST of the four inside ~10,000 DOM
74
+ * elements — an order of magnitude above Lighthouse's ~1,400-element
75
+ * "excessive DOM size" warning, and the last point at which the tree's mount
76
+ * stays under ~3s in an environment that does no layout and no paint at all.
77
+ * 2,000 rows is where that lands, measured rather than interpolated: 10,406
78
+ * elements. It is also ~10x the real application result set this card was
79
+ * filed from (186 rows of `duly_task`), so a genuine working view is nowhere
80
+ * near it, which is the property that keeps the note meaningful when it does
81
+ * appear.
82
+ *
83
+ * ⚠️ These are SHARED-BOX jsdom seconds, not browser wall clock, and jsdom
84
+ * does no layout or paint. The DOM-element counts are the environment-
85
+ * independent half and the reason the ratio between the four views is the
86
+ * load-bearing part of the reading, not the milliseconds. This table is a
87
+ * reading taken once, when objectui#7210 landed; nothing re-derives it.
88
+ */
89
+ export declare const NON_GRID_ROW_CEILING = 2000;
90
+ /**
91
+ * The query keys a non-grid view sends to honour {@link NON_GRID_ROW_CEILING}:
92
+ * `$top` is the ceiling plus ONE probe row.
93
+ *
94
+ * This is the ONE place the `+ 1` is written (objectui#7508, ruling A′). It
95
+ * used to be a published constant beside the ceiling, which left the
96
+ * mechanism split in two — `$top` at each call site, the slice in
97
+ * {@link applyNonGridRowCeiling} — and gave every caller a number to get
98
+ * wrong. A caller now spreads this into its `find()` query and never spells
99
+ * the probe row itself.
100
+ *
101
+ * The `+ 1` is what makes truncation DETECTABLE, and a site that lost it would
102
+ * look correct and report nothing: with exactly `$top: NON_GRID_ROW_CEILING`,
103
+ * a result set of exactly 2,000 and one of 200,000 both come back as 2,000
104
+ * rows, and the only thing separating them is a `total` the adapter is not
105
+ * obliged to send (`QueryResult.total` is optional, and an adapter answering
106
+ * with a bare array carries none at all). One extra row makes the distinction
107
+ * a fact about the rows in hand.
108
+ *
109
+ * A fresh object per call, so a caller that mutates the query it built cannot
110
+ * change what the next caller sends.
111
+ */
112
+ export declare function nonGridRowCeilingQuery(): {
113
+ $top: number;
114
+ };
115
+ /** What {@link applyNonGridRowCeiling} tells a caller about its result set. */
116
+ export interface NonGridCeilingResult<T = any> {
117
+ /** The rows to draw — never more than {@link NON_GRID_ROW_CEILING}. */
118
+ rows: T[];
119
+ /**
120
+ * The size of the whole filtered result set when the adapter reported one
121
+ * (`QueryResult.total`), otherwise `undefined`. `undefined` is NOT "not
122
+ * truncated" — see {@link NonGridCeilingResult.truncated}, which is decided
123
+ * by the rows in hand and never by this.
124
+ */
125
+ total?: number;
126
+ /** `true` when the source had more rows than the ceiling allows drawn. */
127
+ truncated: boolean;
128
+ }
129
+ /**
130
+ * Cap a non-grid view's result set at {@link NON_GRID_ROW_CEILING} and report
131
+ * whether it had to.
132
+ *
133
+ * Truncation is decided by the PROBE ROW (`rows.length > NON_GRID_ROW_CEILING`
134
+ * against a query built by {@link nonGridRowCeilingQuery}), never by comparing
135
+ * against `total`: `total` is optional on `QueryResult` and absent entirely
136
+ * from a bare-array response, so a `total`-based test would silently stop
137
+ * reporting on exactly the adapters least likely to be paging correctly.
138
+ *
139
+ * The rows are read by `extractRecords`, so this sink accepts exactly the
140
+ * envelopes that helper does (objectui#6839) — pinned in
141
+ * `non-grid-row-ceiling.contractEnvelope-6839.test.ts`.
142
+ */
143
+ export declare function applyNonGridRowCeiling<T = any>(result: unknown): NonGridCeilingResult<T>;
@@ -0,0 +1,141 @@
1
+ /**
2
+ * ObjectUI
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
+ import { extractRecords } from './extract-records.js';
9
+ /**
10
+ * The platform's hard row ceiling for a NON-GRID visualisation — gantt,
11
+ * calendar, map and tree (objectui#7210, maintainer ruling a′, 2026-09-02).
12
+ *
13
+ * ## Why this lives in `@object-ui/core` (objectui#7508, ruling A′)
14
+ *
15
+ * The four views live in four packages, so whatever they share is public by
16
+ * construction: every package here has a single entry and no subpath export.
17
+ * The non-React half of the mechanism — this constant, the query it sends and
18
+ * {@link applyNonGridRowCeiling} — wraps this package's own
19
+ * `extractRecords`, and every one of the four already imports core helpers,
20
+ * so this is its home. `@object-ui/react` re-exports the three names it
21
+ * carried before, and keeps only the React half (`NonGridRowCeilingNote`).
22
+ *
23
+ * ## What was ruled, and what was rejected
24
+ *
25
+ * A non-grid visualisation may fetch the whole FILTERED result set, because a
26
+ * truthful range or layout needs all of it: a gantt cannot compute
27
+ * `min(start) → max(end)` from one page, and a map cannot fit a camera to
28
+ * markers it never received. What it may not do is fetch it UNBOUNDED — at
29
+ * 100k scheduled rows that is the whole table into the browser, with no knob
30
+ * reachable from view metadata, since these four requests never carried a
31
+ * `$top` for `pagination.pageSize` to set.
32
+ *
33
+ * So the fetch carries a ceiling, and the ceiling is a NAMED CONSTANT IN THE
34
+ * RENDERER — not an authorable view key. Three alternatives were considered
35
+ * and are not what landed:
36
+ *
37
+ * - a documentation note only — still the whole table into the browser;
38
+ * - truncate at `pagination.pageSize` — SILENT truncation, and it caps a
39
+ * complete schedule at one page;
40
+ * - an authorable `maxRows` key — a new permanent key every author must set.
41
+ *
42
+ * ⛔ The direction the ruling names as dangerous is SILENT truncation: a
43
+ * cut-off schedule still looks like a schedule, a cut-off map still looks like
44
+ * a map. Nothing on screen distinguishes "these are all the records" from
45
+ * "these are the first 2,000 of 40,000" unless the view says so. That is why
46
+ * `NonGridRowCeilingNote` (`@object-ui/react`) is not decoration — crossing
47
+ * this ceiling quietly is the defect, and the note is the fix.
48
+ *
49
+ * ## Why 2,000, measured
50
+ *
51
+ * The ruling sets ONE constant across the four views, chosen after measuring
52
+ * them, so the binding view decides it. Measured in this repo's jsdom lane
53
+ * (`@testing-library/react`, real child views, inline `value` provider, mount
54
+ * to settled paint), DOM elements materialised and mount duration:
55
+ *
56
+ * | rows | gantt | calendar | map | tree |
57
+ * |------:|-----------:|----------:|---------------:|----------------:|
58
+ * | 250 | 726 · 314ms | 415 · 231ms | 478 · 152ms | 1,306 · 326ms |
59
+ * | 1,000 | 726 · 226ms | 415 · 235ms | 1,512 · 299ms | 5,206 · 1,025ms |
60
+ * | 2,000 | 726 · 484ms | 415 · 237ms | 2,770 · 1,250ms | 10,406 · 2,720ms |
61
+ * | 4,000 | 726 · 420ms | 415 · 211ms | 3,020 · 648ms | 20,806 · 3,105ms |
62
+ * | 8,000 | — | — | — | 41,606 · 7,597ms |
63
+ *
64
+ * Three of the four hold their DOM flat as rows grow, and for structural
65
+ * reasons that will not change: the gantt VIRTUALISES its task list and
66
+ * timeline window, the calendar month grid draws at most four events per day
67
+ * cell, and the map auto-clusters above 100 markers. Their cost in rows is the
68
+ * O(n) transform, not the DOM.
69
+ *
70
+ * `ObjectTree` is the outlier and therefore the constraint: it flattens every
71
+ * expanded node into the document, measured at a strictly linear **5.2 DOM
72
+ * elements per record**, with no virtualisation anywhere on that path.
73
+ *
74
+ * The budget applied to that: keep the WORST of the four inside ~10,000 DOM
75
+ * elements — an order of magnitude above Lighthouse's ~1,400-element
76
+ * "excessive DOM size" warning, and the last point at which the tree's mount
77
+ * stays under ~3s in an environment that does no layout and no paint at all.
78
+ * 2,000 rows is where that lands, measured rather than interpolated: 10,406
79
+ * elements. It is also ~10x the real application result set this card was
80
+ * filed from (186 rows of `duly_task`), so a genuine working view is nowhere
81
+ * near it, which is the property that keeps the note meaningful when it does
82
+ * appear.
83
+ *
84
+ * ⚠️ These are SHARED-BOX jsdom seconds, not browser wall clock, and jsdom
85
+ * does no layout or paint. The DOM-element counts are the environment-
86
+ * independent half and the reason the ratio between the four views is the
87
+ * load-bearing part of the reading, not the milliseconds. This table is a
88
+ * reading taken once, when objectui#7210 landed; nothing re-derives it.
89
+ */
90
+ export const NON_GRID_ROW_CEILING = 2000;
91
+ /**
92
+ * The query keys a non-grid view sends to honour {@link NON_GRID_ROW_CEILING}:
93
+ * `$top` is the ceiling plus ONE probe row.
94
+ *
95
+ * This is the ONE place the `+ 1` is written (objectui#7508, ruling A′). It
96
+ * used to be a published constant beside the ceiling, which left the
97
+ * mechanism split in two — `$top` at each call site, the slice in
98
+ * {@link applyNonGridRowCeiling} — and gave every caller a number to get
99
+ * wrong. A caller now spreads this into its `find()` query and never spells
100
+ * the probe row itself.
101
+ *
102
+ * The `+ 1` is what makes truncation DETECTABLE, and a site that lost it would
103
+ * look correct and report nothing: with exactly `$top: NON_GRID_ROW_CEILING`,
104
+ * a result set of exactly 2,000 and one of 200,000 both come back as 2,000
105
+ * rows, and the only thing separating them is a `total` the adapter is not
106
+ * obliged to send (`QueryResult.total` is optional, and an adapter answering
107
+ * with a bare array carries none at all). One extra row makes the distinction
108
+ * a fact about the rows in hand.
109
+ *
110
+ * A fresh object per call, so a caller that mutates the query it built cannot
111
+ * change what the next caller sends.
112
+ */
113
+ export function nonGridRowCeilingQuery() {
114
+ return { $top: NON_GRID_ROW_CEILING + 1 };
115
+ }
116
+ /**
117
+ * Cap a non-grid view's result set at {@link NON_GRID_ROW_CEILING} and report
118
+ * whether it had to.
119
+ *
120
+ * Truncation is decided by the PROBE ROW (`rows.length > NON_GRID_ROW_CEILING`
121
+ * against a query built by {@link nonGridRowCeilingQuery}), never by comparing
122
+ * against `total`: `total` is optional on `QueryResult` and absent entirely
123
+ * from a bare-array response, so a `total`-based test would silently stop
124
+ * reporting on exactly the adapters least likely to be paging correctly.
125
+ *
126
+ * The rows are read by `extractRecords`, so this sink accepts exactly the
127
+ * envelopes that helper does (objectui#6839) — pinned in
128
+ * `non-grid-row-ceiling.contractEnvelope-6839.test.ts`.
129
+ */
130
+ export function applyNonGridRowCeiling(result) {
131
+ const all = extractRecords(result);
132
+ const rawTotal = result && typeof result === 'object' && typeof result.total === 'number'
133
+ ? result.total
134
+ : undefined;
135
+ const truncated = all.length > NON_GRID_ROW_CEILING;
136
+ return {
137
+ rows: truncated ? all.slice(0, NON_GRID_ROW_CEILING) : all,
138
+ total: rawTotal,
139
+ truncated,
140
+ };
141
+ }