Delete no Form — Excluir direto do formulário editar arquivo

0:00 / 0:00

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).

CÓDIGO COMPLETO — DepartamentoProdutoForm
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.

CÓDIGO COMPLETO — DepartamentoManager
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.

O mecanismo — 5 mudanças pra deletar do Form

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çaOndePapel
2 imports novosDepartamentoProdutoFormToast (notificação) + SQLConstraintException (tratamento de FK)
Botão "Excluir"br()Visível só em isUpdate(FORM), disparador da action Delete
Inner class DeleteDepartamentoProdutoFormCiclo composto: pergunta → apaga → fecha modal + Toast
Constante CONFIRMfim da classeDistingue a 1ª chamada (sem confirm) da 2ª (CONFIRM=1)
regAction(Delete.class)DepartamentoManagerTorna 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.

Mudança 1 — 2 imports novos
import br.jasap.gui.Toast;
import br.jasap.util.exceptions.SQLConstraintException;

Toast

Componente 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.

SQLConstraintException

Exceçã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.

Mudança 2 — Botão "Excluir" no 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".

Mudança 3 — Inner class 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.

O 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.

  • 1ª chamada (clique no botão Excluir): CONFIRM é null, o equals(null, 1) retorna false, cai no else — dispara o swalConfirm.
  • 2ª chamada (clique "Sim, excluir" no swal): o link embutido traz .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.

Mudança 4 — Constante 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.

Por que uma constante separada em vez de string literal

O padrão Jasap evita strings mágicas no código. getInput().getInteger("__CONFIRM") compilaria, mas:

  • Typo passa batido (compila com "__CONFIR", vira bug silencioso);
  • Refactor de busca não pega;
  • Sem o prefixo do 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.

Por que CONFIRM e não CONFIRM_LIST

O 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.

Mudança 5 — 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.

O ciclo composto — swalConfirm

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.

PassoQuem disparaO que chega no backendBranch executado
1Clique no botão ExcluirCONFIRM=nullelse — dispara swalConfirm
2Browser renderiza o modal SweetAlert
3Clique em "Sim, excluir" no modalCONFIRM=1 (via .putInteger)if — executa daoDelete

Por que não guardar estado entre as duas chamadas

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.

E se o usuário fechar o swal?

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.

O que NÃO faz parte do Delete no Form

Três coisas parecem relacionadas mas não são afetadas:

ItemPor que fica de fora
DeleteFromList na ListContinua 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.
DepartamentoProdutoWBeanNenhuma mudança. Filtros da lista não têm nada a ver com exclusão individual.
DepartamentoProdutoDAOO método daoDelete já existia desde o CRUD DELETE. O Form só consome — não altera o DAO.

Por que manter DeleteFromList em vez de migrar tudo pro Form

Dois fluxos reais de uso cobrem casos diferentes:

  • DeleteFromList — "Essa lista tem 50 itens, quero apagar 3 rapidinho, sem abrir cada um." Clique-lixeira-confirma, clique-lixeira-confirma, em cima da lista.
  • Delete no Form — "Abri o produto pra editar, mas analisei melhor e quero apagar em vez de salvar." Botão vermelho no fim do form, dentro do mesmo contexto de edição.

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.

Resumo — o que mudou
ArquivoTipoEdição
DepartamentoProdutoFormeditar2 imports (Toast, SQLConstraintException), botão excluir no br(), bar.addLeft(excluir), inner class Delete, constante CONFIRM
DepartamentoManagereditarregAction(DepartamentoProdutoForm.Delete.class)
DepartamentoProdutoBeanNada
DepartamentoProdutoDAONada — daoDelete já existia do CRUD DELETE
DepartamentoProdutoListNada — DeleteFromList continua no ar
BancoNada

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.