@dile/crud 2.6.4 → 2.7.2
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 +142 -0
- package/components/crud/src/DileCrud.js +23 -3
- package/components/list/__screenshots__/crud-data-grid.spec.js/dile-crud-data-grid-always-forces-sort-mode-to-external-regardless-of-config-1.png +0 -0
- package/components/list/__screenshots__/crud-data-grid.spec.js/dile-crud-data-grid-forwards-stickyFirstColumn--striped--responsiveMode-and-emptyMessage-with-sensible-defaults-1.png +0 -0
- package/components/list/__screenshots__/crud-data-grid.spec.js/dile-crud-data-grid-leaves-computeRowId-unset-when-config-computeItemId-is-not-a-function-1.png +0 -0
- package/components/list/crud-data-grid.js +5 -0
- package/components/list/crud-data-grid.spec.js +207 -0
- package/components/list/crud-item-actions.js +5 -0
- package/components/list/crud-item-actions.spec.js +122 -0
- package/components/list/crud-list.spec.js +319 -0
- package/components/list/index.js +4 -2
- package/components/list/src/DileCrudDataGrid.js +93 -0
- package/components/list/src/DileCrudItemActions.js +191 -0
- package/components/list/src/DileCrudList.js +62 -11
- package/components/list/src/DileCrudListItem.js +78 -124
- package/components/ui/__screenshots__/crud-sort-form.spec.js/dile-crud-sort-form-syncSort-suppresses-exactly-one-radio-group-echo--preserving-the-synced-direction-instead-of-falling-back-to-the-option-static-default-1.png +0 -0
- package/components/ui/crud-sort-form.spec.js +132 -0
- package/components/ui/src/DileCrudSortForm.js +24 -5
- package/lib/CrudConfigBuilder.spec.js +32 -0
- package/lib/deepMerge.spec.js +45 -0
- package/lib/defaultConfig.js +4 -1
- package/package.json +3 -3
|
@@ -7,6 +7,7 @@ import '../../list/crud-list-pagination-links.js';
|
|
|
7
7
|
import '../crud-list-item.js';
|
|
8
8
|
import '../crud-select-all.js';
|
|
9
9
|
import '../crud-list-service.js';
|
|
10
|
+
import '../crud-data-grid.js';
|
|
10
11
|
import { DileI18nMixin } from '../../../lib/DileI18nMixin.js';
|
|
11
12
|
|
|
12
13
|
export class DileCrudList extends DileI18nMixin(DileLoading(LitElement)) {
|
|
@@ -67,9 +68,14 @@ export class DileCrudList extends DileI18nMixin(DileLoading(LitElement)) {
|
|
|
67
68
|
}
|
|
68
69
|
.elements-container {
|
|
69
70
|
display: grid;
|
|
70
|
-
grid-template-columns: var(--dile-crud-list-elements-container-template-columns,
|
|
71
|
+
grid-template-columns: var(--dile-crud-list-elements-container-template-columns, minmax(0, 1fr));
|
|
71
72
|
gap: var(--dile-crud-list-elements-container-gap, 0);
|
|
72
73
|
}
|
|
74
|
+
.grid-container {
|
|
75
|
+
display: block;
|
|
76
|
+
width: 100%;
|
|
77
|
+
margin: var(--dile-crud-list-grid-container-margin, 0 0 1rem 0);
|
|
78
|
+
}
|
|
73
79
|
`
|
|
74
80
|
];
|
|
75
81
|
|
|
@@ -94,7 +100,7 @@ export class DileCrudList extends DileI18nMixin(DileLoading(LitElement)) {
|
|
|
94
100
|
|
|
95
101
|
constructor() {
|
|
96
102
|
super();
|
|
97
|
-
this.paginationData = {}
|
|
103
|
+
this.paginationData = {};
|
|
98
104
|
this.elements = [];
|
|
99
105
|
this.pageSize = 10;
|
|
100
106
|
this.keyword = '';
|
|
@@ -207,6 +213,7 @@ export class DileCrudList extends DileI18nMixin(DileLoading(LitElement)) {
|
|
|
207
213
|
></dile-ajax>
|
|
208
214
|
`;
|
|
209
215
|
}
|
|
216
|
+
|
|
210
217
|
get emptyTemplate() {
|
|
211
218
|
return html`
|
|
212
219
|
<div class="empty">
|
|
@@ -243,6 +250,33 @@ export class DileCrudList extends DileI18nMixin(DileLoading(LitElement)) {
|
|
|
243
250
|
}
|
|
244
251
|
|
|
245
252
|
get elementsTemplate() {
|
|
253
|
+
if (this.config?.templates?.grid && typeof this.config.templates.grid === 'function') {
|
|
254
|
+
return html`
|
|
255
|
+
<div
|
|
256
|
+
class="grid-container"
|
|
257
|
+
@item-checkbox-changed=${this.onItemsCheckboxChanged}
|
|
258
|
+
@dile-data-grid-sort=${this.onGridSort}
|
|
259
|
+
>
|
|
260
|
+
${this.config.templates.grid(this.elements, this.actionIds, this.config, this.sort)}
|
|
261
|
+
</div>
|
|
262
|
+
`;
|
|
263
|
+
}
|
|
264
|
+
if (this.config?.grid?.columns && Array.isArray(this.config.grid.columns)) {
|
|
265
|
+
return html`
|
|
266
|
+
<div
|
|
267
|
+
class="grid-container"
|
|
268
|
+
@item-checkbox-changed=${this.onItemsCheckboxChanged}
|
|
269
|
+
@dile-data-grid-sort=${this.onGridSort}
|
|
270
|
+
>
|
|
271
|
+
<dile-crud-data-grid
|
|
272
|
+
.items=${this.elements}
|
|
273
|
+
.selectedIds=${this.actionIds}
|
|
274
|
+
.config=${this.config}
|
|
275
|
+
.sort=${this.sort}
|
|
276
|
+
></dile-crud-data-grid>
|
|
277
|
+
</div>
|
|
278
|
+
`;
|
|
279
|
+
}
|
|
246
280
|
return html`
|
|
247
281
|
<div class="elements-container">
|
|
248
282
|
${this.elements.map(element => html`
|
|
@@ -252,7 +286,7 @@ export class DileCrudList extends DileI18nMixin(DileLoading(LitElement)) {
|
|
|
252
286
|
.actionIds="${this.actionIds}"
|
|
253
287
|
?disableEdit=${!this.isItemEditable(element)}
|
|
254
288
|
?disableDelete=${!this.isItemDeletable(element)}
|
|
255
|
-
?disableRestore=${this.config?.customization?.disableRestore}
|
|
289
|
+
?disableRestore=${this.config?.customization?.disableRestore || this.config?.customization?.disableListActions}
|
|
256
290
|
?hideCheckboxSelection="${this.config?.customization?.hideCheckboxSelection}"
|
|
257
291
|
@item-checkbox-changed=${this.onItemsCheckboxChanged}
|
|
258
292
|
?isDeleted=${element.deleted_at}
|
|
@@ -276,13 +310,13 @@ export class DileCrudList extends DileI18nMixin(DileLoading(LitElement)) {
|
|
|
276
310
|
}
|
|
277
311
|
|
|
278
312
|
isItemEditable(element) {
|
|
279
|
-
const isGloballyDisabled = this.config?.customization?.disableEdit;
|
|
313
|
+
const isGloballyDisabled = this.config?.customization?.disableListActions || this.config?.customization?.disableEdit;
|
|
280
314
|
const isItemEditable = this.config?.isItemEditable(element) ?? true;
|
|
281
315
|
return !isGloballyDisabled && isItemEditable;
|
|
282
316
|
}
|
|
283
317
|
|
|
284
318
|
isItemDeletable(element) {
|
|
285
|
-
const isGloballyDisabled = this.config?.customization?.disableDelete;
|
|
319
|
+
const isGloballyDisabled = this.config?.customization?.disableListActions || this.config?.customization?.disableDelete;
|
|
286
320
|
const isItemDeletable = this.config?.isItemDeletable(element) ?? true;
|
|
287
321
|
return !isGloballyDisabled && isItemDeletable;
|
|
288
322
|
}
|
|
@@ -314,11 +348,11 @@ export class DileCrudList extends DileI18nMixin(DileLoading(LitElement)) {
|
|
|
314
348
|
}
|
|
315
349
|
|
|
316
350
|
goNext() {
|
|
317
|
-
this.elservice.goNext()
|
|
351
|
+
this.elservice.goNext();
|
|
318
352
|
}
|
|
319
353
|
|
|
320
354
|
goPrev() {
|
|
321
|
-
this.elservice.goPrev()
|
|
355
|
+
this.elservice.goPrev();
|
|
322
356
|
}
|
|
323
357
|
|
|
324
358
|
refresh() {
|
|
@@ -332,7 +366,6 @@ export class DileCrudList extends DileI18nMixin(DileLoading(LitElement)) {
|
|
|
332
366
|
this.shadowRoot.querySelector('dile-crud-select-all').reset();
|
|
333
367
|
}
|
|
334
368
|
this.elservice.refresh();
|
|
335
|
-
|
|
336
369
|
}
|
|
337
370
|
|
|
338
371
|
setKeyword(keyword) {
|
|
@@ -345,7 +378,26 @@ export class DileCrudList extends DileI18nMixin(DileLoading(LitElement)) {
|
|
|
345
378
|
this.loading = true;
|
|
346
379
|
this.sort = sortObject;
|
|
347
380
|
this.elservice.setSort(sortObject);
|
|
348
|
-
|
|
381
|
+
this.dispatchEvent(new CustomEvent('crud-list-sort-changed', {
|
|
382
|
+
bubbles: true,
|
|
383
|
+
composed: true,
|
|
384
|
+
detail: sortObject,
|
|
385
|
+
}));
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
onGridSort(e) {
|
|
389
|
+
if (e.detail?.field) {
|
|
390
|
+
this.setSort({
|
|
391
|
+
sortField: e.detail.field,
|
|
392
|
+
sortDirection: e.detail.direction || 'asc',
|
|
393
|
+
});
|
|
394
|
+
} else {
|
|
395
|
+
this.setSort({
|
|
396
|
+
sortField: null,
|
|
397
|
+
sortDirection: null,
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
}
|
|
349
401
|
|
|
350
402
|
setPageSize(size) {
|
|
351
403
|
this.loading = true;
|
|
@@ -389,7 +441,7 @@ export class DileCrudList extends DileI18nMixin(DileLoading(LitElement)) {
|
|
|
389
441
|
if(this.config.customization?.disablePagination) {
|
|
390
442
|
let ids = this.elements.map(item => this.config.computeItemId(item));
|
|
391
443
|
this.dispactSelectAll(ids);
|
|
392
|
-
} else{
|
|
444
|
+
} else {
|
|
393
445
|
let data = {
|
|
394
446
|
keyword: this.keyword,
|
|
395
447
|
filters: this.filters,
|
|
@@ -413,7 +465,6 @@ export class DileCrudList extends DileI18nMixin(DileLoading(LitElement)) {
|
|
|
413
465
|
if (!e.detail.checked && this.isSelectAllActive) {
|
|
414
466
|
this.shadowRoot.querySelector('dile-crud-select-all').resetWithoutDispatch();
|
|
415
467
|
}
|
|
416
|
-
|
|
417
468
|
}
|
|
418
469
|
|
|
419
470
|
onListError(e) {
|
|
@@ -1,67 +1,42 @@
|
|
|
1
|
-
import { LitElement, html, css } from 'lit';
|
|
1
|
+
import { LitElement, html, css, nothing } from 'lit';
|
|
2
2
|
import '@dile/ui/components/checkbox/checkbox.js';
|
|
3
|
-
import '
|
|
4
|
-
import '@dile/iconlib/material-icons/edit.js';
|
|
5
|
-
import '@dile/iconlib/material-icons/delete.js';
|
|
6
|
-
import '@dile/iconlib/material-icons/restore-from-trash.js';
|
|
3
|
+
import '../crud-item-actions.js';
|
|
7
4
|
|
|
8
5
|
export class DileCrudListItem extends LitElement {
|
|
9
6
|
static styles = [
|
|
10
7
|
css`
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
8
|
+
:host {
|
|
9
|
+
display: var(--dile-crud-list-item-display, block);
|
|
10
|
+
max-width: var(--dile-crud-list-item-max-width, 100%);
|
|
11
|
+
width: var(--dile-crud-list-item-width, 100%);
|
|
12
|
+
}
|
|
13
|
+
section {
|
|
14
|
+
max-width: 100%;
|
|
15
|
+
overflow: hidden;
|
|
16
|
+
display: flex;
|
|
17
|
+
align-items: center;
|
|
18
|
+
padding: var(--dile-crud-list-item-padding, 0.5rem 0.5rem);
|
|
19
|
+
border-bottom: var(--dile-crud-list-item-line-separator, 1px solid #ddd);
|
|
20
|
+
}
|
|
21
|
+
dile-checkbox {
|
|
22
|
+
margin-right: 0.5rem;
|
|
23
|
+
--dile-checkbox-unchecked-color: #888;
|
|
24
|
+
}
|
|
25
|
+
main {
|
|
26
|
+
flex-grow: 1;
|
|
27
|
+
overflow: hidden;
|
|
28
|
+
}
|
|
29
|
+
.actions {
|
|
30
|
+
margin-left: 0.5rem;
|
|
31
|
+
display: flex;
|
|
32
|
+
align-items: center;
|
|
33
|
+
text-align: right;
|
|
34
|
+
}
|
|
35
|
+
@media (min-width: 550px) {
|
|
16
36
|
section {
|
|
17
|
-
|
|
18
|
-
overflow: hidden;
|
|
19
|
-
display: flex;
|
|
20
|
-
align-items: center;
|
|
21
|
-
padding: var(--dile-crud-list-item-padding, 0.5rem 0.5rem);
|
|
22
|
-
border-bottom: var(--dile-crud-list-item-line-separator, 1px solid #ddd);
|
|
23
|
-
}
|
|
24
|
-
dile-checkbox {
|
|
25
|
-
margin-right: 0.5rem;
|
|
26
|
-
--dile-checkbox-unchecked-color: #888;
|
|
27
|
-
}
|
|
28
|
-
main {
|
|
29
|
-
flex-grow: 1;
|
|
30
|
-
overflow: hidden;
|
|
31
|
-
}
|
|
32
|
-
.actions {
|
|
33
|
-
margin-left: 0.5rem;
|
|
34
|
-
display: flex;
|
|
35
|
-
align-items: center;
|
|
36
|
-
text-align: right;
|
|
37
|
-
}
|
|
38
|
-
dile-button.action-button {
|
|
39
|
-
--dile-button-background-color: var(--dile-crud-list-item-action-button-background-color, transparent);
|
|
40
|
-
--dile-button-border-color: var(--dile-crud-list-item-action-button-border-color, transparent);
|
|
41
|
-
--dile-button-hover-background-color: var(--dile-crud-list-item-action-button-hover-background-color, var(--dile-neutral-color, transparent));
|
|
42
|
-
--dile-button-hover-border-color: var(--dile-crud-list-item-action-button-hover-border-color, transparent);
|
|
43
|
-
--dile-button-padding-y: var(--dile-crud-list-item-action-button-padding-y, 0.25rem);
|
|
44
|
-
--dile-button-padding-x: var(--dile-crud-list-item-action-button-padding-x, 0.25rem);
|
|
45
|
-
--dile-button-icon-size: var(--dile-crud-list-item-action-button-icon-size, 24px);
|
|
46
|
-
--dile-button-border-radius: var(--dile-crud-list-item-action-button-border-radius, 2rem);
|
|
47
|
-
}
|
|
48
|
-
dile-button.action-button.edit {
|
|
49
|
-
--dile-button-icon-color: var(--edit-icon-color, var(--dile-alert-neutral-color, #2889a7));
|
|
50
|
-
--dile-button-icon-hover-color: var(--dile-crud-list-item-edit-icon-hover-color, var(--edit-icon-color, var(--dile-alert-neutral-color, #2889a7)));
|
|
51
|
-
}
|
|
52
|
-
dile-button.action-button.delete {
|
|
53
|
-
--dile-button-icon-color: var(--delete-icon-color, var(--dile-danger-color, #e33));
|
|
54
|
-
--dile-button-icon-hover-color: var(--dile-crud-list-item-delete-icon-hover-color, var(--delete-icon-color, var(--dile-danger-color, #e33)));
|
|
55
|
-
}
|
|
56
|
-
dile-button.action-button.restore {
|
|
57
|
-
--dile-button-icon-color: var(--restore-icon-color, var(--dile-alert-success-color, #00900f));
|
|
58
|
-
--dile-button-icon-hover-color: var(--dile-crud-list-item-restore-icon-hover-color, var(--restore-icon-color, var(--dile-alert-success-color, #00900f)));
|
|
59
|
-
}
|
|
60
|
-
@media(min-width: 550px) {
|
|
61
|
-
section {
|
|
62
|
-
padding: var(--dile-crud-list-item-padding, 0.5rem 1rem);
|
|
63
|
-
}
|
|
37
|
+
padding: var(--dile-crud-list-item-padding, 0.5rem 1rem);
|
|
64
38
|
}
|
|
39
|
+
}
|
|
65
40
|
`
|
|
66
41
|
];
|
|
67
42
|
|
|
@@ -72,6 +47,7 @@ export class DileCrudListItem extends LitElement {
|
|
|
72
47
|
actionIds: { type: Array },
|
|
73
48
|
disableEdit: { type: Boolean },
|
|
74
49
|
disableDelete: { type: Boolean },
|
|
50
|
+
disableRestore: { type: Boolean },
|
|
75
51
|
/** Hide checkboxes on the item list */
|
|
76
52
|
hideCheckboxSelection: { type: Boolean },
|
|
77
53
|
isDeleted: { type: Boolean },
|
|
@@ -85,86 +61,64 @@ export class DileCrudListItem extends LitElement {
|
|
|
85
61
|
this.disableDelete = false;
|
|
86
62
|
this.disableRestore = false;
|
|
87
63
|
this.hideCheckboxSelection = false;
|
|
64
|
+
this.isDeleted = false;
|
|
65
|
+
}
|
|
88
66
|
|
|
67
|
+
get hasActions() {
|
|
68
|
+
if (this.isDeleted) {
|
|
69
|
+
return !this.disableRestore;
|
|
70
|
+
}
|
|
71
|
+
return !this.disableEdit || !this.disableDelete;
|
|
89
72
|
}
|
|
90
73
|
|
|
91
74
|
render() {
|
|
92
75
|
return html`
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
76
|
+
<section>
|
|
77
|
+
${this.hideCheckboxSelection
|
|
78
|
+
? ''
|
|
79
|
+
: html`
|
|
80
|
+
<dile-checkbox
|
|
81
|
+
?checked="${this.includes(this.actionIds, this.itemId)}"
|
|
82
|
+
@dile-checkbox-changed=${this.checkboxChanged}
|
|
83
|
+
></dile-checkbox>
|
|
96
84
|
`}
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
85
|
+
<main>
|
|
86
|
+
<slot></slot>
|
|
87
|
+
</main>
|
|
88
|
+
${this.hasActions
|
|
89
|
+
? html`
|
|
90
|
+
<div class="actions">
|
|
91
|
+
<dile-crud-item-actions
|
|
92
|
+
.item=${this.item}
|
|
93
|
+
itemId="${this.itemId}"
|
|
94
|
+
?disableEdit=${this.disableEdit}
|
|
95
|
+
?disableDelete=${this.disableDelete}
|
|
96
|
+
?disableRestore=${this.disableRestore}
|
|
97
|
+
?isDeleted=${this.isDeleted}
|
|
98
|
+
></dile-crud-item-actions>
|
|
99
|
+
</div>
|
|
100
|
+
`
|
|
101
|
+
: nothing
|
|
102
|
+
}
|
|
103
|
+
</section>
|
|
107
104
|
`;
|
|
108
105
|
}
|
|
109
106
|
|
|
110
|
-
get regularActionsTemplate() {
|
|
111
|
-
return html`
|
|
112
|
-
${this.disableEdit
|
|
113
|
-
? ''
|
|
114
|
-
: html`<dile-button class="action-button edit" icon="material.edit" label="Edit" @click=${this.editClick}></dile-button>`
|
|
115
|
-
}
|
|
116
|
-
${this.disableDelete
|
|
117
|
-
? ''
|
|
118
|
-
: html`<dile-button class="action-button delete" icon="material.delete" label="Delete" @click=${this.deleteClick}></dile-button>`
|
|
119
|
-
}
|
|
120
|
-
`
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
get restoreActionsTemplate() {
|
|
124
|
-
return html`
|
|
125
|
-
${this.disableRestore
|
|
126
|
-
? ''
|
|
127
|
-
: html`<dile-button class="action-button restore" icon="material.restore-from-trash" label="Restore" @click=${this.restoreClick}></dile-button>`
|
|
128
|
-
}
|
|
129
|
-
`
|
|
130
|
-
}
|
|
131
|
-
|
|
132
107
|
includes(actionIds, itemId) {
|
|
133
108
|
const stringIds = actionIds.map(String);
|
|
134
109
|
return stringIds.includes(String(itemId));
|
|
135
110
|
}
|
|
136
111
|
|
|
137
112
|
checkboxChanged(e) {
|
|
138
|
-
this.dispatchEvent(
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
_dispatchItemEvent(eventName) {
|
|
149
|
-
this.dispatchEvent(new CustomEvent(eventName, {
|
|
150
|
-
bubbles: true,
|
|
151
|
-
composed: true,
|
|
152
|
-
detail: {
|
|
153
|
-
item: this.item,
|
|
154
|
-
itemId: this.itemId,
|
|
155
|
-
}
|
|
156
|
-
}));
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
editClick() {
|
|
160
|
-
this._dispatchItemEvent('crud-item-edit');
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
deleteClick() {
|
|
164
|
-
this._dispatchItemEvent('crud-item-delete');
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
restoreClick() {
|
|
168
|
-
this._dispatchItemEvent('crud-item-restore');
|
|
113
|
+
this.dispatchEvent(
|
|
114
|
+
new CustomEvent('item-checkbox-changed', {
|
|
115
|
+
bubbles: true,
|
|
116
|
+
composed: true,
|
|
117
|
+
detail: {
|
|
118
|
+
checked: e.detail.checked,
|
|
119
|
+
itemId: this.itemId,
|
|
120
|
+
},
|
|
121
|
+
})
|
|
122
|
+
);
|
|
169
123
|
}
|
|
170
|
-
}
|
|
124
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { describe, it, expect, afterEach } from 'vitest';
|
|
2
|
+
import './crud-sort-form.js';
|
|
3
|
+
|
|
4
|
+
describe('dile-crud-sort-form', () => {
|
|
5
|
+
afterEach(() => {
|
|
6
|
+
document.body.innerHTML = '';
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
const sortOptions = [
|
|
10
|
+
{ name: 'name', label: 'Name', direction: 'asc' },
|
|
11
|
+
{ name: 'id', label: 'Id', direction: 'asc' },
|
|
12
|
+
];
|
|
13
|
+
|
|
14
|
+
async function createSortForm(props = {}) {
|
|
15
|
+
const el = document.createElement('dile-crud-sort-form');
|
|
16
|
+
el.sortOptions = props.sortOptions ?? sortOptions;
|
|
17
|
+
if (props.sortField !== undefined) {
|
|
18
|
+
el.sortField = props.sortField;
|
|
19
|
+
}
|
|
20
|
+
document.body.appendChild(el);
|
|
21
|
+
await el.updateComplete;
|
|
22
|
+
return el;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
it('renders one radio/order-switch pair per configured sort option', async () => {
|
|
26
|
+
const el = await createSortForm();
|
|
27
|
+
expect(el.shadowRoot.querySelectorAll('dile-radio').length).toBe(2);
|
|
28
|
+
expect(el.shadowRoot.querySelectorAll('dile-order-switch').length).toBe(2);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('getDirection returns the configured direction for a known field', async () => {
|
|
32
|
+
const el = await createSortForm();
|
|
33
|
+
await el.shadowRoot.querySelector('dile-order-switch[name="name"]').updateComplete;
|
|
34
|
+
expect(el.getDirection('name')).toBe('asc');
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('getDirection returns undefined instead of throwing for a field with no matching order-switch', async () => {
|
|
38
|
+
// Reproduces a DataGrid column marked sortable whose field isn't listed in config.sort.options.
|
|
39
|
+
const el = await createSortForm();
|
|
40
|
+
expect(() => el.getDirection('continent')).not.toThrow();
|
|
41
|
+
expect(el.getDirection('continent')).toBeUndefined();
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('getDirection returns undefined for a falsy field without touching the DOM', async () => {
|
|
45
|
+
const el = await createSortForm();
|
|
46
|
+
expect(el.getDirection(null)).toBeUndefined();
|
|
47
|
+
expect(el.getDirection('')).toBeUndefined();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// "year" has a static configured direction of "desc" so it can expose the regression:
|
|
51
|
+
// syncing an "asc" sort must not get silently corrected back to "desc".
|
|
52
|
+
const yearOptions = [{ name: 'year', label: 'Year', direction: 'desc' }];
|
|
53
|
+
|
|
54
|
+
it('syncSort reflects an externally-applied sort without re-dispatching sort-changed', async () => {
|
|
55
|
+
const el = await createSortForm({ sortOptions: yearOptions });
|
|
56
|
+
|
|
57
|
+
let dispatchCount = 0;
|
|
58
|
+
el.addEventListener('sort-changed', () => { dispatchCount++; });
|
|
59
|
+
|
|
60
|
+
el.syncSort('year', 'asc');
|
|
61
|
+
await el.updateComplete;
|
|
62
|
+
|
|
63
|
+
expect(el.sortField).toBe('year');
|
|
64
|
+
expect(el.sortDirection).toBe('asc');
|
|
65
|
+
expect(dispatchCount).toBe(0);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('a genuine radio-group selection (not preceded by syncSort) still defaults to the option configured direction', async () => {
|
|
69
|
+
const el = await createSortForm({ sortOptions: yearOptions });
|
|
70
|
+
|
|
71
|
+
let receivedDetail = null;
|
|
72
|
+
el.addEventListener('sort-changed', (e) => { receivedDetail = e.detail; });
|
|
73
|
+
|
|
74
|
+
const radioGroup = el.shadowRoot.querySelector('dile-radio-group');
|
|
75
|
+
radioGroup.dispatchEvent(new CustomEvent('dile-radio-group-changed', {
|
|
76
|
+
detail: { name: 'selector', value: 'year' },
|
|
77
|
+
}));
|
|
78
|
+
await el.updateComplete;
|
|
79
|
+
|
|
80
|
+
expect(el.sortField).toBe('year');
|
|
81
|
+
expect(el.sortDirection).toBe('desc');
|
|
82
|
+
expect(receivedDetail).toEqual({ sortField: 'year', sortDirection: 'desc' });
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('clicking the label of an inactive option switches to it using its own configured direction, without flipping it', async () => {
|
|
86
|
+
const el = await createSortForm({
|
|
87
|
+
sortOptions: [
|
|
88
|
+
{ name: 'name', label: 'Name', direction: 'asc' },
|
|
89
|
+
{ name: 'year', label: 'Year', direction: 'desc' },
|
|
90
|
+
],
|
|
91
|
+
sortField: 'name',
|
|
92
|
+
});
|
|
93
|
+
el.sortDirection = 'asc';
|
|
94
|
+
await el.updateComplete;
|
|
95
|
+
|
|
96
|
+
let receivedDetail = null;
|
|
97
|
+
el.addEventListener('sort-changed', (e) => { receivedDetail = e.detail; });
|
|
98
|
+
|
|
99
|
+
el.shadowRoot.querySelector('dile-order-switch[name="year"]').shadowRoot.querySelector('span').click();
|
|
100
|
+
await el.updateComplete;
|
|
101
|
+
|
|
102
|
+
expect(el.sortField).toBe('year');
|
|
103
|
+
expect(el.sortDirection).toBe('desc'); // year's own configured default, not flipped
|
|
104
|
+
expect(receivedDetail).toEqual({ sortField: 'year', sortDirection: 'desc' });
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('clicking the label of the already-active option toggles its direction instead of doing nothing', async () => {
|
|
108
|
+
// Reproduces the reported confusion: clicking "Name" again while already sorted by
|
|
109
|
+
// name silently did nothing, because the label always re-announced the same value.
|
|
110
|
+
const el = await createSortForm({
|
|
111
|
+
sortOptions: [{ name: 'name', label: 'Name', direction: 'asc' }],
|
|
112
|
+
sortField: 'name',
|
|
113
|
+
});
|
|
114
|
+
el.sortDirection = 'asc';
|
|
115
|
+
await el.updateComplete;
|
|
116
|
+
|
|
117
|
+
let receivedDetail = null;
|
|
118
|
+
el.addEventListener('sort-changed', (e) => { receivedDetail = e.detail; });
|
|
119
|
+
|
|
120
|
+
const label = () => el.shadowRoot.querySelector('dile-order-switch[name="name"]').shadowRoot.querySelector('span');
|
|
121
|
+
|
|
122
|
+
label().click();
|
|
123
|
+
await el.updateComplete;
|
|
124
|
+
expect(el.sortField).toBe('name');
|
|
125
|
+
expect(el.sortDirection).toBe('desc'); // toggled, not a no-op
|
|
126
|
+
expect(receivedDetail).toEqual({ sortField: 'name', sortDirection: 'desc' });
|
|
127
|
+
|
|
128
|
+
label().click();
|
|
129
|
+
await el.updateComplete;
|
|
130
|
+
expect(el.sortDirection).toBe('asc'); // toggles back on a second click
|
|
131
|
+
});
|
|
132
|
+
});
|
|
@@ -45,6 +45,19 @@ export class DileCrudSortForm extends DileI18nMixin(LitElement) {
|
|
|
45
45
|
constructor() {
|
|
46
46
|
super();
|
|
47
47
|
this.sortOptions = [];
|
|
48
|
+
this._suppressChangeEcho = false;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Reflects a sort that was already applied elsewhere (e.g. a DataGrid column header
|
|
52
|
+
// click) without re-dispatching sort-changed. Setting `sortField` below causes the
|
|
53
|
+
// inner <dile-radio-group> to echo a "changed" event on its own (it can't tell a
|
|
54
|
+
// programmatic update from a user click), which radioGroupChanged() would otherwise
|
|
55
|
+
// treat as a real selection and re-derive sortDirection from the option's *static*
|
|
56
|
+
// configured default — silently overriding whichever direction was actually requested.
|
|
57
|
+
syncSort(sortField, sortDirection) {
|
|
58
|
+
this._suppressChangeEcho = true;
|
|
59
|
+
this.sortField = sortField;
|
|
60
|
+
this.sortDirection = sortDirection;
|
|
48
61
|
}
|
|
49
62
|
|
|
50
63
|
render() {
|
|
@@ -63,10 +76,11 @@ export class DileCrudSortForm extends DileI18nMixin(LitElement) {
|
|
|
63
76
|
?checked=${this.sortField === option.name}
|
|
64
77
|
value="${option.name}"
|
|
65
78
|
></dile-radio>
|
|
66
|
-
<dile-order-switch
|
|
67
|
-
label="${option.label}"
|
|
68
|
-
name="${option.name}"
|
|
69
|
-
value="${option.direction}"
|
|
79
|
+
<dile-order-switch
|
|
80
|
+
label="${option.label}"
|
|
81
|
+
name="${option.name}"
|
|
82
|
+
value="${option.direction}"
|
|
83
|
+
?selected=${this.sortField === option.name}
|
|
70
84
|
@element-changed=${this.elementChanged}
|
|
71
85
|
></dile-order-switch>
|
|
72
86
|
</div>
|
|
@@ -83,6 +97,10 @@ export class DileCrudSortForm extends DileI18nMixin(LitElement) {
|
|
|
83
97
|
}
|
|
84
98
|
|
|
85
99
|
radioGroupChanged(e) {
|
|
100
|
+
if (this._suppressChangeEcho) {
|
|
101
|
+
this._suppressChangeEcho = false;
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
86
104
|
this.sortField = e.detail.value;
|
|
87
105
|
this.sortDirection = this.getDirection(this.sortField);
|
|
88
106
|
this.dispatchChanged();
|
|
@@ -90,7 +108,8 @@ export class DileCrudSortForm extends DileI18nMixin(LitElement) {
|
|
|
90
108
|
|
|
91
109
|
getDirection(field) {
|
|
92
110
|
if(field) {
|
|
93
|
-
|
|
111
|
+
const orderSwitch = this.shadowRoot.querySelector(`dile-order-switch[name="${field}"]`);
|
|
112
|
+
return orderSwitch ? orderSwitch.value : undefined;
|
|
94
113
|
}
|
|
95
114
|
}
|
|
96
115
|
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { CrudConfigBuilder } from './CrudConfigBuilder.js';
|
|
3
|
+
|
|
4
|
+
describe('CrudConfigBuilder', () => {
|
|
5
|
+
it('merges customConfig.grid.columns over the default null grid', () => {
|
|
6
|
+
const columns = [{ field: 'id', header: 'ID' }];
|
|
7
|
+
|
|
8
|
+
const config = new CrudConfigBuilder('https://example.test/api/items', {
|
|
9
|
+
grid: { columns },
|
|
10
|
+
}).getConfig();
|
|
11
|
+
|
|
12
|
+
expect(config.grid.columns).toEqual(columns);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('sets the endpoint from the constructor argument', () => {
|
|
16
|
+
const config = new CrudConfigBuilder('https://example.test/api/items', {}).getConfig();
|
|
17
|
+
|
|
18
|
+
expect(config.endpoint).toBe('https://example.test/api/items');
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('getConfig(endpoint) returns a copy with an overridden endpoint without mutating the original config', () => {
|
|
22
|
+
const builder = new CrudConfigBuilder('https://example.test/api/items', {
|
|
23
|
+
grid: { columns: [{ field: 'id' }] },
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
const relatedConfig = builder.getConfig('https://example.test/api/items/1/related');
|
|
27
|
+
|
|
28
|
+
expect(relatedConfig.endpoint).toBe('https://example.test/api/items/1/related');
|
|
29
|
+
expect(builder.getConfig().endpoint).toBe('https://example.test/api/items');
|
|
30
|
+
expect(relatedConfig.grid.columns).toEqual([{ field: 'id' }]);
|
|
31
|
+
});
|
|
32
|
+
});
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { deepMerge } from './deepMerge.js';
|
|
3
|
+
|
|
4
|
+
describe('deepMerge', () => {
|
|
5
|
+
it('replaces a null target property with a plain object from source', () => {
|
|
6
|
+
const target = { grid: null };
|
|
7
|
+
const source = { grid: { columns: [{ field: 'id' }] } };
|
|
8
|
+
|
|
9
|
+
const result = deepMerge(target, source);
|
|
10
|
+
|
|
11
|
+
expect(result.grid).toEqual({ columns: [{ field: 'id' }] });
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it('preserves array order and content when merging a columns array under a previously-null key', () => {
|
|
15
|
+
const columns = [
|
|
16
|
+
{ field: 'id', header: 'ID' },
|
|
17
|
+
{ field: 'name', header: 'Name' },
|
|
18
|
+
{ field: 'continent', header: 'Continent' },
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
const result = deepMerge({ grid: null }, { grid: { columns } });
|
|
22
|
+
|
|
23
|
+
expect(result.grid.columns).toEqual(columns);
|
|
24
|
+
expect(result.grid.columns).not.toBe(columns);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('does not mutate the source when merging into a null target key', () => {
|
|
28
|
+
const source = { grid: { columns: [{ field: 'id' }] } };
|
|
29
|
+
|
|
30
|
+
const result = deepMerge({ grid: null }, source);
|
|
31
|
+
result.grid.columns.push({ field: 'extra' });
|
|
32
|
+
|
|
33
|
+
expect(source.grid.columns.length).toBe(1);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('overwrites configured overwriteProperties instances as-is instead of merging them', () => {
|
|
37
|
+
class Adapter {}
|
|
38
|
+
const targetAdapter = new Adapter();
|
|
39
|
+
const sourceAdapter = new Adapter();
|
|
40
|
+
|
|
41
|
+
const result = deepMerge({ responseAdapter: targetAdapter }, { responseAdapter: sourceAdapter });
|
|
42
|
+
|
|
43
|
+
expect(result.responseAdapter).toBe(sourceAdapter);
|
|
44
|
+
});
|
|
45
|
+
});
|