新版本流程编辑器

This commit is contained in:
mlogclub
2026-07-27 19:04:15 +08:00
parent d33960961a
commit e072141953
281 changed files with 25145 additions and 3405 deletions
@@ -0,0 +1,40 @@
/**
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*/
import styled from 'styled-components';
import { FieldError, FieldState, FieldWarning } from '@flowgram.ai/free-layout-editor';
interface StatePanelProps {
errors?: FieldState['errors'];
warnings?: FieldState['warnings'];
invalid?: boolean;
}
const Error = styled.span`
font-size: 12px;
color: red;
`;
const Warning = styled.span`
font-size: 12px;
color: orange;
`;
export const Feedback = ({ errors, warnings, invalid }: StatePanelProps) => {
const renderFeedbacks = (fs: FieldError[] | FieldWarning[] | undefined) => {
if (!fs) return null;
return fs.map((f) => <span key={f.name}>{f.message}</span>);
};
return (
<div>
<div>
<Error>{renderFeedbacks(errors)}</Error>
</div>
<div>
<Warning>{renderFeedbacks(warnings)}</Warning>
</div>
</div>
);
};
@@ -0,0 +1,29 @@
/**
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*/
import React from 'react';
import { FlowNodeRegistry } from '@flowgram.ai/free-layout-editor';
import { useIsSidebar, useNodeRenderContext } from '../../hooks';
import { FormTitleDescription, FormWrapper } from './styles';
/**
* @param props
* @constructor
*/
export function FormContent(props: { children?: React.ReactNode }) {
const { node, expanded } = useNodeRenderContext();
const isSidebar = useIsSidebar();
const registry = node.getNodeRegistry<FlowNodeRegistry>();
return (
<FormWrapper>
<>
{isSidebar && <FormTitleDescription>{registry.info?.description}</FormTitleDescription>}
{(expanded || isSidebar) && props.children}
</>
</FormWrapper>
);
}
@@ -0,0 +1,26 @@
/**
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*/
import styled from 'styled-components';
export const FormWrapper = styled.div`
box-sizing: border-box;
width: 100%;
display: flex;
flex-direction: column;
gap: 6px;
background-color: rgb(251, 251, 251);
border-radius: 0 0 8px 8px;
padding: 0 12px 12px;
`;
export const FormTitleDescription = styled.div`
color: var(--semi-color-text-2);
font-size: 12px;
line-height: 20px;
padding: 0px 4px;
word-break: break-all;
white-space: break-spaces;
`;
@@ -0,0 +1,73 @@
/**
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*/
import { useState, useEffect } from 'react';
import { useClientContext, CommandService } from '@flowgram.ai/free-layout-editor';
import { Button } from '@douyinfe/semi-ui';
import { IconClose, IconSmallTriangleDown, IconSmallTriangleLeft } from '@douyinfe/semi-icons';
import { toggleLoopExpanded } from '../../utils';
import { FlowCommandId } from '../../shortcuts';
import { useNodeFormPanel } from '../../plugins/panel-manager-plugin/hooks';
import { useIsSidebar, useNodeRenderContext } from '../../hooks';
import { NodeMenu } from '../../components/node-menu';
import { getIcon } from './utils';
import { TitleInput } from './title-input';
import { Header, Operators } from './styles';
export function FormHeader() {
const { node, expanded, toggleExpand, readonly } = useNodeRenderContext();
const [titleEdit, updateTitleEdit] = useState<boolean>(false);
const ctx = useClientContext();
const isSidebar = useIsSidebar();
const handleExpand = (e: React.MouseEvent) => {
toggleExpand();
e.stopPropagation(); // Disable clicking prevents the sidebar from opening
};
const { close: closePanel } = useNodeFormPanel();
const handleDelete = () => {
ctx.get<CommandService>(CommandService).executeCommand(FlowCommandId.DELETE, [node]);
};
const handleClose = () => {
closePanel();
};
useEffect(() => {
// 折叠 loop 子节点
if (node.flowNodeType === 'loop') {
toggleLoopExpanded(node, expanded);
}
}, [expanded]);
return (
<Header>
{getIcon(node)}
<TitleInput readonly={readonly} updateTitleEdit={updateTitleEdit} titleEdit={titleEdit} />
{node.renderData.expandable && !isSidebar && (
<Button
type="primary"
icon={expanded ? <IconSmallTriangleDown /> : <IconSmallTriangleLeft />}
size="small"
theme="borderless"
onClick={handleExpand}
/>
)}
{readonly ? undefined : (
<Operators>
<NodeMenu node={node} deleteNode={handleDelete} updateTitleEdit={updateTitleEdit} />
</Operators>
)}
{isSidebar && (
<Button
type="primary"
icon={<IconClose />}
size="small"
theme="borderless"
onClick={handleClose}
/>
)}
</Header>
);
}
@@ -0,0 +1,41 @@
/**
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*/
import styled from 'styled-components';
export const Header = styled.div`
box-sizing: border-box;
display: flex;
justify-content: flex-start;
align-items: center;
width: 100%;
column-gap: 8px;
border-radius: 8px 8px 0 0;
cursor: move;
background: linear-gradient(#f2f2ff 0%, rgb(251, 251, 251) 100%);
overflow: hidden;
padding: 8px;
`;
export const Title = styled.div`
font-size: 20px;
flex: 1;
width: 0;
`;
export const Icon = styled.img`
width: 24px;
height: 24px;
scale: 0.8;
border-radius: 4px;
`;
export const Operators = styled.div`
display: flex;
align-items: center;
column-gap: 4px;
`;
@@ -0,0 +1,50 @@
/**
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*/
import { useRef, useEffect } from 'react';
import { Field, FieldRenderProps } from '@flowgram.ai/free-layout-editor';
import { Typography, Input } from '@douyinfe/semi-ui';
import { Title } from './styles';
import { Feedback } from '../feedback';
const { Text } = Typography;
export function TitleInput(props: {
readonly: boolean;
titleEdit: boolean;
updateTitleEdit: (setEdit: boolean) => void;
}): JSX.Element {
const { readonly, titleEdit, updateTitleEdit } = props;
const ref = useRef<any>();
const titleEditing = titleEdit && !readonly;
useEffect(() => {
if (titleEditing) {
ref.current?.focus();
}
}, [titleEditing]);
return (
<Title>
<Field name="title">
{({ field: { value, onChange }, fieldState }: FieldRenderProps<string>) => (
<div style={{ height: 24 }}>
{titleEditing ? (
<Input
value={value}
onChange={onChange}
ref={ref}
onBlur={() => updateTitleEdit(false)}
/>
) : (
<Text ellipsis={{ showTooltip: true }}>{value}</Text>
)}
<Feedback errors={fieldState?.errors} />
</div>
)}
</Field>
</Title>
);
}
@@ -0,0 +1,15 @@
/**
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*/
import { type FlowNodeEntity } from '@flowgram.ai/free-layout-editor';
import { FlowNodeRegistry } from '../../typings';
import { Icon } from './styles';
export const getIcon = (node: FlowNodeEntity) => {
const icon = node.getNodeRegistry<FlowNodeRegistry>().info?.icon;
if (!icon) return null;
return <Icon src={icon} />;
};
@@ -0,0 +1,68 @@
/**
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*/
import { Field } from '@flowgram.ai/free-layout-editor';
import { DynamicValueInput, PromptEditorWithVariables } from '@flowgram.ai/form-materials';
import { FormItem } from '../form-item';
import { Feedback } from '../feedback';
import { JsonSchema } from '../../typings';
import { useNodeRenderContext } from '../../hooks';
export function FormInputs() {
const { readonly } = useNodeRenderContext();
return (
<Field<JsonSchema> name="inputs">
{({ field: inputsField }) => {
const required = inputsField.value?.required || [];
const properties = inputsField.value?.properties;
if (!properties) {
return <></>;
}
const content = Object.keys(properties).map((key) => {
const property = properties[key];
const formComponent = property.extra?.formComponent;
const vertical = ['prompt-editor'].includes(formComponent || '');
return (
<Field key={key} name={`inputsValues.${key}`} defaultValue={property.default}>
{({ field, fieldState }) => (
<FormItem
name={key}
vertical={vertical}
type={property.type as string}
required={required.includes(key)}
>
{formComponent === 'prompt-editor' && (
<PromptEditorWithVariables
value={field.value}
onChange={field.onChange}
readonly={readonly}
hasError={Object.keys(fieldState?.errors || {}).length > 0}
/>
)}
{!formComponent && (
<DynamicValueInput
value={field.value}
onChange={field.onChange}
readonly={readonly}
hasError={Object.keys(fieldState?.errors || {}).length > 0}
schema={property}
/>
)}
<Feedback errors={fieldState?.errors} warnings={fieldState?.warnings} />
</FormItem>
)}
</Field>
);
});
return <>{content}</>;
}}
</Field>
);
}
@@ -0,0 +1,8 @@
/**
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*/
// import styled from 'styled-components';
// TODO
@@ -0,0 +1,14 @@
/**
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*/
.form-item-type-tag {
color: inherit;
padding: 0 2px;
height: 18px;
width: 18px;
vertical-align: middle;
flex-shrink: 0;
flex-grow: 0;
}
@@ -0,0 +1,95 @@
/**
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*/
import React, { useCallback } from 'react';
import { DisplaySchemaTag } from '@flowgram.ai/form-materials';
import { Typography, Tooltip } from '@douyinfe/semi-ui';
import './index.css';
const { Text } = Typography;
interface FormItemProps {
children: React.ReactNode;
name: string;
type?: string;
required?: boolean;
description?: string;
labelWidth?: number;
labelStyle?: React.CSSProperties;
vertical?: boolean;
style?: React.CSSProperties;
}
export function FormItem({
children,
name,
required,
description,
type,
labelWidth,
labelStyle,
vertical,
style,
}: FormItemProps): JSX.Element {
const renderTitle = useCallback(
(showTooltip?: boolean) => (
<div style={{ width: '0', display: 'flex', flex: '1' }}>
<Text style={{ width: '100%' }} ellipsis={{ showTooltip: !!showTooltip }}>
{name}
{required && <span style={{ color: '#f93920', paddingLeft: '2px' }}>*</span>}
</Text>
</div>
),
[]
);
return (
<div
style={{
fontSize: 12,
marginBottom: 6,
width: '100%',
position: 'relative',
display: 'flex',
gap: 8,
...(vertical
? { flexDirection: 'column' }
: {
justifyContent: 'center',
alignItems: 'center',
}),
...style,
}}
>
<div
style={{
justifyContent: 'center',
alignItems: 'center',
color: 'var(--semi-color-text-0)',
width: labelWidth || 118,
minWidth: labelWidth || 118,
maxWidth: labelWidth || 118,
position: 'relative',
display: 'flex',
columnGap: 4,
flexShrink: 0,
...labelStyle,
}}
>
{type && <DisplaySchemaTag value={{ type }} />}
{description ? <Tooltip content={description}>{renderTitle()}</Tooltip> : renderTitle(true)}
</div>
<div
style={{
flexGrow: 1,
minWidth: 0,
}}
>
{children}
</div>
</div>
);
}
@@ -0,0 +1,10 @@
/**
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*/
export * from './feedback';
export * from './form-content';
export * from './form-inputs';
export * from './form-header';
export * from './form-item';