Skip to content
Snippets Groups Projects
Commit cc8ff21b authored by Michael Lee's avatar Michael Lee
Browse files

Installing drupal/entity_clone (1.0.0-beta7)

parent f708cde5
No related branches found
No related tags found
No related merge requests found
Showing
with 1038 additions and 83 deletions
......@@ -115,7 +115,7 @@
"drupal/embed": "1.6",
"drupal/entity": "1.2",
"drupal/entity_browser": "2.8",
"drupal/entity_clone": "1.0.0-beta5",
"drupal/entity_clone": "1.0.0-beta7",
"drupal/entity_embed": "1.3",
"drupal/entity_reference_revisions": "1.9",
"drupal/exif_orientation": "^1.1",
......@@ -287,7 +287,7 @@
"UTC Time Adjustment": "patches/utc-time-adjustment.patch"
},
"drupal/entity_clone": {
"3060223": "https://www.drupal.org/files/issues/2019-10-17/%20entity_clone-corrupted-paragraph-cloning-3060223-5.patch"
"3050027": "https://www.drupal.org/files/issues/2022-10-10/entity_clone-clone_layout_builder-3050027-34.patch"
},
"drupal/entity_embed": {
"3077225": "https://www.drupal.org/files/issues/2019-12-11/3077225-10.reduce-invalid-config-logs.patch"
......
......@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "57c898b347489d215f8a0e5ace949ee7",
"content-hash": "cdcdcd30a5e546a59b25821bef7e01d6",
"packages": [
{
"name": "alchemy/zippy",
......@@ -4024,26 +4024,32 @@
},
{
"name": "drupal/entity_clone",
"version": "1.0.0-beta5",
"version": "1.0.0-beta7",
"source": {
"type": "git",
"url": "https://git.drupalcode.org/project/entity_clone.git",
"reference": "8.x-1.0-beta5"
"reference": "8.x-1.0-beta7"
},
"dist": {
"type": "zip",
"url": "https://ftp.drupal.org/files/projects/entity_clone-8.x-1.0-beta5.zip",
"reference": "8.x-1.0-beta5",
"shasum": "5220d23ac01cd13ff7ef48589e9f6438659a8736"
"url": "https://ftp.drupal.org/files/projects/entity_clone-8.x-1.0-beta7.zip",
"reference": "8.x-1.0-beta7",
"shasum": "964b72b21a7e219cf337743ed5c8850235197594"
},
"require": {
"drupal/core": "^8 || ^9"
},
"require-dev": {
"drupal/entity_browser": "2.x-dev",
"drupal/entity_usage": "2.x-dev",
"drupal/paragraphs": "^1.0",
"drupal/search_api": "~1.0"
},
"type": "drupal-module",
"extra": {
"drupal": {
"version": "8.x-1.0-beta5",
"datestamp": "1615822112",
"version": "8.x-1.0-beta7",
"datestamp": "1663573486",
"security-coverage": {
"status": "not-covered",
"message": "Beta releases are not covered by Drupal security advisories."
......@@ -4076,8 +4082,8 @@
"homepage": "https://www.drupal.org/user/1361586"
}
],
"description": "Add a clone action for all entities",
"homepage": "https://www.drupal.org/project/entity_clone",
"description": "Add a clone action for all entities.",
"homepage": "https://drupal.org/project/entity_clone",
"support": {
"source": "https://git.drupalcode.org/project/entity_clone"
}
......
......@@ -4141,34 +4141,40 @@
},
{
"name": "drupal/entity_clone",
"version": "1.0.0-beta5",
"version_normalized": "1.0.0.0-beta5",
"version": "1.0.0-beta7",
"version_normalized": "1.0.0.0-beta7",
"source": {
"type": "git",
"url": "https://git.drupalcode.org/project/entity_clone.git",
"reference": "8.x-1.0-beta5"
"reference": "8.x-1.0-beta7"
},
"dist": {
"type": "zip",
"url": "https://ftp.drupal.org/files/projects/entity_clone-8.x-1.0-beta5.zip",
"reference": "8.x-1.0-beta5",
"shasum": "5220d23ac01cd13ff7ef48589e9f6438659a8736"
"url": "https://ftp.drupal.org/files/projects/entity_clone-8.x-1.0-beta7.zip",
"reference": "8.x-1.0-beta7",
"shasum": "964b72b21a7e219cf337743ed5c8850235197594"
},
"require": {
"drupal/core": "^8 || ^9"
},
"require-dev": {
"drupal/entity_browser": "2.x-dev",
"drupal/entity_usage": "2.x-dev",
"drupal/paragraphs": "^1.0",
"drupal/search_api": "~1.0"
},
"type": "drupal-module",
"extra": {
"drupal": {
"version": "8.x-1.0-beta5",
"datestamp": "1615822112",
"version": "8.x-1.0-beta7",
"datestamp": "1663573486",
"security-coverage": {
"status": "not-covered",
"message": "Beta releases are not covered by Drupal security advisories."
}
},
"patches_applied": {
"3060223": "https://www.drupal.org/files/issues/2019-10-17/%20entity_clone-corrupted-paragraph-cloning-3060223-5.patch"
"3050027": "https://www.drupal.org/files/issues/2022-10-10/entity_clone-clone_layout_builder-3050027-34.patch"
}
},
"installation-source": "dist",
......@@ -4177,21 +4183,29 @@
"GPL-2.0-or-later"
],
"authors": [
{
"name": "colan",
"homepage": "https://www.drupal.org/user/58704"
},
{
"name": "NickDickinsonWilde",
"homepage": "https://www.drupal.org/user/3094661"
},
{
"name": "colan",
"homepage": "https://www.drupal.org/user/58704"
"name": "Rajeshreeputra",
"homepage": "https://www.drupal.org/user/3418561"
},
{
"name": "Upchuk",
"homepage": "https://www.drupal.org/user/1885838"
},
{
"name": "vpeltot",
"homepage": "https://www.drupal.org/user/1361586"
}
],
"description": "Add a clone action for all entities",
"homepage": "https://www.drupal.org/project/entity_clone",
"description": "Add a clone action for all entities.",
"homepage": "https://drupal.org/project/entity_clone",
"support": {
"source": "https://git.drupalcode.org/project/entity_clone"
},
......
......@@ -3,7 +3,7 @@
'name' => 'osu-asc-webservices/d8-upstream',
'pretty_version' => 'dev-master',
'version' => 'dev-master',
'reference' => '2475984787276b11778650ebbcd6963d3ed140b6',
'reference' => 'f708cde51271cc92aa33da1cf90b333b9ab06e6d',
'type' => 'project',
'install_path' => __DIR__ . '/../../',
'aliases' => array(),
......@@ -749,9 +749,9 @@
'dev_requirement' => false,
),
'drupal/entity_clone' => array(
'pretty_version' => '1.0.0-beta5',
'version' => '1.0.0.0-beta5',
'reference' => '8.x-1.0-beta5',
'pretty_version' => '1.0.0-beta7',
'version' => '1.0.0.0-beta7',
'reference' => '8.x-1.0-beta7',
'type' => 'drupal-module',
'install_path' => __DIR__ . '/../../web/modules/entity_clone',
'aliases' => array(),
......@@ -1594,7 +1594,7 @@
'osu-asc-webservices/d8-upstream' => array(
'pretty_version' => 'dev-master',
'version' => 'dev-master',
'reference' => '2475984787276b11778650ebbcd6963d3ed140b6',
'reference' => 'f708cde51271cc92aa33da1cf90b333b9ab06e6d',
'type' => 'project',
'install_path' => __DIR__ . '/../../',
'aliases' => array(),
......
This file was automatically generated by Composer Patches (https://github.com/cweagans/composer-patches)
Patches applied to this directory:
3060223
Source: https://www.drupal.org/files/issues/2019-10-17/%20entity_clone-corrupted-paragraph-cloning-3060223-5.patch
3050027
Source: https://www.drupal.org/files/issues/2022-10-10/entity_clone-clone_layout_builder-3050027-34.patch
{
"name": "drupal/entity_clone",
"description": "Add a clone action for all entities.",
"homepage": "https://drupal.org/project/entity_clone",
"type": "drupal-module",
"license": "GPL-2.0-or-later",
"require-dev": {
"drupal/entity_browser": "2.x-dev",
"drupal/entity_usage": "2.x-dev",
"drupal/paragraphs": "^1.0",
"drupal/search_api": "~1.0"
}
}
......@@ -4,8 +4,13 @@ core: "8.x"
core_version_requirement: ^8 || ^9
type: module
configure: entity_clone.settings
test_dependencies:
- entity_browser:entity_browser
- entity_usage:entity_usage
- paragraphs:paragraphs
- search_api:search_api
# Information added by Drupal.org packaging script on 2021-03-15
version: '8.x-1.0-beta5'
# Information added by Drupal.org packaging script on 2022-09-19
version: '8.x-1.0-beta7'
project: 'entity_clone'
datestamp: 1615822114
datestamp: 1663573487
......@@ -6,6 +6,7 @@
*/
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Cache\CacheableMetadata;
use Drupal\Core\Entity\ContentEntityTypeInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Routing\RouteMatchInterface;
......@@ -121,7 +122,7 @@ function entity_clone_entity_type_build(array &$entity_types) {
* @see \Drupal\Core\Entity\EntityListBuilderInterface::getOperations()
*/
function entity_clone_entity_operation(EntityInterface $entity) {
if ($entity->hasLinkTemplate('clone-form') && $entity->access('clone')) {
if ($entity->hasLinkTemplate('clone-form') && $entity->access('clone') && !$entity->isNew()) {
return [
'clone' => [
'title' => t('Clone'),
......@@ -139,8 +140,39 @@ function entity_clone_entity_operation(EntityInterface $entity) {
* Implements hook_entity_access().
*/
function entity_clone_entity_access(EntityInterface $entity, $operation, AccountInterface $account) {
if ($operation === 'clone') {
return AccessResult::allowedIfHasPermission($account, 'clone ' . $entity->getEntityTypeId() . ' entity');
if ($operation !== 'clone') {
return AccessResult::neutral();
}
return AccessResult::neutral();
$cache = new CacheableMetadata();
$cache->addCacheContexts(['user.permissions']);
// Deny access if the user cannot clone the entity.
$access = AccessResult::forbiddenIf(!$account->hasPermission('clone ' . $entity->getEntityTypeId() . ' entity'));
if ($access->isForbidden()) {
return $access->addCacheableDependency($cache);
}
// Deny access if the user can clone but cannot create new entities of this
// type. However, we have some exceptions in which the access control handler
// doesn't have a say in things. In these cases, we go based on the clone
// permission only.
$exceptions = [
'file',
'paragraph',
];
if (in_array($entity->getEntityTypeId(), $exceptions)) {
return AccessResult::allowed()->addCacheableDependency($cache);
}
$handler = \Drupal::entityTypeManager()->getAccessControlHandler($entity->getEntityTypeId());
$access = $handler->createAccess($entity->bundle(), $account, [], TRUE);
if (!$access->isAllowed()) {
$cache->addCacheableDependency($access);
$forbidden = AccessResult::forbidden();
return $forbidden->addCacheableDependency($cache);
}
return AccessResult::allowed()->addCacheableDependency($cache);
}
......@@ -7,3 +7,6 @@ services:
arguments: ['@entity_type.manager']
tags:
- { name: event_subscriber }
entity_clone.service_provider:
class: Drupal\entity_clone\Services\EntityCloneServiceProvider
arguments: [ ]
......@@ -2,6 +2,7 @@
namespace Drupal\entity_clone\EntityClone\Content;
use Drupal\block_content\BlockContentInterface;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Entity\ContentEntityStorageInterface;
use Drupal\Core\Entity\EntityHandlerInterface;
......@@ -15,6 +16,7 @@
use Drupal\Core\StringTranslation\TranslationManager;
use Drupal\entity_clone\EntityClone\EntityCloneFormInterface;
use Drupal\entity_clone\EntityCloneSettingsManager;
use Drupal\layout_builder\SectionListInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
......@@ -100,6 +102,12 @@ public function formElement(EntityInterface $entity, $parent = TRUE, &$discovere
$form['recursive'] = array_merge($form['recursive'], $this->getRecursiveFormElement($field_definition, $field_id, $field, $discovered_entities));
}
}
if ($field_definition instanceof FieldConfigInterface && $field_definition->getType() === 'layout_section') {
$field = $entity->get($field_id);
if ($field->count() > 0) {
$form['recursive'] = array_merge($form['recursive'], $this->getRecursiveLayoutFormElement($field_definition, $field_id, $field, $discovered_entities));
}
}
}
if ($parent) {
......@@ -115,6 +123,87 @@ public function formElement(EntityInterface $entity, $parent = TRUE, &$discovere
return $form;
}
/**
* Get the recursive Inline Blocks form element.
*
* @param \Drupal\Core\Field\FieldConfigInterface $field_definition
* The field definition.
* @param string $field_id
* The field ID.
* @param \Drupal\layout_builder\SectionListInterface $field
* The field item.
* @param array $discovered_entities
* List of all entities already discovered.
*
* @return array
* The form element for a recursive clone.
*
* @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
* @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
*/
protected function getRecursiveLayoutFormElement(FieldConfigInterface $field_definition, $field_id, SectionListInterface $field, array &$discovered_entities) {
$form_element = [
'#tree' => TRUE,
];
$target = $field_definition->getFieldStorageDefinition()->getSetting('target_type');
if ($target !== NULL) {
$fieldset_access = !$this->entityCloneSettingsManager->getHiddenValue($target);
}
else {
$fieldset_access = !$this->entityCloneSettingsManager->getLayoutReferencesSetting();
}
$form_element[$field_definition->id()] = [
'#type' => 'fieldset',
'#title' => $this->translationManager->translate('Entities referenced by field <em>@label (@field_id)</em>.', [
'@label' => $field_definition->label(),
'@field_id' => $field_id,
]),
'#description' => $this->translationManager->translate('Layout builder blocks always have to be cloned in order to preserve the integrity of the system.'),
'#access' => $fieldset_access,
'#description_should_be_shown' => $fieldset_access,
];
foreach ($field as $value) {
if ($value->isEmpty()) {
continue;
}
$section = $value->section;
if (!$section) {
continue;
}
$components = $section->getComponents();
foreach ($components as $component) {
// If it is not a revisionable block, skip.
if (!isset($component->toArray()['configuration']) || !isset($component->toArray()['configuration']['block_revision_id'])) {
continue;
}
$configuration = $component->toArray()['configuration'];
$referenced_entity = \Drupal::entityTypeManager()->getStorage('block_content')->loadRevision($configuration['block_revision_id']);
if ($referenced_entity instanceof BlockContentInterface && $referenced_entity->id()) {
$form_element[$field_definition->id()]['references'][$referenced_entity->id()]['clone'] = [
'#type' => 'checkbox',
'#title' => $this->translationManager->translate('Clone entity <strong>ID:</strong> <em>@entity_id</em>, <strong>Type:</strong> <em>@entity_type - @bundle</em>, <strong>Label:</strong> <em>@entity_label</em>', [
'@entity_id' => $referenced_entity->id(),
'@entity_type' => $referenced_entity->getEntityTypeId(),
'@bundle' => $referenced_entity->bundle(),
'@entity_label' => $referenced_entity->label(),
]),
'#default_value' => TRUE,
'#disabled' => TRUE,
'#access' => $referenced_entity->access('view label'),
];
}
if ($referenced_entity instanceof ContentEntityInterface) {
$form_element[$field_definition->id()]['references'][$referenced_entity->id()]['children'] = $this->getChildren($referenced_entity, $discovered_entities);
}
}
}
return $form_element;
}
/**
* Get the recursive form element.
*
......
......@@ -16,6 +16,9 @@ class FileEntityClone extends ContentEntityCloneBase {
public function cloneEntity(EntityInterface $entity, EntityInterface $cloned_entity, array $properties = [], array &$already_cloned = []) {
/** @var \Drupal\file\FileInterface $cloned_entity */
$cloned_file = file_copy($cloned_entity, $cloned_entity->getFileUri(), FileSystemInterface::EXISTS_RENAME);
if (isset($properties['take_ownership']) && $properties['take_ownership'] === 1) {
$cloned_file->setOwnerId(\Drupal::currentUser()->id());
}
return parent::cloneEntity($entity, $cloned_file, $properties);
}
......
......@@ -6,6 +6,7 @@
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\StringTranslation\TranslationManager;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Drupal\entity_clone\Services\EntityCloneServiceProvider;
/**
* Provides dynamic permissions of the entity_clone module.
......@@ -26,6 +27,13 @@ class EntityClonePermissions implements ContainerInjectionInterface {
*/
protected $translationManager;
/**
* The Service Provider that verifies if entity has ownership.
*
* @var \Drupal\entity_clone\Services\EntityCloneServiceProvider
*/
protected $serviceProvider;
/**
* Constructs a new EntityClonePermissions instance.
*
......@@ -33,10 +41,13 @@ class EntityClonePermissions implements ContainerInjectionInterface {
* The entity type manager.
* @param \Drupal\Core\StringTranslation\TranslationManager $string_translation
* The string translation manager.
* @param \\Drupal\entity_clone\Services\EntityCloneServiceProvider $service_provider
* The Service Provider that verifies if entity has ownership.
*/
public function __construct(EntityTypeManagerInterface $entity_manager, TranslationManager $string_translation) {
public function __construct(EntityTypeManagerInterface $entity_manager, TranslationManager $string_translation, EntityCloneServiceProvider $service_provider) {
$this->entityTypeManager = $entity_manager;
$this->translationManager = $string_translation;
$this->serviceProvider = $service_provider;
}
/**
......@@ -45,7 +56,8 @@ public function __construct(EntityTypeManagerInterface $entity_manager, Translat
public static function create(ContainerInterface $container) {
return new static(
$container->get('entity_type.manager'),
$container->get('string_translation')
$container->get('string_translation'),
$container->get('entity_clone.service_provider')
);
}
......@@ -62,6 +74,12 @@ public function permissions() {
$permissions['clone ' . $entity_type_id . ' entity'] = $this->translationManager->translate('Clone all <em>@label</em> entities.', [
'@label' => $entity_type->getLabel(),
]);
if ($this->serviceProvider->entityTypeHasOwnerTrait($entity_type)) {
$permissions['take_ownership_on_clone ' . $entity_type_id . ' entity'] = $this->translationManager->translate('Allow user to take ownership of <em>@label</em> cloned entities', [
'@label' => $entity_type->getLabel(),
]);
}
}
return $permissions;
......
......@@ -146,4 +146,38 @@ public function getHiddenValue($entity_type_id) {
return FALSE;
}
/**
* Set the take ownership setting.
*
* @param int $setting
* The settings from the form.
*/
public function setTakeOwnershipSettings(int $setting) {
$this->editableConfig->set('take_ownership', $setting)->save();
}
/**
* Get the take ownership settings.
*/
public function getTakeOwnershipSetting() {
return $this->config->get('take_ownership') ?? FALSE;
}
/**
* Set the layout builder references setting.
*
* @param int $setting
* The settings from the form.
*/
public function setLayoutReferencesSettings(int $setting) {
$this->editableConfig->set('hide_layout_builder_referenced_entities', $setting)->save();
}
/**
* Get the layout builder references settings.
*/
public function getLayoutReferencesSetting() {
return $this->config->get('hide_layout_builder_referenced_entities') ?? FALSE;
}
}
......@@ -8,7 +8,10 @@
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Messenger\Messenger;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Session\AccountProxyInterface;
use Drupal\Core\StringTranslation\TranslationManager;
use Drupal\entity_clone\Services\EntityCloneServiceProvider;
use Drupal\entity_clone\EntityCloneSettingsManager;
use Drupal\entity_clone\Event\EntityCloneEvent;
use Drupal\entity_clone\Event\EntityCloneEvents;
use Symfony\Component\DependencyInjection\ContainerInterface;
......@@ -61,6 +64,27 @@ class EntityCloneForm extends FormBase {
*/
protected $messenger;
/**
* The current user.
*
* @var \Drupal\Core\Session\AccountInterface
*/
protected $currentUser;
/**
* The entity clone settings manager service.
*
* @var \Drupal\entity_clone\EntityCloneSettingsManager
*/
protected $entityCloneSettingsManager;
/**
* The Service Provider that verifies if entity has ownership.
*
* @var \Drupal\entity_clone\Services\EntityCloneServiceProvider
*/
protected $serviceProvider;
/**
* Constructs a new Entity Clone form.
*
......@@ -74,10 +98,16 @@ class EntityCloneForm extends FormBase {
* The event dispatcher service.
* @param \Drupal\Core\Messenger\Messenger $messenger
* The messenger service.
* @param \Drupal\Core\Session\AccountProxyInterface $currentUser
* The current user.
* @param \Drupal\entity_clone\EntityCloneSettingsManager $entity_clone_settings_manager
* The entity clone settings manager.
* @param \Drupal\entity_clone\Services\EntityCloneServiceProvider $service_provider
* The Service Provider that verifies if entity has ownership.
*
* @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
*/
public function __construct(EntityTypeManagerInterface $entity_type_manager, RouteMatchInterface $route_match, TranslationManager $string_translation, EventDispatcherInterface $eventDispatcher, Messenger $messenger) {
public function __construct(EntityTypeManagerInterface $entity_type_manager, RouteMatchInterface $route_match, TranslationManager $string_translation, EventDispatcherInterface $eventDispatcher, Messenger $messenger, AccountProxyInterface $currentUser, EntityCloneSettingsManager $entity_clone_settings_manager, EntityCloneServiceProvider $service_provider) {
$this->entityTypeManager = $entity_type_manager;
$this->stringTranslationManager = $string_translation;
$this->eventDispatcher = $eventDispatcher;
......@@ -87,6 +117,9 @@ public function __construct(EntityTypeManagerInterface $entity_type_manager, Rou
$this->entity = $route_match->getParameter($parameter_name);
$this->entityTypeDefinition = $entity_type_manager->getDefinition($this->entity->getEntityTypeId());
$this->currentUser = $currentUser;
$this->entityCloneSettingsManager = $entity_clone_settings_manager;
$this->serviceProvider = $service_provider;
}
/**
......@@ -98,7 +131,10 @@ public static function create(ContainerInterface $container) {
$container->get('current_route_match'),
$container->get('string_translation'),
$container->get('event_dispatcher'),
$container->get('messenger')
$container->get('messenger'),
$container->get('current_user'),
$container->get('entity_clone.settings.manager'),
$container->get('entity_clone.service_provider')
);
}
......@@ -120,6 +156,15 @@ public function buildForm(array $form, FormStateInterface $form_state) {
$entity_clone_form_handler = $this->entityTypeManager->getHandler($this->entityTypeDefinition->id(), 'entity_clone_form');
$form = array_merge($form, $entity_clone_form_handler->formElement($this->entity));
}
$entityType = $this->getEntity()->getEntityTypeId();
if ($this->serviceProvider->entityTypeHasOwnerTrait($this->getEntity()->getEntityType()) && $this->currentUser->hasPermission('take_ownership_on_clone ' . $entityType . ' entity')) {
$form['take_ownership'] = [
'#type' => 'checkbox',
'#title' => $this->stringTranslationManager->translate('Take ownership'),
'#default_value' => $this->entityCloneSettingsManager->getTakeOwnershipSetting(),
'#description' => $this->stringTranslationManager->translate('Take ownership of the newly created cloned entity.'),
];
}
$form['actions'] = ['#type' => 'actions'];
$form['actions']['clone'] = [
......@@ -186,7 +231,7 @@ public function cancelForm(array &$form, FormStateInterface $form_state) {
}
/**
* Set a redirect on form state.
* Sets a redirect on form state.
*
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current state of the form.
......@@ -195,7 +240,7 @@ public function cancelForm(array &$form, FormStateInterface $form_state) {
*/
protected function formSetRedirect(FormStateInterface $form_state, EntityInterface $entity) {
if ($entity && $entity->hasLinkTemplate('canonical')) {
$form_state->setRedirect($entity->toUrl()->getRouteName(), $entity->toUrl()->getRouteParameters());
$form_state->setRedirect($entity->toUrl('canonical')->getRouteName(), $entity->toUrl('canonical')->getRouteParameters());
}
else {
$form_state->setRedirect('<front>');
......
......@@ -111,6 +111,20 @@ public function buildForm(array $form, FormStateInterface $form_state) {
];
}
$form['take_ownership'] = [
'#type' => 'checkbox',
'#title' => $this->t('Take ownership'),
'#description' => $this->t('Whether the "Take ownership" option should be checked by default on the entity clone form.'),
'#default_value' => $this->entityCloneSettingsManager->getTakeOwnershipSetting(),
];
$form['hide_layout_builder_referenced_entities'] = [
'#type' => 'checkbox',
'#title' => $this->t('Hide Layout builder referenced entities checkboxes'),
'#description' => $this->t('Whether the "Entities referenced by field Layout" information should be shown on the entity clone form.'),
'#default_value' => $this->entityCloneSettingsManager->getLayoutReferencesSetting(),
];
return parent::buildForm($form, $form_state);
}
......@@ -119,6 +133,8 @@ public function buildForm(array $form, FormStateInterface $form_state) {
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
$this->entityCloneSettingsManager->setFormSettings($form_state->getValue('form_settings'));
$this->entityCloneSettingsManager->setTakeOwnershipSettings($form_state->getValue('take_ownership'));
$this->entityCloneSettingsManager->setLayoutReferencesSettings($form_state->getValue('hide_layout_builder_referenced_entities'));
parent::submitForm($form, $form_state);
}
......
<?php
namespace Drupal\entity_clone\Services;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\user\EntityOwnerTrait;
/**
* Service Provider Class.
*/
class EntityCloneServiceProvider {
/**
* Constructs a new ServiceProvider object.
*/
public function __construct() {}
/**
* Checks if the given entity implements has owner trait.
*
* @param \Drupal\Core\Entity\EntityTypeInterface $entity
* Entity to be tested.
*
* @return bool
* Returns boolean for the owner trait test.
*/
public function entityTypeHasOwnerTrait(EntityTypeInterface $entityType) {
try {
$reflectionClass = new \ReflectionClass($entityType->getOriginalClass());
} catch (\ReflectionException $e) {
return FALSE;
}
return in_array(
EntityOwnerTrait::class,
array_keys($reflectionClass->getTraits())
);
}
}
<?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());
}
}
......@@ -2,6 +2,8 @@
namespace Drupal\Tests\entity_clone\Functional;
use Drupal\Core\Url;
use Drupal\language\Entity\ConfigurableLanguage;
use Drupal\node\Entity\Node;
use Drupal\Tests\field\Traits\EntityReferenceTestTrait;
use Drupal\Tests\node\Functional\NodeTestBase;
......@@ -20,7 +22,7 @@ class EntityCloneContentTest extends NodeTestBase {
*
* @var array
*/
public static $modules = ['entity_clone', 'block', 'node', 'datetime', 'taxonomy'];
public static $modules = ['entity_clone', 'block', 'node', 'datetime', 'taxonomy', 'content_translation', 'language'];
/**
* Theme to enable by default
......@@ -54,6 +56,9 @@ protected function setUp(): void {
$this->adminUser = $this->drupalCreateUser($this->permissions);
$this->drupalLogin($this->adminUser);
ConfigurableLanguage::createFromLangcode('fr')->save();
\Drupal::service('content_translation.manager')->setEnabled('node', 'page', TRUE);
}
/**
......@@ -78,6 +83,9 @@ public function testContentEntityClone() {
$this->assertInstanceOf(Node::class, $node, 'Test node cloned found in database.');
}
/**
* Test content reference config entity.
*/
public function testContentReferenceConfigEntity() {
$this->createEntityReferenceField('node', 'page', 'config_field_reference', 'Config field reference', 'taxonomy_vocabulary');
......@@ -93,4 +101,64 @@ public function testContentReferenceConfigEntity() {
$this->assertSession()->elementNotExists('css', '#edit-recursive-nodepageconfig-field-reference');
}
/**
* Test the cloned entity's created and changed dates.
*
* For entities that support these kinds of dates, both are reset to the
* current time.
*/
public function testCreatedAndChangedDate() {
// Create the original node.
$original_node_creation_date = new \DateTimeImmutable('1 year 1 month 1 day ago');
$translation_creation_date = new \DateTimeImmutable('1 month 1 day ago');
$original_node = Node::create([
'type' => 'page',
'title' => 'Test',
'created' => $original_node_creation_date->getTimestamp(),
'changed' => $original_node_creation_date->getTimestamp(),
]);
$original_node->addTranslation('fr', $original_node->toArray());
// The translation was created and updated later.
$translation = $original_node->getTranslation('fr');
$translation->setCreatedTime($translation_creation_date->getTimestamp());
$translation->setChangedTime($translation_creation_date->getTimestamp());
$original_node->save();
$original_node = Node::load($original_node->id());
$this->assertEquals($original_node_creation_date->getTimestamp(), $original_node->getCreatedTime());
$this->assertEquals($original_node_creation_date->getTimestamp(), $original_node->getChangedTime());
$this->assertEquals($translation_creation_date->getTimestamp(), $original_node->getTranslation('fr')->getCreatedTime());
$this->assertEquals($translation_creation_date->getTimestamp(), $original_node->getTranslation('fr')->getChangedTime());
// Clone the node.
$this->drupalPostForm('entity_clone/node/' . $original_node->id(), [], t('Clone'));
// Find the cloned node.
$nodes = \Drupal::entityTypeManager()
->getStorage('node')
->loadByProperties([
'title' => sprintf('%s - Cloned', $original_node->label()),
]);
$this->assertGreaterThanOrEqual(1, count($nodes));
/** @var \Drupal\node\NodeInterface $cloned_node */
$cloned_node = reset($nodes);
// Validate the cloned node's created time is more recent than the original
// node.
$this->assertNotEquals($original_node->getCreatedTime(), $cloned_node->getCreatedTime());
$this->assertGreaterThanOrEqual($original_node->getCreatedTime(), $cloned_node->getCreatedTime());
// Assert the changed time is equal to the newly created time since we
// cannot have a changed date before it.
$this->assertEquals($cloned_node->getCreatedTime(), $cloned_node->getChangedTime());
// Validate the translation created and updated dates.
$this->assertTrue($cloned_node->hasTranslation('fr'));
$translation = $cloned_node->getTranslation('fr');
// The created and updated times should be the same between the original
// and the translation as both should be reset.
$this->assertEquals($cloned_node->getCreatedTime(), $translation->getCreatedTime());
$this->assertEquals($cloned_node->getChangedTime(), $translation->getChangedTime());
}
}
......@@ -17,7 +17,7 @@ class EntityCloneEntityFormModeTest extends BrowserTestBase {
*
* @var array
*/
public static $modules = ['entity_clone'];
public static $modules = ['entity_clone', 'field_ui'];
/**
* Theme to enable by default
......@@ -32,6 +32,7 @@ class EntityCloneEntityFormModeTest extends BrowserTestBase {
*/
protected $permissions = [
'clone entity_form_mode entity',
'administer display modes',
];
/**
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment