@kubex/zinc 1.1.150 → 1.1.152
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/dist/custom-elements.json +22 -3
- package/dist/vscode.html-custom-data.json +5 -0
- package/dist/web-types.json +11 -1
- package/dist/zn.d.ts +6 -0
- package/dist/zn.min.js +291 -291
- package/docs/pages/components/data-table.md +25 -3
- package/package.json +1 -1
- package/src/components/data-table/data-table.component.ts +26 -17
- package/src/components/data-table/data-table.test.ts +92 -7
- package/src/components/page/page.component.ts +4 -2
- package/src/components/page/page.scss +6 -1
|
@@ -926,6 +926,30 @@ The data table expects responses in the following format:
|
|
|
926
926
|
|
|
927
927
|
For POST requests, the table sends a JSON body:
|
|
928
928
|
|
|
929
|
+
```json
|
|
930
|
+
{
|
|
931
|
+
"page": 1,
|
|
932
|
+
"perPage": 10,
|
|
933
|
+
"sortColumn": "name",
|
|
934
|
+
"sortDirection": "asc",
|
|
935
|
+
"filter": "",
|
|
936
|
+
"search": "search term",
|
|
937
|
+
"status": "open"
|
|
938
|
+
}
|
|
939
|
+
```
|
|
940
|
+
|
|
941
|
+
Field values from inputs slotted into a `zn-data-table-search`, and anything set on the table's `requestParams`, are merged at the **root** of the body under their own `name`. The search text itself is sent as `search`.
|
|
942
|
+
|
|
943
|
+
Parameters from the `inputs` slot are context/system values (a CSRF token, a package name, and the like) sent with every request. They are merged at the root too.
|
|
944
|
+
|
|
945
|
+
### Wrapped search fields
|
|
946
|
+
|
|
947
|
+
Add `wrap-search-fields` to nest the search field values under a `searchFields` object instead, so a backend can bind them to a single map rather than arbitrary top-level keys:
|
|
948
|
+
|
|
949
|
+
```html
|
|
950
|
+
<zn-data-table wrap-search-fields data-uri="/data"></zn-data-table>
|
|
951
|
+
```
|
|
952
|
+
|
|
929
953
|
```json
|
|
930
954
|
{
|
|
931
955
|
"page": 1,
|
|
@@ -941,6 +965,4 @@ For POST requests, the table sends a JSON body:
|
|
|
941
965
|
}
|
|
942
966
|
```
|
|
943
967
|
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
Parameters from the `inputs` slot are context/system values (a CSRF token, a package name, and the like) sent with every request. They are merged at the **root** of the body, not inside `searchFields`.
|
|
968
|
+
`q` mirrors the `search` text, which is still sent at the root. Empty values are dropped, and `searchFields` is `null` when nothing meaningful remains — no search text and no field values — so the backend can treat that as "no search". Parameters from the `inputs` slot stay at the root either way.
|
package/package.json
CHANGED
|
@@ -151,8 +151,6 @@ interface DataRequest {
|
|
|
151
151
|
sortDirection: string;
|
|
152
152
|
filter: string;
|
|
153
153
|
search: string;
|
|
154
|
-
// Search-related fields (from the search component's `fields` slot) plus `q` (the search text),
|
|
155
|
-
// wrapped so the backend can bind them to a single map instead of arbitrary root-level keys.
|
|
156
154
|
searchFields?: Record<string, any> | null;
|
|
157
155
|
}
|
|
158
156
|
|
|
@@ -278,6 +276,13 @@ export default class ZnDataTable extends ZincElement {
|
|
|
278
276
|
*/
|
|
279
277
|
@property({attribute: 'sharable', type: Boolean}) sharable: boolean = false;
|
|
280
278
|
|
|
279
|
+
/**
|
|
280
|
+
* When set, extra request params (search-component field values) are nested under a
|
|
281
|
+
* `searchFields` object - with `q` mirroring the search text - instead of being merged at the
|
|
282
|
+
* root of the request body.
|
|
283
|
+
*/
|
|
284
|
+
@property({attribute: 'wrap-search-fields', type: Boolean}) wrapSearchFields: boolean = false;
|
|
285
|
+
|
|
281
286
|
@property({attribute: 'group-by'}) groupBy = '';
|
|
282
287
|
|
|
283
288
|
@property() groups = '';
|
|
@@ -357,7 +362,7 @@ export default class ZnDataTable extends ZincElement {
|
|
|
357
362
|
};
|
|
358
363
|
|
|
359
364
|
// Inputs-slot values are context/system params (e.g. csrf token, package name) sent with
|
|
360
|
-
// every request
|
|
365
|
+
// every request.
|
|
361
366
|
const inputs = this.hasSlotController.getSlots(ActionSlots.inputs.valueOf());
|
|
362
367
|
const params: Record<string, any> = {};
|
|
363
368
|
if (inputs) {
|
|
@@ -374,24 +379,28 @@ export default class ZnDataTable extends ZincElement {
|
|
|
374
379
|
Object.assign(requestData, params);
|
|
375
380
|
}
|
|
376
381
|
|
|
377
|
-
//
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
382
|
+
// Add any extra request params
|
|
383
|
+
const extraParams = requestParams && typeof requestParams === 'object'
|
|
384
|
+
? requestParams as Record<string, unknown>
|
|
385
|
+
: {};
|
|
386
|
+
|
|
387
|
+
if (this.wrapSearchFields) {
|
|
388
|
+
// Opt-in shape: field values nested under `searchFields`, with `q` mirroring the search
|
|
389
|
+
// text, so a backend can bind them to one map. Empty values are dropped, and the key is
|
|
390
|
+
// null when nothing is set. The root `search` key is sent either way.
|
|
391
|
+
const searchFields: Record<string, any> = {};
|
|
392
|
+
for (const [key, value] of Object.entries(extraParams)) {
|
|
385
393
|
if (value !== undefined && value !== null && value !== '') {
|
|
386
394
|
searchFields[key] = value;
|
|
387
395
|
}
|
|
388
396
|
}
|
|
397
|
+
if (this.search || Object.keys(searchFields).length > 0) {
|
|
398
|
+
searchFields.q = this.search;
|
|
399
|
+
}
|
|
400
|
+
requestData.searchFields = Object.keys(searchFields).length > 0 ? searchFields : null;
|
|
401
|
+
} else {
|
|
402
|
+
Object.assign(requestData, extraParams);
|
|
389
403
|
}
|
|
390
|
-
if (this.search || Object.keys(searchFields).length > 0) {
|
|
391
|
-
searchFields.q = this.search;
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
requestData.searchFields = Object.keys(searchFields).length > 0 ? searchFields : null;
|
|
395
404
|
|
|
396
405
|
// This is also used for Rubix, so it may not work for your application.
|
|
397
406
|
const response = await fetch(dataUri, {
|
|
@@ -623,7 +632,7 @@ export default class ZnDataTable extends ZincElement {
|
|
|
623
632
|
params.set(key, str);
|
|
624
633
|
};
|
|
625
634
|
|
|
626
|
-
// Extra field values
|
|
635
|
+
// Extra field values - their default is empty, so setParam writes them only when set.
|
|
627
636
|
const known = new Set(ZnDataTable._sharableKnownKeys);
|
|
628
637
|
Object.entries(this.requestParams).forEach(([key, value]) => {
|
|
629
638
|
if (known.has(key) || key === 'searchUri') return;
|
|
@@ -323,9 +323,9 @@ describe('<zn-data-table>', () => {
|
|
|
323
323
|
expect(link?.hasAttribute('title')).to.be.false;
|
|
324
324
|
});
|
|
325
325
|
|
|
326
|
-
// Search-component field values
|
|
327
|
-
//
|
|
328
|
-
describe('
|
|
326
|
+
// Search-component field values and any other extra request params are merged at the root of
|
|
327
|
+
// the POST body, alongside the dedicated `search` key.
|
|
328
|
+
describe('extra request params', () => {
|
|
329
329
|
const rowResponse = () => new Response(JSON.stringify({
|
|
330
330
|
rows: [{id: '1', cells: [{text: 'Row', column: 'name'}]}],
|
|
331
331
|
page: 1,
|
|
@@ -333,7 +333,7 @@ describe('<zn-data-table>', () => {
|
|
|
333
333
|
total: 1,
|
|
334
334
|
}), {status: 200, headers: {'Content-Type': 'application/json'}});
|
|
335
335
|
|
|
336
|
-
it('
|
|
336
|
+
it('sends search-component field values at the root alongside search', async () => {
|
|
337
337
|
const originalFetch = window.fetch;
|
|
338
338
|
const bodies: Record<string, unknown>[] = [];
|
|
339
339
|
window.fetch = (_url: RequestInfo | URL, options?: RequestInit) => {
|
|
@@ -355,6 +355,89 @@ describe('<zn-data-table>', () => {
|
|
|
355
355
|
|
|
356
356
|
const body = bodies[bodies.length - 1];
|
|
357
357
|
expect(body.search).to.equal('foo');
|
|
358
|
+
expect(body.status).to.equal('open');
|
|
359
|
+
expect(body.searchFields).to.be.undefined;
|
|
360
|
+
} finally {
|
|
361
|
+
window.fetch = originalFetch;
|
|
362
|
+
}
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
it('sends no extra keys when no search or field values are set', async () => {
|
|
366
|
+
const originalFetch = window.fetch;
|
|
367
|
+
const bodies: Record<string, unknown>[] = [];
|
|
368
|
+
window.fetch = (_url: RequestInfo | URL, options?: RequestInit) => {
|
|
369
|
+
if (options?.body) bodies.push(JSON.parse(options.body as string) as Record<string, unknown>);
|
|
370
|
+
return Promise.resolve(rowResponse());
|
|
371
|
+
};
|
|
372
|
+
|
|
373
|
+
try {
|
|
374
|
+
await fixture<ZnDataTable>(html`
|
|
375
|
+
<zn-data-table data-uri="/test-data" headers='{"name": {"key": "name", "label": "Name"}}'></zn-data-table>`);
|
|
376
|
+
await waitUntil(() => bodies.length > 0);
|
|
377
|
+
|
|
378
|
+
const known = ['filter', 'page', 'perPage', 'search', 'sortColumn', 'sortDirection'];
|
|
379
|
+
expect(Object.keys(bodies[bodies.length - 1]).filter(k => !known.includes(k))).to.deep.equal([]);
|
|
380
|
+
} finally {
|
|
381
|
+
window.fetch = originalFetch;
|
|
382
|
+
}
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
it('sends inputs-slot params at the request root', async () => {
|
|
386
|
+
const originalFetch = window.fetch;
|
|
387
|
+
const bodies: Record<string, unknown>[] = [];
|
|
388
|
+
window.fetch = (_url: RequestInfo | URL, options?: RequestInit) => {
|
|
389
|
+
if (options?.body) bodies.push(JSON.parse(options.body as string) as Record<string, unknown>);
|
|
390
|
+
return Promise.resolve(rowResponse());
|
|
391
|
+
};
|
|
392
|
+
|
|
393
|
+
try {
|
|
394
|
+
const el = await fixture<ZnDataTable>(html`
|
|
395
|
+
<zn-data-table data-uri="/test-data" headers='{"name": {"key": "name", "label": "Name"}}'>
|
|
396
|
+
<input slot="inputs" name="csrf" value="tok">
|
|
397
|
+
</zn-data-table>`);
|
|
398
|
+
await waitUntil(() => bodies.length > 0);
|
|
399
|
+
el.refresh();
|
|
400
|
+
await waitUntil(() => bodies.length > 1);
|
|
401
|
+
|
|
402
|
+
expect(bodies[bodies.length - 1].csrf).to.equal('tok');
|
|
403
|
+
} finally {
|
|
404
|
+
window.fetch = originalFetch;
|
|
405
|
+
}
|
|
406
|
+
});
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
// With `wrap-search-fields`, the same values are nested under `searchFields` instead, with `q`
|
|
410
|
+
// mirroring the search text.
|
|
411
|
+
describe('wrap-search-fields', () => {
|
|
412
|
+
const rowResponse = () => new Response(JSON.stringify({
|
|
413
|
+
rows: [{id: '1', cells: [{text: 'Row', column: 'name'}]}],
|
|
414
|
+
page: 1,
|
|
415
|
+
perPage: 10,
|
|
416
|
+
total: 1,
|
|
417
|
+
}), {status: 200, headers: {'Content-Type': 'application/json'}});
|
|
418
|
+
|
|
419
|
+
it('nests field values under searchFields with q mirroring the search text', async () => {
|
|
420
|
+
const originalFetch = window.fetch;
|
|
421
|
+
const bodies: Record<string, unknown>[] = [];
|
|
422
|
+
window.fetch = (_url: RequestInfo | URL, options?: RequestInit) => {
|
|
423
|
+
if (options?.body) bodies.push(JSON.parse(options.body as string) as Record<string, unknown>);
|
|
424
|
+
return Promise.resolve(rowResponse());
|
|
425
|
+
};
|
|
426
|
+
|
|
427
|
+
try {
|
|
428
|
+
const el = await fixture<ZnDataTable>(html`
|
|
429
|
+
<zn-data-table wrap-search-fields data-uri="/test-data"
|
|
430
|
+
headers='{"name": {"key": "name", "label": "Name"}}'></zn-data-table>`);
|
|
431
|
+
await waitUntil(() => bodies.length > 0);
|
|
432
|
+
|
|
433
|
+
el.search = 'foo';
|
|
434
|
+
el.requestParams = {status: 'open', empty: ''};
|
|
435
|
+
el.refresh();
|
|
436
|
+
await waitUntil(() => bodies.some(b => b.search === 'foo'));
|
|
437
|
+
|
|
438
|
+
const body = bodies[bodies.length - 1];
|
|
439
|
+
expect(body.search).to.equal('foo');
|
|
440
|
+
expect(body.status).to.be.undefined;
|
|
358
441
|
expect(body.searchFields).to.deep.equal({status: 'open', q: 'foo'});
|
|
359
442
|
} finally {
|
|
360
443
|
window.fetch = originalFetch;
|
|
@@ -371,7 +454,8 @@ describe('<zn-data-table>', () => {
|
|
|
371
454
|
|
|
372
455
|
try {
|
|
373
456
|
await fixture<ZnDataTable>(html`
|
|
374
|
-
<zn-data-table data-uri="/test-data"
|
|
457
|
+
<zn-data-table wrap-search-fields data-uri="/test-data"
|
|
458
|
+
headers='{"name": {"key": "name", "label": "Name"}}'></zn-data-table>`);
|
|
375
459
|
await waitUntil(() => bodies.length > 0);
|
|
376
460
|
|
|
377
461
|
expect(bodies[bodies.length - 1].searchFields).to.be.null;
|
|
@@ -380,7 +464,7 @@ describe('<zn-data-table>', () => {
|
|
|
380
464
|
}
|
|
381
465
|
});
|
|
382
466
|
|
|
383
|
-
it('keeps inputs-slot params at the request root
|
|
467
|
+
it('keeps inputs-slot params at the request root', async () => {
|
|
384
468
|
const originalFetch = window.fetch;
|
|
385
469
|
const bodies: Record<string, unknown>[] = [];
|
|
386
470
|
window.fetch = (_url: RequestInfo | URL, options?: RequestInit) => {
|
|
@@ -390,7 +474,8 @@ describe('<zn-data-table>', () => {
|
|
|
390
474
|
|
|
391
475
|
try {
|
|
392
476
|
const el = await fixture<ZnDataTable>(html`
|
|
393
|
-
<zn-data-table data-uri="/test-data"
|
|
477
|
+
<zn-data-table wrap-search-fields data-uri="/test-data"
|
|
478
|
+
headers='{"name": {"key": "name", "label": "Name"}}'>
|
|
394
479
|
<input slot="inputs" name="csrf" value="tok">
|
|
395
480
|
</zn-data-table>`);
|
|
396
481
|
await waitUntil(() => bodies.length > 0);
|
|
@@ -44,7 +44,7 @@ export default class ZnPage extends ZnTabs {
|
|
|
44
44
|
'zn-tab': ZnTab
|
|
45
45
|
};
|
|
46
46
|
|
|
47
|
-
private readonly pageSlotController = new HasSlotController(this, 'breadcrumb', 'actions', 'caption');
|
|
47
|
+
private readonly pageSlotController = new HasSlotController(this, 'breadcrumb', 'actions', 'caption', 'description');
|
|
48
48
|
|
|
49
49
|
@property() caption: string;
|
|
50
50
|
@property({attribute: 'entity-id'}) entityId: string;
|
|
@@ -442,6 +442,7 @@ export default class ZnPage extends ZnTabs {
|
|
|
442
442
|
const hasEntityId = this.entityId;
|
|
443
443
|
const hasFullLocation = this.fullLocation;
|
|
444
444
|
const hasPreviousPath = this.previousPath;
|
|
445
|
+
const hasDescription = !!this.summary || this.pageSlotController.test('description');
|
|
445
446
|
|
|
446
447
|
return html`
|
|
447
448
|
<div class="page" part="base" @scroll="${this.handlePageScroll}">
|
|
@@ -455,7 +456,8 @@ export default class ZnPage extends ZnTabs {
|
|
|
455
456
|
'header--has-entity-id': hasEntityId,
|
|
456
457
|
'header--has-full-location': hasFullLocation,
|
|
457
458
|
'header--has-navigation': hasNavigation,
|
|
458
|
-
'header--has-previous': hasPreviousPath
|
|
459
|
+
'header--has-previous': hasPreviousPath,
|
|
460
|
+
'header--has-description': hasDescription
|
|
459
461
|
})}">
|
|
460
462
|
${hasFullLocation || hasEntityId ? html`
|
|
461
463
|
<div class="${classMap({
|
|
@@ -184,7 +184,7 @@
|
|
|
184
184
|
display: flex;
|
|
185
185
|
justify-content: flex-start;
|
|
186
186
|
flex-direction: column;
|
|
187
|
-
gap: var(--zn-spacing-
|
|
187
|
+
gap: var(--zn-spacing-2x-small);
|
|
188
188
|
min-height: 36px;
|
|
189
189
|
flex-shrink: 1;
|
|
190
190
|
min-width: 0;
|
|
@@ -215,6 +215,11 @@
|
|
|
215
215
|
min-height: 36px; // Keep in step with the actions row: both bands share a centre line.
|
|
216
216
|
}
|
|
217
217
|
|
|
218
|
+
// Stacked above a description the band buys no alignment, it only pads the gap.
|
|
219
|
+
&--has-description &__caption {
|
|
220
|
+
min-height: 0;
|
|
221
|
+
}
|
|
222
|
+
|
|
218
223
|
&__description {
|
|
219
224
|
@include wc.text-style(subheading);
|
|
220
225
|
display: block;
|