RSS Git Download  Clone
Raw Blame History 4kB 116 lines
<script setup lang="ts">
/**
 * JsonViewDialog — exact port of React JsonViewDialog.tsx
 * Interactive JSON tree viewer with expand/collapse and syntax colors.
 */
import { ref, computed, watch } from 'vue'
import { storeToRefs } from 'pinia'
import { useI18nStore } from '../stores/i18n'
import { useThemeStore } from '../stores/theme'
import P3xrDialog from '../components/P3xrDialog.vue'
import JsonTreeNode, { type JsonNode } from '../components/JsonTreeNode.vue'

function jsonToNode(key: string, value: any): JsonNode {
    if (value === null) return { key, value: null, type: 'null' }
    if (Array.isArray(value)) {
        const children = value.map((item, i) => jsonToNode(String(i), item))
        return { key, value, type: 'array', children, childCount: children.length }
    }
    if (typeof value === 'object') {
        const children = Object.keys(value).map(k => jsonToNode(k, value[k]))
        return { key, value, type: 'object', children, childCount: children.length }
    }
    return { key, value, type: typeof value as any }
}

const props = defineProps<{
    open: boolean
    value: string
}>()

const emit = defineEmits<{ close: [] }>()

const i18n = useI18nStore()
const strings = computed(() => i18n.strings)
const { themeKey } = storeToRefs(useThemeStore())
const isDark = computed(() => ['dark', 'darkNeu', 'darkoBluo', 'matrix'].includes(themeKey.value))

const expandedKeys = ref<Set<string>>(new Set())

const rootLabel = computed(() => strings.value?.label?.tree ?? 'root')

const parseResult = computed(() => {
    try {
        const obj = JSON.parse(props.value)
        return { isJson: true, tree: jsonToNode(rootLabel.value, obj) }
    } catch {
        return { isJson: false, tree: null }
    }
})

// Reset: expand root (level 0) on open — matches Angular expanded=true (first level)
watch(() => props.open, (v) => {
    if (v && parseResult.value.isJson) {
        expandedKeys.value = new Set([`0-${rootLabel.value}`])
    }
})

function toggleExpand(path: string) {
    const s = new Set(expandedKeys.value)
    s.has(path) ? s.delete(path) : s.add(path)
    expandedKeys.value = s
}

function expandAll() {
    if (!parseResult.value.tree) return
    const keys = new Set<string>()
    const collect = (node: JsonNode, level: number) => {
        const path = `${level}-${node.key}`
        if (node.type === 'object' || node.type === 'array') {
            keys.add(path)
            node.children?.forEach(c => collect(c, level + 1))
        }
    }
    collect(parseResult.value.tree, 0)
    expandedKeys.value = keys
}

function collapseAll() {
    expandedKeys.value = new Set([`0-${rootLabel.value}`])
}
</script>

<template>
    <P3xrDialog v-if="open" :open="true" :title="strings?.intention?.jsonViewShow" @close="emit('close')">
        <template #headerActions>
            <v-tooltip :text="strings?.page?.treeControls?.expandAll" location="top">
                <template #activator="{ props: tp }">
                    <v-btn v-bind="tp" icon variant="text" color="inherit" size="small" @click="expandAll">
                        <v-icon size="small">mdi-chevron-down</v-icon>
                    </v-btn>
                </template>
            </v-tooltip>
            <v-tooltip :text="strings?.page?.treeControls?.collapseAll" location="top">
                <template #activator="{ props: tp }">
                    <v-btn v-bind="tp" icon variant="text" color="inherit" size="small" @click="collapseAll">
                        <v-icon size="small">mdi-chevron-up</v-icon>
                    </v-btn>
                </template>
            </v-tooltip>
        </template>

        <div v-if="parseResult.isJson && parseResult.tree" style="min-height: 200px; max-height: 70vh; overflow: auto;">
            <JsonTreeNode :node="parseResult.tree" :level="0" :expanded-keys="expandedKeys" :is-dark="isDark"
                @toggle="toggleExpand" />
        </div>
        <div v-else>{{ strings?.label?.jsonViewNotParsable }}</div>

        <template #actions>
            <v-btn variant="flat" color="secondary" size="small" @click="emit('close')">
                <v-icon class="mr-1">mdi-close</v-icon>
                <span>{{ strings?.intention?.close }}</span>
            </v-btn>
        </template>
    </P3xrDialog>
</template>