Docs›Comunicação›Busca global (⌘K)
Comunicação

Busca global (⌘K)

Command palette do header: ações, programas do menu.xml e registros pesquisáveis.

Busca global — command palette ⌘K

Palette do header estilo VSCode/Linear: atalho global ⌘K / Ctrl+K, três fontes de resultado, tudo gateado pelo mesmo gate das rotas (Mad\Security\PermissionGate::canAccess — fail-closed).

Seção Fonte Quando aparece Endpoint
Ações config('mad.search.actions') termo casa o label (topo) embutida no fragmento
Programas menu.xml (permissão + tradução _t{}) sempre; scroll infinito 20/20 embutida no fragmento
Registros config('mad.search.records') lazy: 2+ caracteres, debounce 250ms GET /app/SearchBox/records?static=1&q=

Arquitetura

layout.blade.php (#search_box_slot na pill do header)
   └─ MadTemplate.loadSearchBar()
        └─ fetchFragment('app/SearchBox/menu?static=1')
             └─ App\Control\Sys\SearchBox::menu()  →  resources/views/shell/search-box.blade.php
                  ├─ items   = SearchBox::items()    (menu.xml + permissão)
                  ├─ actions = SearchBox::actions()  (config + permissão)
                  └─ Alpine: ⌘K, ↑↓/↵/esc, overlay teleportado,
                             fetch lazy de /app/SearchBox/records

App\Control\Sys\SearchBox estende MadComponent apenas pelo contrato do dispatcher (MadAppController só roteia subclasses); os entry points reais são os métodos estáticos menu() e records(), chamados via ?static=1. Registro da rota em routes/web.php:

MadRoutes::exposeClass('SearchBox');

A classe está nos defaultPermissions (todo usuário logado tem acesso). Anônimo recebe redirect de login — o menu nunca vaza para quem não está autenticado.

mad.search.actions — comandos rápidos

// config/mad.php
'search' => [
    'actions' => [
        [
            'label'    => 'Nova mensagem',
            'icon'     => 'lucide:send',          // formato menu.xml (fa:/fas:/far:/mi:/lucide:/img)
            'shortcut' => '',                     // dica VISUAL opcional (não registra tecla)
            'program'  => 'MessageForm',           // gate; null = todo logado
            'url'      => ['MessageForm'],          // [classe] ou [classe, método] → URL amigável
        ],
        [
            'label'   => 'Alternar tema escuro',
            'icon'    => 'lucide:moon',
            'program' => null,
            'js'      => 'notchToggleDark',        // função GLOBAL do casco; roda local e fecha o palette
        ],
    ],
    'records' => [],
],

Contrato: url ou js (url ganha se ambos estiverem presentes). url é montada por MadRoutes::toFriendlyUrl() no servidor — o cliente nunca monta rota manualmente. Ação js executa window[nome]() e não navega.

mad.search.records — registros pesquisáveis

'search' => [
    'records' => [
        [
            'model'    => 'TesteCliente',          // FQCN ou short name (App\Models\)
            'database' => 'minierp',               // conexão EXPLÍCITA (default: 'business')
            'columns'  => ['nome'],                // LIKE %q% com OR entre colunas
            'display'  => '{nome}',                // máscara do título
            'subtitle' => '{cidade} ({uf})',        // máscara da linha 2 (opcional)
            'icon'     => 'fas:user',
            'program'  => 'TesteClienteForm',       // gate = TELA DE DESTINO
            'edit'     => ['TesteClienteForm', 'onEdit'], // navega com &id={pk}
        ],
    ],
],
  • Fail-closed: sem program, sem permissão nele, ou sem edit → a fonte inteira fica fora do resultado (nem chega a consultar o banco).
  • Conexão: $cls::on($src['database'] ?? 'business') — omitir database não usa o $connection do model, cai em business. Declare sempre.
  • Busca: orWhere($col, 'like', "%{$q}%") com OR entre as columns, dentro de um where(function …) — as demais cláusulas da fonte não vazam do agrupamento.
  • Limite de 5 resultados por fonte (QuerySource::recordsFromQuery($qb, null, 5)); máscaras ({nome}, {cidade} ({uf})) resolvidas por ModelOptionsLoader::mask().
  • Resiliente por fonte: se a query de uma fonte lançar (tabela/conexão ausente), o Throwable é logado via error_log('[SearchBox] fonte …') e só aquela fonte é pulada — o palette continua respondendo as outras.
  • A URL de edição é amigável (ex.: /app/testes/cliente/onEdit?id=5) — abre o shell com o drawer/form de edição já preenchido. O id vem de $rec->id (coluna literal id), então o model da fonte precisa expor esse atributo.
  • Escala por config: uma nova fonte de busca entra com zero PHP — só uma entrada no array.

Gates de permissão

As três fontes usam \Mad\Security\PermissionGate::canAccess($program) — o mesmo gate de MadAppController/rotas. Isso inclui os defaultPermissions (telas universais como MessageForm).

Não use SystemPermission::checkPermission em features de busca/navegação: ele só olha session('programs') + public_classes e não inclui os defaults — telas universais somem da busca para um usuário real (bug já pego em validação visual). Regra: o que a rota deixa abrir, a busca pode mostrar.

  • Escolha (clique ou ↵) navega por URL amigável (window.location = it.url).
  • Nunca Mad.go(classe) para telas de slug: no modo allowlist, classe crua sem exposeClass responde 404 — a rota canônica é o slug (/app/usuarios, não /app/SystemUserList/show).
  • Teclado: ⌘K/Ctrl+K abre · ↑↓ navega as 3 seções como lista única (com scrollIntoView no item ativo) · ↵ abre · esc fecha.
  • Scroll infinito dos programas: janela de 20, expande a 48px do fim da lista E quando a seta ↓ chega perto do fim.

Gotchas conhecidos

Sintoma Causa Fix
Editei a blade do palette e nada mudou cache do MadBlade segura o fragmento compilado rm -rf storage/framework/mad-blade/*
Programa novo não aparece na busca session('programs') é populada no login logout/login (ou re-loadSessionVars)
Editei MadTemplate.js e o browser ignora cache por ?appver= bump do appver na tag em resources/views/shell/partials/libraries-builder.blade.php
Item navega para 404 alvo sem rota no allowlist use o slug (MadRoutes::screen/expose) — toFriendlyUrl resolve sozinho se a rota existir
fetch de fragmento "falha" com 200 dispatcher static=1 responde Content-Type: application/json use o helper fetchFragment do MadTemplate (já trata e loga falha com a URL)

Testes

tests/Feature/SearchBoxTest.php — 10 cenários: permissão por fonte (fail-closed), tradução/URL amigável, atalho global no markup, 2+ caracteres, coexistência de fontes com permissões independentes, destino de edição renderizando preenchido, ação js local.

Veja também

  • Correio interno — a ação rápida "Nova mensagem" do palette aponta para MessageForm.
  • Roteamento › Rotas públicas — MadRoutes::screen/ expose/exposeClass e como o slug vira a URL amigável usada pela navegação do palette.