picocrank 1.24.0 → 1.25.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/AGENTS.md +2 -0
- package/docs/filter-query-v1.md +180 -0
- package/logo.svg +7 -7
- package/package.json +7 -5
- package/public/themes/README.md +19 -2
- package/public/themes/index.json +1 -5
- package/styles/dark-theme.css +16 -1
- package/supplemental-themes/README.md +9 -0
- package/supplemental-themes/catppuccin-latte-frappe/theme.css +276 -0
- package/supplemental-themes/dracula-alucard/theme.css +258 -0
- package/supplemental-themes/gruvbox-dark-light/theme.css +264 -0
- package/supplemental-themes/index.json +17 -0
- package/{public/themes → supplemental-themes}/waffles/theme.css +99 -0
- package/vite.config.js +91 -0
- package/vue/components/Header.vue +3 -3
- package/vue/components/Login.vue +1 -3
- package/vue/components/Section.vue +45 -14
- package/vue/components/Sidebar.vue +2 -1
- package/vue/components/SidebarNavLink.vue +6 -0
- package/vue/components/Table.vue +1113 -73
- package/vue/components/TableColumnFilterPopover.vue +263 -44
- package/vue/components/TableColumnOptionsPopover.vue +1116 -0
- package/vue/components/ThemeSwitcher.vue +119 -0
- package/vue/components/TopBar.vue +2 -2
- package/vue/composables/tableColumnPresets.js +296 -0
- package/vue/composables/tableFilterQuery.js +202 -0
- package/vue/composables/tableFilters.js +167 -3
- package/vue/composables/useCustomTheme.js +167 -24
- package/vue/data/examplePeople.js +2 -6
- package/vue/examples/App.vue +1 -1
- package/vue/examples/LoginExample.vue +1 -3
- package/vue/examples/TableExample.vue +329 -11
- package/vue/examples/TableRemoteExample.vue +112 -4
- package/vue/examples/ThemePreview.vue +17 -31
- package/public/themes/catppuccin/theme.css +0 -195
- package/public/themes/dracula/theme.css +0 -181
package/vue/components/Table.vue
CHANGED
|
@@ -1,55 +1,117 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
<span
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
2
|
+
<div
|
|
3
|
+
v-if="showTableChrome"
|
|
4
|
+
:class="needsTableWrapper ? tableWrapperClasses : undefined"
|
|
5
|
+
:style="needsTableWrapper ? undefined : { display: 'contents' }"
|
|
6
|
+
>
|
|
7
|
+
<table class="row-hover" :class="{ loading: isLoading }">
|
|
8
|
+
<thead>
|
|
9
|
+
<tr>
|
|
10
|
+
<th
|
|
11
|
+
v-for="(header, index) in visibleHeaders"
|
|
12
|
+
:key="header.key || index"
|
|
13
|
+
:class="[headerClasses(header), header.class]"
|
|
14
|
+
:style="{ width: header.width || 'auto' }"
|
|
15
|
+
@click="onHeaderClick(header, $event)"
|
|
16
|
+
@contextmenu.prevent="openFilter(header, $event)"
|
|
17
|
+
@pointerdown="onHeaderPointerDown(header, $event)"
|
|
18
|
+
@pointermove="longPress.onPointerMove"
|
|
19
|
+
@pointerup="longPress.onPointerUp"
|
|
20
|
+
@pointercancel="longPress.onPointerCancel"
|
|
21
|
+
@pointerleave="longPress.onPointerLeave"
|
|
22
|
+
>
|
|
23
|
+
<span class="header-label">
|
|
24
|
+
{{ header.label || header.key }}<template v-if="headerFilterCount(header)"> ({{ headerFilterCount(header) }})</template>
|
|
25
|
+
</span>
|
|
26
|
+
|
|
27
|
+
<span v-if="filterable && isHeaderFiltered(header)" class="filter-indicator" title="Filtered" aria-hidden="true">⧩</span>
|
|
28
|
+
|
|
29
|
+
<span v-if="header.sortable" class="sort-indicator">
|
|
30
|
+
<span v-if="sortBy === header.key">
|
|
31
|
+
<span v-if="sortDir === 'asc'">▲</span>
|
|
32
|
+
<span v-else-if="sortDir === 'desc'">▼</span>
|
|
33
|
+
</span>
|
|
34
|
+
</span>
|
|
35
|
+
</th>
|
|
36
|
+
<th
|
|
37
|
+
v-if="showColumnOptionsButton"
|
|
38
|
+
class="actions table-column-options-header"
|
|
39
|
+
>
|
|
40
|
+
<div class="table-column-options-header-content">
|
|
41
|
+
<span v-if="activeLayoutLabel" class="table-layout-label">{{ activeLayoutLabel }}</span>
|
|
42
|
+
<span class="actions-menu-trigger">
|
|
43
|
+
<button
|
|
44
|
+
ref="columnOptionsButtonRef"
|
|
45
|
+
type="button"
|
|
46
|
+
aria-haspopup="dialog"
|
|
47
|
+
aria-label="Column options"
|
|
48
|
+
:aria-expanded="columnOptionsOpen ? 'true' : 'false'"
|
|
49
|
+
@click="openColumnOptions"
|
|
50
|
+
>
|
|
51
|
+
<HugeiconsIcon
|
|
52
|
+
:icon="LayoutGridIcon"
|
|
53
|
+
width="0.95em"
|
|
54
|
+
height="0.95em"
|
|
55
|
+
:strokeWidth="2"
|
|
56
|
+
aria-hidden="true"
|
|
57
|
+
/>
|
|
58
|
+
</button>
|
|
59
|
+
</span>
|
|
60
|
+
</div>
|
|
61
|
+
</th>
|
|
62
|
+
</tr>
|
|
63
|
+
</thead>
|
|
64
|
+
<tbody v-if="hasRows">
|
|
65
|
+
<tr
|
|
66
|
+
v-for="(row, rowIndex) in displayRows"
|
|
67
|
+
:key="resolveRowKey(row, rowIndex)"
|
|
68
|
+
:class="{ 'row-clickable': rowClickable }"
|
|
69
|
+
@click="onRowClick(row, rowIndex, $event)"
|
|
70
|
+
>
|
|
71
|
+
<td
|
|
72
|
+
v-for="(header, cellIndex) in visibleHeaders"
|
|
73
|
+
:key="header.key || cellIndex"
|
|
74
|
+
:class="[cellClasses(header), header.class]"
|
|
75
|
+
>
|
|
76
|
+
<slot
|
|
77
|
+
v-if="slots[`cell-${header.key}`]"
|
|
78
|
+
:name="`cell-${header.key}`"
|
|
79
|
+
:row="row"
|
|
80
|
+
:value="row[header.key]"
|
|
81
|
+
/>
|
|
82
|
+
<slot
|
|
83
|
+
v-else-if="slots.cell"
|
|
84
|
+
name="cell"
|
|
85
|
+
:row="row"
|
|
86
|
+
:value="row[header.key]"
|
|
87
|
+
/>
|
|
88
|
+
<span v-else>
|
|
89
|
+
{{ row[header.key] }}
|
|
90
|
+
</span>
|
|
91
|
+
</td>
|
|
92
|
+
<td
|
|
93
|
+
v-if="showColumnOptionsButton"
|
|
94
|
+
class="actions table-column-options-cell"
|
|
95
|
+
aria-hidden="true"
|
|
44
96
|
/>
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
97
|
+
</tr>
|
|
98
|
+
</tbody>
|
|
99
|
+
</table>
|
|
100
|
+
|
|
101
|
+
<div v-if="isFilteredEmpty" class="table-filtered-empty-state">
|
|
102
|
+
<slot name="filtered-empty" :clear-filters="clearAllFilters">
|
|
103
|
+
<p class="table-filtered-empty-message">No rows match the current filters.</p>
|
|
104
|
+
<button type="button" class="neutral" @click="clearAllFilters">Clear filters</button>
|
|
105
|
+
</slot>
|
|
106
|
+
</div>
|
|
107
|
+
</div>
|
|
108
|
+
<div v-else class="table-empty-state" :class="{ loading: isLoading }">
|
|
109
|
+
<span v-if="isLoading">Loading…</span>
|
|
110
|
+
<slot v-else name="empty">
|
|
111
|
+
<div class="table-empty">There are 0 items to show</div>
|
|
112
|
+
</slot>
|
|
113
|
+
</div>
|
|
114
|
+
<div v-if="showPagination && showTableChrome" class="padding">
|
|
53
115
|
<Pagination :total="totalCount" v-model:page="page" v-model:page-size="pageSize" />
|
|
54
116
|
</div>
|
|
55
117
|
|
|
@@ -65,25 +127,72 @@
|
|
|
65
127
|
@clear="onFilterClear"
|
|
66
128
|
@cancel="onFilterCancel"
|
|
67
129
|
/>
|
|
130
|
+
|
|
131
|
+
<TableColumnOptionsPopover
|
|
132
|
+
v-if="showColumnOptionsButton"
|
|
133
|
+
v-model:open="columnOptionsOpen"
|
|
134
|
+
:headers="props.headers"
|
|
135
|
+
:column-keys="resolvedColumnOrder"
|
|
136
|
+
:visible-keys="visibleColumnKeys"
|
|
137
|
+
:column-priorities="resolvedColumnPriorities"
|
|
138
|
+
:filters="activeFilters"
|
|
139
|
+
:rows="props.data"
|
|
140
|
+
:table-id="props.tableId"
|
|
141
|
+
:layout-presets-enabled="layoutPresetsAvailable"
|
|
142
|
+
:presets="savedPresets"
|
|
143
|
+
:default-preset-id="defaultPresetId"
|
|
144
|
+
:save-layout-preset="onSavePreset"
|
|
145
|
+
:load-layout-preset="onLoadPreset"
|
|
146
|
+
:delete-layout-preset="onDeletePreset"
|
|
147
|
+
:set-default-layout-preset="onSetDefaultPreset"
|
|
148
|
+
:load-developer-defaults="loadDeveloperDefaults"
|
|
149
|
+
:anchor-el="columnOptionsButtonRef"
|
|
150
|
+
@apply="onColumnOptionsApply"
|
|
151
|
+
@cancel="onColumnOptionsCancel"
|
|
152
|
+
/>
|
|
68
153
|
</template>
|
|
69
154
|
|
|
70
155
|
<script setup>
|
|
71
|
-
import { ref, computed, watch, onMounted, onBeforeUnmount, useSlots } from 'vue'
|
|
156
|
+
import { ref, computed, watch, onMounted, onBeforeUnmount, useSlots, useAttrs } from 'vue'
|
|
157
|
+
import { HugeiconsIcon } from '@hugeicons/vue'
|
|
158
|
+
import { LayoutGridIcon } from '@hugeicons/core-free-icons'
|
|
72
159
|
import Pagination from './Pagination.vue'
|
|
73
160
|
import TableColumnFilterPopover from './TableColumnFilterPopover.vue'
|
|
161
|
+
import TableColumnOptionsPopover from './TableColumnOptionsPopover.vue'
|
|
74
162
|
import { useLongPress } from '../composables/useLongPress.js'
|
|
75
163
|
import {
|
|
76
164
|
applyFilters,
|
|
77
165
|
buildSelectOptions,
|
|
166
|
+
cloneColumnFilterEntries,
|
|
78
167
|
cloneFilters,
|
|
79
|
-
|
|
168
|
+
countActiveFilters,
|
|
169
|
+
isColumnFiltered,
|
|
170
|
+
isFilterEntryActive,
|
|
171
|
+
normalizeColumnFilterEntries,
|
|
80
172
|
sortRows,
|
|
81
173
|
} from '../composables/tableFilters.js'
|
|
174
|
+
import {
|
|
175
|
+
applyFilterQuery,
|
|
176
|
+
buildFilterQuery,
|
|
177
|
+
isFilterQueryActive,
|
|
178
|
+
} from '../composables/tableFilterQuery.js'
|
|
179
|
+
import {
|
|
180
|
+
getDefaultPreset,
|
|
181
|
+
getPresetById,
|
|
182
|
+
listPresets,
|
|
183
|
+
savePreset,
|
|
184
|
+
deletePreset,
|
|
185
|
+
setDefaultPreset,
|
|
186
|
+
sanitizePresetState,
|
|
187
|
+
layoutStatesEqual,
|
|
188
|
+
} from '../composables/tableColumnPresets.js'
|
|
189
|
+
|
|
190
|
+
const DEFAULT_PAGE_SIZE = 10
|
|
82
191
|
|
|
83
192
|
const sortBy = ref(null)
|
|
84
193
|
const sortDir = ref('asc')
|
|
85
194
|
const page = ref(1)
|
|
86
|
-
const pageSize = ref(
|
|
195
|
+
const pageSize = ref(DEFAULT_PAGE_SIZE)
|
|
87
196
|
|
|
88
197
|
const props = defineProps({
|
|
89
198
|
headers: {
|
|
@@ -118,6 +227,51 @@ const props = defineProps({
|
|
|
118
227
|
type: Number,
|
|
119
228
|
default: 300,
|
|
120
229
|
},
|
|
230
|
+
horizontalScroll: {
|
|
231
|
+
type: Boolean,
|
|
232
|
+
default: false,
|
|
233
|
+
},
|
|
234
|
+
stickyCols: {
|
|
235
|
+
type: Number,
|
|
236
|
+
default: null,
|
|
237
|
+
validator: (value) => value === null || (value >= 1 && value <= 3),
|
|
238
|
+
},
|
|
239
|
+
columnOptions: {
|
|
240
|
+
type: Boolean,
|
|
241
|
+
default: true,
|
|
242
|
+
},
|
|
243
|
+
columnVisibility: {
|
|
244
|
+
type: Object,
|
|
245
|
+
default: undefined,
|
|
246
|
+
},
|
|
247
|
+
columnOrder: {
|
|
248
|
+
type: Array,
|
|
249
|
+
default: undefined,
|
|
250
|
+
},
|
|
251
|
+
columnPriorities: {
|
|
252
|
+
type: Object,
|
|
253
|
+
default: undefined,
|
|
254
|
+
},
|
|
255
|
+
tableId: {
|
|
256
|
+
type: String,
|
|
257
|
+
default: '',
|
|
258
|
+
},
|
|
259
|
+
loadSavedLayout: {
|
|
260
|
+
type: Boolean,
|
|
261
|
+
default: true,
|
|
262
|
+
},
|
|
263
|
+
defaultColumnVisibility: {
|
|
264
|
+
type: Object,
|
|
265
|
+
default: undefined,
|
|
266
|
+
},
|
|
267
|
+
rowKey: {
|
|
268
|
+
type: [String, Function],
|
|
269
|
+
default: 'id',
|
|
270
|
+
},
|
|
271
|
+
layoutPresets: {
|
|
272
|
+
type: Object,
|
|
273
|
+
default: null,
|
|
274
|
+
},
|
|
121
275
|
})
|
|
122
276
|
|
|
123
277
|
const emit = defineEmits([
|
|
@@ -125,9 +279,19 @@ const emit = defineEmits([
|
|
|
125
279
|
'filter-change',
|
|
126
280
|
'query-change',
|
|
127
281
|
'fetch-error',
|
|
282
|
+
'row-click',
|
|
283
|
+
'update:columnVisibility',
|
|
284
|
+
'column-visibility-change',
|
|
285
|
+
'update:columnOrder',
|
|
286
|
+
'column-order-change',
|
|
287
|
+
'update:columnPriorities',
|
|
288
|
+
'column-priorities-change',
|
|
128
289
|
])
|
|
129
290
|
|
|
291
|
+
const attrs = useAttrs()
|
|
130
292
|
const slots = useSlots()
|
|
293
|
+
|
|
294
|
+
const rowClickable = computed(() => Boolean(attrs.onRowClick))
|
|
131
295
|
const internalFilters = ref({})
|
|
132
296
|
const remoteRows = ref([])
|
|
133
297
|
const remoteTotal = ref(0)
|
|
@@ -137,12 +301,27 @@ const fetchAbortController = ref(null)
|
|
|
137
301
|
const fetchDebounceTimer = ref(null)
|
|
138
302
|
|
|
139
303
|
const filterPopoverOpen = ref(false)
|
|
304
|
+
const columnOptionsOpen = ref(false)
|
|
305
|
+
const columnOptionsButtonRef = ref(null)
|
|
140
306
|
const activeFilterHeader = ref(null)
|
|
141
307
|
const activeFilterAnchor = ref(null)
|
|
142
|
-
const activeFilterValue = ref(
|
|
308
|
+
const activeFilterValue = ref([])
|
|
309
|
+
const internalHiddenColumnKeys = ref([])
|
|
310
|
+
const internalColumnOrder = ref([])
|
|
311
|
+
const internalColumnPriorities = ref({})
|
|
312
|
+
const savedPresets = ref([])
|
|
313
|
+
const defaultPresetId = ref(null)
|
|
314
|
+
const activeLayoutPresetId = ref(null)
|
|
143
315
|
|
|
144
316
|
const isRemote = computed(() => typeof props.fetchRows === 'function')
|
|
145
317
|
|
|
318
|
+
const layoutPresetsAvailable = computed(() => {
|
|
319
|
+
if (isRemote.value) {
|
|
320
|
+
return props.layoutPresets != null
|
|
321
|
+
}
|
|
322
|
+
return Boolean(props.tableId)
|
|
323
|
+
})
|
|
324
|
+
|
|
146
325
|
const activeFilters = computed(() => {
|
|
147
326
|
if (!props.filterable) {
|
|
148
327
|
return {}
|
|
@@ -150,15 +329,122 @@ const activeFilters = computed(() => {
|
|
|
150
329
|
return props.filters !== undefined ? props.filters : internalFilters.value
|
|
151
330
|
})
|
|
152
331
|
|
|
332
|
+
const activeFilterQuery = computed(() =>
|
|
333
|
+
buildFilterQuery(activeFilters.value, props.headers, props.data),
|
|
334
|
+
)
|
|
335
|
+
|
|
153
336
|
const isLoading = computed(() => props.loading || internalLoading.value)
|
|
154
337
|
|
|
155
|
-
const
|
|
338
|
+
const configurableHeaders = computed(() =>
|
|
339
|
+
props.headers.filter((header) => header?.key && header.hideable !== false && !header.hidden),
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
const manageableColumnKeys = computed(() =>
|
|
343
|
+
props.headers
|
|
344
|
+
.filter((header) => header?.key && !header.hidden)
|
|
345
|
+
.map((header) => header.key),
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
const resolvedColumnOrder = computed(() => {
|
|
349
|
+
const keys = manageableColumnKeys.value
|
|
350
|
+
const order = props.columnOrder !== undefined ? props.columnOrder : internalColumnOrder.value
|
|
351
|
+
const fromOrder = order.filter((key) => keys.includes(key))
|
|
352
|
+
const missing = keys.filter((key) => !fromOrder.includes(key))
|
|
353
|
+
return [...fromOrder, ...missing]
|
|
354
|
+
})
|
|
355
|
+
|
|
356
|
+
const orderedHeaders = computed(() => {
|
|
357
|
+
const headerByKey = new Map(
|
|
358
|
+
props.headers
|
|
359
|
+
.filter((header) => header?.key)
|
|
360
|
+
.map((header) => [header.key, header]),
|
|
361
|
+
)
|
|
362
|
+
return resolvedColumnOrder.value
|
|
363
|
+
.map((key) => headerByKey.get(key))
|
|
364
|
+
.filter(Boolean)
|
|
365
|
+
})
|
|
366
|
+
|
|
367
|
+
const showColumnOptionsButton = computed(() =>
|
|
368
|
+
props.columnOptions && manageableColumnKeys.value.length > 0,
|
|
369
|
+
)
|
|
370
|
+
|
|
371
|
+
function isHeaderHidden(header) {
|
|
372
|
+
if (header.hidden) {
|
|
373
|
+
return true
|
|
374
|
+
}
|
|
375
|
+
if (props.columnVisibility !== undefined) {
|
|
376
|
+
return props.columnVisibility[header.key] === false
|
|
377
|
+
}
|
|
378
|
+
return internalHiddenColumnKeys.value.includes(header.key)
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
const visibleHeaders = computed(() => orderedHeaders.value.filter((header) => !isHeaderHidden(header)))
|
|
382
|
+
|
|
383
|
+
const visibleColumnKeys = computed(() =>
|
|
384
|
+
orderedHeaders.value
|
|
385
|
+
.filter((header) => !isHeaderHidden(header))
|
|
386
|
+
.map((header) => header.key),
|
|
387
|
+
)
|
|
388
|
+
|
|
389
|
+
function defaultColumnPriority(header) {
|
|
390
|
+
return isValidColPriority(header?.colPriority) ? header.colPriority : null
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
function resolvedColumnPriority(header) {
|
|
394
|
+
if (!header?.key) {
|
|
395
|
+
return null
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
const overrides = props.columnPriorities !== undefined
|
|
399
|
+
? props.columnPriorities
|
|
400
|
+
: internalColumnPriorities.value
|
|
401
|
+
|
|
402
|
+
if (Object.prototype.hasOwnProperty.call(overrides, header.key)) {
|
|
403
|
+
const override = overrides[header.key]
|
|
404
|
+
return isValidColPriority(override) ? override : null
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
return defaultColumnPriority(header)
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
const resolvedColumnPriorities = computed(() =>
|
|
411
|
+
Object.fromEntries(
|
|
412
|
+
manageableColumnKeys.value.map((key) => {
|
|
413
|
+
const header = props.headers.find((item) => item.key === key)
|
|
414
|
+
return [key, resolvedColumnPriority(header)]
|
|
415
|
+
}),
|
|
416
|
+
),
|
|
417
|
+
)
|
|
418
|
+
|
|
419
|
+
const hasColPriorities = computed(() =>
|
|
420
|
+
orderedHeaders.value.some((header) => isValidColPriority(resolvedColumnPriority(header))),
|
|
421
|
+
)
|
|
422
|
+
|
|
423
|
+
const hasHorizontalScroll = computed(() =>
|
|
424
|
+
props.horizontalScroll || (props.stickyCols >= 1 && props.stickyCols <= 3),
|
|
425
|
+
)
|
|
426
|
+
|
|
427
|
+
const needsTableWrapper = computed(() => hasColPriorities.value || hasHorizontalScroll.value)
|
|
428
|
+
|
|
429
|
+
const tableWrapperClasses = computed(() => {
|
|
430
|
+
const classes = []
|
|
431
|
+
if (hasColPriorities.value) {
|
|
432
|
+
classes.push('responsive-cols')
|
|
433
|
+
}
|
|
434
|
+
if (hasHorizontalScroll.value) {
|
|
435
|
+
classes.push('table-scroll')
|
|
436
|
+
if (props.stickyCols >= 1 && props.stickyCols <= 3) {
|
|
437
|
+
classes.push(`sticky-cols-${props.stickyCols}`)
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
return classes
|
|
441
|
+
})
|
|
156
442
|
|
|
157
443
|
const filteredItems = computed(() => {
|
|
158
444
|
if (isRemote.value) {
|
|
159
445
|
return remoteRows.value
|
|
160
446
|
}
|
|
161
|
-
return
|
|
447
|
+
return applyFilterQuery(props.data, props.headers, activeFilterQuery.value)
|
|
162
448
|
})
|
|
163
449
|
|
|
164
450
|
const sortedItems = computed(() => {
|
|
@@ -177,6 +463,40 @@ const displayRows = computed(() => {
|
|
|
177
463
|
return sortedItems.value.slice(start, start + pageSize.value)
|
|
178
464
|
})
|
|
179
465
|
|
|
466
|
+
const hasRows = computed(() => displayRows.value.length > 0)
|
|
467
|
+
|
|
468
|
+
const hasActiveFilters = computed(() =>
|
|
469
|
+
isFilterQueryActive(activeFilterQuery.value, props.headers, props.data),
|
|
470
|
+
)
|
|
471
|
+
|
|
472
|
+
const hasSourceData = computed(() => {
|
|
473
|
+
if (isRemote.value) {
|
|
474
|
+
return remoteTotal.value > 0 || hasActiveFilters.value
|
|
475
|
+
}
|
|
476
|
+
return props.data.length > 0
|
|
477
|
+
})
|
|
478
|
+
|
|
479
|
+
const isFilteredEmpty = computed(() => {
|
|
480
|
+
if (isLoading.value || hasRows.value || !hasActiveFilters.value) {
|
|
481
|
+
return false
|
|
482
|
+
}
|
|
483
|
+
return hasSourceData.value
|
|
484
|
+
})
|
|
485
|
+
|
|
486
|
+
const showTableChrome = computed(() => hasRows.value || isFilteredEmpty.value)
|
|
487
|
+
|
|
488
|
+
watch(showTableChrome, (visible) => {
|
|
489
|
+
if (visible) {
|
|
490
|
+
return
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
filterPopoverOpen.value = false
|
|
494
|
+
columnOptionsOpen.value = false
|
|
495
|
+
activeFilterHeader.value = null
|
|
496
|
+
activeFilterAnchor.value = null
|
|
497
|
+
activeFilterValue.value = []
|
|
498
|
+
})
|
|
499
|
+
|
|
180
500
|
const totalCount = computed(() => {
|
|
181
501
|
if (isRemote.value) {
|
|
182
502
|
return remoteTotal.value
|
|
@@ -191,17 +511,39 @@ const activeSelectOptions = computed(() => {
|
|
|
191
511
|
return buildSelectOptions(props.data, activeFilterHeader.value)
|
|
192
512
|
})
|
|
193
513
|
|
|
194
|
-
const queryParams = computed(() =>
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
514
|
+
const queryParams = computed(() => {
|
|
515
|
+
const params = {
|
|
516
|
+
page: page.value,
|
|
517
|
+
pageSize: pageSize.value,
|
|
518
|
+
sortBy: sortBy.value,
|
|
519
|
+
sortDir: sortDir.value,
|
|
520
|
+
filterQuery: activeFilterQuery.value,
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
// Remote tables use filterQuery only; see docs/filter-query-v1.md.
|
|
524
|
+
if (!isRemote.value) {
|
|
525
|
+
params.filters = cloneFilters(activeFilters.value)
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
return params
|
|
529
|
+
})
|
|
530
|
+
|
|
531
|
+
function isValidColPriority(priority) {
|
|
532
|
+
return Number.isInteger(priority) && priority >= 1 && priority <= 5
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
function colPriorityClass(priority) {
|
|
536
|
+
return isValidColPriority(priority) ? `col-priority-${priority}` : null
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
function headerFilterCount(header) {
|
|
540
|
+
if (!isHeaderFilterable(header)) {
|
|
541
|
+
return 0
|
|
542
|
+
}
|
|
201
543
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
544
|
+
return normalizeColumnFilterEntries(activeFilters.value?.[header.key], header, props.data)
|
|
545
|
+
.filter((entry) => isFilterEntryActive(entry))
|
|
546
|
+
.length
|
|
205
547
|
}
|
|
206
548
|
|
|
207
549
|
function isHeaderFilterable(header) {
|
|
@@ -209,15 +551,45 @@ function isHeaderFilterable(header) {
|
|
|
209
551
|
}
|
|
210
552
|
|
|
211
553
|
function isHeaderFiltered(header) {
|
|
212
|
-
return
|
|
554
|
+
return isColumnFiltered(activeFilters.value?.[header.key], header, props.data)
|
|
213
555
|
}
|
|
214
556
|
|
|
215
557
|
function headerClasses(header) {
|
|
558
|
+
const priority = resolvedColumnPriority(header)
|
|
216
559
|
return {
|
|
217
560
|
sortable: header.sortable,
|
|
218
561
|
filterable: isHeaderFilterable(header),
|
|
219
562
|
filtered: isHeaderFiltered(header),
|
|
563
|
+
'menu-open': filterPopoverOpen.value && activeFilterHeader.value?.key === header.key,
|
|
564
|
+
[colPriorityClass(priority)]: isValidColPriority(priority),
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
function cellClasses(header) {
|
|
569
|
+
const priority = resolvedColumnPriority(header)
|
|
570
|
+
return {
|
|
571
|
+
hidden: header.hidden,
|
|
572
|
+
[colPriorityClass(priority)]: isValidColPriority(priority),
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
function resolveRowKey(row, index) {
|
|
577
|
+
if (typeof props.rowKey === 'function') {
|
|
578
|
+
const key = props.rowKey(row, index)
|
|
579
|
+
if (key !== null && key !== undefined && key !== '') {
|
|
580
|
+
return key
|
|
581
|
+
}
|
|
582
|
+
return index
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
if (typeof props.rowKey === 'string' && props.rowKey) {
|
|
586
|
+
const key = row?.[props.rowKey]
|
|
587
|
+
if (key !== null && key !== undefined && key !== '') {
|
|
588
|
+
return key
|
|
589
|
+
}
|
|
220
590
|
}
|
|
591
|
+
|
|
592
|
+
return index
|
|
221
593
|
}
|
|
222
594
|
|
|
223
595
|
function setFilters(nextFilters) {
|
|
@@ -229,6 +601,15 @@ function setFilters(nextFilters) {
|
|
|
229
601
|
emit('filter-change', cloned)
|
|
230
602
|
}
|
|
231
603
|
|
|
604
|
+
function clearAllFilters() {
|
|
605
|
+
setFilters({})
|
|
606
|
+
page.value = 1
|
|
607
|
+
if (isRemote.value) {
|
|
608
|
+
scheduleFetchRows()
|
|
609
|
+
}
|
|
610
|
+
emitQueryChange()
|
|
611
|
+
}
|
|
612
|
+
|
|
232
613
|
function emitQueryChange() {
|
|
233
614
|
emit('query-change', queryParams.value)
|
|
234
615
|
}
|
|
@@ -254,6 +635,20 @@ function onHeaderClick(header, event) {
|
|
|
254
635
|
toggleSort(header)
|
|
255
636
|
}
|
|
256
637
|
|
|
638
|
+
function filteredRowIndex(displayIndex) {
|
|
639
|
+
if (props.showPagination) {
|
|
640
|
+
return (page.value - 1) * pageSize.value + displayIndex
|
|
641
|
+
}
|
|
642
|
+
return displayIndex
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
function onRowClick(row, displayIndex, event) {
|
|
646
|
+
if (event.target.closest('a, button, input, select, textarea, [data-row-click-ignore]')) {
|
|
647
|
+
return
|
|
648
|
+
}
|
|
649
|
+
emit('row-click', { row, index: filteredRowIndex(displayIndex) })
|
|
650
|
+
}
|
|
651
|
+
|
|
257
652
|
function openFilter(header, event) {
|
|
258
653
|
if (!isHeaderFilterable(header)) {
|
|
259
654
|
return
|
|
@@ -261,7 +656,11 @@ function openFilter(header, event) {
|
|
|
261
656
|
|
|
262
657
|
activeFilterHeader.value = header
|
|
263
658
|
activeFilterAnchor.value = event.currentTarget
|
|
264
|
-
activeFilterValue.value =
|
|
659
|
+
activeFilterValue.value = cloneColumnFilterEntries(
|
|
660
|
+
activeFilters.value?.[header.key],
|
|
661
|
+
header,
|
|
662
|
+
props.data,
|
|
663
|
+
)
|
|
265
664
|
filterPopoverOpen.value = true
|
|
266
665
|
}
|
|
267
666
|
|
|
@@ -282,14 +681,14 @@ function onHeaderPointerDown(header, event) {
|
|
|
282
681
|
longPress.onPointerDown(event)
|
|
283
682
|
}
|
|
284
683
|
|
|
285
|
-
function onFilterApply(
|
|
684
|
+
function onFilterApply(filterEntries) {
|
|
286
685
|
if (!activeFilterHeader.value) {
|
|
287
686
|
return
|
|
288
687
|
}
|
|
289
688
|
|
|
290
689
|
const nextFilters = cloneFilters(activeFilters.value)
|
|
291
|
-
if (
|
|
292
|
-
nextFilters[activeFilterHeader.value.key] =
|
|
690
|
+
if (filterEntries?.length) {
|
|
691
|
+
nextFilters[activeFilterHeader.value.key] = cloneColumnFilterEntries(filterEntries)
|
|
293
692
|
} else {
|
|
294
693
|
delete nextFilters[activeFilterHeader.value.key]
|
|
295
694
|
}
|
|
@@ -314,9 +713,431 @@ function onFilterClear() {
|
|
|
314
713
|
function onFilterCancel() {
|
|
315
714
|
activeFilterHeader.value = null
|
|
316
715
|
activeFilterAnchor.value = null
|
|
317
|
-
activeFilterValue.value =
|
|
716
|
+
activeFilterValue.value = []
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
function buildColumnVisibility(visibleKeys) {
|
|
720
|
+
const visibleKeySet = new Set(visibleKeys)
|
|
721
|
+
return Object.fromEntries(
|
|
722
|
+
configurableHeaders.value.map((header) => [header.key, visibleKeySet.has(header.key)]),
|
|
723
|
+
)
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
function setColumnVisibility(visibleKeys) {
|
|
727
|
+
const visibleKeySet = new Set(visibleKeys)
|
|
728
|
+
const hiddenKeys = configurableHeaders.value
|
|
729
|
+
.map((header) => header.key)
|
|
730
|
+
.filter((key) => !visibleKeySet.has(key))
|
|
731
|
+
|
|
732
|
+
if (props.columnVisibility === undefined) {
|
|
733
|
+
internalHiddenColumnKeys.value = hiddenKeys
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
const nextVisibility = buildColumnVisibility(visibleKeys)
|
|
737
|
+
emit('update:columnVisibility', nextVisibility)
|
|
738
|
+
emit('column-visibility-change', nextVisibility)
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
function syncInternalColumnOrder() {
|
|
742
|
+
if (props.columnOrder !== undefined) {
|
|
743
|
+
return
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
const keys = manageableColumnKeys.value
|
|
747
|
+
const merged = [
|
|
748
|
+
...internalColumnOrder.value.filter((key) => keys.includes(key)),
|
|
749
|
+
...keys.filter((key) => !internalColumnOrder.value.includes(key)),
|
|
750
|
+
]
|
|
751
|
+
internalColumnOrder.value = merged
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
function setColumnOrder(order) {
|
|
755
|
+
const keys = manageableColumnKeys.value
|
|
756
|
+
const nextOrder = [
|
|
757
|
+
...order.filter((key) => keys.includes(key)),
|
|
758
|
+
...keys.filter((key) => !order.includes(key)),
|
|
759
|
+
]
|
|
760
|
+
|
|
761
|
+
if (props.columnOrder === undefined) {
|
|
762
|
+
internalColumnOrder.value = nextOrder
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
emit('update:columnOrder', nextOrder)
|
|
766
|
+
emit('column-order-change', nextOrder)
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
function setColumnPriorities(overrides) {
|
|
770
|
+
const cloned = { ...overrides }
|
|
771
|
+
|
|
772
|
+
if (props.columnPriorities === undefined) {
|
|
773
|
+
internalColumnPriorities.value = cloned
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
emit('update:columnPriorities', cloned)
|
|
777
|
+
emit('column-priorities-change', cloned)
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
function openColumnOptions() {
|
|
781
|
+
columnOptionsOpen.value = true
|
|
782
|
+
void refreshSavedPresets()
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
function onColumnOptionsApply({ order, visibleKeys, priorities }) {
|
|
786
|
+
setColumnOrder(order)
|
|
787
|
+
setColumnVisibility(visibleKeys)
|
|
788
|
+
setColumnPriorities(priorities)
|
|
789
|
+
updateActiveLayoutLabel()
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
function onColumnOptionsCancel() {
|
|
793
|
+
columnOptionsOpen.value = false
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
async function refreshSavedPresets() {
|
|
797
|
+
if (isRemote.value) {
|
|
798
|
+
if (!props.layoutPresets || typeof props.layoutPresets.list !== 'function') {
|
|
799
|
+
savedPresets.value = []
|
|
800
|
+
defaultPresetId.value = null
|
|
801
|
+
return
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
try {
|
|
805
|
+
const result = await props.layoutPresets.list()
|
|
806
|
+
savedPresets.value = Array.isArray(result?.presets) ? result.presets : []
|
|
807
|
+
defaultPresetId.value = result?.defaultPresetId ?? null
|
|
808
|
+
} catch {
|
|
809
|
+
savedPresets.value = []
|
|
810
|
+
defaultPresetId.value = null
|
|
811
|
+
}
|
|
812
|
+
return
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
if (!props.tableId) {
|
|
816
|
+
savedPresets.value = []
|
|
817
|
+
defaultPresetId.value = null
|
|
818
|
+
return
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
const { presets, defaultPresetId: nextDefaultPresetId } = listPresets(props.tableId)
|
|
822
|
+
savedPresets.value = presets
|
|
823
|
+
defaultPresetId.value = nextDefaultPresetId
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
function findSavedPreset(presetId) {
|
|
827
|
+
return savedPresets.value.find((preset) => preset.id === presetId)
|
|
828
|
+
?? (props.tableId ? getPresetById(props.tableId, presetId) : null)
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
function getCurrentLayoutState() {
|
|
832
|
+
const priorityOverrides = props.columnPriorities !== undefined
|
|
833
|
+
? { ...props.columnPriorities }
|
|
834
|
+
: { ...internalColumnPriorities.value }
|
|
835
|
+
|
|
836
|
+
return {
|
|
837
|
+
columnOrder: [...resolvedColumnOrder.value],
|
|
838
|
+
columnVisibility: buildColumnVisibility(visibleColumnKeys.value),
|
|
839
|
+
columnPriorities: priorityOverrides,
|
|
840
|
+
filters: cloneFilters(activeFilters.value),
|
|
841
|
+
sortBy: sortBy.value,
|
|
842
|
+
sortDir: sortDir.value,
|
|
843
|
+
pageSize: pageSize.value,
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
function applyPresetState(state) {
|
|
848
|
+
const sanitized = sanitizePresetState(state, props.headers, props.data)
|
|
849
|
+
if (!sanitized) {
|
|
850
|
+
return
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
if (sanitized.columnOrder?.length) {
|
|
854
|
+
setColumnOrder(sanitized.columnOrder)
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
if (sanitized.columnVisibility) {
|
|
858
|
+
const visibleKeys = manageableColumnKeys.value.filter(
|
|
859
|
+
(key) => sanitized.columnVisibility[key] !== false,
|
|
860
|
+
)
|
|
861
|
+
const fallbackKeys = configurableHeaders.value
|
|
862
|
+
.filter((header) => header.hideable === false)
|
|
863
|
+
.map((header) => header.key)
|
|
864
|
+
const nextVisibleKeys = visibleKeys.length > 0
|
|
865
|
+
? visibleKeys
|
|
866
|
+
: (fallbackKeys.length > 0 ? fallbackKeys : manageableColumnKeys.value.slice(0, 1))
|
|
867
|
+
|
|
868
|
+
setColumnVisibility(nextVisibleKeys)
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
setColumnPriorities(sanitized.columnPriorities ?? {})
|
|
872
|
+
setFilters(sanitized.filters ?? {})
|
|
873
|
+
|
|
874
|
+
if (sanitized.sortBy) {
|
|
875
|
+
sortBy.value = sanitized.sortBy
|
|
876
|
+
sortDir.value = sanitized.sortDir === 'desc' ? 'desc' : 'asc'
|
|
877
|
+
} else {
|
|
878
|
+
sortBy.value = null
|
|
879
|
+
sortDir.value = 'asc'
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
if (sanitized.pageSize) {
|
|
883
|
+
pageSize.value = sanitized.pageSize
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
page.value = 1
|
|
887
|
+
if (isRemote.value) {
|
|
888
|
+
scheduleFetchRows()
|
|
889
|
+
}
|
|
890
|
+
emitQueryChange()
|
|
891
|
+
updateActiveLayoutLabel()
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
async function onSavePreset({ name, setAsDefault }) {
|
|
895
|
+
const state = getCurrentLayoutState()
|
|
896
|
+
|
|
897
|
+
if (isRemote.value) {
|
|
898
|
+
if (!props.layoutPresets || typeof props.layoutPresets.save !== 'function') {
|
|
899
|
+
return { ok: false, error: 'Remote layout save is unavailable.' }
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
try {
|
|
903
|
+
const result = await props.layoutPresets.save({ name, state, setAsDefault })
|
|
904
|
+
if (result?.ok) {
|
|
905
|
+
await refreshSavedPresets()
|
|
906
|
+
if (result.preset?.id) {
|
|
907
|
+
activeLayoutPresetId.value = result.preset.id
|
|
908
|
+
}
|
|
909
|
+
updateActiveLayoutLabel()
|
|
910
|
+
}
|
|
911
|
+
return result ?? { ok: false, error: 'Unable to save layout.' }
|
|
912
|
+
} catch {
|
|
913
|
+
return { ok: false, error: 'Unable to save layout.' }
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
if (!props.tableId) {
|
|
918
|
+
return { ok: false, error: 'Table identity is required.' }
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
const result = savePreset(props.tableId, {
|
|
922
|
+
name,
|
|
923
|
+
state,
|
|
924
|
+
setAsDefault,
|
|
925
|
+
})
|
|
926
|
+
if (result.ok) {
|
|
927
|
+
refreshSavedPresets()
|
|
928
|
+
if (result.preset?.id) {
|
|
929
|
+
activeLayoutPresetId.value = result.preset.id
|
|
930
|
+
}
|
|
931
|
+
updateActiveLayoutLabel()
|
|
932
|
+
}
|
|
933
|
+
return result
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
async function onLoadPreset(presetId) {
|
|
937
|
+
if (isRemote.value) {
|
|
938
|
+
if (!props.layoutPresets || typeof props.layoutPresets.load !== 'function') {
|
|
939
|
+
return { ok: false, error: 'Remote layout load is unavailable.' }
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
try {
|
|
943
|
+
const result = await props.layoutPresets.load(presetId)
|
|
944
|
+
if (!result?.ok || !result.preset?.state) {
|
|
945
|
+
return result ?? { ok: false, error: 'Preset not found.' }
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
applyPresetState(result.preset.state)
|
|
949
|
+
activeLayoutPresetId.value = presetId
|
|
950
|
+
updateActiveLayoutLabel()
|
|
951
|
+
return { ok: true }
|
|
952
|
+
} catch {
|
|
953
|
+
return { ok: false, error: 'Unable to load layout.' }
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
if (!props.tableId) {
|
|
958
|
+
return { ok: false, error: 'Table identity is required.' }
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
const preset = getPresetById(props.tableId, presetId)
|
|
962
|
+
if (!preset) {
|
|
963
|
+
return { ok: false, error: 'Preset not found.' }
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
applyPresetState(preset.state)
|
|
967
|
+
activeLayoutPresetId.value = presetId
|
|
968
|
+
updateActiveLayoutLabel()
|
|
969
|
+
return { ok: true }
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
async function onDeletePreset(presetId) {
|
|
973
|
+
if (isRemote.value) {
|
|
974
|
+
if (!props.layoutPresets || typeof props.layoutPresets.delete !== 'function') {
|
|
975
|
+
return { ok: false, error: 'Remote layout delete is unavailable.' }
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
try {
|
|
979
|
+
const result = await props.layoutPresets.delete(presetId)
|
|
980
|
+
if (result?.ok) {
|
|
981
|
+
await refreshSavedPresets()
|
|
982
|
+
if (activeLayoutPresetId.value === presetId) {
|
|
983
|
+
activeLayoutPresetId.value = null
|
|
984
|
+
}
|
|
985
|
+
updateActiveLayoutLabel()
|
|
986
|
+
}
|
|
987
|
+
return result ?? { ok: false, error: 'Unable to delete layout.' }
|
|
988
|
+
} catch {
|
|
989
|
+
return { ok: false, error: 'Unable to delete layout.' }
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
if (!props.tableId) {
|
|
994
|
+
return { ok: false, error: 'Table identity is required.' }
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
const result = deletePreset(props.tableId, presetId)
|
|
998
|
+
if (result.ok) {
|
|
999
|
+
refreshSavedPresets()
|
|
1000
|
+
if (activeLayoutPresetId.value === presetId) {
|
|
1001
|
+
activeLayoutPresetId.value = null
|
|
1002
|
+
}
|
|
1003
|
+
updateActiveLayoutLabel()
|
|
1004
|
+
}
|
|
1005
|
+
return result
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
async function onSetDefaultPreset(presetId) {
|
|
1009
|
+
if (isRemote.value) {
|
|
1010
|
+
if (!props.layoutPresets || typeof props.layoutPresets.setDefault !== 'function') {
|
|
1011
|
+
return { ok: false, error: 'Unable to update default layout.' }
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
try {
|
|
1015
|
+
const result = await props.layoutPresets.setDefault(presetId)
|
|
1016
|
+
if (result?.ok) {
|
|
1017
|
+
await refreshSavedPresets()
|
|
1018
|
+
}
|
|
1019
|
+
return result ?? { ok: false, error: 'Unable to update default layout.' }
|
|
1020
|
+
} catch {
|
|
1021
|
+
return { ok: false, error: 'Unable to update default layout.' }
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
if (!props.tableId) {
|
|
1026
|
+
return { ok: false, error: 'Table identity is required.' }
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
const result = setDefaultPreset(props.tableId, presetId)
|
|
1030
|
+
if (result.ok) {
|
|
1031
|
+
refreshSavedPresets()
|
|
1032
|
+
}
|
|
1033
|
+
return result
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
function getDeveloperDefaultColumnOrder() {
|
|
1037
|
+
return props.headers
|
|
1038
|
+
.filter((header) => header?.key && !header.hidden)
|
|
1039
|
+
.map((header) => header.key)
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
function getDeveloperDefaultVisibleKeys() {
|
|
1043
|
+
const keys = manageableColumnKeys.value
|
|
1044
|
+
if (props.defaultColumnVisibility !== undefined) {
|
|
1045
|
+
return keys.filter((key) => props.defaultColumnVisibility[key] !== false)
|
|
1046
|
+
}
|
|
1047
|
+
return [...keys]
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
function getDeveloperDefaultLayoutState() {
|
|
1051
|
+
return {
|
|
1052
|
+
columnOrder: getDeveloperDefaultColumnOrder(),
|
|
1053
|
+
columnVisibility: buildColumnVisibility(getDeveloperDefaultVisibleKeys()),
|
|
1054
|
+
columnPriorities: {},
|
|
1055
|
+
filters: {},
|
|
1056
|
+
sortBy: null,
|
|
1057
|
+
sortDir: 'asc',
|
|
1058
|
+
pageSize: DEFAULT_PAGE_SIZE,
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
function updateActiveLayoutLabel() {
|
|
1063
|
+
const current = getCurrentLayoutState()
|
|
1064
|
+
const defaults = getDeveloperDefaultLayoutState()
|
|
1065
|
+
|
|
1066
|
+
if (layoutStatesEqual(current, defaults, props.headers, props.data)) {
|
|
1067
|
+
activeLayoutPresetId.value = null
|
|
1068
|
+
return
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
if (activeLayoutPresetId.value && layoutPresetsAvailable.value) {
|
|
1072
|
+
const preset = findSavedPreset(activeLayoutPresetId.value)
|
|
1073
|
+
if (preset && layoutStatesEqual(current, preset.state, props.headers, props.data)) {
|
|
1074
|
+
return
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
if (layoutPresetsAvailable.value) {
|
|
1079
|
+
const matchingPreset = savedPresets.value.find((preset) =>
|
|
1080
|
+
layoutStatesEqual(current, preset.state, props.headers, props.data))
|
|
1081
|
+
if (matchingPreset) {
|
|
1082
|
+
activeLayoutPresetId.value = matchingPreset.id
|
|
1083
|
+
return
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
activeLayoutPresetId.value = null
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
const activeLayoutLabel = computed(() => {
|
|
1091
|
+
const current = getCurrentLayoutState()
|
|
1092
|
+
const defaults = getDeveloperDefaultLayoutState()
|
|
1093
|
+
|
|
1094
|
+
if (layoutStatesEqual(current, defaults, props.headers, props.data)) {
|
|
1095
|
+
return ''
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
if (activeLayoutPresetId.value && layoutPresetsAvailable.value) {
|
|
1099
|
+
const preset = findSavedPreset(activeLayoutPresetId.value)
|
|
1100
|
+
if (preset?.name) {
|
|
1101
|
+
return preset.name
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
return 'Custom'
|
|
1106
|
+
})
|
|
1107
|
+
|
|
1108
|
+
function loadDeveloperDefaults() {
|
|
1109
|
+
setFilters({})
|
|
1110
|
+
sortBy.value = null
|
|
1111
|
+
sortDir.value = 'asc'
|
|
1112
|
+
setColumnOrder(getDeveloperDefaultColumnOrder())
|
|
1113
|
+
setColumnPriorities({})
|
|
1114
|
+
|
|
1115
|
+
const visibleKeys = getDeveloperDefaultVisibleKeys()
|
|
1116
|
+
const fixedVisibleKeys = configurableHeaders.value
|
|
1117
|
+
.filter((header) => header.hideable === false)
|
|
1118
|
+
.map((header) => header.key)
|
|
1119
|
+
const nextVisibleKeys = visibleKeys.length > 0
|
|
1120
|
+
? visibleKeys
|
|
1121
|
+
: (fixedVisibleKeys.length > 0 ? fixedVisibleKeys : manageableColumnKeys.value.slice(0, 1))
|
|
1122
|
+
|
|
1123
|
+
setColumnVisibility(nextVisibleKeys)
|
|
1124
|
+
activeLayoutPresetId.value = null
|
|
1125
|
+
pageSize.value = DEFAULT_PAGE_SIZE
|
|
1126
|
+
page.value = 1
|
|
1127
|
+
if (isRemote.value) {
|
|
1128
|
+
scheduleFetchRows()
|
|
1129
|
+
}
|
|
1130
|
+
emitQueryChange()
|
|
1131
|
+
updateActiveLayoutLabel()
|
|
1132
|
+
return { ok: true }
|
|
318
1133
|
}
|
|
319
1134
|
|
|
1135
|
+
watch(() => props.columnOptions, (enabled) => {
|
|
1136
|
+
if (!enabled) {
|
|
1137
|
+
columnOptionsOpen.value = false
|
|
1138
|
+
}
|
|
1139
|
+
})
|
|
1140
|
+
|
|
320
1141
|
async function runFetchRows() {
|
|
321
1142
|
if (!isRemote.value) {
|
|
322
1143
|
return
|
|
@@ -421,13 +1242,106 @@ watch(
|
|
|
421
1242
|
},
|
|
422
1243
|
)
|
|
423
1244
|
|
|
1245
|
+
watch(
|
|
1246
|
+
() => props.columnVisibility,
|
|
1247
|
+
(nextVisibility) => {
|
|
1248
|
+
if (nextVisibility === undefined) {
|
|
1249
|
+
return
|
|
1250
|
+
}
|
|
1251
|
+
internalHiddenColumnKeys.value = configurableHeaders.value
|
|
1252
|
+
.filter((header) => nextVisibility[header.key] === false)
|
|
1253
|
+
.map((header) => header.key)
|
|
1254
|
+
},
|
|
1255
|
+
{ deep: true },
|
|
1256
|
+
)
|
|
1257
|
+
|
|
1258
|
+
watch(
|
|
1259
|
+
() => props.columnOrder,
|
|
1260
|
+
(nextOrder) => {
|
|
1261
|
+
if (nextOrder === undefined) {
|
|
1262
|
+
return
|
|
1263
|
+
}
|
|
1264
|
+
internalColumnOrder.value = [...nextOrder]
|
|
1265
|
+
},
|
|
1266
|
+
{ deep: true },
|
|
1267
|
+
)
|
|
1268
|
+
|
|
1269
|
+
watch(
|
|
1270
|
+
() => props.columnPriorities,
|
|
1271
|
+
(nextPriorities) => {
|
|
1272
|
+
if (nextPriorities === undefined) {
|
|
1273
|
+
return
|
|
1274
|
+
}
|
|
1275
|
+
internalColumnPriorities.value = { ...nextPriorities }
|
|
1276
|
+
},
|
|
1277
|
+
{ deep: true },
|
|
1278
|
+
)
|
|
1279
|
+
|
|
1280
|
+
watch(
|
|
1281
|
+
manageableColumnKeys,
|
|
1282
|
+
() => {
|
|
1283
|
+
syncInternalColumnOrder()
|
|
1284
|
+
},
|
|
1285
|
+
{ immediate: true },
|
|
1286
|
+
)
|
|
1287
|
+
|
|
1288
|
+
watch(
|
|
1289
|
+
() => [props.tableId, props.layoutPresets],
|
|
1290
|
+
() => {
|
|
1291
|
+
void refreshSavedPresets()
|
|
1292
|
+
updateActiveLayoutLabel()
|
|
1293
|
+
},
|
|
1294
|
+
{ immediate: true },
|
|
1295
|
+
)
|
|
1296
|
+
|
|
1297
|
+
watch(
|
|
1298
|
+
[
|
|
1299
|
+
() => resolvedColumnOrder.value,
|
|
1300
|
+
() => visibleColumnKeys.value,
|
|
1301
|
+
() => props.columnPriorities,
|
|
1302
|
+
internalColumnPriorities,
|
|
1303
|
+
activeFilters,
|
|
1304
|
+
sortBy,
|
|
1305
|
+
sortDir,
|
|
1306
|
+
pageSize,
|
|
1307
|
+
() => props.headers,
|
|
1308
|
+
savedPresets,
|
|
1309
|
+
],
|
|
1310
|
+
() => {
|
|
1311
|
+
updateActiveLayoutLabel()
|
|
1312
|
+
},
|
|
1313
|
+
{ deep: true },
|
|
1314
|
+
)
|
|
1315
|
+
|
|
424
1316
|
watch(sortedItems, () => {
|
|
425
1317
|
if (!isRemote.value) {
|
|
426
1318
|
page.value = 1
|
|
427
1319
|
}
|
|
428
1320
|
})
|
|
429
1321
|
|
|
430
|
-
onMounted(() => {
|
|
1322
|
+
onMounted(async () => {
|
|
1323
|
+
if (props.loadSavedLayout) {
|
|
1324
|
+
if (isRemote.value && props.layoutPresets && typeof props.layoutPresets.getDefault === 'function') {
|
|
1325
|
+
try {
|
|
1326
|
+
const result = await props.layoutPresets.getDefault()
|
|
1327
|
+
if (result?.preset?.state) {
|
|
1328
|
+
applyPresetState(result.preset.state)
|
|
1329
|
+
activeLayoutPresetId.value = result.preset.id
|
|
1330
|
+
}
|
|
1331
|
+
} catch {
|
|
1332
|
+
// Ignore default layout load failures in remote mode.
|
|
1333
|
+
}
|
|
1334
|
+
} else if (!isRemote.value && props.tableId) {
|
|
1335
|
+
const preset = getDefaultPreset(props.tableId)
|
|
1336
|
+
if (preset?.state) {
|
|
1337
|
+
applyPresetState(preset.state)
|
|
1338
|
+
activeLayoutPresetId.value = preset.id
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
await refreshSavedPresets()
|
|
1344
|
+
updateActiveLayoutLabel()
|
|
431
1345
|
emitQueryChange()
|
|
432
1346
|
if (isRemote.value) {
|
|
433
1347
|
runFetchRows()
|
|
@@ -458,6 +1372,11 @@ table thead th.filtered {
|
|
|
458
1372
|
color: #0366d6;
|
|
459
1373
|
}
|
|
460
1374
|
|
|
1375
|
+
table thead th.menu-open {
|
|
1376
|
+
background-color: var(--hover-background-color, rgba(3, 102, 214, 0.08));
|
|
1377
|
+
color: var(--text-color, inherit);
|
|
1378
|
+
}
|
|
1379
|
+
|
|
461
1380
|
table.loading tbody {
|
|
462
1381
|
opacity: 0.65;
|
|
463
1382
|
}
|
|
@@ -483,4 +1402,125 @@ td:first-child,
|
|
|
483
1402
|
th:first-child {
|
|
484
1403
|
padding-left: 1rem;
|
|
485
1404
|
}
|
|
1405
|
+
|
|
1406
|
+
tbody tr.row-clickable {
|
|
1407
|
+
cursor: pointer;
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1410
|
+
.table-empty-state {
|
|
1411
|
+
display: flex;
|
|
1412
|
+
align-items: center;
|
|
1413
|
+
justify-content: center;
|
|
1414
|
+
min-height: 6rem;
|
|
1415
|
+
padding: 2rem 1rem;
|
|
1416
|
+
text-align: center;
|
|
1417
|
+
color: var(--text-muted, #666);
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1420
|
+
.table-empty-state.loading {
|
|
1421
|
+
opacity: 0.65;
|
|
1422
|
+
}
|
|
1423
|
+
|
|
1424
|
+
.table-empty {
|
|
1425
|
+
display: flex;
|
|
1426
|
+
align-items: center;
|
|
1427
|
+
justify-content: center;
|
|
1428
|
+
width: 100%;
|
|
1429
|
+
}
|
|
1430
|
+
|
|
1431
|
+
.table-filtered-empty-state {
|
|
1432
|
+
display: flex;
|
|
1433
|
+
flex-direction: column;
|
|
1434
|
+
align-items: center;
|
|
1435
|
+
justify-content: center;
|
|
1436
|
+
gap: 0.75rem;
|
|
1437
|
+
padding: 2rem 1rem;
|
|
1438
|
+
text-align: center;
|
|
1439
|
+
color: var(--text-muted, #666);
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1442
|
+
.table-filtered-empty-message {
|
|
1443
|
+
margin: 0;
|
|
1444
|
+
}
|
|
1445
|
+
|
|
1446
|
+
th.table-column-options-header,
|
|
1447
|
+
td.table-column-options-cell {
|
|
1448
|
+
padding-right: 1rem;
|
|
1449
|
+
text-align: right;
|
|
1450
|
+
vertical-align: middle;
|
|
1451
|
+
}
|
|
1452
|
+
|
|
1453
|
+
th.table-column-options-header {
|
|
1454
|
+
width: auto;
|
|
1455
|
+
min-width: 2.5rem;
|
|
1456
|
+
max-width: none;
|
|
1457
|
+
white-space: nowrap;
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1460
|
+
td.table-column-options-cell {
|
|
1461
|
+
width: 2.5rem;
|
|
1462
|
+
min-width: 2.5rem;
|
|
1463
|
+
max-width: 2.5rem;
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1466
|
+
.table-column-options-header-content {
|
|
1467
|
+
display: inline-flex;
|
|
1468
|
+
align-items: center;
|
|
1469
|
+
justify-content: flex-end;
|
|
1470
|
+
gap: 0.5rem;
|
|
1471
|
+
width: 100%;
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1474
|
+
.table-layout-label {
|
|
1475
|
+
font-size: 0.85rem;
|
|
1476
|
+
color: var(--text-muted, #666);
|
|
1477
|
+
max-width: 10rem;
|
|
1478
|
+
overflow: hidden;
|
|
1479
|
+
text-overflow: ellipsis;
|
|
1480
|
+
white-space: nowrap;
|
|
1481
|
+
}
|
|
1482
|
+
|
|
1483
|
+
th.table-column-options-header .actions-menu-trigger {
|
|
1484
|
+
display: inline-flex;
|
|
1485
|
+
justify-content: flex-end;
|
|
1486
|
+
flex: 0 0 auto;
|
|
1487
|
+
}
|
|
1488
|
+
|
|
1489
|
+
.table-scroll th.table-column-options-header,
|
|
1490
|
+
.table-scroll td.table-column-options-cell {
|
|
1491
|
+
position: sticky;
|
|
1492
|
+
right: 0;
|
|
1493
|
+
z-index: 1;
|
|
1494
|
+
background-color: var(--table-sticky-bg);
|
|
1495
|
+
box-shadow: -1px 0 0 var(--border-color);
|
|
1496
|
+
}
|
|
1497
|
+
|
|
1498
|
+
.table-scroll thead th.table-column-options-header {
|
|
1499
|
+
z-index: 2;
|
|
1500
|
+
}
|
|
1501
|
+
|
|
1502
|
+
.table-scroll table.row-hover tbody tr:hover td.table-column-options-cell,
|
|
1503
|
+
.table-scroll table.row-hover tbody tr:focus-within td.table-column-options-cell {
|
|
1504
|
+
background-color: var(--table-sticky-hover-bg);
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1507
|
+
th.actions .actions-menu-trigger button {
|
|
1508
|
+
display: inline-flex;
|
|
1509
|
+
align-items: center;
|
|
1510
|
+
justify-content: center;
|
|
1511
|
+
font-weight: normal;
|
|
1512
|
+
padding: 0.2em 0.5em;
|
|
1513
|
+
line-height: 1;
|
|
1514
|
+
vertical-align: middle;
|
|
1515
|
+
background: transparent;
|
|
1516
|
+
border: 0;
|
|
1517
|
+
color: inherit;
|
|
1518
|
+
cursor: pointer;
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1521
|
+
th.actions .actions-menu-trigger button:hover,
|
|
1522
|
+
th.actions .actions-menu-trigger button:focus-visible {
|
|
1523
|
+
background-color: var(--hover-background-color);
|
|
1524
|
+
color: var(--text-color, inherit);
|
|
1525
|
+
}
|
|
486
1526
|
</style>
|