Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
182 changes: 153 additions & 29 deletions src/features/modals/NodeModal/index.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
import React from "react";
import type { ModalProps } from "@mantine/core";
import { Modal, Stack, Text, ScrollArea, Flex, CloseButton } from "@mantine/core";
import { Modal, Stack, Text, ScrollArea, Flex, Button, Textarea, Group } from "@mantine/core";
import { CodeHighlight } from "@mantine/code-highlight";
import { toast } from "react-hot-toast";
import type { NodeData } from "../../../types/graph";
import useGraph from "../../editor/views/GraphView/stores/useGraph";
import useJson from "../../../store/useJson";
import useFile from "../../../store/useFile";

// return object from json removing array and object fields
const normalizeNodeData = (nodeRows: NodeData["text"]) => {
if (!nodeRows || nodeRows.length === 0) return "{}";
if (nodeRows.length === 1 && !nodeRows[0].key) return `${nodeRows[0].value}`;

const obj = {};
const obj: Record<string, any> = {};
nodeRows?.forEach(row => {
if (row.type !== "array" && row.type !== "object") {
if (row.key) obj[row.key] = row.value;
Expand All @@ -26,42 +29,163 @@ const jsonPathToString = (path?: NodeData["path"]) => {
return `$[${segments.join("][")}]`;
};

// set a value at path (mutates target)
function setValueAtPath(target: any, path: (string | number)[], value: any) {
if (!path || path.length === 0) {
return value;
}
let cur = target;
for (let i = 0; i < path.length - 1; i++) {
const seg = path[i];
if (typeof seg === "number") {
if (!Array.isArray(cur)) cur = []; // defensive
if (typeof cur[seg] === "undefined") cur[seg] = {};
cur = cur[seg];
} else {
if (typeof cur[seg] === "undefined") cur[seg] = {};
cur = cur[seg];
}
}
const last = path[path.length - 1];
cur[last as any] = value;
return target;
}

export const NodeModal = ({ opened, onClose }: ModalProps) => {
const nodeData = useGraph(state => state.selectedNode);
const getJson = useJson(state => state.getJson);
const setContents = useFile(state => state.setContents);

const [isEditing, setIsEditing] = React.useState(false);
const [value, setValue] = React.useState<string>("");

React.useEffect(() => {
if (opened) {
setIsEditing(false);
setValue(normalizeNodeData(nodeData?.text ?? []));
}
}, [opened, nodeData]);

const startEdit = () => {
setValue(normalizeNodeData(nodeData?.text ?? []));
setIsEditing(true);
};

const handleCancel = () => {
setIsEditing(false);
setValue(normalizeNodeData(nodeData?.text ?? []));
};

const handleSave = () => {
try {
// try parse edited value
let parsed: any;
try {
parsed = JSON.parse(value);
} catch (err) {
toast.error("Invalid JSON. Fix the content before saving.");
return;
}

const docJson = JSON.parse(getJson());
const path = nodeData?.path ?? [];

let updated: any;
if (!path || path.length === 0) {
updated = parsed;
} else {
const clone = JSON.parse(JSON.stringify(docJson));
setValueAtPath(clone, path, parsed);
updated = clone;
}

setContents({ contents: JSON.stringify(updated, null, 2), hasChanges: true });
toast.success("Node updated");
setIsEditing(false);
onClose?.();
} catch (error: any) {
toast.error(error?.message ?? "Failed to save node content.");
}
};

return (
<Modal size="auto" opened={opened} onClose={onClose} centered withCloseButton={false}>
<Stack pb="sm" gap="sm">
<Stack gap="xs">
<Flex justify="space-between" align="center">
<Text fz="xs" fw={500}>
Content
</Text>
<CloseButton onClick={onClose} />
</Flex>
<ScrollArea.Autosize mah={250} maw={600}>
<Modal
title="Node conent"
size="auto"
opened={opened}
onClose={() => {
setIsEditing(false);
onClose?.();
}}
centered
>
<Stack pb="sm" gap="sm" style={{ position: "relative", alignItems: "center", width: "100%" }}>
{/* New header row: "Content" on left, edit controls aligned to the right */}
<Flex align="center" justify="space-between" style={{ width: "100%", padding: "6px 4px" }}>
<Text fz="xs" fw={600}>
Content
</Text>

<Group spacing="xs" style={{ marginLeft: "auto" }}>
{isEditing ? (
<>
<Button size="xs" color="green" onClick={handleSave}>
Save
</Button>
<Button size="xs" variant="default" onClick={handleCancel}>
Cancel
</Button>
</>
) : (
<Button size="xs" variant="filled" color="blue" onClick={startEdit}>
Edit
</Button>
)}
</Group>
</Flex>

<Stack gap="xs" style={{ width: "100%", alignItems: "center" }}>
<ScrollArea.Autosize mah={isEditing ? 340 : 300} maw={640} style={{ width: "100%", display: "flex", justifyContent: "center" }}>
<div style={{ maxWidth: 560, width: "100%" }}>
{isEditing ? (
<Textarea
value={value}
onChange={e => setValue(e.currentTarget.value)}
minRows={10}
autosize
styles={{
input: { fontFamily: "monospace", fontSize: 13, whiteSpace: "pre-wrap" },
}}
/>
) : (
<CodeHighlight
code={normalizeNodeData(nodeData?.text ?? [])}
miw={350}
maw={600}
language="json"
withCopyButton
style={{ textAlign: "left" }}
/>
)}
</div>
</ScrollArea.Autosize>
</Stack>

<Text fz="xs" fw={500} style={{ marginTop: 6 }}>
JSON Path
</Text>
<ScrollArea.Autosize mah={200} maw={640} style={{ width: "100%", display: "flex", justifyContent: "center" }}>
<div style={{ maxWidth: 560, width: "100%" }}>
<CodeHighlight
code={normalizeNodeData(nodeData?.text ?? [])}
code={jsonPathToString(nodeData?.path)}
miw={350}
maw={600}
mah={250}
language="json"
copyLabel="Copy to clipboard"
copiedLabel="Copied to clipboard"
withCopyButton
/>
</ScrollArea.Autosize>
</Stack>
<Text fz="xs" fw={500}>
JSON Path
</Text>
<ScrollArea.Autosize maw={600}>
<CodeHighlight
code={jsonPathToString(nodeData?.path)}
miw={350}
mah={250}
language="json"
copyLabel="Copy to clipboard"
copiedLabel="Copied to clipboard"
withCopyButton
/>
</div>
</ScrollArea.Autosize>
</Stack>
</Modal>
Expand Down