Corrigir ícones e adicionar exclusão no explorador de arquivos

- Trocar ícone archive (lixeira) por download em todos os botões
- Adicionar botão de excluir com ícone trash-2 em cada entrada
- Rota DELETE /api/files com proteção contra exclusão da raiz
- Confirmação via modal antes de excluir
This commit is contained in:
Frederico Castro
2026-02-28 03:00:45 -03:00
parent 2fccaaac40
commit e9f65c2845
4 changed files with 46 additions and 4 deletions

View File

@@ -144,6 +144,7 @@ const API = {
files: {
list(path) { return API.request('GET', `/files${path ? '?path=' + encodeURIComponent(path) : ''}`); },
delete(path) { return API.request('DELETE', `/files?path=${encodeURIComponent(path)}`); },
},
reports: {

View File

@@ -774,6 +774,7 @@ const App = {
case 'navigate-files': FilesUI.navigate(path || ''); break;
case 'download-file': FilesUI.downloadFile(path); break;
case 'download-folder': FilesUI.downloadFolder(path); break;
case 'delete-entry': FilesUI.deleteEntry(path, el.dataset.entryType); break;
}
});

View File

@@ -40,7 +40,7 @@ const FilesUI = {
${breadcrumb}
<div class="files-toolbar">
<span class="files-count">${entries.length} ${entries.length === 1 ? 'item' : 'itens'}</span>
<button class="btn btn--ghost btn--sm" data-action="download-folder" data-path="${Utils.escapeHtml(data.path || '')}" title="Baixar pasta como .tar.gz"><i data-lucide="archive" style="width:14px;height:14px"></i> Baixar tudo</button>
<button class="btn btn--ghost btn--sm" data-action="download-folder" data-path="${Utils.escapeHtml(data.path || '')}" title="Baixar pasta como .tar.gz"><i data-lucide="download" style="width:14px;height:14px"></i> Baixar tudo</button>
</div>
<div class="files-table-wrapper">
<table class="files-table">
@@ -87,9 +87,11 @@ const FilesUI = {
? `<a href="#" class="files-entry-link files-entry-dir" data-action="navigate-files" data-path="${Utils.escapeHtml(fullPath)}"><i data-lucide="${icon}" style="width:16px;height:16px;color:${iconColor};flex-shrink:0"></i> ${Utils.escapeHtml(entry.name)}</a>`
: `<span class="files-entry-link files-entry-file"><i data-lucide="${icon}" style="width:16px;height:16px;color:${iconColor};flex-shrink:0"></i> ${Utils.escapeHtml(entry.name)}</span>`;
const actions = entry.type === 'directory'
? `<button class="btn btn--ghost btn--sm" data-action="download-folder" data-path="${Utils.escapeHtml(fullPath)}" title="Baixar pasta"><i data-lucide="archive" style="width:14px;height:14px"></i></button>`
const downloadBtn = entry.type === 'directory'
? `<button class="btn btn--ghost btn--sm" data-action="download-folder" data-path="${Utils.escapeHtml(fullPath)}" title="Baixar pasta"><i data-lucide="download" style="width:14px;height:14px"></i></button>`
: `<button class="btn btn--ghost btn--sm" data-action="download-file" data-path="${Utils.escapeHtml(fullPath)}" title="Baixar arquivo"><i data-lucide="download" style="width:14px;height:14px"></i></button>`;
const deleteBtn = `<button class="btn btn--ghost btn--sm btn-danger" data-action="delete-entry" data-path="${Utils.escapeHtml(fullPath)}" data-entry-type="${entry.type}" title="Excluir"><i data-lucide="trash-2" style="width:14px;height:14px"></i></button>`;
const actions = `${downloadBtn}${deleteBtn}`;
return `
<tr class="files-row">
@@ -147,6 +149,24 @@ const FilesUI = {
a.download = '';
a.click();
},
async deleteEntry(path, entryType) {
const label = entryType === 'directory' ? 'pasta' : 'arquivo';
const name = path.split('/').pop();
const confirmed = await Modal.confirm(
`Excluir ${label}`,
`Tem certeza que deseja excluir "${name}"? Esta ação não pode ser desfeita.`
);
if (!confirmed) return;
try {
await API.files.delete(path);
Toast.success(`${label.charAt(0).toUpperCase() + label.slice(1)} excluído`);
await FilesUI.navigate(FilesUI.currentPath);
} catch (err) {
Toast.error(`Erro ao excluir: ${err.message}`);
}
},
};
window.FilesUI = FilesUI;

View File

@@ -11,7 +11,7 @@ import * as pipeline from '../agents/pipeline.js';
import { getBinPath, updateMaxConcurrent, cancelAllExecutions, getActiveExecutions } from '../agents/executor.js';
import { invalidateAgentMapCache } from '../agents/pipeline.js';
import { cached } from '../cache/index.js';
import { readdirSync, readFileSync, unlinkSync, existsSync, mkdirSync, statSync, createReadStream } from 'fs';
import { readdirSync, readFileSync, unlinkSync, existsSync, mkdirSync, statSync, createReadStream, rmSync } from 'fs';
import { join, dirname, resolve as pathResolve, extname, basename, relative } from 'path';
import { createGzip } from 'zlib';
import { Readable } from 'stream';
@@ -1140,4 +1140,24 @@ router.get('/files/download-folder', (req, res) => {
}
});
router.delete('/files', (req, res) => {
try {
const targetPath = resolveProjectPath(req.query.path || '');
if (!targetPath) return res.status(400).json({ error: 'Caminho inválido' });
if (targetPath === PROJECTS_DIR) return res.status(400).json({ error: 'Não é permitido excluir o diretório raiz' });
if (!existsSync(targetPath)) return res.status(404).json({ error: 'Arquivo ou pasta não encontrado' });
const stat = statSync(targetPath);
if (stat.isDirectory()) {
rmSync(targetPath, { recursive: true, force: true });
} else {
unlinkSync(targetPath);
}
res.json({ success: true });
} catch (err) {
res.status(500).json({ error: err.message });
}
});
export default router;