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
128 changes: 118 additions & 10 deletions src/features/modals/NodeModal/index.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
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, CloseButton, Button, Textarea } from "@mantine/core";
import { CodeHighlight } from "@mantine/code-highlight";
import type { NodeData } from "../../../types/graph";
import useGraph from "../../editor/views/GraphView/stores/useGraph";
import useFile from "../../../store/useFile";
import { contentToJson, jsonToContent } from "../../../lib/utils/jsonAdapter";
import { toast } from "react-hot-toast";

// return object from json removing array and object fields
const normalizeNodeData = (nodeRows: NodeData["text"]) => {
Expand All @@ -28,25 +31,130 @@ const jsonPathToString = (path?: NodeData["path"]) => {

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

const [isEditing, setIsEditing] = React.useState(false);
const [draftContent, setDraftContent] = React.useState<string>("{}");
const [liveContent, setLiveContent] = React.useState<string>(normalizeNodeData(nodeData?.text ?? []));

React.useEffect(() => {
const normalized = normalizeNodeData(nodeData?.text ?? []);
setDraftContent(normalized);
setLiveContent(normalized);
setIsEditing(false);
}, [nodeData]);

return (
<Modal size="auto" opened={opened} onClose={onClose} centered withCloseButton={false}>
<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 gap="sm" align="center">
{!isEditing && (
<Button size="xs" variant="outline" onClick={() => setIsEditing(true)}>
Edit
</Button>
)}
{isEditing && (
<>
<Button
size="xs"
color="green"
variant="filled"
onClick={async () => {
// Save handler
try {
const format = fileStore.getFormat ? fileStore.getFormat() : fileStore.format;
const contents = fileStore.getContents ? fileStore.getContents() : fileStore.contents;
const parsedFile = await contentToJson(contents, format);

let parsedDraft: any = undefined;
try {
parsedDraft = JSON.parse(draftContent);
} catch (e) {
// allow primitive values like string/number
try {
parsedDraft = JSON.parse(draftContent.trim());
} catch (err) {
toast.error("Invalid JSON in content. Please fix before saving.");
return;
}
}

// If no path provided, replace root
const path = nodeData?.path ?? [];
if (!path || path.length === 0) {
// replace whole document
const newContent = await jsonToContent(JSON.stringify(parsedDraft, null, 2), format);
fileStore.setContents({ contents: newContent });
} else {
// traverse to parent
let parent: any = parsedFile;
for (let i = 0; i < path.length - 1; i++) {
parent = parent[path[i] as any];
if (typeof parent === "undefined") break;
}
const last = path[path.length - 1] as any;
if (typeof parent !== "undefined") {
parent[last] = parsedDraft;
const newContent = await jsonToContent(JSON.stringify(parsedFile, null, 2), format);
fileStore.setContents({ contents: newContent });
} else {
toast.error("Unable to resolve JSON path for this node.");
return;
}
}

// update visible modal content immediately
setLiveContent(draftContent);
setIsEditing(false);
toast.success("Saved node content");
} catch (error) {
console.error(error);
toast.error("Failed to save node content");
}
}}
>
Save
</Button>
<Button
size="xs"
color="red"
variant="filled"
onClick={() => {
// cancel edits: revert to the last visible/saved content (liveContent)
setDraftContent(liveContent);
setIsEditing(false);
}}
>
Cancel
</Button>
</>
)}
<CloseButton onClick={onClose} />
</Flex>
</Flex>
<ScrollArea.Autosize mah={250} maw={600}>
<CodeHighlight
code={normalizeNodeData(nodeData?.text ?? [])}
miw={350}
maw={600}
language="json"
withCopyButton
/>
{!isEditing ? (
<CodeHighlight
code={liveContent}
miw={350}
maw={600}
language="json"
withCopyButton
/>
) : (
<Textarea
minRows={4}
maxRows={12}
value={draftContent}
onChange={e => setDraftContent(e.currentTarget.value)}
styles={{ input: { fontFamily: "monospace" } }}
/>
)}
</ScrollArea.Autosize>
</Stack>
<Text fz="xs" fw={500}>
Expand Down