Skip to content
Open
Show file tree
Hide file tree
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
138 changes: 125 additions & 13 deletions src/features/modals/NodeModal/index.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
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, TextInput, Group } from "@mantine/core";
import { CodeHighlight } from "@mantine/code-highlight";
import type { NodeData } from "../../../types/graph";
import type { NodeData, NodeRow } from "../../../types/graph";
import useGraph from "../../editor/views/GraphView/stores/useGraph";
import { useEditNode } from "../../../store/useEditNode";
import useJson from "../../../store/useJson";
import useFile from "../../../store/useFile";
import { updateJsonByPath } from "../../../lib/utils/jsonPathUpdater";

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

export const NodeModal = ({ opened, onClose }: ModalProps) => {
const nodeData = useGraph(state => state.selectedNode);
const { isEditing, editedData, startEditing, updateEditedData, cancelEditing, resetEditState } = useEditNode();
const setJson = useJson(state => state.setJson);
const getJson = useJson(state => state.getJson);
const setContents = useFile(state => state.setContents);

const handleEdit = () => {
if (nodeData) {
startEditing(nodeData.id, nodeData.text);
}
};

const handleSave = () => {
if (!nodeData || !editedData) return;

try {
// Parse the current JSON
const currentJson = JSON.parse(getJson());

// Update the JSON at the specific path
const updatedJson = updateJsonByPath(currentJson, nodeData.path, editedData);

// Convert back to string with formatting
const updatedJsonString = JSON.stringify(updatedJson, null, 2);

// Update both the JSON store (for graph) AND the file store (for text editor)
setJson(updatedJsonString);
setContents({ contents: updatedJsonString, hasChanges: true });

// Reset edit state and close modal
resetEditState();
onClose();
} catch (error) {
console.error("Error saving node data:", error);
alert("Failed to save changes. Please check the console for details.");
}
};

const handleCancel = () => {
cancelEditing();
};

const handleValueChange = (index: number, newValue: string) => {
if (!editedData) return;

const updated = [...editedData];
updated[index] = { ...updated[index], value: newValue };
updateEditedData(updated);
};

const handleClose = () => {
resetEditState();
onClose();
};

const displayData = isEditing ? editedData : nodeData?.text;

return (
<Modal size="auto" opened={opened} onClose={onClose} centered withCloseButton={false}>
<Modal size="auto" opened={opened} onClose={handleClose} 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} />
<CloseButton onClick={handleClose} />
</Flex>
<ScrollArea.Autosize mah={250} maw={600}>
<CodeHighlight
code={normalizeNodeData(nodeData?.text ?? [])}
miw={350}
maw={600}
language="json"
withCopyButton
/>
</ScrollArea.Autosize>

{isEditing ? (
<ScrollArea.Autosize mah={250} maw={600}>
<Stack gap="xs" miw={350}>
{editedData?.map((row, index) => {
// Skip array and object types (they're references)
if (row.type === "array" || row.type === "object") {
return (
<Group key={index} gap="xs">
{row.key && <Text size="xs" fw={500}>{row.key}:</Text>}
<Text size="xs" c="dimmed">
{row.type === "array" ? `[${row.childrenCount ?? 0} items]` : `{${row.childrenCount ?? 0} keys}`}
</Text>
</Group>
);
}

return (
<Group key={index} gap="xs" align="center">
{row.key && <Text size="xs" fw={500} miw={80}>{row.key}:</Text>}
<TextInput
size="xs"
value={row.value?.toString() ?? ""}
onChange={(e) => handleValueChange(index, e.currentTarget.value)}
style={{ flex: 1 }}
placeholder="Enter value"
/>
</Group>
);
})}
</Stack>
</ScrollArea.Autosize>
) : (
<ScrollArea.Autosize mah={250} maw={600}>
<CodeHighlight
code={normalizeNodeData(displayData ?? [])}
miw={350}
maw={600}
language="json"
withCopyButton
/>
</ScrollArea.Autosize>
)}
</Stack>

{/* Action Buttons */}
<Flex gap="sm" justify="flex-end">
{!isEditing ? (
<Button size="xs" onClick={handleEdit}>
Edit
</Button>
) : (
<>
<Button size="xs" variant="default" onClick={handleCancel}>
Cancel
</Button>
<Button size="xs" onClick={handleSave}>
Save
</Button>
</>
)}
</Flex>

<Text fz="xs" fw={500}>
JSON Path
</Text>
Expand Down
100 changes: 100 additions & 0 deletions src/lib/utils/jsonPathUpdater.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
/**
* Helper functions for updating JSON by path
*/
import type { JSONPath } from "jsonc-parser";
import type { NodeRow } from "../../types/graph";

/**
* Updates a value in a JSON object at a specific path
* @param json - The JSON object to update
* @param path - The path to the value to update (e.g., ["customer", "address", 0, "street"])
* @param newData - The new data to set at the path
* @returns The updated JSON object
*/
export function updateJsonByPath(
json: any,
path: JSONPath | undefined,
newData: NodeRow[]
): any {
if (!path || path.length === 0) {
// Root level update
return createObjectFromNodeRows(newData);
}

// Deep clone to avoid mutation
const result = JSON.parse(JSON.stringify(json));

// Navigate to the parent of the target
let current = result;
for (let i = 0; i < path.length - 1; i++) {
const segment = path[i];
current = current[segment];

if (current === undefined) {
throw new Error(`Invalid path: cannot find segment "${segment}"`);
}
}

// Update the target
const lastSegment = path[path.length - 1];
const newValue = createObjectFromNodeRows(newData);

// If it's a simple value (not an object/array), set it directly
if (newData.length === 1 && newData[0].key === null) {
current[lastSegment] = newData[0].value;
} else {
current[lastSegment] = newValue;
}

return result;
}

/**
* Creates an object or value from NodeRow data
* @param nodeRows - The node rows to convert
* @returns The created object or primitive value
*/
function createObjectFromNodeRows(nodeRows: NodeRow[]): any {
// Handle primitive values (single row with no key)
if (nodeRows.length === 1 && nodeRows[0].key === null) {
return parseValue(nodeRows[0].value, nodeRows[0].type);
}

// Handle objects
const result: any = {};

for (const row of nodeRows) {
// Skip array and object types as they're references to other nodes
if (row.type === "array" || row.type === "object") {
continue;
}

if (row.key !== null) {
result[row.key] = parseValue(row.value, row.type);
}
}

return result;
}

/**
* Parse a value based on its type
* @param value - The value to parse
* @param type - The type of the value
* @returns The parsed value
*/
function parseValue(value: string | number | null, type: string): any {
if (value === null || type === "null") return null;

switch (type) {
case "boolean":
if (typeof value === "boolean") return value;
return value === "true";
case "number":
return typeof value === "number" ? value : parseFloat(value as string);
case "string":
return String(value);
default:
return value;
}
}
52 changes: 52 additions & 0 deletions src/store/useEditNode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { create } from "zustand";
import type { NodeData, NodeRow } from "../types/graph";

interface EditNodeState {
isEditing: boolean;
editingNodeId: string | null;
originalData: NodeRow[] | null;
editedData: NodeRow[] | null;
}

interface EditNodeActions {
startEditing: (nodeId: string, nodeData: NodeRow[]) => void;
updateEditedData: (data: NodeRow[]) => void;
cancelEditing: () => void;
resetEditState: () => void;
}

const initialState: EditNodeState = {
isEditing: false,
editingNodeId: null,
originalData: null,
editedData: null,
};

export const useEditNode = create<EditNodeState & EditNodeActions>((set, get) => ({
...initialState,

startEditing: (nodeId, nodeData) => {
// Deep copy the original data
const originalCopy = JSON.parse(JSON.stringify(nodeData));
const editedCopy = JSON.parse(JSON.stringify(nodeData));

set({
isEditing: true,
editingNodeId: nodeId,
originalData: originalCopy,
editedData: editedCopy,
});
},

updateEditedData: (data) => {
set({ editedData: data });
},

cancelEditing: () => {
set(initialState);
},

resetEditState: () => {
set(initialState);
},
}));