diff --git a/core/modules/content_moderation/src/EntityOperations.php b/core/modules/content_moderation/src/EntityOperations.php index 3f66100..2b11b5e 100644 --- a/core/modules/content_moderation/src/EntityOperations.php +++ b/core/modules/content_moderation/src/EntityOperations.php @@ -3,6 +3,7 @@ namespace Drupal\content_moderation; use Drupal\content_moderation\Entity\ContentModerationState as ContentModerationStateEntity; +use Drupal\Core\Database\Connection; use Drupal\Core\DependencyInjection\ContainerInjectionInterface; use Drupal\Core\Entity\Display\EntityViewDisplayInterface; use Drupal\Core\Entity\EntityInterface; @@ -55,6 +56,13 @@ class EntityOperations implements ContainerInjectionInterface { protected $bundleInfo; /** + * The database connection. + * + * @var \Drupal\Core\Database\Connection + */ + protected $database; + + /** * Constructs a new EntityOperations object. * * @param \Drupal\content_moderation\ModerationInformationInterface $moderation_info @@ -67,13 +75,16 @@ class EntityOperations implements ContainerInjectionInterface { * The revision tracker. * @param \Drupal\Core\Entity\EntityTypeBundleInfoInterface $bundle_info * The entity bundle information service. + * @param \Drupal\Core\Database\Connection $database + * The database connection. */ - public function __construct(ModerationInformationInterface $moderation_info, EntityTypeManagerInterface $entity_type_manager, FormBuilderInterface $form_builder, RevisionTrackerInterface $tracker, EntityTypeBundleInfoInterface $bundle_info) { + public function __construct(ModerationInformationInterface $moderation_info, EntityTypeManagerInterface $entity_type_manager, FormBuilderInterface $form_builder, RevisionTrackerInterface $tracker, EntityTypeBundleInfoInterface $bundle_info, Connection $database) { $this->moderationInfo = $moderation_info; $this->entityTypeManager = $entity_type_manager; $this->formBuilder = $form_builder; $this->tracker = $tracker; $this->bundleInfo = $bundle_info; + $this->database = $database; } /** @@ -85,7 +96,8 @@ public static function create(ContainerInterface $container) { $container->get('entity_type.manager'), $container->get('form_builder'), $container->get('content_moderation.revision_tracker'), - $container->get('entity_type.bundle.info') + $container->get('entity_type.bundle.info'), + $container->get('database') ); } @@ -145,6 +157,25 @@ public function entityUpdate(EntityInterface $entity) { $this->updateOrCreateFromEntity($entity); $this->setLatestRevision($entity); } + + if ($entity->getEntityTypeId() == 'workflow' && $entity->get('type') === 'content_moderation') { + $deleted_states = array_diff_key($entity->original->getStates(), $entity->getStates()); + /** @var \Drupal\content_moderation\ContentModerationState $deleted_state */ + foreach ($deleted_states as $deleted_state) { + $new_state = $deleted_state->isPublishedState() ? 'published' : 'draft'; + $content_moderation_state_definition = $this->entityTypeManager->getDefinition('content_moderation_state'); + $this->database + ->update($content_moderation_state_definition->getDataTable()) + ->condition('moderation_state', $deleted_state->id()) + ->fields(['moderation_state' => $new_state]) + ->execute(); + $this->database + ->update($content_moderation_state_definition->getRevisionDataTable()) + ->condition('moderation_state', $deleted_state->id()) + ->fields(['moderation_state' => $new_state]) + ->execute(); + } + } } /** diff --git a/core/modules/content_moderation/src/EntityTypeInfo.php b/core/modules/content_moderation/src/EntityTypeInfo.php index 8f6ac4d..cda8d5f 100644 --- a/core/modules/content_moderation/src/EntityTypeInfo.php +++ b/core/modules/content_moderation/src/EntityTypeInfo.php @@ -292,6 +292,27 @@ public function formAlter(array &$form, FormStateInterface $form_state, $form_id $form['actions']['submit']['#submit'][] = [EntityTypeInfo::class, 'bundleFormRedirect']; } } + + if ($form_id == 'workflow_state_delete_form') { + /** @var \Drupal\workflows\WorkflowInterface $workflow */ + list($workflow, $state) = $form_state->getBuildInfo()['args']; + if ($workflow->get('type') === 'content_moderation') { + $count = $this->entityTypeManager + ->getStorage('content_moderation_state') + ->getQuery() + ->count() + ->condition('moderation_state', $state) + ->execute(); + if ($count > 0) { + $state = $workflow->getState($state); + $new_state = $state->isPublishedState() ? $workflow->getState('published') : $workflow->getState('draft'); + $form['description']['#markup'] = $this->t('This moderation state is assigned to @count. Deleting it will revert these to @new_state.', [ + '@count' => $this->stringTranslation->formatPlural($count, '1 item', '@count items'), + '@new_state' => $new_state->label() + ]); + } + } + } } /** diff --git a/core/modules/content_moderation/tests/src/Functional/ContentModerationWorkflowTypeTest.php b/core/modules/content_moderation/tests/src/Functional/ContentModerationWorkflowTypeTest.php index ff37bd1..13c4f34 100644 --- a/core/modules/content_moderation/tests/src/Functional/ContentModerationWorkflowTypeTest.php +++ b/core/modules/content_moderation/tests/src/Functional/ContentModerationWorkflowTypeTest.php @@ -2,7 +2,10 @@ namespace Drupal\Tests\content_moderation\Functional; +use Drupal\node\Entity\Node; +use Drupal\node\Entity\NodeType; use Drupal\Tests\BrowserTestBase; +use Drupal\workflows\Entity\Workflow; /** * Test the workflow type plugin in the content_moderation module. @@ -75,4 +78,56 @@ public function testNewWorkflow() { $this->assertSession()->fieldDisabled('type_settings[content_moderation][default_revision]'); } + /** + * Test users are warned before deleting states with existing content. + */ + public function testStateDeleteWarning() { + NodeType::create([ + 'type' => 'example', + 'label' => 'Test type', + ])->save(); + + $workflow = Workflow::load('editorial'); + $workflow->addState('revert_to_draft', 'Revert to draft'); + $workflow->addState('revert_to_published', 'Revert to published'); + $workflow->getTypePlugin()->setConfiguration([ + 'states' => [ + 'revert_to_draft' => [ + 'published' => FALSE, + 'default_revision' => FALSE, + ], + 'revert_to_published' => [ + 'published' => TRUE, + 'default_revision' => TRUE, + ], + ], + ]); + $workflow->getTypePlugin()->addEntityTypeAndBundle('node', 'example'); + $workflow->save(); + + $this->drupalGet('/admin/config/workflow/workflows/manage/editorial/state/revert_to_draft/delete'); + $this->assertSession()->pageTextNotContains('This moderation state is assigned'); + + $this->drupalGet('/admin/config/workflow/workflows/manage/editorial/state/revert_to_published/delete'); + $this->assertSession()->pageTextNotContains('This moderation state is assigned'); + + // Create example content for both states. + Node::create([ + 'type' => 'example', + 'title' => 'Test node', + 'moderation_state' => 'revert_to_published', + ])->save(); + Node::create([ + 'type' => 'example', + 'title' => 'Test node', + 'moderation_state' => 'revert_to_draft', + ])->save(); + + $this->drupalGet('/admin/config/workflow/workflows/manage/editorial/state/revert_to_draft/delete'); + $this->assertSession()->pageTextContains('This moderation state is assigned to 1 item. Deleting it will revert these to Draft.'); + + $this->drupalGet('/admin/config/workflow/workflows/manage/editorial/state/revert_to_published/delete'); + $this->assertSession()->pageTextContains('This moderation state is assigned to 1 item. Deleting it will revert these to Published.'); + } + } diff --git a/core/modules/content_moderation/tests/src/Kernel/DeleteWorkflowStateTest.php b/core/modules/content_moderation/tests/src/Kernel/DeleteWorkflowStateTest.php new file mode 100644 index 0000000..6101966 --- /dev/null +++ b/core/modules/content_moderation/tests/src/Kernel/DeleteWorkflowStateTest.php @@ -0,0 +1,163 @@ +installSchema('node', 'node_access'); + $this->installEntitySchema('node'); + $this->installEntitySchema('user'); + $this->installEntitySchema('content_moderation_state'); + $this->installConfig('content_moderation'); + + $this->entityTypeManager = $this->container->get('entity_type.manager'); + $this->configFactory = $this->container->get('config.factory'); + $this->workflowStorage = $this->container->get('entity_type.manager')->getStorage('workflow'); + + NodeType::create([ + 'type' => 'example', + ])->save(); + + $workflow = Workflow::load('editorial'); + $workflow->addState('test', 'Test'); + $workflow->addState('unpub_test', 'Unpublished Test'); + $workflow->getTypePlugin()->setConfiguration([ + 'states' => [ + 'test' => [ + 'published' => TRUE, + 'default_revision' => TRUE, + ], + 'unpub_test' => [ + 'published' => FALSE, + 'default_revision' => FALSE, + ], + ], + ]); + $workflow->getTypePlugin()->addEntityTypeAndBundle('node', 'example'); + $workflow->save(); + $this->workflow = $workflow; + } + + /** + * Tests deleting a published moderation state. + */ + function testDeletingPublishedState() { + $node = Node::create([ + 'type' => 'example', + 'title' => 'Test title', + 'moderation_state' => 'test', + ]); + $node->save(); + + $this->assertEquals('test', $node->moderation_state->value); + $this->assertTrue($node->isPublished()); + + $this->workflow->deleteState('test'); + $this->workflow->save(); + + $reloaded_node = Node::load($node->id()); + $this->assertEquals('published', $reloaded_node->moderation_state->value); + $this->assertTrue($reloaded_node->isPublished()); + } + + /** + * Tests deleting an unpublished moderation state. + */ + public function testDeletingUnpublishedState() { + $node = Node::create([ + 'type' => 'example', + 'title' => 'Test title', + 'moderation_state' => 'unpub_test', + ]); + $node->save(); + + $this->assertEquals('unpub_test', $node->moderation_state->value); + $this->assertFalse($node->isPublished()); + + $this->workflow->deleteState('unpub_test'); + $this->workflow->save(); + + $reloaded_node = Node::load($node->id()); + $this->assertEquals('draft', $reloaded_node->moderation_state->value); + $this->assertFalse($reloaded_node->isPublished()); + } + + /** + * Test deleting a state without using Workflow::deleteState. + * + * To ensure our implementation works when deploying configuration, we should + * not rely on Workflow::deleteState being called to update states. + */ + public function testDeletingStateViaConfiguration() { + $node = Node::create([ + 'type' => 'example', + 'title' => 'Test title', + 'moderation_state' => 'test', + ]); + $node->save(); + + $this->assertEquals('test', $node->moderation_state->value); + $this->assertTrue($node->isPublished()); + + // Remove the state configuration values manually to create the conditions + // of a configuration deployment. + $workflow_config = $this->configFactory->get('workflows.workflow.editorial')->getRawData(); + unset($workflow_config['states']['test']); + unset($workflow_config['type_settings']['states']['test']); + + $this->workflowStorage->updateFromStorageRecord(Workflow::load('editorial'), $workflow_config)->save(); + + $reloaded_node = Node::load($node->id()); + $this->assertEquals('published', $reloaded_node->moderation_state->value); + $this->assertTrue($reloaded_node->isPublished()); + } + +}