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,220 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LocalStorage - Browser localStorage storage adapter
|
|
3
|
+
*
|
|
4
|
+
* Implements the storage interface using browser localStorage.
|
|
5
|
+
* Useful for:
|
|
6
|
+
* - Offline-first applications
|
|
7
|
+
* - User preferences
|
|
8
|
+
* - Draft saving
|
|
9
|
+
* - Small datasets that persist across sessions
|
|
10
|
+
*
|
|
11
|
+
* Usage:
|
|
12
|
+
* ```js
|
|
13
|
+
* const storage = new LocalStorage({
|
|
14
|
+
* key: 'my_entities',
|
|
15
|
+
* idField: 'id',
|
|
16
|
+
* generateId: () => crypto.randomUUID()
|
|
17
|
+
* })
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
export class LocalStorage {
|
|
21
|
+
constructor(options = {}) {
|
|
22
|
+
const {
|
|
23
|
+
key,
|
|
24
|
+
idField = 'id',
|
|
25
|
+
generateId = () => Date.now().toString(36) + Math.random().toString(36).substr(2)
|
|
26
|
+
} = options
|
|
27
|
+
|
|
28
|
+
this.key = key
|
|
29
|
+
this.idField = idField
|
|
30
|
+
this.generateId = generateId
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Get all items from localStorage
|
|
35
|
+
* @returns {Array}
|
|
36
|
+
*/
|
|
37
|
+
_getAll() {
|
|
38
|
+
try {
|
|
39
|
+
const stored = localStorage.getItem(this.key)
|
|
40
|
+
return stored ? JSON.parse(stored) : []
|
|
41
|
+
} catch {
|
|
42
|
+
return []
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Save all items to localStorage
|
|
48
|
+
* @param {Array} items
|
|
49
|
+
*/
|
|
50
|
+
_saveAll(items) {
|
|
51
|
+
localStorage.setItem(this.key, JSON.stringify(items))
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* List entities with pagination/filtering
|
|
56
|
+
* @param {object} params - { page, page_size, sort_by, sort_order, ...filters }
|
|
57
|
+
* @returns {Promise<{ items: Array, total: number }>}
|
|
58
|
+
*/
|
|
59
|
+
async list(params = {}) {
|
|
60
|
+
const { page = 1, page_size = 20, sort_by, sort_order = 'asc', ...filters } = params
|
|
61
|
+
|
|
62
|
+
let items = this._getAll()
|
|
63
|
+
|
|
64
|
+
// Apply filters
|
|
65
|
+
for (const [key, value] of Object.entries(filters)) {
|
|
66
|
+
if (value === null || value === undefined || value === '') continue
|
|
67
|
+
items = items.filter(item => {
|
|
68
|
+
const itemValue = item[key]
|
|
69
|
+
if (typeof value === 'string' && typeof itemValue === 'string') {
|
|
70
|
+
return itemValue.toLowerCase().includes(value.toLowerCase())
|
|
71
|
+
}
|
|
72
|
+
return itemValue === value
|
|
73
|
+
})
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const total = items.length
|
|
77
|
+
|
|
78
|
+
// Apply sorting
|
|
79
|
+
if (sort_by) {
|
|
80
|
+
items.sort((a, b) => {
|
|
81
|
+
const aVal = a[sort_by]
|
|
82
|
+
const bVal = b[sort_by]
|
|
83
|
+
if (aVal < bVal) return sort_order === 'asc' ? -1 : 1
|
|
84
|
+
if (aVal > bVal) return sort_order === 'asc' ? 1 : -1
|
|
85
|
+
return 0
|
|
86
|
+
})
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Apply pagination
|
|
90
|
+
const start = (page - 1) * page_size
|
|
91
|
+
items = items.slice(start, start + page_size)
|
|
92
|
+
|
|
93
|
+
return { items, total }
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Get a single entity by ID
|
|
98
|
+
* @param {string|number} id
|
|
99
|
+
* @returns {Promise<object>}
|
|
100
|
+
*/
|
|
101
|
+
async get(id) {
|
|
102
|
+
const items = this._getAll()
|
|
103
|
+
const item = items.find(i => i[this.idField] === id)
|
|
104
|
+
if (!item) {
|
|
105
|
+
const error = new Error(`Entity not found: ${id}`)
|
|
106
|
+
error.status = 404
|
|
107
|
+
throw error
|
|
108
|
+
}
|
|
109
|
+
return item
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Get multiple entities by IDs (batch fetch)
|
|
114
|
+
* @param {Array<string|number>} ids
|
|
115
|
+
* @returns {Promise<Array<object>>}
|
|
116
|
+
*/
|
|
117
|
+
async getMany(ids) {
|
|
118
|
+
if (!ids || ids.length === 0) return []
|
|
119
|
+
const items = this._getAll()
|
|
120
|
+
const idSet = new Set(ids)
|
|
121
|
+
return items.filter(i => idSet.has(i[this.idField]))
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Create a new entity
|
|
126
|
+
* @param {object} data
|
|
127
|
+
* @returns {Promise<object>}
|
|
128
|
+
*/
|
|
129
|
+
async create(data) {
|
|
130
|
+
const items = this._getAll()
|
|
131
|
+
const newItem = {
|
|
132
|
+
...data,
|
|
133
|
+
[this.idField]: data[this.idField] || this.generateId(),
|
|
134
|
+
created_at: data.created_at || new Date().toISOString()
|
|
135
|
+
}
|
|
136
|
+
items.push(newItem)
|
|
137
|
+
this._saveAll(items)
|
|
138
|
+
return newItem
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Update an entity (PUT - full replacement)
|
|
143
|
+
* @param {string|number} id
|
|
144
|
+
* @param {object} data
|
|
145
|
+
* @returns {Promise<object>}
|
|
146
|
+
*/
|
|
147
|
+
async update(id, data) {
|
|
148
|
+
const items = this._getAll()
|
|
149
|
+
const index = items.findIndex(i => i[this.idField] === id)
|
|
150
|
+
if (index === -1) {
|
|
151
|
+
const error = new Error(`Entity not found: ${id}`)
|
|
152
|
+
error.status = 404
|
|
153
|
+
throw error
|
|
154
|
+
}
|
|
155
|
+
const updated = {
|
|
156
|
+
...data,
|
|
157
|
+
[this.idField]: id,
|
|
158
|
+
updated_at: new Date().toISOString()
|
|
159
|
+
}
|
|
160
|
+
items[index] = updated
|
|
161
|
+
this._saveAll(items)
|
|
162
|
+
return updated
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Partially update an entity (PATCH)
|
|
167
|
+
* @param {string|number} id
|
|
168
|
+
* @param {object} data
|
|
169
|
+
* @returns {Promise<object>}
|
|
170
|
+
*/
|
|
171
|
+
async patch(id, data) {
|
|
172
|
+
const items = this._getAll()
|
|
173
|
+
const index = items.findIndex(i => i[this.idField] === id)
|
|
174
|
+
if (index === -1) {
|
|
175
|
+
const error = new Error(`Entity not found: ${id}`)
|
|
176
|
+
error.status = 404
|
|
177
|
+
throw error
|
|
178
|
+
}
|
|
179
|
+
const updated = {
|
|
180
|
+
...items[index],
|
|
181
|
+
...data,
|
|
182
|
+
updated_at: new Date().toISOString()
|
|
183
|
+
}
|
|
184
|
+
items[index] = updated
|
|
185
|
+
this._saveAll(items)
|
|
186
|
+
return updated
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Delete an entity
|
|
191
|
+
* @param {string|number} id
|
|
192
|
+
* @returns {Promise<void>}
|
|
193
|
+
*/
|
|
194
|
+
async delete(id) {
|
|
195
|
+
const items = this._getAll()
|
|
196
|
+
const index = items.findIndex(i => i[this.idField] === id)
|
|
197
|
+
if (index === -1) {
|
|
198
|
+
const error = new Error(`Entity not found: ${id}`)
|
|
199
|
+
error.status = 404
|
|
200
|
+
throw error
|
|
201
|
+
}
|
|
202
|
+
items.splice(index, 1)
|
|
203
|
+
this._saveAll(items)
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Clear all items
|
|
208
|
+
* @returns {Promise<void>}
|
|
209
|
+
*/
|
|
210
|
+
async clear() {
|
|
211
|
+
localStorage.removeItem(this.key)
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Factory function to create a LocalStorage
|
|
217
|
+
*/
|
|
218
|
+
export function createLocalStorage(options) {
|
|
219
|
+
return new LocalStorage(options)
|
|
220
|
+
}
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MemoryStorage - In-memory storage adapter
|
|
3
|
+
*
|
|
4
|
+
* Implements the storage interface using in-memory Map.
|
|
5
|
+
* Useful for:
|
|
6
|
+
* - Testing
|
|
7
|
+
* - Ephemeral data
|
|
8
|
+
* - Caching layer
|
|
9
|
+
* - Client-side state management
|
|
10
|
+
*
|
|
11
|
+
* Usage:
|
|
12
|
+
* ```js
|
|
13
|
+
* const storage = new MemoryStorage({
|
|
14
|
+
* idField: 'id',
|
|
15
|
+
* generateId: () => crypto.randomUUID(),
|
|
16
|
+
* initialData: [{ id: '1', name: 'Alice' }]
|
|
17
|
+
* })
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
export class MemoryStorage {
|
|
21
|
+
constructor(options = {}) {
|
|
22
|
+
const {
|
|
23
|
+
idField = 'id',
|
|
24
|
+
generateId = () => Date.now().toString(36) + Math.random().toString(36).substr(2),
|
|
25
|
+
initialData = []
|
|
26
|
+
} = options
|
|
27
|
+
|
|
28
|
+
this.idField = idField
|
|
29
|
+
this.generateId = generateId
|
|
30
|
+
this._data = new Map()
|
|
31
|
+
|
|
32
|
+
// Initialize with initial data
|
|
33
|
+
for (const item of initialData) {
|
|
34
|
+
const id = item[idField]
|
|
35
|
+
if (id) {
|
|
36
|
+
this._data.set(String(id), { ...item })
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Get all items as array
|
|
43
|
+
* @returns {Array}
|
|
44
|
+
*/
|
|
45
|
+
_getAll() {
|
|
46
|
+
return Array.from(this._data.values())
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* List entities with pagination/filtering
|
|
51
|
+
* @param {object} params - { page, page_size, sort_by, sort_order, ...filters }
|
|
52
|
+
* @returns {Promise<{ items: Array, total: number }>}
|
|
53
|
+
*/
|
|
54
|
+
async list(params = {}) {
|
|
55
|
+
const { page = 1, page_size = 20, sort_by, sort_order = 'asc', ...filters } = params
|
|
56
|
+
|
|
57
|
+
let items = this._getAll()
|
|
58
|
+
|
|
59
|
+
// Apply filters
|
|
60
|
+
for (const [key, value] of Object.entries(filters)) {
|
|
61
|
+
if (value === null || value === undefined || value === '') continue
|
|
62
|
+
items = items.filter(item => {
|
|
63
|
+
const itemValue = item[key]
|
|
64
|
+
if (typeof value === 'string' && typeof itemValue === 'string') {
|
|
65
|
+
return itemValue.toLowerCase().includes(value.toLowerCase())
|
|
66
|
+
}
|
|
67
|
+
return itemValue === value
|
|
68
|
+
})
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const total = items.length
|
|
72
|
+
|
|
73
|
+
// Apply sorting
|
|
74
|
+
if (sort_by) {
|
|
75
|
+
items.sort((a, b) => {
|
|
76
|
+
const aVal = a[sort_by]
|
|
77
|
+
const bVal = b[sort_by]
|
|
78
|
+
if (aVal < bVal) return sort_order === 'asc' ? -1 : 1
|
|
79
|
+
if (aVal > bVal) return sort_order === 'asc' ? 1 : -1
|
|
80
|
+
return 0
|
|
81
|
+
})
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Apply pagination
|
|
85
|
+
const start = (page - 1) * page_size
|
|
86
|
+
items = items.slice(start, start + page_size)
|
|
87
|
+
|
|
88
|
+
return { items, total }
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Get a single entity by ID
|
|
93
|
+
* @param {string|number} id
|
|
94
|
+
* @returns {Promise<object>}
|
|
95
|
+
*/
|
|
96
|
+
async get(id) {
|
|
97
|
+
const item = this._data.get(String(id))
|
|
98
|
+
if (!item) {
|
|
99
|
+
const error = new Error(`Entity not found: ${id}`)
|
|
100
|
+
error.status = 404
|
|
101
|
+
throw error
|
|
102
|
+
}
|
|
103
|
+
return { ...item }
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Create a new entity
|
|
108
|
+
* @param {object} data
|
|
109
|
+
* @returns {Promise<object>}
|
|
110
|
+
*/
|
|
111
|
+
async create(data) {
|
|
112
|
+
const id = data[this.idField] || this.generateId()
|
|
113
|
+
const newItem = {
|
|
114
|
+
...data,
|
|
115
|
+
[this.idField]: id,
|
|
116
|
+
created_at: data.created_at || new Date().toISOString()
|
|
117
|
+
}
|
|
118
|
+
this._data.set(String(id), newItem)
|
|
119
|
+
return { ...newItem }
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Update an entity (PUT - full replacement)
|
|
124
|
+
* @param {string|number} id
|
|
125
|
+
* @param {object} data
|
|
126
|
+
* @returns {Promise<object>}
|
|
127
|
+
*/
|
|
128
|
+
async update(id, data) {
|
|
129
|
+
if (!this._data.has(String(id))) {
|
|
130
|
+
const error = new Error(`Entity not found: ${id}`)
|
|
131
|
+
error.status = 404
|
|
132
|
+
throw error
|
|
133
|
+
}
|
|
134
|
+
const updated = {
|
|
135
|
+
...data,
|
|
136
|
+
[this.idField]: id,
|
|
137
|
+
updated_at: new Date().toISOString()
|
|
138
|
+
}
|
|
139
|
+
this._data.set(String(id), updated)
|
|
140
|
+
return { ...updated }
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Partially update an entity (PATCH)
|
|
145
|
+
* @param {string|number} id
|
|
146
|
+
* @param {object} data
|
|
147
|
+
* @returns {Promise<object>}
|
|
148
|
+
*/
|
|
149
|
+
async patch(id, data) {
|
|
150
|
+
const existing = this._data.get(String(id))
|
|
151
|
+
if (!existing) {
|
|
152
|
+
const error = new Error(`Entity not found: ${id}`)
|
|
153
|
+
error.status = 404
|
|
154
|
+
throw error
|
|
155
|
+
}
|
|
156
|
+
const updated = {
|
|
157
|
+
...existing,
|
|
158
|
+
...data,
|
|
159
|
+
updated_at: new Date().toISOString()
|
|
160
|
+
}
|
|
161
|
+
this._data.set(String(id), updated)
|
|
162
|
+
return { ...updated }
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Delete an entity
|
|
167
|
+
* @param {string|number} id
|
|
168
|
+
* @returns {Promise<void>}
|
|
169
|
+
*/
|
|
170
|
+
async delete(id) {
|
|
171
|
+
if (!this._data.has(String(id))) {
|
|
172
|
+
const error = new Error(`Entity not found: ${id}`)
|
|
173
|
+
error.status = 404
|
|
174
|
+
throw error
|
|
175
|
+
}
|
|
176
|
+
this._data.delete(String(id))
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Clear all items
|
|
181
|
+
* @returns {Promise<void>}
|
|
182
|
+
*/
|
|
183
|
+
async clear() {
|
|
184
|
+
this._data.clear()
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Get current item count
|
|
189
|
+
* @returns {number}
|
|
190
|
+
*/
|
|
191
|
+
get size() {
|
|
192
|
+
return this._data.size
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Factory function to create a MemoryStorage
|
|
198
|
+
*/
|
|
199
|
+
export function createMemoryStorage(options) {
|
|
200
|
+
return new MemoryStorage(options)
|
|
201
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Storage Adapters
|
|
3
|
+
*
|
|
4
|
+
* Boilerplate implementations for common storage backends.
|
|
5
|
+
* EntityManagers can use these or implement their own storage.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export { ApiStorage, createApiStorage } from './ApiStorage.js'
|
|
9
|
+
export { LocalStorage, createLocalStorage } from './LocalStorage.js'
|
|
10
|
+
export { MemoryStorage, createMemoryStorage } from './MemoryStorage.js'
|
package/src/index.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* qdadm - Vue 3 Admin Dashboard Framework
|
|
3
|
+
*
|
|
4
|
+
* A framework for building admin dashboards with Vue 3, PrimeVue, and Vue Router.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
// Kernel (simplified bootstrap)
|
|
8
|
+
export * from './kernel/index.js'
|
|
9
|
+
|
|
10
|
+
// Plugin (manual bootstrap)
|
|
11
|
+
export { createQdadm } from './plugin.js'
|
|
12
|
+
|
|
13
|
+
// Entity system
|
|
14
|
+
export * from './entity/index.js'
|
|
15
|
+
|
|
16
|
+
// Orchestrator
|
|
17
|
+
export * from './orchestrator/index.js'
|
|
18
|
+
|
|
19
|
+
// Composables
|
|
20
|
+
export * from './composables/index.js'
|
|
21
|
+
|
|
22
|
+
// Components
|
|
23
|
+
export * from './components/index.js'
|
|
24
|
+
|
|
25
|
+
// Module system
|
|
26
|
+
export * from './module/index.js'
|
|
27
|
+
|
|
28
|
+
// Utils
|
|
29
|
+
export * from './utils/index.js'
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Kernel - Simplified bootstrap for qdadm applications
|
|
3
|
+
*
|
|
4
|
+
* Handles all the boilerplate:
|
|
5
|
+
* - Vue app creation
|
|
6
|
+
* - Pinia, PrimeVue, ToastService, ConfirmationService
|
|
7
|
+
* - Router with auth guard
|
|
8
|
+
* - Module discovery
|
|
9
|
+
* - qdadm plugin installation
|
|
10
|
+
*
|
|
11
|
+
* The constructor is purely declarative (stores config only).
|
|
12
|
+
* All initialization happens in createApp(), allowing you to:
|
|
13
|
+
* - Modify options before app creation
|
|
14
|
+
* - Add custom plugins/directives after createApp() but before mount()
|
|
15
|
+
*
|
|
16
|
+
* Basic usage:
|
|
17
|
+
* ```js
|
|
18
|
+
* const kernel = new Kernel({ root: App, managers, authAdapter, ... })
|
|
19
|
+
* kernel.createApp().mount('#app')
|
|
20
|
+
* ```
|
|
21
|
+
*
|
|
22
|
+
* Advanced usage (tweaking before/after):
|
|
23
|
+
* ```js
|
|
24
|
+
* const kernel = new Kernel({ ... })
|
|
25
|
+
*
|
|
26
|
+
* // Tweak config before app creation
|
|
27
|
+
* kernel.options.features.poweredBy = false
|
|
28
|
+
*
|
|
29
|
+
* // Create app
|
|
30
|
+
* const app = kernel.createApp()
|
|
31
|
+
*
|
|
32
|
+
* // Add custom plugins/components before mount
|
|
33
|
+
* app.component('MyGlobalComponent', MyComponent)
|
|
34
|
+
* app.directive('focus', focusDirective)
|
|
35
|
+
*
|
|
36
|
+
* app.mount('#app')
|
|
37
|
+
* ```
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
import { createApp } from 'vue'
|
|
41
|
+
import { createPinia } from 'pinia'
|
|
42
|
+
import { createRouter, createWebHistory } from 'vue-router'
|
|
43
|
+
import ToastService from 'primevue/toastservice'
|
|
44
|
+
import ConfirmationService from 'primevue/confirmationservice'
|
|
45
|
+
import Tooltip from 'primevue/tooltip'
|
|
46
|
+
|
|
47
|
+
import { createQdadm } from '../plugin.js'
|
|
48
|
+
import { initModules, getRoutes, setSectionOrder } from '../module/moduleRegistry.js'
|
|
49
|
+
import { Orchestrator } from '../orchestrator/Orchestrator.js'
|
|
50
|
+
|
|
51
|
+
export class Kernel {
|
|
52
|
+
/**
|
|
53
|
+
* @param {object} options
|
|
54
|
+
* @param {object} options.root - Root Vue component
|
|
55
|
+
* @param {object} options.modules - Result of import.meta.glob for module init files
|
|
56
|
+
* @param {string[]} options.sectionOrder - Navigation section order
|
|
57
|
+
* @param {object} options.managers - Entity managers { name: EntityManager }
|
|
58
|
+
* @param {object} options.authAdapter - Auth adapter for login/logout
|
|
59
|
+
* @param {object} options.pages - Page components { login, layout }
|
|
60
|
+
* @param {string} options.homeRoute - Route name for home redirect
|
|
61
|
+
* @param {object} options.app - App config { name, shortName, version, logo, theme }
|
|
62
|
+
* @param {object} options.features - Feature toggles { auth, poweredBy }
|
|
63
|
+
* @param {object} options.primevue - PrimeVue config { plugin, theme, options }
|
|
64
|
+
*/
|
|
65
|
+
constructor(options) {
|
|
66
|
+
this.options = options
|
|
67
|
+
this.vueApp = null
|
|
68
|
+
this.router = null
|
|
69
|
+
this.orchestrator = null
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Create and configure the Vue app
|
|
74
|
+
* @returns {App} Vue app instance ready to mount
|
|
75
|
+
*/
|
|
76
|
+
createApp() {
|
|
77
|
+
this._initModules()
|
|
78
|
+
this._createRouter()
|
|
79
|
+
this._createOrchestrator()
|
|
80
|
+
this._createVueApp()
|
|
81
|
+
this._installPlugins()
|
|
82
|
+
return this.vueApp
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Initialize modules from glob import
|
|
87
|
+
*/
|
|
88
|
+
_initModules() {
|
|
89
|
+
if (this.options.sectionOrder) {
|
|
90
|
+
setSectionOrder(this.options.sectionOrder)
|
|
91
|
+
}
|
|
92
|
+
if (this.options.modules) {
|
|
93
|
+
initModules(this.options.modules)
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Create Vue Router with auth guard
|
|
99
|
+
*/
|
|
100
|
+
_createRouter() {
|
|
101
|
+
const { pages, homeRoute, authAdapter } = this.options
|
|
102
|
+
|
|
103
|
+
// Validate required pages
|
|
104
|
+
if (!pages?.login) {
|
|
105
|
+
throw new Error('[Kernel] pages.login is required')
|
|
106
|
+
}
|
|
107
|
+
if (!pages?.layout) {
|
|
108
|
+
throw new Error('[Kernel] pages.layout is required')
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Build routes
|
|
112
|
+
const routes = [
|
|
113
|
+
{
|
|
114
|
+
path: '/login',
|
|
115
|
+
name: 'login',
|
|
116
|
+
component: pages.login
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
path: '/',
|
|
120
|
+
component: pages.layout,
|
|
121
|
+
meta: { requiresAuth: true },
|
|
122
|
+
children: [
|
|
123
|
+
{ path: '', redirect: { name: homeRoute || 'home' } },
|
|
124
|
+
...getRoutes()
|
|
125
|
+
]
|
|
126
|
+
}
|
|
127
|
+
]
|
|
128
|
+
|
|
129
|
+
this.router = createRouter({
|
|
130
|
+
history: createWebHistory(),
|
|
131
|
+
routes
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
// Auth guard
|
|
135
|
+
if (authAdapter) {
|
|
136
|
+
this.router.beforeEach((to, from, next) => {
|
|
137
|
+
if (to.meta.requiresAuth && !authAdapter.isAuthenticated()) {
|
|
138
|
+
next({ name: 'login' })
|
|
139
|
+
} else {
|
|
140
|
+
next()
|
|
141
|
+
}
|
|
142
|
+
})
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Create orchestrator with managers
|
|
148
|
+
*/
|
|
149
|
+
_createOrchestrator() {
|
|
150
|
+
this.orchestrator = new Orchestrator({
|
|
151
|
+
managers: this.options.managers || {}
|
|
152
|
+
})
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Create Vue app instance
|
|
157
|
+
*/
|
|
158
|
+
_createVueApp() {
|
|
159
|
+
if (!this.options.root) {
|
|
160
|
+
throw new Error('[Kernel] root component is required')
|
|
161
|
+
}
|
|
162
|
+
this.vueApp = createApp(this.options.root)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Install all plugins on Vue app
|
|
167
|
+
*/
|
|
168
|
+
_installPlugins() {
|
|
169
|
+
const app = this.vueApp
|
|
170
|
+
const { managers, authAdapter, features, primevue } = this.options
|
|
171
|
+
|
|
172
|
+
// Pinia
|
|
173
|
+
app.use(createPinia())
|
|
174
|
+
|
|
175
|
+
// PrimeVue (plugin passed by app to avoid peer dep issues)
|
|
176
|
+
if (primevue?.plugin) {
|
|
177
|
+
const pvConfig = {
|
|
178
|
+
theme: {
|
|
179
|
+
preset: primevue.theme,
|
|
180
|
+
options: {
|
|
181
|
+
darkModeSelector: '.dark-mode',
|
|
182
|
+
...primevue.options
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
app.use(primevue.plugin, pvConfig)
|
|
187
|
+
app.use(ToastService)
|
|
188
|
+
app.use(ConfirmationService)
|
|
189
|
+
app.directive('tooltip', Tooltip)
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Router
|
|
193
|
+
app.use(this.router)
|
|
194
|
+
|
|
195
|
+
// qdadm plugin
|
|
196
|
+
app.use(createQdadm({
|
|
197
|
+
orchestrator: this.orchestrator,
|
|
198
|
+
managers,
|
|
199
|
+
authAdapter,
|
|
200
|
+
router: this.router,
|
|
201
|
+
toast: app.config.globalProperties.$toast,
|
|
202
|
+
app: this.options.app,
|
|
203
|
+
features: {
|
|
204
|
+
auth: !!authAdapter,
|
|
205
|
+
poweredBy: true,
|
|
206
|
+
...features
|
|
207
|
+
}
|
|
208
|
+
}))
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Get the Vue Router instance
|
|
213
|
+
* @returns {Router}
|
|
214
|
+
*/
|
|
215
|
+
getRouter() {
|
|
216
|
+
return this.router
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Get the Vue app instance (available after mount)
|
|
221
|
+
* @returns {App}
|
|
222
|
+
*/
|
|
223
|
+
getApp() {
|
|
224
|
+
return this.vueApp
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Get the Orchestrator instance
|
|
229
|
+
* @returns {Orchestrator}
|
|
230
|
+
*/
|
|
231
|
+
getOrchestrator() {
|
|
232
|
+
return this.orchestrator
|
|
233
|
+
}
|
|
234
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* qdadm - Module system exports
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export {
|
|
6
|
+
registry,
|
|
7
|
+
initModules,
|
|
8
|
+
setSectionOrder,
|
|
9
|
+
getRoutes,
|
|
10
|
+
getNavSections,
|
|
11
|
+
getRouteFamilies,
|
|
12
|
+
getEntityConfigs,
|
|
13
|
+
getEntityConfig,
|
|
14
|
+
isRouteInFamily,
|
|
15
|
+
resetRegistry
|
|
16
|
+
} from './moduleRegistry'
|