diff --git a/core/config/schema/core.workflow.schema.yml b/core/config/schema/core.workflow.schema.yml index f41af5c..77cf526 100644 --- a/core/config/schema/core.workflow.schema.yml +++ b/core/config/schema/core.workflow.schema.yml @@ -34,6 +34,9 @@ core.workflow.*: type: mapping label: 'Transition from state to state' mapping: + label: + type: label + label: 'Transition label' from: type: sequence label: 'From state IDs' @@ -43,6 +46,6 @@ core.workflow.*: to: type: string label: 'To state ID' - label: - type: label - label: 'Transition label' + weight: + type: integer + label: 'Weight' diff --git a/core/lib/Drupal/Core/Workflow/Entity/Workflow.php b/core/lib/Drupal/Core/Workflow/Entity/Workflow.php index b57751d..b9520e6 100644 --- a/core/lib/Drupal/Core/Workflow/Entity/Workflow.php +++ b/core/lib/Drupal/Core/Workflow/Entity/Workflow.php @@ -249,9 +249,15 @@ public function addTransition($transition_id, $label, array $from_state_ids, $to if (!$this->hasState($to_state_id)) { throw new \InvalidArgumentException("The state '$to_state_id' does not exist in workflow '{$this->id()}'"); } + // Always add to the end. + $weight = array_reduce($this->transitions, function ($carry, $transition_info) { + return $carry = max($carry, $transition_info['weight'] + 1); + }, 0); $this->transitions[$transition_id] = [ - 'to' => $to_state_id, 'label' => $label, + 'from' => [], + 'to' => $to_state_id, + 'weight' => $weight ]; try { @@ -275,22 +281,17 @@ public function getTransitions(array $transition_ids = NULL) { /** @var \Drupal\Core\Workflow\TransitionInterface[] $transitions */ $transitions = array_combine($transition_ids, array_map([$this, 'getTransition'], $transition_ids)); if (count($transitions) > 1) { - // Sort transitions by from state weights and then to state weights and - // then transition labels. - $from_weights = $to_weights = $labels = []; + // Sort transitions by weights and then labels. + $weights = $labels = []; foreach ($transitions as $id => $transition) { - // @todo sort out what to do about from weights. - // $from_weights[$id] = $transition->from()->weight(); - $from_weights[$id] = 0; - $to_weights[$id] = $transition->to()->weight(); + $weights[$id] = $transition->weight(); $labels[$id] = $transition->label(); } array_multisort( - $from_weights, SORT_NUMERIC, SORT_ASC, - $to_weights, SORT_NUMERIC, SORT_ASC, + $weights, SORT_NUMERIC, SORT_ASC, $labels, SORT_NATURAL, SORT_ASC ); - $transitions = array_replace($from_weights, $transitions); + $transitions = array_replace($weights, $transitions); } return $transitions; } @@ -307,7 +308,8 @@ public function getTransition($transition_id) { $transition_id, $this->transitions[$transition_id]['label'], $this->transitions[$transition_id]['from'], - $this->transitions[$transition_id]['to'] + $this->transitions[$transition_id]['to'], + $this->transitions[$transition_id]['weight'] ); return $this->getTypePlugin()->decorateTransition($transition); } @@ -360,7 +362,7 @@ public function hasTransitionFromStateToState($from_state_id, $to_state_id) { */ protected function getTransitionIdFromStateToState($from_state_id, $to_state_id) { foreach ($this->transitions as $transition_id => $transition) { - if (!empty($transition['from']) && in_array($from_state_id, $transition['from'], TRUE) && $transition['to'] === $to_state_id) { + if (in_array($from_state_id, $transition['from'], TRUE) && $transition['to'] === $to_state_id) { return $transition_id; } } @@ -383,6 +385,19 @@ public function setTransitionLabel($transition_id, $label) { /** * {@inheritdoc} */ + public function setTransitionWeight($transition_id, $weight) { + if (isset($this->transitions[$transition_id])) { + $this->transitions[$transition_id]['weight'] = $weight; + } + else { + throw new \InvalidArgumentException("The transition '$transition_id' does not exist in workflow '{$this->id()}'"); + } + return $this; + } + + /** + * {@inheritdoc} + */ public function setTransitionFromStates($transition_id, array $from_state_ids) { if (!isset($this->transitions[$transition_id])) { throw new \InvalidArgumentException("The transition '$transition_id' does not exist in workflow '{$this->id()}'"); diff --git a/core/lib/Drupal/Core/Workflow/Transition.php b/core/lib/Drupal/Core/Workflow/Transition.php index ad5274a..8ed3890 100644 --- a/core/lib/Drupal/Core/Workflow/Transition.php +++ b/core/lib/Drupal/Core/Workflow/Transition.php @@ -26,6 +26,13 @@ class Transition implements TransitionInterface { protected $id; /** + * The transition's label. + * + * @var string + */ + protected $label; + + /** * The transition's from state IDs. * * @var string[] @@ -40,11 +47,11 @@ class Transition implements TransitionInterface { protected $toStateId; /** - * The transition's label. + * The transition's weight. * - * @var string + * @var int */ - protected $label; + protected $weight; /** * Transition constructor. @@ -59,13 +66,16 @@ class Transition implements TransitionInterface { * A list of from state IDs. * @param string $to_state_id * The to state ID. + * @param int $weight + * (optional) The transition's weight. Defaults to 0. */ - public function __construct(WorkflowInterface $workflow, $id, $label, array $from_state_ids, $to_state_id) { + public function __construct(WorkflowInterface $workflow, $id, $label, array $from_state_ids, $to_state_id, $weight = 0) { $this->workflow = $workflow; $this->id = $id; + $this->label = $label; $this->fromStateIds = $from_state_ids; $this->toStateId = $to_state_id; - $this->label = $label; + $this->weight = $weight; } /** @@ -96,4 +106,11 @@ public function to() { return $this->workflow->getState($this->toStateId); } + /** + * {@inheritdoc} + */ + public function weight() { + return $this->weight; + } + } diff --git a/core/lib/Drupal/Core/Workflow/TransitionInterface.php b/core/lib/Drupal/Core/Workflow/TransitionInterface.php index 9e03c8f..a819310 100644 --- a/core/lib/Drupal/Core/Workflow/TransitionInterface.php +++ b/core/lib/Drupal/Core/Workflow/TransitionInterface.php @@ -43,4 +43,12 @@ public function from(); */ public function to(); + /** + * Gets the transition's weight. + * + * @return string + * The transition's weight. + */ + public function weight(); + } diff --git a/core/lib/Drupal/Core/Workflow/WorkflowInterface.php b/core/lib/Drupal/Core/Workflow/WorkflowInterface.php index e42baf5..68873f5 100644 --- a/core/lib/Drupal/Core/Workflow/WorkflowInterface.php +++ b/core/lib/Drupal/Core/Workflow/WorkflowInterface.php @@ -233,7 +233,23 @@ public function hasTransitionFromStateToState($from_state_id, $to_state_id); public function setTransitionLabel($transition_id, $label); /** - * Sets a transition's label. + * Sets a transition's weight. + * + * @param string $transition_id + * The transition ID. + * @param int $weight + * The transition's weight. + * + * @return \Drupal\Core\Workflow\WorkflowInterface + * The workflow entity. + * + * @throws \InvalidArgumentException + * Thrown if the transition does not exist. + */ + public function setTransitionWeight($transition_id, $weight); + + /** + * Sets a transition's from states. * * @param string $transition_id * The transition ID. diff --git a/core/modules/content_moderation/config/install/core.workflow.typical.yml b/core/modules/content_moderation/config/install/core.workflow.typical.yml index c2b5464..af6ff86 100644 --- a/core/modules/content_moderation/config/install/core.workflow.typical.yml +++ b/core/modules/content_moderation/config/install/core.workflow.typical.yml @@ -16,33 +16,38 @@ states: label: Published weight: 0 transitions: + archive: + label: Archive + from: + - published + to: archived + weight: 2 archived_draft: + label: 'Un-archive to Draft' from: - archived to: draft - label: 'Un-archive to Draft' + weight: 3 archived_published: + label: Un-archive from: - archived to: published - label: Un-archive + weight: 4 create_new_draft: + label: 'Create New Draft' from: - draft - published to: draft - label: 'Create New Draft' + weight: 0 publish: + label: Publish from: - draft - published to: published - label: Publish - archive: - from: - - published - to: archived - label: Archive + weight: 1 type: content_moderation type_settings: states: diff --git a/core/modules/system/tests/modules/workflow_type_test/src/DecoratedTransition.php b/core/modules/system/tests/modules/workflow_type_test/src/DecoratedTransition.php index 3777f40..9d6d4db 100644 --- a/core/modules/system/tests/modules/workflow_type_test/src/DecoratedTransition.php +++ b/core/modules/system/tests/modules/workflow_type_test/src/DecoratedTransition.php @@ -73,4 +73,11 @@ public function to() { return $this->transition->to(); } + /** + * {@inheritdoc} + */ + public function weight() { + return $this->transition->weight(); + } + } diff --git a/core/modules/workflow_ui/src/Form/WorkflowEditForm.php b/core/modules/workflow_ui/src/Form/WorkflowEditForm.php index 955a1b2..1b1d7ab 100644 --- a/core/modules/workflow_ui/src/Form/WorkflowEditForm.php +++ b/core/modules/workflow_ui/src/Form/WorkflowEditForm.php @@ -103,6 +103,7 @@ public function form(array $form, FormStateInterface $form_state) { $header = [ 'label' => $this->t('Label'), + 'weight' => $this->t('Weight'), 'from' => $this->t('From'), 'to' => $this->t('To'), 'operations' => $this->t('Operations') @@ -122,6 +123,13 @@ public function form(array $form, FormStateInterface $form_state) { '#header' => $header, '#title' => $this->t('Transitions'), '#empty' => $this->t('There are no transitions yet.'), + '#tabledrag' => [ + [ + 'action' => 'order', + 'relationship' => 'sibling', + 'group' => 'transition-weight', + ], + ], ]; foreach ($workflow->getTransitions() as $transition) { $links['edit'] = [ @@ -135,7 +143,16 @@ public function form(array $form, FormStateInterface $form_state) { 'attributes' => ['aria-label' => $this->t('Delete \'@transition\' transition', ['@transition' => $transition->label()])], ]; $form['transitions_container']['transitions'][$transition->id()] = [ + '#attributes' => ['class' => ['draggable']], 'label' => ['#markup' => $transition->label()], + '#weight' => $transition->weight(), + 'weight' => [ + '#type' => 'weight', + '#title' => t('Weight for @title', ['@title' => $transition->label()]), + '#title_display' => 'invisible', + '#default_value' => $transition->weight(), + '#attributes' => ['class' => ['transition-weight']], + ], 'from' => [ '#theme' => 'item_list', '#items' => array_map([State::class, 'labelCallback'], $transition->from()), @@ -179,6 +196,9 @@ protected function copyFormValuesToEntity(EntityInterface $entity, array $form, foreach ($values['states'] as $state_id => $state_values) { $entity->setStateWeight($state_id, $state_values['weight']); } + foreach ($values['transitions'] as $transition_id => $transition_values) { + $entity->setTransitionWeight($transition_id, $transition_values['weight']); + } } } diff --git a/core/modules/workflow_ui/tests/src/Functional/WorkflowUiTest.php b/core/modules/workflow_ui/tests/src/Functional/WorkflowUiTest.php index 5f3c1c4..29dbc38 100644 --- a/core/modules/workflow_ui/tests/src/Functional/WorkflowUiTest.php +++ b/core/modules/workflow_ui/tests/src/Functional/WorkflowUiTest.php @@ -131,7 +131,7 @@ public function testWorkflowCreation() { $this->submitForm(['id' => 'save_and_publish', 'label' => 'Save and publish', 'from[published]' => 'published', 'to' => 'published'], 'Save'); $this->assertSession()->pageTextContains('Created Save and publish transition.'); // Edit the new transition and try to add an existing transition. - $this->clickLink('Edit', 3); + $this->clickLink('Edit', 4); $this->submitForm(['from[draft]' => 'draft'], 'Save'); $this->assertSession()->pageTextContains('The transition from Draft to Live already exists.'); @@ -155,11 +155,23 @@ public function testWorkflowCreation() { $this->assertEquals('published', $workflow->getInitialState()->id()); $this->drupalGet('admin/config/workflow/workflows/manage/test'); $this->submitForm(['states[draft][weight]' => '-1'], 'Save'); + $workflow = $workflow_storage->loadUnchanged('test'); + $this->assertEquals('draft', $workflow->getInitialState()->id()); + // This will take us to the list of workflows, so we need to edit the // workflow again. $this->clickLink('Edit'); + + // Ensure that weight changes the transition ordering. + $this->assertEquals(['publish', 'create_new_draft'], array_keys($workflow->getTransitions())); + $this->drupalGet('admin/config/workflow/workflows/manage/test'); + $this->submitForm(['transitions[create_new_draft][weight]' => '-1'], 'Save'); $workflow = $workflow_storage->loadUnchanged('test'); - $this->assertEquals('draft', $workflow->getInitialState()->id()); + $this->assertEquals(['create_new_draft', 'publish'], array_keys($workflow->getTransitions())); + + // This will take us to the list of workflows, so we need to edit the + // workflow again. + $this->clickLink('Edit'); // Delete the Draft state. $this->clickLink('Delete'); diff --git a/core/tests/Drupal/Tests/Core/Workflow/WorkflowTest.php b/core/tests/Drupal/Tests/Core/Workflow/WorkflowTest.php index aeb592d..1f20a68 100644 --- a/core/tests/Drupal/Tests/Core/Workflow/WorkflowTest.php +++ b/core/tests/Drupal/Tests/Core/Workflow/WorkflowTest.php @@ -275,8 +275,12 @@ public function testAddTransition() { $this->assertFalse($workflow->getState('draft')->canTransitionTo('published')); $workflow->addTransition('publish', 'Publish', ['draft'], 'published'); $this->assertTrue($workflow->getState('draft')->canTransitionTo('published')); + $this->assertEquals(0, $workflow->getTransition('publish')->weight()); $this->assertTrue($workflow->hasTransition('publish')); $this->assertFalse($workflow->hasTransition('draft')); + + $workflow->addTransition('save_publish', 'Save', ['published'], 'published'); + $this->assertEquals(1, $workflow->getTransition('save_publish')->weight()); } /** @@ -352,6 +356,7 @@ public function testAddTransitionMissingToException() { /** * @covers ::getTransitions + * @covers ::setTransitionWeight */ public function testGetTransitions() { $workflow = new Workflow(['id' => 'test', 'type' => 'test_type'], 'workflow'); @@ -374,11 +379,11 @@ public function testGetTransitions() { $this->assertArrayEquals(['a_a', 'a_b'], array_keys($workflow->getTransitions())); // The order of states is by weight. - $workflow->setStateWeight('b', -1); + $workflow->setTransitionWeight('a_b', -1); $this->assertArrayEquals(['a_b', 'a_a'], array_keys($workflow->getTransitions())); // If all weights are equal it will fallback to labels. - $workflow->setStateWeight('b', 0); + $workflow->setTransitionWeight('a_b', 0); $this->assertArrayEquals(['a_a', 'a_b'], array_keys($workflow->getTransitions())); $workflow->setTransitionLabel('a_b', 'A B'); $this->assertArrayEquals(['a_b', 'a_a'], array_keys($workflow->getTransitions())); @@ -510,6 +515,30 @@ public function testSetTransitionLabelException() { } /** + * @covers ::setTransitionWeight + */ + public function testSetTransitionWeight() { + $workflow = new Workflow(['id' => 'test', 'type' => 'test_type'], 'workflow'); + $workflow + ->addState('draft', 'Draft') + ->addState('published', 'Published') + ->addTransition('publish', 'Publish', ['draft'], 'published'); + $this->assertEquals(0, $workflow->getTransition('publish')->weight()); + $workflow->setTransitionWeight('publish', 10); + $this->assertEquals(10, $workflow->getTransition('publish')->weight()); + } + + /** + * @covers ::setTransitionWeight + */ + public function testSetTransitionWeightException() { + $this->setExpectedException(\InvalidArgumentException::class, "The transition 'draft-published' does not exist in workflow 'test'"); + $workflow = new Workflow(['id' => 'test', 'type' => 'test_type'], 'workflow'); + $workflow->addState('published', 'Published'); + $workflow->setTransitionWeight('draft-published', 10); + } + + /** * @covers ::setTransitionFromStates */ public function testSetTransitionFromStates() {