From 08a121134138899d0f04093c63ca40c01a1b6a86 Mon Sep 17 00:00:00 2001 From: Tianyang Date: Sat, 1 Aug 2026 18:44:32 +0800 Subject: [PATCH] feat: favicon, sidebar tooltip, fix Meilisearch auth and test warnings - Add SVG favicon using pine/paper design tokens; wire into index.html - Add SidebarLabel component with ResizeObserver overflow detection and CSS-only tooltip (pine-deep bg, IBM Plex Mono, 0.08s fade, no delay) - Fix Meilisearch invalid_api_key: default api key to None instead of '' so no Authorization header is sent when auth is disabled - Stub ResizeObserver in test setup for jsdom compatibility - Wrap async renders in act() and advance fake timers inside act() to eliminate all act() warnings in App.test.tsx --- backend/app/config.py | 2 +- frontend/index.html | 1 + frontend/public/favicon.svg | 17 ++++++++++ frontend/src/components/FileExplorer.tsx | 27 ++++++++++++++-- frontend/src/styles.css | 41 ++++++++++++++++++++---- frontend/tests/App.test.tsx | 21 ++++++++---- frontend/tests/setup.ts | 7 ++++ 7 files changed, 99 insertions(+), 17 deletions(-) create mode 100644 frontend/public/favicon.svg 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() {} +};