diff --git a/core/modules/field_ui/field_ui.js b/core/modules/field_ui/field_ui.js index 3000cd1..9153b19 100644 --- a/core/modules/field_ui/field_ui.js +++ b/core/modules/field_ui/field_ui.js @@ -9,7 +9,7 @@ Drupal.behaviors.fieldUIFieldOverview = { attach: function (context, settings) { - $(context).find('table#field-overview').once('field-overview', function () { + $(context).find('#field-overview').once('field-overview', function () { Drupal.fieldUIFieldOverview.attachUpdateSelects(this, settings); }); } @@ -32,7 +32,7 @@ Drupal.fieldUIFieldOverview = { // 'Field type' select updates its 'Widget' select. $table.find('.field-type-select').each(function () { var $this = $(this); - this.targetSelect = $this.closest('tr').find('.widget-type-select'); + this.targetSelect = $('.new-field-settings .widget-type-select'); $this.bind('change keyup', function () { var selectedFieldType = this.options[this.selectedIndex].value; @@ -48,7 +48,7 @@ Drupal.fieldUIFieldOverview = { // 'Existing field' select updates its 'Widget' select and 'Label' textfield. $table.find('.field-select').each(function () { var $this = $(this); - var $tr = $this.closest('tr'); + var $tr = $('.existing-field-settings'); this.targetSelect = $tr.find('.widget-type-select'); this.targetTextfield = $tr.find('.label-textfield'); this.targetTextfield diff --git a/core/modules/field_ui/lib/Drupal/field_ui/FieldOverview.php b/core/modules/field_ui/lib/Drupal/field_ui/FieldOverview.php index ec602ff..94be7fd 100644 --- a/core/modules/field_ui/lib/Drupal/field_ui/FieldOverview.php +++ b/core/modules/field_ui/lib/Drupal/field_ui/FieldOverview.php @@ -124,6 +124,18 @@ public function buildForm(array $form, array &$form_state, $entity_type = NULL, ), ); + $form['inline_actions'] = array( + '#prefix' => '', + ); + $form['inline_actions']['add'] = array( + '#theme' => 'menu_local_action', + '#link' => array( + 'href' => 'admin/structure/types/manage/' . $this->bundle . '/add-field', + 'title' => t('Add field'), + ), + ); + // Fields. foreach ($instances as $name => $instance) { $field = field_info_field($instance['field_name']); diff --git a/core/modules/field_ui/lib/Drupal/field_ui/FieldUI.php b/core/modules/field_ui/lib/Drupal/field_ui/FieldUI.php index 6e4a6a2..002db1a 100644 --- a/core/modules/field_ui/lib/Drupal/field_ui/FieldUI.php +++ b/core/modules/field_ui/lib/Drupal/field_ui/FieldUI.php @@ -33,4 +33,64 @@ public static function getNextDestination() { return $next_destination; } + /** + * returns an array of existing fields to be added to a bundle. + * + * @return array + * an array of existing fields keyed by field name. + */ + public static function getExistingFieldOptions($entity_type, $bundle) { + $info = array(); + $field_types = field_info_field_types(); + + foreach (field_info_instances() as $existing_entity_type => $bundles) { + foreach ($bundles as $existing_bundle => $instances) { + // No need to look in the current bundle. + if (!($existing_bundle == $bundle && $existing_entity_type == $entity_type)) { + foreach ($instances as $instance) { + $field = field_info_field($instance['field_name']); + // Don't show + // - locked fields, + // - fields already in the current bundle, + // - fields that cannot be added to the entity type, + // - fields that should not be added via user interface. + + if (empty($field['locked']) + && !field_info_instance($entity_type, $field['field_name'], $bundle) + && (empty($field['entity_types']) || in_array($entity_type, $field['entity_types'])) + && empty($field_types[$field['type']]['no_ui'])) { + $widget = entity_get_form_display($instance['entity_type'], $instance['bundle'], 'default')->getComponent($instance['field_name']); + $info[$instance['field_name']] = array( + 'type' => $field['type'], + 'type_label' => $field_types[$field['type']]['label'], + 'field' => $field['field_name'], + 'label' => $instance['label'], + 'widget_type' => $widget['type'], + ); + } + } + } + } + } + return $info; + } + + /** + * Checks if a field machine name is taken. + * + * @param string $value + * The machine name, not prefixed with 'field_'. + * + * @return bool + * Whether or not the field machine name is taken. + */ + public static function fieldNameExists($value) { + // Prefix with 'field_'. + $field_name = 'field_' . $value; + + // We need to check inactive fields as well, so we can't use + // field_info_fields(). + return (bool) field_read_fields(array('field_name' => $field_name), array('include_inactive' => TRUE)); + } + } diff --git a/core/modules/field_ui/lib/Drupal/field_ui/Form/FieldAddForm.php b/core/modules/field_ui/lib/Drupal/field_ui/Form/FieldAddForm.php new file mode 100644 index 0000000..65d4196 --- /dev/null +++ b/core/modules/field_ui/lib/Drupal/field_ui/Form/FieldAddForm.php @@ -0,0 +1,560 @@ +entityManager = $entity_manager; + $this->widgetManager = $widget_manager; + } + + /** + * {@inheritdoc} + */ + public function getFormID() { + return 'field_ui_field_add_form'; + } + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('plugin.manager.entity'), + $container->get('plugin.manager.field.widget') + ); + } + + /** + * {@inheritdoc} + */ + public function buildForm(array $form, array &$form_state, $entity_type = NULL, $bundle = NULL, $form_mode = NULL) { + $entity_info = $this->entityManager->getDefinition($entity_type); + if (!empty($entity_info['bundle_prefix'])) { + $bundle = $entity_info['bundle_prefix'] . $bundle; + } + + $this->entity_type = $entity_type; + $this->bundle = $bundle; + $this->mode = $form_mode; + $this->adminPath = $this->entityManager->getAdminPath($this->entity_type, $this->bundle); + + $form['#attributes'] = array( + 'class' => array('field-ui-overview'), + 'id' => 'field-overview', + ); + + // Gather bundle information. + $instances = field_info_instances($entity_type, $bundle); + $field_types = field_info_field_types(); + $extra_fields = field_info_extra_fields($entity_type, $bundle, 'form'); + $entity_form_display = entity_get_form_display($entity_type, $bundle, $form_mode); + + // Field prefix. + $field_prefix = config('field_ui.settings')->get('field_prefix'); + + drupal_set_title(t('Add a new field.')); + $form = array( + '#entity_type' => $this->entity_type, + '#bundle' => $this->bundle, + '#fields' => array_keys($instances), + '#extra' => array_keys($extra_fields), + '#tree' => TRUE, + '#attributes' => array( + 'class' => array('field-ui-overview'), + 'id' => 'field-overview', + ), + ); + + $max_weight = $entity_form_display->getHighestWeight(); + + // Prepare the widget types to be display as options. + $widget_options = $this->widgetManager->getOptions(); + $widget_type_options = array(); + foreach ($widget_options as $field_type => $widgets) { + $widget_type_options[$field_types[$field_type]['label']] = $widgets; + } + + // Gather valid field types. + $field_type_options = array(); + foreach ($field_types as $name => $field_type) { + // Skip field types which have no widget types, or should not be added via + // user interface. + if (isset($widget_options[$name]) && empty($field_type['no_ui'])) { + $field_type_options[$name] = $field_type['label']; + } + } + asort($field_type_options); + + $form['type'] = array( + '#type' => 'select', + '#title' => t('Type of new field'), + '#title_display' => 'invisible', + '#options' => $field_type_options, + '#empty_option' => t('- Select a field type -'), + '#description' => t('Type of data to store.'), + '#attributes' => array('class' => array('field-type-select')), + ); + + // Determine which options from the 'type' field already have existing + // fields and add them to the $states array so we can determine if the + // new-or-existing select list should be displayed. + $existing_fields = FieldUI::getExistingFieldOptions($this->entity_type, $this->bundle); + $states[] = array('value' => FALSE); + foreach ($existing_fields as $field) { + $states[] = array('value' => $field['type']); + } + + $form['new-or-existing'] = array( + '#type' => 'select', + '#title' => t('New or Existing'), + '#options' => array( + t('New'), + t('Existing'), + ), + '#empty_option' => t('- New or Existing -'), + '#states' => array( + 'visible' => array( + 'select[name="type"]' => $states, + ), + ), + ); + + if ($field_type_options && $widget_type_options) { + $states[] = array('value' => ''); + $name = '_add_new_field'; + $form[$name] = array( + '#type' => 'container', + '#attributes' => array('class' => array('new-field-settings')), + // Only show this field if a type has been selected and 'Existing' + // has not explicitly been selected. + '#states' => array( + 'invisible' => array( + 'select[name="type"]' => $states, + 'select[name="new-or-existing"]' => array('!value' => 0), + ), + ), + 'label' => array( + '#type' => 'textfield', + '#title' => t('New field label'), + '#size' => 15, + '#description' => t('Label'), + ), + 'weight' => array( + '#type' => 'textfield', + '#default_value' => $max_weight + 1, + '#size' => 3, + '#title' => t('Weight for new field'), + '#attributes' => array('class' => array('field-weight', 'element-invisible')), + ), + 'field_name' => array( + '#type' => 'machine_name', + '#title' => t('New field name'), + // This field should stay LTR even for RTL languages. + '#field_prefix' => '' . $field_prefix, + '#field_suffix' => '‎', + '#size' => 15, + '#description' => 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' => Field::ID_MAX_LENGTH - strlen($field_prefix), + '#machine_name' => array( + 'source' => array($name, 'label'), + 'exists' => array(new FieldUI, 'fieldNameExists'), + 'standalone' => TRUE, + 'label' => '', + ), + '#required' => FALSE, + ), + 'widget_type' => array( + '#type' => 'select', + '#title' => t('Widget for new field'), + '#options' => $widget_type_options, + '#empty_option' => t('- Select a widget -'), + '#description' => t('Form element to edit the data.'), + '#attributes' => array('class' => array('widget-type-select')), + '#prefix' => '
', + '#suffix' => '
', + ), + // 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' => FALSE, + ), + ); + } + + // Additional row: re-use existing field. + if ($existing_fields && $widget_type_options) { + // Build list of options. + $existing_field_options = array(); + foreach ($existing_fields as $field_name => $info) { + $text = t('@type: @field (@label)', array( + '@type' => $info['type_label'], + '@label' => $info['label'], + '@field' => $info['field'], + )); + $existing_field_options[$field_name] = truncate_utf8($text, 80, FALSE, TRUE); + } + asort($existing_field_options); + $name = '_add_existing_field'; + $form[$name] = array( + '#type' => 'container', + '#attributes' => array('class' => array('existing-field-settings')), + // Only show this field if 'Existing' has explicitly been selected. + '#states' => array( + 'invisible' => array( + 'select[name="new-or-existing"]' => array('!value' => 1), + ), + ), + 'field_name' => array( + '#type' => 'select', + '#title' => t('Existing field to share'), + '#options' => $existing_field_options, + '#empty_option' => t('- Select an existing field -'), + '#description' => t('Field to share'), + '#attributes' => array('class' => array('field-select')), + ), + 'label' => array( + '#type' => 'textfield', + '#title' => t('Existing field label'), + '#size' => 15, + '#description' => t('Label'), + '#attributes' => array('class' => array('label-textfield')), + ), + 'widget_type' => array( + '#type' => 'select', + '#title' => t('Widget for existing field'), + '#options' => $widget_type_options, + '#empty_option' => t('- Select a widget -'), + '#description' => t('Form element to edit the data.'), + '#attributes' => array('class' => array('widget-type-select')), + ), + 'weight' => array( + '#type' => 'textfield', + '#default_value' => $max_weight + 2, + '#size' => 3, + '#title' => t('Weight for added field'), + '#attributes' => array('class' => array('field-weight', 'element-invisible')), + ), + ); + } + + // This key is used to store the current updated field. + $form_state += array( + 'formatter_settings_edit' => NULL, + ); + + $form['actions'] = array('#type' => 'actions'); + $form['actions']['submit'] = array('#type' => 'submit', '#value' => t('Save field settings')); + + $form['#attached']['library'][] = array('field_ui', 'drupal.field_ui'); + + // Add settings for the update selects behavior. + $js_fields = array(); + foreach ($existing_fields as $field_name => $info) { + $js_fields[$field_name] = array('label' => $info['label'], 'type' => $info['type'], 'widget' => $info['widget_type']); + } + + $form['#attached']['js'][] = array( + 'type' => 'setting', + 'data' => array('fields' => $js_fields, 'fieldWidgetTypes' => $widget_options), + ); + + return $form; + } + + /** + * {@inheritdoc} + */ + public function validateForm(array &$form, array &$form_state) { + $this->validateAddNew($form, $form_state); + $this->validateAddExisting($form, $form_state); + } + + /** + * Validates the 'add new field' field settings. + * + * @param array $form + * An associative array containing the structure of the form. + * @param array $form_state + * A reference to a keyed array containing the current state of the form. + * + * @see Drupal\field_ui\FieldOverview::validateForm() + */ + protected function validateAddNew(array $form, array &$form_state) { + if (!($form_state['values']['new-or-existing'])) { + $field = $form_state['values']['_add_new_field']; + $value = $form_state['values']; + + // Missing label. + if (!$field['label']) { + form_set_error('fields][_add_new_field:][label', t('Add new field: you need to provide a label.')); + } + + // Missing field name.jk + if (!$field['field_name']) { + form_set_error('fields][_add_new_field][field_name', t('Add new field: you need to provide a field name.')); + } + // Field name validation. + else { + $field_name = $field['field_name']; + + // Add the 'field_' prefix. + $field_name = 'field_' . $field_name; + form_set_value($form['_add_new_field']['field_name'], $field_name, $form_state); + } + + // Missing field type. + if (!$form_state['values']['type']) { + form_set_error('type', t('Add new field: you need to select a field type.')); + } + + // Missing widget type. + if (!$field['widget_type']) { + form_set_error('_add_new_field][widget_type', t('Add new field: you need to select a widget.')); + } + // Wrong widget type. + elseif ($form_state['values']['type']) { + $widget_types = $this->widgetManager->getOptions($form_state['values']['type']); + if (!isset($widget_types[$field['widget_type']])) { + form_set_error('_add_new_field][widget_type', t('Add new field: invalid widget.')); + } + } + } + } + + /** + * Validates the 're-use existing field' field settings. + * + * @param array $form + * An associative array containing the structure of the form. + * @param array $form_state + * A reference to a keyed array containing the current state of the form. + * + * @see Drupal\field_ui\FieldOverview::validate() + */ + protected function validateAddExisting(array $form, array &$form_state) { + // The form element might be absent if no existing fields can be added to + // this bundle. + if (isset($form_state['values']['_add_existing_field'])) { + $field = $form_state['values']['_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'], $field['widget_type']))) { + // Missing label. + if (!$field['label']) { + form_set_error('fields][_add_existing_field][label', t('Re-use existing field: you need to provide a label.')); + } + + // Missing existing field name. + if (!$field['field_name']) { + form_set_error('fields][_add_existing_field][field_name', t('Re-use existing field: you need to select a field.')); + } + + // Missing widget type. + if (!$field['widget_type']) { + form_set_error('fields][_add_existing_field][widget_type', t('Re-use existing field: you need to select a widget.')); + } + // Wrong widget type. + elseif ($field['field_name'] && ($existing_field = field_info_field($field['field_name']))) { + $widget_types = $this->widgetManager->getOptions($existing_field['type']); + if (!isset($widget_types[$field['widget_type']])) { + form_set_error('fields][_add_existing_field][widget_type', t('Re-use existing field: invalid widget.')); + } + } + } + } + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, array &$form_state) { + $form_values = $form_state['values']; + $entity_form_display = entity_get_form_display($this->entity_type, $this->bundle, $this->mode); + + // Save the form display. + $entity_form_display->save(); + + $destinations = array(); + + // Create new field. + if (!empty($form_state['values']['_add_new_field']['field_name'])) { + $values = $form_values['_add_new_field']; + + $field = array( + 'field_name' => $values['field_name'], + 'type' => $form_state['values']['type'], + 'translatable' => $values['translatable'], + ); + $instance = array( + 'field_name' => $field['field_name'], + 'entity_type' => $this->entity_type, + 'bundle' => $this->bundle, + 'label' => $values['label'], + ); + + // Create the field and instance. + try { + $this->entityManager->getStorageController('field_entity')->create($field)->save(); + $new_instance = $this->entityManager->getStorageController('field_instance')->create($instance); + $new_instance->save(); + + // Make sure the field is displayed in the 'default' form mode (using + // the configured widget and default settings). + entity_get_form_display($this->entity_type, $this->bundle, 'default') + ->setComponent($field['field_name'], array( + 'type' => $values['widget_type'], + 'weight' => $values['weight'], + )) + ->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['field_name']) + ->save(); + + // Always show the field settings step, as the cardinality needs to be + // configured for new fields. + $destinations[] = $this->adminPath. '/fields/' . $new_instance->id() . '/field'; + $destinations[] = $this->adminPath . '/fields/' . $new_instance->id(); + + // Store new field information for any additional submit handlers. + $form_state['fields_added']['_add_new_field'] = $field['field_name']; + } + catch (\Exception $e) { + drupal_set_message(t('There was a problem creating field %label: !message', array('%label' => $instance['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 = field_info_field($values['field_name']); + if (!empty($field['locked'])) { + drupal_set_message(t('The field %label cannot be added because it is locked.', array('%label' => $values['label'])), 'error'); + } + else { + $instance = array( + 'field_name' => $field['field_name'], + 'entity_type' => $this->entity_type, + 'bundle' => $this->bundle, + 'label' => $values['label'], + ); + + try { + $new_instance = $this->entityManager->getStorageController('field_instance')->create($instance); + $new_instance->save(); + + // Make sure the field is displayed in the 'default' form mode (using + // the configured widget and default settings). + entity_get_form_display($this->entity_type, $this->bundle, 'default') + ->setComponent($field['field_name'], array( + 'type' => $values['widget_type'], + 'weight' => $values['weight'], + )) + ->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['field_name']) + ->save(); + + $destinations[] = $this->adminPath . '/fields/' . $new_instance->id(); + // Store new field information for any additional submit handlers. + $form_state['fields_added']['_add_existing_field'] = $instance['field_name']; + } + catch (\Exception $e) { + drupal_set_message(t('There was a problem creating field instance %label: @message.', array('%label' => $instance['label'], '@message' => $e->getMessage())), 'error'); + } + } + } + + if ($destinations) { + $destination = drupal_get_destination(); + $destinations[] = $destination['destination']; + unset($_GET['destination']); + $path = array_shift($destinations); + $options = drupal_parse_url($path); + $options['query']['destinations'] = $destinations; + $form_state['redirect'] = array($options['path'], $options); + } + else { + drupal_set_message(t('Your settings have been saved.')); + } + } + +} diff --git a/core/modules/field_ui/lib/Drupal/field_ui/Routing/RouteSubscriber.php b/core/modules/field_ui/lib/Drupal/field_ui/Routing/RouteSubscriber.php index 55477a0..82971d9 100644 --- a/core/modules/field_ui/lib/Drupal/field_ui/Routing/RouteSubscriber.php +++ b/core/modules/field_ui/lib/Drupal/field_ui/Routing/RouteSubscriber.php @@ -100,6 +100,13 @@ public function routes(RouteBuildEvent $event) { ); $collection->add("field_ui.display_overview.$entity_type", $route); + $route = new Route( + "$path/add-field", + array('_form' => '\Drupal\field_ui\Form\FieldAddForm') + $defaults, + array('_permission' => 'administer ' . $entity_type . ' fields') + ); + $collection->add("field_ui.add.$entity_type", $route); + foreach (entity_get_view_modes($entity_type) as $view_mode => $view_mode_info) { $route = new Route( "$path/display/$view_mode",