@bexis2/bexis2-core-ui 0.4.4 → 0.4.6
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 +12 -0
- package/dist/components/Table/Table.svelte +10 -2
- package/dist/components/Table/TableContent.svelte +119 -96
- package/dist/components/Table/TableContent.svelte.d.ts +1 -0
- package/dist/components/Table/TableFilter.svelte +3 -1
- package/dist/components/Table/TableFilterServer.svelte +41 -23
- package/dist/components/Table/TableFilterServer.svelte.d.ts +1 -0
- package/dist/components/Table/shared.d.ts +3 -3
- package/dist/components/Table/shared.js +48 -11
- package/dist/models/Models.d.ts +2 -0
- package/package.json +4 -3
- package/src/lib/components/Table/Table.svelte +10 -2
- package/src/lib/components/Table/TableContent.svelte +123 -99
- package/src/lib/components/Table/TableFilter.svelte +3 -1
- package/src/lib/components/Table/TableFilterServer.svelte +53 -33
- package/src/lib/components/Table/shared.ts +58 -14
- package/src/lib/models/Models.ts +3 -1
package/README.md
CHANGED
|
@@ -1,4 +1,16 @@
|
|
|
1
1
|
# bexis-core-ui
|
|
2
|
+
## 0.4.6
|
|
3
|
+
- table
|
|
4
|
+
- Adds config for enabling and disabling Search field on Table.
|
|
5
|
+
- Adds action dispatcher as prop for the renderComponent.
|
|
6
|
+
- Adds unique IDs for Search reset and submit buttons.
|
|
7
|
+
|
|
8
|
+
## 0.4.5
|
|
9
|
+
- table
|
|
10
|
+
- adds searching for missing values in the filters #744
|
|
11
|
+
- missing values for every data type
|
|
12
|
+
- display patterns for date/time/datetime types
|
|
13
|
+
|
|
2
14
|
## 0.4.4
|
|
3
15
|
- update libs
|
|
4
16
|
- remove eslint-plugin-svelte3
|
|
@@ -1,8 +1,16 @@
|
|
|
1
1
|
<script>import Table from "./TableContent.svelte";
|
|
2
2
|
export let config;
|
|
3
|
+
let fetched = false;
|
|
3
4
|
const data = config.data;
|
|
5
|
+
$:
|
|
6
|
+
if ($data.length > 0)
|
|
7
|
+
fetched = true;
|
|
4
8
|
</script>
|
|
5
9
|
|
|
6
|
-
{#key
|
|
7
|
-
<Table
|
|
10
|
+
{#key fetched}
|
|
11
|
+
<Table
|
|
12
|
+
{config}
|
|
13
|
+
on:fetch={(columns) => (config = { ...config, columns: columns.detail })}
|
|
14
|
+
on:action
|
|
15
|
+
/>
|
|
8
16
|
{/key}
|
|
@@ -49,6 +49,8 @@ let {
|
|
|
49
49
|
// Default page size - number of rows to display per page
|
|
50
50
|
toggle = false,
|
|
51
51
|
// Whether to display the fitToScreen toggle
|
|
52
|
+
search = true,
|
|
53
|
+
// Whether to display the search input
|
|
52
54
|
pageSizes = [5, 10, 15, 20],
|
|
53
55
|
// Page sizes to display in the pagination component
|
|
54
56
|
fitToScreen = true,
|
|
@@ -66,7 +68,7 @@ let {
|
|
|
66
68
|
entityId = 0,
|
|
67
69
|
// Entity ID to send with the request
|
|
68
70
|
versionId = 0
|
|
69
|
-
// Version ID to send with the request
|
|
71
|
+
// Version ID to send with the request,
|
|
70
72
|
} = config;
|
|
71
73
|
let searchValue = "";
|
|
72
74
|
let isFetching = false;
|
|
@@ -138,7 +140,7 @@ const tableColumns = [
|
|
|
138
140
|
accessor,
|
|
139
141
|
// Render the cell with the provided component, or use the toStringFn if provided, or just use the value
|
|
140
142
|
cell: ({ value, row }) => {
|
|
141
|
-
return renderComponent ? createRender(renderComponent, { value, row }) : toStringFn ? toStringFn(value) : value;
|
|
143
|
+
return renderComponent ? createRender(renderComponent, { value, row, dispatchFn: actionDispatcher }) : toStringFn ? toStringFn(value) : value;
|
|
142
144
|
},
|
|
143
145
|
plugins: {
|
|
144
146
|
// Sorting config
|
|
@@ -162,7 +164,8 @@ const tableColumns = [
|
|
|
162
164
|
updateTable,
|
|
163
165
|
pageIndex,
|
|
164
166
|
toFilterableValueFn,
|
|
165
|
-
filters
|
|
167
|
+
filters,
|
|
168
|
+
toStringFn
|
|
166
169
|
}) : createRender(colFilterComponent ?? TableFilter, {
|
|
167
170
|
filterValue: filterValue2,
|
|
168
171
|
id,
|
|
@@ -270,9 +273,22 @@ const updateTable = async () => {
|
|
|
270
273
|
}
|
|
271
274
|
const response = await fetchData.json();
|
|
272
275
|
if (response.columns !== void 0) {
|
|
273
|
-
columns = convertServerColumns(response.columns);
|
|
276
|
+
columns = convertServerColumns(response.columns, columns);
|
|
277
|
+
const clientCols = response.columns.reduce((acc, col) => {
|
|
278
|
+
acc[col.key] = col.column;
|
|
279
|
+
return acc;
|
|
280
|
+
}, {});
|
|
281
|
+
const tmpArr = [];
|
|
282
|
+
response.data.forEach((row, index) => {
|
|
283
|
+
const tmp = {};
|
|
284
|
+
Object.keys(row).forEach((key) => {
|
|
285
|
+
tmp[clientCols[key]] = row[key];
|
|
286
|
+
});
|
|
287
|
+
tmpArr.push(tmp);
|
|
288
|
+
});
|
|
289
|
+
dispatch("fetch", columns);
|
|
290
|
+
$data = tmpArr;
|
|
274
291
|
}
|
|
275
|
-
$data = response.data;
|
|
276
292
|
$serverItems = response.count;
|
|
277
293
|
return response;
|
|
278
294
|
};
|
|
@@ -294,87 +310,92 @@ $:
|
|
|
294
310
|
</script>
|
|
295
311
|
|
|
296
312
|
<div class="grid gap-2 overflow-auto" class:w-fit={!fitToScreen} class:w-full={fitToScreen}>
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
$filterValue = searchValue;
|
|
305
|
-
}}
|
|
306
|
-
>
|
|
307
|
-
<div class="relative w-full flex items-center">
|
|
308
|
-
<input
|
|
309
|
-
class="input p-2 border border-primary-500"
|
|
310
|
-
type="text"
|
|
311
|
-
bind:value={searchValue}
|
|
312
|
-
placeholder="Search rows..."
|
|
313
|
-
id="{tableId}-search"
|
|
314
|
-
/><button
|
|
315
|
-
type="reset"
|
|
316
|
-
class="absolute right-3 items-center"
|
|
317
|
-
on:click|preventDefault={() => {
|
|
318
|
-
searchValue = '';
|
|
319
|
-
sendModel.q = '';
|
|
320
|
-
$filterValue = '';
|
|
321
|
-
}}><Fa icon={faXmark} /></button
|
|
322
|
-
>
|
|
323
|
-
</div>
|
|
324
|
-
<button
|
|
325
|
-
type="submit"
|
|
326
|
-
class="btn variant-filled-primary"
|
|
327
|
-
on:click|preventDefault={() => {
|
|
328
|
-
$filterValue = searchValue;
|
|
313
|
+
{#if $data.length > 0 || (columns && Object.keys(columns).length > 0)}
|
|
314
|
+
<div class="table-container">
|
|
315
|
+
<!-- Enable the search filter if table is not empty -->
|
|
316
|
+
{#if !serverSide && search}
|
|
317
|
+
<form
|
|
318
|
+
class="flex gap-2"
|
|
319
|
+
on:submit|preventDefault={() => {
|
|
329
320
|
sendModel.q = searchValue;
|
|
330
|
-
|
|
321
|
+
$filterValue = searchValue;
|
|
322
|
+
}}
|
|
331
323
|
>
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
{#if resizable !== 'none'}
|
|
324
|
+
<div class="relative w-full flex items-center">
|
|
325
|
+
<input
|
|
326
|
+
class="input p-2 border border-primary-500"
|
|
327
|
+
type="text"
|
|
328
|
+
bind:value={searchValue}
|
|
329
|
+
placeholder="Search rows..."
|
|
330
|
+
id="{tableId}-search"
|
|
331
|
+
/><button
|
|
332
|
+
type="reset"
|
|
333
|
+
id="{tableId}-searchReset"
|
|
334
|
+
class="absolute right-3 items-center"
|
|
335
|
+
on:click|preventDefault={() => {
|
|
336
|
+
searchValue = '';
|
|
337
|
+
sendModel.q = '';
|
|
338
|
+
$filterValue = '';
|
|
339
|
+
}}><Fa icon={faXmark} /></button
|
|
340
|
+
>
|
|
341
|
+
</div>
|
|
351
342
|
<button
|
|
352
|
-
type="
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
<button
|
|
360
|
-
type="button"
|
|
361
|
-
class="btn btn-sm variant-filled-primary rounded-full order-last"
|
|
362
|
-
on:click|preventDefault={() => exportAsCsv(tableId, $exportedData)}
|
|
363
|
-
>Export as CSV</button
|
|
343
|
+
type="submit"
|
|
344
|
+
id="{tableId}-searchSubmit"
|
|
345
|
+
class="btn variant-filled-primary"
|
|
346
|
+
on:click|preventDefault={() => {
|
|
347
|
+
$filterValue = searchValue;
|
|
348
|
+
sendModel.q = searchValue;
|
|
349
|
+
}}>Search</button
|
|
364
350
|
>
|
|
365
|
-
|
|
351
|
+
</form>
|
|
352
|
+
{/if}
|
|
353
|
+
|
|
354
|
+
<div class="flex justify-between items-center w-full {search && 'py-2'}">
|
|
355
|
+
<div>
|
|
356
|
+
<!-- Enable the fitToScreen toggle if toggle === true -->
|
|
357
|
+
{#if toggle}
|
|
358
|
+
<SlideToggle
|
|
359
|
+
name="slider-label"
|
|
360
|
+
active="bg-primary-500"
|
|
361
|
+
size="sm"
|
|
362
|
+
checked={fitToScreen}
|
|
363
|
+
id="{tableId}-toggle"
|
|
364
|
+
on:change={() => (fitToScreen = !fitToScreen)}>Fit to screen</SlideToggle
|
|
365
|
+
>
|
|
366
|
+
{/if}
|
|
367
|
+
</div>
|
|
368
|
+
<div class="flex gap-2">
|
|
369
|
+
<!-- Enable the resetResize button if resizable !== 'none' -->
|
|
370
|
+
{#if resizable !== 'none'}
|
|
371
|
+
<button
|
|
372
|
+
type="button"
|
|
373
|
+
class="btn btn-sm variant-filled-primary rounded-full order-last"
|
|
374
|
+
on:click|preventDefault={() =>
|
|
375
|
+
resetResize($headerRows, $pageRows, tableId, columns, resizable)}
|
|
376
|
+
>Reset sizing</button
|
|
377
|
+
>
|
|
378
|
+
{/if}
|
|
379
|
+
{#if exportable}
|
|
380
|
+
<button
|
|
381
|
+
type="button"
|
|
382
|
+
class="btn btn-sm variant-filled-primary rounded-full order-last"
|
|
383
|
+
on:click|preventDefault={() => exportAsCsv(tableId, $exportedData)}
|
|
384
|
+
>Export as CSV</button
|
|
385
|
+
>
|
|
386
|
+
{/if}
|
|
387
|
+
</div>
|
|
366
388
|
</div>
|
|
367
|
-
</div>
|
|
368
389
|
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
390
|
+
<div class="overflow-auto" style="height: {height}px">
|
|
391
|
+
<table
|
|
392
|
+
{...$tableAttrs}
|
|
393
|
+
class="table table-auto table-compact bg-tertiary-500/30 dark:bg-tertiary-900/10 overflow-clip"
|
|
394
|
+
id="{tableId}-table"
|
|
395
|
+
>
|
|
396
|
+
<!-- If table height is provided, making the top row sticky -->
|
|
397
|
+
<thead class={height != null && $pageRows.length > 0 ? `sticky top-0` : ''}>
|
|
398
|
+
<!-- {#if $data.length > 0} -->
|
|
378
399
|
{#each $headerRows as headerRow (headerRow.id)}
|
|
379
400
|
<Subscribe
|
|
380
401
|
rowAttrs={headerRow.attrs()}
|
|
@@ -382,7 +403,7 @@ $:
|
|
|
382
403
|
rowProps={headerRow.props()}
|
|
383
404
|
let:rowProps
|
|
384
405
|
>
|
|
385
|
-
<tr {...rowAttrs} class="bg-primary-300 dark:bg-primary-
|
|
406
|
+
<tr {...rowAttrs} class="bg-primary-300 dark:bg-primary-800">
|
|
386
407
|
{#each headerRow.cells as cell (cell.id)}
|
|
387
408
|
<Subscribe attrs={cell.attrs()} props={cell.props()} let:props let:attrs>
|
|
388
409
|
<th scope="col" class="!p-2" {...attrs} style={cellStyle(cell.id, columns)}>
|
|
@@ -428,16 +449,9 @@ $:
|
|
|
428
449
|
</tr>
|
|
429
450
|
</Subscribe>
|
|
430
451
|
{/each}
|
|
431
|
-
|
|
432
|
-
<div class="p-10"><Spinner /></div>
|
|
433
|
-
{:else}
|
|
434
|
-
<!-- Table is empty -->
|
|
435
|
-
<p class="items-center justify-center flex w-full p-10 italic">Nothing to show here.</p>
|
|
436
|
-
{/if}
|
|
437
|
-
</thead>
|
|
452
|
+
</thead>
|
|
438
453
|
|
|
439
|
-
|
|
440
|
-
{#if $data.length > 0}
|
|
454
|
+
<tbody class="overflow-auto" {...$tableBodyAttrs}>
|
|
441
455
|
{#each $pageRows as row (row.id)}
|
|
442
456
|
<Subscribe rowAttrs={row.attrs()} let:rowAttrs>
|
|
443
457
|
<tr {...rowAttrs} id="{tableId}-row-{row.id}" class="">
|
|
@@ -465,13 +479,22 @@ $:
|
|
|
465
479
|
</tr>
|
|
466
480
|
</Subscribe>
|
|
467
481
|
{/each}
|
|
468
|
-
|
|
469
|
-
</
|
|
470
|
-
</
|
|
482
|
+
</tbody>
|
|
483
|
+
</table>
|
|
484
|
+
</div>
|
|
471
485
|
</div>
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
486
|
+
{:else}
|
|
487
|
+
<div class="p-10 w-full h-full flex justify-center items-center bg-neutral-200 rounded">
|
|
488
|
+
<p>No data available</p>
|
|
489
|
+
</div>
|
|
490
|
+
{/if}
|
|
491
|
+
|
|
492
|
+
{#if isFetching}
|
|
493
|
+
<div class="p-10 w-full h-full flex justify-center items-center"><Spinner /></div>
|
|
494
|
+
{/if}
|
|
495
|
+
|
|
496
|
+
<!-- Adding pagination, if table is not empty -->
|
|
497
|
+
{#if $data.length > 0 || (columns && Object.keys(columns).length > 0)}
|
|
475
498
|
{#if serverSide}
|
|
476
499
|
<TablePaginationServer
|
|
477
500
|
{pageIndex}
|
|
@@ -18,7 +18,9 @@ $values.forEach((item) => {
|
|
|
18
18
|
if (item) {
|
|
19
19
|
type = typeof (toFilterableValueFn ? toFilterableValueFn(item) : item);
|
|
20
20
|
if (type === "object") {
|
|
21
|
-
if (item instanceof Date) {
|
|
21
|
+
if (toFilterableValueFn && toFilterableValueFn(item) instanceof Date) {
|
|
22
|
+
isDate = true;
|
|
23
|
+
} else if (item instanceof Date) {
|
|
22
24
|
isDate = true;
|
|
23
25
|
}
|
|
24
26
|
}
|
|
@@ -9,7 +9,21 @@ export let toFilterableValueFn = void 0;
|
|
|
9
9
|
export let filters;
|
|
10
10
|
export let updateTable;
|
|
11
11
|
export let pageIndex;
|
|
12
|
+
export let toStringFn = void 0;
|
|
12
13
|
let active = false;
|
|
14
|
+
let type = "string";
|
|
15
|
+
let isDate = false;
|
|
16
|
+
let dropdowns = [];
|
|
17
|
+
$values.forEach((item) => {
|
|
18
|
+
if (item) {
|
|
19
|
+
type = typeof (toFilterableValueFn ? toFilterableValueFn(item) : item);
|
|
20
|
+
if (type === "object") {
|
|
21
|
+
if ((toFilterableValueFn ? toFilterableValueFn(item) : item) instanceof Date) {
|
|
22
|
+
isDate = true;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
});
|
|
13
27
|
const options = {
|
|
14
28
|
number: [
|
|
15
29
|
{
|
|
@@ -83,35 +97,34 @@ const options = {
|
|
|
83
97
|
{
|
|
84
98
|
value: FilterOptionsEnum.b,
|
|
85
99
|
label: "Is before"
|
|
86
|
-
},
|
|
87
|
-
{
|
|
88
|
-
value: FilterOptionsEnum.no,
|
|
89
|
-
label: "Is not on"
|
|
90
100
|
}
|
|
101
|
+
// TODO: 'Not on' filter should be fixed on the server side
|
|
102
|
+
// {
|
|
103
|
+
// value: FilterOptionsEnum.no,
|
|
104
|
+
// label: 'Is not on'
|
|
105
|
+
// }
|
|
91
106
|
]
|
|
92
107
|
};
|
|
93
|
-
let dropdowns = [];
|
|
94
108
|
const popupId = `${tableId}-${id}`;
|
|
95
109
|
const popupFeatured = {
|
|
96
110
|
event: "click",
|
|
97
111
|
target: popupId,
|
|
98
112
|
placement: "bottom-start"
|
|
99
113
|
};
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
});
|
|
114
|
+
const stringValues = $values.map((item) => toStringFn ? toStringFn(item) : item);
|
|
115
|
+
const missingValues = stringValues.reduce((acc, item, index) => {
|
|
116
|
+
acc[typeof item === "string" ? item.toLowerCase() : item] = $values[index];
|
|
117
|
+
return acc;
|
|
118
|
+
}, {});
|
|
119
|
+
const getMissingValue = (value) => {
|
|
120
|
+
return Object.keys(missingValues).includes(value.toLowerCase()) ? missingValues[value.toLowerCase()] : value;
|
|
121
|
+
};
|
|
112
122
|
const optionChangeHandler = (e, index) => {
|
|
113
123
|
delete $filters[id][dropdowns[index].option];
|
|
114
|
-
$filters[id] = {
|
|
124
|
+
$filters[id] = {
|
|
125
|
+
...$filters[id],
|
|
126
|
+
[e.target.value]: getMissingValue(dropdowns[index].value)
|
|
127
|
+
};
|
|
115
128
|
$filters = $filters;
|
|
116
129
|
dropdowns[index] = {
|
|
117
130
|
...dropdowns[index],
|
|
@@ -121,11 +134,14 @@ const optionChangeHandler = (e, index) => {
|
|
|
121
134
|
const valueChangeHandler = (e, index) => {
|
|
122
135
|
dropdowns[index] = {
|
|
123
136
|
...dropdowns[index],
|
|
124
|
-
value: type === "
|
|
137
|
+
value: type === "date" ? new Date(e.target.value) : e.target.value
|
|
125
138
|
};
|
|
126
139
|
$filters = {
|
|
127
140
|
...$filters,
|
|
128
|
-
[id]: {
|
|
141
|
+
[id]: {
|
|
142
|
+
...$filters[id],
|
|
143
|
+
[dropdowns[index].option]: type === "number" ? parseFloat(getMissingValue(e.target.value)) || void 0 : getMissingValue(e.target.value)
|
|
144
|
+
}
|
|
129
145
|
};
|
|
130
146
|
};
|
|
131
147
|
const addFilter = (option, value) => {
|
|
@@ -165,6 +181,8 @@ $:
|
|
|
165
181
|
);
|
|
166
182
|
$:
|
|
167
183
|
addFilter(options[type][0].value, void 0);
|
|
184
|
+
$:
|
|
185
|
+
console.log($filters);
|
|
168
186
|
</script>
|
|
169
187
|
|
|
170
188
|
<form class="">
|
|
@@ -222,14 +240,14 @@ $:
|
|
|
222
240
|
{/if}
|
|
223
241
|
</div>
|
|
224
242
|
|
|
225
|
-
{#if type === 'number'}
|
|
243
|
+
<!-- {#if type === 'number'}
|
|
226
244
|
<input
|
|
227
245
|
type="number"
|
|
228
246
|
class="input p-1 border border-primary-500"
|
|
229
247
|
on:input={(e) => valueChangeHandler(e, index)}
|
|
230
248
|
bind:value={dropdown.value}
|
|
231
|
-
/>
|
|
232
|
-
{
|
|
249
|
+
/> -->
|
|
250
|
+
{#if type === 'number' || type === 'string'}
|
|
233
251
|
<input
|
|
234
252
|
type="text"
|
|
235
253
|
class="input p-1 border border-primary-500"
|
|
@@ -25,7 +25,7 @@ export declare const normalizeFilters: (filters: {
|
|
|
25
25
|
}) => Filter[];
|
|
26
26
|
export declare const exportAsCsv: (tableId: string, exportedData: string) => void;
|
|
27
27
|
export declare const resetResize: (headerRows: any, pageRows: any, tableId: string, columns: Columns | undefined, resizable: 'none' | 'rows' | 'columns' | 'both') => void;
|
|
28
|
-
export declare const missingValuesFn: (key: number, missingValues: {
|
|
28
|
+
export declare const missingValuesFn: (key: number | string, missingValues: {
|
|
29
29
|
[key: string | number]: string;
|
|
30
|
-
}) => string;
|
|
31
|
-
export declare const convertServerColumns: (
|
|
30
|
+
}) => string | number;
|
|
31
|
+
export declare const convertServerColumns: (serverColumns: ServerColumn[], columns: Columns | undefined) => Columns;
|
|
@@ -89,29 +89,66 @@ export const resetResize = (headerRows, pageRows, tableId, columns, resizable) =
|
|
|
89
89
|
}
|
|
90
90
|
};
|
|
91
91
|
export const missingValuesFn = (key, missingValues) => {
|
|
92
|
-
|
|
92
|
+
const foundKey = typeof key === 'number' && key.toString().includes('e')
|
|
93
|
+
? Object.keys(missingValues).find((item) => {
|
|
94
|
+
return item.toLowerCase() === key.toString().toLowerCase();
|
|
95
|
+
})
|
|
96
|
+
: typeof key === 'string' && parseInt(key).toString().length !== key.length && new Date(key)
|
|
97
|
+
? Object.keys(missingValues).find((item) => new Date(item).getTime() === new Date(key).getTime())
|
|
98
|
+
: key in missingValues
|
|
99
|
+
? key
|
|
100
|
+
: undefined;
|
|
101
|
+
return foundKey ? missingValues[foundKey] : key;
|
|
93
102
|
};
|
|
94
|
-
export const convertServerColumns = (columns) => {
|
|
103
|
+
export const convertServerColumns = (serverColumns, columns) => {
|
|
95
104
|
const columnsConfig = {};
|
|
96
|
-
|
|
105
|
+
serverColumns.forEach((col) => {
|
|
97
106
|
let instructions = {};
|
|
98
107
|
if (col.instructions?.displayPattern) {
|
|
108
|
+
let dp = col.instructions.displayPattern;
|
|
109
|
+
// Swap 'm' and 'M' to match the backend date format
|
|
110
|
+
for (let i = 0; i < col.instructions.displayPattern.length; i++) {
|
|
111
|
+
if (col.instructions.displayPattern[i] === 'm') {
|
|
112
|
+
dp = `${dp.slice(0, i)}M${dp.slice(i + 1)}`;
|
|
113
|
+
}
|
|
114
|
+
else if (col.instructions.displayPattern[i] === 'M') {
|
|
115
|
+
dp = `${dp.slice(0, i)}m${dp.slice(i + 1)}`;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
99
118
|
instructions = {
|
|
100
|
-
toStringFn: (date) =>
|
|
101
|
-
|
|
102
|
-
|
|
119
|
+
toStringFn: (date) => {
|
|
120
|
+
if (col.instructions?.missingValues) {
|
|
121
|
+
const missingValue = missingValuesFn(date, col.instructions?.missingValues || {});
|
|
122
|
+
if (missingValue === date) {
|
|
123
|
+
return dateFormat(new Date(date), dp);
|
|
124
|
+
}
|
|
125
|
+
return missingValue;
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
return dateFormat(new Date(date), dp);
|
|
129
|
+
}
|
|
130
|
+
},
|
|
131
|
+
toSortableValueFn: (date) => new Date(date).getTime(),
|
|
132
|
+
toFilterableValueFn: (date) => new Date(date)
|
|
103
133
|
};
|
|
104
134
|
}
|
|
105
|
-
if (col.instructions?.missingValues) {
|
|
135
|
+
else if (col.instructions?.missingValues) {
|
|
106
136
|
instructions = {
|
|
107
137
|
...instructions,
|
|
108
138
|
toStringFn: (key) => missingValuesFn(key, col.instructions?.missingValues || {})
|
|
109
139
|
};
|
|
110
140
|
}
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
141
|
+
if (columns && col.column in columns) {
|
|
142
|
+
columnsConfig[col.column] = {
|
|
143
|
+
...columns[col.column],
|
|
144
|
+
instructions
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
else {
|
|
148
|
+
columnsConfig[col.column] = {
|
|
149
|
+
instructions
|
|
150
|
+
};
|
|
151
|
+
}
|
|
115
152
|
});
|
|
116
153
|
return columnsConfig;
|
|
117
154
|
};
|
package/dist/models/Models.d.ts
CHANGED
|
@@ -85,6 +85,7 @@ export interface TableConfig<T> {
|
|
|
85
85
|
data: Writable<T[]>;
|
|
86
86
|
resizable?: 'none' | 'rows' | 'columns' | 'both';
|
|
87
87
|
toggle?: boolean;
|
|
88
|
+
search?: boolean;
|
|
88
89
|
fitToScreen?: boolean;
|
|
89
90
|
height?: null | number;
|
|
90
91
|
rowHeight?: number;
|
|
@@ -131,6 +132,7 @@ export interface notificationStoreType {
|
|
|
131
132
|
}
|
|
132
133
|
export type ServerColumn = {
|
|
133
134
|
column: string;
|
|
135
|
+
key: string;
|
|
134
136
|
exclude?: boolean;
|
|
135
137
|
instructions?: {
|
|
136
138
|
missingValues?: {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bexis2/bexis2-core-ui",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.6",
|
|
4
4
|
"private": false,
|
|
5
5
|
"scripts": {
|
|
6
6
|
"dev": "vite dev",
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
"@skeletonlabs/tw-plugin": "^0.3.1",
|
|
26
26
|
"@sveltejs/adapter-auto": "^3.2.0",
|
|
27
27
|
"@sveltejs/adapter-static": "^3.0.1",
|
|
28
|
+
"@sveltejs/package": "^2.3.1",
|
|
28
29
|
"@tailwindcss/forms": "^0.5.7",
|
|
29
30
|
"@tailwindcss/typography": "^0.5.12",
|
|
30
31
|
"@types/node": "^20.12.5",
|
|
@@ -39,7 +40,6 @@
|
|
|
39
40
|
"raw-loader": "^4.0.2",
|
|
40
41
|
"svelte": "^4.2.12",
|
|
41
42
|
"svelte-check": "^3.6.9",
|
|
42
|
-
"@sveltejs/package": "^2.3.1",
|
|
43
43
|
"tailwindcss": "^3.4.3",
|
|
44
44
|
"tslib": "^2.6.2",
|
|
45
45
|
"typescript": "^5.4.4",
|
|
@@ -69,6 +69,8 @@
|
|
|
69
69
|
"@fortawesome/fontawesome-svg-core": "^6.5.2",
|
|
70
70
|
"@fortawesome/free-regular-svg-icons": "^6.5.2",
|
|
71
71
|
"@fortawesome/free-solid-svg-icons": "^6.5.2",
|
|
72
|
+
"@sveltejs/kit": "^2.5.5",
|
|
73
|
+
"@sveltejs/vite-plugin-svelte": "^3.1.0",
|
|
72
74
|
"axios": "^1.6.8",
|
|
73
75
|
"codemirror": "^6.0.1",
|
|
74
76
|
"dateformat": "^5.0.3",
|
|
@@ -83,7 +85,6 @@
|
|
|
83
85
|
"svelte-file-dropzone": "^2.0.7",
|
|
84
86
|
"svelte-headless-table": "^0.18.2",
|
|
85
87
|
"svelte-select": "5.8.3",
|
|
86
|
-
"@sveltejs/kit": "^2.5.5",
|
|
87
88
|
"vest": "^5.2.12"
|
|
88
89
|
},
|
|
89
90
|
"author": "David Schöne",
|
|
@@ -3,9 +3,17 @@
|
|
|
3
3
|
import type { TableConfig } from '$models/Models';
|
|
4
4
|
|
|
5
5
|
export let config: TableConfig<any>;
|
|
6
|
+
|
|
7
|
+
let fetched = false;
|
|
6
8
|
const data = config.data;
|
|
9
|
+
|
|
10
|
+
$: if ($data.length > 0) fetched = true;
|
|
7
11
|
</script>
|
|
8
12
|
|
|
9
|
-
{#key
|
|
10
|
-
<Table
|
|
13
|
+
{#key fetched}
|
|
14
|
+
<Table
|
|
15
|
+
{config}
|
|
16
|
+
on:fetch={(columns) => (config = { ...config, columns: columns.detail })}
|
|
17
|
+
on:action
|
|
18
|
+
/>
|
|
11
19
|
{/key}
|
|
@@ -33,8 +33,8 @@
|
|
|
33
33
|
resetResize,
|
|
34
34
|
convertServerColumns
|
|
35
35
|
} from './shared';
|
|
36
|
-
import { Receive, Send } from '$
|
|
37
|
-
import type { TableConfig } from '$
|
|
36
|
+
import { Receive, Send } from '$models/Models';
|
|
37
|
+
import type { TableConfig } from '$models/Models';
|
|
38
38
|
import type { FilterOptionsEnum } from '$models/Enums';
|
|
39
39
|
|
|
40
40
|
export let config: TableConfig<any>;
|
|
@@ -50,6 +50,7 @@
|
|
|
50
50
|
optionsComponent, // Custom component to render in the last column
|
|
51
51
|
defaultPageSize = 10, // Default page size - number of rows to display per page
|
|
52
52
|
toggle = false, // Whether to display the fitToScreen toggle
|
|
53
|
+
search = true, // Whether to display the search input
|
|
53
54
|
pageSizes = [5, 10, 15, 20], // Page sizes to display in the pagination component
|
|
54
55
|
fitToScreen = true, // Whether to fit the table to the screen,
|
|
55
56
|
exportable = false, // Whether to display the export button and enable export functionality
|
|
@@ -58,7 +59,7 @@
|
|
|
58
59
|
token = '', // Bearer token to authenticate the request
|
|
59
60
|
sendModel = new Send(), // Model to send requests
|
|
60
61
|
entityId = 0, // Entity ID to send with the request
|
|
61
|
-
versionId = 0 // Version ID to send with the request
|
|
62
|
+
versionId = 0 // Version ID to send with the request,
|
|
62
63
|
} = config;
|
|
63
64
|
|
|
64
65
|
let searchValue = '';
|
|
@@ -154,7 +155,7 @@
|
|
|
154
155
|
// Render the cell with the provided component, or use the toStringFn if provided, or just use the value
|
|
155
156
|
cell: ({ value, row }) => {
|
|
156
157
|
return renderComponent
|
|
157
|
-
? createRender(renderComponent, { value, row })
|
|
158
|
+
? createRender(renderComponent, { value, row, dispatchFn: actionDispatcher })
|
|
158
159
|
: toStringFn
|
|
159
160
|
? toStringFn(value)
|
|
160
161
|
: value;
|
|
@@ -188,7 +189,8 @@
|
|
|
188
189
|
updateTable,
|
|
189
190
|
pageIndex,
|
|
190
191
|
toFilterableValueFn,
|
|
191
|
-
filters
|
|
192
|
+
filters,
|
|
193
|
+
toStringFn
|
|
192
194
|
})
|
|
193
195
|
: createRender(colFilterComponent ?? TableFilter, {
|
|
194
196
|
filterValue,
|
|
@@ -318,11 +320,26 @@
|
|
|
318
320
|
|
|
319
321
|
// Format server columns to the client columns
|
|
320
322
|
if (response.columns !== undefined) {
|
|
321
|
-
columns = convertServerColumns(response.columns);
|
|
323
|
+
columns = convertServerColumns(response.columns, columns);
|
|
324
|
+
|
|
325
|
+
const clientCols = response.columns.reduce((acc, col) => {
|
|
326
|
+
acc[col.key] = col.column;
|
|
327
|
+
return acc;
|
|
328
|
+
}, {});
|
|
329
|
+
|
|
330
|
+
const tmpArr: any[] = [];
|
|
331
|
+
|
|
332
|
+
response.data.forEach((row, index) => {
|
|
333
|
+
const tmp: { [key: string]: any } = {};
|
|
334
|
+
Object.keys(row).forEach((key) => {
|
|
335
|
+
tmp[clientCols[key]] = row[key];
|
|
336
|
+
});
|
|
337
|
+
tmpArr.push(tmp);
|
|
338
|
+
});
|
|
339
|
+
dispatch('fetch', columns);
|
|
340
|
+
$data = tmpArr;
|
|
322
341
|
}
|
|
323
342
|
|
|
324
|
-
// Update data store
|
|
325
|
-
$data = response.data;
|
|
326
343
|
$serverItems = response.count;
|
|
327
344
|
|
|
328
345
|
return response;
|
|
@@ -348,87 +365,92 @@
|
|
|
348
365
|
</script>
|
|
349
366
|
|
|
350
367
|
<div class="grid gap-2 overflow-auto" class:w-fit={!fitToScreen} class:w-full={fitToScreen}>
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
$filterValue = searchValue;
|
|
359
|
-
}}
|
|
360
|
-
>
|
|
361
|
-
<div class="relative w-full flex items-center">
|
|
362
|
-
<input
|
|
363
|
-
class="input p-2 border border-primary-500"
|
|
364
|
-
type="text"
|
|
365
|
-
bind:value={searchValue}
|
|
366
|
-
placeholder="Search rows..."
|
|
367
|
-
id="{tableId}-search"
|
|
368
|
-
/><button
|
|
369
|
-
type="reset"
|
|
370
|
-
class="absolute right-3 items-center"
|
|
371
|
-
on:click|preventDefault={() => {
|
|
372
|
-
searchValue = '';
|
|
373
|
-
sendModel.q = '';
|
|
374
|
-
$filterValue = '';
|
|
375
|
-
}}><Fa icon={faXmark} /></button
|
|
376
|
-
>
|
|
377
|
-
</div>
|
|
378
|
-
<button
|
|
379
|
-
type="submit"
|
|
380
|
-
class="btn variant-filled-primary"
|
|
381
|
-
on:click|preventDefault={() => {
|
|
382
|
-
$filterValue = searchValue;
|
|
368
|
+
{#if $data.length > 0 || (columns && Object.keys(columns).length > 0)}
|
|
369
|
+
<div class="table-container">
|
|
370
|
+
<!-- Enable the search filter if table is not empty -->
|
|
371
|
+
{#if !serverSide && search}
|
|
372
|
+
<form
|
|
373
|
+
class="flex gap-2"
|
|
374
|
+
on:submit|preventDefault={() => {
|
|
383
375
|
sendModel.q = searchValue;
|
|
384
|
-
|
|
376
|
+
$filterValue = searchValue;
|
|
377
|
+
}}
|
|
385
378
|
>
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
{#if resizable !== 'none'}
|
|
405
|
-
<button
|
|
406
|
-
type="button"
|
|
407
|
-
class="btn btn-sm variant-filled-primary rounded-full order-last"
|
|
408
|
-
on:click|preventDefault={() =>
|
|
409
|
-
resetResize($headerRows, $pageRows, tableId, columns, resizable)}>Reset sizing</button
|
|
410
|
-
>
|
|
411
|
-
{/if}
|
|
412
|
-
{#if exportable}
|
|
379
|
+
<div class="relative w-full flex items-center">
|
|
380
|
+
<input
|
|
381
|
+
class="input p-2 border border-primary-500"
|
|
382
|
+
type="text"
|
|
383
|
+
bind:value={searchValue}
|
|
384
|
+
placeholder="Search rows..."
|
|
385
|
+
id="{tableId}-search"
|
|
386
|
+
/><button
|
|
387
|
+
type="reset"
|
|
388
|
+
id="{tableId}-searchReset"
|
|
389
|
+
class="absolute right-3 items-center"
|
|
390
|
+
on:click|preventDefault={() => {
|
|
391
|
+
searchValue = '';
|
|
392
|
+
sendModel.q = '';
|
|
393
|
+
$filterValue = '';
|
|
394
|
+
}}><Fa icon={faXmark} /></button
|
|
395
|
+
>
|
|
396
|
+
</div>
|
|
413
397
|
<button
|
|
414
|
-
type="
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
398
|
+
type="submit"
|
|
399
|
+
id="{tableId}-searchSubmit"
|
|
400
|
+
class="btn variant-filled-primary"
|
|
401
|
+
on:click|preventDefault={() => {
|
|
402
|
+
$filterValue = searchValue;
|
|
403
|
+
sendModel.q = searchValue;
|
|
404
|
+
}}>Search</button
|
|
418
405
|
>
|
|
419
|
-
|
|
406
|
+
</form>
|
|
407
|
+
{/if}
|
|
408
|
+
|
|
409
|
+
<div class="flex justify-between items-center w-full {search && 'py-2'}">
|
|
410
|
+
<div>
|
|
411
|
+
<!-- Enable the fitToScreen toggle if toggle === true -->
|
|
412
|
+
{#if toggle}
|
|
413
|
+
<SlideToggle
|
|
414
|
+
name="slider-label"
|
|
415
|
+
active="bg-primary-500"
|
|
416
|
+
size="sm"
|
|
417
|
+
checked={fitToScreen}
|
|
418
|
+
id="{tableId}-toggle"
|
|
419
|
+
on:change={() => (fitToScreen = !fitToScreen)}>Fit to screen</SlideToggle
|
|
420
|
+
>
|
|
421
|
+
{/if}
|
|
422
|
+
</div>
|
|
423
|
+
<div class="flex gap-2">
|
|
424
|
+
<!-- Enable the resetResize button if resizable !== 'none' -->
|
|
425
|
+
{#if resizable !== 'none'}
|
|
426
|
+
<button
|
|
427
|
+
type="button"
|
|
428
|
+
class="btn btn-sm variant-filled-primary rounded-full order-last"
|
|
429
|
+
on:click|preventDefault={() =>
|
|
430
|
+
resetResize($headerRows, $pageRows, tableId, columns, resizable)}
|
|
431
|
+
>Reset sizing</button
|
|
432
|
+
>
|
|
433
|
+
{/if}
|
|
434
|
+
{#if exportable}
|
|
435
|
+
<button
|
|
436
|
+
type="button"
|
|
437
|
+
class="btn btn-sm variant-filled-primary rounded-full order-last"
|
|
438
|
+
on:click|preventDefault={() => exportAsCsv(tableId, $exportedData)}
|
|
439
|
+
>Export as CSV</button
|
|
440
|
+
>
|
|
441
|
+
{/if}
|
|
442
|
+
</div>
|
|
420
443
|
</div>
|
|
421
|
-
</div>
|
|
422
444
|
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
445
|
+
<div class="overflow-auto" style="height: {height}px">
|
|
446
|
+
<table
|
|
447
|
+
{...$tableAttrs}
|
|
448
|
+
class="table table-auto table-compact bg-tertiary-500/30 dark:bg-tertiary-900/10 overflow-clip"
|
|
449
|
+
id="{tableId}-table"
|
|
450
|
+
>
|
|
451
|
+
<!-- If table height is provided, making the top row sticky -->
|
|
452
|
+
<thead class={height != null && $pageRows.length > 0 ? `sticky top-0` : ''}>
|
|
453
|
+
<!-- {#if $data.length > 0} -->
|
|
432
454
|
{#each $headerRows as headerRow (headerRow.id)}
|
|
433
455
|
<Subscribe
|
|
434
456
|
rowAttrs={headerRow.attrs()}
|
|
@@ -436,7 +458,7 @@
|
|
|
436
458
|
rowProps={headerRow.props()}
|
|
437
459
|
let:rowProps
|
|
438
460
|
>
|
|
439
|
-
<tr {...rowAttrs} class="bg-primary-300 dark:bg-primary-
|
|
461
|
+
<tr {...rowAttrs} class="bg-primary-300 dark:bg-primary-800">
|
|
440
462
|
{#each headerRow.cells as cell (cell.id)}
|
|
441
463
|
<Subscribe attrs={cell.attrs()} props={cell.props()} let:props let:attrs>
|
|
442
464
|
<th scope="col" class="!p-2" {...attrs} style={cellStyle(cell.id, columns)}>
|
|
@@ -482,16 +504,9 @@
|
|
|
482
504
|
</tr>
|
|
483
505
|
</Subscribe>
|
|
484
506
|
{/each}
|
|
485
|
-
|
|
486
|
-
<div class="p-10"><Spinner /></div>
|
|
487
|
-
{:else}
|
|
488
|
-
<!-- Table is empty -->
|
|
489
|
-
<p class="items-center justify-center flex w-full p-10 italic">Nothing to show here.</p>
|
|
490
|
-
{/if}
|
|
491
|
-
</thead>
|
|
507
|
+
</thead>
|
|
492
508
|
|
|
493
|
-
|
|
494
|
-
{#if $data.length > 0}
|
|
509
|
+
<tbody class="overflow-auto" {...$tableBodyAttrs}>
|
|
495
510
|
{#each $pageRows as row (row.id)}
|
|
496
511
|
<Subscribe rowAttrs={row.attrs()} let:rowAttrs>
|
|
497
512
|
<tr {...rowAttrs} id="{tableId}-row-{row.id}" class="">
|
|
@@ -519,13 +534,22 @@
|
|
|
519
534
|
</tr>
|
|
520
535
|
</Subscribe>
|
|
521
536
|
{/each}
|
|
522
|
-
|
|
523
|
-
</
|
|
524
|
-
</
|
|
537
|
+
</tbody>
|
|
538
|
+
</table>
|
|
539
|
+
</div>
|
|
525
540
|
</div>
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
541
|
+
{:else}
|
|
542
|
+
<div class="p-10 w-full h-full flex justify-center items-center bg-neutral-200 rounded">
|
|
543
|
+
<p>No data available</p>
|
|
544
|
+
</div>
|
|
545
|
+
{/if}
|
|
546
|
+
|
|
547
|
+
{#if isFetching}
|
|
548
|
+
<div class="p-10 w-full h-full flex justify-center items-center"><Spinner /></div>
|
|
549
|
+
{/if}
|
|
550
|
+
|
|
551
|
+
<!-- Adding pagination, if table is not empty -->
|
|
552
|
+
{#if $data.length > 0 || (columns && Object.keys(columns).length > 0)}
|
|
529
553
|
{#if serverSide}
|
|
530
554
|
<TablePaginationServer
|
|
531
555
|
{pageIndex}
|
|
@@ -30,7 +30,9 @@
|
|
|
30
30
|
type = typeof (toFilterableValueFn ? toFilterableValueFn(item) : item);
|
|
31
31
|
|
|
32
32
|
if (type === 'object') {
|
|
33
|
-
if (item instanceof Date) {
|
|
33
|
+
if (toFilterableValueFn && toFilterableValueFn(item) instanceof Date) {
|
|
34
|
+
isDate = true;
|
|
35
|
+
} else if (item instanceof Date) {
|
|
34
36
|
isDate = true;
|
|
35
37
|
}
|
|
36
38
|
}
|
|
@@ -13,9 +13,29 @@
|
|
|
13
13
|
export let filters;
|
|
14
14
|
export let updateTable;
|
|
15
15
|
export let pageIndex;
|
|
16
|
+
export let toStringFn: undefined | ((value: any) => string) = undefined;
|
|
16
17
|
|
|
17
18
|
// If the filter is applied and the displayed values are filtered
|
|
18
19
|
let active = false;
|
|
20
|
+
let type: string = 'string';
|
|
21
|
+
let isDate = false;
|
|
22
|
+
let dropdowns: {
|
|
23
|
+
option: FilterOptionsEnum;
|
|
24
|
+
value: string | number | Date | undefined;
|
|
25
|
+
}[] = [];
|
|
26
|
+
|
|
27
|
+
// Check the type of the column
|
|
28
|
+
$values.forEach((item) => {
|
|
29
|
+
if (item) {
|
|
30
|
+
type = typeof (toFilterableValueFn ? toFilterableValueFn(item) : item);
|
|
31
|
+
|
|
32
|
+
if (type === 'object') {
|
|
33
|
+
if ((toFilterableValueFn ? toFilterableValueFn(item) : item) instanceof Date) {
|
|
34
|
+
isDate = true;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
});
|
|
19
39
|
|
|
20
40
|
// Options for different types of values
|
|
21
41
|
const options = {
|
|
@@ -92,18 +112,14 @@
|
|
|
92
112
|
value: FilterOptionsEnum.b,
|
|
93
113
|
label: 'Is before'
|
|
94
114
|
},
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
115
|
+
// TODO: 'Not on' filter should be fixed on the server side
|
|
116
|
+
// {
|
|
117
|
+
// value: FilterOptionsEnum.no,
|
|
118
|
+
// label: 'Is not on'
|
|
119
|
+
// }
|
|
99
120
|
]
|
|
100
121
|
};
|
|
101
122
|
|
|
102
|
-
let dropdowns: {
|
|
103
|
-
option: FilterOptionsEnum;
|
|
104
|
-
value: string | number | Date | undefined;
|
|
105
|
-
}[] = [];
|
|
106
|
-
|
|
107
123
|
// Unique ID for the column filter popup
|
|
108
124
|
const popupId = `${tableId}-${id}`;
|
|
109
125
|
// Popup config
|
|
@@ -113,24 +129,25 @@
|
|
|
113
129
|
placement: 'bottom-start'
|
|
114
130
|
};
|
|
115
131
|
|
|
116
|
-
|
|
117
|
-
let isDate = false;
|
|
118
|
-
// Check the type of the column
|
|
119
|
-
$values.forEach((item) => {
|
|
120
|
-
if (item) {
|
|
121
|
-
type = typeof (toFilterableValueFn ? toFilterableValueFn(item) : item);
|
|
132
|
+
const stringValues = $values.map((item) => (toStringFn ? toStringFn(item) : item));
|
|
122
133
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
134
|
+
const missingValues = stringValues.reduce((acc, item, index) => {
|
|
135
|
+
acc[typeof item === 'string' ? item.toLowerCase() : item] = $values[index];
|
|
136
|
+
return acc;
|
|
137
|
+
}, {});
|
|
138
|
+
|
|
139
|
+
const getMissingValue = (value: string) => {
|
|
140
|
+
return Object.keys(missingValues).includes(value.toLowerCase())
|
|
141
|
+
? missingValues[value.toLowerCase()]
|
|
142
|
+
: value;
|
|
143
|
+
};
|
|
130
144
|
|
|
131
145
|
const optionChangeHandler = (e, index) => {
|
|
132
146
|
delete $filters[id][dropdowns[index].option];
|
|
133
|
-
$filters[id] = {
|
|
147
|
+
$filters[id] = {
|
|
148
|
+
...$filters[id],
|
|
149
|
+
[e.target.value]: getMissingValue(dropdowns[index].value as string)
|
|
150
|
+
};
|
|
134
151
|
$filters = $filters;
|
|
135
152
|
|
|
136
153
|
dropdowns[index] = {
|
|
@@ -142,17 +159,18 @@
|
|
|
142
159
|
const valueChangeHandler = (e, index) => {
|
|
143
160
|
dropdowns[index] = {
|
|
144
161
|
...dropdowns[index],
|
|
145
|
-
value:
|
|
146
|
-
type === 'number'
|
|
147
|
-
? +e.target.value
|
|
148
|
-
: type === 'date'
|
|
149
|
-
? new Date(e.target.value)
|
|
150
|
-
: e.target.value
|
|
162
|
+
value: type === 'date' ? new Date(e.target.value) : e.target.value
|
|
151
163
|
};
|
|
152
164
|
|
|
153
165
|
$filters = {
|
|
154
166
|
...$filters,
|
|
155
|
-
[id]: {
|
|
167
|
+
[id]: {
|
|
168
|
+
...$filters[id],
|
|
169
|
+
[dropdowns[index].option]:
|
|
170
|
+
type === 'number'
|
|
171
|
+
? parseFloat(getMissingValue(e.target.value)) || undefined
|
|
172
|
+
: getMissingValue(e.target.value)
|
|
173
|
+
}
|
|
156
174
|
};
|
|
157
175
|
};
|
|
158
176
|
|
|
@@ -201,6 +219,8 @@
|
|
|
201
219
|
|
|
202
220
|
// Start by adding the default filter
|
|
203
221
|
$: addFilter(options[type][0].value, undefined);
|
|
222
|
+
|
|
223
|
+
$: console.log($filters);
|
|
204
224
|
</script>
|
|
205
225
|
|
|
206
226
|
<form class="">
|
|
@@ -258,14 +278,14 @@
|
|
|
258
278
|
{/if}
|
|
259
279
|
</div>
|
|
260
280
|
|
|
261
|
-
{#if type === 'number'}
|
|
281
|
+
<!-- {#if type === 'number'}
|
|
262
282
|
<input
|
|
263
283
|
type="number"
|
|
264
284
|
class="input p-1 border border-primary-500"
|
|
265
285
|
on:input={(e) => valueChangeHandler(e, index)}
|
|
266
286
|
bind:value={dropdown.value}
|
|
267
|
-
/>
|
|
268
|
-
{
|
|
287
|
+
/> -->
|
|
288
|
+
{#if type === 'number' || type === 'string'}
|
|
269
289
|
<input
|
|
270
290
|
type="text"
|
|
271
291
|
class="input p-1 border border-primary-500"
|
|
@@ -107,35 +107,79 @@ export const resetResize = (
|
|
|
107
107
|
}
|
|
108
108
|
};
|
|
109
109
|
|
|
110
|
-
export const missingValuesFn = (
|
|
111
|
-
|
|
110
|
+
export const missingValuesFn = (
|
|
111
|
+
key: number | string,
|
|
112
|
+
missingValues: { [key: string | number]: string }
|
|
113
|
+
) => {
|
|
114
|
+
const foundKey =
|
|
115
|
+
typeof key === 'number' && key.toString().includes('e')
|
|
116
|
+
? Object.keys(missingValues).find((item) => {
|
|
117
|
+
return (item as string).toLowerCase() === key.toString().toLowerCase();
|
|
118
|
+
})
|
|
119
|
+
: typeof key === 'string' && parseInt(key).toString().length !== key.length && new Date(key)
|
|
120
|
+
? Object.keys(missingValues).find(
|
|
121
|
+
(item) => new Date(item).getTime() === new Date(key).getTime()
|
|
122
|
+
)
|
|
123
|
+
: key in missingValues
|
|
124
|
+
? key
|
|
125
|
+
: undefined;
|
|
126
|
+
|
|
127
|
+
return foundKey ? missingValues[foundKey] : key;
|
|
112
128
|
};
|
|
113
129
|
|
|
114
|
-
export const convertServerColumns = (
|
|
130
|
+
export const convertServerColumns = (
|
|
131
|
+
serverColumns: ServerColumn[],
|
|
132
|
+
columns: Columns | undefined
|
|
133
|
+
) => {
|
|
115
134
|
const columnsConfig: Columns = {};
|
|
116
135
|
|
|
117
|
-
|
|
136
|
+
serverColumns.forEach((col) => {
|
|
118
137
|
let instructions = {};
|
|
119
138
|
|
|
120
139
|
if (col.instructions?.displayPattern) {
|
|
140
|
+
let dp = col.instructions.displayPattern;
|
|
141
|
+
|
|
142
|
+
// Swap 'm' and 'M' to match the backend date format
|
|
143
|
+
for (let i = 0; i < col.instructions.displayPattern.length; i++) {
|
|
144
|
+
if (col.instructions.displayPattern[i] === 'm') {
|
|
145
|
+
dp = `${dp.slice(0, i)}M${dp.slice(i + 1)}`;
|
|
146
|
+
} else if (col.instructions.displayPattern[i] === 'M') {
|
|
147
|
+
dp = `${dp.slice(0, i)}m${dp.slice(i + 1)}`;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
121
151
|
instructions = {
|
|
122
|
-
toStringFn: (date:
|
|
123
|
-
|
|
124
|
-
|
|
152
|
+
toStringFn: (date: string) => {
|
|
153
|
+
if (col.instructions?.missingValues) {
|
|
154
|
+
const missingValue = missingValuesFn(date, col.instructions?.missingValues || {});
|
|
155
|
+
if (missingValue === date) {
|
|
156
|
+
return dateFormat(new Date(date), dp);
|
|
157
|
+
}
|
|
158
|
+
return missingValue;
|
|
159
|
+
} else {
|
|
160
|
+
return dateFormat(new Date(date), dp);
|
|
161
|
+
}
|
|
162
|
+
},
|
|
163
|
+
toSortableValueFn: (date: string) => new Date(date).getTime(),
|
|
164
|
+
toFilterableValueFn: (date: string) => new Date(date)
|
|
125
165
|
};
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
if (col.instructions?.missingValues) {
|
|
166
|
+
} else if (col.instructions?.missingValues) {
|
|
129
167
|
instructions = {
|
|
130
168
|
...instructions,
|
|
131
169
|
toStringFn: (key) => missingValuesFn(key, col.instructions?.missingValues || {})
|
|
132
170
|
};
|
|
133
171
|
}
|
|
134
172
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
173
|
+
if (columns && col.column in columns) {
|
|
174
|
+
columnsConfig[col.column] = {
|
|
175
|
+
...columns[col.column],
|
|
176
|
+
instructions
|
|
177
|
+
};
|
|
178
|
+
} else {
|
|
179
|
+
columnsConfig[col.column] = {
|
|
180
|
+
instructions
|
|
181
|
+
};
|
|
182
|
+
}
|
|
139
183
|
});
|
|
140
184
|
|
|
141
185
|
return columnsConfig;
|
package/src/lib/models/Models.ts
CHANGED
|
@@ -110,6 +110,7 @@ export interface TableConfig<T> {
|
|
|
110
110
|
data: Writable<T[]>;
|
|
111
111
|
resizable?: 'none' | 'rows' | 'columns' | 'both'; // none by default
|
|
112
112
|
toggle?: boolean; // false by default
|
|
113
|
+
search?: boolean; // true by default
|
|
113
114
|
fitToScreen?: boolean; // true by default
|
|
114
115
|
height?: null | number; // null by default
|
|
115
116
|
rowHeight?: number; // auto by default
|
|
@@ -167,7 +168,8 @@ export interface notificationStoreType {
|
|
|
167
168
|
|
|
168
169
|
// Table column type for server-side table
|
|
169
170
|
export type ServerColumn = {
|
|
170
|
-
column: string;
|
|
171
|
+
column: string; // column name on client side
|
|
172
|
+
key: string; // column name received from the server
|
|
171
173
|
exclude?: boolean; // false by default
|
|
172
174
|
instructions?: {
|
|
173
175
|
missingValues?: { [key: string | number]: string };
|