276 lines
10 KiB
Markdown
276 lines
10 KiB
Markdown
---
|
|
id: KB-PLUGIN-012
|
|
title: Estendendo o sistema de Tiles do Helpdesk no GLPI 11 — padrões e armadilhas
|
|
domain: plugin-dev
|
|
tags:
|
|
- glpi
|
|
- glpi11
|
|
- helpdesk
|
|
- tiles
|
|
- dom-manipulation
|
|
- mutation-observer
|
|
- twig
|
|
- singleton
|
|
- plugin
|
|
status: active
|
|
severity: high
|
|
created_at: 2026-05-09
|
|
updated_at: 2026-05-09
|
|
applies_to:
|
|
- GLPI 11.0.x
|
|
---
|
|
|
|
# Estendendo o sistema de Tiles do Helpdesk no GLPI 11
|
|
|
|
Documento consolidado das armadilhas encontradas ao implementar o plugin `tilesections` (categorização visual de tiles na "Página Inicial do Helpdesk"). O sistema de tiles do GLPI 11 é nativamente plano (sem agrupamento) e não foi desenhado para extensão por plugins — qualquer modificação visual precisa contornar isso.
|
|
|
|
## 1. Estrutura DOM dos tiles no admin (Entidade > Página Inicial do Helpdesk)
|
|
|
|
Cada tile na grade de configuração é renderizado pela template `templates/pages/admin/helpdesk_home_config_tiles.html.twig` em **dois níveis aninhados**:
|
|
|
|
```html
|
|
<section
|
|
class="col-12 col-lg-6 col-xl-4 d-flex-soft pointer-events-none"
|
|
data-glpi-draggable-item
|
|
data-glpi-helpdesk-config-tile-container ← wrapper externo
|
|
data-glpi-helpdesk-config-action-show-edit-form
|
|
data-bs-toggle="offcanvas"
|
|
>
|
|
<div
|
|
data-glpi-helpdesk-config-tile
|
|
data-glpi-helpdesk-config-tile-id="123" ← ID está aqui
|
|
data-glpi-helpdesk-config-tile-itemtype="..."
|
|
class="card rounded my-2 cursor-pointer"
|
|
>
|
|
...
|
|
</div>
|
|
</section>
|
|
```
|
|
|
|
### Armadilha
|
|
|
|
Mover apenas o `<div>` interno (que tem o `data-glpi-helpdesk-config-tile-id`) **destrói o layout**:
|
|
|
|
- Perde as classes de coluna (`col-12 col-lg-6 col-xl-4`) → tile vira full-width
|
|
- Perde os atributos de drag (`data-glpi-draggable-item`) → não é mais reordenável
|
|
- Perde o trigger do offcanvas (`data-bs-toggle="offcanvas"`) → click pra editar não funciona
|
|
|
|
### Padrão correto
|
|
|
|
Selecionar o **outer container** com `[data-glpi-helpdesk-config-tile-container]` e ler os IDs do `<div>` interno:
|
|
|
|
```js
|
|
const tileOuters = container.querySelectorAll('[data-glpi-helpdesk-config-tile-container]');
|
|
tileOuters.forEach(outer => {
|
|
const inner = outer.querySelector('[data-glpi-helpdesk-config-tile-id]');
|
|
const itemtype = inner.getAttribute('data-glpi-helpdesk-config-tile-itemtype');
|
|
const tileId = inner.getAttribute('data-glpi-helpdesk-config-tile-id');
|
|
// mover `outer` (não `inner`)
|
|
});
|
|
```
|
|
|
|
## 2. TilesManager é singleton — `__construct` é privado
|
|
|
|
```php
|
|
// ❌ ERRADO — Call to private __construct from scope ...
|
|
$manager = new \Glpi\Helpdesk\Tile\TilesManager();
|
|
|
|
// ✅ CORRETO
|
|
$manager = \Glpi\Helpdesk\Tile\TilesManager::getInstance();
|
|
```
|
|
|
|
## 3. Como o GLPI obtém os tiles que serão renderizados no portal público
|
|
|
|
O `Glpi\Controller\Helpdesk\IndexController` (que renderiza o portal `/Helpdesk`) chama:
|
|
|
|
```php
|
|
$manager->getVisibleTilesForSession(Session::getCurrentSessionInfo())
|
|
```
|
|
|
|
Esse método:
|
|
|
|
1. Pega os tiles do **profile atual**; se vazio, sobe pelo entity tree
|
|
2. Filtra por `$tile->isAvailable($session_info)` (permissões, configurações, etc.)
|
|
3. Retorna `array<TileInterface&CommonDBTM>` na ordem que serão renderizados
|
|
|
|
**Use o mesmo método em endpoints AJAX do seu plugin** para garantir alinhamento perfeito com o que o usuário vê — inclusive quando o admin está personificando outro perfil.
|
|
|
|
## 4. Tiles do portal público NÃO têm `data-attribute` de identificação
|
|
|
|
O template `templates/pages/helpdesk/index.html.twig` renderiza apenas:
|
|
|
|
```html
|
|
<a class="card mx-1 my-2 flex-grow-1" href="{{ tile.getTileUrl() }}">
|
|
```
|
|
|
|
Não há `data-tile-id`, `data-itemtype`, etc. **Não dá pra fazer matching reverso por ID.**
|
|
|
|
### Armadilha: matching por URL é frágil
|
|
|
|
Usar `tile.getTileUrl()` como chave de matching falha em casos comuns:
|
|
|
|
- **URL vazia**: `ExternalPageTile` retorna `""` se o campo `url` estiver vazio. Múltiplos tiles com URL vazia colidem na mesma chave (`href::`) e o último processado "ganha", varrendo os outros.
|
|
- **URLs duplicadas**: dois `ExternalPageTile` apontando pra mesma URL têm chave idêntica.
|
|
- **Encoding**: query strings com `[]` (`criteria[0][field]=...`) podem ser escapadas diferentemente entre o servidor e o atributo HTML.
|
|
|
|
### Padrão correto: matching por índice de renderização
|
|
|
|
```php
|
|
// servidor — endpoint AJAX
|
|
$rendered = TilesManager::getInstance()
|
|
->getVisibleTilesForSession(Session::getCurrentSessionInfo());
|
|
|
|
$tile_order = [];
|
|
foreach ($rendered as $tile) {
|
|
$tile_order[] = [
|
|
'itemtype' => get_class($tile),
|
|
'items_id' => (int) $tile->getDatabaseId(),
|
|
'sections_id' => (int) ($mappings[get_class($tile) . '::' . $tile->getDatabaseId()] ?? 0),
|
|
];
|
|
}
|
|
```
|
|
|
|
```js
|
|
// cliente — JS do portal
|
|
const tileWrappers = tilesRow.querySelectorAll(':scope > div');
|
|
tileWrappers.forEach((wrapper, idx) => {
|
|
const tileInfo = tileOrder[idx]; // 1:1 garantido pela ordem
|
|
if (!tileInfo) return;
|
|
const sectionId = tileInfo.sections_id;
|
|
// ...
|
|
});
|
|
```
|
|
|
|
Funciona porque a sessão da chamada AJAX é a mesma sessão que renderizou a página — o `getVisibleTilesForSession` retorna a lista exata na ordem exata.
|
|
|
|
## 5. GLPI re-renderiza tiles via AJAX após delete/reorder
|
|
|
|
Quando o usuário deleta ou reordena um tile na tela de admin, o GLPI faz um AJAX e **substitui o conteúdo do container** `[data-glpi-helpdesk-config-tiles]` com a lista atualizada do servidor. Isso varre qualquer reorganização visual aplicada pelo plugin.
|
|
|
|
### Padrão correto: MutationObserver com guarda de re-entrada
|
|
|
|
```js
|
|
let isReorganizing = false;
|
|
|
|
function watchGridRefreshes(tilesContainer) {
|
|
new MutationObserver(() => {
|
|
if (isReorganizing) return;
|
|
|
|
const hasTiles = tilesContainer.querySelectorAll('[data-glpi-helpdesk-config-tile-container]').length > 0;
|
|
const hasSections = tilesContainer.querySelector('.minha-section-wrapper');
|
|
if (!hasTiles || hasSections) return;
|
|
|
|
// Container voltou ao estado plano — re-aplicar reorganização
|
|
queueMicrotask(async () => {
|
|
await loadServerData();
|
|
reorganize(tilesContainer); // setar isReorganizing dentro
|
|
});
|
|
}).observe(tilesContainer, { childList: true });
|
|
}
|
|
|
|
function reorganize(tilesContainer) {
|
|
isReorganizing = true;
|
|
// ... mutations no DOM ...
|
|
queueMicrotask(() => { isReorganizing = false; });
|
|
}
|
|
```
|
|
|
|
A guarda `isReorganizing` é essencial — sem ela, as próprias mutações do plugin disparam o observer em loop infinito.
|
|
|
|
## 6. Race condition no save de tile — não use polling
|
|
|
|
Quando o usuário salva um novo tile no offcanvas, o GLPI cria via AJAX e adiciona ao DOM **assíncronamente**. Polling com `setInterval` é frágil:
|
|
|
|
- "Último tile no DOM" não garante ser o recém-criado (ordenação muda)
|
|
- Timeout duro (ex: 6s) pode estourar em conexões lentas
|
|
|
|
### Padrão correto: snapshot + MutationObserver com diff
|
|
|
|
```js
|
|
function handleAddSave(tilesContainer, sectionId) {
|
|
// Snapshot: tile keys antes do save
|
|
const before = new Set();
|
|
tilesContainer.querySelectorAll('[data-glpi-helpdesk-config-tile-id]').forEach(el => {
|
|
const itemtype = el.getAttribute('data-glpi-helpdesk-config-tile-itemtype');
|
|
const id = el.getAttribute('data-glpi-helpdesk-config-tile-id');
|
|
before.add(`${itemtype}::${id}`);
|
|
});
|
|
|
|
let resolved = false;
|
|
const observer = new MutationObserver(() => {
|
|
if (resolved) return;
|
|
const tiles = tilesContainer.querySelectorAll('[data-glpi-helpdesk-config-tile-id]');
|
|
for (const el of tiles) {
|
|
const itemtype = el.getAttribute('data-glpi-helpdesk-config-tile-itemtype');
|
|
const id = el.getAttribute('data-glpi-helpdesk-config-tile-id');
|
|
const key = `${itemtype}::${id}`;
|
|
if (!before.has(key)) {
|
|
resolved = true;
|
|
observer.disconnect();
|
|
saveSectionMapping(itemtype, id, sectionId);
|
|
return;
|
|
}
|
|
}
|
|
});
|
|
observer.observe(tilesContainer, { childList: true, subtree: true });
|
|
|
|
setTimeout(() => { if (!resolved) observer.disconnect(); }, 15000);
|
|
}
|
|
```
|
|
|
|
## 7. Anti-flash: esconder container antes da reorganização
|
|
|
|
Se a reorganização visual roda dentro de um `setTimeout` (debounce), o usuário vê os tiles em estado "plano" antes de aparecerem categorizados. Solução: adicionar a classe de loading **imediatamente** ao encontrar o container, não dentro do callback do timeout:
|
|
|
|
```js
|
|
function tryInit() {
|
|
const tilesContainer = document.querySelector('[data-glpi-helpdesk-config-tiles]');
|
|
if (!tilesContainer) return;
|
|
|
|
tilesContainer.classList.add('ts-loading'); // ← imediato
|
|
|
|
setTimeout(() => {
|
|
if (initialized) return;
|
|
initialized = true;
|
|
init(tilesContainer); // remove 'ts-loading' no final
|
|
}, 300);
|
|
}
|
|
```
|
|
|
|
Com CSS:
|
|
```css
|
|
[data-glpi-helpdesk-config-tiles].ts-loading {
|
|
opacity: 0;
|
|
transition: opacity 0.15s ease-in-out;
|
|
}
|
|
```
|
|
|
|
## 8. Templates de plugin têm namespace, não override direto
|
|
|
|
GLPI 11 carrega templates de plugin em namespace dedicado:
|
|
|
|
```php
|
|
// Glpi\Application\View\TemplateRenderer
|
|
$loader->addPath(Plugin::getPhpDir($plugin_key . '/templates'), $plugin_key);
|
|
```
|
|
|
|
Isso registra `@plugin_key/path/to/template.html.twig`. **Não substitui** o template core no mesmo path.
|
|
|
|
Para "estender" templates core, opções são:
|
|
|
|
1. **Hooks**: usar `Hooks::ADD_JAVASCRIPT` / `Hooks::ADD_CSS` para injetar comportamento via JS/CSS
|
|
2. **API server-side**: chamar APIs nativas do GLPI (`TilesManager::getInstance()`) em endpoints AJAX próprios
|
|
3. **DOM manipulation**: aceitar a fragilidade e robustecer com observers (este KB)
|
|
|
|
Override direto de template core via plugin **não é suportado oficialmente**.
|
|
|
|
## Referências do core GLPI 11
|
|
|
|
| Caminho | Conteúdo |
|
|
|---|---|
|
|
| `src/Glpi/Helpdesk/Tile/TilesManager.php` | Singleton com `getInstance()` e `getVisibleTilesForSession()` |
|
|
| `src/Glpi/Helpdesk/Tile/{FormTile,GlpiPageTile,ExternalPageTile}.php` | Implementações de tile |
|
|
| `src/Glpi/Controller/Helpdesk/IndexController.php` | Renderização do portal público |
|
|
| `templates/pages/admin/helpdesk_home_config_tiles.html.twig` | Template do admin grid |
|
|
| `templates/pages/helpdesk/index.html.twig` | Template do portal público |
|
|
| `src/Glpi/Application/View/TemplateRenderer.php` | Mecanismo de namespace de templates de plugin |
|