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.
Files changed (90) hide show
  1. package/build/charts/axis/Axis.d.ts +8 -0
  2. package/build/charts/axis/Axis.d.ts.map +1 -1
  3. package/build/charts/axis/Axis.js +18 -1
  4. package/build/charts/axis/TimeAxis.js +1 -0
  5. package/build/locale/de-de.js +1 -0
  6. package/build/locale/en-us.js +1 -0
  7. package/build/locale/es-es.js +1 -0
  8. package/build/locale/fr-fr.js +1 -0
  9. package/build/locale/nl-nl.js +1 -0
  10. package/build/locale/pt-pt.js +1 -0
  11. package/build/locale/sr-latn-ba.js +1 -0
  12. package/build/ui/Format.d.ts.map +1 -1
  13. package/build/ui/Format.js +26 -2
  14. package/build/util/Format.d.ts.map +1 -1
  15. package/build/util/Format.js +6 -0
  16. package/build/util/date/dateQuarter.d.ts +7 -0
  17. package/build/util/date/dateQuarter.d.ts.map +1 -0
  18. package/build/util/date/dateQuarter.js +8 -0
  19. package/build/util/date/dayBefore.d.ts +12 -0
  20. package/build/util/date/dayBefore.d.ts.map +1 -0
  21. package/build/util/date/dayBefore.js +15 -0
  22. package/build/util/date/index.d.ts +2 -0
  23. package/build/util/date/index.d.ts.map +1 -1
  24. package/build/util/date/index.js +2 -0
  25. package/build/widgets/form/DateTimePicker.d.ts.map +1 -1
  26. package/build/widgets/form/DateTimePicker.js +53 -31
  27. package/build/widgets/form/Field.d.ts.map +1 -1
  28. package/build/widgets/form/Field.js +2 -1
  29. package/build/widgets/form/LookupField.d.ts +6 -0
  30. package/build/widgets/form/LookupField.d.ts.map +1 -1
  31. package/build/widgets/form/LookupField.js +12 -0
  32. package/build/widgets/form/Wheel.d.ts +8 -0
  33. package/build/widgets/form/Wheel.d.ts.map +1 -1
  34. package/build/widgets/form/Wheel.js +30 -7
  35. package/build/widgets/grid/Grid.d.ts +6 -1
  36. package/build/widgets/grid/Grid.d.ts.map +1 -1
  37. package/build/widgets/grid/Grid.js +3 -2
  38. package/dist/charts.css +6 -0
  39. package/dist/charts.js +18 -1
  40. package/dist/manifest.js +880 -871
  41. package/dist/ui.js +33 -1
  42. package/dist/util.js +32 -0
  43. package/dist/widgets.css +4 -0
  44. package/dist/widgets.js +243 -175
  45. package/package.json +1 -1
  46. package/src/charts/BarGraph.scss +31 -31
  47. package/src/charts/Legend.scss +57 -57
  48. package/src/charts/LegendEntry.scss +35 -35
  49. package/src/charts/LineGraph.scss +28 -28
  50. package/src/charts/RangeMarker.scss +3 -0
  51. package/src/charts/axis/Axis.tsx +31 -1
  52. package/src/charts/axis/TimeAxis.tsx +1 -0
  53. package/src/charts/helpers/SnapPointFinder.ts +136 -136
  54. package/src/charts/helpers/ValueAtFinder.ts +72 -72
  55. package/src/charts/index.scss +1 -0
  56. package/src/data/AugmentedViewBase.ts +89 -89
  57. package/src/data/View.ts +301 -301
  58. package/src/data/createAccessorModelProxy.ts +66 -66
  59. package/src/locale/de-de.ts +1 -0
  60. package/src/locale/en-us.ts +1 -0
  61. package/src/locale/es-es.ts +1 -0
  62. package/src/locale/fr-fr.ts +1 -0
  63. package/src/locale/nl-nl.ts +1 -0
  64. package/src/locale/pt-pt.ts +1 -0
  65. package/src/locale/sr-latn-ba.ts +1 -0
  66. package/src/ui/Format.spec.ts +32 -0
  67. package/src/ui/Format.ts +27 -2
  68. package/src/ui/Repeater.spec.tsx +181 -181
  69. package/src/util/Format.spec.ts +11 -0
  70. package/src/util/Format.ts +7 -0
  71. package/src/util/date/dateQuarter.ts +8 -0
  72. package/src/util/date/dayBefore.ts +15 -0
  73. package/src/util/date/index.ts +2 -0
  74. package/src/util/scss/include.scss +69 -69
  75. package/src/widgets/Button.maps.scss +103 -103
  76. package/src/widgets/form/Calendar.tsx +772 -772
  77. package/src/widgets/form/DateTimePicker.tsx +453 -392
  78. package/src/widgets/form/Field.tsx +2 -1
  79. package/src/widgets/form/LookupField.spec.tsx +149 -0
  80. package/src/widgets/form/LookupField.tsx +27 -0
  81. package/src/widgets/form/ValidationGroup.spec.tsx +30 -1
  82. package/src/widgets/form/Wheel.tsx +36 -7
  83. package/src/widgets/grid/Grid.scss +663 -657
  84. package/src/widgets/grid/Grid.tsx +9 -3
  85. package/src/widgets/grid/variables.scss +47 -47
  86. package/src/widgets/index.ts +63 -63
  87. package/src/widgets/nav/MenuItem.scss +150 -150
  88. package/src/widgets/nav/Tab.ts +122 -122
  89. package/src/widgets/overlay/Overlay.tsx +1029 -1029
  90. 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
+ }
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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 = {};
@@ -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
+ });
@@ -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...");