@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 +3 -3
- package/src/components/editors/JsonStructuredField.vue +4 -3
- package/src/components/layout/defaults/DefaultBreadcrumb.vue +38 -1
- package/src/components/show/ShowPage.vue +3 -13
- package/src/composables/index.ts +1 -0
- package/src/composables/useEntityItemShowPage.ts +6 -3
- package/src/composables/useNavContext.ts +67 -0
- package/src/entity/EntityManager.interface.ts +1 -1
- package/src/index.ts +4 -0
- package/src/kernel/KernelContext.entities.ts +3 -2
- package/src/kernel/KernelContext.ts +4 -1
- package/src/plugin.ts +2 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@quazardous/qdadm",
|
|
3
|
-
"version": "2.
|
|
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.
|
|
50
|
-
"@quazardous/qddebug": "^0.
|
|
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 '
|
|
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'
|
|
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
|
*/
|
package/src/composables/index.ts
CHANGED
|
@@ -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
|
|
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
|
|
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?:
|
|
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) =>
|
|
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
|
-
|
|
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