@quazardous/qdadm 2.5.0 → 2.6.1
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.
|
|
3
|
+
"version": "2.6.1",
|
|
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"
|
|
@@ -261,26 +261,47 @@ export function useListPage<T = unknown>(config: UseListPageOptions<T>): UseList
|
|
|
261
261
|
// when the i18n bundle has no translation for the field.
|
|
262
262
|
const columnInlineHeaders = new Map<string, string | undefined>()
|
|
263
263
|
|
|
264
|
+
// One-shot per entity+field (#1255): the catalog winning over an explicit
|
|
265
|
+
// header is by design, but it must never happen silently — the day a key
|
|
266
|
+
// appears, the page author learns which inline header it shadows.
|
|
267
|
+
const warnedHeaderShadows = new Set<string>()
|
|
268
|
+
function warnHeaderShadow(field: string, i18nValue: string, explicit: string | undefined): void {
|
|
269
|
+
if (explicit === undefined || explicit === i18nValue) return
|
|
270
|
+
const key = `${entity}.${field}`
|
|
271
|
+
if (warnedHeaderShadows.has(key)) return
|
|
272
|
+
warnedHeaderShadows.add(key)
|
|
273
|
+
console.warn(
|
|
274
|
+
`[qdadm] column '${field}' (${entity}): i18n key entities.${entity}.fields.${field} ` +
|
|
275
|
+
`("${i18nValue}") shadows explicit header ("${explicit}") — the catalog wins by design; ` +
|
|
276
|
+
`drop the inline header or remove the key.`
|
|
277
|
+
)
|
|
278
|
+
}
|
|
279
|
+
|
|
264
280
|
/**
|
|
265
|
-
* Resolve a column header: i18n hit on entities.{entity}.fields.{field} wins
|
|
281
|
+
* Resolve a column header: i18n hit on entities.{entity}.fields.{field} wins
|
|
282
|
+
* (the inline `header` is the no-key fallback, not an absolute override),
|
|
266
283
|
* otherwise the inline `header` option, otherwise a capitalized field name.
|
|
267
284
|
*/
|
|
268
285
|
function resolveColumnHeader(field: string, inline?: string): string {
|
|
269
286
|
if (entity && kernelI18n) {
|
|
270
287
|
const trace = kernelI18n.resolve(`entities.${entity}.fields.${field}`)
|
|
271
|
-
if (trace.hit)
|
|
288
|
+
if (trace.hit) {
|
|
289
|
+
warnHeaderShadow(field, trace.result, inline)
|
|
290
|
+
return trace.result
|
|
291
|
+
}
|
|
272
292
|
}
|
|
273
293
|
return inline || field.charAt(0).toUpperCase() + field.slice(1)
|
|
274
294
|
}
|
|
275
295
|
|
|
276
296
|
function addColumn(field: string, columnConfig: Partial<ColumnConfig> = {}): void {
|
|
277
297
|
columnInlineHeaders.set(field, columnConfig.header)
|
|
298
|
+
// Resolved header LAST so the catalog wins at registration too (#1255) —
|
|
299
|
+
// previously the inline header survived until the first locale change
|
|
300
|
+
// flipped it to the i18n value, silently and inconsistently.
|
|
278
301
|
columnsMap.value.set(field, {
|
|
279
302
|
field,
|
|
280
|
-
header: resolveColumnHeader(field, columnConfig.header),
|
|
281
303
|
...columnConfig,
|
|
282
|
-
|
|
283
|
-
...(columnConfig.header ? {} : { header: resolveColumnHeader(field) }),
|
|
304
|
+
header: resolveColumnHeader(field, columnConfig.header),
|
|
284
305
|
})
|
|
285
306
|
}
|
|
286
307
|
|
|
@@ -308,8 +329,17 @@ export function useListPage<T = unknown>(config: UseListPageOptions<T>): UseList
|
|
|
308
329
|
*
|
|
309
330
|
* Header resolution: i18n key (`entities.{entity}.fields.{field}`) >
|
|
310
331
|
* `overrides.header` > inline header given to `addColumn` >
|
|
311
|
-
* `manager.fields[name].label` > humanized field name.
|
|
312
|
-
*
|
|
332
|
+
* `manager.fields[name].label` > humanized field name.
|
|
333
|
+
*
|
|
334
|
+
* NOTE the precedence truth (#1255): an inline `header` is the fallback
|
|
335
|
+
* used when no i18n key exists — NOT an absolute override. The catalog
|
|
336
|
+
* wins by design (or a hardcoded header would pin the column to one
|
|
337
|
+
* language); to pin a header, don't create the key. When a key shadows
|
|
338
|
+
* an explicit header with a different value, a one-shot dev warning
|
|
339
|
+
* announces it instead of letting the header change silently.
|
|
340
|
+
*
|
|
341
|
+
* Pure read — it never registers into columnsMap, so calling it during
|
|
342
|
+
* render is safe.
|
|
313
343
|
*/
|
|
314
344
|
function column(name: string, overrides: Partial<ColumnConfig> = {}): ColumnConfig {
|
|
315
345
|
// Depend on the locale so headers re-render on locale change.
|
|
@@ -318,9 +348,15 @@ export function useListPage<T = unknown>(config: UseListPageOptions<T>): UseList
|
|
|
318
348
|
let header: string | undefined
|
|
319
349
|
if (entity && kernelI18n) {
|
|
320
350
|
const trace = kernelI18n.resolve(`entities.${entity}.fields.${name}`)
|
|
321
|
-
if (trace.hit)
|
|
351
|
+
if (trace.hit) {
|
|
352
|
+
header = trace.result
|
|
353
|
+
// Both explicit sources are checked: per-call override AND the
|
|
354
|
+
// inline header registered via addColumn (values compared — a key
|
|
355
|
+
// that merely copies the header stays silent).
|
|
356
|
+
warnHeaderShadow(name, trace.result, overrides.header ?? columnInlineHeaders.get(name))
|
|
357
|
+
}
|
|
322
358
|
}
|
|
323
|
-
const fieldConfig = manager.getFieldConfig
|
|
359
|
+
const fieldConfig = manager.getFieldConfig(name) as { label?: string } | null | undefined
|
|
324
360
|
header ??=
|
|
325
361
|
overrides.header ??
|
|
326
362
|
columnInlineHeaders.get(name) ??
|
|
@@ -61,6 +61,11 @@ export interface ConfirmService {
|
|
|
61
61
|
*/
|
|
62
62
|
export interface ColumnConfig {
|
|
63
63
|
field: string
|
|
64
|
+
/**
|
|
65
|
+
* Column header. When an i18n key `entities.{entity}.fields.{field}`
|
|
66
|
+
* exists, the catalog WINS — an inline header is the no-key fallback,
|
|
67
|
+
* not an absolute override (#1255). To pin a header, don't create the key.
|
|
68
|
+
*/
|
|
64
69
|
header?: string
|
|
65
70
|
sortable?: boolean
|
|
66
71
|
style?: string
|
|
@@ -13,6 +13,15 @@
|
|
|
13
13
|
*
|
|
14
14
|
* All interfaces accept an optional generic parameter `T` (default: `unknown`)
|
|
15
15
|
* that flows through to method signatures for typed entity data.
|
|
16
|
+
*
|
|
17
|
+
* ## Optionality contract (#1253)
|
|
18
|
+
*
|
|
19
|
+
* These views describe the IMPLEMENTER'S MINIMUM, not a mirror of the
|
|
20
|
+
* EntityManager class. A member is REQUIRED iff qdadm's own composables
|
|
21
|
+
* call it unguarded (or capability coherence demands it — e.g. the four
|
|
22
|
+
* can* permission checks travel together). A member stays OPTIONAL only
|
|
23
|
+
* when it is a genuine capability a minimal manager-like need not carry;
|
|
24
|
+
* the reason is documented inline. `?` means "conditional", never "lazy".
|
|
16
25
|
*/
|
|
17
26
|
|
|
18
27
|
import type { EntityBadge } from './EntityManager.types'
|
|
@@ -32,6 +41,9 @@ export interface EntityManagerBase<T = unknown> {
|
|
|
32
41
|
labelField?: string | ((entity: T) => string)
|
|
33
42
|
readOnly?: boolean
|
|
34
43
|
getEntityLabel: (data: T) => string
|
|
44
|
+
// Optional BY DESIGN (#1253): part of the badge/severity presentation
|
|
45
|
+
// capability; call sites guard (e.g. useEntityItemShowPage checks
|
|
46
|
+
// `if (!manager.getEntityBadges)`).
|
|
35
47
|
getEntityBadges?: (data: T) => Array<{ label: string; severity?: string }>
|
|
36
48
|
}
|
|
37
49
|
|
|
@@ -41,7 +53,10 @@ export interface EntityManagerBase<T = unknown> {
|
|
|
41
53
|
* Permission checks — used by guards, list pages, forms.
|
|
42
54
|
*/
|
|
43
55
|
export interface EntityManagerPermissions<T = unknown> {
|
|
44
|
-
|
|
56
|
+
// The four checks are ONE capability — a manager-like answering canUpdate
|
|
57
|
+
// but not canRead is not a smaller contract, it's an incoherent one. The
|
|
58
|
+
// `?` on canRead was a #1191 leftover (#1253).
|
|
59
|
+
canRead: (entity?: T) => boolean
|
|
45
60
|
canCreate: () => boolean
|
|
46
61
|
canUpdate: (entity?: T) => boolean
|
|
47
62
|
canDelete: (entity?: T) => boolean
|
|
@@ -55,13 +70,18 @@ export interface EntityManagerPermissions<T = unknown> {
|
|
|
55
70
|
export interface EntityManagerRead<T = unknown> extends EntityManagerBase<T>, EntityManagerPermissions<T> {
|
|
56
71
|
get: (id: string | number, context?: unknown) => Promise<T>
|
|
57
72
|
list: (params?: unknown, context?: unknown) => Promise<{ items: T[]; total?: number; fromCache?: boolean; [key: string]: unknown }>
|
|
58
|
-
query
|
|
73
|
+
// query/invalidateCache/getFieldConfig are required since #1253: they are
|
|
74
|
+
// unconditionally implemented on the class (prototype-applied) and called
|
|
75
|
+
// unguarded by list pages — the `?` was a #1191 leftover that forced
|
|
76
|
+
// consumer-side casts on base methods.
|
|
77
|
+
query: (params?: unknown, options?: { routingContext?: unknown }) => Promise<{ items: T[]; total?: number; fromCache?: boolean; [key: string]: unknown }>
|
|
59
78
|
delete: (id: string | number, context?: unknown) => Promise<void>
|
|
60
79
|
request: (method: string, path: string, options?: { data?: unknown }) => Promise<unknown>
|
|
61
|
-
invalidateCache
|
|
62
|
-
localFilterThreshold?: number
|
|
80
|
+
invalidateCache: () => void
|
|
63
81
|
/** Field config lookup — read-side too since list column binding (#1255) */
|
|
64
|
-
getFieldConfig
|
|
82
|
+
getFieldConfig: (name: string) => unknown | null
|
|
83
|
+
/** Config-carrying property: always present on the class, VALUE nullable */
|
|
84
|
+
localFilterThreshold?: number | null
|
|
65
85
|
}
|
|
66
86
|
|
|
67
87
|
// ─── CRUD ────────────────────────────────────────────────────────────────────
|
|
@@ -73,11 +93,14 @@ export interface EntityManagerCrud<T = unknown> extends EntityManagerRead<T> {
|
|
|
73
93
|
fields?: Record<string, unknown> | unknown[]
|
|
74
94
|
getInitialData: () => Record<string, unknown>
|
|
75
95
|
getFormFields: () => Array<{ name: string; [key: string]: unknown }>
|
|
76
|
-
getFieldConfig
|
|
96
|
+
// getFieldConfig inherited (required) from EntityManagerRead since #1253
|
|
77
97
|
resolveReferenceOptions: (fieldName: string) => Promise<unknown[]>
|
|
78
98
|
create: (data: unknown, context?: unknown) => Promise<T>
|
|
79
99
|
update: (id: string | number, data: unknown, context?: unknown) => Promise<T>
|
|
80
100
|
patch: (id: string | number, data: unknown, context?: unknown) => Promise<T>
|
|
101
|
+
// Optional BY DESIGN (#1253): the severity/badge presentation capability —
|
|
102
|
+
// a minimal manager-like need not carry it; every qdadm call site guards
|
|
103
|
+
// (`?.` or truthiness). The canonical class always provides it.
|
|
81
104
|
hasSeverityMap?: (field: string) => boolean
|
|
82
105
|
getSeverity?: (field: string, value: string | number, defaultSeverity?: string) => string
|
|
83
106
|
getSeverityDescriptor?: (field: string, value: string | number, defaultSeverity?: string) => { severity: string; icon?: string; label?: string }
|
package/src/index.ts
CHANGED
|
@@ -106,6 +106,17 @@ export {
|
|
|
106
106
|
type CacheInfo,
|
|
107
107
|
} from './entity/EntityManager'
|
|
108
108
|
|
|
109
|
+
// Structural manager/orchestrator views (#1253) — the implementer's minimum
|
|
110
|
+
// that composables consume; import these to type manager-likes and test doubles.
|
|
111
|
+
export type {
|
|
112
|
+
EntityManagerBase,
|
|
113
|
+
EntityManagerPermissions,
|
|
114
|
+
EntityManagerRead,
|
|
115
|
+
EntityManagerCrud,
|
|
116
|
+
EntityManagerLike,
|
|
117
|
+
OrchestratorLike,
|
|
118
|
+
} from './entity/EntityManager.interface'
|
|
119
|
+
|
|
109
120
|
// Re-export core entity types
|
|
110
121
|
export type {
|
|
111
122
|
EntityRecord,
|