@dile/crud 2.6.3 → 2.7.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.
@@ -0,0 +1,191 @@
1
+ import { LitElement, html, css, nothing } from 'lit';
2
+ import '@dile/ui/components/button/button.js';
3
+ import '@dile/iconlib/material-icons/edit.js';
4
+ import '@dile/iconlib/material-icons/delete.js';
5
+ import '@dile/iconlib/material-icons/restore-from-trash.js';
6
+
7
+ export class DileCrudItemActions extends LitElement {
8
+ static styles = [
9
+ css`
10
+ :host {
11
+ display: inline-flex;
12
+ align-items: center;
13
+ gap: var(--dile-crud-item-actions-gap, 0.25rem);
14
+ }
15
+ dile-button.action-button {
16
+ --dile-button-background-color: var(--dile-crud-list-item-action-button-background-color, transparent);
17
+ --dile-button-border-color: var(--dile-crud-list-item-action-button-border-color, transparent);
18
+ --dile-button-hover-background-color: var(--dile-crud-list-item-action-button-hover-background-color, var(--dile-neutral-color, #f1f5f9));
19
+ --dile-button-hover-border-color: var(--dile-crud-list-item-action-button-hover-border-color, transparent);
20
+ --dile-button-padding-y: var(--dile-crud-list-item-action-button-padding-y, 0.25rem);
21
+ --dile-button-padding-x: var(--dile-crud-list-item-action-button-padding-x, 0.25rem);
22
+ --dile-button-icon-size: var(--dile-crud-list-item-action-button-icon-size, 24px);
23
+ --dile-button-border-radius: var(--dile-crud-list-item-action-button-border-radius, 2rem);
24
+ }
25
+ dile-button.action-button.edit {
26
+ --dile-button-icon-color: var(--edit-icon-color, var(--dile-alert-neutral-color, #0c7595));
27
+ --dile-button-icon-hover-color: var(--dile-crud-list-item-edit-icon-hover-color, var(--edit-icon-color, var(--dile-alert-neutral-color, #2889a7)));
28
+ }
29
+ dile-button.action-button.delete {
30
+ --dile-button-icon-color: var(--delete-icon-color, var(--dile-danger-color, #e33));
31
+ --dile-button-icon-hover-color: var(--dile-crud-list-item-delete-icon-hover-color, var(--delete-icon-color, var(--dile-danger-color, #e33)));
32
+ }
33
+ dile-button.action-button.restore {
34
+ --dile-button-icon-color: var(--restore-icon-color, var(--dile-alert-success-color, #00900f));
35
+ --dile-button-icon-hover-color: var(--dile-crud-list-item-restore-icon-hover-color, var(--restore-icon-color, var(--dile-alert-success-color, #00900f)));
36
+ }
37
+ `
38
+ ];
39
+
40
+ static get properties() {
41
+ return {
42
+ item: { type: Object },
43
+ itemId: { type: String },
44
+ config: { type: Object },
45
+ disableEdit: { type: Boolean },
46
+ disableDelete: { type: Boolean },
47
+ disableRestore: { type: Boolean },
48
+ isDeleted: { type: Boolean },
49
+ };
50
+ }
51
+
52
+ constructor() {
53
+ super();
54
+ this.config = null;
55
+ this.disableEdit = false;
56
+ this.disableDelete = false;
57
+ this.disableRestore = false;
58
+ this.isDeleted = false;
59
+ }
60
+
61
+ static hasActions(config) {
62
+ if (!config) {
63
+ return true;
64
+ }
65
+ if (config.customization?.disableListActions) {
66
+ return false;
67
+ }
68
+ const editDisabled = Boolean(config.customization?.disableEdit);
69
+ const deleteDisabled = Boolean(config.customization?.disableDelete);
70
+ const restoreDisabled = Boolean(config.customization?.disableRestore);
71
+
72
+ return !(editDisabled && deleteDisabled && restoreDisabled);
73
+ }
74
+
75
+ get computedIsDeleted() {
76
+ if (this.isDeleted) {
77
+ return true;
78
+ }
79
+ return Boolean(this.item?.deleted_at);
80
+ }
81
+
82
+ get computedItemId() {
83
+ if (this.itemId !== undefined && this.itemId !== null && this.itemId !== '') {
84
+ return this.itemId;
85
+ }
86
+ if (this.config?.computeItemId && typeof this.config.computeItemId === 'function' && this.item) {
87
+ return this.config.computeItemId(this.item);
88
+ }
89
+ return this.item?.id;
90
+ }
91
+
92
+ get isEditable() {
93
+ if (this.disableEdit) {
94
+ return false;
95
+ }
96
+ if (this.config) {
97
+ if (this.config.customization?.disableListActions || this.config.customization?.disableEdit) {
98
+ return false;
99
+ }
100
+ if (typeof this.config.isItemEditable === 'function' && this.item) {
101
+ return this.config.isItemEditable(this.item);
102
+ }
103
+ }
104
+ return true;
105
+ }
106
+
107
+ get isDeletable() {
108
+ if (this.disableDelete) {
109
+ return false;
110
+ }
111
+ if (this.config) {
112
+ if (this.config.customization?.disableListActions || this.config.customization?.disableDelete) {
113
+ return false;
114
+ }
115
+ if (typeof this.config.isItemDeletable === 'function' && this.item) {
116
+ return this.config.isItemDeletable(this.item);
117
+ }
118
+ }
119
+ return true;
120
+ }
121
+
122
+ get isRestorable() {
123
+ if (this.disableRestore) {
124
+ return false;
125
+ }
126
+ if (this.config?.customization?.disableListActions || this.config?.customization?.disableRestore) {
127
+ return false;
128
+ }
129
+ return true;
130
+ }
131
+
132
+ render() {
133
+ return this.computedIsDeleted ? this.restoreActionsTemplate : this.regularActionsTemplate;
134
+ }
135
+
136
+ get regularActionsTemplate() {
137
+ const showEdit = this.isEditable;
138
+ const showDelete = this.isDeletable;
139
+
140
+ if (!showEdit && !showDelete) {
141
+ return nothing;
142
+ }
143
+
144
+ return html`
145
+ ${showEdit
146
+ ? html`<dile-button class="action-button edit" icon="material.edit" label="Edit" @click=${this.editClick}></dile-button>`
147
+ : nothing
148
+ }
149
+ ${showDelete
150
+ ? html`<dile-button class="action-button delete" icon="material.delete" label="Delete" @click=${this.deleteClick}></dile-button>`
151
+ : nothing
152
+ }
153
+ `;
154
+ }
155
+
156
+ get restoreActionsTemplate() {
157
+ if (!this.isRestorable) {
158
+ return nothing;
159
+ }
160
+
161
+ return html`
162
+ <dile-button class="action-button restore" icon="material.restore-from-trash" label="Restore" @click=${this.restoreClick}></dile-button>
163
+ `;
164
+ }
165
+
166
+ _dispatchItemEvent(eventName) {
167
+ this.dispatchEvent(new CustomEvent(eventName, {
168
+ bubbles: true,
169
+ composed: true,
170
+ detail: {
171
+ item: this.item,
172
+ itemId: this.computedItemId,
173
+ }
174
+ }));
175
+ }
176
+
177
+ editClick(e) {
178
+ e?.stopPropagation();
179
+ this._dispatchItemEvent('crud-item-edit');
180
+ }
181
+
182
+ deleteClick(e) {
183
+ e?.stopPropagation();
184
+ this._dispatchItemEvent('crud-item-delete');
185
+ }
186
+
187
+ restoreClick(e) {
188
+ e?.stopPropagation();
189
+ this._dispatchItemEvent('crud-item-restore');
190
+ }
191
+ }
@@ -7,6 +7,7 @@ import '../../list/crud-list-pagination-links.js';
7
7
  import '../crud-list-item.js';
8
8
  import '../crud-select-all.js';
9
9
  import '../crud-list-service.js';
10
+ import '../crud-data-grid.js';
10
11
  import { DileI18nMixin } from '../../../lib/DileI18nMixin.js';
11
12
 
12
13
  export class DileCrudList extends DileI18nMixin(DileLoading(LitElement)) {
@@ -67,9 +68,14 @@ export class DileCrudList extends DileI18nMixin(DileLoading(LitElement)) {
67
68
  }
68
69
  .elements-container {
69
70
  display: grid;
70
- grid-template-columns: var(--dile-crud-list-elements-container-template-columns, minmax(0, 1fr));
71
+ grid-template-columns: var(--dile-crud-list-elements-container-template-columns, minmax(0, 1fr));
71
72
  gap: var(--dile-crud-list-elements-container-gap, 0);
72
73
  }
74
+ .grid-container {
75
+ display: block;
76
+ width: 100%;
77
+ margin: var(--dile-crud-list-grid-container-margin, 0 0 1rem 0);
78
+ }
73
79
  `
74
80
  ];
75
81
 
@@ -94,7 +100,7 @@ export class DileCrudList extends DileI18nMixin(DileLoading(LitElement)) {
94
100
 
95
101
  constructor() {
96
102
  super();
97
- this.paginationData = {}
103
+ this.paginationData = {};
98
104
  this.elements = [];
99
105
  this.pageSize = 10;
100
106
  this.keyword = '';
@@ -207,6 +213,7 @@ export class DileCrudList extends DileI18nMixin(DileLoading(LitElement)) {
207
213
  ></dile-ajax>
208
214
  `;
209
215
  }
216
+
210
217
  get emptyTemplate() {
211
218
  return html`
212
219
  <div class="empty">
@@ -243,6 +250,32 @@ export class DileCrudList extends DileI18nMixin(DileLoading(LitElement)) {
243
250
  }
244
251
 
245
252
  get elementsTemplate() {
253
+ if (this.config?.templates?.grid && typeof this.config.templates.grid === 'function') {
254
+ return html`
255
+ <div
256
+ class="grid-container"
257
+ @item-checkbox-changed=${this.onItemsCheckboxChanged}
258
+ @dile-data-grid-sort=${this.onGridSort}
259
+ >
260
+ ${this.config.templates.grid(this.elements, this.actionIds, this.config)}
261
+ </div>
262
+ `;
263
+ }
264
+ if (this.config?.grid?.columns && Array.isArray(this.config.grid.columns)) {
265
+ return html`
266
+ <div
267
+ class="grid-container"
268
+ @item-checkbox-changed=${this.onItemsCheckboxChanged}
269
+ @dile-data-grid-sort=${this.onGridSort}
270
+ >
271
+ <dile-crud-data-grid
272
+ .items=${this.elements}
273
+ .selectedIds=${this.actionIds}
274
+ .config=${this.config}
275
+ ></dile-crud-data-grid>
276
+ </div>
277
+ `;
278
+ }
246
279
  return html`
247
280
  <div class="elements-container">
248
281
  ${this.elements.map(element => html`
@@ -252,7 +285,7 @@ export class DileCrudList extends DileI18nMixin(DileLoading(LitElement)) {
252
285
  .actionIds="${this.actionIds}"
253
286
  ?disableEdit=${!this.isItemEditable(element)}
254
287
  ?disableDelete=${!this.isItemDeletable(element)}
255
- ?disableRestore=${this.config?.customization?.disableRestore}
288
+ ?disableRestore=${this.config?.customization?.disableRestore || this.config?.customization?.disableListActions}
256
289
  ?hideCheckboxSelection="${this.config?.customization?.hideCheckboxSelection}"
257
290
  @item-checkbox-changed=${this.onItemsCheckboxChanged}
258
291
  ?isDeleted=${element.deleted_at}
@@ -276,13 +309,13 @@ export class DileCrudList extends DileI18nMixin(DileLoading(LitElement)) {
276
309
  }
277
310
 
278
311
  isItemEditable(element) {
279
- const isGloballyDisabled = this.config?.customization?.disableEdit;
312
+ const isGloballyDisabled = this.config?.customization?.disableListActions || this.config?.customization?.disableEdit;
280
313
  const isItemEditable = this.config?.isItemEditable(element) ?? true;
281
314
  return !isGloballyDisabled && isItemEditable;
282
315
  }
283
316
 
284
317
  isItemDeletable(element) {
285
- const isGloballyDisabled = this.config?.customization?.disableDelete;
318
+ const isGloballyDisabled = this.config?.customization?.disableListActions || this.config?.customization?.disableDelete;
286
319
  const isItemDeletable = this.config?.isItemDeletable(element) ?? true;
287
320
  return !isGloballyDisabled && isItemDeletable;
288
321
  }
@@ -314,11 +347,11 @@ export class DileCrudList extends DileI18nMixin(DileLoading(LitElement)) {
314
347
  }
315
348
 
316
349
  goNext() {
317
- this.elservice.goNext()
350
+ this.elservice.goNext();
318
351
  }
319
352
 
320
353
  goPrev() {
321
- this.elservice.goPrev()
354
+ this.elservice.goPrev();
322
355
  }
323
356
 
324
357
  refresh() {
@@ -332,7 +365,6 @@ export class DileCrudList extends DileI18nMixin(DileLoading(LitElement)) {
332
365
  this.shadowRoot.querySelector('dile-crud-select-all').reset();
333
366
  }
334
367
  this.elservice.refresh();
335
-
336
368
  }
337
369
 
338
370
  setKeyword(keyword) {
@@ -347,6 +379,20 @@ export class DileCrudList extends DileI18nMixin(DileLoading(LitElement)) {
347
379
  this.elservice.setSort(sortObject);
348
380
  }
349
381
 
382
+ onGridSort(e) {
383
+ if (e.detail?.field) {
384
+ this.setSort({
385
+ sortField: e.detail.field,
386
+ sortDirection: e.detail.direction || 'asc',
387
+ });
388
+ } else {
389
+ this.setSort({
390
+ sortField: null,
391
+ sortDirection: null,
392
+ });
393
+ }
394
+ }
395
+
350
396
  setPageSize(size) {
351
397
  this.loading = true;
352
398
  this.pageSize = size;
@@ -389,7 +435,7 @@ export class DileCrudList extends DileI18nMixin(DileLoading(LitElement)) {
389
435
  if(this.config.customization?.disablePagination) {
390
436
  let ids = this.elements.map(item => this.config.computeItemId(item));
391
437
  this.dispactSelectAll(ids);
392
- } else{
438
+ } else {
393
439
  let data = {
394
440
  keyword: this.keyword,
395
441
  filters: this.filters,
@@ -413,7 +459,6 @@ export class DileCrudList extends DileI18nMixin(DileLoading(LitElement)) {
413
459
  if (!e.detail.checked && this.isSelectAllActive) {
414
460
  this.shadowRoot.querySelector('dile-crud-select-all').resetWithoutDispatch();
415
461
  }
416
-
417
462
  }
418
463
 
419
464
  onListError(e) {
@@ -1,67 +1,42 @@
1
- import { LitElement, html, css } from 'lit';
1
+ import { LitElement, html, css, nothing } from 'lit';
2
2
  import '@dile/ui/components/checkbox/checkbox.js';
3
- import '@dile/ui/components/button/button.js';
4
- import '@dile/iconlib/material-icons/edit.js';
5
- import '@dile/iconlib/material-icons/delete.js';
6
- import '@dile/iconlib/material-icons/restore-from-trash.js';
3
+ import '../crud-item-actions.js';
7
4
 
8
5
  export class DileCrudListItem extends LitElement {
9
6
  static styles = [
10
7
  css`
11
- :host {
12
- display: var(--dile-crud-list-item-display, block);
13
- max-width: var(--dile-crud-list-item-max-width, 100%);
14
- width: var(--dile-crud-list-item-width, 100%);
15
- }
8
+ :host {
9
+ display: var(--dile-crud-list-item-display, block);
10
+ max-width: var(--dile-crud-list-item-max-width, 100%);
11
+ width: var(--dile-crud-list-item-width, 100%);
12
+ }
13
+ section {
14
+ max-width: 100%;
15
+ overflow: hidden;
16
+ display: flex;
17
+ align-items: center;
18
+ padding: var(--dile-crud-list-item-padding, 0.5rem 0.5rem);
19
+ border-bottom: var(--dile-crud-list-item-line-separator, 1px solid #ddd);
20
+ }
21
+ dile-checkbox {
22
+ margin-right: 0.5rem;
23
+ --dile-checkbox-unchecked-color: #888;
24
+ }
25
+ main {
26
+ flex-grow: 1;
27
+ overflow: hidden;
28
+ }
29
+ .actions {
30
+ margin-left: 0.5rem;
31
+ display: flex;
32
+ align-items: center;
33
+ text-align: right;
34
+ }
35
+ @media (min-width: 550px) {
16
36
  section {
17
- max-width: 100%;
18
- overflow: hidden;
19
- display: flex;
20
- align-items: center;
21
- padding: var(--dile-crud-list-item-padding, 0.5rem 0.5rem);
22
- border-bottom: var(--dile-crud-list-item-line-separator, 1px solid #ddd);
23
- }
24
- dile-checkbox {
25
- margin-right: 0.5rem;
26
- --dile-checkbox-unchecked-color: #888;
27
- }
28
- main {
29
- flex-grow: 1;
30
- overflow: hidden;
31
- }
32
- .actions {
33
- margin-left: 0.5rem;
34
- display: flex;
35
- align-items: center;
36
- text-align: right;
37
- }
38
- dile-button.action-button {
39
- --dile-button-background-color: var(--dile-crud-list-item-action-button-background-color, transparent);
40
- --dile-button-border-color: var(--dile-crud-list-item-action-button-border-color, transparent);
41
- --dile-button-hover-background-color: var(--dile-crud-list-item-action-button-hover-background-color, var(--dile-neutral-color, transparent));
42
- --dile-button-hover-border-color: var(--dile-crud-list-item-action-button-hover-border-color, transparent);
43
- --dile-button-padding-y: var(--dile-crud-list-item-action-button-padding-y, 0.25rem);
44
- --dile-button-padding-x: var(--dile-crud-list-item-action-button-padding-x, 0.25rem);
45
- --dile-button-icon-size: var(--dile-crud-list-item-action-button-icon-size, 24px);
46
- --dile-button-border-radius: var(--dile-crud-list-item-action-button-border-radius, 2rem);
47
- }
48
- dile-button.action-button.edit {
49
- --dile-button-icon-color: var(--edit-icon-color, var(--dile-alert-neutral-color, #2889a7));
50
- --dile-button-icon-hover-color: var(--dile-crud-list-item-edit-icon-hover-color, var(--edit-icon-color, var(--dile-alert-neutral-color, #2889a7)));
51
- }
52
- dile-button.action-button.delete {
53
- --dile-button-icon-color: var(--delete-icon-color, var(--dile-danger-color, #e33));
54
- --dile-button-icon-hover-color: var(--dile-crud-list-item-delete-icon-hover-color, var(--delete-icon-color, var(--dile-danger-color, #e33)));
55
- }
56
- dile-button.action-button.restore {
57
- --dile-button-icon-color: var(--restore-icon-color, var(--dile-alert-success-color, #00900f));
58
- --dile-button-icon-hover-color: var(--dile-crud-list-item-restore-icon-hover-color, var(--restore-icon-color, var(--dile-alert-success-color, #00900f)));
59
- }
60
- @media(min-width: 550px) {
61
- section {
62
- padding: var(--dile-crud-list-item-padding, 0.5rem 1rem);
63
- }
37
+ padding: var(--dile-crud-list-item-padding, 0.5rem 1rem);
64
38
  }
39
+ }
65
40
  `
66
41
  ];
67
42
 
@@ -72,6 +47,7 @@ export class DileCrudListItem extends LitElement {
72
47
  actionIds: { type: Array },
73
48
  disableEdit: { type: Boolean },
74
49
  disableDelete: { type: Boolean },
50
+ disableRestore: { type: Boolean },
75
51
  /** Hide checkboxes on the item list */
76
52
  hideCheckboxSelection: { type: Boolean },
77
53
  isDeleted: { type: Boolean },
@@ -85,86 +61,64 @@ export class DileCrudListItem extends LitElement {
85
61
  this.disableDelete = false;
86
62
  this.disableRestore = false;
87
63
  this.hideCheckboxSelection = false;
64
+ this.isDeleted = false;
65
+ }
88
66
 
67
+ get hasActions() {
68
+ if (this.isDeleted) {
69
+ return !this.disableRestore;
70
+ }
71
+ return !this.disableEdit || !this.disableDelete;
89
72
  }
90
73
 
91
74
  render() {
92
75
  return html`
93
- <section>
94
- ${this.hideCheckboxSelection ? '' : html`
95
- <dile-checkbox ?checked="${this.includes(this.actionIds, this.itemId)}" @dile-checkbox-changed=${this.checkboxChanged}></dile-checkbox>
76
+ <section>
77
+ ${this.hideCheckboxSelection
78
+ ? ''
79
+ : html`
80
+ <dile-checkbox
81
+ ?checked="${this.includes(this.actionIds, this.itemId)}"
82
+ @dile-checkbox-changed=${this.checkboxChanged}
83
+ ></dile-checkbox>
96
84
  `}
97
- <main>
98
- <slot></slot>
99
- </main>
100
- <div class="actions">
101
- ${this.isDeleted
102
- ? this.restoreActionsTemplate
103
- : this.regularActionsTemplate
104
- }
105
- </div>
106
- </section>
85
+ <main>
86
+ <slot></slot>
87
+ </main>
88
+ ${this.hasActions
89
+ ? html`
90
+ <div class="actions">
91
+ <dile-crud-item-actions
92
+ .item=${this.item}
93
+ itemId="${this.itemId}"
94
+ ?disableEdit=${this.disableEdit}
95
+ ?disableDelete=${this.disableDelete}
96
+ ?disableRestore=${this.disableRestore}
97
+ ?isDeleted=${this.isDeleted}
98
+ ></dile-crud-item-actions>
99
+ </div>
100
+ `
101
+ : nothing
102
+ }
103
+ </section>
107
104
  `;
108
105
  }
109
106
 
110
- get regularActionsTemplate() {
111
- return html`
112
- ${this.disableEdit
113
- ? ''
114
- : html`<dile-button class="action-button edit" icon="material.edit" label="Edit" @click=${this.editClick}></dile-button>`
115
- }
116
- ${this.disableDelete
117
- ? ''
118
- : html`<dile-button class="action-button delete" icon="material.delete" label="Delete" @click=${this.deleteClick}></dile-button>`
119
- }
120
- `
121
- }
122
-
123
- get restoreActionsTemplate() {
124
- return html`
125
- ${this.disableRestore
126
- ? ''
127
- : html`<dile-button class="action-button restore" icon="material.restore-from-trash" label="Restore" @click=${this.restoreClick}></dile-button>`
128
- }
129
- `
130
- }
131
-
132
107
  includes(actionIds, itemId) {
133
108
  const stringIds = actionIds.map(String);
134
109
  return stringIds.includes(String(itemId));
135
110
  }
136
111
 
137
112
  checkboxChanged(e) {
138
- this.dispatchEvent(new CustomEvent('item-checkbox-changed', {
139
- bubbles: true,
140
- composed: true,
141
- detail: {
142
- checked: e.detail.checked,
143
- itemId: this.itemId
144
- }
145
- }));
146
- }
147
-
148
- _dispatchItemEvent(eventName) {
149
- this.dispatchEvent(new CustomEvent(eventName, {
150
- bubbles: true,
151
- composed: true,
152
- detail: {
153
- item: this.item,
154
- itemId: this.itemId,
155
- }
156
- }));
157
- }
158
-
159
- editClick() {
160
- this._dispatchItemEvent('crud-item-edit');
161
- }
162
-
163
- deleteClick() {
164
- this._dispatchItemEvent('crud-item-delete');
165
- }
166
-
167
- restoreClick() {
168
- this._dispatchItemEvent('crud-item-restore');
113
+ this.dispatchEvent(
114
+ new CustomEvent('item-checkbox-changed', {
115
+ bubbles: true,
116
+ composed: true,
117
+ detail: {
118
+ checked: e.detail.checked,
119
+ itemId: this.itemId,
120
+ },
121
+ })
122
+ );
169
123
  }
170
- }
124
+ }
@@ -0,0 +1,32 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { CrudConfigBuilder } from './CrudConfigBuilder.js';
3
+
4
+ describe('CrudConfigBuilder', () => {
5
+ it('merges customConfig.grid.columns over the default null grid', () => {
6
+ const columns = [{ field: 'id', header: 'ID' }];
7
+
8
+ const config = new CrudConfigBuilder('https://example.test/api/items', {
9
+ grid: { columns },
10
+ }).getConfig();
11
+
12
+ expect(config.grid.columns).toEqual(columns);
13
+ });
14
+
15
+ it('sets the endpoint from the constructor argument', () => {
16
+ const config = new CrudConfigBuilder('https://example.test/api/items', {}).getConfig();
17
+
18
+ expect(config.endpoint).toBe('https://example.test/api/items');
19
+ });
20
+
21
+ it('getConfig(endpoint) returns a copy with an overridden endpoint without mutating the original config', () => {
22
+ const builder = new CrudConfigBuilder('https://example.test/api/items', {
23
+ grid: { columns: [{ field: 'id' }] },
24
+ });
25
+
26
+ const relatedConfig = builder.getConfig('https://example.test/api/items/1/related');
27
+
28
+ expect(relatedConfig.endpoint).toBe('https://example.test/api/items/1/related');
29
+ expect(builder.getConfig().endpoint).toBe('https://example.test/api/items');
30
+ expect(relatedConfig.grid.columns).toEqual([{ field: 'id' }]);
31
+ });
32
+ });
@@ -0,0 +1,45 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { deepMerge } from './deepMerge.js';
3
+
4
+ describe('deepMerge', () => {
5
+ it('replaces a null target property with a plain object from source', () => {
6
+ const target = { grid: null };
7
+ const source = { grid: { columns: [{ field: 'id' }] } };
8
+
9
+ const result = deepMerge(target, source);
10
+
11
+ expect(result.grid).toEqual({ columns: [{ field: 'id' }] });
12
+ });
13
+
14
+ it('preserves array order and content when merging a columns array under a previously-null key', () => {
15
+ const columns = [
16
+ { field: 'id', header: 'ID' },
17
+ { field: 'name', header: 'Name' },
18
+ { field: 'continent', header: 'Continent' },
19
+ ];
20
+
21
+ const result = deepMerge({ grid: null }, { grid: { columns } });
22
+
23
+ expect(result.grid.columns).toEqual(columns);
24
+ expect(result.grid.columns).not.toBe(columns);
25
+ });
26
+
27
+ it('does not mutate the source when merging into a null target key', () => {
28
+ const source = { grid: { columns: [{ field: 'id' }] } };
29
+
30
+ const result = deepMerge({ grid: null }, source);
31
+ result.grid.columns.push({ field: 'extra' });
32
+
33
+ expect(source.grid.columns.length).toBe(1);
34
+ });
35
+
36
+ it('overwrites configured overwriteProperties instances as-is instead of merging them', () => {
37
+ class Adapter {}
38
+ const targetAdapter = new Adapter();
39
+ const sourceAdapter = new Adapter();
40
+
41
+ const result = deepMerge({ responseAdapter: targetAdapter }, { responseAdapter: sourceAdapter });
42
+
43
+ expect(result.responseAdapter).toBe(sourceAdapter);
44
+ });
45
+ });
@@ -16,6 +16,7 @@ export const defaultConfig = {
16
16
  initial: 25,
17
17
  },
18
18
  maxBatchActionItems: 100,
19
+ grid: null,
19
20
  customization: {
20
21
  disableKeywordSearch: true,
21
22
  hideCountSummary: false,
@@ -26,6 +27,7 @@ export const defaultConfig = {
26
27
  disableEdit: false,
27
28
  disableDelete: false,
28
29
  disableRestore: false,
30
+ disableListActions: false,
29
31
  disablePagination: true,
30
32
  disableSort: true,
31
33
  disableFilter: true,
@@ -62,6 +64,7 @@ export const defaultConfig = {
62
64
  },
63
65
  templates: {
64
66
  item: () => templatePlaceholder('item'),
67
+ grid: null,
65
68
  insertForm: (belongsTo, relationId) => templatePlaceholder('insertForm'),
66
69
  updateForm: () => templatePlaceholder('updateForm'),
67
70
  help: () => templatePlaceholder('help'),
@@ -84,4 +87,4 @@ export const defaultConfig = {
84
87
  onActionListSuccess(actionSuccessDetail) {},
85
88
  onActionSingleSuccess(actionSuccessDetail) {},
86
89
  destructiveActionNames: ['DeleteAction'],
87
- }
90
+ }