From ac44e80134a60971e1309e72f07e98159a181abe Mon Sep 17 00:00:00 2001 From: zgq Date: Wed, 9 Sep 2026 14:19:42 +0800 Subject: [PATCH 1/2] fix(frontend): prevent duplicate datasource mutations --- chat2db-community-client/package.json | 2 +- .../src/blocks/NewTree/index.tsx | 91 +++++------------- .../NewTree/treePositionMutation.test.ts | 93 +++++++++++++++++++ .../blocks/NewTree/treePositionMutation.ts | 20 ++++ .../src/components/ConnectionEdit/index.tsx | 18 +++- .../ConnectionEdit/submissionGuard.test.ts | 56 +++++++++++ .../ConnectionEdit/submissionGuard.ts | 16 ++++ 7 files changed, 224 insertions(+), 72 deletions(-) create mode 100644 chat2db-community-client/src/blocks/NewTree/treePositionMutation.test.ts create mode 100644 chat2db-community-client/src/blocks/NewTree/treePositionMutation.ts create mode 100644 chat2db-community-client/src/components/ConnectionEdit/submissionGuard.test.ts create mode 100644 chat2db-community-client/src/components/ConnectionEdit/submissionGuard.ts diff --git a/chat2db-community-client/package.json b/chat2db-community-client/package.json index 0656b5bffa..c19ce25995 100644 --- a/chat2db-community-client/package.json +++ b/chat2db-community-client/package.json @@ -43,7 +43,7 @@ "test:data-source-identity": "tsx src/utils/dataSourceIdentity.test.ts && tsx src/store/tree/dataSourceIdentity.test.ts && tsx src/store/tree/dataSourceList.test.ts && tsx src/store/tree/treeNodeLoadState.test.ts && tsx src/blocks/NewTree/dataSourceIdentity.test.ts && tsx src/blocks/NewTree/dataSourceColorMenu.test.ts && tsx src/blocks/NewTree/treeMenuDanger.test.ts && tsx src/blocks/NewTree/treeMenuGrouping.test.ts && tsx src/blocks/NewTree/dataSourceIdentityColorRequest.test.ts && tsx src/components/ConnectionEdit/identityColorUpdate.test.ts && tsx src/utils/editorDataSourceLifecycle.test.ts && tsx src/components/SelectBoundInfo/cascadeRequestGuard.test.ts && tsx src/components/SelectBoundInfo/dataSourceSelection.test.ts && tsx src/blocks/AI/dataSourceContext.test.ts", "test:database-object-sorting": "tsx src/blocks/NewTree/utils/sortTreeNodes.test.ts", "test:data-source-authorization": "tsx src/utils/dataSourceAuthorization.test.ts", - "test:data-source-mutation-refresh": "tsx src/store/tree/dataSourceMutationRefresh.test.ts", + "test:data-source-mutation-refresh": "tsx src/store/tree/dataSourceMutationRefresh.test.ts && tsx src/components/ConnectionEdit/submissionGuard.test.ts && tsx src/blocks/NewTree/treePositionMutation.test.ts", "test:dashboard-store": "tsx src/store/dashboard/slices/common/refreshCurrentDashboard.test.ts", "test:dynamic-database-registry": "tsx src/utils/dynamicDatabaseRegistry.test.ts", "test:execution-console": "tsx src/blocks/SearchResult/components/ExecutionConsole/executionConsolePreferences.test.ts", diff --git a/chat2db-community-client/src/blocks/NewTree/index.tsx b/chat2db-community-client/src/blocks/NewTree/index.tsx index 1e472e953a..30ecb4561a 100644 --- a/chat2db-community-client/src/blocks/NewTree/index.tsx +++ b/chat2db-community-client/src/blocks/NewTree/index.tsx @@ -1,5 +1,5 @@ import React, { memo, useCallback, useEffect, useLayoutEffect, useRef, useMemo, forwardRef, useState } from 'react'; -import { Tree, TreeProps, ConfigProvider, TreeDataNode, Spin } from 'antd'; +import { Tree, TreeProps, ConfigProvider, Spin } from 'antd'; import { useStyles } from './style'; import { useStyles as renderTitleUseStyles } from './renderTitleStyle'; @@ -24,6 +24,7 @@ import connectionService from '@/service/connection'; import { useSize } from 'ahooks'; import { decorateDataSourceIdentityTree } from './dataSourceIdentity'; import { measureTreeScrollWidth, resolveNextTreeScrollWidth } from './treeScrollWidth'; +import { TreePositionMutationCoordinator } from './treePositionMutation'; interface IProps extends TreeProps { className?: string; @@ -69,12 +70,12 @@ const NewTree = (props: IProps, ref: React.ForwardedRef) => { const horizontalMeasureFrameRef = useRef(); const horizontalMeasureResetRef = useRef(false); const horizontalScrollHideTimerRef = useRef(); + const treePositionMutationRef = useRef(new TreePositionMutationCoordinator()); const [treeScrollWidth, setTreeScrollWidth] = useState(); const [isHorizontalScrolling, setIsHorizontalScrolling] = useState(false); const filteredTreeData = useTrimTreeData({ leafNodes, hiddenNoPermission, excludeNodes }); const { editingTreeNode, - setTreeData, selectedKeys, setSelectedKeys, setTreeRef, @@ -83,9 +84,9 @@ const NewTree = (props: IProps, ref: React.ForwardedRef) => { setScrollTargetKey, searchBarValue, dataSourceList, + getTreeData, } = useTreeStore((state) => ({ editingTreeNode: state.editingTreeNode, - setTreeData: state.setTreeData, selectedKeys: state.selectedKeys, setSelectedKeys: state.setSelectedKeys, setTreeRef: state.setTreeRef, @@ -94,6 +95,7 @@ const NewTree = (props: IProps, ref: React.ForwardedRef) => { setScrollTargetKey: state.setScrollTargetKey, searchBarValue: state.searchBarValue, dataSourceList: state.dataSourceList, + getTreeData: state.getTreeData, })); const identityTreeData = useMemo( () => decorateDataSourceIdentityTree(filteredTreeData, dataSourceList), @@ -202,76 +204,29 @@ const NewTree = (props: IProps, ref: React.ForwardedRef) => { }; const onDrop: TreeProps['onDrop'] = (info: any) => { - const dropKey = info.node.key; const dragKey = info.dragNode.key; const dropPos = info.node.pos.split('-'); const dropPosition = info.dropPosition - Number(dropPos[dropPos.length - 1]); // the drop position relative to the drop node, inside 0, top -1, bottom 1 - const loop = ( - data: TreeDataNode[], - key: React.Key, - callback: (node: TreeDataNode, i: number, data: TreeDataNode[]) => void, - ) => { - for (let i = 0; i < data.length; i++) { - if (data[i].key === key) { - return callback(data[i], i, data); - } - if (data[i].children) { - loop(data[i].children!, key, callback); - } - } - }; - - const data = [...(filteredTreeData || [])]; - - // Find dragObject - let dragObj: TreeDataNode; - loop(data, dragKey, (item, index, arr) => { - arr.splice(index, 1); - dragObj = item; - }); - - if (!info.dropToGap) { - // Drop on the content - loop(data, dropKey, (item) => { - item.children = item.children || []; - // where to insert. New item was inserted to the start of the array in this example, but can be anywhere - item.children.unshift(dragObj); - }); - } else { - let ar: TreeDataNode[] = []; - let i: number; - loop(data, dropKey, (_item, index, arr) => { - ar = arr; - i = index; - }); - if (dropPosition === -1) { - // Drop on the top of the drop node - ar.splice(i!, 0, dragObj!); - } else { - // Drop on the bottom of the drop node - ar.splice(i! + 1, 0, dragObj!); - } - } - - connectionService - .updatePosition({ - dragNode: { - id: info.dragNode.id, - type: info.dragNode.treeNodeType === TreeNodeType.GROUP ? 'NAMESPACE' : 'DATA_SOURCE', - // name: info.dragNode.originalTitle, - }, - dropToNode: { - id: info.node.id, - type: info.node.treeNodeType === TreeNodeType.GROUP ? 'NAMESPACE' : 'DATA_SOURCE', - // name: info.node.originalTitle, - }, - dropPosition: dropPosition as 0 | 1 | -1, - }) - .then(() => { - setTreeData(data); - }); + void treePositionMutationRef.current + .run( + dragKey, + () => + connectionService.updatePosition({ + dragNode: { + id: info.dragNode.id, + type: info.dragNode.treeNodeType === TreeNodeType.GROUP ? 'NAMESPACE' : 'DATA_SOURCE', + }, + dropToNode: { + id: info.node.id, + type: info.node.treeNodeType === TreeNodeType.GROUP ? 'NAMESPACE' : 'DATA_SOURCE', + }, + dropPosition: dropPosition as 0 | 1 | -1, + }), + () => getTreeData({ refresh: true, throwOnError: true }), + ) + .catch(() => undefined); }; const titleRender = (nodeData: TreeNodeData) => { diff --git a/chat2db-community-client/src/blocks/NewTree/treePositionMutation.test.ts b/chat2db-community-client/src/blocks/NewTree/treePositionMutation.test.ts new file mode 100644 index 0000000000..5ea6339894 --- /dev/null +++ b/chat2db-community-client/src/blocks/NewTree/treePositionMutation.test.ts @@ -0,0 +1,93 @@ +import assert from 'node:assert/strict'; +import { TreePositionMutationCoordinator } from './treePositionMutation'; + +function deferred() { + let resolve!: () => void; + let reject!: (error: Error) => void; + const promise = new Promise((resolvePromise, rejectPromise) => { + resolve = resolvePromise; + reject = rejectPromise; + }); + return { promise, resolve, reject }; +} + +async function testSuccessfulMoveRefreshesTree() { + const coordinator = new TreePositionMutationCoordinator(); + let refreshCount = 0; + + const committed = await coordinator.run( + 'dataSource_1', + () => Promise.resolve(), + () => { + refreshCount += 1; + return Promise.resolve(); + }, + ); + + assert.equal(committed, true); + assert.equal(refreshCount, 1); +} + +async function testFailedMoveDoesNotRefreshTree() { + const coordinator = new TreePositionMutationCoordinator(); + let refreshCount = 0; + + await assert.rejects( + coordinator.run( + 'dataSource_1', + () => Promise.reject(new Error('move failed')), + () => { + refreshCount += 1; + return Promise.resolve(); + }, + ), + /move failed/, + ); + + assert.equal(refreshCount, 0); +} + +async function testConcurrentMoveForSameNodeIsRejected() { + const coordinator = new TreePositionMutationCoordinator(); + const request = deferred(); + let updateCount = 0; + const updatePosition = () => { + updateCount += 1; + return request.promise; + }; + + const first = coordinator.run('dataSource_1', updatePosition, () => Promise.resolve()); + const second = coordinator.run('dataSource_1', updatePosition, () => Promise.resolve()); + + assert.equal(await second, false); + await Promise.resolve(); + assert.equal(updateCount, 1); + request.resolve(); + assert.equal(await first, true); +} + +async function testMoveCanRetryAfterFailure() { + const coordinator = new TreePositionMutationCoordinator(); + const request = deferred(); + const first = coordinator.run('dataSource_1', () => request.promise, () => Promise.resolve()); + request.reject(new Error('move failed')); + await assert.rejects(first, /move failed/); + + assert.equal( + await coordinator.run('dataSource_1', () => Promise.resolve(), () => Promise.resolve()), + true, + ); +} + +async function main() { + await testSuccessfulMoveRefreshesTree(); + await testFailedMoveDoesNotRefreshTree(); + await testConcurrentMoveForSameNodeIsRejected(); + await testMoveCanRetryAfterFailure(); + console.log('Tree position mutation tests passed'); +} + +main().catch((error) => { + console.error(error); + process.exitCode = 1; +}); diff --git a/chat2db-community-client/src/blocks/NewTree/treePositionMutation.ts b/chat2db-community-client/src/blocks/NewTree/treePositionMutation.ts new file mode 100644 index 0000000000..4095a069d1 --- /dev/null +++ b/chat2db-community-client/src/blocks/NewTree/treePositionMutation.ts @@ -0,0 +1,20 @@ +import type { Key } from 'react'; + +export class TreePositionMutationCoordinator { + private readonly pendingKeys = new Set(); + + run(key: Key, updatePosition: () => Promise, refreshTree: () => Promise): Promise { + if (this.pendingKeys.has(key)) { + return Promise.resolve(false); + } + + this.pendingKeys.add(key); + return Promise.resolve() + .then(updatePosition) + .then(refreshTree) + .then(() => true) + .finally(() => { + this.pendingKeys.delete(key); + }); + } +} diff --git a/chat2db-community-client/src/components/ConnectionEdit/index.tsx b/chat2db-community-client/src/components/ConnectionEdit/index.tsx index 0deceb97bc..f1e29fe24c 100644 --- a/chat2db-community-client/src/components/ConnectionEdit/index.tsx +++ b/chat2db-community-client/src/components/ConnectionEdit/index.tsx @@ -11,13 +11,14 @@ import { FolderOpenOutlined } from '@ant-design/icons'; import { Button, Checkbox, Collapse, Form, Input, Select, Table, Tooltip } from 'antd'; import classnames from 'classnames'; import { CircleHelp } from 'lucide-react'; -import React, { ForwardedRef, Fragment, forwardRef, useEffect, useImperativeHandle, useMemo, useState } from 'react'; +import React, { ForwardedRef, Fragment, forwardRef, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react'; import Driver from './components/Driver'; import { dataSourceFormConfigs } from './config/dataSource'; import { InputType } from './config/enum'; import { IConnectionConfig, IFormItem, ILocalizedConnectionText, ISelect } from './config/types'; import { applyConnectionIdentityColorUpdate } from './identityColorUpdate'; import styles from './index.less'; +import { SubmissionGuard } from './submissionGuard'; import { formatJdbcHostForUrl, normalizeJdbcHostFromUrl, shouldSyncJdbcUrlForField } from './utils/jdbcUrl'; // ----- store ----- @@ -398,6 +399,7 @@ const ConnectionEdit = forwardRef((props: IProps, ref: ForwardedRef ({ curOrg: s.curOrg })); const dataSourceFormConfigPropsMemo = useMemo(() => { @@ -530,7 +532,11 @@ const ConnectionEdit = forwardRef((props: IProps, ref: ForwardedRef submit(p, type)); + if (!request) { + return; + } + request .catch((error: any) => { staticMessage.error(getConnectionErrorMessage(error)); }) @@ -543,7 +549,13 @@ const ConnectionEdit = forwardRef((props: IProps, ref: ForwardedRef connectionService[type](p)) + : connectionService[type](p); + if (!api) { + return; + } if (type === submitType.TEST) { api .then((res: any) => { diff --git a/chat2db-community-client/src/components/ConnectionEdit/submissionGuard.test.ts b/chat2db-community-client/src/components/ConnectionEdit/submissionGuard.test.ts new file mode 100644 index 0000000000..089c3a7bcd --- /dev/null +++ b/chat2db-community-client/src/components/ConnectionEdit/submissionGuard.test.ts @@ -0,0 +1,56 @@ +import assert from 'node:assert/strict'; +import { SubmissionGuard } from './submissionGuard'; + +function deferred() { + let resolve!: (value: T) => void; + let reject!: (error: Error) => void; + const promise = new Promise((resolvePromise, rejectPromise) => { + resolve = resolvePromise; + reject = rejectPromise; + }); + return { promise, resolve, reject }; +} + +async function testConcurrentSubmissionIsRejected() { + const guard = new SubmissionGuard(); + const request = deferred(); + let submitCount = 0; + const submit = () => { + submitCount += 1; + return request.promise; + }; + + const first = guard.run(submit); + const second = guard.run(submit); + + assert.ok(first); + assert.equal(second, null); + await Promise.resolve(); + assert.equal(submitCount, 1); + request.resolve(1); + assert.equal(await first, 1); +} + +async function testSubmissionCanRetryAfterFailure() { + const guard = new SubmissionGuard(); + const failedRequest = deferred(); + const first = guard.run(() => failedRequest.promise); + assert.ok(first); + failedRequest.reject(new Error('save failed')); + await assert.rejects(first, /save failed/); + + const second = guard.run(() => Promise.resolve('saved')); + assert.ok(second); + assert.equal(await second, 'saved'); +} + +async function main() { + await testConcurrentSubmissionIsRejected(); + await testSubmissionCanRetryAfterFailure(); + console.log('Connection submission guard tests passed'); +} + +main().catch((error) => { + console.error(error); + process.exitCode = 1; +}); diff --git a/chat2db-community-client/src/components/ConnectionEdit/submissionGuard.ts b/chat2db-community-client/src/components/ConnectionEdit/submissionGuard.ts new file mode 100644 index 0000000000..f492665c42 --- /dev/null +++ b/chat2db-community-client/src/components/ConnectionEdit/submissionGuard.ts @@ -0,0 +1,16 @@ +export class SubmissionGuard { + private pending = false; + + run(submit: () => Promise): Promise | null { + if (this.pending) { + return null; + } + + this.pending = true; + return Promise.resolve() + .then(submit) + .finally(() => { + this.pending = false; + }); + } +} From 562c71686717f14f3838b47d97e507b89295b1a3 Mon Sep 17 00:00:00 2001 From: zgq Date: Wed, 9 Sep 2026 15:20:44 +0800 Subject: [PATCH 2/2] fix(frontend): report datasource tree refresh failures --- .../scripts/i18n-source-hashes.json | 4 ++-- .../src/blocks/NewTree/index.tsx | 10 ++++++-- .../NewTree/treePositionMutation.test.ts | 23 ++++++++++++++++++- .../blocks/NewTree/treePositionMutation.ts | 15 +++++++++++- .../src/i18n/en-US/workspace.ts | 1 + .../src/i18n/es-ES/workspace.ts | 1 + .../src/i18n/ja-JP/workspace.ts | 1 + .../src/i18n/ko-KR/workspace.ts | 1 + .../src/i18n/zh-CN/workspace.ts | 1 + 9 files changed, 51 insertions(+), 6 deletions(-) diff --git a/chat2db-community-client/scripts/i18n-source-hashes.json b/chat2db-community-client/scripts/i18n-source-hashes.json index 3577c9d8aa..745b231a6e 100644 --- a/chat2db-community-client/scripts/i18n-source-hashes.json +++ b/chat2db-community-client/scripts/i18n-source-hashes.json @@ -21,7 +21,7 @@ "spaceSetting.ts": "defdf3138e5e1782f0282760bcef4ce1b8550cc80b03052fea468d97165cb980", "sqlEditor.ts": "5c41e5f24d5ba78909795bcc681cabb8460cca075233b665dc4940afe82ce0c9", "stream.ts": "2e2fca0b1ba1ecd5a1501b767be8857cf0b72d800d69a4acf9cd62a61d5ceaf5", - "workspace.ts": "ee72537a2ca1ed7bd71558a051f23cdfe202b298a000f2f928316bff0b85fd2d" + "workspace.ts": "f59aa207c100881a66f0cc9d3a61308c9c7cfb02d2fc6ecf1c9f79f8571f5ac4" }, "ko-KR": { "ai.ts": "e7a2fcbfb3ed647bd14f63aca4b6c35c8fb402c49a6a1f36bb04f76f4e8415af", @@ -42,7 +42,7 @@ "spaceSetting.ts": "defdf3138e5e1782f0282760bcef4ce1b8550cc80b03052fea468d97165cb980", "sqlEditor.ts": "5c41e5f24d5ba78909795bcc681cabb8460cca075233b665dc4940afe82ce0c9", "stream.ts": "2e2fca0b1ba1ecd5a1501b767be8857cf0b72d800d69a4acf9cd62a61d5ceaf5", - "workspace.ts": "ee72537a2ca1ed7bd71558a051f23cdfe202b298a000f2f928316bff0b85fd2d" + "workspace.ts": "f59aa207c100881a66f0cc9d3a61308c9c7cfb02d2fc6ecf1c9f79f8571f5ac4" } } } diff --git a/chat2db-community-client/src/blocks/NewTree/index.tsx b/chat2db-community-client/src/blocks/NewTree/index.tsx index 30ecb4561a..c47ba3c807 100644 --- a/chat2db-community-client/src/blocks/NewTree/index.tsx +++ b/chat2db-community-client/src/blocks/NewTree/index.tsx @@ -24,7 +24,9 @@ import connectionService from '@/service/connection'; import { useSize } from 'ahooks'; import { decorateDataSourceIdentityTree } from './dataSourceIdentity'; import { measureTreeScrollWidth, resolveNextTreeScrollWidth } from './treeScrollWidth'; -import { TreePositionMutationCoordinator } from './treePositionMutation'; +import { TreePositionMutationCoordinator, TreePositionRefreshError } from './treePositionMutation'; +import i18n from '@/i18n'; +import { staticMessage } from '@chat2db/ui'; interface IProps extends TreeProps { className?: string; @@ -226,7 +228,11 @@ const NewTree = (props: IProps, ref: React.ForwardedRef) => { }), () => getTreeData({ refresh: true, throwOnError: true }), ) - .catch(() => undefined); + .catch((error) => { + if (error instanceof TreePositionRefreshError) { + staticMessage.error(i18n('workspace.tips.treePositionRefreshFailed')); + } + }); }; const titleRender = (nodeData: TreeNodeData) => { diff --git a/chat2db-community-client/src/blocks/NewTree/treePositionMutation.test.ts b/chat2db-community-client/src/blocks/NewTree/treePositionMutation.test.ts index 5ea6339894..716dfde6f8 100644 --- a/chat2db-community-client/src/blocks/NewTree/treePositionMutation.test.ts +++ b/chat2db-community-client/src/blocks/NewTree/treePositionMutation.test.ts @@ -1,5 +1,5 @@ import assert from 'node:assert/strict'; -import { TreePositionMutationCoordinator } from './treePositionMutation'; +import { TreePositionMutationCoordinator, TreePositionRefreshError } from './treePositionMutation'; function deferred() { let resolve!: () => void; @@ -79,11 +79,32 @@ async function testMoveCanRetryAfterFailure() { ); } +async function testRefreshFailureIsDistinguishedAndCanRetry() { + const coordinator = new TreePositionMutationCoordinator(); + let updateCount = 0; + const updatePosition = () => { + updateCount += 1; + return Promise.resolve(); + }; + + await assert.rejects( + coordinator.run('dataSource_1', updatePosition, () => Promise.reject(new Error('refresh failed'))), + TreePositionRefreshError, + ); + + assert.equal( + await coordinator.run('dataSource_1', updatePosition, () => Promise.resolve()), + true, + ); + assert.equal(updateCount, 2); +} + async function main() { await testSuccessfulMoveRefreshesTree(); await testFailedMoveDoesNotRefreshTree(); await testConcurrentMoveForSameNodeIsRejected(); await testMoveCanRetryAfterFailure(); + await testRefreshFailureIsDistinguishedAndCanRetry(); console.log('Tree position mutation tests passed'); } diff --git a/chat2db-community-client/src/blocks/NewTree/treePositionMutation.ts b/chat2db-community-client/src/blocks/NewTree/treePositionMutation.ts index 4095a069d1..b9e89ce736 100644 --- a/chat2db-community-client/src/blocks/NewTree/treePositionMutation.ts +++ b/chat2db-community-client/src/blocks/NewTree/treePositionMutation.ts @@ -1,5 +1,12 @@ import type { Key } from 'react'; +export class TreePositionRefreshError extends Error { + constructor() { + super('The tree position was updated, but refreshing the tree failed'); + this.name = 'TreePositionRefreshError'; + } +} + export class TreePositionMutationCoordinator { private readonly pendingKeys = new Set(); @@ -11,7 +18,13 @@ export class TreePositionMutationCoordinator { this.pendingKeys.add(key); return Promise.resolve() .then(updatePosition) - .then(refreshTree) + .then(async () => { + try { + await refreshTree(); + } catch { + throw new TreePositionRefreshError(); + } + }) .then(() => true) .finally(() => { this.pendingKeys.delete(key); diff --git a/chat2db-community-client/src/i18n/en-US/workspace.ts b/chat2db-community-client/src/i18n/en-US/workspace.ts index 93d638d7d4..211e27ece7 100644 --- a/chat2db-community-client/src/i18n/en-US/workspace.ts +++ b/chat2db-community-client/src/i18n/en-US/workspace.ts @@ -82,6 +82,7 @@ export default { 'workspace.tips.cannotMoveLastSplitTab': 'Keep at least one tab in the current split pane', 'workspace.menu.newGroup': 'New group', 'workspace.menu.moveToGroup': 'Move to', + 'workspace.tips.treePositionRefreshFailed': 'The position was updated, but the data source list failed to refresh. Please refresh it manually.', 'workspace.menu.deleteGroup': 'Delete group', 'workspace.menu.renameGroup': 'Rename', 'workspace.menu.newDataSource': 'New connection', diff --git a/chat2db-community-client/src/i18n/es-ES/workspace.ts b/chat2db-community-client/src/i18n/es-ES/workspace.ts index 60e3b8dd10..498fa882a0 100644 --- a/chat2db-community-client/src/i18n/es-ES/workspace.ts +++ b/chat2db-community-client/src/i18n/es-ES/workspace.ts @@ -82,6 +82,7 @@ export default { 'workspace.tips.cannotMoveLastSplitTab': 'Mantenga al menos una pestaña en el panel dividido actual', 'workspace.menu.newGroup': 'Nuevo grupo', 'workspace.menu.moveToGroup': 'Mover a', + 'workspace.tips.treePositionRefreshFailed': 'La posición se actualizó, pero no se pudo actualizar la lista de fuentes de datos. Actualícela manualmente.', 'workspace.menu.deleteGroup': 'Eliminar grupo', 'workspace.menu.renameGroup': 'Cambiar nombre', 'workspace.menu.newDataSource': 'Nueva conexión', diff --git a/chat2db-community-client/src/i18n/ja-JP/workspace.ts b/chat2db-community-client/src/i18n/ja-JP/workspace.ts index fc8d702297..3655fd310a 100644 --- a/chat2db-community-client/src/i18n/ja-JP/workspace.ts +++ b/chat2db-community-client/src/i18n/ja-JP/workspace.ts @@ -81,6 +81,7 @@ export default { 'workspace.tips.cannotMoveLastSplitTab': '現在の分割ペインには少なくとも1つのタブを残してください', 'workspace.menu.newGroup': '新しいグループ', 'workspace.menu.moveToGroup': '移動先', + 'workspace.tips.treePositionRefreshFailed': '位置は更新されましたが、データソース一覧を更新できませんでした。手動で更新してください。', 'workspace.menu.deleteGroup': 'グループを削除', 'workspace.menu.renameGroup': '名前を変更', 'workspace.menu.newDataSource': '新しい接続', diff --git a/chat2db-community-client/src/i18n/ko-KR/workspace.ts b/chat2db-community-client/src/i18n/ko-KR/workspace.ts index 144c660d00..ad464217b3 100644 --- a/chat2db-community-client/src/i18n/ko-KR/workspace.ts +++ b/chat2db-community-client/src/i18n/ko-KR/workspace.ts @@ -81,6 +81,7 @@ export default { 'workspace.tips.cannotMoveLastSplitTab': '현재 분할 창에 탭을 하나 이상 유지하세요', 'workspace.menu.newGroup': '새 그룹', 'workspace.menu.moveToGroup': '이동', + 'workspace.tips.treePositionRefreshFailed': '위치는 변경되었지만 데이터 소스 목록을 새로 고치지 못했습니다. 수동으로 새로 고쳐 주세요.', 'workspace.menu.deleteGroup': '그룹 삭제', 'workspace.menu.renameGroup': '이름 변경', 'workspace.menu.newDataSource': '새 연결', diff --git a/chat2db-community-client/src/i18n/zh-CN/workspace.ts b/chat2db-community-client/src/i18n/zh-CN/workspace.ts index 35d950712a..2565b8e00a 100644 --- a/chat2db-community-client/src/i18n/zh-CN/workspace.ts +++ b/chat2db-community-client/src/i18n/zh-CN/workspace.ts @@ -78,6 +78,7 @@ export default { 'workspace.tips.cannotMoveLastSplitTab': '当前分屏至少需要保留一个标签', 'workspace.menu.newGroup': '新建组', 'workspace.menu.moveToGroup': '移动到', + 'workspace.tips.treePositionRefreshFailed': '位置已更新,但数据源列表刷新失败,请手动刷新', 'workspace.menu.deleteGroup': '删除组', 'workspace.menu.renameGroup': '重命名', 'workspace.menu.newDataSource': '新建连接',