@rkosafo/cai.components 0.0.78 → 0.0.80

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.
Files changed (103) hide show
  1. package/README.md +8 -8
  2. package/dist/baseEditor/index.svelte +32 -32
  3. package/dist/builders/filters/FilterBuilder.svelte +641 -641
  4. package/dist/forms/FormCheckbox/FormCheckbox.svelte +53 -53
  5. package/dist/forms/FormClEditor/ClEdito.svelte +68 -68
  6. package/dist/forms/FormDatepicker/FormDatepicker.svelte +159 -159
  7. package/dist/forms/FormFileUpload/FormFileUplad.svelte +134 -134
  8. package/dist/forms/FormInput/FormInput.svelte +87 -87
  9. package/dist/forms/FormRadio/FormRadio.svelte +53 -53
  10. package/dist/forms/FormSelect/FormSelect.svelte +88 -88
  11. package/dist/forms/FormTextarea/FormTextarea.svelte +78 -78
  12. package/dist/forms/button-toggle/ButtonToggle.svelte +119 -119
  13. package/dist/forms/button-toggle/CheckIcon.svelte +28 -28
  14. package/dist/forms/checkbox/Checkbox.svelte +82 -82
  15. package/dist/forms/checkbox/CheckboxButton.svelte +92 -92
  16. package/dist/forms/datepicker/Datepicker.svelte +707 -707
  17. package/dist/forms/form/Form.svelte +69 -69
  18. package/dist/forms/input/Input.svelte +363 -363
  19. package/dist/forms/label/Label.svelte +38 -38
  20. package/dist/forms/radio/Radio.svelte +48 -48
  21. package/dist/forms/radio/RadioButton.svelte +22 -22
  22. package/dist/forms/select/Select.svelte +56 -56
  23. package/dist/forms/textarea/Textarea.svelte +165 -165
  24. package/dist/forms/toggle/Toggle.svelte +70 -70
  25. package/dist/layout/Chat/CategorySelector.svelte +52 -52
  26. package/dist/layout/Chat/ChatEntry.svelte +246 -246
  27. package/dist/layout/Chat/ChatEntrySkeleton.svelte +81 -81
  28. package/dist/layout/Chat/ChatHeader.svelte +172 -172
  29. package/dist/layout/Chat/ChatInput.svelte +207 -207
  30. package/dist/layout/Chat/DraggableWindow.svelte +230 -230
  31. package/dist/layout/Chat/PreviewPage.svelte +182 -182
  32. package/dist/layout/Chat/RichText.svelte +216 -216
  33. package/dist/layout/ComponentCanvas/Canvas.svelte +40 -40
  34. package/dist/layout/ComponentCanvas/ComponentRenderer.svelte +85 -85
  35. package/dist/layout/TF/Content/Content.svelte +21 -21
  36. package/dist/layout/TF/Header/Header.svelte +166 -166
  37. package/dist/layout/TF/Sidebar/Sidebar.svelte +148 -148
  38. package/dist/layout/TF/Wrapper/Wrapper.svelte +17 -17
  39. package/dist/layout/mailing/MailPaginator.svelte +36 -36
  40. package/dist/layout/mailing/MailSidebar.svelte +39 -39
  41. package/dist/layout/mailing/MailToolBar.svelte +174 -174
  42. package/dist/layout/mailing/MailingContent.svelte +10 -10
  43. package/dist/layout/mailing/MailingHeader.svelte +55 -55
  44. package/dist/layout/mailing/MailingMessageCard.svelte +112 -112
  45. package/dist/layout/mailing/MailingMessageViewer.svelte +87 -87
  46. package/dist/layout/mailing/MailingModule.svelte +448 -448
  47. package/dist/styles/docs.css +615 -615
  48. package/dist/styles/tf-layout.css +185 -185
  49. package/dist/themes/ThemeProvider.svelte +20 -20
  50. package/dist/types/index.d.ts +2 -0
  51. package/dist/typography/heading/Heading.svelte +35 -35
  52. package/dist/ui/accordion/Accordion.svelte +49 -49
  53. package/dist/ui/accordion/AccordionItem.svelte +173 -173
  54. package/dist/ui/alert/Alert.svelte +83 -83
  55. package/dist/ui/alertDialog/AlertDialog.svelte +40 -40
  56. package/dist/ui/avatar/Avatar.svelte +77 -77
  57. package/dist/ui/box/Box.svelte +28 -28
  58. package/dist/ui/breadcrumb/Breadcrumb.svelte +39 -39
  59. package/dist/ui/buttons/ActionButton.svelte +234 -234
  60. package/dist/ui/buttons/Button.svelte +102 -102
  61. package/dist/ui/buttons/GradientButton.svelte +59 -59
  62. package/dist/ui/datatable/Datatable.svelte +525 -525
  63. package/dist/ui/drawer/Drawer.svelte +300 -300
  64. package/dist/ui/dropdown/Dropdown.svelte +36 -36
  65. package/dist/ui/dropdown/DropdownDivider.svelte +11 -11
  66. package/dist/ui/dropdown/DropdownGroup.svelte +14 -14
  67. package/dist/ui/dropdown/DropdownHeader.svelte +14 -14
  68. package/dist/ui/dropdown/DropdownItem.svelte +52 -52
  69. package/dist/ui/footer/Footer.svelte +15 -15
  70. package/dist/ui/footer/FooterBrand.svelte +37 -37
  71. package/dist/ui/footer/FooterCopyright.svelte +45 -45
  72. package/dist/ui/footer/FooterIcon.svelte +22 -22
  73. package/dist/ui/footer/FooterLink.svelte +33 -33
  74. package/dist/ui/footer/FooterLinkGroup.svelte +13 -13
  75. package/dist/ui/icons/IconifyIcon.svelte +7 -7
  76. package/dist/ui/indicator/Indicator.svelte +42 -42
  77. package/dist/ui/modal/Modal.svelte +265 -265
  78. package/dist/ui/notificationList/NotificationList.svelte +123 -123
  79. package/dist/ui/pageLoader/PageLoader.svelte +14 -14
  80. package/dist/ui/pageLoader/PageLoader2.svelte +99 -0
  81. package/dist/ui/pageLoader/PageLoader2.svelte.d.ts +24 -0
  82. package/dist/ui/pageLoader/index.d.ts +2 -1
  83. package/dist/ui/pageLoader/index.js +2 -1
  84. package/dist/ui/paginate/Paginate.svelte +96 -96
  85. package/dist/ui/speedDial/SpeedDial.svelte +77 -77
  86. package/dist/ui/speedDial/SpeedDialButton.svelte +75 -75
  87. package/dist/ui/speedDial/SpeedDialTrigger.svelte +79 -79
  88. package/dist/ui/tab/Tab.svelte +93 -67
  89. package/dist/ui/table/Table.svelte +396 -396
  90. package/dist/ui/tableLoader/TableLoader.svelte +24 -24
  91. package/dist/ui/toast/Toast.svelte +337 -337
  92. package/dist/ui/toast/Toast.svelte.d.ts +10 -10
  93. package/dist/ui/toolbar/Toolbar.svelte +59 -59
  94. package/dist/ui/toolbar/ToolbarButton.svelte +56 -56
  95. package/dist/ui/toolbar/ToolbarGroup.svelte +43 -43
  96. package/dist/ui/tooltip/Tooltip.svelte +51 -51
  97. package/dist/utils/Popper.svelte +257 -257
  98. package/dist/utils/closeButton/CloseButton.svelte +88 -88
  99. package/dist/utils/index.d.ts +2 -2
  100. package/dist/utils/index.js +3 -3
  101. package/dist/utils/singleSelection.svelte.js +48 -48
  102. package/dist/youtube/index.svelte +12 -12
  103. package/package.json +1 -1
@@ -1,525 +1,525 @@
1
- <script lang="ts">
2
- import { goto } from '$app/navigation';
3
- import { page } from '$app/state';
4
- import {
5
- AlertDialog,
6
- Button,
7
- Datepicker,
8
- Drawer,
9
- extractQueryParam,
10
- Input,
11
- loadFromLocalStorage,
12
- Paginate,
13
- saveToLocalStorage,
14
- Select,
15
- Table,
16
- TableLoader,
17
- toast,
18
- type DatatableProps,
19
- type IFormChangeProp,
20
- type TableFilter
21
- } from '../../index.js';
22
- import { PageInfo } from '../../utils/paginate.svelte.js';
23
- import debounce from 'lodash/debounce';
24
- import { onMount } from 'svelte';
25
- import { writable } from 'svelte/store';
26
- import { slide } from 'svelte/transition';
27
-
28
- let {
29
- persitFiltersToUrl = false,
30
- showTopActionsBackground = true,
31
- fillSpace = true,
32
- onAfterAction,
33
- hideSearchBox = false,
34
- showAdd = true,
35
- addButtonLabel = 'Add New',
36
- searchPlaceholder = 'Search...',
37
- addNewHeading = 'Add New Record',
38
- updateHeading = 'Update',
39
- headerColor = 'blue',
40
- newRecord = {},
41
- query = $bindable(''),
42
- hiddenColumns = [],
43
- customFilterValues = {},
44
- onAddNew,
45
- busy = false,
46
- showFilterDateRange = false,
47
- loadingBodySize = 10,
48
- loadingHeaderSize = 6,
49
- allowLoadAfterCreate = true,
50
- allowDispatchAfterAction = false,
51
- modalSize = 'md',
52
- take = 15,
53
- onActionClicked,
54
- onRowClicked,
55
- onView,
56
- onEdit,
57
- onDelete,
58
- editor,
59
- customFilter,
60
- handleCheckbox,
61
- read = $bindable((skip?: number, take?: number, filter?: TableFilter<any>) => {
62
- return null;
63
- }),
64
- updateEntry = (x: any) => {
65
- return null;
66
- },
67
- createEntry = async (x: any) => {
68
- return { success: false, data: null, error: null } as any;
69
- },
70
- deleteEntry = async (id: string | number) => {
71
- return { success: false, data: null, error: null } as any;
72
- },
73
- modalFooter,
74
- showModalButtons = true,
75
- customAddAction,
76
- ...otherProps
77
- }: DatatableProps = $props();
78
-
79
- const PAGE_SIZE_KEY = 'customTablePageSize';
80
- let editorHeading = $state('');
81
- let hideForm = $state(true);
82
- let editing = $state(false);
83
- let activeEntry = $state<any>({});
84
- let pageInfo = new PageInfo();
85
- const rangeDate = $derived(getRange());
86
- let allColumns = $state(writable<any>([]));
87
- let pathname = $state('');
88
- let search = $state('');
89
- let pageNumber = $state(0);
90
- let searchParams = $state<any>(null);
91
- let showAlert = $state(false);
92
- let tableData = $state<any[]>([]);
93
- let isLoading = $state(false);
94
- let deleteLoading = $state(false);
95
- let isValid = $state(false);
96
- let viewing = $state(false);
97
- let dataToSave = $state<any>({});
98
-
99
- function addNew() {
100
- if (onAddNew) {
101
- onAddNew();
102
- return;
103
- }
104
- editorHeading = addNewHeading;
105
- activeEntry = (newRecord && { ...newRecord }) || null;
106
- editing = false;
107
- hideForm = false;
108
- viewing = false;
109
- }
110
-
111
- async function fetchData(page: number, params: TableFilter) {
112
- try {
113
- let currentPage = 0;
114
- busy = true;
115
- if (params.search) {
116
- pageInfo.setCurrentPage(1);
117
- currentPage = pageInfo.currentPage;
118
- } else {
119
- currentPage = page || pageInfo.currentPage;
120
- }
121
- let newParams: any = {
122
- page: currentPage,
123
- pageSize: pageInfo.pageSize,
124
- search: params.search ?? '',
125
- filter: params.filter ?? {},
126
- order: params.order ?? []
127
- };
128
- const ret = await read(newParams.page, newParams.pageSize, {
129
- ...params,
130
- filter: { ...customFilterValues },
131
- search: newParams.search ?? ''
132
- });
133
- if (!ret?.success) {
134
- // showError(ret?.message || 'Failed to load data');
135
- toast.error(ret?.message || 'Failed to load data');
136
- return true;
137
- }
138
- const xs = ret.data;
139
- // console.log({xs})
140
- pageInfo.totalItems = xs.totalCount;
141
- pageInfo.setHasNextPage(xs.pageInfo.hasNextPage);
142
- pageInfo.setHasPrevPage(xs.pageInfo.hasPreviousPage);
143
- tableData = xs.items;
144
- } catch (e: any) {
145
- toast.error(e.message || e);
146
- } finally {
147
- busy = false;
148
- }
149
- }
150
-
151
- const debouncedSearch = debounce(fetchData, 300);
152
-
153
- function onSearchChange(e: Event) {
154
- const target = e.target as HTMLInputElement | null;
155
- const value = target ? target.value : '';
156
- debouncedSearch(pageNumber, { search: value });
157
- }
158
-
159
- function getRange() {
160
- return { from: undefined, to: undefined };
161
- }
162
- function handleDateRangeChange(e) {
163
- console.log({ e });
164
- }
165
-
166
- async function handlePageSize(val: number) {
167
- pageInfo.setPageSize(val);
168
- await fetchData(pageInfo.currentPage, { search: query });
169
-
170
- saveToLocalStorage(PAGE_SIZE_KEY, { pageSize: val });
171
- }
172
-
173
- async function getMore() {
174
- if (pageInfo.gotoNext()) {
175
- const value = pageInfo.currentPage;
176
- if (persitFiltersToUrl) {
177
- const current = new URLSearchParams(Array.from(searchParams.entries()) as any);
178
-
179
- if (!value) {
180
- current.delete('page');
181
- } else {
182
- current.set('page', String(value));
183
- }
184
-
185
- const search = current.toString();
186
- const page = search ? `?${search}` : '';
187
- await goto(`${pathname}${page}`);
188
- }
189
- await fetchData(value, { search: query });
190
- }
191
- }
192
-
193
- async function getLess() {
194
- if (pageInfo.gotoPrev()) {
195
- const value = pageInfo.currentPage;
196
- if (persitFiltersToUrl) {
197
- const current = new URLSearchParams(Array.from(searchParams.entries()) as any);
198
- if (!value) {
199
- current.delete('page');
200
- } else {
201
- current.set('page', String(value));
202
- }
203
- const search = current.toString();
204
- const page = search ? `?${search}` : '';
205
- await goto(`${pathname}${page}`);
206
- }
207
- await fetchData(value, { search: query });
208
- }
209
- }
210
-
211
- function handleExternalFetch(event: CustomEvent) {
212
- const { page: customPage, params: customParams } = event.detail || {};
213
- const fetchPage = customPage !== undefined ? customPage : pageNumber;
214
- const fetchParams = customParams || { search: query };
215
-
216
- fetchData(fetchPage, fetchParams);
217
- }
218
-
219
- function handleEdit(val: any) {
220
- if (onEdit) {
221
- onEdit(val);
222
- return;
223
- }
224
- activeEntry = val;
225
- editorHeading = updateHeading;
226
- editing = true;
227
- hideForm = false;
228
- viewing = false;
229
- }
230
- function handleView(val: any) {
231
- if (onView) {
232
- onView(val);
233
- return;
234
- }
235
- activeEntry = val;
236
- editorHeading = 'View Record';
237
- editing = true;
238
- hideForm = false;
239
- viewing = true;
240
- }
241
-
242
- function handleRowClicked(val: any) {
243
- if (onRowClicked) {
244
- onRowClicked(val);
245
- return;
246
- }
247
- activeEntry = val;
248
- editorHeading = addNewHeading;
249
- editing = true;
250
- hideForm = false;
251
- }
252
- function handleDelete(val: any) {
253
- if (onDelete) {
254
- onDelete(val);
255
- return;
256
- }
257
- activeEntry = val;
258
- showAlert = true;
259
- // editorHeading = updateHeading;
260
- // editing = true;
261
- // hideForm = true;
262
- }
263
-
264
- function closeSideModal() {
265
- hideForm = true;
266
- activeEntry = null;
267
- viewing = false;
268
- }
269
-
270
- async function handleAction({ action, data }: { action: string; data: FormData }) {
271
- // let formData = data;
272
- let formData = dataToSave;
273
-
274
- // console.log({ formData });
275
- try {
276
- isLoading = true;
277
- const ret = editing
278
- ? await updateEntry({ ...formData, id: activeEntry.id })
279
- : await createEntry(formData);
280
-
281
- if (ret?.success) {
282
- const successMessage = allowLoadAfterCreate
283
- ? editing
284
- ? 'Successfully updated record'
285
- : 'Successfully added record'
286
- : '';
287
- toast.success(ret.message || successMessage);
288
- allowLoadAfterCreate && (await fetchData(pageNumber, { search: query }));
289
-
290
- allowDispatchAfterAction &&
291
- onAfterAction &&
292
- onAfterAction({ type: 'create', values: formData, data: ret.data });
293
-
294
- hideForm = true;
295
- activeEntry = {};
296
- dataToSave = {};
297
- closeSideModal();
298
- } else {
299
- toast.error(ret?.message || 'Failed');
300
- }
301
- } catch (error: any) {
302
- toast.error(error?.message || 'failed');
303
- } finally {
304
- isLoading = false;
305
- }
306
- }
307
-
308
- function closeAlert() {
309
- activeEntry = {};
310
- if (showAlert) showAlert = false;
311
- if (deleteLoading) deleteLoading = false;
312
- }
313
-
314
- async function doDelete(id: string) {
315
- try {
316
- deleteLoading = true;
317
- const ret = await deleteEntry(id);
318
- if (!ret?.success) {
319
- toast.error(ret?.message || 'Failed to delete');
320
- return;
321
- }
322
- toast.success(ret.message);
323
- closeAlert();
324
- await fetchData(pageNumber, { search: query });
325
- } catch (error: any) {
326
- toast.error(error?.message || error);
327
- } finally {
328
- deleteLoading = false;
329
- }
330
- }
331
-
332
- function handleFormChange(val: IFormChangeProp<any>) {
333
- const { values, isValid: validForm } = val;
334
- isValid = validForm;
335
- dataToSave = values;
336
- }
337
-
338
- $effect(() => {
339
- searchParams = page.url.searchParams;
340
- search = page.url.search;
341
- pathname = page.url.pathname;
342
-
343
- pageNumber = extractQueryParam(page.url.search, 'page')
344
- ? Number(extractQueryParam(page.url.search, 'page'))
345
- : pageInfo.currentPage;
346
- });
347
-
348
- onMount(() => {
349
- const res = loadFromLocalStorage<{ pageSize: string }>(PAGE_SIZE_KEY);
350
- if (res?.pageSize) {
351
- pageInfo.setPageSize(Number(res?.pageSize));
352
- } else {
353
- pageInfo.setPageSize(take);
354
- }
355
-
356
- if (pageNumber) {
357
- pageInfo.setCurrentPage(pageNumber);
358
- fetchData(pageNumber, { search: query });
359
- } else {
360
- fetchData(pageInfo.currentPage, { search: query });
361
- }
362
-
363
- // Listen for custom event from parent
364
- window.addEventListener('reFetchTableData', handleExternalFetch as EventListener);
365
-
366
- // Cleanup on destroy
367
- return () => {
368
- window.removeEventListener('reFetchTableData', handleExternalFetch as EventListener);
369
- };
370
- });
371
- </script>
372
-
373
- <div class="flex h-full w-full flex-col gap-2">
374
- <div
375
- class="flex w-full flex-col gap-2"
376
- class:custom-container={!fillSpace}
377
- class:px-4={fillSpace}
378
- class:table-background={showTopActionsBackground}
379
- class:py-2={!showTopActionsBackground}
380
- >
381
- <div class="flex w-full flex-col gap-2 sm:justify-between lg:flex-row lg:items-center">
382
- <div class="flex w-full flex-col gap-2 sm:flex-row">
383
- <div class:hidden={hideSearchBox} class="w-full lg:max-w-lg">
384
- <Input
385
- value={query}
386
- oninput={onSearchChange}
387
- placeholder={searchPlaceholder}
388
- class="h-9.5 ps-8"
389
- >
390
- {#snippet left()}
391
- <iconify-icon icon="eva:search-outline" class="text-xl text-gray-500"></iconify-icon>
392
- {/snippet}
393
- </Input>
394
- </div>
395
- <div class:hidden={!showFilterDateRange} class="w-full sm:max-w-60">
396
- <Datepicker
397
- placeholder="Date Range"
398
- onselect={handleDateRangeChange}
399
- range
400
- rangeFrom={rangeDate.from}
401
- rangeTo={rangeDate.to}
402
- />
403
- </div>
404
- </div>
405
-
406
- <div class="flex shrink-0 flex-col gap-4 sm:flex-row sm:items-center">
407
- <div class="flex items-center gap-2">
408
- <div class="text-sm">Page Size</div>
409
- <Select
410
- value={pageInfo.pageSize}
411
- clearable={false}
412
- searchable={false}
413
- options={[
414
- { value: 15, label: '15' },
415
- { value: 20, label: '20' },
416
- { value: 25, label: '25' },
417
- { value: 30, label: '30' },
418
- { value: 40, label: '40' },
419
- { value: 45, label: '45' },
420
- { value: 50, label: '50' },
421
- { value: 100, label: '100' }
422
- ]}
423
- placeholder="15"
424
- onChange={(e) => {
425
- const { value } = e;
426
- handlePageSize(value);
427
- }}
428
- />
429
- </div>
430
- <div>
431
- <Paginate
432
- onNextPage={getMore}
433
- onPreviousPage={getLess}
434
- currentPage={pageInfo.currentPage}
435
- totalPages={pageInfo.totalPages}
436
- hasNextPage={pageInfo.hasNextPage}
437
- hasPreviousPage={pageInfo.hasPrevPage}
438
- recordCount={pageInfo.totalItems}
439
- refresh={() => {
440
- fetchData(pageNumber, { search: query });
441
- }}
442
- tableColumns={$allColumns}
443
- bind:hiddenColumns
444
- />
445
- </div>
446
- {#if customAddAction}
447
- {@render customAddAction?.()}
448
- {:else}
449
- <div class:hidden={!showAdd} class="shrink-0">
450
- <Button onclick={addNew}>{addButtonLabel}</Button>
451
- </div>
452
- {/if}
453
- </div>
454
- </div>
455
- </div>
456
- <div class:custom-container={!fillSpace} class="flex h-full w-full flex-col gap-2">
457
- {@render customFilter?.({
458
- pageSize: pageInfo.pageSize ?? 0,
459
- currentPage: pageInfo.currentPage ?? 0,
460
- search: query ?? ''
461
- })}
462
- {#if busy}
463
- <TableLoader bodySize={loadingBodySize} headerSize={loadingHeaderSize} />
464
- {:else}
465
- <div class="h-full w-full flex-grow">
466
- <Table
467
- data={tableData}
468
- {headerColor}
469
- bind:hiddenColumns
470
- onEdit={handleEdit}
471
- onView={handleView}
472
- onDelete={handleDelete}
473
- onRowClicked={handleRowClicked}
474
- {onActionClicked}
475
- {handleCheckbox}
476
- {...otherProps}
477
- />
478
- </div>
479
- {/if}
480
- </div>
481
- </div>
482
-
483
- <Drawer
484
- bind:hidden={hideForm}
485
- placement="right"
486
- activateClickOutside={false}
487
- title={editorHeading}
488
- size={modalSize}
489
- onclose={closeSideModal}
490
- form
491
- transitionType={slide}
492
- transitionParams={{
493
- duration: 300,
494
- axis: 'x'
495
- }}
496
- onaction={handleAction}
497
- >
498
- {@render editor?.({
499
- recordId: activeEntry.id,
500
- data: activeEntry,
501
- onChange: handleFormChange as any,
502
- readonly: viewing
503
- })}
504
-
505
- {#snippet footer()}
506
- {#if showModalButtons}
507
- {#if modalFooter}
508
- {@render modalFooter()}
509
- {:else if !viewing}
510
- <Button disabled={!isValid} loading={isLoading} type="submit" class="w-full" value="submit"
511
- >Submit</Button
512
- >
513
- {/if}
514
- {/if}
515
- {/snippet}
516
- </Drawer>
517
-
518
- <AlertDialog
519
- bind:open={showAlert}
520
- message="Are you sure you want to delete this?"
521
- icon="ion:trash"
522
- onCancel={closeAlert}
523
- busy={deleteLoading}
524
- onYes={() => doDelete(activeEntry?.id)}
525
- />
1
+ <script lang="ts">
2
+ import { goto } from '$app/navigation';
3
+ import { page } from '$app/state';
4
+ import {
5
+ AlertDialog,
6
+ Button,
7
+ Datepicker,
8
+ Drawer,
9
+ extractQueryParam,
10
+ Input,
11
+ loadFromLocalStorage,
12
+ Paginate,
13
+ saveToLocalStorage,
14
+ Select,
15
+ Table,
16
+ TableLoader,
17
+ toast,
18
+ type DatatableProps,
19
+ type IFormChangeProp,
20
+ type TableFilter
21
+ } from '../../index.js';
22
+ import { PageInfo } from '../../utils/paginate.svelte.js';
23
+ import debounce from 'lodash/debounce';
24
+ import { onMount } from 'svelte';
25
+ import { writable } from 'svelte/store';
26
+ import { slide } from 'svelte/transition';
27
+
28
+ let {
29
+ persitFiltersToUrl = false,
30
+ showTopActionsBackground = true,
31
+ fillSpace = true,
32
+ onAfterAction,
33
+ hideSearchBox = false,
34
+ showAdd = true,
35
+ addButtonLabel = 'Add New',
36
+ searchPlaceholder = 'Search...',
37
+ addNewHeading = 'Add New Record',
38
+ updateHeading = 'Update',
39
+ headerColor = 'blue',
40
+ newRecord = {},
41
+ query = $bindable(''),
42
+ hiddenColumns = [],
43
+ customFilterValues = {},
44
+ onAddNew,
45
+ busy = false,
46
+ showFilterDateRange = false,
47
+ loadingBodySize = 10,
48
+ loadingHeaderSize = 6,
49
+ allowLoadAfterCreate = true,
50
+ allowDispatchAfterAction = false,
51
+ modalSize = 'md',
52
+ take = 15,
53
+ onActionClicked,
54
+ onRowClicked,
55
+ onView,
56
+ onEdit,
57
+ onDelete,
58
+ editor,
59
+ customFilter,
60
+ handleCheckbox,
61
+ read = $bindable((skip?: number, take?: number, filter?: TableFilter<any>) => {
62
+ return null;
63
+ }),
64
+ updateEntry = (x: any) => {
65
+ return null;
66
+ },
67
+ createEntry = async (x: any) => {
68
+ return { success: false, data: null, error: null } as any;
69
+ },
70
+ deleteEntry = async (id: string | number) => {
71
+ return { success: false, data: null, error: null } as any;
72
+ },
73
+ modalFooter,
74
+ showModalButtons = true,
75
+ customAddAction,
76
+ ...otherProps
77
+ }: DatatableProps = $props();
78
+
79
+ const PAGE_SIZE_KEY = 'customTablePageSize';
80
+ let editorHeading = $state('');
81
+ let hideForm = $state(true);
82
+ let editing = $state(false);
83
+ let activeEntry = $state<any>({});
84
+ let pageInfo = new PageInfo();
85
+ const rangeDate = $derived(getRange());
86
+ let allColumns = $state(writable<any>([]));
87
+ let pathname = $state('');
88
+ let search = $state('');
89
+ let pageNumber = $state(0);
90
+ let searchParams = $state<any>(null);
91
+ let showAlert = $state(false);
92
+ let tableData = $state<any[]>([]);
93
+ let isLoading = $state(false);
94
+ let deleteLoading = $state(false);
95
+ let isValid = $state(false);
96
+ let viewing = $state(false);
97
+ let dataToSave = $state<any>({});
98
+
99
+ function addNew() {
100
+ if (onAddNew) {
101
+ onAddNew();
102
+ return;
103
+ }
104
+ editorHeading = addNewHeading;
105
+ activeEntry = (newRecord && { ...newRecord }) || null;
106
+ editing = false;
107
+ hideForm = false;
108
+ viewing = false;
109
+ }
110
+
111
+ async function fetchData(page: number, params: TableFilter) {
112
+ try {
113
+ let currentPage = 0;
114
+ busy = true;
115
+ if (params.search) {
116
+ pageInfo.setCurrentPage(1);
117
+ currentPage = pageInfo.currentPage;
118
+ } else {
119
+ currentPage = page || pageInfo.currentPage;
120
+ }
121
+ let newParams: any = {
122
+ page: currentPage,
123
+ pageSize: pageInfo.pageSize,
124
+ search: params.search ?? '',
125
+ filter: params.filter ?? {},
126
+ order: params.order ?? []
127
+ };
128
+ const ret = await read(newParams.page, newParams.pageSize, {
129
+ ...params,
130
+ filter: { ...customFilterValues },
131
+ search: newParams.search ?? ''
132
+ });
133
+ if (!ret?.success) {
134
+ // showError(ret?.message || 'Failed to load data');
135
+ toast.error(ret?.message || 'Failed to load data');
136
+ return true;
137
+ }
138
+ const xs = ret.data;
139
+ // console.log({xs})
140
+ pageInfo.totalItems = xs.totalCount;
141
+ pageInfo.setHasNextPage(xs.pageInfo.hasNextPage);
142
+ pageInfo.setHasPrevPage(xs.pageInfo.hasPreviousPage);
143
+ tableData = xs.items;
144
+ } catch (e: any) {
145
+ toast.error(e.message || e);
146
+ } finally {
147
+ busy = false;
148
+ }
149
+ }
150
+
151
+ const debouncedSearch = debounce(fetchData, 300);
152
+
153
+ function onSearchChange(e: Event) {
154
+ const target = e.target as HTMLInputElement | null;
155
+ const value = target ? target.value : '';
156
+ debouncedSearch(pageNumber, { search: value });
157
+ }
158
+
159
+ function getRange() {
160
+ return { from: undefined, to: undefined };
161
+ }
162
+ function handleDateRangeChange(e) {
163
+ console.log({ e });
164
+ }
165
+
166
+ async function handlePageSize(val: number) {
167
+ pageInfo.setPageSize(val);
168
+ await fetchData(pageInfo.currentPage, { search: query });
169
+
170
+ saveToLocalStorage(PAGE_SIZE_KEY, { pageSize: val });
171
+ }
172
+
173
+ async function getMore() {
174
+ if (pageInfo.gotoNext()) {
175
+ const value = pageInfo.currentPage;
176
+ if (persitFiltersToUrl) {
177
+ const current = new URLSearchParams(Array.from(searchParams.entries()) as any);
178
+
179
+ if (!value) {
180
+ current.delete('page');
181
+ } else {
182
+ current.set('page', String(value));
183
+ }
184
+
185
+ const search = current.toString();
186
+ const page = search ? `?${search}` : '';
187
+ await goto(`${pathname}${page}`);
188
+ }
189
+ await fetchData(value, { search: query });
190
+ }
191
+ }
192
+
193
+ async function getLess() {
194
+ if (pageInfo.gotoPrev()) {
195
+ const value = pageInfo.currentPage;
196
+ if (persitFiltersToUrl) {
197
+ const current = new URLSearchParams(Array.from(searchParams.entries()) as any);
198
+ if (!value) {
199
+ current.delete('page');
200
+ } else {
201
+ current.set('page', String(value));
202
+ }
203
+ const search = current.toString();
204
+ const page = search ? `?${search}` : '';
205
+ await goto(`${pathname}${page}`);
206
+ }
207
+ await fetchData(value, { search: query });
208
+ }
209
+ }
210
+
211
+ function handleExternalFetch(event: CustomEvent) {
212
+ const { page: customPage, params: customParams } = event.detail || {};
213
+ const fetchPage = customPage !== undefined ? customPage : pageNumber;
214
+ const fetchParams = customParams || { search: query };
215
+
216
+ fetchData(fetchPage, fetchParams);
217
+ }
218
+
219
+ function handleEdit(val: any) {
220
+ if (onEdit) {
221
+ onEdit(val);
222
+ return;
223
+ }
224
+ activeEntry = val;
225
+ editorHeading = updateHeading;
226
+ editing = true;
227
+ hideForm = false;
228
+ viewing = false;
229
+ }
230
+ function handleView(val: any) {
231
+ if (onView) {
232
+ onView(val);
233
+ return;
234
+ }
235
+ activeEntry = val;
236
+ editorHeading = 'View Record';
237
+ editing = true;
238
+ hideForm = false;
239
+ viewing = true;
240
+ }
241
+
242
+ function handleRowClicked(val: any) {
243
+ if (onRowClicked) {
244
+ onRowClicked(val);
245
+ return;
246
+ }
247
+ activeEntry = val;
248
+ editorHeading = addNewHeading;
249
+ editing = true;
250
+ hideForm = false;
251
+ }
252
+ function handleDelete(val: any) {
253
+ if (onDelete) {
254
+ onDelete(val);
255
+ return;
256
+ }
257
+ activeEntry = val;
258
+ showAlert = true;
259
+ // editorHeading = updateHeading;
260
+ // editing = true;
261
+ // hideForm = true;
262
+ }
263
+
264
+ function closeSideModal() {
265
+ hideForm = true;
266
+ activeEntry = null;
267
+ viewing = false;
268
+ }
269
+
270
+ async function handleAction({ action, data }: { action: string; data: FormData }) {
271
+ // let formData = data;
272
+ let formData = dataToSave;
273
+
274
+ // console.log({ formData });
275
+ try {
276
+ isLoading = true;
277
+ const ret = editing
278
+ ? await updateEntry({ ...formData, id: activeEntry.id })
279
+ : await createEntry(formData);
280
+
281
+ if (ret?.success) {
282
+ const successMessage = allowLoadAfterCreate
283
+ ? editing
284
+ ? 'Successfully updated record'
285
+ : 'Successfully added record'
286
+ : '';
287
+ toast.success(ret.message || successMessage);
288
+ allowLoadAfterCreate && (await fetchData(pageNumber, { search: query }));
289
+
290
+ allowDispatchAfterAction &&
291
+ onAfterAction &&
292
+ onAfterAction({ type: 'create', values: formData, data: ret.data });
293
+
294
+ hideForm = true;
295
+ activeEntry = {};
296
+ dataToSave = {};
297
+ closeSideModal();
298
+ } else {
299
+ toast.error(ret?.message || 'Failed');
300
+ }
301
+ } catch (error: any) {
302
+ toast.error(error?.message || 'failed');
303
+ } finally {
304
+ isLoading = false;
305
+ }
306
+ }
307
+
308
+ function closeAlert() {
309
+ activeEntry = {};
310
+ if (showAlert) showAlert = false;
311
+ if (deleteLoading) deleteLoading = false;
312
+ }
313
+
314
+ async function doDelete(id: string) {
315
+ try {
316
+ deleteLoading = true;
317
+ const ret = await deleteEntry(id);
318
+ if (!ret?.success) {
319
+ toast.error(ret?.message || 'Failed to delete');
320
+ return;
321
+ }
322
+ toast.success(ret.message);
323
+ closeAlert();
324
+ await fetchData(pageNumber, { search: query });
325
+ } catch (error: any) {
326
+ toast.error(error?.message || error);
327
+ } finally {
328
+ deleteLoading = false;
329
+ }
330
+ }
331
+
332
+ function handleFormChange(val: IFormChangeProp<any>) {
333
+ const { values, isValid: validForm } = val;
334
+ isValid = validForm;
335
+ dataToSave = values;
336
+ }
337
+
338
+ $effect(() => {
339
+ searchParams = page.url.searchParams;
340
+ search = page.url.search;
341
+ pathname = page.url.pathname;
342
+
343
+ pageNumber = extractQueryParam(page.url.search, 'page')
344
+ ? Number(extractQueryParam(page.url.search, 'page'))
345
+ : pageInfo.currentPage;
346
+ });
347
+
348
+ onMount(() => {
349
+ const res = loadFromLocalStorage<{ pageSize: string }>(PAGE_SIZE_KEY);
350
+ if (res?.pageSize) {
351
+ pageInfo.setPageSize(Number(res?.pageSize));
352
+ } else {
353
+ pageInfo.setPageSize(take);
354
+ }
355
+
356
+ if (pageNumber) {
357
+ pageInfo.setCurrentPage(pageNumber);
358
+ fetchData(pageNumber, { search: query });
359
+ } else {
360
+ fetchData(pageInfo.currentPage, { search: query });
361
+ }
362
+
363
+ // Listen for custom event from parent
364
+ window.addEventListener('reFetchTableData', handleExternalFetch as EventListener);
365
+
366
+ // Cleanup on destroy
367
+ return () => {
368
+ window.removeEventListener('reFetchTableData', handleExternalFetch as EventListener);
369
+ };
370
+ });
371
+ </script>
372
+
373
+ <div class="flex h-full w-full flex-col gap-2">
374
+ <div
375
+ class="flex w-full flex-col gap-2"
376
+ class:custom-container={!fillSpace}
377
+ class:px-4={fillSpace}
378
+ class:table-background={showTopActionsBackground}
379
+ class:py-2={!showTopActionsBackground}
380
+ >
381
+ <div class="flex w-full flex-col gap-2 sm:justify-between lg:flex-row lg:items-center">
382
+ <div class="flex w-full flex-col gap-2 sm:flex-row">
383
+ <div class:hidden={hideSearchBox} class="w-full lg:max-w-lg">
384
+ <Input
385
+ value={query}
386
+ oninput={onSearchChange}
387
+ placeholder={searchPlaceholder}
388
+ class="h-9.5 ps-8"
389
+ >
390
+ {#snippet left()}
391
+ <iconify-icon icon="eva:search-outline" class="text-xl text-gray-500"></iconify-icon>
392
+ {/snippet}
393
+ </Input>
394
+ </div>
395
+ <div class:hidden={!showFilterDateRange} class="w-full sm:max-w-60">
396
+ <Datepicker
397
+ placeholder="Date Range"
398
+ onselect={handleDateRangeChange}
399
+ range
400
+ rangeFrom={rangeDate.from}
401
+ rangeTo={rangeDate.to}
402
+ />
403
+ </div>
404
+ </div>
405
+
406
+ <div class="flex shrink-0 flex-col gap-4 sm:flex-row sm:items-center">
407
+ <div class="flex items-center gap-2">
408
+ <div class="text-sm">Page Size</div>
409
+ <Select
410
+ value={pageInfo.pageSize}
411
+ clearable={false}
412
+ searchable={false}
413
+ options={[
414
+ { value: 15, label: '15' },
415
+ { value: 20, label: '20' },
416
+ { value: 25, label: '25' },
417
+ { value: 30, label: '30' },
418
+ { value: 40, label: '40' },
419
+ { value: 45, label: '45' },
420
+ { value: 50, label: '50' },
421
+ { value: 100, label: '100' }
422
+ ]}
423
+ placeholder="15"
424
+ onChange={(e) => {
425
+ const { value } = e;
426
+ handlePageSize(value);
427
+ }}
428
+ />
429
+ </div>
430
+ <div>
431
+ <Paginate
432
+ onNextPage={getMore}
433
+ onPreviousPage={getLess}
434
+ currentPage={pageInfo.currentPage}
435
+ totalPages={pageInfo.totalPages}
436
+ hasNextPage={pageInfo.hasNextPage}
437
+ hasPreviousPage={pageInfo.hasPrevPage}
438
+ recordCount={pageInfo.totalItems}
439
+ refresh={() => {
440
+ fetchData(pageNumber, { search: query });
441
+ }}
442
+ tableColumns={$allColumns}
443
+ bind:hiddenColumns
444
+ />
445
+ </div>
446
+ {#if customAddAction}
447
+ {@render customAddAction?.()}
448
+ {:else}
449
+ <div class:hidden={!showAdd} class="shrink-0">
450
+ <Button onclick={addNew}>{addButtonLabel}</Button>
451
+ </div>
452
+ {/if}
453
+ </div>
454
+ </div>
455
+ </div>
456
+ <div class:custom-container={!fillSpace} class="flex h-full w-full flex-col gap-2">
457
+ {@render customFilter?.({
458
+ pageSize: pageInfo.pageSize ?? 0,
459
+ currentPage: pageInfo.currentPage ?? 0,
460
+ search: query ?? ''
461
+ })}
462
+ {#if busy}
463
+ <TableLoader bodySize={loadingBodySize} headerSize={loadingHeaderSize} />
464
+ {:else}
465
+ <div class="h-full w-full flex-grow">
466
+ <Table
467
+ data={tableData}
468
+ {headerColor}
469
+ bind:hiddenColumns
470
+ onEdit={handleEdit}
471
+ onView={handleView}
472
+ onDelete={handleDelete}
473
+ onRowClicked={handleRowClicked}
474
+ {onActionClicked}
475
+ {handleCheckbox}
476
+ {...otherProps}
477
+ />
478
+ </div>
479
+ {/if}
480
+ </div>
481
+ </div>
482
+
483
+ <Drawer
484
+ bind:hidden={hideForm}
485
+ placement="right"
486
+ activateClickOutside={false}
487
+ title={editorHeading}
488
+ size={modalSize}
489
+ onclose={closeSideModal}
490
+ form
491
+ transitionType={slide}
492
+ transitionParams={{
493
+ duration: 300,
494
+ axis: 'x'
495
+ }}
496
+ onaction={handleAction}
497
+ >
498
+ {@render editor?.({
499
+ recordId: activeEntry.id,
500
+ data: activeEntry,
501
+ onChange: handleFormChange as any,
502
+ readonly: viewing
503
+ })}
504
+
505
+ {#snippet footer()}
506
+ {#if showModalButtons}
507
+ {#if modalFooter}
508
+ {@render modalFooter()}
509
+ {:else if !viewing}
510
+ <Button disabled={!isValid} loading={isLoading} type="submit" class="w-full" value="submit"
511
+ >Submit</Button
512
+ >
513
+ {/if}
514
+ {/if}
515
+ {/snippet}
516
+ </Drawer>
517
+
518
+ <AlertDialog
519
+ bind:open={showAlert}
520
+ message="Are you sure you want to delete this?"
521
+ icon="ion:trash"
522
+ onCancel={closeAlert}
523
+ busy={deleteLoading}
524
+ onYes={() => doDelete(activeEntry?.id)}
525
+ />