CRUD LINKBOX — Vincular produto a uma pessoa responsável 1 arquivo novo + 3 edits

0:00 / 0:00

Até aqui, Produto e Pessoa viviam em tabelas separadas, cada uma com seu CRUD próprio. O LinkBox é o primeiro vínculo entre elas: cada produto passa a ter uma pessoa responsável, representada no formulário por um campo de leitura com botão verde ao lado — clicar abre um modal de seleção, escolher uma pessoa preenche o nome no campo e grava a FK no bean ao salvar.

É uma FK 1-N vista do lado do filho: o Produto aponta pra uma Pessoa (coluna fk_pessoa_produto em departamento.produto), e cada Pessoa pode ter várias produtos sob sua responsabilidade. A mudança envolve 4 arquivos: o Bean do Produto ganha um objeto Pessoa embutido, o Form do Produto ganha o campo e a action de callback, o arquivo novo DepartamentoPessoaSelect é o modal de seleção, e o Manager registra as 4 actions novas.

CÓDIGO COMPLETO — DepartamentoProdutoBean
0:00 / 0:00
package br.xt.app.departamento.produto;

import br.jasap.dao.DBInfo;
import br.jasap.util.DomainValue;
import br.jasap.util.JasapList;
import br.xt.app.departamento.pessoa.DepartamentoPessoaBean;
import java.io.Serializable;

public class DepartamentoProdutoBean implements Serializable {

    public static String TABLE = "departamento.produto";

    private Integer id_produto;
    private String  nome_produto;
    private Double  vl_produto;
    private Integer qtd_produto;
    private String  obs_produto;
    private String  qs_produto;
    private Integer st_produto;
    private Integer insert_chk;
    private DepartamentoPessoaBean pessoa;

    @DBInfo(serial=true, pk=true)
    public Integer getId_produto() { return id_produto; }
    public void setId_produto(Integer id_produto) { this.id_produto = id_produto; }

    // ... getters/setters dos outros campos (sem alteração) ...

    public DepartamentoPessoaBean getPessoa() {
        if (pessoa == null) pessoa = new DepartamentoPessoaBean();
        return pessoa;
    }
    public void setPessoa(DepartamentoPessoaBean pessoa) { this.pessoa = pessoa; }

    public Integer getFk_pessoa_produto()           { return getPessoa().getId_pessoa(); }
    public void    setFk_pessoa_produto(Integer fk) { getPessoa().setId_pessoa(fk); }

    // ... DomStatus (sem alteração) ...

    public static String ID_PRODUTO        = "id_produto";
    public static String NOME_PRODUTO      = "nome_produto";
    public static String VL_PRODUTO        = "vl_produto";
    public static String QTD_PRODUTO       = "qtd_produto";
    public static String OBS_PRODUTO       = "obs_produto";
    public static String QS_PRODUTO        = "qs_produto";
    public static String ST_PRODUTO        = "st_produto";
    public static String FK_PESSOA_PRODUTO = "fk_pessoa_produto";
    public static String INSERT_CHK        = "insert_chk";

}

Mudanças neste arquivo: 1 import novo (DepartamentoPessoaBean), campo private pessoa, getter/setter de pessoa (com lazy init), getter/setter de fk_pessoa_produto delegando pro objeto embutido (sem campo próprio), constante FK_PESSOA_PRODUTO. A coluna no banco não muda.

CÓDIGO COMPLETO — DepartamentoPessoaSelect novo arquivo
0:00 / 0:00
package br.xt.app.departamento.pessoa;

import br.jasap.core.Effect;
import br.jasap.effect.Response;
import br.jasap.gui.Bar;
import br.jasap.gui.JasapPage;
import br.jasap.gui.ListColumn;
import br.jasap.gui.ListLine;
import br.jasap.gui.ListView;
import br.jasap.gui.Table;
import br.jasap.gui.form.LinkBox;
import br.jasap.gui.form.Text;
import br.jasap.util.JasapList;
import br.jasap.util.Js;
import br.xt.acore.view.XtPage;

public class DepartamentoPessoaSelect extends DepartamentoPessoaAction {

    @Override
    public Effect execute() throws Exception {
        getSession().remove(S_ROOT);
        render();
        return new Response();
    }

    public static class Sort extends DepartamentoPessoaSelect {
        @Override
        public Effect execute() throws Exception {
            update(listV().getDIV_HEADER(),   listV().getHeader());
            update(listV().getDIV_BODY(),     listV().getBody());
            update(listV().getDIV_NAVIGATE(), listV().getNavForm());
            return new Response();
        }
    }

    public static class QuickSearch extends DepartamentoPessoaSelect {
        @Override
        public Effect execute() throws Exception {
            getFiltroS().setQs_pessoa(getInput().getString(DepartamentoPessoaBean.QS_PESSOA));
            update(listV().getDIV_BODY(),     listV().getBody());
            update(listV().getDIV_NAVIGATE(), listV().getNavForm());
            return new Response();
        }
    }

    public void render() throws Exception {
        XtPage page = new XtPage(getManager());
        if (isAjaxCall()) {
            update(JasapPage.DIV_WINDOW, page.content(window().toHtml()));
            eval(Js.CLOSE_SUB_WINDOWS);
        } else {
            page.getTable().setBorder(4).rowC("100%")
                    .setContent(page.content(window().toHtml()))
                    .setStyle(ui().modalBorder());
            page.setWinTitle("Selecionar Pessoa");
            getOutput().write(this, page);
        }
    }

    public Table window() throws Exception {
        Table w = new Table(getManager()).setSize("100%", "100%");
        w.rowC("1%",  JasapPage.DIV_TITLE, ui().title("SELEÇÃO DE PESSOA"));
        w.rowC("99%").setId(JasapPage.DIV_MASTER).setContent(listV()).table();
        w.rowC("1%",  null, ui().line());
        w.rowC("1%",  null, qs_pessoa());
        w.rowC("1%",  JasapPage.DIV_BOTTOM, br());
        return w;
    }

    public String qs_pessoa() throws Exception {
        Text qs = ui().text(DepartamentoPessoaBean.QS_PESSOA)
                .setLabel("Consulta")
                .setStyle("width:300")
                .setMaxlength(300)
                .setValue(getFiltroS().getQs_pessoa())
                .setOnkeyup(Js.pressEnter(
                        link(QuickSearch.class)
                                .putScript(DepartamentoPessoaBean.QS_PESSOA, Js.SELF_VALUE)
                                .putInteger(listV().getPAGE(), 0)
                                .ajax()));

        Table aux = new Table(getManager(), "100%", "30")
                .setStyle(ui().bgDark())
                .rowC()
                .setAlign(Table.ALIGN_CENTER)
                .setVerticalAlign(Table.ALIGN_MIDDLE)
                .setContent(qs)
                .table();

        return aux.toHtml();
    }

    private Bar br() throws Exception {
        return ui().bar().addCenter(listV().nav(listV().getNavForm()));
    }

    private ListView lv = null;
    public ListView listV() throws Exception {
        if (lv == null) {
            lv = ui().lView();
            lv.setCallBackURL(getSession().getString(LinkBox.CALLBACK_URL.concat("_PESSOA"), getInput()))
                    .setSortAct(url(Sort.class))
                    .setPageAction(url(Sort.class))
                    .setPage(getSession().getInteger(S_LIST.concat(lv.getPAGE()), getInput()))
                    .setOrderBy(getSession().getString(S_LIST.concat(lv.getORDER_BY()), getInput()))
                    .setSort(getSession().getString(S_LIST.concat(lv.getSORT()), getInput()))
                    .ajax();

            if (lv.getData().getOrderBy() == null) {
                lv.getData().setOrderBy(DepartamentoPessoaBean.NOME_PESSOA);
                lv.getData().setSort(JasapList.SORT_ASC);
            }

            lv.setFiltro(getFiltroS());
            getFactory().departamento().pesModel().daoList(lv.getData());

            ListColumn col_nome    = lv.newColumn("Nome").setOrderBy(DepartamentoPessoaBean.NOME_PESSOA).setPadding(";padding:10 8 10 8;");
            ListColumn col_apelido = lv.newColumn("Apelido").setOrderBy(DepartamentoPessoaBean.APELIDO_PESSOA).setWidth(180).setPadding(";padding:10 8 10 8;");

            while (lv.hasNext()) {
                DepartamentoPessoaBean lb = (DepartamentoPessoaBean) lv.next();
                ListLine line = lv.createLine()
                        .setOnclick(link(lv.getCallBackURL())
                                .putInteger(DepartamentoPessoaBean.ID_PESSOA, lb.getId_pessoa())
                                .noWait().ajax());
                col_nome.setContent(lb.getNome_pessoa());
                col_apelido.setContent(lb.getApelido_pessoa());
                lv.addLine(line);
            }
        }
        return lv;
    }

    public DepartamentoPessoaWBean getFiltroS() throws Exception {
        DepartamentoPessoaWBean filtro = (DepartamentoPessoaWBean) getSession().getObject(S_FILTRO);
        if (filtro == null) {
            filtro = pesWBean();
            getSession().addObj(S_FILTRO, filtro);
        }
        return filtro;
    }

    public static final String S_ROOT   = ROOT.concat("__PESSOA_SELECT/");
    public static final String S_LIST   = S_ROOT.concat("__LIST/");
    public static final String S_FILTRO = S_LIST.concat("__FILTRO");

}

Arquivo novo. 3 actions (principal + Sort + QuickSearch), método listV() com memoization, filtro em sessão, callback via LinkBox.CALLBACK_URL + "_PESSOA".

CÓDIGO COMPLETO — DepartamentoProdutoForm (apenas blocos novos)
0:00 / 0:00
// Imports novos:
import br.jasap.gui.form.LinkBox;
import br.xt.app.departamento.pessoa.DepartamentoPessoaBean;
import br.xt.app.departamento.pessoa.DepartamentoPessoaSelect;

// ... ShowInsert, Insert, ShowUpdate, Update, Cancelar, Delete, render(), window(), br() (sem alteração) ...

// Linha adicionada no form(), entre st_produto e obs_produto:
public String form() throws Exception {
    Form frm = ui().form();
    frm.addHidden(DepartamentoProdutoBean.ID_PRODUTO, proBean().getId_produto());

    frm.line().add(nome_produto(),       "150");
    frm.line().add(vl_produto(),         "150");
    frm.line().add(qtd_produto(),        "150");
    frm.line().add(st_produto(),         "150");
    frm.line().add(fk_pessoa_produto(),  "150");   // ADICIONADA
    frm.line().add(obs_produto(),        "150");
    frm.line().add(insert_chk(),         "150");

    return frm.getTable().toHtml();
}

// ... nome_produto(), vl_produto(), qtd_produto(), st_produto(), obs_produto() (sem alteração) ...

// Método novo:
private LinkBox fk_pessoa_produto = null;
public LinkBox fk_pessoa_produto() throws Exception {
    if (fk_pessoa_produto == null) {
        if (proBean().getFk_pessoa_produto() != null) {
            getFactory().departamento().pesModel().daoSingle(proBean().getPessoa());
        }
        fk_pessoa_produto = ui().linkBox("FK_PESSOA", "500", "400")
                .setLabel("Responsável")
                .setValue(proBean().getPessoa().getNome_pessoa())
                .setWidth(250)
                .addKey(DepartamentoProdutoBean.FK_PESSOA_PRODUTO, proBean().getFk_pessoa_produto())
                .setSelectLnk(link(DepartamentoPessoaSelect.class)).setCallBackID("_PESSOA")
                .setCallBackURL(url(Fk_pessoa_produto.class));
    }
    return fk_pessoa_produto;
}

// Inner class nova:
public static class Fk_pessoa_produto extends DepartamentoProdutoForm {
    @Override
    public Effect execute() throws Exception {
        proBean().setFk_pessoa_produto(getInput().getInteger(DepartamentoPessoaBean.ID_PESSOA));
        if (proBean().getFk_pessoa_produto() != null) {
            updateParent("FK_PESSOA", fk_pessoa_produto().textValue());
            evalParent(Js.CLOSE_SUB_WINDOWS);
        } else {
            update("FK_PESSOA", fk_pessoa_produto().textValue());
        }
        return new Response();
    }
}

Mudanças neste arquivo: 3 imports novos, 1 linha no form(), método fk_pessoa_produto(), inner class Fk_pessoa_produto. O resto do Form (ShowInsert, Insert, Update, Delete, render, window, br, getters dos outros campos) fica igual.

CÓDIGO COMPLETO — DepartamentoManager
0:00 / 0:00
package br.xt.app.departamento;

import br.xt.app.departamento.pessoa.DepartamentoPessoaForm;
import br.xt.app.departamento.pessoa.DepartamentoPessoaList;
import br.xt.app.departamento.pessoa.DepartamentoPessoaSelect;
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 do Home, ProdutoList, ProdutoForm (Show, Insert, Update, Cancelar, Delete) ...

        regAction(DepartamentoProdutoForm.Fk_pessoa_produto.class);

        // ... regAction do PessoaList, PessoaForm (Show, Insert, Update, Cancelar, Delete) ...

        regAction(DepartamentoPessoaSelect.class);
        regAction(DepartamentoPessoaSelect.Sort.class);
        regAction(DepartamentoPessoaSelect.QuickSearch.class);

    }
}

Mudanças neste arquivo: 1 import novo (DepartamentoPessoaSelect), 4 regAction novas — 1 pra inner class de callback do Form e 3 pras actions do Select.

O mecanismo — o circuito LinkBox + Select + Callback

O LinkBox é uma FK vista do lado do filho: na tela aparece como um input de texto (mostra o nome do pai) + um botão verde ao lado. O usuário não digita — clica no botão, escolhe no modal e o texto aparece. Três peças formam o circuito:

PeçaPapelOnde mora
LinkBoxInput visual com botão. Guarda a FK num hidden e o nome do pai visívelfk_pessoa_produto() no DepartamentoProdutoForm
SelectModal que lista os candidatos e dispara callback ao clicar numa linhaArquivo novo DepartamentoPessoaSelect
CallbackAction que recebe o ID escolhido, grava no bean, atualiza a telaInner class Fk_pessoa_produto no Form

Os dois lados (LinkBox e Select) não se referenciam diretamente — a ponte é a sessão HTTP: o LinkBox grava a URL de callback numa chave identificada por um sufixo (_PESSOA), e o Select lê a mesma chave pra saber pra onde chamar de volta.

Fluxo completo de uma vinculação

PassoQuem disparaO que acontece
1Form do Produto renderizafk_pessoa_produto() monta o LinkBox, grava na sessão a URL de callback (url(Fk_pessoa_produto.class)) sob a chave LinkBox.CALLBACK_URL + "_PESSOA"
2Usuário clica no botão verdeLink setSelectLnk abre o modal DepartamentoPessoaSelect em Ajax
3Select carregalistV() lê da sessão a URL de callback e configura cada linha com setOnclick(link(callbackURL).putInteger(ID_PESSOA, ...).ajax())
4Usuário clica numa pessoaBrowser dispara a URL de callback com id_pessoa=42. Chega na inner class Fk_pessoa_produto.execute()
5Callback executaproBean().setFk_pessoa_produto(42) → via delegação, vai pra getPessoa().setId_pessoa(42). updateParent("FK_PESSOA", textValue()) injeta o texto novo no Form. evalParent(CLOSE_SUB_WINDOWS) fecha o modal
6Usuário clica "Salvar"Update roda, o hidden fk_pessoa_produto viaja junto com o form, populateBean o grava no bean, daoUpdate emite UPDATE ... SET fk_pessoa_produto=42
Mudança 1 — Bean: objeto Pessoa embutido e FK delegada
private DepartamentoPessoaBean pessoa;

public DepartamentoPessoaBean getPessoa() {
    if (pessoa == null) pessoa = new DepartamentoPessoaBean();
    return pessoa;
}
public void setPessoa(DepartamentoPessoaBean pessoa) { this.pessoa = pessoa; }

public Integer getFk_pessoa_produto()           { return getPessoa().getId_pessoa(); }
public void    setFk_pessoa_produto(Integer fk) { getPessoa().setId_pessoa(fk); }

A mudança é pequena em linhas mas conceitual: o Bean do Produto deixa de guardar fk_pessoa_produto como campo próprio e passa a guardar um objeto Pessoa embutido. A FK vira uma delegação — pega o id_pessoa da pessoa embutida.

Por que não deixar private Integer fk_pessoa_produto como antes?

Porque o LinkBox precisa exibir o nome da pessoa vinculada, não só o ID. Guardando só um Integer, o Form teria que fazer uma consulta extra em cada render pra descobrir o nome. Com o objeto embutido, basta o DAO chamar daoSingle(proBean().getPessoa()) uma vez e o nome fica disponível em getPessoa().getNome_pessoa().

Lazy init no getter

Lazy init = "criar só quando precisar". O campo pessoa começa null; só vira objeto de verdade na primeira chamada a getPessoa(). Duas razões:

  • Compatível com populateBean — o Jasap popula o Bean a partir do ResultSet e deixa pessoa como null. Quem chamar getPessoa() depois garante que retorna um objeto, nunca null
  • Economia — se a listagem só usa campos do próprio Produto, o objeto Pessoa nunca é criado

Delegação da FK

SituaçãoO que acontece
DAO lê do bancosetFk_pessoa_produto(42)getPessoa().setId_pessoa(42). Objeto pessoa criado com só o ID
Form carrega o nomedaoSingle(proBean().getPessoa()) preenche os demais campos (nome, apelido, etc)
LinkBox exibeproBean().getPessoa().getNome_pessoa() — já tá lá
DAO grava no bancogetFk_pessoa_produto()getPessoa().getId_pessoa() → ID correto pra coluna

Do ponto de vista do banco, nada muda — a coluna fk_pessoa_produto continua existindo e sendo escrita com o ID. A diferença é interna: o valor passa por uma ponte (objeto pessoa) em vez de ficar isolado num campo Integer.

Mudança 2 — Form: campo fk_pessoa_produto() no form()
frm.line().add(fk_pessoa_produto(),  "150");   // ADICIONADA, entre st_produto e obs_produto

private LinkBox fk_pessoa_produto = null;
public LinkBox fk_pessoa_produto() throws Exception {
    if (fk_pessoa_produto == null) {
        if (proBean().getFk_pessoa_produto() != null) {
            getFactory().departamento().pesModel().daoSingle(proBean().getPessoa());
        }
        fk_pessoa_produto = ui().linkBox("FK_PESSOA", "500", "400")
                .setLabel("Responsável")
                .setValue(proBean().getPessoa().getNome_pessoa())
                .setWidth(250)
                .addKey(DepartamentoProdutoBean.FK_PESSOA_PRODUTO, proBean().getFk_pessoa_produto())
                .setSelectLnk(link(DepartamentoPessoaSelect.class)).setCallBackID("_PESSOA")
                .setCallBackURL(url(Fk_pessoa_produto.class));
    }
    return fk_pessoa_produto;
}

Carregar o nome antes de mostrar

Se o produto já tem responsável (cenário update), vai no banco buscar os dados da pessoa. O daoSingle recebe o objeto pessoa (que já tem o id_pessoa preenchido, graças à delegação no Bean) e enche os outros campos — inclusive o nome_pessoa, que é o que o LinkBox vai exibir. Se a FK é null, pula.

Configuração do LinkBox

TrechoO que faz
ui().linkBox("FK_PESSOA", "500", "400")Cria o LinkBox com ID FK_PESSOA (usado nos update/updateParent) e tamanho do modal 500×400
.setLabel("Responsável")Texto à esquerda do campo
.setValue(proBean().getPessoa().getNome_pessoa())Pré-preenche o texto com o nome da pessoa (ou null)
.setWidth(250)Largura do campo de texto (não do modal)
.addKey(FK_PESSOA_PRODUTO, getFk_pessoa_produto())Adiciona um hidden com a FK. Viaja junto no submit do Form; o populateBean do Update o lê e seta a FK antes do UPDATE
.setSelectLnk(link(DepartamentoPessoaSelect.class))Link disparado ao clicar no botão verde — abre o Select
.setCallBackID("_PESSOA")Sufixo anexado à chave de sessão LinkBox.CALLBACK_URL. Os dois lados precisam combinar o mesmo sufixo
.setCallBackURL(url(Fk_pessoa_produto.class))URL que o Select vai chamar. O LinkBox grava essa URL na sessão sob CALLBACK_URL + "_PESSOA"

Lazy init (if (fk_pessoa_produto == null)) igual aos outros campos do Form — cria uma vez por requisição e reaproveita.

Mudança 3 — Form: inner class Fk_pessoa_produto (callback)
public static class Fk_pessoa_produto extends DepartamentoProdutoForm {
    @Override
    public Effect execute() throws Exception {
        proBean().setFk_pessoa_produto(getInput().getInteger(DepartamentoPessoaBean.ID_PESSOA));
        if (proBean().getFk_pessoa_produto() != null) {
            updateParent("FK_PESSOA", fk_pessoa_produto().textValue());
            evalParent(Js.CLOSE_SUB_WINDOWS);
        } else {
            update("FK_PESSOA", fk_pessoa_produto().textValue());
        }
        return new Response();
    }
}

Esta action é o "outro lado" do LinkBox. Chamada pelo DepartamentoPessoaSelect quando o usuário clica numa pessoa da lista (o Select dispara o callback configurado pelo LinkBox).

LinhaO que faz
setFk_pessoa_produto(getInput().getInteger(ID_PESSOA))Lê o id_pessoa que o Select passou como parâmetro e grava no bean. Via delegação, também preenche getPessoa().setId_pessoa(...)
updateParent("FK_PESSOA", fk_pessoa_produto().textValue())Atualiza o texto do campo na janela pai (o Form atrás do modal). textValue() retorna o HTML com o nome já preenchido
evalParent(Js.CLOSE_SUB_WINDOWS)Fecha o modal do Select — retorna o foco pro Form
update("FK_PESSOA", ...) (else)Cenário em que a FK veio null: só atualiza local, sem fechar modal
extends DepartamentoProdutoFormHerda fk_pessoa_produto(), proBean() e textValue(). Essencial pra re-montar o componente e pegar o texto atualizado

Por que é public static class dentro do Form e não um arquivo separado? Jasap reaproveita o escopo da classe externa: proBean(), fk_pessoa_produto() etc são visíveis sem refatoração. É o mesmo padrão de ShowInsert, Insert, Delete.

Mudança 4 — DepartamentoPessoaSelect: o modal de seleção

Arquivo novo e o maior da sequência. Um Select no Jasap é uma ListView em miniatura dentro de um modal: parecido com a List comum (ordenação, paginação, busca), mas com duas diferenças:

  • Ao clicar numa linha, em vez de abrir o Form daquela entidade, chama a URL de callback informada pelo LinkBox
  • O estado (busca, página, ordenação) fica salvo num namespace próprio na sessão (S_ROOT) — separado da List principal e de outros Selects

3 entry points (actions)

ActionQuando é chamadaO que faz
DepartamentoPessoaSelect (principal)Quando o LinkBox abre o modal pela primeira vezremove(S_ROOT) limpa toda a sub-árvore de sessão (zera busca/página anteriores) e chama render()
.SortClique no cabeçalho de uma coluna ou troca de páginaRe-renderiza só header + body + navegação via Ajax — não redesenha o modal inteiro
.QuickSearchEnter no campo de buscaGrava o termo no filtro em sessão, re-renderiza body + navegação

Como o Select conversa com o LinkBox

lv.setCallBackURL(getSession().getString(LinkBox.CALLBACK_URL.concat("_PESSOA"), getInput()))

O LinkBox, ao ser montado no Form, gravou a URL de callback na sessão sob a chave LinkBox.CALLBACK_URL + "_PESSOA". O Select essa chave e configura cada linha pra disparar aquela URL com o id_pessoa do registro clicado:

ListLine line = lv.createLine()
        .setOnclick(link(lv.getCallBackURL())
                .putInteger(DepartamentoPessoaBean.ID_PESSOA, lb.getId_pessoa())
                .noWait().ajax());

Filtro cacheado em sessão

public DepartamentoPessoaWBean getFiltroS() throws Exception {
    DepartamentoPessoaWBean filtro = (DepartamentoPessoaWBean) getSession().getObject(S_FILTRO);
    if (filtro == null) {
        filtro = pesWBean();
        getSession().addObj(S_FILTRO, filtro);
    }
    return filtro;
}

O WBean fica salvo sob S_FILTRO. Primeira chamada cria em branco e salva; chamadas seguintes (Sort, QuickSearch, paginação) retornam o mesmo filtro. Isso permite que a busca digitada no QuickSearch persista entre reordenações — sem o cache, cada clique recriaria o filtro em branco.

Namespace de sessão em árvore

public static final String S_ROOT   = ROOT.concat("__PESSOA_SELECT/");
public static final String S_LIST   = S_ROOT.concat("__LIST/");
public static final String S_FILTRO = S_LIST.concat("__FILTRO");
ConstantePapel
S_ROOTRaiz. remove(S_ROOT) apaga tudo abaixo de uma vez — garante que reabrir o modal começa do zero
S_LISTSubárvore dos parâmetros da ListView (página, coluna, direção)
S_FILTROO WBean cacheado com o termo de busca

listV() com memoization

O método listV() tem guard if (lv == null) — a ListView é montada uma única vez por requisição. Sort, QuickSearch e render() podem chamar listV() várias vezes e recebem sempre o mesmo objeto já configurado. Sem esse cache, a lista seria re-consultada do banco a cada chamada.

Mudança 5 — Manager: 4 regAction novas
// Bloco do Form do Produto — uma regAction nova:
regAction(DepartamentoProdutoForm.Fk_pessoa_produto.class);

// Bloco do Select — bloco inteiro novo:
regAction(DepartamentoPessoaSelect.class);
regAction(DepartamentoPessoaSelect.Sort.class);
regAction(DepartamentoPessoaSelect.QuickSearch.class);

Toda action nova precisa ser registrada no Manager, senão o Jasap não a encontra no mapa global e a chamada retorna 404.

regActionOnde no ManagerPor quê
DepartamentoProdutoForm.Fk_pessoa_produtoFinal do bloco do Form do Produto — depois de DeleteInner class que recebe o callback do LinkBox. Sem isso, o Select chama a URL e o Jasap não reconhece
DepartamentoPessoaSelectBloco novo no finalAction principal — entrada chamada pelo botão do LinkBox
DepartamentoPessoaSelect.SortBloco do SelectReordenação e paginação dentro do modal
DepartamentoPessoaSelect.QuickSearchBloco do SelectBusca rápida digitada no campo

Convenção do projeto: ordem dos regAction reflete a ordem conceitual — cada entidade em bloco (List → Form → Select). O Fk_pessoa_produto pertence ao Form (é callback de campo do Form), então fecha o bloco do ProdutoForm. O DepartamentoPessoaSelect é componente auxiliar, vem depois da entidade "dona" dele.

Bug silencioso — esquecer de registrar

Compila, o link é gerado com a URL correta, mas o Manager não conhece a classe. No browser, o sintoma é mudo — nada explode, nada responde:

SintomaCausa real
Clicar numa pessoa no modal não faz nadaFk_pessoa_produto não registrado
Botão verde do LinkBox não abre modalDepartamentoPessoaSelect não registrado
Reordenar colunas ou digitar busca no modal não faz nadaSort ou QuickSearch não registrados
O que NÃO faz parte do LinkBox

Quatro coisas parecem relacionadas mas não são tocadas:

ItemPor que fica de fora
DDL do bancoA coluna fk_pessoa_produto já existia na tabela. Nenhum ALTER TABLE — só um objeto embutido no Bean que delega pra mesma coluna
DepartamentoProdutoDAOO daoInsert/daoUpdate continuam iguais. O populateBean do framework lê a coluna e chama setFk_pessoa_produto — que agora delega, mas a assinatura externa é a mesma
DepartamentoProdutoWBeanFiltros da listagem de Produto não mudam. Filtrar produtos por responsável é cenário futuro (Master/Detail)
DepartamentoPessoaFormO Form da Pessoa fica igual — não precisa saber que produtos existem vinculados. O LinkBox é unidirecional: o filho aponta pro pai, o pai não "sabe" dos filhos

Ver os produtos a partir do lado da Pessoa é a próxima etapa da sequência (Master/Detail) — aí sim o PessoaForm ganha uma tabela de detail, e aí sim o ProdutoDAO ganha daoLinkPessoa/daoUnlinkPessoa.

Resumo — o que mudou
ArquivoTipoEdição
DepartamentoProdutoBeaneditar1 import, campo private pessoa, getter/setter com lazy init, getFk/setFk delegando, constante FK_PESSOA_PRODUTO
DepartamentoPessoaSelectnovo arquivoAction principal + Sort + QuickSearch, listV() com memoization, filtro em sessão, callback via CALLBACK_URL + "_PESSOA"
DepartamentoProdutoFormeditar3 imports, 1 linha no form(), método fk_pessoa_produto(), inner class Fk_pessoa_produto
DepartamentoManagereditar1 import, 4 regAction (1 pro callback, 3 pro Select)
BancoNada — coluna fk_pessoa_produto já existia

1 arquivo novo + 3 edits, nenhuma mudança de schema. Resultado: campo "Responsável" no form do Produto com modal de seleção de Pessoa, callback que atualiza texto e FK, e o UPDATE do Produto grava a FK junto com os outros campos.