diff --git a/config/schema/entity_browser.schema.yml b/config/schema/entity_browser.schema.yml index 7416799..7e52912 100644 --- a/config/schema/entity_browser.schema.yml +++ b/config/schema/entity_browser.schema.yml @@ -227,3 +227,17 @@ field.widget.settings.entity_browser_file: views.display.entity_browser: type: views_display label: 'Entity browser display options' + +views.argument_default.entity_browser_widget_context: + type: mapping + label: 'Entity Browser Context' + mapping: + context_key: + type: string + label: 'Context key' + fallback: + type: string + label: 'Fallback value' + multiple: + type: string + label: 'Multiple values' diff --git a/src/Plugin/Field/FieldWidget/EntityReferenceBrowserWidget.php b/src/Plugin/Field/FieldWidget/EntityReferenceBrowserWidget.php index 792c335..8c26a43 100644 --- a/src/Plugin/Field/FieldWidget/EntityReferenceBrowserWidget.php +++ b/src/Plugin/Field/FieldWidget/EntityReferenceBrowserWidget.php @@ -572,11 +572,15 @@ class EntityReferenceBrowserWidget extends WidgetBase implements ContainerFactor * Data that should persist after the Entity Browser is rendered. */ protected function getPersistentData() { + $settings = $this->fieldDefinition->getSettings(); + $handler = $settings['handler_settings']; return [ 'validators' => [ - 'entity_type' => ['type' => $this->fieldDefinition->getFieldStorageDefinition()->getSetting('target_type')], + 'entity_type' => ['type' => $settings['target_type']], + ], + 'widget_context' => [ + 'target_bundles' => !empty($handler['target_bundles']) ? $handler['target_bundles'] : [], ], - 'widget_context' => [], ]; } diff --git a/src/Plugin/Field/FieldWidget/FileBrowserWidget.php b/src/Plugin/Field/FieldWidget/FileBrowserWidget.php index 849a1bc..fa9a52a 100644 --- a/src/Plugin/Field/FieldWidget/FileBrowserWidget.php +++ b/src/Plugin/Field/FieldWidget/FileBrowserWidget.php @@ -17,6 +17,7 @@ use Drupal\image\Entity\ImageStyle; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Drupal\Core\Session\AccountInterface; +use Symfony\Component\HttpFoundation\File\MimeType\MimeTypeGuesserInterface; /** * Entity browser file widget. @@ -63,6 +64,13 @@ class FileBrowserWidget extends EntityReferenceBrowserWidget { protected $displayRepository; /** + * The mime type guesser service. + * + * @var \Symfony\Component\HttpFoundation\File\MimeType\MimeTypeGuesserInterface + */ + protected $mimeTypeGuesser; + + /** * Constructs widget plugin. * * @param string $plugin_id @@ -89,13 +97,16 @@ class FileBrowserWidget extends EntityReferenceBrowserWidget { * The module handler service. * @param \Drupal\Core\Session\AccountInterface $current_user * The current user. + * @param \Symfony\Component\HttpFoundation\File\MimeType\MimeTypeGuesserInterface $mime_type_guesser + * The mime type guesser service. */ - public function __construct($plugin_id, $plugin_definition, FieldDefinitionInterface $field_definition, array $settings, array $third_party_settings, EntityTypeManagerInterface $entity_type_manager, EventDispatcherInterface $event_dispatcher, FieldWidgetDisplayManager $field_display_manager, ConfigFactoryInterface $config_factory, EntityDisplayRepositoryInterface $display_repository, ModuleHandlerInterface $module_handler, AccountInterface $current_user) { + public function __construct($plugin_id, $plugin_definition, FieldDefinitionInterface $field_definition, array $settings, array $third_party_settings, EntityTypeManagerInterface $entity_type_manager, EventDispatcherInterface $event_dispatcher, FieldWidgetDisplayManager $field_display_manager, ConfigFactoryInterface $config_factory, EntityDisplayRepositoryInterface $display_repository, ModuleHandlerInterface $module_handler, AccountInterface $current_user, MimeTypeGuesserInterface $mime_type_guesser) { parent::__construct($plugin_id, $plugin_definition, $field_definition, $settings, $third_party_settings, $entity_type_manager, $event_dispatcher, $field_display_manager, $module_handler, $current_user); $this->entityTypeManager = $entity_type_manager; $this->fieldDisplayManager = $field_display_manager; $this->configFactory = $config_factory; $this->displayRepository = $display_repository; + $this->mimeTypeGuesser = $mime_type_guesser; } /** @@ -114,7 +125,8 @@ class FileBrowserWidget extends EntityReferenceBrowserWidget { $container->get('config.factory'), $container->get('entity_display.repository'), $container->get('module_handler'), - $container->get('current_user') + $container->get('current_user'), + $container->get('file.mime_type.guesser') ); } @@ -493,6 +505,16 @@ class FileBrowserWidget extends EntityReferenceBrowserWidget { // Provide context for widgets to enhance their configuration. $data['widget_context']['upload_location'] = $settings['uri_scheme'] . '://' . $settings['file_directory']; $data['widget_context']['upload_validators'] = $this->getFileValidators(TRUE); + // Assemble valid mime types for filtering. This is required if we want to + // contextually filter allowed extensions in views, as views arguments can + // only filter on exact values. Otherwise we would pass %png or use REGEXP. + $mimetypes = []; + foreach (explode(' ', $settings['file_extensions']) as $extension) { + if ($guess = $this->mimeTypeGuesser->guess('file.' . $extension)) { + $mimetypes[] = $guess; + } + } + $data['widget_context']['target_file_mimetypes'] = $mimetypes; return $data; } diff --git a/src/Plugin/views/argument_default/EntityBrowserWidgetContext.php b/src/Plugin/views/argument_default/EntityBrowserWidgetContext.php new file mode 100644 index 0000000..17b4d40 --- /dev/null +++ b/src/Plugin/views/argument_default/EntityBrowserWidgetContext.php @@ -0,0 +1,126 @@ +selectionStorage = $selection_storage; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + return new static( + $configuration, + $plugin_id, + $plugin_definition, + $container->get('entity_browser.selection_storage') + ); + } + + /** + * {@inheritdoc} + */ + protected function defineOptions() { + $options = parent::defineOptions(); + $options['context_key'] = ['default' => '']; + $options['fallback'] = ['default' => '']; + $options['multiple'] = ['default' => 'and']; + return $options; + } + + /** + * {@inheritdoc} + */ + public function buildOptionsForm(&$form, FormStateInterface $form_state) { + parent::buildOptionsForm($form, $form_state); + $form['context_key'] = [ + '#type' => 'textfield', + '#title' => $this->t('Context key'), + '#description' => $this->t('The key within the widget context. If the corresponding value is an array its values will be joined with an AND.'), + '#default_value' => $this->options['context_key'], + ]; + $form['fallback'] = [ + '#type' => 'textfield', + '#title' => $this->t('Fallback value'), + '#description' => $this->t('The fallback value to use when the context is not present. (ex: "all")'), + '#default_value' => $this->options['fallback'], + ]; + $form['multiple'] = [ + '#type' => 'radios', + '#title' => $this->t('Multiple values'), + '#description' => $this->t('Conjunction to use when handling multiple values.'), + '#default_value' => $this->options['multiple'], + '#options' => [ + 'and' => $this->t('AND'), + 'or' => $this->t('OR'), + ], + ]; + } + + /** + * {@inheritdoc} + */ + public function access() { + return $this->view->getDisplay()->pluginId === 'entity_browser'; + } + + /** + * {@inheritdoc} + */ + public function getArgument() { + $current_request = $this->view->getRequest(); + $context_key = $this->options['context_key']; + $argument = $this->options['fallback']; + // Check if the widget context is available. + if (!empty($context_key) && $current_request->query->has('uuid')) { + $uuid = $current_request->query->get('uuid'); + if ($storage = $this->selectionStorage->get($uuid)) { + if (isset($storage['widget_context']) && !empty($storage['widget_context'][$context_key])) { + $value = $storage['widget_context'][$context_key]; + if (is_string($value)) { + $argument = $value; + } + // If the context value is an array, test that it can be imploded. + else if (is_array($value)) { + $non_scalar = array_filter($value, function ($item) { + return ! is_scalar($item); + }); + if (empty($non_scalar)) { + $conjunction = ($this->options['multiple'] == 'and') ? ',' : '+'; + $argument = implode($conjunction, $value); + } + } + } + } + } + return $argument; + } + +} diff --git a/tests/modules/entity_browser_test/config/install/entity_browser.browser.test_contextual_filter.yml b/tests/modules/entity_browser_test/config/install/entity_browser.browser.test_contextual_filter.yml new file mode 100644 index 0000000..d6bfd16 --- /dev/null +++ b/tests/modules/entity_browser_test/config/install/entity_browser.browser.test_contextual_filter.yml @@ -0,0 +1,31 @@ +uuid: 9a607a9b-d72e-412c-b4b0-8ea8c4018817 +langcode: en +status: true +dependencies: + config: + - views.view.test_contextual_filter + module: + - views +name: test_contextual_filter +label: 'Test contextual filter' +display: iframe +display_configuration: + width: '650' + height: '500' + link_text: 'Select entities' + auto_open: false +selection_display: no_display +selection_display_configuration: { } +widget_selector: single +widget_selector_configuration: { } +widgets: + 1868bb2c-8c45-48f4-b2ca-8c2902dfd15b: + settings: + view: test_contextual_filter + view_display: entity_browser_1 + submit_text: 'Select entities' + auto_select: false + uuid: 1868bb2c-8c45-48f4-b2ca-8c2902dfd15b + weight: 1 + label: view + id: view diff --git a/tests/modules/entity_browser_test/config/install/views.view.test_contextual_filter.yml b/tests/modules/entity_browser_test/config/install/views.view.test_contextual_filter.yml new file mode 100644 index 0000000..33f91bf --- /dev/null +++ b/tests/modules/entity_browser_test/config/install/views.view.test_contextual_filter.yml @@ -0,0 +1,381 @@ +uuid: b56ecf0a-010a-416b-bcd4-8f1be8e5134e +langcode: en +status: true +dependencies: + module: + - entity_browser + - node + - user +id: test_contextual_filter +label: 'Test contextual filter' +module: views +description: '' +tag: '' +base_table: node_field_data +base_field: nid +core: 8.x +display: + default: + display_plugin: default + id: default + display_title: Master + position: 0 + display_options: + access: + type: perm + options: + perm: 'access content' + cache: + type: tag + options: { } + query: + type: views_query + options: + disable_sql_rewrite: false + distinct: false + replica: false + query_comment: '' + query_tags: { } + exposed_form: + type: basic + options: + submit_button: Apply + reset_button: false + reset_button_label: Reset + exposed_sorts_label: 'Sort by' + expose_sort_order: true + sort_asc_label: Asc + sort_desc_label: Desc + pager: + type: mini + options: + items_per_page: 10 + offset: 0 + id: 0 + total_pages: null + expose: + items_per_page: false + items_per_page_label: 'Items per page' + items_per_page_options: '5, 10, 25, 50' + items_per_page_options_all: false + items_per_page_options_all_label: '- All -' + offset: false + offset_label: Offset + tags: + previous: ‹‹ + next: ›› + style: + type: table + options: + grouping: { } + row_class: '' + default_row_class: true + override: true + sticky: false + caption: '' + summary: '' + description: '' + columns: + title: title + info: + title: + sortable: false + default_sort_order: asc + align: '' + separator: '' + empty_column: false + responsive: '' + default: '-1' + empty_table: false + row: + type: fields + options: + inline: { } + separator: '' + hide_empty: false + default_field_elements: true + fields: + entity_browser_select: + id: entity_browser_select + table: node + field: entity_browser_select + relationship: none + group_type: group + admin_label: '' + label: '' + exclude: false + alter: + alter_text: false + text: '' + make_link: false + path: '' + absolute: false + external: false + replace_spaces: false + path_case: none + trim_whitespace: false + alt: '' + rel: '' + link_class: '' + prefix: '' + suffix: '' + target: '' + nl2br: false + max_length: 0 + word_boundary: true + ellipsis: true + more_link: false + more_link_text: '' + more_link_path: '' + strip_tags: false + trim: false + preserve_tags: '' + html: false + element_type: '' + element_class: '' + element_label_type: '' + element_label_class: '' + element_label_colon: false + element_wrapper_type: '' + element_wrapper_class: '' + element_default_classes: true + empty: '' + hide_empty: false + empty_zero: false + hide_alter_empty: true + entity_type: node + plugin_id: entity_browser_select + title: + id: title + table: node_field_data + field: title + relationship: none + group_type: group + admin_label: '' + label: Title + exclude: false + alter: + alter_text: false + text: '' + make_link: false + path: '' + absolute: false + external: false + replace_spaces: false + path_case: none + trim_whitespace: false + alt: '' + rel: '' + link_class: '' + prefix: '' + suffix: '' + target: '' + nl2br: false + max_length: 0 + word_boundary: false + ellipsis: false + more_link: false + more_link_text: '' + more_link_path: '' + strip_tags: false + trim: false + preserve_tags: '' + html: false + element_type: '' + element_class: '' + element_label_type: '' + element_label_class: '' + element_label_colon: false + element_wrapper_type: '' + element_wrapper_class: '' + element_default_classes: true + empty: '' + hide_empty: false + empty_zero: false + hide_alter_empty: true + click_sort_column: value + type: string + settings: + link_to_entity: true + group_column: value + group_columns: { } + group_rows: true + delta_limit: 0 + delta_offset: 0 + delta_reversed: false + delta_first_last: false + multi_type: separator + separator: ', ' + field_api_classes: false + entity_type: node + entity_field: title + plugin_id: field + type: + id: type + table: node_field_data + field: type + relationship: none + group_type: group + admin_label: '' + label: Type + exclude: false + alter: + alter_text: false + text: '' + make_link: false + path: '' + absolute: false + external: false + replace_spaces: false + path_case: none + trim_whitespace: false + alt: '' + rel: '' + link_class: '' + prefix: '' + suffix: '' + target: '' + nl2br: false + max_length: 0 + word_boundary: true + ellipsis: true + more_link: false + more_link_text: '' + more_link_path: '' + strip_tags: false + trim: false + preserve_tags: '' + html: false + element_type: '' + element_class: '' + element_label_type: '' + element_label_class: '' + element_label_colon: true + element_wrapper_type: '' + element_wrapper_class: '' + element_default_classes: true + empty: '' + hide_empty: false + empty_zero: false + hide_alter_empty: true + click_sort_column: target_id + type: entity_reference_label + settings: + link: false + group_column: target_id + group_columns: { } + group_rows: true + delta_limit: 0 + delta_offset: 0 + delta_reversed: false + delta_first_last: false + multi_type: separator + separator: ', ' + field_api_classes: false + entity_type: node + entity_field: type + plugin_id: field + filters: + status: + value: '1' + table: node_field_data + field: status + plugin_id: boolean + entity_type: node + entity_field: status + id: status + expose: + operator: '' + group: 1 + sorts: + created: + id: created + table: node_field_data + field: created + order: DESC + entity_type: node + entity_field: created + plugin_id: date + relationship: none + group_type: group + admin_label: '' + exposed: false + expose: + label: '' + granularity: second + header: { } + footer: { } + empty: { } + relationships: { } + arguments: + type: + id: type + table: node_field_data + field: type + relationship: none + group_type: group + admin_label: '' + default_action: default + exception: + value: all + title_enable: false + title: All + title_enable: false + title: '' + default_argument_type: entity_browser_widget_context + default_argument_options: + context_key: target_bundles + fallback: all + multiple: or + default_argument_skip_url: false + summary_options: + base_path: '' + count: true + items_per_page: 25 + override: false + summary: + sort_order: asc + number_of_records: 0 + format: default_summary + specify_validation: false + validate: + type: none + fail: 'not found' + validate_options: { } + glossary: false + limit: 0 + case: none + path_case: none + transform_dash: false + break_phrase: true + entity_type: node + entity_field: type + plugin_id: node_type + display_extenders: { } + cache_metadata: + max-age: 0 + contexts: + - 'languages:language_content' + - 'languages:language_interface' + - url + - url.query_args + - 'user.node_grants:view' + - user.permissions + tags: { } + entity_browser_1: + display_plugin: entity_browser + id: entity_browser_1 + display_title: 'Entity browser' + position: 1 + display_options: + display_extenders: { } + cache_metadata: + max-age: 0 + contexts: + - 'languages:language_content' + - 'languages:language_interface' + - url + - url.query_args + - 'user.node_grants:view' + - user.permissions + tags: { } diff --git a/tests/src/FunctionalJavascript/EntityBrowserTest.php b/tests/src/FunctionalJavascript/EntityBrowserTest.php index e8add2c..e46f01a 100644 --- a/tests/src/FunctionalJavascript/EntityBrowserTest.php +++ b/tests/src/FunctionalJavascript/EntityBrowserTest.php @@ -210,4 +210,60 @@ class EntityBrowserTest extends EntityBrowserJavascriptTestBase { $this->assertSession()->buttonNotExists('Second submit button'); } + /** + * Tests the EntityBrowserWidgetContext argument_default views plugin. + */ + public function testContextualFilter() { + $this->drupalCreateContentType(['type' => 'type_one', 'name' => 'Type One']); + $this->drupalCreateContentType(['type' => 'type_two', 'name' => 'Type Two']); + $this->drupalCreateContentType(['type' => 'type_three', 'name' => 'Type Three']); + $this->createNode(['type' => 'type_one', 'title' => 'Type one node']); + $this->createNode(['type' => 'type_two', 'title' => 'Type two node']); + $this->createNode(['type' => 'type_three', 'title' => 'Type three node']); + + /** @var \Drupal\Core\Entity\Display\EntityFormDisplayInterface $form_display */ + $form_display = $this->container->get('entity_type.manager') + ->getStorage('entity_form_display') + ->load('node.article.default'); + + $form_display->setComponent('field_reference', [ + 'type' => 'entity_browser_entity_reference', + 'settings' => [ + 'entity_browser' => 'test_contextual_filter', + 'field_widget_display' => 'label', + 'open' => TRUE, + ], + ])->save(); + + /** @var \Drupal\Core\Field\FieldConfigInterface $field_config */ + $field_config = $this->container->get('entity_type.manager') + ->getStorage('field_config') + ->load('node.article.field_reference'); + $handler_settings = $field_config->getSetting('handler_settings'); + $handler_settings['target_bundles'] = [ + 'type_one' => 'type_one', + 'type_three' => 'type_three', + ]; + $field_config->setSetting('handler_settings', $handler_settings); + $field_config->save(); + + $account = $this->drupalCreateUser([ + 'access test_contextual_filter entity browser pages', + 'create article content', + 'access content', + ]); + $this->drupalLogin($account); + + $this->drupalGet('node/add/article'); + + // Open the entity browser widget form. + $this->getSession()->getPage()->clickLink('Select entities'); + $this->getSession()->switchToIFrame('entity_browser_iframe_test_contextual_filter'); + + // Check that only nodes of an allowed type are listed. + $this->assertSession()->pageTextContains('Type one node'); + $this->assertSession()->pageTextNotContains('Type two node'); + $this->assertSession()->pageTextContains('Type three node'); + } + }