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,165 @@
|
|
|
1
|
+
import { computed, inject } from 'vue'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Composable for building sub-editor components that edit a portion of parent form data.
|
|
5
|
+
*
|
|
6
|
+
* Sub-editors receive data via v-model and emit updates, but don't handle persistence.
|
|
7
|
+
* The parent form (using useForm/useBareForm) handles loading, saving, and dirty tracking.
|
|
8
|
+
*
|
|
9
|
+
* Usage in sub-editor:
|
|
10
|
+
* const props = defineProps({ modelValue: Object })
|
|
11
|
+
* const emit = defineEmits(['update:modelValue'])
|
|
12
|
+
*
|
|
13
|
+
* const { data, update, field } = useSubEditor(props, emit)
|
|
14
|
+
*
|
|
15
|
+
* // Access data
|
|
16
|
+
* data.value.someField
|
|
17
|
+
*
|
|
18
|
+
* // Update single field
|
|
19
|
+
* update('someField', newValue)
|
|
20
|
+
*
|
|
21
|
+
* // Computed getter/setter for field
|
|
22
|
+
* const myField = field('someField', defaultValue)
|
|
23
|
+
* myField.value = 'new value' // auto-emits update
|
|
24
|
+
*
|
|
25
|
+
* // Nested field access
|
|
26
|
+
* const nested = field('config.subsection.value', 0)
|
|
27
|
+
*
|
|
28
|
+
* Features:
|
|
29
|
+
* - Simplified update pattern (no manual spread)
|
|
30
|
+
* - Computed fields with get/set for v-model binding
|
|
31
|
+
* - Nested path support (dot notation)
|
|
32
|
+
* - Default values
|
|
33
|
+
* - Optional dirty field indicator integration with parent
|
|
34
|
+
*
|
|
35
|
+
* @param {Object} props - Component props (must include modelValue)
|
|
36
|
+
* @param {Function} emit - Component emit function
|
|
37
|
+
* @param {Object} options - Additional options
|
|
38
|
+
* @param {*} options.defaultData - Default data structure if modelValue is empty
|
|
39
|
+
*/
|
|
40
|
+
export function useSubEditor(props, emit, options = {}) {
|
|
41
|
+
const { defaultData = {} } = options
|
|
42
|
+
|
|
43
|
+
// Computed reference to data (with default fallback)
|
|
44
|
+
const data = computed(() => props.modelValue ?? defaultData)
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Get value at nested path
|
|
48
|
+
*/
|
|
49
|
+
function getNestedValue(obj, path) {
|
|
50
|
+
if (!path.includes('.')) {
|
|
51
|
+
return obj?.[path]
|
|
52
|
+
}
|
|
53
|
+
return path.split('.').reduce((curr, key) => curr?.[key], obj)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Set value at nested path, returning new object
|
|
58
|
+
*/
|
|
59
|
+
function setNestedValue(obj, path, value) {
|
|
60
|
+
if (!path.includes('.')) {
|
|
61
|
+
return { ...obj, [path]: value }
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const keys = path.split('.')
|
|
65
|
+
const result = { ...obj }
|
|
66
|
+
let current = result
|
|
67
|
+
|
|
68
|
+
for (let i = 0; i < keys.length - 1; i++) {
|
|
69
|
+
const key = keys[i]
|
|
70
|
+
current[key] = { ...current[key] }
|
|
71
|
+
current = current[key]
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
current[keys[keys.length - 1]] = value
|
|
75
|
+
return result
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Update a field and emit the new value
|
|
80
|
+
* @param {string} path - Field path (supports dot notation for nested)
|
|
81
|
+
* @param {*} value - New value
|
|
82
|
+
*/
|
|
83
|
+
function update(path, value) {
|
|
84
|
+
const newData = setNestedValue(data.value, path, value)
|
|
85
|
+
emit('update:modelValue', newData)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Replace entire data object
|
|
90
|
+
* @param {Object} newData - Complete new data object
|
|
91
|
+
*/
|
|
92
|
+
function replace(newData) {
|
|
93
|
+
emit('update:modelValue', newData)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Create a computed ref for a field with get/set
|
|
98
|
+
* Useful for v-model binding in template
|
|
99
|
+
*
|
|
100
|
+
* @param {string} path - Field path (supports dot notation)
|
|
101
|
+
* @param {*} defaultValue - Default value if field is undefined
|
|
102
|
+
* @returns {ComputedRef} Writable computed ref
|
|
103
|
+
*/
|
|
104
|
+
function field(path, defaultValue = undefined) {
|
|
105
|
+
return computed({
|
|
106
|
+
get: () => {
|
|
107
|
+
const value = getNestedValue(data.value, path)
|
|
108
|
+
return value !== undefined ? value : defaultValue
|
|
109
|
+
},
|
|
110
|
+
set: (value) => update(path, value)
|
|
111
|
+
})
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Create multiple field refs at once
|
|
116
|
+
* @param {Object} fields - { fieldName: defaultValue, ... }
|
|
117
|
+
* @returns {Object} { fieldName: computedRef, ... }
|
|
118
|
+
*/
|
|
119
|
+
function fields(fieldDefs) {
|
|
120
|
+
const result = {}
|
|
121
|
+
for (const [path, defaultValue] of Object.entries(fieldDefs)) {
|
|
122
|
+
result[path] = field(path, defaultValue)
|
|
123
|
+
}
|
|
124
|
+
return result
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Get value with default fallback (non-reactive, for one-time reads)
|
|
129
|
+
*/
|
|
130
|
+
function get(path, defaultValue = undefined) {
|
|
131
|
+
const value = getNestedValue(data.value, path)
|
|
132
|
+
return value !== undefined ? value : defaultValue
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Try to inject field-level dirty tracking from parent (if available)
|
|
136
|
+
const parentIsFieldDirty = inject('isFieldDirty', null)
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Check if a field is dirty (if parent provides tracking)
|
|
140
|
+
* This allows sub-editors to show field-level dirty indicators
|
|
141
|
+
*/
|
|
142
|
+
function isFieldDirty(path) {
|
|
143
|
+
if (!parentIsFieldDirty) return false
|
|
144
|
+
// The parent tracks 'form.value.fieldName' or similar
|
|
145
|
+
// We need to prefix with the path the parent uses
|
|
146
|
+
return parentIsFieldDirty(path)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return {
|
|
150
|
+
// Data access
|
|
151
|
+
data,
|
|
152
|
+
get,
|
|
153
|
+
|
|
154
|
+
// Updates
|
|
155
|
+
update,
|
|
156
|
+
replace,
|
|
157
|
+
|
|
158
|
+
// Field helpers
|
|
159
|
+
field,
|
|
160
|
+
fields,
|
|
161
|
+
|
|
162
|
+
// Dirty tracking (from parent)
|
|
163
|
+
isFieldDirty
|
|
164
|
+
}
|
|
165
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useTabSync - Composable for tab navigation with URL hash sync
|
|
3
|
+
*
|
|
4
|
+
* Provides:
|
|
5
|
+
* - activeTab computed from URL hash
|
|
6
|
+
* - onTabChange handler to update URL
|
|
7
|
+
* - Support for conditional tabs (edit mode only)
|
|
8
|
+
* - Lazy loading trigger on tab activation
|
|
9
|
+
*
|
|
10
|
+
* Usage:
|
|
11
|
+
* const { activeTab, onTabChange } = useTabSync({
|
|
12
|
+
* validTabs: ['general', 'style', 'behavior', 'newsrooms'],
|
|
13
|
+
* defaultTab: 'general',
|
|
14
|
+
* restrictedTabs: { newsrooms: () => isEdit.value },
|
|
15
|
+
* onTabActivate: { newsrooms: () => loadNewsroomData() }
|
|
16
|
+
* })
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { computed } from 'vue'
|
|
20
|
+
import { useRoute, useRouter } from 'vue-router'
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @param {Object} options
|
|
24
|
+
* @param {string[]} options.validTabs - List of valid tab values
|
|
25
|
+
* @param {string} [options.defaultTab='general'] - Default tab when hash is invalid
|
|
26
|
+
* @param {Object.<string, Function>} [options.restrictedTabs={}] - Tab name -> condition function
|
|
27
|
+
* @param {Object.<string, Function>} [options.onTabActivate={}] - Tab name -> callback on first activation
|
|
28
|
+
*/
|
|
29
|
+
export function useTabSync(options = {}) {
|
|
30
|
+
const {
|
|
31
|
+
validTabs = ['general'],
|
|
32
|
+
defaultTab = 'general',
|
|
33
|
+
restrictedTabs = {},
|
|
34
|
+
onTabActivate = {}
|
|
35
|
+
} = options
|
|
36
|
+
|
|
37
|
+
const route = useRoute()
|
|
38
|
+
const router = useRouter()
|
|
39
|
+
|
|
40
|
+
// Track which tabs have been activated (for lazy loading)
|
|
41
|
+
const activatedTabs = new Set([defaultTab])
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Active tab derived from URL hash
|
|
45
|
+
*/
|
|
46
|
+
const activeTab = computed(() => {
|
|
47
|
+
const hash = route.hash?.replace('#', '')
|
|
48
|
+
|
|
49
|
+
if (hash && validTabs.includes(hash)) {
|
|
50
|
+
// Check if tab is restricted
|
|
51
|
+
const restriction = restrictedTabs[hash]
|
|
52
|
+
if (restriction && !restriction()) {
|
|
53
|
+
return defaultTab
|
|
54
|
+
}
|
|
55
|
+
return hash
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return defaultTab
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Handle tab change - update URL hash and trigger callbacks
|
|
63
|
+
* @param {string} newTab
|
|
64
|
+
*/
|
|
65
|
+
function onTabChange(newTab) {
|
|
66
|
+
// Update URL hash
|
|
67
|
+
router.replace({ ...route, hash: `#${newTab}` })
|
|
68
|
+
|
|
69
|
+
// Trigger lazy load callback if first activation
|
|
70
|
+
if (!activatedTabs.has(newTab)) {
|
|
71
|
+
activatedTabs.add(newTab)
|
|
72
|
+
const callback = onTabActivate[newTab]
|
|
73
|
+
if (callback) {
|
|
74
|
+
callback()
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Check if a tab should be visible
|
|
81
|
+
* @param {string} tabName
|
|
82
|
+
* @returns {boolean}
|
|
83
|
+
*/
|
|
84
|
+
function isTabVisible(tabName) {
|
|
85
|
+
const restriction = restrictedTabs[tabName]
|
|
86
|
+
if (restriction) {
|
|
87
|
+
return restriction()
|
|
88
|
+
}
|
|
89
|
+
return true
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Navigate to a specific tab programmatically
|
|
94
|
+
* @param {string} tabName
|
|
95
|
+
*/
|
|
96
|
+
function goToTab(tabName) {
|
|
97
|
+
if (validTabs.includes(tabName) && isTabVisible(tabName)) {
|
|
98
|
+
onTabChange(tabName)
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
activeTab,
|
|
104
|
+
onTabChange,
|
|
105
|
+
isTabVisible,
|
|
106
|
+
goToTab
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export default useTabSync
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Composable for guarding against unsaved changes when leaving a page
|
|
3
|
+
*
|
|
4
|
+
* Usage:
|
|
5
|
+
* const { dirty, takeSnapshot, checkDirty } = useDirtyState(...)
|
|
6
|
+
* useUnsavedChangesGuard(dirty, {
|
|
7
|
+
* onSave: () => handleSubmit(false),
|
|
8
|
+
* message: 'You have unsaved changes.'
|
|
9
|
+
* })
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { onBeforeUnmount, ref } from 'vue'
|
|
13
|
+
import { onBeforeRouteLeave } from 'vue-router'
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Guard against leaving page with unsaved changes
|
|
17
|
+
*
|
|
18
|
+
* Shows a dialog with up to 3 options:
|
|
19
|
+
* - "Save & Leave" (if onSave provided) - saves then navigates
|
|
20
|
+
* - "Leave" - navigates without saving (discards changes)
|
|
21
|
+
* - "Stay" - cancels navigation
|
|
22
|
+
*
|
|
23
|
+
* @param {Ref<boolean>} dirty - Reactive dirty state
|
|
24
|
+
* @param {Object} options - Options
|
|
25
|
+
* @param {Function} options.onSave - Async function to save changes
|
|
26
|
+
* @param {string} options.message - Custom message (optional)
|
|
27
|
+
* @param {boolean} options.browserGuard - Show native browser dialog on tab close (default: false)
|
|
28
|
+
*/
|
|
29
|
+
export function useUnsavedChangesGuard(dirty, options = {}) {
|
|
30
|
+
const {
|
|
31
|
+
onSave = null,
|
|
32
|
+
message = 'You have unsaved changes that will be lost.',
|
|
33
|
+
browserGuard = false // Disabled by default - use soft JS modal only
|
|
34
|
+
} = options
|
|
35
|
+
|
|
36
|
+
// State for custom dialog
|
|
37
|
+
const showDialog = ref(false)
|
|
38
|
+
const pendingNext = ref(null)
|
|
39
|
+
const saving = ref(false)
|
|
40
|
+
|
|
41
|
+
// Browser beforeunload event (for closing tab/window) - optional
|
|
42
|
+
function handleBeforeUnload(e) {
|
|
43
|
+
if (dirty.value) {
|
|
44
|
+
e.preventDefault()
|
|
45
|
+
e.returnValue = message
|
|
46
|
+
return message
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Add browser event listener only if browserGuard is enabled
|
|
51
|
+
if (browserGuard && typeof window !== 'undefined') {
|
|
52
|
+
window.addEventListener('beforeunload', handleBeforeUnload)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Clean up on unmount
|
|
56
|
+
onBeforeUnmount(() => {
|
|
57
|
+
if (browserGuard && typeof window !== 'undefined') {
|
|
58
|
+
window.removeEventListener('beforeunload', handleBeforeUnload)
|
|
59
|
+
}
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
// Dialog actions
|
|
63
|
+
async function handleSaveAndLeave() {
|
|
64
|
+
if (!onSave || !pendingNext.value) return
|
|
65
|
+
|
|
66
|
+
saving.value = true
|
|
67
|
+
try {
|
|
68
|
+
await onSave()
|
|
69
|
+
showDialog.value = false
|
|
70
|
+
pendingNext.value()
|
|
71
|
+
pendingNext.value = null
|
|
72
|
+
} catch (e) {
|
|
73
|
+
console.error('Save failed:', e)
|
|
74
|
+
// Stay on page - don't close dialog, let user retry or choose Leave
|
|
75
|
+
} finally {
|
|
76
|
+
saving.value = false
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function handleLeave() {
|
|
81
|
+
if (!pendingNext.value) return
|
|
82
|
+
showDialog.value = false
|
|
83
|
+
pendingNext.value()
|
|
84
|
+
pendingNext.value = null
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function handleStay() {
|
|
88
|
+
showDialog.value = false
|
|
89
|
+
if (pendingNext.value) {
|
|
90
|
+
pendingNext.value(false)
|
|
91
|
+
pendingNext.value = null
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Vue Router navigation guard
|
|
96
|
+
onBeforeRouteLeave((to, from, next) => {
|
|
97
|
+
if (!dirty.value) {
|
|
98
|
+
next()
|
|
99
|
+
return
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Store the next callback and show dialog
|
|
103
|
+
pendingNext.value = next
|
|
104
|
+
showDialog.value = true
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
dirty,
|
|
109
|
+
// Dialog state and handlers for custom dialog component
|
|
110
|
+
guardDialog: {
|
|
111
|
+
visible: showDialog,
|
|
112
|
+
saving,
|
|
113
|
+
message,
|
|
114
|
+
hasOnSave: !!onSave,
|
|
115
|
+
onSaveAndLeave: handleSaveAndLeave,
|
|
116
|
+
onLeave: handleLeave,
|
|
117
|
+
onStay: handleStay
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export default useUnsavedChangesGuard
|