diff --git a/apps/ui/src/components/views/project-settings-view/project-identity-section.tsx b/apps/ui/src/components/views/project-settings-view/project-identity-section.tsx index d938ee73..669b7879 100644 --- a/apps/ui/src/components/views/project-settings-view/project-identity-section.tsx +++ b/apps/ui/src/components/views/project-settings-view/project-identity-section.tsx @@ -8,6 +8,7 @@ import { useAppStore } from '@/store/app-store'; import { IconPicker } from '@/components/layout/project-switcher/components/icon-picker'; import { getAuthenticatedImageUrl } from '@/lib/api-fetch'; import { getHttpApiClient } from '@/lib/http-api-client'; +import { toast } from 'sonner'; import type { Project } from '@/lib/electron'; interface ProjectIdentitySectionProps { @@ -61,11 +62,17 @@ export function ProjectIdentitySection({ project }: ProjectIdentitySectionProps) // Validate file type const validTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp']; if (!validTypes.includes(file.type)) { + toast.error('Invalid file type', { + description: 'Please upload a PNG, JPG, GIF, or WebP image.', + }); return; } // Validate file size (max 2MB for icons) if (file.size > 2 * 1024 * 1024) { + toast.error('File too large', { + description: 'Please upload an image smaller than 2MB.', + }); return; } @@ -74,20 +81,39 @@ export function ProjectIdentitySection({ project }: ProjectIdentitySectionProps) // Convert to base64 const reader = new FileReader(); reader.onload = async () => { - const base64Data = reader.result as string; - const result = await getHttpApiClient().saveImageToTemp( - base64Data, - `project-icon-${file.name}`, - file.type, - project.path - ); - if (result.success && result.path) { - handleCustomIconChange(result.path); + try { + const base64Data = reader.result as string; + const result = await getHttpApiClient().saveImageToTemp( + base64Data, + `project-icon-${file.name}`, + file.type, + project.path + ); + if (result.success && result.path) { + handleCustomIconChange(result.path); + toast.success('Icon uploaded successfully'); + } else { + toast.error('Failed to upload icon', { + description: result.error || 'Please try again.', + }); + } + } catch (error) { + toast.error('Failed to upload icon', { + description: 'Network error. Please try again.', + }); + } finally { + setIsUploadingIcon(false); } + }; + reader.onerror = () => { + toast.error('Failed to read file', { + description: 'Please try again with a different file.', + }); setIsUploadingIcon(false); }; reader.readAsDataURL(file); } catch { + toast.error('Failed to upload icon'); setIsUploadingIcon(false); } }; diff --git a/apps/ui/src/components/views/project-settings-view/worktree-preferences-section.tsx b/apps/ui/src/components/views/project-settings-view/worktree-preferences-section.tsx index af85eb03..c289d382 100644 --- a/apps/ui/src/components/views/project-settings-view/worktree-preferences-section.tsx +++ b/apps/ui/src/components/views/project-settings-view/worktree-preferences-section.tsx @@ -64,35 +64,48 @@ export function WorktreePreferencesSection({ project }: WorktreePreferencesSecti // Load project settings (including useWorktrees) when project changes useEffect(() => { + let isCancelled = false; + const currentPath = project.path; + const loadProjectSettings = async () => { try { const httpClient = getHttpApiClient(); - const response = await httpClient.settings.getProject(project.path); + const response = await httpClient.settings.getProject(currentPath); + + // Avoid updating state if component unmounted or project changed + if (isCancelled) return; + if (response.success && response.settings) { // Sync useWorktrees to store if it has a value if (response.settings.useWorktrees !== undefined) { - setProjectUseWorktrees(project.path, response.settings.useWorktrees); + setProjectUseWorktrees(currentPath, response.settings.useWorktrees); } // Also sync other settings to store if (response.settings.showInitScriptIndicator !== undefined) { - setShowInitScriptIndicator(project.path, response.settings.showInitScriptIndicator); + setShowInitScriptIndicator(currentPath, response.settings.showInitScriptIndicator); } if (response.settings.defaultDeleteBranchWithWorktree !== undefined) { - setDefaultDeleteBranch(project.path, response.settings.defaultDeleteBranchWithWorktree); + setDefaultDeleteBranch(currentPath, response.settings.defaultDeleteBranchWithWorktree); } if (response.settings.autoDismissInitScriptIndicator !== undefined) { setAutoDismissInitScriptIndicator( - project.path, + currentPath, response.settings.autoDismissInitScriptIndicator ); } } } catch (error) { - console.error('Failed to load project settings:', error); + if (!isCancelled) { + console.error('Failed to load project settings:', error); + } } }; loadProjectSettings(); + + return () => { + isCancelled = true; + }; }, [ project.path, setProjectUseWorktrees, @@ -103,12 +116,19 @@ export function WorktreePreferencesSection({ project }: WorktreePreferencesSecti // Load init script content when project changes useEffect(() => { + let isCancelled = false; + const currentPath = project.path; + const loadInitScript = async () => { setIsLoading(true); try { const response = await apiGet( - `/api/worktree/init-script?projectPath=${encodeURIComponent(project.path)}` + `/api/worktree/init-script?projectPath=${encodeURIComponent(currentPath)}` ); + + // Avoid updating state if component unmounted or project changed + if (isCancelled) return; + if (response.success) { const content = response.content || ''; setScriptContent(content); @@ -116,13 +136,21 @@ export function WorktreePreferencesSection({ project }: WorktreePreferencesSecti setScriptExists(response.exists); } } catch (error) { - console.error('Failed to load init script:', error); + if (!isCancelled) { + console.error('Failed to load init script:', error); + } } finally { - setIsLoading(false); + if (!isCancelled) { + setIsLoading(false); + } } }; loadInitScript(); + + return () => { + isCancelled = true; + }; }, [project.path]); // Save script