Commit f3f12e54 authored by webchick's avatar webchick

Issue #2446511 by amateescu, Wim Leers, yched, Bojhan: Add a "preconfigured...

Issue #2446511 by amateescu, Wim Leers, yched, Bojhan: Add a "preconfigured field options" concept in Field UI
parent 03d3c27c
......@@ -202,6 +202,16 @@ class EntityType implements EntityTypeInterface {
*/
protected $field_ui_base_route;
/**
* Indicates whether this entity type is commonly used as a reference target.
*
* This is used by the Entity reference field to promote an entity type in the
* add new field select list in Field UI.
*
* @var bool
*/
protected $common_reference_target = FALSE;
/**
* The list cache contexts for this entity type.
*
......@@ -724,4 +734,11 @@ public function getConfigDependencyKey() {
return 'content';
}
/**
* {@inheritdoc}
*/
public function isCommonReferenceTarget() {
return $this->common_reference_target;
}
}
......@@ -685,4 +685,12 @@ public function getListCacheTags();
*/
public function getConfigDependencyKey();
/**
* Indicates whether this entity type is commonly used as a reference target.
*
* @return bool
* TRUE if the entity type is a common reference; FALSE otherwise.
*/
public function isCommonReferenceTarget();
}
......@@ -131,9 +131,28 @@ public function getDefaultFieldSettings($type) {
*/
public function getUiDefinitions() {
$definitions = $this->getDefinitions();
return array_filter($definitions, function ($definition) {
// Filter out definitions that can not be configured in Field UI.
$definitions = array_filter($definitions, function ($definition) {
return empty($definition['no_ui']);
});
// Add preconfigured definitions.
foreach ($definitions as $id => $definition) {
if (is_subclass_of($definition['class'], '\Drupal\Core\Field\PreconfiguredFieldUiOptionsInterface')) {
foreach ($definition['class']::getPreconfiguredOptions() as $key => $option) {
$definitions['field_ui:' . $id . ':' . $key] = [
'label' => $option['label'],
] + $definition;
if (isset($option['category'])) {
$definitions['field_ui:' . $id . ':' . $key]['category'] = $option['category'];
}
}
}
}
return $definitions;
}
/**
......
<?php
/**
* @file
* Contains \Drupal\Core\Field\PreconfiguredFieldUiOptionsInterface.
*/
namespace Drupal\Core\Field;
/**
* Defines an interface for exposing "preconfigured" field definitions.
*
* These field definitions will be exposed as additional options in the 'Add
* field' form in Field UI, together with individual field types.
*
* @see \Drupal\Core\Field\FieldTypePluginManager::getUiDefinitions()
* @see \Drupal\field_ui\Form\FieldStorageAddForm::submitForm()
*/
interface PreconfiguredFieldUiOptionsInterface {
/**
* Returns preconfigured field options for a field type.
*
* @return mixed[][]
* A multi-dimensional array with string keys and the following structure:
* - label: The label to show in the field type selection list.
* - category: (optional) The category in which to put the field label.
* Defaults to the category of the field type.
* - field_storage_config: An array with the following supported keys:
* - cardinality: The field cardinality.
* - settings: Field-type specific storage settings.
* - field_config: An array with the following supported keys:
* - required: Indicates whether the field is required.
* - settings: Field-type specific settings.
* - entity_form_display: An array with the following supported keys:
* - type: The widget to be used in the 'default' form mode.
* - entity_view_display: An array with the following supported keys:
* - type: The formatter to be used in the 'default' view mode.
*
* @see \Drupal\field\Entity\FieldStorageConfig
* @see \Drupal\field\Entity\FieldConfig
* @see \Drupal\Core\Entity\Display\EntityDisplayInterface::setComponent()
*/
public static function getPreconfiguredOptions();
}
......@@ -114,6 +114,16 @@ function entity_reference_field_config_presave(FieldConfigInterface $field) {
$field->settings['handler'] = $selection_manager->getPluginId($target_type, $current_handler);
}
/**
* Implements hook_form_FORM_ID_alter() for 'field_ui_field_storage_add_form'.
*/
function entity_reference_form_field_ui_field_storage_add_form_alter(array &$form) {
// Move the "Entity reference" option to the end of the list and rename it to
// "Other".
unset($form['add']['new_storage_type']['#options'][t('Reference')]['entity_reference']);
$form['add']['new_storage_type']['#options'][t('Reference')]['entity_reference'] = t('Other…');
}
/**
* Render API callback: Processes the field settings form and allows access to
* the form state.
......
......@@ -8,8 +8,10 @@
namespace Drupal\entity_reference;
use Drupal\Component\Utility\String;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
use Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem;
use Drupal\Core\Field\PreconfiguredFieldUiOptionsInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Form\OptGroup;
use Drupal\Core\Session\AccountInterface;
......@@ -29,7 +31,7 @@
*
* @see entity_reference_field_info_alter().
*/
class ConfigurableEntityReferenceItem extends EntityReferenceItem implements OptionsProviderInterface {
class ConfigurableEntityReferenceItem extends EntityReferenceItem implements OptionsProviderInterface, PreconfiguredFieldUiOptionsInterface {
/**
* {@inheritdoc}
......@@ -207,4 +209,32 @@ public static function fieldSettingsFormValidate(array $form, FormStateInterface
}
}
/**
* {@inheritdoc}
*/
public static function getPreconfiguredOptions() {
$options = array();
// Add all the commonly referenced entity types as distinct pre-configured
// options.
$entity_types = \Drupal::entityManager()->getDefinitions();
$common_references = array_filter($entity_types, function (EntityTypeInterface $entity_type) {
return $entity_type->isCommonReferenceTarget();
});
/** @var \Drupal\Core\Entity\EntityTypeInterface $entity_type */
foreach ($common_references as $entity_type) {
$options[$entity_type->id()] = [
'label' => $entity_type->getLabel(),
'field_storage_config' => [
'settings' => [
'target_type' => $entity_type->id(),
]
]
];
}
return $options;
}
}
......@@ -62,7 +62,13 @@ public function testFieldAdminHandler() {
$bundle_path = 'admin/structure/types/manage/' . $this->type;
// First step: 'Add new field' on the 'Manage fields' page.
$this->drupalPostForm($bundle_path . '/fields/add-field', array(
$this->drupalGet($bundle_path . '/fields/add-field');
// Check if the commonly referenced entity types appear in the list.
$this->assertOption('edit-new-storage-type', 'field_ui:entity_reference:node');
$this->assertOption('edit-new-storage-type', 'field_ui:entity_reference:user');
$this->drupalPostForm(NULL, array(
'label' => 'Test label',
'field_name' => 'test',
'new_storage_type' => 'entity_reference',
......
......@@ -74,6 +74,10 @@ field.storage_settings.hidden_test_field:
type: field.storage_settings.test_field
label: 'Hidden test field storage settings'
field.storage_settings.test_field_with_preconfigured_options:
type: field.storage_settings.test_field
label: 'Test field with preconfigured options storage settings'
field.field_settings.test_field:
type: mapping
label: 'Test field field settings'
......@@ -93,6 +97,10 @@ field.field_settings.hidden_test_field:
type: field.field_settings.test_field
label: 'Hidden test field field settings'
field.field_settings.test_field_with_preconfigured_options:
type: field.field_settings.test_field
label: 'Test field with preconfigured settings'
field.value.test_field:
type: mapping
label: 'Default value'
......
......@@ -18,6 +18,7 @@
*/
function field_test_field_widget_info_alter(&$info) {
$info['test_field_widget_multiple']['field_types'][] = 'test_field';
$info['test_field_widget_multiple']['field_types'][] = 'test_field_with_preconfigured_options';
}
/**
......
......@@ -19,7 +19,8 @@
* label = @Translation("Default"),
* description = @Translation("Default formatter"),
* field_types = {
* "test_field"
* "test_field",
* "test_field_with_preconfigured_options"
* },
* weight = 1
* )
......
......@@ -19,7 +19,8 @@
* label = @Translation("Multiple"),
* description = @Translation("Multiple formatter"),
* field_types = {
* "test_field"
* "test_field",
* "test_field_with_preconfigured_options"
* },
* weight = 5
* )
......
<?php
/**
* @file
* Contains \Drupal\field_test\Plugin\Field\FieldType\TestItemWithPreconfiguredOptions.
*/
namespace Drupal\field_test\Plugin\Field\FieldType;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
use Drupal\Core\Field\PreconfiguredFieldUiOptionsInterface;
/**
* Defines the 'test_field_with_preconfigured_options' entity field item.
*
* @FieldType(
* id = "test_field_with_preconfigured_options",
* label = @Translation("Test field with preconfigured options"),
* description = @Translation("Dummy field type used for tests."),
* default_widget = "test_field_widget",
* default_formatter = "field_test_default"
* )
*/
class TestItemWithPreconfiguredOptions extends TestItem implements PreconfiguredFieldUiOptionsInterface {
/**
* {inheritdoc}
*/
public static function getPreconfiguredOptions() {
return [
'custom_options' => [
'label' => t('All custom options'),
'category' => t('Custom category'),
'field_storage_config' => [
'cardinality' => FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED,
'settings' => [
'test_field_storage_setting' => 'preconfigured_storage_setting'
],
],
'field_config' => [
'required' => TRUE,
'settings' => [
'test_field_setting' => 'preconfigured_field_setting',
],
],
'entity_form_display' => [
'type' => 'test_field_widget_multiple',
],
'entity_view_display' => [
'type' => 'field_test_multiple',
],
],
];
}
}
......@@ -19,8 +19,9 @@
* id = "test_field_widget",
* label = @Translation("Test widget"),
* field_types = {
* "test_field",
* "hidden_test_field"
* "test_field",
* "hidden_test_field",
* "test_field_with_preconfigured_options"
* },
* weight = -10
* )
......
......@@ -312,26 +312,60 @@ public function submitForm(array &$form, FormStateInterface $form_state) {
// Create new field.
if ($values['new_storage_type']) {
$field_storage_values = [
'field_name' => $values['field_name'],
'entity_type' => $this->entityTypeId,
'type' => $values['new_storage_type'],
'translatable' => $values['translatable'],
];
$field_values = [
'field_name' => $values['field_name'],
'entity_type' => $this->entityTypeId,
'bundle' => $this->bundle,
'label' => $values['label'],
// Field translatability should be explicitly enabled by the users.
'translatable' => FALSE,
];
$widget_id = $formatter_id = NULL;
// Check if we're dealing with a preconfigured field.
if (strpos($field_storage_values['type'], 'field_ui:') !== FALSE) {
list(, $field_type, $option_key) = explode(':', $field_storage_values['type'], 3);
$field_storage_values['type'] = $field_type;
$field_type_class = $this->fieldTypePluginManager->getDefinition($field_type)['class'];
$field_options = $field_type_class::getPreconfiguredOptions()[$option_key];
// Merge in preconfigured field storage options.
if (isset($field_options['field_storage_config'])) {
foreach (array('cardinality', 'settings') as $key) {
if (isset($field_options['field_storage_config'][$key])) {
$field_storage_values[$key] = $field_options['field_storage_config'][$key];
}
}
}
// Merge in preconfigured field options.
if (isset($field_options['field_config'])) {
foreach (array('required', 'settings') as $key) {
if (isset($field_options['field_config'][$key])) {
$field_values[$key] = $field_options['field_config'][$key];
}
}
}
$widget_id = isset($field_options['entity_form_display']['type']) ? $field_options['entity_form_display']['type'] : NULL;
$formatter_id = isset($field_options['entity_view_display']['type']) ? $field_options['entity_view_display']['type'] : NULL;
}
// Create the field storage and field.
try {
$this->entityManager->getStorage('field_storage_config')->create(array(
'field_name' => $values['field_name'],
'entity_type' => $this->entityTypeId,
'type' => $values['new_storage_type'],
'translatable' => $values['translatable'],
))->save();
$field = $this->entityManager->getStorage('field_config')->create(array(
'field_name' => $values['field_name'],
'entity_type' => $this->entityTypeId,
'bundle' => $this->bundle,
'label' => $values['label'],
// Field translatability should be explicitly enabled by the users.
'translatable' => FALSE,
));
$this->entityManager->getStorage('field_storage_config')->create($field_storage_values)->save();
$field = $this->entityManager->getStorage('field_config')->create($field_values);
$field->save();
$this->configureEntityDisplays($values['field_name']);
$this->configureEntityFormDisplay($values['field_name'], $widget_id);
$this->configureEntityViewDisplay($values['field_name'], $formatter_id);
// Always show the field settings step, as the cardinality needs to be
// configured for new fields.
......@@ -364,7 +398,8 @@ public function submitForm(array &$form, FormStateInterface $form_state) {
));
$field->save();
$this->configureEntityDisplays($field_name);
$this->configureEntityFormDisplay($field_name);
$this->configureEntityViewDisplay($field_name);
$route_parameters = array(
'field_config' => $field->id(),
......@@ -396,20 +431,34 @@ public function submitForm(array &$form, FormStateInterface $form_state) {
*
* @param string $field_name
* The field name.
* @param string|null $widget_id
* (optional) The plugin ID of the widget. Defaults to NULL.
*/
protected function configureEntityDisplays($field_name) {
protected function configureEntityFormDisplay($field_name, $widget_id = NULL) {
// Make sure the field is displayed in the 'default' form mode (using
// default widget and settings). It stays hidden for other form modes
// until it is explicitly configured.
$options = $widget_id ? ['type' => $widget_id] : [];
entity_get_form_display($this->entityTypeId, $this->bundle, 'default')
->setComponent($field_name)
->setComponent($field_name, $options)
->save();
}
/**
* Configures the newly created field for the default view and form modes.
*
* @param string $field_name
* The field name.
* @param string|null $formatter_id
* (optional) The plugin ID of the formatter. Defaults to NULL.
*/
protected function configureEntityViewDisplay($field_name, $formatter_id = NULL) {
// Make sure the field is displayed in the 'default' view mode (using
// default formatter and settings). It stays hidden for other view
// modes until it is explicitly configured.
$options = $formatter_id ? ['type' => $formatter_id] : [];
entity_get_display($this->entityTypeId, $this->bundle, 'default')
->setComponent($field_name)
->setComponent($field_name, $options)
->save();
}
......
......@@ -31,9 +31,9 @@ class ManageFieldsTest extends WebTestBase {
public static $modules = array('node', 'field_ui', 'field_test', 'taxonomy', 'image', 'block');
/**
* A custom content type created for testing.
* The ID of the custom content type created for testing.
*
* @var \Drupal\node\Entity\NodeType
* @var string
*/
protected $contentType;
......@@ -681,4 +681,34 @@ function fieldListAdminPage() {
$this->assertText($this->fieldName, 'Field name is displayed in field list.');
$this->assertTrue($this->assertLinkByHref('admin/structure/types/manage/' . $this->contentType . '/fields'), 'Link to content type using field is displayed in field list.');
}
/**
* Tests the "preconfigured field" functionality.
*
* @see \Drupal\Core\Field\PreconfiguredFieldUiOptionsInterface
*/
public function testPreconfiguredFields() {
$this->drupalGet('admin/structure/types/manage/article/fields/add-field');
// Check that the preconfigured field option exist alongside the regular
// field type option.
$this->assertOption('edit-new-storage-type', 'field_ui:test_field_with_preconfigured_options:custom_options');
$this->assertOption('edit-new-storage-type', 'test_field_with_preconfigured_options');
// Add a field with every possible preconfigured value.
$this->fieldUIAddNewField(NULL, 'test_custom_options', 'Test label', 'field_ui:test_field_with_preconfigured_options:custom_options');
$field_storage = FieldStorageConfig::loadByName('node', 'field_test_custom_options');
$this->assertEqual($field_storage->getCardinality(), FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED);
$this->assertEqual($field_storage->getSetting('test_field_storage_setting'), 'preconfigured_storage_setting');
$field = FieldConfig::loadByName('node', 'article', 'field_test_custom_options');
$this->assertTrue($field->isRequired());
$this->assertEqual($field->getSetting('test_field_setting'), 'preconfigured_field_setting');
$form_display = entity_get_form_display('node', 'article', 'default');
$this->assertEqual($form_display->getComponent('field_test_custom_options')['type'], 'test_field_widget_multiple');
$view_display = entity_get_display('node', 'article', 'default');
$this->assertEqual($view_display->getComponent('field_test_custom_options')['type'], 'field_test_multiple');
}
}
......@@ -56,6 +56,7 @@
* },
* bundle_entity_type = "node_type",
* field_ui_base_route = "entity.node_type.edit_form",
* common_reference_target = TRUE,
* permission_granularity = "bundle",
* links = {
* "canonical" = "/node/{node}",
......
......@@ -20,7 +20,7 @@
*
* @FieldType(
* id = "taxonomy_term_reference",
* label = @Translation("Term Reference"),
* label = @Translation("Taxonomy term"),
* description = @Translation("This field stores a reference to a taxonomy term."),
* category = @Translation("Reference"),
* default_widget = "options_select",
......
......@@ -62,20 +62,20 @@ function taxonomy_help($route_name, RouteMatchInterface $route_match) {
$output .= '<dt>' . t('Managing terms') . '</dt>';
$output .= '<dd>' . t('Users who have the <em>Administer vocabularies and terms</em> permission or the <em>Edit terms</em> permission for a particular vocabulary can add, edit, and organize the terms in a vocabulary from a vocabulary\'s term listing page, which can be accessed by going to the <a href="!taxonomy_admin">Taxonomy administration page</a> and clicking <em>List terms</em> in the <em>Operations</em> column. Users must have the <em>Administer vocabularies and terms</em> permission or the <em>Delete terms</em> permission for a particular vocabulary to delete terms.' , array('!taxonomy_admin' => \Drupal::url('entity.taxonomy_vocabulary.collection'))) . ' </dd>';
$output .= '<dt>' . t('Classifying entity content') . '</dt>';
$output .= '<dd>' . t('A user with the <em>Administer fields</em> permission for a certain entity type may add reference fields to the entity, which will allow entities to be classified using taxonomy terms. See the <a href="!field">Field module help</a> and the <a href="!field_ui">Field UI help</a> pages for general information on fields and how to create and manage them. The reference field can either be a <em>Term reference</em> or an <em>Entity reference</em> field:' , array('!field_ui' => $field_ui_url, '!field' => \Drupal::url('help.page', array('name' => 'field'))));
$output .= '<dd>' . t('A user with the <em>Administer fields</em> permission for a certain entity type may add reference fields to the entity, which will allow entities to be classified using taxonomy terms. See the <a href="!field">Field module help</a> and the <a href="!field_ui">Field UI help</a> pages for general information on fields and how to create and manage them. The reference field can either be a <em>Taxonomy Term reference</em> or an <em>Entity reference</em> field:' , array('!field_ui' => $field_ui_url, '!field' => \Drupal::url('help.page', array('name' => 'field'))));
$output .= '<ul>';
$output .= '<li>' . t('A <em>Term Reference</em> field corresponds to <em>one</em> vocabulary.') . '</li>';
$output .= '<li>' . t('An <em>Entity Reference</em> field can reference <em>several</em> vocabularies at the same time, and it has more configuration options than a Term Reference field.') . '</li>';
$output .= '<li>' . t('A <em>Taxonomy Term reference</em> field corresponds to <em>one</em> vocabulary.') . '</li>';
$output .= '<li>' . t('An <em>Entity reference</em> field can reference <em>several</em> vocabularies at the same time, and it has more configuration options than a Taxonomy Term Reference field.') . '</li>';
$output .= '</ul>';
$output .= '</dd>';
$output .= '<dt>' . t('Adding new terms during content creation') . '</dt>';
$output .= '<dd>' . t('Allowing users to add new terms gradually builds a vocabulary as content is added and edited. Users can add new terms if the <em>Autocomplete term widget (tagging)</em> is chosen for a <em>Term reference</em> field, or if either of the two <em>Autocomplete</em> widgets is chosen for an <em>Entity reference</em> field. In this case you need to enable the <em>Create referenced entity</em> option, and restrict the field to one vocabulary.') . '</dd>';
$output .= '<dd>' . t('Allowing users to add new terms gradually builds a vocabulary as content is added and edited. Users can add new terms if the <em>Autocomplete term widget (tagging)</em> is chosen for a <em>Taxonomy Term reference</em> field, or if either of the two <em>Autocomplete</em> widgets is chosen for an <em>Entity reference</em> field. In this case you need to enable the <em>Create referenced entity</em> option, and restrict the field to one vocabulary.') . '</dd>';
$output .= '<dt>' . t('Configuring displays and form displays ') . '</dt>';
$output .= '<dd>' . t('The reference fields have several formatters and widgets available on the Manage display and Manage form display pages, respectively:');
$output .= '<ul>';
$output .= '<li>' . t('The <em>Check boxes/radio buttons</em> widget displays the existing terms of the vocabulary as check boxes or radio buttons based on the <em>Allowed number of values</em> set for the field.') . '</li>';
$output .= '<li>' . t('The <em>Select list</em> widget displays the existing terms in a drop-down list or scrolling list box based on the <em>Allowed number of values</em> set for the field.') . '</li>';
$output .= '<li>' . t('The <em>Autocomplete term widget (tagging)</em> widget of a <em>Term reference</em> field displays a text field in which users can type a comma-separated list of terms.') . '</li>';
$output .= '<li>' . t('The <em>Autocomplete term widget (tagging)</em> widget of a <em>Taxonomy Term reference</em> field displays a text field in which users can type a comma-separated list of terms.') . '</li>';
$output .= '<li>' . t('The <em>Autocomplete</em> widget of an <em>Entity Reference</em> field displays text fields in which users can type terms based on the <em>Allowed number of values</em>. The display of matching terms can include all terms that contain the typed characters or restricted to those starting with them.') . '</li>';
$output .= '<li>' . t('The <em>Autocomplete (Tags style)</em> widget of an <em>Entity Reference</em> field displays a multi-text field in which users can type in a comma-separated list of terms.') . '</li>';
$output .= '<li>' . t('The <em>Plain text</em> formatter displays only the name of the term.') . '</li>';
......
......@@ -57,7 +57,8 @@
* "cancel-form" = "/user/{user}/cancel",
* "collection" = "/admin/people",
* },
* field_ui_base_route = "entity.user.admin_form"
* field_ui_base_route = "entity.user.admin_form",
* common_reference_target = TRUE
* )
*/
class User extends ContentEntityBase implements UserInterface {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment