From 465601141a296ddd5d4117821d2cf20e30e28011 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 26 Aug 2026 16:17:41 +0000 Subject: [PATCH] Use vue-json-pretty for collapsible JSON views, collapse body by default Replace the raw
JSON.stringify()
dumps in AdminView and EmailSignalCard's signal-object modal with a shared JsonView component built on vue-json-pretty. Attributes named in collapsedKeys (defaults to ["body"]) collapse automatically via pathCollapsible for objects/arrays and a custom renderNodeValue truncation for long strings, instead of hand-rolling tree collapse state. --- package-lock.json | 16 ++++- package.json | 1 + src/components/EmailSignalCard.vue | 10 +-- src/components/ui/JsonView.vue | 105 +++++++++++++++++++++++++++++ src/views/AdminView.vue | 5 +- 5 files changed, 130 insertions(+), 7 deletions(-) create mode 100644 src/components/ui/JsonView.vue diff --git a/package-lock.json b/package-lock.json index 018726b..4c10e79 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26,6 +26,7 @@ "textarea-caret": "^3.1.0", "vue": "^3.5.13", "vue-echarts": "^8.0.1", + "vue-json-pretty": "^2.6.0", "vue-router": "^4.6.4" }, "devDependencies": { @@ -15492,7 +15493,7 @@ "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -17173,6 +17174,19 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/vue-json-pretty": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/vue-json-pretty/-/vue-json-pretty-2.6.0.tgz", + "integrity": "sha512-glz1aBVS35EO8+S9agIl3WOQaW2cJZW192UVKTuGmryx01ZvOVWc4pR3t+5UcyY4jdOfBUgVHjcpRpcnjRhCAg==", + "license": "MIT", + "engines": { + "node": ">= 10.0.0", + "npm": ">= 5.0.0" + }, + "peerDependencies": { + "vue": ">=3.0.0" + } + }, "node_modules/vue-router": { "version": "4.6.4", "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.6.4.tgz", diff --git a/package.json b/package.json index f4b3240..8439e54 100644 --- a/package.json +++ b/package.json @@ -43,6 +43,7 @@ "textarea-caret": "^3.1.0", "vue": "^3.5.13", "vue-echarts": "^8.0.1", + "vue-json-pretty": "^2.6.0", "vue-router": "^4.6.4" }, "devDependencies": { diff --git a/src/components/EmailSignalCard.vue b/src/components/EmailSignalCard.vue index 47b5e6a..7d4ef9e 100644 --- a/src/components/EmailSignalCard.vue +++ b/src/components/EmailSignalCard.vue @@ -11,6 +11,7 @@ import { api } from '@/lib/api' import ActionBadge from '@/components/ActionBadge.vue' import CopyMenuItem from '@/components/CopyMenuItem.vue' import OverflowMenu from '@/components/ui/OverflowMenu.vue' +import JsonView from '@/components/ui/JsonView.vue' const props = withDefaults(defineProps<{ signal: Signal; defaultExpanded?: boolean }>(), { defaultExpanded: true }) const emit = defineEmits<{ reply: []; reprocessed: [] }>() @@ -126,7 +127,8 @@ const subjectLine = computed(() => { return props.signal.data.subject }) -const signalObjectJson = ref('') +const signalObjectData = ref(null) +const signalObjectJson = computed(() => (signalObjectData.value ? JSON.stringify(signalObjectData.value, null, 2) : '')) const showSignalObjectModal = ref(false) const showOriginalModal = ref(false) const showMatchedRulesModal = ref(false) @@ -148,14 +150,14 @@ function viewSignalObject() { showSignalObjectModal.value = true signalObjectError.value = null - if (signalObjectJson.value) return + if (signalObjectData.value) return const threadId = props.signal.threadId ?? (props.signal.status === 'block_hidden' || props.signal.status === 'block_reject' ? 'BLOCKED' : 'QUARANTINED') signalObjectLoading.value = true void api.getSignal(accountStore.accountId, threadId, props.signal.signalId).then((result) => { signalObjectLoading.value = false if (result.isOk()) { - signalObjectJson.value = JSON.stringify(result.value, null, 2) + signalObjectData.value = result.value } else { signalObjectError.value = result.error.message } @@ -535,7 +537,7 @@ const iframeStyle = {
{{ signalObjectError }}
-
{{ signalObjectJson }}
+ diff --git a/src/components/ui/JsonView.vue b/src/components/ui/JsonView.vue new file mode 100644 index 0000000..292f176 --- /dev/null +++ b/src/components/ui/JsonView.vue @@ -0,0 +1,105 @@ + + + + + diff --git a/src/views/AdminView.vue b/src/views/AdminView.vue index c38ba31..ffc9d01 100644 --- a/src/views/AdminView.vue +++ b/src/views/AdminView.vue @@ -5,6 +5,7 @@ import { useAccountStore } from '@/stores/account' import { useLogStore } from '@/stores/logs' import { getUndoExpiresAt } from '@/composables/usePendingSend' import AsyncButton from '@/components/ui/AsyncButton.vue' +import JsonView from '@/components/ui/JsonView.vue' import BuildInfo from '@/components/BuildInfo.vue' import type { Thread, Signal, HealthCheckValidation, HealthCheckStatus } from '@/types/server' @@ -319,7 +320,7 @@ async function lookup() {

Thread

-
{{ JSON.stringify(displayedThread, null, 2) }}
+

Thread

@@ -364,7 +365,7 @@ async function lookup() { -
{{ JSON.stringify(displayedSignal, null, 2) }}
+