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
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);
|
||||
},
|
||||
);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue