mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-05-19 23:30:11 +08:00
Feature/add aibook (#160)
* feat: 增加技术圈连接 * feat: 增强水平滚动条在大量数据下支持鼠标滚轮 (refs #146) * feat: 新增连接标签分组功能,支持创建/编辑/删除标签、拖拽归组、右键移至标签 refs #148
This commit is contained in:
@@ -2705,29 +2705,31 @@ const DataGrid: React.FC<DataGridProps> = ({
|
||||
horizontalSyncSourceRef.current = '';
|
||||
}, []);
|
||||
|
||||
const handleExternalHorizontalWheel = useCallback((event: React.WheelEvent<HTMLDivElement>) => {
|
||||
// 非虚拟模式:外部水平滚动条的 wheel 处理(通过原生事件绑定,确保 preventDefault 生效)
|
||||
useEffect(() => {
|
||||
const externalScroll = externalHScrollRef.current;
|
||||
if (!(externalScroll instanceof HTMLDivElement)) {
|
||||
return;
|
||||
}
|
||||
const dominantDelta = Math.abs(event.deltaX) > Math.abs(event.deltaY) ? event.deltaX : event.deltaY;
|
||||
if (!Number.isFinite(dominantDelta) || Math.abs(dominantDelta) < 0.5) {
|
||||
return;
|
||||
}
|
||||
if (!externalScroll || !horizontalScrollVisible) return;
|
||||
|
||||
const maxScrollLeft = Math.max(0, externalScroll.scrollWidth - externalScroll.clientWidth);
|
||||
if (maxScrollLeft <= 0) {
|
||||
return;
|
||||
}
|
||||
const handleExternalWheel = (e: WheelEvent) => {
|
||||
// 鼠标在水平滚动条区域时,始终阻止垂直滚动冒泡
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
const nextScrollLeft = Math.max(0, Math.min(maxScrollLeft, externalScroll.scrollLeft + dominantDelta));
|
||||
if (Math.abs(nextScrollLeft - externalScroll.scrollLeft) < 0.5) {
|
||||
return;
|
||||
}
|
||||
const dominantDelta = Math.abs(e.deltaX) > Math.abs(e.deltaY) ? e.deltaX : e.deltaY;
|
||||
if (!Number.isFinite(dominantDelta) || Math.abs(dominantDelta) < 0.5) return;
|
||||
|
||||
event.preventDefault();
|
||||
externalScroll.scrollLeft = nextScrollLeft;
|
||||
}, []);
|
||||
const maxScrollLeft = Math.max(0, externalScroll.scrollWidth - externalScroll.clientWidth);
|
||||
if (maxScrollLeft <= 0) return;
|
||||
|
||||
const nextScrollLeft = Math.max(0, Math.min(maxScrollLeft, externalScroll.scrollLeft + dominantDelta));
|
||||
externalScroll.scrollLeft = nextScrollLeft;
|
||||
};
|
||||
|
||||
externalScroll.addEventListener('wheel', handleExternalWheel, { passive: false, capture: true });
|
||||
return () => {
|
||||
externalScroll.removeEventListener('wheel', handleExternalWheel, { capture: true } as EventListenerOptions);
|
||||
};
|
||||
}, [horizontalScrollVisible]);
|
||||
|
||||
useEffect(() => {
|
||||
if (viewMode !== 'table') return;
|
||||
@@ -2735,19 +2737,24 @@ const DataGrid: React.FC<DataGridProps> = ({
|
||||
return () => cancelAnimationFrame(rafId);
|
||||
}, [viewMode, totalWidth, mergedDisplayData.length, recalculateTableMetrics]);
|
||||
|
||||
// 虚拟模式下,为 rc-virtual-list 的内置水平滚动条添加鼠标滚轮支持
|
||||
// rc-virtual-list 的 ScrollBar 组件原生只支持拖拽,不支持 wheel 事件
|
||||
// 方案:使用 MutationObserver 发现滚动条元素后直接绑定 wheel 事件
|
||||
// 虚拟模式下,在容器级别监听 wheel 事件,当鼠标在底部水平滚动条区域时拦截并转为水平滚动
|
||||
useEffect(() => {
|
||||
if (viewMode !== 'table' || !enableVirtual) return;
|
||||
const container = tableContainerRef.current;
|
||||
if (!container) return;
|
||||
|
||||
let currentScrollbarEl: HTMLElement | null = null;
|
||||
// 滚动条区域高度:滚动条高度 + 间距 + 容错
|
||||
const scrollbarZoneHeight = floatingScrollbarHeight + floatingScrollbarGap + 8;
|
||||
|
||||
const handleScrollbarWheel = (e: WheelEvent) => {
|
||||
const innerEl = container.querySelector('.rc-virtual-list-holder-inner') as HTMLElement | null;
|
||||
const holderEl = container.querySelector('.rc-virtual-list-holder') as HTMLElement | null;
|
||||
const handleContainerWheel = (e: WheelEvent) => {
|
||||
// 判断鼠标是否在底部滚动条区域
|
||||
const containerRect = container.getBoundingClientRect();
|
||||
if (e.clientY < containerRect.bottom - scrollbarZoneHeight) return;
|
||||
|
||||
// 适配 antd 的虚拟列表类名
|
||||
const holderEl = container.querySelector('.ant-table-tbody-virtual-holder') as HTMLElement | null;
|
||||
const innerEl = holderEl?.querySelector('.ant-table-tbody-virtual-holder-inner') as HTMLElement | null;
|
||||
|
||||
if (!innerEl || !holderEl) return;
|
||||
|
||||
const dominantDelta = Math.abs(e.deltaX) > Math.abs(e.deltaY) ? e.deltaX : e.deltaY;
|
||||
@@ -2769,12 +2776,13 @@ const DataGrid: React.FC<DataGridProps> = ({
|
||||
innerEl.style.marginLeft = `${-newOffset}px`;
|
||||
|
||||
// 同步 scrollbar thumb 位置
|
||||
if (currentScrollbarEl && maxScroll > 0) {
|
||||
const thumbEl = currentScrollbarEl.querySelector('[class*="scrollbar-thumb"]') as HTMLElement | null;
|
||||
const scrollbarEl = container.querySelector('.ant-table-tbody-virtual-scrollbar-horizontal') as HTMLElement | null;
|
||||
if (scrollbarEl && maxScroll > 0) {
|
||||
const thumbEl = scrollbarEl.querySelector('[class*="scrollbar-thumb"]') as HTMLElement | null;
|
||||
if (thumbEl) {
|
||||
const ratio = newOffset / maxScroll;
|
||||
const thumbWidth = parseFloat(thumbEl.style.width) || thumbEl.offsetWidth;
|
||||
const trackWidth = currentScrollbarEl.clientWidth;
|
||||
const trackWidth = scrollbarEl.clientWidth;
|
||||
const thumbMaxOffset = trackWidth - thumbWidth;
|
||||
thumbEl.style.left = `${ratio * thumbMaxOffset}px`;
|
||||
}
|
||||
@@ -2787,33 +2795,12 @@ const DataGrid: React.FC<DataGridProps> = ({
|
||||
}
|
||||
};
|
||||
|
||||
const bindScrollbar = () => {
|
||||
const el = container.querySelector('.ant-table-tbody-virtual-scrollbar-horizontal') as HTMLElement | null;
|
||||
if (el && el !== currentScrollbarEl) {
|
||||
if (currentScrollbarEl) {
|
||||
currentScrollbarEl.removeEventListener('wheel', handleScrollbarWheel);
|
||||
}
|
||||
currentScrollbarEl = el;
|
||||
el.addEventListener('wheel', handleScrollbarWheel, { passive: false });
|
||||
}
|
||||
};
|
||||
|
||||
// 初次尝试绑定
|
||||
bindScrollbar();
|
||||
|
||||
// 使用 MutationObserver 监听 DOM 变化,确保即使元素延迟渲染也能绑定
|
||||
const observer = new MutationObserver(() => {
|
||||
bindScrollbar();
|
||||
});
|
||||
observer.observe(container, { childList: true, subtree: true });
|
||||
container.addEventListener('wheel', handleContainerWheel, { passive: false, capture: true });
|
||||
|
||||
return () => {
|
||||
observer.disconnect();
|
||||
if (currentScrollbarEl) {
|
||||
currentScrollbarEl.removeEventListener('wheel', handleScrollbarWheel);
|
||||
}
|
||||
container.removeEventListener('wheel', handleContainerWheel, { capture: true } as EventListenerOptions);
|
||||
};
|
||||
}, [viewMode, enableVirtual, tableScrollX, mergedDisplayData.length]);
|
||||
}, [viewMode, enableVirtual, tableScrollX, floatingScrollbarHeight, floatingScrollbarGap]);
|
||||
|
||||
useEffect(() => {
|
||||
if (viewMode !== 'table') return;
|
||||
@@ -3307,7 +3294,6 @@ const DataGrid: React.FC<DataGridProps> = ({
|
||||
className="data-grid-external-hscroll"
|
||||
aria-hidden={!horizontalScrollVisible}
|
||||
onScroll={applyExternalScrollToTableTargets}
|
||||
onWheel={handleExternalHorizontalWheel}
|
||||
style={{
|
||||
opacity: horizontalScrollVisible ? 1 : 0,
|
||||
pointerEvents: horizontalScrollVisible ? 'auto' : 'none',
|
||||
|
||||
@@ -6,6 +6,7 @@ import { Tree, message, Dropdown, MenuProps, Input, Button, Modal, Form, Badge,
|
||||
EyeOutlined,
|
||||
ConsoleSqlOutlined,
|
||||
HddOutlined,
|
||||
FolderOutlined,
|
||||
FolderOpenOutlined,
|
||||
FileTextOutlined,
|
||||
CopyOutlined,
|
||||
@@ -42,7 +43,7 @@ interface TreeNode {
|
||||
children?: TreeNode[];
|
||||
icon?: React.ReactNode;
|
||||
dataRef?: any;
|
||||
type?: 'connection' | 'database' | 'table' | 'view' | 'db-trigger' | 'routine' | 'object-group' | 'queries-folder' | 'saved-query' | 'folder-columns' | 'folder-indexes' | 'folder-fks' | 'folder-triggers' | 'redis-db';
|
||||
type?: 'connection' | 'database' | 'table' | 'view' | 'db-trigger' | 'routine' | 'object-group' | 'queries-folder' | 'saved-query' | 'folder-columns' | 'folder-indexes' | 'folder-fks' | 'folder-triggers' | 'redis-db' | 'tag';
|
||||
}
|
||||
|
||||
type BatchTableExportMode = 'schema' | 'backup' | 'dataOnly';
|
||||
@@ -64,6 +65,12 @@ const Sidebar: React.FC<{ onEditConnection?: (conn: SavedConnection) => void }>
|
||||
const addTab = useStore(state => state.addTab);
|
||||
const setActiveContext = useStore(state => state.setActiveContext);
|
||||
const removeConnection = useStore(state => state.removeConnection);
|
||||
const connectionTags = useStore(state => state.connectionTags);
|
||||
const addConnectionTag = useStore(state => state.addConnectionTag);
|
||||
const updateConnectionTag = useStore(state => state.updateConnectionTag);
|
||||
const removeConnectionTag = useStore(state => state.removeConnectionTag);
|
||||
const moveConnectionToTag = useStore(state => state.moveConnectionToTag);
|
||||
const reorderTags = useStore(state => state.reorderTags);
|
||||
const closeTabsByConnection = useStore(state => state.closeTabsByConnection);
|
||||
const closeTabsByDatabase = useStore(state => state.closeTabsByDatabase);
|
||||
const theme = useStore(state => state.theme);
|
||||
@@ -127,6 +134,10 @@ const Sidebar: React.FC<{ onEditConnection?: (conn: SavedConnection) => void }>
|
||||
const [renameViewForm] = Form.useForm();
|
||||
const [renameViewTarget, setRenameViewTarget] = useState<any>(null);
|
||||
|
||||
// Connection Tag Modals
|
||||
const [isCreateTagModalOpen, setIsCreateTagModalOpen] = useState(false);
|
||||
const [createTagForm] = Form.useForm();
|
||||
|
||||
// Batch Operations Modal
|
||||
const [isBatchModalOpen, setIsBatchModalOpen] = useState(false);
|
||||
const [batchTables, setBatchTables] = useState<BatchObjectItem[]>([]);
|
||||
@@ -208,11 +219,21 @@ const Sidebar: React.FC<{ onEditConnection?: (conn: SavedConnection) => void }>
|
||||
useEffect(() => {
|
||||
setTreeData((prev) => {
|
||||
const prevMap = new Map<string, TreeNode>();
|
||||
prev.forEach((node) => {
|
||||
prevMap.set(String(node.key), node);
|
||||
});
|
||||
|
||||
// We need to recursively extract connections from old tag structures
|
||||
// so if a user expands a connection that was tagged, the state remains
|
||||
const recurseCollect = (nodes: TreeNode[]) => {
|
||||
nodes.forEach((node) => {
|
||||
if (node.type === 'tag') {
|
||||
if (node.children) recurseCollect(node.children);
|
||||
} else if (node.type === 'connection') {
|
||||
prevMap.set(String(node.key), node);
|
||||
}
|
||||
});
|
||||
};
|
||||
recurseCollect(prev);
|
||||
|
||||
return connections.map((conn) => {
|
||||
const buildConnectionNode = (conn: SavedConnection): TreeNode => {
|
||||
const existing = prevMap.get(conn.id);
|
||||
return {
|
||||
title: conn.name,
|
||||
@@ -223,9 +244,32 @@ const Sidebar: React.FC<{ onEditConnection?: (conn: SavedConnection) => void }>
|
||||
isLeaf: false,
|
||||
children: existing?.children,
|
||||
} as TreeNode;
|
||||
};
|
||||
|
||||
const taggedConnIds = new Set<string>();
|
||||
const tagNodes: TreeNode[] = connectionTags.map((tag) => {
|
||||
tag.connectionIds.forEach(id => taggedConnIds.add(id));
|
||||
return {
|
||||
title: tag.name,
|
||||
key: `tag-${tag.id}`,
|
||||
icon: <FolderOutlined style={{ color: '#faad14' }} />,
|
||||
type: 'tag',
|
||||
dataRef: tag,
|
||||
isLeaf: false,
|
||||
children: tag.connectionIds
|
||||
.map(cid => connections.find(c => c.id === cid))
|
||||
.filter(Boolean)
|
||||
.map(conn => buildConnectionNode(conn!)),
|
||||
} as TreeNode;
|
||||
});
|
||||
|
||||
const ungroupedNodes: TreeNode[] = connections
|
||||
.filter(c => !taggedConnIds.has(c.id))
|
||||
.map(conn => buildConnectionNode(conn));
|
||||
|
||||
return [...tagNodes, ...ungroupedNodes];
|
||||
});
|
||||
}, [connections]);
|
||||
}, [connections, connectionTags]);
|
||||
|
||||
const updateTreeData = (list: TreeNode[], key: React.Key, children: TreeNode[] | undefined): TreeNode[] => {
|
||||
return list.map(node => {
|
||||
@@ -1042,6 +1086,7 @@ const Sidebar: React.FC<{ onEditConnection?: (conn: SavedConnection) => void }>
|
||||
};
|
||||
|
||||
const onLoadData = async ({ key, children, dataRef, type }: any) => {
|
||||
if (type === 'tag') return;
|
||||
if (children) return;
|
||||
|
||||
if (type === 'connection') {
|
||||
@@ -2284,6 +2329,38 @@ const Sidebar: React.FC<{ onEditConnection?: (conn: SavedConnection) => void }>
|
||||
return routineMenu;
|
||||
}
|
||||
|
||||
// Connection Tag Menu — must be BEFORE the connection check
|
||||
if (node.type === 'tag') {
|
||||
return [
|
||||
{
|
||||
key: 'edit-tag',
|
||||
label: '编辑标签',
|
||||
icon: <EditOutlined />,
|
||||
onClick: () => {
|
||||
createTagForm.setFieldsValue({ name: node.title, connectionIds: node.dataRef.connectionIds });
|
||||
setRenameViewTarget(node);
|
||||
setIsCreateTagModalOpen(true);
|
||||
}
|
||||
},
|
||||
{ type: 'divider' },
|
||||
{
|
||||
key: 'delete-tag',
|
||||
label: '删除标签',
|
||||
icon: <DeleteOutlined />,
|
||||
danger: true,
|
||||
onClick: () => {
|
||||
Modal.confirm({
|
||||
title: '确认删除',
|
||||
content: `确定要删除标签 "${node.title}" 吗?这不会删除里面的连接。`,
|
||||
onOk: () => {
|
||||
removeConnectionTag(node.dataRef.id);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
if (node.type === 'connection') {
|
||||
// Redis connection menu
|
||||
if (isRedis) {
|
||||
@@ -2358,6 +2435,22 @@ const Sidebar: React.FC<{ onEditConnection?: (conn: SavedConnection) => void }>
|
||||
];
|
||||
}
|
||||
|
||||
// Tag submenu for connection
|
||||
const tagSubMenuItems: MenuProps['items'] = connectionTags.map(tag => ({
|
||||
key: `move-to-tag-${tag.id}`,
|
||||
label: tag.name,
|
||||
icon: <FolderOutlined />,
|
||||
onClick: () => moveConnectionToTag(node.key, tag.id)
|
||||
}));
|
||||
if (connectionTags.length > 0) {
|
||||
tagSubMenuItems.push({ type: 'divider' });
|
||||
}
|
||||
tagSubMenuItems.push({
|
||||
key: 'move-to-ungrouped',
|
||||
label: '移出标签',
|
||||
onClick: () => moveConnectionToTag(node.key, null)
|
||||
});
|
||||
|
||||
// Regular database connection menu
|
||||
return [
|
||||
{
|
||||
@@ -2400,6 +2493,12 @@ const Sidebar: React.FC<{ onEditConnection?: (conn: SavedConnection) => void }>
|
||||
if (onEditConnection) onEditConnection(node.dataRef);
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'move-to-tag',
|
||||
label: '移至标签',
|
||||
icon: <FolderOpenOutlined />,
|
||||
children: tagSubMenuItems
|
||||
},
|
||||
{
|
||||
key: 'disconnect',
|
||||
label: '断开连接',
|
||||
@@ -2741,6 +2840,72 @@ const Sidebar: React.FC<{ onEditConnection?: (conn: SavedConnection) => void }>
|
||||
return <span title={hoverTitle}>{statusBadge}{displayTitle}</span>;
|
||||
};
|
||||
|
||||
const handleDrop = (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]);
|
||||
|
||||
const dragNode = info.dragNode;
|
||||
const dropNode = info.node;
|
||||
|
||||
// Tag to Tag reordering
|
||||
if (dragNode.type === 'tag') {
|
||||
// You can only drop tags onto the root level (before/after other tags or connections at root)
|
||||
if (dropNode.type === 'tag' || dropNode.type === 'connection') {
|
||||
// Get current order
|
||||
const currentTagOrder = connectionTags.map(t => t.id);
|
||||
const dragTagId = dragNode.dataRef.id;
|
||||
|
||||
// Filter out the dragging tag
|
||||
const newOrder = currentTagOrder.filter(id => id !== dragTagId);
|
||||
|
||||
let insertIndex = newOrder.length;
|
||||
if (dropNode.type === 'tag') {
|
||||
const dropTagId = dropNode.dataRef.id;
|
||||
const dropIndex = newOrder.indexOf(dropTagId);
|
||||
|
||||
if (dropPosition === -1) {
|
||||
insertIndex = dropIndex;
|
||||
} else {
|
||||
insertIndex = dropIndex + 1;
|
||||
}
|
||||
} else {
|
||||
// Dropped onto a root connection, usually meaning moving to the end of tags
|
||||
// Since tags are always displayed before ungrouped connections, just put it at the end
|
||||
insertIndex = newOrder.length;
|
||||
}
|
||||
|
||||
newOrder.splice(insertIndex, 0, dragTagId);
|
||||
reorderTags(newOrder);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Connection moving to tag (any drop position on a tag node counts as "into")
|
||||
if (dragNode.type === 'connection' && dropNode.type === 'tag') {
|
||||
moveConnectionToTag(dragNode.key, dropNode.dataRef.id);
|
||||
return;
|
||||
}
|
||||
|
||||
// Connection moving to another connection inside a tag
|
||||
if (dragNode.type === 'connection' && dropNode.type === 'connection') {
|
||||
// Find if drop target is under a tag
|
||||
const targetTag = connectionTags.find(t => t.connectionIds.includes(dropNode.key));
|
||||
if (targetTag) {
|
||||
moveConnectionToTag(dragNode.key, targetTag.id);
|
||||
return;
|
||||
}
|
||||
|
||||
// Drop target is NOT under a tag (ungrouped) -> move OUT of tag
|
||||
const sourceTag = connectionTags.find(t => t.connectionIds.includes(dragNode.key));
|
||||
if (sourceTag) {
|
||||
moveConnectionToTag(dragNode.key, null);
|
||||
return;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const onRightClick = ({ event, node }: any) => {
|
||||
const items = getNodeMenuItems(node);
|
||||
if (items && items.length > 0) {
|
||||
@@ -2758,13 +2923,25 @@ const Sidebar: React.FC<{ onEditConnection?: (conn: SavedConnection) => void }>
|
||||
<Search placeholder="搜索..." onChange={onSearch} size="small" />
|
||||
</div>
|
||||
|
||||
{/* Toolbar for batch operations - always visible */}
|
||||
<div style={{ padding: '4px 8px', borderBottom: 'none', display: 'flex', gap: 4 }}>
|
||||
{/* Toolbar */}
|
||||
<div style={{ padding: '4px 8px', borderBottom: 'none', display: 'flex', flexWrap: 'wrap', gap: 4 }}>
|
||||
<Button
|
||||
size="small"
|
||||
icon={<FolderOpenOutlined />}
|
||||
onClick={() => {
|
||||
setRenameViewTarget(null); // Create mode
|
||||
createTagForm.resetFields();
|
||||
setIsCreateTagModalOpen(true);
|
||||
}}
|
||||
style={{ flex: '1 1 auto' }}
|
||||
>
|
||||
新建组
|
||||
</Button>
|
||||
<Button
|
||||
size="small"
|
||||
icon={<CheckSquareOutlined />}
|
||||
onClick={() => openBatchOperationModal()}
|
||||
style={{ flex: 1 }}
|
||||
style={{ flex: '1 1 auto' }}
|
||||
>
|
||||
批量操作表
|
||||
</Button>
|
||||
@@ -2772,7 +2949,7 @@ const Sidebar: React.FC<{ onEditConnection?: (conn: SavedConnection) => void }>
|
||||
size="small"
|
||||
icon={<CheckSquareOutlined />}
|
||||
onClick={() => openBatchDatabaseModal()}
|
||||
style={{ flex: 1 }}
|
||||
style={{ flex: '1 1 auto' }}
|
||||
>
|
||||
批量操作库
|
||||
</Button>
|
||||
@@ -2781,6 +2958,11 @@ const Sidebar: React.FC<{ onEditConnection?: (conn: SavedConnection) => void }>
|
||||
<div ref={treeContainerRef} style={{ flex: 1, overflow: 'hidden', minHeight: 0 }}>
|
||||
<Tree
|
||||
showIcon
|
||||
draggable={{
|
||||
icon: false,
|
||||
nodeDraggable: (node: any) => node.type === 'connection' || node.type === 'tag'
|
||||
}}
|
||||
onDrop={handleDrop}
|
||||
loadData={onLoadData}
|
||||
treeData={displayTreeData}
|
||||
onDoubleClick={onDoubleClick}
|
||||
@@ -2809,6 +2991,60 @@ const Sidebar: React.FC<{ onEditConnection?: (conn: SavedConnection) => void }>
|
||||
</Dropdown>
|
||||
)}
|
||||
|
||||
<Modal
|
||||
title={renameViewTarget?.type === 'tag' ? "编辑标签" : "新建组"}
|
||||
open={isCreateTagModalOpen}
|
||||
onOk={() => {
|
||||
createTagForm.validateFields().then(values => {
|
||||
if (renameViewTarget?.type === 'tag') {
|
||||
// Rename
|
||||
updateConnectionTag({
|
||||
...renameViewTarget.dataRef,
|
||||
name: values.name,
|
||||
connectionIds: values.connectionIds || []
|
||||
});
|
||||
// update cross-connections
|
||||
const allOtherTagsIds = connectionTags.filter(t => t.id !== renameViewTarget.dataRef.id).flatMap(t => t.connectionIds);
|
||||
(values.connectionIds || []).forEach((cid: string) => {
|
||||
if (allOtherTagsIds.includes(cid)) {
|
||||
moveConnectionToTag(cid, renameViewTarget.dataRef.id);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// Create
|
||||
const tagId = Date.now().toString();
|
||||
addConnectionTag({
|
||||
id: tagId,
|
||||
name: values.name,
|
||||
connectionIds: values.connectionIds || []
|
||||
});
|
||||
(values.connectionIds || []).forEach((cid: string) => {
|
||||
moveConnectionToTag(cid, tagId);
|
||||
});
|
||||
}
|
||||
setIsCreateTagModalOpen(false);
|
||||
});
|
||||
}}
|
||||
onCancel={() => setIsCreateTagModalOpen(false)}
|
||||
>
|
||||
<Form form={createTagForm} layout="vertical">
|
||||
<Form.Item name="name" label="标签名称" rules={[{ required: true, message: '请输入标签名称' }]}>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item name="connectionIds" label="选择连接">
|
||||
<Checkbox.Group style={{ width: '100%' }}>
|
||||
<Space direction="vertical" style={{ width: '100%', maxHeight: '400px', overflowY: 'auto' }}>
|
||||
{connections.map(conn => (
|
||||
<Checkbox key={conn.id} value={conn.id}>
|
||||
{conn.name} {conn.config.host ? `(${conn.config.host})` : ''}
|
||||
</Checkbox>
|
||||
))}
|
||||
</Space>
|
||||
</Checkbox.Group>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
title="新建数据库"
|
||||
open={isCreateDbModalOpen}
|
||||
|
||||
Reference in New Issue
Block a user