feat: get node variables in prompt editor (#2087)

* feat: get node variables in prompt editor

* fix

* fix build

* merge

* fix build

* delete default parent

* fix

* fix
This commit is contained in:
heheer
2024-07-23 11:20:23 +08:00
committed by GitHub
parent f24e41f5ec
commit a4787bce5c
23 changed files with 897 additions and 61 deletions

View File

@@ -5,7 +5,7 @@ import { ContentEditable } from '@lexical/react/LexicalContentEditable';
import { HistoryPlugin } from '@lexical/react/LexicalHistoryPlugin';
import { OnChangePlugin } from '@lexical/react/LexicalOnChangePlugin';
import LexicalErrorBoundary from '@lexical/react/LexicalErrorBoundary';
import VariablePickerPlugin from './plugins/VariablePickerPlugin';
import VariableLabelPickerPlugin from './plugins/VariableLabelPickerPlugin';
import { Box } from '@chakra-ui/react';
import styles from './index.module.scss';
import VariablePlugin from './plugins/VariablePlugin';
@@ -18,6 +18,8 @@ import { getNanoid } from '@fastgpt/global/common/string/tools';
import FocusPlugin from './plugins/FocusPlugin';
import { textToEditorState } from './utils';
import { MaxLengthPlugin } from './plugins/MaxLengthPlugin';
import { VariableLabelNode } from './plugins/VariableLabelPlugin/node';
import VariableLabelPlugin from './plugins/VariableLabelPlugin';
export default function Editor({
h = 200,
@@ -51,7 +53,7 @@ export default function Editor({
const initialConfig = {
namespace: 'promptEditor',
nodes: [VariableNode],
nodes: [VariableNode, VariableLabelNode],
editorState: textToEditorState(value),
onError: (error: Error) => {
throw error;
@@ -127,8 +129,9 @@ export default function Editor({
});
}}
/>
<VariablePickerPlugin variables={variables} />
<VariableLabelPickerPlugin variables={variables} />
<VariablePlugin variables={variables} />
<VariableLabelPlugin variables={variables} />
<OnBlurPlugin onBlur={onBlur} />
</LexicalComposer>
{showResize && (

View File

@@ -2,10 +2,11 @@
position: relative;
height: 100%;
width: 100%;
border: 1px solid var(--chakra-colors-borderColor-base);
border: 1px solid rgb(232, 235, 240);
border-radius: var(--chakra-radii-md);
padding: 8px 12px;
background: var(--chakra-colors-gray-50);
background: #fff;
font-size: var(--chakra-fontSizes-sm);
overflow-y: auto;
}

View File

@@ -0,0 +1,228 @@
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
import { LexicalTypeaheadMenuPlugin } from '@lexical/react/LexicalTypeaheadMenuPlugin';
import { $createTextNode, $getSelection, $isRangeSelection, TextNode } from 'lexical';
import * as React from 'react';
import { useCallback, useState } from 'react';
import * as ReactDOM from 'react-dom';
import { Box, Flex } from '@chakra-ui/react';
import { useBasicTypeaheadTriggerMatch } from '../../utils';
import { EditorVariablePickerType } from '../../type';
import { WorkflowIOValueTypeEnum } from '@fastgpt/global/core/workflow/constants';
import { useTranslation } from 'react-i18next';
import Avatar from '../../../../Avatar';
type EditorVariablePickerType1 = {
key: string;
label: string;
required?: boolean;
icon?: string;
valueType?: WorkflowIOValueTypeEnum;
index: number;
};
interface TransformedParent {
id: string;
label: string;
avatar: string;
children: EditorVariablePickerType1[];
}
export default function VariableLabelPickerPlugin({
variables
}: {
variables: EditorVariablePickerType[];
}) {
const { t } = useTranslation();
const [editor] = useLexicalComposerContext();
const [queryString, setQueryString] = useState<string | null>(null);
const checkForTriggerMatch = useBasicTypeaheadTriggerMatch('/', {
minLength: 0
});
const onSelectOption = useCallback(
(selectedOption: any, nodeToRemove: TextNode | null, closeMenu: () => void) => {
editor.update(() => {
const selection = $getSelection();
if (!$isRangeSelection(selection) || selectedOption == null) {
return;
}
if (nodeToRemove) {
nodeToRemove.remove();
}
selection.insertNodes([
$createTextNode(`{{$${selectedOption.parent?.id}.${selectedOption.key}$}}`)
]);
closeMenu();
});
},
[editor]
);
return (
<LexicalTypeaheadMenuPlugin
onQueryChange={setQueryString}
onSelectOption={onSelectOption}
triggerFn={checkForTriggerMatch}
options={variables}
menuRenderFn={(
anchorElementRef,
{ selectedIndex, selectOptionAndCleanUp, setHighlightedIndex }
) => {
if (anchorElementRef.current == null) {
return null;
}
return anchorElementRef.current && variables.length
? ReactDOM.createPortal(
<Box
bg={'white'}
boxShadow={'lg'}
borderWidth={'1px'}
borderColor={'borderColor.base'}
p={2}
borderRadius={'md'}
position={'absolute'}
w={'auto'}
maxH={'300px'}
minW={'240px'}
overflow={'auto'}
zIndex={99999}
>
{variableFilter(variables, queryString || '').length === variables.length && (
<Box fontSize={'xs'} ml={4}>
{t('workflow:variable_picker_tips')}
</Box>
)}
{variableFilter(variables, queryString || '').length > 0 ? (
transformData(variableFilter(variables, queryString || '')).map((item) => {
return (
<Flex
key={item.id}
flexDirection={'column'}
px={4}
py={2}
_notLast={{
borderBottom: '1px solid',
borderColor: 'myGray.200'
}}
>
<Flex alignItems={'center'} mb={1.5}>
<Avatar
src={item.avatar as any}
w={'16px'}
borderRadius={'2.8px'}
display={'inline-flex'}
verticalAlign={'middle'}
/>
<Box
mx={2}
fontSize={'sm'}
whiteSpace={'nowrap'}
color={'myGray.600'}
fontWeight={'semibold'}
>
{item.label}
</Box>
</Flex>
{item.children?.map((child, index) => (
<Flex
alignItems={'center'}
as={'li'}
key={child.key}
px={4}
py={1.5}
rounded={'md'}
cursor={'pointer'}
overflow={'auto'}
_notLast={{
mb: 1
}}
{...(selectedIndex === child.index
? {
bg: '#1118240D',
color: 'primary.700'
}
: {
bg: 'white',
color: 'myGray.600'
})}
onClick={() => {
setHighlightedIndex(child.index);
selectOptionAndCleanUp({ ...child, parent: item });
}}
onMouseEnter={() => {
setHighlightedIndex(child.index);
}}
>
<Box ml={2} fontSize={'sm'} whiteSpace={'nowrap'}>
{child.label}
</Box>
</Flex>
))}
</Flex>
);
})
) : (
<Box p={2} color={'myGray.400'} fontSize={'sm'}>
{t('common:unusable_variable')}
</Box>
)}
</Box>,
anchorElementRef.current
)
: null;
}}
/>
);
}
function transformData(data: EditorVariablePickerType[]): TransformedParent[] {
const transformedData: TransformedParent[] = [];
const parentMap: { [key: string]: TransformedParent } = {};
data.forEach((item, index) => {
const parentId = item.parent!.id;
const parentLabel = item.parent!.label;
const parentAvatar = item.parent!.avatar;
if (!parentMap[parentId]) {
parentMap[parentId] = {
id: parentId,
label: parentLabel,
avatar: parentAvatar || '',
children: []
};
}
parentMap[parentId].children.push({
label: item.label,
key: item.key,
icon: item.icon,
index
});
});
const addedParents = new Set<string>();
data.forEach((item) => {
const parentId = item.parent!.id;
if (!addedParents.has(parentId)) {
transformedData.push(parentMap[parentId]);
addedParents.add(parentId);
}
});
return transformedData;
}
function variableFilter(
data: EditorVariablePickerType[],
queryString: string
): EditorVariablePickerType[] {
const lowerCaseQuery = queryString.toLowerCase();
return data.filter((item) => {
const labelMatch = item.label.toLowerCase().includes(lowerCaseQuery);
const keyMatch = item.key.toLowerCase().includes(lowerCaseQuery);
const parentLabelMatch = item.parent!.label.toLowerCase().includes(lowerCaseQuery);
return labelMatch || keyMatch || parentLabelMatch;
});
}

View File

@@ -0,0 +1,51 @@
import { ChevronRightIcon } from '@chakra-ui/icons';
import { Box } from '@chakra-ui/react';
import { useTranslation } from 'react-i18next';
import Avatar from '../../../../../../../components/common/Avatar';
export default function VariableLabel({
variableLabel,
nodeAvatar
}: {
variableLabel: string;
nodeAvatar: string;
}) {
const { t } = useTranslation();
const [parentLabel, childLabel] = variableLabel.split('.');
return (
<>
<Box
display="inline-flex"
alignItems="center"
m={'2px'}
rounded={'4px'}
px={1.5}
py={'1px'}
bg={parentLabel !== 'undefined' ? 'primary.50' : 'red.50'}
color={parentLabel !== 'undefined' ? 'myGray.900' : 'red.600'}
>
{parentLabel !== 'undefined' ? (
<span>
<Avatar
src={nodeAvatar as any}
w={'16px'}
mr={1}
borderRadius={'2.8px'}
display={'inline-flex'}
verticalAlign={'middle'}
mb={'3px'}
/>
{parentLabel}
<ChevronRightIcon />
{childLabel}
</span>
) : (
<>
<Box>{t('common:invalid_variable')}</Box>
</>
)}
</Box>
</>
);
}

View File

@@ -0,0 +1,58 @@
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
import { EditorVariablePickerType } from '../../type';
import { useCallback, useEffect, useMemo } from 'react';
import { $createVariableLabelNode, VariableLabelNode } from './node';
import { TextNode } from 'lexical';
import { getHashtagRegexString } from './utils';
import { mergeRegister } from '@lexical/utils';
import { registerLexicalTextEntity } from '../../utils';
const REGEX = new RegExp(getHashtagRegexString(), 'i');
export default function VariableLabelPlugin({
variables
}: {
variables: EditorVariablePickerType[];
}) {
const [editor] = useLexicalComposerContext();
useEffect(() => {
if (!editor.hasNodes([VariableLabelNode]))
throw new Error('VariableLabelPlugin: VariableLabelPlugin not registered on editor');
}, [editor]);
const createVariableLabelPlugin = useCallback((textNode: TextNode): VariableLabelNode => {
const [parentKey, childrenKey] = textNode.getTextContent().slice(3, -3).split('.');
const currentVariable = variables.find(
(item) => item.parent?.id === parentKey && item.key === childrenKey
);
const variableLabel = `${currentVariable && currentVariable.parent?.label}.${currentVariable?.label}`;
const nodeAvatar = currentVariable?.parent?.avatar || '';
return $createVariableLabelNode(textNode.getTextContent(), variableLabel, nodeAvatar);
}, []);
const getVariableMatch = useCallback((text: string) => {
const matches = REGEX.exec(text);
if (!matches) return null;
// if (variableKeys.indexOf(matches[4]) === -1) return null;
const hashtagLength = matches[4].length + 6;
const startOffset = matches.index;
const endOffset = startOffset + hashtagLength;
return {
end: endOffset,
start: startOffset
};
}, []);
useEffect(() => {
mergeRegister(
...registerLexicalTextEntity(
editor,
getVariableMatch,
VariableLabelNode,
createVariableLabelPlugin
)
);
}, [createVariableLabelPlugin, editor, getVariableMatch]);
return null;
}

View File

@@ -0,0 +1,124 @@
import {
DecoratorNode,
DOMConversionMap,
DOMExportOutput,
EditorConfig,
LexicalEditor,
LexicalNode,
NodeKey,
SerializedLexicalNode,
Spread,
TextFormatType
} from 'lexical';
import VariableLabel from './components/VariableLabel';
export type SerializedVariableLabelNode = Spread<
{
variableKey: string;
variableLabel: string;
nodeAvatar: string;
format: number | TextFormatType;
},
SerializedLexicalNode
>;
export class VariableLabelNode extends DecoratorNode<JSX.Element> {
__format: number | TextFormatType;
__variableKey: string;
__variableLabel: string;
__nodeAvatar: string;
static getType(): string {
return 'variableLabel';
}
static clone(node: VariableLabelNode): VariableLabelNode {
return new VariableLabelNode(
node.__variableKey,
node.__variableLabel,
node.__nodeAvatar,
node.__format,
node.__key
);
}
constructor(
variableKey: string,
variableLabel: string,
nodeAvatar: string,
format?: number | TextFormatType,
key?: NodeKey
) {
super(key);
this.__variableKey = variableKey;
this.__format = format || 0;
this.__variableLabel = variableLabel;
this.__nodeAvatar = nodeAvatar;
}
static importJSON(serializedNode: SerializedVariableLabelNode): VariableLabelNode {
const node = $createVariableLabelNode(
serializedNode.variableKey,
serializedNode.variableLabel,
serializedNode.nodeAvatar
);
node.setFormat(serializedNode.format);
return node;
}
setFormat(format: number | TextFormatType): void {
const self = this.getWritable();
self.__format = format;
}
getFormat(): number | TextFormatType {
return this.__format;
}
exportJSON(): SerializedVariableLabelNode {
return {
format: this.__format || 0,
type: 'variableLabel',
version: 1,
variableKey: this.getVariableKey(),
variableLabel: this.__variableLabel,
nodeAvatar: this.__nodeAvatar
};
}
createDOM(): HTMLElement {
const element = document.createElement('span');
return element;
}
exportDOM(): DOMExportOutput {
const element = document.createElement('span');
return { element };
}
static importDOM(): DOMConversionMap | null {
return {};
}
updateDOM(): false {
return false;
}
getVariableKey(): string {
return this.__variableKey;
}
getTextContent(
_includeInert?: boolean | undefined,
_includeDirectionless?: false | undefined
): string {
return `${this.__variableKey}`;
}
decorate(_editor: LexicalEditor, config: EditorConfig): JSX.Element {
return <VariableLabel variableLabel={this.__variableLabel} nodeAvatar={this.__nodeAvatar} />;
}
}
export function $createVariableLabelNode(
variableKey: string,
variableLabel: string,
nodeAvatar: string
): VariableLabelNode {
return new VariableLabelNode(variableKey, variableLabel, nodeAvatar);
}
export function $isVariableLabelNode(
node: VariableLabelNode | LexicalNode | null | undefined
): node is VariableLabelNode {
return node instanceof VariableLabelNode;
}

View File

@@ -0,0 +1,228 @@
function getHashtagRegexVariableLabels(): Readonly<{
alpha: string;
alphanumeric: string;
leftChars: string;
rightChars: string;
middleChars: string;
}> {
// Latin accented characters
// Excludes 0xd7 from the range
// (the multiplication sign, confusable with "x").
// Also excludes 0xf7, the division sign
const latinAccents =
'\xC0-\xD6' +
'\xD8-\xF6' +
'\xF8-\xFF' +
'\u0100-\u024F' +
'\u0253-\u0254' +
'\u0256-\u0257' +
'\u0259' +
'\u025B' +
'\u0263' +
'\u0268' +
'\u026F' +
'\u0272' +
'\u0289' +
'\u028B' +
'\u02BB' +
'\u0300-\u036F' +
'\u1E00-\u1EFF';
// Cyrillic (Russian, Ukrainian, etc.)
const nonLatinChars =
'\u0400-\u04FF' + // Cyrillic
'\u0500-\u0527' + // Cyrillic Supplement
'\u2DE0-\u2DFF' + // Cyrillic Extended A
'\uA640-\uA69F' + // Cyrillic Extended B
'\u0591-\u05BF' + // Hebrew
'\u05C1-\u05C2' +
'\u05C4-\u05C5' +
'\u05C7' +
'\u05D0-\u05EA' +
'\u05F0-\u05F4' +
'\uFB12-\uFB28' + // Hebrew Presentation Forms
'\uFB2A-\uFB36' +
'\uFB38-\uFB3C' +
'\uFB3E' +
'\uFB40-\uFB41' +
'\uFB43-\uFB44' +
'\uFB46-\uFB4F' +
'\u0610-\u061A' + // Arabic
'\u0620-\u065F' +
'\u066E-\u06D3' +
'\u06D5-\u06DC' +
'\u06DE-\u06E8' +
'\u06EA-\u06EF' +
'\u06FA-\u06FC' +
'\u06FF' +
'\u0750-\u077F' + // Arabic Supplement
'\u08A0' + // Arabic Extended A
'\u08A2-\u08AC' +
'\u08E4-\u08FE' +
'\uFB50-\uFBB1' + // Arabic Pres. Forms A
'\uFBD3-\uFD3D' +
'\uFD50-\uFD8F' +
'\uFD92-\uFDC7' +
'\uFDF0-\uFDFB' +
'\uFE70-\uFE74' + // Arabic Pres. Forms B
'\uFE76-\uFEFC' +
'\u200C-\u200C' + // Zero-Width Non-Joiner
'\u0E01-\u0E3A' + // Thai
'\u0E40-\u0E4E' + // Hangul (Korean)
'\u1100-\u11FF' + // Hangul Jamo
'\u3130-\u3185' + // Hangul Compatibility Jamo
'\uA960-\uA97F' + // Hangul Jamo Extended-A
'\uAC00-\uD7AF' + // Hangul Syllables
'\uD7B0-\uD7FF' + // Hangul Jamo Extended-B
'\uFFA1-\uFFDC'; // Half-width Hangul
const charCode = String.fromCharCode;
const cjkChars =
'\u30A1-\u30FA\u30FC-\u30FE' + // Katakana (full-width)
'\uFF66-\uFF9F' + // Katakana (half-width)
'\uFF10-\uFF19\uFF21-\uFF3A' +
'\uFF41-\uFF5A' + // Latin (full-width)
'\u3041-\u3096\u3099-\u309E' + // Hiragana
'\u3400-\u4DBF' + // Kanji (CJK Extension A)
`\u4E00-\u9FFF${
// Kanji (Unified)
// Disabled as it breaks the Regex.
// charCode(0x20000) + '-' + charCode(0x2A6DF) + // Kanji (CJK Extension B)
charCode(0x2a700)
}-${
charCode(0x2b73f) // Kanji (CJK Extension C)
}${charCode(0x2b740)}-${
charCode(0x2b81f) // Kanji (CJK Extension D)
}${charCode(0x2f800)}-${charCode(0x2fa1f)}\u3003\u3005\u303B`; // Kanji (CJK supplement)
const otherChars = latinAccents + nonLatinChars + cjkChars;
// equivalent of \p{L}
const unicodeLetters =
'\u0041-\u005A\u0061-\u007A\u00AA\u00B5\u00BA\u00C0-\u00D6\u00D8-\u00F6' +
'\u00F8-\u0241\u0250-\u02C1\u02C6-\u02D1\u02E0-\u02E4\u02EE\u037A\u0386' +
'\u0388-\u038A\u038C\u038E-\u03A1\u03A3-\u03CE\u03D0-\u03F5\u03F7-\u0481' +
'\u048A-\u04CE\u04D0-\u04F9\u0500-\u050F\u0531-\u0556\u0559\u0561-\u0587' +
'\u05D0-\u05EA\u05F0-\u05F2\u0621-\u063A\u0640-\u064A\u066E-\u066F' +
'\u0671-\u06D3\u06D5\u06E5-\u06E6\u06EE-\u06EF\u06FA-\u06FC\u06FF\u0710' +
'\u0712-\u072F\u074D-\u076D\u0780-\u07A5\u07B1\u0904-\u0939\u093D\u0950' +
'\u0958-\u0961\u097D\u0985-\u098C\u098F-\u0990\u0993-\u09A8\u09AA-\u09B0' +
'\u09B2\u09B6-\u09B9\u09BD\u09CE\u09DC-\u09DD\u09DF-\u09E1\u09F0-\u09F1' +
'\u0A05-\u0A0A\u0A0F-\u0A10\u0A13-\u0A28\u0A2A-\u0A30\u0A32-\u0A33' +
'\u0A35-\u0A36\u0A38-\u0A39\u0A59-\u0A5C\u0A5E\u0A72-\u0A74\u0A85-\u0A8D' +
'\u0A8F-\u0A91\u0A93-\u0AA8\u0AAA-\u0AB0\u0AB2-\u0AB3\u0AB5-\u0AB9\u0ABD' +
'\u0AD0\u0AE0-\u0AE1\u0B05-\u0B0C\u0B0F-\u0B10\u0B13-\u0B28\u0B2A-\u0B30' +
'\u0B32-\u0B33\u0B35-\u0B39\u0B3D\u0B5C-\u0B5D\u0B5F-\u0B61\u0B71\u0B83' +
'\u0B85-\u0B8A\u0B8E-\u0B90\u0B92-\u0B95\u0B99-\u0B9A\u0B9C\u0B9E-\u0B9F' +
'\u0BA3-\u0BA4\u0BA8-\u0BAA\u0BAE-\u0BB9\u0C05-\u0C0C\u0C0E-\u0C10' +
'\u0C12-\u0C28\u0C2A-\u0C33\u0C35-\u0C39\u0C60-\u0C61\u0C85-\u0C8C' +
'\u0C8E-\u0C90\u0C92-\u0CA8\u0CAA-\u0CB3\u0CB5-\u0CB9\u0CBD\u0CDE' +
'\u0CE0-\u0CE1\u0D05-\u0D0C\u0D0E-\u0D10\u0D12-\u0D28\u0D2A-\u0D39' +
'\u0D60-\u0D61\u0D85-\u0D96\u0D9A-\u0DB1\u0DB3-\u0DBB\u0DBD\u0DC0-\u0DC6' +
'\u0E01-\u0E30\u0E32-\u0E33\u0E40-\u0E46\u0E81-\u0E82\u0E84\u0E87-\u0E88' +
'\u0E8A\u0E8D\u0E94-\u0E97\u0E99-\u0E9F\u0EA1-\u0EA3\u0EA5\u0EA7' +
'\u0EAA-\u0EAB\u0EAD-\u0EB0\u0EB2-\u0EB3\u0EBD\u0EC0-\u0EC4\u0EC6' +
'\u0EDC-\u0EDD\u0F00\u0F40-\u0F47\u0F49-\u0F6A\u0F88-\u0F8B\u1000-\u1021' +
'\u1023-\u1027\u1029-\u102A\u1050-\u1055\u10A0-\u10C5\u10D0-\u10FA\u10FC' +
'\u1100-\u1159\u115F-\u11A2\u11A8-\u11F9\u1200-\u1248\u124A-\u124D' +
'\u1250-\u1256\u1258\u125A-\u125D\u1260-\u1288\u128A-\u128D\u1290-\u12B0' +
'\u12B2-\u12B5\u12B8-\u12BE\u12C0\u12C2-\u12C5\u12C8-\u12D6\u12D8-\u1310' +
'\u1312-\u1315\u1318-\u135A\u1380-\u138F\u13A0-\u13F4\u1401-\u166C' +
'\u166F-\u1676\u1681-\u169A\u16A0-\u16EA\u1700-\u170C\u170E-\u1711' +
'\u1720-\u1731\u1740-\u1751\u1760-\u176C\u176E-\u1770\u1780-\u17B3\u17D7' +
'\u17DC\u1820-\u1877\u1880-\u18A8\u1900-\u191C\u1950-\u196D\u1970-\u1974' +
'\u1980-\u19A9\u19C1-\u19C7\u1A00-\u1A16\u1D00-\u1DBF\u1E00-\u1E9B' +
'\u1EA0-\u1EF9\u1F00-\u1F15\u1F18-\u1F1D\u1F20-\u1F45\u1F48-\u1F4D' +
'\u1F50-\u1F57\u1F59\u1F5B\u1F5D\u1F5F-\u1F7D\u1F80-\u1FB4\u1FB6-\u1FBC' +
'\u1FBE\u1FC2-\u1FC4\u1FC6-\u1FCC\u1FD0-\u1FD3\u1FD6-\u1FDB\u1FE0-\u1FEC' +
'\u1FF2-\u1FF4\u1FF6-\u1FFC\u2071\u207F\u2090-\u2094\u2102\u2107' +
'\u210A-\u2113\u2115\u2119-\u211D\u2124\u2126\u2128\u212A-\u212D' +
'\u212F-\u2131\u2133-\u2139\u213C-\u213F\u2145-\u2149\u2C00-\u2C2E' +
'\u2C30-\u2C5E\u2C80-\u2CE4\u2D00-\u2D25\u2D30-\u2D65\u2D6F\u2D80-\u2D96' +
'\u2DA0-\u2DA6\u2DA8-\u2DAE\u2DB0-\u2DB6\u2DB8-\u2DBE\u2DC0-\u2DC6' +
'\u2DC8-\u2DCE\u2DD0-\u2DD6\u2DD8-\u2DDE\u3005-\u3006\u3031-\u3035' +
'\u303B-\u303C\u3041-\u3096\u309D-\u309F\u30A1-\u30FA\u30FC-\u30FF' +
'\u3105-\u312C\u3131-\u318E\u31A0-\u31B7\u31F0-\u31FF\u3400-\u4DB5' +
'\u4E00-\u9FBB\uA000-\uA48C\uA800-\uA801\uA803-\uA805\uA807-\uA80A' +
'\uA80C-\uA822\uAC00-\uD7A3\uF900-\uFA2D\uFA30-\uFA6A\uFA70-\uFAD9' +
'\uFB00-\uFB06\uFB13-\uFB17\uFB1D\uFB1F-\uFB28\uFB2A-\uFB36\uFB38-\uFB3C' +
'\uFB3E\uFB40-\uFB41\uFB43-\uFB44\uFB46-\uFBB1\uFBD3-\uFD3D\uFD50-\uFD8F' +
'\uFD92-\uFDC7\uFDF0-\uFDFB\uFE70-\uFE74\uFE76-\uFEFC\uFF21-\uFF3A' +
'\uFF41-\uFF5A\uFF66-\uFFBE\uFFC2-\uFFC7\uFFCA-\uFFCF\uFFD2-\uFFD7' +
'\uFFDA-\uFFDC';
// equivalent of \p{Mn}\p{Mc}
const unicodeAccents =
'\u0300-\u036F\u0483-\u0486\u0591-\u05B9\u05BB-\u05BD\u05BF' +
'\u05C1-\u05C2\u05C4-\u05C5\u05C7\u0610-\u0615\u064B-\u065E\u0670' +
'\u06D6-\u06DC\u06DF-\u06E4\u06E7-\u06E8\u06EA-\u06ED\u0711\u0730-\u074A' +
'\u07A6-\u07B0\u0901-\u0903\u093C\u093E-\u094D\u0951-\u0954\u0962-\u0963' +
'\u0981-\u0983\u09BC\u09BE-\u09C4\u09C7-\u09C8\u09CB-\u09CD\u09D7' +
'\u09E2-\u09E3\u0A01-\u0A03\u0A3C\u0A3E-\u0A42\u0A47-\u0A48\u0A4B-\u0A4D' +
'\u0A70-\u0A71\u0A81-\u0A83\u0ABC\u0ABE-\u0AC5\u0AC7-\u0AC9\u0ACB-\u0ACD' +
'\u0AE2-\u0AE3\u0B01-\u0B03\u0B3C\u0B3E-\u0B43\u0B47-\u0B48\u0B4B-\u0B4D' +
'\u0B56-\u0B57\u0B82\u0BBE-\u0BC2\u0BC6-\u0BC8\u0BCA-\u0BCD\u0BD7' +
'\u0C01-\u0C03\u0C3E-\u0C44\u0C46-\u0C48\u0C4A-\u0C4D\u0C55-\u0C56' +
'\u0C82-\u0C83\u0CBC\u0CBE-\u0CC4\u0CC6-\u0CC8\u0CCA-\u0CCD\u0CD5-\u0CD6' +
'\u0D02-\u0D03\u0D3E-\u0D43\u0D46-\u0D48\u0D4A-\u0D4D\u0D57\u0D82-\u0D83' +
'\u0DCA\u0DCF-\u0DD4\u0DD6\u0DD8-\u0DDF\u0DF2-\u0DF3\u0E31\u0E34-\u0E3A' +
'\u0E47-\u0E4E\u0EB1\u0EB4-\u0EB9\u0EBB-\u0EBC\u0EC8-\u0ECD\u0F18-\u0F19' +
'\u0F35\u0F37\u0F39\u0F3E-\u0F3F\u0F71-\u0F84\u0F86-\u0F87\u0F90-\u0F97' +
'\u0F99-\u0FBC\u0FC6\u102C-\u1032\u1036-\u1039\u1056-\u1059\u135F' +
'\u1712-\u1714\u1732-\u1734\u1752-\u1753\u1772-\u1773\u17B6-\u17D3\u17DD' +
'\u180B-\u180D\u18A9\u1920-\u192B\u1930-\u193B\u19B0-\u19C0\u19C8-\u19C9' +
'\u1A17-\u1A1B\u1DC0-\u1DC3\u20D0-\u20DC\u20E1\u20E5-\u20EB\u302A-\u302F' +
'\u3099-\u309A\uA802\uA806\uA80B\uA823-\uA827\uFB1E\uFE00-\uFE0F' +
'\uFE20-\uFE23';
// equivalent of \p{Dn}
const unicodeDigits =
'\u0030-\u0039\u0660-\u0669\u06F0-\u06F9\u0966-\u096F\u09E6-\u09EF' +
'\u0A66-\u0A6F\u0AE6-\u0AEF\u0B66-\u0B6F\u0BE6-\u0BEF\u0C66-\u0C6F' +
'\u0CE6-\u0CEF\u0D66-\u0D6F\u0E50-\u0E59\u0ED0-\u0ED9\u0F20-\u0F29' +
'\u1040-\u1049\u17E0-\u17E9\u1810-\u1819\u1946-\u194F\u19D0-\u19D9' +
'\uFF10-\uFF19';
// An alpha char is a unicode chars including unicode marks or
// letter or char in otherChars range
const alpha = unicodeLetters;
// A numeric character is any with the number digit property, or
// underscore. These characters can be included in hashtags, but a hashtag
// cannot have only these characters.
const numeric = `${unicodeDigits}_`;
// Alphanumeric char is any alpha char or a unicode char with decimal
// number property \p{Nd}
const alphanumeric = alpha + numeric;
const leftChars = '{';
const rightChars = '}';
const middleChars = '$';
return {
alpha,
alphanumeric,
leftChars,
rightChars,
middleChars
};
}
export function getHashtagRegexString(): string {
const { leftChars, rightChars, middleChars } = getHashtagRegexVariableLabels();
const hashLeftCharList = `[${leftChars}]`;
const hashRightCharList = `[${rightChars}]`;
const hashMiddleCharList = `[${middleChars}]`;
// A hashtag contains characters, numbers and underscores,
// but not all numbers.
const hashtag =
`(${hashLeftCharList})` +
`(${hashLeftCharList})` +
`(${hashMiddleCharList})([a-zA-Z0-9_\\.]{0,29})(${hashMiddleCharList})` +
`(${hashRightCharList})(${hashRightCharList})`;
return hashtag;
}

View File

@@ -6,4 +6,9 @@ export type EditorVariablePickerType = {
required?: boolean;
icon?: string;
valueType?: WorkflowIOValueTypeEnum;
parent?: {
id: string;
label: string;
avatar?: string;
};
};

View File

@@ -1,9 +1,10 @@
import type { Klass, LexicalEditor, LexicalNode } from 'lexical';
import type { DecoratorNode, Klass, LexicalEditor, LexicalNode } from 'lexical';
import type { EntityMatch } from '@lexical/text';
import { $createTextNode, $getRoot, $isTextNode, TextNode } from 'lexical';
import { useCallback } from 'react';
import { VariableLabelNode } from './plugins/VariableLabelPlugin/node';
export function registerLexicalTextEntity<T extends TextNode>(
export function registerLexicalTextEntity<T extends TextNode | VariableLabelNode>(
editor: LexicalEditor,
getMatch: (text: string) => null | EntityMatch,
targetNode: Klass<T>,
@@ -13,7 +14,7 @@ export function registerLexicalTextEntity<T extends TextNode>(
return node instanceof targetNode;
};
const replaceWithSimpleText = (node: TextNode): void => {
const replaceWithSimpleText = (node: TextNode | VariableLabelNode): void => {
const textNode = $createTextNode(node.getTextContent());
textNode.setFormat(node.getFormat());
node.replace(textNode);
@@ -136,7 +137,7 @@ export function registerLexicalTextEntity<T extends TextNode>(
return;
}
if (text.length > match.end) {
if (text.length > match.end && $isTextNode(node)) {
// This will split out the rest of the text as simple text
node.splitText(match.end);
@@ -163,7 +164,7 @@ export function registerLexicalTextEntity<T extends TextNode>(
};
const removePlainTextTransform = editor.registerNodeTransform(TextNode, textNodeTransform);
const removeReverseNodeTransform = editor.registerNodeTransform<T>(
const removeReverseNodeTransform = editor.registerNodeTransform<any>(
targetNode,
reverseNodeTransform
);
@@ -217,6 +218,8 @@ export function editorStateToText(editor: LexicalEditor) {
`);
} else if (child.text) {
paragraphText.push(child.text);
} else if (child.type === 'variableLabel') {
paragraphText.push(child.variableKey);
}
});
editorStateTextString.push(paragraphText.join(''));