@quazardous/qdadm 2.11.0 → 2.13.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/README.md +8 -134
- package/package.json +4 -1
- package/src/composables/useDirtyState.ts +31 -1
- package/src/entity/storage/MockApiStorage.ts +6 -1
- package/src/vite/qdadmPlugin.ts +52 -6
package/README.md
CHANGED
|
@@ -2,23 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
**Vue 3 admin framework. PrimeVue. Zero boilerplate.**
|
|
4
4
|
|
|
5
|
-
Quick start: [README](https://github.com/quazardous/qdadm/blob/main/README.md) (5 min tutorial)
|
|
6
|
-
|
|
7
|
-
Concepts & patterns: [QDADM_CREDO](https://github.com/quazardous/qdadm/blob/main/packages/qdadm/QDADM_CREDO.md)
|
|
8
|
-
|
|
9
|
-
Changelog: [CHANGELOG](https://github.com/quazardous/qdadm/blob/main/CHANGELOG.md)
|
|
10
|
-
|
|
11
5
|
## Installation
|
|
12
6
|
|
|
13
7
|
```bash
|
|
14
8
|
npm install @quazardous/qdadm primevue @primeuix/themes primeicons vue-router pinia
|
|
15
9
|
```
|
|
16
10
|
|
|
17
|
-
qdadm ships raw TS/Vue sources, so Vite needs a small config assist to keep
|
|
18
|
-
PrimeVue as a single module instance in dev. One line:
|
|
19
|
-
|
|
20
11
|
```ts
|
|
21
|
-
// vite.config.ts
|
|
12
|
+
// vite.config.ts — the plugin wires the resolver/optimizer settings
|
|
13
|
+
// qdadm's raw-source packaging needs (npm and file:/workspace installs)
|
|
22
14
|
import { qdadmVitePlugin } from '@quazardous/qdadm/vite'
|
|
23
15
|
|
|
24
16
|
export default defineConfig({
|
|
@@ -26,135 +18,17 @@ export default defineConfig({
|
|
|
26
18
|
})
|
|
27
19
|
```
|
|
28
20
|
|
|
29
|
-
(It applies `resolve.dedupe` + the `optimizeDeps` exclude/include set — see
|
|
30
|
-
the plugin's JSDoc for details. Without it, dev boots to a blank page with
|
|
31
|
-
`Error: No PrimeVue Toast provided!`.)
|
|
32
|
-
|
|
33
|
-
## Exports
|
|
34
|
-
|
|
35
|
-
```js
|
|
36
|
-
// Core
|
|
37
|
-
import { Kernel, EntityManager } from '@quazardous/qdadm'
|
|
38
|
-
|
|
39
|
-
// Storage backends
|
|
40
|
-
import { MockApiStorage, ApiStorage, SdkStorage } from '@quazardous/qdadm'
|
|
41
|
-
|
|
42
|
-
// Auth
|
|
43
|
-
import { SessionAuthAdapter, LocalStorageSessionAuthAdapter } from '@quazardous/qdadm'
|
|
44
|
-
|
|
45
|
-
// Security (permissions, roles)
|
|
46
|
-
import { SecurityChecker, PermissionMatcher, PermissionRegistry } from '@quazardous/qdadm/security'
|
|
47
|
-
import { PersistableRoleProvider, createLocalStorageRolesProvider } from '@quazardous/qdadm/security'
|
|
48
|
-
|
|
49
|
-
// Composables
|
|
50
|
-
import { useBareForm, useListPage } from '@quazardous/qdadm/composables'
|
|
51
|
-
|
|
52
|
-
// Components
|
|
53
|
-
import { ListPage, PageLayout, AppLayout, FormField, FormActions } from '@quazardous/qdadm/components'
|
|
54
|
-
|
|
55
|
-
// Module system
|
|
56
|
-
import { KernelContext } from '@quazardous/qdadm/module'
|
|
57
|
-
|
|
58
|
-
// Styles
|
|
59
|
-
import '@quazardous/qdadm/styles'
|
|
60
|
-
```
|
|
61
|
-
|
|
62
|
-
## ctx.crud() - Route Helper
|
|
63
|
-
|
|
64
|
-
Register CRUD routes + navigation in one call:
|
|
65
|
-
|
|
66
|
-
```js
|
|
67
|
-
class BooksModule extends Module {
|
|
68
|
-
async connect(ctx) {
|
|
69
|
-
ctx.entity('books', { ... })
|
|
70
|
-
|
|
71
|
-
// Full CRUD with single form
|
|
72
|
-
ctx.crud('books', {
|
|
73
|
-
list: () => import('./pages/BookList.vue'),
|
|
74
|
-
form: () => import('./pages/BookForm.vue') // create + edit
|
|
75
|
-
}, {
|
|
76
|
-
nav: { section: 'Library', icon: 'pi pi-book' }
|
|
77
|
-
})
|
|
78
|
-
|
|
79
|
-
// List only (read-only entity)
|
|
80
|
-
ctx.crud('settings', {
|
|
81
|
-
list: () => import('./pages/SettingsPage.vue')
|
|
82
|
-
}, { nav: { section: 'Config', icon: 'pi pi-cog' } })
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
```
|
|
86
|
-
|
|
87
|
-
Auto-generates: routes, route family, nav item. Use `ctx.routes()` for custom pages.
|
|
88
|
-
|
|
89
|
-
## SdkStorage
|
|
90
|
-
|
|
91
|
-
Adapter for generated SDK clients (hey-api, openapi-generator, etc.):
|
|
92
|
-
|
|
93
|
-
```js
|
|
94
|
-
import { SdkStorage } from '@quazardous/qdadm'
|
|
95
|
-
|
|
96
|
-
const storage = new SdkStorage({
|
|
97
|
-
sdk,
|
|
98
|
-
methods: {
|
|
99
|
-
list: 'getApiBooks',
|
|
100
|
-
get: 'getApiBooksById',
|
|
101
|
-
create: 'postApiBooks',
|
|
102
|
-
update: 'patchApiBooksById',
|
|
103
|
-
delete: 'deleteApiBooksById'
|
|
104
|
-
}
|
|
105
|
-
})
|
|
106
|
-
```
|
|
107
|
-
|
|
108
|
-
Options: `transformRequest`, `transformResponse`, `responseFormat`, `clientSidePagination`.
|
|
109
|
-
|
|
110
21
|
## Documentation
|
|
111
22
|
|
|
112
|
-
|
|
113
|
-
|-----|---------|
|
|
114
|
-
| [QDADM_CREDO](https://github.com/quazardous/qdadm/blob/main/packages/qdadm/QDADM_CREDO.md) | Philosophy, patterns, concepts |
|
|
115
|
-
| [Page compositions](https://github.com/quazardous/qdadm/blob/main/docs/page-compositions.md) | **Start here** — which composition for which need |
|
|
116
|
-
| [CRUD Pages](https://github.com/quazardous/qdadm/blob/main/docs/crud.md) | List / form / show / child pages reference |
|
|
117
|
-
| [Navigation](https://github.com/quazardous/qdadm/blob/main/docs/navigation.md) | Breadcrumb, sibling navlinks, View↔Edit toggle |
|
|
118
|
-
| [Architecture](https://github.com/quazardous/qdadm/blob/main/docs/architecture.md) | PAC pattern, layers |
|
|
119
|
-
| [Extension](https://github.com/quazardous/qdadm/blob/main/docs/extension.md) | Decorators, bundles, multi-storage |
|
|
120
|
-
| [Hooks](https://github.com/quazardous/qdadm/blob/main/docs/hooks.md) | Alter / invoke registry |
|
|
121
|
-
| [Signals](https://github.com/quazardous/qdadm/blob/main/docs/signals.md) | Event bus, SSE bridge |
|
|
122
|
-
| [Zones](https://github.com/quazardous/qdadm/blob/main/docs/zones.md) | Extensible UI blocks |
|
|
123
|
-
| [Security](https://github.com/quazardous/qdadm/blob/main/docs/security.md) | Permissions, roles |
|
|
124
|
-
| [i18n](https://github.com/quazardous/qdadm/blob/main/docs/i18n.md) | Messages, providers, strategies |
|
|
125
|
-
| [Codegen](https://github.com/quazardous/qdadm/blob/main/docs/gen.md) | Generate managers (OpenAPI / manual) |
|
|
126
|
-
| [Deferred](https://github.com/quazardous/qdadm/blob/main/docs/deferred.md) | Deferred values & warmup |
|
|
127
|
-
| [AGENT_GUIDE](https://github.com/quazardous/qdadm/blob/main/docs/AGENT_GUIDE.md) | Quick index for AI agents |
|
|
128
|
-
|
|
129
|
-
## Peer Dependencies
|
|
130
|
-
|
|
131
|
-
- vue ^3.3.0
|
|
132
|
-
- vue-router ^4.0.0
|
|
133
|
-
- primevue ^4.0.0
|
|
134
|
-
- pinia ^2.0.0
|
|
23
|
+
Start with the **[step-by-step tutorial](https://github.com/quazardous/qdadm/blob/main/docs/tutorial-mini-admin.md)** — bootstrap, CRUD, auth, navigation, parent-child in ~310 lines.
|
|
135
24
|
|
|
136
|
-
|
|
25
|
+
- [Full documentation](https://github.com/quazardous/qdadm/tree/main/docs)
|
|
26
|
+
- [Concepts & patterns (CREDO)](https://github.com/quazardous/qdadm/blob/main/docs/QDADM_CREDO.md)
|
|
27
|
+
- [Changelog](https://github.com/quazardous/qdadm/blob/main/CHANGELOG.md)
|
|
137
28
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
Symptom: PrimeVue components render without their Aura preset tokens — component-level CSS variables (`--p-paginator-nav-button-width`, etc.) end up undefined, so paginator buttons collapse to 16×16, datatables lose padding, rounded corners disappear. Plugin singletons rely on a shared runtime registry; two instances split the registry in half.
|
|
141
|
-
|
|
142
|
-
Fix in the host's bundler config — `dedupe` collapses duplicates back to a single resolved copy:
|
|
143
|
-
|
|
144
|
-
```ts
|
|
145
|
-
// vite.config.ts
|
|
146
|
-
export default defineConfig({
|
|
147
|
-
resolve: {
|
|
148
|
-
dedupe: ['vue', 'vue-router', 'primevue', '@primeuix/themes'],
|
|
149
|
-
},
|
|
150
|
-
})
|
|
151
|
-
```
|
|
152
|
-
|
|
153
|
-
This is the canonical Vite/Webpack pattern for Vue plugin singletons. The `file:` link can stay in place — useful for hot-reload during qdadm development — and the dedupe ensures only one instance of each peer ends up in the bundle.
|
|
154
|
-
|
|
155
|
-
Installing qdadm from npm avoids the *duplicate-copy* trap (the published tarball ships no `node_modules/`, so peers resolve from the host) — but a related *split-pipeline* trap still applies in Vite dev: the host's `primevue` import gets pre-bundled into `.vite/deps` while qdadm's raw sources import `node_modules/primevue/` directly, which also yields two PrimeVue instances. `qdadmVitePlugin()` (see Installation above) handles both cases — dedupe for links, optimizeDeps exclude/include for the pre-bundle split.
|
|
29
|
+
## Peer Dependencies
|
|
156
30
|
|
|
157
|
-
|
|
31
|
+
vue ^3.3 · vue-router ^4 || ^5 · primevue ^4 · pinia ^2 || ^3 || ^4
|
|
158
32
|
|
|
159
33
|
## License
|
|
160
34
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@quazardous/qdadm",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.13.0",
|
|
4
4
|
"description": "Vue 3 framework for admin dashboards with PrimeVue",
|
|
5
5
|
"author": "quazardous",
|
|
6
6
|
"license": "MIT",
|
|
@@ -71,6 +71,9 @@
|
|
|
71
71
|
},
|
|
72
72
|
"vite": {
|
|
73
73
|
"optional": true
|
|
74
|
+
},
|
|
75
|
+
"primevue": {
|
|
76
|
+
"optional": true
|
|
74
77
|
}
|
|
75
78
|
},
|
|
76
79
|
"optionalDependencies": {
|
|
@@ -59,6 +59,10 @@ export function useDirtyState<T extends FormState = FormState>(
|
|
|
59
59
|
// initial side each time doubled the per-key work, and the old
|
|
60
60
|
// JSON.parse(JSON.stringify(state)) deep clone tripled the snapshot cost.
|
|
61
61
|
let initialFieldSnapshots: Map<string, string> | null = null
|
|
62
|
+
// Parsed deep copy of the initial form, kept for path-aware isFieldDirty
|
|
63
|
+
// (#1396). Built once per snapshot; the per-keystroke checkDirty path
|
|
64
|
+
// (#1194) never touches it.
|
|
65
|
+
let initialForm: Record<string, unknown> | null = null
|
|
62
66
|
const ready = ref(false)
|
|
63
67
|
|
|
64
68
|
/** Resolve the form object inside the state (common 'form'/'formData' patterns). */
|
|
@@ -76,6 +80,16 @@ export function useDirtyState<T extends FormState = FormState>(
|
|
|
76
80
|
return s === undefined ? 'undefined' : s
|
|
77
81
|
}
|
|
78
82
|
|
|
83
|
+
/** Resolve a dot path ('config.login') in an object, undefined-safe. */
|
|
84
|
+
function getByPath(obj: unknown, path: string): unknown {
|
|
85
|
+
let cur: unknown = obj
|
|
86
|
+
for (const seg of path.split('.')) {
|
|
87
|
+
if (cur === null || cur === undefined || typeof cur !== 'object') return undefined
|
|
88
|
+
cur = (cur as Record<string, unknown>)[seg]
|
|
89
|
+
}
|
|
90
|
+
return cur
|
|
91
|
+
}
|
|
92
|
+
|
|
79
93
|
function getFormSnapshot(): string {
|
|
80
94
|
return JSON.stringify(getState())
|
|
81
95
|
}
|
|
@@ -96,6 +110,7 @@ export function useDirtyState<T extends FormState = FormState>(
|
|
|
96
110
|
fieldSnapshots.set(key, stringifyField(form[key]))
|
|
97
111
|
}
|
|
98
112
|
initialFieldSnapshots = fieldSnapshots
|
|
113
|
+
initialForm = JSON.parse(JSON.stringify(form)) as Record<string, unknown>
|
|
99
114
|
ready.value = true
|
|
100
115
|
})
|
|
101
116
|
}
|
|
@@ -126,7 +141,21 @@ export function useDirtyState<T extends FormState = FormState>(
|
|
|
126
141
|
}
|
|
127
142
|
|
|
128
143
|
function isFieldDirty(fieldName: string): boolean {
|
|
129
|
-
|
|
144
|
+
// Dot-free names: exact historical behavior.
|
|
145
|
+
if (!fieldName.includes('.')) {
|
|
146
|
+
return dirtyFields.value.includes(fieldName)
|
|
147
|
+
}
|
|
148
|
+
// Path-aware (#1396): a sub-field can only be dirty if its ROOT key is —
|
|
149
|
+
// the pre-filter reuses the per-key work checkDirty already did, and
|
|
150
|
+
// reading dirtyFields keeps the same reactive trigger as flat names.
|
|
151
|
+
const root = fieldName.slice(0, fieldName.indexOf('.'))
|
|
152
|
+
if (!dirtyFields.value.includes(root)) return false
|
|
153
|
+
if (!initialForm) return false
|
|
154
|
+
const currentForm = resolveForm(getState())
|
|
155
|
+
return (
|
|
156
|
+
stringifyField(getByPath(currentForm, fieldName)) !==
|
|
157
|
+
stringifyField(getByPath(initialForm, fieldName))
|
|
158
|
+
)
|
|
130
159
|
}
|
|
131
160
|
|
|
132
161
|
function reset(): void {
|
|
@@ -134,6 +163,7 @@ export function useDirtyState<T extends FormState = FormState>(
|
|
|
134
163
|
dirtyFields.value = []
|
|
135
164
|
initialSnapshot.value = null
|
|
136
165
|
initialFieldSnapshots = null
|
|
166
|
+
initialForm = null
|
|
137
167
|
ready.value = false
|
|
138
168
|
}
|
|
139
169
|
|
|
@@ -13,6 +13,10 @@ export interface MockApiStorageOptions<T extends EntityRecord = EntityRecord> {
|
|
|
13
13
|
generateId?: () => string
|
|
14
14
|
initialData?: T[] | null
|
|
15
15
|
authCheck?: (() => boolean) | null
|
|
16
|
+
/** Override the localStorage key (default `mockapi_${entityName}_data`).
|
|
17
|
+
* Needed when several apps sharing an origin use the same entity names
|
|
18
|
+
* (e.g. multiple demos on one GitHub Pages site) (#1382). */
|
|
19
|
+
storageKey?: string
|
|
16
20
|
}
|
|
17
21
|
|
|
18
22
|
/**
|
|
@@ -63,6 +67,7 @@ export class MockApiStorage<T extends EntityRecord = EntityRecord> extends IStor
|
|
|
63
67
|
generateId = defaultGenerateId,
|
|
64
68
|
initialData = null,
|
|
65
69
|
authCheck = null,
|
|
70
|
+
storageKey = null,
|
|
66
71
|
} = options
|
|
67
72
|
|
|
68
73
|
if (!entityName) {
|
|
@@ -72,7 +77,7 @@ export class MockApiStorage<T extends EntityRecord = EntityRecord> extends IStor
|
|
|
72
77
|
this.entityName = entityName
|
|
73
78
|
this.idField = idField
|
|
74
79
|
this.generateId = generateId
|
|
75
|
-
this._storageKey = `mockapi_${entityName}_data`
|
|
80
|
+
this._storageKey = storageKey || `mockapi_${entityName}_data`
|
|
76
81
|
this._data = new Map()
|
|
77
82
|
this._authCheck = authCheck
|
|
78
83
|
|
package/src/vite/qdadmPlugin.ts
CHANGED
|
@@ -53,6 +53,26 @@ export interface QdadmVitePluginOptions {
|
|
|
53
53
|
* (vue, vue-router, primevue, pinia).
|
|
54
54
|
*/
|
|
55
55
|
dedupe?: string[]
|
|
56
|
+
/**
|
|
57
|
+
* PrimeVue "flavor" (#1393): alias every `primevue/*` import to a
|
|
58
|
+
* compatible fork (e.g. OpenVue). qdadm officially maintains and tests
|
|
59
|
+
* `primevue@4` (MIT) only — a flavor keeping the v4 module layout/API is
|
|
60
|
+
* drop-in; divergences are the consumer's to absorb.
|
|
61
|
+
*
|
|
62
|
+
* ```ts
|
|
63
|
+
* qdadmVitePlugin({ primevue: { package: 'openvue' } })
|
|
64
|
+
* ```
|
|
65
|
+
*/
|
|
66
|
+
primevue?: {
|
|
67
|
+
/** Package that replaces `primevue` (e.g. 'openvue'). */
|
|
68
|
+
package: string
|
|
69
|
+
/**
|
|
70
|
+
* Optional package replacing `@primeuix/themes` — only needed if the
|
|
71
|
+
* flavor ships its own themes fork (none does today; OpenVue keeps the
|
|
72
|
+
* MIT @primeuix/* versions).
|
|
73
|
+
*/
|
|
74
|
+
themes?: string
|
|
75
|
+
}
|
|
56
76
|
}
|
|
57
77
|
|
|
58
78
|
export function qdadmVitePlugin(options: QdadmVitePluginOptions = {}): Plugin {
|
|
@@ -64,26 +84,52 @@ export function qdadmVitePlugin(options: QdadmVitePluginOptions = {}): Plugin {
|
|
|
64
84
|
// Symlinked install (file:/workspace link)? Allow serving from the
|
|
65
85
|
// link target, which typically lives outside the consumer's root.
|
|
66
86
|
const fsAllow: string[] = []
|
|
87
|
+
let symlinked = false
|
|
67
88
|
try {
|
|
68
89
|
const pkgDir = path.join(root, 'node_modules', '@quazardous', 'qdadm')
|
|
69
90
|
const real = fs.realpathSync(pkgDir)
|
|
70
91
|
if (real !== pkgDir) {
|
|
92
|
+
symlinked = true
|
|
71
93
|
fsAllow.push(searchForWorkspaceRoot(root), real)
|
|
72
94
|
}
|
|
73
95
|
} catch {
|
|
74
96
|
// not installed under root node_modules (aliased/hoisted) — nothing to allow
|
|
75
97
|
}
|
|
76
98
|
|
|
99
|
+
// Flavor aliasing (#1393): after the alias, no `primevue`/themes ids
|
|
100
|
+
// remain in the graph — dedupe/exclude lists must name the flavor.
|
|
101
|
+
const flavor = options.primevue ?? null
|
|
102
|
+
const widgetPkg = flavor?.package ?? 'primevue'
|
|
103
|
+
const themesPkg = flavor?.themes ?? '@primeuix/themes'
|
|
104
|
+
const alias = []
|
|
105
|
+
if (flavor) {
|
|
106
|
+
alias.push({ find: /^primevue(\/|$)/, replacement: `${flavor.package}$1` })
|
|
107
|
+
if (flavor.themes) {
|
|
108
|
+
alias.push({ find: /^@primeuix\/themes(\/|$)/, replacement: `${flavor.themes}$1` })
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// With a flavor, the original packages may still be installed — keep
|
|
113
|
+
// them excluded too, or the optimizer pre-bundles a stray
|
|
114
|
+
// `primevue/*` id that bypassed the alias (split-instance again).
|
|
115
|
+
const widgetExcludes = flavor ? [widgetPkg, 'primevue'] : [widgetPkg]
|
|
116
|
+
const themesExcludes =
|
|
117
|
+
flavor?.themes ? [themesPkg, '@primeuix/themes'] : [themesPkg]
|
|
118
|
+
|
|
77
119
|
return {
|
|
78
120
|
resolve: {
|
|
79
|
-
|
|
121
|
+
...(alias.length > 0 ? { alias } : {}),
|
|
122
|
+
dedupe: ['vue', 'vue-router', widgetPkg, 'pinia', ...(options.dedupe ?? [])],
|
|
80
123
|
},
|
|
81
124
|
optimizeDeps: {
|
|
82
|
-
exclude: [
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
125
|
+
exclude: [...widgetExcludes, ...themesExcludes, '@quazardous/qdadm'],
|
|
126
|
+
// One form per install mode: the nested path resolves through a
|
|
127
|
+
// real node_modules chain (npm installs), the plain form through a
|
|
128
|
+
// symlink realpath — declaring the unused one logs a "Failed to
|
|
129
|
+
// resolve dependency" warning (skybot testbed, #1389).
|
|
130
|
+
include: symlinked
|
|
131
|
+
? [...CJS_TRANSITIVES]
|
|
132
|
+
: CJS_TRANSITIVES.map((dep) => `@quazardous/qdadm > ${dep}`),
|
|
87
133
|
},
|
|
88
134
|
...(fsAllow.length > 0 ? { server: { fs: { allow: fsAllow } } } : {}),
|
|
89
135
|
}
|