cx 26.6.0 → 26.7.1
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/build/data/ExposedValueView.js +1 -1
- package/build/data/comparer.d.ts +6 -8
- package/build/data/comparer.d.ts.map +1 -1
- package/build/ui/Cx.d.ts +5 -0
- package/build/ui/Cx.d.ts.map +1 -1
- package/build/ui/Cx.js +34 -3
- package/build/ui/Prop.d.ts +1 -1
- package/build/ui/Prop.d.ts.map +1 -1
- package/build/ui/Restate.d.ts +6 -0
- package/build/ui/Restate.d.ts.map +1 -1
- package/build/ui/Restate.js +2 -1
- package/build/ui/adapter/ArrayAdapter.d.ts +4 -6
- package/build/ui/adapter/ArrayAdapter.d.ts.map +1 -1
- package/build/ui/adapter/GroupAdapter.d.ts +22 -2
- package/build/ui/adapter/GroupAdapter.d.ts.map +1 -1
- package/build/ui/adapter/GroupAdapter.js +80 -5
- package/build/widgets/DocumentTitle.d.ts.map +1 -1
- package/build/widgets/DocumentTitle.js +2 -0
- package/build/widgets/drag-drop/DragSource.d.ts +3 -2
- package/build/widgets/drag-drop/DragSource.d.ts.map +1 -1
- package/build/widgets/drag-drop/DropZone.d.ts +4 -4
- package/build/widgets/drag-drop/DropZone.d.ts.map +1 -1
- package/build/widgets/drag-drop/ops.d.ts +13 -2
- package/build/widgets/drag-drop/ops.d.ts.map +1 -1
- package/build/widgets/drag-drop/ops.js +36 -9
- package/build/widgets/form/NumberField.js +1 -0
- package/build/widgets/grid/Grid.d.ts +19 -3
- package/build/widgets/grid/Grid.d.ts.map +1 -1
- package/build/widgets/grid/Grid.js +2 -0
- package/build/widgets/grid/TreeNode.d.ts +2 -0
- package/build/widgets/grid/TreeNode.d.ts.map +1 -1
- package/build/widgets/overlay/captureMouse.d.ts +4 -4
- package/build/widgets/overlay/captureMouse.d.ts.map +1 -1
- package/build/widgets/overlay/captureMouse.js +8 -3
- package/dist/data.js +1 -1
- package/dist/manifest.js +866 -866
- package/dist/ui.js +130 -12
- package/dist/widgets.css +4 -0
- package/dist/widgets.js +43 -10
- package/package.json +1 -1
- package/src/data/ExposedValueView.spec.ts +57 -0
- package/src/data/ExposedValueView.ts +1 -1
- package/src/data/comparer.ts +6 -7
- package/src/ui/Cx.tsx +44 -3
- package/src/ui/DataProxy.ts +55 -55
- package/src/ui/Prop.ts +1 -1
- package/src/ui/Rescope.ts +50 -50
- package/src/ui/Restate.tsx +9 -0
- package/src/ui/adapter/ArrayAdapter.ts +6 -8
- package/src/ui/adapter/GroupAdapter.spec.ts +75 -0
- package/src/ui/adapter/GroupAdapter.ts +103 -10
- package/src/ui/exprHelpers.ts +96 -96
- package/src/widgets/DocumentTitle.ts +2 -0
- package/src/widgets/Sandbox.ts +104 -104
- package/src/widgets/drag-drop/DragSource.tsx +3 -3
- package/src/widgets/drag-drop/DropZone.tsx +5 -5
- package/src/widgets/drag-drop/ops.tsx +54 -12
- package/src/widgets/form/ColorField.scss +112 -112
- package/src/widgets/form/DateTimeField.scss +111 -111
- package/src/widgets/form/LookupField.maps.scss +26 -26
- package/src/widgets/form/MonthField.scss +113 -113
- package/src/widgets/form/NumberField.scss +4 -0
- package/src/widgets/form/NumberField.tsx +1 -0
- package/src/widgets/form/Select.scss +104 -104
- package/src/widgets/form/TextField.scss +66 -66
- package/src/widgets/grid/Grid.tsx +23 -2
- package/src/widgets/grid/TreeNode.tsx +3 -0
- package/src/widgets/nav/MenuItem.tsx +525 -525
- package/src/widgets/overlay/captureMouse.ts +14 -7
package/src/ui/Rescope.ts
CHANGED
|
@@ -1,50 +1,50 @@
|
|
|
1
|
-
import { Widget } from "./Widget";
|
|
2
|
-
import { PureContainerBase, PureContainerConfig } from "./PureContainer";
|
|
3
|
-
import { Binding } from "../data/Binding";
|
|
4
|
-
import { ZoomIntoPropertyView } from "../data/ZoomIntoPropertyView";
|
|
5
|
-
import { StructuredInstanceDataAccessor } from "./StructuredInstanceDataAccessor";
|
|
6
|
-
import { isObject } from "../util/isObject";
|
|
7
|
-
import { StructuredProp } from "./Prop";
|
|
8
|
-
import { AccessorChain } from "../data/createAccessorModelProxy";
|
|
9
|
-
|
|
10
|
-
export interface RescopeConfig extends PureContainerConfig {
|
|
11
|
-
bind: string | AccessorChain<any>;
|
|
12
|
-
rootName?: string | AccessorChain<any>;
|
|
13
|
-
rootAlias?: string | AccessorChain<any>;
|
|
14
|
-
data?: StructuredProp;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export class Rescope extends PureContainerBase<RescopeConfig> {
|
|
18
|
-
declare bind: string;
|
|
19
|
-
declare binding: any;
|
|
20
|
-
declare rootAlias?: string;
|
|
21
|
-
declare rootName: string;
|
|
22
|
-
declare data?: any;
|
|
23
|
-
|
|
24
|
-
init() {
|
|
25
|
-
this.binding = Binding.get(this.bind);
|
|
26
|
-
if (this.rootAlias) this.rootName = this.rootAlias;
|
|
27
|
-
super.init();
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
initInstance(context: any, instance: any) {
|
|
31
|
-
instance.store = new ZoomIntoPropertyView({
|
|
32
|
-
store: instance.parentStore,
|
|
33
|
-
binding: this.binding,
|
|
34
|
-
rootName: this.rootName,
|
|
35
|
-
nestedData: isObject(this.data)
|
|
36
|
-
? new StructuredInstanceDataAccessor({ instance, data: this.data, useParentStore: true })
|
|
37
|
-
: undefined,
|
|
38
|
-
});
|
|
39
|
-
super.initInstance(context, instance);
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
applyParentStore(instance: any) {
|
|
43
|
-
instance.store.setStore(instance.parentStore);
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
Rescope.prototype.bind = "$page";
|
|
48
|
-
Rescope.prototype.rootName = "$root";
|
|
49
|
-
|
|
50
|
-
Widget.alias("rescope", Rescope);
|
|
1
|
+
import { Widget } from "./Widget";
|
|
2
|
+
import { PureContainerBase, PureContainerConfig } from "./PureContainer";
|
|
3
|
+
import { Binding } from "../data/Binding";
|
|
4
|
+
import { ZoomIntoPropertyView } from "../data/ZoomIntoPropertyView";
|
|
5
|
+
import { StructuredInstanceDataAccessor } from "./StructuredInstanceDataAccessor";
|
|
6
|
+
import { isObject } from "../util/isObject";
|
|
7
|
+
import { StructuredProp } from "./Prop";
|
|
8
|
+
import { AccessorChain } from "../data/createAccessorModelProxy";
|
|
9
|
+
|
|
10
|
+
export interface RescopeConfig extends PureContainerConfig {
|
|
11
|
+
bind: string | AccessorChain<any>;
|
|
12
|
+
rootName?: string | AccessorChain<any>;
|
|
13
|
+
rootAlias?: string | AccessorChain<any>;
|
|
14
|
+
data?: StructuredProp;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export class Rescope extends PureContainerBase<RescopeConfig> {
|
|
18
|
+
declare bind: string;
|
|
19
|
+
declare binding: any;
|
|
20
|
+
declare rootAlias?: string;
|
|
21
|
+
declare rootName: string;
|
|
22
|
+
declare data?: any;
|
|
23
|
+
|
|
24
|
+
init() {
|
|
25
|
+
this.binding = Binding.get(this.bind);
|
|
26
|
+
if (this.rootAlias) this.rootName = this.rootAlias;
|
|
27
|
+
super.init();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
initInstance(context: any, instance: any) {
|
|
31
|
+
instance.store = new ZoomIntoPropertyView({
|
|
32
|
+
store: instance.parentStore,
|
|
33
|
+
binding: this.binding,
|
|
34
|
+
rootName: this.rootName,
|
|
35
|
+
nestedData: isObject(this.data)
|
|
36
|
+
? new StructuredInstanceDataAccessor({ instance, data: this.data, useParentStore: true })
|
|
37
|
+
: undefined,
|
|
38
|
+
});
|
|
39
|
+
super.initInstance(context, instance);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
applyParentStore(instance: any) {
|
|
43
|
+
instance.store.setStore(instance.parentStore);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
Rescope.prototype.bind = "$page";
|
|
48
|
+
Rescope.prototype.rootName = "$root";
|
|
49
|
+
|
|
50
|
+
Widget.alias("rescope", Rescope);
|
package/src/ui/Restate.tsx
CHANGED
|
@@ -33,6 +33,12 @@ export interface RestateConfig extends PureContainerConfig {
|
|
|
33
33
|
/** Set to `true` to disable batching of updates. */
|
|
34
34
|
immediate?: boolean;
|
|
35
35
|
|
|
36
|
+
/** Cap re-entrant synchronous updates on the detached `Cx` this Restate renders: once a burst of
|
|
37
|
+
* back-to-back updates gets too deep, defer the excess to the coalesced branch instead of updating
|
|
38
|
+
* synchronously. Avoids React's "Maximum update depth exceeded" on very large synchronous render bursts.
|
|
39
|
+
* Only applies in `detached` mode (a non-detached Restate has no `Cx` of its own). Off by default. */
|
|
40
|
+
limitSyncBursts?: boolean;
|
|
41
|
+
|
|
36
42
|
/** Error handler callback. */
|
|
37
43
|
onError?: (error: Error, instance: Instance) => void;
|
|
38
44
|
|
|
@@ -53,6 +59,7 @@ export class Restate<Config extends RestateConfig = RestateConfig> extends PureC
|
|
|
53
59
|
declare onError?: (error: Error, instance: Instance) => void;
|
|
54
60
|
declare waitForIdle: boolean;
|
|
55
61
|
declare immediate: boolean;
|
|
62
|
+
declare limitSyncBursts: boolean;
|
|
56
63
|
|
|
57
64
|
declareData(...args: any[]) {
|
|
58
65
|
return super.declareData(...args, {
|
|
@@ -138,6 +145,7 @@ export class Restate<Config extends RestateConfig = RestateConfig> extends PureC
|
|
|
138
145
|
deferredUntilIdle={instance.data.deferredUntilIdle}
|
|
139
146
|
idleTimeout={instance.data.idleTimeout}
|
|
140
147
|
immediate={this.immediate}
|
|
148
|
+
limitSyncBursts={this.limitSyncBursts}
|
|
141
149
|
cultureInfo={instance.cultureInfo}
|
|
142
150
|
/>
|
|
143
151
|
);
|
|
@@ -147,6 +155,7 @@ export class Restate<Config extends RestateConfig = RestateConfig> extends PureC
|
|
|
147
155
|
Restate.prototype.detached = false;
|
|
148
156
|
Restate.prototype.waitForIdle = false;
|
|
149
157
|
Restate.prototype.immediate = false;
|
|
158
|
+
Restate.prototype.limitSyncBursts = false;
|
|
150
159
|
Restate.prototype.culture = null;
|
|
151
160
|
|
|
152
161
|
export const PrivateStore = Restate;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { DataAdapter, DataAdapterRecord, DataAdapterConfig } from "./DataAdapter";
|
|
2
2
|
import { ReadOnlyDataView } from "../../data/ReadOnlyDataView";
|
|
3
|
-
import { sorter } from "../../data/comparer";
|
|
3
|
+
import { sorter, type ExtendedSorter } from "../../data/comparer";
|
|
4
4
|
import { isArray } from "../../util/isArray";
|
|
5
5
|
import { ArrayElementView } from "../../data/ArrayElementView";
|
|
6
6
|
import { Accessor, getAccessor } from "../../data/getAccessor";
|
|
@@ -9,7 +9,7 @@ import { isDefined, isObject } from "../../util";
|
|
|
9
9
|
import { RenderingContext } from "../RenderingContext";
|
|
10
10
|
import { Instance } from "../Instance";
|
|
11
11
|
import { View } from "../../data/View";
|
|
12
|
-
import { Prop,
|
|
12
|
+
import { Prop, CollatorOptions } from "../Prop";
|
|
13
13
|
|
|
14
14
|
export interface RecordStoreCache {
|
|
15
15
|
recordStoreCache: WeakMap<any, View>;
|
|
@@ -26,10 +26,8 @@ export interface ArrayAdapterConfig extends DataAdapterConfig {
|
|
|
26
26
|
preserveOrder?: boolean;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
sortOptions?: CollatorOptions;
|
|
32
|
-
}
|
|
29
|
+
// Re-exported for back-compat; the canonical definition lives in `data/comparer`.
|
|
30
|
+
export type { ExtendedSorter } from "../../data/comparer";
|
|
33
31
|
|
|
34
32
|
export interface ResolvedSorter {
|
|
35
33
|
getter: (x: any) => any;
|
|
@@ -213,9 +211,9 @@ export class ArrayAdapter<T = any> extends DataAdapter<T> {
|
|
|
213
211
|
}
|
|
214
212
|
}
|
|
215
213
|
|
|
216
|
-
public sort(sorters?:
|
|
214
|
+
public sort(sorters?: ExtendedSorter[]): void {
|
|
217
215
|
if (sorters) {
|
|
218
|
-
this.buildSorter(sorters
|
|
216
|
+
this.buildSorter(sorters);
|
|
219
217
|
}
|
|
220
218
|
}
|
|
221
219
|
}
|
|
@@ -39,4 +39,79 @@ describe("GroupAdapter", () => {
|
|
|
39
39
|
|
|
40
40
|
assert.deepStrictEqual(newKeys, [], `Grouping config was mutated. New keys: ${newKeys.join(", ")}`);
|
|
41
41
|
});
|
|
42
|
+
|
|
43
|
+
// Region totals after aggregation: A=10, B=30, C=20.
|
|
44
|
+
const records = [
|
|
45
|
+
{ region: "A", sales: 10, product: "p3" },
|
|
46
|
+
{ region: "B", sales: 30, product: "p1" },
|
|
47
|
+
{ region: "C", sales: 20, product: "p2" },
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
const baseGrouping: GroupingConfig = {
|
|
51
|
+
key: { region: { bind: "$record.region" } },
|
|
52
|
+
aggregates: { total: { type: "sum", value: { bind: "$record.sales" } } },
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
// Runs the full grouping pipeline and returns the resulting group order (region keys).
|
|
56
|
+
function groupOrder(grouping: GroupingConfig[], options?: { sortGroupsBySorters?: boolean }, sorters?: any[]) {
|
|
57
|
+
const adapter = new GroupAdapter({ groupings: grouping, ...options });
|
|
58
|
+
adapter.init();
|
|
59
|
+
if (sorters) adapter.sort(sorters);
|
|
60
|
+
const result = adapter.getRecords({} as any, {} as any, records, new Store({ data: {} }));
|
|
61
|
+
return result.filter((r) => r.type === "group-header").map((r: any) => r.group.region);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
it("sorts groups by an aggregate via sortField/sortDirection", () => {
|
|
65
|
+
assert.deepStrictEqual(groupOrder([{ ...baseGrouping, sortField: "total", sortDirection: "DESC" }]), [
|
|
66
|
+
"B",
|
|
67
|
+
"C",
|
|
68
|
+
"A",
|
|
69
|
+
]);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("sorts groups by a key field via sortField", () => {
|
|
73
|
+
assert.deepStrictEqual(groupOrder([{ ...baseGrouping, sortField: "region", sortDirection: "DESC" }]), [
|
|
74
|
+
"C",
|
|
75
|
+
"B",
|
|
76
|
+
"A",
|
|
77
|
+
]);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("supports a multi-field sorters array", () => {
|
|
81
|
+
assert.deepStrictEqual(groupOrder([{ ...baseGrouping, sorters: [{ field: "total", direction: "ASC" }] }]), [
|
|
82
|
+
"A",
|
|
83
|
+
"C",
|
|
84
|
+
"B",
|
|
85
|
+
]);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("defaults to ascending sort by key", () => {
|
|
89
|
+
assert.deepStrictEqual(groupOrder([baseGrouping]), ["A", "B", "C"]);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("lets an explicit comparer win over sortField", () => {
|
|
93
|
+
// Custom comparer sorts groups by total ascending, despite sortField asking for DESC.
|
|
94
|
+
const comparer = (a: any, b: any) => a.aggregates.total - b.aggregates.total;
|
|
95
|
+
assert.deepStrictEqual(groupOrder([{ ...baseGrouping, comparer, sortField: "total", sortDirection: "DESC" }]), [
|
|
96
|
+
"A",
|
|
97
|
+
"C",
|
|
98
|
+
"B",
|
|
99
|
+
]);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("reorders groups by the active column sort when sortGroupsBySorters is set", () => {
|
|
103
|
+
// The record-level value selector must be ignored; the column field drives group order.
|
|
104
|
+
const order = groupOrder([baseGrouping], { sortGroupsBySorters: true }, [
|
|
105
|
+
{ field: "total", direction: "DESC", value: () => 999 },
|
|
106
|
+
]);
|
|
107
|
+
assert.deepStrictEqual(order, ["B", "C", "A"]);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("keeps configured group order when the sorted column is not a group field", () => {
|
|
111
|
+
// `product` is neither a key nor an aggregate, so groups keep their default (key ASC) order.
|
|
112
|
+
const order = groupOrder([baseGrouping], { sortGroupsBySorters: true }, [
|
|
113
|
+
{ field: "product", direction: "DESC" },
|
|
114
|
+
]);
|
|
115
|
+
assert.deepStrictEqual(order, ["A", "B", "C"]);
|
|
116
|
+
});
|
|
42
117
|
});
|
|
@@ -4,11 +4,13 @@ import { ReadOnlyDataView } from "../../data/ReadOnlyDataView";
|
|
|
4
4
|
import { View } from "../../data/View";
|
|
5
5
|
import { isDataRecord } from "../../util";
|
|
6
6
|
import { isArray } from "../../util/isArray";
|
|
7
|
+
import { isDefined } from "../../util/isDefined";
|
|
8
|
+
import { isNonEmptyArray } from "../../util/isNonEmptyArray";
|
|
7
9
|
import { Culture } from "../Culture";
|
|
8
10
|
import { Instance } from "../Instance";
|
|
9
11
|
import { Prop, SortDirection, Sorter, StructuredProp } from "../Prop";
|
|
10
12
|
import { RenderingContext } from "../RenderingContext";
|
|
11
|
-
import { ArrayAdapter, ArrayAdapterConfig, RecordStoreCache } from "./ArrayAdapter";
|
|
13
|
+
import { ArrayAdapter, ArrayAdapterConfig, ExtendedSorter, RecordStoreCache } from "./ArrayAdapter";
|
|
12
14
|
import { DataAdapterRecord } from "./DataAdapter";
|
|
13
15
|
|
|
14
16
|
export interface GroupKey {
|
|
@@ -21,7 +23,17 @@ export interface GroupingConfig {
|
|
|
21
23
|
text?: Prop<string>;
|
|
22
24
|
includeHeader?: boolean;
|
|
23
25
|
includeFooter?: boolean;
|
|
26
|
+
/** Custom group comparer. Takes precedence over `sortField`/`sortDirection`/`sorters`. */
|
|
24
27
|
comparer?: ((a: any, b: any) => number) | null;
|
|
28
|
+
/**
|
|
29
|
+
* Sort groups by a single field resolved against the group's `key`, `aggregates` and `name`.
|
|
30
|
+
* Use an aggregate alias (e.g. `"total"`) to sort by an aggregate, or a key field to sort by key.
|
|
31
|
+
*/
|
|
32
|
+
sortField?: string;
|
|
33
|
+
/** Sort direction used together with `sortField`. Defaults to `"ASC"`. */
|
|
34
|
+
sortDirection?: SortDirection;
|
|
35
|
+
/** Multi-field group sorting. Each sorter's `field` is resolved against the group's `key`, `aggregates` and `name`. */
|
|
36
|
+
sorters?: Sorter[];
|
|
25
37
|
header?: any;
|
|
26
38
|
footer?: any;
|
|
27
39
|
}
|
|
@@ -50,6 +62,8 @@ export interface GroupAdapterConfig extends ArrayAdapterConfig {
|
|
|
50
62
|
groupRecordsName?: string;
|
|
51
63
|
groupings?: GroupingConfig[] | null;
|
|
52
64
|
groupName?: string;
|
|
65
|
+
/** When enabled, the active record sorters also reorder groups (resolved against each group's key/aggregates/name). */
|
|
66
|
+
sortGroupsBySorters?: boolean;
|
|
53
67
|
}
|
|
54
68
|
|
|
55
69
|
export class GroupAdapter<T = any> extends ArrayAdapter<T> {
|
|
@@ -58,11 +72,41 @@ export class GroupAdapter<T = any> extends ArrayAdapter<T> {
|
|
|
58
72
|
declare public groupRecordsName?: string;
|
|
59
73
|
declare public groupings?: ResolvedGrouping[] | null;
|
|
60
74
|
declare public groupName: string;
|
|
75
|
+
declare public sortGroupsBySorters?: boolean;
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Per-level comparer derived from the active record sorters, used to reorder groups when
|
|
79
|
+
* `sortGroupsBySorters` is set. A level's entry is `null` when no active sorter maps to that
|
|
80
|
+
* level's key/aggregate, so the grouping's own configured order is kept.
|
|
81
|
+
*/
|
|
82
|
+
protected groupSortComparers?: (((a: any, b: any) => number) | null)[];
|
|
61
83
|
|
|
62
84
|
constructor(config?: GroupAdapterConfig) {
|
|
63
85
|
super(config);
|
|
64
86
|
}
|
|
65
87
|
|
|
88
|
+
public sort(sorters?: ExtendedSorter[]): void {
|
|
89
|
+
super.sort(sorters);
|
|
90
|
+
|
|
91
|
+
// When enabled, derive a group comparer from the active record sorters so that interactive
|
|
92
|
+
// column sorting also reorders the groups. A column only reorders groups at levels where its
|
|
93
|
+
// field is a group key or aggregate; other levels keep their configured order. The record-level
|
|
94
|
+
// value selector is ignored — the field is resolved against the group's key/aggregates/name.
|
|
95
|
+
if (this.sortGroupsBySorters && this.groupings) {
|
|
96
|
+
const cultureComparer = this.sortOptions ? Culture.getComparer(this.sortOptions) : undefined;
|
|
97
|
+
const colSorters: ExtendedSorter[] = isNonEmptyArray(sorters)
|
|
98
|
+
? sorters.map((s) => ({ field: s.field, direction: s.direction, comparer: s.comparer }))
|
|
99
|
+
: [];
|
|
100
|
+
|
|
101
|
+
this.groupSortComparers = this.groupings.map((g) => {
|
|
102
|
+
if (colSorters.length === 0) return null;
|
|
103
|
+
const fields = groupFieldNames(g, this.aggregates);
|
|
104
|
+
const applicable = colSorters.filter((s) => s.field && fields.has(s.field));
|
|
105
|
+
return applicable.length > 0 ? buildGroupComparer(applicable, cultureComparer) : null;
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
66
110
|
public init(): void {
|
|
67
111
|
super.init();
|
|
68
112
|
|
|
@@ -116,8 +160,13 @@ export class GroupAdapter<T = any> extends ArrayAdapter<T> {
|
|
|
116
160
|
grouper.processAll(records);
|
|
117
161
|
let results = grouper.getResults();
|
|
118
162
|
|
|
119
|
-
|
|
120
|
-
|
|
163
|
+
// An active column sort that maps to this level's key/aggregate (when `sortGroupsBySorters` is
|
|
164
|
+
// enabled) takes over; otherwise the grouping's configured comparer (or implicit key order) applies.
|
|
165
|
+
const dynamicComparer = this.sortGroupsBySorters ? this.groupSortComparers?.[level] : undefined;
|
|
166
|
+
const comparer = dynamicComparer ?? grouping.comparer;
|
|
167
|
+
|
|
168
|
+
if (comparer && !this.preserveOrder) {
|
|
169
|
+
results.sort(comparer);
|
|
121
170
|
}
|
|
122
171
|
|
|
123
172
|
results.forEach((gr) => {
|
|
@@ -208,15 +257,22 @@ export class GroupAdapter<T = any> extends ArrayAdapter<T> {
|
|
|
208
257
|
g.text,
|
|
209
258
|
);
|
|
210
259
|
|
|
260
|
+
const cultureComparer = this.sortOptions ? Culture.getComparer(this.sortOptions) : undefined;
|
|
261
|
+
|
|
262
|
+
// Sort groups by an aggregate/key/name through `sortField`/`sortDirection` or a `sorters` array.
|
|
263
|
+
let sortSorters: Sorter[] | null = null;
|
|
264
|
+
if (isNonEmptyArray(g.sorters)) sortSorters = g.sorters!;
|
|
265
|
+
else if (g.sortField) sortSorters = [{ field: g.sortField, direction: g.sortDirection ?? "ASC" }];
|
|
266
|
+
|
|
267
|
+
// `comparer`, `sortField`/`sorters` and the implicit key order are equivalent alternatives; an
|
|
268
|
+
// explicit `comparer` wins, then declarative sorters, then sorting by key in declaration order.
|
|
211
269
|
const comparer =
|
|
212
270
|
g.comparer ??
|
|
213
|
-
(
|
|
214
|
-
?
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
)
|
|
219
|
-
: null);
|
|
271
|
+
(sortSorters
|
|
272
|
+
? buildGroupComparer(sortSorters, cultureComparer)
|
|
273
|
+
: groupSorters.length > 0
|
|
274
|
+
? getComparer(groupSorters, (x: any) => x.key, cultureComparer)
|
|
275
|
+
: null);
|
|
220
276
|
|
|
221
277
|
return {
|
|
222
278
|
...g,
|
|
@@ -233,6 +289,43 @@ export class GroupAdapter<T = any> extends ArrayAdapter<T> {
|
|
|
233
289
|
|
|
234
290
|
GroupAdapter.prototype.groupName = "$group";
|
|
235
291
|
GroupAdapter.prototype.preserveOrder = false;
|
|
292
|
+
GroupAdapter.prototype.sortGroupsBySorters = false;
|
|
293
|
+
|
|
294
|
+
// The set of fields a group can be sorted by at a given level: its key fields, its aggregate aliases
|
|
295
|
+
// and the group name. Used to decide whether an active column sort applies to that grouping level.
|
|
296
|
+
function groupFieldNames(grouping: ResolvedGrouping, adapterAggregates?: StructuredProp): Set<string> {
|
|
297
|
+
const names = new Set<string>();
|
|
298
|
+
for (const k of grouping.grouper.keys) names.add(k.name);
|
|
299
|
+
const aggregates = { ...adapterAggregates, ...grouping.aggregates };
|
|
300
|
+
for (const a in aggregates) names.add(a);
|
|
301
|
+
names.add("name");
|
|
302
|
+
names.add("$name");
|
|
303
|
+
return names;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Reads a sort field straight from the GroupResult — first its key fields, then its aggregates, then
|
|
307
|
+
// its name. Returns a plain selector so groups can be sorted without cloning the result per comparison.
|
|
308
|
+
function groupFieldSelector(field: string): (gr: any) => any {
|
|
309
|
+
if (field === "name" || field === "$name") return (gr) => gr?.name;
|
|
310
|
+
return (gr) => {
|
|
311
|
+
if (gr?.key && field in gr.key) return gr.key[field];
|
|
312
|
+
if (gr?.aggregates && field in gr.aggregates) return gr.aggregates[field];
|
|
313
|
+
return undefined;
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Builds a group comparer from a list of sorters. Each sorter resolves by its explicit `value`
|
|
318
|
+
// selector, or by `field` looked up against the group's key/aggregates/name.
|
|
319
|
+
function buildGroupComparer(
|
|
320
|
+
sorters: ExtendedSorter[],
|
|
321
|
+
cultureComparer?: (a: any, b: any) => number,
|
|
322
|
+
): (a: any, b: any) => number {
|
|
323
|
+
return getComparer(
|
|
324
|
+
sorters.map((s) => (isDefined(s.value) || !s.field ? s : { ...s, value: groupFieldSelector(s.field) })),
|
|
325
|
+
undefined,
|
|
326
|
+
cultureComparer,
|
|
327
|
+
);
|
|
328
|
+
}
|
|
236
329
|
|
|
237
330
|
function serializeKey(data: any): string {
|
|
238
331
|
if (isDataRecord(data)) {
|
package/src/ui/exprHelpers.ts
CHANGED
|
@@ -1,96 +1,96 @@
|
|
|
1
|
-
import { computable } from "../data/computable";
|
|
2
|
-
import { AccessorChain } from "../data/createAccessorModelProxy";
|
|
3
|
-
import { Selector } from "../data/Selector";
|
|
4
|
-
import { Format } from "../util/Format";
|
|
5
|
-
import { expr } from "./expr";
|
|
6
|
-
|
|
7
|
-
/** Returns a selector that converts the value to boolean using !! */
|
|
8
|
-
export function truthy<V>(arg: AccessorChain<V>): Selector<boolean> {
|
|
9
|
-
return expr(arg, (x) => !!x);
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
/** Returns a selector that checks if the value is falsy using ! */
|
|
13
|
-
export function falsy<V>(arg: AccessorChain<V>): Selector<boolean> {
|
|
14
|
-
return expr(arg, (x) => !x);
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
/** Returns a selector that checks if the value is strictly true (=== true) */
|
|
18
|
-
export function isTrue(arg: AccessorChain<any>): Selector<boolean> {
|
|
19
|
-
return expr(arg, (x) => x === true);
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/** Returns a selector that checks if the value is strictly false (=== false) */
|
|
23
|
-
export function isFalse(arg: AccessorChain<any>): Selector<boolean> {
|
|
24
|
-
return expr(arg, (x) => x === false);
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/** Returns a selector that checks if the value is not null or undefined (x != null) */
|
|
28
|
-
export function hasValue<V>(arg: AccessorChain<V>): Selector<boolean> {
|
|
29
|
-
return expr(arg, (x) => x != null);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/** Returns a selector that checks if a string or array is empty (null, undefined, or length === 0) */
|
|
33
|
-
export function isEmpty(arg: AccessorChain<string | any[] | null | undefined>): Selector<boolean> {
|
|
34
|
-
return expr(arg, (x) => x == null || x.length === 0);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/** Returns a selector that checks if a string or array is non-empty (not null/undefined and length > 0) */
|
|
38
|
-
export function isNonEmpty(arg: AccessorChain<string | any[] | null | undefined>): Selector<boolean> {
|
|
39
|
-
return expr(arg, (x) => x != null && x.length > 0);
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/** Returns a selector that checks if the value is less than the given value */
|
|
43
|
-
export function lessThan<V>(arg: AccessorChain<V>, value: V): Selector<boolean> {
|
|
44
|
-
return expr(arg, (x) => x < value);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/** Returns a selector that checks if the value is less than or equal to the given value */
|
|
48
|
-
export function lessThanOrEqual<V>(arg: AccessorChain<V>, value: V): Selector<boolean> {
|
|
49
|
-
return expr(arg, (x) => x <= value);
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/** Returns a selector that checks if the value is greater than the given value */
|
|
53
|
-
export function greaterThan<V>(arg: AccessorChain<V>, value: V): Selector<boolean> {
|
|
54
|
-
return expr(arg, (x) => x > value);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/** Returns a selector that checks if the value is greater than or equal to the given value */
|
|
58
|
-
export function greaterThanOrEqual<V>(arg: AccessorChain<V>, value: V): Selector<boolean> {
|
|
59
|
-
return expr(arg, (x) => x >= value);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/** Returns a selector that checks if the value equals the given value using == */
|
|
63
|
-
export function equal<V>(arg: AccessorChain<V>, value: V): Selector<boolean> {
|
|
64
|
-
return expr(arg, (x) => x == value);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/** Returns a selector that checks if the value does not equal the given value using != */
|
|
68
|
-
export function notEqual<V>(arg: AccessorChain<V>, value: V): Selector<boolean> {
|
|
69
|
-
return expr(arg, (x) => x != value);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/** Returns a selector that checks if the value strictly equals the given value using === */
|
|
73
|
-
export function strictEqual<V>(arg: AccessorChain<V>, value: V): Selector<boolean> {
|
|
74
|
-
return expr(arg, (x) => x === value);
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/** Returns a selector that checks if the value strictly does not equal the given value using !== */
|
|
78
|
-
export function strictNotEqual<V>(arg: AccessorChain<V>, value: V): Selector<boolean> {
|
|
79
|
-
return expr(arg, (x) => x !== value);
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/** Returns a selector that formats the value using the specified format string.
|
|
83
|
-
* Format strings use semicolon-separated syntax: "formatType;param1;param2"
|
|
84
|
-
* @param arg - The accessor chain to the value
|
|
85
|
-
* @param fmt - The format string
|
|
86
|
-
* @param nullText - Optional text to display for null/undefined values
|
|
87
|
-
* @example
|
|
88
|
-
* format(m.price, "n;2") // formats as number with 2 decimal places
|
|
89
|
-
* format(m.date, "d") // formats as date
|
|
90
|
-
* format(m.value, "p;0;2") // formats as percentage with 0-2 decimal places
|
|
91
|
-
* format(m.value, "n;2", "N/A") // shows "N/A" for null values
|
|
92
|
-
*/
|
|
93
|
-
export function format<V>(arg: AccessorChain<V>, fmt: string, nullText?: string): Selector<string> {
|
|
94
|
-
let f = nullText != null ? `${fmt}|${nullText}` : fmt;
|
|
95
|
-
return computable(arg, (x) => Format.value(x, f));
|
|
96
|
-
}
|
|
1
|
+
import { computable } from "../data/computable";
|
|
2
|
+
import { AccessorChain } from "../data/createAccessorModelProxy";
|
|
3
|
+
import { Selector } from "../data/Selector";
|
|
4
|
+
import { Format } from "../util/Format";
|
|
5
|
+
import { expr } from "./expr";
|
|
6
|
+
|
|
7
|
+
/** Returns a selector that converts the value to boolean using !! */
|
|
8
|
+
export function truthy<V>(arg: AccessorChain<V>): Selector<boolean> {
|
|
9
|
+
return expr(arg, (x) => !!x);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** Returns a selector that checks if the value is falsy using ! */
|
|
13
|
+
export function falsy<V>(arg: AccessorChain<V>): Selector<boolean> {
|
|
14
|
+
return expr(arg, (x) => !x);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Returns a selector that checks if the value is strictly true (=== true) */
|
|
18
|
+
export function isTrue(arg: AccessorChain<any>): Selector<boolean> {
|
|
19
|
+
return expr(arg, (x) => x === true);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Returns a selector that checks if the value is strictly false (=== false) */
|
|
23
|
+
export function isFalse(arg: AccessorChain<any>): Selector<boolean> {
|
|
24
|
+
return expr(arg, (x) => x === false);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Returns a selector that checks if the value is not null or undefined (x != null) */
|
|
28
|
+
export function hasValue<V>(arg: AccessorChain<V>): Selector<boolean> {
|
|
29
|
+
return expr(arg, (x) => x != null);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Returns a selector that checks if a string or array is empty (null, undefined, or length === 0) */
|
|
33
|
+
export function isEmpty(arg: AccessorChain<string | any[] | null | undefined>): Selector<boolean> {
|
|
34
|
+
return expr(arg, (x) => x == null || x.length === 0);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Returns a selector that checks if a string or array is non-empty (not null/undefined and length > 0) */
|
|
38
|
+
export function isNonEmpty(arg: AccessorChain<string | any[] | null | undefined>): Selector<boolean> {
|
|
39
|
+
return expr(arg, (x) => x != null && x.length > 0);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Returns a selector that checks if the value is less than the given value */
|
|
43
|
+
export function lessThan<V>(arg: AccessorChain<V>, value: V): Selector<boolean> {
|
|
44
|
+
return expr(arg, (x) => x < value);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Returns a selector that checks if the value is less than or equal to the given value */
|
|
48
|
+
export function lessThanOrEqual<V>(arg: AccessorChain<V>, value: V): Selector<boolean> {
|
|
49
|
+
return expr(arg, (x) => x <= value);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Returns a selector that checks if the value is greater than the given value */
|
|
53
|
+
export function greaterThan<V>(arg: AccessorChain<V>, value: V): Selector<boolean> {
|
|
54
|
+
return expr(arg, (x) => x > value);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Returns a selector that checks if the value is greater than or equal to the given value */
|
|
58
|
+
export function greaterThanOrEqual<V>(arg: AccessorChain<V>, value: V): Selector<boolean> {
|
|
59
|
+
return expr(arg, (x) => x >= value);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Returns a selector that checks if the value equals the given value using == */
|
|
63
|
+
export function equal<V>(arg: AccessorChain<V>, value: V): Selector<boolean> {
|
|
64
|
+
return expr(arg, (x) => x == value);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Returns a selector that checks if the value does not equal the given value using != */
|
|
68
|
+
export function notEqual<V>(arg: AccessorChain<V>, value: V): Selector<boolean> {
|
|
69
|
+
return expr(arg, (x) => x != value);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Returns a selector that checks if the value strictly equals the given value using === */
|
|
73
|
+
export function strictEqual<V>(arg: AccessorChain<V>, value: V): Selector<boolean> {
|
|
74
|
+
return expr(arg, (x) => x === value);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Returns a selector that checks if the value strictly does not equal the given value using !== */
|
|
78
|
+
export function strictNotEqual<V>(arg: AccessorChain<V>, value: V): Selector<boolean> {
|
|
79
|
+
return expr(arg, (x) => x !== value);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Returns a selector that formats the value using the specified format string.
|
|
83
|
+
* Format strings use semicolon-separated syntax: "formatType;param1;param2"
|
|
84
|
+
* @param arg - The accessor chain to the value
|
|
85
|
+
* @param fmt - The format string
|
|
86
|
+
* @param nullText - Optional text to display for null/undefined values
|
|
87
|
+
* @example
|
|
88
|
+
* format(m.price, "n;2") // formats as number with 2 decimal places
|
|
89
|
+
* format(m.date, "d") // formats as date
|
|
90
|
+
* format(m.value, "p;0;2") // formats as percentage with 0-2 decimal places
|
|
91
|
+
* format(m.value, "n;2", "N/A") // shows "N/A" for null values
|
|
92
|
+
*/
|
|
93
|
+
export function format<V>(arg: AccessorChain<V>, fmt: string, nullText?: string): Selector<string> {
|
|
94
|
+
let f = nullText != null ? `${fmt}|${nullText}` : fmt;
|
|
95
|
+
return computable(arg, (x) => Format.value(x, f));
|
|
96
|
+
}
|
|
@@ -80,6 +80,8 @@ export class DocumentTitle extends Widget<DocumentTitleConfig> {
|
|
|
80
80
|
}
|
|
81
81
|
|
|
82
82
|
prepare(context: RenderingContext, instance: Instance): void {
|
|
83
|
+
if (typeof document == "undefined") return;
|
|
84
|
+
|
|
83
85
|
if ((context as any).documentTitle.activeInstance == instance)
|
|
84
86
|
document.title = (context as any).documentTitle.title;
|
|
85
87
|
}
|