@object-ui/core 17.6.0 → 17.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (134) hide show
  1. package/CHANGELOG.md +6610 -0
  2. package/README.md +101 -14
  3. package/dist/actions/ActionEngine.d.ts +4 -2
  4. package/dist/actions/ActionEngine.js +13 -8
  5. package/dist/actions/ActionRunner.d.ts +288 -27
  6. package/dist/actions/ActionRunner.js +443 -63
  7. package/dist/actions/TransactionManager.d.ts +7 -2
  8. package/dist/actions/actionKeys.d.ts +9 -7
  9. package/dist/actions/actionKeys.js +57 -14
  10. package/dist/actions/actionResponse.js +2 -2
  11. package/dist/actions/index.d.ts +1 -0
  12. package/dist/actions/index.js +1 -0
  13. package/dist/actions/recordDelete.d.ts +118 -0
  14. package/dist/actions/recordDelete.js +112 -0
  15. package/dist/adapters/ValueDataSource.d.ts +6 -1
  16. package/dist/adapters/ValueDataSource.js +1090 -98
  17. package/dist/adapters/batchTransaction.d.ts +1 -1
  18. package/dist/adapters/batchTransaction.js +1 -1
  19. package/dist/builder/schema-builder.d.ts +38 -81
  20. package/dist/builder/schema-builder.js +33 -152
  21. package/dist/data-scope/ViewDataProvider.d.ts +1 -1
  22. package/dist/data-scope/ViewDataProvider.js +21 -1
  23. package/dist/data-scope/element-data-source.d.ts +261 -7
  24. package/dist/data-scope/element-data-source.js +342 -52
  25. package/dist/data-scope/index.d.ts +9 -4
  26. package/dist/data-scope/index.js +9 -4
  27. package/dist/evaluator/ExpressionEvaluator.d.ts +86 -0
  28. package/dist/evaluator/ExpressionEvaluator.js +184 -12
  29. package/dist/evaluator/FormulaFunctions.d.ts +0 -10
  30. package/dist/evaluator/FormulaFunctions.js +166 -27
  31. package/dist/evaluator/declaredPredicate.d.ts +93 -9
  32. package/dist/evaluator/declaredPredicate.js +117 -16
  33. package/dist/evaluator/fieldRules.d.ts +88 -4
  34. package/dist/evaluator/fieldRules.js +203 -31
  35. package/dist/evaluator/index.d.ts +1 -0
  36. package/dist/evaluator/index.js +1 -0
  37. package/dist/evaluator/listConditional.d.ts +103 -34
  38. package/dist/evaluator/listConditional.js +127 -50
  39. package/dist/evaluator/optionRules.d.ts +77 -6
  40. package/dist/evaluator/optionRules.js +87 -10
  41. package/dist/evaluator/predicateInput.d.ts +15 -4
  42. package/dist/evaluator/predicateInput.js +35 -2
  43. package/dist/evaluator/rowPredicateCanon.d.ts +7 -18
  44. package/dist/evaluator/rowPredicateCanon.js +65 -115
  45. package/dist/evaluator/subjectPermissions.d.ts +76 -0
  46. package/dist/evaluator/subjectPermissions.js +44 -0
  47. package/dist/evaluator/unevaluablePredicate.d.ts +57 -0
  48. package/dist/evaluator/unevaluablePredicate.js +70 -0
  49. package/dist/index.d.ts +14 -0
  50. package/dist/index.js +80 -2
  51. package/dist/protocols/index.d.ts +0 -1
  52. package/dist/protocols/index.js +9 -1
  53. package/dist/registry/Registry.d.ts +358 -31
  54. package/dist/registry/Registry.js +263 -11
  55. package/dist/registry/WidgetRegistry.js +7 -3
  56. package/dist/registry/html-tier-intrinsics.d.ts +90 -0
  57. package/dist/registry/html-tier-intrinsics.js +110 -0
  58. package/dist/registry/public-blocks.d.ts +8 -0
  59. package/dist/registry/public-blocks.js +57 -0
  60. package/dist/theme/ThemeEngine.js +1 -1
  61. package/dist/utils/chart-category-key.d.ts +82 -0
  62. package/dist/utils/chart-category-key.js +84 -0
  63. package/dist/utils/chart-measure-key.d.ts +64 -0
  64. package/dist/utils/chart-measure-key.js +65 -0
  65. package/dist/utils/chart-presentation.d.ts +95 -112
  66. package/dist/utils/chart-presentation.js +169 -104
  67. package/dist/utils/chart-series.d.ts +131 -5
  68. package/dist/utils/chart-series.js +98 -16
  69. package/dist/utils/column-sortability.d.ts +157 -0
  70. package/dist/utils/column-sortability.js +217 -0
  71. package/dist/utils/dashboard-filters.d.ts +38 -6
  72. package/dist/utils/dashboard-filters.js +107 -61
  73. package/dist/utils/dataset-format.d.ts +211 -30
  74. package/dist/utils/dataset-format.js +389 -46
  75. package/dist/utils/date-display.d.ts +354 -0
  76. package/dist/utils/date-display.js +463 -0
  77. package/dist/utils/date-macros.js +7 -0
  78. package/dist/utils/drill-down.d.ts +78 -0
  79. package/dist/utils/drill-down.js +93 -0
  80. package/dist/utils/emptiness.d.ts +96 -0
  81. package/dist/utils/emptiness.js +101 -0
  82. package/dist/utils/expand-fields.d.ts +76 -6
  83. package/dist/utils/expand-fields.js +76 -6
  84. package/dist/utils/extract-records.d.ts +65 -5
  85. package/dist/utils/extract-records.js +65 -8
  86. package/dist/utils/filter-converter.d.ts +152 -4
  87. package/dist/utils/filter-converter.js +1092 -22
  88. package/dist/utils/filter-tokens.d.ts +49 -5
  89. package/dist/utils/filter-tokens.js +105 -33
  90. package/dist/utils/grouping-fields.d.ts +61 -0
  91. package/dist/utils/grouping-fields.js +84 -0
  92. package/dist/utils/managedBy.d.ts +3 -3
  93. package/dist/utils/managedBy.js +5 -5
  94. package/dist/utils/native-date-value.d.ts +48 -0
  95. package/dist/utils/native-date-value.js +167 -0
  96. package/dist/utils/non-grid-row-ceiling.d.ts +143 -0
  97. package/dist/utils/non-grid-row-ceiling.js +141 -0
  98. package/dist/utils/normalize-list-view.d.ts +121 -1
  99. package/dist/utils/normalize-list-view.js +186 -6
  100. package/dist/utils/number-display.d.ts +63 -14
  101. package/dist/utils/number-display.js +34 -12
  102. package/dist/utils/object-aggregate-query.d.ts +113 -0
  103. package/dist/utils/object-aggregate-query.js +50 -0
  104. package/dist/utils/parent-scope.d.ts +123 -0
  105. package/dist/utils/parent-scope.js +140 -0
  106. package/dist/utils/predicate-fields.d.ts +45 -12
  107. package/dist/utils/predicate-fields.js +137 -12
  108. package/dist/utils/record-color.d.ts +75 -0
  109. package/dist/utils/record-color.js +65 -0
  110. package/dist/utils/record-source.d.ts +298 -0
  111. package/dist/utils/record-source.js +331 -0
  112. package/dist/utils/record-title.d.ts +148 -8
  113. package/dist/utils/record-title.js +220 -26
  114. package/dist/utils/reference-keys.d.ts +21 -22
  115. package/dist/utils/reference-keys.js +532 -29
  116. package/dist/utils/sort-query.d.ts +115 -15
  117. package/dist/utils/sort-query.js +240 -13
  118. package/dist/utils/text-comparand.d.ts +92 -0
  119. package/dist/utils/text-comparand.js +99 -0
  120. package/dist/utils/without-denied-fields.d.ts +49 -0
  121. package/dist/utils/without-denied-fields.js +52 -0
  122. package/dist/validation/index.d.ts +8 -4
  123. package/dist/validation/index.js +8 -4
  124. package/dist/validation/schema-validator.d.ts +1 -1
  125. package/dist/validation/schema-validator.js +200 -82
  126. package/dist/validation/validators/object-validation-engine.d.ts +1 -1
  127. package/dist/validation/validators/object-validation-engine.js +14 -3
  128. package/package.json +8 -9
  129. package/dist/data-scope/DataScopeManager.d.ts +0 -127
  130. package/dist/data-scope/DataScopeManager.js +0 -229
  131. package/dist/protocols/ResponsiveProtocol.d.ts +0 -73
  132. package/dist/protocols/ResponsiveProtocol.js +0 -158
  133. package/dist/validation/validation-engine.d.ts +0 -88
  134. package/dist/validation/validation-engine.js +0 -434
@@ -0,0 +1,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
+ }
@@ -5,6 +5,7 @@
5
5
  * This source code is licensed under the MIT license found in the
6
6
  * LICENSE file in the root directory of this source tree.
7
7
  */
8
+ import type { ViewType } from '@object-ui/types';
8
9
  import type { RowHeight } from '@objectstack/spec/ui';
9
10
  /** ListView's toolbar density vocabulary — three steps, not the spec's five. */
10
11
  export type DensityMode = 'compact' | 'comfortable' | 'spacious';
@@ -62,6 +63,101 @@ export declare const ROW_HEIGHT_TO_DENSITY_MODE: Record<RowHeight, DensityMode>;
62
63
  * something typed `DensityMode`.
63
64
  */
64
65
  export declare function rowHeightToDensityMode(rowHeight: unknown): DensityMode | undefined;
66
+ /**
67
+ * A visualization `ListView` actually draws — one member per `case` in its
68
+ * `viewComponentSchema` switch (`packages/plugin-list/src/ListView.tsx`).
69
+ *
70
+ * DERIVED from {@link ViewType} (`ca3942729`), which is itself derived from
71
+ * `@objectstack/spec/ui` `ListView['type']`. Spelled as an `Exclude` of the
72
+ * members this renderer does not draw rather than as the spec's own
73
+ * `VisualizationType`, deliberately: `Exclude` leaves the SPEC's list as the
74
+ * thing being subtracted from, so ANY member the spec adds — visualization or
75
+ * not — lands here as a required key and fails the build, which is what the
76
+ * promise below actually says. Keying on `VisualizationType` instead would let
77
+ * a future non-visualization list type (another `page`) pass this site in
78
+ * silence, which is the exact failure being fixed.
79
+ *
80
+ * ⭐ What it subtracts is {@link UndrawableViewKind} — the keys of
81
+ * {@link UNDRAWABLE_VIEW_KINDS} the current `ViewType` still carries — and
82
+ * never a literal list of kinds (objectui#9880). A literal list is a second
83
+ * hand-written copy of the vocabulary, the failure class `ca3942729` removed
84
+ * from `ViewType` itself, and it rots in the direction the spec actually moved:
85
+ * `Exclude<ViewType, 'list' | 'detail' | 'page'>` kept subtracting `'page'`
86
+ * after objectstack RETIRED it, where `Exclude` of a non-member is a silent
87
+ * no-op no compiler reads. The sibling record's excess `page:` key is what went
88
+ * red; this line stayed green while saying something that had stopped being
89
+ * true.
90
+ *
91
+ * It happens to equal `@objectstack/spec/ui`'s `VisualizationType` today. That
92
+ * is a fact about the resolved spec, not the definition.
93
+ */
94
+ export type ListViewVisualization = Exclude<ViewType, UndrawableViewKind>;
95
+ /**
96
+ * Every member of {@link ViewType} that {@link LIST_VIEW_KINDS} does NOT carry,
97
+ * with the reason — and, where the answer is "the author authored something
98
+ * valid and will get a grid", the sentence to say out loud.
99
+ *
100
+ * The two tables together still partition the derived union — a member the spec
101
+ * ADDS must be classed as drawable or explained HERE, and until it is the build
102
+ * is red — but the red lands on {@link LIST_VIEW_KINDS} alone, because a kind
103
+ * neither table classes is not extracted into {@link UndrawableViewKind} and so
104
+ * stays inside {@link ListViewVisualization}, whose record is total. A key
105
+ * MISSPELLED here fails the same way, for the same reason: the kind it was
106
+ * meant to class is not extracted either. Both directions are pinned in
107
+ * `__tests__/normalize-list-view.undrawableBothLegs-9880.test.ts` rather than
108
+ * asserted in this sentence.
109
+ *
110
+ * ⛔ NOT annotated `Record<Exclude<ViewType, ListViewVisualization>, …>`, and
111
+ * the `satisfies` is not a style choice (objectui#9880). An annotation makes
112
+ * the literal EXACT in BOTH directions: a kind the spec adds is a missing key
113
+ * (wanted) and a kind the spec RETIRES is an EXCESS PROPERTY — a TS2353 that
114
+ * demands a deletion this repository cannot make while it resolves a published
115
+ * spec which still carries the kind (as it did with `page` through 17.4.0).
116
+ * `satisfies` keeps the value constraint
117
+ * (every reason is a `string` or `null`) and drops the exactness, which is what
118
+ * lets one spelling compile against both the pinned published spec and one
119
+ * built from objectstack `main`.
120
+ *
121
+ * `null` means silence is correct — these are objectui view CATEGORIES, never
122
+ * authored as a list-view kind:
123
+ * - `list` is the view CATEGORY, not a kind — it already folds to `grid`.
124
+ * - `detail` is a different renderer (`plugin-detail`), never a ListView case.
125
+ *
126
+ * `page` had a row here until objectui#11073. objectstack#17063 removed
127
+ * `type: 'page'` from the list-view enum, and the row answered for it while this
128
+ * repository still resolved a spec that published the kind. `@objectstack/spec`
129
+ * 17.5.0 carries the removal: both published `@object-ui/types` faces refuse
130
+ * `page`, `Extract` no longer extracted the row, and the residual pins that held
131
+ * it (`normalize-list-view.pageResidual-8429.test.ts`) were converted, so it was
132
+ * deleted as it said it would be. A stored `page` view now degrades like any
133
+ * kind outside the vocabulary.
134
+ */
135
+ declare const UNDRAWABLE_VIEW_KINDS: {
136
+ list: null;
137
+ detail: null;
138
+ };
139
+ /**
140
+ * The members of the current {@link ViewType} that {@link UNDRAWABLE_VIEW_KINDS}
141
+ * classes — the other half of the partition, derived rather than written out.
142
+ *
143
+ * `Extract` over the table's keys, not the table's raw `keyof`, is the load-
144
+ * bearing operator (objectui#9880): the table may carry a row for a kind the
145
+ * spec has since RETIRED, and `Extract` drops that row from the TYPE without
146
+ * the table having to lose it at runtime. That is what made one spelling
147
+ * correct against both the published spec (which published `page` through
148
+ * 17.4.0) and a spec built from objectstack `main` (which retired it), and what
149
+ * made the row's deletion at 17.5.0 a plain deletion (objectui#11073).
150
+ */
151
+ type UndrawableViewKind = Extract<ViewType, keyof typeof UNDRAWABLE_VIEW_KINDS>;
152
+ /**
153
+ * Whether `value` names a visualization `ListView` draws.
154
+ *
155
+ * Exported so a read site cannot restate the membership question as its own
156
+ * literal array: the gate and the seam must answer one question, which is the
157
+ * rule this file's own `availableViews` notes already argue for (objectui#5042,
158
+ * objectui#7544, and now `ca3942729`).
159
+ */
160
+ export declare function isListViewVisualization(value: unknown): value is ListViewVisualization;
65
161
  /**
66
162
  * ObjectUI's `list-view` node historically used a different vocabulary from
67
163
  * `@objectstack/spec` for the same concepts (`fields` where the spec says
@@ -114,10 +210,34 @@ export declare function rowHeightToDensityMode(rowHeight: unknown): DensityMode
114
210
  * so objectui's field is typed from the spec but used as something else.
115
211
  * That mismatch is real and out of scope here; converting formats inside a
116
212
  * vocabulary fold would change what reaches the data source.
213
+ * - `data: { provider: 'object', object }` → `objectName` (#7477, step 6 of
214
+ * #2890). This is the spelling the published `react-blocks` contract
215
+ * recommends and the one `@objectstack/spec`'s `ViewDataSchema` declares;
216
+ * ListView read it at ZERO sites, so a page bound that way validated green
217
+ * upstream and rendered an empty list here with no diagnostic. The `object`
218
+ * provider is a `strictObject` carrying exactly `{ provider, object }`, so
219
+ * `objectName` captures all of it. Two deliberate departures from the folds
220
+ * above, both narrowing:
221
+ * · an already-present `objectName` WINS — this fold only fills a gap, it
222
+ * never re-points a binding that already resolves;
223
+ * · `data` is NOT deleted. Every other fold deletes because the legacy key
224
+ * has one meaning and one home; `data` has four providers, `api`/`value`
225
+ * are read live in `ListView`, and the whole block is FORWARDED to child
226
+ * views (the gantt branch), whose own `getDataConfig` reads `data` before
227
+ * `objectName`. Deleting it for one provider would rewrite what a child
228
+ * resolves. Nothing in `ListView` reads `data.provider === 'object'`, so
229
+ * keeping it cannot create a second de-facto contract inside the renderer.
117
230
  * - `viewType`: a missing kind, or the view CATEGORY `'list'` that AI-authored
118
231
  * metadata stores and hosts forward verbatim, becomes the renderable `'grid'`
119
232
  * — otherwise it reaches the renderer's typeless default branch and shows as
120
- * a red "Unknown component type" box.
233
+ * a red "Unknown component type" box. Before that default applies, the
234
+ * AUTHOR's kind is read (#7477): `specType` — the slot
235
+ * `components/renderers/layout/react-page.tsx` parks a react-tier `type` in
236
+ * when the SDUI envelope claims the `type` key (ADR-0078) — and then a bare
237
+ * `type` when it names a kind ListView draws, which the component
238
+ * discriminator (`'list-view'`) never does. Same two legs, same order, as
239
+ * `normalizeChartSchema`'s chart-family read. An explicit `viewType` still
240
+ * wins: this leg only fills the gap that used to resolve to `'grid'`.
121
241
  * - each `columns` entry's IDENTITY — `name` / `fieldName` → the spec's `field`
122
242
  * (#3104). This one MIRRORS instead of deleting, for the reason given on
123
243
  * {@link normalizeColumnIdentities}: `columns` entries cross the package