新版本流程编辑器
This commit is contained in:
@@ -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';
|
||||
Reference in New Issue
Block a user