qdadm 0.13.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/CHANGELOG.md +270 -0
- package/LICENSE +21 -0
- package/README.md +166 -0
- package/package.json +48 -0
- package/src/assets/logo.svg +6 -0
- package/src/components/BoolCell.vue +28 -0
- package/src/components/dialogs/BulkStatusDialog.vue +43 -0
- package/src/components/dialogs/MultiStepDialog.vue +321 -0
- package/src/components/dialogs/SimpleDialog.vue +108 -0
- package/src/components/dialogs/UnsavedChangesDialog.vue +87 -0
- package/src/components/display/CardsGrid.vue +155 -0
- package/src/components/display/CopyableId.vue +92 -0
- package/src/components/display/EmptyState.vue +114 -0
- package/src/components/display/IntensityBar.vue +171 -0
- package/src/components/display/RichCardsGrid.vue +220 -0
- package/src/components/editors/JsonEditorFoldable.vue +467 -0
- package/src/components/editors/JsonStructuredField.vue +218 -0
- package/src/components/editors/JsonViewer.vue +91 -0
- package/src/components/editors/KeyValueEditor.vue +314 -0
- package/src/components/editors/LanguageEditor.vue +245 -0
- package/src/components/editors/ScopeEditor.vue +341 -0
- package/src/components/editors/VanillaJsonEditor.vue +185 -0
- package/src/components/forms/FormActions.vue +104 -0
- package/src/components/forms/FormField.vue +64 -0
- package/src/components/forms/FormTab.vue +217 -0
- package/src/components/forms/FormTabs.vue +108 -0
- package/src/components/index.js +44 -0
- package/src/components/layout/AppLayout.vue +430 -0
- package/src/components/layout/Breadcrumb.vue +106 -0
- package/src/components/layout/PageHeader.vue +75 -0
- package/src/components/layout/PageLayout.vue +93 -0
- package/src/components/lists/ActionButtons.vue +41 -0
- package/src/components/lists/ActionColumn.vue +37 -0
- package/src/components/lists/FilterBar.vue +53 -0
- package/src/components/lists/ListPage.vue +319 -0
- package/src/composables/index.js +19 -0
- package/src/composables/useApp.js +43 -0
- package/src/composables/useAuth.js +49 -0
- package/src/composables/useBareForm.js +143 -0
- package/src/composables/useBreadcrumb.js +221 -0
- package/src/composables/useDirtyState.js +103 -0
- package/src/composables/useEntityTitle.js +121 -0
- package/src/composables/useForm.js +254 -0
- package/src/composables/useGuardStore.js +37 -0
- package/src/composables/useJsonSyntax.js +101 -0
- package/src/composables/useListPageBuilder.js +1176 -0
- package/src/composables/useNavigation.js +89 -0
- package/src/composables/usePageBuilder.js +334 -0
- package/src/composables/useStatus.js +146 -0
- package/src/composables/useSubEditor.js +165 -0
- package/src/composables/useTabSync.js +110 -0
- package/src/composables/useUnsavedChangesGuard.js +122 -0
- package/src/entity/EntityManager.js +540 -0
- package/src/entity/index.js +11 -0
- package/src/entity/storage/ApiStorage.js +146 -0
- package/src/entity/storage/LocalStorage.js +220 -0
- package/src/entity/storage/MemoryStorage.js +201 -0
- package/src/entity/storage/index.js +10 -0
- package/src/index.js +29 -0
- package/src/kernel/Kernel.js +234 -0
- package/src/kernel/index.js +7 -0
- package/src/module/index.js +16 -0
- package/src/module/moduleRegistry.js +222 -0
- package/src/orchestrator/Orchestrator.js +141 -0
- package/src/orchestrator/index.js +8 -0
- package/src/orchestrator/useOrchestrator.js +61 -0
- package/src/plugin.js +142 -0
- package/src/styles/_alerts.css +48 -0
- package/src/styles/_code.css +33 -0
- package/src/styles/_dialogs.css +17 -0
- package/src/styles/_markdown.css +82 -0
- package/src/styles/_show-pages.css +84 -0
- package/src/styles/index.css +16 -0
- package/src/styles/main.css +845 -0
- package/src/styles/theme/components.css +286 -0
- package/src/styles/theme/index.css +10 -0
- package/src/styles/theme/tokens.css +125 -0
- package/src/styles/theme/utilities.css +172 -0
- package/src/utils/debugInjector.js +261 -0
- package/src/utils/formatters.js +165 -0
- package/src/utils/index.js +35 -0
- package/src/utils/transformers.js +105 -0
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useForm - CRUD form composable extending useBareForm
|
|
3
|
+
*
|
|
4
|
+
* Provides standardized form handling:
|
|
5
|
+
* - Loading and saving states (via EntityManager)
|
|
6
|
+
* - Dirty state tracking (via useBareForm)
|
|
7
|
+
* - Unsaved changes guard (via useBareForm)
|
|
8
|
+
* - Toast notifications
|
|
9
|
+
* - Navigation helpers
|
|
10
|
+
*
|
|
11
|
+
* Usage:
|
|
12
|
+
* ```js
|
|
13
|
+
* // Minimal - reads config from EntityManager
|
|
14
|
+
* const { form, loading, saving, dirty, isEdit, submit, cancel } = useForm({
|
|
15
|
+
* entity: 'users',
|
|
16
|
+
* getId: () => route.params.id
|
|
17
|
+
* })
|
|
18
|
+
*
|
|
19
|
+
* // With overrides
|
|
20
|
+
* const { form, ... } = useForm({
|
|
21
|
+
* entity: 'users',
|
|
22
|
+
* getId: () => route.params.id,
|
|
23
|
+
* initialData: { name: '', email: '' }, // Override manager.getInitialData()
|
|
24
|
+
* routePrefix: 'user', // Override manager.routePrefix
|
|
25
|
+
* entityName: 'User' // Override manager.label
|
|
26
|
+
* })
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
import { ref, watch, onMounted, inject } from 'vue'
|
|
30
|
+
import { useBareForm } from './useBareForm'
|
|
31
|
+
import { deepClone } from '../utils/transformers'
|
|
32
|
+
|
|
33
|
+
export function useForm(options = {}) {
|
|
34
|
+
const {
|
|
35
|
+
entity,
|
|
36
|
+
getId = null,
|
|
37
|
+
// Callbacks
|
|
38
|
+
transformLoad = (data) => data,
|
|
39
|
+
transformSave = (data) => data,
|
|
40
|
+
onLoadSuccess = null,
|
|
41
|
+
onSaveSuccess = null,
|
|
42
|
+
// Options
|
|
43
|
+
enableGuard = true,
|
|
44
|
+
redirectOnCreate = true,
|
|
45
|
+
getDirtyState = null,
|
|
46
|
+
usePatch = false // Use PATCH instead of PUT for updates
|
|
47
|
+
} = options
|
|
48
|
+
|
|
49
|
+
// Get EntityManager via orchestrator
|
|
50
|
+
const orchestrator = inject('qdadmOrchestrator')
|
|
51
|
+
if (!orchestrator) {
|
|
52
|
+
throw new Error('[qdadm] Orchestrator not provided. Make sure to use createQdadm() with entityFactory.')
|
|
53
|
+
}
|
|
54
|
+
const manager = orchestrator.get(entity)
|
|
55
|
+
|
|
56
|
+
// Read config from manager with option overrides
|
|
57
|
+
const routePrefix = options.routePrefix ?? manager.routePrefix
|
|
58
|
+
const entityName = options.entityName ?? manager.label
|
|
59
|
+
const initialData = options.initialData ?? manager.getInitialData()
|
|
60
|
+
|
|
61
|
+
// Form-specific state
|
|
62
|
+
const form = ref(deepClone(initialData))
|
|
63
|
+
const originalData = ref(null)
|
|
64
|
+
|
|
65
|
+
// Dirty state getter
|
|
66
|
+
const dirtyStateGetter = getDirtyState || (() => ({ form: form.value }))
|
|
67
|
+
|
|
68
|
+
// Use base form for common functionality
|
|
69
|
+
const {
|
|
70
|
+
// Dependencies
|
|
71
|
+
router,
|
|
72
|
+
toast,
|
|
73
|
+
// State from useBareForm
|
|
74
|
+
loading,
|
|
75
|
+
saving,
|
|
76
|
+
dirty,
|
|
77
|
+
dirtyFields,
|
|
78
|
+
isEdit,
|
|
79
|
+
entityId,
|
|
80
|
+
// Dirty tracking
|
|
81
|
+
isFieldDirty,
|
|
82
|
+
takeSnapshot,
|
|
83
|
+
checkDirty,
|
|
84
|
+
// Helpers
|
|
85
|
+
cancel,
|
|
86
|
+
// Guard dialog for unsaved changes
|
|
87
|
+
guardDialog
|
|
88
|
+
} = useBareForm({
|
|
89
|
+
getState: dirtyStateGetter,
|
|
90
|
+
routePrefix,
|
|
91
|
+
guard: enableGuard,
|
|
92
|
+
onGuardSave: () => submit(false),
|
|
93
|
+
getId
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
// Watch for changes
|
|
97
|
+
watch(form, checkDirty, { deep: true })
|
|
98
|
+
|
|
99
|
+
// Load entity
|
|
100
|
+
async function load() {
|
|
101
|
+
if (!isEdit.value) {
|
|
102
|
+
form.value = deepClone(initialData)
|
|
103
|
+
takeSnapshot()
|
|
104
|
+
return
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
loading.value = true
|
|
108
|
+
try {
|
|
109
|
+
const responseData = await manager.get(entityId.value)
|
|
110
|
+
const data = transformLoad(responseData)
|
|
111
|
+
form.value = data
|
|
112
|
+
originalData.value = deepClone(data)
|
|
113
|
+
takeSnapshot()
|
|
114
|
+
|
|
115
|
+
if (onLoadSuccess) {
|
|
116
|
+
await onLoadSuccess(data)
|
|
117
|
+
}
|
|
118
|
+
} catch (error) {
|
|
119
|
+
console.error(`Failed to load ${entityName}:`, error)
|
|
120
|
+
toast.add({
|
|
121
|
+
severity: 'error',
|
|
122
|
+
summary: 'Error',
|
|
123
|
+
detail: error.response?.data?.detail || `Failed to load ${entityName}`,
|
|
124
|
+
life: 5000
|
|
125
|
+
})
|
|
126
|
+
} finally {
|
|
127
|
+
loading.value = false
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Submit form
|
|
132
|
+
async function submit(andClose = true) {
|
|
133
|
+
saving.value = true
|
|
134
|
+
try {
|
|
135
|
+
const payload = transformSave(deepClone(form.value))
|
|
136
|
+
|
|
137
|
+
let responseData
|
|
138
|
+
if (isEdit.value) {
|
|
139
|
+
if (usePatch) {
|
|
140
|
+
responseData = await manager.patch(entityId.value, payload)
|
|
141
|
+
} else {
|
|
142
|
+
responseData = await manager.update(entityId.value, payload)
|
|
143
|
+
}
|
|
144
|
+
} else {
|
|
145
|
+
responseData = await manager.create(payload)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
toast.add({
|
|
149
|
+
severity: 'success',
|
|
150
|
+
summary: 'Success',
|
|
151
|
+
detail: `${entityName} ${isEdit.value ? 'updated' : 'created'} successfully`,
|
|
152
|
+
life: 3000
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
// Update original data and snapshot
|
|
156
|
+
const savedData = transformLoad(responseData)
|
|
157
|
+
form.value = savedData
|
|
158
|
+
originalData.value = deepClone(savedData)
|
|
159
|
+
takeSnapshot()
|
|
160
|
+
|
|
161
|
+
if (onSaveSuccess) {
|
|
162
|
+
await onSaveSuccess(responseData, andClose)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (andClose) {
|
|
166
|
+
router.push({ name: routePrefix })
|
|
167
|
+
} else if (!isEdit.value && redirectOnCreate) {
|
|
168
|
+
// Redirect to edit mode after create
|
|
169
|
+
const newId = responseData.id || responseData.key
|
|
170
|
+
router.replace({ name: `${routePrefix}-edit`, params: { id: newId } })
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return responseData
|
|
174
|
+
} catch (error) {
|
|
175
|
+
console.error(`Failed to save ${entityName}:`, error)
|
|
176
|
+
toast.add({
|
|
177
|
+
severity: 'error',
|
|
178
|
+
summary: 'Error',
|
|
179
|
+
detail: error.response?.data?.detail || `Failed to save ${entityName}`,
|
|
180
|
+
life: 5000
|
|
181
|
+
})
|
|
182
|
+
throw error
|
|
183
|
+
} finally {
|
|
184
|
+
saving.value = false
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Delete entity
|
|
189
|
+
async function remove() {
|
|
190
|
+
if (!isEdit.value) return
|
|
191
|
+
|
|
192
|
+
try {
|
|
193
|
+
await manager.delete(entityId.value)
|
|
194
|
+
toast.add({
|
|
195
|
+
severity: 'success',
|
|
196
|
+
summary: 'Success',
|
|
197
|
+
detail: `${entityName} deleted successfully`,
|
|
198
|
+
life: 3000
|
|
199
|
+
})
|
|
200
|
+
router.push({ name: routePrefix })
|
|
201
|
+
} catch (error) {
|
|
202
|
+
console.error(`Failed to delete ${entityName}:`, error)
|
|
203
|
+
toast.add({
|
|
204
|
+
severity: 'error',
|
|
205
|
+
summary: 'Error',
|
|
206
|
+
detail: error.response?.data?.detail || `Failed to delete ${entityName}`,
|
|
207
|
+
life: 5000
|
|
208
|
+
})
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Reset form to original data
|
|
213
|
+
function reset() {
|
|
214
|
+
if (originalData.value) {
|
|
215
|
+
form.value = deepClone(originalData.value)
|
|
216
|
+
} else {
|
|
217
|
+
form.value = deepClone(initialData)
|
|
218
|
+
}
|
|
219
|
+
takeSnapshot()
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Load on mount if editing
|
|
223
|
+
onMounted(() => {
|
|
224
|
+
load()
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
return {
|
|
228
|
+
// Manager access
|
|
229
|
+
manager,
|
|
230
|
+
|
|
231
|
+
// State
|
|
232
|
+
form,
|
|
233
|
+
loading,
|
|
234
|
+
saving,
|
|
235
|
+
dirty,
|
|
236
|
+
dirtyFields,
|
|
237
|
+
isEdit,
|
|
238
|
+
entityId,
|
|
239
|
+
originalData,
|
|
240
|
+
|
|
241
|
+
// Actions
|
|
242
|
+
load,
|
|
243
|
+
submit,
|
|
244
|
+
cancel,
|
|
245
|
+
remove,
|
|
246
|
+
reset,
|
|
247
|
+
takeSnapshot,
|
|
248
|
+
checkDirty,
|
|
249
|
+
isFieldDirty,
|
|
250
|
+
|
|
251
|
+
// Guard dialog (for UnsavedChangesDialog - pass to PageLayout)
|
|
252
|
+
guardDialog
|
|
253
|
+
}
|
|
254
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared reactive store for the unsaved changes guard dialog
|
|
3
|
+
*
|
|
4
|
+
* This store allows child components (forms) to register their guard dialog,
|
|
5
|
+
* and parent components (AppLayout) to render it.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { ref, shallowRef } from 'vue'
|
|
9
|
+
|
|
10
|
+
// Shared reactive state
|
|
11
|
+
const currentGuardDialog = shallowRef(null)
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Register a guard dialog (called by useBareForm)
|
|
15
|
+
* @param {Object} guardDialog - The guardDialog object from useUnsavedChangesGuard
|
|
16
|
+
*/
|
|
17
|
+
export function registerGuardDialog(guardDialog) {
|
|
18
|
+
currentGuardDialog.value = guardDialog
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Unregister the current guard dialog (called on form unmount)
|
|
23
|
+
* @param {Object} guardDialog - The guardDialog to unregister (only unregisters if it matches)
|
|
24
|
+
*/
|
|
25
|
+
export function unregisterGuardDialog(guardDialog) {
|
|
26
|
+
if (currentGuardDialog.value === guardDialog) {
|
|
27
|
+
currentGuardDialog.value = null
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Get the current guard dialog ref (used by AppLayout)
|
|
33
|
+
* @returns {ShallowRef} The reactive guard dialog reference
|
|
34
|
+
*/
|
|
35
|
+
export function useGuardDialog() {
|
|
36
|
+
return currentGuardDialog
|
|
37
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useJsonSyntax - Shared JSON syntax highlighting utilities
|
|
3
|
+
*
|
|
4
|
+
* Provides consistent JSON formatting and syntax highlighting
|
|
5
|
+
* across JsonEditor, JsonViewer, and JsonEditorFoldable components.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Apply syntax highlighting to JSON string
|
|
10
|
+
* @param {string} jsonText - Raw JSON text
|
|
11
|
+
* @returns {string} HTML with syntax highlighting spans
|
|
12
|
+
*/
|
|
13
|
+
export function highlightJson(jsonText) {
|
|
14
|
+
if (!jsonText) {
|
|
15
|
+
return ''
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return jsonText
|
|
19
|
+
.replace(/&/g, '&')
|
|
20
|
+
.replace(/</g, '<')
|
|
21
|
+
.replace(/>/g, '>')
|
|
22
|
+
// Strings (values only - after colon or in arrays)
|
|
23
|
+
.replace(/("(?:[^"\\]|\\.)*")(?=\s*[,\]}]|\s*$)/gm, '<span class="json-string">$1</span>')
|
|
24
|
+
// Keys (before colon)
|
|
25
|
+
.replace(/("(?:[^"\\]|\\.)*")(\s*:)/g, '<span class="json-key">$1</span>$2')
|
|
26
|
+
// Numbers
|
|
27
|
+
.replace(/\b(-?\d+\.?\d*)\b/g, '<span class="json-number">$1</span>')
|
|
28
|
+
// Booleans
|
|
29
|
+
.replace(/\b(true|false)\b/g, '<span class="json-boolean">$1</span>')
|
|
30
|
+
// Null
|
|
31
|
+
.replace(/\bnull\b/g, '<span class="json-null">null</span>')
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Get the type of a JSON value for display
|
|
36
|
+
* @param {any} value - The value to check
|
|
37
|
+
* @returns {string} Type name: 'string', 'number', 'boolean', 'null', 'array', 'object'
|
|
38
|
+
*/
|
|
39
|
+
export function getJsonValueType(value) {
|
|
40
|
+
if (value === null) return 'null'
|
|
41
|
+
if (Array.isArray(value)) return 'array'
|
|
42
|
+
return typeof value
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Get a short preview of a JSON value
|
|
47
|
+
* @param {any} value - The value to preview
|
|
48
|
+
* @param {number} maxLength - Maximum length for string previews
|
|
49
|
+
* @returns {string} Short preview text
|
|
50
|
+
*/
|
|
51
|
+
export function getJsonPreview(value, maxLength = 50) {
|
|
52
|
+
const type = getJsonValueType(value)
|
|
53
|
+
|
|
54
|
+
switch (type) {
|
|
55
|
+
case 'string': {
|
|
56
|
+
const preview = value.substring(0, maxLength)
|
|
57
|
+
return preview + (value.length > maxLength ? '...' : '')
|
|
58
|
+
}
|
|
59
|
+
case 'array':
|
|
60
|
+
return `[${value.length} item${value.length !== 1 ? 's' : ''}]`
|
|
61
|
+
case 'object':
|
|
62
|
+
return `{${Object.keys(value).length} key${Object.keys(value).length !== 1 ? 's' : ''}}`
|
|
63
|
+
case 'null':
|
|
64
|
+
return 'null'
|
|
65
|
+
case 'boolean':
|
|
66
|
+
return String(value)
|
|
67
|
+
case 'number':
|
|
68
|
+
return String(value)
|
|
69
|
+
default:
|
|
70
|
+
return String(value)
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Safely parse JSON with error handling
|
|
76
|
+
* @param {string} text - JSON text to parse
|
|
77
|
+
* @returns {{ value: any, error: string | null }} Parse result
|
|
78
|
+
*/
|
|
79
|
+
export function safeJsonParse(text) {
|
|
80
|
+
if (!text || !text.trim()) {
|
|
81
|
+
return { value: null, error: null }
|
|
82
|
+
}
|
|
83
|
+
try {
|
|
84
|
+
return { value: JSON.parse(text), error: null }
|
|
85
|
+
} catch (e) {
|
|
86
|
+
return { value: null, error: e.message }
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* CSS class names for JSON syntax highlighting
|
|
92
|
+
* Import these in your component's style section
|
|
93
|
+
*/
|
|
94
|
+
export const JSON_SYNTAX_CLASSES = `
|
|
95
|
+
.json-key { color: #881391; }
|
|
96
|
+
.json-string { color: #1a1aa6; }
|
|
97
|
+
.json-number { color: #1c00cf; }
|
|
98
|
+
.json-boolean { color: #0d22aa; font-weight: 500; }
|
|
99
|
+
.json-null { color: #808080; font-style: italic; }
|
|
100
|
+
.json-placeholder { color: var(--p-surface-400); }
|
|
101
|
+
`
|