mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-02 08:33:36 +00:00
♻️ refactor: streamline sidebar component structure and enhance functionality
- Extracted new components: ProjectSelectorWithOptions, SidebarFooter, TrashDialog, and OnboardingDialog to improve code organization and reusability. - Introduced new hooks: useProjectCreation, useSetupDialog, and useTrashDialog for better state management and modularity. - Updated sidebar.tsx to utilize the new components and hooks, reducing complexity and improving maintainability. - Enhanced project creation and setup processes with dedicated dialogs and streamlined user interactions. This refactor aims to enhance the user experience and maintainability of the sidebar by modularizing functionality and improving the overall structure.
This commit is contained in:
@@ -6,3 +6,7 @@ export { useTrashOperations } from './use-trash-operations';
|
||||
export { useProjectPicker } from './use-project-picker';
|
||||
export { useSpecRegeneration } from './use-spec-regeneration';
|
||||
export { useNavigation } from './use-navigation';
|
||||
export { useProjectCreation } from './use-project-creation';
|
||||
export { useSetupDialog } from './use-setup-dialog';
|
||||
export { useTrashDialog } from './use-trash-dialog';
|
||||
export { useProjectTheme } from './use-project-theme';
|
||||
|
||||
@@ -0,0 +1,175 @@
|
||||
import { useState, useCallback } from 'react';
|
||||
import { getElectronAPI } from '@/lib/electron';
|
||||
import { initializeProject } from '@/lib/project-init';
|
||||
import { toast } from 'sonner';
|
||||
import type { StarterTemplate } from '@/lib/templates';
|
||||
import type { ThemeMode } from '@/store/app-store';
|
||||
import type { TrashedProject, Project } from '@/lib/electron';
|
||||
|
||||
interface UseProjectCreationProps {
|
||||
trashedProjects: TrashedProject[];
|
||||
currentProject: Project | null;
|
||||
globalTheme: ThemeMode;
|
||||
upsertAndSetCurrentProject: (path: string, name: string, theme: ThemeMode) => Project;
|
||||
}
|
||||
|
||||
export function useProjectCreation({
|
||||
trashedProjects,
|
||||
currentProject,
|
||||
globalTheme,
|
||||
upsertAndSetCurrentProject,
|
||||
}: UseProjectCreationProps) {
|
||||
// Modal state
|
||||
const [showNewProjectModal, setShowNewProjectModal] = useState(false);
|
||||
const [isCreatingProject, setIsCreatingProject] = useState(false);
|
||||
|
||||
// Onboarding state
|
||||
const [showOnboardingDialog, setShowOnboardingDialog] = useState(false);
|
||||
const [newProjectName, setNewProjectName] = useState('');
|
||||
const [newProjectPath, setNewProjectPath] = useState('');
|
||||
|
||||
/**
|
||||
* Common logic for all project creation flows
|
||||
*/
|
||||
const finalizeProjectCreation = useCallback(
|
||||
async (projectPath: string, projectName: string) => {
|
||||
try {
|
||||
// Initialize .automaker directory structure
|
||||
await initializeProject(projectPath);
|
||||
|
||||
// Write initial app_spec.txt with basic XML structure
|
||||
const api = getElectronAPI();
|
||||
await api.fs.writeFile(
|
||||
`${projectPath}/app_spec.txt`,
|
||||
`<?xml version="1.0" encoding="UTF-8"?>\n<project>\n <name>${projectName}</name>\n <description>Add your project description here</description>\n</project>`
|
||||
);
|
||||
|
||||
// Determine theme: try trashed project theme, then current project theme, then global
|
||||
const trashedProject = trashedProjects.find((p) => p.path === projectPath);
|
||||
const effectiveTheme =
|
||||
(trashedProject?.theme as ThemeMode | undefined) ||
|
||||
(currentProject?.theme as ThemeMode | undefined) ||
|
||||
globalTheme;
|
||||
|
||||
upsertAndSetCurrentProject(projectPath, projectName, effectiveTheme);
|
||||
|
||||
setShowNewProjectModal(false);
|
||||
|
||||
// Show onboarding dialog for new project
|
||||
setNewProjectName(projectName);
|
||||
setNewProjectPath(projectPath);
|
||||
setShowOnboardingDialog(true);
|
||||
|
||||
toast.success('Project created successfully');
|
||||
} catch (error) {
|
||||
console.error('[ProjectCreation] Failed to finalize project:', error);
|
||||
toast.error('Failed to initialize project', {
|
||||
description: error instanceof Error ? error.message : 'Unknown error',
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
[trashedProjects, currentProject, globalTheme, upsertAndSetCurrentProject]
|
||||
);
|
||||
|
||||
/**
|
||||
* Create a blank project with .automaker structure
|
||||
*/
|
||||
const handleCreateBlankProject = useCallback(
|
||||
async (projectName: string, parentDir: string) => {
|
||||
setIsCreatingProject(true);
|
||||
try {
|
||||
const api = getElectronAPI();
|
||||
const projectPath = `${parentDir}/${projectName}`;
|
||||
|
||||
// Create project directory
|
||||
await api.fs.createFolder(projectPath);
|
||||
|
||||
// Finalize project setup
|
||||
await finalizeProjectCreation(projectPath, projectName);
|
||||
} catch (error) {
|
||||
console.error('[ProjectCreation] Failed to create blank project:', error);
|
||||
toast.error('Failed to create project', {
|
||||
description: error instanceof Error ? error.message : 'Unknown error',
|
||||
});
|
||||
} finally {
|
||||
setIsCreatingProject(false);
|
||||
}
|
||||
},
|
||||
[finalizeProjectCreation]
|
||||
);
|
||||
|
||||
/**
|
||||
* Create project from a starter template
|
||||
*/
|
||||
const handleCreateFromTemplate = useCallback(
|
||||
async (template: StarterTemplate, projectName: string, parentDir: string) => {
|
||||
setIsCreatingProject(true);
|
||||
try {
|
||||
const api = getElectronAPI();
|
||||
const projectPath = `${parentDir}/${projectName}`;
|
||||
|
||||
// Clone template repository
|
||||
await api.git.clone(template.githubUrl, projectPath);
|
||||
|
||||
// Finalize project setup
|
||||
await finalizeProjectCreation(projectPath, projectName);
|
||||
} catch (error) {
|
||||
console.error('[ProjectCreation] Failed to create from template:', error);
|
||||
toast.error('Failed to create project from template', {
|
||||
description: error instanceof Error ? error.message : 'Unknown error',
|
||||
});
|
||||
} finally {
|
||||
setIsCreatingProject(false);
|
||||
}
|
||||
},
|
||||
[finalizeProjectCreation]
|
||||
);
|
||||
|
||||
/**
|
||||
* Create project from a custom GitHub URL
|
||||
*/
|
||||
const handleCreateFromCustomUrl = useCallback(
|
||||
async (repoUrl: string, projectName: string, parentDir: string) => {
|
||||
setIsCreatingProject(true);
|
||||
try {
|
||||
const api = getElectronAPI();
|
||||
const projectPath = `${parentDir}/${projectName}`;
|
||||
|
||||
// Clone custom repository
|
||||
await api.git.clone(repoUrl, projectPath);
|
||||
|
||||
// Finalize project setup
|
||||
await finalizeProjectCreation(projectPath, projectName);
|
||||
} catch (error) {
|
||||
console.error('[ProjectCreation] Failed to create from custom URL:', error);
|
||||
toast.error('Failed to create project from URL', {
|
||||
description: error instanceof Error ? error.message : 'Unknown error',
|
||||
});
|
||||
} finally {
|
||||
setIsCreatingProject(false);
|
||||
}
|
||||
},
|
||||
[finalizeProjectCreation]
|
||||
);
|
||||
|
||||
return {
|
||||
// Modal state
|
||||
showNewProjectModal,
|
||||
setShowNewProjectModal,
|
||||
isCreatingProject,
|
||||
|
||||
// Onboarding state
|
||||
showOnboardingDialog,
|
||||
setShowOnboardingDialog,
|
||||
newProjectName,
|
||||
setNewProjectName,
|
||||
newProjectPath,
|
||||
setNewProjectPath,
|
||||
|
||||
// Handlers
|
||||
handleCreateBlankProject,
|
||||
handleCreateFromTemplate,
|
||||
handleCreateFromCustomUrl,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { useAppStore } from '@/store/app-store';
|
||||
import { useThemePreview } from './use-theme-preview';
|
||||
|
||||
/**
|
||||
* Hook that manages project theme state and preview handlers
|
||||
*/
|
||||
export function useProjectTheme() {
|
||||
// Get theme-related values from store
|
||||
const { theme: globalTheme, setTheme, setProjectTheme, setPreviewTheme } = useAppStore();
|
||||
|
||||
// Get debounced preview handlers
|
||||
const { handlePreviewEnter, handlePreviewLeave } = useThemePreview({ setPreviewTheme });
|
||||
|
||||
return {
|
||||
// Theme state
|
||||
globalTheme,
|
||||
setTheme,
|
||||
setProjectTheme,
|
||||
setPreviewTheme,
|
||||
|
||||
// Preview handlers
|
||||
handlePreviewEnter,
|
||||
handlePreviewLeave,
|
||||
};
|
||||
}
|
||||
147
apps/ui/src/components/layout/sidebar/hooks/use-setup-dialog.ts
Normal file
147
apps/ui/src/components/layout/sidebar/hooks/use-setup-dialog.ts
Normal file
@@ -0,0 +1,147 @@
|
||||
import { useState, useCallback } from 'react';
|
||||
import { getElectronAPI } from '@/lib/electron';
|
||||
import { toast } from 'sonner';
|
||||
import type { FeatureCount } from '@/components/views/spec-view/types';
|
||||
|
||||
interface UseSetupDialogProps {
|
||||
setSpecCreatingForProject: (path: string | null) => void;
|
||||
newProjectPath: string;
|
||||
setNewProjectName: (name: string) => void;
|
||||
setNewProjectPath: (path: string) => void;
|
||||
setShowOnboardingDialog: (show: boolean) => void;
|
||||
}
|
||||
|
||||
export function useSetupDialog({
|
||||
setSpecCreatingForProject,
|
||||
newProjectPath,
|
||||
setNewProjectName,
|
||||
setNewProjectPath,
|
||||
setShowOnboardingDialog,
|
||||
}: UseSetupDialogProps) {
|
||||
// Setup dialog state
|
||||
const [showSetupDialog, setShowSetupDialog] = useState(false);
|
||||
const [setupProjectPath, setSetupProjectPath] = useState('');
|
||||
const [projectOverview, setProjectOverview] = useState('');
|
||||
const [generateFeatures, setGenerateFeatures] = useState(true);
|
||||
const [analyzeProject, setAnalyzeProject] = useState(true);
|
||||
const [featureCount, setFeatureCount] = useState<FeatureCount>(50);
|
||||
|
||||
/**
|
||||
* Handle creating initial spec for new project
|
||||
*/
|
||||
const handleCreateInitialSpec = useCallback(async () => {
|
||||
if (!setupProjectPath || !projectOverview.trim()) return;
|
||||
|
||||
// Set store state immediately so the loader shows up right away
|
||||
setSpecCreatingForProject(setupProjectPath);
|
||||
setShowSetupDialog(false);
|
||||
|
||||
try {
|
||||
const api = getElectronAPI();
|
||||
if (!api.specRegeneration) {
|
||||
toast.error('Spec regeneration not available');
|
||||
setSpecCreatingForProject(null);
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await api.specRegeneration.create(
|
||||
setupProjectPath,
|
||||
projectOverview.trim(),
|
||||
generateFeatures,
|
||||
analyzeProject,
|
||||
generateFeatures ? featureCount : undefined // only pass maxFeatures if generating features
|
||||
);
|
||||
|
||||
if (!result.success) {
|
||||
console.error('[SetupDialog] Failed to start spec creation:', result.error);
|
||||
setSpecCreatingForProject(null);
|
||||
toast.error('Failed to create specification', {
|
||||
description: result.error,
|
||||
});
|
||||
} else {
|
||||
// Show processing toast to inform user
|
||||
toast.info('Generating app specification...', {
|
||||
description: "This may take a minute. You'll be notified when complete.",
|
||||
});
|
||||
}
|
||||
// If successful, we'll wait for the events to update the state
|
||||
} catch (error) {
|
||||
console.error('[SetupDialog] Failed to create spec:', error);
|
||||
setSpecCreatingForProject(null);
|
||||
toast.error('Failed to create specification', {
|
||||
description: error instanceof Error ? error.message : 'Unknown error',
|
||||
});
|
||||
}
|
||||
}, [
|
||||
setupProjectPath,
|
||||
projectOverview,
|
||||
generateFeatures,
|
||||
analyzeProject,
|
||||
featureCount,
|
||||
setSpecCreatingForProject,
|
||||
]);
|
||||
|
||||
/**
|
||||
* Handle skipping setup
|
||||
*/
|
||||
const handleSkipSetup = useCallback(() => {
|
||||
setShowSetupDialog(false);
|
||||
setProjectOverview('');
|
||||
setSetupProjectPath('');
|
||||
|
||||
// Clear onboarding state if we came from onboarding
|
||||
if (newProjectPath) {
|
||||
setNewProjectName('');
|
||||
setNewProjectPath('');
|
||||
}
|
||||
|
||||
toast.info('Setup skipped', {
|
||||
description: 'You can set up your app_spec.txt later from the Spec view.',
|
||||
});
|
||||
}, [newProjectPath, setNewProjectName, setNewProjectPath]);
|
||||
|
||||
/**
|
||||
* Handle onboarding dialog - generate spec
|
||||
*/
|
||||
const handleOnboardingGenerateSpec = useCallback(() => {
|
||||
setShowOnboardingDialog(false);
|
||||
// Navigate to the setup dialog flow
|
||||
setSetupProjectPath(newProjectPath);
|
||||
setProjectOverview('');
|
||||
setShowSetupDialog(true);
|
||||
}, [newProjectPath, setShowOnboardingDialog]);
|
||||
|
||||
/**
|
||||
* Handle onboarding dialog - skip
|
||||
*/
|
||||
const handleOnboardingSkip = useCallback(() => {
|
||||
setShowOnboardingDialog(false);
|
||||
setNewProjectName('');
|
||||
setNewProjectPath('');
|
||||
toast.info('You can generate your app_spec.txt anytime from the Spec view', {
|
||||
description: 'Your project is ready to use!',
|
||||
});
|
||||
}, [setShowOnboardingDialog, setNewProjectName, setNewProjectPath]);
|
||||
|
||||
return {
|
||||
// State
|
||||
showSetupDialog,
|
||||
setShowSetupDialog,
|
||||
setupProjectPath,
|
||||
setSetupProjectPath,
|
||||
projectOverview,
|
||||
setProjectOverview,
|
||||
generateFeatures,
|
||||
setGenerateFeatures,
|
||||
analyzeProject,
|
||||
setAnalyzeProject,
|
||||
featureCount,
|
||||
setFeatureCount,
|
||||
|
||||
// Handlers
|
||||
handleCreateInitialSpec,
|
||||
handleSkipSetup,
|
||||
handleOnboardingGenerateSpec,
|
||||
handleOnboardingSkip,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import { useState } from 'react';
|
||||
import { useTrashOperations } from './use-trash-operations';
|
||||
import type { TrashedProject } from '@/lib/electron';
|
||||
|
||||
interface UseTrashDialogProps {
|
||||
restoreTrashedProject: (projectId: string) => void;
|
||||
deleteTrashedProject: (projectId: string) => void;
|
||||
emptyTrash: () => void;
|
||||
trashedProjects: TrashedProject[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook that combines trash operations with dialog state management
|
||||
*/
|
||||
export function useTrashDialog({
|
||||
restoreTrashedProject,
|
||||
deleteTrashedProject,
|
||||
emptyTrash,
|
||||
trashedProjects,
|
||||
}: UseTrashDialogProps) {
|
||||
// Dialog state
|
||||
const [showTrashDialog, setShowTrashDialog] = useState(false);
|
||||
|
||||
// Reuse existing trash operations logic
|
||||
const trashOperations = useTrashOperations({
|
||||
restoreTrashedProject,
|
||||
deleteTrashedProject,
|
||||
emptyTrash,
|
||||
trashedProjects,
|
||||
});
|
||||
|
||||
return {
|
||||
// Dialog state
|
||||
showTrashDialog,
|
||||
setShowTrashDialog,
|
||||
|
||||
// Trash operations (spread from existing hook)
|
||||
...trashOperations,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user