diff --git a/backend/app/config.py b/backend/app/config.py index d6fa0ea..8ae9591 100644 --- a/backend/app/config.py +++ b/backend/app/config.py @@ -5,7 +5,7 @@ class Settings: def __init__(self): self.storage_dir = os.environ.get("STORAGE_DIR", "/data/documents") self.meilisearch_host = os.environ.get("MEILISEARCH_HOST", "http://localhost:7700") - self.meilisearch_api_key = os.environ.get("MEILISEARCH_API_KEY", "") + self.meilisearch_api_key = os.environ.get("MEILISEARCH_API_KEY") or None self.meilisearch_index = os.environ.get("MEILISEARCH_INDEX", "documents") self.cors_origins = os.environ.get("CORS_ORIGINS", "http://localhost:5173").split(",") diff --git a/frontend/index.html b/frontend/index.html index 0694783..86963df 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -2,6 +2,7 @@ + DocHub diff --git a/frontend/public/favicon.svg b/frontend/public/favicon.svg new file mode 100644 index 0000000..8b4eb70 --- /dev/null +++ b/frontend/public/favicon.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/frontend/src/components/FileExplorer.tsx b/frontend/src/components/FileExplorer.tsx index 270c551..50528cd 100644 --- a/frontend/src/components/FileExplorer.tsx +++ b/frontend/src/components/FileExplorer.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState } from 'react'; +import { useEffect, useRef, useState } from 'react'; import { listDocuments, type DocumentFile } from '../api/client'; interface FileExplorerProps { @@ -37,6 +37,27 @@ export default function FileExplorer({ onSelectFile, selectedKey, refreshKey }: ); } +function SidebarLabel({ text }: { text: string }) { + const ref = useRef(null); + const [overflows, setOverflows] = useState(false); + + useEffect(() => { + const el = ref.current; + if (!el) return; + const check = () => setOverflows(el.scrollWidth > el.clientWidth); + check(); + const ro = new ResizeObserver(check); + ro.observe(el); + return () => ro.disconnect(); + }, [text]); + + return ( + + {text} + + ); +} + function FileRow({ file, onSelectFile, @@ -56,7 +77,7 @@ function FileRow({ - {file.name} + ); } @@ -96,7 +117,7 @@ function FolderNode({ - {name} + {expanded && (
diff --git a/frontend/src/styles.css b/frontend/src/styles.css index 147b26b..768a470 100644 --- a/frontend/src/styles.css +++ b/frontend/src/styles.css @@ -234,12 +234,6 @@ body { min-width: 0; } -.tree-item .foldername { - flex: 1; - overflow: hidden; - text-overflow: ellipsis; -} - .tree-item:hover { background: rgba(27, 27, 24, 0.045); } @@ -278,10 +272,43 @@ body { padding-left: 20px; } -.file-row .filename { +/* ---------- Sidebar label + tooltip ---------- */ +.sidebar-label { flex: 1; + min-width: 0; + position: relative; +} + +.label-text { + display: block; overflow: hidden; text-overflow: ellipsis; + white-space: nowrap; +} + +.sidebar-label.has-tip::after { + content: attr(data-tip); + position: fixed; + left: 282px; + transform: translateY(-50%); + top: var(--tip-y, 50%); + background: var(--pine-deep); + color: var(--paper); + font-family: 'IBM Plex Mono', monospace; + font-size: 11px; + font-weight: 400; + white-space: nowrap; + padding: 5px 9px; + border-radius: 5px; + pointer-events: none; + opacity: 0; + box-shadow: 0 4px 14px rgba(27, 27, 24, 0.18); + z-index: 200; + transition: opacity 0.08s; +} + +.sidebar-label.has-tip:hover::after { + opacity: 1; } .file-icon { diff --git a/frontend/tests/App.test.tsx b/frontend/tests/App.test.tsx index ff3c9a6..fe8ec53 100644 --- a/frontend/tests/App.test.tsx +++ b/frontend/tests/App.test.tsx @@ -1,4 +1,4 @@ -import { render, screen, fireEvent } from '@testing-library/react'; +import { render, screen, fireEvent, act } from '@testing-library/react'; import { test, expect, beforeEach, afterEach, vi } from 'vitest'; import App from '../src/App'; import userEvent from '@testing-library/user-event'; @@ -39,8 +39,10 @@ afterEach(() => { vi.restoreAllMocks(); }); -test('renders the DocHub heading', () => { - render(); +test('renders the DocHub heading', async () => { + await act(async () => { + render(); + }); expect(screen.getByRole('heading', { name: 'DocHub' })).toBeInTheDocument(); }); @@ -149,18 +151,25 @@ test('jumping to a search result clears the search input so it does not show sta }); test('typing a query does not show a false "no results" message before the debounced search resolves', async () => { - vi.useFakeTimers(); vi.mocked(listDocuments).mockResolvedValue({ folders: [], files: [] }); vi.mocked(search).mockResolvedValue({ results: [] }); - render(); + // Settle the initial FileExplorer fetch before switching to fake timers. + await act(async () => { + render(); + }); + + vi.useFakeTimers(); const searchInput = screen.getByRole('textbox', { name: '搜索文档' }); fireEvent.change(searchInput, { target: { value: '检索' } }); expect(screen.queryByTestId('search-no-results')).not.toBeInTheDocument(); - await vi.advanceTimersByTimeAsync(300); + // Advance the debounce and flush the resulting state updates inside act. + await act(async () => { + await vi.advanceTimersByTimeAsync(300); + }); expect(screen.getByTestId('search-no-results')).toBeInTheDocument(); diff --git a/frontend/tests/setup.ts b/frontend/tests/setup.ts index bb02c60..2d2e71b 100644 --- a/frontend/tests/setup.ts +++ b/frontend/tests/setup.ts @@ -1 +1,8 @@ import '@testing-library/jest-dom/vitest'; + +// jsdom doesn't implement ResizeObserver; provide a no-op stub for tests. +global.ResizeObserver = class ResizeObserver { + observe() {} + unobserve() {} + disconnect() {} +};