fix(web): dedupe tree keys and guard against circular JSON
Key tree nodes by their index path instead of GUID, since TTS saves can contain duplicate GUIDs. Skip the Parent back-reference in the default viewer and stringify with a circular-reference guard.
This commit is contained in:
@@ -9,11 +9,12 @@ interface Props {
|
|||||||
export default function ObjectTree({ nodes, selectedGuid, onSelect }: Props) {
|
export default function ObjectTree({ nodes, selectedGuid, onSelect }: Props) {
|
||||||
return (
|
return (
|
||||||
<ul className="space-y-0.5">
|
<ul className="space-y-0.5">
|
||||||
{nodes.map((node) => (
|
{nodes.map((node, index) => (
|
||||||
<TreeNode
|
<TreeNode
|
||||||
key={node.object.GUID}
|
key={index}
|
||||||
node={node}
|
node={node}
|
||||||
depth={0}
|
depth={0}
|
||||||
|
path={`${index}`}
|
||||||
selectedGuid={selectedGuid}
|
selectedGuid={selectedGuid}
|
||||||
onSelect={onSelect}
|
onSelect={onSelect}
|
||||||
/>
|
/>
|
||||||
@@ -25,11 +26,14 @@ export default function ObjectTree({ nodes, selectedGuid, onSelect }: Props) {
|
|||||||
function TreeNode({
|
function TreeNode({
|
||||||
node,
|
node,
|
||||||
depth,
|
depth,
|
||||||
|
path,
|
||||||
selectedGuid,
|
selectedGuid,
|
||||||
onSelect,
|
onSelect,
|
||||||
}: {
|
}: {
|
||||||
node: ObjectTreeNode;
|
node: ObjectTreeNode;
|
||||||
depth: number;
|
depth: number;
|
||||||
|
/** Index path from the root, used as a stable unique key. */
|
||||||
|
path: string;
|
||||||
selectedGuid: string | null;
|
selectedGuid: string | null;
|
||||||
onSelect: (guid: string) => void;
|
onSelect: (guid: string) => void;
|
||||||
}) {
|
}) {
|
||||||
@@ -52,11 +56,12 @@ function TreeNode({
|
|||||||
</button>
|
</button>
|
||||||
{node.children.length > 0 && (
|
{node.children.length > 0 && (
|
||||||
<ul>
|
<ul>
|
||||||
{node.children.map((child) => (
|
{node.children.map((child, index) => (
|
||||||
<TreeNode
|
<TreeNode
|
||||||
key={child.object.GUID}
|
key={`${path}-${index}`}
|
||||||
node={child}
|
node={child}
|
||||||
depth={depth + 1}
|
depth={depth + 1}
|
||||||
|
path={`${path}-${index}`}
|
||||||
selectedGuid={selectedGuid}
|
selectedGuid={selectedGuid}
|
||||||
onSelect={onSelect}
|
onSelect={onSelect}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -27,20 +27,41 @@ export function resolveViewer(object: TTSObject): ObjectViewer['component'] {
|
|||||||
export function DefaultViewer({ object }: { object: TTSObject }) {
|
export function DefaultViewer({ object }: { object: TTSObject }) {
|
||||||
return (
|
return (
|
||||||
<dl className="grid grid-cols-1 gap-2 text-sm sm:grid-cols-2">
|
<dl className="grid grid-cols-1 gap-2 text-sm sm:grid-cols-2">
|
||||||
{Object.entries(object).map(([key, value]) => (
|
{Object.entries(object).map(([key, value]) => {
|
||||||
<div key={key} className="rounded-lg border border-zinc-800 bg-zinc-900 p-3">
|
// `Parent` is a back-reference added during traversal; it creates a
|
||||||
<dt className="font-mono text-xs uppercase text-zinc-500">{key}</dt>
|
// cycle with `ContainedObjects`, so skip it in the default view.
|
||||||
<dd className="mt-1 break-words text-zinc-200">
|
if (key === 'Parent') return null;
|
||||||
{typeof value === 'string' || typeof value === 'number' ? (
|
return (
|
||||||
value
|
<div key={key} className="rounded-lg border border-zinc-800 bg-zinc-900 p-3">
|
||||||
) : (
|
<dt className="font-mono text-xs uppercase text-zinc-500">{key}</dt>
|
||||||
<pre className="whitespace-pre-wrap font-mono text-xs">
|
<dd className="mt-1 break-words text-zinc-200">
|
||||||
{JSON.stringify(value, null, 2)}
|
{typeof value === 'string' || typeof value === 'number' ? (
|
||||||
</pre>
|
value
|
||||||
)}
|
) : (
|
||||||
</dd>
|
<pre className="whitespace-pre-wrap font-mono text-xs">
|
||||||
</div>
|
{safeStringify(value)}
|
||||||
))}
|
</pre>
|
||||||
|
)}
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</dl>
|
</dl>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** JSON-stringify a value, replacing any circular references with a marker. */
|
||||||
|
function safeStringify(value: unknown): string {
|
||||||
|
const seen = new Set<object>();
|
||||||
|
return JSON.stringify(
|
||||||
|
value,
|
||||||
|
(_key, v) => {
|
||||||
|
if (typeof v === 'object' && v !== null) {
|
||||||
|
if (seen.has(v)) return '[Circular]';
|
||||||
|
seen.add(v);
|
||||||
|
}
|
||||||
|
return v;
|
||||||
|
},
|
||||||
|
2,
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user