@bexis2/bexis2-core-ui 0.4.22 → 0.4.24

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.
@@ -1,9 +1,9 @@
1
1
  <script lang="ts">
2
- import { createEventDispatcher } from 'svelte';
2
+ import { afterUpdate, createEventDispatcher, onDestroy } 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,
@@ -26,16 +26,9 @@
26
26
  import TablePagination from './TablePagination.svelte';
27
27
  import TablePaginationServer from './TablePaginationServer.svelte';
28
28
  import ColumnsMenu from './ColumnsMenu.svelte';
29
+ import * as utils from './utils';
29
30
  import { columnFilter, searchFilter } from './filter';
30
- import {
31
- cellStyle,
32
- exportAsCsv,
33
- fixedWidth,
34
- normalizeFilters,
35
- resetResize,
36
- convertServerColumns
37
- } from './shared';
38
- import { Receive, Send } from '$models/Models';
31
+ import { Send } from '$models/Models';
39
32
  import type { TableConfig } from '$models/Models';
40
33
  import type { FilterOptionsEnum } from '$models/Enums';
41
34
 
@@ -46,6 +39,7 @@
46
39
  id: tableId, // Unique table ID
47
40
  data, // Data store
48
41
  columns, // Column configuration
42
+ showColumnsMenu = false, // Whether to display the columns menu
49
43
  resizable = 'none', // Resizability config
50
44
  height = null, // Table height
51
45
  rowHeight = null, // Row height
@@ -54,6 +48,7 @@
54
48
  toggle = false, // Whether to display the fitToScreen toggle
55
49
  search = true, // Whether to display the search input
56
50
  pageSizes = [5, 10, 20, 50, 100], // Page sizes to display in the pagination component
51
+ pageIndexStringType = 'pages', // pages by default
57
52
  fitToScreen = true, // Whether to fit the table to the screen,
58
53
  exportable = false, // Whether to display the export button and enable export functionality
59
54
  server
@@ -61,8 +56,10 @@
61
56
 
62
57
  let searchValue = '';
63
58
  let isFetching = false;
59
+ let tableRef: HTMLTableElement;
60
+
64
61
  const serverSide = server !== undefined;
65
- const { baseUrl, entityId, versionId, sendModel = new Send() } = server ?? {};
62
+ const { sendModel = new Send() } = server ?? {};
66
63
 
67
64
  const filters = writable<{
68
65
  [key: string]: { [key in FilterOptionsEnum]?: number | string | Date };
@@ -75,7 +72,12 @@
75
72
  const dispatch = createEventDispatcher();
76
73
  const actionDispatcher = (obj) => dispatch('action', obj);
77
74
 
78
- const serverItems = serverSide ? writable<Number>(0) : undefined;
75
+ // Stores to hold the width and height information for resizing
76
+ const rowHeights = writable<{ [key: number]: { max: number; min: number } }>({});
77
+ const colWidths = writable<number[]>([]);
78
+
79
+ // Server-side variables
80
+ const serverItems = serverSide ? writable<number>(0) : undefined;
79
81
  const serverItemCount = serverSide
80
82
  ? readable<Number>(0, (set) => {
81
83
  serverItems!.subscribe((val) => set(val));
@@ -100,7 +102,7 @@
100
102
  serverItemCount
101
103
  } as PaginationConfig),
102
104
  expand: addExpandedRows(),
103
- export: addDataExport({ format: 'csv' })
105
+ export: addDataExport({ format: 'json' })
104
106
  });
105
107
 
106
108
  // A variable to hold all the keys
@@ -198,7 +200,7 @@
198
200
  id,
199
201
  tableId,
200
202
  values,
201
- updateTable,
203
+ updateTable: updateTableWithParams,
202
204
  pageIndex,
203
205
  toFilterableValueFn,
204
206
  filters,
@@ -232,7 +234,7 @@
232
234
  accessor: accessor,
233
235
  cell: ({ value }) => {
234
236
  // If null or undefined, return an empty string
235
- return value ? value : '';
237
+ return value ?? '';
236
238
  },
237
239
  plugins: {
238
240
  // Sorting enabled by default
@@ -246,7 +248,7 @@
246
248
  id,
247
249
  tableId,
248
250
  values,
249
- updateTable,
251
+ updateTable: updateTableWithParams,
250
252
  pageIndex,
251
253
  filters
252
254
  })
@@ -290,7 +292,7 @@
290
292
  // Creating the table columns
291
293
  const createdTableColumns = table.createColumns(tableColumns);
292
294
  // Creating the table view model
293
- const { headerRows, pageRows, tableAttrs, tableBodyAttrs, pluginStates } =
295
+ const { headerRows, pageRows, tableAttrs, tableBodyAttrs, pluginStates, rows } =
294
296
  table.createViewModel(createdTableColumns);
295
297
  // Extracting filterValue to bind it for the search input and search immediately on input
296
298
  const { filterValue } = pluginStates.tableFilter;
@@ -301,82 +303,110 @@
301
303
  // Column visibility configuration
302
304
  const { hiddenColumnIds } = pluginStates.hideColumns;
303
305
 
304
- const updateTable = async () => {
306
+ const sortServer = (order: 'asc' | 'desc' | undefined, id: string) => {
305
307
  if (!sendModel) throw new Error('Server-side configuration is missing');
308
+ // Set parameter for sorting
309
+ if (order === undefined) {
310
+ sendModel.order = [];
311
+ } else {
312
+ sendModel.order = [{ column: id, direction: order }];
313
+ }
314
+
315
+ // Reset pagination
316
+ $pageIndex = 0;
306
317
 
307
- sendModel.limit = $pageSize;
308
- sendModel.offset = $pageSize * $pageIndex;
309
- sendModel.version = versionId || -1;
310
- sendModel.id = entityId || -1;
311
- sendModel.filter = normalizeFilters($filters);
318
+ updateTableWithParams();
319
+ };
312
320
 
313
- let fetchData;
321
+ // Function to update the table with the provided parameters for easily passing to other components
322
+ const updateTableWithParams = async () => {
323
+ isFetching = true;
324
+ const result = await utils.updateTable(
325
+ $pageSize,
326
+ $pageIndex,
327
+ server,
328
+ $filters,
329
+ data,
330
+ serverItems,
331
+ columns,
332
+ dispatch
333
+ );
334
+ isFetching = false;
314
335
 
315
- try {
316
- isFetching = true;
317
- fetchData = await fetch(baseUrl || '', {
318
- headers: {
319
- 'Content-Type': 'application/json'
320
- },
321
- method: 'POST',
322
- body: JSON.stringify(sendModel)
323
- });
324
- } catch (error) {
325
- throw new Error(`Network error: ${(error as Error).message}`);
326
- } finally {
327
- isFetching = false;
328
- }
336
+ return result;
337
+ };
329
338
 
330
- if (!fetchData.ok) {
331
- throw new Error('Failed to fetch data');
339
+ // Initializes observers for rows and columns
340
+ const getDimensions = () => {
341
+ if (!tableRef) return;
342
+ if (resizable === 'none') return;
343
+ else if (resizable === 'columns') {
344
+ observeHeaderColumns();
345
+ } else if (resizable === 'rows') {
346
+ observeFirstCells();
347
+ } else {
348
+ observeHeaderColumns();
349
+ observeFirstCells();
332
350
  }
351
+ };
333
352
 
334
- const response: Receive = await fetchData.json();
353
+ // Resize observer for rows
354
+ const resizeRowsObserver = new ResizeObserver(() => {
355
+ utils.getMaxCellHeightInRow(
356
+ tableRef,
357
+ resizable,
358
+ optionsComponent,
359
+ rowHeights,
360
+ tableId,
361
+ rowHeight
362
+ );
363
+ });
335
364
 
336
- // Format server columns to the client columns
337
- if (response.columns !== undefined) {
338
- columns = convertServerColumns(response.columns, columns);
365
+ // Resize observers for columns
366
+ const resizeColumnsObserver = new ResizeObserver(() => {
367
+ utils.getMinCellWidthInColumn(tableRef, colWidths, $headerRows[0].cells.length, resizable);
368
+ });
339
369
 
340
- const clientCols = response.columns.reduce((acc, col) => {
341
- acc[col.key] = col.column;
342
- return acc;
343
- }, {});
370
+ // Adds observers on the first cells of each row to resize rows
371
+ const observeFirstCells = () => {
372
+ if (!tableRef) return;
344
373
 
345
- const tmpArr: any[] = [];
374
+ tableRef.querySelectorAll('tbody tr td:first-child').forEach((cell) => {
375
+ resizeRowsObserver.observe(cell);
376
+ });
346
377
 
347
- response.data.forEach((row, index) => {
348
- const tmp: { [key: string]: any } = {};
349
- Object.keys(row).forEach((key) => {
350
- tmp[clientCols[key]] = row[key];
351
- });
352
- tmpArr.push(tmp);
353
- });
354
- dispatch('fetch', columns);
355
- $data = tmpArr;
356
- }
378
+ return resizeRowsObserver;
379
+ };
357
380
 
358
- $serverItems = response.count;
381
+ // Adds observers on the header columns to resize columns
382
+ const observeHeaderColumns = () => {
383
+ if (!tableRef) return;
359
384
 
360
- return response;
385
+ tableRef.querySelectorAll('thead tr th').forEach((cell) => {
386
+ resizeColumnsObserver.observe(cell);
387
+ });
361
388
  };
362
389
 
363
- const sortServer = (order: 'asc' | 'desc' | undefined, id: string) => {
364
- if (!sendModel) throw new Error('Server-side configuration is missing');
365
- // Set parameter for sorting
366
- if (order === undefined) {
367
- sendModel.order = [];
368
- } else {
369
- sendModel.order = [{ column: id, direction: order }];
390
+ afterUpdate(() => {
391
+ // If not resizable, return
392
+ if (resizable !== 'rows' && resizable !== 'both') {
393
+ return;
370
394
  }
395
+ // Making sure tableRef is up to date and contains the new rows
396
+ // If it contains even one element, it means it contains them all
397
+ const e = tableRef?.querySelector(`#${tableId}-row-${$pageRows[0].id}`);
398
+ if (e) {
399
+ getDimensions();
400
+ }
401
+ });
371
402
 
372
- // Reset pagination
373
- $pageIndex = 0;
374
-
375
- updateTable();
376
- };
403
+ onDestroy(() => {
404
+ resizeColumnsObserver.disconnect();
405
+ resizeRowsObserver.disconnect();
406
+ });
377
407
 
378
408
  $: sortKeys = pluginStates.sort.sortKeys;
379
- $: serverSide && updateTable();
409
+ $: serverSide && updateTableWithParams();
380
410
  $: serverSide && sortServer($sortKeys[0]?.order, $sortKeys[0]?.id);
381
411
  $: $hiddenColumnIds = shownColumns.filter((col) => !col.visible).map((col) => col.id);
382
412
  </script>
@@ -445,7 +475,8 @@
445
475
  {/if}
446
476
 
447
477
  <div
448
- class="flex justify-between items-center w-full {search && 'py-2'} {!search &&
478
+ class="flex justify-between overflow-x-auto items-center w-full {search &&
479
+ 'py-2'} {!search &&
449
480
  (shownColumns.length > 0 || toggle || resizable !== 'none' || exportable) &&
450
481
  'pb-2'}"
451
482
  >
@@ -472,25 +503,26 @@
472
503
  {#if resizable !== 'none'}
473
504
  <button
474
505
  type="button"
475
- title="Reset column and row sizing"
476
- class="btn btn-sm variant-filled-primary rounded-full order-last"
506
+ class="btn btn-sm variant-filled-primary rounded-full order-last flex gap-2 items-center"
477
507
  aria-label="Reset sizing of columns and rows"
478
508
  on:click|preventDefault={() =>
479
- resetResize($headerRows, $pageRows, tableId, columns, resizable)}
480
- >Reset sizing</button
509
+ utils.resetResize($headerRows, $pageRows, tableId, columns, resizable)}
510
+ ><Fa icon={faCompress} /> Reset sizing</button
481
511
  >
482
512
  {/if}
513
+ <!-- Enable export as CSV button if exportable === true -->
483
514
  {#if exportable}
484
515
  <button
485
516
  type="button"
486
- title="Export table data as CSV"
487
- class="btn btn-sm variant-filled-primary rounded-full order-last"
517
+ class="btn btn-sm variant-filled-primary rounded-full order-last flex items-center gap-2"
488
518
  aria-label="Export table data as CSV"
489
- on:click|preventDefault={() => exportAsCsv(tableId, $exportedData)}
490
- >Export as CSV</button
519
+ on:click|preventDefault={() =>
520
+ utils.exportAsCsv(tableId, utils.jsonToCsv($exportedData))}
521
+ ><Fa icon={faDownload} /> Export as CSV</button
491
522
  >
492
523
  {/if}
493
- {#if shownColumns.length > 0}
524
+ <!-- Enable show/hide columns menu if showColumnsMenu === true -->
525
+ {#if showColumnsMenu && shownColumns.length > 0}
494
526
  <ColumnsMenu bind:columns={shownColumns} {tableId} />
495
527
  {/if}
496
528
  </div>
@@ -498,6 +530,7 @@
498
530
 
499
531
  <div class="overflow-auto" style="height: {height}px">
500
532
  <table
533
+ bind:this={tableRef}
501
534
  {...$tableAttrs}
502
535
  class="table table-auto table-compact bg-tertiary-500/30 dark:bg-tertiary-900/10 overflow-clip"
503
536
  id="{tableId}-table"
@@ -514,14 +547,29 @@
514
547
  let:rowProps
515
548
  >
516
549
  <tr {...rowAttrs} class="bg-primary-300 dark:bg-primary-800">
517
- {#each headerRow.cells as cell (cell.id)}
550
+ {#each headerRow.cells as cell, index (cell.id)}
518
551
  <Subscribe attrs={cell.attrs()} props={cell.props()} let:props let:attrs>
519
- <th scope="col" class="!p-2" {...attrs} style={cellStyle(cell.id, columns)}>
552
+ <th
553
+ scope="col"
554
+ class="!p-2"
555
+ {...attrs}
556
+ style={`
557
+ width: ${cell.isData() ? 'auto' : '0'};
558
+ ${utils.cellStyle(cell.id, columns)}
559
+ `}
560
+ >
520
561
  <div
521
562
  class="overflow-auto"
522
563
  class:resize-x={(resizable === 'columns' || resizable === 'both') &&
523
- !fixedWidth(cell.id, columns)}
564
+ !utils.fixedWidth(cell.id, columns)}
524
565
  id="th-{tableId}-{cell.id}"
566
+ style={`
567
+ min-width: ${
568
+ utils.minWidth(cell.id, columns)
569
+ ? utils.minWidth(cell.id, columns)
570
+ : $colWidths[index]
571
+ }px;
572
+ `}
525
573
  >
526
574
  <div class="flex justify-between items-center">
527
575
  <div class="flex gap-1 whitespace-pre-wrap">
@@ -572,20 +620,34 @@
572
620
  <tr {...rowAttrs} id="{tableId}-row-{row.id}" class="">
573
621
  {#each row.cells as cell, index (cell?.id)}
574
622
  <Subscribe attrs={cell.attrs()} let:attrs>
575
- <td {...attrs} class="!p-2">
623
+ <td {...attrs} class="">
576
624
  <div
577
- class=" overflow-auto h-max {index === 0 &&
625
+ class=" h-full {index === 0 &&
578
626
  (resizable === 'rows' || resizable === 'both')
579
- ? 'resize-y'
580
- : ''}"
627
+ ? 'resize-y overflow-auto'
628
+ : 'block'}"
581
629
  id="{tableId}-{cell.id}-{row.id}"
630
+ style={utils.getResizeStyles($rowHeights, row.id, index)}
582
631
  >
583
632
  <!-- Adding config for initial rowHeight, if provided -->
584
633
  <div
585
- class="flex items-center overflow-auto"
586
- style="height: {rowHeight ? `${rowHeight}px` : 'auto'};"
634
+ class="flex items-start overflow-auto"
635
+ style={`
636
+ max-height: ${$rowHeights && $rowHeights[+row.id] ? `${$rowHeights[+row.id].max}px` : 'auto'};
637
+ `}
587
638
  >
588
- <div class="grow h-full"><Render of={cell.render()} /></div>
639
+ <div
640
+ class="grow overflow-auto"
641
+ style={cell.isData()
642
+ ? `width: ${
643
+ utils.minWidth(cell.id, columns)
644
+ ? utils.minWidth(cell.id, columns)
645
+ : $colWidths[index]
646
+ }px;`
647
+ : 'max-width: min-content;'}
648
+ >
649
+ <Render of={cell.render()} />
650
+ </div>
589
651
  </div>
590
652
  </div>
591
653
  </td>
@@ -622,12 +684,19 @@
622
684
  {pageIndex}
623
685
  {pageSize}
624
686
  {serverItemCount}
625
- {updateTable}
687
+ updateTable={updateTableWithParams}
626
688
  {pageSizes}
689
+ {pageIndexStringType}
627
690
  id={tableId}
628
691
  />
629
692
  {:else}
630
- <TablePagination pageConfig={pluginStates.page} {pageSizes} id={tableId} />
693
+ <TablePagination
694
+ itemCount={$rows.length}
695
+ pageConfig={pluginStates.page}
696
+ {pageSizes}
697
+ id={tableId}
698
+ {pageIndexStringType}
699
+ />
631
700
  {/if}
632
701
  {/if}
633
702
  </div>
@@ -9,10 +9,14 @@
9
9
  } from '@fortawesome/free-solid-svg-icons';
10
10
  import { ListBox, ListBoxItem, popup, type PopupSettings } from '@skeletonlabs/skeleton';
11
11
 
12
+ export let itemCount;
12
13
  export let pageConfig;
13
14
  export let pageSizes;
15
+ export let pageIndexStringType;
14
16
  export let id;
15
17
 
18
+ let indexInformation = '';
19
+
16
20
  const { pageIndex, pageCount, pageSize, hasNextPage, hasPreviousPage } = pageConfig;
17
21
 
18
22
  const goToFirstPage = () => ($pageIndex = 0);
@@ -42,11 +46,23 @@
42
46
  closeQuery: '.listbox-item'
43
47
  };
44
48
 
49
+ const getIndexInfomationString = () => {
50
+ if (pageIndexStringType === 'pages') {
51
+ return $pageCount > 0 ? `Page ${$pageIndex + 1} of ${$pageCount}` : 'No pages';
52
+ } else {
53
+ return itemCount === 0 ? 'No items' : `Displaying items ${$pageIndex * $pageSize + 1} - ${Math.min(
54
+ ($pageIndex + 1) * $pageSize,
55
+ itemCount
56
+ )} of ${Math.min($pageCount * $pageSize, itemCount)}`;
57
+ }
58
+ };
59
+
45
60
  $: goToFirstPageDisabled = !$pageIndex;
46
61
  $: goToLastPageDisabled = $pageIndex == $pageCount - 1;
47
62
  $: goToNextPageDisabled = !$hasNextPage;
48
63
  $: goToPreviousPageDisabled = !$hasPreviousPage;
49
64
  $: $pageSize = pageSizeDropdownValue;
65
+ $: $pageCount, $pageIndex, $pageSize, (indexInformation = getIndexInfomationString());
50
66
  </script>
51
67
 
52
68
  <div class="flex justify-between w-full items-stretch gap-10">
@@ -124,12 +140,6 @@
124
140
  >
125
141
  </div>
126
142
  <div class="flex justify-end items-center">
127
- <span class="text-sm text-gray-500">
128
- {#if $pageCount > 0}
129
- Page {$pageIndex + 1} of {$pageCount}
130
- {:else}
131
- No pages
132
- {/if}
133
- </span>
143
+ <span class="text-sm text-gray-500">{indexInformation}</span>
134
144
  </div>
135
145
  </div>
@@ -10,6 +10,7 @@
10
10
  export let id; // Unique table ID
11
11
  export let pageIndex;
12
12
  export let pageSize;
13
+ export let pageIndexStringType;
13
14
  export let pageSizes; // Available page sizes
14
15
  export let serverItemCount; // Total number of items expected from the server. `serverSide` must be true on table config.
15
16
  export let updateTable; // Function to update table
@@ -20,6 +21,9 @@
20
21
  let goToNextPageDisabled = true;
21
22
  let goToPreviousPageDisabled = true;
22
23
 
24
+ // Index information string
25
+ let indexInformation = '';
26
+
23
27
  // Handles the input change event
24
28
  const handleChange = (e) => {
25
29
  const value = e.target.value;
@@ -58,12 +62,23 @@
58
62
  updateTable();
59
63
  };
60
64
 
65
+ const getIndexInfomationString = () => {
66
+ if (pageIndexStringType === 'pages') {
67
+ return pageCount > 0 ? `Page ${$pageIndex + 1} of ${pageCount}` : 'No pages';
68
+ } else {
69
+ return `Showing ${$pageIndex * $pageSize + 1} - ${($pageIndex + 1) * $pageSize} of ${
70
+ pageCount * $pageSize
71
+ } items`;
72
+ }
73
+ };
74
+
61
75
  $: pageCount = Math.ceil($serverItemCount / $pageSize);
62
76
  $: goToFirstPageDisabled = !$pageIndex;
63
77
  $: goToLastPageDisabled = $pageIndex == pageCount - 1;
64
78
  $: goToNextPageDisabled = $pageIndex == pageCount - 1;
65
79
  $: goToPreviousPageDisabled = !$pageIndex;
66
80
  $: $pageSize && updateTable(); // Update query when page size changes
81
+ $: pageCount, $pageIndex, $pageSize, (indexInformation = getIndexInfomationString());
67
82
 
68
83
  updateTable();
69
84
  </script>
@@ -127,15 +142,7 @@
127
142
  </div>
128
143
  <div class="flex justify-end items-center">
129
144
  <span class="text-sm text-gray-500">
130
- {#if pageCount > 0}
131
- {#if pageCount == 1}
132
- 1 page
133
- {:else}
134
- {pageCount} pages
135
- {/if}
136
- {:else}
137
- No pages
138
- {/if}
145
+ {indexInformation}
139
146
  </span>
140
147
  </div>
141
148
  </div>