@quazardous/qdadm 2.18.0 → 2.20.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/package.json +5 -7
- package/src/components/editors/VanillaJsonEditor.vue +14 -4
- package/src/components/lists/ListPage.vue +7 -0
- package/src/composables/index.ts +3 -0
- package/src/composables/useLayoutResolver.ts +10 -0
- package/src/composables/useListPage.filters.ts +137 -67
- package/src/composables/useListPage.ts +148 -26
- package/src/composables/useListPage.types.ts +74 -5
- package/src/composables/useListPage.utils.ts +58 -0
- package/src/entity/EntityManager.interface.ts +11 -0
- package/src/entity/EntityManager.ts +5 -0
- package/src/entity/EntityManager.types.ts +11 -0
- package/src/entity/storage/ApiStorage.ts +16 -2
- package/src/entity/storage/LocalStorage.ts +1 -1
- package/src/entity/storage/MemoryStorage.ts +7 -2
- package/src/entity/storage/MockApiStorage.ts +1 -1
- package/src/entity/storage/SdkStorage.ts +12 -3
- package/src/gen/createManagers.ts +15 -1
- package/src/gen/index.ts +7 -1
- package/src/index.ts +19 -0
- package/src/kernel/Kernel.registries.ts +202 -46
- package/src/kernel/Kernel.ts +34 -9
- package/src/kernel/Kernel.types.ts +19 -0
- package/src/kernel/Kernel.vue.ts +7 -0
- package/src/kernel/KernelContext.routing.ts +18 -7
- package/src/plugin.ts +1 -1
- package/src/query/clientFilter.ts +14 -3
- package/src/routeState/CompositePersister.ts +144 -0
- package/src/routeState/CookiePersister.ts +109 -0
- package/src/routeState/MemoryPersister.ts +60 -0
- package/src/routeState/NullPersister.ts +33 -0
- package/src/routeState/RouteStatePersister.ts +86 -0
- package/src/routeState/UrlPersister.ts +207 -0
- package/src/routeState/WebStoragePersister.ts +149 -0
- package/src/routeState/defaultPersister.ts +66 -0
- package/src/routeState/factory.ts +86 -0
- package/src/routeState/index.ts +32 -0
- package/src/routeState/keyLocator.ts +84 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@quazardous/qdadm",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.20.0",
|
|
4
4
|
"description": "Vue 3 framework for admin dashboards with PrimeVue",
|
|
5
5
|
"author": "quazardous",
|
|
6
6
|
"license": "MIT",
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
"test": "vitest run",
|
|
20
20
|
"test:watch": "vitest",
|
|
21
21
|
"gen:i18n-defaults": "node scripts/gen-i18n-defaults.mjs",
|
|
22
|
-
"type-check": "vue-tsc --noEmit",
|
|
22
|
+
"type-check": "vue-tsc --noEmit && vue-tsc --noEmit -p tsconfig.typetest.json",
|
|
23
23
|
"lint": "eslint src/",
|
|
24
24
|
"lint:fix": "eslint src/ --fix",
|
|
25
25
|
"format": "prettier --write src/",
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"dist/node"
|
|
55
55
|
],
|
|
56
56
|
"dependencies": {
|
|
57
|
-
"@quazardous/qdcore": "^1.
|
|
57
|
+
"@quazardous/qdcore": "^1.2.0",
|
|
58
58
|
"@quazardous/qddebug": "^1.2.0",
|
|
59
59
|
"@quazardous/quarkernel": "^2.1.0",
|
|
60
60
|
"yaml": "^2.8.4"
|
|
@@ -63,6 +63,7 @@
|
|
|
63
63
|
"pinia": "^2.0.0 || ^3.0.0 || ^4.0.0",
|
|
64
64
|
"primevue": "^4.0.0",
|
|
65
65
|
"sass-embedded": "^1.0.0",
|
|
66
|
+
"vanilla-jsoneditor": "^3.13.0",
|
|
66
67
|
"vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0",
|
|
67
68
|
"vue": "^3.3.0",
|
|
68
69
|
"vue-router": "^4.0.0 || ^5.0.0"
|
|
@@ -78,12 +79,9 @@
|
|
|
78
79
|
"optional": true
|
|
79
80
|
},
|
|
80
81
|
"sass-embedded": {
|
|
81
|
-
"optional":
|
|
82
|
+
"optional": true
|
|
82
83
|
}
|
|
83
84
|
},
|
|
84
|
-
"optionalDependencies": {
|
|
85
|
-
"vanilla-jsoneditor": "^0.23.0"
|
|
86
|
-
},
|
|
87
85
|
"keywords": [
|
|
88
86
|
"vue",
|
|
89
87
|
"vue3",
|
|
@@ -15,7 +15,17 @@
|
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
import { ref, watch, onMounted, onUnmounted, nextTick } from 'vue'
|
|
18
|
-
|
|
18
|
+
// vanilla-jsoneditor 3.x (#1901): the `new JSONEditor(...)` constructor is
|
|
19
|
+
// deprecated — `JSONEditor` is now the Svelte component itself, and the
|
|
20
|
+
// instance type is exported as `JsonEditor`.
|
|
21
|
+
import {
|
|
22
|
+
createJSONEditor,
|
|
23
|
+
type JsonEditor,
|
|
24
|
+
type Content,
|
|
25
|
+
type ContentErrors,
|
|
26
|
+
type OnChangeStatus,
|
|
27
|
+
type Mode,
|
|
28
|
+
} from 'vanilla-jsoneditor'
|
|
19
29
|
import { buildJsonValidator, type Validator, type JSONSchema, type JsonValidatorOptions } from './jsonValidator'
|
|
20
30
|
|
|
21
31
|
type JsonValue = Record<string, unknown> | unknown[] | string | null
|
|
@@ -74,8 +84,8 @@ const emit = defineEmits<{
|
|
|
74
84
|
'error': [errors: ContentErrors]
|
|
75
85
|
}>()
|
|
76
86
|
|
|
77
|
-
const containerRef = ref<
|
|
78
|
-
let editor:
|
|
87
|
+
const containerRef = ref<HTMLDivElement | null>(null)
|
|
88
|
+
let editor: JsonEditor | null = null
|
|
79
89
|
// Flag to prevent onChange from firing during programmatic updates (instance-specific)
|
|
80
90
|
const updatingFromProp = ref<boolean>(false)
|
|
81
91
|
|
|
@@ -102,7 +112,7 @@ onMounted(() => {
|
|
|
102
112
|
json: parseValue(props.modelValue)
|
|
103
113
|
}
|
|
104
114
|
|
|
105
|
-
editor =
|
|
115
|
+
editor = createJSONEditor({
|
|
106
116
|
target: containerRef.value,
|
|
107
117
|
props: {
|
|
108
118
|
content,
|
|
@@ -104,6 +104,12 @@ const props = defineProps({
|
|
|
104
104
|
rows: { type: Number, default: 10 },
|
|
105
105
|
rowsPerPageOptions: { type: Array as PropType<number[]>, default: () => [10, 50, 100] },
|
|
106
106
|
totalRecords: { type: Number, default: 0 },
|
|
107
|
+
/**
|
|
108
|
+
* Row offset of the page being shown — what positions the paginator (#2145).
|
|
109
|
+
* Required in lazy mode: the table is handed one page of rows and cannot
|
|
110
|
+
* infer which page they are.
|
|
111
|
+
*/
|
|
112
|
+
first: { type: Number, default: 0 },
|
|
107
113
|
lazy: { type: Boolean, default: false },
|
|
108
114
|
|
|
109
115
|
// Sorting
|
|
@@ -438,6 +444,7 @@ function getFilterClass(filter: FilterConfig): Record<string, boolean> {
|
|
|
438
444
|
:rows="rows"
|
|
439
445
|
:rowsPerPageOptions="rowsPerPageOptions"
|
|
440
446
|
:totalRecords="totalRecords"
|
|
447
|
+
:first="first"
|
|
441
448
|
:lazy="lazy"
|
|
442
449
|
:sortField="sortField ?? undefined"
|
|
443
450
|
:sortOrder="sortOrder"
|
package/src/composables/index.ts
CHANGED
|
@@ -30,10 +30,13 @@ export * from './useJsonSyntax'
|
|
|
30
30
|
export {
|
|
31
31
|
useListPage,
|
|
32
32
|
PAGE_SIZE_OPTIONS,
|
|
33
|
+
FILTER_TYPES,
|
|
33
34
|
type UseListPageOptions,
|
|
34
35
|
type UseListPageReturn,
|
|
35
36
|
type ColumnConfig,
|
|
36
37
|
type FilterConfig,
|
|
38
|
+
type FilterOptions,
|
|
39
|
+
type FilterType,
|
|
37
40
|
type ActionConfig as ListActionConfig,
|
|
38
41
|
type ResolvedAction,
|
|
39
42
|
type HeaderActionConfig,
|
|
@@ -34,6 +34,10 @@ import { useRoute } from 'vue-router'
|
|
|
34
34
|
export const LAYOUT_TYPES = {
|
|
35
35
|
LIST: 'list',
|
|
36
36
|
FORM: 'form',
|
|
37
|
+
// `show` was emitted by crud({ show }) long before it existed here (#1922).
|
|
38
|
+
// The resolver returned it, found no component under that key, and fell
|
|
39
|
+
// back to `base` — gracefully, which is exactly what made it invisible.
|
|
40
|
+
SHOW: 'show',
|
|
37
41
|
DASHBOARD: 'dashboard',
|
|
38
42
|
BASE: 'base',
|
|
39
43
|
} as const
|
|
@@ -46,6 +50,7 @@ export type LayoutType = (typeof LAYOUT_TYPES)[keyof typeof LAYOUT_TYPES]
|
|
|
46
50
|
export interface LayoutComponentsMap {
|
|
47
51
|
list?: Component | null
|
|
48
52
|
form?: Component | null
|
|
53
|
+
show?: Component | null
|
|
49
54
|
dashboard?: Component | null
|
|
50
55
|
base?: Component | null
|
|
51
56
|
[key: string]: Component | null | undefined
|
|
@@ -63,6 +68,7 @@ interface PageNamePattern {
|
|
|
63
68
|
const PAGE_NAME_PATTERNS: PageNamePattern[] = [
|
|
64
69
|
{ pattern: /List(Page)?$/i, layout: LAYOUT_TYPES.LIST },
|
|
65
70
|
{ pattern: /(Edit|Create|Form)(Page)?$/i, layout: LAYOUT_TYPES.FORM },
|
|
71
|
+
{ pattern: /Show(Page)?$/i, layout: LAYOUT_TYPES.SHOW },
|
|
66
72
|
{ pattern: /Dashboard(Page)?$/i, layout: LAYOUT_TYPES.DASHBOARD },
|
|
67
73
|
]
|
|
68
74
|
|
|
@@ -70,6 +76,10 @@ const PAGE_NAME_PATTERNS: PageNamePattern[] = [
|
|
|
70
76
|
const ROUTE_NAME_PATTERNS: PageNamePattern[] = [
|
|
71
77
|
{ pattern: /-list$/i, layout: LAYOUT_TYPES.LIST },
|
|
72
78
|
{ pattern: /-(edit|create)$/i, layout: LAYOUT_TYPES.FORM },
|
|
79
|
+
// Load-bearing, not decoration: an app that declares its detail route by
|
|
80
|
+
// hand rather than through crud({ show }) has no meta.layout to go on, and
|
|
81
|
+
// this is the only path that can reach `show` for it (#1922).
|
|
82
|
+
{ pattern: /-show$/i, layout: LAYOUT_TYPES.SHOW },
|
|
73
83
|
{ pattern: /-dashboard$/i, layout: LAYOUT_TYPES.DASHBOARD },
|
|
74
84
|
// Common patterns without suffix
|
|
75
85
|
{ pattern: /^dashboard$/i, layout: LAYOUT_TYPES.DASHBOARD },
|
|
@@ -7,9 +7,10 @@
|
|
|
7
7
|
* useListPage composes it back in; behavior is unchanged.
|
|
8
8
|
*/
|
|
9
9
|
import { ref, computed, type Ref, type ComputedRef } from 'vue'
|
|
10
|
-
import type { RouteLocationNormalizedLoaded, Router } from 'vue-router'
|
|
11
10
|
import { FilterQuery, type QueryOrchestratorLike } from '../query/FilterQuery'
|
|
12
|
-
import
|
|
11
|
+
import { FILTER_TYPES } from './useListPage.types'
|
|
12
|
+
import type { FilterConfig, FilterOptions, SearchConfig } from './useListPage.types'
|
|
13
|
+
import type { RouteStatePersister } from '../routeState'
|
|
13
14
|
import {
|
|
14
15
|
SMART_FILTER_THRESHOLD,
|
|
15
16
|
clearSessionFilters,
|
|
@@ -29,14 +30,38 @@ export interface UseListFiltersDeps {
|
|
|
29
30
|
items: Ref<unknown[]>
|
|
30
31
|
/** Current page ref — filter changes reset it to 1. */
|
|
31
32
|
page: Ref<number>
|
|
33
|
+
/** Rows per page — persisted alongside the rest (#2146). */
|
|
34
|
+
pageSize: Ref<number>
|
|
35
|
+
/** Active sort field, persisted alongside the rest (#2146). */
|
|
36
|
+
sortField: Ref<string | null>
|
|
37
|
+
/** Active sort direction. */
|
|
38
|
+
sortOrder: Ref<number>
|
|
39
|
+
/**
|
|
40
|
+
* Whether the sort is remembered at all.
|
|
41
|
+
*
|
|
42
|
+
* Its own flag rather than folded into `routeStateWrites`, because it has
|
|
43
|
+
* always been separately switchable and a seam refactor must not quietly
|
|
44
|
+
* take an option away.
|
|
45
|
+
*/
|
|
46
|
+
persistSort: boolean
|
|
32
47
|
/** Search query ref (shared with the search subsystem). */
|
|
33
48
|
searchQuery: Ref<string>
|
|
34
|
-
route: RouteLocationNormalizedLoaded
|
|
35
|
-
router: Router
|
|
36
49
|
/** Session-restored filter values (already stripped of _search). */
|
|
37
50
|
savedFilters: Record<string, unknown> | null
|
|
38
51
|
persistFilters: boolean
|
|
39
|
-
|
|
52
|
+
/** Where this list's state is remembered (#2146). */
|
|
53
|
+
persister: RouteStatePersister | null
|
|
54
|
+
/** This list's namespace, so two lists on one route stop colliding. */
|
|
55
|
+
routeStateScope: string
|
|
56
|
+
/**
|
|
57
|
+
* Whether this list may WRITE its state back.
|
|
58
|
+
*
|
|
59
|
+
* Separate from having a persister because reading and writing were always
|
|
60
|
+
* separate here: `syncUrlParams: false` stopped the query string being
|
|
61
|
+
* written and never stopped it being read, so a hand-typed deep link still
|
|
62
|
+
* restored. Collapsing the two would have changed that quietly.
|
|
63
|
+
*/
|
|
64
|
+
routeStateWrites: boolean
|
|
40
65
|
autoLoadFilters: boolean
|
|
41
66
|
filterSessionKey: string
|
|
42
67
|
/** Entity filters registry (injected by the consuming app). */
|
|
@@ -51,7 +76,7 @@ export interface UseListFiltersReturn {
|
|
|
51
76
|
filterValues: Ref<Record<string, unknown>>
|
|
52
77
|
filters: ComputedRef<FilterConfig[]>
|
|
53
78
|
hasActiveFilters: ComputedRef<boolean>
|
|
54
|
-
addFilter: (name: string, filterConfig:
|
|
79
|
+
addFilter: (name: string, filterConfig: FilterOptions) => void
|
|
55
80
|
removeFilter: (name: string) => void
|
|
56
81
|
setFilterValue: (name: string, value: unknown) => void
|
|
57
82
|
updateFilters: (newValues: Record<string, unknown>) => void
|
|
@@ -74,11 +99,15 @@ export function useListFilters(deps: UseListFiltersDeps): UseListFiltersReturn {
|
|
|
74
99
|
items,
|
|
75
100
|
page,
|
|
76
101
|
searchQuery,
|
|
77
|
-
route,
|
|
78
|
-
router,
|
|
79
102
|
savedFilters,
|
|
80
103
|
persistFilters,
|
|
81
|
-
|
|
104
|
+
persister,
|
|
105
|
+
routeStateScope,
|
|
106
|
+
routeStateWrites,
|
|
107
|
+
pageSize,
|
|
108
|
+
sortField,
|
|
109
|
+
sortOrder,
|
|
110
|
+
persistSort,
|
|
82
111
|
autoLoadFilters,
|
|
83
112
|
filterSessionKey,
|
|
84
113
|
entityFilters,
|
|
@@ -89,16 +118,21 @@ export function useListFilters(deps: UseListFiltersDeps): UseListFiltersReturn {
|
|
|
89
118
|
const filtersMap = ref<Map<string, FilterConfig>>(new Map())
|
|
90
119
|
const filterValues = ref<Record<string, unknown>>(savedFilters || {})
|
|
91
120
|
|
|
92
|
-
/** Names the
|
|
121
|
+
/** Names the route state already owns — a filter cannot have them (#2113). */
|
|
93
122
|
const RESERVED_QUERY_KEYS = new Set(['page', 'search'])
|
|
94
123
|
const warnedReserved = new Set<string>()
|
|
95
124
|
|
|
96
125
|
/**
|
|
97
|
-
* A filter named `page` or `search` silently overwrites the
|
|
126
|
+
* A filter named `page` or `search` silently overwrites the stored key of
|
|
98
127
|
* the same name, and is overwritten back on restore. `search` has behaved
|
|
99
128
|
* this way since the URL sync existed and nobody ever reported it, which
|
|
100
|
-
* says how quiet the failure is; `page`
|
|
101
|
-
*
|
|
129
|
+
* says how quiet the failure is; `page` joined it once the page number
|
|
130
|
+
* started being remembered too.
|
|
131
|
+
*
|
|
132
|
+
* SCOPING DID NOT FIX THIS. Prefixing keys by entity separates this list
|
|
133
|
+
* from other lists; it does not separate the list from qdadm's own two
|
|
134
|
+
* keys, which live in the same scope it does — `offers.page` is written by
|
|
135
|
+
* the pager whatever a filter called `page` wants.
|
|
102
136
|
*
|
|
103
137
|
* We name what happens INSTEAD of what was asked, rather than dropping the
|
|
104
138
|
* filter — renaming someone's filter behind their back would be a worse
|
|
@@ -106,20 +140,53 @@ export function useListFilters(deps: UseListFiltersDeps): UseListFiltersReturn {
|
|
|
106
140
|
*/
|
|
107
141
|
function warnIfReservedName(name: string): void {
|
|
108
142
|
if (!RESERVED_QUERY_KEYS.has(name)) return
|
|
109
|
-
|
|
143
|
+
// Gated on writing, not on the URL specifically: the collision is with
|
|
144
|
+
// whatever medium this list persists to.
|
|
145
|
+
if (!routeStateWrites) return
|
|
110
146
|
if (warnedReserved.has(name)) return
|
|
111
147
|
warnedReserved.add(name)
|
|
112
148
|
console.warn(
|
|
113
|
-
`[qdadm] Filter "${name}" on "${entityName}" collides with the
|
|
114
|
-
`
|
|
149
|
+
`[qdadm] Filter "${name}" on "${entityName}" collides with the key of ` +
|
|
150
|
+
`the same name in this list's persisted route state. The list's own ` +
|
|
115
151
|
`${name === 'page' ? 'page number' : 'search query'} wins and this ` +
|
|
116
|
-
`filter will not survive a reload. Rename the filter, or
|
|
117
|
-
`
|
|
152
|
+
`filter will not survive a reload. Rename the filter, or turn the ` +
|
|
153
|
+
`persistence off on this list with syncUrlParams: false.`
|
|
154
|
+
)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const warnedTypes = new Set<string>()
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* A filter type qdadm cannot render (#2147).
|
|
161
|
+
*
|
|
162
|
+
* `ListPage` is binary — an autocomplete, or a `Select` for everything
|
|
163
|
+
* else, unknown types included. So an invented type does not fail; it
|
|
164
|
+
* renders as a dropdown with no options, reading "No available options",
|
|
165
|
+
* and the user concludes there is nothing to choose. Reported by a consumer
|
|
166
|
+
* whose two `type: 'text'` filters had never filtered anything.
|
|
167
|
+
*
|
|
168
|
+
* Warned HERE because this is the last place the filter's NAME is known.
|
|
169
|
+
* Three screens later there is only an empty control and no way back to
|
|
170
|
+
* the declaration that caused it.
|
|
171
|
+
*/
|
|
172
|
+
function warnIfUnknownType(name: string, type: unknown): void {
|
|
173
|
+
if (type === undefined || type === null) return
|
|
174
|
+
if (FILTER_TYPES.includes(type as (typeof FILTER_TYPES)[number])) return
|
|
175
|
+
const key = `${name}:${String(type)}`
|
|
176
|
+
if (warnedTypes.has(key)) return
|
|
177
|
+
warnedTypes.add(key)
|
|
178
|
+
console.warn(
|
|
179
|
+
`[qdadm] Filter "${name}" on "${entityName}" has type "${String(type)}", ` +
|
|
180
|
+
`which qdadm cannot render. It will fall back to a dropdown, and ` +
|
|
181
|
+
`unless you also give it options that dropdown will be empty — ` +
|
|
182
|
+
`"No available options" — rather than the control you asked for. ` +
|
|
183
|
+
`Known types: ${FILTER_TYPES.join(', ')}.`
|
|
118
184
|
)
|
|
119
185
|
}
|
|
120
186
|
|
|
121
|
-
function addFilter(name: string, filterConfig:
|
|
187
|
+
function addFilter(name: string, filterConfig: FilterOptions): void {
|
|
122
188
|
warnIfReservedName(name)
|
|
189
|
+
warnIfUnknownType(name, filterConfig?.type)
|
|
123
190
|
filtersMap.value.set(name, {
|
|
124
191
|
name,
|
|
125
192
|
type: 'select',
|
|
@@ -155,34 +222,37 @@ export function useListFilters(deps: UseListFiltersDeps): UseListFiltersReturn {
|
|
|
155
222
|
* for a detail view, so that coming back restores it.
|
|
156
223
|
*/
|
|
157
224
|
function writeStateToUrl(): void {
|
|
158
|
-
if (!
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
225
|
+
if (!persister || !routeStateWrites) return
|
|
226
|
+
|
|
227
|
+
// Page 1 is the default and stays implicit, exactly like an unset filter:
|
|
228
|
+
// the persister drops null/undefined/'' rather than storing them empty,
|
|
229
|
+
// so a pristine list leaves a clean link.
|
|
230
|
+
//
|
|
231
|
+
// `search` and `page` still share the namespace with the filter names —
|
|
232
|
+
// the scope separates this list from OTHER lists, not from qdadm's own
|
|
233
|
+
// keys — which is why addFilter() still warns about a filter called
|
|
234
|
+
// `page` or `search`.
|
|
235
|
+
persister.write(routeStateScope, {
|
|
236
|
+
...filterValues.value,
|
|
237
|
+
search: searchQuery.value || null,
|
|
238
|
+
page: page.value > 1 ? page.value : null,
|
|
239
|
+
// Always written, never conditional on a default: rows-per-page is a
|
|
240
|
+
// choice somebody made, and the default is not "unset" — it is what
|
|
241
|
+
// they get back if we forget. The default composition routes it to a
|
|
242
|
+
// cookie, so it does not land in a shareable link.
|
|
243
|
+
pageSize: pageSize.value,
|
|
244
|
+
// Sort goes to the session under the default composition, so it does
|
|
245
|
+
// not reach a link either. `persistSort: false` writes null, which the
|
|
246
|
+
// persister removes — the option has always been able to turn this off
|
|
247
|
+
// and still can.
|
|
248
|
+
//
|
|
249
|
+
// The DIRECTION is stored only when there is a field to apply it to.
|
|
250
|
+
// An order on its own is not partial state, it is meaningless state,
|
|
251
|
+
// and storing it would leave `sortOrder: -1` sitting in every unsorted
|
|
252
|
+
// list's namespace.
|
|
253
|
+
sort: persistSort ? sortField.value : null,
|
|
254
|
+
sortOrder: persistSort && sortField.value ? sortOrder.value : null,
|
|
255
|
+
})
|
|
186
256
|
}
|
|
187
257
|
|
|
188
258
|
function onFiltersChanged(): void {
|
|
@@ -220,15 +290,7 @@ export function useListFilters(deps: UseListFiltersDeps): UseListFiltersReturn {
|
|
|
220
290
|
clearSessionFilters(filterSessionKey)
|
|
221
291
|
}
|
|
222
292
|
page.value = 1
|
|
223
|
-
if (
|
|
224
|
-
const query = { ...route.query } as Record<string, string>
|
|
225
|
-
for (const key of filtersMap.value.keys()) {
|
|
226
|
-
delete query[key]
|
|
227
|
-
}
|
|
228
|
-
delete query.search
|
|
229
|
-
delete query.page
|
|
230
|
-
router.replace({ query })
|
|
231
|
-
}
|
|
293
|
+
if (routeStateWrites) persister?.clear(routeStateScope)
|
|
232
294
|
loadItems()
|
|
233
295
|
}
|
|
234
296
|
|
|
@@ -472,23 +534,31 @@ export function useListFilters(deps: UseListFiltersDeps): UseListFiltersReturn {
|
|
|
472
534
|
}
|
|
473
535
|
|
|
474
536
|
function restoreFilters(): void {
|
|
537
|
+
const stored = persister?.read(routeStateScope)
|
|
538
|
+
if (!stored) return
|
|
539
|
+
|
|
540
|
+
// Only keys this list declares as filters: the scope keeps other lists
|
|
541
|
+
// out, but a persister may still hand back something the app put there.
|
|
475
542
|
for (const key of filtersMap.value.keys()) {
|
|
476
|
-
if (
|
|
477
|
-
let value: unknown = route.query[key]
|
|
478
|
-
if (value === 'true') value = true
|
|
479
|
-
else if (value === 'false') value = false
|
|
480
|
-
else if (value === 'null') value = null
|
|
481
|
-
else if (!isNaN(Number(value)) && value !== '') value = Number(value)
|
|
482
|
-
filterValues.value[key] = value
|
|
483
|
-
}
|
|
543
|
+
if (stored[key] !== undefined) filterValues.value[key] = stored[key]
|
|
484
544
|
}
|
|
485
|
-
|
|
486
|
-
|
|
545
|
+
|
|
546
|
+
// NOT `typeof === 'string'` (#2147). A term that survives a numeric round
|
|
547
|
+
// trip — an order id, an invoice, a reference — comes back from the URL
|
|
548
|
+
// as a number, and a string-only guard dropped it: the box came back
|
|
549
|
+
// empty and the list unfiltered, so a shared link showed the recipient
|
|
550
|
+
// something other than what the sender searched for.
|
|
551
|
+
//
|
|
552
|
+
// Safe to stringify because the persister no longer coerces lossily: a
|
|
553
|
+
// value that could not be written back as the same text was left as text.
|
|
554
|
+
if (stored.search !== null && stored.search !== undefined && stored.search !== '') {
|
|
555
|
+
searchQuery.value = String(stored.search)
|
|
487
556
|
}
|
|
557
|
+
|
|
488
558
|
// The page must be restored HERE, before the first loadItems(): restoring
|
|
489
559
|
// it later would mean a first request for page 1 and a second for the
|
|
490
560
|
// real one (#2113).
|
|
491
|
-
const restoredPage = Number(
|
|
561
|
+
const restoredPage = Number(stored.page)
|
|
492
562
|
if (Number.isInteger(restoredPage) && restoredPage > 0) {
|
|
493
563
|
page.value = restoredPage
|
|
494
564
|
}
|