@dile/crud 2.9.4 → 2.10.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/components/crud/crud.spec.js +91 -0
- package/components/crud/src/DileCrud.js +7 -2
- package/components/list/__screenshots__/crud-list.spec.js/dile-crud-list-scope-changes-and-page-reset-keeps-the-current-page-on-refresh---and-goes-back-to-page-1-on-refresh---resetPage--true----1.png +0 -0
- package/components/list/__screenshots__/crud-list.spec.js/dile-crud-list-scope-changes-and-page-reset-reloads-from-the-first-page-with-the-new-scope-when-belongsTo-and-relationId-change-1.png +0 -0
- package/components/list/__screenshots__/crud-list.spec.js/dile-crud-list-scope-changes-and-page-reset-reloads-when-only-relationId-changes-1.png +0 -0
- package/components/list/crud-list.spec.js +82 -0
- package/components/list/src/DileCrudList.js +8 -2
- package/components/list/src/DileCrudListService.js +1 -1
- package/components/steps-form/__screenshots__/steps-form.spec.js/dile-steps-form-translates-the-navigation-labels-with-the-language-property-1.png +0 -0
- package/components/steps-form/index.js +3 -0
- package/components/steps-form/src/DileStepForm.js +202 -0
- package/components/steps-form/src/DileStepNextPrev.js +134 -0
- package/components/steps-form/src/DileStepsForm.js +347 -0
- package/components/steps-form/step-form.js +5 -0
- package/components/steps-form/step-next-prev.js +5 -0
- package/components/steps-form/steps-form.js +5 -0
- package/components/steps-form/steps-form.spec.js +230 -0
- package/lib/i18n/en.js +6 -1
- package/lib/i18n/es.js +6 -1
- package/package.json +3 -3
|
@@ -360,3 +360,94 @@ describe('dile-crud list/grid view switch', () => {
|
|
|
360
360
|
expect(el.listElement.viewMode).toBe('item');
|
|
361
361
|
});
|
|
362
362
|
});
|
|
363
|
+
|
|
364
|
+
describe('dile-crud scope and refresh', () => {
|
|
365
|
+
afterEach(() => {
|
|
366
|
+
document.body.innerHTML = '';
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
370
|
+
|
|
371
|
+
async function createCrud() {
|
|
372
|
+
const config = new CrudConfigBuilder('https://example.test/api/invoices', {
|
|
373
|
+
customization: {
|
|
374
|
+
disableInsert: true,
|
|
375
|
+
disableHelp: true,
|
|
376
|
+
disableKeywordSearch: true,
|
|
377
|
+
disableSort: true,
|
|
378
|
+
disableFilter: true,
|
|
379
|
+
disablePagination: true,
|
|
380
|
+
hideCheckboxSelection: true,
|
|
381
|
+
},
|
|
382
|
+
templates: { item: (item) => html`<span>${item.name}</span>` },
|
|
383
|
+
}).getConfig();
|
|
384
|
+
|
|
385
|
+
const el = document.createElement('dile-crud');
|
|
386
|
+
el.config = config;
|
|
387
|
+
el.belongsTo = 'customer';
|
|
388
|
+
el.relationId = '10';
|
|
389
|
+
document.body.appendChild(el);
|
|
390
|
+
await el.updateComplete;
|
|
391
|
+
const list = el.listElement;
|
|
392
|
+
await list.updateComplete;
|
|
393
|
+
await list.elservice.updateComplete;
|
|
394
|
+
|
|
395
|
+
// Intercept the outgoing requests instead of hitting the network,
|
|
396
|
+
// and let the initial load (fired on start) settle first.
|
|
397
|
+
const requests = [];
|
|
398
|
+
const ajax = list.elservice.ajaxget;
|
|
399
|
+
ajax.generateRequest = () => requests.push({ url: ajax.url, data: ajax.data });
|
|
400
|
+
list.elservice.delayTime = 10;
|
|
401
|
+
await wait(250);
|
|
402
|
+
requests.length = 0;
|
|
403
|
+
return { el, list, requests, ajax };
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
it('setScope() updates belongsTo and relationId and reloads the list once from page 1', async () => {
|
|
407
|
+
const { el, list, requests, ajax } = await createCrud();
|
|
408
|
+
ajax.url = 'https://example.test/api/invoices?page=2';
|
|
409
|
+
|
|
410
|
+
el.setScope('supplier', '20');
|
|
411
|
+
expect(el.belongsTo).toBe('supplier');
|
|
412
|
+
expect(el.relationId).toBe('20');
|
|
413
|
+
await el.updateComplete;
|
|
414
|
+
await list.updateComplete;
|
|
415
|
+
await wait(50);
|
|
416
|
+
|
|
417
|
+
expect(list.belongsTo).toBe('supplier');
|
|
418
|
+
expect(list.relationId).toBe('20');
|
|
419
|
+
expect(requests).toHaveLength(1);
|
|
420
|
+
expect(requests[0].url).toBe('https://example.test/api/invoices');
|
|
421
|
+
expect(requests[0].data.belongsTo).toBe('supplier');
|
|
422
|
+
expect(requests[0].data.relationId).toBe('20');
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
it('setScope() without arguments clears the scope', async () => {
|
|
426
|
+
const { el, list, requests } = await createCrud();
|
|
427
|
+
|
|
428
|
+
el.setScope();
|
|
429
|
+
await el.updateComplete;
|
|
430
|
+
await list.updateComplete;
|
|
431
|
+
await wait(50);
|
|
432
|
+
|
|
433
|
+
expect(el.belongsTo).toBe('');
|
|
434
|
+
expect(el.relationId).toBe('');
|
|
435
|
+
expect(requests).toHaveLength(1);
|
|
436
|
+
expect(requests[0].data.belongsTo).toBeUndefined();
|
|
437
|
+
expect(requests[0].data.relationId).toBeUndefined();
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
it('refresh() keeps the current page and refresh({ resetPage: true }) goes back to page 1', async () => {
|
|
441
|
+
const { el, requests, ajax } = await createCrud();
|
|
442
|
+
const pagedUrl = 'https://example.test/api/invoices?page=2';
|
|
443
|
+
|
|
444
|
+
ajax.url = pagedUrl;
|
|
445
|
+
el.refresh();
|
|
446
|
+
await wait(50);
|
|
447
|
+
expect(requests.at(-1).url).toBe(pagedUrl);
|
|
448
|
+
|
|
449
|
+
el.refresh({ resetPage: true });
|
|
450
|
+
await wait(50);
|
|
451
|
+
expect(requests.at(-1).url).toBe('https://example.test/api/invoices');
|
|
452
|
+
});
|
|
453
|
+
});
|
|
@@ -525,8 +525,13 @@ export class DileCrud extends DileI18nMixin(DileCrudMixin(LitElement)) {
|
|
|
525
525
|
this.refresh();
|
|
526
526
|
}
|
|
527
527
|
|
|
528
|
-
refresh() {
|
|
529
|
-
this.listElement.refresh();
|
|
528
|
+
refresh({ resetPage = false } = {}) {
|
|
529
|
+
this.listElement.refresh({ resetPage });
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
setScope(belongsTo = '', relationId = '') {
|
|
533
|
+
this.belongsTo = belongsTo;
|
|
534
|
+
this.relationId = relationId;
|
|
530
535
|
}
|
|
531
536
|
|
|
532
537
|
setCustomizationOption(optionName, value) {
|
|
Binary file
|
|
@@ -371,3 +371,85 @@ describe('dile-crud-list grid template integration', () => {
|
|
|
371
371
|
expect(el.shadowRoot.querySelector('dile-crud-data-grid')).toBeTruthy();
|
|
372
372
|
});
|
|
373
373
|
});
|
|
374
|
+
|
|
375
|
+
describe('dile-crud-list scope changes and page reset', () => {
|
|
376
|
+
afterEach(() => {
|
|
377
|
+
document.body.innerHTML = '';
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
381
|
+
|
|
382
|
+
async function createList({ belongsTo, relationId } = {}) {
|
|
383
|
+
const config = new CrudConfigBuilder('https://example.test/api/invoices', {
|
|
384
|
+
templates: { item: (item) => html`<span>${item.name}</span>` },
|
|
385
|
+
}).getConfig();
|
|
386
|
+
|
|
387
|
+
const el = document.createElement('dile-crud-list');
|
|
388
|
+
el.disableLoadOnStart = true;
|
|
389
|
+
el.config = config;
|
|
390
|
+
if (belongsTo !== undefined) el.belongsTo = belongsTo;
|
|
391
|
+
if (relationId !== undefined) el.relationId = relationId;
|
|
392
|
+
document.body.appendChild(el);
|
|
393
|
+
await el.updateComplete;
|
|
394
|
+
await el.elservice.updateComplete;
|
|
395
|
+
|
|
396
|
+
// Intercept the outgoing requests instead of hitting the network.
|
|
397
|
+
const requests = [];
|
|
398
|
+
const ajax = el.elservice.ajaxget;
|
|
399
|
+
ajax.generateRequest = () => requests.push({ url: ajax.url, data: ajax.data });
|
|
400
|
+
el.elservice.delayTime = 10;
|
|
401
|
+
return { el, requests, ajax };
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
it('does not fire an extra request on the initial render when scope is set from the start', async () => {
|
|
405
|
+
const { requests } = await createList({ belongsTo: 'customer', relationId: '10' });
|
|
406
|
+
await wait(50);
|
|
407
|
+
expect(requests).toHaveLength(0);
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
it('reloads from the first page with the new scope when belongsTo and relationId change', async () => {
|
|
411
|
+
const { el, requests, ajax } = await createList({ belongsTo: 'customer', relationId: '10' });
|
|
412
|
+
ajax.url = 'https://example.test/api/invoices?page=3';
|
|
413
|
+
|
|
414
|
+
let refreshEvents = 0;
|
|
415
|
+
el.addEventListener('crud-list-refresh', () => refreshEvents++);
|
|
416
|
+
|
|
417
|
+
el.belongsTo = 'supplier';
|
|
418
|
+
el.relationId = '20';
|
|
419
|
+
await el.updateComplete;
|
|
420
|
+
expect(el.loading).toBe(true);
|
|
421
|
+
|
|
422
|
+
await wait(50);
|
|
423
|
+
expect(refreshEvents).toBe(1);
|
|
424
|
+
expect(requests).toHaveLength(1);
|
|
425
|
+
expect(requests[0].url).toBe('https://example.test/api/invoices');
|
|
426
|
+
expect(requests[0].data.belongsTo).toBe('supplier');
|
|
427
|
+
expect(requests[0].data.relationId).toBe('20');
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
it('reloads when only relationId changes', async () => {
|
|
431
|
+
const { el, requests } = await createList({ belongsTo: 'customer', relationId: '10' });
|
|
432
|
+
|
|
433
|
+
el.relationId = '11';
|
|
434
|
+
await el.updateComplete;
|
|
435
|
+
await wait(50);
|
|
436
|
+
|
|
437
|
+
expect(requests).toHaveLength(1);
|
|
438
|
+
expect(requests[0].data.relationId).toBe('11');
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
it('keeps the current page on refresh() and goes back to page 1 on refresh({ resetPage: true })', async () => {
|
|
442
|
+
const { el, requests, ajax } = await createList();
|
|
443
|
+
const pagedUrl = 'https://example.test/api/invoices?page=3';
|
|
444
|
+
|
|
445
|
+
ajax.url = pagedUrl;
|
|
446
|
+
el.refresh();
|
|
447
|
+
await wait(50);
|
|
448
|
+
expect(requests.at(-1).url).toBe(pagedUrl);
|
|
449
|
+
|
|
450
|
+
el.refresh({ resetPage: true });
|
|
451
|
+
await wait(50);
|
|
452
|
+
expect(requests.at(-1).url).toBe('https://example.test/api/invoices');
|
|
453
|
+
expect(requests).toHaveLength(2);
|
|
454
|
+
});
|
|
455
|
+
});
|
|
@@ -132,6 +132,12 @@ export class DileCrudList extends DileI18nMixin(DileLoading(LitElement)) {
|
|
|
132
132
|
}
|
|
133
133
|
}
|
|
134
134
|
|
|
135
|
+
willUpdate(changedProperties) {
|
|
136
|
+
if (this.hasUpdated && (changedProperties.has('belongsTo') || changedProperties.has('relationId'))) {
|
|
137
|
+
this.refresh({ resetPage: true });
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
135
141
|
render() {
|
|
136
142
|
return html`
|
|
137
143
|
${this.ajaxTemplate}
|
|
@@ -363,7 +369,7 @@ export class DileCrudList extends DileI18nMixin(DileLoading(LitElement)) {
|
|
|
363
369
|
this.elservice.goPrev();
|
|
364
370
|
}
|
|
365
371
|
|
|
366
|
-
refresh() {
|
|
372
|
+
refresh({ resetPage = false } = {}) {
|
|
367
373
|
this.dispatchEvent(new CustomEvent('crud-list-refresh', {
|
|
368
374
|
bubbles: true,
|
|
369
375
|
composed: true,
|
|
@@ -373,7 +379,7 @@ export class DileCrudList extends DileI18nMixin(DileLoading(LitElement)) {
|
|
|
373
379
|
if (this.isSelectAllActive) {
|
|
374
380
|
this.shadowRoot.querySelector('dile-crud-select-all').reset();
|
|
375
381
|
}
|
|
376
|
-
this.elservice.refresh();
|
|
382
|
+
resetPage ? this.elservice.cleanAndRefresh() : this.elservice.refresh();
|
|
377
383
|
}
|
|
378
384
|
|
|
379
385
|
setKeyword(keyword) {
|
|
Binary file
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import { LitElement, html, css } from "lit";
|
|
2
|
+
import "../../ajax/ajax.js";
|
|
3
|
+
import { ResponseApiAdapter } from "../../../lib/ResponseApiAdapter.js";
|
|
4
|
+
import "../step-next-prev.js";
|
|
5
|
+
|
|
6
|
+
export class DileStepForm extends LitElement {
|
|
7
|
+
static styles = [
|
|
8
|
+
css`
|
|
9
|
+
:host {
|
|
10
|
+
display: block;
|
|
11
|
+
}
|
|
12
|
+
:host(:not([active])) {
|
|
13
|
+
display: none;
|
|
14
|
+
}
|
|
15
|
+
`,
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
static get properties() {
|
|
19
|
+
return {
|
|
20
|
+
/** Label of the step, shown in the steps indicator */
|
|
21
|
+
label: { type: String },
|
|
22
|
+
/** The step is the visible one. Managed by dile-steps-form */
|
|
23
|
+
active: { type: Boolean, reflect: true },
|
|
24
|
+
/** Endpoint where the step data is saved */
|
|
25
|
+
endpoint: { type: String },
|
|
26
|
+
/** HTTP method used to save the step */
|
|
27
|
+
method: { type: String },
|
|
28
|
+
/** Zero-based index of the step, set by dile-steps-form */
|
|
29
|
+
step: { type: Number },
|
|
30
|
+
/** Labels of all the steps, set by dile-steps-form */
|
|
31
|
+
steps: { type: Array },
|
|
32
|
+
/** Shows the alternative exit link in this step */
|
|
33
|
+
alternativeExit: { type: Boolean },
|
|
34
|
+
alternativeExitLabel: { type: String },
|
|
35
|
+
/** Name of the field added to the payload when the alternative exit is used. Empty to not send it */
|
|
36
|
+
alternativeExitField: { type: String },
|
|
37
|
+
prevLabel: { type: String },
|
|
38
|
+
nextLabel: { type: String },
|
|
39
|
+
endLabel: { type: String },
|
|
40
|
+
/** Default labels, set by dile-steps-form: { prev, next, end } */
|
|
41
|
+
defaultLabels: { type: Object },
|
|
42
|
+
/** The form of this step receives the data returned when another step is saved */
|
|
43
|
+
refreshOnSave: { type: Boolean },
|
|
44
|
+
sendDataAsFormData: { type: Boolean },
|
|
45
|
+
responseAdapter: { type: Object },
|
|
46
|
+
language: { type: String },
|
|
47
|
+
loading: { type: Boolean },
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
constructor() {
|
|
52
|
+
super();
|
|
53
|
+
this.active = false;
|
|
54
|
+
this.method = "patch";
|
|
55
|
+
this.steps = [];
|
|
56
|
+
this.alternativeExit = false;
|
|
57
|
+
this.alternativeExitField = "alternativeExit";
|
|
58
|
+
this.defaultLabels = {};
|
|
59
|
+
this.refreshOnSave = false;
|
|
60
|
+
this.sendDataAsFormData = false;
|
|
61
|
+
this.language = "en";
|
|
62
|
+
this.loading = false;
|
|
63
|
+
this.alternativeExitRequested = false;
|
|
64
|
+
this.defaultResponseAdapter = new ResponseApiAdapter();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
get elajax() {
|
|
68
|
+
return this.shadowRoot.getElementById("ajaxsave");
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
get form() {
|
|
72
|
+
return this.querySelector('[slot="form"]');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
get adapter() {
|
|
76
|
+
return this.responseAdapter ?? this.defaultResponseAdapter;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
updated(changedProperties) {
|
|
80
|
+
if (
|
|
81
|
+
changedProperties.has("label") &&
|
|
82
|
+
changedProperties.get("label") !== undefined
|
|
83
|
+
) {
|
|
84
|
+
this.dispatchEvent(
|
|
85
|
+
new CustomEvent("dile-step-form-label-change", {
|
|
86
|
+
bubbles: true,
|
|
87
|
+
detail: { label: this.label },
|
|
88
|
+
}),
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
render() {
|
|
94
|
+
return html`
|
|
95
|
+
<dile-ajax
|
|
96
|
+
id="ajaxsave"
|
|
97
|
+
method="${this.method}"
|
|
98
|
+
url="${this.endpoint}"
|
|
99
|
+
language="${this.language}"
|
|
100
|
+
?sendDataAsFormData=${this.sendDataAsFormData}
|
|
101
|
+
@ajax-success="${this.doSuccessSave}"
|
|
102
|
+
@ajax-error="${this.doErrorSave}"
|
|
103
|
+
></dile-ajax>
|
|
104
|
+
<slot name="form"></slot>
|
|
105
|
+
<dile-step-next-prev
|
|
106
|
+
.current=${this.step}
|
|
107
|
+
.steps=${this.steps}
|
|
108
|
+
?alternativeExit=${this.alternativeExit}
|
|
109
|
+
.alternativeExitLabel=${this.alternativeExitLabel}
|
|
110
|
+
.prevLabel=${this.prevLabel || this.defaultLabels?.prev}
|
|
111
|
+
.nextLabel=${this.nextLabel || this.defaultLabels?.next}
|
|
112
|
+
.endLabel=${this.endLabel || this.defaultLabels?.end}
|
|
113
|
+
language="${this.language}"
|
|
114
|
+
?loading=${this.loading}
|
|
115
|
+
@dile-step-next-click=${this.next}
|
|
116
|
+
@dile-step-alternative-exit=${this.alternativeNext}
|
|
117
|
+
@dile-step-prev-click=${this.navigatePrev}
|
|
118
|
+
></dile-step-next-prev>
|
|
119
|
+
`;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
next() {
|
|
123
|
+
this.submit(false);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
alternativeNext() {
|
|
127
|
+
this.submit(true);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
submit(alternativeExit) {
|
|
131
|
+
const form = this.form;
|
|
132
|
+
if (!form) {
|
|
133
|
+
throw new Error(
|
|
134
|
+
`[dile-step-form] No element with slot="form" was found in step ${this.step + 1}. The form element must have the slot="form" attribute.`,
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
const data = form.getData();
|
|
138
|
+
if (alternativeExit && this.alternativeExitField) {
|
|
139
|
+
data[this.alternativeExitField] = true;
|
|
140
|
+
}
|
|
141
|
+
this.alternativeExitRequested = alternativeExit;
|
|
142
|
+
this.setLoading(true);
|
|
143
|
+
this.elajax.data = data;
|
|
144
|
+
this.elajax.generateRequest();
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
setLoading(loading) {
|
|
148
|
+
this.loading = loading;
|
|
149
|
+
this.dispatchEvent(
|
|
150
|
+
new CustomEvent("dile-step-form-loading", {
|
|
151
|
+
bubbles: true,
|
|
152
|
+
composed: true,
|
|
153
|
+
detail: { loading },
|
|
154
|
+
}),
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
doSuccessSave(e) {
|
|
159
|
+
const alternativeExit = this.alternativeExitRequested;
|
|
160
|
+
this.alternativeExitRequested = false;
|
|
161
|
+
this.setLoading(false);
|
|
162
|
+
this.adapter.setResponse(e.detail);
|
|
163
|
+
const data = this.adapter.getData();
|
|
164
|
+
this.dispatchEvent(
|
|
165
|
+
new CustomEvent("dile-step-form-success", {
|
|
166
|
+
bubbles: true,
|
|
167
|
+
composed: true,
|
|
168
|
+
detail: { message: this.adapter.getMessage(), data },
|
|
169
|
+
}),
|
|
170
|
+
);
|
|
171
|
+
this.dispatchEvent(
|
|
172
|
+
new CustomEvent("dile-step-next", {
|
|
173
|
+
bubbles: true,
|
|
174
|
+
detail: { data, alternativeExit },
|
|
175
|
+
}),
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
doErrorSave(e) {
|
|
180
|
+
this.alternativeExitRequested = false;
|
|
181
|
+
this.setLoading(false);
|
|
182
|
+
this.adapter.setResponse(e.detail);
|
|
183
|
+
const errors = this.adapter.getValidationErrors();
|
|
184
|
+
this.dispatchEvent(
|
|
185
|
+
new CustomEvent("dile-step-form-error", {
|
|
186
|
+
bubbles: true,
|
|
187
|
+
composed: true,
|
|
188
|
+
detail: { message: e.detail.message, errors, status: e.detail.status },
|
|
189
|
+
}),
|
|
190
|
+
);
|
|
191
|
+
this.form?.showErrors(errors);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
navigatePrev() {
|
|
195
|
+
this.dispatchEvent(
|
|
196
|
+
new CustomEvent("dile-step-prev", {
|
|
197
|
+
bubbles: true,
|
|
198
|
+
detail: { index: this.step - 1 },
|
|
199
|
+
}),
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { LitElement, html, css } from "lit";
|
|
2
|
+
import "@dile/ui/components/button/button.js";
|
|
3
|
+
import { DileI18nMixin } from "../../../lib/DileI18nMixin.js";
|
|
4
|
+
|
|
5
|
+
export class DileStepNextPrev extends DileI18nMixin(LitElement) {
|
|
6
|
+
static styles = [
|
|
7
|
+
css`
|
|
8
|
+
:host {
|
|
9
|
+
display: block;
|
|
10
|
+
container-type: inline-size;
|
|
11
|
+
margin-top: var(--dile-step-next-prev-margin-top, 1.5rem);
|
|
12
|
+
}
|
|
13
|
+
section {
|
|
14
|
+
display: flex;
|
|
15
|
+
flex-direction: column;
|
|
16
|
+
justify-content: space-between;
|
|
17
|
+
gap: 0.25rem;
|
|
18
|
+
align-items: center;
|
|
19
|
+
}
|
|
20
|
+
@container (min-width: 420px) {
|
|
21
|
+
section {
|
|
22
|
+
flex-direction: row;
|
|
23
|
+
gap: 1rem;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
.alternative-exit {
|
|
27
|
+
flex-grow: 1;
|
|
28
|
+
display: flex;
|
|
29
|
+
justify-content: flex-end;
|
|
30
|
+
}
|
|
31
|
+
.alternative-exit button {
|
|
32
|
+
color: var(--dile-step-alternative-exit-color, var(--dile-link-color));
|
|
33
|
+
background: none;
|
|
34
|
+
border: none;
|
|
35
|
+
padding: 0;
|
|
36
|
+
font: inherit;
|
|
37
|
+
cursor: pointer;
|
|
38
|
+
text-decoration: none;
|
|
39
|
+
}
|
|
40
|
+
.alternative-exit button:disabled {
|
|
41
|
+
cursor: default;
|
|
42
|
+
opacity: 0.5;
|
|
43
|
+
}
|
|
44
|
+
`,
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
static get properties() {
|
|
48
|
+
return {
|
|
49
|
+
current: { type: Number },
|
|
50
|
+
steps: { type: Array },
|
|
51
|
+
prevLabel: { type: String },
|
|
52
|
+
nextLabel: { type: String },
|
|
53
|
+
endLabel: { type: String },
|
|
54
|
+
alternativeExit: { type: Boolean },
|
|
55
|
+
alternativeExitLabel: { type: String },
|
|
56
|
+
loading: { type: Boolean },
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
constructor() {
|
|
61
|
+
super();
|
|
62
|
+
this.current = 0;
|
|
63
|
+
this.steps = [];
|
|
64
|
+
this.alternativeExit = false;
|
|
65
|
+
this.loading = false;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
get isLast() {
|
|
69
|
+
return this.current >= this.steps.length - 1;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
get computedPrevLabel() {
|
|
73
|
+
return this.prevLabel || this.translations?.step_prev_label || "Previous";
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
get computedNextLabel() {
|
|
77
|
+
return this.nextLabel || this.translations?.step_next_label || "Next";
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
get computedEndLabel() {
|
|
81
|
+
return this.endLabel || this.translations?.step_end_label || "Finish";
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
render() {
|
|
85
|
+
return html`
|
|
86
|
+
<section>
|
|
87
|
+
<div class="prev">
|
|
88
|
+
${this.current > 0
|
|
89
|
+
? html`<dile-button ?disabled=${this.loading} @click=${this.prev}
|
|
90
|
+
>${this.computedPrevLabel}</dile-button
|
|
91
|
+
>`
|
|
92
|
+
: ""}
|
|
93
|
+
</div>
|
|
94
|
+
${this.alternativeExit && this.alternativeExitLabel
|
|
95
|
+
? html`<div class="alternative-exit">
|
|
96
|
+
<button
|
|
97
|
+
type="button"
|
|
98
|
+
?disabled=${this.loading}
|
|
99
|
+
@click=${this.alternativeExitClick}
|
|
100
|
+
>
|
|
101
|
+
${this.alternativeExitLabel}
|
|
102
|
+
</button>
|
|
103
|
+
</div>`
|
|
104
|
+
: ""}
|
|
105
|
+
<div class="next">
|
|
106
|
+
<dile-button ?loading=${this.loading} @click=${this.next}>
|
|
107
|
+
${this.isLast ? this.computedEndLabel : this.computedNextLabel}
|
|
108
|
+
</dile-button>
|
|
109
|
+
</div>
|
|
110
|
+
</section>
|
|
111
|
+
`;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
dispatchNavigationEvent(eventName) {
|
|
115
|
+
this.dispatchEvent(
|
|
116
|
+
new CustomEvent(eventName, {
|
|
117
|
+
detail: {
|
|
118
|
+
current: this.current,
|
|
119
|
+
},
|
|
120
|
+
}),
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
next() {
|
|
125
|
+
this.dispatchNavigationEvent("dile-step-next-click");
|
|
126
|
+
}
|
|
127
|
+
prev() {
|
|
128
|
+
this.dispatchNavigationEvent("dile-step-prev-click");
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
alternativeExitClick() {
|
|
132
|
+
this.dispatchNavigationEvent("dile-step-alternative-exit");
|
|
133
|
+
}
|
|
134
|
+
}
|
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
import { LitElement, html, css } from "lit";
|
|
2
|
+
import "@dile/ui/components/steps/steps.js";
|
|
3
|
+
import "../../ajax/ajax.js";
|
|
4
|
+
import "@dile/ui/components/inline-feedback/inline-feedback.js";
|
|
5
|
+
import { ResponseApiAdapter } from "../../../lib/ResponseApiAdapter.js";
|
|
6
|
+
import { DileI18nMixin } from "../../../lib/DileI18nMixin.js";
|
|
7
|
+
|
|
8
|
+
export class DileStepsForm extends DileI18nMixin(LitElement) {
|
|
9
|
+
static styles = [
|
|
10
|
+
css`
|
|
11
|
+
:host {
|
|
12
|
+
display: block;
|
|
13
|
+
}
|
|
14
|
+
dile-steps {
|
|
15
|
+
margin-bottom: 1rem;
|
|
16
|
+
}
|
|
17
|
+
.header {
|
|
18
|
+
margin-bottom: 1.5rem;
|
|
19
|
+
font-size: var(--dile-steps-form-header-font-size, 1rem);
|
|
20
|
+
color: var(--dile-steps-form-header-color, var(--dile-on-background-color, #303030));
|
|
21
|
+
}
|
|
22
|
+
.title {
|
|
23
|
+
font-weight: var(--dile-steps-form-title-font-weight, 600);
|
|
24
|
+
color: var(--dile-on-background-color, #303030);
|
|
25
|
+
}
|
|
26
|
+
.separator {
|
|
27
|
+
margin: 0 0.4rem;
|
|
28
|
+
opacity: 0.5;
|
|
29
|
+
}
|
|
30
|
+
.step-label {
|
|
31
|
+
opacity: 0.75;
|
|
32
|
+
color: var(--dile-on-background-color, #303030);
|
|
33
|
+
}
|
|
34
|
+
.header:focus-visible {
|
|
35
|
+
outline: 2px solid var(--dile-step-active-border-color, var(--dile-primary-dark-color, #0039cb));
|
|
36
|
+
outline-offset: 2px;
|
|
37
|
+
}
|
|
38
|
+
dile-inline-feedback {
|
|
39
|
+
margin-top: var(--dile-steps-form-feedback-margin-top, 1rem);
|
|
40
|
+
}
|
|
41
|
+
:host([hidefeedback]) dile-inline-feedback {
|
|
42
|
+
display: none;
|
|
43
|
+
}
|
|
44
|
+
`,
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
static get properties() {
|
|
48
|
+
return {
|
|
49
|
+
/** Labels of the steps. If empty, the label attribute of each dile-step-form is used */
|
|
50
|
+
steps: { type: Array },
|
|
51
|
+
title: { type: String },
|
|
52
|
+
/** Endpoint to load the data of every step form */
|
|
53
|
+
endpoint: { type: String },
|
|
54
|
+
current: { type: Number },
|
|
55
|
+
reached: { type: Number },
|
|
56
|
+
freeNavigation: { type: Boolean },
|
|
57
|
+
vertical: { type: Boolean },
|
|
58
|
+
/** Default labels for the navigation buttons of every step */
|
|
59
|
+
prevLabel: { type: String },
|
|
60
|
+
nextLabel: { type: String },
|
|
61
|
+
endLabel: { type: String },
|
|
62
|
+
/** Response adapter for the initial load and, if set, for every step */
|
|
63
|
+
responseAdapter: { type: Object },
|
|
64
|
+
/** Hides the inline feedback messages */
|
|
65
|
+
hideFeedback: { type: Boolean, reflect: true },
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
constructor() {
|
|
70
|
+
super();
|
|
71
|
+
this.steps = [];
|
|
72
|
+
this.current = 0;
|
|
73
|
+
this.reached = 0;
|
|
74
|
+
this.freeNavigation = false;
|
|
75
|
+
this.vertical = false;
|
|
76
|
+
this.hideFeedback = false;
|
|
77
|
+
this.defaultResponseAdapter = new ResponseApiAdapter();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
get adapter() {
|
|
81
|
+
return this.responseAdapter ?? this.defaultResponseAdapter;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
connectedCallback() {
|
|
85
|
+
super.connectedCallback();
|
|
86
|
+
this.addEventListener("dile-step-next", this.onStepNext);
|
|
87
|
+
this.addEventListener("dile-step-prev", this.onStepPrev);
|
|
88
|
+
this.addEventListener("dile-step-form-loading", this.onStepLoading);
|
|
89
|
+
this.addEventListener("dile-step-form-success", this.onStepSuccess);
|
|
90
|
+
this.addEventListener("dile-step-form-error", this.onStepError);
|
|
91
|
+
this.addEventListener("dile-step-form-label-change", this.syncStepForms);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
disconnectedCallback() {
|
|
95
|
+
super.disconnectedCallback();
|
|
96
|
+
this.removeEventListener("dile-step-next", this.onStepNext);
|
|
97
|
+
this.removeEventListener("dile-step-prev", this.onStepPrev);
|
|
98
|
+
this.removeEventListener("dile-step-form-loading", this.onStepLoading);
|
|
99
|
+
this.removeEventListener("dile-step-form-success", this.onStepSuccess);
|
|
100
|
+
this.removeEventListener("dile-step-form-error", this.onStepError);
|
|
101
|
+
this.removeEventListener("dile-step-form-label-change", this.syncStepForms);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
updated(changedProperties) {
|
|
105
|
+
if (
|
|
106
|
+
["steps", "current", "prevLabel", "nextLabel", "endLabel", "responseAdapter", "language"].some(
|
|
107
|
+
(property) => changedProperties.has(property),
|
|
108
|
+
)
|
|
109
|
+
) {
|
|
110
|
+
this.syncStepForms();
|
|
111
|
+
}
|
|
112
|
+
if (changedProperties.has("endpoint") && this.endpoint) {
|
|
113
|
+
this.fetchData();
|
|
114
|
+
}
|
|
115
|
+
if (
|
|
116
|
+
changedProperties.has("current") &&
|
|
117
|
+
changedProperties.get("current") !== undefined
|
|
118
|
+
) {
|
|
119
|
+
this.focusHeader();
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
render() {
|
|
124
|
+
return html`
|
|
125
|
+
${this.endpoint
|
|
126
|
+
? html`
|
|
127
|
+
<dile-ajax
|
|
128
|
+
id="ajaxget"
|
|
129
|
+
method="get"
|
|
130
|
+
url="${this.endpoint}"
|
|
131
|
+
language="${this.language}"
|
|
132
|
+
@ajax-success="${this.onFetchSuccess}"
|
|
133
|
+
@ajax-error="${this.onFetchError}"
|
|
134
|
+
></dile-ajax>
|
|
135
|
+
`
|
|
136
|
+
: ""}
|
|
137
|
+
${this.headerTemplate}
|
|
138
|
+
<dile-steps
|
|
139
|
+
.steps=${this.stepLabels}
|
|
140
|
+
current="${this.current}"
|
|
141
|
+
reached="${this.reached}"
|
|
142
|
+
?freeNavigation=${this.freeNavigation}
|
|
143
|
+
?vertical=${this.vertical}
|
|
144
|
+
@dile-steps-select=${this.onStepSelect}
|
|
145
|
+
></dile-steps>
|
|
146
|
+
<slot @slotchange=${this.syncStepForms}></slot>
|
|
147
|
+
<dile-inline-feedback id="feedback"></dile-inline-feedback>
|
|
148
|
+
`;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
get feedback() {
|
|
152
|
+
return this.shadowRoot.getElementById("feedback");
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
get stepForms() {
|
|
156
|
+
return [...this.querySelectorAll(":scope > dile-step-form")];
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
get stepLabels() {
|
|
160
|
+
if (this.steps.length) {
|
|
161
|
+
return this.steps;
|
|
162
|
+
}
|
|
163
|
+
return this.stepForms.map((stepForm) => stepForm.label ?? "");
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
get formElements() {
|
|
167
|
+
return this.stepForms
|
|
168
|
+
.map((stepForm) => stepForm.form)
|
|
169
|
+
.filter((form) => form);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
get currentStepLabel() {
|
|
173
|
+
return this.stepLabels[this.current] ?? "";
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
get headerTemplate() {
|
|
177
|
+
if (!this.title && !this.currentStepLabel) return "";
|
|
178
|
+
return html`
|
|
179
|
+
<div class="header" tabindex="-1" role="status">
|
|
180
|
+
${this.title
|
|
181
|
+
? html`<span class="title">${this.title}</span
|
|
182
|
+
><span class="separator">›</span
|
|
183
|
+
><span class="step-label">${this.currentStepLabel}</span>`
|
|
184
|
+
: html`<span class="step-label">${this.currentStepLabel}</span>`}
|
|
185
|
+
</div>
|
|
186
|
+
`;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
focusHeader() {
|
|
190
|
+
this.shadowRoot.querySelector(".header")?.focus();
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
syncStepForms() {
|
|
194
|
+
const forms = this.stepForms;
|
|
195
|
+
const labels = this.stepLabels;
|
|
196
|
+
forms.forEach((form, index) => {
|
|
197
|
+
form.step = index;
|
|
198
|
+
form.steps = labels;
|
|
199
|
+
form.active = index === this.current;
|
|
200
|
+
form.language = this.language;
|
|
201
|
+
form.defaultLabels = {
|
|
202
|
+
prev: this.prevLabel,
|
|
203
|
+
next: this.nextLabel,
|
|
204
|
+
end: this.endLabel,
|
|
205
|
+
};
|
|
206
|
+
if (this.responseAdapter) {
|
|
207
|
+
form.responseAdapter = this.responseAdapter;
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
if (this.steps.length && forms.length && forms.length !== this.steps.length) {
|
|
211
|
+
console.warn(
|
|
212
|
+
`[dile-steps-form] The number of <dile-step-form> elements (${forms.length}) does not match steps.length (${this.steps.length}).`,
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
this.requestUpdate();
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
dispatchLoading(loading) {
|
|
219
|
+
this.dispatchEvent(
|
|
220
|
+
new CustomEvent("dile-steps-form-loading", {
|
|
221
|
+
bubbles: true,
|
|
222
|
+
composed: true,
|
|
223
|
+
detail: { loading },
|
|
224
|
+
}),
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
fetchData() {
|
|
229
|
+
this.dispatchLoading(true);
|
|
230
|
+
this.shadowRoot.getElementById("ajaxget")?.generateRequest();
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
async onFetchSuccess(e) {
|
|
234
|
+
this.adapter.setResponse(e.detail);
|
|
235
|
+
const data = this.adapter.getData();
|
|
236
|
+
await Promise.all(this.formElements.map((form) => form.updateComplete));
|
|
237
|
+
this.setData(data);
|
|
238
|
+
this.dispatchLoading(false);
|
|
239
|
+
this.dispatchEvent(
|
|
240
|
+
new CustomEvent("dile-steps-form-loaded", {
|
|
241
|
+
bubbles: true,
|
|
242
|
+
composed: true,
|
|
243
|
+
detail: { data },
|
|
244
|
+
}),
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
onFetchError(e) {
|
|
249
|
+
this.dispatchLoading(false);
|
|
250
|
+
this.dispatchEvent(
|
|
251
|
+
new CustomEvent("dile-steps-form-error", {
|
|
252
|
+
bubbles: true,
|
|
253
|
+
composed: true,
|
|
254
|
+
detail: {
|
|
255
|
+
message: e.detail.message,
|
|
256
|
+
data: e.detail.data,
|
|
257
|
+
errors: e.detail.errors,
|
|
258
|
+
status: e.detail.status,
|
|
259
|
+
},
|
|
260
|
+
}),
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/** Loads again the data of every step form from the endpoint */
|
|
265
|
+
reload() {
|
|
266
|
+
if (this.endpoint) {
|
|
267
|
+
this.fetchData();
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/** Sets the same data in the form of every step */
|
|
272
|
+
setData(data) {
|
|
273
|
+
this.formElements.forEach((form) => form.setData(data));
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
onStepLoading(e) {
|
|
277
|
+
if (e.detail.loading) {
|
|
278
|
+
this.feedback?.clear();
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
onStepSuccess(e) {
|
|
283
|
+
this.stepForms
|
|
284
|
+
.filter((stepForm) => stepForm.refreshOnSave && !e.composedPath().includes(stepForm))
|
|
285
|
+
.forEach((stepForm) => stepForm.form?.setData(e.detail.data));
|
|
286
|
+
this.feedback?.positiveFeedbackWithDelay(
|
|
287
|
+
e.detail.message || this.translations?.step_success || "Data saved",
|
|
288
|
+
5000,
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
onStepError(e) {
|
|
293
|
+
this.feedback?.negativeFeedbackWithDelay(
|
|
294
|
+
e.detail.message || this.translations?.step_error || "Data could not be saved",
|
|
295
|
+
5000,
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
onStepNext(e) {
|
|
300
|
+
e.stopPropagation();
|
|
301
|
+
const next = this.current + 1;
|
|
302
|
+
if (next < this.stepForms.length && !e.detail.alternativeExit) {
|
|
303
|
+
this.navigateTo(next);
|
|
304
|
+
} else {
|
|
305
|
+
this.dispatchEvent(
|
|
306
|
+
new CustomEvent("dile-steps-form-complete", {
|
|
307
|
+
bubbles: true,
|
|
308
|
+
composed: true,
|
|
309
|
+
detail: {
|
|
310
|
+
data: e.detail.data,
|
|
311
|
+
alternativeExit: Boolean(e.detail.alternativeExit),
|
|
312
|
+
},
|
|
313
|
+
}),
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
onStepPrev(e) {
|
|
319
|
+
e.stopPropagation();
|
|
320
|
+
this.feedback?.clear();
|
|
321
|
+
this.navigateTo(this.current - 1);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
onStepSelect(e) {
|
|
325
|
+
e.stopPropagation();
|
|
326
|
+
this.feedback?.clear();
|
|
327
|
+
this.navigateTo(e.detail.index);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/** Shows the step at the given zero-based index */
|
|
331
|
+
navigateTo(index) {
|
|
332
|
+
if (index < 0 || index >= this.stepForms.length) return;
|
|
333
|
+
this.current = index;
|
|
334
|
+
if (index > this.reached) this.reached = index;
|
|
335
|
+
this.stepForms.forEach((stepForm, i) => {
|
|
336
|
+
stepForm.active = i === index;
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
next() {
|
|
341
|
+
this.navigateTo(this.current + 1);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
prev() {
|
|
345
|
+
this.navigateTo(this.current - 1);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import { describe, it, expect, afterEach, beforeEach, vi } from 'vitest';
|
|
2
|
+
import './steps-form.js';
|
|
3
|
+
import './step-form.js';
|
|
4
|
+
|
|
5
|
+
class TestStepForm extends HTMLElement {
|
|
6
|
+
constructor() {
|
|
7
|
+
super();
|
|
8
|
+
this.value = '';
|
|
9
|
+
this.receivedData = [];
|
|
10
|
+
this.errors = null;
|
|
11
|
+
}
|
|
12
|
+
getData() {
|
|
13
|
+
return { name: this.value };
|
|
14
|
+
}
|
|
15
|
+
setData(data) {
|
|
16
|
+
this.receivedData.push(data);
|
|
17
|
+
}
|
|
18
|
+
showErrors(errors) {
|
|
19
|
+
this.errors = errors;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
if (!customElements.get('test-step-form')) {
|
|
23
|
+
customElements.define('test-step-form', TestStepForm);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const loadedResource = { id: 1, name: 'Loaded' };
|
|
27
|
+
|
|
28
|
+
function okResponse(data, message = 'Saved') {
|
|
29
|
+
return Promise.resolve({ status: 200, data: { message, data } });
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function validationError(errors) {
|
|
33
|
+
return Promise.reject({
|
|
34
|
+
response: { status: 422, data: { message: 'Invalid data', errors } },
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const flush = () => new Promise(resolve => setTimeout(resolve, 0));
|
|
39
|
+
|
|
40
|
+
describe('dile-steps-form', () => {
|
|
41
|
+
let get;
|
|
42
|
+
let patch;
|
|
43
|
+
|
|
44
|
+
beforeEach(() => {
|
|
45
|
+
get = vi.fn(() => okResponse(loadedResource));
|
|
46
|
+
patch = vi.fn((url, data) => okResponse({ ...loadedResource, ...data, url }));
|
|
47
|
+
window.axiosInstance = { get, patch };
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
afterEach(() => {
|
|
51
|
+
document.body.innerHTML = '';
|
|
52
|
+
delete window.axiosInstance;
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
async function renderStepsForm(attributes = '', thirdStepAttributes = '') {
|
|
56
|
+
document.body.innerHTML = `
|
|
57
|
+
<dile-steps-form endpoint="/api/resource" ${attributes}>
|
|
58
|
+
<dile-step-form label="First" endpoint="/api/resource/first">
|
|
59
|
+
<test-step-form slot="form"></test-step-form>
|
|
60
|
+
</dile-step-form>
|
|
61
|
+
<dile-step-form label="Second" endpoint="/api/resource/second" alternativeExit alternativeExitLabel="Save as draft">
|
|
62
|
+
<test-step-form slot="form"></test-step-form>
|
|
63
|
+
</dile-step-form>
|
|
64
|
+
<dile-step-form label="Third" endpoint="/api/resource/third" ${thirdStepAttributes}>
|
|
65
|
+
<test-step-form slot="form"></test-step-form>
|
|
66
|
+
</dile-step-form>
|
|
67
|
+
</dile-steps-form>
|
|
68
|
+
`;
|
|
69
|
+
const el = document.body.querySelector('dile-steps-form');
|
|
70
|
+
await el.updateComplete;
|
|
71
|
+
await flush();
|
|
72
|
+
await el.updateComplete;
|
|
73
|
+
return el;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function stepForms(el) {
|
|
77
|
+
return [...el.querySelectorAll('dile-step-form')];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function forms(el) {
|
|
81
|
+
return [...el.querySelectorAll('test-step-form')];
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
it('takes the step labels from the label attribute of each step', async () => {
|
|
85
|
+
const el = await renderStepsForm();
|
|
86
|
+
|
|
87
|
+
expect(el.shadowRoot.querySelector('dile-steps').steps).toEqual(['First', 'Second', 'Third']);
|
|
88
|
+
expect(el.shadowRoot.querySelector('.step-label').textContent).toBe('First');
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('only shows the current step', async () => {
|
|
92
|
+
const el = await renderStepsForm();
|
|
93
|
+
|
|
94
|
+
expect(stepForms(el).map(step => step.active)).toEqual([true, false, false]);
|
|
95
|
+
expect(stepForms(el).map(step => getComputedStyle(step).display)).toEqual(['block', 'none', 'none']);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('loads the endpoint and sets the data in every form', async () => {
|
|
99
|
+
const el = await renderStepsForm();
|
|
100
|
+
let loadedDetail;
|
|
101
|
+
el.addEventListener('dile-steps-form-loaded', e => { loadedDetail = e.detail; });
|
|
102
|
+
|
|
103
|
+
el.reload();
|
|
104
|
+
await flush();
|
|
105
|
+
|
|
106
|
+
expect(get).toHaveBeenCalledWith('/api/resource', expect.anything());
|
|
107
|
+
expect(loadedDetail).toEqual({ data: loadedResource });
|
|
108
|
+
forms(el).forEach(form => expect(form.receivedData.at(-1)).toEqual(loadedResource));
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('saves the step with PATCH and moves to the next step', async () => {
|
|
112
|
+
const el = await renderStepsForm();
|
|
113
|
+
forms(el)[0].value = 'Acme';
|
|
114
|
+
let successDetail;
|
|
115
|
+
el.addEventListener('dile-step-form-success', e => { successDetail = e.detail; });
|
|
116
|
+
|
|
117
|
+
stepForms(el)[0].next();
|
|
118
|
+
await flush();
|
|
119
|
+
await el.updateComplete;
|
|
120
|
+
|
|
121
|
+
expect(patch).toHaveBeenCalledWith('/api/resource/first', { name: 'Acme' }, undefined);
|
|
122
|
+
expect(successDetail.message).toBe('Saved');
|
|
123
|
+
expect(el.current).toBe(1);
|
|
124
|
+
expect(stepForms(el).map(step => step.active)).toEqual([false, true, false]);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('dispatches dile-steps-form-complete after saving the last step', async () => {
|
|
128
|
+
const el = await renderStepsForm();
|
|
129
|
+
let completeDetail;
|
|
130
|
+
el.addEventListener('dile-steps-form-complete', e => { completeDetail = e.detail; });
|
|
131
|
+
|
|
132
|
+
el.navigateTo(2);
|
|
133
|
+
stepForms(el)[2].next();
|
|
134
|
+
await flush();
|
|
135
|
+
|
|
136
|
+
expect(completeDetail.alternativeExit).toBe(false);
|
|
137
|
+
expect(completeDetail.data.url).toBe('/api/resource/third');
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('completes with the alternative exit and sends the alternativeExit field', async () => {
|
|
141
|
+
const el = await renderStepsForm();
|
|
142
|
+
let completeDetail;
|
|
143
|
+
el.addEventListener('dile-steps-form-complete', e => { completeDetail = e.detail; });
|
|
144
|
+
|
|
145
|
+
el.navigateTo(1);
|
|
146
|
+
stepForms(el)[1].alternativeNext();
|
|
147
|
+
await flush();
|
|
148
|
+
|
|
149
|
+
expect(patch).toHaveBeenCalledWith('/api/resource/second', { name: '', alternativeExit: true }, undefined);
|
|
150
|
+
expect(completeDetail.alternativeExit).toBe(true);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it('does not keep the alternative exit after a failed save', async () => {
|
|
154
|
+
const el = await renderStepsForm();
|
|
155
|
+
let completed = false;
|
|
156
|
+
el.addEventListener('dile-steps-form-complete', () => { completed = true; });
|
|
157
|
+
el.navigateTo(1);
|
|
158
|
+
|
|
159
|
+
patch.mockImplementationOnce(() => validationError({ name: ['Required'] }));
|
|
160
|
+
stepForms(el)[1].alternativeNext();
|
|
161
|
+
await flush();
|
|
162
|
+
expect(forms(el)[1].errors).toEqual({ name: ['Required'] });
|
|
163
|
+
|
|
164
|
+
stepForms(el)[1].next();
|
|
165
|
+
await flush();
|
|
166
|
+
|
|
167
|
+
expect(completed).toBe(false);
|
|
168
|
+
expect(el.current).toBe(2);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it('dispatches dile-step-form-error with the validation errors and status', async () => {
|
|
172
|
+
const el = await renderStepsForm();
|
|
173
|
+
let errorDetail;
|
|
174
|
+
el.addEventListener('dile-step-form-error', e => { errorDetail = e.detail; });
|
|
175
|
+
patch.mockImplementationOnce(() => validationError({ name: ['Required'] }));
|
|
176
|
+
|
|
177
|
+
stepForms(el)[0].next();
|
|
178
|
+
await flush();
|
|
179
|
+
|
|
180
|
+
expect(errorDetail).toEqual({ message: 'Invalid data', errors: { name: ['Required'] }, status: 422 });
|
|
181
|
+
expect(el.current).toBe(0);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it('sends the saved data to the steps with refreshOnSave', async () => {
|
|
185
|
+
const el = await renderStepsForm('', 'refreshOnSave');
|
|
186
|
+
forms(el).forEach(form => { form.receivedData = []; });
|
|
187
|
+
|
|
188
|
+
stepForms(el)[0].next();
|
|
189
|
+
await flush();
|
|
190
|
+
|
|
191
|
+
expect(forms(el)[2].receivedData).toHaveLength(1);
|
|
192
|
+
expect(forms(el)[2].receivedData[0].url).toBe('/api/resource/first');
|
|
193
|
+
expect(forms(el)[1].receivedData).toHaveLength(0);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it('dispatches dile-steps-form-error with the status when the load fails', async () => {
|
|
197
|
+
get.mockImplementation(() => Promise.reject({
|
|
198
|
+
response: { status: 404, data: { message: 'Not found' } },
|
|
199
|
+
}));
|
|
200
|
+
let errorDetail;
|
|
201
|
+
document.body.addEventListener('dile-steps-form-error', e => { errorDetail = e.detail; });
|
|
202
|
+
|
|
203
|
+
await renderStepsForm();
|
|
204
|
+
await flush();
|
|
205
|
+
|
|
206
|
+
expect(errorDetail.status).toBe(404);
|
|
207
|
+
expect(errorDetail.message).toBe('Not found');
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it('translates the navigation labels with the language property', async () => {
|
|
211
|
+
const el = await renderStepsForm('language="es"');
|
|
212
|
+
const nextPrev = stepForms(el)[0].shadowRoot.querySelector('dile-step-next-prev');
|
|
213
|
+
|
|
214
|
+
await vi.waitFor(() => {
|
|
215
|
+
expect(nextPrev.shadowRoot.querySelector('.next dile-button').textContent.trim()).toBe('Siguiente');
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it('uses the labels defined in the step before the translations', async () => {
|
|
220
|
+
const el = await renderStepsForm('', 'endLabel="Create invoice"');
|
|
221
|
+
el.navigateTo(2);
|
|
222
|
+
await el.updateComplete;
|
|
223
|
+
await stepForms(el)[2].updateComplete;
|
|
224
|
+
const nextPrev = stepForms(el)[2].shadowRoot.querySelector('dile-step-next-prev');
|
|
225
|
+
await nextPrev.updateComplete;
|
|
226
|
+
|
|
227
|
+
expect(nextPrev.shadowRoot.querySelector('.next dile-button').textContent.trim()).toBe('Create invoice');
|
|
228
|
+
expect(nextPrev.shadowRoot.querySelector('.prev dile-button').textContent.trim()).toBe('Previous');
|
|
229
|
+
});
|
|
230
|
+
});
|
package/lib/i18n/en.js
CHANGED
|
@@ -77,4 +77,9 @@ export const translations = {
|
|
|
77
77
|
retry_label: "Retry",
|
|
78
78
|
list_view_label: "List view",
|
|
79
79
|
grid_view_label: "Grid view",
|
|
80
|
-
|
|
80
|
+
step_prev_label: "Previous",
|
|
81
|
+
step_next_label: "Next",
|
|
82
|
+
step_end_label: "Finish",
|
|
83
|
+
step_success: "Data saved",
|
|
84
|
+
step_error: "Data could not be saved",
|
|
85
|
+
};
|
package/lib/i18n/es.js
CHANGED
|
@@ -77,4 +77,9 @@ export const translations = {
|
|
|
77
77
|
retry_label: "Reintentar",
|
|
78
78
|
list_view_label: "Lista",
|
|
79
79
|
grid_view_label: "Rejilla",
|
|
80
|
-
|
|
80
|
+
step_prev_label: "Anterior",
|
|
81
|
+
step_next_label: "Siguiente",
|
|
82
|
+
step_end_label: "Finalizar",
|
|
83
|
+
step_success: "Datos guardados",
|
|
84
|
+
step_error: "No se han podido guardar los datos",
|
|
85
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dile/crud",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.10.0",
|
|
4
4
|
"description": "Components to create a generic crud system based on Web Components and Lit",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -25,12 +25,12 @@
|
|
|
25
25
|
},
|
|
26
26
|
"homepage": "https://dile-components.com/",
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@dile/ui": "^3.
|
|
28
|
+
"@dile/ui": "^3.18.0",
|
|
29
29
|
"axios": "^1.19.0",
|
|
30
30
|
"lit": "^2.7.0 || ^3.0.0"
|
|
31
31
|
},
|
|
32
32
|
"publishConfig": {
|
|
33
33
|
"access": "public"
|
|
34
34
|
},
|
|
35
|
-
"gitHead": "
|
|
35
|
+
"gitHead": "1b065736666cc26f8afc1106ec7ffc98feaec60e"
|
|
36
36
|
}
|