cx 26.4.4 → 26.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/charts/axis/Axis.d.ts +8 -0
- package/build/charts/axis/Axis.d.ts.map +1 -1
- package/build/charts/axis/Axis.js +18 -1
- package/build/charts/axis/TimeAxis.js +1 -0
- package/build/ui/Format.d.ts.map +1 -1
- package/build/ui/Format.js +26 -2
- package/build/util/Format.d.ts.map +1 -1
- package/build/util/Format.js +6 -0
- package/build/util/date/dateQuarter.d.ts +7 -0
- package/build/util/date/dateQuarter.d.ts.map +1 -0
- package/build/util/date/dateQuarter.js +8 -0
- package/build/util/date/dayBefore.d.ts +12 -0
- package/build/util/date/dayBefore.d.ts.map +1 -0
- package/build/util/date/dayBefore.js +15 -0
- package/build/util/date/index.d.ts +2 -0
- package/build/util/date/index.d.ts.map +1 -1
- package/build/util/date/index.js +2 -0
- package/build/widgets/form/DateTimePicker.d.ts.map +1 -1
- package/build/widgets/form/DateTimePicker.js +53 -31
- package/build/widgets/form/Field.d.ts.map +1 -1
- package/build/widgets/form/Field.js +2 -1
- package/build/widgets/form/Wheel.d.ts +8 -0
- package/build/widgets/form/Wheel.d.ts.map +1 -1
- package/build/widgets/form/Wheel.js +30 -7
- package/build/widgets/grid/Grid.d.ts +1 -1
- package/build/widgets/grid/Grid.d.ts.map +1 -1
- package/dist/charts.css +6 -0
- package/dist/charts.js +18 -1
- package/dist/manifest.js +787 -778
- package/dist/ui.js +33 -1
- package/dist/util.js +32 -0
- package/dist/widgets.js +225 -173
- package/package.json +1 -1
- package/src/charts/BarGraph.scss +31 -31
- package/src/charts/Legend.scss +57 -57
- package/src/charts/LegendEntry.scss +35 -35
- package/src/charts/LineGraph.scss +28 -28
- package/src/charts/RangeMarker.scss +3 -0
- package/src/charts/axis/Axis.tsx +31 -1
- package/src/charts/axis/TimeAxis.tsx +1 -0
- package/src/charts/helpers/SnapPointFinder.ts +136 -136
- package/src/charts/helpers/ValueAtFinder.ts +72 -72
- package/src/charts/index.scss +1 -0
- package/src/data/AugmentedViewBase.ts +89 -89
- package/src/data/View.ts +301 -301
- package/src/data/createAccessorModelProxy.ts +66 -66
- package/src/ui/Format.spec.ts +32 -0
- package/src/ui/Format.ts +27 -2
- package/src/ui/Repeater.spec.tsx +181 -181
- package/src/util/Format.spec.ts +11 -0
- package/src/util/Format.ts +7 -0
- package/src/util/date/dateQuarter.ts +8 -0
- package/src/util/date/dayBefore.ts +15 -0
- package/src/util/date/index.ts +2 -0
- package/src/util/scss/include.scss +69 -69
- package/src/widgets/Button.maps.scss +103 -103
- package/src/widgets/form/Calendar.tsx +772 -772
- package/src/widgets/form/DateTimePicker.tsx +453 -392
- package/src/widgets/form/Field.tsx +2 -1
- package/src/widgets/form/ValidationGroup.spec.tsx +30 -1
- package/src/widgets/form/Wheel.tsx +36 -7
- package/src/widgets/grid/Grid.scss +657 -657
- package/src/widgets/grid/Grid.tsx +1 -1
- package/src/widgets/grid/variables.scss +47 -47
- package/src/widgets/index.ts +63 -63
- package/src/widgets/nav/MenuItem.scss +150 -150
- package/src/widgets/nav/Tab.ts +122 -122
- package/src/widgets/overlay/Overlay.tsx +1029 -1029
- package/src/widgets/variables.scss +61 -61
|
@@ -1,66 +1,66 @@
|
|
|
1
|
-
// Homomorphic mapped type preserves Go-to-Definition in IDEs
|
|
2
|
-
// -? strips optionality, as clause filters conflicting method names
|
|
3
|
-
type AccessorChainMap<M> = {
|
|
4
|
-
[K in keyof M as Exclude<K, "toString" | "valueOf" | "nameOf">]-?: AccessorChain<M[K]>;
|
|
5
|
-
};
|
|
6
|
-
|
|
7
|
-
// Check if a type is `any` using the intersection trick
|
|
8
|
-
type IsAny<T> = 0 extends 1 & T ? true : false;
|
|
9
|
-
|
|
10
|
-
export type AccessorChain<M> = (IsAny<M> extends true
|
|
11
|
-
? { [key: string]: any } // Allow any property access for `any` type
|
|
12
|
-
: [M] extends [object]
|
|
13
|
-
? AccessorChainMap<M> // Direct mapping preserves IDE navigation
|
|
14
|
-
: [NonNullable<M>] extends [object]
|
|
15
|
-
? AccessorChainMap<NonNullable<M>> // Fallback for nullable types (e.g. optional properties)
|
|
16
|
-
: {}) & {
|
|
17
|
-
toString(): string;
|
|
18
|
-
valueOf(): M | undefined;
|
|
19
|
-
nameOf(): string;
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
const emptyFn = () => {};
|
|
23
|
-
|
|
24
|
-
export function createAccessorModelProxy<M>(chain: string = ""): AccessorChain<M> {
|
|
25
|
-
let lastOp: string | null = null;
|
|
26
|
-
|
|
27
|
-
const proxy = new Proxy(emptyFn, {
|
|
28
|
-
get: (_, name: string | symbol) => {
|
|
29
|
-
if (typeof name !== "string") return proxy;
|
|
30
|
-
|
|
31
|
-
switch (name) {
|
|
32
|
-
case "isAccessorChain":
|
|
33
|
-
return true;
|
|
34
|
-
|
|
35
|
-
case "toString":
|
|
36
|
-
case "valueOf":
|
|
37
|
-
case "nameOf":
|
|
38
|
-
lastOp = name;
|
|
39
|
-
return proxy;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
let newChain = chain;
|
|
43
|
-
if (newChain.length > 0) newChain += ".";
|
|
44
|
-
newChain += name;
|
|
45
|
-
return createAccessorModelProxy(newChain);
|
|
46
|
-
},
|
|
47
|
-
|
|
48
|
-
apply(): string {
|
|
49
|
-
switch (lastOp) {
|
|
50
|
-
case "nameOf":
|
|
51
|
-
const lastDotIndex = chain.lastIndexOf(".");
|
|
52
|
-
return lastDotIndex > 0 ? chain.substring(lastDotIndex + 1) : chain;
|
|
53
|
-
|
|
54
|
-
default:
|
|
55
|
-
return chain;
|
|
56
|
-
}
|
|
57
|
-
},
|
|
58
|
-
});
|
|
59
|
-
return proxy as unknown as AccessorChain<M>;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
export const createModel = createAccessorModelProxy;
|
|
63
|
-
|
|
64
|
-
export function isAccessorChain<M>(value: unknown): value is AccessorChain<M> {
|
|
65
|
-
return value != null && !!(value as any).isAccessorChain;
|
|
66
|
-
}
|
|
1
|
+
// Homomorphic mapped type preserves Go-to-Definition in IDEs
|
|
2
|
+
// -? strips optionality, as clause filters conflicting method names
|
|
3
|
+
type AccessorChainMap<M> = {
|
|
4
|
+
[K in keyof M as Exclude<K, "toString" | "valueOf" | "nameOf">]-?: AccessorChain<M[K]>;
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
// Check if a type is `any` using the intersection trick
|
|
8
|
+
type IsAny<T> = 0 extends 1 & T ? true : false;
|
|
9
|
+
|
|
10
|
+
export type AccessorChain<M> = (IsAny<M> extends true
|
|
11
|
+
? { [key: string]: any } // Allow any property access for `any` type
|
|
12
|
+
: [M] extends [object]
|
|
13
|
+
? AccessorChainMap<M> // Direct mapping preserves IDE navigation
|
|
14
|
+
: [NonNullable<M>] extends [object]
|
|
15
|
+
? AccessorChainMap<NonNullable<M>> // Fallback for nullable types (e.g. optional properties)
|
|
16
|
+
: {}) & {
|
|
17
|
+
toString(): string;
|
|
18
|
+
valueOf(): M | undefined;
|
|
19
|
+
nameOf(): string;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const emptyFn = () => {};
|
|
23
|
+
|
|
24
|
+
export function createAccessorModelProxy<M>(chain: string = ""): AccessorChain<M> {
|
|
25
|
+
let lastOp: string | null = null;
|
|
26
|
+
|
|
27
|
+
const proxy = new Proxy(emptyFn, {
|
|
28
|
+
get: (_, name: string | symbol) => {
|
|
29
|
+
if (typeof name !== "string") return proxy;
|
|
30
|
+
|
|
31
|
+
switch (name) {
|
|
32
|
+
case "isAccessorChain":
|
|
33
|
+
return true;
|
|
34
|
+
|
|
35
|
+
case "toString":
|
|
36
|
+
case "valueOf":
|
|
37
|
+
case "nameOf":
|
|
38
|
+
lastOp = name;
|
|
39
|
+
return proxy;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
let newChain = chain;
|
|
43
|
+
if (newChain.length > 0) newChain += ".";
|
|
44
|
+
newChain += name;
|
|
45
|
+
return createAccessorModelProxy(newChain);
|
|
46
|
+
},
|
|
47
|
+
|
|
48
|
+
apply(): string {
|
|
49
|
+
switch (lastOp) {
|
|
50
|
+
case "nameOf":
|
|
51
|
+
const lastDotIndex = chain.lastIndexOf(".");
|
|
52
|
+
return lastDotIndex > 0 ? chain.substring(lastDotIndex + 1) : chain;
|
|
53
|
+
|
|
54
|
+
default:
|
|
55
|
+
return chain;
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
return proxy as unknown as AccessorChain<M>;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export const createModel = createAccessorModelProxy;
|
|
63
|
+
|
|
64
|
+
export function isAccessorChain<M>(value: unknown): value is AccessorChain<M> {
|
|
65
|
+
return value != null && !!(value as any).isAccessorChain;
|
|
66
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import assert from "assert";
|
|
2
|
+
import { Format } from "./Format";
|
|
3
|
+
|
|
4
|
+
// The `quarter` formatter is registered eagerly when ui/Format is imported,
|
|
5
|
+
// so these tests do not need enableCultureSensitiveFormatting().
|
|
6
|
+
describe("Format - quarter", function () {
|
|
7
|
+
it("renders the calendar quarter with the default pattern", function () {
|
|
8
|
+
assert.equal(Format.value(new Date(2020, 0, 15), "quarter"), "Q1 2020");
|
|
9
|
+
assert.equal(Format.value(new Date(2020, 5, 1), "quarter"), "Q2 2020");
|
|
10
|
+
assert.equal(Format.value(new Date(2020, 11, 31), "quarter"), "Q4 2020");
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it("supports custom patterns and the {yy} token", function () {
|
|
14
|
+
assert.equal(Format.value(new Date(2020, 0, 1), "quarter;{yy}Q{q}"), "20Q1");
|
|
15
|
+
assert.equal(Format.value(new Date(2020, 8, 1), "quarter;{q}Q{yyyy}"), "3Q2020");
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("accepts both lowercase and uppercase placeholders", function () {
|
|
19
|
+
assert.equal(Format.value(new Date(2020, 0, 1), "quarter;{YY}Q{Q}"), "20Q1");
|
|
20
|
+
assert.equal(Format.value(new Date(2020, 0, 1), "quarter;Q{Q} {YYYY}"), "Q1 2020");
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("treats the input as an exclusive range end with the exclusive flag", function () {
|
|
24
|
+
assert.equal(Format.value(new Date(2021, 0, 1), "quarter;{yy}Q{q};exclusive"), "20Q4");
|
|
25
|
+
assert.equal(Format.value(new Date(2021, 0, 1), "quarter;{yy}Q{q};ex"), "20Q4");
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("falls back to the default pattern when the pattern argument is empty (quarter;;e)", function () {
|
|
29
|
+
assert.equal(Format.value(new Date(2021, 0, 1), "quarter;;e"), "Q4 2020");
|
|
30
|
+
assert.equal(Format.value(new Date(2020, 0, 1), "quarter;;e"), "Q4 2019");
|
|
31
|
+
});
|
|
32
|
+
});
|
package/src/ui/Format.ts
CHANGED
|
@@ -1,12 +1,31 @@
|
|
|
1
1
|
import { Culture, getCurrentCultureCache } from "./Culture";
|
|
2
2
|
import { Format as Fmt, resolveMinMaxFractionDigits, setGetFormatCacheCallback } from "../util/Format";
|
|
3
3
|
import { setGetExpressionCacheCallback } from "../data/Expression";
|
|
4
|
-
import { setGetStringTemplateCacheCallback } from "../data/StringTemplate";
|
|
5
|
-
import { parseDateInvariant } from "../util";
|
|
4
|
+
import { setGetStringTemplateCacheCallback, StringTemplate } from "../data/StringTemplate";
|
|
5
|
+
import { dateQuarter, dayBefore, parseDateInvariant } from "../util";
|
|
6
6
|
import { GlobalCacheIdentifier } from "../util/GlobalCacheIdentifier";
|
|
7
7
|
|
|
8
8
|
export const Format = Fmt;
|
|
9
9
|
|
|
10
|
+
// The `quarter` formatter renders a calendar quarter via a string-template
|
|
11
|
+
// pattern with `{q}` (quarter number), `{yyyy}` and `{yy}` (year) placeholders.
|
|
12
|
+
// Placeholders are case-insensitive (`{Q}`, `{YYYY}`, `{YY}` work too). It lives
|
|
13
|
+
// here rather than in util/Format because it depends on StringTemplate from the
|
|
14
|
+
// data layer, which util/ cannot import. It is registered eagerly since it does
|
|
15
|
+
// not depend on culture settings.
|
|
16
|
+
Fmt.registerFactory("quarter", (fmt: any, pattern?: string, mode?: string) => {
|
|
17
|
+
let exclusive = mode === "exclusive" || mode === "ex" || mode === "e";
|
|
18
|
+
let template = StringTemplate.get(pattern || "Q{q} {yyyy}");
|
|
19
|
+
return (value: any) => {
|
|
20
|
+
let date = parseDateInvariant(value);
|
|
21
|
+
if (exclusive) date = dayBefore(date);
|
|
22
|
+
let q = dateQuarter(date);
|
|
23
|
+
let yyyy = date.getFullYear();
|
|
24
|
+
let yy = String(yyyy % 100).padStart(2, "0");
|
|
25
|
+
return template({ q, Q: q, yyyy, YYYY: yyyy, yy, YY: yy });
|
|
26
|
+
};
|
|
27
|
+
});
|
|
28
|
+
|
|
10
29
|
let cultureSensitiveFormatsRegistered = false;
|
|
11
30
|
|
|
12
31
|
export function resolveNumberFormattingFlags(flags?: string): any {
|
|
@@ -86,6 +105,12 @@ export function enableCultureSensitiveFormatting() {
|
|
|
86
105
|
return (value: any) => formatter.format(parseDateInvariant(value));
|
|
87
106
|
});
|
|
88
107
|
|
|
108
|
+
Fmt.registerFactory(["dayBefore", "daybefore"], (fmt: any, format = "yyyyMd hhmm") => {
|
|
109
|
+
let culture = Culture.getDateTimeCulture();
|
|
110
|
+
let formatter = culture.getFormatter(format);
|
|
111
|
+
return (value: any) => formatter.format(dayBefore(parseDateInvariant(value)));
|
|
112
|
+
});
|
|
113
|
+
|
|
89
114
|
setGetFormatCacheCallback(() => {
|
|
90
115
|
let cache = getCurrentCultureCache();
|
|
91
116
|
if (!cache.formatCache) cache.formatCache = {};
|
package/src/ui/Repeater.spec.tsx
CHANGED
|
@@ -1,181 +1,181 @@
|
|
|
1
|
-
import { Store } from "../data/Store";
|
|
2
|
-
import { Repeater } from "./Repeater";
|
|
3
|
-
import { bind } from "./bind";
|
|
4
|
-
import { createTestRenderer, act } from "../util/test/createTestRenderer";
|
|
5
|
-
import { createAccessorModelProxy } from "../data/createAccessorModelProxy";
|
|
6
|
-
|
|
7
|
-
import assert from "assert";
|
|
8
|
-
|
|
9
|
-
describe("Repeater", () => {
|
|
10
|
-
it("allows sorting", async () => {
|
|
11
|
-
let data = [
|
|
12
|
-
{
|
|
13
|
-
value: "C",
|
|
14
|
-
},
|
|
15
|
-
{
|
|
16
|
-
value: "B",
|
|
17
|
-
},
|
|
18
|
-
{
|
|
19
|
-
value: "A",
|
|
20
|
-
},
|
|
21
|
-
];
|
|
22
|
-
|
|
23
|
-
let widget = (
|
|
24
|
-
<cx>
|
|
25
|
-
<div>
|
|
26
|
-
<Repeater records={data} sorters={[{ field: "value", direction: "ASC" }]} recordAlias="$item">
|
|
27
|
-
<div text={bind("$item.value")} />
|
|
28
|
-
</Repeater>
|
|
29
|
-
</div>
|
|
30
|
-
</cx>
|
|
31
|
-
);
|
|
32
|
-
|
|
33
|
-
let store = new Store();
|
|
34
|
-
|
|
35
|
-
const component = await createTestRenderer(store, widget);
|
|
36
|
-
|
|
37
|
-
let tree = component.toJSON();
|
|
38
|
-
assert.deepEqual(tree, {
|
|
39
|
-
type: "div",
|
|
40
|
-
props: {},
|
|
41
|
-
children: [
|
|
42
|
-
{
|
|
43
|
-
type: "div",
|
|
44
|
-
props: {},
|
|
45
|
-
children: ["A"],
|
|
46
|
-
},
|
|
47
|
-
{
|
|
48
|
-
type: "div",
|
|
49
|
-
props: {},
|
|
50
|
-
children: ["B"],
|
|
51
|
-
},
|
|
52
|
-
{
|
|
53
|
-
type: "div",
|
|
54
|
-
props: {},
|
|
55
|
-
children: ["C"],
|
|
56
|
-
},
|
|
57
|
-
],
|
|
58
|
-
});
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
it("changes are properly updated", async () => {
|
|
62
|
-
let divInstances: any[] = [];
|
|
63
|
-
let widget = (
|
|
64
|
-
<cx>
|
|
65
|
-
<div>
|
|
66
|
-
<Repeater records={bind("data")} sorters={[{ field: "value", direction: "ASC" }]} recordAlias="$item">
|
|
67
|
-
<div
|
|
68
|
-
text={bind("$item.value")}
|
|
69
|
-
onExplore={(context, instance) => {
|
|
70
|
-
divInstances.push(instance);
|
|
71
|
-
}}
|
|
72
|
-
/>
|
|
73
|
-
</Repeater>
|
|
74
|
-
</div>
|
|
75
|
-
</cx>
|
|
76
|
-
);
|
|
77
|
-
|
|
78
|
-
let store = new Store({
|
|
79
|
-
data: {
|
|
80
|
-
data: [
|
|
81
|
-
{
|
|
82
|
-
value: "C",
|
|
83
|
-
},
|
|
84
|
-
{
|
|
85
|
-
value: "B",
|
|
86
|
-
},
|
|
87
|
-
],
|
|
88
|
-
},
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
const component = await createTestRenderer(store, widget);
|
|
92
|
-
|
|
93
|
-
let tree = component.toJSON();
|
|
94
|
-
assert.deepEqual(tree, {
|
|
95
|
-
type: "div",
|
|
96
|
-
props: {},
|
|
97
|
-
children: [
|
|
98
|
-
{
|
|
99
|
-
type: "div",
|
|
100
|
-
props: {},
|
|
101
|
-
children: ["B"],
|
|
102
|
-
},
|
|
103
|
-
{
|
|
104
|
-
type: "div",
|
|
105
|
-
props: {},
|
|
106
|
-
children: ["C"],
|
|
107
|
-
},
|
|
108
|
-
],
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
divInstances = [];
|
|
112
|
-
|
|
113
|
-
await act(async () => {
|
|
114
|
-
store.update("data", (data) => [{ value: "A" }, ...data]);
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
assert.deepEqual(component.toJSON(), {
|
|
118
|
-
type: "div",
|
|
119
|
-
props: {},
|
|
120
|
-
children: [
|
|
121
|
-
{
|
|
122
|
-
type: "div",
|
|
123
|
-
props: {},
|
|
124
|
-
children: ["A"],
|
|
125
|
-
},
|
|
126
|
-
{
|
|
127
|
-
type: "div",
|
|
128
|
-
props: {},
|
|
129
|
-
children: ["B"],
|
|
130
|
-
},
|
|
131
|
-
{
|
|
132
|
-
type: "div",
|
|
133
|
-
props: {},
|
|
134
|
-
children: ["C"],
|
|
135
|
-
},
|
|
136
|
-
],
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
assert.equal(divInstances.length, 3);
|
|
140
|
-
assert.equal(divInstances[0].store.get("$item.value"), "A");
|
|
141
|
-
assert.equal(divInstances[1].store.get("$item.value"), "B");
|
|
142
|
-
assert.equal(divInstances[2].store.get("$item.value"), "C");
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
it("infers T from AccessorChain<T[]> for onCreateFilter callback", () => {
|
|
146
|
-
interface Item {
|
|
147
|
-
name: string;
|
|
148
|
-
active: boolean;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
interface AppModel {
|
|
152
|
-
items: Item[];
|
|
153
|
-
$item: Item;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
const m = createAccessorModelProxy<AppModel>();
|
|
157
|
-
|
|
158
|
-
// onCreateFilter should receive (record: Item) => boolean when T is inferred from AccessorChain<Item[]>
|
|
159
|
-
const widget = (
|
|
160
|
-
<cx>
|
|
161
|
-
<div>
|
|
162
|
-
<Repeater
|
|
163
|
-
records={m.items}
|
|
164
|
-
recordAlias={m.$item}
|
|
165
|
-
onCreateFilter={() => (record) => {
|
|
166
|
-
// If T is correctly inferred as Item, record.name should be string
|
|
167
|
-
const name: string = record.name;
|
|
168
|
-
// @ts-expect-error - record.name should be string, not number
|
|
169
|
-
const wrong: number = record.name;
|
|
170
|
-
return record.active;
|
|
171
|
-
}}
|
|
172
|
-
>
|
|
173
|
-
<div text={m.$item.name} />
|
|
174
|
-
</Repeater>
|
|
175
|
-
</div>
|
|
176
|
-
</cx>
|
|
177
|
-
);
|
|
178
|
-
|
|
179
|
-
assert.ok(widget);
|
|
180
|
-
});
|
|
181
|
-
});
|
|
1
|
+
import { Store } from "../data/Store";
|
|
2
|
+
import { Repeater } from "./Repeater";
|
|
3
|
+
import { bind } from "./bind";
|
|
4
|
+
import { createTestRenderer, act } from "../util/test/createTestRenderer";
|
|
5
|
+
import { createAccessorModelProxy } from "../data/createAccessorModelProxy";
|
|
6
|
+
|
|
7
|
+
import assert from "assert";
|
|
8
|
+
|
|
9
|
+
describe("Repeater", () => {
|
|
10
|
+
it("allows sorting", async () => {
|
|
11
|
+
let data = [
|
|
12
|
+
{
|
|
13
|
+
value: "C",
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
value: "B",
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
value: "A",
|
|
20
|
+
},
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
let widget = (
|
|
24
|
+
<cx>
|
|
25
|
+
<div>
|
|
26
|
+
<Repeater records={data} sorters={[{ field: "value", direction: "ASC" }]} recordAlias="$item">
|
|
27
|
+
<div text={bind("$item.value")} />
|
|
28
|
+
</Repeater>
|
|
29
|
+
</div>
|
|
30
|
+
</cx>
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
let store = new Store();
|
|
34
|
+
|
|
35
|
+
const component = await createTestRenderer(store, widget);
|
|
36
|
+
|
|
37
|
+
let tree = component.toJSON();
|
|
38
|
+
assert.deepEqual(tree, {
|
|
39
|
+
type: "div",
|
|
40
|
+
props: {},
|
|
41
|
+
children: [
|
|
42
|
+
{
|
|
43
|
+
type: "div",
|
|
44
|
+
props: {},
|
|
45
|
+
children: ["A"],
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
type: "div",
|
|
49
|
+
props: {},
|
|
50
|
+
children: ["B"],
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
type: "div",
|
|
54
|
+
props: {},
|
|
55
|
+
children: ["C"],
|
|
56
|
+
},
|
|
57
|
+
],
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("changes are properly updated", async () => {
|
|
62
|
+
let divInstances: any[] = [];
|
|
63
|
+
let widget = (
|
|
64
|
+
<cx>
|
|
65
|
+
<div>
|
|
66
|
+
<Repeater records={bind("data")} sorters={[{ field: "value", direction: "ASC" }]} recordAlias="$item">
|
|
67
|
+
<div
|
|
68
|
+
text={bind("$item.value")}
|
|
69
|
+
onExplore={(context, instance) => {
|
|
70
|
+
divInstances.push(instance);
|
|
71
|
+
}}
|
|
72
|
+
/>
|
|
73
|
+
</Repeater>
|
|
74
|
+
</div>
|
|
75
|
+
</cx>
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
let store = new Store({
|
|
79
|
+
data: {
|
|
80
|
+
data: [
|
|
81
|
+
{
|
|
82
|
+
value: "C",
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
value: "B",
|
|
86
|
+
},
|
|
87
|
+
],
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
const component = await createTestRenderer(store, widget);
|
|
92
|
+
|
|
93
|
+
let tree = component.toJSON();
|
|
94
|
+
assert.deepEqual(tree, {
|
|
95
|
+
type: "div",
|
|
96
|
+
props: {},
|
|
97
|
+
children: [
|
|
98
|
+
{
|
|
99
|
+
type: "div",
|
|
100
|
+
props: {},
|
|
101
|
+
children: ["B"],
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
type: "div",
|
|
105
|
+
props: {},
|
|
106
|
+
children: ["C"],
|
|
107
|
+
},
|
|
108
|
+
],
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
divInstances = [];
|
|
112
|
+
|
|
113
|
+
await act(async () => {
|
|
114
|
+
store.update("data", (data) => [{ value: "A" }, ...data]);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
assert.deepEqual(component.toJSON(), {
|
|
118
|
+
type: "div",
|
|
119
|
+
props: {},
|
|
120
|
+
children: [
|
|
121
|
+
{
|
|
122
|
+
type: "div",
|
|
123
|
+
props: {},
|
|
124
|
+
children: ["A"],
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
type: "div",
|
|
128
|
+
props: {},
|
|
129
|
+
children: ["B"],
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
type: "div",
|
|
133
|
+
props: {},
|
|
134
|
+
children: ["C"],
|
|
135
|
+
},
|
|
136
|
+
],
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
assert.equal(divInstances.length, 3);
|
|
140
|
+
assert.equal(divInstances[0].store.get("$item.value"), "A");
|
|
141
|
+
assert.equal(divInstances[1].store.get("$item.value"), "B");
|
|
142
|
+
assert.equal(divInstances[2].store.get("$item.value"), "C");
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it("infers T from AccessorChain<T[]> for onCreateFilter callback", () => {
|
|
146
|
+
interface Item {
|
|
147
|
+
name: string;
|
|
148
|
+
active: boolean;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
interface AppModel {
|
|
152
|
+
items: Item[];
|
|
153
|
+
$item: Item;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const m = createAccessorModelProxy<AppModel>();
|
|
157
|
+
|
|
158
|
+
// onCreateFilter should receive (record: Item) => boolean when T is inferred from AccessorChain<Item[]>
|
|
159
|
+
const widget = (
|
|
160
|
+
<cx>
|
|
161
|
+
<div>
|
|
162
|
+
<Repeater
|
|
163
|
+
records={m.items}
|
|
164
|
+
recordAlias={m.$item}
|
|
165
|
+
onCreateFilter={() => (record) => {
|
|
166
|
+
// If T is correctly inferred as Item, record.name should be string
|
|
167
|
+
const name: string = record.name;
|
|
168
|
+
// @ts-expect-error - record.name should be string, not number
|
|
169
|
+
const wrong: number = record.name;
|
|
170
|
+
return record.active;
|
|
171
|
+
}}
|
|
172
|
+
>
|
|
173
|
+
<div text={m.$item.name} />
|
|
174
|
+
</Repeater>
|
|
175
|
+
</div>
|
|
176
|
+
</cx>
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
assert.ok(widget);
|
|
180
|
+
});
|
|
181
|
+
});
|
package/src/util/Format.spec.ts
CHANGED
|
@@ -47,6 +47,17 @@ describe("Format", function () {
|
|
|
47
47
|
});
|
|
48
48
|
});
|
|
49
49
|
|
|
50
|
+
describe("daybefore", function () {
|
|
51
|
+
it("formats the date shifted back by one day", function () {
|
|
52
|
+
assert.equal(Format.value(new Date(2015, 3, 1, 5, 6, 14), "daybefore"), "3/31/2015 05:06");
|
|
53
|
+
assert.equal(Format.value(new Date(2015, 3, 1, 5, 6, 14), "dayBefore"), "3/31/2015 05:06");
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("steps back across month and year boundaries", function () {
|
|
57
|
+
assert.equal(Format.value(new Date(2021, 0, 1), "daybefore"), "12/31/2020 00:00");
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
50
61
|
describe("ellipsis", function () {
|
|
51
62
|
it("can shorten long texts", function () {
|
|
52
63
|
assert.equal(Format.value("This is a very long text.", "ellipsis;7"), "This...");
|
package/src/util/Format.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { isUndefined } from "../util/isUndefined";
|
|
|
5
5
|
import { isArray } from "../util/isArray";
|
|
6
6
|
import { capitalize } from "./capitalize";
|
|
7
7
|
import { parseDateInvariant } from "./date/parseDateInvariant";
|
|
8
|
+
import { dayBefore } from "./date/dayBefore";
|
|
8
9
|
|
|
9
10
|
//Culture dependent formatters are defined in the ui package.
|
|
10
11
|
|
|
@@ -98,6 +99,11 @@ let formatFactory: Record<string, (...args: any[]) => (value: any) => string> =
|
|
|
98
99
|
return (value: any) => date(value) + " " + time(value);
|
|
99
100
|
},
|
|
100
101
|
|
|
102
|
+
dayBefore: function () {
|
|
103
|
+
let datetime = formatFactory.datetime();
|
|
104
|
+
return (value: any) => datetime(dayBefore(parseDateInvariant(value)));
|
|
105
|
+
},
|
|
106
|
+
|
|
101
107
|
ellipsis: function (part0, length, where) {
|
|
102
108
|
length = Number(length);
|
|
103
109
|
if (!(length > 3)) length = 10;
|
|
@@ -168,6 +174,7 @@ formatFactory.ps = formatFactory.percentageSign;
|
|
|
168
174
|
formatFactory.d = formatFactory.date;
|
|
169
175
|
formatFactory.t = formatFactory.time;
|
|
170
176
|
formatFactory.dt = formatFactory.datetime;
|
|
177
|
+
formatFactory.daybefore = formatFactory.dayBefore;
|
|
171
178
|
formatFactory.zeropad = formatFactory.zeroPad;
|
|
172
179
|
formatFactory.leftpad = formatFactory.leftPad;
|
|
173
180
|
formatFactory.capitalize = formatFactory.capitalize;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Returns a new `Date` representing the calendar day before the given date,
|
|
3
|
+
* keeping the same time of day. Month and year boundaries are handled
|
|
4
|
+
* automatically. The input is not mutated.
|
|
5
|
+
*
|
|
6
|
+
* Useful for displaying the exclusive end of a date range as an inclusive
|
|
7
|
+
* value, e.g. a period ending at `2021-01-01` shown as `Dec 2020`.
|
|
8
|
+
* @param date
|
|
9
|
+
* @returns {Date}
|
|
10
|
+
*/
|
|
11
|
+
export function dayBefore(date: Date): Date {
|
|
12
|
+
let result = new Date(date.getTime());
|
|
13
|
+
result.setDate(result.getDate() - 1);
|
|
14
|
+
return result;
|
|
15
|
+
}
|