fix: custom fields obrigatórios na criação de projeto + identifier lowercase (1.5.6)

- form das abas Redmine exibe/envia custom_field_values (Tipo de Projeto etc.)
- createProjectFull normaliza o identificador (minúsculas)
- sync da aba de projeto usa syncMetadata completo (não apaga mais o cache)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Gemini 2026-07-02 09:53:23 -03:00
parent 913fd82272
commit c8765ca5ff
8 changed files with 120 additions and 13 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.6]
### Corrigido
- **Criação de projeto Redmine falhava** em instâncias com campos personalizados
obrigatórios de projeto (ex.: "Tipo de Projeto", "Tipo de Faturamento"): o form
das abas Redmine agora exibe esses campos (dropdown para listas) e os envia
como `custom_field_values`.
- **Identificador** é normalizado (minúsculas) na criação — "GNA" virava
"Identificador não é válido".
- O "Sincronizar Metadados" da aba de projeto fazia um sync parcial que apagava
trackers/status/prioridades do cache; agora usa o sync completo (inclui usuários).
## [1.5.5]
### Corrigido

View file

@ -64,6 +64,20 @@ if (isset($_POST["action"])) {
$payload['enabled_module_names'] = $_POST['enabled_module_names'];
}
// Custom fields de projeto (obrigatórios em alguns Redmine).
// O empty-choice do dropdown envia '0' — descartar junto com vazios.
if (!empty($_POST['custom_field_values']) && is_array($_POST['custom_field_values'])) {
$cfv = [];
foreach ($_POST['custom_field_values'] as $cfid => $cfval) {
if ($cfval !== '' && $cfval !== null && $cfval !== '0' && $cfval !== 0) {
$cfv[(string) (int) $cfid] = $cfval;
}
}
if (!empty($cfv)) {
$payload['custom_field_values'] = $cfv;
}
}
$new_redmine_id = PluginRedmineRedmineapi::createProjectFull($payload);
if ($new_redmine_id) {
$DB->insert('glpi_plugin_redmine_entities', [

View file

@ -14,22 +14,21 @@ if (isset($_POST["action"])) {
}
if ($action === 'sync') {
// Fetch projects from Redmine and cache them
$redmine_projects = PluginRedmineRedmineapi::getProjects();
if ($redmine_projects !== false) {
// Full metadata sync (partial sync used to wipe trackers/statuses from the cache).
$metadata = PluginRedmineRedmineapi::syncMetadata();
if ($metadata !== false) {
global $DB;
$metadata = ['projects' => $redmine_projects];
$DB->update('glpi_plugin_redmine_configs', [
'metadata_cache' => json_encode($metadata)
], [
'id' => 1
]);
PluginRedmineRedmineapi::syncUsers();
Session::addMessageAfterRedirect("Metadados sincronizados com sucesso!");
} else {
Session::addMessageAfterRedirect("Erro ao sincronizar com Redmine.", false, ERROR);
}
}
}
elseif ($action === 'set_category') {
global $DB;
$DB->update('glpi_plugin_redmine_projects', [
@ -72,6 +71,20 @@ if (isset($_POST["action"])) {
$payload['enabled_module_names'] = $_POST['enabled_module_names'];
}
// Custom fields de projeto (obrigatórios em alguns Redmine).
// O empty-choice do dropdown envia '0' — descartar junto com vazios.
if (!empty($_POST['custom_field_values']) && is_array($_POST['custom_field_values'])) {
$cfv = [];
foreach ($_POST['custom_field_values'] as $cfid => $cfval) {
if ($cfval !== '' && $cfval !== null && $cfval !== '0' && $cfval !== 0) {
$cfv[(string) (int) $cfid] = $cfval;
}
}
if (!empty($cfv)) {
$payload['custom_field_values'] = $cfv;
}
}
$new_redmine_id = PluginRedmineRedmineapi::createProjectFull($payload);
if ($new_redmine_id) {

View file

@ -75,6 +75,7 @@ class PluginRedmineEntity extends CommonDBTM {
'owner_field' => 'entities_id',
'owner_id' => $entities_id,
'project_options' => PluginRedmineProject::getRedmineProjectOptions(true),
'custom_fields' => PluginRedmineProject::getProjectCustomFieldDefs(),
'project_name' => $entity->fields['name'],
'project_description' => $description,
'project_identifier' => 'glpi-ent-' . $entities_id,

View file

@ -75,6 +75,7 @@ class PluginRedmineProject extends CommonDBTM {
'owner_field' => 'projects_id',
'owner_id' => $projects_id,
'project_options' => self::getRedmineProjectOptions(true),
'custom_fields' => self::getProjectCustomFieldDefs(),
'project_name' => $project->fields['name'],
'project_description' => $description,
'project_identifier' => 'glpi-proj-' . $projects_id,
@ -107,6 +108,35 @@ class PluginRedmineProject extends CommonDBTM {
return $opts;
}
/**
* Project custom field definitions for the create form (from metadata cache).
* Some Redmine setups make these mandatory on project creation.
* @return array of ['id','name','field_format','is_required','options'=>{value: value}]
*/
static function getProjectCustomFieldDefs() {
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) : [];
$defs = [];
foreach (($metadata['project_custom_fields'] ?? []) as $f) {
$options = [];
foreach (($f['possible_values'] ?? []) as $pv) {
if (isset($pv['value'])) {
$options[$pv['value']] = $pv['value'];
}
}
$defs[] = [
'id' => (int) $f['id'],
'name' => $f['name'],
'field_format' => $f['field_format'] ?? 'string',
'is_required' => (bool) ($f['is_required'] ?? false),
'options' => $options,
];
}
return $defs;
}
/**
* Redmine project modules offered on creation.
* @return array

View file

@ -201,6 +201,10 @@ class PluginRedmineRedmineapi {
* @return int|false Returns project ID on success
*/
public static function createProjectFull($projectData) {
// Redmine identifiers must be lowercase (a-z, 0-9, dashes/underscores).
if (isset($projectData['identifier'])) {
$projectData['identifier'] = strtolower(preg_replace('/[^a-zA-Z0-9\-_]/', '-', $projectData['identifier']));
}
$payload = [
'project' => $projectData
];
@ -296,6 +300,22 @@ class PluginRedmineRedmineapi {
return ($result && isset($result['roles'])) ? $result['roles'] : [];
}
/**
* Get the custom field definitions that apply to PROJECTS (admin only).
* Needed because some Redmine setups make them mandatory on creation.
* @return array
*/
public static function getProjectCustomFields() {
$result = self::get("/custom_fields.json");
$out = [];
foreach (($result['custom_fields'] ?? []) as $f) {
if (($f['customized_type'] ?? '') === 'project') {
$out[] = $f;
}
}
return $out;
}
/**
* Ensure a Redmine user is a member of a project (idempotent).
* Impersonation requires the user to be a project member with a role.
@ -363,12 +383,13 @@ class PluginRedmineRedmineapi {
return false; // API unreachable / not configured
}
return [
'projects' => $projects,
'trackers' => self::getTrackers(),
'statuses' => self::getStatuses(),
'priorities' => self::getPriorities(),
'activities' => self::getActivities(),
'roles' => self::getRoles(),
'projects' => $projects,
'trackers' => self::getTrackers(),
'statuses' => self::getStatuses(),
'priorities' => self::getPriorities(),
'activities' => self::getActivities(),
'roles' => self::getRoles(),
'project_custom_fields' => self::getProjectCustomFields(),
];
}

View file

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

View file

@ -58,6 +58,22 @@
'values': ['issue_tracking', 'time_tracking'],
'full_width': true
}) }}
{# Campos personalizados de projeto do Redmine (alguns são obrigatórios na criação) #}
{% for cf in custom_fields|default([]) %}
{% if cf.field_format == 'list' and cf.options is not empty %}
{{ fields.dropdownArrayField('custom_field_values[' ~ cf.id ~ ']', '', cf.options, cf.name, {
'display_emptychoice': true,
'full_width': true,
'required': cf.is_required
}) }}
{% else %}
{{ fields.textField('custom_field_values[' ~ cf.id ~ ']', '', cf.name, {
'full_width': true,
'required': cf.is_required
}) }}
{% endif %}
{% endfor %}
</div>
</div>