@quazardous/qdadm 2.2.1 → 2.3.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quazardous/qdadm",
3
- "version": "2.2.1",
3
+ "version": "2.3.0",
4
4
  "description": "Vue 3 framework for admin dashboards with PrimeVue",
5
5
  "author": "quazardous",
6
6
  "license": "MIT",
@@ -20,6 +20,7 @@
20
20
  */
21
21
  import { computed, inject, type Ref } from 'vue'
22
22
  import Tag from 'primevue/tag'
23
+ import type { ButtonSeverity } from '../../types'
23
24
 
24
25
  interface TitleParts {
25
26
  simple?: string
@@ -30,7 +31,7 @@ interface TitleParts {
30
31
 
31
32
  interface BadgeConfig {
32
33
  label: string
33
- severity?: string
34
+ severity?: ButtonSeverity
34
35
  }
35
36
 
36
37
  interface Props {
@@ -80,7 +81,7 @@ const hasDecoratedTitle = computed((): boolean => {
80
81
  v-for="badge in badges"
81
82
  :key="badge.label"
82
83
  :value="badge.label"
83
- :severity="(badge.severity as any) || 'secondary'"
84
+ :severity="badge.severity || 'secondary'"
84
85
  class="entity-badge"
85
86
  />
86
87
  <template v-if="!effectiveTitleParts && !simpleTitle">{{ title }}</template>
@@ -17,7 +17,7 @@
17
17
  */
18
18
  import { computed, watch, inject, type Ref } from 'vue'
19
19
  import { useRoute, useRouter } from 'vue-router'
20
- import { getSiblingRoutes, getChildRoutes, type ModuleRoute, type ParentConfig } from '../../module/moduleRegistry'
20
+ import { getSiblingRoutes, getChildRoutes, routeParamsSatisfied, type ModuleRoute, type ParentConfig } from '../../module/moduleRegistry'
21
21
  import { useOrchestrator } from '../../orchestrator/useOrchestrator.js'
22
22
  import type { EntityManager } from '../../entity/EntityManager'
23
23
  import type { EntityRecord } from '../../types'
@@ -70,12 +70,15 @@ const siblingNavlinks = computed<NavLink[]>(() => {
70
70
  const { entity: parentEntityName, param } = parentConfig.value
71
71
  const siblings = getSiblingRoutes(parentEntityName, param)
72
72
 
73
- // Filter out action routes (create, edit) - they're not navigation tabs
73
+ // Filter out item-level routes (create, edit, show) — they're not
74
+ // navigation tabs — and any route whose required params aren't all
75
+ // available here (#1205: a child show route needs its own :id, which
76
+ // doesn't exist at list level; useLink would throw).
74
77
  const navRoutes = siblings.filter((r: ModuleRoute) => {
75
78
  const name = r.name || ''
76
79
  const path = r.path || ''
77
- // Exclude routes ending with -create, -edit, -new or paths with /create, /edit, /new
78
- return !name.match(/-(create|edit|new)$/) && !path.match(/\/(create|edit|new)$/)
80
+ if (name.match(/-(create|edit|new|show)$/) || path.match(/\/(create|edit|new)$/)) return false
81
+ return routeParamsSatisfied(path, route.params as Record<string, unknown>)
79
82
  })
80
83
 
81
84
  // Build navlinks with current route params
@@ -109,11 +112,14 @@ const childNavlinks = computed<NavLink[]>(() => {
109
112
  const children = getChildRoutes(entityName)
110
113
  if (children.length === 0) return []
111
114
 
112
- // Filter out action routes (create, edit) - they're not navigation tabs
115
+ // Filter out item-level routes (create, edit, show) and any route whose
116
+ // required params aren't satisfiable from the parent id alone (#1205).
113
117
  const navRoutes = children.filter((r: ModuleRoute) => {
114
118
  const name = r.name || ''
115
119
  const path = r.path || ''
116
- return !name.match(/-(create|edit|new)$/) && !path.match(/\/(create|edit|new)$/)
120
+ if (name.match(/-(create|edit|new|show)$/) || path.match(/\/(create|edit|new)$/)) return false
121
+ const parentParam = (r.meta?.parent as ParentConfig)?.param || currentManager?.idField || 'id'
122
+ return routeParamsSatisfied(path, { [parentParam]: entityId })
117
123
  })
118
124
 
119
125
  if (navRoutes.length === 0) return []
@@ -80,6 +80,14 @@ defineProps({
80
80
  sortOrder: {
81
81
  type: Number as PropType<1 | -1>,
82
82
  default: 1
83
+ },
84
+ /**
85
+ * Extra props forwarded verbatim to the PrimeVue DataTable (#1217) —
86
+ * e.g. { nullSortOrder: -1 } to sort null values last.
87
+ */
88
+ tableProps: {
89
+ type: Object as PropType<Record<string, unknown>>,
90
+ default: () => ({})
83
91
  }
84
92
  })
85
93
 
@@ -110,6 +118,7 @@ function onSort(event: DataTableSortEvent): void {
110
118
  :sortOrder="sortOrder"
111
119
  :stripedRows="stripedRows"
112
120
  :removableSort="removableSort"
121
+ v-bind="tableProps"
113
122
  @update:selection="onSelectionChange"
114
123
  @sort="onSort"
115
124
  class="default-table"
@@ -86,6 +86,12 @@ const props = defineProps({
86
86
 
87
87
  // Table data
88
88
  items: { type: Array as PropType<unknown[]>, required: true },
89
+ /**
90
+ * Extra props forwarded verbatim to the underlying PrimeVue DataTable
91
+ * (#1217) — e.g. { nullSortOrder: -1 } to sort null values last. Bound
92
+ * last, so they can override the defaults ListPage sets.
93
+ */
94
+ tableProps: { type: Object as PropType<Record<string, unknown>>, default: () => ({}) },
89
95
  loading: { type: Boolean, default: false },
90
96
  dataKey: { type: String, default: 'id' },
91
97
 
@@ -441,6 +447,7 @@ function getFilterClass(filter: FilterConfig): Record<string, boolean> {
441
447
  :class="{ 'datatable-row-clickable': hasRowTapAction }"
442
448
  stripedRows
443
449
  removableSort
450
+ v-bind="tableProps"
444
451
  >
445
452
  <!-- Columns from slot -->
446
453
  <slot name="columns" ></slot>
@@ -23,6 +23,7 @@ import Card from 'primevue/card'
23
23
  import InputText from 'primevue/inputtext'
24
24
  import Password from 'primevue/password'
25
25
  import Button from 'primevue/button'
26
+ import type { OrchestratorLike } from '../../entity/EntityManager.interface'
26
27
 
27
28
  /**
28
29
  * Auth adapter interface
@@ -46,15 +47,8 @@ interface LoginResult {
46
47
  /**
47
48
  * Orchestrator interface
48
49
  */
49
- interface Orchestrator {
50
- toast: {
51
- success: (summary: string, detail?: string, emitter?: string) => void
52
- error: (summary: string, detail?: string, emitter?: string) => void
53
- }
54
- signals: {
55
- emit: (signal: string, payload: unknown) => void
56
- }
57
- }
50
+ // #1191 — shared structural view (was: local redeclaration)
51
+ type Orchestrator = OrchestratorLike
58
52
 
59
53
  /**
60
54
  * App config interface
@@ -184,7 +178,7 @@ async function handleLogin(): Promise<void> {
184
178
  })
185
179
 
186
180
  // Emit auth:login signal for debug bar and other listeners
187
- orchestrator?.signals.emit('auth:login', { user: result.user })
181
+ orchestrator?.signals?.emit('auth:login', { user: result.user })
188
182
  orchestrator?.toast.success(
189
183
  'Welcome',
190
184
  `Logged in as ${result.user?.username || result.user?.email || username.value}`,
@@ -202,7 +196,7 @@ async function handleLogin(): Promise<void> {
202
196
  || axiosError.message
203
197
  || 'Invalid credentials'
204
198
 
205
- orchestrator?.signals.emit('auth:login:error', {
199
+ orchestrator?.signals?.emit('auth:login:error', {
206
200
  username: username.value,
207
201
  error: message,
208
202
  status: axiosError.response?.status
@@ -44,6 +44,7 @@ import Button from 'primevue/button'
44
44
  import Message from 'primevue/message'
45
45
  import FieldGroups from '../item/FieldGroups.vue'
46
46
  import ShowField from './ShowField.vue'
47
+ import type { ButtonSeverity } from '../../types'
47
48
 
48
49
  /**
49
50
  * Page title parts for PageHeader
@@ -61,7 +62,7 @@ interface ResolvedAction {
61
62
  name: string
62
63
  label: string
63
64
  icon?: string
64
- severity?: string
65
+ severity?: ButtonSeverity
65
66
  isLoading: boolean
66
67
  isDisabled: boolean
67
68
  onClick: () => void | Promise<void>
@@ -110,7 +111,7 @@ const props = defineProps({
110
111
  titleParts: { type: Object as PropType<PageTitleParts | null>, default: null },
111
112
 
112
113
  // Custom badges from entity manager
113
- badges: { type: Array as PropType<Array<{ label: string; severity?: string }>>, default: () => [] },
114
+ badges: { type: Array as PropType<Array<{ label: string; severity?: ButtonSeverity }>>, default: () => [] },
114
115
 
115
116
  // Fields (for auto-rendering - optional, can use #fields slot instead)
116
117
  fields: { type: Array as PropType<ResolvedFieldConfig[]>, default: () => [] },
@@ -200,7 +201,7 @@ const fetchErrorMessage = computed<string | null>(() => {
200
201
  :key="action.name"
201
202
  :label="action.label"
202
203
  :icon="action.icon"
203
- :severity="(action.severity as any)"
204
+ :severity="action.severity"
204
205
  :loading="action.isLoading"
205
206
  :disabled="action.isDisabled"
206
207
  @click="action.onClick"
@@ -283,7 +284,7 @@ const fetchErrorMessage = computed<string | null>(() => {
283
284
  :key="action.name"
284
285
  :label="action.label"
285
286
  :icon="action.icon"
286
- :severity="(action.severity as any)"
287
+ :severity="action.severity"
287
288
  :loading="action.isLoading"
288
289
  :disabled="action.isDisabled"
289
290
  @click="action.onClick"
@@ -344,7 +345,7 @@ const fetchErrorMessage = computed<string | null>(() => {
344
345
  :key="action.name"
345
346
  :label="action.label"
346
347
  :icon="action.icon"
347
- :severity="(action.severity as any)"
348
+ :severity="action.severity"
348
349
  :loading="action.isLoading"
349
350
  :disabled="action.isDisabled"
350
351
  @click="action.onClick"
@@ -35,28 +35,14 @@ import { useDirtyState } from './useDirtyState'
35
35
  import { useUnsavedChangesGuard, type GuardDialogState } from './useUnsavedChangesGuard'
36
36
  import { useBreadcrumb, type BreadcrumbDisplayItem } from './useBreadcrumb'
37
37
  import { registerGuardDialog, unregisterGuardDialog } from './useGuardStore'
38
+ import type { EntityManagerLike, OrchestratorLike } from '../entity/EntityManager.interface'
39
+
40
+ // #1191 — shared minimal structural views (was: local redeclarations)
41
+ type EntityManager = EntityManagerLike
42
+ type Orchestrator = OrchestratorLike
43
+
38
44
 
39
- /**
40
- * Entity manager interface (minimal for this composable)
41
- */
42
- interface EntityManager {
43
- label?: string
44
- labelField?: string
45
- getEntityLabel: (data: unknown) => string
46
- }
47
45
 
48
- /**
49
- * Orchestrator interface
50
- */
51
- interface Orchestrator {
52
- get: (entityName: string) => EntityManager
53
- toast: {
54
- success: (summary: string, detail?: string, emitter?: unknown) => void
55
- error: (summary: string, detail?: string, emitter?: unknown) => void
56
- warn: (summary: string, detail?: string, emitter?: unknown) => void
57
- info: (summary: string, detail?: string, emitter?: unknown) => void
58
- }
59
- }
60
46
 
61
47
  /**
62
48
  * Toast helper interface
@@ -171,7 +157,7 @@ export function useBareForm(options: UseBareFormOptions): UseBareFormReturn {
171
157
  const orchestrator = inject<Orchestrator | null>('qdadmOrchestrator', null)
172
158
  if (orchestrator) {
173
159
  try {
174
- manager = orchestrator.get(entity)
160
+ manager = orchestrator.get(entity) ?? null
175
161
  } catch {
176
162
  // Manager not found, continue without it
177
163
  }
@@ -237,7 +223,8 @@ export function useBareForm(options: UseBareFormOptions): UseBareFormReturn {
237
223
  return manager.getEntityLabel(formData)
238
224
  }
239
225
  if (typeof effectiveLabelField === 'function') {
240
- return effectiveLabelField(formData)
226
+ // labelField's param is contravariant-`never` in the Like — safe to call
227
+ return (effectiveLabelField as (data: unknown) => string)(formData)
241
228
  }
242
229
  return (formData[effectiveLabelField as string] as string) || null
243
230
  }
@@ -15,6 +15,12 @@
15
15
  import { computed, inject, type ComputedRef } from 'vue'
16
16
  import { useRoute } from 'vue-router'
17
17
  import { useStackHydrator, type StackHydratorReturn } from '../chain/useStackHydrator'
18
+ import type { EntityManagerLike, OrchestratorLike } from '../entity/EntityManager.interface'
19
+
20
+ // #1191 — shared minimal structural views (was: local redeclarations)
21
+ type EntityManager = EntityManagerLike
22
+ type Orchestrator = OrchestratorLike
23
+
18
24
 
19
25
  /**
20
26
  * Parent config from route meta
@@ -28,21 +34,10 @@ interface ParentConfig {
28
34
  /**
29
35
  * Entity manager interface (subset used by child pages)
30
36
  */
31
- interface EntityManager {
32
- idField: string
33
- label?: string
34
- labelPlural?: string
35
- routePrefix?: string
36
- getEntityLabel: (data: unknown) => string
37
- getEntityBadges?: (data: unknown) => Array<{ label: string; severity?: string }>
38
- }
39
37
 
40
38
  /**
41
39
  * Orchestrator interface
42
40
  */
43
- interface Orchestrator {
44
- get: (entityName: string) => EntityManager | null
45
- }
46
41
 
47
42
  /**
48
43
  * Return type for useChildPage
@@ -10,20 +10,13 @@ import type { BreadcrumbDisplayItem } from './useBreadcrumb'
10
10
  import type { ParentConfig, EntityManager } from './useEntityItemPage'
11
11
  import type { FieldGroup, GroupDefinition, GroupOptions, MoveFieldPosition } from './useFieldManager'
12
12
  import type { StackHydratorReturn } from '../chain/useStackHydrator'
13
+ import type { OrchestratorLike } from '../entity/EntityManager.interface'
13
14
 
14
15
  /**
15
16
  * Orchestrator interface
16
17
  */
17
- export interface Orchestrator {
18
- get: (entityName: string) => EntityManager
19
- toast?: {
20
- success: (summary: string, detail?: string, emitter?: unknown) => void
21
- error: (summary: string, detail?: string, emitter?: unknown) => void
22
- warn: (summary: string, detail?: string, emitter?: unknown) => void
23
- info: (summary: string, detail?: string, emitter?: unknown) => void
24
- [key: string]: ((summary: string, detail?: string, emitter?: unknown) => void) | undefined
25
- }
26
- }
18
+ // #1191 — shared structural view, parameterized on the CRUD tier
19
+ export type Orchestrator = OrchestratorLike<EntityManager>
27
20
 
28
21
  /**
29
22
  * Field definition from EntityManager schema
@@ -16,6 +16,7 @@ import { ref, computed, onMounted, inject, provide, type Ref, type ComputedRef }
16
16
  import { useRoute } from 'vue-router'
17
17
  import { useStackHydrator, type StackHydratorReturn } from '../chain/useStackHydrator'
18
18
  import type { EntityManagerCrud } from '../entity/EntityManager.interface'
19
+ import type { OrchestratorLike } from '../entity/EntityManager.interface'
19
20
 
20
21
  /**
21
22
  * Parent configuration from route.meta.parent
@@ -61,12 +62,8 @@ export interface CreateContext {
61
62
  */
62
63
  export type EntityManager = EntityManagerCrud
63
64
 
64
- /**
65
- * Orchestrator interface
66
- */
67
- interface Orchestrator {
68
- get: (entityName: string) => EntityManager
69
- }
65
+ // #1191 — shared structural view, parameterized on the CRUD tier
66
+ type Orchestrator = OrchestratorLike<EntityManager>
70
67
 
71
68
  /**
72
69
  * Options for useEntityItemPage
@@ -54,19 +54,10 @@ import {
54
54
  type ShowFieldDefinition,
55
55
  type ShowResolvedFieldConfig,
56
56
  } from './createShowFieldResolver'
57
+ import type { OrchestratorLike } from '../entity/EntityManager.interface'
57
58
 
58
- /**
59
- * Orchestrator interface
60
- */
61
- interface Orchestrator {
62
- get: (entityName: string) => EntityManager
63
- toast?: {
64
- success: (summary: string, detail?: string, emitter?: unknown) => void
65
- error: (summary: string, detail?: string, emitter?: unknown) => void
66
- warn: (summary: string, detail?: string, emitter?: unknown) => void
67
- info: (summary: string, detail?: string, emitter?: unknown) => void
68
- }
69
- }
59
+ // #1191 — shared structural view (manager tier from useEntityItemPage)
60
+ type Orchestrator = OrchestratorLike<EntityManager>
70
61
 
71
62
  /**
72
63
  * Field definition from EntityManager schema.
@@ -7,7 +7,7 @@ import type { Ref, ComputedRef } from 'vue'
7
7
  import type { Router } from 'vue-router'
8
8
  import type { FilterQuery } from '../query/FilterQuery'
9
9
  import type { ParentConfig, UseEntityItemPageReturn } from './useEntityItemPage.js'
10
- import type { EntityManagerRead } from '../entity/EntityManager.interface'
10
+ import type { EntityManagerRead, OrchestratorLike } from '../entity/EntityManager.interface'
11
11
 
12
12
  /**
13
13
  * Entity manager interface for list pages (re-export for convenience)
@@ -27,16 +27,8 @@ export interface ListResponse<T = unknown> {
27
27
  /**
28
28
  * Orchestrator interface
29
29
  */
30
- export interface Orchestrator {
31
- get: (entityName: string) => EntityManager
32
- toast: {
33
- success: (summary: string, detail?: string, emitter?: unknown) => void
34
- error: (summary: string, detail?: string, emitter?: unknown) => void
35
- warn: (summary: string, detail?: string, emitter?: unknown) => void
36
- info: (summary: string, detail?: string, emitter?: unknown) => void
37
- [key: string]: ((summary: string, detail?: string, emitter?: unknown) => void) | undefined
38
- }
39
- }
30
+ // #1191 — shared structural view, parameterized on the Read tier
31
+ export type Orchestrator = OrchestratorLike<EntityManager>
40
32
 
41
33
  /**
42
34
  * Toast helper interface
@@ -21,6 +21,12 @@ import { useRoute, useRouter, type RouteParamsRawGeneric } from 'vue-router'
21
21
  import { getSiblingRoutes } from '../module/moduleRegistry'
22
22
  import { useSemanticBreadcrumb, type SemanticBreadcrumbItem } from './useSemanticBreadcrumb'
23
23
  import { useStackHydrator, type HydratedLevel } from '../chain/useStackHydrator'
24
+ import type { EntityManagerLike, OrchestratorLike } from '../entity/EntityManager.interface'
25
+
26
+ // #1191 — shared minimal structural views (was: local redeclarations)
27
+ type EntityManager = EntityManagerLike
28
+ type Orchestrator = OrchestratorLike
29
+
24
30
 
25
31
  /**
26
32
  * Navigation chain segment types
@@ -44,20 +50,10 @@ export interface NavChainSegment {
44
50
  /**
45
51
  * Entity manager interface
46
52
  */
47
- interface EntityManager {
48
- routePrefix?: string
49
- labelPlural?: string
50
- idField?: string
51
- readOnly?: boolean
52
- getEntityLabel: (data: unknown) => string
53
- }
54
53
 
55
54
  /**
56
55
  * Orchestrator interface
57
56
  */
58
- interface Orchestrator {
59
- get: (entityName: string) => EntityManager | null
60
- }
61
57
 
62
58
  /**
63
59
  * Breadcrumb item
@@ -307,7 +303,7 @@ export function useNavContext(_options: UseNavContextOptions = {}): UseNavContex
307
303
  })
308
304
  } else if (segment.type === 'item') {
309
305
  const data = chainData.value.get(i)
310
- const label = data && segment.manager ? segment.manager.getEntityLabel(data) : '...'
306
+ const label = (data && segment.manager ? segment.manager.getEntityLabel(data) : null) ?? '...'
311
307
  const idField = segment.manager?.idField || 'id'
312
308
 
313
309
  // Only create link if we have a valid id
@@ -15,6 +15,11 @@ import { getNavSections, alterMenuSections, isMenuAltered } from '../module/modu
15
15
  import { useSemanticBreadcrumb } from './useSemanticBreadcrumb'
16
16
  import { useI18n } from '../i18n/useI18n'
17
17
  import type { HookRegistry } from '../hooks/HookRegistry'
18
+ import type { OrchestratorLike } from '../entity/EntityManager.interface'
19
+
20
+ // #1191 — shared minimal structural view (was: local redeclaration)
21
+ type Orchestrator = OrchestratorLike
22
+
18
23
 
19
24
  /**
20
25
  * Navigation item interface
@@ -39,17 +44,10 @@ export interface NavSection {
39
44
  /**
40
45
  * Entity manager interface
41
46
  */
42
- interface EntityManager {
43
- canRead: () => boolean
44
- }
45
47
 
46
48
  /**
47
49
  * Orchestrator interface
48
50
  */
49
- interface Orchestrator {
50
- signals?: unknown
51
- get: (entityName: string) => EntityManager | null
52
- }
53
51
 
54
52
  /**
55
53
  * Return type for useNavigation
@@ -37,7 +37,7 @@ import {
37
37
  type Ref,
38
38
  type ComputedRef,
39
39
  } from 'vue'
40
- import type { SignalBus } from '../kernel/SignalBus'
40
+ import type { EntityManagerLike, OrchestratorLike } from '../entity/EntityManager.interface'
41
41
 
42
42
  /**
43
43
  * User record type
@@ -55,28 +55,18 @@ interface AuthAdapterWithImpersonation {
55
55
  getOriginalUser?: () => UserRecord | null
56
56
  }
57
57
 
58
- /**
59
- * Entity auth adapter interface
60
- */
61
- interface EntityAuthAdapter {
62
- isGranted?: (permission: string) => boolean
63
- }
64
-
65
58
  /**
66
59
  * Orchestrator interface
67
60
  */
68
- interface Orchestrator {
69
- signals?: SignalBus
70
- entityAuthAdapter?: EntityAuthAdapter
71
- get: (entityName: string) => EntityManager | null
61
+ // #1191 — shared structural view + the impersonator's extra surface
62
+ type ImpersonatorManager = EntityManagerLike & {
63
+ findAll?: () => Promise<{ data?: UserRecord[] } | UserRecord[]>
72
64
  }
65
+ type Orchestrator = OrchestratorLike<ImpersonatorManager | null | undefined>
73
66
 
74
67
  /**
75
68
  * Entity manager interface
76
69
  */
77
- interface EntityManager {
78
- findAll: () => Promise<{ data?: UserRecord[] } | UserRecord[]>
79
- }
80
70
 
81
71
  /**
82
72
  * Options for useUserImpersonator
@@ -214,7 +204,7 @@ export function useUserImpersonator(
214
204
  error.value = null
215
205
  const manager = orchestrator.get(entity)
216
206
  if (manager) {
217
- const result = await manager.findAll()
207
+ const result = await manager.findAll?.()
218
208
  users.value =
219
209
  (result as { data?: UserRecord[] })?.data ||
220
210
  (result as UserRecord[]) ||
@@ -15,6 +15,10 @@
15
15
  * that flows through to method signatures for typed entity data.
16
16
  */
17
17
 
18
+ import type { EntityBadge } from './EntityManager.types'
19
+ import type { SignalBus } from '../kernel/SignalBus'
20
+ import type { EntityAuthAdapter } from './auth/EntityAuthAdapter'
21
+
18
22
  // ─── Base ────────────────────────────────────────────────────────────────────
19
23
 
20
24
  /**
@@ -76,3 +80,71 @@ export interface EntityManagerCrud<T = unknown> extends EntityManagerRead<T> {
76
80
  getSeverity?: (field: string, value: string | number, defaultSeverity?: string) => string
77
81
  getSeverityDescriptor?: (field: string, value: string | number, defaultSeverity?: string) => { severity: string; icon?: string; label?: string }
78
82
  }
83
+ // ============================================================================
84
+ // ASSIGNMENT-SAFE STRUCTURAL VIEWS (#1191)
85
+ // ============================================================================
86
+ //
87
+ // Composables and components used to redeclare tiny local `EntityManager` /
88
+ // `Orchestrator` interfaces (24 sites) because the canonical generics don't
89
+ // assign across erased type parameters. These shared, non-generic views are
90
+ // the single replacement: the canonical classes satisfy them structurally
91
+ // (compile-time asserted in orchestrator/Orchestrator.ts), and consumers
92
+ // declare exactly the surface they touch by intersecting when they need more.
93
+ //
94
+ // NOTE: members use METHOD syntax on purpose — TS keeps method parameters
95
+ // bivariant (even under strictFunctionTypes), which is what lets
96
+ // `EntityManager<T>` assign to the erased signatures below.
97
+
98
+ /** Toast surface exposed by the orchestrator (structural ToastHelper). */
99
+ export interface ToastLike {
100
+ success(summary: string, detail?: string, emitter?: unknown): void
101
+ error(summary: string, detail?: string, emitter?: unknown): void
102
+ warn(summary: string, detail?: string, emitter?: unknown): void
103
+ info(summary: string, detail?: string, emitter?: unknown): void
104
+ }
105
+
106
+ /**
107
+ * Minimal structural view of an EntityManager for composables/components.
108
+ * Every member exists on the canonical `EntityManager`; optionals reflect
109
+ * genuinely optional configuration.
110
+ */
111
+ export interface EntityManagerLike {
112
+ idField: string
113
+ label?: string
114
+ labelPlural?: string
115
+ // Function-typed PROPERTY → strictly contravariant param; `never` is the
116
+ // contravariant top, so the canonical `(entity: T) => string` assigns.
117
+ // Cast at the call site when invoking.
118
+ labelField?: string | ((data: never) => string)
119
+ routePrefix?: string
120
+ readOnly?: boolean
121
+ getEntityLabel(data: unknown): string | null
122
+ getEntityBadges?(data: unknown): EntityBadge[]
123
+ canRead(entity?: unknown): boolean
124
+ canCreate(): boolean
125
+ canUpdate(entity?: unknown): boolean
126
+ canDelete(entity?: unknown): boolean
127
+ list(params?: Record<string, unknown>, context?: unknown): Promise<{
128
+ items: unknown[]
129
+ total?: number
130
+ fromCache?: boolean
131
+ }>
132
+ get(id: string | number, context?: unknown): Promise<unknown>
133
+ create(data: Record<string, unknown>, context?: unknown): Promise<unknown>
134
+ update(id: string | number, data: Record<string, unknown>, context?: unknown): Promise<unknown>
135
+ delete(id: string | number, context?: unknown): Promise<unknown>
136
+ }
137
+
138
+ /**
139
+ * Minimal structural view of the Orchestrator for composables/components.
140
+ * `get()` is typed nullable so consumers guard — the canonical class throws
141
+ * on unknown names, injected test doubles commonly return null.
142
+ */
143
+ export interface OrchestratorLike<M = EntityManagerLike | null | undefined> {
144
+ get(name: string): M
145
+ has?(name: string): boolean
146
+ isRegistered?(name: string): boolean
147
+ toast: ToastLike
148
+ signals?: SignalBus | null
149
+ entityAuthAdapter?: EntityAuthAdapter | null
150
+ }
@@ -288,3 +288,4 @@ export type EntityManagerInternal<T extends EntityRecord = EntityRecord> = Entit
288
288
  // Re-export types used by helpers
289
289
  export type { EntityRecord, ListResult, IStorage, StorageCapabilities }
290
290
  export type { SignalBus, HookRegistry }
291
+
@@ -0,0 +1,46 @@
1
+ /**
2
+ * TestManager - Auto-generated EntityManager
3
+ *
4
+ * Generated by qdadm/gen/generateManagers
5
+ * DO NOT EDIT MANUALLY - Changes will be overwritten
6
+ *
7
+ * Entity: test
8
+ * Endpoint: /test
9
+ */
10
+
11
+ import { EntityManager } from '@quazardous/qdadm'
12
+ import type { EntityRecord } from '@quazardous/qdadm'
13
+ import { ApiStorage } from '@quazardous/qdadm'
14
+
15
+ export interface TestEntity extends EntityRecord {
16
+ }
17
+
18
+ /**
19
+ * Schema definition for test
20
+ */
21
+ export const testSchema = {
22
+ name: "test",
23
+ endpoint: "/test",
24
+ fields: {}
25
+ } as const
26
+
27
+ /**
28
+ * Storage options for test
29
+ */
30
+ const storageOptions = {
31
+ endpoint: "/test"
32
+ }
33
+
34
+ /**
35
+ * TestManager instance
36
+ *
37
+ * Provides CRUD operations for test entity.
38
+ */
39
+ export const testManager = new EntityManager<TestEntity>({
40
+ ...{
41
+ name: "test",
42
+ idField: "id",
43
+ fields: {}
44
+ },
45
+ storage: new ApiStorage<TestEntity>(storageOptions)
46
+ })
@@ -442,6 +442,21 @@ export function getEntityConfig(name: string): EntityConfig | undefined {
442
442
  * @param parentParam - Parent route param
443
443
  * @returns Routes with matching parent config
444
444
  */
445
+ /**
446
+ * Check that every REQUIRED `:param` in a route path is present (non-empty)
447
+ * in the given params. Used by PageNav to avoid building navlinks that
448
+ * vue-router's useLink would reject with "Missing required param" (#1205).
449
+ */
450
+ export function routeParamsSatisfied(path: string, params: Record<string, unknown>): boolean {
451
+ const tokens = path.match(/:([A-Za-z0-9_]+)\??/g) || []
452
+ return tokens.every((token) => {
453
+ if (token.endsWith('?')) return true // optional param
454
+ const name = token.slice(1)
455
+ const value = params[name]
456
+ return value !== undefined && value !== null && value !== ''
457
+ })
458
+ }
459
+
445
460
  export function getSiblingRoutes(parentEntity: string, parentParam: string): ModuleRoute[] {
446
461
  return routes.filter((route) => {
447
462
  const parent = route.meta?.parent
@@ -34,6 +34,7 @@ import type { EntityRecord } from '../types'
34
34
  import type { SignalBus } from '../kernel/SignalBus'
35
35
  import type { HookRegistry } from '../hooks/HookRegistry'
36
36
  import type { EntityAuthAdapter } from '../entity/auth/EntityAuthAdapter'
37
+ import type { EntityManagerLike, OrchestratorLike } from '../entity/EntityManager.interface'
37
38
 
38
39
  /**
39
40
  * DeferredRegistry interface (minimal for Orchestrator needs)
@@ -425,3 +426,16 @@ Example fix in your module:
425
426
  export function createOrchestrator(options?: OrchestratorOptions): Orchestrator {
426
427
  return new Orchestrator(options)
427
428
  }
429
+
430
+ // ============================================================================
431
+ // #1191 — compile-time guarantee that the canonical classes satisfy the
432
+ // minimal structural views consumed across composables/components. If a
433
+ // rename breaks the structural match, this is where the compiler complains.
434
+ // ============================================================================
435
+ type _AssertManagerLike<T extends EntityManagerLike> = T
436
+ type _AssertOrchestratorLike<T extends OrchestratorLike> = T
437
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
438
+ type _LikeChecks = [
439
+ _AssertManagerLike<EntityManager>,
440
+ _AssertOrchestratorLike<Orchestrator>,
441
+ ]
@@ -52,7 +52,7 @@ export interface FilterQueryOptions<T = unknown> {
52
52
  source: FilterQuerySource
53
53
  entity?: string | null
54
54
  field?: string | null
55
- parentManager?: EntityManagerLike | null
55
+ parentManager?: QueryManagerLike | null
56
56
  label?: ValueResolver<T>
57
57
  value?: ValueResolver<T>
58
58
  transform?: ((options: FilterOption[]) => FilterOption[]) | null
@@ -62,7 +62,10 @@ export interface FilterQueryOptions<T = unknown> {
62
62
  /**
63
63
  * Minimal interface for EntityManager (to avoid circular deps)
64
64
  */
65
- interface EntityManagerLike {
65
+ // #1191 note: FilterQuery deliberately keeps a self-contained duck type —
66
+ // callers pass minimal mocks ({ _cache, list }) that the full
67
+ // QueryManagerLike would reject. Not a drift case.
68
+ interface QueryManagerLike {
66
69
  _cache?: unknown[]
67
70
  list(params?: { page_size?: number }): Promise<{ items: unknown[] }>
68
71
  }
@@ -70,8 +73,8 @@ interface EntityManagerLike {
70
73
  /**
71
74
  * Minimal interface for Orchestrator (to avoid circular deps)
72
75
  */
73
- interface OrchestratorLike {
74
- get(name: string): EntityManagerLike | undefined
76
+ interface QueryOrchestratorLike {
77
+ get(name: string): QueryManagerLike | null | undefined
75
78
  }
76
79
 
77
80
  /**
@@ -97,7 +100,7 @@ export class FilterQuery<T = unknown> {
97
100
  source: FilterQuerySource
98
101
  entity: string | null
99
102
  field: string | null
100
- parentManager: EntityManagerLike | null
103
+ parentManager: QueryManagerLike | null
101
104
  label: ValueResolver<T>
102
105
  value: ValueResolver<T>
103
106
  transform: ((options: FilterOption[]) => FilterOption[]) | null
@@ -146,7 +149,7 @@ export class FilterQuery<T = unknown> {
146
149
  /**
147
150
  * Set the parent manager (called by useListPage for field source)
148
151
  */
149
- setParentManager(manager: EntityManagerLike): this {
152
+ setParentManager(manager: QueryManagerLike): this {
150
153
  this.parentManager = manager
151
154
  return this
152
155
  }
@@ -211,7 +214,7 @@ export class FilterQuery<T = unknown> {
211
214
  /**
212
215
  * Get options for dropdown
213
216
  */
214
- async getOptions(orchestrator: OrchestratorLike | null = null): Promise<FilterOption[]> {
217
+ async getOptions(orchestrator: QueryOrchestratorLike | null = null): Promise<FilterOption[]> {
215
218
  // Return cached options if available
216
219
  if (this._options !== null) {
217
220
  return this._options
@@ -248,7 +251,7 @@ export class FilterQuery<T = unknown> {
248
251
  /**
249
252
  * Fetch items from entity manager
250
253
  */
251
- private async _fetchFromEntity(orchestrator: OrchestratorLike | null): Promise<unknown[]> {
254
+ private async _fetchFromEntity(orchestrator: QueryOrchestratorLike | null): Promise<unknown[]> {
252
255
  if (!orchestrator) {
253
256
  throw new Error('FilterQuery: orchestrator is required for entity source')
254
257
  }
@@ -5,6 +5,7 @@ import type {
5
5
  RoleHierarchyMap,
6
6
  RoleProviderContext,
7
7
  } from './RolesProvider'
8
+ import type { EntityManagerLike, OrchestratorLike } from '../entity/EntityManager.interface'
8
9
 
9
10
  /**
10
11
  * Options for EntityRoleProvider
@@ -28,19 +29,13 @@ interface RoleEntity {
28
29
  /**
29
30
  * Entity manager interface (minimal for adapter needs)
30
31
  */
31
- interface EntityManager {
32
- list(params?: Record<string, unknown>): Promise<{ items: RoleEntity[]; data?: RoleEntity[] }>
33
- create(data: Record<string, unknown>): Promise<RoleEntity>
34
- update(id: string | number, data: Record<string, unknown>): Promise<RoleEntity>
35
- delete(id: string | number): Promise<void>
36
- }
32
+ // #1191 — shared structural view; role-typed results are cast at call sites
33
+ type EntityManager = EntityManagerLike
37
34
 
38
35
  /**
39
36
  * Orchestrator interface (minimal for adapter needs)
40
37
  */
41
- interface Orchestrator {
42
- get(name: string): EntityManager | undefined
43
- }
38
+ type Orchestrator = OrchestratorLike
44
39
 
45
40
  /**
46
41
  * Cache structure for roles
@@ -162,7 +157,7 @@ export class EntityRoleProvider extends RoleProvider {
162
157
  }
163
158
 
164
159
  try {
165
- const { items: roles } = await manager.list({ page_size: 1000 })
160
+ const { items: roles } = (await manager.list({ page_size: 1000 })) as { items: RoleEntity[] }
166
161
 
167
162
  // Build cache
168
163
  const permissions: Record<string, string[]> = {}
@@ -317,7 +312,7 @@ export class EntityRoleProvider extends RoleProvider {
317
312
  [this._permissionsField]: permissions,
318
313
  [this._inheritsField]: inherits,
319
314
  }
320
- const result = await manager.create(data)
315
+ const result = (await manager.create(data)) as RoleEntity
321
316
  this.invalidate()
322
317
  return result
323
318
  }
@@ -334,10 +329,10 @@ export class EntityRoleProvider extends RoleProvider {
334
329
  const manager = this._getManager()
335
330
 
336
331
  // Find the role entity by name
337
- const response = await manager.list({
332
+ const response = (await manager.list({
338
333
  filter: { [this._nameField]: name },
339
334
  limit: 1,
340
- })
335
+ })) as { items: RoleEntity[]; data?: RoleEntity[] }
341
336
  const roles = response.data || response.items
342
337
  if (roles.length === 0) {
343
338
  throw new Error(`Role '${name}' does not exist`)
@@ -352,7 +347,7 @@ export class EntityRoleProvider extends RoleProvider {
352
347
  if (permissions !== undefined) updates[this._permissionsField] = permissions
353
348
  if (inherits !== undefined) updates[this._inheritsField] = inherits
354
349
 
355
- const result = await manager.update(roleEntity.id as string | number, updates)
350
+ const result = (await manager.update(roleEntity.id as string | number, updates)) as RoleEntity
356
351
  this.invalidate()
357
352
  return result
358
353
  }
@@ -366,10 +361,10 @@ export class EntityRoleProvider extends RoleProvider {
366
361
  const manager = this._getManager()
367
362
 
368
363
  // Find the role entity by name
369
- const response = await manager.list({
364
+ const response = (await manager.list({
370
365
  filter: { [this._nameField]: name },
371
366
  limit: 1,
372
- })
367
+ })) as { items: RoleEntity[]; data?: RoleEntity[] }
373
368
  const roles = response.data || response.items
374
369
  if (roles.length === 0) {
375
370
  throw new Error(`Role '${name}' does not exist`)
@@ -283,3 +283,19 @@ export interface RequestOptions {
283
283
  headers?: Record<string, string>
284
284
  context?: unknown
285
285
  }
286
+
287
+ // ============ UI TYPES ============
288
+
289
+ /**
290
+ * PrimeVue Button/Tag severity values (#1191) — one shared union instead of
291
+ * `string` + `as any` casts at every :severity binding.
292
+ */
293
+ export type ButtonSeverity =
294
+ | 'primary'
295
+ | 'secondary'
296
+ | 'success'
297
+ | 'info'
298
+ | 'warn'
299
+ | 'danger'
300
+ | 'help'
301
+ | 'contrast'