aloha-vue 2.63.0 → 2.64.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.
@@ -1,9 +1,9 @@
1
- import {
2
- computed,
3
- nextTick,
4
- reactive,
5
- ref,
6
- } from "vue";
1
+ import {
2
+ computed,
3
+ nextTick,
4
+ reactive,
5
+ ref,
6
+ } from "vue";
7
7
  import {
8
8
  describe,
9
9
  expect,
@@ -15,18 +15,20 @@ import {
15
15
  } from "@vue/test-utils";
16
16
 
17
17
  import AForm from "../../ui/AForm/AForm";
18
- import ATableForm from "../ATableForm";
19
- import ATableFormCellList from "../ATableFormCellList/ATableFormCellList";
20
-
21
- import CellDisabledAPI from "../ATableFormCell/compositionAPI/DisabledAPI";
22
- import CellReadonlyAPI from "../ATableFormCell/compositionAPI/ReadonlyAPI";
23
- import CellActionStylesAPI from "../ATableFormCellAction/compositionAPI/StylesAPI";
24
- import ColumnsAPI from "../compositionAPI/ColumnsAPI";
25
- import ColumnsGrowAPI from "../compositionAPI/ColumnsGrowAPI";
18
+ import ATableFormCellList from "../ATableFormCellList/ATableFormCellList";
19
+
20
+ import CellActionStylesAPI from "../ATableFormCellAction/compositionAPI/StylesAPI";
21
+ import CellDisabledAPI from "../ATableFormCell/compositionAPI/DisabledAPI";
22
+ import CellReadonlyAPI from "../ATableFormCell/compositionAPI/ReadonlyAPI";
23
+ import ColumnsAPI from "../compositionAPI/ColumnsAPI";
24
+ import ColumnsGrowAPI from "../compositionAPI/ColumnsGrowAPI";
26
25
  import DataAPI from "../ATableFormCellList/compositionAPI/DataAPI";
27
26
  import RowEditAPI from "../ATableFormRow/compositionAPI/EditAPI";
28
27
  import TableEditAPI from "../compositionAPI/EditAPI";
29
28
 
29
+ import ATableForm from "../ATableForm";
30
+ import getFormElement from "../utils/getFormElement";
31
+
30
32
  jest.mock("../../ui/AForm/AForm", () => {
31
33
  const {
32
34
  h,
@@ -176,214 +178,297 @@ jest.mock("aloha-svg/dist/js/bootstrap/XLg", () => ({
176
178
  default: "<svg></svg>",
177
179
  }));
178
180
 
179
- describe("ATableForm list view", () => {
180
- it("uses disabled fields for editable rows and readonly fields for non-editable rows", () => {
181
- const props = reactive({
182
- column: {
183
- formElement: {},
184
- },
185
- isEditable: true,
186
- isEditMode: false,
187
- isFooter: false,
188
- });
189
- const {
190
- disabledLocal,
191
- } = CellDisabledAPI(props);
192
- const {
193
- readonlyLocal,
194
- } = CellReadonlyAPI(props);
195
-
196
- expect(disabledLocal.value).toBe(true);
197
- expect(readonlyLocal.value).toBe(false);
198
-
199
- props.isEditable = false;
200
-
201
- expect(disabledLocal.value).toBe(false);
202
- expect(readonlyLocal.value).toBe(true);
203
- });
204
-
205
- it.each([
206
- [true, true, 112],
207
- [true, false, 64],
208
- [false, true, 64],
209
- [false, false, 64],
210
- ])("uses the expected action width for editable=%s and deletable=%s", (isEditable, isDeletable, width) => {
211
- const props = reactive({
212
- hasActiveEditRow: false,
213
- isActiveEditMode: false,
214
- isDeletable,
215
- isDeletableConfirm: false,
216
- isEditable,
217
- widths: {
218
- actionsColumnDouble: 112,
219
- actionsColumnSingle: 64,
220
- },
221
- });
222
- const {
223
- columnStyles,
224
- } = CellActionStylesAPI(props);
225
-
226
- expect(columnStyles.value.width).toBe(`${ width }px`);
227
- });
228
-
229
- it("shows the actions column only for edit, delete, or an active add row", () => {
230
- const isAddRowActive = ref(false);
231
- const props = reactive({
232
- columns: [{ id: "name" }],
233
- isDeletable: false,
234
- isDeletableConfirm: false,
235
- isDragAndDrop: false,
236
- isEditable: false,
237
- rowView: "table",
238
- });
239
- const {
240
- allColumnsLength,
241
- hasActionsColumn,
242
- } = ColumnsAPI(props, {
243
- isAddRowActive,
244
- });
245
-
246
- expect(hasActionsColumn.value).toBe(false);
247
- expect(allColumnsLength.value).toBe(1);
248
-
249
- props.isEditable = true;
250
- expect(hasActionsColumn.value).toBe(true);
251
- expect(allColumnsLength.value).toBe(2);
252
-
253
- props.isEditable = false;
254
- props.isDeletableConfirm = true;
255
- expect(hasActionsColumn.value).toBe(true);
256
-
257
- props.isDeletableConfirm = false;
258
- isAddRowActive.value = true;
259
- expect(hasActionsColumn.value).toBe(true);
260
- });
261
-
262
- it("grows all columns equally only when no column has a positive grow value", async() => {
263
- const resizeObserverOriginal = global.ResizeObserver;
264
- global.ResizeObserver = class {
265
- disconnect() {}
266
- observe() {}
267
- unobserve() {}
268
- };
269
-
270
- const wrapperElement = document.createElement("div");
271
- const tableElement = document.createElement("table");
272
- tableElement.id = "table_grow_fallback";
273
- wrapperElement.getBoundingClientRect = () => ({ width: 500 });
274
- wrapperElement.appendChild(tableElement);
275
- document.body.appendChild(wrapperElement);
276
-
277
- const props = reactive({
278
- id: "table_grow_fallback",
279
- isColumnsGrow: true,
280
- isDeletable: false,
281
- isDeletableConfirm: false,
282
- isDragAndDrop: false,
283
- isEditable: true,
284
- });
285
- const columns = ref([
286
- { grow: 0, id: "first", width: "100px" },
287
- { id: "second", width: "100px" },
288
- ]);
289
- const columnsVisible = computed(() => columns.value);
290
- const {
291
- columnsStylesGrow,
292
- destroyColumnsGrowObserver,
293
- initColumnsGrowObserver,
294
- } = ColumnsGrowAPI(props, {
295
- columnsVisible,
296
- hasActionsColumn: computed(() => true),
297
- widthsLocal: computed(() => ({
298
- actionsColumnDouble: 112,
299
- actionsColumnSingle: 64,
300
- dndColumn: 56,
301
- })),
302
- });
303
-
304
- initColumnsGrowObserver();
305
-
306
- expect(columnsStylesGrow.value).toEqual({
307
- first: {
308
- maxWidth: "217px",
309
- minWidth: "217px",
310
- width: "217px",
311
- },
312
- second: {
313
- maxWidth: "217px",
314
- minWidth: "217px",
315
- width: "217px",
316
- },
317
- });
318
-
319
- columns.value = [
320
- { grow: 1, id: "first", width: "100px" },
321
- { grow: 0, id: "second", width: "100px" },
322
- ];
323
- await nextTick();
324
-
325
- expect(columnsStylesGrow.value.first.width).toBe("334px");
326
- expect(columnsStylesGrow.value.second.width).toBe("100px");
327
-
328
- destroyColumnsGrowObserver();
329
- wrapperElement.remove();
330
- global.ResizeObserver = resizeObserverOriginal;
331
- });
332
-
333
- it("allows adding editable data when row editing is disabled", async() => {
334
- const wrapper = mount(ATableForm, {
335
- props: {
336
- addRow: jest.fn(),
337
- columns: [
338
- {
339
- id: "name",
340
- formElement: {
341
- type: "text",
342
- },
343
- },
344
- ],
345
- isAddable: true,
346
- isEditable: false,
347
- rowView: "list",
348
- rows: [],
349
- },
350
- });
351
-
352
- expect(wrapper.find(".a_table_form__cell_actions").exists()).toBe(false);
353
-
354
- await wrapper.find(".a_mt_3 button").trigger("click");
355
-
356
- expect(wrapper.find("tbody button[id$='_cancel']").exists()).toBe(true);
357
- expect(wrapper.find("tbody button[id$='_save']").exists()).toBe(true);
358
- expect(wrapper.findComponent(AForm).props("readonly")).toBe(false);
359
- });
360
-
361
- it("allows DND but not row-click editing when rows are not editable", async() => {
362
- const wrapper = mount(ATableForm, {
363
- props: {
364
- columns: [
365
- {
366
- id: "name",
367
- formElement: {
368
- type: "text",
369
- },
370
- },
371
- ],
372
- isDragAndDrop: true,
373
- isEditable: false,
374
- isEditOnRowClick: true,
375
- rows: [{ name: "Readonly" }],
376
- },
377
- });
378
-
379
- expect(wrapper.find("tbody .a_table_form__reorder_handle").attributes("draggable")).toBe("true");
380
-
381
- await wrapper.find("tbody tr.a_table_form__row").trigger("click");
382
-
383
- expect(wrapper.find("tbody button[id$='_save']").exists()).toBe(false);
384
- });
385
-
386
- it("enters edit mode by row click only when enabled and row edit is available", async() => {
181
+ describe("ATableForm list view", () => {
182
+ it("uses disabled fields for editable rows and readonly fields for non-editable rows", () => {
183
+ const props = reactive({
184
+ column: {
185
+ formElement: {},
186
+ },
187
+ isEditable: true,
188
+ isEditMode: false,
189
+ isFooter: false,
190
+ });
191
+ const {
192
+ disabledLocal,
193
+ } = CellDisabledAPI(props);
194
+ const {
195
+ readonlyLocal,
196
+ } = CellReadonlyAPI(props);
197
+
198
+ expect(disabledLocal.value).toBe(true);
199
+ expect(readonlyLocal.value).toBe(false);
200
+
201
+ props.isEditable = false;
202
+
203
+ expect(disabledLocal.value).toBe(false);
204
+ expect(readonlyLocal.value).toBe(true);
205
+ });
206
+
207
+ it("merges row-specific form element props only in edit mode", () => {
208
+ const formElementEditPropsCallback = jest.fn(({ rowData }) => ({
209
+ disabled: rowData.locked,
210
+ min: rowData.minimum,
211
+ readonly: true,
212
+ }));
213
+ const props = reactive({
214
+ column: {
215
+ id: "score",
216
+ formElement: {
217
+ disabled: false,
218
+ min: 0,
219
+ type: "integer",
220
+ },
221
+ formElementEditPropsCallback,
222
+ },
223
+ columnIndex: 1,
224
+ isCreateMode: true,
225
+ isEditable: true,
226
+ isEditMode: false,
227
+ isFooter: false,
228
+ row: {
229
+ id: 1,
230
+ locked: true,
231
+ minimum: 10,
232
+ },
233
+ rowData: {
234
+ id: 1,
235
+ locked: true,
236
+ minimum: 20,
237
+ },
238
+ rowIndex: 2,
239
+ rows: [],
240
+ });
241
+ const formElement = computed(() => getFormElement({
242
+ column: props.column,
243
+ columnIndex: props.columnIndex,
244
+ isCreateMode: props.isCreateMode,
245
+ isEditMode: props.isEditMode,
246
+ row: props.row,
247
+ rowData: props.rowData,
248
+ rowIndex: props.rowIndex,
249
+ rows: props.rows,
250
+ }));
251
+ const {
252
+ disabledLocal,
253
+ } = CellDisabledAPI(props, {
254
+ formElement,
255
+ });
256
+ const {
257
+ readonlyLocal,
258
+ } = CellReadonlyAPI(props, {
259
+ formElement,
260
+ });
261
+
262
+ expect(formElement.value).toEqual(props.column.formElement);
263
+ expect(formElementEditPropsCallback).not.toHaveBeenCalled();
264
+
265
+ props.isEditMode = true;
266
+
267
+ expect(formElement.value).toEqual(props.column.formElement);
268
+ expect(formElementEditPropsCallback).not.toHaveBeenCalled();
269
+
270
+ props.isCreateMode = false;
271
+
272
+ expect(formElement.value).toEqual({
273
+ disabled: true,
274
+ min: 20,
275
+ readonly: true,
276
+ type: "integer",
277
+ });
278
+ expect(disabledLocal.value).toBe(true);
279
+ expect(readonlyLocal.value).toBe(true);
280
+ expect(formElementEditPropsCallback).toHaveBeenCalledWith({
281
+ column: props.column,
282
+ columnIndex: 1,
283
+ row: props.row,
284
+ rowData: props.rowData,
285
+ rowIndex: 2,
286
+ rows: props.rows,
287
+ });
288
+ });
289
+
290
+ it.each([
291
+ [true, true, 112],
292
+ [true, false, 64],
293
+ [false, true, 64],
294
+ [false, false, 64],
295
+ ])("uses the expected action width for editable=%s and deletable=%s", (isEditable, isDeletable, width) => {
296
+ const props = reactive({
297
+ hasActiveEditRow: false,
298
+ isActiveEditMode: false,
299
+ isDeletable,
300
+ isDeletableConfirm: false,
301
+ isEditable,
302
+ widths: {
303
+ actionsColumnDouble: 112,
304
+ actionsColumnSingle: 64,
305
+ },
306
+ });
307
+ const {
308
+ columnStyles,
309
+ } = CellActionStylesAPI(props);
310
+
311
+ expect(columnStyles.value.width).toBe(`${ width }px`);
312
+ });
313
+
314
+ it("shows the actions column only for edit, delete, or an active add row", () => {
315
+ const isAddRowActive = ref(false);
316
+ const props = reactive({
317
+ columns: [{ id: "name" }],
318
+ isDeletable: false,
319
+ isDeletableConfirm: false,
320
+ isDragAndDrop: false,
321
+ isEditable: false,
322
+ rowView: "table",
323
+ });
324
+ const {
325
+ allColumnsLength,
326
+ hasActionsColumn,
327
+ } = ColumnsAPI(props, {
328
+ isAddRowActive,
329
+ });
330
+
331
+ expect(hasActionsColumn.value).toBe(false);
332
+ expect(allColumnsLength.value).toBe(1);
333
+
334
+ props.isEditable = true;
335
+ expect(hasActionsColumn.value).toBe(true);
336
+ expect(allColumnsLength.value).toBe(2);
337
+
338
+ props.isEditable = false;
339
+ props.isDeletableConfirm = true;
340
+ expect(hasActionsColumn.value).toBe(true);
341
+
342
+ props.isDeletableConfirm = false;
343
+ isAddRowActive.value = true;
344
+ expect(hasActionsColumn.value).toBe(true);
345
+ });
346
+
347
+ it("grows all columns equally only when no column has a positive grow value", async() => {
348
+ const resizeObserverOriginal = global.ResizeObserver;
349
+ global.ResizeObserver = class {
350
+ disconnect() {}
351
+ observe() {}
352
+ unobserve() {}
353
+ };
354
+
355
+ const wrapperElement = document.createElement("div");
356
+ const tableElement = document.createElement("table");
357
+ tableElement.id = "table_grow_fallback";
358
+ wrapperElement.getBoundingClientRect = () => ({ width: 500 });
359
+ wrapperElement.appendChild(tableElement);
360
+ document.body.appendChild(wrapperElement);
361
+
362
+ const props = reactive({
363
+ id: "table_grow_fallback",
364
+ isColumnsGrow: true,
365
+ isDeletable: false,
366
+ isDeletableConfirm: false,
367
+ isDragAndDrop: false,
368
+ isEditable: true,
369
+ });
370
+ const columns = ref([
371
+ { grow: 0, id: "first", width: "100px" },
372
+ { id: "second", width: "100px" },
373
+ ]);
374
+ const columnsVisible = computed(() => columns.value);
375
+ const {
376
+ columnsStylesGrow,
377
+ destroyColumnsGrowObserver,
378
+ initColumnsGrowObserver,
379
+ } = ColumnsGrowAPI(props, {
380
+ columnsVisible,
381
+ hasActionsColumn: computed(() => true),
382
+ widthsLocal: computed(() => ({
383
+ actionsColumnDouble: 112,
384
+ actionsColumnSingle: 64,
385
+ dndColumn: 56,
386
+ })),
387
+ });
388
+
389
+ initColumnsGrowObserver();
390
+
391
+ expect(columnsStylesGrow.value).toEqual({
392
+ first: {
393
+ maxWidth: "217px",
394
+ minWidth: "217px",
395
+ width: "217px",
396
+ },
397
+ second: {
398
+ maxWidth: "217px",
399
+ minWidth: "217px",
400
+ width: "217px",
401
+ },
402
+ });
403
+
404
+ columns.value = [
405
+ { grow: 1, id: "first", width: "100px" },
406
+ { grow: 0, id: "second", width: "100px" },
407
+ ];
408
+ await nextTick();
409
+
410
+ expect(columnsStylesGrow.value.first.width).toBe("334px");
411
+ expect(columnsStylesGrow.value.second.width).toBe("100px");
412
+
413
+ destroyColumnsGrowObserver();
414
+ wrapperElement.remove();
415
+ global.ResizeObserver = resizeObserverOriginal;
416
+ });
417
+
418
+ it("allows adding editable data when row editing is disabled", async() => {
419
+ const wrapper = mount(ATableForm, {
420
+ props: {
421
+ addRow: jest.fn(),
422
+ columns: [
423
+ {
424
+ id: "name",
425
+ formElement: {
426
+ type: "text",
427
+ },
428
+ },
429
+ ],
430
+ isAddable: true,
431
+ isEditable: false,
432
+ rowView: "list",
433
+ rows: [],
434
+ },
435
+ });
436
+
437
+ expect(wrapper.find(".a_table_form__cell_actions").exists()).toBe(false);
438
+
439
+ await wrapper.find(".a_mt_3 button").trigger("click");
440
+
441
+ expect(wrapper.find("tbody button[id$='_cancel']").exists()).toBe(true);
442
+ expect(wrapper.find("tbody button[id$='_save']").exists()).toBe(true);
443
+ expect(wrapper.findComponent(AForm).props("readonly")).toBe(false);
444
+ });
445
+
446
+ it("allows DND but not row-click editing when rows are not editable", async() => {
447
+ const wrapper = mount(ATableForm, {
448
+ props: {
449
+ columns: [
450
+ {
451
+ id: "name",
452
+ formElement: {
453
+ type: "text",
454
+ },
455
+ },
456
+ ],
457
+ isDragAndDrop: true,
458
+ isEditable: false,
459
+ isEditOnRowClick: true,
460
+ rows: [{ name: "Readonly" }],
461
+ },
462
+ });
463
+
464
+ expect(wrapper.find("tbody .a_table_form__reorder_handle").attributes("draggable")).toBe("true");
465
+
466
+ await wrapper.find("tbody tr.a_table_form__row").trigger("click");
467
+
468
+ expect(wrapper.find("tbody button[id$='_save']").exists()).toBe(false);
469
+ });
470
+
471
+ it("enters edit mode by row click only when enabled and row edit is available", async() => {
387
472
  const rows = [
388
473
  {
389
474
  id: 1,
@@ -634,6 +719,49 @@ describe("ATableForm list view", () => {
634
719
  expect(dataForm.value[0].children[0].readonly).toBe(false);
635
720
  });
636
721
 
722
+ it("applies row-specific form element props in list edit mode", () => {
723
+ const props = reactive({
724
+ columns: [
725
+ {
726
+ id: "score",
727
+ formElement: {
728
+ min: 0,
729
+ type: "integer",
730
+ },
731
+ formElementEditPropsCallback: ({ rowData }) => ({
732
+ disabled: rowData.locked,
733
+ min: rowData.minimum,
734
+ }),
735
+ },
736
+ ],
737
+ isEditable: true,
738
+ isEditMode: false,
739
+ row: {
740
+ locked: true,
741
+ minimum: 10,
742
+ },
743
+ rowData: {
744
+ locked: true,
745
+ minimum: 20,
746
+ },
747
+ rowIndex: 0,
748
+ rows: [],
749
+ });
750
+ const {
751
+ dataForm,
752
+ } = DataAPI(props);
753
+
754
+ expect(dataForm.value[0].min).toBe(0);
755
+
756
+ props.isEditMode = true;
757
+
758
+ expect(dataForm.value[0]).toEqual(expect.objectContaining({
759
+ disabled: true,
760
+ min: 20,
761
+ type: "integer",
762
+ }));
763
+ });
764
+
637
765
  it("passes the complete errors object to AForm and emits its full model", async() => {
638
766
  const errors = {
639
767
  negative: ["Value is too low"],
@@ -0,0 +1,32 @@
1
+ import {
2
+ isFunction,
3
+ } from "lodash-es";
4
+
5
+ export default function getFormElement({
6
+ column,
7
+ columnIndex,
8
+ isCreateMode,
9
+ isEditMode,
10
+ row,
11
+ rowData,
12
+ rowIndex,
13
+ rows,
14
+ }) {
15
+ const formElement = column?.formElement || {};
16
+
17
+ if (!isEditMode || isCreateMode || !isFunction(column?.formElementEditPropsCallback)) {
18
+ return formElement;
19
+ }
20
+
21
+ return {
22
+ ...formElement,
23
+ ...column.formElementEditPropsCallback({
24
+ column,
25
+ columnIndex,
26
+ row,
27
+ rowData,
28
+ rowIndex,
29
+ rows,
30
+ }),
31
+ };
32
+ }
@@ -318,6 +318,7 @@ export default {
318
318
  const {
319
319
  adjustFloatPartAndDivider,
320
320
  getCleanIntValue,
321
+ normalizeValueFromModel,
321
322
  } = UtilsAPI(props);
322
323
 
323
324
  const {
@@ -391,6 +392,7 @@ export default {
391
392
  increase,
392
393
  modelNumber,
393
394
  modelUndefinedLocal,
395
+ normalizeValueFromModel,
394
396
  onBlur,
395
397
  onFocus,
396
398
  setCurrentValue,
@@ -18,6 +18,7 @@ export default function InputEventsAPI(props, {
18
18
  increase = () => {},
19
19
  modelNumber = computed(() => undefined),
20
20
  modelUndefinedLocal = computed(() => undefined),
21
+ normalizeValueFromModel = value => value,
21
22
  onBlur = () => {},
22
23
  onFocus = () => {},
23
24
  setCurrentValue = () => {},
@@ -129,7 +130,7 @@ export default function InputEventsAPI(props, {
129
130
  }
130
131
  let value;
131
132
  if (updateOutside) {
132
- value = typeof _value === "number" ? `${ _value }`.replace(".", decimalDivider.value) : _value;
133
+ value = normalizeValueFromModel(_value);
133
134
  } else {
134
135
  value = isNil(_value) ? $event.target.value : `${ _value }`;
135
136
  }
@@ -713,7 +714,7 @@ export default function InputEventsAPI(props, {
713
714
  const hasModel = modelValue.value || modelValue.value === 0;
714
715
  let valueToSet;
715
716
  if (hasModel) {
716
- valueToSet = modelValue.value.toString().replace(".", decimalDivider.value);
717
+ valueToSet = normalizeValueFromModel(modelValue.value);
717
718
  if (decimalDivider.value) {
718
719
  const splitVal = valueToSet.toString().split(decimalDivider.value);
719
720
  const intPart = splitVal[0];