Botão Commit & Push nos projetos e correção do resume de sessão

- Adicionar botão de commit & push para cada projeto na página de arquivos
- Criar rota POST /api/files/commit-push com git add, commit e push
- Adicionar Modal.prompt reutilizável para inputs com valor padrão
- Corrigir detecção de erro no executor (is_error/errors do CLI)
- Fallback automático para nova execução quando sessão expira no resume
This commit is contained in:
Frederico Castro
2026-02-28 08:55:39 -03:00
parent 87062c288e
commit 356411d388
9 changed files with 204 additions and 33 deletions

View File

@@ -172,6 +172,7 @@ const API = {
list(path) { return API.request('GET', `/files${path ? '?path=' + encodeURIComponent(path) : ''}`); },
delete(path) { return API.request('DELETE', `/files?path=${encodeURIComponent(path)}`); },
publish(path) { return API.request('POST', '/files/publish', { path }); },
commitPush(path, message) { return API.request('POST', '/files/commit-push', { path, message }); },
},
reports: {

View File

@@ -783,6 +783,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 'commit-push': FilesUI.commitPush(path); break;
case 'publish-project': FilesUI.publishProject(path); break;
case 'delete-entry': FilesUI.deleteEntry(path, el.dataset.entryType); break;
}

View File

@@ -91,11 +91,14 @@ const FilesUI = {
? `<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 isRootDir = entry.type === 'directory' && !currentPath;
const commitPushBtn = isRootDir
? `<button class="btn btn--ghost btn--sm btn-commit-push" data-action="commit-push" data-path="${Utils.escapeHtml(fullPath)}" title="Commit & Push para o Gitea"><i data-lucide="git-commit-horizontal" style="width:14px;height:14px"></i></button>`
: '';
const publishBtn = isRootDir
? `<button class="btn btn--ghost btn--sm btn-publish" data-action="publish-project" data-path="${Utils.escapeHtml(fullPath)}" title="Publicar projeto"><i data-lucide="rocket" 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}${publishBtn}${deleteBtn}`;
const actions = `${downloadBtn}${commitPushBtn}${publishBtn}${deleteBtn}`;
return `
<tr class="files-row">
@@ -188,6 +191,29 @@ const FilesUI = {
}
},
async commitPush(path) {
const name = path.split('/').pop();
const message = await Modal.prompt(
'Commit & Push',
`Mensagem do commit para <strong>${name}</strong>:`,
`Atualização - ${new Date().toLocaleDateString('pt-BR')} ${new Date().toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit' })}`
);
if (message === null) return;
try {
Toast.info('Realizando commit e push...');
const result = await API.files.commitPush(path, message || undefined);
if (result.status === 'clean') {
Toast.info(result.message);
} else {
Toast.success(`${result.changes} arquivo(s) enviados ao Gitea`);
}
} catch (err) {
Toast.error(`Erro no commit/push: ${err.message}`);
}
},
async deleteEntry(path, entryType) {
const label = entryType === 'directory' ? 'pasta' : 'arquivo';
const name = path.split('/').pop();

View File

@@ -58,6 +58,33 @@ const Modal = {
}
},
_promptResolve: null,
prompt(title, message, defaultValue = '') {
return new Promise((resolve) => {
Modal._promptResolve = resolve;
const titleEl = document.getElementById('prompt-modal-title');
const messageEl = document.getElementById('prompt-modal-message');
const inputEl = document.getElementById('prompt-modal-input');
if (titleEl) titleEl.textContent = title;
if (messageEl) messageEl.innerHTML = message;
if (inputEl) inputEl.value = defaultValue;
Modal.open('prompt-modal-overlay');
});
},
_resolvePrompt(result) {
const inputEl = document.getElementById('prompt-modal-input');
Modal.close('prompt-modal-overlay');
if (Modal._promptResolve) {
Modal._promptResolve(result ? (inputEl?.value || '') : null);
Modal._promptResolve = null;
}
},
_setupListeners() {
document.addEventListener('click', (e) => {
if (e.target.classList.contains('modal-overlay')) {
@@ -65,6 +92,8 @@ const Modal = {
if (modalId === 'confirm-modal-overlay') {
Modal._resolveConfirm(false);
} else if (modalId === 'prompt-modal-overlay') {
Modal._resolvePrompt(false);
} else {
Modal.close(modalId);
}
@@ -77,6 +106,8 @@ const Modal = {
if (targetId === 'confirm-modal-overlay') {
Modal._resolveConfirm(false);
} else if (targetId === 'prompt-modal-overlay') {
Modal._resolvePrompt(false);
} else {
Modal.close(targetId);
}
@@ -91,6 +122,8 @@ const Modal = {
if (activeModal.id === 'confirm-modal-overlay') {
Modal._resolveConfirm(false);
} else if (activeModal.id === 'prompt-modal-overlay') {
Modal._resolvePrompt(false);
} else {
Modal.close(activeModal.id);
}
@@ -98,6 +131,17 @@ const Modal = {
const confirmBtn = document.getElementById('confirm-modal-confirm-btn');
if (confirmBtn) confirmBtn.addEventListener('click', () => Modal._resolveConfirm(true));
const promptConfirmBtn = document.getElementById('prompt-modal-confirm-btn');
if (promptConfirmBtn) promptConfirmBtn.addEventListener('click', () => Modal._resolvePrompt(true));
const promptCancelBtn = document.getElementById('prompt-modal-cancel-btn');
if (promptCancelBtn) promptCancelBtn.addEventListener('click', () => Modal._resolvePrompt(false));
const promptInput = document.getElementById('prompt-modal-input');
if (promptInput) promptInput.addEventListener('keydown', (e) => {
if (e.key === 'Enter') Modal._resolvePrompt(true);
});
},
};