@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.
@@ -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
- Field values from a slotted `zn-data-table-search`'s [`fields` slot](/components/data-table-search#filter-fields) are wrapped under `searchFields`, keeping them out of the request root so the backend can bind them to a single map rather than arbitrary top-level keys. `q` mirrors the `search` text, and the root `search` key is retained for back-compatibility. 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".
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kubex/zinc",
3
- "version": "1.1.150",
3
+ "version": "1.1.152",
4
4
  "description": "A collection of web components for building web applications based off of @shoelace-style/Shoelace",
5
5
  "keywords": [
6
6
  "web components",
@@ -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 - they stay at the root of the payload.
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
- // Search-related fields (from <zn-data-table-search>'s `fields` slot, delivered via
378
- // requestParams) are wrapped under `searchFields` so the backend can bind them to a single
379
- // map rather than arbitrary root-level keys. `q` mirrors the search text; the root `search`
380
- // key is still sent for back-compatibility. Empty values are dropped, and `searchFields` is
381
- // null when nothing meaningful remains so the backend can treat it as "no search".
382
- const searchFields: Record<string, any> = {};
383
- if (requestParams && typeof requestParams === 'object') {
384
- for (const [key, value] of Object.entries(requestParams as Record<string, unknown>)) {
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 (searchFields) - their default is empty, so setParam writes them only when set.
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 are nested under `searchFields` in the POST body, with `q`
327
- // mirroring the search text, while the root `search` key is retained for back-compatibility.
328
- describe('searchFields request wrapping', () => {
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('wraps search-component field values under searchFields with q mirroring the search text', async () => {
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" headers='{"name": {"key": "name", "label": "Name"}}'></zn-data-table>`);
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, not inside searchFields', async () => {
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" headers='{"name": {"key": "name", "label": "Name"}}'>
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-x-small);
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;