diff --git a/config/schema/inline_entity_form.schema.yml b/config/schema/inline_entity_form.schema.yml index 2d7a8f2268497c4897672d4c0895e7ef74f0b279..5de578a29821cbd2976653e4fde4c8d23d82b06a 100644 --- a/config/schema/inline_entity_form.schema.yml +++ b/config/schema/inline_entity_form.schema.yml @@ -66,6 +66,9 @@ field.widget.settings.inline_entity_form_complex: allow_duplicate: type: boolean label: "Allow duplicate" + allow_asymmetric_translation: + type: boolean + label: "Allow asymmetric translation" collapsible: type: boolean label: "Collapsible" diff --git a/inline_entity_form.module b/inline_entity_form.module index 92588284e01fad3195fda53c27de83373bbdf164..98fc4503357577596805e206e6492e61a418588a 100644 --- a/inline_entity_form.module +++ b/inline_entity_form.module @@ -47,7 +47,11 @@ function inline_entity_form_form_alter(&$form, FormStateInterface $form_state, $ if (!is_null($widget_state)) { ElementSubmit::attach($form, $form_state); WidgetSubmit::attach($form, $form_state); + // A translation submit handler for the case where the main entity + // changes its language code but the widget is not active. + WidgetSubmit::attach($form, $form_state, TRUE); } + } /** diff --git a/src/Event/ShouldTranslateEvent.php b/src/Event/ShouldTranslateEvent.php new file mode 100644 index 0000000000000000000000000000000000000000..ed334799c5ab9db810e215a62f922094aaab1cc0 --- /dev/null +++ b/src/Event/ShouldTranslateEvent.php @@ -0,0 +1,68 @@ +shouldTranslate; + } + + /** + * Update the current value of shouldTranslate. + */ + public function setShouldTranslate(bool $shouldTranslate): void { + $this->shouldTranslate = $shouldTranslate; + } + + /** + * Get the parent form langcode. + */ + public function getFormLangcode(): string { + return $this->formLangcode; + } + + /** + * Get the inline entity. + */ + public function getEntity(): ContentEntityInterface { + return $this->entity; + } + + /** + * Get the parent form state. + */ + public function getFormState(): FormStateInterface { + return $this->formState; + } + +} diff --git a/src/Form/EntityInlineForm.php b/src/Form/EntityInlineForm.php index 9d193c95773ac170a06fe5ca2f710813b5db7ff0..e1ee090a070250235e02068f79780f7c52774986 100644 --- a/src/Form/EntityInlineForm.php +++ b/src/Form/EntityInlineForm.php @@ -2,16 +2,21 @@ namespace Drupal\inline_entity_form\Form; +use Drupal\Component\Datetime\TimeInterface; use Drupal\Core\Entity\ContentEntityInterface; use Drupal\Core\Entity\ContentEntityTypeInterface; use Drupal\Core\Entity\Entity\EntityFormDisplay; +use Drupal\Core\Entity\EntityChangedInterface; use Drupal\Core\Entity\EntityFieldManagerInterface; use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Entity\EntityTypeInterface; use Drupal\Core\Entity\EntityTypeManagerInterface; +use Drupal\Core\Entity\FieldableEntityInterface; +use Drupal\Core\Entity\TranslatableInterface; use Drupal\Core\Extension\ModuleHandlerInterface; use Drupal\Core\Field\WidgetBase; use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Language\LanguageManagerInterface; use Drupal\Core\Render\Element; use Drupal\Core\StringTranslation\StringTranslationTrait; use Drupal\Core\Theme\ThemeManagerInterface; @@ -61,6 +66,20 @@ class EntityInlineForm implements InlineFormInterface { */ protected $themeManager; + /** + * The language manager. + * + * @var \Drupal\Core\Language\LanguageManagerInterface + */ + private LanguageManagerInterface $languageManager; + + /** + * The time service. + * + * @var \Drupal\Component\Datetime\TimeInterface + */ + private TimeInterface $time; + /** * Constructs the inline entity form controller. * @@ -74,13 +93,19 @@ class EntityInlineForm implements InlineFormInterface { * The entity type. * @param \Drupal\Core\Theme\ThemeManagerInterface $theme_manager * The theme manager. + * @param \Drupal\Core\Language\LanguageManagerInterface $languageManager + * The language manager service. + * @param \Drupal\Component\Datetime\TimeInterface $time + * The time service. */ - public function __construct(EntityFieldManagerInterface $entity_field_manager, EntityTypeManagerInterface $entity_type_manager, ModuleHandlerInterface $module_handler, EntityTypeInterface $entity_type, ThemeManagerInterface $theme_manager) { + public function __construct(EntityFieldManagerInterface $entity_field_manager, EntityTypeManagerInterface $entity_type_manager, ModuleHandlerInterface $module_handler, EntityTypeInterface $entity_type, ThemeManagerInterface $theme_manager, LanguageManagerInterface $languageManager, TimeInterface $time) { $this->entityFieldManager = $entity_field_manager; $this->entityTypeManager = $entity_type_manager; $this->moduleHandler = $module_handler; $this->entityType = $entity_type; $this->themeManager = $theme_manager; + $this->languageManager = $languageManager; + $this->time = $time; } /** @@ -92,7 +117,9 @@ class EntityInlineForm implements InlineFormInterface { $container->get('entity_type.manager'), $container->get('module_handler'), $entity_type, - $container->get('theme.manager') + $container->get('theme.manager'), + $container->get('language_manager'), + $container->get('datetime.time') ); } @@ -194,7 +221,7 @@ class EntityInlineForm implements InlineFormInterface { // Safely restrict access. Entity cacheability already set. RenderArray::alter($entity_form[$langcode_key])->restrictAccess(FALSE, NULL); } - if (!empty($entity_form['#translating'])) { + if (!empty($entity_form['#translating']) && !$entity->isDefaultTranslation()) { // Hide the non-translatable fields. foreach ($entity->getFieldDefinitions() as $field_name => $definition) { if (isset($entity_form[$field_name]) && $field_name != $langcode_key) { @@ -295,6 +322,18 @@ class EntityInlineForm implements InlineFormInterface { * {@inheritdoc} */ public function save(EntityInterface $entity) { + // Make sure that if an inline entity is + // a new translation it also receives its own + // changed and created timestamp rather than the + // values of the entity it was translated from. + if ($entity instanceof TranslatableInterface && $entity->isTranslatable() && $entity->isNewTranslation()) { + if ($entity instanceof EntityChangedInterface) { + $entity->setChangedTime($this->time->getRequestTime()); + if ($entity instanceof FieldableEntityInterface && $entity->hasField('created')) { + $entity->set('created', $this->time->getRequestTime()); + } + } + } $entity->save(); } diff --git a/src/Plugin/Field/FieldWidget/InlineEntityFormBase.php b/src/Plugin/Field/FieldWidget/InlineEntityFormBase.php index 69869be8e099ea191d6fdbcfcd5c4acda05c0ad9..0245268c1329d3a4f407075ea1a51868310d28d9 100644 --- a/src/Plugin/Field/FieldWidget/InlineEntityFormBase.php +++ b/src/Plugin/Field/FieldWidget/InlineEntityFormBase.php @@ -390,10 +390,8 @@ abstract class InlineEntityFormBase extends WidgetBase implements ContainerFacto // Store the $items entities in the widget state, for further // manipulation. foreach ($items->referencedEntities() as $delta => $entity) { - // Display the entity in the correct translation. - if ($translating) { - $entity = TranslationHelper::prepareEntity($entity, $form_state); - } + // Always display the entity in the correct translation. + $entity = TranslationHelper::prepareEntity($entity, $form_state); $widget_state['entities'][$delta] = [ 'entity' => $entity, 'weight' => $delta, @@ -524,6 +522,11 @@ abstract class InlineEntityFormBase extends WidgetBase implements ContainerFacto $ief_id = $entity_form['#ief_id']; /** @var \Drupal\Core\Entity\EntityInterface $entity */ $entity = $entity_form['#entity']; + // The host langcode might have changed in the meanwhile as it uses an + // entity builder. + // @see \Drupal\Core\Entity\ContentEntityForm::form() + // @see \Drupal\Core\Entity\ContentEntityForm::updateFormLangcode() + $entity = TranslationHelper::prepareEntity($entity, $form_state); if (in_array($entity_form['#op'], ['add', 'duplicate'])) { // Determine the correct weight of the new element. diff --git a/src/Plugin/Field/FieldWidget/InlineEntityFormComplex.php b/src/Plugin/Field/FieldWidget/InlineEntityFormComplex.php index bd176e0b2387a26fcf020379bbdef0a104a83b4d..519d71df998cef22328ca04ac305de8dfda2f560 100644 --- a/src/Plugin/Field/FieldWidget/InlineEntityFormComplex.php +++ b/src/Plugin/Field/FieldWidget/InlineEntityFormComplex.php @@ -6,6 +6,7 @@ use Drupal\Component\Utility\NestedArray; use Drupal\Component\Utility\Tags; use Drupal\Core\Entity\Element\EntityAutocomplete; use Drupal\Core\Entity\EntityDisplayRepositoryInterface; +use Drupal\Core\Entity\EntityFormInterface; use Drupal\Core\Entity\EntityReferenceSelection\SelectionPluginManagerInterface; use Drupal\Core\Entity\EntityTypeBundleInfoInterface; use Drupal\Core\Entity\EntityTypeManagerInterface; @@ -122,6 +123,7 @@ class InlineEntityFormComplex extends InlineEntityFormBase implements ContainerF 'removed_reference' => self::REMOVED_OPTIONAL, 'match_operator' => 'CONTAINS', 'allow_duplicate' => FALSE, + 'allow_asymmetric_translation' => FALSE, ]; return $defaults; @@ -163,6 +165,15 @@ class InlineEntityFormComplex extends InlineEntityFormBase implements ContainerF '#default_value' => $this->getSetting('allow_duplicate'), ]; + // Allow setting only for translatable fields. + if ($this->fieldDefinition->isTranslatable()) { + $element['allow_asymmetric_translation'] = [ + '#type' => 'checkbox', + '#title' => $this->t('Allow user to make asymmetric translation'), + '#default_value' => $this->getSetting('allow_asymmetric_translation'), + ]; + } + $description = $this->t('Select whether a @child should be deleted altogether if removed as a reference here.
Delete always is recommended whenever each @child is exclusively managed within a single @parent without creating new revisions.
Otherwise Keep always is the safest.', [ @@ -212,6 +223,10 @@ class InlineEntityFormComplex extends InlineEntityFormBase implements ContainerF $summary[] = $this->t('@label can not be duplicated.', ['@label' => $labels['plural']]); } + if ($this->getSetting('allow_asymmetric_translation')) { + $summary[] = $this->t('@label can have asymmetric translations.', ['@label' => $labels['plural']]); + } + switch ($this->getSetting('removed_reference')) { case self::REMOVED_KEEP: $summary[] = $this->t('Always keep unreferenced @label.', ['@label' => $labels['plural']]); @@ -289,7 +304,7 @@ class InlineEntityFormComplex extends InlineEntityFormBase implements ContainerF '#suffix' => '', '#ief_id' => $this->getIefId(), '#ief_root' => TRUE, - '#translating' => $this->isTranslating($form_state), + '#allow_asymmetric_translation' => $this->getSetting('allow_asymmetric_translation') || !$this->isTranslating($form_state), '#field_title' => $this->fieldDefinition->getLabel(), '#after_build' => [ [get_class($this), 'removeTranslatabilityClue'], @@ -300,7 +315,7 @@ class InlineEntityFormComplex extends InlineEntityFormBase implements ContainerF $element['#open'] = $form_state->getUserInput() ?: !$this->getSetting('collapsed'); } - $this->prepareFormState($form_state, $items, $element['#translating']); + $this->prepareFormState($form_state, $items); $entities = $form_state->get([ 'inline_entity_form', $this->getIefId(), 'entities', @@ -511,15 +526,15 @@ class InlineEntityFormComplex extends InlineEntityFormBase implements ContainerF '#ief_row_delta' => $key, '#ief_row_form' => 'remove', // It's OK to set #access when creating the whole element. - '#access' => !$element['#translating'], + '#access' => $element['#allow_asymmetric_translation'], ]; } } } - // When in translation, the widget only supports editing (translating) - // already added entities, so there's no need to show the rest. - if ($element['#translating']) { + // When in symmetric translation mode, + // the widget only supports editing (translating) + if (!$element['#allow_asymmetric_translation']) { if (empty($entities)) { // There are no entities available for translation, hide the widget. // Safely restrict access. Entity cacheability already set. @@ -705,19 +720,46 @@ class InlineEntityFormComplex extends InlineEntityFormBase implements ContainerF $items->filterEmptyItems(); return; } + $triggering_element = $form_state->getTriggeringElement(); + $field_name = $this->fieldDefinition->getName(); + $parents = array_merge($form['#parents'], [$field_name, 'form']); + $ief_id = $this->makeIefId($parents); + + // The form might be sent for processing in order to process changing + // langcode. + // The language code in the form state gets updated only after the main + // entity was already fully built, meaning that in the first iteration + // we will get the previous language code unless we read it ourselves. + $form_object = $form_state->getFormObject(); + if ($form_object instanceof EntityFormInterface) { + $main_entity = $form_object->getEntity(); + $main_entity_langcode_key = $main_entity->getEntityType()->getKey('langcode'); + if ($main_entity_langcode_key) { + $langcode = $form_state->getValue([$main_entity_langcode_key, 0, 'value']); + if ($langcode && ($form_state->get('langcode') !== $langcode)) { + $entities = []; + foreach ($items as $item) { + $entities[] = TranslationHelper::prepareEntity($item->entity, $form_state, $langcode); + } + if (!empty($entities)) { + $ief_entities_language_update = $form_state->get('ief_entities_language_update'); + $ief_entities_language_update[] = $entities; + $form_state->set('ief_entities_language_update', $ief_entities_language_update); + } + } + } + } + if (empty($triggering_element['#ief_submit_trigger'])) { return; } - $field_name = $this->fieldDefinition->getName(); - $parents = array_merge($form['#parents'], [$field_name, 'form']); - $ief_id = $this->makeIefId($parents); $this->setIefId($ief_id); $widget_state = &$form_state->get(['inline_entity_form', $ief_id]); foreach ($widget_state['entities'] as $key => $value) { - $changed = TranslationHelper::updateEntityLangcode($value['entity'], $form_state); - if ($changed) { + $value['entity'] = TranslationHelper::prepareEntity($value['entity'], $form_state); + if ($value['entity']->isTranslatable() && $value['entity']->isNewTranslation()) { $widget_state['entities'][$key]['entity'] = $value['entity']; $widget_state['entities'][$key]['needs_save'] = TRUE; } @@ -923,7 +965,7 @@ class InlineEntityFormComplex extends InlineEntityFormBase implements ContainerF if (!empty($entity_id) && $this->getSetting('removed_reference') === self::REMOVED_OPTIONAL && $entity->access('delete')) { $form['delete'] = [ '#type' => 'checkbox', - '#title' => $this->t('Delete this @type_singular from the system.', ['@type_singular' => $labels['singular']]), + '#title' => $this->t('Delete this @type_singular from the system. This will also delete its translations.', ['@type_singular' => $labels['singular']]), ]; } diff --git a/src/Plugin/Field/FieldWidget/InlineEntityFormSimple.php b/src/Plugin/Field/FieldWidget/InlineEntityFormSimple.php index 42fa2f5ec7d1348c1dc6778910729a31cf35dc67..21c4382909f9eede1d5ddf11ccb19ab026d191eb 100644 --- a/src/Plugin/Field/FieldWidget/InlineEntityFormSimple.php +++ b/src/Plugin/Field/FieldWidget/InlineEntityFormSimple.php @@ -167,7 +167,7 @@ class InlineEntityFormSimple extends InlineEntityFormBase { 'entities' => [], ]; foreach ($items as $delta => $value) { - TranslationHelper::updateEntityLangcode($value->entity, $form_state); + $value->entity = TranslationHelper::prepareEntity($value->entity, $form_state); $widget_state['entities'][$delta] = [ 'entity' => $value->entity, 'needs_save' => TRUE, diff --git a/src/TranslationHelper.php b/src/TranslationHelper.php index fd5d88c5080a8268122e041e5c2a6248bdb57e9c..45fc25d5dd705e6d3d9c0016b57ff0ed1d24076e 100644 --- a/src/TranslationHelper.php +++ b/src/TranslationHelper.php @@ -4,6 +4,7 @@ namespace Drupal\inline_entity_form; use Drupal\Core\Entity\ContentEntityInterface; use Drupal\Core\Form\FormStateInterface; +use Drupal\inline_entity_form\Event\ShouldTranslateEvent; /** * Provides content translation helpers. @@ -17,33 +18,49 @@ class TranslationHelper { * The inline entity. * @param \Drupal\Core\Form\FormStateInterface $form_state * The form state. + * @param string|null $form_langcode + * The form language code or NULL to use the form state value. * * @return \Drupal\Core\Entity\ContentEntityInterface * The prepared entity. * * @see \Drupal\Core\Entity\ContentEntityForm::initFormLangcodes() */ - public static function prepareEntity(ContentEntityInterface $entity, FormStateInterface $form_state) { - $form_langcode = $form_state->get('langcode'); + public static function prepareEntity(ContentEntityInterface $entity, FormStateInterface $form_state, ?string $form_langcode = NULL) { + $form_langcode = $form_langcode ?? $form_state->get('langcode'); if (empty($form_langcode) || !$entity->isTranslatable()) { return $entity; } $entity_langcode = $entity->language()->getId(); - if (self::isTranslating($form_state) && !$entity->hasTranslation($form_langcode)) { - // Create a translation from the source language values. - $source = $form_state->get(['content_translation', 'source']); - $source_langcode = $source ? $source->getId() : $entity_langcode; - if (!$entity->hasTranslation($source_langcode)) { - $entity->addTranslation($source_langcode, $entity->toArray()); + // Create a translation from the source language values if we are on a + // translation form or use the default translation as a source instead. + if (!$entity->hasTranslation($form_langcode)) { + // If the entity is new and the form language code changed during the + // form submission then do not translate but change the language of the + // entity. + if ($entity->isNew()) { + $langcode_key = $entity->getEntityType()->getKey('langcode'); + $entity->set($langcode_key, $form_langcode); } - $source_translation = $entity->getTranslation($source_langcode); - $entity->addTranslation($form_langcode, $source_translation->toArray()); - $translation = $entity->getTranslation($form_langcode); - $translation->set('content_translation_source', $source_langcode); - // Make sure we do not inherit the affected status from the source values. - if ($entity->getEntityType()->isRevisionable()) { - $translation->setRevisionTranslationAffected(NULL); + else { + $should_translate_event = new ShouldTranslateEvent(TRUE, $form_state, $form_langcode, $entity); + \Drupal::service('event_dispatcher')->dispatch($should_translate_event); + $should_translate = $should_translate_event->getShouldTranslate(); + if ($should_translate) { + $source = $form_state->get(['content_translation', 'source']); + $source_langcode = $source && $entity->hasTranslation($source->getId()) ? $source->getId() : $entity_langcode; + $source_translation = $entity->getTranslation($source_langcode); + $entity = $entity->addTranslation($form_langcode, $source_translation->toArray()); + if ($entity->hasField('content_translation_source')) { + $entity->set('content_translation_source', $source_langcode); + } + // Make sure we do not inherit the affected status from the + // source values. + if ($entity->getEntityType()->isRevisionable()) { + $entity->setRevisionTranslationAffected(NULL); + } + } } } @@ -55,39 +72,6 @@ class TranslationHelper { return $entity; } - /** - * Updates the entity langcode to match the form langcode. - * - * Called on submit to allow the user to select a different language through - * the langcode form element, which is then transferred to form state. - * - * @param \Drupal\Core\Entity\ContentEntityInterface $entity - * The entity. - * @param \Drupal\Core\Form\FormStateInterface $form_state - * The current state of the form. - * - * @return bool - * TRUE if the entity langcode was updated, FALSE otherwise. - */ - public static function updateEntityLangcode(ContentEntityInterface $entity, FormStateInterface $form_state) { - $changed = FALSE; - // This method is first called during form validation, at which point - // the 'langcode' form state flag hasn't been updated with the new value. - $form_langcode = $form_state->getValue(['langcode', 0, 'value'], $form_state->get('langcode')); - if (empty($form_langcode) || !$entity->isTranslatable()) { - return $changed; - } - - $entity_langcode = $entity->language()->getId(); - if ($entity_langcode != $form_langcode && !$entity->hasTranslation($form_langcode)) { - $langcode_key = $entity->getEntityType()->getKey('langcode'); - $entity->set($langcode_key, $form_langcode); - $changed = TRUE; - } - - return $changed; - } - /** * Determines whether there's a translation in progress. * diff --git a/src/WidgetSubmit.php b/src/WidgetSubmit.php index 966ceffd4c03d5d88649b65ef13b893d6d8b0c65..a111a81f868398ee5b6632fa8357c8031ad48187 100644 --- a/src/WidgetSubmit.php +++ b/src/WidgetSubmit.php @@ -21,12 +21,47 @@ class WidgetSubmit { * The form. * @param \Drupal\Core\Form\FormStateInterface $form_state * The form state. + * @param bool $translation_submit + * (optional) A translation submit for the case where the main entity + * changes its language code but the widget is not active. */ - public static function attach(array &$form, FormStateInterface $form_state) { - // $form['#ief_element_submit'] runs after the #ief_element_submit - // callbacks of all subelements, which means that doSubmit() has - // access to the final IEF $form_state. - $form['#ief_element_submit'][] = [get_called_class(), 'doSubmit']; + public static function attach(array &$form, FormStateInterface $form_state, $translation_submit = FALSE) { + if (!$translation_submit) { + // $form['#ief_element_submit'] runs after the #ief_element_submit + // callbacks of all subelements, which means that doSubmit() has + // access to the final IEF $form_state. + $form['#ief_element_submit'][] = [get_called_class(), 'doSubmit']; + } + else { + // Entity form actions. + foreach (['submit', 'publish', 'unpublish'] as $action) { + if (!empty($form['actions'][$action])) { + self::addTranslationSubmit($form['actions'][$action], $form); + } + } + // Generic submit button. + if (!empty($form['submit'])) { + self::addTranslationSubmit($form['submit'], $form); + } + } + } + + /** + * Adds the translation submit handler to the given submit element. + * + * @param array $element + * The submit element. + * @param array $complete_form + * The complete form. + */ + public static function addTranslationSubmit(array &$element, array $complete_form) { + if (empty($element['#submit'])) { + // Drupal runs either the button-level callbacks or the form-level ones. + // Having no button-level callbacks indicates that the form has relied + // on form-level callbacks, which now need to be transferred. + $element['#submit'] = $complete_form['#submit']; + } + $element['#submit'] = array_merge([[get_called_class(), 'doTranslationSubmit']], $element['#submit']); } /** @@ -68,4 +103,27 @@ class WidgetSubmit { } } + /** + * Saves entities as part of a translation submit. + * + * @param array $form + * The form. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The form state. + */ + public static function doTranslationSubmit(array $form, FormStateInterface $form_state) { + $entities = $form_state->get('ief_entities_language_update') ?? []; + if (!empty($entities)) { + $referenceUpgrader = new ReferenceUpgrader(); + foreach ($entities as $entity_group) { + foreach ($entity_group as $entity) { + $handler = InlineEntityForm::getInlineFormHandler($entity->getEntityTypeId()); + $referenceUpgrader->upgradeEntityReferences($entity); + $handler->save($entity); + $referenceUpgrader->registerEntity($entity); + } + } + } + } + } diff --git a/tests/modules/inline_entity_form_test/config/install/field.field.node.ief_test_complex.multi.yml b/tests/modules/inline_entity_form_test/config/install/field.field.node.ief_test_complex.multi.yml index 389fce7255ec27a7f7f732a07819a3fb80cb23db..f0a1f543c778f6f75cb7ad7d643c5029e7848d06 100644 --- a/tests/modules/inline_entity_form_test/config/install/field.field.node.ief_test_complex.multi.yml +++ b/tests/modules/inline_entity_form_test/config/install/field.field.node.ief_test_complex.multi.yml @@ -5,6 +5,7 @@ dependencies: - field.storage.node.multi - node.type.ief_reference_type - node.type.ief_test_custom + - node.type.ief_test_complex module: - inline_entity_form_test enforced: @@ -17,7 +18,7 @@ bundle: ief_test_complex label: Multiple nodes description: 'Reference multiple nodes. A complex widget on [site:name].' required: true -translatable: false +translatable: true default_value: { } default_value_callback: '' settings: diff --git a/tests/modules/inline_entity_form_test/config/install/field.storage.node.multi.yml b/tests/modules/inline_entity_form_test/config/install/field.storage.node.multi.yml index c8fc0fdc9a7a0cd1f9ae3015ffb79ed9861f4198..0436d018da0ff2b4f6922a0810d4640f6d804580 100644 --- a/tests/modules/inline_entity_form_test/config/install/field.storage.node.multi.yml +++ b/tests/modules/inline_entity_form_test/config/install/field.storage.node.multi.yml @@ -16,6 +16,6 @@ settings: module: core locked: false cardinality: -1 -translatable: false +translatable: true indexes: { } persist_with_no_fields: false diff --git a/tests/modules/inline_entity_form_translation_test/config/install/language.content_settings.node.ief_test_complex.yml b/tests/modules/inline_entity_form_translation_test/config/install/language.content_settings.node.ief_test_complex.yml index 7790a43430244807035e2a0dfe0a19e0c3c3105e..39c4aecddfbb58f79d870d9fed7a42077c428213 100644 --- a/tests/modules/inline_entity_form_translation_test/config/install/language.content_settings.node.ief_test_complex.yml +++ b/tests/modules/inline_entity_form_translation_test/config/install/language.content_settings.node.ief_test_complex.yml @@ -9,7 +9,7 @@ third_party_settings: content_translation: enabled: true bundle_settings: - untranslatable_fields_hide: '0' + untranslatable_fields_hide: '1' id: node.ief_test_complex target_entity_type_id: node target_bundle: ief_test_complex diff --git a/tests/src/FunctionalJavascript/ComplexWidgetTest.php b/tests/src/FunctionalJavascript/ComplexWidgetTest.php index a2b7ab6fc1c103a1738f2abd026142e35a84ee5d..af7efeea06aeed2a406b4090ed2f162ee1a4f914 100644 --- a/tests/src/FunctionalJavascript/ComplexWidgetTest.php +++ b/tests/src/FunctionalJavascript/ComplexWidgetTest.php @@ -337,7 +337,7 @@ class ComplexWidgetTest extends InlineEntityFormTestBase { $inner_title_field_xpath = $this->getXpathForNthInputByLabelText('Title', 2); $first_name_field_xpath = $this->getXpathForNthInputByLabelText('First name', 1); $last_name_field_xpath = $this->getXpathForNthInputByLabelText('Last name', 1); - $first_delete_checkbox_xpath = $this->getXpathForNthInputByLabelText('Delete this node from the system.', 1); + $first_delete_checkbox_xpath = $this->getXpathForNthInputByLabelText('Delete this node from the system. This will also delete its translations.', 1); $assert_session = $this->assertSession(); $page = $this->getSession()->getPage(); @@ -725,7 +725,7 @@ class ComplexWidgetTest extends InlineEntityFormTestBase { // Without delete permission, the checkbox to delete the entity when // removing the reference should not be visible. - $delete_checkbox_xpath = $this->getXpathForNthInputByLabelText('Delete this node from the system.', 1); + $delete_checkbox_xpath = $this->getXpathForNthInputByLabelText('Delete this node from the system. This will also delete its translations.', 1); $assert_session->elementsCount('css', 'tr.ief-row-entity', 5); $assert_session->elementExists('xpath', '(//input[@value="Remove"])[1]')->press(); $assert_session->elementNotExists('xpath', $delete_checkbox_xpath); diff --git a/tests/src/FunctionalJavascript/TranslationTest.php b/tests/src/FunctionalJavascript/TranslationTest.php index 3bc4421988efcdbb790d6acbcaeb356503a78677..ecb1caa2ad9a6d40cc797d6a0ef9fac90c04a6e2 100644 --- a/tests/src/FunctionalJavascript/TranslationTest.php +++ b/tests/src/FunctionalJavascript/TranslationTest.php @@ -53,9 +53,9 @@ class TranslationTest extends InlineEntityFormTestBase { } /** - * Tests translating inline entities. + * Tests symmetric translation of inline entities. */ - public function testTranslation() { + public function testSymmetricTranslation() { // Get the xpath selectors for the fields in this test. $first_nested_title_field_xpath = $this->getXpathForNthInputByLabelText('Title', 2); $first_name_field_xpath = $this->getXpathForNthInputByLabelText('First name', 1); @@ -64,12 +64,16 @@ class TranslationTest extends InlineEntityFormTestBase { $assert_session = $this->assertSession(); $page = $this->getSession()->getPage(); + $changed_timestamp = 1727766128; + $created_timestamp = 1727766128; // Create a German node with a French translation. $first_inline_node = Node::create([ 'type' => 'ief_reference_type', 'langcode' => 'de', 'title' => 'Kann ein Känguru höher als ein Haus springen?', 'first_name' => 'Dieter', + 'changed' => $changed_timestamp, + 'created' => $created_timestamp, ]); $translation = $first_inline_node->toArray(); $translation['title'][0]['value'] = "Un kangourou peut-il sauter plus haut qu'une maison?"; @@ -102,10 +106,23 @@ class TranslationTest extends InlineEntityFormTestBase { $page->pressButton('Save'); $assert_session->pageTextContains('IEF test complex A node has been created.'); - // Both inline nodes should now be in English. + // The German node should now have an additional English translation + // and the English node should just have English. $first_inline_node = $this->drupalGetNodeByTitle('Kann ein Känguru höher als ein Haus springen?'); + $this->assertEquals($created_timestamp, $first_inline_node->getCreatedTime()); + $this->assertEquals($changed_timestamp, $first_inline_node->getChangedTime()); + $this->assertSame('de', $first_inline_node->get('langcode')->value, 'The first inline entity has the correct langcode.'); + $this->assertTrue($first_inline_node->hasTranslation('fr'), 'The first inline entity has a fr translation.'); + $this->assertTrue($first_inline_node->hasTranslation('en'), 'The first inline entity has an en translation.'); + + // Ensure the timestamps for translated nodes get updated + // when new translations are created. + $english_translation = $first_inline_node->getTranslation('en'); + $this->assertEquals('en', $english_translation->get('langcode')->value); + $this->assertGreaterThan($created_timestamp, $english_translation->getCreatedTime()); + $this->assertGreaterThan($changed_timestamp, $english_translation->getChangedTime()); + $second_inline_node = $this->drupalGetNodeByTitle('Can a kangaroo jump higher than a house?'); - $this->assertSame('en', $first_inline_node->get('langcode')->value, 'The first inline entity has the correct langcode.'); $this->assertEquals('en', $second_inline_node->get('langcode')->value, 'The second inline entity has the correct langcode.'); // Edit the parent node and change the source language to German. @@ -113,12 +130,16 @@ class TranslationTest extends InlineEntityFormTestBase { $this->drupalGet('node/' . $node->id() . '/edit'); $page->selectFieldOption('langcode[0][value]', 'de'); $page->pressButton('Save'); + $assert_session->pageTextContains('IEF test complex A node has been updated.'); - // Both inline nodes should now be in German. + // The English node should now have a German translation too. $first_inline_node = $this->drupalGetNodeByTitle('Kann ein Känguru höher als ein Haus springen?', TRUE); $second_inline_node = $this->drupalGetNodeByTitle('Can a kangaroo jump higher than a house?', TRUE); $this->assertSame('de', $first_inline_node->get('langcode')->value, 'The first inline entity has the correct langcode.'); - $this->assertSame('de', $second_inline_node->get('langcode')->value, 'The second inline entity has the correct langcode.'); + $this->assertTrue($first_inline_node->hasTranslation('fr'), 'The first inline entity has a fr translation.'); + $this->assertTrue($first_inline_node->hasTranslation('en'), 'The first inline entity has an en translation.'); + $this->assertSame('en', $second_inline_node->get('langcode')->value, 'The second inline entity has the correct langcode.'); + $this->assertTrue($second_inline_node->hasTranslation('de'), 'The second inline entity has a de translation.'); // Add a German -> French translation of the parent node. $this->drupalGet('node/' . $node->id() . '/translations/add/de/fr'); @@ -168,4 +189,171 @@ class TranslationTest extends InlineEntityFormTestBase { $this->assertSame('Jacques', $second_translation->first_name->value); } + /** + * Test asymmetric translations. + */ + public function testAsymmetricTranslations(): void { + + // Allow asymmetric translation. + $form_display_storage = $this->container->get('entity_type.manager')->getStorage('entity_form_display'); + /** @var \Drupal\Core\Entity\Display\EntityFormDisplayInterface $display */ + $display = $form_display_storage->load('node.ief_test_complex.default'); + $component = $display->getComponent('multi'); + $component['settings']['allow_asymmetric_translation'] = TRUE; + $display->setComponent('multi', $component)->save(); + + $assert_session = $this->assertSession(); + $page = $this->getSession()->getPage(); + + // Create inline node 1 in en. + $first_inline_node = Node::create([ + 'type' => 'ief_reference_type', + 'langcode' => 'en', + 'title' => 'This is inline node 1', + 'first_name' => 'John', + ]); + $first_inline_node->save(); + + // Create inline node 2 in en. + $second_inline_node = Node::create([ + 'type' => 'ief_reference_type', + 'langcode' => 'en', + 'title' => 'This is inline node 2', + 'first_name' => 'Jim', + ]); + $second_inline_node->save(); + + // Create inline node 3 in de. + $third_inline_node = Node::create([ + 'type' => 'ief_reference_type', + 'langcode' => 'de', + 'title' => 'This is inline node 3', + 'first_name' => 'Constantin', + ]); + $third_inline_node->save(); + + $this->drupalGet('node/add/ief_test_complex'); + $multi_fieldset = $assert_session->elementExists('css', 'fieldset[data-drupal-selector="edit-multi"]'); + + // Reference node 1. + $multi_fieldset->pressButton('Add existing node'); + $this->assertNotEmpty($field = $assert_session->waitForElement('xpath', $this->getXpathForAutoCompleteInput())); + $field->setValue('This is inline node 1 (' . $first_inline_node->id() . ')'); + $page->pressButton('Add node'); + $this->waitForRowByTitle('This is inline node 1'); + + // Reference node 2. + $multi_fieldset->pressButton('Add existing node'); + $this->assertNotEmpty($field = $assert_session->waitForElement('xpath', $this->getXpathForAutoCompleteInput())); + $field->setValue('This is inline node 2 (' . $second_inline_node->id() . ')'); + $page->pressButton('Add node'); + $this->waitForRowByTitle('This is inline node 2'); + + // Save the parent node in en. + $assert_session->elementsCount('css', 'tr.ief-row-entity', 2); + $page->fillField('title[0][value]', 'A node'); + $page->selectFieldOption('langcode[0][value]', 'en'); + $page->pressButton('Save'); + + // Make some initial assertions about the inline entities. + $node = $this->drupalGetNodeByTitle('A node'); + $this->drupalGet('/node/' . $node->id() . '/edit'); + $this->assertRowByTitle('This is inline node 1'); + $this->assertRowByTitle('This is inline node 2'); + + // Add an English -> German translation of the parent node. + $this->drupalGet('/node/' . $node->id() . '/translations/add/en/de'); + $assert_session->elementTextContains('xpath', '//fieldset[@id="edit-multi"]/legend/span', 'Multiple nodes'); + + // Confirm that the add and remove buttons are present with enabled + // 'allow_asymmetric_translation' setting. + $this->assertTrue((bool) $this->xpath('//input[@type="submit" and @value="Add new node" and @data-drupal-selector="edit-multi-actions-ief-add"]'), 'Add new node button appears in the table.'); + $this->assertTrue((bool) $this->xpath('//input[@type="submit" and @value="Add existing node" and @data-drupal-selector="edit-multi-actions-ief-add-existing"]'), 'Add existing node button appears in the table.'); + $this->assertTrue((bool) $this->xpath('//input[@type="submit" and @value="Remove"]'), 'Remove button appears in the table.'); + + // Change the title of node 1 for easier identification. + $first_nested_title_field_xpath = $this->getXpathForNthInputByLabelText('Title', 2); + $first_reference = $this->assertRowByTitle('This is inline node 1'); + $first_reference->getParent()->pressButton('Edit'); + $this->assertNotEmpty($update_button = $assert_session->waitForButton('Update node')); + $assert_session->elementExists('xpath', $first_nested_title_field_xpath)->setValue("This is inline node 1 (translated)"); + $update_button->press(); + $this->waitForRowByTitle("This is inline node 1 (translated)"); + + // Change the title of node 2 for easier identification. + $second_reference = $this->assertRowByTitle('This is inline node 2'); + $second_reference->getParent()->pressButton('Edit'); + $this->assertNotEmpty($update_button = $assert_session->waitForButton('Update node')); + $assert_session->elementExists('xpath', $first_nested_title_field_xpath)->setValue("This is inline node 2 (translated)"); + $update_button->press(); + $this->waitForRowByTitle("This is inline node 2 (translated)"); + + // Test asymmetric translation by referencing node 3. + $multi_fieldset->pressButton('Add existing node'); + $assert_session->assertWaitOnAjaxRequest(); + $this->assertNotEmpty($field = $assert_session->waitForElement('xpath', $this->getXpathForAutoCompleteInput())); + $field->setValue('This is inline node 3 (' . $third_inline_node->id() . ')'); + $page->pressButton('Add node'); + $this->waitForRowByTitle('This is inline node 3'); + + // Save the page so that translations get created. + $page->pressButton('Save (this translation)'); + + // Now revisit the page and test asymmetric translation + // by removing node 1 from this translation. + $this->drupalGet('/de/node/' . $node->id() . '/edit'); + $multi_fieldset->pressButton('Remove'); + $this->assertTrue($assert_session->waitForText('Are you sure you want to remove This is inline node 1 (translated)?')); + $multi_fieldset->pressButton('Remove'); + $assert_session->assertWaitOnAjaxRequest(); + + $assert_session->elementsCount('css', 'tr.ief-row-entity', 2); + $this->assertRowByTitle('This is inline node 2 (translated)'); + $this->assertRowByTitle('This is inline node 3'); + + // Re-save the page so the entity gets removed + // from this translation. + $page->pressButton('Save (this translation)'); + + $this->drupalGet('/node/' . $node->id() . '/edit'); + + $node1 = $this->drupalGetNodeByTitle('This is inline node 1'); + $this->assertNotEmpty($node1); + $this->assertEquals('en', $node1->language()->getId()); + $this->assertTrue($node1->hasTranslation('de')); + + $node2 = $this->drupalGetNodeByTitle('This is inline node 2'); + $this->assertNotEmpty($node2); + $this->assertEquals('en', $node2->language()->getId()); + $this->assertTrue($node2->hasTranslation('de')); + + $node3 = $this->drupalGetNodeByTitle('This is inline node 3'); + $this->assertNotEmpty($node3); + $this->assertEquals('de', $node3->language()->getId()); + $this->assertFalse($node3->hasTranslation('en')); + + $assert_session->elementsCount('css', 'tr.ief-row-entity', 2); + $this->assertRowByTitle('This is inline node 1'); + $this->assertRowByTitle('This is inline node 2'); + + // Test asymmetric translation by removing node 2 + // completely from the system. + $this->drupalGet('/de/node/' . $node->id() . '/edit'); + + $multi_fieldset->pressButton('Remove'); + $this->assertTrue($assert_session->waitForText('Are you sure you want to remove This is inline node 2 (translated)?')); + $multi_fieldset->checkField('Delete this node from the system. This will also delete its translations.'); + $multi_fieldset->pressButton('Remove'); + $assert_session->assertWaitOnAjaxRequest(); + + $assert_session->elementsCount('css', 'tr.ief-row-entity', 1); + $this->assertRowByTitle('This is inline node 3'); + + $page->pressButton('Save (this translation)'); + + $node2 = $this->drupalGetNodeByTitle('This is inline node 2', TRUE); + $this->assertEmpty($node2); + + } + }