mirror of
https://github.com/siteboon/claudecodeui.git
synced 2026-02-27 19:17:40 +00:00
feat: enhance project creation wizard with folder creation and git clone progress
- Add "+" button to create new folders directly from folder browser - Add SSE endpoint for git clone with real-time progress display - Show clone progress (receiving objects, resolving deltas) in UI - Detect SSH URLs and display "SSH Key" instead of "No authentication" - Hide token section for SSH URLs (tokens only work with HTTPS) - Fix auto-advance behavior: only auto-advance for "Existing Workspace" - Fix various misleading UI messages - Support auth token via query param for SSE endpoints
This commit is contained in:
@@ -550,6 +550,34 @@ app.get('/api/browse-filesystem', authenticateToken, async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
app.post('/api/create-folder', authenticateToken, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { path: folderPath } = req.body;
|
||||||
|
if (!folderPath) {
|
||||||
|
return res.status(400).json({ error: 'Path is required' });
|
||||||
|
}
|
||||||
|
const homeDir = os.homedir();
|
||||||
|
const targetPath = path.resolve(folderPath.replace('~', homeDir));
|
||||||
|
const parentDir = path.dirname(targetPath);
|
||||||
|
try {
|
||||||
|
await fs.promises.access(parentDir);
|
||||||
|
} catch (err) {
|
||||||
|
return res.status(404).json({ error: 'Parent directory does not exist' });
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await fs.promises.access(targetPath);
|
||||||
|
return res.status(409).json({ error: 'Folder already exists' });
|
||||||
|
} catch (err) {
|
||||||
|
// Folder doesn't exist, which is what we want
|
||||||
|
}
|
||||||
|
await fs.promises.mkdir(targetPath, { recursive: false });
|
||||||
|
res.json({ success: true, path: targetPath });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error creating folder:', error);
|
||||||
|
res.status(500).json({ error: 'Failed to create folder' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Read file content endpoint
|
// Read file content endpoint
|
||||||
app.get('/api/projects/:projectName/file', authenticateToken, async (req, res) => {
|
app.get('/api/projects/:projectName/file', authenticateToken, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -37,7 +37,12 @@ const authenticateToken = async (req, res, next) => {
|
|||||||
|
|
||||||
// Normal OSS JWT validation
|
// Normal OSS JWT validation
|
||||||
const authHeader = req.headers['authorization'];
|
const authHeader = req.headers['authorization'];
|
||||||
const token = authHeader && authHeader.split(' ')[1]; // Bearer TOKEN
|
let token = authHeader && authHeader.split(' ')[1]; // Bearer TOKEN
|
||||||
|
|
||||||
|
// Also check query param for SSE endpoints (EventSource can't set headers)
|
||||||
|
if (!token && req.query.token) {
|
||||||
|
token = req.query.token;
|
||||||
|
}
|
||||||
|
|
||||||
if (!token) {
|
if (!token) {
|
||||||
return res.status(401).json({ error: 'Access denied. No token provided.' });
|
return res.status(401).json({ error: 'Access denied. No token provided.' });
|
||||||
|
|||||||
@@ -212,20 +212,7 @@ router.post('/create-workspace', async (req, res) => {
|
|||||||
|
|
||||||
// Handle new workspace creation
|
// Handle new workspace creation
|
||||||
if (workspaceType === 'new') {
|
if (workspaceType === 'new') {
|
||||||
// Check if path already exists
|
// Create the directory if it doesn't exist
|
||||||
try {
|
|
||||||
await fs.access(absolutePath);
|
|
||||||
return res.status(400).json({
|
|
||||||
error: 'Path already exists. Please choose a different path or use "existing workspace" option.'
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
if (error.code !== 'ENOENT') {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
// Path doesn't exist - good, we can create it
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create the directory
|
|
||||||
await fs.mkdir(absolutePath, { recursive: true });
|
await fs.mkdir(absolutePath, { recursive: true });
|
||||||
|
|
||||||
// If GitHub URL is provided, clone the repository
|
// If GitHub URL is provided, clone the repository
|
||||||
@@ -246,22 +233,35 @@ router.post('/create-workspace', async (req, res) => {
|
|||||||
githubToken = newGithubToken;
|
githubToken = newGithubToken;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clone the repository
|
// Extract repo name from URL for the clone destination
|
||||||
|
const repoName = githubUrl.replace(/\.git$/, '').split('/').pop();
|
||||||
|
const clonePath = path.join(absolutePath, repoName);
|
||||||
|
|
||||||
|
// Clone the repository into a subfolder
|
||||||
try {
|
try {
|
||||||
await cloneGitHubRepository(githubUrl, absolutePath, githubToken);
|
await cloneGitHubRepository(githubUrl, clonePath, githubToken);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Clean up created directory on failure
|
// Clean up created directory on failure
|
||||||
try {
|
try {
|
||||||
await fs.rm(absolutePath, { recursive: true, force: true });
|
await fs.rm(clonePath, { recursive: true, force: true });
|
||||||
} catch (cleanupError) {
|
} catch (cleanupError) {
|
||||||
console.error('Failed to clean up directory after clone failure:', cleanupError);
|
console.error('Failed to clean up directory after clone failure:', cleanupError);
|
||||||
// Continue to throw original error
|
// Continue to throw original error
|
||||||
}
|
}
|
||||||
throw new Error(`Failed to clone repository: ${error.message}`);
|
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
|
// Add the new workspace to the project list (no clone)
|
||||||
const project = await addProjectManually(absolutePath);
|
const project = await addProjectManually(absolutePath);
|
||||||
|
|
||||||
return res.json({
|
return res.json({
|
||||||
@@ -305,31 +305,158 @@ async function getGithubTokenById(tokenId, userId) {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clone repository with progress streaming (SSE)
|
||||||
|
* GET /api/projects/clone-progress
|
||||||
|
*/
|
||||||
|
router.get('/clone-progress', async (req, res) => {
|
||||||
|
const { path: workspacePath, githubUrl, githubTokenId, newGithubToken } = req.query;
|
||||||
|
|
||||||
|
res.setHeader('Content-Type', 'text/event-stream');
|
||||||
|
res.setHeader('Cache-Control', 'no-cache');
|
||||||
|
res.setHeader('Connection', 'keep-alive');
|
||||||
|
res.flushHeaders();
|
||||||
|
|
||||||
|
const sendEvent = (type, data) => {
|
||||||
|
res.write(`data: ${JSON.stringify({ type, ...data })}\n\n`);
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!workspacePath || !githubUrl) {
|
||||||
|
sendEvent('error', { message: 'workspacePath and githubUrl are required' });
|
||||||
|
res.end();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const validation = await validateWorkspacePath(workspacePath);
|
||||||
|
if (!validation.valid) {
|
||||||
|
sendEvent('error', { message: validation.error });
|
||||||
|
res.end();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const absolutePath = validation.resolvedPath;
|
||||||
|
|
||||||
|
await fs.mkdir(absolutePath, { recursive: true });
|
||||||
|
|
||||||
|
let githubToken = null;
|
||||||
|
if (githubTokenId) {
|
||||||
|
const token = await getGithubTokenById(parseInt(githubTokenId), req.user?.id);
|
||||||
|
if (token) {
|
||||||
|
githubToken = token.github_token;
|
||||||
|
}
|
||||||
|
} else if (newGithubToken) {
|
||||||
|
githubToken = newGithubToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
const repoName = githubUrl.replace(/\.git$/, '').split('/').pop();
|
||||||
|
const clonePath = path.join(absolutePath, repoName);
|
||||||
|
|
||||||
|
let cloneUrl = githubUrl;
|
||||||
|
if (githubToken) {
|
||||||
|
try {
|
||||||
|
const url = new URL(githubUrl);
|
||||||
|
url.username = githubToken;
|
||||||
|
url.password = '';
|
||||||
|
cloneUrl = url.toString();
|
||||||
|
} catch (error) {
|
||||||
|
// SSH URL or invalid - use as-is
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sendEvent('progress', { message: `Cloning into '${repoName}'...` });
|
||||||
|
|
||||||
|
const gitProcess = spawn('git', ['clone', '--progress', cloneUrl, clonePath], {
|
||||||
|
stdio: ['ignore', 'pipe', 'pipe'],
|
||||||
|
env: {
|
||||||
|
...process.env,
|
||||||
|
GIT_TERMINAL_PROMPT: '0'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let lastError = '';
|
||||||
|
|
||||||
|
gitProcess.stdout.on('data', (data) => {
|
||||||
|
const message = data.toString().trim();
|
||||||
|
if (message) {
|
||||||
|
sendEvent('progress', { message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
gitProcess.stderr.on('data', (data) => {
|
||||||
|
const message = data.toString().trim();
|
||||||
|
lastError = message;
|
||||||
|
if (message) {
|
||||||
|
sendEvent('progress', { message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
gitProcess.on('close', async (code) => {
|
||||||
|
if (code === 0) {
|
||||||
|
try {
|
||||||
|
const project = await addProjectManually(clonePath);
|
||||||
|
sendEvent('complete', { project, message: 'Repository cloned successfully' });
|
||||||
|
} catch (error) {
|
||||||
|
sendEvent('error', { message: `Clone succeeded but failed to add project: ${error.message}` });
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let errorMessage = 'Git clone failed';
|
||||||
|
if (lastError.includes('Authentication failed') || lastError.includes('could not read Username')) {
|
||||||
|
errorMessage = 'Authentication failed. Please check your credentials.';
|
||||||
|
} else if (lastError.includes('Repository not found')) {
|
||||||
|
errorMessage = 'Repository not found. Please check the URL and ensure you have access.';
|
||||||
|
} else if (lastError.includes('already exists')) {
|
||||||
|
errorMessage = 'Directory already exists';
|
||||||
|
} else if (lastError) {
|
||||||
|
errorMessage = lastError;
|
||||||
|
}
|
||||||
|
sendEvent('error', { message: errorMessage });
|
||||||
|
}
|
||||||
|
res.end();
|
||||||
|
});
|
||||||
|
|
||||||
|
gitProcess.on('error', (error) => {
|
||||||
|
if (error.code === 'ENOENT') {
|
||||||
|
sendEvent('error', { message: 'Git is not installed or not in PATH' });
|
||||||
|
} else {
|
||||||
|
sendEvent('error', { message: error.message });
|
||||||
|
}
|
||||||
|
res.end();
|
||||||
|
});
|
||||||
|
|
||||||
|
req.on('close', () => {
|
||||||
|
gitProcess.kill();
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
sendEvent('error', { message: error.message });
|
||||||
|
res.end();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Helper function to clone a GitHub repository
|
* Helper function to clone a GitHub repository
|
||||||
*/
|
*/
|
||||||
function cloneGitHubRepository(githubUrl, destinationPath, githubToken = null) {
|
function cloneGitHubRepository(githubUrl, destinationPath, githubToken = null) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
// Parse GitHub URL and inject token if provided
|
|
||||||
let cloneUrl = githubUrl;
|
let cloneUrl = githubUrl;
|
||||||
|
|
||||||
if (githubToken) {
|
if (githubToken) {
|
||||||
try {
|
try {
|
||||||
const url = new URL(githubUrl);
|
const url = new URL(githubUrl);
|
||||||
// Format: https://TOKEN@github.com/user/repo.git
|
|
||||||
url.username = githubToken;
|
url.username = githubToken;
|
||||||
url.password = '';
|
url.password = '';
|
||||||
cloneUrl = url.toString();
|
cloneUrl = url.toString();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return reject(new Error('Invalid GitHub URL format'));
|
// SSH URL - use as-is
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const gitProcess = spawn('git', ['clone', cloneUrl, destinationPath], {
|
const gitProcess = spawn('git', ['clone', '--progress', cloneUrl, destinationPath], {
|
||||||
stdio: ['ignore', 'pipe', 'pipe'],
|
stdio: ['ignore', 'pipe', 'pipe'],
|
||||||
env: {
|
env: {
|
||||||
...process.env,
|
...process.env,
|
||||||
GIT_TERMINAL_PROMPT: '0' // Disable git password prompts
|
GIT_TERMINAL_PROMPT: '0'
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -348,7 +475,6 @@ function cloneGitHubRepository(githubUrl, destinationPath, githubToken = null) {
|
|||||||
if (code === 0) {
|
if (code === 0) {
|
||||||
resolve({ stdout, stderr });
|
resolve({ stdout, stderr });
|
||||||
} else {
|
} else {
|
||||||
// Parse git error messages to provide helpful feedback
|
|
||||||
let errorMessage = 'Git clone failed';
|
let errorMessage = 'Git clone failed';
|
||||||
|
|
||||||
if (stderr.includes('Authentication failed') || stderr.includes('could not read Username')) {
|
if (stderr.includes('Authentication failed') || stderr.includes('could not read Username')) {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { X, FolderPlus, GitBranch, Key, ChevronRight, ChevronLeft, Check, Loader2, AlertCircle, FolderOpen, Eye, EyeOff } from 'lucide-react';
|
import { X, FolderPlus, GitBranch, Key, ChevronRight, ChevronLeft, Check, Loader2, AlertCircle, FolderOpen, Eye, EyeOff, Plus } from 'lucide-react';
|
||||||
import { Button } from './ui/button';
|
import { Button } from './ui/button';
|
||||||
import { Input } from './ui/input';
|
import { Input } from './ui/input';
|
||||||
import { api } from '../utils/api';
|
import { api } from '../utils/api';
|
||||||
@@ -30,6 +30,10 @@ const ProjectCreationWizard = ({ onClose, onProjectCreated }) => {
|
|||||||
const [browserFolders, setBrowserFolders] = useState([]);
|
const [browserFolders, setBrowserFolders] = useState([]);
|
||||||
const [loadingFolders, setLoadingFolders] = useState(false);
|
const [loadingFolders, setLoadingFolders] = useState(false);
|
||||||
const [showHiddenFolders, setShowHiddenFolders] = useState(false);
|
const [showHiddenFolders, setShowHiddenFolders] = useState(false);
|
||||||
|
const [showNewFolderInput, setShowNewFolderInput] = useState(false);
|
||||||
|
const [newFolderName, setNewFolderName] = useState('');
|
||||||
|
const [creatingFolder, setCreatingFolder] = useState(false);
|
||||||
|
const [cloneProgress, setCloneProgress] = useState('');
|
||||||
|
|
||||||
// Load available GitHub tokens when needed
|
// Load available GitHub tokens when needed
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -78,9 +82,10 @@ const ProjectCreationWizard = ({ onClose, onProjectCreated }) => {
|
|||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
if (data.suggestions) {
|
if (data.suggestions) {
|
||||||
// Filter suggestions based on the input
|
// Filter suggestions based on the input, excluding exact match
|
||||||
const filtered = data.suggestions.filter(s =>
|
const filtered = data.suggestions.filter(s =>
|
||||||
s.path.toLowerCase().startsWith(inputPath.toLowerCase())
|
s.path.toLowerCase().startsWith(inputPath.toLowerCase()) &&
|
||||||
|
s.path.toLowerCase() !== inputPath.toLowerCase()
|
||||||
);
|
);
|
||||||
setPathSuggestions(filtered.slice(0, 5));
|
setPathSuggestions(filtered.slice(0, 5));
|
||||||
setShowPathDropdown(filtered.length > 0);
|
setShowPathDropdown(filtered.length > 0);
|
||||||
@@ -118,24 +123,62 @@ const ProjectCreationWizard = ({ onClose, onProjectCreated }) => {
|
|||||||
const handleCreate = async () => {
|
const handleCreate = async () => {
|
||||||
setIsCreating(true);
|
setIsCreating(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
|
setCloneProgress('');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
if (workspaceType === 'new' && githubUrl) {
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
path: workspacePath.trim(),
|
||||||
|
githubUrl: githubUrl.trim(),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (tokenMode === 'stored' && selectedGithubToken) {
|
||||||
|
params.append('githubTokenId', selectedGithubToken);
|
||||||
|
} else if (tokenMode === 'new' && newGithubToken) {
|
||||||
|
params.append('newGithubToken', newGithubToken.trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
const token = localStorage.getItem('auth-token');
|
||||||
|
const url = `/api/projects/clone-progress?${params}${token ? `&token=${token}` : ''}`;
|
||||||
|
|
||||||
|
await new Promise((resolve, reject) => {
|
||||||
|
const eventSource = new EventSource(url);
|
||||||
|
|
||||||
|
eventSource.onmessage = (event) => {
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(event.data);
|
||||||
|
|
||||||
|
if (data.type === 'progress') {
|
||||||
|
setCloneProgress(data.message);
|
||||||
|
} else if (data.type === 'complete') {
|
||||||
|
eventSource.close();
|
||||||
|
if (onProjectCreated) {
|
||||||
|
onProjectCreated(data.project);
|
||||||
|
}
|
||||||
|
onClose();
|
||||||
|
resolve();
|
||||||
|
} else if (data.type === 'error') {
|
||||||
|
eventSource.close();
|
||||||
|
reject(new Error(data.message));
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Error parsing SSE event:', e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
eventSource.onerror = () => {
|
||||||
|
eventSource.close();
|
||||||
|
reject(new Error('Connection lost during clone'));
|
||||||
|
};
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const payload = {
|
const payload = {
|
||||||
workspaceType,
|
workspaceType,
|
||||||
path: workspacePath.trim(),
|
path: workspacePath.trim(),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Add GitHub info if creating new workspace with GitHub URL
|
|
||||||
if (workspaceType === 'new' && githubUrl) {
|
|
||||||
payload.githubUrl = githubUrl.trim();
|
|
||||||
|
|
||||||
if (tokenMode === 'stored' && selectedGithubToken) {
|
|
||||||
payload.githubTokenId = parseInt(selectedGithubToken);
|
|
||||||
} else if (tokenMode === 'new' && newGithubToken) {
|
|
||||||
payload.newGithubToken = newGithubToken.trim();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await api.createWorkspace(payload);
|
const response = await api.createWorkspace(payload);
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
@@ -143,7 +186,6 @@ const ProjectCreationWizard = ({ onClose, onProjectCreated }) => {
|
|||||||
throw new Error(data.error || t('projectWizard.errors.failedToCreate'));
|
throw new Error(data.error || t('projectWizard.errors.failedToCreate'));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Success!
|
|
||||||
if (onProjectCreated) {
|
if (onProjectCreated) {
|
||||||
onProjectCreated(data.project);
|
onProjectCreated(data.project);
|
||||||
}
|
}
|
||||||
@@ -193,6 +235,29 @@ const ProjectCreationWizard = ({ onClose, onProjectCreated }) => {
|
|||||||
await loadBrowserFolders(folderPath);
|
await loadBrowserFolders(folderPath);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const createNewFolder = async () => {
|
||||||
|
if (!newFolderName.trim()) return;
|
||||||
|
setCreatingFolder(true);
|
||||||
|
setError(null);
|
||||||
|
try {
|
||||||
|
const separator = browserCurrentPath.includes('\\') ? '\\' : '/';
|
||||||
|
const folderPath = `${browserCurrentPath}${separator}${newFolderName.trim()}`;
|
||||||
|
const response = await api.createFolder(folderPath);
|
||||||
|
const data = await response.json();
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(data.error || t('projectWizard.errors.failedToCreateFolder', 'Failed to create folder'));
|
||||||
|
}
|
||||||
|
setNewFolderName('');
|
||||||
|
setShowNewFolderInput(false);
|
||||||
|
await loadBrowserFolders(data.path || folderPath);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error creating folder:', error);
|
||||||
|
setError(error.message || t('projectWizard.errors.failedToCreateFolder', 'Failed to create folder'));
|
||||||
|
} finally {
|
||||||
|
setCreatingFolder(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="fixed top-0 left-0 right-0 bottom-0 bg-black/50 backdrop-blur-sm flex items-center justify-center z-[60] p-0 sm:p-4">
|
<div className="fixed top-0 left-0 right-0 bottom-0 bg-black/50 backdrop-blur-sm flex items-center justify-center z-[60] p-0 sm:p-4">
|
||||||
<div className="bg-white dark:bg-gray-800 rounded-none sm:rounded-lg shadow-xl w-full h-full sm:h-auto sm:max-w-2xl border-0 sm:border border-gray-200 dark:border-gray-700 overflow-y-auto">
|
<div className="bg-white dark:bg-gray-800 rounded-none sm:rounded-lg shadow-xl w-full h-full sm:h-auto sm:max-w-2xl border-0 sm:border border-gray-200 dark:border-gray-700 overflow-y-auto">
|
||||||
@@ -388,8 +453,8 @@ const ProjectCreationWizard = ({ onClose, onProjectCreated }) => {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* GitHub Token (only if GitHub URL is provided) */}
|
{/* GitHub Token (only for HTTPS URLs - SSH uses SSH keys) */}
|
||||||
{githubUrl && (
|
{githubUrl && !githubUrl.startsWith('git@') && !githubUrl.startsWith('ssh://') && (
|
||||||
<div className="bg-gray-50 dark:bg-gray-900/50 rounded-lg p-4 border border-gray-200 dark:border-gray-700">
|
<div className="bg-gray-50 dark:bg-gray-900/50 rounded-lg p-4 border border-gray-200 dark:border-gray-700">
|
||||||
<div className="flex items-start gap-3 mb-4">
|
<div className="flex items-start gap-3 mb-4">
|
||||||
<Key className="w-5 h-5 text-gray-600 dark:text-gray-400 flex-shrink-0 mt-0.5" />
|
<Key className="w-5 h-5 text-gray-600 dark:text-gray-400 flex-shrink-0 mt-0.5" />
|
||||||
@@ -551,6 +616,8 @@ const ProjectCreationWizard = ({ onClose, onProjectCreated }) => {
|
|||||||
? `${t('projectWizard.step3.usingStoredToken')} ${availableTokens.find(t => t.id.toString() === selectedGithubToken)?.credential_name || 'Unknown'}`
|
? `${t('projectWizard.step3.usingStoredToken')} ${availableTokens.find(t => t.id.toString() === selectedGithubToken)?.credential_name || 'Unknown'}`
|
||||||
: tokenMode === 'new' && newGithubToken
|
: tokenMode === 'new' && newGithubToken
|
||||||
? t('projectWizard.step3.usingProvidedToken')
|
? t('projectWizard.step3.usingProvidedToken')
|
||||||
|
: (githubUrl.startsWith('git@') || githubUrl.startsWith('ssh://'))
|
||||||
|
? t('projectWizard.step3.sshKey', 'SSH Key')
|
||||||
: t('projectWizard.step3.noAuthentication')}
|
: t('projectWizard.step3.noAuthentication')}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -560,13 +627,22 @@ const ProjectCreationWizard = ({ onClose, onProjectCreated }) => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="bg-blue-50 dark:bg-blue-900/20 rounded-lg p-4 border border-blue-200 dark:border-blue-800">
|
<div className="bg-blue-50 dark:bg-blue-900/20 rounded-lg p-4 border border-blue-200 dark:border-blue-800">
|
||||||
<p className="text-sm text-blue-800 dark:text-blue-200">
|
{isCreating && cloneProgress ? (
|
||||||
{workspaceType === 'existing'
|
<div className="space-y-2">
|
||||||
? t('projectWizard.step3.existingInfo')
|
<p className="text-sm font-medium text-blue-800 dark:text-blue-200">{t('projectWizard.step3.cloningRepository', 'Cloning repository...')}</p>
|
||||||
: githubUrl
|
<code className="block text-xs font-mono text-blue-700 dark:text-blue-300 whitespace-pre-wrap break-all">
|
||||||
? t('projectWizard.step3.newWithClone')
|
{cloneProgress}
|
||||||
: t('projectWizard.step3.newEmpty')}
|
</code>
|
||||||
</p>
|
</div>
|
||||||
|
) : (
|
||||||
|
<p className="text-sm text-blue-800 dark:text-blue-200">
|
||||||
|
{workspaceType === 'existing'
|
||||||
|
? t('projectWizard.step3.existingInfo')
|
||||||
|
: githubUrl
|
||||||
|
? t('projectWizard.step3.newWithClone')
|
||||||
|
: t('projectWizard.step3.newEmpty')}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -596,7 +672,7 @@ const ProjectCreationWizard = ({ onClose, onProjectCreated }) => {
|
|||||||
{isCreating ? (
|
{isCreating ? (
|
||||||
<>
|
<>
|
||||||
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
||||||
{t('projectWizard.buttons.creating')}
|
{githubUrl ? t('projectWizard.buttons.cloning', 'Cloning...') : t('projectWizard.buttons.creating')}
|
||||||
</>
|
</>
|
||||||
) : step === 3 ? (
|
) : step === 3 ? (
|
||||||
<>
|
<>
|
||||||
@@ -639,6 +715,17 @@ const ProjectCreationWizard = ({ onClose, onProjectCreated }) => {
|
|||||||
>
|
>
|
||||||
{showHiddenFolders ? <Eye className="w-5 h-5" /> : <EyeOff className="w-5 h-5" />}
|
{showHiddenFolders ? <Eye className="w-5 h-5" /> : <EyeOff className="w-5 h-5" />}
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setShowNewFolderInput(!showNewFolderInput)}
|
||||||
|
className={`p-2 rounded-md transition-colors ${
|
||||||
|
showNewFolderInput
|
||||||
|
? 'text-blue-600 dark:text-blue-400 bg-blue-50 dark:bg-blue-900/30'
|
||||||
|
: 'text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700'
|
||||||
|
}`}
|
||||||
|
title="Create new folder"
|
||||||
|
>
|
||||||
|
<Plus className="w-5 h-5" />
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => setShowFolderBrowser(false)}
|
onClick={() => setShowFolderBrowser(false)}
|
||||||
className="p-2 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 rounded-md hover:bg-gray-100 dark:hover:bg-gray-700"
|
className="p-2 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 rounded-md hover:bg-gray-100 dark:hover:bg-gray-700"
|
||||||
@@ -648,6 +735,46 @@ const ProjectCreationWizard = ({ onClose, onProjectCreated }) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* New Folder Input */}
|
||||||
|
{showNewFolderInput && (
|
||||||
|
<div className="px-4 py-3 border-b border-gray-200 dark:border-gray-700 bg-blue-50 dark:bg-blue-900/20">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
value={newFolderName}
|
||||||
|
onChange={(e) => setNewFolderName(e.target.value)}
|
||||||
|
placeholder="New folder name"
|
||||||
|
className="flex-1"
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === 'Enter') createNewFolder();
|
||||||
|
if (e.key === 'Escape') {
|
||||||
|
setShowNewFolderInput(false);
|
||||||
|
setNewFolderName('');
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
onClick={createNewFolder}
|
||||||
|
disabled={!newFolderName.trim() || creatingFolder}
|
||||||
|
>
|
||||||
|
{creatingFolder ? <Loader2 className="w-4 h-4 animate-spin" /> : 'Create'}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
onClick={() => {
|
||||||
|
setShowNewFolderInput(false);
|
||||||
|
setNewFolderName('');
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Folder List */}
|
{/* Folder List */}
|
||||||
<div className="flex-1 overflow-y-auto p-4">
|
<div className="flex-1 overflow-y-auto p-4">
|
||||||
{loadingFolders ? (
|
{loadingFolders ? (
|
||||||
@@ -699,7 +826,7 @@ const ProjectCreationWizard = ({ onClose, onProjectCreated }) => {
|
|||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => selectFolder(folder.path, true)}
|
onClick={() => selectFolder(folder.path, workspaceType === 'existing')}
|
||||||
className="text-xs px-3"
|
className="text-xs px-3"
|
||||||
>
|
>
|
||||||
Select
|
Select
|
||||||
@@ -722,13 +849,17 @@ const ProjectCreationWizard = ({ onClose, onProjectCreated }) => {
|
|||||||
<div className="flex items-center justify-end gap-2 p-4">
|
<div className="flex items-center justify-end gap-2 p-4">
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => setShowFolderBrowser(false)}
|
onClick={() => {
|
||||||
|
setShowFolderBrowser(false);
|
||||||
|
setShowNewFolderInput(false);
|
||||||
|
setNewFolderName('');
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => selectFolder(browserCurrentPath, true)}
|
onClick={() => selectFolder(browserCurrentPath, workspaceType === 'existing')}
|
||||||
>
|
>
|
||||||
Use this folder
|
Use this folder
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -158,6 +158,12 @@ export const api = {
|
|||||||
return authenticatedFetch(`/api/browse-filesystem?${params}`);
|
return authenticatedFetch(`/api/browse-filesystem?${params}`);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
createFolder: (folderPath) =>
|
||||||
|
authenticatedFetch('/api/create-folder', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({ path: folderPath }),
|
||||||
|
}),
|
||||||
|
|
||||||
// User endpoints
|
// User endpoints
|
||||||
user: {
|
user: {
|
||||||
gitConfig: () => authenticatedFetch('/api/user/git-config'),
|
gitConfig: () => authenticatedFetch('/api/user/git-config'),
|
||||||
|
|||||||
Reference in New Issue
Block a user