feat: manually mirror opencoze's code from bytedance
Change-Id: I09a73aadda978ad9511264a756b2ce51f5761adf
This commit is contained in:
@@ -0,0 +1,35 @@
|
||||
.popup_container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
.nano {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
align-self: stretch;
|
||||
pointer-events: none;
|
||||
:global {
|
||||
.semi-portal-inner {
|
||||
left: 50% !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.tooltip {
|
||||
&.top-level {
|
||||
word-break: break-word;
|
||||
border-radius: 6px;
|
||||
background: var(--light-color-grey-grey-7, #41414C);
|
||||
max-width: 400px;
|
||||
color: var(--light-usage-bg-color-bg-0, #FFF);
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 22px;
|
||||
}
|
||||
& > svg > path {
|
||||
fill: var(--light-color-grey-grey-7, #41414C);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { type PropsWithChildren, useRef } from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { type TooltipProps } from '@coze-arch/bot-semi/Tooltip';
|
||||
import { Tooltip } from '@coze-arch/bot-semi';
|
||||
|
||||
import styles from './index.module.less';
|
||||
|
||||
type AutoSizeTooltipProps = PropsWithChildren<
|
||||
{
|
||||
className?: string;
|
||||
style?: React.CSSProperties;
|
||||
containerClassName?: string;
|
||||
containerStyle?: React.CSSProperties;
|
||||
tooltipClassName?: string;
|
||||
tooltipStyle?: React.CSSProperties;
|
||||
} & Omit<TooltipProps, 'className' | 'style'>
|
||||
>;
|
||||
|
||||
export default function AutoSizeTooltip({
|
||||
children,
|
||||
className,
|
||||
style,
|
||||
tooltipClassName,
|
||||
tooltipStyle,
|
||||
containerClassName,
|
||||
containerStyle,
|
||||
...rest
|
||||
}: AutoSizeTooltipProps) {
|
||||
const nanoRef = useRef<HTMLDivElement | null>(null);
|
||||
const renderContent = () => (
|
||||
<>
|
||||
<div
|
||||
ref={nanoRef}
|
||||
className={classNames(styles.nano, containerClassName)}
|
||||
style={containerStyle}
|
||||
/>
|
||||
<Tooltip
|
||||
{...rest}
|
||||
className={classNames(
|
||||
styles.tooltip,
|
||||
styles['top-level'],
|
||||
tooltipClassName,
|
||||
)}
|
||||
style={{ left: 0, ...tooltipStyle }}
|
||||
>
|
||||
{children}
|
||||
</Tooltip>
|
||||
</>
|
||||
);
|
||||
return (
|
||||
<div
|
||||
className={classNames(styles.popup_container, className)}
|
||||
style={style}
|
||||
>
|
||||
{renderContent()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,234 @@
|
||||
/** 基本组件样式 */
|
||||
.dot {
|
||||
width: 4px;
|
||||
height: 4px;
|
||||
border-radius: 50%;
|
||||
background: gray;
|
||||
}
|
||||
|
||||
.empty-block {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.help-line-block {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
.line {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 1px;
|
||||
height: 100%;
|
||||
background: gray;
|
||||
}
|
||||
}
|
||||
|
||||
/** 业务组件样式 */
|
||||
.half-top-root {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
.line {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
border-left: 1px solid gray;
|
||||
border-bottom: 1px solid gray;
|
||||
border-radius: 0 0 0 4px;
|
||||
width: 80%;
|
||||
height: 22px;
|
||||
}
|
||||
|
||||
.dot {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&.children {
|
||||
.dot {
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 22px;
|
||||
left: 70%;
|
||||
transform: translate3d(0, -60%, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.half-bottom-root {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
.line {
|
||||
position: absolute;
|
||||
top: 22px;
|
||||
left: 0;
|
||||
border-left: 1px solid gray;
|
||||
border-top: 1px solid gray;
|
||||
border-radius: 4px 0 0;
|
||||
width: 80%;
|
||||
height: calc(100% - 12px);
|
||||
}
|
||||
|
||||
.dot {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&.children {
|
||||
.dot {
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 22px;
|
||||
left: 70%;
|
||||
transform: translate3d(0, -40%, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.full-root {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
.top-line {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 80%;
|
||||
height: 22px;
|
||||
border-left: 1px solid gray;
|
||||
border-bottom: 1px solid gray;
|
||||
border-radius: 0 0 0 4px;
|
||||
}
|
||||
|
||||
.bottom-line {
|
||||
position: absolute;
|
||||
top: 19px;
|
||||
left: 0;
|
||||
width: 1px;
|
||||
height: calc(100% - 19px);
|
||||
background: gray;
|
||||
}
|
||||
|
||||
.dot {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&.children {
|
||||
.dot {
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 22px;
|
||||
left: 70%;
|
||||
transform: translate3d(0, -60%, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.half-top-child {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
.line {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
border-left: 1px solid gray;
|
||||
border-bottom: 1px solid gray;
|
||||
border-radius: 0 0 0 4px;
|
||||
width: 80%;
|
||||
height: 22px;
|
||||
}
|
||||
|
||||
.dot {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&.children {
|
||||
.dot {
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 22px;
|
||||
left: 70%;
|
||||
transform: translate3d(0, -60%, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.full-child {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
.top-line {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 80%;
|
||||
height: 22px;
|
||||
border-left: 1px solid gray;
|
||||
border-bottom: 1px solid gray;
|
||||
border-radius: 0 0 0 4px;
|
||||
}
|
||||
|
||||
.bottom-line {
|
||||
position: absolute;
|
||||
top: 19px;
|
||||
left: 0;
|
||||
width: 1px;
|
||||
height: calc(100% - 19px);
|
||||
background: gray;
|
||||
}
|
||||
|
||||
.dot {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&.children {
|
||||
.dot {
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 22px;
|
||||
left: 70%;
|
||||
transform: translate3d(0, -60%, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.multiline {
|
||||
.line {
|
||||
position: absolute;
|
||||
top: -51px;
|
||||
height: 73px;
|
||||
}
|
||||
|
||||
.top-line {
|
||||
position: absolute;
|
||||
top: -51px;
|
||||
height: 73px;
|
||||
}
|
||||
|
||||
&.with-name-error {
|
||||
.line {
|
||||
position: absolute;
|
||||
top: -7px;
|
||||
height: 29px;
|
||||
}
|
||||
|
||||
.top-line {
|
||||
position: absolute;
|
||||
top: -7px;
|
||||
height: 29px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,262 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { LineShowResult, getLineShowResult } from '@/parameters/utils/utils';
|
||||
|
||||
import { type TreeNodeCustomData } from '../../type';
|
||||
|
||||
import styles from './index.module.less';
|
||||
|
||||
interface IconComponentProps {
|
||||
className?: string;
|
||||
style?: React.CSSProperties;
|
||||
}
|
||||
|
||||
export function Dot({ className, style }: IconComponentProps) {
|
||||
return <div className={classNames(styles.dot, className)} style={style} />;
|
||||
}
|
||||
|
||||
export function EmptyBlock({ className, style }: IconComponentProps) {
|
||||
return (
|
||||
<div
|
||||
className={classNames(styles['empty-block'], className)}
|
||||
style={style}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function HelpLineBlock({ className, style }: IconComponentProps) {
|
||||
return (
|
||||
<div
|
||||
className={classNames(styles['help-line-block'], className)}
|
||||
style={style}
|
||||
>
|
||||
<div className={styles.line} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function HalfTopRoot({ className, style }: IconComponentProps) {
|
||||
return (
|
||||
<div
|
||||
className={classNames(styles['half-top-root'], className)}
|
||||
style={style}
|
||||
>
|
||||
<div className={styles.line} />
|
||||
<Dot className={styles.dot} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function HalfTopRootWithChildren({
|
||||
className,
|
||||
style,
|
||||
}: IconComponentProps) {
|
||||
return (
|
||||
<HalfTopRoot
|
||||
className={classNames(styles.children, className)}
|
||||
style={style}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function HalfBottomRoot({ className, style }: IconComponentProps) {
|
||||
return (
|
||||
<div
|
||||
className={classNames(styles['half-bottom-root'], className)}
|
||||
style={style}
|
||||
>
|
||||
<div className={styles.line} />
|
||||
<Dot className={styles.dot} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function HalfBottomRootWithChildren({
|
||||
className,
|
||||
style,
|
||||
}: IconComponentProps) {
|
||||
return (
|
||||
<HalfBottomRoot
|
||||
className={classNames(styles.children, className)}
|
||||
style={style}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function FullRoot({ className, style }: IconComponentProps) {
|
||||
return (
|
||||
<div className={classNames(styles['full-root'], className)} style={style}>
|
||||
<div className={styles['top-line']} />
|
||||
<div className={styles['bottom-line']} />
|
||||
<Dot className={styles.dot} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function FullRootWithChildren({ className, style }: IconComponentProps) {
|
||||
return (
|
||||
<FullRoot
|
||||
className={classNames(styles.children, className)}
|
||||
style={style}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function HalfTopChild({ className, style }: IconComponentProps) {
|
||||
return (
|
||||
<div
|
||||
className={classNames(styles['half-top-child'], className)}
|
||||
style={style}
|
||||
>
|
||||
<div className={styles.line} />
|
||||
<Dot className={styles.dot} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function HalfTopChildWithChildren({
|
||||
className,
|
||||
style,
|
||||
}: IconComponentProps) {
|
||||
return (
|
||||
<HalfTopChild
|
||||
className={classNames(styles.children, className)}
|
||||
style={style}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function FullChild({ className, style }: IconComponentProps) {
|
||||
return (
|
||||
<div className={classNames(styles['full-child'], className)} style={style}>
|
||||
<div className={styles['top-line']} />
|
||||
<div className={styles['bottom-line']} />
|
||||
<Dot className={styles.dot} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function FullChildWithChildren({
|
||||
className,
|
||||
style,
|
||||
}: IconComponentProps) {
|
||||
return (
|
||||
<FullChild
|
||||
className={classNames(styles.children, className)}
|
||||
style={style}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
interface LevelLineProps {
|
||||
level: number;
|
||||
data: TreeNodeCustomData;
|
||||
className?: string;
|
||||
style?: React.CSSProperties;
|
||||
multiInfo?: {
|
||||
multiline: boolean;
|
||||
withNameError?: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export default function LevelLine({
|
||||
level,
|
||||
data,
|
||||
className,
|
||||
style,
|
||||
multiInfo = { multiline: false },
|
||||
}: LevelLineProps) {
|
||||
// getLineShowResult 返回数据,暂时没涉及到 root 画线
|
||||
const lineShowResult = getLineShowResult({ level, data });
|
||||
const showMap: Record<LineShowResult, React.ReactNode> = {
|
||||
[LineShowResult.HalfTopRoot]: (
|
||||
<HalfTopRoot className={className} style={style} />
|
||||
),
|
||||
[LineShowResult.HalfTopRootWithChildren]: (
|
||||
<HalfTopRootWithChildren className={className} style={style} />
|
||||
),
|
||||
[LineShowResult.HalfBottomRoot]: (
|
||||
<HalfBottomRoot className={className} style={style} />
|
||||
),
|
||||
[LineShowResult.HalfBottomRootWithChildren]: (
|
||||
<HalfBottomRootWithChildren className={className} style={style} />
|
||||
),
|
||||
[LineShowResult.FullRoot]: <FullRoot className={className} style={style} />,
|
||||
[LineShowResult.FullRootWithChildren]: (
|
||||
<FullRootWithChildren className={className} style={style} />
|
||||
),
|
||||
// 在 output tree 中,暂时没涉及到 root 画线
|
||||
[LineShowResult.HalfTopChild]: (
|
||||
<HalfTopChild
|
||||
className={classNames(
|
||||
multiInfo?.multiline ? styles.multiline : null,
|
||||
multiInfo?.withNameError ? styles['with-name-error'] : null,
|
||||
className,
|
||||
)}
|
||||
style={style}
|
||||
/>
|
||||
),
|
||||
[LineShowResult.HalfTopChildWithChildren]: (
|
||||
<HalfTopChildWithChildren
|
||||
className={classNames(
|
||||
multiInfo?.multiline ? styles.multiline : null,
|
||||
multiInfo?.withNameError ? styles['with-name-error'] : null,
|
||||
className,
|
||||
)}
|
||||
style={style}
|
||||
/>
|
||||
),
|
||||
[LineShowResult.FullChild]: (
|
||||
<FullChild
|
||||
className={classNames(
|
||||
multiInfo?.multiline ? styles.multiline : null,
|
||||
multiInfo?.withNameError ? styles['with-name-error'] : null,
|
||||
className,
|
||||
)}
|
||||
style={style}
|
||||
/>
|
||||
),
|
||||
[LineShowResult.FullChildWithChildren]: (
|
||||
<FullChildWithChildren
|
||||
className={classNames(
|
||||
multiInfo?.multiline ? styles.multiline : null,
|
||||
multiInfo?.withNameError ? styles['with-name-error'] : null,
|
||||
className,
|
||||
)}
|
||||
style={style}
|
||||
/>
|
||||
),
|
||||
[LineShowResult.EmptyBlock]: (
|
||||
<EmptyBlock className={className} style={style} />
|
||||
),
|
||||
[LineShowResult.HelpLineBlock]: (
|
||||
<HelpLineBlock className={className} style={style} />
|
||||
),
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{lineShowResult.map((item, index) => (
|
||||
<React.Fragment key={index}>{showMap[item]}</React.Fragment>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
.container {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
position: relative;
|
||||
align-self: stretch;
|
||||
flex-direction: column;
|
||||
margin-left: 8px;
|
||||
|
||||
.desc-not-focus {
|
||||
textarea {
|
||||
padding: 0 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.desc-not-focus-with-value {
|
||||
textarea {
|
||||
padding: 0 12px;
|
||||
-webkit-line-clamp: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import cs from 'classnames';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
|
||||
import WorkflowSLTextArea from '../workflow-sl-textarea';
|
||||
import { type TreeNodeCustomData } from '../../type';
|
||||
import { DescriptionLine } from '../../constants';
|
||||
|
||||
import styles from './index.module.less';
|
||||
|
||||
interface ParamNameProps {
|
||||
data: TreeNodeCustomData;
|
||||
disabled?: boolean;
|
||||
onChange: (desc: string) => void;
|
||||
onLineChange?: (type: DescriptionLine) => void;
|
||||
hasObjectLike?: boolean;
|
||||
}
|
||||
|
||||
export default function ParamDescription({
|
||||
data,
|
||||
disabled,
|
||||
onChange,
|
||||
onLineChange,
|
||||
hasObjectLike,
|
||||
}: ParamNameProps) {
|
||||
const [inputFocus, setInputFocus] = useState(false);
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<WorkflowSLTextArea
|
||||
className={cs(
|
||||
inputFocus
|
||||
? null
|
||||
: data.description
|
||||
? styles['desc-not-focus-with-value']
|
||||
: styles['desc-not-focus'],
|
||||
styles.desc,
|
||||
hasObjectLike ? styles['desc-object-like'] : null,
|
||||
)}
|
||||
value={data.description}
|
||||
ellipsis={true}
|
||||
// 好像不生效
|
||||
disabled={disabled}
|
||||
handleBlur={() => {
|
||||
setInputFocus(false);
|
||||
onLineChange?.(DescriptionLine.Single);
|
||||
}}
|
||||
handleChange={(desc: string) => {
|
||||
onChange(desc);
|
||||
}}
|
||||
handleFocus={() => {
|
||||
setInputFocus(true);
|
||||
onLineChange?.(DescriptionLine.Multi);
|
||||
}}
|
||||
textAreaProps={
|
||||
inputFocus
|
||||
? {
|
||||
placeholder: I18n.t('workflow_detail_llm_output_decription'),
|
||||
maxLength: 50,
|
||||
rows: 2,
|
||||
autosize: false,
|
||||
maxCount: 50,
|
||||
}
|
||||
: {
|
||||
placeholder: I18n.t('workflow_detail_llm_output_decription'),
|
||||
rows: 1,
|
||||
autosize: false,
|
||||
}
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
.container {
|
||||
display: flex;
|
||||
position: relative;
|
||||
align-self: stretch;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
|
||||
&.withDescription {
|
||||
flex: auto;
|
||||
flex-grow: 0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.name {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
align-self: stretch;
|
||||
width: 100%;
|
||||
|
||||
&>div:first-child {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import type { CSSProperties } from 'react';
|
||||
|
||||
import cx from 'classnames';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
|
||||
import useErrorMessage from '@/parameters/hooks/use-error-message';
|
||||
import useConfig from '@/parameters/hooks/use-config';
|
||||
|
||||
import WorkflowSLInput from '../workflow-sl-input';
|
||||
import { type TreeNodeCustomData } from '../../type';
|
||||
|
||||
import styles from './index.module.less';
|
||||
|
||||
interface ParamNameProps {
|
||||
data: TreeNodeCustomData;
|
||||
disabled?: boolean;
|
||||
style?: CSSProperties;
|
||||
onChange: (name: string) => void;
|
||||
}
|
||||
|
||||
export default function ParamName({
|
||||
disabled,
|
||||
data,
|
||||
style,
|
||||
onChange,
|
||||
}: ParamNameProps) {
|
||||
const errorMessage = useErrorMessage('name');
|
||||
const [slient, setSlient] = useState(true);
|
||||
const showError = slient === false && errorMessage;
|
||||
const { withDescription } = useConfig();
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cx(styles.container, {
|
||||
[styles.withDescription]: withDescription,
|
||||
})}
|
||||
style={style}
|
||||
>
|
||||
<WorkflowSLInput
|
||||
className={styles.name}
|
||||
value={data.name || ''}
|
||||
disabled={disabled}
|
||||
handleBlur={() => setSlient(false)}
|
||||
handleChange={(name: string) => {
|
||||
setSlient(true);
|
||||
onChange(name);
|
||||
}}
|
||||
inputProps={{
|
||||
size: 'small',
|
||||
placeholder: I18n.t('workflow_detail_end_output_entername'),
|
||||
disabled,
|
||||
}}
|
||||
errorMsg={showError ? errorMessage : ''}
|
||||
validateStatus={showError ? 'error' : 'default'}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { UIIconButton } from '@coze-arch/bot-semi';
|
||||
import { IconAdd } from '@coze-arch/bot-icons';
|
||||
|
||||
import styles from './index.module.less';
|
||||
|
||||
type AddOperationProps = React.PropsWithChildren<{
|
||||
readonly?: boolean;
|
||||
onClick: React.MouseEventHandler<HTMLButtonElement>;
|
||||
className?: string;
|
||||
style?: React.CSSProperties;
|
||||
disabled?: boolean;
|
||||
}>;
|
||||
|
||||
export default function AddOperation({
|
||||
readonly,
|
||||
onClick,
|
||||
className,
|
||||
style,
|
||||
disabled,
|
||||
}: AddOperationProps) {
|
||||
if (readonly) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<UIIconButton
|
||||
onClick={onClick}
|
||||
className={classNames(
|
||||
styles.container,
|
||||
disabled ? styles.disabled : null,
|
||||
className,
|
||||
)}
|
||||
style={style}
|
||||
icon={<IconAdd className={styles.icon} />}
|
||||
disabled={disabled}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
.container {
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
align-self: stretch;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
margin-left: 8px;
|
||||
|
||||
.icon-no {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
cursor: pointer;
|
||||
color: #888D92;
|
||||
|
||||
&.disabled {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
&>svg {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.add {
|
||||
margin-left: 8px;
|
||||
color: #4D53E8;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
line-height: 16px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { Tooltip } from '@coze-arch/bot-semi';
|
||||
import { IconNo } from '@coze-arch/bot-icons';
|
||||
|
||||
import { OperatorLargeSize, OperatorSmallSize } from '@/parameters/constants';
|
||||
|
||||
import { type TreeNodeCustomData } from '../../type';
|
||||
import { ObjectLikeTypes } from '../../constants';
|
||||
import AddOperation from './add-operation';
|
||||
|
||||
import styles from './index.module.less';
|
||||
|
||||
interface ParamOperatorProps {
|
||||
data: TreeNodeCustomData;
|
||||
level: number;
|
||||
onAppend: () => void;
|
||||
onDelete: () => void;
|
||||
disableDelete: boolean;
|
||||
hasObjectLike?: boolean;
|
||||
}
|
||||
|
||||
export default function ParamOperator({
|
||||
data,
|
||||
level,
|
||||
onDelete,
|
||||
onAppend,
|
||||
disableDelete,
|
||||
hasObjectLike,
|
||||
}: ParamOperatorProps) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-magic-numbers
|
||||
const isLimited = level >= 3;
|
||||
// 是否展示新增子项的按钮
|
||||
const needRenderAppendChild =
|
||||
ObjectLikeTypes.includes(data.type) && !isLimited;
|
||||
const computedOperatorStyle = (): React.CSSProperties => {
|
||||
if (!hasObjectLike) {
|
||||
return { width: OperatorSmallSize };
|
||||
}
|
||||
return { width: OperatorLargeSize };
|
||||
};
|
||||
return (
|
||||
<div className={styles.container} style={computedOperatorStyle()}>
|
||||
<div
|
||||
className={styles['icon-no']}
|
||||
onClick={() => {
|
||||
if (disableDelete) {
|
||||
return;
|
||||
}
|
||||
onDelete();
|
||||
}}
|
||||
>
|
||||
<IconNo
|
||||
className={classNames({
|
||||
[styles.icon]: true,
|
||||
[styles.disabled]: disableDelete,
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
{needRenderAppendChild && (
|
||||
<div className={styles.add}>
|
||||
<Tooltip content={I18n.t('workflow_detail_node_output_add_subitem')}>
|
||||
<div>
|
||||
<AddOperation onClick={onAppend} />
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
.container {
|
||||
display: flex;
|
||||
// align-items: center;
|
||||
position: relative;
|
||||
align-self: stretch;
|
||||
flex-direction: column;
|
||||
width: 155px;
|
||||
margin-left: 8px;
|
||||
|
||||
|
||||
|
||||
.pop-container {
|
||||
align-self: self-start;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.param-type {
|
||||
width: 100%;
|
||||
height: 24px;
|
||||
padding: 0 1px;
|
||||
}
|
||||
|
||||
// 已经无用,可以删除,添加子项按钮已经放在单独组件中了
|
||||
.param-operator {
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
align-self: stretch;
|
||||
width: 86px;
|
||||
height: 24px;
|
||||
margin-left: 8px;
|
||||
|
||||
.icon-no {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
cursor: pointer;
|
||||
color: #888D92;
|
||||
|
||||
&.disabled {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
&>svg {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.add {
|
||||
margin-left: 8px;
|
||||
color: #4D53E8;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
line-height: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { type SelectProps } from '@coze-arch/bot-semi/Select';
|
||||
import { Select } from '@coze-arch/bot-semi';
|
||||
|
||||
import convertMapToOptions from '@/parameters/utils/convert-map-to-options';
|
||||
import { PARAM_TYPE_ALIAS_MAP, ParamTypeAlias } from '@/parameters/types';
|
||||
|
||||
import PopupContainer from '../popup-container';
|
||||
import { type TreeNodeCustomData } from '../../type';
|
||||
import { ObjectLikeTypes } from '../../constants';
|
||||
|
||||
import styles from './index.module.less';
|
||||
|
||||
interface ParamTypeProps {
|
||||
data: TreeNodeCustomData;
|
||||
level: number;
|
||||
onSelectChange?: SelectProps['onChange'];
|
||||
disabled?: boolean;
|
||||
// 不支持使用的类型
|
||||
disabledTypes?: ParamTypeAlias[];
|
||||
}
|
||||
|
||||
export default function ParamType({
|
||||
data,
|
||||
onSelectChange,
|
||||
level,
|
||||
disabled,
|
||||
disabledTypes = [],
|
||||
}: ParamTypeProps) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-magic-numbers
|
||||
const isLimited = level >= 3;
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<PopupContainer className={styles['pop-container']}>
|
||||
<Select
|
||||
placeholder={I18n.t('workflow_detail_start_variable_type')}
|
||||
disabled={disabled}
|
||||
onChange={val => {
|
||||
onSelectChange?.(val);
|
||||
}}
|
||||
className={styles['param-type']}
|
||||
optionList={convertMapToOptions(PARAM_TYPE_ALIAS_MAP, {
|
||||
computedValue: Number,
|
||||
passItem: item => item === ParamTypeAlias.List.toString(),
|
||||
}).map(item => ({
|
||||
...item,
|
||||
disabled:
|
||||
disabledTypes?.includes(Number(item.value)) ||
|
||||
(isLimited && ObjectLikeTypes.includes(Number(item.value))),
|
||||
}))}
|
||||
value={data.type}
|
||||
/>
|
||||
</PopupContainer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
.popup-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.popup-container-id {
|
||||
position: relative;
|
||||
:global {
|
||||
.semi-portal-inner {
|
||||
top: 2px !important;
|
||||
left: 0 !important;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React, {
|
||||
type PropsWithChildren,
|
||||
type ReactElement,
|
||||
useMemo,
|
||||
} from 'react';
|
||||
|
||||
import { nanoid } from 'nanoid';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import s from './index.module.less';
|
||||
|
||||
export const PopupContainer: React.FC<
|
||||
PropsWithChildren<{
|
||||
className?: string;
|
||||
containerName?: string;
|
||||
containerClassName?: string;
|
||||
containerStyle?: React.CSSProperties;
|
||||
}>
|
||||
> = ({
|
||||
className,
|
||||
children,
|
||||
containerName,
|
||||
containerClassName,
|
||||
containerStyle,
|
||||
}) => {
|
||||
const _nanoid = useMemo(
|
||||
() => `${containerName || 'popup_container'}_${nanoid()}`,
|
||||
[containerName],
|
||||
);
|
||||
const _children = React.cloneElement(children as unknown as ReactElement, {
|
||||
getPopupContainer: () => document.getElementById(_nanoid) as HTMLElement,
|
||||
});
|
||||
return (
|
||||
<div className={classNames(s['popup-container'], className)}>
|
||||
{_children}
|
||||
<div
|
||||
id={_nanoid}
|
||||
style={containerStyle}
|
||||
className={classNames([
|
||||
'nowheel',
|
||||
s['popup-container-id'],
|
||||
containerClassName,
|
||||
])}
|
||||
></div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PopupContainer;
|
||||
@@ -0,0 +1,47 @@
|
||||
.input-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
|
||||
>* {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
span {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.error-content {
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.error-float {
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.error-text {
|
||||
font-size: 14px;
|
||||
font-family: "SF Pro Display", -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
line-height: 22px;
|
||||
color: var(--semi-color-danger)
|
||||
}
|
||||
|
||||
.input {
|
||||
input {
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.limit-count {
|
||||
padding-left: 8px;
|
||||
padding-right: 12px;
|
||||
overflow: hidden;
|
||||
color: var(--light-usage-text-color-text-3, rgba(28, 31, 35, 0.35));
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 16px;
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React, {
|
||||
type ComponentProps,
|
||||
useRef,
|
||||
useImperativeHandle,
|
||||
useMemo,
|
||||
useEffect,
|
||||
type ForwardedRef,
|
||||
} from 'react';
|
||||
|
||||
import isNumber from 'lodash-es/isNumber';
|
||||
import cs from 'classnames';
|
||||
import { useReactive } from 'ahooks';
|
||||
import { type TooltipProps } from '@coze-arch/bot-semi/Tooltip';
|
||||
import { type InputProps } from '@coze-arch/bot-semi/Input';
|
||||
import { UIInput } from '@coze-arch/bot-semi';
|
||||
|
||||
import AutoSizeTooltip from '../auto-size-tooltip';
|
||||
|
||||
import s from './index.module.less';
|
||||
|
||||
export interface WorkflowSLInputRefType {
|
||||
triggerFocus?: () => void;
|
||||
}
|
||||
|
||||
export type WorkflowSLInputProps = ComponentProps<typeof UIInput> & {
|
||||
value: string | undefined;
|
||||
onRef?: ForwardedRef<WorkflowSLInputRefType>;
|
||||
ellipsis?: boolean;
|
||||
handleChange?: (v: string) => void;
|
||||
handleBlur?: (v: string) => void;
|
||||
handleFocus?: (v: string) => void;
|
||||
ellipsisTooltipProps?: TooltipProps;
|
||||
onFocusTooltipProps?: TooltipProps;
|
||||
tooltipProps?: TooltipProps;
|
||||
inputProps?: InputProps;
|
||||
errorMsg?: string;
|
||||
errorMsgFloat?: boolean;
|
||||
maxCount?: number;
|
||||
className?: string;
|
||||
style?: React.CSSProperties;
|
||||
};
|
||||
|
||||
const SL_INPUT_TIMEOUT = 10;
|
||||
|
||||
export default function WorkflowSLInput(props: WorkflowSLInputProps) {
|
||||
const { ellipsis = true, maxCount } = props;
|
||||
const showCount = isNumber(maxCount) && maxCount > 0;
|
||||
useImperativeHandle(props.onRef, () => ({
|
||||
triggerFocus,
|
||||
}));
|
||||
const $state = useReactive({
|
||||
value: props.value,
|
||||
inputOnFocus: false,
|
||||
inputEle: false,
|
||||
});
|
||||
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const triggerFocus = () => {
|
||||
$state.inputEle = true;
|
||||
inputRef?.current?.focus();
|
||||
};
|
||||
|
||||
const onFocus = () => {
|
||||
$state.inputOnFocus = true;
|
||||
$state.inputEle = true;
|
||||
props?.handleFocus?.($state.value || '');
|
||||
};
|
||||
|
||||
const onBlur = (e: React.FocusEvent<HTMLInputElement>) => {
|
||||
$state.inputOnFocus = false;
|
||||
props?.handleBlur?.($state.value || '');
|
||||
props?.onBlur?.(e);
|
||||
$state.inputEle = false;
|
||||
};
|
||||
|
||||
const onChange = (v: string) => {
|
||||
$state.value = v;
|
||||
props?.handleChange?.(v);
|
||||
};
|
||||
|
||||
const onclick = () => {
|
||||
if (!$state.inputEle) {
|
||||
setTimeout(() => {
|
||||
inputRef?.current?.focus();
|
||||
}, SL_INPUT_TIMEOUT);
|
||||
}
|
||||
$state.inputEle = true;
|
||||
};
|
||||
const hasEllipsis = useMemo(() => {
|
||||
const clientWidth = inputRef.current?.clientWidth || 0;
|
||||
const scrollWidth = inputRef.current?.scrollWidth || 0;
|
||||
return clientWidth < scrollWidth - 1;
|
||||
}, [
|
||||
ellipsis,
|
||||
$state.inputOnFocus,
|
||||
$state.value,
|
||||
inputRef.current?.clientWidth,
|
||||
inputRef.current?.scrollWidth,
|
||||
$state.inputEle,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
$state.value = props.value;
|
||||
}, [props.value]);
|
||||
|
||||
const LimitCountNode = (
|
||||
<span className={s['limit-count']}>
|
||||
{$state.value?.length || 0}/{maxCount}
|
||||
</span>
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cs(s['input-wrapper'], props.className)}
|
||||
style={props.style}
|
||||
>
|
||||
{!$state.inputEle && hasEllipsis ? (
|
||||
<AutoSizeTooltip
|
||||
content={$state.value}
|
||||
position={'top'}
|
||||
showArrow
|
||||
mouseEnterDelay={300}
|
||||
{...props.tooltipProps}
|
||||
>
|
||||
<div
|
||||
className={cs(props?.errorMsg ? s['error-wrapper'] : null)}
|
||||
onClick={onclick}
|
||||
>
|
||||
<UIInput
|
||||
{...props.inputProps}
|
||||
validateStatus={props.validateStatus}
|
||||
ref={inputRef}
|
||||
value={$state.value}
|
||||
className={ellipsis ? s.input : ''}
|
||||
suffix={showCount ? LimitCountNode : undefined}
|
||||
></UIInput>
|
||||
</div>
|
||||
</AutoSizeTooltip>
|
||||
) : (
|
||||
<div className={cs(props?.errorMsg ? s['error-wrapper'] : null)}>
|
||||
<AutoSizeTooltip
|
||||
{...props.onFocusTooltipProps}
|
||||
trigger="custom"
|
||||
visible={
|
||||
Boolean(props.onFocusTooltipProps?.content) && $state.inputOnFocus
|
||||
}
|
||||
showArrow
|
||||
>
|
||||
<UIInput
|
||||
{...props.inputProps}
|
||||
validateStatus={props.validateStatus}
|
||||
ref={inputRef}
|
||||
value={$state.value}
|
||||
className={ellipsis ? s.input : ''}
|
||||
onChange={onChange}
|
||||
onFocus={onFocus}
|
||||
onBlur={onBlur}
|
||||
suffix={showCount ? LimitCountNode : undefined}
|
||||
></UIInput>
|
||||
</AutoSizeTooltip>
|
||||
</div>
|
||||
)}
|
||||
{props?.errorMsg && (
|
||||
<div
|
||||
className={cs(
|
||||
s['error-content'],
|
||||
props?.errorMsgFloat ? s['error-float'] : null,
|
||||
)}
|
||||
>
|
||||
<div className={s['error-text']}>{props?.errorMsg}</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
// todo: 这里需要修复为正确的值
|
||||
@error-red: #ddd;
|
||||
|
||||
.input-wrapper {
|
||||
width: 100%;
|
||||
|
||||
span {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.error-wrapper {
|
||||
:global {
|
||||
.semi-input-wrapper {
|
||||
border: 1px solid @error-red;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.error-content {
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.error-float {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.error-text {
|
||||
position: absolute;
|
||||
|
||||
padding-top: 2px;
|
||||
padding-left: 12px;
|
||||
|
||||
font-size: 12px;
|
||||
color: @error-red;
|
||||
}
|
||||
|
||||
.textarea-pd {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.inputting {
|
||||
textarea {
|
||||
.textarea-pd;
|
||||
}
|
||||
}
|
||||
|
||||
.input-blur {
|
||||
textarea {
|
||||
.textarea-pd;
|
||||
|
||||
overflow: hidden;
|
||||
display: -webkit-box;
|
||||
|
||||
text-overflow: ellipsis;
|
||||
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,213 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/* eslint-disable @coze-arch/max-line-per-function */
|
||||
import React, {
|
||||
type ComponentProps,
|
||||
useRef,
|
||||
useImperativeHandle,
|
||||
useMemo,
|
||||
useEffect,
|
||||
type ForwardedRef,
|
||||
} from 'react';
|
||||
|
||||
import cs from 'classnames';
|
||||
import { useReactive } from 'ahooks';
|
||||
import { type TooltipProps } from '@coze-arch/bot-semi/Tooltip';
|
||||
import { type TextAreaProps } from '@coze-arch/bot-semi/Input';
|
||||
import { TextArea } from '@coze-arch/bot-semi';
|
||||
|
||||
import AutoSizeTooltip from '../auto-size-tooltip';
|
||||
|
||||
import s from './index.module.less';
|
||||
|
||||
export interface WorkflowSLTextAreaRefType {
|
||||
triggerFocus?: () => void;
|
||||
}
|
||||
|
||||
export type WorkflowSLTextAreaProps = ComponentProps<typeof TextArea> & {
|
||||
value: string | undefined;
|
||||
onRef?: ForwardedRef<WorkflowSLTextAreaRefType>;
|
||||
ellipsis?: boolean;
|
||||
handleChange?: (v: string) => void;
|
||||
handleBlur?: (v: string) => void;
|
||||
handleFocus?: (v: string) => void;
|
||||
ellipsisTooltipProps?: TooltipProps;
|
||||
onFocusTooltipProps?: TooltipProps;
|
||||
textAreaProps?: TextAreaProps;
|
||||
errorMsg?: string;
|
||||
errorMsgFloat?: boolean;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* @component TextArea 在 Workflow 场景下的二次封装;
|
||||
* focus(inputting) 的时候提供多行滚动输入能力,blur 的时候提供 ellipsis 和 tooltip 提示能力
|
||||
*/
|
||||
export default function WorkflowSLTextArea(props: WorkflowSLTextAreaProps) {
|
||||
const { ellipsis = true } = props;
|
||||
useImperativeHandle(props.onRef, () => ({
|
||||
triggerFocus,
|
||||
}));
|
||||
const $state = useReactive({
|
||||
value: props.value,
|
||||
inputOnFocus: false,
|
||||
inputHover: false,
|
||||
});
|
||||
|
||||
const textAreaRef = useRef<HTMLTextAreaElement>(null);
|
||||
|
||||
const triggerFocus = () => {
|
||||
$state.inputOnFocus = true;
|
||||
textAreaRef?.current?.focus();
|
||||
};
|
||||
|
||||
const onFocus = () => {
|
||||
$state.inputOnFocus = true;
|
||||
props?.handleFocus?.($state.value || '');
|
||||
};
|
||||
|
||||
const onBlur = (e: React.FocusEvent<HTMLTextAreaElement>) => {
|
||||
$state.inputOnFocus = false;
|
||||
props?.handleBlur?.($state.value || '');
|
||||
props?.onBlur?.(e);
|
||||
// 失焦的时候,滚动到最顶端
|
||||
if (textAreaRef?.current) {
|
||||
textAreaRef.current.scrollTop = 0;
|
||||
}
|
||||
};
|
||||
|
||||
const onChange = (v: string) => {
|
||||
$state.value = v;
|
||||
props?.handleChange?.(v);
|
||||
};
|
||||
|
||||
// 输入法输入结束
|
||||
const onCompositionEnd = (e: React.CompositionEvent<HTMLTextAreaElement>) => {
|
||||
const target = e.target as HTMLTextAreaElement;
|
||||
|
||||
if (
|
||||
props.textAreaProps?.maxCount &&
|
||||
(target.textLength || 0) > props.textAreaProps?.maxCount
|
||||
) {
|
||||
const v = target.value?.slice(0, props.textAreaProps?.maxCount);
|
||||
$state.value = v;
|
||||
props?.handleChange?.(v);
|
||||
}
|
||||
};
|
||||
|
||||
const hasEllipsis = useMemo(() => {
|
||||
const clientHeight = textAreaRef.current?.clientHeight || 0;
|
||||
const scrollHeight = textAreaRef.current?.scrollHeight || 0;
|
||||
return clientHeight < scrollHeight - 1;
|
||||
}, [
|
||||
ellipsis,
|
||||
$state.inputOnFocus,
|
||||
$state.value,
|
||||
textAreaRef.current?.clientHeight,
|
||||
textAreaRef.current?.scrollHeight,
|
||||
props.textAreaProps?.rows,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
$state.value = props.value;
|
||||
}, [props.value]);
|
||||
|
||||
/** 是否处于失焦缩略状态 */
|
||||
const ellipsisWithBlur = useMemo(
|
||||
() => !$state.inputOnFocus && hasEllipsis,
|
||||
[hasEllipsis, $state.inputOnFocus],
|
||||
);
|
||||
|
||||
const showTooltip = useMemo(
|
||||
() =>
|
||||
ellipsisWithBlur
|
||||
? Boolean($state.value) && $state.inputHover
|
||||
: Boolean(props.onFocusTooltipProps?.content) && $state.inputOnFocus,
|
||||
[
|
||||
ellipsisWithBlur,
|
||||
$state.inputHover,
|
||||
$state.inputOnFocus,
|
||||
props.onFocusTooltipProps?.content,
|
||||
],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={cs(s['input-wrapper'], props.className)}>
|
||||
<AutoSizeTooltip
|
||||
content={
|
||||
<article
|
||||
style={{
|
||||
maxWidth: 200,
|
||||
wordWrap: 'break-word',
|
||||
wordBreak: 'normal',
|
||||
textAlign: 'left',
|
||||
}}
|
||||
>
|
||||
{$state.value}
|
||||
</article>
|
||||
}
|
||||
position={'top'}
|
||||
showArrow
|
||||
mouseEnterDelay={300}
|
||||
trigger="custom"
|
||||
visible={showTooltip}
|
||||
{...(ellipsisWithBlur
|
||||
? props.ellipsisTooltipProps
|
||||
: props.onFocusTooltipProps)}
|
||||
>
|
||||
<div
|
||||
className={cs(props?.errorMsg ? s['error-wrapper'] : null)}
|
||||
onMouseEnter={() => {
|
||||
$state.inputHover = true;
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
$state.inputHover = false;
|
||||
}}
|
||||
>
|
||||
<TextArea
|
||||
{...props.textAreaProps}
|
||||
ref={textAreaRef}
|
||||
value={$state.value}
|
||||
className={
|
||||
ellipsis
|
||||
? !$state.inputOnFocus
|
||||
? s['input-blur']
|
||||
: s.inputting
|
||||
: ''
|
||||
}
|
||||
onChange={onChange}
|
||||
onFocus={onFocus}
|
||||
onBlur={onBlur}
|
||||
disabled={props.disabled}
|
||||
onCompositionEnd={onCompositionEnd}
|
||||
></TextArea>
|
||||
</div>
|
||||
</AutoSizeTooltip>
|
||||
|
||||
{props?.errorMsg && (
|
||||
<div
|
||||
className={cs(
|
||||
s['error-content'],
|
||||
props?.errorMsgFloat ? s['error-float'] : null,
|
||||
)}
|
||||
>
|
||||
<div className={s['error-text']}>{props?.errorMsg}</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { ParamTypeAlias } from '../../types';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
export const ObjectLikeTypes = [
|
||||
ParamTypeAlias.Object,
|
||||
ParamTypeAlias.ArrayObject,
|
||||
];
|
||||
|
||||
export enum ChangeMode {
|
||||
Update,
|
||||
Delete,
|
||||
Append,
|
||||
DeleteChildren,
|
||||
}
|
||||
|
||||
export enum DescriptionLine {
|
||||
Single = 'singleline',
|
||||
Multi = 'multiline',
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
.font-normal {
|
||||
color: rgba(28, 31, 35, 0.80);
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
align-items: center !important;
|
||||
position: relative;
|
||||
|
||||
// 下面是兼容线的样式的,不要动
|
||||
&:first-child {
|
||||
& > div:first-child {
|
||||
& > div {
|
||||
& > div:first-child {
|
||||
top: 12px;
|
||||
}
|
||||
& > div:last-child {
|
||||
top: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
& > div:last-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
:global {
|
||||
.semi-form-field {
|
||||
flex: 1;
|
||||
}
|
||||
.semi-tree-option-expand-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 24px;
|
||||
}
|
||||
}
|
||||
.level-icon {
|
||||
display: flex;
|
||||
align-self: stretch;
|
||||
}
|
||||
.wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-top: 10px;
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
.readonly-icon-container {
|
||||
margin-top: 10px;
|
||||
&.more-level {
|
||||
cursor: default;
|
||||
& > span,& > div {
|
||||
cursor: pointer;
|
||||
}
|
||||
&:hover, &:active {
|
||||
background: transparent;
|
||||
}
|
||||
}
|
||||
&:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
.readonly-container {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
.name {
|
||||
.font-normal();
|
||||
word-break: keep-all;
|
||||
}
|
||||
.tag {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 2px 8px;
|
||||
border-radius: 3px;
|
||||
background: rgba(230, 232, 234, 0.76);
|
||||
margin-left: 8px;
|
||||
.label {
|
||||
.font-normal();
|
||||
color: rgba(28, 31, 35, 0.60);
|
||||
font-weight: 400;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,222 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/* eslint-disable @coze-arch/max-line-per-function */
|
||||
import React, { useCallback, useRef } from 'react';
|
||||
|
||||
import isNumber from 'lodash-es/isNumber';
|
||||
import classNames from 'classnames';
|
||||
import { type RenderFullLabelProps } from '@coze-arch/bot-semi/Tree';
|
||||
|
||||
import { PARAM_TYPE_ALIAS_MAP, type ParamTypeAlias } from '../../types';
|
||||
import useConfig from '../../hooks/use-config';
|
||||
import NodeContext from '../../context/node-context';
|
||||
import { type TreeNodeCustomData, type ActiveMultiInfo } from './type';
|
||||
import { ChangeMode, ObjectLikeTypes, DescriptionLine } from './constants';
|
||||
import ParamType from './components/param-type';
|
||||
import ParamOperator from './components/param-operator';
|
||||
import ParamName from './components/param-name';
|
||||
import ParamDescription from './components/param-description';
|
||||
import LevelLine from './components/line-component';
|
||||
|
||||
import styles from './index.module.less';
|
||||
|
||||
export interface CustomTreeNodeProps extends RenderFullLabelProps {
|
||||
onChange: (mode: ChangeMode, param: TreeNodeCustomData) => void;
|
||||
// Description 组件变换为多行时,其下面第一个 child 需被记录
|
||||
onActiveMultiInfoChange?: (info: ActiveMultiInfo) => void;
|
||||
activeMultiInfo?: ActiveMultiInfo;
|
||||
// 不支持使用的类型
|
||||
disabledTypes?: ParamTypeAlias[];
|
||||
}
|
||||
|
||||
const LEVEL_LINE_STEP_WIDTH = 15;
|
||||
|
||||
export default function CustomTreeNode(props: CustomTreeNodeProps) {
|
||||
const {
|
||||
data,
|
||||
onExpand,
|
||||
expandIcon,
|
||||
className,
|
||||
level,
|
||||
onChange,
|
||||
onActiveMultiInfoChange,
|
||||
activeMultiInfo,
|
||||
disabledTypes = [],
|
||||
} = props;
|
||||
const { allowValueEmpty, readonly, hasObjectLike, withDescription } =
|
||||
useConfig();
|
||||
// 当前值
|
||||
const value = data as TreeNodeCustomData;
|
||||
const isTopLevel = level === 0;
|
||||
const isOnlyOneData = value.isSingle && isTopLevel;
|
||||
const IndentationWidth = level * LEVEL_LINE_STEP_WIDTH;
|
||||
const paramNameWidth = 181;
|
||||
|
||||
const treeNodeRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const disableDelete = Boolean(
|
||||
!allowValueEmpty && isOnlyOneData && isTopLevel,
|
||||
);
|
||||
// 删除时
|
||||
const onDelete = () => {
|
||||
onChange(ChangeMode.Delete, value);
|
||||
};
|
||||
// 新增子项时
|
||||
const onAppend = () => {
|
||||
onChange(ChangeMode.Append, value);
|
||||
};
|
||||
// 类型切换时
|
||||
const onSelectChange = (
|
||||
val?: string | number | Array<unknown> | Record<string, unknown>,
|
||||
) => {
|
||||
if (val === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isNumber(val)) {
|
||||
const isObjectLike = ObjectLikeTypes.includes(val);
|
||||
if (!isObjectLike) {
|
||||
// 如果不是类Object,判断是否有children,如果有,删除掉
|
||||
if (value.children && value.children.length > 0) {
|
||||
delete value.children;
|
||||
}
|
||||
}
|
||||
|
||||
onChange(ChangeMode.Update, { ...value, type: val });
|
||||
}
|
||||
|
||||
// 更新type
|
||||
};
|
||||
// 更新
|
||||
const onNameChange = (name: string) => {
|
||||
onChange(ChangeMode.Update, { ...value, name });
|
||||
};
|
||||
|
||||
// 更新
|
||||
const onDescriptionChange = useCallback(
|
||||
(description: string) => {
|
||||
onChange(ChangeMode.Update, { ...value, description });
|
||||
},
|
||||
[onChange, value],
|
||||
);
|
||||
|
||||
/**
|
||||
* Description 组件单行 / 多行变换时,其下面第一个 child 的竖线需要缩短 / 延长
|
||||
*/
|
||||
const onDescriptionLineChange = useCallback(
|
||||
(type: DescriptionLine) => {
|
||||
const errorDoms = treeNodeRef.current?.getElementsByClassName(
|
||||
'output-param-name-error-text',
|
||||
);
|
||||
|
||||
if (type === DescriptionLine.Multi && value.children?.[0]?.field) {
|
||||
onActiveMultiInfoChange?.({
|
||||
activeMultiKey: value.children[0].field,
|
||||
withNameError: Boolean(errorDoms?.length || 0),
|
||||
// withNameError: Boolean(nameError || ''),
|
||||
});
|
||||
} else {
|
||||
onActiveMultiInfoChange?.({
|
||||
activeMultiKey: '',
|
||||
});
|
||||
}
|
||||
},
|
||||
[onActiveMultiInfoChange, value],
|
||||
);
|
||||
|
||||
if (readonly) {
|
||||
return (
|
||||
// 提高class的css 权重
|
||||
<div
|
||||
className={classNames(
|
||||
styles['readonly-icon-container'],
|
||||
styles['more-level'],
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{expandIcon}
|
||||
<div className={styles['readonly-container']} onClick={onExpand}>
|
||||
<span className={styles.name}>{value.name || '-'}</span>
|
||||
<div className={styles.tag}>
|
||||
<span className={styles.label}>
|
||||
{PARAM_TYPE_ALIAS_MAP[value.type] || '-'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<NodeContext.Provider value={{ field: data.field }}>
|
||||
<div
|
||||
className={classNames({
|
||||
[styles.container]: true,
|
||||
[className]: Boolean(className),
|
||||
})}
|
||||
ref={treeNodeRef}
|
||||
>
|
||||
{/* 每增加一级多15长度 */}
|
||||
<div
|
||||
style={{ width: IndentationWidth }}
|
||||
className={styles['level-icon']}
|
||||
>
|
||||
<LevelLine
|
||||
level={level}
|
||||
data={value}
|
||||
multiInfo={{
|
||||
multiline: activeMultiInfo?.activeMultiKey === value.field,
|
||||
withNameError: activeMultiInfo?.withNameError,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.wrapper}>
|
||||
<ParamName
|
||||
style={{ width: paramNameWidth - IndentationWidth }}
|
||||
data={value}
|
||||
onChange={onNameChange}
|
||||
/>
|
||||
<ParamType
|
||||
data={value}
|
||||
onSelectChange={onSelectChange}
|
||||
level={level}
|
||||
disabledTypes={disabledTypes}
|
||||
/>
|
||||
{/* LLM 节点输出才有 description */}
|
||||
{withDescription ? (
|
||||
<ParamDescription
|
||||
data={value}
|
||||
onChange={onDescriptionChange}
|
||||
onLineChange={onDescriptionLineChange}
|
||||
hasObjectLike={hasObjectLike}
|
||||
/>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
<ParamOperator
|
||||
data={value}
|
||||
level={level}
|
||||
onDelete={onDelete}
|
||||
onAppend={onAppend}
|
||||
disableDelete={disableDelete}
|
||||
hasObjectLike={hasObjectLike}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</NodeContext.Provider>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { type TreeNodeData } from '@coze-arch/bot-semi/Tree';
|
||||
|
||||
import { type RecursedParamDefinition } from '../../types';
|
||||
import { type ChangeMode } from './constants';
|
||||
|
||||
export type TreeNodeCustomData = TreeNodeData &
|
||||
Pick<
|
||||
RecursedParamDefinition,
|
||||
| 'name'
|
||||
| 'type'
|
||||
| 'isQuote'
|
||||
| 'fixedValue'
|
||||
| 'quotedValue'
|
||||
| 'fieldRandomKey'
|
||||
> & {
|
||||
// 行唯一值
|
||||
key: string;
|
||||
// Form的field
|
||||
field?: string;
|
||||
// 是否是第一项
|
||||
isFirst?: boolean;
|
||||
// 是否是最后一项
|
||||
isLast?: boolean;
|
||||
// 是否只有该项一条数据
|
||||
isSingle?: boolean;
|
||||
// 该项的嵌套层级,从0开始
|
||||
level?: number;
|
||||
// 辅助线展示的字段
|
||||
helpLineShow?: Array<boolean>;
|
||||
children?: Array<TreeNodeCustomData>;
|
||||
// 变量描述,用于作为隐藏的引导
|
||||
description?: string;
|
||||
};
|
||||
|
||||
export interface CustomTreeNodeFuncRef {
|
||||
data: TreeNodeCustomData;
|
||||
level: number;
|
||||
readonly: boolean;
|
||||
// 通用change方法
|
||||
onChange: (mode: ChangeMode, param: TreeNodeCustomData) => void;
|
||||
// 定制的类型改变的change方法,主要用于自定义render使用
|
||||
// 添加子项
|
||||
onAppend: () => void;
|
||||
// 删除该项
|
||||
onDelete: () => void;
|
||||
// 删除该项下面的所有子项
|
||||
onDeleteChildren: () => void;
|
||||
// 类型改变时内部的调用方法,主要用于从类Object类型转为其他类型时需要删除所有子项
|
||||
onSelectChange: (
|
||||
val?: string | number | Array<unknown> | Record<string, unknown>,
|
||||
) => void;
|
||||
}
|
||||
|
||||
export interface ActiveMultiInfo {
|
||||
// 当前行是否处于多行状态,多行状态竖线需要延长
|
||||
activeMultiKey: string;
|
||||
// 当前行paramName数据是否出现错误信息
|
||||
withNameError?: boolean;
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.name {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.type {
|
||||
margin-left: 8px;
|
||||
width: 155px;
|
||||
flex-grow: 0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.description {
|
||||
margin-left: 8px;
|
||||
width: 312px;
|
||||
}
|
||||
|
||||
&.withDescription {
|
||||
.name {
|
||||
flex: auto;
|
||||
width: 181px;
|
||||
flex-grow: 0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.text {
|
||||
color: var(--light-usage-text-color-text-3, rgb(28 31 35 / 35%));
|
||||
font-size: 10px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 16px;
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import cx from 'classnames';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
|
||||
import useConfig from '../../hooks/use-config';
|
||||
import {
|
||||
OperatorLargeSize,
|
||||
OperatorSmallSize,
|
||||
SpacingSize,
|
||||
OperatorTypeBaseWidth,
|
||||
} from '../../constants';
|
||||
|
||||
import styles from './index.module.less';
|
||||
|
||||
export default function Header() {
|
||||
const { readonly, withDescription, hasObjectLike } = useConfig();
|
||||
|
||||
if (readonly) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cx(styles.header, {
|
||||
[styles.withDescription]: withDescription,
|
||||
})}
|
||||
>
|
||||
{/* name */}
|
||||
<div className={styles.name}>
|
||||
<span className={styles.text}>
|
||||
{I18n.t('workflow_detail_end_output_name')}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* type */}
|
||||
<div
|
||||
className={styles.type}
|
||||
style={
|
||||
withDescription
|
||||
? {
|
||||
width: OperatorTypeBaseWidth,
|
||||
}
|
||||
: !hasObjectLike
|
||||
? { width: OperatorSmallSize + SpacingSize + OperatorTypeBaseWidth }
|
||||
: { width: OperatorLargeSize + SpacingSize + OperatorTypeBaseWidth }
|
||||
}
|
||||
>
|
||||
<span className={styles.text}>
|
||||
{I18n.t('workflow_detail_start_variable_type')}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* description 目前只在 LLM 的 output 中存在 */}
|
||||
{withDescription ? (
|
||||
<div className={styles.description}>
|
||||
<span className={styles.text}>
|
||||
{I18n.t('workflow_detail_llm_output_decription_title')}
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/* eslint-disable @typescript-eslint/naming-convention */
|
||||
// 类型选择控件基础宽度
|
||||
export const OperatorTypeBaseWidth = 155;
|
||||
|
||||
// 61 = 删除按钮 + 添加按钮 的上层容器宽度
|
||||
export const OperatorLargeSize = 61;
|
||||
// 31 = 删除按钮 的上层容器宽度
|
||||
export const OperatorSmallSize = 31;
|
||||
// 8 = 删除按钮与变量类型中间的 margin 距离
|
||||
export const SpacingSize = 8;
|
||||
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/* eslint-disable @typescript-eslint/naming-convention */
|
||||
import { createContext } from 'react';
|
||||
|
||||
import type { ParametersProps } from '../types';
|
||||
|
||||
export type Configs = Omit<
|
||||
ParametersProps,
|
||||
'value' | 'onChange' | 'className' | 'style' | 'disabledTypes'
|
||||
> & { hasObjectLike?: boolean };
|
||||
|
||||
const ConfigContext = createContext<Configs>({});
|
||||
|
||||
export default ConfigContext;
|
||||
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/* eslint-disable @typescript-eslint/naming-convention */
|
||||
import { createContext } from 'react';
|
||||
|
||||
export interface Node {
|
||||
field?: string;
|
||||
}
|
||||
|
||||
const NodeContext = createContext<Node>({});
|
||||
|
||||
export default NodeContext;
|
||||
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { useContext } from 'react';
|
||||
|
||||
import ConfigContext from '../context/config-context';
|
||||
import type { Configs } from '../context/config-context';
|
||||
|
||||
export default function useConfig(): Configs {
|
||||
const config = useContext(ConfigContext);
|
||||
return config;
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import resolvePath from '../utils/resolve-path';
|
||||
import useNode from './use-node';
|
||||
import useParametersConfig from './use-config';
|
||||
|
||||
export default function useErrorMessage(key: string): string {
|
||||
const { errors = [] } = useParametersConfig();
|
||||
const { field = '' } = useNode();
|
||||
const pathSearched = resolvePath(field, key);
|
||||
|
||||
const error = errors.find(({ path }) => pathSearched === path);
|
||||
|
||||
return error?.message || '';
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { useContext } from 'react';
|
||||
|
||||
import NodeContext, { type Node } from '../context/node-context';
|
||||
|
||||
export default function useNode(): Node {
|
||||
const node = useContext(NodeContext);
|
||||
return node;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { Parameters } from './parameters';
|
||||
export { ParamTypeAlias } from './types';
|
||||
export type { ParameterValue, ParametersError, ParametersProps } from './types';
|
||||
@@ -0,0 +1,46 @@
|
||||
.container {
|
||||
position: relative;
|
||||
|
||||
.content {
|
||||
overflow-x: auto;
|
||||
|
||||
&.readonly {
|
||||
max-height: 300px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.add-hot-area {
|
||||
height: 16px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
:global {
|
||||
.semi-tree-option-list {
|
||||
overflow: initial;
|
||||
|
||||
&>div:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:global {
|
||||
.semi-tree-option-list {
|
||||
overflow: inherit;
|
||||
|
||||
.semi-tree-option {
|
||||
padding-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.semi-tree-option-list-block .semi-tree-option {
|
||||
cursor: default;
|
||||
|
||||
&:hover,
|
||||
&:active {
|
||||
background: transparent;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,202 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/* eslint-disable @coze-arch/max-line-per-function */
|
||||
import React, { type PropsWithChildren, useState } from 'react';
|
||||
|
||||
import { nanoid } from 'nanoid';
|
||||
import { cloneDeep } from 'lodash-es';
|
||||
import classNames from 'classnames';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { type TreeNodeData } from '@coze-arch/bot-semi/Tree';
|
||||
import { Toast, Tree } from '@coze-arch/bot-semi';
|
||||
|
||||
import { findCustomTreeNodeDataResult, formatTreeData } from './utils/utils';
|
||||
import { traverse } from './utils/traverse';
|
||||
import { ParamTypeAlias } from './types';
|
||||
import type { ParametersProps } from './types';
|
||||
import ConfigContext from './context/config-context';
|
||||
import Header from './components/header';
|
||||
import {
|
||||
type TreeNodeCustomData,
|
||||
type ActiveMultiInfo,
|
||||
} from './components/custom-tree-node/type';
|
||||
import { ChangeMode } from './components/custom-tree-node/constants';
|
||||
import CustomTreeNode from './components/custom-tree-node';
|
||||
|
||||
import styles from './parameters.module.less';
|
||||
|
||||
const getDefaultAppendValue = () => ({
|
||||
fieldRandomKey: nanoid(),
|
||||
type: ParamTypeAlias.String,
|
||||
});
|
||||
|
||||
export function Parameters(props: PropsWithChildren<ParametersProps>) {
|
||||
const {
|
||||
value,
|
||||
readonly = false,
|
||||
withDescription = false,
|
||||
disabledTypes = [],
|
||||
className = '',
|
||||
style = {},
|
||||
errors = [],
|
||||
allowValueEmpty = true,
|
||||
onChange,
|
||||
} = props;
|
||||
// 监听该值的变化
|
||||
const isValueEmpty = !value || value.length === 0;
|
||||
const { data: formattedTreeData, hasObjectLike } = formatTreeData(
|
||||
cloneDeep(value) as TreeNodeCustomData[],
|
||||
);
|
||||
|
||||
/**
|
||||
* 表示当前哪一行的父亲节点的 description 处于多行状态(LLM节点)
|
||||
* 用于渲染树形竖线,处于多行文本的下一行竖线应该延长
|
||||
* 若 param name 有错误信息,竖线从错误信息下方延展,长度有所变化
|
||||
*/
|
||||
const [activeMultiInfo, setActiveMultiInfo] = useState<ActiveMultiInfo>({
|
||||
activeMultiKey: '',
|
||||
});
|
||||
|
||||
// 该组件的 change 方法
|
||||
const onValueChange = (freshValue?: Array<TreeNodeCustomData>) => {
|
||||
if (onChange) {
|
||||
freshValue = (freshValue || []).concat([]);
|
||||
// 清理掉无用字段
|
||||
traverse<TreeNodeCustomData>(freshValue, node => {
|
||||
const { key, name, type, description, children } = node;
|
||||
// eslint-disable-next-line guard-for-in
|
||||
for (const prop in node) {
|
||||
delete node[prop];
|
||||
}
|
||||
node.key = key;
|
||||
node.name = name;
|
||||
node.type = type;
|
||||
node.description = description;
|
||||
|
||||
if (children) {
|
||||
node.children = children;
|
||||
}
|
||||
});
|
||||
onChange(freshValue);
|
||||
}
|
||||
};
|
||||
|
||||
// 树节点的 change 方法
|
||||
const onTreeNodeChange = (mode: ChangeMode, param: TreeNodeCustomData) => {
|
||||
// 先clone一份,因为Tree内部会对treeData执行isEqual,克隆一份一定是false
|
||||
const cloneDeepTreeData = cloneDeep(
|
||||
formattedTreeData,
|
||||
) as Array<TreeNodeCustomData>;
|
||||
const findResult = findCustomTreeNodeDataResult(
|
||||
cloneDeepTreeData,
|
||||
param.field as string,
|
||||
);
|
||||
if (findResult) {
|
||||
switch (mode) {
|
||||
case ChangeMode.Append: {
|
||||
// 新增不可以用 parentData 做标准,要在当前 data 下新增
|
||||
const { data } = findResult;
|
||||
const currentChildren = data.children || [];
|
||||
// @ts-expect-error 有些值不需要此时指定,因为在 rerender 的时候会执行 format
|
||||
data.children = currentChildren.concat({
|
||||
...getDefaultAppendValue(),
|
||||
// 增加 field
|
||||
field: `${data.field}.children[${currentChildren.length}]`,
|
||||
});
|
||||
onValueChange(cloneDeepTreeData);
|
||||
break;
|
||||
}
|
||||
case ChangeMode.Update: {
|
||||
const targetArray = findResult.isRoot
|
||||
? cloneDeepTreeData
|
||||
: findResult.parentData?.children;
|
||||
const index = targetArray?.findIndex(item => item.key === param.key);
|
||||
|
||||
if (index !== undefined) {
|
||||
targetArray?.splice(index, 1, param);
|
||||
onValueChange(cloneDeepTreeData);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case ChangeMode.Delete: {
|
||||
if (findResult.isRoot) {
|
||||
const freshValue = (cloneDeepTreeData || []).filter(
|
||||
item => item.key !== param.key,
|
||||
);
|
||||
onValueChange(freshValue);
|
||||
} else {
|
||||
const parentData = findResult.parentData as TreeNodeData;
|
||||
parentData.children = (parentData.children || []).filter(
|
||||
item => item.key !== param.key,
|
||||
);
|
||||
onValueChange(cloneDeepTreeData);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case ChangeMode.DeleteChildren: {
|
||||
const { data } = findResult;
|
||||
data.children = [];
|
||||
onValueChange(cloneDeepTreeData);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
}
|
||||
} else {
|
||||
Toast.error(I18n.t('workflow_detail_node_output_parsingfailed'));
|
||||
}
|
||||
};
|
||||
|
||||
if (readonly && isValueEmpty) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<ConfigContext.Provider
|
||||
value={{
|
||||
errors,
|
||||
allowValueEmpty,
|
||||
withDescription,
|
||||
readonly,
|
||||
hasObjectLike,
|
||||
}}
|
||||
>
|
||||
<div className={`${styles.container} ${className}`} style={style}>
|
||||
<Header />
|
||||
<Tree
|
||||
expandAll={!readonly}
|
||||
style={readonly ? {} : { overflow: 'inherit' }}
|
||||
motion={false}
|
||||
className={classNames({
|
||||
[styles.content]: true,
|
||||
[styles.readonly]: readonly,
|
||||
[styles['content-fix-pop-container']]: !readonly,
|
||||
})}
|
||||
renderFullLabel={renderFullLabelProps => (
|
||||
<CustomTreeNode
|
||||
{...renderFullLabelProps}
|
||||
onChange={onTreeNodeChange}
|
||||
onActiveMultiInfoChange={setActiveMultiInfo}
|
||||
activeMultiInfo={activeMultiInfo}
|
||||
disabledTypes={disabledTypes}
|
||||
/>
|
||||
)}
|
||||
treeData={formattedTreeData}
|
||||
/>
|
||||
</div>
|
||||
</ConfigContext.Provider>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export enum ParamTypeAlias {
|
||||
String = 1,
|
||||
Integer,
|
||||
Boolean,
|
||||
Number,
|
||||
/** 理论上没有 List 了,此项仅作兼容 */
|
||||
List = 5,
|
||||
Object = 6,
|
||||
// 上面是 api 中定义的 InputType。下面是整合后的。从 99 开始,避免和后端定义撞车
|
||||
ArrayString = 99,
|
||||
ArrayInteger,
|
||||
ArrayBoolean,
|
||||
ArrayNumber,
|
||||
ArrayObject,
|
||||
}
|
||||
|
||||
export const PARAM_TYPE_ALIAS_MAP: Record<ParamTypeAlias, string> = {
|
||||
[ParamTypeAlias.String]: 'String',
|
||||
[ParamTypeAlias.Integer]: 'Integer',
|
||||
[ParamTypeAlias.Boolean]: 'Boolean',
|
||||
[ParamTypeAlias.Number]: 'Number',
|
||||
[ParamTypeAlias.List]: 'List',
|
||||
[ParamTypeAlias.Object]: 'Object',
|
||||
[ParamTypeAlias.ArrayString]: 'Array<String>',
|
||||
[ParamTypeAlias.ArrayInteger]: 'Array<Integer>',
|
||||
[ParamTypeAlias.ArrayBoolean]: 'Array<Boolean>',
|
||||
[ParamTypeAlias.ArrayNumber]: 'Array<Number>',
|
||||
[ParamTypeAlias.ArrayObject]: 'Array<Object>',
|
||||
};
|
||||
|
||||
export enum ParamValueType {
|
||||
QUOTE = 'quote',
|
||||
FIXED = 'fixed',
|
||||
}
|
||||
|
||||
export interface RecursedParamDefinition {
|
||||
name?: string;
|
||||
/** Tree 组件要求每一个节点都有 key,而 key 不适合用名称(前后缀)等任何方式赋值,最终确定由接口转换层一次性提供随机 key */
|
||||
fieldRandomKey?: string;
|
||||
desc?: string;
|
||||
required?: boolean;
|
||||
type: ParamTypeAlias;
|
||||
children?: RecursedParamDefinition[];
|
||||
// region 参数值定义
|
||||
// 输入参数的值可以来自上游变量引用,也可以是用户输入的定值(复杂类型则只允许引用)
|
||||
// 如果是定值,传 fixedValue
|
||||
// 如果是引用,传 quotedValue
|
||||
isQuote?: ParamValueType;
|
||||
/** 参数定值 */
|
||||
fixedValue?: string;
|
||||
/** 参数引用 */
|
||||
quotedValue?: [nodeId: string, ...path: string[]]; // string[]
|
||||
// endregion
|
||||
}
|
||||
|
||||
export interface ParameterValue {
|
||||
key: string;
|
||||
name?: string;
|
||||
type: ParamTypeAlias;
|
||||
description?: string;
|
||||
children?: ParameterValue[];
|
||||
}
|
||||
|
||||
export interface ParametersError {
|
||||
path: string;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface ParametersProps {
|
||||
value: Array<ParameterValue>;
|
||||
onChange?: (value: Array<ParameterValue>) => void;
|
||||
readonly?: boolean;
|
||||
className?: string;
|
||||
style?: React.CSSProperties;
|
||||
withDescription?: boolean;
|
||||
// 不支持使用的类型
|
||||
disabledTypes?: ParamTypeAlias[];
|
||||
errors?: ParametersError[];
|
||||
// 支持空值 & 空数组
|
||||
allowValueEmpty?: boolean;
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { isFunction } from 'lodash-es';
|
||||
|
||||
/**
|
||||
* 将 { value: label } 形式的结构体转成Select需要的options Array<{ label, value }>
|
||||
* computedValue:将value值转化一次作为options的value
|
||||
* passItem:判断当前value值是否需要跳过遍历
|
||||
*/
|
||||
export default function convertMaptoOptions<Value = number>(
|
||||
map: Record<string, unknown>,
|
||||
convertOptions: {
|
||||
computedValue?: (val: unknown) => Value;
|
||||
passItem?: (val: unknown) => boolean;
|
||||
/**
|
||||
* 由于 i18n 的实现方式问题,写成常量的文案需要惰性加载
|
||||
* 因此涉及到 i18n 的 { value: label } 结构一律需要写成 { value: () => label }
|
||||
* 该属性启用时,会额外进行一次惰性加载
|
||||
* @default false
|
||||
* @link
|
||||
*/
|
||||
i18n?: boolean;
|
||||
} = {},
|
||||
) {
|
||||
const res: Array<{ label: string; value: Value }> = [];
|
||||
for (const [value, label] of Object.entries(map)) {
|
||||
const pass = convertOptions.passItem
|
||||
? convertOptions.passItem(value)
|
||||
: false;
|
||||
if (pass) {
|
||||
continue;
|
||||
}
|
||||
const computedValue = convertOptions.computedValue
|
||||
? convertOptions.computedValue(value)
|
||||
: (value as Value);
|
||||
|
||||
const finalLabel: string = convertOptions.i18n
|
||||
? isFunction(label)
|
||||
? label()
|
||||
: label
|
||||
: label;
|
||||
res.push({ label: finalLabel, value: computedValue });
|
||||
}
|
||||
return res;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export default function resolvePath(path1: string, path2: string): string {
|
||||
if (path1 && path2) {
|
||||
return `${path1}.${path2}`;
|
||||
}
|
||||
|
||||
return path2 || '';
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
interface TreeNode<T> {
|
||||
children?: T[];
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export function traverse<T extends TreeNode<T>>(
|
||||
nodeOrNodes: T | T[],
|
||||
action: (node: T) => void,
|
||||
) {
|
||||
const nodes = Array.isArray(nodeOrNodes) ? nodeOrNodes : [nodeOrNodes];
|
||||
|
||||
nodes.forEach(node => {
|
||||
action(node);
|
||||
|
||||
if (node.children && node.children.length > 0) {
|
||||
traverse(node.children, action);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { nanoid } from 'nanoid';
|
||||
|
||||
import { type TreeNodeCustomData } from '../components/custom-tree-node/type';
|
||||
import { ObjectLikeTypes } from '../components/custom-tree-node/constants';
|
||||
|
||||
interface RootFindResult {
|
||||
isRoot: true;
|
||||
data: TreeNodeCustomData;
|
||||
parentData: null;
|
||||
}
|
||||
interface ChildrenFindResult {
|
||||
isRoot: false;
|
||||
parentData: TreeNodeCustomData;
|
||||
data: TreeNodeCustomData;
|
||||
}
|
||||
|
||||
export type FindDataResult = RootFindResult | ChildrenFindResult | null;
|
||||
/**
|
||||
* 根据target数组,找到key在该项的值和位置,主要是获取位置,方便操作parent的children
|
||||
*/
|
||||
export function findCustomTreeNodeDataResult(
|
||||
target: Array<TreeNodeCustomData>,
|
||||
findField: string,
|
||||
): FindDataResult {
|
||||
const dataInRoot = target.find(item => item.field === findField);
|
||||
if (dataInRoot) {
|
||||
// 如果是根节点
|
||||
return {
|
||||
isRoot: true,
|
||||
parentData: null,
|
||||
data: dataInRoot,
|
||||
};
|
||||
}
|
||||
function findDataInChildrenLoop(
|
||||
customChildren: Array<TreeNodeCustomData>,
|
||||
parentData?: TreeNodeCustomData,
|
||||
): FindDataResult {
|
||||
function findDataLoop(
|
||||
customData: TreeNodeCustomData,
|
||||
_parentData: TreeNodeCustomData,
|
||||
): FindDataResult {
|
||||
if (customData.field === findField) {
|
||||
return {
|
||||
isRoot: false,
|
||||
parentData: _parentData,
|
||||
data: customData,
|
||||
};
|
||||
}
|
||||
if (customData.children && customData.children.length > 0) {
|
||||
return findDataInChildrenLoop(
|
||||
customData.children as Array<TreeNodeCustomData>,
|
||||
customData,
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
for (const child of customChildren) {
|
||||
const childResult = findDataLoop(child, parentData || child);
|
||||
if (childResult) {
|
||||
return childResult;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
return findDataInChildrenLoop(target);
|
||||
}
|
||||
|
||||
const MAX_LINE_LEVEL = 2;
|
||||
export function formatTreeData(data: Array<TreeNodeCustomData>) {
|
||||
let hasObjectLike = false;
|
||||
function resolveActionParamList(
|
||||
list: Array<TreeNodeCustomData>,
|
||||
field: string,
|
||||
// 主要是用来辅助展示线的判断的
|
||||
{
|
||||
parentData,
|
||||
level,
|
||||
}: {
|
||||
parentData?: TreeNodeCustomData;
|
||||
level: number;
|
||||
},
|
||||
) {
|
||||
list?.forEach((item, index) => {
|
||||
const keyField = field ? `${field}.${index}` : `${index}`;
|
||||
hasObjectLike = hasObjectLike || ObjectLikeTypes.includes(item.type);
|
||||
// 赋值children
|
||||
item.key = item.key ?? item.fieldRandomKey ?? nanoid();
|
||||
item.field = keyField;
|
||||
item.isFirst = index === 0;
|
||||
item.isLast = index === list.length - 1;
|
||||
item.isSingle = item.isFirst && item.isLast;
|
||||
item.level = level;
|
||||
// 第一级不展示辅助线,需要判断level
|
||||
// 也就是第二级(level = 1)只需要自身的层级线
|
||||
// 在第三级(level = 2)之后需要辅助线展示上一级的辅助线
|
||||
item.helpLineShow =
|
||||
parentData && level >= MAX_LINE_LEVEL
|
||||
? (parentData.helpLineShow || []).concat(!parentData.isLast)
|
||||
: [];
|
||||
if (item.children) {
|
||||
resolveActionParamList(
|
||||
item.children as Array<TreeNodeCustomData>,
|
||||
`${keyField}.children`,
|
||||
{
|
||||
parentData: item,
|
||||
level: level + 1,
|
||||
},
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
resolveActionParamList(data as TreeNodeCustomData[], '', { level: 0 });
|
||||
|
||||
return { data, hasObjectLike };
|
||||
}
|
||||
|
||||
export enum LineShowResult {
|
||||
HalfTopRoot,
|
||||
HalfTopRootWithChildren,
|
||||
HalfBottomRoot,
|
||||
HalfBottomRootWithChildren,
|
||||
FullRoot,
|
||||
FullRootWithChildren,
|
||||
HalfTopChild,
|
||||
HalfTopChildWithChildren,
|
||||
FullChild,
|
||||
FullChildWithChildren,
|
||||
EmptyBlock,
|
||||
HelpLineBlock,
|
||||
}
|
||||
|
||||
// eslint-disable-next-line complexity
|
||||
export function getLineShowResult({
|
||||
level,
|
||||
data,
|
||||
}: {
|
||||
level: number;
|
||||
data: TreeNodeCustomData;
|
||||
}): Array<LineShowResult> {
|
||||
const isRootWithChildren = level === 0 && (data.children || []).length > 0;
|
||||
const isRootWithoutChildren =
|
||||
level === 0 && (data.children || []).length === 0;
|
||||
const isChildWithChildren = level > 0 && (data.children || []).length > 0;
|
||||
const isChildWithoutChildren =
|
||||
level > 0 && (data.children || []).length === 0;
|
||||
const res: Array<LineShowResult> =
|
||||
data.helpLineShow?.map(item =>
|
||||
item ? LineShowResult.HelpLineBlock : LineShowResult.EmptyBlock,
|
||||
) || [];
|
||||
const isRoot = isRootWithoutChildren || isRootWithChildren;
|
||||
// 根节点不需要展示线,只有非根节点才需要辅助线
|
||||
if (!isRoot) {
|
||||
if (isChildWithChildren) {
|
||||
if (data.isLast) {
|
||||
res.push(LineShowResult.HalfTopChildWithChildren);
|
||||
} else if (data.isFirst) {
|
||||
res.push(LineShowResult.FullChildWithChildren);
|
||||
} else {
|
||||
res.push(LineShowResult.FullChildWithChildren);
|
||||
}
|
||||
} else if (isChildWithoutChildren) {
|
||||
if (data.isLast) {
|
||||
res.push(LineShowResult.HalfTopChild);
|
||||
} else if (data.isFirst) {
|
||||
res.push(LineShowResult.FullChild);
|
||||
} else {
|
||||
res.push(LineShowResult.FullChild);
|
||||
}
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
Reference in New Issue
Block a user