diff --git a/src/Entity/EntitySubqueue.php b/src/Entity/EntitySubqueue.php index 37bd2d31f139882fcbdb88bda07a3b6f747d6b49..773f58f3b0ad3268ada5784b4aabf8bfbfa32fd9 100644 --- a/src/Entity/EntitySubqueue.php +++ b/src/Entity/EntitySubqueue.php @@ -289,7 +289,21 @@ class EntitySubqueue extends EditorialContentEntityBase implements EntitySubqueu * {@inheritdoc} */ public function addItem(EntityInterface $entity) { - $this->get('items')->appendItem($entity->id()); + $items = $this->get('items'); + $queue = $this->getQueue(); + + // Reversed queues get new items at the top. Generic list operations like + // ::appendItem() must keep appending, since widgets and serializers rely + // on it. + if ($queue && $queue->isReversed()) { + $values = $items->getValue(); + array_unshift($values, ['target_id' => $entity->id()]); + $items->setValue($values); + } + else { + $items->appendItem($entity->id()); + } + return $this; } diff --git a/src/EntitySubqueueItemsFieldItemList.php b/src/EntitySubqueueItemsFieldItemList.php index e09c7b96eb42f9227f65365118598dcd0cf55959..f06abc28c367b0c88e0fc6b2db3aad2425c1bc08 100644 --- a/src/EntitySubqueueItemsFieldItemList.php +++ b/src/EntitySubqueueItemsFieldItemList.php @@ -11,24 +11,4 @@ use Drupal\Core\Field\EntityReferenceFieldItemList; */ class EntitySubqueueItemsFieldItemList extends EntityReferenceFieldItemList { - /** - * {@inheritdoc} - */ - public function appendItem($value = NULL) { - // Get the queue associated with the entity. - $queue = $this->getEntity()->getQueue(); - - // Check if the queue is available and is reversed. - if ($queue && $queue->isReversed()) { - // If the queue is reversed, new items should be added - // to the top of the queue. - $item = $this->createItem(0, $value); - array_unshift($this->list, $item); - return $item; - } - - // If the queue is not reversed or not available, use parent implementation. - return parent::appendItem($value); - } - } diff --git a/src/Plugin/Field/FieldWidget/EntityqueueDragtableWidget.php b/src/Plugin/Field/FieldWidget/EntityqueueDragtableWidget.php index 6612f114ffe2f2f138f8cb8145344f0f5ebc92c1..1e608fa5ccac1ea8ed719eb6e2a6f98738223daf 100644 --- a/src/Plugin/Field/FieldWidget/EntityqueueDragtableWidget.php +++ b/src/Plugin/Field/FieldWidget/EntityqueueDragtableWidget.php @@ -109,11 +109,13 @@ class EntityqueueDragtableWidget extends EntityReferenceAutocompleteWidget { // Stop offering the 'add item' control once a subqueue is full; the widget // can also be used on ordinary entity reference fields, which have no max. $at_max = FALSE; + $add_to_top = FALSE; $entity = $items->getEntity(); if ($entity instanceof EntitySubqueueInterface) { $queue = $entity->getQueue(); $max_size = $queue->getMaximumSize(); $at_max = $max_size && !$queue->getActAsQueue() && $field_state['items_count'] >= $max_size; + $add_to_top = $queue->isReversed(); } if ($wrapper_id && !$at_max && !$form_state->isProgrammed()) { @@ -132,6 +134,7 @@ class EntityqueueDragtableWidget extends EntityReferenceAutocompleteWidget { '#name' => strtr($id_prefix, '-', '_') . '_add_more', '#value' => $this->t('Add item'), '#attributes' => ['class' => ['field-add-more-submit']], + '#add_to_top' => $add_to_top, // Validate only the new item, not the whole field, so adding to a // required field is not blocked by its empty NotNull constraint. '#limit_validation_errors' => [array_merge($form['#parents'], [$field_name, 'add_more', 'new_item'])], @@ -262,7 +265,16 @@ class EntityqueueDragtableWidget extends EntityReferenceAutocompleteWidget { $user_input = $form_state->getUserInput(); $items_path = array_merge($parents, [$field_name]); $rows = NestedArray::getValue($user_input, $items_path) ?: []; - $rows[$delta] = ['target_id' => $row_value, '_weight' => $delta]; + + // Reversed queues show new items at the top, so the new row gets a + // weight below every existing row. Rows are sorted by weight both when + // the table is rendered and when the submitted values are extracted. + $weight = $delta; + if (!empty($button['#add_to_top'])) { + $weights = array_map('intval', array_column($rows, '_weight')); + $weight = $weights ? min($weights) - 1 : 0; + } + $rows[$delta] = ['target_id' => $row_value, '_weight' => $weight]; NestedArray::setValue($user_input, $items_path, $rows); // Clear the 'add item' autocomplete so it's blank for the next entry. diff --git a/tests/src/Functional/EntityQueueUiTest.php b/tests/src/Functional/EntityQueueUiTest.php index 33de2b391a030e3225272c25fdd84891c510b4d6..7d8f7c35a5e7de3a14dcd026baf8e9acc6b90065 100644 --- a/tests/src/Functional/EntityQueueUiTest.php +++ b/tests/src/Functional/EntityQueueUiTest.php @@ -115,7 +115,7 @@ class EntityQueueUiTest extends BrowserTestBase { $this->createContentType(['type' => 'article', 'name' => 'Article']); $first = $this->createNode(['type' => 'article', 'title' => 'First article']); $second = $this->createNode(['type' => 'article', 'title' => 'Second article']); - $this->createNodeQueue('q'); + $queue = $this->createNodeQueue('q'); $url = '/admin/structure/entityqueue/q/q'; $this->drupalGet($url); @@ -144,6 +144,17 @@ class EntityQueueUiTest extends BrowserTestBase { $this->assertCount(1, $this->removeButtons()); $this->submitForm([], 'Save'); $this->assertSame([(int) $first->id()], $this->queuedIds('q')); + + // A reversed queue adds each new item above the existing ones, and + // removing a row keeps that order. + $queue->set('queue_settings', ['min_size' => 0, 'max_size' => 0, 'act_as_queue' => FALSE, 'reverse' => TRUE])->save(); + $third = $this->createNode(['type' => 'article', 'title' => 'Third article']); + $this->drupalGet($url); + $this->submitForm(['items[add_more][new_item][target_id]' => 'Second article (' . $second->id() . ')'], 'Add item'); + $this->submitForm(['items[add_more][new_item][target_id]' => 'Third article (' . $third->id() . ')'], 'Add item'); + $this->submitForm([], 'items_0_remove_button'); + $this->submitForm([], 'Save'); + $this->assertSame([(int) $third->id(), (int) $second->id()], $this->queuedIds('q')); } /** diff --git a/tests/src/Kernel/EntitySubQueueAPITest.php b/tests/src/Kernel/EntitySubQueueAPITest.php index f990a5b6bd97422ea24fe18453d7423856761ca3..6d1d8a1dc17e8b93a8a07a343433636a3d51cf2e 100644 --- a/tests/src/Kernel/EntitySubQueueAPITest.php +++ b/tests/src/Kernel/EntitySubQueueAPITest.php @@ -111,4 +111,55 @@ class EntitySubQueueAPITest extends EntityKernelTestBase { $this->assertFalse($access_handler->createAccess('simple_queue', $admin)); } + /** + * Tests item order and deltas on a queue that adds new items to the top. + */ + public function testReversedQueueItems(): void { + $queue = EntityQueue::create([ + 'id' => 'reversed_queue', + 'label' => 'Reversed queue', + 'handler' => 'simple', + 'entity_settings' => [ + 'target_type' => 'entity_test', + ], + 'queue_settings' => [ + 'min_size' => 0, + 'max_size' => 0, + 'act_as_queue' => FALSE, + 'reverse' => TRUE, + ], + ]); + $queue->save(); + + $entities = []; + $ids = []; + for ($i = 0; $i < 3; $i++) { + $entity = EntityTest::create(); + $entity->save(); + $entities[] = $entity; + $ids[] = (int) $entity->id(); + } + + $subqueue = EntitySubqueue::load($queue->id()); + foreach ($entities as $entity) { + $subqueue->addItem($entity); + } + $subqueue->save(); + $items = EntitySubqueue::load($queue->id())->get('items'); + $this->assertSame(array_reverse($ids), array_map('intval', array_column($items->getValue(), 'target_id'))); + + // Generic list operations keep appending. ItemList::set() appends when the + // delta is new, and widgets append an empty trailing item. + $items->setValue([$ids[0]]); + $items->set(1, $ids[1]); + $items->set(2, $ids[2]); + $this->assertSame($ids, array_map('intval', array_column($items->getValue(), 'target_id'))); + $items->appendItem(); + $this->assertCount(4, $items); + $this->assertTrue($items->get(3)->isEmpty()); + foreach ($items as $delta => $item) { + $this->assertSame($delta, $item->getName()); + } + } + }