@quazardous/qdadm 2.19.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 +2 -2
- package/src/components/lists/ListPage.vue +7 -0
- package/src/composables/index.ts +3 -0
- package/src/composables/useListPage.filters.ts +137 -67
- package/src/composables/useListPage.ts +106 -18
- 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/index.ts +19 -0
- package/src/kernel/Kernel.registries.ts +2 -2
- package/src/kernel/Kernel.types.ts +16 -0
- package/src/kernel/Kernel.vue.ts +3 -0
- 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
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a list uses when nobody chose (#2146).
|
|
3
|
+
*
|
|
4
|
+
* Not simply `UrlPersister`, because not all list state wants the same
|
|
5
|
+
* medium. Filters, search and the page belong in a link — that is the whole
|
|
6
|
+
* argument for the URL being the default. Rows-per-page does not: it is a
|
|
7
|
+
* comfort setting somebody picked once, and a link carrying it would impose
|
|
8
|
+
* the sender's row count on the reader.
|
|
9
|
+
*
|
|
10
|
+
* So the default is a composition, and it says out loud what qdadm was
|
|
11
|
+
* already doing by accident:
|
|
12
|
+
*
|
|
13
|
+
* | State | Medium | Scope |
|
|
14
|
+
* |---|---|---|
|
|
15
|
+
* | filters, search, page | query string | the list |
|
|
16
|
+
* | `pageSize` | cookie, a year | the app |
|
|
17
|
+
* | `sort` | `sessionStorage` | the list |
|
|
18
|
+
*
|
|
19
|
+
* **Sort stays in the session** — decided on #2146 rather than moved to the
|
|
20
|
+
* URL. Moving it would make a list link carry its ordering, which is a real
|
|
21
|
+
* gain; it would also mean the sort no longer survives opening a clean
|
|
22
|
+
* `/offers`, which it has always done. Between a gain nobody asked for and a
|
|
23
|
+
* loss every existing user would feel, the seam is worth having without the
|
|
24
|
+
* behaviour change: this expresses today's arrangement rather than replacing
|
|
25
|
+
* it.
|
|
26
|
+
*/
|
|
27
|
+
import { CompositePersister } from './CompositePersister'
|
|
28
|
+
import { CookiePersister, type CookieJar } from './CookiePersister'
|
|
29
|
+
import { WebStoragePersister, type RouteStateStorage } from './WebStoragePersister'
|
|
30
|
+
import { UrlPersister, type UrlPersisterRoute, type UrlPersisterRouter } from './UrlPersister'
|
|
31
|
+
import type { RouteStatePersister } from './RouteStatePersister'
|
|
32
|
+
|
|
33
|
+
export interface DefaultRouteStateOptions {
|
|
34
|
+
router: UrlPersisterRouter
|
|
35
|
+
route: UrlPersisterRoute
|
|
36
|
+
/** Stand-in for `document`, for a test or an environment without one. */
|
|
37
|
+
cookieJar?: CookieJar
|
|
38
|
+
/** Stand-in for `sessionStorage`, same reason. */
|
|
39
|
+
sessionStorage?: RouteStateStorage
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** The scope `pageSize` is remembered under: one setting for the whole app. */
|
|
43
|
+
export const APP_ROUTE_STATE_SCOPE = 'app'
|
|
44
|
+
|
|
45
|
+
export function createDefaultRouteStatePersister(
|
|
46
|
+
options: DefaultRouteStateOptions
|
|
47
|
+
): RouteStatePersister {
|
|
48
|
+
const session = new WebStoragePersister({
|
|
49
|
+
storage: options.sessionStorage ?? globalThis.sessionStorage,
|
|
50
|
+
name: 'session_storage',
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
return new CompositePersister({
|
|
54
|
+
fallback: new UrlPersister(options),
|
|
55
|
+
keys: {
|
|
56
|
+
pageSize: {
|
|
57
|
+
persister: new CookiePersister(options.cookieJar ? { jar: options.cookieJar } : {}),
|
|
58
|
+
scope: APP_ROUTE_STATE_SCOPE,
|
|
59
|
+
},
|
|
60
|
+
// Per-list, unlike pageSize: an ordering belongs to the screen you were
|
|
61
|
+
// looking at, not to the whole app.
|
|
62
|
+
sort: { persister: session },
|
|
63
|
+
sortOrder: { persister: session },
|
|
64
|
+
},
|
|
65
|
+
})
|
|
66
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Slugs to persisters (#2146).
|
|
3
|
+
*
|
|
4
|
+
* Mirrors what `entity/storage/factory.ts` does for storages — an instance, or
|
|
5
|
+
* a name the framework knows — so a consumer meets one convention rather than
|
|
6
|
+
* two.
|
|
7
|
+
*
|
|
8
|
+
* | Slug | Medium | Reach for it when |
|
|
9
|
+
* |---|---|---|
|
|
10
|
+
* | `default` | query string + cookie | what a list uses when nobody chose |
|
|
11
|
+
* | `url` | query string | EVERYTHING in the link, row count included |
|
|
12
|
+
* | `local_storage` | `localStorage` | worth keeping, not worth linking |
|
|
13
|
+
* | `session_storage` | `sessionStorage` | worth keeping until the tab closes |
|
|
14
|
+
* | `cookie` | one cookie per scope | the SERVER needs to read it |
|
|
15
|
+
* | `memory` | a shared Map | survives navigation, not a reload |
|
|
16
|
+
* | `none` | nowhere | this screen forgets, and says so |
|
|
17
|
+
*/
|
|
18
|
+
import type { RouteStatePersister, RouteStatePersisterFactory } from './RouteStatePersister'
|
|
19
|
+
import { UrlPersister, type UrlPersisterRoute, type UrlPersisterRouter } from './UrlPersister'
|
|
20
|
+
import { WebStoragePersister, type RouteStateStorage } from './WebStoragePersister'
|
|
21
|
+
import { CookiePersister, type CookieJar } from './CookiePersister'
|
|
22
|
+
import { MemoryPersister } from './MemoryPersister'
|
|
23
|
+
import { NullPersister } from './NullPersister'
|
|
24
|
+
import { createDefaultRouteStatePersister } from './defaultPersister'
|
|
25
|
+
|
|
26
|
+
export interface RouteStateFactoryContext {
|
|
27
|
+
router: UrlPersisterRouter
|
|
28
|
+
route: UrlPersisterRoute
|
|
29
|
+
/**
|
|
30
|
+
* Overrides for the browser globals the storage-backed slugs reach for.
|
|
31
|
+
*
|
|
32
|
+
* Only needed off a browser — SSR, or a test that would rather not share
|
|
33
|
+
* one `localStorage` between cases. Left alone, each slug uses the real
|
|
34
|
+
* thing, resolved AT BUILD TIME rather than captured here, so a factory
|
|
35
|
+
* created before the app mounts still works.
|
|
36
|
+
*/
|
|
37
|
+
localStorage?: RouteStateStorage
|
|
38
|
+
sessionStorage?: RouteStateStorage
|
|
39
|
+
cookieJar?: CookieJar
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* The built-in factory.
|
|
44
|
+
*
|
|
45
|
+
* Returns null for a slug it does not know — the CALLER throws, with the name
|
|
46
|
+
* in the message. Returning a default here would make an unknown slug behave
|
|
47
|
+
* like `url` and say nothing, which is the failure ADR 0011 forbids and the
|
|
48
|
+
* one this seam is cleaning up after.
|
|
49
|
+
*/
|
|
50
|
+
export function createRouteStatePersisterFactory(
|
|
51
|
+
context: RouteStateFactoryContext
|
|
52
|
+
): RouteStatePersisterFactory {
|
|
53
|
+
return (slug: string): RouteStatePersister | null => {
|
|
54
|
+
switch (slug) {
|
|
55
|
+
// Nameable, so "the default, stated out loud" has a way to be said.
|
|
56
|
+
// It is NOT `url`: the default routes `pageSize` to a cookie, because a
|
|
57
|
+
// link should not impose the sender's row count on its reader.
|
|
58
|
+
case 'default':
|
|
59
|
+
return createDefaultRouteStatePersister({
|
|
60
|
+
router: context.router,
|
|
61
|
+
route: context.route,
|
|
62
|
+
cookieJar: context.cookieJar,
|
|
63
|
+
})
|
|
64
|
+
case 'url':
|
|
65
|
+
return new UrlPersister(context)
|
|
66
|
+
case 'local_storage':
|
|
67
|
+
return new WebStoragePersister({
|
|
68
|
+
storage: context.localStorage ?? globalThis.localStorage,
|
|
69
|
+
name: 'local_storage',
|
|
70
|
+
})
|
|
71
|
+
case 'session_storage':
|
|
72
|
+
return new WebStoragePersister({
|
|
73
|
+
storage: context.sessionStorage ?? globalThis.sessionStorage,
|
|
74
|
+
name: 'session_storage',
|
|
75
|
+
})
|
|
76
|
+
case 'cookie':
|
|
77
|
+
return new CookiePersister(context.cookieJar ? { jar: context.cookieJar } : {})
|
|
78
|
+
case 'memory':
|
|
79
|
+
return new MemoryPersister()
|
|
80
|
+
case 'none':
|
|
81
|
+
return new NullPersister()
|
|
82
|
+
default:
|
|
83
|
+
return null
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Route-state persistence (#2146).
|
|
3
|
+
*
|
|
4
|
+
* The seam that decides WHERE a route remembers what it is showing. The list
|
|
5
|
+
* is one consumer; a detail page's active tab and a dashboard's date range
|
|
6
|
+
* are the same problem.
|
|
7
|
+
*/
|
|
8
|
+
export type {
|
|
9
|
+
RouteStatePersister,
|
|
10
|
+
RouteStateScope,
|
|
11
|
+
RouteStatePersisterSpec,
|
|
12
|
+
RouteStatePersisterFactory,
|
|
13
|
+
} from './RouteStatePersister'
|
|
14
|
+
export { isRouteStatePersister, resolveRouteStatePersister } from './RouteStatePersister'
|
|
15
|
+
export { UrlPersister } from './UrlPersister'
|
|
16
|
+
export type { UrlPersisterOptions, UrlPersisterRouter, UrlPersisterRoute } from './UrlPersister'
|
|
17
|
+
export { createRouteStatePersisterFactory } from './factory'
|
|
18
|
+
export { dottedKeyLocator, namespacedKeyLocator } from './keyLocator'
|
|
19
|
+
export type { RouteStateKeyLocator } from './keyLocator'
|
|
20
|
+
export { WebStoragePersister } from './WebStoragePersister'
|
|
21
|
+
export type { WebStoragePersisterOptions, RouteStateStorage } from './WebStoragePersister'
|
|
22
|
+
export { CookiePersister } from './CookiePersister'
|
|
23
|
+
export type { CookiePersisterOptions, CookieJar } from './CookiePersister'
|
|
24
|
+
export { MemoryPersister, clearRouteStateMemory } from './MemoryPersister'
|
|
25
|
+
export type { MemoryPersisterOptions, RouteStateMemoryStore } from './MemoryPersister'
|
|
26
|
+
export { NullPersister } from './NullPersister'
|
|
27
|
+
export { underscoreScopeLocator } from './keyLocator'
|
|
28
|
+
export type { RouteStateScopeLocator } from './keyLocator'
|
|
29
|
+
export type { RouteStateFactoryContext } from './factory'
|
|
30
|
+
export { CompositePersister } from './CompositePersister'
|
|
31
|
+
export type { CompositePersisterOptions, RoutedKey } from './CompositePersister'
|
|
32
|
+
export { createDefaultRouteStatePersister } from './defaultPersister'
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How a (scope, key) pair becomes a stored key — and back (#2146).
|
|
3
|
+
*
|
|
4
|
+
* Hard-coding `offers.page` inside the URL persister would make the naming
|
|
5
|
+
* scheme a property of qdadm rather than of the app. An app that wants
|
|
6
|
+
* `o_page`, or a single packed parameter, or a `[]`-suffixed convention its
|
|
7
|
+
* backend already understands, must be able to say so — the same way it can
|
|
8
|
+
* already override how a storage is resolved.
|
|
9
|
+
*
|
|
10
|
+
* ONE PER PERSISTER TYPE, because the right scheme differs by medium: dots
|
|
11
|
+
* read well in a query string, colons are the convention in `localStorage`,
|
|
12
|
+
* and a cookie has its own constraints entirely.
|
|
13
|
+
*
|
|
14
|
+
* `decode` returns null for a key that is not ours. That is what lets a
|
|
15
|
+
* persister pick its own entries out of a medium it shares with everything
|
|
16
|
+
* else — the whole reason the scope exists.
|
|
17
|
+
*/
|
|
18
|
+
export interface RouteStateKeyLocator {
|
|
19
|
+
encode(scope: string, key: string): string
|
|
20
|
+
/** The bare key, or null when this stored key belongs to someone else. */
|
|
21
|
+
decode(scope: string, storedKey: string): string | null
|
|
22
|
+
/**
|
|
23
|
+
* Whether this key carries a scope at all — ANY scope, not just one.
|
|
24
|
+
*
|
|
25
|
+
* A legacy-compatibility reader needs this: `offers.page` and `jobs.page`
|
|
26
|
+
* both belong to somebody, and treating them as unscoped leftovers would
|
|
27
|
+
* hand one list the other's state. Only a key that is scoped by nobody can
|
|
28
|
+
* be read as pre-scope state.
|
|
29
|
+
*/
|
|
30
|
+
looksScoped(storedKey: string): boolean
|
|
31
|
+
readonly name: string
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** `offers` + `page` → `offers.page`. The default in a query string. */
|
|
35
|
+
export const dottedKeyLocator: RouteStateKeyLocator = {
|
|
36
|
+
name: 'dotted',
|
|
37
|
+
encode: (scope, key) => (scope ? `${scope}.${key}` : key),
|
|
38
|
+
decode: (scope, storedKey) => {
|
|
39
|
+
if (!scope) return storedKey
|
|
40
|
+
const prefix = `${scope}.`
|
|
41
|
+
return storedKey.startsWith(prefix) ? storedKey.slice(prefix.length) : null
|
|
42
|
+
},
|
|
43
|
+
looksScoped: (storedKey) => storedKey.includes('.'),
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** `offers` + `page` → `qdadm:offers:page`. The default in web storage. */
|
|
47
|
+
export const namespacedKeyLocator: RouteStateKeyLocator = {
|
|
48
|
+
name: 'namespaced',
|
|
49
|
+
encode: (scope, key) => `qdadm:${scope}:${key}`,
|
|
50
|
+
decode: (scope, storedKey) => {
|
|
51
|
+
const prefix = `qdadm:${scope}:`
|
|
52
|
+
return storedKey.startsWith(prefix) ? storedKey.slice(prefix.length) : null
|
|
53
|
+
},
|
|
54
|
+
looksScoped: (storedKey) => storedKey.startsWith('qdadm:'),
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* How a SCOPE becomes a stored name, for media that keep one entry per scope.
|
|
59
|
+
*
|
|
60
|
+
* The cookie jar is the case: a cookie rides on every request, so a scope
|
|
61
|
+
* gets one cookie holding a blob rather than one per key. There is no key
|
|
62
|
+
* half to encode, which is why this is its own small interface rather than
|
|
63
|
+
* `RouteStateKeyLocator` called with a key nobody uses.
|
|
64
|
+
*/
|
|
65
|
+
export interface RouteStateScopeLocator {
|
|
66
|
+
encode(scope: string): string
|
|
67
|
+
/** The bare scope, or null when this stored name belongs to someone else. */
|
|
68
|
+
decode(storedName: string): string | null
|
|
69
|
+
readonly name: string
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* `offers` → `qdadm_offers`. The default in a cookie jar.
|
|
74
|
+
*
|
|
75
|
+
* Underscores rather than the colons web storage uses: a cookie NAME is an
|
|
76
|
+
* RFC 6265 token, and `:` is a separator there. `qdadm:offers` is not a
|
|
77
|
+
* cookie name a server is obliged to accept.
|
|
78
|
+
*/
|
|
79
|
+
export const underscoreScopeLocator: RouteStateScopeLocator = {
|
|
80
|
+
name: 'underscore',
|
|
81
|
+
encode: (scope) => `qdadm_${scope}`,
|
|
82
|
+
decode: (storedName) =>
|
|
83
|
+
storedName.startsWith('qdadm_') ? storedName.slice('qdadm_'.length) : null,
|
|
84
|
+
}
|