Skip to content

Commit 365629d

Browse files
committed
feat(prototype): clean struct rendering + query-this-key actions
- display transform strips $class/$type meta before rendering: class instances show only real props, function stubs collapse to {}, Map/Set wrappers unwrap to their entries/values, Date/RegExp/BigInt/... render as bare values; badges carry the type info instead, via WeakMap side-tables (object identity for objects, parent+key for primitives) - discovery's built-in query actions are enabled: the struct value-actions popup now offers 'Create a subquery from the path' (pipes the path onto the query) and 'Append path to current query' (textual append), both emitting up to set the editor and re-run
1 parent 8007183 commit 365629d

4 files changed

Lines changed: 220 additions & 31 deletions

File tree

examples/prototype-data-inspector/src/spa/App.vue

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,20 @@ function queryProp(key: string): void {
5757
wb.query.value = /^[a-z_$][\w$]*$/i.test(key) ? key : `$["${key.replaceAll('"', '\\"')}"]`
5858
void wb.runNow()
5959
}
60+
61+
/** "Create a subquery from the path": pipe the path onto the current query. */
62+
function querySubquery(path: string): void {
63+
const current = wb.query.value.trim()
64+
wb.query.value = current && current !== '$' ? `${current}\n| ${path}` : path
65+
void wb.runNow()
66+
}
67+
68+
/** "Append path to current query": plain textual append. */
69+
function queryAppend(path: string): void {
70+
const current = wb.query.value.trim()
71+
wb.query.value = current ? `${current}${path.startsWith('[') ? '' : '.'}${path}` : path
72+
void wb.runNow()
73+
}
6074
</script>
6175

6276
<template>
@@ -171,6 +185,8 @@ function queryProp(key: string): void {
171185
:error="wb.serverError.value"
172186
:running="wb.running.value"
173187
@rerun="wb.runNow()"
188+
@query-subquery="querySubquery"
189+
@query-append="queryAppend"
174190
/>
175191
</Pane>
176192
</LayoutSplitPane>

examples/prototype-data-inspector/src/spa/components/ResultViewer.vue

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import DisplayBytes from '@antfu/design/components/Display/DisplayBytes.vue'
66
import DisplayDuration from '@antfu/design/components/Display/DisplayDuration.vue'
77
import { shallowRef, watch } from 'vue'
88
import { useDiscoveryViewer } from '../composables/discovery'
9+
import { prepareForDisplay } from '../composables/display-transform'
910
import { colorScheme } from '../composables/scheme'
1011
1112
const props = defineProps<{
@@ -17,14 +18,23 @@ const props = defineProps<{
1718
running: boolean
1819
}>()
1920
20-
const emit = defineEmits<{ rerun: [] }>()
21+
const emit = defineEmits<{
22+
rerun: []
23+
/** From the struct's value actions: replace the query with this jora path. */
24+
querySubquery: [path: string]
25+
/** From the struct's value actions: append this jora path to the query. */
26+
queryAppend: [path: string]
27+
}>()
2128
2229
const containerEl = shallowRef<HTMLElement | null>(null)
23-
const viewer = useDiscoveryViewer(containerEl, colorScheme)
30+
const viewer = useDiscoveryViewer(containerEl, colorScheme, { view: 'struct', expanded: 2 }, {
31+
onQuerySubquery: path => emit('querySubquery', path),
32+
onQueryAppend: path => emit('queryAppend', path),
33+
})
2434
2535
watch(() => props.result, (value) => {
2636
if (props.hasResult)
27-
void viewer.setData(value)
37+
void viewer.setData(prepareForDisplay(value))
2838
})
2939
</script>
3040

examples/prototype-data-inspector/src/spa/composables/discovery.ts

Lines changed: 43 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import type { ColorScheme } from './scheme'
1010
import { ViewModel } from '@discoveryjs/discovery'
1111
import discoveryCss from '@discoveryjs/discovery/dist/discovery.css?inline'
1212
import { onMounted, onUnmounted, shallowRef, watch } from 'vue'
13+
import { keyBadges, objectBadges } from './display-transform'
1314

1415
// Bridge discovery's theme custom props to the design tokens (see style.css
1516
// for the `.di-result-host` values that flip with `.dark`), zero out the
@@ -51,44 +52,48 @@ interface AnnotationBadge {
5152
tooltip?: unknown
5253
}
5354

54-
/** Badge the normalizer's tag objects with their type. */
55-
function typeAnnotation(value: unknown): AnnotationBadge | undefined {
56-
if (!value || typeof value !== 'object' || Array.isArray(value))
57-
return undefined
58-
const v = value as Record<string, unknown>
55+
interface AnnotationContext {
56+
host?: unknown
57+
key?: string | number
58+
}
5959

60-
if (typeof v.$ref === 'string') {
61-
return { place: 'after', style: 'badge', text: '#Circular', className: 'di-type-badge di-type-ref' }
62-
}
63-
if (typeof v.$type === 'string') {
64-
const type = v.$type
65-
const size = typeof v.size === 'number' ? `(${v.size})` : ''
66-
const kind = type === 'function'
67-
? 'di-type-function'
68-
: type === 'Map'
69-
? 'di-type-map'
70-
: type === 'Set'
71-
? 'di-type-set'
72-
: type === 'Date'
73-
? 'di-type-date'
74-
: 'di-type-other'
75-
const label = type === 'function'
76-
? (v.name && v.name !== '(anonymous)')
77-
? `fn ${v.name}`
78-
: 'Function'
79-
: `${type}${size}`
80-
return { place: 'after', style: 'badge', text: label, className: `di-type-badge ${kind}` }
60+
/**
61+
* Badge values from the display-transform side-tables (`$class`/`$type` meta
62+
* is stripped from the rendered data; badges carry the type info instead).
63+
*/
64+
function typeAnnotation(value: unknown, context?: AnnotationContext): AnnotationBadge | undefined {
65+
// Object-valued entries: identity lookup.
66+
if (value && typeof value === 'object') {
67+
const v = value as Record<string, unknown>
68+
if (typeof v.$ref === 'string')
69+
return { place: 'after', style: 'badge', text: '#Circular', className: 'di-type-badge di-type-ref' }
70+
const badge = objectBadges.get(value as object)
71+
if (badge)
72+
return { place: 'after', style: 'badge', ...badge }
73+
return undefined
8174
}
82-
if (typeof v.$class === 'string') {
83-
return { place: 'after', style: 'badge', text: `class ${v.$class}`, className: 'di-type-badge di-type-class' }
75+
// Primitive-valued entries (Date strings, BigInt, ...): parent+key lookup.
76+
const parent = context?.host
77+
if (parent && typeof parent === 'object' && context?.key !== undefined) {
78+
const badge = keyBadges.get(parent as object)?.[context.key]
79+
if (badge)
80+
return { place: 'after', style: 'badge', ...badge }
8481
}
8582
return undefined
8683
}
8784

85+
export interface DiscoveryQueryActions {
86+
/** "Create a subquery from the path" in the struct value-actions popup. */
87+
onQuerySubquery?: (path: string) => void
88+
/** "Append path to current query" in the struct value-actions popup. */
89+
onQueryAppend?: (path: string) => void
90+
}
91+
8892
export function useDiscoveryViewer(
8993
container: Readonly<ShallowRef<HTMLElement | null>>,
9094
scheme: Ref<ColorScheme>,
9195
viewConfig: Record<string, unknown> = { view: 'struct', expanded: 2 },
96+
actions: DiscoveryQueryActions = {},
9297
) {
9398
const host = shallowRef<ViewModel | null>(null)
9499
let pendingData: { data: unknown } | null = null
@@ -109,6 +114,16 @@ export function useDiscoveryViewer(
109114
...viewConfig,
110115
} as never,
111116
)
117+
// Opting into discovery's built-in query actions makes the struct view's
118+
// per-value actions popup offer "query this key" entries; the callbacks
119+
// receive a ready-made jora path (host.pathToQuery).
120+
if (actions.onQuerySubquery || actions.onQueryAppend) {
121+
vm.action.define('queryAcceptChanges', () => true)
122+
if (actions.onQuerySubquery)
123+
vm.action.define('querySubquery', path => actions.onQuerySubquery?.(String(path)))
124+
if (actions.onQueryAppend)
125+
vm.action.define('queryAppend', path => actions.onQueryAppend?.(String(path)))
126+
}
112127
await vm.dom.ready
113128
host.value = vm
114129
if (pendingData) {
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
/**
2+
* PROTOTYPE — display transform for the discovery struct view.
3+
*
4+
* The normalizer's meta tags (`$class`, `$type`, function stubs, Map/Set
5+
* wrappers) carry type info, but rendering them as plain props is noise once
6+
* badges exist. This transform rewrites a normalized result into its clean
7+
* display shape and parks the badge info in WeakMap side-tables the
8+
* annotation reads back:
9+
*
10+
* - `{ $class: 'X', ...props }` -> `{ ...props }` + `class X` badge
11+
* - `{ $type: 'function', name }` -> `{}` + `fn name` badge
12+
* - `{ $type: 'Map', size, value|entries }` -> inner object/array + `Map(n)` badge
13+
* - `{ $type: 'Set', size, values }` -> values array + `Set(n)` badge
14+
* - `{ $type: 'Date'|'RegExp'|..., value }` -> the value string + type badge (keyed by parent+key)
15+
* - `{ $ref }` / `{ $truncated }` -> untouched (informative as data)
16+
*/
17+
18+
export interface DisplayBadge {
19+
text: string
20+
className: string
21+
}
22+
23+
/** Badges for transformed values that are objects/arrays (identity lookup). */
24+
export const objectBadges = new WeakMap<object, DisplayBadge>()
25+
/** Badges for primitive-valued entries, keyed by (parent object, key). */
26+
export const keyBadges = new WeakMap<object, Record<string | number, DisplayBadge>>()
27+
28+
const KIND_BY_TYPE: Record<string, string> = {
29+
'function': 'di-type-function',
30+
'Map': 'di-type-map',
31+
'Set': 'di-type-set',
32+
'Date': 'di-type-date',
33+
'RegExp': 'di-type-date',
34+
'URL': 'di-type-date',
35+
'bigint': 'di-type-other',
36+
'symbol': 'di-type-other',
37+
'Error': 'di-type-ref',
38+
'getter-error': 'di-type-ref',
39+
'Promise': 'di-type-other',
40+
}
41+
42+
interface Walked {
43+
value: unknown
44+
badge?: DisplayBadge
45+
}
46+
47+
function badgeFor(type: string, extra?: string): DisplayBadge {
48+
return { text: extra ?? type, className: `di-type-badge ${KIND_BY_TYPE[type] ?? 'di-type-other'}` }
49+
}
50+
51+
function walk(value: unknown): Walked {
52+
if (!value || typeof value !== 'object')
53+
return { value }
54+
55+
if (Array.isArray(value)) {
56+
const out: unknown[] = Array.from({ length: value.length })
57+
const childKeyBadges: Record<number, DisplayBadge> = {}
58+
let hasKeyBadges = false
59+
value.forEach((item, i) => {
60+
const walked = walk(item)
61+
out[i] = walked.value
62+
if (walked.badge) {
63+
if (walked.value && typeof walked.value === 'object') {
64+
objectBadges.set(walked.value as object, walked.badge)
65+
}
66+
else {
67+
childKeyBadges[i] = walked.badge
68+
hasKeyBadges = true
69+
}
70+
}
71+
})
72+
if (hasKeyBadges)
73+
keyBadges.set(out, childKeyBadges)
74+
return { value: out }
75+
}
76+
77+
const obj = value as Record<string, unknown>
78+
79+
// ── normalizer stubs ────────────────────────────────────────────────
80+
if (typeof obj.$type === 'string') {
81+
const type = obj.$type
82+
switch (type) {
83+
case 'function': {
84+
const name = typeof obj.name === 'string' && obj.name !== '(anonymous)' ? obj.name : ''
85+
return { value: {}, badge: badgeFor('function', name ? `fn ${name}` : 'Function') }
86+
}
87+
case 'Map': {
88+
const inner = walk(obj.value ?? obj.entries ?? {})
89+
return { value: inner.value, badge: badgeFor('Map', `Map(${obj.size ?? '?'})`) }
90+
}
91+
case 'Set': {
92+
const inner = walk(obj.values ?? [])
93+
return { value: inner.value, badge: badgeFor('Set', `Set(${obj.size ?? '?'})`) }
94+
}
95+
case 'Date':
96+
case 'RegExp':
97+
case 'URL':
98+
case 'bigint':
99+
case 'symbol':
100+
return { value: obj.value, badge: badgeFor(type, type === 'bigint' ? 'BigInt' : type === 'symbol' ? 'Symbol' : type) }
101+
case 'Error':
102+
return { value: `${obj.name}: ${obj.message}`, badge: badgeFor('Error') }
103+
case 'getter-error':
104+
return { value: String(obj.message ?? ''), badge: badgeFor('getter-error', 'getter threw') }
105+
default:
106+
// Promise, WeakMap, TypedArray tags, ... - opaque stubs
107+
return {
108+
value: {},
109+
badge: badgeFor(type, typeof obj.length === 'number' ? `${type}(${obj.length})` : type),
110+
}
111+
}
112+
}
113+
114+
// ── plain object / class instance ───────────────────────────────────
115+
const out: Record<string, unknown> = {}
116+
const childKeyBadges: Record<string, DisplayBadge> = {}
117+
let hasKeyBadges = false
118+
let classBadge: DisplayBadge | undefined
119+
120+
for (const [key, child] of Object.entries(obj)) {
121+
if (key === '$class' && typeof child === 'string') {
122+
classBadge = { text: `class ${child}`, className: 'di-type-badge di-type-class' }
123+
continue
124+
}
125+
const walked = walk(child)
126+
out[key] = walked.value
127+
if (walked.badge) {
128+
if (walked.value && typeof walked.value === 'object') {
129+
objectBadges.set(walked.value as object, walked.badge)
130+
}
131+
else {
132+
childKeyBadges[key] = walked.badge
133+
hasKeyBadges = true
134+
}
135+
}
136+
}
137+
if (hasKeyBadges)
138+
keyBadges.set(out, childKeyBadges)
139+
return { value: out, badge: classBadge }
140+
}
141+
142+
/** Rewrite a normalized result into its display shape; badges land in the tables. */
143+
export function prepareForDisplay(result: unknown): unknown {
144+
const walked = walk(result)
145+
if (walked.badge && walked.value && typeof walked.value === 'object')
146+
objectBadges.set(walked.value as object, walked.badge)
147+
return walked.value
148+
}

0 commit comments

Comments
 (0)