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.
Files changed (69) hide show
  1. package/build/data/ExposedValueView.js +1 -1
  2. package/build/data/comparer.d.ts +6 -8
  3. package/build/data/comparer.d.ts.map +1 -1
  4. package/build/ui/Cx.d.ts +5 -0
  5. package/build/ui/Cx.d.ts.map +1 -1
  6. package/build/ui/Cx.js +34 -3
  7. package/build/ui/Prop.d.ts +1 -1
  8. package/build/ui/Prop.d.ts.map +1 -1
  9. package/build/ui/Restate.d.ts +6 -0
  10. package/build/ui/Restate.d.ts.map +1 -1
  11. package/build/ui/Restate.js +2 -1
  12. package/build/ui/adapter/ArrayAdapter.d.ts +4 -6
  13. package/build/ui/adapter/ArrayAdapter.d.ts.map +1 -1
  14. package/build/ui/adapter/GroupAdapter.d.ts +22 -2
  15. package/build/ui/adapter/GroupAdapter.d.ts.map +1 -1
  16. package/build/ui/adapter/GroupAdapter.js +80 -5
  17. package/build/widgets/DocumentTitle.d.ts.map +1 -1
  18. package/build/widgets/DocumentTitle.js +2 -0
  19. package/build/widgets/drag-drop/DragSource.d.ts +3 -2
  20. package/build/widgets/drag-drop/DragSource.d.ts.map +1 -1
  21. package/build/widgets/drag-drop/DropZone.d.ts +4 -4
  22. package/build/widgets/drag-drop/DropZone.d.ts.map +1 -1
  23. package/build/widgets/drag-drop/ops.d.ts +13 -2
  24. package/build/widgets/drag-drop/ops.d.ts.map +1 -1
  25. package/build/widgets/drag-drop/ops.js +36 -9
  26. package/build/widgets/form/NumberField.js +1 -0
  27. package/build/widgets/grid/Grid.d.ts +19 -3
  28. package/build/widgets/grid/Grid.d.ts.map +1 -1
  29. package/build/widgets/grid/Grid.js +2 -0
  30. package/build/widgets/grid/TreeNode.d.ts +2 -0
  31. package/build/widgets/grid/TreeNode.d.ts.map +1 -1
  32. package/build/widgets/overlay/captureMouse.d.ts +4 -4
  33. package/build/widgets/overlay/captureMouse.d.ts.map +1 -1
  34. package/build/widgets/overlay/captureMouse.js +8 -3
  35. package/dist/data.js +1 -1
  36. package/dist/manifest.js +866 -866
  37. package/dist/ui.js +130 -12
  38. package/dist/widgets.css +4 -0
  39. package/dist/widgets.js +43 -10
  40. package/package.json +1 -1
  41. package/src/data/ExposedValueView.spec.ts +57 -0
  42. package/src/data/ExposedValueView.ts +1 -1
  43. package/src/data/comparer.ts +6 -7
  44. package/src/ui/Cx.tsx +44 -3
  45. package/src/ui/DataProxy.ts +55 -55
  46. package/src/ui/Prop.ts +1 -1
  47. package/src/ui/Rescope.ts +50 -50
  48. package/src/ui/Restate.tsx +9 -0
  49. package/src/ui/adapter/ArrayAdapter.ts +6 -8
  50. package/src/ui/adapter/GroupAdapter.spec.ts +75 -0
  51. package/src/ui/adapter/GroupAdapter.ts +103 -10
  52. package/src/ui/exprHelpers.ts +96 -96
  53. package/src/widgets/DocumentTitle.ts +2 -0
  54. package/src/widgets/Sandbox.ts +104 -104
  55. package/src/widgets/drag-drop/DragSource.tsx +3 -3
  56. package/src/widgets/drag-drop/DropZone.tsx +5 -5
  57. package/src/widgets/drag-drop/ops.tsx +54 -12
  58. package/src/widgets/form/ColorField.scss +112 -112
  59. package/src/widgets/form/DateTimeField.scss +111 -111
  60. package/src/widgets/form/LookupField.maps.scss +26 -26
  61. package/src/widgets/form/MonthField.scss +113 -113
  62. package/src/widgets/form/NumberField.scss +4 -0
  63. package/src/widgets/form/NumberField.tsx +1 -0
  64. package/src/widgets/form/Select.scss +104 -104
  65. package/src/widgets/form/TextField.scss +66 -66
  66. package/src/widgets/grid/Grid.tsx +23 -2
  67. package/src/widgets/grid/TreeNode.tsx +3 -0
  68. package/src/widgets/nav/MenuItem.tsx +525 -525
  69. 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);
@@ -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, Sorter, CollatorOptions } from "../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
- export interface ExtendedSorter extends Sorter {
30
- comparer?: (a: any, b: any) => number;
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?: Sorter[] | ExtendedSorter[]): void {
214
+ public sort(sorters?: ExtendedSorter[]): void {
217
215
  if (sorters) {
218
- this.buildSorter(sorters as ExtendedSorter[]);
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
- if (grouping.comparer && !this.preserveOrder) {
120
- results.sort(grouping.comparer);
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
- (groupSorters.length > 0
214
- ? getComparer(
215
- groupSorters,
216
- (x: any) => x.key,
217
- this.sortOptions ? Culture.getComparer(this.sortOptions) : undefined,
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)) {
@@ -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
  }