qdadm 0.30.0 → 0.32.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 -1
- package/src/components/forms/FormPage.vue +1 -1
- package/src/components/layout/AppLayout.vue +13 -1
- package/src/components/layout/Zone.vue +40 -23
- package/src/composables/index.js +1 -0
- package/src/composables/useAuth.js +44 -4
- package/src/composables/useCurrentEntity.js +44 -0
- package/src/composables/useFormPageBuilder.js +3 -3
- package/src/composables/useNavContext.js +24 -8
- package/src/debug/AuthCollector.js +340 -0
- package/src/debug/Collector.js +235 -0
- package/src/debug/DebugBridge.js +163 -0
- package/src/debug/DebugModule.js +215 -0
- package/src/debug/EntitiesCollector.js +403 -0
- package/src/debug/ErrorCollector.js +66 -0
- package/src/debug/LocalStorageAdapter.js +150 -0
- package/src/debug/SignalCollector.js +87 -0
- package/src/debug/ToastCollector.js +82 -0
- package/src/debug/ZonesCollector.js +300 -0
- package/src/debug/components/DebugBar.vue +1232 -0
- package/src/debug/components/ObjectTree.vue +194 -0
- package/src/debug/components/index.js +8 -0
- package/src/debug/components/panels/AuthPanel.vue +174 -0
- package/src/debug/components/panels/EntitiesPanel.vue +712 -0
- package/src/debug/components/panels/EntriesPanel.vue +188 -0
- package/src/debug/components/panels/ToastsPanel.vue +112 -0
- package/src/debug/components/panels/ZonesPanel.vue +232 -0
- package/src/debug/components/panels/index.js +8 -0
- package/src/debug/index.js +31 -0
- package/src/entity/EntityManager.js +142 -20
- package/src/entity/auth/CompositeAuthAdapter.js +212 -0
- package/src/entity/auth/factory.js +207 -0
- package/src/entity/auth/factory.test.js +257 -0
- package/src/entity/auth/index.js +14 -0
- package/src/entity/storage/MockApiStorage.js +51 -2
- package/src/entity/storage/index.js +9 -2
- package/src/index.js +7 -0
- package/src/kernel/Kernel.js +468 -48
- package/src/kernel/KernelContext.js +385 -0
- package/src/kernel/Module.js +111 -0
- package/src/kernel/ModuleLoader.js +573 -0
- package/src/kernel/SignalBus.js +2 -7
- package/src/kernel/index.js +14 -0
- package/src/toast/ToastBridgeModule.js +70 -0
- package/src/toast/ToastListener.vue +47 -0
- package/src/toast/index.js +15 -0
- package/src/toast/useSignalToast.js +113 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "qdadm",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.32.0",
|
|
4
4
|
"description": "Vue 3 framework for admin dashboards with PrimeVue",
|
|
5
5
|
"author": "quazardous",
|
|
6
6
|
"license": "MIT",
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
"./editors": "./src/editors/index.js",
|
|
26
26
|
"./module": "./src/module/index.js",
|
|
27
27
|
"./utils": "./src/utils/index.js",
|
|
28
|
+
"./debug": "./src/debug/index.js",
|
|
28
29
|
"./styles": "./src/styles/index.scss",
|
|
29
30
|
"./styles/breakpoints": "./src/styles/_breakpoints.scss",
|
|
30
31
|
"./gen": "./src/gen/index.js",
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* form.generateFields()
|
|
17
17
|
* form.addSaveAction()
|
|
18
18
|
*
|
|
19
|
-
* <FormPage v-bind="form.props" v-on="form.events">
|
|
19
|
+
* <FormPage v-bind="form.props.value" v-on="form.events">
|
|
20
20
|
* <template #fields>
|
|
21
21
|
* <FormField v-model="form.data.title" name="title" />
|
|
22
22
|
* </template>
|
|
@@ -157,8 +157,20 @@ function handleLogout() {
|
|
|
157
157
|
const slots = useSlots()
|
|
158
158
|
const hasSlotContent = computed(() => !!slots.default)
|
|
159
159
|
|
|
160
|
+
// Current page entity data - pages can inject and set this to avoid double fetch
|
|
161
|
+
// When a page loads an entity, it sets this ref, and useNavContext uses it for breadcrumb
|
|
162
|
+
const currentEntityData = ref(null)
|
|
163
|
+
provide('qdadmCurrentEntityData', currentEntityData)
|
|
164
|
+
|
|
165
|
+
// Clear entity data on route change (before new page mounts)
|
|
166
|
+
watch(() => route.fullPath, () => {
|
|
167
|
+
currentEntityData.value = null
|
|
168
|
+
})
|
|
169
|
+
|
|
160
170
|
// Navigation context (breadcrumb + navlinks from route config)
|
|
161
|
-
const { breadcrumb: defaultBreadcrumb, navlinks: defaultNavlinks } = useNavContext(
|
|
171
|
+
const { breadcrumb: defaultBreadcrumb, navlinks: defaultNavlinks } = useNavContext({
|
|
172
|
+
entityData: currentEntityData
|
|
173
|
+
})
|
|
162
174
|
|
|
163
175
|
// Allow child pages to override breadcrumb/navlinks via provide/inject
|
|
164
176
|
const breadcrumbOverride = ref(null)
|
|
@@ -87,16 +87,22 @@ const defaultComp = computed(() => {
|
|
|
87
87
|
* This creates a functional component that builds the nested structure
|
|
88
88
|
* once per block, avoiding the infinite re-render issue.
|
|
89
89
|
*
|
|
90
|
+
* Slot content from the Zone is passed to the innermost block component,
|
|
91
|
+
* allowing the block to render Zone's children (e.g., form fields).
|
|
92
|
+
*
|
|
90
93
|
* @param {object} block - Block config with wrappers array
|
|
91
94
|
* @param {object} mergedProps - Props to pass to the innermost component
|
|
95
|
+
* @param {Function} slotFn - Slot function to pass to innermost component
|
|
92
96
|
* @returns {object} - Vue component definition
|
|
93
97
|
*/
|
|
94
|
-
function createWrappedComponent(block, mergedProps) {
|
|
98
|
+
function createWrappedComponent(block, mergedProps, slotFn) {
|
|
95
99
|
return defineComponent({
|
|
96
100
|
name: 'WrappedBlock',
|
|
97
101
|
render() {
|
|
98
102
|
// Start with the innermost component (the original block)
|
|
99
|
-
|
|
103
|
+
// Pass Zone's slot content to allow block to render it
|
|
104
|
+
const innerSlots = slotFn ? { default: slotFn } : {}
|
|
105
|
+
let current = h(block.component, mergedProps, innerSlots)
|
|
100
106
|
|
|
101
107
|
// Build from inside out: last wrapper in array wraps the innermost
|
|
102
108
|
// Wrappers are already sorted by weight (lower = outer)
|
|
@@ -118,14 +124,19 @@ function createWrappedComponent(block, mergedProps) {
|
|
|
118
124
|
/**
|
|
119
125
|
* Computed map of wrapped block components
|
|
120
126
|
* Memoizes the wrapped components to avoid re-creating them on every render
|
|
127
|
+
*
|
|
128
|
+
* Note: Slot function is passed to allow Zone's children to be rendered
|
|
129
|
+
* inside the innermost block component.
|
|
121
130
|
*/
|
|
122
131
|
const wrappedComponents = computed(() => {
|
|
123
132
|
const map = new Map()
|
|
133
|
+
// Get slot function to pass to innermost block
|
|
134
|
+
const slotFn = slots.default
|
|
124
135
|
for (const block of blocks.value) {
|
|
125
136
|
if (block.wrappers && block.wrappers.length > 0) {
|
|
126
137
|
const key = block.id || block.weight
|
|
127
138
|
const mergedProps = { ...props.blockProps, ...block.props }
|
|
128
|
-
map.set(key, createWrappedComponent(block, mergedProps))
|
|
139
|
+
map.set(key, createWrappedComponent(block, mergedProps, slotFn))
|
|
129
140
|
}
|
|
130
141
|
}
|
|
131
142
|
return map
|
|
@@ -141,25 +152,31 @@ function getWrappedComponent(block) {
|
|
|
141
152
|
</script>
|
|
142
153
|
|
|
143
154
|
<template>
|
|
144
|
-
<
|
|
145
|
-
<template v-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
155
|
+
<div :data-zone="name" class="qdadm-zone">
|
|
156
|
+
<template v-if="hasBlocks">
|
|
157
|
+
<template v-for="block in blocks" :key="block.id || block.weight">
|
|
158
|
+
<!-- Render wrapped blocks using memoized component -->
|
|
159
|
+
<component
|
|
160
|
+
v-if="block.wrappers"
|
|
161
|
+
:is="getWrappedComponent(block)"
|
|
162
|
+
/>
|
|
163
|
+
<!-- Render simple blocks directly, passing Zone's slot content -->
|
|
164
|
+
<component
|
|
165
|
+
v-else
|
|
166
|
+
:is="block.component"
|
|
167
|
+
v-bind="{ ...blockProps, ...block.props }"
|
|
168
|
+
>
|
|
169
|
+
<slot />
|
|
170
|
+
</component>
|
|
171
|
+
</template>
|
|
157
172
|
</template>
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
173
|
+
<component
|
|
174
|
+
v-else-if="showDefault"
|
|
175
|
+
:is="defaultComp"
|
|
176
|
+
v-bind="blockProps"
|
|
177
|
+
>
|
|
178
|
+
<slot />
|
|
179
|
+
</component>
|
|
180
|
+
<slot v-else-if="showSlot" />
|
|
181
|
+
</div>
|
|
165
182
|
</template>
|
package/src/composables/index.js
CHANGED
|
@@ -12,6 +12,7 @@ export { useListPageBuilder, PAGE_SIZE_OPTIONS } from './useListPageBuilder'
|
|
|
12
12
|
export { usePageTitle } from './usePageTitle'
|
|
13
13
|
export { useApp } from './useApp'
|
|
14
14
|
export { useAuth } from './useAuth'
|
|
15
|
+
export { useCurrentEntity } from './useCurrentEntity'
|
|
15
16
|
export { useNavContext } from './useNavContext'
|
|
16
17
|
export { useNavigation } from './useNavigation'
|
|
17
18
|
export { useUnsavedChangesGuard } from './useUnsavedChangesGuard'
|
|
@@ -1,9 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* useAuth - Access authentication state
|
|
2
|
+
* useAuth - Access authentication state (component-only)
|
|
3
3
|
*
|
|
4
4
|
* Provides access to authAdapter set via createQdadm bootstrap.
|
|
5
5
|
* Returns neutral values if auth is disabled.
|
|
6
6
|
*
|
|
7
|
+
* IMPORTANT: Must be called within component setup() function.
|
|
8
|
+
* For route guards or services, use authAdapter directly.
|
|
9
|
+
*
|
|
10
|
+
* Reactivity:
|
|
11
|
+
* - Listens to auth:login, auth:logout, auth:impersonate, auth:impersonate:stop signals
|
|
12
|
+
* - User computed re-evaluates when these signals fire
|
|
13
|
+
* - No polling or manual refresh needed
|
|
14
|
+
*
|
|
7
15
|
* Note: Permission checking (canRead/canWrite) is handled by EntityManager,
|
|
8
16
|
* not by useAuth. This keeps auth simple and delegates permission logic
|
|
9
17
|
* to where it belongs (the entity layer).
|
|
@@ -12,11 +20,18 @@
|
|
|
12
20
|
* const { isAuthenticated, user, logout } = useAuth()
|
|
13
21
|
*/
|
|
14
22
|
|
|
15
|
-
import { inject, computed, ref } from 'vue'
|
|
23
|
+
import { inject, computed, ref, onUnmounted, getCurrentInstance } from 'vue'
|
|
16
24
|
|
|
17
25
|
export function useAuth() {
|
|
26
|
+
// Strict context check - must be called in component setup()
|
|
27
|
+
const instance = getCurrentInstance()
|
|
28
|
+
if (!instance) {
|
|
29
|
+
throw new Error('[qdadm] useAuth() must be called within component setup()')
|
|
30
|
+
}
|
|
31
|
+
|
|
18
32
|
const auth = inject('authAdapter')
|
|
19
33
|
const features = inject('qdadmFeatures')
|
|
34
|
+
const signals = inject('qdadmSignals', null)
|
|
20
35
|
|
|
21
36
|
// If auth disabled or not provided, return neutral values
|
|
22
37
|
if (!features?.auth || !auth) {
|
|
@@ -30,8 +45,28 @@ export function useAuth() {
|
|
|
30
45
|
}
|
|
31
46
|
}
|
|
32
47
|
|
|
33
|
-
// Reactive
|
|
48
|
+
// Reactive trigger - incremented on auth signals to force computed re-evaluation
|
|
49
|
+
const authTick = ref(0)
|
|
50
|
+
|
|
51
|
+
// Subscribe to auth signals for reactivity
|
|
52
|
+
const cleanups = []
|
|
53
|
+
if (signals) {
|
|
54
|
+
cleanups.push(signals.on('auth:login', () => { authTick.value++ }))
|
|
55
|
+
cleanups.push(signals.on('auth:logout', () => { authTick.value++ }))
|
|
56
|
+
cleanups.push(signals.on('auth:impersonate', () => { authTick.value++ }))
|
|
57
|
+
cleanups.push(signals.on('auth:impersonate:stop', () => { authTick.value++ }))
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Cleanup signal subscriptions on unmount
|
|
61
|
+
onUnmounted(() => {
|
|
62
|
+
cleanups.forEach(cleanup => cleanup())
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
// Reactive user state - re-evaluates when authTick changes
|
|
34
66
|
const user = computed(() => {
|
|
67
|
+
// Touch authTick to create reactive dependency
|
|
68
|
+
// eslint-disable-next-line no-unused-expressions
|
|
69
|
+
authTick.value
|
|
35
70
|
if (typeof auth.getUser === 'function') {
|
|
36
71
|
return auth.getUser()
|
|
37
72
|
}
|
|
@@ -42,7 +77,12 @@ export function useAuth() {
|
|
|
42
77
|
login: auth.login,
|
|
43
78
|
logout: auth.logout,
|
|
44
79
|
getCurrentUser: auth.getCurrentUser,
|
|
45
|
-
isAuthenticated: computed(() =>
|
|
80
|
+
isAuthenticated: computed(() => {
|
|
81
|
+
// Touch authTick for reactivity
|
|
82
|
+
// eslint-disable-next-line no-unused-expressions
|
|
83
|
+
authTick.value
|
|
84
|
+
return auth.isAuthenticated()
|
|
85
|
+
}),
|
|
46
86
|
user,
|
|
47
87
|
authEnabled: true
|
|
48
88
|
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useCurrentEntity - Share page entity data with navigation context
|
|
3
|
+
*
|
|
4
|
+
* When a page loads an entity (e.g., ProductDetailPage fetches a product),
|
|
5
|
+
* it can call setCurrentEntity() to share that data with the navigation context.
|
|
6
|
+
* This avoids a second fetch for breadcrumb display.
|
|
7
|
+
*
|
|
8
|
+
* Usage in a detail page:
|
|
9
|
+
* ```js
|
|
10
|
+
* const { setCurrentEntity } = useCurrentEntity()
|
|
11
|
+
*
|
|
12
|
+
* async function loadProduct() {
|
|
13
|
+
* product.value = await productsManager.get(productId)
|
|
14
|
+
* setCurrentEntity(product.value) // Share with navigation
|
|
15
|
+
* }
|
|
16
|
+
* ```
|
|
17
|
+
*
|
|
18
|
+
* The navigation context (useNavContext) will use this data instead of
|
|
19
|
+
* fetching the entity again for the breadcrumb.
|
|
20
|
+
*/
|
|
21
|
+
import { inject } from 'vue'
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Composable to share current page entity with navigation context
|
|
25
|
+
* @returns {{ setCurrentEntity: (data: object) => void }}
|
|
26
|
+
*/
|
|
27
|
+
export function useCurrentEntity() {
|
|
28
|
+
const currentEntityData = inject('qdadmCurrentEntityData', null)
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Set the current entity data for navigation context
|
|
32
|
+
* Call this after loading an entity to avoid double fetch
|
|
33
|
+
* @param {object} data - Entity data
|
|
34
|
+
*/
|
|
35
|
+
function setCurrentEntity(data) {
|
|
36
|
+
if (currentEntityData) {
|
|
37
|
+
currentEntityData.value = data
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
setCurrentEntity
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* form.addSaveAction()
|
|
18
18
|
* form.addDeleteAction()
|
|
19
19
|
*
|
|
20
|
-
* <FormPage v-bind="form.props" v-on="form.events">
|
|
20
|
+
* <FormPage v-bind="form.props.value" v-on="form.events">
|
|
21
21
|
* <template #fields>
|
|
22
22
|
* <FormField v-model="form.data.title" name="title" />
|
|
23
23
|
* </template>
|
|
@@ -1011,7 +1011,7 @@ export function useFormPageBuilder(config = {}) {
|
|
|
1011
1011
|
|
|
1012
1012
|
/**
|
|
1013
1013
|
* Props object for FormPage component
|
|
1014
|
-
* Use with v-bind: <FormPage v-bind="form.props">
|
|
1014
|
+
* Use with v-bind: <FormPage v-bind="form.props.value">
|
|
1015
1015
|
*/
|
|
1016
1016
|
const formProps = computed(() => ({
|
|
1017
1017
|
// Mode
|
|
@@ -1049,7 +1049,7 @@ export function useFormPageBuilder(config = {}) {
|
|
|
1049
1049
|
|
|
1050
1050
|
/**
|
|
1051
1051
|
* Event handlers for FormPage
|
|
1052
|
-
* Use with v-on: <FormPage v-bind="form.props" v-on="form.events">
|
|
1052
|
+
* Use with v-on: <FormPage v-bind="form.props.value" v-on="form.events">
|
|
1053
1053
|
*/
|
|
1054
1054
|
const formEvents = {
|
|
1055
1055
|
save: () => submit(false),
|
|
@@ -207,30 +207,46 @@ export function useNavContext(options = {}) {
|
|
|
207
207
|
|
|
208
208
|
/**
|
|
209
209
|
* Fetch entity data for all 'item' segments in the chain
|
|
210
|
+
*
|
|
211
|
+
* For the LAST item (current entity):
|
|
212
|
+
* - Uses entityData if provided by the page via setCurrentEntity()
|
|
213
|
+
* - Does NOT fetch automatically - page is responsible for providing data
|
|
214
|
+
* - Breadcrumb shows "..." until page provides the data
|
|
215
|
+
*
|
|
216
|
+
* For PARENT items: always fetches from manager
|
|
210
217
|
*/
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
218
|
+
// Watch navChain and entityData ref (deep watch to catch value changes)
|
|
219
|
+
const entityDataRef = computed(() => options.entityData ? unref(options.entityData) : null)
|
|
220
|
+
watch([navChain, entityDataRef], async ([chain, externalData]) => {
|
|
221
|
+
// Build new Map (reassignment triggers Vue reactivity, Map.set() doesn't)
|
|
222
|
+
const newChainData = new Map()
|
|
214
223
|
|
|
215
224
|
for (let i = 0; i < chain.length; i++) {
|
|
216
225
|
const segment = chain[i]
|
|
217
226
|
if (segment.type !== 'item') continue
|
|
218
227
|
|
|
219
|
-
// For the last item, use external data if provided (from useForm)
|
|
220
228
|
const isLastItem = !chain.slice(i + 1).some(s => s.type === 'item')
|
|
221
|
-
|
|
222
|
-
|
|
229
|
+
|
|
230
|
+
// For the last item, use external data from page (don't fetch)
|
|
231
|
+
if (isLastItem) {
|
|
232
|
+
if (externalData) {
|
|
233
|
+
newChainData.set(i, externalData)
|
|
234
|
+
}
|
|
235
|
+
// If no externalData, breadcrumb will show "..." until page provides it
|
|
223
236
|
continue
|
|
224
237
|
}
|
|
225
238
|
|
|
226
|
-
//
|
|
239
|
+
// For parent items, fetch from manager
|
|
227
240
|
try {
|
|
228
241
|
const data = await segment.manager.get(segment.id)
|
|
229
|
-
|
|
242
|
+
newChainData.set(i, data)
|
|
230
243
|
} catch (e) {
|
|
231
244
|
console.warn(`[useNavContext] Failed to fetch ${segment.entity}:${segment.id}`, e)
|
|
232
245
|
}
|
|
233
246
|
}
|
|
247
|
+
|
|
248
|
+
// Assign new Map to trigger reactivity
|
|
249
|
+
chainData.value = newChainData
|
|
234
250
|
}, { immediate: true, deep: true })
|
|
235
251
|
|
|
236
252
|
// ============================================================================
|