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
65 changes: 61 additions & 4 deletions src/features/editor/views/GraphView/CustomNode/ObjectNode.tsx
Original file line number Diff line number Diff line change
@@ -1,35 +1,92 @@
import React from "react";
import styled from "styled-components";
import type { CustomNodeProps } from ".";
import { NODE_DIMENSIONS } from "../../../../../constants/graph";
import type { NodeData } from "../../../../../types/graph";
import { TextRenderer } from "./TextRenderer";
import * as Styled from "./styles";
import useNodeEdit from "../../../../../store/useNodeEdit";
import { useModal } from "../../../../../store/useModal";

type RowProps = {
row: NodeData["text"][number];
x: number;
y: number;
index: number;
node: NodeData;
};

const Row = ({ row, x, y, index }: RowProps) => {
const StyledRowWrapper = styled.span`
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
gap: 8px;

.edit-button {
opacity: 0;
transition: opacity 0.2s;
}

&:hover .edit-button {
opacity: 1;
}
`;

const EditButton = styled.button`
background: transparent;
border: none;
color: ${({ theme }) => theme.INTERACTIVE_NORMAL};
cursor: pointer;
font-size: 12px;
padding: 2px 6px;
pointer-events: all;
flex-shrink: 0;

&:hover {
color: ${({ theme }) => theme.TEXT_POSITIVE};
}
`;

const Row = ({ row, x, y, index, node }: RowProps) => {
const rowPosition = index * NODE_DIMENSIONS.ROW_HEIGHT;

const startEditNode = useNodeEdit(state => state.startEditNode);
const setVisible = useModal(state => state.setVisible);

const getRowText = () => {
if (row.type === "object") return `{${row.childrenCount ?? 0} keys}`;
if (row.type === "array") return `[${row.childrenCount ?? 0} items]`;
return row.value;
};

const handleEditClick = (e: React.MouseEvent) => {
e.stopPropagation();
startEditNode(node);
setVisible("EditNodeModal", true);
};

const canEdit = row.type !== "object" && row.type !== "array";

return (
<Styled.StyledRow
$value={row.value}
data-key={`${row.key}: ${row.value}`}
data-x={x}
data-y={y + rowPosition}
onClick={e => e.stopPropagation()}
>
<Styled.StyledKey $type="object">{row.key}: </Styled.StyledKey>
<TextRenderer>{getRowText()}</TextRenderer>
<StyledRowWrapper>
<span>
<Styled.StyledKey $type="object">{row.key}: </Styled.StyledKey>
<TextRenderer>{getRowText()}</TextRenderer>
</span>
{canEdit && (
<EditButton className="edit-button" onClick={handleEditClick} aria-label="Edit value" title="Edit value">
</EditButton>
)}
</StyledRowWrapper>
</Styled.StyledRow>
);
};
Expand All @@ -44,7 +101,7 @@ const Node = ({ node, x, y }: CustomNodeProps) => (
$isObject
>
{node.text.map((row, index) => (
<Row key={`${node.id}-${index}`} row={row} x={x} y={y} index={index} />
<Row key={`${node.id}-${index}`} row={row} x={x} y={y} index={index} node={node} />
))}
</Styled.StyledForeignObject>
);
Expand Down
36 changes: 36 additions & 0 deletions src/features/editor/views/GraphView/CustomNode/TextNode.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import useConfig from "../../../../../store/useConfig";
import { isContentImage } from "../lib/utils/calculateNodeSize";
import { TextRenderer } from "./TextRenderer";
import * as Styled from "./styles";
import useNodeEdit from "../../../../../store/useNodeEdit";
import { useModal } from "../../../../../store/useModal";

const StyledTextNodeWrapper = styled.span<{ $isParent: boolean }>`
display: flex;
Expand All @@ -14,6 +16,11 @@ const StyledTextNodeWrapper = styled.span<{ $isParent: boolean }>`
width: 100%;
overflow: hidden;
padding: 0 10px;
position: relative;

&:hover .edit-button {
opacity: 1;
}
`;

const StyledImageWrapper = styled.div`
Expand All @@ -26,12 +33,38 @@ const StyledImage = styled.img`
background: ${({ theme }) => theme.BACKGROUND_MODIFIER_ACCENT};
`;

const EditButton = styled.button`
margin-left: 8px;
background: transparent;
border: none;
color: ${({ theme }) => theme.INTERACTIVE_NORMAL};
cursor: pointer;
font-size: 14px;
padding: 2px 6px;
opacity: 0;
transition: opacity 0.2s;
pointer-events: all;

&:hover {
color: ${({ theme }) => theme.TEXT_POSITIVE};
}
`;

const Node = ({ node, x, y }: CustomNodeProps) => {
const { text, width, height } = node;
const imagePreviewEnabled = useConfig(state => state.imagePreviewEnabled);
const isImage = imagePreviewEnabled && isContentImage(JSON.stringify(text[0].value));
const value = text[0].value;

const startEditNode = useNodeEdit(state => state.startEditNode);
const setVisible = useModal(state => state.setVisible);

const handleEditClick = (e: React.MouseEvent) => {
e.stopPropagation();
startEditNode(node);
setVisible("EditNodeModal", true);
};

return (
<Styled.StyledForeignObject
data-id={`node-${node.id}`}
Expand All @@ -54,6 +87,9 @@ const Node = ({ node, x, y }: CustomNodeProps) => {
<Styled.StyledKey $value={value} $type={typeof text[0].value}>
<TextRenderer>{value}</TextRenderer>
</Styled.StyledKey>
<EditButton className="edit-button" onClick={handleEditClick} aria-label="Edit value" title="Edit value">
</EditButton>
</StyledTextNodeWrapper>
)}
</Styled.StyledForeignObject>
Expand Down
2 changes: 1 addition & 1 deletion src/features/editor/views/GraphView/CustomNode/styles.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export const StyledForeignObject = styled.foreignObject<{ $isObject?: boolean }>
font-size: 12px;
font-weight: 500;
overflow: hidden;
pointer-events: none;
pointer-events: all;

&.searched {
background: rgba(27, 255, 0, 0.1);
Expand Down
48 changes: 48 additions & 0 deletions src/features/editor/views/GraphView/lib/updateJsonAtPath.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { modify, applyEdits } from "jsonc-parser";

export function parseEditedValue(input: string, originalType?: string) {
const trimmed = input?.trim();

if (originalType === "string") return String(input);

if (trimmed === "null") return null;
if (trimmed === "true") return true;
if (trimmed === "false") return false;

// number detection
if (/^-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?$/.test(trimmed)) {
try {
return Number(trimmed);
} catch (e) {
return String(input);
}
}

// try JSON parse for arrays or objects
if (trimmed && (trimmed.startsWith("[") || trimmed.startsWith("{"))) {
try {
return JSON.parse(trimmed);
} catch (e) {
// fallthrough to string
}
}

// fallback to string
return String(input);
}

export function updateJsonAtPath(json: string, path: Array<string | number>, value: unknown) {
try {
const edits = modify(json, path as any, value, {
formattingOptions: { insertSpaces: true, tabSize: 2 },
});

const newJson = applyEdits(json, edits);
return newJson;
} catch (e) {
// rethrow so callers can show errors
throw e;
}
}

export default updateJsonAtPath;
Loading