@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.
- package/CHANGELOG.md +7404 -0
- package/README.md +127 -26
- package/dist/actions/ActionEngine.d.ts +5 -3
- package/dist/actions/ActionEngine.js +14 -10
- package/dist/actions/ActionRunner.d.ts +363 -30
- package/dist/actions/ActionRunner.js +544 -58
- package/dist/actions/TransactionManager.d.ts +7 -2
- package/dist/actions/actionKeys.d.ts +75 -13
- package/dist/actions/actionKeys.js +131 -16
- package/dist/actions/actionResponse.js +3 -3
- package/dist/actions/index.d.ts +2 -0
- package/dist/actions/index.js +2 -0
- package/dist/actions/recordDelete.d.ts +118 -0
- package/dist/actions/recordDelete.js +112 -0
- package/dist/actions/recordIdParam.d.ts +77 -0
- package/dist/actions/recordIdParam.js +37 -0
- package/dist/actions/serverActionHandler.d.ts +1 -1
- package/dist/actions/serverActionHandler.js +1 -1
- package/dist/adapters/ValueDataSource.d.ts +6 -1
- package/dist/adapters/ValueDataSource.js +1090 -98
- package/dist/adapters/batchTransaction.d.ts +1 -1
- package/dist/adapters/batchTransaction.js +1 -1
- package/dist/builder/schema-builder.d.ts +38 -81
- package/dist/builder/schema-builder.js +33 -152
- package/dist/data-scope/ViewDataProvider.d.ts +1 -1
- package/dist/data-scope/ViewDataProvider.js +21 -1
- package/dist/data-scope/element-data-source.d.ts +261 -7
- package/dist/data-scope/element-data-source.js +342 -52
- package/dist/data-scope/index.d.ts +9 -4
- package/dist/data-scope/index.js +9 -4
- package/dist/evaluator/ExpressionEvaluator.d.ts +128 -1
- package/dist/evaluator/ExpressionEvaluator.js +226 -13
- package/dist/evaluator/FormulaFunctions.d.ts +0 -10
- package/dist/evaluator/FormulaFunctions.js +166 -27
- package/dist/evaluator/SafeExpressionParser.js +0 -4
- package/dist/evaluator/declaredPredicate.d.ts +93 -9
- package/dist/evaluator/declaredPredicate.js +118 -17
- package/dist/evaluator/fieldRules.d.ts +107 -4
- package/dist/evaluator/fieldRules.js +231 -35
- package/dist/evaluator/index.d.ts +2 -0
- package/dist/evaluator/index.js +2 -0
- package/dist/evaluator/listConditional.d.ts +125 -9
- package/dist/evaluator/listConditional.js +167 -26
- package/dist/evaluator/optionRules.d.ts +126 -0
- package/dist/evaluator/optionRules.js +141 -4
- package/dist/evaluator/predicateInput.d.ts +15 -4
- package/dist/evaluator/predicateInput.js +35 -2
- package/dist/evaluator/rowPredicateCanon.d.ts +56 -0
- package/dist/evaluator/rowPredicateCanon.js +158 -0
- package/dist/evaluator/subjectPermissions.d.ts +76 -0
- package/dist/evaluator/subjectPermissions.js +44 -0
- package/dist/evaluator/unevaluablePredicate.d.ts +57 -0
- package/dist/evaluator/unevaluablePredicate.js +70 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.js +97 -2
- package/dist/protocols/index.d.ts +0 -1
- package/dist/protocols/index.js +9 -1
- package/dist/registry/Registry.d.ts +397 -48
- package/dist/registry/Registry.js +263 -11
- package/dist/registry/WidgetRegistry.js +7 -3
- package/dist/registry/html-tier-intrinsics.d.ts +90 -0
- package/dist/registry/html-tier-intrinsics.js +110 -0
- package/dist/registry/public-blocks.d.ts +8 -0
- package/dist/registry/public-blocks.js +57 -0
- package/dist/theme/ThemeEngine.js +1 -1
- package/dist/theme/index.d.ts +1 -1
- package/dist/theme/index.js +1 -1
- package/dist/types/index.d.ts +92 -13
- package/dist/utils/chart-category-key.d.ts +82 -0
- package/dist/utils/chart-category-key.js +84 -0
- package/dist/utils/chart-measure-key.d.ts +64 -0
- package/dist/utils/chart-measure-key.js +65 -0
- package/dist/utils/chart-presentation.d.ts +188 -0
- package/dist/utils/chart-presentation.js +345 -0
- package/dist/utils/chart-series.d.ts +271 -31
- package/dist/utils/chart-series.js +478 -55
- package/dist/utils/column-identity.d.ts +43 -0
- package/dist/utils/column-identity.js +47 -1
- package/dist/utils/column-sortability.d.ts +157 -0
- package/dist/utils/column-sortability.js +217 -0
- package/dist/utils/dashboard-filters.d.ts +38 -6
- package/dist/utils/dashboard-filters.js +107 -61
- package/dist/utils/dataset-format.d.ts +211 -30
- package/dist/utils/dataset-format.js +407 -52
- package/dist/utils/date-display.d.ts +354 -0
- package/dist/utils/date-display.js +463 -0
- package/dist/utils/date-macros.js +7 -0
- package/dist/utils/debug.js +7 -0
- package/dist/utils/drill-down.d.ts +78 -0
- package/dist/utils/drill-down.js +93 -0
- package/dist/utils/emptiness.d.ts +96 -0
- package/dist/utils/emptiness.js +101 -0
- package/dist/utils/expand-fields.d.ts +171 -11
- package/dist/utils/expand-fields.js +171 -11
- package/dist/utils/extract-records.d.ts +65 -5
- package/dist/utils/extract-records.js +65 -8
- package/dist/utils/filter-converter.d.ts +152 -4
- package/dist/utils/filter-converter.js +1092 -22
- package/dist/utils/filter-tokens.d.ts +49 -5
- package/dist/utils/filter-tokens.js +105 -34
- package/dist/utils/freeze-schema.d.ts +38 -1
- package/dist/utils/freeze-schema.js +10 -0
- package/dist/utils/grouping-fields.d.ts +61 -0
- package/dist/utils/grouping-fields.js +84 -0
- package/dist/utils/humanize-label.d.ts +60 -0
- package/dist/utils/humanize-label.js +62 -0
- package/dist/utils/managedBy.d.ts +30 -5
- package/dist/utils/managedBy.js +5 -5
- package/dist/utils/native-date-value.d.ts +48 -0
- package/dist/utils/native-date-value.js +167 -0
- package/dist/utils/non-grid-row-ceiling.d.ts +143 -0
- package/dist/utils/non-grid-row-ceiling.js +141 -0
- package/dist/utils/normalize-list-view.d.ts +131 -1
- package/dist/utils/normalize-list-view.js +248 -6
- package/dist/utils/number-display.d.ts +70 -17
- package/dist/utils/number-display.js +34 -12
- package/dist/utils/object-aggregate-query.d.ts +113 -0
- package/dist/utils/object-aggregate-query.js +50 -0
- package/dist/utils/parent-scope.d.ts +123 -0
- package/dist/utils/parent-scope.js +140 -0
- package/dist/utils/predicate-fields.d.ts +62 -4
- package/dist/utils/predicate-fields.js +169 -4
- package/dist/utils/record-color.d.ts +75 -0
- package/dist/utils/record-color.js +65 -0
- package/dist/utils/record-source.d.ts +298 -0
- package/dist/utils/record-source.js +331 -0
- package/dist/utils/record-title.d.ts +148 -8
- package/dist/utils/record-title.js +220 -26
- package/dist/utils/reference-keys.d.ts +21 -22
- package/dist/utils/reference-keys.js +532 -29
- package/dist/utils/retired-field-types.d.ts +90 -0
- package/dist/utils/retired-field-types.js +124 -0
- package/dist/utils/sort-query.d.ts +128 -26
- package/dist/utils/sort-query.js +240 -13
- package/dist/utils/text-comparand.d.ts +92 -0
- package/dist/utils/text-comparand.js +99 -0
- package/dist/utils/unmaterialized-fields.d.ts +52 -0
- package/dist/utils/unmaterialized-fields.js +57 -0
- package/dist/utils/without-denied-fields.d.ts +49 -0
- package/dist/utils/without-denied-fields.js +52 -0
- package/dist/validation/index.d.ts +9 -4
- package/dist/validation/index.js +9 -4
- package/dist/validation/schema-validator.d.ts +1 -1
- package/dist/validation/schema-validator.js +305 -82
- package/dist/validation/server-owned-value.d.ts +62 -0
- package/dist/validation/server-owned-value.js +99 -0
- package/dist/validation/validators/object-validation-engine.d.ts +1 -1
- package/dist/validation/validators/object-validation-engine.js +14 -5
- package/package.json +8 -9
- package/dist/__benchmarks__/core.bench.d.ts +0 -8
- package/dist/__benchmarks__/core.bench.js +0 -53
- package/dist/data-scope/DataScopeManager.d.ts +0 -127
- package/dist/data-scope/DataScopeManager.js +0 -229
- package/dist/protocols/ResponsiveProtocol.d.ts +0 -73
- package/dist/protocols/ResponsiveProtocol.js +0 -158
- package/dist/validation/validation-engine.d.ts +0 -88
- 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
|
-
|
|
55
|
-
|
|
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
|
|
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
|
package/dist/utils/managedBy.js
CHANGED
|
@@ -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
|
|
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
|
+
}
|