diff --git a/core/modules/comment/comment.module b/core/modules/comment/comment.module index 3196509..820d8e6 100644 --- a/core/modules/comment/comment.module +++ b/core/modules/comment/comment.module @@ -311,17 +311,18 @@ 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['#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['#entity_type_id'])) { // You cannot use comment fields on entity types with non-integer IDs. - unset($form['fields']['_add_new_field']['type']['#options']['comment']); + // @todo This looks very brittle, we should implement hook_field_presave() + // instead. + unset($form['field']['type']['#options']['comment']); } } diff --git a/core/modules/comment/src/Tests/CommentNonNodeTest.php b/core/modules/comment/src/Tests/CommentNonNodeTest.php index ccaa163..01fd2a7 100644 --- a/core/modules/comment/src/Tests/CommentNonNodeTest.php +++ b/core/modules/comment/src/Tests/CommentNonNodeTest.php @@ -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/add-field'); // Ensure field isn't shown for string IDs. - $this->assertNoOption('edit-fields-add-new-field-type', 'comment'); + $this->assertNoOption('edit-type', 'comment'); // Ensure a core field type shown. - $this->assertOption('edit-fields-add-new-field-type', 'boolean'); + $this->assertOption('edit-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/add-field'); // Ensure field isn't shown for empty IDs. - $this->assertNoOption('edit-fields-add-new-field-type', 'comment'); + $this->assertNoOption('edit-type', 'comment'); // Ensure a core field type shown. - $this->assertOption('edit-fields-add-new-field-type', 'boolean'); + $this->assertOption('edit-type', 'boolean'); } } diff --git a/core/modules/contact/src/Tests/ContactSitewideTest.php b/core/modules/contact/src/Tests/ContactSitewideTest.php index 53d8269..569b299 100644 --- a/core/modules/contact/src/Tests/ContactSitewideTest.php +++ b/core/modules/contact/src/Tests/ContactSitewideTest.php @@ -256,6 +256,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()); diff --git a/core/modules/entity_reference/src/Tests/EntityReferenceAdminTest.php b/core/modules/entity_reference/src/Tests/EntityReferenceAdminTest.php index a89606c..f0896e9 100644 --- a/core/modules/entity_reference/src/Tests/EntityReferenceAdminTest.php +++ b/core/modules/entity_reference/src/Tests/EntityReferenceAdminTest.php @@ -55,11 +55,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 . '/add-field', array( + 'label' => 'Test label', + 'field_name' => 'test', + 'type' => 'entity_reference', + ), t('Save and continue')); // Node should be selected by default. $this->assertFieldByName('field_storage[settings][target_type]', 'node'); @@ -192,11 +192,11 @@ public function createEntityReferenceField($target_type, $bundle = NULL) { $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')); + $this->drupalPostForm($bundle_path . '/add-field', array( + 'label' => $this->randomMachineName(), + 'field_name' => $field_name, + 'type' => 'entity_reference', + ), t('Save and continue')); // 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')); diff --git a/core/modules/field_ui/css/field_ui.admin.css b/core/modules/field_ui/css/field_ui.admin.css index 29bef62..ede3d06 100644 --- a/core/modules/field_ui/css/field_ui.admin.css +++ b/core/modules/field_ui/css/field_ui.admin.css @@ -3,6 +3,16 @@ * Stylesheet for the Field UI module. */ +/* Add new field page. */ +.field-ui-field-storage-add-form .field-type-wrapper .form-item { + float: left; + margin-right: 1em; + vertical-align: text-bottom; +} +.field-ui-field-storage-add-form .field-type-wrapper .form-type-item { + margin-top: 2.3em; +} + /* 'Manage fields' and 'Manage display' overviews */ .field-ui-overview .add-new .label-input { float: left; /* LTR */ diff --git a/core/modules/field_ui/field_ui.js b/core/modules/field_ui/field_ui.js index 3fed9ef..ffbe499 100644 --- a/core/modules/field_ui/field_ui.js +++ b/core/modules/field_ui/field_ui.js @@ -7,6 +7,29 @@ "use strict"; + Drupal.behaviors.fieldUIFieldStorageAddForm = { + attach: function (context) { + var $fieldType = $(context).find('select[name="type"]'); + var $existingField = $(context).find('select[name="existing_field_name"]'); + + // When the user selects a new field type, clear the "existing field" + // selection. + $fieldType.change(function () { + if ($(this).val() != '') { + $existingField.val(''); + } + }); + + // When the user selects an existing field, clear the "new field type" + // selection. + $existingField.change(function () { + if ($(this).val() != '') { + $fieldType.val('').change(); + } + }); + } + }; + Drupal.behaviors.fieldUIDisplayOverview = { attach: function (context, settings) { $(context).find('table#field-display-overview').once('field-display-overview', function () { diff --git a/core/modules/field_ui/field_ui.links.action.yml b/core/modules/field_ui/field_ui.links.action.yml index dae1583..b03bac5 100644 --- a/core/modules/field_ui/field_ui.links.action.yml +++ b/core/modules/field_ui/field_ui.links.action.yml @@ -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 diff --git a/core/modules/field_ui/src/Controller/FieldConfigListController.php b/core/modules/field_ui/src/Controller/FieldConfigListController.php new file mode 100644 index 0000000..8ed9be0 --- /dev/null +++ b/core/modules/field_ui/src/Controller/FieldConfigListController.php @@ -0,0 +1,39 @@ +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); + } + +} diff --git a/core/modules/field_ui/src/DisplayOverviewBase.php b/core/modules/field_ui/src/DisplayOverviewBase.php index adab34e..ebe217f 100644 --- a/core/modules/field_ui/src/DisplayOverviewBase.php +++ b/core/modules/field_ui/src/DisplayOverviewBase.php @@ -16,13 +16,50 @@ 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 entity type of the entity bundle. + * + * @var string + */ + protected $bundleEntityType; + + /** + * 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 +102,7 @@ * 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 +121,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( @@ -102,6 +154,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. * * @return \Drupal\Core\Field\FieldDefinitionInterface[] @@ -118,8 +184,19 @@ 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->bundleEntityType = $entity_type->getBundleEntityType(); + $stored_bundle = $form_state->get('bundle'); + if (!$stored_bundle) { + if (!$bundle) { + $bundle = $this->getRequest()->attributes->get('_raw_variables')->get($this->bundleEntityType); + } + $stored_bundle = $bundle; + $form_state->set('bundle', $bundle); + } + $this->entity_type = $entity_type_id; + $this->bundle = $stored_bundle; $this->mode = $mode; $field_definitions = $this->getFieldDefinitions(); @@ -690,6 +767,120 @@ public function multistepAjax($form, FormStateInterface $form_state) { } /** + * 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 = drupal_html_class($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. * * @param string $mode diff --git a/core/modules/field_ui/src/FieldConfigListBuilder.php b/core/modules/field_ui/src/FieldConfigListBuilder.php index cae4250..f0290ea 100644 --- a/core/modules/field_ui/src/FieldConfigListBuilder.php +++ b/core/modules/field_ui/src/FieldConfigListBuilder.php @@ -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; /** @@ -19,6 +24,34 @@ 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 type of the target entity bundle. + * + * @var string + */ + protected $targetBundleEntityType; + + /** + * An array of field type definitions. + * + * @var array + */ + protected $fieldTypes; + + /** * The entity manager. * * @var \Drupal\Core\Entity\EntityManagerInterface @@ -32,27 +65,104 @@ 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; + $this->targetBundleEntityType = $this->entityManager->getDefinition($target_entity_type_id)->getBundleEntityType(); + $this->fieldTypes = $this->fieldTypeManager->getDefinitions(); + + $build = parent::render(); + $build['#attributes']['id'] = 'field-overview'; + $build['#empty'] = $this->t('No fields are present yet.'); + + return $build; } /** * {@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 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 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(); + $route_parameters = array( + $this->targetBundleEntityType => $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->fieldTypes[$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'; + } + + return $row; } /** diff --git a/core/modules/field_ui/src/FieldOverview.php b/core/modules/field_ui/src/FieldOverview.php deleted file mode 100644 index c780936..0000000 --- a/core/modules/field_ui/src/FieldOverview.php +++ /dev/null @@ -1,536 +0,0 @@ -fieldTypeManager = $field_type_manager; - } - - /** - * {@inheritdoc} - */ - public static function create(ContainerInterface $container) { - return new static( - $container->get('entity.manager'), - $container->get('plugin.manager.field.field_type') - ); - } - - /** - * {@inheritdoc} - */ - public function getRegions() { - return array( - 'content' => array( - 'title' => $this->t('Content'), - 'invisible' => TRUE, - // @todo Bring back this message in https://drupal.org/node/1963340. - //'message' => $this->t('No fields are present yet.'), - ), - ); - } - - /** - * {@inheritdoc} - */ - public function getFormId() { - return 'field_ui_field_overview_form'; - } - - /** - * {@inheritdoc} - */ - public function buildForm(array $form, FormStateInterface $form_state, $entity_type_id = NULL, $bundle = NULL) { - parent::buildForm($form, $form_state, $entity_type_id, $bundle); - - // Gather bundle information. - $fields = array_filter(\Drupal::entityManager()->getFieldDefinitions($this->entity_type, $this->bundle), function ($field_definition) { - return $field_definition instanceof FieldConfigInterface; - }); - $field_types = $this->fieldTypeManager->getDefinitions(); - - // Field prefix. - $field_prefix = \Drupal::config('field_ui.settings')->get('field_prefix'); - - $form += array( - '#entity_type' => $this->entity_type, - '#bundle' => $this->bundle, - '#fields' => array_keys($fields), - ); - - $table = array( - '#type' => 'field_ui_table', - '#tree' => TRUE, - '#header' => array( - $this->t('Label'), - array( - 'data' => $this->t('Machine name'), - 'class' => array(RESPONSIVE_PRIORITY_MEDIUM), - ), - $this->t('Field type'), - $this->t('Operations'), - ), - '#regions' => $this->getRegions(), - '#attributes' => array( - 'class' => array('field-ui-overview'), - 'id' => 'field-overview', - ), - ); - - // Fields. - foreach ($fields as $name => $field) { - $field_storage = $field->getFieldStorageDefinition(); - $route_parameters = array( - $this->bundleEntityType => $this->bundle, - 'field_config' => $field->id(), - ); - $table[$name] = array( - '#attributes' => array( - 'id' => drupal_html_class($name), - ), - 'label' => array( - '#markup' => String::checkPlain($field->getLabel()), - ), - 'field_name' => array( - '#markup' => $field->getName(), - ), - 'type' => array( - '#type' => 'link', - '#title' => $field_types[$field_storage->getType()]['label'], - '#url' => Url::fromRoute('field_ui.storage_edit_' . $this->entity_type, $route_parameters), - '#options' => array('attributes' => array('title' => $this->t('Edit field settings.'))), - ), - ); - - $table[$name]['operations']['data'] = array( - '#type' => 'operations', - '#links' => $this->entityManager->getListBuilder('field_config')->getOperations($field), - ); - - if (!empty($field_storage->locked)) { - $table[$name]['operations'] = array('#markup' => $this->t('Locked')); - $table[$name]['#attributes']['class'][] = 'menu-disabled'; - } - } - - // Gather valid field types. - $field_type_options = array(); - foreach ($field_types as $name => $field_type) { - // Skip field types which should not be added via user interface. - if (empty($field_type['no_ui'])) { - $field_type_options[$name] = $field_type['label']; - } - } - asort($field_type_options); - - // Additional row: add new field. - if ($field_type_options) { - $name = '_add_new_field'; - $table[$name] = array( - '#attributes' => array('class' => array('add-new')), - 'label' => array( - '#type' => 'textfield', - '#title' => $this->t('New field label'), - '#title_display' => 'invisible', - '#size' => 15, - '#description' => $this->t('Label'), - '#prefix' => '
' . $this->t('Add new field') .'
', - '#suffix' => '
', - ), - 'field_name' => array( - '#type' => 'machine_name', - '#title' => $this->t('New field name'), - '#title_display' => 'invisible', - // This field should stay LTR even for RTL languages. - '#field_prefix' => '' . $field_prefix, - '#field_suffix' => '‎', - '#size' => 15, - '#description' => $this->t('A unique machine-readable name containing letters, numbers, and underscores.'), - // Calculate characters depending on the length of the field prefix - // setting. Maximum length is 32. - '#maxlength' => FieldStorageConfig::NAME_MAX_LENGTH - strlen($field_prefix), - '#prefix' => '
 
', - '#machine_name' => array( - 'source' => array('fields', $name, 'label'), - 'exists' => array($this, 'fieldNameExists'), - 'standalone' => TRUE, - 'label' => '', - ), - '#required' => FALSE, - ), - 'type' => array( - '#type' => 'select', - '#title' => $this->t('Type of new field'), - '#title_display' => 'invisible', - '#options' => $field_type_options, - '#empty_option' => $this->t('- Select a field type -'), - '#description' => $this->t('Type of data to store.'), - '#attributes' => array('class' => array('field-type-select')), - '#cell_attributes' => array('colspan' => 2), - '#prefix' => '
 
', - ), - // Place the 'translatable' property as an explicit value so that - // contrib modules can form_alter() the value for newly created fields. - 'translatable' => array( - '#type' => 'value', - '#value' => TRUE, - ), - ); - } - - // Additional row: re-use existing field storages. - $existing_fields = $this->getExistingFieldStorageOptions(); - if ($existing_fields) { - // Build list of options. - $existing_field_options = array(); - foreach ($existing_fields as $field_name => $info) { - $text = $this->t('@type: @field', array( - '@type' => $info['type_label'], - '@field' => $info['field'], - )); - $existing_field_options[$field_name] = Unicode::truncate($text, 80, FALSE, TRUE); - } - asort($existing_field_options); - $name = '_add_existing_field'; - $table[$name] = array( - '#attributes' => array('class' => array('add-new')), - '#row_type' => 'add_new_field', - '#region_callback' => array($this, 'getRowRegion'), - 'label' => array( - '#type' => 'textfield', - '#title' => $this->t('Existing field label'), - '#title_display' => 'invisible', - '#size' => 15, - '#description' => $this->t('Label'), - '#attributes' => array('class' => array('label-textfield')), - '#prefix' => '
' . $this->t('Re-use existing field') .'
', - '#suffix' => '
', - ), - 'field_name' => array( - '#type' => 'select', - '#title' => $this->t('Existing field to share'), - '#title_display' => 'invisible', - '#options' => $existing_field_options, - '#empty_option' => $this->t('- Select an existing field -'), - '#description' => $this->t('Field to share'), - '#attributes' => array('class' => array('field-select')), - '#cell_attributes' => array('colspan' => 3), - '#prefix' => '
 
', - ), - ); - } - - // We can set the 'rows_order' element, needed by theme_field_ui_table(), - // here instead of a #pre_render callback because this form doesn't have the - // tabledrag behavior anymore. - $table['#regions']['content']['rows_order'] = array(); - foreach (Element::children($table) as $name) { - $table['#regions']['content']['rows_order'][] = $name; - } - - $form['fields'] = $table; - - $form['actions'] = array('#type' => 'actions'); - $form['actions']['submit'] = array( - '#type' => 'submit', - '#button_type' => 'primary', - '#value' => $this->t('Save')); - - return $form; - } - - /** - * {@inheritdoc} - */ - public function validateForm(array &$form, FormStateInterface $form_state) { - $this->validateAddNew($form, $form_state); - $this->validateAddExisting($form, $form_state); - } - - /** - * Validates the 'add new field' row. - * - * @param array $form - * An associative array containing the structure of the form. - * @param \Drupal\Core\Form\FormStateInterface $form_state - * The current state of the form. - * - * @see \Drupal\field_ui\FieldOverview::validateForm() - */ - protected function validateAddNew(array $form, FormStateInterface $form_state) { - $field = $form_state->getValue(array('fields', '_add_new_field')); - - // Validate if any information was provided in the 'add new field' row. - if (array_filter(array($field['label'], $field['field_name'], $field['type']))) { - // Missing label. - if (!$field['label']) { - $form_state->setErrorByName('fields][_add_new_field][label', $this->t('Add new field: you need to provide a label.')); - } - - // Missing field name. - if (!$field['field_name']) { - $form_state->setErrorByName('fields][_add_new_field][field_name', $this->t('Add new field: you need to provide a machine name for the field.')); - } - // Field name validation. - else { - $field_name = $field['field_name']; - - // Add the field prefix. - $field_name = \Drupal::config('field_ui.settings')->get('field_prefix') . $field_name; - $form_state->setValueForElement($form['fields']['_add_new_field']['field_name'], $field_name); - } - - // Missing field type. - if (!$field['type']) { - $form_state->setErrorByName('fields][_add_new_field][type', $this->t('Add new field: you need to select a field type.')); - } - } - } - - /** - * Validates the 're-use existing field' row. - * - * @param array $form - * An associative array containing the structure of the form. - * @param \Drupal\Core\Form\FormStateInterface $form_state - * The current state of the form. - * - * @see \Drupal\field_ui\FieldOverview::validate() - */ - protected function validateAddExisting(array $form, FormStateInterface $form_state) { - // The form element might be absent if no existing fields can be added to - // this bundle. - if ($field = $form_state->getValue(array('fields', '_add_existing_field'))) { - // Validate if any information was provided in the - // 're-use existing field' row. - if (array_filter(array($field['label'], $field['field_name']))) { - // Missing label. - if (!$field['label']) { - $form_state->setErrorByName('fields][_add_existing_field][label', $this->t('Re-use existing field: you need to provide a label.')); - } - - // Missing existing field name. - if (!$field['field_name']) { - $form_state->setErrorByName('fields][_add_existing_field][field_name', $this->t('Re-use existing field: you need to select a field.')); - } - } - } - } - - /** - * Overrides \Drupal\field_ui\OverviewBase::submitForm(). - */ - public function submitForm(array &$form, FormStateInterface $form_state) { - $error = FALSE; - $form_values = $form_state->getValue('fields'); - $destinations = array(); - - // Create new field. - if (!empty($form_values['_add_new_field']['field_name'])) { - $values = $form_values['_add_new_field']; - - $field_storage = array( - 'field_name' => $values['field_name'], - 'entity_type' => $this->entity_type, - 'type' => $values['type'], - 'translatable' => $values['translatable'], - ); - $field = array( - 'field_name' => $values['field_name'], - 'entity_type' => $this->entity_type, - 'bundle' => $this->bundle, - 'label' => $values['label'], - // Field translatability should be explicitly enabled by the users. - 'translatable' => FALSE, - ); - - // Create the field storage and field. - try { - $this->entityManager->getStorage('field_storage_config')->create($field_storage)->save(); - $new_field = $this->entityManager->getStorage('field_config')->create($field); - $new_field->save(); - - // 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. - entity_get_form_display($this->entity_type, $this->bundle, 'default') - ->setComponent($values['field_name']) - ->save(); - - // 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. - entity_get_display($this->entity_type, $this->bundle, 'default') - ->setComponent($values['field_name']) - ->save(); - - // Always show the field settings step, as the cardinality needs to be - // configured for new fields. - $route_parameters = array( - $this->bundleEntityType => $this->bundle, - 'field_config' => $new_field->id(), - ); - $destinations[] = array('route_name' => 'field_ui.storage_edit_' . $this->entity_type, 'route_parameters' => $route_parameters); - $destinations[] = array('route_name' => 'field_ui.field_edit_' . $this->entity_type, 'route_parameters' => $route_parameters); - - // Store new field information for any additional submit handlers. - $form_state->set(['fields_added', '_add_new_field'], $values['field_name']); - } - catch (\Exception $e) { - $error = TRUE; - drupal_set_message($this->t('There was a problem creating field %label: !message', array('%label' => $field['label'], '!message' => $e->getMessage())), 'error'); - } - } - - // Re-use existing field. - if (!empty($form_values['_add_existing_field']['field_name'])) { - $values = $form_values['_add_existing_field']; - $field_name = $values['field_name']; - $field_storage = FieldStorageConfig::loadByName($this->entity_type, $field_name); - if (!empty($field_storage->locked)) { - drupal_set_message($this->t('The field %label cannot be added because it is locked.', array('%label' => $values['label'])), 'error'); - } - else { - $field = array( - 'field_name' => $field_name, - 'entity_type' => $this->entity_type, - 'bundle' => $this->bundle, - 'label' => $values['label'], - ); - - try { - $new_field = $this->entityManager->getStorage('field_config')->create($field); - $new_field->save(); - - // 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. - entity_get_form_display($this->entity_type, $this->bundle, 'default') - ->setComponent($field_name) - ->save(); - - // 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. - entity_get_display($this->entity_type, $this->bundle, 'default') - ->setComponent($field_name) - ->save(); - - $destinations[] = array( - 'route_name' => 'field_ui.field_edit_' . $this->entity_type, - 'route_parameters' => array( - $this->bundleEntityType => $this->bundle, - 'field_config' => $new_field->id(), - ), - ); - // Store new field information for any additional submit handlers. - $form_state->set(['fields_added', '_add_existing_field'], $field['field_name']); - } - catch (\Exception $e) { - $error = TRUE; - drupal_set_message($this->t('There was a problem creating field %label: @message.', array('%label' => $field['label'], '@message' => $e->getMessage())), 'error'); - } - } - } - - if ($destinations) { - $destination = drupal_get_destination(); - $destinations[] = $destination['destination']; - $form_state->setRedirectUrl(FieldUI::getNextDestination($destinations, $form_state)); - } - elseif (!$error) { - drupal_set_message($this->t('Your settings have been saved.')); - } - } - - /** - * Returns an array of existing field storages that can be added to a bundle. - * - * @return array - * An array of existing field storages keyed by name. - */ - protected function getExistingFieldStorageOptions() { - $options = array(); - // Load the field_storages and build the list of options. - $field_types = $this->fieldTypeManager->getDefinitions(); - foreach ($this->entityManager->getFieldStorageDefinitions($this->entity_type) as $field_name => $field_storage) { - // Do not show: - // - non-configurable field storages, - // - locked field_storages, - // - field_storages that should not be added via user interface, - // - field_storages that already have a field in the bundle. - $field_type = $field_storage->getType(); - if ($field_storage instanceof FieldStorageConfigInterface - && !$field_storage->isLocked() - && empty($field_types[$field_type]['no_ui']) - && !in_array($this->bundle, $field_storage->getBundles(), TRUE)) { - $options[$field_name] = array( - 'type' => $field_type, - 'type_label' => $field_types[$field_type]['label'], - 'field' => $field_name, - ); - } - } - - return $options; - } - - /** - * Checks if a field machine name is taken. - * - * @param string $value - * The machine name, not prefixed. - * - * @return bool - * Whether or not the field machine name is taken. - */ - public function fieldNameExists($value) { - // Add the field prefix. - $field_name = \Drupal::config('field_ui.settings')->get('field_prefix') . $value; - - $field_storage_definitions = \Drupal::entityManager()->getFieldStorageDefinitions($this->entity_type); - return isset($field_storage_definitions[$field_name]); - } - -} diff --git a/core/modules/field_ui/src/Form/FieldStorageAddForm.php b/core/modules/field_ui/src/Form/FieldStorageAddForm.php new file mode 100644 index 0000000..891413f --- /dev/null +++ b/core/modules/field_ui/src/Form/FieldStorageAddForm.php @@ -0,0 +1,473 @@ +entityManager = $entity_manager; + $this->fieldTypePluginManager = $field_type_plugin_manager; + } + + /** + * {@inheritdoc} + */ + public function getFormID() { + return 'field_ui_field_storage_add_form'; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('entity.manager'), + $container->get('plugin.manager.field.field_type') + ); + } + + /** + * {@inheritdoc} + */ + public function buildForm(array $form, FormStateInterface $form_state, $entity_type_id = NULL, $bundle = NULL) { + $entity_type = $this->entityManager->getDefinition($entity_type_id); + $this->bundleEntityTypeId = $entity_type->getBundleEntityType(); + if (!$form_state->hasValue('bundle')) { + if (!$bundle) { + $bundle = $this->getRequest()->attributes->get('_raw_variables')->get($this->bundleEntityTypeId); + } + $form_state->setValue('bundle', $bundle); + } + + $this->entityTypeId = $entity_type_id; + $this->bundle = $form_state->getValue('bundle'); + + $form += array( + '#entity_type_id' => $this->entityTypeId, + '#bundle' => $this->bundle, + ); + + // Gather valid field types. + $field_type_options = array(); + foreach ($this->fieldTypePluginManager->getDefinitions() as $name => $field_type) { + // Skip field types which should not be added via user interface. + if (empty($field_type['no_ui'])) { + $field_type_options[$name] = $field_type['label']; + } + } + asort($field_type_options); + + $form['field'] = array( + '#type' => 'container', + '#attributes' => array('class' => array('field-type-wrapper', 'clearfix')), + ); + + $form['field']['type'] = array( + '#type' => 'select', + '#title' => $this->t('Add a new field'), + '#options' => $field_type_options, + '#empty_option' => $this->t('- Select a field type -'), + ); + + // Re-use existing field. + if ($existing_fields = $this->getExistingFieldStorageOptions()) { + // Build list of options. + $existing_field_options = array(); + foreach ($existing_fields as $field_name => $info) { + $text = $this->t('@type: @field', array( + '@type' => $info['type_label'], + '@field' => $info['field'], + )); + $existing_field_options[$field_name] = Unicode::truncate($text, 80, FALSE, TRUE); + } + asort($existing_field_options); + + $form['field']['separator'] = array( + '#type' => 'item', + '#markup' => $this->t('or'), + ); + $form['field']['existing_field_name'] = array( + '#type' => 'select', + '#title' => $this->t('Re-use an existing field'), + '#options' => $existing_field_options, + '#empty_option' => $this->t('- Select an existing field -'), + ); + } + else { + // Provide a placeholder form element to simply the validation code. + $form['field']['existing_field_name'] = array( + '#type' => 'value', + '#value' => FALSE, + ); + } + + $form['new_field_wrapper'] = array( + '#type' => 'container', + '#states' => array( + '!visible' => array( + ':input[name="type"]' => array('value' => ''), + ), + ), + ); + $form['new_field_wrapper']['label'] = array( + '#type' => 'textfield', + '#title' => $this->t('Label'), + '#size' => 15, + ); + + $field_prefix = $this->config('field_ui.settings')->get('field_prefix'); + $form['new_field_wrapper']['field_name'] = array( + '#type' => 'machine_name', + // This field should stay LTR even for RTL languages. + '#field_prefix' => '' . $field_prefix, + '#field_suffix' => '‎', + '#size' => 15, + '#description' => $this->t('A unique machine-readable name containing letters, numbers, and underscores.'), + // Calculate characters depending on the length of the field prefix + // setting. Maximum length is 32. + '#maxlength' => FieldStorageConfig::NAME_MAX_LENGTH - strlen($field_prefix), + '#machine_name' => array( + 'source' => array('new_field_wrapper', 'label'), + 'exists' => array($this, 'fieldNameExists'), + ), + '#required' => FALSE, + ); + + // Place the 'translatable' property as an explicit value so that + // contrib modules can form_alter() the value for newly created fields. + $form['translatable'] = array( + '#type' => 'value', + '#value' => FALSE, + ); + + $form['actions'] = array('#type' => 'actions'); + $form['actions']['submit'] = array( + '#type' => 'submit', + '#value' => $this->t('Save and continue'), + '#button_type' => 'primary', + ); + + $form['#attached']['library'][] = 'field_ui/drupal.field_ui'; + + return $form; + } + + /** + * {@inheritdoc} + */ + public function validateForm(array &$form, FormStateInterface $form_state) { + // Missing field type. + if (!$form_state->getValue('type') && !$form_state->getValue('existing_field_name')) { + $form_state->setErrorByName('type', $this->t('You need to select a field type.')); + } + // Both field type and existing field option selected. This is prevented in + // the UI with JavaScript but we also need a proper server-side validation. + elseif ($form_state->getValue('type') && $form_state->getValue('existing_field_name')) { + $form_state->setErrorByName('type', $this->t('Adding a new field and re-using an existing field at the same time is not allowed.')); + return; + } + + $this->validateAddNew($form, $form_state); + $this->validateAddExisting($form, $form_state); + } + + /** + * Validates the 'add new field' case. + * + * @param array $form + * An associative array containing the structure of the form. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The current state of the form. + * + * @see \Drupal\field_ui\Form\FieldStorageAddForm::validateForm() + */ + protected function validateAddNew(array $form, FormStateInterface $form_state) { + $field = $form_state->getValues(); + + // Validate if any information was provided in the 'add new field' case. + if (!$field['existing_field_name'] && array_filter(array($field['label'], $field['field_name'], $field['type']))) { + // Missing label. + if (!$field['label']) { + $form_state->setErrorByName('label', $this->t('Add new field: you need to provide a label.')); + } + + // Missing field name. + if (!$field['field_name']) { + $form_state->setErrorByName('field_name', $this->t('Add new field: you need to provide a machine name for the field.')); + } + // Field name validation. + else { + $field_name = $field['field_name']; + + // Add the field prefix. + $field_name = \Drupal::config('field_ui.settings')->get('field_prefix') . $field_name; + $form_state->setValueForElement($form['new_field_wrapper']['field_name'], $field_name); + } + } + } + + /** + * Validates the 're-use existing field' case. + * + * @param array $form + * An associative array containing the structure of the form. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The current state of the form. + * + * @see \Drupal\field_ui\Form\FieldStorageAddForm::validateForm() + */ + protected function validateAddExisting(array $form, FormStateInterface $form_state) { + // The form element might be absent if no existing fields can be added to + // this bundle. + if ($form_state->getValue('existing_field_name')) { + // Discard any values that might be set for the 'label' and 'field_name' + // form elements. + $form_state->setValueForElement($form['new_field_wrapper']['label'], ''); + $form_state->setValueForElement($form['new_field_wrapper']['field_name'], ''); + } + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + $error = FALSE; + $values = $form_state->getValues(); + $destinations = array(); + + // Create new field. + if ($values['type']) { + $field_storage = array( + 'field_name' => $values['field_name'], + 'entity_type' => $this->entityTypeId, + 'type' => $values['type'], + 'translatable' => $values['translatable'], + ); + $field = 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, + ); + + // Create the field storage and field. + try { + $this->entityManager->getStorage('field_storage_config')->create($field_storage)->save(); + $new_field = $this->entityManager->getStorage('field_config')->create($field); + $new_field->save(); + + $this->configureEntityDisplays($values['field_name']); + + // Always show the field settings step, as the cardinality needs to be + // configured for new fields. + $route_parameters = array( + $this->bundleEntityTypeId => $this->bundle, + 'field_config' => $new_field->id(), + ); + $destinations[] = array('route_name' => 'field_ui.storage_edit_' . $this->entityTypeId, 'route_parameters' => $route_parameters); + $destinations[] = array('route_name' => 'field_ui.field_edit_' . $this->entityTypeId, 'route_parameters' => $route_parameters); + $destinations[] = array('route_name' => 'field_ui.overview_' . $this->entityTypeId, 'route_parameters' => $route_parameters); + + // Store new field information for any additional submit handlers. + $form_state->set(['fields_added', '_add_new_field'], $values['field_name']); + } + catch (\Exception $e) { + $error = TRUE; + drupal_set_message($this->t('There was a problem creating field %label: !message', array('%label' => $field['label'], '!message' => $e->getMessage())), 'error'); + } + } + + // Re-use existing field. + if (!empty($values['existing_field_name'])) { + $field_name = $values['existing_field_name']; + $field_storage = FieldStorageConfig::loadByName($this->entityTypeId, $field_name); + if ($field_storage->isLocked()) { + drupal_set_message($this->t('The field %field_name cannot be added because it is locked.', array('%field_name' => $field_storage->getName())), 'error'); + } + else { + $field = array( + 'field_name' => $field_name, + 'entity_type' => $this->entityTypeId, + 'bundle' => $this->bundle, + // @todo Should we try to populate this value on a best-effort basis? + // (i.e. use the label of the first instance of this field storage) + 'label' => '', + ); + + try { + $new_field = $this->entityManager->getStorage('field_config')->create($field); + $new_field->save(); + + $this->configureEntityDisplays($field_name); + + $route_parameters = array( + $this->bundleEntityTypeId => $this->bundle, + 'field_config' => $new_field->id(), + ); + $destinations[] = array('route_name' => 'field_ui.field_edit_' . $this->entityTypeId, 'route_parameters' => $route_parameters); + $destinations[] = array('route_name' => 'field_ui.overview_' . $this->entityTypeId, 'route_parameters' => $route_parameters); + + // Store new field information for any additional submit handlers. + $form_state->set(['fields_added', '_add_existing_field'], $field['field_name']); + } + catch (\Exception $e) { + $error = TRUE; + drupal_set_message($this->t('There was a problem creating field %field_name: @message.', array('%field_name' => $field_storage->getName(), '@message' => $e->getMessage())), 'error'); + } + } + } + + if ($destinations) { + $destination = drupal_get_destination(); + $destinations[] = $destination['destination']; + $form_state->setRedirectUrl(FieldUI::getNextDestination($destinations, $form_state)); + } + elseif (!$error) { + drupal_set_message($this->t('Your settings have been saved.')); + } + } + + /** + * Configures the newly created field for the default view and form modes. + * + * @param string $field_name + * The field name. + */ + protected function configureEntityDisplays($field_name) { + // 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. + entity_get_form_display($this->entityTypeId, $this->bundle, 'default') + ->setComponent($field_name) + ->save(); + + // 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. + entity_get_display($this->entityTypeId, $this->bundle, 'default') + ->setComponent($field_name) + ->save(); + } + + /** + * Returns an array of existing field storages that can be added to a bundle. + * + * @return array + * An array of existing field storages keyed by name. + */ + protected function getExistingFieldStorageOptions() { + $options = array(); + // Load the field_storages and build the list of options. + $field_types = $this->fieldTypePluginManager->getDefinitions(); + foreach ($this->entityManager->getFieldStorageDefinitions($this->entityTypeId) as $field_name => $field_storage) { + // Do not show: + // - non-configurable field storages, + // - locked field_storages, + // - field_storages that should not be added via user interface, + // - field_storages that already have a field in the bundle. + $field_type = $field_storage->getType(); + if ($field_storage instanceof FieldStorageConfigInterface + && !$field_storage->isLocked() + && empty($field_types[$field_type]['no_ui']) + && !in_array($this->bundle, $field_storage->getBundles(), TRUE)) { + $options[$field_name] = array( + 'type' => $field_type, + 'type_label' => $field_types[$field_type]['label'], + 'field' => $field_name, + ); + } + } + + return $options; + } + + /** + * Checks if a field machine name is taken. + * + * @param string $value + * The machine name, not prefixed. + * @param array $element + * An array containing the structure of the 'field_name' element. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The current state of the form. + * + * @return bool + * Whether or not the field machine name is taken. + */ + public function fieldNameExists($value, $element, FormStateInterface $form_state) { + // Don't validate the case when an existing field has been selected. + if ($form_state->getValue('existing_field_name')) { + return FALSE; + } + + // Add the field prefix. + $field_name = \Drupal::config('field_ui.settings')->get('field_prefix') . $value; + + $field_storage_definitions = \Drupal::entityManager()->getFieldStorageDefinitions($this->entityTypeId); + return isset($field_storage_definitions[$field_name]); + } + +} diff --git a/core/modules/field_ui/src/OverviewBase.php b/core/modules/field_ui/src/OverviewBase.php deleted file mode 100644 index 93adaa8..0000000 --- a/core/modules/field_ui/src/OverviewBase.php +++ /dev/null @@ -1,246 +0,0 @@ -entityManager = $entity_manager; - } - - /** - * {@inheritdoc} - */ - public static function create(ContainerInterface $container) { - return new static( - $container->get('entity.manager') - ); - } - - /** - * {@inheritdoc} - */ - public function buildForm(array $form, FormStateInterface $form_state, $entity_type_id = NULL, $bundle = NULL) { - $entity_type = $this->entityManager->getDefinition($entity_type_id); - $this->bundleEntityType = $entity_type->getBundleEntityType(); - $stored_bundle = $form_state->get('bundle'); - if (!$stored_bundle) { - if (!$bundle) { - $bundle = $this->getRequest()->attributes->get('_raw_variables')->get($this->bundleEntityType); - } - $stored_bundle = $bundle; - $form_state->set('bundle', $bundle); - } - - $this->entity_type = $entity_type_id; - $this->bundle = $stored_bundle; - } - - /** - * {@inheritdoc} - */ - public function submitForm(array &$form, FormStateInterface $form_state) { - } - - /** - * 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 - */ - abstract public function getRegions(); - - /** - * Returns an associative array of all regions. - */ - public function getRegionOptions() { - $options = array(); - foreach ($this->getRegions() as $region => $data) { - $options[$region] = $data['title']; - } - return $options; - } - - /** - * 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 $element - * 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 = drupal_html_class($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\OverviewBase::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; - } - -} diff --git a/core/modules/field_ui/src/Plugin/Derivative/FieldUiLocalAction.php b/core/modules/field_ui/src/Plugin/Derivative/FieldUiLocalAction.php new file mode 100644 index 0000000..88b47e8 --- /dev/null +++ b/core/modules/field_ui/src/Plugin/Derivative/FieldUiLocalAction.php @@ -0,0 +1,77 @@ +routeProvider = $route_provider; + $this->entityManager = $entity_manager; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, $base_plugin_id) { + return new static( + $container->get('router.route_provider'), + $container->get('entity.manager') + ); + } + + /** + * {@inheritdoc} + */ + public function getDerivativeDefinitions($base_plugin_definition) { + $this->derivatives = array(); + + foreach ($this->entityManager->getDefinitions() as $entity_type_id => $entity_type) { + if ($entity_type->get('field_ui_base_route')) { + $this->derivatives["field_storage_config_add_$entity_type_id"] = array( + 'route_name' => "field_ui.field_storage_config_add_$entity_type_id", + 'title' => $this->t('Add field'), + 'appears_on' => array("field_ui.overview_$entity_type_id"), + ); + } + } + + foreach ($this->derivatives as &$entry) { + $entry += $base_plugin_definition; + } + + return $this->derivatives; + } + +} diff --git a/core/modules/field_ui/src/Routing/RouteSubscriber.php b/core/modules/field_ui/src/Routing/RouteSubscriber.php index 3abb6cf..51fa63b 100644 --- a/core/modules/field_ui/src/Routing/RouteSubscriber.php +++ b/core/modules/field_ui/src/Routing/RouteSubscriber.php @@ -90,7 +90,7 @@ protected function alterRoutes(RouteCollection $collection) { $route = new Route( "$path/fields", array( - '_form' => '\Drupal\field_ui\FieldOverview', + '_controller' => '\Drupal\field_ui\Controller\FieldConfigListController::listing', '_title' => 'Manage fields', ) + $defaults, array('_permission' => 'administer ' . $entity_type_id . ' fields'), @@ -99,6 +99,16 @@ protected function alterRoutes(RouteCollection $collection) { $collection->add("field_ui.overview_$entity_type_id", $route); $route = new Route( + "$path/add-field", + array( + '_form' => '\Drupal\field_ui\Form\FieldStorageAddForm', + '_title' => 'Add field', + ) + $defaults, + array('_permission' => 'administer ' . $entity_type_id . ' fields') + ); + $collection->add("field_ui.field_storage_config_add_$entity_type_id", $route); + + $route = new Route( "$path/form-display", array( '_form' => '\Drupal\field_ui\FormDisplayOverview', diff --git a/core/modules/field_ui/src/Tests/FieldUIRouteTest.php b/core/modules/field_ui/src/Tests/FieldUIRouteTest.php index 1f4d3ea..f88d7a2 100644 --- a/core/modules/field_ui/src/Tests/FieldUIRouteTest.php +++ b/core/modules/field_ui/src/Tests/FieldUIRouteTest.php @@ -19,7 +19,7 @@ class FieldUIRouteTest extends WebTestBase { /** * Modules to enable. */ - public static $modules = array('field_ui_test'); + public static $modules = array('entity_test', 'field_ui'); /** * {@inheritdoc} @@ -34,10 +34,8 @@ protected function setUp() { * Ensures that entity types with bundles do not break following entity types. */ public function testFieldUIRoutes() { - $this->drupalGet('field-ui-test-no-bundle/manage/fields'); - // @todo Bring back this assertion in https://drupal.org/node/1963340. - // @see \Drupal\field_ui\FieldOverview::getRegions() - //$this->assertText('No fields are present yet.'); + $this->drupalGet('entity_test_no_id/structure/entity_test/fields'); + $this->assertText('No fields are present yet.'); $this->drupalGet('admin/config/people/accounts/fields'); $this->assertTitle('Manage fields | Drupal'); diff --git a/core/modules/field_ui/src/Tests/FieldUiTestTrait.php b/core/modules/field_ui/src/Tests/FieldUiTestTrait.php index d20fd91..57afdbe 100644 --- a/core/modules/field_ui/src/Tests/FieldUiTestTrait.php +++ b/core/modules/field_ui/src/Tests/FieldUiTestTrait.php @@ -34,19 +34,19 @@ public function fieldUIAddNewField($bundle_path, $field_name, $label = NULL, $field_type = 'test_field', array $storage_edit = array(), array $field_edit = array()) { $label = $label ?: $this->randomString(); $initial_edit = array( - 'fields[_add_new_field][field_name]' => $field_name, - 'fields[_add_new_field][type]' => $field_type, - 'fields[_add_new_field][label]' => $label, + 'type' => $field_type, + 'label' => $label, + 'field_name' => $field_name, ); // Allow the caller to set a NULL path in case they navigated to the right // page before calling this method. if ($bundle_path !== NULL) { - $bundle_path = "$bundle_path/fields"; + $bundle_path = "$bundle_path/add-field"; } - // First step : 'Add new field' on the 'Manage fields' page. - $this->drupalPostForm($bundle_path, $initial_edit, t('Save')); + // First step : 'Add field' page. + $this->drupalPostForm($bundle_path, $initial_edit, t('Save and continue')); $this->assertRaw(t('These settings apply to the %label field everywhere it is used.', array('%label' => $label)), 'Storage settings page was displayed.'); // Test Breadcrumbs. $this->assertLink($label, 0, 'Field label is correct in the breadcrumb of the storage settings page.'); @@ -80,15 +80,15 @@ public function fieldUIAddNewField($bundle_path, $field_name, $label = NULL, $fi public function fieldUIAddExistingField($bundle_path, $existing_field_name, $label = NULL, array $field_edit = array()) { $label = $label ?: $this->randomString(); $initial_edit = array( - 'fields[_add_existing_field][label]' => $label, - 'fields[_add_existing_field][field_name]' => $existing_field_name, + 'existing_field_name' => $existing_field_name, ); - // First step : 'Re-use existing field' on the 'Manage fields' page. - $this->drupalPostForm("$bundle_path/fields", $initial_edit, t('Save')); + // First step: 'Re-use existing field' on the 'Add fields' page. + $this->drupalPostForm("$bundle_path/add-field", $initial_edit, t('Save and continue')); $this->assertNoRaw('<', 'The page does not have double escaped HTML tags.'); - // Second step : 'Field settings' form. + // Second step: 'Field settings' form. + $field_edit += array('field[label]' => $label); $this->drupalPostForm(NULL, $field_edit, t('Save settings')); $this->assertRaw(t('Saved %label configuration.', array('%label' => $label)), 'Redirected to "Manage fields" page.'); diff --git a/core/modules/field_ui/src/Tests/ManageFieldsTest.php b/core/modules/field_ui/src/Tests/ManageFieldsTest.php index 955034c..f6b4a14 100644 --- a/core/modules/field_ui/src/Tests/ManageFieldsTest.php +++ b/core/modules/field_ui/src/Tests/ManageFieldsTest.php @@ -118,11 +118,8 @@ function manageFieldsPage($type = '') { $this->assertRaw($table_header . '', format_string('%table_header table header was found.', array('%table_header' => $table_header))); } - // "Add new field" and "Re-use existing field" aren't a table heading so just - // test the text. - foreach (array('Add new field', 'Re-use existing field') as $element) { - $this->assertText($element, format_string('"@element" was found.', array('@element' => $element))); - } + // Test the "Add field" action link. + $this->assertLink('Add field'); // Assert entity operations for all fields. $result = $this->xpath('//ul[@class = "dropbutton"]/li/a'); @@ -178,12 +175,12 @@ function updateField() { */ function addExistingField() { // Check "Re-use existing field" appears. - $this->drupalGet('admin/structure/types/manage/page/fields'); - $this->assertRaw(t('Re-use existing field'), '"Re-use existing field" was found.'); + $this->drupalGet('admin/structure/types/manage/page/add-field'); + $this->assertRaw(t('Re-use an existing field'), '"Re-use existing field" was found.'); // Check that fields of other entity types (here, the 'comment_body' field) // do not show up in the "Re-use existing field" list. - $this->assertFalse($this->xpath('//select[@id="edit-add-existing-field-field-name"]//option[@value="comment"]'), 'The list of options respects entity type restrictions.'); + $this->assertFalse($this->xpath('//select[@id="edit-existing-field"]//option[@value="comment"]'), 'The list of options respects entity type restrictions.'); // Add a new field based on an existing field. $this->fieldUIAddExistingField("admin/structure/types/manage/page", $this->field_name, $this->field_label . '_2'); @@ -254,8 +251,8 @@ protected function addPersistentFieldStorage() { $this->drupalPostForm(NULL, array(), t('Delete')); } // Check "Re-use existing field" appears. - $this->drupalGet('admin/structure/types/manage/page/fields'); - $this->assertRaw(t('Re-use existing field'), '"Re-use existing field" was found.'); + $this->drupalGet('admin/structure/types/manage/page/add-field'); + $this->assertRaw(t('Re-use an existing field'), '"Re-use existing field" was found.'); // Add a new field for the orphaned storage. $this->fieldUIAddExistingField("admin/structure/types/manage/page", $this->field_name); } @@ -296,11 +293,11 @@ function testFieldPrefix() { // Try to create the field. $edit = array( - 'fields[_add_new_field][label]' => $field_exceed_max_length_label, - 'fields[_add_new_field][field_name]' => $field_exceed_max_length_input, + 'label' => $field_exceed_max_length_label, + 'field_name' => $field_exceed_max_length_input, ); - $this->drupalPostForm('admin/structure/types/manage/' . $this->type . '/fields', $edit, t('Save')); - $this->assertText('New field name cannot be longer than 22 characters but is currently 23 characters long.'); + $this->drupalPostForm('admin/structure/types/manage/' . $this->type . '/add-field', $edit, t('Save and continue')); + $this->assertText('Machine-readable name cannot be longer than 22 characters but is currently 23 characters long.'); // Create a valid field. $this->fieldUIAddNewField('admin/structure/types/manage/' . $this->type, $this->field_name_input, $this->field_label); @@ -409,20 +406,20 @@ function testDisallowedFieldNames() { $label = 'Disallowed field'; $edit = array( - 'fields[_add_new_field][label]' => $label, - 'fields[_add_new_field][type]' => 'test_field', + 'label' => $label, + 'type' => 'test_field', ); // Try with an entity key. - $edit['fields[_add_new_field][field_name]'] = 'title'; + $edit['field_name'] = 'title'; $bundle_path = 'admin/structure/types/manage/' . $this->type; - $this->drupalPostForm("$bundle_path/fields", $edit, t('Save')); + $this->drupalPostForm("$bundle_path/add-field", $edit, t('Save and continue')); $this->assertText(t('The machine-readable name is already in use. It must be unique.')); // Try with a base field. - $edit['fields[_add_new_field][field_name]'] = 'sticky'; + $edit['field_name'] = 'sticky'; $bundle_path = 'admin/structure/types/manage/' . $this->type; - $this->drupalPostForm("$bundle_path/fields", $edit, t('Save')); + $this->drupalPostForm("$bundle_path/add-field", $edit, t('Save and continue')); $this->assertText(t('The machine-readable name is already in use. It must be unique.')); } @@ -467,11 +464,9 @@ function testLockedField() { * Tests that Field UI respects the 'no_ui' flag in the field type definition. */ function testHiddenFields() { - $bundle_path = 'admin/structure/types/manage/' . $this->type . '/fields/'; - // Check that the field type is not available in the 'add new field' row. - $this->drupalGet($bundle_path); - $this->assertFalse($this->xpath('//select[@id="edit-fields-add-new-field-type"]//option[@value="hidden_test_field"]'), "The 'add new field' select respects field types 'no_ui' property."); + $this->drupalGet('admin/structure/types/manage/' . $this->type . '/add-field'); + $this->assertFalse($this->xpath('//select[@id="edit-type"]//option[@value="hidden_test_field"]'), "The 'add new field' select respects field types 'no_ui' property."); // Create a field storage and a field programmatically. $field_name = 'hidden_test_field'; @@ -494,23 +489,22 @@ function testHiddenFields() { // Check that the newly added field appears on the 'Manage Fields' // screen. - $this->drupalGet($bundle_path); + $this->drupalGet('admin/structure/types/manage/' . $this->type . '/fields'); $this->assertFieldByXPath('//table[@id="field-overview"]//tr[@id="hidden-test-field"]//td[1]', $field['label'], 'Field was created and appears in the overview page.'); // Check that the field does not appear in the 're-use existing field' row // on other bundles. - $bundle_path = 'admin/structure/types/manage/article/fields/'; - $this->drupalGet($bundle_path); - $this->assertFalse($this->xpath('//select[@id="edit-add-existing-field-field-name"]//option[@value=:field_name]', array(':field_name' => $field_name)), "The 're-use existing field' select respects field types 'no_ui' property."); + $this->drupalGet('admin/structure/types/manage/article/add-field'); + $this->assertFalse($this->xpath('//select[@id="edit-existing-field"]//option[@value=:field_name]', array(':field_name' => $field_name)), "The 're-use existing field' select respects field types 'no_ui' property."); // Check that non-configurable fields are not available. $field_types = \Drupal::service('plugin.manager.field.field_type')->getDefinitions(); foreach ($field_types as $field_type => $definition) { if (empty($definition['no_ui'])) { - $this->assertTrue($this->xpath('//select[@id="edit-fields-add-new-field-type"]//option[@value=:field_type]', array(':field_type' => $field_type)), String::format('Configurable field type @field_type is available.', array('@field_type' => $field_type))); + $this->assertTrue($this->xpath('//select[@id="edit-type"]//option[@value=:field_type]', array(':field_type' => $field_type)), String::format('Configurable field type @field_type is available.', array('@field_type' => $field_type))); } else { - $this->assertFalse($this->xpath('//select[@id="edit-fields-add-new-field-type"]//option[@value=:field_type]', array(':field_type' => $field_type)), String::format('Non-configurable field type @field_type is not available.', array('@field_type' => $field_type))); + $this->assertFalse($this->xpath('//select[@id="edit-type"]//option[@value=:field_type]', array(':field_type' => $field_type)), String::format('Non-configurable field type @field_type is not available.', array('@field_type' => $field_type))); } } } @@ -535,12 +529,12 @@ function testDuplicateFieldName() { // field_tags already exists, so we're expecting an error when trying to // create a new field with the same name. $edit = array( - 'fields[_add_new_field][field_name]' => 'tags', - 'fields[_add_new_field][label]' => $this->randomMachineName(), - 'fields[_add_new_field][type]' => 'taxonomy_term_reference', + 'field_name' => 'tags', + 'label' => $this->randomMachineName(), + 'type' => 'taxonomy_term_reference', ); - $url = 'admin/structure/types/manage/' . $this->type . '/fields'; - $this->drupalPostForm($url, $edit, t('Save')); + $url = 'admin/structure/types/manage/' . $this->type . '/add-field'; + $this->drupalPostForm($url, $edit, t('Save and continue')); $this->assertText(t('The machine-readable name is already in use. It must be unique.')); $this->assertUrl($url, array(), 'Stayed on the same page.'); diff --git a/core/modules/field_ui/tests/modules/field_ui_test/field_ui_test.info.yml b/core/modules/field_ui/tests/modules/field_ui_test/field_ui_test.info.yml deleted file mode 100644 index 9d7a5d1..0000000 --- a/core/modules/field_ui/tests/modules/field_ui_test/field_ui_test.info.yml +++ /dev/null @@ -1,10 +0,0 @@ -name: "Field UI tests" -type: module -description: "Support module for Field UI testing." -package: Testing -version: VERSION -core: 8.x - -dependencies: - - field_ui - - entity_test diff --git a/core/modules/field_ui/tests/modules/field_ui_test/src/Entity/FieldUITestNoBundle.php b/core/modules/field_ui/tests/modules/field_ui_test/src/Entity/FieldUITestNoBundle.php deleted file mode 100644 index ec75368..0000000 --- a/core/modules/field_ui/tests/modules/field_ui_test/src/Entity/FieldUITestNoBundle.php +++ /dev/null @@ -1,26 +0,0 @@ -