diff --git a/CHANGELOG.md b/CHANGELOG.md index 330c1f7..09516f3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/front/entity.form.php b/front/entity.form.php index 6c5506f..335b1c7 100644 --- a/front/entity.form.php +++ b/front/entity.form.php @@ -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', [ diff --git a/front/project.form.php b/front/project.form.php index 1dab564..2a59303 100644 --- a/front/project.form.php +++ b/front/project.form.php @@ -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) { diff --git a/inc/entity.class.php b/inc/entity.class.php index d167c20..5ea7dd5 100644 --- a/inc/entity.class.php +++ b/inc/entity.class.php @@ -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, diff --git a/inc/project.class.php b/inc/project.class.php index 2f7390e..a5a4ded 100644 --- a/inc/project.class.php +++ b/inc/project.class.php @@ -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 diff --git a/inc/redmineapi.class.php b/inc/redmineapi.class.php index 28a906f..71b3e00 100644 --- a/inc/redmineapi.class.php +++ b/inc/redmineapi.class.php @@ -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(), ]; } diff --git a/setup.php b/setup.php index 5738353..657c9a5 100644 --- a/setup.php +++ b/setup.php @@ -1,6 +1,6 @@