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:
Mahmoud-Emad
2025-08-24 13:40:51 +03:00
parent cc93081b15
commit d6ea18e6db
5 changed files with 1603 additions and 272 deletions

View File

@@ -431,7 +431,7 @@ fn (mut wsp Workspace) save() !&Workspace {
}
// Generate a random name for the workspace
fn generate_random_workspace_name() string {
pub fn generate_random_workspace_name() string {
adjectives := [
'brave',
'bright',

View File

@@ -30,7 +30,7 @@ pub fn (app &App) api_heroprompt_list(mut ctx Context) veb.Result {
@['/api/heroprompt/workspaces'; post]
pub fn (app &App) api_heroprompt_create(mut ctx Context) veb.Result {
name := ctx.form['name'] or { 'default' }
name_input := ctx.form['name'] or { '' }
base_path_in := ctx.form['base_path'] or { '' }
if base_path_in.len == 0 {
return ctx.text('{"error":"base_path required"}')
@@ -40,13 +40,20 @@ pub fn (app &App) api_heroprompt_create(mut ctx Context) veb.Result {
home := os.home_dir()
base_path = os.join_path(home, base_path.all_after('~'))
}
_ := hp.get(name: name, create: true, path: base_path) or {
// If no name provided, generate a random name
mut name := name_input.trim(' \t\n\r')
if name.len == 0 {
name = hp.generate_random_workspace_name()
}
wsp := hp.get(name: name, create: true, path: base_path) or {
return ctx.text('{"error":"create failed"}')
}
ctx.set_content_type('application/json')
return ctx.text(json.encode({
'name': name
'base_path': base_path
'name': wsp.name
'base_path': wsp.base_path
}))
}
@@ -63,6 +70,61 @@ pub fn (app &App) api_heroprompt_get(mut ctx Context, name string) veb.Result {
}))
}
@['/api/heroprompt/workspaces/:name'; put]
pub fn (app &App) api_heroprompt_update(mut ctx Context, name string) veb.Result {
wsp := hp.get(name: name, create: false) or {
return ctx.text('{"error":"workspace not found"}')
}
new_name := ctx.form['name'] or { name }
new_base_path_in := ctx.form['base_path'] or { wsp.base_path }
mut new_base_path := new_base_path_in
if new_base_path.starts_with('~') {
home := os.home_dir()
new_base_path = os.join_path(home, new_base_path.all_after('~'))
}
// Update the workspace using the update_workspace method
updated_wsp := wsp.update_workspace(
name: new_name
base_path: new_base_path
) or { return ctx.text('{"error":"failed to update workspace"}') }
ctx.set_content_type('application/json')
return ctx.text(json.encode({
'name': updated_wsp.name
'base_path': updated_wsp.base_path
}))
}
@['/api/heroprompt/workspaces/:name'; delete]
pub fn (app &App) api_heroprompt_delete(mut ctx Context, name string) veb.Result {
wsp := hp.get(name: name, create: false) or {
return ctx.text('{"error":"workspace not found"}')
}
// Delete the workspace
wsp.delete_workspace() or { return ctx.text('{"error":"failed to delete workspace"}') }
ctx.set_content_type('application/json')
return ctx.text('{"ok":true}')
}
// Alternative delete endpoint using POST with action parameter
@['/api/heroprompt/workspaces/:name/delete'; post]
pub fn (app &App) api_heroprompt_delete_alt(mut ctx Context, name string) veb.Result {
wsp := hp.get(name: name, create: false) or {
return ctx.text('{"error":"workspace not found"}')
}
// Delete the workspace
wsp.delete_workspace() or { return ctx.text('{"error":"failed to delete workspace"}') }
ctx.set_content_type('application/json')
return ctx.text('{"ok":true}')
}
@['/api/heroprompt/directory'; get]
pub fn (app &App) api_heroprompt_directory(mut ctx Context) veb.Result {
wsname := ctx.query['name'] or { 'default' }

File diff suppressed because it is too large Load Diff

View File

@@ -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');
});
});
}

View File

@@ -35,12 +35,9 @@
<div class="d-flex align-items-center mb-3">
<h5 class="mb-0">Heroprompt</h5>
<div class="ms-auto">
<button id="wsCreateBtn" class="btn btn-primary btn-sm me-2">
<button id="wsCreateBtn" class="btn btn-primary btn-sm">
+ New Workspace
</button>
<button id="refreshWs" class="btn btn-outline-secondary btn-sm">
↻ Refresh
</button>
</div>
</div>
@@ -52,19 +49,13 @@
<div class="d-flex justify-content-between align-items-center mb-2">
<h6 class="mb-0 text-uppercase fw-bold explorer-title">Explorer</h6>
<div class="explorer-actions">
<button id="collapseAll" class="btn btn-sm btn-ghost me-1" title="Collapse All">
<i class="icon-collapse"></i>
</button>
<button id="refreshExplorer" class="btn btn-sm btn-ghost me-1" title="Refresh">
<button id="refreshExplorer" class="btn btn-lg btn-ghost me-2"
title="Refresh Workspace">
<i class="icon-refresh"></i>
</button>
<button id="wsDetailsBtn" class="btn btn-sm btn-ghost me-1"
title="Workspace Settings">
<button id="wsDetailsBtn" class="btn btn-lg btn-ghost" title="Workspace Details">
<i class="icon-settings"></i>
</button>
<button id="openWsManage" class="btn btn-sm btn-ghost" title="Manage Workspaces">
<i class="icon-manage"></i>
</button>
</div>
</div>
@@ -95,13 +86,6 @@
</span>
</div>
<div class="selection-actions">
<button id="selectAll" class="btn btn-xs btn-ghost" title="Select All Visible">
Select All
</button>
<button id="clearSelection" class="btn btn-xs btn-ghost"
title="Clear Selection">
Clear
</button>
</div>
</div>
</div>
@@ -151,52 +135,19 @@
<div class="selection-workspace">
<div class="selection-header">
<h6 class="section-title">Selected Files & Directories</h6>
<div class="selection-actions">
<button id="exportSelection" class="btn btn-sm btn-ghost"
title="Export Selection">
<i class="icon-export"></i>
</button>
<button id="importSelection" class="btn btn-sm btn-ghost"
title="Import Selection">
<i class="icon-import"></i>
</button>
</div>
</div>
<div class="selection-content">
<div class="selection-list-panel">
<div class="selection-cards-container">
<div class="panel-header">
<span class="panel-title">Selected Items</span>
<button id="clearAllSelection" class="btn btn-xs btn-ghost">Clear
All</button>
<span class="panel-title">Selected Files & Directories</span>
</div>
<div class="selection-list">
<ul id="selected" class="selected-items">
<li class="empty-selection">
<i class="icon-empty"></i>
<p>No files selected</p>
<small>Use checkboxes in the explorer to select files</small>
</li>
</ul>
</div>
</div>
<div class="preview-panel">
<div class="panel-header">
<span class="panel-title">File Preview</span>
<div class="preview-actions">
<button id="copyPreview" class="btn btn-xs btn-ghost"
title="Copy Content">
<i class="icon-copy"></i>
</button>
</div>
</div>
<div class="preview-content">
<div id="preview" class="file-preview">
<div class="empty-preview">
<i class="icon-file"></i>
<p>Select a file to preview</p>
</div>
<div id="selectedCards" class="selection-cards">
<div class="empty-selection-cards">
<i class="icon-empty"></i>
<p>No files selected</p>
<small>Use checkboxes in the explorer to select files and
directories</small>
</div>
</div>
</div>
@@ -361,7 +312,7 @@ Example:
<button type="button" class="btn btn-danger" id="wdDelete">Delete</button>
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal"
id="wdCancel">Cancel</button>
<button type="button" class="btn btn-primary" id="wdSave">Save</button>
<button type="button" class="btn btn-primary" id="wdUpdate">Update</button>
</div>
</div>
</div>
@@ -384,6 +335,25 @@ Example:
</div>
</div>
<!-- Confirmation Modal -->
<div class="modal fade" id="confirmDeleteModal" tabindex="-1">
<div class="modal-dialog modal-sm">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Confirm Delete</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<p id="confirmDeleteMessage">Are you sure you want to delete this workspace?</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-danger" id="confirmDeleteBtn">Delete</button>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js"
integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM"
crossorigin="anonymous"></script>