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,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useNavigation - Navigation composable for AppLayout
|
|
3
|
+
*
|
|
4
|
+
* Provides reactive navigation state from moduleRegistry.
|
|
5
|
+
* All data comes from module init declarations.
|
|
6
|
+
* Nav items are filtered based on EntityManager.canRead() permissions.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { computed, inject } from 'vue'
|
|
10
|
+
import { useRoute, useRouter } from 'vue-router'
|
|
11
|
+
import { getNavSections, isRouteInFamily } from '../module/moduleRegistry'
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Navigation composable
|
|
15
|
+
*/
|
|
16
|
+
export function useNavigation() {
|
|
17
|
+
const route = useRoute()
|
|
18
|
+
const router = useRouter()
|
|
19
|
+
const orchestrator = inject('qdadmOrchestrator', null)
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Check if user can access a nav item based on its entity's canRead()
|
|
23
|
+
*/
|
|
24
|
+
function canAccessNavItem(item) {
|
|
25
|
+
if (!item.entity || !orchestrator) return true
|
|
26
|
+
const manager = orchestrator.get(item.entity)
|
|
27
|
+
if (!manager) return true
|
|
28
|
+
return manager.canRead()
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Get nav sections from registry, filtering items based on permissions
|
|
32
|
+
const navSections = computed(() => {
|
|
33
|
+
const sections = getNavSections()
|
|
34
|
+
return sections
|
|
35
|
+
.map(section => ({
|
|
36
|
+
...section,
|
|
37
|
+
items: section.items.filter(item => canAccessNavItem(item))
|
|
38
|
+
}))
|
|
39
|
+
.filter(section => section.items.length > 0) // Hide empty sections
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Check if a nav item is currently active
|
|
44
|
+
*/
|
|
45
|
+
function isNavActive(item) {
|
|
46
|
+
const currentRouteName = route.name
|
|
47
|
+
|
|
48
|
+
// Exact match mode
|
|
49
|
+
if (item.exact) {
|
|
50
|
+
return currentRouteName === item.route
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Use registry's family detection
|
|
54
|
+
return isRouteInFamily(currentRouteName, item.route)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Check if section contains active item
|
|
59
|
+
*/
|
|
60
|
+
function sectionHasActiveItem(section) {
|
|
61
|
+
return section.items.some(item => isNavActive(item))
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Handle nav click - force navigation if on same route with query params
|
|
66
|
+
*/
|
|
67
|
+
function handleNavClick(event, item) {
|
|
68
|
+
if (route.name === item.route && Object.keys(route.query).length > 0) {
|
|
69
|
+
event.preventDefault()
|
|
70
|
+
router.push({ name: item.route })
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
// Data (from moduleRegistry)
|
|
76
|
+
navSections,
|
|
77
|
+
|
|
78
|
+
// Active state
|
|
79
|
+
isNavActive,
|
|
80
|
+
sectionHasActiveItem,
|
|
81
|
+
|
|
82
|
+
// Event handlers
|
|
83
|
+
handleNavClick,
|
|
84
|
+
|
|
85
|
+
// Current route info (reactive)
|
|
86
|
+
currentRouteName: computed(() => route.name),
|
|
87
|
+
currentRoutePath: computed(() => route.path)
|
|
88
|
+
}
|
|
89
|
+
}
|
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
import { ref, computed } from 'vue'
|
|
2
|
+
import { useRouter } from 'vue-router'
|
|
3
|
+
import { useToast } from 'primevue/usetoast'
|
|
4
|
+
import { useConfirm } from 'primevue/useconfirm'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* usePageBuilder - Base builder for dashboard pages
|
|
8
|
+
*
|
|
9
|
+
* Provides procedural API for:
|
|
10
|
+
* - Header actions (addHeaderAction, addRefreshAction, etc.)
|
|
11
|
+
* - Cards zone (addCard)
|
|
12
|
+
* - Main content configuration (datatable, tree, custom)
|
|
13
|
+
*
|
|
14
|
+
* Usage:
|
|
15
|
+
* const page = usePageBuilder()
|
|
16
|
+
*
|
|
17
|
+
* // Header
|
|
18
|
+
* page.addHeaderAction('refresh', { icon: 'pi pi-refresh', onClick: refresh })
|
|
19
|
+
*
|
|
20
|
+
* // Cards
|
|
21
|
+
* page.addCard('total', { value: 42, label: 'Total Events' })
|
|
22
|
+
*
|
|
23
|
+
* // Main content - DataTable
|
|
24
|
+
* page.setDataTable({ lazy: true, paginator: true })
|
|
25
|
+
* page.addColumn('name', { header: 'Name', sortable: true })
|
|
26
|
+
* page.addRowAction('edit', { icon: 'pi pi-pencil', onClick: edit })
|
|
27
|
+
*
|
|
28
|
+
* // Or Tree view
|
|
29
|
+
* page.setTreeView({ selectionMode: 'single' })
|
|
30
|
+
*
|
|
31
|
+
* // Or fully custom (use slot in template)
|
|
32
|
+
* page.setCustomContent()
|
|
33
|
+
*/
|
|
34
|
+
export function usePageBuilder(_config = {}) {
|
|
35
|
+
const router = useRouter()
|
|
36
|
+
const toast = useToast()
|
|
37
|
+
const confirm = useConfirm()
|
|
38
|
+
|
|
39
|
+
// ============ HEADER ACTIONS ============
|
|
40
|
+
const headerActionsMap = ref(new Map())
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Add a header action button
|
|
44
|
+
*/
|
|
45
|
+
function addHeaderAction(name, actionConfig) {
|
|
46
|
+
headerActionsMap.value.set(name, {
|
|
47
|
+
name,
|
|
48
|
+
severity: 'secondary',
|
|
49
|
+
...actionConfig
|
|
50
|
+
})
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function removeHeaderAction(name) {
|
|
54
|
+
headerActionsMap.value.delete(name)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function updateHeaderAction(name, updates) {
|
|
58
|
+
const existing = headerActionsMap.value.get(name)
|
|
59
|
+
if (existing) {
|
|
60
|
+
headerActionsMap.value.set(name, { ...existing, ...updates })
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Get header actions with resolved state
|
|
66
|
+
*/
|
|
67
|
+
function getHeaderActions(state = {}) {
|
|
68
|
+
const actions = []
|
|
69
|
+
for (const [, action] of headerActionsMap.value) {
|
|
70
|
+
if (action.visible && !action.visible(state)) continue
|
|
71
|
+
actions.push({
|
|
72
|
+
...action,
|
|
73
|
+
label: typeof action.label === 'function' ? action.label(state) : action.label,
|
|
74
|
+
isLoading: action.loading ? action.loading(state) : false
|
|
75
|
+
})
|
|
76
|
+
}
|
|
77
|
+
return actions
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const headerActions = computed(() => getHeaderActions())
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Add standard refresh action
|
|
84
|
+
*/
|
|
85
|
+
function addRefreshAction(onClick, options = {}) {
|
|
86
|
+
const loadingRef = options.loadingRef || ref(false)
|
|
87
|
+
addHeaderAction('refresh', {
|
|
88
|
+
label: 'Refresh',
|
|
89
|
+
icon: 'pi pi-refresh',
|
|
90
|
+
onClick,
|
|
91
|
+
loading: () => loadingRef.value,
|
|
92
|
+
...options
|
|
93
|
+
})
|
|
94
|
+
return loadingRef
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// ============ CARDS ============
|
|
98
|
+
const cardsMap = ref(new Map())
|
|
99
|
+
|
|
100
|
+
function addCard(name, cardConfig) {
|
|
101
|
+
cardsMap.value.set(name, { name, ...cardConfig })
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function updateCard(name, cardConfig) {
|
|
105
|
+
const existing = cardsMap.value.get(name)
|
|
106
|
+
if (existing) {
|
|
107
|
+
cardsMap.value.set(name, { ...existing, ...cardConfig })
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function removeCard(name) {
|
|
112
|
+
cardsMap.value.delete(name)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const cards = computed(() => Array.from(cardsMap.value.values()))
|
|
116
|
+
|
|
117
|
+
// ============ MAIN CONTENT ============
|
|
118
|
+
const contentType = ref('custom') // 'datatable', 'tree', 'custom'
|
|
119
|
+
const contentConfig = ref({})
|
|
120
|
+
|
|
121
|
+
// DataTable specific
|
|
122
|
+
const columnsMap = ref(new Map())
|
|
123
|
+
const rowActionsMap = ref(new Map())
|
|
124
|
+
const items = ref([])
|
|
125
|
+
const loading = ref(false)
|
|
126
|
+
const selected = ref([])
|
|
127
|
+
|
|
128
|
+
// Pagination
|
|
129
|
+
const page = ref(1)
|
|
130
|
+
const pageSize = ref(10)
|
|
131
|
+
const totalRecords = ref(0)
|
|
132
|
+
const sortField = ref(null)
|
|
133
|
+
const sortOrder = ref(1)
|
|
134
|
+
|
|
135
|
+
// Search & Filters
|
|
136
|
+
const searchQuery = ref('')
|
|
137
|
+
const filtersMap = ref(new Map())
|
|
138
|
+
const filterValues = ref({})
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Set main content to DataTable
|
|
142
|
+
*/
|
|
143
|
+
function setDataTable(options = {}) {
|
|
144
|
+
contentType.value = 'datatable'
|
|
145
|
+
contentConfig.value = {
|
|
146
|
+
paginator: true,
|
|
147
|
+
rows: 10,
|
|
148
|
+
rowsPerPageOptions: [10, 25, 50],
|
|
149
|
+
stripedRows: true,
|
|
150
|
+
removableSort: true,
|
|
151
|
+
dataKey: 'id',
|
|
152
|
+
...options
|
|
153
|
+
}
|
|
154
|
+
if (options.pageSize) pageSize.value = options.pageSize
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Set main content to Tree view
|
|
159
|
+
*/
|
|
160
|
+
function setTreeView(options = {}) {
|
|
161
|
+
contentType.value = 'tree'
|
|
162
|
+
contentConfig.value = {
|
|
163
|
+
selectionMode: 'single',
|
|
164
|
+
...options
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Set main content to custom (use slot)
|
|
170
|
+
*/
|
|
171
|
+
function setCustomContent() {
|
|
172
|
+
contentType.value = 'custom'
|
|
173
|
+
contentConfig.value = {}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Add a column to DataTable
|
|
178
|
+
*/
|
|
179
|
+
function addColumn(field, columnConfig) {
|
|
180
|
+
columnsMap.value.set(field, {
|
|
181
|
+
field,
|
|
182
|
+
header: field.charAt(0).toUpperCase() + field.slice(1),
|
|
183
|
+
...columnConfig
|
|
184
|
+
})
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function removeColumn(field) {
|
|
188
|
+
columnsMap.value.delete(field)
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const columns = computed(() => Array.from(columnsMap.value.values()))
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Add a row action
|
|
195
|
+
*/
|
|
196
|
+
function addRowAction(name, actionConfig) {
|
|
197
|
+
rowActionsMap.value.set(name, {
|
|
198
|
+
name,
|
|
199
|
+
severity: 'secondary',
|
|
200
|
+
...actionConfig
|
|
201
|
+
})
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function removeRowAction(name) {
|
|
205
|
+
rowActionsMap.value.delete(name)
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function resolveValue(value, row) {
|
|
209
|
+
return typeof value === 'function' ? value(row) : value
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function getRowActions(row) {
|
|
213
|
+
const actions = []
|
|
214
|
+
for (const [, action] of rowActionsMap.value) {
|
|
215
|
+
if (action.visible && !action.visible(row)) continue
|
|
216
|
+
actions.push({
|
|
217
|
+
name: action.name,
|
|
218
|
+
icon: resolveValue(action.icon, row),
|
|
219
|
+
tooltip: resolveValue(action.tooltip, row),
|
|
220
|
+
severity: resolveValue(action.severity, row) || 'secondary',
|
|
221
|
+
isDisabled: action.disabled ? action.disabled(row) : false,
|
|
222
|
+
handler: () => action.onClick(row)
|
|
223
|
+
})
|
|
224
|
+
}
|
|
225
|
+
return actions
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Add a filter
|
|
230
|
+
*/
|
|
231
|
+
function addFilter(name, filterConfig) {
|
|
232
|
+
filtersMap.value.set(name, {
|
|
233
|
+
name,
|
|
234
|
+
type: 'select',
|
|
235
|
+
placeholder: name,
|
|
236
|
+
...filterConfig
|
|
237
|
+
})
|
|
238
|
+
if (filterValues.value[name] === undefined) {
|
|
239
|
+
filterValues.value[name] = filterConfig.default ?? null
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function removeFilter(name) {
|
|
244
|
+
filtersMap.value.delete(name)
|
|
245
|
+
delete filterValues.value[name]
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const filters = computed(() => Array.from(filtersMap.value.values()))
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Set search config
|
|
252
|
+
*/
|
|
253
|
+
const searchConfig = ref({ placeholder: 'Search...', fields: [] })
|
|
254
|
+
|
|
255
|
+
function setSearch(config) {
|
|
256
|
+
searchConfig.value = { ...searchConfig.value, ...config }
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Pagination handlers
|
|
260
|
+
function onPage(event) {
|
|
261
|
+
page.value = event.page + 1
|
|
262
|
+
pageSize.value = event.rows
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function onSort(event) {
|
|
266
|
+
sortField.value = event.sortField
|
|
267
|
+
sortOrder.value = event.sortOrder
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Selection
|
|
271
|
+
const hasSelection = computed(() => selected.value.length > 0)
|
|
272
|
+
const selectionCount = computed(() => selected.value.length)
|
|
273
|
+
|
|
274
|
+
return {
|
|
275
|
+
// Header Actions
|
|
276
|
+
headerActions,
|
|
277
|
+
addHeaderAction,
|
|
278
|
+
removeHeaderAction,
|
|
279
|
+
updateHeaderAction,
|
|
280
|
+
getHeaderActions,
|
|
281
|
+
addRefreshAction,
|
|
282
|
+
|
|
283
|
+
// Cards
|
|
284
|
+
cards,
|
|
285
|
+
addCard,
|
|
286
|
+
updateCard,
|
|
287
|
+
removeCard,
|
|
288
|
+
|
|
289
|
+
// Main Content
|
|
290
|
+
contentType,
|
|
291
|
+
contentConfig,
|
|
292
|
+
setDataTable,
|
|
293
|
+
setTreeView,
|
|
294
|
+
setCustomContent,
|
|
295
|
+
|
|
296
|
+
// DataTable
|
|
297
|
+
columns,
|
|
298
|
+
addColumn,
|
|
299
|
+
removeColumn,
|
|
300
|
+
items,
|
|
301
|
+
loading,
|
|
302
|
+
selected,
|
|
303
|
+
hasSelection,
|
|
304
|
+
selectionCount,
|
|
305
|
+
|
|
306
|
+
// Row Actions
|
|
307
|
+
addRowAction,
|
|
308
|
+
removeRowAction,
|
|
309
|
+
getRowActions,
|
|
310
|
+
|
|
311
|
+
// Pagination
|
|
312
|
+
page,
|
|
313
|
+
pageSize,
|
|
314
|
+
totalRecords,
|
|
315
|
+
sortField,
|
|
316
|
+
sortOrder,
|
|
317
|
+
onPage,
|
|
318
|
+
onSort,
|
|
319
|
+
|
|
320
|
+
// Search & Filters
|
|
321
|
+
searchQuery,
|
|
322
|
+
searchConfig,
|
|
323
|
+
setSearch,
|
|
324
|
+
filters,
|
|
325
|
+
filterValues,
|
|
326
|
+
addFilter,
|
|
327
|
+
removeFilter,
|
|
328
|
+
|
|
329
|
+
// Utilities
|
|
330
|
+
toast,
|
|
331
|
+
confirm,
|
|
332
|
+
router
|
|
333
|
+
}
|
|
334
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useStatus - Generic status composable
|
|
3
|
+
*
|
|
4
|
+
* Loads and formats status options from any API endpoint.
|
|
5
|
+
* Provides caching, label/severity lookup helpers.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* const { options, loadOptions, getLabel, getSeverity } = useStatus({
|
|
9
|
+
* endpoint: '/reference/queue-statuses',
|
|
10
|
+
* labelField: 'label',
|
|
11
|
+
* valueField: 'value',
|
|
12
|
+
* severityField: 'severity'
|
|
13
|
+
* })
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { ref, inject } from 'vue'
|
|
17
|
+
|
|
18
|
+
// Global cache for status options (keyed by endpoint)
|
|
19
|
+
const statusCache = new Map()
|
|
20
|
+
|
|
21
|
+
export function useStatus(config = {}) {
|
|
22
|
+
const {
|
|
23
|
+
endpoint,
|
|
24
|
+
labelField = 'label',
|
|
25
|
+
valueField = 'value',
|
|
26
|
+
severityField = 'severity',
|
|
27
|
+
// Optional: static options instead of API fetch
|
|
28
|
+
staticOptions = null,
|
|
29
|
+
// Optional: custom severity mapping function
|
|
30
|
+
severityMapper = null,
|
|
31
|
+
// Optional: default severity if not found
|
|
32
|
+
defaultSeverity = 'secondary'
|
|
33
|
+
} = config
|
|
34
|
+
|
|
35
|
+
const api = inject('apiAdapter')
|
|
36
|
+
const options = ref([])
|
|
37
|
+
const loading = ref(false)
|
|
38
|
+
const error = ref(null)
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Load options from API or use static options
|
|
42
|
+
*/
|
|
43
|
+
async function loadOptions(force = false) {
|
|
44
|
+
// Use static options if provided
|
|
45
|
+
if (staticOptions) {
|
|
46
|
+
options.value = staticOptions
|
|
47
|
+
return options.value
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (!endpoint) {
|
|
51
|
+
console.warn('[useStatus] No endpoint provided')
|
|
52
|
+
return []
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Check cache first (unless force refresh)
|
|
56
|
+
if (!force && statusCache.has(endpoint)) {
|
|
57
|
+
options.value = statusCache.get(endpoint)
|
|
58
|
+
return options.value
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (!api) {
|
|
62
|
+
console.warn('[useStatus] apiAdapter not provided')
|
|
63
|
+
return []
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
loading.value = true
|
|
67
|
+
error.value = null
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
const response = await api.request('GET', endpoint)
|
|
71
|
+
// Handle both array response and { items: [] } response
|
|
72
|
+
const items = Array.isArray(response) ? response : (response?.items || response?.data || [])
|
|
73
|
+
options.value = items
|
|
74
|
+
statusCache.set(endpoint, items)
|
|
75
|
+
return items
|
|
76
|
+
} catch (e) {
|
|
77
|
+
error.value = e.message
|
|
78
|
+
console.error(`[useStatus] Failed to load options from ${endpoint}:`, e)
|
|
79
|
+
return []
|
|
80
|
+
} finally {
|
|
81
|
+
loading.value = false
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Get label for a status value
|
|
87
|
+
*/
|
|
88
|
+
function getLabel(value) {
|
|
89
|
+
if (value == null) return ''
|
|
90
|
+
const option = options.value.find(opt =>
|
|
91
|
+
(typeof opt === 'string' ? opt : opt[valueField]) === value
|
|
92
|
+
)
|
|
93
|
+
if (!option) return String(value)
|
|
94
|
+
return typeof option === 'string' ? option : option[labelField]
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Get severity for a status value (for Tag/Badge color)
|
|
99
|
+
*/
|
|
100
|
+
function getSeverity(value) {
|
|
101
|
+
if (value == null) return defaultSeverity
|
|
102
|
+
|
|
103
|
+
// Use custom mapper if provided
|
|
104
|
+
if (severityMapper) {
|
|
105
|
+
return severityMapper(value) || defaultSeverity
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const option = options.value.find(opt =>
|
|
109
|
+
(typeof opt === 'string' ? opt : opt[valueField]) === value
|
|
110
|
+
)
|
|
111
|
+
if (!option || typeof option === 'string') return defaultSeverity
|
|
112
|
+
return option[severityField] || defaultSeverity
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Get full option object for a value
|
|
117
|
+
*/
|
|
118
|
+
function getOption(value) {
|
|
119
|
+
if (value == null) return null
|
|
120
|
+
return options.value.find(opt =>
|
|
121
|
+
(typeof opt === 'string' ? opt : opt[valueField]) === value
|
|
122
|
+
)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Clear cache for this endpoint or all endpoints
|
|
127
|
+
*/
|
|
128
|
+
function clearCache(all = false) {
|
|
129
|
+
if (all) {
|
|
130
|
+
statusCache.clear()
|
|
131
|
+
} else if (endpoint) {
|
|
132
|
+
statusCache.delete(endpoint)
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return {
|
|
137
|
+
options,
|
|
138
|
+
loading,
|
|
139
|
+
error,
|
|
140
|
+
loadOptions,
|
|
141
|
+
getLabel,
|
|
142
|
+
getSeverity,
|
|
143
|
+
getOption,
|
|
144
|
+
clearCache
|
|
145
|
+
}
|
|
146
|
+
}
|