feat: manually mirror opencoze's code from bytedance
Change-Id: I09a73aadda978ad9511264a756b2ce51f5761adf
This commit is contained in:
@@ -0,0 +1,153 @@
|
||||
/*
|
||||
* 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 FC, useState } from 'react';
|
||||
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { InputComponentType } from '@coze-arch/bot-api/connector_api';
|
||||
|
||||
import { type InputConfigFe } from '../types';
|
||||
import {
|
||||
useConfigAsserted,
|
||||
useConfigStoreGuarded,
|
||||
} from '../context/store-context';
|
||||
import { ERROR_LINE_HEIGHT } from '../constants';
|
||||
import { type HeaderItem, SortableFieldTable } from './sortable-field-table';
|
||||
import {
|
||||
BaseInputFieldLine,
|
||||
inputFieldColumnWidth,
|
||||
type InputComponentOption,
|
||||
InputLineCommonContext,
|
||||
} from './field-line/input-config-line';
|
||||
|
||||
export const BaseInputFieldsTable: FC = () => {
|
||||
const config = useConfigAsserted();
|
||||
const inputFields = config.input_config || [];
|
||||
const updateConfigByImmer = useConfigStoreGuarded()(
|
||||
state => state.updateConfigByImmer,
|
||||
);
|
||||
const [errorLines, setErrorLines] = useState<string[]>([]);
|
||||
|
||||
return (
|
||||
<div className="mt-[4px]">
|
||||
<InputLineCommonContext.Provider
|
||||
value={{
|
||||
inputFieldsSelectorList: config.input_type_list,
|
||||
onChange: val => {
|
||||
updateConfigByImmer(cfg => {
|
||||
const fields = cfg.input_config;
|
||||
const idx = fields.findIndex(field => field._id === val._id);
|
||||
fields[idx] = val;
|
||||
});
|
||||
},
|
||||
inputOptions: getInputOptions(),
|
||||
onToggleError: (id, error) => {
|
||||
setErrorLines(lines => {
|
||||
if (!error) {
|
||||
return lines.filter(lineId => lineId !== id);
|
||||
}
|
||||
const inLines = lines.includes(id);
|
||||
if (!inLines) {
|
||||
return [...lines, id];
|
||||
}
|
||||
return lines;
|
||||
});
|
||||
},
|
||||
}}
|
||||
>
|
||||
<SortableFieldTable<InputConfigFe>
|
||||
enabled={inputFields.length > 1}
|
||||
headers={getInputFieldsHeaders()}
|
||||
data={inputFields.map(field => ({
|
||||
deletable: field.invalid ?? false,
|
||||
bizComponent: BaseInputFieldLine,
|
||||
data: field,
|
||||
getKey: data => data._id,
|
||||
onDelete: delItem => {
|
||||
updateConfigByImmer(cfg => {
|
||||
cfg.input_config = cfg.input_config.filter(
|
||||
e => e._id !== delItem._id,
|
||||
);
|
||||
});
|
||||
},
|
||||
deleteButtonStyle: {
|
||||
width: 32,
|
||||
},
|
||||
lineStyle: {
|
||||
marginTop: 8,
|
||||
paddingBottom: errorLines.some(id => id.includes(field._id))
|
||||
? ERROR_LINE_HEIGHT
|
||||
: 0,
|
||||
},
|
||||
}))}
|
||||
getId={data => data.data._id}
|
||||
onChange={mix =>
|
||||
updateConfigByImmer(cfg => {
|
||||
cfg.input_config = mix.map(x => x.data);
|
||||
})
|
||||
}
|
||||
/>
|
||||
</InputLineCommonContext.Provider>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const getInputFieldsHeaders = (): HeaderItem[] => [
|
||||
{
|
||||
name: I18n.t('publish_base_configFields_field'),
|
||||
required: false,
|
||||
width: inputFieldColumnWidth.field,
|
||||
},
|
||||
{
|
||||
name: I18n.t('publish_base_configFields_title'),
|
||||
required: true,
|
||||
width: inputFieldColumnWidth.title,
|
||||
},
|
||||
{
|
||||
name: I18n.t('publish_base_configFields_placeholder'),
|
||||
required: false,
|
||||
width: inputFieldColumnWidth.placeholder,
|
||||
},
|
||||
{
|
||||
name: I18n.t('publish_base_configFields_component'),
|
||||
required: true,
|
||||
width: inputFieldColumnWidth.control,
|
||||
},
|
||||
{
|
||||
name: I18n.t('required'),
|
||||
required: false,
|
||||
width: inputFieldColumnWidth.required,
|
||||
},
|
||||
];
|
||||
|
||||
const getInputOptions = (): InputComponentOption[] => [
|
||||
{
|
||||
label: I18n.t('publish_base_inputFieldConfig_textInput'),
|
||||
value: InputComponentType.Text,
|
||||
},
|
||||
{
|
||||
label: I18n.t('publish_base_inputFieldConfig_singleSelect'),
|
||||
value: InputComponentType.SingleSelect,
|
||||
},
|
||||
{
|
||||
label: I18n.t('publish_base_inputFieldConfig_multiSelect'),
|
||||
value: InputComponentType.MultiSelect,
|
||||
},
|
||||
{
|
||||
label: I18n.t('publish_base_inputFieldConfig_fieldSelector'),
|
||||
value: InputComponentType.FieldSelector,
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,365 @@
|
||||
/*
|
||||
* 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 ReactMarkdown from 'react-markdown';
|
||||
import {
|
||||
type FC,
|
||||
type PropsWithChildren,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
|
||||
import { nanoid } from 'nanoid';
|
||||
import classNames from 'classnames';
|
||||
import { useMutationObserver } from 'ahooks';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { IconCozPlus } from '@coze-arch/coze-design/icons';
|
||||
import { Button, Select } from '@coze-arch/coze-design';
|
||||
import {
|
||||
type OutputTypeInfo,
|
||||
OutputSubComponentType,
|
||||
} from '@coze-arch/bot-api/connector_api';
|
||||
|
||||
import { getIsStructOutput } from '../validate/utils';
|
||||
import {
|
||||
validateOutputStructGroupByKey,
|
||||
validateOutputStructPrimaryKey,
|
||||
} from '../validate';
|
||||
import {
|
||||
type BaseOutputStructLineType,
|
||||
type FeishuBaseConfigFe,
|
||||
type OutputSubComponentFe,
|
||||
} from '../types';
|
||||
import { default as mdStyles } from '../md-tooltip/index.module.less';
|
||||
import { useConfigStoreGuarded } from '../context/store-context';
|
||||
import { ERROR_LINE_HEIGHT } from '../constants';
|
||||
import { type HeaderItem, SortableFieldTable } from './sortable-field-table';
|
||||
import { FormSubtitle } from './form-title';
|
||||
import { useRequireVerifyCenter } from './field-line/require-verify-center';
|
||||
import {
|
||||
BaseOutputStructLine,
|
||||
OutputLineCommonContext,
|
||||
outputStructColumnWidth,
|
||||
type OutputStructVerifyRes,
|
||||
} from './field-line/output-struct-line';
|
||||
|
||||
import styles from './index.module.less';
|
||||
|
||||
export const BaseOutputFieldsTable: FC<{
|
||||
config: FeishuBaseConfigFe;
|
||||
}> = ({ config }) => {
|
||||
const outputTypeId = config.output_type;
|
||||
const updateConfigByImmer = useConfigStoreGuarded()(
|
||||
state => state.updateConfigByImmer,
|
||||
);
|
||||
const outputTypeTips = getOutputInfo(
|
||||
config.output_type_list,
|
||||
outputTypeId,
|
||||
)?.tips;
|
||||
|
||||
return (
|
||||
<div className="mt-[6px]">
|
||||
<Select
|
||||
style={{
|
||||
width: '100%',
|
||||
}}
|
||||
defaultValue={config.output_type}
|
||||
optionList={config.output_type_list.map(info => ({
|
||||
label: info.name,
|
||||
value: info.id,
|
||||
}))}
|
||||
onChange={val => {
|
||||
updateConfigByImmer(cfg => {
|
||||
const type = Number(val);
|
||||
cfg.output_type = type;
|
||||
if (getIsStructOutput(type)) {
|
||||
cfg.output_sub_component.type = OutputSubComponentType.Object;
|
||||
const itemList = cfg.output_sub_component.item_list;
|
||||
if (!itemList?.length) {
|
||||
cfg.output_sub_component.item_list = [
|
||||
getDefaultStructFieldItem(),
|
||||
];
|
||||
}
|
||||
} else {
|
||||
cfg.output_sub_component.type = OutputSubComponentType.None;
|
||||
}
|
||||
});
|
||||
}}
|
||||
/>
|
||||
{getIsStructOutput(outputTypeId) ? (
|
||||
<OutputStructConfig config={config} />
|
||||
) : null}
|
||||
{outputTypeTips ? (
|
||||
<div
|
||||
className={classNames(
|
||||
'rounded-[8px] px-[8px] py-[12px] coz-mg-hglt mt-[8px]',
|
||||
'text-[12px] leading-[16px] coz-fg-primary',
|
||||
)}
|
||||
style={{
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment -- .
|
||||
// @ts-expect-error
|
||||
'--tooltip-content-max-width': '580px',
|
||||
}}
|
||||
>
|
||||
<ReactMarkdown className={mdStyles.md_wrap}>
|
||||
{outputTypeTips}
|
||||
</ReactMarkdown>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const getOutputInfo = (
|
||||
infoList: OutputTypeInfo[],
|
||||
id: number,
|
||||
): OutputTypeInfo | undefined => infoList.find(info => info.id === id);
|
||||
|
||||
const getDefaultStructFieldItem = (): BaseOutputStructLineType => ({
|
||||
key: '',
|
||||
output_type: undefined,
|
||||
_id: nanoid(),
|
||||
});
|
||||
|
||||
const OutputStructConfig: FC<{
|
||||
config: FeishuBaseConfigFe;
|
||||
}> = ({ config }) => {
|
||||
const structFields = config.output_sub_component.item_list;
|
||||
const updateConfigByImmer = useConfigStoreGuarded()(
|
||||
state => state.updateConfigByImmer,
|
||||
);
|
||||
const [requiredToCheck, setRequiredToCheck] = useState(false);
|
||||
|
||||
const { registerVerifyFn } = useRequireVerifyCenter();
|
||||
const [errorLines, setErrorLinesRaw] = useState<string[]>([]);
|
||||
const setErrorLines = (hasError: boolean, id: string) => {
|
||||
setErrorLinesRaw(lines => {
|
||||
if (!hasError) {
|
||||
return lines.filter(lineId => lineId !== id);
|
||||
}
|
||||
const inLines = lines.includes(id);
|
||||
if (!inLines) {
|
||||
return [...lines, id];
|
||||
}
|
||||
return lines;
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const unregister = registerVerifyFn(() => setRequiredToCheck(true));
|
||||
return unregister;
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="ml-[8px] mt-[16px]">
|
||||
<FormSubtitle
|
||||
title={I18n.t('publish_base_config_structOutputConfig')}
|
||||
required
|
||||
tooltip={config.output_sub_component.struct_output_desc}
|
||||
suffix={
|
||||
<Button
|
||||
icon={<IconCozPlus />}
|
||||
onClick={() => {
|
||||
updateConfigByImmer(cfg => {
|
||||
const newList = cfg.output_sub_component.item_list || [];
|
||||
newList.push(getDefaultStructFieldItem());
|
||||
cfg.output_sub_component.item_list = newList;
|
||||
});
|
||||
}}
|
||||
color="secondary"
|
||||
size="small"
|
||||
className="ml-auto"
|
||||
>
|
||||
{I18n.t('Add_1')}
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<OutputLineCommonContext.Provider
|
||||
value={{
|
||||
onChange: val => {
|
||||
updateConfigByImmer(cfg => {
|
||||
const fields = cfg.output_sub_component.item_list;
|
||||
if (!fields) {
|
||||
return;
|
||||
}
|
||||
const idx =
|
||||
fields?.findIndex(field => field._id === val._id) ?? -1;
|
||||
if (idx < 0) {
|
||||
return;
|
||||
}
|
||||
fields[idx] = val;
|
||||
});
|
||||
},
|
||||
list: config.object_value_type_list,
|
||||
getShowRequireWarn: curLine => {
|
||||
const res = getShowRequireWarnImpl({
|
||||
curLine,
|
||||
allFields: config.output_sub_component?.item_list || [],
|
||||
requiredToCheck,
|
||||
});
|
||||
return res;
|
||||
},
|
||||
onToggleError: (id, error) => {
|
||||
setErrorLines(error, id);
|
||||
},
|
||||
}}
|
||||
>
|
||||
<SortableFieldTable<BaseOutputStructLineType>
|
||||
linesWrapper={OutputConfigLinesWrapper}
|
||||
enabled={(structFields?.length || 0) > 1}
|
||||
headers={getBaseInfoHeaders(config.output_sub_component)}
|
||||
onChange={mix =>
|
||||
updateConfigByImmer(cfg => {
|
||||
cfg.output_sub_component.item_list = mix.map(m => m.data);
|
||||
})
|
||||
}
|
||||
getId={mix => mix.data._id}
|
||||
style={{
|
||||
marginTop: 6,
|
||||
}}
|
||||
data={(structFields || []).map(field => ({
|
||||
bizComponent: BaseOutputStructLine,
|
||||
deletable: (structFields || []).length > 1,
|
||||
data: field,
|
||||
getKey: data => data._id,
|
||||
onDelete: delItem => {
|
||||
updateConfigByImmer(cfg => {
|
||||
const list = cfg.output_sub_component.item_list;
|
||||
if (!list) {
|
||||
return;
|
||||
}
|
||||
cfg.output_sub_component.item_list = list.filter(
|
||||
item => item._id !== delItem._id,
|
||||
);
|
||||
});
|
||||
},
|
||||
lineStyle: {
|
||||
paddingBottom: errorLines.some(id => id.includes(field._id))
|
||||
? ERROR_LINE_HEIGHT
|
||||
: 0,
|
||||
},
|
||||
deleteButtonStyle: {
|
||||
width: 24,
|
||||
minWidth: 0,
|
||||
height: 24,
|
||||
padding: 0,
|
||||
},
|
||||
}))}
|
||||
/>
|
||||
</OutputLineCommonContext.Provider>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const OutputConfigLinesWrapper: FC<PropsWithChildren> = ({ children }) => {
|
||||
const wrapRef = useRef<HTMLDivElement | null>(null);
|
||||
const [withScrollbar, setWithScrollbar] = useState(false);
|
||||
const detectScrollbar = () => {
|
||||
if (!wrapRef.current) {
|
||||
return;
|
||||
}
|
||||
const isVerticalScrollbar =
|
||||
wrapRef.current.scrollHeight > wrapRef.current.clientHeight;
|
||||
setWithScrollbar(isVerticalScrollbar);
|
||||
};
|
||||
useEffect(detectScrollbar, []);
|
||||
useMutationObserver(detectScrollbar, wrapRef, {
|
||||
childList: true,
|
||||
});
|
||||
return (
|
||||
<div
|
||||
ref={wrapRef}
|
||||
className={classNames(
|
||||
'overflow-x-hidden',
|
||||
!withScrollbar && 'pr-[8px]',
|
||||
styles.output_config,
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const getBaseInfoHeaders = (
|
||||
outputComponent: OutputSubComponentFe,
|
||||
): HeaderItem[] => [
|
||||
{
|
||||
name: I18n.t('publish_base_configFields_key'),
|
||||
required: true,
|
||||
width: outputStructColumnWidth.key,
|
||||
},
|
||||
{
|
||||
name: I18n.t('publish_base_configStruct_dataType'),
|
||||
required: true,
|
||||
width: outputStructColumnWidth.outputType,
|
||||
},
|
||||
{
|
||||
name: I18n.t('publish_base_configStruct_id'),
|
||||
required: true,
|
||||
width: outputStructColumnWidth.groupByKey,
|
||||
tooltip: outputComponent.struct_id_desc,
|
||||
},
|
||||
{
|
||||
name: I18n.t('publish_base_configStruct_primary'),
|
||||
required: true,
|
||||
width: outputStructColumnWidth.primary,
|
||||
tooltip: outputComponent.struct_primary_desc,
|
||||
style: I18n.language.includes('zh')
|
||||
? {}
|
||||
: {
|
||||
fontSize: 12,
|
||||
lineHeight: '16px',
|
||||
display: 'inline-block',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const getShowRequireWarnImpl = ({
|
||||
curLine,
|
||||
allFields,
|
||||
requiredToCheck,
|
||||
}: {
|
||||
curLine: BaseOutputStructLineType;
|
||||
allFields: BaseOutputStructLineType[];
|
||||
requiredToCheck: boolean;
|
||||
}): OutputStructVerifyRes => {
|
||||
const idx = allFields.findIndex(line => line._id === curLine._id);
|
||||
const res: OutputStructVerifyRes = {
|
||||
groupByKey: {
|
||||
warn: false,
|
||||
},
|
||||
primary: {
|
||||
warn: false,
|
||||
},
|
||||
};
|
||||
if (!requiredToCheck || idx > 0) {
|
||||
return res;
|
||||
}
|
||||
const groupByKeyVerifyRes = validateOutputStructGroupByKey(allFields);
|
||||
const primaryKeyVerifyRes = validateOutputStructPrimaryKey(allFields);
|
||||
if (!groupByKeyVerifyRes.ok) {
|
||||
res.groupByKey.tip = groupByKeyVerifyRes.error;
|
||||
res.groupByKey.warn = true;
|
||||
}
|
||||
if (!primaryKeyVerifyRes.ok) {
|
||||
res.primary.tip = primaryKeyVerifyRes.error;
|
||||
res.primary.warn = true;
|
||||
}
|
||||
return res;
|
||||
};
|
||||
@@ -0,0 +1,5 @@
|
||||
.input_deletable {
|
||||
:global(.semi-input-suffix) {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
/*
|
||||
* 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, type FC, useContext } from 'react';
|
||||
|
||||
import { nanoid } from 'nanoid';
|
||||
import { produce } from 'immer';
|
||||
import classNames from 'classnames';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { IconCozPlus, IconCozTrashCan } from '@coze-arch/coze-design/icons';
|
||||
import { Button, Input } from '@coze-arch/coze-design';
|
||||
|
||||
import { SortableFieldTable } from '../sortable-field-table';
|
||||
import type { InputComponentSelectOption, InputConfigFe } from '../../types';
|
||||
|
||||
import styles from './index.module.less';
|
||||
|
||||
const SelectSubEditContext = createContext<{
|
||||
onChange?: (choice: InputComponentSelectOption) => void;
|
||||
onDelete?: (data: InputComponentSelectOption) => void;
|
||||
choiceLength?: number;
|
||||
}>({});
|
||||
|
||||
export const SelectSubEditComponent: FC<{
|
||||
config: InputConfigFe;
|
||||
onUpdate: (cfg: InputConfigFe) => void;
|
||||
}> = ({ config, onUpdate }) => (
|
||||
<div>
|
||||
<div className="flex ml-[8px] mt-[20px]">
|
||||
<span className="coz-fg-secondary text-[12px] font-medium leading-[16px]">
|
||||
{I18n.t('publish_base_inputFieldConfig_options')}
|
||||
</span>
|
||||
<Button
|
||||
icon={<IconCozPlus />}
|
||||
onClick={() => {
|
||||
onUpdate(
|
||||
produce<InputConfigFe>(cfg => {
|
||||
cfg.input_component.choice.push({
|
||||
name: '',
|
||||
id: nanoid(),
|
||||
});
|
||||
})(config),
|
||||
);
|
||||
}}
|
||||
color="secondary"
|
||||
size="small"
|
||||
className="ml-auto"
|
||||
>
|
||||
{I18n.t('Add_1')}
|
||||
</Button>
|
||||
</div>
|
||||
<SelectSubEditContext.Provider
|
||||
value={{
|
||||
onChange: choiceItem => {
|
||||
onUpdate(
|
||||
produce<InputConfigFe>(cfg => {
|
||||
const { choice } = cfg.input_component;
|
||||
const idx = choice.findIndex(i => i.id === choiceItem.id);
|
||||
choice.splice(idx, 1, choiceItem);
|
||||
})(config),
|
||||
);
|
||||
},
|
||||
choiceLength: config.input_component.choice?.length || 0,
|
||||
onDelete: delData => {
|
||||
onUpdate(
|
||||
produce<InputConfigFe>(cfg => {
|
||||
cfg.input_component.choice = cfg.input_component.choice.filter(
|
||||
it => it.id !== delData.id,
|
||||
);
|
||||
})(config),
|
||||
);
|
||||
},
|
||||
}}
|
||||
>
|
||||
<SortableFieldTable<InputComponentSelectOption>
|
||||
enabled={config.input_component.choice.length > 1}
|
||||
headless
|
||||
style={{
|
||||
padding: 0,
|
||||
}}
|
||||
headers={[
|
||||
{
|
||||
width: 192,
|
||||
name: '',
|
||||
required: false,
|
||||
},
|
||||
]}
|
||||
data={config.input_component.choice.map(data => ({
|
||||
data,
|
||||
deletable: false,
|
||||
lineStyle: {
|
||||
paddingRight: 0,
|
||||
paddingTop: 8,
|
||||
},
|
||||
getKey: it => it.id,
|
||||
bizComponent: SelectEditLine,
|
||||
}))}
|
||||
getId={data => data.data.id}
|
||||
onChange={data => {
|
||||
const choice = data.map(it => it.data);
|
||||
onUpdate(
|
||||
produce<InputConfigFe>(cfg => {
|
||||
cfg.input_component.choice = choice;
|
||||
})(config),
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</SelectSubEditContext.Provider>
|
||||
</div>
|
||||
);
|
||||
|
||||
const SelectEditLine: FC<{
|
||||
data: InputComponentSelectOption;
|
||||
}> = ({ data }) => {
|
||||
const { onChange, onDelete, choiceLength } = useContext(SelectSubEditContext);
|
||||
|
||||
if (choiceLength === undefined || !onChange || !onDelete) {
|
||||
throw new Error('impossible context member miss');
|
||||
}
|
||||
|
||||
return (
|
||||
<Input
|
||||
value={data.name}
|
||||
onChange={str => {
|
||||
onChange({
|
||||
id: data.id,
|
||||
name: str,
|
||||
});
|
||||
}}
|
||||
className={classNames(styles.input_deletable, 'w-full mr-[8px]')}
|
||||
suffix={
|
||||
choiceLength <= 1 ? null : (
|
||||
<Button
|
||||
color="secondary"
|
||||
onClick={() => onDelete(data)}
|
||||
icon={<IconCozTrashCan />}
|
||||
style={{
|
||||
width: 24,
|
||||
height: 24,
|
||||
minWidth: 0,
|
||||
padding: 0,
|
||||
}}
|
||||
></Button>
|
||||
)
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,386 @@
|
||||
/*
|
||||
* 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, type FC, useContext, useState } from 'react';
|
||||
|
||||
import { nanoid } from 'nanoid';
|
||||
import { produce } from 'immer';
|
||||
import classnames from 'classnames';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { IconCozInfoCircle } from '@coze-arch/coze-design/icons';
|
||||
import {
|
||||
Button,
|
||||
Input,
|
||||
InputNumber,
|
||||
Popover,
|
||||
Radio,
|
||||
RadioGroup,
|
||||
Select,
|
||||
Tag,
|
||||
Typography,
|
||||
} from '@coze-arch/coze-design';
|
||||
import {
|
||||
InputComponentType,
|
||||
type InputTypeInfo,
|
||||
} from '@coze-arch/bot-api/connector_api';
|
||||
|
||||
import { getIsSelectType } from '../../validate/utils';
|
||||
import { validateSingleInputFieldControl } from '../../validate';
|
||||
import { type InputConfigFe } from '../../types';
|
||||
import { type ConfigStoreState } from '../../store';
|
||||
import { MdTooltip } from '../../md-tooltip';
|
||||
import { useConfigStoreGuarded } from '../../context/store-context';
|
||||
import { INPUT_CONFIG_TEXT_MAX_CHAR } from '../../constants';
|
||||
import { BigCheckbox } from '../../big-checkbox';
|
||||
import { useRequireVerify } from './use-require-verify';
|
||||
import { RequiredWarn } from './required-warn';
|
||||
import { SelectSubEditComponent } from './input-config-line-select-edit';
|
||||
|
||||
export const inputFieldColumnWidth = {
|
||||
field: 110,
|
||||
title: 118,
|
||||
placeholder: 118,
|
||||
control: 118,
|
||||
required: 60,
|
||||
};
|
||||
|
||||
export interface InputComponentOption {
|
||||
label: string;
|
||||
value: InputComponentType;
|
||||
}
|
||||
|
||||
const USER_QUERY_FIELD_NAME = 'user_query';
|
||||
const INVALID_LINE_OPACITY = 0.3;
|
||||
|
||||
export const InputLineCommonContext = createContext<{
|
||||
onChange?: (val: InputConfigFe) => void;
|
||||
inputFieldsSelectorList?: InputTypeInfo[];
|
||||
inputOptions?: InputComponentOption[];
|
||||
onToggleError?: (id: string, error: boolean) => void;
|
||||
}>({});
|
||||
|
||||
const getInputConfig = (storeState: ConfigStoreState, id: string) => {
|
||||
const data = storeState.config?.input_config.find(item => item._id === id);
|
||||
if (!data) {
|
||||
throw new Error(`cannot find data of ${id}`);
|
||||
}
|
||||
return data;
|
||||
};
|
||||
|
||||
export const BaseInputFieldLine: FC<{
|
||||
data: InputConfigFe;
|
||||
// eslint-disable-next-line @coze-arch/max-line-per-function -- 鼠鼠我呀,也很无奈
|
||||
}> = ({ data: { _id: id } }) => {
|
||||
const { onChange, inputFieldsSelectorList, inputOptions, onToggleError } =
|
||||
useContext(InputLineCommonContext);
|
||||
const store = useConfigStoreGuarded();
|
||||
const data = useConfigStoreGuarded()(state => getInputConfig(state, id));
|
||||
if (!data) {
|
||||
throw new Error(`cannot find data of ${id}`);
|
||||
}
|
||||
if (
|
||||
!inputFieldsSelectorList ||
|
||||
!onChange ||
|
||||
!inputOptions ||
|
||||
!onToggleError
|
||||
) {
|
||||
throw new Error('impossible context member miss');
|
||||
}
|
||||
const changeByImmer = (updater: (sth: InputConfigFe) => void) => {
|
||||
onChange(produce<InputConfigFe>(updater)(data));
|
||||
};
|
||||
const isUserQuery = data.field === USER_QUERY_FIELD_NAME;
|
||||
|
||||
const [showPopover, setShowPopover] = useState(false);
|
||||
|
||||
const getVal = () => getInputConfig(store.getState(), id);
|
||||
const titleRequire = useRequireVerify({
|
||||
getVal,
|
||||
verify: config => !!config?.title,
|
||||
onChange: isError => onToggleError(`${id}#title`, isError),
|
||||
});
|
||||
const controlRequire = useRequireVerify({
|
||||
getVal,
|
||||
verify: config => !!config?.input_component.type,
|
||||
onChange: isError => onToggleError(`${id}#control`, isError),
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className="coz-fg-secondary text-[14px] leading-[20px] flex items-center"
|
||||
style={{
|
||||
width: inputFieldColumnWidth.field,
|
||||
opacity: data.invalid ? INVALID_LINE_OPACITY : 1,
|
||||
}}
|
||||
>
|
||||
<Typography.Text
|
||||
className={classnames('mr-[3px] coz-fg-secondary')}
|
||||
ellipsis={{ showTooltip: true }}
|
||||
>
|
||||
{data.field}
|
||||
</Typography.Text>
|
||||
{data.desc ? (
|
||||
<MdTooltip content={data.desc} tooltipPosition="right">
|
||||
<Button
|
||||
className="!w-[14px] !h-[16px] !min-w-0 !p-0"
|
||||
theme="borderless"
|
||||
type="secondary"
|
||||
color="secondary"
|
||||
icon={<IconCozInfoCircle className="text-[12px]" />}
|
||||
/>
|
||||
</MdTooltip>
|
||||
) : null}
|
||||
{data.invalid ? (
|
||||
<Tag color="primary" size="mini">
|
||||
{I18n.t('publish_base_configFields_invalid')}
|
||||
</Tag>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
width: inputFieldColumnWidth.title,
|
||||
position: 'relative',
|
||||
}}
|
||||
>
|
||||
<Input
|
||||
error={titleRequire.showWarn}
|
||||
onBlur={titleRequire.onTrigger}
|
||||
value={data.title}
|
||||
onChange={val =>
|
||||
changeByImmer(origin => {
|
||||
origin.title = val;
|
||||
})
|
||||
}
|
||||
placeholder={I18n.t('publish_base_configFields_title_placeholder')}
|
||||
disabled={data.invalid}
|
||||
maxLength={30}
|
||||
/>
|
||||
{titleRequire.showWarn ? <RequiredWarn /> : null}
|
||||
</div>
|
||||
|
||||
<Input
|
||||
value={data.placeholder}
|
||||
onChange={val =>
|
||||
changeByImmer(origin => {
|
||||
origin.placeholder = val;
|
||||
})
|
||||
}
|
||||
placeholder={I18n.t(
|
||||
'publish_base_configFields_placeholder_placeholder',
|
||||
)}
|
||||
style={{
|
||||
width: inputFieldColumnWidth.placeholder,
|
||||
}}
|
||||
disabled={data.invalid}
|
||||
maxLength={30}
|
||||
/>
|
||||
|
||||
<Popover
|
||||
visible={showPopover}
|
||||
trigger="custom"
|
||||
position="top"
|
||||
content={
|
||||
<InputFieldControlConfig
|
||||
originConfig={data}
|
||||
inputOptions={inputOptions}
|
||||
closePanel={() => {
|
||||
setShowPopover(false);
|
||||
controlRequire.onTrigger();
|
||||
}}
|
||||
onUpdate={(config: InputConfigFe) => {
|
||||
onChange(config);
|
||||
}}
|
||||
inputFieldsSelectorList={inputFieldsSelectorList}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
width: inputFieldColumnWidth.control,
|
||||
position: 'relative',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
onClick={() => {
|
||||
if (!data.invalid) {
|
||||
setShowPopover(true);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Select
|
||||
disabled={data.invalid}
|
||||
optionList={inputOptions}
|
||||
value={data.input_component.type}
|
||||
className="w-full"
|
||||
dropdownStyle={{
|
||||
display: 'none',
|
||||
}}
|
||||
renderOptionItem={() => null}
|
||||
placeholder={I18n.t(
|
||||
'publish_base_configFields_component_placeholder',
|
||||
)}
|
||||
hasError={controlRequire.showWarn}
|
||||
/>
|
||||
</div>
|
||||
{controlRequire.showWarn ? <RequiredWarn /> : null}
|
||||
</div>
|
||||
</Popover>
|
||||
|
||||
{data.invalid ? null : (
|
||||
<BigCheckbox
|
||||
style={{
|
||||
marginLeft: 'auto',
|
||||
}}
|
||||
checked={isUserQuery || data.required}
|
||||
disabled={isUserQuery}
|
||||
onChange={e => {
|
||||
const val = Boolean(e.target.checked);
|
||||
changeByImmer(cur => {
|
||||
cur.required = val;
|
||||
});
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const InputFieldControlConfig: FC<{
|
||||
onUpdate: (config: InputConfigFe) => void;
|
||||
inputOptions: InputComponentOption[];
|
||||
inputFieldsSelectorList: InputTypeInfo[];
|
||||
originConfig: InputConfigFe;
|
||||
closePanel: () => void;
|
||||
}> = ({
|
||||
inputOptions,
|
||||
onUpdate: submitSubConfig,
|
||||
originConfig,
|
||||
closePanel,
|
||||
inputFieldsSelectorList,
|
||||
}) => {
|
||||
const [config, setConfig] = useState(() => originConfig);
|
||||
const fieldsSelectorOptions = inputFieldsSelectorList.map(opt => ({
|
||||
value: opt.id,
|
||||
label: opt.name,
|
||||
}));
|
||||
return (
|
||||
<div className="pl-[12px] pb-[16px]">
|
||||
<div className="overflow-y-auto max-h-[320px] pt-[12px] pr-[12px]">
|
||||
<div className="ml-[8px]">
|
||||
<p className="coz-fg-secondary text-[12px] font-medium leading-[16px]">
|
||||
{I18n.t('publish_base_configFields_component')}
|
||||
</p>
|
||||
</div>
|
||||
<RadioGroup
|
||||
className="mx-[8px] mt-[14px] grid grid-cols-2 gap-[12px]"
|
||||
defaultValue={config.input_component.type}
|
||||
onChange={val => {
|
||||
const type = val.target.value as InputComponentType;
|
||||
setConfig(
|
||||
produce<InputConfigFe>(curConfig => {
|
||||
curConfig.input_component.type = type;
|
||||
if (
|
||||
getIsSelectType(type) &&
|
||||
!curConfig.input_component.choice?.length
|
||||
) {
|
||||
curConfig.input_component.choice.push({
|
||||
name: '',
|
||||
id: nanoid(),
|
||||
});
|
||||
}
|
||||
}),
|
||||
);
|
||||
}}
|
||||
>
|
||||
{inputOptions.map(option => (
|
||||
<Radio key={option.value} value={option.value} className="">
|
||||
{option.label}
|
||||
</Radio>
|
||||
))}
|
||||
</RadioGroup>
|
||||
{config.input_component.type === InputComponentType.Text ? (
|
||||
<div className="ml-[8px] mt-[20px]">
|
||||
<div className="coz-fg-secondary text-[12px] font-medium leading-[16px]">
|
||||
{I18n.t('publish_base_inputFieldConfig_maxChars')}
|
||||
</div>
|
||||
<InputNumber
|
||||
style={{
|
||||
marginTop: 6,
|
||||
}}
|
||||
defaultValue={config.input_component.max_char}
|
||||
max={INPUT_CONFIG_TEXT_MAX_CHAR}
|
||||
min={config.required ? 1 : 0}
|
||||
onChange={val => {
|
||||
setConfig(
|
||||
produce<InputConfigFe>(curConfig => {
|
||||
curConfig.input_component.max_char = Number(val);
|
||||
}),
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
{getIsSelectType(config.input_component.type) ? (
|
||||
<SelectSubEditComponent config={config} onUpdate={setConfig} />
|
||||
) : null}
|
||||
{config.input_component.type === InputComponentType.FieldSelector ? (
|
||||
<div>
|
||||
<div className="flex ml-[8px] mt-[20px]">
|
||||
<span className="coz-fg-secondary text-[12px] font-medium leading-[16px]">
|
||||
{I18n.t('publish_base_inputFieldConfig_supports')}
|
||||
</span>
|
||||
</div>
|
||||
<Select
|
||||
style={{
|
||||
width: 256,
|
||||
marginTop: 6,
|
||||
}}
|
||||
optionList={fieldsSelectorOptions}
|
||||
multiple
|
||||
defaultValue={config.input_component.supported_type}
|
||||
maxTagCount={2}
|
||||
expandRestTagsOnClick
|
||||
onChange={valRaw => {
|
||||
const val = valRaw as number[];
|
||||
setConfig(
|
||||
produce<InputConfigFe>(curConfig => {
|
||||
curConfig.input_component.supported_type = val;
|
||||
}),
|
||||
);
|
||||
}}
|
||||
></Select>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="flex gap-[8px] items-center mt-[24px] mr-[12px]">
|
||||
<Button color="primary" onClick={closePanel} className="ml-auto">
|
||||
{I18n.t('Cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
disabled={!validateSingleInputFieldControl(config)}
|
||||
onClick={() => {
|
||||
submitSubConfig(config);
|
||||
closePanel();
|
||||
}}
|
||||
>
|
||||
{I18n.t('Confirm')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,234 @@
|
||||
/*
|
||||
* 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 FC, createContext, useContext, useEffect } from 'react';
|
||||
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { Input, Select } from '@coze-arch/coze-design';
|
||||
import { type OutputTypeInfo } from '@coze-arch/bot-api/connector_api';
|
||||
|
||||
import { getIsNumberOutput, getIsTextOutput } from '../../validate/utils';
|
||||
import { type BaseOutputStructLineType } from '../../types';
|
||||
import type { ConfigStoreState } from '../../store';
|
||||
import { useConfigStoreGuarded } from '../../context/store-context';
|
||||
import { BigCheckbox } from '../../big-checkbox';
|
||||
import { useRequireVerify } from './use-require-verify';
|
||||
import { RequiredWarn } from './required-warn';
|
||||
|
||||
export const OutputLineCommonContext = createContext<{
|
||||
onChange?: (val: BaseOutputStructLineType) => void;
|
||||
list?: OutputTypeInfo[];
|
||||
getShowRequireWarn?: (val: BaseOutputStructLineType) => OutputStructVerifyRes;
|
||||
onToggleError?: (id: string, error: boolean) => void;
|
||||
}>({});
|
||||
|
||||
export interface OutputStructVerifyRes {
|
||||
groupByKey: {
|
||||
warn: boolean;
|
||||
tip?: string;
|
||||
};
|
||||
primary: {
|
||||
warn: boolean;
|
||||
tip?: string;
|
||||
};
|
||||
}
|
||||
const getOutputFieldConfig = (storeState: ConfigStoreState, id: string) => {
|
||||
const data = storeState.config?.output_sub_component.item_list?.find(
|
||||
item => item._id === id,
|
||||
);
|
||||
if (!data) {
|
||||
throw new Error(`cannot find data of ${id}`);
|
||||
}
|
||||
return data;
|
||||
};
|
||||
|
||||
export const BaseOutputStructLine: FC<{
|
||||
data: BaseOutputStructLineType;
|
||||
// eslint-disable-next-line @coze-arch/max-line-per-function -- /
|
||||
}> = ({ data: { _id: id } }) => {
|
||||
const { list, onChange, getShowRequireWarn, onToggleError } = useContext(
|
||||
OutputLineCommonContext,
|
||||
);
|
||||
const store = useConfigStoreGuarded();
|
||||
const data = useConfigStoreGuarded()(state =>
|
||||
getOutputFieldConfig(state, id),
|
||||
);
|
||||
if (!data) {
|
||||
throw new Error(`cannot find data of ${id}`);
|
||||
}
|
||||
if (!list || !onChange || !getShowRequireWarn || !onToggleError) {
|
||||
throw new Error('impossible context member miss');
|
||||
}
|
||||
|
||||
const { groupByKey: groupByKeyRequire, primary: primaryRequire } =
|
||||
getShowRequireWarn(data);
|
||||
|
||||
const getVal = () => getOutputFieldConfig(store.getState(), id);
|
||||
const keyRequire = useRequireVerify({
|
||||
getVal,
|
||||
verify: config => !!config?.key,
|
||||
onChange: isError => onToggleError(`${id}#key`, isError),
|
||||
});
|
||||
const typeRequire = useRequireVerify({
|
||||
getVal,
|
||||
verify: config => Number.isInteger(config.output_type),
|
||||
onChange: isError => onToggleError(`${id}$type`, isError),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const hasError = groupByKeyRequire.warn || primaryRequire.warn;
|
||||
onToggleError(data._id, hasError);
|
||||
}, [groupByKeyRequire.warn, primaryRequire.warn]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
style={{
|
||||
width: outputStructColumnWidth.key,
|
||||
margin: '6px 0',
|
||||
position: 'relative',
|
||||
}}
|
||||
>
|
||||
<Input
|
||||
error={keyRequire.showWarn}
|
||||
value={data.key}
|
||||
onBlur={keyRequire.onTrigger}
|
||||
placeholder={I18n.t('publish_base_configFields_key_placeholder')}
|
||||
onChange={val => {
|
||||
onChange({
|
||||
...data,
|
||||
key: val,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
{keyRequire.showWarn ? <RequiredWarn /> : null}
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
width: outputStructColumnWidth.outputType,
|
||||
position: 'relative',
|
||||
}}
|
||||
>
|
||||
<Select
|
||||
defaultValue={data.output_type}
|
||||
optionList={list.map(info => ({
|
||||
value: info.id,
|
||||
label: info.name,
|
||||
}))}
|
||||
placeholder={I18n.t('publish_base_configFields_dataType_placeholder')}
|
||||
onBlur={typeRequire.onTrigger}
|
||||
onChange={val => {
|
||||
onChange({
|
||||
...data,
|
||||
output_type: Number(val),
|
||||
});
|
||||
typeRequire.onTrigger();
|
||||
}}
|
||||
hasError={typeRequire.showWarn}
|
||||
style={{
|
||||
width: '100%',
|
||||
}}
|
||||
/>
|
||||
{typeRequire.showWarn ? <RequiredWarn /> : null}
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
width: outputStructColumnWidth.groupByKey,
|
||||
position: 'relative',
|
||||
}}
|
||||
>
|
||||
<BigCheckbox
|
||||
checked={data.is_group_by_key}
|
||||
/**
|
||||
* is_group_by_key: 只允许提交 text 类型
|
||||
* 可以切换场景:
|
||||
* 1. 已经勾选:任意类型
|
||||
* 2. 未勾选:仅 text 类型
|
||||
*/
|
||||
disabled={
|
||||
!(data.is_group_by_key || getIsTextOutput(data.output_type))
|
||||
}
|
||||
isError={groupByKeyRequire.warn}
|
||||
onChange={e => {
|
||||
const val = Boolean(e.target.checked);
|
||||
onChange({
|
||||
...data,
|
||||
is_group_by_key: val,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
{groupByKeyRequire.warn ? (
|
||||
<RequiredWarn
|
||||
text={groupByKeyRequire.tip}
|
||||
style={{
|
||||
marginLeft: 0,
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
width: outputStructColumnWidth.primary,
|
||||
position: 'relative',
|
||||
}}
|
||||
>
|
||||
<BigCheckbox
|
||||
checked={data.is_primary}
|
||||
isError={primaryRequire.warn}
|
||||
/**
|
||||
* is_primary: 只允许提交 text 或 number 类型
|
||||
* 可以切换场景:
|
||||
* 1. 已经勾选:任意类型
|
||||
* 2. 未勾选:仅 text 与 number 类型
|
||||
*/
|
||||
disabled={
|
||||
!(
|
||||
data.is_primary ||
|
||||
getIsNumberOutput(data.output_type) ||
|
||||
getIsTextOutput(data.output_type)
|
||||
)
|
||||
}
|
||||
onChange={e => {
|
||||
const val = Boolean(e.target.checked);
|
||||
onChange({
|
||||
...data,
|
||||
is_primary: val,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
{primaryRequire.warn ? (
|
||||
<RequiredWarn
|
||||
text={primaryRequire.tip}
|
||||
style={{
|
||||
marginLeft: 0,
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const FIRST_TWO_COLUMN_TRANSFER_SPACE = 30;
|
||||
// 总和为 566,滚动条留 8,左侧拖拽按钮 16,gap 8 * 4, 删除按钮 24
|
||||
// (566 - 8 - 16 - 4 * 8 - 24 - (44 + 96)) / 2 = 173
|
||||
export const outputStructColumnWidth = {
|
||||
key: 173 + FIRST_TWO_COLUMN_TRANSFER_SPACE,
|
||||
outputType: 173 - FIRST_TWO_COLUMN_TRANSFER_SPACE,
|
||||
groupByKey: 44,
|
||||
// 给国际化预留一些宽度
|
||||
primary: 96,
|
||||
};
|
||||
@@ -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,
|
||||
type FC,
|
||||
type PropsWithChildren,
|
||||
useContext,
|
||||
useRef,
|
||||
} from 'react';
|
||||
|
||||
interface FieldsRequireCenter {
|
||||
verifyFns: Set<() => void>;
|
||||
triggerAllVerify: () => void;
|
||||
registerVerifyFn: (fn: () => void) => () => void;
|
||||
}
|
||||
|
||||
const FieldsRequireCenterContext = createContext<FieldsRequireCenter>({
|
||||
verifyFns: new Set(),
|
||||
triggerAllVerify: () => undefined,
|
||||
registerVerifyFn: () => () => undefined,
|
||||
});
|
||||
|
||||
export const FieldsRequireCenterWrapper: FC<PropsWithChildren> = ({
|
||||
children,
|
||||
}) => {
|
||||
const fns = useRef(new Set<() => void>());
|
||||
|
||||
return (
|
||||
<FieldsRequireCenterContext.Provider
|
||||
value={{
|
||||
verifyFns: fns.current,
|
||||
triggerAllVerify: () => {
|
||||
fns.current.forEach(fn => fn());
|
||||
},
|
||||
registerVerifyFn: fn => {
|
||||
fns.current.add(fn);
|
||||
|
||||
return () => {
|
||||
fns.current.delete(fn);
|
||||
};
|
||||
},
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</FieldsRequireCenterContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
FieldsRequireCenterWrapper.displayName = 'FieldsRequireCenterWrapper';
|
||||
|
||||
export const useRequireVerifyCenter = (): Omit<
|
||||
FieldsRequireCenter,
|
||||
'verifyFns'
|
||||
> => useContext(FieldsRequireCenterContext);
|
||||
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
* 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 CSSProperties, type FC } from 'react';
|
||||
|
||||
import { merge } from 'lodash-es';
|
||||
import classNames from 'classnames';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
|
||||
import { ERROR_LINE_HEIGHT } from '../../constants';
|
||||
|
||||
export const RequiredWarn: FC<{
|
||||
text?: string;
|
||||
className?: string;
|
||||
style?: CSSProperties;
|
||||
absolute?: boolean;
|
||||
}> = props => {
|
||||
const { text, style, className, absolute = true } = props;
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
className,
|
||||
'coz-fg-hglt-red text-[10px]',
|
||||
'ml-[8px]',
|
||||
'whitespace-nowrap',
|
||||
absolute ? 'absolute' : '',
|
||||
)}
|
||||
style={merge(
|
||||
{
|
||||
lineHeight: `${ERROR_LINE_HEIGHT}px`,
|
||||
},
|
||||
style,
|
||||
)}
|
||||
>
|
||||
{text || I18n.t('publish_base_configFields_requiredWarn')}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -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 { useEffect, useState } from 'react';
|
||||
|
||||
import { useRequireVerifyCenter } from './require-verify-center';
|
||||
|
||||
export const useRequireVerify = <T>({
|
||||
getVal,
|
||||
verify,
|
||||
onChange,
|
||||
}: {
|
||||
getVal: () => T;
|
||||
verify: (val: T) => boolean;
|
||||
onChange?: (isError: boolean) => void;
|
||||
}) => {
|
||||
const [showWarn, setShowWarn] = useState(false);
|
||||
const { registerVerifyFn } = useRequireVerifyCenter();
|
||||
|
||||
const onTrigger = () => {
|
||||
const val = getVal();
|
||||
const verified = verify(val);
|
||||
const isError = !verified;
|
||||
setShowWarn(isError);
|
||||
onChange?.(isError);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const unregister = registerVerifyFn(onTrigger);
|
||||
return unregister;
|
||||
}, []);
|
||||
|
||||
return {
|
||||
showWarn,
|
||||
onTrigger,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,80 @@
|
||||
/*
|
||||
* 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 CSSProperties, type FC, type ReactNode } from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { IconCozInfoCircle } from '@coze-arch/coze-design/icons';
|
||||
import { Tooltip } from '@coze-arch/coze-design';
|
||||
|
||||
import { MdTooltip } from '../md-tooltip';
|
||||
|
||||
export const FormTitle: FC<{
|
||||
title: string;
|
||||
tooltip?: string;
|
||||
style?: CSSProperties;
|
||||
required?: boolean;
|
||||
}> = ({ title, style, tooltip, required }) => (
|
||||
<p
|
||||
style={style}
|
||||
className={classNames(
|
||||
'text-[16px]',
|
||||
'coz-fg-plus',
|
||||
'font-medium',
|
||||
'leading-[22px]',
|
||||
'flex',
|
||||
'items-center',
|
||||
)}
|
||||
>
|
||||
<span>{title}</span>
|
||||
{required ? (
|
||||
<i className="coz-fg-hglt-red text-[12px] font-medium">*</i>
|
||||
) : null}
|
||||
{tooltip ? (
|
||||
<Tooltip content={tooltip}>
|
||||
<span className="cursor-pointer ml-[4px] h-[22px] flex items-center">
|
||||
<IconCozInfoCircle className="text-[14px] coz-fg-secondary" />
|
||||
</span>
|
||||
</Tooltip>
|
||||
) : null}
|
||||
</p>
|
||||
);
|
||||
|
||||
export const FormSubtitle: FC<{
|
||||
title: string;
|
||||
required: boolean;
|
||||
tooltip?: string;
|
||||
style?: CSSProperties;
|
||||
suffix?: ReactNode;
|
||||
}> = ({ title, required, tooltip, style, suffix }) => (
|
||||
<p
|
||||
className={classNames('flex', 'justify-start', 'items-center')}
|
||||
style={style}
|
||||
>
|
||||
<span className="text-[12px] coz-fg-secondary leading-[16px] font-medium">
|
||||
{title}
|
||||
</span>
|
||||
{required ? (
|
||||
<i className="coz-fg-hglt-red text-[12px] font-medium">*</i>
|
||||
) : null}
|
||||
<MdTooltip content={tooltip}>
|
||||
<span className="cursor-pointer ml-[4px] h-[16px] flex items-center">
|
||||
<IconCozInfoCircle className="text-[12px] coz-fg-secondary" />
|
||||
</span>
|
||||
</MdTooltip>
|
||||
{suffix}
|
||||
</p>
|
||||
);
|
||||
@@ -0,0 +1,10 @@
|
||||
.output_config {
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: var(--coz-fg-dim);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 7px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,403 @@
|
||||
/*
|
||||
* 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 ReactMarkdown from 'react-markdown';
|
||||
import {
|
||||
forwardRef,
|
||||
useImperativeHandle,
|
||||
useRef,
|
||||
useState,
|
||||
memo,
|
||||
type FC,
|
||||
} from 'react';
|
||||
|
||||
import { nanoid } from 'nanoid';
|
||||
import { cloneDeep, omit } from 'lodash-es';
|
||||
import { useRequest } from 'ahooks';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import {
|
||||
IconCozCross,
|
||||
IconCozLongArrowUp,
|
||||
} from '@coze-arch/coze-design/icons';
|
||||
import {
|
||||
Button,
|
||||
IconButton,
|
||||
Modal,
|
||||
Spin,
|
||||
Tag,
|
||||
Toast,
|
||||
} from '@coze-arch/coze-design';
|
||||
import { useSpaceStore } from '@coze-arch/bot-studio-store';
|
||||
import { type PublishConnectorInfo } from '@coze-arch/bot-api/developer_api';
|
||||
import {
|
||||
type FeishuBaseConfig,
|
||||
type InputComponent,
|
||||
type InputConfig,
|
||||
type OutputSubComponent,
|
||||
type OutputSubComponentItem,
|
||||
} from '@coze-arch/bot-api/connector_api';
|
||||
import { connectorApi, DeveloperApi } from '@coze-arch/bot-api';
|
||||
|
||||
import { OUTPUT_TYPE_TEXT } from '../validate/utils';
|
||||
import { useSubscribeAndUpdateConfig } from '../validate/field-interaction';
|
||||
import { validateFullConfig } from '../validate';
|
||||
import {
|
||||
type FeishuBaseConfigFe,
|
||||
type InputComponentFe,
|
||||
type InputConfigFe,
|
||||
type OutputSubComponentFe,
|
||||
type SaveConfigPayload,
|
||||
} from '../types';
|
||||
import { type ConfigStore, createConfigStore } from '../store';
|
||||
import { LoadFailedDisplay } from '../expection-display';
|
||||
import { StoreContext, useConfigAsserted } from '../context/store-context';
|
||||
import { StepIndicator } from './step-indicator';
|
||||
import { FormSubtitle, FormTitle } from './form-title';
|
||||
import {
|
||||
FieldsRequireCenterWrapper,
|
||||
useRequireVerifyCenter,
|
||||
} from './field-line/require-verify-center';
|
||||
import { BaseOutputFieldsTable } from './base-output-fields-table';
|
||||
import { BaseInputFieldsTable } from './base-input-fields-table';
|
||||
export const JumpButton: FC<{
|
||||
url: string;
|
||||
completed: boolean;
|
||||
}> = ({ url, completed }) => (
|
||||
<Button
|
||||
color="secondary"
|
||||
onClick={() => {
|
||||
window.open(url);
|
||||
}}
|
||||
icon={<IconCozLongArrowUp className="rotate-45" />}
|
||||
iconPosition="right"
|
||||
size="small"
|
||||
className={!completed ? '!coz-fg-hglt' : ''}
|
||||
>
|
||||
{!completed
|
||||
? I18n.t('publish_base_configFields_complete_Information_fill_out')
|
||||
: I18n.t('publish_base_configFields_complete_Information_edit')}
|
||||
</Button>
|
||||
);
|
||||
export const FeishuBaseModal = memo(
|
||||
forwardRef<
|
||||
{
|
||||
openModal: () => void;
|
||||
},
|
||||
{
|
||||
botId: string;
|
||||
record: PublishConnectorInfo;
|
||||
onSaved: (id: string) => void;
|
||||
}
|
||||
>(({ botId, record, onSaved }, ref) => {
|
||||
const [showModal, setShowModal] = useState(false);
|
||||
useImperativeHandle(ref, () => ({
|
||||
openModal: () => {
|
||||
setShowModal(true);
|
||||
run();
|
||||
},
|
||||
}));
|
||||
const storeRef = useRef<ConfigStore | null>(null);
|
||||
|
||||
const formRef = useRef<{
|
||||
configFormSubmit: () => void;
|
||||
} | null>(null);
|
||||
|
||||
if (!storeRef.current) {
|
||||
storeRef.current = createConfigStore();
|
||||
}
|
||||
useSubscribeAndUpdateConfig(storeRef.current);
|
||||
|
||||
const { data, loading, run, mutate, cancel, error } = useRequest(
|
||||
async () => {
|
||||
const { config } = await connectorApi.GetFeishuBaseConfig({
|
||||
bot_id: botId,
|
||||
});
|
||||
if (!config) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return convertBaseConfig(config);
|
||||
},
|
||||
{
|
||||
manual: true,
|
||||
onSuccess: res => {
|
||||
if (!res) {
|
||||
return;
|
||||
}
|
||||
storeRef.current?.getState().setConfig(res);
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const hideModalAndClearData = () => {
|
||||
setShowModal(false);
|
||||
cancel();
|
||||
mutate();
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
visible={showModal}
|
||||
onCancel={hideModalAndClearData}
|
||||
closeOnEsc={false}
|
||||
maskClosable={false}
|
||||
footer={
|
||||
<Button
|
||||
color="hgltplus"
|
||||
size="default"
|
||||
onClick={() => {
|
||||
formRef.current?.configFormSubmit();
|
||||
}}
|
||||
>
|
||||
{I18n.t('Confirm')}
|
||||
</Button>
|
||||
}
|
||||
size="large"
|
||||
linearGradientMask
|
||||
header={
|
||||
<div className="flex items-center justify-between h-[40px]">
|
||||
<span className="text-[20px] font-medium leading-[28px] coz-fg-primary">
|
||||
{I18n.t('publish_base_config_configFeishuBase')}
|
||||
</span>
|
||||
<IconButton
|
||||
onClick={hideModalAndClearData}
|
||||
icon={<IconCozCross className="text-[18px]" />}
|
||||
className="w-[40px] !h-[40px] -pr-2"
|
||||
color="secondary"
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Spin spinning={loading}>
|
||||
{data?.description ? (
|
||||
<ReactMarkdown
|
||||
linkTarget="_blank"
|
||||
className="coz-fg-secondary text-[14px] leading-[20px]"
|
||||
>
|
||||
{data.description}
|
||||
</ReactMarkdown>
|
||||
) : null}
|
||||
{data ? (
|
||||
<StoreContext.Provider value={{ store: storeRef.current }}>
|
||||
<FieldsRequireCenterWrapper>
|
||||
<ConfigForm
|
||||
ref={formRef}
|
||||
record={record}
|
||||
botId={botId}
|
||||
onSaved={() => {
|
||||
onSaved(record.id);
|
||||
setShowModal(false);
|
||||
}}
|
||||
/>
|
||||
</FieldsRequireCenterWrapper>
|
||||
</StoreContext.Provider>
|
||||
) : (
|
||||
<div className="h-[60px]" />
|
||||
)}
|
||||
{!data && error ? <LoadFailedDisplay /> : null}
|
||||
</Spin>
|
||||
</Modal>
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
const ConfigForm = forwardRef<
|
||||
{ configFormSubmit: () => void },
|
||||
{
|
||||
botId: string;
|
||||
record: PublishConnectorInfo;
|
||||
onSaved: () => void;
|
||||
}
|
||||
>(({ botId, record, onSaved }, ref) => {
|
||||
const config = useConfigAsserted();
|
||||
const { input_desc, output_desc, to_complete_info } = config;
|
||||
const { url = '', completed = false } = to_complete_info ?? {};
|
||||
const couldSubmit = validateFullConfig(config);
|
||||
const { triggerAllVerify } = useRequireVerifyCenter();
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
configFormSubmit: () => {
|
||||
triggerAllVerify();
|
||||
if (!couldSubmit) {
|
||||
Toast.error({
|
||||
content: I18n.t('publish_base_configFields_ unfinished_toast'),
|
||||
});
|
||||
return;
|
||||
}
|
||||
submitConfig();
|
||||
},
|
||||
}));
|
||||
|
||||
const { run: submitConfig } = useRequest(
|
||||
() => {
|
||||
const spaceId = useSpaceStore.getState().getSpaceId();
|
||||
return DeveloperApi.BindConnector({
|
||||
space_id: spaceId,
|
||||
bot_id: botId,
|
||||
connector_id: record.id,
|
||||
connector_info: {
|
||||
config: JSON.stringify(getSubmitPayload(config)),
|
||||
},
|
||||
});
|
||||
},
|
||||
{
|
||||
manual: true,
|
||||
onSuccess: () => {
|
||||
Toast.success({
|
||||
content: I18n.t('Save_success'),
|
||||
});
|
||||
onSaved();
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="mt-[28px] pb-[32px]">
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<StepIndicator number={1} />
|
||||
<FormTitle title={I18n.t('publish_base_config_configBaseInfo')} />
|
||||
</div>
|
||||
|
||||
<FormSubtitle
|
||||
required
|
||||
title={I18n.t('publish_base_config_configOutputType')}
|
||||
tooltip={output_desc}
|
||||
style={{
|
||||
marginTop: 9,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<BaseOutputFieldsTable config={config} />
|
||||
<div className="mt-[32px]">
|
||||
<div className="flex items-center gap-2">
|
||||
<StepIndicator number={2} />
|
||||
<FormTitle
|
||||
title={I18n.t('publish_base_configFields')}
|
||||
tooltip={input_desc}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<BaseInputFieldsTable />
|
||||
|
||||
{to_complete_info ? (
|
||||
<>
|
||||
<div className="flex items-center gap-2 mt-6">
|
||||
<StepIndicator number={3} />
|
||||
<FormTitle
|
||||
title={I18n.t(
|
||||
'publish_base_configFields_complete_Information_title',
|
||||
)}
|
||||
required
|
||||
/>
|
||||
{completed ? (
|
||||
<>
|
||||
<Tag color="green">
|
||||
{I18n.t('publish_base_configFields_status_completed')}
|
||||
</Tag>
|
||||
<JumpButton completed={completed} url={url} />
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
{!completed ? (
|
||||
<div className="mt-[6px] flex items-center">
|
||||
{I18n.t(
|
||||
'publish_base_configFields_complete_Information_describe',
|
||||
)}
|
||||
<JumpButton completed={completed} url={url} />
|
||||
</div>
|
||||
) : null}
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
const getSubmitPayload = (config: FeishuBaseConfigFe): SaveConfigPayload => {
|
||||
const res: SaveConfigPayload = cloneDeep({
|
||||
output_type: config.output_type,
|
||||
input_config: config.input_config.map(cfg => {
|
||||
const inputConfig: InputConfig = {
|
||||
...cfg,
|
||||
input_component: reverseInputComponent(cfg.input_component),
|
||||
};
|
||||
return omit(inputConfig, '_id');
|
||||
}),
|
||||
output_sub_component: reverseOutputSubComponent(
|
||||
config.output_sub_component,
|
||||
),
|
||||
});
|
||||
res.output_sub_component.item_list = (
|
||||
res.output_sub_component.item_list || []
|
||||
).map(cfg => omit(cfg, '_id'));
|
||||
return res;
|
||||
};
|
||||
|
||||
const reverseOutputSubComponent = (
|
||||
output: OutputSubComponentFe,
|
||||
): OutputSubComponent => ({
|
||||
...output,
|
||||
item_list: (output.item_list || []).map(item => {
|
||||
const res: OutputSubComponentItem = {
|
||||
...omit(item, '_id'),
|
||||
output_type: item.output_type ?? OUTPUT_TYPE_TEXT,
|
||||
};
|
||||
return res;
|
||||
}),
|
||||
});
|
||||
|
||||
const convertInputComponent = (cfg: InputComponent): InputComponentFe => {
|
||||
const { choice } = cfg;
|
||||
const res: InputComponentFe = {
|
||||
...cfg,
|
||||
choice: (choice || []).map(c => ({
|
||||
name: c,
|
||||
id: nanoid(),
|
||||
})),
|
||||
};
|
||||
return res;
|
||||
};
|
||||
|
||||
const reverseInputComponent = (cfg: InputComponentFe): InputComponent => {
|
||||
const { choice } = cfg;
|
||||
const res: InputComponent = {
|
||||
...cfg,
|
||||
choice: choice.map(c => c.name),
|
||||
};
|
||||
return res;
|
||||
};
|
||||
|
||||
const convertBaseConfig = (config: FeishuBaseConfig): FeishuBaseConfigFe => {
|
||||
const configFe: FeishuBaseConfigFe = {
|
||||
...config,
|
||||
output_sub_component: {
|
||||
...config.output_sub_component,
|
||||
item_list: (config.output_sub_component.item_list || []).map(item => ({
|
||||
...item,
|
||||
_id: nanoid(),
|
||||
})),
|
||||
},
|
||||
input_config: config.input_config?.map(cfg => {
|
||||
const res: InputConfigFe = {
|
||||
...cfg,
|
||||
input_component: convertInputComponent(cfg.input_component),
|
||||
_id: nanoid(),
|
||||
};
|
||||
return res;
|
||||
}),
|
||||
};
|
||||
return configFe;
|
||||
};
|
||||
@@ -0,0 +1,240 @@
|
||||
/*
|
||||
* 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 ComponentType,
|
||||
type CSSProperties,
|
||||
type FC,
|
||||
forwardRef,
|
||||
type JSX,
|
||||
type PropsWithChildren,
|
||||
type ReactElement,
|
||||
type ReactNode,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
} from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { SortableList } from '@coze-studio/components/sortable-list';
|
||||
import { type ITemRenderProps, type ConnectDnd } from '@coze-studio/components';
|
||||
import { IconCozHandle, IconCozTrashCan } from '@coze-arch/coze-design/icons';
|
||||
import { Button } from '@coze-arch/coze-design';
|
||||
|
||||
import { MdTooltip } from '../md-tooltip';
|
||||
|
||||
export interface HeaderItem {
|
||||
name: string;
|
||||
required: boolean;
|
||||
width: number;
|
||||
tooltip?: string;
|
||||
style?: CSSProperties;
|
||||
}
|
||||
|
||||
export interface SortableFieldTableMethod {
|
||||
addRow: () => boolean;
|
||||
}
|
||||
|
||||
export interface IData<Data extends object> {
|
||||
data: Data;
|
||||
deletable: boolean;
|
||||
getKey: (data: Data) => string;
|
||||
onDelete?: (data: Data) => void;
|
||||
bizComponent: ComponentType<{ data: Data }>;
|
||||
lineStyle?: CSSProperties;
|
||||
deleteButtonStyle?: CSSProperties;
|
||||
}
|
||||
|
||||
interface SortableFieldTableProps<Data extends object> {
|
||||
className?: string;
|
||||
headers: HeaderItem[];
|
||||
data: IData<Data>[];
|
||||
getId: (data: IData<Data>) => string;
|
||||
onChange: (data: IData<Data>[]) => void;
|
||||
headless?: boolean;
|
||||
style?: CSSProperties;
|
||||
enabled: boolean;
|
||||
linesWrapper?: ComponentType;
|
||||
}
|
||||
|
||||
const DefaultLinesWrapper: FC<PropsWithChildren> = ({ children }) => (
|
||||
<>{children}</>
|
||||
);
|
||||
|
||||
export const SortableFieldTable = <T extends object>({
|
||||
className,
|
||||
headers,
|
||||
data,
|
||||
getId,
|
||||
onChange,
|
||||
headless,
|
||||
enabled,
|
||||
style,
|
||||
linesWrapper,
|
||||
}: SortableFieldTableProps<T>): ReactElement => {
|
||||
const uniqueSymbol = useMemo(() => Symbol(), []);
|
||||
const LinesWrapper = linesWrapper || DefaultLinesWrapper;
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
headless ? '' : 'coz-bg-primary',
|
||||
headless ? '' : 'coz-stroke-primary border-solid border-[1px]',
|
||||
'px-[12px] pt-[12px] pb-[12px]',
|
||||
'rounded-[8px]',
|
||||
className,
|
||||
)}
|
||||
style={style}
|
||||
>
|
||||
{headless ? null : <FieldTableHeader headers={headers} />}
|
||||
<LinesWrapper>
|
||||
<SortableList
|
||||
enabled={enabled}
|
||||
getId={getId}
|
||||
list={data}
|
||||
onChange={onChange}
|
||||
itemRender={ItemRender as never}
|
||||
type={uniqueSymbol}
|
||||
/>
|
||||
</LinesWrapper>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const ItemRender = <Data extends object>(
|
||||
props: ITemRenderProps<IData<Data>>,
|
||||
): JSX.Element => {
|
||||
const { data: bizProps } = props;
|
||||
const BizComponent = bizProps.bizComponent;
|
||||
return (
|
||||
<FieldSortLine
|
||||
gap={8}
|
||||
connect={props.connect}
|
||||
deletable={bizProps.deletable}
|
||||
onDelete={() => bizProps.onDelete?.(bizProps.data)}
|
||||
style={bizProps.lineStyle}
|
||||
deleteButtonStyle={bizProps.deleteButtonStyle}
|
||||
>
|
||||
<BizComponent key={bizProps.getKey(bizProps.data)} data={bizProps.data} />
|
||||
</FieldSortLine>
|
||||
);
|
||||
};
|
||||
|
||||
const TableFieldLine = forwardRef<
|
||||
HTMLDivElement,
|
||||
PropsWithChildren<{
|
||||
className?: string;
|
||||
gap?: number;
|
||||
prefix?: ReactNode;
|
||||
style?: CSSProperties;
|
||||
}>
|
||||
>(({ children, className, gap, prefix, style }, ref) => (
|
||||
<div
|
||||
className={classNames(className, 'flex items-center')}
|
||||
ref={ref}
|
||||
style={style}
|
||||
>
|
||||
{prefix}
|
||||
<div
|
||||
className="flex items-center w-full"
|
||||
style={{
|
||||
gap,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
));
|
||||
|
||||
TableFieldLine.displayName = 'TableFieldLine';
|
||||
|
||||
const FieldSortLine: FC<
|
||||
PropsWithChildren<{
|
||||
deletable?: boolean;
|
||||
connect: ConnectDnd;
|
||||
gap?: number;
|
||||
style?: CSSProperties;
|
||||
deleteButtonStyle?: CSSProperties;
|
||||
onDelete: () => void;
|
||||
}>
|
||||
> = ({
|
||||
children,
|
||||
deletable,
|
||||
connect,
|
||||
gap,
|
||||
style,
|
||||
onDelete,
|
||||
deleteButtonStyle,
|
||||
}) => {
|
||||
const dropRef = useRef<HTMLDivElement | null>(null);
|
||||
const dragRef = useRef<HTMLDivElement | null>(null);
|
||||
useEffect(() => {
|
||||
connect(dropRef, dragRef);
|
||||
}, []);
|
||||
return (
|
||||
<TableFieldLine
|
||||
style={style}
|
||||
gap={gap}
|
||||
ref={dropRef}
|
||||
prefix={
|
||||
<div className="cursor-grab h-full mr-[4px] w-[12px]" ref={dragRef}>
|
||||
<IconCozHandle className="text-[12px]" />
|
||||
</div>
|
||||
}
|
||||
>
|
||||
{children}
|
||||
{deletable ? (
|
||||
<Button
|
||||
color="secondary"
|
||||
onClick={onDelete}
|
||||
style={deleteButtonStyle}
|
||||
icon={<IconCozTrashCan />}
|
||||
></Button>
|
||||
) : null}
|
||||
</TableFieldLine>
|
||||
);
|
||||
};
|
||||
|
||||
const FieldTableHeader: FC<{ headers: HeaderItem[] }> = ({ headers }) => (
|
||||
<TableFieldLine
|
||||
className="border-0 border-b-[1px] coz-stroke-primary border-solid h-[28px] mb-[12px]"
|
||||
gap={8}
|
||||
prefix={
|
||||
<div
|
||||
style={{
|
||||
minWidth: 16,
|
||||
}}
|
||||
/>
|
||||
}
|
||||
>
|
||||
{headers.map(header => (
|
||||
<div
|
||||
key={header.name}
|
||||
className={classNames(
|
||||
'text-[14px] coz-fg-secondary font-medium leading-[20px]',
|
||||
'inline-flex items-center',
|
||||
)}
|
||||
style={{
|
||||
width: header.width,
|
||||
...header.style,
|
||||
}}
|
||||
>
|
||||
{header.name}
|
||||
{header.required ? <i className="coz-fg-hglt-red">*</i> : null}
|
||||
<MdTooltip content={header.tooltip} />
|
||||
</div>
|
||||
))}
|
||||
</TableFieldLine>
|
||||
);
|
||||
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* 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 CSSProperties, type FC } from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
|
||||
export const StepIndicator: FC<{
|
||||
number: number;
|
||||
className?: string;
|
||||
style?: CSSProperties;
|
||||
}> = ({ number, className, style }) => (
|
||||
<div
|
||||
style={style}
|
||||
className={classNames(
|
||||
className,
|
||||
'coz-mg-hglt',
|
||||
'w-[20px]',
|
||||
'h-[20px]',
|
||||
'coz-fg-hglt',
|
||||
'text-[14px]',
|
||||
'font-medium',
|
||||
'flex',
|
||||
'items-center',
|
||||
'justify-center',
|
||||
'rounded-[50%]',
|
||||
)}
|
||||
>
|
||||
{number}
|
||||
</div>
|
||||
);
|
||||
Reference in New Issue
Block a user