@wishbone-media/spark 0.18.0 → 0.19.0

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/formkit.theme.mjs CHANGED
@@ -344,7 +344,7 @@ const classes = {
344
344
  "flex": true,
345
345
  "items-center": true,
346
346
  "text-gray-700": true,
347
- "z-200": true,
347
+ "z-1000": true,
348
348
  "dark:text-gray-300": true,
349
349
  "data-[disabled]:cursor-not-allowed": true
350
350
  },
@@ -356,7 +356,7 @@ const classes = {
356
356
  "items-center": true,
357
357
  "text-gray-700": true,
358
358
  "hover:text-red-400": true,
359
- "z-200": true,
359
+ "z-1000": true,
360
360
  "dark:text-gray-300": true
361
361
  },
362
362
  "family:dropdown__controlLabel": {
@@ -1615,7 +1615,7 @@ const classes = {
1615
1615
  "mr-2.5": true,
1616
1616
  "text-gray-700": true,
1617
1617
  "hover:text-red-400": true,
1618
- "z-200": true,
1618
+ "z-1000": true,
1619
1619
  "dark:text-gray-300": true
1620
1620
  },
1621
1621
  "datepicker__clearIcon": {
@@ -1631,7 +1631,7 @@ const classes = {
1631
1631
  "rounded-md": true,
1632
1632
  "p-5": true,
1633
1633
  "bg-white": true,
1634
- "z-200": true,
1634
+ "z-1000": true,
1635
1635
  "dark:bg-gray-800": true,
1636
1636
  "[@media(max-width:431px)_and_(hover:none)]:group-[&:not([data-inline])]:!fixed": true,
1637
1637
  "[@media(max-width:431px)_and_(hover:none)]:group-[&:not([data-inline])]:top-auto": true,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wishbone-media/spark",
3
- "version": "0.18.0",
3
+ "version": "0.19.0",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.js",
@@ -1,3 +1,18 @@
1
+ .ht-theme-classic,
2
+ .ht-theme-classic-dark,
3
+ .ht-theme-classic-dark-auto {
4
+ --ht-font-size: 12px;
5
+ --ht-line-height: 16px;
6
+ --ht-wrapper-border-width: 1px;
7
+ --ht-wrapper-border-radius: 6px;
8
+ --ht-wrapper-border-color: var(--color-gray-300);
9
+ --ht-header-highlighted-background-color: transparent;
10
+ }
11
+
12
+ .spark-table .ht_master .wtBorder {
13
+ @apply !hidden;
14
+ }
15
+
1
16
  .spark-table-table {
2
17
  tbody {
3
18
  tr:nth-child(even) td {
@@ -30,17 +45,13 @@
30
45
  @apply cursor-pointer;
31
46
  }
32
47
 
33
- &.ht__highlight .relative {
34
- @apply bg-gray-200;
35
- }
36
-
37
48
  &.ht__active_highlight .relative {
38
49
  @apply bg-blue-400 text-black;
39
50
  }
40
51
 
41
52
  & > .relative {
42
53
  @apply text-left text-xs font-semibold bg-gray-50 text-gray-800 whitespace-normal
43
- tracking-wider focus:outline-hidden drop-shadow-sm filter flex items-center;
54
+ tracking-wider focus:outline-hidden filter flex items-center;
44
55
 
45
56
  @apply !px-5 !py-3;
46
57
 
@@ -73,7 +84,7 @@
73
84
  }
74
85
 
75
86
  .colHeader {
76
- @apply leading-5;
87
+ @apply leading-4 text-gray-800;
77
88
  }
78
89
  }
79
90
 
@@ -97,3 +108,17 @@
97
108
  .spark-table-head-sorting {
98
109
  @apply absolute right-5 w-5 h-5 border border-gray-200 rounded-md grid place-items-center;
99
110
  }
111
+
112
+ .spark-table-head-title-wrapper {
113
+ @apply h-full;
114
+ }
115
+
116
+ .spark-table-toolbar-header {
117
+ ::placeholder {
118
+ @apply text-gray-700;
119
+ }
120
+ }
121
+
122
+ .spark-table-toolbar-footer {
123
+ @apply min-h-15;
124
+ }
@@ -25,24 +25,24 @@
25
25
  class="flex px-[22px] py-[15px] cursor-pointer"
26
26
  @click="selectBrand(brand)"
27
27
  >
28
- <div class="gap-y-1 flex">
29
- <div class="flex items-center mr-4">
30
- <img :src="brand.logo" :alt="`${brand.name} logo`" class="h-8 w-auto" />
31
- </div>
32
- <div class="ml-auto flex flex-col">
33
- <div class="text-base text-gray-800 flex items-center">
34
- <div class="font-medium">{{ brand.name }}</div>
28
+ <div class="w-full gap-y-1 flex justify-between">
29
+ <div class="flex flex-col">
30
+ <div class="flex items-center">
31
+ <div class="font-medium text-base text-gray-900">{{ brand.name }}</div>
35
32
  <span
36
33
  v-if="brand.current"
37
- class="inline-flex items-center rounded-full bg-green-100 px-1.5 py-0.5 text-xs font-medium text-green-700 ml-1"
34
+ class="inline-flex items-center rounded-full bg-green-100 px-1.5 py-0.5 text-xs font-medium text-green-800 ml-1"
38
35
  >
39
36
  Current
40
37
  </span>
41
38
  </div>
42
- <div class="text-sm text-gray-500">
39
+ <div class="text-sm text-gray-500 font-normal">
43
40
  {{ brand.current ? 'Current Brand' : 'Change to' }}
44
41
  </div>
45
42
  </div>
43
+ <div class="flex items-center">
44
+ <img :src="brand.logo" :alt="`${brand.name} logo`" class="h-8 w-auto" />
45
+ </div>
46
46
  </div>
47
47
  </div>
48
48
  <div></div>
@@ -1,6 +1,6 @@
1
1
  <template>
2
2
  <TransitionRoot as="template" :show="sparkModalService.state.isVisible">
3
- <Dialog class="relative z-200" @close="sparkModalService.hide">
3
+ <Dialog class="relative z-1000" @close="sparkModalService.hide">
4
4
  <TransitionChild
5
5
  as="template"
6
6
  enter="ease-out duration-300"
@@ -2,7 +2,7 @@
2
2
  <TransitionRoot :show="overlayInstance.state.isVisible" as="template">
3
3
  <Dialog
4
4
  :initialFocus="panelRef"
5
- class="relative z-200"
5
+ class="relative z-1000"
6
6
  @close="handleClose"
7
7
  >
8
8
  <TransitionChild
@@ -1,7 +1,7 @@
1
1
  <template>
2
2
  <div class="spark-table">
3
3
  <!-- Header Toolbar: All plugins flow left to right -->
4
- <spark-table-toolbar v-if="sparkTable.computed.ready" position="header">
4
+ <spark-table-toolbar v-if="sparkTable.computed.ready && headerPlugins && headerPlugins.length" position="header">
5
5
  <component
6
6
  v-for="plugin in headerPlugins"
7
7
  :key="plugin.name"
@@ -12,6 +12,7 @@
12
12
  />
13
13
  <slot name="header" :spark-table="sparkTable" :loading="loading" :error="error"></slot>
14
14
  </spark-table-toolbar>
15
+ <div v-else class="pt-5"></div>
15
16
 
16
17
  <!-- Table Grid -->
17
18
  <HotTable theme-name="ht-theme-classic" ref="table" :settings="sparkTable.tableSettings" />
@@ -38,8 +39,8 @@
38
39
 
39
40
  <script>
40
41
  const defaultOptions = {
41
- perPages: [100, 200, 500],
42
- limit: 100,
42
+ perPages: [15, 30, 50, 100, 200, 500],
43
+ limit: 15,
43
44
  }
44
45
 
45
46
  const defaultParams = {
@@ -60,7 +61,7 @@ const defaultTableSettings = {
60
61
  }
61
62
  </script>
62
63
  <script setup>
63
- import { computed, onMounted, ref, reactive, inject } from 'vue'
64
+ import { computed, onMounted, onUnmounted, ref, reactive, inject, watch } from 'vue'
64
65
  import nprogress from 'nprogress'
65
66
  import { has, get, find } from 'lodash'
66
67
  import { HotTable } from '@handsontable/vue3'
@@ -78,6 +79,7 @@ import { watchDebounced } from '@vueuse/core'
78
79
  import { customiseHeader, syncSortClasses } from '@/utils/sparkTable/header.js'
79
80
  import { registerSparkRenderers } from '@/utils/sparkTable/renderers'
80
81
  import { updateRow } from '@/utils/sparkTable/update-row.js'
82
+ import { useSparkTableRouteSync } from '@/composables/useSparkTableRouteSync.js'
81
83
  import SparkTablePaginationDetails from './SparkTablePaginationDetails.vue'
82
84
  import SparkTablePaginationPaging from './SparkTablePaginationPaging.vue'
83
85
  import SparkTablePaginationPerPage from './SparkTablePaginationPerPage.vue'
@@ -141,6 +143,16 @@ const props = defineProps({
141
143
  type: Object,
142
144
  default: () => ({}),
143
145
  },
146
+
147
+ syncToRoute: {
148
+ type: [Boolean, String],
149
+ default: false,
150
+ },
151
+
152
+ persistToStorage: {
153
+ type: Boolean,
154
+ default: false,
155
+ },
144
156
  })
145
157
 
146
158
  registerAllCellTypes()
@@ -155,6 +167,7 @@ const emit = defineEmits([
155
167
  'viewRow',
156
168
  'editRow',
157
169
  'deleteRow',
170
+ 'downloadRow',
158
171
  'ready',
159
172
  'loading',
160
173
  'load',
@@ -168,6 +181,7 @@ const axios = inject('axios')
168
181
  const table = ref(null)
169
182
  const loading = ref(false)
170
183
  const error = ref(null)
184
+ let isUnmounted = false
171
185
 
172
186
  // Plugin component mapping by type
173
187
  const pluginComponentsByType = {
@@ -178,16 +192,9 @@ const pluginComponentsByType = {
178
192
  reset: SparkTableReset,
179
193
  }
180
194
 
181
- // Header plugins: all enabled plugins flow left to right
182
- const headerPlugins = computed(() => {
183
- return Object.entries(props.plugins)
184
- .filter(([_, config]) => config && config.enabled)
185
- .map(([name, config]) => ({ name, config }))
186
- })
187
-
188
195
  const sparkTable = reactive({
189
196
  hotInstance: null,
190
- url: props.url,
197
+ url: computed(() => props.url),
191
198
  plugins: props.plugins,
192
199
  response: {},
193
200
  params: {
@@ -242,6 +249,9 @@ const sparkTable = reactive({
242
249
  return
243
250
  }
244
251
 
252
+ // Check if component was unmounted during async operation
253
+ if (isUnmounted) return
254
+
245
255
  sparkTable.hotInstance.updateData(sparkTable.response.data)
246
256
 
247
257
  if (sparkTable.options.callback && typeof sparkTable.options.callback === 'function') {
@@ -249,8 +259,15 @@ const sparkTable = reactive({
249
259
  }
250
260
 
251
261
  // @see: https://forum.handsontable.com/t/table-not-maintaning-width/3116/11
262
+ // Selectively recalculate column widths - skip columns with explicit width
252
263
  const autoColumnSize = sparkTable.hotInstance.getPlugin('autoColumnSize')
253
- autoColumnSize.recalculateAllColumnsWidth()
264
+ const columns = get(props.settings, 'columns', [])
265
+
266
+ columns.forEach((column, index) => {
267
+ if (!column.width) {
268
+ autoColumnSize.calculateColumnsWidth(index, index, true)
269
+ }
270
+ })
254
271
 
255
272
  emit('load', {
256
273
  data: sparkTable.response.data,
@@ -325,11 +342,112 @@ const sparkTable = reactive({
325
342
  }),
326
343
  afterChange: (changes, source) => updateRow(changes, source, sparkTable),
327
344
  afterRender: () => syncSortClasses(sparkTable),
345
+ /**
346
+ * Prevent columns with explicit width from being stretched
347
+ * This hook fires BEFORE stretchH is applied, allowing us to cap specific columns
348
+ * while letting others stretch normally
349
+ */
350
+ beforeStretchingColumnWidth: (stretchedWidth, column) => {
351
+ const columns = get(props.settings, 'columns', [])
352
+ const columnSettings = columns[column]
353
+ if (columnSettings && columnSettings.width !== undefined) {
354
+ return columnSettings.width
355
+ }
356
+ return stretchedWidth
357
+ },
328
358
  },
329
359
  ...props.settings,
330
360
  })),
331
361
  })
332
362
 
363
+ /**
364
+ * Get the param key for a plugin based on its type and configuration
365
+ */
366
+ const getPluginParamKey = (pluginConfig) => {
367
+ if (!pluginConfig) return null
368
+
369
+ switch (pluginConfig.type) {
370
+ case 'search':
371
+ return pluginConfig.param || 'search'
372
+ case 'filterSelect':
373
+ case 'filterButtons':
374
+ case 'datePicker':
375
+ return pluginConfig.param || `filter[${pluginConfig.key}]`
376
+ default:
377
+ return null
378
+ }
379
+ }
380
+
381
+ /**
382
+ * Check if a plugin is currently enabled
383
+ */
384
+ const isPluginEnabled = (pluginConfig) => {
385
+ if (!pluginConfig) return false
386
+ return typeof pluginConfig.enabled === 'function'
387
+ ? pluginConfig.enabled(sparkTable.params)
388
+ : pluginConfig.enabled
389
+ }
390
+
391
+ // Header plugins: all enabled plugins flow left to right
392
+ // `enabled` can be a boolean or a function that receives current params
393
+ const headerPlugins = computed(() => {
394
+ return Object.entries(props.plugins)
395
+ .filter(([_, config]) => isPluginEnabled(config))
396
+ .map(([name, config]) => ({ name, config }))
397
+ })
398
+
399
+ // Get all plugin param keys (regardless of enabled state)
400
+ const getAllPluginParamKeys = () => {
401
+ const keys = []
402
+ Object.values(props.plugins).forEach((config) => {
403
+ const paramKey = getPluginParamKey(config)
404
+ if (paramKey) keys.push(paramKey)
405
+ })
406
+ return keys
407
+ }
408
+
409
+ // Watch sparkTable.params for changes and clear params of disabled plugins
410
+ // Uses flush: 'sync' to run synchronously before other watchers (like the debounced loadTable)
411
+ watch(
412
+ () => ({ ...sparkTable.params }),
413
+ () => {
414
+ // Check each plugin - if it's disabled but has a param value, remove it
415
+ const paramsToRemove = []
416
+
417
+ Object.values(props.plugins).forEach((config) => {
418
+ const paramKey = getPluginParamKey(config)
419
+ if (!paramKey) return
420
+
421
+ const isEnabled = isPluginEnabled(config)
422
+ const hasValue = sparkTable.params[paramKey] !== undefined
423
+
424
+ if (!isEnabled && hasValue) {
425
+ paramsToRemove.push(paramKey)
426
+ }
427
+ })
428
+
429
+ // Clear disabled plugin params synchronously before loadTable runs
430
+ if (paramsToRemove.length > 0) {
431
+ paramsToRemove.forEach((paramKey) => {
432
+ delete sparkTable.params[paramKey]
433
+ })
434
+ }
435
+ },
436
+ { deep: true, flush: 'sync' }
437
+ )
438
+
439
+ // Setup route sync and/or storage persistence if enabled
440
+ if (props.syncToRoute || props.persistToStorage) {
441
+ // If syncToRoute is a string, use it as namespace (for multi-table pages)
442
+ // If syncToRoute is true (boolean), use null for flat URLs
443
+ const hasExplicitNamespace = typeof props.syncToRoute === 'string'
444
+ useSparkTableRouteSync(sparkTable, {
445
+ namespace: hasExplicitNamespace ? props.syncToRoute : null,
446
+ syncToRoute: !!props.syncToRoute,
447
+ persistToStorage: props.persistToStorage,
448
+ })
449
+ }
450
+
333
451
  watchDebounced(
334
452
  () => props.params,
335
453
  async () => {
@@ -346,11 +464,25 @@ watchDebounced(
346
464
  { debounce: 50, maxWait: 1000 },
347
465
  )
348
466
 
467
+ watch(
468
+ () => props.url,
469
+ async (newUrl, oldUrl) => {
470
+ if (newUrl !== oldUrl) {
471
+ sparkTable.params.page = 1
472
+ await sparkTable.methods.loadTable()
473
+ }
474
+ }
475
+ )
476
+
349
477
  onMounted(async () => {
350
478
  await sparkTable.methods.loadTable()
351
479
  emit('ready')
352
480
  })
353
481
 
482
+ onUnmounted(() => {
483
+ isUnmounted = true
484
+ })
485
+
354
486
  registerSparkRenderers(sparkTable)
355
487
 
356
488
  defineExpose({
@@ -1,6 +1,6 @@
1
1
  <template>
2
2
  <div>
3
- <div class="flex items-center gap-4 px-4 py-3">
3
+ <div class="flex items-center gap-4 py-3">
4
4
  <div class="shrink-0">
5
5
  <div class="text-sm text-gray-700">
6
6
  Showing
@@ -20,7 +20,7 @@ const slots = useSlots()
20
20
  const hasContent = computed(() => !!slots.default)
21
21
 
22
22
  const toolbarClass = computed(() => {
23
- const baseClasses = 'spark-table-toolbar flex flex-wrap items-center gap-x-5 px-5 w-full'
23
+ const baseClasses = 'spark-table-toolbar flex flex-wrap items-center gap-x-5 w-full'
24
24
 
25
25
  // Footer has pagination details on left, controls on right
26
26
  if (props.position === 'footer') {
@@ -105,11 +105,17 @@ watch(selectedValue, (newValue) => {
105
105
  }
106
106
  })
107
107
 
108
- // Watch for external param changes
108
+ // Watch for external param changes (e.g., from route sync, reset button)
109
109
  watch(
110
110
  () => props.sparkTable.params[paramKey],
111
111
  (newValue) => {
112
- if (!newValue && selectedValue.value) {
112
+ if (newValue !== undefined && newValue !== null && newValue !== '') {
113
+ // Param was set externally (e.g., restored from URL)
114
+ if (selectedValue.value !== newValue) {
115
+ selectedValue.value = newValue
116
+ }
117
+ } else if (selectedValue.value) {
118
+ // Param was cleared externally
113
119
  selectedValue.value = ''
114
120
  }
115
121
  },
@@ -77,42 +77,71 @@ const handleReset = () => {
77
77
  // Get all plugin params from the plugins config
78
78
  const plugins = props.sparkTable.plugins || {}
79
79
 
80
- // Collect all params to clear
80
+ // Collect params to clear and params to reset to initialValue
81
81
  const paramsToClear = []
82
+ const paramsToReset = {}
82
83
 
83
- Object.entries(plugins).forEach(([name, pluginConfig]) => {
84
- if (!pluginConfig || !pluginConfig.enabled || pluginConfig.type === 'reset') {
84
+ Object.entries(plugins).forEach(([_, pluginConfig]) => {
85
+ if (!pluginConfig || pluginConfig.type === 'reset') {
86
+ return
87
+ }
88
+
89
+ // Check if plugin is enabled (can be boolean or function)
90
+ const isEnabled = typeof pluginConfig.enabled === 'function'
91
+ ? pluginConfig.enabled(props.sparkTable.params)
92
+ : pluginConfig.enabled
93
+
94
+ if (!isEnabled) {
85
95
  return
86
96
  }
87
97
 
88
98
  // Get the param key based on plugin type
99
+ let param = null
89
100
  if (pluginConfig.type === 'search') {
90
- if (pluginConfig.param) {
91
- paramsToClear.push(pluginConfig.param)
92
- }
101
+ param = pluginConfig.param || 'search'
93
102
  } else if (pluginConfig.type === 'filterSelect' || pluginConfig.type === 'filterButtons') {
94
- const param = pluginConfig.param || `filter[${pluginConfig.key}]`
95
- paramsToClear.push(param)
103
+ param = pluginConfig.param || `filter[${pluginConfig.key}]`
96
104
  } else if (pluginConfig.type === 'datePicker') {
97
- const param = pluginConfig.param || `filter[${pluginConfig.key}]`
105
+ param = pluginConfig.param || `filter[${pluginConfig.key}]`
106
+ }
107
+
108
+ if (!param) return
109
+
110
+ // Check if this plugin has an initialValue
111
+ if (pluginConfig.initialValue !== undefined && pluginConfig.initialValue !== null) {
112
+ // Reset to initialValue
113
+ paramsToReset[param] = pluginConfig.initialValue
114
+ } else {
115
+ // Clear the param
98
116
  paramsToClear.push(param)
99
117
  }
100
118
  })
101
119
 
102
- // Check if any params actually exist and need to be cleared
120
+ // Check if any params actually need to be changed
103
121
  const paramsToActuallyClear = paramsToClear.filter(param =>
104
122
  props.sparkTable.params[param] !== undefined &&
105
123
  props.sparkTable.params[param] !== null &&
106
124
  props.sparkTable.params[param] !== ''
107
125
  )
108
126
 
109
- // If nothing to clear, don't do anything
110
- if (paramsToActuallyClear.length === 0) {
127
+ const paramsToActuallyReset = Object.entries(paramsToReset).filter(([param, value]) =>
128
+ props.sparkTable.params[param] !== value
129
+ )
130
+
131
+ // If nothing to change, don't do anything
132
+ if (paramsToActuallyClear.length === 0 && paramsToActuallyReset.length === 0) {
111
133
  return
112
134
  }
113
135
 
114
- // Use the clearParams helper method to clear all params at once
115
- props.sparkTable.methods.clearParams(paramsToActuallyClear)
136
+ // Build the new params object
137
+ // First, clear all the params that should be cleared
138
+ paramsToActuallyClear.forEach(param => {
139
+ delete props.sparkTable.params[param]
140
+ })
141
+
142
+ // Then apply the reset values and go to page 1
143
+ const resetParams = Object.fromEntries(paramsToActuallyReset)
144
+ props.sparkTable.methods.applyParams({ ...resetParams, page: 1 })
116
145
  }
117
146
  </script>
118
147
 
@@ -4,9 +4,10 @@
4
4
  v-model="searchValue"
5
5
  type="text"
6
6
  :placeholder="placeholder"
7
+ suffixIcon="search"
7
8
  outer-class="!mb-0"
8
9
  wrapper-class="!mb-0"
9
- input-class="!w-64 !pr-8"
10
+ input-class="!w-44 !pr-8"
10
11
  v-bind="props.config.formkitProps || {}"
11
12
  />
12
13
  </div>
@@ -25,7 +26,7 @@
25
26
  * enabled: true,
26
27
  * placeholder: 'Search brands...',
27
28
  * param: 'search',
28
- * debounce: 300,
29
+ * debounce: 400,
29
30
  * position: 'header-left',
30
31
  * },
31
32
  * }
@@ -54,7 +55,7 @@ const props = defineProps({
54
55
  default: () => ({
55
56
  placeholder: 'Search...',
56
57
  param: 'search',
57
- debounce: 300,
58
+ debounce: 400,
58
59
  }),
59
60
  },
60
61
  })
@@ -97,11 +98,15 @@ watch(searchValue, (newValue) => {
97
98
 
98
99
  const placeholder = props.config.placeholder || 'Search...'
99
100
 
100
- // Watch for external param changes
101
+ // Watch for external param changes (e.g., from route sync, reset button)
101
102
  watch(
102
103
  () => props.sparkTable.params[paramKey],
103
104
  (newValue) => {
104
- if (!newValue && searchValue.value) {
105
+ if (newValue && newValue !== searchValue.value) {
106
+ // Param was set externally (e.g., restored from URL)
107
+ searchValue.value = newValue
108
+ } else if (!newValue && searchValue.value) {
109
+ // Param was cleared externally
105
110
  searchValue.value = ''
106
111
  }
107
112
  },
@@ -1,3 +1,4 @@
1
1
  export { sparkModalService } from './sparkModalService.js'
2
2
  export { sparkOverlayService } from './sparkOverlayService.js'
3
- export { useSparkOverlay } from './useSparkOverlay.js'
3
+ export { useSparkOverlay } from './useSparkOverlay.js'
4
+ export { useSparkTableRouteSync } from './useSparkTableRouteSync.js'