@iankibetsh/shframework 5.8.3 → 5.8.4

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
@@ -22,6 +22,9 @@ A robust table component that handles server-side pagination, searching, and cus
22
22
  - **Auto-Label Generation**: Automatically generates human-readable labels from keys if not explicitly provided (e.g., `user.first_name` becomes "First Name").
23
23
  - **Named Slots for Custom Formatting**: Use named slots for columns to provide custom formatting (e.g., `<template #age="{ row }">`).
24
24
  - **Multi-Action Support**: Enable row selection and collective operations with a floating action bar.
25
+ - **Caching & Background Loading**: Uses IndexedDB to cache data. Shows cached data immediately while fetching fresh data in the background (enabled via `enableTableCache` config or `:cache="true"` prop).
26
+ - **Row Links**: Easily define clickable rows with dynamic placeholders (e.g., `:row-link="'/users/{id}'"`).
27
+ - **Search Optimization**: Automatically clears stale data and shows a spinner when searching to ensure fresh results.
25
28
  - **Links & Actions**: Easily define column links and action buttons.
26
29
 
27
30
  ```html
@@ -1,4 +1,36 @@
1
1
 
2
+ .sh-phone{
3
+ display: flex;
4
+ width: 100%;
5
+ align-items: center;
6
+ padding: 0 0.25rem;
7
+ }
8
+ .phone-country{
9
+ width: 2rem;
10
+ border: none;
11
+ align-self: center;
12
+ outline: none !important;
13
+ padding: 0.4rem;
14
+ border-right: 1px solid #0003;
15
+ }
16
+ .phone-number{
17
+ width: calc(100% - 2.2rem);
18
+ border: none;
19
+ align-self: center;
20
+ outline: none;
21
+ margin-bottom: 0;
22
+ padding: 0.4rem;
23
+ }
24
+ .sh-phone img{
25
+ padding: 0.125rem;
26
+ width: 2rem;
27
+ height: 2rem;
28
+ }
29
+ .phone-number::placeholder{
30
+ font-weight: 300;
31
+ opacity: 0.5;
32
+ }
33
+
2
34
  /* Step Container */
3
35
  .sh-form-steps-container {
4
36
  margin-bottom: 2.5rem;
@@ -300,53 +332,8 @@
300
332
  z-index: 1050;
301
333
  min-width: 300px;
302
334
  }
303
-
304
- .sh-phone{
305
- display: flex;
306
- width: 100%;
307
- align-items: center;
308
- padding: 0 0.25rem;
309
- }
310
- .phone-country{
311
- width: 2rem;
312
- border: none;
313
- align-self: center;
314
- outline: none !important;
315
- padding: 0.4rem;
316
- border-right: 1px solid #0003;
317
- }
318
- .phone-number{
319
- width: calc(100% - 2.2rem);
320
- border: none;
321
- align-self: center;
322
- outline: none;
323
- margin-bottom: 0;
324
- padding: 0.4rem;
325
- }
326
- .sh-phone img{
327
- padding: 0.125rem;
328
- width: 2rem;
329
- height: 2rem;
330
- }
331
- .phone-number::placeholder{
332
- font-weight: 300;
333
- opacity: 0.5;
334
- }
335
-
336
- .sh-selected-item{
337
- line-height: unset!important;
338
- }
339
- .sh-suggestion-input{
340
- padding: 0.375rem 0.75rem;
341
- }
342
- .sh-suggest{
343
- margin-bottom: 1rem;
344
- padding: 0rem 0rem;
345
- }
346
- .sh-suggest-control::after{
347
- margin-top: auto;
348
- margin-bottom: auto;
349
- margin-right: 0.255em;
335
+ .cursor-pointer {
336
+ cursor: pointer;
350
337
  }
351
338
 
352
339
  .permissions-main {
@@ -381,6 +368,22 @@
381
368
  flex-grow: 1;
382
369
  }
383
370
 
371
+ .sh-selected-item{
372
+ line-height: unset!important;
373
+ }
374
+ .sh-suggestion-input{
375
+ padding: 0.375rem 0.75rem;
376
+ }
377
+ .sh-suggest{
378
+ margin-bottom: 1rem;
379
+ padding: 0rem 0rem;
380
+ }
381
+ .sh-suggest-control::after{
382
+ margin-top: auto;
383
+ margin-bottom: auto;
384
+ margin-right: 0.255em;
385
+ }
386
+
384
387
  .callout{
385
388
  --bs-link-color-rgb: 110,168,254;
386
389
  --bs-code-color: #e685b5;
package/dist/library.js CHANGED
@@ -19,7 +19,7 @@ var Swal__default = /*#__PURE__*/_interopDefaultLegacy(Swal);
19
19
  var NProgress__default = /*#__PURE__*/_interopDefaultLegacy(NProgress);
20
20
  var ___default = /*#__PURE__*/_interopDefaultLegacy(_);
21
21
 
22
- function setItem (key, value) {
22
+ function setItem$1 (key, value) {
23
23
  let toStore = value;
24
24
  if (typeof value === 'object') {
25
25
  toStore = JSON.stringify(value);
@@ -27,20 +27,20 @@ function setItem (key, value) {
27
27
  return localStorage.setItem(key, toStore)
28
28
  }
29
29
 
30
- function getItem (key) {
30
+ function getItem$1 (key) {
31
31
  try {
32
32
  return JSON.parse(localStorage.getItem(key))
33
33
  } catch (err) {
34
34
  return localStorage.getItem(key)
35
35
  }
36
36
  }
37
- function removeItem (key) {
37
+ function removeItem$1 (key) {
38
38
  return localStorage.removeItem(key)
39
39
  }
40
40
  var shStorage = {
41
- setItem,
42
- getItem,
43
- removeItem
41
+ setItem: setItem$1,
42
+ getItem: getItem$1,
43
+ removeItem: removeItem$1
44
44
  };
45
45
 
46
46
  function swalSuccess(message){
@@ -5464,6 +5464,112 @@ function render$1(_ctx, _cache, $props, $setup, $data, $options) {
5464
5464
  script$e.render = render$1;
5465
5465
  script$e.__file = "src/lib/components/list_templates/Pagination.vue";
5466
5466
 
5467
+ const DB_NAME = 'ShTableCacheDB';
5468
+ const STORE_NAME = 'table_cache';
5469
+ const DB_VERSION = 1;
5470
+
5471
+ let dbPromise = null;
5472
+
5473
+ function getDB() {
5474
+ if (dbPromise) return dbPromise;
5475
+
5476
+ dbPromise = new Promise((resolve, reject) => {
5477
+ const request = indexedDB.open(DB_NAME, DB_VERSION);
5478
+
5479
+ request.onupgradeneeded = (event) => {
5480
+ const db = event.target.result;
5481
+ if (!db.objectStoreNames.contains(STORE_NAME)) {
5482
+ db.createObjectStore(STORE_NAME);
5483
+ }
5484
+ };
5485
+
5486
+ request.onsuccess = (event) => {
5487
+ resolve(event.target.result);
5488
+ };
5489
+
5490
+ request.onerror = (event) => {
5491
+ console.error('IndexedDB error:', event.target.error);
5492
+ reject(event.target.error);
5493
+ };
5494
+ });
5495
+
5496
+ return dbPromise;
5497
+ }
5498
+
5499
+ async function setItem(key, value) {
5500
+ try {
5501
+ const db = await getDB();
5502
+ return new Promise((resolve, reject) => {
5503
+ const transaction = db.transaction([STORE_NAME], 'readwrite');
5504
+ const store = transaction.objectStore(STORE_NAME);
5505
+ const request = store.put(value, key);
5506
+
5507
+ request.onsuccess = () => resolve();
5508
+ request.onerror = (event) => reject(event.target.error);
5509
+ });
5510
+ } catch (error) {
5511
+ console.error('ShIndexedDB setItem error:', error);
5512
+ }
5513
+ }
5514
+
5515
+ async function getItem(key, defaultValue = null) {
5516
+ try {
5517
+ const db = await getDB();
5518
+ return new Promise((resolve, reject) => {
5519
+ const transaction = db.transaction([STORE_NAME], 'readonly');
5520
+ const store = transaction.objectStore(STORE_NAME);
5521
+ const request = store.get(key);
5522
+
5523
+ request.onsuccess = (event) => {
5524
+ resolve(event.target.result !== undefined ? event.target.result : defaultValue);
5525
+ };
5526
+ request.onerror = (event) => reject(event.target.error);
5527
+ });
5528
+ } catch (error) {
5529
+ console.error('ShIndexedDB getItem error:', error);
5530
+ return defaultValue;
5531
+ }
5532
+ }
5533
+
5534
+ async function removeItem(key) {
5535
+ try {
5536
+ const db = await getDB();
5537
+ return new Promise((resolve, reject) => {
5538
+ const transaction = db.transaction([STORE_NAME], 'readwrite');
5539
+ const store = transaction.objectStore(STORE_NAME);
5540
+ const request = store.delete(key);
5541
+
5542
+ request.onsuccess = () => resolve();
5543
+ request.onerror = (event) => reject(event.target.error);
5544
+ });
5545
+ } catch (error) {
5546
+ console.error('ShIndexedDB removeItem error:', error);
5547
+ }
5548
+ }
5549
+
5550
+ async function clear() {
5551
+ try {
5552
+ const db = await getDB();
5553
+ return new Promise((resolve, reject) => {
5554
+ const transaction = db.transaction([STORE_NAME], 'readwrite');
5555
+ const store = transaction.objectStore(STORE_NAME);
5556
+ const request = store.clear();
5557
+
5558
+ request.onsuccess = () => resolve();
5559
+ request.onerror = (event) => reject(event.target.error);
5560
+ });
5561
+ } catch (error) {
5562
+ console.error('ShIndexedDB clear error:', error);
5563
+ }
5564
+ }
5565
+
5566
+ var shIndexedDB = {
5567
+ setItem,
5568
+ getItem,
5569
+ removeItem,
5570
+ clear
5571
+ };
5572
+
5467
5573
  const _hoisted_1$b = { class: "auto-table mt-2" };
5468
5574
  const _hoisted_2$8 = {
5469
5575
  key: 0,
@@ -5648,6 +5754,10 @@ var script$d = {
5648
5754
  selectedRange: [Object, null],
5649
5755
  noRecordsMessage: [String, null],
5650
5756
  multiActions: { type: Array, default: () => [] },
5757
+ // Caching configuration: true to enable, false to disable. If null, respects global configure 'enableTableCache'
5758
+ cache: { type: Boolean, default: null },
5759
+ // Dynamic link for the entire row. Supports placeholders like '/user/{id}'
5760
+ rowLink: [String, null],
5651
5761
  },
5652
5762
  emits: ["rowSelected", "dataReloaded", "dataLoaded"],
5653
5763
  setup(__props, { emit: __emit }) {
@@ -5706,7 +5816,7 @@ const hasRecordsSlot = vue.computed(() => !!slots.records);
5706
5816
  const hasEmptySlot = vue.computed(() => !!slots.empty);
5707
5817
 
5708
5818
  // --- Lifecycle
5709
- vue.onMounted(() => {
5819
+ vue.onMounted(async () => {
5710
5820
  if (props.headers) tableHeaders.value = props.headers;
5711
5821
 
5712
5822
  if (props.actions?.actions) {
@@ -5715,7 +5825,7 @@ vue.onMounted(() => {
5715
5825
  });
5716
5826
  }
5717
5827
 
5718
- if (props.cacheKey) setCachedData();
5828
+ if (shouldCache.value) await setCachedData();
5719
5829
 
5720
5830
  reloadData();
5721
5831
 
@@ -5793,14 +5903,28 @@ const canvasClosed = () => {
5793
5903
  selectedRecord.value = null;
5794
5904
  };
5795
5905
 
5906
+ const router = vueRouter.useRouter();
5796
5907
  const rowSelected = (row) => {
5797
5908
  selectedRecord.value = null;
5798
5909
  setTimeout(() => {
5799
5910
  selectedRecord.value = row;
5800
5911
  emit("rowSelected", row);
5912
+ if (props.rowLink) {
5913
+ router.push(replaceRowLink(props.rowLink, row));
5914
+ }
5801
5915
  }, 100);
5802
5916
  };
5803
5917
 
5918
+ const replaceRowLink = (p, obj) => {
5919
+ let path = p;
5920
+ const matches = path.match(/\{(.*?)\}/g);
5921
+ matches?.forEach((k) => {
5922
+ const key = k.replace("{", "").replace("}", "");
5923
+ path = path.replace(`{${key}}`, obj[key]);
5924
+ });
5925
+ return path;
5926
+ };
5927
+
5804
5928
  const changeKey = (key, value) => {
5805
5929
  if (key === "order_by") {
5806
5930
  order_by.value = value;
@@ -5941,20 +6065,46 @@ const exportData = () => {
5941
6065
  });
5942
6066
  };
5943
6067
 
5944
- const setCachedData = () => {
5945
- if (props.cacheKey) {
5946
- records.value = shStorage.getItem("sh_table_cache_" + props.cacheKey, null);
6068
+ // Attempts to load data from IndexedDB before API call
6069
+ const setCachedData = async () => {
6070
+ if (shouldCache.value) {
6071
+ const cached = await shIndexedDB.getItem(computedCacheKey.value, null);
6072
+ if (cached) {
6073
+ records.value = cached;
6074
+ // Set to 'done' immediately to show cached data without initial spinner
6075
+ loading.value = "done";
6076
+ }
5947
6077
  }
5948
6078
  };
5949
6079
 
6080
+ // Determines if caching should be active based on component props or global configuration
6081
+ const shouldCache = vue.computed(() => {
6082
+ if (props.cache !== null) return props.cache;
6083
+ return shRepo.getShConfig("enableTableCache", false);
6084
+ });
6085
+
6086
+ // Generates a unique, slug-safe key for IndexedDB storage
6087
+ const computedCacheKey = vue.computed(() => {
6088
+ if (props.cacheKey) return "sh_table_cache_" + props.cacheKey;
6089
+ const keyBase = props.endPoint || props.query || "default";
6090
+ const safeBase = keyBase.replace(/[^a-z0-9]/gi, "_").toLowerCase();
6091
+ return "sh_table_cache_" + safeBase;
6092
+ });
6093
+
5950
6094
  // Main loader
6095
+ // Main data fetcher. Handles background updates when cache is present
5951
6096
  const reloadData = (newPage, append) => {
5952
6097
  if (typeof newPage !== "undefined") page.value = newPage;
5953
6098
 
5954
- if (props.cacheKey && records.value !== null) {
6099
+ // If we have cached data and not searching, we don't show the initial loading spinner
6100
+ if (shouldCache.value && records.value && records.value.length > 0 && !filter_value.value) {
5955
6101
  loading.value = "done";
5956
6102
  } else if (!append) {
5957
6103
  loading.value = "loading";
6104
+ // Clear records when searching to ensure we show fresh results
6105
+ if (filter_value.value) {
6106
+ records.value = [];
6107
+ }
5958
6108
  }
5959
6109
 
5960
6110
  let data = {
@@ -5993,8 +6143,8 @@ const reloadData = (newPage, append) => {
5993
6143
  const response = req.data.data;
5994
6144
  emit("dataLoaded", response);
5995
6145
 
5996
- if (page.value < 2 && props.cacheKey) {
5997
- shStorage.setItem("sh_table_cache_" + props.cacheKey, response.data);
6146
+ if (page.value < 2 && shouldCache.value) {
6147
+ shIndexedDB.setItem(computedCacheKey.value, response.data);
5998
6148
  }
5999
6149
 
6000
6150
  pagination_data.value = {
@@ -6179,7 +6329,7 @@ return (_ctx, _cache) => {
6179
6329
  : vue.createCommentVNode("v-if", true),
6180
6330
  (hasDefaultSlot.value)
6181
6331
  ? (vue.openBlock(), vue.createElementBlock(vue.Fragment, { key: 2 }, [
6182
- (loading.value === 'loading')
6332
+ (loading.value === 'loading' && records.value.length === 0)
6183
6333
  ? (vue.openBlock(), vue.createElementBlock("div", _hoisted_9$2, [...(_cache[13] || (_cache[13] = [
6184
6334
  vue.createElementVNode("div", {
6185
6335
  class: "spinner-border",
@@ -6188,12 +6338,12 @@ return (_ctx, _cache) => {
6188
6338
  vue.createElementVNode("span", { class: "visually-hidden" }, "Loading...")
6189
6339
  ], -1 /* CACHED */)
6190
6340
  ]))]))
6191
- : (loading.value === 'error')
6341
+ : (loading.value === 'error' && records.value.length === 0)
6192
6342
  ? (vue.openBlock(), vue.createElementBlock("div", _hoisted_10$2, [
6193
6343
  vue.createElementVNode("span", null, vue.toDisplayString(loading_error.value), 1 /* TEXT */)
6194
6344
  ]))
6195
6345
  : vue.createCommentVNode("v-if", true),
6196
- (loading.value === 'done')
6346
+ (loading.value === 'done' || records.value.length > 0)
6197
6347
  ? (vue.openBlock(), vue.createElementBlock(vue.Fragment, { key: 2 }, [
6198
6348
  (records.value.length === 0)
6199
6349
  ? (vue.openBlock(), vue.createElementBlock(vue.Fragment, { key: 0 }, [
@@ -6217,7 +6367,7 @@ return (_ctx, _cache) => {
6217
6367
  ], 64 /* STABLE_FRAGMENT */))
6218
6368
  : (hasRecordsSlot.value)
6219
6369
  ? (vue.openBlock(), vue.createElementBlock(vue.Fragment, { key: 3 }, [
6220
- (loading.value === 'loading' && !__props.cacheKey)
6370
+ (loading.value === 'loading' && records.value.length === 0)
6221
6371
  ? (vue.openBlock(), vue.createElementBlock("div", _hoisted_12$1, [...(_cache[15] || (_cache[15] = [
6222
6372
  vue.createElementVNode("div", {
6223
6373
  class: "spinner-border",
@@ -6226,12 +6376,12 @@ return (_ctx, _cache) => {
6226
6376
  vue.createElementVNode("span", { class: "visually-hidden" }, "Loading...")
6227
6377
  ], -1 /* CACHED */)
6228
6378
  ]))]))
6229
- : (loading.value === 'error' && !__props.cacheKey)
6379
+ : (loading.value === 'error' && records.value.length === 0)
6230
6380
  ? (vue.openBlock(), vue.createElementBlock("div", _hoisted_13$1, [
6231
6381
  vue.createElementVNode("span", null, vue.toDisplayString(loading_error.value), 1 /* TEXT */)
6232
6382
  ]))
6233
6383
  : vue.createCommentVNode("v-if", true),
6234
- (loading.value === 'done' || __props.cacheKey)
6384
+ (loading.value === 'done' || records.value.length > 0)
6235
6385
  ? (vue.openBlock(), vue.createElementBlock(vue.Fragment, { key: 2 }, [
6236
6386
  (!records.value || records.value.length === 0)
6237
6387
  ? (vue.openBlock(), vue.createElementBlock(vue.Fragment, { key: 0 }, [
@@ -6308,7 +6458,7 @@ return (_ctx, _cache) => {
6308
6458
  ])
6309
6459
  ]),
6310
6460
  vue.createElementVNode("tbody", _hoisted_22, [
6311
- (loading.value === 'loading')
6461
+ (loading.value === 'loading' && records.value.length === 0)
6312
6462
  ? (vue.openBlock(), vue.createElementBlock("tr", _hoisted_23, [
6313
6463
  vue.createElementVNode("td", {
6314
6464
  colspan:
@@ -6327,7 +6477,7 @@ return (_ctx, _cache) => {
6327
6477
  ], -1 /* CACHED */)
6328
6478
  ]))], 8 /* PROPS */, _hoisted_24)
6329
6479
  ]))
6330
- : (loading.value === 'error')
6480
+ : (loading.value === 'error' && records.value.length === 0)
6331
6481
  ? (vue.openBlock(), vue.createElementBlock("tr", _hoisted_25, [
6332
6482
  vue.createElementVNode("td", {
6333
6483
  colspan:
@@ -6358,7 +6508,7 @@ return (_ctx, _cache) => {
6358
6508
  ? (vue.openBlock(true), vue.createElementBlock(vue.Fragment, { key: 3 }, vue.renderList(records.value, (record, index) => {
6359
6509
  return (vue.openBlock(), vue.createElementBlock("tr", {
6360
6510
  key: record.id,
6361
- class: vue.normalizeClass(record.class),
6511
+ class: vue.normalizeClass([record.class, props.rowLink ? 'cursor-pointer' : '']),
6362
6512
  onClick: $event => (rowSelected(record))
6363
6513
  }, [
6364
6514
  (activeMultiActions.value.length > 0)
@@ -6455,7 +6605,7 @@ return (_ctx, _cache) => {
6455
6605
  (vue.openBlock(true), vue.createElementBlock(vue.Fragment, null, vue.renderList(records.value, (record, index) => {
6456
6606
  return (vue.openBlock(), vue.createElementBlock("div", {
6457
6607
  key: record.id,
6458
- class: "single-mobile-req bg-light p-3",
6608
+ class: vue.normalizeClass(["single-mobile-req bg-light p-3", props.rowLink ? 'cursor-pointer' : '']),
6459
6609
  onClick: $event => (rowSelected(record))
6460
6610
  }, [
6461
6611
  (activeMultiActions.value.length > 0)
@@ -6560,7 +6710,7 @@ return (_ctx, _cache) => {
6560
6710
  }, null, 8 /* PROPS */, ["actions", "record"])
6561
6711
  ]))
6562
6712
  : vue.createCommentVNode("v-if", true)
6563
- ], 8 /* PROPS */, _hoisted_44))
6713
+ ], 10 /* CLASS, PROPS */, _hoisted_44))
6564
6714
  }), 128 /* KEYED_FRAGMENT */))
6565
6715
  ]))
6566
6716
  : (vue.openBlock(), vue.createElementBlock("div", _hoisted_62, [
@@ -8397,8 +8547,8 @@ const ShFrontend = {
8397
8547
  }
8398
8548
  //filter unwanted config items from options to be put in local storage
8399
8549
  const removeKeys = ['formTextInput','router','shFormElementClasses'];
8400
- const allowKeys = [];
8401
- Object.keys(options).map(key=> ((!['string','integer','number'].includes(typeof options[key]) && !allowKeys.includes(key)) || removeKeys.includes(key)) && delete options[key]);
8550
+ const allowKeys = ['enableTableCache'];
8551
+ Object.keys(options).map(key=> ((!['string','integer','number','boolean'].includes(typeof options[key]) && !allowKeys.includes(key)) || removeKeys.includes(key)) && delete options[key]);
8402
8552
 
8403
8553
  shStorage.setItem('ShConfig',options);
8404
8554
  }
package/dist/library.mjs CHANGED
@@ -5,10 +5,10 @@ import { Modal, Offcanvas } from 'bootstrap';
5
5
  import NProgress from 'nprogress';
6
6
  import { ref, computed, watch, onMounted, openBlock, createElementBlock, createElementVNode, createTextVNode, toDisplayString, createCommentVNode, withDirectives, Fragment, renderList, unref, vModelSelect, vModelText, normalizeClass, createBlock, resolveDynamicComponent, resolveComponent, inject, useTemplateRef, mergeProps, vShow, renderSlot, normalizeStyle, Teleport, createVNode, withCtx, useSlots, onBeforeUnmount, reactive, vModelCheckbox, withModifiers, resolveDirective, shallowRef, markRaw, isRef } from 'vue';
7
7
  import _ from 'lodash';
8
- import { useRoute, useRouter } from 'vue-router';
8
+ import { useRouter, useRoute } from 'vue-router';
9
9
  import { defineStore, storeToRefs } from 'pinia';
10
10
 
11
- function setItem (key, value) {
11
+ function setItem$1 (key, value) {
12
12
  let toStore = value;
13
13
  if (typeof value === 'object') {
14
14
  toStore = JSON.stringify(value);
@@ -16,20 +16,20 @@ function setItem (key, value) {
16
16
  return localStorage.setItem(key, toStore)
17
17
  }
18
18
 
19
- function getItem (key) {
19
+ function getItem$1 (key) {
20
20
  try {
21
21
  return JSON.parse(localStorage.getItem(key))
22
22
  } catch (err) {
23
23
  return localStorage.getItem(key)
24
24
  }
25
25
  }
26
- function removeItem (key) {
26
+ function removeItem$1 (key) {
27
27
  return localStorage.removeItem(key)
28
28
  }
29
29
  var shStorage = {
30
- setItem,
31
- getItem,
32
- removeItem
30
+ setItem: setItem$1,
31
+ getItem: getItem$1,
32
+ removeItem: removeItem$1
33
33
  };
34
34
 
35
35
  function swalSuccess(message){
@@ -5453,6 +5453,112 @@ function render$1(_ctx, _cache, $props, $setup, $data, $options) {
5453
5453
  script$e.render = render$1;
5454
5454
  script$e.__file = "src/lib/components/list_templates/Pagination.vue";
5455
5455
 
5456
+ const DB_NAME = 'ShTableCacheDB';
5457
+ const STORE_NAME = 'table_cache';
5458
+ const DB_VERSION = 1;
5459
+
5460
+ let dbPromise = null;
5461
+
5462
+ function getDB() {
5463
+ if (dbPromise) return dbPromise;
5464
+
5465
+ dbPromise = new Promise((resolve, reject) => {
5466
+ const request = indexedDB.open(DB_NAME, DB_VERSION);
5467
+
5468
+ request.onupgradeneeded = (event) => {
5469
+ const db = event.target.result;
5470
+ if (!db.objectStoreNames.contains(STORE_NAME)) {
5471
+ db.createObjectStore(STORE_NAME);
5472
+ }
5473
+ };
5474
+
5475
+ request.onsuccess = (event) => {
5476
+ resolve(event.target.result);
5477
+ };
5478
+
5479
+ request.onerror = (event) => {
5480
+ console.error('IndexedDB error:', event.target.error);
5481
+ reject(event.target.error);
5482
+ };
5483
+ });
5484
+
5485
+ return dbPromise;
5486
+ }
5487
+
5488
+ async function setItem(key, value) {
5489
+ try {
5490
+ const db = await getDB();
5491
+ return new Promise((resolve, reject) => {
5492
+ const transaction = db.transaction([STORE_NAME], 'readwrite');
5493
+ const store = transaction.objectStore(STORE_NAME);
5494
+ const request = store.put(value, key);
5495
+
5496
+ request.onsuccess = () => resolve();
5497
+ request.onerror = (event) => reject(event.target.error);
5498
+ });
5499
+ } catch (error) {
5500
+ console.error('ShIndexedDB setItem error:', error);
5501
+ }
5502
+ }
5503
+
5504
+ async function getItem(key, defaultValue = null) {
5505
+ try {
5506
+ const db = await getDB();
5507
+ return new Promise((resolve, reject) => {
5508
+ const transaction = db.transaction([STORE_NAME], 'readonly');
5509
+ const store = transaction.objectStore(STORE_NAME);
5510
+ const request = store.get(key);
5511
+
5512
+ request.onsuccess = (event) => {
5513
+ resolve(event.target.result !== undefined ? event.target.result : defaultValue);
5514
+ };
5515
+ request.onerror = (event) => reject(event.target.error);
5516
+ });
5517
+ } catch (error) {
5518
+ console.error('ShIndexedDB getItem error:', error);
5519
+ return defaultValue;
5520
+ }
5521
+ }
5522
+
5523
+ async function removeItem(key) {
5524
+ try {
5525
+ const db = await getDB();
5526
+ return new Promise((resolve, reject) => {
5527
+ const transaction = db.transaction([STORE_NAME], 'readwrite');
5528
+ const store = transaction.objectStore(STORE_NAME);
5529
+ const request = store.delete(key);
5530
+
5531
+ request.onsuccess = () => resolve();
5532
+ request.onerror = (event) => reject(event.target.error);
5533
+ });
5534
+ } catch (error) {
5535
+ console.error('ShIndexedDB removeItem error:', error);
5536
+ }
5537
+ }
5538
+
5539
+ async function clear() {
5540
+ try {
5541
+ const db = await getDB();
5542
+ return new Promise((resolve, reject) => {
5543
+ const transaction = db.transaction([STORE_NAME], 'readwrite');
5544
+ const store = transaction.objectStore(STORE_NAME);
5545
+ const request = store.clear();
5546
+
5547
+ request.onsuccess = () => resolve();
5548
+ request.onerror = (event) => reject(event.target.error);
5549
+ });
5550
+ } catch (error) {
5551
+ console.error('ShIndexedDB clear error:', error);
5552
+ }
5553
+ }
5554
+
5555
+ var shIndexedDB = {
5556
+ setItem,
5557
+ getItem,
5558
+ removeItem,
5559
+ clear
5560
+ };
5561
+
5456
5562
  const _hoisted_1$b = { class: "auto-table mt-2" };
5457
5563
  const _hoisted_2$8 = {
5458
5564
  key: 0,
@@ -5637,6 +5743,10 @@ var script$d = {
5637
5743
  selectedRange: [Object, null],
5638
5744
  noRecordsMessage: [String, null],
5639
5745
  multiActions: { type: Array, default: () => [] },
5746
+ // Caching configuration: true to enable, false to disable. If null, respects global configure 'enableTableCache'
5747
+ cache: { type: Boolean, default: null },
5748
+ // Dynamic link for the entire row. Supports placeholders like '/user/{id}'
5749
+ rowLink: [String, null],
5640
5750
  },
5641
5751
  emits: ["rowSelected", "dataReloaded", "dataLoaded"],
5642
5752
  setup(__props, { emit: __emit }) {
@@ -5695,7 +5805,7 @@ const hasRecordsSlot = computed(() => !!slots.records);
5695
5805
  const hasEmptySlot = computed(() => !!slots.empty);
5696
5806
 
5697
5807
  // --- Lifecycle
5698
- onMounted(() => {
5808
+ onMounted(async () => {
5699
5809
  if (props.headers) tableHeaders.value = props.headers;
5700
5810
 
5701
5811
  if (props.actions?.actions) {
@@ -5704,7 +5814,7 @@ onMounted(() => {
5704
5814
  });
5705
5815
  }
5706
5816
 
5707
- if (props.cacheKey) setCachedData();
5817
+ if (shouldCache.value) await setCachedData();
5708
5818
 
5709
5819
  reloadData();
5710
5820
 
@@ -5782,14 +5892,28 @@ const canvasClosed = () => {
5782
5892
  selectedRecord.value = null;
5783
5893
  };
5784
5894
 
5895
+ const router = useRouter();
5785
5896
  const rowSelected = (row) => {
5786
5897
  selectedRecord.value = null;
5787
5898
  setTimeout(() => {
5788
5899
  selectedRecord.value = row;
5789
5900
  emit("rowSelected", row);
5901
+ if (props.rowLink) {
5902
+ router.push(replaceRowLink(props.rowLink, row));
5903
+ }
5790
5904
  }, 100);
5791
5905
  };
5792
5906
 
5907
+ const replaceRowLink = (p, obj) => {
5908
+ let path = p;
5909
+ const matches = path.match(/\{(.*?)\}/g);
5910
+ matches?.forEach((k) => {
5911
+ const key = k.replace("{", "").replace("}", "");
5912
+ path = path.replace(`{${key}}`, obj[key]);
5913
+ });
5914
+ return path;
5915
+ };
5916
+
5793
5917
  const changeKey = (key, value) => {
5794
5918
  if (key === "order_by") {
5795
5919
  order_by.value = value;
@@ -5930,20 +6054,46 @@ const exportData = () => {
5930
6054
  });
5931
6055
  };
5932
6056
 
5933
- const setCachedData = () => {
5934
- if (props.cacheKey) {
5935
- records.value = shStorage.getItem("sh_table_cache_" + props.cacheKey, null);
6057
+ // Attempts to load data from IndexedDB before API call
6058
+ const setCachedData = async () => {
6059
+ if (shouldCache.value) {
6060
+ const cached = await shIndexedDB.getItem(computedCacheKey.value, null);
6061
+ if (cached) {
6062
+ records.value = cached;
6063
+ // Set to 'done' immediately to show cached data without initial spinner
6064
+ loading.value = "done";
6065
+ }
5936
6066
  }
5937
6067
  };
5938
6068
 
6069
+ // Determines if caching should be active based on component props or global configuration
6070
+ const shouldCache = computed(() => {
6071
+ if (props.cache !== null) return props.cache;
6072
+ return shRepo.getShConfig("enableTableCache", false);
6073
+ });
6074
+
6075
+ // Generates a unique, slug-safe key for IndexedDB storage
6076
+ const computedCacheKey = computed(() => {
6077
+ if (props.cacheKey) return "sh_table_cache_" + props.cacheKey;
6078
+ const keyBase = props.endPoint || props.query || "default";
6079
+ const safeBase = keyBase.replace(/[^a-z0-9]/gi, "_").toLowerCase();
6080
+ return "sh_table_cache_" + safeBase;
6081
+ });
6082
+
5939
6083
  // Main loader
6084
+ // Main data fetcher. Handles background updates when cache is present
5940
6085
  const reloadData = (newPage, append) => {
5941
6086
  if (typeof newPage !== "undefined") page.value = newPage;
5942
6087
 
5943
- if (props.cacheKey && records.value !== null) {
6088
+ // If we have cached data and not searching, we don't show the initial loading spinner
6089
+ if (shouldCache.value && records.value && records.value.length > 0 && !filter_value.value) {
5944
6090
  loading.value = "done";
5945
6091
  } else if (!append) {
5946
6092
  loading.value = "loading";
6093
+ // Clear records when searching to ensure we show fresh results
6094
+ if (filter_value.value) {
6095
+ records.value = [];
6096
+ }
5947
6097
  }
5948
6098
 
5949
6099
  let data = {
@@ -5982,8 +6132,8 @@ const reloadData = (newPage, append) => {
5982
6132
  const response = req.data.data;
5983
6133
  emit("dataLoaded", response);
5984
6134
 
5985
- if (page.value < 2 && props.cacheKey) {
5986
- shStorage.setItem("sh_table_cache_" + props.cacheKey, response.data);
6135
+ if (page.value < 2 && shouldCache.value) {
6136
+ shIndexedDB.setItem(computedCacheKey.value, response.data);
5987
6137
  }
5988
6138
 
5989
6139
  pagination_data.value = {
@@ -6168,7 +6318,7 @@ return (_ctx, _cache) => {
6168
6318
  : createCommentVNode("v-if", true),
6169
6319
  (hasDefaultSlot.value)
6170
6320
  ? (openBlock(), createElementBlock(Fragment, { key: 2 }, [
6171
- (loading.value === 'loading')
6321
+ (loading.value === 'loading' && records.value.length === 0)
6172
6322
  ? (openBlock(), createElementBlock("div", _hoisted_9$2, [...(_cache[13] || (_cache[13] = [
6173
6323
  createElementVNode("div", {
6174
6324
  class: "spinner-border",
@@ -6177,12 +6327,12 @@ return (_ctx, _cache) => {
6177
6327
  createElementVNode("span", { class: "visually-hidden" }, "Loading...")
6178
6328
  ], -1 /* CACHED */)
6179
6329
  ]))]))
6180
- : (loading.value === 'error')
6330
+ : (loading.value === 'error' && records.value.length === 0)
6181
6331
  ? (openBlock(), createElementBlock("div", _hoisted_10$2, [
6182
6332
  createElementVNode("span", null, toDisplayString(loading_error.value), 1 /* TEXT */)
6183
6333
  ]))
6184
6334
  : createCommentVNode("v-if", true),
6185
- (loading.value === 'done')
6335
+ (loading.value === 'done' || records.value.length > 0)
6186
6336
  ? (openBlock(), createElementBlock(Fragment, { key: 2 }, [
6187
6337
  (records.value.length === 0)
6188
6338
  ? (openBlock(), createElementBlock(Fragment, { key: 0 }, [
@@ -6206,7 +6356,7 @@ return (_ctx, _cache) => {
6206
6356
  ], 64 /* STABLE_FRAGMENT */))
6207
6357
  : (hasRecordsSlot.value)
6208
6358
  ? (openBlock(), createElementBlock(Fragment, { key: 3 }, [
6209
- (loading.value === 'loading' && !__props.cacheKey)
6359
+ (loading.value === 'loading' && records.value.length === 0)
6210
6360
  ? (openBlock(), createElementBlock("div", _hoisted_12$1, [...(_cache[15] || (_cache[15] = [
6211
6361
  createElementVNode("div", {
6212
6362
  class: "spinner-border",
@@ -6215,12 +6365,12 @@ return (_ctx, _cache) => {
6215
6365
  createElementVNode("span", { class: "visually-hidden" }, "Loading...")
6216
6366
  ], -1 /* CACHED */)
6217
6367
  ]))]))
6218
- : (loading.value === 'error' && !__props.cacheKey)
6368
+ : (loading.value === 'error' && records.value.length === 0)
6219
6369
  ? (openBlock(), createElementBlock("div", _hoisted_13$1, [
6220
6370
  createElementVNode("span", null, toDisplayString(loading_error.value), 1 /* TEXT */)
6221
6371
  ]))
6222
6372
  : createCommentVNode("v-if", true),
6223
- (loading.value === 'done' || __props.cacheKey)
6373
+ (loading.value === 'done' || records.value.length > 0)
6224
6374
  ? (openBlock(), createElementBlock(Fragment, { key: 2 }, [
6225
6375
  (!records.value || records.value.length === 0)
6226
6376
  ? (openBlock(), createElementBlock(Fragment, { key: 0 }, [
@@ -6297,7 +6447,7 @@ return (_ctx, _cache) => {
6297
6447
  ])
6298
6448
  ]),
6299
6449
  createElementVNode("tbody", _hoisted_22, [
6300
- (loading.value === 'loading')
6450
+ (loading.value === 'loading' && records.value.length === 0)
6301
6451
  ? (openBlock(), createElementBlock("tr", _hoisted_23, [
6302
6452
  createElementVNode("td", {
6303
6453
  colspan:
@@ -6316,7 +6466,7 @@ return (_ctx, _cache) => {
6316
6466
  ], -1 /* CACHED */)
6317
6467
  ]))], 8 /* PROPS */, _hoisted_24)
6318
6468
  ]))
6319
- : (loading.value === 'error')
6469
+ : (loading.value === 'error' && records.value.length === 0)
6320
6470
  ? (openBlock(), createElementBlock("tr", _hoisted_25, [
6321
6471
  createElementVNode("td", {
6322
6472
  colspan:
@@ -6347,7 +6497,7 @@ return (_ctx, _cache) => {
6347
6497
  ? (openBlock(true), createElementBlock(Fragment, { key: 3 }, renderList(records.value, (record, index) => {
6348
6498
  return (openBlock(), createElementBlock("tr", {
6349
6499
  key: record.id,
6350
- class: normalizeClass(record.class),
6500
+ class: normalizeClass([record.class, props.rowLink ? 'cursor-pointer' : '']),
6351
6501
  onClick: $event => (rowSelected(record))
6352
6502
  }, [
6353
6503
  (activeMultiActions.value.length > 0)
@@ -6444,7 +6594,7 @@ return (_ctx, _cache) => {
6444
6594
  (openBlock(true), createElementBlock(Fragment, null, renderList(records.value, (record, index) => {
6445
6595
  return (openBlock(), createElementBlock("div", {
6446
6596
  key: record.id,
6447
- class: "single-mobile-req bg-light p-3",
6597
+ class: normalizeClass(["single-mobile-req bg-light p-3", props.rowLink ? 'cursor-pointer' : '']),
6448
6598
  onClick: $event => (rowSelected(record))
6449
6599
  }, [
6450
6600
  (activeMultiActions.value.length > 0)
@@ -6549,7 +6699,7 @@ return (_ctx, _cache) => {
6549
6699
  }, null, 8 /* PROPS */, ["actions", "record"])
6550
6700
  ]))
6551
6701
  : createCommentVNode("v-if", true)
6552
- ], 8 /* PROPS */, _hoisted_44))
6702
+ ], 10 /* CLASS, PROPS */, _hoisted_44))
6553
6703
  }), 128 /* KEYED_FRAGMENT */))
6554
6704
  ]))
6555
6705
  : (openBlock(), createElementBlock("div", _hoisted_62, [
@@ -8386,8 +8536,8 @@ const ShFrontend = {
8386
8536
  }
8387
8537
  //filter unwanted config items from options to be put in local storage
8388
8538
  const removeKeys = ['formTextInput','router','shFormElementClasses'];
8389
- const allowKeys = [];
8390
- Object.keys(options).map(key=> ((!['string','integer','number'].includes(typeof options[key]) && !allowKeys.includes(key)) || removeKeys.includes(key)) && delete options[key]);
8539
+ const allowKeys = ['enableTableCache'];
8540
+ Object.keys(options).map(key=> ((!['string','integer','number','boolean'].includes(typeof options[key]) && !allowKeys.includes(key)) || removeKeys.includes(key)) && delete options[key]);
8391
8541
 
8392
8542
  shStorage.setItem('ShConfig',options);
8393
8543
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iankibetsh/shframework",
3
- "version": "5.8.3",
3
+ "version": "5.8.4",
4
4
  "description": "Vue library for handling laravel backend",
5
5
  "repository": {
6
6
  "type": "git",