@dile/crud 2.2.7 → 2.3.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/list/src/DileCrudListService.js +6 -1
- package/components/many-relation/src/DileManyRelation.js +6 -6
- package/components/select-ajax-simple/__screenshots__/select-ajax-simple.spec.js/dile-select-ajax-simple-supports-getSelectResultList-as-custom-extractor-1.png +0 -0
- package/components/select-ajax-simple/index.js +1 -0
- package/components/select-ajax-simple/select-ajax-simple.js +5 -0
- package/components/select-ajax-simple/select-ajax-simple.spec.js +133 -0
- package/components/select-ajax-simple/src/DileSelectAjaxSimple.js +172 -0
- package/components/ui/src/DileCrudFilters.js +1 -0
- package/components/ui/src/DileCrudFiltersForm.js +22 -0
- package/package.json +2 -2
|
@@ -70,10 +70,15 @@ export class DileCrudListService extends DileI18nMixin(LitElement) {
|
|
|
70
70
|
|
|
71
71
|
doRefresh() {
|
|
72
72
|
this.delayTimer = null;
|
|
73
|
+
// Normalize filter types: select_ajax → select for backend compatibility
|
|
74
|
+
const normalizedFilters = this.filters.map(filter => ({
|
|
75
|
+
...filter,
|
|
76
|
+
type: filter.type === 'select_ajax' ? 'select' : filter.type
|
|
77
|
+
}));
|
|
73
78
|
let data = {
|
|
74
79
|
per_page: this.pageSize,
|
|
75
80
|
keyword: this.keyword,
|
|
76
|
-
filters:
|
|
81
|
+
filters: normalizedFilters,
|
|
77
82
|
}
|
|
78
83
|
if (this.sort && this.sort.sortField) {
|
|
79
84
|
data.sortField = this.sort.sortField;
|
|
@@ -165,7 +165,7 @@ export class DileManyRelation extends DileI18nMixin(LitElement) {
|
|
|
165
165
|
this._ajaxrem = this.shadowRoot.getElementById('ajaxrem');
|
|
166
166
|
|
|
167
167
|
if (this.loadFromEndpoint) {
|
|
168
|
-
this.
|
|
168
|
+
this.refreshList();
|
|
169
169
|
}
|
|
170
170
|
}
|
|
171
171
|
|
|
@@ -268,8 +268,8 @@ export class DileManyRelation extends DileI18nMixin(LitElement) {
|
|
|
268
268
|
|
|
269
269
|
_onAddSuccess(e) {
|
|
270
270
|
this._selectedId = null;
|
|
271
|
-
this.
|
|
272
|
-
this.
|
|
271
|
+
this.clearSelect();
|
|
272
|
+
this.refreshList();
|
|
273
273
|
if (this.hideErrorOnInput && this.errored) {
|
|
274
274
|
this.message = '';
|
|
275
275
|
this.errored = false;
|
|
@@ -290,7 +290,7 @@ export class DileManyRelation extends DileI18nMixin(LitElement) {
|
|
|
290
290
|
|
|
291
291
|
_onRemoveSuccess() {
|
|
292
292
|
this._removingItemId = undefined;
|
|
293
|
-
this.
|
|
293
|
+
this.refreshList();
|
|
294
294
|
this._dispatch('many-relation-remove-success');
|
|
295
295
|
}
|
|
296
296
|
|
|
@@ -299,7 +299,7 @@ export class DileManyRelation extends DileI18nMixin(LitElement) {
|
|
|
299
299
|
this._dispatch('many-relation-remove-error');
|
|
300
300
|
}
|
|
301
301
|
|
|
302
|
-
|
|
302
|
+
refreshList() {
|
|
303
303
|
this._loadingList = true;
|
|
304
304
|
this.updateComplete.then(() => this._ajaxlist.generateRequest());
|
|
305
305
|
}
|
|
@@ -314,7 +314,7 @@ export class DileManyRelation extends DileI18nMixin(LitElement) {
|
|
|
314
314
|
this._dispatch('many-relation-list-error');
|
|
315
315
|
}
|
|
316
316
|
|
|
317
|
-
|
|
317
|
+
clearSelect() {
|
|
318
318
|
const select = this.shadowRoot.getElementById('theselect');
|
|
319
319
|
if (select) {
|
|
320
320
|
select.clear();
|
|
Binary file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { DileSelectAjaxSimple } from './src/DileSelectAjaxSimple.js';
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
2
|
+
import './select-ajax-simple.js';
|
|
3
|
+
|
|
4
|
+
const MOCK_DATA = [
|
|
5
|
+
{ id: 1, name: 'Fantasy' },
|
|
6
|
+
{ id: 2, name: 'Strategy' },
|
|
7
|
+
];
|
|
8
|
+
|
|
9
|
+
function mockAxiosSuccess(data = MOCK_DATA) {
|
|
10
|
+
window.axiosInstance = {
|
|
11
|
+
get: vi.fn().mockResolvedValue({ status: 200, data: { data } }),
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function mockAxiosError() {
|
|
16
|
+
window.axiosInstance = {
|
|
17
|
+
get: vi.fn().mockRejectedValue(new Error('Network error')),
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async function renderEl(html) {
|
|
22
|
+
document.body.innerHTML = html;
|
|
23
|
+
const el = document.body.querySelector('dile-select-ajax-simple');
|
|
24
|
+
await el.updateComplete;
|
|
25
|
+
// Wait for async loadData to complete and re-render
|
|
26
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
27
|
+
await el.updateComplete;
|
|
28
|
+
return el;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
describe('dile-select-ajax-simple', () => {
|
|
32
|
+
beforeEach(() => {
|
|
33
|
+
mockAxiosSuccess();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
afterEach(() => {
|
|
37
|
+
document.body.innerHTML = '';
|
|
38
|
+
delete window.axiosInstance;
|
|
39
|
+
vi.restoreAllMocks();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('shows a spinner while loading', async () => {
|
|
43
|
+
document.body.innerHTML = '<dile-select-ajax-simple endpoint="/api/tags" name="tag"></dile-select-ajax-simple>';
|
|
44
|
+
const el = document.body.querySelector('dile-select-ajax-simple');
|
|
45
|
+
await el.updateComplete;
|
|
46
|
+
|
|
47
|
+
expect(el.shadowRoot.querySelector('dile-spinner-horizontal')).toBeTruthy();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('renders options after successful data load', async () => {
|
|
51
|
+
const el = await renderEl('<dile-select-ajax-simple endpoint="/api/tags" name="tag"></dile-select-ajax-simple>');
|
|
52
|
+
|
|
53
|
+
const options = el.shadowRoot.querySelectorAll('option');
|
|
54
|
+
// placeholder + 2 data options
|
|
55
|
+
expect(options.length).toBe(3);
|
|
56
|
+
expect(options[1].textContent).toBe('Fantasy');
|
|
57
|
+
expect(options[2].textContent).toBe('Strategy');
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('uses displayProperty and idProperty for option rendering', async () => {
|
|
61
|
+
window.axiosInstance = {
|
|
62
|
+
get: vi.fn().mockResolvedValue({
|
|
63
|
+
status: 200,
|
|
64
|
+
data: { data: [{ code: 'FR', title: 'France' }] }
|
|
65
|
+
}),
|
|
66
|
+
};
|
|
67
|
+
const el = await renderEl(
|
|
68
|
+
'<dile-select-ajax-simple endpoint="/api/countries" name="country" displayProperty="title" idProperty="code"></dile-select-ajax-simple>'
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
const options = el.shadowRoot.querySelectorAll('option');
|
|
72
|
+
expect(options[1].value).toBe('FR');
|
|
73
|
+
expect(options[1].textContent).toBe('France');
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('shows error message when request fails', async () => {
|
|
77
|
+
mockAxiosError();
|
|
78
|
+
const el = await renderEl('<dile-select-ajax-simple endpoint="/api/tags" name="tag"></dile-select-ajax-simple>');
|
|
79
|
+
|
|
80
|
+
expect(el.shadowRoot.querySelector('p.error')).toBeTruthy();
|
|
81
|
+
expect(el.shadowRoot.querySelector('dile-select')).toBeNull();
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('shows empty message when response has no items', async () => {
|
|
85
|
+
window.axiosInstance = {
|
|
86
|
+
get: vi.fn().mockResolvedValue({ status: 200, data: { data: [] } }),
|
|
87
|
+
};
|
|
88
|
+
const el = await renderEl('<dile-select-ajax-simple endpoint="/api/tags" name="tag"></dile-select-ajax-simple>');
|
|
89
|
+
|
|
90
|
+
expect(el.shadowRoot.querySelector('p.empty')).toBeTruthy();
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('dispatches element-changed when an option is selected', async () => {
|
|
94
|
+
const el = await renderEl('<dile-select-ajax-simple endpoint="/api/tags" name="tag"></dile-select-ajax-simple>');
|
|
95
|
+
|
|
96
|
+
let detail = null;
|
|
97
|
+
el.addEventListener('element-changed', (e) => { detail = e.detail; });
|
|
98
|
+
|
|
99
|
+
el.value = '1';
|
|
100
|
+
await el.updateComplete;
|
|
101
|
+
|
|
102
|
+
expect(detail).not.toBeNull();
|
|
103
|
+
expect(detail.name).toBe('tag');
|
|
104
|
+
expect(detail.value).toBe('1');
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('supports getSelectResultList as custom extractor', async () => {
|
|
108
|
+
window.axiosInstance = {
|
|
109
|
+
get: vi.fn().mockResolvedValue({ status: 200, data: { result: { items: MOCK_DATA } } }),
|
|
110
|
+
};
|
|
111
|
+
// Set getSelectResultList before connectedCallback fires
|
|
112
|
+
const el = document.createElement('dile-select-ajax-simple');
|
|
113
|
+
el.getSelectResultList = (json) => json.result.items;
|
|
114
|
+
el.endpoint = '/api/tags';
|
|
115
|
+
el.name = 'tag';
|
|
116
|
+
document.body.appendChild(el);
|
|
117
|
+
await el.updateComplete;
|
|
118
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
119
|
+
await el.updateComplete;
|
|
120
|
+
|
|
121
|
+
const options = el.shadowRoot.querySelectorAll('option');
|
|
122
|
+
expect(options.length).toBe(3);
|
|
123
|
+
expect(options[1].textContent).toBe('Fantasy');
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('renders label when provided', async () => {
|
|
127
|
+
const el = await renderEl('<dile-select-ajax-simple endpoint="/api/tags" name="tag" label="Category"></dile-select-ajax-simple>');
|
|
128
|
+
|
|
129
|
+
const label = el.shadowRoot.querySelector('label');
|
|
130
|
+
expect(label).toBeTruthy();
|
|
131
|
+
expect(label.textContent).toBe('Category');
|
|
132
|
+
});
|
|
133
|
+
});
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { LitElement, html, css } from 'lit';
|
|
2
|
+
import { DileEmmitChange } from '@dile/ui/mixins/form/index.js';
|
|
3
|
+
import { DileAxios } from '../../../lib/DileAxios.js';
|
|
4
|
+
import '@dile/ui/components/select/select.js';
|
|
5
|
+
import '@dile/ui/components/spinner/spinner-horizontal.js';
|
|
6
|
+
import { labelStyles } from '@dile/ui/components/input/src/label-styles.js';
|
|
7
|
+
import { messageStyles } from '@dile/ui/components/input/src/message-styles.js';
|
|
8
|
+
|
|
9
|
+
export class DileSelectAjaxSimple extends DileAxios(DileEmmitChange(LitElement)) {
|
|
10
|
+
|
|
11
|
+
static get styles() {
|
|
12
|
+
return [
|
|
13
|
+
labelStyles,
|
|
14
|
+
messageStyles,
|
|
15
|
+
css`
|
|
16
|
+
:host {
|
|
17
|
+
display: block;
|
|
18
|
+
margin-bottom: 10px;
|
|
19
|
+
}
|
|
20
|
+
dile-select {
|
|
21
|
+
--dile-input-width: 100%;
|
|
22
|
+
margin-bottom: 0;
|
|
23
|
+
}
|
|
24
|
+
.spinner-container {
|
|
25
|
+
min-height: 2.5rem;
|
|
26
|
+
display: flex;
|
|
27
|
+
align-items: center;
|
|
28
|
+
}
|
|
29
|
+
p.empty {
|
|
30
|
+
margin: 0;
|
|
31
|
+
font-size: var(--dile-input-label-font-size, 1em);
|
|
32
|
+
color: var(--dile-on-background-color, #303030);
|
|
33
|
+
}
|
|
34
|
+
p.error {
|
|
35
|
+
margin: 0;
|
|
36
|
+
font-size: var(--dile-input-label-font-size, 1em);
|
|
37
|
+
color: var(--dile-input-message-error-color, #c00);
|
|
38
|
+
}
|
|
39
|
+
`
|
|
40
|
+
];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
static get properties() {
|
|
44
|
+
return {
|
|
45
|
+
name: { type: String, reflect: true },
|
|
46
|
+
label: { type: String },
|
|
47
|
+
endpoint: { type: String },
|
|
48
|
+
value: { type: String },
|
|
49
|
+
disabled: { type: Boolean },
|
|
50
|
+
errored: { type: Boolean },
|
|
51
|
+
message: { type: String },
|
|
52
|
+
selectDefaultPlaceholder: { type: String },
|
|
53
|
+
emptyMessage: { type: String },
|
|
54
|
+
ajaxErrorMessage: { type: String },
|
|
55
|
+
resultDataProperty: { type: String },
|
|
56
|
+
getSelectResultList: { type: Object },
|
|
57
|
+
displayProperty: { type: String },
|
|
58
|
+
idProperty: { type: String },
|
|
59
|
+
maxResults: { type: Number },
|
|
60
|
+
pageParamName: { type: String },
|
|
61
|
+
additionalQueryString: { type: Object },
|
|
62
|
+
loading: { type: Boolean, state: true },
|
|
63
|
+
ajaxError: { type: Boolean, state: true },
|
|
64
|
+
data: { type: Array, state: true },
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
constructor() {
|
|
69
|
+
super();
|
|
70
|
+
this.selectDefaultPlaceholder = 'Select an option...';
|
|
71
|
+
this.emptyMessage = 'No data available';
|
|
72
|
+
this.ajaxErrorMessage = 'Error loading data';
|
|
73
|
+
this.resultDataProperty = 'data';
|
|
74
|
+
this.displayProperty = 'name';
|
|
75
|
+
this.idProperty = 'id';
|
|
76
|
+
this.loading = false;
|
|
77
|
+
this.ajaxError = false;
|
|
78
|
+
this.data = [];
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
connectedCallback() {
|
|
82
|
+
super.connectedCallback();
|
|
83
|
+
this.loadData();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
updated(changedProperties) {
|
|
87
|
+
if (changedProperties.has('value')) {
|
|
88
|
+
this.emmitChange();
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
loadData() {
|
|
93
|
+
this.loading = true;
|
|
94
|
+
this.ajaxError = false;
|
|
95
|
+
let params = { ...(this.additionalQueryString || {}) };
|
|
96
|
+
if (this.pageParamName && this.maxResults) {
|
|
97
|
+
params[this.pageParamName] = this.maxResults;
|
|
98
|
+
}
|
|
99
|
+
this.axiosInstance.get(this.endpoint, { params })
|
|
100
|
+
.then((response) => {
|
|
101
|
+
if (response.status === 200) {
|
|
102
|
+
this.data = this.getResultData(response.data);
|
|
103
|
+
this.updateComplete.then(() => {
|
|
104
|
+
this.loading = false;
|
|
105
|
+
if (this.value) {
|
|
106
|
+
this.selectCurrentValue();
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
} else {
|
|
110
|
+
this.registerError();
|
|
111
|
+
}
|
|
112
|
+
})
|
|
113
|
+
.catch(() => this.registerError());
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
getResultData(json) {
|
|
117
|
+
if (this.getSelectResultList) {
|
|
118
|
+
return this.getSelectResultList(json) ?? [];
|
|
119
|
+
}
|
|
120
|
+
if (this.resultDataProperty) {
|
|
121
|
+
return json[this.resultDataProperty] ?? [];
|
|
122
|
+
}
|
|
123
|
+
return Array.isArray(json) ? json : [];
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
registerError() {
|
|
127
|
+
this.ajaxError = true;
|
|
128
|
+
this.loading = false;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Sets the native select value after options are rendered
|
|
132
|
+
selectCurrentValue() {
|
|
133
|
+
const select = this.shadowRoot.querySelector('dile-select');
|
|
134
|
+
if (select) {
|
|
135
|
+
select.value = this.value;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
onSelected(e) {
|
|
140
|
+
this.value = e.detail.value;
|
|
141
|
+
e.stopPropagation();
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
render() {
|
|
145
|
+
return html`
|
|
146
|
+
${this.label ? html`<label>${this.label}</label>` : ''}
|
|
147
|
+
${this.loading
|
|
148
|
+
? html`<div class="spinner-container"><dile-spinner-horizontal active></dile-spinner-horizontal></div>`
|
|
149
|
+
: this.ajaxError
|
|
150
|
+
? html`<p class="error">${this.ajaxErrorMessage}</p>`
|
|
151
|
+
: this.data.length === 0
|
|
152
|
+
? html`<p class="empty">${this.emptyMessage}</p>`
|
|
153
|
+
: html`
|
|
154
|
+
<dile-select
|
|
155
|
+
name="${this.name}"
|
|
156
|
+
?disabled=${this.disabled}
|
|
157
|
+
?errored=${this.errored}
|
|
158
|
+
message="${this.message || ''}"
|
|
159
|
+
@element-changed=${this.onSelected}
|
|
160
|
+
>
|
|
161
|
+
<select slot="select">
|
|
162
|
+
<option value="">${this.selectDefaultPlaceholder}</option>
|
|
163
|
+
${this.data.map(item => html`
|
|
164
|
+
<option value="${item[this.idProperty]}">${item[this.displayProperty]}</option>
|
|
165
|
+
`)}
|
|
166
|
+
</select>
|
|
167
|
+
</dile-select>
|
|
168
|
+
`
|
|
169
|
+
}
|
|
170
|
+
`;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
@@ -2,6 +2,7 @@ import { LitElement, html, css } from 'lit';
|
|
|
2
2
|
import { DileFormChangeDetect, DileForm } from '@dile/ui/mixins/form/index.js';
|
|
3
3
|
import '@dile/ui/components/checkbox/checkbox.js';
|
|
4
4
|
import '@dile/ui/components/select/select.js';
|
|
5
|
+
import '@dile/crud/components/select-ajax-simple/select-ajax-simple.js';
|
|
5
6
|
|
|
6
7
|
export class DileCrudFiltersForm extends DileFormChangeDetect(DileForm(LitElement)) {
|
|
7
8
|
static styles = css`
|
|
@@ -47,6 +48,27 @@ export class DileCrudFiltersForm extends DileFormChangeDetect(DileForm(LitElemen
|
|
|
47
48
|
</select>
|
|
48
49
|
</dile-select>
|
|
49
50
|
`;
|
|
51
|
+
case 'select_ajax':
|
|
52
|
+
return html`
|
|
53
|
+
<dile-select-ajax-simple
|
|
54
|
+
label="${filter.label}"
|
|
55
|
+
name="${filter.name}"
|
|
56
|
+
endpoint="${filter.endpoint}"
|
|
57
|
+
displayProperty="${filter.optionLabelField || 'name'}"
|
|
58
|
+
idProperty="${filter.optionValueField || 'id'}"
|
|
59
|
+
resultDataProperty="${filter.resultDataProperty || 'data'}"
|
|
60
|
+
selectDefaultPlaceholder="${filter.selectDefaultPlaceholder || ''}"
|
|
61
|
+
emptyMessage="${filter.emptyMessage || ''}"
|
|
62
|
+
ajaxErrorMessage="${filter.ajaxErrorMessage || ''}"
|
|
63
|
+
message="${filter.message || ''}"
|
|
64
|
+
pageParamName="${filter.pageParamName || ''}"
|
|
65
|
+
.maxResults=${filter.maxResults}
|
|
66
|
+
.getSelectResultList=${filter.getSelectResultList}
|
|
67
|
+
.additionalQueryString=${filter.additionalQueryString}
|
|
68
|
+
?disabled=${filter.disabled || false}
|
|
69
|
+
?errored=${filter.errored || false}
|
|
70
|
+
></dile-select-ajax-simple>
|
|
71
|
+
`;
|
|
50
72
|
default:
|
|
51
73
|
return html`
|
|
52
74
|
<p>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dile/crud",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.3.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",
|
|
@@ -32,5 +32,5 @@
|
|
|
32
32
|
"publishConfig": {
|
|
33
33
|
"access": "public"
|
|
34
34
|
},
|
|
35
|
-
"gitHead": "
|
|
35
|
+
"gitHead": "288663c7de6829d806f1fe2e360c587a13c97972"
|
|
36
36
|
}
|