@quazardous/qdadm 2.20.0 → 2.22.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/dist/node/vite/qdadmDebugPlugin.d.ts +9 -4
- package/dist/node/vite/qdadmDebugPlugin.js +36 -10
- package/dist/node/vite/qdadmPlugin.js +2 -0
- package/dist/styles/qdadm.css +1 -0
- package/package.json +10 -4
- package/src/components/edit/FormField.vue +51 -4
- package/src/components/edit/FormInput.vue +44 -2
- package/src/components/edit/formFieldIds.ts +11 -0
- package/src/components/editors/RoleGrantsEditor.vue +355 -0
- package/src/components/index.ts +1 -0
- package/src/components/show/ShowPage.vue +4 -2
- package/src/composables/confirmDelete.ts +35 -0
- package/src/composables/index.ts +1 -0
- package/src/composables/useEntityItemFormPage.ts +2 -3
- package/src/composables/useEntityItemFormPage.types.ts +1 -0
- package/src/composables/useEntityItemShowPage.ts +3 -3
- package/src/composables/useListPage.ts +3 -6
- package/src/composables/useListPage.types.ts +1 -0
- package/src/composables/useSecurity.ts +41 -0
- package/src/entity/auth/SecurityChecker.ts +88 -2
- package/src/entity/auth/index.ts +2 -0
- package/src/i18n/defaults/DefaultCoreProvider.ts +4 -2
- package/src/i18n/defaults/__tests__/generated-sync.test.ts +9 -0
- package/src/i18n/defaults/core.en.generated.ts +20 -0
- package/src/i18n/defaults/core.en.yml +20 -0
- package/src/i18n/defaults/core.fr.generated.ts +20 -0
- package/src/i18n/defaults/core.fr.yml +20 -0
- package/src/index.ts +1 -0
- package/src/kernel/Kernel.auth.ts +4 -0
- package/src/kernel/Kernel.registries.ts +24 -5
- package/src/kernel/Kernel.routing.ts +20 -4
- package/src/kernel/Kernel.ts +3 -0
- package/src/kernel/Kernel.types.ts +7 -1
- package/src/kernel/Kernel.vue.ts +3 -0
- package/src/kernel/SignalBus.ts +6 -0
- package/src/kernel/chunkReload.ts +110 -0
- package/src/kernel/vitePluginCheck.ts +61 -0
- package/src/modules/debug/AuthCollector.ts +15 -1
- package/src/modules/debug/DebugModule.ts +9 -0
- package/src/modules/debug/components/panels/AuthPanel.vue +1 -0
- package/src/notifications/NotificationListener.vue +2 -1
- package/src/security/RolesProvider.ts +8 -0
- package/src/security/index.ts +17 -0
- package/src/security/pages/RoleForm.vue +42 -191
- package/src/security/roleGrants.ts +210 -0
- package/src/styles/_forms.scss +10 -2
- package/src/styles/index.d.ts +3 -4
- package/src/toast/ToastListener.vue +2 -1
- package/src/types/index.ts +10 -0
- package/src/vite/qdadmDebugPlugin.ts +46 -10
- package/src/vite/qdadmPlugin.ts +2 -0
package/src/styles/_forms.scss
CHANGED
|
@@ -82,18 +82,26 @@
|
|
|
82
82
|
font-size: 0.85rem;
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
-
// Force PrimeVue inputs to be full width
|
|
85
|
+
// Force PrimeVue inputs to be full width. Text-like native inputs only: a native checkbox, radio, range or
|
|
86
|
+
// colour keeps its own size (#2319). PrimeVue's Checkbox, RadioButton and ToggleSwitch size their hidden input
|
|
87
|
+
// themselves.
|
|
86
88
|
.p-inputtext,
|
|
87
89
|
.p-textarea,
|
|
88
90
|
.p-select,
|
|
89
91
|
.p-multiselect,
|
|
90
92
|
.p-inputwrapper,
|
|
91
|
-
input,
|
|
93
|
+
input:not([type='checkbox']):not([type='radio']):not([type='range']):not([type='color']),
|
|
92
94
|
textarea {
|
|
93
95
|
width: 100% !important;
|
|
94
96
|
box-sizing: border-box;
|
|
95
97
|
}
|
|
96
98
|
|
|
99
|
+
// The field is a flex column: a native checkbox or radio placed directly in it would be stretched across the row.
|
|
100
|
+
> input[type='checkbox'],
|
|
101
|
+
> input[type='radio'] {
|
|
102
|
+
align-self: flex-start;
|
|
103
|
+
}
|
|
104
|
+
|
|
97
105
|
.p-textarea,
|
|
98
106
|
textarea {
|
|
99
107
|
min-height: 100px;
|
package/src/styles/index.d.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
// Type companion for the `@quazardous/qdadm/styles`
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
// `import '@quazardous/qdadm/styles'` typecheck in strict consumers.
|
|
1
|
+
// Type companion for the `@quazardous/qdadm/styles` and `@quazardous/qdadm/styles/scss` exports (#1386, #2260).
|
|
2
|
+
// They resolve to a stylesheet (compiled .css, or the raw .scss), which TypeScript cannot see through the
|
|
3
|
+
// exports map — this empty module makes the side-effect imports typecheck in strict consumers.
|
|
5
4
|
export {}
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
import { onMounted, onUnmounted, inject } from 'vue'
|
|
11
11
|
import { useToast } from 'primevue/usetoast'
|
|
12
12
|
import type { SignalBus } from '../kernel/SignalBus'
|
|
13
|
+
import { explainMissingToast } from '../kernel/vitePluginCheck'
|
|
13
14
|
|
|
14
15
|
interface ToastEventData {
|
|
15
16
|
summary?: string
|
|
@@ -17,7 +18,7 @@ interface ToastEventData {
|
|
|
17
18
|
life?: number
|
|
18
19
|
}
|
|
19
20
|
|
|
20
|
-
const toast = useToast
|
|
21
|
+
const toast = explainMissingToast(useToast)
|
|
21
22
|
const signals = inject<SignalBus | null>('qdadmSignals', null)
|
|
22
23
|
|
|
23
24
|
let unsubscribe: (() => void) | null = null
|
package/src/types/index.ts
CHANGED
|
@@ -86,6 +86,16 @@ export interface FieldConfig {
|
|
|
86
86
|
required?: boolean
|
|
87
87
|
/** number fields: false → no locale digit grouping in displays (#1388) */
|
|
88
88
|
useGrouping?: boolean
|
|
89
|
+
/**
|
|
90
|
+
* number fields: digits the form input takes after the decimal point (#2316) — `2` for always two, or
|
|
91
|
+
* `{ min, max }`. Without it the input takes whole numbers only.
|
|
92
|
+
*/
|
|
93
|
+
fractionDigits?: number | { min?: number; max?: number }
|
|
94
|
+
/** number fields: bounds of the form input (#2316) */
|
|
95
|
+
min?: number
|
|
96
|
+
max?: number
|
|
97
|
+
/** number fields: increment of the form input (#2316) */
|
|
98
|
+
step?: number
|
|
89
99
|
default?: unknown | (() => unknown)
|
|
90
100
|
options?: Array<{ label: string; value: unknown }>
|
|
91
101
|
reference?: {
|
|
@@ -8,10 +8,12 @@
|
|
|
8
8
|
* GET /__qdadm/snapshot.json — bridge.dump() (live state)
|
|
9
9
|
* POST /__qdadm/call — body {collector, action, args?}
|
|
10
10
|
*
|
|
11
|
-
* Session selection: every browser tab
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
11
|
+
* Session selection: every browser tab gets a session id (uuid in the
|
|
12
|
+
* injected client), kept in sessionStorage so a reload is the SAME session
|
|
13
|
+
* (#2231). A tab that says bye stays known through a short grace window: a
|
|
14
|
+
* request to it answers "reloading" instead of timing out. All endpoints
|
|
15
|
+
* accept a `?session=<id|latest>` query param. Default is `latest` (the most
|
|
16
|
+
* recently active connected session).
|
|
15
17
|
*
|
|
16
18
|
* Wire-up: the plugin injects a small ESM script via `transformIndexHtml`.
|
|
17
19
|
* That inline script lives in Vite's HMR pipeline (so `import.meta.hot`
|
|
@@ -78,6 +80,8 @@ interface SessionState {
|
|
|
78
80
|
describe: { data: unknown; at: number } | null
|
|
79
81
|
snapshot: { data: unknown; at: number } | null
|
|
80
82
|
meta: Record<string, unknown>
|
|
83
|
+
/** Set by bye; cleared by the next push from the same tab. */
|
|
84
|
+
disconnectedAt: number | null
|
|
81
85
|
}
|
|
82
86
|
|
|
83
87
|
/** Broker surface exposed to sibling plugins via `plugin.api` (#1398). */
|
|
@@ -95,6 +99,7 @@ export interface QdadmDebugPluginApi {
|
|
|
95
99
|
ageMs: number
|
|
96
100
|
hasDescribe: boolean
|
|
97
101
|
hasSnapshot: boolean
|
|
102
|
+
connected: boolean
|
|
98
103
|
meta: Record<string, unknown>
|
|
99
104
|
}>
|
|
100
105
|
prefix: string
|
|
@@ -107,12 +112,15 @@ export interface QdadmDebugPluginOptions {
|
|
|
107
112
|
timeoutMs?: number
|
|
108
113
|
/** Drop sessions inactive for this many ms (default 10 min) */
|
|
109
114
|
sessionTtlMs?: number
|
|
115
|
+
/** Keep a tab that said bye this long, so its reload keeps it (default 30 s) */
|
|
116
|
+
reloadGraceMs?: number
|
|
110
117
|
}
|
|
111
118
|
|
|
112
119
|
export function qdadmDebugPlugin(options: QdadmDebugPluginOptions = {}): Plugin {
|
|
113
120
|
const prefix = options.prefix ?? '/__qdadm'
|
|
114
121
|
const timeoutMs = options.timeoutMs ?? 3000
|
|
115
122
|
const sessionTtlMs = options.sessionTtlMs ?? 10 * 60 * 1000
|
|
123
|
+
const reloadGraceMs = options.reloadGraceMs ?? 30 * 1000
|
|
116
124
|
|
|
117
125
|
const sessions = new Map<string, SessionState>()
|
|
118
126
|
const pending = new Map<string, PendingRequest>()
|
|
@@ -120,18 +128,25 @@ export function qdadmDebugPlugin(options: QdadmDebugPluginOptions = {}): Plugin
|
|
|
120
128
|
let nextId = 1
|
|
121
129
|
|
|
122
130
|
function pruneStaleSessions(): void {
|
|
123
|
-
const
|
|
131
|
+
const now = Date.now()
|
|
124
132
|
for (const [id, s] of sessions) {
|
|
125
|
-
if (s.lastSeenAt <
|
|
133
|
+
if (s.lastSeenAt < now - sessionTtlMs) sessions.delete(id)
|
|
134
|
+
else if (s.disconnectedAt !== null && s.disconnectedAt < now - reloadGraceMs) sessions.delete(id)
|
|
126
135
|
}
|
|
127
136
|
}
|
|
128
137
|
|
|
129
138
|
function pickSession(sessionParam: string | null): SessionState | null {
|
|
130
139
|
pruneStaleSessions()
|
|
131
140
|
if (!sessionParam || sessionParam === 'latest') {
|
|
141
|
+
// A connected tab wins; a reloading one only when nothing else is up,
|
|
142
|
+
// so the caller hears "reloading" rather than "no session".
|
|
132
143
|
let best: SessionState | null = null
|
|
133
144
|
for (const s of sessions.values()) {
|
|
134
|
-
|
|
145
|
+
const better =
|
|
146
|
+
!best ||
|
|
147
|
+
(best.disconnectedAt !== null && s.disconnectedAt === null) ||
|
|
148
|
+
((best.disconnectedAt === null) === (s.disconnectedAt === null) && s.lastSeenAt > best.lastSeenAt)
|
|
149
|
+
if (better) best = s
|
|
135
150
|
}
|
|
136
151
|
return best
|
|
137
152
|
}
|
|
@@ -144,6 +159,15 @@ export function qdadmDebugPlugin(options: QdadmDebugPluginOptions = {}): Plugin
|
|
|
144
159
|
sessionId?: string
|
|
145
160
|
): Promise<unknown> {
|
|
146
161
|
if (!server) return Promise.reject(new Error('[qdadm-debug] vite server not ready'))
|
|
162
|
+
const target = sessionId ? sessions.get(sessionId) : undefined
|
|
163
|
+
if (target?.disconnectedAt) {
|
|
164
|
+
const away = Math.round((Date.now() - target.disconnectedAt) / 1000)
|
|
165
|
+
return Promise.reject(
|
|
166
|
+
new Error(
|
|
167
|
+
`[qdadm-debug] session ${target.id.slice(0, 8)} left ${away}s ago — the tab is most likely reloading; retry in a few seconds`
|
|
168
|
+
)
|
|
169
|
+
)
|
|
170
|
+
}
|
|
147
171
|
const id = String(nextId++)
|
|
148
172
|
return new Promise((resolve, reject) => {
|
|
149
173
|
const timeout = setTimeout(() => {
|
|
@@ -200,6 +224,7 @@ export function qdadmDebugPlugin(options: QdadmDebugPluginOptions = {}): Plugin
|
|
|
200
224
|
ageMs: number
|
|
201
225
|
hasDescribe: boolean
|
|
202
226
|
hasSnapshot: boolean
|
|
227
|
+
connected: boolean
|
|
203
228
|
meta: Record<string, unknown>
|
|
204
229
|
}> {
|
|
205
230
|
pruneStaleSessions()
|
|
@@ -213,6 +238,7 @@ export function qdadmDebugPlugin(options: QdadmDebugPluginOptions = {}): Plugin
|
|
|
213
238
|
ageMs: now - s.lastSeenAt,
|
|
214
239
|
hasDescribe: !!s.describe,
|
|
215
240
|
hasSnapshot: !!s.snapshot,
|
|
241
|
+
connected: s.disconnectedAt === null,
|
|
216
242
|
meta: s.meta,
|
|
217
243
|
}))
|
|
218
244
|
}
|
|
@@ -227,10 +253,12 @@ export function qdadmDebugPlugin(options: QdadmDebugPluginOptions = {}): Plugin
|
|
|
227
253
|
describe: null,
|
|
228
254
|
snapshot: null,
|
|
229
255
|
meta: meta ?? {},
|
|
256
|
+
disconnectedAt: null,
|
|
230
257
|
}
|
|
231
258
|
sessions.set(id, s)
|
|
232
259
|
} else {
|
|
233
260
|
s.lastSeenAt = Date.now()
|
|
261
|
+
s.disconnectedAt = null
|
|
234
262
|
if (meta) s.meta = { ...s.meta, ...meta }
|
|
235
263
|
}
|
|
236
264
|
return s
|
|
@@ -287,7 +315,7 @@ export function qdadmDebugPlugin(options: QdadmDebugPluginOptions = {}): Plugin
|
|
|
287
315
|
} else if (msg.type === 'snapshot' && msg.data !== undefined) {
|
|
288
316
|
session.snapshot = { data: msg.data, at }
|
|
289
317
|
} else if (msg.type === 'bye') {
|
|
290
|
-
|
|
318
|
+
session.disconnectedAt = at
|
|
291
319
|
}
|
|
292
320
|
})
|
|
293
321
|
|
|
@@ -376,7 +404,8 @@ function readBody(req: IncomingMessage): Promise<string> {
|
|
|
376
404
|
* Client-side bridge — injected verbatim into index.html as an ES module.
|
|
377
405
|
* Lives in Vite's HMR pipeline (so `import.meta.hot` works here).
|
|
378
406
|
*
|
|
379
|
-
* Each tab gets its own session id (random uuid
|
|
407
|
+
* Each tab gets its own session id (random uuid, kept across reloads in
|
|
408
|
+
* sessionStorage — #2231). Pushes describe + snapshot
|
|
380
409
|
* on every reactive tick of the bridge so the plugin's per-session cache
|
|
381
410
|
* stays fresh. Listens for ad-hoc requests targeted at this session.
|
|
382
411
|
*/
|
|
@@ -413,7 +442,14 @@ const hot = import.meta.hot
|
|
|
413
442
|
if (!hot) {
|
|
414
443
|
console.warn('[qdadm-debug] import.meta.hot unavailable in injected script')
|
|
415
444
|
} else {
|
|
416
|
-
|
|
445
|
+
// Same tab, same session across reloads (#2231): an agent's target survives F5.
|
|
446
|
+
const sessionId = (() => {
|
|
447
|
+
try {
|
|
448
|
+
let v = sessionStorage.getItem('qdadm-debug:session')
|
|
449
|
+
if (!v) { v = crypto.randomUUID(); sessionStorage.setItem('qdadm-debug:session', v) }
|
|
450
|
+
return v
|
|
451
|
+
} catch (e) { return crypto.randomUUID() }
|
|
452
|
+
})()
|
|
417
453
|
const meta = { userAgent: navigator.userAgent, location: location.pathname }
|
|
418
454
|
let bridge = null
|
|
419
455
|
let lastTick = -1
|
package/src/vite/qdadmPlugin.ts
CHANGED
|
@@ -118,6 +118,8 @@ export function qdadmVitePlugin(options: QdadmVitePluginOptions = {}): Plugin {
|
|
|
118
118
|
exclude: [...widgetExcludes, ...themesExcludes, '@quazardous/qdadm'],
|
|
119
119
|
},
|
|
120
120
|
...(fsAllow.length > 0 ? { server: { fs: { allow: fsAllow } } } : {}),
|
|
121
|
+
// The kernel checks it: an app without this plugin is told what to add (#2259).
|
|
122
|
+
define: { __QDADM_VITE_PLUGIN__: 'true' },
|
|
121
123
|
}
|
|
122
124
|
},
|
|
123
125
|
}
|