feat: manually mirror opencoze's code from bytedance
Change-Id: I09a73aadda978ad9511264a756b2ce51f5761adf
This commit is contained in:
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { Steps } from '@coze-arch/coze-design';
|
||||
|
||||
export enum BatchImportStep {
|
||||
Upload,
|
||||
Config,
|
||||
Preview,
|
||||
Process,
|
||||
}
|
||||
|
||||
export interface BatchImportStepsProps {
|
||||
step: BatchImportStep;
|
||||
}
|
||||
|
||||
export function BatchImportSteps({ step }: BatchImportStepsProps) {
|
||||
return (
|
||||
<Steps
|
||||
type="basic"
|
||||
hasLine={false}
|
||||
current={step}
|
||||
className={classNames(
|
||||
'my-[24px] justify-center',
|
||||
'[&_.semi-steps-item]:flex-none',
|
||||
'[&_.semi-steps-item-title]:!max-w-[unset]',
|
||||
)}
|
||||
>
|
||||
<Steps.Step title={I18n.t('db_optimize_014')} />
|
||||
<Steps.Step title={I18n.t('db_optimize_015')} />
|
||||
<Steps.Step title={I18n.t('db_optimize_016')} />
|
||||
<Steps.Step title={I18n.t('db_optimize_017')} />
|
||||
</Steps>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
/*
|
||||
* 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 } from 'react';
|
||||
|
||||
import {
|
||||
type UnitItem,
|
||||
UploadStatus,
|
||||
} from '@coze-data/knowledge-resource-processor-core';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { type TableType, type TableSheet } from '@coze-arch/bot-api/memory';
|
||||
import { Button, Modal } from '@coze-arch/coze-design';
|
||||
|
||||
import { type TableFieldData } from '../database-table-data/type';
|
||||
import { StepUpload } from './steps/upload';
|
||||
import { StepProcess } from './steps/process';
|
||||
import { StepPreview } from './steps/preview';
|
||||
import { StepConfig } from './steps/config';
|
||||
import { BatchImportStep, BatchImportSteps } from './import-steps';
|
||||
|
||||
export interface BatchImportModalProps {
|
||||
visible: boolean;
|
||||
databaseId: string;
|
||||
tableFields: TableFieldData[];
|
||||
tableType: TableType;
|
||||
connectorId?: string;
|
||||
onClose?: () => void;
|
||||
onComplete?: () => void;
|
||||
}
|
||||
|
||||
export function BatchImportModal({
|
||||
visible,
|
||||
databaseId,
|
||||
tableFields,
|
||||
tableType,
|
||||
connectorId,
|
||||
onClose,
|
||||
onComplete,
|
||||
}: BatchImportModalProps) {
|
||||
const [currentStep, setCurrentStep] = useState(BatchImportStep.Upload);
|
||||
const [unitList, setUnitList] = useState<UnitItem[]>([]);
|
||||
const [tableSheet, setTableSheet] = useState<TableSheet>();
|
||||
|
||||
const resetSteps = () => {
|
||||
setCurrentStep(BatchImportStep.Upload);
|
||||
setUnitList([]);
|
||||
setTableSheet(undefined);
|
||||
};
|
||||
|
||||
const getNextDisabled = () => {
|
||||
switch (currentStep) {
|
||||
case BatchImportStep.Upload:
|
||||
return (
|
||||
unitList.length <= 0 ||
|
||||
unitList.some(item => item.status !== UploadStatus.SUCCESS)
|
||||
);
|
||||
case BatchImportStep.Config:
|
||||
return !tableSheet;
|
||||
case BatchImportStep.Preview:
|
||||
return false;
|
||||
case BatchImportStep.Process:
|
||||
return false;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
visible={visible}
|
||||
title={I18n.t('db_optimize_013')}
|
||||
onCancel={onClose}
|
||||
width={1120}
|
||||
className="[&_.semi-modal-content]:min-h-[520px]"
|
||||
footer={
|
||||
<>
|
||||
{currentStep !== BatchImportStep.Process ? (
|
||||
<>
|
||||
<Button
|
||||
color="primary"
|
||||
disabled={currentStep === BatchImportStep.Upload}
|
||||
onClick={() => setCurrentStep(currentStep - 1)}
|
||||
>
|
||||
{I18n.t('db_optimize_020')}
|
||||
</Button>
|
||||
<Button
|
||||
disabled={getNextDisabled()}
|
||||
onClick={() => setCurrentStep(currentStep + 1)}
|
||||
>
|
||||
{I18n.t('db_optimize_021')}
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<Button
|
||||
onClick={() => {
|
||||
onClose?.();
|
||||
onComplete?.();
|
||||
resetSteps();
|
||||
}}
|
||||
>
|
||||
{I18n.t('db2_004')}
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
>
|
||||
<BatchImportSteps step={currentStep} />
|
||||
{currentStep === BatchImportStep.Upload ? (
|
||||
<StepUpload
|
||||
databaseId={databaseId}
|
||||
tableType={tableType}
|
||||
unitList={unitList}
|
||||
onUnitListChange={setUnitList}
|
||||
/>
|
||||
) : null}
|
||||
{currentStep === BatchImportStep.Config ? (
|
||||
<StepConfig
|
||||
databaseId={databaseId}
|
||||
tableFields={tableFields}
|
||||
tableType={tableType}
|
||||
fileUri={unitList[0].uri}
|
||||
onTableSheetChange={setTableSheet}
|
||||
/>
|
||||
) : null}
|
||||
{currentStep === BatchImportStep.Preview ? (
|
||||
<StepPreview
|
||||
databaseId={databaseId}
|
||||
tableFields={tableFields}
|
||||
fileUri={unitList[0].uri}
|
||||
tableSheet={tableSheet}
|
||||
/>
|
||||
) : null}
|
||||
{currentStep === BatchImportStep.Process ? (
|
||||
<StepProcess
|
||||
databaseId={databaseId}
|
||||
tableType={tableType}
|
||||
fileItem={unitList[0]}
|
||||
tableSheet={tableSheet}
|
||||
connectorId={connectorId}
|
||||
/>
|
||||
) : null}
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,193 @@
|
||||
/*
|
||||
* 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, useEffect } from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { useRequest } from 'ahooks';
|
||||
import { type TableSettings } from '@coze-data/knowledge-resource-processor-base/types';
|
||||
import { TableSettingBar } from '@coze-data/knowledge-resource-processor-base/components/table-format';
|
||||
import { FIELD_TYPE_OPTIONS } from '@coze-data/database-v2-base/constants';
|
||||
import { DatabaseFieldTitle } from '@coze-data/database-v2-base/components/database-field-title';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import {
|
||||
type TableType,
|
||||
type TableSheet,
|
||||
TableDataType,
|
||||
type GetDocumentTableInfoResponse,
|
||||
ColumnType,
|
||||
FieldItemType,
|
||||
} from '@coze-arch/bot-api/memory';
|
||||
import { MemoryApi } from '@coze-arch/bot-api';
|
||||
import { type ColumnProps, Spin, Table } from '@coze-arch/coze-design';
|
||||
|
||||
import { type TableFieldData } from '../../database-table-data/type';
|
||||
|
||||
type TableSettingsData = Pick<
|
||||
GetDocumentTableInfoResponse,
|
||||
'sheet_list' | 'preview_data'
|
||||
>;
|
||||
|
||||
function tableSettingsToSheet(tableSettings: TableSettings): TableSheet {
|
||||
return {
|
||||
sheet_id: tableSettings?.sheet_id?.toString() ?? '',
|
||||
header_line_idx: tableSettings?.header_line_idx?.toString() ?? '',
|
||||
start_line_idx: tableSettings?.start_line_idx?.toString() ?? '',
|
||||
};
|
||||
}
|
||||
|
||||
export interface StepConfigProps {
|
||||
databaseId: string;
|
||||
tableType: TableType;
|
||||
tableFields: TableFieldData[];
|
||||
fileUri: string;
|
||||
onTableSheetChange: (tableSheet?: TableSheet) => void;
|
||||
}
|
||||
|
||||
export function StepConfig({
|
||||
databaseId,
|
||||
tableType,
|
||||
tableFields,
|
||||
fileUri,
|
||||
onTableSheetChange,
|
||||
}: StepConfigProps) {
|
||||
const [tableData, setTableData] = useState<TableSettingsData>();
|
||||
// 默认值:使用第1个数据表,第1行是表头,第2行开始是数据
|
||||
const [tableSettings, setTableSettings] = useState<TableSettings>({
|
||||
sheet_id: 0,
|
||||
header_line_idx: 0,
|
||||
start_line_idx: 1,
|
||||
});
|
||||
const [tableStructure, setTableStructure] = useState<TableFieldData[]>([]);
|
||||
|
||||
const { loading } = useRequest(
|
||||
() =>
|
||||
MemoryApi.GetTableSchema({
|
||||
database_id: databaseId,
|
||||
source_file: { tos_uri: fileUri },
|
||||
table_data_type: TableDataType.OnlySchema,
|
||||
table_sheet: tableSettingsToSheet(tableSettings),
|
||||
}),
|
||||
{
|
||||
refreshDeps: [fileUri, tableSettings],
|
||||
onSuccess: res => {
|
||||
setTableData({
|
||||
sheet_list: res.sheet_list,
|
||||
preview_data: {}, // TableSettingBar 并没有读取 preview_data,但是在判断它非空
|
||||
});
|
||||
if (res.table_meta) {
|
||||
setTableStructure(
|
||||
res.table_meta.map(column => {
|
||||
// 表结构中有同名字段时,使用原本的类型及描述
|
||||
const matchedField = tableFields.find(
|
||||
field => field.fieldName === column.column_name,
|
||||
);
|
||||
return (
|
||||
matchedField ??
|
||||
({
|
||||
fieldName: column.column_name ?? '-',
|
||||
fieldDescription: column.desc ?? '-',
|
||||
type: convertColumnType(column.column_type),
|
||||
required: false,
|
||||
} satisfies TableFieldData)
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
MemoryApi.ValidateTableSchema({
|
||||
database_id: databaseId,
|
||||
source_file: { tos_uri: fileUri },
|
||||
table_type: tableType,
|
||||
table_sheet: tableSettingsToSheet(tableSettings),
|
||||
})
|
||||
.then(res => {
|
||||
if (!res.schema_valid_result) {
|
||||
onTableSheetChange(tableSettingsToSheet(tableSettings));
|
||||
} else {
|
||||
onTableSheetChange();
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
onTableSheetChange();
|
||||
});
|
||||
}, [tableSettings]);
|
||||
|
||||
return !tableData ? (
|
||||
<Spin size="large" wrapperClassName="w-full h-[288px]" />
|
||||
) : (
|
||||
<>
|
||||
<TableSettingBar
|
||||
data={tableData}
|
||||
tableSettings={tableSettings}
|
||||
setTableSettings={setTableSettings}
|
||||
/>
|
||||
<Table
|
||||
tableProps={{
|
||||
loading,
|
||||
columns: getTableStructureColumns(),
|
||||
dataSource: tableStructure,
|
||||
}}
|
||||
className={classNames(
|
||||
'[&_.semi-table-row-head]:!border-b-[1px]',
|
||||
'[&_.semi-table-row-cell]:!h-[56px]',
|
||||
'[&_.semi-table-row-cell]:!border-b-0',
|
||||
'[&_.semi-table-row-cell]:!bg-none',
|
||||
'[&_.semi-table-row-cell]:!bg-transparent',
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function convertColumnType(type?: ColumnType): FieldItemType {
|
||||
switch (type) {
|
||||
case ColumnType.Text:
|
||||
return FieldItemType.Text;
|
||||
case ColumnType.Number:
|
||||
return FieldItemType.Number;
|
||||
case ColumnType.Date:
|
||||
return FieldItemType.Date;
|
||||
case ColumnType.Float:
|
||||
return FieldItemType.Float;
|
||||
case ColumnType.Boolean:
|
||||
return FieldItemType.Boolean;
|
||||
default:
|
||||
return FieldItemType.Text;
|
||||
}
|
||||
}
|
||||
|
||||
function getTableStructureColumns(): ColumnProps<TableFieldData>[] {
|
||||
return [
|
||||
{
|
||||
title: <DatabaseFieldTitle field={I18n.t('db_add_table_field_name')} />,
|
||||
render: (_, record) => record.fieldName,
|
||||
},
|
||||
{
|
||||
title: <DatabaseFieldTitle field={I18n.t('db_add_table_field_desc')} />,
|
||||
render: (_, record) => record.fieldDescription,
|
||||
},
|
||||
{
|
||||
title: <DatabaseFieldTitle field={I18n.t('db_add_table_field_type')} />,
|
||||
render: (_, record) =>
|
||||
FIELD_TYPE_OPTIONS.find(i => i.value === record.type)?.label ?? '-',
|
||||
},
|
||||
];
|
||||
}
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
import { useMemo, useState } from 'react';
|
||||
|
||||
import { useRequest } from 'ahooks';
|
||||
import {
|
||||
type TableInfo,
|
||||
type TableSettings,
|
||||
} from '@coze-data/knowledge-resource-processor-base/types';
|
||||
import { TablePreview } from '@coze-data/knowledge-resource-processor-base/components/table-format';
|
||||
import {
|
||||
type TableSheet,
|
||||
type DocTableColumn,
|
||||
TableDataType,
|
||||
} from '@coze-arch/bot-api/memory';
|
||||
import { ColumnType } from '@coze-arch/bot-api/knowledge';
|
||||
import { FieldItemType } from '@coze-arch/bot-api/developer_api';
|
||||
import { MemoryApi } from '@coze-arch/bot-api';
|
||||
import { Spin } from '@coze-arch/coze-design';
|
||||
|
||||
import { type TableFieldData } from '../../database-table-data/type';
|
||||
|
||||
function convertFieldItemType(type?: FieldItemType): ColumnType {
|
||||
switch (type) {
|
||||
case FieldItemType.Text:
|
||||
return ColumnType.Text;
|
||||
case FieldItemType.Number:
|
||||
return ColumnType.Number;
|
||||
case FieldItemType.Date:
|
||||
return ColumnType.Date;
|
||||
case FieldItemType.Float:
|
||||
return ColumnType.Float;
|
||||
case FieldItemType.Boolean:
|
||||
return ColumnType.Boolean;
|
||||
default:
|
||||
return ColumnType.Text;
|
||||
}
|
||||
}
|
||||
|
||||
function tableSheetToSettings(tableSheet: TableSheet): TableSettings {
|
||||
return {
|
||||
sheet_id: Number.parseInt(tableSheet?.sheet_id ?? '0'),
|
||||
header_line_idx: Number.parseInt(tableSheet?.header_line_idx ?? '0'),
|
||||
start_line_idx: Number.parseInt(tableSheet?.start_line_idx ?? '0'),
|
||||
};
|
||||
}
|
||||
|
||||
export interface StepPreviewProps {
|
||||
databaseId: string;
|
||||
tableFields: TableFieldData[];
|
||||
fileUri: string;
|
||||
tableSheet?: TableSheet;
|
||||
}
|
||||
|
||||
export function StepPreview({
|
||||
databaseId,
|
||||
tableFields,
|
||||
fileUri,
|
||||
tableSheet,
|
||||
}: StepPreviewProps) {
|
||||
const [tableInfo, setTableInfo] = useState<TableInfo>();
|
||||
const tableMeta: DocTableColumn[] = useMemo(
|
||||
() =>
|
||||
tableFields.map((field, index) => ({
|
||||
column_name: field.fieldName,
|
||||
column_type: convertFieldItemType(field.type),
|
||||
desc: field.fieldDescription,
|
||||
sequence: `${index}`,
|
||||
is_semantic: false,
|
||||
id: `${index}`,
|
||||
})),
|
||||
[tableFields],
|
||||
);
|
||||
|
||||
const { loading } = useRequest(
|
||||
() =>
|
||||
MemoryApi.GetTableSchema({
|
||||
database_id: databaseId,
|
||||
source_file: { tos_uri: fileUri },
|
||||
table_data_type: TableDataType.OnlyPreview,
|
||||
table_sheet: tableSheet,
|
||||
}),
|
||||
{
|
||||
onSuccess: res => {
|
||||
const sheetId = tableSheet?.sheet_id;
|
||||
if (sheetId) {
|
||||
setTableInfo({
|
||||
sheet_list: res.sheet_list,
|
||||
table_meta: { [sheetId]: tableMeta },
|
||||
preview_data: { [sheetId]: res.preview_data ?? [] },
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
return loading || !tableInfo || !tableSheet ? (
|
||||
<Spin size="large" wrapperClassName="w-full h-[288px]" />
|
||||
) : (
|
||||
<TablePreview
|
||||
data={tableInfo}
|
||||
settings={tableSheetToSettings(tableSheet)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { useState, useEffect, useMemo } from 'react';
|
||||
|
||||
import { useRequest } from 'ahooks';
|
||||
import { type UnitItem } from '@coze-data/knowledge-resource-processor-core';
|
||||
import {
|
||||
type ProcessProgressItemProps,
|
||||
ProcessStatus,
|
||||
} from '@coze-data/knowledge-resource-processor-base/types';
|
||||
import { ProcessProgressItem } from '@coze-data/knowledge-resource-processor-base';
|
||||
import { I18n, type I18nKeysNoOptionsType } from '@coze-arch/i18n';
|
||||
import { formatBytes } from '@coze-arch/bot-utils';
|
||||
import { IconUploadXLS } from '@coze-arch/bot-icons';
|
||||
import { type TableType, type TableSheet } from '@coze-arch/bot-api/memory';
|
||||
import { MemoryApi } from '@coze-arch/bot-api';
|
||||
import { Typography } from '@coze-arch/coze-design';
|
||||
|
||||
type ProcessProps = Pick<
|
||||
ProcessProgressItemProps,
|
||||
'mainText' | 'subText' | 'tipText' | 'percent' | 'status' | 'actions'
|
||||
>;
|
||||
|
||||
const INIT_PERCENT = 10;
|
||||
const COMPLETE_PERCENT = 100;
|
||||
|
||||
const statusTextMap: Record<ProcessStatus, I18nKeysNoOptionsType> = {
|
||||
[ProcessStatus.Processing]: 'datasets_createFileModel_step4_processing',
|
||||
[ProcessStatus.Complete]: 'datasets_createFileModel_step4_Finish',
|
||||
[ProcessStatus.Failed]: 'datasets_createFileModel_step4_failed',
|
||||
};
|
||||
|
||||
export interface StepProcessProps {
|
||||
databaseId: string;
|
||||
tableType: TableType;
|
||||
fileItem: UnitItem;
|
||||
tableSheet?: TableSheet;
|
||||
connectorId?: string;
|
||||
}
|
||||
|
||||
export function StepProcess({
|
||||
databaseId,
|
||||
tableType,
|
||||
fileItem,
|
||||
tableSheet,
|
||||
connectorId,
|
||||
}: StepProcessProps) {
|
||||
const fileSize = useMemo(
|
||||
() => formatBytes(fileItem.fileInstance?.size ?? 0),
|
||||
[fileItem],
|
||||
);
|
||||
const [progressProps, setProgressProps] = useState<ProcessProps>({
|
||||
// 第一行文本(文件名)
|
||||
mainText: fileItem.name,
|
||||
// 第二行文本(文件大小)
|
||||
subText: fileSize,
|
||||
// hover 时显示的第二行文本,与上面保持一致
|
||||
tipText: fileSize,
|
||||
// 进度条百分比,初始 10% 与 @coze-data/knowledge-resource-processor-base/unit-progress 保持一致
|
||||
percent: INIT_PERCENT,
|
||||
status: ProcessStatus.Processing,
|
||||
});
|
||||
|
||||
const { run, cancel } = useRequest(
|
||||
() =>
|
||||
MemoryApi.DatabaseFileProgressData({
|
||||
database_id: databaseId,
|
||||
table_type: tableType,
|
||||
}),
|
||||
{
|
||||
manual: true,
|
||||
pollingInterval: 3000,
|
||||
onSuccess: res => {
|
||||
const { data } = res;
|
||||
if (data) {
|
||||
// 有错误信息代表处理失败,展示错误信息,并停止轮询
|
||||
if (data.status_descript) {
|
||||
const msg = data.status_descript;
|
||||
setProgressProps(props => ({
|
||||
...props,
|
||||
subText: msg,
|
||||
tipText: msg,
|
||||
status: ProcessStatus.Failed,
|
||||
}));
|
||||
cancel();
|
||||
} else {
|
||||
setProgressProps(props => ({
|
||||
...props,
|
||||
percent: data.progress ?? 0,
|
||||
}));
|
||||
// 进度 100 代表处理完成,更新状态并停止轮询
|
||||
if (data.progress === COMPLETE_PERCENT) {
|
||||
setProgressProps(props => ({
|
||||
...props,
|
||||
status: ProcessStatus.Complete,
|
||||
actions: [I18n.t('datasets_unit_process_success')],
|
||||
}));
|
||||
cancel();
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
// 提交任务,并开始轮询进度
|
||||
useEffect(() => {
|
||||
MemoryApi.SubmitDatabaseInsertTask({
|
||||
database_id: databaseId,
|
||||
table_type: tableType,
|
||||
file_uri: fileItem.uri,
|
||||
table_sheet: tableSheet,
|
||||
connector_id: connectorId,
|
||||
}).finally(() => {
|
||||
run();
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="h-[32px] leading-[32px] mb-[8px]">
|
||||
<Typography.Text fontSize="14px" weight={500}>
|
||||
{I18n.t(statusTextMap[progressProps.status])}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
<ProcessProgressItem
|
||||
avatar={<IconUploadXLS />}
|
||||
{...progressProps}
|
||||
className="[&_.process-progress-item-actions]:!block"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
/*
|
||||
* 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 classNames from 'classnames';
|
||||
import {
|
||||
type UnitItem,
|
||||
UnitType,
|
||||
} from '@coze-data/knowledge-resource-processor-core';
|
||||
import { ActionRenderByDelete } from '@coze-data/knowledge-resource-processor-base/components/upload-unit-table';
|
||||
import {
|
||||
UploadUnitFile,
|
||||
UploadUnitTable,
|
||||
} from '@coze-data/knowledge-resource-processor-base';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { Typography } from '@coze-arch/coze-design';
|
||||
import { type TableType } from '@coze-arch/bot-api/memory';
|
||||
import { MemoryApi } from '@coze-arch/bot-api';
|
||||
|
||||
export interface StepUploadProps {
|
||||
databaseId: string;
|
||||
tableType: TableType;
|
||||
unitList: UnitItem[];
|
||||
onUnitListChange: (list: UnitItem[]) => void;
|
||||
}
|
||||
|
||||
export function StepUpload({
|
||||
databaseId,
|
||||
tableType,
|
||||
unitList,
|
||||
onUnitListChange,
|
||||
}: StepUploadProps) {
|
||||
useEffect(() => {
|
||||
onUnitListChange(unitList);
|
||||
}, [onUnitListChange, unitList]);
|
||||
|
||||
const downloadTemplate = async () => {
|
||||
const res = await MemoryApi.GetDatabaseTemplate({
|
||||
database_id: databaseId,
|
||||
table_type: tableType,
|
||||
});
|
||||
if (res.TosUrl) {
|
||||
window.open(res.TosUrl, '_blank');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<UploadUnitFile
|
||||
unitList={unitList}
|
||||
setUnitList={onUnitListChange}
|
||||
onFinish={onUnitListChange}
|
||||
limit={1}
|
||||
multiple={false}
|
||||
accept=".csv,.xlsx"
|
||||
maxSizeMB={20}
|
||||
showRetry={false}
|
||||
dragMainText={I18n.t('datasets_createFileModel_step2_UploadDoc')}
|
||||
dragSubText={I18n.t('datasets_unit_update_exception_tips3')}
|
||||
action=""
|
||||
className={classNames('[&_.semi-upload-drag-area]:!h-[290px]', {
|
||||
hidden: unitList.length > 0,
|
||||
})}
|
||||
showIllustration={false}
|
||||
/>
|
||||
<Typography.Paragraph
|
||||
type="secondary"
|
||||
className={classNames('mt-[8px]', { hidden: unitList.length > 0 })}
|
||||
>
|
||||
{I18n.t('db_optimize_018')}
|
||||
<Typography.Text link className="ml-[8px]" onClick={downloadTemplate}>
|
||||
{I18n.t('db_optimize_019')}
|
||||
</Typography.Text>
|
||||
</Typography.Paragraph>
|
||||
<UploadUnitTable
|
||||
edit={false}
|
||||
type={UnitType.TABLE_DOC}
|
||||
unitList={unitList}
|
||||
onChange={onUnitListChange}
|
||||
disableRetry
|
||||
getColumns={(record, index) => ({
|
||||
actions: [
|
||||
<ActionRenderByDelete
|
||||
record={record}
|
||||
index={index}
|
||||
params={{
|
||||
unitList,
|
||||
onChange: onUnitListChange,
|
||||
type: UnitType.TABLE_DOC,
|
||||
edit: false,
|
||||
}}
|
||||
/>,
|
||||
],
|
||||
})}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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,58 @@
|
||||
.tab {
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
|
||||
:global {
|
||||
.semi-tabs-bar {
|
||||
min-height: 56px;
|
||||
}
|
||||
|
||||
.semi-tabs-bar-extra {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: end;
|
||||
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.semi-tabs-content {
|
||||
overflow: auto;
|
||||
flex-grow: 1;
|
||||
padding: 0;
|
||||
|
||||
.coz-tab-bar-content.semi-tabs-pane-active {
|
||||
height: 100%;
|
||||
|
||||
.semi-tabs-pane-motion-overlay {
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.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%;
|
||||
}
|
||||
@@ -0,0 +1,411 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect, useMemo } from 'react';
|
||||
|
||||
import { pick } from 'lodash-es';
|
||||
import classNames from 'classnames';
|
||||
import { userStoreService } from '@coze-studio/user-store';
|
||||
import { type DatabaseInfo as DatabaseInitInfo } from '@coze-studio/bot-detail-store';
|
||||
import { type WidgetUIState } from '@coze-data/knowledge-stores';
|
||||
import { BotE2e } from '@coze-data/e2e';
|
||||
import { DatabaseTabs } from '@coze-data/database-v2-base/types';
|
||||
import { DismissibleBanner } from '@coze-data/database-v2-base/components/dismissible-banner';
|
||||
import {
|
||||
type FormData,
|
||||
ModalMode,
|
||||
} from '@coze-data/database-v2-base/components/base-info-modal';
|
||||
import { DatabaseModeSelect } from '@coze-data/database-v2-adapter/components/database-mode-select';
|
||||
import { DatabaseCreateTableModal } from '@coze-data/database-v2-adapter/components/create-table-modal';
|
||||
import { DatabaseBaseInfoModal } from '@coze-data/database-v2-adapter/components/base-info-modal';
|
||||
import { DatabaseDetailWaring } from '@coze-data/database-v2-adapter';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import {
|
||||
IconCozEdit,
|
||||
IconCozCross,
|
||||
IconCozArrowLeft,
|
||||
} from '@coze-arch/coze-design/icons';
|
||||
import {
|
||||
Button,
|
||||
IconButton,
|
||||
TabBar,
|
||||
Toast,
|
||||
CozAvatar,
|
||||
Typography,
|
||||
Space,
|
||||
} from '@coze-arch/coze-design';
|
||||
import {
|
||||
BotTableRWMode,
|
||||
TableType,
|
||||
type DatabaseInfo,
|
||||
type UpdateDatabaseRequest,
|
||||
} from '@coze-arch/bot-api/memory';
|
||||
import { MemoryApi } from '@coze-arch/bot-api';
|
||||
|
||||
import { DatabaseTableStructureReadonly } from '../database-table-structure-readonly';
|
||||
import { DatabaseTableData } from '../database-table-data';
|
||||
|
||||
import styles from './index.module.less';
|
||||
|
||||
export interface DatabaseDetailProps {
|
||||
version?: string;
|
||||
needHideCloseIcon?: boolean;
|
||||
databaseId: string;
|
||||
enterFrom: string;
|
||||
initialTab?: `${DatabaseTabs}`;
|
||||
addRemoveButtonText: string;
|
||||
onClose?: () => void;
|
||||
onAfterEditBasicInfo?: () => void;
|
||||
onAfterEditRecords?: () => void;
|
||||
onIDECallback?: {
|
||||
onStatusChange?: (v: WidgetUIState) => void;
|
||||
onUpdateDisplayName?: (v: string) => void;
|
||||
};
|
||||
onClickAddRemoveButton: (databaseId?: string) => void;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @coze-arch/max-line-per-function, max-lines-per-function, complexity
|
||||
export const DatabaseDetail = ({
|
||||
version,
|
||||
enterFrom,
|
||||
initialTab,
|
||||
needHideCloseIcon = false,
|
||||
addRemoveButtonText,
|
||||
onClose,
|
||||
onClickAddRemoveButton,
|
||||
onIDECallback,
|
||||
onAfterEditBasicInfo,
|
||||
onAfterEditRecords,
|
||||
databaseId,
|
||||
}: DatabaseDetailProps) => {
|
||||
const userId = userStoreService.useUserInfo()?.user_id_str;
|
||||
|
||||
const [basicInfoVisible, setBasicInfoVisible] = useState(false);
|
||||
const [createTableVisible, setCreateTableVisible] = useState(false);
|
||||
// database basicInfo
|
||||
const [databaseInfo, setDatabaseInfo] = useState<DatabaseInfo>({});
|
||||
// tab key
|
||||
const [activeKey, setActiveKey] = useState(
|
||||
version ? DatabaseTabs.Structure : (initialTab ?? DatabaseTabs.Structure),
|
||||
);
|
||||
// btn loading
|
||||
const [btnLoading, setBtnLoading] = useState(false);
|
||||
// page loading
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
// fetch database basicInfo
|
||||
const fetchDatabaseInfo = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const res = await MemoryApi.GetDatabaseByID({
|
||||
id: databaseId,
|
||||
...(version ? { version } : {}),
|
||||
});
|
||||
if (res.database_info) {
|
||||
setDatabaseInfo(res.database_info);
|
||||
if (res.database_info.table_name) {
|
||||
onIDECallback?.onUpdateDisplayName?.(res.database_info.table_name);
|
||||
onIDECallback?.onStatusChange?.('normal');
|
||||
}
|
||||
} else {
|
||||
onIDECallback?.onStatusChange?.('error');
|
||||
}
|
||||
} catch {
|
||||
onIDECallback?.onStatusChange?.('error');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 需要一个 store,后续改造
|
||||
const isReadOnlyMode = databaseInfo.creator_id !== userId || !!version;
|
||||
|
||||
const tableInitData: DatabaseInitInfo = useMemo(
|
||||
() => ({
|
||||
tableId: databaseInfo.id || '',
|
||||
name: databaseInfo.table_name || '',
|
||||
desc: databaseInfo.table_desc || '',
|
||||
icon_uri: databaseInfo.icon_uri || '',
|
||||
readAndWriteMode: databaseInfo.rw_mode || BotTableRWMode.LimitedReadWrite,
|
||||
tableMemoryList: databaseInfo.field_list || [],
|
||||
}),
|
||||
[databaseInfo],
|
||||
);
|
||||
|
||||
const basicInitData: FormData = useMemo(
|
||||
() => ({
|
||||
name: databaseInfo.table_name || '',
|
||||
description: databaseInfo.table_desc || '',
|
||||
icon_uri: [
|
||||
{
|
||||
url: databaseInfo.icon_url || '',
|
||||
uri: databaseInfo.icon_uri || '',
|
||||
uid: databaseInfo.icon_uri || '',
|
||||
isDefault: true,
|
||||
},
|
||||
],
|
||||
}),
|
||||
[databaseInfo],
|
||||
);
|
||||
|
||||
const handleEditBasicInfo = async (obj: UpdateDatabaseRequest) => {
|
||||
const res = await MemoryApi.UpdateDatabase({
|
||||
...pick(databaseInfo, [
|
||||
'id',
|
||||
'icon_uri',
|
||||
'table_name',
|
||||
'table_desc',
|
||||
'field_list',
|
||||
'rw_mode',
|
||||
'prompt_disabled',
|
||||
'extra_info',
|
||||
]),
|
||||
...obj,
|
||||
});
|
||||
if (res?.database_info?.id) {
|
||||
await fetchDatabaseInfo();
|
||||
// update basicInfo callback
|
||||
if (onAfterEditBasicInfo) {
|
||||
onAfterEditBasicInfo();
|
||||
}
|
||||
// close basicInfo modal
|
||||
if (basicInfoVisible) {
|
||||
setBasicInfoVisible(false);
|
||||
}
|
||||
} else {
|
||||
Toast.error('Update database failed');
|
||||
}
|
||||
};
|
||||
|
||||
const handleChangeDatabaseMode = async (mode: BotTableRWMode) => {
|
||||
const res = await MemoryApi.UpdateDatabase({
|
||||
...databaseInfo,
|
||||
rw_mode: mode,
|
||||
});
|
||||
if (res?.database_info?.id) {
|
||||
await fetchDatabaseInfo();
|
||||
// update basicInfo callback
|
||||
if (onAfterEditBasicInfo) {
|
||||
onAfterEditBasicInfo();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleEditTable = async () => {
|
||||
await fetchDatabaseInfo();
|
||||
// update basicInfo callback
|
||||
if (onAfterEditBasicInfo) {
|
||||
onAfterEditBasicInfo();
|
||||
}
|
||||
};
|
||||
|
||||
const handleBtnAction = () => {
|
||||
if (onClickAddRemoveButton) {
|
||||
try {
|
||||
setBtnLoading(true);
|
||||
onClickAddRemoveButton(
|
||||
enterFrom === 'workflow' ? databaseId : databaseInfo?.draft_id,
|
||||
);
|
||||
} finally {
|
||||
setBtnLoading(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchDatabaseInfo();
|
||||
}, []);
|
||||
|
||||
const fromLibrary = ['create', 'library'].includes(enterFrom);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className={classNames(
|
||||
'h-full w-full max-w-[100vw] flex flex-col overflow-hidden',
|
||||
enterFrom === 'project'
|
||||
? 'coz-bg-max rounded-b-[8px] border-solid coz-stroke-primary'
|
||||
: 'coz-bg-plus',
|
||||
)}
|
||||
>
|
||||
{/* header */}
|
||||
<div
|
||||
className={classNames(
|
||||
'flex flex-row items-center justify-between shrink-0',
|
||||
fromLibrary
|
||||
? 'h-[40px] m-[24px]'
|
||||
: 'h-[64px] px-[16px] py-[12px] border-0 border-b border-solid coz-stroke-primary',
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center gap-[8px]">
|
||||
{needHideCloseIcon ? null : (
|
||||
<IconButton
|
||||
color="secondary"
|
||||
icon={fromLibrary ? <IconCozArrowLeft /> : <IconCozCross />}
|
||||
onClick={onClose}
|
||||
/>
|
||||
)}
|
||||
<CozAvatar
|
||||
type="bot"
|
||||
color="grey"
|
||||
src={basicInitData.icon_uri?.[0]?.url}
|
||||
/>
|
||||
<div className="flex flex-col">
|
||||
<div className="flex flex-row items-center gap-[2px] leading-none">
|
||||
<Typography.Text weight={500} fontSize="14px">
|
||||
{basicInitData.name}
|
||||
</Typography.Text>
|
||||
{isReadOnlyMode ? null : (
|
||||
<IconButton
|
||||
size="mini"
|
||||
color="secondary"
|
||||
icon={<IconCozEdit className="coz-fg-secondary" />}
|
||||
onClick={() => setBasicInfoVisible(true)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<Typography.Text fontSize="12px">
|
||||
{basicInitData.description}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-[8px]">
|
||||
<DatabaseModeSelect
|
||||
disabled={isReadOnlyMode}
|
||||
value={databaseInfo.rw_mode}
|
||||
onChange={handleChangeDatabaseMode}
|
||||
/>
|
||||
{enterFrom.includes('bot') || enterFrom === 'workflow' ? (
|
||||
<Button
|
||||
disabled={isReadOnlyMode}
|
||||
loading={btnLoading}
|
||||
onClick={handleBtnAction}
|
||||
>
|
||||
{addRemoveButtonText}
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
{/* content */}
|
||||
<div
|
||||
className={classNames(
|
||||
'grow overflow-hidden',
|
||||
fromLibrary ? 'mx-[24px]' : 'mx-[16px]',
|
||||
)}
|
||||
>
|
||||
<TabBar
|
||||
className={styles.tab}
|
||||
type="text"
|
||||
align="left"
|
||||
tabBarExtraContent={
|
||||
<Space spacing={16}>
|
||||
<DatabaseDetailWaring />
|
||||
{activeKey === DatabaseTabs.Structure ? (
|
||||
<Button
|
||||
data-testid={BotE2e.BotDatabaseEditTableStructureBtn}
|
||||
onClick={() => setCreateTableVisible(true)}
|
||||
icon={<IconCozEdit />}
|
||||
color="highlight"
|
||||
disabled={isReadOnlyMode}
|
||||
>
|
||||
{I18n.t('db_new_0003')}
|
||||
</Button>
|
||||
) : null}
|
||||
</Space>
|
||||
}
|
||||
tabBarClassName="flex flex-row items-center w-full"
|
||||
activeKey={activeKey}
|
||||
onChange={(key: string) => setActiveKey(key as DatabaseTabs)}
|
||||
lazyRender
|
||||
>
|
||||
<TabBar.TabPanel
|
||||
tab={I18n.t('db_new_0001')}
|
||||
itemKey={DatabaseTabs.Structure}
|
||||
>
|
||||
<DatabaseTableStructureReadonly
|
||||
loading={loading}
|
||||
fieldList={databaseInfo.field_list ?? []}
|
||||
/>
|
||||
</TabBar.TabPanel>
|
||||
<TabBar.TabPanel
|
||||
tab={I18n.t('db_optimize_009')}
|
||||
itemKey={DatabaseTabs.Draft}
|
||||
disabled={!!version}
|
||||
>
|
||||
<DismissibleBanner
|
||||
type="info"
|
||||
persistentKey="_coze_database_draft_data_warning"
|
||||
>
|
||||
{I18n.t('db_optimize_010')}
|
||||
</DismissibleBanner>
|
||||
<DatabaseTableData
|
||||
databaseId={databaseId}
|
||||
tableType={TableType.DraftTable}
|
||||
tableFields={databaseInfo.field_list || []}
|
||||
// 测试数据无需控制权限,只要能看到的数据就能修改删除
|
||||
isReadonlyMode={false}
|
||||
enterFrom={enterFrom}
|
||||
onAfterEditRecords={onAfterEditRecords}
|
||||
/>
|
||||
</TabBar.TabPanel>
|
||||
<TabBar.TabPanel
|
||||
tab={I18n.t('db_new_0002')}
|
||||
itemKey={DatabaseTabs.Online}
|
||||
disabled={!!version}
|
||||
>
|
||||
<DismissibleBanner
|
||||
type="info"
|
||||
persistentKey="_coze_database_online_data_warning"
|
||||
>
|
||||
{I18n.t('database_optimize_200')}
|
||||
</DismissibleBanner>
|
||||
<DatabaseTableData
|
||||
databaseId={databaseId}
|
||||
tableType={TableType.OnlineTable}
|
||||
tableFields={databaseInfo.field_list || []}
|
||||
isReadonlyMode={isReadOnlyMode}
|
||||
enterFrom={enterFrom}
|
||||
onAfterEditRecords={onAfterEditRecords}
|
||||
/>
|
||||
</TabBar.TabPanel>
|
||||
</TabBar>
|
||||
</div>
|
||||
</div>
|
||||
<DatabaseBaseInfoModal
|
||||
visible={basicInfoVisible}
|
||||
onSubmit={formData =>
|
||||
handleEditBasicInfo({
|
||||
table_name: formData.name,
|
||||
icon_uri: formData.icon_uri?.[0]?.uri,
|
||||
table_desc: formData.description,
|
||||
})
|
||||
}
|
||||
initValues={basicInitData}
|
||||
mode={ModalMode.EDIT}
|
||||
onClose={() => setBasicInfoVisible(false)}
|
||||
/>
|
||||
<DatabaseCreateTableModal
|
||||
visible={createTableVisible}
|
||||
initValue={tableInitData}
|
||||
onSubmit={handleEditTable}
|
||||
showDatabaseBaseInfo={false}
|
||||
onlyShowDatabaseInfoRWMode={true}
|
||||
onReturn={() => setCreateTableVisible(false)}
|
||||
onClose={() => setCreateTableVisible(false)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,247 @@
|
||||
/* stylelint-disable block-no-empty */
|
||||
/* stylelint-disable declaration-no-important */
|
||||
/* stylelint-disable no-descending-specificity */
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.input {}
|
||||
|
||||
.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,655 @@
|
||||
/*
|
||||
* 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 max-lines */
|
||||
/* eslint-disable @coze-arch/max-line-per-function */
|
||||
/* eslint-disable max-lines-per-function */
|
||||
|
||||
import { useRef, useMemo, useEffect, useState } from 'react';
|
||||
|
||||
import { nanoid } from 'nanoid';
|
||||
import { useLocalStorageState } 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 {
|
||||
CreateType,
|
||||
type NL2DBInfo,
|
||||
type OnSave,
|
||||
} from '@coze-data/database-v2-base/types';
|
||||
import { TEMPLATE_INFO } from '@coze-data/database-v2-base/constants';
|
||||
import {
|
||||
DatabaseTableStructure,
|
||||
type DatabaseTableStructureRef,
|
||||
} from '@coze-data/database-v2-base/components/database-table-structure';
|
||||
import { FormDatabaseModeSelect } from '@coze-data/database-v2-adapter/components/database-mode-select';
|
||||
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,
|
||||
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 {
|
||||
IconCozWarning as IconAlertTriangle,
|
||||
IconCozCross as IconClose,
|
||||
} from '@coze-arch/coze-design/icons';
|
||||
|
||||
import { BotDebugButton } from '../bot-debug-button';
|
||||
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 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;
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention -- 历史逻辑
|
||||
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 [
|
||||
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 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>
|
||||
</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 null;
|
||||
}
|
||||
|
||||
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}
|
||||
renderModeSelect={params => <FormDatabaseModeSelect {...params} />}
|
||||
/>
|
||||
</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,57 @@
|
||||
/*
|
||||
* 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 { I18n } from '@coze-arch/i18n';
|
||||
import { IconCozCross, IconCozTrashCan } from '@coze-arch/coze-design/icons';
|
||||
import { Button, Divider, IconButton, Typography } from '@coze-arch/coze-design';
|
||||
|
||||
export interface BatchDeleteToolbarProps {
|
||||
selectedCount?: number;
|
||||
onDelete: () => void;
|
||||
onCancel: () => void;
|
||||
}
|
||||
|
||||
export function BatchDeleteToolbar({
|
||||
selectedCount = 0,
|
||||
onDelete,
|
||||
onCancel,
|
||||
}: BatchDeleteToolbarProps) {
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
'flex items-center p-[8px] gap-[8px] rounded-[12px]',
|
||||
'coz-bg-max border-solid coz-stroke-primary coz-shadow-default',
|
||||
'fixed bottom-[8px] left-[50%] translate-x-[-50%] z-10',
|
||||
{ hidden: selectedCount <= 0 },
|
||||
)}
|
||||
>
|
||||
<Typography.Text type="secondary">
|
||||
{I18n.t('db_optimize_031', { n: selectedCount })}
|
||||
</Typography.Text>
|
||||
<Divider layout="vertical" />
|
||||
<Button color="red" icon={<IconCozTrashCan />} onClick={onDelete}>
|
||||
{I18n.t('db_optimize_030')}
|
||||
</Button>
|
||||
<Divider layout="vertical" />
|
||||
<IconButton
|
||||
color="secondary"
|
||||
icon={<IconCozCross />}
|
||||
onClick={onCancel}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,260 @@
|
||||
/*
|
||||
* 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 TableMemoryItem } from '@coze-studio/bot-detail-store';
|
||||
import {
|
||||
SYSTEM_FIELDS,
|
||||
SYSTEM_FIELD_ROW_INDEX,
|
||||
} from '@coze-data/database-v2-base/constants';
|
||||
import { DatabaseFieldTitle } from '@coze-data/database-v2-base/components/database-field-title';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { FieldItemType } from '@coze-arch/bot-api/memory';
|
||||
import { IconCozEdit, IconCozTrashCan } from '@coze-arch/coze-design/icons';
|
||||
import {
|
||||
type ColumnProps,
|
||||
IconButton,
|
||||
Popconfirm,
|
||||
Space,
|
||||
Typography,
|
||||
} from '@coze-arch/coze-design';
|
||||
|
||||
import { type TableRow, type TableField, type TableFieldData } from './type';
|
||||
|
||||
export function formatTableStructList(
|
||||
structList: TableMemoryItem[],
|
||||
): TableFieldData[] {
|
||||
return structList.map(item => ({
|
||||
fieldName: item.name ?? '',
|
||||
fieldDescription: item.desc ?? '',
|
||||
required: item.must_required ?? false,
|
||||
type: item.type ?? FieldItemType.Text,
|
||||
}));
|
||||
}
|
||||
|
||||
export function formatTableDataRow(
|
||||
structList: TableFieldData[],
|
||||
dataRow: Record<string, string>[],
|
||||
): TableRow[] {
|
||||
return dataRow.map(_data => {
|
||||
const dataRowFieldList = Object.keys(_data);
|
||||
const formattedDataRow: TableRow = {};
|
||||
dataRowFieldList.forEach(_key => {
|
||||
const structItem = structList.find(i => i.fieldName === _key);
|
||||
if (!structItem) {
|
||||
// 系统字段
|
||||
formattedDataRow[_key] = {
|
||||
fieldName: _key,
|
||||
type: FieldItemType.Text,
|
||||
required: true,
|
||||
value: _data[_key as unknown as number],
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
switch (structItem.type) {
|
||||
case FieldItemType.Boolean:
|
||||
formattedDataRow[_key] = {
|
||||
fieldName: _key,
|
||||
value: _data[_key as unknown as number] as unknown as boolean,
|
||||
type: FieldItemType.Boolean,
|
||||
required: structItem.required,
|
||||
};
|
||||
break;
|
||||
case FieldItemType.Number:
|
||||
formattedDataRow[_key] = {
|
||||
fieldName: _key,
|
||||
value: _data[_key as unknown as number] as unknown as number,
|
||||
type: FieldItemType.Number,
|
||||
required: structItem.required,
|
||||
};
|
||||
break;
|
||||
case FieldItemType.Date:
|
||||
formattedDataRow[_key] = {
|
||||
fieldName: _key,
|
||||
value: _data[_key as unknown as number] as unknown as string,
|
||||
type: FieldItemType.Date,
|
||||
required: structItem.required,
|
||||
};
|
||||
break;
|
||||
case FieldItemType.Float:
|
||||
formattedDataRow[_key] = {
|
||||
fieldName: _key,
|
||||
value: _data[_key as unknown as number] as unknown as string,
|
||||
type: FieldItemType.Float,
|
||||
required: structItem.required,
|
||||
};
|
||||
break;
|
||||
case FieldItemType.Text:
|
||||
formattedDataRow[_key] = {
|
||||
fieldName: _key,
|
||||
value: _data[_key as unknown as number] as unknown as string,
|
||||
type: FieldItemType.Text,
|
||||
required: structItem.required,
|
||||
};
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
return formattedDataRow;
|
||||
});
|
||||
}
|
||||
|
||||
const SystemFieldWidth: Record<string, number | undefined> = {
|
||||
id: 200,
|
||||
sys_platform: 180,
|
||||
uuid: 260,
|
||||
bstudio_create_time: 200,
|
||||
};
|
||||
|
||||
interface GetTableColumnsParams {
|
||||
fieldList: TableFieldData[];
|
||||
connectorNames: Record<string, string>;
|
||||
isReadonlyMode: boolean;
|
||||
handleEditRow: (row: TableRow) => void;
|
||||
handleDeleteRow: (row: TableRow) => void;
|
||||
}
|
||||
|
||||
interface DatabaseTableCellProps {
|
||||
value?: string | number | boolean;
|
||||
}
|
||||
|
||||
function DatabaseTableCell({ value }: DatabaseTableCellProps) {
|
||||
const stringValue = value?.toString() ?? '';
|
||||
|
||||
return (
|
||||
<Typography.Text
|
||||
ellipsis={{
|
||||
showTooltip: {
|
||||
opts: {
|
||||
className: classNames(
|
||||
'[&_.semi-tooltip-content]:max-h-[110px]',
|
||||
'[&_.semi-tooltip-content]:line-clamp-5',
|
||||
),
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
{stringValue}
|
||||
</Typography.Text>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 Table Field 表头数据
|
||||
*/
|
||||
export const getTableColumns = ({
|
||||
fieldList,
|
||||
connectorNames,
|
||||
isReadonlyMode,
|
||||
handleEditRow,
|
||||
handleDeleteRow,
|
||||
}: GetTableColumnsParams) => {
|
||||
const columns: ColumnProps<TableRow>[] = [];
|
||||
|
||||
// 系统字段列
|
||||
columns.push(
|
||||
...SYSTEM_FIELDS.map(item => ({
|
||||
title: () => (
|
||||
<DatabaseFieldTitle
|
||||
field={item.name}
|
||||
type={item.type}
|
||||
tip={item.desc}
|
||||
required
|
||||
/>
|
||||
),
|
||||
dataIndex: SYSTEM_FIELD_ROW_INDEX[item.name ?? ''],
|
||||
width: SystemFieldWidth[item.name ?? ''] ?? 260,
|
||||
render: (field: TableField) =>
|
||||
field.fieldName === 'bstudio_connector_id' ? (
|
||||
<Typography.Text ellipsis>
|
||||
{connectorNames[field.value as string] ?? field.value}
|
||||
</Typography.Text>
|
||||
) : (
|
||||
<DatabaseTableCell value={field.value} />
|
||||
),
|
||||
})),
|
||||
);
|
||||
|
||||
// 用户字段列
|
||||
columns.push(
|
||||
...fieldList.map(item => ({
|
||||
title: () => (
|
||||
<DatabaseFieldTitle
|
||||
field={item.fieldName}
|
||||
type={item.type}
|
||||
tip={item.fieldDescription}
|
||||
required={item.required}
|
||||
/>
|
||||
),
|
||||
dataIndex: item.fieldName,
|
||||
width: 260,
|
||||
render: (field: TableField) => <DatabaseTableCell value={field?.value} />,
|
||||
})),
|
||||
);
|
||||
|
||||
// 操作列
|
||||
columns.push({
|
||||
title: I18n.t('db_table_0126_021'),
|
||||
width: 100,
|
||||
resize: false,
|
||||
fixed: 'right',
|
||||
render: (_: TableField, row: TableRow, _index: number) =>
|
||||
isReadonlyMode ? (
|
||||
<Space>
|
||||
<IconButton
|
||||
disabled
|
||||
icon={<IconCozEdit />}
|
||||
size="small"
|
||||
color="secondary"
|
||||
/>
|
||||
<IconButton
|
||||
disabled
|
||||
icon={<IconCozTrashCan />}
|
||||
size="small"
|
||||
color="secondary"
|
||||
/>
|
||||
</Space>
|
||||
) : (
|
||||
<Space>
|
||||
<IconButton
|
||||
icon={<IconCozEdit />}
|
||||
size="default"
|
||||
color="secondary"
|
||||
onClick={() => handleEditRow(row)}
|
||||
/>
|
||||
<Popconfirm
|
||||
title={I18n.t('db_optimize_026')}
|
||||
content={I18n.t('db_optimize_027')}
|
||||
okText={I18n.t('db_optimize_028')}
|
||||
okButtonColor="red"
|
||||
cancelText={I18n.t('db_optimize_029')}
|
||||
onConfirm={() => handleDeleteRow(row)}
|
||||
>
|
||||
<IconButton
|
||||
icon={<IconCozTrashCan />}
|
||||
size="default"
|
||||
color="secondary"
|
||||
/>
|
||||
</Popconfirm>
|
||||
</Space>
|
||||
),
|
||||
});
|
||||
|
||||
return columns;
|
||||
};
|
||||
@@ -0,0 +1,149 @@
|
||||
/* stylelint-disable max-nesting-depth */
|
||||
/* stylelint-disable no-descending-specificity */
|
||||
|
||||
.table {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
flex-grow: 1;
|
||||
|
||||
.table-wrapper {
|
||||
:global {
|
||||
.semi-table-wrapper {
|
||||
line-height: unset;
|
||||
}
|
||||
|
||||
// 横向滚动到最左/最右边时,隐藏左/右固定列的阴影
|
||||
.semi-table-scroll-position-left .semi-table-tbody>.semi-table-row>.semi-table-cell-fixed-left-last,
|
||||
.semi-table-scroll-position-left .semi-table-thead>.semi-table-row>.semi-table-cell-fixed-left-last,
|
||||
.semi-table-scroll-position-right .semi-table-tbody>.semi-table-row>.semi-table-cell-fixed-right-first,
|
||||
.semi-table-scroll-position-right .semi-table-thead>.semi-table-row>.semi-table-cell-fixed-right-first {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
// 左边最后一个固定列的阴影
|
||||
.semi-table-row>.semi-table-cell-fixed-left-last {
|
||||
// 因为 overflow: hidden 对 display: table-row 元素无效,
|
||||
// 所以使阴影 y 轴方向偏移 2px ,避免本行的阴影遮挡上一行元素,同时下一行元素的背景色可以遮挡本行的阴影
|
||||
box-shadow: 2px 2px 3px rgb(0 0 0 / 8%);
|
||||
}
|
||||
|
||||
// 右边第一个固定列的阴影
|
||||
.semi-table-row>.semi-table-cell-fixed-right-first {
|
||||
box-shadow: -2px 2px 3px rgb(0 0 0 / 8%);
|
||||
}
|
||||
|
||||
// 重置表格行 hover 时的鼠标指针
|
||||
.semi-table-tbody>.semi-table-row {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
// 右边固定列不需要 text-align: right
|
||||
.semi-table-thead > .semi-table-row > .semi-table-row-head:last-child,
|
||||
.semi-table-tbody > .semi-table-row > .semi-table-row-cell:last-child {
|
||||
text-align: unset;
|
||||
}
|
||||
|
||||
// 去掉表头高度限制
|
||||
.coz-table-list .semi-table-fixed-header table {
|
||||
height: unset;
|
||||
}
|
||||
|
||||
// 表头高度对齐设计稿
|
||||
.semi-table-thead>.semi-table-row>.semi-table-row-head {
|
||||
height: 28px;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
/** table header样式 **/
|
||||
.semi-table-thead {
|
||||
// 拖拽列宽度的图标样式
|
||||
.semi-table-row {
|
||||
.react-resizable-handle {
|
||||
background: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.react-resizable:not(.semi-table-cell-fixed-left, .resizing, .not-resize-handle) {
|
||||
.react-resizable-handle {
|
||||
width: 7px;
|
||||
border-right: 2px solid var(--coz-stroke-plus);
|
||||
border-left: 1px solid var(--coz-stroke-plus);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 拖拽列宽时的高亮右边框
|
||||
&>.semi-table-row>.semi-table-row-head.resizing {
|
||||
border-right-color: var(--coz-stroke-hglt);
|
||||
border-right-width: 1px;
|
||||
}
|
||||
|
||||
// 去掉左边固定列的右边框
|
||||
&>.semi-table-row>.semi-table-row-head.semi-table-cell-fixed-left-last {
|
||||
border-right: 0;
|
||||
}
|
||||
|
||||
// 去掉右边固定列的左边框
|
||||
&>.semi-table-row>.semi-table-row-head.semi-table-cell-fixed-right-first {
|
||||
border-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/** table body部分样式 **/
|
||||
.semi-table-tbody {
|
||||
.semi-table-row {
|
||||
>.semi-table-row-cell {
|
||||
// 修复行高,对齐设计稿
|
||||
height: 56px;
|
||||
|
||||
// 拖拽列宽时的高亮右边框
|
||||
&.resizing {
|
||||
border-right-color: var(--coz-stroke-hglt);
|
||||
}
|
||||
}
|
||||
|
||||
// 去掉左边固定列的右边框
|
||||
>.semi-table-cell-fixed-left-last {
|
||||
border-right: 0;
|
||||
}
|
||||
|
||||
// 去掉右边固定列的左边框
|
||||
>.semi-table-cell-fixed-right-first {
|
||||
border-left: 0;
|
||||
}
|
||||
|
||||
// 去掉固定列未在 hover 状态时的奇怪圆角
|
||||
&:not(:hover) {
|
||||
>.semi-table-row-cell.semi-table-cell-fixed-left {
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
}
|
||||
|
||||
>.semi-table-row-cell.semi-table-cell-fixed-right {
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.semi-table-pagination-outer {
|
||||
height: 48px;
|
||||
min-height: unset;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.table-wrapper-project {
|
||||
:global {
|
||||
// 适配 Project IDE 中白色背景 table 样式
|
||||
.coz-table-list .semi-table-thead>.semi-table-row>.semi-table-row-head.semi-table-cell-fixed-left::before,
|
||||
.coz-table-list .semi-table-thead>.semi-table-row>.semi-table-row-head.semi-table-cell-fixed-right::before,
|
||||
.coz-table-list .semi-table-thead>.semi-table-row>.semi-table-row-head,
|
||||
.coz-table-list .semi-table-tbody>.semi-table-row:not(:hover)>.semi-table-row-cell {
|
||||
background-color: var(--coz-bg-max);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,288 @@
|
||||
/*
|
||||
* 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, useMemo, useRef, useEffect } from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { useRequest } from 'ahooks';
|
||||
import { IllustrationNoContent } from '@douyinfe/semi-illustrations';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { type TableType, type FieldItem } from '@coze-arch/bot-api/memory';
|
||||
import { MemoryApi } from '@coze-arch/bot-api';
|
||||
import {
|
||||
Modal,
|
||||
Table,
|
||||
Divider,
|
||||
Typography,
|
||||
CozPagination,
|
||||
Empty,
|
||||
} from '@coze-arch/coze-design';
|
||||
|
||||
import { RowEditModal } from '../row-edit-modal';
|
||||
import { resizeFn } from '../../utils/table';
|
||||
import { useConnectorOptions } from '../../hooks/use-connector-options';
|
||||
import { type TableRow } from './type';
|
||||
import { ToolButtonsBar } from './tool-buttons-bar';
|
||||
import {
|
||||
formatTableDataRow,
|
||||
formatTableStructList,
|
||||
getTableColumns,
|
||||
} from './formatter';
|
||||
import { BatchDeleteToolbar } from './batch-delete-toolbar';
|
||||
|
||||
import styles from './index.module.less';
|
||||
|
||||
interface DatabaseTableDataProps {
|
||||
databaseId: string;
|
||||
tableType: TableType;
|
||||
tableFields: FieldItem[];
|
||||
isReadonlyMode: boolean;
|
||||
enterFrom?: string;
|
||||
onAfterEditRecords?: () => void;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @coze-arch/max-line-per-function
|
||||
export function DatabaseTableData({
|
||||
databaseId,
|
||||
tableType,
|
||||
tableFields,
|
||||
isReadonlyMode,
|
||||
enterFrom,
|
||||
onAfterEditRecords,
|
||||
}: DatabaseTableDataProps) {
|
||||
const fields = useMemo(
|
||||
() => formatTableStructList(tableFields),
|
||||
[tableFields],
|
||||
);
|
||||
|
||||
const [pageSize, setPageSize] = useState(20);
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [totalRecords, setTotalRecords] = useState(0);
|
||||
const [dataRows, setDataRows] = useState<Record<string, string>[]>([]);
|
||||
|
||||
const { loading, refresh } = useRequest(
|
||||
() =>
|
||||
MemoryApi.ListDatabaseRecords({
|
||||
database_id: databaseId,
|
||||
table_type: tableType,
|
||||
offset: (currentPage - 1) * pageSize,
|
||||
limit: pageSize,
|
||||
}),
|
||||
{
|
||||
onSuccess: res => {
|
||||
setTotalRecords(res.TotalNum);
|
||||
setDataRows(res.data);
|
||||
},
|
||||
refreshDeps: [databaseId, tableType, pageSize, currentPage],
|
||||
},
|
||||
);
|
||||
|
||||
const tableDataSource = useMemo(
|
||||
() => formatTableDataRow(fields, dataRows),
|
||||
[fields, dataRows],
|
||||
);
|
||||
|
||||
const afterEdit = () => {
|
||||
refresh();
|
||||
onAfterEditRecords?.();
|
||||
};
|
||||
|
||||
const connectorOptions = useConnectorOptions({ includeMigrated: true });
|
||||
const connectorNames = useMemo(
|
||||
() =>
|
||||
Object.fromEntries(
|
||||
connectorOptions.map(item => [item.value, item.label]),
|
||||
),
|
||||
[connectorOptions],
|
||||
);
|
||||
|
||||
const [selectedRows, setSelectedRows] = useState<TableRow[]>([]);
|
||||
|
||||
const handleBatchDelete = () =>
|
||||
Modal.confirm({
|
||||
title: I18n.t('db_optimize_026'),
|
||||
content: I18n.t('db_optimize_027'),
|
||||
okText: I18n.t('dialog_240305_03'),
|
||||
okButtonColor: 'red',
|
||||
cancelText: I18n.t('dialog_240305_04'),
|
||||
onOk: async () => {
|
||||
await MemoryApi.UpdateDatabaseRecords({
|
||||
database_id: databaseId,
|
||||
table_type: tableType,
|
||||
record_data_delete: selectedRows.map(row => ({
|
||||
bstudio_id: row.bstudio_id.value as string,
|
||||
})),
|
||||
});
|
||||
setSelectedRows([]);
|
||||
afterEdit();
|
||||
},
|
||||
});
|
||||
|
||||
const [rowEditModelVisible, setRowEditModelVisible] = useState(false);
|
||||
const [editingRow, setEditingRow] = useState<TableRow>();
|
||||
const handleEditRow = (row?: TableRow) => {
|
||||
setEditingRow(row);
|
||||
setRowEditModelVisible(true);
|
||||
};
|
||||
|
||||
const handleRowEditSubmit = async (
|
||||
values: Record<string, string>,
|
||||
originalConnectorId?: string,
|
||||
) => {
|
||||
if (!originalConnectorId) {
|
||||
await MemoryApi.UpdateDatabaseRecords({
|
||||
database_id: databaseId,
|
||||
table_type: tableType,
|
||||
record_data_add: [values],
|
||||
});
|
||||
} else {
|
||||
await MemoryApi.UpdateDatabaseRecords({
|
||||
database_id: databaseId,
|
||||
table_type: tableType,
|
||||
record_data_alter: [values],
|
||||
// 编辑行时,要带上原始的 connector_id,后端需要判断数据是否来自/目标为“豆包”渠道
|
||||
ori_connector_id: originalConnectorId,
|
||||
});
|
||||
}
|
||||
setRowEditModelVisible(false);
|
||||
setEditingRow(undefined);
|
||||
afterEdit();
|
||||
};
|
||||
|
||||
const handleDeleteRow = async (row: TableRow) => {
|
||||
await MemoryApi.UpdateDatabaseRecords({
|
||||
database_id: databaseId,
|
||||
table_type: tableType,
|
||||
record_data_delete: [
|
||||
{
|
||||
bstudio_id: row.bstudio_id.value as string,
|
||||
},
|
||||
],
|
||||
});
|
||||
afterEdit();
|
||||
};
|
||||
|
||||
const tableFieldColumns = useMemo(
|
||||
() =>
|
||||
getTableColumns({
|
||||
fieldList: fields,
|
||||
isReadonlyMode,
|
||||
connectorNames,
|
||||
handleDeleteRow,
|
||||
handleEditRow,
|
||||
}),
|
||||
[fields, isReadonlyMode, connectorNames],
|
||||
);
|
||||
|
||||
const [tableHeight, setTableHeight] = useState(0);
|
||||
const tableWrapperRef = useRef<HTMLDivElement>(null);
|
||||
useEffect(() => {
|
||||
const observer = new ResizeObserver(entires => {
|
||||
for (const e of entires) {
|
||||
if (e.target === tableWrapperRef.current) {
|
||||
setTableHeight(e.contentRect.height);
|
||||
}
|
||||
}
|
||||
});
|
||||
if (tableWrapperRef.current) {
|
||||
observer.observe(tableWrapperRef.current);
|
||||
}
|
||||
return () => observer.disconnect();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className={styles.table} ref={tableWrapperRef}>
|
||||
<ToolButtonsBar
|
||||
readonly={isReadonlyMode}
|
||||
databaseId={databaseId}
|
||||
tableType={tableType}
|
||||
tableFields={fields}
|
||||
onNewRow={() => handleEditRow()}
|
||||
onRefresh={refresh}
|
||||
/>
|
||||
<Table
|
||||
tableProps={{
|
||||
loading,
|
||||
columns: tableFieldColumns,
|
||||
dataSource: tableDataSource,
|
||||
rowSelection: {
|
||||
fixed: true,
|
||||
selectedRowKeys: selectedRows.map(
|
||||
r => r.bstudio_id?.value as string,
|
||||
),
|
||||
onChange: (_, rows) => setSelectedRows(rows ?? []),
|
||||
},
|
||||
resizable: {
|
||||
onResize: resizeFn,
|
||||
},
|
||||
rowKey: (record: TableRow) => record?.bstudio_id?.value as string,
|
||||
scroll: {
|
||||
// 128 = ToolButtonsBar(52) + 表头(28) + Pagination(48)
|
||||
y: tableHeight > 128 ? tableHeight - 128 : 'auto',
|
||||
},
|
||||
pagination: {
|
||||
total: totalRecords,
|
||||
currentPage,
|
||||
pageSize,
|
||||
onChange: (current, size) => {
|
||||
setCurrentPage(current);
|
||||
setPageSize(size);
|
||||
setSelectedRows([]);
|
||||
},
|
||||
},
|
||||
renderPagination: paginationProps => (
|
||||
<div className="w-full flex gap-[8px] items-center justify-end">
|
||||
<Typography.Text type="secondary" fontSize="12px">
|
||||
{I18n.t('db_optimize_032', { n: totalRecords })}
|
||||
</Typography.Text>
|
||||
<Divider layout="vertical" className="h-[16px]" />
|
||||
<CozPagination
|
||||
size="small"
|
||||
showSizeChanger
|
||||
pageSizeOpts={[20, 50, 100]}
|
||||
{...paginationProps}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
}}
|
||||
wrapperClassName={classNames(styles['table-wrapper'], {
|
||||
// database 数据表格在 Project IDE 中要使用 coz-bg-max 白色背景
|
||||
[styles['table-wrapper-project']]: enterFrom === 'project',
|
||||
})}
|
||||
empty={
|
||||
<Empty
|
||||
image={<IllustrationNoContent className="w-[140px] h-[140px]" />}
|
||||
title={I18n.t('timecapsule_0108_003')}
|
||||
/>
|
||||
}
|
||||
indexRowSelection
|
||||
/>
|
||||
<BatchDeleteToolbar
|
||||
selectedCount={selectedRows.length}
|
||||
onDelete={handleBatchDelete}
|
||||
onCancel={() => setSelectedRows([])}
|
||||
/>
|
||||
<RowEditModal
|
||||
fields={fields}
|
||||
visible={rowEditModelVisible}
|
||||
tableType={tableType}
|
||||
initialValues={editingRow}
|
||||
onSubmit={handleRowEditSubmit}
|
||||
onCancel={() => setRowEditModelVisible(false)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { type CSSProperties, type HTMLAttributes } from 'react';
|
||||
|
||||
import { CSS } from '@dnd-kit/utilities';
|
||||
import { useSortable } from '@dnd-kit/sortable';
|
||||
|
||||
/**
|
||||
* 拆分自 packages/data/database-v2/src/components/database-table-data/index.tsx
|
||||
* 原本实现基本是从 Semi 文档复制过来的,排序后的数据也没有提交给服务端,PM 似乎也不知道有这个功能,所以 ...
|
||||
* @see
|
||||
*/
|
||||
export const SortableRow = (
|
||||
// https://github.com/DouyinFE/semi-design/blob/v2.69.2/packages/semi-ui/table/Body/BaseRow.tsx#L396
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention -- semi 没有导出 table row props 的类型
|
||||
sortProps: HTMLAttributes<HTMLTableRowElement> & { 'data-row-key': string },
|
||||
) => {
|
||||
const {
|
||||
attributes,
|
||||
listeners,
|
||||
setNodeRef,
|
||||
transform,
|
||||
transition,
|
||||
isDragging,
|
||||
} = useSortable({
|
||||
id: sortProps['data-row-key'],
|
||||
});
|
||||
const style: CSSProperties = {
|
||||
...sortProps.style,
|
||||
transform: CSS.Transform.toString(transform),
|
||||
transition,
|
||||
cursor: isDragging ? 'grabbing' : 'grab',
|
||||
zIndex: isDragging ? 1 : undefined,
|
||||
position: isDragging ? 'relative' : undefined,
|
||||
};
|
||||
|
||||
return (
|
||||
<tr
|
||||
{...sortProps}
|
||||
ref={setNodeRef}
|
||||
style={style}
|
||||
{...attributes}
|
||||
{...listeners}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,218 @@
|
||||
/*
|
||||
* 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 TableMemoryItem } from '@coze-studio/bot-detail-store';
|
||||
import { FieldItemType } from '@coze-arch/bot-api/memory';
|
||||
|
||||
import { type TestDataRow } from './type';
|
||||
|
||||
export const testStructList: TableMemoryItem[] = [
|
||||
{
|
||||
nanoid: 'id1',
|
||||
name: 'city',
|
||||
desc: 'city',
|
||||
must_required: true,
|
||||
type: FieldItemType.Text,
|
||||
},
|
||||
{
|
||||
nanoid: 'id2',
|
||||
name: 'level',
|
||||
desc: 'level',
|
||||
must_required: true,
|
||||
type: FieldItemType.Text,
|
||||
},
|
||||
{
|
||||
nanoid: 'id3',
|
||||
name: 't_gdp',
|
||||
desc: 't_gdp',
|
||||
must_required: true,
|
||||
type: FieldItemType.Number,
|
||||
},
|
||||
{
|
||||
nanoid: 'id4',
|
||||
name: 'p_gdp',
|
||||
desc: 'p_gdp',
|
||||
must_required: true,
|
||||
type: FieldItemType.Float,
|
||||
},
|
||||
{
|
||||
nanoid: 'id7',
|
||||
name: 'international_trade_gdp',
|
||||
desc: 'p_gdp',
|
||||
must_required: true,
|
||||
type: FieldItemType.Float,
|
||||
},
|
||||
{
|
||||
nanoid: 'id8',
|
||||
name: 'international_trade_p_gdp',
|
||||
desc: 'p_gdp',
|
||||
must_required: true,
|
||||
type: FieldItemType.Float,
|
||||
},
|
||||
{
|
||||
nanoid: 'id5',
|
||||
name: 'is_allowed',
|
||||
desc: 'is_allowed',
|
||||
must_required: true,
|
||||
type: FieldItemType.Boolean,
|
||||
},
|
||||
{
|
||||
nanoid: 'id6',
|
||||
name: 'update_time',
|
||||
desc: 'update_time',
|
||||
must_required: true,
|
||||
type: FieldItemType.Date,
|
||||
},
|
||||
];
|
||||
|
||||
export const testData: TestDataRow[] = [
|
||||
[
|
||||
{
|
||||
field_name: 'city',
|
||||
value: '北京',
|
||||
},
|
||||
{
|
||||
field_name: 'level',
|
||||
value: '一线',
|
||||
},
|
||||
{
|
||||
field_name: 't_gdp',
|
||||
value: 10000,
|
||||
},
|
||||
{
|
||||
field_name: 'p_gdp',
|
||||
value: 10000.1,
|
||||
},
|
||||
{
|
||||
field_name: 'is_allowed',
|
||||
value: true,
|
||||
},
|
||||
{
|
||||
field_name: 'update_time',
|
||||
value: '2023-08-23 12:00:00',
|
||||
},
|
||||
{
|
||||
field_name: 'international_trade_gdp',
|
||||
value: 10000,
|
||||
},
|
||||
{
|
||||
field_name: 'international_trade_p_gdp',
|
||||
value: 10000.1,
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
field_name: 'city',
|
||||
value: '上海',
|
||||
},
|
||||
{
|
||||
field_name: 'level',
|
||||
value: '一线',
|
||||
},
|
||||
{
|
||||
field_name: 't_gdp',
|
||||
value: 20000,
|
||||
},
|
||||
{
|
||||
field_name: 'p_gdp',
|
||||
value: 20000.1,
|
||||
},
|
||||
{
|
||||
field_name: 'is_allowed',
|
||||
value: false,
|
||||
},
|
||||
{
|
||||
field_name: 'update_time',
|
||||
value: '2023-08-23 12:30:00',
|
||||
},
|
||||
{
|
||||
field_name: 'international_trade_gdp',
|
||||
value: 10000,
|
||||
},
|
||||
{
|
||||
field_name: 'international_trade_p_gdp',
|
||||
value: 10000.1,
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
field_name: 'city',
|
||||
value: '深圳',
|
||||
},
|
||||
{
|
||||
field_name: 'level',
|
||||
value: '一线',
|
||||
},
|
||||
{
|
||||
field_name: 't_gdp',
|
||||
value: 30000,
|
||||
},
|
||||
{
|
||||
field_name: 'p_gdp',
|
||||
value: 30000.1,
|
||||
},
|
||||
{
|
||||
field_name: 'is_allowed',
|
||||
value: true,
|
||||
},
|
||||
{
|
||||
field_name: 'update_time',
|
||||
value: '2023-08-23 12:20:00',
|
||||
},
|
||||
{
|
||||
field_name: 'international_trade_gdp',
|
||||
value: 10000,
|
||||
},
|
||||
{
|
||||
field_name: 'international_trade_p_gdp',
|
||||
value: 10000.1,
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
field_name: 'city',
|
||||
value: '广州',
|
||||
},
|
||||
{
|
||||
field_name: 'level',
|
||||
value: '一线',
|
||||
},
|
||||
{
|
||||
field_name: 't_gdp',
|
||||
value: 40000,
|
||||
},
|
||||
{
|
||||
field_name: 'p_gdp',
|
||||
value: 40000.1,
|
||||
},
|
||||
{
|
||||
field_name: 'is_allowed',
|
||||
value: false,
|
||||
},
|
||||
{
|
||||
field_name: 'update_time',
|
||||
value: '2023-08-23 14:00:00',
|
||||
},
|
||||
{
|
||||
field_name: 'international_trade_gdp',
|
||||
value: 10000,
|
||||
},
|
||||
{
|
||||
field_name: 'international_trade_p_gdp',
|
||||
value: 10000.1,
|
||||
},
|
||||
],
|
||||
];
|
||||
@@ -0,0 +1,164 @@
|
||||
/*
|
||||
* 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 } from 'react';
|
||||
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { TableType } from '@coze-arch/bot-api/memory';
|
||||
import { MemoryApi } from '@coze-arch/bot-api';
|
||||
import {
|
||||
IconCozArrowDown,
|
||||
IconCozImport,
|
||||
IconCozPlus,
|
||||
IconCozRefresh,
|
||||
IconCozTrashCan,
|
||||
} from '@coze-arch/coze-design/icons';
|
||||
import { Button, Dropdown, Modal } from '@coze-arch/coze-design';
|
||||
|
||||
import { BatchImportModal } from '../batch-import-modal';
|
||||
import { useConnectorOptions } from '../../hooks/use-connector-options';
|
||||
import { type TableFieldData } from './type';
|
||||
|
||||
export interface ToolButtonsBarProps {
|
||||
readonly: boolean;
|
||||
databaseId: string;
|
||||
tableType: TableType;
|
||||
tableFields: TableFieldData[];
|
||||
onNewRow: () => void;
|
||||
onRefresh: () => void;
|
||||
}
|
||||
|
||||
export function ToolButtonsBar({
|
||||
readonly,
|
||||
databaseId,
|
||||
tableType,
|
||||
tableFields,
|
||||
onNewRow,
|
||||
onRefresh,
|
||||
}: ToolButtonsBarProps) {
|
||||
const [connectorDropdownVisible, setConnectorDropdownVisible] =
|
||||
useState(false);
|
||||
const [batchImportVisible, setBatchImportVisible] = useState(false);
|
||||
const [batchImportConnectorId, setBatchImportConnectorId] = useState<
|
||||
string | undefined
|
||||
>();
|
||||
const connectorOptions = useConnectorOptions();
|
||||
const showBatchImportModal = (connectorId?: string) => {
|
||||
setConnectorDropdownVisible(false);
|
||||
setBatchImportVisible(true);
|
||||
setBatchImportConnectorId(connectorId);
|
||||
};
|
||||
|
||||
const handleClearDatabase = () =>
|
||||
Modal.confirm({
|
||||
title: I18n.t('dialog_240305_01'),
|
||||
content: I18n.t('dialog_240305_02'),
|
||||
okText: I18n.t('dialog_240305_03'),
|
||||
okButtonColor: 'red',
|
||||
cancelText: I18n.t('dialog_240305_04'),
|
||||
onOk: async () => {
|
||||
await MemoryApi.ResetBotTable({
|
||||
database_info_id: databaseId,
|
||||
table_type: tableType,
|
||||
});
|
||||
onRefresh();
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="flex gap-[8px] mt-[8px] mb-[12px]">
|
||||
<Button
|
||||
color="secondary"
|
||||
icon={<IconCozPlus className={readonly ? '' : 'coz-fg-hglt'} />}
|
||||
disabled={readonly}
|
||||
onClick={onNewRow}
|
||||
>
|
||||
<span className={readonly ? '' : 'coz-fg-hglt'}>
|
||||
{I18n.t('db_optimize_022')}
|
||||
</span>
|
||||
</Button>
|
||||
{tableType === TableType.DraftTable ? (
|
||||
<Button
|
||||
color="secondary"
|
||||
icon={<IconCozImport />}
|
||||
disabled={readonly}
|
||||
onClick={() => showBatchImportModal()}
|
||||
>
|
||||
{I18n.t('db_optimize_013')}
|
||||
</Button>
|
||||
) : (
|
||||
<Dropdown
|
||||
trigger="custom"
|
||||
visible={connectorDropdownVisible}
|
||||
onClickOutSide={() => setConnectorDropdownVisible(false)}
|
||||
position="bottomLeft"
|
||||
render={
|
||||
<>
|
||||
<Dropdown.Title className="pl-[32px] border-0 border-b border-solid coz-stroke-primary">
|
||||
{I18n.t('database_optimize_100')}
|
||||
</Dropdown.Title>
|
||||
<div className="min-w-[170px] max-h-[220px] overflow-auto">
|
||||
<Dropdown.Menu>
|
||||
{connectorOptions.map(item => (
|
||||
<Dropdown.Item
|
||||
key={item.value}
|
||||
onClick={() => showBatchImportModal(item.value)}
|
||||
>
|
||||
{item.label}
|
||||
</Dropdown.Item>
|
||||
))}
|
||||
</Dropdown.Menu>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<Button
|
||||
color="secondary"
|
||||
icon={<IconCozImport />}
|
||||
disabled={readonly}
|
||||
onClick={() => setConnectorDropdownVisible(true)}
|
||||
>
|
||||
<span>{I18n.t('db_optimize_013')}</span>
|
||||
<IconCozArrowDown className="ml-[4px]" />
|
||||
</Button>
|
||||
</Dropdown>
|
||||
)}
|
||||
<div className="ml-auto"></div>
|
||||
{tableType === TableType.DraftTable ? (
|
||||
<Button
|
||||
color="secondary"
|
||||
icon={<IconCozTrashCan />}
|
||||
disabled={readonly}
|
||||
onClick={handleClearDatabase}
|
||||
>
|
||||
{I18n.t('db_optimize_011')}
|
||||
</Button>
|
||||
) : null}
|
||||
<Button color="secondary" icon={<IconCozRefresh />} onClick={onRefresh}>
|
||||
{I18n.t('db_optimize_012')}
|
||||
</Button>
|
||||
<BatchImportModal
|
||||
visible={batchImportVisible}
|
||||
databaseId={databaseId}
|
||||
tableFields={tableFields}
|
||||
tableType={tableType}
|
||||
connectorId={batchImportConnectorId}
|
||||
onClose={() => setBatchImportVisible(false)}
|
||||
onComplete={onRefresh}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
/*
|
||||
* 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 TableMemoryItem } from '@coze-studio/bot-detail-store';
|
||||
import { type FieldItemType } from '@coze-arch/bot-api/memory';
|
||||
|
||||
// 期待的数据结构是什么样的?
|
||||
export interface TableRowCommonData {
|
||||
fieldName: string;
|
||||
required: boolean;
|
||||
}
|
||||
|
||||
export type TableRowInferData =
|
||||
| {
|
||||
type: FieldItemType.Boolean;
|
||||
value: boolean | string;
|
||||
}
|
||||
| {
|
||||
type: FieldItemType.Date;
|
||||
value: string;
|
||||
}
|
||||
| {
|
||||
type: FieldItemType.Float;
|
||||
value: string;
|
||||
}
|
||||
| {
|
||||
type: FieldItemType.Number;
|
||||
value: number;
|
||||
}
|
||||
| {
|
||||
type: FieldItemType.Text;
|
||||
value: string;
|
||||
};
|
||||
|
||||
export type TableField = TableRowCommonData & TableRowInferData;
|
||||
|
||||
export type TableRow = Record<string, TableField>;
|
||||
|
||||
export enum RowInternalStatus {
|
||||
Normal,
|
||||
UnSubmit,
|
||||
Error,
|
||||
}
|
||||
|
||||
export enum RowServiceStatus {
|
||||
Deleted,
|
||||
Normal,
|
||||
Shield,
|
||||
}
|
||||
|
||||
export interface TableRowData {
|
||||
rowData: TableRow;
|
||||
status: RowServiceStatus;
|
||||
internalStatus: RowInternalStatus;
|
||||
}
|
||||
|
||||
export type TableList = TableRowData[];
|
||||
|
||||
export interface TableFieldData {
|
||||
fieldName: string;
|
||||
fieldDescription: string;
|
||||
required: boolean;
|
||||
type: FieldItemType;
|
||||
}
|
||||
|
||||
export interface TableData {
|
||||
fieldList: TableFieldData[];
|
||||
dataList: TableList;
|
||||
}
|
||||
|
||||
export interface FormatTableDataProps {
|
||||
structList: TableMemoryItem[];
|
||||
dataRow: Array<Record<string, string>>;
|
||||
}
|
||||
|
||||
export interface TestDataStruct {
|
||||
field_name: string;
|
||||
value: string | number | boolean;
|
||||
}
|
||||
|
||||
export type TestDataRow = TestDataStruct[];
|
||||
|
||||
export interface ChangeDataParams {
|
||||
// rowKey: string;
|
||||
// fieldName: string;
|
||||
// value: string | number | boolean;
|
||||
newRowData: TableRow;
|
||||
}
|
||||
|
||||
export interface DeleteDataParams {
|
||||
rowKey: string;
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
.table-structure-wrapper {
|
||||
overflow: auto;
|
||||
height: 100%;
|
||||
|
||||
:global {
|
||||
.coz-table-spin {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.semi-table-container .semi-table-row {
|
||||
.semi-table-row-head,
|
||||
.semi-table-row-cell {
|
||||
padding: 6px 8px;
|
||||
}
|
||||
|
||||
.semi-table-row-head {
|
||||
border-bottom-width: 1px;
|
||||
}
|
||||
|
||||
.semi-table-row-cell {
|
||||
height: 56px;
|
||||
font-weight: 500;
|
||||
background: none;
|
||||
border-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
/*
|
||||
* 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, useRef, useState } from 'react';
|
||||
|
||||
import { type TableMemoryItem } from '@coze-studio/bot-detail-store';
|
||||
import {
|
||||
FIELD_TYPE_OPTIONS,
|
||||
SYSTEM_FIELDS,
|
||||
} from '@coze-data/database-v2-base/constants';
|
||||
import { DatabaseFieldTitle } from '@coze-data/database-v2-base/components/database-field-title';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { Image, Table, type ColumnProps } from '@coze-arch/coze-design';
|
||||
|
||||
import keyExample from '../../assets/key-example.png';
|
||||
|
||||
import s from './index.module.less';
|
||||
|
||||
function getTableStructureColumns(): ColumnProps<TableMemoryItem>[] {
|
||||
// 字段表头内容来自 ../database-table-structure/index.tsx:578
|
||||
return [
|
||||
{
|
||||
title: (
|
||||
<DatabaseFieldTitle
|
||||
field={I18n.t('db_add_table_field_name')}
|
||||
tip={
|
||||
<article className="w-[494px]">
|
||||
<p className="mb-[8px]">
|
||||
{I18n.t('db_add_table_field_name_tips')}
|
||||
</p>
|
||||
<Image
|
||||
preview={false}
|
||||
width={494}
|
||||
height={163}
|
||||
src={keyExample}
|
||||
/>
|
||||
</article>
|
||||
}
|
||||
/>
|
||||
),
|
||||
dataIndex: 'name',
|
||||
width: 261,
|
||||
},
|
||||
{
|
||||
title: (
|
||||
<DatabaseFieldTitle
|
||||
field={I18n.t('db_add_table_field_desc')}
|
||||
tip={
|
||||
<article className="w-[327px]">
|
||||
{I18n.t('db_add_table_field_desc_tips')}
|
||||
</article>
|
||||
}
|
||||
/>
|
||||
),
|
||||
dataIndex: 'desc',
|
||||
},
|
||||
{
|
||||
title: (
|
||||
<DatabaseFieldTitle
|
||||
field={I18n.t('db_add_table_field_type')}
|
||||
tip={
|
||||
<article className="w-[327px]">
|
||||
{I18n.t('db_add_table_field_type_tips')}
|
||||
</article>
|
||||
}
|
||||
/>
|
||||
),
|
||||
dataIndex: 'type',
|
||||
width: 214,
|
||||
render: (_, record) =>
|
||||
FIELD_TYPE_OPTIONS.find(i => i.value === record.type)?.label ??
|
||||
record.type,
|
||||
},
|
||||
{
|
||||
title: (
|
||||
<DatabaseFieldTitle
|
||||
field={I18n.t('db_add_table_field_necessary')}
|
||||
tip={
|
||||
<article className="w-[327px]">
|
||||
<p>{I18n.t('db_add_table_field_necessary_tips1')}</p>
|
||||
<p>{I18n.t('db_add_table_field_necessary_tips2')}</p>
|
||||
</article>
|
||||
}
|
||||
/>
|
||||
),
|
||||
dataIndex: 'must_required',
|
||||
width: 108,
|
||||
render: (_, record) =>
|
||||
I18n.t(record.must_required ? 'db_optimize_037' : 'db_optimize_038'),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
export interface DatabaseTableStructureReadonlyProps {
|
||||
loading?: boolean;
|
||||
fieldList: TableMemoryItem[];
|
||||
}
|
||||
|
||||
export function DatabaseTableStructureReadonly({
|
||||
loading,
|
||||
fieldList,
|
||||
}: DatabaseTableStructureReadonlyProps) {
|
||||
const columns = getTableStructureColumns();
|
||||
const dataSource = SYSTEM_FIELDS.concat(fieldList);
|
||||
|
||||
const [tableHeight, setTableHeight] = useState(0);
|
||||
const tableWrapperRef = useRef<HTMLDivElement>(null);
|
||||
useEffect(() => {
|
||||
const observer = new ResizeObserver(entries => {
|
||||
for (const e of entries) {
|
||||
if (e.target === tableWrapperRef.current) {
|
||||
setTableHeight(e.contentRect.height);
|
||||
}
|
||||
}
|
||||
});
|
||||
if (tableWrapperRef.current) {
|
||||
observer.observe(tableWrapperRef.current);
|
||||
}
|
||||
return () => observer.disconnect();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="h-full mt-[8px]" ref={tableWrapperRef}>
|
||||
<Table
|
||||
tableProps={{
|
||||
loading,
|
||||
columns,
|
||||
dataSource,
|
||||
scroll: {
|
||||
// 表头的高度是 40px
|
||||
y: tableHeight > 40 ? tableHeight - 40 : 'auto',
|
||||
},
|
||||
}}
|
||||
className={s['table-structure-wrapper']}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
.date {
|
||||
:global {
|
||||
.semi-select:hover {
|
||||
@apply coz-bg-max !important;
|
||||
}
|
||||
|
||||
.coz-date-picker-select {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { type FC, useState, useRef } from 'react';
|
||||
|
||||
import { isEmpty, cloneDeep } from 'lodash-es';
|
||||
import { format } from 'date-fns';
|
||||
import classNames from 'classnames';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { type BaseDatePicker, DatePicker } from '@coze-arch/coze-design';
|
||||
|
||||
import {
|
||||
type ChangeDataParams,
|
||||
type TableRow,
|
||||
} from '../../database-table-data/type';
|
||||
|
||||
import styles from './index.module.less';
|
||||
|
||||
interface IProps {
|
||||
rowData: TableRow;
|
||||
value: string | undefined;
|
||||
rowKey: string;
|
||||
fieldName: string;
|
||||
required: boolean;
|
||||
onChange?: (params: ChangeDataParams) => void;
|
||||
disabled: boolean;
|
||||
}
|
||||
|
||||
const formatValue = (dValue: string | Date | Date[] | string[] | undefined) => {
|
||||
let formattedValue = '';
|
||||
if (!dValue) {
|
||||
return '';
|
||||
}
|
||||
|
||||
try {
|
||||
if (dValue instanceof Date) {
|
||||
// 单个Date对象
|
||||
formattedValue = format(dValue, 'yyyy-MM-dd HH:mm:ss');
|
||||
} else if (Array.isArray(dValue)) {
|
||||
// Date[] 或 string[]
|
||||
formattedValue = dValue
|
||||
.map(item => {
|
||||
if (item instanceof Date) {
|
||||
return format(item, 'yyyy-MM-dd HH:mm:ss');
|
||||
} else if (typeof item === 'string') {
|
||||
// 假设字符串为有效日期格式
|
||||
return format(new Date(item), 'yyyy-MM-dd HH:mm:ss');
|
||||
}
|
||||
return '';
|
||||
})
|
||||
.join(', '); // 使用逗号分隔不同的日期
|
||||
} else if (typeof dValue === 'string') {
|
||||
// 单个字符串
|
||||
formattedValue = format(new Date(dValue), 'yyyy-MM-dd HH:mm:ss');
|
||||
}
|
||||
} catch {
|
||||
formattedValue = '';
|
||||
}
|
||||
|
||||
return formattedValue;
|
||||
};
|
||||
|
||||
export const EditKitDatePicker: FC<IProps> = props => {
|
||||
const { value, onChange, fieldName, required, rowData, disabled } = props;
|
||||
|
||||
const [clicked, setClicked] = useState(false);
|
||||
const [internalValue, setIntervalValue] = useState(formatValue(value));
|
||||
|
||||
const ref = useRef<BaseDatePicker>(null);
|
||||
|
||||
const handlePlaceholderClick = () => {
|
||||
if (disabled) {
|
||||
return;
|
||||
}
|
||||
setClicked(true);
|
||||
|
||||
setTimeout(() => {
|
||||
ref.current?.focus();
|
||||
ref.current?.open();
|
||||
}, 50);
|
||||
};
|
||||
|
||||
const handleInputBlur = () => {
|
||||
setClicked(false);
|
||||
};
|
||||
|
||||
const handleChange = (
|
||||
newValue: string | Date | Date[] | string[] | undefined,
|
||||
) => {
|
||||
const formattedValue = formatValue(newValue);
|
||||
setIntervalValue(formattedValue);
|
||||
const newRowData = cloneDeep(rowData);
|
||||
newRowData[fieldName].value = formattedValue;
|
||||
onChange?.({
|
||||
newRowData,
|
||||
});
|
||||
};
|
||||
|
||||
const showRequiredTips = required && isEmpty(internalValue);
|
||||
|
||||
if (disabled) {
|
||||
return (
|
||||
<div className="w-full h-[32px] cursor-not-allowed rounded-[8px] px-[8px] flex items-center border-[1px] border-solid border-transparent">
|
||||
<span
|
||||
className={'text-[14px] leading-[20px] truncate coz-fg-secondary'}
|
||||
>
|
||||
{internalValue}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!clicked) {
|
||||
return (
|
||||
<div
|
||||
className="w-full h-[32px] rounded-[8px] px-[8px] flex items-center hover:coz-mg-secondary-hovered cursor-pointer border-[1px] border-solid border-transparent"
|
||||
onClick={handlePlaceholderClick}
|
||||
>
|
||||
<span
|
||||
className={classNames('text-[14px] leading-[20px] truncate', {
|
||||
'coz-fg-secondary': !showRequiredTips,
|
||||
'coz-fg-hglt-red': showRequiredTips,
|
||||
})}
|
||||
>
|
||||
{showRequiredTips ? I18n.t('db2_008') : internalValue}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<DatePicker
|
||||
type="dateTime"
|
||||
value={internalValue}
|
||||
onChange={handleChange}
|
||||
onBlur={handleInputBlur}
|
||||
timePickerOpts={{
|
||||
scrollItemProps: { cycled: false },
|
||||
}}
|
||||
ref={ref}
|
||||
showPrefix={false}
|
||||
showSuffix={false}
|
||||
className={classNames(
|
||||
'w-full !coz-bg-max rounded-[8px] hover:!coz-bg-max',
|
||||
styles.date,
|
||||
)}
|
||||
disabled={disabled}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,7 @@
|
||||
.input {
|
||||
:global {
|
||||
.semi-input-wrapper,.semi-input-wrapper:hover {
|
||||
@apply coz-bg-max !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { useState, type FC, useRef } from 'react';
|
||||
|
||||
import { cloneDeep, isUndefined } from 'lodash-es';
|
||||
import classNames from 'classnames';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { Input, InputNumber } from '@coze-arch/coze-design';
|
||||
|
||||
import {
|
||||
type ChangeDataParams,
|
||||
type TableRow,
|
||||
} from '../../database-table-data/type';
|
||||
|
||||
import styles from './index.module.less';
|
||||
|
||||
interface IProps {
|
||||
rowData: TableRow;
|
||||
value: React.ReactText | undefined;
|
||||
type: 'string' | 'float' | 'integer';
|
||||
rowKey: string;
|
||||
fieldName: string;
|
||||
required: boolean;
|
||||
onChange?: (params: ChangeDataParams) => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export const EditKitInput: FC<IProps> = props => {
|
||||
const {
|
||||
value,
|
||||
type,
|
||||
fieldName,
|
||||
onChange,
|
||||
required,
|
||||
rowData,
|
||||
disabled = false,
|
||||
} = props;
|
||||
|
||||
const [clicked, setClicked] = useState(false);
|
||||
|
||||
const [internalValue, setInternalValue] = useState(value);
|
||||
|
||||
const handleChange = (newValue: React.ReactText) => {
|
||||
setInternalValue(newValue);
|
||||
};
|
||||
|
||||
const ref = useRef<HTMLInputElement>(null);
|
||||
|
||||
const handlePlaceholderClick = () => {
|
||||
if (disabled) {
|
||||
return;
|
||||
}
|
||||
setClicked(true);
|
||||
|
||||
setTimeout(() => {
|
||||
ref.current?.focus();
|
||||
}, 50);
|
||||
};
|
||||
|
||||
const handleInputBlur = () => {
|
||||
const newRowData = cloneDeep(rowData);
|
||||
newRowData[fieldName].value = internalValue || '';
|
||||
onChange?.({
|
||||
newRowData,
|
||||
});
|
||||
setClicked(false);
|
||||
};
|
||||
|
||||
const showRequiredTips =
|
||||
required && (isUndefined(internalValue) || internalValue === '');
|
||||
|
||||
if (disabled) {
|
||||
return (
|
||||
<div className="w-full h-[32px] rounded-[8px] cursor-not-allowed px-[8px] flex items-center border-[1px] border-solid border-transparent">
|
||||
<span
|
||||
className={'text-[14px] leading-[20px] truncate coz-fg-secondary'}
|
||||
>
|
||||
{internalValue}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!clicked) {
|
||||
return (
|
||||
<div
|
||||
className={`w-full h-[32px] rounded-[8px] px-[8px] flex items-center border-[1px] border-solid border-transparent ${
|
||||
disabled
|
||||
? 'cursor-not-allowed'
|
||||
: 'hover:coz-mg-secondary-hovered cursor-pointer'
|
||||
}`}
|
||||
onClick={handlePlaceholderClick}
|
||||
>
|
||||
<span
|
||||
className={classNames('text-[14px] leading-[20px] truncate', {
|
||||
'coz-fg-secondary': !showRequiredTips,
|
||||
'coz-fg-dim': showRequiredTips,
|
||||
})}
|
||||
>
|
||||
{showRequiredTips ? I18n.t('db2_008') : internalValue}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (type === 'float') {
|
||||
return (
|
||||
<InputNumber
|
||||
value={internalValue}
|
||||
onChange={handleChange}
|
||||
ref={ref}
|
||||
onBlur={handleInputBlur}
|
||||
keepFocus={true}
|
||||
max={Number.MAX_SAFE_INTEGER}
|
||||
min={Number.MIN_SAFE_INTEGER}
|
||||
hideButtons={true}
|
||||
className={classNames('w-full', styles.input)}
|
||||
disabled={disabled}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (type === 'integer') {
|
||||
return (
|
||||
<InputNumber
|
||||
value={internalValue}
|
||||
onChange={handleChange}
|
||||
precision={0}
|
||||
ref={ref}
|
||||
onBlur={handleInputBlur}
|
||||
keepFocus={true}
|
||||
max={Number.MAX_SAFE_INTEGER}
|
||||
min={Number.MIN_SAFE_INTEGER}
|
||||
hideButtons={true}
|
||||
className={classNames('w-full', styles.input)}
|
||||
disabled={disabled}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Input
|
||||
value={internalValue}
|
||||
onChange={handleChange}
|
||||
ref={ref}
|
||||
onBlur={handleInputBlur}
|
||||
className={classNames('w-full', styles.input)}
|
||||
disabled={disabled}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { type FC, useState } from 'react';
|
||||
|
||||
import { cloneDeep } from 'lodash-es';
|
||||
import { Switch } from '@coze-arch/coze-design';
|
||||
|
||||
import {
|
||||
type ChangeDataParams,
|
||||
type TableRow,
|
||||
} from '../../database-table-data/type';
|
||||
|
||||
interface IProps {
|
||||
rowData: TableRow;
|
||||
checked: boolean | undefined;
|
||||
rowKey: string;
|
||||
fieldName: string;
|
||||
required: boolean;
|
||||
disabled: boolean;
|
||||
onChange?: (params: ChangeDataParams) => void;
|
||||
}
|
||||
|
||||
export const EditKitSwitch: FC<IProps> = props => {
|
||||
const { checked, onChange, fieldName, rowData, disabled } = props;
|
||||
|
||||
const [internalValue, setInternalValue] = useState(checked);
|
||||
|
||||
const handleChange = (isChecked: boolean) => {
|
||||
setInternalValue(isChecked);
|
||||
const newRowData = cloneDeep(rowData);
|
||||
newRowData[fieldName].value = isChecked;
|
||||
onChange?.({
|
||||
newRowData,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Switch
|
||||
disabled={disabled}
|
||||
checked={internalValue}
|
||||
onChange={handleChange}
|
||||
size="small"
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,117 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { useState, type FC, useRef } from 'react';
|
||||
|
||||
import { isUndefined, cloneDeep } from 'lodash-es';
|
||||
import classNames from 'classnames';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { TextArea } from '@coze-arch/coze-design';
|
||||
|
||||
import {
|
||||
type ChangeDataParams,
|
||||
type TableRow,
|
||||
} from '../../database-table-data/type';
|
||||
|
||||
interface IProps {
|
||||
rowData: TableRow;
|
||||
value: string | undefined;
|
||||
rowKey: string;
|
||||
fieldName: string;
|
||||
required: boolean;
|
||||
disabled: boolean;
|
||||
onChange?: (params: ChangeDataParams) => void;
|
||||
}
|
||||
|
||||
export const EditKitTextarea: FC<IProps> = props => {
|
||||
const { value, fieldName, onChange, required, rowData, disabled } = props;
|
||||
|
||||
const [clicked, setClicked] = useState(false);
|
||||
|
||||
const [internalValue, setInternalValue] = useState(value);
|
||||
|
||||
const handleChange = (newValue: string) => {
|
||||
setInternalValue(newValue);
|
||||
};
|
||||
|
||||
const ref = useRef<HTMLTextAreaElement>(null);
|
||||
|
||||
const handlePlaceholderClick = () => {
|
||||
setClicked(true);
|
||||
|
||||
setTimeout(() => {
|
||||
ref.current?.focus();
|
||||
}, 50);
|
||||
};
|
||||
|
||||
const handleInputBlur = () => {
|
||||
const newRowData = cloneDeep(rowData);
|
||||
newRowData[fieldName].value = internalValue || '';
|
||||
onChange?.({
|
||||
newRowData,
|
||||
});
|
||||
setClicked(false);
|
||||
};
|
||||
|
||||
const showRequiredTips =
|
||||
required && (isUndefined(internalValue) || internalValue === '');
|
||||
|
||||
if (disabled) {
|
||||
return (
|
||||
<div className="w-full h-[32px] cursor-not-allowed rounded-[8px] px-[8px] flex items-center border-[1px] border-solid border-transparent">
|
||||
<span
|
||||
className={'text-[14px] leading-[20px] truncate coz-fg-secondary'}
|
||||
>
|
||||
{internalValue}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!clicked) {
|
||||
return (
|
||||
<div
|
||||
className="w-full h-[32px] rounded-[8px] px-[8px] flex items-center hover:coz-mg-secondary-hovered cursor-pointer border-[1px] border-solid border-transparent"
|
||||
onClick={handlePlaceholderClick}
|
||||
>
|
||||
<span
|
||||
className={classNames('text-[14px] leading-[20px] truncate', {
|
||||
'coz-fg-secondary': !showRequiredTips,
|
||||
'coz-fg-dim': showRequiredTips,
|
||||
})}
|
||||
>
|
||||
{showRequiredTips ? I18n.t('db2_008') : internalValue}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<TextArea
|
||||
disabled={disabled}
|
||||
value={internalValue}
|
||||
onChange={handleChange}
|
||||
ref={ref}
|
||||
onBlur={handleInputBlur}
|
||||
className={classNames('w-full !coz-bg-max')}
|
||||
rows={1}
|
||||
autosize={{
|
||||
minRows: 1,
|
||||
maxRows: 5,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,280 @@
|
||||
/*
|
||||
* 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, useRef, useState } from 'react';
|
||||
|
||||
import dayjs from 'dayjs';
|
||||
import classNames from 'classnames';
|
||||
import { type TableMemoryItem } from '@coze-studio/bot-detail-store';
|
||||
import {
|
||||
PLATFORM_FIELD,
|
||||
SYSTEM_FIELD_ROW_INDEX,
|
||||
} from '@coze-data/database-v2-base/constants';
|
||||
import { DatabaseFieldTitle } from '@coze-data/database-v2-base/components/database-field-title';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { FieldItemType, TableType } from '@coze-arch/bot-api/memory';
|
||||
import {
|
||||
CozInputNumber,
|
||||
type DatePickerProps,
|
||||
DatePicker,
|
||||
Form,
|
||||
TextArea,
|
||||
Modal,
|
||||
Select,
|
||||
withField,
|
||||
type CommonFieldProps,
|
||||
} from '@coze-arch/coze-design';
|
||||
|
||||
import {
|
||||
type TableRow,
|
||||
type TableFieldData,
|
||||
} from '../database-table-data/type';
|
||||
import { isInInt64Range } from '../../utils/is-in-int64-range';
|
||||
import { useConnectorOptions } from '../../hooks/use-connector-options';
|
||||
|
||||
const FormTextArea = withField(TextArea);
|
||||
const FormInputNumber = withField(CozInputNumber);
|
||||
const FormDatePicker = withField(
|
||||
(
|
||||
props: Omit<DatePickerProps, 'onChange'> & {
|
||||
onChange?: (dateString: string) => void;
|
||||
},
|
||||
) => (
|
||||
<DatePicker
|
||||
{...props}
|
||||
type="dateTime"
|
||||
// Semi DatePicker 使用 date-fns 格式
|
||||
format="yyyy-MM-dd HH:mm:ss"
|
||||
onChange={date =>
|
||||
props.onChange?.(dayjs(date as Date).format('YYYY-MM-DD HH:mm:ss'))
|
||||
}
|
||||
/>
|
||||
),
|
||||
);
|
||||
const FormSelect = withField(Select);
|
||||
|
||||
function tableRowToFormValues(row: TableRow): Record<string, string> {
|
||||
return Object.fromEntries(
|
||||
Object.values(row).map(field => [
|
||||
field.fieldName,
|
||||
field.value?.toString() ?? '',
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
function stringifyFormValues(
|
||||
values: Record<string, string | number | boolean>,
|
||||
) {
|
||||
return Object.fromEntries(
|
||||
Object.entries(values).map(([key, value]) => [
|
||||
key,
|
||||
value?.toString() ?? '',
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
export interface RowEditModalProps {
|
||||
visible: boolean;
|
||||
fields: TableFieldData[];
|
||||
tableType?: TableType;
|
||||
initialValues?: TableRow;
|
||||
onSubmit: (
|
||||
values: Record<string, string>,
|
||||
originalConnectorId?: string,
|
||||
) => Promise<void>;
|
||||
onCancel: () => void;
|
||||
}
|
||||
|
||||
export function RowEditModal({
|
||||
visible,
|
||||
fields,
|
||||
tableType,
|
||||
initialValues,
|
||||
onSubmit,
|
||||
onCancel,
|
||||
}: RowEditModalProps) {
|
||||
const isAdd = typeof initialValues !== 'object';
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const formRef = useRef<Form>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (visible && initialValues) {
|
||||
formRef.current?.formApi?.setValues(tableRowToFormValues(initialValues));
|
||||
}
|
||||
}, [visible, initialValues]);
|
||||
|
||||
const connectorOptions = useConnectorOptions();
|
||||
|
||||
return (
|
||||
<Modal
|
||||
visible={visible}
|
||||
title={I18n.t(isAdd ? 'db_optimize_022' : 'db_optimize_023')}
|
||||
okText={I18n.t(isAdd ? 'db_optimize_025' : 'db_edit_save')}
|
||||
okButtonProps={{ loading: isSubmitting }}
|
||||
onOk={async () => {
|
||||
setIsSubmitting(true);
|
||||
try {
|
||||
const values = await formRef.current?.formApi?.validate();
|
||||
if (values) {
|
||||
await onSubmit(
|
||||
Object.assign(
|
||||
initialValues ? tableRowToFormValues(initialValues) : {},
|
||||
stringifyFormValues(values),
|
||||
),
|
||||
initialValues?.bstudio_connector_id?.value as string | undefined,
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
}}
|
||||
cancelText={I18n.t('db_optimize_024')}
|
||||
onCancel={() => {
|
||||
onCancel();
|
||||
formRef.current?.formApi?.reset();
|
||||
}}
|
||||
>
|
||||
<Form<Record<string, unknown>> allowEmpty ref={formRef}>
|
||||
{tableType === TableType.OnlineTable ? (
|
||||
// 只有“线上数据”支持修改“渠道”字段
|
||||
<FormSelect
|
||||
{...getSystemFieldCommonProps(PLATFORM_FIELD)}
|
||||
optionList={connectorOptions}
|
||||
className="w-full"
|
||||
/>
|
||||
) : null}
|
||||
{fields.map(field => {
|
||||
const commonProps = getUserFieldCommonProps(field);
|
||||
switch (field.type) {
|
||||
case FieldItemType.Text: {
|
||||
return (
|
||||
<FormTextArea
|
||||
{...commonProps}
|
||||
autosize={{ minRows: 1, maxRows: 5 }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
case FieldItemType.Number: {
|
||||
return (
|
||||
<Form.Input
|
||||
{...commonProps}
|
||||
className={classNames(
|
||||
'w-full',
|
||||
'[&_.semi-input-wrapper]:coz-stroke-plus',
|
||||
'focus-within:[&_.semi-input-wrapper]:coz-stroke-hglt',
|
||||
'[&_.semi-input-wrapper.semi-input-wrapper-error]:coz-stroke-hglt-red',
|
||||
)}
|
||||
validate={value => {
|
||||
if (!isInInt64Range(value?.toString() ?? '')) {
|
||||
return 'invalid Integer';
|
||||
}
|
||||
return '';
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
case FieldItemType.Date: {
|
||||
return (
|
||||
<FormDatePicker
|
||||
{...commonProps}
|
||||
className={classNames(
|
||||
'w-full',
|
||||
'[&_.semi-datepicker-input]:w-full',
|
||||
'[&_.coz-date-picker-select]:w-full',
|
||||
'[&[aria-invalid]_.coz-date-picker-select]:coz-stroke-hglt-red',
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
case FieldItemType.Float: {
|
||||
return (
|
||||
<FormInputNumber
|
||||
{...commonProps}
|
||||
className={classNames(
|
||||
'w-full',
|
||||
'[&_.semi-input-wrapper]:coz-stroke-plus',
|
||||
'focus-within:[&_.semi-input-wrapper]:coz-stroke-hglt',
|
||||
'[&_.semi-input-wrapper.semi-input-wrapper-error]:coz-stroke-hglt-red',
|
||||
)}
|
||||
validate={value => {
|
||||
if (Number.isNaN(value) || Math.abs(value) === Infinity) {
|
||||
return 'invalid Float';
|
||||
}
|
||||
return '';
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
case FieldItemType.Boolean: {
|
||||
return (
|
||||
<FormSelect
|
||||
{...commonProps}
|
||||
optionList={[
|
||||
{ value: 'true', label: 'true' },
|
||||
{ value: 'false', label: 'false' },
|
||||
]}
|
||||
className="w-full"
|
||||
/>
|
||||
);
|
||||
}
|
||||
default: {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
})}
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
type FieldCommonProps = React.Attributes & CommonFieldProps;
|
||||
|
||||
function getSystemFieldCommonProps(field: TableMemoryItem): FieldCommonProps {
|
||||
return {
|
||||
key: field.name,
|
||||
field: SYSTEM_FIELD_ROW_INDEX[field.name ?? ''] ?? '',
|
||||
label: (
|
||||
<DatabaseFieldTitle
|
||||
field={field.name}
|
||||
textType="primary"
|
||||
type={field.type}
|
||||
tip={field.desc}
|
||||
required
|
||||
/>
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
function getUserFieldCommonProps(field: TableFieldData): FieldCommonProps {
|
||||
return {
|
||||
key: field.fieldName,
|
||||
field: field.fieldName,
|
||||
rules: [{ required: field.required }],
|
||||
label: {
|
||||
text: (
|
||||
<DatabaseFieldTitle
|
||||
field={field.fieldName}
|
||||
textType="primary"
|
||||
type={field.type}
|
||||
tip={field.fieldDescription}
|
||||
required={field.required}
|
||||
/>
|
||||
),
|
||||
// DatabaseFieldTitle 中已经显示 required * 符号
|
||||
required: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
/* stylelint-disable no-descending-specificity */
|
||||
/* stylelint-disable no-duplicate-selectors */
|
||||
/* stylelint-disable declaration-no-important */
|
||||
/* stylelint-disable font-family-no-missing-generic-family-keyword */
|
||||
.select {
|
||||
:global {
|
||||
.semi-select:hover,
|
||||
.semi-select:active {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
.semi-select-focus, .semi-select-open {
|
||||
border: none;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.semi-select-selection .semi-select-selection-text {
|
||||
font-weight: 600!important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.bottom-shadow {
|
||||
background: linear-gradient(180deg, rgba(249, 249, 249, 0%) 0%, rgba(249, 249, 249, 100%) 100%);
|
||||
}
|
||||
|
||||
|
||||
.label {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
font-style: normal;
|
||||
line-height: 20px; /* 142.857% */
|
||||
color: var(--Fg-COZ-fg-secondary, rgba(6, 7, 9, 50%));
|
||||
}
|
||||
|
||||
.tips-wrapper {
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.tip-title {
|
||||
margin-bottom: 10px;
|
||||
|
||||
/* COZText12Bold */
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
font-style: normal;
|
||||
line-height: 16px; /* 133.333% */
|
||||
color: var(--Fg-COZ-fg-plus, #FFF);
|
||||
}
|
||||
|
||||
.tip-desc {
|
||||
margin: 8px 0;
|
||||
|
||||
/* COZText12Regular */
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
line-height: 16px; /* 133.333% */
|
||||
color: rgba(255, 255, 255, 39%);
|
||||
}
|
||||
|
||||
.bot-bg {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
align-items: flex-start;
|
||||
align-self: stretch;
|
||||
|
||||
padding: 16px 12px;
|
||||
|
||||
background: var(--Mg-COZ-mg-primary, rgba(255, 255, 255, 6%));
|
||||
border-radius: var(--default, 8px);
|
||||
}
|
||||
|
||||
.bot-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: start;
|
||||
}
|
||||
|
||||
.bot-img {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
width: 24px !important;
|
||||
height: 24px !important;
|
||||
margin-right: 8px;
|
||||
padding: 8px;
|
||||
|
||||
font-family: "SF Pro Display";
|
||||
font-size: 8px;
|
||||
font-weight: 500;
|
||||
font-style: normal;
|
||||
line-height: 7.254px; /* 90.677% */
|
||||
color: #FFF;
|
||||
|
||||
border-radius: 50%;
|
||||
|
||||
&.img-user {
|
||||
background-color: var(--Fg-COZ-fg-color-blue, #0084FF);
|
||||
}
|
||||
|
||||
&.img-bot {
|
||||
background-color: var(--Fg-COZ-fg-color-cyan, #00B9B5);
|
||||
}
|
||||
}
|
||||
|
||||
.bot-content {
|
||||
width: 100%;
|
||||
padding: 8px 12px;
|
||||
|
||||
/* COZText10Regular */
|
||||
font-size: 10px;
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
line-height: 14px; /* 140% */
|
||||
color: var(--Fg-COZ-fg-primary, rgba(255, 255, 255, 79%));
|
||||
|
||||
border-radius: var(--default, 8px);
|
||||
|
||||
&.content-user {
|
||||
background: var(--Mg-COZ-mg-hglt-plus-dim, rgba(94, 94, 255, 37%));
|
||||
}
|
||||
|
||||
&.content-bot {
|
||||
background: var(--Mg-COZ-mg-plus, rgba(255, 255, 255, 9%));
|
||||
}
|
||||
}
|
||||
|
||||
.loading-more,
|
||||
.no-more {
|
||||
position: relative;
|
||||
|
||||
display: flex;
|
||||
grid-column: 1 / -1;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
width: 100%;
|
||||
padding: 13px 0;
|
||||
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
line-height: 20px;
|
||||
color: var(--light-usage-text-color-text-2,
|
||||
var(--light-usage-text-color-text-2, rgb(28 31 35 / 60%)));
|
||||
}
|
||||
|
||||
.database-add {
|
||||
min-width: 48px;
|
||||
min-height: 30px;
|
||||
max-height: 30px;
|
||||
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
font-style: normal;
|
||||
line-height: 16.5px; /* 137.5% */
|
||||
color: var(--coz-fg-primary) !important;
|
||||
|
||||
background-color: var(--coz-mg-primary) !important;
|
||||
}
|
||||
|
||||
.database-added {
|
||||
min-width: 48px;
|
||||
min-height: 30px;
|
||||
max-height: 30px;
|
||||
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
font-style: normal;
|
||||
line-height: 16.5px; /* 137.5% */
|
||||
color: var(--coz-fg-dim) !important;
|
||||
|
||||
background-color: var(--coz-mg-primary) !important;
|
||||
|
||||
&.added-mousein {
|
||||
color: var(--light-color-red-red-5, #ff441e) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.list {
|
||||
:global(.semi-spin-children) {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,599 @@
|
||||
/*
|
||||
* 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 max-lines */
|
||||
import React, {
|
||||
useState,
|
||||
type FC,
|
||||
useRef,
|
||||
useEffect,
|
||||
type ReactNode,
|
||||
} from 'react';
|
||||
|
||||
import { debounce } from 'lodash-es';
|
||||
import classNames from 'classnames';
|
||||
import { useInfiniteScroll } from 'ahooks';
|
||||
import { IllustrationNoContent } from '@douyinfe/semi-illustrations';
|
||||
import { IconSpin } from '@douyinfe/semi-icons';
|
||||
import { userStoreService } from '@coze-studio/user-store';
|
||||
import { type DatabaseInfo as DatabaseInitInfo } from '@coze-studio/bot-detail-store';
|
||||
import { DatabaseCreateTableModal } from '@coze-data/database-v2-adapter/components/create-table-modal';
|
||||
import { getUnReactiveLanguage, I18n } from '@coze-arch/i18n';
|
||||
import {
|
||||
Image,
|
||||
UICompositionModal,
|
||||
UICompositionModalMain,
|
||||
UICompositionModalSider,
|
||||
} from '@coze-arch/bot-semi';
|
||||
import { IconCozArrowDown } from '@coze-arch/bot-icons';
|
||||
import {
|
||||
BotTableRWMode,
|
||||
type DatabaseInfo,
|
||||
TableType,
|
||||
SortDirection,
|
||||
type SingleDatabaseResponse,
|
||||
} from '@coze-arch/bot-api/memory';
|
||||
import { FormatType } from '@coze-arch/bot-api/knowledge';
|
||||
import { MemoryApi, KnowledgeApi } from '@coze-arch/bot-api';
|
||||
import {
|
||||
Button,
|
||||
Dropdown,
|
||||
Input,
|
||||
Tag,
|
||||
Popover,
|
||||
Spin,
|
||||
Select,
|
||||
Empty,
|
||||
} from '@coze-arch/coze-design';
|
||||
|
||||
import { useLibraryCreateDatabaseModal } from '../../hooks/use-library-create-database-modal';
|
||||
import tipsTemplateEN from '../../assets/tips-template-en.png';
|
||||
import tipsTemplateCN from '../../assets/tips-template-cn.png';
|
||||
import SiderCategory from './sider-category';
|
||||
import { DatabaseListItem } from './items';
|
||||
|
||||
import styles from './index.module.less';
|
||||
|
||||
interface SelectDatabaseModalProps {
|
||||
visible: boolean;
|
||||
onClose: () => void;
|
||||
onAddDatabase: (id: string, addCallback?: () => void) => void;
|
||||
onRemoveDatabase?: (id: string, removeCallback?: () => void) => void;
|
||||
onClickDatabase: (id: string) => void;
|
||||
onCreateDatabase?: (id: string, draftId: string) => void;
|
||||
enterFrom: string;
|
||||
botId?: string;
|
||||
workflowId?: string;
|
||||
spaceId: string;
|
||||
workflowAddList?: string[];
|
||||
projectID?: string;
|
||||
tips?: ReactNode;
|
||||
}
|
||||
|
||||
interface GetDatabaseListData {
|
||||
list: DatabaseInfo[];
|
||||
nextOffset: number;
|
||||
total: number;
|
||||
hasMore: boolean | undefined;
|
||||
}
|
||||
|
||||
enum ModalMode {
|
||||
CUSTOMIZE = 'customize',
|
||||
TEMPLATE = 'template',
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @coze-arch/max-line-per-function, max-lines-per-function
|
||||
export const useSelectDatabaseModal = ({
|
||||
visible,
|
||||
onClose,
|
||||
onAddDatabase,
|
||||
onRemoveDatabase,
|
||||
onClickDatabase,
|
||||
onCreateDatabase,
|
||||
enterFrom,
|
||||
botId,
|
||||
spaceId,
|
||||
workflowAddList = [],
|
||||
projectID,
|
||||
tips,
|
||||
}: SelectDatabaseModalProps) => {
|
||||
const language = getUnReactiveLanguage();
|
||||
const userInfo = userStoreService.useUserInfo();
|
||||
|
||||
const scrollRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const [category, setCategory] = useState<'library' | 'project'>(
|
||||
projectID ? 'project' : 'library',
|
||||
);
|
||||
|
||||
// dropdown visible
|
||||
const [dropdownVisible, setDropdownVisible] = useState<boolean>(false);
|
||||
// whether there is a shadow on th bottom
|
||||
const [showBottomShadow, setShowBottomShadow] = useState(true);
|
||||
// filter creator
|
||||
const [filterCreator, setFilterCreator] = useState<string>('all');
|
||||
// sotr method
|
||||
const [sort, setSort] = useState<string>('create_time');
|
||||
// search value
|
||||
const [keyword, setKeyword] = useState<string>('');
|
||||
// create table init value
|
||||
const [initValue, setInitValue] = useState<DatabaseInitInfo>({
|
||||
tableId: '',
|
||||
name: '',
|
||||
desc: '',
|
||||
icon_uri: '',
|
||||
readAndWriteMode: BotTableRWMode.LimitedReadWrite,
|
||||
tableMemoryList: [],
|
||||
});
|
||||
// modal visible
|
||||
const [createVisible, setCreateVisible] = useState<boolean>(false);
|
||||
|
||||
const fetchDatabaseList = async (reqParams: {
|
||||
key_word: string;
|
||||
filter_creator: string;
|
||||
page_offset: number;
|
||||
sort_by: string;
|
||||
}) => {
|
||||
const { key_word, filter_creator, page_offset, sort_by } = reqParams;
|
||||
const res = await MemoryApi.ListDatabase({
|
||||
...(category === 'project' ? { project_id: projectID } : {}),
|
||||
bot_id: enterFrom === 'bot' ? botId : '0',
|
||||
space_id: spaceId,
|
||||
table_type:
|
||||
enterFrom === 'bot' ? TableType.DraftTable : TableType.OnlineTable,
|
||||
table_name: key_word,
|
||||
creator_id: filter_creator === 'all' ? '0' : filter_creator,
|
||||
// 暂时不做分页加载
|
||||
limit: 50,
|
||||
offset: page_offset,
|
||||
order_by: [
|
||||
{
|
||||
field: sort_by,
|
||||
direction: SortDirection.Desc,
|
||||
},
|
||||
],
|
||||
});
|
||||
return {
|
||||
list: res.database_info_list || [],
|
||||
nextOffset: page_offset + 1,
|
||||
total: res.total_count as number,
|
||||
hasMore: res.has_more,
|
||||
};
|
||||
};
|
||||
|
||||
const { loading, data, loadingMore, reload } = useInfiniteScroll(
|
||||
(newData?: GetDatabaseListData): Promise<GetDatabaseListData> =>
|
||||
fetchDatabaseList({
|
||||
key_word: keyword,
|
||||
filter_creator: filterCreator,
|
||||
page_offset: newData?.nextOffset || 0,
|
||||
sort_by: sort,
|
||||
}),
|
||||
{
|
||||
manual: true,
|
||||
// true meas there is more data
|
||||
isNoMore: newData => Boolean(!newData?.total || !newData.hasMore),
|
||||
reloadDeps: [keyword, filterCreator, sort, category, projectID],
|
||||
target: scrollRef,
|
||||
},
|
||||
);
|
||||
|
||||
// onScroll 判断 scrollRef 是否触底
|
||||
const handleScroll = () => {
|
||||
if (!scrollRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { scrollTop, scrollHeight, clientHeight } = scrollRef.current;
|
||||
const scrollBottom = scrollHeight - (scrollTop + clientHeight);
|
||||
|
||||
if (scrollBottom < 1) {
|
||||
setShowBottomShadow(false);
|
||||
} else {
|
||||
setShowBottomShadow(true);
|
||||
}
|
||||
};
|
||||
|
||||
const handleAddDatabase = (item: DatabaseInfo) => {
|
||||
if (onAddDatabase && item.id) {
|
||||
onAddDatabase?.(item.id, reload);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemoveDatabase = (item: DatabaseInfo) => {
|
||||
if (onRemoveDatabase && item.id) {
|
||||
onRemoveDatabase?.(item.id, reload);
|
||||
}
|
||||
};
|
||||
|
||||
const handleClickDatabase = (item: DatabaseInfo) => {
|
||||
if (onClickDatabase && item.id) {
|
||||
onClickDatabase?.(item.id);
|
||||
}
|
||||
};
|
||||
|
||||
const openCreateTableModal = (mode: ModalMode) => {
|
||||
if (mode === ModalMode.TEMPLATE) {
|
||||
setInitValue({
|
||||
...initValue,
|
||||
name: 'reading_notes',
|
||||
desc: 'for saving reading notes',
|
||||
readAndWriteMode: BotTableRWMode.LimitedReadWrite,
|
||||
extra_info: {
|
||||
prompt_disabled: 'true',
|
||||
},
|
||||
tableMemoryList: [
|
||||
{
|
||||
name: 'name',
|
||||
desc: '',
|
||||
type: 1,
|
||||
must_required: true,
|
||||
},
|
||||
{
|
||||
name: 'section',
|
||||
desc: '',
|
||||
type: 2,
|
||||
must_required: true,
|
||||
},
|
||||
{
|
||||
name: 'note',
|
||||
desc: '',
|
||||
type: 1,
|
||||
must_required: true,
|
||||
},
|
||||
],
|
||||
});
|
||||
} else {
|
||||
setInitValue({
|
||||
tableId: '',
|
||||
name: '',
|
||||
desc: '',
|
||||
readAndWriteMode: BotTableRWMode.LimitedReadWrite,
|
||||
tableMemoryList: [],
|
||||
});
|
||||
}
|
||||
setCreateVisible(true);
|
||||
};
|
||||
|
||||
const renderTemplateTips = () => (
|
||||
<div className={styles['tips-wrapper']}>
|
||||
<div className={styles['tip-title']}>{I18n.t('db2_018')}:</div>
|
||||
<p className="my-[8px]">
|
||||
💡 <em className={styles['tip-desc']}>{I18n.t('db2_019')}:</em>
|
||||
</p>
|
||||
<Image
|
||||
height={136}
|
||||
src={language === 'zh-CN' ? tipsTemplateCN : tipsTemplateEN}
|
||||
/>
|
||||
<div className={styles['tip-title']}>{I18n.t('db2_020')}:</div>
|
||||
<div className={styles['bot-bg']}>
|
||||
<div className={classNames(styles['bot-item'], 'mb-[12px]')}>
|
||||
<div className={classNames(styles['bot-img'], styles['img-user'])}>
|
||||
{I18n.t('db2_021')}
|
||||
</div>
|
||||
<div
|
||||
className={classNames(
|
||||
styles['bot-content'],
|
||||
styles['content-user'],
|
||||
)}
|
||||
>
|
||||
{I18n.t('db2_022')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles['bot-item']}>
|
||||
<div className={classNames(styles['bot-img'], styles['img-bot'])}>
|
||||
{I18n.t('db2_023')}
|
||||
</div>
|
||||
<div
|
||||
className={classNames(styles['bot-content'], styles['content-bot'])}
|
||||
>
|
||||
{I18n.t('db2_024')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const renderInput = () => (
|
||||
<Input
|
||||
placeholder={I18n.t('db2_014')}
|
||||
className="w-full"
|
||||
value={keyword}
|
||||
onChange={debounce(v => {
|
||||
setKeyword(v);
|
||||
}, 500)}
|
||||
/>
|
||||
);
|
||||
|
||||
const renderFilter = () => (
|
||||
<div className="flex flex-row items-center w-full justify-between pr-[12px]">
|
||||
<div className={classNames(styles.select, 'flex flex-row flex-1')}>
|
||||
<div className="flex flex-row items-center">
|
||||
<Select
|
||||
showArrow
|
||||
size="default"
|
||||
className="border-none ml-[4px] hover:border-none bg-transparent outline-none"
|
||||
value={filterCreator}
|
||||
onChange={v => setFilterCreator(v as string)}
|
||||
insetLabel={<p className={styles.label}>{I18n.t('db2_009')}</p>}
|
||||
>
|
||||
<Select.Option value={'all'} label={I18n.t('db2_010')} />
|
||||
{userInfo ? (
|
||||
<Select.Option
|
||||
value={userInfo.user_id_str}
|
||||
label={userInfo.name}
|
||||
key={userInfo.user_id_str}
|
||||
/>
|
||||
) : null}
|
||||
</Select>
|
||||
</div>
|
||||
<div className="flex flex-row items-center ml-[12px]">
|
||||
<Select
|
||||
showArrow
|
||||
size="default"
|
||||
className="border-none ml-[4px] hover:border-none bg-transparent outline-none"
|
||||
value={sort}
|
||||
onChange={v => setSort(v as string)}
|
||||
insetLabel={<p className={styles.label}>{I18n.t('db2_011')}</p>}
|
||||
>
|
||||
<Select.Option value="create_time" label={I18n.t('db2_012')} />
|
||||
<Select.Option value="update_time" label={I18n.t('db2_013')} />
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const renderList = () => (
|
||||
<div
|
||||
className="overflow-y-auto relative h-full"
|
||||
ref={scrollRef}
|
||||
onScroll={handleScroll}
|
||||
>
|
||||
{/* FIXME: 这里需要根据实际做渲染 */}
|
||||
{data?.list.map((item, index) => (
|
||||
<DatabaseListItem
|
||||
icon={item.icon_url}
|
||||
title={item.table_name}
|
||||
description={item.table_desc}
|
||||
isAdd={
|
||||
enterFrom === 'workflow'
|
||||
? Boolean(
|
||||
item.id &&
|
||||
workflowAddList?.length &&
|
||||
workflowAddList?.includes(item.id),
|
||||
)
|
||||
: Boolean(item.is_added_to_bot)
|
||||
}
|
||||
onClick={() => handleClickDatabase(item)}
|
||||
onAdd={() => handleAddDatabase(item)}
|
||||
onRemove={() => handleRemoveDatabase(item)}
|
||||
key={index}
|
||||
/>
|
||||
))}
|
||||
{loadingMore ? (
|
||||
<div className={styles['loading-more']}>
|
||||
<IconSpin spin style={{ marginRight: '4px' }} />
|
||||
<div>{I18n.t('Loading')}</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
|
||||
const renderEmpty = () => (
|
||||
<div className="overflow-y-auto relative w-full h-full flex justify-center items-center">
|
||||
<Empty
|
||||
image={<IllustrationNoContent style={{ width: 150, height: 150 }} />}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
const handleClose = () => {
|
||||
onClose();
|
||||
};
|
||||
|
||||
const handleJumpDatabase = (res: SingleDatabaseResponse) => {
|
||||
handleClose();
|
||||
const { id, draft_id } = res.database_info ?? {};
|
||||
if (id && draft_id) {
|
||||
onCreateDatabase?.(id, draft_id);
|
||||
}
|
||||
};
|
||||
|
||||
const fetchDefaultIcon = async () => {
|
||||
const res = await KnowledgeApi.GetIcon({
|
||||
format_type: FormatType.Database,
|
||||
});
|
||||
if (res.icon?.uri) {
|
||||
setInitValue({
|
||||
...initValue,
|
||||
icon_uri: res.icon?.uri,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (visible) {
|
||||
reload();
|
||||
fetchDefaultIcon();
|
||||
}
|
||||
}, [visible]);
|
||||
|
||||
const {
|
||||
modal: createDatabaseModal,
|
||||
open: openCreateDatabaseModal,
|
||||
close: closeCreateDatabaseModal,
|
||||
} = useLibraryCreateDatabaseModal({
|
||||
projectID,
|
||||
enterFrom: 'library',
|
||||
onFinish: (databaseID, draftId) => {
|
||||
closeCreateDatabaseModal();
|
||||
onCreateDatabase?.(databaseID, draftId);
|
||||
},
|
||||
});
|
||||
|
||||
const renderContent = () => (
|
||||
<>
|
||||
{tips}
|
||||
<Spin
|
||||
spinning={loading}
|
||||
wrapperClassName={classNames(['overflow-hidden', styles.list])}
|
||||
>
|
||||
{data?.list.length !== 0 ? renderList() : renderEmpty()}
|
||||
</Spin>
|
||||
{showBottomShadow ? (
|
||||
<div
|
||||
className={classNames(
|
||||
styles['bottom-shadow'],
|
||||
'w-full h-[80px] absolute left-0 bottom-0',
|
||||
'pointer-events-none',
|
||||
)}
|
||||
></div>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
|
||||
const renderDatabase = () => (
|
||||
<React.Fragment>
|
||||
{createDatabaseModal}
|
||||
<UICompositionModal
|
||||
closable
|
||||
visible={visible}
|
||||
onCancel={handleClose}
|
||||
header={I18n.t('db2_025')}
|
||||
filter={renderFilter()}
|
||||
sider={
|
||||
<UICompositionModalSider className="!pt-[16px]">
|
||||
<UICompositionModalSider.Header className="mb-[16px] gap-[12px]">
|
||||
{renderInput()}
|
||||
<Dropdown
|
||||
trigger="custom"
|
||||
visible={dropdownVisible}
|
||||
render={
|
||||
<Dropdown.Menu className="w-[196px]">
|
||||
<Dropdown.Item
|
||||
className="!pl-[8px]"
|
||||
onClick={() => {
|
||||
setDropdownVisible(false);
|
||||
openCreateDatabaseModal();
|
||||
}}
|
||||
>
|
||||
{I18n.t('db2_015')}
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item
|
||||
className="!pl-[8px] [&_.coz-item-text]:w-full"
|
||||
onClick={() => {
|
||||
setDropdownVisible(false);
|
||||
openCreateTableModal(ModalMode.TEMPLATE);
|
||||
}}
|
||||
>
|
||||
<div className="flex justify-between">
|
||||
<span>{I18n.t('db2_016')}</span>
|
||||
<Popover
|
||||
style={{
|
||||
maxWidth: '460px',
|
||||
backgroundColor: 'var(--Bg-COZ-bg-max, #363D4D)',
|
||||
boxShadow:
|
||||
'0 4px 12px 0 rgba(0, 0, 0, 8%), 0 8px 24px 0 rgba(0, 0, 0, 4%)',
|
||||
}}
|
||||
trigger="hover"
|
||||
content={renderTemplateTips()}
|
||||
zIndex={9999}
|
||||
showArrow
|
||||
>
|
||||
<Tag
|
||||
color="primary"
|
||||
size="small"
|
||||
className="ml-[8px]"
|
||||
>
|
||||
{I18n.t('db2_017')}
|
||||
</Tag>
|
||||
</Popover>
|
||||
</div>
|
||||
</Dropdown.Item>
|
||||
</Dropdown.Menu>
|
||||
}
|
||||
onClickOutSide={() => {
|
||||
setDropdownVisible(false);
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
color="brand"
|
||||
iconPosition="right"
|
||||
icon={<IconCozArrowDown />}
|
||||
onClick={() => setDropdownVisible(true)}
|
||||
>
|
||||
{I18n.t('db_add_table_title')}
|
||||
</Button>
|
||||
</Dropdown>
|
||||
</UICompositionModalSider.Header>
|
||||
<UICompositionModalSider.Content className="flex flex-col gap-[4px]">
|
||||
<SiderCategory
|
||||
label={I18n.t('project_resource_modal_library_resources', {
|
||||
resource: I18n.t('resource_type_database'),
|
||||
})}
|
||||
onClick={() => {
|
||||
setCategory('library');
|
||||
}}
|
||||
selected={category === 'library'}
|
||||
/>
|
||||
{projectID ? (
|
||||
<SiderCategory
|
||||
label={I18n.t('project_resource_modal_project_resources', {
|
||||
resource: I18n.t('resource_type_database'),
|
||||
})}
|
||||
onClick={() => {
|
||||
setCategory('project');
|
||||
}}
|
||||
selected={category === 'project'}
|
||||
/>
|
||||
) : null}
|
||||
</UICompositionModalSider.Content>
|
||||
</UICompositionModalSider>
|
||||
}
|
||||
content={
|
||||
<UICompositionModalMain className="relative px-[12px] gap-[16px]">
|
||||
{renderContent()}
|
||||
</UICompositionModalMain>
|
||||
}
|
||||
></UICompositionModal>
|
||||
|
||||
<DatabaseCreateTableModal
|
||||
visible={createVisible}
|
||||
onClose={() => setCreateVisible(false)}
|
||||
onReturn={() => setCreateVisible(false)}
|
||||
onSubmit={handleJumpDatabase}
|
||||
showDatabaseBaseInfo
|
||||
onlyShowDatabaseInfoRWMode={false}
|
||||
initValue={initValue}
|
||||
extraParams={{
|
||||
botId,
|
||||
spaceId,
|
||||
creatorId: userInfo?.user_id_str,
|
||||
}}
|
||||
/>
|
||||
</React.Fragment>
|
||||
);
|
||||
|
||||
return { renderDatabase, renderContent, renderInput, renderFilter };
|
||||
};
|
||||
|
||||
export const SelectDatabaseModal: FC<SelectDatabaseModalProps> = props => {
|
||||
const { renderDatabase } = useSelectDatabaseModal(props);
|
||||
|
||||
return <>{renderDatabase()}</>;
|
||||
};
|
||||
@@ -0,0 +1,120 @@
|
||||
/*
|
||||
* 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 cn from 'classnames';
|
||||
import { useBoolean } from 'ahooks';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { type ButtonProps } from '@coze-arch/bot-semi/Button';
|
||||
import { Button, Typography } from '@coze-arch/bot-semi';
|
||||
|
||||
import styles from './index.module.less';
|
||||
|
||||
interface IProps {
|
||||
icon: string | undefined;
|
||||
title: string | undefined;
|
||||
description: string | undefined;
|
||||
isAdd: boolean;
|
||||
onClick: () => void;
|
||||
onAdd: () => void;
|
||||
onRemove?: () => void;
|
||||
}
|
||||
|
||||
const AddedButton = (buttonProps: ButtonProps) => {
|
||||
const [isMouseIn, { setFalse, setTrue }] = useBoolean(false);
|
||||
|
||||
const onMouseEnter = () => {
|
||||
setTrue();
|
||||
};
|
||||
const onMouseLeave = () => {
|
||||
setFalse();
|
||||
};
|
||||
|
||||
return (
|
||||
<Button
|
||||
onMouseEnter={onMouseEnter}
|
||||
onMouseLeave={onMouseLeave}
|
||||
{...buttonProps}
|
||||
className={cn(styles['database-added'], {
|
||||
[styles['added-mousein']]: isMouseIn,
|
||||
})}
|
||||
>
|
||||
{isMouseIn ? I18n.t('Remove') : I18n.t('Added')}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
export const DatabaseListItem: FC<IProps> = props => {
|
||||
const { icon, title, description, isAdd, onClick, onAdd, onRemove } = props;
|
||||
|
||||
const operateDatabase = () => {
|
||||
if (isAdd) {
|
||||
onRemove?.();
|
||||
return;
|
||||
} else {
|
||||
onAdd?.();
|
||||
return;
|
||||
}
|
||||
};
|
||||
return (
|
||||
<div
|
||||
onClick={onClick}
|
||||
className="flex flex-row items-center p-[16px] border-t-0 border-l-0 border-r-0 border-b-[1px] border-solid coz-stroke-primary last:border-b-0 cursor-pointer"
|
||||
>
|
||||
<img src={icon} className="w-[36px] h-[36px] rounded-[8px]" />
|
||||
<div className="flex flex-col ml-[12px] min-w-0 flex-grow">
|
||||
<p className="text-[14px] font-medium leading-[20px] coz-fg-primary mb-[4px]">
|
||||
{title}
|
||||
</p>
|
||||
<Typography.Text
|
||||
ellipsis={{
|
||||
showTooltip: {
|
||||
opts: { content: description },
|
||||
},
|
||||
}}
|
||||
className="text-[12px] leading-[16px] coz-fg-secondary truncate !max-w-[680px]"
|
||||
>
|
||||
{description}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
<div className="ml-[16px]">
|
||||
{isAdd ? (
|
||||
<AddedButton
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
operateDatabase();
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<Button
|
||||
data-testid="bot.database.add.modal.add.button"
|
||||
className={cn(
|
||||
'w-[53px] flex justify-center items-center',
|
||||
styles['database-add'],
|
||||
)}
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
operateDatabase();
|
||||
}}
|
||||
>
|
||||
{I18n.t('Add_2')}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* 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 { IconCozKnowledgeFill } from '@coze-arch/coze-design/icons';
|
||||
|
||||
interface SiderCategoryProps {
|
||||
label: string;
|
||||
selected: boolean;
|
||||
|
||||
onClick?: React.MouseEventHandler<HTMLDivElement>;
|
||||
}
|
||||
|
||||
const SiderCategory = ({ label, onClick, selected }: SiderCategoryProps) => (
|
||||
<div
|
||||
onClick={onClick}
|
||||
className={classNames([
|
||||
'flex items-center gap-[8px] px-[12px]',
|
||||
'px-[12px] py-[6px] rounded-[8px]',
|
||||
'cursor-pointer',
|
||||
'hover:text-[var(--light-usage-text-color-text-0,#1c1f23)]',
|
||||
'hover:bg-[var(--light-usage-fill-color-fill-0,rgba(46,50,56,5%))]',
|
||||
selected &&
|
||||
'text-[var(--light-usage-text-color-text-0,#1c1d23)] bg-[var(--light-usage-fill-color-fill-0,rgba(46,47,56,5%))]',
|
||||
])}
|
||||
>
|
||||
<IconCozKnowledgeFill />
|
||||
{label}
|
||||
</div>
|
||||
);
|
||||
|
||||
export default SiderCategory;
|
||||
Reference in New Issue
Block a user