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
16 changes: 16 additions & 0 deletions src/features/editor/views/GraphView/stores/useGraph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ interface GraphActions {
centerView: () => void;
clearGraph: () => void;
setZoomFactor: (zoomFactor: number) => void;
updateNode: (id: string, updatedNode: Partial<NodeData>) => void;
}

const useGraph = create<Graph & GraphActions>((set, get) => ({
Expand Down Expand Up @@ -101,6 +102,21 @@ const useGraph = create<Graph & GraphActions>((set, get) => ({
},
toggleFullscreen: fullscreen => set({ fullscreen }),
setViewPort: viewPort => set({ viewPort }),
updateNode: (id, updatedNode) => {
set(state => {
const updatedNodes = state.nodes.map(node =>
node.id === id ? { ...node, ...updatedNode } : node
);

return {
nodes: updatedNodes,
selectedNode:
state.selectedNode?.id === id
? { ...state.selectedNode, ...updatedNode }
: state.selectedNode,
};
});
},
}));

export default useGraph;
119 changes: 83 additions & 36 deletions src/features/modals/NodeModal/index.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,24 @@
import React from "react";
import React, { useState } 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,
TextInput,
Button,
} from "@mantine/core";
import { CodeHighlight } from "@mantine/code-highlight";
import type { NodeData } from "../../../types/graph";
import useGraph from "../../editor/views/GraphView/stores/useGraph";

// 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, unknown> = {};
nodeRows?.forEach(row => {
if (row.type !== "array" && row.type !== "object") {
if (row.key) obj[row.key] = row.value;
Expand All @@ -19,50 +27,89 @@ const normalizeNodeData = (nodeRows: NodeData["text"]) => {
return JSON.stringify(obj, null, 2);
};

// return json path in the format $["customer"]
const jsonPathToString = (path?: NodeData["path"]) => {
if (!path || path.length === 0) return "$";
const segments = path.map(seg => (typeof seg === "number" ? seg : `"${seg}"`));
return `$[${segments.join("][")}]`;
};

export const NodeModal = ({ opened, onClose }: ModalProps) => {
const nodeData = useGraph(state => state.selectedNode);
const { selectedNode, updateNode } = useGraph();
const [isEditing, setIsEditing] = useState(false);
const [editedFields, setEditedFields] = useState<Record<string, string>>({});

if (!selectedNode) return null;

const handleEditToggle = () => {
if (!isEditing) {
// Prefill input values for all editable fields
const fields: Record<string, string> = {};
selectedNode.text.forEach(row => {
if (row.key && row.type !== "array" && row.type !== "object") {
fields[row.key] = row.value ?? "";
}
});
setEditedFields(fields);
}
setIsEditing(!isEditing);
};

const handleChange = (key: string, value: string) => {
setEditedFields(prev => ({ ...prev, [key]: value }));
};

const handleSave = () => {
if (selectedNode) {
const updatedText = selectedNode.text.map(row =>
row.key && editedFields[row.key] !== undefined
? { ...row, value: editedFields[row.key] }
: row
);

updateNode(selectedNode.id, {
...selectedNode,
text: updatedText,
});
}
setIsEditing(false);
};

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 opened={opened} onClose={onClose} title={`Node: ${selectedNode.id}`}>
<Stack>
<Text>Path: {jsonPathToString(selectedNode.path)}</Text>
<ScrollArea>
{isEditing ? (
<Stack>
{Object.entries(editedFields).map(([key, value]) => (
<TextInput
key={key}
label={key}
value={value}
onChange={e => handleChange(key, e.target.value)}
/>
))}
</Stack>
) : (
<CodeHighlight
code={normalizeNodeData(nodeData?.text ?? [])}
miw={350}
maw={600}
language="json"
withCopyButton
code={normalizeNodeData(selectedNode.text)}
/>
</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
/>
</ScrollArea.Autosize>
)}
</ScrollArea>

<Flex justify="space-between" mt="sm">
{isEditing ? (
<>
<Button onClick={handleSave}>Save</Button>
<Button variant="outline" onClick={handleEditToggle}>
Cancel
</Button>
</>
) : (
<Button onClick={handleEditToggle}>Edit</Button>
)}
</Flex>
</Stack>
</Modal>
);
Expand Down