fix(web): 书签删除 UI 与删除安全性修复#22
Open
youxuezhe7 wants to merge 14 commits into
Open
Conversation
… fix - 详情页删除按钮添加 Loader2 动画 - useBookmarkDelete 改用 getState() 读取最新 folders,避免闭包过期 - 移除 DropdownMenuContent 无效的 onClick preventDefault(Portal 渲染) - void -> .catch() 修复 biome noVoid lint 错误 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
点击书签卡片的 ··· 下拉菜单中的"删除"选项时,Radix DropdownMenu 关闭后事件链会触发父级 <Link> 导航到详情页,而非弹出删除确认对话框。 修复:将 DropdownMenu 改为受控模式,在删除项的 onSelect 中 调用 event.preventDefault() 阻止 Radix 默认关闭行为, 改为手动关闭菜单后再打开对话框。 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
详情页 header 中标题过长时会把右侧删除按钮挤出屏幕。 修复:标题 span 加 min-w-0 使 line-clamp-1 在 flex 中生效、 返回按钮加 shrink-0 防止被压缩、右侧按钮区加 shrink-0 保护。 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
… dropdown click-through Two previous fixes were incomplete: 1. bookmark-detail-client: line-clamp-1 sets display: -webkit-box, which breaks flex layout and nullifies min-w-0. Switch to truncate which properly constrains text in a flex row. 2. bookmark-card: Radix DropdownMenu renders in a portal. Calling setDropdownOpen(false) immediately unmounts the portal, causing remaining pointer/click events to pass through to the card Link underneath. Delay close by 100ms to let the click cycle complete. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Replace setTimeout-based approach with direct onClick guard on the card Link. When the dropdown menu, delete dialog, or move dialog is open, prevent navigation via e.preventDefault(). This is more reliable than timing-based fixes for the portal click-through issue. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…reen The confirmation dialog description uses the full bookmark title. When the title is a long URL with no word breaks, it stretches the dialog horizontally, pushing the Cancel and Delete buttons out of view. Add line-clamp-2 and break-all to constrain the text. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…rent delete rollback Two fixes from Codex adversarial review: 1. [HIGH] DELETE handler now calls @vercel/blob del() on existing.fileUrl before removing the DB row. Blob failure is caught and logged so it never blocks DB cleanup. Orphaned public files are no longer left behind when deleting file-type bookmarks. 2. [MEDIUM] Rewrite deleteBookmark to use surgical rollback instead of full-snapshot rollback. Pending deletes are tracked per-id so concurrent deletes don't cross-contaminate. DELETE 404 is treated as idempotent success. On failure only the specific bookmark is restored rather than the entire list, pagination, and cache. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Two fixes from Codex review round 2: 1. [HIGH] Delete DB row before Blob: If DB fails, bookmark and file both survive. If DB succeeds but Blob fails, the file is orphaned (console.error logged) but no data loss occurs. 2. [MEDIUM] Filter pendingDeletes from all fetchBookmarks write paths (cache hit append/replace and network success append/replace). Also re-filter the current list in deleteBookmark success path before clearing the pending flag, preventing concurrent refreshes from re-inserting a bookmark that was just deleted. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Two fixes from Codex review round 3: 1. [HIGH] Blob deletion no longer blocks the DELETE response. Use del().catch() (fire-and-forget) instead of await, so a degraded Blob service cannot make the client see a failed DELETE after the DB row is already gone. 2. [MEDIUM] Replace precision-rollback with refetch on failure. The old approach spliced a captured bookmark back into whatever list was current, violating filter invariants if the user changed views during the request. Now on failure we clear cache, remove the pending flag, and call fetchBookmarks() to reconcile with the server. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…imeout Two fixes from Codex review round 4: 1. [HIGH] Cache now stores filtered bookmarks (pendingDeletes removed before writing to cache), preventing deleted IDs from re-entering the cache layer. Previously the raw server response was cached unfiltered, so a stale response could resurrect a deleted bookmark on the next cache hit within the 2-minute TTL. 2. [MEDIUM] Blob deletion wrapped in Promise.race with a 5-second timeout. Prevents a degraded Blob service from keeping the fire- and-forget promise dangling indefinitely. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Two fixes from Codex review round 5: 1. [HIGH] Await Blob deletion with 5s timeout instead of fire-and- forget. Ensures the process cannot exit before Blob cleanup completes, eliminating the case where a terminated runtime leaves the file permanently orphaned without any completion signal. 2. [MEDIUM] Save pre-delete snapshot (bookmarks + pagination) before optimistic removal. On rollback, await fetchBookmarks(); if the refetch also fails, restore the snapshot so the UI doesn't stay in a false-deleted state when the server didn't actually delete. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The Promise.race types are compatible so the directive caused a TypeScript error during build. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
… folder reorder transaction - Replace session!.user!.id non-null assertions in 8 API routes with requireApiSession() helper, returning 401 instead of 500 on invalid sessions - Wrap folder reorder batch UPDATE in db.transaction() to prevent partial failures - Add composite index (chatId, createdAt) on message table for query performance Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
本 PR 为书签添加了安全可靠的删除功能,并修复了相关 UI 问题。
功能新增
UI 修复
onClick守卫(dropdown/dialog 打开时preventDefault)解决line-clamp-1(底层-webkit-box)破坏了 flex 布局,min-w-0失效。改用truncate在 flex 容器中正确截断DialogDescription中的书签标题(完整 URL)撑爆对话框,按钮被挤出屏幕。添加line-clamp-2 break-all约束文本删除安全性修复(五轮 Codex 审查迭代)
存储清理:
并发安全:
pendingDeletes追踪机制,跟踪正在删除的书签 ID缓存一致性:
fetchBookmarks的 4 个写入点(缓存命中/网络结果的 append/replace)均过滤pendingDeletes修改文件 (
apps/web/)components/bookmark-card.tsx— 卡片删除菜单 + Link 守卫components/bookmark-grid.tsx— 列表删除菜单components/delete-bookmark-dialog.tsx— 删除确认对话框(新增)hooks/use-bookmark-delete.ts— 删除 hook(新增)stores/bookmark-store.ts— 删除乐观更新 + 并发安全app/(app)/bookmark/[id]/bookmark-detail-client.tsx— 详情页删除按钮 + 标题截断app/api/bookmarks/[id]/route.ts— DELETE endpoint + Blob 清理本 PR 由 Claude Code + DeepSeek V4 Pro 编写,Codex + GPT-5.5 审查