@quazardous/qdadm 2.24.0 → 2.26.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/item/FieldGroups.vue +44 -3
- package/src/components/show/ShowPage.vue +32 -3
- package/src/composables/useEntityItemFormPage.ts +31 -2
- package/src/composables/useNavigation.ts +13 -1
- package/src/entity/EntityManager.crud.ts +6 -4
- package/src/entity/EntityManager.interface.ts +4 -2
- package/src/entity/EntityManager.ts +3 -1
- package/src/entity/EntityManager.types.ts +13 -0
- package/src/kernel/Kernel.routing.ts +7 -2
- package/src/kernel/Kernel.vue.ts +3 -0
- package/src/kernel/KernelContext.routing.ts +4 -2
- package/src/security/RolesManager.ts +6 -0
- package/src/security/UsersManager.ts +6 -0
package/package.json
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
* </FieldGroups>
|
|
21
21
|
* ```
|
|
22
22
|
*/
|
|
23
|
-
import { computed, type PropType } from 'vue'
|
|
23
|
+
import { computed, ref, watch, type PropType } from 'vue'
|
|
24
24
|
import Tabs from 'primevue/tabs'
|
|
25
25
|
import TabList from 'primevue/tablist'
|
|
26
26
|
import Tab from 'primevue/tab'
|
|
@@ -84,8 +84,21 @@ const props = defineProps({
|
|
|
84
84
|
type: String as PropType<LayoutMode>,
|
|
85
85
|
default: 'sections',
|
|
86
86
|
},
|
|
87
|
+
/**
|
|
88
|
+
* The active group (`tabs`: a group name) or the open ones (`accordion`:
|
|
89
|
+
* names), for `v-model:active` (#2435). Optional: without it the component
|
|
90
|
+
* keeps its own, starting on the first group.
|
|
91
|
+
*/
|
|
92
|
+
active: {
|
|
93
|
+
type: [String, Array] as PropType<string | string[] | null>,
|
|
94
|
+
default: undefined,
|
|
95
|
+
},
|
|
87
96
|
})
|
|
88
97
|
|
|
98
|
+
const emit = defineEmits<{
|
|
99
|
+
'update:active': [value: string | string[]]
|
|
100
|
+
}>()
|
|
101
|
+
|
|
89
102
|
// Expose slots for field rendering
|
|
90
103
|
defineSlots<{
|
|
91
104
|
field: (props: { field: BaseFieldConfig; value: unknown; groupName: string }) => unknown
|
|
@@ -105,6 +118,34 @@ const displayGroups = computed(() => {
|
|
|
105
118
|
})
|
|
106
119
|
})
|
|
107
120
|
|
|
121
|
+
// The active tab / open panels are state, not a constant: a re-render must not
|
|
122
|
+
// send the user back to the first group (#2435). An app owning it passes
|
|
123
|
+
// v-model:active; otherwise this is the only copy.
|
|
124
|
+
const localActive = ref<string | string[] | null | undefined>(props.active)
|
|
125
|
+
watch(
|
|
126
|
+
() => props.active,
|
|
127
|
+
(value) => {
|
|
128
|
+
if (value !== undefined) localActive.value = value
|
|
129
|
+
}
|
|
130
|
+
)
|
|
131
|
+
const firstGroupName = () => displayGroups.value[0]?.name || '0'
|
|
132
|
+
const activeTab = computed(() => (typeof localActive.value === 'string' ? localActive.value : firstGroupName()))
|
|
133
|
+
const openPanels = computed(() => {
|
|
134
|
+
const value = localActive.value
|
|
135
|
+
if (Array.isArray(value)) return value
|
|
136
|
+
return typeof value === 'string' ? [value] : [firstGroupName()]
|
|
137
|
+
})
|
|
138
|
+
function setActive(value: string | string[]) {
|
|
139
|
+
localActive.value = value
|
|
140
|
+
emit('update:active', value)
|
|
141
|
+
}
|
|
142
|
+
function onTabChange(value: string | number) {
|
|
143
|
+
setActive(String(value))
|
|
144
|
+
}
|
|
145
|
+
function onPanelsChange(value: unknown) {
|
|
146
|
+
setActive(Array.isArray(value) ? value.map(String) : value == null ? [] : [String(value)])
|
|
147
|
+
}
|
|
148
|
+
|
|
108
149
|
// Get value from data
|
|
109
150
|
function getValue(fieldName: string): unknown {
|
|
110
151
|
return props.data?.[fieldName]
|
|
@@ -227,7 +268,7 @@ function getBadgeValue(group: FieldGroup): string | number | null {
|
|
|
227
268
|
|
|
228
269
|
<!-- Tabs layout -->
|
|
229
270
|
<template v-else-if="layout === 'tabs'">
|
|
230
|
-
<Tabs :value="
|
|
271
|
+
<Tabs :value="activeTab" @update:value="onTabChange">
|
|
231
272
|
<TabList>
|
|
232
273
|
<Tab
|
|
233
274
|
v-for="group in displayGroups"
|
|
@@ -275,7 +316,7 @@ function getBadgeValue(group: FieldGroup): string | number | null {
|
|
|
275
316
|
|
|
276
317
|
<!-- Accordion layout -->
|
|
277
318
|
<template v-else-if="layout === 'accordion'">
|
|
278
|
-
<Accordion :value="
|
|
319
|
+
<Accordion :value="openPanels" multiple @update:value="onPanelsChange">
|
|
279
320
|
<AccordionPanel
|
|
280
321
|
v-for="group in displayGroups"
|
|
281
322
|
:key="group.name"
|
|
@@ -35,7 +35,8 @@
|
|
|
35
35
|
* - #fields: Field content (required) - renders in right column (or full width if no media)
|
|
36
36
|
* - #footer: Custom footer (replaces ShowActions if provided)
|
|
37
37
|
* - #error: Custom error display
|
|
38
|
-
* - #loading: Custom
|
|
38
|
+
* - #loading: Custom display for the first load. A reload of a record already
|
|
39
|
+
* on screen keeps it there, marked busy (#2435).
|
|
39
40
|
*/
|
|
40
41
|
import { computed, type PropType } from 'vue'
|
|
41
42
|
import PageHeader from '../layout/PageHeader.vue'
|
|
@@ -203,8 +204,11 @@ type ShowFieldValue = string | number | boolean | Date | Record<string, unknown>
|
|
|
203
204
|
<!-- Toolbar slot (between header and content) -->
|
|
204
205
|
<slot name="toolbar" />
|
|
205
206
|
|
|
206
|
-
<!--
|
|
207
|
-
|
|
207
|
+
<!-- First load only. A reload of a record already shown keeps the content
|
|
208
|
+
mounted (#2435): unmounting it threw away the active tab, open panels,
|
|
209
|
+
scroll and any local state — on every action reload and every live
|
|
210
|
+
update of the record. -->
|
|
211
|
+
<template v-if="loading && !data">
|
|
208
212
|
<slot name="loading">
|
|
209
213
|
<div class="loading-state">
|
|
210
214
|
<i class="pi pi-spin pi-spinner" style="font-size: 2rem"></i>
|
|
@@ -223,6 +227,7 @@ type ShowFieldValue = string | number | boolean | Date | Record<string, unknown>
|
|
|
223
227
|
|
|
224
228
|
<!-- Content -->
|
|
225
229
|
<template v-else>
|
|
230
|
+
<div v-if="loading" class="show-refreshing" role="progressbar" aria-label="Refreshing" />
|
|
226
231
|
<!-- Single content body; Card wrapper is conditional (#1193) -->
|
|
227
232
|
<CardShell :card="cardWrapper">
|
|
228
233
|
<!-- Grid layout with optional media zone -->
|
|
@@ -230,6 +235,7 @@ type ShowFieldValue = string | number | boolean | Date | Record<string, unknown>
|
|
|
230
235
|
class="show-content"
|
|
231
236
|
:class="{ 'show-content--with-media': slots.media }"
|
|
232
237
|
:style="slots.media ? { '--media-width': mediaWidth } : {}"
|
|
238
|
+
:aria-busy="loading ? 'true' : undefined"
|
|
233
239
|
>
|
|
234
240
|
<!-- Media zone (optional) -->
|
|
235
241
|
<div v-if="slots.media" class="show-media">
|
|
@@ -304,6 +310,29 @@ type ShowFieldValue = string | number | boolean | Date | Record<string, unknown>
|
|
|
304
310
|
padding: 3rem;
|
|
305
311
|
}
|
|
306
312
|
|
|
313
|
+
/* A record being refreshed stays on screen: a thin bar says so (#2435). */
|
|
314
|
+
.show-refreshing {
|
|
315
|
+
height: 3px;
|
|
316
|
+
margin-bottom: -1rem; /* inside the page gap: the content does not move */
|
|
317
|
+
background: linear-gradient(90deg, transparent, var(--p-primary-color, #3b82f6), transparent);
|
|
318
|
+
background-size: 40% 100%;
|
|
319
|
+
background-repeat: no-repeat;
|
|
320
|
+
animation: show-refreshing 1.1s ease-in-out infinite;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
@keyframes show-refreshing {
|
|
324
|
+
from { background-position: -40% 0; }
|
|
325
|
+
to { background-position: 140% 0; }
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
@media (prefers-reduced-motion: reduce) {
|
|
329
|
+
.show-refreshing {
|
|
330
|
+
animation: none;
|
|
331
|
+
background: var(--p-primary-color, #3b82f6);
|
|
332
|
+
opacity: 0.4;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
307
336
|
.show-error-message {
|
|
308
337
|
margin: 1rem 0;
|
|
309
338
|
}
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
* </FormPage>
|
|
25
25
|
* ```
|
|
26
26
|
*/
|
|
27
|
-
import { ref, computed, watch, onMounted, onUnmounted, provide, type Ref } from 'vue'
|
|
27
|
+
import { ref, computed, watch, onMounted, onUnmounted, provide, inject, type Ref } from 'vue'
|
|
28
28
|
import { useRouter, useRoute } from 'vue-router'
|
|
29
29
|
import { useConfirm } from 'primevue/useconfirm'
|
|
30
30
|
import { requireDeleteConfirmation } from './confirmDelete'
|
|
@@ -56,6 +56,9 @@ import { createOrchestratorToast } from './useOrchestratorToast'
|
|
|
56
56
|
import { useActionRegistry } from './useActionRegistry'
|
|
57
57
|
import { TYPE_MAPPINGS, TYPE_VALIDATORS, snakeCaseToTitle, isEmpty } from './useEntityItemFormPage.types'
|
|
58
58
|
|
|
59
|
+
/** Missing fields already reported, as `entity:field` — each is said once per page load of the app (#2484). */
|
|
60
|
+
const reportedMissingFields = new Set<string>()
|
|
61
|
+
|
|
59
62
|
// Re-export public types
|
|
60
63
|
export type {
|
|
61
64
|
FieldDefinition,
|
|
@@ -126,6 +129,7 @@ export function useEntityItemFormPage<T extends Record<string, unknown> = Record
|
|
|
126
129
|
const router = useRouter()
|
|
127
130
|
const route = useRoute()
|
|
128
131
|
const confirm = useConfirm() as ConfirmService
|
|
132
|
+
const debug = inject<boolean>('qdadmDebug', false)
|
|
129
133
|
|
|
130
134
|
// Use useEntityItemPage for common infrastructure
|
|
131
135
|
const itemPage = useEntityItemPage({
|
|
@@ -247,6 +251,28 @@ export function useEntityItemFormPage<T extends Record<string, unknown> = Record
|
|
|
247
251
|
|
|
248
252
|
// ============ LOADING ============
|
|
249
253
|
|
|
254
|
+
/**
|
|
255
|
+
* A field the form edits that the loaded record does not carry at all —
|
|
256
|
+
* `undefined`, not empty — is shown empty and saved back empty (#2484). Said
|
|
257
|
+
* in debug mode only, once per entity and field: some APIs omit null fields.
|
|
258
|
+
* Password fields are left out: no API returns one.
|
|
259
|
+
*/
|
|
260
|
+
function reportMissingFields(record: Record<string, unknown> | null): void {
|
|
261
|
+
if (!record || typeof record !== 'object') return
|
|
262
|
+
const entityName = String((manager as { name?: unknown }).name ?? entity)
|
|
263
|
+
for (const field of fields.value) {
|
|
264
|
+
if (field.type === 'password' || field.name.includes('.')) continue
|
|
265
|
+
if (record[field.name] !== undefined) continue
|
|
266
|
+
const key = `${entityName}:${field.name}`
|
|
267
|
+
if (reportedMissingFields.has(key)) continue
|
|
268
|
+
reportedMissingFields.add(key)
|
|
269
|
+
console.warn(
|
|
270
|
+
`[qdadm] ${entityName} #${entityId.value} loaded without "${field.name}", which the form edits: ` +
|
|
271
|
+
'saving would send it back empty. Check that the record endpoint returns it.'
|
|
272
|
+
)
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
250
276
|
async function load(): Promise<void> {
|
|
251
277
|
if (!isEdit.value) {
|
|
252
278
|
data.value = deepClone(initialData) as T
|
|
@@ -256,8 +282,11 @@ export function useEntityItemFormPage<T extends Record<string, unknown> = Record
|
|
|
256
282
|
|
|
257
283
|
loading.value = true
|
|
258
284
|
try {
|
|
259
|
-
|
|
285
|
+
// The item itself, never a cached list row: a list row may be a summary,
|
|
286
|
+
// and whatever this form shows empty it saves back empty (#2484).
|
|
287
|
+
const responseData = await manager.get(entityId.value!, undefined, { listCache: false })
|
|
260
288
|
const transformed = transformLoad(responseData)
|
|
289
|
+
if (debug) reportMissingFields(transformed as Record<string, unknown>)
|
|
261
290
|
data.value = transformed as T
|
|
262
291
|
originalData.value = deepClone(transformed)
|
|
263
292
|
takeSnapshot()
|
|
@@ -93,15 +93,27 @@ export function useNavigation(): UseNavigationReturn {
|
|
|
93
93
|
const alterVersion = ref(0)
|
|
94
94
|
|
|
95
95
|
/**
|
|
96
|
-
* Check if user can access a nav item
|
|
96
|
+
* Check if user can access a nav item: canList() when it opens a list route, canRead() otherwise
|
|
97
97
|
*/
|
|
98
98
|
function canAccessNavItem(item: NavItem): boolean {
|
|
99
99
|
if (!item.entity || !orchestrator) return true
|
|
100
100
|
const manager = orchestrator.get(item.entity)
|
|
101
101
|
if (!manager) return true
|
|
102
|
+
// A menu entry is shown to whoever may do what its page does (#2497): a
|
|
103
|
+
// ctx.crud() entry opens a list, so it follows `list`. A route that
|
|
104
|
+
// declares no action, or is not registered, keeps `read`.
|
|
105
|
+
if (routeAction(item.route) === 'list' && manager.canList) return manager.canList()
|
|
102
106
|
return manager.canRead()
|
|
103
107
|
}
|
|
104
108
|
|
|
109
|
+
function routeAction(name: string): string | undefined {
|
|
110
|
+
try {
|
|
111
|
+
return router.resolve({ name }).meta?.entityAction as string | undefined
|
|
112
|
+
} catch {
|
|
113
|
+
return undefined
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
105
117
|
// Note: Auth signal listeners removed - Kernel.invalidateApp() remounts entire app
|
|
106
118
|
// on auth changes, so composable is re-initialized with fresh state
|
|
107
119
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ListParams, ListResult } from '../types'
|
|
2
|
-
import type { EntityManagerInternal, Orchestrator, RoutingContext } from './EntityManager.types'
|
|
2
|
+
import type { EntityManagerInternal, GetOptions, Orchestrator, RoutingContext } from './EntityManager.types'
|
|
3
3
|
|
|
4
4
|
type Self = EntityManagerInternal<any>
|
|
5
5
|
|
|
@@ -208,7 +208,8 @@ export function applyCrudMethods(EntityManagerClass: { prototype: any }): void {
|
|
|
208
208
|
proto.get = async function (
|
|
209
209
|
this: Self,
|
|
210
210
|
id: string | number,
|
|
211
|
-
context?: RoutingContext
|
|
211
|
+
context?: RoutingContext,
|
|
212
|
+
options: GetOptions = {}
|
|
212
213
|
): Promise<any> {
|
|
213
214
|
const { storage, endpoint } = this._normalizeResolveResult(
|
|
214
215
|
this.resolveStorage('get', context),
|
|
@@ -287,8 +288,9 @@ export function applyCrudMethods(EntityManagerClass: { prototype: any }): void {
|
|
|
287
288
|
|
|
288
289
|
// ── Symmetric mode (default): try list cache ──
|
|
289
290
|
|
|
290
|
-
// Try cache first if valid and complete
|
|
291
|
-
|
|
291
|
+
// Try cache first if valid and complete — unless the caller needs the item
|
|
292
|
+
// itself: a list row may be a summary (#2484).
|
|
293
|
+
if (options.listCache !== false && cache.valid && !this.overflow) {
|
|
292
294
|
const cached = cache.items.find(
|
|
293
295
|
(item: any) => String(item[this.idField]) === idStr
|
|
294
296
|
)
|
|
@@ -71,7 +71,7 @@ export interface EntityManagerPermissions<T = unknown> {
|
|
|
71
71
|
* Read operations + delete — used by list pages and other read-oriented views.
|
|
72
72
|
*/
|
|
73
73
|
export interface EntityManagerRead<T = unknown> extends EntityManagerBase<T>, EntityManagerPermissions<T> {
|
|
74
|
-
get: (id: string | number, context?: unknown) => Promise<T>
|
|
74
|
+
get: (id: string | number, context?: unknown, options?: { listCache?: boolean }) => Promise<T>
|
|
75
75
|
list: (params?: unknown, context?: unknown) => Promise<{ items: T[]; total?: number; fromCache?: boolean; [key: string]: unknown }>
|
|
76
76
|
// query/invalidateCache/getFieldConfig are required since #1253: they are
|
|
77
77
|
// unconditionally implemented on the class (prototype-applied) and called
|
|
@@ -160,6 +160,8 @@ export interface EntityManagerLike {
|
|
|
160
160
|
getEntityLabel(data: unknown): string | null
|
|
161
161
|
getEntityBadges?(data: unknown): EntityBadge[]
|
|
162
162
|
canRead(entity?: unknown): boolean
|
|
163
|
+
/** Whether the user may list — what a list page and its menu entry need (#2497). */
|
|
164
|
+
canList?(): boolean
|
|
163
165
|
canCreate(): boolean
|
|
164
166
|
canUpdate(entity?: unknown): boolean
|
|
165
167
|
canDelete(entity?: unknown): boolean
|
|
@@ -168,7 +170,7 @@ export interface EntityManagerLike {
|
|
|
168
170
|
total?: number
|
|
169
171
|
fromCache?: boolean
|
|
170
172
|
}>
|
|
171
|
-
get(id: string | number, context?: unknown): Promise<unknown>
|
|
173
|
+
get(id: string | number, context?: unknown, options?: { listCache?: boolean }): Promise<unknown>
|
|
172
174
|
create(data: Record<string, unknown>, context?: unknown): Promise<unknown>
|
|
173
175
|
update(id: string | number, data: Record<string, unknown>, context?: unknown): Promise<unknown>
|
|
174
176
|
delete(id: string | number, context?: unknown): Promise<unknown>
|
|
@@ -29,6 +29,7 @@ import type {
|
|
|
29
29
|
ResolvedStorage,
|
|
30
30
|
StorageResolution,
|
|
31
31
|
Orchestrator,
|
|
32
|
+
GetOptions,
|
|
32
33
|
} from './EntityManager.types'
|
|
33
34
|
import type { NullSortMode } from '../query/clientFilter'
|
|
34
35
|
|
|
@@ -48,6 +49,7 @@ export type {
|
|
|
48
49
|
SeverityMapValue,
|
|
49
50
|
SeverityMap,
|
|
50
51
|
EntityManagerOptions,
|
|
52
|
+
GetOptions,
|
|
51
53
|
} from './EntityManager.types'
|
|
52
54
|
|
|
53
55
|
// Import concrete types needed in method signatures
|
|
@@ -1001,7 +1003,7 @@ export interface EntityManager<T extends EntityRecord = EntityRecord> {
|
|
|
1001
1003
|
|
|
1002
1004
|
// --- CRUD methods ---
|
|
1003
1005
|
list(params?: ListParams & { _internal?: boolean }, context?: RoutingContext): Promise<ListResult<T>>
|
|
1004
|
-
get(id: string | number, context?: RoutingContext): Promise<T>
|
|
1006
|
+
get(id: string | number, context?: RoutingContext, options?: GetOptions): Promise<T>
|
|
1005
1007
|
getMany(ids: Array<string | number>, context?: RoutingContext): Promise<T[]>
|
|
1006
1008
|
create(data: Partial<T>, context?: RoutingContext): Promise<T>
|
|
1007
1009
|
update(id: string | number, data: Partial<T>, context?: RoutingContext): Promise<T>
|
|
@@ -32,6 +32,19 @@ export interface CacheState<T> {
|
|
|
32
32
|
overflowed?: boolean
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
/**
|
|
36
|
+
* Options for `EntityManager.get()` (#2484).
|
|
37
|
+
*/
|
|
38
|
+
export interface GetOptions {
|
|
39
|
+
/**
|
|
40
|
+
* `false` reads the item from storage instead of a cached list row. A list
|
|
41
|
+
* row may be a summary: whoever loads a record in order to write it back
|
|
42
|
+
* needs the item itself. Default true — in symmetric mode, a valid list
|
|
43
|
+
* cache answers `get()`. Asymmetric entities never read the list cache.
|
|
44
|
+
*/
|
|
45
|
+
listCache?: boolean
|
|
46
|
+
}
|
|
47
|
+
|
|
35
48
|
/**
|
|
36
49
|
* Detail cache entry for asymmetric entities (full detail per item)
|
|
37
50
|
*/
|
|
@@ -255,12 +255,17 @@ export function applyRoutingMethods(KernelClass: { prototype: Kernel }): void {
|
|
|
255
255
|
const orchestrator = this.orchestrator
|
|
256
256
|
if (!entity || !orchestrator || !orchestrator.isRegistered(entity)) return
|
|
257
257
|
|
|
258
|
+
// The action the route declares (#2497): a list route is governed by
|
|
259
|
+
// `list`. A route that declares none — any custom page — checks `read`.
|
|
260
|
+
const action = to.meta?.entityAction === 'list' ? 'list' : 'read'
|
|
261
|
+
|
|
258
262
|
const checkEntity = (): { path: string } | undefined => {
|
|
259
263
|
try {
|
|
260
264
|
const manager = orchestrator.get(entity)
|
|
261
|
-
|
|
265
|
+
const allowed = manager && (action === 'list' ? manager.canList() : manager.canRead())
|
|
266
|
+
if (manager && !allowed) {
|
|
262
267
|
console.warn(
|
|
263
|
-
`[qdadm] Access denied to ${to.path} (entity: ${entity})`
|
|
268
|
+
`[qdadm] Access denied to ${to.path} (entity: ${entity}, action: ${action})`
|
|
264
269
|
)
|
|
265
270
|
orchestrator.toast.error(
|
|
266
271
|
'Access Denied',
|
package/src/kernel/Kernel.vue.ts
CHANGED
|
@@ -323,6 +323,9 @@ export function applyVueMethods(KernelClass: { prototype: Kernel }): void {
|
|
|
323
323
|
}
|
|
324
324
|
|
|
325
325
|
app.provide('qdadmSignals', this.signals)
|
|
326
|
+
// Whether the app runs in debug mode, for composables that only speak up
|
|
327
|
+
// then (#2484). Until now it reached nothing but window.__qdadm.
|
|
328
|
+
app.provide('qdadmDebug', !!this.options.debug)
|
|
326
329
|
|
|
327
330
|
if (this.i18nInstance) {
|
|
328
331
|
app.provide(I18N_INJECTION_KEY, this.i18nInstance)
|
|
@@ -245,7 +245,9 @@ export function applyRoutingMethods(KernelContextClass: { prototype: KernelConte
|
|
|
245
245
|
path: '',
|
|
246
246
|
name: routePrefix,
|
|
247
247
|
component: pages.list,
|
|
248
|
-
|
|
248
|
+
// The action this route performs, for the guard and the menu (#2497):
|
|
249
|
+
// a list page is governed by `list`, not by `read`.
|
|
250
|
+
meta: { layout: 'list', entityAction: 'list' },
|
|
249
251
|
})
|
|
250
252
|
}
|
|
251
253
|
|
|
@@ -254,7 +256,7 @@ export function applyRoutingMethods(KernelContextClass: { prototype: KernelConte
|
|
|
254
256
|
path: `:${idParam}`,
|
|
255
257
|
name: `${routePrefix}-show`,
|
|
256
258
|
component: pages.show,
|
|
257
|
-
meta: { layout: 'show' },
|
|
259
|
+
meta: { layout: 'show', entityAction: 'read' },
|
|
258
260
|
})
|
|
259
261
|
}
|
|
260
262
|
|
|
@@ -129,6 +129,12 @@ export class RolesManager extends EntityManager<RoleRecord> {
|
|
|
129
129
|
return this._isAdmin()
|
|
130
130
|
}
|
|
131
131
|
|
|
132
|
+
// The list page and its menu entry check `list` (#2497): without this, they
|
|
133
|
+
// would follow `entity:<x>:list` and show a system entity to non-admins.
|
|
134
|
+
canList(): boolean {
|
|
135
|
+
return this._isAdmin()
|
|
136
|
+
}
|
|
137
|
+
|
|
132
138
|
canCreate(): boolean {
|
|
133
139
|
return this._isAdmin()
|
|
134
140
|
}
|
|
@@ -146,6 +146,12 @@ export class UsersManager extends EntityManager {
|
|
|
146
146
|
return this._isAdmin()
|
|
147
147
|
}
|
|
148
148
|
|
|
149
|
+
// The list page and its menu entry check `list` (#2497): without this, they
|
|
150
|
+
// would follow `entity:<x>:list` and show a system entity to non-admins.
|
|
151
|
+
canList(): boolean {
|
|
152
|
+
return this._isAdmin()
|
|
153
|
+
}
|
|
154
|
+
|
|
149
155
|
canCreate(): boolean {
|
|
150
156
|
// little dumb but should work for simple storages
|
|
151
157
|
return this._isAdmin()
|