Commit a529f46a authored by michaellander's avatar michaellander Committed by michaellander

Issue #2876110 by michaellander, arthur_lorenz, opi, Bhanu951, switzern:...

Issue #2876110 by michaellander, arthur_lorenz, opi, Bhanu951, switzern: Select a Category of blocks instead of individual blocks when configuring the field
parent 0de6dfb2
<?php
/**
* @file
* Install, update and uninstall functions for the Block Field module.
*/
/**
* Update 'block_field' definition settings to reflect new plugin format.
*/
function block_field_update_8001() {
$entity_field_manager = \Drupal::service('entity_field.manager');
// Loop through all entities where 'block_fields' are defined.
foreach ($entity_field_manager->getFieldMapByFieldType('block_field') as $entity_type_id => $fields) {
foreach ($fields as $field) {
// Loop through each bundle and load definition.
foreach ($field['bundles'] as $bundle) {
foreach ($entity_field_manager->getFieldDefinitions($entity_type_id, $bundle) as $definition) {
if ($definition->getType() == 'block_field') {
$settings = $definition->get('settings');
// Upgrade path for patches pre-comment 20
// https://www.drupal.org/project/block_field/issues/2876110
if (isset($settings['filter']) && $settings['filter'] == 'categories') {
$settings = [
'selection' => 'categories',
'selection_settings' => [
'categories' => $settings['categories'],
],
];
}
// Update settings for existing configuration.
elseif (isset($settings['plugin_ids'])) {
$settings = [
'selection' => 'blocks',
'selection_settings' => [
'plugin_ids' => $settings['plugin_ids'],
],
];
}
$definition->setSettings($settings);
// Unset existing indices because setSettings is additive only.
$settings_raw = $definition->get('settings');
unset($settings_raw['plugin_ids']);
unset($settings_raw['filter']);
$definition->set('settings', $settings_raw);
$definition->save();
}
}
}
}
}
}
......@@ -2,3 +2,6 @@ services:
block_field.manager:
class: Drupal\block_field\BlockFieldManager
arguments: ['@plugin.manager.block', '@context.repository']
plugin.manager.block_field_selection:
class: Drupal\block_field\BlockFieldSelectionManager
parent: default_plugin_manager
......@@ -26,7 +26,25 @@ field.storage_settings.block_field:
field.field_settings.block_field:
type: mapping
label: 'Block field settings'
label: 'Block Field field settings'
mapping:
selection:
type: string
label: 'Selection method'
selection_settings:
type: block_field_selection.[%parent.selection]
label: 'Block Field selection plugin settings'
block_field_selection:
type: mapping
label: 'Block field selection handler settings'
block_field_selection.*:
type: block_field_selection
block_field_selection.blocks:
type: block_field_selection
label: 'Blocks(default) selection handler settings'
mapping:
plugin_ids:
type: sequence
......@@ -34,6 +52,16 @@ field.field_settings.block_field:
sequence:
type: string
block_field_selection.categories:
type: block_field_selection
label: 'Categories selection handler settings'
mapping:
categories:
type: sequence
label: 'Categories'
sequence:
type: string
field.value.block_field:
type: mapping
label: 'Default value'
......
<?php
namespace Drupal\block_field\Annotation;
use Drupal\Component\Annotation\Plugin;
/**
* Defines a Block field selection item annotation object.
*
* @see \Drupal\block_field\BlockFieldSelectionManager
* @see plugin_api
*
* @Annotation
*/
class BlockFieldSelection extends Plugin {
/**
* The plugin ID.
*
* @var string
*/
public $id;
/**
* The label of the plugin.
*
* @var \Drupal\Core\Annotation\Translation
*
* @ingroup plugin_translatable
*/
public $label;
}
......@@ -45,4 +45,11 @@ class BlockFieldManager implements BlockFieldManagerInterface {
return $this->blockManager->getSortedDefinitions($definitions);
}
/**
* {@inheritdoc}
*/
public function getBlockCategories() {
return $this->blockManager->getCategories();
}
}
......@@ -15,4 +15,12 @@ interface BlockFieldManagerInterface {
*/
public function getBlockDefinitions();
/**
* Get list of all block categories.
*
* @return string[]
* A numerically indexed array of block categories.
*/
public function getBlockCategories();
}
<?php
namespace Drupal\block_field;
use Drupal\Component\Plugin\ConfigurablePluginInterface;
use Drupal\Component\Plugin\PluginBase;
use Drupal\Component\Utility\NestedArray;
use Drupal\Core\Form\FormStateInterface;
/**
* Base class for Block field selection plugins.
*/
abstract class BlockFieldSelectionBase extends PluginBase implements BlockFieldSelectionInterface, ConfigurablePluginInterface {
/**
* Constructs a new selection object.
*
* @param array $configuration
* A configuration array containing information about the plugin instance.
* @param string $plugin_id
* The plugin_id for the plugin instance.
* @param mixed $plugin_definition
* The plugin implementation definition.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->setConfiguration($configuration);
}
/**
* {@inheritdoc}
*/
public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
return $form;
}
/**
* {@inheritdoc}
*/
public function validateConfigurationForm(array &$form, FormStateInterface $form_state) {
}
/**
* {@inheritdoc}
*/
public function submitConfigurationForm(array &$form, FormStateInterface $form_state) {
}
/**
* {@inheritdoc}
*/
public function defaultConfiguration() {
return [];
}
/**
* {@inheritdoc}
*/
public function getConfiguration() {
return $this->configuration;
}
/**
* {@inheritdoc}
*/
public function setConfiguration(array $configuration) {
// Merge in defaults.
$this->configuration = NestedArray::mergeDeep(
$this->defaultConfiguration(),
$configuration
);
}
/**
* {@inheritdoc}
*/
public function calculateDependencies() {
// TODO: Implement calculateDependencies() method.
}
/**
* Move settings up a level for easier processing and storage.
*/
public function formProcessMergeParent($element) {
$parents = $element['#parents'];
array_pop($parents);
$element['#parents'] = $parents;
return $element;
}
}
<?php
namespace Drupal\block_field;
use Drupal\Component\Plugin\PluginInspectionInterface;
use Drupal\Core\Plugin\PluginFormInterface;
/**
* Defines an interface for Block field selection plugins.
*/
interface BlockFieldSelectionInterface extends PluginInspectionInterface, PluginFormInterface {
/**
* Returns filtered block definitions based on plugin settings.
*
* @return array
* An array of filtered block definitions.
*/
public function getReferenceableBlockDefinitions();
}
<?php
namespace Drupal\block_field;
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\Plugin\DefaultPluginManager;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
/**
* Provides the Block field selection plugin manager.
*/
class BlockFieldSelectionManager extends DefaultPluginManager {
/**
* Constructs a new BlockFieldSelectionManager object.
*
* @param \Traversable $namespaces
* An object that implements \Traversable which contains the root paths
* keyed by the corresponding namespace to look for plugin implementations.
* @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend
* Cache backend instance to use.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler to invoke the alter hook with.
*/
public function __construct(\Traversable $namespaces, CacheBackendInterface $cache_backend, ModuleHandlerInterface $module_handler) {
parent::__construct('Plugin/block_field/BlockFieldSelection', $namespaces, $module_handler, 'Drupal\block_field\BlockFieldSelectionInterface', 'Drupal\block_field\Annotation\BlockFieldSelection');
$this->alterInfo('block_field_block_field_selection_info');
$this->setCacheBackend($cache_backend, 'block_field_block_field_selection_plugins');
}
/**
* Loads all definitions and returns key => value array.
*
* @return array
* Array of options from definitions.
*/
public function getOptions() {
$definitions = $this->getDefinitions();
$options = [];
foreach ($definitions as $plugin_id => $definition) {
$options[$plugin_id] = $definition['label'];
}
return $options;
}
/**
* Returns an instance of BlockFieldSelectionInterface from $field.
*
* @param \Drupal\Core\Field\FieldDefinitionInterface $field
* The 'block_field' field definition.
*
* @return \Drupal\block_field\BlockFieldSelectionInterface
* The BlockFieldSelectionInterface instance.
*
* @throws \Drupal\Component\Plugin\Exception\PluginException
*/
public function getSelectionHandler(FieldDefinitionInterface $field) {
$settings = $field->getSetting('selection_settings') ? $field->getSetting('selection_settings') : [];
return $this->createInstance($field->getSetting('selection'), $settings);
}
/**
* Returns an key => value array based on allowed referenceable blocks.
*
* @param \Drupal\Core\Field\FieldDefinitionInterface $field
* The 'block_field' field definition.
*
* @return array
* Array of options from definitions.
*
* @throws \Drupal\Component\Plugin\Exception\PluginException
*/
public function getWidgetOptions(FieldDefinitionInterface $field) {
$handler = $this->getSelectionHandler($field);
$options = [];
foreach ($handler->getReferenceableBlockDefinitions() as $plugin_id => $definition) {
$category = (string) $definition['category'];
$options[$category][$plugin_id] = $definition['admin_label'];
}
return $options;
}
}
......@@ -3,9 +3,11 @@
namespace Drupal\block_field\Plugin\Field\FieldType;
use Drupal\block_field\BlockFieldItemInterface;
use Drupal\Console\Core\Utils\NestedArray;
use Drupal\Core\Field\FieldItemBase;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Render\Element;
use Drupal\Core\TypedData\DataDefinition;
use Drupal\Core\TypedData\MapDataDefinition;
......@@ -28,7 +30,8 @@ class BlockFieldItem extends FieldItemBase implements BlockFieldItemInterface {
*/
public static function defaultFieldSettings() {
return [
'plugin_ids' => [],
'selection' => 'blocks',
'selection_settings' => [],
] + parent::defaultFieldSettings();
}
......@@ -81,42 +84,51 @@ class BlockFieldItem extends FieldItemBase implements BlockFieldItemInterface {
public function fieldSettingsForm(array $form, FormStateInterface $form_state) {
$field = $form_state->getFormObject()->getEntity();
/** @var \Drupal\block_field\BlockFieldManagerInterface $block_field_manager */
$block_field_manager = \Drupal::service('block_field.manager');
$definitions = $block_field_manager->getBlockDefinitions();
foreach ($definitions as $plugin_id => $definition) {
$options[$plugin_id] = [
['category' => (string) $definition['category']],
['label' => $definition['admin_label'] . ' (' . $plugin_id . ')'],
['provider' => $definition['provider']],
];
}
$default_value = $field->getSetting('plugin_ids') ?: array_keys($options);
/** @var \Drupal\block_field\BlockFieldSelectionManager $block_field_selection_manager */
$block_field_selection_manager = \Drupal::service('plugin.manager.block_field_selection');
$options = $block_field_selection_manager->getOptions();
$form = [
'#type' => 'container',
'#process' => [[get_class($this), 'fieldSettingsAjaxProcess']],
'#element_validate' => [[get_class($this), 'fieldSettingsFormValidate']],
$element = [];
$element['blocks'] = [
];
$form['selection'] = [
'#type' => 'details',
'#title' => $this->t('Blocks'),
'#description' => $this->t('Please select available blocks.'),
'#open' => $field->getSetting('plugin_ids') ? TRUE : FALSE,
'#title' => t('Available blocks'),
'#open' => TRUE,
'#tree' => TRUE,
'#process' => [[get_class($this), 'formProcessMergeParent']],
];
$element['blocks']['plugin_ids'] = [
'#type' => 'tableselect',
'#header' => [
'Category',
'Label/ID',
'Provider',
],
$form['selection']['selection'] = [
'#type' => 'select',
'#title' => t('Selection method'),
'#options' => $options,
'#js_select' => TRUE,
'#default_value' => $field->getSetting('selection'),
'#required' => TRUE,
'#empty' => t('No blocks are available.'),
'#parents' => ['settings', 'plugin_ids'],
'#element_validate' => [[get_called_class(), 'validatePluginIds']],
'#default_value' => array_combine($default_value, $default_value),
'#ajax' => TRUE,
'#limit_validation_errors' => [],
];
return $element;
$form['selection']['selection_submit'] = [
'#type' => 'submit',
'#value' => t('Change selection'),
'#limit_validation_errors' => [],
'#attributes' => [
'class' => ['js-hide'],
],
'#submit' => [[get_class($this), 'settingsAjaxSubmit']],
];
$form['selection']['selection_settings'] = [
'#type' => 'container',
'#attributes' => ['class' => ['block_field-settings']],
];
$selection = $block_field_selection_manager->getSelectionHandler($field);
$form['selection']['selection_settings'] += $selection->buildConfigurationForm([], $form_state);
return $form;
}
/**
......@@ -181,17 +193,85 @@ class BlockFieldItem extends FieldItemBase implements BlockFieldItemInterface {
}
/**
* Validates plugin_ids table select element.
* Render API callback.
*
* Processes the field settings form and allows access to
* the form state.
*
* @see \Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem
* @see static::fieldSettingsForm()
*/
public static function validatePluginIds(array &$element, FormStateInterface $form_state, &$complete_form) {
$value = array_filter($element['#value']);
if (array_keys($element['#options']) == array_keys($value)) {
$form_state->setValueForElement($element, []);
public static function fieldSettingsAjaxProcess($form, FormStateInterface $form_state) {
static::fieldSettingsAjaxProcessElement($form, $form);
return $form;
}
/**
* Adds block_field specific properties to AJAX form elements from settings.
*
* @see \Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem
* @see static::fieldSettingsAjaxProcess()
*/
public static function fieldSettingsAjaxProcessElement(&$element, $main_form) {
if (!empty($element['#ajax'])) {
$element['#ajax'] = [
'callback' => [get_called_class(), 'settingsAjax'],
'wrapper' => $main_form['#id'],
'element' => $main_form['#array_parents'],
];
}
else {
$form_state->setValueForElement($element, $value);
foreach (Element::children($element) as $key) {
static::fieldSettingsAjaxProcessElement($element[$key], $main_form);
}
}
/**
* Ajax callback for the selection settings form.
*
* @see \Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem
* @see static::fieldSettingsForm()
*/
public static function settingsAjax($form, FormStateInterface $form_state) {
return NestedArray::getValue($form, $form_state->getTriggeringElement()['#ajax']['element']);
}
/**
* Submit selection for the non-JS case.
*
* @see \Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem
* @see static::fieldSettingsForm()
*/
public static function settingsAjaxSubmit($form, FormStateInterface $form_state) {
$form_state->setRebuild();
}
/**
* Render API callback.
*
* Moves block_field specific Form API elements
* (i.e. 'selection_settings') up a level for easier processing by the
* validation and submission selections.
*/
public static function formProcessMergeParent($element) {
$parents = $element['#parents'];
array_pop($parents);
$element['#parents'] = $parents;
return $element;
}
/**
* Form element validation handler; Invokes selection plugin's validation.
*
* @param array $form
* The form where the settings form is being included in.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The form state of the (entire) configuration form.
*/
public static function fieldSettingsFormValidate(array $form, FormStateInterface $form_state) {
$field = $form_state->getFormObject()->getEntity();
$handler = \Drupal::service('plugin.manager.block_field_selection')->getSelectionHandler($field);
$handler->validateConfigurationForm($form, $form_state);
}
}
......@@ -2,6 +2,7 @@
namespace Drupal\block_field\Plugin\Field\FieldWidget;
use Drupal\block_field\BlockFieldSelectionManager;
use Drupal\Component\Utility\NestedArray;
use Drupal\Core\Block\BlockManagerInterface;
use Drupal\Core\Field\FieldDefinitionInterface;
......@@ -34,12 +35,20 @@ class BlockFieldWidget extends WidgetBase implements ContainerFactoryPluginInter
*/
protected $blockManager;
/**
* The block field selection manager.
*
* @var \Drupal\block_field\BlockFieldSelectionManager
*/
protected $blockFieldSelectionManager;
/**
* {@inheritdoc}
*/
public function __construct($plugin_id, $plugin_definition, FieldDefinitionInterface $field_definition, array $settings, array $third_party_settings, BlockManagerInterface $block_manager) {
public function __construct($plugin_id, $plugin_definition, FieldDefinitionInterface $field_definition, array $settings, array $third_party_settings, BlockManagerInterface $block_manager, BlockFieldSelectionManager $block_field_selection_manager) {
parent::__construct($plugin_id, $plugin_definition, $field_definition, $settings, $third_party_settings);
$this->blockManager = $block_manager;
$this->blockFieldSelectionManager = $block_field_selection_manager;
}
/**
......@@ -52,7 +61,8 @@ class BlockFieldWidget extends WidgetBase implements ContainerFactoryPluginInter
$configuration['field_definition'],
$configuration['settings'],
$configuration['third_party_settings'],
$container->get('plugin.manager.block')
$container->get('plugin.manager.block'),
$container->get('plugin.manager.block_field_selection')
);
}
......@@ -107,8 +117,6 @@ class BlockFieldWidget extends WidgetBase implements ContainerFactoryPluginInter
[$field_name, $delta, 'settings']
));
$plugin_ids = $this->fieldDefinition->getSetting('plugin_ids');
$values = $form_state->getValues();
$item->plugin_id = (isset($values[$field_name][$delta]['plugin_id'])) ? $values[$field_name][$delta]['plugin_id'] : $item->plugin_id;
if (!empty($values[$field_name][$delta]['settings'])) {
......@@ -118,27 +126,13 @@ class BlockFieldWidget extends WidgetBase implements ContainerFactoryPluginInter
$item->settings = $item->settings ?: [];
}
$options = [];
/** @var \Drupal\block_field\BlockFieldManagerInterface $block_field_manager */
$block_field_manager = \Drupal::service('block_field.manager');
$definitions = $block_field_manager->getBlockDefinitions();
foreach ($definitions as $id => $definition) {
// If allowed plugin ids are set then check that this block should be
// included.
if ($plugin_ids && !isset($plugin_ids[$id])) {
// Remove the definition, so that we have an accurate list of allowed
// blocks definitions.
unset($definitions[$id]);
continue;
$options = $this->blockFieldSelectionManager->getWidgetOptions($this->fieldDefinition);
if ($item->plugin_id) {
// If plugin_id not in second level arrays, unset plugin_id and settings..
if (!in_array($item->plugin_id, array_keys(call_user_func_array('array_merge', $options)), TRUE)) {
$item->plugin_id = '';
$item->setting = [];
}
$category = (string) $definition['category'];
$options[$category][$id] = $definition['admin_label'];
}
// Make sure the plugin id is allowed, if not clear all settings.
if ($item->plugin_id && !isset($definitions[$item->plugin_id])) {
$item->plugin_id = '';
$item->setting = [];
}
$element['plugin_id'] = [
......
<?php
namespace Drupal\block_field\Plugin\block_field\BlockFieldSelection;
use Drupal\block_field\BlockFieldSelectionBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
/**
* Provides a 'categories' BlockFieldSection.
*
* @BlockFieldSelection(
* id = "blocks",
* label = @Translation("Blocks"),
* )
*/
class Blocks extends BlockFieldSelectionBase {
use StringTranslationTrait;
/**
* {@inheritdoc}
*/
public function defaultConfiguration() {
return [
'plugin_ids' => [],
] + parent::defaultConfiguration();
}
/**
* {@inheritdoc}
*/
public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
$form = parent::buildConfigurationForm($form, $form_state);
/** @var \Drupal\block_field\BlockFieldManagerInterface $block_field_manager */
$block_field_manager = \Drupal::service('block_field.manager');
$definitions = $block_field_manager->getBlockDefinitions();
foreach ($definitions as $plugin_id => $definition) {
$options[$plugin_id] = [
['category' => (string) $definition['category']],
['label' => $definition['admin_label'] . ' (' . $plugin_id . ')'],
['provider' => $definition['provider']],
];
}
$default_value = !empty($this->getConfiguration()['plugin_ids']) ? $this->getConfiguration()['plugin_ids'] : array_keys($options);
$form['blocks'] = [
'#type' => 'details',
'#title' => $this->t('Blocks'),
'#description' => $this->t('Please select available blocks.'),
'#open' => empty($this->getConfiguration()['plugin_ids']),
'#process' => [[$this, 'formProcessMergeParent']],
];
$form['blocks']['plugin_ids'] = [
'#type' => 'tableselect',
'#header' => [
$this->t('Category'),
$this->t('Label/ID'),
$this->t('Provider'),
],
'#options' => $options,
'#js_select' => TRUE,
'#required' => TRUE,
'#empty' => $this->t('No blocks are available.'),
'#element_validate' => [[get_called_class(), 'validatePluginIds']],
'#default_value' => array_combine($default_value, $default_value),
];
return $form;
}
/**
* Validates plugin_ids table select element.
*
* @param array $element
* A form element.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The $form_state from complete form.
* @param array $complete_form
* Complete parent form.
*
* @return array
* Returns element with validated plugin ids.
*/
public static function validatePluginIds(array &$element, FormStateInterface $form_state, array &$complete_form) {
$value = array_filter($element['#value']);
if (array_keys($element['#options']) == array_keys($value)) {
$form_state->setValueForElement($element, []);