@bexis2/bexis2-core-ui 0.4.22 → 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 +7 -0
- package/dist/components/Table/ColumnsMenu.svelte +35 -9
- package/dist/components/Table/TableContent.svelte +147 -21
- package/dist/components/Table/shared.d.ts +1 -0
- package/dist/components/Table/shared.js +25 -0
- package/dist/components/form/MultiSelect.svelte +1 -1
- package/package.json +1 -1
- package/src/lib/components/Table/ColumnsMenu.svelte +36 -8
- package/src/lib/components/Table/TableContent.svelte +176 -21
- package/src/lib/components/Table/shared.ts +38 -7
- package/src/lib/components/form/MultiSelect.svelte +1 -1
package/README.md
CHANGED
|
@@ -1,4 +1,11 @@
|
|
|
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
|
+
|
|
2
9
|
## 0.4.22
|
|
3
10
|
- Facets
|
|
4
11
|
- Replace column class function with more efficient solution
|
|
@@ -1,32 +1,58 @@
|
|
|
1
|
-
<script>import
|
|
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
|
-
|
|
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}
|
|
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
52
|
aria-label={`${column.visible ? 'Hide' : 'Show'} ${column.label} column`}
|
|
28
53
|
type="checkbox"
|
|
29
|
-
|
|
54
|
+
class="checkbox"
|
|
55
|
+
id={column.id}
|
|
30
56
|
bind:checked={column.visible}
|
|
31
57
|
title={`${column.visible ? 'Hide' : 'Show'} ${column.label} column`}
|
|
32
58
|
disabled={columns.filter((c) => c.visible).length === 1 && column.visible}
|
|
@@ -35,5 +61,5 @@ const popupCombobox = {
|
|
|
35
61
|
</div>
|
|
36
62
|
{/each}
|
|
37
63
|
|
|
38
|
-
<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" />
|
|
39
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: "
|
|
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);
|
|
@@ -377,7 +470,8 @@ $: $hiddenColumnIds = shownColumns.filter((col) => !col.visible).map((col) => co
|
|
|
377
470
|
{/if}
|
|
378
471
|
|
|
379
472
|
<div
|
|
380
|
-
class="flex justify-between items-center w-full {search &&
|
|
473
|
+
class="flex justify-between overflow-x-auto items-center w-full {search &&
|
|
474
|
+
'py-2'} {!search &&
|
|
381
475
|
(shownColumns.length > 0 || toggle || resizable !== 'none' || exportable) &&
|
|
382
476
|
'pb-2'}"
|
|
383
477
|
>
|
|
@@ -404,22 +498,20 @@ $: $hiddenColumnIds = shownColumns.filter((col) => !col.visible).map((col) => co
|
|
|
404
498
|
{#if resizable !== 'none'}
|
|
405
499
|
<button
|
|
406
500
|
type="button"
|
|
407
|
-
|
|
408
|
-
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"
|
|
409
502
|
aria-label="Reset sizing of columns and rows"
|
|
410
503
|
on:click|preventDefault={() =>
|
|
411
504
|
resetResize($headerRows, $pageRows, tableId, columns, resizable)}
|
|
412
|
-
|
|
505
|
+
><Fa icon={faCompress} /> Reset sizing</button
|
|
413
506
|
>
|
|
414
507
|
{/if}
|
|
415
508
|
{#if exportable}
|
|
416
509
|
<button
|
|
417
510
|
type="button"
|
|
418
|
-
|
|
419
|
-
class="btn btn-sm variant-filled-primary rounded-full order-last"
|
|
511
|
+
class="btn btn-sm variant-filled-primary rounded-full order-last flex items-center gap-2"
|
|
420
512
|
aria-label="Export table data as CSV"
|
|
421
|
-
on:click|preventDefault={() => exportAsCsv(tableId, $exportedData)}
|
|
422
|
-
|
|
513
|
+
on:click|preventDefault={() => exportAsCsv(tableId, jsonToCsv($exportedData))}
|
|
514
|
+
><Fa icon={faDownload} /> Export as CSV</button
|
|
423
515
|
>
|
|
424
516
|
{/if}
|
|
425
517
|
{#if shownColumns.length > 0}
|
|
@@ -430,6 +522,7 @@ $: $hiddenColumnIds = shownColumns.filter((col) => !col.visible).map((col) => co
|
|
|
430
522
|
|
|
431
523
|
<div class="overflow-auto" style="height: {height}px">
|
|
432
524
|
<table
|
|
525
|
+
bind:this={tableRef}
|
|
433
526
|
{...$tableAttrs}
|
|
434
527
|
class="table table-auto table-compact bg-tertiary-500/30 dark:bg-tertiary-900/10 overflow-clip"
|
|
435
528
|
id="{tableId}-table"
|
|
@@ -446,14 +539,25 @@ $: $hiddenColumnIds = shownColumns.filter((col) => !col.visible).map((col) => co
|
|
|
446
539
|
let:rowProps
|
|
447
540
|
>
|
|
448
541
|
<tr {...rowAttrs} class="bg-primary-300 dark:bg-primary-800">
|
|
449
|
-
{#each headerRow.cells as cell (cell.id)}
|
|
542
|
+
{#each headerRow.cells as cell, index (cell.id)}
|
|
450
543
|
<Subscribe attrs={cell.attrs()} props={cell.props()} let:props let:attrs>
|
|
451
|
-
<th
|
|
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
|
+
>
|
|
452
553
|
<div
|
|
453
554
|
class="overflow-auto"
|
|
454
555
|
class:resize-x={(resizable === 'columns' || resizable === 'both') &&
|
|
455
556
|
!fixedWidth(cell.id, columns)}
|
|
456
557
|
id="th-{tableId}-{cell.id}"
|
|
558
|
+
style={`
|
|
559
|
+
min-width: ${minWidth(cell.id, columns) ? minWidth(cell.id, columns) : $colWidths[index]}px;
|
|
560
|
+
`}
|
|
457
561
|
>
|
|
458
562
|
<div class="flex justify-between items-center">
|
|
459
563
|
<div class="flex gap-1 whitespace-pre-wrap">
|
|
@@ -504,20 +608,42 @@ $: $hiddenColumnIds = shownColumns.filter((col) => !col.visible).map((col) => co
|
|
|
504
608
|
<tr {...rowAttrs} id="{tableId}-row-{row.id}" class="">
|
|
505
609
|
{#each row.cells as cell, index (cell?.id)}
|
|
506
610
|
<Subscribe attrs={cell.attrs()} let:attrs>
|
|
507
|
-
<td {...attrs} class="
|
|
611
|
+
<td {...attrs} class="">
|
|
508
612
|
<div
|
|
509
|
-
class="
|
|
613
|
+
class=" h-full {index === 0 &&
|
|
510
614
|
(resizable === 'rows' || resizable === 'both')
|
|
511
|
-
? 'resize-y'
|
|
512
|
-
: ''}"
|
|
615
|
+
? 'resize-y overflow-auto'
|
|
616
|
+
: 'block'}"
|
|
513
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
|
+
`}
|
|
514
627
|
>
|
|
515
628
|
<!-- Adding config for initial rowHeight, if provided -->
|
|
516
629
|
<div
|
|
517
|
-
class="flex items-
|
|
518
|
-
style=
|
|
630
|
+
class="flex items-start overflow-auto"
|
|
631
|
+
style={`
|
|
632
|
+
max-height: ${$rowHeights && $rowHeights[+row.id] ? `${$rowHeights[+row.id].max}px` : 'auto'};
|
|
633
|
+
`}
|
|
519
634
|
>
|
|
520
|
-
<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>
|
|
521
647
|
</div>
|
|
522
648
|
</div>
|
|
523
649
|
</td>
|
|
@@ -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
|
package/package.json
CHANGED
|
@@ -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,29 +10,55 @@
|
|
|
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
|
-
|
|
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}
|
|
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
58
|
aria-label={`${column.visible ? 'Hide' : 'Show'} ${column.label} column`}
|
|
32
59
|
type="checkbox"
|
|
33
|
-
|
|
60
|
+
class="checkbox"
|
|
61
|
+
id={column.id}
|
|
34
62
|
bind:checked={column.visible}
|
|
35
63
|
title={`${column.visible ? 'Hide' : 'Show'} ${column.label} column`}
|
|
36
64
|
disabled={columns.filter((c) => c.visible).length === 1 && column.visible}
|
|
@@ -39,5 +67,5 @@
|
|
|
39
67
|
</div>
|
|
40
68
|
{/each}
|
|
41
69
|
|
|
42
|
-
<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" />
|
|
43
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: '
|
|
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);
|
|
@@ -445,7 +567,8 @@
|
|
|
445
567
|
{/if}
|
|
446
568
|
|
|
447
569
|
<div
|
|
448
|
-
class="flex justify-between items-center w-full {search &&
|
|
570
|
+
class="flex justify-between overflow-x-auto items-center w-full {search &&
|
|
571
|
+
'py-2'} {!search &&
|
|
449
572
|
(shownColumns.length > 0 || toggle || resizable !== 'none' || exportable) &&
|
|
450
573
|
'pb-2'}"
|
|
451
574
|
>
|
|
@@ -472,22 +595,20 @@
|
|
|
472
595
|
{#if resizable !== 'none'}
|
|
473
596
|
<button
|
|
474
597
|
type="button"
|
|
475
|
-
|
|
476
|
-
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"
|
|
477
599
|
aria-label="Reset sizing of columns and rows"
|
|
478
600
|
on:click|preventDefault={() =>
|
|
479
601
|
resetResize($headerRows, $pageRows, tableId, columns, resizable)}
|
|
480
|
-
|
|
602
|
+
><Fa icon={faCompress} /> Reset sizing</button
|
|
481
603
|
>
|
|
482
604
|
{/if}
|
|
483
605
|
{#if exportable}
|
|
484
606
|
<button
|
|
485
607
|
type="button"
|
|
486
|
-
|
|
487
|
-
class="btn btn-sm variant-filled-primary rounded-full order-last"
|
|
608
|
+
class="btn btn-sm variant-filled-primary rounded-full order-last flex items-center gap-2"
|
|
488
609
|
aria-label="Export table data as CSV"
|
|
489
|
-
on:click|preventDefault={() => exportAsCsv(tableId, $exportedData)}
|
|
490
|
-
|
|
610
|
+
on:click|preventDefault={() => exportAsCsv(tableId, jsonToCsv($exportedData))}
|
|
611
|
+
><Fa icon={faDownload} /> Export as CSV</button
|
|
491
612
|
>
|
|
492
613
|
{/if}
|
|
493
614
|
{#if shownColumns.length > 0}
|
|
@@ -498,6 +619,7 @@
|
|
|
498
619
|
|
|
499
620
|
<div class="overflow-auto" style="height: {height}px">
|
|
500
621
|
<table
|
|
622
|
+
bind:this={tableRef}
|
|
501
623
|
{...$tableAttrs}
|
|
502
624
|
class="table table-auto table-compact bg-tertiary-500/30 dark:bg-tertiary-900/10 overflow-clip"
|
|
503
625
|
id="{tableId}-table"
|
|
@@ -514,14 +636,25 @@
|
|
|
514
636
|
let:rowProps
|
|
515
637
|
>
|
|
516
638
|
<tr {...rowAttrs} class="bg-primary-300 dark:bg-primary-800">
|
|
517
|
-
{#each headerRow.cells as cell (cell.id)}
|
|
639
|
+
{#each headerRow.cells as cell, index (cell.id)}
|
|
518
640
|
<Subscribe attrs={cell.attrs()} props={cell.props()} let:props let:attrs>
|
|
519
|
-
<th
|
|
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
|
+
>
|
|
520
650
|
<div
|
|
521
651
|
class="overflow-auto"
|
|
522
652
|
class:resize-x={(resizable === 'columns' || resizable === 'both') &&
|
|
523
653
|
!fixedWidth(cell.id, columns)}
|
|
524
654
|
id="th-{tableId}-{cell.id}"
|
|
655
|
+
style={`
|
|
656
|
+
min-width: ${minWidth(cell.id, columns) ? minWidth(cell.id, columns) : $colWidths[index]}px;
|
|
657
|
+
`}
|
|
525
658
|
>
|
|
526
659
|
<div class="flex justify-between items-center">
|
|
527
660
|
<div class="flex gap-1 whitespace-pre-wrap">
|
|
@@ -572,20 +705,42 @@
|
|
|
572
705
|
<tr {...rowAttrs} id="{tableId}-row-{row.id}" class="">
|
|
573
706
|
{#each row.cells as cell, index (cell?.id)}
|
|
574
707
|
<Subscribe attrs={cell.attrs()} let:attrs>
|
|
575
|
-
<td {...attrs} class="
|
|
708
|
+
<td {...attrs} class="">
|
|
576
709
|
<div
|
|
577
|
-
class="
|
|
710
|
+
class=" h-full {index === 0 &&
|
|
578
711
|
(resizable === 'rows' || resizable === 'both')
|
|
579
|
-
? 'resize-y'
|
|
580
|
-
: ''}"
|
|
712
|
+
? 'resize-y overflow-auto'
|
|
713
|
+
: 'block'}"
|
|
581
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
|
+
`}
|
|
582
724
|
>
|
|
583
725
|
<!-- Adding config for initial rowHeight, if provided -->
|
|
584
726
|
<div
|
|
585
|
-
class="flex items-
|
|
586
|
-
style=
|
|
727
|
+
class="flex items-start overflow-auto"
|
|
728
|
+
style={`
|
|
729
|
+
max-height: ${$rowHeights && $rowHeights[+row.id] ? `${$rowHeights[+row.id].max}px` : 'auto'};
|
|
730
|
+
`}
|
|
587
731
|
>
|
|
588
|
-
<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>
|
|
589
744
|
</div>
|
|
590
745
|
</div>
|
|
591
746
|
</td>
|
|
@@ -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
|
-
|
|
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
|
-
|
|
151
|
+
? Object.keys(missingValues).find(
|
|
121
152
|
(item) => new Date(item).getTime() === new Date(key).getTime()
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
153
|
+
)
|
|
154
|
+
: key in missingValues
|
|
155
|
+
? key
|
|
156
|
+
: undefined;
|
|
126
157
|
|
|
127
158
|
return foundKey ? missingValues[foundKey] : key;
|
|
128
159
|
};
|