Modal de detalhe e grids
detailModal com abas summary, form, relatedGrid, readonlyGrid e customComponent; edição inline por linha; rowActions com interpolação, condições e refresh entre abas.
Centrais frequentemente têm uma entidade principal com filhos operacionais — Projeto → Itens → Pagamentos, Pedido → Produtos → Expedições, Contrato → Parcelas → Documentos. A modal de detalhe resolve esse padrão sem página React.
Grid principal
└── ação "Editar"
└── modal com abas
├── Resumo (summary)
├── Dados Principais (form)
├── Itens (relatedGrid editável inline)
└── Pagamentos (readonlyGrid)
Exemplo completo
<CoreCollection
model="OrcamentoProjeto"
label="Orçamentos/Projetos"
endpoint="/orcamentos-projetos"
list={{
filters: [{ field: "tipoRegistro", label: "Tipo", type: "select", options: [
{ label: "Todos", value: "" }, { label: "Só orçamentos", value: "Orçamento" }, { label: "Só projetos", value: "Projeto" },
] }],
columns: ["tipoRegistro", "codigo", "nome", "cliente", "status", "totalParaComImpostos"],
rowActions: [{ type: "openDetailModal", label: "Editar", icon: "edit", initialTab: "resumo" }],
}}
relations={{
itens: { model: "OrcamentoItem", foreignKey: "projetoId", parentKey: "_id" },
pagamentos: { model: "Pagamento", foreignKey: "projetoId", parentKey: "_id" },
}}
detailModal={{
enabled: true,
titleField: "nome",
size: "xl",
defaultTab: "resumo",
tabs: [
{ id: "resumo", label: "Resumo", type: "summary", cards: [
{ label: "Itens", source: "relatedCount", relation: "itens" },
{ label: "Pagamentos", source: "relatedCount", relation: "pagamentos" },
{ label: "Total", field: "totalParaComImpostos", format: "currency" },
] },
{ id: "dados", label: "Dados Principais", type: "form", groups: [
{ label: "Identificação", fields: ["tipoRegistro", "codigo", "nome", "status", "cliente"], columns: 2 },
{ label: "Evento", fields: ["dataEvento", "localEvento", "contato"] },
] },
{ id: "itens", label: "Itens", type: "relatedGrid", relation: "itens", editable: true, editMode: "inline",
columns: [
{ field: "item", editable: true },
{ field: "status", editable: true },
{ field: "totalParaComImpostos", editable: true, format: "currency" },
{ field: "pagamentoId", label: "Pagamento", readonly: true, display: "badgeExists" },
],
rowActions: [{
id: "gerarPagamento", label: "Gerar pagamento", type: "apiAction", method: "POST",
endpoint: "/orcamentos-itens/:id/gerar-pagamento",
confirm: { title: "Gerar pagamento?", description: "Será criado um ticket financeiro vinculado a este item." },
disabledWhen: { field: "pagamentoId", exists: true },
refresh: ["self", "pagamentos", "resumo", "parent"],
}],
},
{ id: "pagamentos", label: "Pagamentos", type: "readonlyGrid", relation: "pagamentos",
columns: ["codigo", "descricao", "statusEsteira", "valorFechamento"] },
],
}}
/>
Abas
type |
Uso | Configuração |
|---|---|---|
summary |
Cards de indicadores | cards[] com source: field, relatedCount, relatedSum, relatedAvg, customMetric; format |
form |
Dados do registro principal (salva o pai) | groups[] com label, description, fields, columns (1–4), ou fields |
relatedGrid |
Filhos editáveis | relation, editable, editMode (inline, modal, drawer), columns, rowActions |
readonlyGrid |
Relacionados só leitura | relation, columns, rowActions |
customComponent |
Componente registrado | component: "custom:Chave", props |
detailModal.size: md, lg, xl, full.
Relações
relations: { itens: { model: "OrcamentoItem", foreignKey: "projetoId", parentKey: "_id", label: "Itens" } }
A aba busca os filhos por foreignKey = parent[parentKey]. Declare uma vez e reutilize em abas, filtros e ações.
Edição inline
Com editable: true e editMode: "inline":
- célula editável conforme a metadata do campo (ou
editorexplícito:string,number,currency,date,enum,select,ref,boolean,textarea); - linha marcada como alterada, com Salvar e Cancelar por linha;
PUT /:modelPath/:idpelo CRUD genérico;- loading e erro por linha;
- refresh pós-salvamento.
Em esteiras, relatedGrid também aceita create (inclusão vinculada ao pai) e delete (com confirmação).
Ações por linha
type |
Efeito |
|---|---|
openDetailModal |
Abre a modal (initialTab) |
navigate |
Navega para href |
apiAction |
Chama endpoint com method |
customAction |
Renderer próprio (renderer) |
Interpolação de endpoint
:id (linha), :parentId (registro pai), :campo (campo da linha) e :parent.campo (campo do pai).
Condições
disabledWhen: { field: "pagamentoId", exists: true }
hiddenWhen: { field: "status", equals: "Cancelado" }
Operadores: exists, equals, notEquals, in, gt, gte, lt, lte.
Refresh
self (grid atual), parent (registro principal), all (todas as queries da modal) ou o id de uma aba.
RBAC visual
Abas, colunas e ações aceitam roles/permissions. A UI oculta ou desabilita conforme a permissão — mas a autoridade final é o backend: a rota de apiAction precisa declarar sua permission.
Componentes envolvidos
CoreDetailModal, CoreTabbedDetail, CoreSummaryCards, CoreRelatedGrid, CoreInlineEditableCell e CoreRowAction implementam o contrato de forma genérica.