knowledge-base/records/plugin-dev/KB-PLUGIN-038-glpi11-frontend-assets-monaco-public.md
Gemini 235e4679ac KB-PLUGIN-038: GLPI 11 — Monaco, fim do jQuery UI, add_css e estáticos em public/
Gotchas de porte GLPI 10 → 11 aprendidos no plugin butterfly:
CodeMirror removido (padrão GLPI.Monaco.createEditor + formdata),
jQuery UI extinto, add_css interpola valor uma vez (+?v= do core),
e estáticos de plugin só servidos de public/ sem o segmento /public.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 18:07:37 -03:00

5.1 KiB

id title domain tags status severity created_at updated_at applies_to related_records
KB-PLUGIN-038 GLPI 11 — Assets de plugin: Monaco no lugar do CodeMirror, fim do jQuery UI, add_css/add_javascript e estáticos só em public/ plugin-dev
glpi11
plugin
assets
monaco
codemirror
jquery-ui
add_css
add_javascript
public
gotcha
active high 2026-07-03 2026-07-03
plugins GLPI 11.x portados do GLPI 9/10
qualquer plugin que injete CSS/JS ou sirva arquivos estáticos
KB-PLUGIN-004
KB-PLUGIN-028
KB-PLUGIN-036

GLPI 11 — Assets de plugin: Monaco, fim do jQuery UI, add_css e public/

Aprendizados do porte do plugin butterfly (2.1.0, GLPI 10 → 11.0.8, 2026-07-03). Quatro armadilhas independentes que atingem qualquer plugin vindo do GLPI 9/10.

1. CodeMirror foi removido — o editor de código do core é o Monaco

public/lib/ do GLPI 11 não tem mais CodeMirror; tem monaco-editor/monaco.js. Qualquer CodeMirror.fromTextArea(...) de plugin quebra com ReferenceError silencioso no console (o form continua funcionando como textarea puro).

Carregar a lib

monaco é chave válida do mecanismo $CFG_GLPI['javascript'] (testado em Html.php ~linha 1178, in_array('monaco', $jslibs)):

// setup.php, em plugin_init_<key>()
$CFG_GLPI['javascript']['config']['config'][] = 'monaco';

Padrão de uso (o mesmo do core em templates/pages/admin/entity/custom_ui.html.twig)

echo '<div id="meu_editor" style="height: 300px"></div>';
echo Html::scriptBlock('
   $(function() {
      window.GLPI.Monaco.createEditor("meu_editor", "scss", ' . json_encode($valor, JSON_UNESCAPED_UNICODE | JSON_HEX_TAG | JSON_HEX_AMP) . ', []).then(() => {
         $("#meu_editor").closest("form").on("formdata", (e) => {
            const editors = window.monaco.editor.getEditors().filter(
               (ed) => ed._domElement.id === "meu_editor"
            );
            if (editors.length) {
               e.originalEvent.formData.delete("meu_campo");
               e.originalEvent.formData.append("meu_campo", editors[0].getValue());
            }
         });
      });
   });
');

Pontos-chave:

  • O Monaco não usa textarea: é um <div> container + sync no submit via evento formdata (por isso o form precisa ser um <form> nativo).
  • Manter um <textarea class="d-none" name="meu_campo"> com o valor original como fallback: se a lib não carregar, o valor antigo é preservado no POST (o handler formdata faz delete + append quando o editor existe).
  • Em Twig, preferir a macro fields_macros.html.twig do core que já embute esse padrão.

2. jQuery UI não existe mais

$(...).tabs(), .dialog(), .sortable() etc. quebram com TypeError: ... is not a function. Plugins antigos costumam ter tabs jQuery UI na tela de config. Substituir por tabs do Tabler/Bootstrap ou remover. (jQuery em si continua disponível.)

3. add_css / add_javascript: valor é interpolado UMA vez e ganha ?v= do core

Glpi\Application\View\Extension\PluginExtension monta "/plugins/{$plugin}/{$file}" e acrescenta 'version' => Plugin::getPluginFilesVersion($plugin) (vira ?v=<hash> na URL). Consequências:

  • Truques de objeto com __toString para injetar query string (padrão "file_exists chama primeiro, concatenação chama depois" do GLPI 9/10) não funcionam mais — o cast acontece uma única vez. Usar string simples.
  • Cache busting por mudança de config deve ser resolvido pelo próprio endpoint (ex.: redirect para ?_=<timestamp>) ou aceitando o ?v= por versão.
  • Continua valendo KB-PLUGIN-004: a chave do array é o plugin key exato.

4. Estáticos de plugin: só o que está em public/ é servido

RequestRouterTrait (core) só expõe recursos de plugin que estejam no diretório public/ do plugin, acessados sem o segmento /public na URL:

plugins/meuplugin/public/pics/logo.png  →  GET /plugins/meuplugin/pics/logo.png

Arquivos fora de public/ (ex.: pics/, css/ na raiz do plugin) retornam 404 — mesmo que .htaccess os liberasse no Apache do GLPI 10. Scripts PHP legados (front/*.php) continuam roteados pelo LegacyFileLoadController (com as firewall strategies registradas no setup.php).

Migração típica: git mv pics public/pics e trocar referências relativas (../plugins/...) por $CFG_GLPI['root_doc'] . '/plugins/<key>/...'.

Validação padrão

# lib presente?
ls /var/www/glpi/public/lib | grep -i monaco        # existe
ls /var/www/glpi/public/lib | grep -i codemirror    # NÃO existe
# estático servido?
curl -s -o /dev/null -w "%{http_code}" http://localhost/plugins/<key>/pics/x.png

Regra para agentes

Ao portar plugin GLPI 9/10 → 11, fazer grep por: CodeMirror, .tabs(, .dialog(, new .*FileVersion|__toString, '../plugins/, e conferir se todo asset estático referenciado por URL está dentro de public/. Cada acerto desses é quebra silenciosa (só aparece no console do browser ou como 404).

Classificação

  • Tipo: gotchas de porte GLPI 10 → 11 (front-end/assets).
  • Reutilização: obrigatória em qualquer porte de plugin com UI.