Commit 4da50eb8 by DigHuang Committed by GitHub

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

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