Loading src/EntityClone/Content/ContentEntityCloneBase.php +110 −10 Original line number Diff line number Diff line Loading @@ -3,6 +3,9 @@ namespace Drupal\entity_clone\EntityClone\Content; use Drupal\Component\Datetime\TimeInterface; use Drupal\content_moderation\Entity\ContentModerationState; use Drupal\content_moderation\Plugin\Field\ModerationStateFieldItemList; use Drupal\Core\Entity\ContentEntityInterface; use Drupal\Core\Entity\EntityHandlerInterface; use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Entity\EntityTypeInterface; Loading Loading @@ -35,18 +38,18 @@ class ContentEntityCloneBase implements EntityHandlerInterface, EntityCloneInter protected $entityTypeId; /** * The current user. * A service for obtaining the system's time. * * @var \Drupal\Core\Session\AccountProxyInterface * @var \Drupal\Component\Datetime\TimeInterface */ protected $currentUser; protected $timeService; /** * A service for obtaining the system's time. * The current user. * * @var \Drupal\Component\Datetime\TimeInterface * @var \Drupal\Core\Session\AccountProxyInterface */ protected $timeService; protected $currentUser; /** * Constructs a new ContentEntityCloneBase. Loading @@ -55,16 +58,18 @@ class ContentEntityCloneBase implements EntityHandlerInterface, EntityCloneInter * The entity type manager. * @param string $entity_type_id * The entity type ID. * @param \Drupal\Core\Session\AccountProxyInterface $currentUser * The current user. * @param \Drupal\Component\Datetime\TimeInterface $time_service * A service for obtaining the system's time. * @param \Drupal\Core\Session\AccountProxyInterface $current_user * The current user. * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler * The module handler. */ public function __construct(EntityTypeManagerInterface $entity_type_manager, $entity_type_id, TimeInterface $time_service, AccountProxyInterface $currentUser) { public function __construct(EntityTypeManagerInterface $entity_type_manager, $entity_type_id, TimeInterface $time_service, AccountProxyInterface $current_user) { $this->entityTypeManager = $entity_type_manager; $this->entityTypeId = $entity_type_id; $this->timeService = $time_service; $this->currentUser = $currentUser; $this->currentUser = $current_user; } /** Loading Loading @@ -111,7 +116,28 @@ class ContentEntityCloneBase implements EntityHandlerInterface, EntityCloneInter $cloned_entity->setCreatedTime($this->timeService->getRequestTime()); } if ($this->hasTranslatableModerationState($cloned_entity)) { // If we are using moderation state, ensure that each translation gets // the same moderation state BEFORE we save so that upon save, each // translation gets its publishing status updated according to the // moderation state. After the entity is saved, we kick in the creation // of translations of created moderation state entity. foreach ($cloned_entity->getTranslationLanguages(TRUE) as $language) { $translation = $cloned_entity->getTranslation($language->getId()); $translation->set('moderation_state', $cloned_entity->get('moderation_state')->value); } } $cloned_entity->save(); // If we are using content moderation, make sure the moderation state // entity gets translated to reflect the available translations on the // source entity. Thus, we call this after the save because we need the // original moderation state entity to have been created. if ($this->hasTranslatableModerationState($cloned_entity)) { $this->setTranslationModerationState($entity, $cloned_entity); } return $cloned_entity; } Loading @@ -134,6 +160,7 @@ class ContentEntityCloneBase implements EntityHandlerInterface, EntityCloneInter if (($field_definition instanceof FieldConfigInterface) && $type_is_clonable) { return TRUE; } return FALSE; } Loading Loading @@ -199,6 +226,7 @@ class ContentEntityCloneBase implements EntityHandlerInterface, EntityCloneInter $referenced_entities[] = $referenced_entity; } } return $referenced_entities; } Loading @@ -223,7 +251,79 @@ class ContentEntityCloneBase implements EntityHandlerInterface, EntityCloneInter if (!isset($child_properties['children'])) { $child_properties['children'] = []; } return $child_properties; } /** * Create moderation_state translations for the cloned entities. * * When a new translation is saved, content moderation creates a corresponding * translation to the moderation_state entity as well. However, for this to * happen, the translation itself needs to be saved. When we clone, this * doesn't happen as the original entity gets cloned together with the * translations and a save is called on the original language being cloned. So * we have to do this manually. * * This is doing essentially what * Drupal\content_moderation\EntityOperations::updateOrCreateFromEntity but * we had to replicate it because if a user clones a node translation * directly, updateOrCreateFromEntity() would not create a translation for * the original language but would override the language when passing the * original entity translation. */ protected function setTranslationModerationState(ContentEntityInterface $entity, ContentEntityInterface $cloned_entity) { $languages = $cloned_entity->getTranslationLanguages(); // Load the existing moderation state entity for the cloned entity. This // should exist and have only 1 translation. $needs_save = FALSE; $moderation_state = ContentModerationState::loadFromModeratedEntity($cloned_entity); $original_translation = $cloned_entity->getUntranslated(); if ($moderation_state->language()->getId() !== $original_translation->language()->getId()) { // If we are cloning a node while not being in the original translation // language, Drupal core will set the default language of the moderation // state to that language whereas the node is simply duplicated and will // keep the original default language. So we need to change it to that // also in the moderation state to keep things consistent. $moderation_state->set($moderation_state->getEntityType()->getKey('langcode'), $original_translation->language()->getId()); $needs_save = TRUE; } foreach ($languages as $language) { $translation = $cloned_entity->getTranslation($language->getId()); if (!$moderation_state->hasTranslation($translation->language()->getId())) { // We make a 1 to 1 copy of the moderation state entity from the // original created already by the content_moderation module. This is ok // because even if translations can be in different moderation states, // when cloning, the moderation state is reset to whatever the workflow // default is configured to be. So we anyway should end up with the // same state across all languages. $moderation_state->addTranslation($translation->language()->getId(), $moderation_state->toArray()); $needs_save = TRUE; } } if ($needs_save) { ContentModerationState::updateOrCreateFromEntity($moderation_state); } } /** * Checks if the entity has the moderation state field and can be moderated. * * @param \Drupal\Core\Entity\ContentEntityInterface $entity * The entity. * * @return bool * Whether it can be moderated. */ protected function hasTranslatableModerationState(ContentEntityInterface $entity): bool { if (!$entity->hasField('moderation_state') || !$entity->get('moderation_state') instanceof ModerationStateFieldItemList) { return FALSE; } return !empty($entity->getTranslationLanguages(FALSE)); } } tests/src/Functional/EntityCloneContentModerationTest.php 0 → 100644 +274 −0 Original line number Diff line number Diff line <?php namespace Drupal\Tests\entity_clone\Functional; use Drupal\content_moderation\Entity\ContentModerationState as ContentModerationStateEntity; use Drupal\Core\Url; use Drupal\language\Entity\ConfigurableLanguage; use Drupal\node\Entity\Node; use Drupal\Tests\content_moderation\Traits\ContentModerationTestTrait; use Drupal\Tests\node\Functional\NodeTestBase; /** * Create a moderated content and test the clone of its moderation state. * * @group entity_clone */ class EntityCloneContentModerationTest extends NodeTestBase { use ContentModerationTestTrait; /** * {@inheritdoc} */ protected static $modules = [ 'entity_clone', 'content_moderation', 'language', 'content_translation', 'block', ]; /** * {@inheritdoc} */ protected $defaultTheme = 'classy'; /** * Permissions to grant admin user. * * @var array */ protected $permissions = [ 'bypass node access', 'administer nodes', 'clone node entity', 'use editorial transition create_new_draft', 'use editorial transition publish', 'use editorial transition archive', 'use editorial transition archived_draft', 'use editorial transition archived_published', ]; /** * A user with permission to bypass content access checks. * * @var \Drupal\user\UserInterface */ protected $adminUser; /** * Sets the test up. */ protected function setUp(): void { parent::setUp(); ConfigurableLanguage::createFromLangcode('fr')->save(); \Drupal::service('content_translation.manager')->setEnabled('node', 'page', TRUE); $workflow = $this->createEditorialWorkflow(); $this->addEntityTypeAndBundleToWorkflow($workflow, 'node', 'page'); $this->adminUser = $this->drupalCreateUser($this->permissions); $this->drupalLogin($this->adminUser); } /** * Test content entity clone. */ public function testContentModerationEntityClone() { $node = Node::create([ 'type' => 'page', 'title' => 'My node', ]); $node->save(); $translation = $node->addTranslation('fr', $node->toArray()); // Unfortunately content moderation only creates translations to the // moderation state entities when the actual translation of the source // entity gets saved (as opposed to an original node with multiple // translations). $translation->save(); // Assert that we have a moderation state translation for each language. $node = Node::load($node->id()); $this->assertCount(2, $node->getTranslationLanguages()); $moderation_state = ContentModerationStateEntity::loadFromModeratedEntity($node); $this->assertFalse($moderation_state->isNew()); $this->assertCount(2, $moderation_state->getTranslationLanguages()); foreach ($moderation_state->getTranslationLanguages() as $language) { $this->assertEquals('draft', $moderation_state->getTranslation($language->getId())->get('moderation_state')->value); } $moderation_state_id = $moderation_state->id(); // Clone the node and assert that the moderation state is cloned and has // a translation for each language. $this->drupalGet(Url::fromUserInput('/entity_clone/node/' . $node->id())); $this->submitForm([], t('Clone')); $nodes = \Drupal::entityTypeManager() ->getStorage('node') ->loadByProperties([ 'title' => 'My node - Cloned', ]); $clone = reset($nodes); $this->assertInstanceOf(Node::class, $clone, 'Test node cloned found in database.'); $this->assertCount(2, $clone->getTranslationLanguages()); $clone_moderation_state = ContentModerationStateEntity::loadFromModeratedEntity($clone); $this->assertNotEquals($moderation_state_id, $clone_moderation_state->id()); $this->assertFalse($clone_moderation_state->isNew()); $this->assertCount(2, $clone_moderation_state->getTranslationLanguages()); foreach ($clone_moderation_state->getTranslationLanguages() as $language) { $this->assertEquals('draft', $clone_moderation_state->getTranslation($language->getId())->get('moderation_state')->value); } // Create another node, but this time, move the state to published. $node = Node::create([ 'type' => 'page', 'title' => 'My second node', ]); $node->save(); $node->set('moderation_state', 'published'); $node->setNewRevision(); $node->save(); $translation = $node->addTranslation('fr', $node->toArray()); $translation->save(); $moderation_state = ContentModerationStateEntity::loadFromModeratedEntity($node); $this->assertFalse($moderation_state->isNew()); $this->assertCount(2, $moderation_state->getTranslationLanguages()); foreach ($moderation_state->getTranslationLanguages() as $language) { $this->assertEquals('published', $moderation_state->getTranslation($language->getId())->get('moderation_state')->value); } // Clone the node and assert that the moderation state is cloned and has // a translation for each language. $this->drupalGet(Url::fromUserInput('/entity_clone/node/' . $node->id())); $this->submitForm([], t('Clone')); $nodes = \Drupal::entityTypeManager() ->getStorage('node') ->loadByProperties([ 'title' => 'My second node - Cloned', ]); $clone = reset($nodes); $this->assertInstanceOf(Node::class, $clone, 'Test node cloned found in database.'); $this->assertCount(2, $clone->getTranslationLanguages()); $clone_moderation_state = ContentModerationStateEntity::loadFromModeratedEntity($clone); $this->assertFalse($clone_moderation_state->isNew()); $this->assertCount(2, $clone_moderation_state->getTranslationLanguages()); foreach ($clone_moderation_state->getTranslationLanguages() as $language) { // When we clone, the default moderation state is set on the clone for // both languages (draft), even if the cloned content was published. $this->assertEquals('draft', $clone_moderation_state->getTranslation($language->getId())->get('moderation_state')->value); } // Create another node, but this time the original should be published but // the translation should be draft. $node = Node::create([ 'type' => 'page', 'title' => 'My third node', ]); $node->save(); $translation = $node->addTranslation('fr', $node->toArray()); $translation->save(); $node->set('moderation_state', 'published'); $node->setNewRevision(); $node->save(); $moderation_state = ContentModerationStateEntity::loadFromModeratedEntity($node); $this->assertFalse($moderation_state->isNew()); $this->assertCount(2, $moderation_state->getTranslationLanguages()); $expected_map = [ 'en' => 'published', 'fr' => 'draft', ]; foreach ($moderation_state->getTranslationLanguages() as $language) { $this->assertEquals($expected_map[$language->getId()], $moderation_state->getTranslation($language->getId())->get('moderation_state')->value); } $this->assertTrue($node->getTranslation('en')->isPublished()); $this->assertFalse($node->getTranslation('fr')->isPublished()); // Clone the node and assert that the moderation state is reset to draft // for both languages. $this->drupalGet(Url::fromUserInput('/entity_clone/node/' . $node->id())); $this->submitForm([], t('Clone')); $nodes = \Drupal::entityTypeManager() ->getStorage('node') ->loadByProperties([ 'title' => 'My third node - Cloned', ]); $clone = reset($nodes); $this->assertInstanceOf(Node::class, $clone, 'Test node cloned found in database.'); $this->assertCount(2, $clone->getTranslationLanguages()); $this->assertFalse($clone->getTranslation('en')->isPublished()); $this->assertFalse($clone->getTranslation('fr')->isPublished()); $clone_moderation_state = ContentModerationStateEntity::loadFromModeratedEntity($clone); $this->assertFalse($clone_moderation_state->isNew()); $this->assertCount(2, $clone_moderation_state->getTranslationLanguages()); foreach ($clone_moderation_state->getTranslationLanguages() as $language) { $this->assertEquals('draft', $clone_moderation_state->getTranslation($language->getId())->get('moderation_state')->value); } // Create another node but this time clone while on the French and assert // that the moderation state default language is the same as of the node. $node = Node::create([ 'type' => 'page', 'title' => 'My fourth node', ]); $node->save(); $translation = $node->addTranslation('fr', ['title' => 'My fourth node FR'] + $node->toArray()); $translation->save(); $node = Node::load($node->id()); $this->assertCount(2, $node->getTranslationLanguages()); $this->drupalGet(Url::fromUserInput('/fr/entity_clone/node/' . $node->id())); $this->submitForm([], t('Clone')); $clone = Node::load($node->id() + 1); $this->assertInstanceOf(Node::class, $clone, 'Test node cloned found in database.'); $this->assertCount(2, $clone->getTranslationLanguages()); $this->assertEquals('My fourth node FR - Cloned', $clone->getTranslation('fr')->label()); $clone_moderation_state = ContentModerationStateEntity::loadFromModeratedEntity($clone); $this->assertFalse($clone_moderation_state->isNew()); $this->assertCount(2, $clone_moderation_state->getTranslationLanguages()); foreach ($clone_moderation_state->getTranslationLanguages() as $language) { $this->assertEquals('draft', $clone_moderation_state->getTranslation($language->getId())->get('moderation_state')->value); } $this->assertTrue($clone_moderation_state->isDefaultTranslation()); $this->assertEquals('en', $clone_moderation_state->language()->getId()); // Create another node, published, translated and assert that upon cloning // the node status is reset to 0 to match the fact that it's a draft. $node = Node::create([ 'type' => 'page', 'title' => 'My fifth node', 'moderation_state' => 'published', ]); $node->save(); $translation = $node->addTranslation('fr', $node->toArray()); $translation->save(); $node = Node::load($node->id()); $this->assertCount(2, $node->getTranslationLanguages()); $this->assertTrue($node->getTranslation('en')->isPublished()); $this->assertTrue($node->getTranslation('fr')->isPublished()); $this->drupalGet(Url::fromUserInput('/entity_clone/node/' . $node->id())); $this->submitForm([], t('Clone')); $nodes = \Drupal::entityTypeManager() ->getStorage('node') ->loadByProperties([ 'title' => 'My fifth node - Cloned', ]); $clone = reset($nodes); $this->assertInstanceOf(Node::class, $clone, 'Test node cloned found in database.'); $this->assertCount(2, $clone->getTranslationLanguages()); $this->assertFalse($clone->getTranslation('en')->isPublished()); $this->assertFalse($clone->getTranslation('fr')->isPublished()); } } Loading
src/EntityClone/Content/ContentEntityCloneBase.php +110 −10 Original line number Diff line number Diff line Loading @@ -3,6 +3,9 @@ namespace Drupal\entity_clone\EntityClone\Content; use Drupal\Component\Datetime\TimeInterface; use Drupal\content_moderation\Entity\ContentModerationState; use Drupal\content_moderation\Plugin\Field\ModerationStateFieldItemList; use Drupal\Core\Entity\ContentEntityInterface; use Drupal\Core\Entity\EntityHandlerInterface; use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Entity\EntityTypeInterface; Loading Loading @@ -35,18 +38,18 @@ class ContentEntityCloneBase implements EntityHandlerInterface, EntityCloneInter protected $entityTypeId; /** * The current user. * A service for obtaining the system's time. * * @var \Drupal\Core\Session\AccountProxyInterface * @var \Drupal\Component\Datetime\TimeInterface */ protected $currentUser; protected $timeService; /** * A service for obtaining the system's time. * The current user. * * @var \Drupal\Component\Datetime\TimeInterface * @var \Drupal\Core\Session\AccountProxyInterface */ protected $timeService; protected $currentUser; /** * Constructs a new ContentEntityCloneBase. Loading @@ -55,16 +58,18 @@ class ContentEntityCloneBase implements EntityHandlerInterface, EntityCloneInter * The entity type manager. * @param string $entity_type_id * The entity type ID. * @param \Drupal\Core\Session\AccountProxyInterface $currentUser * The current user. * @param \Drupal\Component\Datetime\TimeInterface $time_service * A service for obtaining the system's time. * @param \Drupal\Core\Session\AccountProxyInterface $current_user * The current user. * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler * The module handler. */ public function __construct(EntityTypeManagerInterface $entity_type_manager, $entity_type_id, TimeInterface $time_service, AccountProxyInterface $currentUser) { public function __construct(EntityTypeManagerInterface $entity_type_manager, $entity_type_id, TimeInterface $time_service, AccountProxyInterface $current_user) { $this->entityTypeManager = $entity_type_manager; $this->entityTypeId = $entity_type_id; $this->timeService = $time_service; $this->currentUser = $currentUser; $this->currentUser = $current_user; } /** Loading Loading @@ -111,7 +116,28 @@ class ContentEntityCloneBase implements EntityHandlerInterface, EntityCloneInter $cloned_entity->setCreatedTime($this->timeService->getRequestTime()); } if ($this->hasTranslatableModerationState($cloned_entity)) { // If we are using moderation state, ensure that each translation gets // the same moderation state BEFORE we save so that upon save, each // translation gets its publishing status updated according to the // moderation state. After the entity is saved, we kick in the creation // of translations of created moderation state entity. foreach ($cloned_entity->getTranslationLanguages(TRUE) as $language) { $translation = $cloned_entity->getTranslation($language->getId()); $translation->set('moderation_state', $cloned_entity->get('moderation_state')->value); } } $cloned_entity->save(); // If we are using content moderation, make sure the moderation state // entity gets translated to reflect the available translations on the // source entity. Thus, we call this after the save because we need the // original moderation state entity to have been created. if ($this->hasTranslatableModerationState($cloned_entity)) { $this->setTranslationModerationState($entity, $cloned_entity); } return $cloned_entity; } Loading @@ -134,6 +160,7 @@ class ContentEntityCloneBase implements EntityHandlerInterface, EntityCloneInter if (($field_definition instanceof FieldConfigInterface) && $type_is_clonable) { return TRUE; } return FALSE; } Loading Loading @@ -199,6 +226,7 @@ class ContentEntityCloneBase implements EntityHandlerInterface, EntityCloneInter $referenced_entities[] = $referenced_entity; } } return $referenced_entities; } Loading @@ -223,7 +251,79 @@ class ContentEntityCloneBase implements EntityHandlerInterface, EntityCloneInter if (!isset($child_properties['children'])) { $child_properties['children'] = []; } return $child_properties; } /** * Create moderation_state translations for the cloned entities. * * When a new translation is saved, content moderation creates a corresponding * translation to the moderation_state entity as well. However, for this to * happen, the translation itself needs to be saved. When we clone, this * doesn't happen as the original entity gets cloned together with the * translations and a save is called on the original language being cloned. So * we have to do this manually. * * This is doing essentially what * Drupal\content_moderation\EntityOperations::updateOrCreateFromEntity but * we had to replicate it because if a user clones a node translation * directly, updateOrCreateFromEntity() would not create a translation for * the original language but would override the language when passing the * original entity translation. */ protected function setTranslationModerationState(ContentEntityInterface $entity, ContentEntityInterface $cloned_entity) { $languages = $cloned_entity->getTranslationLanguages(); // Load the existing moderation state entity for the cloned entity. This // should exist and have only 1 translation. $needs_save = FALSE; $moderation_state = ContentModerationState::loadFromModeratedEntity($cloned_entity); $original_translation = $cloned_entity->getUntranslated(); if ($moderation_state->language()->getId() !== $original_translation->language()->getId()) { // If we are cloning a node while not being in the original translation // language, Drupal core will set the default language of the moderation // state to that language whereas the node is simply duplicated and will // keep the original default language. So we need to change it to that // also in the moderation state to keep things consistent. $moderation_state->set($moderation_state->getEntityType()->getKey('langcode'), $original_translation->language()->getId()); $needs_save = TRUE; } foreach ($languages as $language) { $translation = $cloned_entity->getTranslation($language->getId()); if (!$moderation_state->hasTranslation($translation->language()->getId())) { // We make a 1 to 1 copy of the moderation state entity from the // original created already by the content_moderation module. This is ok // because even if translations can be in different moderation states, // when cloning, the moderation state is reset to whatever the workflow // default is configured to be. So we anyway should end up with the // same state across all languages. $moderation_state->addTranslation($translation->language()->getId(), $moderation_state->toArray()); $needs_save = TRUE; } } if ($needs_save) { ContentModerationState::updateOrCreateFromEntity($moderation_state); } } /** * Checks if the entity has the moderation state field and can be moderated. * * @param \Drupal\Core\Entity\ContentEntityInterface $entity * The entity. * * @return bool * Whether it can be moderated. */ protected function hasTranslatableModerationState(ContentEntityInterface $entity): bool { if (!$entity->hasField('moderation_state') || !$entity->get('moderation_state') instanceof ModerationStateFieldItemList) { return FALSE; } return !empty($entity->getTranslationLanguages(FALSE)); } }
tests/src/Functional/EntityCloneContentModerationTest.php 0 → 100644 +274 −0 Original line number Diff line number Diff line <?php namespace Drupal\Tests\entity_clone\Functional; use Drupal\content_moderation\Entity\ContentModerationState as ContentModerationStateEntity; use Drupal\Core\Url; use Drupal\language\Entity\ConfigurableLanguage; use Drupal\node\Entity\Node; use Drupal\Tests\content_moderation\Traits\ContentModerationTestTrait; use Drupal\Tests\node\Functional\NodeTestBase; /** * Create a moderated content and test the clone of its moderation state. * * @group entity_clone */ class EntityCloneContentModerationTest extends NodeTestBase { use ContentModerationTestTrait; /** * {@inheritdoc} */ protected static $modules = [ 'entity_clone', 'content_moderation', 'language', 'content_translation', 'block', ]; /** * {@inheritdoc} */ protected $defaultTheme = 'classy'; /** * Permissions to grant admin user. * * @var array */ protected $permissions = [ 'bypass node access', 'administer nodes', 'clone node entity', 'use editorial transition create_new_draft', 'use editorial transition publish', 'use editorial transition archive', 'use editorial transition archived_draft', 'use editorial transition archived_published', ]; /** * A user with permission to bypass content access checks. * * @var \Drupal\user\UserInterface */ protected $adminUser; /** * Sets the test up. */ protected function setUp(): void { parent::setUp(); ConfigurableLanguage::createFromLangcode('fr')->save(); \Drupal::service('content_translation.manager')->setEnabled('node', 'page', TRUE); $workflow = $this->createEditorialWorkflow(); $this->addEntityTypeAndBundleToWorkflow($workflow, 'node', 'page'); $this->adminUser = $this->drupalCreateUser($this->permissions); $this->drupalLogin($this->adminUser); } /** * Test content entity clone. */ public function testContentModerationEntityClone() { $node = Node::create([ 'type' => 'page', 'title' => 'My node', ]); $node->save(); $translation = $node->addTranslation('fr', $node->toArray()); // Unfortunately content moderation only creates translations to the // moderation state entities when the actual translation of the source // entity gets saved (as opposed to an original node with multiple // translations). $translation->save(); // Assert that we have a moderation state translation for each language. $node = Node::load($node->id()); $this->assertCount(2, $node->getTranslationLanguages()); $moderation_state = ContentModerationStateEntity::loadFromModeratedEntity($node); $this->assertFalse($moderation_state->isNew()); $this->assertCount(2, $moderation_state->getTranslationLanguages()); foreach ($moderation_state->getTranslationLanguages() as $language) { $this->assertEquals('draft', $moderation_state->getTranslation($language->getId())->get('moderation_state')->value); } $moderation_state_id = $moderation_state->id(); // Clone the node and assert that the moderation state is cloned and has // a translation for each language. $this->drupalGet(Url::fromUserInput('/entity_clone/node/' . $node->id())); $this->submitForm([], t('Clone')); $nodes = \Drupal::entityTypeManager() ->getStorage('node') ->loadByProperties([ 'title' => 'My node - Cloned', ]); $clone = reset($nodes); $this->assertInstanceOf(Node::class, $clone, 'Test node cloned found in database.'); $this->assertCount(2, $clone->getTranslationLanguages()); $clone_moderation_state = ContentModerationStateEntity::loadFromModeratedEntity($clone); $this->assertNotEquals($moderation_state_id, $clone_moderation_state->id()); $this->assertFalse($clone_moderation_state->isNew()); $this->assertCount(2, $clone_moderation_state->getTranslationLanguages()); foreach ($clone_moderation_state->getTranslationLanguages() as $language) { $this->assertEquals('draft', $clone_moderation_state->getTranslation($language->getId())->get('moderation_state')->value); } // Create another node, but this time, move the state to published. $node = Node::create([ 'type' => 'page', 'title' => 'My second node', ]); $node->save(); $node->set('moderation_state', 'published'); $node->setNewRevision(); $node->save(); $translation = $node->addTranslation('fr', $node->toArray()); $translation->save(); $moderation_state = ContentModerationStateEntity::loadFromModeratedEntity($node); $this->assertFalse($moderation_state->isNew()); $this->assertCount(2, $moderation_state->getTranslationLanguages()); foreach ($moderation_state->getTranslationLanguages() as $language) { $this->assertEquals('published', $moderation_state->getTranslation($language->getId())->get('moderation_state')->value); } // Clone the node and assert that the moderation state is cloned and has // a translation for each language. $this->drupalGet(Url::fromUserInput('/entity_clone/node/' . $node->id())); $this->submitForm([], t('Clone')); $nodes = \Drupal::entityTypeManager() ->getStorage('node') ->loadByProperties([ 'title' => 'My second node - Cloned', ]); $clone = reset($nodes); $this->assertInstanceOf(Node::class, $clone, 'Test node cloned found in database.'); $this->assertCount(2, $clone->getTranslationLanguages()); $clone_moderation_state = ContentModerationStateEntity::loadFromModeratedEntity($clone); $this->assertFalse($clone_moderation_state->isNew()); $this->assertCount(2, $clone_moderation_state->getTranslationLanguages()); foreach ($clone_moderation_state->getTranslationLanguages() as $language) { // When we clone, the default moderation state is set on the clone for // both languages (draft), even if the cloned content was published. $this->assertEquals('draft', $clone_moderation_state->getTranslation($language->getId())->get('moderation_state')->value); } // Create another node, but this time the original should be published but // the translation should be draft. $node = Node::create([ 'type' => 'page', 'title' => 'My third node', ]); $node->save(); $translation = $node->addTranslation('fr', $node->toArray()); $translation->save(); $node->set('moderation_state', 'published'); $node->setNewRevision(); $node->save(); $moderation_state = ContentModerationStateEntity::loadFromModeratedEntity($node); $this->assertFalse($moderation_state->isNew()); $this->assertCount(2, $moderation_state->getTranslationLanguages()); $expected_map = [ 'en' => 'published', 'fr' => 'draft', ]; foreach ($moderation_state->getTranslationLanguages() as $language) { $this->assertEquals($expected_map[$language->getId()], $moderation_state->getTranslation($language->getId())->get('moderation_state')->value); } $this->assertTrue($node->getTranslation('en')->isPublished()); $this->assertFalse($node->getTranslation('fr')->isPublished()); // Clone the node and assert that the moderation state is reset to draft // for both languages. $this->drupalGet(Url::fromUserInput('/entity_clone/node/' . $node->id())); $this->submitForm([], t('Clone')); $nodes = \Drupal::entityTypeManager() ->getStorage('node') ->loadByProperties([ 'title' => 'My third node - Cloned', ]); $clone = reset($nodes); $this->assertInstanceOf(Node::class, $clone, 'Test node cloned found in database.'); $this->assertCount(2, $clone->getTranslationLanguages()); $this->assertFalse($clone->getTranslation('en')->isPublished()); $this->assertFalse($clone->getTranslation('fr')->isPublished()); $clone_moderation_state = ContentModerationStateEntity::loadFromModeratedEntity($clone); $this->assertFalse($clone_moderation_state->isNew()); $this->assertCount(2, $clone_moderation_state->getTranslationLanguages()); foreach ($clone_moderation_state->getTranslationLanguages() as $language) { $this->assertEquals('draft', $clone_moderation_state->getTranslation($language->getId())->get('moderation_state')->value); } // Create another node but this time clone while on the French and assert // that the moderation state default language is the same as of the node. $node = Node::create([ 'type' => 'page', 'title' => 'My fourth node', ]); $node->save(); $translation = $node->addTranslation('fr', ['title' => 'My fourth node FR'] + $node->toArray()); $translation->save(); $node = Node::load($node->id()); $this->assertCount(2, $node->getTranslationLanguages()); $this->drupalGet(Url::fromUserInput('/fr/entity_clone/node/' . $node->id())); $this->submitForm([], t('Clone')); $clone = Node::load($node->id() + 1); $this->assertInstanceOf(Node::class, $clone, 'Test node cloned found in database.'); $this->assertCount(2, $clone->getTranslationLanguages()); $this->assertEquals('My fourth node FR - Cloned', $clone->getTranslation('fr')->label()); $clone_moderation_state = ContentModerationStateEntity::loadFromModeratedEntity($clone); $this->assertFalse($clone_moderation_state->isNew()); $this->assertCount(2, $clone_moderation_state->getTranslationLanguages()); foreach ($clone_moderation_state->getTranslationLanguages() as $language) { $this->assertEquals('draft', $clone_moderation_state->getTranslation($language->getId())->get('moderation_state')->value); } $this->assertTrue($clone_moderation_state->isDefaultTranslation()); $this->assertEquals('en', $clone_moderation_state->language()->getId()); // Create another node, published, translated and assert that upon cloning // the node status is reset to 0 to match the fact that it's a draft. $node = Node::create([ 'type' => 'page', 'title' => 'My fifth node', 'moderation_state' => 'published', ]); $node->save(); $translation = $node->addTranslation('fr', $node->toArray()); $translation->save(); $node = Node::load($node->id()); $this->assertCount(2, $node->getTranslationLanguages()); $this->assertTrue($node->getTranslation('en')->isPublished()); $this->assertTrue($node->getTranslation('fr')->isPublished()); $this->drupalGet(Url::fromUserInput('/entity_clone/node/' . $node->id())); $this->submitForm([], t('Clone')); $nodes = \Drupal::entityTypeManager() ->getStorage('node') ->loadByProperties([ 'title' => 'My fifth node - Cloned', ]); $clone = reset($nodes); $this->assertInstanceOf(Node::class, $clone, 'Test node cloned found in database.'); $this->assertCount(2, $clone->getTranslationLanguages()); $this->assertFalse($clone->getTranslation('en')->isPublished()); $this->assertFalse($clone->getTranslation('fr')->isPublished()); } }