feat: implement upsert project functionality in sidebar and welcome view

- Refactored project handling in Sidebar and WelcomeView components to use a new `upsertAndSetCurrentProject` action for creating or updating projects.
- Enhanced theme preservation logic during project creation and updates by integrating theme management directly into the store action.
- Cleaned up redundant code related to project existence checks and state updates, improving maintainability and readability.
This commit is contained in:
Cody Seibert
2025-12-12 23:06:22 -05:00
parent c991d5f2f7
commit b32af0c86b
4 changed files with 105 additions and 72 deletions

View File

@@ -2,7 +2,7 @@
import { useState, useMemo, useEffect, useCallback, useRef } from "react";
import { cn } from "@/lib/utils";
import { useAppStore, formatShortcut } from "@/store/app-store";
import { useAppStore, formatShortcut, type ThemeMode } from "@/store/app-store";
import { CoursePromoBadge } from "@/components/ui/course-promo-badge";
import { IS_MARKETING } from "@/config/app-config";
import {
@@ -188,7 +188,7 @@ export function Sidebar() {
currentView,
sidebarOpen,
projectHistory,
addProject,
upsertAndSetCurrentProject,
setCurrentProject,
setCurrentView,
toggleSidebar,
@@ -473,39 +473,14 @@ export function Sidebar() {
return;
}
// Check if project already exists (by path) to preserve theme and other settings
const existingProject = projects.find((p) => p.path === path);
let project: Project;
if (existingProject) {
// Update existing project, preserving theme and other properties
project = {
...existingProject,
name, // Update name in case it changed
lastOpened: new Date().toISOString(),
};
// Update the project in the store (this will update the existing entry)
const updatedProjects = projects.map((p) =>
p.id === existingProject.id ? project : p
);
useAppStore.setState({ projects: updatedProjects });
} else {
// Create new project - check for trashed project with same path first (preserves theme if deleted/recreated)
// Then fall back to current effective theme, then global theme
const trashedProject = trashedProjects.find((p) => p.path === path);
const effectiveTheme =
trashedProject?.theme || currentProject?.theme || globalTheme;
project = {
id: `project-${Date.now()}`,
name,
path,
lastOpened: new Date().toISOString(),
theme: effectiveTheme,
};
addProject(project);
}
setCurrentProject(project);
// Upsert project and set as current (handles both create and update cases)
// Theme preservation is handled by the store action
const trashedProject = trashedProjects.find((p) => p.path === path);
const effectiveTheme =
(trashedProject?.theme as ThemeMode | undefined) ||
(currentProject?.theme as ThemeMode | undefined) ||
globalTheme;
const project = upsertAndSetCurrentProject(path, name, effectiveTheme);
// Check if app_spec.txt exists
const specExists = await hasAppSpec(path);
@@ -540,10 +515,8 @@ export function Sidebar() {
}
}
}, [
projects,
trashedProjects,
addProject,
setCurrentProject,
upsertAndSetCurrentProject,
currentProject,
globalTheme,
]);

View File

@@ -10,7 +10,7 @@ import {
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { useAppStore } from "@/store/app-store";
import { useAppStore, type ThemeMode } from "@/store/app-store";
import { getElectronAPI, type Project } from "@/lib/electron";
import { initializeProject } from "@/lib/project-init";
import {
@@ -36,8 +36,14 @@ import { getHttpApiClient } from "@/lib/http-api-client";
import type { StarterTemplate } from "@/lib/templates";
export function WelcomeView() {
const { projects, addProject, setCurrentProject, setCurrentView } =
useAppStore();
const {
projects,
trashedProjects,
currentProject,
upsertAndSetCurrentProject,
setCurrentView,
theme: globalTheme,
} = useAppStore();
const [showNewProjectModal, setShowNewProjectModal] = useState(false);
const [isCreating, setIsCreating] = useState(false);
const [isOpening, setIsOpening] = useState(false);
@@ -98,35 +104,14 @@ export function WelcomeView() {
return;
}
// Check if project already exists (by path) to preserve theme and other settings
const existingProject = projects.find((p) => p.path === path);
let project: Project;
if (existingProject) {
// Update existing project, preserving theme and other properties
project = {
...existingProject,
name, // Update name in case it changed
lastOpened: new Date().toISOString(),
};
// Update the project in the store (this will update the existing entry)
const updatedProjects = projects.map((p) =>
p.id === existingProject.id ? project : p
);
// We need to manually update projects since addProject would create a duplicate
useAppStore.setState({ projects: updatedProjects });
} else {
// Create new project
project = {
id: `project-${Date.now()}`,
name,
path,
lastOpened: new Date().toISOString(),
};
addProject(project);
}
setCurrentProject(project);
// Upsert project and set as current (handles both create and update cases)
// Theme preservation is handled by the store action
const trashedProject = trashedProjects.find((p) => p.path === path);
const effectiveTheme =
(trashedProject?.theme as ThemeMode | undefined) ||
(currentProject?.theme as ThemeMode | undefined) ||
globalTheme;
const project = upsertAndSetCurrentProject(path, name, effectiveTheme);
// Show initialization dialog if files were created
if (initResult.createdFiles && initResult.createdFiles.length > 0) {
@@ -161,7 +146,13 @@ export function WelcomeView() {
setIsOpening(false);
}
},
[projects, addProject, setCurrentProject, analyzeProject]
[
trashedProjects,
currentProject,
globalTheme,
upsertAndSetCurrentProject,
analyzeProject,
]
);
const handleOpenProject = useCallback(async () => {

View File

@@ -416,6 +416,11 @@ export interface AppActions {
deleteTrashedProject: (projectId: string) => void;
emptyTrash: () => void;
setCurrentProject: (project: Project | null) => void;
upsertAndSetCurrentProject: (
path: string,
name: string,
theme?: ThemeMode
) => Project; // Upsert project by path and set as current
reorderProjects: (oldIndex: number, newIndex: number) => void;
cyclePrevProject: () => void; // Cycle back through project history (Q)
cycleNextProject: () => void; // Cycle forward through project history (E)
@@ -767,6 +772,58 @@ export const useAppStore = create<AppState & AppActions>()(
}
},
upsertAndSetCurrentProject: (path, name, theme) => {
const {
projects,
trashedProjects,
currentProject,
theme: globalTheme,
} = get();
const existingProject = projects.find((p) => p.path === path);
let project: Project;
if (existingProject) {
// Update existing project, preserving theme and other properties
project = {
...existingProject,
name, // Update name in case it changed
lastOpened: new Date().toISOString(),
};
// Update the project in the store
const updatedProjects = projects.map((p) =>
p.id === existingProject.id ? project : p
);
set({ projects: updatedProjects });
} else {
// Create new project - check for trashed project with same path first (preserves theme if deleted/recreated)
// Then fall back to provided theme, then current project theme, then global theme
const trashedProject = trashedProjects.find((p) => p.path === path);
const effectiveTheme =
theme ||
trashedProject?.theme ||
currentProject?.theme ||
globalTheme;
project = {
id: `project-${Date.now()}`,
name,
path,
lastOpened: new Date().toISOString(),
theme: effectiveTheme,
};
// Add the new project to the store
set({
projects: [
...projects,
{ ...project, lastOpened: new Date().toISOString() },
],
});
}
// Set as current project (this will also update history and view)
get().setCurrentProject(project);
return project;
},
cyclePrevProject: () => {
const { projectHistory, projectHistoryIndex, projects } = get();
@@ -1222,7 +1279,9 @@ export const useAppStore = create<AppState & AppActions>()(
const current = get().lastSelectedSessionByProject;
if (sessionId === null) {
// Remove the entry for this project
const { [projectPath]: _, ...rest } = current;
const rest = Object.fromEntries(
Object.entries(current).filter(([key]) => key !== projectPath)
);
set({ lastSelectedSessionByProject: rest });
} else {
set({

View File

@@ -55,6 +55,16 @@ export function createTemplatesRoutes(): Router {
// Build full project path
const projectPath = path.join(parentDir, sanitizedName);
const resolvedParent = path.resolve(parentDir);
const resolvedProject = path.resolve(projectPath);
const relativePath = path.relative(resolvedParent, resolvedProject);
if (relativePath.startsWith("..") || path.isAbsolute(relativePath)) {
return res.status(400).json({
success: false,
error: "Invalid project name; potential path traversal attempt.",
});
}
// Check if directory already exists
try {
await fs.access(projectPath);