feat: manually mirror opencoze's code from bytedance

Change-Id: I09a73aadda978ad9511264a756b2ce51f5761adf
This commit is contained in:
fanlv
2025-07-20 17:36:12 +08:00
commit 890153324f
14811 changed files with 1923430 additions and 0 deletions

View File

@@ -0,0 +1,17 @@
/*
* 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 { FieldItem, type FieldItemProps } from './item';

View File

@@ -0,0 +1,46 @@
.field-item {
margin-bottom: 12px;
}
.item-title {
position: relative;
margin-bottom: 4px;
font-size: 12px;
line-height: 16px;
overflow-wrap: break-word;
.item-label {
display: flex;
align-items: center;
margin-bottom: 2px;
}
.item-tag {
flex-shrink: 0;
margin-left: 4px;
}
.tooltip-icon {
margin-left: 4px;
color: var(--coz-fg-dim);
}
}
.title-text {
color: var(--coz-fg-primary);
}
.title-required {
color: var(--coz-fg-hglt-red);
}
.item-description {
color: var(--coz-fg-secondary);
}
.item-feedback {
margin-top: 2px;
font-size: 12px;
line-height: 16px;
color: var(--coz-fg-hglt-red);
overflow-wrap: break-word;
}

View File

@@ -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 from 'react';
import { IconCozInfoCircle } from '@coze-arch/coze-design/icons';
import { Tag, Tooltip, Typography } from '@coze-arch/coze-design';
import css from './item.module.less';
export interface FieldItemProps {
title?: React.ReactNode;
description?: React.ReactNode;
tag?: React.ReactNode;
tooltip?: React.ReactNode;
feedback?: string;
required?: boolean;
['data-testid']?: string;
}
export const FieldItem: React.FC<React.PropsWithChildren<FieldItemProps>> = ({
title,
required,
tooltip,
tag,
description,
children,
feedback,
...props
}) => (
<div className={css['field-item']} data-testid={props['data-testid']}>
{/* title */}
<div className={css['item-title']}>
<div className={css['item-label']}>
<Typography.Text className={css['title-text']} strong size="small">
{title}
</Typography.Text>
{required ? (
<Typography.Text className={css['title-required']}>*</Typography.Text>
) : null}
{tooltip ? (
<Tooltip content={tooltip}>
<IconCozInfoCircle className={css['tooltip-icon']} />
</Tooltip>
) : null}
{tag ? (
<Tag className={css['item-tag']} size="mini" color="primary">
{tag}
</Tag>
) : null}
</div>
{description ? (
<Typography.Text
ellipsis={{
showTooltip: {
opts: {
position: 'left',
style: {
maxWidth: 500,
},
},
},
}}
className={css['item-description']}
size="small"
>
{description}
</Typography.Text>
) : null}
</div>
{/* children */}
<div>{children}</div>
{/* feedback */}
{feedback ? <div className={css['item-feedback']}>{feedback}</div> : null}
</div>
);

View File

@@ -0,0 +1,53 @@
.collapse-title {
display: flex;
align-items: center;
font-size: 14px;
font-weight: 500;
height: 40px;
position: sticky;
top: 0;
background: var(--coz-bg-plus);
border-bottom: 1px solid var(--coz-bg-plus);
z-index: 1;
cursor: pointer;
&:hover .collapse-icon{
opacity: 1;
}
&.is-sticky {
border-color: var(--coz-stroke-primary);
}
}
.collapse-label {
font-size: 14px;
font-weight: 500;
height: 20px;
color: var(--coz-fg-primary);
}
.collapse-label-tooltip {
font-size: 14px;
margin-left: 4px;
color: var(--coz-fg-dim);
}
.collapse-icon {
transition: transform 0.3s ease-in-out, opacity 0.2s ease-in-out;
opacity: 0;
color: var(--coz-fg-dim);
font-size: 14px;
margin: 0 1px;
&.is-show {
opacity: 1;
}
&.is-close {
transform: rotate(-90deg);
}
}
.collapse-content {
padding: 0 16px;
}
.collapse-extra {
width: 0;
flex-grow: 1;
padding-right: 16px;
}

View File

@@ -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, { useState, useRef } from 'react';
import { clsx } from 'clsx';
import { useInViewport } from 'ahooks';
import {
IconCozArrowDownFill,
IconCozInfoCircle,
} from '@coze-arch/coze-design/icons';
import { Collapsible, Tooltip } from '@coze-arch/coze-design';
import css from './collapse.module.less';
interface CollapseProps {
label: React.ReactNode;
tooltip?: React.ReactNode;
extra?: React.ReactNode;
fade?: boolean;
duration?: number;
}
export const GroupCollapse: React.FC<
React.PropsWithChildren<CollapseProps>
> = ({ label, tooltip, extra, children }) => {
const [isOpen, setIsOpen] = useState(true);
const ref = useRef(null);
/**
* 探测标题是否处于 sticky 状态
*/
const [inViewport] = useInViewport(ref);
return (
<div>
{/* 探测元素 */}
<div ref={ref} />
{/* header */}
<div
onClick={() => setIsOpen(!isOpen)}
className={clsx(
css['collapse-title'],
(!inViewport || !isOpen) && css['is-sticky'],
)}
>
<IconCozArrowDownFill
className={clsx(css['collapse-icon'], !isOpen && css['is-close'])}
/>
<span className={css['collapse-label']}>{label}</span>
{tooltip ? (
<Tooltip content={tooltip}>
<IconCozInfoCircle className={css['collapse-label-tooltip']} />
</Tooltip>
) : null}
{extra ? (
<div
className={css['collapse-extra']}
onClick={e => e.stopPropagation()}
>
{extra}
</div>
) : null}
</div>
{/* children */}
<Collapsible isOpen={isOpen} keepDOM fade duration={300}>
<div className={css['collapse-content']}>{children}</div>
</Collapsible>
</div>
);
};

View File

@@ -0,0 +1,17 @@
/*
* 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 { GroupCollapse } from './collapse';

View File

@@ -0,0 +1,27 @@
/*
* 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 { InputNumber, InputNumberProps } from './input-number';
export { InputString, type InputStringProps } from './input-string';
export { InputTime, type InputTimeProps } from './input-time';
export { InputJson, type InputJsonProps } from './input-json';
export { SelectBoolean } from './select-boolean';
export { SelectVoice } from './select-voice';
export { FieldItem, type FieldItemProps } from './field-item';
export { GroupCollapse } from './group-collapse';

View File

@@ -0,0 +1,17 @@
/*
* 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 { InputJson, type InputJsonProps } from './json';

View File

@@ -0,0 +1,33 @@
.input-json-wrap {
border: 1px solid var(--coz-stroke-plus);
border-radius: 8px;
&.disabled {
background-color: rgba(var(--coze-bg-5), var(--coze-bg-5-alpha));
cursor: not-allowed;
}
&.error {
border: 1px solid var(--coz-stroke-hglt-red);
}
}
.json-header {
display: flex;
align-items: center;
justify-content: space-between;
height: 28px;
padding: 0 4px;
border-bottom: 1px solid var(--coz-stroke-plus);
border-top-left-radius: 8px;
border-top-right-radius: 8px;
.json-label {
font-size: 12px;
color: var(--coz-fg-secondary);
font-weight: 500;
}
}
.json-editor {
overflow: hidden;
border-bottom-left-radius: 8px;
}

View File

@@ -0,0 +1,88 @@
/*
* 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 { clsx } from 'clsx';
import {
JsonEditor,
safeFormatJsonString,
} from '@coze-workflow/test-run-shared';
import { I18n } from '@coze-arch/i18n';
import { IconCozBroom } from '@coze-arch/coze-design/icons';
import { Tooltip, IconButton } from '@coze-arch/coze-design';
import css from './json.module.less';
export interface InputJsonProps {
value?: string;
disabled?: boolean;
extensions?: any;
jsonSchema?: any;
height?: string;
validateStatus?: 'error';
['data-testid']?: string;
onChange?: (v?: string) => void;
didMount?: (editor: any) => void;
}
export const InputJson: React.FC<InputJsonProps> = ({
value,
disabled,
validateStatus,
onChange,
...props
}) => {
const handleFormat = () => {
const next = safeFormatJsonString(value);
if (next !== value) {
onChange?.(next);
}
};
return (
<div
className={clsx(
css['input-json-wrap'],
disabled && css.disabled,
validateStatus === 'error' && css.error,
)}
data-testid={props['data-testid']}
>
<div className={css['json-header']}>
<div className={css['json-label']}>JSON</div>
<div>
<Tooltip content={I18n.t('workflow_exception_ignore_format')}>
<IconButton
icon={<IconCozBroom />}
disabled={disabled}
size="small"
color="secondary"
onMouseDown={e => e.preventDefault()}
onClick={handleFormat}
/>
</Tooltip>
</div>
</div>
<div className={css['json-editor']}>
<JsonEditor
value={value}
disabled={disabled}
onChange={onChange}
{...props}
/>
</div>
</div>
);
};

View File

@@ -0,0 +1,17 @@
/*
* 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 { InputNumber, type InputNumberProps } from './input-number';

View File

@@ -0,0 +1,27 @@
.buttons {
display: flex;
flex-direction: column;
width: 20px;
position: relative;
right: -7px;
}
.button {
font-size: 10px;
color: var(--coz-fg-primary);
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
&:hover {
background-color: rgba(var(--coze-bg-5), var(--coze-bg-5-alpha));
}
&.up {
border-top-left-radius: var(--coze-5);
border-top-right-radius: var(--coze-5);
}
&.down {
border-bottom-left-radius: var(--coze-5);
border-bottom-right-radius: var(--coze-5);
}
}

View File

@@ -0,0 +1,147 @@
/*
* 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 { useRef, useEffect } from 'react';
import clsx from 'clsx';
import BigNumber, { type BigNumber as IBigNumber } from 'bignumber.js';
import {
IconCozArrowDownFill,
IconCozArrowUpFill,
} from '@coze-arch/coze-design/icons';
import { Input, type InputProps } from '@coze-arch/coze-design';
import css from './input-number.module.less';
export interface InputNumberProps {
value?: string;
style?: React.CSSProperties;
placeholder?: string;
validateStatus?: InputProps['validateStatus'];
disabled?: boolean;
onChange: (v?: string) => void;
onBlur: () => void;
onFocus: () => void;
/** 整型 */
int?: boolean;
}
/** 是否是合法的数字字符串 */
function isValidNumber(str: string) {
try {
const value = new BigNumber(str);
return !value.isNaN();
} catch {
return false;
}
}
function normalizeNumber(str?: string) {
if (!str || !isValidNumber(str)) {
return;
}
return new BigNumber(str);
}
export const InputNumber: React.FC<InputNumberProps> = ({
int,
onChange,
onBlur,
...props
}) => {
const verifiedRef = useRef<undefined | IBigNumber>(
normalizeNumber(props.value),
);
const fixed = (num: IBigNumber, innerInt?: boolean) =>
innerInt ? num.toFixed(0, BigNumber.ROUND_DOWN) : num.toFixed();
const handleBlur = () => {
if (props.value === '' || props.value === undefined) {
/** 失焦时若值为空,则同时清空验证值 */
verifiedRef.current = undefined;
if (props.value === '') {
onChange(undefined);
}
} else {
/** 失焦时若值不为空,则需要验证值的合法性 */
/**
* 1. 若值本身合法,则对值做格式化
* 2. 若值不合法,则采纳最近一次的合法值
* 3. 若都没有,则返回 undefined
*/
let next: undefined | string;
const nextBig = normalizeNumber(props.value) || verifiedRef.current;
if (nextBig) {
next = fixed(nextBig, int);
}
if (next !== props.value) {
onChange(next);
}
}
onBlur();
};
const handlePlus = () => {
let next = '1';
if (verifiedRef.current) {
const nextNum = verifiedRef.current.plus('1');
next = fixed(nextNum, int);
}
onChange(next);
};
const handleMinus = () => {
let next = '0';
if (verifiedRef.current) {
const nextNum = verifiedRef.current.minus('1');
next = fixed(nextNum, int);
}
onChange(next);
};
/** 当值发生变化,需要把值同步到合法数字 */
useEffect(() => {
if (props.value === '' || props.value === undefined) {
verifiedRef.current = undefined;
}
const next = normalizeNumber(props.value);
if (next) {
verifiedRef.current = normalizeNumber(props.value);
}
}, [props.value]);
return (
<Input
onChange={onChange}
onBlur={handleBlur}
size="small"
suffix={
props.disabled ? null : (
<div className={css.buttons}>
<div className={clsx(css.button, css.up)} onClick={handlePlus}>
<IconCozArrowUpFill />
</div>
<div className={clsx(css.button, css.down)} onClick={handleMinus}>
<IconCozArrowDownFill />
</div>
</div>
)
}
{...props}
/>
);
};

View File

@@ -0,0 +1,17 @@
/*
* 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 { InputString, type InputStringProps } from './input';

View File

@@ -0,0 +1,14 @@
// semi 和 coze 都不支持小尺寸的 textarea 需要业务实现
.input-string.small {
font-size: 12px;
line-height: 20px;
padding: 1px 3px;
border-radius: 6px;
:global textarea {
font-size: 12px;
line-height: 20px;
}
:global .semi-input-clearbtn {
height: 22px;
}
}

View File

@@ -0,0 +1,34 @@
/*
* 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 clsx from 'clsx';
import { TextArea } from '@coze-arch/coze-design';
import css from './input.module.less';
export interface InputStringProps {
value?: string;
}
export const InputString: React.FC<InputStringProps> = props => (
<TextArea
className={clsx(css['input-string'], css.small)}
autosize={{ minRows: 1, maxRows: 5 }}
rows={1}
showClear
{...props}
/>
);

View File

@@ -0,0 +1,17 @@
/*
* 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 { InputTime, type InputTimeProps } from './time';

View File

@@ -0,0 +1,6 @@
.input-time {
width: 100%;
:global .semi-select {
width: 100%;
}
}

View File

@@ -0,0 +1,50 @@
/*
* 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 { clsx } from 'clsx';
import { DatePicker } from '@coze-arch/coze-design';
import css from './time.module.less';
export interface InputTimeProps {
className?: string;
value?: string;
onChange?: (v?: string) => void;
}
export const InputTime: React.FC<InputTimeProps> = ({
className,
value,
onChange,
...props
}) => (
<DatePicker
className={clsx(css['input-time'], className)}
type="dateTime"
size="small"
showClear={false}
showSuffix={false}
value={value}
onChange={(_date, dateString) => {
if (typeof dateString === 'string' || dateString === undefined) {
onChange?.(dateString);
}
}}
{...props}
/>
);

View File

@@ -0,0 +1,17 @@
/*
* 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 { SelectBoolean } from './select';

View File

@@ -0,0 +1,3 @@
.select-boolean {
width: 100%;
}

View File

@@ -0,0 +1,61 @@
/*
* 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, { useCallback, useMemo } from 'react';
import { clsx } from 'clsx';
import { Select, type SelectProps } from '@coze-arch/coze-design';
import css from './select.module.less';
interface SelectBooleanProps {
className?: string;
value?: boolean;
onChange?: (v?: boolean) => void;
}
export const SelectBoolean: React.FC<SelectBooleanProps> = ({
className,
value,
onChange,
...props
}) => {
const formattedValue = useMemo(
() => (value === undefined ? undefined : Number(value)),
[value],
);
const handleChange = useCallback(
(v?: SelectProps['value']) => {
const next = v === undefined ? v : Boolean(v);
onChange?.(next);
},
[onChange],
);
return (
<Select
className={clsx(css['select-boolean'], className)}
size="small"
value={formattedValue}
onChange={handleChange}
{...props}
>
<Select.Option value={1}>True</Select.Option>
<Select.Option value={0}>False</Select.Option>
</Select>
);
};

View File

@@ -0,0 +1,36 @@
/*
* 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 { VoiceSelect } from '@coze-workflow/components';
interface SelectVoiceProps {
value?: string;
onChange?: (v?: string) => void;
onBlur?: () => void;
}
export const SelectVoice: React.FC<SelectVoiceProps> = ({
onChange,
onBlur,
...props
}) => {
const handleChange = (v?: string) => {
onChange?.(v);
onBlur?.();
};
return <VoiceSelect onChange={handleChange} {...props} />;
};

View File

@@ -0,0 +1,50 @@
/*
* 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 {
FieldItem as BaseFieldItem,
type FieldItemProps,
} from '../../base-form-materials';
import { useFieldSchema } from '../../../form-engine';
import { TestFormFieldName } from '../../../constants';
export const FieldItem: React.FC<React.PropsWithChildren<FieldItemProps>> = ({
tag,
...props
}) => {
const schema = useFieldSchema();
const isBatchField = schema.path.includes(TestFormFieldName.Batch);
/** 批处理变量 tag 增加额外描述 */
const currentTag =
tag && isBatchField
? `${tag} - ${I18n.t('workflow_detail_node_batch')}`
: tag;
return (
<BaseFieldItem
title={schema.title}
description={schema.description}
required={schema.required}
tag={currentTag}
{...props}
/>
);
};

View File

@@ -0,0 +1,27 @@
/*
* 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 { InputString } from './input-string';
export { FieldItem } from './field-item';
export { InputNumber } from './input-number';
export { InputInteger } from './input-integer';
export { InputTime } from './input-time';
export { InputJson } from './input-json';
export { SelectBoolean } from './select-boolean';
export { SelectVoice } from './select-voice';

View File

@@ -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.
*/
import { InputNumber as BaseInputNumber } from '../../base-form-materials';
export const InputInteger = props => <BaseInputNumber int {...props} />;

View File

@@ -0,0 +1,17 @@
/*
* 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 { InputJson } from '../../base-form-materials';

View File

@@ -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.
*/
import { InputNumber as BaseInputNumber } from '../../base-form-materials';
export const InputNumber = props => <BaseInputNumber {...props} />;

View File

@@ -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.
*/
import { InputString as BaseInputString } from '../../base-form-materials';
export const InputString = BaseInputString;

View File

@@ -0,0 +1,17 @@
/*
* 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 { InputTime } from '../../base-form-materials';

View File

@@ -0,0 +1,17 @@
/*
* 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 { SelectBoolean } from '../../base-form-materials';

View File

@@ -0,0 +1,17 @@
/*
* 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 { SelectVoice } from '../../base-form-materials';

View File

@@ -0,0 +1,17 @@
/*
* 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 { TestRunForm } from './test-run-form';

View File

@@ -0,0 +1,72 @@
/*
* 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 { Form, type FormModel } from '@flowgram-adapter/free-layout-editor';
import {
InputString,
InputNumber,
InputInteger,
InputJson,
SelectBoolean,
SelectVoice,
InputTime,
FieldItem,
} from '../form-materials';
import {
createSchemaField,
type FormSchema,
useCreateForm,
type IFormSchema,
type FormSchemaReactComponents,
} from '../../form-engine';
const SchemaField = createSchemaField({
components: {
InputString,
InputNumber,
InputInteger,
InputTime,
InputJson,
SelectBoolean,
SelectVoice,
FieldItem,
},
});
interface TestRunFormProps {
schema: IFormSchema;
components?: FormSchemaReactComponents;
onFormValuesChange?: (payload: any) => void;
onMounted?: (formModel: FormModel, schema: FormSchema) => void;
}
export const TestRunForm: React.FC<TestRunFormProps> = ({
schema,
components,
onFormValuesChange,
onMounted,
}) => {
const { control, formSchema } = useCreateForm(schema, {
onFormValuesChange,
onMounted,
});
return (
<Form control={control}>
<SchemaField schema={formSchema} components={components} />
</Form>
);
};

View File

@@ -0,0 +1,32 @@
/*
* 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.
*/
/**
* 固定的内部 field name
*/
export enum TestFormFieldName {
Node = '_node',
Batch = '_batch',
Input = '_input',
Setting = '_setting',
JSON = '_json',
/** 关联内容 */
Related = '_related',
Bot = '_bot',
Conversation = '_conversation',
TestsetSelect = '_testset_select',
TestsetSave = '_testset_save',
}

View File

@@ -0,0 +1,68 @@
/*
* 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 { createContext, useContext, useRef } from 'react';
import {
createWithEqualityFn,
type UseBoundStoreWithEqualityFn,
} from 'zustand/traditional';
import { shallow } from 'zustand/shallow';
import { type StoreApi } from 'zustand';
import { type IFormSchema } from '../form-engine';
/**
* 单一表单内的全局性质状态集中管理
*/
export interface TestRunFormState {
schema: IFormSchema | null;
mode: 'form' | 'json';
patch: (next: Partial<TestRunFormState>) => void;
getSchema: () => TestRunFormState['schema'];
}
const createStore = () =>
createWithEqualityFn<TestRunFormState>(
(set, get) => ({
schema: null,
mode: 'form',
patch: next => set(() => next),
getSchema: () => get().schema,
}),
shallow,
);
type FormStore = UseBoundStoreWithEqualityFn<StoreApi<TestRunFormState>>;
const FormContext = createContext<FormStore>({} as unknown as FormStore);
export const TestRunFormProvider: React.FC<React.PropsWithChildren> = ({
children,
}) => {
const ref = useRef(createStore());
return (
<FormContext.Provider value={ref.current}>{children}</FormContext.Provider>
);
};
export const useTestRunFormStore = <T,>(
selector: (s: TestRunFormState) => T,
) => {
const store = useContext(FormContext);
return store(selector);
};

View File

@@ -0,0 +1,21 @@
/*
* 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 {
TestRunFormProvider,
useTestRunFormStore,
type TestRunFormState,
} from './form';

View File

@@ -0,0 +1,43 @@
/*
* 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 { type FormSchemaReactComponents } from '../types';
import { SchemaField, type SchemaFieldProps } from './schema-field';
interface CreateSchemaFieldOptions {
components: FormSchemaReactComponents;
}
type InnerSchemaField = React.FC<
Omit<SchemaFieldProps, 'components'> &
Pick<Partial<SchemaFieldProps>, 'components'>
>;
export const createSchemaField = (options: CreateSchemaFieldOptions) => {
const InnerSchemaField: InnerSchemaField = ({ components, ...props }) => (
<SchemaField
components={{
...options.components,
...components,
}}
{...props}
/>
);
return InnerSchemaField;
};

View File

@@ -0,0 +1,41 @@
/*
* 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 { Field } from '@flowgram-adapter/free-layout-editor';
import { SchemaContext, type FormSchema } from '../shared';
import { useFieldUIState } from '../hooks';
import { ReactiveField } from './reactive-field';
interface FieldProps {
name: string;
schema: FormSchema;
}
export const GeneralField: React.FC<React.PropsWithChildren<FieldProps>> = ({
schema,
}) => {
const parentUIState = useFieldUIState();
return (
<SchemaContext.Provider value={schema}>
<Field name={schema.path.join('.')} defaultValue={schema.defaultValue}>
<ReactiveField parentUIState={parentUIState} />
</Field>
</SchemaContext.Provider>
);
};

View File

@@ -0,0 +1,17 @@
/*
* 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 { createSchemaField } from './create-schema-field';

View File

@@ -0,0 +1,46 @@
/*
* 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 { SchemaContext, type FormSchema } from '../shared';
import { useComponents } from '../hooks';
interface ObjectFieldProps {
schema: FormSchema;
}
export const ObjectField: React.FC<
React.PropsWithChildren<ObjectFieldProps>
> = ({ schema, children }) => {
const components = useComponents();
const renderDecorator = () => {
if (!schema.decoratorType || !components[schema.decoratorType]) {
return <>{children}</>;
}
return React.createElement(
components[schema.decoratorType],
schema.decoratorProps,
children,
);
};
return (
<SchemaContext.Provider value={schema}>
{renderDecorator()}
</SchemaContext.Provider>
);
};

View File

@@ -0,0 +1,87 @@
/*
* 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 { useCurrentField, useCurrentFieldState } from '@flowgram-adapter/free-layout-editor';
import { type FormSchemaUIState } from '../types';
import {
useFieldUIState,
useFieldSchema,
useComponents,
useFormUIState,
} from '../hooks';
interface ReactiveFieldProps {
parentUIState?: FormSchemaUIState;
}
/**
* 接入响应式的 Field
*/
const ReactiveField: React.FC<ReactiveFieldProps> = ({ parentUIState }) => {
const components = useComponents();
const schema = useFieldSchema();
const field = useCurrentField();
const uiState = useFieldUIState();
const formUIState = useFormUIState();
const fieldState = useCurrentFieldState();
/**
* 自生的 disabled 态由父亲和自身一起控制
*/
const disabled =
parentUIState?.disabled || uiState.disabled || formUIState.disabled;
const validateStatus = fieldState.errors?.length ? 'error' : undefined;
const renderComponent = () => {
if (!schema.componentType || !components[schema.componentType]) {
return null;
}
return React.createElement(components[schema.componentType], {
disabled,
validateStatus,
value: field.value,
onChange: field.onChange,
onFocus: field.onFocus,
onBlur: field.onBlur,
['data-testid']: ['workflow', 'testrun', 'form', 'component']
.concat(schema.path)
.join('.'),
...schema.componentProps,
});
};
const renderDecorator = (children: React.ReactNode) => {
if (!schema.decoratorType || !components[schema.decoratorType]) {
return <>{children}</>;
}
return React.createElement(
components[schema.decoratorType],
{
...schema.decoratorProps,
['data-testid']: ['workflow', 'testrun', 'form', 'decorator']
.concat(schema.path)
.join('.'),
},
children,
);
};
return renderDecorator(renderComponent());
};
export { ReactiveField, ReactiveFieldProps };

View File

@@ -0,0 +1,62 @@
/*
* 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 { FormSchema } from '../shared';
import { ObjectField } from './object-field';
import { GeneralField } from './general-field';
interface RecursionFieldProps {
schema: FormSchema;
name?: string;
}
const computePath = (path?: string[], name?: string) =>
[...(path || []), name].filter((i): i is string => Boolean(i));
/**
* 递归 Field
*/
const RecursionField: React.FC<RecursionFieldProps> = ({ name, schema }) => {
const renderProperties = () => {
const properties = FormSchema.getProperties(schema);
if (!properties.length) {
return null;
}
const { path } = schema;
return (
<ObjectField schema={schema}>
{properties.map((item, index) => (
<RecursionField
name={item.key}
schema={new FormSchema(item.schema, computePath(path, item.key))}
key={`${index}-${item.key}`}
/>
))}
</ObjectField>
);
};
if (!name) {
return renderProperties();
}
if (schema.type === 'object') {
return renderProperties();
}
return <GeneralField name={name} schema={schema} />;
};
export { RecursionField, type RecursionFieldProps };

View File

@@ -0,0 +1,38 @@
/*
* 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 type { FormSchemaReactComponents } from '../types';
import {
ComponentsContext,
FormSchemaContext,
type FormSchema,
} from '../shared';
import { RecursionField } from './recursion-field';
export interface SchemaFieldProps {
schema: FormSchema;
components: FormSchemaReactComponents;
}
export const SchemaField: React.FC<SchemaFieldProps> = props => (
<ComponentsContext.Provider value={props.components}>
<FormSchemaContext.Provider value={props.schema}>
<RecursionField schema={props.schema} />
</FormSchemaContext.Provider>
</ComponentsContext.Provider>
);

View File

@@ -0,0 +1,22 @@
/*
* 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 { useCreateForm } from './use-create-form';
export { useFieldSchema } from './use-field-schema';
export { useFieldUIState } from './use-field-ui-state';
export { useComponents } from './use-components';
export { useFormSchema } from './use-form-schema';
export { useFormUIState } from './use-form-ui-state';

View File

@@ -0,0 +1,21 @@
/*
* 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 { ComponentsContext } from '../shared';
export const useComponents = () => useContext(ComponentsContext);

View File

@@ -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 { useEffect, useMemo } from 'react';
import { createForm, ValidateTrigger } from '@flowgram-adapter/free-layout-editor';
import type { IFormSchema, IFormSchemaValidate } from '../types';
import { FormSchema } from '../shared';
type Rules = Record<string, IFormSchemaValidate>;
const getFieldPath = (...args: (string | undefined)[]) =>
args.filter(path => path).join('.');
export function validateResolver(schema: IFormSchema): Rules {
const rules = {};
visit(schema);
return rules;
function visit(current: IFormSchema, name?: string) {
if (name && current['x-validator']) {
rules[name] = current['x-validator'];
}
if (current.type === 'object' && current.properties) {
Object.entries(current.properties).forEach(([key, value]) => {
visit(value, getFieldPath(name, key));
});
}
}
}
export const useCreateForm = (schema: IFormSchema, options: any = {}) => {
const { validate } = options;
const innerValidate = useMemo(
() => ({
...validateResolver(schema),
...validate,
}),
[schema],
);
const { form, control } = useMemo(
() =>
createForm({
validate: innerValidate,
validateTrigger: ValidateTrigger.onBlur,
...options,
}),
[schema, innerValidate],
);
const formSchema = useMemo(
() => new FormSchema({ type: 'object', ...schema }),
[schema],
);
useEffect(() => {
if (options.onMounted) {
options.onMounted(control._formModel, formSchema);
}
const disposable = control._formModel.onFormValuesUpdated(payload => {
if (options?.onFormValuesChange) {
options.onFormValuesChange(payload);
}
});
return () => disposable.dispose();
}, [control]);
return {
form,
control,
model: control._formModel,
formSchema,
};
};

View File

@@ -0,0 +1,21 @@
/*
* 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 { SchemaContext } from '../shared';
export const useFieldSchema = () => useContext(SchemaContext);

View File

@@ -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 { useObserve } from '@flowgram-adapter/common';
import { useFieldSchema } from './use-field-schema';
export const useFieldUIState = () => {
const schema = useFieldSchema();
return useObserve(schema?.uiState?.value);
};

View File

@@ -0,0 +1,21 @@
/*
* 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 { FormSchemaContext } from '../shared';
export const useFormSchema = () => useContext(FormSchemaContext);

View File

@@ -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 { useObserve } from '@flowgram-adapter/common';
import { useFormSchema } from './use-form-schema';
export const useFormUIState = () => {
const schema = useFormSchema();
return useObserve(schema?.uiState?.value);
};

View File

@@ -0,0 +1,33 @@
/*
* 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 { createSchemaField } from './fields';
export { FormSchema } from './shared';
export { useCreateForm, useFieldSchema, useFormSchema } from './hooks';
export type {
IFormSchema,
IFormSchemaValidate,
FormSchemaReactComponents,
} from './types';
export {
useForm,
useCurrentFieldState,
type FormModel,
} from '@flowgram-adapter/free-layout-editor';

View File

@@ -0,0 +1,21 @@
/*
* 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 { createContext } from 'react';
import type { FormSchemaReactComponents } from '../types';
export const ComponentsContext = createContext<FormSchemaReactComponents>({});

View File

@@ -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 { type ReactNode } from 'react';
import { ReactiveState } from '@flowgram-adapter/common';
import type { IFormSchema, FormSchemaUIState } from '../types';
interface PropertyWithKey {
key: string;
schema: IFormSchema;
}
export class FormSchema implements IFormSchema {
/** IFormSchema 透传属性 */
type?: string | undefined;
title?: ReactNode;
description?: ReactNode;
required?: boolean;
properties?: Record<string, IFormSchema>;
defaultValue?: any;
/** 模型属性 */
uiState = new ReactiveState<FormSchemaUIState>({ disabled: false });
path: string[] = [];
constructor(json: IFormSchema, path: string[] = []) {
this.fromJSON(json);
this.path = path;
}
get componentType() {
return this['x-component'];
}
get componentProps() {
return this['x-component-props'];
}
get decoratorType() {
return this['x-decorator'];
}
get decoratorProps() {
return this['x-decorator-props'];
}
fromJSON(json: IFormSchema) {
Object.entries(json).forEach(([key, value]) => {
this[key] = value;
});
this.uiState.value.disabled = json['x-disabled'] ?? false;
}
/**
* 获得有序的 properties
*/
static getProperties(schema: FormSchema | IFormSchema) {
const orderProperties: PropertyWithKey[] = [];
const unOrderProperties: PropertyWithKey[] = [];
Object.entries(schema.properties || {}).forEach(([key, item]) => {
const index = item['x-index'];
if (index !== undefined && !isNaN(index)) {
orderProperties[index] = { schema: item, key };
} else {
unOrderProperties.push({ schema: item, key });
}
});
return orderProperties.concat(unOrderProperties).filter(item => !!item);
}
}

View File

@@ -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 { ComponentsContext } from './components-context';
export { FormSchema } from './form-schema';
export { SchemaContext, FormSchemaContext } from './schema-context';

View File

@@ -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.
*/
import { createContext } from 'react';
import type { FormSchema } from './form-schema';
export const SchemaContext = createContext<FormSchema>({} as any);
export const FormSchemaContext = createContext<FormSchema>({} as any);

View File

@@ -0,0 +1,21 @@
/*
* 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.
*/
type JSXComponent =
| keyof JSX.IntrinsicElements
| React.JSXElementConstructor<any>;
export type FormSchemaReactComponents = Record<string, JSXComponent>;

View File

@@ -0,0 +1,22 @@
/*
* 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 type {
IFormSchema,
IFormSchemaValidate,
FormSchemaUIState,
} from './schema';
export type { FormSchemaReactComponents } from './context';

View File

@@ -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.
*/
import type { Validate } from '@flowgram-adapter/free-layout-editor';
export type FormSchemaTypes =
| 'string'
| 'object'
| 'array'
| 'number'
| 'boolean'
| 'void'
| string;
export type IFormSchemaValidate = Validate;
export interface FormSchemaUIState {
disabled: boolean;
}
export interface IFormSchema<FrameworkComponent = React.ReactNode> {
/*******************************************************
* 核心属性
*/
version?: string;
name?: string;
type?: FormSchemaTypes;
/** 默认值“default” 是 jsonSchema 标准字段,但其为 js 关键字,遂使用 defaultValue */
defaultValue?: any;
/*******************************************************
* 下钻属性
*/
properties?: Record<string, IFormSchema<FrameworkComponent>>;
items?: IFormSchema<FrameworkComponent>[];
/*******************************************************
* ui 属性
*/
title?: FrameworkComponent | string;
description?: FrameworkComponent | string;
/** 顺序 */
['x-index']?: number;
['x-visible']?: boolean;
['x-hidden']?: boolean;
['x-disabled']?: boolean;
/** 渲染的组件 */
['x-component']?: string;
['x-component-props']?: Record<string, unknown>;
/** 装饰器 */
['x-decorator']?: string;
['x-decorator-props']?: Record<string, unknown>;
/*******************************************************
* 合法性属性
*/
required?: boolean;
['x-validator']?: IFormSchemaValidate;
/*******************************************************
* 不常用或实现成本较高
*/
['x-reactions']?: any;
['x-content']?: FrameworkComponent;
/** 通配符字段 */
patternProperties?: Record<string, IFormSchema<FrameworkComponent>>;
/** 定义之外的字段 */
additionalProperties?: IFormSchema<FrameworkComponent>;
/** 定义之外的项 */
additionalItems?: IFormSchema<FrameworkComponent>;
/*******************************************************
* 业务自定义字段
*/
/** 节点 id */
['x-node-id']?: string;
/** 节点类型 */
['x-node-type']?: string;
/** 表单模式 */
['x-form-mode']?: 'form' | 'json';
/** 字段对应变量原始类型 */
['x-origin-type']?: string;
[key: string]: any;
}

View File

@@ -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.
*/
/// <reference types='@coze-arch/bot-typings' />
declare module '*.otf' {
const content: string;
export default content;
}
declare module '*.ttf' {
const content: string;
export default content;
}

View File

@@ -0,0 +1,55 @@
/*
* 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.
*/
/**
* TestRun Form
*/
/** Form Engine */
export {
createSchemaField,
useFormSchema,
useForm,
useCurrentFieldState,
FormSchema,
type FormModel,
type IFormSchema,
} from './form-engine';
/** components */
export { TestRunForm } from './components/test-run-form';
export {
InputJson as FormBaseInputJson,
GroupCollapse as FormBaseGroupCollapse,
FieldItem as FormBaseFieldItem,
} from './components/base-form-materials';
/** context */
export {
TestRunFormProvider,
useTestRunFormStore,
type TestRunFormState,
} from './context';
/** utils */
export {
generateField,
generateFieldValidator,
isFormSchemaPropertyEmpty,
stringifyFormValuesFromBacked,
} from './utils';
/** constants */
export { TestFormFieldName } from './constants';

View File

@@ -0,0 +1,87 @@
/*
* 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 { ViewVariableType, getFileAccept } from '@coze-workflow/base';
interface GenerateFieldComponentOptions {
type: ViewVariableType;
validateJsonSchema?: any;
}
export const generateFieldComponent = (
options: GenerateFieldComponentOptions,
) => {
const { type, validateJsonSchema } = options;
/** 音色类型 */
if (ViewVariableType.Voice === type) {
return {
['x-component']: 'SelectVoice',
};
}
/** 文件类型 */
if (ViewVariableType.isFileType(type)) {
const fileType = [
ViewVariableType.Image,
ViewVariableType.ArrayImage,
].includes(type)
? 'image'
: 'object';
return {
['x-component']: 'TypedFileInput',
['x-component-props']: {
// 如果是数组类型,则表明是多选的文件选择器
multiple: ViewVariableType.isArrayType(type),
accept: getFileAccept(type),
fileType,
},
};
}
/** 排除文件类型的对象类型、数组类型 */
if (ViewVariableType.isArrayType(type) || ViewVariableType.Object === type) {
return {
['x-component']: 'InputJson',
['x-component-props']: {
jsonSchema: validateJsonSchema,
},
defaultValue: ViewVariableType.Object === type ? '{}' : '[]',
};
}
if (type === ViewVariableType.Integer) {
return {
['x-component']: 'InputInteger',
};
}
if (type === ViewVariableType.Number) {
return {
['x-component']: 'InputNumber',
};
}
if (type === ViewVariableType.Boolean) {
return {
['x-component']: 'SelectBoolean',
defaultValue: true,
};
}
if (type === ViewVariableType.Time) {
return {
['x-component']: 'InputTime',
};
}
/** string 类型和其它未知类型都渲染普通输入框 */
return {
['x-component']: 'InputString',
};
};

View File

@@ -0,0 +1,72 @@
/*
* 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 Ajv from 'ajv';
import { I18n } from '@coze-arch/i18n';
import { type IFormSchemaValidate } from '../form-engine';
const isEmptyValue = (v: unknown) => v === undefined || v === null || v === '';
interface GenerateFieldValidatorOptions {
name: string;
title?: string;
required?: boolean;
validateJsonSchema?: any;
}
/**
* ajv 实例缓存
* 无需导入创建或者多次创建,优化内存开销
*/
let ajvCache: undefined | Ajv;
export const generateFieldValidator = (
options: GenerateFieldValidatorOptions,
) => {
const { required, title, name, validateJsonSchema } = options;
const validator: IFormSchemaValidate = ({ value }) => {
if (required && isEmptyValue(value)) {
return I18n.t('workflow_testset_required_tip', {
param_name: title || name,
});
}
// 如果有结构化描述,还需要对值进行反序列化校验
if (validateJsonSchema && value !== undefined) {
if (!ajvCache) {
ajvCache = new Ajv();
}
try {
const valueObject = JSON.parse(value);
const validate = ajvCache.compile(validateJsonSchema);
const valid = validate(valueObject);
return valid ? undefined : I18n.t('workflow_debug_wrong_json');
} catch {
/**
* 报错有多种可能,预期结果都是校验不通过
* 1. 值反序列化失败
* 2. 反序列化的值不合法
*/
return I18n.t('workflow_debug_wrong_json');
}
}
};
return {
['x-validator']: validator,
};
};

View File

@@ -0,0 +1,66 @@
/*
* 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 { ViewVariableType } from '@coze-workflow/base';
import { type IFormSchema } from '../form-engine';
import { generateFieldValidator } from './generate-field-validator';
import { generateFieldComponent } from './generate-field-component';
interface GenerateFieldOptions {
type: ViewVariableType;
name: string;
title?: string;
required?: boolean;
description?: string;
defaultValue?: string;
validateJsonSchema?: any;
extra?: IFormSchema;
}
/**
* 表单 Field Schema 计算
*/
export const generateField = (options: GenerateFieldOptions): IFormSchema => {
const {
type,
name,
title,
required = true,
description,
defaultValue,
validateJsonSchema,
extra,
} = options;
return {
name,
title,
description,
required,
['x-decorator']: 'FieldItem',
['x-decorator-props']: {
tag: ViewVariableType.LabelMap[type],
},
['x-origin-type']: type as unknown as string,
...generateFieldValidator(options),
// 渲染组件相关
...generateFieldComponent({ type, validateJsonSchema }),
// component 也自带默认值,入参的默认值优先级更高
defaultValue,
...extra,
};
};

View File

@@ -0,0 +1,20 @@
/*
* 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 { generateField } from './generate-field';
export { generateFieldValidator } from './generate-field-validator';
export { isFormSchemaPropertyEmpty } from './is-property-empty';
export { stringifyFormValuesFromBacked } from './stringify-form-values-from-backed';

View File

@@ -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.
*/
import { isObject } from 'lodash-es';
/**
* 是否是空的 properties
*/
export const isFormSchemaPropertyEmpty = (properties: unknown) =>
isObject(properties) ? !Object.keys(properties).length : true;

View File

@@ -0,0 +1,34 @@
/*
* 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 { isBoolean } from 'lodash-es';
export const stringifyFormValuesFromBacked = (value: object) => {
if (!value) {
return undefined;
}
return Object.keys(value).reduce((acc, key) => {
const val = value[key];
if (val === null || val === undefined) {
acc[key] = undefined;
} else if (typeof val === 'string' || isBoolean(val)) {
acc[key] = val;
} else {
acc[key] = JSON.stringify(value[key]);
}
return acc;
}, {});
};