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 d2208a1..ea82ec7 100644 --- a/core/modules/field_ui/lib/Drupal/field_ui/FieldOverview.php +++ b/core/modules/field_ui/lib/Drupal/field_ui/FieldOverview.php @@ -116,6 +116,19 @@ public function buildForm(array $form, array &$form_state, $entity_type = NULL, ), ); + // @todo + $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']); @@ -167,114 +180,6 @@ public function buildForm(array $form, array &$form_state, $entity_type = NULL, } } - // 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' => t('New field label'), - '#title_display' => 'invisible', - '#size' => 15, - '#description' => t('Label'), - '#prefix' => '
' . t('Add new field') .'
', - '#suffix' => '
', - ), - 'field_name' => array( - '#type' => 'machine_name', - '#title' => 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' => 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), - '#prefix' => '
 
', - '#machine_name' => array( - 'source' => array('fields', $name, 'label'), - 'exists' => array($this, 'fieldNameExists'), - 'standalone' => TRUE, - 'label' => '', - ), - '#required' => FALSE, - ), - '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')), - '#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' => FALSE, - ), - ); - } - - // Additional row: re-use existing field. - $existing_fields = $this->getExistingFieldOptions(); - if ($existing_fields) { - // 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'; - $table[$name] = array( - '#attributes' => array('class' => array('add-new')), - '#row_type' => 'add_new_field', - '#region_callback' => array($this, 'getRowRegion'), - 'label' => array( - '#type' => 'textfield', - '#title' => t('Existing field label'), - '#title_display' => 'invisible', - '#size' => 15, - '#description' => t('Label'), - '#attributes' => array('class' => array('label-textfield')), - '#prefix' => '
' . t('Re-use existing field') .'
', - '#suffix' => '
', - ), - 'field_name' => array( - '#type' => 'select', - '#title' => t('Existing field to share'), - '#title_display' => 'invisible', - '#options' => $existing_field_options, - '#empty_option' => t('- Select an existing field -'), - '#description' => 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. 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..9bbd23a 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,5 @@ public static function getNextDestination() { return $next_destination; } + } 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..5496ed4 --- /dev/null +++ b/core/modules/field_ui/lib/Drupal/field_ui/Form/FieldAddForm.php @@ -0,0 +1,525 @@ +entityManager = $entity_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') + ); + } + + /** + * {@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-display-overview', + ); + + // Gather bundle information. + $instances = \Drupal::service('field.info')->getBundleInstances($entity_type, $bundle); + $field_types = \Drupal::service('plugin.manager.entity.field.field_type')->getDefinitions(); + $extra_fields = \Drupal\field\Field::fieldInfo()->getBundleExtraFields($entity_type, $bundle); + $extra_fields = isset($extra_fields['form']) ? $extra_fields['form'] : array(); + + // Field prefix. + $field_prefix = \Drupal::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-display-overview', + ), + ); + + // 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); + + $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 = $this->getExistingFieldOptions(); + $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) { + $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'), + ), + '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($this, 'fieldNameExists'), + 'standalone' => TRUE, + 'label' => '', + ), + '#required' => FALSE, + ), + // 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) { + // 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')), + ), + ); + } + + // 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')); + + 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' row. + * + * @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) { + $field = $form_state['values']['_add_new_field']; + $values = $form_state['values']; + + // Validate if any information was provided in the 'add new field' row. + if (array_filter(array($field['label'], $field['field_name'], $values['type']))) { + // Missing label. + if (!$field['label']) { + form_set_error('_add_new_field][label', t('Add new field: you need to provide a label.')); + } + + // Missing field name. + if (!$field['field_name']) { + form_set_error('_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_prefix = \Drupal::config('field_ui.settings')->get('field_prefix'); + $field_name = $field_prefix . $field_name; + form_set_value($form['_add_new_field']['field_name'], $field_name, $form_state); + } + + // Missing field type. + if (!$values['type']) { + form_set_error('_add_new_field][type', 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 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']))) { + // Missing label. + if (!$field['label']) { + form_set_error('_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('_add_existing_field][field_name', t('Re-use existing field: you need to select a field.')); + } + } + } + } + + /** + * Overrides \Drupal\field_ui\OverviewBase::submitForm(). + */ + public function submitForm(array &$form, array &$form_state) { + $destinations = array(); + + // Create new field. + if (!empty($form_state['values']['_add_new_field']['field_name'])) { + $values = $form_state['values']['_add_new_field']; + + $field = array( + 'field_name' => $form_state['values']['_add_new_field']['field_name'], + 'type' => $form_state['values']['type'], + 'translatable' => $form_state['values']['_add_new_field']['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 + // 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['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['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_state['values']['_add_existing_field']['field_name'])) { + $values = $form_state['values']['_add_existing_field']; + $field = \Drupal\field\Field::fieldInfo()->getField($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 + // 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['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['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.')); + } + } + + /** + * Returns an array of existing fields to be added to a bundle. + * + * @return array + * An array of existing fields keyed by field name. + */ + protected function getExistingFieldOptions() { + $info = array(); + $field_types = \Drupal::service('plugin.manager.entity.field.field_type')->getDefinitions(); + + $instances = \Drupal\field\Field::fieldInfo()->getInstances($this->entity_type); + foreach ($instances as $bundle => $instance) { + foreach ($instance as $field_name => $instance_settings) { + if (!($instance_settings->bundle == $this->bundle && $instance_settings->entity_type == $this->entity_type)) { + $field = \Drupal\field\Field::fieldInfo()->getField($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) + && !\Drupal\field\Field::fieldInfo()->getInstance($this->entity_type, $this->bundle, $field->id) + && !empty($field_types[$field->type]['no_ui'])) { + $info[$instance_settings['field_name']] = array( + 'type' => $field->type, + 'type_label' => $field_types[$field->type]['label'], + 'field' => $field->id, + 'label' => $instance_settings['label'], + ); + } + } + } + } + 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 function fieldNameExists($value) { + // Get the configured field prefix. + $field_prefix = \Drupal::config('field_ui.settings')->get('field_prefix'); + + // We need to check inactive fields as well, so we can't use + // field_info_fields(). + return (bool) \Drupal::entityManager() + ->getStorageController('field_entity') + ->loadByProperties(array( + 'field_name' => $field_prefix . $value, + 'include_inactive' => TRUE, + 'include_deleted' => TRUE) + ); + } + +} 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 e994202..cf50aa8 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 @@ -111,6 +111,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",