fix(web): VariableSelect & editor
This commit is contained in:
@@ -2,7 +2,7 @@
|
|||||||
* @Author: ZhaoYing
|
* @Author: ZhaoYing
|
||||||
* @Date: 2025-12-23 16:22:51
|
* @Date: 2025-12-23 16:22:51
|
||||||
* @Last Modified by: ZhaoYing
|
* @Last Modified by: ZhaoYing
|
||||||
* @Last Modified time: 2026-03-25 10:58:47
|
* @Last Modified time: 2026-04-03 20:44:16
|
||||||
*/
|
*/
|
||||||
import { type FC, useState, useMemo } from 'react';
|
import { type FC, useState, useMemo } from 'react';
|
||||||
import { LexicalComposer } from '@lexical/react/LexicalComposer';
|
import { LexicalComposer } from '@lexical/react/LexicalComposer';
|
||||||
@@ -147,7 +147,7 @@ const Editor: FC<LexicalEditorProps> =({
|
|||||||
<HistoryPlugin />
|
<HistoryPlugin />
|
||||||
<CommandPlugin />
|
<CommandPlugin />
|
||||||
<AutocompletePlugin options={options} enableJinja2={false} />
|
<AutocompletePlugin options={options} enableJinja2={false} />
|
||||||
<CharacterCountPlugin setCount={(count) => { setCount(count) }} onChange={onChange} />
|
<CharacterCountPlugin setCount={setCount} onChange={onChange} />
|
||||||
<InitialValuePlugin value={value} options={options} enableLineNumbers={false} />
|
<InitialValuePlugin value={value} options={options} enableLineNumbers={false} />
|
||||||
<BlurPlugin enableJinja2={false} />
|
<BlurPlugin enableJinja2={false} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -32,8 +32,7 @@ const VariableComponent: React.FC<{ nodeKey: NodeKey; data: Suggestion }> = ({
|
|||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
setSelected(!isSelected);
|
setSelected(!isSelected);
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log('data', data)
|
|
||||||
return (
|
return (
|
||||||
<span
|
<span
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
|
|||||||
@@ -6,15 +6,12 @@ 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);
|
const onChangeRef = useRef(onChange);
|
||||||
|
onChangeRef.current = onChange;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return editor.registerUpdateListener(({ editorState, tags }) => {
|
return editor.registerUpdateListener(({ editorState, tags }) => {
|
||||||
if (tags.has('programmatic')) {
|
if (tags.has('programmatic')) return;
|
||||||
isReadyRef.current = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!isReadyRef.current) return;
|
|
||||||
editorState.read(() => {
|
editorState.read(() => {
|
||||||
const root = $getRoot();
|
const root = $getRoot();
|
||||||
let serializedContent = '';
|
let serializedContent = '';
|
||||||
@@ -38,10 +35,10 @@ const CharacterCountPlugin = ({ setCount, onChange }: { setCount: (count: number
|
|||||||
serializedContent = paragraphs.join('\n');
|
serializedContent = paragraphs.join('\n');
|
||||||
|
|
||||||
setCount(serializedContent.length);
|
setCount(serializedContent.length);
|
||||||
onChange?.(serializedContent);
|
onChangeRef.current?.(serializedContent);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}, [editor, setCount, onChange]);
|
}, [editor, setCount]);
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -67,11 +67,10 @@ const FilterConditions: FC<FilterConditionsProps> = ({
|
|||||||
const form = Form.useFormInstance();
|
const form = Form.useFormInstance();
|
||||||
|
|
||||||
const handleKeyFieldChange = (index: number, newValue: string) => {
|
const handleKeyFieldChange = (index: number, newValue: string) => {
|
||||||
form.setFieldValue(['filter_by', index], {
|
form.setFieldValue([parentName, 'conditions', index], {
|
||||||
key: newValue,
|
key: newValue,
|
||||||
comparison_operator: undefined,
|
comparison_operator: undefined,
|
||||||
value: undefined,
|
value: undefined,
|
||||||
value_type: undefined,
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -85,8 +84,8 @@ const FilterConditions: FC<FilterConditionsProps> = ({
|
|||||||
className="rb:relative"
|
className="rb:relative"
|
||||||
>
|
>
|
||||||
{fields.map((field, index) => {
|
{fields.map((field, index) => {
|
||||||
const filter_by = form.getFieldValue(['filter_by']) || [];
|
const conditions = form.getFieldValue([parentName, 'conditions']) || [];
|
||||||
const currentCondition = filter_by[index] || {};
|
const currentCondition = conditions[index] || {};
|
||||||
const currentOperator = currentCondition.comparison_operator;
|
const currentOperator = currentCondition.comparison_operator;
|
||||||
const hideValueField = currentOperator === 'empty' || currentOperator === 'not_empty';
|
const hideValueField = currentOperator === 'empty' || currentOperator === 'not_empty';
|
||||||
const keyFieldValue = currentCondition.key;
|
const keyFieldValue = currentCondition.key;
|
||||||
@@ -103,9 +102,7 @@ const FilterConditions: FC<FilterConditionsProps> = ({
|
|||||||
>
|
>
|
||||||
<div className="rb:flex-1 rb:bg-[#F6F6F6] rb:rounded-lg">
|
<div className="rb:flex-1 rb:bg-[#F6F6F6] rb:rounded-lg">
|
||||||
{variableType === 'array[file]' &&
|
{variableType === 'array[file]' &&
|
||||||
<Row className={clsx("rb:p-1!", {
|
<Row className="rb:p-1! rb-border-b">
|
||||||
'rb-border-b': !hideValueField
|
|
||||||
})}>
|
|
||||||
<Col span={24}>
|
<Col span={24}>
|
||||||
<Form.Item name={[field.name, 'key']} noStyle>
|
<Form.Item name={[field.name, 'key']} noStyle>
|
||||||
<Select
|
<Select
|
||||||
@@ -121,7 +118,7 @@ const FilterConditions: FC<FilterConditionsProps> = ({
|
|||||||
</Row>
|
</Row>
|
||||||
}
|
}
|
||||||
<Row>
|
<Row>
|
||||||
<Col flex="96px">
|
<Col flex={hideValueField ? '1' : "96px"}>
|
||||||
<Form.Item name={[field.name, 'comparison_operator']} noStyle>
|
<Form.Item name={[field.name, 'comparison_operator']} noStyle>
|
||||||
<Select
|
<Select
|
||||||
options={operatorList.map(vo => ({
|
options={operatorList.map(vo => ({
|
||||||
|
|||||||
@@ -20,12 +20,12 @@ const ListOperator: FC<ListOperatorProps> = ({ options }) => {
|
|||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const form = Form.useFormInstance()
|
const form = Form.useFormInstance()
|
||||||
const values = Form.useWatch([], form) || {}
|
const values = Form.useWatch([], form) || {}
|
||||||
const variableOption = options.find(option => `{{${option.value}}}` === values?.variable)
|
const variableOption = options.find(option => `{{${option.value}}}` === values?.input_list)
|
||||||
const variableType = variableOption?.dataType
|
const variableType = variableOption?.dataType
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Form.Item name="variable" label={t('workflow.config.list-operator.variable')} required>
|
<Form.Item name="input_list" label={t('workflow.config.list-operator.variable')} required>
|
||||||
<VariableSelect
|
<VariableSelect
|
||||||
placeholder={t('common.pleaseSelect')}
|
placeholder={t('common.pleaseSelect')}
|
||||||
options={options.filter(vo => vo.dataType.includes('array') && vo.dataType !== 'array[object]')}
|
options={options.filter(vo => vo.dataType.includes('array') && vo.dataType !== 'array[object]')}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
* @Author: ZhaoYing
|
* @Author: ZhaoYing
|
||||||
* @Date: 2026-02-03 15:40:13
|
* @Date: 2026-02-03 15:40:13
|
||||||
* @Last Modified by: ZhaoYing
|
* @Last Modified by: ZhaoYing
|
||||||
* @Last Modified time: 2026-04-03 18:51:17
|
* @Last Modified time: 2026-04-03 20:19:34
|
||||||
*/
|
*/
|
||||||
import { useState, useRef, useEffect, useLayoutEffect, type FC } from 'react'
|
import { useState, useRef, useEffect, useLayoutEffect, type FC } from 'react'
|
||||||
import { createPortal } from 'react-dom'
|
import { createPortal } from 'react-dom'
|
||||||
@@ -49,36 +49,26 @@ const VariableSelect: FC<VariableSelectProps> = ({
|
|||||||
|
|
||||||
const CHILD_PANEL_HEIGHT = 280; // max-h-60 (240) + header (~40)
|
const CHILD_PANEL_HEIGHT = 280; // max-h-60 (240) + header (~40)
|
||||||
|
|
||||||
// Calculate dropdown position when opening
|
// Calculate dropdown position (runs synchronously after DOM paint to avoid flicker)
|
||||||
useEffect(() => {
|
|
||||||
if (!open || !containerRef.current) return;
|
|
||||||
const rect = containerRef.current.getBoundingClientRect();
|
|
||||||
setDropdownPos({ top: rect.bottom + 8, left: rect.left, width: rect.width });
|
|
||||||
}, [open]);
|
|
||||||
|
|
||||||
// Adjust dropdown vertical position after render
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
if (!open || !dropdownRef.current || !containerRef.current) return;
|
if (!open || !containerRef.current) return;
|
||||||
const triggerRect = containerRef.current.getBoundingClientRect();
|
const triggerRect = containerRef.current.getBoundingClientRect();
|
||||||
|
const MARGIN = 8;
|
||||||
|
const width = triggerRect.width;
|
||||||
|
// Set initial width/left immediately; top will be refined once dropdownRef is available
|
||||||
|
if (!dropdownRef.current) {
|
||||||
|
setDropdownPos({ top: triggerRect.bottom + MARGIN, left: triggerRect.left, width });
|
||||||
|
return;
|
||||||
|
}
|
||||||
const dropdownHeight = dropdownRef.current.offsetHeight;
|
const dropdownHeight = dropdownRef.current.offsetHeight;
|
||||||
const dropdownWidth = dropdownRef.current.offsetWidth;
|
const dropdownWidth = dropdownRef.current.offsetWidth;
|
||||||
const viewportHeight = window.innerHeight;
|
|
||||||
const MARGIN = 8;
|
|
||||||
|
|
||||||
// Horizontal: left-align to trigger, clamp to viewport
|
|
||||||
const left = Math.min(triggerRect.left, window.innerWidth - dropdownWidth - 10);
|
const left = Math.min(triggerRect.left, window.innerWidth - dropdownWidth - 10);
|
||||||
|
const spaceBelow = window.innerHeight - triggerRect.bottom - MARGIN;
|
||||||
const spaceBelow = viewportHeight - triggerRect.bottom - MARGIN;
|
|
||||||
const spaceAbove = triggerRect.top - MARGIN;
|
const spaceAbove = triggerRect.top - MARGIN;
|
||||||
|
const top = (spaceBelow >= dropdownHeight || spaceBelow >= spaceAbove)
|
||||||
let finalTop: number;
|
? triggerRect.bottom + MARGIN
|
||||||
if (spaceBelow >= dropdownHeight || spaceBelow >= spaceAbove) {
|
: Math.max(MARGIN, triggerRect.top - dropdownHeight - MARGIN);
|
||||||
finalTop = triggerRect.bottom + MARGIN;
|
setDropdownPos({ top, left, width });
|
||||||
} else {
|
|
||||||
finalTop = triggerRect.top - dropdownHeight - MARGIN;
|
|
||||||
if (finalTop < MARGIN) finalTop = MARGIN;
|
|
||||||
}
|
|
||||||
setDropdownPos(prev => ({ ...prev, top: finalTop, left }));
|
|
||||||
}, [open, search, Array.isArray(value) ? value.length : 0]);
|
}, [open, search, Array.isArray(value) ? value.length : 0]);
|
||||||
|
|
||||||
const filteredOptions = filterBooleanType
|
const filteredOptions = filterBooleanType
|
||||||
@@ -182,9 +172,10 @@ const VariableSelect: FC<VariableSelectProps> = ({
|
|||||||
const el = itemRefs.current.get(key);
|
const el = itemRefs.current.get(key);
|
||||||
if (el) {
|
if (el) {
|
||||||
const rect = el.getBoundingClientRect();
|
const rect = el.getBoundingClientRect();
|
||||||
const absoluteBottom = rect.top + CHILD_PANEL_HEIGHT;
|
const spaceBelow = window.innerHeight - rect.top - 10;
|
||||||
const overflow = absoluteBottom - (window.innerHeight - 10);
|
const top = spaceBelow >= CHILD_PANEL_HEIGHT
|
||||||
const top = overflow > 0 ? rect.top - overflow : rect.top;
|
? rect.top
|
||||||
|
: Math.max(10, window.innerHeight - CHILD_PANEL_HEIGHT - 10);
|
||||||
setChildPanelPos({ top, right: window.innerWidth - rect.left + 8 });
|
setChildPanelPos({ top, right: window.innerWidth - rect.left + 8 });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -203,7 +194,7 @@ const VariableSelect: FC<VariableSelectProps> = ({
|
|||||||
variant === 'outlined' && 'rb:border rb:border-[#d9d9d9] hover:rb:border-[#4096ff]',
|
variant === 'outlined' && 'rb:border rb:border-[#d9d9d9] hover:rb:border-[#4096ff]',
|
||||||
variant === 'outlined' && open && 'rb:border-[#4096ff] rb:shadow-[0_0_0_2px_rgba(5,145,255,0.1)]',
|
variant === 'outlined' && open && 'rb:border-[#4096ff] rb:shadow-[0_0_0_2px_rgba(5,145,255,0.1)]',
|
||||||
variant === 'borderless' && 'rb:border-none rb:shadow-none rb:bg-transparent',
|
variant === 'borderless' && 'rb:border-none rb:shadow-none rb:bg-transparent',
|
||||||
multiple ? 'rb:min-h-8 rb:py-1' : size === 'small' ? 'rb:h-6 rb:text-[10px]' : size === 'large' ? 'rb:h-10' : 'rb:h-8 rb:text-[12px]',
|
multiple && size === 'small' ? 'rb:min-h-6 rb:py-0.75' : multiple ? 'rb:min-h-8 rb:py-1' : size === 'small' ? 'rb:h-6 rb:text-[10px]' : size === 'large' ? 'rb:h-10' : 'rb:h-8 rb:text-[12px]',
|
||||||
!multiple && (size === 'small' ? 'rb:text-[10px]' : 'rb:text-[12px]'),
|
!multiple && (size === 'small' ? 'rb:text-[10px]' : 'rb:text-[12px]'),
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
* @Author: ZhaoYing
|
* @Author: ZhaoYing
|
||||||
* @Date: 2026-02-03 15:06:18
|
* @Date: 2026-02-03 15:06:18
|
||||||
* @Last Modified by: ZhaoYing
|
* @Last Modified by: ZhaoYing
|
||||||
* @Last Modified time: 2026-03-31 10:08:26
|
* @Last Modified time: 2026-04-03 20:28:08
|
||||||
*/
|
*/
|
||||||
import LoopNode from './components/Nodes/LoopNode';
|
import LoopNode from './components/Nodes/LoopNode';
|
||||||
import NormalNode from './components/Nodes/NormalNode';
|
import NormalNode from './components/Nodes/NormalNode';
|
||||||
@@ -463,7 +463,7 @@ export const nodeLibrary: NodeLibrary[] = [
|
|||||||
},
|
},
|
||||||
{ type: "list-operator", icon: 'rb:bg-[url("@/assets/images/workflow/list-operator.svg")]',
|
{ type: "list-operator", icon: 'rb:bg-[url("@/assets/images/workflow/list-operator.svg")]',
|
||||||
config: {
|
config: {
|
||||||
variable: {
|
input_list: {
|
||||||
type: 'variableList',
|
type: 'variableList',
|
||||||
},
|
},
|
||||||
filter_by: {
|
filter_by: {
|
||||||
|
|||||||
Reference in New Issue
Block a user