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.
Files changed (82) hide show
  1. package/CHANGELOG.md +270 -0
  2. package/LICENSE +21 -0
  3. package/README.md +166 -0
  4. package/package.json +48 -0
  5. package/src/assets/logo.svg +6 -0
  6. package/src/components/BoolCell.vue +28 -0
  7. package/src/components/dialogs/BulkStatusDialog.vue +43 -0
  8. package/src/components/dialogs/MultiStepDialog.vue +321 -0
  9. package/src/components/dialogs/SimpleDialog.vue +108 -0
  10. package/src/components/dialogs/UnsavedChangesDialog.vue +87 -0
  11. package/src/components/display/CardsGrid.vue +155 -0
  12. package/src/components/display/CopyableId.vue +92 -0
  13. package/src/components/display/EmptyState.vue +114 -0
  14. package/src/components/display/IntensityBar.vue +171 -0
  15. package/src/components/display/RichCardsGrid.vue +220 -0
  16. package/src/components/editors/JsonEditorFoldable.vue +467 -0
  17. package/src/components/editors/JsonStructuredField.vue +218 -0
  18. package/src/components/editors/JsonViewer.vue +91 -0
  19. package/src/components/editors/KeyValueEditor.vue +314 -0
  20. package/src/components/editors/LanguageEditor.vue +245 -0
  21. package/src/components/editors/ScopeEditor.vue +341 -0
  22. package/src/components/editors/VanillaJsonEditor.vue +185 -0
  23. package/src/components/forms/FormActions.vue +104 -0
  24. package/src/components/forms/FormField.vue +64 -0
  25. package/src/components/forms/FormTab.vue +217 -0
  26. package/src/components/forms/FormTabs.vue +108 -0
  27. package/src/components/index.js +44 -0
  28. package/src/components/layout/AppLayout.vue +430 -0
  29. package/src/components/layout/Breadcrumb.vue +106 -0
  30. package/src/components/layout/PageHeader.vue +75 -0
  31. package/src/components/layout/PageLayout.vue +93 -0
  32. package/src/components/lists/ActionButtons.vue +41 -0
  33. package/src/components/lists/ActionColumn.vue +37 -0
  34. package/src/components/lists/FilterBar.vue +53 -0
  35. package/src/components/lists/ListPage.vue +319 -0
  36. package/src/composables/index.js +19 -0
  37. package/src/composables/useApp.js +43 -0
  38. package/src/composables/useAuth.js +49 -0
  39. package/src/composables/useBareForm.js +143 -0
  40. package/src/composables/useBreadcrumb.js +221 -0
  41. package/src/composables/useDirtyState.js +103 -0
  42. package/src/composables/useEntityTitle.js +121 -0
  43. package/src/composables/useForm.js +254 -0
  44. package/src/composables/useGuardStore.js +37 -0
  45. package/src/composables/useJsonSyntax.js +101 -0
  46. package/src/composables/useListPageBuilder.js +1176 -0
  47. package/src/composables/useNavigation.js +89 -0
  48. package/src/composables/usePageBuilder.js +334 -0
  49. package/src/composables/useStatus.js +146 -0
  50. package/src/composables/useSubEditor.js +165 -0
  51. package/src/composables/useTabSync.js +110 -0
  52. package/src/composables/useUnsavedChangesGuard.js +122 -0
  53. package/src/entity/EntityManager.js +540 -0
  54. package/src/entity/index.js +11 -0
  55. package/src/entity/storage/ApiStorage.js +146 -0
  56. package/src/entity/storage/LocalStorage.js +220 -0
  57. package/src/entity/storage/MemoryStorage.js +201 -0
  58. package/src/entity/storage/index.js +10 -0
  59. package/src/index.js +29 -0
  60. package/src/kernel/Kernel.js +234 -0
  61. package/src/kernel/index.js +7 -0
  62. package/src/module/index.js +16 -0
  63. package/src/module/moduleRegistry.js +222 -0
  64. package/src/orchestrator/Orchestrator.js +141 -0
  65. package/src/orchestrator/index.js +8 -0
  66. package/src/orchestrator/useOrchestrator.js +61 -0
  67. package/src/plugin.js +142 -0
  68. package/src/styles/_alerts.css +48 -0
  69. package/src/styles/_code.css +33 -0
  70. package/src/styles/_dialogs.css +17 -0
  71. package/src/styles/_markdown.css +82 -0
  72. package/src/styles/_show-pages.css +84 -0
  73. package/src/styles/index.css +16 -0
  74. package/src/styles/main.css +845 -0
  75. package/src/styles/theme/components.css +286 -0
  76. package/src/styles/theme/index.css +10 -0
  77. package/src/styles/theme/tokens.css +125 -0
  78. package/src/styles/theme/utilities.css +172 -0
  79. package/src/utils/debugInjector.js +261 -0
  80. package/src/utils/formatters.js +165 -0
  81. package/src/utils/index.js +35 -0
  82. 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, '&lt;')
21
+ .replace(/>/g, '&gt;')
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
+ `