@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 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
- | Doc | Purpose |
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
- ## Consuming via `file:` link from a sibling monorepo
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
- If you install qdadm with a `file:../path/to/qdadm` link instead of from npm — typically during local development on qdadm itself, or before a release lands — pnpm/npm resolves the symlink into the qdadm-monorepo's hoisted `node_modules`. That can pull a *second* copy of `primevue` (and `vue`, `vue-router`, `@primeuix/themes`) into your host bundle even though qdadm itself only declares them as peers.
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
- One more caveat: qdadm ships TypeScript sources, so with a `file:` link your `vue-tsc` / lint / test counters measure qdadm's **working tree**, not a released version. Any local qdadm edit moves your numbers with zero change on your side — don't calibrate quality ratchets or CI thresholds against a linked checkout; pin them to the published package your CI actually installs.
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.11.0",
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
- return dirtyFields.value.includes(fieldName)
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
 
@@ -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
- dedupe: ['vue', 'vue-router', 'primevue', 'pinia', ...(options.dedupe ?? [])],
121
+ ...(alias.length > 0 ? { alias } : {}),
122
+ dedupe: ['vue', 'vue-router', widgetPkg, 'pinia', ...(options.dedupe ?? [])],
80
123
  },
81
124
  optimizeDeps: {
82
- exclude: ['primevue', '@primeuix/themes', '@quazardous/qdadm'],
83
- include: [
84
- ...CJS_TRANSITIVES.map((dep) => `@quazardous/qdadm > ${dep}`),
85
- ...CJS_TRANSITIVES,
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
  }