|
1 | | -import React, { useState, useEffect, useCallback, useRef } from "react"; |
2 | | -import { createPortal } from "react-dom"; |
| 1 | +import React, { useState, useEffect, useCallback } from "react"; |
3 | 2 | import { cn } from "@/common/lib/utils"; |
4 | 3 | import type { FrontendWorkspaceMetadata } from "@/common/types/workspace"; |
5 | 4 | import { usePersistedState } from "@/browser/hooks/usePersistedState"; |
@@ -28,6 +27,8 @@ import { RenameProvider } from "@/browser/contexts/WorkspaceRenameContext"; |
28 | 27 | import { useProjectContext } from "@/browser/contexts/ProjectContext"; |
29 | 28 | import { ChevronRight, KeyRound } from "lucide-react"; |
30 | 29 | import { useWorkspaceContext } from "@/browser/contexts/WorkspaceContext"; |
| 30 | +import { usePopoverError } from "@/browser/hooks/usePopoverError"; |
| 31 | +import { PopoverError } from "./PopoverError"; |
31 | 32 |
|
32 | 33 | // Re-export WorkspaceSelection for backwards compatibility |
33 | 34 | export type { WorkspaceSelection } from "./WorkspaceListItem"; |
@@ -240,12 +241,8 @@ const ProjectSidebarInner: React.FC<ProjectSidebarProps> = ({ |
240 | 241 | Record<string, boolean> |
241 | 242 | >("expandedOldWorkspaces", {}); |
242 | 243 | const [deletingWorkspaceIds, setDeletingWorkspaceIds] = useState<Set<string>>(new Set()); |
243 | | - const [removeError, setRemoveError] = useState<{ |
244 | | - workspaceId: string; |
245 | | - error: string; |
246 | | - position: { top: number; left: number }; |
247 | | - } | null>(null); |
248 | | - const removeErrorTimeoutRef = useRef<number | null>(null); |
| 244 | + const workspaceRemoveError = usePopoverError(); |
| 245 | + const projectRemoveError = usePopoverError(); |
249 | 246 | const [secretsModalState, setSecretsModalState] = useState<{ |
250 | 247 | isOpen: boolean; |
251 | 248 | projectPath: string; |
@@ -284,39 +281,6 @@ const ProjectSidebarInner: React.FC<ProjectSidebarProps> = ({ |
284 | 281 | })); |
285 | 282 | }; |
286 | 283 |
|
287 | | - const showRemoveError = useCallback( |
288 | | - (workspaceId: string, error: string, anchor?: { top: number; left: number }) => { |
289 | | - if (removeErrorTimeoutRef.current) { |
290 | | - window.clearTimeout(removeErrorTimeoutRef.current); |
291 | | - } |
292 | | - |
293 | | - const position = anchor ?? { |
294 | | - top: window.scrollY + 32, |
295 | | - left: Math.max(window.innerWidth - 420, 16), |
296 | | - }; |
297 | | - |
298 | | - setRemoveError({ |
299 | | - workspaceId, |
300 | | - error, |
301 | | - position, |
302 | | - }); |
303 | | - |
304 | | - removeErrorTimeoutRef.current = window.setTimeout(() => { |
305 | | - setRemoveError(null); |
306 | | - removeErrorTimeoutRef.current = null; |
307 | | - }, 5000); |
308 | | - }, |
309 | | - [] |
310 | | - ); |
311 | | - |
312 | | - useEffect(() => { |
313 | | - return () => { |
314 | | - if (removeErrorTimeoutRef.current) { |
315 | | - window.clearTimeout(removeErrorTimeoutRef.current); |
316 | | - } |
317 | | - }; |
318 | | - }, []); |
319 | | - |
320 | 284 | const handleRemoveWorkspace = useCallback( |
321 | 285 | async (workspaceId: string, buttonElement: HTMLElement) => { |
322 | 286 | // Mark workspace as being deleted for UI feedback |
@@ -378,7 +342,7 @@ const ProjectSidebarInner: React.FC<ProjectSidebarProps> = ({ |
378 | 342 | const errorMessage = result.error ?? "Failed to remove workspace"; |
379 | 343 | console.error("Force delete failed:", result.error); |
380 | 344 |
|
381 | | - showRemoveError(workspaceId, errorMessage, modalState?.anchor ?? undefined); |
| 345 | + workspaceRemoveError.showError(workspaceId, errorMessage, modalState?.anchor ?? undefined); |
382 | 346 | } |
383 | 347 | } finally { |
384 | 348 | // Clear deleting state |
@@ -582,9 +546,20 @@ const ProjectSidebarInner: React.FC<ProjectSidebarProps> = ({ |
582 | 546 | <button |
583 | 547 | onClick={(event) => { |
584 | 548 | event.stopPropagation(); |
585 | | - void onRemoveProject(projectPath); |
| 549 | + const buttonElement = event.currentTarget; |
| 550 | + void (async () => { |
| 551 | + const result = await onRemoveProject(projectPath); |
| 552 | + if (!result.success) { |
| 553 | + const error = result.error ?? "Failed to remove project"; |
| 554 | + const rect = buttonElement.getBoundingClientRect(); |
| 555 | + const anchor = { |
| 556 | + top: rect.top + window.scrollY, |
| 557 | + left: rect.right + 10, |
| 558 | + }; |
| 559 | + projectRemoveError.showError(projectPath, error, anchor); |
| 560 | + } |
| 561 | + })(); |
586 | 562 | }} |
587 | | - title="Remove project" |
588 | 563 | aria-label={`Remove project ${projectName}`} |
589 | 564 | data-project-path={projectPath} |
590 | 565 | className="text-muted-dark hover:text-danger-light hover:bg-danger-light/10 mr-1 flex h-5 w-5 shrink-0 cursor-pointer items-center justify-center rounded-[3px] border-none bg-transparent text-base opacity-0 transition-all duration-200" |
@@ -754,19 +729,16 @@ const ProjectSidebarInner: React.FC<ProjectSidebarProps> = ({ |
754 | 729 | onForceDelete={handleForceDelete} |
755 | 730 | /> |
756 | 731 | )} |
757 | | - {removeError && |
758 | | - createPortal( |
759 | | - <div |
760 | | - className="bg-error-bg border-error text-error font-monospace pointer-events-auto fixed z-[10000] max-w-96 rounded-md border p-3 px-4 text-xs leading-[1.4] break-words whitespace-pre-wrap shadow-[0_4px_16px_rgba(0,0,0,0.5)]" |
761 | | - style={{ |
762 | | - top: `${removeError.position.top}px`, |
763 | | - left: `${removeError.position.left}px`, |
764 | | - }} |
765 | | - > |
766 | | - Failed to remove workspace: {removeError.error} |
767 | | - </div>, |
768 | | - document.body |
769 | | - )} |
| 732 | + <PopoverError |
| 733 | + error={workspaceRemoveError.error} |
| 734 | + prefix="Failed to remove workspace" |
| 735 | + onDismiss={workspaceRemoveError.clearError} |
| 736 | + /> |
| 737 | + <PopoverError |
| 738 | + error={projectRemoveError.error} |
| 739 | + prefix="Failed to remove project" |
| 740 | + onDismiss={projectRemoveError.clearError} |
| 741 | + /> |
770 | 742 | </div> |
771 | 743 | </DndProvider> |
772 | 744 | </RenameProvider> |
|
0 commit comments