knowledge-base/records/plugin-dev/KB-PLUGIN-015-querySelector-row-collision.md

3.8 KiB

id title domain tags status severity created_at updated_at applies_to
KB-PLUGIN-015 querySelector('.row') colide com elementos de outros plugins no Helpdesk portal plugin-dev
glpi
glpi11
helpdesk
tilesections
dom
selectors
bootstrap
cross-plugin-interference
active high 2026-05-10 2026-05-10
GLPI 11.0.x

querySelector('.row') colide com outros plugins no Helpdesk portal

Sintoma

Plugin funciona perfeitamente no ambiente de desenvolvimento mas falha silenciosamente em produção — não há erro no console, não há AJAX falhando, mas o DOM não é manipulado. A causa é difícil de descobrir porque o servidor retorna os dados corretos e o JS chega a rodar.

No caso real: tilesections/public/js/helpdesk_home.js em produção não reorganizava os tiles em categorias. Em dev funcionava. A diferença era a presença de outro plugin (news_alert) em produção que injetava um elemento com classe row antes dos tiles.

Causa

A container-xl do portal do Helpdesk pode conter múltiplos elementos com class row:

<div class="container-xl">
    <table class="central">
        <tbody><tr><td>
            <!-- Plugin news_alert injeta isto via hook DISPLAY_CENTRAL -->
            <div class="plugin_news_alert-container row align-items-stretch"></div>
        </td></tr></tbody>
    </table>

    <!-- A row dos tiles -->
    <div class="row">
        <div class="col-12 col-sm-6 col-md-4 d-flex">
            <a class="card" href="...">...</a>
        </div>
    </div>
</div>

Código defeituoso:

const container = tilesSection.querySelector('.container-xl');
const tilesRow = container.querySelector('.row');   // ❌ pega o do news_alert

querySelector faz busca em profundidade e retorna o primeiro match na árvore — incluindo elementos aninhados. O plugin_news_alert-container está dentro da <table> que vem antes da row dos tiles, então é encontrado primeiro.

Depois disso, tilesRow.querySelectorAll(':scope > div') retorna zero (o container do news plugin está vazio), e a função desiste sem fazer nada.

Padrão correto

Usar :scope > .row para garantir que apenas filhos diretos sejam considerados:

const tilesRow = container.querySelector(':scope > .row');   // ✅ só filhos diretos

:scope referencia o elemento de partida da query (no caso, container). :scope > .row significa "row que é filha direta de container".

Lições

  • Nunca confie que sua produção tem a mesma estrutura DOM do dev. Plugins de terceiros (news, dashboards, customizações antigas) injetam elementos em pontos imprevisíveis.
  • Para qualquer seletor que dependa de hierarquia, prefira :scope > ou caminhos específicos ao invés de buscas globais com classes Bootstrap genéricas como .row, .card, .col-*.
  • Quando funcionar local e não em produção, não assuma diferenças de container/Docker. Inspecione o DOM real do cliente. O usuário pode rodar no console:
    document.querySelector('.tiles-banner .container-xl').innerHTML.substring(0, 600)
    
    e mandar o resultado.

Hierarquia de especificidade recomendada

Para seletores de containers em plugins GLPI, em ordem de robustez:

  1. [data-glpi-*] atributos — quando GLPI fornece (ex: [data-glpi-helpdesk-config-tiles]). Mais estável.
  2. :scope > .classe — quando só filhos diretos importam.
  3. Caminho específico — ex: .tiles-banner > .container-xl > .row.
  4. .row soltoevitar. Praticamente garante colisão com outros plugins.

Onde isso pegou na vida real

docker/glpi/plugins/tilesections/public/js/helpdesk_home.js::reorganizeTiles() — corrigido em tilesections v1.1.2. O bug não era reproduzível em dev porque o ambiente Docker não tinha o plugin news_alert instalado.