@dile/crud 2.7.1 → 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/crud-data-grid.spec.js +15 -0
- package/components/list/crud-list.spec.js +65 -1
- package/components/list/src/DileCrudDataGrid.js +4 -0
- package/components/list/src/DileCrudList.js +8 -2
- 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/package.json +3 -3
|
@@ -132,3 +132,145 @@ describe('dile-crud initial filters', () => {
|
|
|
132
132
|
expect(select.value).toBe('');
|
|
133
133
|
});
|
|
134
134
|
});
|
|
135
|
+
|
|
136
|
+
describe('dile-crud sort synchronization', () => {
|
|
137
|
+
afterEach(() => {
|
|
138
|
+
document.body.innerHTML = '';
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('syncs dile-crud-sort-form when the list sort changes (e.g. from a DataGrid column header)', async () => {
|
|
142
|
+
const config = new CrudConfigBuilder('https://example.test/api/customers', {
|
|
143
|
+
customization: {
|
|
144
|
+
disableInsert: true,
|
|
145
|
+
disableHelp: true,
|
|
146
|
+
disableKeywordSearch: true,
|
|
147
|
+
disableSort: false,
|
|
148
|
+
disableFilter: true,
|
|
149
|
+
disablePagination: true,
|
|
150
|
+
hideCheckboxSelection: true,
|
|
151
|
+
},
|
|
152
|
+
sort: {
|
|
153
|
+
options: [
|
|
154
|
+
{ name: 'name', label: 'Name', direction: 'asc' },
|
|
155
|
+
],
|
|
156
|
+
initialSortField: null,
|
|
157
|
+
},
|
|
158
|
+
}).getConfig();
|
|
159
|
+
|
|
160
|
+
const el = document.createElement('dile-crud');
|
|
161
|
+
el.config = config;
|
|
162
|
+
document.body.appendChild(el);
|
|
163
|
+
await el.updateComplete;
|
|
164
|
+
await Promise.resolve();
|
|
165
|
+
|
|
166
|
+
expect(el.sortFormElement).toBeTruthy();
|
|
167
|
+
|
|
168
|
+
el.listElement.setSort({ sortField: 'name', sortDirection: 'desc' });
|
|
169
|
+
await el.updateComplete;
|
|
170
|
+
|
|
171
|
+
expect(el.sortFormElement.sortField).toBe('name');
|
|
172
|
+
expect(el.sortFormElement.sortDirection).toBe('desc');
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it('ignores a grid sort field that is not listed in config.sort.options instead of crashing the sort-form', async () => {
|
|
176
|
+
// Reproduces a DataGrid column marked `sortable: true` for a field (e.g. "continent")
|
|
177
|
+
// that has no corresponding entry in config.sort.options.
|
|
178
|
+
const config = new CrudConfigBuilder('https://example.test/api/countries', {
|
|
179
|
+
customization: {
|
|
180
|
+
disableInsert: true,
|
|
181
|
+
disableHelp: true,
|
|
182
|
+
disableKeywordSearch: true,
|
|
183
|
+
disableSort: false,
|
|
184
|
+
disableFilter: true,
|
|
185
|
+
disablePagination: true,
|
|
186
|
+
hideCheckboxSelection: true,
|
|
187
|
+
},
|
|
188
|
+
grid: {
|
|
189
|
+
columns: [
|
|
190
|
+
{ field: 'name', header: 'Name', sortable: true },
|
|
191
|
+
{ field: 'continent', header: 'Continent', sortable: true },
|
|
192
|
+
],
|
|
193
|
+
},
|
|
194
|
+
sort: {
|
|
195
|
+
options: [
|
|
196
|
+
{ name: 'name', label: 'Name', direction: 'asc' },
|
|
197
|
+
],
|
|
198
|
+
initialSortField: 'name',
|
|
199
|
+
},
|
|
200
|
+
}).getConfig();
|
|
201
|
+
|
|
202
|
+
const el = document.createElement('dile-crud');
|
|
203
|
+
el.config = config;
|
|
204
|
+
document.body.appendChild(el);
|
|
205
|
+
await el.updateComplete;
|
|
206
|
+
await Promise.resolve();
|
|
207
|
+
|
|
208
|
+
const initialSortField = el.sortFormElement.sortField;
|
|
209
|
+
|
|
210
|
+
expect(() => {
|
|
211
|
+
el.listElement.dispatchEvent(new CustomEvent('crud-list-sort-changed', {
|
|
212
|
+
bubbles: true,
|
|
213
|
+
composed: true,
|
|
214
|
+
detail: { sortField: 'continent', sortDirection: 'asc' },
|
|
215
|
+
}));
|
|
216
|
+
}).not.toThrow();
|
|
217
|
+
await el.updateComplete;
|
|
218
|
+
|
|
219
|
+
// The sort-form's selection is left untouched rather than pointed at an unknown option.
|
|
220
|
+
expect(el.sortFormElement.sortField).toBe(initialSortField);
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it('does not let the sort-form echo override a grid-driven sort with the field static configured default direction', async () => {
|
|
224
|
+
// "year" is configured with a static default direction of "desc". A DataGrid column
|
|
225
|
+
// header click always starts at "asc" on first click — reproduces the exact scenario
|
|
226
|
+
// where the sort silently snapped back to "desc" and repeated clicks appeared to
|
|
227
|
+
// "lose" the requested order.
|
|
228
|
+
const config = new CrudConfigBuilder('https://example.test/api/board-games', {
|
|
229
|
+
customization: {
|
|
230
|
+
disableInsert: true,
|
|
231
|
+
disableHelp: true,
|
|
232
|
+
disableKeywordSearch: true,
|
|
233
|
+
disableSort: false,
|
|
234
|
+
disableFilter: true,
|
|
235
|
+
disablePagination: true,
|
|
236
|
+
hideCheckboxSelection: true,
|
|
237
|
+
},
|
|
238
|
+
grid: {
|
|
239
|
+
columns: [
|
|
240
|
+
{ field: 'year', header: 'Year', sortable: true },
|
|
241
|
+
],
|
|
242
|
+
},
|
|
243
|
+
sort: {
|
|
244
|
+
options: [
|
|
245
|
+
{ name: 'year', label: 'Year', direction: 'desc' },
|
|
246
|
+
],
|
|
247
|
+
initialSortField: 'year',
|
|
248
|
+
},
|
|
249
|
+
}).getConfig();
|
|
250
|
+
|
|
251
|
+
const el = document.createElement('dile-crud');
|
|
252
|
+
el.config = config;
|
|
253
|
+
document.body.appendChild(el);
|
|
254
|
+
await el.updateComplete;
|
|
255
|
+
await Promise.resolve();
|
|
256
|
+
|
|
257
|
+
let lastSetSort = null;
|
|
258
|
+
el.listElement.setSort = (sortObj) => {
|
|
259
|
+
lastSetSort = sortObj;
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
// Simulate what DileCrudList.setSort() dispatches after a grid header click requesting "asc".
|
|
263
|
+
el.listElement.dispatchEvent(new CustomEvent('crud-list-sort-changed', {
|
|
264
|
+
bubbles: true,
|
|
265
|
+
composed: true,
|
|
266
|
+
detail: { sortField: 'year', sortDirection: 'asc' },
|
|
267
|
+
}));
|
|
268
|
+
await el.updateComplete;
|
|
269
|
+
await new Promise((resolve) => setTimeout(resolve, 0)); // flush any echo microtasks
|
|
270
|
+
|
|
271
|
+
expect(el.sortFormElement.sortField).toBe('year');
|
|
272
|
+
expect(el.sortFormElement.sortDirection).toBe('asc');
|
|
273
|
+
// The echo must not have triggered a second, corrupting setSort() call.
|
|
274
|
+
expect(lastSetSort).toBeNull();
|
|
275
|
+
});
|
|
276
|
+
});
|
|
@@ -124,6 +124,9 @@ export class DileCrud extends DileI18nMixin(DileCrudMixin(LitElement)) {
|
|
|
124
124
|
get filtersElement() {
|
|
125
125
|
return this.shadowRoot.getElementById('elfilters');
|
|
126
126
|
}
|
|
127
|
+
get sortFormElement() {
|
|
128
|
+
return this.shadowRoot.getElementById('elsort');
|
|
129
|
+
}
|
|
127
130
|
|
|
128
131
|
// TEMPLATES
|
|
129
132
|
|
|
@@ -145,6 +148,7 @@ export class DileCrud extends DileI18nMixin(DileCrudMixin(LitElement)) {
|
|
|
145
148
|
@item-checkbox-changed=${this.itemCheckboxChanged}
|
|
146
149
|
@dile-chip-icon-click=${this.removeFilter}
|
|
147
150
|
@crud-list-all-ids-selected=${this.crudSelectAll}
|
|
151
|
+
@crud-list-sort-changed=${this.listSortChanged}
|
|
148
152
|
>
|
|
149
153
|
${this.listTemplate}
|
|
150
154
|
</div>
|
|
@@ -265,14 +269,15 @@ export class DileCrud extends DileI18nMixin(DileCrudMixin(LitElement)) {
|
|
|
265
269
|
${this.config.customization.disableSort
|
|
266
270
|
? ''
|
|
267
271
|
: html`
|
|
268
|
-
<dile-crud-sort-form
|
|
272
|
+
<dile-crud-sort-form
|
|
273
|
+
id="elsort"
|
|
269
274
|
class="action-controller"
|
|
270
|
-
.sortOptions=${this.config.sort.options || []}
|
|
275
|
+
.sortOptions=${this.config.sort.options || []}
|
|
271
276
|
sortField="${this.config.sort.initialSortField}"
|
|
272
277
|
sortDirection="${this.config.sort.initialSortDirection || 'desc'}"
|
|
273
278
|
@sort-changed=${this.sortFormChanged}
|
|
274
279
|
language="${this.language}"
|
|
275
|
-
></dile-crud-sort-form>
|
|
280
|
+
></dile-crud-sort-form>
|
|
276
281
|
`
|
|
277
282
|
}
|
|
278
283
|
</div>
|
|
@@ -343,6 +348,21 @@ export class DileCrud extends DileI18nMixin(DileCrudMixin(LitElement)) {
|
|
|
343
348
|
this.listElement.setSort(e.detail);
|
|
344
349
|
}
|
|
345
350
|
|
|
351
|
+
listSortChanged(e) {
|
|
352
|
+
if (!this.sortFormElement) {
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
const sortField = e.detail?.sortField;
|
|
356
|
+
const isKnownSortOption = !sortField || (this.config.sort?.options || []).some(option => option.name === sortField);
|
|
357
|
+
if (!isKnownSortOption) {
|
|
358
|
+
// The sort came from a source the sort-form doesn't know about (e.g. a DataGrid
|
|
359
|
+
// column not listed in config.sort.options) — leave the form's selection as-is
|
|
360
|
+
// instead of pointing it at an option it can't render.
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
this.sortFormElement.syncSort(sortField, e.detail?.sortDirection);
|
|
364
|
+
}
|
|
365
|
+
|
|
346
366
|
pageSizeChanged(e) {
|
|
347
367
|
this.listElement.setPageSize(e.detail.pageSize);
|
|
348
368
|
}
|
|
@@ -24,6 +24,9 @@ describe('dile-crud-data-grid', () => {
|
|
|
24
24
|
el.config = config;
|
|
25
25
|
el.items = props.items ?? sampleItems;
|
|
26
26
|
el.selectedIds = props.selectedIds ?? [];
|
|
27
|
+
if (props.sort !== undefined) {
|
|
28
|
+
el.sort = props.sort;
|
|
29
|
+
}
|
|
27
30
|
document.body.appendChild(el);
|
|
28
31
|
await el.updateComplete;
|
|
29
32
|
return el;
|
|
@@ -147,6 +150,18 @@ describe('dile-crud-data-grid', () => {
|
|
|
147
150
|
expect(innerGrid(el).sortMode).toBe('external');
|
|
148
151
|
});
|
|
149
152
|
|
|
153
|
+
it('forwards the sort property as sortField/sortDirection so the header keeps its sorted state', async () => {
|
|
154
|
+
const el = await createGrid({ grid: { columns: sampleColumns } }, { sort: { sortField: 'name', sortDirection: 'desc' } });
|
|
155
|
+
expect(innerGrid(el).sortField).toBe('name');
|
|
156
|
+
expect(innerGrid(el).sortDirection).toBe('desc');
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('defaults sortField/sortDirection to empty strings when sort is not set', async () => {
|
|
160
|
+
const el = await createGrid({ grid: { columns: sampleColumns } });
|
|
161
|
+
expect(innerGrid(el).sortField).toBe('');
|
|
162
|
+
expect(innerGrid(el).sortDirection).toBe('');
|
|
163
|
+
});
|
|
164
|
+
|
|
150
165
|
it('wires computeRowId from config.computeItemId so selection matches custom ids', async () => {
|
|
151
166
|
const el = await createGrid(
|
|
152
167
|
{
|
|
@@ -52,6 +52,7 @@ describe('dile-crud-list grid template integration', () => {
|
|
|
52
52
|
el.disableLoadOnStart = true;
|
|
53
53
|
el.config = config;
|
|
54
54
|
el.elements = sampleElements;
|
|
55
|
+
el.sort = { sortField: 'name', sortDirection: 'desc' };
|
|
55
56
|
el.loading = false;
|
|
56
57
|
document.body.appendChild(el);
|
|
57
58
|
await el.updateComplete;
|
|
@@ -64,6 +65,7 @@ describe('dile-crud-list grid template integration', () => {
|
|
|
64
65
|
expect(crudDataGrid.items).toEqual(sampleElements);
|
|
65
66
|
expect(crudDataGrid.columns.length).toBe(3); // id, name + automatically injected Actions
|
|
66
67
|
expect(crudDataGrid.columns[2].header).toBe('Actions');
|
|
68
|
+
expect(crudDataGrid.sort).toEqual({ sortField: 'name', sortDirection: 'desc' });
|
|
67
69
|
});
|
|
68
70
|
|
|
69
71
|
it('hides actions in traditional list items when disableListActions is true', async () => {
|
|
@@ -96,12 +98,14 @@ describe('dile-crud-list grid template integration', () => {
|
|
|
96
98
|
it('renders custom grid template when templates.grid is configured', async () => {
|
|
97
99
|
let receivedElements = null;
|
|
98
100
|
let receivedActionIds = null;
|
|
101
|
+
let receivedSort = null;
|
|
99
102
|
|
|
100
103
|
const config = new CrudConfigBuilder('https://example.test/api/customers', {
|
|
101
104
|
templates: {
|
|
102
|
-
grid: (elements, actionIds) => {
|
|
105
|
+
grid: (elements, actionIds, config, sort) => {
|
|
103
106
|
receivedElements = elements;
|
|
104
107
|
receivedActionIds = actionIds;
|
|
108
|
+
receivedSort = sort;
|
|
105
109
|
return html`
|
|
106
110
|
<dile-data-grid
|
|
107
111
|
.items="${elements}"
|
|
@@ -122,6 +126,7 @@ describe('dile-crud-list grid template integration', () => {
|
|
|
122
126
|
el.config = config;
|
|
123
127
|
el.elements = sampleElements;
|
|
124
128
|
el.actionIds = [2];
|
|
129
|
+
el.sort = { sortField: 'name', sortDirection: 'asc' };
|
|
125
130
|
el.loading = false;
|
|
126
131
|
document.body.appendChild(el);
|
|
127
132
|
await el.updateComplete;
|
|
@@ -136,6 +141,7 @@ describe('dile-crud-list grid template integration', () => {
|
|
|
136
141
|
expect(dataGrid).toBeTruthy();
|
|
137
142
|
expect(receivedElements).toEqual(sampleElements);
|
|
138
143
|
expect(receivedActionIds).toEqual([2]);
|
|
144
|
+
expect(receivedSort).toEqual({ sortField: 'name', sortDirection: 'asc' });
|
|
139
145
|
});
|
|
140
146
|
|
|
141
147
|
it('updates sort when dile-data-grid-sort is dispatched', async () => {
|
|
@@ -252,4 +258,62 @@ describe('dile-crud-list grid template integration', () => {
|
|
|
252
258
|
expect(el.shadowRoot.querySelector('dile-crud-data-grid')).toBeNull();
|
|
253
259
|
expect(el.shadowRoot.querySelector('dile-data-grid')).toBeTruthy();
|
|
254
260
|
});
|
|
261
|
+
|
|
262
|
+
it('dispatches crud-list-sort-changed with the new sort whenever setSort is called', async () => {
|
|
263
|
+
const config = new CrudConfigBuilder('https://example.test/api/customers', {
|
|
264
|
+
grid: { columns: [{ field: 'name', header: 'Name', sortable: true }] },
|
|
265
|
+
}).getConfig();
|
|
266
|
+
|
|
267
|
+
const el = document.createElement('dile-crud-list');
|
|
268
|
+
el.disableLoadOnStart = true;
|
|
269
|
+
el.config = config;
|
|
270
|
+
el.elements = sampleElements;
|
|
271
|
+
el.loading = false;
|
|
272
|
+
document.body.appendChild(el);
|
|
273
|
+
await el.updateComplete;
|
|
274
|
+
|
|
275
|
+
let receivedDetail = null;
|
|
276
|
+
el.addEventListener('crud-list-sort-changed', (e) => {
|
|
277
|
+
receivedDetail = e.detail;
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
el.setSort({ sortField: 'name', sortDirection: 'desc' });
|
|
281
|
+
|
|
282
|
+
expect(receivedDetail).toEqual({ sortField: 'name', sortDirection: 'desc' });
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
it('keeps reporting the correct sort state to a freshly remounted grid after a loading round-trip', async () => {
|
|
286
|
+
// Reproduces the real scenario: setSort() sets loading=true, which unmounts
|
|
287
|
+
// elementsTemplate (and therefore <dile-crud-data-grid>/<dile-data-grid>) while
|
|
288
|
+
// the request is in flight, then getSuccess() remounts it from scratch.
|
|
289
|
+
const config = new CrudConfigBuilder('https://example.test/api/customers', {
|
|
290
|
+
grid: { columns: [{ field: 'name', header: 'Name', sortable: true }] },
|
|
291
|
+
}).getConfig();
|
|
292
|
+
|
|
293
|
+
const el = document.createElement('dile-crud-list');
|
|
294
|
+
el.disableLoadOnStart = true;
|
|
295
|
+
el.config = config;
|
|
296
|
+
el.elements = sampleElements;
|
|
297
|
+
el.loading = false;
|
|
298
|
+
document.body.appendChild(el);
|
|
299
|
+
await el.updateComplete;
|
|
300
|
+
|
|
301
|
+
el.elservice.setSort = () => {}; // avoid a real network round-trip
|
|
302
|
+
el.setSort({ sortField: 'name', sortDirection: 'asc' });
|
|
303
|
+
await el.updateComplete;
|
|
304
|
+
|
|
305
|
+
expect(el.shadowRoot.querySelector('.grid-container')).toBeNull(); // unmounted while loading
|
|
306
|
+
|
|
307
|
+
el.getSuccess({ detail: { elements: sampleElements, numItems: 2, paginationData: {} } });
|
|
308
|
+
await el.updateComplete;
|
|
309
|
+
|
|
310
|
+
const freshDataGrid = el.shadowRoot.querySelector('dile-crud-data-grid');
|
|
311
|
+
expect(freshDataGrid).toBeTruthy();
|
|
312
|
+
expect(freshDataGrid.sort).toEqual({ sortField: 'name', sortDirection: 'asc' });
|
|
313
|
+
|
|
314
|
+
const innerGrid = freshDataGrid.shadowRoot.querySelector('dile-data-grid');
|
|
315
|
+
await innerGrid.updateComplete;
|
|
316
|
+
expect(innerGrid.sortField).toBe('name');
|
|
317
|
+
expect(innerGrid.sortDirection).toBe('asc');
|
|
318
|
+
});
|
|
255
319
|
});
|
|
@@ -17,6 +17,7 @@ export class DileCrudDataGrid extends LitElement {
|
|
|
17
17
|
items: { type: Array },
|
|
18
18
|
selectedIds: { type: Array },
|
|
19
19
|
config: { type: Object },
|
|
20
|
+
sort: { type: Object },
|
|
20
21
|
};
|
|
21
22
|
}
|
|
22
23
|
|
|
@@ -25,6 +26,7 @@ export class DileCrudDataGrid extends LitElement {
|
|
|
25
26
|
this.items = [];
|
|
26
27
|
this.selectedIds = [];
|
|
27
28
|
this.config = null;
|
|
29
|
+
this.sort = null;
|
|
28
30
|
}
|
|
29
31
|
|
|
30
32
|
get columns() {
|
|
@@ -77,6 +79,8 @@ export class DileCrudDataGrid extends LitElement {
|
|
|
77
79
|
.selectedIds=${this.selectedIds}
|
|
78
80
|
.computeRowId=${this.computeRowId}
|
|
79
81
|
.rowClass=${this.rowClass}
|
|
82
|
+
.sortField=${this.sort?.sortField || ''}
|
|
83
|
+
.sortDirection=${this.sort?.sortDirection || ''}
|
|
80
84
|
?selectable=${gridConfig.selectable !== undefined ? gridConfig.selectable : !this.config?.customization?.hideCheckboxSelection}
|
|
81
85
|
?sticky-first-column=${gridConfig.stickyFirstColumn !== undefined ? gridConfig.stickyFirstColumn : true}
|
|
82
86
|
?striped=${gridConfig.striped !== undefined ? gridConfig.striped : true}
|
|
@@ -257,7 +257,7 @@ export class DileCrudList extends DileI18nMixin(DileLoading(LitElement)) {
|
|
|
257
257
|
@item-checkbox-changed=${this.onItemsCheckboxChanged}
|
|
258
258
|
@dile-data-grid-sort=${this.onGridSort}
|
|
259
259
|
>
|
|
260
|
-
${this.config.templates.grid(this.elements, this.actionIds, this.config)}
|
|
260
|
+
${this.config.templates.grid(this.elements, this.actionIds, this.config, this.sort)}
|
|
261
261
|
</div>
|
|
262
262
|
`;
|
|
263
263
|
}
|
|
@@ -272,6 +272,7 @@ export class DileCrudList extends DileI18nMixin(DileLoading(LitElement)) {
|
|
|
272
272
|
.items=${this.elements}
|
|
273
273
|
.selectedIds=${this.actionIds}
|
|
274
274
|
.config=${this.config}
|
|
275
|
+
.sort=${this.sort}
|
|
275
276
|
></dile-crud-data-grid>
|
|
276
277
|
</div>
|
|
277
278
|
`;
|
|
@@ -377,7 +378,12 @@ export class DileCrudList extends DileI18nMixin(DileLoading(LitElement)) {
|
|
|
377
378
|
this.loading = true;
|
|
378
379
|
this.sort = sortObject;
|
|
379
380
|
this.elservice.setSort(sortObject);
|
|
380
|
-
|
|
381
|
+
this.dispatchEvent(new CustomEvent('crud-list-sort-changed', {
|
|
382
|
+
bubbles: true,
|
|
383
|
+
composed: true,
|
|
384
|
+
detail: sortObject,
|
|
385
|
+
}));
|
|
386
|
+
}
|
|
381
387
|
|
|
382
388
|
onGridSort(e) {
|
|
383
389
|
if (e.detail?.field) {
|
|
@@ -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
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dile/crud",
|
|
3
|
-
"version": "2.7.
|
|
3
|
+
"version": "2.7.2",
|
|
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.12.
|
|
28
|
+
"@dile/ui": "^3.12.2",
|
|
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": "27dbd2411bd5856021209c49da6aed285c3fae28"
|
|
36
36
|
}
|