cx 26.7.6 → 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 +17 -4
- package/dist/charts.js +156 -165
- package/dist/manifest.js +703 -700
- package/dist/widgets.js +14 -3
- 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 +28 -5
|
@@ -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;
|
|
@@ -944,9 +953,15 @@ export class Grid<T = unknown> extends ContainerBase<GridConfig<T>, GridInstance
|
|
|
944
953
|
if (sortField) {
|
|
945
954
|
for (let l = 0; l < 10; l++) {
|
|
946
955
|
let line = instance.row[`line${l}`];
|
|
947
|
-
let sortColumn =
|
|
956
|
+
let sortColumn =
|
|
957
|
+
line && line.columns && line.columns.find((c: any) => (c.sortField || c.field) == sortField);
|
|
948
958
|
if (sortColumn) {
|
|
949
|
-
|
|
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;
|
|
950
965
|
data.sorters[0].comparer = sortColumn.comparer;
|
|
951
966
|
data.sorters[0].sortOptions = sortColumn.sortOptions;
|
|
952
967
|
break;
|
|
@@ -1312,13 +1327,19 @@ export class Grid<T = unknown> extends ContainerBase<GridConfig<T>, GridInstance
|
|
|
1312
1327
|
mods.push("sortable");
|
|
1313
1328
|
let sorter = data.sorters && data.sorters[0];
|
|
1314
1329
|
let sortColumnField = hdwidget.sortField || hdwidget.field;
|
|
1315
|
-
let sortColumnValue = isDefined(hdwidget.sortValue)
|
|
1330
|
+
let sortColumnValue = isDefined(hdwidget.sortValue)
|
|
1331
|
+
? hdwidget.sortValue
|
|
1332
|
+
: hdwidget.sortField
|
|
1333
|
+
? undefined
|
|
1334
|
+
: hdwidget.value;
|
|
1316
1335
|
// a sort is identified by its (field, value selector) pair, so columns
|
|
1317
1336
|
// sorting by the same field through different value selectors don't both match
|
|
1318
1337
|
let sorted =
|
|
1319
1338
|
sorter &&
|
|
1320
1339
|
!!sorter.direction &&
|
|
1321
|
-
(sortColumnField
|
|
1340
|
+
(sortColumnField
|
|
1341
|
+
? sorter.field == sortColumnField
|
|
1342
|
+
: !sorter.field && isDefined(sortColumnValue)) &&
|
|
1322
1343
|
sorter.value === sortColumnValue;
|
|
1323
1344
|
if (sorted) {
|
|
1324
1345
|
mods.push("sorted-" + sorter.direction.toLowerCase());
|
|
@@ -1494,7 +1515,9 @@ export class Grid<T = unknown> extends ContainerBase<GridConfig<T>, GridInstance
|
|
|
1494
1515
|
let header = column.components[`header${headerLine + 1}`];
|
|
1495
1516
|
|
|
1496
1517
|
let field = column.sortField || column.field;
|
|
1497
|
-
|
|
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;
|
|
1498
1521
|
let comparer = column.comparer;
|
|
1499
1522
|
let sortOptions = column.sortOptions;
|
|
1500
1523
|
|