diff --git a/config/schema/entity_browser.views.schema.yml b/config/schema/entity_browser.views.schema.yml new file mode 100644 index 0000000..a2a2e28 --- /dev/null +++ b/config/schema/entity_browser.views.schema.yml @@ -0,0 +1,7 @@ +views.field.entity_browser_select: + type: views_field + label: 'Entity Browser select' + mapping: + use_field_cardinality: + type: boolean + label: 'Use field cardinality' diff --git a/entity_browser.module b/entity_browser.module index 8721645..6d8268d 100644 --- a/entity_browser.module +++ b/entity_browser.module @@ -10,6 +10,7 @@ use \Drupal\Core\Render\Element; use Drupal\Core\Url; use \Drupal\file\FileInterface; use Drupal\Core\Routing\RouteMatchInterface; +use Drupal\views\ViewExecutable; /** * Implements hook_help(). diff --git a/js/entity_browser.view.js b/js/entity_browser.view.js index 921bebd..782a5b2 100644 --- a/js/entity_browser.view.js +++ b/js/entity_browser.view.js @@ -65,7 +65,7 @@ // Ensure to use input (checkbox) field from entity browser // column dedicated for selection checkbox. - var $input = $row.find('.views-field-entity-browser-select input.form-checkbox'); + var $input = $row.find('.views-field-entity-browser-select input.form-checkbox, .views-field-entity-browser-select input.form-radio'); // Get selection display element and trigger adding of entity // over ajax request. diff --git a/modules/example/config/install/views.view.files_entity_browser.yml b/modules/example/config/install/views.view.files_entity_browser.yml index ece197a..7ba408c 100644 --- a/modules/example/config/install/views.view.files_entity_browser.yml +++ b/modules/example/config/install/views.view.files_entity_browser.yml @@ -181,6 +181,7 @@ display: hide_alter_empty: true entity_type: file plugin_id: entity_browser_select + use_field_cardinality: false filename: id: filename table: file_managed diff --git a/src/Plugin/EntityBrowser/Widget/View.php b/src/Plugin/EntityBrowser/Widget/View.php index 111f9b7..4165386 100644 --- a/src/Plugin/EntityBrowser/Widget/View.php +++ b/src/Plugin/EntityBrowser/Widget/View.php @@ -3,9 +3,11 @@ namespace Drupal\entity_browser\Plugin\EntityBrowser\Widget; use Drupal\Component\Plugin\Exception\PluginNotFoundException; +use Drupal\Component\Utility\NestedArray; use Drupal\Core\Access\AccessResult; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Render\Element; +use Drupal\entity_browser\Plugin\views\field\SelectForm; use Drupal\entity_browser\WidgetBase; use Drupal\Core\Url; use Drupal\entity_browser\WidgetValidationManager; @@ -118,6 +120,14 @@ class View extends WidgetBase implements ContainerFactoryPluginInterface { } } + $cardinality = NestedArray::getValue($form_state->getStorage(), ['entity_browser', 'validators', 'cardinality', 'cardinality']); + $form_state->set('view', $view); + $form_state->set('view_display', $this->configuration['view_display']); + + if ($cardinality) { + $view->cardinality = $cardinality; + } + $form['view'] = $view->executeDisplay($this->configuration['view_display']); if (empty($view->field['entity_browser_select'])) { @@ -172,7 +182,29 @@ class View extends WidgetBase implements ContainerFactoryPluginInterface { public function validate(array &$form, FormStateInterface $form_state) { $user_input = $form_state->getUserInput(); if (isset($user_input['entity_browser_select'])) { - $selected_rows = array_values(array_filter($user_input['entity_browser_select'])); + if (is_array($user_input['entity_browser_select'])) { + $selected_rows = array_values(array_filter($user_input['entity_browser_select'])); + } + else { + $selected_rows = [$user_input['entity_browser_select']]; + } + + $cardinality = NestedArray::getValue($form_state->getStorage(), ['entity_browser', 'validators', 'cardinality', 'cardinality']); + $display = $form_state->get('view')->getDisplay($form_state->get(['view_display'])); + + $use_field_cardinality = FALSE; + foreach ($display->getHandlers('field') as $handler) { + if ($handler instanceof SelectForm && $handler->options['use_field_cardinality']) { + $use_field_cardinality = TRUE; + } + } + + if ($use_field_cardinality && $cardinality > 0) { + if (count($selected_rows) > $cardinality) { + $form_state->setError($form['widget']['view']['entity_browser_select'], $this->t('You can only select up to @number items.', ['@number' => $cardinality])); + } + } + foreach ($selected_rows as $row) { // Verify that the user input is a string and split it. // Each $row is in the format entity_type:id. @@ -210,13 +242,22 @@ class View extends WidgetBase implements ContainerFactoryPluginInterface { * {@inheritdoc} */ protected function prepareEntities(array $form, FormStateInterface $form_state) { - $selected_rows = array_values(array_filter($form_state->getUserInput()['entity_browser_select'])); + if (is_array($form_state->getUserInput()['entity_browser_select'])) { + $selected_rows = array_values(array_filter($form_state->getUserInput()['entity_browser_select'])); + } + else { + $selected_rows = [$form_state->getUserInput()['entity_browser_select']]; + } + $entities = []; foreach ($selected_rows as $row) { - list($type, $id) = explode(':', $row); - $storage = $this->entityTypeManager->getStorage($type); - if ($entity = $storage->load($id)) { - $entities[] = $entity; + $item = explode(':', $row); + if (count($item) == 2) { + list($type, $id) = $item; + $storage = $this->entityTypeManager->getStorage($type); + if ($entity = $storage->load($id)) { + $entities[] = $entity; + } } } return $entities; diff --git a/src/Plugin/views/display/EntityBrowser.php b/src/Plugin/views/display/EntityBrowser.php index 5b382c3..7616f65 100644 --- a/src/Plugin/views/display/EntityBrowser.php +++ b/src/Plugin/views/display/EntityBrowser.php @@ -4,6 +4,8 @@ namespace Drupal\entity_browser\Plugin\views\display; use Drupal\Core\Form\FormStateInterface; use Drupal\views\Plugin\views\display\DisplayPluginBase; +use Symfony\Component\DependencyInjection\ContainerInterface; +use Symfony\Component\HttpFoundation\RequestStack; /** * The plugin that handles entity browser display. @@ -26,6 +28,42 @@ use Drupal\views\Plugin\views\display\DisplayPluginBase; class EntityBrowser extends DisplayPluginBase { /** + * The current request. + * + * @var null|\Symfony\Component\HttpFoundation\Request + */ + protected $currentRequest; + + /** + * EntityBrowser constructor. + * + * @param array $configuration + * The plugin configuration. + * @param string $plugin_id + * The plugin id. + * @param array $plugin_definition + * The plugin definition. + * @param \Symfony\Component\HttpFoundation\RequestStack $request_stack + * The request stack. + */ + public function __construct(array $configuration, $plugin_id, $plugin_definition, RequestStack $request_stack) { + parent::__construct($configuration, $plugin_id, $plugin_definition); + $this->currentRequest = $request_stack->getCurrentRequest(); + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + return new static( + $configuration, + $plugin_id, + $plugin_definition, + $container->get('request_stack') + ); + } + + /** * {@inheritdoc} */ public function execute() { @@ -141,6 +179,22 @@ class EntityBrowser extends DisplayPluginBase { $element['#attachment_after'] = $view->attachment_after; } + if ($cardinality = $this->getCardinality()) { + // Add the cardinality query params, so + // subsequent AJAX calls for other pages have this info available. + if (!empty($element['#pager'])) { + $element['#pager']['#parameters']['cardinality'] = $cardinality; + } + + // Do the same for exposed filters. However, once here the submission + // happens in a POST request, we inject our overridden config in the view + // JS settings, that will be appended to the real query string later in + // the AJAX behavior. + if (!empty($view->exposed_widgets) && empty($view->is_attachment) && empty($view->live_preview)) { + $view->element['#attached']['drupalSettings']['views']['ajaxViews']['views_dom_id:' . $view->dom_id]['view_query'] .= '&cardinality=' . $cardinality; + } + } + return $element; } @@ -222,4 +276,30 @@ class EntityBrowser extends DisplayPluginBase { return $content; } + /** + * Get the cardinality parameter. + * + * @return int|null + * The integer representing cardinality or null if unable to find it. + */ + public function getCardinality() { + $cardinality = NULL; + if (isset($this->view->cardinality)) { + $cardinality = $this->view->cardinality; + } + elseif ($this->currentRequest->request->get('view_query')) { + $view_query = $this->currentRequest->request->get('view_query'); + + if ($view_query) { + parse_str($view_query, $params); + + if (isset($params['cardinality'])) { + $cardinality = $params['cardinality']; + } + } + } + + return $cardinality; + } + } diff --git a/src/Plugin/views/field/SelectForm.php b/src/Plugin/views/field/SelectForm.php index d498082..1242b5e 100644 --- a/src/Plugin/views/field/SelectForm.php +++ b/src/Plugin/views/field/SelectForm.php @@ -2,10 +2,13 @@ namespace Drupal\entity_browser\Plugin\views\field; +use Drupal\Core\Form\FormStateInterface; use Drupal\views\Plugin\views\field\FieldPluginBase; use Drupal\views\Plugin\views\style\Table; use Drupal\views\ResultRow; use Drupal\views\Render\ViewsRenderPipelineMarkup; +use Symfony\Component\DependencyInjection\ContainerInterface; +use Symfony\Component\HttpFoundation\RequestStack; /** * Defines a bulk operation form element that works with entity browser. @@ -14,6 +17,70 @@ use Drupal\views\Render\ViewsRenderPipelineMarkup; */ class SelectForm extends FieldPluginBase { + + + /** + * The current request. + * + * @var null|\Symfony\Component\HttpFoundation\Request + */ + protected $currentRequest; + + /** + * EntityBrowser constructor. + * + * @param array $configuration + * The plugin configuration. + * @param string $plugin_id + * The plugin id. + * @param mixed $plugin_definition + * The plugin definition. + * @param \Symfony\Component\HttpFoundation\RequestStack $request_stack + * The request stack. + */ + public function __construct(array $configuration, $plugin_id, $plugin_definition, RequestStack $request_stack) { + parent::__construct($configuration, $plugin_id, $plugin_definition); + $this->currentRequest = $request_stack->getCurrentRequest(); + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + return new static( + $configuration, + $plugin_id, + $plugin_definition, + $container->get('request_stack') + ); + } + + /** + * {@inheritdoc} + */ + protected function defineOptions() { + $options = parent::defineOptions(); + $options['use_field_cardinality'] = [ + 'default' => TRUE, + ]; + + return $options; + } + + /** + * {@inheritdoc} + */ + public function buildOptionsForm(&$form, FormStateInterface $form_state) { + parent::buildOptionsForm($form, $form_state); + + $form['use_field_cardinality'] = [ + '#type' => 'checkbox', + '#title' => $this->t('Use field cardinality'), + '#default_value' => $this->options['use_field_cardinality'], + '#description' => $this->t('In case of cardinality 1, radios instead of checboxes will be displayed.') + ]; + } + /** * Returns the ID for a result row. * @@ -74,6 +141,17 @@ class SelectForm extends FieldPluginBase { '#attributes' => ['name' => "entity_browser_select[$value]"], '#default_value' => NULL, ]; + + // Handle cardinality. + $cardinality = $this->displayHandler->getCardinality(); + + if ($this->options['use_field_cardinality'] && $cardinality && $cardinality == 1) { + $render[$this->options['id']][$value]['#type'] = 'radio'; + $render[$this->options['id']][$value]['#attributes'] = ['name' => "entity_browser_select"]; + $render[$this->options['id']][$value]['#parents'] = ['entity_browser_select']; + // Init the #value property to suppress php notice in Radio.php + $render[$this->options['id']][$value]['#value'] = FALSE; + } } } } diff --git a/tests/modules/entity_browser_test/config/install/views.view.files_entity_browser.yml b/tests/modules/entity_browser_test/config/install/views.view.files_entity_browser.yml index 9351d53..efff872 100644 --- a/tests/modules/entity_browser_test/config/install/views.view.files_entity_browser.yml +++ b/tests/modules/entity_browser_test/config/install/views.view.files_entity_browser.yml @@ -149,6 +149,7 @@ display: empty_zero: false hide_alter_empty: true entity_type: file + use_field_cardinality: false plugin_id: entity_browser_select filename: id: filename diff --git a/tests/modules/entity_browser_test/config/install/views.view.files_entity_browser_grid.yml b/tests/modules/entity_browser_test/config/install/views.view.files_entity_browser_grid.yml index 4610a12..cb4dcd9 100644 --- a/tests/modules/entity_browser_test/config/install/views.view.files_entity_browser_grid.yml +++ b/tests/modules/entity_browser_test/config/install/views.view.files_entity_browser_grid.yml @@ -131,6 +131,7 @@ display: empty_zero: false hide_alter_empty: true entity_type: file + use_field_cardinality: false plugin_id: entity_browser_select filename: id: filename diff --git a/tests/modules/entity_browser_test/config/install/views.view.files_entity_browser_html.yml b/tests/modules/entity_browser_test/config/install/views.view.files_entity_browser_html.yml index 7a1586d..bd30b7e 100644 --- a/tests/modules/entity_browser_test/config/install/views.view.files_entity_browser_html.yml +++ b/tests/modules/entity_browser_test/config/install/views.view.files_entity_browser_html.yml @@ -129,6 +129,7 @@ display: empty_zero: false hide_alter_empty: true entity_type: file + use_field_cardinality: false plugin_id: entity_browser_select filename: id: filename diff --git a/tests/modules/entity_browser_test/config/install/views.view.files_entity_browser_unformatted.yml b/tests/modules/entity_browser_test/config/install/views.view.files_entity_browser_unformatted.yml index 7792d65..1afcd59 100644 --- a/tests/modules/entity_browser_test/config/install/views.view.files_entity_browser_unformatted.yml +++ b/tests/modules/entity_browser_test/config/install/views.view.files_entity_browser_unformatted.yml @@ -126,6 +126,7 @@ display: empty_zero: false hide_alter_empty: true entity_type: file + use_field_cardinality: false plugin_id: entity_browser_select filename: id: filename diff --git a/tests/modules/entity_browser_test/config/install/views.view.test_deprecated_field.yml b/tests/modules/entity_browser_test/config/install/views.view.test_deprecated_field.yml index 629bcad..cbfab7b 100644 --- a/tests/modules/entity_browser_test/config/install/views.view.test_deprecated_field.yml +++ b/tests/modules/entity_browser_test/config/install/views.view.test_deprecated_field.yml @@ -174,6 +174,7 @@ display: empty_zero: false hide_alter_empty: true entity_type: node + use_field_cardinality: false plugin_id: entity_browser_select filters: status: diff --git a/tests/modules/entity_browser_test/config/install/views.view.test_entity_reference_widget_view.yml b/tests/modules/entity_browser_test/config/install/views.view.test_entity_reference_widget_view.yml index 15d5baf..21b09d2 100644 --- a/tests/modules/entity_browser_test/config/install/views.view.test_entity_reference_widget_view.yml +++ b/tests/modules/entity_browser_test/config/install/views.view.test_entity_reference_widget_view.yml @@ -149,6 +149,7 @@ display: empty_zero: false hide_alter_empty: true entity_type: node + use_field_cardinality: false plugin_id: entity_browser_select title: id: title diff --git a/tests/modules/entity_browser_test_paragraphs/config/install/views.view.nodes_entity_browser.yml b/tests/modules/entity_browser_test_paragraphs/config/install/views.view.nodes_entity_browser.yml index 3be9988..01ec758 100644 --- a/tests/modules/entity_browser_test_paragraphs/config/install/views.view.nodes_entity_browser.yml +++ b/tests/modules/entity_browser_test_paragraphs/config/install/views.view.nodes_entity_browser.yml @@ -147,6 +147,7 @@ display: empty_zero: false hide_alter_empty: true entity_type: node + use_field_cardinality: false plugin_id: entity_browser_select title: id: title diff --git a/tests/src/FunctionalJavascript/EntityBrowserViewsWidgetTest.php b/tests/src/FunctionalJavascript/EntityBrowserViewsWidgetTest.php index 5f4c8c1..b99ba01 100644 --- a/tests/src/FunctionalJavascript/EntityBrowserViewsWidgetTest.php +++ b/tests/src/FunctionalJavascript/EntityBrowserViewsWidgetTest.php @@ -49,6 +49,7 @@ class EntityBrowserViewsWidgetTest extends EntityBrowserJavascriptTestBase { // Visit a test entity browser page that defaults to using a View widget. $this->drupalGet('/entity-browser/iframe/test_entity_browser_file'); + $this->saveHtmlOutput(); $field = 'entity_browser_select[file:' . $file->id() . ']'; // Test exposed filters. diff --git a/tests/src/FunctionalJavascript/EntityReferenceWidgetTest.php b/tests/src/FunctionalJavascript/EntityReferenceWidgetTest.php index 33a3086..bcba1c4 100644 --- a/tests/src/FunctionalJavascript/EntityReferenceWidgetTest.php +++ b/tests/src/FunctionalJavascript/EntityReferenceWidgetTest.php @@ -306,6 +306,98 @@ class EntityReferenceWidgetTest extends EntityBrowserJavascriptTestBase { $this->waitForAjaxToFinish(); $assert_session->buttonNotExists('edit-field-entity-reference1-current-items-0-edit-button'); + // Test the cardinality handling. + $role = Role::load('authenticated'); + $this->grantPermissions($role, [ + 'access test_entity_browser_iframe_node_view entity browser pages', + 'bypass node access', + 'administer node form display', + ]); + FieldStorageConfig::load('node.field_entity_reference1')->setCardinality(2)->save(); + // Without using field cardinality, the view should contain checkboxes. + $view = \Drupal::configFactory()->getEditable('views.view.test_entity_reference_widget_view'); + $field = $view->get('display.default.display_options.fields.entity_browser_select', TRUE); + $field['use_field_cardinality'] = FALSE; + $view->set('display.default.display_options.fields.entity_browser_select', $field); + $view->save(); + $page->fillField('title[0][value]', 'Referencing node 3'); + $open_iframe_link = $assert_session->elementExists('css', 'a[data-drupal-selector="edit-field-entity-reference1-entity-browser-entity-browser-link"]'); + $open_iframe_link->click(); + $this->waitForAjaxToFinish(); + $session->switchToIFrame('entity_browser_iframe_test_entity_browser_iframe_node_view'); + $this->waitForAjaxToFinish(); + $style_selector = $page->find('css', 'input[value="node:1"].form-checkbox'); + $this->assertTrue($style_selector->isVisible()); + // If using field cardinality and field cardinality is greater than 1 then + // there should be still checkboxes. + $field = $view->get('display.default.display_options.fields.entity_browser_select', TRUE); + $field['use_field_cardinality'] = TRUE; + $view->set('display.default.display_options.fields.entity_browser_select', $field); + $view->save(); + $this->drupalGet('/node/add/article'); + $open_iframe_link = $assert_session->elementExists('css', 'a[data-drupal-selector="edit-field-entity-reference1-entity-browser-entity-browser-link"]'); + $open_iframe_link->click(); + $this->waitForAjaxToFinish(); + $session->switchToIFrame('entity_browser_iframe_test_entity_browser_iframe_node_view'); + $this->waitForAjaxToFinish(); + $style_selector = $page->find('css', 'input[value="node:1"].form-checkbox'); + $this->assertTrue($style_selector->isVisible()); + // If we select 3 nodes, EB shouldn't allow us. + $target_node3 = Node::create([ + 'title' => 'Target example node 3', + 'type' => 'article', + ]); + $target_node3->save(); + $this->drupalGet('/node/add/article'); + $open_iframe_link = $assert_session->elementExists('css', 'a[data-drupal-selector="edit-field-entity-reference1-entity-browser-entity-browser-link"]'); + $open_iframe_link->click(); + $this->waitForAjaxToFinish(); + $session->switchToIFrame('entity_browser_iframe_test_entity_browser_iframe_node_view'); + $this->waitForAjaxToFinish(); + $style_selector = $page->find('css', 'input[value="node:1"].form-checkbox'); + $this->assertTrue($style_selector->isVisible()); + $style_selector->click(); + $style_selector = $page->find('css', 'input[value="node:3"].form-checkbox'); + $this->assertTrue($style_selector->isVisible()); + $style_selector->click(); + $style_selector = $page->find('css', 'input[value="node:4"].form-checkbox'); + $this->assertTrue($style_selector->isVisible()); + $style_selector->click(); + $page->pressButton('Select entities'); + $assert_session->pageTextContains('You can only select up to 2 items'); + // If we change the cardinality to 1, we should have radios. + FieldStorageConfig::load('node.field_entity_reference1')->setCardinality(1)->save(); + $this->drupalGet('/node/add/article'); + $open_iframe_link = $assert_session->elementExists('css', 'a[data-drupal-selector="edit-field-entity-reference1-entity-browser-entity-browser-link"]'); + $open_iframe_link->click(); + $this->waitForAjaxToFinish(); + $session->switchToIFrame('entity_browser_iframe_test_entity_browser_iframe_node_view'); + $this->waitForAjaxToFinish(); + $style_selector = $page->find('css', 'input[value="node:1"].form-radio'); + $this->assertTrue($style_selector->isVisible()); + $style_selector->click(); + $page->pressButton('Select entities'); + $session->switchToIFrame(); + $this->waitForAjaxToFinish(); + // Assert the selected entity. + $assert_session->pageTextContains('Target example node 1'); + // Attempt to select more than one element. + $page->pressButton('Replace'); + $this->waitForAjaxToFinish(); + $session->switchToIFrame('entity_browser_iframe_test_entity_browser_iframe_node_view'); + $this->waitForAjaxToFinish(); + $style_selector = $page->find('css', 'input[value="node:1"].form-radio'); + $this->assertTrue($style_selector->isVisible()); + $style_selector->click(); + $style_selector = $page->find('css', 'input[value="node:3"].form-radio'); + $this->assertTrue($style_selector->isVisible()); + $style_selector->click(); + $page->pressButton('Select entities'); + $session->switchToIFrame(); + $this->waitForAjaxToFinish(); + // Assert the selected entity. + $assert_session->pageTextContains('Target example node 2'); + $assert_session->pageTextNotContains('Target example node 1'); } }