cx 26.7.5 → 26.8.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/charts/LineGraph.d.ts +8 -16
- package/build/charts/LineGraph.d.ts.map +1 -1
- package/build/charts/LineGraph.js +76 -69
- package/build/charts/axis/NumericAxis.d.ts.map +1 -1
- package/build/charts/axis/NumericAxis.js +1 -254
- package/build/charts/axis/NumericScale.d.ts +59 -0
- package/build/charts/axis/NumericScale.d.ts.map +1 -0
- package/build/charts/axis/NumericScale.js +254 -0
- package/build/widgets/grid/Grid.d.ts +9 -0
- package/build/widgets/grid/Grid.d.ts.map +1 -1
- package/build/widgets/grid/Grid.js +38 -12
- package/build/widgets/index.d.ts +15 -16
- package/build/widgets/index.d.ts.map +1 -1
- package/build/widgets/index.js +15 -17
- package/dist/charts.js +156 -165
- package/dist/manifest.js +730 -727
- package/dist/widgets.js +3159 -351
- package/package.json +1 -1
- package/src/charts/LineGraph.tsx +80 -98
- package/src/charts/axis/NumericAxis.spec.ts +40 -0
- package/src/charts/axis/NumericAxis.tsx +1 -290
- package/src/charts/axis/NumericScale.ts +290 -0
- package/src/widgets/grid/Grid.sorting.spec.tsx +188 -0
- package/src/widgets/grid/Grid.tsx +49 -14
- package/src/widgets/index.ts +41 -63
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
import { Stack } from "./Stack";
|
|
2
|
+
import { isNumber } from "../../util/isNumber";
|
|
3
|
+
import { Console } from "../../util/Console";
|
|
4
|
+
|
|
5
|
+
export class NumericScale {
|
|
6
|
+
min: number;
|
|
7
|
+
max: number;
|
|
8
|
+
snapToTicks: number;
|
|
9
|
+
tickDivisions: number[][];
|
|
10
|
+
minLabelDistance: number;
|
|
11
|
+
minLabelTickSize: number;
|
|
12
|
+
minTickDistance: number;
|
|
13
|
+
minTickStep: number;
|
|
14
|
+
tickSizes: number[];
|
|
15
|
+
normalized: boolean;
|
|
16
|
+
inverted: boolean;
|
|
17
|
+
minValue?: number;
|
|
18
|
+
maxValue?: number;
|
|
19
|
+
minValuePadded: number;
|
|
20
|
+
maxValuePadded: number;
|
|
21
|
+
stacks: Record<string, Stack>;
|
|
22
|
+
lowerDeadZone: number;
|
|
23
|
+
upperDeadZone: number;
|
|
24
|
+
origin: number;
|
|
25
|
+
scale: { factor: number; min: number; max: number; minPadding: number; maxPadding: number };
|
|
26
|
+
a: number;
|
|
27
|
+
b: number;
|
|
28
|
+
shouldUpdate: boolean;
|
|
29
|
+
|
|
30
|
+
reset(
|
|
31
|
+
min: number,
|
|
32
|
+
max: number,
|
|
33
|
+
snapToTicks: number,
|
|
34
|
+
tickDivisions: number[][],
|
|
35
|
+
minTickDistance: number,
|
|
36
|
+
minTickStep: number,
|
|
37
|
+
minLabelDistance: number,
|
|
38
|
+
minLabelTickSize: number,
|
|
39
|
+
normalized: boolean,
|
|
40
|
+
inverted: boolean,
|
|
41
|
+
lowerDeadZone: number,
|
|
42
|
+
upperDeadZone: number,
|
|
43
|
+
): void {
|
|
44
|
+
this.min = min;
|
|
45
|
+
this.max = max;
|
|
46
|
+
this.snapToTicks = snapToTicks;
|
|
47
|
+
this.tickDivisions = tickDivisions;
|
|
48
|
+
this.minLabelDistance = minLabelDistance;
|
|
49
|
+
this.minLabelTickSize = minLabelTickSize;
|
|
50
|
+
this.minTickDistance = minTickDistance;
|
|
51
|
+
this.minTickStep = minTickStep;
|
|
52
|
+
this.tickSizes = [];
|
|
53
|
+
this.normalized = normalized;
|
|
54
|
+
this.inverted = inverted;
|
|
55
|
+
delete this.minValue;
|
|
56
|
+
delete this.maxValue;
|
|
57
|
+
this.stacks = {};
|
|
58
|
+
this.lowerDeadZone = lowerDeadZone || 0;
|
|
59
|
+
this.upperDeadZone = upperDeadZone || 0;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
map(v: number, offset: number = 0): number {
|
|
63
|
+
return this.origin + (v + offset - this.scale.min + this.scale.minPadding) * this.scale.factor;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
decodeValue(n: number): number {
|
|
67
|
+
return n;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
encodeValue(v: number): number {
|
|
71
|
+
return v;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
constrainValue(v: number): number {
|
|
75
|
+
return Math.max(this.scale.min, Math.min(this.scale.max, v));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
trackValue(v: number, offset: number = 0, constrain: boolean = false): number {
|
|
79
|
+
let value = (v - this.origin) / this.scale.factor - offset + this.scale.min - this.scale.minPadding;
|
|
80
|
+
if (constrain) value = this.constrainValue(value);
|
|
81
|
+
return value;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
hash(): any {
|
|
85
|
+
let r: any = {
|
|
86
|
+
origin: this.origin,
|
|
87
|
+
factor: this.scale.factor,
|
|
88
|
+
min: this.scale.min,
|
|
89
|
+
max: this.scale.max,
|
|
90
|
+
minPadding: this.scale.minPadding,
|
|
91
|
+
maxPadding: this.scale.maxPadding,
|
|
92
|
+
};
|
|
93
|
+
r.stacks = Object.keys(this.stacks)
|
|
94
|
+
.map((s) => this.stacks[s].info?.join(","))
|
|
95
|
+
.join(":");
|
|
96
|
+
return r;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
isSame(x: any): boolean {
|
|
100
|
+
let hash = this.hash();
|
|
101
|
+
let same = x && !Object.keys(hash).some((k) => x[k] !== hash[k]);
|
|
102
|
+
this.shouldUpdate = !same;
|
|
103
|
+
return same;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
measure(a: number, b: number): void {
|
|
107
|
+
this.a = a;
|
|
108
|
+
this.b = b;
|
|
109
|
+
|
|
110
|
+
if (this.minValue != null && this.min == null) this.min = this.minValue;
|
|
111
|
+
if (this.maxValue != null && this.max == null) this.max = this.maxValue;
|
|
112
|
+
|
|
113
|
+
for (let s in this.stacks) {
|
|
114
|
+
let info = this.stacks[s].measure(this.normalized);
|
|
115
|
+
let [min, max] = info;
|
|
116
|
+
if (this.min == null || min < this.min) this.min = min;
|
|
117
|
+
if (this.max == null || max > this.max) this.max = max;
|
|
118
|
+
this.stacks[s].info = info;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (this.min == null) this.min = 0;
|
|
122
|
+
if (this.max == null) this.max = this.normalized ? 1 : 100;
|
|
123
|
+
|
|
124
|
+
if (this.min == this.max) {
|
|
125
|
+
if (this.min == 0) {
|
|
126
|
+
this.min = -1;
|
|
127
|
+
this.max = 1;
|
|
128
|
+
} else {
|
|
129
|
+
let delta = Math.abs(this.min) * 0.1;
|
|
130
|
+
this.min -= delta;
|
|
131
|
+
this.max += delta;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
this.origin = this.inverted ? this.b : this.a;
|
|
136
|
+
|
|
137
|
+
this.scale = this.getScale();
|
|
138
|
+
|
|
139
|
+
this.calculateTicks();
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
getScale(tickSizes?: number[]): { factor: number; min: number; max: number; minPadding: number; maxPadding: number } {
|
|
143
|
+
let { min, max } = this;
|
|
144
|
+
let smin = min;
|
|
145
|
+
let smax = max;
|
|
146
|
+
|
|
147
|
+
let tickSize;
|
|
148
|
+
if (tickSizes && isNumber(this.snapToTicks) && tickSizes.length > 0) {
|
|
149
|
+
tickSize = tickSizes[Math.min(tickSizes.length - 1, this.snapToTicks)];
|
|
150
|
+
smin = Math.floor(smin / tickSize) * tickSize;
|
|
151
|
+
smax = Math.ceil(smax / tickSize) * tickSize;
|
|
152
|
+
} else {
|
|
153
|
+
if (this.minValue === min) smin = this.minValuePadded;
|
|
154
|
+
if (this.maxValue === max) smax = this.maxValuePadded;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
let minPadding = this.minValue === min ? Math.max(0, smin - this.minValuePadded) : 0;
|
|
158
|
+
let maxPadding = this.maxValue === max ? Math.max(0, this.maxValuePadded - smax) : 0;
|
|
159
|
+
|
|
160
|
+
let sign = this.b > this.a ? 1 : -1;
|
|
161
|
+
|
|
162
|
+
let factor =
|
|
163
|
+
smin < smax
|
|
164
|
+
? (Math.abs(this.b - this.a) - this.lowerDeadZone - this.upperDeadZone) /
|
|
165
|
+
(smax - smin + minPadding + maxPadding)
|
|
166
|
+
: 0;
|
|
167
|
+
|
|
168
|
+
if (factor < 0) factor = 0;
|
|
169
|
+
|
|
170
|
+
if (factor > 0 && (this.lowerDeadZone > 0 || this.upperDeadZone > 0)) {
|
|
171
|
+
while (factor * (min - smin) < this.lowerDeadZone) smin -= this.lowerDeadZone / factor;
|
|
172
|
+
|
|
173
|
+
while (factor * (smax - max) < this.upperDeadZone) smax += this.upperDeadZone / factor;
|
|
174
|
+
|
|
175
|
+
if (tickSize! > 0 && isNumber(this.snapToTicks)) {
|
|
176
|
+
smin = Math.floor(smin / tickSize!) * tickSize!;
|
|
177
|
+
smax = Math.ceil(smax / tickSize!) * tickSize!;
|
|
178
|
+
minPadding = this.minValue === min ? Math.max(0, smin - this.minValuePadded) : 0;
|
|
179
|
+
maxPadding = this.maxValue === max ? Math.max(0, this.maxValuePadded - smax) : 0;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
factor = smin < smax ? Math.abs(this.b - this.a) / (smax - smin + minPadding + maxPadding) : 0;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return {
|
|
186
|
+
factor: sign * (this.inverted ? -factor : factor),
|
|
187
|
+
min: smin,
|
|
188
|
+
max: smax,
|
|
189
|
+
minPadding,
|
|
190
|
+
maxPadding,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
acknowledge(value: number, width: number = 0, offset: number = 0): void {
|
|
195
|
+
if (value == null) return;
|
|
196
|
+
|
|
197
|
+
if (this.minValue == null || value < this.minValue) {
|
|
198
|
+
this.minValue = value;
|
|
199
|
+
this.minValuePadded = value + offset - width / 2;
|
|
200
|
+
}
|
|
201
|
+
if (this.maxValue == null || value > this.maxValue) {
|
|
202
|
+
this.maxValue = value;
|
|
203
|
+
this.maxValuePadded = value + offset + width / 2;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
getStack(name: string): Stack {
|
|
208
|
+
let s = this.stacks[name];
|
|
209
|
+
if (!s) s = this.stacks[name] = new Stack();
|
|
210
|
+
return s;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
stacknowledge(name: string, ordinal: any, value: any): any {
|
|
214
|
+
return this.getStack(name).acknowledge(ordinal, value);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
stack(name: string, ordinal: any, value: any): number | null {
|
|
218
|
+
let v = this.getStack(name).stack(ordinal, value);
|
|
219
|
+
return v != null ? this.map(v) : null;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
findTickSize(minPxDist: number): number | undefined {
|
|
223
|
+
return this.tickSizes.find((a) => a >= this.minLabelTickSize && a * Math.abs(this.scale.factor) >= minPxDist);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
getTickSizes(): number[] {
|
|
227
|
+
return this.tickSizes;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
calculateTicks(): void {
|
|
231
|
+
let dist = this.minLabelDistance / Math.abs(this.scale.factor);
|
|
232
|
+
let unit = Math.pow(10, Math.floor(Math.log10(dist)));
|
|
233
|
+
|
|
234
|
+
let bestLabelDistance = Infinity;
|
|
235
|
+
let bestTicks: number[] = [];
|
|
236
|
+
let bestScale = this.scale;
|
|
237
|
+
|
|
238
|
+
for (let i = 0; i < this.tickDivisions.length; i++) {
|
|
239
|
+
let divs = this.tickDivisions[i];
|
|
240
|
+
let tickSizes = divs.filter((ts) => ts >= this.minTickStep).map((ts) => ts * unit);
|
|
241
|
+
let scale = this.getScale(tickSizes);
|
|
242
|
+
tickSizes.forEach((size, level) => {
|
|
243
|
+
let labelDistance = size * Math.abs(scale.factor);
|
|
244
|
+
if (labelDistance >= this.minLabelDistance && labelDistance < bestLabelDistance) {
|
|
245
|
+
bestScale = scale;
|
|
246
|
+
bestTicks = tickSizes;
|
|
247
|
+
bestLabelDistance = labelDistance;
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
this.scale = bestScale;
|
|
252
|
+
this.tickSizes = bestTicks.filter(
|
|
253
|
+
(ts) => ts >= this.minTickStep && ts * Math.abs(bestScale.factor) >= this.minTickDistance,
|
|
254
|
+
);
|
|
255
|
+
if (this.tickSizes.length > 0) {
|
|
256
|
+
let max = this.tickSizes[this.tickSizes.length - 1];
|
|
257
|
+
this.tickSizes.push(2 * max);
|
|
258
|
+
this.tickSizes.push(5 * max);
|
|
259
|
+
this.tickSizes.push(10 * max);
|
|
260
|
+
let min = this.tickSizes[0];
|
|
261
|
+
let minDist = min * Math.abs(bestScale.factor);
|
|
262
|
+
if (min / 10 >= this.minTickStep && minDist / 10 >= this.minTickDistance) this.tickSizes.splice(0, 0, min / 10);
|
|
263
|
+
else if (min / 5 >= this.minTickStep && minDist / 5 >= this.minTickDistance) this.tickSizes.splice(0, 0, min / 5);
|
|
264
|
+
else if (min / 2 >= this.minTickStep && minDist / 2 >= this.minTickDistance) this.tickSizes.splice(0, 0, min / 2);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
getTicks(tickSizes: number[]): number[][] {
|
|
269
|
+
return tickSizes.map((size) => {
|
|
270
|
+
let start = Math.ceil((this.scale.min - this.scale.minPadding) / size);
|
|
271
|
+
let end = Math.floor((this.scale.max + this.scale.maxPadding) / size);
|
|
272
|
+
let result: number[] = [];
|
|
273
|
+
for (let i = start; i <= end; i++) result.push(i * size + 0);
|
|
274
|
+
return result;
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
mapGridlines(): number[] {
|
|
279
|
+
let size = this.tickSizes[0];
|
|
280
|
+
let start = Math.ceil((this.scale.min - this.scale.minPadding) / size);
|
|
281
|
+
let end = Math.floor((this.scale.max + this.scale.maxPadding) / size);
|
|
282
|
+
let result: number[] = [];
|
|
283
|
+
for (let i = start; i <= end; i++) result.push(this.map(i * size));
|
|
284
|
+
return result;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
book(): void {
|
|
288
|
+
Console.warn("NumericAxis does not support the autoSize flag for column and bar graphs.");
|
|
289
|
+
}
|
|
290
|
+
}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import assert from "assert";
|
|
2
|
+
import { Store } from "../../data/Store";
|
|
3
|
+
import { Grid } from "./Grid";
|
|
4
|
+
import { bind } from "../../ui/bind";
|
|
5
|
+
import { computable } from "../../data/computable";
|
|
6
|
+
import { createTestRenderer, act } from "../../util/test/createTestRenderer";
|
|
7
|
+
|
|
8
|
+
// priorities 1, 2, 10 sort differently as numbers (1, 2, 10) than their
|
|
9
|
+
// "P{priority}" labels do as strings (P1, P10, P2), which makes the sort key
|
|
10
|
+
// precedence (sortValue > sortField > value > field) observable in the output
|
|
11
|
+
let records = [
|
|
12
|
+
{ id: 1, priority: 2 },
|
|
13
|
+
{ id: 2, priority: 10 },
|
|
14
|
+
{ id: 3, priority: 1 },
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
function textOf(node: any): string {
|
|
18
|
+
if (node == null) return "";
|
|
19
|
+
if (typeof node == "string") return node;
|
|
20
|
+
if (Array.isArray(node)) return node.map(textOf).join("");
|
|
21
|
+
return textOf(node.children);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function findAll(node: any, predicate: (el: any) => boolean, result: any[] = []): any[] {
|
|
25
|
+
if (node == null || typeof node == "string") return result;
|
|
26
|
+
if (Array.isArray(node)) {
|
|
27
|
+
node.forEach((child) => findAll(child, predicate, result));
|
|
28
|
+
return result;
|
|
29
|
+
}
|
|
30
|
+
if (predicate(node)) result.push(node);
|
|
31
|
+
findAll(node.children, predicate, result);
|
|
32
|
+
return result;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function getRenderedLabels(component: any): string[] {
|
|
36
|
+
let tds = findAll(component.toJSON(), (el) => el.type == "td");
|
|
37
|
+
return tds.map(textOf);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function clickHeader(component: any, text: string) {
|
|
41
|
+
let th = component.root.findAllByType("th").find((t: any) => thText(t) == text);
|
|
42
|
+
assert.ok(th, `Header cell "${text}" not found`);
|
|
43
|
+
await act(async () => {
|
|
44
|
+
th.props.onClick({ preventDefault() {}, stopPropagation() {} });
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function thText(th: any): string {
|
|
49
|
+
return th.children.map((c: any) => (typeof c == "string" ? c : "")).join("");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
describe("Grid sorting precedence", () => {
|
|
53
|
+
it("sorts by sortField instead of the displayed value when both are set", async () => {
|
|
54
|
+
let widget = (
|
|
55
|
+
<cx>
|
|
56
|
+
<Grid
|
|
57
|
+
records={bind("records")}
|
|
58
|
+
columns={[
|
|
59
|
+
{
|
|
60
|
+
header: "Priority",
|
|
61
|
+
sortField: "priority",
|
|
62
|
+
value: { tpl: "P{$record.priority}" },
|
|
63
|
+
sortable: true,
|
|
64
|
+
},
|
|
65
|
+
]}
|
|
66
|
+
/>
|
|
67
|
+
</cx>
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
let store = new Store({ data: { records } });
|
|
71
|
+
const component = await createTestRenderer(store, widget);
|
|
72
|
+
|
|
73
|
+
await clickHeader(component, "Priority");
|
|
74
|
+
assert.deepStrictEqual(getRenderedLabels(component), ["P1", "P2", "P10"]);
|
|
75
|
+
|
|
76
|
+
await clickHeader(component, "Priority");
|
|
77
|
+
assert.deepStrictEqual(getRenderedLabels(component), ["P10", "P2", "P1"]);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("sortValue takes precedence over sortField", async () => {
|
|
81
|
+
let widget = (
|
|
82
|
+
<cx>
|
|
83
|
+
<Grid
|
|
84
|
+
records={bind("records")}
|
|
85
|
+
columns={[
|
|
86
|
+
{
|
|
87
|
+
header: "Priority",
|
|
88
|
+
sortField: "priority",
|
|
89
|
+
sortValue: computable("$record.priority", (p: number) => -p),
|
|
90
|
+
value: { tpl: "P{$record.priority}" },
|
|
91
|
+
sortable: true,
|
|
92
|
+
},
|
|
93
|
+
]}
|
|
94
|
+
/>
|
|
95
|
+
</cx>
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
let store = new Store({ data: { records } });
|
|
99
|
+
const component = await createTestRenderer(store, widget);
|
|
100
|
+
|
|
101
|
+
await clickHeader(component, "Priority");
|
|
102
|
+
assert.deepStrictEqual(getRenderedLabels(component), ["P10", "P2", "P1"]);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("sorts by the displayed value when no sortField is set", async () => {
|
|
106
|
+
let widget = (
|
|
107
|
+
<cx>
|
|
108
|
+
<Grid
|
|
109
|
+
records={bind("records")}
|
|
110
|
+
columns={[
|
|
111
|
+
{
|
|
112
|
+
header: "Priority",
|
|
113
|
+
field: "priority",
|
|
114
|
+
value: { tpl: "P{$record.priority}" },
|
|
115
|
+
sortable: true,
|
|
116
|
+
},
|
|
117
|
+
]}
|
|
118
|
+
/>
|
|
119
|
+
</cx>
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
let store = new Store({ data: { records } });
|
|
123
|
+
const component = await createTestRenderer(store, widget);
|
|
124
|
+
|
|
125
|
+
await clickHeader(component, "Priority");
|
|
126
|
+
assert.deepStrictEqual(getRenderedLabels(component), ["P1", "P10", "P2"]);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it("applies sortField precedence to sorters restored from sortField/sortDirection bindings", async () => {
|
|
130
|
+
let widget = (
|
|
131
|
+
<cx>
|
|
132
|
+
<Grid
|
|
133
|
+
records={bind("records")}
|
|
134
|
+
sortField={bind("sortField")}
|
|
135
|
+
sortDirection={bind("sortDirection")}
|
|
136
|
+
columns={[
|
|
137
|
+
{
|
|
138
|
+
header: "Priority",
|
|
139
|
+
sortField: "priority",
|
|
140
|
+
value: { tpl: "P{$record.priority}" },
|
|
141
|
+
sortable: true,
|
|
142
|
+
},
|
|
143
|
+
]}
|
|
144
|
+
/>
|
|
145
|
+
</cx>
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
let store = new Store({
|
|
149
|
+
data: { records, sortField: "priority", sortDirection: "ASC" },
|
|
150
|
+
});
|
|
151
|
+
const component = await createTestRenderer(store, widget);
|
|
152
|
+
|
|
153
|
+
assert.deepStrictEqual(getRenderedLabels(component), ["P1", "P2", "P10"]);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it("marks only the clicked column as sorted", async () => {
|
|
157
|
+
let widget = (
|
|
158
|
+
<cx>
|
|
159
|
+
<Grid
|
|
160
|
+
records={bind("records")}
|
|
161
|
+
columns={[
|
|
162
|
+
{
|
|
163
|
+
header: "Priority",
|
|
164
|
+
sortField: "priority",
|
|
165
|
+
value: { tpl: "P{$record.priority}" },
|
|
166
|
+
sortable: true,
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
header: "Label",
|
|
170
|
+
value: { tpl: "P{$record.priority}" },
|
|
171
|
+
sortable: true,
|
|
172
|
+
},
|
|
173
|
+
]}
|
|
174
|
+
/>
|
|
175
|
+
</cx>
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
let store = new Store({ data: { records } });
|
|
179
|
+
const component = await createTestRenderer(store, widget);
|
|
180
|
+
|
|
181
|
+
await clickHeader(component, "Priority");
|
|
182
|
+
|
|
183
|
+
let ths = component.root.findAllByType("th");
|
|
184
|
+
let sorted = ths.filter((th: any) => (th.props.className || "").includes("sorted-asc"));
|
|
185
|
+
assert.strictEqual(sorted.length, 1);
|
|
186
|
+
assert.strictEqual(thText(sorted[0]), "Priority");
|
|
187
|
+
});
|
|
188
|
+
});
|
|
@@ -241,7 +241,16 @@ export interface GridColumnConfig {
|
|
|
241
241
|
children?: ChildNode | ChildNode[];
|
|
242
242
|
key?: string;
|
|
243
243
|
pad?: boolean;
|
|
244
|
+
/**
|
|
245
|
+
* Record field used for sorting instead of `field` or the displayed `value`.
|
|
246
|
+
* Use it when the column displays a computed value but should sort by raw data.
|
|
247
|
+
* Sort key precedence: `sortValue` > `sortField` > `value` > `field`.
|
|
248
|
+
*/
|
|
244
249
|
sortField?: string;
|
|
250
|
+
/**
|
|
251
|
+
* Selector (binding, template, expression or computable) used for sorting instead
|
|
252
|
+
* of `field`, `sortField` or the displayed `value`. Takes the highest precedence.
|
|
253
|
+
*/
|
|
245
254
|
sortValue?: Prop<any>;
|
|
246
255
|
style?: StyleProp;
|
|
247
256
|
trimWhitespace?: boolean;
|
|
@@ -919,10 +928,13 @@ export class Grid<T = unknown> extends ContainerBase<GridConfig<T>, GridInstance
|
|
|
919
928
|
|
|
920
929
|
let sortField = null;
|
|
921
930
|
|
|
922
|
-
|
|
931
|
+
// rebuild sorters from the sortField/sortDirection bindings only if a sort field
|
|
932
|
+
// is actually set; sorts identified by a value selector (columns without a field)
|
|
933
|
+
// live in data.sorters/state.sorters and cannot round-trip through a field name
|
|
934
|
+
if (isDefined(this.sortField) && isDefined(this.sortDirection) && data.sortField) {
|
|
923
935
|
let sorter = {
|
|
924
936
|
field: data.sortField,
|
|
925
|
-
direction: data.sortDirection,
|
|
937
|
+
direction: data.sortDirection || "ASC",
|
|
926
938
|
};
|
|
927
939
|
sortField = data.sortField;
|
|
928
940
|
data.sorters = [sorter];
|
|
@@ -939,11 +951,17 @@ export class Grid<T = unknown> extends ContainerBase<GridConfig<T>, GridInstance
|
|
|
939
951
|
}
|
|
940
952
|
|
|
941
953
|
if (sortField) {
|
|
942
|
-
for (let l =
|
|
954
|
+
for (let l = 0; l < 10; l++) {
|
|
943
955
|
let line = instance.row[`line${l}`];
|
|
944
|
-
let sortColumn =
|
|
956
|
+
let sortColumn =
|
|
957
|
+
line && line.columns && line.columns.find((c: any) => (c.sortField || c.field) == sortField);
|
|
945
958
|
if (sortColumn) {
|
|
946
|
-
|
|
959
|
+
// precedence: sortValue > sortField > value > field
|
|
960
|
+
data.sorters[0].value = isDefined(sortColumn.sortValue)
|
|
961
|
+
? sortColumn.sortValue
|
|
962
|
+
: sortColumn.sortField
|
|
963
|
+
? undefined
|
|
964
|
+
: sortColumn.value;
|
|
947
965
|
data.sorters[0].comparer = sortColumn.comparer;
|
|
948
966
|
data.sorters[0].sortOptions = sortColumn.sortOptions;
|
|
949
967
|
break;
|
|
@@ -1307,8 +1325,24 @@ export class Grid<T = unknown> extends ContainerBase<GridConfig<T>, GridInstance
|
|
|
1307
1325
|
|
|
1308
1326
|
if (hdwidget.sortable && header.widget.allowSorting) {
|
|
1309
1327
|
mods.push("sortable");
|
|
1310
|
-
|
|
1311
|
-
|
|
1328
|
+
let sorter = data.sorters && data.sorters[0];
|
|
1329
|
+
let sortColumnField = hdwidget.sortField || hdwidget.field;
|
|
1330
|
+
let sortColumnValue = isDefined(hdwidget.sortValue)
|
|
1331
|
+
? hdwidget.sortValue
|
|
1332
|
+
: hdwidget.sortField
|
|
1333
|
+
? undefined
|
|
1334
|
+
: hdwidget.value;
|
|
1335
|
+
// a sort is identified by its (field, value selector) pair, so columns
|
|
1336
|
+
// sorting by the same field through different value selectors don't both match
|
|
1337
|
+
let sorted =
|
|
1338
|
+
sorter &&
|
|
1339
|
+
!!sorter.direction &&
|
|
1340
|
+
(sortColumnField
|
|
1341
|
+
? sorter.field == sortColumnField
|
|
1342
|
+
: !sorter.field && isDefined(sortColumnValue)) &&
|
|
1343
|
+
sorter.value === sortColumnValue;
|
|
1344
|
+
if (sorted) {
|
|
1345
|
+
mods.push("sorted-" + sorter.direction.toLowerCase());
|
|
1312
1346
|
sortIcon = <DropDownIcon className={CSS.element(baseClass, "column-sort-icon")} />;
|
|
1313
1347
|
}
|
|
1314
1348
|
}
|
|
@@ -1481,17 +1515,18 @@ export class Grid<T = unknown> extends ContainerBase<GridConfig<T>, GridInstance
|
|
|
1481
1515
|
let header = column.components[`header${headerLine + 1}`];
|
|
1482
1516
|
|
|
1483
1517
|
let field = column.sortField || column.field;
|
|
1484
|
-
|
|
1518
|
+
// precedence: sortValue > sortField > value > field; the comparer prefers value
|
|
1519
|
+
// over field, so value must not be attached when an explicit sortField is set
|
|
1520
|
+
let value = isDefined(column.sortValue) ? column.sortValue : column.sortField ? undefined : column.value;
|
|
1485
1521
|
let comparer = column.comparer;
|
|
1486
1522
|
let sortOptions = column.sortOptions;
|
|
1487
1523
|
|
|
1488
|
-
if (header && header.allowSorting && column.sortable && (field || value
|
|
1524
|
+
if (header && header.allowSorting && column.sortable && (field || isDefined(value))) {
|
|
1489
1525
|
let direction = column.primarySortDirection ?? "ASC";
|
|
1490
|
-
if
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
) {
|
|
1526
|
+
// the column matches the active sorter only if both the field and the value
|
|
1527
|
+
// selector are the same; two columns may sort by the same field through
|
|
1528
|
+
// different value selectors and represent different sorts
|
|
1529
|
+
if (isNonEmptyArray(data.sorters) && data.sorters[0].field == field && data.sorters[0].value === value) {
|
|
1495
1530
|
if (data.sorters[0].direction == "ASC" && (!this.clearableSort || direction == "ASC")) direction = "DESC";
|
|
1496
1531
|
else if (data.sorters[0].direction == "DESC" && (!this.clearableSort || direction == "DESC"))
|
|
1497
1532
|
direction = "ASC";
|