From c517f6d146ccd13fa69dd70b0322e7abc6c6bfd1 Mon Sep 17 00:00:00 2001 From: clean Date: Wed, 16 Sep 2026 18:02:21 +0800 Subject: [PATCH 1/2] =?UTF-8?q?refactor(ui):=20=E6=8C=89=E8=AE=BE=E8=AE=A1?= =?UTF-8?q?=E7=A8=BF=E9=87=8D=E6=9E=84=E5=B7=A5=E4=BD=9C=E5=8C=BA=E4=B8=8E?= =?UTF-8?q?=20Diff=20=E5=B8=83=E5=B1=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 6 +- apps/web/src/components/DiffViewer.tsx | 117 ++- apps/web/src/components/Layout.tsx | 469 ++++++------ apps/web/src/components/LayoutSettings.tsx | 100 ++- apps/web/src/components/PanelResizeHandle.tsx | 16 +- apps/web/src/components/PanelToggle.tsx | 47 ++ apps/web/src/components/WorkspaceMenu.tsx | 91 +++ apps/web/src/components/WorkspaceTree.tsx | 216 ++++-- apps/web/src/components/diff-lines.ts | 17 + .../components/settings/SettingsCenter.tsx | 2 +- apps/web/src/pages/RepositoryDetailPage.tsx | 666 +++++++++--------- apps/web/src/settings.css | 8 +- apps/web/src/stores/workspaceLayout.ts | 17 +- apps/web/src/workspace-layout.css | 629 +++++++++++++++-- apps/web/tests/diff-preview.test.mjs | 28 +- apps/web/tests/workspace-fixture.mjs | 20 +- apps/web/tests/workspace-layout.test.mjs | 56 +- 17 files changed, 1763 insertions(+), 742 deletions(-) create mode 100644 apps/web/src/components/PanelToggle.tsx create mode 100644 apps/web/src/components/WorkspaceMenu.tsx diff --git a/README.md b/README.md index e444f07..65bed43 100644 --- a/README.md +++ b/README.md @@ -115,8 +115,10 @@ Linux CI 的桌面测试使用 `xvfb-run -a`。测试中的模拟更新适配器 空文件显示“新增空文件”,二进制文件和无法读取的文件有明确反馈。预览支持 UTF-8 文本,新增文件/暂存内容及差异输出限制为 1 MiB,差异补丁最多 10,000 行;超限时显示提示。预览不写入文件或暂存区。验证范围和结果见 [新增文件差异预览验收](https://github.com/ShaoClean/remote-git/wiki/Issue-18-Validation)。 -工作区侧栏与改动列表支持拖动调宽,也可从“布局设置”使用滑块调整或恢复默认。分隔线支持方向键、Shift 加速和 Home / End;`Cmd/Ctrl + \` 开关侧栏。布局偏好在本机保存,保留现有工作区树的展开与排序设置。 +工作区采用左侧仓库导航、中央 Diff、右侧改动与提交布局。顶栏按钮可分别隐藏或重新打开两侧面板,Diff 工具栏的“专注阅读差异”可一次隐藏两侧。左下角的应用菜单集中提供设置与帮助;左栏隐藏时,菜单移至顶栏。 + +两侧面板支持拖动调宽,也可在“设置 → 布局”调整显隐、宽度、默认 Diff 模式或恢复默认。分隔线支持方向键、Shift 加速和 Home / End;`Cmd/Ctrl+B` 切换左侧,`Cmd/Ctrl+Shift+B` 切换右侧,`Cmd/Ctrl+,` 打开设置,原有 `Cmd/Ctrl+\` 仍可切换左侧。布局偏好在本机保存,恢复默认不会清除工作区树的展开与排序设置。分栏 Diff 的长行自动折行。 窄窗口选择文件或提交后进入检查器,使用“返回列表”继续操作。提交摘要和描述在本次页面会话内按仓库保留;刷新页面会清空草稿。 -界面截图、验证结果和测试环境说明见 [工作区布局验收](https://github.com/ShaoClean/remote-git/wiki/Issue-6-Validation)。 +本次重构截图与验证结果见 [Issue #31 验收](https://github.com/ShaoClean/remote-git/wiki/Issue-31-Validation)。既有工作区布局记录见 [工作区布局验收](https://github.com/ShaoClean/remote-git/wiki/Issue-6-Validation)。 diff --git a/apps/web/src/components/DiffViewer.tsx b/apps/web/src/components/DiffViewer.tsx index 6c03e2b..f20133b 100644 --- a/apps/web/src/components/DiffViewer.tsx +++ b/apps/web/src/components/DiffViewer.tsx @@ -2,13 +2,17 @@ import { useEffect, useRef, useState } from 'react'; import { Button, Modal, Segmented } from 'antd'; import { CloseOutlined, DiffOutlined, ExpandOutlined } from '@ant-design/icons'; import ReactDiffViewer, { DiffMethod } from 'react-diff-viewer-continued'; -import { getDiffLines, getDiffNotice } from './diff-lines'; +import { getNumberedDiffLines, getDiffNotice } from './diff-lines'; +import type { NumberedDiffLine } from './diff-lines'; +import { useWorkspaceStore } from '../stores/workspaceStore'; interface Props { oldCode?: string; newCode?: string; diff?: string; title?: string; + subtitle?: string; + onFocus?: () => void; splitView?: boolean; onClose?: () => void; loading?: boolean; @@ -23,10 +27,12 @@ interface SplitDiffRow { right?: string; leftKind?: SplitCellKind; rightKind?: SplitCellKind; + oldLine?: number; + newLine?: number; } function getSplitDiffRows(diff: string): SplitDiffRow[] { - const lines = getDiffLines(diff); + const lines = getNumberedDiffLines(diff); const rows: SplitDiffRow[] = []; for (let index = 0; index < lines.length; ) { @@ -38,21 +44,23 @@ function getSplitDiffRows(diff: string): SplitDiffRow[] { } if (line.kind === 'remove') { - const removed: string[] = []; + const removed: NumberedDiffLine[] = []; while (index < lines.length && lines[index].kind === 'remove') { - removed.push(lines[index].text.slice(1)); + removed.push(lines[index]); index += 1; } - const added: string[] = []; + const added: NumberedDiffLine[] = []; while (index < lines.length && lines[index].kind === 'add') { - added.push(lines[index].text.slice(1)); + added.push(lines[index]); index += 1; } const rowCount = Math.max(removed.length, added.length); for (let row = 0; row < rowCount; row += 1) { rows.push({ - left: removed[row] || '', - right: added[row] || '', + left: removed[row]?.text.slice(1) || '', + right: added[row]?.text.slice(1) || '', + oldLine: removed[row]?.oldLine, + newLine: added[row]?.newLine, leftKind: removed[row] === undefined ? 'empty' : 'remove', rightKind: added[row] === undefined ? 'empty' : 'add', }); @@ -61,19 +69,32 @@ function getSplitDiffRows(diff: string): SplitDiffRow[] { } if (line.kind === 'add') { - const added: string[] = []; + const added: NumberedDiffLine[] = []; while (index < lines.length && lines[index].kind === 'add') { - added.push(lines[index].text.slice(1)); + added.push(lines[index]); index += 1; } added.forEach((value) => - rows.push({ left: '', right: value, leftKind: 'empty', rightKind: 'add' }), + rows.push({ + left: '', + right: value.text.slice(1), + newLine: value.newLine, + leftKind: 'empty', + rightKind: 'add', + }), ); continue; } const context = line.text.slice(1); - rows.push({ left: context, right: context, leftKind: 'context', rightKind: 'context' }); + rows.push({ + left: context, + right: context, + oldLine: line.oldLine, + newLine: line.newLine, + leftKind: 'context', + rightKind: 'context', + }); index += 1; } @@ -85,12 +106,20 @@ export function DiffViewer({ newCode = '', diff, title, - splitView = false, + subtitle, + onFocus, + splitView, onClose, loading = false, error, }: Props) { - const [mode, setMode] = useState<'unified' | 'split'>(splitView ? 'split' : 'unified'); + const preferredMode = useWorkspaceStore((state) => state.layout.diffMode); + const [mode, setMode] = useState<'unified' | 'split'>( + splitView === undefined ? preferredMode : splitView ? 'split' : 'unified', + ); + useEffect(() => { + setMode(splitView === undefined ? preferredMode : splitView ? 'split' : 'unified'); + }, [preferredMode, splitView]); const [zoomed, setZoomed] = useState(false); const bodyRef = useRef(null); const zoomBodyRef = useRef(null); @@ -102,24 +131,37 @@ export function DiffViewer({ }, [diff, error, loading, mode, title, zoomed]); const renderUnifiedDiff = (value: string) => { - const lines = getDiffLines(value); + const lines = getNumberedDiffLines(value); return ( -
-        {lines.map(({ text: line, kind }, index) => {
-          const className = kind === 'context' ? undefined : `diff-line--${kind}`;
+      
+ {lines.map(({ text: line, kind, oldLine, newLine }, index) => { + const className = `diff-code-row diff-code-row--${kind}`; return ( - - {line} - {index < lines.length - 1 ? '\n' : ''} - +
+ {kind !== 'meta' && ( + <> + + + + )} + {line} +
); })} -
+ ); }; const renderSplitDiff = (value: string) => (
+
+ 原版本 + 修改后 +
{getSplitDiffRows(value).map((row, index) => row.meta !== undefined ? (
@@ -127,8 +169,18 @@ export function DiffViewer({
) : (
- {row.left} - {row.right} + + + {row.left} + + + + {row.right} +
), )} @@ -175,7 +227,10 @@ export function DiffViewer({
- {title || '差异预览'} +
+ {title || '差异预览'} + {subtitle && {subtitle}} +
视图 @@ -192,13 +247,11 @@ export function DiffViewer({ type="text" size="small" icon={} - aria-label="放大查看差异" - title="放大查看差异" + aria-label={onFocus ? '专注阅读差异' : '放大查看差异'} + title={onFocus ? '专注阅读差异 · 隐藏两侧面板' : '放大查看差异'} disabled={loading || Boolean(error) || !hasDiff} - onClick={() => setZoomed(true)} - > - 放大 - + onClick={() => (onFocus ? onFocus() : setZoomed(true))} + /> {onClose && ( + 工作区 + {compact && ( + setMobileNavOpen(false)} + /> + )}
-
+ } + allowClear + value={repositoryQuery} + onChange={(event) => setRepositoryQuery(event.target.value)} + />
-
- 工作区 - - {connections.length + repositories.length} - -
-
-
- - - - - - - - - -
-
- -
- RemoteGit - 工作区就绪 -
- - {isDesktop - ? updateState - ? `v${updateState.currentVersion}` - : '…' - : `v${__APP_VERSION__}`} - -
+ {sidebarVisible && ( + { + setMobileNavOpen(false); + openSettings(); + }} + /> + )}
@@ -359,144 +393,111 @@ export function Layout() { )}
-
- {compact && ( -
{workspaceOutlet.current}
-
-
-
+
+
+ - - {activeRepository && ( - <> - - - {activeRepository.path || '仓库工作区'} - - - )} -
-
- - 上次刷新{' '} - {new Date().toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' })} - -
-
- + + 已打开仓库 · {openRepositories.length} + + + + {activeRepository && ( + <> + + + {activeRepository.path || '仓库工作区'} + + + )} +
+
+ + 上次刷新{' '} + {new Date().toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' })} + +
+
); diff --git a/apps/web/src/components/LayoutSettings.tsx b/apps/web/src/components/LayoutSettings.tsx index 4c7cc82..5e83b1a 100644 --- a/apps/web/src/components/LayoutSettings.tsx +++ b/apps/web/src/components/LayoutSettings.tsx @@ -1,4 +1,4 @@ -import { Button, Modal, Switch } from 'antd'; +import { Button, Modal, Segmented, Switch } from 'antd'; import { useWorkspaceStore } from '../stores/workspaceStore'; import { CHANGES_MAX, CHANGES_MIN, SIDEBAR_MAX, SIDEBAR_MIN } from '../stores/workspaceLayout'; @@ -20,37 +20,73 @@ export function LayoutSettingsContent() { const { layout, updateLayout, resetLayout } = useWorkspaceStore(); return (
-

窗口较小时自动适配;放大后恢复已保存的宽度。

- - -
- 收起工作区 - updateLayout({ sidebarCollapsed })} - /> -
+
+

侧边面板

+

独立显示或隐藏两侧面板,为差异阅读留出空间。

+
+ + 显示左侧工作区连接与仓库 · ⌘ / Ctrl B + + updateLayout({ sidebarCollapsed: !visible })} + /> +
+
+ + 显示右侧面板改动、提交与历史 · ⌘ / Ctrl ⇧ B + + updateLayout({ changesCollapsed: !visible })} + /> +
+
+
+

面板宽度

+

窗口较小时自动适配;放大后恢复已保存的宽度。

+ + +
+
+

差异视图

+
+ + 默认 Diff 模式也可在差异工具栏随时切换 + + updateLayout({ diffMode: diffMode as 'unified' | 'split' })} + /> +
+

布局在此设备保存,恢复默认布局会保留工作区树的展开与排序设置。

diff --git a/apps/web/src/components/PanelResizeHandle.tsx b/apps/web/src/components/PanelResizeHandle.tsx index cb87149..12fabd4 100644 --- a/apps/web/src/components/PanelResizeHandle.tsx +++ b/apps/web/src/components/PanelResizeHandle.tsx @@ -9,6 +9,7 @@ interface Props { max: number; onChange: (width: number) => void; className?: string; + side?: 'left' | 'right'; } export function PanelResizeHandle({ @@ -19,6 +20,7 @@ export function PanelResizeHandle({ max, onChange, className = '', + side = 'left', }: Props) { const start = useRef<{ x: number; width: number } | null>(null); const [dragging, setDragging] = useState(false); @@ -45,7 +47,14 @@ export function PanelResizeHandle({ }} onPointerMove={(event) => { if (start.current && event.currentTarget.hasPointerCapture(event.pointerId)) { - onChange(clampWidth(start.current.width + event.clientX - start.current.x, min, max)); + const direction = side === 'right' ? -1 : 1; + onChange( + clampWidth( + start.current.width + (event.clientX - start.current.x) * direction, + min, + max, + ), + ); } }} onPointerUp={(event) => { @@ -58,15 +67,16 @@ export function PanelResizeHandle({ }} onKeyDown={(event) => { const step = event.shiftKey ? 40 : 10; + const direction = side === 'right' ? -1 : 1; const next = event.key === 'Home' ? min : event.key === 'End' ? max : event.key === 'ArrowLeft' - ? value - step + ? value - step * direction : event.key === 'ArrowRight' - ? value + step + ? value + step * direction : undefined; if (next === undefined) return; event.preventDefault(); diff --git a/apps/web/src/components/PanelToggle.tsx b/apps/web/src/components/PanelToggle.tsx new file mode 100644 index 0000000..562fe05 --- /dev/null +++ b/apps/web/src/components/PanelToggle.tsx @@ -0,0 +1,47 @@ +interface Props { + side: 'left' | 'right'; + expanded: boolean; + controls: string; + onClick: () => void; + disabled?: boolean; +} + +export function PanelToggle({ side, expanded, controls, onClick, disabled = false }: Props) { + const label = `${expanded ? '隐藏' : '显示'}${side === 'left' ? '左侧工作区' : '右侧面板'}`; + return ( + + ); +} diff --git a/apps/web/src/components/WorkspaceMenu.tsx b/apps/web/src/components/WorkspaceMenu.tsx new file mode 100644 index 0000000..d96dc6e --- /dev/null +++ b/apps/web/src/components/WorkspaceMenu.tsx @@ -0,0 +1,91 @@ +import { useRef, useState } from 'react'; +import { Dropdown } from 'antd'; +import { QuestionCircleOutlined, SettingOutlined, UpOutlined } from '@ant-design/icons'; +import { BrandIcon } from './BrandIcon'; + +export function WorkspaceMenu({ + compact = false, + version, + onSettings, +}: { + compact?: boolean; + version: string; + onSettings: () => void; +}) { + const [open, setOpen] = useState(false); + const trigger = useRef(null); + const close = () => { + setOpen(false); + trigger.current?.focus(); + }; + return ( + , + label: ( + + 设置⌘ / Ctrl , + + ), + onClick: onSettings, + }, + { type: 'divider' }, + { + key: 'help', + icon: , + label: ( + + 帮助与文档 + + ), + }, + ], + onClick: close, + onKeyDown: (event) => { + if (event.key === 'Escape') { + event.stopPropagation(); + close(); + } + }, + }} + > + + + ); +} diff --git a/apps/web/src/components/WorkspaceTree.tsx b/apps/web/src/components/WorkspaceTree.tsx index 25f5362..a85af04 100644 --- a/apps/web/src/components/WorkspaceTree.tsx +++ b/apps/web/src/components/WorkspaceTree.tsx @@ -1,6 +1,12 @@ import { useEffect, useRef, useState } from 'react'; import type { DragEvent, KeyboardEvent } from 'react'; -import { BranchesOutlined, DownOutlined, FolderOpenOutlined, HolderOutlined, RightOutlined } from '@ant-design/icons'; +import { + BranchesOutlined, + DownOutlined, + FolderOpenOutlined, + HolderOutlined, + RightOutlined, +} from '@ant-design/icons'; import type { Repository } from '@remote-git/shared'; import { useWorkspaceStore } from '../stores/workspaceStore'; import { RepositoryStatusIndicator } from './RepositoryStatusIndicator'; @@ -8,20 +14,37 @@ import { useRepositoryStore } from '../stores/repositoryStore'; import { canMoveTreeItem, orderItems } from '../stores/sidebarOrder'; import type { Placement, TreeItem } from '../stores/sidebarOrder'; -interface Connection { id: string; name: string; status?: string } +interface Connection { + id: string; + name: string; + status?: string; +} interface Props { connections: Connection[]; repositories: Repository[]; testResults: Record; activeId?: string; + query?: string; onOpenRepository: (repo: Repository) => void; } type DropTarget = { item: TreeItem; placement: Placement }; -export function WorkspaceTree({ connections, repositories, testResults, activeId, onOpenRepository }: Props) { +export function WorkspaceTree({ + connections, + repositories, + testResults, + activeId, + query = '', + onOpenRepository, +}: Props) { const { - treeOpen, setTreeOpen, collapsedConnectionIds, setConnectionCollapsed, - connectionOrder, repositoryOrderByConnection, moveTreeItem, + treeOpen, + setTreeOpen, + collapsedConnectionIds, + setConnectionCollapsed, + connectionOrder, + repositoryOrderByConnection, + moveTreeItem, } = useWorkspaceStore(); const { listLoaded, listError, fetchRepositories } = useRepositoryStore(); const [dragging, setDragging] = useState(null); @@ -33,10 +56,21 @@ export function WorkspaceTree({ connections, repositories, testResults, activeId const scrollFrame = useRef(null); const pointerY = useRef(null); - const groups = orderItems(connections, connectionOrder).map((connection) => ({ - ...connection, - repositories: orderItems(repositories.filter((repo) => repo.connectionId === connection.id), repositoryOrderByConnection[connection.id]), - })); + const search = query.trim().toLowerCase(); + const groups = orderItems(connections, connectionOrder) + .map((connection) => ({ + ...connection, + repositories: orderItems( + repositories.filter( + (repo) => + repo.connectionId === connection.id && + (!search || + `${connection.name} ${repo.name} ${repo.path}`.toLowerCase().includes(search)), + ), + repositoryOrderByConnection[connection.id], + ), + })) + .filter((connection) => !search || connection.repositories.length > 0); const stopScroll = () => { if (scrollFrame.current !== null) cancelAnimationFrame(scrollFrame.current); @@ -56,7 +90,10 @@ export function WorkspaceTree({ connections, repositories, testResults, activeId if (event.key === 'Escape') finishDrag(); }; document.addEventListener('keydown', cancel); - return () => { document.removeEventListener('keydown', cancel); stopScroll(); }; + return () => { + document.removeEventListener('keydown', cancel); + stopScroll(); + }; }, []); const autoScroll = (event: DragEvent) => { @@ -65,10 +102,17 @@ export function WorkspaceTree({ connections, repositories, testResults, activeId if (scrollFrame.current !== null) return; const tick = () => { const container = tree.current?.closest('.app-sidebar__content'); - if (!container || pointerY.current === null) { stopScroll(); return; } + if (!container || pointerY.current === null) { + stopScroll(); + return; + } const { top, bottom } = container.getBoundingClientRect(); - const distance = pointerY.current - top < 36 ? pointerY.current - top - 36 - : bottom - pointerY.current < 36 ? 36 - (bottom - pointerY.current) : 0; + const distance = + pointerY.current - top < 36 + ? pointerY.current - top - 36 + : bottom - pointerY.current < 36 + ? 36 - (bottom - pointerY.current) + : 0; container.scrollTop += Math.max(-14, Math.min(14, distance / 2)); scrollFrame.current = requestAnimationFrame(tick); }; @@ -81,7 +125,8 @@ export function WorkspaceTree({ connections, repositories, testResults, activeId setDragging(item); event.dataTransfer.effectAllowed = 'move'; event.dataTransfer.setData('application/x-remote-git-tree', JSON.stringify(item)); - if (event.currentTarget.parentElement) event.dataTransfer.setDragImage(event.currentTarget.parentElement, 15, 15); + if (event.currentTarget.parentElement) + event.dataTransfer.setDragImage(event.currentTarget.parentElement, 15, 15); }; const position = (event: DragEvent): Placement => { @@ -102,8 +147,13 @@ export function WorkspaceTree({ connections, repositories, testResults, activeId event.preventDefault(); event.dataTransfer.dropEffect = 'move'; const placement = position(event); - setDropTarget((current) => current?.item.kind === item.kind && current.item.id === item.id && current.placement === placement - ? current : { item, placement }); + setDropTarget((current) => + current?.item.kind === item.kind && + current.item.id === item.id && + current.placement === placement + ? current + : { item, placement }, + ); }; const drop = (event: DragEvent, item: TreeItem) => { @@ -115,7 +165,12 @@ export function WorkspaceTree({ connections, repositories, testResults, activeId finishDrag(); }; - const keyboardMove = (event: KeyboardEvent, item: TreeItem, siblings: { id: string }[], name: string) => { + const keyboardMove = ( + event: KeyboardEvent, + item: TreeItem, + siblings: { id: string }[], + name: string, + ) => { if (!event.altKey || !['ArrowUp', 'ArrowDown'].includes(event.key)) return; event.preventDefault(); event.stopPropagation(); @@ -137,66 +192,141 @@ export function WorkspaceTree({ connections, repositories, testResults, activeId aria-label={`调整${item.kind === 'connection' ? '连接' : '仓库'} ${name} 的顺序`} aria-keyshortcuts="Alt+ArrowUp Alt+ArrowDown" title="拖动排序,或按 Alt + ↑ / ↓" - disabled={siblings.length < 2} - draggable={siblings.length > 1} + disabled={Boolean(search) || siblings.length < 2} + draggable={!search && siblings.length > 1} onDragStart={(event) => startDrag(event, item)} onDragEnd={finishDrag} onClick={(event) => event.stopPropagation()} onKeyDown={(event) => keyboardMove(event, item, siblings, name)} - > + > + + ); - const dropClass = (item: TreeItem) => dropTarget?.item.kind === item.kind && dropTarget.item.id === item.id - ? ` tree-drop--${dropTarget.placement}` : ''; - const draggingClass = (item: TreeItem) => dragging?.kind === item.kind && dragging.id === item.id ? ' tree-item--dragging' : ''; + const dropClass = (item: TreeItem) => + dropTarget?.item.kind === item.kind && dropTarget.item.id === item.id + ? ` tree-drop--${dropTarget.placement}` + : ''; + const draggingClass = (item: TreeItem) => + dragging?.kind === item.kind && dragging.id === item.id ? ' tree-item--dragging' : ''; return ( <> - - {treeOpen && ( + {(treeOpen || search) && (
{ - if (!event.currentTarget.contains(event.relatedTarget as Node | null)) { setDropTarget(null); stopScroll(); } + if (!event.currentTarget.contains(event.relatedTarget as Node | null)) { + setDropTarget(null); + stopScroll(); + } }} onClickCapture={(event) => { - if (dragSource.current || Date.now() < suppressClickUntil.current) { event.preventDefault(); event.stopPropagation(); } + if (dragSource.current || Date.now() < suppressClickUntil.current) { + event.preventDefault(); + event.stopPropagation(); + } }} > - {listError ?
仓库列表加载失败
- : !listLoaded &&
正在加载已登记仓库…
} - {groups.length === 0 &&
暂无连接
} + {listError ? ( +
+ 仓库列表加载失败{' '} + +
+ ) : ( + !listLoaded && ( +
+ 正在加载已登记仓库… +
+ ) + )} + {groups.length === 0 && ( +
{search ? '没有匹配的仓库' : '暂无连接'}
+ )} {groups.map((connection) => { const item: TreeItem = { kind: 'connection', id: connection.id }; - const open = !collapsedConnectionIds.includes(connection.id); + const open = Boolean(search) || !collapsedConnectionIds.includes(connection.id); const childrenId = `connection-repositories-${connection.id}`; return ( -
dragOver(event, item)} onDrop={(event) => drop(event, item)}> +
dragOver(event, item)} + onDrop={(event) => drop(event, item)} + >
{sortHandle(item, groups, connection.name)} -