Commit 4da50eb8 by DigHuang Committed by GitHub

fix(workflow): materialize editable node text (#7308)

parent 6527bebf
...@@ -45,8 +45,7 @@ const SearchButton = (props: ButtonProps) => { ...@@ -45,8 +45,7 @@ const SearchButton = (props: ButtonProps) => {
} }
const searchResult = nodes.filter((node) => { const searchResult = nodes.filter((node) => {
const nodeName = t(node.data.name as any); return node.data.name.toLowerCase().includes(keyword.toLowerCase());
return nodeName.toLowerCase().includes(keyword.toLowerCase());
}); });
if (searchResult.length === 0) { if (searchResult.length === 0) {
......
...@@ -362,12 +362,6 @@ const NodeTemplateList = ({ ...@@ -362,12 +362,6 @@ const NodeTemplateList = ({
const newNode = nodeTemplate2FlowNode({ const newNode = nodeTemplate2FlowNode({
template: { template: {
...templateNode, ...templateNode,
name: computedNewNodeName({
templateName: t(templateNode.name as any),
flowNodeType: templateNode.flowNodeType,
pluginId: templateNode.pluginId
}),
intro: t(templateNode.intro as any),
inputs: templateNode.inputs inputs: templateNode.inputs
.filter((input) => input.deprecated !== true) .filter((input) => input.deprecated !== true)
.map((input) => ({ .map((input) => ({
...@@ -400,7 +394,13 @@ const NodeTemplateList = ({ ...@@ -400,7 +394,13 @@ const NodeTemplateList = ({
position, position,
selected: true, selected: true,
parentNodeId: effectiveParentNodeId, parentNodeId: effectiveParentNodeId,
t t,
formatName: (templateName) =>
computedNewNodeName({
templateName,
flowNodeType: templateNode.flowNodeType,
pluginId: templateNode.pluginId
})
}); });
const newNodes = [newNode]; const newNodes = [newNode];
......
import React, { useState, useCallback } from 'react'; import React, { useState, useCallback } from 'react';
import { Box, Input, Textarea, type BoxProps } from '@chakra-ui/react'; import { Box, Input, Textarea, type BoxProps } from '@chakra-ui/react';
import { useTranslation } from 'next-i18next';
export type InlineEditProps = BoxProps & { export type InlineEditProps = BoxProps & {
value: string; value: string;
...@@ -22,11 +21,10 @@ export const InlineEdit = React.memo(function InlineEdit({ ...@@ -22,11 +21,10 @@ export const InlineEdit = React.memo(function InlineEdit({
maxLength, maxLength,
placeholder, placeholder,
innerH, innerH,
noOfLines = 1, noOfLines,
renderDisplay, renderDisplay,
...rest ...rest
}: InlineEditProps) { }: InlineEditProps) {
const { t } = useTranslation();
const [isEditing, setIsEditing] = useState(false); const [isEditing, setIsEditing] = useState(false);
const [prevValue, setPrevValue] = useState(value); const [prevValue, setPrevValue] = useState(value);
const [tempValue, setTempValue] = useState(value); const [tempValue, setTempValue] = useState(value);
...@@ -96,7 +94,7 @@ export const InlineEdit = React.memo(function InlineEdit({ ...@@ -96,7 +94,7 @@ export const InlineEdit = React.memo(function InlineEdit({
color={'inherit'} color={'inherit'}
variant={'unstyled'} variant={'unstyled'}
value={tempValue} value={tempValue}
placeholder={placeholder ? t(placeholder as any) : ''} placeholder={placeholder}
maxLength={maxLength} maxLength={maxLength}
onChange={(e) => setTempValue(e.target.value)} onChange={(e) => setTempValue(e.target.value)}
autoFocus autoFocus
...@@ -129,7 +127,7 @@ export const InlineEdit = React.memo(function InlineEdit({ ...@@ -129,7 +127,7 @@ export const InlineEdit = React.memo(function InlineEdit({
</Box> </Box>
<Textarea <Textarea
value={tempValue} value={tempValue}
placeholder={placeholder ? t(placeholder as any) : ''} placeholder={placeholder}
maxLength={maxLength} maxLength={maxLength}
onChange={(e) => setTempValue(e.target.value)} onChange={(e) => setTempValue(e.target.value)}
autoFocus autoFocus
...@@ -162,11 +160,12 @@ export const InlineEdit = React.memo(function InlineEdit({ ...@@ -162,11 +160,12 @@ export const InlineEdit = React.memo(function InlineEdit({
<Box <Box
cursor={'pointer'} cursor={'pointer'}
onClick={() => setIsEditing(true)} onClick={() => setIsEditing(true)}
title={t(value as any)} title={value}
w={'100%'} w={'100%'}
minW={0} minW={0}
maxW={'100%'} maxW={'100%'}
noOfLines={noOfLines} noOfLines={noOfLines ?? (type === 'input' ? 1 : undefined)}
whiteSpace={type === 'textarea' ? 'pre-wrap' : undefined}
wordBreak={'break-all'} wordBreak={'break-all'}
borderRadius={'sm'} borderRadius={'sm'}
border={'1px solid transparent'} border={'1px solid transparent'}
...@@ -186,9 +185,7 @@ export const InlineEdit = React.memo(function InlineEdit({ ...@@ -186,9 +185,7 @@ export const InlineEdit = React.memo(function InlineEdit({
color={type === 'input' ? 'myGray.900' : 'myGray.500'} color={type === 'input' ? 'myGray.900' : 'myGray.500'}
{...rest} {...rest}
> >
{renderDisplay {renderDisplay ? renderDisplay(value) : value || placeholder || ''}
? renderDisplay(value)
: t(value as any) || (placeholder ? t(placeholder as any) : '')}
</Box> </Box>
); );
}); });
......
...@@ -209,11 +209,11 @@ const NodeCard = (props: Props) => { ...@@ -209,11 +209,11 @@ const NodeCard = (props: Props) => {
whiteSpace={'nowrap'} whiteSpace={'nowrap'}
maxW={'80%'} maxW={'80%'}
> >
{t(name as any)} {name}
</Box> </Box>
</Flex> </Flex>
); );
}, [isFolded, avatar, avatarLinear, name, handleDoubleClick, t]); }, [isFolded, avatar, avatarLinear, name, handleDoubleClick]);
const { outlineColor, outlineWidth } = useMemo(() => { const { outlineColor, outlineWidth } = useMemo(() => {
// error mode // error mode
...@@ -563,14 +563,9 @@ const NodeTitleSection = React.memo<{ ...@@ -563,14 +563,9 @@ const NodeTitleSection = React.memo<{
const renderDisplay = useCallback( const renderDisplay = useCallback(
(val: string) => ( (val: string) => (
<HighlightText <HighlightText rawText={val} matchText={searchedText ?? ''} mode={'bg'} color={'#ffe82d'} />
rawText={t(val as any)}
matchText={searchedText ?? ''}
mode={'bg'}
color={'#ffe82d'}
/>
), ),
[searchedText, t] [searchedText]
); );
return ( return (
...@@ -646,7 +641,6 @@ const NodeIntro = React.memo(function NodeIntro({ ...@@ -646,7 +641,6 @@ const NodeIntro = React.memo(function NodeIntro({
minH={'20px'} minH={'20px'}
py={'3px'} py={'3px'}
px={'6px'} px={'6px'}
noOfLines={1}
/> />
</Box> </Box>
); );
...@@ -1205,7 +1199,6 @@ const PresentationModeOverlay = React.memo(function PresentationModeOverlay({ ...@@ -1205,7 +1199,6 @@ const PresentationModeOverlay = React.memo(function PresentationModeOverlay({
isLoopNode: boolean; isLoopNode: boolean;
onDoubleClick: () => void; onDoubleClick: () => void;
}) { }) {
const { t } = useTranslation();
const [presentationHeight, setPresentationHeight] = useState<number>(0); const [presentationHeight, setPresentationHeight] = useState<number>(0);
const presentationOverlayRef = useCallback((node: HTMLDivElement | null) => { const presentationOverlayRef = useCallback((node: HTMLDivElement | null) => {
...@@ -1271,7 +1264,7 @@ const PresentationModeOverlay = React.memo(function PresentationModeOverlay({ ...@@ -1271,7 +1264,7 @@ const PresentationModeOverlay = React.memo(function PresentationModeOverlay({
whiteSpace={'nowrap'} whiteSpace={'nowrap'}
maxW={'80%'} maxW={'80%'}
> >
{t(name as any)} {name}
</Box> </Box>
)} )}
{intro && presentationHeight > 320 && ( {intro && presentationHeight > 320 && (
...@@ -1284,7 +1277,7 @@ const PresentationModeOverlay = React.memo(function PresentationModeOverlay({ ...@@ -1284,7 +1277,7 @@ const PresentationModeOverlay = React.memo(function PresentationModeOverlay({
whiteSpace={'nowrap'} whiteSpace={'nowrap'}
maxW={'80%'} maxW={'80%'}
> >
{t(intro as any)} {intro}
</Box> </Box>
)} )}
</Flex> </Flex>
......
...@@ -93,7 +93,7 @@ export const useReference = ({ ...@@ -93,7 +93,7 @@ export const useReference = ({
label: ( label: (
<Flex alignItems={'center'}> <Flex alignItems={'center'}>
<Avatar src={node.avatar} w={isArray ? '1rem' : '1.05rem'} borderRadius={'xs'} /> <Avatar src={node.avatar} w={isArray ? '1rem' : '1.05rem'} borderRadius={'xs'} />
<Box ml={1}>{t(node.name as any)}</Box> <Box ml={1}>{node.name}</Box>
</Flex> </Flex>
), ),
value: node.nodeId, value: node.nodeId,
......
...@@ -28,8 +28,8 @@ export const getEmptyAppsTemplate = (t: any) => { ...@@ -28,8 +28,8 @@ export const getEmptyAppsTemplate = (t: any) => {
nodes: [ nodes: [
{ {
nodeId: 'userGuide', nodeId: 'userGuide',
name: i18nT('common:core.module.template.system_config'), name: t(i18nT('common:core.module.template.system_config')),
intro: i18nT('common:core.module.template.config_params'), intro: t(i18nT('common:core.module.template.config_params')),
avatar: 'core/workflow/template/systemConfig', avatar: 'core/workflow/template/systemConfig',
flowNodeType: FlowNodeTypeEnum.systemConfig, flowNodeType: FlowNodeTypeEnum.systemConfig,
position: { position: {
...@@ -93,7 +93,7 @@ export const getEmptyAppsTemplate = (t: any) => { ...@@ -93,7 +93,7 @@ export const getEmptyAppsTemplate = (t: any) => {
}, },
{ {
nodeId: '448745', nodeId: '448745',
name: i18nT('common:core.module.template.work_start'), name: t(i18nT('common:core.module.template.work_start')),
intro: '', intro: '',
avatar: 'core/workflow/template/workflowStart', avatar: 'core/workflow/template/workflowStart',
flowNodeType: FlowNodeTypeEnum.workflowStart, flowNodeType: FlowNodeTypeEnum.workflowStart,
...@@ -124,8 +124,8 @@ export const getEmptyAppsTemplate = (t: any) => { ...@@ -124,8 +124,8 @@ export const getEmptyAppsTemplate = (t: any) => {
}, },
{ {
nodeId: 'loOvhld2ZTKa', nodeId: 'loOvhld2ZTKa',
name: i18nT('common:core.module.template.ai_chat'), name: t(i18nT('common:core.module.template.ai_chat')),
intro: i18nT('common:core.module.template.ai_chat_intro'), intro: t(i18nT('common:core.module.template.ai_chat_intro')),
avatar: 'core/workflow/template/aiChat', avatar: 'core/workflow/template/aiChat',
flowNodeType: FlowNodeTypeEnum.chatNode, flowNodeType: FlowNodeTypeEnum.chatNode,
showStatus: true, showStatus: true,
...@@ -264,8 +264,8 @@ export const getEmptyAppsTemplate = (t: any) => { ...@@ -264,8 +264,8 @@ export const getEmptyAppsTemplate = (t: any) => {
nodes: [ nodes: [
{ {
nodeId: 'userGuide', nodeId: 'userGuide',
name: i18nT('common:core.module.template.system_config'), name: t(i18nT('common:core.module.template.system_config')),
intro: i18nT('common:core.module.template.system_config_info'), intro: t(i18nT('common:core.module.template.system_config_info')),
avatar: 'core/workflow/template/systemConfig', avatar: 'core/workflow/template/systemConfig',
flowNodeType: FlowNodeTypeEnum.systemConfig, flowNodeType: FlowNodeTypeEnum.systemConfig,
position: { position: {
...@@ -329,7 +329,7 @@ export const getEmptyAppsTemplate = (t: any) => { ...@@ -329,7 +329,7 @@ export const getEmptyAppsTemplate = (t: any) => {
}, },
{ {
nodeId: '448745', nodeId: '448745',
name: i18nT('common:core.module.template.work_start'), name: t(i18nT('common:core.module.template.work_start')),
intro: '', intro: '',
avatar: 'core/workflow/template/workflowStart', avatar: 'core/workflow/template/workflowStart',
flowNodeType: FlowNodeTypeEnum.workflowStart, flowNodeType: FlowNodeTypeEnum.workflowStart,
...@@ -368,7 +368,7 @@ export const getEmptyAppsTemplate = (t: any) => { ...@@ -368,7 +368,7 @@ export const getEmptyAppsTemplate = (t: any) => {
nodes: [ nodes: [
{ {
nodeId: 'pluginInput', nodeId: 'pluginInput',
name: i18nT('workflow:template.plugin_start'), name: t(i18nT('workflow:template.plugin_start')),
avatar: 'core/workflow/template/workflowStart', avatar: 'core/workflow/template/workflowStart',
flowNodeType: FlowNodeTypeEnum.pluginInput, flowNodeType: FlowNodeTypeEnum.pluginInput,
showStatus: false, showStatus: false,
...@@ -382,7 +382,7 @@ export const getEmptyAppsTemplate = (t: any) => { ...@@ -382,7 +382,7 @@ export const getEmptyAppsTemplate = (t: any) => {
}, },
{ {
nodeId: 'pluginOutput', nodeId: 'pluginOutput',
name: i18nT('common:core.module.template.self_output'), name: t(i18nT('common:core.module.template.self_output')),
avatar: '/imgs/workflow/output.png', avatar: '/imgs/workflow/output.png',
flowNodeType: FlowNodeTypeEnum.pluginOutput, flowNodeType: FlowNodeTypeEnum.pluginOutput,
showStatus: false, showStatus: false,
...@@ -396,7 +396,7 @@ export const getEmptyAppsTemplate = (t: any) => { ...@@ -396,7 +396,7 @@ export const getEmptyAppsTemplate = (t: any) => {
}, },
{ {
nodeId: 'pluginConfig', nodeId: 'pluginConfig',
name: i18nT('common:core.module.template.system_config'), name: t(i18nT('common:core.module.template.system_config')),
intro: '', intro: '',
avatar: 'core/workflow/template/systemConfig', avatar: 'core/workflow/template/systemConfig',
flowNodeType: FlowNodeTypeEnum.pluginConfig, flowNodeType: FlowNodeTypeEnum.pluginConfig,
......
...@@ -84,13 +84,18 @@ export const adaptStoreNodeInputs = (storeNode: StoreNodeItemType): FlowNodeInpu ...@@ -84,13 +84,18 @@ export const adaptStoreNodeInputs = (storeNode: StoreNodeItemType): FlowNodeInpu
}); });
}; };
/**
* 将节点模板转换为画布节点,并按创建时语言初始化可编辑文本。
* `formatName` 在翻译完成后执行,用于基于实例名称追加重名序号。
*/
export const nodeTemplate2FlowNode = ({ export const nodeTemplate2FlowNode = ({
template, template,
position, position,
selected, selected,
parentNodeId, parentNodeId,
zIndex, zIndex,
t t,
formatName
}: { }: {
template: FlowNodeTemplateType; template: FlowNodeTemplateType;
position: XYPosition; position: XYPosition;
...@@ -98,11 +103,15 @@ export const nodeTemplate2FlowNode = ({ ...@@ -98,11 +103,15 @@ export const nodeTemplate2FlowNode = ({
parentNodeId?: string; parentNodeId?: string;
zIndex?: number; zIndex?: number;
t: TFunction; t: TFunction;
formatName?: (name: string) => string;
}): Node<FlowNodeItemType> => { }): Node<FlowNodeItemType> => {
const name = t(template.name as any);
// replace item data // replace item data
const moduleItem: FlowNodeItemType = { const moduleItem: FlowNodeItemType = {
...template, ...template,
name: t(template.name as any), name: formatName?.(name) ?? name,
intro: template.intro ? t(template.intro as any) : template.intro,
nodeId: getNanoid(), nodeId: getNanoid(),
parentNodeId parentNodeId
}; };
...@@ -126,6 +135,11 @@ export const nodeTemplate2FlowNode = ({ ...@@ -126,6 +135,11 @@ export const nodeTemplate2FlowNode = ({
zIndex zIndex
}; };
}; };
/**
* 将持久化节点恢复为画布节点,并在加载时实体化历史 i18n 文本。
* 名称或描述命中翻译 key 时使用当前语言文本,后续保存会写回实体文本。
*/
export const storeNode2FlowNode = ({ export const storeNode2FlowNode = ({
item: storeNode, item: storeNode,
selected = false, selected = false,
...@@ -160,6 +174,8 @@ export const storeNode2FlowNode = ({ ...@@ -160,6 +174,8 @@ export const storeNode2FlowNode = ({
parentNodeId, parentNodeId,
...template, ...template,
...storeNode, ...storeNode,
name: t(storeNode.name as any),
intro: storeNode.intro ? t(storeNode.intro as any) : storeNode.intro,
avatar: template.avatar ?? storeNode.avatar, avatar: template.avatar ?? storeNode.avatar,
version: template.version || storeNode.version, version: template.version || storeNode.version,
catchError: storeNode.catchError ?? template.catchError, catchError: storeNode.catchError ?? template.catchError,
......
import { describe, expect, it } from 'vitest';
import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
import { getEmptyAppsTemplate } from '@/web/core/app/templates';
describe('getEmptyAppsTemplate', () => {
it('创建空应用时按当前语言初始化节点标题和描述', () => {
const templates = getEmptyAppsTemplate(((key: string) => `translated:${key}`) as any);
const nodes = [
...templates[AppTypeEnum.simple].nodes,
...templates[AppTypeEnum.workflow].nodes,
...templates[AppTypeEnum.workflowTool].nodes
];
expect(nodes.every((node) => node.name.startsWith('translated:'))).toBe(true);
expect(nodes.filter((node) => node.intro).map((node) => node.intro)).toEqual([
'translated:common:core.module.template.config_params',
'translated:common:core.module.template.ai_chat_intro',
'translated:common:core.module.template.system_config_info'
]);
});
});
import { describe, it, expect } from 'vitest'; import { describe, it, expect, vi } from 'vitest';
import type { import type {
FlowNodeItemType, FlowNodeItemType,
FlowNodeTemplateType, FlowNodeTemplateType,
...@@ -27,22 +27,31 @@ import type { FlowNodeOutputItemType } from '@fastgpt/global/core/workflow/type/ ...@@ -27,22 +27,31 @@ import type { FlowNodeOutputItemType } from '@fastgpt/global/core/workflow/type/
import { NodeOutputKeyEnum, VARIABLE_NODE_ID } from '@fastgpt/global/core/workflow/constants'; import { NodeOutputKeyEnum, VARIABLE_NODE_ID } from '@fastgpt/global/core/workflow/constants';
describe('nodeTemplate2FlowNode', () => { describe('nodeTemplate2FlowNode', () => {
it('should convert template to flow node', () => { it('should initialize template text once before formatting the instance name', () => {
const template: FlowNodeTemplateType = { const template: FlowNodeTemplateType = {
id: 'template1', id: 'template1',
templateType: 'formInput', templateType: 'formInput',
name: 'Test Node', name: 'workflow:template_name',
intro: 'workflow:template_intro',
flowNodeType: FlowNodeTypeEnum.formInput, flowNodeType: FlowNodeTypeEnum.formInput,
inputs: [], inputs: [],
outputs: [] outputs: []
}; };
const t = vi.fn(
(key: string) =>
({
'workflow:template_name': 'Template Name',
'workflow:template_intro': 'Template Intro'
})[key] ?? key
);
const result = nodeTemplate2FlowNode({ const result = nodeTemplate2FlowNode({
template, template,
position: { x: 100, y: 100 }, position: { x: 100, y: 100 },
selected: true, selected: true,
parentNodeId: 'parent1', parentNodeId: 'parent1',
t: ((key: any) => key) as any t: t as any,
formatName: (name) => `${name} 2`
}); });
expect(result).toMatchObject({ expect(result).toMatchObject({
...@@ -50,38 +59,52 @@ describe('nodeTemplate2FlowNode', () => { ...@@ -50,38 +59,52 @@ describe('nodeTemplate2FlowNode', () => {
position: { x: 100, y: 100 }, position: { x: 100, y: 100 },
selected: true, selected: true,
data: { data: {
name: 'Test Node', name: 'Template Name 2',
intro: 'Template Intro',
flowNodeType: FlowNodeTypeEnum.formInput, flowNodeType: FlowNodeTypeEnum.formInput,
parentNodeId: 'parent1' parentNodeId: 'parent1'
} }
}); });
expect(result.id).toBeDefined(); expect(result.id).toBeDefined();
expect(t.mock.calls.map(([key]) => key)).toEqual([
'workflow:template_name',
'workflow:template_intro'
]);
}); });
}); });
describe('storeNode2FlowNode', () => { describe('storeNode2FlowNode', () => {
it('should convert store node to flow node', () => { it('should materialize stored editable text when it matches an i18n key', () => {
const storeNode: StoreNodeItemType = { const storeNode: StoreNodeItemType = {
nodeId: 'node1', nodeId: 'node1',
flowNodeType: FlowNodeTypeEnum.formInput, flowNodeType: FlowNodeTypeEnum.formInput,
position: { x: 100, y: 100 }, position: { x: 100, y: 100 },
inputs: [], inputs: [],
outputs: [], outputs: [],
name: 'Test Node', name: 'workflow:stored_name',
intro: 'workflow:stored_intro',
version: '1.0' version: '1.0'
}; };
const result = storeNode2FlowNode({ const result = storeNode2FlowNode({
item: storeNode, item: storeNode,
selected: true, selected: true,
t: ((key: any) => key) as any t: ((key: string) =>
({
'workflow:stored_name': 'Stored Name',
'workflow:stored_intro': 'Stored Intro'
})[key] ?? key) as any
}); });
expect(result).toMatchObject({ expect(result).toMatchObject({
id: 'node1', id: 'node1',
type: FlowNodeTypeEnum.formInput, type: FlowNodeTypeEnum.formInput,
position: { x: 100, y: 100 }, position: { x: 100, y: 100 },
selected: true selected: true,
data: {
name: 'Stored Name',
intro: 'Stored Intro'
}
}); });
}); });
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment