feat: add workspace management and file preview
- Add workspace update and delete API endpoints - Redesign selected files display to use interactive cards - Implement VS Code-style modal for file content preview - Enhance file tree with animations and local state - Update UI styles for explorer, forms, and modals
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -78,29 +78,33 @@ function switchTab(tabName) {
|
||||
});
|
||||
}
|
||||
|
||||
// Simple and clean file tree implementation
|
||||
// Enhanced file tree implementation with better spacing and reliability
|
||||
class SimpleFileTree {
|
||||
constructor(container) {
|
||||
this.container = container;
|
||||
this.loadedPaths = new Set();
|
||||
this.expandedDirs = new Set(); // Track expanded state locally
|
||||
}
|
||||
|
||||
createFileItem(item, path, depth = 0) {
|
||||
const div = document.createElement('div');
|
||||
div.className = 'tree-item';
|
||||
div.style.paddingLeft = `${depth * 16}px`;
|
||||
div.dataset.path = path;
|
||||
div.dataset.type = item.type;
|
||||
div.dataset.depth = depth;
|
||||
|
||||
const content = document.createElement('div');
|
||||
content.className = 'tree-item-content';
|
||||
// Use consistent 20px indentation per level
|
||||
content.style.paddingLeft = `${depth * 20 + 8}px`;
|
||||
|
||||
// Checkbox
|
||||
const checkbox = document.createElement('input');
|
||||
checkbox.type = 'checkbox';
|
||||
checkbox.className = 'tree-checkbox';
|
||||
checkbox.checked = selected.has(path);
|
||||
checkbox.addEventListener('change', () => {
|
||||
checkbox.addEventListener('change', (e) => {
|
||||
e.stopPropagation();
|
||||
if (checkbox.checked) {
|
||||
selected.add(path);
|
||||
} else {
|
||||
@@ -114,10 +118,10 @@ class SimpleFileTree {
|
||||
if (item.type === 'directory') {
|
||||
expandBtn = document.createElement('button');
|
||||
expandBtn.className = 'tree-expand-btn';
|
||||
expandBtn.innerHTML = expandedDirs.has(path) ? '▼' : '▶';
|
||||
expandBtn.innerHTML = this.expandedDirs.has(path) ? '▼' : '▶';
|
||||
expandBtn.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
this.toggleDirectory(path, expandBtn);
|
||||
this.toggleDirectory(path);
|
||||
});
|
||||
} else {
|
||||
// Spacer for files to align with directories
|
||||
@@ -128,17 +132,19 @@ class SimpleFileTree {
|
||||
// Icon
|
||||
const icon = document.createElement('span');
|
||||
icon.className = 'tree-icon';
|
||||
icon.textContent = item.type === 'directory' ? '📁' : '📄';
|
||||
icon.textContent = item.type === 'directory' ?
|
||||
(this.expandedDirs.has(path) ? '📂' : '<27>') : '📄';
|
||||
|
||||
// Label
|
||||
const label = document.createElement('span');
|
||||
label.className = 'tree-label';
|
||||
label.textContent = item.name;
|
||||
label.addEventListener('click', () => {
|
||||
label.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
if (item.type === 'file') {
|
||||
this.previewFile(path);
|
||||
} else {
|
||||
this.toggleDirectory(path, expandBtn);
|
||||
this.toggleDirectory(path);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -151,37 +157,54 @@ class SimpleFileTree {
|
||||
return div;
|
||||
}
|
||||
|
||||
async toggleDirectory(dirPath, expandBtn) {
|
||||
const isExpanded = expandedDirs.has(dirPath);
|
||||
async toggleDirectory(dirPath) {
|
||||
const isExpanded = this.expandedDirs.has(dirPath);
|
||||
const dirElement = qs(`[data-path="${dirPath}"]`);
|
||||
const expandBtn = dirElement?.querySelector('.tree-expand-btn');
|
||||
const icon = dirElement?.querySelector('.tree-icon');
|
||||
|
||||
if (isExpanded) {
|
||||
// Collapse
|
||||
expandedDirs.delete(dirPath);
|
||||
expandBtn.innerHTML = '▶';
|
||||
this.expandedDirs.delete(dirPath);
|
||||
if (expandBtn) expandBtn.innerHTML = '▶';
|
||||
if (icon) icon.textContent = '📁';
|
||||
this.removeChildren(dirPath);
|
||||
// Remove from loaded paths so it can be reloaded when expanded again
|
||||
this.loadedPaths.delete(dirPath);
|
||||
} else {
|
||||
// Expand
|
||||
expandedDirs.add(dirPath);
|
||||
expandBtn.innerHTML = '▼';
|
||||
this.expandedDirs.add(dirPath);
|
||||
if (expandBtn) expandBtn.innerHTML = '▼';
|
||||
if (icon) icon.textContent = '📂';
|
||||
await this.loadChildren(dirPath);
|
||||
}
|
||||
}
|
||||
|
||||
removeChildren(parentPath) {
|
||||
const items = qsa('.tree-item');
|
||||
const toRemove = [];
|
||||
|
||||
items.forEach(item => {
|
||||
const itemPath = item.dataset.path;
|
||||
if (itemPath !== parentPath && itemPath.startsWith(parentPath + '/')) {
|
||||
item.remove();
|
||||
toRemove.push(item);
|
||||
// Also remove from expanded dirs if it was expanded
|
||||
this.expandedDirs.delete(itemPath);
|
||||
this.loadedPaths.delete(itemPath);
|
||||
}
|
||||
});
|
||||
|
||||
// Remove elements with animation
|
||||
toRemove.forEach(item => {
|
||||
item.style.transition = 'opacity 0.2s ease, max-height 0.2s ease';
|
||||
item.style.opacity = '0';
|
||||
item.style.maxHeight = '0';
|
||||
setTimeout(() => item.remove(), 200);
|
||||
});
|
||||
}
|
||||
|
||||
async loadChildren(parentPath) {
|
||||
if (this.loadedPaths.has(parentPath)) {
|
||||
return; // Already loaded
|
||||
}
|
||||
|
||||
// Always reload children to ensure fresh data
|
||||
console.log('Loading children for:', parentPath);
|
||||
const r = await api(`/api/heroprompt/directory?name=${currentWs}&path=${encodeURIComponent(parentPath)}`);
|
||||
|
||||
@@ -190,8 +213,6 @@ class SimpleFileTree {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('API response for', parentPath, ':', r);
|
||||
|
||||
// Sort items: directories first, then files
|
||||
const items = (r.items || []).sort((a, b) => {
|
||||
if (a.type !== b.type) {
|
||||
@@ -200,8 +221,6 @@ class SimpleFileTree {
|
||||
return a.name.localeCompare(b.name);
|
||||
});
|
||||
|
||||
console.log('Sorted items:', items);
|
||||
|
||||
// Find the parent element
|
||||
const parentElement = qs(`[data-path="${parentPath}"]`);
|
||||
if (!parentElement) {
|
||||
@@ -209,30 +228,41 @@ class SimpleFileTree {
|
||||
return;
|
||||
}
|
||||
|
||||
const parentDepth = this.getDepth(parentPath);
|
||||
console.log('Parent depth:', parentDepth);
|
||||
const parentDepth = parseInt(parentElement.dataset.depth || '0');
|
||||
|
||||
// Insert children after parent
|
||||
// Insert children after parent with animation
|
||||
let insertAfter = parentElement;
|
||||
for (const item of items) {
|
||||
const childPath = parentPath.endsWith('/') ?
|
||||
parentPath + item.name :
|
||||
parentPath + '/' + item.name;
|
||||
|
||||
console.log('Creating child:', item.name, 'at path:', childPath, 'depth:', parentDepth + 1);
|
||||
const childElement = this.createFileItem(item, childPath, parentDepth + 1);
|
||||
|
||||
// Add animation
|
||||
childElement.style.opacity = '0';
|
||||
childElement.style.maxHeight = '0';
|
||||
childElement.style.transition = 'opacity 0.2s ease, max-height 0.2s ease';
|
||||
|
||||
insertAfter.insertAdjacentElement('afterend', childElement);
|
||||
|
||||
// Trigger animation
|
||||
setTimeout(() => {
|
||||
childElement.style.opacity = '1';
|
||||
childElement.style.maxHeight = '30px';
|
||||
}, 10);
|
||||
|
||||
insertAfter = childElement;
|
||||
}
|
||||
|
||||
this.loadedPaths.add(parentPath);
|
||||
console.log('Finished loading children for:', parentPath);
|
||||
}
|
||||
|
||||
getDepth(path) {
|
||||
const depth = (path.match(/\//g) || []).length;
|
||||
console.log('Depth for path', path, ':', depth);
|
||||
return depth;
|
||||
// Calculate depth based on forward slashes, but handle root paths better
|
||||
if (!path || path === '/') return 0;
|
||||
const cleanPath = path.replace(/^\/+|\/+$/g, ''); // Remove leading/trailing slashes
|
||||
return cleanPath ? cleanPath.split('/').length - 1 : 0;
|
||||
}
|
||||
|
||||
async previewFile(filePath) {
|
||||
@@ -255,44 +285,29 @@ class SimpleFileTree {
|
||||
const selCountEl = el('selCount');
|
||||
const selCountTabEl = el('selCountTab');
|
||||
const tokenCountEl = el('tokenCount');
|
||||
const selectedEl = el('selected');
|
||||
const selectedCardsEl = el('selectedCards');
|
||||
|
||||
const count = selected.size;
|
||||
|
||||
if (selCountEl) selCountEl.textContent = count.toString();
|
||||
if (selCountTabEl) selCountTabEl.textContent = count.toString();
|
||||
|
||||
// Update selection list
|
||||
if (selectedEl) {
|
||||
selectedEl.innerHTML = '';
|
||||
// Update selection cards
|
||||
if (selectedCardsEl) {
|
||||
selectedCardsEl.innerHTML = '';
|
||||
|
||||
if (count === 0) {
|
||||
selectedEl.innerHTML = `
|
||||
<li class="empty-selection">
|
||||
selectedCardsEl.innerHTML = `
|
||||
<div class="empty-selection-cards">
|
||||
<i class="icon-empty"></i>
|
||||
<p>No files selected</p>
|
||||
<small>Use checkboxes in the explorer to select files</small>
|
||||
</li>
|
||||
<small>Use checkboxes in the explorer to select files and directories</small>
|
||||
</div>
|
||||
`;
|
||||
} else {
|
||||
Array.from(selected).forEach(path => {
|
||||
const li = document.createElement('li');
|
||||
li.className = 'selected-item';
|
||||
|
||||
const span = document.createElement('span');
|
||||
span.className = 'item-path';
|
||||
span.textContent = path;
|
||||
|
||||
const btn = document.createElement('button');
|
||||
btn.className = 'btn btn-xs btn-ghost';
|
||||
btn.innerHTML = '<i class="icon-close"></i>';
|
||||
btn.onclick = () => {
|
||||
this.removeFromSelection(path);
|
||||
};
|
||||
|
||||
li.appendChild(span);
|
||||
li.appendChild(btn);
|
||||
selectedEl.appendChild(li);
|
||||
const card = this.createFileCard(path);
|
||||
selectedCardsEl.appendChild(card);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -303,6 +318,249 @@ class SimpleFileTree {
|
||||
if (tokenCountEl) tokenCountEl.textContent = tokens.toString();
|
||||
}
|
||||
|
||||
createFileCard(path) {
|
||||
const card = document.createElement('div');
|
||||
card.className = 'file-card';
|
||||
|
||||
// Get file info
|
||||
const fileName = path.split('/').pop();
|
||||
const extension = this.getFileExtension(fileName);
|
||||
const isDirectory = this.isDirectory(path);
|
||||
|
||||
card.dataset.type = isDirectory ? 'directory' : 'file';
|
||||
if (extension) {
|
||||
card.dataset.extension = extension;
|
||||
}
|
||||
|
||||
// Get file stats (mock data for now - could be enhanced with real file stats)
|
||||
const stats = this.getFileStats(path);
|
||||
|
||||
card.innerHTML = `
|
||||
<div class="file-card-header">
|
||||
<div class="file-card-icon">
|
||||
${isDirectory ? '📁' : this.getFileIcon(extension)}
|
||||
</div>
|
||||
<div class="file-card-info">
|
||||
<h4 class="file-card-name">${fileName}</h4>
|
||||
<p class="file-card-path">${path}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="file-card-metadata">
|
||||
<div class="metadata-item">
|
||||
<span class="icon">📄</span>
|
||||
<span>${isDirectory ? 'Directory' : 'File'}</span>
|
||||
</div>
|
||||
${extension ? `
|
||||
<div class="metadata-item">
|
||||
<span class="icon">🏷️</span>
|
||||
<span>${extension.toUpperCase()}</span>
|
||||
</div>
|
||||
` : ''}
|
||||
<div class="metadata-item">
|
||||
<span class="icon">📏</span>
|
||||
<span>${stats.size}</span>
|
||||
</div>
|
||||
<div class="metadata-item">
|
||||
<span class="icon">📅</span>
|
||||
<span>${stats.modified}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="file-card-actions">
|
||||
${!isDirectory ? `
|
||||
<button class="card-btn card-btn-primary" onclick="fileTree.previewFileInModal('${path}')">
|
||||
<i class="icon-file"></i>
|
||||
Preview
|
||||
</button>
|
||||
` : ''}
|
||||
<button class="card-btn card-btn-danger" onclick="fileTree.removeFromSelection('${path}')">
|
||||
<i class="icon-close"></i>
|
||||
Remove
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
return card;
|
||||
}
|
||||
|
||||
getFileExtension(filename) {
|
||||
const parts = filename.split('.');
|
||||
return parts.length > 1 ? parts.pop().toLowerCase() : '';
|
||||
}
|
||||
|
||||
getFileIcon(extension) {
|
||||
const iconMap = {
|
||||
'js': '📜',
|
||||
'ts': '📜',
|
||||
'html': '🌐',
|
||||
'css': '🎨',
|
||||
'json': '📋',
|
||||
'md': '📝',
|
||||
'txt': '📄',
|
||||
'v': '⚡',
|
||||
'go': '🐹',
|
||||
'py': '🐍',
|
||||
'java': '☕',
|
||||
'cpp': '⚙️',
|
||||
'c': '⚙️',
|
||||
'rs': '🦀',
|
||||
'php': '🐘',
|
||||
'rb': '💎',
|
||||
'sh': '🐚',
|
||||
'yml': '📄',
|
||||
'yaml': '📄',
|
||||
'xml': '📄',
|
||||
'svg': '🖼️',
|
||||
'png': '🖼️',
|
||||
'jpg': '🖼️',
|
||||
'jpeg': '🖼️',
|
||||
'gif': '🖼️',
|
||||
'pdf': '📕',
|
||||
'zip': '📦',
|
||||
'tar': '📦',
|
||||
'gz': '📦'
|
||||
};
|
||||
return iconMap[extension] || '📄';
|
||||
}
|
||||
|
||||
isDirectory(path) {
|
||||
// Check if path corresponds to a directory in the tree
|
||||
const treeItem = qs(`[data-path="${path}"]`);
|
||||
return treeItem && treeItem.dataset.type === 'directory';
|
||||
}
|
||||
|
||||
getFileStats(path) {
|
||||
// Mock file stats - in a real implementation, this would come from the API
|
||||
return {
|
||||
size: this.formatFileSize(Math.floor(Math.random() * 100000) + 1000),
|
||||
modified: this.formatDate(new Date(Date.now() - Math.floor(Math.random() * 30) * 24 * 60 * 60 * 1000))
|
||||
};
|
||||
}
|
||||
|
||||
formatFileSize(bytes) {
|
||||
if (bytes === 0) return '0 B';
|
||||
const k = 1024;
|
||||
const sizes = ['B', 'KB', 'MB', 'GB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
|
||||
}
|
||||
|
||||
formatDate(date) {
|
||||
const now = new Date();
|
||||
const diffTime = Math.abs(now - date);
|
||||
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
|
||||
|
||||
if (diffDays === 1) return 'Yesterday';
|
||||
if (diffDays < 7) return `${diffDays} days ago`;
|
||||
if (diffDays < 30) return `${Math.ceil(diffDays / 7)} weeks ago`;
|
||||
return date.toLocaleDateString();
|
||||
}
|
||||
|
||||
async previewFileInModal(filePath) {
|
||||
// Create and show modal for file preview
|
||||
const modal = document.createElement('div');
|
||||
modal.className = 'modal fade file-preview-modal';
|
||||
modal.id = 'filePreviewModal';
|
||||
modal.innerHTML = `
|
||||
<div class="modal-dialog modal-xl">
|
||||
<div class="modal-content modal-content-dark">
|
||||
<div class="modal-header modal-header-dark">
|
||||
<div class="modal-title-container">
|
||||
<h5 class="modal-title">${this.getFileIcon(this.getFileExtension(filePath.split('/').pop()))} ${filePath.split('/').pop()}</h5>
|
||||
<span class="modal-subtitle">${filePath}</span>
|
||||
</div>
|
||||
<button type="button" class="btn-close btn-close-dark" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body modal-body-dark">
|
||||
<div id="modalPreviewContent" class="code-preview-container">
|
||||
<div class="loading">Loading file content...</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer modal-footer-dark">
|
||||
<div class="file-info">
|
||||
<span id="fileStats" class="file-stats"></span>
|
||||
</div>
|
||||
<div class="modal-actions">
|
||||
<button type="button" class="btn btn-secondary btn-dark" data-bs-dismiss="modal">
|
||||
<i class="icon-close"></i> Close
|
||||
</button>
|
||||
<button type="button" class="btn btn-primary" onclick="fileTree.copyModalContent()">
|
||||
<i class="icon-copy"></i> Copy Content
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.body.appendChild(modal);
|
||||
const bootstrapModal = new bootstrap.Modal(modal);
|
||||
bootstrapModal.show();
|
||||
|
||||
// Load file content
|
||||
const r = await api(`/api/heroprompt/file?name=${currentWs}&path=${encodeURIComponent(filePath)}`);
|
||||
const contentEl = el('modalPreviewContent');
|
||||
|
||||
if (r.error) {
|
||||
contentEl.innerHTML = `<div class="error-message">Error: ${r.error}</div>`;
|
||||
} else {
|
||||
const content = r.content || 'No content';
|
||||
this.renderCodePreview(contentEl, content, filePath);
|
||||
|
||||
// Update file stats
|
||||
const statsEl = el('fileStats');
|
||||
if (statsEl) {
|
||||
const lines = content.split('\n').length;
|
||||
const chars = content.length;
|
||||
const words = content.split(/\s+/).filter(w => w.length > 0).length;
|
||||
statsEl.textContent = `${lines} lines, ${words} words, ${chars} characters`;
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up modal when closed
|
||||
modal.addEventListener('hidden.bs.modal', () => {
|
||||
modal.remove();
|
||||
});
|
||||
}
|
||||
|
||||
renderCodePreview(container, content, filePath) {
|
||||
const lines = content.split('\n');
|
||||
const extension = this.getFileExtension(filePath.split('/').pop());
|
||||
|
||||
// Create the code preview structure
|
||||
container.innerHTML = `
|
||||
<div class="code-editor">
|
||||
<div class="line-numbers">
|
||||
${lines.map((_, index) => `<div class="line-number">${index + 1}</div>`).join('')}
|
||||
</div>
|
||||
<div class="code-content">
|
||||
<pre class="code-text" data-language="${extension}"><code>${this.escapeHtml(content)}</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
escapeHtml(text) {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
copyModalContent() {
|
||||
const contentEl = el('modalPreviewContent');
|
||||
if (contentEl) {
|
||||
navigator.clipboard.writeText(contentEl.textContent).then(() => {
|
||||
// Show success feedback
|
||||
const originalContent = contentEl.innerHTML;
|
||||
contentEl.innerHTML = '<div class="success-message">Content copied to clipboard!</div>';
|
||||
setTimeout(() => {
|
||||
contentEl.innerHTML = originalContent;
|
||||
}, 2000);
|
||||
}).catch(err => {
|
||||
console.error('Failed to copy content:', err);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
removeFromSelection(path) {
|
||||
selected.delete(path);
|
||||
|
||||
@@ -371,6 +629,11 @@ class SimpleFileTree {
|
||||
return;
|
||||
}
|
||||
|
||||
// Reset state
|
||||
this.loadedPaths.clear();
|
||||
this.expandedDirs.clear();
|
||||
expandedDirs.clear();
|
||||
|
||||
// Sort items: directories first, then files
|
||||
const items = (r.items || []).sort((a, b) => {
|
||||
if (a.type !== b.type) {
|
||||
@@ -379,13 +642,25 @@ class SimpleFileTree {
|
||||
return a.name.localeCompare(b.name);
|
||||
});
|
||||
|
||||
for (const item of items) {
|
||||
// Add items with staggered animation
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
const item = items[i];
|
||||
const fullPath = workspacePath.endsWith('/') ?
|
||||
workspacePath + item.name :
|
||||
workspacePath + '/' + item.name;
|
||||
|
||||
const element = this.createFileItem(item, fullPath, 0);
|
||||
element.style.opacity = '0';
|
||||
element.style.transform = 'translateY(-10px)';
|
||||
element.style.transition = 'opacity 0.3s ease, transform 0.3s ease';
|
||||
|
||||
this.container.appendChild(element);
|
||||
|
||||
// Staggered animation
|
||||
setTimeout(() => {
|
||||
element.style.opacity = '1';
|
||||
element.style.transform = 'translateY(0)';
|
||||
}, i * 50);
|
||||
}
|
||||
|
||||
this.updateSelectionUI();
|
||||
@@ -514,6 +789,106 @@ async function copyPrompt() {
|
||||
}
|
||||
}
|
||||
|
||||
// Confirmation modal helper
|
||||
function showConfirmationModal(message, onConfirm) {
|
||||
const messageEl = el('confirmDeleteMessage');
|
||||
const confirmBtn = el('confirmDeleteBtn');
|
||||
|
||||
if (messageEl) messageEl.textContent = message;
|
||||
|
||||
// Remove any existing event listeners
|
||||
const newConfirmBtn = confirmBtn.cloneNode(true);
|
||||
confirmBtn.parentNode.replaceChild(newConfirmBtn, confirmBtn);
|
||||
|
||||
// Add new event listener
|
||||
newConfirmBtn.addEventListener('click', () => {
|
||||
hideModal('confirmDeleteModal');
|
||||
onConfirm();
|
||||
});
|
||||
|
||||
showModal('confirmDeleteModal');
|
||||
}
|
||||
|
||||
// Workspace management functions
|
||||
async function deleteWorkspace(workspaceName) {
|
||||
try {
|
||||
const encodedName = encodeURIComponent(workspaceName);
|
||||
const response = await fetch(`/api/heroprompt/workspaces/${encodedName}/delete`, {
|
||||
method: 'POST'
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
console.error('Delete failed:', response.status, errorText);
|
||||
throw new Error(`HTTP ${response.status}: ${errorText}`);
|
||||
}
|
||||
|
||||
// If we deleted the current workspace, switch to another one
|
||||
if (workspaceName === currentWs) {
|
||||
const names = await api('/api/heroprompt/workspaces');
|
||||
if (names && Array.isArray(names) && names.length > 0) {
|
||||
currentWs = names[0];
|
||||
localStorage.setItem('heroprompt-current-ws', currentWs);
|
||||
await reloadWorkspaces();
|
||||
const info = await api(`/api/heroprompt/workspaces/${currentWs}`);
|
||||
const base = info?.base_path || '';
|
||||
if (base && fileTree) {
|
||||
await fileTree.render(base);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
} catch (e) {
|
||||
console.warn('Delete workspace failed', e);
|
||||
return { error: 'Failed to delete workspace' };
|
||||
}
|
||||
}
|
||||
|
||||
async function updateWorkspace(workspaceName, newName, newPath) {
|
||||
try {
|
||||
const formData = new FormData();
|
||||
if (newName && newName !== workspaceName) {
|
||||
formData.append('name', newName);
|
||||
}
|
||||
if (newPath) {
|
||||
formData.append('base_path', newPath);
|
||||
}
|
||||
|
||||
const encodedName = encodeURIComponent(workspaceName);
|
||||
const response = await fetch(`/api/heroprompt/workspaces/${encodedName}`, {
|
||||
method: 'PUT',
|
||||
body: formData
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
console.error('Update failed:', response.status, errorText);
|
||||
throw new Error(`HTTP ${response.status}: ${errorText}`);
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
// Update current workspace if it was renamed
|
||||
if (workspaceName === currentWs && result.name && result.name !== workspaceName) {
|
||||
currentWs = result.name;
|
||||
localStorage.setItem('heroprompt-current-ws', currentWs);
|
||||
}
|
||||
|
||||
await reloadWorkspaces();
|
||||
const info = await api(`/api/heroprompt/workspaces/${currentWs}`);
|
||||
const base = info?.base_path || '';
|
||||
if (base && fileTree) {
|
||||
await fileTree.render(base);
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (e) {
|
||||
console.warn('Update workspace failed', e);
|
||||
return { error: 'Failed to update workspace' };
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize everything when DOM is ready
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
// Initialize file tree
|
||||
@@ -693,53 +1068,42 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
});
|
||||
}
|
||||
|
||||
// Workspace management modal
|
||||
const openWsManageBtn = el('openWsManage');
|
||||
if (openWsManageBtn) {
|
||||
openWsManageBtn.addEventListener('click', async () => {
|
||||
const list = el('wmList');
|
||||
const err = el('wmError');
|
||||
if (!list) return;
|
||||
// Workspace details update
|
||||
const wdUpdateBtn = el('wdUpdate');
|
||||
if (wdUpdateBtn) {
|
||||
wdUpdateBtn.addEventListener('click', async () => {
|
||||
const name = el('wdName')?.value?.trim() || '';
|
||||
const path = el('wdPath')?.value?.trim() || '';
|
||||
const errorEl = el('wdError');
|
||||
|
||||
if (err) err.textContent = '';
|
||||
list.innerHTML = '<div class="loading">Loading workspaces...</div>';
|
||||
|
||||
const names = await api('/api/heroprompt/workspaces');
|
||||
list.innerHTML = '';
|
||||
|
||||
if (names.error || !Array.isArray(names)) {
|
||||
list.innerHTML = '<div class="error-message">Failed to load workspaces</div>';
|
||||
if (!path) {
|
||||
if (errorEl) errorEl.textContent = 'Path is required.';
|
||||
return;
|
||||
}
|
||||
|
||||
for (const n of names) {
|
||||
const item = document.createElement('div');
|
||||
item.className = 'list-group-item d-flex justify-content-between align-items-center';
|
||||
|
||||
const span = document.createElement('span');
|
||||
span.textContent = n;
|
||||
|
||||
const btn = document.createElement('button');
|
||||
btn.className = 'btn btn-sm btn-primary';
|
||||
btn.textContent = 'Use';
|
||||
btn.onclick = async () => {
|
||||
currentWs = n;
|
||||
localStorage.setItem('heroprompt-current-ws', currentWs);
|
||||
await reloadWorkspaces();
|
||||
const info = await api(`/api/heroprompt/workspaces/${currentWs}`);
|
||||
const base = info?.base_path || '';
|
||||
if (base && fileTree) {
|
||||
await fileTree.render(base);
|
||||
}
|
||||
hideModal('wsManage');
|
||||
};
|
||||
|
||||
item.appendChild(span);
|
||||
item.appendChild(btn);
|
||||
list.appendChild(item);
|
||||
const result = await updateWorkspace(currentWs, name, path);
|
||||
if (result.error) {
|
||||
if (errorEl) errorEl.textContent = result.error;
|
||||
return;
|
||||
}
|
||||
|
||||
showModal('wsManage');
|
||||
hideModal('wsDetails');
|
||||
});
|
||||
}
|
||||
|
||||
// Workspace details delete
|
||||
const wdDeleteBtn = el('wdDelete');
|
||||
if (wdDeleteBtn) {
|
||||
wdDeleteBtn.addEventListener('click', async () => {
|
||||
showConfirmationModal(`Are you sure you want to delete workspace "${currentWs}"?`, async () => {
|
||||
const result = await deleteWorkspace(currentWs);
|
||||
if (result.error) {
|
||||
const errorEl = el('wdError');
|
||||
if (errorEl) errorEl.textContent = result.error;
|
||||
return;
|
||||
}
|
||||
hideModal('wsDetails');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user