diff --git a/drupalci.yml b/drupalci.yml new file mode 100644 index 0000000..deac396 --- /dev/null +++ b/drupalci.yml @@ -0,0 +1,44 @@ +build: + assessment: + validate_codebase: + phplint: { } + container_composer: + options: ' install --prefer-dist --no-suggest --no-progress --no-interaction' + halt-on-fail: true + host_command: + commands: + - 'curl "https://www.drupal.org/files/issues/2018-04-27/content-translation-untranslatable-paragraphs-2960253-12.patch" | patch -p1' + csslint: + halt-on-fail: false + eslint: + halt-on-fail: false + phpcs: + sniff-all-files: false + halt-on-fail: false + coder-version: ^8.2@stable + testing: + run_tests.standard: + types: 'Simpletest,PHPUnit-Unit,PHPUnit-Kernel,PHPUnit-Functional' + testgroups: '--all' + color: true + die-on-fail: false + keep-results: true + keep-results-table: false + verbose: false + concurrency: 0 + halt-on-fail: false + repeat: 1 + suppress-deprecations: true + run_tests.js: + concurrency: 1 + types: PHPUnit-FunctionalJavascript + testgroups: '--all' + color: true + die-on-fail: false + keep-results: true + keep-results-table: false + verbose: false + halt-on-fail: false + repeat: 1 + suppress-deprecations: true + nightwatchjs: { } diff --git a/entity_reference_revisions.module b/entity_reference_revisions.module index ba9ac49..7205eab 100644 --- a/entity_reference_revisions.module +++ b/entity_reference_revisions.module @@ -6,6 +6,8 @@ */ use Drupal\Component\Utility\NestedArray; +use Drupal\Core\Entity\ContentEntityInterface; +use Drupal\Core\Entity\TranslatableRevisionableStorageInterface; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Render\Element; use Drupal\Core\Routing\RouteMatchInterface; @@ -218,3 +220,29 @@ function entity_reference_revisions_form_field_ui_field_storage_add_form_alter(a unset($form['add']['new_storage_type']['#options'][(string) t('Reference revisions')]['entity_reference_revisions']); $form['add']['new_storage_type']['#options'][(string) t('Reference revisions')]['entity_reference_revisions'] = t('Other…'); } + +/** + * Implements hook_entity_revision_skip_fields_alter(). + */ +function entity_reference_revisions_entity_revision_skip_fields_alter(array &$skipped_field_names, ContentEntityInterface $entity, $context) { + $entity_type_manager = \Drupal::entityTypeManager(); + foreach ($entity->getFieldDefinitions() as $field_name => $field_definition) { + if ($field_definition->getType() == 'entity_reference_revisions' && !$field_definition->isTranslatable()) { + $target_entity_type_id = $field_definition->getSetting('target_type'); + if ($entity_type_manager->getDefinition($target_entity_type_id)->get('entity_revision_parent_id_field')) { + $skipped_field_names[$field_name] = TRUE; + + // For now, use the same hook to also create an corresponding revision + // for the referenced entity. + $active_langcode = $entity->language()->getId(); + $target_storage = \Drupal::entityTypeManager()->getStorage($target_entity_type_id); + if ($target_storage instanceof TranslatableRevisionableStorageInterface) { + foreach ($entity->get($field_name) as $delta => $item) { + $target_entity = $item->entity->hasTranslation($active_langcode) ? $item->entity->getTranslation($active_langcode) : $item->entity; + $item->entity = $target_storage->createRevision($target_entity, $context['default'], $context['keep_untranslatable_fields']); + } + } + } + } + } +} diff --git a/src/EntityReferenceRevisionsFieldItemList.php b/src/EntityReferenceRevisionsFieldItemList.php index ba90727..ed8460a 100644 --- a/src/EntityReferenceRevisionsFieldItemList.php +++ b/src/EntityReferenceRevisionsFieldItemList.php @@ -3,6 +3,8 @@ namespace Drupal\entity_reference_revisions; use Drupal\Core\Entity\FieldableEntityInterface; +use Drupal\Core\Field\FieldItemListInterface; +use Drupal\Core\Field\FieldItemListTranslationChangesInterface; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Field\FieldDefinitionInterface; use Drupal\Core\Field\EntityReferenceFieldItemList; @@ -122,4 +124,28 @@ class EntityReferenceRevisionsFieldItemList extends EntityReferenceFieldItemList return $default_value; } + /** + * {@inheritdoc} + */ + public function hasAffectingChanges($langcode, FieldItemListInterface $other_items) { + foreach ($this as $delta => $item) { + // If this is a different entity, then it is an affecting change. + if (!$other_items->offsetExists($delta) || $item->target_id != $other_items[$delta]->target_id) { + return TRUE; + } + // If it is the same entity, only consider it as having affecting changes + // if the target entity itself has changes. + if ($item->entity && $item->entity->hasTranslation($langcode) && $item->entity->getTranslation($langcode)->hasTranslationChanges()) { + return TRUE; + } + } + + // If there are fewer items, then it is also a change. + if (count($this) < count($other_items)) { + return TRUE; + } + + return FALSE; + } + } diff --git a/src/Plugin/Field/FieldType/EntityReferenceRevisionsItem.php b/src/Plugin/Field/FieldType/EntityReferenceRevisionsItem.php index c2b2c66..f1768ee 100644 --- a/src/Plugin/Field/FieldType/EntityReferenceRevisionsItem.php +++ b/src/Plugin/Field/FieldType/EntityReferenceRevisionsItem.php @@ -5,6 +5,7 @@ namespace Drupal\entity_reference_revisions\Plugin\Field\FieldType; use Drupal\Component\Utility\Random; use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Entity\EntityTypeInterface; +use Drupal\Core\Entity\TranslatableRevisionableInterface; use Drupal\Core\Entity\TypedData\EntityDataDefinition; use Drupal\Core\Field\FieldDefinitionInterface; use Drupal\Core\Field\FieldStorageDefinitionInterface; @@ -254,24 +255,35 @@ class EntityReferenceRevisionsItem extends EntityReferenceItem implements Option // If it is a new entity, parent will save it. parent::preSave(); + $is_affected = TRUE; if (!$has_new) { // Create a new revision if it is a composite entity in a host with a new // revision. $host = $this->getEntity(); $needs_save = $this->entity instanceof EntityNeedsSaveInterface && $this->entity->needsSave(); - if (!$host->isNew() && $host->isNewRevision() && $this->entity && $this->entity->getEntityType()->get('entity_revision_parent_id_field')) { - $this->entity->setNewRevision(); - if ($host->isDefaultRevision()) { - $this->entity->isDefaultRevision(TRUE); + + // The item is considered to be affected if the field is either + // untranslatable or there are translation changes. This prevents + // that new revisions are created on translatable fields for all + // translations every time the entity is saved. + $is_affected = !$this->getFieldDefinition()->isTranslatable() || ($host instanceof TranslatableRevisionableInterface && $host->hasTranslationChanges()); + if ($is_affected && !$host->isNew() && $this->entity && $this->entity->getEntityType()->get('entity_revision_parent_id_field')) { + if ($host->isNewRevision()) { + $this->entity->setNewRevision(); + $needs_save = TRUE; + } + // Additionally ensure that the default revision state is kept in sync. + if ($this->entity && $host->isDefaultRevision() != $this->entity->isDefaultRevision()) { + $this->entity->isDefaultRevision($host->isDefaultRevision()); + $needs_save = TRUE; } - $needs_save = TRUE; } if ($needs_save) { $this->entity->save(); } } - if ($this->entity) { + if ($this->entity && $is_affected) { $this->target_revision_id = $this->entity->getRevisionId(); } } diff --git a/tests/modules/entity_composite_relationship_test/src/Entity/EntityTestCompositeRelationship.php b/tests/modules/entity_composite_relationship_test/src/Entity/EntityTestCompositeRelationship.php index fb61ad1..c74765f 100644 --- a/tests/modules/entity_composite_relationship_test/src/Entity/EntityTestCompositeRelationship.php +++ b/tests/modules/entity_composite_relationship_test/src/Entity/EntityTestCompositeRelationship.php @@ -18,6 +18,7 @@ use Drupal\entity_test\Entity\EntityTestMulRev; * revision_table = "entity_test_composite_revision", * data_table = "entity_test_composite_field_data", * revision_data_table = "entity_test_composite_field_revision", + * content_translation_ui_skip = TRUE, * translatable = TRUE, * entity_revision_parent_type_field = "parent_type", * entity_revision_parent_id_field = "parent_id", diff --git a/tests/src/Kernel/EntityReferenceRevisionsCompositeTest.php b/tests/src/Kernel/EntityReferenceRevisionsCompositeTest.php index 38ca7cb..bffbaf2 100644 --- a/tests/src/Kernel/EntityReferenceRevisionsCompositeTest.php +++ b/tests/src/Kernel/EntityReferenceRevisionsCompositeTest.php @@ -9,8 +9,8 @@ use Drupal\KernelTests\Core\Entity\EntityKernelTestBase; use Drupal\language\Entity\ConfigurableLanguage; use Drupal\node\Entity\Node; use Drupal\node\Entity\NodeType; -use Drupal\simpletest\ContentTypeCreationTrait; -use Drupal\simpletest\NodeCreationTrait; +use Drupal\Tests\node\Traits\ContentTypeCreationTrait; +use Drupal\Tests\node\Traits\NodeCreationTrait; /** * Tests the entity_reference_revisions composite relationship. @@ -102,12 +102,15 @@ class EntityReferenceRevisionsCompositeTest extends EntityKernelTestBase { $this->assertEquals(1, $composite_revisions_count); // Create a node with a reference to the test composite entity. + /** @var \Drupal\node\NodeInterface $node */ $node = Node::create(array( 'title' => $this->randomMachineName(), 'type' => 'article', - 'composite_reference' => $composite, )); $node->save(); + $node->set('composite_reference', $composite); + $this->assertTrue($node->hasTranslationChanges()); + $node->save(); // Assert that there is only 1 revision when creating a node. $node_revisions_count = \Drupal::entityQuery('node')->condition('nid', $node->id())->allRevisions()->count()->execute(); @@ -146,6 +149,14 @@ class EntityReferenceRevisionsCompositeTest extends EntityKernelTestBase { $this->assertNotEqual('2nd revision', $node->getTitle(), 'Node did not keep changed title after reversion.'); $this->assertNotEqual($original_composite_revision, $node->composite_reference[0]->target_revision_id, 'Composite entity got new revision when its host reverted to an old revision.'); + // Test that removing/changing composite references results in translation + // changes. + $node->set('composite_reference', []); + $this->assertTrue($node->hasTranslationChanges()); + + // Revert the changes to avoid interfering with the delete test. + $node->set('composite_reference', $composite); + // Test that the composite entity is deleted when its parent is deleted. $node->delete(); $this->assertNull(EntityTestCompositeRelationship::load($composite->id())); diff --git a/tests/src/Kernel/EntityReferenceRevisionsCompositeTranslatableFieldTest.php b/tests/src/Kernel/EntityReferenceRevisionsCompositeTranslatableFieldTest.php new file mode 100644 index 0000000..4e261bf --- /dev/null +++ b/tests/src/Kernel/EntityReferenceRevisionsCompositeTranslatableFieldTest.php @@ -0,0 +1,351 @@ +save(); + ConfigurableLanguage::createFromLangcode('fr')->save(); + + $this->installEntitySchema('entity_test_composite'); + $this->installSchema('node', ['node_access']); + + // Create article content type. + NodeType::create(['type' => 'article', 'name' => 'Article'])->save(); + + // Create the reference to the composite entity test. + $field_storage = FieldStorageConfig::create(array( + 'field_name' => 'composite_reference', + 'entity_type' => 'node', + 'type' => 'entity_reference_revisions', + 'settings' => array( + 'target_type' => 'entity_test_composite' + ), + )); + $field_storage->save(); + $field = FieldConfig::create(array( + 'field_storage' => $field_storage, + 'bundle' => 'article', + 'translatable' => TRUE, + )); + $field->save(); + + // Inject database connection and entity type manager for the tests. + $this->database = \Drupal::database(); + $this->entityTypeManager = \Drupal::entityTypeManager(); + + // @todo content_translation should not be needed for a storage test, but + // \Drupal\Core\Entity\ContentEntityBase::isTranslatable() only returns + // TRUE if the bundle is explicitly translatable. + \Drupal::service('content_translation.manager')->setEnabled('node', 'article', TRUE); + \Drupal::service('content_translation.manager')->setEnabled('entity_test_composite', 'entity_test_composite', TRUE); + \Drupal::service('content_translation.manager')->setBundleTranslationSettings('node', 'article', [ + 'untranslatable_fields_hide' => TRUE, + ]); + \Drupal::service('entity_type.bundle.info')->clearCachedBundles(); + } + + /** + * Test the storage for handling pending revisions with translations. + */ + public function testCompositePendingRevisionTranslation() { + /** @var \Drupal\node\NodeStorageInterface $node_storage */ + $node_storage = \Drupal::entityTypeManager()->getStorage('node'); + + // Create the test composite entity. + $composite = EntityTestCompositeRelationship::create([ + 'langcode' => 'en', + 'name' => 'Initial Source Composite', + ]); + $composite->save(); + + // Create a node with a reference to the test composite entity. + $node = Node::create([ + 'langcode' => 'en', + 'title' => 'Initial Source Node', + 'type' => 'article', + 'composite_reference' => $composite, + ]); + $node->save(); + + /** @var \Drupal\node\NodeInterface $node */ + $node = $node_storage->load($node->id()); + + // Assert the revision count. + $this->assertRevisionCount(1, 'node', $node->id()); + $this->assertRevisionCount(1, 'entity_test_composite', $composite->id()); + + // Create a translation as a pending revision for both the composite and the + // node. While technically, the referenced composite could be the same + // entity, for translatable fields, it makes more sense if each translation + // points to a separate entity, each only with a single language. + $composite_de = $node->get('composite_reference')->entity->createDuplicate(); + $composite_de->set('langcode', 'de'); + $composite_de->set('name', 'Pending Revision Composite #1 DE'); + /** @var \Drupal\node\NodeInterface $node_de */ + $node_de = $node->addTranslation('de', ['title' => 'Pending Revision Node #1 DE', 'composite_reference' => $composite_de] + $node->toArray()); + $node_de->setNewRevision(TRUE); + $node_de->isDefaultRevision(FALSE); + $node_de->save(); + + // Assert the revision count. + $this->assertRevisionCount(2, 'node', $node->id()); + $this->assertRevisionCount(1, 'entity_test_composite', $composite->id()); + $this->assertRevisionCount(1, 'entity_test_composite', $composite_de->id()); + + // The DE translation will now reference to a pending revision of the + // composite entity but the en translation will reference the existing, + // unchanged revision. + /** @var \Drupal\node\NodeInterface $node_revision */ + $node_revision = $node_storage->loadRevision($node_de->getRevisionId()); + $this->assertFalse($node_revision->isDefaultRevision()); + $this->assertFalse((bool) $node_revision->isRevisionTranslationAffected()); + $this->assertEquals('Initial Source Node', $node_revision->label()); + $this->assertTrue($node_revision->get('composite_reference')->entity->isDefaultRevision()); + $this->assertEquals('Initial Source Composite', $node_revision->get('composite_reference')->entity->label()); + $this->assertFalse($node_revision->get('composite_reference')->entity->hasTranslation('de')); + $this->assertEquals($node->get('composite_reference')->target_revision_id, $node_revision->get('composite_reference')->target_revision_id); + + $node_de = $node_revision->getTranslation('de'); + $this->assertTrue((bool) $node_de->isRevisionTranslationAffected()); + $this->assertEquals('Pending Revision Node #1 DE', $node_de->label()); + // The composite is the default revision because it is a new entity. + $this->assertTrue($node_de->get('composite_reference')->entity->isDefaultRevision()); + $this->assertEquals('Pending Revision Composite #1 DE', $node_de->get('composite_reference')->entity->label()); + $this->assertNotEquals($node->get('composite_reference')->target_revision_id, $node_de->get('composite_reference')->target_revision_id); + + // Reload the default revision of the node, make sure that the composite + // there is unchanged. + $node = $node_storage->load($node->id()); + $this->assertFalse($node->hasTranslation('de')); + $this->assertEquals('Initial Source Node', $node->label()); + $this->assertFalse($node->get('composite_reference')->entity->hasTranslation('de')); + $this->assertEquals('Initial Source Composite', $node->get('composite_reference')->entity->label()); + + // Create a second translation revision for FR. + $composite_fr = $node->get('composite_reference')->entity->createDuplicate(); + $composite_fr->set('langcode', 'fr'); + $composite_fr->set('name', 'Pending Revision Composite #1 FR'); + $node_fr = $node->addTranslation('fr', ['title' => 'Pending Revision Node #1 FR', 'composite_reference' => $composite_fr] + $node->toArray()); + $node_fr->setNewRevision(TRUE); + $node_fr->isDefaultRevision(FALSE); + $node_fr->save(); + + // Assert the revision count. + $this->assertRevisionCount(3, 'node', $node->id()); + $this->assertRevisionCount(1, 'entity_test_composite', $composite->id()); + $this->assertRevisionCount(1, 'entity_test_composite', $composite_de->id()); + $this->assertRevisionCount(1, 'entity_test_composite', $composite_fr->id()); + + // Now assert that all 3 revisions exist as expected. Two translation + // pending revisions, each has the original revision as parent without + // any existing translation. + /** @var \Drupal\node\NodeInterface $node_fr */ + $node_revision = $node_storage->loadRevision($node_fr->getRevisionId()); + $this->assertFalse($node_revision->isDefaultRevision()); + $this->assertFalse((bool) $node_revision->isRevisionTranslationAffected()); + $this->assertEquals('Initial Source Node', $node_revision->label()); + $this->assertTrue($node_revision->get('composite_reference')->entity->isDefaultRevision()); + $this->assertEquals('Initial Source Composite', $node_revision->get('composite_reference')->entity->label()); + $this->assertFalse($node_revision->get('composite_reference')->entity->hasTranslation('de')); + $this->assertEquals($node->get('composite_reference')->target_revision_id, $node_revision->get('composite_reference')->target_revision_id); + + $node_fr = $node_revision->getTranslation('fr'); + $this->assertTrue((bool) $node_fr->isRevisionTranslationAffected()); + $this->assertEquals('Pending Revision Node #1 FR', $node_fr->label()); + $this->assertTrue($node_fr->get('composite_reference')->entity->isDefaultRevision()); + $this->assertEquals('Pending Revision Composite #1 FR', $node_fr->get('composite_reference')->entity->label()); + $this->assertNotEquals($node->get('composite_reference')->target_revision_id, $node_fr->get('composite_reference')->target_revision_id); + + $node_de = $node_storage->loadRevision($node_de->getRevisionId())->getTranslation('de'); + $this->assertTrue((bool) $node_de->isRevisionTranslationAffected()); + $this->assertEquals('Pending Revision Node #1 DE', $node_de->label()); + $this->assertTrue($node_de->get('composite_reference')->entity->isDefaultRevision()); + $this->assertEquals('Pending Revision Composite #1 DE', $node_de->get('composite_reference')->entity->label()); + $this->assertNotEquals($node->get('composite_reference')->target_revision_id, $node_de->get('composite_reference')->target_revision_id); + + // Reload the default revision of the node, make sure that the composite + // there is unchanged. + $node = $node_storage->load($node->id()); + $this->assertFalse($node->hasTranslation('de')); + $this->assertEquals('Initial Source Node', $node->label()); + $this->assertFalse($node->get('composite_reference')->entity->hasTranslation('de')); + $this->assertEquals('Initial Source Composite', $node->get('composite_reference')->entity->label()); + + // Now make a change to the initial source revision, save as a new default + // revision. + $initial_revision_id = $node->getRevisionId(); + $node->get('composite_reference')->entity->set('name', 'Updated Source Composite'); + $node->setTitle('Updated Source Node'); + $node->setNewRevision(TRUE); + $node->save(); + + // Assert the revision count. + $this->assertRevisionCount(4, 'node', $node->id()); + $this->assertRevisionCount(2, 'entity_test_composite', $composite->id()); + $this->assertRevisionCount(1, 'entity_test_composite', $composite_de->id()); + $this->assertRevisionCount(1, 'entity_test_composite', $composite_fr->id()); + + // Assert the two english revisions. + // Reload the default revision of the node, make sure that the composite + // there is unchanged. + $node = $node_storage->load($node->id()); + $this->assertTrue($node->isDefaultRevision()); + $this->assertFalse($node->hasTranslation('de')); + $this->assertFalse($node->hasTranslation('fr')); + $this->assertTrue((bool) $node->isRevisionTranslationAffected()); + $this->assertEquals('Updated Source Node', $node->label()); + $this->assertTrue($node->get('composite_reference')->entity->isDefaultRevision()); + $this->assertFalse($node->get('composite_reference')->entity->hasTranslation('de')); + $this->assertEquals('Updated Source Composite', $node->get('composite_reference')->entity->label()); + + $node_initial = $node_storage->loadRevision($initial_revision_id); + $this->assertFalse($node_initial->isDefaultRevision()); + $this->assertFalse($node_initial->hasTranslation('de')); + $this->assertFalse($node_initial->hasTranslation('fr')); + $this->assertEquals('Initial Source Node', $node_initial->label()); + $this->assertFalse($node_initial->get('composite_reference')->entity->isDefaultRevision()); + $this->assertFalse($node_initial->get('composite_reference')->entity->hasTranslation('de')); + $this->assertEquals('Initial Source Composite', $node_initial->get('composite_reference')->entity->label()); + + // Now publish the FR pending revision. + $node_storage->createRevision($node_fr->getTranslation('fr'))->save(); + + // Assert the revision count. + $this->assertRevisionCount(5, 'node', $node->id()); + $this->assertRevisionCount(2, 'entity_test_composite', $composite->id()); + $this->assertRevisionCount(1, 'entity_test_composite', $composite_de->id()); + $this->assertRevisionCount(1, 'entity_test_composite', $composite_fr->id()); + + // The new default revision should now have the updated english source and + // the french pending revision. + $node = $node_storage->load($node->id()); + $this->assertTrue($node->isDefaultRevision()); + $this->assertFalse($node->hasTranslation('de')); + $this->assertTrue($node->hasTranslation('fr')); + $node_fr = $node->getTranslation('fr'); + $this->assertFalse((bool) $node->isRevisionTranslationAffected()); + $this->assertTrue((bool) $node->getTranslation('fr')->isRevisionTranslationAffected()); + $this->assertEquals('Updated Source Node', $node->label()); + $this->assertTrue($node->get('composite_reference')->entity->isDefaultRevision()); + $this->assertFalse($node->get('composite_reference')->entity->hasTranslation('de')); + $this->assertTrue($node_fr->get('composite_reference')->entity->hasTranslation('fr')); + $this->assertEquals('Pending Revision Node #1 FR', $node_fr->label()); + $this->assertEquals('Pending Revision Composite #1 FR', $node_fr->get('composite_reference')->entity->getTranslation('fr')->label()); + $this->assertEquals('Updated Source Composite', $node->get('composite_reference')->entity->label()); + + // Now publish the DE pending revision as well. + $node_storage->createRevision($node_de->getTranslation('de'))->save(); + + // Assert the revision count. + $this->assertRevisionCount(6, 'node', $node->id()); + $this->assertRevisionCount(2, 'entity_test_composite', $composite->id()); + $this->assertRevisionCount(1, 'entity_test_composite', $composite_de->id()); + $this->assertRevisionCount(1, 'entity_test_composite', $composite_fr->id()); + + // The new default revision should now have the updated source and both + // translations. + $node = $node_storage->load($node->id()); + $this->assertTrue($node->isDefaultRevision()); + $this->assertTrue($node->hasTranslation('de')); + $this->assertTrue($node->hasTranslation('fr')); + $node_fr = $node->getTranslation('fr'); + $node_de = $node->getTranslation('de'); + $this->assertFalse((bool) $node->isRevisionTranslationAffected()); + $this->assertFalse((bool) $node->getTranslation('fr')->isRevisionTranslationAffected()); + $this->assertTrue((bool) $node->getTranslation('de')->isRevisionTranslationAffected()); + $this->assertEquals('Updated Source Node', $node->label()); + + // Each translation only has the composite in its translation. + $this->assertTrue($node->get('composite_reference')->entity->hasTranslation('en')); + $this->assertFalse($node->get('composite_reference')->entity->hasTranslation('de')); + $this->assertFalse($node->get('composite_reference')->entity->hasTranslation('fr')); + $this->assertFalse($node_fr->get('composite_reference')->entity->hasTranslation('en')); + $this->assertTrue($node_fr->get('composite_reference')->entity->hasTranslation('fr')); + $this->assertFalse($node_fr->get('composite_reference')->entity->hasTranslation('de')); + $this->assertFalse($node_de->get('composite_reference')->entity->hasTranslation('en')); + $this->assertTrue($node_de->get('composite_reference')->entity->hasTranslation('de')); + $this->assertFalse($node_de->get('composite_reference')->entity->hasTranslation('fr')); + + $this->assertEquals('Pending Revision Node #1 FR', $node_fr->label()); + $this->assertEquals('Pending Revision Composite #1 FR', $node_fr->get('composite_reference')->entity->getTranslation('fr')->label()); + $this->assertEquals('Pending Revision Node #1 DE', $node_de->label()); + $this->assertEquals('Pending Revision Composite #1 DE', $node_de->get('composite_reference')->entity->getTranslation('de')->label()); + $this->assertEquals('Updated Source Composite', $node->get('composite_reference')->entity->label()); + } + + /** + * Asserts the revision count of a certain entity. + * + * @param int $expected + * The expected count. + * @param string $entity_type_id + * The entity type ID, e.g. node. + * @param int $entity_id + * The entity ID. + */ + protected function assertRevisionCount($expected, $entity_type_id, $entity_id) { + $id_field = \Drupal::entityTypeManager()->getDefinition($entity_type_id)->getKey('id'); + + $revision_count = \Drupal::entityQuery($entity_type_id) + ->condition($id_field, $entity_id) + ->allRevisions() + ->count() + ->execute(); + $this->assertEquals($expected, $revision_count); + } + +} diff --git a/tests/src/Kernel/EntityReferenceRevisionsCompositeTranslationTest.php b/tests/src/Kernel/EntityReferenceRevisionsCompositeTranslationTest.php new file mode 100644 index 0000000..91f42a9 --- /dev/null +++ b/tests/src/Kernel/EntityReferenceRevisionsCompositeTranslationTest.php @@ -0,0 +1,282 @@ +save(); + ConfigurableLanguage::createFromLangcode('fr')->save(); + + $this->installEntitySchema('entity_test_composite'); + $this->installSchema('node', ['node_access']); + + // Create article content type. + NodeType::create(['type' => 'article', 'name' => 'Article'])->save(); + + // Create the reference to the composite entity test. + $field_storage = FieldStorageConfig::create(array( + 'field_name' => 'composite_reference', + 'entity_type' => 'node', + 'type' => 'entity_reference_revisions', + 'settings' => array( + 'target_type' => 'entity_test_composite' + ), + )); + $field_storage->save(); + $field = FieldConfig::create(array( + 'field_storage' => $field_storage, + 'bundle' => 'article', + 'translatable' => FALSE, + )); + $field->save(); + + // Inject database connection and entity type manager for the tests. + $this->database = \Drupal::database(); + $this->entityTypeManager = \Drupal::entityTypeManager(); + + // @todo content_translation should not be needed for a storage test, but + // \Drupal\Core\Entity\ContentEntityBase::isTranslatable() only returns + // TRUE if the bundle is explicitly translatable. + \Drupal::service('content_translation.manager')->setEnabled('node', 'article', TRUE); + \Drupal::service('content_translation.manager')->setEnabled('entity_test_composite', 'entity_test_composite', TRUE); + \Drupal::service('content_translation.manager')->setBundleTranslationSettings('node', 'article', [ + 'untranslatable_fields_hide' => TRUE, + ]); + \Drupal::service('entity_type.bundle.info')->clearCachedBundles(); + } + + /** + * Test the storage for handling pending revisions with translations. + */ + public function testCompositePendingRevisionTranslation() { + /** @var \Drupal\node\NodeStorageInterface $node_storage */ + $node_storage = \Drupal::entityTypeManager()->getStorage('node'); + + // Create the test composite entity. + $composite = EntityTestCompositeRelationship::create([ + 'langcode' => 'en', + 'name' => 'Initial Source Composite', + ]); + $composite->save(); + + // Create a node with a reference to the test composite entity. + $node = Node::create([ + 'langcode' => 'en', + 'title' => 'Initial Source Node', + 'type' => 'article', + 'composite_reference' => $composite, + ]); + $node->save(); + + /** @var \Drupal\node\NodeInterface $node */ + $node = $node_storage->load($node->id()); + + // Assert that there is only 1 revision when creating a node. + $node_revisions_count = \Drupal::entityQuery('node') + ->condition('nid', $node->id()) + ->allRevisions() + ->count() + ->execute(); + $this->assertEquals(1, $node_revisions_count); + // Assert there is no new composite revision after creating a host entity. + $composite_revisions_count = \Drupal::entityQuery('entity_test_composite') + ->condition('id', $composite->id()) + ->allRevisions() + ->count() + ->execute(); + $this->assertEquals(1, $composite_revisions_count); + + // Create a translation as a pending revision for both the composite and the + // node. + $node->get('composite_reference')->entity->addTranslation('de', ['name' => 'Pending Revision Composite #1 DE'] + $composite->toArray()); + $node_de = $node->addTranslation('de', ['title' => 'Pending Revision Node #1 DE'] + $node->toArray()); + $node_de->setNewRevision(TRUE); + $node_de->isDefaultRevision(FALSE); + $node_de->save(); + + /** @var \Drupal\node\NodeInterface $node_de */ + $node_de = $node_storage->loadRevision($node_de->getRevisionId()); + $this->assertFalse($node_de->isDefaultRevision()); + $this->assertFalse((bool) $node_de->isRevisionTranslationAffected()); + $this->assertTrue((bool) $node_de->getTranslation('de')->isRevisionTranslationAffected()); + $this->assertEquals('Pending Revision Node #1 DE', $node_de->getTranslation('de')->label()); + $this->assertEquals('Initial Source Node', $node_de->label()); + $this->assertFalse($node_de->get('composite_reference')->entity->isDefaultRevision()); + $this->assertEquals('Pending Revision Composite #1 DE', $node_de->get('composite_reference')->entity->getTranslation('de')->label()); + $this->assertEquals('Initial Source Composite', $node_de->get('composite_reference')->entity->label()); + + // Reload the default revision of the node, make sure that the composite + // there is unchanged. + $node = $node_storage->load($node->id()); + $this->assertFalse($node->hasTranslation('de')); + $this->assertEquals('Initial Source Node', $node->label()); + $this->assertFalse($node->get('composite_reference')->entity->hasTranslation('de')); + $this->assertEquals('Initial Source Composite', $node->get('composite_reference')->entity->label()); + + // Create a second translation revision for FR. + $node->get('composite_reference')->entity->addTranslation('fr', ['name' => 'Pending Revision Composite #1 FR'] + $composite->toArray()); + $node_fr = $node->addTranslation('fr', ['title' => 'Pending Revision Node #1 FR'] + $node->toArray()); + $node_fr->setNewRevision(TRUE); + $node_fr->isDefaultRevision(FALSE); + $node_fr->save(); + + // Now assert that all 3 revisions exist as expected. Two translation + // pending revisions, each has the original revision as parent without + // any existing translation. + /** @var \Drupal\node\NodeInterface $node_fr */ + $node_fr = $node_storage->loadRevision($node_fr->getRevisionId()); + $this->assertFalse($node_fr->isDefaultRevision()); + $this->assertFalse($node_fr->hasTranslation('de')); + $this->assertFalse((bool) $node_fr->isRevisionTranslationAffected()); + $this->assertTrue((bool) $node_fr->getTranslation('fr')->isRevisionTranslationAffected()); + $this->assertEquals('Pending Revision Node #1 FR', $node_fr->getTranslation('fr')->label()); + $this->assertEquals('Initial Source Node', $node_fr->label()); + $this->assertFalse($node_fr->get('composite_reference')->entity->isDefaultRevision()); + $this->assertFalse($node_fr->get('composite_reference')->entity->hasTranslation('de')); + $this->assertEquals('Pending Revision Composite #1 FR', $node_fr->get('composite_reference')->entity->getTranslation('fr')->label()); + $this->assertEquals('Initial Source Composite', $node_fr->get('composite_reference')->entity->label()); + + $node_de = $node_storage->loadRevision($node_de->getRevisionId()); + $this->assertFalse($node_de->isDefaultRevision()); + $this->assertFalse($node_de->hasTranslation('fr')); + $this->assertEquals('Pending Revision Node #1 DE', $node_de->getTranslation('de')->label()); + $this->assertEquals('Initial Source Node', $node_de->label()); + $this->assertFalse($node_de->get('composite_reference')->entity->isDefaultRevision()); + $this->assertFalse($node_de->get('composite_reference')->entity->hasTranslation('fr')); + $this->assertEquals('Pending Revision Composite #1 DE', $node_de->get('composite_reference')->entity->getTranslation('de')->label()); + $this->assertEquals('Initial Source Composite', $node_de->get('composite_reference')->entity->label()); + + // Reload the default revision of the node, make sure that the composite + // there is unchanged. + $node = $node_storage->load($node->id()); + $this->assertFalse($node->hasTranslation('de')); + $this->assertEquals('Initial Source Node', $node->label()); + $this->assertFalse($node->get('composite_reference')->entity->hasTranslation('de')); + $this->assertEquals('Initial Source Composite', $node->get('composite_reference')->entity->label()); + + // Now make a change to the initial source revision, save as a new default + // revision. + $initial_revision_id = $node->getRevisionId(); + $node->get('composite_reference')->entity->set('name', 'Updated Source Composite'); + $node->setTitle('Updated Source Node'); + $node->setNewRevision(TRUE); + $node->save(); + + // Assert the two english revisions. + // Reload the default revision of the node, make sure that the composite + // there is unchanged. + $node = $node_storage->load($node->id()); + $this->assertTrue($node->isDefaultRevision()); + $this->assertFalse($node->hasTranslation('de')); + $this->assertFalse($node->hasTranslation('fr')); + $this->assertTrue((bool) $node->isRevisionTranslationAffected()); + $this->assertEquals('Updated Source Node', $node->label()); + $this->assertTrue($node->get('composite_reference')->entity->isDefaultRevision()); + $this->assertFalse($node->get('composite_reference')->entity->hasTranslation('de')); + $this->assertEquals('Updated Source Composite', $node->get('composite_reference')->entity->label()); + + $node_initial = $node_storage->loadRevision($initial_revision_id); + $this->assertFalse($node_initial->isDefaultRevision()); + $this->assertFalse($node_initial->hasTranslation('de')); + $this->assertFalse($node_initial->hasTranslation('fr')); + $this->assertEquals('Initial Source Node', $node_initial->label()); + $this->assertFalse($node_initial->get('composite_reference')->entity->isDefaultRevision()); + $this->assertFalse($node_initial->get('composite_reference')->entity->hasTranslation('de')); + $this->assertEquals('Initial Source Composite', $node_initial->get('composite_reference')->entity->label()); + + // Now publish the FR pending revision. + $node_storage->createRevision($node_fr->getTranslation('fr'))->save(); + + // The new default revision should now have the updated english source and + // the french pending revision. + $node = $node_storage->load($node->id()); + $this->assertTrue($node->isDefaultRevision()); + $this->assertFalse($node->hasTranslation('de')); + $this->assertTrue($node->hasTranslation('fr')); + $this->assertFalse((bool) $node->isRevisionTranslationAffected()); + $this->assertTrue((bool) $node->getTranslation('fr')->isRevisionTranslationAffected()); + $this->assertEquals('Updated Source Node', $node->label()); + $this->assertTrue($node->get('composite_reference')->entity->isDefaultRevision()); + $this->assertFalse($node->get('composite_reference')->entity->hasTranslation('de')); + $this->assertTrue($node->get('composite_reference')->entity->hasTranslation('fr')); + $this->assertEquals('Pending Revision Node #1 FR', $node->getTranslation('fr')->label()); + $this->assertEquals('Pending Revision Composite #1 FR', $node->get('composite_reference')->entity->getTranslation('fr')->label()); + $this->assertEquals('Updated Source Composite', $node->get('composite_reference')->entity->label()); + + // Now publish the DE pending revision as well. + $node_storage->createRevision($node_de->getTranslation('de'))->save(); + + // The new default revision should now have the updated source and both + // translations. + $node = $node_storage->load($node->id()); + $this->assertTrue($node->isDefaultRevision()); + $this->assertTrue($node->hasTranslation('de')); + $this->assertTrue($node->hasTranslation('fr')); + $this->assertFalse((bool) $node->isRevisionTranslationAffected()); + $this->assertFalse((bool) $node->getTranslation('fr')->isRevisionTranslationAffected()); + $this->assertTrue((bool) $node->getTranslation('de')->isRevisionTranslationAffected()); + $this->assertEquals('Updated Source Node', $node->label()); + $this->assertTrue($node->get('composite_reference')->entity->isDefaultRevision()); + $this->assertTrue($node->get('composite_reference')->entity->hasTranslation('de')); + $this->assertTrue($node->get('composite_reference')->entity->hasTranslation('fr')); + $this->assertEquals('Pending Revision Node #1 FR', $node->getTranslation('fr')->label()); + $this->assertEquals('Pending Revision Composite #1 FR', $node->get('composite_reference')->entity->getTranslation('fr')->label()); + $this->assertEquals('Pending Revision Node #1 DE', $node->getTranslation('de')->label()); + $this->assertEquals('Pending Revision Composite #1 DE', $node->get('composite_reference')->entity->getTranslation('de')->label()); + $this->assertEquals('Updated Source Composite', $node->get('composite_reference')->entity->label()); + } + +} diff --git a/tests/src/Kernel/EntityReferenceRevisionsSaveTest.php b/tests/src/Kernel/EntityReferenceRevisionsSaveTest.php index 58dcda0..2b66a63 100644 --- a/tests/src/Kernel/EntityReferenceRevisionsSaveTest.php +++ b/tests/src/Kernel/EntityReferenceRevisionsSaveTest.php @@ -88,7 +88,8 @@ class EntityReferenceRevisionsSaveTest extends KernelTestBase { 'type' => 'article', 'composite_reference' => $entity_test, ]); - // Check the name is properly set. + // Check the name is properly set and that getValue() returns the entity + // when it is marked as needs save." $values = $node->composite_reference->getValue(); $this->assertTrue(isset($values[0]['entity'])); static::assertEquals($values[0]['entity']->name->value, $text); @@ -102,20 +103,22 @@ class EntityReferenceRevisionsSaveTest extends KernelTestBase { static::assertEquals($entity_test_after->name->value, $text); $new_text = 'Dummy text again'; - // Set the name again. - $entity_test->name = $new_text; - $entity_test->setNeedsSave(FALSE); + // Set another name and save the node without marking it as needs saving. + $entity_test_after->name = $new_text; + $entity_test_after->setNeedsSave(FALSE); - // Load the Node and check the composite reference field is not set. + // Load the Node and check the composite reference entity is not returned + // from getValue() if it is not marked as needs saving. $node = Node::load($node->id()); $values = $node->composite_reference->getValue(); $this->assertFalse(isset($values[0]['entity'])); - $node->composite_reference = $entity_test; + $node->composite_reference = $entity_test_after; $node->save(); // Check the name is not updated. + \Drupal::entityTypeManager()->getStorage('entity_test_composite')->resetCache(); $entity_test_after = EntityTestCompositeRelationship::load($entity_test->id()); - static::assertEquals($entity_test_after->name->value, $text); + static::assertEquals($text, $entity_test_after->name->value); // Test if after delete the referenced entity there are no problems setting // the referencing values to the parent.