Merge pull request #790 from SuanmoSuanyangTechnology/feature/ui_upgrade_zy

Feature/UI upgrade zy
This commit is contained in:
yingzhao
2026-04-03 20:50:05 +08:00
committed by GitHub
8 changed files with 76 additions and 116 deletions

View File

@@ -2,7 +2,7 @@
* @Author: ZhaoYing
* @Date: 2025-12-23 16:22:51
* @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 { LexicalComposer } from '@lexical/react/LexicalComposer';
@@ -149,10 +149,10 @@ const Editor: FC<LexicalEditorProps> =({
/>
<HistoryPlugin />
<CommandPlugin />
<AutocompletePlugin options={options} />
<CharacterCountPlugin setCount={(count) => { setCount(count) }} onChange={onChange} waitForInit={waitForInit || !!value} />
<InitialValuePlugin value={value} options={options} />
<BlurPlugin />
<AutocompletePlugin options={options} enableJinja2={false} />
<CharacterCountPlugin setCount={setCount} onChange={onChange} />
<InitialValuePlugin value={value} options={options} enableLineNumbers={false} />
<BlurPlugin enableJinja2={false} />
</div>
</LexicalComposer>
);

View File

@@ -32,8 +32,7 @@ const VariableComponent: React.FC<{ nodeKey: NodeKey; data: Suggestion }> = ({
e.stopPropagation();
setSelected(!isSelected);
};
console.log('data', data)
return (
<span
onClick={handleClick}

View File

@@ -1,73 +1,46 @@
/*
* @Author: ZhaoYing
* @Date: 2025-12-23 16:22:51
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-04-02 17:13:45
*/
import { useEffect, useRef } from 'react';
import { $getRoot, $isParagraphNode } from 'lexical';
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
import { $isVariableNode } from '../nodes/VariableNode';
const serialize = (root: ReturnType<typeof $getRoot>): string => {
const paragraphs: string[] = [];
root.getChildren().forEach(child => {
if ($isParagraphNode(child)) {
let content = '';
child.getChildren().forEach(node => {
content += $isVariableNode(node) ? node.getTextContent() : node.getTextContent();
});
paragraphs.push(content);
}
});
return paragraphs.join('\n');
};
const CharacterCountPlugin = ({
setCount,
onChange,
waitForInit = false,
}: {
setCount: (count: number) => void;
onChange?: (value: string) => void;
waitForInit?: boolean;
}) => {
const CharacterCountPlugin = ({ setCount, onChange }: { setCount: (count: number) => void; onChange?: (value: string) => void }) => {
const [editor] = useLexicalComposerContext();
// lastProgrammaticValue tracks what InitialValuePlugin wrote, so we can
// suppress onChange when the content hasn't actually changed from that value.
const lastProgrammaticValueRef = useRef<string | null>(null);
const isReadyRef = useRef(!waitForInit);
const isFirstUpdateRef = useRef(true);
const onChangeRef = useRef(onChange);
onChangeRef.current = onChange;
useEffect(() => {
return editor.registerUpdateListener(({ editorState, tags }) => {
if (tags.has('programmatic')) {
isReadyRef.current = true;
isFirstUpdateRef.current = false;
editorState.read(() => {
lastProgrammaticValueRef.current = serialize($getRoot());
});
return;
}
if (!isReadyRef.current) return;
if (tags.has('programmatic')) return;
editorState.read(() => {
const content = serialize($getRoot());
// Skip the first update if content is empty (editor initial render)
if (isFirstUpdateRef.current) {
isFirstUpdateRef.current = false;
if (content === '') return;
}
// Skip if content is identical to what was programmatically written
if (content === lastProgrammaticValueRef.current) return;
lastProgrammaticValueRef.current = null;
setCount(content.length);
onChange?.(content);
const root = $getRoot();
let serializedContent = '';
// Traverse all nodes and serialize properly
const paragraphs: string[] = [];
root.getChildren().forEach(child => {
if ($isParagraphNode(child)) {
let paragraphContent = '';
child.getChildren().forEach(node => {
if ($isVariableNode(node)) {
paragraphContent += node.getTextContent();
} else {
paragraphContent += node.getTextContent();
}
});
paragraphs.push(paragraphContent);
}
});
serializedContent = paragraphs.join('\n');
setCount(serializedContent.length);
onChangeRef.current?.(serializedContent);
});
});
}, [editor, setCount, onChange]);
}, [editor, setCount]);
return null;
};
}
export default CharacterCountPlugin;
export default CharacterCountPlugin

View File

@@ -2,7 +2,7 @@
* @Author: ZhaoYing
* @Date: 2026-02-03 15:17:39
* @Last Modified by: ZhaoYing
* @Last Modified time: 2026-04-03 18:39:07
* @Last Modified time: 2026-04-03 20:13:16
*/
import { useEffect, type FC } from 'react'
import { useTranslation } from 'react-i18next';
@@ -91,12 +91,12 @@ const GroupVariableList: FC<GroupVariableListProps> = ({
const firstVariable = allSuggestions.find(opt => `{{${opt.value}}}` === firstVariableValue);
if (firstVariable) {
filteredOptions = options.flatMap(opt => {
if (opt.dataType === 'file' && opt.children?.length) {
return [{ ...opt, children: opt.children.map(c => ({ ...c, disabled: c.dataType !== firstVariable.dataType })) }];
if (opt.children?.length) {
const filteredChildren = opt.children.filter(c => c.dataType === firstVariable.dataType);
if (filteredChildren.length) return [{ ...opt, disabled: opt.dataType !== firstVariable.dataType, children: filteredChildren }];
return [{ ...opt, children: [] }];
}
if (opt.dataType === firstVariable.dataType && !opt.children?.length) return [opt];
const filteredChildren = opt.children?.filter(c => c.dataType === firstVariable.dataType);
if (filteredChildren?.length) return [{ ...opt, children: filteredChildren }];
if (opt.dataType === firstVariable.dataType) return [opt];
return [];
});
}
@@ -182,12 +182,12 @@ const GroupVariableList: FC<GroupVariableListProps> = ({
if (firstVariable) {
return options.flatMap(vo => {
if (vo.dataType === 'file' && vo.children?.length) {
return [{ ...vo, children: vo.children.map(c => ({ ...c, disabled: c.dataType !== firstVariable.dataType })) }];
if (vo.children?.length) {
const filteredChildren = vo.children.filter(c => c.dataType === firstVariable.dataType);
if (filteredChildren.length) return [{ ...vo, disabled: vo.dataType !== firstVariable.dataType, children: filteredChildren }];
return [{ ...vo, children: [] }];
}
if (vo.dataType === firstVariable.dataType && (!vo.children || vo.children.length < 1)) return [vo];
const filteredChildren = vo.children?.filter(sub => sub.dataType === firstVariable.dataType);
if (filteredChildren?.length) return [{ ...vo, children: filteredChildren }];
if (vo.dataType === firstVariable.dataType) return [vo];
return [];
});
}

View File

@@ -67,11 +67,10 @@ const FilterConditions: FC<FilterConditionsProps> = ({
const form = Form.useFormInstance();
const handleKeyFieldChange = (index: number, newValue: string) => {
form.setFieldValue(['filter_by', index], {
form.setFieldValue([parentName, 'conditions', index], {
key: newValue,
comparison_operator: undefined,
value: undefined,
value_type: undefined,
});
};
@@ -85,8 +84,8 @@ const FilterConditions: FC<FilterConditionsProps> = ({
className="rb:relative"
>
{fields.map((field, index) => {
const filter_by = form.getFieldValue(['filter_by']) || [];
const currentCondition = filter_by[index] || {};
const conditions = form.getFieldValue([parentName, 'conditions']) || [];
const currentCondition = conditions[index] || {};
const currentOperator = currentCondition.comparison_operator;
const hideValueField = currentOperator === 'empty' || currentOperator === 'not_empty';
const keyFieldValue = currentCondition.key;
@@ -103,9 +102,7 @@ const FilterConditions: FC<FilterConditionsProps> = ({
>
<div className="rb:flex-1 rb:bg-[#F6F6F6] rb:rounded-lg">
{variableType === 'array[file]' &&
<Row className={clsx("rb:p-1!", {
'rb-border-b': !hideValueField
})}>
<Row className="rb:p-1! rb-border-b">
<Col span={24}>
<Form.Item name={[field.name, 'key']} noStyle>
<Select
@@ -121,7 +118,7 @@ const FilterConditions: FC<FilterConditionsProps> = ({
</Row>
}
<Row>
<Col flex="96px">
<Col flex={hideValueField ? '1' : "96px"}>
<Form.Item name={[field.name, 'comparison_operator']} noStyle>
<Select
options={operatorList.map(vo => ({

View File

@@ -20,12 +20,12 @@ const ListOperator: FC<ListOperatorProps> = ({ options }) => {
const { t } = useTranslation()
const form = Form.useFormInstance()
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
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
placeholder={t('common.pleaseSelect')}
options={options.filter(vo => vo.dataType.includes('array') && vo.dataType !== 'array[object]')}

View File

@@ -2,7 +2,7 @@
* @Author: ZhaoYing
* @Date: 2026-02-03 15:40:13
* @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 { createPortal } from 'react-dom'
@@ -49,36 +49,26 @@ const VariableSelect: FC<VariableSelectProps> = ({
const CHILD_PANEL_HEIGHT = 280; // max-h-60 (240) + header (~40)
// Calculate dropdown position when opening
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
// Calculate dropdown position (runs synchronously after DOM paint to avoid flicker)
useLayoutEffect(() => {
if (!open || !dropdownRef.current || !containerRef.current) return;
if (!open || !containerRef.current) return;
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 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 spaceBelow = viewportHeight - triggerRect.bottom - MARGIN;
const spaceBelow = window.innerHeight - triggerRect.bottom - MARGIN;
const spaceAbove = triggerRect.top - MARGIN;
let finalTop: number;
if (spaceBelow >= dropdownHeight || spaceBelow >= spaceAbove) {
finalTop = triggerRect.bottom + MARGIN;
} else {
finalTop = triggerRect.top - dropdownHeight - MARGIN;
if (finalTop < MARGIN) finalTop = MARGIN;
}
setDropdownPos(prev => ({ ...prev, top: finalTop, left }));
const top = (spaceBelow >= dropdownHeight || spaceBelow >= spaceAbove)
? triggerRect.bottom + MARGIN
: Math.max(MARGIN, triggerRect.top - dropdownHeight - MARGIN);
setDropdownPos({ top, left, width });
}, [open, search, Array.isArray(value) ? value.length : 0]);
const filteredOptions = filterBooleanType
@@ -182,9 +172,10 @@ const VariableSelect: FC<VariableSelectProps> = ({
const el = itemRefs.current.get(key);
if (el) {
const rect = el.getBoundingClientRect();
const absoluteBottom = rect.top + CHILD_PANEL_HEIGHT;
const overflow = absoluteBottom - (window.innerHeight - 10);
const top = overflow > 0 ? rect.top - overflow : rect.top;
const spaceBelow = window.innerHeight - rect.top - 10;
const top = spaceBelow >= CHILD_PANEL_HEIGHT
? rect.top
: Math.max(10, window.innerHeight - CHILD_PANEL_HEIGHT - 10);
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' && 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',
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]'),
className
)}

View File

@@ -2,7 +2,7 @@
* @Author: ZhaoYing
* @Date: 2026-02-03 15:06:18
* @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 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")]',
config: {
variable: {
input_list: {
type: 'variableList',
},
filter_by: {