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); + } +}