picocrank 1.33.0 → 1.34.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/docs/filter-query-v1.md +5 -1
- package/package.json +1 -1
- package/router.js +33 -0
- package/styles/picocrank-extensions.css +23 -2
- package/vue/components/Accordion.spec.js +48 -0
- package/vue/components/Accordion.vue +46 -0
- package/vue/components/AccordionItem.vue +103 -0
- package/vue/components/FieldValue.spec.js +88 -0
- package/vue/components/FieldValue.vue +222 -0
- package/vue/components/FormField.spec.js +1 -0
- package/vue/components/FormField.vue +38 -10
- package/vue/components/Header.vue +24 -2
- package/vue/components/NotificationBlock.vue +25 -0
- package/vue/components/ReorderList.spec.js +318 -0
- package/vue/components/ReorderList.vue +614 -0
- package/vue/components/Table.spec.js +27 -0
- package/vue/components/Table.vue +458 -112
- package/vue/components/TableColumnFilterPopover.vue +63 -19
- package/vue/components/TableColumnOptionsPopover.vue +248 -273
- package/vue/components/TopBar.vue +6 -2
- package/vue/components/accordionContext.js +1 -0
- package/vue/composables/tableColumnPresets.js +33 -0
- package/vue/composables/tableGroupBy.js +173 -0
- package/vue/composables/tableGroupBy.spec.js +69 -0
- package/vue/composables/tableViewSettings.js +34 -0
- package/vue/composables/tableViewSettings.spec.js +21 -0
- package/vue/composables/useHeaderTopBarFit.js +91 -0
- package/vue/composables/useResponsiveNav.js +5 -3
- package/vue/data/navSectionHubs.js +16 -1
- package/vue/examples/AccordionExample.vue +33 -0
- package/vue/examples/FieldValueExample.vue +143 -0
- package/vue/examples/FormExample.vue +11 -6
- package/vue/examples/ReorderListExample.vue +236 -0
- package/vue/examples/TableExample.vue +5 -1
- package/vue/examples/TableRemoteExample.vue +28 -4
package/vue/components/Table.vue
CHANGED
|
@@ -41,11 +41,38 @@
|
|
|
41
41
|
|
|
42
42
|
<span v-if="filterable && isHeaderFiltered(header)" class="filter-indicator" title="Filtered" aria-hidden="true">⧩</span>
|
|
43
43
|
|
|
44
|
+
<span
|
|
45
|
+
v-if="isHeaderGrouped(header)"
|
|
46
|
+
class="group-indicator"
|
|
47
|
+
title="Grouped"
|
|
48
|
+
aria-hidden="true"
|
|
49
|
+
>
|
|
50
|
+
<HugeiconsIcon
|
|
51
|
+
:icon="Group01Icon"
|
|
52
|
+
width="0.95em"
|
|
53
|
+
height="0.95em"
|
|
54
|
+
:strokeWidth="2"
|
|
55
|
+
aria-hidden="true"
|
|
56
|
+
/>
|
|
57
|
+
</span>
|
|
58
|
+
|
|
44
59
|
<span v-if="header.sortable" class="sort-indicator">
|
|
45
|
-
<
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
60
|
+
<HugeiconsIcon
|
|
61
|
+
v-if="sortBy === header.key && sortDir === 'asc'"
|
|
62
|
+
:icon="SortByUp02Icon"
|
|
63
|
+
width="0.95em"
|
|
64
|
+
height="0.95em"
|
|
65
|
+
:strokeWidth="2"
|
|
66
|
+
aria-hidden="true"
|
|
67
|
+
/>
|
|
68
|
+
<HugeiconsIcon
|
|
69
|
+
v-else-if="sortBy === header.key && sortDir === 'desc'"
|
|
70
|
+
:icon="SortByDown02Icon"
|
|
71
|
+
width="0.95em"
|
|
72
|
+
height="0.95em"
|
|
73
|
+
:strokeWidth="2"
|
|
74
|
+
aria-hidden="true"
|
|
75
|
+
/>
|
|
49
76
|
</span>
|
|
50
77
|
</th>
|
|
51
78
|
<th
|
|
@@ -74,77 +101,112 @@
|
|
|
74
101
|
</tr>
|
|
75
102
|
</thead>
|
|
76
103
|
<tbody v-if="hasRows">
|
|
77
|
-
<
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
{
|
|
82
|
-
'row-clickable': rowClickable,
|
|
83
|
-
'row-selected': selectable && isRowSelected(row, rowIndex),
|
|
84
|
-
'row-context-menu-active': isRowContextMenuTarget(row, rowIndex),
|
|
85
|
-
},
|
|
86
|
-
resolveRowClass(row, rowIndex),
|
|
87
|
-
]"
|
|
88
|
-
:style="resolveRowStyle(row, rowIndex)"
|
|
89
|
-
@click="onRowClick(row, rowIndex, $event)"
|
|
90
|
-
@contextmenu="onRowContextMenu(row, rowIndex, $event)"
|
|
91
|
-
>
|
|
92
|
-
<td
|
|
93
|
-
v-if="selectable"
|
|
94
|
-
class="table-select-col"
|
|
95
|
-
data-row-click-ignore
|
|
96
|
-
@click.stop
|
|
104
|
+
<template v-for="(item, itemIndex) in displayBodyItems" :key="bodyItemKey(item, itemIndex)">
|
|
105
|
+
<tr
|
|
106
|
+
v-if="item.kind === 'group'"
|
|
107
|
+
class="table-group-row"
|
|
108
|
+
:class="{ 'table-group-row-collapsed': item.collapsed }"
|
|
97
109
|
>
|
|
98
|
-
<
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
110
|
+
<td
|
|
111
|
+
v-if="selectable"
|
|
112
|
+
class="table-select-col table-group-cell"
|
|
113
|
+
data-row-click-ignore
|
|
114
|
+
/>
|
|
115
|
+
<td
|
|
116
|
+
:colspan="bodyGroupColspan"
|
|
117
|
+
class="table-group-cell"
|
|
118
|
+
>
|
|
119
|
+
<button
|
|
120
|
+
type="button"
|
|
121
|
+
class="table-group-toggle neutral"
|
|
122
|
+
:aria-expanded="item.collapsed ? 'false' : 'true'"
|
|
123
|
+
:aria-label="item.collapsed ? 'Expand group' : 'Collapse group'"
|
|
124
|
+
data-row-click-ignore
|
|
125
|
+
@click.stop="toggleGroupCollapsed(item.groupId)"
|
|
105
126
|
>
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
127
|
+
<HugeiconsIcon
|
|
128
|
+
class="table-group-chevron"
|
|
129
|
+
:icon="ArrowDown01Icon"
|
|
130
|
+
width="1em"
|
|
131
|
+
height="1em"
|
|
132
|
+
aria-hidden="true"
|
|
133
|
+
/>
|
|
134
|
+
</button>
|
|
135
|
+
<span class="table-group-label">{{ formatGroupValue(item.value) }}</span>
|
|
136
|
+
<span class="table-group-count subtle">({{ item.count }})</span>
|
|
137
|
+
</td>
|
|
138
|
+
</tr>
|
|
139
|
+
<tr
|
|
140
|
+
v-else
|
|
114
141
|
:class="[
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
142
|
+
{
|
|
143
|
+
'row-clickable': rowClickable,
|
|
144
|
+
'row-selected': selectable && isRowSelected(item.row, item.displayIndex),
|
|
145
|
+
'row-context-menu-active': isRowContextMenuTarget(item.row, item.displayIndex),
|
|
146
|
+
},
|
|
147
|
+
resolveRowClass(item.row, item.displayIndex),
|
|
120
148
|
]"
|
|
121
|
-
:style="
|
|
122
|
-
|
|
123
|
-
@
|
|
149
|
+
:style="resolveRowStyle(item.row, item.displayIndex)"
|
|
150
|
+
@click="onRowClick(item.row, item.displayIndex, $event)"
|
|
151
|
+
@contextmenu="onRowContextMenu(item.row, item.displayIndex, $event)"
|
|
124
152
|
>
|
|
125
|
-
<
|
|
126
|
-
v-if="
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
153
|
+
<td
|
|
154
|
+
v-if="selectable"
|
|
155
|
+
class="table-select-col"
|
|
156
|
+
data-row-click-ignore
|
|
157
|
+
@click.stop
|
|
158
|
+
>
|
|
159
|
+
<label class="table-select-label">
|
|
160
|
+
<input
|
|
161
|
+
type="checkbox"
|
|
162
|
+
:checked="isRowSelected(item.row, item.displayIndex)"
|
|
163
|
+
:aria-label="`Select row ${resolveRowKey(item.row, item.displayIndex)}`"
|
|
164
|
+
@click.stop="rememberSelectionCheckboxClick(item.row, item.displayIndex, $event)"
|
|
165
|
+
@change="onSelectionCheckboxChange(item.row, item.displayIndex)"
|
|
166
|
+
>
|
|
167
|
+
</label>
|
|
168
|
+
</td>
|
|
169
|
+
<td
|
|
170
|
+
v-for="(header, cellIndex) in visibleHeaders"
|
|
171
|
+
:key="header.key || cellIndex"
|
|
172
|
+
:ref="(element) => registerCellElement(element, item.row, header, item.displayIndex)"
|
|
173
|
+
v-bind="resolveCellAttrs(item.row, header, item.displayIndex)"
|
|
174
|
+
:colspan="bodyColumnColspan(cellIndex) || undefined"
|
|
175
|
+
:class="[
|
|
176
|
+
cellClasses(header),
|
|
177
|
+
header.class,
|
|
178
|
+
resolveCellClass(item.row, header, item.displayIndex),
|
|
179
|
+
{ 'table-active-cell': isActiveCell(item.row, header, item.displayIndex) },
|
|
180
|
+
isLastColumnWithOptions(cellIndex) ? 'table-last-column-with-options' : null,
|
|
181
|
+
]"
|
|
182
|
+
:style="resolveCellStyle(item.row, header, item.displayIndex)"
|
|
183
|
+
:tabindex="isActiveCell(item.row, header, item.displayIndex) ? 0 : -1"
|
|
184
|
+
@click="onCellClick(item.row, header, item.displayIndex, $event)"
|
|
185
|
+
>
|
|
186
|
+
<slot
|
|
187
|
+
v-if="slots[`cell-${header.key}`]"
|
|
188
|
+
:name="`cell-${header.key}`"
|
|
189
|
+
:row="item.row"
|
|
190
|
+
:value="item.row[header.key]"
|
|
191
|
+
:header="header"
|
|
192
|
+
:row-index="item.displayIndex"
|
|
193
|
+
:source-index="sourceRowIndex(item.row, item.displayIndex)"
|
|
194
|
+
/>
|
|
195
|
+
<slot
|
|
196
|
+
v-else-if="slots.cell"
|
|
197
|
+
name="cell"
|
|
198
|
+
:row="item.row"
|
|
199
|
+
:value="item.row[header.key]"
|
|
200
|
+
:header="header"
|
|
201
|
+
:row-index="item.displayIndex"
|
|
202
|
+
:source-index="sourceRowIndex(item.row, item.displayIndex)"
|
|
203
|
+
/>
|
|
204
|
+
<span v-else>
|
|
205
|
+
{{ item.row[header.key] }}
|
|
206
|
+
</span>
|
|
207
|
+
</td>
|
|
208
|
+
</tr>
|
|
209
|
+
</template>
|
|
148
210
|
</tbody>
|
|
149
211
|
</table>
|
|
150
212
|
|
|
@@ -174,10 +236,13 @@
|
|
|
174
236
|
:rows="props.data"
|
|
175
237
|
:select-options="activeSelectOptions"
|
|
176
238
|
:can-hide="canHideActiveFilterColumn"
|
|
239
|
+
:can-group-by="canGroupByActiveFilterColumn"
|
|
240
|
+
:is-grouped-column="isActiveFilterHeaderGrouped"
|
|
177
241
|
@apply="onFilterApply"
|
|
178
242
|
@clear="onFilterClear"
|
|
179
243
|
@cancel="onFilterCancel"
|
|
180
244
|
@hide="onFilterHideColumn"
|
|
245
|
+
@group-by="onFilterGroupBy"
|
|
181
246
|
/>
|
|
182
247
|
|
|
183
248
|
<TableColumnOptionsPopover
|
|
@@ -195,10 +260,13 @@
|
|
|
195
260
|
:presets="savedPresets"
|
|
196
261
|
:default-preset-id="defaultPresetId"
|
|
197
262
|
:save-layout-preset="onSavePreset"
|
|
263
|
+
:overwrite-layout-preset="onOverwritePreset"
|
|
198
264
|
:load-layout-preset="onLoadPreset"
|
|
199
265
|
:delete-layout-preset="onDeletePreset"
|
|
200
266
|
:set-default-layout-preset="onSetDefaultPreset"
|
|
201
267
|
:load-developer-defaults="loadDeveloperDefaults"
|
|
268
|
+
:group-by="activeGroupBy"
|
|
269
|
+
:groupable="props.groupable"
|
|
202
270
|
:anchor-el="columnOptionsButtonRef"
|
|
203
271
|
@apply="onColumnOptionsApply"
|
|
204
272
|
@cancel="onColumnOptionsCancel"
|
|
@@ -217,7 +285,13 @@
|
|
|
217
285
|
<script setup>
|
|
218
286
|
import { ref, computed, watch, nextTick, onMounted, onBeforeUnmount, useSlots, useAttrs } from 'vue'
|
|
219
287
|
import { HugeiconsIcon } from '@hugeicons/vue'
|
|
220
|
-
import {
|
|
288
|
+
import {
|
|
289
|
+
ArrowDown01Icon,
|
|
290
|
+
Group01Icon,
|
|
291
|
+
LayoutGridIcon,
|
|
292
|
+
SortByDown02Icon,
|
|
293
|
+
SortByUp02Icon,
|
|
294
|
+
} from '@hugeicons/core-free-icons'
|
|
221
295
|
import Pagination from './Pagination.vue'
|
|
222
296
|
import TableColumnFilterPopover from './TableColumnFilterPopover.vue'
|
|
223
297
|
import TableColumnOptionsPopover from './TableColumnOptionsPopover.vue'
|
|
@@ -242,15 +316,26 @@ import {
|
|
|
242
316
|
getPresetById,
|
|
243
317
|
listPresets,
|
|
244
318
|
savePreset,
|
|
319
|
+
overwritePreset,
|
|
245
320
|
deletePreset,
|
|
246
321
|
setDefaultPreset,
|
|
247
322
|
sanitizePresetState,
|
|
248
323
|
layoutStatesEqual,
|
|
249
324
|
} from '../composables/tableColumnPresets.js'
|
|
325
|
+
import {
|
|
326
|
+
getViewSettingsHandlers,
|
|
327
|
+
isViewSettingsControlEnabled,
|
|
328
|
+
VIEW_CONTROL_CALLBACK,
|
|
329
|
+
} from '../composables/tableViewSettings.js'
|
|
250
330
|
import {
|
|
251
331
|
defaultColumnPriorityForIndex,
|
|
252
332
|
isValidColPriority,
|
|
253
333
|
} from '../composables/tableColumnPriorities.js'
|
|
334
|
+
import {
|
|
335
|
+
buildDisplayBodyItems,
|
|
336
|
+
formatGroupValue,
|
|
337
|
+
isHeaderGroupable,
|
|
338
|
+
} from '../composables/tableGroupBy.js'
|
|
254
339
|
|
|
255
340
|
const DEFAULT_PAGE_SIZE = 10
|
|
256
341
|
|
|
@@ -343,6 +428,21 @@ const props = defineProps({
|
|
|
343
428
|
type: String,
|
|
344
429
|
default: '',
|
|
345
430
|
},
|
|
431
|
+
/**
|
|
432
|
+
* How saved table views (column order, visibility, filters in presets, etc.) are stored.
|
|
433
|
+
* - `localStorageControlled`: requires `tableId`; uses browser localStorage (default).
|
|
434
|
+
* - `callbackControlled`: requires `viewSettings` callbacks (server or app-owned persistence).
|
|
435
|
+
*/
|
|
436
|
+
viewControlMode: {
|
|
437
|
+
type: String,
|
|
438
|
+
default: 'localStorageControlled',
|
|
439
|
+
validator: (value) => ['localStorageControlled', 'callbackControlled'].includes(value),
|
|
440
|
+
},
|
|
441
|
+
/** Callback handlers when `viewControlMode` is `callbackControlled`. */
|
|
442
|
+
viewSettings: {
|
|
443
|
+
type: Object,
|
|
444
|
+
default: null,
|
|
445
|
+
},
|
|
346
446
|
loadSavedLayout: {
|
|
347
447
|
type: Boolean,
|
|
348
448
|
default: true,
|
|
@@ -355,6 +455,7 @@ const props = defineProps({
|
|
|
355
455
|
type: [String, Function],
|
|
356
456
|
default: 'id',
|
|
357
457
|
},
|
|
458
|
+
/** @deprecated Use `viewSettings` with `viewControlMode="callbackControlled"`. */
|
|
358
459
|
layoutPresets: {
|
|
359
460
|
type: Object,
|
|
360
461
|
default: null,
|
|
@@ -406,6 +507,14 @@ const props = defineProps({
|
|
|
406
507
|
type: [Array, Function],
|
|
407
508
|
default: null,
|
|
408
509
|
},
|
|
510
|
+
groupable: {
|
|
511
|
+
type: Boolean,
|
|
512
|
+
default: true,
|
|
513
|
+
},
|
|
514
|
+
groupBy: {
|
|
515
|
+
type: String,
|
|
516
|
+
default: undefined,
|
|
517
|
+
},
|
|
409
518
|
})
|
|
410
519
|
|
|
411
520
|
const emit = defineEmits([
|
|
@@ -432,6 +541,8 @@ const emit = defineEmits([
|
|
|
432
541
|
'column-priorities-change',
|
|
433
542
|
'update:layoutLabel',
|
|
434
543
|
'layout-label-change',
|
|
544
|
+
'update:groupBy',
|
|
545
|
+
'group-by-change',
|
|
435
546
|
])
|
|
436
547
|
|
|
437
548
|
const attrs = useAttrs()
|
|
@@ -561,15 +672,24 @@ const internalColumnPriorities = ref({})
|
|
|
561
672
|
const savedPresets = ref([])
|
|
562
673
|
const defaultPresetId = ref(null)
|
|
563
674
|
const activeLayoutPresetId = ref(null)
|
|
675
|
+
const internalGroupBy = ref(null)
|
|
676
|
+
const collapsedGroupIds = ref([])
|
|
564
677
|
|
|
565
678
|
const isRemote = computed(() => typeof props.fetchRows === 'function')
|
|
566
679
|
|
|
567
|
-
const
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
680
|
+
const viewSettingsHandlers = computed(() =>
|
|
681
|
+
getViewSettingsHandlers(props.viewSettings, props.layoutPresets))
|
|
682
|
+
|
|
683
|
+
const isCallbackViewControl = computed(
|
|
684
|
+
() => props.viewControlMode === VIEW_CONTROL_CALLBACK,
|
|
685
|
+
)
|
|
686
|
+
|
|
687
|
+
const layoutPresetsAvailable = computed(() =>
|
|
688
|
+
isViewSettingsControlEnabled(
|
|
689
|
+
props.viewControlMode,
|
|
690
|
+
props.tableId,
|
|
691
|
+
viewSettingsHandlers.value,
|
|
692
|
+
))
|
|
573
693
|
|
|
574
694
|
const activeFilters = computed(() => {
|
|
575
695
|
if (!props.filterable) {
|
|
@@ -629,6 +749,53 @@ function bodyColumnColspan(index) {
|
|
|
629
749
|
return isLastColumnWithOptions(index) ? 2 : null
|
|
630
750
|
}
|
|
631
751
|
|
|
752
|
+
const bodyGroupColspan = computed(() => {
|
|
753
|
+
let colspan = visibleHeaders.value.length
|
|
754
|
+
if (showColumnOptionsButton.value) {
|
|
755
|
+
colspan += 1
|
|
756
|
+
}
|
|
757
|
+
return colspan
|
|
758
|
+
})
|
|
759
|
+
|
|
760
|
+
function bodyItemKey(item, index) {
|
|
761
|
+
if (item.kind === 'group') {
|
|
762
|
+
return `group-${item.groupId}`
|
|
763
|
+
}
|
|
764
|
+
return resolveRowKey(item.row, item.sourceIndex ?? index)
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
function isHeaderGrouped(header) {
|
|
768
|
+
return Boolean(activeGroupBy.value && header?.key === activeGroupBy.value)
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
function toggleGroupCollapsed(groupId) {
|
|
772
|
+
const next = new Set(collapsedGroupIdSet.value)
|
|
773
|
+
if (next.has(groupId)) {
|
|
774
|
+
next.delete(groupId)
|
|
775
|
+
} else {
|
|
776
|
+
next.add(groupId)
|
|
777
|
+
}
|
|
778
|
+
collapsedGroupIds.value = [...next]
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
function setGroupBy(nextGroupBy) {
|
|
782
|
+
const header = props.headers.find((item) => item.key === nextGroupBy)
|
|
783
|
+
const normalized = isHeaderGroupable(header, props.groupable) ? nextGroupBy : null
|
|
784
|
+
|
|
785
|
+
if (props.groupBy === undefined) {
|
|
786
|
+
internalGroupBy.value = normalized
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
collapsedGroupIds.value = []
|
|
790
|
+
page.value = 1
|
|
791
|
+
emit('update:groupBy', normalized)
|
|
792
|
+
emit('group-by-change', normalized)
|
|
793
|
+
if (isRemote.value) {
|
|
794
|
+
scheduleFetchRows()
|
|
795
|
+
}
|
|
796
|
+
emitQueryChange()
|
|
797
|
+
}
|
|
798
|
+
|
|
632
799
|
function isHeaderHidden(header) {
|
|
633
800
|
if (header.hidden) {
|
|
634
801
|
return true
|
|
@@ -750,16 +917,45 @@ const sortedItems = computed(() => {
|
|
|
750
917
|
)
|
|
751
918
|
})
|
|
752
919
|
|
|
753
|
-
const
|
|
754
|
-
if (
|
|
755
|
-
return
|
|
920
|
+
const activeGroupBy = computed(() => {
|
|
921
|
+
if (!props.groupable) {
|
|
922
|
+
return null
|
|
923
|
+
}
|
|
924
|
+
if (props.groupBy !== undefined) {
|
|
925
|
+
return props.groupBy || null
|
|
756
926
|
}
|
|
927
|
+
return internalGroupBy.value
|
|
928
|
+
})
|
|
757
929
|
|
|
758
|
-
|
|
759
|
-
|
|
930
|
+
const collapsedGroupIdSet = computed(() => new Set(collapsedGroupIds.value))
|
|
931
|
+
|
|
932
|
+
const displayBodyItems = computed(() => {
|
|
933
|
+
const paginate = !isRemote.value && props.showPagination
|
|
934
|
+
const items = buildDisplayBodyItems(sortedItems.value, {
|
|
935
|
+
groupBy: activeGroupBy.value,
|
|
936
|
+
page: page.value,
|
|
937
|
+
pageSize: pageSize.value,
|
|
938
|
+
paginate,
|
|
939
|
+
collapsedGroupIds: collapsedGroupIdSet.value,
|
|
940
|
+
})
|
|
941
|
+
let displayIndex = 0
|
|
942
|
+
return items.map((item) => {
|
|
943
|
+
if (item.kind !== 'row') {
|
|
944
|
+
return item
|
|
945
|
+
}
|
|
946
|
+
const indexed = { ...item, displayIndex }
|
|
947
|
+
displayIndex += 1
|
|
948
|
+
return indexed
|
|
949
|
+
})
|
|
760
950
|
})
|
|
761
951
|
|
|
762
|
-
const
|
|
952
|
+
const displayRows = computed(() =>
|
|
953
|
+
displayBodyItems.value
|
|
954
|
+
.filter((item) => item.kind === 'row')
|
|
955
|
+
.map((item) => item.row),
|
|
956
|
+
)
|
|
957
|
+
|
|
958
|
+
const hasRows = computed(() => displayBodyItems.value.length > 0)
|
|
763
959
|
|
|
764
960
|
const hasActiveFilters = computed(() =>
|
|
765
961
|
isFilterQueryActive(activeFilterQuery.value, props.headers, props.data),
|
|
@@ -813,6 +1009,7 @@ const queryParams = computed(() => {
|
|
|
813
1009
|
pageSize: pageSize.value,
|
|
814
1010
|
sortBy: sortBy.value,
|
|
815
1011
|
sortDir: sortDir.value,
|
|
1012
|
+
groupBy: activeGroupBy.value,
|
|
816
1013
|
filterQuery: activeFilterQuery.value,
|
|
817
1014
|
}
|
|
818
1015
|
|
|
@@ -852,6 +1049,7 @@ function headerClasses(header) {
|
|
|
852
1049
|
sortable: header.sortable,
|
|
853
1050
|
filterable: isHeaderFilterable(header),
|
|
854
1051
|
filtered: isHeaderFiltered(header),
|
|
1052
|
+
grouped: isHeaderGrouped(header),
|
|
855
1053
|
'menu-open': filterPopoverOpen.value && activeFilterHeader.value?.key === header.key,
|
|
856
1054
|
[colPriorityClass(priority)]: isValidColPriority(priority),
|
|
857
1055
|
}
|
|
@@ -1402,7 +1600,7 @@ function closeRowContextMenu() {
|
|
|
1402
1600
|
rowContextMenuAnchor.value = null
|
|
1403
1601
|
}
|
|
1404
1602
|
|
|
1405
|
-
function onRowContextMenu(row,
|
|
1603
|
+
function onRowContextMenu(row, sourceIndex, event) {
|
|
1406
1604
|
if (!hasRowContextMenu.value) {
|
|
1407
1605
|
return
|
|
1408
1606
|
}
|
|
@@ -1413,8 +1611,8 @@ function onRowContextMenu(row, displayIndex, event) {
|
|
|
1413
1611
|
event.preventDefault()
|
|
1414
1612
|
event.stopPropagation()
|
|
1415
1613
|
|
|
1416
|
-
const key = resolveRowKey(row,
|
|
1417
|
-
const index = filteredRowIndex(
|
|
1614
|
+
const key = resolveRowKey(row, sourceIndex)
|
|
1615
|
+
const index = filteredRowIndex(sourceIndex)
|
|
1418
1616
|
let targetKeys
|
|
1419
1617
|
|
|
1420
1618
|
if (props.selectable && selectedKeySet.value.has(key) && activeSelectedKeys.value.length > 0) {
|
|
@@ -1549,6 +1747,17 @@ function onFilterCancel() {
|
|
|
1549
1747
|
activeFilterValue.value = []
|
|
1550
1748
|
}
|
|
1551
1749
|
|
|
1750
|
+
const canGroupByActiveFilterColumn = computed(() => {
|
|
1751
|
+
if (!props.groupable || !activeFilterHeader.value) {
|
|
1752
|
+
return false
|
|
1753
|
+
}
|
|
1754
|
+
return isHeaderGroupable(activeFilterHeader.value, props.groupable)
|
|
1755
|
+
})
|
|
1756
|
+
|
|
1757
|
+
const isActiveFilterHeaderGrouped = computed(() =>
|
|
1758
|
+
Boolean(activeFilterHeader.value?.key && activeFilterHeader.value.key === activeGroupBy.value),
|
|
1759
|
+
)
|
|
1760
|
+
|
|
1552
1761
|
const canHideActiveFilterColumn = computed(() => {
|
|
1553
1762
|
if (!showColumnOptionsButton.value || !activeFilterHeader.value) {
|
|
1554
1763
|
return false
|
|
@@ -1563,6 +1772,12 @@ const canHideActiveFilterColumn = computed(() => {
|
|
|
1563
1772
|
return configurableHeaders.value.some((item) => item.key === header.key)
|
|
1564
1773
|
})
|
|
1565
1774
|
|
|
1775
|
+
function onFilterGroupBy(columnKey) {
|
|
1776
|
+
setGroupBy(columnKey || null)
|
|
1777
|
+
filterPopoverOpen.value = false
|
|
1778
|
+
onFilterCancel()
|
|
1779
|
+
}
|
|
1780
|
+
|
|
1566
1781
|
function onFilterHideColumn() {
|
|
1567
1782
|
const header = activeFilterHeader.value
|
|
1568
1783
|
if (!header?.key || !canHideActiveFilterColumn.value) {
|
|
@@ -1648,10 +1863,13 @@ function openColumnOptions() {
|
|
|
1648
1863
|
void refreshSavedPresets()
|
|
1649
1864
|
}
|
|
1650
1865
|
|
|
1651
|
-
function onColumnOptionsApply({ order, visibleKeys, priorities }) {
|
|
1866
|
+
function onColumnOptionsApply({ order, visibleKeys, priorities, groupBy }) {
|
|
1652
1867
|
setColumnOrder(order)
|
|
1653
1868
|
setColumnVisibility(visibleKeys)
|
|
1654
1869
|
setColumnPriorities(priorities)
|
|
1870
|
+
if (groupBy !== undefined && groupBy !== activeGroupBy.value) {
|
|
1871
|
+
setGroupBy(groupBy)
|
|
1872
|
+
}
|
|
1655
1873
|
updateActiveLayoutLabel()
|
|
1656
1874
|
}
|
|
1657
1875
|
|
|
@@ -1660,15 +1878,16 @@ function onColumnOptionsCancel() {
|
|
|
1660
1878
|
}
|
|
1661
1879
|
|
|
1662
1880
|
async function refreshSavedPresets() {
|
|
1663
|
-
if (
|
|
1664
|
-
|
|
1881
|
+
if (isCallbackViewControl.value) {
|
|
1882
|
+
const handlers = viewSettingsHandlers.value
|
|
1883
|
+
if (!handlers || typeof handlers.list !== 'function') {
|
|
1665
1884
|
savedPresets.value = []
|
|
1666
1885
|
defaultPresetId.value = null
|
|
1667
1886
|
return
|
|
1668
1887
|
}
|
|
1669
1888
|
|
|
1670
1889
|
try {
|
|
1671
|
-
const result = await
|
|
1890
|
+
const result = await handlers.list()
|
|
1672
1891
|
savedPresets.value = Array.isArray(result?.presets) ? result.presets : []
|
|
1673
1892
|
defaultPresetId.value = result?.defaultPresetId ?? null
|
|
1674
1893
|
} catch {
|
|
@@ -1706,6 +1925,7 @@ function getCurrentLayoutState() {
|
|
|
1706
1925
|
filters: cloneFilters(activeFilters.value),
|
|
1707
1926
|
sortBy: sortBy.value,
|
|
1708
1927
|
sortDir: sortDir.value,
|
|
1928
|
+
groupBy: activeGroupBy.value,
|
|
1709
1929
|
pageSize: pageSize.value,
|
|
1710
1930
|
}
|
|
1711
1931
|
}
|
|
@@ -1745,6 +1965,12 @@ function applyPresetState(state) {
|
|
|
1745
1965
|
sortDir.value = 'asc'
|
|
1746
1966
|
}
|
|
1747
1967
|
|
|
1968
|
+
if (sanitized.groupBy) {
|
|
1969
|
+
setGroupBy(sanitized.groupBy)
|
|
1970
|
+
} else {
|
|
1971
|
+
setGroupBy(null)
|
|
1972
|
+
}
|
|
1973
|
+
|
|
1748
1974
|
if (sanitized.pageSize) {
|
|
1749
1975
|
pageSize.value = sanitized.pageSize
|
|
1750
1976
|
}
|
|
@@ -1760,13 +1986,14 @@ function applyPresetState(state) {
|
|
|
1760
1986
|
async function onSavePreset({ name, setAsDefault }) {
|
|
1761
1987
|
const state = getCurrentLayoutState()
|
|
1762
1988
|
|
|
1763
|
-
if (
|
|
1764
|
-
|
|
1765
|
-
|
|
1989
|
+
if (isCallbackViewControl.value) {
|
|
1990
|
+
const handlers = viewSettingsHandlers.value
|
|
1991
|
+
if (!handlers || typeof handlers.save !== 'function') {
|
|
1992
|
+
return { ok: false, error: 'View save callback is unavailable.' }
|
|
1766
1993
|
}
|
|
1767
1994
|
|
|
1768
1995
|
try {
|
|
1769
|
-
const result = await
|
|
1996
|
+
const result = await handlers.save({ name, state, setAsDefault })
|
|
1770
1997
|
if (result?.ok) {
|
|
1771
1998
|
await refreshSavedPresets()
|
|
1772
1999
|
if (result.preset?.id) {
|
|
@@ -1799,14 +2026,50 @@ async function onSavePreset({ name, setAsDefault }) {
|
|
|
1799
2026
|
return result
|
|
1800
2027
|
}
|
|
1801
2028
|
|
|
2029
|
+
async function onOverwritePreset({ presetId }) {
|
|
2030
|
+
const state = getCurrentLayoutState()
|
|
2031
|
+
|
|
2032
|
+
if (isCallbackViewControl.value) {
|
|
2033
|
+
const handlers = viewSettingsHandlers.value
|
|
2034
|
+
if (!handlers || typeof handlers.overwrite !== 'function') {
|
|
2035
|
+
return { ok: false, error: 'View overwrite callback is unavailable.' }
|
|
2036
|
+
}
|
|
2037
|
+
|
|
2038
|
+
try {
|
|
2039
|
+
const result = await handlers.overwrite({ presetId, state })
|
|
2040
|
+
if (result?.ok) {
|
|
2041
|
+
await refreshSavedPresets()
|
|
2042
|
+
activeLayoutPresetId.value = presetId
|
|
2043
|
+
updateActiveLayoutLabel()
|
|
2044
|
+
}
|
|
2045
|
+
return result ?? { ok: false, error: 'Unable to overwrite layout.' }
|
|
2046
|
+
} catch {
|
|
2047
|
+
return { ok: false, error: 'Unable to overwrite layout.' }
|
|
2048
|
+
}
|
|
2049
|
+
}
|
|
2050
|
+
|
|
2051
|
+
if (!props.tableId) {
|
|
2052
|
+
return { ok: false, error: 'Table identity is required.' }
|
|
2053
|
+
}
|
|
2054
|
+
|
|
2055
|
+
const result = overwritePreset(props.tableId, presetId, { state })
|
|
2056
|
+
if (result.ok) {
|
|
2057
|
+
refreshSavedPresets()
|
|
2058
|
+
activeLayoutPresetId.value = presetId
|
|
2059
|
+
updateActiveLayoutLabel()
|
|
2060
|
+
}
|
|
2061
|
+
return result
|
|
2062
|
+
}
|
|
2063
|
+
|
|
1802
2064
|
async function onLoadPreset(presetId) {
|
|
1803
|
-
if (
|
|
1804
|
-
|
|
1805
|
-
|
|
2065
|
+
if (isCallbackViewControl.value) {
|
|
2066
|
+
const handlers = viewSettingsHandlers.value
|
|
2067
|
+
if (!handlers || typeof handlers.load !== 'function') {
|
|
2068
|
+
return { ok: false, error: 'View load callback is unavailable.' }
|
|
1806
2069
|
}
|
|
1807
2070
|
|
|
1808
2071
|
try {
|
|
1809
|
-
const result = await
|
|
2072
|
+
const result = await handlers.load(presetId)
|
|
1810
2073
|
if (!result?.ok || !result.preset?.state) {
|
|
1811
2074
|
return result ?? { ok: false, error: 'Preset not found.' }
|
|
1812
2075
|
}
|
|
@@ -1836,13 +2099,14 @@ async function onLoadPreset(presetId) {
|
|
|
1836
2099
|
}
|
|
1837
2100
|
|
|
1838
2101
|
async function onDeletePreset(presetId) {
|
|
1839
|
-
if (
|
|
1840
|
-
|
|
1841
|
-
|
|
2102
|
+
if (isCallbackViewControl.value) {
|
|
2103
|
+
const handlers = viewSettingsHandlers.value
|
|
2104
|
+
if (!handlers || typeof handlers.delete !== 'function') {
|
|
2105
|
+
return { ok: false, error: 'View delete callback is unavailable.' }
|
|
1842
2106
|
}
|
|
1843
2107
|
|
|
1844
2108
|
try {
|
|
1845
|
-
const result = await
|
|
2109
|
+
const result = await handlers.delete(presetId)
|
|
1846
2110
|
if (result?.ok) {
|
|
1847
2111
|
await refreshSavedPresets()
|
|
1848
2112
|
if (activeLayoutPresetId.value === presetId) {
|
|
@@ -1872,13 +2136,14 @@ async function onDeletePreset(presetId) {
|
|
|
1872
2136
|
}
|
|
1873
2137
|
|
|
1874
2138
|
async function onSetDefaultPreset(presetId) {
|
|
1875
|
-
if (
|
|
1876
|
-
|
|
2139
|
+
if (isCallbackViewControl.value) {
|
|
2140
|
+
const handlers = viewSettingsHandlers.value
|
|
2141
|
+
if (!handlers || typeof handlers.setDefault !== 'function') {
|
|
1877
2142
|
return { ok: false, error: 'Unable to update default layout.' }
|
|
1878
2143
|
}
|
|
1879
2144
|
|
|
1880
2145
|
try {
|
|
1881
|
-
const result = await
|
|
2146
|
+
const result = await handlers.setDefault(presetId)
|
|
1882
2147
|
if (result?.ok) {
|
|
1883
2148
|
await refreshSavedPresets()
|
|
1884
2149
|
}
|
|
@@ -1921,6 +2186,7 @@ function getDeveloperDefaultLayoutState() {
|
|
|
1921
2186
|
filters: {},
|
|
1922
2187
|
sortBy: null,
|
|
1923
2188
|
sortDir: 'asc',
|
|
2189
|
+
groupBy: null,
|
|
1924
2190
|
pageSize: DEFAULT_PAGE_SIZE,
|
|
1925
2191
|
}
|
|
1926
2192
|
}
|
|
@@ -1980,6 +2246,7 @@ function loadDeveloperDefaults() {
|
|
|
1980
2246
|
setFilters({})
|
|
1981
2247
|
sortBy.value = null
|
|
1982
2248
|
sortDir.value = 'asc'
|
|
2249
|
+
setGroupBy(null)
|
|
1983
2250
|
setColumnOrder(getDeveloperDefaultColumnOrder())
|
|
1984
2251
|
setColumnPriorities({})
|
|
1985
2252
|
|
|
@@ -2074,6 +2341,17 @@ watch(
|
|
|
2074
2341
|
},
|
|
2075
2342
|
)
|
|
2076
2343
|
|
|
2344
|
+
watch(
|
|
2345
|
+
activeGroupBy,
|
|
2346
|
+
() => {
|
|
2347
|
+
page.value = 1
|
|
2348
|
+
if (isRemote.value) {
|
|
2349
|
+
scheduleFetchRows()
|
|
2350
|
+
}
|
|
2351
|
+
emitQueryChange()
|
|
2352
|
+
},
|
|
2353
|
+
)
|
|
2354
|
+
|
|
2077
2355
|
watch(
|
|
2078
2356
|
() => [sortBy.value, sortDir.value],
|
|
2079
2357
|
() => {
|
|
@@ -2184,6 +2462,24 @@ watch(
|
|
|
2184
2462
|
{ deep: true },
|
|
2185
2463
|
)
|
|
2186
2464
|
|
|
2465
|
+
watch(
|
|
2466
|
+
() => props.groupBy,
|
|
2467
|
+
(nextGroupBy) => {
|
|
2468
|
+
if (nextGroupBy !== undefined) {
|
|
2469
|
+
collapsedGroupIds.value = []
|
|
2470
|
+
}
|
|
2471
|
+
},
|
|
2472
|
+
)
|
|
2473
|
+
|
|
2474
|
+
watch(
|
|
2475
|
+
() => props.groupable,
|
|
2476
|
+
(enabled) => {
|
|
2477
|
+
if (!enabled) {
|
|
2478
|
+
setGroupBy(null)
|
|
2479
|
+
}
|
|
2480
|
+
},
|
|
2481
|
+
)
|
|
2482
|
+
|
|
2187
2483
|
watch(
|
|
2188
2484
|
() => props.columnPriorities,
|
|
2189
2485
|
(nextPriorities) => {
|
|
@@ -2204,7 +2500,7 @@ watch(
|
|
|
2204
2500
|
)
|
|
2205
2501
|
|
|
2206
2502
|
watch(
|
|
2207
|
-
() => [props.
|
|
2503
|
+
() => [props.viewControlMode, props.tableId, viewSettingsHandlers.value],
|
|
2208
2504
|
() => {
|
|
2209
2505
|
void refreshSavedPresets()
|
|
2210
2506
|
updateActiveLayoutLabel()
|
|
@@ -2221,6 +2517,7 @@ watch(
|
|
|
2221
2517
|
activeFilters,
|
|
2222
2518
|
sortBy,
|
|
2223
2519
|
sortDir,
|
|
2520
|
+
activeGroupBy,
|
|
2224
2521
|
pageSize,
|
|
2225
2522
|
() => props.headers,
|
|
2226
2523
|
savedPresets,
|
|
@@ -2247,17 +2544,18 @@ watch(
|
|
|
2247
2544
|
|
|
2248
2545
|
onMounted(async () => {
|
|
2249
2546
|
if (props.loadSavedLayout) {
|
|
2250
|
-
|
|
2547
|
+
const handlers = viewSettingsHandlers.value
|
|
2548
|
+
if (isCallbackViewControl.value && handlers && typeof handlers.getDefault === 'function') {
|
|
2251
2549
|
try {
|
|
2252
|
-
const result = await
|
|
2550
|
+
const result = await handlers.getDefault()
|
|
2253
2551
|
if (result?.preset?.state) {
|
|
2254
2552
|
applyPresetState(result.preset.state)
|
|
2255
2553
|
activeLayoutPresetId.value = result.preset.id
|
|
2256
2554
|
}
|
|
2257
2555
|
} catch {
|
|
2258
|
-
// Ignore default layout load failures in
|
|
2556
|
+
// Ignore default layout load failures in callback mode.
|
|
2259
2557
|
}
|
|
2260
|
-
} else if (!
|
|
2558
|
+
} else if (!isCallbackViewControl.value && props.tableId) {
|
|
2261
2559
|
const preset = getDefaultPreset(props.tableId)
|
|
2262
2560
|
if (preset?.state) {
|
|
2263
2561
|
applyPresetState(preset.state)
|
|
@@ -2330,9 +2628,57 @@ table.loading tbody {
|
|
|
2330
2628
|
}
|
|
2331
2629
|
|
|
2332
2630
|
.sort-indicator {
|
|
2631
|
+
display: inline-flex;
|
|
2632
|
+
align-items: center;
|
|
2633
|
+
justify-content: center;
|
|
2333
2634
|
width: 1.5em;
|
|
2334
|
-
|
|
2335
|
-
|
|
2635
|
+
vertical-align: middle;
|
|
2636
|
+
}
|
|
2637
|
+
|
|
2638
|
+
.group-indicator {
|
|
2639
|
+
display: inline-flex;
|
|
2640
|
+
vertical-align: middle;
|
|
2641
|
+
margin-right: 0.25rem;
|
|
2642
|
+
opacity: 0.85;
|
|
2643
|
+
}
|
|
2644
|
+
|
|
2645
|
+
table thead th.grouped {
|
|
2646
|
+
color: var(--table-header-active-fg);
|
|
2647
|
+
}
|
|
2648
|
+
|
|
2649
|
+
tbody tr.table-group-row > td.table-group-cell {
|
|
2650
|
+
background-color: var(--table-header-active-bg);
|
|
2651
|
+
color: var(--table-header-fg);
|
|
2652
|
+
font-weight: 600;
|
|
2653
|
+
padding-top: 0.65rem;
|
|
2654
|
+
padding-bottom: 0.65rem;
|
|
2655
|
+
}
|
|
2656
|
+
|
|
2657
|
+
.table-group-toggle {
|
|
2658
|
+
display: inline-flex;
|
|
2659
|
+
align-items: center;
|
|
2660
|
+
justify-content: center;
|
|
2661
|
+
min-width: 1.5rem;
|
|
2662
|
+
margin-right: 0.35rem;
|
|
2663
|
+
padding: 0.1rem 0.35rem;
|
|
2664
|
+
vertical-align: middle;
|
|
2665
|
+
}
|
|
2666
|
+
|
|
2667
|
+
.table-group-chevron {
|
|
2668
|
+
flex-shrink: 0;
|
|
2669
|
+
transition: transform 0.15s ease;
|
|
2670
|
+
}
|
|
2671
|
+
|
|
2672
|
+
.table-group-row:not(.table-group-row-collapsed) .table-group-chevron {
|
|
2673
|
+
transform: rotate(180deg);
|
|
2674
|
+
}
|
|
2675
|
+
|
|
2676
|
+
.table-group-label {
|
|
2677
|
+
margin-right: 0.35rem;
|
|
2678
|
+
}
|
|
2679
|
+
|
|
2680
|
+
.table-group-count {
|
|
2681
|
+
font-weight: normal;
|
|
2336
2682
|
}
|
|
2337
2683
|
|
|
2338
2684
|
td:first-child,
|