A lista já tinha o botão de lixeira na coluna (DeleteFromList) desde o CRUD DELETE. Mas existe um segundo contexto natural pra excluir: quando o usuário já está com o registro aberto pra editar e decide, no meio da edição, que quer apagar em vez de salvar. Forçar ele a fechar o form e caçar o botão na lista é fricção desnecessária.
A solução: um botão "Excluir" dentro do form, visível só em modo edição, que dispara um swalConfirm e — se confirmado — exclui, fecha o modal e atualiza a lista. 5 mudanças pequenas em dois arquivos: DepartamentoProdutoForm (4 mudanças) e DepartamentoManager (1 mudança).
package br.xt.app.departamento.produto;
import br.jasap.core.Effect;
import br.jasap.effect.Response;
import br.jasap.gui.Bar;
import br.jasap.gui.Button;
import br.jasap.gui.JasapPage;
import br.jasap.gui.Table;
import br.jasap.gui.Toast;
import br.jasap.gui.form.Check;
import br.jasap.gui.form.Form;
import br.jasap.gui.form.Radio;
import br.jasap.gui.form.Text;
import br.jasap.gui.form.Textarea;
import br.jasap.util.DomainValue;
import br.jasap.util.JasapFunctions;
import br.jasap.util.JasapList;
import br.jasap.util.Js;
import br.jasap.util.exceptions.SQLConstraintException;
import br.jasap.util.filters.TransactionFilter;
import br.xt.acore.view.XtPage;
public class DepartamentoProdutoForm extends DepartamentoProdutoAction {
// ... execute(), ShowInsert, Insert, ShowUpdate, Update, Cancelar (sem alteração)
public static class Delete extends DepartamentoProdutoAction {
public Delete() { super.getFilters().add(new TransactionFilter()); }
@Override
public Effect execute() throws Exception {
try {
if (JasapFunctions.equals(getInput().getInteger(CONFIRM), 1)) {
proBean().setId_produto(id_produto());
getFactory().departamento().proModel().daoDelete(proBean());
eval(new Toast("Registro excluído.").success());
evalParent(Js.CLOSE_SUB_WINDOWS);
} else {
eval(Js.swalConfirm("Confirma a exclusão desse registro?",
link(Delete.class).putInteger(CONFIRM, 1).ajax(), ""));
}
return new Response();
} catch (SQLConstraintException e) {
eval(new Toast("Falha ao excluir: esse registro está sendo usado em outra tabela.").fail());
return new Response();
}
}
}
// ... render(), window() (sem alteração)
protected Bar br() throws Exception {
Button salvarInsert = ui().button(" Salvar ") /* ... */;
Button salvarUpdate = ui().button(" Salvar ") /* ... */;
Button cancelar = ui().button(" Cancelar ") /* ... */;
Button excluir = ui().button(" Excluir ").setCss("btn btn-danger btn-lg").setNoSize()
.setOnClick(link(Delete.class).ajax())
.setVisible(isUpdate(FORM));
Bar bar = ui().bar();
bar.addLeft(excluir);
bar.addRight(cancelar);
bar.addRight(" ");
bar.addRight(salvarInsert);
bar.addRight(salvarUpdate);
return bar;
}
// ... form() e getters dos campos (sem alteração)
public static String FORM = ROOT.concat("__FORM");
public static String CONFIRM = ROOT.concat("__CONFIRM");
}
Mudanças neste arquivo: 2 imports novos (Toast, SQLConstraintException), botão excluir no br() + bar.addLeft(excluir), inner class Delete, e constante CONFIRM. Nenhuma mudança nos métodos existentes.
package br.xt.app.departamento;
import br.xt.app.departamento.produto.DepartamentoProdutoForm;
import br.xt.app.departamento.produto.DepartamentoProdutoList;
import br.xt.app.painel.PnlManager;
public class DepartamentoManager extends PnlManager {
public static final String F_ACESSO_MODULO = "XT.PAINEL_CONTROLE.ACESSO_MODULO.DEPARTAMENTO";
@Override
public void config() throws Exception {
regFun("PAINEL DE CONTROLE", "Acesso ao Módulo", "DEPARTAMENTO", F_ACESSO_MODULO);
regAction(DepartamentoHome.class);
regAction(DepartamentoHome.Title.class);
regAction(DepartamentoHome.MenuItem.class);
regAction(DepartamentoHome.MenuInicial.class);
regAction(DepartamentoProdutoList.class);
regAction(DepartamentoProdutoList.Sort.class);
regAction(DepartamentoProdutoList.QuickSearch.class);
regAction(DepartamentoProdutoList.TabAtivos.class);
regAction(DepartamentoProdutoList.TabInativos.class);
regAction(DepartamentoProdutoList.TabArquivados.class);
regAction(DepartamentoProdutoList.TabTodos.class);
regAction(DepartamentoProdutoList.DeleteFromList.class);
regAction(DepartamentoProdutoForm.class);
regAction(DepartamentoProdutoForm.ShowInsert.class);
regAction(DepartamentoProdutoForm.ShowUpdate.class);
regAction(DepartamentoProdutoForm.Insert.class);
regAction(DepartamentoProdutoForm.Update.class);
regAction(DepartamentoProdutoForm.Cancelar.class);
regAction(DepartamentoProdutoForm.Delete.class);
}
}
Mudança neste arquivo: uma única linha — registrar a nova inner class Delete no framework. Sem registro, o framework não sabe que a action existe e responde 404 ao clique do botão.
A mecânica é simples: o form em modo edição ganha um botão vermelho; clicar dispara a action Delete; a primeira execução pergunta (swalConfirm), a segunda (com CONFIRM=1) realmente apaga. Se o banco barrar por FK, um Toast vermelho explica em vez de quebrar a tela.
| Mudança | Onde | Papel |
|---|---|---|
| 2 imports novos | DepartamentoProdutoForm | Toast (notificação) + SQLConstraintException (tratamento de FK) |
| Botão "Excluir" | br() | Visível só em isUpdate(FORM), disparador da action Delete |
Inner class Delete | DepartamentoProdutoForm | Ciclo composto: pergunta → apaga → fecha modal + Toast |
Constante CONFIRM | fim da classe | Distingue a 1ª chamada (sem confirm) da 2ª (CONFIRM=1) |
regAction(Delete.class) | DepartamentoManager | Torna a action conhecida pelo framework |
As 4 primeiras mudanças moram no Form. A 5ª é a formalidade no Manager — sem ela, o clique do botão dá 404.
import br.jasap.gui.Toast;
import br.jasap.util.exceptions.SQLConstraintException;
ToastComponente de notificação visual do Jasap. Aparece por alguns segundos no canto da tela e some sozinho. Dois métodos disponíveis: success() (verde, ícone de check) e fail() (vermelho, ícone de X). Usado aqui pra confirmar sucesso da exclusão e pra avisar se o banco barrou por FK.
Sem interação — só informa. Diferente do swalConfirm, que pede resposta do usuário.
SQLConstraintExceptionExceção específica lançada pelo driver JDBC (via Jasap) quando um DELETE/UPDATE/INSERT viola uma constraint — tipicamente uma foreign key referenciando o registro alvo. Pegar essa exceção no catch transforma uma stack trace no console numa mensagem amigável pro usuário.
Observação: o JasapFunctions, Js e TransactionFilter já estavam importados desde o CRUD CREATE. Apenas Toast e SQLConstraintException são novidade.
br()
Button excluir = ui().button(" Excluir ").setCss("btn btn-danger btn-lg").setNoSize()
.setOnClick(link(Delete.class).ajax())
.setVisible(isUpdate(FORM));
Bar bar = ui().bar();
bar.addLeft(excluir);
bar.addRight(cancelar);
bar.addRight(" ");
bar.addRight(salvarInsert);
bar.addRight(salvarUpdate);
.setCss("btn btn-danger btn-lg")Classe Bootstrap btn-danger — fundo vermelho. Sinalização visual: essa é a ação destrutiva, trate com cuidado. Mesma família do btn-success (Salvar) e btn-primary (Cancelar), mas com cor que comunica "perigo".
.setOnClick(link(Delete.class).ajax())link(Delete.class) gera a URL da action pelo nome da classe. .ajax() transforma em chamada AJAX — sem submit de form, sem reload de página. O framework envia a requisição, recebe o response (que contém os comandos AjaxBack), e o browser executa.
Por que não submit? O submit é pra quando o clique precisa enviar valores de campos preenchidos (Insert, Update). Delete não precisa de nada do form — só do id_produto já na sessão. link é mais leve e suficiente.
.setVisible(isUpdate(FORM))Botão só aparece quando o form está em modo edição. Em modo inserção, não existe registro pra excluir — o botão some. É a mesma lógica do salvarUpdate (que também usa isUpdate(FORM)), invertida em relação ao salvarInsert (isInsert(FORM)).
bar.addLeft(excluir)O Bar tem 3 regiões: addLeft, addCenter, addRight. Convenção XT: ação destrutiva à esquerda, ações de fluxo principal (Cancelar, Salvar) à direita. A distância visual é intencional — evita o clique acidental no botão vermelho quando o usuário está mirando "Salvar".
Delete
public static class Delete extends DepartamentoProdutoAction {
public Delete() { super.getFilters().add(new TransactionFilter()); }
@Override
public Effect execute() throws Exception {
try {
if (JasapFunctions.equals(getInput().getInteger(CONFIRM), 1)) {
proBean().setId_produto(id_produto());
getFactory().departamento().proModel().daoDelete(proBean());
eval(new Toast("Registro excluído.").success());
evalParent(Js.CLOSE_SUB_WINDOWS);
} else {
eval(Js.swalConfirm("Confirma a exclusão desse registro?",
link(Delete.class).putInteger(CONFIRM, 1).ajax(), ""));
}
return new Response();
} catch (SQLConstraintException e) {
eval(new Toast("Falha ao excluir: esse registro está sendo usado em outra tabela.").fail());
return new Response();
}
}
}
extends DepartamentoProdutoAction (não DepartamentoProdutoForm)Diferença sutil mas importante: Insert e Update também estendem DepartamentoProdutoAction, não o Form. O motivo é que essas actions não renderizam o form — elas apenas executam a operação no banco e devolvem comandos AjaxBack. Estender o Form carregaria o overhead dos getters dos campos (nome_produto(), vl_produto() etc.) sem usar nenhum deles.
ShowInsert, ShowUpdate e Cancelar estendem o Form porque precisam chamar render(). As actions de escrita (Insert, Update, Delete) estendem diretamente a Action base.
public Delete() { super.getFilters().add(new TransactionFilter()); }Construtor que pluga o TransactionFilter na pilha de filtros da action. O filtro abre uma transação antes do execute() e — aqui está a parte crítica — faz rollback automático se qualquer Exception explodir. Se o daoDelete lança SQLConstraintException, o filtro vê a exception, desfaz a transação, e só depois a exception vira catch.
Sem esse filtro, o delete poderia executar parcialmente e deixar o banco em estado inconsistente. É o mesmo padrão de Insert e Update — sempre que a action escreve no banco.
if (JasapFunctions.equals(getInput().getInteger(CONFIRM), 1))A chave do ciclo composto: a mesma action é chamada duas vezes, e o input CONFIRM decide o comportamento.
CONFIRM é null, o equals(null, 1) retorna false, cai no else — dispara o swalConfirm..putInteger(CONFIRM, 1), o equals(1, 1) retorna true, executa o delete.JasapFunctions.equals é null-safe — não explode se o input vier null, só retorna false. Usar .equals() de Integer sem null-check quebraria com NullPointerException.
proBean().setId_produto(id_produto());id_produto() lê da sessão — o valor foi gravado lá pelo ShowUpdate quando o form abriu. Como o botão Excluir só aparece em modo edição, a sessão tem o valor garantido.
Ler da sessão (em vez do input) tem vantagem: depois do swalConfirm, o 2º clique só precisa carregar CONFIRM=1 — não precisa reenviar o id_produto. Menos superfície pra erro.
getFactory().departamento().proModel().daoDelete(proBean());Dispara o DELETE FROM departamento.produto WHERE id_produto = ?. O daoDelete já existia desde o CRUD DELETE — o Form só está consumindo o que a List também consome pelo DeleteFromList.
eval(new Toast("Registro excluído.").success());Toast verde no canto da tela. Confirma pro usuário que deu certo sem exigir ação dele (nada pra clicar, nada pra fechar — some sozinho).
evalParent(Js.CLOSE_SUB_WINDOWS);Fecha o modal do form (que é o iframe "filho"). O evalParent executa o JS na janela pai. Ao fechar, o setOnCloseURL(url(Sort.class)) configurado no ShowUpdate (lá na List) dispara automaticamente — e a lista se atualiza sem o registro excluído.
Detalhe bonito: o Form não precisa saber que existe uma lista esperando refresh. A lista definiu no momento em que abriu o modal ("quando fechar, chama Sort") e o Form só precisa fechar o modal. Desacoplamento correto.
eval(Js.swalConfirm("Confirma a exclusão desse registro?", link(Delete.class).putInteger(CONFIRM, 1).ajax(), ""));Dispara o modal de confirmação (SweetAlert). Se o usuário clicar "Sim", executa o JS passado no 2º parâmetro — que é o link pra si mesmo com CONFIRM=1. Se clicar "Não", fecha sem fazer nada.
O 3º parâmetro vazio ("") seria o JS a rodar ao cancelar — aqui não precisamos de nada.
catch (SQLConstraintException e)Captura especificamente violações de constraint (FK). Outras exceptions (driver caiu, SQL inválido) propagam e são tratadas pelo filtro global ErrorFilter. Esse catch é só pra o caso esperado de "o usuário tá tentando excluir algo que tem filho" — cenário que não é bug, é regra de negócio.
O Toast vermelho explica o motivo sem expor stack trace. O TransactionFilter já fez rollback antes do catch disparar, então o banco está limpo.
CONFIRM
public static String FORM = ROOT.concat("__FORM");
public static String CONFIRM = ROOT.concat("__CONFIRM");
Uma linha, junto à FORM no fim da classe. ROOT vem da action base — é o namespace do módulo ("XT.PAINEL_CONTROLE.ACESSO_MODULO.DEPARTAMENTO__PRODUTO/"). Concatenar garante chave única entre módulos — se dois módulos tivessem constante "__CONFIRM" sem o prefixo, colidiriam no input/sessão.
O padrão Jasap evita strings mágicas no código. getInput().getInteger("__CONFIRM") compilaria, mas:
"__CONFIR", vira bug silencioso);ROOT, colide com outros módulos.A constante centraliza a chave. Mesmo padrão de FORM, LIST, FILTRO, CONFIRM_LIST — todas existem pela mesma razão.
CONFIRM e não CONFIRM_LISTO DeleteFromList na List já usa CONFIRM_LIST. São contextos separados: um botão é da lista, outro é do form. Usar nomes diferentes deixa explícito e evita acoplamento — se amanhã alguém mudar a chave do List, o Form não é afetado.
regAction(Delete.class) no Manager
regAction(DepartamentoProdutoForm.Cancelar.class);
regAction(DepartamentoProdutoForm.Delete.class);
Uma linha no config() do DepartamentoManager, logo após Cancelar. Sem ela, o framework não sabe que a action Delete existe — o clique do botão bateria numa URL que o controller não reconhece, e a resposta seria 404.
O regAction grava a classe no mapa de actions do módulo durante a inicialização da aplicação. É chamado uma única vez, quando o Tomcat sobe — depois o framework consulta o mapa a cada requisição. Sempre que uma inner class nova de action for criada, tem que aparecer aqui.
Padrão do projeto: manter a ordem dos regAction refletindo a ordem das inner classes no arquivo Java. Facilita revisar se algum registro foi esquecido — comparar visualmente as duas listas. Delete vem depois de Cancelar porque é a última inner class no Form.
A exclusão precisa de dois cliques do usuário: o primeiro no botão "Excluir", o segundo no "Sim, excluir" do swal. Entre esses dois cliques, duas requisições HTTP separadas viajam até o servidor e voltam. Do ponto de vista do backend, são duas execuções independentes da mesma action.
| Passo | Quem dispara | O que chega no backend | Branch executado |
|---|---|---|---|
| 1 | Clique no botão Excluir | CONFIRM=null | else — dispara swalConfirm |
| 2 | Browser renderiza o modal SweetAlert | — | — |
| 3 | Clique em "Sim, excluir" no modal | CONFIRM=1 (via .putInteger) | if — executa daoDelete |
O HTTP é sem estado — o servidor esquece tudo entre requisições. Cada execução da Delete.execute() nasce e morre isolada. Pra a 2ª chamada saber que veio de um swal confirmado, o link(Delete.class).putInteger(CONFIRM, 1).ajax() embute essa informação no próprio link. Quando o usuário clica "Sim", o browser dispara esse link com CONFIRM=1.
É o padrão correto pra ciclos compostos: o estado viaja no link, não na sessão. A sessão é pra coisas que precisam persistir entre telas inteiras (usuário logado, filtros de lista). Um confirm de swal é efêmero — dura um clique.
Nada acontece. O 3º parâmetro do swalConfirm (JS a rodar no cancelamento) está vazio. Se o usuário clicar "Cancelar" ou pressionar Esc, o modal fecha e a aplicação fica no mesmo estado anterior — form aberto, nada apagado. A ação destrutiva nunca dispara sem a confirmação explícita.
Três coisas parecem relacionadas mas não são afetadas:
| Item | Por que fica de fora |
|---|---|
DeleteFromList na List | Continua existindo. É outro contexto — usuário que ainda não abriu o form, só quer apagar da linha. Ter as duas opções é conveniência, não redundância. |
DepartamentoProdutoWBean | Nenhuma mudança. Filtros da lista não têm nada a ver com exclusão individual. |
DepartamentoProdutoDAO | O método daoDelete já existia desde o CRUD DELETE. O Form só consome — não altera o DAO. |
DeleteFromList em vez de migrar tudo pro FormDois fluxos reais de uso cobrem casos diferentes:
Forçar um caminho só seria pior UX. Os dois podem coexistir porque dividem o daoDelete — a lógica de banco é única, só os pontos de entrada mudam.
| Arquivo | Tipo | Edição |
|---|---|---|
| DepartamentoProdutoForm | editar | 2 imports (Toast, SQLConstraintException), botão excluir no br(), bar.addLeft(excluir), inner class Delete, constante CONFIRM |
| DepartamentoManager | editar | regAction(DepartamentoProdutoForm.Delete.class) |
| DepartamentoProdutoBean | — | Nada |
| DepartamentoProdutoDAO | — | Nada — daoDelete já existia do CRUD DELETE |
| DepartamentoProdutoList | — | Nada — DeleteFromList continua no ar |
| Banco | — | Nada |
2 arquivos editados, nenhum arquivo novo, nenhuma mudança de schema. Resultado: botão "Excluir" vermelho no canto inferior esquerdo do formulário em modo edição, swalConfirm antes de apagar, Toast de confirmação (ou de erro se FK barrar), modal fecha e a lista se atualiza sem o registro excluído.