@quazardous/qdadm 1.19.7 → 1.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/README.md CHANGED
@@ -31,7 +31,7 @@ import { SecurityChecker, PermissionMatcher, PermissionRegistry } from '@quazard
31
31
  import { PersistableRoleProvider, createLocalStorageRolesProvider } from '@quazardous/qdadm/security'
32
32
 
33
33
  // Composables
34
- import { useForm, useBareForm, useListPageBuilder } from '@quazardous/qdadm/composables'
34
+ import { useForm, useBareForm, useListPage } from '@quazardous/qdadm/composables'
35
35
 
36
36
  // Components
37
37
  import { ListPage, PageLayout, AppLayout, FormField, FormActions } from '@quazardous/qdadm/components'
@@ -96,8 +96,18 @@ Options: `transformRequest`, `transformResponse`, `responseFormat`, `clientSideP
96
96
  | Doc | Purpose |
97
97
  |-----|---------|
98
98
  | [QDADM_CREDO](QDADM_CREDO.md) | Philosophy, patterns, concepts |
99
+ | [Page compositions](../../docs/page-compositions.md) | **Start here** — which composition for which need |
100
+ | [CRUD Pages](../../docs/crud.md) | List / form / show / child pages reference |
99
101
  | [Architecture](../../docs/architecture.md) | PAC pattern, layers |
100
- | [Extension](../../docs/extension.md) | Hooks, signals, zones |
102
+ | [Extension](../../docs/extension.md) | Decorators, bundles, multi-storage |
103
+ | [Hooks](../../docs/hooks.md) | Alter / invoke registry |
104
+ | [Signals](../../docs/signals.md) | Event bus, SSE bridge |
105
+ | [Zones](../../docs/zones.md) | Extensible UI blocks |
106
+ | [Security](../../docs/security.md) | Permissions, roles |
107
+ | [i18n](../../docs/i18n.md) | Messages, providers, strategies |
108
+ | [Codegen](../../docs/gen.md) | Generate managers (OpenAPI / manual) |
109
+ | [Deferred](../../docs/deferred.md) | Deferred values & warmup |
110
+ | [AGENT_GUIDE](../../docs/AGENT_GUIDE.md) | Quick index for AI agents |
101
111
 
102
112
  ## Peer Dependencies
103
113
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quazardous/qdadm",
3
- "version": "1.19.7",
3
+ "version": "1.20.0",
4
4
  "description": "Vue 3 framework for admin dashboards with PrimeVue",
5
5
  "author": "quazardous",
6
6
  "license": "MIT",
@@ -36,6 +36,7 @@ export { default as LookupPickerDialog } from './edit/LookupPickerDialog.vue'
36
36
  export { default as ShowPage } from './show/ShowPage.vue'
37
37
  export { default as ShowField } from './show/ShowField.vue'
38
38
  export { default as ShowDisplay } from './show/ShowDisplay.vue'
39
+ export { default as ParentCard } from './show/ParentCard.vue'
39
40
 
40
41
  // Lists
41
42
  export { default as ListPage } from './lists/ListPage.vue'
@@ -0,0 +1,112 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * ParentCard - normalized parent detail embedded at the top of a child ListPage
4
+ *
5
+ * The "B2" hybrid composition (see docs/page-compositions.md): a child list is
6
+ * the host page, and the parent record is shown as a read-only cartouche above
7
+ * the table. `useListPage` already resolves and exposes the parent on a child
8
+ * route (`parentData` / `parentLoading`) — this component just renders it, with
9
+ * the SAME field resolver as `ShowPage` so the look matches real detail pages.
10
+ * No extra fetch.
11
+ *
12
+ * Fields are auto-derived from the parent entity's manager. Pass `fields` to
13
+ * restrict/order them, or use the default slot to render the parent yourself.
14
+ *
15
+ * Usage (inside a child ListPage):
16
+ * <template #beforeTable>
17
+ * <ParentCard
18
+ * :entity="'books'"
19
+ * :data="list.parentData.value"
20
+ * :loading="list.parentLoading.value"
21
+ * :fields="['title', 'author']"
22
+ * />
23
+ * </template>
24
+ */
25
+ import { computed, type PropType } from 'vue'
26
+ import { useOrchestrator } from '../../orchestrator/useOrchestrator'
27
+ import { useFieldManager } from '../../composables/useFieldManager'
28
+ import {
29
+ createShowFieldResolver,
30
+ type ShowResolvedFieldConfig,
31
+ } from '../../composables/createShowFieldResolver'
32
+ import ShowField from './ShowField.vue'
33
+
34
+ const props = defineProps({
35
+ /** Parent entity name (route-stable). */
36
+ entity: { type: String, required: true },
37
+ /** Parent record (e.g. `list.parentData.value`). */
38
+ data: { type: Object as PropType<Record<string, unknown> | null>, default: null },
39
+ /** Parent loading state (e.g. `list.parentLoading.value`). */
40
+ loading: { type: Boolean, default: false },
41
+ /** Restrict/order the displayed fields. Omit to show all manager fields. */
42
+ fields: { type: Array as PropType<string[] | null>, default: null },
43
+ /** Exclude these fields (applied when `fields` is not set). */
44
+ exclude: { type: Array as PropType<string[]>, default: () => [] },
45
+ /** Render fields horizontally (label beside value). Default true. */
46
+ horizontal: { type: Boolean, default: true },
47
+ /** Optional heading above the fields. */
48
+ title: { type: String, default: '' },
49
+ })
50
+
51
+ const { orchestrator, getManager } = useOrchestrator()
52
+ const manager = getManager(props.entity)
53
+
54
+ // Same resolver as ShowPage → identical rendering (types, references, severity).
55
+ const fieldManager = useFieldManager<ShowResolvedFieldConfig>({
56
+ resolveFieldConfig: createShowFieldResolver(manager, orchestrator),
57
+ getSchemaFieldConfig: (name) => manager.getFieldConfig?.(name) || null,
58
+ entity: props.entity,
59
+ })
60
+
61
+ fieldManager.generateFields(manager.getFormFields?.() || [], {
62
+ only: props.fields ?? null,
63
+ exclude: props.fields ? [] : props.exclude,
64
+ })
65
+ // Honor the caller's field order when an explicit list is given.
66
+ if (props.fields) fieldManager.setFieldOrder(props.fields)
67
+
68
+ const resolvedFields = fieldManager.fields
69
+ const hasData = computed(() => props.data !== null && props.data !== undefined)
70
+ </script>
71
+
72
+ <template>
73
+ <div class="parent-card">
74
+ <div v-if="loading" class="parent-card__loading">
75
+ <i class="pi pi-spin pi-spinner" />
76
+ </div>
77
+
78
+ <template v-else-if="hasData">
79
+ <div v-if="title" class="parent-card__title">{{ title }}</div>
80
+ <!-- Escape hatch: render the parent yourself with the resolved fields. -->
81
+ <slot :fields="resolvedFields" :data="data">
82
+ <div class="parent-card__fields">
83
+ <ShowField
84
+ v-for="f in resolvedFields"
85
+ :key="f.name"
86
+ :field="f"
87
+ :value="(data?.[f.name] ?? null) as any"
88
+ :horizontal="horizontal"
89
+ />
90
+ </div>
91
+ </slot>
92
+ </template>
93
+ </div>
94
+ </template>
95
+
96
+ <style scoped>
97
+ .parent-card {
98
+ margin-bottom: 1rem;
99
+ }
100
+ .parent-card__loading {
101
+ padding: 0.5rem 0;
102
+ }
103
+ .parent-card__title {
104
+ font-weight: 600;
105
+ margin-bottom: 0.5rem;
106
+ }
107
+ .parent-card__fields {
108
+ display: flex;
109
+ flex-direction: column;
110
+ gap: 0.25rem;
111
+ }
112
+ </style>
@@ -0,0 +1,170 @@
1
+ /**
2
+ * createShowFieldResolver - shared read-only field resolver
3
+ *
4
+ * Factory producing the field resolver used by read-only display surfaces
5
+ * (`useEntityItemShowPage`, `<ParentCard>`). Given an entity manager and the
6
+ * orchestrator, it maps a raw schema field config to a resolved display config:
7
+ *
8
+ * - schema type → display type (via {@link SHOW_TYPE_MAPPINGS})
9
+ * - auto reference route when a field points at another entity
10
+ * - auto severity (badge) from the manager's severity maps
11
+ *
12
+ * Extracted so the parent cartouche of an embedded child list (#1038) renders
13
+ * fields exactly like a real `ShowPage` — same resolver, same look — without
14
+ * duplicating the mapping logic.
15
+ */
16
+ import { snakeCaseToTitle } from './useFieldManager'
17
+
18
+ /** Severity descriptor returned by a manager's severity maps. */
19
+ type SeverityDescriptor = string | { severity: string; icon?: string; label?: string }
20
+
21
+ /**
22
+ * Minimal manager surface the resolver needs — kept structural (not the full
23
+ * generic `EntityManager`) so it accepts any concrete manager without variance
24
+ * friction.
25
+ */
26
+ export interface ShowResolverManager {
27
+ routePrefix?: string
28
+ idField: string
29
+ hasSeverityMap?: (name: string) => boolean
30
+ getSeverityDescriptor?: (name: string, value: string | number, fallback: string) => SeverityDescriptor
31
+ getSeverity?: (name: string, value: string | number, fallback: string) => SeverityDescriptor
32
+ }
33
+
34
+ /**
35
+ * Field definition from an EntityManager schema.
36
+ */
37
+ export interface ShowFieldDefinition {
38
+ name: string
39
+ type?: string
40
+ schemaType?: string
41
+ label?: string
42
+ reference?: string
43
+ referenceRoute?: string | ((value: unknown) => { name: string; params: Record<string, unknown> })
44
+ referenceLabel?: string | ((value: unknown, option?: unknown) => string)
45
+ options?: unknown[]
46
+ optionLabel?: string
47
+ optionValue?: string
48
+ // Display options
49
+ dateFormat?: string
50
+ currencyCode?: string
51
+ locale?: string
52
+ booleanLabels?: { true: string; false: string }
53
+ severity?: string | ((value: unknown) => string | { severity: string; icon?: string; label?: string })
54
+ imageWidth?: string
55
+ imageHeight?: string
56
+ render?: (value: unknown) => string
57
+ [key: string]: unknown
58
+ }
59
+
60
+ /**
61
+ * Resolved field configuration for display rendering.
62
+ */
63
+ export interface ShowResolvedFieldConfig extends ShowFieldDefinition {
64
+ name: string
65
+ type: string
66
+ schemaType: string
67
+ label: string
68
+ group?: string
69
+ }
70
+
71
+ /**
72
+ * Minimal orchestrator surface the resolver needs (reference-route lookup).
73
+ */
74
+ export interface ShowResolverOrchestrator {
75
+ get: (entityName: string) => ShowResolverManager | undefined
76
+ }
77
+
78
+ /**
79
+ * Schema type → display type mapping.
80
+ */
81
+ export const SHOW_TYPE_MAPPINGS: Record<string, string> = {
82
+ text: 'text',
83
+ string: 'text',
84
+ email: 'email',
85
+ password: 'password',
86
+ number: 'number',
87
+ integer: 'number',
88
+ float: 'number',
89
+ decimal: 'number',
90
+ currency: 'currency',
91
+ boolean: 'boolean',
92
+ bool: 'boolean',
93
+ date: 'date',
94
+ datetime: 'datetime',
95
+ time: 'datetime',
96
+ timestamp: 'datetime',
97
+ select: 'select',
98
+ enum: 'select',
99
+ textarea: 'textarea',
100
+ longtext: 'textarea',
101
+ reference: 'reference',
102
+ foreignKey: 'reference',
103
+ image: 'image',
104
+ url: 'url',
105
+ link: 'url',
106
+ json: 'json',
107
+ object: 'json',
108
+ array: 'json',
109
+ badge: 'badge',
110
+ tag: 'badge',
111
+ }
112
+
113
+ /**
114
+ * Build a show-style field resolver bound to a manager + orchestrator.
115
+ */
116
+ export function createShowFieldResolver(
117
+ manager: ShowResolverManager,
118
+ orchestrator: ShowResolverOrchestrator
119
+ ): (name: string, fieldConfig: Partial<ShowFieldDefinition>) => ShowResolvedFieldConfig {
120
+ return function resolveFieldConfig(
121
+ name: string,
122
+ fieldConfig: Partial<ShowFieldDefinition>
123
+ ): ShowResolvedFieldConfig {
124
+ const {
125
+ type = 'text',
126
+ schemaType = type,
127
+ label = '',
128
+ reference = '',
129
+ severity,
130
+ ...rest
131
+ } = fieldConfig
132
+
133
+ let resolvedDisplayType = SHOW_TYPE_MAPPINGS[type] || SHOW_TYPE_MAPPINGS[schemaType] || 'text'
134
+ const resolvedLabel = label || snakeCaseToTitle(name)
135
+
136
+ // Auto-set reference route if reference entity is specified
137
+ let referenceRoute = rest.referenceRoute
138
+ if (reference && !referenceRoute) {
139
+ const refManager = orchestrator.get(reference)
140
+ if (refManager) {
141
+ referenceRoute = (value: unknown) => ({
142
+ name: `${refManager.routePrefix || reference}-show`,
143
+ params: { [refManager.idField]: value },
144
+ })
145
+ }
146
+ }
147
+
148
+ // Auto-inject severity from manager's severity maps
149
+ let resolvedSeverity = severity
150
+ if (!resolvedSeverity && manager.hasSeverityMap?.(name)) {
151
+ resolvedSeverity = manager.getSeverityDescriptor
152
+ ? (value: unknown) => manager.getSeverityDescriptor!(name, value as string | number, 'secondary')
153
+ : (value: unknown) => manager.getSeverity!(name, value as string | number, 'secondary')
154
+ if (resolvedDisplayType === 'text') {
155
+ resolvedDisplayType = 'badge'
156
+ }
157
+ }
158
+
159
+ return {
160
+ name,
161
+ type: resolvedDisplayType,
162
+ schemaType,
163
+ label: resolvedLabel,
164
+ reference,
165
+ referenceRoute,
166
+ severity: resolvedSeverity,
167
+ ...rest,
168
+ }
169
+ }
170
+ }
@@ -33,24 +33,27 @@
33
33
  * <ShowPage v-bind="show.props.value" v-on="show.events" />
34
34
  * ```
35
35
  */
36
- import { ref, computed, watch, type Ref, type ComputedRef } from 'vue'
37
- import { useRouter, useRoute, type Router } from 'vue-router'
36
+ import { ref, computed, type Ref, type ComputedRef } from 'vue'
37
+ import { useRouter, type Router } from 'vue-router'
38
38
  import { useConfirm } from 'primevue/useconfirm'
39
39
  import {
40
40
  useEntityItemPage,
41
41
  type ParentConfig,
42
42
  type EntityManager,
43
- type UseEntityItemPageReturn,
44
43
  } from './useEntityItemPage'
45
44
  import type { StackHydratorReturn } from '../chain/useStackHydrator'
46
45
  import {
47
46
  useFieldManager,
48
- snakeCaseToTitle,
49
47
  type FieldGroup,
50
48
  type GroupDefinition,
51
49
  type GroupOptions,
52
50
  type AddFieldOptions,
53
51
  } from './useFieldManager'
52
+ import {
53
+ createShowFieldResolver,
54
+ type ShowFieldDefinition,
55
+ type ShowResolvedFieldConfig,
56
+ } from './createShowFieldResolver'
54
57
 
55
58
  /**
56
59
  * Orchestrator interface
@@ -66,41 +69,16 @@ interface Orchestrator {
66
69
  }
67
70
 
68
71
  /**
69
- * Field definition from EntityManager schema
72
+ * Field definition from EntityManager schema.
73
+ * Re-exported from the shared resolver under the historical name.
70
74
  */
71
- export interface FieldDefinition {
72
- name: string
73
- type?: string
74
- schemaType?: string
75
- label?: string
76
- reference?: string
77
- referenceRoute?: string | ((value: unknown) => { name: string; params: Record<string, unknown> })
78
- referenceLabel?: string | ((value: unknown, option?: unknown) => string)
79
- options?: unknown[]
80
- optionLabel?: string
81
- optionValue?: string
82
- // Display options
83
- dateFormat?: string
84
- currencyCode?: string
85
- locale?: string
86
- booleanLabels?: { true: string; false: string }
87
- severity?: string | ((value: unknown) => string | { severity: string; icon?: string; label?: string })
88
- imageWidth?: string
89
- imageHeight?: string
90
- render?: (value: unknown) => string
91
- [key: string]: unknown
92
- }
75
+ export type FieldDefinition = ShowFieldDefinition
93
76
 
94
77
  /**
95
- * Resolved field configuration for display rendering
78
+ * Resolved field configuration for display rendering.
79
+ * Re-exported from the shared resolver under the historical name.
96
80
  */
97
- export interface ResolvedFieldConfig extends FieldDefinition {
98
- name: string
99
- type: string
100
- schemaType: string
101
- label: string
102
- group?: string
103
- }
81
+ export type ResolvedFieldConfig = ShowResolvedFieldConfig
104
82
 
105
83
  // FieldGroup, GroupDefinition, GroupOptions imported from useFieldManager
106
84
 
@@ -176,43 +154,6 @@ export interface ShowPageEvents {
176
154
  back: () => void
177
155
  }
178
156
 
179
- /**
180
- * Type mapping from schema types to display types
181
- */
182
- const TYPE_MAPPINGS: Record<string, string> = {
183
- text: 'text',
184
- string: 'text',
185
- email: 'email',
186
- password: 'password',
187
- number: 'number',
188
- integer: 'number',
189
- float: 'number',
190
- decimal: 'number',
191
- currency: 'currency',
192
- boolean: 'boolean',
193
- bool: 'boolean',
194
- date: 'date',
195
- datetime: 'datetime',
196
- time: 'datetime',
197
- timestamp: 'datetime',
198
- select: 'select',
199
- enum: 'select',
200
- textarea: 'textarea',
201
- longtext: 'textarea',
202
- reference: 'reference',
203
- foreignKey: 'reference',
204
- image: 'image',
205
- url: 'url',
206
- link: 'url',
207
- json: 'json',
208
- object: 'json',
209
- array: 'json',
210
- badge: 'badge',
211
- tag: 'badge',
212
- }
213
-
214
- // snakeCaseToTitle imported from useFieldManager
215
-
216
157
  /**
217
158
  * Options for useEntityItemShowPage
218
159
  */
@@ -327,7 +268,6 @@ export function useEntityItemShowPage<T = unknown>(
327
268
  const { entity, loadOnMount = true, transformLoad, onLoadSuccess, onLoadError } = options
328
269
 
329
270
  const router: Router = useRouter()
330
- const route = useRoute()
331
271
  const confirm = useConfirm() as ConfirmService
332
272
 
333
273
  // Lazy action state (declared before base so onLoadSuccess wrapper can reference it)
@@ -376,56 +316,8 @@ export function useEntityItemShowPage<T = unknown>(
376
316
 
377
317
  // ============ FIELD & GROUP MANAGEMENT (via useFieldManager) ============
378
318
 
379
- /**
380
- * Show-specific field resolver with reference route auto-detection
381
- */
382
- function resolveFieldConfig(name: string, fieldConfig: Partial<FieldDefinition>): ResolvedFieldConfig {
383
- const {
384
- type = 'text',
385
- schemaType = type,
386
- label = '',
387
- reference = '',
388
- severity,
389
- ...rest
390
- } = fieldConfig
391
-
392
- let resolvedDisplayType = TYPE_MAPPINGS[type] || TYPE_MAPPINGS[schemaType] || 'text'
393
- const resolvedLabel = label || snakeCaseToTitle(name)
394
-
395
- // Auto-set reference route if reference entity is specified
396
- let referenceRoute = rest.referenceRoute
397
- if (reference && !referenceRoute) {
398
- const refManager = orchestrator.get(reference)
399
- if (refManager) {
400
- referenceRoute = (value: unknown) => ({
401
- name: `${refManager.routePrefix || reference}-show`,
402
- params: { [refManager.idField]: value },
403
- })
404
- }
405
- }
406
-
407
- // Auto-inject severity from manager's severity maps
408
- let resolvedSeverity = severity
409
- if (!resolvedSeverity && manager.hasSeverityMap?.(name)) {
410
- resolvedSeverity = manager.getSeverityDescriptor
411
- ? (value: unknown) => manager.getSeverityDescriptor!(name, value as string | number, 'secondary')
412
- : (value: unknown) => manager.getSeverity!(name, value as string | number, 'secondary')
413
- if (resolvedDisplayType === 'text') {
414
- resolvedDisplayType = 'badge'
415
- }
416
- }
417
-
418
- return {
419
- name,
420
- type: resolvedDisplayType,
421
- schemaType,
422
- label: resolvedLabel,
423
- reference,
424
- referenceRoute,
425
- severity: resolvedSeverity,
426
- ...rest,
427
- }
428
- }
319
+ // Show-specific field resolver (shared with <ParentCard>, see #1038)
320
+ const resolveFieldConfig = createShowFieldResolver(manager, orchestrator)
429
321
 
430
322
  // Create field manager with show-specific resolver
431
323
  const fieldManager = useFieldManager<ResolvedFieldConfig>({