From a60904bd5185d6c2047705b98c76e9f5ab76d29d Mon Sep 17 00:00:00 2001 From: Shirone Date: Tue, 27 Jan 2026 00:09:55 +0100 Subject: [PATCH] fix(ui,server): Fix project icon updates and image upload issues - Fix setProjectCustomIcon using wrong property name (customIcon -> customIconPath) - Add currentProject state update to setProjectIcon and setProjectCustomIcon - Fix data URL regex to handle all formats (e.g., charset=utf-8 in GIFs) - Increase project icon size limit from 2MB to 5MB for animated GIFs - Add toast notifications for upload validation errors - Add image error fallback to folder icon in project switcher - Make HttpApiClient get/put methods public for store access - Fix TypeScript errors in app-store.ts (trashedAt type, font properties) Co-Authored-By: Claude Opus 4.5 --- .../routes/fs/routes/save-board-background.ts | 4 +- .../server/src/routes/fs/routes/save-image.ts | 4 +- .../components/edit-project-dialog.tsx | 23 ++- .../components/project-context-menu.tsx | 32 +++- .../components/project-switcher-item.tsx | 6 +- .../project-selector-with-options.tsx | 17 +- .../sidebar/components/theme-menu-item.tsx | 18 +-- .../project-identity-section.tsx | 8 +- apps/ui/src/lib/http-api-client.ts | 4 +- apps/ui/src/store/app-store.ts | 153 +++++++----------- 10 files changed, 129 insertions(+), 140 deletions(-) diff --git a/apps/server/src/routes/fs/routes/save-board-background.ts b/apps/server/src/routes/fs/routes/save-board-background.ts index e8988c6c..a0c2164a 100644 --- a/apps/server/src/routes/fs/routes/save-board-background.ts +++ b/apps/server/src/routes/fs/routes/save-board-background.ts @@ -31,7 +31,9 @@ export function createSaveBoardBackgroundHandler() { await secureFs.mkdir(boardDir, { recursive: true }); // Decode base64 data (remove data URL prefix if present) - const base64Data = data.replace(/^data:image\/\w+;base64,/, ''); + // Use a regex that handles all data URL formats including those with extra params + // e.g., data:image/gif;charset=utf-8;base64,R0lGOD... + const base64Data = data.replace(/^data:[^,]+,/, ''); const buffer = Buffer.from(base64Data, 'base64'); // Use a fixed filename for the board background (overwrite previous) diff --git a/apps/server/src/routes/fs/routes/save-image.ts b/apps/server/src/routes/fs/routes/save-image.ts index 059abfaf..c8cfdda7 100644 --- a/apps/server/src/routes/fs/routes/save-image.ts +++ b/apps/server/src/routes/fs/routes/save-image.ts @@ -31,7 +31,9 @@ export function createSaveImageHandler() { await secureFs.mkdir(imagesDir, { recursive: true }); // Decode base64 data (remove data URL prefix if present) - const base64Data = data.replace(/^data:image\/\w+;base64,/, ''); + // Use a regex that handles all data URL formats including those with extra params + // e.g., data:image/gif;charset=utf-8;base64,R0lGOD... + const base64Data = data.replace(/^data:[^,]+,/, ''); const buffer = Buffer.from(base64Data, 'base64'); // Generate unique filename with timestamp diff --git a/apps/ui/src/components/layout/project-switcher/components/edit-project-dialog.tsx b/apps/ui/src/components/layout/project-switcher/components/edit-project-dialog.tsx index 0cb598b2..70ada5ee 100644 --- a/apps/ui/src/components/layout/project-switcher/components/edit-project-dialog.tsx +++ b/apps/ui/src/components/layout/project-switcher/components/edit-project-dialog.tsx @@ -15,6 +15,7 @@ import { getAuthenticatedImageUrl } from '@/lib/api-fetch'; import { getHttpApiClient } from '@/lib/http-api-client'; import type { Project } from '@/lib/electron'; import { IconPicker } from './icon-picker'; +import { toast } from 'sonner'; interface EditProjectDialogProps { project: Project; @@ -52,11 +53,18 @@ export function EditProjectDialog({ project, open, onOpenChange }: EditProjectDi // Validate file type const validTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp']; if (!validTypes.includes(file.type)) { + toast.error( + `Invalid file type: ${file.type || 'unknown'}. Please use JPG, PNG, GIF or WebP.` + ); return; } - // Validate file size (max 2MB for icons) - if (file.size > 2 * 1024 * 1024) { + // Validate file size (max 5MB for icons - allows animated GIFs) + const maxSize = 5 * 1024 * 1024; + if (file.size > maxSize) { + toast.error( + `File too large (${(file.size / 1024 / 1024).toFixed(2)} MB). Maximum size is 5 MB.` + ); return; } @@ -72,15 +80,24 @@ export function EditProjectDialog({ project, open, onOpenChange }: EditProjectDi file.type, project.path ); + if (result.success && result.path) { setCustomIconPath(result.path); // Clear the Lucide icon when custom icon is set setIcon(null); + toast.success('Icon uploaded successfully'); + } else { + toast.error('Failed to upload icon'); } setIsUploadingIcon(false); }; + reader.onerror = () => { + toast.error('Failed to read file'); + setIsUploadingIcon(false); + }; reader.readAsDataURL(file); } catch { + toast.error('Failed to upload icon'); setIsUploadingIcon(false); } }; @@ -162,7 +179,7 @@ export function EditProjectDialog({ project, open, onOpenChange }: EditProjectDi {isUploadingIcon ? 'Uploading...' : 'Upload Custom Icon'}

- PNG, JPG, GIF or WebP. Max 2MB. + PNG, JPG, GIF or WebP. Max 5MB.

diff --git a/apps/ui/src/components/layout/project-switcher/components/project-context-menu.tsx b/apps/ui/src/components/layout/project-switcher/components/project-context-menu.tsx index 249aa6a1..e8cf2f3f 100644 --- a/apps/ui/src/components/layout/project-switcher/components/project-context-menu.tsx +++ b/apps/ui/src/components/layout/project-switcher/components/project-context-menu.tsx @@ -59,7 +59,7 @@ interface ThemeButtonProps { /** Handler for pointer leave events (used to clear preview) */ onPointerLeave: (e: React.PointerEvent) => void; /** Handler for click events (used to select theme) */ - onClick: () => void; + onClick: (e: React.MouseEvent) => void; } /** @@ -77,6 +77,7 @@ const ThemeButton = memo(function ThemeButton({ const Icon = option.icon; return (

- PNG, JPG, GIF or WebP. Max 2MB. + PNG, JPG, GIF or WebP. Max 5MB.

diff --git a/apps/ui/src/lib/http-api-client.ts b/apps/ui/src/lib/http-api-client.ts index f468f07c..1f79ff07 100644 --- a/apps/ui/src/lib/http-api-client.ts +++ b/apps/ui/src/lib/http-api-client.ts @@ -946,7 +946,7 @@ export class HttpApiClient implements ElectronAPI { return response.json(); } - private async get(endpoint: string): Promise { + async get(endpoint: string): Promise { // Ensure API key is initialized before making request await waitForApiKeyInit(); const response = await fetch(`${this.serverUrl}${endpoint}`, { @@ -976,7 +976,7 @@ export class HttpApiClient implements ElectronAPI { return response.json(); } - private async put(endpoint: string, body?: unknown): Promise { + async put(endpoint: string, body?: unknown): Promise { // Ensure API key is initialized before making request await waitForApiKeyInit(); const response = await fetch(`${this.serverUrl}${endpoint}`, { diff --git a/apps/ui/src/store/app-store.ts b/apps/ui/src/store/app-store.ts index 52744339..aa4ff227 100644 --- a/apps/ui/src/store/app-store.ts +++ b/apps/ui/src/store/app-store.ts @@ -1,7 +1,7 @@ import { create } from 'zustand'; // Note: persist middleware removed - settings now sync via API (use-settings-sync.ts) import type { Project, TrashedProject } from '@/lib/electron'; -import { getElectronAPI } from '@/lib/electron'; +import { saveProjects, saveTrashedProjects } from '@/lib/electron'; import { getHttpApiClient } from '@/lib/http-api-client'; import { createLogger } from '@automaker/utils/logger'; // Note: setItem/getItem moved to ./utils/theme-utils.ts @@ -360,7 +360,7 @@ export const useAppStore = create()((set, get) => ({ const trashedProject: TrashedProject = { ...project, - trashedAt: Date.now(), + trashedAt: new Date().toISOString(), }; set((state) => ({ @@ -369,12 +369,9 @@ export const useAppStore = create()((set, get) => ({ currentProject: state.currentProject?.id === projectId ? null : state.currentProject, })); - // Persist to Electron store if available - const electronAPI = getElectronAPI(); - if (electronAPI) { - electronAPI.projects.setProjects(get().projects); - electronAPI.projects.setTrashedProjects(get().trashedProjects); - } + // Persist to storage + saveProjects(get().projects); + saveTrashedProjects(get().trashedProjects); }, restoreTrashedProject: (projectId: string) => { @@ -390,12 +387,9 @@ export const useAppStore = create()((set, get) => ({ trashedProjects: state.trashedProjects.filter((p) => p.id !== projectId), })); - // Persist to Electron store if available - const electronAPI = getElectronAPI(); - if (electronAPI) { - electronAPI.projects.setProjects(get().projects); - electronAPI.projects.setTrashedProjects(get().trashedProjects); - } + // Persist to storage + saveProjects(get().projects); + saveTrashedProjects(get().trashedProjects); }, deleteTrashedProject: (projectId: string) => { @@ -403,21 +397,15 @@ export const useAppStore = create()((set, get) => ({ trashedProjects: state.trashedProjects.filter((p) => p.id !== projectId), })); - // Persist to Electron store if available - const electronAPI = getElectronAPI(); - if (electronAPI) { - electronAPI.projects.setTrashedProjects(get().trashedProjects); - } + // Persist to storage + saveTrashedProjects(get().trashedProjects); }, emptyTrash: () => { set({ trashedProjects: [] }); - // Persist to Electron store if available - const electronAPI = getElectronAPI(); - if (electronAPI) { - electronAPI.projects.setTrashedProjects([]); - } + // Persist to storage + saveTrashedProjects([]); }, setCurrentProject: (project) => { @@ -474,14 +462,10 @@ export const useAppStore = create()((set, get) => ({ get().addProject(newProject); get().setCurrentProject(newProject); - // Persist to Electron store if available - const electronAPI = getElectronAPI(); - if (electronAPI) { - // Small delay to ensure state is updated before persisting - setTimeout(() => { - electronAPI.projects.setProjects(get().projects); - }, 0); - } + // Persist to storage (small delay to ensure state is updated) + setTimeout(() => { + saveProjects(get().projects); + }, 0); return newProject; }, @@ -564,11 +548,8 @@ export const useAppStore = create()((set, get) => ({ ), })); - // Persist to Electron store if available - const electronAPI = getElectronAPI(); - if (electronAPI) { - electronAPI.projects.setProjects(get().projects); - } + // Persist to storage + saveProjects(get().projects); }, setProjectIcon: (projectId: string, icon: string | null) => { @@ -576,27 +557,31 @@ export const useAppStore = create()((set, get) => ({ projects: state.projects.map((p) => p.id === projectId ? { ...p, icon: icon ?? undefined } : p ), + // Also update currentProject if it's the one being modified + currentProject: + state.currentProject?.id === projectId + ? { ...state.currentProject, icon: icon ?? undefined } + : state.currentProject, })); - // Persist to Electron store if available - const electronAPI = getElectronAPI(); - if (electronAPI) { - electronAPI.projects.setProjects(get().projects); - } + // Persist to storage + saveProjects(get().projects); }, setProjectCustomIcon: (projectId: string, customIconPath: string | null) => { set((state) => ({ projects: state.projects.map((p) => - p.id === projectId ? { ...p, customIcon: customIconPath ?? undefined } : p + p.id === projectId ? { ...p, customIconPath: customIconPath ?? undefined } : p ), + // Also update currentProject if it's the one being modified + currentProject: + state.currentProject?.id === projectId + ? { ...state.currentProject, customIconPath: customIconPath ?? undefined } + : state.currentProject, })); - // Persist to Electron store if available - const electronAPI = getElectronAPI(); - if (electronAPI) { - electronAPI.projects.setProjects(get().projects); - } + // Persist to storage + saveProjects(get().projects); }, setProjectName: (projectId: string, name: string) => { @@ -609,11 +594,8 @@ export const useAppStore = create()((set, get) => ({ : state.currentProject, })); - // Persist to Electron store if available - const electronAPI = getElectronAPI(); - if (electronAPI) { - electronAPI.projects.setProjects(get().projects); - } + // Persist to storage + saveProjects(get().projects); }, // View actions @@ -659,11 +641,8 @@ export const useAppStore = create()((set, get) => ({ ); } - // Persist to Electron store if available - const electronAPI = getElectronAPI(); - if (electronAPI) { - electronAPI.projects.setProjects(get().projects); - } + // Persist to storage + saveProjects(get().projects); }, getEffectiveTheme: () => { const state = get(); @@ -696,11 +675,8 @@ export const useAppStore = create()((set, get) => ({ : state.currentProject, })); - // Persist to Electron store if available - const electronAPI = getElectronAPI(); - if (electronAPI) { - electronAPI.projects.setProjects(get().projects); - } + // Persist to storage + saveProjects(get().projects); }, setProjectFontMono: (projectId: string, fontFamily: string | null) => { set((state) => ({ @@ -714,20 +690,17 @@ export const useAppStore = create()((set, get) => ({ : state.currentProject, })); - // Persist to Electron store if available - const electronAPI = getElectronAPI(); - if (electronAPI) { - electronAPI.projects.setProjects(get().projects); - } + // Persist to storage + saveProjects(get().projects); }, getEffectiveFontSans: () => { const state = get(); - const projectFont = state.currentProject?.fontSans; + const projectFont = state.currentProject?.fontFamilySans; return getEffectiveFont(projectFont, state.fontFamilySans, UI_SANS_FONT_OPTIONS); }, getEffectiveFontMono: () => { const state = get(); - const projectFont = state.currentProject?.fontMono; + const projectFont = state.currentProject?.fontFamilyMono; return getEffectiveFont(projectFont, state.fontFamilyMono, UI_MONO_FONT_OPTIONS); }, @@ -744,11 +717,8 @@ export const useAppStore = create()((set, get) => ({ : state.currentProject, })); - // Persist to Electron store if available - const electronAPI = getElectronAPI(); - if (electronAPI) { - electronAPI.projects.setProjects(get().projects); - } + // Persist to storage + saveProjects(get().projects); }, // Project Phase Model Overrides @@ -781,11 +751,8 @@ export const useAppStore = create()((set, get) => ({ }; }); - // Persist to Electron store if available - const electronAPI = getElectronAPI(); - if (electronAPI) { - electronAPI.projects.setProjects(get().projects); - } + // Persist to storage + saveProjects(get().projects); }, clearAllProjectPhaseModelOverrides: (projectId: string) => { @@ -804,11 +771,8 @@ export const useAppStore = create()((set, get) => ({ }; }); - // Persist to Electron store if available - const electronAPI = getElectronAPI(); - if (electronAPI) { - electronAPI.projects.setProjects(get().projects); - } + // Persist to storage + saveProjects(get().projects); }, // Project Default Feature Model Override @@ -830,11 +794,8 @@ export const useAppStore = create()((set, get) => ({ }; }); - // Persist to Electron store if available - const electronAPI = getElectronAPI(); - if (electronAPI) { - electronAPI.projects.setProjects(get().projects); - } + // Persist to storage + saveProjects(get().projects); }, // Feature actions @@ -845,7 +806,7 @@ export const useAppStore = create()((set, get) => ({ })), addFeature: (feature) => { const id = feature.id ?? `feature-${Date.now()}-${Math.random().toString(36).slice(2)}`; - const newFeature: Feature = { ...feature, id }; + const newFeature = { ...feature, id } as Feature; set((state) => ({ features: [...state.features, newFeature] })); return newFeature; }, @@ -2471,8 +2432,7 @@ export const useAppStore = create()((set, get) => ({ try { const httpApi = getHttpApiClient(); - const response = await httpApi.get('/api/codex/models'); - const data = response.data as { + const data = await httpApi.get<{ success: boolean; models?: Array<{ id: string; @@ -2484,7 +2444,7 @@ export const useAppStore = create()((set, get) => ({ isDefault: boolean; }>; error?: string; - }; + }>('/api/codex/models'); if (data.success && data.models) { set({ @@ -2542,8 +2502,7 @@ export const useAppStore = create()((set, get) => ({ try { const httpApi = getHttpApiClient(); - const response = await httpApi.get('/api/opencode/models'); - const data = response.data as { + const data = await httpApi.get<{ success: boolean; models?: ModelDefinition[]; providers?: Array<{ @@ -2553,7 +2512,7 @@ export const useAppStore = create()((set, get) => ({ authMethod?: string; }>; error?: string; - }; + }>('/api/opencode/models'); if (data.success && data.models) { // Filter out Bedrock models