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,467 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
/**
|
|
3
|
+
* JsonEditorFoldable - JSON editor with collapsible sections
|
|
4
|
+
*
|
|
5
|
+
* Displays each top-level key as a foldable section.
|
|
6
|
+
* Long content (like "content" field) can be collapsed to reduce visual noise.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* <JsonEditorFoldable v-model="jsonData" :defaultExpanded="['content']" />
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { ref, computed, watch } from 'vue'
|
|
13
|
+
import Button from 'primevue/button'
|
|
14
|
+
import Textarea from 'primevue/textarea'
|
|
15
|
+
import InputText from 'primevue/inputtext'
|
|
16
|
+
import Message from 'primevue/message'
|
|
17
|
+
import { getJsonValueType, getJsonPreview } from '../../composables/useJsonSyntax'
|
|
18
|
+
|
|
19
|
+
const props = defineProps({
|
|
20
|
+
modelValue: {
|
|
21
|
+
type: [Object, null],
|
|
22
|
+
default: () => ({})
|
|
23
|
+
},
|
|
24
|
+
defaultExpanded: {
|
|
25
|
+
type: Array,
|
|
26
|
+
default: () => ['content'] // "content" expanded by default
|
|
27
|
+
},
|
|
28
|
+
height: {
|
|
29
|
+
type: String,
|
|
30
|
+
default: '400px'
|
|
31
|
+
}
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
const emit = defineEmits(['update:modelValue'])
|
|
35
|
+
|
|
36
|
+
// Internal state: track which sections are expanded
|
|
37
|
+
const expandedSections = ref(new Set(props.defaultExpanded))
|
|
38
|
+
|
|
39
|
+
// Internal copy of data for editing
|
|
40
|
+
const localData = ref({})
|
|
41
|
+
|
|
42
|
+
// Parse error for raw JSON mode
|
|
43
|
+
const parseError = ref(null)
|
|
44
|
+
const rawJsonMode = ref(false)
|
|
45
|
+
const rawJsonText = ref('')
|
|
46
|
+
|
|
47
|
+
// Sync from modelValue
|
|
48
|
+
watch(() => props.modelValue, (newVal) => {
|
|
49
|
+
if (newVal && typeof newVal === 'object') {
|
|
50
|
+
localData.value = JSON.parse(JSON.stringify(newVal))
|
|
51
|
+
rawJsonText.value = JSON.stringify(newVal, null, 2)
|
|
52
|
+
} else {
|
|
53
|
+
localData.value = {}
|
|
54
|
+
rawJsonText.value = '{}'
|
|
55
|
+
}
|
|
56
|
+
}, { immediate: true, deep: true })
|
|
57
|
+
|
|
58
|
+
// Computed: sorted keys with "content" first if present
|
|
59
|
+
const sortedKeys = computed(() => {
|
|
60
|
+
const keys = Object.keys(localData.value || {})
|
|
61
|
+
// Put 'content' first, then sort rest alphabetically
|
|
62
|
+
return keys.sort((a, b) => {
|
|
63
|
+
if (a === 'content') return -1
|
|
64
|
+
if (b === 'content') return 1
|
|
65
|
+
return a.localeCompare(b)
|
|
66
|
+
})
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
function isExpanded(key) {
|
|
70
|
+
return expandedSections.value.has(key)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function toggleSection(key) {
|
|
74
|
+
if (expandedSections.value.has(key)) {
|
|
75
|
+
expandedSections.value.delete(key)
|
|
76
|
+
} else {
|
|
77
|
+
expandedSections.value.add(key)
|
|
78
|
+
}
|
|
79
|
+
// Trigger reactivity
|
|
80
|
+
expandedSections.value = new Set(expandedSections.value)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function expandAll() {
|
|
84
|
+
expandedSections.value = new Set(sortedKeys.value)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function collapseAll() {
|
|
88
|
+
expandedSections.value = new Set()
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Update a specific field
|
|
92
|
+
function updateField(key, newValue) {
|
|
93
|
+
const type = getJsonValueType(localData.value[key])
|
|
94
|
+
|
|
95
|
+
if (type === 'string') {
|
|
96
|
+
localData.value[key] = newValue
|
|
97
|
+
} else if (type === 'number') {
|
|
98
|
+
localData.value[key] = parseFloat(newValue) || 0
|
|
99
|
+
} else if (type === 'boolean') {
|
|
100
|
+
localData.value[key] = newValue === 'true' || newValue === true
|
|
101
|
+
} else if (type === 'array' || type === 'object') {
|
|
102
|
+
// Parse as JSON for complex types
|
|
103
|
+
try {
|
|
104
|
+
localData.value[key] = JSON.parse(newValue)
|
|
105
|
+
} catch {
|
|
106
|
+
// Invalid JSON, don't update
|
|
107
|
+
return
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
emitUpdate()
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function emitUpdate() {
|
|
115
|
+
emit('update:modelValue', JSON.parse(JSON.stringify(localData.value)))
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Raw JSON mode handling
|
|
119
|
+
function toggleRawMode() {
|
|
120
|
+
if (rawJsonMode.value) {
|
|
121
|
+
// Switching from raw to structured - parse the raw JSON
|
|
122
|
+
try {
|
|
123
|
+
localData.value = JSON.parse(rawJsonText.value)
|
|
124
|
+
parseError.value = null
|
|
125
|
+
emitUpdate()
|
|
126
|
+
} catch (e) {
|
|
127
|
+
parseError.value = e.message
|
|
128
|
+
return // Don't switch if invalid
|
|
129
|
+
}
|
|
130
|
+
} else {
|
|
131
|
+
// Switching to raw mode - serialize current data
|
|
132
|
+
rawJsonText.value = JSON.stringify(localData.value, null, 2)
|
|
133
|
+
}
|
|
134
|
+
rawJsonMode.value = !rawJsonMode.value
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function onRawJsonInput(event) {
|
|
138
|
+
rawJsonText.value = event.target.value
|
|
139
|
+
try {
|
|
140
|
+
JSON.parse(rawJsonText.value)
|
|
141
|
+
parseError.value = null
|
|
142
|
+
} catch (e) {
|
|
143
|
+
parseError.value = e.message
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function saveRawJson() {
|
|
148
|
+
try {
|
|
149
|
+
localData.value = JSON.parse(rawJsonText.value)
|
|
150
|
+
parseError.value = null
|
|
151
|
+
emitUpdate()
|
|
152
|
+
} catch (e) {
|
|
153
|
+
parseError.value = e.message
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Get appropriate editor for value type
|
|
158
|
+
function isMultiline(value) {
|
|
159
|
+
if (typeof value !== 'string') return false
|
|
160
|
+
return value.length > 100 || value.includes('\n')
|
|
161
|
+
}
|
|
162
|
+
</script>
|
|
163
|
+
|
|
164
|
+
<template>
|
|
165
|
+
<div class="json-editor-foldable">
|
|
166
|
+
<!-- Toolbar -->
|
|
167
|
+
<div class="editor-toolbar">
|
|
168
|
+
<div class="toolbar-left">
|
|
169
|
+
<Button
|
|
170
|
+
:icon="rawJsonMode ? 'pi pi-list' : 'pi pi-code'"
|
|
171
|
+
:label="rawJsonMode ? 'Structured' : 'Raw JSON'"
|
|
172
|
+
size="small"
|
|
173
|
+
severity="secondary"
|
|
174
|
+
text
|
|
175
|
+
@click="toggleRawMode"
|
|
176
|
+
/>
|
|
177
|
+
</div>
|
|
178
|
+
<div class="toolbar-right" v-if="!rawJsonMode">
|
|
179
|
+
<Button
|
|
180
|
+
icon="pi pi-plus"
|
|
181
|
+
label="Expand All"
|
|
182
|
+
size="small"
|
|
183
|
+
severity="secondary"
|
|
184
|
+
text
|
|
185
|
+
@click="expandAll"
|
|
186
|
+
/>
|
|
187
|
+
<Button
|
|
188
|
+
icon="pi pi-minus"
|
|
189
|
+
label="Collapse All"
|
|
190
|
+
size="small"
|
|
191
|
+
severity="secondary"
|
|
192
|
+
text
|
|
193
|
+
@click="collapseAll"
|
|
194
|
+
/>
|
|
195
|
+
</div>
|
|
196
|
+
</div>
|
|
197
|
+
|
|
198
|
+
<!-- Structured Mode -->
|
|
199
|
+
<div v-if="!rawJsonMode" class="sections-container" :style="{ maxHeight: height }">
|
|
200
|
+
<div
|
|
201
|
+
v-for="key in sortedKeys"
|
|
202
|
+
:key="key"
|
|
203
|
+
class="section"
|
|
204
|
+
:class="{ expanded: isExpanded(key) }"
|
|
205
|
+
>
|
|
206
|
+
<div class="section-header" @click="toggleSection(key)">
|
|
207
|
+
<i
|
|
208
|
+
class="pi"
|
|
209
|
+
:class="isExpanded(key) ? 'pi-chevron-down' : 'pi-chevron-right'"
|
|
210
|
+
></i>
|
|
211
|
+
<span class="section-key">{{ key }}</span>
|
|
212
|
+
<span class="section-type">{{ getJsonValueType(localData[key]) }}</span>
|
|
213
|
+
<span v-if="!isExpanded(key)" class="section-preview">{{ getJsonPreview(localData[key]) }}</span>
|
|
214
|
+
</div>
|
|
215
|
+
|
|
216
|
+
<div v-if="isExpanded(key)" class="section-content">
|
|
217
|
+
<!-- String (multiline) -->
|
|
218
|
+
<Textarea
|
|
219
|
+
v-if="getJsonValueType(localData[key]) === 'string' && isMultiline(localData[key])"
|
|
220
|
+
:modelValue="localData[key]"
|
|
221
|
+
@update:modelValue="updateField(key, $event)"
|
|
222
|
+
rows="8"
|
|
223
|
+
class="w-full field-textarea"
|
|
224
|
+
autoResize
|
|
225
|
+
/>
|
|
226
|
+
|
|
227
|
+
<!-- String (single line) -->
|
|
228
|
+
<InputText
|
|
229
|
+
v-else-if="getJsonValueType(localData[key]) === 'string'"
|
|
230
|
+
:modelValue="localData[key]"
|
|
231
|
+
@update:modelValue="updateField(key, $event)"
|
|
232
|
+
class="w-full"
|
|
233
|
+
/>
|
|
234
|
+
|
|
235
|
+
<!-- Number -->
|
|
236
|
+
<InputText
|
|
237
|
+
v-else-if="getJsonValueType(localData[key]) === 'number'"
|
|
238
|
+
type="number"
|
|
239
|
+
:modelValue="String(localData[key])"
|
|
240
|
+
@update:modelValue="updateField(key, $event)"
|
|
241
|
+
class="w-full"
|
|
242
|
+
/>
|
|
243
|
+
|
|
244
|
+
<!-- Boolean -->
|
|
245
|
+
<select
|
|
246
|
+
v-else-if="getJsonValueType(localData[key]) === 'boolean'"
|
|
247
|
+
:value="String(localData[key])"
|
|
248
|
+
@change="updateField(key, $event.target.value)"
|
|
249
|
+
class="field-select"
|
|
250
|
+
>
|
|
251
|
+
<option value="true">true</option>
|
|
252
|
+
<option value="false">false</option>
|
|
253
|
+
</select>
|
|
254
|
+
|
|
255
|
+
<!-- Array or Object - show as JSON -->
|
|
256
|
+
<Textarea
|
|
257
|
+
v-else-if="getJsonValueType(localData[key]) === 'array' || getJsonValueType(localData[key]) === 'object'"
|
|
258
|
+
:modelValue="JSON.stringify(localData[key], null, 2)"
|
|
259
|
+
@update:modelValue="updateField(key, $event)"
|
|
260
|
+
rows="6"
|
|
261
|
+
class="w-full field-json"
|
|
262
|
+
autoResize
|
|
263
|
+
/>
|
|
264
|
+
|
|
265
|
+
<!-- Null -->
|
|
266
|
+
<span v-else-if="getJsonValueType(localData[key]) === 'null'" class="null-value">null</span>
|
|
267
|
+
</div>
|
|
268
|
+
</div>
|
|
269
|
+
|
|
270
|
+
<div v-if="sortedKeys.length === 0" class="empty-state">
|
|
271
|
+
<i class="pi pi-inbox"></i>
|
|
272
|
+
<span>No data</span>
|
|
273
|
+
</div>
|
|
274
|
+
</div>
|
|
275
|
+
|
|
276
|
+
<!-- Raw JSON Mode -->
|
|
277
|
+
<div v-else class="raw-json-container" :style="{ height }">
|
|
278
|
+
<textarea
|
|
279
|
+
class="raw-json-textarea"
|
|
280
|
+
:class="{ 'has-error': parseError }"
|
|
281
|
+
:value="rawJsonText"
|
|
282
|
+
@input="onRawJsonInput"
|
|
283
|
+
spellcheck="false"
|
|
284
|
+
></textarea>
|
|
285
|
+
<div v-if="parseError" class="raw-json-actions">
|
|
286
|
+
<Message severity="error" :closable="false" class="parse-error">
|
|
287
|
+
{{ parseError }}
|
|
288
|
+
</Message>
|
|
289
|
+
</div>
|
|
290
|
+
<div v-else class="raw-json-actions">
|
|
291
|
+
<Button
|
|
292
|
+
label="Apply Changes"
|
|
293
|
+
icon="pi pi-check"
|
|
294
|
+
size="small"
|
|
295
|
+
@click="saveRawJson"
|
|
296
|
+
/>
|
|
297
|
+
</div>
|
|
298
|
+
</div>
|
|
299
|
+
</div>
|
|
300
|
+
</template>
|
|
301
|
+
|
|
302
|
+
<style scoped>
|
|
303
|
+
.json-editor-foldable {
|
|
304
|
+
display: flex;
|
|
305
|
+
flex-direction: column;
|
|
306
|
+
border: 1px solid var(--p-surface-300);
|
|
307
|
+
border-radius: 0.375rem;
|
|
308
|
+
background: var(--p-surface-0);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
.editor-toolbar {
|
|
312
|
+
display: flex;
|
|
313
|
+
justify-content: space-between;
|
|
314
|
+
align-items: center;
|
|
315
|
+
padding: 0.5rem;
|
|
316
|
+
border-bottom: 1px solid var(--p-surface-200);
|
|
317
|
+
background: var(--p-surface-50);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
.toolbar-left,
|
|
321
|
+
.toolbar-right {
|
|
322
|
+
display: flex;
|
|
323
|
+
gap: 0.25rem;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
.sections-container {
|
|
327
|
+
overflow-y: auto;
|
|
328
|
+
padding: 0.5rem;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
.section {
|
|
332
|
+
border: 1px solid var(--p-surface-200);
|
|
333
|
+
border-radius: 0.375rem;
|
|
334
|
+
margin-bottom: 0.5rem;
|
|
335
|
+
background: var(--p-surface-0);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
.section:last-child {
|
|
339
|
+
margin-bottom: 0;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
.section-header {
|
|
343
|
+
display: flex;
|
|
344
|
+
align-items: center;
|
|
345
|
+
gap: 0.5rem;
|
|
346
|
+
padding: 0.75rem;
|
|
347
|
+
cursor: pointer;
|
|
348
|
+
user-select: none;
|
|
349
|
+
background: var(--p-surface-50);
|
|
350
|
+
border-radius: 0.375rem;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
.section.expanded .section-header {
|
|
354
|
+
border-bottom: 1px solid var(--p-surface-200);
|
|
355
|
+
border-radius: 0.375rem 0.375rem 0 0;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
.section-header:hover {
|
|
359
|
+
background: var(--p-surface-100);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
.section-header .pi {
|
|
363
|
+
font-size: 0.75rem;
|
|
364
|
+
color: var(--p-surface-500);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
.section-key {
|
|
368
|
+
font-weight: 600;
|
|
369
|
+
color: var(--p-primary-700);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
.section-type {
|
|
373
|
+
font-size: 0.75rem;
|
|
374
|
+
color: var(--p-surface-400);
|
|
375
|
+
padding: 0.125rem 0.375rem;
|
|
376
|
+
background: var(--p-surface-100);
|
|
377
|
+
border-radius: 0.25rem;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
.section-preview {
|
|
381
|
+
flex: 1;
|
|
382
|
+
font-size: 0.8125rem;
|
|
383
|
+
color: var(--p-surface-500);
|
|
384
|
+
overflow: hidden;
|
|
385
|
+
text-overflow: ellipsis;
|
|
386
|
+
white-space: nowrap;
|
|
387
|
+
text-align: right;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
.section-content {
|
|
391
|
+
padding: 0.75rem;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
.field-textarea,
|
|
395
|
+
.field-json {
|
|
396
|
+
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
|
397
|
+
font-size: 0.8125rem;
|
|
398
|
+
line-height: 1.5;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
.field-json {
|
|
402
|
+
background: var(--p-surface-50);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
.field-select {
|
|
406
|
+
padding: 0.5rem;
|
|
407
|
+
border: 1px solid var(--p-surface-300);
|
|
408
|
+
border-radius: 0.375rem;
|
|
409
|
+
background: var(--p-surface-0);
|
|
410
|
+
font-size: 0.875rem;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
.null-value {
|
|
414
|
+
color: var(--p-surface-400);
|
|
415
|
+
font-style: italic;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
.empty-state {
|
|
419
|
+
display: flex;
|
|
420
|
+
flex-direction: column;
|
|
421
|
+
align-items: center;
|
|
422
|
+
justify-content: center;
|
|
423
|
+
padding: 2rem;
|
|
424
|
+
color: var(--p-surface-400);
|
|
425
|
+
gap: 0.5rem;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
.empty-state .pi {
|
|
429
|
+
font-size: 2rem;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/* Raw JSON mode */
|
|
433
|
+
.raw-json-container {
|
|
434
|
+
display: flex;
|
|
435
|
+
flex-direction: column;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
.raw-json-textarea {
|
|
439
|
+
flex: 1;
|
|
440
|
+
padding: 1rem;
|
|
441
|
+
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
|
442
|
+
font-size: 0.8125rem;
|
|
443
|
+
line-height: 1.5;
|
|
444
|
+
border: none;
|
|
445
|
+
outline: none;
|
|
446
|
+
resize: none;
|
|
447
|
+
background: var(--p-surface-50);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
.raw-json-textarea.has-error {
|
|
451
|
+
background: var(--p-red-50);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
.raw-json-actions {
|
|
455
|
+
padding: 0.5rem;
|
|
456
|
+
border-top: 1px solid var(--p-surface-200);
|
|
457
|
+
background: var(--p-surface-50);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
.parse-error {
|
|
461
|
+
margin: 0;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
.w-full {
|
|
465
|
+
width: 100%;
|
|
466
|
+
}
|
|
467
|
+
</style>
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
/**
|
|
3
|
+
* JsonStructuredField - Reusable field with structured/JSON toggle
|
|
4
|
+
*
|
|
5
|
+
* A form field that shows either a structured view (slot) or raw JSON editor.
|
|
6
|
+
* Uses a mini SelectButton to toggle between views.
|
|
7
|
+
*
|
|
8
|
+
* Usage patterns:
|
|
9
|
+
*
|
|
10
|
+
* 1. Simple - same v-model for both views:
|
|
11
|
+
* <JsonStructuredField v-model="myData">
|
|
12
|
+
* <MyStructuredEditor v-model="myData" />
|
|
13
|
+
* </JsonStructuredField>
|
|
14
|
+
*
|
|
15
|
+
* 2. With separate JSON binding (for complex computed mappings):
|
|
16
|
+
* <JsonStructuredField v-model="myData" :jsonValue="jsonComputed" @update:jsonValue="onJsonUpdate">
|
|
17
|
+
* <MyStructuredEditor v-model="myData" />
|
|
18
|
+
* </JsonStructuredField>
|
|
19
|
+
*
|
|
20
|
+
* 3. Controlled mode (parent manages viewMode):
|
|
21
|
+
* <JsonStructuredField v-model="myData" v-model:mode="viewMode">
|
|
22
|
+
* <MyStructuredEditor v-model="myData" />
|
|
23
|
+
* </JsonStructuredField>
|
|
24
|
+
*
|
|
25
|
+
* Props:
|
|
26
|
+
* - modelValue: The JSON object to edit (used for JSON view if jsonValue not provided)
|
|
27
|
+
* - jsonValue: Optional separate binding for JSON editor (for complex computed mappings)
|
|
28
|
+
* - mode: Optional v-model for view mode ('structured' or 'json')
|
|
29
|
+
* - label: Optional label for the field
|
|
30
|
+
* - jsonHeight: Height of JSON editor (default: 400px)
|
|
31
|
+
* - jsonMode: Mode for JSON editor - 'tree' or 'text' (default: 'text')
|
|
32
|
+
* - defaultMode: Initial mode if not using v-model:mode (default: 'structured')
|
|
33
|
+
* - showToggle: Whether to show the toggle (default: true, useful to hide in nested usage)
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
import { ref, computed, watch } from 'vue'
|
|
37
|
+
import SelectButton from 'primevue/selectbutton'
|
|
38
|
+
import VanillaJsonEditor from './VanillaJsonEditor.vue'
|
|
39
|
+
|
|
40
|
+
const props = defineProps({
|
|
41
|
+
modelValue: {
|
|
42
|
+
type: [Object, Array],
|
|
43
|
+
default: () => ({})
|
|
44
|
+
},
|
|
45
|
+
jsonValue: {
|
|
46
|
+
type: [Object, Array],
|
|
47
|
+
default: null
|
|
48
|
+
},
|
|
49
|
+
mode: {
|
|
50
|
+
type: String,
|
|
51
|
+
default: null
|
|
52
|
+
},
|
|
53
|
+
label: {
|
|
54
|
+
type: String,
|
|
55
|
+
default: null
|
|
56
|
+
},
|
|
57
|
+
jsonHeight: {
|
|
58
|
+
type: String,
|
|
59
|
+
default: '400px'
|
|
60
|
+
},
|
|
61
|
+
jsonMode: {
|
|
62
|
+
type: String,
|
|
63
|
+
default: 'text',
|
|
64
|
+
validator: (v) => ['tree', 'text'].includes(v)
|
|
65
|
+
},
|
|
66
|
+
defaultMode: {
|
|
67
|
+
type: String,
|
|
68
|
+
default: 'structured',
|
|
69
|
+
validator: (v) => ['structured', 'json'].includes(v)
|
|
70
|
+
},
|
|
71
|
+
showToggle: {
|
|
72
|
+
type: Boolean,
|
|
73
|
+
default: true
|
|
74
|
+
}
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
const emit = defineEmits(['update:modelValue', 'update:jsonValue', 'update:mode'])
|
|
78
|
+
|
|
79
|
+
// Internal view mode (used when not controlled externally)
|
|
80
|
+
const internalMode = ref(props.defaultMode)
|
|
81
|
+
|
|
82
|
+
// Computed view mode - use external if provided, else internal
|
|
83
|
+
const viewMode = computed({
|
|
84
|
+
get: () => props.mode ?? internalMode.value,
|
|
85
|
+
set: (val) => {
|
|
86
|
+
if (props.mode !== null) {
|
|
87
|
+
emit('update:mode', val)
|
|
88
|
+
} else {
|
|
89
|
+
internalMode.value = val
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
const viewModeOptions = [
|
|
95
|
+
{ label: 'Structured', value: 'structured', icon: 'pi pi-list' },
|
|
96
|
+
{ label: 'JSON', value: 'json', icon: 'pi pi-code' }
|
|
97
|
+
]
|
|
98
|
+
|
|
99
|
+
// Computed JSON value - use separate jsonValue if provided, else modelValue
|
|
100
|
+
const effectiveJsonValue = computed(() => {
|
|
101
|
+
return props.jsonValue !== null ? props.jsonValue : props.modelValue
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
// Handle JSON editor updates
|
|
105
|
+
function onJsonUpdate(newValue) {
|
|
106
|
+
if (props.jsonValue !== null) {
|
|
107
|
+
// Using separate jsonValue binding
|
|
108
|
+
emit('update:jsonValue', newValue)
|
|
109
|
+
} else {
|
|
110
|
+
// Using modelValue for both
|
|
111
|
+
emit('update:modelValue', newValue)
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Forward modelValue updates (for structured view)
|
|
116
|
+
// eslint-disable-next-line no-unused-vars
|
|
117
|
+
function emitUpdate(newValue) {
|
|
118
|
+
emit('update:modelValue', newValue)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Sync internal mode with prop if it changes
|
|
122
|
+
watch(() => props.mode, (newMode) => {
|
|
123
|
+
if (newMode !== null) {
|
|
124
|
+
internalMode.value = newMode
|
|
125
|
+
}
|
|
126
|
+
})
|
|
127
|
+
</script>
|
|
128
|
+
|
|
129
|
+
<template>
|
|
130
|
+
<div class="json-structured-field">
|
|
131
|
+
<!-- Header with label and mode toggle -->
|
|
132
|
+
<div v-if="showToggle || label" class="field-header">
|
|
133
|
+
<label v-if="label" class="field-label">{{ label }}</label>
|
|
134
|
+
<SelectButton
|
|
135
|
+
v-if="showToggle"
|
|
136
|
+
v-model="viewMode"
|
|
137
|
+
:options="viewModeOptions"
|
|
138
|
+
optionLabel="label"
|
|
139
|
+
optionValue="value"
|
|
140
|
+
:allowEmpty="false"
|
|
141
|
+
class="mode-toggle"
|
|
142
|
+
>
|
|
143
|
+
<template #option="{ option }">
|
|
144
|
+
<i :class="option.icon"></i>
|
|
145
|
+
<span class="toggle-label">{{ option.label }}</span>
|
|
146
|
+
</template>
|
|
147
|
+
</SelectButton>
|
|
148
|
+
</div>
|
|
149
|
+
|
|
150
|
+
<!-- JSON View -->
|
|
151
|
+
<div v-if="viewMode === 'json'" class="json-view">
|
|
152
|
+
<VanillaJsonEditor
|
|
153
|
+
:modelValue="effectiveJsonValue"
|
|
154
|
+
@update:modelValue="onJsonUpdate"
|
|
155
|
+
:mode="jsonMode"
|
|
156
|
+
:height="jsonHeight"
|
|
157
|
+
/>
|
|
158
|
+
</div>
|
|
159
|
+
|
|
160
|
+
<!-- Structured View (slot) -->
|
|
161
|
+
<div v-else class="structured-view">
|
|
162
|
+
<slot></slot>
|
|
163
|
+
</div>
|
|
164
|
+
</div>
|
|
165
|
+
</template>
|
|
166
|
+
|
|
167
|
+
<style scoped>
|
|
168
|
+
.json-structured-field {
|
|
169
|
+
width: 100%;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
.field-header {
|
|
173
|
+
display: flex;
|
|
174
|
+
justify-content: space-between;
|
|
175
|
+
align-items: center;
|
|
176
|
+
margin-bottom: 0.75rem;
|
|
177
|
+
flex-wrap: wrap;
|
|
178
|
+
gap: 0.5rem;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
.field-label {
|
|
182
|
+
font-weight: 600;
|
|
183
|
+
font-size: 0.95rem;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
.mode-toggle {
|
|
187
|
+
flex-shrink: 0;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/* Smaller toggle buttons */
|
|
191
|
+
.mode-toggle :deep(.p-button) {
|
|
192
|
+
padding: 0.35rem 0.6rem;
|
|
193
|
+
font-size: 0.8rem;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
.mode-toggle :deep(.p-button i) {
|
|
197
|
+
font-size: 0.75rem;
|
|
198
|
+
margin-right: 0.25rem;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
.toggle-label {
|
|
202
|
+
display: none;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
@media (min-width: 640px) {
|
|
206
|
+
.toggle-label {
|
|
207
|
+
display: inline;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
.json-view {
|
|
212
|
+
margin-top: 0.25rem;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
.structured-view {
|
|
216
|
+
margin-top: 0.25rem;
|
|
217
|
+
}
|
|
218
|
+
</style>
|