From 6cfe617711ec7b0b5fdd3d21b355a025bb1101ee Mon Sep 17 00:00:00 2001 From: Haileyesus Date: Fri, 27 Mar 2026 22:16:56 +0300 Subject: [PATCH] refactor: setup project wizards with only three steps --- server/src/modules/agent/agent.routes.js | 20 +- .../projects/projects.inline.routes.js | 18 +- .../src/modules/projects/projects.routes.js | 391 ++++-------------- server/src/modules/projects/projects.utils.ts | 159 +++++++ .../workspace-original-paths.db.ts | 17 +- .../ProjectCreationWizard.tsx | 57 +-- .../components/FolderBrowserModal.tsx | 2 +- .../components/StepConfiguration.tsx | 75 ++-- .../components/StepReview.tsx | 19 +- .../components/StepTypeSelection.tsx | 71 ---- .../components/WizardFooter.tsx | 4 +- .../components/WizardProgress.tsx | 10 +- .../components/WorkspacePathField.tsx | 14 +- .../{workspaceApi.ts => projectWizardApi.ts} | 6 +- .../hooks/useGithubTokens.ts | 2 +- .../project-creation-wizard/types.ts | 9 +- .../utils/pathUtils.ts | 11 +- .../refactored/sidebar/data/workspacesApi.ts | 2 +- src/i18n/locales/en/common.json | 8 +- 19 files changed, 332 insertions(+), 563 deletions(-) create mode 100644 server/src/modules/projects/projects.utils.ts delete mode 100644 src/components/project-creation-wizard/components/StepTypeSelection.tsx rename src/components/project-creation-wizard/data/{workspaceApi.ts => projectWizardApi.ts} (96%) diff --git a/server/src/modules/agent/agent.routes.js b/server/src/modules/agent/agent.routes.js index d35172f3..dfa51b7f 100644 --- a/server/src/modules/agent/agent.routes.js +++ b/server/src/modules/agent/agent.routes.js @@ -7,7 +7,8 @@ import crypto from 'crypto'; import { userDb } from '@/shared/database/repositories/users.js'; import { apiKeysDb } from '@/shared/database/repositories/api-keys.js'; import { githubTokensDb } from '@/shared/database/repositories/github-tokens.js'; -import { addProjectManually } from '../../../projects.js'; +import { workspaceOriginalPathsDb } from '@/shared/database/repositories/workspace-original-paths.db.js'; +import { getWorkspaceNameFromPath } from '@/modules/projects/projects.utils.js'; import { queryClaudeSDK } from '../../../claude-sdk.js'; import { spawnCursor } from '../../../cursor-cli.js'; import { queryCodex } from '../../../openai-codex.js'; @@ -902,20 +903,9 @@ router.post('/', validateExternalApiKey, async (req, res) => { } } - // Register the project (or use existing registration) - let project; - try { - project = await addProjectManually(finalProjectPath); - console.log('📦 Project registered:', project); - } catch (error) { - // If project already exists, that's fine - continue with the existing registration - if (error.message && error.message.includes('Project already configured')) { - console.log('📦 Using existing project registration for:', finalProjectPath); - project = { path: finalProjectPath }; - } else { - throw error; - } - } + // Register the workspace path in the refactored metadata table. + workspaceOriginalPathsDb.createWorkspacePath(finalProjectPath, getWorkspaceNameFromPath(finalProjectPath)); + console.log('📦 Workspace registered:', finalProjectPath); // Set up writer based on streaming mode if (stream) { diff --git a/server/src/modules/projects/projects.inline.routes.js b/server/src/modules/projects/projects.inline.routes.js index 21ce852c..9154d4ad 100644 --- a/server/src/modules/projects/projects.inline.routes.js +++ b/server/src/modules/projects/projects.inline.routes.js @@ -8,12 +8,12 @@ import { renameProject, deleteSession, deleteProject, - addProjectManually, searchConversations } from '../../../projects.js'; import { applyCustomSessionNames, sessionNamesDb } from '@/shared/database/repositories/session-names.js'; +import { workspaceOriginalPathsDb } from '@/shared/database/repositories/workspace-original-paths.db.js'; import { authenticateToken } from '../auth/auth.middleware.js'; -import { WORKSPACES_ROOT, validateWorkspacePath } from './projects.routes.js'; +import { getWorkspaceNameFromPath, WORKSPACES_ROOT, validateWorkspacePath } from './projects.utils.js'; const router = express.Router(); @@ -100,8 +100,18 @@ router.post('/api/projects/create', authenticateToken, async (req, res) => { return res.status(400).json({ error: 'Project path is required' }); } - const project = await addProjectManually(projectPath.trim()); - res.json({ success: true, project }); + const resolvedPath = path.resolve(projectPath.trim()); + const validation = await validateWorkspacePath(resolvedPath); + if (!validation.valid) { + return res.status(400).json({ + error: 'Invalid workspace path', + details: validation.error + }); + } + + const safePath = validation.resolvedPath || resolvedPath; + workspaceOriginalPathsDb.createWorkspacePath(safePath, getWorkspaceNameFromPath(safePath)); + res.json({ success: true, message: 'Workspace saved successfully' }); } catch (error) { console.error('Error creating project:', error); res.status(500).json({ error: error.message }); diff --git a/server/src/modules/projects/projects.routes.js b/server/src/modules/projects/projects.routes.js index a6737138..7ce9425e 100644 --- a/server/src/modules/projects/projects.routes.js +++ b/server/src/modules/projects/projects.routes.js @@ -2,9 +2,9 @@ import express from 'express'; import { promises as fs } from 'fs'; import path from 'path'; import spawn from 'cross-spawn'; -import os from 'os'; -import { addProjectManually } from '../../../projects.js'; import { githubTokensDb } from '@/shared/database/repositories/github-tokens.js'; +import { workspaceOriginalPathsDb } from '@/shared/database/repositories/workspace-original-paths.db.js'; +import { getWorkspaceNameFromPath, validateWorkspacePath } from './projects.utils.js'; const router = express.Router(); @@ -13,177 +13,65 @@ function sanitizeGitError(message, token) { return message.replace(new RegExp(token.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'), '***'); } -// Configure allowed workspace root (defaults to user's home directory) -export const WORKSPACES_ROOT = process.env.WORKSPACES_ROOT || os.homedir(); - -// System-critical paths that should never be used as workspace directories -export const FORBIDDEN_PATHS = [ - // Unix - '/', - '/etc', - '/bin', - '/sbin', - '/usr', - '/dev', - '/proc', - '/sys', - '/var', - '/boot', - '/root', - '/lib', - '/lib64', - '/opt', - '/tmp', - '/run', - // Windows - 'C:\\Windows', - 'C:\\Program Files', - 'C:\\Program Files (x86)', - 'C:\\ProgramData', - 'C:\\System Volume Information', - 'C:\\$Recycle.Bin' -]; - -/** - * Validates that a path is safe for workspace operations - * @param {string} requestedPath - The path to validate - * @returns {Promise<{valid: boolean, resolvedPath?: string, error?: string}>} - */ -export async function validateWorkspacePath(requestedPath) { +router.patch('/workspace-name', async (req, res) => { try { - // Resolve to absolute path - let absolutePath = path.resolve(requestedPath); + const { path: workspacePath, customWorkspaceName } = req.body; - // Check if path is a forbidden system directory - const normalizedPath = path.normalize(absolutePath); - if (FORBIDDEN_PATHS.includes(normalizedPath) || normalizedPath === '/') { - return { - valid: false, - error: 'Cannot use system-critical directories as workspace locations' - }; + if (!workspacePath || !String(workspacePath).trim()) { + return res.status(400).json({ error: 'path is required' }); } - // Additional check for paths starting with forbidden directories - for (const forbidden of FORBIDDEN_PATHS) { - if (normalizedPath === forbidden || - normalizedPath.startsWith(forbidden + path.sep)) { - // Exception: /var/tmp and similar user-accessible paths might be allowed - // but /var itself and most /var subdirectories should be blocked - if (forbidden === '/var' && - (normalizedPath.startsWith('/var/tmp') || - normalizedPath.startsWith('/var/folders'))) { - continue; // Allow these specific cases - } - - return { - valid: false, - error: `Cannot create workspace in system directory: ${forbidden}` - }; - } + const normalizedPath = path.resolve(String(workspacePath).trim()); + const validation = await validateWorkspacePath(normalizedPath); + if (!validation.valid) { + return res.status(400).json({ + error: 'Invalid workspace path', + details: validation.error, + }); } - // Try to resolve the real path (following symlinks) - let realPath; - try { - // Check if path exists to resolve real path - await fs.access(absolutePath); - realPath = await fs.realpath(absolutePath); - } catch (error) { - if (error.code === 'ENOENT') { - // Path doesn't exist yet - check parent directory - let parentPath = path.dirname(absolutePath); - try { - const parentRealPath = await fs.realpath(parentPath); + const safePath = validation.resolvedPath || normalizedPath; + const normalizedCustomName = + typeof customWorkspaceName === 'string' && customWorkspaceName.trim() + ? customWorkspaceName.trim() + : null; - // Reconstruct the full path with real parent - realPath = path.join(parentRealPath, path.basename(absolutePath)); - } catch (parentError) { - if (parentError.code === 'ENOENT') { - // Parent doesn't exist either - use the absolute path as-is - // We'll validate it's within allowed root - realPath = absolutePath; - } else { - throw parentError; - } - } - } else { - throw error; - } - } - - // Resolve the workspace root to its real path - const resolvedWorkspaceRoot = await fs.realpath(WORKSPACES_ROOT); - - // Ensure the resolved path is contained within the allowed workspace root - if (!realPath.startsWith(resolvedWorkspaceRoot + path.sep) && - realPath !== resolvedWorkspaceRoot) { - return { - valid: false, - error: `Workspace path must be within the allowed workspace root: ${WORKSPACES_ROOT}` - }; - } - - // Additional symlink check for existing paths - try { - await fs.access(absolutePath); - const stats = await fs.lstat(absolutePath); - - if (stats.isSymbolicLink()) { - // Verify symlink target is also within allowed root - const linkTarget = await fs.readlink(absolutePath); - const resolvedTarget = path.resolve(path.dirname(absolutePath), linkTarget); - const realTarget = await fs.realpath(resolvedTarget); - - if (!realTarget.startsWith(resolvedWorkspaceRoot + path.sep) && - realTarget !== resolvedWorkspaceRoot) { - return { - valid: false, - error: 'Symlink target is outside the allowed workspace root' - }; - } - } - } catch (error) { - if (error.code !== 'ENOENT') { - throw error; - } - // Path doesn't exist - that's fine for new workspace creation - } - - return { - valid: true, - resolvedPath: realPath - }; + workspaceOriginalPathsDb.updateCustomWorkspaceName(safePath, normalizedCustomName); + return res.json({ + success: true, + message: 'Workspace name updated successfully', + }); } catch (error) { - return { - valid: false, - error: `Path validation failed: ${error.message}` - }; + console.error('Error updating workspace name:', error); + return res.status(500).json({ + error: 'Failed to update workspace name', + details: process.env.NODE_ENV === 'development' ? error.message : undefined, + }); } -} +}); /** - * Create a new workspace + * Create or add a workspace * POST /api/projects/create-workspace * * Body: - * - workspaceType: 'existing' | 'new' * - path: string (workspace path) - * - githubUrl?: string (optional, for new workspaces) - * - githubTokenId?: number (optional, ID of stored token) - * - newGithubToken?: string (optional, one-time token) */ router.post('/create-workspace', async (req, res) => { try { - const { workspaceType, path: workspacePath, githubUrl, githubTokenId, newGithubToken } = req.body; + const { path: workspacePath } = req.body; // Validate required fields - if (!workspaceType || !workspacePath) { - return res.status(400).json({ error: 'workspaceType and path are required' }); + if (!workspacePath) { + return res.status(400).json({ error: 'path is required' }); } - if (!['existing', 'new'].includes(workspaceType)) { - return res.status(400).json({ error: 'workspaceType must be "existing" or "new"' }); + // Cloning must go through the SSE clone endpoint. + if (req.body.githubUrl || req.body.githubTokenId || req.body.newGithubToken) { + return res.status(400).json({ + error: 'Git clone options are not supported on /create-workspace. Use /clone-progress instead.', + }); } // Validate path safety before any operations @@ -197,107 +85,27 @@ router.post('/create-workspace', async (req, res) => { const absolutePath = validation.resolvedPath; - // Handle existing workspace - if (workspaceType === 'existing') { - // Check if the path exists - try { - await fs.access(absolutePath); - const stats = await fs.stat(absolutePath); - - if (!stats.isDirectory()) { - return res.status(400).json({ error: 'Path exists but is not a directory' }); - } - } catch (error) { - if (error.code === 'ENOENT') { - return res.status(404).json({ error: 'Workspace path does not exist' }); - } + // Add existing workspace or create it if it doesn't exist. + let workspaceAlreadyExists = false; + try { + const stats = await fs.stat(absolutePath); + if (!stats.isDirectory()) { + return res.status(400).json({ error: 'Path exists but is not a directory' }); + } + workspaceAlreadyExists = true; + } catch (error) { + if (error.code === 'ENOENT') { + await fs.mkdir(absolutePath, { recursive: true }); + } else { throw error; } - - // Add the existing workspace to the project list - const project = await addProjectManually(absolutePath); - - return res.json({ - success: true, - project, - message: 'Existing workspace added successfully' - }); } - // Handle new workspace creation - if (workspaceType === 'new') { - // Create the directory if it doesn't exist - await fs.mkdir(absolutePath, { recursive: true }); - - // If GitHub URL is provided, clone the repository - if (githubUrl) { - let githubToken = null; - - // Get GitHub token if needed - if (githubTokenId) { - // Fetch token from database - const token = await getGithubTokenById(githubTokenId, req.user.id); - if (!token) { - // Clean up created directory - await fs.rm(absolutePath, { recursive: true, force: true }); - return res.status(404).json({ error: 'GitHub token not found' }); - } - githubToken = token.github_token; - } else if (newGithubToken) { - githubToken = newGithubToken; - } - - // Extract repo name from URL for the clone destination - const normalizedUrl = githubUrl.replace(/\/+$/, '').replace(/\.git$/, ''); - const repoName = normalizedUrl.split('/').pop() || 'repository'; - const clonePath = path.join(absolutePath, repoName); - - // Check if clone destination already exists to prevent data loss - try { - await fs.access(clonePath); - return res.status(409).json({ - error: 'Directory already exists', - details: `The destination path "${clonePath}" already exists. Please choose a different location or remove the existing directory.` - }); - } catch (err) { - // Directory doesn't exist, which is what we want - } - - // Clone the repository into a subfolder - try { - await cloneGitHubRepository(githubUrl, clonePath, githubToken); - } catch (error) { - // Only clean up if clone created partial data (check if dir exists and is empty or partial) - try { - const stats = await fs.stat(clonePath); - if (stats.isDirectory()) { - await fs.rm(clonePath, { recursive: true, force: true }); - } - } catch (cleanupError) { - // Directory doesn't exist or cleanup failed - ignore - } - throw new Error(`Failed to clone repository: ${error.message}`); - } - - // Add the cloned repo path to the project list - const project = await addProjectManually(clonePath); - - return res.json({ - success: true, - project, - message: 'New workspace created and repository cloned successfully' - }); - } - - // Add the new workspace to the project list (no clone) - const project = await addProjectManually(absolutePath); - - return res.json({ - success: true, - project, - message: 'New workspace created successfully' - }); - } + workspaceOriginalPathsDb.createWorkspacePath(absolutePath, getWorkspaceNameFromPath(absolutePath)); + return res.json({ + success: true, + message: workspaceAlreadyExists ? 'Workspace added successfully' : 'Workspace created successfully' + }); } catch (error) { console.error('Error creating workspace:', error); @@ -347,13 +155,25 @@ router.get('/clone-progress', async (req, res) => { const absolutePath = validation.resolvedPath; - await fs.mkdir(absolutePath, { recursive: true }); + try { + const existingPathStats = await fs.stat(absolutePath); + if (!existingPathStats.isDirectory()) { + sendEvent('error', { message: 'Path exists but is not a directory' }); + res.end(); + return; + } + } catch (error) { + if (error.code === 'ENOENT') { + await fs.mkdir(absolutePath, { recursive: true }); + } else { + throw error; + } + } let githubToken = null; if (githubTokenId) { const token = await getGithubTokenById(parseInt(githubTokenId), req.user.id); if (!token) { - await fs.rm(absolutePath, { recursive: true, force: true }); sendEvent('error', { message: 'GitHub token not found' }); res.end(); return; @@ -419,10 +239,10 @@ router.get('/clone-progress', async (req, res) => { gitProcess.on('close', async (code) => { if (code === 0) { try { - const project = await addProjectManually(clonePath); - sendEvent('complete', { project, message: 'Repository cloned successfully' }); + workspaceOriginalPathsDb.createWorkspacePath(clonePath, getWorkspaceNameFromPath(clonePath)); + sendEvent('complete', { message: 'Repository cloned successfully' }); } catch (error) { - sendEvent('error', { message: `Clone succeeded but failed to add project: ${error.message}` }); + sendEvent('error', { message: `Clone succeeded but failed to register workspace: ${error.message}` }); } } else { const sanitizedError = sanitizeGitError(lastError, githubToken); @@ -465,71 +285,4 @@ router.get('/clone-progress', async (req, res) => { } }); -/** - * Helper function to clone a GitHub repository - */ -function cloneGitHubRepository(githubUrl, destinationPath, githubToken = null) { - return new Promise((resolve, reject) => { - let cloneUrl = githubUrl; - - if (githubToken) { - try { - const url = new URL(githubUrl); - url.username = githubToken; - url.password = ''; - cloneUrl = url.toString(); - } catch (error) { - // SSH URL - use as-is - } - } - - const gitProcess = spawn('git', ['clone', '--progress', cloneUrl, destinationPath], { - stdio: ['ignore', 'pipe', 'pipe'], - env: { - ...process.env, - GIT_TERMINAL_PROMPT: '0' - } - }); - - let stdout = ''; - let stderr = ''; - - gitProcess.stdout.on('data', (data) => { - stdout += data.toString(); - }); - - gitProcess.stderr.on('data', (data) => { - stderr += data.toString(); - }); - - gitProcess.on('close', (code) => { - if (code === 0) { - resolve({ stdout, stderr }); - } else { - let errorMessage = 'Git clone failed'; - - if (stderr.includes('Authentication failed') || stderr.includes('could not read Username')) { - errorMessage = 'Authentication failed. Please check your GitHub token.'; - } else if (stderr.includes('Repository not found')) { - errorMessage = 'Repository not found. Please check the URL and ensure you have access.'; - } else if (stderr.includes('already exists')) { - errorMessage = 'Directory already exists'; - } else if (stderr) { - errorMessage = stderr; - } - - reject(new Error(errorMessage)); - } - }); - - gitProcess.on('error', (error) => { - if (error.code === 'ENOENT') { - reject(new Error('Git is not installed or not in PATH')); - } else { - reject(error); - } - }); - }); -} - export default router; diff --git a/server/src/modules/projects/projects.utils.ts b/server/src/modules/projects/projects.utils.ts new file mode 100644 index 00000000..b6ed90a6 --- /dev/null +++ b/server/src/modules/projects/projects.utils.ts @@ -0,0 +1,159 @@ +import { promises as fs } from 'fs'; +import os from 'os'; +import path from 'path'; + +// Configure allowed workspace root (defaults to user's home directory) +export const WORKSPACES_ROOT = process.env.WORKSPACES_ROOT || os.homedir(); + +// System-critical paths that should never be used as workspace directories +export const FORBIDDEN_PATHS = [ + // Unix + '/', + '/etc', + '/bin', + '/sbin', + '/usr', + '/dev', + '/proc', + '/sys', + '/var', + '/boot', + '/root', + '/lib', + '/lib64', + '/opt', + '/tmp', + '/run', + // Windows + 'C:\\Windows', + 'C:\\Program Files', + 'C:\\Program Files (x86)', + 'C:\\ProgramData', + 'C:\\System Volume Information', + 'C:\\$Recycle.Bin', +]; + +export const getWorkspaceNameFromPath = (workspacePath: string): string => { + const trimmed = workspacePath.trim(); + const normalizedPath = path.normalize(trimmed).replace(/[\\/]+$/, ''); + const baseName = path.basename(normalizedPath); + return baseName || normalizedPath; +}; + +/** + * Validates that a path is safe for workspace operations. + */ +export async function validateWorkspacePath(requestedPath: string): Promise<{ + valid: boolean; + resolvedPath?: string; + error?: string; +}> { + try { + // Resolve to absolute path + const absolutePath = path.resolve(requestedPath); + + // Check if path is a forbidden system directory + const normalizedPath = path.normalize(absolutePath); + if (FORBIDDEN_PATHS.includes(normalizedPath) || normalizedPath === '/') { + return { + valid: false, + error: 'Cannot use system-critical directories as workspace locations', + }; + } + + // Additional check for paths starting with forbidden directories + for (const forbidden of FORBIDDEN_PATHS) { + if (normalizedPath === forbidden || normalizedPath.startsWith(forbidden + path.sep)) { + // Exception: /var/tmp and similar user-accessible paths might be allowed + // but /var itself and most /var subdirectories should be blocked + if ( + forbidden === '/var' && + (normalizedPath.startsWith('/var/tmp') || normalizedPath.startsWith('/var/folders')) + ) { + continue; + } + + return { + valid: false, + error: `Cannot create workspace in system directory: ${forbidden}`, + }; + } + } + + // Try to resolve the real path (following symlinks) + let realPath: string; + try { + // Check if path exists to resolve real path + await fs.access(absolutePath); + realPath = await fs.realpath(absolutePath); + } catch (error: any) { + if (error.code === 'ENOENT') { + // Path doesn't exist yet - check parent directory + const parentPath = path.dirname(absolutePath); + try { + const parentRealPath = await fs.realpath(parentPath); + + // Reconstruct the full path with real parent + realPath = path.join(parentRealPath, path.basename(absolutePath)); + } catch (parentError: any) { + if (parentError.code === 'ENOENT') { + // Parent doesn't exist either - use the absolute path as-is + // We'll validate it's within allowed root + realPath = absolutePath; + } else { + throw parentError; + } + } + } else { + throw error; + } + } + + // Resolve the workspace root to its real path + const resolvedWorkspaceRoot = await fs.realpath(WORKSPACES_ROOT); + + // Ensure the resolved path is contained within the allowed workspace root + if (!realPath.startsWith(resolvedWorkspaceRoot + path.sep) && realPath !== resolvedWorkspaceRoot) { + return { + valid: false, + error: `Workspace path must be within the allowed workspace root: ${WORKSPACES_ROOT}`, + }; + } + + // Additional symlink check for existing paths + try { + await fs.access(absolutePath); + const stats = await fs.lstat(absolutePath); + + if (stats.isSymbolicLink()) { + // Verify symlink target is also within allowed root + const linkTarget = await fs.readlink(absolutePath); + const resolvedTarget = path.resolve(path.dirname(absolutePath), linkTarget); + const realTarget = await fs.realpath(resolvedTarget); + + if (!realTarget.startsWith(resolvedWorkspaceRoot + path.sep) && realTarget !== resolvedWorkspaceRoot) { + return { + valid: false, + error: 'Symlink target is outside the allowed workspace root', + }; + } + } + } catch (error: any) { + if (error.code !== 'ENOENT') { + throw error; + } + // Path doesn't exist - that's fine for new workspace creation + } + + return { + valid: true, + resolvedPath: realPath, + }; + } catch (error: any) { + return { + valid: false, + error: `Path validation failed: ${error.message}`, + }; + } +} + diff --git a/server/src/shared/database/repositories/workspace-original-paths.db.ts b/server/src/shared/database/repositories/workspace-original-paths.db.ts index 8d0f3668..31abd605 100644 --- a/server/src/shared/database/repositories/workspace-original-paths.db.ts +++ b/server/src/shared/database/repositories/workspace-original-paths.db.ts @@ -2,13 +2,18 @@ import { getConnection } from '@/shared/database/connection.js'; import type { WorkspaceOriginalPathRow } from '@/shared/database/types.js'; export const workspaceOriginalPathsDb = { - createWorkspacePath(workspacePath: string): void { + createWorkspacePath(workspacePath: string, customWorkspaceName: string | null = null): void { const db = getConnection(); db.prepare(` - INSERT INTO workspace_original_paths (workspace_path) - VALUES (?) - ON CONFLICT(workspace_path) DO NOTHING - `).run(workspacePath); + INSERT INTO workspace_original_paths (workspace_path, custom_workspace_name) + VALUES (?, ?) + ON CONFLICT(workspace_path) DO UPDATE SET + custom_workspace_name = CASE + WHEN workspace_original_paths.custom_workspace_name IS NULL OR workspace_original_paths.custom_workspace_name = '' + THEN excluded.custom_workspace_name + ELSE workspace_original_paths.custom_workspace_name + END + `).run(workspacePath, customWorkspaceName); }, getCustomWorkspaceName(workspacePath: string): string | null { @@ -30,4 +35,4 @@ export const workspaceOriginalPathsDb = { ON CONFLICT(workspace_path) DO UPDATE SET custom_workspace_name = excluded.custom_workspace_name `).run(workspacePath, customWorkspaceName); }, -} +}; diff --git a/src/components/project-creation-wizard/ProjectCreationWizard.tsx b/src/components/project-creation-wizard/ProjectCreationWizard.tsx index fca05c53..c50bacd2 100644 --- a/src/components/project-creation-wizard/ProjectCreationWizard.tsx +++ b/src/components/project-creation-wizard/ProjectCreationWizard.tsx @@ -4,21 +4,19 @@ import { useTranslation } from 'react-i18next'; import ErrorBanner from './components/ErrorBanner'; import StepConfiguration from './components/StepConfiguration'; import StepReview from './components/StepReview'; -import StepTypeSelection from './components/StepTypeSelection'; import WizardFooter from './components/WizardFooter'; import WizardProgress from './components/WizardProgress'; import { useGithubTokens } from './hooks/useGithubTokens'; -import { cloneWorkspaceWithProgress, createWorkspaceRequest } from './data/workspaceApi'; +import { cloneWorkspaceWithProgress, createWorkspaceRequest } from './data/projectWizardApi'; import { isCloneWorkflow, shouldShowGithubAuthentication } from './utils/pathUtils'; -import type { TokenMode, WizardFormState, WizardStep, WorkspaceType } from './types'; +import type { TokenMode, WizardFormState, WizardStep } from './types'; type ProjectCreationWizardProps = { onClose: () => void; - onProjectCreated?: (project?: Record) => void; + onProjectCreated?: () => void; }; const initialFormState: WizardFormState = { - workspaceType: 'existing', workspacePath: '', githubUrl: '', tokenMode: 'stored', @@ -37,8 +35,7 @@ export default function ProjectCreationWizard({ const [error, setError] = useState(null); const [cloneProgress, setCloneProgress] = useState(''); - const shouldLoadTokens = - step === 2 && shouldShowGithubAuthentication(formState.workspaceType, formState.githubUrl); + const shouldLoadTokens = step === 1 && shouldShowGithubAuthentication(formState.githubUrl); const autoSelectToken = useCallback((tokenId: string) => { setFormState((previous) => ({ ...previous, selectedGithubToken: tokenId })); @@ -60,11 +57,6 @@ export default function ProjectCreationWizard({ setFormState((previous) => ({ ...previous, [key]: value })); }, []); - const updateWorkspaceType = useCallback( - (workspaceType: WorkspaceType) => updateField('workspaceType', workspaceType), - [updateField], - ); - const updateTokenMode = useCallback( (tokenMode: TokenMode) => updateField('tokenMode', tokenMode), [updateField], @@ -74,22 +66,13 @@ export default function ProjectCreationWizard({ setError(null); if (step === 1) { - if (!formState.workspaceType) { - setError(t('projectWizard.errors.selectType')); - return; - } - setStep(2); - return; - } - - if (step === 2) { if (!formState.workspacePath.trim()) { setError(t('projectWizard.errors.providePath')); return; } - setStep(3); + setStep(2); } - }, [formState.workspacePath, formState.workspaceType, step, t]); + }, [formState.workspacePath, step, t]); const handleBack = useCallback(() => { setError(null); @@ -102,10 +85,10 @@ export default function ProjectCreationWizard({ setCloneProgress(''); try { - const shouldCloneRepository = isCloneWorkflow(formState.workspaceType, formState.githubUrl); + const shouldCloneRepository = isCloneWorkflow(formState.githubUrl); if (shouldCloneRepository) { - const project = await cloneWorkspaceWithProgress( + await cloneWorkspaceWithProgress( { workspacePath: formState.workspacePath, githubUrl: formState.githubUrl, @@ -118,17 +101,16 @@ export default function ProjectCreationWizard({ }, ); - onProjectCreated?.(project); + onProjectCreated?.(); onClose(); return; } - const project = await createWorkspaceRequest({ - workspaceType: formState.workspaceType, + await createWorkspaceRequest({ path: formState.workspacePath.trim(), }); - onProjectCreated?.(project); + onProjectCreated?.(); onClose(); } catch (createError) { const errorMessage = @@ -141,10 +123,7 @@ export default function ProjectCreationWizard({ } }, [formState, onClose, onProjectCreated, t]); - const shouldCloneRepository = useMemo( - () => isCloneWorkflow(formState.workspaceType, formState.githubUrl), - [formState.githubUrl, formState.workspaceType], - ); + const shouldCloneRepository = useMemo(() => isCloneWorkflow(formState.githubUrl), [formState.githubUrl]); return (
@@ -173,15 +152,7 @@ export default function ProjectCreationWizard({ {error && } {step === 1 && ( - - )} - - {step === 2 && ( updateField('newGithubToken', newGithubToken) } - onAdvanceToConfirm={() => setStep(3)} + onAdvanceToConfirm={() => setStep(2)} /> )} - {step === 3 && ( + {step === 2 && (

- {workspaceType === 'existing' - ? t('projectWizard.step2.existingHelp') - : t('projectWizard.step2.newHelp')} + {t('projectWizard.step2.newHelp')}

- {workspaceType === 'new' && ( - <> -
- - onGithubUrlChange(event.target.value)} - placeholder="https://github.com/username/repository" - className="w-full" - disabled={isCreating} - /> -

- {t('projectWizard.step2.githubHelp')} -

-
+
+ + onGithubUrlChange(event.target.value)} + placeholder="https://github.com/username/repository" + className="w-full" + disabled={isCreating} + /> +

+ {t('projectWizard.step2.githubHelp')} +

+
- {showGithubAuth && ( - - )} - + {showGithubAuth && ( + )}
); diff --git a/src/components/project-creation-wizard/components/StepReview.tsx b/src/components/project-creation-wizard/components/StepReview.tsx index 843726a4..8dccecd6 100644 --- a/src/components/project-creation-wizard/components/StepReview.tsx +++ b/src/components/project-creation-wizard/components/StepReview.tsx @@ -42,17 +42,6 @@ export default function StepReview({
-
- - {t('projectWizard.step3.workspaceType')} - - - {formState.workspaceType === 'existing' - ? t('projectWizard.step3.existingWorkspace') - : t('projectWizard.step3.newWorkspace')} - -
-
{t('projectWizard.step3.path')} @@ -60,7 +49,7 @@ export default function StepReview({
- {formState.workspaceType === 'new' && formState.githubUrl && ( + {formState.githubUrl && ( <>
@@ -94,11 +83,7 @@ export default function StepReview({
) : (

- {formState.workspaceType === 'existing' - ? t('projectWizard.step3.existingInfo') - : formState.githubUrl - ? t('projectWizard.step3.newWithClone') - : t('projectWizard.step3.newEmpty')} + {formState.githubUrl ? t('projectWizard.step3.newWithClone') : t('projectWizard.step3.newEmpty')}

)}
diff --git a/src/components/project-creation-wizard/components/StepTypeSelection.tsx b/src/components/project-creation-wizard/components/StepTypeSelection.tsx deleted file mode 100644 index efa4345e..00000000 --- a/src/components/project-creation-wizard/components/StepTypeSelection.tsx +++ /dev/null @@ -1,71 +0,0 @@ -import { FolderPlus, GitBranch } from 'lucide-react'; -import { useTranslation } from 'react-i18next'; -import type { WorkspaceType } from '../types'; - -type StepTypeSelectionProps = { - workspaceType: WorkspaceType; - onWorkspaceTypeChange: (workspaceType: WorkspaceType) => void; -}; - -export default function StepTypeSelection({ - workspaceType, - onWorkspaceTypeChange, -}: StepTypeSelectionProps) { - const { t } = useTranslation(); - - return ( -
-

- {t('projectWizard.step1.question')} -

- -
- - - -
-
- ); -} diff --git a/src/components/project-creation-wizard/components/WizardFooter.tsx b/src/components/project-creation-wizard/components/WizardFooter.tsx index 5fbf64ab..78cf2fec 100644 --- a/src/components/project-creation-wizard/components/WizardFooter.tsx +++ b/src/components/project-creation-wizard/components/WizardFooter.tsx @@ -37,7 +37,7 @@ export default function WizardFooter({ )} -