Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
### Fixed

- Fix search criterion value on multiple dropdown fields being reset after a search, which emptied the criterion when sorting or paging results
- Fix `Some mandatory fields are empty` error displayed when creating an item with an assigned technician

## [1.24.7] - 2026-10-28

Expand Down
41 changes: 40 additions & 1 deletion inc/container.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,11 @@ class PluginFieldsContainer extends CommonDBTM

public static $rightname = 'config';

/**
* Values of the items being added, not stored yet
*/
private static ?WeakMap $pending_values = null;

public static function canCreate(): bool
{
return self::canUpdate();
Expand Down Expand Up @@ -2424,6 +2429,7 @@ public static function findAllContainers($itemtype)
public static function postItemAdd(CommonDBTM $item)
{
PluginFieldsDestinationField::registerCreatedItem($item);
self::$pending_values?->offsetUnset($item);

if (array_key_exists('_plugin_fields_data', $item->input)) {
$container = new self();
Expand Down Expand Up @@ -2592,6 +2598,16 @@ public static function preItem(CommonDBTM $item)
}
}

if ($item->isNewItem()) {
$values_by_container = [];
foreach ($submitted_data as $data) {
$values_by_container[(int) $data['plugin_fields_containers_id']] = $data;
}

self::$pending_values ??= new WeakMap();
self::$pending_values[$item] = $values_by_container;
}

if ($submitted_data !== []) {
$item->input['_plugin_fields_data'] = $submitted_data;

Expand All @@ -2601,6 +2617,26 @@ public static function preItem(CommonDBTM $item)
return false;
}

/**
* Container values of an item being added, not stored yet
*
* @return array<string, mixed>
*/
private static function getPendingValues(CommonDBTM $item, int $c_id): array
{
if (!self::$pending_values instanceof WeakMap || $item->isNewItem()) {
return [];
}

foreach (self::$pending_values as $added_item => $values) {
if ($added_item::class === $item::class && $added_item->getID() === $item->getID()) {
return $values[$c_id] ?? [];
}
}

return [];
}

/**
* Validate a single container's mandatory fields for the given item, using
* either the values submitted in this request or, if none were submitted
Expand Down Expand Up @@ -2655,12 +2691,15 @@ private static function checkContainerMandatory(CommonDBTM $item, PluginFieldsCo
if (!$item->isNewItem()) {
$data['items_id'] = $item->getID();

// merge already persisted values to avoid false positives
// merge already persisted values to avoid false positives,
// or the values still to be stored when the item is being added
$classname = self::getClassname($item::getType(), $loc_c->fields['name']);
$dbu = new DbUtils();
$obj = $dbu->getItemForItemtype($classname);
if ($obj !== false && $obj->getFromDBByCrit(['items_id' => $item->getID()])) {
$data += $obj->fields;
} else {
$data += self::getPendingValues($item, $c_id);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tab containers are skipped on creation (line 2667, isNewItem() && type !== 'dom'), so they never get an entry in $pending_values; but during the nested status update the item is no longer new, so a tab container with a mandatory field is now validated, finds no row and no pending value, and still raises Some mandatory fields are empty and blocks the switch to Assigned.
This happens whenever the tab container is checked during that nested update (no c_id in the request, i.e. no dom container for the itemtype, or API / generic path), which is the same symptom the PR fixes for dom containers.
Treating an item that is still in $pending_values as being created for the tab skip would close it, with a test using a mandatory field in a tab container.

if (($item->isNewItem() || self::isBeingAdded($item)) && $loc_c->fields['type'] !== 'dom') {
    return [];
}

Dedicated test should be added

}
}

Expand Down
46 changes: 46 additions & 0 deletions tests/Units/ContainerItemUpdateTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
use Entity;
use Notification;
use Problem;
use User;

require_once __DIR__ . '/../FieldTestCase.php';

Expand Down Expand Up @@ -386,6 +387,51 @@ public function testCreateIsBlockedWhenMandatoryDomFieldIsMissing(): void
$this->assertSame('filled value', $plugin_row[$field_name]);
}

public function testCreateWithAssignedTechnicianAndMandatoryDomField(): void
{
$this->login();

$container = $this->createFieldContainer([
'label' => 'Mandatory Assign Container',
'type' => 'dom',
'itemtypes' => [Ticket::class],
'is_active' => 1,
'entities_id' => 0,
'is_recursive' => 1,
]);

$field = $this->createField([
'label' => 'Mandatory Field',
'type' => 'text',
PluginFieldsContainer::getForeignKeyField() => $container->getID(),
'ranking' => 1,
'is_active' => 1,
'is_readonly' => 0,
'mandatory' => 1,
]);
$field_name = $field->fields['name'];

$this->simulateApiBoot();

$ticket = $this->createItem(Ticket::class, [
'name' => 'Ticket with mandatory field and technician',
'content' => 'Test creation',
'entities_id' => 0,
'_actors' => [
'assign' => [
['itemtype' => User::class, 'items_id' => getItemByTypeName(User::class, 'tech', true)],
],
],
$field_name => 'filled value',
], [$field_name]);
$this->hasNoSessionMessage(ERROR);
$this->assertSame(Ticket::ASSIGNED, $ticket->fields['status']);

$plugin_row = $this->getPluginFieldValues(Ticket::class, $ticket->getID(), $container->getID());
$this->assertNotFalse($plugin_row);
$this->assertSame('filled value', $plugin_row[$field_name]);
}

public function testCreateIsNotBlockedForInventoryCreatedItem(): void
{
$this->login();
Expand Down
Loading