@bexis2/bexis2-core-ui 0.4.21 → 0.4.23

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/README.md CHANGED
@@ -1,4 +1,18 @@
1
1
  # bexis-core-ui
2
+ ## 0.4.23
3
+ - Table
4
+ - fix resizing issues after page size or page index changes
5
+ - Add Select All and Deselect All in columns menu
6
+ - Convert JSON to CSV and export as CSV
7
+ - Export as JSON to fix special characters and encoding issues
8
+
9
+ ## 0.4.22
10
+ - Facets
11
+ - Replace column class function with more efficient solution
12
+ - Sort options in ShowMore alphabetically
13
+ Table
14
+ - Add titles for components for better accessibility
15
+ - Remove z-index from pagination buttons
2
16
 
3
17
  ## 0.4.21
4
18
  - change footer position in page component
@@ -1,7 +1,7 @@
1
1
  <script>export let group;
2
2
  export let handleApply;
3
3
  export let handleCancel;
4
- let selected = structuredClone(group.children);
4
+ let selected = Object.keys(group.children).sort((a, b) => group.children[a].displayName.localeCompare(group.children[b].displayName)).map((key) => ({ [key]: { ...group.children[key] } })).reduce((acc, val) => ({ ...acc, ...val }), {});
5
5
  const selectAll = () => {
6
6
  Object.keys(selected).forEach((key) => selected[key].selected = true);
7
7
  };
@@ -15,64 +15,44 @@ const onApply = () => {
15
15
  });
16
16
  };
17
17
  const onCancel = () => {
18
- console.log(selected, group.children);
19
18
  selected = structuredClone(group.children);
20
19
  handleCancel(group.name);
21
20
  };
22
- const gridClass = (items) => {
23
- const ceil = Math.ceil(Math.sqrt(items.length));
24
- const max = Math.max(ceil, Math.floor(items.length / 3));
25
- const classes = [
26
- "grid-rows-1",
27
- "grid-rows-2",
28
- "grid-rows-3",
29
- "grid-rows-4",
30
- "grid-rows-5",
31
- "grid-rows-6",
32
- "grid-rows-7",
33
- "grid-rows-8",
34
- "grid-rows-9",
35
- "grid-rows-10",
36
- "grid-rows-11",
37
- "grid-rows-12"
38
- ];
39
- if (max > 12) {
40
- return "grid-rows-12";
41
- } else return classes[max - 1 || 1];
42
- };
43
21
  </script>
44
22
 
45
- <div class="p-5 rounded-md max-w-6xl bg-surface-50 dark:bg-surface-800 border-primary-500 border-2">
46
- <!-- Header -->
47
- <h2 class="text-xl font-semibold">{group.displayName}</h2>
23
+ <div class="w-full flex justify-center max-w-[800px]">
24
+ <div class="grow max-h-[500px]">
25
+ <div
26
+ class="p-5 rounded-md w-full bg-surface-50 dark:bg-surface-800 border-primary-500 border-2"
27
+ >
28
+ <!-- Header -->
29
+ <h2 class="text-xl font-semibold">{group.displayName}</h2>
48
30
 
49
- <!-- Items -->
50
- <div
51
- class="{gridClass(
52
- Object.keys(selected)
53
- )} grid grid-flow-col gap-x-10 gap-y-2 py-10 px-2 h-full overflow-x-auto"
54
- >
55
- {#each Object.keys(selected) as key}
56
- <label class="flex gap-3 items-center w-48">
57
- <input type="checkbox" class="checkbox" bind:checked={selected[key].selected} />
58
- <span
59
- title={selected[key].displayName}
60
- class="whitespace-nowrap break-before-avoid break-after-avoid truncate"
61
- >{selected[key].displayName}</span
62
- >
63
- </label>
64
- {/each}
65
- </div>
31
+ <!-- Items -->
32
+ <div class="gap-x-10 space-y-2 py-6 px-[2px] max-h-[500px] columns-[192px] overflow-auto min-h">
33
+ {#each Object.keys(selected) as key}
34
+ <label class="flex gap-3 items-center w-48">
35
+ <input type="checkbox" class="checkbox" bind:checked={selected[key].selected} />
36
+ <span
37
+ title={selected[key].displayName}
38
+ class="whitespace-nowrap break-before-avoid break-after-avoid truncate"
39
+ >{selected[key].displayName}</span
40
+ >
41
+ </label>
42
+ {/each}
43
+ </div>
66
44
 
67
- <!-- Footer -->
68
- <div class="flex w-full justify-between gap-5">
69
- <div class="flex gap-3">
70
- <button class="btn btn-sm variant-filled-tertiary" on:click={selectNone}>None</button>
71
- <button class="btn btn-sm variant-filled-tertiary" on:click={selectAll}>All</button>
72
- </div>
73
- <div class="flex gap-3">
74
- <button class="btn btn-sm variant-filled-primary" on:click={onApply}>Apply</button>
75
- <button class="btn btn-sm variant-filled-secondary" on:click={onCancel}>Cancel</button>
45
+ <!-- Footer -->
46
+ <div class="flex w-full justify-between gap-5">
47
+ <div class="flex gap-3">
48
+ <button class="btn btn-sm variant-filled-tertiary" on:click={selectNone}>None</button>
49
+ <button class="btn btn-sm variant-filled-tertiary" on:click={selectAll}>All</button>
50
+ </div>
51
+ <div class="flex gap-3">
52
+ <button class="btn btn-sm variant-filled-primary" on:click={onApply}>Apply</button>
53
+ <button class="btn btn-sm variant-filled-secondary" on:click={onCancel}>Cancel</button>
54
+ </div>
55
+ </div>
76
56
  </div>
77
57
  </div>
78
58
  </div>
@@ -1,38 +1,65 @@
1
- <script>import { popup } from "@skeletonlabs/skeleton";
1
+ <script>import Fa from "svelte-fa";
2
+ import { faEye } from "@fortawesome/free-solid-svg-icons";
3
+ import { popup } from "@skeletonlabs/skeleton";
2
4
  export let columns = [];
3
5
  export let tableId;
4
6
  const popupCombobox = {
5
7
  event: "click",
6
8
  target: `${tableId}-columns-menu`,
7
- placement: "bottom"
9
+ placement: "bottom",
10
+ closeQuery: ""
11
+ };
12
+ const selectAll = () => {
13
+ columns = columns.map((column) => ({ ...column, visible: true }));
14
+ };
15
+ const deselectAll = () => {
16
+ columns = columns.map((column) => ({ ...column, visible: false }));
17
+ columns[0].visible = true;
8
18
  };
9
19
  </script>
10
20
 
11
21
  <button
12
22
  type="button"
13
- title="Hide or show columns"
14
- class="btn btn-sm variant-filled-primary rounded-full order-last"
23
+ class="btn btn-sm variant-filled-primary rounded-full order-last gap-2"
15
24
  aria-label="Open menu to hide/show columns"
16
- use:popup={popupCombobox}>Columns</button
25
+ use:popup={popupCombobox}><Fa icon={faEye} /> Columns</button
17
26
  >
18
-
19
27
  <div
20
- class="bg-white dark:bg-surface-500 p-4 rounded-md shadow-md z-10"
28
+ class="bg-white dark:bg-surface-500 p-4 px-5 rounded-md shadow-md z-10 border border-primary-500"
21
29
  data-popup="{tableId}-columns-menu"
22
30
  >
31
+ <div class="flex items-center gap-4 pb-5 grow justify-between">
32
+ <button
33
+ on:click|preventDefault={selectAll}
34
+ type="button"
35
+ class="btn p-0 text-sm grow underline text-primary-600"
36
+ >
37
+ Select All
38
+ </button>
39
+ <div class="border border-r border-neutral-200 h-6" />
40
+ <button
41
+ on:click|preventDefault={deselectAll}
42
+ type="button"
43
+ class="btn p-0 text-sm grow underline text-neutral-500"
44
+ >
45
+ Deselect All
46
+ </button>
47
+ </div>
23
48
  {#each columns as column}
24
49
  <div class="flex gap-3 items-center">
25
50
  <label for={column.id} class="cursor-pointer" title={column.label}></label>
26
51
  <input
27
- aria-label="Toggle column visibility for column {column.label}"
52
+ aria-label={`${column.visible ? 'Hide' : 'Show'} ${column.label} column`}
28
53
  type="checkbox"
29
- id = {column.id}
54
+ class="checkbox"
55
+ id={column.id}
30
56
  bind:checked={column.visible}
57
+ title={`${column.visible ? 'Hide' : 'Show'} ${column.label} column`}
31
58
  disabled={columns.filter((c) => c.visible).length === 1 && column.visible}
32
59
  />
33
60
  <span>{column.label}</span>
34
61
  </div>
35
62
  {/each}
36
63
 
37
- <div class="arrow bg-white dark:bg-surface-500" />
64
+ <div class="arrow bg-white dark:bg-surface-500 border-l border-t border-primary-500" />
38
65
  </div>
@@ -1,7 +1,7 @@
1
- <script>import { createEventDispatcher } from "svelte";
1
+ <script>import { afterUpdate, onDestroy, createEventDispatcher } from "svelte";
2
2
  import { readable, writable } from "svelte/store";
3
3
  import Fa from "svelte-fa";
4
- import { faXmark } from "@fortawesome/free-solid-svg-icons";
4
+ import { faCompress, faDownload, faXmark } from "@fortawesome/free-solid-svg-icons";
5
5
  import { createTable, Subscribe, Render, createRender } from "svelte-headless-table";
6
6
  import {
7
7
  addSortBy,
@@ -25,10 +25,12 @@ import { columnFilter, searchFilter } from "./filter";
25
25
  import {
26
26
  cellStyle,
27
27
  exportAsCsv,
28
+ jsonToCsv,
28
29
  fixedWidth,
29
30
  normalizeFilters,
30
31
  resetResize,
31
- convertServerColumns
32
+ convertServerColumns,
33
+ minWidth
32
34
  } from "./shared";
33
35
  import { Receive, Send } from "../../models/Models";
34
36
  export let config;
@@ -63,11 +65,14 @@ let {
63
65
  } = config;
64
66
  let searchValue = "";
65
67
  let isFetching = false;
68
+ let tableRef;
66
69
  const serverSide = server !== void 0;
67
70
  const { baseUrl, entityId, versionId, sendModel = new Send() } = server ?? {};
68
71
  const filters = writable({});
69
72
  const dispatch = createEventDispatcher();
70
73
  const actionDispatcher = (obj) => dispatch("action", obj);
74
+ const rowHeights = writable({});
75
+ const colWidths = writable([]);
71
76
  const serverItems = serverSide ? writable(0) : void 0;
72
77
  const serverItemCount = serverSide ? readable(0, (set) => {
73
78
  serverItems.subscribe((val) => set(val));
@@ -89,7 +94,7 @@ const table = createTable(data, {
89
94
  serverItemCount
90
95
  }),
91
96
  expand: addExpandedRows(),
92
- export: addDataExport({ format: "csv" })
97
+ export: addDataExport({ format: "json" })
93
98
  });
94
99
  const allCols = {};
95
100
  $data.forEach((item) => {
@@ -307,6 +312,94 @@ const sortServer = (order, id) => {
307
312
  $pageIndex = 0;
308
313
  updateTable();
309
314
  };
315
+ const getMaxCellHeightInRow = () => {
316
+ if (!tableRef || resizable === "columns" || resizable === "none") return;
317
+ tableRef.querySelectorAll("tbody tr").forEach((row, index) => {
318
+ const cells = row.querySelectorAll("td");
319
+ let maxHeight = optionsComponent ? 56 : 44;
320
+ let minHeight = optionsComponent ? 56 : 44;
321
+ cells.forEach((cell) => {
322
+ const cellHeight = cell.getBoundingClientRect().height;
323
+ if (cellHeight > maxHeight) {
324
+ maxHeight = cellHeight + 2;
325
+ }
326
+ if (cellHeight < minHeight) {
327
+ minHeight = cellHeight + 2;
328
+ }
329
+ });
330
+ rowHeights.update((rh) => {
331
+ const id = +row.id.split(`${tableId}-row-`)[1];
332
+ return {
333
+ ...rh,
334
+ [id]: {
335
+ max: maxHeight - 24,
336
+ min: Math.max(minHeight - 24, rowHeight ?? 20)
337
+ }
338
+ };
339
+ });
340
+ });
341
+ };
342
+ const getMinCellWidthInColumn = () => {
343
+ if (!tableRef || resizable === "rows" || resizable === "none") return;
344
+ if ($colWidths.length === 0) {
345
+ $colWidths = Array.from({ length: $headerRows[0].cells.length }, () => 100);
346
+ }
347
+ colWidths.update((cw) => {
348
+ tableRef?.querySelectorAll("thead tr th span").forEach((cell, index) => {
349
+ cw[index] = cw[index] === 100 ? cell.getBoundingClientRect().width + 12 + 32 : cw[index];
350
+ });
351
+ return cw;
352
+ });
353
+ };
354
+ const resizeRowsObserver = new ResizeObserver(() => {
355
+ getMaxCellHeightInRow();
356
+ });
357
+ const resizeColumnsObserver = new ResizeObserver(() => {
358
+ getMinCellWidthInColumn();
359
+ });
360
+ const observeFirstCells = () => {
361
+ if (!tableRef) return;
362
+ $pageRows.forEach((row) => {
363
+ const cell = tableRef.querySelector(`#${tableId}-row-${row.id}`);
364
+ if (cell) {
365
+ resizeRowsObserver.observe(cell);
366
+ }
367
+ });
368
+ tableRef.querySelectorAll("tbody tr td:first-child").forEach((cell) => {
369
+ resizeRowsObserver.observe(cell);
370
+ });
371
+ };
372
+ const observeHeaderColumns = () => {
373
+ if (!tableRef) return;
374
+ tableRef.querySelectorAll("thead tr th").forEach((cell) => {
375
+ resizeColumnsObserver.observe(cell);
376
+ });
377
+ };
378
+ afterUpdate(() => {
379
+ if (resizable !== "rows" && resizable !== "both") {
380
+ return;
381
+ }
382
+ const e = tableRef?.querySelector(`#${tableId}-row-${$pageRows[0].id}`);
383
+ if (e) {
384
+ getDimensions();
385
+ }
386
+ });
387
+ onDestroy(() => {
388
+ resizeRowsObserver.disconnect();
389
+ resizeColumnsObserver.disconnect();
390
+ });
391
+ const getDimensions = () => {
392
+ if (!tableRef) return;
393
+ if (resizable === "none") return;
394
+ else if (resizable === "columns") {
395
+ observeHeaderColumns();
396
+ } else if (resizable === "rows") {
397
+ observeFirstCells();
398
+ } else {
399
+ observeHeaderColumns();
400
+ observeFirstCells();
401
+ }
402
+ };
310
403
  $: sortKeys = pluginStates.sort.sortKeys;
311
404
  $: serverSide && updateTable();
312
405
  $: serverSide && sortServer($sortKeys[0]?.order, $sortKeys[0]?.id);
@@ -337,6 +430,7 @@ $: $hiddenColumnIds = shownColumns.filter((col) => !col.visible).map((col) => co
337
430
  title="Search within all table rows"
338
431
  bind:value={searchValue}
339
432
  placeholder="Search rows..."
433
+ aria-label="Searchbox for searching rows"
340
434
  id="{tableId}-search"
341
435
  /><button
342
436
  type="reset"
@@ -361,6 +455,7 @@ $: $hiddenColumnIds = shownColumns.filter((col) => !col.visible).map((col) => co
361
455
  title="Search"
362
456
  id="{tableId}-searchSubmit"
363
457
  class="btn variant-filled-primary"
458
+ aria-label="Search"
364
459
  on:click|preventDefault={() => {
365
460
  if (serverSide && !sendModel) {
366
461
  throw new Error('Server-side configuration is missing');
@@ -375,7 +470,8 @@ $: $hiddenColumnIds = shownColumns.filter((col) => !col.visible).map((col) => co
375
470
  {/if}
376
471
 
377
472
  <div
378
- class="flex justify-between items-center w-full {search && 'py-2'} {!search &&
473
+ class="flex justify-between overflow-x-auto items-center w-full {search &&
474
+ 'py-2'} {!search &&
379
475
  (shownColumns.length > 0 || toggle || resizable !== 'none' || exportable) &&
380
476
  'pb-2'}"
381
477
  >
@@ -389,6 +485,10 @@ $: $hiddenColumnIds = shownColumns.filter((col) => !col.visible).map((col) => co
389
485
  size="sm"
390
486
  checked={fitToScreen}
391
487
  id="{tableId}-toggle"
488
+ title={fitToScreen ? 'Fit table data to screen' : `Don't fit table data to screen`}
489
+ aria-label={fitToScreen
490
+ ? 'Fit table data to screen'
491
+ : `Don't fit table data to screen`}
392
492
  on:change={() => (fitToScreen = !fitToScreen)}>Fit to screen</SlideToggle
393
493
  >
394
494
  {/if}
@@ -398,20 +498,20 @@ $: $hiddenColumnIds = shownColumns.filter((col) => !col.visible).map((col) => co
398
498
  {#if resizable !== 'none'}
399
499
  <button
400
500
  type="button"
401
- title="Reset column and row sizing"
402
- class="btn btn-sm variant-filled-primary rounded-full order-last"
501
+ class="btn btn-sm variant-filled-primary rounded-full order-last flex gap-2 items-center"
502
+ aria-label="Reset sizing of columns and rows"
403
503
  on:click|preventDefault={() =>
404
504
  resetResize($headerRows, $pageRows, tableId, columns, resizable)}
405
- >Reset sizing</button
505
+ ><Fa icon={faCompress} /> Reset sizing</button
406
506
  >
407
507
  {/if}
408
508
  {#if exportable}
409
509
  <button
410
510
  type="button"
411
- title="Export table data as CSV"
412
- class="btn btn-sm variant-filled-primary rounded-full order-last"
413
- on:click|preventDefault={() => exportAsCsv(tableId, $exportedData)}
414
- >Export as CSV</button
511
+ class="btn btn-sm variant-filled-primary rounded-full order-last flex items-center gap-2"
512
+ aria-label="Export table data as CSV"
513
+ on:click|preventDefault={() => exportAsCsv(tableId, jsonToCsv($exportedData))}
514
+ ><Fa icon={faDownload} /> Export as CSV</button
415
515
  >
416
516
  {/if}
417
517
  {#if shownColumns.length > 0}
@@ -422,6 +522,7 @@ $: $hiddenColumnIds = shownColumns.filter((col) => !col.visible).map((col) => co
422
522
 
423
523
  <div class="overflow-auto" style="height: {height}px">
424
524
  <table
525
+ bind:this={tableRef}
425
526
  {...$tableAttrs}
426
527
  class="table table-auto table-compact bg-tertiary-500/30 dark:bg-tertiary-900/10 overflow-clip"
427
528
  id="{tableId}-table"
@@ -438,14 +539,25 @@ $: $hiddenColumnIds = shownColumns.filter((col) => !col.visible).map((col) => co
438
539
  let:rowProps
439
540
  >
440
541
  <tr {...rowAttrs} class="bg-primary-300 dark:bg-primary-800">
441
- {#each headerRow.cells as cell (cell.id)}
542
+ {#each headerRow.cells as cell, index (cell.id)}
442
543
  <Subscribe attrs={cell.attrs()} props={cell.props()} let:props let:attrs>
443
- <th scope="col" class="!p-2" {...attrs} style={cellStyle(cell.id, columns)}>
544
+ <th
545
+ scope="col"
546
+ class="!p-2"
547
+ {...attrs}
548
+ style={`
549
+ width: ${cell.isData() ? 'auto' : '0'};
550
+ ${cellStyle(cell.id, columns)}
551
+ `}
552
+ >
444
553
  <div
445
554
  class="overflow-auto"
446
555
  class:resize-x={(resizable === 'columns' || resizable === 'both') &&
447
556
  !fixedWidth(cell.id, columns)}
448
557
  id="th-{tableId}-{cell.id}"
558
+ style={`
559
+ min-width: ${minWidth(cell.id, columns) ? minWidth(cell.id, columns) : $colWidths[index]}px;
560
+ `}
449
561
  >
450
562
  <div class="flex justify-between items-center">
451
563
  <div class="flex gap-1 whitespace-pre-wrap">
@@ -458,6 +570,11 @@ $: $hiddenColumnIds = shownColumns.filter((col) => !col.visible).map((col) => co
458
570
  class:cursor-pointer={!props.sort.disabled}
459
571
  on:click={props.sort.toggle}
460
572
  on:keydown={props.sort.toggle}
573
+ title={props.sort.order === 'asc'
574
+ ? `Sort by ${cell.label} column in descending order`
575
+ : props.sort.order === 'desc'
576
+ ? `Remove sorting by ${cell.label} column`
577
+ : `Sort by ${cell.label} column in ascending order`}
461
578
  >
462
579
  {cell.render()}
463
580
  </span>
@@ -491,20 +608,42 @@ $: $hiddenColumnIds = shownColumns.filter((col) => !col.visible).map((col) => co
491
608
  <tr {...rowAttrs} id="{tableId}-row-{row.id}" class="">
492
609
  {#each row.cells as cell, index (cell?.id)}
493
610
  <Subscribe attrs={cell.attrs()} let:attrs>
494
- <td {...attrs} class="!p-2">
611
+ <td {...attrs} class="">
495
612
  <div
496
- class=" overflow-auto h-max {index === 0 &&
613
+ class=" h-full {index === 0 &&
497
614
  (resizable === 'rows' || resizable === 'both')
498
- ? 'resize-y'
499
- : ''}"
615
+ ? 'resize-y overflow-auto'
616
+ : 'block'}"
500
617
  id="{tableId}-{cell.id}-{row.id}"
618
+ style={`
619
+ min-height: ${$rowHeights && $rowHeights[+row.id] ? `${$rowHeights[+row.id].min}px` : 'auto'};
620
+ max-height: ${
621
+ index !== 0 && $rowHeights && $rowHeights[+row.id]
622
+ ? `${$rowHeights[+row.id].max}px`
623
+ : 'auto'
624
+ };
625
+ height: ${$rowHeights && $rowHeights[+row.id] ? `${$rowHeights[+row.id].min}px` : 'auto'};
626
+ `}
501
627
  >
502
628
  <!-- Adding config for initial rowHeight, if provided -->
503
629
  <div
504
- class="flex items-center overflow-auto"
505
- style="height: {rowHeight ? `${rowHeight}px` : 'auto'};"
630
+ class="flex items-start overflow-auto"
631
+ style={`
632
+ max-height: ${$rowHeights && $rowHeights[+row.id] ? `${$rowHeights[+row.id].max}px` : 'auto'};
633
+ `}
506
634
  >
507
- <div class="grow h-full"><Render of={cell.render()} /></div>
635
+ <div
636
+ class="grow overflow-auto"
637
+ style={cell.isData()
638
+ ? `width: ${
639
+ minWidth(cell.id, columns)
640
+ ? minWidth(cell.id, columns)
641
+ : $colWidths[index]
642
+ }px;`
643
+ : 'max-width: min-content;'}
644
+ >
645
+ <Render of={cell.render()} />
646
+ </div>
508
647
  </div>
509
648
  </div>
510
649
  </td>
@@ -201,7 +201,7 @@ onMount(() => {
201
201
  type="button"
202
202
  use:popup={popupFeatured}
203
203
  id="{popupId}-button"
204
- aria-label="Open filter menu for column {id}"
204
+ aria-label="Open filter menu for {id} column"
205
205
  >
206
206
  <Fa icon={faFilter} size="12" />
207
207
  </button>
@@ -39,7 +39,7 @@ $: goToPreviousPageDisabled = !$hasPreviousPage;
39
39
  $: $pageSize = pageSizeDropdownValue;
40
40
  </script>
41
41
 
42
- <div class="flex justify-between w-full items-stretch gap-10 z-50">
42
+ <div class="flex justify-between w-full items-stretch gap-10">
43
43
  <div class="flex justify-start">
44
44
  <!-- <select
45
45
  name="pageSize"
@@ -53,7 +53,7 @@ $: $pageSize = pageSizeDropdownValue;
53
53
  </select> -->
54
54
 
55
55
  <button
56
- aria-label="Open menu to select number of items per page"
56
+ aria-label="Open menu to select number of items to display per page"
57
57
  class="btn variant-filled-primary w-20 justify-between"
58
58
  use:popup={pageSizePopup}
59
59
  >
@@ -7,6 +7,7 @@ export declare const normalizeFilters: (filters: {
7
7
  [key: string]: { [key in FilterOptionsEnum]?: number | string | Date; };
8
8
  }) => Filter[];
9
9
  export declare const exportAsCsv: (tableId: string, exportedData: string) => void;
10
+ export declare const jsonToCsv: (data: string) => string;
10
11
  export declare const resetResize: (headerRows: any, pageRows: any, tableId: string, columns: Columns | undefined, resizable: "none" | "rows" | "columns" | "both") => void;
11
12
  export declare const missingValuesFn: (key: number | string, missingValues: {
12
13
  [key: string | number]: string;
@@ -52,6 +52,31 @@ export const exportAsCsv = (tableId, exportedData) => {
52
52
  anchor.click();
53
53
  document.body.removeChild(anchor);
54
54
  };
55
+ export const jsonToCsv = (data) => {
56
+ const json = JSON.parse(data);
57
+ if (json.length === 0)
58
+ return '';
59
+ // Extract headers (keys)
60
+ const headers = Object.keys(json[0]);
61
+ // Escape and format a single cell
62
+ const escapeCsvCell = (value) => {
63
+ if (value === null || value === undefined)
64
+ return '';
65
+ let cell = String(value);
66
+ // Escape quotes by doubling them, and wrap the value in quotes if it contains special characters
67
+ if (/[",\n]/.test(cell)) {
68
+ cell = `"${cell.replace(/"/g, '""')}"`;
69
+ }
70
+ return cell;
71
+ };
72
+ // Create CSV rows
73
+ const rows = [
74
+ headers.join(','), // Header row
75
+ ...json.map((row) => headers.map(header => escapeCsvCell(row[header])).join(',')) // Data rows
76
+ ];
77
+ // Join rows with newlines
78
+ return rows.join('\n');
79
+ };
55
80
  // Resetting the resized columns and/or rows
56
81
  export const resetResize = (headerRows, pageRows, tableId, columns, resizable) => {
57
82
  // Run only if resizable is not none
@@ -71,7 +71,7 @@
71
71
  }
72
72
 
73
73
  if (!complexSource && !complexTarget && isLoaded && !isMulti) {
74
- console.log('🚀 ~ updateTarget ~ selection:', selection);
74
+ //console.log('🚀 ~ updateTarget ~ selection:', selection);
75
75
  if (selection) {
76
76
  target = selection.value;
77
77
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bexis2/bexis2-core-ui",
3
- "version": "0.4.21",
3
+ "version": "0.4.23",
4
4
  "private": false,
5
5
  "scripts": {
6
6
  "dev": "vite dev",
@@ -5,7 +5,10 @@
5
5
  export let handleApply: (group: SelectedFacetGroup) => {};
6
6
  export let handleCancel: (groupName: string) => {};
7
7
 
8
- let selected = structuredClone(group.children);
8
+ let selected = Object.keys(group.children)
9
+ .sort((a, b) => group.children[a].displayName.localeCompare(group.children[b].displayName))
10
+ .map((key) => ({ [key]: { ...group.children[key] } }))
11
+ .reduce((acc, val) => ({ ...acc, ...val }), {});
9
12
 
10
13
  const selectAll = () => {
11
14
  Object.keys(selected).forEach((key) => (selected[key].selected = true));
@@ -23,67 +26,44 @@
23
26
  };
24
27
 
25
28
  const onCancel = () => {
26
- console.log(selected, group.children);
27
29
  selected = structuredClone(group.children);
28
30
  handleCancel(group.name);
29
31
  };
30
-
31
- const gridClass = (items: any[]) => {
32
- const ceil = Math.ceil(Math.sqrt(items.length));
33
- const max = Math.max(ceil, Math.floor(items.length / 3));
34
-
35
- const classes = [
36
- 'grid-rows-1',
37
- 'grid-rows-2',
38
- 'grid-rows-3',
39
- 'grid-rows-4',
40
- 'grid-rows-5',
41
- 'grid-rows-6',
42
- 'grid-rows-7',
43
- 'grid-rows-8',
44
- 'grid-rows-9',
45
- 'grid-rows-10',
46
- 'grid-rows-11',
47
- 'grid-rows-12'
48
- ];
49
-
50
- if (max > 12) {
51
- return 'grid-rows-12';
52
- } else return classes[max - 1 || 1];
53
- };
54
32
  </script>
55
33
 
56
- <div class="p-5 rounded-md max-w-6xl bg-surface-50 dark:bg-surface-800 border-primary-500 border-2">
57
- <!-- Header -->
58
- <h2 class="text-xl font-semibold">{group.displayName}</h2>
34
+ <div class="w-full flex justify-center max-w-[800px]">
35
+ <div class="grow max-h-[500px]">
36
+ <div
37
+ class="p-5 rounded-md w-full bg-surface-50 dark:bg-surface-800 border-primary-500 border-2"
38
+ >
39
+ <!-- Header -->
40
+ <h2 class="text-xl font-semibold">{group.displayName}</h2>
59
41
 
60
- <!-- Items -->
61
- <div
62
- class="{gridClass(
63
- Object.keys(selected)
64
- )} grid grid-flow-col gap-x-10 gap-y-2 py-10 px-2 h-full overflow-x-auto"
65
- >
66
- {#each Object.keys(selected) as key}
67
- <label class="flex gap-3 items-center w-48">
68
- <input type="checkbox" class="checkbox" bind:checked={selected[key].selected} />
69
- <span
70
- title={selected[key].displayName}
71
- class="whitespace-nowrap break-before-avoid break-after-avoid truncate"
72
- >{selected[key].displayName}</span
73
- >
74
- </label>
75
- {/each}
76
- </div>
42
+ <!-- Items -->
43
+ <div class="gap-x-10 space-y-2 py-6 px-[2px] max-h-[500px] columns-[192px] overflow-auto min-h">
44
+ {#each Object.keys(selected) as key}
45
+ <label class="flex gap-3 items-center w-48">
46
+ <input type="checkbox" class="checkbox" bind:checked={selected[key].selected} />
47
+ <span
48
+ title={selected[key].displayName}
49
+ class="whitespace-nowrap break-before-avoid break-after-avoid truncate"
50
+ >{selected[key].displayName}</span
51
+ >
52
+ </label>
53
+ {/each}
54
+ </div>
77
55
 
78
- <!-- Footer -->
79
- <div class="flex w-full justify-between gap-5">
80
- <div class="flex gap-3">
81
- <button class="btn btn-sm variant-filled-tertiary" on:click={selectNone}>None</button>
82
- <button class="btn btn-sm variant-filled-tertiary" on:click={selectAll}>All</button>
83
- </div>
84
- <div class="flex gap-3">
85
- <button class="btn btn-sm variant-filled-primary" on:click={onApply}>Apply</button>
86
- <button class="btn btn-sm variant-filled-secondary" on:click={onCancel}>Cancel</button>
56
+ <!-- Footer -->
57
+ <div class="flex w-full justify-between gap-5">
58
+ <div class="flex gap-3">
59
+ <button class="btn btn-sm variant-filled-tertiary" on:click={selectNone}>None</button>
60
+ <button class="btn btn-sm variant-filled-tertiary" on:click={selectAll}>All</button>
61
+ </div>
62
+ <div class="flex gap-3">
63
+ <button class="btn btn-sm variant-filled-primary" on:click={onApply}>Apply</button>
64
+ <button class="btn btn-sm variant-filled-secondary" on:click={onCancel}>Cancel</button>
65
+ </div>
66
+ </div>
87
67
  </div>
88
68
  </div>
89
69
  </div>
@@ -1,4 +1,6 @@
1
1
  <script lang="ts">
2
+ import Fa from 'svelte-fa';
3
+ import { faEye } from '@fortawesome/free-solid-svg-icons';
2
4
  import { popup } from '@skeletonlabs/skeleton';
3
5
  import type { PopupSettings } from '@skeletonlabs/skeleton';
4
6
 
@@ -8,35 +10,62 @@
8
10
  const popupCombobox: PopupSettings = {
9
11
  event: 'click',
10
12
  target: `${tableId}-columns-menu`,
11
- placement: 'bottom'
13
+ placement: 'bottom',
14
+ closeQuery: ''
15
+ };
16
+
17
+ const selectAll = () => {
18
+ columns = columns.map((column) => ({ ...column, visible: true }));
19
+ };
20
+
21
+ const deselectAll = () => {
22
+ columns = columns.map((column) => ({ ...column, visible: false }));
23
+ columns[0].visible = true;
12
24
  };
13
25
  </script>
14
26
 
15
27
  <button
16
28
  type="button"
17
- title="Hide or show columns"
18
- class="btn btn-sm variant-filled-primary rounded-full order-last"
29
+ class="btn btn-sm variant-filled-primary rounded-full order-last gap-2"
19
30
  aria-label="Open menu to hide/show columns"
20
- use:popup={popupCombobox}>Columns</button
31
+ use:popup={popupCombobox}><Fa icon={faEye} /> Columns</button
21
32
  >
22
-
23
33
  <div
24
- class="bg-white dark:bg-surface-500 p-4 rounded-md shadow-md z-10"
34
+ class="bg-white dark:bg-surface-500 p-4 px-5 rounded-md shadow-md z-10 border border-primary-500"
25
35
  data-popup="{tableId}-columns-menu"
26
36
  >
37
+ <div class="flex items-center gap-4 pb-5 grow justify-between">
38
+ <button
39
+ on:click|preventDefault={selectAll}
40
+ type="button"
41
+ class="btn p-0 text-sm grow underline text-primary-600"
42
+ >
43
+ Select All
44
+ </button>
45
+ <div class="border border-r border-neutral-200 h-6" />
46
+ <button
47
+ on:click|preventDefault={deselectAll}
48
+ type="button"
49
+ class="btn p-0 text-sm grow underline text-neutral-500"
50
+ >
51
+ Deselect All
52
+ </button>
53
+ </div>
27
54
  {#each columns as column}
28
55
  <div class="flex gap-3 items-center">
29
56
  <label for={column.id} class="cursor-pointer" title={column.label}></label>
30
57
  <input
31
- aria-label="Toggle column visibility for column {column.label}"
58
+ aria-label={`${column.visible ? 'Hide' : 'Show'} ${column.label} column`}
32
59
  type="checkbox"
33
- id = {column.id}
60
+ class="checkbox"
61
+ id={column.id}
34
62
  bind:checked={column.visible}
63
+ title={`${column.visible ? 'Hide' : 'Show'} ${column.label} column`}
35
64
  disabled={columns.filter((c) => c.visible).length === 1 && column.visible}
36
65
  />
37
66
  <span>{column.label}</span>
38
67
  </div>
39
68
  {/each}
40
69
 
41
- <div class="arrow bg-white dark:bg-surface-500" />
70
+ <div class="arrow bg-white dark:bg-surface-500 border-l border-t border-primary-500" />
42
71
  </div>
@@ -1,9 +1,9 @@
1
1
  <script lang="ts">
2
- import { createEventDispatcher } from 'svelte';
2
+ import { afterUpdate, onDestroy, createEventDispatcher } from 'svelte';
3
3
  import { readable, writable } from 'svelte/store';
4
4
 
5
5
  import Fa from 'svelte-fa';
6
- import { faXmark } from '@fortawesome/free-solid-svg-icons';
6
+ import { faCompress, faDownload, faXmark } from '@fortawesome/free-solid-svg-icons';
7
7
  import { createTable, Subscribe, Render, createRender } from 'svelte-headless-table';
8
8
  import {
9
9
  addSortBy,
@@ -30,10 +30,12 @@
30
30
  import {
31
31
  cellStyle,
32
32
  exportAsCsv,
33
+ jsonToCsv,
33
34
  fixedWidth,
34
35
  normalizeFilters,
35
36
  resetResize,
36
- convertServerColumns
37
+ convertServerColumns,
38
+ minWidth
37
39
  } from './shared';
38
40
  import { Receive, Send } from '$models/Models';
39
41
  import type { TableConfig } from '$models/Models';
@@ -61,6 +63,8 @@
61
63
 
62
64
  let searchValue = '';
63
65
  let isFetching = false;
66
+ let tableRef: HTMLTableElement;
67
+
64
68
  const serverSide = server !== undefined;
65
69
  const { baseUrl, entityId, versionId, sendModel = new Send() } = server ?? {};
66
70
 
@@ -75,6 +79,11 @@
75
79
  const dispatch = createEventDispatcher();
76
80
  const actionDispatcher = (obj) => dispatch('action', obj);
77
81
 
82
+ // Stores to hold the width and height information for resizing
83
+ const rowHeights = writable<{ [key: number]: { max: number; min: number } }>({});
84
+ const colWidths = writable<number[]>([]);
85
+
86
+ // Server-side variables
78
87
  const serverItems = serverSide ? writable<Number>(0) : undefined;
79
88
  const serverItemCount = serverSide
80
89
  ? readable<Number>(0, (set) => {
@@ -100,7 +109,7 @@
100
109
  serverItemCount
101
110
  } as PaginationConfig),
102
111
  expand: addExpandedRows(),
103
- export: addDataExport({ format: 'csv' })
112
+ export: addDataExport({ format: 'json' })
104
113
  });
105
114
 
106
115
  // A variable to hold all the keys
@@ -375,6 +384,119 @@
375
384
  updateTable();
376
385
  };
377
386
 
387
+ const getMaxCellHeightInRow = () => {
388
+ if (!tableRef || resizable === 'columns' || resizable === 'none') return;
389
+
390
+ tableRef.querySelectorAll('tbody tr').forEach((row, index) => {
391
+ const cells = row.querySelectorAll('td');
392
+
393
+ let maxHeight = optionsComponent ? 56 : 44;
394
+ let minHeight = optionsComponent ? 56 : 44;
395
+
396
+ cells.forEach((cell) => {
397
+ const cellHeight = cell.getBoundingClientRect().height;
398
+ // + 2 pixels for rendering borders correctly
399
+ if (cellHeight > maxHeight) {
400
+ maxHeight = cellHeight + 2;
401
+ }
402
+ if (cellHeight < minHeight) {
403
+ minHeight = cellHeight + 2;
404
+ }
405
+ });
406
+
407
+ rowHeights.update((rh) => {
408
+ const id = +row.id.split(`${tableId}-row-`)[1];
409
+ return {
410
+ ...rh,
411
+ [id]: {
412
+ max: maxHeight - 24,
413
+ min: Math.max(minHeight - 24, rowHeight ?? 20)
414
+ }
415
+ };
416
+ });
417
+ });
418
+ };
419
+
420
+ const getMinCellWidthInColumn = () => {
421
+ if (!tableRef || resizable === 'rows' || resizable === 'none') return;
422
+
423
+ // Initialize the colWidths array if it is empty
424
+ if ($colWidths.length === 0) {
425
+ $colWidths = Array.from({ length: $headerRows[0].cells.length }, () => 100);
426
+ }
427
+
428
+ colWidths.update((cw) => {
429
+ tableRef?.querySelectorAll('thead tr th span').forEach((cell, index) => {
430
+ // + 12 pixels for padding and + 32 pixels for filter icon
431
+ // If the column width is 100, which means it has not been initialized, then calculate the width
432
+ cw[index] = cw[index] === 100 ? cell.getBoundingClientRect().width + 12 + 32 : cw[index];
433
+ });
434
+ return cw;
435
+ });
436
+ };
437
+
438
+ const resizeRowsObserver = new ResizeObserver(() => {
439
+ getMaxCellHeightInRow();
440
+ });
441
+
442
+ const resizeColumnsObserver = new ResizeObserver(() => {
443
+ getMinCellWidthInColumn();
444
+ });
445
+
446
+ const observeFirstCells = () => {
447
+ if (!tableRef) return;
448
+
449
+ $pageRows.forEach((row) => {
450
+ const cell = tableRef.querySelector(`#${tableId}-row-${row.id}`);
451
+ if (cell) {
452
+ resizeRowsObserver.observe(cell);
453
+ }
454
+ });
455
+
456
+ tableRef.querySelectorAll('tbody tr td:first-child').forEach((cell) => {
457
+ resizeRowsObserver.observe(cell);
458
+ });
459
+ };
460
+
461
+ const observeHeaderColumns = () => {
462
+ if (!tableRef) return;
463
+
464
+ tableRef.querySelectorAll('thead tr th').forEach((cell) => {
465
+ resizeColumnsObserver.observe(cell);
466
+ });
467
+ };
468
+
469
+ afterUpdate(() => {
470
+ if (resizable !== 'rows' && resizable !== 'both') {
471
+ return;
472
+ }
473
+ // Making sure tableRef is up to date and contains the new rows
474
+ // If it contains even one element, it means it contains them all
475
+ const e = tableRef?.querySelector(`#${tableId}-row-${$pageRows[0].id}`);
476
+ if (e) {
477
+ getDimensions();
478
+ }
479
+ });
480
+
481
+ // Remove the resize observer when the component is destroyed for performance reasons
482
+ onDestroy(() => {
483
+ resizeRowsObserver.disconnect();
484
+ resizeColumnsObserver.disconnect();
485
+ });
486
+
487
+ const getDimensions = () => {
488
+ if (!tableRef) return;
489
+ if (resizable === 'none') return;
490
+ else if (resizable === 'columns') {
491
+ observeHeaderColumns();
492
+ } else if (resizable === 'rows') {
493
+ observeFirstCells();
494
+ } else {
495
+ observeHeaderColumns();
496
+ observeFirstCells();
497
+ }
498
+ };
499
+
378
500
  $: sortKeys = pluginStates.sort.sortKeys;
379
501
  $: serverSide && updateTable();
380
502
  $: serverSide && sortServer($sortKeys[0]?.order, $sortKeys[0]?.id);
@@ -405,6 +527,7 @@
405
527
  title="Search within all table rows"
406
528
  bind:value={searchValue}
407
529
  placeholder="Search rows..."
530
+ aria-label="Searchbox for searching rows"
408
531
  id="{tableId}-search"
409
532
  /><button
410
533
  type="reset"
@@ -429,6 +552,7 @@
429
552
  title="Search"
430
553
  id="{tableId}-searchSubmit"
431
554
  class="btn variant-filled-primary"
555
+ aria-label="Search"
432
556
  on:click|preventDefault={() => {
433
557
  if (serverSide && !sendModel) {
434
558
  throw new Error('Server-side configuration is missing');
@@ -443,7 +567,8 @@
443
567
  {/if}
444
568
 
445
569
  <div
446
- class="flex justify-between items-center w-full {search && 'py-2'} {!search &&
570
+ class="flex justify-between overflow-x-auto items-center w-full {search &&
571
+ 'py-2'} {!search &&
447
572
  (shownColumns.length > 0 || toggle || resizable !== 'none' || exportable) &&
448
573
  'pb-2'}"
449
574
  >
@@ -457,6 +582,10 @@
457
582
  size="sm"
458
583
  checked={fitToScreen}
459
584
  id="{tableId}-toggle"
585
+ title={fitToScreen ? 'Fit table data to screen' : `Don't fit table data to screen`}
586
+ aria-label={fitToScreen
587
+ ? 'Fit table data to screen'
588
+ : `Don't fit table data to screen`}
460
589
  on:change={() => (fitToScreen = !fitToScreen)}>Fit to screen</SlideToggle
461
590
  >
462
591
  {/if}
@@ -466,20 +595,20 @@
466
595
  {#if resizable !== 'none'}
467
596
  <button
468
597
  type="button"
469
- title="Reset column and row sizing"
470
- class="btn btn-sm variant-filled-primary rounded-full order-last"
598
+ class="btn btn-sm variant-filled-primary rounded-full order-last flex gap-2 items-center"
599
+ aria-label="Reset sizing of columns and rows"
471
600
  on:click|preventDefault={() =>
472
601
  resetResize($headerRows, $pageRows, tableId, columns, resizable)}
473
- >Reset sizing</button
602
+ ><Fa icon={faCompress} /> Reset sizing</button
474
603
  >
475
604
  {/if}
476
605
  {#if exportable}
477
606
  <button
478
607
  type="button"
479
- title="Export table data as CSV"
480
- class="btn btn-sm variant-filled-primary rounded-full order-last"
481
- on:click|preventDefault={() => exportAsCsv(tableId, $exportedData)}
482
- >Export as CSV</button
608
+ class="btn btn-sm variant-filled-primary rounded-full order-last flex items-center gap-2"
609
+ aria-label="Export table data as CSV"
610
+ on:click|preventDefault={() => exportAsCsv(tableId, jsonToCsv($exportedData))}
611
+ ><Fa icon={faDownload} /> Export as CSV</button
483
612
  >
484
613
  {/if}
485
614
  {#if shownColumns.length > 0}
@@ -490,6 +619,7 @@
490
619
 
491
620
  <div class="overflow-auto" style="height: {height}px">
492
621
  <table
622
+ bind:this={tableRef}
493
623
  {...$tableAttrs}
494
624
  class="table table-auto table-compact bg-tertiary-500/30 dark:bg-tertiary-900/10 overflow-clip"
495
625
  id="{tableId}-table"
@@ -506,14 +636,25 @@
506
636
  let:rowProps
507
637
  >
508
638
  <tr {...rowAttrs} class="bg-primary-300 dark:bg-primary-800">
509
- {#each headerRow.cells as cell (cell.id)}
639
+ {#each headerRow.cells as cell, index (cell.id)}
510
640
  <Subscribe attrs={cell.attrs()} props={cell.props()} let:props let:attrs>
511
- <th scope="col" class="!p-2" {...attrs} style={cellStyle(cell.id, columns)}>
641
+ <th
642
+ scope="col"
643
+ class="!p-2"
644
+ {...attrs}
645
+ style={`
646
+ width: ${cell.isData() ? 'auto' : '0'};
647
+ ${cellStyle(cell.id, columns)}
648
+ `}
649
+ >
512
650
  <div
513
651
  class="overflow-auto"
514
652
  class:resize-x={(resizable === 'columns' || resizable === 'both') &&
515
653
  !fixedWidth(cell.id, columns)}
516
654
  id="th-{tableId}-{cell.id}"
655
+ style={`
656
+ min-width: ${minWidth(cell.id, columns) ? minWidth(cell.id, columns) : $colWidths[index]}px;
657
+ `}
517
658
  >
518
659
  <div class="flex justify-between items-center">
519
660
  <div class="flex gap-1 whitespace-pre-wrap">
@@ -526,6 +667,11 @@
526
667
  class:cursor-pointer={!props.sort.disabled}
527
668
  on:click={props.sort.toggle}
528
669
  on:keydown={props.sort.toggle}
670
+ title={props.sort.order === 'asc'
671
+ ? `Sort by ${cell.label} column in descending order`
672
+ : props.sort.order === 'desc'
673
+ ? `Remove sorting by ${cell.label} column`
674
+ : `Sort by ${cell.label} column in ascending order`}
529
675
  >
530
676
  {cell.render()}
531
677
  </span>
@@ -559,20 +705,42 @@
559
705
  <tr {...rowAttrs} id="{tableId}-row-{row.id}" class="">
560
706
  {#each row.cells as cell, index (cell?.id)}
561
707
  <Subscribe attrs={cell.attrs()} let:attrs>
562
- <td {...attrs} class="!p-2">
708
+ <td {...attrs} class="">
563
709
  <div
564
- class=" overflow-auto h-max {index === 0 &&
710
+ class=" h-full {index === 0 &&
565
711
  (resizable === 'rows' || resizable === 'both')
566
- ? 'resize-y'
567
- : ''}"
712
+ ? 'resize-y overflow-auto'
713
+ : 'block'}"
568
714
  id="{tableId}-{cell.id}-{row.id}"
715
+ style={`
716
+ min-height: ${$rowHeights && $rowHeights[+row.id] ? `${$rowHeights[+row.id].min}px` : 'auto'};
717
+ max-height: ${
718
+ index !== 0 && $rowHeights && $rowHeights[+row.id]
719
+ ? `${$rowHeights[+row.id].max}px`
720
+ : 'auto'
721
+ };
722
+ height: ${$rowHeights && $rowHeights[+row.id] ? `${$rowHeights[+row.id].min}px` : 'auto'};
723
+ `}
569
724
  >
570
725
  <!-- Adding config for initial rowHeight, if provided -->
571
726
  <div
572
- class="flex items-center overflow-auto"
573
- style="height: {rowHeight ? `${rowHeight}px` : 'auto'};"
727
+ class="flex items-start overflow-auto"
728
+ style={`
729
+ max-height: ${$rowHeights && $rowHeights[+row.id] ? `${$rowHeights[+row.id].max}px` : 'auto'};
730
+ `}
574
731
  >
575
- <div class="grow h-full"><Render of={cell.render()} /></div>
732
+ <div
733
+ class="grow overflow-auto"
734
+ style={cell.isData()
735
+ ? `width: ${
736
+ minWidth(cell.id, columns)
737
+ ? minWidth(cell.id, columns)
738
+ : $colWidths[index]
739
+ }px;`
740
+ : 'max-width: min-content;'}
741
+ >
742
+ <Render of={cell.render()} />
743
+ </div>
576
744
  </div>
577
745
  </div>
578
746
  </td>
@@ -240,7 +240,7 @@
240
240
  type="button"
241
241
  use:popup={popupFeatured}
242
242
  id="{popupId}-button"
243
- aria-label="Open filter menu for column {id}"
243
+ aria-label="Open filter menu for {id} column"
244
244
  >
245
245
  <Fa icon={faFilter} size="12" />
246
246
  </button>
@@ -49,7 +49,7 @@
49
49
  $: $pageSize = pageSizeDropdownValue;
50
50
  </script>
51
51
 
52
- <div class="flex justify-between w-full items-stretch gap-10 z-50">
52
+ <div class="flex justify-between w-full items-stretch gap-10">
53
53
  <div class="flex justify-start">
54
54
  <!-- <select
55
55
  name="pageSize"
@@ -63,7 +63,7 @@
63
63
  </select> -->
64
64
 
65
65
  <button
66
- aria-label="Open menu to select number of items per page"
66
+ aria-label="Open menu to select number of items to display per page"
67
67
  class="btn variant-filled-primary w-20 justify-between"
68
68
  use:popup={pageSizePopup}
69
69
  >
@@ -63,6 +63,37 @@ export const exportAsCsv = (tableId: string, exportedData: string) => {
63
63
  document.body.removeChild(anchor);
64
64
  };
65
65
 
66
+ export const jsonToCsv = (data: string): string => {
67
+ const json = JSON.parse(data);
68
+
69
+ if (json.length === 0) return '';
70
+
71
+ // Extract headers (keys)
72
+ const headers = Object.keys(json[0]);
73
+
74
+ // Escape and format a single cell
75
+ const escapeCsvCell = (value: any): string => {
76
+ if (value === null || value === undefined) return '';
77
+ let cell = String(value);
78
+ // Escape quotes by doubling them, and wrap the value in quotes if it contains special characters
79
+ if (/[",\n]/.test(cell)) {
80
+ cell = `"${cell.replace(/"/g, '""')}"`;
81
+ }
82
+ return cell;
83
+ };
84
+
85
+ // Create CSV rows
86
+ const rows = [
87
+ headers.join(','), // Header row
88
+ ...json.map((row) =>
89
+ headers.map(header => escapeCsvCell(row[header])).join(',')
90
+ ) // Data rows
91
+ ];
92
+
93
+ // Join rows with newlines
94
+ return rows.join('\n');
95
+ }
96
+
66
97
  // Resetting the resized columns and/or rows
67
98
  export const resetResize = (
68
99
  headerRows: any,
@@ -114,15 +145,15 @@ export const missingValuesFn = (
114
145
  const foundKey =
115
146
  typeof key === 'number' && key.toString().includes('e')
116
147
  ? Object.keys(missingValues).find((item) => {
117
- return (item as string).toLowerCase() === key.toString().toLowerCase();
118
- })
148
+ return (item as string).toLowerCase() === key.toString().toLowerCase();
149
+ })
119
150
  : typeof key === 'string' && parseInt(key).toString().length !== key.length && new Date(key)
120
- ? Object.keys(missingValues).find(
151
+ ? Object.keys(missingValues).find(
121
152
  (item) => new Date(item).getTime() === new Date(key).getTime()
122
- )
123
- : key in missingValues
124
- ? key
125
- : undefined;
153
+ )
154
+ : key in missingValues
155
+ ? key
156
+ : undefined;
126
157
 
127
158
  return foundKey ? missingValues[foundKey] : key;
128
159
  };
@@ -71,7 +71,7 @@
71
71
  }
72
72
 
73
73
  if (!complexSource && !complexTarget && isLoaded && !isMulti) {
74
- console.log('🚀 ~ updateTarget ~ selection:', selection);
74
+ //console.log('🚀 ~ updateTarget ~ selection:', selection);
75
75
  if (selection) {
76
76
  target = selection.value;
77
77
  }