@quazardous/qdadm 2.6.0 → 2.7.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.6.0",
3
+ "version": "2.7.0",
4
4
  "description": "Vue 3 framework for admin dashboards with PrimeVue",
5
5
  "author": "quazardous",
6
6
  "license": "MIT",
@@ -46,8 +46,8 @@
46
46
  "LICENSE"
47
47
  ],
48
48
  "dependencies": {
49
- "@quazardous/qdcore": "^0.2.1",
50
- "@quazardous/qddebug": "^0.2.1",
49
+ "@quazardous/qdcore": "^1.0.0",
50
+ "@quazardous/qddebug": "^1.0.0",
51
51
  "@quazardous/quarkernel": "^2.1.0",
52
52
  "pluralize": "^8.0.0",
53
53
  "yaml": "^2.8.4"
@@ -28,7 +28,8 @@
28
28
  * - mode: Optional v-model for view mode ('structured' or 'json')
29
29
  * - label: Optional label for the field
30
30
  * - jsonHeight: Height of JSON editor (default: 400px)
31
- * - jsonMode: Mode for JSON editor - 'tree' or 'text' (default: 'text')
31
+ * - jsonMode: Mode for JSON editor - 'tree', 'text' or 'table' (default: 'text');
32
+ * string literals accepted so consumers don't need the vanilla-jsoneditor Mode enum (#1280)
32
33
  * - defaultMode: Initial mode if not using v-model:mode (default: 'structured')
33
34
  * - showToggle: Whether to show the toggle (default: true, useful to hide in nested usage)
34
35
  * - guardInvalidJson: Block switching to structured view while JSON is invalid (default: true)
@@ -60,7 +61,7 @@ interface Props {
60
61
  mode?: ViewMode | null
61
62
  label?: string | null
62
63
  jsonHeight?: string
63
- jsonMode?: Mode
64
+ jsonMode?: Mode | 'tree' | 'text' | 'table'
64
65
  defaultMode?: ViewMode
65
66
  showToggle?: boolean
66
67
  guardInvalidJson?: boolean
@@ -73,7 +74,7 @@ const props = withDefaults(defineProps<Props>(), {
73
74
  mode: null,
74
75
  label: null,
75
76
  jsonHeight: '400px',
76
- jsonMode: 'text' as Mode,
77
+ jsonMode: 'text',
77
78
  defaultMode: 'structured',
78
79
  showToggle: true,
79
80
  guardInvalidJson: true,
@@ -15,6 +15,8 @@ import Breadcrumb from 'primevue/breadcrumb'
15
15
 
16
16
  interface FeaturesConfig {
17
17
  breadcrumb?: boolean
18
+ /** Opt-in (#1332): render a View↔Edit toggle next to the terminal crumb */
19
+ breadcrumbModeToggle?: boolean
18
20
  [key: string]: unknown
19
21
  }
20
22
 
@@ -22,7 +24,7 @@ const route = useRoute()
22
24
  const features = inject<FeaturesConfig>('qdadmFeatures', { breadcrumb: true })
23
25
 
24
26
  // Navigation context (breadcrumb + navlinks from route config)
25
- const { breadcrumb: defaultBreadcrumb, navlinks: defaultNavlinks } = useNavContext()
27
+ const { breadcrumb: defaultBreadcrumb, navlinks: defaultNavlinks, modeToggle } = useNavContext()
26
28
 
27
29
  // Allow child pages to override breadcrumb/navlinks via provide/inject
28
30
  const breadcrumbOverride = inject<Ref<BreadcrumbItem[] | null>>('qdadmBreadcrumbOverride', ref(null))
@@ -39,6 +41,12 @@ const showBreadcrumb = computed<boolean>(() => {
39
41
  if (route.name === 'dashboard') return false
40
42
  return true
41
43
  })
44
+
45
+ // View↔Edit toggle on the terminal crumb (#1332) — opt-in, and only when
46
+ // useNavContext resolved a reachable, permitted twin-mode route
47
+ const shownModeToggle = computed(() =>
48
+ features.breadcrumbModeToggle === true ? modeToggle.value : null
49
+ )
42
50
  </script>
43
51
 
44
52
  <template>
@@ -56,6 +64,17 @@ const showBreadcrumb = computed<boolean>(() => {
56
64
  </template>
57
65
  </Breadcrumb>
58
66
 
67
+ <!-- View↔Edit mode toggle (#1332) — plain navigation; the form page's
68
+ own route-leave guard covers unsaved changes on edit→show -->
69
+ <RouterLink
70
+ v-if="shownModeToggle"
71
+ :to="shownModeToggle.to"
72
+ class="breadcrumb-mode-toggle"
73
+ >
74
+ <i :class="shownModeToggle.target === 'edit' ? 'pi pi-pencil' : 'pi pi-eye'"></i>
75
+ <span>{{ shownModeToggle.label }}</span>
76
+ </RouterLink>
77
+
59
78
  <!-- Navlinks (provided by PageNav for child routes) -->
60
79
  <div v-if="navlinks.length > 0" class="breadcrumb-navlinks">
61
80
  <template v-for="(link, index) in navlinks" :key="link.to?.name || index">
@@ -92,6 +111,24 @@ const showBreadcrumb = computed<boolean>(() => {
92
111
  font-size: 0.875rem;
93
112
  }
94
113
 
114
+ /* Mode toggle sits right after the crumbs; auto margin keeps navlinks pinned right */
115
+ .breadcrumb-mode-toggle {
116
+ display: inline-flex;
117
+ align-items: center;
118
+ gap: 0.35rem;
119
+ margin-left: 0.75rem;
120
+ margin-right: auto;
121
+ font-size: 0.875rem;
122
+ color: var(--p-primary-500, #3b82f6);
123
+ text-decoration: none;
124
+ transition: color 0.15s;
125
+ }
126
+
127
+ .breadcrumb-mode-toggle:hover {
128
+ color: var(--p-primary-700, #1d4ed8);
129
+ text-decoration: underline;
130
+ }
131
+
95
132
  .navlinks-separator {
96
133
  color: var(--p-surface-400, #94a3b8);
97
134
  }
@@ -46,6 +46,9 @@ import Message from 'primevue/message'
46
46
  import FieldGroups from '../item/FieldGroups.vue'
47
47
  import ShowField from './ShowField.vue'
48
48
  import type { ButtonSeverity } from '../../types'
49
+ // Single source of truth (#1281): the props must accept exactly what the
50
+ // composable emits — a local redeclaration is where the two drifted apart.
51
+ import type { ResolvedAction } from '../../composables/useEntityItemShowPage'
49
52
 
50
53
  /**
51
54
  * Page title parts for PageHeader
@@ -56,19 +59,6 @@ interface PageTitleParts {
56
59
  entityLabel: string | undefined
57
60
  }
58
61
 
59
- /**
60
- * Action configuration
61
- */
62
- interface ResolvedAction {
63
- name: string
64
- label: string
65
- icon?: string
66
- severity?: ButtonSeverity
67
- isLoading: boolean
68
- isDisabled: boolean
69
- onClick: () => void | Promise<void>
70
- }
71
-
72
62
  /**
73
63
  * Resolved field config
74
64
  */
@@ -73,6 +73,7 @@ export {
73
73
  type NavChainType,
74
74
  type BreadcrumbItem,
75
75
  type NavLinkItem,
76
+ type BreadcrumbModeToggle,
76
77
  } from './useNavContext'
77
78
  export { useNavigation, type UseNavigationReturn, type NavItem, type NavSection } from './useNavigation'
78
79
  export {
@@ -55,6 +55,7 @@ import {
55
55
  type ShowResolvedFieldConfig,
56
56
  } from './createShowFieldResolver'
57
57
  import type { OrchestratorLike } from '../entity/EntityManager.interface'
58
+ import type { ButtonSeverity } from '../types'
58
59
  import { useActionRegistry } from './useActionRegistry'
59
60
 
60
61
  // #1191 — shared structural view (manager tier from useEntityItemPage)
@@ -81,7 +82,8 @@ export interface ActionConfig {
81
82
  name: string
82
83
  label: string
83
84
  icon?: string
84
- severity?: string
85
+ /** PrimeVue Button severity — the shared union, not bare string (#1281) */
86
+ severity?: ButtonSeverity
85
87
  onClick: () => void | Promise<void>
86
88
  visible?: (ctx: { canEdit: boolean; canDelete: boolean }) => boolean
87
89
  disabled?: (ctx: { loading: boolean }) => boolean
@@ -99,7 +101,8 @@ export interface LazyActionConfig {
99
101
  name: string
100
102
  label: string
101
103
  icon?: string
102
- severity?: string
104
+ /** PrimeVue Button severity — the shared union, not bare string (#1281) */
105
+ severity?: ButtonSeverity
103
106
  onClick: () => void | Promise<void>
104
107
  resolve: (data: unknown) => Promise<{ visible?: boolean; disabled?: boolean }>
105
108
  loading?: () => boolean
@@ -129,7 +132,7 @@ export interface ShowPageProps {
129
132
  loading: boolean
130
133
  title: string
131
134
  titleParts: PageTitleParts
132
- badges: Array<{ label: string; severity?: string }>
135
+ badges: Array<{ label: string; severity?: ButtonSeverity }>
133
136
  fields: ResolvedFieldConfig[]
134
137
  groups: FieldGroup<ResolvedFieldConfig>[]
135
138
  data: Record<string, unknown> | null
@@ -21,6 +21,7 @@ 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 { useI18n } from '../i18n/useI18n'
24
25
  import type { EntityManagerLike, OrchestratorLike } from '../entity/EntityManager.interface'
25
26
 
26
27
  // #1191 — shared minimal structural views (was: local redeclarations)
@@ -64,6 +65,29 @@ export interface BreadcrumbItem {
64
65
  icon?: string
65
66
  }
66
67
 
68
+ /**
69
+ * View↔Edit toggle for the breadcrumb terminal (#1332)
70
+ *
71
+ * Non-null only when the current route is an entity item mode
72
+ * (`entity-show` / `entity-edit`), the twin-mode route exists
73
+ * (`${routePrefix}-edit` / `-show` + `router.hasRoute`), and — for the
74
+ * Edit affordance — `manager.canUpdate()` allows it. The permission check
75
+ * passes the hydrated entity when available, so ownership-conditional
76
+ * permissions are honored once the item is loaded.
77
+ */
78
+ export interface BreadcrumbModeToggle {
79
+ /** Mode of the current route */
80
+ current: 'show' | 'edit'
81
+ /** Mode the toggle navigates to */
82
+ target: 'show' | 'edit'
83
+ /** Twin-route location — a plain router.push; any dirty-form protection
84
+ * comes from the form page's own onBeforeRouteLeave guard */
85
+ to: { name: string; params: RouteParamsRawGeneric }
86
+ /** Locale-reactive label: i18n `breadcrumb.view` / `breadcrumb.edit`,
87
+ * falling back to "View" / "Edit" */
88
+ label: string
89
+ }
90
+
67
91
  /**
68
92
  * Nav link item
69
93
  */
@@ -105,6 +129,7 @@ export interface UseNavContextReturn {
105
129
  // Navigation
106
130
  breadcrumb: ComputedRef<BreadcrumbItem[]>
107
131
  navlinks: ComputedRef<NavLinkItem[]>
132
+ modeToggle: ComputedRef<BreadcrumbModeToggle | null>
108
133
 
109
134
  // Helpers
110
135
  parentConfig: ComputedRef<ParentConfig | undefined>
@@ -125,6 +150,9 @@ export function useNavContext(_options: UseNavContextOptions = {}): UseNavContex
125
150
  // Stack hydrator for entity data (has async-loaded data and labels)
126
151
  const hydrator = useStackHydrator()
127
152
 
153
+ // i18n for the mode-toggle labels (no-op shim outside a kernel)
154
+ const { i18n: kernelI18n, locale: i18nLocale } = useI18n()
155
+
128
156
  function getManager(entityName: string): EntityManager | null {
129
157
  return orchestrator?.get(entityName) ?? null
130
158
  }
@@ -414,6 +442,44 @@ export function useNavContext(_options: UseNavContextOptions = {}): UseNavContex
414
442
  return null
415
443
  })
416
444
 
445
+ // ============================================================================
446
+ // MODE TOGGLE (#1332)
447
+ // ============================================================================
448
+
449
+ function toggleLabel(target: 'show' | 'edit'): string {
450
+ const trace = kernelI18n?.resolve(target === 'edit' ? 'breadcrumb.edit' : 'breadcrumb.view')
451
+ if (trace?.hit) return trace.result
452
+ return target === 'edit' ? 'Edit' : 'View'
453
+ }
454
+
455
+ const modeToggle = computed<BreadcrumbModeToggle | null>(() => {
456
+ void i18nLocale.value // label re-resolves on locale change
457
+
458
+ const semantic = semanticBreadcrumb.value
459
+ const terminal = semantic[semantic.length - 1]
460
+ if (!terminal?.entity || !terminal.id) return null
461
+ if (terminal.kind !== 'entity-show' && terminal.kind !== 'entity-edit') return null
462
+
463
+ const manager = getManager(terminal.entity)
464
+ if (!manager?.routePrefix) return null
465
+
466
+ const current = terminal.kind === 'entity-edit' ? 'edit' : 'show'
467
+ // Target is the OPPOSITE of the current mode — getDefaultItemRoute's
468
+ // edit-preference is a landing default, not a toggle.
469
+ const target = current === 'edit' ? 'show' : 'edit'
470
+ const twinRoute = `${manager.routePrefix}-${target}`
471
+ if (!router.hasRoute(twinRoute)) return null
472
+
473
+ if (target === 'edit' && !manager.canUpdate(entityData.value ?? undefined)) return null
474
+
475
+ return {
476
+ current,
477
+ target,
478
+ to: { name: twinRoute, params: { [manager.idField || 'id']: terminal.id } },
479
+ label: toggleLabel(target),
480
+ }
481
+ })
482
+
417
483
  return {
418
484
  // Analysis
419
485
  navChain,
@@ -427,6 +493,7 @@ export function useNavContext(_options: UseNavContextOptions = {}): UseNavContex
427
493
  // Navigation
428
494
  breadcrumb,
429
495
  navlinks,
496
+ modeToggle,
430
497
 
431
498
  // Helpers
432
499
  parentConfig,
@@ -44,7 +44,7 @@ export interface EntityManagerBase<T = unknown> {
44
44
  // Optional BY DESIGN (#1253): part of the badge/severity presentation
45
45
  // capability; call sites guard (e.g. useEntityItemShowPage checks
46
46
  // `if (!manager.getEntityBadges)`).
47
- getEntityBadges?: (data: T) => Array<{ label: string; severity?: string }>
47
+ getEntityBadges?: (data: T) => EntityBadge[]
48
48
  }
49
49
 
50
50
  // ─── Permissions ─────────────────────────────────────────────────────────────
package/src/index.ts CHANGED
@@ -128,6 +128,9 @@ export type {
128
128
  RelationConfig,
129
129
  NavConfig,
130
130
  StorageCapabilities,
131
+ // PrimeVue severity union (#1281) — type ActionConfig/badge literals against
132
+ // this instead of bare strings.
133
+ ButtonSeverity,
131
134
  } from './types'
132
135
 
133
136
  // Manager Factory
@@ -266,6 +269,7 @@ export {
266
269
  type NavChainType,
267
270
  type BreadcrumbItem,
268
271
  type NavLinkItem,
272
+ type BreadcrumbModeToggle,
269
273
  } from './composables/useNavContext'
270
274
  export {
271
275
  useNavigation,
@@ -6,6 +6,7 @@
6
6
 
7
7
  import { managerFactory, type ManagerFactoryContext } from '../entity/factory.js'
8
8
  import type { EntityManager } from '../entity/EntityManager'
9
+ import type { EntityRecord } from '../types'
9
10
  import { UsersManager } from '../security/UsersManager'
10
11
  import type { UserEntityOptions } from './KernelContext.types'
11
12
  import type { KernelContext } from './KernelContext'
@@ -22,10 +23,10 @@ export function applyEntityMethods(KernelContextClass: { prototype: KernelContex
22
23
  * Uses managerFactory to resolve config into a manager instance.
23
24
  * Registers with Orchestrator for CRUD operations.
24
25
  */
25
- proto.entity = function (
26
+ proto.entity = function <T extends EntityRecord>(
26
27
  this: Self,
27
28
  name: string,
28
- config: string | Record<string, unknown> | EntityManager
29
+ config: string | Record<string, unknown> | EntityManager<T>
29
30
  ): Self {
30
31
  const factoryContext = {
31
32
  storageResolver: this._kernel.options.storageResolver,
@@ -46,6 +46,7 @@ import type { DeferredRegistry } from '../deferred/DeferredRegistry.js'
46
46
  import type { SecurityChecker } from '../entity/auth/SecurityChecker'
47
47
  import type { PermissionRegistry } from '../security/PermissionRegistry'
48
48
  import type { EntityManager } from '../entity/EntityManager'
49
+ import type { EntityRecord } from '../types'
49
50
  import type { I18n } from '../i18n/I18n'
50
51
  import type { AliasPattern, MessagesBundle, TranslationProvider } from '../i18n/types'
51
52
 
@@ -195,7 +196,9 @@ export class KernelContext {
195
196
  // eslint-disable-next-line @typescript-eslint/no-empty-object-type
196
197
  export interface KernelContext {
197
198
  // Entities (KernelContext.entities.ts)
198
- entity(name: string, config: string | Record<string, unknown> | EntityManager): this
199
+ // Generic so narrowed managers pass: EntityManager<T> is invariant in T,
200
+ // so EntityManager<EntityRecord> would reject every typed subclass (#1281).
201
+ entity<T extends EntityRecord>(name: string, config: string | Record<string, unknown> | EntityManager<T>): this
199
202
  userEntity(options: UserEntityOptions): this
200
203
 
201
204
  // Routing (KernelContext.routing.ts)
package/src/plugin.ts CHANGED
@@ -63,6 +63,8 @@ export interface FeaturesConfig {
63
63
  auth?: boolean
64
64
  poweredBy?: boolean
65
65
  breadcrumb?: boolean
66
+ /** Opt-in (#1332): View↔Edit toggle on the breadcrumb terminal crumb */
67
+ breadcrumbModeToggle?: boolean
66
68
  [key: string]: boolean | undefined
67
69
  }
68
70