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
63 changes: 61 additions & 2 deletions 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,45 @@ public static function preItem(CommonDBTM $item)
return false;
}

/**
* Values by container of an item being added
* or null if the item is not being added
*
* @return array<int, array<string, mixed>>|null
*/
private static function getPendingEntry(CommonDBTM $item): ?array
{
if (!self::$pending_values instanceof WeakMap || $item->isNewItem()) {
return null;
}

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

return null;
}

/**
* Whether the item is being added
*/
private static function isBeingAdded(CommonDBTM $item): bool
{
return self::getPendingEntry($item) !== null;
}

/**
* Container values of an item being added, not stored yet
*
* @return array<string, mixed>
*/
private static function getPendingValues(CommonDBTM $item, int $c_id): array
{
return self::getPendingEntry($item)[$c_id] ?? [];
}

/**
* 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 @@ -2628,7 +2683,8 @@ private static function checkContainerMandatory(CommonDBTM $item, PluginFieldsCo

//nothing submitted for this container in this request (e.g. untouched Tab)
//tab containers can't be filled before the item exists, so skip on creation
if ($item->isNewItem() && $loc_c->fields['type'] !== 'dom') {
//(including the updates triggered while the item is being added)
if (($item->isNewItem() || self::isBeingAdded($item)) && $loc_c->fields['type'] !== 'dom') {
return [];
}

Expand All @@ -2655,12 +2711,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);
Comment thread
MyvTsv marked this conversation as resolved.
Resolved
}
}

Expand Down
136 changes: 136 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,141 @@ 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 testCreateFromFormWithAssignedTechnicianAndMandatoryDomField(): void
{
$this->login();

$container = $this->createFieldContainer([
'label' => 'Mandatory Assign Form 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();

// the ticket form submits the dom container id
$_REQUEST['c_id'] = $container->getID();

$ticket = $this->createItem(Ticket::class, [
'name' => 'Ticket from form 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 testCreateWithAssignedTechnicianAndMandatoryTabField(): void
{
$this->login();

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

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

$this->simulateApiBoot();

// tab fields can't be filled before the item exists: the status update
// triggered by the assigned technician must not be blocked by them
$ticket = $this->createItem(Ticket::class, [
'name' => 'Ticket with mandatory tab field and technician',
'content' => 'Test creation',
'entities_id' => 0,
'_actors' => [
'assign' => [
['itemtype' => User::class, 'items_id' => getItemByTypeName(User::class, 'tech', true)],
],
],
]);
$this->hasNoSessionMessage(ERROR);
$this->assertSame(Ticket::ASSIGNED, $ticket->fields['status']);
$this->assertFalse($this->getPluginFieldValues(Ticket::class, $ticket->getID(), $container->getID()));
}

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