feat: adds new JSON viewer (#5407)
* feat: Add JsonEditor component for JSON editing This commit adds a new component called JsonEditor, which allows users to edit JSON data. The component uses the JSONEditor library and provides options for customizing the editor's appearance and behavior. It also includes functionality for updating the edited data and triggering a callback when the data changes. The component is initialized with an initial data object and can be updated with new data through props. The component is designed to be reusable and can be easily integrated into other parts of the application. * feat: Add JsonEditor component for JSON editing and replace JsonView The commit adds a new component called JsonEditor for editing JSON data. It replaces the previous JsonView component used for displaying JSON data. This change improves the functionality and user experience of the application. * [autofix.ci] apply automated fixes * Added json editor to package * Change jsonEditor to use new vanilla jsonEditor * Added color variables * Removed unused buttons on json editor * Removed unused dark store * Fixed state management on dictAreaModal * Change default DictComponent value to dict * removed unused checks * Changed to forward ref of json editor * removed nav bar * Fixed value not being received * Added check if value is null and handleOnNewValue on this case * Removed unused button on json editor * Adds auto focus and change mode to text * ♻️ (jsonEditor/index.tsx): remove unnecessary comments and improve code readability by removing redundant comments and empty dependency array in useEffect. * Fixed dict component test * Refactor json-input component to use VanillaJsonEditor and JsonEditor component * Remove react-json-view-lite and react18-json-view dependencies * [autofix.ci] apply automated fixes * Refactor jsonEditor component to add readOnly prop * Refactor json-output-view component to add read-only prop * Refactor switchOutputView component to add JSON output view * Refactor CSS styles for JSON editor buttons * Update package-lock.json to add new dependencies for @mapbox/node-pre-gyp and remove jsdom and its related modules * [autofix.ci] apply automated fixes * Refactor textModal component to remove unused imports and dependencies * add filter property to jsonEditor * [autofix.ci] apply automated fixes * Refactor jsonEditor component to handle transform queries and display error messages * [autofix.ci] apply automated fixes * Refactor jsonEditor component to add filter property and handle transform queries * Refactor jsonEditor component to add filter property and handle transform queries * Refactor Output class to add options property * Refactor json-output-view.tsx to update default height value * Refactor jsonEditor component to normalize transform query path * Refactor jsonEditor component to add JSONQuery support for transform queries * Add jsonquerylang library as a dependency * Refactor utils.py to add support for applying JSON filters * Refactor base.py to add apply_options method for JSON filtering * Fix jsonOutputView to set filter option to undefined when empty * Enhance apply_json_filter to support Data objects and improve query handling * Improve json filtering in Output class to return original result if filtered result is None * Refactor Component class to simplify map_outputs method and enhance output handling * move jsonquerylang to langflow-base * Refactor Component and Output classes for improved output handling and type consistency; enhance apply_json_filter to ensure proper data processing and add comprehensive unit tests. * Add filter_data method to Data class and update apply_options in Output class to utilize it; enhance apply_json_filter return type to Data. * Import apply_json_filter in Data class to enable data filtering functionality * Add validation for JSON results in JsonEditor; ensure only objects and arrays are accepted and handle serialization errors * 📝 (App.css): Add styles for jse-menu separator and cm-gutters to improve UI layout ♻️ (index.tsx): Refactor imports and code formatting for better readability and maintainability 🔧 (index.tsx): Update BaseModal component styles to improve UI layout and responsiveness * 📝 (custom.css): add extra line at the end of the file for consistency 📝 (App.css): add styling for .jse-menu .jse-button.jse-group-button class 📝 (classes.css): add extra line at the end of the file for consistency * 🐛 (data.py): fix custom_serializer to handle bytes objects by decoding them to utf-8 ♻️ (base.py): refactor options field in Output class to accept BaseModel, dict, or None for better flexibility and compatibility * Refactor apply_json_filter to directly use jsonquery on result, removing unnecessary string conversion * ✨ (base.py): Introduce OutputOptions model for output filtering and update Output class to use it * 🐛 (classes.css): Hide search box background and adjust container positioning for better layout * ✨ (jsonEditor): Enhance filtering functionality with improved state management and user feedback * Fixed package lock * [autofix.ci] apply automated fixes * Updated package lock * [autofix.ci] apply automated fixes --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Lucas Oliveira <lucas.edu.oli@hotmail.com> Co-authored-by: cristhianzl <cristhian.lousa@gmail.com> Co-authored-by: Gabriel Luiz Freitas Almeida <gabriel@langflow.org>
This commit is contained in:
parent
aeb228ce94
commit
eec1fb23d3
25 changed files with 3171 additions and 1155 deletions
|
|
@ -276,3 +276,4 @@ body {
|
|||
) {
|
||||
margin-top: 0.25rem !important;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -155,8 +155,7 @@ class Component(CustomComponent):
|
|||
self._reset_all_output_values()
|
||||
if self.inputs is not None:
|
||||
self.map_inputs(self.inputs)
|
||||
if self.outputs is not None:
|
||||
self.map_outputs(self.outputs)
|
||||
self.map_outputs()
|
||||
|
||||
# Final setup
|
||||
self._set_output_types(list(self._outputs_map.values()))
|
||||
|
|
@ -405,7 +404,7 @@ class Component(CustomComponent):
|
|||
msg = f"Output {name} not found in {self.__class__.__name__}"
|
||||
raise ValueError(msg)
|
||||
|
||||
def map_outputs(self, outputs: list[Output]) -> None:
|
||||
def map_outputs(self) -> None:
|
||||
"""Maps the given list of outputs to the component.
|
||||
|
||||
Args:
|
||||
|
|
@ -417,6 +416,19 @@ class Component(CustomComponent):
|
|||
Returns:
|
||||
None
|
||||
"""
|
||||
# override outputs (generated from the class code) with vertex outputs
|
||||
# if they exist (generated from the frontend)
|
||||
outputs = []
|
||||
if self._vertex and self._vertex.outputs:
|
||||
for output in self._vertex.outputs:
|
||||
try:
|
||||
output_ = Output(**output)
|
||||
outputs.append(output_)
|
||||
except ValidationError as e:
|
||||
msg = f"Invalid output: {e}"
|
||||
raise ValueError(msg) from e
|
||||
else:
|
||||
outputs = self.outputs
|
||||
for output in outputs:
|
||||
if output.name is None:
|
||||
msg = "Output name cannot be None."
|
||||
|
|
@ -980,8 +992,9 @@ class Component(CustomComponent):
|
|||
and self._vertex.graph.flow_id is not None
|
||||
):
|
||||
result.set_flow_id(self._vertex.graph.flow_id)
|
||||
|
||||
result = output.apply_options(result)
|
||||
output.value = result
|
||||
|
||||
return result
|
||||
|
||||
def _build_artifact(self, result):
|
||||
|
|
|
|||
|
|
@ -235,6 +235,19 @@ class Data(BaseModel):
|
|||
def __eq__(self, /, other):
|
||||
return isinstance(other, Data) and self.data == other.data
|
||||
|
||||
def filter_data(self, filter_str: str) -> "Data":
|
||||
"""Filters the data dictionary based on the filter string.
|
||||
|
||||
Args:
|
||||
filter_str (str): The filter string to apply to the data dictionary.
|
||||
|
||||
Returns:
|
||||
Data: The filtered Data.
|
||||
"""
|
||||
from langflow.template.utils import apply_json_filter
|
||||
|
||||
return apply_json_filter(self.data, filter_str)
|
||||
|
||||
|
||||
def custom_serializer(obj):
|
||||
if isinstance(obj, datetime):
|
||||
|
|
@ -246,6 +259,8 @@ def custom_serializer(obj):
|
|||
return str(obj)
|
||||
if isinstance(obj, BaseModel):
|
||||
return obj.model_dump()
|
||||
if isinstance(obj, bytes):
|
||||
return obj.decode("utf-8", errors="replace")
|
||||
# Add more custom serialization rules as needed
|
||||
msg = f"Type {type(obj)} not serializable"
|
||||
raise TypeError(msg)
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ from pydantic import (
|
|||
from langflow.field_typing import Text
|
||||
from langflow.field_typing.range_spec import RangeSpec
|
||||
from langflow.helpers.custom import format_type
|
||||
from langflow.schema.data import Data
|
||||
from langflow.type_extraction.type_extraction import post_process_type
|
||||
|
||||
|
||||
|
|
@ -172,6 +173,11 @@ class Input(BaseModel):
|
|||
return v
|
||||
|
||||
|
||||
class OutputOptions(BaseModel):
|
||||
filter: str | None = None
|
||||
"""Filter to be applied to the output data."""
|
||||
|
||||
|
||||
class Output(BaseModel):
|
||||
types: list[str] = Field(default=[])
|
||||
"""List of output types for the field."""
|
||||
|
|
@ -202,6 +208,9 @@ class Output(BaseModel):
|
|||
allows_loop: bool = Field(default=False)
|
||||
"""Specifies if the output allows looping."""
|
||||
|
||||
options: OutputOptions | None = Field(default=None)
|
||||
"""Options for the output."""
|
||||
|
||||
tool_mode: bool = Field(default=True)
|
||||
"""Specifies if the output should be used as a tool"""
|
||||
|
||||
|
|
@ -222,7 +231,6 @@ class Output(BaseModel):
|
|||
result = handler(self)
|
||||
if self.value == UNDEFINED:
|
||||
result["value"] = UNDEFINED.value
|
||||
|
||||
return result
|
||||
|
||||
@model_validator(mode="after")
|
||||
|
|
@ -234,4 +242,14 @@ class Output(BaseModel):
|
|||
raise ValueError(msg)
|
||||
if self.display_name is None:
|
||||
self.display_name = self.name
|
||||
# Convert dict options to OutputOptions model
|
||||
if isinstance(self.options, dict):
|
||||
self.options = OutputOptions(**self.options)
|
||||
return self
|
||||
|
||||
def apply_options(self, result):
|
||||
if not self.options:
|
||||
return result
|
||||
if self.options.filter and isinstance(result, Data):
|
||||
return result.filter_data(self.options.filter)
|
||||
return result
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@ from pathlib import Path
|
|||
|
||||
from platformdirs import user_cache_dir
|
||||
|
||||
from langflow.schema.data import Data
|
||||
|
||||
|
||||
def raw_frontend_data_is_valid(raw_frontend_data):
|
||||
"""Check if the raw frontend data is valid for processing."""
|
||||
|
|
@ -96,3 +98,65 @@ def update_frontend_node_with_template_values(frontend_node, raw_frontend_node):
|
|||
frontend_node["description"] = raw_frontend_node.get("description", frontend_node.get("description", ""))
|
||||
|
||||
return frontend_node
|
||||
|
||||
|
||||
def apply_json_filter(result, filter_) -> Data:
|
||||
"""Apply a json filter to the result.
|
||||
|
||||
Args:
|
||||
result (Data): The JSON data to filter
|
||||
filter_ (str): The filter query string in jsonquery format
|
||||
|
||||
Returns:
|
||||
Data: The filtered result
|
||||
"""
|
||||
if not filter_ or not filter_.strip():
|
||||
return result
|
||||
# if result is a Data object, get the data
|
||||
if isinstance(result, Data):
|
||||
result = result.data
|
||||
try:
|
||||
from jsonquerylang import jsonquery
|
||||
|
||||
# If query doesn't start with '.', add it to match jsonquery syntax
|
||||
return Data(data=jsonquery(result, filter_))
|
||||
|
||||
except (ImportError, ValueError, TypeError):
|
||||
# Fallback to basic path-based filtering
|
||||
# or if there's an error processing the query
|
||||
# Normalize array access notation and handle direct key access
|
||||
filter_str = filter_.strip()
|
||||
normalized_query = "." + filter_str if not filter_str.startswith(".") else filter_str
|
||||
normalized_query = normalized_query.replace("[", ".[")
|
||||
path = normalized_query.strip().split(".")
|
||||
path = [p for p in path if p]
|
||||
|
||||
current = result
|
||||
for key in path:
|
||||
if current is None:
|
||||
return None
|
||||
|
||||
# Handle array access
|
||||
if key.startswith("[") and key.endswith("]"):
|
||||
try:
|
||||
index = int(key[1:-1])
|
||||
if not isinstance(current, list) or index >= len(current):
|
||||
return None
|
||||
current = current[index]
|
||||
except (ValueError, TypeError):
|
||||
return None
|
||||
# Handle object access
|
||||
elif isinstance(current, dict):
|
||||
if key not in current:
|
||||
return None
|
||||
current = current[key]
|
||||
# Handle array operation
|
||||
elif isinstance(current, list):
|
||||
try:
|
||||
current = [item[key] for item in current if isinstance(item, dict) and key in item]
|
||||
except (TypeError, KeyError):
|
||||
return None
|
||||
else:
|
||||
return None
|
||||
|
||||
return Data(data=current)
|
||||
|
|
|
|||
|
|
@ -81,6 +81,7 @@ dependencies = [
|
|||
"mcp>=1.1.2",
|
||||
"aiosqlite>=0.20.0",
|
||||
"greenlet>=3.1.1",
|
||||
"jsonquerylang>=1.1.1",
|
||||
"sqlalchemy[aiosqlite]>=2.0.38,<3.0.0",
|
||||
]
|
||||
|
||||
|
|
|
|||
1
src/backend/tests/unit/template/utils/__init__.py
Normal file
1
src/backend/tests/unit/template/utils/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
"""Utils tests package."""
|
||||
|
|
@ -0,0 +1,97 @@
|
|||
import pytest
|
||||
from hypothesis import given
|
||||
from hypothesis import strategies as st
|
||||
from langflow.schema.data import Data
|
||||
from langflow.template.utils import apply_json_filter
|
||||
|
||||
|
||||
# Helper function to create nested dictionaries
|
||||
def dict_strategy():
|
||||
return st.recursive(
|
||||
st.one_of(st.integers(), st.text(), st.floats(allow_nan=False, allow_infinity=False), st.booleans()),
|
||||
lambda children: st.lists(children, max_size=5) | st.dictionaries(st.text(), children, max_size=5),
|
||||
max_leaves=10,
|
||||
)
|
||||
|
||||
|
||||
# Test basic dictionary access
|
||||
@given(data=st.dictionaries(st.text(), st.integers()), key=st.text())
|
||||
def test_basic_dict_access(data, key):
|
||||
if key in data:
|
||||
result = apply_json_filter(data, key)
|
||||
assert result == data[key]
|
||||
else:
|
||||
result = apply_json_filter(data, key)
|
||||
assert result is None
|
||||
|
||||
|
||||
# Test array access
|
||||
@given(data=st.lists(st.integers(), min_size=1), index=st.integers())
|
||||
def test_array_access(data, index):
|
||||
filter_str = f"[{index}]"
|
||||
result = apply_json_filter(data, filter_str)
|
||||
if 0 <= index < len(data):
|
||||
assert result == data[index]
|
||||
else:
|
||||
assert result is None
|
||||
|
||||
|
||||
# Test nested object access
|
||||
@given(nested_data=dict_strategy())
|
||||
def test_nested_object_access(nested_data):
|
||||
# Wrap in Data object to test both raw and Data object inputs
|
||||
data_obj = Data(data=nested_data)
|
||||
result = apply_json_filter(data_obj, "")
|
||||
assert result == nested_data
|
||||
|
||||
|
||||
# Test edge cases
|
||||
@pytest.mark.parametrize(
|
||||
("input_data", "filter_str", "expected"),
|
||||
[
|
||||
({}, "", {}), # Empty dict, empty filter
|
||||
([], "", []), # Empty list, empty filter
|
||||
(None, "any.path", None), # None input
|
||||
({"a": 1}, None, {"a": 1}), # None filter
|
||||
({"a": 1}, " ", {"a": 1}), # Whitespace filter
|
||||
],
|
||||
)
|
||||
def test_edge_cases(input_data, filter_str, expected):
|
||||
result = apply_json_filter(input_data, filter_str)
|
||||
assert result == expected
|
||||
|
||||
|
||||
# Test complex nested access
|
||||
@given(data=st.dictionaries(keys=st.text(), values=st.dictionaries(keys=st.text(), values=st.lists(st.integers()))))
|
||||
def test_complex_nested_access(data):
|
||||
if data:
|
||||
outer_key = next(iter(data))
|
||||
if data[outer_key]:
|
||||
inner_key = next(iter(data[outer_key]))
|
||||
filter_str = f"{outer_key}.{inner_key}"
|
||||
result = apply_json_filter(data, filter_str)
|
||||
assert result == data[outer_key][inner_key]
|
||||
|
||||
|
||||
# Test array operations on objects
|
||||
@given(data=st.lists(st.dictionaries(keys=st.text(), values=st.integers(), min_size=1), min_size=1))
|
||||
def test_array_object_operations(data):
|
||||
if data and all(data):
|
||||
key = next(iter(data[0]))
|
||||
result = apply_json_filter(data, key)
|
||||
expected = [item[key] for item in data if key in item]
|
||||
assert result == expected
|
||||
|
||||
|
||||
# Test invalid inputs
|
||||
@pytest.mark.parametrize(
|
||||
("input_data", "filter_str"),
|
||||
[
|
||||
({"a": 1}, "[invalid]"), # Invalid array index
|
||||
([1, 2, 3], "nonexistent"), # Nonexistent key on array
|
||||
({"a": 1}, "..[invalid]"), # Invalid syntax
|
||||
],
|
||||
)
|
||||
def test_invalid_inputs(input_data, filter_str):
|
||||
result = apply_json_filter(input_data, filter_str)
|
||||
assert result is None or isinstance(result, dict | list | Data)
|
||||
3205
src/frontend/package-lock.json
generated
3205
src/frontend/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -67,13 +67,11 @@
|
|||
"react-hook-form": "^7.52.0",
|
||||
"react-hotkeys-hook": "^4.5.0",
|
||||
"react-icons": "^5.2.1",
|
||||
"react-json-view-lite": "^1.5.0",
|
||||
"react-laag": "^2.0.5",
|
||||
"react-markdown": "^8.0.7",
|
||||
"react-pdf": "^9.0.0",
|
||||
"react-router-dom": "^6.23.1",
|
||||
"react-syntax-highlighter": "^15.5.0",
|
||||
"react18-json-view": "^0.2.8",
|
||||
"reactflow": "^11.11.3",
|
||||
"rehype-mathjax": "^4.0.3",
|
||||
"rehype-raw": "^6.1.1",
|
||||
|
|
@ -84,6 +82,7 @@
|
|||
"tailwind-merge": "^2.3.0",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"uuid": "^10.0.0",
|
||||
"vanilla-jsoneditor": "^2.3.3",
|
||||
"vite-plugin-svgr": "^4.2.0",
|
||||
"vite-tsconfig-paths": "^4.3.2",
|
||||
"web-vitals": "^4.1.1",
|
||||
|
|
|
|||
|
|
@ -219,3 +219,33 @@ code {
|
|||
box-shadow: none !important;
|
||||
border-radius: 0px !important;
|
||||
}
|
||||
|
||||
.jse-space {
|
||||
border-radius: var(--radius) !important;
|
||||
}
|
||||
.jse-button.jse-first {
|
||||
border-radius: 5px !important;
|
||||
border-top-right-radius: 0px !important;
|
||||
border-bottom-right-radius: 0px !important;
|
||||
}
|
||||
.jse-group-button.jse-button:not(.jse-first) {
|
||||
border-radius: 5px !important;
|
||||
border-top-left-radius: 0px !important;
|
||||
border-bottom-left-radius: 0px !important;
|
||||
border-right: solid !important;
|
||||
}
|
||||
.jse-group-button.jse-button.jse-selected:not(.jse-first) {
|
||||
border-right: none !important;
|
||||
}
|
||||
|
||||
.jse-menu .jse-button.jse-group-button {
|
||||
border: 1px solid var(--jse-menu-color, var(--jse-text-color-inverse, #fff)) !important;
|
||||
}
|
||||
|
||||
.jse-menu .jse-separator {
|
||||
margin-left: 10px !important;
|
||||
}
|
||||
|
||||
.cm-gutters {
|
||||
display: none !important;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import JsonOutputViewComponent from "@/components/core/jsonOutputComponent/json-output-view";
|
||||
import { MAX_TEXT_LENGTH } from "@/constants/constants";
|
||||
import { LogsLogType, OutputLogType } from "@/types/api";
|
||||
import { useMemo } from "react";
|
||||
|
|
@ -36,7 +37,8 @@ const SwitchOutputView: React.FC<SwitchOutputViewProps> = ({
|
|||
: flowPoolNode?.data?.logs?.[outputName]) ?? {};
|
||||
const resultType = results?.type;
|
||||
let resultMessage = results?.message ?? {};
|
||||
const RECORD_TYPES = ["data", "object", "array", "message"];
|
||||
const RECORD_TYPES = ["array", "message"];
|
||||
const JSON_TYPES = ["data", "object"];
|
||||
if (resultMessage?.raw) {
|
||||
resultMessage = resultMessage.raw;
|
||||
}
|
||||
|
|
@ -50,7 +52,6 @@ const SwitchOutputView: React.FC<SwitchOutputViewProps> = ({
|
|||
) {
|
||||
return `${resultMessage.substring(0, MAX_TEXT_LENGTH)}...`;
|
||||
}
|
||||
|
||||
if (Array.isArray(resultMessage)) {
|
||||
return resultMessage.map((item) => {
|
||||
if (item?.data && typeof item?.data === "object") {
|
||||
|
|
@ -105,6 +106,13 @@ const SwitchOutputView: React.FC<SwitchOutputViewProps> = ({
|
|||
columnMode="union"
|
||||
/>
|
||||
</Case>
|
||||
<Case condition={JSON_TYPES.includes(resultType)}>
|
||||
<JsonOutputViewComponent
|
||||
nodeId={nodeId}
|
||||
outputName={outputName}
|
||||
data={resultMessageMemoized}
|
||||
/>
|
||||
</Case>
|
||||
|
||||
<Case condition={resultType === "stream"}>
|
||||
<div className="flex h-full w-full items-center justify-center align-middle">
|
||||
|
|
|
|||
|
|
@ -203,9 +203,7 @@ export default function Dropdown({
|
|||
className="h-4 w-4"
|
||||
/>
|
||||
)}
|
||||
{value && filteredOptions.includes(value)
|
||||
? value
|
||||
: placeholderName}{" "}
|
||||
{value && filteredOptions.includes(value) ? value : placeholderName}{" "}
|
||||
</span>
|
||||
<ForwardedIconComponent
|
||||
name="ChevronsUpDown"
|
||||
|
|
|
|||
410
src/frontend/src/components/core/jsonEditor/index.tsx
Normal file
410
src/frontend/src/components/core/jsonEditor/index.tsx
Normal file
|
|
@ -0,0 +1,410 @@
|
|||
import { jsonquery } from "@jsonquerylang/jsonquery";
|
||||
import { Check } from "lucide-react";
|
||||
import { KeyboardEvent, useEffect, useRef, useState } from "react";
|
||||
import {
|
||||
Content,
|
||||
createJSONEditor,
|
||||
JsonEditor as VanillaJsonEditor,
|
||||
} from "vanilla-jsoneditor";
|
||||
import useAlertStore from "../../../stores/alertStore";
|
||||
import { cn } from "../../../utils/utils";
|
||||
import { Button } from "../../ui/button";
|
||||
import { Input } from "../../ui/input";
|
||||
|
||||
interface JsonEditorProps {
|
||||
data?: Content;
|
||||
onChange?: (data: Content) => void;
|
||||
readOnly?: boolean;
|
||||
options?: any;
|
||||
jsonRef?: React.MutableRefObject<VanillaJsonEditor | null>;
|
||||
width?: string;
|
||||
height?: string;
|
||||
className?: string;
|
||||
setFilter?: (filter: string) => void;
|
||||
allowFilter?: boolean;
|
||||
initialFilter?: string;
|
||||
}
|
||||
|
||||
const JsonEditor = ({
|
||||
data = { json: {} },
|
||||
onChange,
|
||||
readOnly,
|
||||
jsonRef,
|
||||
options = {},
|
||||
width = "100%",
|
||||
height = "400px",
|
||||
className,
|
||||
setFilter,
|
||||
allowFilter = false,
|
||||
initialFilter,
|
||||
}: JsonEditorProps) => {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const jsonEditorRef = useRef<VanillaJsonEditor | null>(null);
|
||||
const setErrorData = useAlertStore((state) => state.setErrorData);
|
||||
const newRef = jsonRef ?? jsonEditorRef;
|
||||
const [transformQuery, setTransformQuery] = useState(initialFilter ?? "");
|
||||
const [originalData, setOriginalData] = useState(data);
|
||||
const [isFiltered, setIsFiltered] = useState(false);
|
||||
const [showSuccess, setShowSuccess] = useState(false);
|
||||
|
||||
// Apply initial filter when component mounts
|
||||
useEffect(() => {
|
||||
if (initialFilter && newRef.current) {
|
||||
setTransformQuery(initialFilter);
|
||||
handleTransform(true);
|
||||
}
|
||||
}, [initialFilter, newRef.current]);
|
||||
|
||||
const isValidResult = (result: any): boolean => {
|
||||
// Only allow objects and arrays
|
||||
return (
|
||||
result !== null &&
|
||||
(Array.isArray(result) ||
|
||||
(typeof result === "object" && !Array.isArray(result)))
|
||||
);
|
||||
};
|
||||
|
||||
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setTransformQuery(e.target.value);
|
||||
setIsFiltered(false);
|
||||
setShowSuccess(false);
|
||||
};
|
||||
|
||||
const applyFilter = (filtered: { json: any }, query: string) => {
|
||||
onChange?.(filtered);
|
||||
setFilter?.(query.trim());
|
||||
setShowSuccess(true);
|
||||
setTimeout(() => {
|
||||
setShowSuccess(false);
|
||||
}, 5000);
|
||||
};
|
||||
|
||||
const handleTransform = (isInitial = false) => {
|
||||
if (!newRef.current) return;
|
||||
|
||||
// If query is empty, act as reset
|
||||
if (!transformQuery.trim()) {
|
||||
handleReset();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Always start with original data for transformation
|
||||
const json =
|
||||
"json" in originalData
|
||||
? originalData.json
|
||||
: JSON.parse(originalData.text!);
|
||||
|
||||
// Try JSONQuery first
|
||||
try {
|
||||
const result = jsonquery(json, transformQuery);
|
||||
if (result !== undefined) {
|
||||
// Validate that result is a JSON object or array
|
||||
if (isValidResult(result)) {
|
||||
try {
|
||||
JSON.stringify(result); // Still check JSON serializability
|
||||
const filteredContent = { json: result };
|
||||
newRef.current.set(filteredContent);
|
||||
if (isFiltered && !isInitial) {
|
||||
// Apply the filter
|
||||
applyFilter(filteredContent, transformQuery.trim());
|
||||
} else {
|
||||
// Just preview the filter
|
||||
setIsFiltered(true);
|
||||
}
|
||||
return;
|
||||
} catch (jsonError) {
|
||||
setErrorData({
|
||||
title: "Invalid Result",
|
||||
list: [
|
||||
"The filtered result contains values that cannot be serialized to JSON",
|
||||
],
|
||||
});
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
setErrorData({
|
||||
title: "Invalid Result",
|
||||
list: [
|
||||
"The filtered result must be a JSON object or array, not a primitive value",
|
||||
],
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
} catch (jsonQueryError) {
|
||||
// If JSONQuery fails, continue with our path-based method
|
||||
console.debug(
|
||||
"JSONQuery parsing failed, falling back to path-based method:",
|
||||
jsonQueryError,
|
||||
);
|
||||
}
|
||||
|
||||
// Fallback to our path-based method
|
||||
const normalizedQuery = transformQuery.replace(/\[/g, ".[");
|
||||
const path = normalizedQuery.trim().split(".").filter(Boolean);
|
||||
let result = json;
|
||||
|
||||
for (const key of path) {
|
||||
if (result === undefined || result === null) {
|
||||
setErrorData({
|
||||
title: "Invalid Path",
|
||||
list: [`Path '${transformQuery}' led to undefined or null value`],
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (Array.isArray(result)) {
|
||||
// Handle array access with [index] notation
|
||||
const indexMatch = key.match(/\[(\d+)\]/);
|
||||
if (indexMatch) {
|
||||
const index = parseInt(indexMatch[1]);
|
||||
if (index >= result.length) {
|
||||
setErrorData({
|
||||
title: "Invalid Array Index",
|
||||
list: [
|
||||
`Index ${index} is out of bounds for array of length ${result.length}`,
|
||||
],
|
||||
});
|
||||
return;
|
||||
}
|
||||
result = result[index];
|
||||
continue;
|
||||
}
|
||||
// Apply operation to all array items
|
||||
result = result
|
||||
.map((item) => {
|
||||
if (!(key in item)) {
|
||||
setErrorData({
|
||||
title: "Invalid Property",
|
||||
list: [`Property '${key}' does not exist in array items`],
|
||||
});
|
||||
return undefined;
|
||||
}
|
||||
return item[key];
|
||||
})
|
||||
.filter((item) => item !== undefined);
|
||||
} else {
|
||||
if (!(key in result)) {
|
||||
setErrorData({
|
||||
title: "Invalid Property",
|
||||
list: [`Property '${key}' does not exist in object`],
|
||||
});
|
||||
return;
|
||||
}
|
||||
result = result[key];
|
||||
}
|
||||
}
|
||||
|
||||
if (result !== undefined) {
|
||||
// Validate that result is a JSON object or array
|
||||
if (isValidResult(result)) {
|
||||
try {
|
||||
JSON.stringify(result); // Still check JSON serializability
|
||||
const filteredContent = { json: result };
|
||||
newRef.current.set(filteredContent);
|
||||
|
||||
if (isFiltered && !isInitial) {
|
||||
// Apply the filter
|
||||
applyFilter(filteredContent, transformQuery.trim());
|
||||
} else {
|
||||
// Just preview the filter
|
||||
setIsFiltered(true);
|
||||
}
|
||||
return;
|
||||
} catch (jsonError) {
|
||||
setErrorData({
|
||||
title: "Invalid Result",
|
||||
list: [
|
||||
"The filtered result contains values that cannot be serialized to JSON",
|
||||
],
|
||||
});
|
||||
}
|
||||
} else {
|
||||
setErrorData({
|
||||
title: "Invalid Result",
|
||||
list: [
|
||||
"The filtered result must be a JSON object or array, not a primitive value",
|
||||
],
|
||||
});
|
||||
}
|
||||
} else {
|
||||
setErrorData({
|
||||
title: "Invalid Result",
|
||||
list: ["Transform resulted in undefined value"],
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error applying transform:", error);
|
||||
setErrorData({
|
||||
title: "Transform Error",
|
||||
list: [(error as Error).message],
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
if (!newRef.current) return;
|
||||
newRef.current.set(originalData);
|
||||
onChange?.(originalData);
|
||||
setTransformQuery("");
|
||||
setFilter?.("");
|
||||
setIsFiltered(false);
|
||||
setShowSuccess(false);
|
||||
};
|
||||
|
||||
const handleKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
handleTransform();
|
||||
}
|
||||
};
|
||||
|
||||
const getFilteredContent = (
|
||||
sourceJson: any,
|
||||
query: string,
|
||||
): { json: any } | undefined => {
|
||||
// Try JSONQuery first
|
||||
try {
|
||||
const result = jsonquery(sourceJson, query);
|
||||
if (result !== undefined && isValidResult(result)) {
|
||||
try {
|
||||
JSON.stringify(result); // Check serializability
|
||||
return { json: result };
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
} catch (jsonQueryError) {
|
||||
console.debug(
|
||||
"JSONQuery parsing failed, falling back to path-based method:",
|
||||
jsonQueryError,
|
||||
);
|
||||
}
|
||||
|
||||
// Fallback to path-based method
|
||||
try {
|
||||
const normalizedQuery = query.replace(/\[/g, ".[");
|
||||
const path = normalizedQuery.trim().split(".").filter(Boolean);
|
||||
let result = sourceJson;
|
||||
|
||||
for (const key of path) {
|
||||
if (result === undefined || result === null) return undefined;
|
||||
if (Array.isArray(result)) {
|
||||
const indexMatch = key.match(/\[(\d+)\]/);
|
||||
if (indexMatch) {
|
||||
const index = parseInt(indexMatch[1]);
|
||||
if (index >= result.length) return undefined;
|
||||
result = result[index];
|
||||
continue;
|
||||
}
|
||||
result = result
|
||||
.map((item) => (key in item ? item[key] : undefined))
|
||||
.filter((item) => item !== undefined);
|
||||
} else {
|
||||
if (!(key in result)) return undefined;
|
||||
result = result[key];
|
||||
}
|
||||
}
|
||||
|
||||
if (result !== undefined && isValidResult(result)) {
|
||||
try {
|
||||
JSON.stringify(result);
|
||||
return { json: result };
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!containerRef.current) return;
|
||||
|
||||
let initialContent = data;
|
||||
if (initialFilter?.trim()) {
|
||||
try {
|
||||
const json = "json" in data ? data.json : JSON.parse(data.text!);
|
||||
const filtered = getFilteredContent(json, initialFilter);
|
||||
if (filtered) {
|
||||
initialContent = filtered;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error applying initial filter:", error);
|
||||
}
|
||||
}
|
||||
|
||||
const editor = createJSONEditor({
|
||||
target: containerRef.current,
|
||||
props: {
|
||||
...options,
|
||||
navigationBar: false,
|
||||
mode: "text",
|
||||
content: initialContent,
|
||||
readOnly,
|
||||
onChange: (content) => {
|
||||
onChange?.(content);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
setTimeout(() => editor.focus(), 100);
|
||||
|
||||
newRef.current = editor;
|
||||
setOriginalData(data);
|
||||
|
||||
return () => {
|
||||
if (newRef.current) {
|
||||
newRef.current.destroy();
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="flex min-h-0 flex-1 flex-col">
|
||||
{allowFilter && (
|
||||
<div className="mb-2 flex shrink-0 gap-2">
|
||||
<Input
|
||||
placeholder="Enter path (e.g. users[0].name) or JSONQuery (e.g. .users | filter(.age > 25))"
|
||||
value={transformQuery}
|
||||
onChange={handleInputChange}
|
||||
onKeyDown={handleKeyDown}
|
||||
className="font-mono text-sm"
|
||||
/>
|
||||
<Button
|
||||
onClick={() => handleTransform()}
|
||||
variant="primary"
|
||||
size="sm"
|
||||
className={cn(
|
||||
"min-w-[60px] whitespace-nowrap",
|
||||
showSuccess && "!bg-green-500 hover:!bg-green-600",
|
||||
)}
|
||||
>
|
||||
{showSuccess ? (
|
||||
<Check className="h-4 w-4" />
|
||||
) : isFiltered ? (
|
||||
"Apply"
|
||||
) : (
|
||||
"Filter"
|
||||
)}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleReset}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="whitespace-nowrap"
|
||||
>
|
||||
Clear
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
<div className="relative min-h-0 flex-1">
|
||||
<div ref={containerRef} className={cn("absolute inset-0", className)} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default JsonEditor;
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
import useFlowStore from "@/stores/flowStore";
|
||||
import { APIClassType } from "@/types/api";
|
||||
import React from "react";
|
||||
import JsonEditor from "../jsonEditor";
|
||||
|
||||
interface JsonOutputViewComponentProps {
|
||||
data: string | object;
|
||||
width?: string;
|
||||
height?: string;
|
||||
nodeId: string;
|
||||
outputName: string;
|
||||
}
|
||||
|
||||
const JsonOutputViewComponent: React.FC<JsonOutputViewComponentProps> = ({
|
||||
data,
|
||||
nodeId,
|
||||
outputName,
|
||||
}) => {
|
||||
const jsonData = typeof data === "string" ? JSON.parse(data) : data;
|
||||
const setNode = useFlowStore((state) => state.setNode);
|
||||
const node = useFlowStore((state) => state.getNode(nodeId));
|
||||
const outputs = (node?.data.node as APIClassType)?.outputs;
|
||||
const output = outputs?.find((o) => o.name === outputName);
|
||||
const initialFilter = output?.options?.filter;
|
||||
|
||||
return (
|
||||
<div className="flex flex-1 flex-col">
|
||||
<JsonEditor
|
||||
data={{ json: jsonData }}
|
||||
readOnly={true}
|
||||
className="flex-1 rounded border border-border"
|
||||
setFilter={(filter) => {
|
||||
setNode(nodeId, (old) => {
|
||||
const outputs = (old.data.node as APIClassType).outputs;
|
||||
const output = outputs?.find((o) => o.name === outputName);
|
||||
if (output) {
|
||||
output.options = {
|
||||
...output.options,
|
||||
filter: filter !== "" ? filter : undefined,
|
||||
};
|
||||
}
|
||||
return {
|
||||
...old,
|
||||
data: {
|
||||
...old.data,
|
||||
node: {
|
||||
...old.data.node,
|
||||
outputs: outputs,
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
}}
|
||||
allowFilter={true}
|
||||
initialFilter={initialFilter}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default JsonOutputViewComponent;
|
||||
|
|
@ -8,7 +8,7 @@ import { Button } from "../../../../ui/button";
|
|||
import { InputProps } from "../../types";
|
||||
|
||||
export default function DictComponent({
|
||||
value = [],
|
||||
value,
|
||||
handleOnNewValue,
|
||||
disabled,
|
||||
editNode = false,
|
||||
|
|
@ -16,7 +16,7 @@ export default function DictComponent({
|
|||
name = "",
|
||||
}: InputProps<object | object[] | string, { name: string }>): JSX.Element {
|
||||
useEffect(() => {
|
||||
if (disabled) {
|
||||
if (disabled || value === null) {
|
||||
handleOnNewValue({ value: {} }, { skipSnapshot: true });
|
||||
}
|
||||
}, [disabled]);
|
||||
|
|
|
|||
|
|
@ -1,44 +1,30 @@
|
|||
import JsonEditor from "@/components/core/jsonEditor";
|
||||
import { IOJSONInputComponentType } from "@/types/components";
|
||||
import { useEffect, useRef } from "react";
|
||||
import JsonView from "react18-json-view";
|
||||
import { useDarkStore } from "../../../../../stores/darkStore";
|
||||
|
||||
import { JsonEditor as VanillaJsonEditor } from "vanilla-jsoneditor";
|
||||
export default function IoJsonInput({
|
||||
value = [],
|
||||
onChange,
|
||||
left,
|
||||
output,
|
||||
}: IOJSONInputComponentType): JSX.Element {
|
||||
useEffect(() => {
|
||||
if (value) onChange(value);
|
||||
}, [value]);
|
||||
const isDark = useDarkStore((state) => state.dark);
|
||||
|
||||
const ref = useRef<any>(null);
|
||||
ref.current = value;
|
||||
|
||||
const getClassNames = () => {
|
||||
if (!isDark && !left) return "json-view-playground-white";
|
||||
if (!isDark && left) return "json-view-playground-white-left";
|
||||
if (isDark && left) return "json-view-playground-dark-left";
|
||||
if (isDark && !left) return "json-view-playground-dark";
|
||||
};
|
||||
const jsonEditorRef = useRef<VanillaJsonEditor | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (jsonEditorRef.current) {
|
||||
jsonEditorRef.current.set({ json: value || {} });
|
||||
}
|
||||
}, [value]);
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
<JsonView
|
||||
className={getClassNames()}
|
||||
theme="vscode"
|
||||
dark={isDark}
|
||||
editable={!output}
|
||||
enableClipboard
|
||||
onEdit={(edit) => {
|
||||
ref.current = edit["src"];
|
||||
}}
|
||||
onChange={(edit) => {
|
||||
ref.current = edit["src"];
|
||||
}}
|
||||
src={ref.current}
|
||||
<div className="h-400px w-full">
|
||||
<JsonEditor
|
||||
data={{ json: value }}
|
||||
jsonRef={jsonEditorRef}
|
||||
height="400px"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -44,8 +44,8 @@ const Content: React.FC<ContentProps> = ({
|
|||
return (
|
||||
<div
|
||||
className={cn(
|
||||
`flex w-full flex-grow flex-col transition-all duration-300`,
|
||||
overflowHidden ? "overflow-hidden" : "overflow-visible",
|
||||
`flex flex-1 flex-col rounded-md transition-all duration-300`,
|
||||
overflowHidden ? "overflow-hidden" : "overflow-auto",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
|
|
@ -238,10 +238,12 @@ function BaseModal({
|
|||
const contentClasses = cn(
|
||||
minWidth,
|
||||
height,
|
||||
"flex flex-col duration-300 overflow-hidden",
|
||||
"flex flex-col flex-1 overflow-hidden",
|
||||
className,
|
||||
);
|
||||
|
||||
const formClasses = "flex flex-col flex-1 gap-6 overflow-hidden";
|
||||
|
||||
//UPDATE COLORS AND STYLE CLASSSES
|
||||
return (
|
||||
<>
|
||||
|
|
@ -251,7 +253,9 @@ function BaseModal({
|
|||
<ModalContent className={contentClasses}>{modalContent}</ModalContent>
|
||||
</Modal>
|
||||
) : type === "full-screen" ? (
|
||||
<div className="min-h-full w-full flex-1">{modalContent}</div>
|
||||
<div className="min-h-full w-full flex-1 overflow-hidden">
|
||||
{modalContent}
|
||||
</div>
|
||||
) : (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
{triggerChild}
|
||||
|
|
@ -267,7 +271,7 @@ function BaseModal({
|
|||
event.preventDefault();
|
||||
onSubmit();
|
||||
}}
|
||||
className="flex h-full flex-col gap-6"
|
||||
className={formClasses}
|
||||
>
|
||||
{modalContent}
|
||||
</Form.Root>
|
||||
|
|
|
|||
|
|
@ -1,16 +1,8 @@
|
|||
import "ace-builds/src-noconflict/ace";
|
||||
import "ace-builds/src-noconflict/ext-language_tools";
|
||||
import "ace-builds/src-noconflict/mode-python";
|
||||
import "ace-builds/src-noconflict/theme-github";
|
||||
import "ace-builds/src-noconflict/theme-twilight";
|
||||
// import "ace-builds/webpack-resolver";
|
||||
import { cloneDeep } from "lodash";
|
||||
import { useEffect, useState } from "react";
|
||||
import JsonView from "react18-json-view";
|
||||
import "react18-json-view/src/dark.css";
|
||||
import "react18-json-view/src/style.css";
|
||||
import useAlertStore from "@/stores/alertStore";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { JsonEditor as VanillaJsonEditor } from "vanilla-jsoneditor";
|
||||
import IconComponent from "../../components/common/genericIconComponent";
|
||||
import { useDarkStore } from "../../stores/darkStore";
|
||||
import JsonEditor from "../../components/core/jsonEditor";
|
||||
import BaseModal from "../baseModal";
|
||||
|
||||
export default function DictAreaModal({
|
||||
|
|
@ -25,41 +17,53 @@ export default function DictAreaModal({
|
|||
disabled?: boolean;
|
||||
}): JSX.Element {
|
||||
const [open, setOpen] = useState(false);
|
||||
const isDark = useDarkStore((state) => state.dark);
|
||||
const [componentValue, setComponentValue] = useState(value);
|
||||
|
||||
const setErrorData = useAlertStore((state) => state.setErrorData);
|
||||
("");
|
||||
const jsonEditorRef = useRef<VanillaJsonEditor | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
setComponentValue(value);
|
||||
if (jsonEditorRef.current) {
|
||||
jsonEditorRef.current.set({ json: value || {} });
|
||||
}
|
||||
}, [value, open]);
|
||||
|
||||
const handleSubmit = () => {
|
||||
if (onChange) {
|
||||
onChange(componentValue);
|
||||
setOpen(false);
|
||||
try {
|
||||
const componentValue = jsonEditorRef.current?.get() ?? { json: {} };
|
||||
const jsonValue =
|
||||
"json" in componentValue
|
||||
? JSON.parse(JSON.stringify(componentValue.json))
|
||||
: JSON.parse(componentValue.text!);
|
||||
|
||||
onChange(jsonValue);
|
||||
setOpen(false);
|
||||
} catch (error) {
|
||||
console.error("Error getting JSON:", error);
|
||||
setErrorData({
|
||||
title: "Error getting dictionary",
|
||||
list: ["Check your dictionary format"],
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleJsonChange = (edit) => {
|
||||
setComponentValue(edit.src);
|
||||
};
|
||||
|
||||
const customizeCopy = (copy) => {
|
||||
navigator.clipboard.writeText(JSON.stringify(copy));
|
||||
};
|
||||
|
||||
const handleChangeType = (type: "array" | "object") => {
|
||||
setComponentValue((value) => {
|
||||
if (type === "array") {
|
||||
if (value && Object.keys(value).length > 0) {
|
||||
return [value];
|
||||
}
|
||||
return [];
|
||||
jsonEditorRef?.current?.set(typeChanged(type));
|
||||
};
|
||||
|
||||
const typeChanged = (type: "array" | "object") => {
|
||||
if (type === "array") {
|
||||
if (value && Object.keys(value).length > 0) {
|
||||
return { json: [value] };
|
||||
}
|
||||
if (value && Array.isArray(value) && value.length > 0) {
|
||||
return value[0];
|
||||
}
|
||||
return {};
|
||||
});
|
||||
return { json: [] };
|
||||
}
|
||||
if (value && Array.isArray(value) && value.length > 0) {
|
||||
return { json: value[0] };
|
||||
}
|
||||
return { json: {} };
|
||||
};
|
||||
|
||||
const IteractiveReader = () => {
|
||||
|
|
@ -100,13 +104,10 @@ export default function DictAreaModal({
|
|||
const renderContent = () => (
|
||||
<BaseModal.Content>
|
||||
<div className="flex h-full w-full flex-col transition-all">
|
||||
<JsonView
|
||||
theme="vscode"
|
||||
editable={!!onChange}
|
||||
enableClipboard
|
||||
onChange={handleJsonChange}
|
||||
src={cloneDeep(componentValue)}
|
||||
customizeCopy={customizeCopy}
|
||||
<JsonEditor
|
||||
data={{ json: value }}
|
||||
jsonRef={jsonEditorRef}
|
||||
height="400px"
|
||||
/>
|
||||
</div>
|
||||
</BaseModal.Content>
|
||||
|
|
@ -118,6 +119,7 @@ export default function DictAreaModal({
|
|||
open={open}
|
||||
disable={disabled}
|
||||
setOpen={setOpen}
|
||||
className="overflow-visible"
|
||||
onSubmit={onChange ? handleSubmit : undefined}
|
||||
>
|
||||
<BaseModal.Trigger className="h-full" asChild>
|
||||
|
|
|
|||
|
|
@ -5,8 +5,6 @@ import "ace-builds/src-noconflict/theme-github";
|
|||
import "ace-builds/src-noconflict/theme-twilight";
|
||||
// import "ace-builds/webpack-resolver";
|
||||
import { useState } from "react";
|
||||
import "react18-json-view/src/dark.css";
|
||||
import "react18-json-view/src/style.css";
|
||||
import IconComponent from "../../components/common/genericIconComponent";
|
||||
import { Button } from "../../components/ui/button";
|
||||
import BaseModal from "../baseModal";
|
||||
|
|
|
|||
|
|
@ -291,6 +291,21 @@ input[type="search"]::-webkit-search-cancel-button {
|
|||
min-width: 78px;
|
||||
}
|
||||
|
||||
.jse-group-button.jse-last,
|
||||
.jse-button.jse-sort,
|
||||
.jse-dropdown-button:last-child,
|
||||
.jse-button.jse-transform {
|
||||
display: none !important;
|
||||
}
|
||||
.jse-search-box-background {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.jse-search-box-container {
|
||||
position: absolute !important;
|
||||
right: 0 !important;
|
||||
}
|
||||
|
||||
.linenumber {
|
||||
font-style: normal !important;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -44,6 +44,24 @@
|
|||
--tooltip: 0 0% 0%; /* hsl(0, 0%, 0%) */
|
||||
--tooltip-foreground: 0 0% 100%; /* hsl(0, 0%, 100%) */
|
||||
|
||||
--jse-theme-color: hsl(0, 0%, 100%);
|
||||
--jse-theme-color-highlight: hsl(240, 6%, 90%);
|
||||
--jse-menu-color: hsl(0, 0%, 0%);
|
||||
--jse-main-border: hsl(240, 6%, 90%);
|
||||
--jse-background-color: hsl(0, 0%, 100%);
|
||||
--jse-text-color: hsl(0, 0%, 0%);
|
||||
--jse-selection-background-color: hsl(240, 5%, 96%);
|
||||
--jse-selection-background-inactive-color: hsl(240, 6%, 90%);
|
||||
--jse-hover-background-color: hsl(240, 5%, 96%);
|
||||
--jse-active-line-background-color: hsl(240, 5%, 96%);
|
||||
--jse-search-match-background-color: hsl(240, 5%, 96%);
|
||||
--jse-search-match-color: hsl(243, 75%, 59%);
|
||||
--jse-text-readonly: hsl(240, 4%, 46%);
|
||||
--jse-error-color: hsl(0, 72%, 51%);
|
||||
--jse-warning-color: hsl(48, 96%, 89%);
|
||||
--jse-info-color: hsl(221, 83%, 53%);
|
||||
--jse-success-color: hsl(142, 76%, 36%);
|
||||
|
||||
--node-selected: 243 75% 59%;
|
||||
--round-btn-shadow: #00000063;
|
||||
--ice: #31a3cc;
|
||||
|
|
@ -207,6 +225,98 @@
|
|||
--accent-pink-foreground: 329 86% 70%; /* hsl(329, 86%, 70%) */
|
||||
--tooltip: 0 0% 100%; /* hsl(0, 0%, 100%) */
|
||||
|
||||
--jse-theme-color: hsl(240, 6%, 10%) !important;
|
||||
--jse-theme-color-highlight: hsl(240, 4%, 16%);
|
||||
--jse-panel-background: hsl(240, 4%, 16%);
|
||||
--jse-menu-color: hsl(240, 6%, 90%);
|
||||
--jse-main-border: hsl(240, 5%, 26%);
|
||||
--jse-background-color: hsl(0, 0%, 0%);
|
||||
--jse-text-color: hsl(0, 0%, 100%);
|
||||
--jse-selection-background-color: hsl(240, 4%, 16%);
|
||||
--jse-selection-background-inactive-color: hsl(240, 5%, 26%);
|
||||
--jse-hover-background-color: hsl(240, 4%, 16%);
|
||||
--jse-active-line-background-color: hsl(240, 4%, 16%);
|
||||
--jse-search-match-background-color: hsl(240, 4%, 16%);
|
||||
--jse-search-match-color: hsl(234, 89%, 74%);
|
||||
--jse-text-readonly: hsl(240, 5%, 65%);
|
||||
--jse-error-color: hsl(0, 84%, 60%);
|
||||
--jse-warning-color: hsl(45, 97%, 65%);
|
||||
--jse-info-color: hsl(221, 83%, 53%);
|
||||
--jse-success-color: hsl(142, 76%, 36%);
|
||||
|
||||
--jse-theme: dark;
|
||||
--jse-text-color-inverse: #4d4d4d;
|
||||
--jse-modal-background: #2f2f2f;
|
||||
--jse-modal-overlay-background: rgba(0, 0, 0, 0.5);
|
||||
--jse-modal-code-background: #2f2f2f;
|
||||
--jse-tooltip-color: var(--jse-text-color);
|
||||
--jse-tooltip-background: #4b4b4b;
|
||||
--jse-tooltip-border: 1px solid #737373;
|
||||
--jse-tooltip-action-button-color: inherit;
|
||||
--jse-tooltip-action-button-background: #737373;
|
||||
--jse-panel-background-border: 1px solid #464646;
|
||||
--jse-panel-color: var(--jse-text-color);
|
||||
--jse-panel-color-readonly: #737373;
|
||||
--jse-panel-border: 1px solid #3c3c3c;
|
||||
--jse-panel-button-color-highlight: #e5e5e5;
|
||||
--jse-panel-button-background-highlight: #464646;
|
||||
--jse-navigation-bar-background: #656565;
|
||||
--jse-navigation-bar-background-highlight: #7e7e7e;
|
||||
--jse-navigation-bar-dropdown-color: var(--jse-text-color);
|
||||
--jse-context-menu-background: #4b4b4b;
|
||||
--jse-context-menu-background-highlight: #595959;
|
||||
--jse-context-menu-separator-color: #595959;
|
||||
--jse-context-menu-color: var(--jse-text-color);
|
||||
--jse-context-menu-pointer-background: #737373;
|
||||
--jse-context-menu-pointer-background-highlight: #818181;
|
||||
--jse-context-menu-pointer-color: var(--jse-context-menu-color);
|
||||
--jse-key-color: #9cdcfe;
|
||||
--jse-value-color: var(--jse-text-color);
|
||||
--jse-value-color-number: #b5cea8;
|
||||
--jse-value-color-boolean: #569cd6;
|
||||
--jse-value-color-null: #569cd6;
|
||||
--jse-value-color-string: #ce9178;
|
||||
--jse-value-color-url: #ce9178;
|
||||
--jse-delimiter-color: #949494;
|
||||
--jse-edit-outline: 2px solid var(--jse-text-color);
|
||||
--jse-search-match-background-color: #343434;
|
||||
--jse-collapsed-items-background-color: #333333;
|
||||
--jse-collapsed-items-selected-background-color: #565656;
|
||||
--jse-collapsed-items-link-color: #b2b2b2;
|
||||
--jse-collapsed-items-link-color-highlight: #ec8477;
|
||||
--jse-search-match-color: #724c27;
|
||||
--jse-search-match-outline: 1px solid #966535;
|
||||
--jse-search-match-active-color: #9f6c39;
|
||||
--jse-search-match-active-outline: 1px solid #bb7f43;
|
||||
--jse-tag-background: #444444;
|
||||
--jse-tag-color: #bdbdbd;
|
||||
--jse-table-header-background: #333333;
|
||||
--jse-table-header-background-highlight: #424242;
|
||||
--jse-table-row-odd-background: rgba(255, 255, 255, 0.1);
|
||||
--jse-input-background: #3d3d3d;
|
||||
--jse-input-border: var(--jse-main-border);
|
||||
--jse-button-background: #808080;
|
||||
--jse-button-background-highlight: #7a7a7a;
|
||||
--jse-button-color: #e0e0e0;
|
||||
--jse-button-secondary-background: #494949;
|
||||
--jse-button-secondary-background-highlight: #5d5d5d;
|
||||
--jse-button-secondary-background-disabled: #9d9d9d;
|
||||
--jse-button-secondary-color: var(--jse-text-color);
|
||||
--jse-a-color: #55abff;
|
||||
--jse-a-color-highlight: #4387c9;
|
||||
--jse-svelte-select-background: #3d3d3d;
|
||||
--jse-svelte-select-border: 1px solid #4f4f4f;
|
||||
--list-background: #3d3d3d;
|
||||
--item-hover-bg: #505050;
|
||||
--multi-item-bg: #5b5b5b;
|
||||
--input-color: #d4d4d4;
|
||||
--multi-clear-bg: #8a8a8a;
|
||||
--multi-item-clear-icon-color: #d4d4d4;
|
||||
--multi-item-outline: 1px solid #696969;
|
||||
--list-shadow: 0 2px 8px 0 rgba(0, 0, 0, 0.4);
|
||||
--jse-color-picker-background: #656565;
|
||||
--jse-color-picker-border-box-shadow: #8c8c8c 0 0 0 1px;
|
||||
|
||||
--tooltip-foreground: 0 0% 0%; /* hsl(0, 0%, 0%) */
|
||||
--error-red: 0, 75%, 15%; /*hsla(0, 75%, 15%)*/
|
||||
--error-red-border: 0, 70%, 35%; /*hsla(0,70%,35%)*/
|
||||
|
|
|
|||
|
|
@ -106,6 +106,7 @@ export type OutputFieldType = {
|
|||
hidden?: boolean;
|
||||
proxy?: OutputFieldProxyType;
|
||||
allows_loop?: boolean;
|
||||
options?: { [key: string]: any };
|
||||
};
|
||||
export type errorsTypeAPI = {
|
||||
function: { errors: Array<string> };
|
||||
|
|
|
|||
|
|
@ -29,33 +29,22 @@ test(
|
|||
await adjustScreenView(page);
|
||||
|
||||
await page.getByTestId("dict_nesteddict_metadata").first().click();
|
||||
await page.getByText("{}").last().clear();
|
||||
|
||||
await page
|
||||
.getByText("{")
|
||||
.getByRole("textbox")
|
||||
.last()
|
||||
.hover()
|
||||
.then(async () => {
|
||||
await page.locator(".json-view--edit").first().click();
|
||||
await page.locator(".json-view--input").first().fill("keytest");
|
||||
await page.locator(".json-view--edit").first().click();
|
||||
.fill(
|
||||
'{"keytest": "proptest", "keytest1": "proptest1", "keytest2": "proptest2"}',
|
||||
);
|
||||
|
||||
await page.locator(".json-view--edit").first().click();
|
||||
await page.locator(".json-view--input").first().fill("keytest1");
|
||||
await page.locator(".json-view--edit").first().click();
|
||||
|
||||
await page.locator(".json-view--edit").first().click();
|
||||
await page.locator(".json-view--input").first().fill("keytest2");
|
||||
await page.locator(".json-view--edit").first().click();
|
||||
});
|
||||
|
||||
await page
|
||||
.locator(".json-view--pair")
|
||||
.first()
|
||||
.hover()
|
||||
.then(async () => {
|
||||
await page.locator(".json-view--edit").nth(2).click();
|
||||
await page.locator(".json-view--null").first().fill("proptest1");
|
||||
await page.locator(".json-view--edit").nth(2).click();
|
||||
});
|
||||
await page.getByTitle("Switch to tree mode (current mode: text)").click();
|
||||
expect(await page.getByText("keytest", { exact: true }).count()).toBe(1);
|
||||
expect(await page.getByText("keytest1", { exact: true }).count()).toBe(1);
|
||||
expect(await page.getByText("keytest2", { exact: true }).count()).toBe(1);
|
||||
expect(await page.getByText("proptest", { exact: true }).count()).toBe(1);
|
||||
expect(await page.getByText("proptest1", { exact: true }).count()).toBe(1);
|
||||
expect(await page.getByText("proptest2", { exact: true }).count()).toBe(1);
|
||||
|
||||
await page.getByText("Save").last().click();
|
||||
|
||||
|
|
@ -65,22 +54,25 @@ test(
|
|||
await page.getByTestId("advanced-button-modal").click();
|
||||
|
||||
await page.getByTestId("edit_dict_nesteddict_edit_metadata").last().click();
|
||||
await page.getByTitle("Switch to tree mode (current mode: text)").click();
|
||||
await page.waitForSelector(".jse-bracket", {
|
||||
timeout: 3000,
|
||||
});
|
||||
|
||||
expect(await page.getByText("keytest", { exact: true }).count()).toBe(1);
|
||||
expect(await page.getByText("keytest1", { exact: true }).count()).toBe(1);
|
||||
expect(await page.getByText("keytest2", { exact: true }).count()).toBe(1);
|
||||
expect(await page.getByText("proptest1").count()).toBe(1);
|
||||
expect(await page.getByText("proptest", { exact: true }).count()).toBe(1);
|
||||
expect(await page.getByText("proptest1", { exact: true }).count()).toBe(1);
|
||||
expect(await page.getByText("proptest2", { exact: true }).count()).toBe(1);
|
||||
|
||||
await page
|
||||
.locator(".json-view--pair")
|
||||
.first()
|
||||
.hover()
|
||||
.then(async () => {
|
||||
await page.locator(".json-view--edit").nth(3).click();
|
||||
await page.locator(".json-view--edit").nth(2).click();
|
||||
});
|
||||
.getByText("proptest", { exact: true })
|
||||
.last()
|
||||
.click({ button: "right" });
|
||||
await page.getByText("Remove").last().click();
|
||||
|
||||
expect(await page.getByText("keytest", { exact: true }).count()).toBe(0);
|
||||
expect(await page.getByText("proptest1").count()).toBe(0);
|
||||
expect(await page.getByText("proptest", { exact: true }).count()).toBe(0);
|
||||
},
|
||||
);
|
||||
|
|
|
|||
47
uv.lock
generated
47
uv.lock
generated
|
|
@ -634,7 +634,7 @@ name = "blessed"
|
|||
version = "1.20.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "jinxed", marker = "sys_platform == 'win32'" },
|
||||
{ name = "jinxed", marker = "platform_system == 'Windows'" },
|
||||
{ name = "six" },
|
||||
{ name = "wcwidth" },
|
||||
]
|
||||
|
|
@ -1140,7 +1140,7 @@ name = "click"
|
|||
version = "8.1.8"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||
{ name = "colorama", marker = "platform_system == 'Windows'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593 }
|
||||
wheels = [
|
||||
|
|
@ -3436,7 +3436,7 @@ name = "ipykernel"
|
|||
version = "6.29.5"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "appnope", marker = "sys_platform == 'darwin'" },
|
||||
{ name = "appnope", marker = "platform_system == 'Darwin'" },
|
||||
{ name = "comm" },
|
||||
{ name = "debugpy" },
|
||||
{ name = "ipython" },
|
||||
|
|
@ -3545,7 +3545,7 @@ name = "jinxed"
|
|||
version = "1.3.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "ansicon", marker = "sys_platform == 'win32'" },
|
||||
{ name = "ansicon", marker = "platform_system == 'Windows'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/20/d0/59b2b80e7a52d255f9e0ad040d2e826342d05580c4b1d7d7747cfb8db731/jinxed-1.3.0.tar.gz", hash = "sha256:1593124b18a41b7a3da3b078471442e51dbad3d77b4d4f2b0c26ab6f7d660dbf", size = 80981 }
|
||||
wheels = [
|
||||
|
|
@ -3724,6 +3724,15 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/71/92/5e77f98553e9e75130c78900d000368476aed74276eb8ae8796f65f00918/jsonpointer-3.0.0-py2.py3-none-any.whl", hash = "sha256:13e088adc14fca8b6aa8177c044e12701e6ad4b28ff10e65f2267a90109c9942", size = 7595 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jsonquerylang"
|
||||
version = "1.1.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a1/49/2bc349923d322ef682391c149b59f1e766356b3c98e340233a3cc8c5edc9/jsonquerylang-1.1.1.tar.gz", hash = "sha256:b66cc9cae12135fe9fb7f6e2d4f1f703685806223a180d85d695afa414b9d2ba", size = 13573 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/37/8a/d4ef935f41b13b617fb72a27a2ca7f0d950cb777fabe4e57b121ed688195/jsonquerylang-1.1.1-py3-none-any.whl", hash = "sha256:40906259692439fce4da345f9175e455a820b38c87d738b8bd9567078644f3d8", size = 12724 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jsonref"
|
||||
version = "1.1.0"
|
||||
|
|
@ -4610,6 +4619,7 @@ dependencies = [
|
|||
{ name = "httpx", extra = ["http2"] },
|
||||
{ name = "jq", marker = "sys_platform != 'win32'" },
|
||||
{ name = "json-repair" },
|
||||
{ name = "jsonquerylang" },
|
||||
{ name = "langchain" },
|
||||
{ name = "langchain-community" },
|
||||
{ name = "langchain-core" },
|
||||
|
|
@ -4746,6 +4756,7 @@ requires-dist = [
|
|||
{ name = "httpx", extras = ["http2"], specifier = ">=0.27,<1.0.0" },
|
||||
{ name = "jq", marker = "sys_platform != 'win32'", specifier = ">=1.7.0,<2.0.0" },
|
||||
{ name = "json-repair", specifier = ">=0.30.3" },
|
||||
{ name = "jsonquerylang", specifier = ">=1.1.1" },
|
||||
{ name = "langchain", specifier = "~=0.3.10" },
|
||||
{ name = "langchain-community", specifier = "~=0.3.10" },
|
||||
{ name = "langchain-core", specifier = "~=0.3.15" },
|
||||
|
|
@ -6737,7 +6748,7 @@ name = "portalocker"
|
|||
version = "2.10.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "pywin32", marker = "sys_platform == 'win32'" },
|
||||
{ name = "pywin32", marker = "platform_system == 'Windows'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ed/d3/c6c64067759e87af98cc668c1cc75171347d0f1577fab7ca3749134e3cd4/portalocker-2.10.1.tar.gz", hash = "sha256:ef1bf844e878ab08aee7e40184156e1151f228f103aa5c6bd0724cc330960f8f", size = 40891 }
|
||||
wheels = [
|
||||
|
|
@ -9218,19 +9229,19 @@ dependencies = [
|
|||
{ name = "fsspec" },
|
||||
{ name = "jinja2" },
|
||||
{ name = "networkx" },
|
||||
{ name = "nvidia-cublas-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
|
||||
{ name = "nvidia-cuda-cupti-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
|
||||
{ name = "nvidia-cuda-nvrtc-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
|
||||
{ name = "nvidia-cuda-runtime-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
|
||||
{ name = "nvidia-cudnn-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
|
||||
{ name = "nvidia-cufft-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
|
||||
{ name = "nvidia-curand-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
|
||||
{ name = "nvidia-cusolver-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
|
||||
{ name = "nvidia-cusparse-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
|
||||
{ name = "nvidia-nccl-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
|
||||
{ name = "nvidia-nvtx-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
|
||||
{ name = "nvidia-cublas-cu12", marker = "platform_machine == 'x86_64' and platform_system == 'Linux'" },
|
||||
{ name = "nvidia-cuda-cupti-cu12", marker = "platform_machine == 'x86_64' and platform_system == 'Linux'" },
|
||||
{ name = "nvidia-cuda-nvrtc-cu12", marker = "platform_machine == 'x86_64' and platform_system == 'Linux'" },
|
||||
{ name = "nvidia-cuda-runtime-cu12", marker = "platform_machine == 'x86_64' and platform_system == 'Linux'" },
|
||||
{ name = "nvidia-cudnn-cu12", marker = "platform_machine == 'x86_64' and platform_system == 'Linux'" },
|
||||
{ name = "nvidia-cufft-cu12", marker = "platform_machine == 'x86_64' and platform_system == 'Linux'" },
|
||||
{ name = "nvidia-curand-cu12", marker = "platform_machine == 'x86_64' and platform_system == 'Linux'" },
|
||||
{ name = "nvidia-cusolver-cu12", marker = "platform_machine == 'x86_64' and platform_system == 'Linux'" },
|
||||
{ name = "nvidia-cusparse-cu12", marker = "platform_machine == 'x86_64' and platform_system == 'Linux'" },
|
||||
{ name = "nvidia-nccl-cu12", marker = "platform_machine == 'x86_64' and platform_system == 'Linux'" },
|
||||
{ name = "nvidia-nvtx-cu12", marker = "platform_machine == 'x86_64' and platform_system == 'Linux'" },
|
||||
{ name = "sympy" },
|
||||
{ name = "triton", marker = "python_full_version < '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux'" },
|
||||
{ name = "triton", marker = "python_full_version < '3.13' and platform_machine == 'x86_64' and platform_system == 'Linux'" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
wheels = [
|
||||
|
|
@ -9271,7 +9282,7 @@ name = "tqdm"
|
|||
version = "4.67.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||
{ name = "colorama", marker = "platform_system == 'Windows'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a8/4b/29b4ef32e036bb34e4ab51796dd745cdba7ed47ad142a9f4a1eb8e0c744d/tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2", size = 169737 }
|
||||
wheels = [
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue