feat: manually mirror opencoze's code from bytedance
Change-Id: I09a73aadda978ad9511264a756b2ce51f5761adf
This commit is contained in:
@@ -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 { type Ref, forwardRef, type FC } from 'react';
|
||||
|
||||
import { type ButtonProps } from '@coze-arch/bot-semi/Button';
|
||||
import { Button } from '@coze-arch/bot-semi';
|
||||
|
||||
export type BotDebugButtonProps = ButtonProps & {
|
||||
readonly: boolean;
|
||||
};
|
||||
export const BotDebugButton: FC<BotDebugButtonProps> = forwardRef(
|
||||
(props: BotDebugButtonProps, ref: Ref<Button>) => {
|
||||
const { readonly, ...rest } = props;
|
||||
|
||||
if (readonly) {
|
||||
return null;
|
||||
}
|
||||
return <Button {...rest} ref={ref} />;
|
||||
},
|
||||
);
|
||||
@@ -0,0 +1,10 @@
|
||||
/* stylelint-disable declaration-no-important */
|
||||
.processing-tag-process {
|
||||
color: var(--Light-color-green---green-6, #32A247) !important;
|
||||
background: #D2F3D5 !important;
|
||||
}
|
||||
|
||||
.processing-tag-failed {
|
||||
color: var(--Light-color-red---red-6, #DB2E13) !important;
|
||||
background: #FFE0D2 !important;
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* 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 } from 'react';
|
||||
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { Tag, Tooltip } from '@coze-arch/bot-semi';
|
||||
|
||||
import { useUploadProgress } from '../../hooks/use-upload-progress';
|
||||
import { ImportFileTaskStatus } from '../../datamodel';
|
||||
|
||||
import styles from './index.module.less';
|
||||
|
||||
export interface ProcessingTagProps {
|
||||
tableID: string;
|
||||
botID: string;
|
||||
}
|
||||
export const ProcessingTag: FC<ProcessingTagProps> = props => {
|
||||
const { tableID, botID } = props;
|
||||
|
||||
const progressInfo = useUploadProgress({ tableID, botID });
|
||||
|
||||
if (progressInfo?.status === ImportFileTaskStatus.Enqueue) {
|
||||
return (
|
||||
<Tag className={styles['processing-tag-process']}>
|
||||
{I18n.t('db_table_0126_031')}: {progressInfo.progress}%
|
||||
</Tag>
|
||||
);
|
||||
}
|
||||
|
||||
if (progressInfo?.status === ImportFileTaskStatus.Failed) {
|
||||
return (
|
||||
<Tooltip content={progressInfo?.errorMessage}>
|
||||
<Tag className={styles['processing-tag-failed']}>
|
||||
{I18n.t('db_table_0126_031')}
|
||||
</Tag>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
if (progressInfo?.status === ImportFileTaskStatus.Succeed) {
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
@@ -0,0 +1,24 @@
|
||||
.common-svg-icon(@size: 14px, @color: #3370ff) {
|
||||
>svg {
|
||||
width: @size;
|
||||
height: @size;
|
||||
|
||||
>path {
|
||||
fill: @color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.text {
|
||||
margin: 24px 0 17px;
|
||||
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
font-style: normal;
|
||||
line-height: 22px;
|
||||
color: var(--light-usage-text-color-text-0, #1c1f23);
|
||||
}
|
||||
|
||||
.progress-success-icon {
|
||||
.common-svg-icon(20px, var(--semi-color-primary));
|
||||
}
|
||||
@@ -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 { useMemo, type FC } from 'react';
|
||||
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { IconSvgUploadCompletedIcon } from '@coze-arch/bot-icons';
|
||||
|
||||
import { useStepStore } from '../../store/step';
|
||||
import { useInitialConfigStore } from '../../store/initial-config';
|
||||
import outerStyles from '../../index.module.less';
|
||||
import { useUploadProgress } from '../../hooks/use-upload-progress';
|
||||
import { ImportFileTaskStatus } from '../../datamodel';
|
||||
import { UploadProgress } from './upload-progress';
|
||||
|
||||
import styles from './index.module.less';
|
||||
|
||||
export const Processing: FC = () => {
|
||||
const { currentState, tableStructure, upload } = useStepStore(state => ({
|
||||
currentState: state.step4_processing,
|
||||
setCurrentState: state.set_step4_processing,
|
||||
tableStructure: state.step2_tableStructure,
|
||||
upload: state.step1_upload,
|
||||
}));
|
||||
|
||||
const { botId } = useInitialConfigStore(state => ({
|
||||
botId: state.botId,
|
||||
}));
|
||||
|
||||
const { fileList } = upload;
|
||||
const { tableValue } = tableStructure;
|
||||
|
||||
const { tableID } = currentState;
|
||||
|
||||
// @ts-expect-error -- linter-disable-autofix
|
||||
const progressInfo = useUploadProgress({ tableID, botID: botId });
|
||||
|
||||
const headerTitle = useMemo(() => {
|
||||
let msg: string = I18n.t('db_table_0126_029');
|
||||
if (progressInfo?.status === ImportFileTaskStatus.Succeed) {
|
||||
msg = I18n.t('datasets_createFileModel_step4_Finish');
|
||||
} else if (progressInfo?.status === ImportFileTaskStatus.Failed) {
|
||||
msg = I18n.t('datasets_createFileModel_step4_failed');
|
||||
}
|
||||
return msg;
|
||||
}, [progressInfo?.status]);
|
||||
|
||||
return (
|
||||
<div className={outerStyles.stepWrapper}>
|
||||
<div className={styles.text}>{headerTitle}</div>
|
||||
<div className={styles['progress-list']}>
|
||||
<UploadProgress
|
||||
// @ts-expect-error -- linter-disable-autofix
|
||||
key={fileList[0].response.upload_uri}
|
||||
className={styles['dataset-progress']}
|
||||
// @ts-expect-error -- linter-disable-autofix
|
||||
text={tableValue.name}
|
||||
percent={progressInfo?.progress || 0}
|
||||
status={progressInfo?.status || ImportFileTaskStatus.Enqueue}
|
||||
statusDesc={''}
|
||||
format={percent =>
|
||||
percent < 100 ? (
|
||||
`${percent}%`
|
||||
) : (
|
||||
<IconSvgUploadCompletedIcon
|
||||
className={styles['progress-success-icon']}
|
||||
/>
|
||||
)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,75 @@
|
||||
.dataset-progress-wrap {
|
||||
position: relative;
|
||||
margin-bottom: 24px;
|
||||
|
||||
.dataset-progress {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
|
||||
.dataset-progress-content {
|
||||
position: relative;
|
||||
|
||||
overflow: hidden;
|
||||
flex: 1;
|
||||
|
||||
background-color: rgba(46, 50, 56, 5%);
|
||||
border-radius: 8px;
|
||||
|
||||
.progress-bar {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
|
||||
overflow: hidden;
|
||||
|
||||
height: 32px;
|
||||
|
||||
background-color: var(--semi-color-primary);
|
||||
border-radius: 8px;
|
||||
|
||||
transition: width linear 300ms;
|
||||
}
|
||||
|
||||
.text {
|
||||
box-sizing: border-box;
|
||||
height: 32px;
|
||||
padding: 0 12px;
|
||||
|
||||
font-size: 14px;
|
||||
line-height: 32px;
|
||||
word-break: keep-all;
|
||||
}
|
||||
}
|
||||
|
||||
.dataset-progress-format {
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
|
||||
box-sizing: border-box;
|
||||
width: 48px;
|
||||
padding-right: 6px;
|
||||
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
line-height: 20px;
|
||||
color: rgba(28, 31, 35, 60%);
|
||||
}
|
||||
}
|
||||
|
||||
.dataset-progress-error {
|
||||
position: absolute;
|
||||
top: 33px;
|
||||
left: 4px;
|
||||
|
||||
font-size: 12px;
|
||||
color: rgba(255, 39, 16, 100%)
|
||||
}
|
||||
}
|
||||
|
||||
.dataset-progress-wrap:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
@@ -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 { Typography } from '@coze-arch/bot-semi';
|
||||
|
||||
import { ImportFileTaskStatus } from '../../../datamodel';
|
||||
|
||||
import s from './index.module.less';
|
||||
|
||||
interface DatasetProgressProps {
|
||||
className?: string | undefined;
|
||||
style?: React.CSSProperties;
|
||||
text: string;
|
||||
percent: number;
|
||||
format?: (percent: number) => React.ReactNode;
|
||||
status?: ImportFileTaskStatus;
|
||||
statusDesc?: string;
|
||||
}
|
||||
|
||||
export const UploadProgress: React.FC<DatasetProgressProps> = ({
|
||||
className,
|
||||
style,
|
||||
text,
|
||||
percent,
|
||||
format,
|
||||
status,
|
||||
statusDesc,
|
||||
}) => {
|
||||
const SUCCESSIVE_PROCESSING = 100;
|
||||
|
||||
return (
|
||||
<div className={`${s['dataset-progress-wrap']}`}>
|
||||
<div className={`${s['dataset-progress']} ${className}`} style={style}>
|
||||
<div className={s['dataset-progress-content']}>
|
||||
<Typography.Text
|
||||
className={s.text}
|
||||
strong
|
||||
ellipsis={{
|
||||
showTooltip: {
|
||||
opts: { content: text },
|
||||
},
|
||||
}}
|
||||
style={{ color: '#4D53E8' }}
|
||||
>
|
||||
{text}
|
||||
</Typography.Text>
|
||||
<div className={s['progress-bar']} style={{ width: `${percent}%` }}>
|
||||
<Typography.Text
|
||||
className={s.text}
|
||||
strong
|
||||
ellipsis={
|
||||
percent === SUCCESSIVE_PROCESSING
|
||||
? {
|
||||
showTooltip: {
|
||||
opts: { content: text },
|
||||
},
|
||||
}
|
||||
: false
|
||||
}
|
||||
style={{ color: '#fff', width: '100%' }}
|
||||
>
|
||||
{text}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
</div>
|
||||
<div className={s['dataset-progress-format']}>{format?.(percent)}</div>
|
||||
</div>
|
||||
{status && statusDesc && status === ImportFileTaskStatus.Failed ? (
|
||||
<div className={s['dataset-progress-error']}>
|
||||
<span>{statusDesc}</span>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
.table-preview-tips {
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
line-height: 16px;
|
||||
color: var(--Light-usage-text---color-text-3, rgba(29, 28, 35, 35%));
|
||||
text-align: right;
|
||||
|
||||
/* 133.333% */
|
||||
letter-spacing: 0.12px;
|
||||
}
|
||||
|
||||
.footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
/*
|
||||
* 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 { useState, type FC } from 'react';
|
||||
|
||||
import { isNumber } from 'lodash-es';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { Button } from '@coze-arch/bot-semi';
|
||||
|
||||
import { Step } from '../../types';
|
||||
import { useStepStore } from '../../store/step';
|
||||
import { useInitialConfigStore } from '../../store/initial-config';
|
||||
import { useStep } from '../../hooks/use-step';
|
||||
|
||||
import styles from './index.module.less';
|
||||
export const StepFooter: FC = () => {
|
||||
const { getCallbacks } = useStep();
|
||||
const { step, enableGoToNextStep, tablePreview } = useStepStore(state => ({
|
||||
step: state.step,
|
||||
enableGoToNextStep: state.enableGoToNextStep,
|
||||
tablePreview: state.step3_tablePreview,
|
||||
}));
|
||||
const { onCancel } = useInitialConfigStore(state => ({
|
||||
onCancel: state.onCancel,
|
||||
}));
|
||||
|
||||
const stepList = Object.values(Step).filter(i => isNumber(i)) as Step[];
|
||||
const firstStep = Math.min(...stepList);
|
||||
const lastStep = Math.max(...stepList);
|
||||
const isFirstStep = step === firstStep;
|
||||
const isLastStep = step === lastStep;
|
||||
const [submitButtonLoading, setSubmitButtonLoading] = useState(false);
|
||||
|
||||
const { previewData } = tablePreview;
|
||||
const total = previewData?.total_rows || 0;
|
||||
const previewCount = previewData?.preview_rows || 10;
|
||||
|
||||
const handleClickNext = async () => {
|
||||
const { onValidate, onSubmit } = getCallbacks();
|
||||
// onValidate
|
||||
try {
|
||||
const callbackResult = onValidate?.();
|
||||
|
||||
if (callbackResult instanceof Promise) {
|
||||
setSubmitButtonLoading(true);
|
||||
}
|
||||
const res = await callbackResult;
|
||||
|
||||
// 返回 false 则直接 return
|
||||
if (typeof res === 'boolean' && res === false) {
|
||||
setSubmitButtonLoading(false);
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
setSubmitButtonLoading(false);
|
||||
throw e;
|
||||
}
|
||||
|
||||
// onSubmit
|
||||
try {
|
||||
// 判断传入的 submit 函数如果是异步,则按钮 loading
|
||||
const callbackResult = onSubmit?.();
|
||||
if (callbackResult instanceof Promise) {
|
||||
setSubmitButtonLoading(true);
|
||||
}
|
||||
await callbackResult;
|
||||
|
||||
if (isLastStep) {
|
||||
//关闭
|
||||
onCancel?.();
|
||||
} else {
|
||||
// 下一步
|
||||
useStepStore.setState(state => ({
|
||||
step: Math.min(state.step + 1, lastStep),
|
||||
}));
|
||||
}
|
||||
} finally {
|
||||
setSubmitButtonLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleClickPrev = () => {
|
||||
getCallbacks()?.onPrevious?.();
|
||||
if (isFirstStep) {
|
||||
// 关闭
|
||||
onCancel?.();
|
||||
} else {
|
||||
// 上一步
|
||||
useStepStore.setState(state => ({
|
||||
step: Math.max(state.step - 1, firstStep),
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.footer}>
|
||||
{step === Step.Step3_TablePreview ? (
|
||||
<div className={styles['table-preview-tips']}>
|
||||
{I18n.t('db_table_0126_028', {
|
||||
TotalRows: total,
|
||||
ShowRows: previewCount,
|
||||
})}
|
||||
</div>
|
||||
) : null}
|
||||
{isLastStep ? null : (
|
||||
<Button type="tertiary" onClick={onCancel}>
|
||||
{I18n.t('db_table_0126_001')}
|
||||
</Button>
|
||||
)}
|
||||
{isFirstStep || isLastStep ? null : (
|
||||
<Button type="tertiary" onClick={handleClickPrev}>
|
||||
{I18n.t('db_table_0126_004')}
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
theme="solid"
|
||||
type="primary"
|
||||
onClick={handleClickNext}
|
||||
loading={submitButtonLoading}
|
||||
disabled={!enableGoToNextStep}
|
||||
>
|
||||
{isLastStep ? I18n.t('db_table_0126_005') : I18n.t('db_table_0126_003')}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,42 @@
|
||||
/* stylelint-disable declaration-no-important */
|
||||
.table-preview-table-wrapper {
|
||||
min-height: 500px;
|
||||
}
|
||||
|
||||
.table-preview-table {
|
||||
overflow: hidden;
|
||||
flex: none;
|
||||
background: var(--light-color-white-white, #FFF);
|
||||
|
||||
:global {
|
||||
.semi-table-body {
|
||||
max-height: 460px !important;
|
||||
}
|
||||
|
||||
.semi-table-colgroup .semi-table-col {
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
.semi-table-header {
|
||||
overflow-y: hidden !important;
|
||||
}
|
||||
|
||||
.semi-table-tbody>.semi-table-row>.semi-table-row-cell {
|
||||
min-height: 40px;
|
||||
padding: 9px 16px !important;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
.table-preview-tips {
|
||||
margin-top: 16px;
|
||||
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
line-height: 20px;
|
||||
color: #1C1D2399;
|
||||
text-align: left;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
/*
|
||||
* 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 } from 'react';
|
||||
|
||||
import classnames from 'classnames';
|
||||
import { DataNamespace, dataReporter } from '@coze-data/reporter';
|
||||
import { REPORT_EVENTS } from '@coze-arch/report-events';
|
||||
import { EVENT_NAMES, sendTeaEvent } from '@coze-arch/bot-tea';
|
||||
import { type ColumnProps } from '@coze-arch/bot-semi/Table';
|
||||
import { UITable } from '@coze-arch/bot-semi';
|
||||
|
||||
import { useStepStore } from '../../store/step';
|
||||
import { useInitialConfigStore } from '../../store/initial-config';
|
||||
import outerStyles from '../../index.module.less';
|
||||
import { useStep } from '../../hooks/use-step';
|
||||
import { type AddTableResponse } from '../../datamodel';
|
||||
import { CreateType } from '../../../../types';
|
||||
|
||||
import styles from './index.module.less';
|
||||
|
||||
export const TablePreview: FC = () => {
|
||||
const { onSubmit } = useStep();
|
||||
const {
|
||||
botId,
|
||||
onSave,
|
||||
// spaceId
|
||||
} = useInitialConfigStore(state => ({
|
||||
botId: state.botId,
|
||||
onSave: state.onSave,
|
||||
spaceId: state.spaceId,
|
||||
}));
|
||||
|
||||
const {
|
||||
currentState,
|
||||
setProcessing,
|
||||
// upload,
|
||||
tableStructure,
|
||||
} = useStepStore(state => ({
|
||||
currentState: state.step3_tablePreview,
|
||||
setProcessing: state.set_step4_processing,
|
||||
tableStructure: state.step2_tableStructure,
|
||||
upload: state.step1_upload,
|
||||
}));
|
||||
|
||||
const { previewData } = currentState;
|
||||
// @ts-expect-error -- linter-disable-autofix
|
||||
const { headers, datas } = previewData;
|
||||
// const { fileList } = upload;
|
||||
const {
|
||||
// excelValue,
|
||||
tableValue,
|
||||
} = tableStructure;
|
||||
|
||||
/**
|
||||
* headerPart: {
|
||||
* 0: 'name',
|
||||
* 1: 'age',
|
||||
* }
|
||||
*/
|
||||
const headerPart = headers.reduce<Record<number, string>>(
|
||||
(acc, cur, index) => {
|
||||
acc[index] = cur;
|
||||
return acc;
|
||||
},
|
||||
{},
|
||||
);
|
||||
|
||||
/**
|
||||
* dataPart: [
|
||||
* {
|
||||
* 0:'Nick',
|
||||
* 1: 20
|
||||
* },
|
||||
* {
|
||||
* 0:'July',
|
||||
* 1: 30
|
||||
* }
|
||||
* ]
|
||||
*/
|
||||
const dataPart = datas.map(i =>
|
||||
// @ts-expect-error -- linter-disable-autofix
|
||||
i.reduce<Record<number, string>>((acc, cur, index) => {
|
||||
acc[index] = cur;
|
||||
return acc;
|
||||
}, {}),
|
||||
);
|
||||
|
||||
// @ts-expect-error -- linter-disable-autofix
|
||||
const columns: ColumnProps[] = Object.entries(headerPart).map(
|
||||
([key, value]) => ({
|
||||
title: value,
|
||||
dataIndex: key,
|
||||
}),
|
||||
);
|
||||
|
||||
onSubmit(async () => {
|
||||
sendTeaEvent(EVENT_NAMES.create_table_click, {
|
||||
need_login: true,
|
||||
have_access: true,
|
||||
bot_id: botId,
|
||||
// @ts-expect-error -- linter-disable-autofix
|
||||
table_name: tableValue.name,
|
||||
database_create_type: CreateType.excel,
|
||||
});
|
||||
let res: AddTableResponse;
|
||||
try {
|
||||
// TODO:此需求暂停,后端下线,后续待开放
|
||||
// res = await DataModelApi.AddTable({
|
||||
// file: {
|
||||
// tos_uri: fileList[0].response.upload_uri,
|
||||
// sheet_id: excelValue.sheetID,
|
||||
// header_row: excelValue.headerRow,
|
||||
// start_data_row: excelValue.dataStartRow,
|
||||
// },
|
||||
// table: {
|
||||
// bot_id: botId,
|
||||
// space_id: spaceId,
|
||||
// table_name: tableValue.name,
|
||||
// table_desc: tableValue.desc,
|
||||
// table_meta: tableValue.tableMemoryList.map(i => ({
|
||||
// name: i.name,
|
||||
// desc: i.desc,
|
||||
// type: i.type,
|
||||
// must_required: i.must_required,
|
||||
// sequence: i.id as string,
|
||||
// })),
|
||||
// },
|
||||
// rw_mode: tableValue.readAndWriteMode as any as BotTableRWMode,
|
||||
// });
|
||||
} catch (error) {
|
||||
dataReporter.errorEvent(DataNamespace.DATABASE, {
|
||||
eventName: REPORT_EVENTS.DatabaseAddFromExcel,
|
||||
error: error as Error,
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
|
||||
// @ts-expect-error -- linter-disable-autofix
|
||||
if (res) {
|
||||
if (onSave) {
|
||||
await onSave({
|
||||
response: res,
|
||||
// @ts-expect-error -- linter-disable-autofix
|
||||
stateData: tableValue,
|
||||
});
|
||||
}
|
||||
|
||||
setProcessing({
|
||||
tableID: res.table_id as string,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classnames(outerStyles.stepWrapper, styles['table-preview'])}
|
||||
>
|
||||
<UITable
|
||||
tableProps={{
|
||||
columns,
|
||||
dataSource: dataPart,
|
||||
pagination: false,
|
||||
className: styles['table-preview-table'],
|
||||
}}
|
||||
wrapperClassName={styles['table-preview-table-wrapper']}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,50 @@
|
||||
.table-structure {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
|
||||
:global {
|
||||
.semi-form-horizontal .semi-form-field {
|
||||
flex: 1;
|
||||
|
||||
.semi-select {
|
||||
width: 100%;
|
||||
border-radius: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.semi-form-field-label-text {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
font-style: normal;
|
||||
line-height: 20px;
|
||||
color: #1D1C23;
|
||||
}
|
||||
|
||||
.semi-form-field-label {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.semi-form-horizontal .semi-form-field:last-child {
|
||||
margin-right: 0;
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.excel-info-form {
|
||||
width: 100%;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.table-setting-option {
|
||||
:global {
|
||||
.semi-select-option-selected {
|
||||
.semi-select-option-icon {
|
||||
color: #4D53E8
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,350 @@
|
||||
/*
|
||||
* 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, type FC, useState, useMemo } from 'react';
|
||||
|
||||
import { nanoid } from 'nanoid';
|
||||
import classnames from 'classnames';
|
||||
import { DataNamespace, dataReporter } from '@coze-data/reporter';
|
||||
import { REPORT_EVENTS } from '@coze-arch/report-events';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { Form } from '@coze-arch/bot-semi';
|
||||
import {
|
||||
ColumnType,
|
||||
type GetTableSchemaInfoResponse,
|
||||
type FieldItemType,
|
||||
} from '@coze-arch/bot-api/memory';
|
||||
import { MemoryApi } from '@coze-arch/bot-api';
|
||||
|
||||
import { type ExcelValue } from '../../types';
|
||||
import { useStepStore } from '../../store/step';
|
||||
import { useInitialConfigStore } from '../../store/initial-config';
|
||||
import outerStyles from '../../index.module.less';
|
||||
import { useStep } from '../../hooks/use-step';
|
||||
import { type PreviewTableFileResponse } from '../../datamodel';
|
||||
import {
|
||||
DatabaseTableStructure,
|
||||
type DatabaseTableStructureRef,
|
||||
} from '../../../database-table-structure';
|
||||
import { type TableFieldsInfo, CreateType } from '../../../../types';
|
||||
|
||||
import styles from './index.module.less';
|
||||
|
||||
export const TableStructure: FC = () => {
|
||||
const { onSubmit, onValidate, computingEnableGoToNextStep, onPrevious } =
|
||||
useStep();
|
||||
|
||||
const {
|
||||
botId,
|
||||
maxColumnNum,
|
||||
// spaceId
|
||||
} = useInitialConfigStore(state => ({
|
||||
botId: state.botId,
|
||||
maxColumnNum: state.maxColumnNum,
|
||||
spaceId: state.spaceId,
|
||||
}));
|
||||
|
||||
const { currentState, setCurrentState, upload, setTablePreview } =
|
||||
useStepStore(state => ({
|
||||
currentState: state.step2_tableStructure,
|
||||
setCurrentState: state.set_step2_tableStructure,
|
||||
setTablePreview: state.set_step3_tablePreview,
|
||||
upload: state.step1_upload,
|
||||
}));
|
||||
|
||||
const { excelBasicInfo, excelValue, tableValue } = currentState;
|
||||
const { fileList } = upload;
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
// ref
|
||||
const excelInfoFormRef = useRef<Form<ExcelValue>>(null);
|
||||
const tableFormRef = useRef<DatabaseTableStructureRef>(null);
|
||||
|
||||
// options
|
||||
// @ts-expect-error -- linter-disable-autofix
|
||||
const currentSheet = excelBasicInfo.find(i => i.id === excelValue.sheetID);
|
||||
// @ts-expect-error -- linter-disable-autofix
|
||||
const sheetOptions = excelBasicInfo.map(i => ({
|
||||
label: i.sheet_name,
|
||||
value: i.id,
|
||||
}));
|
||||
const currentSheetTotalRow = useMemo(() => {
|
||||
// @ts-expect-error -- linter-disable-autofix
|
||||
let totalRow = currentSheet.total_row;
|
||||
if (totalRow < 2) {
|
||||
totalRow = 2;
|
||||
}
|
||||
if (totalRow > 50) {
|
||||
totalRow = 50;
|
||||
}
|
||||
return totalRow;
|
||||
// @ts-expect-error -- linter-disable-autofix
|
||||
}, [currentSheet.total_row]);
|
||||
|
||||
const headerRowOptions = new Array(currentSheetTotalRow - 1)
|
||||
.fill(0)
|
||||
.map((_, i) => ({
|
||||
label: I18n.t('datasets_createFileModel_tab_dataStarRow_value', {
|
||||
LineNumber: i + 1,
|
||||
}),
|
||||
value: i + 1,
|
||||
}));
|
||||
const dataStartRowOptions = new Array(currentSheetTotalRow)
|
||||
.fill(0)
|
||||
.map((_, i) => ({
|
||||
label: I18n.t('datasets_createFileModel_tab_dataStarRow_value', {
|
||||
LineNumber: i + 1,
|
||||
}),
|
||||
value: i + 1,
|
||||
}))
|
||||
// @ts-expect-error -- linter-disable-autofix
|
||||
.filter(i => i.value >= excelValue.headerRow + 1);
|
||||
|
||||
onSubmit(() => {
|
||||
const tableBasicValue =
|
||||
// @ts-expect-error -- linter-disable-autofix
|
||||
tableFormRef.current.tableBasicInfoFormRef.current.formApi.getValues();
|
||||
|
||||
let res: PreviewTableFileResponse;
|
||||
try {
|
||||
// TODO:此需求暂停,后端下线,后续待开放
|
||||
// res = await DataModelApi.PreviewTableFile({
|
||||
// file: {
|
||||
// tos_uri: fileList[0].response.upload_uri,
|
||||
// sheet_id: excelValue.sheetID,
|
||||
// header_row: excelValue.headerRow,
|
||||
// start_data_row: excelValue.dataStartRow,
|
||||
// },
|
||||
// table: {
|
||||
// space_id: spaceId,
|
||||
// bot_id: botId,
|
||||
// table_name: tableBasicValue.name,
|
||||
// table_desc: tableBasicValue.desc,
|
||||
// table_meta: tableFormRef.current.tableFieldsList.map(i => ({
|
||||
// name: i.name,
|
||||
// desc: i.desc,
|
||||
// type: i.type,
|
||||
// must_required: i.must_required,
|
||||
// sequence: i.id as string,
|
||||
// })),
|
||||
// },
|
||||
// });
|
||||
} catch (error) {
|
||||
dataReporter.errorEvent(DataNamespace.DATABASE, {
|
||||
eventName: REPORT_EVENTS.DatabaseGetPreviewData,
|
||||
error: error as Error,
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
|
||||
// @ts-expect-error -- linter-disable-autofix
|
||||
if (res) {
|
||||
setTablePreview({
|
||||
previewData: res.preview_data,
|
||||
});
|
||||
setCurrentState({
|
||||
tableValue: {
|
||||
// @ts-expect-error -- linter-disable-autofix
|
||||
tableMemoryList: tableFormRef.current.tableFieldsList,
|
||||
...tableBasicValue,
|
||||
// @ts-expect-error -- linter-disable-autofix
|
||||
tableId: tableValue.tableId,
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 上一步保存当前状态
|
||||
onPrevious(() => {
|
||||
const tableBasicValue =
|
||||
// @ts-expect-error -- linter-disable-autofix
|
||||
tableFormRef.current.tableBasicInfoFormRef.current.formApi.getValues();
|
||||
setCurrentState({
|
||||
tableValue: {
|
||||
// @ts-expect-error -- linter-disable-autofix
|
||||
tableMemoryList: tableFormRef.current.tableFieldsList,
|
||||
...tableBasicValue,
|
||||
// @ts-expect-error -- linter-disable-autofix
|
||||
tableId: tableValue.tableId,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
// @ts-expect-error -- linter-disable-autofix
|
||||
onValidate(async () => await tableFormRef.current.validate());
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classnames(outerStyles.stepWrapper, styles['table-structure'])}
|
||||
>
|
||||
<Form<ExcelValue>
|
||||
ref={excelInfoFormRef}
|
||||
layout="horizontal"
|
||||
// @ts-expect-error -- linter-disable-autofix
|
||||
initValues={{ ...excelValue }}
|
||||
className={styles['excel-info-form']}
|
||||
onValueChange={async (values, changedValue) => {
|
||||
const changedKeys = Object.keys(changedValue);
|
||||
|
||||
const reloadTableValue = async (
|
||||
params: {
|
||||
updateTableName?: boolean;
|
||||
} = {},
|
||||
) => {
|
||||
const { updateTableName = false } = params;
|
||||
const basicInfo =
|
||||
// @ts-expect-error -- linter-disable-autofix
|
||||
tableFormRef.current.tableBasicInfoFormRef.current.formApi.getValues();
|
||||
setLoading(true);
|
||||
|
||||
let res: GetTableSchemaInfoResponse;
|
||||
try {
|
||||
res = await MemoryApi.GetTableSchemaInfo({
|
||||
// @ts-expect-error -- linter-disable-autofix
|
||||
tos_uri: fileList[0].response.upload_uri,
|
||||
doc_table_info: {
|
||||
sheet_id: values.sheetID,
|
||||
header_line_idx: values.headerRow - 1,
|
||||
start_line_idx: values.dataStartRow - 1,
|
||||
} as any,
|
||||
});
|
||||
} catch (error) {
|
||||
dataReporter.errorEvent(DataNamespace.DATABASE, {
|
||||
eventName: REPORT_EVENTS.DatabaseGetExcelInfo,
|
||||
error: error as Error,
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
if (res) {
|
||||
// @ts-expect-error -- linter-disable-autofix
|
||||
const newTableMemoryList: TableFieldsInfo = res.table_meta.map(
|
||||
i => ({
|
||||
name: i.column_name,
|
||||
nanoid: nanoid(),
|
||||
desc: '',
|
||||
type:
|
||||
i.column_type === ColumnType.Unknown
|
||||
? undefined
|
||||
: (i.column_type as any as FieldItemType),
|
||||
must_required: false,
|
||||
disableMustRequired: i.contains_empty_value,
|
||||
}),
|
||||
);
|
||||
setCurrentState({
|
||||
excelValue: { ...values },
|
||||
tableValue: {
|
||||
name: updateTableName
|
||||
? // @ts-expect-error -- linter-disable-autofix
|
||||
excelBasicInfo.find(i => i.id === values.sheetID)
|
||||
?.sheet_name || basicInfo.name
|
||||
: basicInfo.name,
|
||||
desc: basicInfo.desc,
|
||||
readAndWriteMode: basicInfo.readAndWriteMode,
|
||||
tableId: '',
|
||||
tableMemoryList: newTableMemoryList,
|
||||
},
|
||||
});
|
||||
// @ts-expect-error -- linter-disable-autofix
|
||||
tableFormRef.current.setTableFieldsList(newTableMemoryList);
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
if (changedKeys.length === 1 && changedKeys.includes('sheetID')) {
|
||||
// FIXME: 此处 semi 有 bug,始终是 override 更新,所以需要也添加上 sheetID 属性
|
||||
// @ts-expect-error -- linter-disable-autofix
|
||||
excelInfoFormRef.current.formApi.setValues({
|
||||
sheetID: changedValue.sheetID,
|
||||
headerRow: 1,
|
||||
dataStartRow: 2,
|
||||
});
|
||||
}
|
||||
|
||||
// 切换 sheet
|
||||
if (
|
||||
changedKeys.length === 3 &&
|
||||
changedKeys.includes('headerRow') &&
|
||||
changedKeys.includes('dataStartRow') &&
|
||||
changedKeys.includes('sheetID')
|
||||
) {
|
||||
await reloadTableValue({ updateTableName: true });
|
||||
}
|
||||
|
||||
// 仅更新 headerRow
|
||||
if (changedKeys.length === 1 && changedKeys.includes('headerRow')) {
|
||||
// @ts-expect-error -- linter-disable-autofix
|
||||
if (changedValue.headerRow >= values.dataStartRow) {
|
||||
// @ts-expect-error -- linter-disable-autofix
|
||||
excelInfoFormRef.current.formApi.setValue(
|
||||
'dataStartRow',
|
||||
// @ts-expect-error -- linter-disable-autofix
|
||||
changedValue.headerRow + 1,
|
||||
);
|
||||
} else {
|
||||
await reloadTableValue();
|
||||
}
|
||||
}
|
||||
|
||||
// 仅更新 dataStartRow
|
||||
if (
|
||||
changedKeys.length === 1 &&
|
||||
changedKeys.includes('dataStartRow')
|
||||
) {
|
||||
await reloadTableValue();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Form.Select
|
||||
field="sheetID"
|
||||
label={I18n.t('datasets_createFileModel_tab_DataSheet')}
|
||||
dropdownClassName={styles['table-setting-option']}
|
||||
optionList={sheetOptions}
|
||||
/>
|
||||
<Form.Select
|
||||
field="headerRow"
|
||||
label={I18n.t('datasets_createFileModel_tab_header')}
|
||||
dropdownClassName={styles['table-setting-option']}
|
||||
optionList={headerRowOptions}
|
||||
/>
|
||||
<Form.Select
|
||||
field="dataStartRow"
|
||||
label={I18n.t('datasets_createFileModel_tab_dataStarRow')}
|
||||
dropdownClassName={styles['table-setting-option']}
|
||||
optionList={dataStartRowOptions}
|
||||
/>
|
||||
</Form>
|
||||
<DatabaseTableStructure
|
||||
// @ts-expect-error -- linter-disable-autofix
|
||||
data={tableValue}
|
||||
botId={botId}
|
||||
forceEdit
|
||||
ref={tableFormRef}
|
||||
useComputingEnableGoToNextStep={(tableFieldsList: TableFieldsInfo) => {
|
||||
computingEnableGoToNextStep(() => {
|
||||
const currentLength = tableFieldsList.length;
|
||||
return currentLength >= 1 && currentLength <= maxColumnNum;
|
||||
});
|
||||
}}
|
||||
loading={loading}
|
||||
enableAdd={false}
|
||||
readAndWriteModeOptions="excel"
|
||||
maxColumnNum={maxColumnNum}
|
||||
createType={CreateType.excel}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,64 @@
|
||||
/* stylelint-disable declaration-no-important */
|
||||
/* stylelint-disable no-descending-specificity */
|
||||
.file-name-wrapper {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
|
||||
.file-name-label {
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
line-height: 20px;
|
||||
color: var(--Light-usage-text---color-text-0, #1D1C23);
|
||||
}
|
||||
|
||||
|
||||
.file-list-table {
|
||||
:global {
|
||||
.semi-table-container {
|
||||
padding: 0 20px;
|
||||
border: 1px solid var(--Light-usage-border---color-border-1, rgba(29, 28, 35, 12%));
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
// 此处不加 !important 优先级不够,会被 semi 的样式覆盖掉
|
||||
.semi-table-row:hover {
|
||||
cursor: default !important;
|
||||
|
||||
>.semi-table-row-cell {
|
||||
background: none !important;
|
||||
border-bottom: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
// 此处不加 !important 优先级不够,会被 semi 的样式覆盖掉
|
||||
.semi-table-row {
|
||||
>.semi-table-row-cell {
|
||||
border-bottom: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
.semi-table-header {
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.semi-table-body {
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.semi-table-row-cell {
|
||||
border: none
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.upload {
|
||||
:global {
|
||||
.semi-upload-drag-area-sub-text {
|
||||
color: rgba(136, 141, 146, 100%)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,461 @@
|
||||
/*
|
||||
* 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 { useCallback, type FC } from 'react';
|
||||
|
||||
import { nanoid } from 'nanoid';
|
||||
import { DataNamespace, dataReporter } from '@coze-data/reporter';
|
||||
import { REPORT_EVENTS } from '@coze-arch/report-events';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { type UploadProps, type FileItem } from '@coze-arch/bot-semi/Upload';
|
||||
import {
|
||||
UIIconButton,
|
||||
UITable,
|
||||
Popover,
|
||||
Progress,
|
||||
Upload as SemiUpload,
|
||||
Toast,
|
||||
Tooltip,
|
||||
} from '@coze-arch/bot-semi';
|
||||
import {
|
||||
IconDeleteOutline,
|
||||
IconUploadFileFail,
|
||||
IconUploadFileSuccess,
|
||||
} from '@coze-arch/bot-icons';
|
||||
import {
|
||||
ColumnType,
|
||||
type GetTableSchemaInfoResponse,
|
||||
BotTableRWMode,
|
||||
type FieldItemType,
|
||||
} from '@coze-arch/bot-api/memory';
|
||||
import {
|
||||
FileBizType,
|
||||
type UploadFileResponse,
|
||||
type UploadFileData,
|
||||
} from '@coze-arch/bot-api/developer_api';
|
||||
import { DeveloperApi, MemoryApi } from '@coze-arch/bot-api';
|
||||
|
||||
import { type SheetItem } from '../../types';
|
||||
import { useStepStore } from '../../store/step';
|
||||
import outerStyles from '../../index.module.less';
|
||||
import { useStep } from '../../hooks/use-step';
|
||||
import { getFileIcon } from '../../helpers/get-file-icon';
|
||||
import { getFileExtension } from '../../helpers/get-file-extension';
|
||||
import { getBase64 } from '../../helpers/get-base64';
|
||||
import { ACCEPT_FILE_TYPES, ACCEPT_FILE_MAX_SIZE } from '../../const';
|
||||
|
||||
import styles from './index.module.less';
|
||||
|
||||
export const UploadStatusComp = (props: {
|
||||
record: FileItem;
|
||||
onRetry: (record: FileItem, index: number) => void;
|
||||
index: number;
|
||||
}) => {
|
||||
const { record, onRetry, index } = props;
|
||||
const { status } = record;
|
||||
if (status === 'uploading' || status === 'validating' || status === 'wait') {
|
||||
return (
|
||||
<span className={styles['upload-status-wrap']}>
|
||||
<span>{I18n.t('datasets_unit_upload_state')}</span>
|
||||
<Progress percent={record.percent} />
|
||||
</span>
|
||||
);
|
||||
}
|
||||
if (status === 'success') {
|
||||
return (
|
||||
<span className={styles['upload-status-wrap']}>
|
||||
<IconUploadFileSuccess />
|
||||
</span>
|
||||
);
|
||||
}
|
||||
if (status === 'validateFail') {
|
||||
return (
|
||||
<span className={styles['upload-status-wrap']}>
|
||||
<IconUploadFileFail />
|
||||
</span>
|
||||
);
|
||||
}
|
||||
if (status === 'uploadFail') {
|
||||
return (
|
||||
<span
|
||||
className={`${styles['upload-status-wrap']} ${styles.retry}`}
|
||||
onClick={() => {
|
||||
onRetry && onRetry(record, index);
|
||||
}}
|
||||
>
|
||||
<Popover
|
||||
className={styles['fail-popover']}
|
||||
content={record.statusDescript}
|
||||
visible
|
||||
trigger="custom"
|
||||
>
|
||||
<IconUploadFileFail />
|
||||
</Popover>
|
||||
<span className={styles['retry-text']}>
|
||||
{I18n.t('datasets_unit_update_retry')}
|
||||
</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
export const transformUnitList = ({
|
||||
fileList,
|
||||
data,
|
||||
fileInstance,
|
||||
index,
|
||||
}: {
|
||||
fileList: FileItem[];
|
||||
data: UploadFileData | undefined;
|
||||
fileInstance: File;
|
||||
index: number;
|
||||
}): FileItem[] => {
|
||||
if (!data) {
|
||||
return fileList;
|
||||
}
|
||||
const filteredList = fileList.map((file, i) => {
|
||||
if (index === i) {
|
||||
return {
|
||||
...file,
|
||||
uri: data.upload_uri || '',
|
||||
status: 'success' as const,
|
||||
percent: 100,
|
||||
fileInstance,
|
||||
};
|
||||
}
|
||||
return file;
|
||||
});
|
||||
return filteredList;
|
||||
};
|
||||
|
||||
export const Upload: FC = () => {
|
||||
const { onSubmit, computingEnableGoToNextStep } = useStep();
|
||||
const { currentState, setCurrentState, tableStructure, setTableStructure } =
|
||||
useStepStore(state => ({
|
||||
currentState: state.step1_upload,
|
||||
setCurrentState: state.set_step1_upload,
|
||||
tableStructure: state.step2_tableStructure,
|
||||
setTableStructure: state.set_step2_tableStructure,
|
||||
tablePreview: state.step3_tablePreview,
|
||||
}));
|
||||
|
||||
const { fileList = [] } = currentState;
|
||||
|
||||
const customRequest: UploadProps['customRequest'] = async options => {
|
||||
const { onSuccess, onError, onProgress, file } = options;
|
||||
|
||||
if (typeof file === 'string') {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 业务
|
||||
const { name, fileInstance } = file;
|
||||
|
||||
if (fileInstance) {
|
||||
const extension = getFileExtension(name);
|
||||
|
||||
const base64 = await getBase64(fileInstance);
|
||||
|
||||
let result: UploadFileResponse;
|
||||
try {
|
||||
result = await DeveloperApi.UploadFile(
|
||||
{
|
||||
file_head: {
|
||||
file_type: extension,
|
||||
biz_type: FileBizType.BIZ_BOT_DATASET,
|
||||
},
|
||||
data: base64,
|
||||
},
|
||||
{
|
||||
onUploadProgress: e => {
|
||||
onProgress({
|
||||
total: e.total ?? fileInstance.size,
|
||||
loaded: e.loaded,
|
||||
});
|
||||
},
|
||||
},
|
||||
);
|
||||
} catch (error) {
|
||||
dataReporter.errorEvent(DataNamespace.DATABASE, {
|
||||
eventName: REPORT_EVENTS.DatabaseUploadExcelFile,
|
||||
error: error as Error,
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
if (result) {
|
||||
onSuccess(result.data);
|
||||
}
|
||||
} else {
|
||||
throw new Error('Failed to upload database');
|
||||
}
|
||||
} catch (e) {
|
||||
onError({
|
||||
status: 0,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const onRetry = async (record: FileItem, index: number) => {
|
||||
try {
|
||||
const { fileInstance } = record;
|
||||
if (fileInstance) {
|
||||
const { name } = fileInstance;
|
||||
const extension = getFileExtension(name);
|
||||
const base64 = await getBase64(fileInstance);
|
||||
const result = await DeveloperApi.UploadFile({
|
||||
file_head: {
|
||||
file_type: extension,
|
||||
biz_type: FileBizType.BIZ_BOT_DATASET,
|
||||
},
|
||||
data: base64,
|
||||
});
|
||||
|
||||
setCurrentState({
|
||||
fileList: transformUnitList({
|
||||
fileList,
|
||||
data: result?.data,
|
||||
fileInstance,
|
||||
index,
|
||||
}),
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
dataReporter.errorEvent(DataNamespace.DATABASE, {
|
||||
eventName: REPORT_EVENTS.DatabaseUploadExcelFile,
|
||||
error: error as Error,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteFile = (index: number) => {
|
||||
setCurrentState({
|
||||
fileList: fileList.filter((f, i) => index !== i),
|
||||
});
|
||||
// reset 配置
|
||||
setTableStructure({
|
||||
excelBasicInfo: undefined,
|
||||
excelValue: undefined,
|
||||
tableValue: undefined,
|
||||
});
|
||||
};
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: I18n.t('db_table_0126_018'),
|
||||
dataIndex: 'name',
|
||||
width: 643,
|
||||
render: (_, record) => {
|
||||
const { name } = record;
|
||||
const extension = getFileExtension(name);
|
||||
return (
|
||||
<div className={styles['file-name-wrapper']}>
|
||||
{getFileIcon(extension)}
|
||||
<span className={styles['file-name-label']}>{name}</span>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: I18n.t('db_table_0126_019'),
|
||||
dataIndex: 'status',
|
||||
render: (_, record, index) => (
|
||||
<UploadStatusComp onRetry={onRetry} record={record} index={index} />
|
||||
),
|
||||
width: 153,
|
||||
},
|
||||
{
|
||||
title: I18n.t('db_table_0126_020'),
|
||||
dataIndex: 'size',
|
||||
width: 100,
|
||||
render: (_, record) => <span>{record.size}</span>,
|
||||
},
|
||||
{
|
||||
title: I18n.t('db_table_0126_021'),
|
||||
dataIndex: 'action',
|
||||
width: 72,
|
||||
render: (_, record: FileItem, index) => {
|
||||
const disabled =
|
||||
record.status === 'uploading' ||
|
||||
record.status === 'validating' ||
|
||||
record.status === 'wait';
|
||||
return (
|
||||
<div
|
||||
className={styles['ui-action-content']}
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
<Tooltip spacing={12} content={I18n.t('Delete')} position="top">
|
||||
<UIIconButton
|
||||
disabled={disabled}
|
||||
icon={<IconDeleteOutline className={styles.icon} />}
|
||||
style={{
|
||||
color: disabled
|
||||
? 'rgba(136, 138, 142, 0.5)'
|
||||
: 'rgba(136, 138, 142, 1)',
|
||||
}}
|
||||
onClick={() => handleDeleteFile(index)}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
onSubmit(async () => {
|
||||
if (!tableStructure.excelBasicInfo) {
|
||||
let res: GetTableSchemaInfoResponse;
|
||||
try {
|
||||
res = await MemoryApi.GetTableSchemaInfo({
|
||||
tos_uri: fileList[0].response.upload_uri,
|
||||
});
|
||||
} catch (error) {
|
||||
dataReporter.errorEvent(DataNamespace.DATABASE, {
|
||||
eventName: REPORT_EVENTS.DatabaseGetExcelInfo,
|
||||
error: error as Error,
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
if (res) {
|
||||
const { sheet_list, table_meta } = res;
|
||||
setTableStructure({
|
||||
excelBasicInfo: sheet_list as SheetItem[],
|
||||
excelValue: {
|
||||
// @ts-expect-error -- linter-disable-autofix
|
||||
sheetID: sheet_list[0]?.id as number,
|
||||
headerRow: 1,
|
||||
dataStartRow: 2,
|
||||
},
|
||||
tableValue: {
|
||||
// @ts-expect-error -- linter-disable-autofix
|
||||
name: sheet_list[0].sheet_name,
|
||||
desc: '',
|
||||
tableId: '',
|
||||
readAndWriteMode: BotTableRWMode.ReadOnly,
|
||||
// @ts-expect-error -- linter-disable-autofix
|
||||
tableMemoryList: table_meta.map(i => ({
|
||||
name: i.column_name,
|
||||
nanoid: nanoid(),
|
||||
desc: '',
|
||||
type:
|
||||
i.column_type === ColumnType.Unknown
|
||||
? undefined
|
||||
: (i.column_type as any as FieldItemType),
|
||||
must_required: false,
|
||||
id: i.sequence,
|
||||
disableMustRequired: i.contains_empty_value,
|
||||
})),
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
computingEnableGoToNextStep(
|
||||
useCallback(
|
||||
() => fileList.length > 0 && fileList.some(i => i.status === 'success'),
|
||||
[fileList],
|
||||
),
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={outerStyles.stepWrapper}>
|
||||
<SemiUpload
|
||||
style={{
|
||||
height: '100%',
|
||||
/**
|
||||
* NOTE: 此处采取 css 隐藏是为了保持 upload 过程,否则会取消上传
|
||||
*/
|
||||
...(fileList.length > 0
|
||||
? {
|
||||
display: 'none',
|
||||
}
|
||||
: {}),
|
||||
}}
|
||||
onAcceptInvalid={() => {
|
||||
Toast.warning({
|
||||
showClose: false,
|
||||
content: I18n.t('db_table_0126_032'),
|
||||
});
|
||||
}}
|
||||
beforeUpload={fileInfo => {
|
||||
// 不通过 maxSize 属性来限制的原因是
|
||||
// 只有 beforeUpload 钩子能改 validateMessage
|
||||
const res = {
|
||||
fileInstance: fileInfo.file.fileInstance,
|
||||
status: fileInfo.file.status,
|
||||
validateMessage: fileInfo.file.validateMessage,
|
||||
shouldUpload: true,
|
||||
autoRemove: false,
|
||||
};
|
||||
const { fileInstance } = fileInfo.file;
|
||||
if (!fileInstance) {
|
||||
return {
|
||||
...res,
|
||||
status: 'uploadFail',
|
||||
shouldUpload: false,
|
||||
};
|
||||
}
|
||||
if (fileInstance.size > ACCEPT_FILE_MAX_SIZE) {
|
||||
Toast.warning({
|
||||
showClose: false,
|
||||
content: I18n.t('file_too_large', {
|
||||
max_size: '20MB',
|
||||
}),
|
||||
});
|
||||
return {
|
||||
...res,
|
||||
shouldUpload: false,
|
||||
status: 'validateFail',
|
||||
validateMessage: I18n.t('file_too_large', {
|
||||
max_size: '20MB',
|
||||
}),
|
||||
};
|
||||
}
|
||||
return res;
|
||||
}}
|
||||
limit={1}
|
||||
draggable={true}
|
||||
showUploadList={false}
|
||||
accept={ACCEPT_FILE_TYPES}
|
||||
customRequest={customRequest}
|
||||
dragMainText={I18n.t('db_table_0126_016')}
|
||||
dragSubText={I18n.t('db_table_0126_017')}
|
||||
onChange={({ fileList: files }) => {
|
||||
// 存在校验通过的才上传
|
||||
if (files.some(f => f.shouldUpload)) {
|
||||
setCurrentState({
|
||||
fileList: files,
|
||||
});
|
||||
}
|
||||
}}
|
||||
className={styles.upload}
|
||||
/>
|
||||
{fileList.length > 0 ? (
|
||||
<UITable
|
||||
tableProps={{
|
||||
dataSource: fileList,
|
||||
columns,
|
||||
className: styles['file-list-table'],
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
// 20MB 限制
|
||||
export const ACCEPT_FILE_MAX_SIZE = 20 * 1024 * 1024;
|
||||
export const ACCEPT_FILE_TYPES = ['.xlsx', '.xls', '.csv'].join(',');
|
||||
@@ -0,0 +1,128 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/* eslint-disable */
|
||||
/* tslint:disable */
|
||||
// @ts-nocheck
|
||||
|
||||
import * as base from './namespaces/base';
|
||||
import * as table_base from './namespaces/table_base';
|
||||
import * as table_import from './namespaces/table_import';
|
||||
export { base, table_base, table_import };
|
||||
export * from './namespaces/base';
|
||||
export * from './namespaces/table_base';
|
||||
export * from './namespaces/table_import';
|
||||
|
||||
export type Int64 = string | number;
|
||||
|
||||
export default class DatamodelService<T> {
|
||||
private request: any = () => {
|
||||
throw new Error('DatamodelService.request is undefined');
|
||||
};
|
||||
private baseURL: string | ((path: string) => string) = '';
|
||||
|
||||
constructor(options?: {
|
||||
baseURL?: string | ((path: string) => string);
|
||||
request?<R>(
|
||||
params: {
|
||||
url: string;
|
||||
method: 'GET' | 'DELETE' | 'POST' | 'PUT' | 'PATCH';
|
||||
data?: any;
|
||||
params?: any;
|
||||
headers?: any;
|
||||
},
|
||||
options?: T,
|
||||
): Promise<R>;
|
||||
}) {
|
||||
this.request = options?.request || this.request;
|
||||
this.baseURL = options?.baseURL || '';
|
||||
}
|
||||
|
||||
private genBaseURL(path: string) {
|
||||
return typeof this.baseURL === 'string'
|
||||
? this.baseURL + path
|
||||
: this.baseURL(path);
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/datamodel/tablefile/preview
|
||||
*
|
||||
* [jump to BAM]()
|
||||
*
|
||||
* 【Table Import】导入文件数据预检查
|
||||
*/
|
||||
PreviewTableFile(
|
||||
req: table_import.PreviewTableFileRequest,
|
||||
options?: T,
|
||||
): Promise<table_import.PreviewTableFileResponse> {
|
||||
const url = this.genBaseURL('/api/datamodel/tablefile/preview');
|
||||
const method = 'POST';
|
||||
const _req = req || {};
|
||||
const data = {
|
||||
table: _req['table'],
|
||||
file: _req['file'],
|
||||
Base: _req['Base'],
|
||||
};
|
||||
return this.request({ url, method, data }, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/datamodel/tablefiletask/query
|
||||
*
|
||||
* [jump to BAM]()
|
||||
*
|
||||
* 【Table Import】导入文件数据任务信息查询
|
||||
*/
|
||||
QueryTableFileTaskStatus(
|
||||
req: table_import.QueryTableFileTaskStatusRequest,
|
||||
options?: T,
|
||||
): Promise<table_import.QueryTableFileTaskStatusResponse> {
|
||||
const url = this.genBaseURL('/api/datamodel/tablefiletask/query');
|
||||
const method = 'POST';
|
||||
const _req = req || {};
|
||||
const data = {
|
||||
table_id: _req['table_id'],
|
||||
bot_id: _req['bot_id'],
|
||||
task_id: _req['task_id'],
|
||||
Base: _req['Base'],
|
||||
};
|
||||
return this.request({ url, method, data }, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/datamodel/table/add
|
||||
*
|
||||
* [jump to BAM]()
|
||||
*
|
||||
* 【Table Import】导入文件添加table
|
||||
*/
|
||||
AddTable(
|
||||
req: table_import.AddTableRequest,
|
||||
options?: T,
|
||||
): Promise<table_import.AddTableResponse> {
|
||||
const url = this.genBaseURL('/api/datamodel/table/add');
|
||||
const method = 'POST';
|
||||
const _req = req || {};
|
||||
const data = {
|
||||
table: _req['table'],
|
||||
file: _req['file'],
|
||||
rw_mode: _req['rw_mode'],
|
||||
Base: _req['Base'],
|
||||
};
|
||||
return this.request({ url, method, data }, options);
|
||||
}
|
||||
}
|
||||
/* eslint-enable */
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
/* eslint-disable */
|
||||
/* tslint:disable */
|
||||
// @ts-nocheck
|
||||
|
||||
export type Int64 = string | number;
|
||||
|
||||
export interface Base {
|
||||
LogID?: string;
|
||||
Caller?: string;
|
||||
Addr?: string;
|
||||
Client?: string;
|
||||
TrafficEnv?: TrafficEnv;
|
||||
Extra?: Record<string, string>;
|
||||
}
|
||||
|
||||
export interface TrafficEnv {
|
||||
Open?: boolean;
|
||||
Env?: string;
|
||||
}
|
||||
/* eslint-enable */
|
||||
@@ -0,0 +1,118 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/* eslint-disable */
|
||||
/* tslint:disable */
|
||||
// @ts-nocheck
|
||||
|
||||
export type Int64 = string | number;
|
||||
|
||||
export enum BotTableRWMode {
|
||||
/** 单用户模式 */
|
||||
LimitedReadWrite = 1,
|
||||
/** 只读模式 */
|
||||
ReadOnly = 2,
|
||||
/** 多用户模式 */
|
||||
UnlimitedReadWrite = 3,
|
||||
/** Max 边界值 */
|
||||
RWModeMax = 4,
|
||||
}
|
||||
|
||||
export enum BotTableStatus {
|
||||
/** 初始化(不可用) */
|
||||
Init = 0,
|
||||
/** 已上线 */
|
||||
Online = 1,
|
||||
/** 删除 */
|
||||
Delete = 2,
|
||||
/** 草稿态(未 publish) */
|
||||
Draft = 3,
|
||||
}
|
||||
|
||||
export enum FieldItemType {
|
||||
/** 文本 */
|
||||
Text = 1,
|
||||
/** 数字 */
|
||||
Number = 2,
|
||||
/** 时间 */
|
||||
Date = 3,
|
||||
/** float */
|
||||
Float = 4,
|
||||
/** bool */
|
||||
Boolean = 5,
|
||||
}
|
||||
|
||||
/** Table model相关常量,结构体定义 */
|
||||
export enum FieldType {
|
||||
/** 文本 */
|
||||
Text = 1,
|
||||
/** 数字 */
|
||||
Number = 2,
|
||||
/** 时间 */
|
||||
Date = 3,
|
||||
/** float */
|
||||
Float = 4,
|
||||
/** bool */
|
||||
Boolean = 5,
|
||||
}
|
||||
|
||||
export enum ImportFileTaskStatus {
|
||||
/** 任务初始化 */
|
||||
Init = 1,
|
||||
/** 任务处理中 */
|
||||
Enqueue = 2,
|
||||
/** 任务成功 */
|
||||
Succeed = 3,
|
||||
/** 任务失败 */
|
||||
Failed = 4,
|
||||
}
|
||||
|
||||
export enum Language {
|
||||
/** 中文 */
|
||||
Chinese = 1,
|
||||
/** 英文 */
|
||||
English = 2,
|
||||
}
|
||||
|
||||
export enum TableType {
|
||||
/** 草稿 */
|
||||
DraftTable = 1,
|
||||
/** 线上 */
|
||||
OnlineTable = 2,
|
||||
}
|
||||
|
||||
export interface FieldItem {
|
||||
/** 字段名称,用户自定义,可能为中文 */
|
||||
name: string;
|
||||
desc?: string;
|
||||
type: FieldItemType;
|
||||
must_required?: boolean;
|
||||
/** 字段Id,服务端生成,全局唯一(新增为0) */
|
||||
id?: Int64;
|
||||
/** 字段名称语言类型 */
|
||||
lang?: Language;
|
||||
/** 物理字段名,服务端生成,单个table下唯一 */
|
||||
physics_name?: string;
|
||||
/** 是否主键 */
|
||||
primary_key?: boolean;
|
||||
/** 字段可见性,1:用户自定义;2:业务定义,对用户可见;3:业务定义,对用户隐藏 */
|
||||
visibility?: number;
|
||||
/** 在excel文档中使用,映射到excel中对应的列 */
|
||||
sequence?: string;
|
||||
/** 业务自定义扩展field元数据 */
|
||||
map_ext_meta?: Record<string, string>;
|
||||
}
|
||||
/* eslint-enable */
|
||||
@@ -0,0 +1,119 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/* eslint-disable */
|
||||
/* tslint:disable */
|
||||
// @ts-nocheck
|
||||
|
||||
import * as table_base from './table_base';
|
||||
import * as base from './base';
|
||||
|
||||
export type Int64 = string | number;
|
||||
|
||||
export interface AddTableRequest {
|
||||
/** table schema */
|
||||
table: ImportTableInfo;
|
||||
/** file source */
|
||||
file: FileInfo;
|
||||
rw_mode: table_base.BotTableRWMode;
|
||||
Base?: base.Base;
|
||||
}
|
||||
|
||||
export interface AddTableResponse {
|
||||
/** 相关id. bot_id */
|
||||
bot_id: string;
|
||||
/** table_id */
|
||||
table_id: string;
|
||||
/** 表名 */
|
||||
table_name: string;
|
||||
/** 上传 TableFile 的 task id,用于后期查询使用.
|
||||
DataModelService 使用 GID 保证 task_id 全局唯一,后续查询时只需要 task_id.
|
||||
DataModel 服务会记录 table 的 lastTaskID, 查询时可以通过 table_id 查到唯一的
|
||||
task_id */
|
||||
task_id: Int64;
|
||||
code: number;
|
||||
msg: string;
|
||||
}
|
||||
|
||||
export interface FileInfo {
|
||||
/** tos uri */
|
||||
tos_uri: string;
|
||||
/** Excel 行号 */
|
||||
header_row: Int64;
|
||||
/** Excel 数据开始行 */
|
||||
start_data_row: Int64;
|
||||
/** Excel sheet id, 0 for default */
|
||||
sheet_id?: number;
|
||||
}
|
||||
|
||||
export interface ImportTableInfo {
|
||||
/** table 所属的 bot_id */
|
||||
bot_id: string;
|
||||
/** 表名 */
|
||||
table_name: string;
|
||||
/** 表描述 */
|
||||
table_desc?: string;
|
||||
/** 字段信息 */
|
||||
table_meta: Array<table_base.FieldItem>;
|
||||
/** 空间ID */
|
||||
space_id: string;
|
||||
}
|
||||
|
||||
export interface PreviewTableFileRequest {
|
||||
table: ImportTableInfo;
|
||||
file: FileInfo;
|
||||
Base?: base.Base;
|
||||
}
|
||||
|
||||
export interface PreviewTableFileResponse {
|
||||
preview_data?: SheetInfo;
|
||||
code: number;
|
||||
msg: string;
|
||||
}
|
||||
|
||||
export interface QueryTableFileTaskStatusRequest {
|
||||
table_id: string;
|
||||
bot_id?: string;
|
||||
task_id?: Int64;
|
||||
Base?: base.Base;
|
||||
}
|
||||
|
||||
export interface QueryTableFileTaskStatusResponse {
|
||||
table_id: string;
|
||||
/** filled by server. callers may use it to do some additional business */
|
||||
table_name: string;
|
||||
/** filled by server. callers may use it to do some additional business */
|
||||
bot_id: string;
|
||||
/** filled by server. callers may use it to do some additional business */
|
||||
task_id: Int64;
|
||||
/** 10 for 10%, 100 for 100% */
|
||||
progress: Int64;
|
||||
status: table_base.ImportFileTaskStatus;
|
||||
/** in json format */
|
||||
summary: string;
|
||||
/** tos url */
|
||||
error_detail_url: string;
|
||||
code: number;
|
||||
msg: string;
|
||||
}
|
||||
|
||||
export interface SheetInfo {
|
||||
headers?: Array<string>;
|
||||
datas?: Array<Array<string>>;
|
||||
total_rows?: Int64;
|
||||
preview_rows?: Int64;
|
||||
}
|
||||
/* eslint-enable */
|
||||
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* 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 Callback } from '../types';
|
||||
|
||||
class EventEmitter {
|
||||
private eventMap = new Map<string, Callback>();
|
||||
|
||||
on(event: string, callback: Callback): void {
|
||||
this.eventMap.set(event, callback);
|
||||
}
|
||||
|
||||
off(event: string): void {
|
||||
this.eventMap.delete(event);
|
||||
}
|
||||
|
||||
getEventCallback(event: string): Callback | undefined {
|
||||
return this.eventMap.get(event);
|
||||
}
|
||||
|
||||
emit(event: string): Promise<void> | void {
|
||||
return this.getEventCallback(event)?.() as any;
|
||||
}
|
||||
}
|
||||
|
||||
export const eventEmitter = new EventEmitter();
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
export function getBase64(file: Blob): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const fileReader = new FileReader();
|
||||
fileReader.onload = event => {
|
||||
const result = event.target?.result;
|
||||
|
||||
if (!result || typeof result !== 'string') {
|
||||
reject(new Error('file read fail'));
|
||||
return;
|
||||
}
|
||||
|
||||
resolve(result.replace(/^.*?,/, ''));
|
||||
};
|
||||
fileReader.readAsDataURL(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 const getFileExtension = (name: string) => {
|
||||
const index = name.lastIndexOf('.');
|
||||
return name.slice(index + 1);
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* 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 { Icon } from '@coze-arch/bot-semi';
|
||||
|
||||
import { ReactComponent as ExcelSVG } from '../../../assets/icon_wiki-excel_colorful.svg';
|
||||
import { ReactComponent as CSVSVG } from '../../../assets/icon_wiki-csv_colorful.svg';
|
||||
|
||||
export const getFileIcon = (extension: string) => {
|
||||
if (extension === 'xlsx' || extension === 'xltx') {
|
||||
return <Icon svg={<ExcelSVG />} />;
|
||||
}
|
||||
if (extension === 'csv') {
|
||||
return <Icon svg={<CSVSVG />} />;
|
||||
}
|
||||
// TODO
|
||||
return <Icon svg={<ExcelSVG />} />;
|
||||
};
|
||||
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import { type Callback } from '../types';
|
||||
import { useStepStore } from '../store/step';
|
||||
import { eventEmitter } from '../helpers/event-emitter';
|
||||
|
||||
const generateEventCallback =
|
||||
(eventName: 'validate' | 'next' | 'prev') =>
|
||||
(callback: Callback): void => {
|
||||
const step = useStepStore(state => state.step);
|
||||
useEffect(() => {
|
||||
const key = `${eventName}-${step}`;
|
||||
eventEmitter.on(key, callback);
|
||||
|
||||
return () => {
|
||||
eventEmitter.off(key);
|
||||
};
|
||||
}, [callback, step]);
|
||||
};
|
||||
|
||||
export const useStep = () => {
|
||||
const step = useStepStore(state => state.step);
|
||||
const enableGoToNextStep = useStepStore(state => state.enableGoToNextStep);
|
||||
const set_enableGoToNextStep = useStepStore(
|
||||
state => state.set_enableGoToNextStep,
|
||||
);
|
||||
|
||||
const computingEnableGoToNextStep = (compute: () => boolean) => {
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks -- linter-disable-autofix
|
||||
useEffect(() => {
|
||||
const res = compute();
|
||||
if (res !== enableGoToNextStep) {
|
||||
set_enableGoToNextStep(res);
|
||||
}
|
||||
}, [compute, enableGoToNextStep]);
|
||||
};
|
||||
|
||||
return {
|
||||
computingEnableGoToNextStep,
|
||||
onValidate: generateEventCallback('validate'),
|
||||
onSubmit: generateEventCallback('next'),
|
||||
onPrevious: generateEventCallback('prev'),
|
||||
getCallbacks: () => ({
|
||||
onValidate: eventEmitter.getEventCallback(`validate-${step}`),
|
||||
onSubmit: eventEmitter.getEventCallback(`next-${step}`),
|
||||
onPrevious: eventEmitter.getEventCallback(`prev-${step}`),
|
||||
}),
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,148 @@
|
||||
/*
|
||||
* 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, useState, useEffect } from 'react';
|
||||
|
||||
import { useLocalStorageState } from 'ahooks';
|
||||
import { DataNamespace, dataReporter } from '@coze-data/reporter';
|
||||
import { REPORT_EVENTS } from '@coze-arch/report-events';
|
||||
|
||||
import {
|
||||
ImportFileTaskStatus,
|
||||
type QueryTableFileTaskStatusResponse,
|
||||
} from '../datamodel';
|
||||
|
||||
export interface ProgressInfo {
|
||||
progress: number;
|
||||
status: ImportFileTaskStatus;
|
||||
errorMessage?: string;
|
||||
}
|
||||
|
||||
export const useUploadProgress = (params: {
|
||||
tableID: string;
|
||||
botID: string;
|
||||
}): ProgressInfo | undefined => {
|
||||
const { tableID, botID } = params;
|
||||
const [pollingInfo, setPollingInfo] = useState<ProgressInfo>();
|
||||
|
||||
const importResultRef = useRef<number>();
|
||||
|
||||
const [
|
||||
mapOfShouldQueryDatabaseProcessStatus,
|
||||
setMapOfShouldQueryDatabaseProcessStatus,
|
||||
] = useLocalStorageState<string | undefined>(
|
||||
'map-of-should-query-database-process-status',
|
||||
{
|
||||
defaultValue: '',
|
||||
},
|
||||
);
|
||||
|
||||
const writeToLocalStorage = () => {
|
||||
try {
|
||||
const lsMap = JSON.parse(mapOfShouldQueryDatabaseProcessStatus || '{}');
|
||||
const value = lsMap?.[botID] || [];
|
||||
value.push(tableID);
|
||||
lsMap[botID] = value;
|
||||
setMapOfShouldQueryDatabaseProcessStatus(JSON.stringify(lsMap));
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
const readFromLocalStorage = (): Record<string, string[]> => {
|
||||
let lsMap = {};
|
||||
try {
|
||||
lsMap = JSON.parse(mapOfShouldQueryDatabaseProcessStatus || '{}');
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
return lsMap;
|
||||
};
|
||||
|
||||
const startReceiveTimeCheck = () => {
|
||||
try {
|
||||
let res: QueryTableFileTaskStatusResponse;
|
||||
try {
|
||||
// TODO:此需求暂停,后端下线,后续待开放
|
||||
// res = await DataModelApi.QueryTableFileTaskStatus({
|
||||
// table_id: tableID,
|
||||
// bot_id: botID,
|
||||
// });
|
||||
} catch (error) {
|
||||
dataReporter.errorEvent(DataNamespace.DATABASE, {
|
||||
eventName: REPORT_EVENTS.DatabaseGetTaskInfo,
|
||||
error: error as Error,
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
|
||||
// 不在Processing则停止轮询
|
||||
// @ts-expect-error -- linter-disable-autofix
|
||||
if (res?.status !== ImportFileTaskStatus.Enqueue) {
|
||||
clearInterval(importResultRef.current);
|
||||
importResultRef.current = undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* 成功后---写入localStorage
|
||||
* 没有任务---写入localStorage
|
||||
*/
|
||||
if (
|
||||
// @ts-expect-error -- linter-disable-autofix
|
||||
!res?.status ||
|
||||
res?.status === (0 as any) ||
|
||||
res?.status === ImportFileTaskStatus.Succeed
|
||||
) {
|
||||
writeToLocalStorage();
|
||||
}
|
||||
|
||||
// @ts-expect-error -- linter-disable-autofix
|
||||
if (res) {
|
||||
setPollingInfo({
|
||||
progress: Number(res.progress),
|
||||
status: res.status,
|
||||
errorMessage: res.summary,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
clearInterval(importResultRef.current);
|
||||
importResultRef.current = undefined;
|
||||
setPollingInfo({
|
||||
progress: 0,
|
||||
status: ImportFileTaskStatus.Failed,
|
||||
errorMessage: (error as Error).message,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
// 读取localStorage,减少不必要的请求次数
|
||||
const lsMap = readFromLocalStorage();
|
||||
const inLocaleStorage = (lsMap[botID] || []).includes(tableID);
|
||||
|
||||
if (tableID && !inLocaleStorage) {
|
||||
importResultRef.current = setInterval(() => {
|
||||
startReceiveTimeCheck();
|
||||
}, 1000) as unknown as number;
|
||||
}
|
||||
|
||||
return () => {
|
||||
clearInterval(importResultRef.current);
|
||||
};
|
||||
}, [tableID]);
|
||||
|
||||
return pollingInfo;
|
||||
};
|
||||
@@ -0,0 +1,18 @@
|
||||
/* stylelint-disable selector-class-pattern */
|
||||
.stepWrapper {
|
||||
width: 100%;
|
||||
height: 500px
|
||||
}
|
||||
|
||||
.create-from-excel-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
|
||||
.steps {
|
||||
width: 700px;
|
||||
margin-bottom: 40px
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
/*
|
||||
* 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, type FC } from 'react';
|
||||
|
||||
import { type DatabaseInfo } from '@coze-studio/bot-detail-store';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { Steps } from '@coze-arch/bot-semi';
|
||||
|
||||
import { Step } from './types';
|
||||
import { useStepStore } from './store/step';
|
||||
import { useInitialConfigStore } from './store/initial-config';
|
||||
import { Upload } from './components/upload';
|
||||
import { TableStructure } from './components/table-structure';
|
||||
import { TablePreview } from './components/table-preview';
|
||||
import { Processing } from './components/processing';
|
||||
|
||||
import styles from './index.module.less';
|
||||
|
||||
const map = {
|
||||
[Step.Step1_Upload]: Upload,
|
||||
[Step.Step2_TableStructure]: TableStructure,
|
||||
[Step.Step3_TablePreview]: TablePreview,
|
||||
[Step.Step4_Processing]: Processing,
|
||||
};
|
||||
|
||||
export interface DatabaseCreateFromExcelProps {
|
||||
onCancel: () => void;
|
||||
botId: string;
|
||||
spaceId: string;
|
||||
maxColumnNum?: number;
|
||||
onSave?: (params: {
|
||||
response: any;
|
||||
stateData: DatabaseInfo;
|
||||
}) => Promise<void>;
|
||||
}
|
||||
export const DatabaseCreateFromExcel: FC<
|
||||
DatabaseCreateFromExcelProps
|
||||
> = props => {
|
||||
const { onCancel, botId, onSave, maxColumnNum = 10, spaceId } = props;
|
||||
|
||||
const { step, reset } = useStepStore(state => ({
|
||||
step: state.step,
|
||||
reset: state.reset,
|
||||
}));
|
||||
|
||||
// init / reset store
|
||||
useEffect(() => {
|
||||
useInitialConfigStore.setState(() => ({
|
||||
onCancel,
|
||||
botId,
|
||||
spaceId,
|
||||
onSave,
|
||||
maxColumnNum,
|
||||
}));
|
||||
return () => {
|
||||
reset();
|
||||
};
|
||||
}, []);
|
||||
|
||||
const Component = map[step];
|
||||
return (
|
||||
<div className={styles['create-from-excel-wrapper']}>
|
||||
<Steps
|
||||
type="basic"
|
||||
size="small"
|
||||
current={step - 1}
|
||||
className={styles.steps}
|
||||
>
|
||||
<Steps.Step title={I18n.t('db_table_0126_012')} />
|
||||
<Steps.Step title={I18n.t('db_table_0126_013')} />
|
||||
<Steps.Step title={I18n.t('db_table_0126_014')} />
|
||||
<Steps.Step title={I18n.t('db_table_0126_015')} />
|
||||
</Steps>
|
||||
<Component />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export { StepFooter } from './components/step-footer';
|
||||
@@ -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 { create } from 'zustand';
|
||||
import { noop } from 'lodash-es';
|
||||
import { type DatabaseInfo } from '@coze-studio/bot-detail-store';
|
||||
|
||||
export interface initialConfigStore {
|
||||
onCancel: () => void;
|
||||
botId: string;
|
||||
spaceId: string;
|
||||
maxColumnNum: number;
|
||||
onSave?: (params: {
|
||||
response: any;
|
||||
stateData: DatabaseInfo;
|
||||
}) => Promise<void>;
|
||||
}
|
||||
|
||||
// 用来存储静态状态,非初始化场景下,仅只读不可修改
|
||||
export const useInitialConfigStore = create<initialConfigStore>()(set => ({
|
||||
onCancel: noop,
|
||||
botId: '',
|
||||
spaceId: '',
|
||||
maxColumnNum: 10,
|
||||
}));
|
||||
@@ -0,0 +1,96 @@
|
||||
/*
|
||||
* 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 { devtools } from 'zustand/middleware';
|
||||
import { create } from 'zustand';
|
||||
|
||||
import { type StepState, type Step } from '../types';
|
||||
|
||||
export interface StepStore {
|
||||
step: Step;
|
||||
enableGoToNextStep: boolean;
|
||||
step1_upload: StepState.Upload;
|
||||
step2_tableStructure: StepState.TableStructure;
|
||||
step3_tablePreview: StepState.TablePreview;
|
||||
step4_processing: StepState.Processing;
|
||||
goToNextStep: () => void;
|
||||
backToPreviousStep: () => void;
|
||||
reset: () => void;
|
||||
set_step1_upload: (newState: StepState.Upload) => void;
|
||||
set_step2_tableStructure: (newState: StepState.TableStructure) => void;
|
||||
set_step3_tablePreview: (newState: StepState.TablePreview) => void;
|
||||
set_step4_processing: (newState: StepState.Processing) => void;
|
||||
set_enableGoToNextStep: (newState: boolean) => void;
|
||||
}
|
||||
|
||||
export const useStepStore = create<StepStore>()(
|
||||
devtools(set => ({
|
||||
step: 1,
|
||||
enableGoToNextStep: true,
|
||||
step1_upload: {},
|
||||
step2_tableStructure: {},
|
||||
step3_tablePreview: {},
|
||||
step4_processing: {},
|
||||
set_step1_upload: (newState: StepState.Upload) =>
|
||||
set(state => ({
|
||||
step1_upload: {
|
||||
...state.step1_upload,
|
||||
...newState,
|
||||
},
|
||||
})),
|
||||
set_step2_tableStructure: (newState: StepState.TableStructure) =>
|
||||
set(state => ({
|
||||
step2_tableStructure: {
|
||||
...state.step2_tableStructure,
|
||||
...newState,
|
||||
},
|
||||
})),
|
||||
set_step3_tablePreview: (newState: StepState.TablePreview) =>
|
||||
set(state => ({
|
||||
step3_tablePreview: {
|
||||
...state.step3_tablePreview,
|
||||
...newState,
|
||||
},
|
||||
})),
|
||||
set_step4_processing: (newState: StepState.Processing) =>
|
||||
set(state => ({
|
||||
step4_processing: {
|
||||
...state.step4_processing,
|
||||
...newState,
|
||||
},
|
||||
})),
|
||||
goToNextStep: () =>
|
||||
set(state => ({
|
||||
step: state.step + 1,
|
||||
})),
|
||||
backToPreviousStep: () =>
|
||||
set(state => ({
|
||||
step: state.step - 1,
|
||||
})),
|
||||
reset: () =>
|
||||
set({
|
||||
step: 1,
|
||||
enableGoToNextStep: true,
|
||||
step1_upload: {},
|
||||
step2_tableStructure: {},
|
||||
step3_tablePreview: {},
|
||||
step4_processing: {},
|
||||
}),
|
||||
set_enableGoToNextStep: (newState: boolean) => {
|
||||
set(() => ({ enableGoToNextStep: newState }));
|
||||
},
|
||||
})),
|
||||
);
|
||||
@@ -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 { type DatabaseInfo } from '@coze-studio/bot-detail-store';
|
||||
import { type FileItem } from '@coze-arch/bot-semi/Upload';
|
||||
|
||||
import { type SheetInfo } from './datamodel';
|
||||
|
||||
export type Callback =
|
||||
| (() => Promise<boolean> | boolean)
|
||||
| (() => Promise<void> | void);
|
||||
|
||||
export enum Step {
|
||||
Step1_Upload = 1,
|
||||
Step2_TableStructure = 2,
|
||||
Step3_TablePreview = 3,
|
||||
Step4_Processing = 4,
|
||||
}
|
||||
|
||||
export interface SheetItem {
|
||||
id: number;
|
||||
sheet_name: string;
|
||||
total_row: number;
|
||||
}
|
||||
|
||||
export interface ExcelValue {
|
||||
sheetID: number;
|
||||
headerRow: number;
|
||||
dataStartRow: number;
|
||||
}
|
||||
|
||||
export type Row = Record<number, string>;
|
||||
|
||||
export declare namespace StepState {
|
||||
export interface Upload {
|
||||
fileList?: FileItem[];
|
||||
}
|
||||
export interface TableStructure {
|
||||
excelBasicInfo?: SheetItem[];
|
||||
excelValue?: ExcelValue;
|
||||
tableValue?: DatabaseInfo;
|
||||
}
|
||||
export interface TablePreview {
|
||||
previewData?: SheetInfo;
|
||||
}
|
||||
export interface Processing {
|
||||
tableID?: string;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,244 @@
|
||||
/* stylelint-disable selector-class-pattern */
|
||||
/* stylelint-disable no-descending-specificity */
|
||||
/* stylelint-disable declaration-no-important */
|
||||
.modal-container {
|
||||
min-width: 100%;
|
||||
}
|
||||
|
||||
.modal-table-btn {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.modal-temp {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.modal-temp-right {
|
||||
position: relative;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
height: 409px;
|
||||
margin-bottom: 24px;
|
||||
padding: 15px 24px;
|
||||
|
||||
background: rgba(255, 255, 255, 100%);
|
||||
border: 1px solid rgba(29, 28, 35, 8%);
|
||||
border-radius: 12px;
|
||||
|
||||
.modal-temp-title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
line-height: 20px;
|
||||
color: rgba(29, 28, 35, 100%);
|
||||
}
|
||||
|
||||
.modal-temp-image {
|
||||
margin-top: 16px;
|
||||
margin-bottom: 12px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.modal-temp-description {
|
||||
height: 64px;
|
||||
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
line-height: 16px;
|
||||
color: rgba(29, 28, 35, 80%);
|
||||
}
|
||||
|
||||
.modal-temp-btn-group {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
|
||||
margin-top: 16px;
|
||||
padding: 8px 0;
|
||||
|
||||
.modal-temp-btn {
|
||||
width: 120px;
|
||||
}
|
||||
}
|
||||
|
||||
.modal-temp-preview {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
|
||||
width: 100%;
|
||||
height: 329px;
|
||||
padding: 16px 21px 24px;
|
||||
|
||||
background: rgba(241, 242, 253, 100%);
|
||||
border-radius: 11px 11px 0 0;
|
||||
|
||||
.title {
|
||||
padding-bottom: 12px;
|
||||
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
line-height: 18px;
|
||||
color: rgba(77, 83, 232, 100%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.modal-modify-tips {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
margin-bottom: 14px;
|
||||
padding: 12px;
|
||||
|
||||
background: var(--light-usage-warning-light-color-warning-light-default,
|
||||
#fff8ea);
|
||||
|
||||
.tip-icon {
|
||||
margin: 0 20px 0 17px;
|
||||
color: rgba(252, 136, 0, 100%);
|
||||
|
||||
>svg {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.description {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-right: 65px;
|
||||
}
|
||||
|
||||
.link {
|
||||
cursor: pointer;
|
||||
margin-left: 20px;
|
||||
color: #3370ff;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
// 新增样式 @zhangyuanzhou.zyz
|
||||
.entry {
|
||||
display: flex;
|
||||
gap: 64px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
height: 409px;
|
||||
margin-bottom: 24px;
|
||||
|
||||
background: rgba(255, 255, 255, 100%);
|
||||
border: 1px solid rgba(29, 28, 35, 8%);
|
||||
border-radius: 12px;
|
||||
|
||||
.entry-method {
|
||||
cursor: pointer;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
|
||||
color: var(--Light-usage-text---color-text-0, #1d1c23);
|
||||
|
||||
.entry-method-icon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.entry-method-title {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
font-style: normal;
|
||||
line-height: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.entry-method:hover {
|
||||
color: var(--Light-color-brand---brand-5, #4d53e8);
|
||||
}
|
||||
}
|
||||
|
||||
// 当窗口足够大时,高度固定为641px
|
||||
// 当窗口太小时,高度随vh变化,333px为内容区距离视窗边缘的距离
|
||||
.database-table-structure-container {
|
||||
overflow: auto;
|
||||
width: 100%;
|
||||
height: min(641px, calc(100vh - 333px));
|
||||
}
|
||||
|
||||
.title-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
|
||||
.right {
|
||||
display: flex;
|
||||
gap: 32px;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.generate-ai-popover-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
|
||||
width: 560px;
|
||||
padding: 24px;
|
||||
|
||||
background-color: rgba(247, 247, 250, 100%);
|
||||
border-radius: 12px;
|
||||
|
||||
.title {
|
||||
margin-bottom: 16px;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
line-height: 24px;
|
||||
}
|
||||
|
||||
.button-wrapper {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.text-area {
|
||||
:global {
|
||||
.semi-input-textarea {
|
||||
overflow: auto;
|
||||
max-height: 191px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.popover {
|
||||
:global {
|
||||
.semi-popover {
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.semi-popover-content {
|
||||
border-radius: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.modal-close-button {
|
||||
height: 24px !important;
|
||||
padding: 4px !important;
|
||||
border-radius: 3px !important;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(46, 50, 56, 5%) !important;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,678 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/* eslint-disable complexity */
|
||||
import { useRef, useMemo, useEffect, useState } from 'react';
|
||||
|
||||
import { nanoid } from 'nanoid';
|
||||
import { useLocalStorageState } from 'ahooks';
|
||||
import { IconAlertTriangle, IconClose } from '@douyinfe/semi-icons';
|
||||
import { type DatabaseInfo } from '@coze-studio/bot-detail-store';
|
||||
import { DataNamespace, dataReporter } from '@coze-data/reporter';
|
||||
import { BotE2e } from '@coze-data/e2e';
|
||||
import { REPORT_EVENTS } from '@coze-arch/report-events';
|
||||
import { I18n, getUnReactiveLanguage } from '@coze-arch/i18n';
|
||||
import { sendTeaEvent, EVENT_NAMES } from '@coze-arch/bot-tea';
|
||||
import {
|
||||
Button,
|
||||
Image,
|
||||
Popconfirm,
|
||||
Divider,
|
||||
Icon,
|
||||
TextArea,
|
||||
Popover,
|
||||
UIButton,
|
||||
Modal,
|
||||
Form,
|
||||
Toast,
|
||||
} from '@coze-arch/bot-semi';
|
||||
import { IconWarningSize24 } from '@coze-arch/bot-icons';
|
||||
import {
|
||||
BotTableRWMode,
|
||||
type RecommendDataModelResponse,
|
||||
SceneType,
|
||||
FieldItemType,
|
||||
} from '@coze-arch/bot-api/memory';
|
||||
import { MemoryApi } from '@coze-arch/bot-api';
|
||||
|
||||
import {
|
||||
DatabaseTableStructure,
|
||||
type DatabaseTableStructureRef,
|
||||
} from '../database-table-structure';
|
||||
import { DatabaseCreateFromExcel } from '../database-create-from-excel';
|
||||
import { BotDebugButton } from '../bot-debug-button';
|
||||
import { CreateType, type OnSave, type NL2DBInfo } from '../../types';
|
||||
import { useCreateFromExcelFG } from '../../hooks/use-create-from-excel-fg';
|
||||
import { TEMPLATE_INFO } from '../../const';
|
||||
import tableTempEN from '../../assets/table-template-en.png';
|
||||
import tableTempCN from '../../assets/table-template-cn.png';
|
||||
import tablePreviewEN from '../../assets/table-preview-en.png';
|
||||
import tablePreviewCN from '../../assets/table-preview-cn.png';
|
||||
import { ReactComponent as UpArrowSVG } from '../../assets/icon_up-arrow.svg';
|
||||
import { ReactComponent as DownArrowSvg } from '../../assets/icon_down-arrow.svg';
|
||||
import { ReactComponent as AddSVG } from '../../assets/icon_add_outlined.svg';
|
||||
import { ReactComponent as GenerateSVG } from '../../assets/generate.svg';
|
||||
import { ReactComponent as FileSVG } from '../../assets/file.svg';
|
||||
|
||||
import s from './index.module.less';
|
||||
|
||||
export interface ExpertModeConfig {
|
||||
isExpertMode: boolean;
|
||||
maxTableNum: number;
|
||||
maxColumnNum: number;
|
||||
readAndWriteModes: BotTableRWMode[];
|
||||
}
|
||||
|
||||
export interface DatabaseModalProps {
|
||||
visible: boolean;
|
||||
onCancel: () => void;
|
||||
database: DatabaseInfo;
|
||||
botId: string;
|
||||
spaceId: string;
|
||||
readonly: boolean;
|
||||
NL2DBInfo: NL2DBInfo | null;
|
||||
expertModeConfig?: ExpertModeConfig;
|
||||
onSave?: OnSave;
|
||||
}
|
||||
|
||||
export const DatabaseModal: React.FC<DatabaseModalProps> = props => {
|
||||
const {
|
||||
database,
|
||||
botId,
|
||||
spaceId,
|
||||
readonly,
|
||||
onCancel,
|
||||
onSave,
|
||||
NL2DBInfo,
|
||||
expertModeConfig,
|
||||
visible,
|
||||
} = props;
|
||||
|
||||
const [generateTableLoading, setGenerateTableLoading] = useState(false);
|
||||
const [contentCheckErrorMsg, setContentCheckErrorMsg] = useState<string>('');
|
||||
const [isEntry, setIsEntry] = useState<boolean>(
|
||||
!database.tableId && !NL2DBInfo,
|
||||
);
|
||||
const [isPreview, setIsPreview] = useState<boolean>(false);
|
||||
const [isDeletedField, setIsDeletedField] = useState<boolean>(false);
|
||||
const [
|
||||
shouldHideDatabaseTableStructureTipsForCurrent,
|
||||
setShouldHideDatabaseTableStructureTipsForCurrent,
|
||||
] = useState<boolean>(false);
|
||||
const [createType, setCreateType] = useState<CreateType>(
|
||||
NL2DBInfo ? CreateType.recommend : CreateType.custom,
|
||||
);
|
||||
const [data, setData] = useState<DatabaseInfo>({
|
||||
tableId: '',
|
||||
name: '',
|
||||
desc: '',
|
||||
readAndWriteMode: BotTableRWMode.LimitedReadWrite,
|
||||
tableMemoryList: [],
|
||||
});
|
||||
const [AIPopoverVisible, setAIPopoverVisible] = useState(false);
|
||||
|
||||
const nlTextAreaRef = useRef<HTMLTextAreaElement>();
|
||||
const tableStructureRef = useRef<DatabaseTableStructureRef>();
|
||||
const enableCreateFromExcel = useCreateFromExcelFG();
|
||||
const [
|
||||
mapOfShouldHidingDatabaseTableStructureTips,
|
||||
setMapOfShouldHidingDatabaseTableStructureTips,
|
||||
] = useLocalStorageState<string | undefined>(
|
||||
// FIXME: 此属性名意义不明确,处为了兼容,暂不修改此属性名,但后续需要使用更明确的命名
|
||||
'use-local-storage-state-modify-tips',
|
||||
{
|
||||
defaultValue: '',
|
||||
},
|
||||
);
|
||||
|
||||
const language = getUnReactiveLanguage();
|
||||
const isEdit = Boolean(data.tableId);
|
||||
const [isReadonly, setIsReadonly] = useState(false);
|
||||
|
||||
const handleSave = async () => {
|
||||
try {
|
||||
setIsReadonly(true);
|
||||
// @ts-expect-error -- linter-disable-autofix
|
||||
await tableStructureRef.current.submit();
|
||||
} finally {
|
||||
setIsReadonly(false);
|
||||
}
|
||||
};
|
||||
|
||||
const hideTableStructureTips = useMemo(() => {
|
||||
const lsMap = JSON.parse(
|
||||
mapOfShouldHidingDatabaseTableStructureTips || '{}',
|
||||
);
|
||||
|
||||
return (
|
||||
!isEdit ||
|
||||
lsMap?.[botId] ||
|
||||
shouldHideDatabaseTableStructureTipsForCurrent
|
||||
);
|
||||
}, [
|
||||
isEdit,
|
||||
shouldHideDatabaseTableStructureTipsForCurrent,
|
||||
mapOfShouldHidingDatabaseTableStructureTips,
|
||||
]);
|
||||
|
||||
const title = useMemo(() => {
|
||||
if (isEdit) {
|
||||
return I18n.t('db_edit_title');
|
||||
}
|
||||
if (createType === CreateType.excel) {
|
||||
return I18n.t('db_table_0126_011');
|
||||
}
|
||||
return I18n.t('db_add_table_title');
|
||||
}, [isEdit, createType]);
|
||||
|
||||
const showEntry = isEntry && !isEdit && !NL2DBInfo;
|
||||
const shouldShowAIGenerate =
|
||||
/**
|
||||
* 1. 入口不展示
|
||||
* 2. 编辑态不展示
|
||||
* 3. Excel导入时不展示
|
||||
*/
|
||||
!showEntry && !isEdit && createType !== CreateType.excel;
|
||||
|
||||
const setDataToDefault = () => {
|
||||
setData({
|
||||
name: '',
|
||||
desc: '',
|
||||
tableId: '',
|
||||
readAndWriteMode: BotTableRWMode.LimitedReadWrite,
|
||||
tableMemoryList: [
|
||||
{
|
||||
nanoid: nanoid(),
|
||||
name: '',
|
||||
desc: '',
|
||||
type: FieldItemType.Text,
|
||||
must_required: false,
|
||||
},
|
||||
],
|
||||
});
|
||||
};
|
||||
|
||||
const setTableFieldsListToDefault = () => {
|
||||
tableStructureRef.current?.setTableFieldsList([
|
||||
{
|
||||
nanoid: nanoid(),
|
||||
name: '',
|
||||
desc: '',
|
||||
type: FieldItemType.Text,
|
||||
must_required: false,
|
||||
},
|
||||
]);
|
||||
};
|
||||
|
||||
const onUseTemplate = () => {
|
||||
setCreateType(CreateType.template);
|
||||
setIsEntry(false);
|
||||
setData({
|
||||
...TEMPLATE_INFO,
|
||||
});
|
||||
};
|
||||
|
||||
const onUseCustom = () => {
|
||||
setCreateType(CreateType.custom);
|
||||
setIsEntry(false);
|
||||
setDataToDefault();
|
||||
};
|
||||
|
||||
const onUseExcel = () => {
|
||||
setCreateType(CreateType.excel);
|
||||
setIsEntry(false);
|
||||
};
|
||||
|
||||
const generateTableByNL = async (text: string, type: SceneType) => {
|
||||
setGenerateTableLoading(true);
|
||||
let res: RecommendDataModelResponse | undefined;
|
||||
try {
|
||||
res = await MemoryApi.RecommendDataModel({
|
||||
bot_id: botId,
|
||||
scene_type: type,
|
||||
text,
|
||||
});
|
||||
} catch (error) {
|
||||
setGenerateTableLoading(false);
|
||||
dataReporter.errorEvent(DataNamespace.DATABASE, {
|
||||
eventName: REPORT_EVENTS.DatabaseNL2DB,
|
||||
error: error as Error,
|
||||
});
|
||||
setDataToDefault();
|
||||
setTableFieldsListToDefault();
|
||||
}
|
||||
|
||||
if (res?.bot_table_list?.[0]) {
|
||||
if (type === SceneType.BotPersona) {
|
||||
setCreateType(CreateType.recommend);
|
||||
}
|
||||
if (type === SceneType.ModelDesc) {
|
||||
setCreateType(CreateType.naturalLanguage);
|
||||
}
|
||||
setData({
|
||||
tableId: '',
|
||||
// @ts-expect-error -- linter-disable-autofix
|
||||
name: res.bot_table_list[0].table_name,
|
||||
// @ts-expect-error -- linter-disable-autofix
|
||||
desc: res.bot_table_list[0].table_desc,
|
||||
readAndWriteMode: BotTableRWMode.LimitedReadWrite,
|
||||
// @ts-expect-error -- linter-disable-autofix
|
||||
tableMemoryList: res.bot_table_list[0].field_list.map(i => ({
|
||||
name: i.name,
|
||||
desc: i.desc,
|
||||
must_required: i.must_required,
|
||||
type: i.type,
|
||||
nanoid: nanoid(),
|
||||
id: Number(i.id),
|
||||
})),
|
||||
});
|
||||
|
||||
// data 是初始值,此处需要手动 setState 更新子组件状态
|
||||
// 若 Modal 已提前关闭,子组件卸载,则 ref 为空,需要加上可选链判断一下
|
||||
tableStructureRef.current?.setTableFieldsList(
|
||||
// @ts-expect-error -- linter-disable-autofix
|
||||
res.bot_table_list[0].field_list.map(i => ({
|
||||
name: i.name,
|
||||
desc: i.desc,
|
||||
must_required: i.must_required,
|
||||
type: i.type,
|
||||
nanoid: nanoid(),
|
||||
id: Number(i.id),
|
||||
})),
|
||||
);
|
||||
} else {
|
||||
if (type === SceneType.BotPersona) {
|
||||
Toast.info(I18n.t('recommended_failed'));
|
||||
setDataToDefault();
|
||||
setTableFieldsListToDefault();
|
||||
}
|
||||
if (type === SceneType.ModelDesc) {
|
||||
Toast.warning(I18n.t('generate_failed'));
|
||||
setAIPopoverVisible(true);
|
||||
}
|
||||
}
|
||||
setGenerateTableLoading(false);
|
||||
};
|
||||
|
||||
const handleGenerate = () => {
|
||||
const generate = () => {
|
||||
const { value } = nlTextAreaRef.current || {};
|
||||
if (value) {
|
||||
generateTableByNL(value, SceneType.ModelDesc);
|
||||
}
|
||||
};
|
||||
|
||||
sendTeaEvent(EVENT_NAMES.generate_with_ai_click, {
|
||||
bot_id: botId,
|
||||
need_login: true,
|
||||
have_access: true,
|
||||
});
|
||||
setAIPopoverVisible(false);
|
||||
|
||||
if (
|
||||
// @ts-expect-error -- linter-disable-autofix
|
||||
tableStructureRef.current.tableFieldsList.filter(i => i.name).length > 0
|
||||
) {
|
||||
Modal.warning({
|
||||
title: I18n.t('bot_database_ai_replace'),
|
||||
content: I18n.t('bot_database_ai_replace_detailed'),
|
||||
okButtonProps: {
|
||||
type: 'warning',
|
||||
},
|
||||
onOk: () => {
|
||||
generate();
|
||||
},
|
||||
maskClosable: false,
|
||||
icon: <IconWarningSize24 />,
|
||||
});
|
||||
} else {
|
||||
generate();
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setAIPopoverVisible(false);
|
||||
setIsPreview(false);
|
||||
setShouldHideDatabaseTableStructureTipsForCurrent(false);
|
||||
setIsDeletedField(false);
|
||||
setContentCheckErrorMsg('');
|
||||
setIsEntry(true);
|
||||
}, [visible]);
|
||||
|
||||
useEffect(() => {
|
||||
setData(database);
|
||||
}, [database]);
|
||||
|
||||
useEffect(() => {
|
||||
setCreateType(NL2DBInfo ? CreateType.recommend : CreateType.custom);
|
||||
}, [NL2DBInfo]);
|
||||
|
||||
useEffect(() => {
|
||||
if (NL2DBInfo && visible) {
|
||||
generateTableByNL(NL2DBInfo.prompt, SceneType.BotPersona);
|
||||
}
|
||||
}, [NL2DBInfo, visible]);
|
||||
|
||||
const DefaultFooter = (
|
||||
<>
|
||||
{contentCheckErrorMsg ? (
|
||||
<Form.ErrorMessage error={contentCheckErrorMsg} />
|
||||
) : null}
|
||||
{hideTableStructureTips ? null : (
|
||||
<div className={s['modal-modify-tips']}>
|
||||
<div className={s.description}>
|
||||
<IconAlertTriangle className={s['tip-icon']} />
|
||||
<span style={{ textAlign: 'left' }}>{I18n.t('db_edit_tips1')}</span>
|
||||
<span
|
||||
className={s.link}
|
||||
onClick={() => {
|
||||
const lsMap = JSON.parse(
|
||||
mapOfShouldHidingDatabaseTableStructureTips || '{}',
|
||||
);
|
||||
lsMap[botId] = true;
|
||||
setMapOfShouldHidingDatabaseTableStructureTips(
|
||||
JSON.stringify(lsMap),
|
||||
);
|
||||
}}
|
||||
>
|
||||
{I18n.t('db_edit_tips2')}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<IconClose
|
||||
onClick={() =>
|
||||
setShouldHideDatabaseTableStructureTipsForCurrent(true)
|
||||
}
|
||||
style={{ cursor: 'pointer' }}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className={s['modal-table-btn']}>
|
||||
{isDeletedField ? (
|
||||
<Popconfirm
|
||||
title={I18n.t('db_del_field_confirm_title')}
|
||||
content={I18n.t('db_del_field_confirm_info')}
|
||||
okText={I18n.t('db_del_field_confirm_yes')}
|
||||
cancelText={I18n.t('db_del_field_confirm_no')}
|
||||
okType="danger"
|
||||
onConfirm={handleSave}
|
||||
>
|
||||
<BotDebugButton
|
||||
loading={isReadonly}
|
||||
theme="solid"
|
||||
type="primary"
|
||||
readonly={readonly}
|
||||
>
|
||||
{I18n.t('db_edit_save')}
|
||||
</BotDebugButton>
|
||||
</Popconfirm>
|
||||
) : (
|
||||
<BotDebugButton
|
||||
readonly={readonly}
|
||||
loading={isReadonly}
|
||||
theme="solid"
|
||||
type="primary"
|
||||
onClick={handleSave}
|
||||
>
|
||||
{I18n.t('db_edit_save')}
|
||||
</BotDebugButton>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
const Entry = (
|
||||
<div className={s['modal-temp']}>
|
||||
<div className={s.entry}>
|
||||
<div
|
||||
className={s['entry-method']}
|
||||
onClick={onUseCustom}
|
||||
data-testid={BotE2e.BotDatabaseAddModalAddCustomBtn}
|
||||
>
|
||||
<Icon svg={<AddSVG />} className={s['entry-method-icon']} />
|
||||
<span className={s['entry-method-title']}>
|
||||
{I18n.t('db_add_table_cust')}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{enableCreateFromExcel ? (
|
||||
<Divider
|
||||
layout="vertical"
|
||||
style={{
|
||||
height: '32px',
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
{enableCreateFromExcel ? (
|
||||
<div className={s['entry-method']} onClick={onUseExcel}>
|
||||
<Icon svg={<FileSVG />} className={s['entry-method-icon']} />
|
||||
<span className={s['entry-method-title']}>
|
||||
{I18n.t('db_table_0126_010')}
|
||||
</span>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
<div className={s['modal-temp-right']}>
|
||||
<div
|
||||
className={s['modal-temp-title']}
|
||||
data-testid={BotE2e.BotDatabaseAddModalTemplateTitle}
|
||||
>
|
||||
{I18n.t('db_add_table_temp_title')}
|
||||
</div>
|
||||
<Image
|
||||
className={s['modal-temp-image']}
|
||||
height={201}
|
||||
src={language === 'zh-CN' ? tableTempCN : tableTempEN}
|
||||
/>
|
||||
<div className={s['modal-temp-description']}>
|
||||
💡{I18n.t('db_add_table_temp_tips')}
|
||||
</div>
|
||||
{isPreview ? (
|
||||
<div className={s['modal-temp-preview']}>
|
||||
<div className={s.title}>
|
||||
{I18n.t('db_add_table_temp_preview_tips')}
|
||||
</div>
|
||||
<Image
|
||||
height={239}
|
||||
src={language === 'zh-CN' ? tablePreviewCN : tablePreviewEN}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
<div className={s['modal-temp-btn-group']}>
|
||||
<Button
|
||||
data-testid={BotE2e.BotDatabaseAddModalPreviewTemplateBtn}
|
||||
theme="light"
|
||||
type="tertiary"
|
||||
onClick={() => setIsPreview(state => !state)}
|
||||
className={s['modal-temp-btn']}
|
||||
>
|
||||
{I18n.t('db_add_table_temp_preview')}
|
||||
</Button>
|
||||
<Button
|
||||
data-testid={BotE2e.BotDatabaseAddModalUseTemplateBtn}
|
||||
theme="solid"
|
||||
type="primary"
|
||||
onClick={onUseTemplate}
|
||||
className={s['modal-temp-btn']}
|
||||
>
|
||||
{I18n.t('db_add_table_temp_use')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const getFooter = () => {
|
||||
if (showEntry) {
|
||||
return null;
|
||||
}
|
||||
if (createType === CreateType.excel) {
|
||||
return null;
|
||||
}
|
||||
return DefaultFooter;
|
||||
};
|
||||
|
||||
const getContent = () => {
|
||||
if (showEntry) {
|
||||
return Entry;
|
||||
}
|
||||
if (createType === CreateType.excel) {
|
||||
return (
|
||||
<DatabaseCreateFromExcel
|
||||
onCancel={onCancel}
|
||||
botId={botId}
|
||||
onSave={onSave}
|
||||
spaceId={spaceId}
|
||||
// @ts-expect-error -- linter-disable-autofix
|
||||
maxColumnNum={expertModeConfig.maxColumnNum}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={s['database-table-structure-container']}>
|
||||
<DatabaseTableStructure
|
||||
data={data}
|
||||
// @ts-expect-error -- linter-disable-autofix
|
||||
ref={tableStructureRef}
|
||||
loading={generateTableLoading}
|
||||
loadingTips={I18n.t('bot_database_ai_waiting')}
|
||||
botId={botId}
|
||||
readAndWriteModeOptions={
|
||||
// @ts-expect-error -- linter-disable-autofix
|
||||
expertModeConfig.isExpertMode ? 'expert' : 'normal'
|
||||
}
|
||||
// @ts-expect-error -- linter-disable-autofix
|
||||
maxColumnNum={expertModeConfig.maxColumnNum}
|
||||
onSave={onSave}
|
||||
onCancel={onCancel}
|
||||
onDeleteField={list => {
|
||||
setIsDeletedField(
|
||||
!database.tableMemoryList.every(i =>
|
||||
// TODO: 当前field id生成规则有问题,故暂时使用 nanoid 替换
|
||||
list.find(j => j.nanoid === i.nanoid),
|
||||
),
|
||||
);
|
||||
}}
|
||||
createType={createType}
|
||||
setContentCheckErrorMsg={setContentCheckErrorMsg}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
visible={visible}
|
||||
onCancel={onCancel}
|
||||
closable={false}
|
||||
width={1138}
|
||||
centered
|
||||
footer={getFooter()}
|
||||
title={
|
||||
<div className={s['title-wrapper']}>
|
||||
<div data-testid={BotE2e.BotDatabaseAddModalTitle}>{title}</div>
|
||||
<div className={s.right}>
|
||||
{shouldShowAIGenerate ? (
|
||||
<Popover
|
||||
trigger="custom"
|
||||
position="bottomRight"
|
||||
content={
|
||||
<div className={s['generate-ai-popover-wrapper']}>
|
||||
<div
|
||||
className={s.title}
|
||||
data-testid={
|
||||
BotE2e.BotDatabaseAddModalTitleCreateAiModalTitle
|
||||
}
|
||||
>
|
||||
{I18n.t('bot_database_ai_create')}
|
||||
</div>
|
||||
<TextArea
|
||||
data-testid={
|
||||
BotE2e.BotDatabaseAddModalTitleCreateAiModalDesc
|
||||
}
|
||||
autosize
|
||||
// @ts-expect-error -- linter-disable-autofix
|
||||
ref={nlTextAreaRef}
|
||||
rows={1}
|
||||
placeholder={I18n.t('bot_database_ai_create_tip')}
|
||||
className={s['text-area']}
|
||||
/>
|
||||
<div className={s['button-wrapper']}>
|
||||
<UIButton
|
||||
data-testid={
|
||||
BotE2e.BotDatabaseAddModalTitleCreateAiModalCreateBtn
|
||||
}
|
||||
theme="borderless"
|
||||
onClick={handleGenerate}
|
||||
icon={<Icon svg={<GenerateSVG />} />}
|
||||
>
|
||||
{I18n.t('bot_database_ai_generate')}
|
||||
</UIButton>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
keepDOM
|
||||
visible={AIPopoverVisible}
|
||||
onVisibleChange={_v => {
|
||||
setAIPopoverVisible(_v);
|
||||
}}
|
||||
onClickOutSide={() => {
|
||||
setAIPopoverVisible(false);
|
||||
}}
|
||||
className={s.popover}
|
||||
>
|
||||
<UIButton
|
||||
data-testid={BotE2e.BotDatabaseAddModalTitleCreateAiBtn}
|
||||
theme="borderless"
|
||||
icon={
|
||||
AIPopoverVisible ? (
|
||||
<Icon svg={<UpArrowSVG />} />
|
||||
) : (
|
||||
<Icon svg={<DownArrowSvg />} />
|
||||
)
|
||||
}
|
||||
iconPosition="right"
|
||||
onClick={() => {
|
||||
sendTeaEvent(EVENT_NAMES.nl2table_create_table_click, {
|
||||
bot_id: botId,
|
||||
need_login: true,
|
||||
have_access: true,
|
||||
});
|
||||
setAIPopoverVisible(true);
|
||||
}}
|
||||
>
|
||||
{I18n.t('bot_database_ai_create')}
|
||||
</UIButton>
|
||||
</Popover>
|
||||
) : null}
|
||||
<UIButton
|
||||
data-testid={BotE2e.BotDatabaseAddModalTitleCloseIcon}
|
||||
icon={<IconClose />}
|
||||
type="tertiary"
|
||||
theme="borderless"
|
||||
onClick={onCancel}
|
||||
className={s['modal-close-button']}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
maskClosable={false}
|
||||
>
|
||||
<div className={s['modal-container']}>{getContent()}</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,11 @@
|
||||
.column-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.table-header-label-tooltip-icon {
|
||||
cursor: pointer;
|
||||
margin-left: 8px;
|
||||
color: rgba(198, 202, 205, 100%);
|
||||
}
|
||||
@@ -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 { type FC, type ReactNode } from 'react';
|
||||
|
||||
import { Icon, Popover } from '@coze-arch/bot-semi';
|
||||
|
||||
import { ReactComponent as InfoSVG } from '../../../../assets/icon_info_outlined.svg';
|
||||
|
||||
import s from './index.module.less';
|
||||
|
||||
export const ColumnHeader: FC<{
|
||||
label: string;
|
||||
required: boolean;
|
||||
tips: ReactNode;
|
||||
}> = p => {
|
||||
const { label, required, tips } = p;
|
||||
return (
|
||||
<div className={s['column-title']}>
|
||||
<span>{label}</span>
|
||||
{required ? <span style={{ color: 'red' }}>*</span> : null}
|
||||
<Popover showArrow position="top" content={<div>{tips}</div>}>
|
||||
<Icon
|
||||
svg={<InfoSVG />}
|
||||
className={s['table-header-label-tooltip-icon']}
|
||||
/>
|
||||
</Popover>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,8 @@
|
||||
/* stylelint-disable declaration-no-important */
|
||||
.modal-key-tip {
|
||||
width: 200px !important;
|
||||
|
||||
ul {
|
||||
padding-left: 16px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { PopoverContent } from '@coze-studio/components';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
|
||||
import s from './index.module.less';
|
||||
|
||||
export const KeyTipsNode: React.FC = () => (
|
||||
<PopoverContent className={s['modal-key-tip']}>{`- ${I18n.t(
|
||||
'db_add_table_field_name_tips1',
|
||||
)}
|
||||
- ${I18n.t('db_add_table_field_name_tips2')}
|
||||
- ${I18n.t('db_add_table_field_name_tips3')}
|
||||
- ${I18n.t('db_add_table_field_name_tips4')}`}</PopoverContent>
|
||||
);
|
||||
@@ -0,0 +1,185 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/* eslint-disable complexity */
|
||||
import { type TableMemoryItem } from '@coze-studio/bot-detail-store';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
|
||||
import { type MapperItem, type TriggerType, VerifyType } from '../../../types';
|
||||
|
||||
// 校验 Table Name 和 Field Name
|
||||
const namingRegexMapper = [
|
||||
{
|
||||
type: 1,
|
||||
regex: /[^a-z0-9_]/,
|
||||
errorMsg: I18n.t('db_add_table_field_name_tips2'),
|
||||
},
|
||||
{
|
||||
type: 2,
|
||||
regex: /^[^a-z]/,
|
||||
errorMsg: I18n.t('db_add_table_field_name_tips3'),
|
||||
},
|
||||
{
|
||||
type: 3,
|
||||
regex: /[\s\S]{64,}/,
|
||||
errorMsg: I18n.t('db_add_table_field_name_tips4'),
|
||||
},
|
||||
];
|
||||
export const validateNaming = (str: string, errList: string[] = []) => {
|
||||
let list = [...errList];
|
||||
namingRegexMapper.forEach(i => {
|
||||
list = list.filter(j => j !== i.errorMsg);
|
||||
if (i.regex.test(str || '')) {
|
||||
list.push(i.errorMsg);
|
||||
}
|
||||
});
|
||||
return list;
|
||||
};
|
||||
|
||||
// 校验 Table Fields
|
||||
export const thMapper: MapperItem[] = [
|
||||
{
|
||||
label: I18n.t('db_add_table_field_name'),
|
||||
key: 'name',
|
||||
validator: [
|
||||
{
|
||||
type: VerifyType.Naming,
|
||||
message: '',
|
||||
},
|
||||
{
|
||||
type: VerifyType.Required,
|
||||
message: I18n.t('db_table_save_exception_nofieldname'),
|
||||
},
|
||||
{
|
||||
type: VerifyType.Unique,
|
||||
message: I18n.t('db_table_save_exception_fieldname'),
|
||||
},
|
||||
],
|
||||
defaultValue: '',
|
||||
require: true,
|
||||
},
|
||||
{
|
||||
label: I18n.t('db_add_table_field_desc'),
|
||||
key: 'desc',
|
||||
require: false,
|
||||
validator: [],
|
||||
defaultValue: '',
|
||||
},
|
||||
{
|
||||
label: I18n.t('db_add_table_field_type'),
|
||||
key: 'type',
|
||||
require: true,
|
||||
validator: [
|
||||
{
|
||||
type: VerifyType.Required,
|
||||
message: I18n.t('db_table_save_exception_fieldtype'),
|
||||
},
|
||||
],
|
||||
defaultValue: '',
|
||||
},
|
||||
{
|
||||
label: I18n.t('db_add_table_field_necessary'),
|
||||
key: 'must_required',
|
||||
require: false,
|
||||
validator: [],
|
||||
defaultValue: true,
|
||||
},
|
||||
];
|
||||
export const validateFields = (
|
||||
list: TableMemoryItem[],
|
||||
trigger: TriggerType,
|
||||
) => {
|
||||
const resList = list.map(_listItem => {
|
||||
const listItem: TableMemoryItem = { ..._listItem };
|
||||
thMapper.forEach(thItem => {
|
||||
const thKey = thItem.key as keyof TableMemoryItem;
|
||||
|
||||
thItem.validator.forEach(verifyItem => {
|
||||
if (!listItem?.errorMapper) {
|
||||
listItem.errorMapper = {};
|
||||
}
|
||||
let errTarget = listItem?.errorMapper?.[thKey];
|
||||
const value = listItem[thKey];
|
||||
if (!errTarget) {
|
||||
listItem.errorMapper[thKey] = [];
|
||||
errTarget = [];
|
||||
}
|
||||
const msg = verifyItem.message;
|
||||
switch (verifyItem.type) {
|
||||
case VerifyType.Required: {
|
||||
// 报错出现时机:点击保存按钮时,出现提示。表中某一行填写了数据,但是未填写必填字段时,需要报错
|
||||
if (
|
||||
trigger === 'save' &&
|
||||
!value &&
|
||||
thMapper.find(
|
||||
i =>
|
||||
!!listItem[i.key as keyof TableMemoryItem] && !i.defaultValue,
|
||||
)
|
||||
) {
|
||||
listItem.errorMapper[thKey].push(msg);
|
||||
}
|
||||
// 报错消失时机:必填输入框输入了内容后,报错立刻消失
|
||||
if (trigger === 'change' && value) {
|
||||
listItem.errorMapper[thKey] = errTarget.filter(i => i !== msg);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case VerifyType.Unique: {
|
||||
// 报错出现时机:点击保存按钮时,出现提示。
|
||||
if (
|
||||
trigger === 'save' &&
|
||||
value &&
|
||||
list.filter(i => i[thKey] === listItem[thKey]).length !== 1
|
||||
) {
|
||||
listItem.errorMapper[thKey].push(msg);
|
||||
}
|
||||
// 报错消失时机:必填输入框输入了内容后,报错立刻消失
|
||||
if (
|
||||
trigger === 'change' &&
|
||||
value &&
|
||||
list.filter(i => i[thKey] === listItem[thKey]).length === 1
|
||||
) {
|
||||
listItem.errorMapper[thKey] = errTarget.filter(i => i !== msg);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case VerifyType.Naming: {
|
||||
// 报错出现时机:命名格式有问题,失去焦点时,立刻校验格式
|
||||
if (
|
||||
trigger === 'save' ||
|
||||
trigger === 'blur' ||
|
||||
(trigger === 'change' && errTarget.length)
|
||||
) {
|
||||
listItem.errorMapper[thKey] = validateNaming(
|
||||
value as string,
|
||||
errTarget,
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
listItem.errorMapper[thKey] = Array.from(
|
||||
new Set(listItem.errorMapper[thKey]),
|
||||
);
|
||||
});
|
||||
});
|
||||
return listItem;
|
||||
});
|
||||
|
||||
return resList;
|
||||
};
|
||||
@@ -0,0 +1,205 @@
|
||||
/* stylelint-disable selector-class-pattern */
|
||||
/* stylelint-disable declaration-no-important */
|
||||
/* stylelint-disable no-descending-specificity */
|
||||
/* stylelint-disable max-nesting-depth */
|
||||
/* stylelint-disable no-duplicate-selectors */
|
||||
.th-tip-name {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.th-tip-dot {
|
||||
position: relative;
|
||||
padding-left: 16px;
|
||||
}
|
||||
|
||||
.th-tip-dot::before {
|
||||
content: '';
|
||||
|
||||
position: absolute;
|
||||
top: 7px;
|
||||
left: 0;
|
||||
|
||||
width: 5px;
|
||||
height: 5px;
|
||||
|
||||
background-color: #000;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
// 新增样式 @zhangyuanzhou.zyz
|
||||
.form-input-error {
|
||||
:global {
|
||||
.semi-input-wrapper {
|
||||
border-color: #f93920
|
||||
}
|
||||
|
||||
.semi-input-wrapper:hover {
|
||||
border-color: var(--semi-color-border, rgba(29, 28, 35, 8%))
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
.table-structure-form {
|
||||
padding-bottom: 24px;
|
||||
|
||||
:global {
|
||||
|
||||
// 去除 error message icon
|
||||
.semi-form-field-validate-status-icon {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.semi-form-field-validate-status-icon+span {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.semi-col-12 {
|
||||
.semi-form-field[x-field-id="prompt_disabled"] {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
|
||||
.semi-form-field-main {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: auto !important;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
.max-row-banner {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.form-item-label-tooltip-icon {
|
||||
cursor: pointer;
|
||||
color: rgba(198, 202, 205, 100%);
|
||||
}
|
||||
|
||||
.table-structure-table {
|
||||
:global {
|
||||
|
||||
// table 圆角和边框
|
||||
.semi-table-container {
|
||||
padding: 0 8px;
|
||||
border: 1px solid var(--Light-usage-border---color-border-1, rgba(29, 28, 35, 8%));
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.semi-table-header {
|
||||
padding: 0 8px;
|
||||
}
|
||||
|
||||
// hover时去除背景色
|
||||
.semi-table-body {
|
||||
.semi-table-row:hover {
|
||||
>.semi-table-row-cell {
|
||||
background: none !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
.semi-table-row {
|
||||
cursor: default !important;
|
||||
|
||||
>.semi-table-row-cell {
|
||||
padding: 12px 8px !important;
|
||||
border-bottom: 1px solid transparent !important;
|
||||
}
|
||||
}
|
||||
|
||||
.semi-table-body {
|
||||
overflow: visible;
|
||||
max-height: none !important;
|
||||
padding: 12px 0;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
|
||||
// 移除 UITable 中的 before元素影响
|
||||
.semi-table-row-cell::before {
|
||||
display: none;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
.table-structure-table-wrapper {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.table-empty-tips {
|
||||
padding: 8px;
|
||||
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
line-height: 16px;
|
||||
color: @error-red;
|
||||
}
|
||||
|
||||
|
||||
.spin {
|
||||
:global {
|
||||
.semi-spin-wrapper div {
|
||||
font-size: 16px;
|
||||
color: rgba(29, 28, 35, 35%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.table-setting-option {
|
||||
:global {
|
||||
.semi-select-option-selected {
|
||||
.semi-select-option-icon {
|
||||
color: #4D53E8
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
.table-name-form-field {
|
||||
padding-bottom: 24px !important;
|
||||
|
||||
// semi 原有的 disable 样式不满足需求,需要更明显的文字颜色
|
||||
:global {
|
||||
.semi-input-wrapper-disabled {
|
||||
-webkit-text-fill-color: rgba(29, 28, 35, 100%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.table-desc-form-field {
|
||||
padding-top: 0 !important;
|
||||
}
|
||||
|
||||
.prompt_disabled_popover {
|
||||
:global {
|
||||
.semi-popover-icon-arrow {
|
||||
right: 8px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.read_mode_popover {
|
||||
:global {
|
||||
.semi-popover-icon-arrow {
|
||||
left: 8px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,859 @@
|
||||
/*
|
||||
* 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,
|
||||
forwardRef,
|
||||
useImperativeHandle,
|
||||
useState,
|
||||
type FC,
|
||||
type MutableRefObject,
|
||||
useCallback,
|
||||
useMemo,
|
||||
} from 'react';
|
||||
|
||||
import { nanoid } from 'nanoid';
|
||||
import { noop } from 'lodash-es';
|
||||
import classNames from 'classnames';
|
||||
import { useBoolean } from 'ahooks';
|
||||
import { type DatabaseInfo } from '@coze-studio/bot-detail-store';
|
||||
import { DataNamespace, dataReporter } from '@coze-data/reporter';
|
||||
import { BotE2e } from '@coze-data/e2e';
|
||||
import { REPORT_EVENTS } from '@coze-arch/report-events';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { EVENT_NAMES, sendTeaEvent } from '@coze-arch/bot-tea';
|
||||
import {
|
||||
UITable,
|
||||
UITableAction,
|
||||
Button,
|
||||
Switch,
|
||||
Tooltip,
|
||||
Image,
|
||||
Form,
|
||||
Banner,
|
||||
withField,
|
||||
Popover,
|
||||
Spin,
|
||||
Icon,
|
||||
Row,
|
||||
Col,
|
||||
} from '@coze-arch/bot-semi';
|
||||
import { IconAdd } from '@coze-arch/bot-icons';
|
||||
import { isApiError } from '@coze-arch/bot-http';
|
||||
import {
|
||||
type InsertBotTableRequest,
|
||||
BotTableRWMode,
|
||||
type FieldItemType,
|
||||
} from '@coze-arch/bot-api/memory';
|
||||
import { MemoryApi } from '@coze-arch/bot-api';
|
||||
|
||||
import { SLSelect } from '../singleline-select';
|
||||
import { SLInput } from '../singleline-input';
|
||||
import {
|
||||
type TriggerType,
|
||||
type TableFieldsInfo,
|
||||
type TableBasicInfo,
|
||||
type ReadAndWriteModeOptions,
|
||||
type CreateType,
|
||||
type OnSave,
|
||||
} from '../../types';
|
||||
import {
|
||||
DATABASE_CONTENT_CHECK_ERROR_CODE,
|
||||
DATABASE_CONTENT_CHECK_ERROR_CODE_NEW,
|
||||
FIELD_TYPE_OPTIONS,
|
||||
RW_MODE_OPTIONS_CONFIG,
|
||||
RW_MODE_OPTIONS_MAP,
|
||||
SYSTEM_FIELDS,
|
||||
} from '../../const';
|
||||
import keyExample from '../../assets/key-example.png';
|
||||
import { ReactComponent as InfoSVG } from '../../assets/icon_info_outlined.svg';
|
||||
import { validateFields, validateNaming } from './helpers/validate';
|
||||
import { KeyTipsNode } from './components/KeyTipsNode';
|
||||
import { ColumnHeader } from './components/ColumnHeader';
|
||||
|
||||
import s from './index.module.less';
|
||||
|
||||
const MIN_COL = 12;
|
||||
const MAX_COL = 24;
|
||||
const MAX_COLUMNS = 20;
|
||||
|
||||
export interface DatabaseTableStructureProps {
|
||||
data: DatabaseInfo;
|
||||
botId: string;
|
||||
forceEdit?: boolean;
|
||||
loading?: boolean;
|
||||
loadingTips?: string;
|
||||
/**
|
||||
* excel: 单用户模式|只读模式
|
||||
* normal: 单用户模式|只读模式
|
||||
* expert: 单用户模式|只读模式|多用户模式
|
||||
* undefined: 不支持读写模式
|
||||
*/
|
||||
readAndWriteModeOptions?: ReadAndWriteModeOptions;
|
||||
enableAdd?: boolean;
|
||||
maxColumnNum?: number;
|
||||
|
||||
useComputingEnableGoToNextStep?: (list: TableFieldsInfo) => void;
|
||||
onCancel?: () => void;
|
||||
onSave?: OnSave;
|
||||
onDeleteField?: (list: TableFieldsInfo) => void;
|
||||
setContentCheckErrorMsg?: (s: string) => void;
|
||||
|
||||
createType: CreateType;
|
||||
}
|
||||
|
||||
export interface DatabaseTableStructureRef {
|
||||
validate: () => Promise<boolean>;
|
||||
submit: () => Promise<void>;
|
||||
isReadonly: boolean;
|
||||
setTableFieldsList: (list: TableFieldsInfo) => void;
|
||||
tableFieldsList: TableFieldsInfo;
|
||||
tableBasicInfoFormRef: MutableRefObject<Form<TableBasicInfo>>;
|
||||
}
|
||||
|
||||
export const DatabaseTableStructure = forwardRef<
|
||||
DatabaseTableStructureRef,
|
||||
DatabaseTableStructureProps
|
||||
>((props, ref) => {
|
||||
const {
|
||||
data: initialData,
|
||||
botId,
|
||||
onSave,
|
||||
onCancel,
|
||||
onDeleteField,
|
||||
forceEdit = false,
|
||||
maxColumnNum = MAX_COLUMNS,
|
||||
useComputingEnableGoToNextStep,
|
||||
readAndWriteModeOptions,
|
||||
enableAdd = true,
|
||||
loading = false,
|
||||
setContentCheckErrorMsg = noop,
|
||||
loadingTips,
|
||||
createType,
|
||||
} = props;
|
||||
const getDefaultTableFieldsList = () => {
|
||||
if (initialData.readAndWriteMode === BotTableRWMode.UnlimitedReadWrite) {
|
||||
return [...SYSTEM_FIELDS, ...initialData.tableMemoryList];
|
||||
}
|
||||
return initialData.tableMemoryList;
|
||||
};
|
||||
|
||||
const [tableFieldsList, setTableFieldsList] = useState<TableFieldsInfo>(
|
||||
getDefaultTableFieldsList(),
|
||||
);
|
||||
|
||||
const inputRef = useRef<{
|
||||
triggerFocus?: () => void;
|
||||
}>(null);
|
||||
const scrollRef = useRef<HTMLDivElement>(null);
|
||||
const tableBasicInfoFormRef = useRef<Form<TableBasicInfo>>();
|
||||
|
||||
const isModify = Boolean(initialData.tableId);
|
||||
const [isReadonly, { setTrue: enableReadonly, setFalse: disableReadonly }] =
|
||||
useBoolean(false);
|
||||
const isRowMaxLimit = tableFieldsList.length >= maxColumnNum;
|
||||
const isExceedRowMaxLimit = tableFieldsList.length > maxColumnNum;
|
||||
const isEmptyList = !tableFieldsList
|
||||
.filter(i => !i.isSystemField)
|
||||
.filter(i => i.name || i.desc || i.type).length;
|
||||
const databaseAuditErrorCodes = [
|
||||
DATABASE_CONTENT_CHECK_ERROR_CODE,
|
||||
DATABASE_CONTENT_CHECK_ERROR_CODE_NEW,
|
||||
];
|
||||
const handleContentCheckError = (error: Error) => {
|
||||
if (
|
||||
isApiError(error) &&
|
||||
databaseAuditErrorCodes.includes(Number(error?.code))
|
||||
) {
|
||||
setContentCheckErrorMsg(
|
||||
error?.msg || I18n.t('knowledge_bot_update_databse_tnserr_msg'),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const handleAdd = (triggerFocus = true) => {
|
||||
if (isReadonly) {
|
||||
return;
|
||||
}
|
||||
const newTableFieldsList = [
|
||||
...tableFieldsList,
|
||||
{
|
||||
nanoid: nanoid(),
|
||||
name: '',
|
||||
desc: '',
|
||||
type: undefined as unknown as FieldItemType,
|
||||
must_required: false,
|
||||
},
|
||||
];
|
||||
|
||||
setTableFieldsList(newTableFieldsList);
|
||||
|
||||
if (triggerFocus) {
|
||||
setTimeout(() => {
|
||||
inputRef.current?.triggerFocus?.();
|
||||
scrollRef.current?.scrollIntoView({
|
||||
block: 'end',
|
||||
behavior: 'smooth',
|
||||
});
|
||||
}, 100);
|
||||
}
|
||||
};
|
||||
|
||||
const verifyTableFields = (trigger: TriggerType) => {
|
||||
setTableFieldsList(newTableFieldsList =>
|
||||
validateFields(newTableFieldsList, trigger),
|
||||
);
|
||||
};
|
||||
|
||||
const verifyAllBeforeSave = async (): Promise<boolean> => {
|
||||
// 触发 tableFields 校验
|
||||
const validatedTableFieldsList = validateFields(tableFieldsList, 'save');
|
||||
setTableFieldsList(validatedTableFieldsList);
|
||||
|
||||
// 触发并校验 tableBasicInfo
|
||||
try {
|
||||
// @ts-expect-error -- linter-disable-autofix
|
||||
await tableBasicInfoFormRef.current.formApi.validate(['name']);
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 校验 tableFields
|
||||
if (
|
||||
validatedTableFieldsList.find(i =>
|
||||
Object.keys(i.errorMapper || {}).find(
|
||||
j => !!i?.errorMapper?.[j]?.length,
|
||||
),
|
||||
)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 校验 tableFields 是否为空
|
||||
if (isEmptyList) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const save = async () => {
|
||||
// @ts-expect-error -- linter-disable-autofix
|
||||
const tableBasicInfo = tableBasicInfoFormRef.current.formApi.getValues();
|
||||
|
||||
if (isModify) {
|
||||
sendTeaEvent(EVENT_NAMES.edit_table_click, {
|
||||
need_login: true,
|
||||
have_access: true,
|
||||
bot_id: botId,
|
||||
table_name: tableBasicInfo.name,
|
||||
});
|
||||
} else {
|
||||
sendTeaEvent(EVENT_NAMES.create_table_click, {
|
||||
need_login: true,
|
||||
have_access: true,
|
||||
bot_id: botId,
|
||||
table_name: tableBasicInfo.name,
|
||||
database_create_type: createType,
|
||||
});
|
||||
}
|
||||
|
||||
let resp;
|
||||
const params: InsertBotTableRequest['bot_table'] = {
|
||||
bot_id: botId,
|
||||
table_name: tableBasicInfo.name,
|
||||
table_desc: tableBasicInfo.desc || '',
|
||||
extra_info: {
|
||||
prompt_disabled: String(tableBasicInfo.prompt_disabled ? false : true),
|
||||
},
|
||||
field_list: tableFieldsList
|
||||
.filter(i => !!i.name && !i.isSystemField)
|
||||
.map(i => ({
|
||||
name: i.name,
|
||||
desc: i.desc || '',
|
||||
type: i.type,
|
||||
must_required: i.must_required,
|
||||
id: i.id,
|
||||
alterId: i.alterId,
|
||||
})),
|
||||
rw_mode: tableBasicInfo.readAndWriteMode,
|
||||
};
|
||||
|
||||
if (!isModify) {
|
||||
try {
|
||||
resp = await MemoryApi.InsertBotTable({
|
||||
bot_table: params,
|
||||
});
|
||||
} catch (error) {
|
||||
dataReporter.errorEvent(DataNamespace.DATABASE, {
|
||||
eventName: REPORT_EVENTS.DatabaseAddTable,
|
||||
error: error as Error,
|
||||
});
|
||||
handleContentCheckError(error as Error);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
resp = await MemoryApi.AlterBotTable({
|
||||
bot_table: {
|
||||
...params,
|
||||
id: initialData.tableId,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
dataReporter.errorEvent(DataNamespace.DATABASE, {
|
||||
eventName: REPORT_EVENTS.DatabaseAlterTable,
|
||||
error: error as Error,
|
||||
});
|
||||
handleContentCheckError(error as Error);
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (onSave) {
|
||||
await onSave({
|
||||
response: resp,
|
||||
});
|
||||
}
|
||||
onCancel?.();
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
try {
|
||||
enableReadonly();
|
||||
|
||||
const validateRes = await verifyAllBeforeSave();
|
||||
if (!validateRes) {
|
||||
return;
|
||||
}
|
||||
|
||||
await save();
|
||||
} finally {
|
||||
disableReadonly();
|
||||
}
|
||||
};
|
||||
|
||||
const getTableNameErrorMessage = (v: string) => {
|
||||
if (!v) {
|
||||
return I18n.t('db_add_table_name_tips');
|
||||
}
|
||||
const errList = validateNaming(v);
|
||||
if (errList.length > 0) {
|
||||
return errList.join('; ');
|
||||
}
|
||||
};
|
||||
|
||||
// 初始化 ref 属性
|
||||
useImperativeHandle<DatabaseTableStructureRef, DatabaseTableStructureRef>(
|
||||
ref,
|
||||
() => ({
|
||||
async submit() {
|
||||
return await handleSave();
|
||||
},
|
||||
async validate() {
|
||||
const res = await verifyAllBeforeSave();
|
||||
return res;
|
||||
},
|
||||
setTableFieldsList,
|
||||
isReadonly,
|
||||
// @ts-expect-error -- linter-disable-autofix
|
||||
tableBasicInfoFormRef,
|
||||
tableFieldsList,
|
||||
}),
|
||||
[isReadonly, tableFieldsList, tableBasicInfoFormRef],
|
||||
);
|
||||
|
||||
// 自定义表单项组件
|
||||
const FormInputInner: FC<any> = useCallback(
|
||||
p => {
|
||||
const { onChange, value, onBlur, validateStatus } = p;
|
||||
const errorMessage =
|
||||
validateStatus === 'error' ? getTableNameErrorMessage(value) : '';
|
||||
return (
|
||||
<SLInput
|
||||
value={value}
|
||||
handleChange={onChange}
|
||||
handleBlur={onBlur}
|
||||
inputProps={{
|
||||
'data-testid': BotE2e.BotDatabaseAddModalTableNameInput,
|
||||
disabled: !forceEdit && (isReadonly || isModify),
|
||||
placeholder: I18n.t('db_add_table_name_tips'),
|
||||
}}
|
||||
onFocusPopoverProps={{
|
||||
position: 'left',
|
||||
content: <KeyTipsNode />,
|
||||
}}
|
||||
errorMsgFloat
|
||||
errorMsg={errorMessage}
|
||||
className={classNames({
|
||||
[s['form-input-error']]: validateStatus === 'error',
|
||||
})}
|
||||
/>
|
||||
);
|
||||
},
|
||||
[isReadonly, isModify, forceEdit],
|
||||
);
|
||||
const FormInput = useMemo(
|
||||
() =>
|
||||
withField(FormInputInner, {
|
||||
valueKey: 'value',
|
||||
onKeyChangeFnName: 'onChange',
|
||||
}),
|
||||
[],
|
||||
);
|
||||
|
||||
// 校验是否 disable 下一步按钮
|
||||
useComputingEnableGoToNextStep?.(tableFieldsList);
|
||||
|
||||
const dataSource = enableAdd
|
||||
? [...tableFieldsList, { operate: 'add' }]
|
||||
: tableFieldsList;
|
||||
|
||||
const resetContentCheckErrorMsg = () => {
|
||||
setContentCheckErrorMsg('');
|
||||
};
|
||||
|
||||
return loading ? (
|
||||
<Spin
|
||||
style={{ height: '100%', width: '100%' }}
|
||||
tip={loadingTips}
|
||||
wrapperClassName={s.spin}
|
||||
/>
|
||||
) : (
|
||||
<div className={s['table-structure-wrapper']}>
|
||||
<Form<TableBasicInfo>
|
||||
// @ts-expect-error -- linter-disable-autofix
|
||||
ref={tableBasicInfoFormRef}
|
||||
layout="vertical"
|
||||
initValues={{
|
||||
name: initialData.name,
|
||||
desc: initialData.desc,
|
||||
prompt_disabled: isModify
|
||||
? initialData.extra_info?.prompt_disabled === 'true'
|
||||
? false
|
||||
: true
|
||||
: true,
|
||||
readAndWriteMode:
|
||||
initialData.readAndWriteMode || BotTableRWMode.LimitedReadWrite,
|
||||
}}
|
||||
className={s['table-structure-form']}
|
||||
onValueChange={(values, changedV) => {
|
||||
if ('name' in changedV || 'desc' in changedV) {
|
||||
resetContentCheckErrorMsg();
|
||||
}
|
||||
if (values.readAndWriteMode === BotTableRWMode.UnlimitedReadWrite) {
|
||||
setTableFieldsList(state => {
|
||||
if (state.some(i => i.isSystemField)) {
|
||||
return state;
|
||||
}
|
||||
return [...SYSTEM_FIELDS, ...state];
|
||||
});
|
||||
} else if (
|
||||
values.readAndWriteMode === BotTableRWMode.LimitedReadWrite
|
||||
) {
|
||||
setTableFieldsList(state => {
|
||||
if (state.some(i => i.isSystemField)) {
|
||||
return state.filter(i => !i.isSystemField);
|
||||
}
|
||||
return state;
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
<FormInput
|
||||
field="name"
|
||||
label={{
|
||||
text: I18n.t('db_add_table_name'),
|
||||
required: true,
|
||||
}}
|
||||
// @ts-expect-error -- linter-disable-autofix
|
||||
validate={v => getTableNameErrorMessage(v)}
|
||||
noErrorMessage
|
||||
trigger={['change', 'blur']}
|
||||
fieldClassName={s['table-name-form-field']}
|
||||
/>
|
||||
<Form.TextArea
|
||||
data-testid={BotE2e.BotDatabaseAddModalTableDescInput}
|
||||
field="desc"
|
||||
label={I18n.t('db_add_table_desc')}
|
||||
disabled={isReadonly}
|
||||
rows={2}
|
||||
placeholder={I18n.t('db_add_table_desc_tips')}
|
||||
fieldClassName={s['table-desc-form-field']}
|
||||
/>
|
||||
<Row type="flex" justify="space-between">
|
||||
<Col span={12}>
|
||||
{readAndWriteModeOptions ? (
|
||||
<Form.Select
|
||||
data-testid={BotE2e.BotDatabaseAddModalTableQueryModeSelect}
|
||||
field="readAndWriteMode"
|
||||
style={{ width: '267px' }}
|
||||
label={{
|
||||
text: I18n.t('db_table_0129_001'),
|
||||
extra: (
|
||||
<Popover
|
||||
className={s.read_mode_popover}
|
||||
content={
|
||||
<div>
|
||||
<article>
|
||||
{RW_MODE_OPTIONS_MAP[readAndWriteModeOptions].map(
|
||||
i => (
|
||||
<p className={s['th-tip-dot']}>
|
||||
{RW_MODE_OPTIONS_CONFIG[i].tips}
|
||||
</p>
|
||||
),
|
||||
)}
|
||||
</article>
|
||||
</div>
|
||||
}
|
||||
position="top"
|
||||
showArrow
|
||||
>
|
||||
<Icon
|
||||
svg={<InfoSVG />}
|
||||
className={s['form-item-label-tooltip-icon']}
|
||||
/>
|
||||
</Popover>
|
||||
),
|
||||
}}
|
||||
dropdownClassName={s['table-setting-option']}
|
||||
optionList={RW_MODE_OPTIONS_MAP[readAndWriteModeOptions].map(
|
||||
i => ({
|
||||
label: RW_MODE_OPTIONS_CONFIG[i].label,
|
||||
value: i,
|
||||
}),
|
||||
)}
|
||||
/>
|
||||
) : null}
|
||||
</Col>
|
||||
<Col span={readAndWriteModeOptions ? MIN_COL : MAX_COL}>
|
||||
<Form.Checkbox
|
||||
style={{
|
||||
width: 24,
|
||||
height: 24,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
className={s['form-item-checkbox']}
|
||||
labelPosition="left"
|
||||
field="prompt_disabled"
|
||||
label={{
|
||||
style: {
|
||||
fontSize: '14px',
|
||||
fontWeight: 400,
|
||||
padding: '0 8px 0 0',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
color: 'var(--Light-usage-text---color-text-0, #1D1C23)',
|
||||
},
|
||||
text: I18n.t('database_240618_01'),
|
||||
extra: (
|
||||
<Popover
|
||||
className={s.prompt_disabled_popover}
|
||||
content={I18n.t('database_240520_03')}
|
||||
position="top"
|
||||
showArrow
|
||||
>
|
||||
<Icon
|
||||
svg={<InfoSVG />}
|
||||
className={s['form-item-label-tooltip-icon']}
|
||||
/>
|
||||
</Popover>
|
||||
),
|
||||
}}
|
||||
></Form.Checkbox>
|
||||
</Col>
|
||||
</Row>
|
||||
</Form>
|
||||
|
||||
{isExceedRowMaxLimit ? (
|
||||
<Banner
|
||||
type="warning"
|
||||
description={I18n.t('db_table_0126_027', {
|
||||
ColumNum: maxColumnNum,
|
||||
})}
|
||||
className={s['max-row-banner']}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<UITable
|
||||
tableProps={{
|
||||
columns: [
|
||||
{
|
||||
dataIndex: 'name',
|
||||
title: (
|
||||
<ColumnHeader
|
||||
label={I18n.t('db_add_table_field_name')}
|
||||
required
|
||||
tips={
|
||||
<div className={s['th-tip-name']}>
|
||||
<span style={{ width: 494, marginBottom: 8 }}>
|
||||
{I18n.t('db_add_table_field_name_tips')}
|
||||
</span>
|
||||
<Image
|
||||
preview={false}
|
||||
width={494}
|
||||
height={163}
|
||||
src={keyExample}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
),
|
||||
render: (text, record, index) =>
|
||||
record.operate !== 'add' ? (
|
||||
<SLInput
|
||||
style={{ position: 'static' }}
|
||||
onRef={inputRef}
|
||||
value={record.name}
|
||||
inputProps={{
|
||||
'data-testid': BotE2e.BotDatabaseAddModalFieldNameInput,
|
||||
'data-dtestid': BotE2e.BotDatabaseAddModalFieldNameInput,
|
||||
disabled: isReadonly || record.isSystemField,
|
||||
placeholder: 'Enter Name',
|
||||
}}
|
||||
errorMsgFloat
|
||||
onFocusPopoverProps={{
|
||||
position: 'left',
|
||||
content: <KeyTipsNode />,
|
||||
}}
|
||||
errorMsg={tableFieldsList[index]?.errorMapper?.name?.join(
|
||||
'; ',
|
||||
)}
|
||||
handleChange={v => {
|
||||
const newTableMemoryList = [...tableFieldsList];
|
||||
newTableMemoryList[index].name = v;
|
||||
setTableFieldsList(newTableMemoryList);
|
||||
verifyTableFields('change');
|
||||
resetContentCheckErrorMsg();
|
||||
}}
|
||||
handleBlur={() => {
|
||||
verifyTableFields('blur');
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<div
|
||||
ref={scrollRef}
|
||||
data-testid={BotE2e.BotDatabaseAddModalAddBtn}
|
||||
>
|
||||
{isRowMaxLimit ? (
|
||||
<div style={{ paddingRight: 10 }}>
|
||||
<Tooltip
|
||||
position="top"
|
||||
content={I18n.t('bot_database_add_field', {
|
||||
number: maxColumnNum,
|
||||
})}
|
||||
>
|
||||
<Button
|
||||
theme="light"
|
||||
style={{ width: 240 }}
|
||||
type="tertiary"
|
||||
disabled
|
||||
icon={<IconAdd />}
|
||||
>
|
||||
{I18n.t('bot_userProfile_add')}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
) : (
|
||||
<Button
|
||||
style={{ width: 240 }}
|
||||
theme="light"
|
||||
type="tertiary"
|
||||
disabled={isReadonly}
|
||||
onClick={() => handleAdd(true)}
|
||||
icon={<IconAdd />}
|
||||
>
|
||||
{I18n.t('bot_userProfile_add')}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
width: 246,
|
||||
},
|
||||
{
|
||||
dataIndex: 'desc',
|
||||
title: (
|
||||
<ColumnHeader
|
||||
label={I18n.t('db_add_table_field_desc')}
|
||||
required={false}
|
||||
tips={
|
||||
<article style={{ width: 327 }}>
|
||||
{I18n.t('db_add_table_field_desc_tips')}
|
||||
</article>
|
||||
}
|
||||
/>
|
||||
),
|
||||
render: (text, record, index) =>
|
||||
record.operate !== 'add' ? (
|
||||
<SLInput
|
||||
value={record.desc}
|
||||
maxCount={300}
|
||||
inputProps={{
|
||||
'data-testid': `${BotE2e.BotDatabaseAddModalFieldDescInput}.${index}.${record.name}`,
|
||||
'data-dtestid': `${BotE2e.BotDatabaseAddModalFieldDescInput}.${index}.${record.name}`,
|
||||
maxLength: 300,
|
||||
disabled: isReadonly || record.isSystemField,
|
||||
placeholder: I18n.t(
|
||||
'bot_edit_variable_description_placeholder',
|
||||
),
|
||||
}}
|
||||
errorMsgFloat
|
||||
handleChange={v => {
|
||||
const newTableMemoryList = [...tableFieldsList];
|
||||
newTableMemoryList[index].desc = v;
|
||||
setTableFieldsList(newTableMemoryList);
|
||||
resetContentCheckErrorMsg();
|
||||
}}
|
||||
/>
|
||||
) : null,
|
||||
width: 376,
|
||||
},
|
||||
{
|
||||
dataIndex: 'type',
|
||||
title: (
|
||||
<ColumnHeader
|
||||
label={I18n.t('db_add_table_field_type')}
|
||||
required={true}
|
||||
tips={
|
||||
<article style={{ width: 327 }}>
|
||||
{I18n.t('db_add_table_field_type_tips')}
|
||||
</article>
|
||||
}
|
||||
/>
|
||||
),
|
||||
render: (text, record, index) =>
|
||||
record.operate !== 'add' ? (
|
||||
<SLSelect
|
||||
value={record.type}
|
||||
selectProps={{
|
||||
'data-testid': `${BotE2e.BotDatabaseAddModalFieldTypeSelect}.${index}.${record.name}`,
|
||||
'data-dtestid': `${BotE2e.BotDatabaseAddModalFieldTypeSelect}.${index}.${record.name}`,
|
||||
disabled:
|
||||
isReadonly ||
|
||||
(isModify && !!record.id) ||
|
||||
record.isSystemField,
|
||||
placeholder: I18n.t('db_table_save_exception_fieldtype'),
|
||||
optionList: FIELD_TYPE_OPTIONS,
|
||||
}}
|
||||
errorMsgFloat
|
||||
errorMsg={tableFieldsList[index]?.errorMapper?.type?.join(
|
||||
'; ',
|
||||
)}
|
||||
handleChange={v => {
|
||||
const newTableMemoryList = [...tableFieldsList];
|
||||
newTableMemoryList[index].type = v as FieldItemType;
|
||||
setTableFieldsList(newTableMemoryList);
|
||||
verifyTableFields('change');
|
||||
}}
|
||||
/>
|
||||
) : null,
|
||||
width: 211,
|
||||
},
|
||||
{
|
||||
dataIndex: 'must_required',
|
||||
title: (
|
||||
<ColumnHeader
|
||||
label={I18n.t('db_add_table_field_necessary')}
|
||||
required={false}
|
||||
tips={
|
||||
<article style={{ width: 327 }}>
|
||||
<p className={s['th-tip-dot']}>
|
||||
{I18n.t('db_add_table_field_necessary_tips1')}
|
||||
</p>
|
||||
<p className={s['th-tip-dot']}>
|
||||
{I18n.t('db_add_table_field_necessary_tips2')}
|
||||
</p>
|
||||
</article>
|
||||
}
|
||||
/>
|
||||
),
|
||||
render: (text, record, index) =>
|
||||
record.operate !== 'add' ? (
|
||||
<div style={{ display: 'flex', alignItems: 'center' }}>
|
||||
<Switch
|
||||
data-testid={`${BotE2e.BotDatabaseAddModalFieldRequiredSwitch}.${index}.${record.name}`}
|
||||
data-dtestid={`${BotE2e.BotDatabaseAddModalFieldRequiredSwitch}.${index}.${record.name}`}
|
||||
style={{ margin: '4px 0 4px 12px' }}
|
||||
disabled={
|
||||
isReadonly ||
|
||||
record.disableMustRequired ||
|
||||
record.isSystemField
|
||||
}
|
||||
checked={record.must_required}
|
||||
onChange={v => {
|
||||
const newTableMemoryList = [...tableFieldsList];
|
||||
newTableMemoryList[index].must_required = v;
|
||||
setTableFieldsList(newTableMemoryList);
|
||||
}}
|
||||
aria-label="a switch for semi demo"
|
||||
/>
|
||||
</div>
|
||||
) : null,
|
||||
width: 108,
|
||||
},
|
||||
{
|
||||
dataIndex: 'operate',
|
||||
title: I18n.t('db_table_0126_021'),
|
||||
render: (text, record, index) =>
|
||||
record.operate !== 'add' ? (
|
||||
<UITableAction
|
||||
deleteProps={{
|
||||
handleClick: () => {
|
||||
if (isReadonly) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newTableMemoryList = [
|
||||
...tableFieldsList.slice(0, index),
|
||||
...tableFieldsList.slice(index + 1),
|
||||
];
|
||||
|
||||
setTableFieldsList(newTableMemoryList);
|
||||
verifyTableFields('change');
|
||||
onDeleteField?.(newTableMemoryList);
|
||||
},
|
||||
popconfirm: {
|
||||
defaultVisible: false,
|
||||
visible: false,
|
||||
},
|
||||
tooltip: {
|
||||
content: I18n.t('datasets_table_title_actions_delete'),
|
||||
},
|
||||
disabled: record.isSystemField,
|
||||
}}
|
||||
editProps={{
|
||||
hide: true,
|
||||
}}
|
||||
/>
|
||||
) : null,
|
||||
width: 85,
|
||||
},
|
||||
],
|
||||
dataSource,
|
||||
pagination: false,
|
||||
className: s['table-structure-table'],
|
||||
rowKey: 'nanoid',
|
||||
}}
|
||||
wrapperClassName={s['table-structure-table-wrapper']}
|
||||
/>
|
||||
{isEmptyList ? (
|
||||
<div className={s['table-empty-tips']}>
|
||||
{I18n.t('db_table_save_exception_nofield')}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,64 @@
|
||||
.input-wrapper {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
|
||||
// semi 原有的 disable 样式不满足需求,需要更明显的文字颜色
|
||||
:global {
|
||||
.semi-input-wrapper-disabled {
|
||||
-webkit-text-fill-color: rgba(29, 28, 35, 100%);
|
||||
}
|
||||
|
||||
.semi-input-suffix {
|
||||
-webkit-text-fill-color: var(--semi-color-disabled-text, rgba(56, 55, 67, 20%))
|
||||
}
|
||||
}
|
||||
|
||||
span {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.error-wrapper {
|
||||
:global {
|
||||
.semi-input-wrapper {
|
||||
border: 1px solid @error-red;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.error-content {
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.error-float {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.error-text {
|
||||
position: absolute;
|
||||
padding-top: 2px;
|
||||
font-size: 12px;
|
||||
color: @error-red;
|
||||
}
|
||||
|
||||
.input {
|
||||
input {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
|
||||
.limit-count {
|
||||
overflow: hidden;
|
||||
|
||||
padding-right: 12px;
|
||||
padding-left: 8px;
|
||||
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
line-height: 16px;
|
||||
color: var(--light-usage-text-color-text-3, rgba(28, 31, 35, 35%));
|
||||
}
|
||||
@@ -0,0 +1,197 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React, {
|
||||
type ComponentProps,
|
||||
useRef,
|
||||
useImperativeHandle,
|
||||
useMemo,
|
||||
useEffect,
|
||||
type ForwardedRef,
|
||||
} from 'react';
|
||||
|
||||
import isNumber from 'lodash-es/isNumber';
|
||||
import cs from 'classnames';
|
||||
import { useReactive } from 'ahooks';
|
||||
import { type TooltipProps } from '@coze-arch/bot-semi/Tooltip';
|
||||
import { type PopoverProps } from '@coze-arch/bot-semi/Popover';
|
||||
import { type InputProps } from '@coze-arch/bot-semi/Input';
|
||||
import { Input, Popover, Tooltip } from '@coze-arch/bot-semi';
|
||||
|
||||
import s from './index.module.less';
|
||||
|
||||
export interface SLInputRefType {
|
||||
triggerFocus?: () => void;
|
||||
}
|
||||
|
||||
export type SLInputProps = ComponentProps<typeof Input> & {
|
||||
value: string | undefined;
|
||||
onRef?: ForwardedRef<SLInputRefType>;
|
||||
ellipsis?: boolean;
|
||||
handleChange?: (v: string) => void;
|
||||
handleBlur?: (v: string) => void;
|
||||
handleFocus?: (v: string) => void;
|
||||
ellipsisPopoverProps?: PopoverProps;
|
||||
onFocusPopoverProps?: PopoverProps;
|
||||
tooltipProps?: TooltipProps;
|
||||
inputProps?: InputProps & { 'data-dtestid'?: string; 'data-testid'?: string };
|
||||
errorMsg?: string;
|
||||
errorMsgFloat?: boolean;
|
||||
maxCount?: number;
|
||||
className?: string;
|
||||
style?: React.CSSProperties;
|
||||
};
|
||||
|
||||
export const SLInput: React.FC<SLInputProps> = props => {
|
||||
const { ellipsis = true, maxCount, errorMsgFloat } = props;
|
||||
const showCount = isNumber(maxCount) && maxCount > 0;
|
||||
useImperativeHandle(props.onRef, () => ({
|
||||
triggerFocus,
|
||||
}));
|
||||
const $state = useReactive({
|
||||
value: props.value,
|
||||
inputOnFocus: false,
|
||||
inputEle: false,
|
||||
});
|
||||
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const triggerFocus = () => {
|
||||
$state.inputEle = true;
|
||||
inputRef?.current?.focus();
|
||||
};
|
||||
|
||||
const onFocus = () => {
|
||||
$state.inputOnFocus = true;
|
||||
$state.inputEle = true;
|
||||
props?.handleFocus?.($state.value || '');
|
||||
};
|
||||
|
||||
const onBlur = (e: React.FocusEvent<HTMLInputElement>) => {
|
||||
$state.inputOnFocus = false;
|
||||
props?.handleBlur?.($state.value || '');
|
||||
props?.onBlur?.(e);
|
||||
$state.inputEle = false;
|
||||
};
|
||||
|
||||
const onChange = (v: string) => {
|
||||
$state.value = v;
|
||||
props?.handleChange?.(v);
|
||||
};
|
||||
|
||||
const onclick = () => {
|
||||
if (!$state.inputEle) {
|
||||
setTimeout(() => {
|
||||
inputRef?.current?.focus();
|
||||
}, 10);
|
||||
}
|
||||
$state.inputEle = true;
|
||||
};
|
||||
const hasEllipsis = useMemo(() => {
|
||||
const clientWidth = inputRef.current?.clientWidth || 0;
|
||||
const scrollWidth = inputRef.current?.scrollWidth || 0;
|
||||
return clientWidth < scrollWidth - 1;
|
||||
}, [
|
||||
ellipsis,
|
||||
$state.inputOnFocus,
|
||||
$state.value,
|
||||
inputRef.current?.clientWidth,
|
||||
inputRef.current?.scrollWidth,
|
||||
$state.inputEle,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
$state.value = props.value;
|
||||
}, [props.value]);
|
||||
|
||||
const LimitCountNode = (
|
||||
<span className={s['limit-count']}>
|
||||
{$state.value?.length || 0}/{maxCount}
|
||||
</span>
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cs(s['input-wrapper'], props.className)}
|
||||
style={props.style}
|
||||
>
|
||||
{!$state.inputEle && hasEllipsis ? (
|
||||
<Tooltip
|
||||
content={
|
||||
<article
|
||||
style={{
|
||||
maxWidth: 200,
|
||||
wordWrap: 'break-word',
|
||||
wordBreak: 'normal',
|
||||
}}
|
||||
>
|
||||
{$state.value}
|
||||
</article>
|
||||
}
|
||||
position={'top'}
|
||||
showArrow
|
||||
mouseEnterDelay={300}
|
||||
{...props.tooltipProps}
|
||||
>
|
||||
<div
|
||||
className={cs(props?.errorMsg ? s['error-wrapper'] : null)}
|
||||
onClick={onclick}
|
||||
>
|
||||
<Input
|
||||
{...props.inputProps}
|
||||
ref={inputRef}
|
||||
value={$state.value}
|
||||
className={ellipsis ? s.input : ''}
|
||||
suffix={showCount ? LimitCountNode : undefined}
|
||||
></Input>
|
||||
</div>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<div className={cs(props?.errorMsg ? s['error-wrapper'] : null)}>
|
||||
<Popover
|
||||
{...props.onFocusPopoverProps}
|
||||
trigger="custom"
|
||||
visible={
|
||||
Boolean(props.onFocusPopoverProps?.content) && $state.inputOnFocus
|
||||
}
|
||||
showArrow
|
||||
>
|
||||
<Input
|
||||
{...props.inputProps}
|
||||
ref={inputRef}
|
||||
value={$state.value}
|
||||
className={ellipsis ? s.input : ''}
|
||||
onChange={onChange}
|
||||
onFocus={onFocus}
|
||||
onBlur={onBlur}
|
||||
suffix={showCount ? LimitCountNode : undefined}
|
||||
></Input>
|
||||
</Popover>
|
||||
</div>
|
||||
)}
|
||||
{props?.errorMsg ? (
|
||||
<div
|
||||
className={cs({
|
||||
[s['error-content']]: true,
|
||||
[s['error-float']]: Boolean(errorMsgFloat),
|
||||
})}
|
||||
>
|
||||
<div className={s['error-text']}>{props?.errorMsg}</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,54 @@
|
||||
.select-wrapper {
|
||||
width: 100%;
|
||||
|
||||
// semi 原有的 disable 样式不满足需求,需要更明显的文字颜色
|
||||
:global {
|
||||
.semi-select-disabled .semi-select-selection {
|
||||
color: rgba(29, 28, 35, 100%);
|
||||
}
|
||||
|
||||
.semi-select-disabled .semi-select-arrow {
|
||||
color: rgba(29, 28, 35, 60%);
|
||||
}
|
||||
}
|
||||
|
||||
span {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.error-wrapper {
|
||||
:global {
|
||||
.semi-select {
|
||||
border: 1px solid @error-red;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.error-content {
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.error-float {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.error-text {
|
||||
position: absolute;
|
||||
padding-top: 2px;
|
||||
font-size: 12px;
|
||||
color: @error-red;
|
||||
}
|
||||
|
||||
|
||||
.selected-option {
|
||||
:global {
|
||||
.semi-select-option-selected {
|
||||
.semi-select-option-icon {
|
||||
color: #4D53E8
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
* 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 classnames from 'classnames';
|
||||
import { type SelectProps } from '@coze-arch/bot-semi/Select';
|
||||
import { type InputProps } from '@coze-arch/bot-semi/Input';
|
||||
import { Select } from '@coze-arch/bot-semi';
|
||||
|
||||
import s from './index.module.less';
|
||||
|
||||
export interface SLSelectRefType {
|
||||
triggerFocus?: () => void;
|
||||
}
|
||||
|
||||
export type SLSelectProps = InputProps & {
|
||||
value: SelectProps['value'];
|
||||
handleChange?: (v: SelectProps['value']) => void;
|
||||
errorMsg?: string;
|
||||
errorMsgFloat?: boolean;
|
||||
selectProps?: SelectProps & {
|
||||
'data-testid'?: string;
|
||||
'data-dtestid'?: string;
|
||||
};
|
||||
};
|
||||
|
||||
export const SLSelect: React.FC<SLSelectProps> = props => {
|
||||
const { errorMsg, errorMsgFloat } = props;
|
||||
return (
|
||||
<div
|
||||
className={classnames({
|
||||
[s['select-wrapper']]: true,
|
||||
[s['error-wrapper']]: Boolean(errorMsg),
|
||||
})}
|
||||
>
|
||||
<Select
|
||||
{...props.selectProps}
|
||||
style={{ width: '100%' }}
|
||||
value={props.value}
|
||||
onChange={v => {
|
||||
props?.handleChange?.(v);
|
||||
}}
|
||||
dropdownClassName={s['selected-option']}
|
||||
/>
|
||||
{errorMsg ? (
|
||||
<div
|
||||
className={classnames({
|
||||
[s['error-content']]: true,
|
||||
[s['error-float']]: Boolean(errorMsgFloat),
|
||||
})}
|
||||
>
|
||||
<div className={s['error-text']}>{errorMsg}</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user