fix: dropdown gravava índice (Twig merge) em vez do ID real do projeto (1.5.3)

- opções do projeto Redmine montadas no PHP (Twig merge renumera chaves inteiras)
- aba mostra o NOME do projeto vinculado (não só o ID)
- botão Desvincular (projeto e entidade) + dropdown só com projetos ativos

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Gemini 2026-06-30 15:27:18 -03:00
parent ecf175728f
commit 6f844e7336
8 changed files with 119 additions and 81 deletions

View file

@ -4,6 +4,18 @@ Todas as mudanças relevantes deste plugin são documentadas aqui.
O formato segue [Keep a Changelog](https://keepachangelog.com/pt-BR/1.0.0/)
e o versionamento segue [SemVer](https://semver.org/lang/pt-BR/).
## [1.5.3]
### Corrigido
- **Vínculo gravava o ID errado do projeto Redmine**: o `merge` do Twig renumera
chaves inteiras, então o dropdown gravava o índice (posição) em vez do ID real
do projeto. As opções passam a ser montadas no PHP (preservam os IDs).
### Adicionado
- Botão **Desvincular** na aba Redmine (projeto e entidade) — antes só dava pra vincular.
- A visão "vinculado" mostra o **nome** do projeto Redmine (não só o ID).
- Dropdown de projetos lista **apenas projetos ativos** (evita vincular projeto fechado/arquivado).
## [1.5.2]
### Corrigido

View file

@ -35,6 +35,10 @@ if (isset($_POST["action"])) {
]);
Session::addMessageAfterRedirect(__('Categoria padrão salva com sucesso!', 'redmine'));
}
elseif ($action === 'unlink') {
$DB->delete('glpi_plugin_redmine_entities', ['entities_id' => $entities_id]);
Session::addMessageAfterRedirect(__('Entidade desvinculada do Redmine.', 'redmine'));
}
elseif ($action === 'process') {
$redmine_project_id = $_POST['redmine_project_id'] ?? '';

View file

@ -39,6 +39,11 @@ if (isset($_POST["action"])) {
]);
Session::addMessageAfterRedirect("Categoria padrão salva com sucesso!");
}
elseif ($action === 'unlink') {
global $DB;
$DB->delete('glpi_plugin_redmine_projects', ['projects_id' => $projects_id]);
Session::addMessageAfterRedirect("Projeto desvinculado do Redmine.");
}
elseif ($action === 'process') {
$redmine_project_id = $_POST['redmine_project_id'] ?? '';

View file

@ -34,6 +34,9 @@ class PluginRedmineEntity extends CommonDBTM {
$entities_id = $entity->getID();
$form_url = $CFG_GLPI['root_doc'] . '/plugins/redmine/front/entity.form.php';
// {id: name} montado no PHP (Twig merge corrompe chaves inteiras).
$project_options = PluginRedmineProject::getRedmineProjectOptions();
// Already linked?
$iterator = $DB->request([
'FROM' => 'glpi_plugin_redmine_entities',
@ -52,47 +55,30 @@ class PluginRedmineEntity extends CommonDBTM {
}
\Glpi\Application\View\TemplateRenderer::getInstance()->display('@redmine/project_linked.html.twig', [
'action_url' => $form_url,
'owner_field' => 'entities_id',
'owner_id' => $entities_id,
'redmine_project_id' => $redmineProjectId,
'cat_options' => $cat_options,
'default_category_id' => (int) ($link['default_category_id'] ?? 0),
'csrf_token_value' => Session::getNewCSRFToken(),
'action_url' => $form_url,
'owner_field' => 'entities_id',
'owner_id' => $entities_id,
'redmine_project_id' => $redmineProjectId,
'redmine_project_name' => $project_options[$redmineProjectId] ?? '',
'cat_options' => $cat_options,
'default_category_id' => (int) ($link['default_category_id'] ?? 0),
'csrf_token_value' => Session::getNewCSRFToken(),
]);
return;
}
// Not linked: show the link/create form (reuses the project tab template).
$config_it = $DB->request(['FROM' => 'glpi_plugin_redmine_configs', 'WHERE' => ['id' => 1]]);
$config = count($config_it) > 0 ? $config_it->current() : [];
$metadata = !empty($config['metadata_cache']) ? json_decode($config['metadata_cache'], true) : [];
$redmine_projects = $metadata['projects'] ?? [];
$modules = [
'issue_tracking' => 'Gerenciamento de Tarefas',
'time_tracking' => 'Gerenciamento de tempo',
'news' => 'Notícias',
'documents' => 'Documentos',
'files' => 'Arquivos',
'wiki' => 'Wiki',
'repository' => 'Repositório',
'boards' => 'Fóruns',
'calendar' => 'Calendário',
'gantt' => 'Gantt'
];
$description = \Glpi\RichText\RichText::getTextFromHtml($entity->fields['comment'] ?? '', false, true, true);
\Glpi\Application\View\TemplateRenderer::getInstance()->display('@redmine/project_form.html.twig', [
'action_url' => $form_url,
'owner_field' => 'entities_id',
'owner_id' => $entities_id,
'redmine_projects' => $redmine_projects,
'project_options' => PluginRedmineProject::getRedmineProjectOptions(true),
'project_name' => $entity->fields['name'],
'project_description' => $description,
'project_identifier' => 'glpi-ent-' . $entities_id,
'modules' => $modules,
'modules' => PluginRedmineProject::getModules(),
'csrf_token_value' => Session::getNewCSRFToken()
]);
}

View file

@ -32,8 +32,13 @@ class PluginRedmineProject extends CommonDBTM {
global $DB, $CFG_GLPI;
$projects_id = $project->getID();
$form_url = $CFG_GLPI['root_doc'] . '/plugins/redmine/front/project.form.php';
// Check if already linked
// Build {id: name} of Redmine projects from cache IN PHP (Twig's merge
// renumbers integer keys, which corrupts the option values).
$project_options = self::getRedmineProjectOptions();
// Already linked?
$iterator = $DB->request([
'FROM' => 'glpi_plugin_redmine_projects',
'WHERE' => ['projects_id' => $projects_id]
@ -43,7 +48,6 @@ class PluginRedmineProject extends CommonDBTM {
$link = $iterator->current();
$redmineProjectId = (int) $link['redmine_project_id'];
// Issue categories are per Redmine project.
$cat_options = [];
foreach (PluginRedmineRedmineapi::getProjectCategories($redmineProjectId) as $c) {
if (isset($c['id'], $c['name'])) {
@ -52,55 +56,73 @@ class PluginRedmineProject extends CommonDBTM {
}
\Glpi\Application\View\TemplateRenderer::getInstance()->display('@redmine/project_linked.html.twig', [
'action_url' => $CFG_GLPI['root_doc'] . '/plugins/redmine/front/project.form.php',
'owner_field' => 'projects_id',
'owner_id' => $projects_id,
'redmine_project_id' => $redmineProjectId,
'cat_options' => $cat_options,
'default_category_id' => (int) ($link['default_category_id'] ?? 0),
'csrf_token_value' => Session::getNewCSRFToken(),
'action_url' => $form_url,
'owner_field' => 'projects_id',
'owner_id' => $projects_id,
'redmine_project_id' => $redmineProjectId,
'redmine_project_name' => $project_options[$redmineProjectId] ?? '',
'cat_options' => $cat_options,
'default_category_id' => (int) ($link['default_category_id'] ?? 0),
'csrf_token_value' => Session::getNewCSRFToken(),
]);
return;
}
// Fetch Redmine Metadata Cache
$config_it = $DB->request(['FROM' => 'glpi_plugin_redmine_configs', 'WHERE' => ['id' => 1]]);
$config = count($config_it) > 0 ? $config_it->current() : [];
$metadata = [];
if (!empty($config['metadata_cache'])) {
$metadata = json_decode($config['metadata_cache'], true);
}
$redmine_projects = $metadata['projects'] ?? [];
$modules = [
'issue_tracking' => 'Gerenciamento de Tarefas',
'time_tracking' => 'Gerenciamento de tempo',
'news' => 'Notícias',
'documents' => 'Documentos',
'files' => 'Arquivos',
'wiki' => 'Wiki',
'repository' => 'Repositório',
'boards' => 'Fóruns',
'calendar' => 'Calendário',
'gantt' => 'Gantt'
];
// Strip HTML from description
$description = \Glpi\RichText\RichText::getTextFromHtml($project->fields['content'], false, true, true);
global $DB, $CFG_GLPI;
\Glpi\Application\View\TemplateRenderer::getInstance()->display('@redmine/project_form.html.twig', [
'action_url' => $CFG_GLPI['root_doc'] . '/plugins/redmine/front/project.form.php',
'owner_field' => 'projects_id',
'owner_id' => $projects_id,
'redmine_projects' => $redmine_projects,
'project_name' => $project->fields['name'],
'action_url' => $form_url,
'owner_field' => 'projects_id',
'owner_id' => $projects_id,
'project_options' => self::getRedmineProjectOptions(true),
'project_name' => $project->fields['name'],
'project_description' => $description,
'project_identifier' => 'glpi-proj-' . $projects_id,
'modules' => $modules,
'csrf_token_value' => Session::getNewCSRFToken()
'project_identifier' => 'glpi-proj-' . $projects_id,
'modules' => self::getModules(),
'csrf_token_value' => Session::getNewCSRFToken()
]);
}
/**
* {id: name} map of the cached Redmine projects (PHP preserves integer keys).
* @return array
*/
static function getRedmineProjectOptions($activeOnly = false) {
global $DB;
$it = $DB->request(['FROM' => 'glpi_plugin_redmine_configs', 'WHERE' => ['id' => 1]]);
$config = count($it) > 0 ? $it->current() : [];
$metadata = !empty($config['metadata_cache']) ? json_decode($config['metadata_cache'], true) : [];
$opts = [];
foreach (($metadata['projects'] ?? []) as $rp) {
if (!isset($rp['id'], $rp['name'])) {
continue;
}
// status: 1=ativo, 5=fechado, 9=arquivado. Issues só podem ser
// criadas em projetos ativos, então o dropdown só mostra esses.
if ($activeOnly && isset($rp['status']) && (int) $rp['status'] !== 1) {
continue;
}
$opts[$rp['id']] = $rp['name'];
}
return $opts;
}
/**
* Redmine project modules offered on creation.
* @return array
*/
static function getModules() {
return [
'issue_tracking' => 'Gerenciamento de Tarefas',
'time_tracking' => 'Gerenciamento de tempo',
'news' => 'Notícias',
'documents' => 'Documentos',
'files' => 'Arquivos',
'wiki' => 'Wiki',
'repository' => 'Repositório',
'boards' => 'Fóruns',
'calendar' => 'Calendário',
'gantt' => 'Gantt'
];
}
}

View file

@ -1,6 +1,6 @@
<?php
define('PLUGIN_REDMINE_VERSION', '1.5.2');
define('PLUGIN_REDMINE_VERSION', '1.5.3');
/**
* Load the Mindplace License class if the autoloader has not run yet.

View file

@ -5,11 +5,8 @@
<input type="hidden" name="_glpi_csrf_token" value="{{ csrf_token_value }}">
<input type="hidden" name="_glpi_simple_form" value="1">
{# Build a {id: name} options map from the cached Redmine projects #}
{% set rp_options = {} %}
{% for rp in redmine_projects %}
{% set rp_options = rp_options|merge({(rp.id): rp.name}) %}
{% endfor %}
{# project_options ({id: name}) é montado no PHP — não usar merge no Twig,
que renumera chaves inteiras e corrompe os valores das opções. #}
<div class="card shadow-sm">
<div class="card-header d-flex justify-content-between align-items-center">
@ -25,7 +22,7 @@
</div>
<div class="row">
{{ fields.dropdownArrayField('redmine_project_id', '', rp_options, __('Vincular a Projeto Existente', 'redmine'), {
{{ fields.dropdownArrayField('redmine_project_id', '', project_options, __('Vincular a Projeto Existente', 'redmine'), {
'display_emptychoice': true,
'full_width': true
}) }}
@ -51,7 +48,7 @@
'helper': __('Projetos públicos são visíveis para todos.', 'redmine')
}) }}
{{ fields.dropdownArrayField('parent_id', '', rp_options, __('Subprojeto de', 'redmine'), {
{{ fields.dropdownArrayField('parent_id', '', project_options, __('Subprojeto de', 'redmine'), {
'display_emptychoice': true,
'full_width': true
}) }}

View file

@ -1,8 +1,20 @@
{% import 'components/form/fields_macros.html.twig' as fields %}
<div class="alert alert-success mt-3 d-flex align-items-center">
<i class="ti ti-link me-2"></i>
{{ __('Projeto vinculado à issue Redmine de ID', 'redmine') }} <strong class="ms-1">#{{ redmine_project_id }}</strong>
<div class="alert alert-success mt-3 d-flex justify-content-between align-items-center">
<span>
<i class="ti ti-link me-2"></i>
{{ __('Vinculado ao projeto Redmine', 'redmine') }}
<strong class="ms-1">{{ redmine_project_name|default('') }} (#{{ redmine_project_id }})</strong>
</span>
<form method="post" action="{{ action_url }}" class="m-0"
onsubmit="return confirm('{{ __('Desvincular este projeto do Redmine?', 'redmine') }}');">
<input type="hidden" name="_glpi_csrf_token" value="{{ csrf_token_value }}">
<input type="hidden" name="{{ owner_field }}" value="{{ owner_id }}">
<input type="hidden" name="action" value="unlink">
<button type="submit" class="btn btn-sm btn-outline-danger">
<i class="ti ti-unlink me-1"></i>{{ __('Desvincular', 'redmine') }}
</button>
</form>
</div>
<form method="post" action="{{ action_url }}" class="mt-2">