@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 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 $data.length && true}
7
- <Table {config} on:action />
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
- <div class="table-container">
298
- <!-- Enable the search filter if table is not empty -->
299
- {#if $data.length > 0}
300
- <form
301
- class="flex gap-2"
302
- on:submit|preventDefault={() => {
303
- sendModel.q = searchValue;
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
- }}>Search</button
321
+ $filterValue = searchValue;
322
+ }}
331
323
  >
332
- </form>
333
- {/if}
334
- <div class="flex justify-between items-center py-2 w-full">
335
- <div>
336
- <!-- Enable the fitToScreen toggle if toggle === true -->
337
- {#if toggle}
338
- <SlideToggle
339
- name="slider-label"
340
- active="bg-primary-500"
341
- size="sm"
342
- checked={fitToScreen}
343
- id="{tableId}-toggle"
344
- on:change={() => (fitToScreen = !fitToScreen)}>Fit to screen</SlideToggle
345
- >
346
- {/if}
347
- </div>
348
- <div class="flex gap-2">
349
- <!-- Enable the resetResize button if resizable !== 'none' -->
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="button"
353
- class="btn btn-sm variant-filled-primary rounded-full order-last"
354
- on:click|preventDefault={() =>
355
- resetResize($headerRows, $pageRows, tableId, columns, resizable)}>Reset sizing</button
356
- >
357
- {/if}
358
- {#if exportable}
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
- {/if}
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
- <div class="overflow-auto" style="height: {height}px">
370
- <table
371
- {...$tableAttrs}
372
- class="table table-auto table-compact bg-tertiary-500/30 dark:bg-tertiary-900/10 overflow-clip"
373
- id="{tableId}-table"
374
- >
375
- <!-- If table height is provided, making the top row sticky -->
376
- <thead class=" {height != null ? `sticky top-0` : ''}">
377
- {#if $data.length > 0}
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-500">
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
- {:else if isFetching}
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
- <tbody class="overflow-auto" {...$tableBodyAttrs}>
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
- {/if}
469
- </tbody>
470
- </table>
482
+ </tbody>
483
+ </table>
484
+ </div>
471
485
  </div>
472
- </div>
473
- {#if $data.length > 0}
474
- <!-- Adding pagination, if table is not empty -->
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}
@@ -6,6 +6,7 @@ declare const __propDef: {
6
6
  };
7
7
  events: {
8
8
  action: CustomEvent<any>;
9
+ fetch: CustomEvent<any>;
9
10
  } & {
10
11
  [evt: string]: CustomEvent<any>;
11
12
  };
@@ -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
- let type = "string";
101
- let isDate = false;
102
- $values.forEach((item) => {
103
- if (item) {
104
- type = typeof (toFilterableValueFn ? toFilterableValueFn(item) : item);
105
- if (type === "object") {
106
- if (item instanceof Date) {
107
- isDate = true;
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] = { ...$filters[id], [e.target.value]: dropdowns[index].value };
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 === "number" ? +e.target.value : type === "date" ? new Date(e.target.value) : e.target.value
137
+ value: type === "date" ? new Date(e.target.value) : e.target.value
125
138
  };
126
139
  $filters = {
127
140
  ...$filters,
128
- [id]: { ...$filters[id], [dropdowns[index].option]: dropdowns[index].value }
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
- {:else if type === 'string'}
249
+ /> -->
250
+ {#if type === 'number' || type === 'string'}
233
251
  <input
234
252
  type="text"
235
253
  class="input p-1 border border-primary-500"
@@ -8,6 +8,7 @@ declare const __propDef: {
8
8
  filters: any;
9
9
  updateTable: any;
10
10
  pageIndex: any;
11
+ toStringFn?: undefined | ((value: any) => string);
11
12
  };
12
13
  events: {
13
14
  [evt: string]: CustomEvent<any>;
@@ -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: (columns: ServerColumn[]) => Columns;
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
- return key in missingValues ? missingValues[key] : key.toString();
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
- columns.forEach((col) => {
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) => dateFormat(date, col.instructions?.displayPattern || ''),
101
- toSortableValueFn: (date) => date.getTime(),
102
- toFilterableValueFn: (date) => date
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
- columnsConfig[col.column] = {
112
- exclude: col.exclude,
113
- instructions
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
  };
@@ -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.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 $data.length && true}
10
- <Table {config} on:action />
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 '$lib/models/Models';
37
- import type { TableConfig } from '$lib/models/Models';
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
- <div class="table-container">
352
- <!-- Enable the search filter if table is not empty -->
353
- {#if $data.length > 0}
354
- <form
355
- class="flex gap-2"
356
- on:submit|preventDefault={() => {
357
- sendModel.q = searchValue;
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
- }}>Search</button
376
+ $filterValue = searchValue;
377
+ }}
385
378
  >
386
- </form>
387
- {/if}
388
- <div class="flex justify-between items-center py-2 w-full">
389
- <div>
390
- <!-- Enable the fitToScreen toggle if toggle === true -->
391
- {#if toggle}
392
- <SlideToggle
393
- name="slider-label"
394
- active="bg-primary-500"
395
- size="sm"
396
- checked={fitToScreen}
397
- id="{tableId}-toggle"
398
- on:change={() => (fitToScreen = !fitToScreen)}>Fit to screen</SlideToggle
399
- >
400
- {/if}
401
- </div>
402
- <div class="flex gap-2">
403
- <!-- Enable the resetResize button if resizable !== 'none' -->
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="button"
415
- class="btn btn-sm variant-filled-primary rounded-full order-last"
416
- on:click|preventDefault={() => exportAsCsv(tableId, $exportedData)}
417
- >Export as CSV</button
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
- {/if}
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
- <div class="overflow-auto" style="height: {height}px">
424
- <table
425
- {...$tableAttrs}
426
- class="table table-auto table-compact bg-tertiary-500/30 dark:bg-tertiary-900/10 overflow-clip"
427
- id="{tableId}-table"
428
- >
429
- <!-- If table height is provided, making the top row sticky -->
430
- <thead class=" {height != null ? `sticky top-0` : ''}">
431
- {#if $data.length > 0}
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-500">
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
- {:else if isFetching}
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
- <tbody class="overflow-auto" {...$tableBodyAttrs}>
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
- {/if}
523
- </tbody>
524
- </table>
537
+ </tbody>
538
+ </table>
539
+ </div>
525
540
  </div>
526
- </div>
527
- {#if $data.length > 0}
528
- <!-- Adding pagination, if table is not empty -->
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
- value: FilterOptionsEnum.no,
97
- label: 'Is not on'
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
- let type: string = 'string';
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
- if (type === 'object') {
124
- if (item instanceof Date) {
125
- isDate = true;
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] = { ...$filters[id], [e.target.value]: dropdowns[index].value };
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]: { ...$filters[id], [dropdowns[index].option]: dropdowns[index].value }
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
- {:else if type === 'string'}
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 = (key: number, missingValues: { [key: string | number]: string }) => {
111
- return key in missingValues ? missingValues[key] : key.toString();
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 = (columns: ServerColumn[]) => {
130
+ export const convertServerColumns = (
131
+ serverColumns: ServerColumn[],
132
+ columns: Columns | undefined
133
+ ) => {
115
134
  const columnsConfig: Columns = {};
116
135
 
117
- columns.forEach((col) => {
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: Date) => dateFormat(date, col.instructions?.displayPattern || ''),
123
- toSortableValueFn: (date: Date) => date.getTime(),
124
- toFilterableValueFn: (date: Date) => date
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
- columnsConfig[col.column] = {
136
- exclude: col.exclude,
137
- instructions
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;
@@ -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 };