qdadm 1.6.1 → 1.9.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 +1 -1
- package/src/components/SeverityTag.vue +26 -11
- package/src/components/layout/AppLayout.vue +3 -1
- package/src/components/show/ShowDisplay.vue +25 -8
- package/src/composables/useEntityItemPage.ts +3 -0
- package/src/composables/useEntityItemShowPage.ts +15 -2
- package/src/composables/useSidebarState.ts +24 -0
- package/src/entity/EntityManager.ts +39 -7
- package/src/index.ts +4 -0
- package/src/notifications/NotificationPanel.vue +1 -0
- package/src/notifications/styles.scss +9 -4
package/package.json
CHANGED
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* SeverityTag - Auto-discovers severity from EntityManager
|
|
4
4
|
*
|
|
5
|
+
* Renders a PrimeVue Tag for simple severity strings, or a rich badge
|
|
6
|
+
* with icon (including animated spinners) when the severity map provides
|
|
7
|
+
* a descriptor with an `icon` field.
|
|
8
|
+
*
|
|
5
9
|
* Entity can be specified explicitly or auto-discovered from list context.
|
|
6
10
|
*
|
|
7
11
|
* @example
|
|
@@ -50,22 +54,33 @@ const manager = computed<EntityManager | null>(() => {
|
|
|
50
54
|
return orchestrator?.get(resolvedEntity.value) ?? null
|
|
51
55
|
})
|
|
52
56
|
|
|
53
|
-
const
|
|
54
|
-
if (
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
57
|
+
const fieldValue = computed(() => {
|
|
58
|
+
if (props.value === null || props.value === undefined) return ''
|
|
59
|
+
return typeof props.value === 'boolean' ? String(props.value) : props.value
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
const descriptor = computed(() => {
|
|
63
|
+
if (!manager.value?.getSeverityDescriptor) {
|
|
64
|
+
return { severity: manager.value?.getSeverity?.(props.field, fieldValue.value as string | number, props.defaultSeverity) ?? props.defaultSeverity }
|
|
65
|
+
}
|
|
66
|
+
return manager.value.getSeverityDescriptor(props.field, fieldValue.value as string | number, props.defaultSeverity)
|
|
62
67
|
})
|
|
63
68
|
|
|
69
|
+
const severity = computed(() => descriptor.value.severity)
|
|
70
|
+
|
|
64
71
|
const displayLabel = computed(() => {
|
|
65
|
-
return props.label ?? props.value
|
|
72
|
+
return props.label ?? descriptor.value.label ?? props.value
|
|
66
73
|
})
|
|
74
|
+
|
|
75
|
+
const hasIcon = computed(() => !!descriptor.value.icon)
|
|
67
76
|
</script>
|
|
68
77
|
|
|
69
78
|
<template>
|
|
70
|
-
|
|
79
|
+
<!-- Rich badge with icon -->
|
|
80
|
+
<span v-if="hasIcon" class="p-tag p-component" :class="`p-tag-${severity}`">
|
|
81
|
+
<i :class="descriptor.icon" />
|
|
82
|
+
<span class="p-tag-label">{{ displayLabel }}</span>
|
|
83
|
+
</span>
|
|
84
|
+
<!-- Simple PrimeVue Tag -->
|
|
85
|
+
<Tag v-else :value="displayLabel" :severity="severity" />
|
|
71
86
|
</template>
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
import { ref, watch, onMounted, onUnmounted, computed, inject, provide, useSlots } from 'vue'
|
|
16
|
+
import { SIDEBAR_COLLAPSED_KEY } from '../../composables/useSidebarState'
|
|
16
17
|
import { RouterLink, RouterView, useRouter, useRoute } from 'vue-router'
|
|
17
18
|
import { useNavigation, type NavSection } from '../../composables/useNavigation'
|
|
18
19
|
import { useApp } from '../../composables/useApp'
|
|
@@ -69,8 +70,9 @@ const collapsedSections = ref<Record<string, boolean>>({})
|
|
|
69
70
|
const sidebarOpen = ref<boolean>(false)
|
|
70
71
|
const MOBILE_BREAKPOINT = 768
|
|
71
72
|
|
|
72
|
-
// Desktop sidebar collapsed state
|
|
73
|
+
// Desktop sidebar collapsed state (provided for child components via useSidebarState)
|
|
73
74
|
const sidebarCollapsed = ref<boolean>(false)
|
|
75
|
+
provide(SIDEBAR_COLLAPSED_KEY, sidebarCollapsed)
|
|
74
76
|
const COLLAPSED_STORAGE_KEY = computed<string>(() => `${app.shortName.toLowerCase()}_sidebar_collapsed`)
|
|
75
77
|
|
|
76
78
|
function toggleSidebar(): void {
|
|
@@ -71,7 +71,7 @@ interface FieldConfig {
|
|
|
71
71
|
optionLabel?: string
|
|
72
72
|
optionValue?: string
|
|
73
73
|
// Badge options
|
|
74
|
-
severity?: string | ((value: unknown) => string)
|
|
74
|
+
severity?: string | { severity: string; icon?: string; label?: string } | ((value: unknown) => string | { severity: string; icon?: string; label?: string })
|
|
75
75
|
// Image options
|
|
76
76
|
imageWidth?: string
|
|
77
77
|
imageHeight?: string
|
|
@@ -200,14 +200,18 @@ const badgeValue = computed(() => {
|
|
|
200
200
|
return String(props.value)
|
|
201
201
|
})
|
|
202
202
|
|
|
203
|
-
// Badge severity
|
|
204
|
-
const
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
return
|
|
203
|
+
// Badge descriptor (normalized to { severity, icon? })
|
|
204
|
+
const badgeDescriptor = computed(() => {
|
|
205
|
+
const raw = typeof props.field.severity === 'function'
|
|
206
|
+
? props.field.severity(props.value)
|
|
207
|
+
: props.field.severity
|
|
208
|
+
if (!raw) return { severity: 'info' }
|
|
209
|
+
return typeof raw === 'string' ? { severity: raw } : raw
|
|
209
210
|
})
|
|
210
211
|
|
|
212
|
+
const badgeSeverity = computed(() => badgeDescriptor.value.severity)
|
|
213
|
+
const badgeIcon = computed(() => badgeDescriptor.value.icon)
|
|
214
|
+
|
|
211
215
|
// JSON formatted
|
|
212
216
|
const jsonValue = computed(() => {
|
|
213
217
|
if (isEmpty.value) return '-'
|
|
@@ -316,7 +320,15 @@ const imageStyle = computed(() => ({
|
|
|
316
320
|
<!-- JSON -->
|
|
317
321
|
<pre v-else-if="displayType === 'json'" class="show-display show-display--json">{{ jsonValue }}</pre>
|
|
318
322
|
|
|
319
|
-
<!-- Badge -->
|
|
323
|
+
<!-- Badge (with optional icon) -->
|
|
324
|
+
<span
|
|
325
|
+
v-else-if="displayType === 'badge' && badgeIcon"
|
|
326
|
+
class="show-display show-display--badge p-tag p-component"
|
|
327
|
+
:class="`p-tag-${badgeSeverity}`"
|
|
328
|
+
>
|
|
329
|
+
<i :class="badgeIcon" />
|
|
330
|
+
<span class="p-tag-label">{{ badgeValue }}</span>
|
|
331
|
+
</span>
|
|
320
332
|
<Tag
|
|
321
333
|
v-else-if="displayType === 'badge'"
|
|
322
334
|
:value="badgeValue"
|
|
@@ -335,6 +347,11 @@ const imageStyle = computed(() => ({
|
|
|
335
347
|
display: inline;
|
|
336
348
|
}
|
|
337
349
|
|
|
350
|
+
.show-display--badge {
|
|
351
|
+
display: inline-flex;
|
|
352
|
+
align-items: center;
|
|
353
|
+
}
|
|
354
|
+
|
|
338
355
|
.show-display--empty {
|
|
339
356
|
color: var(--p-text-muted-color, #6c757d);
|
|
340
357
|
}
|
|
@@ -78,6 +78,9 @@ export interface EntityManager {
|
|
|
78
78
|
canCreate: () => boolean
|
|
79
79
|
canUpdate: (data?: unknown) => boolean
|
|
80
80
|
canDelete: (data?: unknown) => boolean
|
|
81
|
+
hasSeverityMap?: (field: string) => boolean
|
|
82
|
+
getSeverity?: (field: string, value: string | number, defaultSeverity?: string) => string
|
|
83
|
+
getSeverityDescriptor?: (field: string, value: string | number, defaultSeverity?: string) => { severity: string; icon?: string; label?: string }
|
|
81
84
|
}
|
|
82
85
|
|
|
83
86
|
/**
|
|
@@ -384,10 +384,11 @@ export function useEntityItemShowPage(
|
|
|
384
384
|
schemaType = type,
|
|
385
385
|
label = '',
|
|
386
386
|
reference = '',
|
|
387
|
+
severity,
|
|
387
388
|
...rest
|
|
388
389
|
} = fieldConfig
|
|
389
390
|
|
|
390
|
-
|
|
391
|
+
let resolvedDisplayType = TYPE_MAPPINGS[type] || TYPE_MAPPINGS[schemaType] || 'text'
|
|
391
392
|
const resolvedLabel = label || snakeCaseToTitle(name)
|
|
392
393
|
|
|
393
394
|
// Auto-set reference route if reference entity is specified
|
|
@@ -402,13 +403,25 @@ export function useEntityItemShowPage(
|
|
|
402
403
|
}
|
|
403
404
|
}
|
|
404
405
|
|
|
406
|
+
// Auto-inject severity from manager's severity maps
|
|
407
|
+
let resolvedSeverity = severity
|
|
408
|
+
if (!resolvedSeverity && manager.hasSeverityMap?.(name)) {
|
|
409
|
+
resolvedSeverity = manager.getSeverityDescriptor
|
|
410
|
+
? (value: unknown) => manager.getSeverityDescriptor!(name, value as string | number, 'secondary')
|
|
411
|
+
: (value: unknown) => manager.getSeverity!(name, value as string | number, 'secondary')
|
|
412
|
+
if (resolvedDisplayType === 'text') {
|
|
413
|
+
resolvedDisplayType = 'badge'
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
405
417
|
return {
|
|
406
418
|
name,
|
|
407
|
-
type:
|
|
419
|
+
type: resolvedDisplayType,
|
|
408
420
|
schemaType,
|
|
409
421
|
label: resolvedLabel,
|
|
410
422
|
reference,
|
|
411
423
|
referenceRoute,
|
|
424
|
+
severity: resolvedSeverity,
|
|
412
425
|
...rest,
|
|
413
426
|
}
|
|
414
427
|
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useSidebarState - Access sidebar collapsed state from any component
|
|
3
|
+
*
|
|
4
|
+
* The sidebar collapsed ref is provided by AppLayout via Vue's provide/inject.
|
|
5
|
+
* This composable gives blocks rendered inside the sidebar (including zone blocks)
|
|
6
|
+
* programmatic access to the collapsed state.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```ts
|
|
10
|
+
* import { useSidebarState } from 'qdadm'
|
|
11
|
+
*
|
|
12
|
+
* const { collapsed } = useSidebarState()
|
|
13
|
+
* // collapsed.value === true when sidebar is in icon-only mode
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { inject, ref, type Ref } from 'vue'
|
|
18
|
+
|
|
19
|
+
export const SIDEBAR_COLLAPSED_KEY = Symbol('qdadm-sidebar-collapsed') as symbol & { __type: Ref<boolean> }
|
|
20
|
+
|
|
21
|
+
export function useSidebarState(): { collapsed: Ref<boolean> } {
|
|
22
|
+
const collapsed = inject<Ref<boolean>>(SIDEBAR_COLLAPSED_KEY, ref(false))
|
|
23
|
+
return { collapsed }
|
|
24
|
+
}
|
|
@@ -158,6 +158,22 @@ export interface EntityBadge {
|
|
|
158
158
|
severity?: 'secondary' | 'info' | 'success' | 'warn' | 'danger' | 'contrast'
|
|
159
159
|
}
|
|
160
160
|
|
|
161
|
+
/**
|
|
162
|
+
* Rich severity descriptor for a field value.
|
|
163
|
+
* Extends beyond a simple severity string to support icons and label overrides.
|
|
164
|
+
*/
|
|
165
|
+
export interface SeverityDescriptor {
|
|
166
|
+
severity: string
|
|
167
|
+
icon?: string
|
|
168
|
+
label?: string
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** A severity map value: plain string (backward compat) or rich descriptor */
|
|
172
|
+
export type SeverityMapValue = string | SeverityDescriptor
|
|
173
|
+
|
|
174
|
+
/** Severity map: field value → severity string or descriptor */
|
|
175
|
+
export type SeverityMap = Record<string, SeverityMapValue>
|
|
176
|
+
|
|
161
177
|
/**
|
|
162
178
|
* EntityManager constructor options
|
|
163
179
|
*/
|
|
@@ -243,7 +259,7 @@ export class EntityManager<T extends EntityRecord = EntityRecord> {
|
|
|
243
259
|
protected _nav: NavConfig
|
|
244
260
|
|
|
245
261
|
protected _hooks: HookRegistry | null = null
|
|
246
|
-
protected _severityMaps: Record<string,
|
|
262
|
+
protected _severityMaps: Record<string, SeverityMap> = {}
|
|
247
263
|
|
|
248
264
|
protected _cache: CacheState<T> = {
|
|
249
265
|
items: [],
|
|
@@ -939,24 +955,40 @@ export class EntityManager<T extends EntityRecord = EntityRecord> {
|
|
|
939
955
|
// ============ SEVERITY MAPS ============
|
|
940
956
|
|
|
941
957
|
/**
|
|
942
|
-
* Set severity map for a field
|
|
958
|
+
* Set severity map for a field.
|
|
959
|
+
* Values can be plain severity strings or rich descriptors with icon/label.
|
|
943
960
|
*/
|
|
944
|
-
setSeverityMap(field: string, map:
|
|
961
|
+
setSeverityMap(field: string, map: SeverityMap): this {
|
|
945
962
|
this._severityMaps[field] = map
|
|
946
963
|
return this
|
|
947
964
|
}
|
|
948
965
|
|
|
949
966
|
/**
|
|
950
|
-
* Get severity for a field value
|
|
967
|
+
* Get severity string for a field value (backward compat).
|
|
968
|
+
* Extracts .severity from descriptors automatically.
|
|
951
969
|
*/
|
|
952
970
|
getSeverity(
|
|
953
971
|
field: string,
|
|
954
972
|
value: string | number,
|
|
955
973
|
defaultSeverity: string = 'secondary'
|
|
956
974
|
): string {
|
|
957
|
-
const
|
|
958
|
-
if (!
|
|
959
|
-
return
|
|
975
|
+
const entry = this._severityMaps[field]?.[String(value)]
|
|
976
|
+
if (!entry) return defaultSeverity
|
|
977
|
+
return typeof entry === 'string' ? entry : entry.severity
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
/**
|
|
981
|
+
* Get full severity descriptor for a field value.
|
|
982
|
+
* Returns normalized descriptor (plain strings wrapped as { severity }).
|
|
983
|
+
*/
|
|
984
|
+
getSeverityDescriptor(
|
|
985
|
+
field: string,
|
|
986
|
+
value: string | number,
|
|
987
|
+
defaultSeverity: string = 'secondary'
|
|
988
|
+
): SeverityDescriptor {
|
|
989
|
+
const entry = this._severityMaps[field]?.[String(value)]
|
|
990
|
+
if (!entry) return { severity: defaultSeverity }
|
|
991
|
+
return typeof entry === 'string' ? { severity: entry } : entry
|
|
960
992
|
}
|
|
961
993
|
|
|
962
994
|
/**
|
package/src/index.ts
CHANGED
|
@@ -92,6 +92,9 @@ export {
|
|
|
92
92
|
createEntityManager,
|
|
93
93
|
type EntityManagerOptions,
|
|
94
94
|
type EntityBadge,
|
|
95
|
+
type SeverityDescriptor,
|
|
96
|
+
type SeverityMapValue,
|
|
97
|
+
type SeverityMap,
|
|
95
98
|
type RoutingContext,
|
|
96
99
|
type PresaveContext,
|
|
97
100
|
type PostsaveContext,
|
|
@@ -291,6 +294,7 @@ export {
|
|
|
291
294
|
} from './composables/useUserImpersonator'
|
|
292
295
|
export { useCurrentEntity, type UseCurrentEntityReturn } from './composables/useCurrentEntity'
|
|
293
296
|
export { useChildPage, type UseChildPageReturn } from './composables/useChildPage'
|
|
297
|
+
export { useSidebarState, SIDEBAR_COLLAPSED_KEY } from './composables/useSidebarState'
|
|
294
298
|
|
|
295
299
|
// ════════════════════════════════════════════════════════════════════════════
|
|
296
300
|
// COMPONENTS
|
|
@@ -156,6 +156,7 @@ function formatTime(timestamp: number): string {
|
|
|
156
156
|
left: calc(var(--fad-sidebar-width, 15rem) + 0.375rem);
|
|
157
157
|
bottom: 0.5rem;
|
|
158
158
|
width: 22rem;
|
|
159
|
+
min-height: 6.6rem;
|
|
159
160
|
max-height: 50vh;
|
|
160
161
|
background: var(--p-surface-0, #ffffff);
|
|
161
162
|
border: 1px solid var(--p-surface-200, #e2e8f0);
|
|
@@ -18,10 +18,15 @@ $_content-duration: 0.1s;
|
|
|
18
18
|
transition: opacity $_content-duration ease $_content-delay;
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
/* Status label: delayed fade-in on expand, instant hide on collapse.
|
|
22
|
+
Matches SidebarBox content behavior (.sidebar-box-content). */
|
|
23
|
+
.sidebar-notification-status .status-label {
|
|
24
|
+
opacity: 1;
|
|
25
|
+
transition: opacity $_content-duration ease $_content-delay;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.sidebar--collapsed .sidebar-notification-status .status-label {
|
|
22
29
|
opacity: 0;
|
|
23
30
|
visibility: hidden;
|
|
24
|
-
|
|
25
|
-
overflow: hidden;
|
|
26
|
-
transition: opacity 0s, visibility 0s, height 0s;
|
|
31
|
+
transition: opacity 0s, visibility 0s;
|
|
27
32
|
}
|