mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-02 08:33:36 +00:00
feat: separate project settings from global settings
This PR introduces a new dedicated Project Settings screen accessible from the sidebar, clearly separating project-specific settings from global application settings. - Added new route `/project-settings` with dedicated view - Sidebar navigation item "Settings" in Tools section (Shift+S shortcut) - Sidebar-based navigation matching global Settings pattern - Sections: Identity, Worktrees, Theme, Danger Zone **Moved to Project Settings:** - Project name and icon customization - Project-specific theme override - Worktree isolation enable/disable (per-project override) - Init script indicator visibility and auto-dismiss - Delete branch by default preference - Initialization script editor - Delete project (Danger Zone) **Remains in Global Settings:** - Global theme (default for all projects) - Global worktree isolation (default for new projects) - Feature Defaults, Model Defaults - API Keys, AI Providers, MCP Servers - Terminal, Keyboard Shortcuts, Audio - Account, Security, Developer settings Both Theme and Worktree Isolation now follow a consistent override pattern: 1. Global Settings defines the default value 2. New projects inherit the global value 3. Project Settings can override for that specific project 4. Changing global setting doesn't affect projects with overrides - Fixed: Changing global theme was incorrectly overwriting project themes - Fixed: Project worktree setting not persisting across sessions - Project settings now properly load from server on component mount - Shell syntax editor: improved background contrast (bg-background) - Shell syntax editor: removed distracting active line highlight - Project Settings header matches Context/Memory views pattern - `apps/ui/src/routes/project-settings.tsx` - `apps/ui/src/components/views/project-settings-view/` (9 files) - Global settings simplified (removed project-specific options) - Sidebar navigation updated with project settings link - App store: added project-specific useWorktrees state/actions - Types: added projectSettings keyboard shortcut - HTTP client: added missing project settings response fields
This commit is contained in:
@@ -1,118 +1,20 @@
|
||||
import { useState, useRef, useEffect } from 'react';
|
||||
import { useState } from 'react';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Palette, Moon, Sun, Upload, X, ImageIcon } from 'lucide-react';
|
||||
import { Palette, Moon, Sun } from 'lucide-react';
|
||||
import { darkThemes, lightThemes } from '@/config/theme-options';
|
||||
import { cn } from '@/lib/utils';
|
||||
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 type { Theme, Project } from '../shared/types';
|
||||
import type { Theme } from '../shared/types';
|
||||
|
||||
interface AppearanceSectionProps {
|
||||
effectiveTheme: Theme;
|
||||
currentProject: Project | null;
|
||||
onThemeChange: (theme: Theme) => void;
|
||||
}
|
||||
|
||||
export function AppearanceSection({
|
||||
effectiveTheme,
|
||||
currentProject,
|
||||
onThemeChange,
|
||||
}: AppearanceSectionProps) {
|
||||
const { setProjectIcon, setProjectName, setProjectCustomIcon } = useAppStore();
|
||||
export function AppearanceSection({ effectiveTheme, onThemeChange }: AppearanceSectionProps) {
|
||||
const [activeTab, setActiveTab] = useState<'dark' | 'light'>('dark');
|
||||
const [projectName, setProjectNameLocal] = useState(currentProject?.name || '');
|
||||
const [projectIcon, setProjectIconLocal] = useState<string | null>(currentProject?.icon || null);
|
||||
const [customIconPath, setCustomIconPathLocal] = useState<string | null>(
|
||||
currentProject?.customIconPath || null
|
||||
);
|
||||
const [isUploadingIcon, setIsUploadingIcon] = useState(false);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
// Sync local state when currentProject changes
|
||||
useEffect(() => {
|
||||
setProjectNameLocal(currentProject?.name || '');
|
||||
setProjectIconLocal(currentProject?.icon || null);
|
||||
setCustomIconPathLocal(currentProject?.customIconPath || null);
|
||||
}, [currentProject]);
|
||||
|
||||
const themesToShow = activeTab === 'dark' ? darkThemes : lightThemes;
|
||||
|
||||
// Auto-save when values change
|
||||
const handleNameChange = (name: string) => {
|
||||
setProjectNameLocal(name);
|
||||
if (currentProject && name.trim() && name.trim() !== currentProject.name) {
|
||||
setProjectName(currentProject.id, name.trim());
|
||||
}
|
||||
};
|
||||
|
||||
const handleIconChange = (icon: string | null) => {
|
||||
setProjectIconLocal(icon);
|
||||
if (currentProject) {
|
||||
setProjectIcon(currentProject.id, icon);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCustomIconChange = (path: string | null) => {
|
||||
setCustomIconPathLocal(path);
|
||||
if (currentProject) {
|
||||
setProjectCustomIcon(currentProject.id, path);
|
||||
// Clear Lucide icon when custom icon is set
|
||||
if (path) {
|
||||
setProjectIconLocal(null);
|
||||
setProjectIcon(currentProject.id, null);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleCustomIconUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (!file || !currentProject) return;
|
||||
|
||||
// Validate file type
|
||||
const validTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
|
||||
if (!validTypes.includes(file.type)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate file size (max 2MB for icons)
|
||||
if (file.size > 2 * 1024 * 1024) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsUploadingIcon(true);
|
||||
try {
|
||||
// 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,
|
||||
currentProject.path
|
||||
);
|
||||
if (result.success && result.path) {
|
||||
handleCustomIconChange(result.path);
|
||||
}
|
||||
setIsUploadingIcon(false);
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
} catch {
|
||||
setIsUploadingIcon(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemoveCustomIcon = () => {
|
||||
handleCustomIconChange(null);
|
||||
if (fileInputRef.current) {
|
||||
fileInputRef.current.value = '';
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
@@ -134,94 +36,10 @@ export function AppearanceSection({
|
||||
</p>
|
||||
</div>
|
||||
<div className="p-6 space-y-6">
|
||||
{/* Project Details Section */}
|
||||
{currentProject && (
|
||||
<div className="space-y-4 pb-6 border-b border-border/50">
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="project-name-settings">Project Name</Label>
|
||||
<Input
|
||||
id="project-name-settings"
|
||||
value={projectName}
|
||||
onChange={(e) => handleNameChange(e.target.value)}
|
||||
placeholder="Enter project name"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>Project Icon</Label>
|
||||
<p className="text-xs text-muted-foreground mb-2">
|
||||
Choose a preset icon or upload a custom image
|
||||
</p>
|
||||
|
||||
{/* Custom Icon Upload */}
|
||||
<div className="mb-4">
|
||||
<div className="flex items-center gap-3">
|
||||
{customIconPath ? (
|
||||
<div className="relative">
|
||||
<img
|
||||
src={getAuthenticatedImageUrl(customIconPath, currentProject.path)}
|
||||
alt="Custom project icon"
|
||||
className="w-12 h-12 rounded-lg object-cover border border-border"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleRemoveCustomIcon}
|
||||
className="absolute -top-1.5 -right-1.5 w-5 h-5 rounded-full bg-destructive text-destructive-foreground flex items-center justify-center hover:bg-destructive/90"
|
||||
>
|
||||
<X className="w-3 h-3" />
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="w-12 h-12 rounded-lg border border-dashed border-border flex items-center justify-center bg-accent/30">
|
||||
<ImageIcon className="w-5 h-5 text-muted-foreground" />
|
||||
</div>
|
||||
)}
|
||||
<div className="flex-1">
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
accept="image/jpeg,image/png,image/gif,image/webp"
|
||||
onChange={handleCustomIconUpload}
|
||||
className="hidden"
|
||||
id="custom-icon-upload"
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
disabled={isUploadingIcon}
|
||||
className="gap-1.5"
|
||||
>
|
||||
<Upload className="w-3.5 h-3.5" />
|
||||
{isUploadingIcon ? 'Uploading...' : 'Upload Custom Icon'}
|
||||
</Button>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
PNG, JPG, GIF or WebP. Max 2MB.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Preset Icon Picker - only show if no custom icon */}
|
||||
{!customIconPath && (
|
||||
<IconPicker selectedIcon={projectIcon} onSelectIcon={handleIconChange} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Theme Section */}
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label className="text-foreground font-medium">
|
||||
Theme{' '}
|
||||
<span className="text-muted-foreground font-normal">
|
||||
{currentProject ? `(for ${currentProject.name})` : '(Global)'}
|
||||
</span>
|
||||
</Label>
|
||||
<Label className="text-foreground font-medium">Theme</Label>
|
||||
{/* Dark/Light Tabs */}
|
||||
<div className="flex gap-1 p-1 rounded-lg bg-accent/30">
|
||||
<button
|
||||
|
||||
Reference in New Issue
Block a user