Fix: Introducing a new JSON editor
This commit is contained in:
parent
6b370e9e0f
commit
136eca79e6
6 changed files with 763 additions and 14 deletions
|
|
@ -79,6 +79,7 @@
|
||||||
"input-otp": "^1.4.1",
|
"input-otp": "^1.4.1",
|
||||||
"js-base64": "^3.7.5",
|
"js-base64": "^3.7.5",
|
||||||
"jsencrypt": "^3.3.2",
|
"jsencrypt": "^3.3.2",
|
||||||
|
"jsoneditor": "^10.4.2",
|
||||||
"lexical": "^0.23.1",
|
"lexical": "^0.23.1",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"lucide-react": "^0.546.0",
|
"lucide-react": "^0.546.0",
|
||||||
|
|
|
||||||
132
web/src/components/json-edit/css/cloud9_night.less
Normal file
132
web/src/components/json-edit/css/cloud9_night.less
Normal file
|
|
@ -0,0 +1,132 @@
|
||||||
|
.ace-tomorrow-night .ace_gutter {
|
||||||
|
background: var(--bg-card);
|
||||||
|
color: rgb(var(--text-primary));
|
||||||
|
}
|
||||||
|
.ace-tomorrow-night .ace_print-margin {
|
||||||
|
width: 1px;
|
||||||
|
background: #25282c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ace-tomorrow-night {
|
||||||
|
background: var(--bg-card);
|
||||||
|
color: rgb(var(--text-primary));
|
||||||
|
.ace_editor {
|
||||||
|
background: var(--bg-card);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ace-tomorrow-night .ace_cursor {
|
||||||
|
color: #aeafad;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ace-tomorrow-night .ace_marker-layer .ace_selection {
|
||||||
|
background: #373b41;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ace-tomorrow-night.ace_multiselect .ace_selection.ace_start {
|
||||||
|
box-shadow: 0 0 3px 0px #1d1f21;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ace-tomorrow-night .ace_marker-layer .ace_step {
|
||||||
|
background: rgb(102, 82, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ace-tomorrow-night .ace_marker-layer .ace_bracket {
|
||||||
|
margin: -1px 0 0 -1px;
|
||||||
|
border: 1px solid #4b4e55;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ace-tomorrow-night .ace_marker-layer .ace_active-line {
|
||||||
|
background: var(--bg-card);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ace-tomorrow-night .ace_gutter-active-line {
|
||||||
|
background-color: var(--bg-card);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ace-tomorrow-night .ace_marker-layer .ace_selected-word {
|
||||||
|
border: 1px solid #373b41;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ace-tomorrow-night .ace_invisible {
|
||||||
|
color: #4b4e55;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ace-tomorrow-night .ace_keyword,
|
||||||
|
.ace-tomorrow-night .ace_meta,
|
||||||
|
.ace-tomorrow-night .ace_storage,
|
||||||
|
.ace-tomorrow-night .ace_storage.ace_type,
|
||||||
|
.ace-tomorrow-night .ace_support.ace_type {
|
||||||
|
color: #b294bb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ace-tomorrow-night .ace_keyword.ace_operator {
|
||||||
|
color: #8abeb7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ace-tomorrow-night .ace_constant.ace_character,
|
||||||
|
.ace-tomorrow-night .ace_constant.ace_language,
|
||||||
|
.ace-tomorrow-night .ace_constant.ace_numeric,
|
||||||
|
.ace-tomorrow-night .ace_keyword.ace_other.ace_unit,
|
||||||
|
.ace-tomorrow-night .ace_support.ace_constant,
|
||||||
|
.ace-tomorrow-night .ace_variable.ace_parameter {
|
||||||
|
color: #de935f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ace-tomorrow-night .ace_constant.ace_other {
|
||||||
|
color: #ced1cf;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ace-tomorrow-night .ace_invalid {
|
||||||
|
color: #ced2cf;
|
||||||
|
background-color: #df5f5f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ace-tomorrow-night .ace_invalid.ace_deprecated {
|
||||||
|
color: #ced2cf;
|
||||||
|
background-color: #b798bf;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ace-tomorrow-night .ace_fold {
|
||||||
|
background-color: #81a2be;
|
||||||
|
border-color: #c5c8c6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ace-tomorrow-night .ace_entity.ace_name.ace_function,
|
||||||
|
.ace-tomorrow-night .ace_support.ace_function,
|
||||||
|
.ace-tomorrow-night .ace_variable {
|
||||||
|
color: #81a2be;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ace-tomorrow-night .ace_support.ace_class,
|
||||||
|
.ace-tomorrow-night .ace_support.ace_type {
|
||||||
|
color: #f0c674;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ace-tomorrow-night .ace_heading,
|
||||||
|
.ace-tomorrow-night .ace_markup.ace_heading,
|
||||||
|
.ace-tomorrow-night .ace_string {
|
||||||
|
color: #b5bd68;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ace-tomorrow-night .ace_entity.ace_name.ace_tag,
|
||||||
|
.ace-tomorrow-night .ace_entity.ace_other.ace_attribute-name,
|
||||||
|
.ace-tomorrow-night .ace_meta.ace_tag,
|
||||||
|
.ace-tomorrow-night .ace_string.ace_regexp,
|
||||||
|
.ace-tomorrow-night .ace_variable {
|
||||||
|
color: #cc6666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ace-tomorrow-night .ace_comment {
|
||||||
|
color: #969896;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ace-tomorrow-night .ace_indent-guide {
|
||||||
|
background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAACCAYAAACZgbYnAAAAEklEQVQImWNgYGBgYHB3d/8PAAOIAdULw8qMAAAAAElFTkSuQmCC)
|
||||||
|
right repeat-y;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ace-tomorrow-night .ace_indent-guide-active {
|
||||||
|
background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAACCAYAAACZgbYnAAAAEklEQVQIW2PQ1dX9zzBz5sz/ABCcBFFentLlAAAAAElFTkSuQmCC)
|
||||||
|
right repeat-y;
|
||||||
|
}
|
||||||
83
web/src/components/json-edit/css/index.less
Normal file
83
web/src/components/json-edit/css/index.less
Normal file
|
|
@ -0,0 +1,83 @@
|
||||||
|
.jsoneditor {
|
||||||
|
border: none;
|
||||||
|
color: rgb(var(--text-primary));
|
||||||
|
overflow: auto;
|
||||||
|
scrollbar-width: none;
|
||||||
|
background-color: var(--bg-base);
|
||||||
|
.jsoneditor-menu {
|
||||||
|
background-color: var(--bg-base);
|
||||||
|
// border-color: var(--border-button);
|
||||||
|
border-bottom: thin solid var(--border-button);
|
||||||
|
}
|
||||||
|
.jsoneditor-navigation-bar {
|
||||||
|
border-bottom: 1px solid var(--border-button);
|
||||||
|
background-color: var(--bg-input);
|
||||||
|
}
|
||||||
|
.jsoneditor-tree {
|
||||||
|
background: var(--bg-base);
|
||||||
|
}
|
||||||
|
.jsoneditor-highlight {
|
||||||
|
background-color: var(--bg-card);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.jsoneditor-popover,
|
||||||
|
.jsoneditor-schema-error,
|
||||||
|
div.jsoneditor td,
|
||||||
|
div.jsoneditor textarea,
|
||||||
|
div.jsoneditor th,
|
||||||
|
div.jsoneditor-field,
|
||||||
|
div.jsoneditor-value,
|
||||||
|
pre.jsoneditor-preview {
|
||||||
|
font-family: consolas, menlo, monaco, 'Ubuntu Mono', source-code-pro,
|
||||||
|
monospace;
|
||||||
|
font-size: 14px;
|
||||||
|
color: rgb(var(--text-primary));
|
||||||
|
}
|
||||||
|
|
||||||
|
div.jsoneditor-field.jsoneditor-highlight,
|
||||||
|
div.jsoneditor-field[contenteditable='true']:focus,
|
||||||
|
div.jsoneditor-field[contenteditable='true']:hover,
|
||||||
|
div.jsoneditor-value.jsoneditor-highlight,
|
||||||
|
div.jsoneditor-value[contenteditable='true']:focus,
|
||||||
|
div.jsoneditor-value[contenteditable='true']:hover {
|
||||||
|
background-color: var(--bg-input);
|
||||||
|
border: 1px solid var(--border-button);
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jsoneditor-selected,
|
||||||
|
.jsoneditor-contextmenu .jsoneditor-menu li ul {
|
||||||
|
background: var(--bg-base);
|
||||||
|
}
|
||||||
|
|
||||||
|
.jsoneditor-contextmenu .jsoneditor-menu button {
|
||||||
|
color: rgb(var(--text-secondary));
|
||||||
|
}
|
||||||
|
.jsoneditor-menu a.jsoneditor-poweredBy {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.ace-jsoneditor .ace_scroller {
|
||||||
|
background-color: var(--bg-base);
|
||||||
|
}
|
||||||
|
.jsoneditor-statusbar {
|
||||||
|
border-top: 1px solid var(--border-button);
|
||||||
|
background-color: var(--bg-base);
|
||||||
|
color: rgb(var(--text-primary));
|
||||||
|
}
|
||||||
|
.jsoneditor-menu > .jsoneditor-modes > button,
|
||||||
|
.jsoneditor-menu > button {
|
||||||
|
// color: rgb(var(--text-secondary));
|
||||||
|
background-color: var(--text-disabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
.jsoneditor-menu > .jsoneditor-modes > button:active,
|
||||||
|
.jsoneditor-menu > .jsoneditor-modes > button:focus,
|
||||||
|
.jsoneditor-menu > button:active,
|
||||||
|
.jsoneditor-menu > button:focus {
|
||||||
|
background-color: rgb(var(--text-secondary));
|
||||||
|
}
|
||||||
|
.jsoneditor-menu > .jsoneditor-modes > button:hover,
|
||||||
|
.jsoneditor-menu > button:hover {
|
||||||
|
background-color: rgb(var(--text-secondary));
|
||||||
|
border: 1px solid var(--border-button);
|
||||||
|
}
|
||||||
142
web/src/components/json-edit/index.tsx
Normal file
142
web/src/components/json-edit/index.tsx
Normal file
|
|
@ -0,0 +1,142 @@
|
||||||
|
import React, { useEffect, useRef } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import './css/cloud9_night.less';
|
||||||
|
import './css/index.less';
|
||||||
|
import { JsonEditorOptions, JsonEditorProps } from './interface';
|
||||||
|
const defaultConfig: JsonEditorOptions = {
|
||||||
|
mode: 'code',
|
||||||
|
modes: ['tree', 'code'],
|
||||||
|
history: false,
|
||||||
|
search: false,
|
||||||
|
mainMenuBar: false,
|
||||||
|
navigationBar: false,
|
||||||
|
enableSort: false,
|
||||||
|
enableTransform: false,
|
||||||
|
indentation: 2,
|
||||||
|
};
|
||||||
|
const JsonEditor: React.FC<JsonEditorProps> = ({
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
height = '400px',
|
||||||
|
className = '',
|
||||||
|
options = {},
|
||||||
|
}) => {
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
const editorRef = useRef<any>(null);
|
||||||
|
const { i18n } = useTranslation();
|
||||||
|
const currentLanguageRef = useRef<string>(i18n.language);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
const JSONEditor = require('jsoneditor');
|
||||||
|
import('jsoneditor/dist/jsoneditor.min.css');
|
||||||
|
|
||||||
|
if (containerRef.current) {
|
||||||
|
// Default configuration options
|
||||||
|
const defaultOptions: JsonEditorOptions = {
|
||||||
|
...defaultConfig,
|
||||||
|
language: i18n.language === 'zh' ? 'zh-CN' : 'en',
|
||||||
|
onChange: () => {
|
||||||
|
if (editorRef.current && onChange) {
|
||||||
|
try {
|
||||||
|
const updatedJson = editorRef.current.get();
|
||||||
|
onChange(updatedJson);
|
||||||
|
} catch (err) {
|
||||||
|
// Do not trigger onChange when parsing error occurs
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
...options, // Merge user provided options with defaults
|
||||||
|
};
|
||||||
|
|
||||||
|
editorRef.current = new JSONEditor(
|
||||||
|
containerRef.current,
|
||||||
|
defaultOptions,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (value) {
|
||||||
|
editorRef.current.set(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (editorRef.current) {
|
||||||
|
if (typeof editorRef.current.destroy === 'function') {
|
||||||
|
editorRef.current.destroy();
|
||||||
|
}
|
||||||
|
editorRef.current = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Update language when i18n language changes
|
||||||
|
// Since JSONEditor doesn't have a setOptions method, we need to recreate the editor
|
||||||
|
if (editorRef.current && currentLanguageRef.current !== i18n.language) {
|
||||||
|
currentLanguageRef.current = i18n.language;
|
||||||
|
|
||||||
|
// Save current data
|
||||||
|
let currentData;
|
||||||
|
try {
|
||||||
|
currentData = editorRef.current.get();
|
||||||
|
} catch (e) {
|
||||||
|
// If there's an error getting data, use the passed value or empty object
|
||||||
|
currentData = value || {};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Destroy the current editor
|
||||||
|
if (typeof editorRef.current.destroy === 'function') {
|
||||||
|
editorRef.current.destroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recreate the editor with new language
|
||||||
|
const JSONEditor = require('jsoneditor');
|
||||||
|
|
||||||
|
const newOptions: JsonEditorOptions = {
|
||||||
|
...defaultConfig,
|
||||||
|
language: i18n.language === 'zh' ? 'zh-CN' : 'en',
|
||||||
|
onChange: () => {
|
||||||
|
if (editorRef.current && onChange) {
|
||||||
|
try {
|
||||||
|
const updatedJson = editorRef.current.get();
|
||||||
|
onChange(updatedJson);
|
||||||
|
} catch (err) {
|
||||||
|
// Do not trigger onChange when parsing error occurs
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
...options, // Merge user provided options with defaults
|
||||||
|
};
|
||||||
|
|
||||||
|
editorRef.current = new JSONEditor(containerRef.current, newOptions);
|
||||||
|
editorRef.current.set(currentData);
|
||||||
|
}
|
||||||
|
}, [i18n.language, value, onChange, options]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (editorRef.current && value !== undefined) {
|
||||||
|
try {
|
||||||
|
// Only update the editor when the value actually changes
|
||||||
|
const currentJson = editorRef.current.get();
|
||||||
|
if (JSON.stringify(currentJson) !== JSON.stringify(value)) {
|
||||||
|
editorRef.current.set(value);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
// Skip update if there is a syntax error in the current editor
|
||||||
|
editorRef.current.set(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [value]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={containerRef}
|
||||||
|
style={{ height }}
|
||||||
|
className={`ace-tomorrow-night w-full border border-border-button rounded-lg overflow-hidden bg-bg-input ${className} `}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default JsonEditor;
|
||||||
339
web/src/components/json-edit/interface.ts
Normal file
339
web/src/components/json-edit/interface.ts
Normal file
|
|
@ -0,0 +1,339 @@
|
||||||
|
// JSONEditor configuration options interface see: https://github.com/josdejong/jsoneditor/blob/master/docs/api.md
|
||||||
|
export interface JsonEditorOptions {
|
||||||
|
/**
|
||||||
|
* Editor mode. Available values: 'tree' (default), 'view', 'form', 'text', and 'code'.
|
||||||
|
*/
|
||||||
|
mode?: 'tree' | 'view' | 'form' | 'text' | 'code';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Array of available modes
|
||||||
|
*/
|
||||||
|
modes?: Array<'tree' | 'view' | 'form' | 'text' | 'code'>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Field name for the root node. Only applicable for modes 'tree', 'view', and 'form'
|
||||||
|
*/
|
||||||
|
name?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Theme for the editor
|
||||||
|
*/
|
||||||
|
theme?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enable history (undo/redo). True by default. Only applicable for modes 'tree', 'view', and 'form'
|
||||||
|
*/
|
||||||
|
history?: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enable search box. True by default. Only applicable for modes 'tree', 'view', and 'form'
|
||||||
|
*/
|
||||||
|
search?: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main menu bar visibility
|
||||||
|
*/
|
||||||
|
mainMenuBar?: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Navigation bar visibility
|
||||||
|
*/
|
||||||
|
navigationBar?: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Status bar visibility
|
||||||
|
*/
|
||||||
|
statusBar?: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If true, object keys are sorted before display. false by default.
|
||||||
|
*/
|
||||||
|
sortObjectKeys?: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enable transform functionality
|
||||||
|
*/
|
||||||
|
enableTransform?: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enable sort functionality
|
||||||
|
*/
|
||||||
|
enableSort?: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Limit dragging functionality
|
||||||
|
*/
|
||||||
|
limitDragging?: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A JSON schema object
|
||||||
|
*/
|
||||||
|
schema?: any;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Schemas that are referenced using the `$ref` property from the JSON schema
|
||||||
|
*/
|
||||||
|
schemaRefs?: Record<string, any>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Array of template objects
|
||||||
|
*/
|
||||||
|
templates?: Array<{
|
||||||
|
text: string;
|
||||||
|
title?: string;
|
||||||
|
className?: string;
|
||||||
|
field?: string;
|
||||||
|
value: any;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ace editor instance
|
||||||
|
*/
|
||||||
|
ace?: any;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An instance of Ajv JSON schema validator
|
||||||
|
*/
|
||||||
|
ajv?: any;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Switch to enable/disable autocomplete
|
||||||
|
*/
|
||||||
|
autocomplete?: {
|
||||||
|
confirmKey?: string | string[];
|
||||||
|
caseSensitive?: boolean;
|
||||||
|
getOptions?: (
|
||||||
|
text: string,
|
||||||
|
path: Array<string | number>,
|
||||||
|
input: string,
|
||||||
|
editor: any,
|
||||||
|
) => string[] | Promise<string[]> | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Number of indentation spaces. 4 by default. Only applicable for modes 'text' and 'code'
|
||||||
|
*/
|
||||||
|
indentation?: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Available languages
|
||||||
|
*/
|
||||||
|
languages?: string[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Language of the editor
|
||||||
|
*/
|
||||||
|
language?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback method, triggered on change of contents. Does not pass the contents itself.
|
||||||
|
* See also onChangeJSON and onChangeText.
|
||||||
|
*/
|
||||||
|
onChange?: () => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback method, triggered in modes on change of contents, passing the changed contents as JSON.
|
||||||
|
* Only applicable for modes 'tree', 'view', and 'form'.
|
||||||
|
*/
|
||||||
|
onChangeJSON?: (json: any) => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback method, triggered in modes on change of contents, passing the changed contents as stringified JSON.
|
||||||
|
*/
|
||||||
|
onChangeText?: (text: string) => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback method, triggered when an error occurs
|
||||||
|
*/
|
||||||
|
onError?: (error: Error) => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback method, triggered when node is expanded
|
||||||
|
*/
|
||||||
|
onExpand?: (node: any) => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback method, triggered when node is collapsed
|
||||||
|
*/
|
||||||
|
onCollapse?: (node: any) => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback method, determines if a node is editable
|
||||||
|
*/
|
||||||
|
onEditable?: (node: any) => boolean | { field: boolean; value: boolean };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback method, triggered when an event occurs in a JSON field or value.
|
||||||
|
* Only applicable for modes 'form', 'tree' and 'view'
|
||||||
|
*/
|
||||||
|
onEvent?: (node: any, event: Event) => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback method, triggered when the editor comes into focus, passing an object {type, target}.
|
||||||
|
* Applicable for all modes
|
||||||
|
*/
|
||||||
|
onFocus?: (node: any) => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback method, triggered when the editor goes out of focus, passing an object {type, target}.
|
||||||
|
* Applicable for all modes
|
||||||
|
*/
|
||||||
|
onBlur?: (node: any) => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback method, triggered when creating menu items
|
||||||
|
*/
|
||||||
|
onCreateMenu?: (menuItems: any[], node: any) => any[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback method, triggered on node selection change. Only applicable for modes 'tree', 'view', and 'form'
|
||||||
|
*/
|
||||||
|
onSelectionChange?: (selection: any) => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback method, triggered on text selection change. Only applicable for modes 'text' and 'code'
|
||||||
|
*/
|
||||||
|
onTextSelectionChange?: (selection: any) => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback method, triggered when a Node DOM is rendered. Function returns a css class name to be set on a node.
|
||||||
|
* Only applicable for modes 'form', 'tree' and 'view'
|
||||||
|
*/
|
||||||
|
onClassName?: (node: any) => string | undefined;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback method, triggered when validating nodes
|
||||||
|
*/
|
||||||
|
onValidate?: (
|
||||||
|
json: any,
|
||||||
|
) =>
|
||||||
|
| Array<{ path: Array<string | number>; message: string }>
|
||||||
|
| Promise<Array<{ path: Array<string | number>; message: string }>>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback method, triggered when node name is determined
|
||||||
|
*/
|
||||||
|
onNodeName?: (parentNode: any, childNode: any, name: string) => string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback method, triggered when mode changes
|
||||||
|
*/
|
||||||
|
onModeChange?: (newMode: string, oldMode: string) => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Color picker options
|
||||||
|
*/
|
||||||
|
colorPicker?: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback method for color picker
|
||||||
|
*/
|
||||||
|
onColorPicker?: (
|
||||||
|
callback: (color: string) => void,
|
||||||
|
parent: HTMLElement,
|
||||||
|
) => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If true, shows timestamp tag
|
||||||
|
*/
|
||||||
|
timestampTag?: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format for timestamps
|
||||||
|
*/
|
||||||
|
timestampFormat?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If true, unicode characters are escaped. false by default.
|
||||||
|
*/
|
||||||
|
escapeUnicode?: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Number of children allowed for a node in 'tree', 'view', or 'form' mode before
|
||||||
|
* the "show more/show all" buttons appear. 100 by default.
|
||||||
|
*/
|
||||||
|
maxVisibleChilds?: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback method for validation errors
|
||||||
|
*/
|
||||||
|
onValidationError?: (
|
||||||
|
errors: Array<{ path: Array<string | number>; message: string }>,
|
||||||
|
) => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback method for validation warnings
|
||||||
|
*/
|
||||||
|
onValidationWarning?: (
|
||||||
|
warnings: Array<{ path: Array<string | number>; message: string }>,
|
||||||
|
) => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The anchor element to apply an overlay and display the modals in a centered location. Defaults to document.body
|
||||||
|
*/
|
||||||
|
modalAnchor?: HTMLElement | null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Anchor element for popups
|
||||||
|
*/
|
||||||
|
popupAnchor?: HTMLElement | null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Function to create queries
|
||||||
|
*/
|
||||||
|
createQuery?: () => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Function to execute queries
|
||||||
|
*/
|
||||||
|
executeQuery?: () => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Query description
|
||||||
|
*/
|
||||||
|
queryDescription?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allow schema suggestions
|
||||||
|
*/
|
||||||
|
allowSchemaSuggestions?: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show error table
|
||||||
|
*/
|
||||||
|
showErrorTable?: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate current JSON object against the configured JSON schema
|
||||||
|
* Must be implemented by tree mode and text mode
|
||||||
|
*/
|
||||||
|
validate?: () => Promise<any[]>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Refresh the rendered contents
|
||||||
|
* Can be implemented by tree mode and text mode
|
||||||
|
*/
|
||||||
|
refresh?: () => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback method triggered when schema changes
|
||||||
|
*/
|
||||||
|
_onSchemaChange?: (schema: any, schemaRefs: any) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface JsonEditorProps {
|
||||||
|
// JSON data to be displayed in the editor
|
||||||
|
value?: any;
|
||||||
|
|
||||||
|
// Callback function triggered when the JSON data changes
|
||||||
|
onChange?: (value: any) => void;
|
||||||
|
|
||||||
|
// Height of the editor
|
||||||
|
height?: string;
|
||||||
|
|
||||||
|
// Additional CSS class names
|
||||||
|
className?: string;
|
||||||
|
|
||||||
|
// Configuration options for the JSONEditor
|
||||||
|
options?: JsonEditorOptions;
|
||||||
|
}
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
|
import JsonEditor from '@/components/json-edit';
|
||||||
import { BlockButton, Button } from '@/components/ui/button';
|
import { BlockButton, Button } from '@/components/ui/button';
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
import { Segmented } from '@/components/ui/segmented';
|
import { Segmented } from '@/components/ui/segmented';
|
||||||
import { Editor } from '@monaco-editor/react';
|
|
||||||
import { t } from 'i18next';
|
import { t } from 'i18next';
|
||||||
import { Trash2, X } from 'lucide-react';
|
import { Trash2, X } from 'lucide-react';
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
|
|
@ -31,32 +31,80 @@ export const useObjectFields = () => {
|
||||||
},
|
},
|
||||||
[],
|
[],
|
||||||
);
|
);
|
||||||
|
const validateKeys = (
|
||||||
|
obj: any,
|
||||||
|
path: (string | number)[] = [],
|
||||||
|
): Array<{ path: (string | number)[]; message: string }> => {
|
||||||
|
const errors: Array<{ path: (string | number)[]; message: string }> = [];
|
||||||
|
|
||||||
|
if (obj !== null && typeof obj === 'object' && !Array.isArray(obj)) {
|
||||||
|
for (const key in obj) {
|
||||||
|
if (obj.hasOwnProperty(key)) {
|
||||||
|
if (!/^[a-zA-Z_]+$/.test(key)) {
|
||||||
|
errors.push({
|
||||||
|
path: [...path, key],
|
||||||
|
message: `Key "${key}" is invalid. Keys can only contain letters and underscores.`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const nestedErrors = validateKeys(obj[key], [...path, key]);
|
||||||
|
errors.push(...nestedErrors);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (Array.isArray(obj)) {
|
||||||
|
obj.forEach((item, index) => {
|
||||||
|
const nestedErrors = validateKeys(item, [...path, index]);
|
||||||
|
errors.push(...nestedErrors);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors;
|
||||||
|
};
|
||||||
const objectRender = useCallback((field: FieldValues) => {
|
const objectRender = useCallback((field: FieldValues) => {
|
||||||
const fieldValue =
|
// const fieldValue =
|
||||||
typeof field.value === 'object'
|
// typeof field.value === 'object'
|
||||||
? JSON.stringify(field.value, null, 2)
|
// ? JSON.stringify(field.value, null, 2)
|
||||||
: JSON.stringify({}, null, 2);
|
// : JSON.stringify({}, null, 2);
|
||||||
console.log('object-render-field', field, fieldValue);
|
// console.log('object-render-field', field, fieldValue);
|
||||||
return (
|
return (
|
||||||
<Editor
|
// <Editor
|
||||||
height={200}
|
// height={200}
|
||||||
defaultLanguage="json"
|
// defaultLanguage="json"
|
||||||
theme="vs-dark"
|
// theme="vs-dark"
|
||||||
value={fieldValue}
|
// value={fieldValue}
|
||||||
|
// onChange={field.onChange}
|
||||||
|
// />
|
||||||
|
<JsonEditor
|
||||||
|
value={field.value}
|
||||||
onChange={field.onChange}
|
onChange={field.onChange}
|
||||||
|
height="400px"
|
||||||
|
options={{
|
||||||
|
mode: 'code',
|
||||||
|
navigationBar: false,
|
||||||
|
mainMenuBar: true,
|
||||||
|
history: true,
|
||||||
|
onValidate: (json) => {
|
||||||
|
return validateKeys(json);
|
||||||
|
},
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const objectValidate = useCallback((value: any) => {
|
const objectValidate = useCallback((value: any) => {
|
||||||
try {
|
try {
|
||||||
if (!JSON.parse(value)) {
|
if (validateKeys(value, [])?.length > 0) {
|
||||||
throw new Error(t('knowledgeDetails.formatTypeError'));
|
throw new Error(t('flow.formatTypeError'));
|
||||||
|
}
|
||||||
|
if (!z.object({}).safeParse(value).success) {
|
||||||
|
throw new Error(t('flow.formatTypeError'));
|
||||||
|
}
|
||||||
|
if (value && typeof value === 'string' && !JSON.parse(value)) {
|
||||||
|
throw new Error(t('flow.formatTypeError'));
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw new Error(t('knowledgeDetails.formatTypeError'));
|
console.log('object-render-error', e, value);
|
||||||
|
throw new Error(t('flow.formatTypeError'));
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|
@ -219,6 +267,10 @@ export const useObjectFields = () => {
|
||||||
};
|
};
|
||||||
const handleCustomSchema = (value: TypesWithArray) => {
|
const handleCustomSchema = (value: TypesWithArray) => {
|
||||||
switch (value) {
|
switch (value) {
|
||||||
|
case TypesWithArray.Object:
|
||||||
|
return z.object({});
|
||||||
|
case TypesWithArray.ArrayObject:
|
||||||
|
return z.array(z.object({}));
|
||||||
case TypesWithArray.ArrayString:
|
case TypesWithArray.ArrayString:
|
||||||
return z.array(z.string());
|
return z.array(z.string());
|
||||||
case TypesWithArray.ArrayNumber:
|
case TypesWithArray.ArrayNumber:
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue