feat: manually mirror opencoze's code from bytedance

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

View File

@@ -0,0 +1,7 @@
const { defineConfig } = require('@coze-arch/eslint-config');
module.exports = defineConfig({
packageRoot: __dirname,
preset: 'web',
rules: {},
});

View File

@@ -0,0 +1,31 @@
import { mergeConfig } from 'vite';
import svgr from 'vite-plugin-svgr';
/** @type { import('@storybook/react-vite').StorybookConfig } */
const config = {
stories: ['../stories/**/*.mdx', '../stories/**/*.stories.tsx'],
addons: [
'@storybook/addon-links',
'@storybook/addon-essentials',
'@storybook/addon-onboarding',
'@storybook/addon-interactions',
],
framework: {
name: '@storybook/react-vite',
options: {},
},
docs: {
autodocs: 'tag',
},
viteFinal: config =>
mergeConfig(config, {
plugins: [
svgr({
svgrOptions: {
native: false,
},
}),
],
}),
};
export default config;

View File

@@ -0,0 +1,14 @@
/** @type { import('@storybook/react').Preview } */
const preview = {
parameters: {
actions: { argTypesRegex: "^on[A-Z].*" },
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/i,
},
},
},
};
export default preview;

View File

@@ -0,0 +1,5 @@
const { defineConfig } = require('@coze-arch/stylelint-config');
module.exports = defineConfig({
extends: [],
});

View File

@@ -0,0 +1,16 @@
# @coze-data/database-v2
> Project template for react component with storybook.
## Features
- [x] eslint & ts
- [x] esm bundle
- [x] umd bundle
- [x] storybook
## Commands
- init: `rush update`
- dev: `npm run dev`
- build: `npm run build`

View File

@@ -0,0 +1,12 @@
{
"operationSettings": [
{
"operationName": "test:cov",
"outputFolderNames": ["coverage"]
},
{
"operationName": "ts-check",
"outputFolderNames": ["./dist"]
}
]
}

View File

@@ -0,0 +1,7 @@
const { defineConfig } = require('@coze-arch/eslint-config');
module.exports = defineConfig({
packageRoot: __dirname,
preset: 'web',
rules: {},
});

View File

@@ -0,0 +1,80 @@
{
"name": "@coze-data/database-v2",
"version": "0.0.1",
"description": "database v2",
"license": "Apache-2.0",
"author": "liushuoyan@bytedance.com",
"maintainers": [],
"main": "src/index.tsx",
"scripts": {
"build": "exit 0",
"lint": "eslint ./ --cache",
"test": "vitest --run --passWithNoTests",
"test:cov": "npm run test -- --coverage"
},
"dependencies": {
"@coze-arch/bot-api": "workspace:*",
"@coze-arch/bot-error": "workspace:*",
"@coze-arch/bot-http": "workspace:*",
"@coze-arch/bot-icons": "workspace:*",
"@coze-arch/bot-semi": "workspace:*",
"@coze-arch/bot-studio-store": "workspace:*",
"@coze-arch/bot-tea": "workspace:*",
"@coze-arch/bot-utils": "workspace:*",
"@coze-arch/coze-design": "0.0.6-alpha.346d77",
"@coze-arch/i18n": "workspace:*",
"@coze-arch/report-events": "workspace:*",
"@coze-common/table-view": "workspace:*",
"@coze-data/database-v2-adapter": "workspace:*",
"@coze-data/database-v2-base": "workspace:*",
"@coze-data/e2e": "workspace:*",
"@coze-data/knowledge-resource-processor-base": "workspace:*",
"@coze-data/knowledge-resource-processor-core": "workspace:*",
"@coze-data/knowledge-stores": "workspace:*",
"@coze-data/reporter": "workspace:*",
"@coze-data/utils": "workspace:*",
"@coze-studio/bot-detail-store": "workspace:*",
"@coze-studio/components": "workspace:*",
"@coze-studio/user-store": "workspace:*",
"@dnd-kit/core": "^6.0.8",
"@dnd-kit/modifiers": "^7.0.0",
"@dnd-kit/sortable": "^7.0.2",
"@dnd-kit/utilities": "^3.2.1",
"@douyinfe/semi-icons": "^2.36.0",
"@douyinfe/semi-illustrations": "^2.36.0",
"@douyinfe/semi-ui": "~2.72.3",
"ahooks": "^3.7.8",
"classnames": "^2.3.2",
"date-fns": "^2.23.0",
"dayjs": "^1.11.7",
"immer": "^10.0.3",
"lodash-es": "^4.17.21",
"nanoid": "^4.0.2",
"react-router-dom": "^6.11.1"
},
"devDependencies": {
"@coze-arch/bot-typings": "workspace:*",
"@coze-arch/eslint-config": "workspace:*",
"@coze-arch/stylelint-config": "workspace:*",
"@coze-arch/ts-config": "workspace:*",
"@coze-arch/vitest-config": "workspace:*",
"@testing-library/jest-dom": "^6.1.5",
"@testing-library/react": "^14.1.2",
"@testing-library/react-hooks": "^8.0.1",
"@types/lodash-es": "^4.17.10",
"@types/react": "18.2.37",
"@types/react-dom": "18.2.15",
"@vitest/coverage-v8": "~3.0.5",
"react": "~18.2.0",
"react-dom": "~18.2.0",
"stylelint": "^15.11.0",
"typescript": "~5.8.2",
"vite-plugin-svgr": "~3.3.0",
"vitest": "~3.0.5"
},
"peerDependencies": {
"react": ">=18.2.0",
"react-dom": ">=18.2.0"
}
}

View File

@@ -0,0 +1,12 @@
<svg width="24" height="25" viewBox="0 0 24 25" fill="current" xmlns="http://www.w3.org/2000/svg">
<g id="Import Excel/CSV">
<g id="Union">
<path
d="M7 13C7 12.4477 7.44772 12 8 12H16C16.5523 12 17 12.4477 17 13C17 13.5523 16.5523 14 16 14H8C7.44772 14 7 13.5523 7 13Z" />
<path
d="M7 18C7 17.4477 7.44772 17 8 17H13C13.5523 17 14 17.4477 14 18C14 18.5523 13.5523 19 13 19H8C7.44772 19 7 18.5523 7 18Z" />
<path
d="M5 1.5C3.89543 1.5 3 2.39543 3 3.5V22.5C3 23.6046 3.89543 24.5 5 24.5H19C20.1046 24.5 21 23.6046 21 22.5V8.05251C21 7.53746 20.8013 7.04227 20.4453 6.67007L16.0907 2.11755C15.7134 1.7231 15.1913 1.5 14.6454 1.5H5ZM5 22.5V3.5L14.5 3.5V7C14.5 7.55228 14.9477 8 15.5 8H18.9498L19 8.05252V22.5H5Z" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 783 B

View File

@@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M12.3775 2.01277L14.4504 4.08571C15.0228 4.65813 15.0228 5.58622 14.4504 6.15864L6.15864 14.4504C5.58622 15.0228 4.65813 15.0228 4.08571 14.4504L2.01277 12.3775C1.44034 11.805 1.44034 10.8769 2.01277 10.3045L10.3045 2.01277C10.8769 1.44034 11.805 1.44034 12.3775 2.01277ZM6.11644 0.841895C6.18123 0.637199 6.47089 0.637199 6.53568 0.841895L6.74973 1.51819C6.77133 1.58642 6.82478 1.63987 6.89301 1.66146L7.5693 1.87552C7.774 1.94031 7.774 2.22997 7.5693 2.29476L6.89301 2.50881C6.82478 2.53041 6.77133 2.58386 6.74973 2.65208L6.53568 3.32838C6.47089 3.53308 6.18123 3.53308 6.11644 3.32838L5.90239 2.65208C5.88079 2.58386 5.82734 2.53041 5.75912 2.50881L5.08282 2.29476C4.87812 2.22997 4.87812 1.94031 5.08282 1.87552L5.75912 1.66146C5.82734 1.63987 5.88079 1.58642 5.90239 1.51819L6.11644 0.841895ZM1.85997 3.23387L2.22567 2.16297C2.30246 1.9381 2.6205 1.9381 2.6973 2.16297L3.06299 3.23387C3.08793 3.30688 3.14527 3.36423 3.21828 3.38916L4.28918 3.75486C4.51406 3.83165 4.51406 4.14969 4.28918 4.22648L3.21828 4.59218C3.14527 4.61711 3.08793 4.67446 3.06299 4.74747L2.6973 5.81837C2.6205 6.04324 2.30246 6.04324 2.22567 5.81837L1.85997 4.74747C1.83504 4.67446 1.7777 4.61711 1.70468 4.59218L0.633785 4.22648C0.40891 4.14969 0.40891 3.83165 0.633785 3.75486L1.70468 3.38916C1.7777 3.36423 1.83504 3.30688 1.85997 3.23387ZM13.4837 9.6851C13.5605 9.46022 13.8785 9.46022 13.9553 9.6851L14.321 10.756C14.3459 10.829 14.4033 10.8864 14.4763 10.9113L15.5472 11.277C15.7721 11.3538 15.7721 11.6718 15.5472 11.7486L14.4763 12.1143C14.4033 12.1392 14.3459 12.1966 14.321 12.2696L13.9553 13.3405C13.8785 13.5654 13.5605 13.5654 13.4837 13.3405L13.118 12.2696C13.093 12.1966 13.0357 12.1392 12.9627 12.1143L11.8918 11.7486C11.6669 11.6718 11.6669 11.3538 11.8918 11.277L12.9627 10.9113C13.0357 10.8864 13.093 10.829 13.118 10.756L13.4837 9.6851ZM10.5629 4.34447C10.9922 3.91515 11.6882 3.91515 12.1176 4.34447C12.5469 4.77379 12.5469 5.46985 12.1176 5.89917L10.0446 7.97211C9.6153 8.40143 8.91924 8.40143 8.48992 7.97211C8.0606 7.54279 8.0606 6.84673 8.48992 6.41741L10.5629 4.34447Z" fill="#4D53E8"/>
</svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@@ -0,0 +1,6 @@
<svg width="24" height="25" viewBox="0 0 24 25" fill="current" xmlns="http://www.w3.org/2000/svg">
<g id="icon_add_outlined">
<path id="Union"
d="M12 2.5C11.4477 2.5 11 2.94772 11 3.5V11.5H3C2.44772 11.5 2 11.9477 2 12.5C2 13.0523 2.44772 13.5 3 13.5H11V21.5C11 22.0523 11.4477 22.5 12 22.5C12.5523 22.5 13 22.0523 13 21.5V13.5H21C21.5523 13.5 22 13.0523 22 12.5C22 11.9477 21.5523 11.5 21 11.5H13V3.5C13 2.94772 12.5523 2.5 12 2.5Z" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 463 B

View File

@@ -0,0 +1,5 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M4.8149 6.5L11.1851 6.5C11.4362 6.5 11.586 6.73292 11.4467 6.90682L8.26158 10.8835C8.13714 11.0388 7.86286 11.0388 7.73842 10.8835L4.55333 6.90682C4.41405 6.73292 4.56381 6.5 4.8149 6.5Z"
fill="currentColor" />
</svg>

After

Width:  |  Height:  |  Size: 333 B

View File

@@ -0,0 +1,5 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M11.1851 9.5L4.8149 9.5C4.56381 9.5 4.41405 9.26708 4.55333 9.09318L7.73842 5.11652C7.86286 4.96116 8.13714 4.96116 8.26158 5.11652L11.4467 9.09318C11.586 9.26708 11.4362 9.5 11.1851 9.5Z"
fill="currentColor" />
</svg>

After

Width:  |  Height:  |  Size: 334 B

View File

@@ -0,0 +1,11 @@
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="icon_wiki-excel_colorful">
<g id="Group 1321320754">
<path id="Rectangle 2530" d="M4 4.0186C4 2.53559 5.19334 1.33337 6.66541 1.33337H19.4404C19.7939 1.33337 20.1328 1.47483 20.3828 1.72662L27.5983 8.99581C27.8482 9.2476 27.9886 9.5891 27.9886 9.94518V28.1856C27.9886 29.6686 26.7953 30.8708 25.3232 30.8708H6.66541C5.19334 30.8708 4 29.6686 4 28.1856V4.0186Z" fill="#32A645"/>
<path id="csv" d="M12.5336 22.6041L12.5395 22.5629L12.5567 22.4425H12.4351H11.4262H11.3438L11.3238 22.5224L11.312 22.5696C11.2315 22.8919 11.0661 23.1596 10.8265 23.347C10.587 23.5342 10.267 23.6461 9.86853 23.6461C9.36786 23.6461 8.95012 23.4484 8.6554 23.0849C8.35894 22.7191 8.18038 22.177 8.18038 21.4801V21.4742C8.18038 20.7949 8.35456 20.2506 8.64815 19.8786C8.93993 19.5089 9.35476 19.3022 9.85673 19.3022C10.2858 19.3022 10.6109 19.4307 10.8462 19.626C11.0824 19.8221 11.2341 20.0904 11.3061 20.3787L11.3179 20.4259L11.3379 20.5058H11.4203H12.4233H12.5428L12.528 20.3873L12.5221 20.3402C12.3858 19.2377 11.4387 18.1826 9.85673 18.1826C8.97717 18.1826 8.23953 18.503 7.72274 19.0791C7.20694 19.6541 6.91917 20.4746 6.91917 21.4624V21.4683C6.91917 22.4641 7.2006 23.2892 7.71512 23.8668C8.23082 24.4457 8.97178 24.7657 9.86853 24.7657C11.3037 24.7657 12.3606 23.8281 12.5336 22.6041Z" fill="white" stroke="white" stroke-width="0.210982"/>
<path id="csv_2" d="M13.5353 22.837L13.5354 22.8371C13.6003 23.4122 13.8628 23.8975 14.3012 24.2377C14.7384 24.5768 15.3426 24.7657 16.0828 24.7657C17.4813 24.7657 18.572 23.9883 18.572 22.8312V22.8253C18.572 22.369 18.454 21.9985 18.1623 21.7045C17.875 21.4148 17.4306 21.2111 16.804 21.0589L16.8039 21.0589L15.8305 20.823C15.8305 20.823 15.8305 20.8229 15.8304 20.8229C15.4997 20.7424 15.2808 20.6347 15.1455 20.5072C15.0143 20.3835 14.9552 20.2351 14.9552 20.0522V20.0463C14.9552 19.808 15.0547 19.6134 15.2365 19.4754C15.4211 19.3354 15.6973 19.2491 16.0533 19.2491C16.4159 19.2491 16.695 19.3431 16.8913 19.4985C17.0869 19.6532 17.2089 19.8754 17.2469 20.1493L17.2469 20.1498L17.2528 20.1911L17.2658 20.2816H17.3573H18.3367H18.4506L18.4419 20.168L18.436 20.0916C18.436 20.0915 18.436 20.0915 18.436 20.0915C18.3544 18.9994 17.4427 18.1826 16.0533 18.1826C14.6903 18.1826 13.694 18.9588 13.694 20.0935V20.0994C13.694 20.5669 13.8432 20.9506 14.1504 21.2522C14.4542 21.5505 14.9042 21.7603 15.4917 21.9012C15.4918 21.9012 15.4918 21.9013 15.4918 21.9013L16.4648 22.1371C16.4648 22.1371 16.4649 22.1372 16.465 22.1372C16.8074 22.2213 17.0169 22.3206 17.1412 22.4397C17.2598 22.5535 17.3108 22.6954 17.3108 22.8961V22.902C17.3108 23.1432 17.2051 23.3369 17.0048 23.4744C16.8004 23.6146 16.4919 23.6992 16.0887 23.6992C15.684 23.6992 15.3847 23.6122 15.1742 23.4622C14.9656 23.3134 14.8336 23.0952 14.7761 22.8103L14.7643 22.7515L14.7474 22.6667H14.6609H13.6343H13.5164L13.5294 22.7839L13.5353 22.837Z" fill="white" stroke="white" stroke-width="0.210982"/>
<path id="csv_3" d="M21.5641 24.5781L21.5895 24.6472H21.6631H22.7133H22.7869L22.8123 24.578L25.0662 18.4419L25.1183 18.3H24.9672H23.8992H23.8235L23.7993 18.3718L22.1883 23.1543L20.5831 18.3719L20.5589 18.3H20.483H19.4033H19.2521L19.3043 18.442L21.5641 24.5781Z" fill="white" stroke="white" stroke-width="0.210982"/>
<path id="Rectangle 2531" opacity="0.8" d="M19.9802 1.97637C19.9802 1.73986 20.2666 1.62187 20.4332 1.78972L27.5282 8.93745C27.6941 9.10464 27.5757 9.38905 27.3401 9.38905H22.6456C21.1736 9.38905 19.9802 8.18683 19.9802 6.70382V1.97637Z" fill="#278B34"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.5 KiB

View File

@@ -0,0 +1,7 @@
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="icon_wiki-excel_colorful">
<path id="Rectangle 2530" d="M4 4.00004C4 2.52728 5.19391 1.33337 6.66667 1.33337H19.4477C19.8013 1.33337 20.1405 1.47385 20.3905 1.7239L27.6095 8.94285C27.8595 9.1929 28 9.53204 28 9.88566V28C28 29.4728 26.8061 30.6667 25.3333 30.6667H6.66667C5.19391 30.6667 4 29.4728 4 28V4.00004Z" fill="#32A645"/>
<path id="Rectangle 2531" opacity="0.8" d="M20 1.97716C20 1.73959 20.2872 1.62061 20.4552 1.7886L27.5448 8.87815C27.7128 9.04614 27.5938 9.33337 27.3562 9.33337H22.6667C21.1939 9.33337 20 8.13947 20 6.66671V1.97716Z" fill="#258832"/>
<path id="icon_file_excel_nor" d="M11.3955 13.2123H12.8513C12.9158 13.2123 12.9762 13.244 13.0129 13.2971L15.811 17.352L18.6239 13.2967C18.6606 13.2438 18.7208 13.2123 18.7852 13.2123H20.2408C20.3492 13.2123 20.4371 13.3002 20.4371 13.4086C20.4371 13.4495 20.4244 13.4894 20.4007 13.5226L16.7567 18.6342L20.6906 24.175C20.7534 24.2634 20.7326 24.386 20.6441 24.4488C20.6109 24.4723 20.5712 24.485 20.5305 24.485H19.0748C19.0103 24.485 18.95 24.4534 18.9133 24.4004L15.811 19.9165L12.7234 24.4C12.6868 24.4532 12.6263 24.485 12.5617 24.485H11.1058C10.9973 24.485 10.9094 24.3971 10.9094 24.2886C10.9094 24.2481 10.9219 24.2086 10.9453 24.1755L14.8499 18.6342L11.2351 13.522C11.1725 13.4335 11.1935 13.3109 11.2821 13.2483C11.3152 13.2249 11.3548 13.2123 11.3955 13.2123Z" fill="white"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

View File

@@ -0,0 +1,50 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import 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>
);
}

View File

@@ -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>
);
}

View File

@@ -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 ?? '-',
},
];
}

View File

@@ -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)}
/>
);
}

View File

@@ -0,0 +1,147 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { 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"
/>
</>
);
}

View File

@@ -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,
}}
/>,
],
})}
/>
</>
);
}

View File

@@ -0,0 +1,34 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { 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} />;
},
);

View File

@@ -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%;
}

View File

@@ -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)}
/>
</>
);
};

View File

@@ -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;
}
}

View File

@@ -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>
);
};

View File

@@ -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>
);
}

View File

@@ -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;
};

View File

@@ -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);
}
}
}

View File

@@ -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>
);
}

View File

@@ -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}
/>
);
};

View File

@@ -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,
},
],
];

View File

@@ -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>
);
}

View File

@@ -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;
}

View File

@@ -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;
}
}
}
}

View File

@@ -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>
);
}

View File

@@ -0,0 +1,11 @@
.date {
:global {
.semi-select:hover {
@apply coz-bg-max !important;
}
.coz-date-picker-select {
width: 100%;
}
}
}

View File

@@ -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}
/>
);
};

View File

@@ -0,0 +1,7 @@
.input {
:global {
.semi-input-wrapper,.semi-input-wrapper:hover {
@apply coz-bg-max !important;
}
}
}

View File

@@ -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}
/>
);
};

View File

@@ -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"
/>
);
};

View File

@@ -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,
}}
/>
);
};

View File

@@ -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,
},
};
}

View File

@@ -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%;
}
}

View File

@@ -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()}</>;
};

View File

@@ -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>
);
};

View File

@@ -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;

View File

@@ -0,0 +1,81 @@
/*
* 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 { useParams, useSearchParams } from 'react-router-dom';
import { useRequest } from 'ahooks';
import { type DynamicParams } from '@coze-arch/bot-typings/teamspace';
import { MemoryApi } from '@coze-arch/bot-api';
/**
* 已经迁移的旧渠道 id 到新渠道 id 的映射
* key(旧) -> value(新)
*/
const migratedConnectorIds: Record<string, string | undefined> = {
// 微信服务号
'10000114': '10000120',
// 微信订阅号
'10000115': '10000121',
};
export interface ConnectorOption {
label: string;
value: string;
/** 该渠道 id 是否已经迁移 */
migrated?: boolean;
}
export interface UseConnectorOptionsParams {
/** 是否包含已迁移的旧渠道,默认不包含 */
includeMigrated?: boolean;
}
export function useConnectorOptions({
includeMigrated = false,
}: UseConnectorOptionsParams = {}): ConnectorOption[] {
const { space_id } = useParams<DynamicParams>();
// 资源库 workflow 页面的 url 上没有 space_id 参数,需要从 searchParams 中获取
const [searchParams] = useSearchParams();
const spaceId = space_id ?? searchParams.get('space_id') ?? '';
const { data } = useRequest(
async () => {
const res = await MemoryApi.GetConnectorName({
SpaceId: spaceId,
Version: IS_RELEASE_VERSION ? 'release' : 'inhouse',
ListAll: true,
});
const connectors = res.ConnectorList;
return connectors?.map(i => {
const value = i.ConnectorID?.toString() ?? '';
if (migratedConnectorIds[value]) {
const target = connectors.find(
j => j.ConnectorID?.toString() === migratedConnectorIds[value],
);
if (target?.ConnectorName) {
return { label: target.ConnectorName, value, migrated: true };
}
}
return { label: i.ConnectorName ?? '', value };
});
},
{
refreshDeps: [spaceId],
// 设置缓存 key, 防止重复请求
cacheKey: `db_connector_name_${spaceId}`,
},
);
return (includeMigrated ? data : data?.filter(c => !c.migrated)) ?? [];
}

View File

@@ -0,0 +1,85 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { useEffect, useState } from 'react';
import { DataNamespace, dataReporter } from '@coze-data/reporter';
import { REPORT_EVENTS } from '@coze-arch/report-events';
import {
type GetModeConfigResponse,
BotTableRWMode,
} from '@coze-arch/bot-api/memory';
import { MemoryApi } from '@coze-arch/bot-api';
export interface ExpertModeConfig {
isExpertMode: boolean;
maxTableNum: number;
maxColumnNum: number;
readAndWriteModes: BotTableRWMode[];
}
export const useExpertModeConfig = (params: {
botId: string;
}): ExpertModeConfig => {
const { botId } = params;
const defaultConfig = {
isExpertMode: false,
maxTableNum: 1,
maxColumnNum: 10,
readAndWriteModes: [BotTableRWMode.LimitedReadWrite],
};
const [expertConfig, setExpertConfig] =
useState<ExpertModeConfig>(defaultConfig);
useEffect(() => {
(async () => {
if (!botId) {
return;
}
let res: GetModeConfigResponse | undefined;
try {
res = await MemoryApi.GetModeConfig({
bot_id: botId,
});
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- 复制历史文件
} catch (error: any) {
dataReporter.errorEvent(DataNamespace.DATABASE, {
eventName: REPORT_EVENTS.DatabaseGetExpertConfig,
error,
});
}
if (res) {
const result: ExpertModeConfig = {
isExpertMode: res.mode === 'expert',
maxColumnNum: Number(res.max_column_num),
maxTableNum: Number(res.max_table_num),
readAndWriteModes:
Number(res.max_table_num) > 1
? [
BotTableRWMode.LimitedReadWrite,
BotTableRWMode.UnlimitedReadWrite,
]
: defaultConfig.readAndWriteModes,
};
setExpertConfig(result);
}
})();
}, [botId]);
return expertConfig;
};

View File

@@ -0,0 +1,27 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { useRef } from 'react';
import { type TableData } from '../components/database-table-data/type';
export const useGetTableInstantaneousData = (tableData: TableData) => {
// 缓存 Data 数据,用于在事件中获取数据
const dataRef = useRef<TableData>(tableData);
dataRef.current = tableData;
return () => dataRef.current;
};

View File

@@ -0,0 +1,169 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { useRef, useState, useMemo } from 'react';
import { userStoreService } from '@coze-studio/user-store';
import { type DatabaseInfo } from '@coze-studio/bot-detail-store';
import { useDataNavigate } from '@coze-data/knowledge-stores';
import { ModalMode } from '@coze-data/database-v2-base/components/base-info-modal';
import { useDatabaseCreateTableModal } from '@coze-data/database-v2-adapter/components/create-table-modal';
import {
useDatabaseInfoModal,
type FormData,
} from '@coze-data/database-v2-adapter/components/base-info-modal';
import { useSpaceStore } from '@coze-arch/bot-studio-store';
import {
BotTableRWMode,
type SingleDatabaseResponse,
} from '@coze-arch/bot-api/memory';
export const enum Step {
BASE_INFO = 0,
CREATE_TABLE = 1,
}
export const useLibraryCreateDatabaseModal = ({
projectID,
onFinish,
}: {
projectID?: string;
onFinish?: (databaseID: string, draftId: string) => void;
enterFrom?: 'library' | 'project';
}) => {
const step = useRef<Step>(Step.BASE_INFO);
const resourceNavigate = useDataNavigate();
const spaceId = useSpaceStore(store => store.getSpaceId());
const userId = userStoreService.useUserInfo()?.user_id_str;
const [databaseBaseInfo, setDatabaseBaseInfo] = useState<FormData>({
name: '',
description: '',
icon_uri: [
{
uri: '',
url: '',
uid: '',
},
],
});
const tableInitData: DatabaseInfo = useMemo(
() => ({
tableId: '',
name: databaseBaseInfo?.name,
desc: databaseBaseInfo?.description,
icon_uri: databaseBaseInfo.icon_uri?.[0]?.uri,
readAndWriteMode: BotTableRWMode.LimitedReadWrite,
tableMemoryList: [],
}),
[databaseBaseInfo],
);
const handleBaseInfoSubmit = (data: FormData) => {
setDatabaseBaseInfo(data);
step.current = Step.CREATE_TABLE;
closeDatabaseInfoModal();
open();
};
const handleCreateTableSubmit = (createRes: SingleDatabaseResponse) => {
const { id, draft_id } = createRes.database_info ?? {};
if (id && draft_id) {
if (onFinish) {
// bot 绑定数据库需要 draft_id ,其他场景一般只需要用 id
onFinish(id, draft_id);
return;
} else {
resourceNavigate.toResource?.('database', id, {
page_modal: 'normal',
from: 'create',
});
close();
}
}
};
// onReturn
const handleCreateTableModalClose = () => {
step.current = Step.BASE_INFO;
open();
closeCreateTableModal();
};
// onClose
const handleCloseCreateTable = () => {
console.log('open');
step.current = Step.BASE_INFO;
open();
};
const close = () => {
closeDatabaseInfoModal();
closeCreateTableModal();
step.current = Step.BASE_INFO;
};
const {
modal: databaseInfoModal,
open: openDatabaseInfoModal,
close: closeDatabaseInfoModal,
} = useDatabaseInfoModal({
onSubmit: handleBaseInfoSubmit,
initValues: databaseBaseInfo,
mode: ModalMode.CREATE,
});
const {
modal: createTableModal,
open: openCreateTableModal,
close: closeCreateTableModal,
} = useDatabaseCreateTableModal({
onClose: handleCloseCreateTable,
onReturn: handleCreateTableModalClose,
onSubmit: handleCreateTableSubmit,
initValue: tableInitData,
showDatabaseBaseInfo: true,
onlyShowDatabaseInfoRWMode: true,
extraParams: {
spaceId,
creatorId: userId,
},
projectID,
});
const open = () => {
if (step.current === Step.BASE_INFO) {
openDatabaseInfoModal();
} else {
openCreateTableModal();
}
};
const modal = (
<>
{databaseInfoModal}
{createTableModal}
</>
);
return {
modal,
open,
close,
};
};

View File

@@ -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 { useBlocker } from 'react-router-dom';
import { I18n } from '@coze-arch/i18n';
import { Modal } from '@coze-arch/coze-design';
export const useRouteLeavingGuard = (when: boolean) => {
const blocker = useBlocker(
({ currentLocation, nextLocation }) =>
when && currentLocation.pathname !== nextLocation.pathname,
);
const modal = (
<Modal
title={I18n.t('db2_027')}
visible={blocker.state === 'blocked'}
onOk={() => blocker.proceed?.()}
onCancel={() => blocker.reset?.()}
okText={I18n.t('db2_004')}
cancelText={I18n.t('db2_028')}
closeOnEsc={true}
>
{I18n.t('db2_029')}
</Modal>
);
return {
modal,
};
};

View File

@@ -0,0 +1,40 @@
/*
* 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 { produce } from 'immer';
import {
RowServiceStatus,
type TableData,
} from '../components/database-table-data/type';
export const useTableData = (_tableData: TableData) => {
const [tableData, setTableData] = useState(_tableData);
const filteredTableData = useMemo(
() =>
produce(tableData, draft => {
draft.dataList = draft.dataList.filter(
item => item.status !== RowServiceStatus.Deleted,
);
}),
[tableData],
);
return { tableData: filteredTableData, setTableData };
};

View File

@@ -0,0 +1,25 @@
/*
* 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 { useLibraryCreateDatabaseModal } from './hooks/use-library-create-database-modal';
export {
SelectDatabaseModal,
useSelectDatabaseModal,
} from './components/select-database-modal';
export { DatabaseCreateTableModal } from '@coze-data/database-v2-base/components/create-table-modal';
export { DatabaseTabs } from '@coze-data/database-v2-base/types';
export { DatabaseDetail } from './pages/database';
export { DatabaseDetail as DatabaseDetailComponent } from './components/database-detail';

View File

@@ -0,0 +1,56 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import { useKnowledgeParams } from '@coze-data/knowledge-stores';
import { type DatabaseTabs } from '@coze-data/database-v2-base/types';
import { useSpaceStore } from '@coze-arch/bot-studio-store';
import { DatabaseInner } from '../library';
export interface DatabaseDetailProps {
needHideCloseIcon?: boolean;
initialTab?: DatabaseTabs;
version?: string;
}
export const DatabaseDetail = ({
version,
needHideCloseIcon,
initialTab,
}: DatabaseDetailProps) => {
const params = useKnowledgeParams();
const { botID, tableID, biz } = params;
const spaceId = useSpaceStore(store => store.getSpaceId());
if (!tableID) {
return <div>no database id!</div>;
// return null;
}
return (
<DatabaseInner
version={version}
botId={botID ?? ''}
databaseId={tableID}
needHideCloseIcon={needHideCloseIcon}
enterFrom={biz ?? ''}
spaceId={spaceId ?? ''}
initialTab={initialTab ?? (params.initialTab as DatabaseTabs)}
/>
);
};

View File

@@ -0,0 +1,125 @@
/*
* 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 { useNavigate } from 'react-router-dom';
import React, { useMemo, useState } from 'react';
import { useDataCallbacks } from '@coze-data/knowledge-stores';
import { type DatabaseTabs } from '@coze-data/database-v2-base/types';
import { I18n } from '@coze-arch/i18n';
import { MemoryApi } from '@coze-arch/bot-api';
import { Toast } from '@coze-arch/coze-design';
import { DatabaseDetail } from '../../components/database-detail';
interface IProps {
version?: string;
botId: string;
spaceId?: string;
databaseId: string;
enterFrom: string;
needHideCloseIcon?: boolean;
initialTab?: DatabaseTabs;
}
export const DatabaseInner = ({
version,
botId,
spaceId,
databaseId,
enterFrom,
needHideCloseIcon,
initialTab,
}: IProps) => {
const { onStatusChange, onUpdateDisplayName } = useDataCallbacks();
const navigate = useNavigate();
const [actionText, setActionText] = useState<string>(
enterFrom === 'bot_add' ? 'Add' : 'Remove',
);
const handleAddDatabase = async (id: string) => {
const res = await MemoryApi.BindDatabase({
database_id: id,
bot_id: botId,
});
if (res.code === 0) {
Toast.success('Add database success');
setActionText('Remove');
} else {
Toast.error(res.msg);
}
};
const handleRemoveDatabase = async (id: string) => {
const res = await MemoryApi.UnBindDatabase({
database_id: id,
bot_id: botId,
});
if (res.code === 0) {
Toast.success('Remove database success');
setActionText('Add');
} else {
Toast.error(res.msg);
}
};
const handleAddRemoveDatabase = (id?: string) => {
if (!id) {
return;
}
if (actionText === 'Add') {
handleAddDatabase(id);
} else if (actionText === 'Remove') {
handleRemoveDatabase(id);
} else {
return;
}
};
const handleClose = () => {
if (window.history.length === 1) {
navigate(`/space/${spaceId}/library`);
}
navigate(-1);
};
const addRemoveButtonText = useMemo(() => {
if (actionText === 'Add') {
return I18n.t('db2_030');
} else if (actionText === 'Remove') {
return I18n.t('db2_031');
} else {
return '';
}
}, [actionText]);
return (
<DatabaseDetail
version={version}
enterFrom={enterFrom}
databaseId={databaseId}
initialTab={initialTab}
addRemoveButtonText={addRemoveButtonText}
onIDECallback={{
onStatusChange,
onUpdateDisplayName,
}}
onClickAddRemoveButton={handleAddRemoveDatabase}
needHideCloseIcon={needHideCloseIcon}
onClose={handleClose}
/>
);
};

View File

@@ -0,0 +1,17 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/// <reference types='@coze-arch/bot-typings' />

View File

@@ -0,0 +1,23 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const simulateFetch = (v: any) =>
new Promise(resolve => {
setTimeout(() => {
resolve(v);
}, 2000);
});

View File

@@ -0,0 +1,33 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { format } from 'date-fns';
import { FieldItemType } from '@coze-arch/bot-api/memory';
export const getDefaultValue = (type: FieldItemType) => {
if (type === FieldItemType.Boolean) {
return false;
} else if ([FieldItemType.Number, FieldItemType.Float].includes(type)) {
return 0;
} else if (type === FieldItemType.Text) {
return '';
} else if (type === FieldItemType.Date) {
// TODO: @liushuoyan 这里可能存在时区的问题,联调的时候请注意
return format(new Date(), 'yyyy-MM-dd HH:mm:ss');
} else {
return undefined;
}
};

View File

@@ -0,0 +1,18 @@
/*
* 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-next-line @typescript-eslint/no-explicit-any -- 符合预期
export const isEmptyValue = (value: any) => value === '' || value === undefined;

View File

@@ -0,0 +1,48 @@
/*
* 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.
*/
// int64 的最大值和最小值
export const INT64_MAX = BigInt('9223372036854775807');
export const INT64_MIN = BigInt('-9223372036854775808');
/**
* 检查数值是否在 int64 范围内
* @param value - 要检查的字符串
* @returns
* - 如果是有效的 int64 范围内的整数,返回 true
* - 如果无效或超出范围,返回 false
*/
export const isInInt64Range = (value: string): boolean => {
if (
value === '' ||
value === undefined ||
value === null ||
Number.isNaN(value)
) {
return false;
}
try {
const bigIntValue = BigInt(value);
if (bigIntValue > INT64_MAX || bigIntValue < INT64_MIN) {
return false;
}
return true;
// eslint-disable-next-line @coze-arch/use-error-in-catch -- 正常业务逻辑
} catch {
return false;
}
};

View File

@@ -0,0 +1,32 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
RowInternalStatus,
type TableData,
} from '../components/database-table-data/type';
export const isTableDataErrorOrUnSubmit = (tableData: TableData) => {
const hasErrorOrUnSubmit =
Boolean(tableData.dataList.length) &&
tableData.dataList.some(item =>
[RowInternalStatus.UnSubmit, RowInternalStatus.Error].includes(
item.internalStatus,
),
);
return hasErrorOrUnSubmit;
};

View File

@@ -0,0 +1,54 @@
/*
* 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 ColumnProps } from '@coze-arch/coze-design';
import { type TableRow } from '../components/database-table-data/type';
const FIXED_COLUMN_WIDTH = 60;
const MIN_COLUMN_WIDTH = 100;
/**
* 表格列伸缩时的回调,用于限制伸缩边界
* @param column
* @returns
*/
export const resizeFn = (
column: ColumnProps<TableRow>,
): ColumnProps<TableRow> => {
// 多选框/序号列不可伸缩
if (column.key === 'column-selection') {
return {
...column,
resizable: false,
width: FIXED_COLUMN_WIDTH,
};
}
// 固定列(操作列)不可伸缩
if (column.fixed) {
return {
...column,
resizable: false,
};
}
// 其余字段列可伸缩,但需要限制最小宽度
return {
...column,
width:
Number(column.width) < MIN_COLUMN_WIDTH
? MIN_COLUMN_WIDTH
: Number(column.width),
};
};

View File

@@ -0,0 +1,99 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "@coze-arch/ts-config/tsconfig.web.json",
"compilerOptions": {
"types": [],
"strictNullChecks": true,
"noImplicitAny": true,
"rootDir": "./src",
"outDir": "./dist",
"tsBuildInfoFile": "./dist/tsconfig.build.tsbuildinfo",
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["src"],
"references": [
{
"path": "../../../arch/bot-api/tsconfig.build.json"
},
{
"path": "../../../arch/bot-error/tsconfig.build.json"
},
{
"path": "../../../arch/bot-http/tsconfig.build.json"
},
{
"path": "../../../arch/bot-store/tsconfig.build.json"
},
{
"path": "../../../arch/bot-tea/tsconfig.build.json"
},
{
"path": "../../../arch/bot-typings/tsconfig.build.json"
},
{
"path": "../../../arch/bot-utils/tsconfig.build.json"
},
{
"path": "../../../arch/i18n/tsconfig.build.json"
},
{
"path": "../../../arch/report-events/tsconfig.build.json"
},
{
"path": "../../common/e2e/tsconfig.build.json"
},
{
"path": "../../common/reporter/tsconfig.build.json"
},
{
"path": "../../common/utils/tsconfig.build.json"
},
{
"path": "../../../components/bot-icons/tsconfig.build.json"
},
{
"path": "../../../components/bot-semi/tsconfig.build.json"
},
{
"path": "../../../components/table-view/tsconfig.build.json"
},
{
"path": "../../../../config/eslint-config/tsconfig.build.json"
},
{
"path": "../../../../config/stylelint-config/tsconfig.build.json"
},
{
"path": "../../../../config/ts-config/tsconfig.build.json"
},
{
"path": "../../../../config/vitest-config/tsconfig.build.json"
},
{
"path": "../database-v2-adapter/tsconfig.build.json"
},
{
"path": "../database-v2-base/tsconfig.build.json"
},
{
"path": "../../knowledge/common/stores/tsconfig.build.json"
},
{
"path": "../../knowledge/knowledge-resource-processor-base/tsconfig.build.json"
},
{
"path": "../../knowledge/knowledge-resource-processor-core/tsconfig.build.json"
},
{
"path": "../../../studio/components/tsconfig.build.json"
},
{
"path": "../../../studio/stores/bot-detail/tsconfig.build.json"
},
{
"path": "../../../studio/user-store/tsconfig.build.json"
}
]
}

View File

@@ -0,0 +1,15 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"exclude": ["**/*"],
"compilerOptions": {
"composite": true
},
"references": [
{
"path": "./tsconfig.build.json"
},
{
"path": "./tsconfig.misc.json"
}
]
}

View File

@@ -0,0 +1,21 @@
{
"extends": "@coze-arch/ts-config/tsconfig.web.json",
"$schema": "https://json.schemastore.org/tsconfig",
"include": ["__tests__", "stories", "vitest.config.ts", "tailwind.config.ts"],
"exclude": ["./dist"],
"references": [
{
"path": "./tsconfig.build.json"
}
],
"compilerOptions": {
"rootDir": "./",
"outDir": "./dist",
"types": ["vitest/globals"],
"strictNullChecks": true,
"noImplicitAny": true,
"paths": {
"@/*": ["./src/*"]
}
}
}

View File

@@ -0,0 +1,22 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { defineConfig } from '@coze-arch/vitest-config';
export default defineConfig({
dirname: __dirname,
preset: 'web',
});