@dile/crud 2.1.12 → 2.2.1

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.
Files changed (18) hide show
  1. package/components/ajax-select-crud/__screenshots__/ajax-select-crud-overlay.spec.js/dile-ajax-select-crud-overlay-Selecting-a-result--no-extra-click-required--opens-the-popup-as-soon-as-results-load-and-lets-the-user-pick-without-a-second-click-1.png +0 -0
  2. package/components/ajax-select-crud/__screenshots__/ajax-select-crud-overlay.spec.js/dile-ajax-select-crud-overlay-Uses-axios-instead-of-fetch-fetches-the-initial-value-display-text-through-axios-1.png +0 -0
  3. package/components/ajax-select-crud/__screenshots__/ajax-select-crud-overlay.spec.js/dile-ajax-select-crud-overlay-Uses-axios-instead-of-fetch-requests-through-window-axiosInstance-get--not-the-native-fetch-API-1.png +0 -0
  4. package/components/ajax-select-crud/__screenshots__/zzdebug.spec.js/debug-debug-opened-timing-1.png +0 -0
  5. package/components/ajax-select-crud/ajax-select-crud-overlay.js +5 -0
  6. package/components/ajax-select-crud/ajax-select-crud-overlay.spec.js +166 -0
  7. package/components/ajax-select-crud/index.js +2 -1
  8. package/components/ajax-select-crud/src/DileAjaxSelectCrudOverlay.js +80 -0
  9. package/components/many-relation/__screenshots__/many-relation.spec.js/-dile-many-relation---select---dile-ajax-select-crud---loads-the-related-items-list-from-endpointList-on-connect-1.png +0 -0
  10. package/components/many-relation/__screenshots__/many-relation.spec.js/-dile-many-relation---select---dile-ajax-select-crud---removes-an-item-and-dispatches-many-relation-remove-success-1.png +0 -0
  11. package/components/many-relation/__screenshots__/many-relation.spec.js/-dile-many-relation-overlay---select---dile-ajax-select-crud-overlay---loads-the-related-items-list-from-endpointList-on-connect-1.png +0 -0
  12. package/components/many-relation/__screenshots__/many-relation.spec.js/-dile-many-relation-overlay---select---dile-ajax-select-crud-overlay---removes-an-item-and-dispatches-many-relation-remove-success-1.png +0 -0
  13. package/components/many-relation/index.js +1 -0
  14. package/components/many-relation/many-relation-overlay.js +5 -0
  15. package/components/many-relation/many-relation.spec.js +101 -0
  16. package/components/many-relation/src/DileManyRelation.js +9 -5
  17. package/components/many-relation/src/DileManyRelationOverlay.js +6 -0
  18. package/package.json +3 -3
@@ -0,0 +1,5 @@
1
+ import { DileAjaxSelectCrudOverlay } from './src/DileAjaxSelectCrudOverlay.js';
2
+
3
+ if (!customElements.get('dile-ajax-select-crud-overlay')) {
4
+ customElements.define('dile-ajax-select-crud-overlay', DileAjaxSelectCrudOverlay);
5
+ }
@@ -0,0 +1,166 @@
1
+ import { describe, it, expect, afterEach, vi } from 'vitest';
2
+ import './ajax-select-crud-overlay.js';
3
+
4
+ describe('dile-ajax-select-crud-overlay', () => {
5
+ afterEach(() => {
6
+ document.body.innerHTML = '';
7
+ delete window.axiosInstance;
8
+ });
9
+
10
+ function wait(ms) {
11
+ return new Promise((resolve) => setTimeout(resolve, ms));
12
+ }
13
+
14
+ function mockAxios(responseData) {
15
+ const get = vi.fn(() => Promise.resolve({ status: 200, data: responseData }));
16
+ window.axiosInstance = { get };
17
+ return get;
18
+ }
19
+
20
+ async function renderCrudSelect(html) {
21
+ document.body.innerHTML = html;
22
+ const el = document.body.querySelector('dile-ajax-select-crud-overlay');
23
+ await el.updateComplete;
24
+ return el;
25
+ }
26
+
27
+ async function typeKeyword(el, keyword) {
28
+ const input = el.shadowRoot.getElementById('search');
29
+ input.value = keyword;
30
+ input.dispatchEvent(new Event('input', { bubbles: true }));
31
+ await wait(el.delay + 50);
32
+ await el.updateComplete;
33
+ }
34
+
35
+ describe('Rendering', () => {
36
+ it('renders the search input, same as dile-select-ajax-overlay', async () => {
37
+ mockAxios([]);
38
+ const el = await renderCrudSelect(`
39
+ <dile-ajax-select-crud-overlay endpoint="/api/countries" idProperty="id" displayProperty="name"></dile-ajax-select-crud-overlay>
40
+ `);
41
+
42
+ const input = el.shadowRoot.getElementById('search');
43
+ expect(input).toBeTruthy();
44
+ expect(input.getAttribute('role')).toBe('combobox');
45
+ });
46
+ });
47
+
48
+ describe('Uses axios instead of fetch', () => {
49
+ it('requests through window.axiosInstance.get, not the native fetch API', async () => {
50
+ const get = mockAxios([{ id: 1, name: 'Spain' }]);
51
+ const fetchSpy = vi.spyOn(window, 'fetch');
52
+
53
+ const el = await renderCrudSelect(`
54
+ <dile-ajax-select-crud-overlay endpoint="/api/countries" idProperty="id" displayProperty="name" queryStringVariable="keyword" delay="10"></dile-ajax-select-crud-overlay>
55
+ `);
56
+
57
+ await typeKeyword(el, 'sp');
58
+
59
+ expect(get).toHaveBeenCalledWith('/api/countries', { params: { keyword: 'sp' } });
60
+ expect(fetchSpy).not.toHaveBeenCalled();
61
+ expect(el.data.length).toBe(1);
62
+ expect(el.select.opened).toBe(true);
63
+
64
+ fetchSpy.mockRestore();
65
+ });
66
+
67
+ it('fetches the initial value display text through axios', async () => {
68
+ // searchValueInitial() passes response.data (i.e. the "data" envelope, not the raw
69
+ // response) down to registerText(), matching dile-ajax-select-crud's own convention.
70
+ const get = vi.fn(() => Promise.resolve({ status: 200, data: { data: { name: 'Spain' } } }));
71
+ window.axiosInstance = { get };
72
+
73
+ const el = await renderCrudSelect(`
74
+ <dile-ajax-select-crud-overlay endpoint="/api/countries" idProperty="id" displayProperty="name" value="22"></dile-ajax-select-crud-overlay>
75
+ `);
76
+ await wait(50);
77
+ await el.updateComplete;
78
+
79
+ expect(get).toHaveBeenCalledWith('/api/countries/22');
80
+ expect(el.selectedText).toBe('Spain');
81
+ expect(el.isSelected).toBe(true);
82
+ });
83
+ });
84
+
85
+ describe('Selecting a result (no extra click required)', () => {
86
+ it('opens the popup as soon as results load and lets the user pick without a second click', async () => {
87
+ mockAxios([{ id: 1, name: 'Spain' }, { id: 2, name: 'France' }]);
88
+ const el = await renderCrudSelect(`
89
+ <dile-ajax-select-crud-overlay endpoint="/api/countries" idProperty="id" displayProperty="name" delay="10"></dile-ajax-select-crud-overlay>
90
+ `);
91
+
92
+ await typeKeyword(el, 'sp');
93
+ expect(el.select.opened).toBe(true);
94
+
95
+ const options = el.select.shadowRoot.querySelectorAll('[role="option"]');
96
+ options[0].click();
97
+ await el.select.updateComplete;
98
+ await el.updateComplete;
99
+
100
+ expect(el.value).toBe('1');
101
+ expect(el.selectedText).toBe('Spain');
102
+ expect(el.isSelected).toBe(true);
103
+ });
104
+ });
105
+
106
+ describe('maxResults / pageParamName', () => {
107
+ it('adds the page-size param to the request when both are set', async () => {
108
+ const get = mockAxios([]);
109
+ const el = await renderCrudSelect(`
110
+ <dile-ajax-select-crud-overlay endpoint="/api/countries" idProperty="id" displayProperty="name" queryStringVariable="keyword" maxResults="5" pageParamName="limit" delay="10"></dile-ajax-select-crud-overlay>
111
+ `);
112
+
113
+ await typeKeyword(el, 'sp');
114
+
115
+ expect(get).toHaveBeenCalledWith('/api/countries', { params: { keyword: 'sp', limit: 5 } });
116
+ });
117
+
118
+ it('omits the page-size param when pageParamName is not set', async () => {
119
+ const get = mockAxios([]);
120
+ const el = await renderCrudSelect(`
121
+ <dile-ajax-select-crud-overlay endpoint="/api/countries" idProperty="id" displayProperty="name" queryStringVariable="keyword" maxResults="5" delay="10"></dile-ajax-select-crud-overlay>
122
+ `);
123
+
124
+ await typeKeyword(el, 'sp');
125
+
126
+ expect(get).toHaveBeenCalledWith('/api/countries', { params: { keyword: 'sp' } });
127
+ });
128
+ });
129
+
130
+ describe('getSelectResultList', () => {
131
+ it('uses getSelectResultList to extract the list when the response shape needs adapting', async () => {
132
+ mockAxios({ data: { result: { data: [{ id: 1, name: 'Spain' }] } } });
133
+ const el = await renderCrudSelect(`
134
+ <dile-ajax-select-crud-overlay endpoint="/api/countries" idProperty="id" displayProperty="name" delay="10"></dile-ajax-select-crud-overlay>
135
+ `);
136
+ el.getSelectResultList = (response) => response.data.result.data;
137
+
138
+ await typeKeyword(el, 'sp');
139
+
140
+ expect(el.data).toEqual([{ id: 1, name: 'Spain' }]);
141
+ });
142
+
143
+ it('takes priority over resultDataProperty when both are set', async () => {
144
+ mockAxios({ items: [{ id: 1, name: 'Spain' }], data: [] });
145
+ const el = await renderCrudSelect(`
146
+ <dile-ajax-select-crud-overlay endpoint="/api/countries" idProperty="id" displayProperty="name" resultDataProperty="data" delay="10"></dile-ajax-select-crud-overlay>
147
+ `);
148
+ el.getSelectResultList = (response) => response.items;
149
+
150
+ await typeKeyword(el, 'sp');
151
+
152
+ expect(el.data).toEqual([{ id: 1, name: 'Spain' }]);
153
+ });
154
+ });
155
+
156
+ describe('Form Association', () => {
157
+ it('is form-associated, same as dile-select-ajax-overlay', async () => {
158
+ mockAxios([]);
159
+ const el = await renderCrudSelect(`
160
+ <dile-ajax-select-crud-overlay endpoint="/api/countries" idProperty="id" displayProperty="name"></dile-ajax-select-crud-overlay>
161
+ `);
162
+
163
+ expect(el.internals).toBeTruthy();
164
+ });
165
+ });
166
+ });
@@ -1 +1,2 @@
1
- export { DileAjaxSelectCrud } from './src/DileAjaxSelectCrud.js';
1
+ export { DileAjaxSelectCrud } from './src/DileAjaxSelectCrud.js';
2
+ export { DileAjaxSelectCrudOverlay } from './src/DileAjaxSelectCrudOverlay.js';
@@ -0,0 +1,80 @@
1
+ import { DileAxios } from '../../../lib/DileAxios.js';
2
+ import { DileSelectAjaxOverlay } from '@dile/ui/components/select/index.js';
3
+
4
+ export class DileAjaxSelectCrudOverlay extends DileAxios(DileSelectAjaxOverlay) {
5
+
6
+ static get properties() {
7
+ return {
8
+ maxResults: { type: Number },
9
+ pageParamName: { type: String },
10
+ getSelectResultList: { type: Object },
11
+ };
12
+ }
13
+
14
+ constructor() {
15
+ super();
16
+ }
17
+
18
+ searchValueInitial() {
19
+ let request = this.axiosInstance.get(`${this.endpoint}/${this.value}`);
20
+ request.then((response) => {
21
+ if (response.status == 200) {
22
+ let res = response.data;
23
+ if (res.error) {
24
+ this.registerError(res.data);
25
+ } else {
26
+ this.registerText(res.data);
27
+ }
28
+ } else {
29
+ this.registerError('Bad response code');
30
+ }
31
+ })
32
+ .catch(err => {
33
+ this.registerError(err);
34
+ });
35
+ }
36
+
37
+ loadData() {
38
+ this.loading = true;
39
+ let params = this.additionalQueryString || {};
40
+ if (this.pageParamName && this.maxResults) {
41
+ params[this.pageParamName] = this.maxResults;
42
+ }
43
+ params[this.queryStringVariable] = this.keyword;
44
+ let request = this.axiosInstance.get(this.endpoint, { params });
45
+ request.then((response) => {
46
+ if (response.status == 200) {
47
+ let res = response.data;
48
+ if (res.error) {
49
+ this.registerError(res.data);
50
+ } else {
51
+ this.registerData(res);
52
+ }
53
+ } else {
54
+ this.registerError('Bad response code');
55
+ }
56
+ })
57
+ .catch(err => {
58
+ this.registerError(err);
59
+ });
60
+ }
61
+
62
+ registerText(json) {
63
+ this.selectedText = json[this.displayProperty];
64
+ this.loading = false;
65
+ }
66
+
67
+ registerData(json) {
68
+ if(this.getSelectResultList) {
69
+ this.data = this.getSelectResultList(json);
70
+ } else {
71
+ this.data = this.getResultData(json);
72
+ }
73
+ this.updateComplete.then(() => {
74
+ this.loading = false;
75
+ if (this.keyword.length > 0 && this.data.length > 0) {
76
+ this.select?.open();
77
+ }
78
+ });
79
+ }
80
+ }
@@ -1 +1,2 @@
1
1
  export { DileManyRelation } from './src/DileManyRelation.js';
2
+ export { DileManyRelationOverlay } from './src/DileManyRelationOverlay.js';
@@ -0,0 +1,5 @@
1
+ import { DileManyRelationOverlay } from './src/DileManyRelationOverlay.js';
2
+
3
+ if (!customElements.get('dile-many-relation-overlay')) {
4
+ customElements.define('dile-many-relation-overlay', DileManyRelationOverlay);
5
+ }
@@ -0,0 +1,101 @@
1
+ import { describe, it, expect, afterEach, vi } from 'vitest';
2
+ import './many-relation.js';
3
+ import './many-relation-overlay.js';
4
+
5
+ const VARIANTS = [
6
+ { tag: 'dile-many-relation', selectTag: 'dile-ajax-select-crud' },
7
+ { tag: 'dile-many-relation-overlay', selectTag: 'dile-ajax-select-crud-overlay' },
8
+ ];
9
+
10
+ describe.each(VARIANTS)('$tag (select: $selectTag)', ({ tag, selectTag }) => {
11
+ afterEach(() => {
12
+ document.body.innerHTML = '';
13
+ delete window.axiosInstance;
14
+ });
15
+
16
+ function mockAxios({ get = vi.fn(), post = vi.fn(), delete: del = vi.fn() } = {}) {
17
+ window.axiosInstance = { get, post, delete: del };
18
+ return window.axiosInstance;
19
+ }
20
+
21
+ async function renderManyRelation(attrs = '') {
22
+ document.body.innerHTML = `
23
+ <${tag}
24
+ endpointGet="/api/tags"
25
+ endpointList="/api/board-games/1/tags"
26
+ endpointAdd="/api/board-games/1/tags"
27
+ endpointRemove="/api/board-games/1/tags"
28
+ idProperty="id"
29
+ displayProperty="name"
30
+ ${attrs}
31
+ ></${tag}>
32
+ `;
33
+ const el = document.body.querySelector(tag);
34
+ await el.updateComplete;
35
+ return el;
36
+ }
37
+
38
+ function select(el) {
39
+ return el.shadowRoot.getElementById('theselect');
40
+ }
41
+
42
+ it(`renders a ${selectTag} as the select control`, async () => {
43
+ mockAxios({ get: vi.fn(() => Promise.resolve({ status: 200, data: [] })) });
44
+ const el = await renderManyRelation();
45
+
46
+ const theselect = select(el);
47
+ expect(theselect).toBeTruthy();
48
+ expect(theselect.tagName.toLowerCase()).toBe(selectTag);
49
+ });
50
+
51
+ it('loads the related items list from endpointList on connect', async () => {
52
+ const get = vi.fn(() =>
53
+ Promise.resolve({ status: 200, data: { data: [{ id: 1, name: 'Strategy' }] } })
54
+ );
55
+ mockAxios({ get });
56
+ const el = await renderManyRelation('loadFromEndpoint');
57
+ await vi.waitFor(() => expect(el._items).toEqual([{ id: 1, name: 'Strategy' }]));
58
+
59
+ expect(get).toHaveBeenCalledWith('/api/board-games/1/tags', { params: undefined });
60
+ });
61
+
62
+ it('adds the item selected through the select control', async () => {
63
+ const post = vi.fn(() => Promise.resolve({ status: 201, data: {} }));
64
+ const get = vi.fn(() => Promise.resolve({ status: 200, data: { data: [] } }));
65
+ mockAxios({ post, get });
66
+ const el = await renderManyRelation();
67
+
68
+ select(el).dispatchEvent(
69
+ new CustomEvent('element-changed', { detail: { value: '5' } })
70
+ );
71
+ await el.updateComplete;
72
+ expect(el._selectedId).toBe('5');
73
+
74
+ el.shadowRoot.querySelector('button.add-btn').click();
75
+ // adding successfully triggers a list auto-refresh; wait it out before mocks are torn down
76
+ await vi.waitFor(() => expect(get).toHaveBeenCalled());
77
+
78
+ expect(post).toHaveBeenCalledWith('/api/board-games/1/tags', { id: '5' }, undefined);
79
+ });
80
+
81
+ it('removes an item and dispatches many-relation-remove-success', async () => {
82
+ const del = vi.fn(() => Promise.resolve({ status: 200, data: {} }));
83
+ const get = vi.fn(() => Promise.resolve({ status: 200, data: { data: [] } }));
84
+ mockAxios({ get, delete: del });
85
+ const el = await renderManyRelation();
86
+ el.relatedItems = [{ id: 9, name: 'Party' }];
87
+
88
+ const removeListener = vi.fn();
89
+ el.addEventListener('many-relation-remove-success', removeListener);
90
+
91
+ await vi.waitFor(() =>
92
+ expect(el.shadowRoot.querySelector('button.remove-btn')).toBeTruthy()
93
+ );
94
+ el.shadowRoot.querySelector('button.remove-btn').click();
95
+ await vi.waitFor(() => expect(removeListener).toHaveBeenCalled());
96
+ // wait out the list auto-refresh triggered by the removal before mocks are torn down
97
+ await vi.waitFor(() => expect(get).toHaveBeenCalled());
98
+
99
+ expect(del).toHaveBeenCalledWith('/api/board-games/1/tags/9', {}, undefined);
100
+ });
101
+ });
@@ -1,4 +1,5 @@
1
- import { LitElement, html, css } from 'lit';
1
+ import { LitElement, css } from 'lit';
2
+ import { html, unsafeStatic } from 'lit/static-html.js';
2
3
  import { DileI18nMixin } from '../../../lib/DileI18nMixin.js';
3
4
  import { addIcon, clearIcon } from '@dile/icons';
4
5
  import '../../ajax/ajax.js';
@@ -9,13 +10,15 @@ import '@dile/ui/components/input/input-message.js';
9
10
  import { labelStyles } from '@dile/ui/components/input/src/label-styles.js';
10
11
 
11
12
  export class DileManyRelation extends DileI18nMixin(LitElement) {
13
+ static selectTag = 'dile-ajax-select-crud';
14
+
12
15
  static styles = [
13
16
  labelStyles,
14
17
  css`
15
18
  :host {
16
19
  display: block;
17
20
  }
18
- dile-ajax-select-crud{
21
+ #theselect {
19
22
  margin-bottom: 0;
20
23
  }
21
24
  .relation-item {
@@ -61,7 +64,7 @@ export class DileManyRelation extends DileI18nMixin(LitElement) {
61
64
  gap: 0.5rem;
62
65
  margin-bottom: 0.5rem;
63
66
  }
64
- .add-row dile-ajax-select-crud {
67
+ .add-row #theselect {
65
68
  flex: 1;
66
69
  }
67
70
  button.add-btn dile-icon {
@@ -159,6 +162,7 @@ export class DileManyRelation extends DileI18nMixin(LitElement) {
159
162
  }
160
163
 
161
164
  render() {
165
+ const selectTag = unsafeStatic(this.constructor.selectTag);
162
166
  return html`
163
167
  <dile-ajax
164
168
  id="ajaxlist"
@@ -186,7 +190,7 @@ export class DileManyRelation extends DileI18nMixin(LitElement) {
186
190
  : ""
187
191
  }
188
192
  <div class="add-row">
189
- <dile-ajax-select-crud
193
+ <${selectTag}
190
194
  id="theselect"
191
195
  endpoint="${this.endpointGet}"
192
196
  displayProperty="${this.displayProperty}"
@@ -200,7 +204,7 @@ export class DileManyRelation extends DileI18nMixin(LitElement) {
200
204
  .getSelectResultList="${this.getSelectResultList}"
201
205
  language="${this.language}"
202
206
  @element-changed="${this._onSelectChanged}"
203
- ></dile-ajax-select-crud>
207
+ ></${selectTag}>
204
208
  ${!this.addOnSelect ? html`
205
209
  <button
206
210
  class="add-btn"
@@ -0,0 +1,6 @@
1
+ import { DileManyRelation } from './DileManyRelation.js';
2
+ import '../../ajax-select-crud/ajax-select-crud-overlay.js';
3
+
4
+ export class DileManyRelationOverlay extends DileManyRelation {
5
+ static selectTag = 'dile-ajax-select-crud-overlay';
6
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dile/crud",
3
- "version": "2.1.12",
3
+ "version": "2.2.1",
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.4.6",
28
+ "@dile/ui": "^3.5.1",
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": "57a54e50de1a24d64dcf478fa6d39624e1f775eb"
35
+ "gitHead": "7bcf203fb23acc24867e9f3df4f3ca2367eabd97"
36
36
  }