Commit c02f12b7 authored by webchick's avatar webchick

Issue #1963340 by amateescu, dags, andypost, agentrickard, mgifford, yoroy,...

Issue #1963340 by amateescu, dags, andypost, agentrickard, mgifford, yoroy, pguillard, jibran, YesCT, xjm, LewisNyman, swentel, Hydra, yched, tim.plunkett, rteijeiro, ainz, Xano, Bojhan, Berdir: Change field UI so that adding a field is a separate task
parent 7abdb654
......@@ -311,17 +311,16 @@ function comment_view_multiple($comments, $view_mode = 'full', $langcode = NULL)
}
/**
* Implements hook_form_FORM_ID_alter() for field_ui_field_overview_form.
* Implements hook_form_FORM_ID_alter() for field_ui_field_storage_add_form.
*/
function comment_form_field_ui_field_overview_form_alter(&$form, FormStateInterface $form_state) {
function comment_form_field_ui_field_storage_add_form_alter(&$form, FormStateInterface $form_state) {
$request = \Drupal::request();
if ($form['#entity_type'] == 'comment' && $request->attributes->has('commented_entity_type')) {
if ($form_state->get('entity_type_id') == 'comment' && $request->attributes->has('commented_entity_type')) {
$form['#title'] = \Drupal::service('comment.manager')->getFieldUIPageTitle($request->attributes->get('commented_entity_type'), $request->attributes->get('field_name'));
}
$entity_type_id = $form['#entity_type'];
if (!_comment_entity_uses_integer_id($entity_type_id)) {
if (!_comment_entity_uses_integer_id($form_state->get('entity_type_id'))) {
// You cannot use comment fields on entity types with non-integer IDs.
unset($form['fields']['_add_new_field']['type']['#options']['comment']);
unset($form['add']['new_storage_type']['#options']['comment']);
}
}
......
......@@ -408,24 +408,24 @@ public function testsNonIntegerIdEntities() {
'administer entity_test_string_id fields',
));
$this->drupalLogin($limited_user);
// Visit the Field UI overview.
$this->drupalGet('entity_test_string_id/structure/entity_test/fields');
// Visit the Field UI field add page.
$this->drupalGet('entity_test_string_id/structure/entity_test/fields/add-field');
// Ensure field isn't shown for string IDs.
$this->assertNoOption('edit-fields-add-new-field-type', 'comment');
$this->assertNoOption('edit-new-storage-type', 'comment');
// Ensure a core field type shown.
$this->assertOption('edit-fields-add-new-field-type', 'boolean');
$this->assertOption('edit-new-storage-type', 'boolean');
// Create a bundle for entity_test_no_id.
entity_test_create_bundle('entity_test', 'Entity Test', 'entity_test_no_id');
$this->drupalLogin($this->drupalCreateUser(array(
'administer entity_test_no_id fields',
)));
// Visit the Field UI overview.
$this->drupalGet('entity_test_no_id/structure/entity_test/fields');
// Visit the Field UI field add page.
$this->drupalGet('entity_test_no_id/structure/entity_test/fields/add-field');
// Ensure field isn't shown for empty IDs.
$this->assertNoOption('edit-fields-add-new-field-type', 'comment');
$this->assertNoOption('edit-new-storage-type', 'comment');
// Ensure a core field type shown.
$this->assertOption('edit-fields-add-new-field-type', 'boolean');
$this->assertOption('edit-new-storage-type', 'boolean');
}
}
......@@ -247,6 +247,8 @@ function testSiteWideContact() {
$this->clickLink(t('Manage fields'), $i);
$this->assertResponse(200);
$this->clickLink(t('Add field'));
$this->assertResponse(200);
// Create a simple textfield.
$field_name = Unicode::strtolower($this->randomMachineName());
......
......@@ -7,6 +7,7 @@
namespace Drupal\entity_reference\Tests;
use Drupal\field_ui\Tests\FieldUiTestTrait;
use Drupal\simpletest\WebTestBase;
use Drupal\taxonomy\Entity\Vocabulary;
......@@ -17,6 +18,8 @@
*/
class EntityReferenceAdminTest extends WebTestBase {
use FieldUiTestTrait;
/**
* Modules to install.
*
......@@ -55,11 +58,11 @@ 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', array(
'fields[_add_new_field][label]' => 'Test label',
'fields[_add_new_field][field_name]' => 'test',
'fields[_add_new_field][type]' => 'entity_reference',
), t('Save'));
$this->drupalPostForm($bundle_path . '/fields/add-field', array(
'label' => 'Test label',
'field_name' => 'test',
'new_storage_type' => 'entity_reference',
), t('Save and continue'));
// Node should be selected by default.
$this->assertFieldByName('field_storage[settings][target_type]', 'node');
......@@ -198,24 +201,13 @@ public function createEntityReferenceField($target_type, $bundle = NULL) {
// Generate a random field name, must be only lowercase characters.
$field_name = strtolower($this->randomMachineName());
// Create the initial entity reference.
$this->drupalPostForm($bundle_path . '/fields', array(
'fields[_add_new_field][label]' => $this->randomMachineName(),
'fields[_add_new_field][field_name]' => $field_name,
'fields[_add_new_field][type]' => 'entity_reference',
), t('Save'));
// Select the correct target type given in the parameters and save field settings.
$this->drupalPostForm(NULL, array('field_storage[settings][target_type]' => $target_type), t('Save field settings'));
// Select required fields if there are any.
$edit = array();
if($bundle) {
$edit['field[settings][handler_settings][target_bundles][' . $bundle . ']'] = TRUE;
$storage_edit = $field_edit = array();
$storage_edit['field_storage[settings][target_type]'] = $target_type;
if ($bundle) {
$field_edit['field[settings][handler_settings][target_bundles][' . $bundle . ']'] = TRUE;
}
// Save settings.
$this->drupalPostForm(NULL, $edit, t('Save settings'));
$this->fieldUIAddNewField($bundle_path, $field_name, NULL, 'entity_reference', $storage_edit, $field_edit);
// Returns the generated field name.
return $field_name;
......
......@@ -3,37 +3,28 @@
* Stylesheet for the Field UI module.
*/
/* 'Manage fields' and 'Manage display' overviews */
.field-ui-overview .add-new .label-input {
float: left; /* LTR */
/* Add new field page. */
.field-ui-field-storage-add-form .field-type-wrapper .form-item {
float: left;
margin-right: 1em;
vertical-align: text-bottom;
}
[dir="rtl"] .field-ui-overview .add-new .label-input {
[dir="rtl"] .field-ui-field-storage-add-form .field-type-wrapper .form-item {
float: right;
margin-left: 1em;
margin-right: 0;
}
.field-ui-overview .add-new .description {
margin-bottom: 0;
max-width: 250px;
}
.field-ui-overview .add-new .form-type-machine-name .description {
white-space: normal;
}
.field-ui-overview .add-new .add-new-placeholder {
font-weight: bold;
padding-bottom: .5em;
.field-ui-field-storage-add-form .field-type-wrapper .form-item-separator {
margin-top: 2.3em;
}
/* 'Manage fields' and 'Manage display' overviews */
.field-ui-overview .region-title td {
font-weight: bold;
}
.field-ui-overview .region-message td {
font-style: italic;
}
.field-ui-overview .region-add-new-title {
display: none;
}
.field-ui-overview .add-new td {
vertical-align: top;
white-space: nowrap;
}
/* 'Manage form display' and 'Manage display' overview */
.field-ui-overview .field-plugin-summary-cell {
......
......@@ -3,10 +3,53 @@
* Attaches the behaviors for the Field UI module.
*/
(function ($) {
(function ($, Drupal, drupalSettings) {
"use strict";
Drupal.behaviors.fieldUIFieldStorageAddForm = {
attach: function (context) {
var $form = $(context).find('#field-ui-field-storage-add-form').once('field_ui_add');
if ($form.length) {
// Add a few 'form-required' css classes here. We can not use the Form API
// '#required' property because both label elements for "add new" and
// "re-use existing" can never be filled and submitted at the same time.
// The actual validation will happen server-side.
$form.find(
'.form-item-label label,' +
'.form-item-field-name label,' +
'.form-item-existing-storage-label label')
.addClass('form-required');
var $newFieldType = $form.find('select[name="new_storage_type"]');
var $existingStorageName = $form.find('select[name="existing_storage_name"]');
// When the user selects a new field type, clear the "existing field"
// selection.
$newFieldType.change(function () {
if ($(this).val() != '') {
// Reset the "existing storage name" selection.
$existingStorageName.val('').change();
}
});
// When the user selects an existing storage name, clear the "new field
// type" selection and populate the 'existing_storage_label' element.
$existingStorageName.change(function () {
if ($(this).val() != '') {
// Reset the "new field type" selection.
$newFieldType.val('').change();
// Pre-populate the "existing storage label" element.
if (drupalSettings.existingFieldLabels[$(this).val()] !== undefined) {
$(context).find('input[name="existing_storage_label"]').val(drupalSettings.existingFieldLabels[$(this).val()]);
}
}
});
}
}
};
Drupal.behaviors.fieldUIDisplayOverview = {
attach: function (context, settings) {
$(context).find('table#field-display-overview').once('field-display-overview', function () {
......@@ -249,4 +292,4 @@
}
};
})(jQuery);
})(jQuery, Drupal, drupalSettings);
......@@ -11,3 +11,7 @@ field_ui.entity_form_mode_add:
weight: 1
appears_on:
- field_ui.entity_form_mode_list
field_ui.field_storage_config_add:
class: \Drupal\Core\Menu\LocalActionDefault
deriver: \Drupal\field_ui\Plugin\Derivative\FieldUiLocalAction
<?php
/**
* @file
* Contains \Drupal\field_ui\Controller\FieldConfigListController.
*/
namespace Drupal\field_ui\Controller;
use Drupal\Core\Entity\Controller\EntityListController;
use Symfony\Component\HttpFoundation\Request;
/**
* Defines a controller to list field instances.
*/
class FieldConfigListController extends EntityListController {
/**
* Shows the 'Manage fields' page.
*
* @param string $entity_type_id
* The entity type.
* @param string $bundle
* The entity bundle.
* @param \Symfony\Component\HttpFoundation\Request $request
* The current request.
*
* @return array
* A render array as expected by drupal_render().
*/
public function listing($entity_type_id = NULL, $bundle = NULL, Request $request = NULL) {
if (!$bundle) {
$entity_info = $this->entityManager()->getDefinition($entity_type_id);
$bundle = $request->attributes->get('_raw_variables')->get($entity_info->getBundleEntityType());
}
return $this->entityManager()->getListBuilder('field_config')->render($entity_type_id, $bundle, $request);
}
}
......@@ -204,7 +204,7 @@ protected function getTableHeader() {
*/
protected function getOverviewRoute($mode) {
return Url::fromRoute('field_ui.display_overview_view_mode_' . $this->entity_type, [
$this->bundleEntityType => $this->bundle,
$this->bundleEntityTypeId => $this->bundle,
'view_mode_name' => $mode,
]);
}
......
......@@ -9,6 +9,7 @@
use Drupal\Component\Plugin\Factory\DefaultFactory;
use Drupal\Component\Plugin\PluginManagerBase;
use Drupal\Component\Utility\Html;
use Drupal\Component\Utility\String;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Entity\Display\EntityDisplayInterface;
......@@ -16,13 +17,51 @@
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\Field\FieldTypePluginManagerInterface;
use Drupal\Core\Field\PluginSettingsInterface;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Render\Element;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Field UI display overview base class.
*/
abstract class DisplayOverviewBase extends OverviewBase {
abstract class DisplayOverviewBase extends FormBase {
/**
* The name of the entity type.
*
* @var string
*/
protected $entity_type = '';
/**
* The entity bundle.
*
* @var string
*/
protected $bundle = '';
/**
* The name of the entity type which provides bundles for the entity type
* defined above.
*
* @var string
*/
protected $bundleEntityTypeId;
/**
* The entity view or form mode.
*
* @var string
*/
protected $mode = '';
/**
* The entity manager.
*
* @var \Drupal\Core\Entity\EntityManagerInterface
*/
protected $entityManager;
/**
* The display context. Either 'view' or 'form'.
......@@ -65,8 +104,7 @@ abstract class DisplayOverviewBase extends OverviewBase {
* The configuration factory.
*/
public function __construct(EntityManagerInterface $entity_manager, FieldTypePluginManagerInterface $field_type_manager, PluginManagerBase $plugin_manager, ConfigFactoryInterface $config_factory) {
parent::__construct($entity_manager);
$this->entityManager = $entity_manager;
$this->fieldTypes = $field_type_manager->getDefinitions();
$this->pluginManager = $plugin_manager;
$this->configFactory = $config_factory;
......@@ -85,7 +123,23 @@ public static function create(ContainerInterface $container) {
}
/**
* {@inheritdoc}
* Get the regions needed to create the overview form.
*
* @return array
* Example usage:
* @code
* return array(
* 'content' => array(
* // label for the region.
* 'title' => $this->t('Content'),
* // Indicates if the region is visible in the UI.
* 'invisible' => TRUE,
* // A message to indicate that there is nothing to be displayed in
* // the region.
* 'message' => $this->t('No field is displayed.'),
* ),
* );
* @endcode
*/
public function getRegions() {
return array(
......@@ -101,6 +155,20 @@ public function getRegions() {
);
}
/**
* Returns an associative array of all regions.
*
* @return array
* An array containing the region options.
*/
public function getRegionOptions() {
$options = array();
foreach ($this->getRegions() as $region => $data) {
$options[$region] = $data['title'];
}
return $options;
}
/**
* Collects the definitions of fields whose display is configurable.
*
......@@ -118,8 +186,16 @@ protected function getFieldDefinitions() {
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state, $entity_type_id = NULL, $bundle = NULL, $mode = 'default') {
parent::buildForm($form, $form_state, $entity_type_id, $bundle);
$entity_type = $this->entityManager->getDefinition($entity_type_id);
$this->bundleEntityTypeId = $entity_type->getBundleEntityType();
if (!$form_state->get('bundle')) {
$bundle = $bundle ?: $this->getRequest()->attributes->get('_raw_variables')->get($this->bundleEntityTypeId);
$form_state->set('bundle', $bundle);
}
$this->entity_type = $entity_type_id;
$this->bundle = $form_state->get('bundle');
$this->mode = $mode;
$field_definitions = $this->getFieldDefinitions();
......@@ -689,6 +765,120 @@ public function multistepAjax($form, FormStateInterface $form_state) {
return $form['fields'];
}
/**
* Performs pre-render tasks on field_ui_table elements.
*
* This function is assigned as a #pre_render callback in
* field_ui_element_info().
*
* @param array $elements
* A structured array containing two sub-levels of elements. Properties
* used:
* - #tabledrag: The value is a list of $options arrays that are passed to
* drupal_attach_tabledrag(). The HTML ID of the table is added to each
* $options array.
*
* @see drupal_render()
* @see \Drupal\Core\Render\Element\Table::preRenderTable()
*/
public function tablePreRender($elements) {
$js_settings = array();
// For each region, build the tree structure from the weight and parenting
// data contained in the flat form structure, to determine row order and
// indentation.
$regions = $elements['#regions'];
$tree = array('' => array('name' => '', 'children' => array()));
$trees = array_fill_keys(array_keys($regions), $tree);
$parents = array();
$children = Element::children($elements);
$list = array_combine($children, $children);
// Iterate on rows until we can build a known tree path for all of them.
while ($list) {
foreach ($list as $name) {
$row = &$elements[$name];
$parent = $row['parent_wrapper']['parent']['#value'];
// Proceed if parent is known.
if (empty($parent) || isset($parents[$parent])) {
// Grab parent, and remove the row from the next iteration.
$parents[$name] = $parent ? array_merge($parents[$parent], array($parent)) : array();
unset($list[$name]);
// Determine the region for the row.
$region_name = call_user_func($row['#region_callback'], $row);
// Add the element in the tree.
$target = &$trees[$region_name][''];
foreach ($parents[$name] as $key) {
$target = &$target['children'][$key];
}
$target['children'][$name] = array('name' => $name, 'weight' => $row['weight']['#value']);
// Add tabledrag indentation to the first row cell.
if ($depth = count($parents[$name])) {
$children = Element::children($row);
$cell = current($children);
$indentation = array(
'#theme' => 'indentation',
'#size' => $depth,
);
$row[$cell]['#prefix'] = drupal_render($indentation) . (isset($row[$cell]['#prefix']) ? $row[$cell]['#prefix'] : '');
}
// Add row id and associate JS settings.
$id = Html::getClass($name);
$row['#attributes']['id'] = $id;
if (isset($row['#js_settings'])) {
$row['#js_settings'] += array(
'rowHandler' => $row['#row_type'],
'name' => $name,
'region' => $region_name,
);
$js_settings[$id] = $row['#js_settings'];
}
}
}
}
// Determine rendering order from the tree structure.
foreach ($regions as $region_name => $region) {
$elements['#regions'][$region_name]['rows_order'] = array_reduce($trees[$region_name], array($this, 'reduceOrder'));
}
$elements['#attached']['drupalSettings']['fieldUIRowsData'] = $js_settings;
// If the custom #tabledrag is set and there is a HTML ID, add the table's
// HTML ID to the options and attach the behavior.
// @see \Drupal\Core\Render\Element\Table::preRenderTable()
if (!empty($elements['#tabledrag']) && isset($elements['#attributes']['id'])) {
foreach ($elements['#tabledrag'] as $options) {
$options['table_id'] = $elements['#attributes']['id'];
drupal_attach_tabledrag($elements, $options);
}
}
return $elements;
}
/**
* Determines the rendering order of an array representing a tree.
*
* Callback for array_reduce() within
* \Drupal\field_ui\DisplayOverviewBase::tablePreRender().
*/
public function reduceOrder($array, $a) {
$array = !isset($array) ? array() : $array;
if ($a['name']) {
$array[] = $a['name'];
}
if (!empty($a['children'])) {
uasort($a['children'], array('Drupal\Component\Utility\SortArray', 'sortByWeightElement'));
$array = array_merge($array, array_reduce($a['children'], array($this, 'reduceOrder')));
}
return $array;
}
/**
* Returns the entity display object used by this form.
*
......
......@@ -7,10 +7,15 @@
namespace Drupal\field_ui;
use Drupal\Component\Utility\Html;
use Drupal\Component\Utility\String;
use Drupal\Core\Config\Entity\ConfigEntityListBuilder;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityManagerInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Field\FieldTypePluginManagerInterface;
use Drupal\Core\Url;
use Drupal\field\FieldConfigInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
......@@ -18,6 +23,20 @@
*/
class FieldConfigListBuilder extends ConfigEntityListBuilder {
/**
* The name of the entity type the listed fields are attached to.
*
* @var string
*/
protected $targetEntityTypeId;
/**
* The name of the bundle the listed fields are attached to.
*
* @var string
*/
protected $targetBundle;
/**
* The entity manager.
*
......@@ -25,6 +44,13 @@ class FieldConfigListBuilder extends ConfigEntityListBuilder {
*/
protected $entityManager;
/**
* The field type plugin manager.
*
* @var \Drupal\Core\Field\FieldTypePluginManagerInterface
*/
protected $fieldTypeManager;
/**
* Constructs a new class instance.
*
......@@ -32,27 +58,103 @@ class FieldConfigListBuilder extends ConfigEntityListBuilder {
* The entity type definition.
* @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
* The entity manager.
* @param \Drupal\Core\Field\FieldTypePluginManagerInterface $field_type_manager
* The field type manager
*/
public function __construct(EntityTypeInterface $entity_type, EntityManagerInterface $entity_manager) {
public function __construct(EntityTypeInterface $entity_type, EntityManagerInterface $entity_manager, FieldTypePluginManagerInterface $field_type_manager) {
parent::__construct($entity_type, $entity_manager->getStorage($entity_type->id()));
$this->entityManager = $entity_manager;
$this->fieldTypeManager = $field_type_manager;
}
/**
* {@inheritdoc}
*/
public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) {
return new static($entity_type, $container->get('entity.manager'));
return new static($entity_type, $container->get('entity.manager'), $container->get('plugin.manager.field.field_type'));
}
/**
* {@inheritdoc}
*/
public function render($target_entity_type_id = NULL, $target_bundle = NULL) {
$this->targetEntityTypeId = $target_entity_type_id;
$this->targetBundle = $target_bundle;
$build = parent::render();
$build['#attributes']['id'] = 'field-overview';
$build['#empty'] = $this->t('No fields are present yet.');
return $build;
}
/**
* {@inheritdoc}
*/
public function load() {
$entities = array_filter($this->entityManager->getFieldDefinitions($this->targetEntityTypeId, $this->targetBundle), function ($field_definition) {
return $field_definition instanceof FieldConfigInterface;
});
// Sort the entities using the entity class's sort() method.
// See \Drupal\Core\Config\Entity\ConfigEntityBase::sort().
uasort($entities, array($this->entityType->getClass(), 'sort'));
return $entities;
}
/**
* {@inheritdoc}
*/
public function render() {
// The actual field config overview is rendered by
// \Drupal\field_ui\FieldOverview, so we should not use this class to build
// lists.
throw new \Exception('This class is only used for operations and not for building lists.');
public function buildHeader() {
$header = array(
'label' => $this->t('Label'),
'field_name' => array(
'data' => $this->t('Machine name'),
'class' => array(RESPONSIVE_PRIORITY_MEDIUM),
),
'field_type' => $this->t('Field type'),
);
return $header + parent::buildHeader();
}
/**
* {@inheritdoc}
*/
public function buildRow(EntityInterface $field_config) {
/** @var \Drupal\field\FieldConfigInterface $field_config */
$field_storage = $field_config->getFieldStorageDefinition();
$target_bundle_entity_type_id = $this->entityManager->getDefinition($this->targetEntityTypeId)->getBundleEntityType();
$route_parameters = array(
$target_bundle_entity_type_id => $this->targetBundle,
'field_config' => $field_config->id(),
);
$row = array(
'id' => Html::getClass($field_config->getName()),
'data' => array(
'label' => String::checkPlain($field_config->getLabel()),
'field_name' => $field_config->getName(),
'field_type' => array(
'data' => array(
'#type' => 'link',
'#title' => $this->fieldTypeManager->getDefinitions()[$field_storage->getType()]['label'],
'#url' => Url::fromRoute('field_ui.storage_edit_' . $this->targetEntityTypeId, $route_parameters),
'#options' => array('attributes' => array('title' => $this->t('Edit field settings.'))),
),
),
),
);
// Add the operations.
$row['data'] = $row['data'] + parent::buildRow($field_config);
if (!empty($field_storage->locked)) {
$row['data']['operations'] = array('data' => array('#markup' => $this->t('Locked')));
$row['class'][] = 'menu-disabled';
}