feat: manually mirror opencoze's code from bytedance
Change-Id: I09a73aadda978ad9511264a756b2ce51f5761adf
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
<svg width="64" height="64" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<rect width="64" height="64" fill="url(#pattern0_6088_81588)" style=""/>
|
||||
<defs>
|
||||
<pattern id="pattern0_6088_81588" patternContentUnits="objectBoundingBox" width="1" height="1">
|
||||
<use xlink:href="#image0_6088_81588" transform="scale(0.0104167)"/>
|
||||
</pattern>
|
||||
<image id="image0_6088_81588" width="96" height="96" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAYAAADimHc4AAAACXBIWXMAACE4AAAhOAFFljFgAAABaWlDQ1BEaXNwbGF5IFAzAAB4nHWQvUvDUBTFT6tS0DqIDh0cMolD1NIKdnFoKxRFMFQFq1OafgltfCQpUnETVyn4H1jBWXCwiFRwcXAQRAcR3Zw6KbhoeN6XVNoi3sfl/Ticc7lcwBtQGSv2AijplpFMxKS11Lrke4OHnlOqZrKooiwK/v276/PR9d5PiFlNu3YQ2U9cl84ul3aeAlN//V3Vn8maGv3f1EGNGRbgkYmVbYsJ3iUeMWgp4qrgvMvHgtMunzuelWSc+JZY0gpqhrhJLKc79HwHl4plrbWD2N6f1VeXxRzqUcxhEyYYilBRgQQF4X/8044/ji1yV2BQLo8CLMpESRETssTz0KFhEjJxCEHqkLhz634PrfvJbW3vFZhtcM4v2tpCAzidoZPV29p4BBgaAG7qTDVUR+qh9uZywPsJMJgChu8os2HmwiF3e38M6Hvh/GMM8B0CdpXzryPO7RqFn4Er/QcXKWq8MSlPPgAAA8VJREFUeAHtnD9uE0EUxp/BSlwg4SIFBVKClIICidwg5gTACWJOADcgnAA4AeYEwAXAdJRBoqCgMBIFBYURFEmUKOy3u09eb2wwlv2+AX8/abL5v+P5jd/MvBm78eP15pkJGk18uGiHWTkyEceprWel5QKObK0xNBHH8Vk7F3DBBBUJICMBZCSAjASQkQAyEkBGAshIABkJICMBZCSAjASQkQAyEkBGAshIABkJICMBZCSAjASQkQAyEkBGAsg0LWWabbNLO2W5adbaKgrwq3MyNDscjK4/32floCgn6R46S09Au5OV3fLamf3vXNYkIGHYN/v6vPg8IRo4nLtm3/lHE690s7L3d40+DxDw5Wkmo2dMcDTx2C4nIAANfv3Z+ZCybBCmBo9oIlwAbxBGyEDD77yJb3yAe+L+KKgLCY4APGA0PMIOG9QBdSFJ4Ai48WL6gMkAdUGdCMQLQI9b9kA7D6gT4RlJELBnyUKoW7yAFHu/QwiL8QISXpUyBuJ4AYmtRMfAajmYeAFIB6QKoW4EAT1KT/sjea6oZ9Fw1gEf7qYVilAX1IkARwAG4oNbaYQj1AF1IU0OeOsAPOCP3azcKxJj0eCeaHjUwRv/6n2LJl7A9hOzjTujrxF3310rRESEJcR63Av3rI5FWJ9s7Vs08enoTvneIIP9Ih1cB4uhPF2xu7iFERp6+La89s//HD0fHQP0GxYBbz+gU3lzFoQB9MZps6JJW5L4nl+reBjDddYtybzXPxxfna+UAMe3CyOmgRCHBkevn5QWCRaQxp6w7/9uPzb79jIrr4qeu6jBGY2OcQdhDVfiBkydeAEIB9MaAN/P94a7xdcuwcNJ9eRDPazgb70gZOUnKDYLsbPuuBGmovEC0JCzZkQ9/ldnTcuEsDiMn4ZOmvmkwkrkgnzATQ3SURVOKgKrz5SeCWj8Tw+MAS8VgYVYfTUaDe6NdIQ3/sqlIjwfE5mYw0wHU12/r3cApSJsNF/fuD2aSi4Cb3SkI3CtTzeViuhP/l2X4KmIahqivpbwtQFKvn74XPx//O9pCzqlIkqUiiCjVEQgy05F+Is4lIqYglIRYygVUUWpCCJKRRBZqVSEk2IqggB3GuqpCD+bH3E8HDMdNDx6fQIn9NJ5lSRgpSIIpPMqyd8xTyrCT0XMkoog8m8I+I/hv0xV5EgAGQkgIwFkJICMBJCRADISQEYCyEgAGQkgIwFkJICMBJCRADISQEYCyEgAGQkgIwFkJICMBJCRADISQCY/G3pq6/lBIREH2hyUAlp5EfH8AnA31N4fXmhDAAAAAElFTkSuQmCC"/>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.3 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
@@ -0,0 +1,16 @@
|
||||
.database-model-content {
|
||||
:global {
|
||||
.semi-form-field {
|
||||
padding-top: 0;
|
||||
padding-bottom: 16px;
|
||||
}
|
||||
|
||||
.semi-input-wrapper {
|
||||
@apply coz-bg-plus;
|
||||
}
|
||||
|
||||
.semi-input-textarea-wrapper {
|
||||
@apply coz-bg-plus;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,290 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/* eslint-disable @coze-arch/max-line-per-function */
|
||||
import { useState, type FC, useRef, useEffect, useCallback } from 'react';
|
||||
|
||||
import { CozeFormTextArea, CozeInputWithCountField } from '@coze-data/utils';
|
||||
import {
|
||||
PictureUpload,
|
||||
type RenderAutoGenerateParams,
|
||||
} from '@coze-common/biz-components/picture-upload';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { Form, type FormApi, Modal } from '@coze-arch/coze-design';
|
||||
import { FormatType } from '@coze-arch/bot-api/memory';
|
||||
import { FileBizType, IconType } from '@coze-arch/bot-api/developer_api';
|
||||
import { KnowledgeApi } from '@coze-arch/bot-api';
|
||||
|
||||
import styles from './index.module.less';
|
||||
|
||||
export enum ModalMode {
|
||||
CREATE = 'create',
|
||||
EDIT = 'edit',
|
||||
}
|
||||
|
||||
export interface UseDatabaseBaseInfoModalProps {
|
||||
onSubmit: (formData: FormData) => void;
|
||||
onClose?: () => void;
|
||||
initValues: FormData | undefined;
|
||||
mode: ModalMode;
|
||||
renderAutoGenerate?: (params: RenderAutoGenerateParams) => React.ReactNode;
|
||||
}
|
||||
|
||||
export interface DatabaseBaseInfoModalProps
|
||||
extends UseDatabaseBaseInfoModalProps {
|
||||
visible: boolean;
|
||||
}
|
||||
|
||||
export const useDatabaseInfoModal = ({
|
||||
onSubmit,
|
||||
onClose,
|
||||
initValues,
|
||||
mode = ModalMode.CREATE,
|
||||
renderAutoGenerate,
|
||||
}: UseDatabaseBaseInfoModalProps) => {
|
||||
const [visible, setVisible] = useState(false);
|
||||
|
||||
const open = () => {
|
||||
setVisible(true);
|
||||
};
|
||||
|
||||
const close = () => {
|
||||
setVisible(false);
|
||||
onClose?.();
|
||||
};
|
||||
|
||||
return {
|
||||
visible,
|
||||
open,
|
||||
close,
|
||||
modal: (
|
||||
<DatabaseBaseInfoModal
|
||||
visible={visible}
|
||||
onClose={close}
|
||||
onSubmit={onSubmit}
|
||||
initValues={initValues}
|
||||
mode={mode}
|
||||
renderAutoGenerate={renderAutoGenerate}
|
||||
/>
|
||||
),
|
||||
};
|
||||
};
|
||||
|
||||
export interface FormData {
|
||||
name: string;
|
||||
description: string;
|
||||
icon_uri?: Array<{
|
||||
url: string;
|
||||
uri: string;
|
||||
uid?: string;
|
||||
isDefault?: boolean;
|
||||
}>;
|
||||
}
|
||||
|
||||
export const DatabaseBaseInfoModal: FC<DatabaseBaseInfoModalProps> = ({
|
||||
visible,
|
||||
initValues,
|
||||
onClose,
|
||||
onSubmit,
|
||||
mode,
|
||||
renderAutoGenerate,
|
||||
}) => {
|
||||
const formRef = useRef<FormApi<FormData> | null>(null);
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!formRef.current) {
|
||||
return;
|
||||
}
|
||||
const formData = await formRef.current.validate();
|
||||
|
||||
onSubmit({
|
||||
...formData,
|
||||
icon_uri: [
|
||||
{
|
||||
url: formData?.icon_uri?.[0]?.url ?? '',
|
||||
uri: formData?.icon_uri?.[0]?.uid ?? '',
|
||||
},
|
||||
],
|
||||
});
|
||||
};
|
||||
|
||||
const [coverIcon, setCoverIcon] = useState<{
|
||||
uri: string;
|
||||
url: string;
|
||||
}>({
|
||||
uri: initValues?.icon_uri?.[0]?.uri ?? '',
|
||||
url: initValues?.icon_uri?.[0]?.url ?? '',
|
||||
});
|
||||
|
||||
const [iconInfoGenerate, setIconInfoGenerate] = useState<{
|
||||
name: string;
|
||||
desc: string;
|
||||
}>({
|
||||
name: initValues?.name ?? '',
|
||||
desc: initValues?.description ?? '',
|
||||
});
|
||||
|
||||
const setDefaultIcon = async () => {
|
||||
const { icon } = await KnowledgeApi.GetIcon({
|
||||
format_type: FormatType.Database,
|
||||
});
|
||||
setCoverIcon({
|
||||
uri: icon?.uri ?? '',
|
||||
url: icon?.url ?? '',
|
||||
});
|
||||
formRef.current?.setValue('icon_uri', [
|
||||
{
|
||||
url: icon?.url ?? '',
|
||||
uri: icon?.uri ?? '',
|
||||
uid: icon?.uri ?? '',
|
||||
isDefault: true,
|
||||
},
|
||||
]);
|
||||
};
|
||||
const initForm = useCallback(
|
||||
({ name, description, icon_uri }: FormData) => {
|
||||
if (!formRef.current) {
|
||||
return;
|
||||
}
|
||||
formRef.current.setValue('name', name);
|
||||
formRef.current.setValue('description', description);
|
||||
setIconInfoGenerate({
|
||||
name: name ?? '',
|
||||
desc: description ?? '',
|
||||
});
|
||||
if (!icon_uri || !icon_uri[0].url) {
|
||||
setDefaultIcon();
|
||||
return;
|
||||
}
|
||||
formRef.current.setValue('icon_uri', [
|
||||
{
|
||||
url: icon_uri[0].url,
|
||||
uri: icon_uri[0].uri,
|
||||
uid: icon_uri[0].uri,
|
||||
isDefault: true,
|
||||
},
|
||||
]);
|
||||
},
|
||||
[formRef],
|
||||
);
|
||||
useEffect(() => {
|
||||
if (!visible) {
|
||||
return;
|
||||
}
|
||||
if (!initValues) {
|
||||
return;
|
||||
}
|
||||
initForm(initValues);
|
||||
}, [visible, initValues, initForm]);
|
||||
|
||||
const handleClose = () => {
|
||||
if (formRef.current) {
|
||||
formRef.current?.reset(['name', 'description', 'icon_uri']);
|
||||
}
|
||||
onClose?.();
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={
|
||||
mode === ModalMode.CREATE
|
||||
? I18n.t('db_add_table_title')
|
||||
: I18n.t('db_edit_title')
|
||||
}
|
||||
closable
|
||||
visible={visible}
|
||||
onCancel={handleClose}
|
||||
className="w-[480px]"
|
||||
okText={I18n.t('db2_004')}
|
||||
okButtonProps={{
|
||||
// @ts-expect-error -- for e2e
|
||||
'data-testid': 'database.info_modal.button.confirm',
|
||||
}}
|
||||
cancelText={I18n.t('db_del_field_confirm_no')}
|
||||
onOk={handleSubmit}
|
||||
maskClosable={false}
|
||||
>
|
||||
<Form<FormData>
|
||||
className={styles['database-model-content']}
|
||||
getFormApi={formApi => {
|
||||
formRef.current = formApi;
|
||||
}}
|
||||
initValues={initValues}
|
||||
>
|
||||
{({ formState }) => (
|
||||
<>
|
||||
<CozeInputWithCountField
|
||||
data-testid="database.info_modal.input.name"
|
||||
field="name"
|
||||
label={I18n.t('db_add_table_name')}
|
||||
placeholder={I18n.t('db_add_table_name_tips')}
|
||||
required
|
||||
maxLength={50}
|
||||
disabled={mode === ModalMode.EDIT}
|
||||
onChange={(value: string) => {
|
||||
setIconInfoGenerate(prev => ({
|
||||
...prev,
|
||||
name: value?.trim() || '',
|
||||
}));
|
||||
}}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: I18n.t('db2_005'),
|
||||
},
|
||||
{
|
||||
pattern: /^[a-z][a-z0-9_]{0,63}$/,
|
||||
message: I18n.t('db_new_0004'),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<CozeFormTextArea
|
||||
data-testid="database.info_modal.input.description"
|
||||
field="description"
|
||||
label={I18n.t('db_add_table_desc')}
|
||||
placeholder={I18n.t('db_add_table_desc_tips')}
|
||||
maxCount={100}
|
||||
maxlength={100}
|
||||
onChange={(value: string) => {
|
||||
setIconInfoGenerate(prev => ({
|
||||
...prev,
|
||||
desc: value?.trim() || '',
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
<PictureUpload
|
||||
label={I18n.t('datasets_model_create_avatar')}
|
||||
field="icon_uri"
|
||||
fileBizType={FileBizType.BIZ_DATASET_ICON}
|
||||
iconType={IconType.Dataset}
|
||||
uploadClassName="w-auto"
|
||||
generateInfo={iconInfoGenerate}
|
||||
withAutoGenerate={!!renderAutoGenerate}
|
||||
renderAutoGenerate={renderAutoGenerate}
|
||||
initValue={[
|
||||
{
|
||||
url: coverIcon?.url,
|
||||
uid: coverIcon?.uri,
|
||||
isDefault: true,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,6 @@
|
||||
.table-header-label-tooltip-icon {
|
||||
cursor: pointer;
|
||||
margin-left: 8px;
|
||||
color: rgba(198, 202, 205, 100%);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,326 @@
|
||||
/*
|
||||
* 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, useRef, useMemo, useEffect } from 'react';
|
||||
import type { ReactNode, RefObject } from 'react';
|
||||
|
||||
import {
|
||||
type DatabaseInfo,
|
||||
type TableMemoryItem,
|
||||
} from '@coze-studio/bot-detail-store';
|
||||
import { BotE2e } from '@coze-data/e2e';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { IconCozCross } from '@coze-arch/coze-design/icons';
|
||||
import { IconButton, Modal, Button } from '@coze-arch/coze-design';
|
||||
import {
|
||||
type BotTableRWMode,
|
||||
type AlterBotTableResponse,
|
||||
type InsertBotTableResponse,
|
||||
} from '@coze-arch/bot-api/memory';
|
||||
|
||||
import { DismissibleBanner } from '../dismissible-banner';
|
||||
import {
|
||||
DatabaseTableStructure,
|
||||
type DatabaseTableStructureRef,
|
||||
} from '../database-table-structure';
|
||||
import {
|
||||
CreateType,
|
||||
type TableFieldsInfo,
|
||||
type OnSave,
|
||||
} from '../../types/database-field';
|
||||
|
||||
// import { useExpertModeConfig } from '../../hooks/use-expert-mode-config';
|
||||
|
||||
const MAX_COLUMNS = 20;
|
||||
|
||||
interface CreateTableModalExtraParams {
|
||||
botId?: string;
|
||||
spaceId?: string;
|
||||
creatorId?: string;
|
||||
}
|
||||
|
||||
// RenderGenerate属性类型定义
|
||||
export interface RenderGenerateProps {
|
||||
tableStructureRef: RefObject<DatabaseTableStructureRef>;
|
||||
onGenerateChange: (tableMemoryList: TableMemoryItem[]) => void;
|
||||
onGenerating: (loading: boolean) => void;
|
||||
botId: string;
|
||||
}
|
||||
|
||||
export interface RenderModeSelectProps {
|
||||
dataTestId: string;
|
||||
field: string;
|
||||
label: string;
|
||||
type: 'select';
|
||||
options: BotTableRWMode[];
|
||||
}
|
||||
|
||||
export interface DatabaseCreateTableModalProps {
|
||||
visible: boolean;
|
||||
onClose: () => void;
|
||||
onReturn?: () => void;
|
||||
onSubmit?: (response: InsertBotTableResponse | AlterBotTableResponse) => void;
|
||||
initValue: DatabaseInfo;
|
||||
showDatabaseBaseInfo: boolean;
|
||||
extraParams?: CreateTableModalExtraParams;
|
||||
onlyShowDatabaseInfoRWMode: boolean;
|
||||
projectID?: string;
|
||||
renderGenerate?: (props: RenderGenerateProps) => ReactNode;
|
||||
renderModeSelect?: (props: RenderModeSelectProps) => ReactNode;
|
||||
}
|
||||
|
||||
interface UseDatabaseCreateTableModalProps {
|
||||
onClose?: () => void;
|
||||
onReturn?: () => void;
|
||||
initValue: DatabaseInfo;
|
||||
onSubmit?: (response: InsertBotTableResponse | AlterBotTableResponse) => void;
|
||||
showDatabaseBaseInfo: boolean;
|
||||
extraParams?: CreateTableModalExtraParams;
|
||||
onlyShowDatabaseInfoRWMode: boolean;
|
||||
projectID?: string;
|
||||
renderGenerate?: (props: RenderGenerateProps) => ReactNode;
|
||||
renderModeSelect?: (props: RenderModeSelectProps) => ReactNode;
|
||||
}
|
||||
|
||||
export const useDatabaseCreateTableModal = ({
|
||||
onReturn,
|
||||
onSubmit,
|
||||
initValue,
|
||||
showDatabaseBaseInfo,
|
||||
extraParams,
|
||||
onlyShowDatabaseInfoRWMode,
|
||||
projectID,
|
||||
renderGenerate,
|
||||
renderModeSelect,
|
||||
}: UseDatabaseCreateTableModalProps) => {
|
||||
const [visible, setVisible] = useState(false);
|
||||
|
||||
const open = () => {
|
||||
setVisible(true);
|
||||
};
|
||||
|
||||
const close = () => {
|
||||
setVisible(false);
|
||||
// onClose?.();
|
||||
};
|
||||
|
||||
return {
|
||||
visible,
|
||||
open,
|
||||
close,
|
||||
modal: (
|
||||
<DatabaseCreateTableModal
|
||||
visible={visible}
|
||||
onClose={close}
|
||||
onReturn={onReturn}
|
||||
onSubmit={onSubmit}
|
||||
initValue={initValue}
|
||||
showDatabaseBaseInfo={showDatabaseBaseInfo}
|
||||
extraParams={extraParams}
|
||||
onlyShowDatabaseInfoRWMode={onlyShowDatabaseInfoRWMode}
|
||||
projectID={projectID}
|
||||
renderGenerate={renderGenerate}
|
||||
renderModeSelect={renderModeSelect}
|
||||
/>
|
||||
),
|
||||
};
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @coze-arch/max-line-per-function
|
||||
export function DatabaseCreateTableModal({
|
||||
visible,
|
||||
onClose,
|
||||
onReturn,
|
||||
onSubmit,
|
||||
initValue,
|
||||
showDatabaseBaseInfo,
|
||||
onlyShowDatabaseInfoRWMode,
|
||||
extraParams: { botId = '', spaceId = '', creatorId = '' } = {},
|
||||
projectID,
|
||||
renderGenerate,
|
||||
renderModeSelect,
|
||||
}: DatabaseCreateTableModalProps) {
|
||||
// AI generate loading
|
||||
const [generateTableLoading, setGenerateTableLoading] = useState(false);
|
||||
// save button loading
|
||||
const [saveBtnLoading, setSaveBtnLoading] = useState<boolean>(false);
|
||||
// save button disabled
|
||||
const [saveBtnDisabled, setSaveBtnDisabled] = useState<boolean>(false);
|
||||
// database structure
|
||||
const [databaseInitValue, setDatabaseInitValue] =
|
||||
useState<DatabaseInfo>(initValue);
|
||||
|
||||
// export mode's some config(actually nobody knows why this is here...)
|
||||
// const expertModeConfig = useExpertModeConfig({ botId });
|
||||
|
||||
// DataBase Table ref
|
||||
const tableStructureRef = useRef<DatabaseTableStructureRef>(null);
|
||||
|
||||
/**
|
||||
* modal mode
|
||||
* @has tableId: Edit Mode;
|
||||
* @no tableId: Create Mode
|
||||
*/
|
||||
const isModify = useMemo(() => Boolean(initValue.tableId), [initValue]);
|
||||
|
||||
const handleValidateTable = (list: TableFieldsInfo, isEmptyList: boolean) => {
|
||||
if (isEmptyList) {
|
||||
setSaveBtnDisabled(true);
|
||||
return;
|
||||
}
|
||||
// 系统字段不计入字段数量限制
|
||||
if (list.filter(i => !i.isSystemField).length > MAX_COLUMNS) {
|
||||
setSaveBtnDisabled(true);
|
||||
return;
|
||||
}
|
||||
const validateRes = list.every(ele => {
|
||||
if (!ele?.errorMapper) {
|
||||
return true;
|
||||
} else {
|
||||
if (
|
||||
ele.errorMapper.name?.length > 0 ||
|
||||
ele.errorMapper.type?.length > 0
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
});
|
||||
setSaveBtnDisabled(!validateRes);
|
||||
};
|
||||
|
||||
const onSave: OnSave = async ({ response }) => {
|
||||
/**
|
||||
* 在 DatabaseTableStructure 这个组件中,提交已经区分了 edit 和 create 两种状态,
|
||||
* 并且存在一个onSave的回调,因此提交之后的逻辑全部收敛在这里
|
||||
*/
|
||||
await onSubmit?.(response);
|
||||
};
|
||||
|
||||
const onCreateSubmit = async () => {
|
||||
if (tableStructureRef.current) {
|
||||
try {
|
||||
setSaveBtnLoading(true);
|
||||
await tableStructureRef.current.submit();
|
||||
} finally {
|
||||
setSaveBtnLoading(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setDatabaseInitValue(initValue);
|
||||
}, [initValue]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
closable
|
||||
maskClosable={false}
|
||||
visible={visible}
|
||||
onCancel={undefined}
|
||||
onOk={onCreateSubmit}
|
||||
size="xxl"
|
||||
header={
|
||||
<>
|
||||
<div className="flex flex-row items-center">
|
||||
<div className="flex-1 text-[20px] font-medium coz-fg-plus">
|
||||
{isModify
|
||||
? I18n.t('db_edit_title')
|
||||
: I18n.t('db_add_table_title')}
|
||||
</div>
|
||||
{!isModify
|
||||
? renderGenerate?.({
|
||||
tableStructureRef,
|
||||
onGenerateChange: tableMemoryList => {
|
||||
setDatabaseInitValue({
|
||||
...databaseInitValue,
|
||||
tableMemoryList,
|
||||
});
|
||||
},
|
||||
onGenerating: setGenerateTableLoading,
|
||||
botId,
|
||||
})
|
||||
: null}
|
||||
<div>
|
||||
<IconButton
|
||||
color="secondary"
|
||||
icon={<IconCozCross className="text-[20px] coz-fg-secondary" />}
|
||||
onClick={onClose}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/* 编辑弹窗出现 Banner 提示 */}
|
||||
{isModify ? (
|
||||
<DismissibleBanner
|
||||
type="warning"
|
||||
persistentKey="_coze_database_edit_warning"
|
||||
className="mx-[-24px]"
|
||||
>
|
||||
{I18n.t('db_edit_tips1')}
|
||||
</DismissibleBanner>
|
||||
) : null}
|
||||
</>
|
||||
}
|
||||
footer={
|
||||
<div className="coz-modal-footer">
|
||||
<Button
|
||||
color="primary"
|
||||
onClick={() => {
|
||||
if (onReturn) {
|
||||
onReturn();
|
||||
return;
|
||||
}
|
||||
}}
|
||||
>
|
||||
{isModify ? I18n.t('db_del_field_confirm_no') : I18n.t('db2_003')}
|
||||
</Button>
|
||||
<Button
|
||||
data-testid={BotE2e.BotDatabaseAddModalSubmitBtn}
|
||||
loading={saveBtnLoading}
|
||||
onClick={onCreateSubmit}
|
||||
disabled={saveBtnDisabled}
|
||||
>
|
||||
{I18n.t('db_edit_save')}
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div>
|
||||
<DatabaseTableStructure
|
||||
data={databaseInitValue}
|
||||
ref={tableStructureRef}
|
||||
loading={generateTableLoading}
|
||||
loadingTips={I18n.t('bot_database_ai_waiting')}
|
||||
botId={botId}
|
||||
spaceId={spaceId}
|
||||
creatorId={creatorId}
|
||||
// maxColumnNum={expertModeConfig.maxColumnNum}
|
||||
maxColumnNum={MAX_COLUMNS}
|
||||
useComputingEnableGoToNextStep={handleValidateTable}
|
||||
createType={CreateType.custom}
|
||||
hiddenTableBorder
|
||||
readAndWriteModeOptions="expert"
|
||||
showDatabaseBaseInfo={showDatabaseBaseInfo}
|
||||
onlyShowDatabaseInfoRWMode={onlyShowDatabaseInfoRWMode}
|
||||
onSave={onSave}
|
||||
onCancel={onClose}
|
||||
projectID={projectID}
|
||||
renderModeSelect={renderModeSelect}
|
||||
/>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@@ -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 ReactNode } from 'react';
|
||||
|
||||
import { type FieldItemType } from '@coze-arch/bot-api/developer_api';
|
||||
import { IconCozInfoCircle } from '@coze-arch/coze-design/icons';
|
||||
import { Tag, Tooltip, Typography } from '@coze-arch/coze-design';
|
||||
|
||||
import { FIELD_TYPE_OPTIONS } from '../../constants/database-field';
|
||||
|
||||
export interface DatabaseFieldTitleProps {
|
||||
field?: string;
|
||||
textType?: 'primary' | 'secondary';
|
||||
type?: FieldItemType;
|
||||
tip?: ReactNode;
|
||||
required?: boolean;
|
||||
}
|
||||
|
||||
export function DatabaseFieldTitle({
|
||||
field,
|
||||
textType = 'secondary',
|
||||
type,
|
||||
tip,
|
||||
required,
|
||||
}: DatabaseFieldTitleProps) {
|
||||
return (
|
||||
<div className="flex flex-row items-center">
|
||||
<Typography.Text type={textType} weight={500} fontSize="12px" ellipsis>
|
||||
{field}
|
||||
</Typography.Text>
|
||||
{required ? (
|
||||
<span className="coz-fg-hglt-red text-[12px] leading-[16px]">*</span>
|
||||
) : null}
|
||||
{tip ? (
|
||||
<Tooltip content={tip} style={{ maxWidth: 'unset' }}>
|
||||
<IconCozInfoCircle className="w-[12px] h-[12px] ml-[3px] coz-fg-secondary" />
|
||||
</Tooltip>
|
||||
) : null}
|
||||
{typeof type === 'number' ? (
|
||||
<Tag color="primary" size="mini" className="ml-[4px]">
|
||||
{FIELD_TYPE_OPTIONS.find(i => i.value === type)?.label ?? type}
|
||||
</Tag>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
/* stylelint-disable declaration-no-important */
|
||||
.modal-key-tip {
|
||||
width: 200px !important;
|
||||
|
||||
ul {
|
||||
padding-left: 16px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { PopoverContent } from '@coze-studio/components';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
|
||||
import s from './index.module.less';
|
||||
|
||||
export const KeyTipsNode: React.FC = () => (
|
||||
<PopoverContent className={s['modal-key-tip']}>{`- ${I18n.t(
|
||||
'db_add_table_field_name_tips1',
|
||||
)}
|
||||
- ${I18n.t('db_add_table_field_name_tips2')}
|
||||
- ${I18n.t('db_add_table_field_name_tips3')}
|
||||
- ${I18n.t('db_add_table_field_name_tips4')}`}</PopoverContent>
|
||||
);
|
||||
@@ -0,0 +1,189 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/* eslint-disable complexity */
|
||||
import { type TableMemoryItem } from '@coze-studio/bot-detail-store';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
|
||||
import {
|
||||
type MapperItem,
|
||||
type TriggerType,
|
||||
VerifyType,
|
||||
} from '../../../types/database-field';
|
||||
|
||||
// 校验 Table Name 和 Field Name
|
||||
const namingRegexMapper = [
|
||||
{
|
||||
type: 1,
|
||||
regex: /[^a-z0-9_]/,
|
||||
errorMsg: I18n.t('db_add_table_field_name_tips2'),
|
||||
},
|
||||
{
|
||||
type: 2,
|
||||
regex: /^[^a-z]/,
|
||||
errorMsg: I18n.t('db_add_table_field_name_tips3'),
|
||||
},
|
||||
{
|
||||
type: 3,
|
||||
regex: /[\s\S]{64,}/,
|
||||
errorMsg: I18n.t('db_add_table_field_name_tips4'),
|
||||
},
|
||||
];
|
||||
export const validateNaming = (str: string, errList: string[] = []) => {
|
||||
let list = [...errList];
|
||||
namingRegexMapper.forEach(i => {
|
||||
list = list.filter(j => j !== i.errorMsg);
|
||||
if (i.regex.test(str || '')) {
|
||||
list.push(i.errorMsg);
|
||||
}
|
||||
});
|
||||
return list;
|
||||
};
|
||||
|
||||
// 校验 Table Fields
|
||||
export const thMapper: MapperItem[] = [
|
||||
{
|
||||
label: I18n.t('db_add_table_field_name'),
|
||||
key: 'name',
|
||||
validator: [
|
||||
{
|
||||
type: VerifyType.Naming,
|
||||
message: '',
|
||||
},
|
||||
{
|
||||
type: VerifyType.Required,
|
||||
message: I18n.t('db_table_save_exception_nofieldname'),
|
||||
},
|
||||
{
|
||||
type: VerifyType.Unique,
|
||||
message: I18n.t('db_table_save_exception_fieldname'),
|
||||
},
|
||||
],
|
||||
defaultValue: '',
|
||||
require: true,
|
||||
},
|
||||
{
|
||||
label: I18n.t('db_add_table_field_desc'),
|
||||
key: 'desc',
|
||||
require: false,
|
||||
validator: [],
|
||||
defaultValue: '',
|
||||
},
|
||||
{
|
||||
label: I18n.t('db_add_table_field_type'),
|
||||
key: 'type',
|
||||
require: true,
|
||||
validator: [
|
||||
{
|
||||
type: VerifyType.Required,
|
||||
message: I18n.t('db_table_save_exception_fieldtype'),
|
||||
},
|
||||
],
|
||||
defaultValue: '',
|
||||
},
|
||||
{
|
||||
label: I18n.t('db_add_table_field_necessary'),
|
||||
key: 'must_required',
|
||||
require: false,
|
||||
validator: [],
|
||||
defaultValue: true,
|
||||
},
|
||||
];
|
||||
export const validateFields = (
|
||||
list: TableMemoryItem[],
|
||||
trigger: TriggerType,
|
||||
) => {
|
||||
const resList = list.map(_listItem => {
|
||||
const listItem: TableMemoryItem = { ..._listItem };
|
||||
thMapper.forEach(thItem => {
|
||||
const thKey = thItem.key as keyof TableMemoryItem;
|
||||
|
||||
thItem.validator.forEach(verifyItem => {
|
||||
if (!listItem?.errorMapper) {
|
||||
listItem.errorMapper = {};
|
||||
}
|
||||
let errTarget = listItem?.errorMapper?.[thKey];
|
||||
const value = listItem[thKey];
|
||||
if (!errTarget) {
|
||||
listItem.errorMapper[thKey] = [];
|
||||
errTarget = [];
|
||||
}
|
||||
const msg = verifyItem.message;
|
||||
switch (verifyItem.type) {
|
||||
case VerifyType.Required: {
|
||||
// 报错出现时机:点击保存按钮时,出现提示。表中某一行填写了数据,但是未填写必填字段时,需要报错
|
||||
if (
|
||||
trigger === 'save' &&
|
||||
!value &&
|
||||
thMapper.find(
|
||||
i =>
|
||||
!!listItem[i.key as keyof TableMemoryItem] && !i.defaultValue,
|
||||
)
|
||||
) {
|
||||
listItem.errorMapper[thKey].push(msg);
|
||||
}
|
||||
// 报错消失时机:必填输入框输入了内容后,报错立刻消失
|
||||
if (trigger === 'change' && value) {
|
||||
listItem.errorMapper[thKey] = errTarget.filter(i => i !== msg);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case VerifyType.Unique: {
|
||||
// 报错出现时机:点击保存按钮时,出现提示。
|
||||
if (
|
||||
trigger === 'save' &&
|
||||
value &&
|
||||
list.filter(i => i[thKey] === listItem[thKey]).length !== 1
|
||||
) {
|
||||
listItem.errorMapper[thKey].push(msg);
|
||||
}
|
||||
// 报错消失时机:必填输入框输入了内容后,报错立刻消失
|
||||
if (
|
||||
trigger === 'change' &&
|
||||
value &&
|
||||
list.filter(i => i[thKey] === listItem[thKey]).length === 1
|
||||
) {
|
||||
listItem.errorMapper[thKey] = errTarget.filter(i => i !== msg);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case VerifyType.Naming: {
|
||||
// 报错出现时机:命名格式有问题,失去焦点时,立刻校验格式
|
||||
if (
|
||||
trigger === 'save' ||
|
||||
trigger === 'blur' ||
|
||||
(trigger === 'change' && errTarget.length)
|
||||
) {
|
||||
listItem.errorMapper[thKey] = validateNaming(
|
||||
value as string,
|
||||
errTarget,
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
listItem.errorMapper[thKey] = Array.from(
|
||||
new Set(listItem.errorMapper[thKey]),
|
||||
);
|
||||
});
|
||||
});
|
||||
return listItem;
|
||||
});
|
||||
|
||||
return resList;
|
||||
};
|
||||
@@ -0,0 +1,300 @@
|
||||
/* stylelint-disable declaration-no-important */
|
||||
/* stylelint-disable no-descending-specificity */
|
||||
/* stylelint-disable selector-class-pattern */
|
||||
/* stylelint-disable no-duplicate-selectors */
|
||||
/* stylelint-disable font-family-no-missing-generic-family-keyword */
|
||||
|
||||
.th-tip-name {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.th-tip-dot {
|
||||
position: relative;
|
||||
padding-left: 16px;
|
||||
}
|
||||
|
||||
.th-tip-dot::before {
|
||||
content: '';
|
||||
|
||||
position: absolute;
|
||||
top: 7px;
|
||||
left: 0;
|
||||
|
||||
width: 5px;
|
||||
height: 5px;
|
||||
|
||||
background-color: #000;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
// 新增样式 @zhangyuanzhou.zyz
|
||||
.form-input-error {
|
||||
:global {
|
||||
.semi-input-wrapper {
|
||||
border-color: #f93920
|
||||
}
|
||||
|
||||
.semi-input-wrapper:hover {
|
||||
border-color: var(--semi-color-border, rgba(29, 28, 35, 8%))
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
.table-structure-form {
|
||||
padding-bottom: 4px;
|
||||
|
||||
:global {
|
||||
|
||||
// 去除 error message icon
|
||||
.semi-form-field-validate-status-icon {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.semi-form-field-validate-status-icon+span {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.semi-col-12 {
|
||||
.semi-form-field[x-field-id="prompt_disabled"] {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
|
||||
.semi-form-field-main {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: auto !important;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.semi-switch-checked {
|
||||
background-color: rgba(var(--coze-brand-5),1) !important;
|
||||
}
|
||||
|
||||
.semi-switch-checked:hover {
|
||||
background-color: rgba(var(--coze-brand-6),1) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.max-row-banner {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.form-item-label-tooltip-icon {
|
||||
cursor: pointer;
|
||||
color: rgba(198, 202, 205, 100%);
|
||||
}
|
||||
|
||||
.table-setting-select {
|
||||
:global {
|
||||
.semi-select:hover {
|
||||
border: 1px solid var(--semi-color-border) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.table-structure-table {
|
||||
:global {
|
||||
|
||||
// table 圆角和边框
|
||||
.semi-table-container {
|
||||
padding: 0 8px;
|
||||
border: 1px solid var(--Light-usage-border---color-border-1, rgba(29, 28, 35, 8%));
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.semi-table-header {
|
||||
.semi-table-row-head {
|
||||
padding-right: 8px;
|
||||
padding-left: 8px;
|
||||
background: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
// hover时去除背景色
|
||||
.semi-table-body {
|
||||
.semi-table-row {
|
||||
background: none !important;
|
||||
}
|
||||
|
||||
.semi-table-row:hover {
|
||||
>.semi-table-row-cell {
|
||||
background: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
//
|
||||
.semi-table-row {
|
||||
cursor: default !important;
|
||||
|
||||
>.semi-table-row-cell {
|
||||
padding: 12px 8px !important;
|
||||
padding-left: 0;
|
||||
border-bottom: 1px solid transparent !important;
|
||||
}
|
||||
}
|
||||
|
||||
.semi-table-body {
|
||||
overflow: visible;
|
||||
max-height: none !important;
|
||||
padding: 12px 0;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
|
||||
// 移除 UITable 中的 before元素影响
|
||||
.semi-table-row-cell::before {
|
||||
display: none;
|
||||
}
|
||||
|
||||
// 覆盖表结构编辑弹窗的只读字段样式,(在视觉上)由 disabled input 变为 plain text
|
||||
.semi-input-wrapper-disabled {
|
||||
background: none;
|
||||
border: none;
|
||||
|
||||
.semi-input-disabled {
|
||||
padding: 0;
|
||||
font-weight: 500;
|
||||
color: var(--coz-fg-primary);
|
||||
|
||||
-webkit-text-fill-color: var(--coz-fg-primary);
|
||||
}
|
||||
|
||||
.semi-input-suffix {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.semi-select-disabled {
|
||||
cursor: default;
|
||||
opacity: 1;
|
||||
background: none;
|
||||
border: none;
|
||||
|
||||
&:hover {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
.semi-select-selection {
|
||||
cursor: default;
|
||||
margin: 0;
|
||||
|
||||
.semi-select-selection-text {
|
||||
font-weight: 500;
|
||||
color: var(--coz-fg-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.semi-select-arrow {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
.table-structure-table-wrapper {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.table-empty-tips {
|
||||
padding: 8px;
|
||||
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
line-height: 16px;
|
||||
color: @error-red;
|
||||
}
|
||||
|
||||
|
||||
.spin {
|
||||
:global {
|
||||
.semi-spin-wrapper div {
|
||||
font-size: 16px;
|
||||
color: rgba(29, 28, 35, 35%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.table-setting-option {
|
||||
:global {
|
||||
.semi-select-option-selected {
|
||||
.semi-select-option-icon {
|
||||
color: #4D53E8
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
.table-name-form-field {
|
||||
padding-bottom: 24px !important;
|
||||
|
||||
// semi 原有的 disable 样式不满足需求,需要更明显的文字颜色
|
||||
:global {
|
||||
.semi-input-wrapper-disabled {
|
||||
-webkit-text-fill-color: rgba(29, 28, 35, 100%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.table-desc-form-field {
|
||||
padding-top: 0 !important;
|
||||
}
|
||||
|
||||
.prompt_disabled_popover {
|
||||
:global {
|
||||
.semi-popover-icon-arrow {
|
||||
right: 8px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.read_mode_popover {
|
||||
:global {
|
||||
.semi-popover-icon-arrow {
|
||||
left: 8px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.hidden-form-border {
|
||||
:global {
|
||||
.semi-table-container {
|
||||
padding: 0;
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
// .semi-table-row-head {
|
||||
// padding-right: 0 !important;
|
||||
// padding-left: 0 !important;
|
||||
// }
|
||||
|
||||
// .semi-input-wrapper {
|
||||
// @apply coz-bg-plus;
|
||||
// }
|
||||
|
||||
// .semi-input-textarea-wrapper {
|
||||
// @apply coz-bg-plus;
|
||||
// }
|
||||
|
||||
// .semi-select {
|
||||
// @apply coz-bg-plus;
|
||||
// }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,770 @@
|
||||
/*
|
||||
* 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 {
|
||||
useRef,
|
||||
forwardRef,
|
||||
useImperativeHandle,
|
||||
useState,
|
||||
type MutableRefObject,
|
||||
useEffect,
|
||||
type ReactNode,
|
||||
} from 'react';
|
||||
|
||||
import { nanoid } from 'nanoid';
|
||||
import { noop } from 'lodash-es';
|
||||
import classNames from 'classnames';
|
||||
import { useBoolean } from 'ahooks';
|
||||
import { type DatabaseInfo } from '@coze-studio/bot-detail-store';
|
||||
import { DataNamespace, dataReporter } from '@coze-data/reporter';
|
||||
import { BotE2e } from '@coze-data/e2e';
|
||||
import { REPORT_EVENTS } from '@coze-arch/report-events';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { Button, Switch, Form } from '@coze-arch/coze-design';
|
||||
import { EVENT_NAMES, sendTeaEvent } from '@coze-arch/bot-tea';
|
||||
import {
|
||||
UITable,
|
||||
UITableAction,
|
||||
Tooltip,
|
||||
Image,
|
||||
Banner,
|
||||
} from '@coze-arch/bot-semi';
|
||||
import { IconAdd } from '@coze-arch/bot-icons';
|
||||
import { isApiError } from '@coze-arch/bot-http';
|
||||
import {
|
||||
type UpdateDatabaseRequest,
|
||||
type AddDatabaseRequest,
|
||||
BotTableRWMode,
|
||||
type FieldItemType,
|
||||
} from '@coze-arch/bot-api/memory';
|
||||
import { MemoryApi } from '@coze-arch/bot-api';
|
||||
|
||||
import { SLSelect } from '../singleline-select';
|
||||
import { FormSLInput, SLInput } from '../singleline-input';
|
||||
import { DatabaseFieldTitle } from '../database-field-title';
|
||||
import {
|
||||
type TriggerType,
|
||||
type TableFieldsInfo,
|
||||
type TableBasicInfo,
|
||||
type ReadAndWriteModeOptions,
|
||||
type CreateType,
|
||||
type OnSave,
|
||||
} from '../../types/database-field';
|
||||
import {
|
||||
DATABASE_CONTENT_CHECK_ERROR_CODE,
|
||||
DATABASE_CONTENT_CHECK_ERROR_CODE_NEW,
|
||||
FIELD_TYPE_OPTIONS,
|
||||
RW_MODE_OPTIONS_MAP,
|
||||
SYSTEM_FIELDS,
|
||||
} from '../../constants/database-field';
|
||||
import keyExample from '../../assets/key-example.png';
|
||||
import { validateFields, validateNaming } from './helpers/validate';
|
||||
import { KeyTipsNode } from './components/KeyTipsNode';
|
||||
|
||||
import s from './index.module.less';
|
||||
|
||||
const MAX_COLUMNS = 20;
|
||||
|
||||
export interface DatabaseTableStructureProps {
|
||||
data: DatabaseInfo;
|
||||
botId?: string;
|
||||
spaceId?: string;
|
||||
creatorId?: string;
|
||||
forceEdit?: boolean;
|
||||
loading?: boolean;
|
||||
loadingTips?: string;
|
||||
projectID?: string;
|
||||
/**
|
||||
* excel: 单用户模式|只读模式
|
||||
* normal: 单用户模式|只读模式
|
||||
* expert: 单用户模式|只读模式|多用户模式
|
||||
* undefined: 不支持读写模式
|
||||
*/
|
||||
readAndWriteModeOptions?: ReadAndWriteModeOptions;
|
||||
/** databaseInfo中只显示 Mode 的UI */
|
||||
onlyShowDatabaseInfoRWMode?: boolean;
|
||||
enableAdd?: boolean;
|
||||
isReadonlyMode?: boolean;
|
||||
maxColumnNum?: number;
|
||||
/**
|
||||
* 是否展示基本信息(表名、介绍)
|
||||
*/
|
||||
showDatabaseBaseInfo?: boolean;
|
||||
hiddenTableBorder?: boolean;
|
||||
|
||||
useComputingEnableGoToNextStep?: (
|
||||
list: TableFieldsInfo,
|
||||
isEmptyList: boolean,
|
||||
) => void;
|
||||
onCancel?: () => void;
|
||||
onSave?: OnSave;
|
||||
onDeleteField?: (list: TableFieldsInfo) => void;
|
||||
setContentCheckErrorMsg?: (s: string) => void;
|
||||
|
||||
createType: CreateType;
|
||||
|
||||
renderModeSelect?: (props: {
|
||||
dataTestId: string;
|
||||
field: string;
|
||||
label: string;
|
||||
type: 'select';
|
||||
options: BotTableRWMode[];
|
||||
}) => ReactNode;
|
||||
}
|
||||
|
||||
export interface DatabaseTableStructureRef {
|
||||
validate: () => Promise<boolean>;
|
||||
submit: () => Promise<void>;
|
||||
isReadonly: boolean;
|
||||
setTableFieldsList: (list: TableFieldsInfo) => void;
|
||||
tableFieldsList: TableFieldsInfo;
|
||||
tableBasicInfoFormRef: MutableRefObject<Form<TableBasicInfo>>;
|
||||
}
|
||||
|
||||
export const DatabaseTableStructure = forwardRef<
|
||||
DatabaseTableStructureRef,
|
||||
DatabaseTableStructureProps
|
||||
// eslint-disable-next-line max-lines-per-function, @coze-arch/max-line-per-function, complexity -- 历史文件拷贝
|
||||
>((props, ref) => {
|
||||
const {
|
||||
data: initialData,
|
||||
botId = '',
|
||||
spaceId = '',
|
||||
creatorId = '',
|
||||
onSave,
|
||||
onCancel,
|
||||
onDeleteField,
|
||||
forceEdit = false,
|
||||
maxColumnNum = MAX_COLUMNS,
|
||||
useComputingEnableGoToNextStep,
|
||||
readAndWriteModeOptions,
|
||||
onlyShowDatabaseInfoRWMode,
|
||||
enableAdd = true,
|
||||
loading = false,
|
||||
setContentCheckErrorMsg = noop,
|
||||
// TODO 把 AI generate 的 loading tip 放到 table 里面
|
||||
// loadingTips,
|
||||
createType,
|
||||
showDatabaseBaseInfo,
|
||||
hiddenTableBorder,
|
||||
isReadonlyMode,
|
||||
projectID,
|
||||
renderModeSelect,
|
||||
} = props;
|
||||
const [tableFieldsList, setTableFieldsList] = useState<TableFieldsInfo>([]);
|
||||
|
||||
const inputRef = useRef<{
|
||||
triggerFocus?: () => void;
|
||||
}>(null);
|
||||
const scrollRef = useRef<HTMLDivElement>(null);
|
||||
const tableBasicInfoFormRef = useRef<Form<TableBasicInfo>>();
|
||||
|
||||
const isModify = Boolean(initialData.tableId);
|
||||
|
||||
const [isReadonly, { setTrue: enableReadonly, setFalse: disableReadonly }] =
|
||||
useBoolean(false);
|
||||
// 系统字段不计入字段数量限制
|
||||
const userFields = tableFieldsList.filter(i => !i.isSystemField);
|
||||
const isRowMaxLimit = userFields.length >= maxColumnNum;
|
||||
const isExceedRowMaxLimit = userFields.length > maxColumnNum;
|
||||
const isEmptyList =
|
||||
userFields.filter(i => i.name || i.desc || i.type).length <= 0;
|
||||
const databaseAuditErrorCodes = [
|
||||
DATABASE_CONTENT_CHECK_ERROR_CODE,
|
||||
DATABASE_CONTENT_CHECK_ERROR_CODE_NEW,
|
||||
];
|
||||
const handleContentCheckError = (error: Error) => {
|
||||
if (
|
||||
isApiError(error) &&
|
||||
databaseAuditErrorCodes.includes(Number(error?.code))
|
||||
) {
|
||||
setContentCheckErrorMsg(
|
||||
error?.msg || I18n.t('knowledge_bot_update_databse_tnserr_msg'),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const handleAdd = (triggerFocus = true) => {
|
||||
if (isReadonly) {
|
||||
return;
|
||||
}
|
||||
const newTableFieldsList = [
|
||||
...tableFieldsList,
|
||||
{
|
||||
nanoid: nanoid(),
|
||||
name: '',
|
||||
desc: '',
|
||||
type: undefined as unknown as FieldItemType,
|
||||
must_required: false,
|
||||
},
|
||||
];
|
||||
|
||||
setTableFieldsList(newTableFieldsList);
|
||||
|
||||
if (triggerFocus) {
|
||||
setTimeout(() => {
|
||||
inputRef.current?.triggerFocus?.();
|
||||
scrollRef.current?.scrollIntoView({
|
||||
block: 'end',
|
||||
behavior: 'smooth',
|
||||
});
|
||||
}, 100);
|
||||
}
|
||||
};
|
||||
|
||||
const verifyTableFields = (trigger: TriggerType) => {
|
||||
setTableFieldsList(newTableFieldsList =>
|
||||
validateFields(newTableFieldsList, trigger),
|
||||
);
|
||||
};
|
||||
|
||||
const verifyAllBeforeSave = async (): Promise<boolean> => {
|
||||
// 触发 tableFields 校验
|
||||
const validatedTableFieldsList = validateFields(tableFieldsList, 'save');
|
||||
setTableFieldsList(validatedTableFieldsList);
|
||||
|
||||
// 触发并校验 tableBasicInfo
|
||||
if (showDatabaseBaseInfo) {
|
||||
try {
|
||||
// @ts-expect-error -- linter-disable-autofix
|
||||
await tableBasicInfoFormRef.current.formApi.validate(['name']);
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 校验 tableFields
|
||||
if (
|
||||
validatedTableFieldsList.find(i =>
|
||||
Object.keys(i.errorMapper || {}).find(
|
||||
j => !!i?.errorMapper?.[j]?.length,
|
||||
),
|
||||
)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 校验 tableFields 是否为空
|
||||
if (isEmptyList) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const save = async () => {
|
||||
// @ts-expect-error -- linter-disable-autofix
|
||||
let tableBasicInfo: TableBasicInfo = {};
|
||||
if (tableBasicInfoFormRef.current) {
|
||||
tableBasicInfo = tableBasicInfoFormRef.current.formApi.getValues();
|
||||
} else {
|
||||
tableBasicInfo = {
|
||||
name: initialData.name,
|
||||
desc: initialData.desc,
|
||||
readAndWriteMode: initialData.readAndWriteMode,
|
||||
prompt_disabled: initialData.extra_info?.prompt_disabled === 'true',
|
||||
};
|
||||
}
|
||||
if (isModify) {
|
||||
sendTeaEvent(EVENT_NAMES.edit_table_click, {
|
||||
need_login: true,
|
||||
have_access: true,
|
||||
bot_id: botId,
|
||||
table_name: tableBasicInfo.name,
|
||||
});
|
||||
} else {
|
||||
sendTeaEvent(EVENT_NAMES.create_table_click, {
|
||||
need_login: true,
|
||||
have_access: true,
|
||||
bot_id: botId,
|
||||
table_name: tableBasicInfo.name,
|
||||
database_create_type: createType,
|
||||
});
|
||||
}
|
||||
|
||||
let resp;
|
||||
const params: AddDatabaseRequest | UpdateDatabaseRequest = {
|
||||
table_name: onlyShowDatabaseInfoRWMode
|
||||
? initialData.name
|
||||
: tableBasicInfo.name,
|
||||
table_desc: onlyShowDatabaseInfoRWMode
|
||||
? initialData.desc
|
||||
: tableBasicInfo.desc,
|
||||
icon_uri: initialData.icon_uri,
|
||||
prompt_disabled: !!tableBasicInfo.prompt_disabled,
|
||||
field_list: tableFieldsList
|
||||
.filter(i => !!i.name && !i.isSystemField)
|
||||
.map(i => ({
|
||||
name: i.name,
|
||||
desc: i.desc || '',
|
||||
type: i.type,
|
||||
must_required: i.must_required,
|
||||
id: i?.id,
|
||||
alterId: i?.alterId,
|
||||
})),
|
||||
rw_mode: tableBasicInfo.readAndWriteMode,
|
||||
};
|
||||
if (!isModify) {
|
||||
try {
|
||||
resp = await MemoryApi.AddDatabase({
|
||||
...params,
|
||||
space_id: spaceId,
|
||||
creator_id: creatorId,
|
||||
project_id: projectID,
|
||||
});
|
||||
} catch (error) {
|
||||
dataReporter.errorEvent(DataNamespace.DATABASE, {
|
||||
eventName: REPORT_EVENTS.DatabaseAddTable,
|
||||
error: error as Error,
|
||||
});
|
||||
handleContentCheckError(error as Error);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
resp = await MemoryApi.UpdateDatabase({
|
||||
...params,
|
||||
id: initialData.tableId,
|
||||
});
|
||||
} catch (error) {
|
||||
dataReporter.errorEvent(DataNamespace.DATABASE, {
|
||||
eventName: REPORT_EVENTS.DatabaseAlterTable,
|
||||
error: error as Error,
|
||||
});
|
||||
handleContentCheckError(error as Error);
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (onSave) {
|
||||
await onSave({
|
||||
response: resp,
|
||||
});
|
||||
}
|
||||
onCancel?.();
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
try {
|
||||
enableReadonly();
|
||||
|
||||
const validateRes = await verifyAllBeforeSave();
|
||||
if (!validateRes) {
|
||||
return;
|
||||
}
|
||||
|
||||
await save();
|
||||
} finally {
|
||||
disableReadonly();
|
||||
}
|
||||
};
|
||||
|
||||
const getTableNameErrorMessage = (v: string) => {
|
||||
if (!v) {
|
||||
return I18n.t('db_add_table_name_tips');
|
||||
}
|
||||
const errList = validateNaming(v);
|
||||
if (errList.length > 0) {
|
||||
return errList.join('; ');
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
// 初始化 ref 属性
|
||||
useImperativeHandle<DatabaseTableStructureRef, DatabaseTableStructureRef>(
|
||||
ref,
|
||||
() => ({
|
||||
async submit() {
|
||||
return await handleSave();
|
||||
},
|
||||
async validate() {
|
||||
const res = await verifyAllBeforeSave();
|
||||
return res;
|
||||
},
|
||||
setTableFieldsList,
|
||||
isReadonly,
|
||||
// @ts-expect-error -- linter-disable-autofix
|
||||
tableBasicInfoFormRef,
|
||||
tableFieldsList,
|
||||
}),
|
||||
[isReadonly, tableFieldsList, tableBasicInfoFormRef],
|
||||
);
|
||||
|
||||
// 校验是否 disable 下一步按钮
|
||||
useComputingEnableGoToNextStep?.(tableFieldsList, isEmptyList);
|
||||
|
||||
const dataSource = enableAdd
|
||||
? [...tableFieldsList, { operate: 'add' }]
|
||||
: tableFieldsList;
|
||||
|
||||
const resetContentCheckErrorMsg = () => {
|
||||
setContentCheckErrorMsg('');
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setTableFieldsList([...SYSTEM_FIELDS, ...initialData.tableMemoryList]);
|
||||
if (tableBasicInfoFormRef.current && !loading) {
|
||||
tableBasicInfoFormRef.current.formApi.setValues({
|
||||
name: initialData?.name || '',
|
||||
desc: initialData?.desc || '',
|
||||
prompt_disabled: isModify
|
||||
? initialData.extra_info?.prompt_disabled === 'true'
|
||||
: false,
|
||||
readAndWriteMode:
|
||||
initialData?.readAndWriteMode || BotTableRWMode.LimitedReadWrite,
|
||||
});
|
||||
}
|
||||
}, [initialData, loading]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(s['table-structure-wrapper'], {
|
||||
[s['hidden-form-border']]: hiddenTableBorder,
|
||||
})}
|
||||
>
|
||||
{showDatabaseBaseInfo ? (
|
||||
<Form<TableBasicInfo>
|
||||
// @ts-expect-error -- linter-disable-autofix
|
||||
ref={tableBasicInfoFormRef}
|
||||
layout="vertical"
|
||||
className={s['table-structure-form']}
|
||||
onValueChange={(_values, changedV) => {
|
||||
if ('name' in changedV || 'desc' in changedV) {
|
||||
resetContentCheckErrorMsg();
|
||||
}
|
||||
}}
|
||||
>
|
||||
{onlyShowDatabaseInfoRWMode ? null : (
|
||||
<>
|
||||
<FormSLInput
|
||||
field="name"
|
||||
label={{
|
||||
text: I18n.t('db_add_table_name'),
|
||||
required: true,
|
||||
}}
|
||||
validate={v => getTableNameErrorMessage(v)}
|
||||
trigger={['change', 'blur']}
|
||||
fieldClassName={s['table-name-form-field']}
|
||||
disabled={isReadonlyMode}
|
||||
inputProps={{
|
||||
'data-testid': BotE2e.BotDatabaseAddModalTableNameInput,
|
||||
disabled: !forceEdit && (isReadonly || isModify),
|
||||
placeholder: I18n.t('db_add_table_name_tips'),
|
||||
}}
|
||||
onFocusPopoverProps={{
|
||||
style: { padding: '2px 12px' },
|
||||
position: 'left',
|
||||
content: <KeyTipsNode />,
|
||||
}}
|
||||
/>
|
||||
<Form.TextArea
|
||||
data-testid={BotE2e.BotDatabaseAddModalTableDescInput}
|
||||
field="desc"
|
||||
label={I18n.t('db_add_table_desc')}
|
||||
disabled={isReadonly || isReadonlyMode}
|
||||
rows={2}
|
||||
placeholder={I18n.t('db_add_table_desc_tips')}
|
||||
fieldClassName={s['table-desc-form-field']}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{renderModeSelect && readAndWriteModeOptions
|
||||
? renderModeSelect({
|
||||
dataTestId: BotE2e.BotDatabaseAddModalTableQueryModeSelect,
|
||||
field: 'readAndWriteMode',
|
||||
label: I18n.t('db_table_0129_001'),
|
||||
type: 'select',
|
||||
options: RW_MODE_OPTIONS_MAP[readAndWriteModeOptions],
|
||||
})
|
||||
: null}
|
||||
</Form>
|
||||
) : null}
|
||||
|
||||
{isExceedRowMaxLimit ? (
|
||||
<Banner
|
||||
type="warning"
|
||||
description={I18n.t('db_table_0126_027', {
|
||||
ColumNum: maxColumnNum,
|
||||
})}
|
||||
className={s['max-row-banner']}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<UITable
|
||||
tableProps={{
|
||||
loading,
|
||||
columns: [
|
||||
{
|
||||
dataIndex: 'name',
|
||||
title: (
|
||||
<DatabaseFieldTitle
|
||||
field={I18n.t('db_add_table_field_name')}
|
||||
required
|
||||
tip={
|
||||
<div className={s['th-tip-name']}>
|
||||
<span style={{ width: 494, marginBottom: 8 }}>
|
||||
{I18n.t('db_add_table_field_name_tips')}
|
||||
</span>
|
||||
<Image
|
||||
preview={false}
|
||||
width={494}
|
||||
height={163}
|
||||
src={keyExample}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
),
|
||||
render: (text, record, index) =>
|
||||
record.operate !== 'add' ? (
|
||||
<SLInput
|
||||
style={{ position: 'static' }}
|
||||
onRef={inputRef}
|
||||
value={record.name}
|
||||
inputProps={{
|
||||
'data-testid': BotE2e.BotDatabaseAddModalFieldNameInput,
|
||||
'data-dtestid': BotE2e.BotDatabaseAddModalFieldNameInput,
|
||||
disabled:
|
||||
isReadonly || record.isSystemField || isReadonlyMode,
|
||||
placeholder: 'Enter Name',
|
||||
}}
|
||||
errorMsgFloat
|
||||
onFocusPopoverProps={{
|
||||
style: { padding: '2px 12px' },
|
||||
position: 'left',
|
||||
content: <KeyTipsNode />,
|
||||
}}
|
||||
errorMsg={tableFieldsList[index]?.errorMapper?.name?.join(
|
||||
'; ',
|
||||
)}
|
||||
handleChange={v => {
|
||||
const newTableMemoryList = [...tableFieldsList];
|
||||
newTableMemoryList[index].name = v;
|
||||
setTableFieldsList(newTableMemoryList);
|
||||
verifyTableFields('change');
|
||||
resetContentCheckErrorMsg();
|
||||
}}
|
||||
handleBlur={() => {
|
||||
verifyTableFields('blur');
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<div
|
||||
ref={scrollRef}
|
||||
data-testid={BotE2e.BotDatabaseAddModalAddBtn}
|
||||
>
|
||||
{isRowMaxLimit ? (
|
||||
<div style={{ paddingRight: 10 }}>
|
||||
<Tooltip
|
||||
position="top"
|
||||
content={I18n.t('bot_database_add_field', {
|
||||
number: maxColumnNum,
|
||||
})}
|
||||
>
|
||||
<Button color="secondary" disabled icon={<IconAdd />}>
|
||||
{I18n.t('bot_userProfile_add')}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
) : (
|
||||
<Button
|
||||
color="primary"
|
||||
disabled={isReadonly}
|
||||
onClick={() => handleAdd(true)}
|
||||
icon={<IconAdd />}
|
||||
>
|
||||
{I18n.t('bot_userProfile_add')}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
width: 261,
|
||||
},
|
||||
{
|
||||
dataIndex: 'desc',
|
||||
title: (
|
||||
<DatabaseFieldTitle
|
||||
field={I18n.t('db_add_table_field_desc')}
|
||||
tip={
|
||||
<article style={{ width: 327 }}>
|
||||
{I18n.t('db_add_table_field_desc_tips')}
|
||||
</article>
|
||||
}
|
||||
/>
|
||||
),
|
||||
render: (text, record, index) =>
|
||||
record.operate !== 'add' ? (
|
||||
<SLInput
|
||||
value={record.desc}
|
||||
maxCount={300}
|
||||
inputProps={{
|
||||
'data-testid': `${BotE2e.BotDatabaseAddModalFieldDescInput}.${index}.${record.name}`,
|
||||
'data-dtestid': `${BotE2e.BotDatabaseAddModalFieldDescInput}.${index}.${record.name}`,
|
||||
maxLength: 300,
|
||||
disabled:
|
||||
isReadonly || record.isSystemField || isReadonlyMode,
|
||||
placeholder: I18n.t(
|
||||
'bot_edit_variable_description_placeholder',
|
||||
),
|
||||
}}
|
||||
errorMsgFloat
|
||||
handleChange={v => {
|
||||
const newTableMemoryList = [...tableFieldsList];
|
||||
newTableMemoryList[index].desc = v;
|
||||
setTableFieldsList(newTableMemoryList);
|
||||
resetContentCheckErrorMsg();
|
||||
}}
|
||||
/>
|
||||
) : null,
|
||||
width: 369,
|
||||
},
|
||||
{
|
||||
dataIndex: 'type',
|
||||
title: (
|
||||
<DatabaseFieldTitle
|
||||
field={I18n.t('db_add_table_field_type')}
|
||||
required
|
||||
tip={
|
||||
<article style={{ width: 327 }}>
|
||||
{I18n.t('db_add_table_field_type_tips')}
|
||||
</article>
|
||||
}
|
||||
/>
|
||||
),
|
||||
render: (text, record, index) =>
|
||||
record.operate !== 'add' ? (
|
||||
<SLSelect
|
||||
value={record.type}
|
||||
selectProps={{
|
||||
'data-testid': `${BotE2e.BotDatabaseAddModalFieldTypeSelect}.${index}.${record.name}`,
|
||||
'data-dtestid': `${BotE2e.BotDatabaseAddModalFieldTypeSelect}.${index}.${record.name}`,
|
||||
disabled:
|
||||
isReadonly ||
|
||||
(isModify && !!record.id) ||
|
||||
record.isSystemField ||
|
||||
isReadonlyMode,
|
||||
placeholder: I18n.t('db_table_save_exception_fieldtype'),
|
||||
optionList: FIELD_TYPE_OPTIONS,
|
||||
}}
|
||||
errorMsgFloat
|
||||
errorMsg={tableFieldsList[index]?.errorMapper?.type?.join(
|
||||
'; ',
|
||||
)}
|
||||
handleChange={v => {
|
||||
const newTableMemoryList = [...tableFieldsList];
|
||||
newTableMemoryList[index].type = v as FieldItemType;
|
||||
setTableFieldsList(newTableMemoryList);
|
||||
verifyTableFields('change');
|
||||
}}
|
||||
/>
|
||||
) : null,
|
||||
width: 214,
|
||||
},
|
||||
{
|
||||
dataIndex: 'must_required',
|
||||
title: (
|
||||
<DatabaseFieldTitle
|
||||
field={I18n.t('db_add_table_field_necessary')}
|
||||
tip={
|
||||
<article style={{ width: 327 }}>
|
||||
<p className={s['th-tip-dot']}>
|
||||
{I18n.t('db_add_table_field_necessary_tips1')}
|
||||
</p>
|
||||
<p className={s['th-tip-dot']}>
|
||||
{I18n.t('db_add_table_field_necessary_tips2')}
|
||||
</p>
|
||||
</article>
|
||||
}
|
||||
/>
|
||||
),
|
||||
render: (text, record, index) =>
|
||||
record.operate !== 'add' ? (
|
||||
<div style={{ display: 'flex', alignItems: 'center' }}>
|
||||
<Switch
|
||||
data-testid={`${BotE2e.BotDatabaseAddModalFieldRequiredSwitch}.${index}.${record.name}`}
|
||||
data-dtestid={`${BotE2e.BotDatabaseAddModalFieldRequiredSwitch}.${index}.${record.name}`}
|
||||
disabled={
|
||||
isReadonly ||
|
||||
record.disableMustRequired ||
|
||||
record.isSystemField ||
|
||||
isReadonlyMode
|
||||
}
|
||||
checked={record.must_required}
|
||||
onChange={v => {
|
||||
const newTableMemoryList = [...tableFieldsList];
|
||||
newTableMemoryList[index].must_required = v;
|
||||
setTableFieldsList(newTableMemoryList);
|
||||
}}
|
||||
size="small"
|
||||
aria-label="a switch for semi demo"
|
||||
/>
|
||||
</div>
|
||||
) : null,
|
||||
width: 108,
|
||||
},
|
||||
{
|
||||
dataIndex: 'operate',
|
||||
title: <DatabaseFieldTitle field={I18n.t('db_table_0126_021')} />,
|
||||
render: (text, record, index) =>
|
||||
record.operate !== 'add' ? (
|
||||
<UITableAction
|
||||
deleteProps={{
|
||||
handleClick: () => {
|
||||
if (isReadonly) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newTableMemoryList = [
|
||||
...tableFieldsList.slice(0, index),
|
||||
...tableFieldsList.slice(index + 1),
|
||||
];
|
||||
|
||||
setTableFieldsList(newTableMemoryList);
|
||||
verifyTableFields('change');
|
||||
onDeleteField?.(newTableMemoryList);
|
||||
},
|
||||
popconfirm: {
|
||||
defaultVisible: false,
|
||||
visible: false,
|
||||
},
|
||||
tooltip: {
|
||||
content: I18n.t('datasets_table_title_actions_delete'),
|
||||
},
|
||||
disabled: record.isSystemField || isReadonlyMode,
|
||||
}}
|
||||
editProps={{
|
||||
hide: true,
|
||||
}}
|
||||
/>
|
||||
) : null,
|
||||
width: 85,
|
||||
},
|
||||
],
|
||||
dataSource,
|
||||
pagination: false,
|
||||
className: s['table-structure-table'],
|
||||
rowKey: 'nanoid',
|
||||
}}
|
||||
wrapperClassName={s['table-structure-table-wrapper']}
|
||||
/>
|
||||
{/* 表格为空时,底部的错误提示 */}
|
||||
{isEmptyList && !loading ? (
|
||||
<div className={s['table-empty-tips']}>
|
||||
{I18n.t('db_table_save_exception_nofield')}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,87 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { useState, type PropsWithChildren } from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { IconCozCross } from '@coze-arch/coze-design/icons';
|
||||
import { type BannerProps, IconButton, Typography } from '@coze-arch/coze-design';
|
||||
|
||||
export type BannerType = NonNullable<BannerProps['type']>;
|
||||
|
||||
const BannerClassNames: Record<BannerType, string> = {
|
||||
info: 'bg-[rgba(var(--coze-brand-1),var(--coze-brand-1-alpha))]',
|
||||
warning: 'bg-[rgba(var(--coze-yellow-1),var(--coze-yellow-1-alpha))]',
|
||||
danger: 'bg-[rgba(var(--coze-red-1),var(--coze-red-1-alpha))]',
|
||||
success: 'bg-[rgba(var(--coze-green-1),var(--coze-green-1-alpha))]',
|
||||
};
|
||||
|
||||
export interface DismissibleBannerProps extends PropsWithChildren {
|
||||
type?: BannerType;
|
||||
persistentKey: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function DismissibleBanner({
|
||||
type,
|
||||
persistentKey,
|
||||
className,
|
||||
children,
|
||||
}: DismissibleBannerProps) {
|
||||
const [dismissed, setDismissed] = useState(
|
||||
Boolean(localStorage.getItem(persistentKey)),
|
||||
);
|
||||
const [closed, setClosed] = useState(false);
|
||||
|
||||
if (dismissed || closed) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
'p-[8px] flex justify-center',
|
||||
BannerClassNames[type ?? 'info'],
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<div className="flex grow justify-center text-[14px] leading-[20px]">
|
||||
{children}
|
||||
</div>
|
||||
<div className="flex items-center gap-[10px] leading-none">
|
||||
<Typography.Text
|
||||
type="secondary"
|
||||
fontSize="12px"
|
||||
className="cursor-pointer"
|
||||
onClick={() => {
|
||||
localStorage.setItem(persistentKey, '1');
|
||||
setDismissed(true);
|
||||
}}
|
||||
>
|
||||
{I18n.t('not_show_again')}
|
||||
</Typography.Text>
|
||||
<IconButton
|
||||
color="secondary"
|
||||
size="mini"
|
||||
className="!h-[unset]"
|
||||
icon={<IconCozCross className="w-[16px] h-[16px]" />}
|
||||
onClick={() => setClosed(true)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
.input-wrapper {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
|
||||
// semi 原有的 disable 样式不满足需求,需要更明显的文字颜色
|
||||
:global {
|
||||
// .semi-input-wrapper-disabled {
|
||||
// -webkit-text-fill-color: rgba(29, 28, 35, 100%);
|
||||
// }
|
||||
|
||||
// .semi-input-suffix {
|
||||
// -webkit-text-fill-color: var(--semi-color-disabled-text, rgba(56, 55, 67, 20%))
|
||||
// }
|
||||
}
|
||||
|
||||
span {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.error-wrapper {
|
||||
:global {
|
||||
.semi-input-wrapper {
|
||||
border: 1px solid @error-red;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.error-content {
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.error-float {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.error-text {
|
||||
position: absolute;
|
||||
padding-top: 2px;
|
||||
font-size: 12px;
|
||||
color: @error-red;
|
||||
}
|
||||
|
||||
.input {
|
||||
input {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
|
||||
.limit-count {
|
||||
overflow: hidden;
|
||||
|
||||
padding-right: 12px;
|
||||
padding-left: 8px;
|
||||
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
line-height: 16px;
|
||||
color: var(--light-usage-text-color-text-3, rgba(28, 31, 35, 35%));
|
||||
}
|
||||
@@ -0,0 +1,215 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React, {
|
||||
type ComponentProps,
|
||||
useRef,
|
||||
useImperativeHandle,
|
||||
useMemo,
|
||||
useEffect,
|
||||
type ForwardedRef,
|
||||
} from 'react';
|
||||
|
||||
import isNumber from 'lodash-es/isNumber';
|
||||
import cs from 'classnames';
|
||||
import { useReactive } from 'ahooks';
|
||||
import { type CommonexcludeType } from '@coze-arch/bot-semi/Form';
|
||||
import {
|
||||
type CommonFieldProps,
|
||||
withField,
|
||||
type InputProps,
|
||||
Input,
|
||||
type PopoverProps,
|
||||
Popover,
|
||||
type TooltipProps,
|
||||
Tooltip,
|
||||
} from '@coze-arch/coze-design';
|
||||
|
||||
import s from './index.module.less';
|
||||
|
||||
export interface SLInputRefType {
|
||||
triggerFocus?: () => void;
|
||||
}
|
||||
|
||||
export type SLInputProps = ComponentProps<typeof Input> & {
|
||||
value: string | undefined;
|
||||
onRef?: ForwardedRef<SLInputRefType>;
|
||||
ellipsis?: boolean;
|
||||
handleChange?: (v: string) => void;
|
||||
handleBlur?: (v: string) => void;
|
||||
handleFocus?: (v: string) => void;
|
||||
ellipsisPopoverProps?: PopoverProps;
|
||||
onFocusPopoverProps?: PopoverProps;
|
||||
tooltipProps?: TooltipProps;
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention -- 历史逻辑
|
||||
inputProps?: InputProps & { 'data-dtestid'?: string; 'data-testid'?: string };
|
||||
errorMsg?: string;
|
||||
errorMsgFloat?: boolean;
|
||||
maxCount?: number;
|
||||
className?: string;
|
||||
style?: React.CSSProperties;
|
||||
};
|
||||
|
||||
// eslint-disable-next-line complexity
|
||||
export const SLInput: React.FC<SLInputProps> = props => {
|
||||
const { ellipsis = true, maxCount, errorMsgFloat } = props;
|
||||
const showCount = isNumber(maxCount) && maxCount > 0;
|
||||
useImperativeHandle(props.onRef, () => ({
|
||||
triggerFocus,
|
||||
}));
|
||||
const $state = useReactive({
|
||||
value: props.value,
|
||||
inputOnFocus: false,
|
||||
inputEle: false,
|
||||
});
|
||||
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const triggerFocus = () => {
|
||||
$state.inputEle = true;
|
||||
inputRef?.current?.focus();
|
||||
};
|
||||
|
||||
const onFocus = () => {
|
||||
$state.inputOnFocus = true;
|
||||
$state.inputEle = true;
|
||||
props?.handleFocus?.($state.value || '');
|
||||
};
|
||||
|
||||
const onBlur = (e: React.FocusEvent<HTMLInputElement>) => {
|
||||
$state.inputOnFocus = false;
|
||||
props?.handleBlur?.($state.value || '');
|
||||
props?.onBlur?.(e);
|
||||
$state.inputEle = false;
|
||||
};
|
||||
|
||||
const onChange = (v: string) => {
|
||||
$state.value = v;
|
||||
props?.handleChange?.(v);
|
||||
};
|
||||
|
||||
const onclick = () => {
|
||||
if (!$state.inputEle) {
|
||||
setTimeout(() => {
|
||||
inputRef?.current?.focus();
|
||||
}, 10);
|
||||
}
|
||||
$state.inputEle = true;
|
||||
};
|
||||
const hasEllipsis = useMemo(() => {
|
||||
const clientWidth = inputRef.current?.clientWidth || 0;
|
||||
const scrollWidth = inputRef.current?.scrollWidth || 0;
|
||||
return clientWidth < scrollWidth - 1;
|
||||
}, [
|
||||
ellipsis,
|
||||
$state.inputOnFocus,
|
||||
$state.value,
|
||||
inputRef.current?.clientWidth,
|
||||
inputRef.current?.scrollWidth,
|
||||
$state.inputEle,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
$state.value = props.value;
|
||||
}, [props.value]);
|
||||
|
||||
const LimitCountNode = (
|
||||
<span className={s['limit-count']}>
|
||||
{$state.value?.length || 0}/{maxCount}
|
||||
</span>
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cs(s['input-wrapper'], props.className)}
|
||||
style={props.style}
|
||||
>
|
||||
{!$state.inputEle && hasEllipsis ? (
|
||||
<Tooltip
|
||||
content={
|
||||
<article
|
||||
style={{
|
||||
maxWidth: 200,
|
||||
wordWrap: 'break-word',
|
||||
wordBreak: 'normal',
|
||||
}}
|
||||
>
|
||||
{$state.value}
|
||||
</article>
|
||||
}
|
||||
position={'top'}
|
||||
showArrow
|
||||
mouseEnterDelay={300}
|
||||
{...props.tooltipProps}
|
||||
>
|
||||
<div
|
||||
className={cs(props?.errorMsg ? s['error-wrapper'] : null)}
|
||||
onClick={onclick}
|
||||
>
|
||||
<Input
|
||||
{...props.inputProps}
|
||||
ref={inputRef}
|
||||
value={$state.value}
|
||||
className={ellipsis ? s.input : ''}
|
||||
suffix={showCount ? LimitCountNode : undefined}
|
||||
></Input>
|
||||
</div>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<div className={cs(props?.errorMsg ? s['error-wrapper'] : null)}>
|
||||
<Popover
|
||||
{...props.onFocusPopoverProps}
|
||||
trigger="custom"
|
||||
visible={
|
||||
Boolean(props.onFocusPopoverProps?.content) && $state.inputOnFocus
|
||||
}
|
||||
showArrow
|
||||
>
|
||||
<Input
|
||||
{...props.inputProps}
|
||||
ref={inputRef}
|
||||
value={$state.value}
|
||||
className={ellipsis ? s.input : ''}
|
||||
onChange={onChange}
|
||||
onFocus={onFocus}
|
||||
onBlur={onBlur}
|
||||
suffix={showCount ? LimitCountNode : undefined}
|
||||
></Input>
|
||||
</Popover>
|
||||
</div>
|
||||
)}
|
||||
{props?.errorMsg ? (
|
||||
<div
|
||||
className={cs({
|
||||
[s['error-content']]: true,
|
||||
[s['error-float']]: Boolean(errorMsgFloat),
|
||||
})}
|
||||
>
|
||||
<div className={s['error-text']}>{props?.errorMsg}</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Semi 不导出被 withField 包装的组件的 props 类型(甚至是 any ´_>`)
|
||||
// https://github.com/DouyinFE/semi-design/blob/v2.69.2/packages/semi-ui/form/hoc/withField.tsx#L528
|
||||
export const FormSLInput: React.FunctionComponent<
|
||||
CommonFieldProps & Omit<SLInputProps, keyof CommonexcludeType>
|
||||
> = withField(SLInput, {
|
||||
valueKey: 'value',
|
||||
onKeyChangeFnName: 'handleChange',
|
||||
});
|
||||
@@ -0,0 +1,54 @@
|
||||
.select-wrapper {
|
||||
width: 100%;
|
||||
|
||||
// semi 原有的 disable 样式不满足需求,需要更明显的文字颜色
|
||||
:global {
|
||||
// .semi-select-disabled .semi-select-selection {
|
||||
// color: rgba(29, 28, 35, 100%);
|
||||
// }
|
||||
|
||||
.semi-select-disabled .semi-select-arrow {
|
||||
color: rgba(29, 28, 35, 60%);
|
||||
}
|
||||
}
|
||||
|
||||
span {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.error-wrapper {
|
||||
:global {
|
||||
.semi-select {
|
||||
border: 1px solid @error-red;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.error-content {
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.error-float {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.error-text {
|
||||
position: absolute;
|
||||
padding-top: 2px;
|
||||
font-size: 12px;
|
||||
color: @error-red;
|
||||
}
|
||||
|
||||
|
||||
.selected-option {
|
||||
:global {
|
||||
.semi-select-option-selected {
|
||||
.semi-select-option-icon {
|
||||
color: #4D53E8
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
* 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 {
|
||||
Select,
|
||||
type SelectProps,
|
||||
type InputProps,
|
||||
} from '@coze-arch/coze-design';
|
||||
|
||||
import s from './index.module.less';
|
||||
|
||||
export interface SLSelectRefType {
|
||||
triggerFocus?: () => void;
|
||||
}
|
||||
|
||||
export type SLSelectProps = InputProps & {
|
||||
value: SelectProps['value'];
|
||||
handleChange?: (v: SelectProps['value']) => void;
|
||||
errorMsg?: string;
|
||||
errorMsgFloat?: boolean;
|
||||
selectProps?: SelectProps & {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
'data-dtestid'?: string;
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
'data-testid'?: string;
|
||||
};
|
||||
};
|
||||
|
||||
export const SLSelect: React.FC<SLSelectProps> = props => {
|
||||
const { errorMsg, errorMsgFloat } = props;
|
||||
return (
|
||||
<div
|
||||
className={classnames({
|
||||
[s['select-wrapper']]: true,
|
||||
[s['error-wrapper']]: Boolean(errorMsg),
|
||||
})}
|
||||
>
|
||||
<Select
|
||||
{...props.selectProps}
|
||||
style={{ width: '100%' }}
|
||||
value={props.value}
|
||||
onChange={v => {
|
||||
props?.handleChange?.(v);
|
||||
}}
|
||||
dropdownClassName={s['selected-option']}
|
||||
/>
|
||||
{errorMsg ? (
|
||||
<div
|
||||
className={classnames({
|
||||
[s['error-content']]: true,
|
||||
[s['error-float']]: Boolean(errorMsgFloat),
|
||||
})}
|
||||
>
|
||||
<div className={s['error-text']}>{errorMsg}</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,171 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { nanoid } from 'nanoid';
|
||||
import {
|
||||
type DatabaseInfo,
|
||||
type TableMemoryItem,
|
||||
} from '@coze-studio/bot-detail-store';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { FieldItemType, BotTableRWMode } from '@coze-arch/bot-api/memory';
|
||||
|
||||
import { type ReadAndWriteModeOptions } from '../types/database-field';
|
||||
|
||||
export const FIELD_TYPE_OPTIONS = [
|
||||
{ value: FieldItemType.Text, label: I18n.t('db_add_table_field_type_txt') },
|
||||
{ value: FieldItemType.Number, label: I18n.t('db_add_table_field_type_int') },
|
||||
{ value: FieldItemType.Date, label: I18n.t('db_add_table_field_type_time') },
|
||||
{
|
||||
value: FieldItemType.Float,
|
||||
label: I18n.t('db_add_table_field_type_number'),
|
||||
},
|
||||
{
|
||||
value: FieldItemType.Boolean,
|
||||
label: I18n.t('db_add_table_field_type_bool'),
|
||||
},
|
||||
];
|
||||
|
||||
export const TEMPLATE_INFO: DatabaseInfo = {
|
||||
name: 'book_notes',
|
||||
desc: I18n.t('db_add_table_temp_desc'),
|
||||
tableId: '',
|
||||
readAndWriteMode: BotTableRWMode.LimitedReadWrite,
|
||||
tableMemoryList: [
|
||||
{
|
||||
nanoid: nanoid(),
|
||||
name: 'name',
|
||||
desc: I18n.t('db_add_table_temp_field_desc1'),
|
||||
type: FieldItemType.Text,
|
||||
must_required: true,
|
||||
},
|
||||
{
|
||||
name: 'section',
|
||||
nanoid: nanoid(),
|
||||
desc: I18n.t('db_add_table_temp_field_desc2'),
|
||||
type: FieldItemType.Number,
|
||||
must_required: false,
|
||||
},
|
||||
{
|
||||
name: 'note',
|
||||
nanoid: nanoid(),
|
||||
desc: I18n.t('db_add_table_temp_field_desc3'),
|
||||
type: FieldItemType.Text,
|
||||
must_required: false,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const RW_MODE_OPTIONS_CONFIG: Record<
|
||||
BotTableRWMode,
|
||||
{ tips: string; label: string }
|
||||
> = {
|
||||
[BotTableRWMode.LimitedReadWrite]: {
|
||||
tips: I18n.t('db_table_0129_005'),
|
||||
label: I18n.t('db_table_0129_002'),
|
||||
},
|
||||
[BotTableRWMode.ReadOnly]: {
|
||||
tips: I18n.t('db_table_0129_006'),
|
||||
label: I18n.t('db_table_0129_003'),
|
||||
},
|
||||
[BotTableRWMode.UnlimitedReadWrite]: {
|
||||
tips: I18n.t('db_table_0129_007'),
|
||||
label: I18n.t('db_table_0129_004'),
|
||||
},
|
||||
[BotTableRWMode.RWModeMax]: {
|
||||
tips: '',
|
||||
label: '',
|
||||
},
|
||||
};
|
||||
|
||||
export const RW_MODE_OPTIONS_MAP: Record<
|
||||
ReadAndWriteModeOptions,
|
||||
BotTableRWMode[]
|
||||
> = {
|
||||
excel: [BotTableRWMode.LimitedReadWrite],
|
||||
normal: [BotTableRWMode.LimitedReadWrite],
|
||||
expert: [BotTableRWMode.LimitedReadWrite, BotTableRWMode.UnlimitedReadWrite],
|
||||
};
|
||||
|
||||
export const DATABASE_CONTENT_CHECK_ERROR_CODE = 708024072;
|
||||
export const DATABASE_CONTENT_CHECK_ERROR_CODE_NEW = 708334072;
|
||||
|
||||
/**
|
||||
* 内置字段: uuid
|
||||
* bstudio_connector_uid
|
||||
*/
|
||||
export const USER_ID_FIELD: TableMemoryItem = {
|
||||
name: 'uuid',
|
||||
desc: I18n.t('workflow_240221_01'),
|
||||
type: FieldItemType.Text,
|
||||
must_required: true,
|
||||
nanoid: nanoid(),
|
||||
isSystemField: true,
|
||||
};
|
||||
|
||||
/**
|
||||
* 内置字段: id
|
||||
*/
|
||||
export const ID_FIELD: TableMemoryItem = {
|
||||
name: 'id',
|
||||
desc: I18n.t('database_240520_01'),
|
||||
type: FieldItemType.Number,
|
||||
must_required: true,
|
||||
nanoid: nanoid(),
|
||||
isSystemField: true,
|
||||
};
|
||||
|
||||
/**
|
||||
* 内置字段: sys_platform
|
||||
* bstudio_connector_id
|
||||
*/
|
||||
export const PLATFORM_FIELD: TableMemoryItem = {
|
||||
name: 'sys_platform',
|
||||
desc: I18n.t('db_optimize_002'),
|
||||
type: FieldItemType.Text,
|
||||
must_required: true,
|
||||
nanoid: nanoid(),
|
||||
isSystemField: true,
|
||||
};
|
||||
|
||||
/**
|
||||
* 内置字段: connector_id
|
||||
* bstudio_create_time
|
||||
*/
|
||||
export const CREATE_TIME_FIELD: TableMemoryItem = {
|
||||
name: 'bstudio_create_time',
|
||||
desc: I18n.t('db_optimize_003'),
|
||||
type: FieldItemType.Date,
|
||||
must_required: true,
|
||||
nanoid: nanoid(),
|
||||
isSystemField: true,
|
||||
};
|
||||
|
||||
/**
|
||||
* 内置系统字段
|
||||
*/
|
||||
export const SYSTEM_FIELDS = [
|
||||
ID_FIELD,
|
||||
PLATFORM_FIELD,
|
||||
USER_ID_FIELD,
|
||||
CREATE_TIME_FIELD,
|
||||
];
|
||||
|
||||
export const SYSTEM_FIELD_ROW_INDEX: Record<string, string | undefined> = {
|
||||
id: 'bstudio_id',
|
||||
sys_platform: 'bstudio_connector_id',
|
||||
uuid: 'bstudio_connector_uid',
|
||||
bstudio_create_time: 'bstudio_create_time',
|
||||
};
|
||||
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { FIELD_TYPE_OPTIONS } from './database-field';
|
||||
export { TEMPLATE_INFO } from './database-field';
|
||||
export { SYSTEM_FIELDS, SYSTEM_FIELD_ROW_INDEX } from './database-field';
|
||||
export { PLATFORM_FIELD } from './database-field';
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export const DatabaseDetailWaring = () => <></>;
|
||||
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { type DatabaseInfo } from '@coze-studio/bot-detail-store';
|
||||
import {
|
||||
type AlterBotTableResponse,
|
||||
type InsertBotTableResponse,
|
||||
} from '@coze-arch/bot-api/memory';
|
||||
|
||||
export type OnSave = (params: {
|
||||
response: InsertBotTableResponse | AlterBotTableResponse;
|
||||
}) => Promise<void>;
|
||||
|
||||
/* eslint-disable @typescript-eslint/naming-convention -- 历史文件拷贝 */
|
||||
export enum CreateType {
|
||||
custom = 'custom',
|
||||
template = 'template',
|
||||
excel = 'excel',
|
||||
// 推荐建表
|
||||
recommend = 'recommend',
|
||||
// 输入自然语言建表
|
||||
naturalLanguage = 'naturalLanguage',
|
||||
}
|
||||
/* eslint-enable @typescript-eslint/naming-convention -- 历史文件拷贝 */
|
||||
|
||||
export interface MapperItem {
|
||||
label: string;
|
||||
key: string;
|
||||
validator: {
|
||||
type: VerifyType;
|
||||
message: string;
|
||||
}[];
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- 历史文件拷贝
|
||||
defaultValue: any;
|
||||
require: boolean;
|
||||
}
|
||||
|
||||
export type TableBasicInfo = Pick<
|
||||
DatabaseInfo,
|
||||
'name' | 'desc' | 'readAndWriteMode'
|
||||
> & { prompt_disabled: boolean };
|
||||
export type TableFieldsInfo = DatabaseInfo['tableMemoryList'];
|
||||
|
||||
export enum VerifyType {
|
||||
Required = 1,
|
||||
Unique = 2,
|
||||
Naming = 3,
|
||||
}
|
||||
|
||||
export type TriggerType = 'blur' | 'change' | 'save';
|
||||
|
||||
export interface NL2DBInfo {
|
||||
prompt: string;
|
||||
}
|
||||
|
||||
export type ReadAndWriteModeOptions = 'excel' | 'normal' | 'expert';
|
||||
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* 数据库详情页 tab
|
||||
*/
|
||||
export enum DatabaseTabs {
|
||||
/** 表结构 */
|
||||
Structure = 'structure',
|
||||
/** 测试数据 */
|
||||
Draft = 'draft',
|
||||
/** 线上数据 */
|
||||
Online = 'online',
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { DatabaseTabs } from './database-tabs';
|
||||
export { CreateType } from './database-field';
|
||||
export { type NL2DBInfo } from './database-field';
|
||||
export { type OnSave } from './database-field';
|
||||
17
frontend/packages/data/memory/database-v2-base/src/typings.d.ts
vendored
Normal file
17
frontend/packages/data/memory/database-v2-base/src/typings.d.ts
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/// <reference types='@coze-arch/bot-typings' />
|
||||
@@ -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 { REPORT_EVENTS as ReportEventNames } from '@coze-arch/report-events';
|
||||
import { CustomError } from '@coze-arch/bot-error';
|
||||
|
||||
export const getBase64 = (file: Blob): Promise<string> =>
|
||||
new Promise((resolve, reject) => {
|
||||
const fileReader = new FileReader();
|
||||
fileReader.onload = event => {
|
||||
const result = event.target?.result;
|
||||
if (!result || typeof result !== 'string') {
|
||||
reject(
|
||||
new CustomError(ReportEventNames.parmasValidation, 'file read fail'),
|
||||
);
|
||||
return;
|
||||
}
|
||||
resolve(result.replace(/^.*?,/, ''));
|
||||
};
|
||||
fileReader.readAsDataURL(file);
|
||||
});
|
||||
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export const getFileExtension = (name: string) => {
|
||||
const index = name.lastIndexOf('.');
|
||||
return name.slice(index + 1);
|
||||
};
|
||||
Reference in New Issue
Block a user