@bexis2/bexis2-core-ui 0.4.0 → 0.4.1

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,17 @@
1
1
  # bexis-core-ui
2
+ ## 0.4.1
3
+ - table
4
+ - Server-side searching
5
+ - Case-insensitive filtering of missing values
6
+ - Date formatting for display patterns
7
+ - Handle missing values regardless of the data type
8
+
9
+ ## 0.4.0
10
+ - update dependency libaries
11
+ - svelte
12
+ - sveltekit
13
+ - vite
14
+
2
15
  ## 0.3.13
3
16
  ## 0.3.12
4
17
  - table
@@ -14,6 +14,7 @@ import {
14
14
  import { computePosition, autoUpdate, offset, shift, flip, arrow } from "@floating-ui/dom";
15
15
  import { SlideToggle, storePopup } from "@skeletonlabs/skeleton";
16
16
  storePopup.set({ computePosition, autoUpdate, offset, shift, flip, arrow });
17
+ import Spinner from "../page/Spinner.svelte";
17
18
  import TableFilter from "./TableFilter.svelte";
18
19
  import TableFilterServer from "./TableFilterServer.svelte";
19
20
  import TablePagination from "./TablePagination.svelte";
@@ -68,6 +69,7 @@ let {
68
69
  // Version ID to send with the request
69
70
  } = config;
70
71
  let searchValue = "";
72
+ let isFetching = false;
71
73
  const filters = writable({});
72
74
  const dispatch = createEventDispatcher();
73
75
  const actionDispatcher = (obj) => dispatch("action", obj);
@@ -167,7 +169,9 @@ const tableColumns = [
167
169
  tableId,
168
170
  values,
169
171
  toFilterableValueFn,
170
- filters
172
+ filters,
173
+ toStringFn,
174
+ pageIndex
171
175
  });
172
176
  }
173
177
  } : void 0,
@@ -205,7 +209,8 @@ const tableColumns = [
205
209
  id,
206
210
  tableId,
207
211
  values,
208
- filters
212
+ filters,
213
+ pageIndex
209
214
  });
210
215
  }
211
216
  }
@@ -244,14 +249,25 @@ const updateTable = async () => {
244
249
  sendModel.version = versionId;
245
250
  sendModel.id = entityId;
246
251
  sendModel.filter = normalizeFilters($filters);
247
- const fetchData = await fetch(URL, {
248
- headers: {
249
- "Content-Type": "application/json",
250
- Authorization: `Bearer ${token}`
251
- },
252
- method: "POST",
253
- body: JSON.stringify(sendModel)
254
- });
252
+ let fetchData;
253
+ try {
254
+ isFetching = true;
255
+ fetchData = await fetch(URL, {
256
+ headers: {
257
+ "Content-Type": "application/json",
258
+ Authorization: `Bearer ${token}`
259
+ },
260
+ method: "POST",
261
+ body: JSON.stringify(sendModel)
262
+ });
263
+ } catch (error) {
264
+ throw new Error(`Network error: ${error.message}`);
265
+ } finally {
266
+ isFetching = false;
267
+ }
268
+ if (!fetchData.ok) {
269
+ throw new Error("Failed to fetch data");
270
+ }
255
271
  const response = await fetchData.json();
256
272
  if (response.columns !== void 0) {
257
273
  columns = convertServerColumns(response.columns);
@@ -282,7 +298,13 @@ $:
282
298
  <!-- Enable the search filter if table is not empty -->
283
299
  {#if $data.length > 0}
284
300
  {#if !serverSide}
285
- <div class="flex gap-2">
301
+ <form
302
+ class="flex gap-2"
303
+ on:submit|preventDefault={() => {
304
+ sendModel.q = searchValue;
305
+ $filterValue = searchValue;
306
+ }}
307
+ >
286
308
  <div class="relative w-full flex items-center">
287
309
  <input
288
310
  class="input p-2 border border-primary-500"
@@ -295,18 +317,20 @@ $:
295
317
  class="absolute right-3 items-center"
296
318
  on:click|preventDefault={() => {
297
319
  searchValue = '';
320
+ sendModel.q = '';
298
321
  $filterValue = '';
299
322
  }}><Fa icon={faXmark} /></button
300
323
  >
301
324
  </div>
302
325
  <button
303
- type="button"
326
+ type="submit"
304
327
  class="btn variant-filled-primary"
305
328
  on:click|preventDefault={() => {
306
329
  $filterValue = searchValue;
330
+ sendModel.q = searchValue;
307
331
  }}>Search</button
308
332
  >
309
- </div>
333
+ </form>
310
334
  {/if}
311
335
  <div class="flex justify-between items-center py-2 w-full">
312
336
  <div>
@@ -407,6 +431,8 @@ $:
407
431
  </tr>
408
432
  </Subscribe>
409
433
  {/each}
434
+ {:else if isFetching}
435
+ <div class="p-10"><Spinner /></div>
410
436
  {:else}
411
437
  <!-- Table is empty -->
412
438
  <p class="items-center justify-center flex w-full p-10 italic">Nothing to show here.</p>
@@ -6,9 +6,24 @@ export let values;
6
6
  export let id;
7
7
  export let tableId;
8
8
  export let toFilterableValueFn = void 0;
9
+ export let toStringFn = void 0;
9
10
  export let filterValue;
10
11
  export let filters;
12
+ export let pageIndex;
11
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 (item instanceof Date) {
22
+ isDate = true;
23
+ }
24
+ }
25
+ }
26
+ });
12
27
  const options = {
13
28
  number: [
14
29
  {
@@ -89,28 +104,37 @@ const options = {
89
104
  }
90
105
  ]
91
106
  };
92
- let dropdowns = [];
93
107
  const popupId = `${tableId}-${id}`;
94
108
  const popupFeatured = {
95
109
  event: "click",
96
110
  target: popupId,
97
111
  placement: "bottom-start"
98
112
  };
99
- let type = "string";
100
- let isDate = false;
101
- $values.forEach((item) => {
102
- if (item) {
103
- type = typeof (toFilterableValueFn ? toFilterableValueFn(item) : item);
104
- if (type === "object") {
105
- if (item instanceof Date) {
106
- isDate = true;
107
- }
108
- }
109
- }
110
- });
113
+ const stringValues = (
114
+ // type === 'number' ?
115
+ $values.map((item) => toStringFn ? toStringFn(item) : item)
116
+ );
117
+ const missingValues = (
118
+ // type === 'number' ?
119
+ stringValues.reduce((acc, item, index) => {
120
+ acc[typeof item === "string" ? item.toLowerCase() : item] = $values[index];
121
+ return acc;
122
+ }, {})
123
+ );
124
+ const getMissingValue = (value) => {
125
+ return Object.keys(missingValues).includes(value.toLowerCase()) ? missingValues[value.toLowerCase()] : value;
126
+ };
111
127
  const optionChangeHandler = (e, index) => {
112
128
  delete $filters[id][dropdowns[index].option];
113
- $filters[id] = { ...$filters[id], [e.target.value]: dropdowns[index].value };
129
+ $filters[id] = {
130
+ ...$filters[id],
131
+ [e.target.value]: (
132
+ // type === 'number'
133
+ // ?
134
+ getMissingValue(dropdowns[index].value)
135
+ )
136
+ // : dropdowns[index].value
137
+ };
114
138
  $filters = $filters;
115
139
  dropdowns[index] = {
116
140
  ...dropdowns[index],
@@ -120,11 +144,18 @@ const optionChangeHandler = (e, index) => {
120
144
  const valueChangeHandler = (e, index) => {
121
145
  dropdowns[index] = {
122
146
  ...dropdowns[index],
123
- value: type === "number" ? +e.target.value : type === "date" ? new Date(e.target.value) : e.target.value
147
+ value: type === "date" ? new Date(e.target.value) : e.target.value
124
148
  };
125
149
  $filters = {
126
150
  ...$filters,
127
- [id]: { ...$filters[id], [dropdowns[index].option]: dropdowns[index].value }
151
+ [id]: {
152
+ ...$filters[id],
153
+ [dropdowns[index].option]: (
154
+ // type === 'number' ?
155
+ getMissingValue(e.target.value)
156
+ )
157
+ // : dropdowns[index].value
158
+ }
128
159
  };
129
160
  };
130
161
  const addFilter = (option, value) => {
@@ -178,6 +209,7 @@ $:
178
209
  addFilter(options[type][0].value, undefined);
179
210
  $filterValue = $filters[id];
180
211
  active = false;
212
+ $pageIndex = 0;
181
213
  }}>Clear Filters</button
182
214
  >
183
215
 
@@ -212,14 +244,7 @@ $:
212
244
  {/if}
213
245
  </div>
214
246
 
215
- {#if type === 'number'}
216
- <input
217
- type="number"
218
- class="input p-1 border border-primary-500"
219
- on:input={(e) => valueChangeHandler(e, index)}
220
- bind:value={dropdown.value}
221
- />
222
- {:else if type === 'string'}
247
+ {#if type === 'number' || type === 'string'}
223
248
  <input
224
249
  type="text"
225
250
  class="input p-1 border border-primary-500"
@@ -258,6 +283,7 @@ $:
258
283
  class="btn variant-filled-primary btn-sm"
259
284
  type="button"
260
285
  on:click|preventDefault={() => {
286
+ $pageIndex = 0;
261
287
  $filterValue = $filters[id];
262
288
  active = true;
263
289
  }}>Apply</button
@@ -5,8 +5,10 @@ declare const __propDef: {
5
5
  id: any;
6
6
  tableId: any;
7
7
  toFilterableValueFn?: ((value: any) => any) | undefined;
8
+ toStringFn?: ((value: any) => string) | undefined;
8
9
  filterValue: any;
9
10
  filters: any;
11
+ pageIndex: any;
10
12
  };
11
13
  events: {
12
14
  [evt: string]: CustomEvent<any>;
@@ -1,3 +1,4 @@
1
+ import dateFormat from 'dateformat';
1
2
  // Function to determine minWidth for a column to simplify the logic in the HTML
2
3
  export const minWidth = (id, columns) => {
3
4
  if (columns && id in columns) {
@@ -94,14 +95,13 @@ export const convertServerColumns = (columns) => {
94
95
  const columnsConfig = {};
95
96
  columns.forEach((col) => {
96
97
  let instructions = {};
97
- // if (col.instructions?.displayPattern) {
98
- // instructions = {
99
- // toStringFn: (date: Date) =>
100
- // date.toLocaleString('en-US', col.instructions?.displayPattern || {}),
101
- // toSortableValueFn: (date: Date) => date.getTime(),
102
- // toFilterableValueFn: (date: Date) => date
103
- // };
104
- // }
98
+ if (col.instructions?.displayPattern) {
99
+ instructions = {
100
+ toStringFn: (date) => dateFormat(date, col.instructions?.displayPattern || ''),
101
+ toSortableValueFn: (date) => date.getTime(),
102
+ toFilterableValueFn: (date) => date
103
+ };
104
+ }
105
105
  if (col.instructions?.missingValues) {
106
106
  instructions = {
107
107
  ...instructions,
@@ -152,6 +152,7 @@ export declare class Send {
152
152
  id: number;
153
153
  limit: number;
154
154
  offset: number;
155
+ q: string;
155
156
  version?: number;
156
157
  filter: Filter[];
157
158
  order: OrderBy[];
@@ -2,6 +2,7 @@ export class Send {
2
2
  id;
3
3
  limit;
4
4
  offset;
5
+ q;
5
6
  version;
6
7
  filter;
7
8
  order;
@@ -10,6 +11,7 @@ export class Send {
10
11
  this.limit = 10;
11
12
  this.offset = 0;
12
13
  this.version = 0;
14
+ this.q = '';
13
15
  this.filter = [];
14
16
  this.order = [];
15
17
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bexis2/bexis2-core-ui",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
4
4
  "private": false,
5
5
  "scripts": {
6
6
  "dev": "vite dev",
@@ -73,6 +73,7 @@
73
73
  "@fortawesome/free-solid-svg-icons": "^6.5.1",
74
74
  "axios": "^1.6.7",
75
75
  "codemirror": "^6.0.1",
76
+ "dateformat": "^5.0.3",
76
77
  "delay": "^6.0.0",
77
78
  "dotenv": "^16.4.5",
78
79
  "eslint4b-prebuilt": "^6.7.2",
@@ -19,6 +19,7 @@
19
19
 
20
20
  storePopup.set({ computePosition, autoUpdate, offset, shift, flip, arrow });
21
21
 
22
+ import Spinner from '../page/Spinner.svelte';
22
23
  import TableFilter from './TableFilter.svelte';
23
24
  import TableFilterServer from './TableFilterServer.svelte';
24
25
  import TablePagination from './TablePagination.svelte';
@@ -61,6 +62,7 @@
61
62
  } = config;
62
63
 
63
64
  let searchValue = '';
65
+ let isFetching = false;
64
66
 
65
67
  const filters = writable<{
66
68
  [key: string]: { [key in FilterOptionsEnum]?: number | string | Date };
@@ -194,7 +196,9 @@
194
196
  tableId,
195
197
  values,
196
198
  toFilterableValueFn,
197
- filters
199
+ filters,
200
+ toStringFn,
201
+ pageIndex
198
202
  });
199
203
  }
200
204
  }
@@ -237,7 +241,8 @@
237
241
  id,
238
242
  tableId,
239
243
  values,
240
- filters
244
+ filters,
245
+ pageIndex
241
246
  });
242
247
  }
243
248
  }
@@ -280,7 +285,6 @@
280
285
  // Page configuration
281
286
  const { pageIndex, pageSize } = pluginStates.page;
282
287
 
283
- // TODO: Add loading animation for server-side fetch requests
284
288
  const updateTable = async () => {
285
289
  sendModel.limit = $pageSize;
286
290
  sendModel.offset = $pageSize * $pageIndex;
@@ -288,14 +292,27 @@
288
292
  sendModel.id = entityId;
289
293
  sendModel.filter = normalizeFilters($filters);
290
294
 
291
- const fetchData = await fetch(URL, {
292
- headers: {
293
- 'Content-Type': 'application/json',
294
- Authorization: `Bearer ${token}`
295
- },
296
- method: 'POST',
297
- body: JSON.stringify(sendModel)
298
- });
295
+ let fetchData;
296
+
297
+ try {
298
+ isFetching = true;
299
+ fetchData = await fetch(URL, {
300
+ headers: {
301
+ 'Content-Type': 'application/json',
302
+ Authorization: `Bearer ${token}`
303
+ },
304
+ method: 'POST',
305
+ body: JSON.stringify(sendModel)
306
+ });
307
+ } catch (error) {
308
+ throw new Error(`Network error: ${(error as Error).message}`);
309
+ } finally {
310
+ isFetching = false;
311
+ }
312
+
313
+ if (!fetchData.ok) {
314
+ throw new Error('Failed to fetch data');
315
+ }
299
316
 
300
317
  const response: Receive = await fetchData.json();
301
318
 
@@ -335,7 +352,13 @@
335
352
  <!-- Enable the search filter if table is not empty -->
336
353
  {#if $data.length > 0}
337
354
  {#if !serverSide}
338
- <div class="flex gap-2">
355
+ <form
356
+ class="flex gap-2"
357
+ on:submit|preventDefault={() => {
358
+ sendModel.q = searchValue;
359
+ $filterValue = searchValue;
360
+ }}
361
+ >
339
362
  <div class="relative w-full flex items-center">
340
363
  <input
341
364
  class="input p-2 border border-primary-500"
@@ -348,18 +371,20 @@
348
371
  class="absolute right-3 items-center"
349
372
  on:click|preventDefault={() => {
350
373
  searchValue = '';
374
+ sendModel.q = '';
351
375
  $filterValue = '';
352
376
  }}><Fa icon={faXmark} /></button
353
377
  >
354
378
  </div>
355
379
  <button
356
- type="button"
380
+ type="submit"
357
381
  class="btn variant-filled-primary"
358
382
  on:click|preventDefault={() => {
359
383
  $filterValue = searchValue;
384
+ sendModel.q = searchValue;
360
385
  }}>Search</button
361
386
  >
362
- </div>
387
+ </form>
363
388
  {/if}
364
389
  <div class="flex justify-between items-center py-2 w-full">
365
390
  <div>
@@ -460,6 +485,8 @@
460
485
  </tr>
461
486
  </Subscribe>
462
487
  {/each}
488
+ {:else if isFetching}
489
+ <div class="p-10"><Spinner /></div>
463
490
  {:else}
464
491
  <!-- Table is empty -->
465
492
  <p class="items-center justify-center flex w-full p-10 italic">Nothing to show here.</p>
@@ -10,13 +10,33 @@
10
10
  export let id;
11
11
  export let tableId;
12
12
  export let toFilterableValueFn: undefined | ((value: any) => any) = undefined;
13
+ export let toStringFn: undefined | ((value: any) => string) = undefined;
13
14
  export let filterValue;
14
15
  export let filters;
16
+ export let pageIndex;
15
17
 
16
18
  // If the filter is applied and the displayed values are filtered
17
19
  let active = false;
20
+ let type: string = 'string';
21
+ let isDate = false; // Options for different types of values
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 (item instanceof Date) {
34
+ isDate = true;
35
+ }
36
+ }
37
+ }
38
+ });
18
39
 
19
- // Options for different types of values
20
40
  const options = {
21
41
  number: [
22
42
  {
@@ -98,11 +118,6 @@
98
118
  ]
99
119
  };
100
120
 
101
- let dropdowns: {
102
- option: FilterOptionsEnum;
103
- value: string | number | Date | undefined;
104
- }[] = [];
105
-
106
121
  // Unique ID for the column filter popup
107
122
  const popupId = `${tableId}-${id}`;
108
123
  // Popup config
@@ -112,24 +127,39 @@
112
127
  placement: 'bottom-start'
113
128
  };
114
129
 
115
- let type: string = 'string';
116
- let isDate = false;
117
- // Check the type of the column
118
- $values.forEach((item) => {
119
- if (item) {
120
- type = typeof (toFilterableValueFn ? toFilterableValueFn(item) : item);
130
+ // Converted string values and missingValues mapping
131
+ const stringValues =
132
+ // type === 'number' ?
133
+ $values.map((item) => (toStringFn ? toStringFn(item) : item));
134
+ // : [];
121
135
 
122
- if (type === 'object') {
123
- if (item instanceof Date) {
124
- isDate = true;
125
- }
126
- }
127
- }
128
- });
136
+ const missingValues =
137
+ // type === 'number' ?
138
+ stringValues.reduce((acc, item, index) => {
139
+ acc[typeof item === 'string' ? item.toLowerCase() : item] = $values[index];
140
+ return acc;
141
+ }, {});
142
+ // : {};
143
+
144
+ const getMissingValue = (value: string) => {
145
+ // if (type === 'number' ||) {
146
+ return Object.keys(missingValues).includes(value.toLowerCase())
147
+ ? missingValues[value.toLowerCase()]
148
+ : value;
149
+ // }
150
+ // return value;
151
+ };
129
152
 
130
153
  const optionChangeHandler = (e, index) => {
131
154
  delete $filters[id][dropdowns[index].option];
132
- $filters[id] = { ...$filters[id], [e.target.value]: dropdowns[index].value };
155
+ $filters[id] = {
156
+ ...$filters[id],
157
+ [e.target.value]:
158
+ // type === 'number'
159
+ // ?
160
+ getMissingValue(dropdowns[index].value as string)
161
+ // : dropdowns[index].value
162
+ };
133
163
  $filters = $filters;
134
164
 
135
165
  dropdowns[index] = {
@@ -141,17 +171,18 @@
141
171
  const valueChangeHandler = (e, index) => {
142
172
  dropdowns[index] = {
143
173
  ...dropdowns[index],
144
- value:
145
- type === 'number'
146
- ? +e.target.value
147
- : type === 'date'
148
- ? new Date(e.target.value)
149
- : e.target.value
174
+ value: type === 'date' ? new Date(e.target.value) : e.target.value
150
175
  };
151
176
 
152
177
  $filters = {
153
178
  ...$filters,
154
- [id]: { ...$filters[id], [dropdowns[index].option]: dropdowns[index].value }
179
+ [id]: {
180
+ ...$filters[id],
181
+ [dropdowns[index].option]:
182
+ // type === 'number' ?
183
+ getMissingValue(e.target.value)
184
+ // : dropdowns[index].value
185
+ }
155
186
  };
156
187
  };
157
188
 
@@ -212,6 +243,7 @@
212
243
  addFilter(options[type][0].value, undefined);
213
244
  $filterValue = $filters[id];
214
245
  active = false;
246
+ $pageIndex = 0;
215
247
  }}>Clear Filters</button
216
248
  >
217
249
 
@@ -246,14 +278,7 @@
246
278
  {/if}
247
279
  </div>
248
280
 
249
- {#if type === 'number'}
250
- <input
251
- type="number"
252
- class="input p-1 border border-primary-500"
253
- on:input={(e) => valueChangeHandler(e, index)}
254
- bind:value={dropdown.value}
255
- />
256
- {:else if type === 'string'}
281
+ {#if type === 'number' || type === 'string'}
257
282
  <input
258
283
  type="text"
259
284
  class="input p-1 border border-primary-500"
@@ -292,6 +317,7 @@
292
317
  class="btn variant-filled-primary btn-sm"
293
318
  type="button"
294
319
  on:click|preventDefault={() => {
320
+ $pageIndex = 0;
295
321
  $filterValue = $filters[id];
296
322
  active = true;
297
323
  }}>Apply</button
@@ -1,3 +1,5 @@
1
+ import dateFormat from 'dateformat';
2
+
1
3
  import type { FilterOptionsEnum } from '$models/Enums';
2
4
  import type { Columns, Filter, ServerColumn } from '$models/Models';
3
5
 
@@ -115,14 +117,13 @@ export const convertServerColumns = (columns: ServerColumn[]) => {
115
117
  columns.forEach((col) => {
116
118
  let instructions = {};
117
119
 
118
- // if (col.instructions?.displayPattern) {
119
- // instructions = {
120
- // toStringFn: (date: Date) =>
121
- // date.toLocaleString('en-US', col.instructions?.displayPattern || {}),
122
- // toSortableValueFn: (date: Date) => date.getTime(),
123
- // toFilterableValueFn: (date: Date) => date
124
- // };
125
- // }
120
+ if (col.instructions?.displayPattern) {
121
+ instructions = {
122
+ toStringFn: (date: Date) => dateFormat(date, col.instructions?.displayPattern || ''),
123
+ toSortableValueFn: (date: Date) => date.getTime(),
124
+ toFilterableValueFn: (date: Date) => date
125
+ };
126
+ }
126
127
 
127
128
  if (col.instructions?.missingValues) {
128
129
  instructions = {
@@ -190,6 +190,7 @@ export class Send {
190
190
  id: number;
191
191
  limit: number;
192
192
  offset: number;
193
+ q: string;
193
194
  version?: number;
194
195
  filter: Filter[];
195
196
  order: OrderBy[];
@@ -199,6 +200,7 @@ export class Send {
199
200
  this.limit = 10;
200
201
  this.offset = 0;
201
202
  this.version = 0;
203
+ this.q = '';
202
204
  this.filter = [];
203
205
  this.order = [];
204
206
  }