fix(web): VariableSelect & editor

This commit is contained in:
zhaoying
2026-04-03 20:45:37 +08:00
parent 1f72b8aa70
commit bd2a3bd7ef
7 changed files with 37 additions and 53 deletions

View File

@@ -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>

View File

@@ -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}

View File

@@ -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;
} }

View File

@@ -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 => ({

View File

@@ -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]')}

View File

@@ -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
)} )}

View File

@@ -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: {