@bexis2/bexis2-core-ui 0.4.3 → 0.4.5

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,15 @@
1
1
  # bexis-core-ui
2
+ ## 0.4.5
3
+ - table
4
+ - adds searching for missing values in the filters #744
5
+ - missing values for every data type
6
+ - display patterns for date/time/datetime types
7
+
8
+ ## 0.4.4
9
+ - update libs
10
+ - remove eslint-plugin-svelte3
11
+ - update svelte, sveltekit, typescript, tailwind ...
12
+
2
13
  ## 0.4.3
3
14
  - table
4
15
  - Enable searching on server-side
@@ -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}
@@ -66,7 +66,7 @@ let {
66
66
  entityId = 0,
67
67
  // Entity ID to send with the request
68
68
  versionId = 0
69
- // Version ID to send with the request
69
+ // Version ID to send with the request,
70
70
  } = config;
71
71
  let searchValue = "";
72
72
  let isFetching = false;
@@ -162,7 +162,8 @@ const tableColumns = [
162
162
  updateTable,
163
163
  pageIndex,
164
164
  toFilterableValueFn,
165
- filters
165
+ filters,
166
+ toStringFn
166
167
  }) : createRender(colFilterComponent ?? TableFilter, {
167
168
  filterValue: filterValue2,
168
169
  id,
@@ -270,9 +271,23 @@ const updateTable = async () => {
270
271
  }
271
272
  const response = await fetchData.json();
272
273
  if (response.columns !== void 0) {
273
- columns = convertServerColumns(response.columns);
274
+ console.log(response);
275
+ columns = convertServerColumns(response.columns, columns);
276
+ const clientCols = response.columns.reduce((acc, col) => {
277
+ acc[col.key] = col.column;
278
+ return acc;
279
+ }, {});
280
+ const tmpArr = [];
281
+ response.data.forEach((row, index) => {
282
+ const tmp = {};
283
+ Object.keys(row).forEach((key) => {
284
+ tmp[clientCols[key]] = row[key];
285
+ });
286
+ tmpArr.push(tmp);
287
+ });
288
+ dispatch("fetch", columns);
289
+ $data = tmpArr;
274
290
  }
275
- $data = response.data;
276
291
  $serverItems = response.count;
277
292
  return response;
278
293
  };
@@ -294,87 +309,90 @@ $:
294
309
  </script>
295
310
 
296
311
  <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;
312
+ {#if $data.length > 0 || (columns && Object.keys(columns).length > 0)}
313
+ <div class="table-container">
314
+ <!-- Enable the search filter if table is not empty -->
315
+ {#if !serverSide}
316
+ <form
317
+ class="flex gap-2"
318
+ on:submit|preventDefault={() => {
329
319
  sendModel.q = searchValue;
330
- }}>Search</button
320
+ $filterValue = searchValue;
321
+ }}
331
322
  >
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'}
323
+ <div class="relative w-full flex items-center">
324
+ <input
325
+ class="input p-2 border border-primary-500"
326
+ type="text"
327
+ bind:value={searchValue}
328
+ placeholder="Search rows..."
329
+ id="{tableId}-search"
330
+ /><button
331
+ type="reset"
332
+ class="absolute right-3 items-center"
333
+ on:click|preventDefault={() => {
334
+ searchValue = '';
335
+ sendModel.q = '';
336
+ $filterValue = '';
337
+ }}><Fa icon={faXmark} /></button
338
+ >
339
+ </div>
351
340
  <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
341
+ type="submit"
342
+ class="btn variant-filled-primary"
343
+ on:click|preventDefault={() => {
344
+ $filterValue = searchValue;
345
+ sendModel.q = searchValue;
346
+ }}>Search</button
364
347
  >
365
- {/if}
348
+ </form>
349
+ {/if}
350
+
351
+ <div class="flex justify-between items-center py-2 w-full">
352
+ <div>
353
+ <!-- Enable the fitToScreen toggle if toggle === true -->
354
+ {#if toggle}
355
+ <SlideToggle
356
+ name="slider-label"
357
+ active="bg-primary-500"
358
+ size="sm"
359
+ checked={fitToScreen}
360
+ id="{tableId}-toggle"
361
+ on:change={() => (fitToScreen = !fitToScreen)}>Fit to screen</SlideToggle
362
+ >
363
+ {/if}
364
+ </div>
365
+ <div class="flex gap-2">
366
+ <!-- Enable the resetResize button if resizable !== 'none' -->
367
+ {#if resizable !== 'none'}
368
+ <button
369
+ type="button"
370
+ class="btn btn-sm variant-filled-primary rounded-full order-last"
371
+ on:click|preventDefault={() =>
372
+ resetResize($headerRows, $pageRows, tableId, columns, resizable)}
373
+ >Reset sizing</button
374
+ >
375
+ {/if}
376
+ {#if exportable}
377
+ <button
378
+ type="button"
379
+ class="btn btn-sm variant-filled-primary rounded-full order-last"
380
+ on:click|preventDefault={() => exportAsCsv(tableId, $exportedData)}
381
+ >Export as CSV</button
382
+ >
383
+ {/if}
384
+ </div>
366
385
  </div>
367
- </div>
368
386
 
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}
387
+ <div class="overflow-auto" style="height: {height}px">
388
+ <table
389
+ {...$tableAttrs}
390
+ class="table table-auto table-compact bg-tertiary-500/30 dark:bg-tertiary-900/10 overflow-clip"
391
+ id="{tableId}-table"
392
+ >
393
+ <!-- If table height is provided, making the top row sticky -->
394
+ <thead class={height != null && $pageRows.length > 0 ? `sticky top-0` : ''}>
395
+ <!-- {#if $data.length > 0} -->
378
396
  {#each $headerRows as headerRow (headerRow.id)}
379
397
  <Subscribe
380
398
  rowAttrs={headerRow.attrs()}
@@ -382,7 +400,7 @@ $:
382
400
  rowProps={headerRow.props()}
383
401
  let:rowProps
384
402
  >
385
- <tr {...rowAttrs} class="bg-primary-300 dark:bg-primary-500">
403
+ <tr {...rowAttrs} class="bg-primary-300 dark:bg-primary-800">
386
404
  {#each headerRow.cells as cell (cell.id)}
387
405
  <Subscribe attrs={cell.attrs()} props={cell.props()} let:props let:attrs>
388
406
  <th scope="col" class="!p-2" {...attrs} style={cellStyle(cell.id, columns)}>
@@ -428,16 +446,9 @@ $:
428
446
  </tr>
429
447
  </Subscribe>
430
448
  {/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>
449
+ </thead>
438
450
 
439
- <tbody class="overflow-auto" {...$tableBodyAttrs}>
440
- {#if $data.length > 0}
451
+ <tbody class="overflow-auto" {...$tableBodyAttrs}>
441
452
  {#each $pageRows as row (row.id)}
442
453
  <Subscribe rowAttrs={row.attrs()} let:rowAttrs>
443
454
  <tr {...rowAttrs} id="{tableId}-row-{row.id}" class="">
@@ -465,13 +476,22 @@ $:
465
476
  </tr>
466
477
  </Subscribe>
467
478
  {/each}
468
- {/if}
469
- </tbody>
470
- </table>
479
+ </tbody>
480
+ </table>
481
+ </div>
471
482
  </div>
472
- </div>
473
- {#if $data.length > 0}
474
- <!-- Adding pagination, if table is not empty -->
483
+ {:else}
484
+ <div class="p-10 w-full h-full flex justify-center items-center bg-neutral-200 rounded">
485
+ <p>No data available</p>
486
+ </div>
487
+ {/if}
488
+
489
+ {#if isFetching}
490
+ <div class="p-10 w-full h-full flex justify-center items-center"><Spinner /></div>
491
+ {/if}
492
+
493
+ <!-- Adding pagination, if table is not empty -->
494
+ {#if $data.length > 0 || (columns && Object.keys(columns).length > 0)}
475
495
  {#if serverSide}
476
496
  <TablePaginationServer
477
497
  {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
  };
@@ -4,8 +4,8 @@ declare const __propDef: {
4
4
  values: any;
5
5
  id: any;
6
6
  tableId: any;
7
- toFilterableValueFn?: ((value: any) => any) | undefined;
8
- toStringFn?: ((value: any) => string) | undefined;
7
+ toFilterableValueFn?: undefined | ((value: any) => any);
8
+ toStringFn?: undefined | ((value: any) => string);
9
9
  filterValue: any;
10
10
  filters: any;
11
11
  pageIndex: any;
@@ -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"
@@ -4,10 +4,11 @@ declare const __propDef: {
4
4
  values: any;
5
5
  id: any;
6
6
  tableId: any;
7
- toFilterableValueFn?: ((value: any) => any) | undefined;
7
+ toFilterableValueFn?: undefined | ((value: any) => any);
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,8 +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: {
29
- [key: string]: string;
30
- [key: number]: string;
31
- }) => string;
32
- export declare const convertServerColumns: (columns: ServerColumn[]) => Columns;
28
+ export declare const missingValuesFn: (key: number | string, missingValues: {
29
+ [key: string | number]: string;
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
  };
@@ -131,6 +131,7 @@ export interface notificationStoreType {
131
131
  }
132
132
  export type ServerColumn = {
133
133
  column: string;
134
+ key: string;
134
135
  exclude?: boolean;
135
136
  instructions?: {
136
137
  missingValues?: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bexis2/bexis2-core-ui",
3
- "version": "0.4.3",
3
+ "version": "0.4.5",
4
4
  "private": false,
5
5
  "scripts": {
6
6
  "dev": "vite dev",
@@ -20,33 +20,31 @@
20
20
  "3.npm - publish": "npm publish --access public"
21
21
  },
22
22
  "devDependencies": {
23
- "@playwright/test": "^1.42.1",
23
+ "@playwright/test": "^1.43.0",
24
24
  "@skeletonlabs/skeleton": "^2.9.0",
25
25
  "@skeletonlabs/tw-plugin": "^0.3.1",
26
- "@sveltejs/adapter-auto": "^3.1.1",
26
+ "@sveltejs/adapter-auto": "^3.2.0",
27
27
  "@sveltejs/adapter-static": "^3.0.1",
28
- "@sveltejs/kit": "^2.5.3",
29
- "@sveltejs/package": "^2.3.0",
30
28
  "@tailwindcss/forms": "^0.5.7",
31
- "@tailwindcss/typography": "^0.5.10",
32
- "@types/node": "^20.11.26",
33
- "@typescript-eslint/eslint-plugin": "^7.2.0",
34
- "@typescript-eslint/parser": "^7.2.0",
35
- "autoprefixer": "^10.4.18",
36
- "eslint": "^8.57.0",
37
- "eslint-config-prettier": "^9.1.0",
38
- "eslint-plugin-svelte3": "^4.0.0",
39
- "postcss": "^8.4.35",
29
+ "@tailwindcss/typography": "^0.5.12",
30
+ "@types/node": "^20.12.5",
31
+ "@typescript-eslint/eslint-plugin": "^7.5.0",
32
+ "@typescript-eslint/parser": "^7.5.0",
33
+ "autoprefixer": "^10.4.19",
34
+ "eslint": "^8.0.0",
35
+ "eslint-config-prettier": "^8.0.0",
36
+ "postcss": "^8.4.38",
40
37
  "prettier": "^3.2.5",
41
38
  "prettier-plugin-svelte": "^3.2.2",
42
39
  "raw-loader": "^4.0.2",
43
40
  "svelte": "^4.2.12",
44
- "svelte-check": "^3.6.7",
45
- "tailwindcss": "^3.4.1",
41
+ "svelte-check": "^3.6.9",
42
+ "@sveltejs/package": "^2.3.1",
43
+ "tailwindcss": "^3.4.3",
46
44
  "tslib": "^2.6.2",
47
- "typescript": "^5.4.2",
48
- "vite": "^5.1.6",
49
- "vitest": "^1.3.1"
45
+ "typescript": "^5.4.4",
46
+ "vite": "^5.2.8",
47
+ "vitest": "^1.4.0"
50
48
  },
51
49
  "type": "module",
52
50
  "module": "./src/lib/index.ts",
@@ -67,11 +65,11 @@
67
65
  "@codemirror/lint": "^6.5.0",
68
66
  "@codemirror/theme-one-dark": "^6.1.2",
69
67
  "@floating-ui/dom": "^1.6.3",
70
- "@fortawesome/fontawesome-free": "^6.5.1",
71
- "@fortawesome/fontawesome-svg-core": "^6.5.1",
72
- "@fortawesome/free-regular-svg-icons": "^6.5.1",
73
- "@fortawesome/free-solid-svg-icons": "^6.5.1",
74
- "axios": "^1.6.7",
68
+ "@fortawesome/fontawesome-free": "^6.5.2",
69
+ "@fortawesome/fontawesome-svg-core": "^6.5.2",
70
+ "@fortawesome/free-regular-svg-icons": "^6.5.2",
71
+ "@fortawesome/free-solid-svg-icons": "^6.5.2",
72
+ "axios": "^1.6.8",
75
73
  "codemirror": "^6.0.1",
76
74
  "dateformat": "^5.0.3",
77
75
  "delay": "^6.0.0",
@@ -82,10 +80,11 @@
82
80
  "svelte": "^4.2.12",
83
81
  "svelte-codemirror-editor": "^1.3.0",
84
82
  "svelte-fa": "^4.0.2",
85
- "svelte-file-dropzone": "^2.0.4",
83
+ "svelte-file-dropzone": "^2.0.7",
86
84
  "svelte-headless-table": "^0.18.2",
87
85
  "svelte-select": "5.8.3",
88
- "vest": "^5.2.10"
86
+ "@sveltejs/kit": "^2.5.5",
87
+ "vest": "^5.2.12"
89
88
  },
90
89
  "author": "David Schöne",
91
90
  "license": "ISC",
@@ -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>;
@@ -58,7 +58,7 @@
58
58
  token = '', // Bearer token to authenticate the request
59
59
  sendModel = new Send(), // Model to send requests
60
60
  entityId = 0, // Entity ID to send with the request
61
- versionId = 0 // Version ID to send with the request
61
+ versionId = 0 // Version ID to send with the request,
62
62
  } = config;
63
63
 
64
64
  let searchValue = '';
@@ -188,7 +188,8 @@
188
188
  updateTable,
189
189
  pageIndex,
190
190
  toFilterableValueFn,
191
- filters
191
+ filters,
192
+ toStringFn
192
193
  })
193
194
  : createRender(colFilterComponent ?? TableFilter, {
194
195
  filterValue,
@@ -318,11 +319,28 @@
318
319
 
319
320
  // Format server columns to the client columns
320
321
  if (response.columns !== undefined) {
321
- columns = convertServerColumns(response.columns);
322
+ console.log(response);
323
+
324
+ columns = convertServerColumns(response.columns, columns);
325
+
326
+ const clientCols = response.columns.reduce((acc, col) => {
327
+ acc[col.key] = col.column;
328
+ return acc;
329
+ }, {});
330
+
331
+ const tmpArr: any[] = [];
332
+
333
+ response.data.forEach((row, index) => {
334
+ const tmp: { [key: string]: any } = {};
335
+ Object.keys(row).forEach((key) => {
336
+ tmp[clientCols[key]] = row[key];
337
+ });
338
+ tmpArr.push(tmp);
339
+ });
340
+ dispatch('fetch', columns);
341
+ $data = tmpArr;
322
342
  }
323
343
 
324
- // Update data store
325
- $data = response.data;
326
344
  $serverItems = response.count;
327
345
 
328
346
  return response;
@@ -348,87 +366,90 @@
348
366
  </script>
349
367
 
350
368
  <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;
369
+ {#if $data.length > 0 || (columns && Object.keys(columns).length > 0)}
370
+ <div class="table-container">
371
+ <!-- Enable the search filter if table is not empty -->
372
+ {#if !serverSide}
373
+ <form
374
+ class="flex gap-2"
375
+ on:submit|preventDefault={() => {
383
376
  sendModel.q = searchValue;
384
- }}>Search</button
377
+ $filterValue = searchValue;
378
+ }}
385
379
  >
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}
380
+ <div class="relative w-full flex items-center">
381
+ <input
382
+ class="input p-2 border border-primary-500"
383
+ type="text"
384
+ bind:value={searchValue}
385
+ placeholder="Search rows..."
386
+ id="{tableId}-search"
387
+ /><button
388
+ type="reset"
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
+ class="btn variant-filled-primary"
400
+ on:click|preventDefault={() => {
401
+ $filterValue = searchValue;
402
+ sendModel.q = searchValue;
403
+ }}>Search</button
418
404
  >
419
- {/if}
405
+ </form>
406
+ {/if}
407
+
408
+ <div class="flex justify-between items-center py-2 w-full">
409
+ <div>
410
+ <!-- Enable the fitToScreen toggle if toggle === true -->
411
+ {#if toggle}
412
+ <SlideToggle
413
+ name="slider-label"
414
+ active="bg-primary-500"
415
+ size="sm"
416
+ checked={fitToScreen}
417
+ id="{tableId}-toggle"
418
+ on:change={() => (fitToScreen = !fitToScreen)}>Fit to screen</SlideToggle
419
+ >
420
+ {/if}
421
+ </div>
422
+ <div class="flex gap-2">
423
+ <!-- Enable the resetResize button if resizable !== 'none' -->
424
+ {#if resizable !== 'none'}
425
+ <button
426
+ type="button"
427
+ class="btn btn-sm variant-filled-primary rounded-full order-last"
428
+ on:click|preventDefault={() =>
429
+ resetResize($headerRows, $pageRows, tableId, columns, resizable)}
430
+ >Reset sizing</button
431
+ >
432
+ {/if}
433
+ {#if exportable}
434
+ <button
435
+ type="button"
436
+ class="btn btn-sm variant-filled-primary rounded-full order-last"
437
+ on:click|preventDefault={() => exportAsCsv(tableId, $exportedData)}
438
+ >Export as CSV</button
439
+ >
440
+ {/if}
441
+ </div>
420
442
  </div>
421
- </div>
422
443
 
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}
444
+ <div class="overflow-auto" style="height: {height}px">
445
+ <table
446
+ {...$tableAttrs}
447
+ class="table table-auto table-compact bg-tertiary-500/30 dark:bg-tertiary-900/10 overflow-clip"
448
+ id="{tableId}-table"
449
+ >
450
+ <!-- If table height is provided, making the top row sticky -->
451
+ <thead class={height != null && $pageRows.length > 0 ? `sticky top-0` : ''}>
452
+ <!-- {#if $data.length > 0} -->
432
453
  {#each $headerRows as headerRow (headerRow.id)}
433
454
  <Subscribe
434
455
  rowAttrs={headerRow.attrs()}
@@ -436,7 +457,7 @@
436
457
  rowProps={headerRow.props()}
437
458
  let:rowProps
438
459
  >
439
- <tr {...rowAttrs} class="bg-primary-300 dark:bg-primary-500">
460
+ <tr {...rowAttrs} class="bg-primary-300 dark:bg-primary-800">
440
461
  {#each headerRow.cells as cell (cell.id)}
441
462
  <Subscribe attrs={cell.attrs()} props={cell.props()} let:props let:attrs>
442
463
  <th scope="col" class="!p-2" {...attrs} style={cellStyle(cell.id, columns)}>
@@ -482,16 +503,9 @@
482
503
  </tr>
483
504
  </Subscribe>
484
505
  {/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>
506
+ </thead>
492
507
 
493
- <tbody class="overflow-auto" {...$tableBodyAttrs}>
494
- {#if $data.length > 0}
508
+ <tbody class="overflow-auto" {...$tableBodyAttrs}>
495
509
  {#each $pageRows as row (row.id)}
496
510
  <Subscribe rowAttrs={row.attrs()} let:rowAttrs>
497
511
  <tr {...rowAttrs} id="{tableId}-row-{row.id}" class="">
@@ -519,13 +533,22 @@
519
533
  </tr>
520
534
  </Subscribe>
521
535
  {/each}
522
- {/if}
523
- </tbody>
524
- </table>
536
+ </tbody>
537
+ </table>
538
+ </div>
539
+ </div>
540
+ {:else}
541
+ <div class="p-10 w-full h-full flex justify-center items-center bg-neutral-200 rounded">
542
+ <p>No data available</p>
525
543
  </div>
526
- </div>
527
- {#if $data.length > 0}
528
- <!-- Adding pagination, if table is not empty -->
544
+ {/if}
545
+
546
+ {#if isFetching}
547
+ <div class="p-10 w-full h-full flex justify-center items-center"><Spinner /></div>
548
+ {/if}
549
+
550
+ <!-- Adding pagination, if table is not empty -->
551
+ {#if $data.length > 0 || (columns && Object.keys(columns).length > 0)}
529
552
  {#if serverSide}
530
553
  <TablePaginationServer
531
554
  {pageIndex}
@@ -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;
@@ -167,7 +167,8 @@ export interface notificationStoreType {
167
167
 
168
168
  // Table column type for server-side table
169
169
  export type ServerColumn = {
170
- column: string;
170
+ column: string; // column name on client side
171
+ key: string; // column name received from the server
171
172
  exclude?: boolean; // false by default
172
173
  instructions?: {
173
174
  missingValues?: { [key: string | number]: string };