FIX #30801 Filter a dependent select list of extrafields when the field is edited alone (#39488)

This commit is contained in:
Jam Balaya 2026-08-15 10:04:31 +09:00 committed by GitHub
parent a291e0a779
commit 9b9e0889d5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 204 additions and 2 deletions

View file

@ -1186,9 +1186,10 @@ class ExtraFields
* @param int|CommonObject $object Current object or object ID. Preferably, pass the object itself.
* @param string $extrafieldsobjectkey The key to use to store retrieved data (commonly $object->table_element)
* @param int $mode 1=Used for search filters
* @param int $filteronparentvalue 1=Filter the values of a dependent list on the value currently saved for its parent list. Used when the field is edited alone (the parent list is not on the form, so the javascript that filters the list can't work).
* @return string
*/
public function showInputField($key, $value, $moreparam = '', $keysuffix = '', $keyprefix = '', $morecss = '', $object = 0, $extrafieldsobjectkey = '', $mode = 0)
public function showInputField($key, $value, $moreparam = '', $keysuffix = '', $keyprefix = '', $morecss = '', $object = 0, $extrafieldsobjectkey = '', $mode = 0, $filteronparentvalue = 0)
{
global $conf, $langs, $form, $hookmanager;
@ -1471,6 +1472,19 @@ class ExtraFields
if (!empty($valarray[1])) {
$parent = $valarray[1];
}
// When the field is edited alone (not into the whole form), the parent list is not on the page, so the
// javascript that filters a dependent list can't do its job. In this case, we filter the values here, on
// the value currently saved for the parent list. Note that the selected value is always kept, whatever the
// parent value is, so editing the field does not silently clear it.
if ($filteronparentvalue && !empty($parent) && (string) $value != (string) $key2) {
$tmpparent = explode(':', $parent, 2);
if (!empty($tmpparent[1]) && is_object($object)) {
$parentvalue = isset($object->array_options['options_'.$tmpparent[0]]) ? $object->array_options['options_'.$tmpparent[0]] : '';
if ((string) $parentvalue !== '' && (string) $parentvalue !== (string) $tmpparent[1]) {
continue;
}
}
}
$out .= '<option value="'.$key2.'"';
$out .= (((string) $value == (string) $key2) ? ' selected' : '');
$out .= (!empty($parent) ? ' parent="'.$parent.'"' : '');

View file

@ -311,7 +311,9 @@ if (empty($reshook) && !empty($object->table_element) && isset($extrafields->att
print '<input type="hidden" name="token" value="'.newToken().'">';
print '<input type="hidden" name="'.$fieldid.'" value="'.$object->id.'">';
print '<input type="hidden" name="page_y" value="">';
print $extrafields->showInputField($tmpkeyextra, $value, '', '', '', '', $object, $object->table_element);
// Last parameter is 1 because the field is edited alone: a dependent list can't be filtered by javascript
// here (its parent list is not on this form), so it must be filtered on the saved value of its parent.
print $extrafields->showInputField($tmpkeyextra, $value, '', '', '', '', $object, $object->table_element, 0, 1);
print '<input type="submit" class="button reposition" value="'.dolPrintHTMLForAttribute($langs->trans('Modify')).'">';

View file

@ -173,6 +173,9 @@ class AllTests
require_once dirname(__FILE__).'/CommonObjectTest.php';
$suite->addTestSuite('CommonObjectTest');
require_once dirname(__FILE__).'/ExtraFieldsTest.php';
$suite->addTestSuite('ExtraFieldsTest');
require_once dirname(__FILE__).'/ActionCommTest.php';
$suite->addTestSuite('ActionCommTest');
require_once dirname(__FILE__).'/SocieteTest.php';

View file

@ -0,0 +1,183 @@
<?php
/* Copyright (C) 2026 Jam <jambalaya.pyoncafe@gmail.com>
*
* This program 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 3 of the License, or
* (at your option) any later version.
*
* This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
* or see https://www.gnu.org/
*/
/**
* \file test/phpunit/ExtraFieldsTest.php
* \ingroup test
* \brief PHPUnit test
* \remarks To run this script as CLI: phpunit filename.php
*/
global $conf,$user,$langs,$db;
//define('TEST_DB_FORCE_TYPE','mysql'); // This is to force using mysql driver
//require_once 'PHPUnit/Autoload.php';
require_once dirname(__FILE__).'/../../htdocs/master.inc.php';
require_once dirname(__FILE__).'/../../htdocs/core/class/extrafields.class.php';
require_once dirname(__FILE__).'/../../htdocs/societe/class/societe.class.php';
require_once dirname(__FILE__).'/CommonClassTest.class.php';
if (empty($user->id)) {
print "Load permissions for admin user nb 1\n";
$user->fetch(1);
$user->loadRights();
}
$conf->global->MAIN_DISABLE_ALL_MAILS = 1;
/**
* Class for PHPUnit tests
*
* @backupGlobals disabled
* @backupStaticAttributes enabled
* @remarks backupGlobals must be disabled to have db,conf,user and lang not erased.
*/
class ExtraFieldsTest extends CommonClassTest
{
/**
* Build an ExtraFields object with a "select" list depending on another "select" list.
* The child list has one value for the parent value 'a', one for the parent value 'b',
* and one value with no dependency at all.
*
* @return ExtraFields Object with the definitions loaded in memory (nothing is written in database)
*/
private function getExtraFieldsWithDependentList()
{
global $db;
$extrafields = new ExtraFields($db);
$elementtype = 'societe';
foreach (array('parentlist', 'childlist') as $code) {
$extrafields->attributes[$elementtype]['type'][$code] = 'select';
$extrafields->attributes[$elementtype]['label'][$code] = $code;
$extrafields->attributes[$elementtype]['size'][$code] = '';
$extrafields->attributes[$elementtype]['default'][$code] = '';
$extrafields->attributes[$elementtype]['computed'][$code] = '';
$extrafields->attributes[$elementtype]['unique'][$code] = 0;
$extrafields->attributes[$elementtype]['required'][$code] = 0;
$extrafields->attributes[$elementtype]['perms'][$code] = '';
$extrafields->attributes[$elementtype]['langfile'][$code] = '';
$extrafields->attributes[$elementtype]['list'][$code] = '1';
$extrafields->attributes[$elementtype]['totalizable'][$code] = 0;
$extrafields->attributes[$elementtype]['help'][$code] = '';
$extrafields->attributes[$elementtype]['alwayseditable'][$code] = 0;
}
$extrafields->attributes[$elementtype]['param']['parentlist'] = array('options' => array('a' => 'Value A', 'b' => 'Value B'));
$extrafields->attributes[$elementtype]['param']['childlist'] = array('options' => array(
'childofa' => 'Child of A|parentlist:a',
'childofb' => 'Child of B|parentlist:b',
'nodepend' => 'No dependency',
));
return $extrafields;
}
/**
* Build a thirdparty holding a value for the parent list
*
* @param string $parentvalue Value saved for the parent list
* @param string $childvalue Value saved for the dependent list
* @return Societe Object not saved in database
*/
private function getObjectWithParentValue($parentvalue, $childvalue = '')
{
global $db;
$object = new Societe($db);
$object->array_options['options_parentlist'] = $parentvalue;
$object->array_options['options_childlist'] = $childvalue;
return $object;
}
/**
* When the whole form is shown, all the values must be output, whatever the parent value is:
* the filtering is done by javascript, on the value chosen in the parent list of the same form.
*
* @return void
*/
public function testShowInputFieldKeepsAllValuesOfADependentListByDefault()
{
$extrafields = $this->getExtraFieldsWithDependentList();
$object = $this->getObjectWithParentValue('a');
$out = $extrafields->showInputField('childlist', '', '', '', '', '', $object, 'societe');
print __METHOD__." out=".$out."\n";
$this->assertStringContainsString('value="childofa"', $out);
$this->assertStringContainsString('value="childofb"', $out);
$this->assertStringContainsString('parent="parentlist:a"', $out);
}
/**
* When the field is edited alone, the parent list is not on the form, so the values must be
* filtered on the value already saved for the parent list.
*
* @return void
*/
public function testShowInputFieldFiltersADependentListOnTheSavedParentValue()
{
$extrafields = $this->getExtraFieldsWithDependentList();
$object = $this->getObjectWithParentValue('a');
$out = $extrafields->showInputField('childlist', '', '', '', '', '', $object, 'societe', 0, 1);
print __METHOD__." out=".$out."\n";
$this->assertStringContainsString('value="childofa"', $out);
$this->assertStringNotContainsString('value="childofb"', $out);
// A value that does not depend on the parent list is always kept
$this->assertStringContainsString('value="nodepend"', $out);
}
/**
* If nothing is saved for the parent list, we can't filter, so all the values must be kept.
*
* @return void
*/
public function testShowInputFieldKeepsAllValuesWhenParentValueIsEmpty()
{
$extrafields = $this->getExtraFieldsWithDependentList();
$object = $this->getObjectWithParentValue('');
$out = $extrafields->showInputField('childlist', '', '', '', '', '', $object, 'societe', 0, 1);
print __METHOD__." out=".$out."\n";
$this->assertStringContainsString('value="childofa"', $out);
$this->assertStringContainsString('value="childofb"', $out);
}
/**
* The value currently saved must always be output, even when it does not match the parent value,
* otherwise editing the field would silently clear it.
*
* @return void
*/
public function testShowInputFieldKeepsTheSelectedValueNotMatchingTheParentValue()
{
$extrafields = $this->getExtraFieldsWithDependentList();
$object = $this->getObjectWithParentValue('a', 'childofb');
$out = $extrafields->showInputField('childlist', 'childofb', '', '', '', '', $object, 'societe', 0, 1);
print __METHOD__." out=".$out."\n";
$this->assertStringContainsString('value="childofb"', $out);
$this->assertStringContainsString('selected', $out);
$this->assertStringContainsString('value="childofa"', $out);
}
}