fix(web): jinja2 editor
This commit is contained in:
186
web/src/views/Workflow/components/Editor/Jinja2Editor.tsx
Normal file
186
web/src/views/Workflow/components/Editor/Jinja2Editor.tsx
Normal file
@@ -0,0 +1,186 @@
|
|||||||
|
/*
|
||||||
|
* @Author: ZhaoYing
|
||||||
|
* @Date: 2026-04-02 15:15:36
|
||||||
|
* @Last Modified by: ZhaoYing
|
||||||
|
* @Last Modified time: 2026-04-02 15:15:36
|
||||||
|
*/
|
||||||
|
import { type FC, useEffect, useMemo } from 'react';
|
||||||
|
import { LexicalComposer } from '@lexical/react/LexicalComposer';
|
||||||
|
import { RichTextPlugin } from '@lexical/react/LexicalRichTextPlugin';
|
||||||
|
import { ContentEditable } from '@lexical/react/LexicalContentEditable';
|
||||||
|
import { HistoryPlugin } from '@lexical/react/LexicalHistoryPlugin';
|
||||||
|
import { LexicalErrorBoundary } from '@lexical/react/LexicalErrorBoundary';
|
||||||
|
|
||||||
|
import AutocompletePlugin, { type Suggestion } from './plugin/AutocompletePlugin';
|
||||||
|
import CharacterCountPlugin from './plugin/CharacterCountPlugin';
|
||||||
|
import InitialValuePlugin from './plugin/InitialValuePlugin';
|
||||||
|
import CommandPlugin from './plugin/CommandPlugin';
|
||||||
|
import Jinja2HighlightPlugin from './plugin/Jinja2HighlightPlugin';
|
||||||
|
import LineNumberPlugin from './plugin/LineNumberPlugin';
|
||||||
|
import BlurPlugin from './plugin/BlurPlugin';
|
||||||
|
|
||||||
|
const jinja2Theme = {
|
||||||
|
paragraph: 'editor-paragraph',
|
||||||
|
code: 'jinja2-expression',
|
||||||
|
text: {
|
||||||
|
bold: 'editor-text-bold',
|
||||||
|
italic: 'editor-text-italic',
|
||||||
|
code: 'jinja2-inline',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const initialConfig = {
|
||||||
|
namespace: 'AutocompleteEditor',
|
||||||
|
theme: jinja2Theme,
|
||||||
|
nodes: [],
|
||||||
|
onError: (error: Error) => console.error(error),
|
||||||
|
};
|
||||||
|
|
||||||
|
const STYLE_ID = 'code-editor-styles';
|
||||||
|
const JINJA2_STYLES = `
|
||||||
|
.jinja2-expression {
|
||||||
|
background-color: #f6f8fa !important;
|
||||||
|
border: 1px solid #d1d9e0 !important;
|
||||||
|
border-radius: 3px !important;
|
||||||
|
padding: 2px 4px !important;
|
||||||
|
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace !important;
|
||||||
|
font-size: 13px !important;
|
||||||
|
color: #0969da !important;
|
||||||
|
}
|
||||||
|
.jinja2-inline {
|
||||||
|
background-color: #f6f8fa !important;
|
||||||
|
padding: 1px 3px !important;
|
||||||
|
border-radius: 2px !important;
|
||||||
|
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace !important;
|
||||||
|
font-size: 13px !important;
|
||||||
|
color: #0969da !important;
|
||||||
|
}
|
||||||
|
.editor-paragraph { margin: 0; }
|
||||||
|
.editor-with-line-numbers { display: flex; }
|
||||||
|
.line-numbers {
|
||||||
|
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 16px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
text-align: right;
|
||||||
|
user-select: none;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
.line-numbers > div { min-height: 20px; display: flex; align-items: flex-start; }
|
||||||
|
.editor-content-wrapper { flex: 1; }
|
||||||
|
.editor-content-with-numbers {
|
||||||
|
white-space: pre-wrap;
|
||||||
|
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
|
||||||
|
}
|
||||||
|
.editor-content-with-numbers p { margin: 0; min-height: 20px; }
|
||||||
|
`;
|
||||||
|
|
||||||
|
export interface Jinja2EditorProps {
|
||||||
|
placeholder?: string;
|
||||||
|
value?: string;
|
||||||
|
onChange?: (value: string) => void;
|
||||||
|
options?: Suggestion[];
|
||||||
|
variant?: 'outlined' | 'borderless';
|
||||||
|
height?: number;
|
||||||
|
size?: 'default' | 'small';
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Jinja2Editor: FC<Jinja2EditorProps> = ({
|
||||||
|
placeholder = '请输入内容...',
|
||||||
|
value = '',
|
||||||
|
onChange,
|
||||||
|
options = [],
|
||||||
|
variant = 'borderless',
|
||||||
|
size = 'default',
|
||||||
|
height,
|
||||||
|
className,
|
||||||
|
}) => {
|
||||||
|
useEffect(() => {
|
||||||
|
if (!document.getElementById(STYLE_ID)) {
|
||||||
|
const style = document.createElement('style');
|
||||||
|
style.id = STYLE_ID;
|
||||||
|
style.textContent = JINJA2_STYLES;
|
||||||
|
document.head.appendChild(style);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const minheight = useMemo(
|
||||||
|
() => `${height ?? (size === 'small' ? 60 : 120)}px`,
|
||||||
|
[height, size],
|
||||||
|
);
|
||||||
|
|
||||||
|
const fontSize = size === 'small' ? '12px' : '14px';
|
||||||
|
|
||||||
|
const lineHeight = useMemo(
|
||||||
|
() => `${height ? height - 10 : size === 'small' ? 16 : 20}px`,
|
||||||
|
[height, size],
|
||||||
|
);
|
||||||
|
|
||||||
|
const placeHolderMinheight = `${height ? 16 : size === 'small' ? 16 : 30}px`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<LexicalComposer initialConfig={initialConfig}>
|
||||||
|
<div style={{ position: 'relative' }} className={className}>
|
||||||
|
<RichTextPlugin
|
||||||
|
contentEditable={
|
||||||
|
<div
|
||||||
|
className="editor-with-line-numbers"
|
||||||
|
style={{
|
||||||
|
border: variant === 'borderless' ? 'none' : '1px solid #DFE4ED',
|
||||||
|
borderRadius: '6px',
|
||||||
|
minHeight: minheight,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="line-numbers">
|
||||||
|
<div>1</div>
|
||||||
|
</div>
|
||||||
|
<div className="editor-content-wrapper">
|
||||||
|
<ContentEditable
|
||||||
|
className="editor-content-with-numbers"
|
||||||
|
style={{
|
||||||
|
minHeight: minheight,
|
||||||
|
padding: variant === 'borderless' ? '0' : '4px 0',
|
||||||
|
outline: 'none',
|
||||||
|
resize: 'none',
|
||||||
|
fontSize,
|
||||||
|
lineHeight,
|
||||||
|
border: 'none',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
placeholder={
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
minHeight: placeHolderMinheight,
|
||||||
|
position: 'absolute',
|
||||||
|
top: '4px',
|
||||||
|
left: '16px',
|
||||||
|
color: '#A8A9AA',
|
||||||
|
fontSize,
|
||||||
|
lineHeight: placeHolderMinheight,
|
||||||
|
pointerEvents: 'none',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{placeholder}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
ErrorBoundary={LexicalErrorBoundary}
|
||||||
|
/>
|
||||||
|
<HistoryPlugin />
|
||||||
|
<CommandPlugin />
|
||||||
|
<Jinja2HighlightPlugin />
|
||||||
|
<LineNumberPlugin />
|
||||||
|
<AutocompletePlugin options={options} enableJinja2 />
|
||||||
|
<CharacterCountPlugin setCount={() => {}} onChange={onChange} />
|
||||||
|
<InitialValuePlugin value={value} options={options} enableLineNumbers />
|
||||||
|
<BlurPlugin enableJinja2 />
|
||||||
|
</div>
|
||||||
|
</LexicalComposer>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Jinja2Editor;
|
||||||
@@ -4,26 +4,20 @@
|
|||||||
* @Last Modified by: ZhaoYing
|
* @Last Modified by: ZhaoYing
|
||||||
* @Last Modified time: 2026-03-25 10:58:47
|
* @Last Modified time: 2026-03-25 10:58:47
|
||||||
*/
|
*/
|
||||||
import { type FC, useState, useEffect, useMemo } from 'react';
|
import { type FC, useState, useMemo } from 'react';
|
||||||
import { LexicalComposer } from '@lexical/react/LexicalComposer';
|
import { LexicalComposer } from '@lexical/react/LexicalComposer';
|
||||||
import { RichTextPlugin } from '@lexical/react/LexicalRichTextPlugin';
|
import { RichTextPlugin } from '@lexical/react/LexicalRichTextPlugin';
|
||||||
import { ContentEditable } from '@lexical/react/LexicalContentEditable';
|
import { ContentEditable } from '@lexical/react/LexicalContentEditable';
|
||||||
import { HistoryPlugin } from '@lexical/react/LexicalHistoryPlugin';
|
import { HistoryPlugin } from '@lexical/react/LexicalHistoryPlugin';
|
||||||
// import { AutoFocusPlugin } from '@lexical/react/LexicalAutoFocusPlugin';
|
|
||||||
import { LexicalErrorBoundary } from '@lexical/react/LexicalErrorBoundary';
|
import { LexicalErrorBoundary } from '@lexical/react/LexicalErrorBoundary';
|
||||||
// import { HeadingNode, QuoteNode } from '@lexical/rich-text';
|
|
||||||
// import { ListItemNode, ListNode } from '@lexical/list';
|
|
||||||
// import { LinkNode } from '@lexical/link';
|
|
||||||
// import { CodeNode } from '@lexical/code';
|
|
||||||
|
|
||||||
import AutocompletePlugin, { type Suggestion } from './plugin/AutocompletePlugin'
|
import AutocompletePlugin, { type Suggestion } from './plugin/AutocompletePlugin'
|
||||||
import CharacterCountPlugin from './plugin/CharacterCountPlugin'
|
import CharacterCountPlugin from './plugin/CharacterCountPlugin'
|
||||||
import InitialValuePlugin from './plugin/InitialValuePlugin';
|
import InitialValuePlugin from './plugin/InitialValuePlugin';
|
||||||
import CommandPlugin from './plugin/CommandPlugin';
|
import CommandPlugin from './plugin/CommandPlugin';
|
||||||
import Jinja2HighlightPlugin from './plugin/Jinja2HighlightPlugin';
|
|
||||||
import LineNumberPlugin from './plugin/LineNumberPlugin';
|
|
||||||
import BlurPlugin from './plugin/BlurPlugin';
|
import BlurPlugin from './plugin/BlurPlugin';
|
||||||
import { VariableNode } from './nodes/VariableNode'
|
import { VariableNode } from './nodes/VariableNode'
|
||||||
|
import Jinja2Editor from './Jinja2Editor';
|
||||||
|
|
||||||
// Props interface for Lexical Editor component
|
// Props interface for Lexical Editor component
|
||||||
export interface LexicalEditorProps {
|
export interface LexicalEditorProps {
|
||||||
@@ -50,16 +44,6 @@ const theme = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// Theme with Jinja2 syntax highlighting
|
|
||||||
const jinja2Theme = {
|
|
||||||
...theme,
|
|
||||||
code: 'jinja2-expression',
|
|
||||||
text: {
|
|
||||||
...theme.text,
|
|
||||||
code: 'jinja2-inline',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
// Main Lexical Editor component
|
// Main Lexical Editor component
|
||||||
const Editor: FC<LexicalEditorProps> =({
|
const Editor: FC<LexicalEditorProps> =({
|
||||||
placeholder = "请输入内容...",
|
placeholder = "请输入内容...",
|
||||||
@@ -74,97 +58,27 @@ const Editor: FC<LexicalEditorProps> =({
|
|||||||
className
|
className
|
||||||
}) => {
|
}) => {
|
||||||
const [_count, setCount] = useState(0);
|
const [_count, setCount] = useState(0);
|
||||||
const [enableJinja2, setEnableJinja2] = useState(false)
|
|
||||||
const [enableLineNumbers, setEnableLineNumbers] = useState(false)
|
|
||||||
|
|
||||||
// Setup Jinja2 mode and inject styles when language changes
|
if (language === 'jinja2') {
|
||||||
useEffect(() => {
|
return (
|
||||||
const needsLineNumbers = language === 'jinja2';
|
<Jinja2Editor
|
||||||
setEnableJinja2(language === 'jinja2');
|
placeholder={placeholder}
|
||||||
setEnableLineNumbers(needsLineNumbers);
|
value={value}
|
||||||
|
onChange={onChange}
|
||||||
if (needsLineNumbers) {
|
options={options}
|
||||||
const styleId = 'code-editor-styles';
|
variant={variant}
|
||||||
let existingStyle = document.getElementById(styleId);
|
size={size}
|
||||||
|
height={height}
|
||||||
if (!existingStyle) {
|
className={className}
|
||||||
const style = document.createElement('style');
|
/>
|
||||||
style.id = styleId;
|
);
|
||||||
style.textContent = `
|
}
|
||||||
.jinja2-expression {
|
|
||||||
background-color: #f6f8fa !important;
|
|
||||||
border: 1px solid #d1d9e0 !important;
|
|
||||||
border-radius: 3px !important;
|
|
||||||
padding: 2px 4px !important;
|
|
||||||
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace !important;
|
|
||||||
font-size: 13px !important;
|
|
||||||
color: #0969da !important;
|
|
||||||
}
|
|
||||||
.jinja2-inline {
|
|
||||||
background-color: #f6f8fa !important;
|
|
||||||
padding: 1px 3px !important;
|
|
||||||
border-radius: 2px !important;
|
|
||||||
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace !important;
|
|
||||||
font-size: 13px !important;
|
|
||||||
color: #0969da !important;
|
|
||||||
}
|
|
||||||
.editor-paragraph {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
.editor-paragraph:has-text('{') .editor-text,
|
|
||||||
.editor-paragraph:has-text('[') .editor-text {
|
|
||||||
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace !important;
|
|
||||||
}
|
|
||||||
.editor-with-line-numbers {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
.line-numbers {
|
|
||||||
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
|
|
||||||
font-size: 12px;
|
|
||||||
line-height: 16px;
|
|
||||||
padding: 4px 8px;
|
|
||||||
text-align: right;
|
|
||||||
user-select: none;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
.line-numbers > div {
|
|
||||||
min-height: 20px;
|
|
||||||
display: flex;
|
|
||||||
align-items: flex-start;
|
|
||||||
}
|
|
||||||
.editor-content-wrapper {
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
.editor-content-with-numbers {
|
|
||||||
white-space: pre-wrap;
|
|
||||||
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
|
|
||||||
}
|
|
||||||
.editor-content-with-numbers p {
|
|
||||||
margin: 0;
|
|
||||||
min-height: 20px;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
document.head.appendChild(style);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [language])
|
|
||||||
|
|
||||||
// Lexical editor configuration
|
// Lexical editor configuration
|
||||||
const initialConfig = {
|
const initialConfig = {
|
||||||
namespace: 'AutocompleteEditor',
|
namespace: 'AutocompleteEditor',
|
||||||
theme: enableJinja2 ? jinja2Theme : theme,
|
theme,
|
||||||
nodes: enableJinja2 ? [
|
nodes: [VariableNode],
|
||||||
// When Jinja2 is enabled, use plain text instead of VariableNode
|
|
||||||
] : [
|
|
||||||
// HeadingNode,
|
|
||||||
// QuoteNode,
|
|
||||||
// ListItemNode,
|
|
||||||
// ListNode,
|
|
||||||
// LinkNode,
|
|
||||||
// CodeNode,
|
|
||||||
VariableNode,
|
|
||||||
],
|
|
||||||
onError: (error: Error) => {
|
onError: (error: Error) => {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
},
|
},
|
||||||
@@ -198,54 +112,26 @@ const Editor: FC<LexicalEditorProps> =({
|
|||||||
<div style={{ position: 'relative' }} className={className}>
|
<div style={{ position: 'relative' }} className={className}>
|
||||||
<RichTextPlugin
|
<RichTextPlugin
|
||||||
contentEditable={
|
contentEditable={
|
||||||
enableLineNumbers ? (
|
<ContentEditable
|
||||||
// Editor with line numbers for Jinja2 mode
|
style={{
|
||||||
<div className="editor-with-line-numbers" style={{
|
|
||||||
border: variant === 'borderless' ? 'none' : '1px solid #DFE4ED',
|
|
||||||
borderRadius: '6px',
|
|
||||||
minHeight: minheight,
|
minHeight: minheight,
|
||||||
}}>
|
padding: height ? '4px 6px' : variant === 'borderless' ? '0' : '6px 8px',
|
||||||
<div className="line-numbers">
|
border: variant === 'borderless' ? 'none' : '1px solid #EBEBEB',
|
||||||
<div>1</div>
|
borderRadius: '8px',
|
||||||
</div>
|
outline: 'none',
|
||||||
<div className="editor-content-wrapper">
|
resize: 'none',
|
||||||
<ContentEditable
|
fontSize: fontSize,
|
||||||
className="editor-content-with-numbers"
|
lineHeight: lineHeight,
|
||||||
style={{
|
}}
|
||||||
minHeight: minheight,
|
/>
|
||||||
padding: variant === 'borderless' ? '0' : '4px 0',
|
|
||||||
outline: 'none',
|
|
||||||
resize: 'none',
|
|
||||||
fontSize: fontSize,
|
|
||||||
lineHeight: lineHeight,
|
|
||||||
border: 'none',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
// Standard editor without line numbers
|
|
||||||
<ContentEditable
|
|
||||||
style={{
|
|
||||||
minHeight: minheight,
|
|
||||||
padding: height ? '4px 6px' : variant === 'borderless' ? '0' : '6px 8px',
|
|
||||||
border: variant === 'borderless' ? 'none' : '1px solid #EBEBEB',
|
|
||||||
borderRadius: '8px',
|
|
||||||
outline: 'none',
|
|
||||||
resize: 'none',
|
|
||||||
fontSize: fontSize,
|
|
||||||
lineHeight: lineHeight,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
placeholder={
|
placeholder={
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
minHeight: placeHolderMinheight,
|
minHeight: placeHolderMinheight,
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
top: enableLineNumbers ? '4px' : variant === 'borderless' ? '0' : '6px',
|
top: variant === 'borderless' ? '0' : '6px',
|
||||||
left: enableLineNumbers ? '16px' : (variant === 'borderless' ? '0' : '11px'),
|
left: variant === 'borderless' ? '0' : '11px',
|
||||||
color: '#A8A9AA',
|
color: '#A8A9AA',
|
||||||
fontSize: fontSize,
|
fontSize: fontSize,
|
||||||
lineHeight: placeHolderMinheight,
|
lineHeight: placeHolderMinheight,
|
||||||
@@ -257,15 +143,12 @@ const Editor: FC<LexicalEditorProps> =({
|
|||||||
}
|
}
|
||||||
ErrorBoundary={LexicalErrorBoundary}
|
ErrorBoundary={LexicalErrorBoundary}
|
||||||
/>
|
/>
|
||||||
{/* Editor plugins */}
|
|
||||||
<HistoryPlugin />
|
<HistoryPlugin />
|
||||||
<CommandPlugin />
|
<CommandPlugin />
|
||||||
{language === 'jinja2' && <Jinja2HighlightPlugin />}
|
<AutocompletePlugin options={options} enableJinja2={false} />
|
||||||
{enableLineNumbers && <LineNumberPlugin />}
|
|
||||||
<AutocompletePlugin options={options} enableJinja2={enableJinja2} />
|
|
||||||
<CharacterCountPlugin setCount={(count) => { setCount(count) }} onChange={onChange} />
|
<CharacterCountPlugin setCount={(count) => { setCount(count) }} onChange={onChange} />
|
||||||
<InitialValuePlugin value={value} options={options} enableLineNumbers={enableLineNumbers} />
|
<InitialValuePlugin value={value} options={options} enableLineNumbers={false} />
|
||||||
<BlurPlugin enableJinja2={enableJinja2} />
|
<BlurPlugin enableJinja2={false} />
|
||||||
</div>
|
</div>
|
||||||
</LexicalComposer>
|
</LexicalComposer>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useEffect } from 'react';
|
import { useEffect, useRef } from 'react';
|
||||||
import { $getRoot, $isParagraphNode } from 'lexical';
|
import { $getRoot, $isParagraphNode } from 'lexical';
|
||||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
|
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
|
||||||
|
|
||||||
@@ -6,9 +6,15 @@ import { $isVariableNode } from '../nodes/VariableNode';
|
|||||||
|
|
||||||
const CharacterCountPlugin = ({ setCount, onChange }: { setCount: (count: number) => void; onChange?: (value: string) => void }) => {
|
const CharacterCountPlugin = ({ setCount, onChange }: { setCount: (count: number) => void; onChange?: (value: string) => void }) => {
|
||||||
const [editor] = useLexicalComposerContext();
|
const [editor] = useLexicalComposerContext();
|
||||||
|
const isReadyRef = useRef(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return editor.registerUpdateListener(({ editorState }) => {
|
return editor.registerUpdateListener(({ editorState, tags }) => {
|
||||||
|
if (tags.has('programmatic')) {
|
||||||
|
isReadyRef.current = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!isReadyRef.current) return;
|
||||||
editorState.read(() => {
|
editorState.read(() => {
|
||||||
const root = $getRoot();
|
const root = $getRoot();
|
||||||
let serializedContent = '';
|
let serializedContent = '';
|
||||||
|
|||||||
Reference in New Issue
Block a user