cx 26.5.1 → 26.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/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/locale/de-de.js +1 -0
- package/build/locale/en-us.js +1 -0
- package/build/locale/es-es.js +1 -0
- package/build/locale/fr-fr.js +1 -0
- package/build/locale/nl-nl.js +1 -0
- package/build/locale/pt-pt.js +1 -0
- package/build/locale/sr-latn-ba.js +1 -0
- package/build/ui/Prop.d.ts +1 -1
- package/build/ui/Prop.d.ts.map +1 -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/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/LookupField.d.ts +6 -0
- package/build/widgets/form/LookupField.d.ts.map +1 -1
- package/build/widgets/form/LookupField.js +12 -0
- package/build/widgets/form/NumberField.js +1 -0
- package/build/widgets/grid/Grid.d.ts +24 -3
- package/build/widgets/grid/Grid.d.ts.map +1 -1
- package/build/widgets/grid/Grid.js +5 -2
- 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 +857 -857
- package/dist/ui.js +94 -9
- package/dist/widgets.css +8 -0
- package/dist/widgets.js +60 -12
- 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/locale/de-de.ts +1 -0
- package/src/locale/en-us.ts +1 -0
- package/src/locale/es-es.ts +1 -0
- package/src/locale/fr-fr.ts +1 -0
- package/src/locale/nl-nl.ts +1 -0
- package/src/locale/pt-pt.ts +1 -0
- package/src/locale/sr-latn-ba.ts +1 -0
- package/src/ui/Prop.ts +1 -1
- 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/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/LookupField.spec.tsx +149 -0
- package/src/widgets/form/LookupField.tsx +27 -0
- package/src/widgets/form/NumberField.scss +4 -0
- package/src/widgets/form/NumberField.tsx +1 -0
- package/src/widgets/grid/Grid.scss +6 -0
- package/src/widgets/grid/Grid.tsx +31 -4
- package/src/widgets/overlay/captureMouse.ts +14 -7
package/dist/ui.js
CHANGED
|
@@ -3829,9 +3829,38 @@ function startHotAppLoop(appModule, element, store, widgets, options = {}) {
|
|
|
3829
3829
|
}
|
|
3830
3830
|
|
|
3831
3831
|
class GroupAdapter extends ArrayAdapter {
|
|
3832
|
+
/**
|
|
3833
|
+
* Per-level comparer derived from the active record sorters, used to reorder groups when
|
|
3834
|
+
* `sortGroupsBySorters` is set. A level's entry is `null` when no active sorter maps to that
|
|
3835
|
+
* level's key/aggregate, so the grouping's own configured order is kept.
|
|
3836
|
+
*/
|
|
3837
|
+
groupSortComparers;
|
|
3832
3838
|
constructor(config) {
|
|
3833
3839
|
super(config);
|
|
3834
3840
|
}
|
|
3841
|
+
sort(sorters) {
|
|
3842
|
+
super.sort(sorters);
|
|
3843
|
+
// When enabled, derive a group comparer from the active record sorters so that interactive
|
|
3844
|
+
// column sorting also reorders the groups. A column only reorders groups at levels where its
|
|
3845
|
+
// field is a group key or aggregate; other levels keep their configured order. The record-level
|
|
3846
|
+
// value selector is ignored — the field is resolved against the group's key/aggregates/name.
|
|
3847
|
+
if (this.sortGroupsBySorters && this.groupings) {
|
|
3848
|
+
const cultureComparer = this.sortOptions ? Culture.getComparer(this.sortOptions) : undefined;
|
|
3849
|
+
const colSorters = isNonEmptyArray(sorters)
|
|
3850
|
+
? sorters.map((s) => ({
|
|
3851
|
+
field: s.field,
|
|
3852
|
+
direction: s.direction,
|
|
3853
|
+
comparer: s.comparer,
|
|
3854
|
+
}))
|
|
3855
|
+
: [];
|
|
3856
|
+
this.groupSortComparers = this.groupings.map((g) => {
|
|
3857
|
+
if (colSorters.length === 0) return null;
|
|
3858
|
+
const fields = groupFieldNames(g, this.aggregates);
|
|
3859
|
+
const applicable = colSorters.filter((s) => s.field && fields.has(s.field));
|
|
3860
|
+
return applicable.length > 0 ? buildGroupComparer(applicable, cultureComparer) : null;
|
|
3861
|
+
});
|
|
3862
|
+
}
|
|
3863
|
+
}
|
|
3835
3864
|
init() {
|
|
3836
3865
|
super.init();
|
|
3837
3866
|
if (this.groupRecordsAlias) {
|
|
@@ -3866,8 +3895,12 @@ class GroupAdapter extends ArrayAdapter {
|
|
|
3866
3895
|
grouper.reset();
|
|
3867
3896
|
grouper.processAll(records);
|
|
3868
3897
|
let results = grouper.getResults();
|
|
3869
|
-
|
|
3870
|
-
|
|
3898
|
+
// An active column sort that maps to this level's key/aggregate (when `sortGroupsBySorters` is
|
|
3899
|
+
// enabled) takes over; otherwise the grouping's configured comparer (or implicit key order) applies.
|
|
3900
|
+
const dynamicComparer = this.sortGroupsBySorters ? this.groupSortComparers?.[level] : undefined;
|
|
3901
|
+
const comparer = dynamicComparer ?? grouping.comparer;
|
|
3902
|
+
if (comparer && !this.preserveOrder) {
|
|
3903
|
+
results.sort(comparer);
|
|
3871
3904
|
}
|
|
3872
3905
|
results.forEach((gr) => {
|
|
3873
3906
|
keys.push(gr.key);
|
|
@@ -3949,15 +3982,26 @@ class GroupAdapter extends ArrayAdapter {
|
|
|
3949
3982
|
(r) => r.store.getData(),
|
|
3950
3983
|
g.text,
|
|
3951
3984
|
);
|
|
3985
|
+
const cultureComparer = this.sortOptions ? Culture.getComparer(this.sortOptions) : undefined;
|
|
3986
|
+
// Sort groups by an aggregate/key/name through `sortField`/`sortDirection` or a `sorters` array.
|
|
3987
|
+
let sortSorters = null;
|
|
3988
|
+
if (isNonEmptyArray(g.sorters)) sortSorters = g.sorters;
|
|
3989
|
+
else if (g.sortField)
|
|
3990
|
+
sortSorters = [
|
|
3991
|
+
{
|
|
3992
|
+
field: g.sortField,
|
|
3993
|
+
direction: g.sortDirection ?? "ASC",
|
|
3994
|
+
},
|
|
3995
|
+
];
|
|
3996
|
+
// `comparer`, `sortField`/`sorters` and the implicit key order are equivalent alternatives; an
|
|
3997
|
+
// explicit `comparer` wins, then declarative sorters, then sorting by key in declaration order.
|
|
3952
3998
|
const comparer =
|
|
3953
3999
|
g.comparer ??
|
|
3954
|
-
(
|
|
3955
|
-
?
|
|
3956
|
-
|
|
3957
|
-
|
|
3958
|
-
|
|
3959
|
-
)
|
|
3960
|
-
: null);
|
|
4000
|
+
(sortSorters
|
|
4001
|
+
? buildGroupComparer(sortSorters, cultureComparer)
|
|
4002
|
+
: groupSorters.length > 0
|
|
4003
|
+
? getComparer(groupSorters, (x) => x.key, cultureComparer)
|
|
4004
|
+
: null);
|
|
3961
4005
|
return {
|
|
3962
4006
|
...g,
|
|
3963
4007
|
key: resolvedKey,
|
|
@@ -3972,6 +4016,47 @@ class GroupAdapter extends ArrayAdapter {
|
|
|
3972
4016
|
}
|
|
3973
4017
|
GroupAdapter.prototype.groupName = "$group";
|
|
3974
4018
|
GroupAdapter.prototype.preserveOrder = false;
|
|
4019
|
+
GroupAdapter.prototype.sortGroupsBySorters = false;
|
|
4020
|
+
// The set of fields a group can be sorted by at a given level: its key fields, its aggregate aliases
|
|
4021
|
+
// and the group name. Used to decide whether an active column sort applies to that grouping level.
|
|
4022
|
+
function groupFieldNames(grouping, adapterAggregates) {
|
|
4023
|
+
const names = new Set();
|
|
4024
|
+
for (const k of grouping.grouper.keys) names.add(k.name);
|
|
4025
|
+
const aggregates = {
|
|
4026
|
+
...adapterAggregates,
|
|
4027
|
+
...grouping.aggregates,
|
|
4028
|
+
};
|
|
4029
|
+
for (const a in aggregates) names.add(a);
|
|
4030
|
+
names.add("name");
|
|
4031
|
+
names.add("$name");
|
|
4032
|
+
return names;
|
|
4033
|
+
}
|
|
4034
|
+
// Reads a sort field straight from the GroupResult — first its key fields, then its aggregates, then
|
|
4035
|
+
// its name. Returns a plain selector so groups can be sorted without cloning the result per comparison.
|
|
4036
|
+
function groupFieldSelector(field) {
|
|
4037
|
+
if (field === "name" || field === "$name") return (gr) => gr?.name;
|
|
4038
|
+
return (gr) => {
|
|
4039
|
+
if (gr?.key && field in gr.key) return gr.key[field];
|
|
4040
|
+
if (gr?.aggregates && field in gr.aggregates) return gr.aggregates[field];
|
|
4041
|
+
return undefined;
|
|
4042
|
+
};
|
|
4043
|
+
}
|
|
4044
|
+
// Builds a group comparer from a list of sorters. Each sorter resolves by its explicit `value`
|
|
4045
|
+
// selector, or by `field` looked up against the group's key/aggregates/name.
|
|
4046
|
+
function buildGroupComparer(sorters, cultureComparer) {
|
|
4047
|
+
return getComparer(
|
|
4048
|
+
sorters.map((s) =>
|
|
4049
|
+
isDefined(s.value) || !s.field
|
|
4050
|
+
? s
|
|
4051
|
+
: {
|
|
4052
|
+
...s,
|
|
4053
|
+
value: groupFieldSelector(s.field),
|
|
4054
|
+
},
|
|
4055
|
+
),
|
|
4056
|
+
undefined,
|
|
4057
|
+
cultureComparer,
|
|
4058
|
+
);
|
|
4059
|
+
}
|
|
3975
4060
|
function serializeKey(data) {
|
|
3976
4061
|
if (isDataRecord(data)) {
|
|
3977
4062
|
return Object.keys(data)
|
package/dist/widgets.css
CHANGED
|
@@ -1086,6 +1086,10 @@
|
|
|
1086
1086
|
padding-left: 26px;
|
|
1087
1087
|
}
|
|
1088
1088
|
|
|
1089
|
+
.cxs-clear > .cxe-numberfield-input {
|
|
1090
|
+
padding-right: 22px;
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1089
1093
|
.cxe-numberfield-tool {
|
|
1090
1094
|
pointer-events: none;
|
|
1091
1095
|
display: block;
|
|
@@ -5313,6 +5317,10 @@ th.cxe-calendar-display {
|
|
|
5313
5317
|
content: counter(cx-row-number);
|
|
5314
5318
|
}
|
|
5315
5319
|
|
|
5320
|
+
.cxe-grid-group-caption.cxs-reset-row-numbers {
|
|
5321
|
+
counter-set: cx-row-number 0;
|
|
5322
|
+
}
|
|
5323
|
+
|
|
5316
5324
|
.cxe-grid-fixed-header {
|
|
5317
5325
|
overflow: hidden;
|
|
5318
5326
|
position: absolute;
|
package/dist/widgets.js
CHANGED
|
@@ -2050,6 +2050,7 @@ function captureMouse2(e, options) {
|
|
|
2050
2050
|
}
|
|
2051
2051
|
}
|
|
2052
2052
|
e.stopPropagation();
|
|
2053
|
+
return tear;
|
|
2053
2054
|
function move(e) {
|
|
2054
2055
|
if (!active) {
|
|
2055
2056
|
tear();
|
|
@@ -2105,7 +2106,11 @@ function captureMouseOrTouch2(e, options) {
|
|
|
2105
2106
|
el.addEventListener("touchmove", move);
|
|
2106
2107
|
el.addEventListener("touchend", end);
|
|
2107
2108
|
e.stopPropagation();
|
|
2108
|
-
|
|
2109
|
+
return () => {
|
|
2110
|
+
el.removeEventListener("touchmove", move);
|
|
2111
|
+
el.removeEventListener("touchend", end);
|
|
2112
|
+
};
|
|
2113
|
+
} else return captureMouse2(e, options);
|
|
2109
2114
|
}
|
|
2110
2115
|
/**
|
|
2111
2116
|
* Legacy function for capturing mouse events with individual parameters
|
|
@@ -2116,7 +2121,7 @@ function captureMouseOrTouch2(e, options) {
|
|
|
2116
2121
|
* @param cursor - CSS cursor style for the capture surface
|
|
2117
2122
|
*/
|
|
2118
2123
|
function captureMouse(e, onMouseMove, onMouseUp, captureData, cursor) {
|
|
2119
|
-
captureMouse2(e, {
|
|
2124
|
+
return captureMouse2(e, {
|
|
2120
2125
|
onMouseMove,
|
|
2121
2126
|
onMouseUp,
|
|
2122
2127
|
captureData,
|
|
@@ -2132,7 +2137,7 @@ function captureMouse(e, onMouseMove, onMouseUp, captureData, cursor) {
|
|
|
2132
2137
|
* @param cursor - CSS cursor style for the capture surface
|
|
2133
2138
|
*/
|
|
2134
2139
|
function captureMouseOrTouch(e, onMouseMove, onMouseUp, captureData, cursor) {
|
|
2135
|
-
captureMouseOrTouch2(e, {
|
|
2140
|
+
return captureMouseOrTouch2(e, {
|
|
2136
2141
|
onMouseMove,
|
|
2137
2142
|
onMouseUp,
|
|
2138
2143
|
captureData,
|
|
@@ -2403,6 +2408,7 @@ let puppet = null;
|
|
|
2403
2408
|
let scrollTimer = null;
|
|
2404
2409
|
let vscrollParent = null;
|
|
2405
2410
|
let hscrollParent = null;
|
|
2411
|
+
let releaseCapture = null;
|
|
2406
2412
|
function registerDropZone(dropZone) {
|
|
2407
2413
|
dropZones.push(dropZone);
|
|
2408
2414
|
return () => {
|
|
@@ -2488,7 +2494,15 @@ function initiateDragDrop(e, options = {}, onDragEnd) {
|
|
|
2488
2494
|
dragStartedZones.set(zone, true);
|
|
2489
2495
|
});
|
|
2490
2496
|
notifyDragMove(e);
|
|
2491
|
-
captureMouseOrTouch(e, notifyDragMove, notifyDragDrop);
|
|
2497
|
+
releaseCapture = captureMouseOrTouch(e, notifyDragMove, notifyDragDrop);
|
|
2498
|
+
document.addEventListener("keydown", onDragKeyDown, true);
|
|
2499
|
+
}
|
|
2500
|
+
function onDragKeyDown(e) {
|
|
2501
|
+
if (!puppet || e.key !== "Escape") return;
|
|
2502
|
+
//cancel the operation without dropping; there is no pointer event to report
|
|
2503
|
+
e.preventDefault();
|
|
2504
|
+
e.stopPropagation();
|
|
2505
|
+
notifyDragDrop(null, true);
|
|
2492
2506
|
}
|
|
2493
2507
|
function notifyDragMove(e, _captureData) {
|
|
2494
2508
|
let event = getDragEvent(e, "dragmove");
|
|
@@ -2613,21 +2627,36 @@ function clearScrollTimer() {
|
|
|
2613
2627
|
scrollTimer = null;
|
|
2614
2628
|
}
|
|
2615
2629
|
}
|
|
2616
|
-
function notifyDragDrop(e) {
|
|
2630
|
+
function notifyDragDrop(e, cancelled = false) {
|
|
2631
|
+
if (!puppet) return;
|
|
2617
2632
|
clearScrollTimer();
|
|
2618
|
-
|
|
2633
|
+
document.removeEventListener("keydown", onDragKeyDown, true);
|
|
2619
2634
|
if (puppet.stop) puppet.stop();
|
|
2620
|
-
|
|
2635
|
+
//a keyboard cancellation has no pointer event, so the drop and away notifications are skipped
|
|
2636
|
+
let dropEvent = !cancelled && e ? getDragEvent(e, "dragdrop") : null;
|
|
2637
|
+
let result;
|
|
2638
|
+
if (dropEvent && activeZone && activeZone.onDrop) result = activeZone.onDrop(dropEvent);
|
|
2639
|
+
let endEvent = {
|
|
2640
|
+
type: "dragend",
|
|
2641
|
+
event: e,
|
|
2642
|
+
cursor: e ? getCursorPos(e) : null,
|
|
2643
|
+
source: puppet.source,
|
|
2644
|
+
cancelled,
|
|
2645
|
+
result,
|
|
2646
|
+
};
|
|
2621
2647
|
dropZones.forEach((zone) => {
|
|
2622
|
-
if (nearZones != null && zone.onDragAway && nearZones.has(zone)) zone.onDragAway(
|
|
2648
|
+
if (dropEvent && nearZones != null && zone.onDragAway && nearZones.has(zone)) zone.onDragAway(dropEvent);
|
|
2623
2649
|
if (!dragStartedZones.has(zone)) return;
|
|
2624
|
-
if (zone.onDragEnd) zone.onDragEnd(
|
|
2650
|
+
if (zone.onDragEnd) zone.onDragEnd(endEvent);
|
|
2625
2651
|
});
|
|
2626
|
-
if (puppet.onDragEnd) puppet.onDragEnd(
|
|
2652
|
+
if (puppet.onDragEnd) puppet.onDragEnd(endEvent);
|
|
2653
|
+
//tear down the mouse/touch capture when cancelling; on a normal drop the capture ends itself
|
|
2654
|
+
if (cancelled && releaseCapture) releaseCapture();
|
|
2627
2655
|
nearZones = null;
|
|
2628
2656
|
activeZone = null;
|
|
2629
2657
|
puppet = null;
|
|
2630
2658
|
dragStartedZones = null;
|
|
2659
|
+
releaseCapture = null;
|
|
2631
2660
|
}
|
|
2632
2661
|
function getDragEvent(event, type) {
|
|
2633
2662
|
return {
|
|
@@ -8677,6 +8706,19 @@ class LookupField extends Field {
|
|
|
8677
8706
|
}
|
|
8678
8707
|
}
|
|
8679
8708
|
instance.lastDropdown = context.lastDropdown;
|
|
8709
|
+
if (this.validateOptionExists && isArray(data.options) && !this.isEmpty(data)) {
|
|
8710
|
+
let invalid = this.multiple
|
|
8711
|
+
? isArray(data.values) && data.records.length < data.values.length
|
|
8712
|
+
: !data.options.some(($option) =>
|
|
8713
|
+
areKeysEqual(
|
|
8714
|
+
getOptionKey(this.keyBindings, {
|
|
8715
|
+
$option,
|
|
8716
|
+
}),
|
|
8717
|
+
data.selectedKeys[0],
|
|
8718
|
+
),
|
|
8719
|
+
);
|
|
8720
|
+
if (invalid) data.error = this.invalidOptionText;
|
|
8721
|
+
}
|
|
8680
8722
|
super.prepareData(context, instance);
|
|
8681
8723
|
}
|
|
8682
8724
|
renderInput(context, instance, key) {
|
|
@@ -8742,6 +8784,8 @@ LookupField.prototype.hideSearchField = false;
|
|
|
8742
8784
|
LookupField.prototype.minOptionsForSearchField = 7;
|
|
8743
8785
|
LookupField.prototype.loadingText = "Loading...";
|
|
8744
8786
|
LookupField.prototype.queryErrorText = "Error occurred while querying for lookup data.";
|
|
8787
|
+
LookupField.prototype.validateOptionExists = false;
|
|
8788
|
+
LookupField.prototype.invalidOptionText = "The selected option is no longer available.";
|
|
8745
8789
|
LookupField.prototype.noResultsText = "No results found.";
|
|
8746
8790
|
LookupField.prototype.optionIdField = "id";
|
|
8747
8791
|
LookupField.prototype.optionTextField = "text";
|
|
@@ -9775,6 +9819,7 @@ let Input$1 = class Input extends VDOM.Component {
|
|
|
9775
9819
|
visited: state.visited,
|
|
9776
9820
|
focus: this.state.focus,
|
|
9777
9821
|
icon: !!icon,
|
|
9822
|
+
clear: insideButton != null,
|
|
9778
9823
|
empty: empty && !data.placeholder,
|
|
9779
9824
|
error: data.error && (state.visited || !suppressErrorsUntilVisited || !empty),
|
|
9780
9825
|
}),
|
|
@@ -15023,6 +15068,7 @@ class Grid extends ContainerBase {
|
|
|
15023
15068
|
sortOptions: this.sortOptions,
|
|
15024
15069
|
groupings: grouping,
|
|
15025
15070
|
preserveOrder: this.preserveGroupOrder,
|
|
15071
|
+
sortGroupsBySorters: this.sortGroups,
|
|
15026
15072
|
},
|
|
15027
15073
|
this.dataAdapter,
|
|
15028
15074
|
);
|
|
@@ -15600,12 +15646,13 @@ class Grid extends ContainerBase {
|
|
|
15600
15646
|
renderGroupHeader(context, instance, g, level, group, i, store, fixedColumns) {
|
|
15601
15647
|
let { CSS, baseClass } = this;
|
|
15602
15648
|
let data = store.getData();
|
|
15649
|
+
let resetRowNumbers = g.resetRowNumbers ? "reset-row-numbers" : null;
|
|
15603
15650
|
if (g.caption) {
|
|
15604
15651
|
let caption = g.caption(data);
|
|
15605
15652
|
return jsx(
|
|
15606
15653
|
"tbody",
|
|
15607
15654
|
{
|
|
15608
|
-
className: CSS.element(baseClass, "group-caption", ["level-" + level]),
|
|
15655
|
+
className: CSS.element(baseClass, "group-caption", ["level-" + level, resetRowNumbers]),
|
|
15609
15656
|
"data-group-key": group.$key,
|
|
15610
15657
|
"data-group-element": `group-caption-${level}`,
|
|
15611
15658
|
children: jsx("tr", {
|
|
@@ -15696,7 +15743,7 @@ class Grid extends ContainerBase {
|
|
|
15696
15743
|
return jsx(
|
|
15697
15744
|
"tbody",
|
|
15698
15745
|
{
|
|
15699
|
-
className: CSS.element(baseClass, "group-caption", ["level-" + level]),
|
|
15746
|
+
className: CSS.element(baseClass, "group-caption", ["level-" + level, resetRowNumbers]),
|
|
15700
15747
|
"data-group-key": group.$key,
|
|
15701
15748
|
"data-group-element": `group-caption-${level}`,
|
|
15702
15749
|
children: lines,
|
|
@@ -15967,6 +16014,7 @@ Grid.prototype.hoverChannel = "default";
|
|
|
15967
16014
|
Grid.prototype.focusable = null; // automatically resolved
|
|
15968
16015
|
Grid.prototype.allowsFileDrops = false;
|
|
15969
16016
|
Grid.prototype.preserveGroupOrder = false;
|
|
16017
|
+
Grid.prototype.sortGroups = false;
|
|
15970
16018
|
Widget.alias("grid", Grid);
|
|
15971
16019
|
Localization.registerPrototype("cx/widgets/Grid", Grid);
|
|
15972
16020
|
class GridComponent extends VDOM.Component {
|
package/package.json
CHANGED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import assert from "assert";
|
|
2
|
+
|
|
3
|
+
import { Binding } from "./Binding";
|
|
4
|
+
import { Store } from "./Store";
|
|
5
|
+
import { ExposedValueView } from "./ExposedValueView";
|
|
6
|
+
|
|
7
|
+
describe("ExposedValueView", () => {
|
|
8
|
+
// Mirrors how Sandbox wires it: a `storage` object keyed by a slot `key`,
|
|
9
|
+
// exposed to children under `recordName` (e.g. "$page").
|
|
10
|
+
const getView = (key = "~/ordering/new") => {
|
|
11
|
+
let store = new Store({
|
|
12
|
+
data: {
|
|
13
|
+
pages: {
|
|
14
|
+
"~/ordering/new": { description: "draft order", count: 3 },
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
let view = new ExposedValueView({
|
|
20
|
+
store,
|
|
21
|
+
containerBinding: Binding.get("pages"),
|
|
22
|
+
key,
|
|
23
|
+
recordName: "$page",
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
return { store, view };
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
it("exposes the keyed slot under the record name", () => {
|
|
30
|
+
let { view } = getView();
|
|
31
|
+
assert.equal(view.get("$page.description"), "draft order");
|
|
32
|
+
assert.equal(view.get("$page.count"), 3);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("writes back to the keyed slot on set", () => {
|
|
36
|
+
let { store, view } = getView();
|
|
37
|
+
view.set("$page.description", "edited");
|
|
38
|
+
assert.equal(store.get("pages")["~/ordering/new"].description, "edited");
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("deletes a property within the exposed record", () => {
|
|
42
|
+
let { store, view } = getView();
|
|
43
|
+
view.delete("$page.count");
|
|
44
|
+
assert.equal(view.get("$page.count"), undefined);
|
|
45
|
+
assert.ok(!store.get("pages")["~/ordering/new"].hasOwnProperty("count"));
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("deletes the entire exposed record (removes the keyed slot)", () => {
|
|
49
|
+
let { store, view } = getView();
|
|
50
|
+
view.delete("$page");
|
|
51
|
+
assert.equal(view.get("$page"), undefined);
|
|
52
|
+
assert.ok(
|
|
53
|
+
!store.get("pages").hasOwnProperty("~/ordering/new"),
|
|
54
|
+
"the keyed storage slot should be removed"
|
|
55
|
+
);
|
|
56
|
+
});
|
|
57
|
+
});
|
|
@@ -67,7 +67,7 @@ export class ExposedValueView extends View {
|
|
|
67
67
|
if (path == this.recordName) {
|
|
68
68
|
data = this.getData();
|
|
69
69
|
container = this.containerBinding.value(data);
|
|
70
|
-
if (!container || !container.hasOwnProperty(
|
|
70
|
+
if (!container || !container.hasOwnProperty(this.key)) return false;
|
|
71
71
|
newContainer = Object.assign({}, container);
|
|
72
72
|
delete newContainer[this.key];
|
|
73
73
|
this.store.set(this.containerBinding.path, newContainer);
|
package/src/data/comparer.ts
CHANGED
|
@@ -1,17 +1,16 @@
|
|
|
1
1
|
import { getSelector } from "./getSelector";
|
|
2
2
|
import { isDefined } from "../util/isDefined";
|
|
3
3
|
import { defaultCompare } from "./defaultCompare";
|
|
4
|
+
import type { CollatorOptions, Sorter } from "../ui/Prop";
|
|
4
5
|
|
|
5
|
-
interface Sorter {
|
|
6
|
-
value?: any;
|
|
7
|
-
field?: string;
|
|
8
|
-
direction?: string;
|
|
6
|
+
export interface ExtendedSorter extends Sorter {
|
|
9
7
|
comparer?: (a: any, b: any) => number;
|
|
10
8
|
compare?: (a: any, b: any) => number;
|
|
9
|
+
sortOptions?: CollatorOptions;
|
|
11
10
|
}
|
|
12
11
|
|
|
13
12
|
export function getComparer(
|
|
14
|
-
sorters:
|
|
13
|
+
sorters: ExtendedSorter[],
|
|
15
14
|
dataAccessor?: (x: any) => any,
|
|
16
15
|
comparer?: (a: any, b: any) => number,
|
|
17
16
|
): (a: any, b: any) => number {
|
|
@@ -51,7 +50,7 @@ export function getComparer(
|
|
|
51
50
|
}
|
|
52
51
|
|
|
53
52
|
export function indexSorter(
|
|
54
|
-
sorters:
|
|
53
|
+
sorters: ExtendedSorter[],
|
|
55
54
|
dataAccessor?: (x: any) => any,
|
|
56
55
|
compare?: (a: any, b: any) => number,
|
|
57
56
|
): (data: any[]) => number[] {
|
|
@@ -64,7 +63,7 @@ export function indexSorter(
|
|
|
64
63
|
}
|
|
65
64
|
|
|
66
65
|
export function sorter(
|
|
67
|
-
sorters:
|
|
66
|
+
sorters: ExtendedSorter[],
|
|
68
67
|
dataAccessor?: (x: any) => any,
|
|
69
68
|
compare?: (a: any, b: any) => number,
|
|
70
69
|
): (data: any[]) => any[] {
|
package/src/locale/de-de.ts
CHANGED
|
@@ -16,6 +16,7 @@ Localization.localize(c, "cx/widgets/LookupField", {
|
|
|
16
16
|
queryErrorText: "Bei der Abfrage der gesuchten Daten ist ein Felhler aufgetreten.",
|
|
17
17
|
noResultsText: "Keine Ergebnisse gefunden.",
|
|
18
18
|
minQueryLengthMessageText: "Geben Sie mindestens {0} Zeichen ein.",
|
|
19
|
+
invalidOptionText: "Die ausgewählte Option ist nicht mehr verfügbar.",
|
|
19
20
|
});
|
|
20
21
|
|
|
21
22
|
// In common for Calendar and MonthPicker
|
package/src/locale/en-us.ts
CHANGED
|
@@ -15,6 +15,7 @@ Localization.localize(c, "cx/widgets/LookupField", {
|
|
|
15
15
|
queryErrorText: "Error occurred while querying for lookup data.",
|
|
16
16
|
noResultsText: "No results found.",
|
|
17
17
|
minQueryLengthMessageText: "Type in at least {0} character(s).",
|
|
18
|
+
invalidOptionText: "The selected option is no longer available.",
|
|
18
19
|
});
|
|
19
20
|
|
|
20
21
|
// In common for Calendar and MonthPicker
|
package/src/locale/es-es.ts
CHANGED
|
@@ -16,6 +16,7 @@ Localization.localize(c, "cx/widgets/LookupField", {
|
|
|
16
16
|
queryErrorText: "Se produjo un error al consultar los datos de búsqueda.",
|
|
17
17
|
noResultsText: "No se han encontrado resultados.",
|
|
18
18
|
minQueryLengthMessageText: "Escriba al menos {0} caracteres.",
|
|
19
|
+
invalidOptionText: "La opción seleccionada ya no está disponible.",
|
|
19
20
|
});
|
|
20
21
|
|
|
21
22
|
// In common for Calendar and MonthPicker
|
package/src/locale/fr-fr.ts
CHANGED
|
@@ -16,6 +16,7 @@ Localization.localize(c, "cx/widgets/LookupField", {
|
|
|
16
16
|
queryErrorText: "Une erreur s'est produite lors de l'interrogation des données de recherche.",
|
|
17
17
|
noResultsText: "Aucun résultat trouvé.",
|
|
18
18
|
minQueryLengthMessageText: "Tapez au moins {0} caractère (s).",
|
|
19
|
+
invalidOptionText: "L'option sélectionnée n'est plus disponible.",
|
|
19
20
|
});
|
|
20
21
|
|
|
21
22
|
// In common for Calendar and MonthPicker
|
package/src/locale/nl-nl.ts
CHANGED
|
@@ -16,6 +16,7 @@ Localization.localize(c, "cx/widgets/LookupField", {
|
|
|
16
16
|
queryErrorText: "Er is een fout opgetreden bij het weergeven van gegevens.",
|
|
17
17
|
noResultsText: "Geen resultaten gevonden",
|
|
18
18
|
minQueryLengthMessageText: "Voer minimaal {0} tekens in.",
|
|
19
|
+
invalidOptionText: "De geselecteerde optie is niet meer beschikbaar.",
|
|
19
20
|
});
|
|
20
21
|
|
|
21
22
|
// In common for Calendar and MonthPicker
|
package/src/locale/pt-pt.ts
CHANGED
|
@@ -16,6 +16,7 @@ Localization.localize(c, "cx/widgets/LookupField", {
|
|
|
16
16
|
queryErrorText: "Ocorreu um erro ao consultar os dados de pesquisa.",
|
|
17
17
|
noResultsText: "Nenhum resultado encontrado.",
|
|
18
18
|
minQueryLengthMessageText: "Digite pelo menos {0} caractere(s).",
|
|
19
|
+
invalidOptionText: "A opção selecionada já não está disponível.",
|
|
19
20
|
});
|
|
20
21
|
|
|
21
22
|
// In common for Calendar and MonthPicker
|
package/src/locale/sr-latn-ba.ts
CHANGED
|
@@ -16,6 +16,7 @@ Localization.localize(c, "cx/widgets/LookupField", {
|
|
|
16
16
|
queryErrorText: "Došlo je do greške kod pribavljanja podataka za prikaz.",
|
|
17
17
|
noResultsText: "Rezultati nisu pronađeni.",
|
|
18
18
|
minQueryLengthMessageText: "Unesite najmanje {0} karakter(a).",
|
|
19
|
+
invalidOptionText: "Izabrana opcija više nije dostupna.",
|
|
19
20
|
});
|
|
20
21
|
|
|
21
22
|
// In common for Calendar and MonthPicker
|
package/src/ui/Prop.ts
CHANGED
|
@@ -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
|
});
|