From 4aad3c25d918f49c9d3a220b6a9129967887eb7b Mon Sep 17 00:00:00 2001 From: alisher372 Date: Wed, 26 Aug 2026 17:44:49 +0500 Subject: [PATCH] Add per-field hide condition (new "Conditions to hide field" tab) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently, PluginFieldsContainerDisplayCondition only lets you hide the whole block when the main object matches a condition. This PR adds a separate mechanism to hide a single field of the block instead, without affecting the rest of the block. A new tab, "Conditions to hide field", appears on the container's edit page next to the existing "Conditions to hide block" tab. The two are independent: a block-level condition still hides everything, a field-level condition only hides the one field you pick. What's new New table glpi_plugin_fields_fielddisplayconditions and new class PluginFieldsFieldDisplayCondition (CommonDBChild of PluginFieldsContainer), mirroring PluginFieldsContainerDisplayCondition and delegating to it for the parts that don't depend on the storage table. New required "Field to hide" dropdown on the condition form. PluginFieldsField::prepareHtmlFields() now filters out fields whose condition matches the current item, before rendering — covers tab, dom/domtab and massive-action paths through a single integration point. New ajax/field_display_condition.php and front/fielddisplaycondition.form.php controllers, two new Twig templates. container.class.php::defineTabs() registers the new tab. --- CHANGELOG.md | 6 + ajax/field_display_condition.php | 57 +++ front/fielddisplaycondition.form.php | 50 ++ hook.php | 2 + inc/container.class.php | 1 + inc/containerdisplaycondition.class.php | 2 +- inc/field.class.php | 15 + inc/fielddisplaycondition.class.php | 365 ++++++++++++++ templates/field_display_conditions.html.twig | 154 ++++++ .../forms/field_display_condition.html.twig | 103 ++++ tests/Units/FieldDisplayConditionTest.php | 474 ++++++++++++++++++ 11 files changed, 1228 insertions(+), 1 deletion(-) create mode 100644 ajax/field_display_condition.php create mode 100644 front/fielddisplaycondition.form.php create mode 100644 inc/fielddisplaycondition.class.php create mode 100644 templates/field_display_conditions.html.twig create mode 100644 templates/forms/field_display_condition.html.twig create mode 100644 tests/Units/FieldDisplayConditionTest.php diff --git a/CHANGELOG.md b/CHANGELOG.md index f7ff05c1..4be9bece 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). +## [Unreleased] + +### Added + +- Add a "Conditions to hide field" tab on containers, to hide a single field of a block based on a condition on the main object, instead of hiding the whole block. + ## [1.24.4] - 2026-08-06 ### Fixed diff --git a/ajax/field_display_condition.php b/ajax/field_display_condition.php new file mode 100644 index 00000000..79263d59 --- /dev/null +++ b/ajax/field_display_condition.php @@ -0,0 +1,57 @@ +. + * ------------------------------------------------------------------------- + * @copyright Copyright (C) 2013-2023 by Fields plugin team. + * @license GPLv2 https://www.gnu.org/licenses/gpl-2.0.html + * @link https://github.com/pluginsGLPI/fields + * ------------------------------------------------------------------------- + */ +Session::checkRight('config', READ); + +if (isset($_GET['action'])) { + if ($_GET['action'] === 'get_add_form') { + $field_display_condition = new PluginFieldsFieldDisplayCondition(); + $field_display_condition->showForm(0, $_GET); + } elseif ($_GET['action'] === 'get_edit_form') { + $field_display_condition = new PluginFieldsFieldDisplayCondition(); + $field_display_condition->getFromDB($_GET['id']); + $field_display_condition->showForm($_GET['id'], $_GET); + } +} elseif (isset($_POST['action'])) { + if ($_POST['action'] === 'get_itemtype_so') { + if (isset($_POST['itemtype']) && class_exists($_POST['itemtype'])) { + echo PluginFieldsContainerDisplayCondition::showItemtypeFieldForm($_POST['itemtype']); + } else { + echo ''; + } + } elseif ($_POST['action'] === 'get_condition_switch_so') { + if (isset($_POST['search_option_id']) && (isset($_POST['itemtype']) && class_exists($_POST['itemtype']))) { + echo PluginFieldsContainerDisplayCondition::showSearchOptionCondition($_POST['search_option_id'], $_POST['itemtype']); + } else { + echo ''; + } + } +} else { + throw new RuntimeException('Invalid request', 400); +} diff --git a/front/fielddisplaycondition.form.php b/front/fielddisplaycondition.form.php new file mode 100644 index 00000000..96b45f1a --- /dev/null +++ b/front/fielddisplaycondition.form.php @@ -0,0 +1,50 @@ +. + * ------------------------------------------------------------------------- + * @copyright Copyright (C) 2013-2023 by Fields plugin team. + * @license GPLv2 https://www.gnu.org/licenses/gpl-2.0.html + * @link https://github.com/pluginsGLPI/fields + * ------------------------------------------------------------------------- + */ + +Session::checkRight('config', READ); + +$field_display_condition = new PluginFieldsFieldDisplayCondition(); +if (isset($_POST['add'])) { + $field_display_condition->check(-1, CREATE, $_POST); + $field_display_condition->add($_POST); + Html::back(); +} elseif (isset($_POST['update'])) { + $field_display_condition->check($_POST['id'], UPDATE); + $field_display_condition->update($_POST); + Html::back(); +} elseif (isset($_POST['delete'])) { + $field_display_condition->check($_POST['id'], PURGE); + $field_display_condition->delete([ + 'id' => $_POST['id'], + ]); + Html::back(); +} + +Html::back(); diff --git a/hook.php b/hook.php index 409b9af2..3f768d54 100644 --- a/hook.php +++ b/hook.php @@ -67,6 +67,7 @@ function plugin_fields_install() $classesToInstall = [ PluginFieldsContainer::class, PluginFieldsContainerDisplayCondition::class, + PluginFieldsFieldDisplayCondition::class, PluginFieldsDropdown::class, PluginFieldsField::class, PluginFieldsLabelTranslation::class, @@ -137,6 +138,7 @@ function plugin_fields_uninstall() 'PluginFieldsProfile', 'PluginFieldsStatusOverride', 'PluginFieldsContainerDisplayCondition', + 'PluginFieldsFieldDisplayCondition', ]; foreach ($classesToUninstall as $class) { diff --git a/inc/container.class.php b/inc/container.class.php index 0c0b2ebb..8fef1287 100644 --- a/inc/container.class.php +++ b/inc/container.class.php @@ -610,6 +610,7 @@ public function defineTabs($options = []) $this->addStandardTab('PluginFieldsStatusOverride', $ong, $options); $this->addStandardTab('PluginFieldsProfile', $ong, $options); $this->addStandardTab('PluginFieldsContainerDisplayCondition', $ong, $options); + $this->addStandardTab('PluginFieldsFieldDisplayCondition', $ong, $options); $this->addStandardTab('PluginFieldsLabelTranslation', $ong, $options); return $ong; diff --git a/inc/containerdisplaycondition.class.php b/inc/containerdisplaycondition.class.php index 8c87b234..77b68ef8 100644 --- a/inc/containerdisplaycondition.class.php +++ b/inc/containerdisplaycondition.class.php @@ -204,7 +204,7 @@ public static function getDisplayConditionForContainer(int $container_id): array return $conditions; } - private function getItemtypesForContainer(int $container_id): array + public function getItemtypesForContainer(int $container_id): array { /** @var DBmysql $DB */ global $DB; diff --git a/inc/field.class.php b/inc/field.class.php index 07f25d69..d56e5fd9 100644 --- a/inc/field.class.php +++ b/inc/field.class.php @@ -1164,6 +1164,21 @@ public static function prepareHtmlFields( return false; } + //remove fields that must be hidden for this item, based on the + //"conditions to hide field" configured on the container + $displayCondition = new PluginFieldsFieldDisplayCondition(); + $fields = array_filter($fields, static function ($field) use ($displayCondition, $item) { + return $displayCondition->computeDisplayField( + $item, + $field['plugin_fields_containers_id'], + $field['id'], + ); + }); + + if (empty($fields)) { + return false; + } + // check if current profile can edit fields $right = PluginFieldsProfile::getRightOnContainer($_SESSION['glpiactiveprofile']['id'], $container_obj->getID()); if ($right < READ) { diff --git a/inc/fielddisplaycondition.class.php b/inc/fielddisplaycondition.class.php new file mode 100644 index 00000000..e80df9ab --- /dev/null +++ b/inc/fielddisplaycondition.class.php @@ -0,0 +1,365 @@ +. + * ------------------------------------------------------------------------- + * @copyright Copyright (C) 2013-2023 by Fields plugin team. + * @license GPLv2 https://www.gnu.org/licenses/gpl-2.0.html + * @link https://github.com/pluginsGLPI/fields + * ------------------------------------------------------------------------- + */ +use Glpi\Application\View\TemplateRenderer; +use Glpi\Features\Clonable; + +/** + * Conditions to hide a single field of a container (as opposed to + * PluginFieldsContainerDisplayCondition, which hides the whole block). + * + * This class deliberately mirrors PluginFieldsContainerDisplayCondition and + * delegates to it for everything that is independent of the storage table + * (comparison operators, search option rendering, etc). + */ +class PluginFieldsFieldDisplayCondition extends CommonDBChild +{ + use Clonable; + + public static $itemtype = PluginFieldsContainer::class; + + public static $items_id = 'plugin_fields_containers_id'; + + /** + * Install or update plugin base data. + * + * @param Migration $migration Migration instance + * @param string $version Plugin current version + * + * @return boolean + */ + public static function installBaseData(Migration $migration, $version) + { + /** @var DBmysql $DB */ + global $DB; + $default_charset = DBConnection::getDefaultCharset(); + $default_collation = DBConnection::getDefaultCollation(); + $default_key_sign = DBConnection::getDefaultPrimaryKeySignOption(); + $table = self::getTable(); + + if (!$DB->tableExists($table)) { + $migration->displayMessage(sprintf(__('Installing %s'), $table)); + $query = "CREATE TABLE IF NOT EXISTS `{$table}` ( + `id` INT {$default_key_sign} NOT NULL auto_increment, + `plugin_fields_containers_id` INT {$default_key_sign} NOT NULL DEFAULT '0', + `plugin_fields_fields_id` INT {$default_key_sign} NOT NULL DEFAULT '0', + `itemtype` VARCHAR(100) DEFAULT NULL, + `search_option` VARCHAR(255) DEFAULT NULL, + `condition` VARCHAR(255) DEFAULT NULL, + `value` VARCHAR(255) DEFAULT NULL, + `is_visible` TINYINT NOT NULL DEFAULT '0', + PRIMARY KEY (`id`), + KEY `plugin_fields_containers_id_itemtype` (`plugin_fields_containers_id`, `itemtype`), + KEY `plugin_fields_fields_id` (`plugin_fields_fields_id`) + ) ENGINE=InnoDB DEFAULT CHARSET={$default_charset} COLLATE={$default_collation} ROW_FORMAT=DYNAMIC;"; + $DB->doQuery($query); + } + + return true; + } + + public static function uninstall() + { + /** @var DBmysql $DB */ + global $DB; + $DB->doQuery('DROP TABLE IF EXISTS `' . self::getTable() . '`'); + + return true; + } + + public static function getTypeName($nb = 0) + { + return _n('Condition to hide field', 'Conditions to hide field', $nb, 'fields'); + } + + public function getTabNameForItem(CommonGLPI $item, $withtemplate = 0) + { + if (!($item instanceof CommonDBTM)) { + return ''; + } + + return self::createTabEntry( + self::getTypeName(Session::getPluralNumber()), + countElementsInTable(self::getTable(), ['plugin_fields_containers_id' => $item->getID()]), + null, + 'ti ti-forms-off', + ); + } + + public static function displayTabContentForItem(CommonGLPI $item, $tabnum = 1, $withtemplate = 0) + { + if ($item instanceof PluginFieldsContainer) { + self::showForTabContainer($item); + + return true; + } + + return false; + } + + public static function getDisplayConditionForContainer(int $container_id): array + { + /** @var DBmysql $DB */ + global $DB; + $iterator = $DB->request([ + 'SELECT' => [ + self::getTable() . '.*', + ], + 'FROM' => self::getTable(), + 'WHERE' => [ + 'plugin_fields_containers_id' => $container_id, + ], + ]); + + $conditions = []; + foreach ($iterator as $data) { + $conditions[] = $data; + } + + return $conditions; + } + + /** + * Get the list of fields of a container, to fill the mandatory + * "field to hide" dropdown. + * + * @param int $container_id Container's ID + * + * @return array [field_id => field_label] + */ + public static function getFieldsForContainer(int $container_id): array + { + $choices = []; + + $field_obj = new PluginFieldsField(); + $fields = $field_obj->find(['plugin_fields_containers_id' => $container_id], 'ranking'); + foreach ($fields as $field) { + $field['itemtype'] = PluginFieldsField::class; + $choices[$field['id']] = PluginFieldsLabelTranslation::getLabelFor($field); + } + + return $choices; + } + + /** + * Human readable label of the field targeted by a given condition row. + * + * @param array $condition_data Row from the fielddisplaycondition table + * + * @return string + */ + public static function getTargetFieldName(array $condition_data): string + { + $field = new PluginFieldsField(); + if (!$field->getFromDB($condition_data['plugin_fields_fields_id'])) { + return ''; + } + + return PluginFieldsLabelTranslation::getLabelFor($field->fields + ['itemtype' => PluginFieldsField::class]); + } + + /** + * Check whether a single field of a container must be hidden for the given item. + * + * @param CommonDBTM $item Item currently displayed + * @param int $container_id Container's ID + * @param int $field_id Field's ID (PluginFieldsField) + * + * @return bool true if the field must be displayed, false if it must be hidden + */ + public function computeDisplayField($item, $container_id, $field_id): bool + { + if (!$field_id) { + return true; + } + + $displayCondition = new self(); + $found_dc = $displayCondition->find([ + 'itemtype' => $item::class, + 'plugin_fields_containers_id' => $container_id, + 'plugin_fields_fields_id' => $field_id, + ]); + + if (!count($found_dc)) { + //no condition found -> display field + return true; + } + + foreach ($found_dc as $data) { + $displayCondition->getFromDB($data['id']); + if (!$displayCondition->checkCondition($item)) { + return false; + } + } + + return true; + } + + public function checkCondition($item) + { + $value = $this->fields['value']; + $condition = $this->fields['condition']; + $searchOption = Search::getOptions($item::class)[$this->fields['search_option']]; + + $fields = array_merge($item->fields, $item->input); + + switch ($condition) { + case PluginFieldsContainerDisplayCondition::SHOW_CONDITION_EQ: + // '=' + if ($value == $fields[$searchOption['linkfield']]) { + return false; + } + + break; + case PluginFieldsContainerDisplayCondition::SHOW_CONDITION_NE: + // '≠' + if ($value != $fields[$searchOption['linkfield']]) { + return false; + } + + break; + case PluginFieldsContainerDisplayCondition::SHOW_CONDITION_LT: + case PluginFieldsContainerDisplayCondition::SHOW_CONDITION_GT: + // '<'; + if ($fields[$searchOption['linkfield']] > $value) { + return false; + } + + break; + case PluginFieldsContainerDisplayCondition::SHOW_CONDITION_REGEX: + //'regex'; + if ( + PluginFieldsContainerDisplayCondition::checkRegex($value) + && preg_match_all($value . 'i', (string) $fields[$searchOption['linkfield']]) > 0 + ) { + return false; + } + + break; + case PluginFieldsContainerDisplayCondition::SHOW_CONDITION_UNDER: + $sons = getSonsOf($searchOption['table'], $value); + if (in_array($fields[$searchOption['linkfield']], $sons)) { + return false; + } + + break; + case PluginFieldsContainerDisplayCondition::SHOW_CONDITION_NOT_UNDER: + $sons = getSonsOf($searchOption['table'], $value); + if (!in_array($fields[$searchOption['linkfield']], $sons)) { + return false; + } + + break; + } + + return true; + } + + public function prepareInputForAdd($input) + { + // itemtype, search_option, condition, plugin_fields_fields_id must all be set + if (!isset($input['itemtype'], $input['search_option'], $input['condition']) || empty($input['plugin_fields_fields_id'])) { + Session::addMessageAfterRedirect( + __('You must specify a field, an item type, search option and condition.', 'fields'), + true, + ERROR, + ); + + return false; + } + + return parent::prepareInputForAdd($input); + } + + public function prepareInputForUpdate($input) + { + // itemtype, search_option, condition, plugin_fields_fields_id must all be set + if (!isset($input['itemtype'], $input['search_option'], $input['condition']) || empty($input['plugin_fields_fields_id'])) { + Session::addMessageAfterRedirect( + __('You must specify a field, an item type, search option and condition.', 'fields'), + true, + ERROR, + ); + + return false; + } + + return parent::prepareInputForUpdate($input); + } + + public static function showForTabContainer(CommonGLPI $item, $options = []) + { + if (!$item instanceof CommonDBTM) { + return; + } + + $displayCondition_id = $options['displaycondition_id'] ?? 0; + $display_condition = null; + + if ($displayCondition_id) { + $display_condition = new self(); + $display_condition->getFromDB($displayCondition_id); + } + + $container_id = $item->getID(); + $has_fields = countElementsInTable(PluginFieldsField::getTable(), [ + 'plugin_fields_containers_id' => $container_id, + ]) > 0; + $twig_params = [ + 'container_id' => $container_id, + 'field_display_conditions' => self::getDisplayConditionForContainer($container_id), + 'has_fields' => $has_fields, + ]; + + TemplateRenderer::getInstance()->display('@fields/field_display_conditions.html.twig', $twig_params); + } + + public function showForm($ID, array $options = []) + { + $container_id = $options['plugin_fields_containers_id']; + + $twig_params = [ + 'field_display_condition' => $this, + 'container_id' => $container_id, + 'container_itemtypes' => (new PluginFieldsContainerDisplayCondition())->getItemtypesForContainer($container_id), + 'container_fields' => self::getFieldsForContainer($container_id), + 'search_options' => $this->isNewItem() || empty($this->fields['itemtype']) + ? [] + : PluginFieldsContainerDisplayCondition::removeBlackListedOption(Search::getOptions($this->fields['itemtype']), $this->fields['itemtype']), + ]; + TemplateRenderer::getInstance()->display('@fields/forms/field_display_condition.html.twig', $twig_params); + + return true; + } + + public function getCloneRelations(): array + { + return []; + } +} diff --git a/templates/field_display_conditions.html.twig b/templates/field_display_conditions.html.twig new file mode 100644 index 00000000..2b6dcd02 --- /dev/null +++ b/templates/field_display_conditions.html.twig @@ -0,0 +1,154 @@ +{# + # ------------------------------------------------------------------------- + # Fields plugin for GLPI + # ------------------------------------------------------------------------- + # + # LICENSE + # + # This file is part of Fields. + # + # Fields is free software; you can redistribute it and/or modify + # it under the terms of the GNU General Public License as published by + # the Free Software Foundation; either version 2 of the License, or + # (at your option) any later version. + # + # Fields is distributed in the hope that it will be useful, + # but WITHOUT ANY WARRANTY; without even the implied warranty of + # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + # GNU General Public License for more details. + # + # You should have received a copy of the GNU General Public License + # along with Fields. If not, see . + # ------------------------------------------------------------------------- + # @copyright Copyright (C) 2013-2023 by Fields plugin team. + # @license GPLv2 https://www.gnu.org/licenses/gpl-2.0.html + # @link https://github.com/pluginsGLPI/fields + # ------------------------------------------------------------------------- + #} + +{% import 'components/form/fields_macros.html.twig' as fields %} +{% set rand = random() %} + +
+
+
+ {{ __('The engine is used to hide a single field of the block when the main object meets the condition', 'fields') }} +
+
+
+ {% if has_fields %} + + {% else %} +
+ {{ __('This block has no field yet.', 'fields') }} +
+ {% endif %} +
+ + {{ fields.largeTitle(_n('Condition to hide field', 'Conditions to hide field', get_plural_number(), 'fields'), '', false) }} +
+
+
+
+
+ + + + + + + + + + + + + {% if field_display_conditions|length > 0 %} + {% for field_display_condition in field_display_conditions %} + + + + + + + + + {% endfor %} + {% else %} + + + + {% endif %} + +
{{ __('Field to hide', 'fields') }}{{ __('Item type') }}{{ __('Field') }}{{ __('Condition') }}{{ __('Value') }}
{{ call('PluginFieldsFieldDisplayCondition::getTargetFieldName', [field_display_condition]) }}{{ field_display_condition.itemtype|itemtype_name }}{{ call('PluginFieldsContainerDisplayCondition::getFieldName', [field_display_condition.search_option, field_display_condition.itemtype]) }}{{ call('PluginFieldsContainerDisplayCondition::getConditionName', [field_display_condition.condition]) }}{{ call('PluginFieldsContainerDisplayCondition::getRawValue', [field_display_condition.search_option, field_display_condition.itemtype, field_display_condition.value]) }} +
+ + + + + +
+
{{ __('No item found') }}
+
{# .row #} +
{# .row #} +
{# .flex-row #} +
+
{# .card-body #} + +
diff --git a/templates/forms/field_display_condition.html.twig b/templates/forms/field_display_condition.html.twig new file mode 100644 index 00000000..c7c63d29 --- /dev/null +++ b/templates/forms/field_display_condition.html.twig @@ -0,0 +1,103 @@ +{# + # ------------------------------------------------------------------------- + # Fields plugin for GLPI + # ------------------------------------------------------------------------- + # + # LICENSE + # + # This file is part of Fields. + # + # Fields is free software; you can redistribute it and/or modify + # it under the terms of the GNU General Public License as published by + # the Free Software Foundation; either version 2 of the License, or + # (at your option) any later version. + # + # Fields is distributed in the hope that it will be useful, + # but WITHOUT ANY WARRANTY; without even the implied warranty of + # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + # GNU General Public License for more details. + # + # You should have received a copy of the GNU General Public License + # along with Fields. If not, see . + # ------------------------------------------------------------------------- + # @copyright Copyright (C) 2013-2023 by Fields plugin team. + # @license GPLv2 https://www.gnu.org/licenses/gpl-2.0.html + # @link https://github.com/pluginsGLPI/fields + # ------------------------------------------------------------------------- + #} + +{% import 'components/form/fields_macros.html.twig' as fields %} +{% set rand = random() %} + +
+ +
+
+
+
+
+ {{ fields.dropdownArrayField('plugin_fields_fields_id', field_display_condition.fields['plugin_fields_fields_id']|default(null), container_fields, __('Field to hide', 'fields'), {'rand': rand, 'display_emptychoice': true}) }} + + {{ fields.dropdownArrayField('itemtype', field_display_condition.fields['itemtype']|default(null), container_itemtypes, __('Item type'), {'rand': rand, 'display_emptychoice': true}) }} + {% do call('Ajax::updateItemOnSelectEvent', + [ + 'dropdown_itemtype' ~ rand, + 'results_fields', + get_plugin_web_dir('fields') ~ '/ajax/field_display_condition.php', + { + 'itemtype': '__VALUE__', + 'action' : 'get_itemtype_so', + } + ]) %} +
+ {% if not field_display_condition.isNewItem() %} + {{ fields.dropdownArrayField('search_option', field_display_condition.fields['search_option']|default(null), search_options, '', {'no_label': true, 'rand': rand, 'display_emptychoice': false}) }} + {% do call('Ajax::updateItemOnSelectEvent', + [ + 'dropdown_search_option' ~ rand, + 'results_condition', + get_plugin_web_dir('fields') ~ '/ajax/field_display_condition.php', + { + 'search_option_id' : '__VALUE__', + 'itemtype' : field_display_condition.fields['itemtype'], + 'action' : 'get_condition_switch_so' + } + ]) %} + {% endif %} +
+ +
+ {% if not field_display_condition.isNewItem() %} + {{ call( + 'PluginFieldsContainerDisplayCondition::showSearchOptionCondition', + [ + field_display_condition.fields['search_option'], + field_display_condition.fields['itemtype'], + field_display_condition.fields['condition'], + field_display_condition.fields['value'] + ] + )|raw }} + {% endif %} +
+ +
{# .row #} +
+ {% if not field_display_condition.isNewItem() %} + + + {% else %} + + {% endif %} + +
+
{# .row #} +
{# .flex-row #} +
+
{# .card-body #} +
diff --git a/tests/Units/FieldDisplayConditionTest.php b/tests/Units/FieldDisplayConditionTest.php new file mode 100644 index 00000000..6d26d005 --- /dev/null +++ b/tests/Units/FieldDisplayConditionTest.php @@ -0,0 +1,474 @@ +. + * ------------------------------------------------------------------------- + * @copyright Copyright (C) 2013-2023 by Fields plugin team. + * @license GPLv2 https://www.gnu.org/licenses/gpl-2.0.html + * @link https://github.com/pluginsGLPI/fields + * ------------------------------------------------------------------------- + */ + +declare(strict_types=1); + +namespace GlpiPlugin\Field\Tests\Units; + +use Glpi\Tests\DbTestCase; +use Glpi\Tests\GLPITestCase; +use GlpiPlugin\Field\Tests\FieldTestTrait; +use PluginFieldsContainer; +use PluginFieldsContainerDisplayCondition; +use PluginFieldsField; +use PluginFieldsFieldDisplayCondition; +use Search; +use Ticket; + +require_once __DIR__ . '/../FieldTestCase.php'; + +/** + * Tests covering the "hide field" condition feature (as opposed to + * PluginFieldsContainerDisplayCondition, which hides the whole block). + */ +final class FieldDisplayConditionTest extends DbTestCase +{ + use FieldTestTrait; + + public function setUp(): void + { + GLPITestCase::setUp(); + $this->login(); + } + + public function tearDown(): void + { + $this->tearDownFieldTest(); + + $condition = new PluginFieldsFieldDisplayCondition(); + foreach ($condition->find() as $row) { + $condition->delete($row, true); + } + + GLPITestCase::tearDown(); + } + + // ----------------------------------------------------------------------- + // Helpers + // ----------------------------------------------------------------------- + + /** + * Find the search option id whose linkfield is 'name' for the given itemtype, + * so tests can build conditions on the item's title/name without relying on a + * hardcoded search option id (which can differ between GLPI versions). + */ + private function getNameSearchOptionId(string $itemtype): int + { + foreach (Search::getOptions($itemtype) as $so_id => $so) { + if (($so['linkfield'] ?? null) === 'name' && ($so['table'] ?? null) === $itemtype::getTable()) { + return (int) $so_id; + } + } + + $this->fail(sprintf('Unable to find "name" search option for %s', $itemtype)); + } + + private function createHideFieldCondition( + int $field_id, + int $container_id, + string $itemtype, + int $search_option, + int $condition, + string $value, + ): PluginFieldsFieldDisplayCondition { + return $this->createItem(PluginFieldsFieldDisplayCondition::class, [ + 'plugin_fields_fields_id' => $field_id, + 'plugin_fields_containers_id' => $container_id, + 'itemtype' => $itemtype, + 'search_option' => $search_option, + 'condition' => $condition, + 'value' => $value, + ]); + } + + // ----------------------------------------------------------------------- + // getFieldsForContainer() + // ----------------------------------------------------------------------- + + public function testGetFieldsForContainerListsContainerFields(): void + { + $container = $this->createFieldContainer([ + 'label' => 'FieldsForContainer ' . $this->getUniqueString(), + 'type' => 'tab', + 'itemtypes' => [Ticket::class], + 'is_active' => 1, + 'entities_id' => 0, + 'is_recursive' => 1, + ]); + + $field = $this->createField([ + 'label' => 'Listed Field', + 'type' => 'text', + PluginFieldsContainer::getForeignKeyField() => $container->getID(), + 'ranking' => 1, + 'is_active' => 1, + 'is_readonly' => 0, + ]); + + $choices = PluginFieldsFieldDisplayCondition::getFieldsForContainer($container->getID()); + + $this->assertArrayHasKey($field->getID(), $choices); + } + + public function testGetFieldsForContainerIsEmptyForContainerWithoutFields(): void + { + $container = $this->createFieldContainer([ + 'label' => 'EmptyFieldsContainer ' . $this->getUniqueString(), + 'type' => 'tab', + 'itemtypes' => [Ticket::class], + 'is_active' => 1, + 'entities_id' => 0, + 'is_recursive' => 1, + ]); + + $choices = PluginFieldsFieldDisplayCondition::getFieldsForContainer($container->getID()); + + $this->assertSame([], $choices); + } + + // ----------------------------------------------------------------------- + // prepareInputForAdd() / prepareInputForUpdate() + // ----------------------------------------------------------------------- + + public function testAddWithoutTargetFieldIsRejected(): void + { + $container = $this->createFieldContainer([ + 'label' => 'NoTargetField ' . $this->getUniqueString(), + 'type' => 'tab', + 'itemtypes' => [Ticket::class], + 'is_active' => 1, + 'entities_id' => 0, + 'is_recursive' => 1, + ]); + + $so_id = $this->getNameSearchOptionId(Ticket::class); + + $condition = new PluginFieldsFieldDisplayCondition(); + $result = $condition->add([ + // plugin_fields_fields_id intentionally omitted + 'plugin_fields_containers_id' => $container->getID(), + 'itemtype' => Ticket::class, + 'search_option' => $so_id, + 'condition' => PluginFieldsContainerDisplayCondition::SHOW_CONDITION_EQ, + 'value' => 'whatever', + ]); + + $this->assertFalse($result); + } + + public function testAddWithoutItemtypeIsRejected(): void + { + $container = $this->createFieldContainer([ + 'label' => 'NoItemtype ' . $this->getUniqueString(), + 'type' => 'tab', + 'itemtypes' => [Ticket::class], + 'is_active' => 1, + 'entities_id' => 0, + 'is_recursive' => 1, + ]); + + $field = $this->createField([ + 'label' => 'Orphan Condition Field', + 'type' => 'text', + PluginFieldsContainer::getForeignKeyField() => $container->getID(), + 'ranking' => 1, + 'is_active' => 1, + 'is_readonly' => 0, + ]); + + $condition = new PluginFieldsFieldDisplayCondition(); + $result = $condition->add([ + 'plugin_fields_fields_id' => $field->getID(), + 'plugin_fields_containers_id' => $container->getID(), + // itemtype/search_option/condition intentionally omitted + ]); + + $this->assertFalse($result); + } + + // ----------------------------------------------------------------------- + // computeDisplayField() + // ----------------------------------------------------------------------- + + public function testComputeDisplayFieldReturnsTrueWhenNoConditionExists(): void + { + $container = $this->createFieldContainer([ + 'label' => 'NoCondition ' . $this->getUniqueString(), + 'type' => 'tab', + 'itemtypes' => [Ticket::class], + 'is_active' => 1, + 'entities_id' => 0, + 'is_recursive' => 1, + ]); + + $field = $this->createField([ + 'label' => 'Always Visible Field', + 'type' => 'text', + PluginFieldsContainer::getForeignKeyField() => $container->getID(), + 'ranking' => 1, + 'is_active' => 1, + 'is_readonly' => 0, + ]); + + $ticket = $this->createItem(Ticket::class, [ + 'name' => 'Any ticket', + 'content' => 'Test', + 'entities_id' => 0, + ]); + + $displayCondition = new PluginFieldsFieldDisplayCondition(); + $this->assertTrue( + $displayCondition->computeDisplayField($ticket, $container->getID(), $field->getID()), + ); + } + + public function testComputeDisplayFieldHidesFieldWhenConditionMatches(): void + { + $container = $this->createFieldContainer([ + 'label' => 'HideMatch ' . $this->getUniqueString(), + 'type' => 'tab', + 'itemtypes' => [Ticket::class], + 'is_active' => 1, + 'entities_id' => 0, + 'is_recursive' => 1, + ]); + + $field = $this->createField([ + 'label' => 'Conditionally Hidden Field', + 'type' => 'text', + PluginFieldsContainer::getForeignKeyField() => $container->getID(), + 'ranking' => 1, + 'is_active' => 1, + 'is_readonly' => 0, + ]); + + $so_id = $this->getNameSearchOptionId(Ticket::class); + + $this->createHideFieldCondition( + $field->getID(), + $container->getID(), + Ticket::class, + $so_id, + PluginFieldsContainerDisplayCondition::SHOW_CONDITION_EQ, + 'Trigger hide', + ); + + $matching_ticket = $this->createItem(Ticket::class, [ + 'name' => 'Trigger hide', + 'content' => 'Test', + 'entities_id' => 0, + ]); + + $displayCondition = new PluginFieldsFieldDisplayCondition(); + $this->assertFalse( + $displayCondition->computeDisplayField($matching_ticket, $container->getID(), $field->getID()), + 'Field must be hidden when the condition matches.', + ); + } + + public function testComputeDisplayFieldShowsFieldWhenConditionDoesNotMatch(): void + { + $container = $this->createFieldContainer([ + 'label' => 'ShowNoMatch ' . $this->getUniqueString(), + 'type' => 'tab', + 'itemtypes' => [Ticket::class], + 'is_active' => 1, + 'entities_id' => 0, + 'is_recursive' => 1, + ]); + + $field = $this->createField([ + 'label' => 'Conditionally Hidden Field 2', + 'type' => 'text', + PluginFieldsContainer::getForeignKeyField() => $container->getID(), + 'ranking' => 1, + 'is_active' => 1, + 'is_readonly' => 0, + ]); + + $so_id = $this->getNameSearchOptionId(Ticket::class); + + $this->createHideFieldCondition( + $field->getID(), + $container->getID(), + Ticket::class, + $so_id, + PluginFieldsContainerDisplayCondition::SHOW_CONDITION_EQ, + 'Trigger hide', + ); + + $non_matching_ticket = $this->createItem(Ticket::class, [ + 'name' => 'Do not trigger', + 'content' => 'Test', + 'entities_id' => 0, + ]); + + $displayCondition = new PluginFieldsFieldDisplayCondition(); + $this->assertTrue( + $displayCondition->computeDisplayField($non_matching_ticket, $container->getID(), $field->getID()), + 'Field must stay visible when the condition does not match.', + ); + } + + public function testComputeDisplayFieldWithZeroFieldIdAlwaysReturnsTrue(): void + { + $container = $this->createFieldContainer([ + 'label' => 'ZeroFieldId ' . $this->getUniqueString(), + 'type' => 'tab', + 'itemtypes' => [Ticket::class], + 'is_active' => 1, + 'entities_id' => 0, + 'is_recursive' => 1, + ]); + + $ticket = $this->createItem(Ticket::class, [ + 'name' => 'Any ticket', + 'content' => 'Test', + 'entities_id' => 0, + ]); + + $displayCondition = new PluginFieldsFieldDisplayCondition(); + $this->assertTrue($displayCondition->computeDisplayField($ticket, $container->getID(), 0)); + } + + // ----------------------------------------------------------------------- + // End-to-end: rendered fields via PluginFieldsField::prepareHtmlFields() + // ----------------------------------------------------------------------- + + public function testHiddenFieldIsExcludedFromRenderedOutput(): void + { + $container = $this->createFieldContainer([ + 'label' => 'RenderedHidden ' . $this->getUniqueString(), + 'type' => 'tab', + 'itemtypes' => [Ticket::class], + 'is_active' => 1, + 'entities_id' => 0, + 'is_recursive' => 1, + ]); + + $hidden_field = $this->createField([ + 'label' => 'Hidden In Output', + 'type' => 'text', + PluginFieldsContainer::getForeignKeyField() => $container->getID(), + 'ranking' => 1, + 'is_active' => 1, + 'is_readonly' => 0, + ]); + + $visible_field = $this->createField([ + 'label' => 'Stays Visible', + 'type' => 'text', + PluginFieldsContainer::getForeignKeyField() => $container->getID(), + 'ranking' => 2, + 'is_active' => 1, + 'is_readonly' => 0, + ]); + + $so_id = $this->getNameSearchOptionId(Ticket::class); + + $this->createHideFieldCondition( + $hidden_field->getID(), + $container->getID(), + Ticket::class, + $so_id, + PluginFieldsContainerDisplayCondition::SHOW_CONDITION_EQ, + 'Hide the field', + ); + + $ticket = $this->createItem(Ticket::class, [ + 'name' => 'Hide the field', + 'content' => 'Test', + 'entities_id' => 0, + ]); + + $field_obj = new PluginFieldsField(); + $fields = $field_obj->find(['plugin_fields_containers_id' => $container->getID()], 'ranking'); + + $html = PluginFieldsField::prepareHtmlFields($fields, $ticket); + + $this->assertIsString($html); + $this->assertStringNotContainsString( + $hidden_field->fields['name'], + $html, + 'Hidden field must not be present in the rendered output.', + ); + $this->assertStringContainsString( + $visible_field->fields['name'], + $html, + 'Non-targeted field must still be rendered.', + ); + } + + public function testFieldIsRenderedWhenConditionDoesNotMatch(): void + { + $container = $this->createFieldContainer([ + 'label' => 'RenderedVisible ' . $this->getUniqueString(), + 'type' => 'tab', + 'itemtypes' => [Ticket::class], + 'is_active' => 1, + 'entities_id' => 0, + 'is_recursive' => 1, + ]); + + $field = $this->createField([ + 'label' => 'Not Hidden Here', + 'type' => 'text', + PluginFieldsContainer::getForeignKeyField() => $container->getID(), + 'ranking' => 1, + 'is_active' => 1, + 'is_readonly' => 0, + ]); + + $so_id = $this->getNameSearchOptionId(Ticket::class); + + $this->createHideFieldCondition( + $field->getID(), + $container->getID(), + Ticket::class, + $so_id, + PluginFieldsContainerDisplayCondition::SHOW_CONDITION_EQ, + 'Hide the field', + ); + + $ticket = $this->createItem(Ticket::class, [ + 'name' => 'Some other title', + 'content' => 'Test', + 'entities_id' => 0, + ]); + + $field_obj = new PluginFieldsField(); + $fields = $field_obj->find(['plugin_fields_containers_id' => $container->getID()], 'ranking'); + + $html = PluginFieldsField::prepareHtmlFields($fields, $ticket); + + $this->assertIsString($html); + $this->assertStringContainsString($field->fields['name'], $html); + } +}