diff --git a/entity_browser.services.yml b/entity_browser.services.yml index 88fedfd..3c8aa09 100644 --- a/entity_browser.services.yml +++ b/entity_browser.services.yml @@ -22,3 +22,10 @@ services: arguments: ['@module_handler'] tags: - { name: route_enhancer } + entity_browser.selection_storage: + # Symfony will complain if the class is not defined. However, it seems that + # it doesn't use it at all. Interface is not the best thing to set here, but + # it seems the best option at the end of the day. + class: Drupal\Core\KeyValueStore\KeyValueStoreExpirableInterface + factory: keyvalue.expirable:get + arguments: ['entity_browser'] diff --git a/src/DisplayBase.php b/src/DisplayBase.php index f2c2569..1a87c8c 100644 --- a/src/DisplayBase.php +++ b/src/DisplayBase.php @@ -3,8 +3,11 @@ namespace Drupal\entity_browser; use Drupal\Component\Uuid\UuidInterface; +use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\KeyValueStore\KeyValueStoreExpirableInterface; use Drupal\Core\Plugin\PluginBase; use Drupal\Core\Plugin\ContainerFactoryPluginInterface; +use Drupal\Core\Site\Settings; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\EventDispatcher\EventDispatcherInterface; @@ -51,6 +54,13 @@ abstract class DisplayBase extends PluginBase implements DisplayInterface, Conta protected $uuid = NULL; /** + * The selection storage. + * + * @var \Drupal\Core\KeyValueStore\KeyValueStoreExpirableInterface + */ + protected $selectionStorage; + + /** * Constructs display plugin. * * @param array $configuration @@ -64,11 +74,12 @@ abstract class DisplayBase extends PluginBase implements DisplayInterface, Conta * @param \Drupal\Component\Uuid\UuidInterface * UUID generator interface. */ - public function __construct(array $configuration, $plugin_id, $plugin_definition, EventDispatcherInterface $event_dispatcher, UuidInterface $uuid_generator) { + public function __construct(array $configuration, $plugin_id, $plugin_definition, EventDispatcherInterface $event_dispatcher, UuidInterface $uuid_generator, KeyValueStoreExpirableInterface $selection_storage) { parent::__construct($configuration, $plugin_id, $plugin_definition); $this->configuration += $this->defaultConfiguration(); $this->eventDispatcher = $event_dispatcher; $this->uuidGenerator = $uuid_generator; + $this->selectionStorage = $selection_storage; } /** @@ -80,7 +91,8 @@ abstract class DisplayBase extends PluginBase implements DisplayInterface, Conta $plugin_id, $plugin_definition, $container->get('event_dispatcher'), - $container->get('uuid') + $container->get('uuid'), + $container->get('entity_browser.selection_storage') ); } @@ -139,4 +151,15 @@ abstract class DisplayBase extends PluginBase implements DisplayInterface, Conta $this->uuid = $uuid; } + /** + * {@inheritdoc} + */ + public function displayEntityBrowser(FormStateInterface $form_state, array $entities = []) { + // If the existing selection was passed in save it to expirable state for + // the entity browser to be able to load them from there. + if (!empty($entities)) { + $this->selectionStorage->setWithExpire($this->getUuid(), $entities, Settings::get('entity_browser_expire', 21600)); + } + } + } diff --git a/src/DisplayInterface.php b/src/DisplayInterface.php index a73f443..7513852 100644 --- a/src/DisplayInterface.php +++ b/src/DisplayInterface.php @@ -36,11 +36,13 @@ interface DisplayInterface extends PluginInspectionInterface, ConfigurablePlugin * * @param \Drupal\Core\Form\FormStateInterface $form_state * The form state object. + * @param \Drupal\Core\Entity\EntityInterface[] $entities + * (optional) Existing selection that should be passed to the entity browser. * * @return array * An array suitable for drupal_render(). */ - public function displayEntityBrowser(FormStateInterface $form_state); + public function displayEntityBrowser(FormStateInterface $form_state, array $entities = []); /** * Indicates completed selection. diff --git a/src/Form/EntityBrowserForm.php b/src/Form/EntityBrowserForm.php index 1a81471..ae3a12a 100644 --- a/src/Form/EntityBrowserForm.php +++ b/src/Form/EntityBrowserForm.php @@ -5,6 +5,7 @@ namespace Drupal\entity_browser\Form; use Drupal\Component\Uuid\UuidInterface; use Drupal\Core\Form\FormBase; use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\KeyValueStore\KeyValueStoreExpirableInterface; use Drupal\entity_browser\DisplayAjaxInterface; use Drupal\entity_browser\EntityBrowserFormInterface; use Drupal\entity_browser\EntityBrowserInterface; @@ -30,13 +31,21 @@ class EntityBrowserForm extends FormBase implements EntityBrowserFormInterface { protected $entity_browser; /** + * The entity browser selection storage. + * + * @var \Drupal\Core\KeyValueStore\KeyValueStoreExpirableInterface + */ + protected $selectionStorage; + + /** * Constructs a EntityBrowserForm object. * * @param \Drupal\Component\Uuid\UuidInterface $uuid_generator * The UUID generator service. */ - public function __construct(UuidInterface $uuid_generator) { + public function __construct(UuidInterface $uuid_generator, KeyValueStoreExpirableInterface $selection_storage) { $this->uuidGenerator = $uuid_generator; + $this->selectionStorage = $selection_storage; } /** @@ -44,7 +53,8 @@ class EntityBrowserForm extends FormBase implements EntityBrowserFormInterface { */ public static function create(ContainerInterface $container) { return new static( - $container->get('uuid') + $container->get('uuid'), + $container->get('entity_browser.selection_storage') ); } @@ -79,6 +89,11 @@ class EntityBrowserForm extends FormBase implements EntityBrowserFormInterface { } $form_state->set(['entity_browser', 'selected_entities'], []); $form_state->set(['entity_browser', 'selection_completed'], FALSE); + + // Initialize select from the query parameter. + if ($selection = $this->selectionStorage->get($form_state->get(['entity_browser', 'instance_uuid']))) { + $form_state->set(['entity_browser', 'selected_entities'], $selection); + } } /** diff --git a/src/Plugin/EntityBrowser/Display/IFrame.php b/src/Plugin/EntityBrowser/Display/IFrame.php index 9eed34f..c3edcba 100644 --- a/src/Plugin/EntityBrowser/Display/IFrame.php +++ b/src/Plugin/EntityBrowser/Display/IFrame.php @@ -5,6 +5,7 @@ namespace Drupal\entity_browser\Plugin\EntityBrowser\Display; use Drupal\Component\Uuid\UuidInterface; use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\KeyValueStore\KeyValueStoreExpirableInterface; use Drupal\Core\Routing\RouteMatchInterface; use Drupal\Core\Url; use Drupal\entity_browser\DisplayBase; @@ -66,6 +67,8 @@ class IFrame extends DisplayBase implements DisplayRouterInterface { * Event dispatcher service. * @param \Drupal\Component\Uuid\UuidInterface * UUID generator interface. + * @parem \Drupal\Core\KeyValueStore\KeyValueStoreExpirableInterface $selection_storage + * The selection storage. * @param \Drupal\Core\Routing\RouteMatchInterface $current_route_match * The currently active route match object. * @param \Symfony\Component\HttpFoundation\Request $request @@ -73,8 +76,8 @@ class IFrame extends DisplayBase implements DisplayRouterInterface { * @param \Drupal\Core\Path\CurrentPathStack $current_path * The current path. */ - public function __construct(array $configuration, $plugin_id, $plugin_definition, EventDispatcherInterface $event_dispatcher, UuidInterface $uuid, RouteMatchInterface $current_route_match, Request $request, CurrentPathStack $current_path) { - parent::__construct($configuration, $plugin_id, $plugin_definition, $event_dispatcher, $uuid); + public function __construct(array $configuration, $plugin_id, $plugin_definition, EventDispatcherInterface $event_dispatcher, UuidInterface $uuid, KeyValueStoreExpirableInterface $selection_storage, RouteMatchInterface $current_route_match, Request $request, CurrentPathStack $current_path) { + parent::__construct($configuration, $plugin_id, $plugin_definition, $event_dispatcher, $uuid, $selection_storage); $this->currentRouteMatch = $current_route_match; $this->request = $request; $this->currentPath = $current_path; @@ -90,6 +93,7 @@ class IFrame extends DisplayBase implements DisplayRouterInterface { $plugin_definition, $container->get('event_dispatcher'), $container->get('uuid'), + $container->get('entity_browser.selection_storage'), $container->get('current_route_match'), $container->get('request_stack')->getCurrentRequest(), $container->get('path.current') @@ -111,28 +115,29 @@ class IFrame extends DisplayBase implements DisplayRouterInterface { /** * {@inheritdoc} */ - public function displayEntityBrowser(FormStateInterface $form_state) { - $uuid = $this->getUuid(); + public function displayEntityBrowser(FormStateInterface $form_state, array $entities = []) { + parent::displayEntityBrowser($form_state, $entities); /** @var \Drupal\entity_browser\Events\RegisterJSCallbacks $event */ - $js_event_object = new RegisterJSCallbacks($this->configuration['entity_browser_id'], $uuid); + $js_event_object = new RegisterJSCallbacks($this->configuration['entity_browser_id'], $this->getUuid()); $js_event_object->registerCallback('Drupal.entityBrowser.selectionCompleted'); $callback_event = $this->eventDispatcher->dispatch(Events::REGISTER_JS_CALLBACKS, $js_event_object); $original_path = $this->currentPath->getPath(); + $data = [ 'query_parameters' => [ 'query' => [ - 'uuid' => $uuid, + 'uuid' => $this->getUuid(), 'original_path' => $original_path, ], ], 'attributes' => [ 'href' => '#browser', 'class' => ['entity-browser-handle', 'entity-browser-iframe'], - 'data-uuid' => $uuid, + 'data-uuid' => $this->getUuid(), 'data-original-path' => $original_path, ], ]; - $event_object = new AlterEntityBrowserDisplayData($this->configuration['entity_browser_id'], $uuid, $this->getPluginDefinition(), $form_state, $data); + $event_object = new AlterEntityBrowserDisplayData($this->configuration['entity_browser_id'], $this->getUuid(), $this->getPluginDefinition(), $form_state, $data); $event = $this->eventDispatcher->dispatch(Events::ALTER_BROWSER_DISPLAY_DATA, $event_object); $data = $event->getData(); return [ @@ -147,7 +152,7 @@ class IFrame extends DisplayBase implements DisplayRouterInterface { 'drupalSettings' => [ 'entity_browser' => [ 'iframe' => [ - $uuid => [ + $this->getUuid() => [ 'src' => Url::fromRoute('entity_browser.' . $this->configuration['entity_browser_id'], [], $data['query_parameters']) ->toString(), 'width' => $this->configuration['width'], diff --git a/src/Plugin/EntityBrowser/Display/Modal.php b/src/Plugin/EntityBrowser/Display/Modal.php index 433cf27..aa9c8b2 100644 --- a/src/Plugin/EntityBrowser/Display/Modal.php +++ b/src/Plugin/EntityBrowser/Display/Modal.php @@ -7,6 +7,7 @@ use Drupal\Component\Utility\NestedArray; use Drupal\Component\Uuid\UuidInterface; use Drupal\Core\Ajax\OpenDialogCommand; use Drupal\Core\Entity\EntityInterface; +use Drupal\Core\KeyValueStore\KeyValueStoreExpirableInterface; use Drupal\Core\Routing\RouteMatchInterface; use Drupal\Core\Url; use Drupal\entity_browser\DisplayBase; @@ -71,15 +72,17 @@ class Modal extends DisplayBase implements DisplayRouterInterface { * Event dispatcher service. * @param \Drupal\Component\Uuid\UuidInterface * UUID generator interface. - * @param \Drupal\Core\Routing\RouteMatchInterface + * @parem \Drupal\Core\KeyValueStore\KeyValueStoreExpirableInterface $selection_storage + * The selection storage. + * @param \Drupal\Core\Routing\RouteMatchInterface $current_route_match * The currently active route match object. * @param \Symfony\Component\HttpFoundation\Request $request * Current request. * @param \Drupal\Core\Path\CurrentPathStack $current_path * The current path. */ - public function __construct(array $configuration, $plugin_id, $plugin_definition, EventDispatcherInterface $event_dispatcher, UuidInterface $uuid, RouteMatchInterface $current_route_match, CurrentPathStack $current_path, Request $request) { - parent::__construct($configuration, $plugin_id, $plugin_definition, $event_dispatcher, $uuid); + public function __construct(array $configuration, $plugin_id, $plugin_definition, EventDispatcherInterface $event_dispatcher, UuidInterface $uuid, KeyValueStoreExpirableInterface $selection_storage, RouteMatchInterface $current_route_match, CurrentPathStack $current_path, Request $request) { + parent::__construct($configuration, $plugin_id, $plugin_definition, $event_dispatcher, $uuid, $selection_storage); $this->currentRouteMatch = $current_route_match; $this->currentPath = $current_path; $this->request = $request; @@ -95,6 +98,7 @@ class Modal extends DisplayBase implements DisplayRouterInterface { $plugin_definition, $container->get('event_dispatcher'), $container->get('uuid'), + $container->get('entity_browser.selection_storage'), $container->get('current_route_match'), $container->get('path.current'), $container->get('request_stack')->getCurrentRequest() @@ -115,24 +119,25 @@ class Modal extends DisplayBase implements DisplayRouterInterface { /** * {@inheritdoc} */ - public function displayEntityBrowser(FormStateInterface $form_state) { - $uuid = $this->getUuid(); - $js_event_object = new RegisterJSCallbacks($this->configuration['entity_browser_id'], $uuid); + public function displayEntityBrowser(FormStateInterface $form_state, array $entities = []) { + parent::displayEntityBrowser($form_state, $entities); + $js_event_object = new RegisterJSCallbacks($this->configuration['entity_browser_id'], $this->getUuid()); $js_event_object->registerCallback('Drupal.entityBrowser.selectionCompleted'); $js_event = $this->eventDispatcher->dispatch(Events::REGISTER_JS_CALLBACKS, $js_event_object); $original_path = $this->currentPath->getPath(); + $data = [ 'query_parameters' => [ 'query' => [ - 'uuid' => $uuid, + 'uuid' => $this->getUuid(), 'original_path' => $original_path, ], ], 'attributes' => [ - 'data-uuid' => $uuid, + 'data-uuid' => $this->getUuid(), ], ]; - $event_object = new AlterEntityBrowserDisplayData($this->configuration['entity_browser_id'], $uuid, $this->getPluginDefinition(), $form_state, $data); + $event_object = new AlterEntityBrowserDisplayData($this->configuration['entity_browser_id'], $this->getUuid(), $this->getPluginDefinition(), $form_state, $data); $event = $this->eventDispatcher->dispatch(Events::ALTER_BROWSER_DISPLAY_DATA, $event_object); $data = $event->getData(); return [ @@ -146,7 +151,7 @@ class Modal extends DisplayBase implements DisplayRouterInterface { '#value' => $this->configuration['link_text'], '#limit_validation_errors' => [], '#submit' => [], - '#name' => Html::getId('op_' . $this->configuration['entity_browser_id'] . '_' . $uuid), + '#name' => Html::getId('op_' . $this->configuration['entity_browser_id'] . '_' . $this->getUuid()), '#ajax' => [ 'callback' => [$this, 'openModal'], 'event' => 'click', @@ -157,8 +162,8 @@ class Modal extends DisplayBase implements DisplayRouterInterface { 'drupalSettings' => [ 'entity_browser' => [ 'modal' => [ - $uuid => [ - 'uuid' => $uuid, + $this->getUuid() => [ + 'uuid' => $this->getUuid(), 'js_callbacks' => $js_event->getCallbacks(), 'original_path' => $original_path, ], diff --git a/src/Plugin/EntityBrowser/Display/Standalone.php b/src/Plugin/EntityBrowser/Display/Standalone.php index 9b8fbc6..e7c8eb9 100644 --- a/src/Plugin/EntityBrowser/Display/Standalone.php +++ b/src/Plugin/EntityBrowser/Display/Standalone.php @@ -45,7 +45,8 @@ class Standalone extends DisplayBase implements DisplayRouterInterface { /** * {@inheritdoc} */ - public function displayEntityBrowser(FormStateInterface $form_state) { + public function displayEntityBrowser(FormStateInterface $form_state, array $entities = []) { + parent::displayEntityBrowser($form_state, $entities); // @TODO Implement it. } diff --git a/src/Plugin/Field/FieldWidget/EntityReference.php b/src/Plugin/Field/FieldWidget/EntityReference.php index 6075b8a..1d7f42a 100644 --- a/src/Plugin/Field/FieldWidget/EntityReference.php +++ b/src/Plugin/Field/FieldWidget/EntityReference.php @@ -15,6 +15,7 @@ use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Plugin\ContainerFactoryPluginInterface; use Drupal\Core\Url; use Drupal\Core\Validation\Plugin\Validation\Constraint\NotNullConstraint; +use Drupal\entity_browser\EntityCollection; use Drupal\entity_browser\FieldWidgetDisplayManager; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\EventDispatcher\EventDispatcherInterface; @@ -380,7 +381,8 @@ class EntityReference extends WidgetBase implements ContainerFactoryPluginInterf $entity_browser_uuid = sha1(implode('-', array_merge($form['#parents'], [$this->fieldDefinition->getName(), $delta]))); $entity_browser_display = $entity_browser->getDisplay(); $entity_browser_display->setUuid($entity_browser_uuid); - $element['entity_browser'] = $entity_browser_display->displayEntityBrowser($form_state); + + $element['entity_browser'] = $entity_browser_display->displayEntityBrowser($form_state, []); $element['#attached']['library'][] = 'entity_browser/entity_reference'; $element['#attached']['drupalSettings']['entity_browser'] = [ $entity_browser->getDisplay()->getUuid() => [ diff --git a/tests/src/Kernel/Extension/EntityBrowserTest.php b/tests/src/Kernel/Extension/EntityBrowserTest.php index e6e2791..4fef471 100644 --- a/tests/src/Kernel/Extension/EntityBrowserTest.php +++ b/tests/src/Kernel/Extension/EntityBrowserTest.php @@ -60,7 +60,7 @@ class EntityBrowserTest extends KernelTestBase { $this->widgetUUID = $this->container->get('uuid')->generate(); $this->routeProvider = $this->container->get('router.route_provider'); - $this->installSchema('system', ['router']); + $this->installSchema('system', ['router', 'key_value_expire', 'sequences']); } /** @@ -323,6 +323,8 @@ class EntityBrowserTest extends KernelTestBase { $entity->getWidgets()->get($entity->getFirstWidget())->entity = $entity; $this->container->get('form_builder')->buildForm($form_object, $form_state); + $this->assertEquals(0, count($form_state->get(['entity_browser', 'selected_entities'])), 'Correct number of entities was propagated.'); + $this->container->get('form_builder')->submitForm($form_object, $form_state); // Event should be dispatched from widget and added to list of selected entities. @@ -330,4 +332,45 @@ class EntityBrowserTest extends KernelTestBase { $this->assertEquals($selected_entities, [$entity], 'Expected selected entities detected.'); } + /** + * Tests propagation of existing selection. + */ + public function testExistingSelection() { + $this->installConfig(['entity_browser_test']); + $this->installEntitySchema('user'); + + /** @var $entity \Drupal\entity_browser\EntityBrowserInterface */ + $entity = $this->controller->load('test'); + + /** @var \Drupal\user\UserInterface $user */ + $user = $this->container->get('entity_type.manager') + ->getStorage('user') + ->create([ + 'name' => $this->randomString(), + 'mail' => 'info@example.com', + ]); + $user->save(); + + /** @var \Symfony\Component\HttpFoundation\Request $request */ + $uuid = $this->container->get('uuid')->generate(); + $this->container->get('request_stack') + ->getCurrentRequest() + ->query + ->set('uuid', $uuid); + $this->container->get('entity_browser.selection_storage')->setWithExpire($uuid, [$user], 21600); + + /** @var \Drupal\entity_browser\EntityBrowserFormInterface $form_object */ + $form_object = $entity->getFormObject(); + $form_object->setEntityBrowser($entity); + $form_state = new FormState(); + + $form = []; + $form_object->buildForm($form, $form_state); + $propagated_entities = $form_state->get(['entity_browser', 'selected_entities']); + $this->assertEquals(1, count($propagated_entities), 'Correct number of entities was propagated.'); + $this->assertEquals($user->id(), $propagated_entities[0]->id(), 'Propagated entity ID is correct.'); + $this->assertEquals($user->getAccountName(), $propagated_entities[0]->getAccountName(), 'Propagated entity name is correct.'); + $this->assertEquals($user->getEmail(), $propagated_entities[0]->getEmail(), 'Propagated entity name is correct.'); + } + }