cx 26.4.4 → 26.6.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/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/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/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/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/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 +6 -1
- package/build/widgets/grid/Grid.d.ts.map +1 -1
- package/build/widgets/grid/Grid.js +3 -2
- package/dist/charts.css +6 -0
- package/dist/charts.js +18 -1
- package/dist/manifest.js +880 -871
- package/dist/ui.js +33 -1
- package/dist/util.js +32 -0
- package/dist/widgets.css +4 -0
- package/dist/widgets.js +243 -175
- 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/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/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/LookupField.spec.tsx +149 -0
- package/src/widgets/form/LookupField.tsx +27 -0
- package/src/widgets/form/ValidationGroup.spec.tsx +30 -1
- package/src/widgets/form/Wheel.tsx +36 -7
- package/src/widgets/grid/Grid.scss +663 -657
- package/src/widgets/grid/Grid.tsx +9 -3
- 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
|
+
}
|
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
|
|
@@ -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...");
|