cx 26.5.1 → 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/dist/widgets.css CHANGED
@@ -5313,6 +5313,10 @@ th.cxe-calendar-display {
5313
5313
  content: counter(cx-row-number);
5314
5314
  }
5315
5315
 
5316
+ .cxe-grid-group-caption.cxs-reset-row-numbers {
5317
+ counter-set: cx-row-number 0;
5318
+ }
5319
+
5316
5320
  .cxe-grid-fixed-header {
5317
5321
  overflow: hidden;
5318
5322
  position: absolute;
package/dist/widgets.js CHANGED
@@ -8677,6 +8677,19 @@ class LookupField extends Field {
8677
8677
  }
8678
8678
  }
8679
8679
  instance.lastDropdown = context.lastDropdown;
8680
+ if (this.validateOptionExists && isArray(data.options) && !this.isEmpty(data)) {
8681
+ let invalid = this.multiple
8682
+ ? isArray(data.values) && data.records.length < data.values.length
8683
+ : !data.options.some(($option) =>
8684
+ areKeysEqual(
8685
+ getOptionKey(this.keyBindings, {
8686
+ $option,
8687
+ }),
8688
+ data.selectedKeys[0],
8689
+ ),
8690
+ );
8691
+ if (invalid) data.error = this.invalidOptionText;
8692
+ }
8680
8693
  super.prepareData(context, instance);
8681
8694
  }
8682
8695
  renderInput(context, instance, key) {
@@ -8742,6 +8755,8 @@ LookupField.prototype.hideSearchField = false;
8742
8755
  LookupField.prototype.minOptionsForSearchField = 7;
8743
8756
  LookupField.prototype.loadingText = "Loading...";
8744
8757
  LookupField.prototype.queryErrorText = "Error occurred while querying for lookup data.";
8758
+ LookupField.prototype.validateOptionExists = false;
8759
+ LookupField.prototype.invalidOptionText = "The selected option is no longer available.";
8745
8760
  LookupField.prototype.noResultsText = "No results found.";
8746
8761
  LookupField.prototype.optionIdField = "id";
8747
8762
  LookupField.prototype.optionTextField = "text";
@@ -15600,12 +15615,13 @@ class Grid extends ContainerBase {
15600
15615
  renderGroupHeader(context, instance, g, level, group, i, store, fixedColumns) {
15601
15616
  let { CSS, baseClass } = this;
15602
15617
  let data = store.getData();
15618
+ let resetRowNumbers = g.resetRowNumbers ? "reset-row-numbers" : null;
15603
15619
  if (g.caption) {
15604
15620
  let caption = g.caption(data);
15605
15621
  return jsx(
15606
15622
  "tbody",
15607
15623
  {
15608
- className: CSS.element(baseClass, "group-caption", ["level-" + level]),
15624
+ className: CSS.element(baseClass, "group-caption", ["level-" + level, resetRowNumbers]),
15609
15625
  "data-group-key": group.$key,
15610
15626
  "data-group-element": `group-caption-${level}`,
15611
15627
  children: jsx("tr", {
@@ -15696,7 +15712,7 @@ class Grid extends ContainerBase {
15696
15712
  return jsx(
15697
15713
  "tbody",
15698
15714
  {
15699
- className: CSS.element(baseClass, "group-caption", ["level-" + level]),
15715
+ className: CSS.element(baseClass, "group-caption", ["level-" + level, resetRowNumbers]),
15700
15716
  "data-group-key": group.$key,
15701
15717
  "data-group-element": `group-caption-${level}`,
15702
15718
  children: lines,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cx",
3
- "version": "26.5.1",
3
+ "version": "26.6.0",
4
4
  "description": "Advanced JavaScript UI framework for admin and dashboard applications with ready to use grid, form and chart components.",
5
5
  "exports": {
6
6
  "./data": {
@@ -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
@@ -1,5 +1,10 @@
1
1
  import { createAccessorModelProxy } from "../../data/createAccessorModelProxy";
2
2
  import { LookupField } from "./LookupField";
3
+ import { Store } from "../../data/Store";
4
+ import { ValidationGroup } from "./ValidationGroup";
5
+ import { bind } from "../../ui/bind";
6
+ import { createTestRenderer } from "../../util/test/createTestRenderer";
7
+ import assert from "assert";
3
8
 
4
9
  interface User {
5
10
  id: number;
@@ -90,4 +95,148 @@ describe("LookupField", () => {
90
95
  </cx>
91
96
  );
92
97
  });
98
+
99
+ describe("validateOptionExists", () => {
100
+ const options = [
101
+ { id: 1, text: "One" },
102
+ { id: 2, text: "Two" },
103
+ ];
104
+
105
+ it("reports an error when the selected value is missing from options", async () => {
106
+ let widget = (
107
+ <cx>
108
+ <ValidationGroup errors={bind("errors")}>
109
+ <LookupField
110
+ value={bind("value")}
111
+ text={bind("text")}
112
+ options={options}
113
+ validateOptionExists
114
+ />
115
+ </ValidationGroup>
116
+ </cx>
117
+ );
118
+
119
+ let store = new Store();
120
+ store.set("value", 99);
121
+ store.set("text", "Stale");
122
+
123
+ await createTestRenderer(store, widget);
124
+
125
+ let errors = store.get("errors");
126
+ assert.equal(errors.length, 1);
127
+ assert.equal(errors[0].message, "The selected option is no longer available.");
128
+ });
129
+
130
+ it("does not report an error when the selected value matches an option", async () => {
131
+ let widget = (
132
+ <cx>
133
+ <ValidationGroup errors={bind("errors")}>
134
+ <LookupField
135
+ value={bind("value")}
136
+ text={bind("text")}
137
+ options={options}
138
+ validateOptionExists
139
+ />
140
+ </ValidationGroup>
141
+ </cx>
142
+ );
143
+
144
+ let store = new Store();
145
+ store.set("value", 1);
146
+ store.set("text", "One");
147
+
148
+ await createTestRenderer(store, widget);
149
+
150
+ let errors = store.get("errors");
151
+ assert.equal(errors.length, 0);
152
+ });
153
+
154
+ it("does not report an error when the field is empty", async () => {
155
+ let widget = (
156
+ <cx>
157
+ <ValidationGroup errors={bind("errors")}>
158
+ <LookupField
159
+ value={bind("value")}
160
+ text={bind("text")}
161
+ options={options}
162
+ validateOptionExists
163
+ />
164
+ </ValidationGroup>
165
+ </cx>
166
+ );
167
+
168
+ let store = new Store();
169
+
170
+ await createTestRenderer(store, widget);
171
+
172
+ let errors = store.get("errors");
173
+ assert.equal(errors.length, 0);
174
+ });
175
+
176
+ it("does not report an error when options are not provided (server-side mode)", async () => {
177
+ let widget = (
178
+ <cx>
179
+ <ValidationGroup errors={bind("errors")}>
180
+ <LookupField
181
+ value={bind("value")}
182
+ text={bind("text")}
183
+ onQuery={() => []}
184
+ validateOptionExists
185
+ />
186
+ </ValidationGroup>
187
+ </cx>
188
+ );
189
+
190
+ let store = new Store();
191
+ store.set("value", 99);
192
+ store.set("text", "Stale");
193
+
194
+ await createTestRenderer(store, widget);
195
+
196
+ let errors = store.get("errors");
197
+ assert.equal(errors.length, 0);
198
+ });
199
+
200
+ it("reports an error in multiple mode when some ids are not in options", async () => {
201
+ let widget = (
202
+ <cx>
203
+ <ValidationGroup errors={bind("errors")}>
204
+ <LookupField
205
+ multiple
206
+ values={bind("values")}
207
+ options={options}
208
+ validateOptionExists
209
+ />
210
+ </ValidationGroup>
211
+ </cx>
212
+ );
213
+
214
+ let store = new Store();
215
+ store.set("values", [1, 99]);
216
+
217
+ await createTestRenderer(store, widget);
218
+
219
+ let errors = store.get("errors");
220
+ assert.equal(errors.length, 1);
221
+ });
222
+
223
+ it("does not validate by default (back-compat)", async () => {
224
+ let widget = (
225
+ <cx>
226
+ <ValidationGroup errors={bind("errors")}>
227
+ <LookupField value={bind("value")} text={bind("text")} options={options} />
228
+ </ValidationGroup>
229
+ </cx>
230
+ );
231
+
232
+ let store = new Store();
233
+ store.set("value", 99);
234
+ store.set("text", "Stale");
235
+
236
+ await createTestRenderer(store, widget);
237
+
238
+ let errors = store.get("errors");
239
+ assert.equal(errors.length, 0);
240
+ });
241
+ });
93
242
  });
@@ -117,6 +117,12 @@ interface LookupFieldBaseConfig<TOption = any> extends FieldConfig {
117
117
  /** Error message displayed if server query throws an exception. */
118
118
  queryErrorText?: string;
119
119
 
120
+ /** Set to `true` to report a validation error when the selected value is not present in `options`. Only applies when `options` is an array. Default is `false`. */
121
+ validateOptionExists?: boolean;
122
+
123
+ /** Error message displayed when the selected value is not present in `options`. */
124
+ invalidOptionText?: string;
125
+
120
126
  /** Message to be displayed if no entries match the user query. */
121
127
  noResultsText?: string;
122
128
 
@@ -292,6 +298,8 @@ export class LookupField<TOption = any, TRecord = any> extends Field<
292
298
  declare public minOptionsForSearchField: number;
293
299
  declare public loadingText: string;
294
300
  declare public queryErrorText: string;
301
+ declare public validateOptionExists: boolean;
302
+ declare public invalidOptionText: string;
295
303
  declare public noResultsText: string;
296
304
  declare public optionIdField: string;
297
305
  declare public optionTextField: string;
@@ -501,6 +509,22 @@ export class LookupField<TOption = any, TRecord = any> extends Field<
501
509
 
502
510
  (instance as DropdownInstance).lastDropdown = context.lastDropdown;
503
511
 
512
+ if (
513
+ this.validateOptionExists &&
514
+ isArray(data.options) &&
515
+ !this.isEmpty(data)
516
+ ) {
517
+ let invalid = this.multiple
518
+ ? isArray(data.values) && data.records!.length < data.values.length
519
+ : !data.options.some(($option) =>
520
+ areKeysEqual(
521
+ getOptionKey(this.keyBindings!, { $option }),
522
+ data.selectedKeys[0],
523
+ ),
524
+ );
525
+ if (invalid) data.error = this.invalidOptionText;
526
+ }
527
+
504
528
  super.prepareData(context, instance);
505
529
  }
506
530
 
@@ -604,6 +628,9 @@ LookupField.prototype.minOptionsForSearchField = 7;
604
628
  LookupField.prototype.loadingText = "Loading...";
605
629
  LookupField.prototype.queryErrorText =
606
630
  "Error occurred while querying for lookup data.";
631
+ LookupField.prototype.validateOptionExists = false;
632
+ LookupField.prototype.invalidOptionText =
633
+ "The selected option is no longer available.";
607
634
  LookupField.prototype.noResultsText = "No results found.";
608
635
  LookupField.prototype.optionIdField = "id";
609
636
  LookupField.prototype.optionTextField = "text";
@@ -184,6 +184,12 @@
184
184
  content: counter(cx-row-number);
185
185
  }
186
186
 
187
+ // counter-set (not counter-reset) updates the existing row-number counter in place; a
188
+ // counter-reset here would create a nested counter that the following data rows ignore.
189
+ .#{$element}#{$name}-group-caption.#{$state}reset-row-numbers {
190
+ counter-set: cx-row-number 0;
191
+ }
192
+
187
193
  .#{$element}#{$name}-fixed-header {
188
194
  overflow: hidden;
189
195
  position: absolute;
@@ -156,6 +156,11 @@ export interface GridGroupingConfig<T> {
156
156
  showCaption?: boolean;
157
157
  showFooter?: boolean;
158
158
  showHeader?: boolean;
159
+ /**
160
+ * Restart automatic row numbers (`cxe-grid-row-number`) from 1 at the beginning of each group
161
+ * at this grouping level instead of counting continuously across the whole grid. Defaults to `false`.
162
+ */
163
+ resetRowNumbers?: boolean;
159
164
  caption?: StringProp;
160
165
  name?: StringProp;
161
166
  text?: StringProp;
@@ -1510,12 +1515,13 @@ export class Grid<T = unknown> extends ContainerBase<GridConfig<T>, GridInstance
1510
1515
  ) {
1511
1516
  let { CSS, baseClass } = this;
1512
1517
  let data = store.getData();
1518
+ let resetRowNumbers = g.resetRowNumbers ? "reset-row-numbers" : null;
1513
1519
  if (g.caption) {
1514
1520
  let caption = g.caption(data);
1515
1521
  return (
1516
1522
  <tbody
1517
1523
  key={`g-${level}-${group.$key}`}
1518
- className={CSS.element(baseClass, "group-caption", ["level-" + level])}
1524
+ className={CSS.element(baseClass, "group-caption", ["level-" + level, resetRowNumbers])}
1519
1525
  data-group-key={group.$key}
1520
1526
  data-group-element={`group-caption-${level}`}
1521
1527
  >
@@ -1598,7 +1604,7 @@ export class Grid<T = unknown> extends ContainerBase<GridConfig<T>, GridInstance
1598
1604
  return (
1599
1605
  <tbody
1600
1606
  key={"c" + group.$key}
1601
- className={CSS.element(baseClass, "group-caption", ["level-" + level])}
1607
+ className={CSS.element(baseClass, "group-caption", ["level-" + level, resetRowNumbers])}
1602
1608
  data-group-key={group.$key}
1603
1609
  data-group-element={`group-caption-${level}`}
1604
1610
  >