picocrank 1.24.0 → 1.25.1
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 +34 -2
- package/public/themes/index.json +1 -5
- package/styles/dark-theme.css +42 -186
- package/styles/picocrank-extensions.css +15 -0
- package/styles.css +1 -0
- package/supplemental-themes/README.md +9 -0
- package/supplemental-themes/catppuccin-latte-frappe/theme.css +211 -0
- package/supplemental-themes/dracula-alucard/theme.css +193 -0
- package/supplemental-themes/gruvbox-dark-light/theme.css +199 -0
- package/supplemental-themes/index.json +17 -0
- package/{public/themes → supplemental-themes}/waffles/theme.css +69 -46
- 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 +6 -11
- package/vue/components/SidebarNavLink.vue +6 -9
- package/vue/components/Table.vue +1115 -75
- package/vue/components/TableColumnFilterPopover.vue +263 -44
- package/vue/components/TableColumnOptionsPopover.vue +1116 -0
- package/vue/components/Tabs.vue +4 -25
- package/vue/components/ThemeSwitcher.vue +158 -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 +14 -1
- package/vue/examples/ExampleHeaderThemeSwitcher.vue +78 -0
- package/vue/examples/LoginExample.vue +1 -3
- package/vue/examples/PageStructure.vue +17 -0
- package/vue/examples/TableExample.vue +329 -11
- package/vue/examples/TableRemoteExample.vue +112 -4
- package/vue/examples/ThemePreview.vue +45 -38
- package/public/themes/catppuccin/theme.css +0 -195
- package/public/themes/dracula/theme.css +0 -181
package/vue/components/Tabs.vue
CHANGED
|
@@ -146,7 +146,7 @@ defineExpose({
|
|
|
146
146
|
border-radius: 0;
|
|
147
147
|
cursor: pointer;
|
|
148
148
|
font-size: 1em;
|
|
149
|
-
color: var(--
|
|
149
|
+
color: var(--tab-fg);
|
|
150
150
|
transition: all 0.2s ease;
|
|
151
151
|
white-space: nowrap;
|
|
152
152
|
position: relative;
|
|
@@ -158,13 +158,13 @@ defineExpose({
|
|
|
158
158
|
}
|
|
159
159
|
|
|
160
160
|
.tab-button.active {
|
|
161
|
-
color: var(--
|
|
162
|
-
border-bottom-color: var(--
|
|
161
|
+
color: var(--tab-active-fg);
|
|
162
|
+
border-bottom-color: var(--tab-active-fg);
|
|
163
163
|
font-weight: bold;
|
|
164
164
|
}
|
|
165
165
|
|
|
166
166
|
.tab-button:focus {
|
|
167
|
-
outline: 2px solid var(--
|
|
167
|
+
outline: 2px solid var(--tab-focus-outline-color);
|
|
168
168
|
outline-offset: -2px;
|
|
169
169
|
border-radius: 0;
|
|
170
170
|
}
|
|
@@ -196,27 +196,6 @@ defineExpose({
|
|
|
196
196
|
}
|
|
197
197
|
}
|
|
198
198
|
|
|
199
|
-
/* Dark theme support */
|
|
200
|
-
html[data-theme="dark"] {
|
|
201
|
-
.tabs-header {
|
|
202
|
-
border-bottom-color: var(--border-color, #444);
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
.tab-button {
|
|
206
|
-
color: var(--text-muted, #999);
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
.tab-button:hover {
|
|
210
|
-
color: var(--text-color, #fff);
|
|
211
|
-
background-color: var(--hover-background-color, #333);
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
.tab-button.active {
|
|
215
|
-
color: var(--primary-color, #60a5fa);
|
|
216
|
-
border-bottom-color: var(--primary-color, #60a5fa);
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
|
|
220
199
|
/* Responsive */
|
|
221
200
|
@media (max-width: 768px) {
|
|
222
201
|
.tabs-header {
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<label :class="['theme-switcher', { 'theme-switcher--compact': compact }]">
|
|
3
|
+
<span v-if="label" class="theme-switcher-label">{{ label }}</span>
|
|
4
|
+
<select
|
|
5
|
+
ref="selectRef"
|
|
6
|
+
:id="selectId || undefined"
|
|
7
|
+
:value="themePreference"
|
|
8
|
+
:aria-label="compact ? (label || 'Active theme') : undefined"
|
|
9
|
+
@change="onThemeChange"
|
|
10
|
+
>
|
|
11
|
+
<option value="">{{ defaultOptionLabel }}</option>
|
|
12
|
+
<option
|
|
13
|
+
v-for="name in availableThemes"
|
|
14
|
+
:key="name"
|
|
15
|
+
:value="name"
|
|
16
|
+
>
|
|
17
|
+
{{ themeLabels[name] || name }}
|
|
18
|
+
</option>
|
|
19
|
+
</select>
|
|
20
|
+
</label>
|
|
21
|
+
<p v-if="showEmptyHint && !compact && availableThemes.length === 0" class="subtle theme-switcher-hint">
|
|
22
|
+
<slot name="empty">
|
|
23
|
+
No drop-in themes found. Add <code>public/themes/<name>/theme.css</code>
|
|
24
|
+
or set <code>include-supplemental-themes</code> to list bundled supplemental themes.
|
|
25
|
+
</slot>
|
|
26
|
+
</p>
|
|
27
|
+
</template>
|
|
28
|
+
|
|
29
|
+
<script>
|
|
30
|
+
export const HEADER_THEME_SWITCHER_SELECT_ID = 'picocrank-header-theme-switcher'
|
|
31
|
+
</script>
|
|
32
|
+
|
|
33
|
+
<script setup>
|
|
34
|
+
import { onMounted, ref, watch } from 'vue'
|
|
35
|
+
import { useCustomTheme } from '../composables/useCustomTheme.js'
|
|
36
|
+
|
|
37
|
+
const props = defineProps({
|
|
38
|
+
label: {
|
|
39
|
+
type: String,
|
|
40
|
+
default: 'Active theme',
|
|
41
|
+
},
|
|
42
|
+
defaultOptionLabel: {
|
|
43
|
+
type: String,
|
|
44
|
+
default: 'Default (Femtocrank only)',
|
|
45
|
+
},
|
|
46
|
+
includeSupplementalThemes: {
|
|
47
|
+
type: Boolean,
|
|
48
|
+
default: false,
|
|
49
|
+
},
|
|
50
|
+
themesBasePath: {
|
|
51
|
+
type: String,
|
|
52
|
+
default: undefined,
|
|
53
|
+
},
|
|
54
|
+
supplementalThemesBasePath: {
|
|
55
|
+
type: String,
|
|
56
|
+
default: undefined,
|
|
57
|
+
},
|
|
58
|
+
storageKey: {
|
|
59
|
+
type: String,
|
|
60
|
+
default: undefined,
|
|
61
|
+
},
|
|
62
|
+
showEmptyHint: {
|
|
63
|
+
type: Boolean,
|
|
64
|
+
default: false,
|
|
65
|
+
},
|
|
66
|
+
compact: {
|
|
67
|
+
type: Boolean,
|
|
68
|
+
default: false,
|
|
69
|
+
},
|
|
70
|
+
selectId: {
|
|
71
|
+
type: String,
|
|
72
|
+
default: undefined,
|
|
73
|
+
},
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
const emit = defineEmits(['change'])
|
|
77
|
+
|
|
78
|
+
const {
|
|
79
|
+
availableThemes,
|
|
80
|
+
themeLabels,
|
|
81
|
+
themePreference,
|
|
82
|
+
setTheme,
|
|
83
|
+
discoverThemes,
|
|
84
|
+
configure,
|
|
85
|
+
} = useCustomTheme()
|
|
86
|
+
|
|
87
|
+
const selectRef = ref(null)
|
|
88
|
+
|
|
89
|
+
function focusSelect() {
|
|
90
|
+
selectRef.value?.focus()
|
|
91
|
+
selectRef.value?.scrollIntoView({ block: 'nearest', inline: 'nearest' })
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
defineExpose({
|
|
95
|
+
focus: focusSelect,
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
function syncOptions() {
|
|
99
|
+
configure({
|
|
100
|
+
includeSupplementalThemes: props.includeSupplementalThemes,
|
|
101
|
+
themesBasePath: props.themesBasePath,
|
|
102
|
+
supplementalThemesBasePath: props.supplementalThemesBasePath,
|
|
103
|
+
storageKey: props.storageKey,
|
|
104
|
+
})
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function onThemeChange(event) {
|
|
108
|
+
const value = event.target.value
|
|
109
|
+
setTheme(value)
|
|
110
|
+
emit('change', value)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
watch(
|
|
114
|
+
() => [
|
|
115
|
+
props.includeSupplementalThemes,
|
|
116
|
+
props.themesBasePath,
|
|
117
|
+
props.supplementalThemesBasePath,
|
|
118
|
+
props.storageKey,
|
|
119
|
+
],
|
|
120
|
+
() => {
|
|
121
|
+
syncOptions()
|
|
122
|
+
void discoverThemes()
|
|
123
|
+
},
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
onMounted(() => {
|
|
127
|
+
syncOptions()
|
|
128
|
+
void discoverThemes()
|
|
129
|
+
})
|
|
130
|
+
</script>
|
|
131
|
+
|
|
132
|
+
<style scoped>
|
|
133
|
+
.theme-switcher {
|
|
134
|
+
display: flex;
|
|
135
|
+
flex-wrap: wrap;
|
|
136
|
+
align-items: center;
|
|
137
|
+
gap: 0.75rem;
|
|
138
|
+
margin: 0;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
.theme-switcher select {
|
|
142
|
+
min-width: 14rem;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
.theme-switcher--compact {
|
|
146
|
+
flex-wrap: nowrap;
|
|
147
|
+
gap: 0;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
.theme-switcher--compact select {
|
|
151
|
+
min-width: 10rem;
|
|
152
|
+
max-width: 13.5rem;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
.theme-switcher-hint {
|
|
156
|
+
margin: 0.75rem 0 0;
|
|
157
|
+
}
|
|
158
|
+
</style>
|
|
@@ -97,7 +97,7 @@ function handleLinkClick(callback = null) {
|
|
|
97
97
|
padding: 0.5em 0.75em;
|
|
98
98
|
border-radius: 0.25em;
|
|
99
99
|
text-decoration: none;
|
|
100
|
-
color: inherit;
|
|
100
|
+
color: var(--header-fg-color, var(--header-text-color, inherit));
|
|
101
101
|
transition: background-color 0.15s ease;
|
|
102
102
|
white-space: nowrap;
|
|
103
103
|
border: 0;
|
|
@@ -124,7 +124,7 @@ function handleLinkClick(callback = null) {
|
|
|
124
124
|
|
|
125
125
|
html[data-theme="dark"] {
|
|
126
126
|
.topbar-link {
|
|
127
|
-
color: #f8f9fa;
|
|
127
|
+
color: var(--header-fg-color, var(--header-text-color, #f8f9fa));
|
|
128
128
|
}
|
|
129
129
|
|
|
130
130
|
.separator {
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
import {
|
|
2
|
+
normalizeColumnFilterEntries,
|
|
3
|
+
normalizeStoredFilterEntry,
|
|
4
|
+
} from './tableFilters.js'
|
|
5
|
+
|
|
6
|
+
export const PRESET_STORE_VERSION = 1
|
|
7
|
+
export const MAX_PRESETS_PER_TABLE = 20
|
|
8
|
+
export const ALLOWED_PAGE_SIZES = [10, 25, 50, 100]
|
|
9
|
+
|
|
10
|
+
let presetIdCounter = 0
|
|
11
|
+
|
|
12
|
+
export function createPresetId() {
|
|
13
|
+
if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
|
|
14
|
+
return `preset-${crypto.randomUUID()}`
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
presetIdCounter += 1
|
|
18
|
+
return `preset-${presetIdCounter}-${Date.now()}`
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function getPresetStorageKey(tableId) {
|
|
22
|
+
const origin = typeof window !== 'undefined' ? window.location.origin : 'ssr'
|
|
23
|
+
return `picocrank-table-layouts:v${PRESET_STORE_VERSION}:${origin}:${tableId}`
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function createEmptyStore() {
|
|
27
|
+
return {
|
|
28
|
+
version: PRESET_STORE_VERSION,
|
|
29
|
+
defaultPresetId: null,
|
|
30
|
+
presets: [],
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function normalizeStore(raw) {
|
|
35
|
+
if (!raw || typeof raw !== 'object') {
|
|
36
|
+
return createEmptyStore()
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const presets = Array.isArray(raw.presets)
|
|
40
|
+
? raw.presets.filter((preset) => preset?.id && preset?.name && preset?.state)
|
|
41
|
+
: []
|
|
42
|
+
|
|
43
|
+
const defaultPresetId = typeof raw.defaultPresetId === 'string'
|
|
44
|
+
&& presets.some((preset) => preset.id === raw.defaultPresetId)
|
|
45
|
+
? raw.defaultPresetId
|
|
46
|
+
: null
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
version: PRESET_STORE_VERSION,
|
|
50
|
+
defaultPresetId,
|
|
51
|
+
presets,
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function readPresetStore(tableId) {
|
|
56
|
+
if (!tableId || typeof localStorage === 'undefined') {
|
|
57
|
+
return createEmptyStore()
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
const raw = localStorage.getItem(getPresetStorageKey(tableId))
|
|
62
|
+
if (!raw) {
|
|
63
|
+
return createEmptyStore()
|
|
64
|
+
}
|
|
65
|
+
return normalizeStore(JSON.parse(raw))
|
|
66
|
+
} catch {
|
|
67
|
+
return createEmptyStore()
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function writePresetStore(tableId, store) {
|
|
72
|
+
if (!tableId || typeof localStorage === 'undefined') {
|
|
73
|
+
return false
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
localStorage.setItem(getPresetStorageKey(tableId), JSON.stringify(normalizeStore(store)))
|
|
77
|
+
return true
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function listPresets(tableId) {
|
|
81
|
+
const store = readPresetStore(tableId)
|
|
82
|
+
return {
|
|
83
|
+
presets: store.presets.map((preset) => ({ ...preset })),
|
|
84
|
+
defaultPresetId: store.defaultPresetId,
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function getDefaultPreset(tableId) {
|
|
89
|
+
const store = readPresetStore(tableId)
|
|
90
|
+
if (!store.defaultPresetId) {
|
|
91
|
+
return null
|
|
92
|
+
}
|
|
93
|
+
return store.presets.find((preset) => preset.id === store.defaultPresetId) ?? null
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function isValidColPriority(priority) {
|
|
97
|
+
return Number.isInteger(priority) && priority >= 1 && priority <= 5
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function headerMaps(headers = []) {
|
|
101
|
+
const headerByKey = new Map()
|
|
102
|
+
const manageableKeys = []
|
|
103
|
+
const configurableKeys = []
|
|
104
|
+
|
|
105
|
+
for (const header of headers) {
|
|
106
|
+
if (!header?.key || header.hidden) {
|
|
107
|
+
continue
|
|
108
|
+
}
|
|
109
|
+
manageableKeys.push(header.key)
|
|
110
|
+
headerByKey.set(header.key, header)
|
|
111
|
+
if (header.hideable !== false) {
|
|
112
|
+
configurableKeys.push(header.key)
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return { headerByKey, manageableKeys, configurableKeys }
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function sanitizePresetState(state, headers = [], rows = []) {
|
|
120
|
+
if (!state || typeof state !== 'object') {
|
|
121
|
+
return null
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const { headerByKey, manageableKeys, configurableKeys } = headerMaps(headers)
|
|
125
|
+
const sanitized = {}
|
|
126
|
+
|
|
127
|
+
if (Array.isArray(state.columnOrder)) {
|
|
128
|
+
const fromOrder = state.columnOrder.filter((key) => manageableKeys.includes(key))
|
|
129
|
+
const missing = manageableKeys.filter((key) => !fromOrder.includes(key))
|
|
130
|
+
sanitized.columnOrder = [...fromOrder, ...missing]
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (state.columnVisibility && typeof state.columnVisibility === 'object') {
|
|
134
|
+
sanitized.columnVisibility = Object.fromEntries(
|
|
135
|
+
configurableKeys.map((key) => [
|
|
136
|
+
key,
|
|
137
|
+
state.columnVisibility[key] !== false,
|
|
138
|
+
]),
|
|
139
|
+
)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (state.columnPriorities && typeof state.columnPriorities === 'object') {
|
|
143
|
+
const priorities = {}
|
|
144
|
+
for (const [key, value] of Object.entries(state.columnPriorities)) {
|
|
145
|
+
if (!manageableKeys.includes(key)) {
|
|
146
|
+
continue
|
|
147
|
+
}
|
|
148
|
+
if (value === null || value === '') {
|
|
149
|
+
priorities[key] = null
|
|
150
|
+
continue
|
|
151
|
+
}
|
|
152
|
+
if (isValidColPriority(value)) {
|
|
153
|
+
priorities[key] = value
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
sanitized.columnPriorities = priorities
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (state.filters && typeof state.filters === 'object') {
|
|
160
|
+
const filters = {}
|
|
161
|
+
for (const [key, columnFilters] of Object.entries(state.filters)) {
|
|
162
|
+
const header = headerByKey.get(key)
|
|
163
|
+
if (!header || header.filterable === false) {
|
|
164
|
+
continue
|
|
165
|
+
}
|
|
166
|
+
const entries = normalizeColumnFilterEntries(columnFilters, header, rows)
|
|
167
|
+
.map((entry) => normalizeStoredFilterEntry(header, entry, rows))
|
|
168
|
+
.filter(Boolean)
|
|
169
|
+
if (entries.length > 0) {
|
|
170
|
+
filters[key] = entries
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
sanitized.filters = filters
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (state.sortBy) {
|
|
177
|
+
const header = headerByKey.get(state.sortBy)
|
|
178
|
+
if (header?.sortable) {
|
|
179
|
+
sanitized.sortBy = state.sortBy
|
|
180
|
+
sanitized.sortDir = state.sortDir === 'desc' ? 'desc' : 'asc'
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const rawPageSize = Number(state.pageSize)
|
|
185
|
+
if (Number.isFinite(rawPageSize)) {
|
|
186
|
+
const normalizedPageSize = Math.floor(rawPageSize)
|
|
187
|
+
if (ALLOWED_PAGE_SIZES.includes(normalizedPageSize)) {
|
|
188
|
+
sanitized.pageSize = normalizedPageSize
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return sanitized
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export function savePreset(tableId, { name, state, setAsDefault = false }) {
|
|
196
|
+
const trimmedName = String(name ?? '').trim()
|
|
197
|
+
if (!tableId) {
|
|
198
|
+
return { ok: false, error: 'Table identity is required.' }
|
|
199
|
+
}
|
|
200
|
+
if (!trimmedName) {
|
|
201
|
+
return { ok: false, error: 'Preset name is required.' }
|
|
202
|
+
}
|
|
203
|
+
if (!state || typeof state !== 'object') {
|
|
204
|
+
return { ok: false, error: 'Nothing to save.' }
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const store = readPresetStore(tableId)
|
|
208
|
+
if (store.presets.length >= MAX_PRESETS_PER_TABLE) {
|
|
209
|
+
return { ok: false, error: `Maximum of ${MAX_PRESETS_PER_TABLE} saved layouts reached.` }
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const preset = {
|
|
213
|
+
id: createPresetId(),
|
|
214
|
+
name: trimmedName,
|
|
215
|
+
savedAt: new Date().toISOString(),
|
|
216
|
+
state: JSON.parse(JSON.stringify(state)),
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
store.presets.push(preset)
|
|
220
|
+
if (setAsDefault) {
|
|
221
|
+
store.defaultPresetId = preset.id
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
writePresetStore(tableId, store)
|
|
225
|
+
return { ok: true, preset, defaultPresetId: store.defaultPresetId }
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export function deletePreset(tableId, presetId) {
|
|
229
|
+
if (!tableId || !presetId) {
|
|
230
|
+
return { ok: false, error: 'Preset not found.' }
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const store = readPresetStore(tableId)
|
|
234
|
+
const nextPresets = store.presets.filter((preset) => preset.id !== presetId)
|
|
235
|
+
if (nextPresets.length === store.presets.length) {
|
|
236
|
+
return { ok: false, error: 'Preset not found.' }
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
store.presets = nextPresets
|
|
240
|
+
if (store.defaultPresetId === presetId) {
|
|
241
|
+
store.defaultPresetId = null
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
writePresetStore(tableId, store)
|
|
245
|
+
return { ok: true, defaultPresetId: store.defaultPresetId }
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export function setDefaultPreset(tableId, presetId) {
|
|
249
|
+
if (!tableId) {
|
|
250
|
+
return { ok: false, error: 'Table identity is required.' }
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const store = readPresetStore(tableId)
|
|
254
|
+
if (presetId === null) {
|
|
255
|
+
store.defaultPresetId = null
|
|
256
|
+
writePresetStore(tableId, store)
|
|
257
|
+
return { ok: true, defaultPresetId: null }
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const preset = store.presets.find((item) => item.id === presetId)
|
|
261
|
+
if (!preset) {
|
|
262
|
+
return { ok: false, error: 'Preset not found.' }
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
store.defaultPresetId = presetId
|
|
266
|
+
writePresetStore(tableId, store)
|
|
267
|
+
return { ok: true, defaultPresetId: presetId }
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
export function getPresetById(tableId, presetId) {
|
|
271
|
+
const store = readPresetStore(tableId)
|
|
272
|
+
return store.presets.find((preset) => preset.id === presetId) ?? null
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
export function cloneLayoutState(state) {
|
|
276
|
+
return JSON.parse(JSON.stringify(state ?? {}))
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
export function normalizeLayoutStateForCompare(state, headers = [], rows = []) {
|
|
280
|
+
const sanitized = sanitizePresetState(state, headers, rows) ?? {}
|
|
281
|
+
|
|
282
|
+
return {
|
|
283
|
+
columnOrder: sanitized.columnOrder ?? [],
|
|
284
|
+
columnVisibility: sanitized.columnVisibility ?? {},
|
|
285
|
+
columnPriorities: sanitized.columnPriorities ?? {},
|
|
286
|
+
filters: sanitized.filters ?? {},
|
|
287
|
+
sortBy: sanitized.sortBy ?? null,
|
|
288
|
+
sortDir: sanitized.sortDir === 'desc' ? 'desc' : 'asc',
|
|
289
|
+
pageSize: sanitized.pageSize ?? ALLOWED_PAGE_SIZES[0],
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
export function layoutStatesEqual(left, right, headers = [], rows = []) {
|
|
294
|
+
return JSON.stringify(normalizeLayoutStateForCompare(left, headers, rows))
|
|
295
|
+
=== JSON.stringify(normalizeLayoutStateForCompare(right, headers, rows))
|
|
296
|
+
}
|