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,261 @@
|
|
|
1
|
+
/* eslint-disable no-console */
|
|
2
|
+
/**
|
|
3
|
+
* Debug Injector - Bypasses MCP/DevTools limitations
|
|
4
|
+
*
|
|
5
|
+
* Usage in browser console:
|
|
6
|
+
* window.__debug.listFilters()
|
|
7
|
+
* window.__debug.setFilter('queue_status', 'pending')
|
|
8
|
+
* window.__debug.getVueApp()
|
|
9
|
+
* window.__debug.triggerEvent('filterChange', { name: 'queue_status', value: 'pending' })
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
class DebugInjector {
|
|
13
|
+
constructor() {
|
|
14
|
+
this.vueApp = null
|
|
15
|
+
this.components = new Map()
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Initialize and expose to window
|
|
20
|
+
*/
|
|
21
|
+
init() {
|
|
22
|
+
window.__debug = this
|
|
23
|
+
console.log('🔧 Debug Injector initialized. Use window.__debug to access.')
|
|
24
|
+
this.findVueApp()
|
|
25
|
+
return this
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Find Vue app instance
|
|
30
|
+
*/
|
|
31
|
+
findVueApp() {
|
|
32
|
+
const appEl = document.querySelector('#app')
|
|
33
|
+
if (appEl && appEl.__vue_app__) {
|
|
34
|
+
this.vueApp = appEl.__vue_app__
|
|
35
|
+
console.log('✅ Vue app found:', this.vueApp)
|
|
36
|
+
return this.vueApp
|
|
37
|
+
}
|
|
38
|
+
console.warn('⚠️ Vue app not found on #app')
|
|
39
|
+
return null
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Get Vue component instance from DOM element
|
|
44
|
+
*/
|
|
45
|
+
getComponentFromElement(selector) {
|
|
46
|
+
const el = typeof selector === 'string' ? document.querySelector(selector) : selector
|
|
47
|
+
if (!el) return null
|
|
48
|
+
|
|
49
|
+
// Vue 3 stores component on __vueParentComponent
|
|
50
|
+
let vnode = el.__vueParentComponent
|
|
51
|
+
while (vnode && !vnode.proxy) {
|
|
52
|
+
vnode = vnode.parent
|
|
53
|
+
}
|
|
54
|
+
return vnode?.proxy || null
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Find all PrimeVue Select components
|
|
59
|
+
*/
|
|
60
|
+
findSelects() {
|
|
61
|
+
const selects = document.querySelectorAll('[data-pc-name="select"]')
|
|
62
|
+
const results = []
|
|
63
|
+
selects.forEach((el, i) => {
|
|
64
|
+
const component = this.getComponentFromElement(el)
|
|
65
|
+
results.push({
|
|
66
|
+
index: i,
|
|
67
|
+
element: el,
|
|
68
|
+
component,
|
|
69
|
+
value: component?.modelValue,
|
|
70
|
+
placeholder: el.querySelector('[data-pc-section="label"]')?.textContent
|
|
71
|
+
})
|
|
72
|
+
})
|
|
73
|
+
console.table(results.map(r => ({
|
|
74
|
+
index: r.index,
|
|
75
|
+
placeholder: r.placeholder,
|
|
76
|
+
value: r.value
|
|
77
|
+
})))
|
|
78
|
+
return results
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Simulate selecting a value in a PrimeVue Select
|
|
83
|
+
*/
|
|
84
|
+
selectValue(selectIndex, optionText) {
|
|
85
|
+
const selects = this.findSelects()
|
|
86
|
+
const select = selects[selectIndex]
|
|
87
|
+
if (!select) {
|
|
88
|
+
console.error(`Select ${selectIndex} not found`)
|
|
89
|
+
return false
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Click to open dropdown
|
|
93
|
+
select.element.click()
|
|
94
|
+
|
|
95
|
+
// Wait for dropdown to open and find option
|
|
96
|
+
setTimeout(() => {
|
|
97
|
+
const options = document.querySelectorAll('[data-pc-section="option"]')
|
|
98
|
+
for (const opt of options) {
|
|
99
|
+
if (opt.textContent.includes(optionText)) {
|
|
100
|
+
console.log(`🎯 Clicking option: ${opt.textContent}`)
|
|
101
|
+
opt.click()
|
|
102
|
+
return true
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
console.error(`Option "${optionText}" not found`)
|
|
106
|
+
// Close dropdown
|
|
107
|
+
document.body.click()
|
|
108
|
+
}, 100)
|
|
109
|
+
|
|
110
|
+
return true
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Direct Vue reactivity manipulation - find ListPage component
|
|
115
|
+
*/
|
|
116
|
+
findListPage() {
|
|
117
|
+
// Find the main content area
|
|
118
|
+
const main = document.querySelector('main')
|
|
119
|
+
if (!main) return null
|
|
120
|
+
|
|
121
|
+
let component = this.getComponentFromElement(main)
|
|
122
|
+
|
|
123
|
+
// Walk up to find ListPage
|
|
124
|
+
while (component) {
|
|
125
|
+
if (component.$options?.name === 'ListPage' || component.localFilterValues) {
|
|
126
|
+
console.log('✅ Found ListPage component:', component)
|
|
127
|
+
return component
|
|
128
|
+
}
|
|
129
|
+
component = component.$parent
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Alternative: search in Vue app's component tree
|
|
133
|
+
console.warn('ListPage not found via DOM, searching component tree...')
|
|
134
|
+
return null
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Get current filter values from ListPage
|
|
139
|
+
*/
|
|
140
|
+
getFilterValues() {
|
|
141
|
+
const listPage = this.findListPage()
|
|
142
|
+
if (listPage) {
|
|
143
|
+
console.log('Filter values:', listPage.localFilterValues)
|
|
144
|
+
return listPage.localFilterValues
|
|
145
|
+
}
|
|
146
|
+
return null
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Directly set a filter value (bypasses UI)
|
|
151
|
+
*/
|
|
152
|
+
setFilterDirect(filterName, value) {
|
|
153
|
+
const listPage = this.findListPage()
|
|
154
|
+
if (!listPage) {
|
|
155
|
+
console.error('ListPage not found')
|
|
156
|
+
return false
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
console.log(`Setting ${filterName} = ${value}`)
|
|
160
|
+
listPage.localFilterValues[filterName] = value
|
|
161
|
+
|
|
162
|
+
// Trigger the change handler
|
|
163
|
+
if (listPage.onFilterChange) {
|
|
164
|
+
listPage.onFilterChange(filterName)
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return true
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Dispatch a custom event that Vue components can listen to
|
|
172
|
+
*/
|
|
173
|
+
dispatchFilterChange(filterName, value) {
|
|
174
|
+
const event = new CustomEvent('debug:filterChange', {
|
|
175
|
+
detail: { filterName, value },
|
|
176
|
+
bubbles: true
|
|
177
|
+
})
|
|
178
|
+
document.dispatchEvent(event)
|
|
179
|
+
console.log(`📤 Dispatched debug:filterChange for ${filterName}=${value}`)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Log all Vue component instances in the tree
|
|
184
|
+
*/
|
|
185
|
+
logComponentTree(root = null, depth = 0) {
|
|
186
|
+
if (!root) {
|
|
187
|
+
root = this.findVueApp()?._instance
|
|
188
|
+
if (!root) return
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const indent = ' '.repeat(depth)
|
|
192
|
+
const name = root.type?.name || root.type?.__name || 'Anonymous'
|
|
193
|
+
console.log(`${indent}${name}`)
|
|
194
|
+
|
|
195
|
+
if (root.subTree?.children) {
|
|
196
|
+
for (const child of root.subTree.children) {
|
|
197
|
+
if (child?.component) {
|
|
198
|
+
this.logComponentTree(child.component, depth + 1)
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Execute code in Vue component context
|
|
206
|
+
*/
|
|
207
|
+
execInComponent(selector, code) {
|
|
208
|
+
const component = this.getComponentFromElement(selector)
|
|
209
|
+
if (!component) {
|
|
210
|
+
console.error('Component not found')
|
|
211
|
+
return null
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
try {
|
|
215
|
+
const fn = new Function('component', `with(component) { return ${code} }`)
|
|
216
|
+
return fn(component)
|
|
217
|
+
} catch (e) {
|
|
218
|
+
console.error('Exec failed:', e)
|
|
219
|
+
return null
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Monitor all events on an element
|
|
225
|
+
*/
|
|
226
|
+
monitorEvents(selector) {
|
|
227
|
+
const el = document.querySelector(selector)
|
|
228
|
+
if (!el) return
|
|
229
|
+
|
|
230
|
+
const events = ['click', 'change', 'input', 'focus', 'blur', 'mousedown', 'mouseup']
|
|
231
|
+
events.forEach(evt => {
|
|
232
|
+
el.addEventListener(evt, (e) => {
|
|
233
|
+
console.log(`📡 ${evt}:`, e.target, e)
|
|
234
|
+
}, true)
|
|
235
|
+
})
|
|
236
|
+
console.log(`Monitoring events on ${selector}`)
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Quick help
|
|
241
|
+
*/
|
|
242
|
+
help() {
|
|
243
|
+
console.log(`
|
|
244
|
+
🔧 Debug Injector Commands:
|
|
245
|
+
__debug.findSelects() - List all Select components
|
|
246
|
+
__debug.selectValue(0, 'text') - Select option by text in Select #0
|
|
247
|
+
__debug.findListPage() - Find ListPage component
|
|
248
|
+
__debug.getFilterValues() - Get current filter values
|
|
249
|
+
__debug.setFilterDirect('name', 'value') - Directly set filter
|
|
250
|
+
__debug.monitorEvents('selector') - Monitor all events
|
|
251
|
+
__debug.logComponentTree() - Log Vue component tree
|
|
252
|
+
__debug.help() - Show this help
|
|
253
|
+
`)
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Auto-initialize in development
|
|
258
|
+
const debugInjector = new DebugInjector()
|
|
259
|
+
|
|
260
|
+
export default debugInjector
|
|
261
|
+
export { DebugInjector }
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Centralized formatting utilities
|
|
3
|
+
*
|
|
4
|
+
* Usage:
|
|
5
|
+
* import { formatDate, formatDateTime, formatDuration } from '@/dashboard/utils/formatters'
|
|
6
|
+
*
|
|
7
|
+
* {{ formatDate(item.created_at) }}
|
|
8
|
+
* {{ formatDateTime(item.updated_at) }}
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
// =============================================================================
|
|
12
|
+
// DATE/TIME FORMATTING
|
|
13
|
+
// =============================================================================
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Format a date string to localized date+time
|
|
17
|
+
* Default format: toLocaleString() (e.g., "20/12/2025, 14:30:00")
|
|
18
|
+
*
|
|
19
|
+
* @param {string|Date} dateStr - ISO date string or Date object
|
|
20
|
+
* @param {object} options - Intl.DateTimeFormat options
|
|
21
|
+
* @returns {string} Formatted date string or '-' if empty
|
|
22
|
+
*/
|
|
23
|
+
export function formatDate(dateStr, options = {}) {
|
|
24
|
+
if (!dateStr) return '-'
|
|
25
|
+
const date = new Date(dateStr)
|
|
26
|
+
if (isNaN(date.getTime())) return '-'
|
|
27
|
+
|
|
28
|
+
if (Object.keys(options).length === 0) {
|
|
29
|
+
return date.toLocaleString()
|
|
30
|
+
}
|
|
31
|
+
return date.toLocaleString(undefined, options)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Format to full datetime with year (20/12/2025 14:30)
|
|
36
|
+
*/
|
|
37
|
+
export function formatDateTime(dateStr) {
|
|
38
|
+
return formatDate(dateStr, {
|
|
39
|
+
year: 'numeric',
|
|
40
|
+
month: '2-digit',
|
|
41
|
+
day: '2-digit',
|
|
42
|
+
hour: '2-digit',
|
|
43
|
+
minute: '2-digit'
|
|
44
|
+
})
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Format to short datetime without year (20/12 14:30)
|
|
49
|
+
*/
|
|
50
|
+
export function formatDateTimeShort(dateStr) {
|
|
51
|
+
return formatDate(dateStr, {
|
|
52
|
+
month: '2-digit',
|
|
53
|
+
day: '2-digit',
|
|
54
|
+
hour: '2-digit',
|
|
55
|
+
minute: '2-digit'
|
|
56
|
+
})
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Format to date only (20/12/2025)
|
|
61
|
+
*/
|
|
62
|
+
export function formatDateOnly(dateStr) {
|
|
63
|
+
if (!dateStr) return '-'
|
|
64
|
+
const date = new Date(dateStr)
|
|
65
|
+
if (isNaN(date.getTime())) return '-'
|
|
66
|
+
return date.toLocaleDateString(undefined, {
|
|
67
|
+
year: 'numeric',
|
|
68
|
+
month: '2-digit',
|
|
69
|
+
day: '2-digit'
|
|
70
|
+
})
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Format to time only (14:30:00)
|
|
75
|
+
*/
|
|
76
|
+
export function formatTimeOnly(dateStr) {
|
|
77
|
+
if (!dateStr) return '-'
|
|
78
|
+
const date = new Date(dateStr)
|
|
79
|
+
if (isNaN(date.getTime())) return '-'
|
|
80
|
+
return date.toLocaleTimeString()
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Format with "Never" as default (for last_used fields)
|
|
85
|
+
*/
|
|
86
|
+
export function formatDateOrNever(dateStr) {
|
|
87
|
+
if (!dateStr) return 'Never'
|
|
88
|
+
return formatDate(dateStr)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// =============================================================================
|
|
92
|
+
// DURATION FORMATTING
|
|
93
|
+
// =============================================================================
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Format a duration in milliseconds to human-readable string
|
|
97
|
+
*
|
|
98
|
+
* @param {number} ms - Duration in milliseconds
|
|
99
|
+
* @returns {string} Human-readable duration (e.g., "2h 30m 15s" or "45.2s")
|
|
100
|
+
*/
|
|
101
|
+
export function formatDuration(ms) {
|
|
102
|
+
if (ms === null || ms === undefined) return '-'
|
|
103
|
+
if (ms < 1000) return `${ms}ms`
|
|
104
|
+
|
|
105
|
+
const seconds = Math.floor(ms / 1000)
|
|
106
|
+
const minutes = Math.floor(seconds / 60)
|
|
107
|
+
const hours = Math.floor(minutes / 60)
|
|
108
|
+
|
|
109
|
+
if (hours > 0) {
|
|
110
|
+
const remainingMinutes = minutes % 60
|
|
111
|
+
const remainingSeconds = seconds % 60
|
|
112
|
+
return `${hours}h ${remainingMinutes}m ${remainingSeconds}s`
|
|
113
|
+
}
|
|
114
|
+
if (minutes > 0) {
|
|
115
|
+
const remainingSeconds = seconds % 60
|
|
116
|
+
return `${minutes}m ${remainingSeconds}s`
|
|
117
|
+
}
|
|
118
|
+
// Show decimal seconds for short durations
|
|
119
|
+
return `${(ms / 1000).toFixed(1)}s`
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Format duration from start/end timestamps
|
|
124
|
+
*/
|
|
125
|
+
export function formatDurationBetween(startDate, endDate) {
|
|
126
|
+
if (!startDate || !endDate) return '-'
|
|
127
|
+
const start = new Date(startDate)
|
|
128
|
+
const end = new Date(endDate)
|
|
129
|
+
if (isNaN(start.getTime()) || isNaN(end.getTime())) return '-'
|
|
130
|
+
return formatDuration(end - start)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// =============================================================================
|
|
134
|
+
// NUMBER FORMATTING
|
|
135
|
+
// =============================================================================
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Format a number with locale-specific separators
|
|
139
|
+
*/
|
|
140
|
+
export function formatNumber(value, options = {}) {
|
|
141
|
+
if (value === null || value === undefined) return '-'
|
|
142
|
+
return value.toLocaleString(undefined, options)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Format bytes to human-readable size
|
|
147
|
+
*/
|
|
148
|
+
export function formatBytes(bytes) {
|
|
149
|
+
if (bytes === null || bytes === undefined) return '-'
|
|
150
|
+
if (bytes === 0) return '0 B'
|
|
151
|
+
|
|
152
|
+
const units = ['B', 'KB', 'MB', 'GB', 'TB']
|
|
153
|
+
const k = 1024
|
|
154
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
|
155
|
+
|
|
156
|
+
return `${(bytes / Math.pow(k, i)).toFixed(1)} ${units[i]}`
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Format a percentage
|
|
161
|
+
*/
|
|
162
|
+
export function formatPercent(value, decimals = 1) {
|
|
163
|
+
if (value === null || value === undefined) return '-'
|
|
164
|
+
return `${(value * 100).toFixed(decimals)}%`
|
|
165
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* qdadm - Utils exports
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
// Formatters
|
|
6
|
+
export {
|
|
7
|
+
formatDate,
|
|
8
|
+
formatDateTime,
|
|
9
|
+
formatDateTimeShort,
|
|
10
|
+
formatDateOnly,
|
|
11
|
+
formatTimeOnly,
|
|
12
|
+
formatDateOrNever,
|
|
13
|
+
formatDuration,
|
|
14
|
+
formatDurationBetween,
|
|
15
|
+
formatNumber,
|
|
16
|
+
formatBytes,
|
|
17
|
+
formatPercent
|
|
18
|
+
} from './formatters'
|
|
19
|
+
|
|
20
|
+
// Transformers
|
|
21
|
+
export {
|
|
22
|
+
toKeyValueArray,
|
|
23
|
+
toKeyValueObject,
|
|
24
|
+
toLanguageCodes,
|
|
25
|
+
toLanguageObjects,
|
|
26
|
+
toDateObject,
|
|
27
|
+
toIsoDate,
|
|
28
|
+
toIsoDateTime,
|
|
29
|
+
deepClone,
|
|
30
|
+
getSparseUpdate,
|
|
31
|
+
isEqual
|
|
32
|
+
} from './transformers'
|
|
33
|
+
|
|
34
|
+
// Debug Injector
|
|
35
|
+
export { default as debugInjector, DebugInjector } from './debugInjector'
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Data transformation utilities for forms
|
|
3
|
+
*
|
|
4
|
+
* Provides consistent conversions between API data formats and form structures.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Convert object {key: value} to array [{key, value}] for KeyValueEditor
|
|
9
|
+
*/
|
|
10
|
+
export function toKeyValueArray(obj) {
|
|
11
|
+
if (!obj || typeof obj !== 'object') return []
|
|
12
|
+
return Object.entries(obj).map(([key, value]) => ({ key, value }))
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Convert array [{key, value}] to object {key: value} for API
|
|
17
|
+
*/
|
|
18
|
+
export function toKeyValueObject(arr) {
|
|
19
|
+
if (!Array.isArray(arr)) return {}
|
|
20
|
+
return Object.fromEntries(
|
|
21
|
+
arr.filter(item => item.key && item.key.trim())
|
|
22
|
+
.map(item => [item.key.trim(), item.value])
|
|
23
|
+
)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Convert language objects [{code, fluency}] to array of codes ['en', 'fr']
|
|
28
|
+
*/
|
|
29
|
+
export function toLanguageCodes(languages) {
|
|
30
|
+
if (!Array.isArray(languages)) return []
|
|
31
|
+
return languages.map(l => typeof l === 'string' ? l : l.code).filter(Boolean)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Convert array of codes to language objects [{code, fluency}]
|
|
36
|
+
*/
|
|
37
|
+
export function toLanguageObjects(codes, defaultFluency = 1) {
|
|
38
|
+
if (!Array.isArray(codes)) return []
|
|
39
|
+
return codes.filter(Boolean).map(code => ({
|
|
40
|
+
code: typeof code === 'string' ? code : code.code,
|
|
41
|
+
fluency: typeof code === 'object' ? code.fluency : defaultFluency
|
|
42
|
+
}))
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Convert ISO date string to Date object
|
|
47
|
+
*/
|
|
48
|
+
export function toDateObject(dateStr) {
|
|
49
|
+
if (!dateStr) return null
|
|
50
|
+
if (dateStr instanceof Date) return dateStr
|
|
51
|
+
return new Date(dateStr)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Convert Date to ISO date string (YYYY-MM-DD)
|
|
56
|
+
*/
|
|
57
|
+
export function toIsoDate(date) {
|
|
58
|
+
if (!date) return null
|
|
59
|
+
if (typeof date === 'string') return date.split('T')[0]
|
|
60
|
+
return date.toISOString().split('T')[0]
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Convert Date to ISO datetime string
|
|
65
|
+
*/
|
|
66
|
+
export function toIsoDateTime(date) {
|
|
67
|
+
if (!date) return null
|
|
68
|
+
if (typeof date === 'string') return date
|
|
69
|
+
return date.toISOString()
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Deep clone an object (for snapshots)
|
|
74
|
+
*/
|
|
75
|
+
export function deepClone(obj) {
|
|
76
|
+
if (obj === null || typeof obj !== 'object') return obj
|
|
77
|
+
if (obj instanceof Date) return new Date(obj)
|
|
78
|
+
if (Array.isArray(obj)) return obj.map(deepClone)
|
|
79
|
+
return Object.fromEntries(
|
|
80
|
+
Object.entries(obj).map(([k, v]) => [k, deepClone(v)])
|
|
81
|
+
)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Get sparse update object (only changed fields)
|
|
86
|
+
*/
|
|
87
|
+
export function getSparseUpdate(original, modified, excludeFields = []) {
|
|
88
|
+
const patch = {}
|
|
89
|
+
for (const key in modified) {
|
|
90
|
+
if (excludeFields.includes(key)) continue
|
|
91
|
+
const origVal = JSON.stringify(original[key])
|
|
92
|
+
const modVal = JSON.stringify(modified[key])
|
|
93
|
+
if (origVal !== modVal) {
|
|
94
|
+
patch[key] = modified[key]
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return patch
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Check if two values are deeply equal
|
|
102
|
+
*/
|
|
103
|
+
export function isEqual(a, b) {
|
|
104
|
+
return JSON.stringify(a) === JSON.stringify(b)
|
|
105
|
+
}
|