@dile/crud 2.2.8 → 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/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;
|
|
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
|
}
|