@iankibetsh/shframework 5.8.3 → 5.8.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
@@ -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
@@ -300,6 +300,9 @@
300
300
  z-index: 1050;
301
301
  min-width: 300px;
302
302
  }
303
+ .cursor-pointer {
304
+ cursor: pointer;
305
+ }
303
306
 
304
307
  .sh-phone{
305
308
  display: flex;
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,52 @@ 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
+ let keyBase = props.endPoint || props.query || "default";
6090
+
6091
+ // Include date range in the key if active
6092
+ if (from.value || to.value || period.value) {
6093
+ keyBase += `_${from.value}_${to.value}_${period.value}`;
6094
+ }
6095
+
6096
+ const safeBase = keyBase.replace(/[^a-z0-9]/gi, "_").toLowerCase();
6097
+ return "sh_table_cache_" + safeBase;
6098
+ });
6099
+
5950
6100
  // Main loader
6101
+ // Main data fetcher. Handles background updates when cache is present
5951
6102
  const reloadData = (newPage, append) => {
5952
6103
  if (typeof newPage !== "undefined") page.value = newPage;
5953
6104
 
5954
- if (props.cacheKey && records.value !== null) {
6105
+ // If we have cached data and not searching, we don't show the initial loading spinner
6106
+ if (shouldCache.value && records.value && records.value.length > 0 && !filter_value.value) {
5955
6107
  loading.value = "done";
5956
6108
  } else if (!append) {
5957
6109
  loading.value = "loading";
6110
+ // Clear records when searching to ensure we show fresh results
6111
+ if (filter_value.value) {
6112
+ records.value = [];
6113
+ }
5958
6114
  }
5959
6115
 
5960
6116
  let data = {
@@ -5993,8 +6149,8 @@ const reloadData = (newPage, append) => {
5993
6149
  const response = req.data.data;
5994
6150
  emit("dataLoaded", response);
5995
6151
 
5996
- if (page.value < 2 && props.cacheKey) {
5997
- shStorage.setItem("sh_table_cache_" + props.cacheKey, response.data);
6152
+ if (page.value < 2 && shouldCache.value) {
6153
+ shIndexedDB.setItem(computedCacheKey.value, response.data);
5998
6154
  }
5999
6155
 
6000
6156
  pagination_data.value = {
@@ -6179,7 +6335,7 @@ return (_ctx, _cache) => {
6179
6335
  : vue.createCommentVNode("v-if", true),
6180
6336
  (hasDefaultSlot.value)
6181
6337
  ? (vue.openBlock(), vue.createElementBlock(vue.Fragment, { key: 2 }, [
6182
- (loading.value === 'loading')
6338
+ (loading.value === 'loading' && records.value.length === 0)
6183
6339
  ? (vue.openBlock(), vue.createElementBlock("div", _hoisted_9$2, [...(_cache[13] || (_cache[13] = [
6184
6340
  vue.createElementVNode("div", {
6185
6341
  class: "spinner-border",
@@ -6188,12 +6344,12 @@ return (_ctx, _cache) => {
6188
6344
  vue.createElementVNode("span", { class: "visually-hidden" }, "Loading...")
6189
6345
  ], -1 /* CACHED */)
6190
6346
  ]))]))
6191
- : (loading.value === 'error')
6347
+ : (loading.value === 'error' && records.value.length === 0)
6192
6348
  ? (vue.openBlock(), vue.createElementBlock("div", _hoisted_10$2, [
6193
6349
  vue.createElementVNode("span", null, vue.toDisplayString(loading_error.value), 1 /* TEXT */)
6194
6350
  ]))
6195
6351
  : vue.createCommentVNode("v-if", true),
6196
- (loading.value === 'done')
6352
+ (loading.value === 'done' || records.value.length > 0)
6197
6353
  ? (vue.openBlock(), vue.createElementBlock(vue.Fragment, { key: 2 }, [
6198
6354
  (records.value.length === 0)
6199
6355
  ? (vue.openBlock(), vue.createElementBlock(vue.Fragment, { key: 0 }, [
@@ -6217,7 +6373,7 @@ return (_ctx, _cache) => {
6217
6373
  ], 64 /* STABLE_FRAGMENT */))
6218
6374
  : (hasRecordsSlot.value)
6219
6375
  ? (vue.openBlock(), vue.createElementBlock(vue.Fragment, { key: 3 }, [
6220
- (loading.value === 'loading' && !__props.cacheKey)
6376
+ (loading.value === 'loading' && records.value.length === 0)
6221
6377
  ? (vue.openBlock(), vue.createElementBlock("div", _hoisted_12$1, [...(_cache[15] || (_cache[15] = [
6222
6378
  vue.createElementVNode("div", {
6223
6379
  class: "spinner-border",
@@ -6226,12 +6382,12 @@ return (_ctx, _cache) => {
6226
6382
  vue.createElementVNode("span", { class: "visually-hidden" }, "Loading...")
6227
6383
  ], -1 /* CACHED */)
6228
6384
  ]))]))
6229
- : (loading.value === 'error' && !__props.cacheKey)
6385
+ : (loading.value === 'error' && records.value.length === 0)
6230
6386
  ? (vue.openBlock(), vue.createElementBlock("div", _hoisted_13$1, [
6231
6387
  vue.createElementVNode("span", null, vue.toDisplayString(loading_error.value), 1 /* TEXT */)
6232
6388
  ]))
6233
6389
  : vue.createCommentVNode("v-if", true),
6234
- (loading.value === 'done' || __props.cacheKey)
6390
+ (loading.value === 'done' || records.value.length > 0)
6235
6391
  ? (vue.openBlock(), vue.createElementBlock(vue.Fragment, { key: 2 }, [
6236
6392
  (!records.value || records.value.length === 0)
6237
6393
  ? (vue.openBlock(), vue.createElementBlock(vue.Fragment, { key: 0 }, [
@@ -6308,7 +6464,7 @@ return (_ctx, _cache) => {
6308
6464
  ])
6309
6465
  ]),
6310
6466
  vue.createElementVNode("tbody", _hoisted_22, [
6311
- (loading.value === 'loading')
6467
+ (loading.value === 'loading' && records.value.length === 0)
6312
6468
  ? (vue.openBlock(), vue.createElementBlock("tr", _hoisted_23, [
6313
6469
  vue.createElementVNode("td", {
6314
6470
  colspan:
@@ -6327,7 +6483,7 @@ return (_ctx, _cache) => {
6327
6483
  ], -1 /* CACHED */)
6328
6484
  ]))], 8 /* PROPS */, _hoisted_24)
6329
6485
  ]))
6330
- : (loading.value === 'error')
6486
+ : (loading.value === 'error' && records.value.length === 0)
6331
6487
  ? (vue.openBlock(), vue.createElementBlock("tr", _hoisted_25, [
6332
6488
  vue.createElementVNode("td", {
6333
6489
  colspan:
@@ -6358,7 +6514,7 @@ return (_ctx, _cache) => {
6358
6514
  ? (vue.openBlock(true), vue.createElementBlock(vue.Fragment, { key: 3 }, vue.renderList(records.value, (record, index) => {
6359
6515
  return (vue.openBlock(), vue.createElementBlock("tr", {
6360
6516
  key: record.id,
6361
- class: vue.normalizeClass(record.class),
6517
+ class: vue.normalizeClass([record.class, props.rowLink ? 'cursor-pointer' : '']),
6362
6518
  onClick: $event => (rowSelected(record))
6363
6519
  }, [
6364
6520
  (activeMultiActions.value.length > 0)
@@ -6455,7 +6611,7 @@ return (_ctx, _cache) => {
6455
6611
  (vue.openBlock(true), vue.createElementBlock(vue.Fragment, null, vue.renderList(records.value, (record, index) => {
6456
6612
  return (vue.openBlock(), vue.createElementBlock("div", {
6457
6613
  key: record.id,
6458
- class: "single-mobile-req bg-light p-3",
6614
+ class: vue.normalizeClass(["single-mobile-req bg-light p-3", props.rowLink ? 'cursor-pointer' : '']),
6459
6615
  onClick: $event => (rowSelected(record))
6460
6616
  }, [
6461
6617
  (activeMultiActions.value.length > 0)
@@ -6560,7 +6716,7 @@ return (_ctx, _cache) => {
6560
6716
  }, null, 8 /* PROPS */, ["actions", "record"])
6561
6717
  ]))
6562
6718
  : vue.createCommentVNode("v-if", true)
6563
- ], 8 /* PROPS */, _hoisted_44))
6719
+ ], 10 /* CLASS, PROPS */, _hoisted_44))
6564
6720
  }), 128 /* KEYED_FRAGMENT */))
6565
6721
  ]))
6566
6722
  : (vue.openBlock(), vue.createElementBlock("div", _hoisted_62, [
@@ -8397,8 +8553,8 @@ const ShFrontend = {
8397
8553
  }
8398
8554
  //filter unwanted config items from options to be put in local storage
8399
8555
  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]);
8556
+ const allowKeys = ['enableTableCache'];
8557
+ Object.keys(options).map(key=> ((!['string','integer','number','boolean'].includes(typeof options[key]) && !allowKeys.includes(key)) || removeKeys.includes(key)) && delete options[key]);
8402
8558
 
8403
8559
  shStorage.setItem('ShConfig',options);
8404
8560
  }
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,52 @@ 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
+ let keyBase = props.endPoint || props.query || "default";
6079
+
6080
+ // Include date range in the key if active
6081
+ if (from.value || to.value || period.value) {
6082
+ keyBase += `_${from.value}_${to.value}_${period.value}`;
6083
+ }
6084
+
6085
+ const safeBase = keyBase.replace(/[^a-z0-9]/gi, "_").toLowerCase();
6086
+ return "sh_table_cache_" + safeBase;
6087
+ });
6088
+
5939
6089
  // Main loader
6090
+ // Main data fetcher. Handles background updates when cache is present
5940
6091
  const reloadData = (newPage, append) => {
5941
6092
  if (typeof newPage !== "undefined") page.value = newPage;
5942
6093
 
5943
- if (props.cacheKey && records.value !== null) {
6094
+ // If we have cached data and not searching, we don't show the initial loading spinner
6095
+ if (shouldCache.value && records.value && records.value.length > 0 && !filter_value.value) {
5944
6096
  loading.value = "done";
5945
6097
  } else if (!append) {
5946
6098
  loading.value = "loading";
6099
+ // Clear records when searching to ensure we show fresh results
6100
+ if (filter_value.value) {
6101
+ records.value = [];
6102
+ }
5947
6103
  }
5948
6104
 
5949
6105
  let data = {
@@ -5982,8 +6138,8 @@ const reloadData = (newPage, append) => {
5982
6138
  const response = req.data.data;
5983
6139
  emit("dataLoaded", response);
5984
6140
 
5985
- if (page.value < 2 && props.cacheKey) {
5986
- shStorage.setItem("sh_table_cache_" + props.cacheKey, response.data);
6141
+ if (page.value < 2 && shouldCache.value) {
6142
+ shIndexedDB.setItem(computedCacheKey.value, response.data);
5987
6143
  }
5988
6144
 
5989
6145
  pagination_data.value = {
@@ -6168,7 +6324,7 @@ return (_ctx, _cache) => {
6168
6324
  : createCommentVNode("v-if", true),
6169
6325
  (hasDefaultSlot.value)
6170
6326
  ? (openBlock(), createElementBlock(Fragment, { key: 2 }, [
6171
- (loading.value === 'loading')
6327
+ (loading.value === 'loading' && records.value.length === 0)
6172
6328
  ? (openBlock(), createElementBlock("div", _hoisted_9$2, [...(_cache[13] || (_cache[13] = [
6173
6329
  createElementVNode("div", {
6174
6330
  class: "spinner-border",
@@ -6177,12 +6333,12 @@ return (_ctx, _cache) => {
6177
6333
  createElementVNode("span", { class: "visually-hidden" }, "Loading...")
6178
6334
  ], -1 /* CACHED */)
6179
6335
  ]))]))
6180
- : (loading.value === 'error')
6336
+ : (loading.value === 'error' && records.value.length === 0)
6181
6337
  ? (openBlock(), createElementBlock("div", _hoisted_10$2, [
6182
6338
  createElementVNode("span", null, toDisplayString(loading_error.value), 1 /* TEXT */)
6183
6339
  ]))
6184
6340
  : createCommentVNode("v-if", true),
6185
- (loading.value === 'done')
6341
+ (loading.value === 'done' || records.value.length > 0)
6186
6342
  ? (openBlock(), createElementBlock(Fragment, { key: 2 }, [
6187
6343
  (records.value.length === 0)
6188
6344
  ? (openBlock(), createElementBlock(Fragment, { key: 0 }, [
@@ -6206,7 +6362,7 @@ return (_ctx, _cache) => {
6206
6362
  ], 64 /* STABLE_FRAGMENT */))
6207
6363
  : (hasRecordsSlot.value)
6208
6364
  ? (openBlock(), createElementBlock(Fragment, { key: 3 }, [
6209
- (loading.value === 'loading' && !__props.cacheKey)
6365
+ (loading.value === 'loading' && records.value.length === 0)
6210
6366
  ? (openBlock(), createElementBlock("div", _hoisted_12$1, [...(_cache[15] || (_cache[15] = [
6211
6367
  createElementVNode("div", {
6212
6368
  class: "spinner-border",
@@ -6215,12 +6371,12 @@ return (_ctx, _cache) => {
6215
6371
  createElementVNode("span", { class: "visually-hidden" }, "Loading...")
6216
6372
  ], -1 /* CACHED */)
6217
6373
  ]))]))
6218
- : (loading.value === 'error' && !__props.cacheKey)
6374
+ : (loading.value === 'error' && records.value.length === 0)
6219
6375
  ? (openBlock(), createElementBlock("div", _hoisted_13$1, [
6220
6376
  createElementVNode("span", null, toDisplayString(loading_error.value), 1 /* TEXT */)
6221
6377
  ]))
6222
6378
  : createCommentVNode("v-if", true),
6223
- (loading.value === 'done' || __props.cacheKey)
6379
+ (loading.value === 'done' || records.value.length > 0)
6224
6380
  ? (openBlock(), createElementBlock(Fragment, { key: 2 }, [
6225
6381
  (!records.value || records.value.length === 0)
6226
6382
  ? (openBlock(), createElementBlock(Fragment, { key: 0 }, [
@@ -6297,7 +6453,7 @@ return (_ctx, _cache) => {
6297
6453
  ])
6298
6454
  ]),
6299
6455
  createElementVNode("tbody", _hoisted_22, [
6300
- (loading.value === 'loading')
6456
+ (loading.value === 'loading' && records.value.length === 0)
6301
6457
  ? (openBlock(), createElementBlock("tr", _hoisted_23, [
6302
6458
  createElementVNode("td", {
6303
6459
  colspan:
@@ -6316,7 +6472,7 @@ return (_ctx, _cache) => {
6316
6472
  ], -1 /* CACHED */)
6317
6473
  ]))], 8 /* PROPS */, _hoisted_24)
6318
6474
  ]))
6319
- : (loading.value === 'error')
6475
+ : (loading.value === 'error' && records.value.length === 0)
6320
6476
  ? (openBlock(), createElementBlock("tr", _hoisted_25, [
6321
6477
  createElementVNode("td", {
6322
6478
  colspan:
@@ -6347,7 +6503,7 @@ return (_ctx, _cache) => {
6347
6503
  ? (openBlock(true), createElementBlock(Fragment, { key: 3 }, renderList(records.value, (record, index) => {
6348
6504
  return (openBlock(), createElementBlock("tr", {
6349
6505
  key: record.id,
6350
- class: normalizeClass(record.class),
6506
+ class: normalizeClass([record.class, props.rowLink ? 'cursor-pointer' : '']),
6351
6507
  onClick: $event => (rowSelected(record))
6352
6508
  }, [
6353
6509
  (activeMultiActions.value.length > 0)
@@ -6444,7 +6600,7 @@ return (_ctx, _cache) => {
6444
6600
  (openBlock(true), createElementBlock(Fragment, null, renderList(records.value, (record, index) => {
6445
6601
  return (openBlock(), createElementBlock("div", {
6446
6602
  key: record.id,
6447
- class: "single-mobile-req bg-light p-3",
6603
+ class: normalizeClass(["single-mobile-req bg-light p-3", props.rowLink ? 'cursor-pointer' : '']),
6448
6604
  onClick: $event => (rowSelected(record))
6449
6605
  }, [
6450
6606
  (activeMultiActions.value.length > 0)
@@ -6549,7 +6705,7 @@ return (_ctx, _cache) => {
6549
6705
  }, null, 8 /* PROPS */, ["actions", "record"])
6550
6706
  ]))
6551
6707
  : createCommentVNode("v-if", true)
6552
- ], 8 /* PROPS */, _hoisted_44))
6708
+ ], 10 /* CLASS, PROPS */, _hoisted_44))
6553
6709
  }), 128 /* KEYED_FRAGMENT */))
6554
6710
  ]))
6555
6711
  : (openBlock(), createElementBlock("div", _hoisted_62, [
@@ -8386,8 +8542,8 @@ const ShFrontend = {
8386
8542
  }
8387
8543
  //filter unwanted config items from options to be put in local storage
8388
8544
  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]);
8545
+ const allowKeys = ['enableTableCache'];
8546
+ Object.keys(options).map(key=> ((!['string','integer','number','boolean'].includes(typeof options[key]) && !allowKeys.includes(key)) || removeKeys.includes(key)) && delete options[key]);
8391
8547
 
8392
8548
  shStorage.setItem('ShConfig',options);
8393
8549
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iankibetsh/shframework",
3
- "version": "5.8.3",
3
+ "version": "5.8.5",
4
4
  "description": "Vue library for handling laravel backend",
5
5
  "repository": {
6
6
  "type": "git",