@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.
@@ -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: this.filters,
81
+ filters: normalizedFilters,
77
82
  }
78
83
  if (this.sort && this.sort.sortField) {
79
84
  data.sortField = this.sort.sortField;
@@ -0,0 +1 @@
1
+ export { DileSelectAjaxSimple } from './src/DileSelectAjaxSimple.js';
@@ -0,0 +1,5 @@
1
+ import { DileSelectAjaxSimple } from './src/DileSelectAjaxSimple.js';
2
+
3
+ if (!customElements.get('dile-select-ajax-simple')) {
4
+ customElements.define('dile-select-ajax-simple', DileSelectAjaxSimple);
5
+ }
@@ -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
+ }
@@ -38,6 +38,7 @@ export class DileCrudFilters extends DileI18nMixin(LitElement) {
38
38
  if (filter.name in data) {
39
39
  switch(filter.type) {
40
40
  case 'select':
41
+ case 'select_ajax':
41
42
  if(data[filter.name] === '') {
42
43
  filter.active = false;
43
44
  } else {
@@ -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.2.8",
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": "1255147fa9d7247f77899a2da1af6f00bf4a31cd"
35
+ "gitHead": "288663c7de6829d806f1fe2e360c587a13c97972"
36
36
  }