diff --git a/core/core.field_type_categories.yml b/core/core.field_type_categories.yml new file mode 100644 index 0000000000..ea4031d298 --- /dev/null +++ b/core/core.field_type_categories.yml @@ -0,0 +1,19 @@ +plain_text: + label: 'Plain text' + description: 'Text field that does not support markup.' + weight: -50 +number: + label: 'Number' + description: 'Field to store number. I.e. id, price, or quantity.' + weight: -40 +reference: + label: 'Reference' + description: 'Field to reference other content.' + weight: -30 +date_time: + label: 'Date and time' + description: 'Field to store date and time values.' + weight: -10 +general: + label: 'General' + class: \Drupal\Core\Field\FallbackFieldTypeCategory diff --git a/core/core.services.yml b/core/core.services.yml index ce1b47bdc9..1f1b580cd3 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -738,8 +738,12 @@ services: Drupal\Core\Block\BlockManagerInterface: '@plugin.manager.block' plugin.manager.field.field_type: class: Drupal\Core\Field\FieldTypePluginManager - arguments: ['@container.namespaces', '@cache.discovery', '@module_handler', '@typed_data_manager'] + arguments: ['@container.namespaces', '@cache.discovery', '@module_handler', '@typed_data_manager', '@plugin.manager.field.field_type_category'] Drupal\Core\Field\FieldTypePluginManagerInterface: '@plugin.manager.field.field_type' + plugin.manager.field.field_type_category: + class: \Drupal\Core\Field\FieldTypeCategoryManager + arguments: [ '%app.root%', '@module_handler', '@cache.discovery' ] + Drupal\Core\Field\FieldTypeCategoryManagerInterface: '@plugin.manager.field.field_type_category' plugin.manager.field.widget: class: Drupal\Core\Field\WidgetPluginManager arguments: ['@container.namespaces', '@cache.discovery', '@module_handler', '@plugin.manager.field.field_type'] diff --git a/core/lib/Drupal/Core/Field/Annotation/FieldType.php b/core/lib/Drupal/Core/Field/Annotation/FieldType.php index 8bf54cb585..4dcb51ee60 100644 --- a/core/lib/Drupal/Core/Field/Annotation/FieldType.php +++ b/core/lib/Drupal/Core/Field/Annotation/FieldType.php @@ -57,6 +57,13 @@ class FieldType extends DataType { */ public $category = ''; + /** + * The weight of the field type. + * + * @var int + */ + public $weight = 0; + /** * The plugin_id of the default widget for this field type. * diff --git a/core/lib/Drupal/Core/Field/FallbackFieldTypeCategory.php b/core/lib/Drupal/Core/Field/FallbackFieldTypeCategory.php new file mode 100644 index 0000000000..d8319d28d5 --- /dev/null +++ b/core/lib/Drupal/Core/Field/FallbackFieldTypeCategory.php @@ -0,0 +1,23 @@ + $configuration['label'] ?? '', + 'description' => $configuration['description'] ?? '', + 'weight' => $configuration['weight'] ?? 0, + ] + $plugin_definition; + parent::__construct($configuration, $plugin_id, $plugin_definition); + } + +} diff --git a/core/lib/Drupal/Core/Field/FieldConfigBase.php b/core/lib/Drupal/Core/Field/FieldConfigBase.php index b58f49dc69..ca9a5b5285 100644 --- a/core/lib/Drupal/Core/Field/FieldConfigBase.php +++ b/core/lib/Drupal/Core/Field/FieldConfigBase.php @@ -279,6 +279,13 @@ public function postCreate(EntityStorageInterface $storage) { if (empty($this->field_type)) { $this->field_type = $this->getFieldStorageDefinition()->getType(); } + + // Make sure all expected runtime settings are present. + $default_settings = \Drupal::service('plugin.manager.field.field_type') + ->getDefaultFieldSettings($this->getType()); + // Filter out any unknown (unsupported) settings. + $supported_settings = array_intersect_key($this->getSettings(), $default_settings); + $this->set('settings', $supported_settings + $default_settings); } /** @@ -461,10 +468,12 @@ public function setDefaultValueCallback($callback) { * @todo Investigate in https://www.drupal.org/node/1977206. */ public function __sleep() { + $properties = get_object_vars($this); + // Only serialize necessary properties, excluding those that can be // recalculated. - $properties = get_object_vars($this); - unset($properties['fieldStorage'], $properties['itemDefinition'], $properties['original']); + unset($properties['itemDefinition'], $properties['original']); + return array_keys($properties); } diff --git a/core/lib/Drupal/Core/Field/FieldItemList.php b/core/lib/Drupal/Core/Field/FieldItemList.php index 33b3a37ef0..c1d3b70edb 100644 --- a/core/lib/Drupal/Core/Field/FieldItemList.php +++ b/core/lib/Drupal/Core/Field/FieldItemList.php @@ -370,7 +370,17 @@ protected function defaultValueWidget(FormStateInterface $form_state) { ]); } else { - $widget = $field_widget_plugin_manager->getInstance(['field_definition' => $definition]); + $options = [ + 'field_definition' => $this->getFieldDefinition(), + ]; + // If the field does not have a widget configured in the 'default' form + // mode, check if there are default entity form display options defined + // for the 'default' form mode in the form state. + // @see \Drupal\field_ui\Controller\FieldConfigAddController::fieldConfigAddConfigureForm + if (($default_options = $form_state->get('default_options')) && isset($default_options['entity_form_display']['default'])) { + $options['configuration'] = $default_options['entity_form_display']['default']; + } + $widget = $field_widget_plugin_manager->getInstance($options); } $form_state->set('default_value_widget', $widget); diff --git a/core/lib/Drupal/Core/Field/FieldTypeCategory.php b/core/lib/Drupal/Core/Field/FieldTypeCategory.php new file mode 100644 index 0000000000..168616102e --- /dev/null +++ b/core/lib/Drupal/Core/Field/FieldTypeCategory.php @@ -0,0 +1,43 @@ +pluginDefinition['label']; + } + + /** + * {@inheritdoc} + */ + public function getDescription(): TranslatableMarkup { + return $this->pluginDefinition['description']; + } + + /** + * {@inheritdoc} + */ + public function getWeight(): int { + return $this->pluginDefinition['weight']; + } + + /** + * {@inheritdoc} + */ + public function getLibraries(): array { + return $this->pluginDefinition['libraries'] ?? []; + } + +} diff --git a/core/lib/Drupal/Core/Field/FieldTypeCategoryInterface.php b/core/lib/Drupal/Core/Field/FieldTypeCategoryInterface.php new file mode 100644 index 0000000000..3727df1bb7 --- /dev/null +++ b/core/lib/Drupal/Core/Field/FieldTypeCategoryInterface.php @@ -0,0 +1,44 @@ + '', + 'description' => '', + 'weight' => 0, + 'class' => FieldTypeCategory::class, + ]; + + /** + * Constructs a new FieldTypeCategoryManager. + * + * @param string $root + * The app root. + * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler + * The module handler. + * @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend + * The cache backend. + */ + public function __construct(protected readonly string $root, ModuleHandlerInterface $module_handler, CacheBackendInterface $cache_backend) { + $this->moduleHandler = $module_handler; + $this->alterInfo('field_type_category_info'); + $this->setCacheBackend($cache_backend, 'field_type_category_info_plugins'); + } + + /** + * {@inheritdoc} + */ + protected function getDiscovery(): YamlDiscovery { + if (!isset($this->discovery)) { + $directories = ['core' => $this->root . '/core']; + $directories += $this->moduleHandler->getModuleDirectories(); + $this->discovery = new YamlDiscovery('field_type_categories', $directories); + $this->discovery + ->addTranslatableProperty('label') + ->addTranslatableProperty('description'); + } + return $this->discovery; + } + + /** + * {@inheritdoc} + */ + public function getFallbackPluginId($plugin_id, array $configuration = []): string { + return FieldTypeCategoryManagerInterface::FALLBACK_CATEGORY; + } + +} diff --git a/core/lib/Drupal/Core/Field/FieldTypeCategoryManagerInterface.php b/core/lib/Drupal/Core/Field/FieldTypeCategoryManagerInterface.php new file mode 100644 index 0000000000..840e31a897 --- /dev/null +++ b/core/lib/Drupal/Core/Field/FieldTypeCategoryManagerInterface.php @@ -0,0 +1,17 @@ +alterInfo('field_info'); $this->setCacheBackend($cache_backend, 'field_types_plugins'); $this->typedDataManager = $typed_data_manager; + if ($this->fieldTypeCategoryManager === NULL) { + @trigger_error('Calling FieldTypePluginManager::__construct() without the $fieldTypeCategoryManager argument is deprecated in drupal:10.2.0 and will be required in drupal:11.0.0. See https://www.drupal.org/node/3375737', E_USER_DEPRECATED); + $this->fieldTypeCategoryManager = \Drupal::service('plugin.manager.field.field_type_category'); + } } /** @@ -91,9 +100,13 @@ public function processDefinition(&$definition, $plugin_id) { $definition['list_class'] = '\Drupal\Core\Field\FieldItemList'; } - // Ensure that every field type has a category. - if (empty($definition['category'])) { - $definition['category'] = $this->t('General'); + if ($definition['category'] instanceof TranslatableMarkup) { + @trigger_error('Using a translatable string as a category for field type is deprecated in drupal:10.2.0 and is removed from drupal:11.0.0. See https://www.drupal.org/node/3364271', E_USER_DEPRECATED); + $definition['category'] = FieldTypeCategoryManagerInterface::FALLBACK_CATEGORY; + } + elseif (empty($definition['category'])) { + // Ensure that every field type has a category. + $definition['category'] = FieldTypeCategoryManagerInterface::FALLBACK_CATEGORY; } } @@ -145,6 +158,48 @@ public function getFieldSettingsSummary(FieldDefinitionInterface $field_definiti return []; } + /** + * Gets sorted field type definitions grouped by category. + * + * In addition to grouping, both categories and its entries are sorted, + * whereas plugin definitions are sorted by label. + * + * @param array[]|null $definitions + * (optional) The plugin definitions to group. If omitted, all plugin + * definitions are used. + * @param string $label_key + * (optional) The array key to use as the label of the field type. + * @param string $category_label_key + * (optional) The array key to use as the label of the category. + * + * @return array[] + * Keys are category names, and values are arrays of which the keys are + * plugin IDs and the values are plugin definitions. + */ + public function getGroupedDefinitions(array $definitions = NULL, $label_key = 'label', $category_label_key = 'label') { + $grouped_categories = $this->getGroupedDefinitionsTrait($definitions, $label_key); + $category_info = $this->fieldTypeCategoryManager->getDefinitions(); + + // Ensure that all the referenced categories exist. + foreach ($grouped_categories as $group => $definitions) { + if (!isset($category_info[$group])) { + assert(FALSE, "\"$group\" must be defined in MODULE_NAME.field_type_categories.yml"); + if (!isset($grouped_categories[FieldTypeCategoryManagerInterface::FALLBACK_CATEGORY])) { + $grouped_categories[FieldTypeCategoryManagerInterface::FALLBACK_CATEGORY] = []; + } + $grouped_categories[FieldTypeCategoryManagerInterface::FALLBACK_CATEGORY] += $definitions; + unset($grouped_categories[$group]); + } + } + + $normalized_grouped_categories = []; + foreach ($grouped_categories as $group => $definitions) { + $normalized_grouped_categories[(string) $category_info[$group][$category_label_key]] = $definitions; + } + + return $normalized_grouped_categories; + } + /** * {@inheritdoc} */ @@ -162,7 +217,7 @@ public function getUiDefinitions() { foreach ($this->getPreconfiguredOptions($definition['id']) as $key => $option) { $definitions["field_ui:$id:$key"] = array_intersect_key( $option, - ['label' => 0, 'category' => 1] + ['label' => 0, 'category' => 1, 'weight' => 1, 'description' => 0] ) + $definition; } } diff --git a/core/lib/Drupal/Core/Field/Plugin/Field/FieldType/BooleanItem.php b/core/lib/Drupal/Core/Field/Plugin/Field/FieldType/BooleanItem.php index 51a29d860b..b7b03cfd7d 100644 --- a/core/lib/Drupal/Core/Field/Plugin/Field/FieldType/BooleanItem.php +++ b/core/lib/Drupal/Core/Field/Plugin/Field/FieldType/BooleanItem.php @@ -17,7 +17,7 @@ * @FieldType( * id = "boolean", * label = @Translation("Boolean"), - * description = @Translation("An entity field containing a boolean value."), + * description = @Translation("Field to store a true or false value."), * default_widget = "boolean_checkbox", * default_formatter = "boolean", * ) diff --git a/core/lib/Drupal/Core/Field/Plugin/Field/FieldType/DecimalItem.php b/core/lib/Drupal/Core/Field/Plugin/Field/FieldType/DecimalItem.php index 15b7b65545..e6f123c816 100644 --- a/core/lib/Drupal/Core/Field/Plugin/Field/FieldType/DecimalItem.php +++ b/core/lib/Drupal/Core/Field/Plugin/Field/FieldType/DecimalItem.php @@ -14,8 +14,13 @@ * @FieldType( * id = "decimal", * label = @Translation("Number (decimal)"), - * description = @Translation("This field stores a number in the database in a fixed decimal format."), - * category = @Translation("Number"), + * description = { + * @Translation("Ideal for exact counts and measures (prices, temperatures, distances, volumes, etc.)"), + * @Translation("Stores a number in the database in a fixed decimal format"), + * @Translation("For example, 12.34 km or € when used for further detailed calculations (such as summing many of these)"), + * }, + * category = "number", + * weight = -30, * default_widget = "number", * default_formatter = "number_decimal" * ) diff --git a/core/lib/Drupal/Core/Field/Plugin/Field/FieldType/EmailItem.php b/core/lib/Drupal/Core/Field/Plugin/Field/FieldType/EmailItem.php index 59e918ae94..639d8d5e28 100644 --- a/core/lib/Drupal/Core/Field/Plugin/Field/FieldType/EmailItem.php +++ b/core/lib/Drupal/Core/Field/Plugin/Field/FieldType/EmailItem.php @@ -16,7 +16,7 @@ * @FieldType( * id = "email", * label = @Translation("Email"), - * description = @Translation("An entity field containing an email value."), + * description = @Translation("Field to store an email address."), * default_widget = "email_default", * default_formatter = "basic_string" * ) diff --git a/core/lib/Drupal/Core/Field/Plugin/Field/FieldType/EntityReferenceItem.php b/core/lib/Drupal/Core/Field/Plugin/Field/FieldType/EntityReferenceItem.php index b630ea5605..c9b2623233 100644 --- a/core/lib/Drupal/Core/Field/Plugin/Field/FieldType/EntityReferenceItem.php +++ b/core/lib/Drupal/Core/Field/Plugin/Field/FieldType/EntityReferenceItem.php @@ -36,7 +36,7 @@ * id = "entity_reference", * label = @Translation("Entity reference"), * description = @Translation("An entity field containing an entity reference."), - * category = @Translation("Reference"), + * category = "reference", * default_widget = "entity_reference_autocomplete", * default_formatter = "entity_reference_label", * list_class = "\Drupal\Core\Field\EntityReferenceFieldItemList", diff --git a/core/lib/Drupal/Core/Field/Plugin/Field/FieldType/FloatItem.php b/core/lib/Drupal/Core/Field/Plugin/Field/FieldType/FloatItem.php index 739a62c76e..2e120c0ad1 100644 --- a/core/lib/Drupal/Core/Field/Plugin/Field/FieldType/FloatItem.php +++ b/core/lib/Drupal/Core/Field/Plugin/Field/FieldType/FloatItem.php @@ -13,8 +13,13 @@ * @FieldType( * id = "float", * label = @Translation("Number (float)"), - * description = @Translation("This field stores a number in the database in a floating point format."), - * category = @Translation("Number"), + * description = { + * @Translation("In most instances, it is best to use Number (decimal) instead, as decimal numbers stored as floats may contain errors in precision"), + * @Translation("This type of field offers faster processing and more compact storage, but the differences are typically negligible on modern sites"), + * @Translation("For example, 123.4 km when used in imprecise contexts such as a walking trail distance"), + * }, + * category = "number", + * weight = -10, * default_widget = "number", * default_formatter = "number_decimal" * ) diff --git a/core/lib/Drupal/Core/Field/Plugin/Field/FieldType/IntegerItem.php b/core/lib/Drupal/Core/Field/Plugin/Field/FieldType/IntegerItem.php index 1ce7a4463f..dcb4275d9d 100644 --- a/core/lib/Drupal/Core/Field/Plugin/Field/FieldType/IntegerItem.php +++ b/core/lib/Drupal/Core/Field/Plugin/Field/FieldType/IntegerItem.php @@ -13,8 +13,12 @@ * @FieldType( * id = "integer", * label = @Translation("Number (integer)"), - * description = @Translation("This field stores a number in the database as an integer."), - * category = @Translation("Number"), + * description = { + * @Translation("Number without decimals"), + * @Translation("For example, 123"), + * }, + * category = "number", + * weight = -50, * default_widget = "number", * default_formatter = "number_integer" * ) diff --git a/core/lib/Drupal/Core/Field/Plugin/Field/FieldType/StringItem.php b/core/lib/Drupal/Core/Field/Plugin/Field/FieldType/StringItem.php index 10e216e1db..ebb867f370 100644 --- a/core/lib/Drupal/Core/Field/Plugin/Field/FieldType/StringItem.php +++ b/core/lib/Drupal/Core/Field/Plugin/Field/FieldType/StringItem.php @@ -13,8 +13,13 @@ * @FieldType( * id = "string", * label = @Translation("Text (plain)"), - * description = @Translation("A field containing a plain string value."), - * category = @Translation("Text"), + * description = { + * @Translation("Ideal for titles and names"), + * @Translation("Efficient storage for short text"), + * @Translation("Requires specifying a maximum length"), + * @Translation("Good for fields with known or predictable length"), + * }, + * category = "plain_text", * default_widget = "string_textfield", * default_formatter = "string" * ) diff --git a/core/lib/Drupal/Core/Field/Plugin/Field/FieldType/StringLongItem.php b/core/lib/Drupal/Core/Field/Plugin/Field/FieldType/StringLongItem.php index 93c6b27bcb..b98f76fbfb 100644 --- a/core/lib/Drupal/Core/Field/Plugin/Field/FieldType/StringLongItem.php +++ b/core/lib/Drupal/Core/Field/Plugin/Field/FieldType/StringLongItem.php @@ -12,8 +12,12 @@ * @FieldType( * id = "string_long", * label = @Translation("Text (plain, long)"), - * description = @Translation("A field containing a long string value."), - * category = @Translation("Text"), + * description = { + * @Translation("Ideal for longer texts, like body or description"), + * @Translation("Supports long text without specifying a maximum length"), + * @Translation("May use more storage and be slower for searching and sorting"), + * }, + * category = "plain_text", * default_widget = "string_textarea", * default_formatter = "basic_string", * ) diff --git a/core/lib/Drupal/Core/Field/Plugin/Field/FieldType/TimestampItem.php b/core/lib/Drupal/Core/Field/Plugin/Field/FieldType/TimestampItem.php index 5706ed72f8..44ae5a7f18 100644 --- a/core/lib/Drupal/Core/Field/Plugin/Field/FieldType/TimestampItem.php +++ b/core/lib/Drupal/Core/Field/Plugin/Field/FieldType/TimestampItem.php @@ -13,7 +13,12 @@ * @FieldType( * id = "timestamp", * label = @Translation("Timestamp"), - * description = @Translation("An entity field containing a UNIX timestamp value."), + * description = { + * @Translation("Ideal for using date and time calculations or comparisons"), + * @Translation("Date and time stored in the form of seconds since January 1, 1970 (UTC)"), + * @Translation("Compact and efficient for storage, sorting and calculations"), + * }, + * category = "date_time", * default_widget = "datetime_timestamp", * default_formatter = "timestamp", * constraints = { diff --git a/core/misc/cspell/dictionary.txt b/core/misc/cspell/dictionary.txt index d8c3c05c79..1db962a633 100644 --- a/core/misc/cspell/dictionary.txt +++ b/core/misc/cspell/dictionary.txt @@ -1108,6 +1108,7 @@ subforms subjectkeyword subkey subkeys +suboption subparse subplugins subproject diff --git a/core/misc/icons/55565b/boolean.svg b/core/misc/icons/55565b/boolean.svg new file mode 100644 index 0000000000..1667c68bdf --- /dev/null +++ b/core/misc/icons/55565b/boolean.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/core/misc/icons/55565b/comment.svg b/core/misc/icons/55565b/comment.svg new file mode 100644 index 0000000000..e80ec00abe --- /dev/null +++ b/core/misc/icons/55565b/comment.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/core/misc/icons/55565b/date_and_time.svg b/core/misc/icons/55565b/date_and_time.svg new file mode 100644 index 0000000000..00af00670e --- /dev/null +++ b/core/misc/icons/55565b/date_and_time.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/core/misc/icons/55565b/daterange.svg b/core/misc/icons/55565b/daterange.svg new file mode 100644 index 0000000000..8b852f0d95 --- /dev/null +++ b/core/misc/icons/55565b/daterange.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/core/misc/icons/55565b/email.svg b/core/misc/icons/55565b/email.svg new file mode 100644 index 0000000000..331f7f99cc --- /dev/null +++ b/core/misc/icons/55565b/email.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/core/misc/icons/55565b/file_upload.svg b/core/misc/icons/55565b/file_upload.svg new file mode 100644 index 0000000000..4f3ed73ea1 --- /dev/null +++ b/core/misc/icons/55565b/file_upload.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/core/misc/icons/55565b/formatted_text.svg b/core/misc/icons/55565b/formatted_text.svg new file mode 100644 index 0000000000..6492eb303f --- /dev/null +++ b/core/misc/icons/55565b/formatted_text.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/core/misc/icons/55565b/link.svg b/core/misc/icons/55565b/link.svg new file mode 100644 index 0000000000..7d8701b1e4 --- /dev/null +++ b/core/misc/icons/55565b/link.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/core/misc/icons/55565b/media.svg b/core/misc/icons/55565b/media.svg new file mode 100644 index 0000000000..cec884d838 --- /dev/null +++ b/core/misc/icons/55565b/media.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/core/misc/icons/55565b/number.svg b/core/misc/icons/55565b/number.svg new file mode 100644 index 0000000000..c3f693570c --- /dev/null +++ b/core/misc/icons/55565b/number.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/core/misc/icons/55565b/plain_text.svg b/core/misc/icons/55565b/plain_text.svg new file mode 100644 index 0000000000..8547f05fa0 --- /dev/null +++ b/core/misc/icons/55565b/plain_text.svg @@ -0,0 +1,3 @@ + + + diff --git a/core/misc/icons/55565b/reference.svg b/core/misc/icons/55565b/reference.svg new file mode 100644 index 0000000000..471932ae95 --- /dev/null +++ b/core/misc/icons/55565b/reference.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/core/misc/icons/55565b/selection_list.svg b/core/misc/icons/55565b/selection_list.svg new file mode 100644 index 0000000000..c4e86388e4 --- /dev/null +++ b/core/misc/icons/55565b/selection_list.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/core/misc/icons/55565b/telephone.svg b/core/misc/icons/55565b/telephone.svg new file mode 100644 index 0000000000..b7f876bcad --- /dev/null +++ b/core/misc/icons/55565b/telephone.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/core/misc/icons/cacbd2/puzzle_piece_placeholder.svg b/core/misc/icons/cacbd2/puzzle_piece_placeholder.svg new file mode 100644 index 0000000000..715531ee9d --- /dev/null +++ b/core/misc/icons/cacbd2/puzzle_piece_placeholder.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/core/modules/comment/comment.libraries.yml b/core/modules/comment/comment.libraries.yml index 25c8151161..d1106ca8fc 100644 --- a/core/modules/comment/comment.libraries.yml +++ b/core/modules/comment/comment.libraries.yml @@ -36,3 +36,11 @@ drupal.node-new-comments-link: - core/once - core/drupal - history/api + +drupal.comment-icon: + version: VERSION + css: + theme: + css/comment.icon.theme.css: {} + dependencies: + - field_ui/drupal.field_ui.manage_fields diff --git a/core/modules/comment/comment.module b/core/modules/comment/comment.module index 0cbc859155..934f0bdbe2 100644 --- a/core/modules/comment/comment.module +++ b/core/modules/comment/comment.module @@ -16,6 +16,7 @@ use Drupal\comment\Plugin\Field\FieldType\CommentItemInterface; use Drupal\Core\Entity\Entity\EntityViewMode; use Drupal\Core\Entity\EntityInterface; +use Drupal\Core\Field\FieldTypeCategoryManagerInterface; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Routing\RouteMatchInterface; use Drupal\Core\Entity\Display\EntityViewDisplayInterface; @@ -238,12 +239,24 @@ function comment_form_field_ui_field_storage_add_form_alter(&$form, FormStateInt $form['#title'] = \Drupal::service('comment.manager')->getFieldUIPageTitle($route_match->getParameter('commented_entity_type'), $route_match->getParameter('field_name')); } if (!_comment_entity_uses_integer_id($form_state->get('entity_type_id'))) { - $optgroup = (string) t('General'); - // You cannot use comment fields on entity types with non-integer IDs. - unset($form['add']['new_storage_type']['#options'][$optgroup]['comment']); + $form['add']['new_storage_type']['#process'][] = 'comment_new_storage_type_process_callback'; } } +/** + * Process callback to remove comment type field option. + */ +function comment_new_storage_type_process_callback($element, &$form_state, $form) { + foreach ($element as $key => $value) { + if (isset($value['radio']['#return_value']) && $value['radio']['#return_value'] === 'comment') { + // You cannot use comment fields on entity types with non-integer IDs. + unset($element[$key]); + } + } + + return $element; +} + /** * Implements hook_form_FORM_ID_alter(). */ @@ -771,3 +784,14 @@ function comment_entity_view_display_presave(EntityViewDisplayInterface $display } } } + +/** + * Implements hook_field_type_category_info_alter(). + */ +function comment_field_type_category_info_alter(&$definitions) { + // TRICKY: the `comment` field type belongs in the `general` category, so the + // libraries need to be attached using an alter hook. + if (array_key_exists(FieldTypeCategoryManagerInterface::FALLBACK_CATEGORY, $definitions)) { + $definitions[FieldTypeCategoryManagerInterface::FALLBACK_CATEGORY]['libraries'][] = 'comment/drupal.comment-icon'; + } +} diff --git a/core/modules/comment/css/comment.icon.theme.css b/core/modules/comment/css/comment.icon.theme.css new file mode 100644 index 0000000000..bbeb16aabe --- /dev/null +++ b/core/modules/comment/css/comment.icon.theme.css @@ -0,0 +1,10 @@ +/* + * DO NOT EDIT THIS FILE. + * See the following change record for more information, + * https://www.drupal.org/node/3084859 + * @preserve + */ +.field-icon-comment { + color: red; + background-image: url("data:image/svg+xml,%3csvg height='36' viewBox='0 0 36 36' width='36' xmlns='http://www.w3.org/2000/svg'%3e%3cpath d='m21.0909 34-4.3273-5.4634h-8.03633c-.40988 0-.80297-.1645-1.0928-.4572-.28983-.2928-.45265-.6898-.45265-1.1038v-17.00994c0-.414.16282-.81104.45265-1.10378s.68292-.4572 1.0928-.4572h24.72723c.4099 0 .803.16446 1.0928.4572.2899.29274.4527.68978.4527 1.10378v17.00994c0 .414-.1628.811-.4527 1.1038-.2898.2927-.6829.4572-1.0928.4572h-8.0363zm2.8421-8.5854h7.9761v-13.888h-21.6364v13.888h7.9761l2.8421 3.5872zm-21.38755-23.4146h26.27275v3.12195h-24.72729v17.17075h-3.09091v-18.73172c0-.414.16282-.81104.45265-1.10378s.68292-.4572 1.0928-.4572z' fill='%2355565b'/%3e%3c/svg%3e"); +} diff --git a/core/modules/comment/css/comment.icon.theme.pcss.css b/core/modules/comment/css/comment.icon.theme.pcss.css new file mode 100644 index 0000000000..18201178f3 --- /dev/null +++ b/core/modules/comment/css/comment.icon.theme.pcss.css @@ -0,0 +1,4 @@ +.field-icon-comment { + color:red; + background-image: url(../../../misc/icons/55565b/comment.svg); +} diff --git a/core/modules/comment/tests/src/Functional/CommentFieldsTest.php b/core/modules/comment/tests/src/Functional/CommentFieldsTest.php index ae1038cb21..6273177482 100644 --- a/core/modules/comment/tests/src/Functional/CommentFieldsTest.php +++ b/core/modules/comment/tests/src/Functional/CommentFieldsTest.php @@ -158,12 +158,11 @@ public function testCommentFieldCreate() { 'field_name' => 'user_comment', ]; $this->drupalGet('admin/config/people/accounts/fields/add-field'); - $this->submitForm($edit, 'Save and continue'); + $this->submitForm($edit, 'Continue'); // Try to save the comment field without selecting a comment type. $edit = []; - $this->drupalGet('admin/config/people/accounts/fields/user.user.field_user_comment/storage'); - $this->submitForm($edit, 'Save field settings'); + $this->submitForm($edit, 'Update settings'); // We should get an error message. $this->assertSession()->pageTextContains('The submitted value in the Comment type element is not allowed.'); @@ -178,10 +177,10 @@ public function testCommentFieldCreate() { // Select a comment type and try to save again. $edit = [ - 'settings[comment_type]' => 'user_comment_type', + 'field_storage[subform][settings][comment_type]' => 'user_comment_type', ]; - $this->drupalGet('admin/config/people/accounts/fields/user.user.field_user_comment/storage'); - $this->submitForm($edit, 'Save field settings'); + $this->drupalGet('admin/config/people/accounts/add-field/user/field_user_comment'); + $this->submitForm($edit, 'Update settings'); // We shouldn't get an error message. $this->assertSession()->pageTextNotContains('The submitted value in the Comment type element is not allowed.'); @@ -190,7 +189,7 @@ public function testCommentFieldCreate() { $edit = [ 'settings[per_page]' => 0, ]; - $this->drupalGet('admin/config/people/accounts/fields/user.user.field_user_comment'); + $this->drupalGet('admin/config/people/accounts/add-field/user/field_user_comment'); $this->submitForm($edit, 'Save settings'); $this->assertSession()->statusMessageContains('Saved User comment configuration.', 'status'); } diff --git a/core/modules/comment/tests/src/Functional/CommentNonNodeTest.php b/core/modules/comment/tests/src/Functional/CommentNonNodeTest.php index 2d7f3cf176..0987814356 100644 --- a/core/modules/comment/tests/src/Functional/CommentNonNodeTest.php +++ b/core/modules/comment/tests/src/Functional/CommentNonNodeTest.php @@ -277,8 +277,6 @@ public function testCommentFunctionality() { $this->assertSession()->statusCodeEquals(200); $this->assertSession()->fieldNotExists('edit-default-value-input-comment-und-0-status-0'); // Test that field to change cardinality is not available. - $this->drupalGet('entity_test/structure/entity_test/fields/entity_test.entity_test.comment/storage'); - $this->assertSession()->statusCodeEquals(200); $this->assertSession()->fieldNotExists('cardinality_number'); $this->assertSession()->fieldNotExists('cardinality'); @@ -439,7 +437,7 @@ public function testCommentFunctionality() { // Add a new comment field. $storage_edit = [ - 'settings[comment_type]' => 'foobar', + 'field_storage[subform][settings][comment_type]' => 'foobar', ]; $this->fieldUIAddNewField('entity_test/structure/entity_test', 'foobar', 'Foobar', 'comment', $storage_edit); @@ -501,9 +499,9 @@ public function testsNonIntegerIdEntities() { // Visit the Field UI field add page. $this->drupalGet('entity_test_string_id/structure/entity_test/fields/add-field'); // Ensure field isn't shown for string IDs. - $this->assertSession()->optionNotExists('edit-new-storage-type', 'comment'); + $this->assertSession()->elementNotExists('css', "[name='new_storage_type'][value='comment']"); // Ensure a core field type shown. - $this->assertSession()->optionExists('edit-new-storage-type', 'boolean'); + $this->assertSession()->elementExists('css', "[name='new_storage_type'][value='boolean']"); // Attempt to add a comment-type referencing this entity-type. $this->drupalGet('admin/structure/comment/types/add'); @@ -518,9 +516,9 @@ public function testsNonIntegerIdEntities() { // Visit the Field UI field add page. $this->drupalGet('entity_test_no_id/structure/entity_test/fields/add-field'); // Ensure field isn't shown for empty IDs. - $this->assertSession()->optionNotExists('edit-new-storage-type', 'comment'); + $this->assertSession()->elementNotExists('css', "[name='new_storage_type'][value='comment']"); // Ensure a core field type shown. - $this->assertSession()->optionExists('edit-new-storage-type', 'boolean'); + $this->assertSession()->elementExists('css', "[name='new_storage_type'][value='boolean']"); } /** diff --git a/core/modules/content_translation/tests/src/Functional/ContentTranslationSettingsTest.php b/core/modules/content_translation/tests/src/Functional/ContentTranslationSettingsTest.php index 73c2ad3193..88c4576a02 100644 --- a/core/modules/content_translation/tests/src/Functional/ContentTranslationSettingsTest.php +++ b/core/modules/content_translation/tests/src/Functional/ContentTranslationSettingsTest.php @@ -10,6 +10,7 @@ use Drupal\field\Entity\FieldConfig; use Drupal\language\Entity\ContentLanguageSettings; use Drupal\Tests\BrowserTestBase; +use Drupal\Tests\field_ui\Traits\FieldUiTestTrait; /** * Tests the content translation settings UI. @@ -19,6 +20,7 @@ class ContentTranslationSettingsTest extends BrowserTestBase { use CommentTestTrait; + use FieldUiTestTrait; /** * Modules to enable. @@ -290,13 +292,7 @@ public function testFieldTranslatableSettingsUI() { // At least one field needs to be translatable to enable article for // translation. Create an extra field to be used for this purpose. We use // the UI to test our form alterations. - $edit = [ - 'new_storage_type' => 'text', - 'label' => 'Test', - 'field_name' => 'article_text', - ]; - $this->drupalGet('admin/structure/types/manage/article/fields/add-field'); - $this->submitForm($edit, 'Save and continue'); + $this->fieldUIAddNewField('admin/structure/types/manage/article', 'article_text', 'Test', 'text'); // Tests that field doesn't have translatable setting if bundle is not // translatable. diff --git a/core/modules/datetime/src/Plugin/Field/FieldType/DateTimeItem.php b/core/modules/datetime/src/Plugin/Field/FieldType/DateTimeItem.php index 5e17728b99..1a5b215a39 100644 --- a/core/modules/datetime/src/Plugin/Field/FieldType/DateTimeItem.php +++ b/core/modules/datetime/src/Plugin/Field/FieldType/DateTimeItem.php @@ -15,7 +15,12 @@ * @FieldType( * id = "datetime", * label = @Translation("Date"), - * description = @Translation("Create and store date values."), + * description = { + * @Translation("Ideal when date and time needs to be input by users, like event dates and times"), + * @Translation("Date or date and time stored in a readable string format"), + * @Translation("Easy to read and understand for humans"), + * }, + * category = "date_time", * default_widget = "datetime_default", * default_formatter = "datetime_default", * list_class = "\Drupal\datetime\Plugin\Field\FieldType\DateTimeFieldItemList", diff --git a/core/modules/datetime/tests/src/Functional/DateTimeFieldTest.php b/core/modules/datetime/tests/src/Functional/DateTimeFieldTest.php index 968e557fcb..da0c4829c3 100644 --- a/core/modules/datetime/tests/src/Functional/DateTimeFieldTest.php +++ b/core/modules/datetime/tests/src/Functional/DateTimeFieldTest.php @@ -935,8 +935,8 @@ public function testDateStorageSettings() { ]; $this->drupalGet('node/add/date_content'); $this->submitForm($edit, 'Save'); - $this->drupalGet('admin/structure/types/manage/date_content/fields/node.date_content.' . $field_name . '/storage'); - $this->assertSession()->elementsCount('xpath', "//*[@id='edit-settings-datetime-type' and contains(@disabled, 'disabled')]", 1); + $this->drupalGet('admin/structure/types/manage/date_content/fields/node.date_content.' . $field_name); + $this->assertSession()->elementsCount('xpath', "//*[@name='field_storage[subform][settings][datetime_type]' and contains(@disabled, 'disabled')]", 1); } } diff --git a/core/modules/datetime_range/css/datetime_range.icon.theme.css b/core/modules/datetime_range/css/datetime_range.icon.theme.css new file mode 100644 index 0000000000..028163f29e --- /dev/null +++ b/core/modules/datetime_range/css/datetime_range.icon.theme.css @@ -0,0 +1,9 @@ +/* + * DO NOT EDIT THIS FILE. + * See the following change record for more information, + * https://www.drupal.org/node/3084859 + * @preserve + */ +.field-icon-daterange { + background-image: url("data:image/svg+xml,%3csvg height='36' viewBox='0 0 36 36' width='36' xmlns='http://www.w3.org/2000/svg'%3e%3cpath d='m27 3.6h7.2c.9941 0 1.8.8059 1.8 1.8v28.8c0 .9941-.8059 1.8-1.8 1.8h-32.4c-.994104 0-1.8-.8059-1.8-1.8v-28.8c0-.9941.805896-1.8 1.8-1.8h7.2v-3.6h3.6v3.6h10.8v-3.6h3.6zm-23.4 10.8v18h28.8v-18zm3.6 3.6h3.6v3.6h-3.6zm9 0h3.6v3.6h-3.6zm9 0h3.6v3.6h-3.6z' fill='%2355565b'/%3e%3c/svg%3e"); +} diff --git a/core/modules/datetime_range/css/datetime_range.icon.theme.pcss.css b/core/modules/datetime_range/css/datetime_range.icon.theme.pcss.css new file mode 100644 index 0000000000..01b7955ccc --- /dev/null +++ b/core/modules/datetime_range/css/datetime_range.icon.theme.pcss.css @@ -0,0 +1,3 @@ +.field-icon-daterange { + background-image: url(../../../misc/icons/55565b/daterange.svg); +} diff --git a/core/modules/datetime_range/datetime_range.libraries.yml b/core/modules/datetime_range/datetime_range.libraries.yml new file mode 100644 index 0000000000..178d960671 --- /dev/null +++ b/core/modules/datetime_range/datetime_range.libraries.yml @@ -0,0 +1,7 @@ +drupal.datetime_range-icon: + version: VERSION + css: + theme: + css/datetime_range.icon.theme.css: {} + dependencies: + - field_ui/drupal.field_ui.manage_fields diff --git a/core/modules/datetime_range/datetime_range.module b/core/modules/datetime_range/datetime_range.module index 0dbbd8d70d..37283d67a2 100644 --- a/core/modules/datetime_range/datetime_range.module +++ b/core/modules/datetime_range/datetime_range.module @@ -5,6 +5,7 @@ * Field hooks to implement a datetime field that stores a start and end date. */ +use Drupal\Core\Field\FieldTypeCategoryManagerInterface; use Drupal\Core\Url; use Drupal\Core\Routing\RouteMatchInterface; @@ -27,3 +28,14 @@ function datetime_range_help($route_name, RouteMatchInterface $route_match) { return $output; } } + +/** + * Implements hook_field_type_category_info_alter(). + */ +function datetime_range_field_type_category_info_alter(&$definitions) { + // TRICKY: the `datetime_range` field type belongs in the `general` category, + // so the libraries need to be attached using an alter hook. + if (array_key_exists(FieldTypeCategoryManagerInterface::FALLBACK_CATEGORY, $definitions)) { + $definitions[FieldTypeCategoryManagerInterface::FALLBACK_CATEGORY]['libraries'][] = 'datetime_range/drupal.datetime_range-icon'; + } +} diff --git a/core/modules/datetime_range/tests/src/Functional/DateRangeFieldTest.php b/core/modules/datetime_range/tests/src/Functional/DateRangeFieldTest.php index 7d477be4fc..39d0b10f18 100644 --- a/core/modules/datetime_range/tests/src/Functional/DateRangeFieldTest.php +++ b/core/modules/datetime_range/tests/src/Functional/DateRangeFieldTest.php @@ -1422,8 +1422,8 @@ public function testDateStorageSettings() { ]; $this->drupalGet('node/add/date_content'); $this->submitForm($edit, 'Save'); - $this->drupalGet('admin/structure/types/manage/date_content/fields/node.date_content.' . $field_name . '/storage'); - $this->assertSession()->elementsCount('xpath', "//*[@id='edit-settings-datetime-type' and contains(@disabled, 'disabled')]", 1); + $this->drupalGet('admin/structure/types/manage/date_content/fields/node.date_content.' . $field_name); + $this->assertSession()->elementsCount('xpath', "//*[@name='field_storage[subform][settings][datetime_type]' and contains(@disabled, 'disabled')]", 1); } } diff --git a/core/modules/editor/tests/modules/editor_test/src/Plugin/Field/FieldType/EditorTestTextLongItem.php b/core/modules/editor/tests/modules/editor_test/src/Plugin/Field/FieldType/EditorTestTextLongItem.php index 14d22cab7f..5c0c964a6b 100644 --- a/core/modules/editor/tests/modules/editor_test/src/Plugin/Field/FieldType/EditorTestTextLongItem.php +++ b/core/modules/editor/tests/modules/editor_test/src/Plugin/Field/FieldType/EditorTestTextLongItem.php @@ -11,7 +11,6 @@ * id = "editor_test_text_long", * label = @Translation("Filter test text (formatted, long)"), * description = @Translation("This field stores a long text with a text format."), - * category = @Translation("Text"), * default_widget = "text_textarea", * default_formatter = "text_default" * ) diff --git a/core/modules/field/field.api.php b/core/modules/field/field.api.php index e8b07637b3..ed9b86a8af 100644 --- a/core/modules/field/field.api.php +++ b/core/modules/field/field.api.php @@ -410,6 +410,31 @@ function hook_field_purge_field(\Drupal\field\Entity\FieldConfig $field) { ->execute(); } +/** + * Allows modules to alter the field type category information. + * + * This hook provides a way for modules to modify or add to the existing + * category information. Modules can use this hook to modify the properties of + * existing categories. It can also be used to define custom field type + * categories although the use of YAML-based plugins should be preferred over + * the hook. + * + * @param array &$categories + * An associative array of field type categories, keyed by category machine + * name. + * + * @see \Drupal\Core\Field\FieldTypeCategoryManager + */ +function hook_field_type_category_info_alter(array &$categories) { + // Modify or add field type categories. + $categories['my_custom_category'] = [ + 'label' => 'My Custom Category', + 'description' => 'This is a custom category for my field types.', + ]; + // Modify the properties of an existing category. + $categories['text']['description'] = 'Modified Text'; +} + /** * @} End of "addtogroup field_purge". */ diff --git a/core/modules/field/field.module b/core/modules/field/field.module index d5a8182048..f2c6007156 100644 --- a/core/modules/field/field.module +++ b/core/modules/field/field.module @@ -387,16 +387,11 @@ function field_field_storage_config_update(FieldStorageConfigInterface $field_st } /** - * Implements hook_ENTITY_TYPE_presave() for 'field_config'. + * Implements hook_ENTITY_TYPE_create() for 'field_config'. * * Determine the selection handler plugin ID for an entity reference field. */ -function field_field_config_presave(FieldConfigInterface $field) { - // Don't change anything during a configuration sync. - if ($field->isSyncing()) { - return; - } - +function field_field_config_create(FieldConfigInterface $field) { // Act on all sub-types of the entity_reference field type. /** @var \Drupal\Core\Field\FieldTypePluginManager $field_type_manager */ $field_type_manager = \Drupal::service('plugin.manager.field.field_type'); @@ -406,12 +401,39 @@ function field_field_config_presave(FieldConfigInterface $field) { return; } + // If we don't know the target type yet, there's nothing else we can do. + $target_type = $field->getFieldStorageDefinition()->getSetting('target_type'); + if (empty($target_type)) { + return; + } + // Make sure the selection handler plugin is the correct derivative for the // target entity type. - $target_type = $field->getFieldStorageDefinition()->getSetting('target_type'); $selection_manager = \Drupal::service('plugin.manager.entity_reference_selection'); [$current_handler] = explode(':', $field->getSetting('handler'), 2); $field->setSetting('handler', $selection_manager->getPluginId($target_type, $current_handler)); +} + +/** + * Implements hook_ENTITY_TYPE_presave() for 'field_config'. + * + * Determine the selection handler plugin ID for an entity reference field. + */ +function field_field_config_presave(FieldConfigInterface $field) { + // Don't change anything during a configuration sync. + if ($field->isSyncing()) { + return; + } + field_field_config_create($field); + + // Act on all sub-types of the entity_reference field type. + /** @var \Drupal\Core\Field\FieldTypePluginManager $field_type_manager */ + $field_type_manager = \Drupal::service('plugin.manager.field.field_type'); + $item_class = 'Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem'; + $class = $field_type_manager->getPluginClass($field->getType()); + if ($class !== $item_class && !is_subclass_of($class, $item_class)) { + return; + } // In case we removed all the target bundles allowed by the field in // EntityReferenceItem::onDependencyRemoval() or field_entity_bundle_delete() @@ -426,3 +448,51 @@ function field_field_config_presave(FieldConfigInterface $field) { ]); } } + +/** + * Entity form builder for field config edit form. + * + * @param string $entity_type_id + * The entity type identifier. + * @param \Drupal\Core\Entity\EntityInterface $entity + * The entity updated with the submitted values. + * @param array $form + * The complete form array. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The current state of the form. + * + * @see \Drupal\field_ui\Form\FieldConfigEditForm::form + * @see \Drupal\field_ui\Form\FieldConfigEditForm::copyFormValuesToEntity + */ +function field_form_field_config_edit_form_entity_builder($entity_type_id, $entity, &$form, FormStateInterface $form_state) { + assert($entity instanceof FieldConfigInterface); + $previous_field_storage = $form_state->getFormObject()->getEntity()->getFieldStorageDefinition(); + assert($previous_field_storage instanceof FieldStorageConfigInterface); + + // Act on all sub-types of the entity_reference field type. + /** @var \Drupal\Core\Field\FieldTypePluginManager $field_type_manager */ + $field_type_manager = \Drupal::service('plugin.manager.field.field_type'); + $item_class = 'Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem'; + $class = $field_type_manager->getPluginClass($entity->getFieldStorageDefinition()->getType()); + if ($class !== $item_class && !is_subclass_of($class, $item_class)) { + return; + } + + // Update handler settings when target_type is changed. + if ($entity->getFieldStorageDefinition()->getSetting('target_type') !== $previous_field_storage->getSetting('target_type')) { + // @see field_field_storage_config_update(). + $entity->setSetting('handler_settings', []); + // @see field_field_config_presave(). + field_field_config_create($entity); + + // Store updated settings in form state so that the form state can be copied + // directly to the entity. + $form_state->setValue('settings', $entity->getSettings()); + + // Unset user input for the settings because they are not valid after the + // target type has changed. + $user_input = $form_state->getUserInput(); + unset($user_input['settings']); + $form_state->setUserInput($user_input); + } +} diff --git a/core/modules/field/src/Entity/FieldConfig.php b/core/modules/field/src/Entity/FieldConfig.php index f43a798748..fb33059331 100644 --- a/core/modules/field/src/Entity/FieldConfig.php +++ b/core/modules/field/src/Entity/FieldConfig.php @@ -262,7 +262,6 @@ protected function linkTemplates() { $link_templates = parent::linkTemplates(); if (\Drupal::moduleHandler()->moduleExists('field_ui')) { $link_templates["{$this->entity_type}-field-edit-form"] = 'entity.field_config.' . $this->entity_type . '_field_edit_form'; - $link_templates["{$this->entity_type}-storage-edit-form"] = 'entity.field_config.' . $this->entity_type . '_storage_edit_form'; $link_templates["{$this->entity_type}-field-delete-form"] = 'entity.field_config.' . $this->entity_type . '_field_delete_form'; if (isset($link_templates['config-translation-overview'])) { diff --git a/core/modules/field/src/Entity/FieldStorageConfig.php b/core/modules/field/src/Entity/FieldStorageConfig.php index 0aa8dca378..5334c155b2 100644 --- a/core/modules/field/src/Entity/FieldStorageConfig.php +++ b/core/modules/field/src/Entity/FieldStorageConfig.php @@ -6,6 +6,8 @@ use Drupal\Core\Entity\EntityStorageInterface; use Drupal\Core\Entity\FieldableEntityInterface; use Drupal\Core\Entity\FieldableEntityStorageInterface; +use Drupal\Core\Entity\Plugin\DataType\EntityAdapter; +use Drupal\Core\Field\BaseFieldDefinition; use Drupal\Core\Field\FieldException; use Drupal\Core\Field\FieldStorageDefinitionInterface; use Drupal\Core\TypedData\OptionsProviderInterface; @@ -279,6 +281,28 @@ public function id() { return $this->getTargetEntityTypeId() . '.' . $this->getName(); } + /** + * {@inheritdoc} + */ + public function postCreate(EntityStorageInterface $storage) { + parent::postCreate($storage); + + // Check that the field type is known. + $field_type = \Drupal::service('plugin.manager.field.field_type')->getDefinition($this->type, FALSE); + if (!$field_type) { + throw new FieldException("Attempt to create a field storage of unknown type {$this->type}."); + } + $this->module = $field_type['provider']; + + // Make sure all expected runtime settings are present. + $default_settings = \Drupal::service('plugin.manager.field.field_type') + ->getDefaultStorageSettings($this->getType()); + + // Filter out any unknown (unsupported) settings. + $supported_settings = array_intersect_key($this->getSettings(), $default_settings); + $this->set('settings', $supported_settings + $default_settings); + } + /** * Overrides \Drupal\Core\Entity\Entity::preSave(). * @@ -319,7 +343,6 @@ public function preSave(EntityStorageInterface $storage) { */ protected function preSaveNew(EntityStorageInterface $storage) { $entity_field_manager = \Drupal::service('entity_field.manager'); - $field_type_manager = \Drupal::service('plugin.manager.field.field_type'); // Assign the ID. $this->id = $this->id(); @@ -337,13 +360,6 @@ protected function preSaveNew(EntityStorageInterface $storage) { throw new FieldException("Attempt to create field storage {$this->getName()} which is reserved by entity type {$this->getTargetEntityTypeId()}."); } - // Check that the field type is known. - $field_type = $field_type_manager->getDefinition($this->getType(), FALSE); - if (!$field_type) { - throw new FieldException("Attempt to create a field storage of unknown type {$this->getType()}."); - } - $this->module = $field_type['provider']; - // Notify the field storage definition listener. \Drupal::service('field_storage_definition.listener')->onFieldStorageDefinitionCreate($this); } @@ -666,7 +682,19 @@ public function getOptionsProvider($property_name, FieldableEntityInterface $ent // runtime item object, so that it can be used as the options provider // without modifying the entity being worked on. if (is_subclass_of($this->getFieldItemClass(), OptionsProviderInterface::class)) { - $items = $entity->get($this->getName()); + try { + $items = $entity->get($this->getName()); + } + catch (\InvalidArgumentException $e) { + // When a field doesn't exist, create a new field item list using a + // temporary base field definition. This step is necessary since there + // may not be a field configuration for the storage when creating a new + // field. + // @todo Simplify in https://www.drupal.org/project/drupal/issues/3347291. + $field_storage = BaseFieldDefinition::createFromFieldStorageDefinition($this); + $entity_adapter = EntityAdapter::createFromEntity($entity); + $items = \Drupal::typedDataManager()->create($field_storage, name: $field_storage->getName(), parent: $entity_adapter); + } return \Drupal::service('plugin.manager.field.field_type')->createFieldItem($items, 0); } // @todo: Allow setting custom options provider, see @@ -710,7 +738,7 @@ public function getTargetEntityTypeId() { * TRUE if the field has data for any entity; FALSE otherwise. */ public function hasData() { - return \Drupal::entityTypeManager()->getStorage($this->entity_type)->countFieldData($this, TRUE); + return !$this->isNew() && \Drupal::entityTypeManager()->getStorage($this->entity_type)->countFieldData($this, TRUE); } /** diff --git a/core/modules/field/tests/modules/field_plugins_test/field_plugins_test.field_type_categories.yml b/core/modules/field/tests/modules/field_plugins_test/field_plugins_test.field_type_categories.yml new file mode 100644 index 0000000000..fe3b94f826 --- /dev/null +++ b/core/modules/field/tests/modules/field_plugins_test/field_plugins_test.field_type_categories.yml @@ -0,0 +1,6 @@ +test_category: + label: 'Test category' + description: 'This is a test field type category.' + weight: -10 + libraries: + - field_plugins_test/test_library diff --git a/core/modules/field/tests/modules/field_test/src/Plugin/Field/FieldType/TestItem.php b/core/modules/field/tests/modules/field_test/src/Plugin/Field/FieldType/TestItem.php index ad2a135a21..7f4d5f31e9 100644 --- a/core/modules/field/tests/modules/field_test/src/Plugin/Field/FieldType/TestItem.php +++ b/core/modules/field/tests/modules/field_test/src/Plugin/Field/FieldType/TestItem.php @@ -75,7 +75,8 @@ public static function schema(FieldStorageDefinitionInterface $field_definition) * {@inheritdoc} */ public function storageSettingsForm(array &$form, FormStateInterface $form_state, $has_data) { - $form['test_field_storage_setting'] = [ + $element = []; + $element['test_field_storage_setting'] = [ '#type' => 'textfield', '#title' => $this->t('Field test field storage setting'), '#default_value' => $this->getSetting('test_field_storage_setting'), @@ -83,14 +84,15 @@ public function storageSettingsForm(array &$form, FormStateInterface $form_state '#description' => $this->t('A dummy form element to simulate field storage setting.'), ]; - return $form; + return $element; } /** * {@inheritdoc} */ public function fieldSettingsForm(array $form, FormStateInterface $form_state) { - $form['test_field_setting'] = [ + $element = []; + $element['test_field_setting'] = [ '#type' => 'textfield', '#title' => $this->t('Field test field setting'), '#default_value' => $this->getSetting('test_field_setting'), @@ -98,7 +100,7 @@ public function fieldSettingsForm(array $form, FormStateInterface $form_state) { '#description' => $this->t('A dummy form element to simulate field setting.'), ]; - return $form; + return $element; } /** diff --git a/core/modules/field/tests/modules/field_test/src/Plugin/Field/FieldType/TestItemWithPreconfiguredOptions.php b/core/modules/field/tests/modules/field_test/src/Plugin/Field/FieldType/TestItemWithPreconfiguredOptions.php index 1c5754238a..a35642ac13 100644 --- a/core/modules/field/tests/modules/field_test/src/Plugin/Field/FieldType/TestItemWithPreconfiguredOptions.php +++ b/core/modules/field/tests/modules/field_test/src/Plugin/Field/FieldType/TestItemWithPreconfiguredOptions.php @@ -26,7 +26,6 @@ public static function getPreconfiguredOptions() { return [ 'custom_options' => [ 'label' => new TranslatableMarkup('All custom options'), - 'category' => new TranslatableMarkup('Custom category'), 'field_storage_config' => [ 'cardinality' => FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED, 'settings' => [ diff --git a/core/modules/field/tests/src/Functional/EntityReference/EntityReferenceAdminTest.php b/core/modules/field/tests/src/Functional/EntityReference/EntityReferenceAdminTest.php index 328fcd641e..cd52024a77 100644 --- a/core/modules/field/tests/src/Functional/EntityReference/EntityReferenceAdminTest.php +++ b/core/modules/field/tests/src/Functional/EntityReference/EntityReferenceAdminTest.php @@ -4,6 +4,7 @@ use Behat\Mink\Element\NodeElement; use Drupal\Core\Field\FieldStorageDefinitionInterface; +use Drupal\Core\Messenger\MessengerInterface; use Drupal\field\Entity\FieldConfig; use Drupal\node\Entity\Node; use Drupal\taxonomy\Entity\Vocabulary; @@ -113,19 +114,13 @@ public function testFieldAdminHandler() { // Create a test entity reference field. $field_name = 'test_entity_ref_field'; - $edit = [ - 'new_storage_type' => 'field_ui:entity_reference:node', - 'label' => 'Test Entity Reference Field', - 'field_name' => $field_name, - ]; - $this->drupalGet($bundle_path . '/fields/add-field'); - $this->submitForm($edit, 'Save and continue'); + $this->fieldUIAddNewField($bundle_path, $field_name, 'Test Entity Reference Field', 'field_ui:entity_reference:node', [], [], FALSE); // Set to unlimited. $edit = [ - 'cardinality' => FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED, + 'field_storage[subform][cardinality]' => FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED, ]; - $this->submitForm($edit, 'Save field settings'); + $this->submitForm($edit, 'Update settings'); // Add the view to the test field. $edit = [ @@ -137,6 +132,8 @@ public function testFieldAdminHandler() { 'settings[handler_settings][view][view_and_display]' => 'node_test_view:entity_reference_1', ]; $this->submitForm($edit, 'Save settings'); + $this->assertSession()->statusMessageContains("Saved Test Entity Reference Field configuration.", MessengerInterface::TYPE_STATUS); + $this->assertFieldExistsOnOverview('Test Entity Reference Field'); // Create nodes. $node1 = Node::create([ @@ -207,12 +204,11 @@ public function testFieldAdminHandler() { Vocabulary::create(['vid' => 'tags', 'name' => 'tags'])->save(); $taxonomy_term_field_name = $this->createEntityReferenceField('taxonomy_term', ['tags']); $field_path = 'node.' . $this->type . '.field_' . $taxonomy_term_field_name; - $this->drupalGet($bundle_path . '/fields/' . $field_path . '/storage'); + $this->drupalGet($bundle_path . '/fields/' . $field_path); $edit = [ - 'cardinality' => -1, + 'field_storage[subform][cardinality]' => -1, ]; - $this->submitForm($edit, 'Save field settings'); - $this->drupalGet($bundle_path . '/fields/' . $field_path); + $this->submitForm($edit, 'Update settings'); $term_name = $this->randomString(); $result = \Drupal::entityQuery('taxonomy_term') ->condition('name', $term_name) @@ -225,6 +221,7 @@ public function testFieldAdminHandler() { 'settings[handler_settings][auto_create]' => 1, ]; $this->submitForm($edit, 'Save settings'); + $this->assertFieldExistsOnOverview($taxonomy_term_field_name); $this->drupalGet($bundle_path . '/fields/' . $field_path); $edit = [ 'set_default_value' => '1', @@ -232,6 +229,7 @@ public function testFieldAdminHandler() { 'default_value_input[field_' . $taxonomy_term_field_name . '][0][target_id]' => $term_name, ]; $this->submitForm($edit, 'Save settings'); + $this->assertFieldExistsOnOverview($taxonomy_term_field_name); // The term should now exist. $result = \Drupal::entityQuery('taxonomy_term') ->condition('name', $term_name) @@ -383,12 +381,12 @@ protected function createEntityReferenceField($target_type, $bundles = []) { $field_name = strtolower($this->randomMachineName()); $storage_edit = $field_edit = []; - $storage_edit['settings[target_type]'] = $target_type; + $storage_edit['field_storage[subform][settings][target_type]'] = $target_type; foreach ($bundles as $bundle) { $field_edit['settings[handler_settings][target_bundles][' . $bundle . ']'] = TRUE; } - $this->fieldUIAddNewField($bundle_path, $field_name, NULL, 'entity_reference', $storage_edit, $field_edit); + $this->fieldUIAddNewField($bundle_path, $field_name, $field_name, 'entity_reference', $storage_edit, $field_edit); // Returns the generated field name. return $field_name; diff --git a/core/modules/field/tests/src/FunctionalJavascript/EntityReference/EntityReferenceAdminTest.php b/core/modules/field/tests/src/FunctionalJavascript/EntityReference/EntityReferenceAdminTest.php index bc7a4aa913..7a9f823846 100644 --- a/core/modules/field/tests/src/FunctionalJavascript/EntityReference/EntityReferenceAdminTest.php +++ b/core/modules/field/tests/src/FunctionalJavascript/EntityReference/EntityReferenceAdminTest.php @@ -6,6 +6,7 @@ use Drupal\Component\Render\FormattableMarkup; use Drupal\Core\Url; use Drupal\FunctionalJavascriptTests\WebDriverTestBase; +use Drupal\Tests\field_ui\Traits\FieldUiJSTestTrait; use Drupal\Tests\field_ui\Traits\FieldUiTestTrait; use Drupal\field\Entity\FieldConfig; use Drupal\field\Entity\FieldStorageConfig; @@ -18,6 +19,7 @@ class EntityReferenceAdminTest extends WebDriverTestBase { use FieldUiTestTrait; + use FieldUiJSTestTrait; /** * Modules to install. @@ -113,29 +115,25 @@ public function testFieldAdminHandler() { $bundle_path = 'admin/structure/types/manage/' . $this->type; $page = $this->getSession()->getPage(); + /** @var \Drupal\FunctionalJavascriptTests\JSWebAssert $assert_session */ $assert_session = $this->assertSession(); // First step: 'Add new field' on the 'Manage fields' page. $this->drupalGet($bundle_path . '/fields/add-field'); // Check if the commonly referenced entity types appear in the list. - $this->assertSession()->optionExists('edit-new-storage-type', 'field_ui:entity_reference:node'); - $this->assertSession()->optionExists('edit-new-storage-type', 'field_ui:entity_reference:user'); + $page->find('css', "[name='new_storage_type'][value='reference']")->getParent()->click(); + $assert_session->waitForText('Choose an option below'); + $this->assertSession()->elementExists('css', "[name='group_field_options_wrapper'][value='field_ui:entity_reference:node']"); + $this->assertSession()->elementExists('css', "[name='group_field_options_wrapper'][value='field_ui:entity_reference:user']"); - $page->findField('new_storage_type')->setValue('entity_reference'); - $assert_session->waitForField('label')->setValue('Test'); - $machine_name = $assert_session->waitForElement('xpath', '//*[@id="edit-label-machine-name-suffix"]/span[contains(text(), "field_test")]'); - $this->assertNotEmpty($machine_name); - $page->pressButton('Save and continue'); + $this->fieldUIAddNewFieldJS(NULL, 'test', 'Test', 'entity_reference', FALSE); // Node should be selected by default. - $this->assertSession()->fieldValueEquals('settings[target_type]', 'node'); + $this->assertSession()->fieldValueEquals('field_storage[subform][settings][target_type]', 'node'); // Check that all entity types can be referenced. - $this->assertFieldSelectOptions('settings[target_type]', array_keys(\Drupal::entityTypeManager()->getDefinitions())); - - // Second step: 'Field settings' form. - $this->submitForm([], 'Save field settings'); + $this->assertFieldSelectOptions('field_storage[subform][settings][target_type]', array_keys(\Drupal::entityTypeManager()->getDefinitions())); // The base handler should be selected by default. $this->assertSession()->fieldValueEquals('settings[handler]', 'default:node'); @@ -263,23 +261,19 @@ public function testFieldAdminHandler() { // Switch the target type to 'taxonomy_term' and check that the settings // specific to its selection handler are displayed. $field_name = 'node.' . $this->type . '.field_test'; - $edit = [ - 'settings[target_type]' => 'taxonomy_term', - ]; - $this->drupalGet($bundle_path . '/fields/' . $field_name . '/storage'); - $this->submitForm($edit, 'Save field settings'); $this->drupalGet($bundle_path . '/fields/' . $field_name); + $page->findField('field_storage[subform][settings][target_type]')->setValue('taxonomy_term'); + $this->assertSession()->assertWaitOnAjaxRequest(); $this->assertSession()->fieldExists('settings[handler_settings][auto_create]'); + $this->assertSession()->fieldValueEquals('settings[handler]', 'default:taxonomy_term'); // Switch the target type to 'user' and check that the settings specific to // its selection handler are displayed. $field_name = 'node.' . $this->type . '.field_test'; - $edit = [ - 'settings[target_type]' => 'user', - ]; - $this->drupalGet($bundle_path . '/fields/' . $field_name . '/storage'); - $this->submitForm($edit, 'Save field settings'); $this->drupalGet($bundle_path . '/fields/' . $field_name); + $target_type_input = $assert_session->fieldExists('field_storage[subform][settings][target_type]'); + $target_type_input->setValue('user'); + $assert_session->assertWaitOnAjaxRequest(); $this->assertSession()->fieldValueEquals('settings[handler_settings][filter][type]', '_none'); $this->assertSession()->fieldValueEquals('settings[handler_settings][sort][field]', '_none'); $assert_session->optionNotExists('settings[handler_settings][sort][field]', 'nid'); @@ -294,11 +288,8 @@ public function testFieldAdminHandler() { // Switch the target type to 'node'. $field_name = 'node.' . $this->type . '.field_test'; - $edit = [ - 'settings[target_type]' => 'node', - ]; - $this->drupalGet($bundle_path . '/fields/' . $field_name . '/storage'); - $this->submitForm($edit, 'Save field settings'); + $this->drupalGet($bundle_path . '/fields/' . $field_name); + $page->findField('field_storage[subform][settings][target_type]')->setValue('node'); // Try to select the views handler. $this->drupalGet($bundle_path . '/fields/' . $field_name); @@ -327,16 +318,13 @@ public function testFieldAdminHandler() { $assert_session->pageTextContains('Saved Test configuration.'); // Switch the target type to 'entity_test'. - $edit = [ - 'settings[target_type]' => 'entity_test', - ]; - $this->drupalGet($bundle_path . '/fields/' . $field_name . '/storage'); - $this->submitForm($edit, 'Save field settings'); $this->drupalGet($bundle_path . '/fields/' . $field_name); + $page->findField('field_storage[subform][settings][target_type]')->setValue('entity_test'); $page->findField('settings[handler]')->setValue('views'); - $assert_session - ->waitForField('settings[handler_settings][view][view_and_display]') - ->setValue('test_entity_reference_entity_test:entity_reference_1'); + $assert_session->assertWaitOnAjaxRequest(); + $page + ->findField('settings[handler_settings][view][view_and_display]') + ->selectOption('test_entity_reference_entity_test:entity_reference_1'); $edit = [ 'required' => FALSE, ]; diff --git a/core/modules/field/tests/src/Kernel/Entity/FieldEntitySettingsTest.php b/core/modules/field/tests/src/Kernel/Entity/FieldEntitySettingsTest.php new file mode 100644 index 0000000000..da442c604b --- /dev/null +++ b/core/modules/field/tests/src/Kernel/Entity/FieldEntitySettingsTest.php @@ -0,0 +1,120 @@ + 'test', 'label' => 'Test'])->save(); + } + + /** + * @group legacy + */ + public function testFieldEntitiesCarryDefaultSettings(): void { + /** @var \Drupal\field\FieldStorageConfigInterface $field_storage */ + $field_storage = FieldStorageConfig::create([ + 'type' => 'integer', + 'entity_type' => 'entity_test', + 'field_name' => 'test', + ]); + $field = FieldConfig::create([ + 'field_storage' => $field_storage, + 'bundle' => 'test', + ]); + + /** @var \Drupal\Core\Field\FieldTypePluginManagerInterface $plugin_manager */ + $plugin_manager = $this->container->get('plugin.manager.field.field_type'); + $default_storage_settings = $plugin_manager->getDefaultStorageSettings('integer'); + $default_field_settings = $plugin_manager->getDefaultFieldSettings('integer'); + + // Both entities should have the complete, default settings for their + // field type. + $this->assertSame($default_storage_settings, $field_storage->get('settings')); + $this->assertSame($default_field_settings, $field->get('settings')); + + // If we try to set incomplete settings, the existing values should be + // retained. + $storage_settings = $field_storage->setSettings(['size' => 'big']) + ->get('settings'); + // There should be no missing settings. + $missing_storage_settings = array_diff_key($default_storage_settings, $storage_settings); + $this->assertEmpty($missing_storage_settings); + // The value we set should be remembered. + $this->assertSame('big', $storage_settings['size']); + + $field_settings = $field->setSetting('min', 10)->getSettings(); + $missing_field_settings = array_diff_key($default_field_settings, $field_settings); + $this->assertEmpty($missing_field_settings); + $this->assertSame(10, $field_settings['min']); + + $field_settings = $field->setSettings(['max' => 39])->get('settings'); + $missing_field_settings = array_diff_key($default_field_settings, $field_settings); + $this->assertEmpty($missing_field_settings); + $this->assertSame(39, $field_settings['max']); + + // Test that saving settings with incomplete settings is not triggering + // error, and values are retained. + $field_storage->save(); + $missing_storage_settings = array_diff_key($default_storage_settings, $storage_settings); + $this->assertEmpty($missing_storage_settings); + // The value we set should be remembered. + $this->assertSame('big', $storage_settings['size']); + + $field->save(); + $missing_field_settings = array_diff_key($default_field_settings, $field_settings); + $this->assertEmpty($missing_field_settings); + $this->assertSame(39, $field_settings['max']); + } + + /** + * Tests entity reference settings are normalized on field creation and save. + */ + public function testEntityReferenceSettingsNormalized(): void { + $field_storage = FieldStorageConfig::create([ + 'field_name' => 'test_reference', + 'type' => 'entity_reference', + 'entity_type' => 'entity_test', + 'cardinality' => 1, + 'settings' => [ + 'target_type' => 'entity_test', + ], + ]); + $field_storage->save(); + + $field = FieldConfig::create([ + 'field_storage' => $field_storage, + 'bundle' => 'test', + 'label' => 'Test Reference', + 'settings' => [ + 'handler' => 'default', + ], + ]); + $this->assertSame('default:entity_test', $field->getSetting('handler')); + // If the handler is changed, it should be normalized again on pre-save. + $field->setSetting('handler', 'default')->save(); + $this->assertSame('default:entity_test', $field->getSetting('handler')); + } + +} diff --git a/core/modules/field/tests/src/Kernel/FieldTypeCategoryDiscoveryTest.php b/core/modules/field/tests/src/Kernel/FieldTypeCategoryDiscoveryTest.php new file mode 100644 index 0000000000..22fa2ef738 --- /dev/null +++ b/core/modules/field/tests/src/Kernel/FieldTypeCategoryDiscoveryTest.php @@ -0,0 +1,43 @@ +createInstance('test_category'); + $expected = [ + 'Test category', + 'This is a test field type category.', + -10, + ['field_plugins_test/test_library'], + ]; + + $this->assertSame($expected, [ + (string) $category->getLabel(), + (string) $category->getDescription(), + $category->getWeight(), + $category->getLibraries(), + ]); + } + +} diff --git a/core/modules/field_ui/css/field_ui.icons.theme.css b/core/modules/field_ui/css/field_ui.icons.theme.css new file mode 100644 index 0000000000..6679d13efa --- /dev/null +++ b/core/modules/field_ui/css/field_ui.icons.theme.css @@ -0,0 +1,36 @@ +/* + * DO NOT EDIT THIS FILE. + * See the following change record for more information, + * https://www.drupal.org/node/3084859 + * @preserve + */ +/* cspell:ignore cacbd */ +.field-option__icon { + position: relative; + height: 100%; + background-image: url("data:image/svg+xml,%3csvg fill='none' height='36' viewBox='0 0 36 36' width='36' xmlns='http://www.w3.org/2000/svg'%3e%3cpath d='m5 7.71798 8.7937-.00001c-.7328-5.48304 7.3282-7.049628 7.3282 0l7.3281.00001v7.83292c5.8625-.7833 8.7937 9.3995 0 9.3995v7.0496h-23.45v-11.7494-14.02745' stroke='%23cacbd2' stroke-width='3'/%3e%3c/svg%3e"); + background-repeat: no-repeat; + background-position: center; + background-size: 36px; +} +.field-icon-boolean { + background-image: url("data:image/svg+xml,%3csvg height='36' viewBox='0 0 36 36' width='36' xmlns='http://www.w3.org/2000/svg'%3e%3cpath d='m11.02 6.064c-2.287.164-4.788 1.165-6.58 2.634-1.741 1.427-3.084 3.366-3.786 5.466-.852 2.547-.853 5.12-.006 7.656 1.506 4.503 5.535 7.679 10.302 8.119.884.082 13.216.082 14.1 0 5.287-.488 9.574-4.301 10.683-9.502.649-3.043.026-6.328-1.707-8.989a11.927 11.927 0 0 0 -9.157-5.386c-.977-.071-12.861-.069-13.849.002m14.422 2.542c4.167.683 7.319 3.848 7.953 7.984.165 1.079.088 2.688-.182 3.75-.944 3.727-4.045 6.501-7.923 7.088-.789.12-13.787.12-14.58.001-3.514-.53-6.376-2.828-7.627-6.126-.631-1.664-.746-3.857-.295-5.645.918-3.647 3.936-6.404 7.712-7.047.692-.118 14.227-.122 14.942-.005m-2.702 2.548c-2.256.498-3.999 2.206-4.569 4.476-.156.618-.219 2.389-.115 3.18.4 3.027 2.917 5.25 5.944 5.25a5.87 5.87 0 0 0 4.37-1.894 6.1 6.1 0 0 0 1.576-3.415c.1-.847.038-2.503-.117-3.121-.446-1.782-1.586-3.196-3.219-3.994-.879-.43-1.377-.546-2.46-.573-.72-.017-1.002.001-1.41.091' fill='%2355565b'/%3e%3c/svg%3e"); +} +.field-icon-plain_text { + background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='36' height='36' viewBox='0 0 36 36'%3e %3cpath fill='%2355565B' d='M7 7.60001V4H29V7.60001H19.8333V31H16.1667V7.60001H7Z'/%3e%3c/svg%3e"); +} +.field-icon-date_time { + background-image: url("data:image/svg+xml,%3csvg height='33' viewBox='0 0 36 33' width='36' xmlns='http://www.w3.org/2000/svg'%3e%3cpath d='m7.08 1.44v1.44h-5.976l-.318.158a1.326 1.326 0 0 0 -.726.941c-.048.224-.061 3.678-.048 12.311l.018 12 .131.246c.073.135.228.329.345.432.448.393-.104.373 9.978.372l9.14-.002.288.346c.479.574 1.348 1.362 1.936 1.755a9.006 9.006 0 0 0 8.182.98c4.629-1.704 7.072-6.881 5.452-11.555-.939-2.711-3.044-4.81-5.725-5.709l-.627-.211-.03-5.537-.03-5.537-.133-.249c-.162-.303-.513-.603-.797-.682-.125-.035-1.57-.058-3.555-.059h-3.345v-2.88h-2.82v2.88h-8.52v-2.88h-2.82zm18.84 10.912v2.391l-.342.041c-.542.063-1.317.269-1.969.521-2.825 1.095-4.943 3.609-5.613 6.664-.235 1.07-.219 2.683.039 3.936l.04.195h-14.835v-16.14h22.68zm1.185 2.332a2.601 2.601 0 0 1 -.45 0c-.124-.013-.022-.024.225-.024s.349.011.225.024m1.332 3.012c.586.148 1.445.539 1.976.899a6.322 6.322 0 0 1 2.746 5.525c-.079 1.624-.71 3.058-1.845 4.194a5.756 5.756 0 0 1 -1.756 1.24c-.918.435-1.576.581-2.618.583-.585.001-1.008-.03-1.292-.094-2.621-.594-4.532-2.609-4.95-5.219-.107-.664-.045-1.976.121-2.594.636-2.361 2.568-4.177 4.912-4.62.665-.125 2.042-.081 2.706.086m-2.563 5.119.016 3.255 2.415.016 2.415.015v-1.859l-1.605-.016-1.605-.016-.016-2.325-.015-2.325h-1.62z' fill='%2355565b'/%3e%3c/svg%3e"); +} +.field-icon-email { + background-image: url("data:image/svg+xml,%3csvg height='36' viewBox='0 0 36 36' width='36' xmlns='http://www.w3.org/2000/svg'%3e%3cpath d='m18.905 3.0034c-4.8818-.0181-9.89368 2.11996-12.75421 6.14588-2.97685 4.12852-3.72724 9.57882-2.74182 14.50242.69215 3.2862 2.61415 6.3919 5.55652 8.109 3.35621 2.0297 7.44861 2.3335 11.27361 1.8957 1.9198-.2327 3.8097-.7456 5.5656-1.549 0-1.2133 0-2.4267 0-3.64-3.9461 1.6362-8.4574 2.4333-12.6514 1.2587-2.9465-.8305-5.34152-3.3703-5.98961-6.3624-.77366-3.0458-.58571-6.3211.39477-9.2927 1.05002-2.9697 3.32674-5.53882 6.31624-6.61934 2.9829-1.16767 6.4097-1.27462 9.4541-.26651 2.7133.99524 4.9245 3.33755 5.6015 6.14525.7038 2.5698.6228 5.4088-.3714 7.8826-.4383 1.0424-1.4289 2.1055-2.6643 1.867-.6836-.1102-1.2174-.6898-1.2841-1.374-.3646-1.7236.0832-3.4856.0543-5.2278.0939-1.7622.1876-3.5244.2846-5.2865-2.7816-.8329-5.7863-1.36856-8.6563-.6962-2.9057.7966-5.1346 3.4114-5.6209 6.3736-.4246 2.2055-.2402 4.5877.7799 6.5936.9431 1.7193 2.7689 2.9433 4.7485 3.0192 2.1842.205 4.5109-.7068 5.752-2.5513.808 1.8442 2.9703 2.8932 4.9355 2.5197 2.3445-.3217 4.2363-2.1564 5.0624-4.3086 1.3658-3.1906 1.3042-6.8642.3573-10.1616-1.129-3.63941-3.9388-6.75356-7.5656-8.02092-1.8577-.69892-3.8521-.9948-5.8372-.95578zm-.2305 10.5789c.7719-.0025 1.547.0602 2.296.2236-.2194 2.5144.0496 5.147-.9169 7.5287-.4626 1.006-1.4737 1.788-2.6009 1.773-1.18.1157-2.4907-.5777-2.7663-1.7944-.5272-1.6144-.3716-3.4013.1106-5.0038.5405-1.4722 1.9158-2.6924 3.5363-2.7087.1134-.0098.2273-.016.3412-.0184z' fill='%2355565b'/%3e%3c/svg%3e"); +} +.field-icon-number { + background-image: url("data:image/svg+xml,%3csvg height='36' viewBox='0 0 36 36' width='36' xmlns='http://www.w3.org/2000/svg'%3e%3cpath d='m15.497 8.045c-1.353.129-2.556.713-3.559 1.727-1.08 1.092-1.675 2.516-1.677 4.013l-.001.555h2.683l.032-.585c.064-1.137.567-1.998 1.515-2.591.955-.598 2.344-.579 3.304.046 1.046.68 1.594 1.871 1.419 3.085-.134.93-.07.844-4.719 6.377l-4.231 5.038-.002.825-.001.825h11.7v-2.699l-3.627-.016-3.628-.015 2.974-3.54c1.635-1.947 3.08-3.689 3.212-3.87.322-.446.668-1.176.866-1.83.148-.487.164-.634.168-1.5.003-.82-.016-1.035-.133-1.47-.174-.647-.634-1.595-1.02-2.104-1.223-1.611-3.215-2.469-5.275-2.271m-12.872 1.184-2.625.635v1.338c0 .736.01 1.338.023 1.338.012 0 .91-.213 1.995-.473 1.085-.261 2.06-.492 2.167-.515l.195-.042v15.85h2.94v-18.78l-1.035.007-1.035.007zm21.495.701v1.35h3.3c1.815 0 3.3.013 3.3.028 0 .023-4.162 5.318-4.411 5.612-.064.075-.004.224.366.9.243.445.45.832.46.859.01.028.233-.06.496-.195 1.06-.541 1.997-.569 3.012-.087.814.387 1.449 1.12 1.781 2.06.161.457.181.589.181 1.203.001.492-.03.793-.108 1.05-.534 1.778-2.246 2.891-3.886 2.527-1.343-.299-2.334-1.279-2.686-2.655-.082-.322-.129-.41-.211-.403-.058.005-.602.14-1.209.3-.864.228-1.105.312-1.105.389 0 .214.317 1.188.538 1.654.833 1.753 2.35 2.971 4.166 3.345.74.153 1.734.13 2.465-.055a5.578 5.578 0 0 0 2.596-1.435c3.055-2.897 2.51-8.072-1.077-10.218a6 6 0 0 0 -.9-.424c-.257-.091-.467-.179-.467-.195s.905-1.175 2.01-2.574l2.009-2.544v-1.842h-10.62z' fill='%2355565b'/%3e%3c/svg%3e"); +} +.field-icon-reference { + background-image: url("data:image/svg+xml,%3csvg height='36' viewBox='0 0 36 36' width='36' xmlns='http://www.w3.org/2000/svg'%3e%3cpath d='m10.98 9v4.98h5.52v3.06h-9v6h-5.52v9.96h14.04v-9.96h-5.52v-3.06h15v3.06h-5.52v9.96h14.04v-9.96h-5.52v-6h-9v-3.06h5.52v-9.96h-14.04zm11.026.015-.016 1.995h-7.98l-.016-1.995-.016-1.995h8.044zm-8.986 18.975v2.01h-8.04v-4.02h8.04zm18 0v2.01h-8.04v-4.02h8.04z' fill='%2355565b'/%3e%3c/svg%3e"); +} +.field-icon-daterange { + background-image: url("data:image/svg+xml,%3csvg height='36' viewBox='0 0 36 36' width='36' xmlns='http://www.w3.org/2000/svg'%3e%3cpath d='m27 3.6h7.2c.9941 0 1.8.8059 1.8 1.8v28.8c0 .9941-.8059 1.8-1.8 1.8h-32.4c-.994104 0-1.8-.8059-1.8-1.8v-28.8c0-.9941.805896-1.8 1.8-1.8h7.2v-3.6h3.6v3.6h10.8v-3.6h3.6zm-23.4 10.8v18h28.8v-18zm3.6 3.6h3.6v3.6h-3.6zm9 0h3.6v3.6h-3.6zm9 0h3.6v3.6h-3.6z' fill='%2355565b'/%3e%3c/svg%3e"); +} diff --git a/core/modules/field_ui/css/field_ui.icons.theme.pcss.css b/core/modules/field_ui/css/field_ui.icons.theme.pcss.css new file mode 100644 index 0000000000..09bf9cd6a5 --- /dev/null +++ b/core/modules/field_ui/css/field_ui.icons.theme.pcss.css @@ -0,0 +1,37 @@ +/* cspell:ignore cacbd */ +.field-option__icon { + position: relative; + height: 100%; + background-image: url(../../../misc/icons/cacbd2/puzzle_piece_placeholder.svg); + background-repeat: no-repeat; + background-position: center; + background-size: 36px; +} + +.field-icon-boolean { + background-image: url(../../../misc/icons/55565b/boolean.svg); +} + +.field-icon-plain_text { + background-image: url(../../../misc/icons/55565b/plain_text.svg); +} + +.field-icon-date_time { + background-image: url(../../../misc/icons/55565b/date_and_time.svg); +} + +.field-icon-email { + background-image: url(../../../misc/icons/55565b/email.svg); +} + +.field-icon-number { + background-image: url(../../../misc/icons/55565b/number.svg); +} + +.field-icon-reference { + background-image: url(../../../misc/icons/55565b/reference.svg); +} + +.field-icon-daterange { + background-image: url(../../../misc/icons/55565b/daterange.svg); +} diff --git a/core/modules/field_ui/css/field_ui_add_field.module.css b/core/modules/field_ui/css/field_ui_add_field.module.css new file mode 100644 index 0000000000..3e51435133 --- /dev/null +++ b/core/modules/field_ui/css/field_ui_add_field.module.css @@ -0,0 +1,141 @@ +/* + * DO NOT EDIT THIS FILE. + * See the following change record for more information, + * https://www.drupal.org/node/3084859 + * @preserve + */ + +/** + * @file field_ui_add_field.module.css + */ + +.field-ui-field-storage-add-form { + --thumb-size: 4.5rem; +} + +.field-ui-new-storage-wrapper { + margin-bottom: 0.75rem; +} + +.group-field-options-wrapper { + margin-block: 1.5em; +} + +.add-field-container, +.group-field-options { + display: grid; + gap: 0.75rem 1.25rem; + margin-block: 0.625rem; +} + +@media (min-width: 45rem) { + .add-field-container, + .group-field-options { + grid-template-columns: repeat(2, 1fr); + } +} + +@media (min-width: 75rem) { + .add-field-container { + grid-template-columns: repeat(3, 1fr); + } +} + +@media (min-width: 87.5rem) { + .add-field-container { + grid-template-columns: repeat(4, 1fr); + } +} + +.field-option { + display: grid; + grid-template-columns: var(--thumb-size) auto; + align-items: center; + padding: 0.25rem; + padding-inline-end: 0.75rem; + border: 1px solid var(--color-gray-100, #dedfe4); + gap: 0.75rem; + border-radius: 0.25rem; +} + +.field-option__item { + display: grid; + grid-template-rows: auto 2fr; + grid-template-columns: auto 1.1875rem; + align-items: center; + width: 100%; + margin: 0; + column-gap: 1.25rem; + padding-block: 0.25rem; +} + +.field-option__item > *:not(input) { + grid-column: 1; +} + +.field-option__item > input { + grid-row: 1 / -1; + grid-column: 2; +} + +.field-option__thumb { + height: 100%; + min-height: var(--thumb-size); + background-color: var(--color-gray-050, #f3f4f9); +} + +.subfield-option { + margin-block: 0.625rem; + padding: 1rem; + padding-inline-end: 2rem; + border: 1px solid var(--color-gray-200, #d3d4d9); + border-radius: 0.25rem; +} + +.subfield-option .field-option-radio { + margin-inline-end: 0.4375rem; +} + +.subfield-option .item-list ul { + margin-inline: 0; +} + +.field-option, +.subfield-option { + cursor: pointer; +} + +.field-option.focus, +.subfield-option.focus { + outline: 3px solid var(--color-focus); + outline-offset: 2px; +} + +.field-option.hover, +.subfield-option.hover { + border-color: var(--color-gray); + box-shadow: inset 0 0 0 1px var(--color-gray), var(--details-box-shadow); +} + +.field-option:not(.selected, .error):hover .form-boolean, +.subfield-option:not(.selected, .error):hover .form-boolean { + border-color: var(--input-fg-color); + box-shadow: inset 0 0 0 1px var(--input-fg-color); +} + +.field-option.selected, +.subfield-option.selected { + border-color: var(--color-blue); + box-shadow: inset 0 0 0 2px var(--color-blue-600), var(--details-box-shadow); +} + +.field-option.error, +.subfield-option.error { + border-color: var(--color-red-500); + box-shadow: inset 0 0 0 1px var(--color-red-500), var(--details-box-shadow); +} + +.field-option .form-item__label.has-error, +.subfield-option .form-item__label.has-error { + color: currentColor; +} diff --git a/core/modules/field_ui/css/field_ui_add_field.module.pcss.css b/core/modules/field_ui/css/field_ui_add_field.module.pcss.css new file mode 100644 index 0000000000..9a7438dc7c --- /dev/null +++ b/core/modules/field_ui/css/field_ui_add_field.module.pcss.css @@ -0,0 +1,117 @@ +/** + * @file field_ui_add_field.module.css + */ + +.field-ui-field-storage-add-form { + --thumb-size: 72px; +} + +.field-ui-new-storage-wrapper { + margin-bottom: 0.75rem; +} + +.group-field-options-wrapper { + margin-block: 1.5em; +} + +.add-field-container, +.group-field-options { + display: grid; + gap: 0.75rem 1.25rem; + margin-block: 0.625rem; + + @media (min-width: 45rem) { + grid-template-columns: repeat(2, 1fr); + } +} + +.add-field-container { + @media (min-width: 75rem) { + grid-template-columns: repeat(3, 1fr); + } + + @media (min-width: 87.5rem) { + grid-template-columns: repeat(4, 1fr); + } +} + +.field-option { + display: grid; + grid-template-columns: var(--thumb-size) auto; + align-items: center; + padding: 0.25rem; + padding-inline-end: 0.75rem; + border: 1px solid var(--color-gray-100, #dedfe4); + gap: 0.75rem; + border-radius: 4px; +} + +.field-option__item { + display: grid; + grid-template-rows: auto 2fr; + grid-template-columns: auto 1.1875rem; + align-items: center; + width: 100%; + margin: 0; + column-gap: 1.25rem; + padding-block: 0.25rem; + + > *:not(input) { + grid-column: 1; + } + + > input { + grid-row: 1 / -1; + grid-column: 2; + } +} + +.field-option__thumb { + height: 100%; + min-height: var(--thumb-size); + background-color: var(--color-gray-050, #f3f4f9); +} + +.subfield-option { + margin-block: 0.625rem; + padding: 1rem; + padding-inline-end: 2rem; + border: 1px solid var(--color-gray-200, #d3d4d9); + border-radius: 4px; + + .field-option-radio { + margin-inline-end: 0.4375rem; + } + + .item-list ul { + margin-inline: 0; + } +} + +.field-option, +.subfield-option { + cursor: pointer; + &.focus { + outline: 3px solid var(--color-focus); + outline-offset: 2px; + } + &.hover { + border-color: var(--color-gray); + box-shadow: inset 0 0 0 1px var(--color-gray), var(--details-box-shadow); + } + &:not(.selected, .error):hover .form-boolean { + border-color: var(--input-fg-color); + box-shadow: inset 0 0 0 1px var(--input-fg-color); + } + &.selected { + border-color: var(--color-blue); + box-shadow: inset 0 0 0 2px var(--color-blue-600), var(--details-box-shadow); + } + &.error { + border-color: var(--color-red-500); + box-shadow: inset 0 0 0 1px var(--color-red-500), var(--details-box-shadow); + } + & .form-item__label.has-error { + color: currentColor; + } +} diff --git a/core/modules/field_ui/css/field_ui_add_field.theme.css b/core/modules/field_ui/css/field_ui_add_field.theme.css new file mode 100644 index 0000000000..aa1f56f66c --- /dev/null +++ b/core/modules/field_ui/css/field_ui_add_field.theme.css @@ -0,0 +1,21 @@ +/** + * @file field_ui_add_field.theme.css + */ +.field-option .form-item__label, +.subfield-option.subfield-option .form-item__label { + margin: 0; + font-size: 16px; + font-weight: 400; + line-height: 1.5; +} + +.subfield-option.subfield-option .form-item__label { + padding-bottom: 10px; +} + +.field-option .field-option__description, +.subfield-option .description .item-list { + color: var(--color-gray-700); + font-size: 14px; + line-height: 1.2; +} diff --git a/core/modules/field_ui/field_ui.libraries.yml b/core/modules/field_ui/field_ui.libraries.yml index 591cb8d8f2..8aa2a27ac4 100644 --- a/core/modules/field_ui/field_ui.libraries.yml +++ b/core/modules/field_ui/field_ui.libraries.yml @@ -1,7 +1,7 @@ drupal.field_ui: version: VERSION js: - field_ui.js: {} + js/field_ui.js: {} css: theme: css/field_ui.admin.css: {} @@ -12,3 +12,12 @@ drupal.field_ui: - core/once - core/drupal.ajax - core/drupal.dialog + +drupal.field_ui.manage_fields: + version: VERSION + css: + component: + css/field_ui_add_field.module.css: {} + theme: + css/field_ui_add_field.theme.css: {} + css/field_ui.icons.theme.css: {} diff --git a/core/modules/field_ui/field_ui.module b/core/modules/field_ui/field_ui.module index 679aed70e9..0fe20bcd71 100644 --- a/core/modules/field_ui/field_ui.module +++ b/core/modules/field_ui/field_ui.module @@ -12,6 +12,8 @@ use Drupal\Core\Entity\EntityFormModeInterface; use Drupal\Core\Url; use Drupal\field_ui\FieldUI; +use Drupal\field_ui\Form\FieldConfigEditForm; +use Drupal\field_ui\Form\FieldStorageConfigEditForm; use Drupal\field_ui\Plugin\Derivative\FieldUiLocalTask; /** @@ -63,6 +65,13 @@ function field_ui_theme() { 'empty' => '', ], ], + // Provide a dedicated template for new storage options as their styling + // is quite different from a typical form element, so it works best to not + // include default form element classes. + 'form_element__new_storage_type' => [ + 'base hook' => 'form_element', + 'render element' => 'element', + ], ]; } @@ -72,10 +81,12 @@ function field_ui_theme() { function field_ui_entity_type_build(array &$entity_types) { /** @var \Drupal\Core\Entity\EntityTypeInterface[] $entity_types */ $entity_types['field_config']->setFormClass('edit', 'Drupal\field_ui\Form\FieldConfigEditForm'); + $entity_types['field_config']->setFormClass('default', FieldConfigEditForm::class); $entity_types['field_config']->setFormClass('delete', 'Drupal\field_ui\Form\FieldConfigDeleteForm'); $entity_types['field_config']->setListBuilderClass('Drupal\field_ui\FieldConfigListBuilder'); $entity_types['field_storage_config']->setFormClass('edit', 'Drupal\field_ui\Form\FieldStorageConfigEditForm'); + $entity_types['field_storage_config']->setFormClass('default', FieldStorageConfigEditForm::class); $entity_types['field_storage_config']->setListBuilderClass('Drupal\field_ui\FieldStorageConfigListBuilder'); $entity_types['field_storage_config']->setLinkTemplate('collection', '/admin/reports/fields'); @@ -166,7 +177,7 @@ function field_ui_entity_operation(EntityInterface $entity) { $operations['manage-display'] = [ 'title' => t('Manage display'), 'weight' => 25, - 'url' => Url::fromRoute("entity.entity_view_display.{$bundle_of}.default", [ + 'url' => Url::fromRoute("entity.entity_view_display.$bundle_of.default", [ $entity->getEntityTypeId() => $entity->id(), ]), ]; @@ -248,3 +259,12 @@ function field_ui_form_field_ui_field_storage_add_form_alter(array &$form) { unset($form['add']['new_storage_type']['#options'][$optgroup]['entity_reference']); $form['add']['new_storage_type']['#options'][$optgroup]['entity_reference'] = t('Other…'); } + +/** + * Implements hook_preprocess_HOOK(). + */ +function field_ui_preprocess_form_element__new_storage_type(&$variables) { + // Add support for a variant string so radios in the add field form can be + // programmatically distinguished. + $variables['variant'] = $variables['element']['#variant'] ?? NULL; +} diff --git a/core/modules/field_ui/field_ui.js b/core/modules/field_ui/js/field_ui.js similarity index 88% rename from core/modules/field_ui/field_ui.js rename to core/modules/field_ui/js/field_ui.js index 0adfed5b0d..c605ba36bc 100644 --- a/core/modules/field_ui/field_ui.js +++ b/core/modules/field_ui/js/field_ui.js @@ -508,4 +508,61 @@ } }, }; + + /** + * Allows users to select an element which checks a radio button and + * adds a class used for css styling on different states. + * + * @type {Drupal~behavior} + * + * @prop {Drupal~behaviorAttach} attach + * Attaches behavior for selecting field. + */ + Drupal.behaviors.clickToSelect = { + attach(context) { + once('field-click-to-select', '.js-click-to-select', context).forEach( + (clickToSelectEl) => { + const input = clickToSelectEl.querySelector('input'); + if (input) { + Drupal.behaviors.clickToSelect.clickHandler(clickToSelectEl, input); + } + if (input.classList.contains('error')) { + clickToSelectEl.classList.add('error'); + } + if (input.checked) { + this.selectHandler(clickToSelectEl, input); + } + }, + ); + }, + // Adds click event listener to the field card. + clickHandler(clickToSelectEl, input) { + $(clickToSelectEl).on('click', (event) => { + const clickToSelect = event.target.closest('.js-click-to-select'); + this.selectHandler(clickToSelect, input); + $(input).trigger('updateOptions'); + }); + }, + // Handles adding and removing classes for the different states. + selectHandler(clickToSelect, input) { + $(input).on('focus', () => clickToSelect.classList.add('focus')); + $(input).on('blur', () => clickToSelect.classList.remove('focus')); + input.checked = true; + document + .querySelectorAll('.js-click-to-select.selected') + .forEach((item) => { + item.classList.remove('selected'); + }); + clickToSelect.classList.add('selected'); + // Ensure focus is added at the end of the process so wrap in + // a timeout. + setTimeout(() => { + // Remove the disabled attribute added by Drupal ajax so the + // element is focusable. This is safe as clicking the element + // multiple times causes no problems. + input.removeAttribute('disabled'); + input.focus(); + }, 0); + }, + }; })(jQuery, Drupal, drupalSettings, Drupal.debounce); diff --git a/core/modules/field_ui/src/Controller/FieldConfigAddController.php b/core/modules/field_ui/src/Controller/FieldConfigAddController.php new file mode 100644 index 0000000000..8cb929608b --- /dev/null +++ b/core/modules/field_ui/src/Controller/FieldConfigAddController.php @@ -0,0 +1,67 @@ +get('tempstore.private')->get('field_ui'), + ); + } + + /** + * Builds the field config instance form. + * + * @param string $entity_type + * The entity type. + * @param string $field_name + * The name of the field to create. + * + * @return array + * The field instance edit form. + */ + public function fieldConfigAddConfigureForm(string $entity_type, string $field_name): array { + // @see \Drupal\field_ui\Form\FieldStorageAddForm::submitForm + $temp_storage = $this->tempStore->get($entity_type . ':' . $field_name); + if (!$temp_storage) { + throw new NotFoundHttpException(); + } + + /** @var \Drupal\Core\Field\FieldConfigInterface $entity */ + $entity = $this->entityTypeManager()->getStorage('field_config')->create([ + ...$temp_storage['field_config_values'], + 'field_storage' => $temp_storage['field_storage'], + ]); + + return $this->entityFormBuilder()->getForm($entity, 'default', [ + 'default_options' => $temp_storage['default_options'], + ]); + } + +} diff --git a/core/modules/field_ui/src/FieldConfigListBuilder.php b/core/modules/field_ui/src/FieldConfigListBuilder.php index 24cf2cdd91..0e4fbfeb48 100644 --- a/core/modules/field_ui/src/FieldConfigListBuilder.php +++ b/core/modules/field_ui/src/FieldConfigListBuilder.php @@ -204,13 +204,6 @@ public function getDefaultOperations(EntityInterface $entity) { ]; } - $operations['storage-settings'] = [ - 'title' => $this->t('Storage settings'), - 'weight' => 20, - 'attributes' => ['title' => $this->t('Edit storage settings.')], - 'url' => $entity->toUrl("{$entity->getTargetEntityTypeId()}-storage-edit-form"), - ]; - return $operations; } diff --git a/core/modules/field_ui/src/Form/FieldConfigEditForm.php b/core/modules/field_ui/src/Form/FieldConfigEditForm.php index acd2787263..98b2437fd1 100644 --- a/core/modules/field_ui/src/Form/FieldConfigEditForm.php +++ b/core/modules/field_ui/src/Form/FieldConfigEditForm.php @@ -2,13 +2,18 @@ namespace Drupal\field_ui\Form; +use Drupal\Core\Entity\EntityDisplayRepositoryInterface; use Drupal\Core\Entity\EntityForm; +use Drupal\Core\Entity\EntityInterface; +use Drupal\Core\Entity\EntityStorageException; use Drupal\Core\Entity\EntityTypeBundleInfoInterface; use Drupal\Core\Entity\FieldableEntityInterface; use Drupal\Core\Entity\Plugin\DataType\EntityAdapter; use Drupal\Core\Field\FieldFilteredMarkup; use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Form\SubformState; use Drupal\Core\Render\Element; +use Drupal\Core\TempStore\PrivateTempStore; use Drupal\Core\TypedData\TypedDataInterface; use Drupal\Core\TypedData\TypedDataManagerInterface; use Drupal\Core\Url; @@ -23,6 +28,8 @@ */ class FieldConfigEditForm extends EntityForm { + use FieldStorageCreationTrait; + /** * The entity being used by this form. * @@ -37,6 +44,20 @@ class FieldConfigEditForm extends EntityForm { */ protected $entityTypeBundleInfo; + /** + * The name of the entity type. + * + * @var string + */ + protected string $entityTypeId; + + /** + * The entity bundle. + * + * @var string + */ + protected string $bundle; + /** * Constructs a new FieldConfigDeleteForm object. * @@ -44,9 +65,25 @@ class FieldConfigEditForm extends EntityForm { * The entity type bundle info service. * @param \Drupal\Core\TypedData\TypedDataManagerInterface $typedDataManager * The type data manger. + * @param \Drupal\Core\Entity\EntityDisplayRepositoryInterface|null $entityDisplayRepository + * The entity display repository. + * @param \Drupal\Core\TempStore\PrivateTempStore|null $tempStore + * The private tempstore. */ - public function __construct(EntityTypeBundleInfoInterface $entity_type_bundle_info, protected TypedDataManagerInterface $typedDataManager) { + public function __construct( + EntityTypeBundleInfoInterface $entity_type_bundle_info, + protected TypedDataManagerInterface $typedDataManager, + protected ?EntityDisplayRepositoryInterface $entityDisplayRepository = NULL, + protected ?PrivateTempStore $tempStore = NULL) { $this->entityTypeBundleInfo = $entity_type_bundle_info; + if ($this->entityDisplayRepository === NULL) { + @trigger_error('Calling FieldConfigEditForm::__construct() without the $entityDisplayRepository argument is deprecated in drupal:10.2.0 and will be required in drupal:11.0.0. See https://www.drupal.org/node/3383771', E_USER_DEPRECATED); + $this->entityDisplayRepository = \Drupal::service('entity_display.repository'); + } + if ($this->tempStore === NULL) { + @trigger_error('Calling FieldConfigEditForm::__construct() without the $tempStore argument is deprecated in drupal:10.2.0 and will be required in drupal:11.0.0. See https://www.drupal.org/node/3383771', E_USER_DEPRECATED); + $this->tempStore = \Drupal::service('tempstore.private')->get('field_ui'); + } } /** @@ -55,15 +92,25 @@ public function __construct(EntityTypeBundleInfoInterface $entity_type_bundle_in public static function create(ContainerInterface $container) { return new static( $container->get('entity_type.bundle.info'), - $container->get('typed_data_manager') + $container->get('typed_data_manager'), + $container->get('entity_display.repository'), + $container->get('tempstore.private')->get('field_ui'), ); } + /** + * {@inheritdoc} + */ + public function getFormId() { + return 'field_config_edit_form'; + } + /** * {@inheritdoc} */ public function form(array $form, FormStateInterface $form_state) { $form = parent::form($form, $form_state); + $form['#entity_builders'][] = 'field_form_field_config_edit_form_entity_builder'; $field_storage = $this->entity->getFieldStorageDefinition(); $bundles = $this->entityTypeBundleInfo->getBundleInfo($this->entity->getTargetEntityTypeId()); @@ -113,10 +160,44 @@ public function form(array $form, FormStateInterface $form_state) { 'bundle' => $this->entity->getTargetBundle(), 'entity_id' => NULL, ]; + $form['field_storage'] = [ + '#type' => 'fieldset', + '#title' => $this->t('Field Storage'), + '#weight' => -15, + '#tree' => TRUE, + ]; + $form['field_storage']['subform'] = [ + '#parents' => ['field_storage', 'subform'], + ]; + $subform_state = SubformState::createForSubform($form['field_storage']['subform'], $form, $form_state); + $field_storage_form = $this->entityTypeManager->getFormObject('field_storage_config', $this->operation); + $field_storage_form->setEntity($field_storage); + $form['field_storage']['subform'] = $field_storage_form->buildForm($form['field_storage']['subform'], $subform_state, $this->entity); + $form['#entity'] = _field_create_entity_from_ids($ids); - $items = $this->getTypedData($form['#entity']); + $items = $this->getTypedData($this->entity, $form['#entity']); $item = $items->first() ?: $items->appendItem(); + unset($form['field_storage']['subform']['actions']); + $this->addAjaxCallBacks($form['field_storage']['subform']); + + if (isset($form['field_storage']['subform']['cardinality_container'])) { + $form['field_storage']['subform']['cardinality_container']['#parents'] = [ + 'field_storage', + 'subform', + ]; + } + $form['field_storage']['subform']['field_storage_submit'] = [ + '#type' => 'submit', + '#name' => 'field_storage_submit', + '#attributes' => [ + 'class' => ['js-hide'], + ], + '#value' => $this->t('Update settings'), + '#process' => ['::processFieldStorageSubmit'], + '#limit_validation_errors' => [$form['field_storage']['subform']['#parents']], + '#submit' => ['::fieldStorageSubmit'], + ]; // Add field settings for the field type and a container for third party // settings that modules can add to via hook_form_FORM_ID_alter(). $form['settings'] = [ @@ -131,7 +212,7 @@ public function form(array $form, FormStateInterface $form_state) { // Create a new instance of typed data for the field to ensure that default // value widget is always rendered from a clean state. - $items = $this->getTypedData($form['#entity']); + $items = $this->getTypedData($this->entity, $form['#entity']); // Add handling for default value. if ($element = $items->defaultValuesForm($form, $form_state)) { @@ -164,10 +245,50 @@ public function form(array $form, FormStateInterface $form_state) { $form['default_value'] = $element; } - + $form['#prefix'] = '
'; + $form['#suffix'] = '
'; return $form; } + /** + * {@inheritdoc} + */ + public function afterBuild(array $element, FormStateInterface $form_state) { + // Delegate ::afterBuild to the subform. + // @todo remove after https://www.drupal.org/i/3385205 has been addressed. + if (isset($element['field_storage_submit'])) { + $field_storage_form = $this->entityTypeManager->getFormObject('field_storage_config', $this->operation); + $field_storage_form->setEntity($this->entity->getFieldStorageDefinition()); + return $field_storage_form->afterBuild($element, SubformState::createForSubform($element, $form_state->getCompleteForm(), $form_state)); + } + + return parent::afterBuild($element, $form_state); + } + + /** + * {@inheritdoc} + */ + protected function copyFormValuesToEntity(EntityInterface $entity, array $form, FormStateInterface $form_state) { + parent::copyFormValuesToEntity($entity, $form, $form_state); + + // Update the current field storage instance based on subform state. + if (!empty($form['field_storage']['subform'])) { + $subform_state = SubformState::createForSubform($form['field_storage']['subform'], $form, $form_state); + $field_storage_form = $this->entityTypeManager->getFormObject('field_storage_config', $this->operation); + $field_storage_form->setEntity($entity->getFieldStorageDefinition()); + + $reflector = new \ReflectionObject($entity); + + // Update the field storage entity based on subform values. + $property = $reflector->getProperty('fieldStorage'); + $property->setValue($entity, $field_storage_form->buildEntity($form['field_storage']['subform'], $subform_state)); + + // Remove the item definition to make sure it's not storing stale data. + $property = $reflector->getProperty('itemDefinition'); + $property->setValue($entity, NULL); + } + } + /** * A function to check if element contains any required elements. * @@ -230,16 +351,23 @@ protected function actions(array $form, FormStateInterface $form_state) { public function validateForm(array &$form, FormStateInterface $form_state) { parent::validateForm($form, $form_state); - // Before proceeding validation, rebuild the entity to make sure it's - // up-to-date. This is needed because element validators may update form - // state, and other validators use the entity for validating the field. - // @todo remove in https://www.drupal.org/project/drupal/issues/3372934. - $this->entity = $this->buildEntity($form, $form_state); + $field_storage_form = $this->entityTypeManager->getFormObject('field_storage_config', $this->operation); + $field_storage_form->setEntity($this->entity->getFieldStorageDefinition()); + $subform_state = SubformState::createForSubform($form['field_storage']['subform'], $form, $form_state); + $field_storage_form->validateForm($form['field_storage']['subform'], $subform_state); + // Make sure that the default value form is validated using the field + // configuration that was just submitted. + $field_config = $this->buildEntity($form, $form_state); if (isset($form['default_value']) && (!isset($form['set_default_value']) || $form_state->getValue('set_default_value'))) { - $items = $this->getTypedData($form['#entity']); + $items = $this->getTypedData($field_config, $form['#entity']); $items->defaultValuesFormValidate($form['default_value'], $form, $form_state); } + + // The form is rendered based on the entity property, meaning that it must + // be updated based on the latest form state even though it might be invalid + // at this point. + $this->entity = $this->buildEntity($form, $form_state); } /** @@ -248,10 +376,21 @@ public function validateForm(array &$form, FormStateInterface $form_state) { public function submitForm(array &$form, FormStateInterface $form_state) { parent::submitForm($form, $form_state); + $field_storage_form = $this->entityTypeManager->getFormObject('field_storage_config', $this->operation); + $field_storage_form->setEntity($this->entity->getFieldStorageDefinition()); + $field_storage_form->submitForm($form['field_storage']['subform'], SubformState::createForSubform($form['field_storage']['subform'], $form, $form_state)); + try { + $field_storage_form->save($form['field_storage']['subform'], SubformState::createForSubform($form['field_storage']['subform'], $form, $form_state)); + } + catch (EntityStorageException $exception) { + $this->handleEntityStorageException($form_state, $exception); + return; + } + // Handle the default value. $default_value = []; if (isset($form['default_value']) && (!isset($form['set_default_value']) || $form_state->getValue('set_default_value'))) { - $items = $this->getTypedData($form['#entity']); + $items = $this->getTypedData($this->entity, $form['#entity']); $default_value = $items->defaultValuesFormSubmit($form['default_value'], $form, $form_state); } $this->entity->setDefaultValue($default_value); @@ -261,17 +400,52 @@ public function submitForm(array &$form, FormStateInterface $form_state) { * {@inheritdoc} */ public function save(array $form, FormStateInterface $form_state) { - $this->entity->save(); + // Save field config. + try { + try { + $this->entity->save(); + } + catch (EntityStorageException $exception) { + $this->handleEntityStorageException($form_state, $exception); + return; + } - $this->messenger()->addStatus($this->t('Saved %label configuration.', ['%label' => $this->entity->getLabel()])); + if (isset($form_state->getStorage()['default_options'])) { + $default_options = $form_state->getStorage()['default_options']; + // Configure the default display modes. + $this->entityTypeId = $this->entity->getTargetEntityTypeId(); + $this->bundle = $this->entity->getTargetBundle(); + $this->configureEntityFormDisplay($this->entity->getName(), $default_options['entity_form_display'] ?? []); + $this->configureEntityViewDisplay($this->entity->getName(), $default_options['entity_view_display'] ?? []); + } - $request = $this->getRequest(); - if (($destinations = $request->query->all('destinations')) && $next_destination = FieldUI::getNextDestination($destinations)) { - $request->query->remove('destinations'); - $form_state->setRedirectUrl($next_destination); + if ($this->entity->isNew()) { + // Delete the temp store entry. + $this->tempStore->delete($this->entity->getTargetEntityTypeId() . ':' . $this->entity->getName()); + } + + $this->messenger() + ->addStatus($this->t('Saved %label configuration.', ['%label' => $this->entity->getLabel()])); + + $request = $this->getRequest(); + if (($destinations = $request->query->all('destinations')) && $next_destination = FieldUI::getNextDestination($destinations)) { + $request->query->remove('destinations'); + $form_state->setRedirectUrl($next_destination); + } + else { + $form_state->setRedirectUrl(FieldUI::getOverviewRouteInfo($this->entity->getTargetEntityTypeId(), $this->entity->getTargetBundle())); + } } - else { - $form_state->setRedirectUrl(FieldUI::getOverviewRouteInfo($this->entity->getTargetEntityTypeId(), $this->entity->getTargetBundle())); + catch (\Exception $e) { + $this->messenger()->addStatus( + $this->t( + 'Attempt to update field %label failed: %message.', + [ + '%label' => $this->entity->getLabel(), + '%message' => $e->getMessage(), + ] + ) + ); } } @@ -291,14 +465,91 @@ public function getTitle(FieldConfigInterface $field_config) { /** * Gets typed data object for the field. * + * @param \Drupal\field\FieldConfigInterface $field_config + * The field configuration. * @param \Drupal\Core\Entity\FieldableEntityInterface $parent * The parent entity that the field is attached to. * * @return \Drupal\Core\TypedData\TypedDataInterface */ - private function getTypedData(FieldableEntityInterface $parent): TypedDataInterface { + private function getTypedData(FieldConfigInterface $field_config, FieldableEntityInterface $parent): TypedDataInterface { + // Make sure that typed data manager is re-generating the instance. This + // important because we want the returned instance to match the current + // state, which could be different from what has been stored in config. + $this->typedDataManager->clearCachedDefinitions(); + $entity_adapter = EntityAdapter::createFromEntity($parent); - return $this->typedDataManager->create($this->entity, $this->entity->getDefaultValue($parent), $this->entity->getName(), $entity_adapter); + return $this->typedDataManager->create($field_config, $field_config->getDefaultValue($parent), $field_config->getName(), $entity_adapter); + } + + /** + * Process handler for subform submit. + */ + public static function processFieldStorageSubmit(array $element, FormStateInterface $form_state, &$complete_form) { + // Limit validation errors to the field storage form while the field storage + // form is being edited. + $complete_form['#limit_validation_errors'] = [array_slice($element['#parents'], 0, -1)]; + return $element; + } + + /** + * Submit handler for subform submit. + * + * @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. + */ + public function fieldStorageSubmit(&$form, FormStateInterface $form_state) { + // The default value widget needs to be regenerated. + $form_storage = &$form_state->getStorage(); + unset($form_storage['default_value_widget']); + $form_state->setRebuild(); + } + + /** + * Add Ajax callback for all inputs. + * + * @param array $form + * An associative array containing the structure of the form. + */ + private function addAjaxCallBacks(array &$form): void { + $field_types = [ + 'checkbox', + 'select', + 'radios', + 'textarea', + 'number', + 'textfield', + ]; + if (isset($form['#type']) && in_array($form['#type'], $field_types) && !isset($form['#ajax'])) { + $form['#ajax'] = [ + 'trigger_as' => ['name' => 'field_storage_submit'], + 'wrapper' => 'field-combined', + 'event' => 'change', + ]; + } + + foreach (Element::children($form) as $child_key) { + $this->addAjaxCallBacks($form[$child_key]); + } + } + + /** + * Handles entity storage exceptions and redirects the form. + * + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The form state. + * @param \Drupal\Core\Entity\EntityStorageException $exception + * The exception. + */ + protected function handleEntityStorageException(FormStateInterface $form_state, EntityStorageException $exception): void { + $this->tempStore->delete($this->entity->getTargetEntityTypeId() . ':' . $this->entity->getName()); + $form_state->setRedirectUrl(FieldUI::getOverviewRouteInfo($this->entity->getTargetEntityTypeId(), + $this->entity->getTargetBundle())); + $this->messenger() + ->addError($this->t('An error occurred while saving the field: @error', + ['@error' => $exception->getMessage()])); } } diff --git a/core/modules/field_ui/src/Form/FieldStorageAddForm.php b/core/modules/field_ui/src/Form/FieldStorageAddForm.php index a2b19ad877..560e8d7c72 100644 --- a/core/modules/field_ui/src/Form/FieldStorageAddForm.php +++ b/core/modules/field_ui/src/Form/FieldStorageAddForm.php @@ -2,13 +2,17 @@ namespace Drupal\field_ui\Form; +use Drupal\Component\Utility\Html; +use Drupal\Component\Utility\SortArray; use Drupal\Core\Config\ConfigFactoryInterface; -use Drupal\Core\Entity\EntityDisplayRepositoryInterface; use Drupal\Core\Entity\EntityFieldManagerInterface; use Drupal\Core\Entity\EntityTypeManagerInterface; +use Drupal\Core\Field\FallbackFieldTypeCategory; +use Drupal\Core\Field\FieldTypeCategoryManagerInterface; use Drupal\Core\Field\FieldTypePluginManagerInterface; use Drupal\Core\Form\FormBase; use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\TempStore\PrivateTempStore; use Drupal\field\Entity\FieldStorageConfig; use Drupal\field_ui\FieldUI; use Symfony\Component\DependencyInjection\ContainerInterface; @@ -20,8 +24,6 @@ */ class FieldStorageAddForm extends FormBase { - use FieldStorageCreationTrait; - /** * The name of the entity type. * @@ -50,13 +52,6 @@ class FieldStorageAddForm extends FormBase { */ protected $entityFieldManager; - /** - * The entity display repository. - * - * @var \Drupal\Core\Entity\EntityDisplayRepositoryInterface - */ - protected $entityDisplayRepository; - /** * The field type plugin manager. * @@ -80,17 +75,26 @@ class FieldStorageAddForm extends FormBase { * The field type plugin manager. * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory * The configuration factory. - * @param \Drupal\Core\Entity\EntityFieldManagerInterface|null $entity_field_manager + * @param \Drupal\Core\Entity\EntityFieldManagerInterface $entity_field_manager * (optional) The entity field manager. - * @param \Drupal\Core\Entity\EntityDisplayRepositoryInterface $entity_display_repository - * (optional) The entity display repository. + * @param \Drupal\Core\TempStore\PrivateTempStore|null $tempStore + * The private tempstore. + * @param \Drupal\Core\Field\FieldTypeCategoryManagerInterface|null $fieldTypeCategoryManager + * The field type category plugin manager. */ - public function __construct(EntityTypeManagerInterface $entity_type_manager, FieldTypePluginManagerInterface $field_type_plugin_manager, ConfigFactoryInterface $config_factory, EntityFieldManagerInterface $entity_field_manager = NULL, EntityDisplayRepositoryInterface $entity_display_repository = NULL) { + public function __construct(EntityTypeManagerInterface $entity_type_manager, FieldTypePluginManagerInterface $field_type_plugin_manager, ConfigFactoryInterface $config_factory, EntityFieldManagerInterface $entity_field_manager, protected ?PrivateTempStore $tempStore = NULL, protected ?FieldTypeCategoryManagerInterface $fieldTypeCategoryManager = NULL) { $this->entityTypeManager = $entity_type_manager; $this->fieldTypePluginManager = $field_type_plugin_manager; $this->configFactory = $config_factory; $this->entityFieldManager = $entity_field_manager; - $this->entityDisplayRepository = $entity_display_repository; + if ($this->tempStore === NULL) { + @trigger_error('Calling FieldStorageAddForm::__construct() without the $tempStore argument is deprecated in drupal:10.2.0 and will be required in drupal:11.0.0. See https://www.drupal.org/node/3383719', E_USER_DEPRECATED); + $this->tempStore = \Drupal::service('tempstore.private')->get('field_ui'); + } + if ($this->fieldTypeCategoryManager === NULL) { + @trigger_error('Calling FieldStorageAddForm::__construct() without the $fieldTypeCategoryManager argument is deprecated in drupal:10.2.0 and will be required in drupal:11.0.0. See https://www.drupal.org/node/3375740', E_USER_DEPRECATED); + $this->fieldTypeCategoryManager = \Drupal::service('plugin.manager.field.field_type_category'); + } } /** @@ -109,7 +113,8 @@ public static function create(ContainerInterface $container) { $container->get('plugin.manager.field.field_type'), $container->get('config.factory'), $container->get('entity_field.manager'), - $container->get('entity_display.repository') + $container->get('tempstore.private')->get('field_ui'), + $container->get('plugin.manager.field.field_type_category'), ); } @@ -123,33 +128,15 @@ public function buildForm(array $form, FormStateInterface $form_state, $entity_t if (!$form_state->get('bundle')) { $form_state->set('bundle', $bundle); } - $this->entityTypeId = $form_state->get('entity_type_id'); $this->bundle = $form_state->get('bundle'); - // Gather valid field types. - $field_type_options = []; - foreach ($this->fieldTypePluginManager->getGroupedDefinitions($this->fieldTypePluginManager->getUiDefinitions()) as $category => $field_types) { - foreach ($field_types as $name => $field_type) { - $field_type_options[$category][$name] = $field_type['label']; - } - } - - $form['add'] = [ - '#type' => 'container', - '#attributes' => ['class' => ['form--inline', 'clearfix']], - ]; - - $form['add']['new_storage_type'] = [ - '#type' => 'select', - '#title' => $this->t('Add a new field'), - '#options' => $field_type_options, - '#empty_option' => $this->t('- Select a field type -'), - ]; - // Field label and field_name. $form['new_storage_wrapper'] = [ '#type' => 'container', + '#attributes' => [ + 'class' => ['field-ui-new-storage-wrapper'], + ], '#states' => [ '!visible' => [ ':input[name="new_storage_type"]' => ['value' => ''], @@ -159,9 +146,176 @@ public function buildForm(array $form, FormStateInterface $form_state, $entity_t $form['new_storage_wrapper']['label'] = [ '#type' => 'textfield', '#title' => $this->t('Label'), - '#size' => 15, + '#size' => 30, ]; + $field_type_options = $unique_definitions = []; + $grouped_definitions = $this->fieldTypePluginManager->getGroupedDefinitions($this->fieldTypePluginManager->getUiDefinitions(), 'label', 'id'); + $category_definitions = $this->fieldTypeCategoryManager->getDefinitions(); + // Invoke a hook to get category properties. + foreach ($grouped_definitions as $category => $field_types) { + foreach ($field_types as $name => $field_type) { + $unique_definitions[$category][$name] = ['unique_identifier' => $name] + $field_type; + if ($this->fieldTypeCategoryManager->hasDefinition($category)) { + $category_plugin = $this->fieldTypeCategoryManager->createInstance($category, $unique_definitions[$category][$name], $category_definitions[$category]); + $field_type_options[$category_plugin->getPluginId()] = ['unique_identifier' => $name] + $field_type; + } + else { + $field_type_options[(string) $field_type['label']] = ['unique_identifier' => $name] + $field_type; + } + } + } + $form['add-label'] = [ + '#type' => 'label', + '#title' => t('Choose a type of field'), + '#required' => TRUE, + ]; + + $form['add'] = [ + '#type' => 'container', + '#attributes' => [ + 'class' => 'add-field-container', + ], + ]; + $field_type_options_radios = []; + foreach ($field_type_options as $id => $field_type) { + /** @var \Drupal\Core\Field\FieldTypeCategoryInterface $category_info */ + $category_info = $this->fieldTypeCategoryManager->createInstance($field_type['category'], $field_type); + $display_as_group = !($category_info instanceof FallbackFieldTypeCategory); + $cleaned_class_name = Html::getClass($field_type['unique_identifier']); + $field_type_options_radios[$id] = [ + '#type' => 'container', + '#attributes' => [ + 'class' => ['field-option', 'js-click-to-select'], + 'checked' => $this->getRequest()->request->get('new_storage_type') !== NULL && $this->getRequest()->request->get('new_storage_type') == ($display_as_group ? $field_type['category'] : $field_type['unique_identifier']), + ], + '#weight' => $category_info->getWeight(), + 'thumb' => [ + '#type' => 'container', + '#attributes' => [ + 'class' => ['field-option__thumb'], + ], + 'icon' => [ + '#type' => 'container', + '#attributes' => [ + 'class' => ['field-option__icon', $display_as_group ? + "field-icon-$field_type[category]" : "field-icon-$cleaned_class_name", + ], + ], + ], + ], + // Store some data we later need. + '#data' => [ + '#group_display' => $display_as_group, + ], + 'radio' => [ + '#type' => 'radio', + '#title' => $category_info->getLabel(), + '#parents' => ['new_storage_type'], + '#title_display' => 'before', + '#description_display' => 'before', + '#theme_wrappers' => ['form_element__new_storage_type'], + // If it is a category, set return value as the category label, + // otherwise, set it as the field type id. + '#return_value' => $display_as_group ? $field_type['category'] : $field_type['unique_identifier'], + '#attributes' => [ + 'class' => ['field-option-radio'], + ], + '#ajax' => [ + 'callback' => [$this, 'showFieldsCallback'], + 'event' => 'updateOptions', + 'wrapper' => 'group-field-options-wrapper', + 'progress' => 'none', + 'disable-refocus' => TRUE, + ], + '#description' => [ + '#type' => 'container', + '#attributes' => [ + 'class' => ['field-option__description'], + ], + '#markup' => $category_info->getDescription(), + ], + '#variant' => 'field-option', + ], + ]; + + if ($libraries = $category_info->getLibraries()) { + $field_type_options_radios[$id]['#attached']['library'] = $libraries; + } + } + uasort($field_type_options_radios, [SortArray::class, 'sortByWeightProperty']); + $form['add']['new_storage_type'] = $field_type_options_radios; + $form['group_submit'] = [ + '#type' => 'submit', + '#value' => $this->t('Change field group'), + '#limit_validation_errors' => [], + '#attributes' => [ + 'class' => ['js-hide'], + ], + '#submit' => [[static::class, 'showFieldsHandler']], + ]; + $form['group_field_options_wrapper'] = [ + '#prefix' => '
', + '#suffix' => '
', + ]; + + // Set the selected field to the form state by checking + // the checked attribute. + $selected_field_type = NULL; + foreach ($field_type_options_radios as $field_type_options_radio) { + if ($field_type_options_radio['#attributes']['checked']) { + $selected_field_type = $field_type_options_radio['radio']['#return_value']; + $form_state->setValue('selected_field_type', $selected_field_type); + break; + } + } + if (isset($selected_field_type)) { + $group_display = $field_type_options_radios[$selected_field_type]['#data']['#group_display']; + if ($group_display) { + $form['group_field_options_wrapper']['label'] = [ + '#type' => 'label', + '#title' => t('Choose an option below'), + '#required' => TRUE, + ]; + $form['group_field_options_wrapper']['fields'] = [ + '#type' => 'container', + '#attributes' => [ + 'class' => ['group-field-options'], + ], + ]; + + foreach ($unique_definitions[$selected_field_type] as $option_key => $option) { + $radio_element = [ + '#type' => 'radio', + '#theme_wrappers' => ['form_element__new_storage_type'], + '#title' => $option['label'], + '#description' => [ + '#theme' => 'item_list', + '#items' => $unique_definitions[$selected_field_type][$option_key]['description'], + ], + '#id' => $option['unique_identifier'], + '#weight' => $option['weight'], + '#parents' => ['group_field_options_wrapper'], + '#attributes' => [ + 'class' => ['field-option-radio'], + 'data-once' => 'field-click-to-select', + ], + '#wrapper_attributes' => [ + 'class' => ['js-click-to-select', 'subfield-option'], + ], + '#variant' => 'field-suboption', + ]; + $radio_element['#return_value'] = $option['unique_identifier']; + if ((string) $option['unique_identifier'] === 'entity_reference') { + $radio_element['#title'] = 'Other'; + $radio_element['#weight'] = 10; + } + $group_field_options[$option['unique_identifier']] = $radio_element; + } + uasort($group_field_options, [SortArray::class, 'sortByWeightProperty']); + $form['group_field_options_wrapper']['fields'] += $group_field_options; + } + } $field_prefix = $this->config('field_ui.settings')->get('field_prefix'); $form['new_storage_wrapper']['field_name'] = [ '#type' => 'machine_name', @@ -177,7 +331,6 @@ public function buildForm(array $form, FormStateInterface $form_state, $entity_t ], '#required' => FALSE, ]; - // Place the 'translatable' property as an explicit value so that contrib // modules can form_alter() the value for newly created fields. By default // we create field storage as translatable so it will be possible to enable @@ -190,12 +343,15 @@ public function buildForm(array $form, FormStateInterface $form_state, $entity_t $form['actions'] = ['#type' => 'actions']; $form['actions']['submit'] = [ '#type' => 'submit', - '#value' => $this->t('Save and continue'), + '#value' => $this->t('Continue'), '#button_type' => 'primary', ]; - $form['#attached']['library'][] = 'field_ui/drupal.field_ui'; - + $form['#attached']['library'] = [ + 'field_ui/drupal.field_ui', + 'field_ui/drupal.field_ui.manage_fields', + 'core/drupal.ajax', + ]; return $form; } @@ -203,15 +359,11 @@ public function buildForm(array $form, FormStateInterface $form_state, $entity_t * {@inheritdoc} */ public function validateForm(array &$form, FormStateInterface $form_state) { - // Missing field type. if (!$form_state->getValue('new_storage_type')) { $form_state->setErrorByName('new_storage_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('new_storage_type') && $form_state->getValue('existing_storage_name')) { - $form_state->setErrorByName('new_storage_type', $this->t('Adding a new field and re-using an existing field at the same time is not allowed.')); - return; + elseif (isset($form['group_field_options_wrapper']['fields']) && !$form_state->getValue('group_field_options_wrapper')) { + $form_state->setErrorByName('group_field_options_wrapper', $this->t('You need to select a field type.')); } $this->validateAddNew($form, $form_state); @@ -256,17 +408,18 @@ protected function validateAddNew(array $form, FormStateInterface $form_state) { public function submitForm(array &$form, FormStateInterface $form_state) { $values = $form_state->getValues(); $entity_type = $this->entityTypeManager->getDefinition($this->entityTypeId); + + $field_storage_type = $values['group_field_options_wrapper'] ?? $values['new_storage_type']; + $field_name = $values['field_name']; $field_values = [ 'entity_type' => $this->entityTypeId, 'bundle' => $this->bundle, ]; - $field_name = $values['field_name']; - $field_storage_type = $values['new_storage_type']; $default_options = []; // Check if we're dealing with a preconfigured field. if (strpos($field_storage_type, 'field_ui:') === 0) { - list(, $field_type, $preset_key) = explode(':', $field_storage_type, 3); + [, $field_type, $preset_key] = explode(':', $field_storage_type, 3); $default_options = $this->getNewFieldDefaults($field_type, $preset_key); } else { @@ -289,38 +442,28 @@ public function submitForm(array &$form, FormStateInterface $form_state) { ]; try { - // Create the field storage. - $this->entityTypeManager->getStorage('field_storage_config') - ->create($field_storage_values)->save(); - - // Create the field. - $field = $this->entityTypeManager->getStorage('field_config') - ->create($field_values); - $field->save(); - - // Configure the display modes. - $this->configureEntityFormDisplay($field_name, $default_options['entity_form_display'] ?? []); - $this->configureEntityViewDisplay($field_name, $default_options['entity_view_display'] ?? []); + $field_storage_entity = $this->entityTypeManager->getStorage('field_storage_config')->create($field_storage_values); } catch (\Exception $e) { $this->messenger()->addError($this->t('There was a problem creating field %label: @message', ['%label' => $values['label'], '@message' => $e->getMessage()])); return; } + // Save field and field storage values in tempstore. + $this->tempStore->set($this->entityTypeId . ':' . $field_name, [ + 'field_storage' => $field_storage_entity, + 'field_config_values' => $field_values, + 'default_options' => $default_options, + ]); + // Configure next steps in the multi-part form. $destinations = []; $route_parameters = [ - 'field_config' => $field->id(), + 'entity_type' => $this->entityTypeId, + 'field_name' => $field_name, ] + FieldUI::getRouteBundleParameter($entity_type, $this->bundle); - // Always show the field settings step, as the cardinality needs to be - // configured for new fields. $destinations[] = [ - 'route_name' => "entity.field_config.{$this->entityTypeId}_storage_edit_form", - 'route_parameters' => $route_parameters, - ]; - - $destinations[] = [ - 'route_name' => "entity.field_config.{$this->entityTypeId}_field_edit_form", + 'route_name' => "field_ui.field_add_{$this->entityTypeId}", 'route_parameters' => $route_parameters, ]; $destinations[] = [ @@ -335,8 +478,6 @@ public function submitForm(array &$form, FormStateInterface $form_state) { // Store new field information for any additional submit handlers. $form_state->set(['fields_added', '_add_new_field'], $field_name); - - $this->messenger()->addMessage($this->t('Your settings have been saved.')); } /** @@ -382,7 +523,9 @@ protected function getNewFieldDefaults(string $field_name, string $preset_key): // Preconfigured options only apply to the default display modes. foreach (['entity_form_display', 'entity_view_display'] as $key) { if (isset($field_options[$key])) { - $default_options[$key] = ['default' => array_intersect_key($field_options[$key], ['type' => '', 'settings' => []])]; + $default_options[$key] = [ + 'default' => array_intersect_key($field_options[$key], ['type' => '', 'settings' => []]), + ]; } else { $default_options[$key] = ['default' => []]; @@ -413,4 +556,18 @@ public function fieldNameExists($value, $element, FormStateInterface $form_state return isset($field_storage_definitions[$field_name]); } + /** + * Callback for displaying fields after a group has been selected. + */ + public function showFieldsCallback($form, FormStateInterface &$form_state) { + return $form['group_field_options_wrapper']; + } + + /** + * Submit handler for displaying fields after a group is selected. + */ + public static function showFieldsHandler($form, FormStateInterface &$form_state) { + $form_state->setRebuild(); + } + } diff --git a/core/modules/field_ui/src/Form/FieldStorageConfigEditForm.php b/core/modules/field_ui/src/Form/FieldStorageConfigEditForm.php index eeb85e2259..9dc5cbc07d 100644 --- a/core/modules/field_ui/src/Form/FieldStorageConfigEditForm.php +++ b/core/modules/field_ui/src/Form/FieldStorageConfigEditForm.php @@ -3,11 +3,14 @@ namespace Drupal\field_ui\Form; use Drupal\Core\Entity\EntityForm; +use Drupal\Core\Entity\Plugin\DataType\EntityAdapter; use Drupal\Core\Field\FieldStorageDefinitionInterface; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Routing\RouteMatchInterface; +use Drupal\Core\TypedData\TypedDataManagerInterface; use Drupal\field\Entity\FieldConfig; -use Drupal\field_ui\FieldUI; +use Drupal\field\FieldConfigInterface; +use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; /** @@ -24,6 +27,24 @@ class FieldStorageConfigEditForm extends EntityForm { */ protected $entity; + /** + * FieldStorageConfigEditForm constructor. + * + * @param \Drupal\Core\TypedData\TypedDataManagerInterface $typedDataManager + * The typed data manager. + */ + public function __construct( + protected TypedDataManagerInterface $typedDataManager, + ) { + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static($container->get('typed_data_manager')); + } + /** * {@inheritdoc} */ @@ -45,12 +66,15 @@ public function getEntityFromRouteMatch(RouteMatchInterface $route_match, $entit * A nested array form elements comprising the form. * @param \Drupal\Core\Form\FormStateInterface $form_state * The current state of the form. - * @param string $field_config + * @param \Drupal\field\FieldConfigInterface|string $field_config * The ID of the field config whose field storage config is being edited. */ - public function buildForm(array $form, FormStateInterface $form_state, $field_config = NULL) { + public function buildForm(array $form, FormStateInterface $form_state, FieldConfigInterface|string $field_config = NULL) { if ($field_config) { - $field = FieldConfig::load($field_config); + $field = $field_config; + if (is_string($field)) { + $field = FieldConfig::load($field_config); + } $form_state->set('field_config', $field); $form_state->set('entity_type_id', $field->getTargetEntityTypeId()); @@ -67,7 +91,6 @@ public function form(array $form, FormStateInterface $form_state) { $form = parent::form($form, $form_state); $field_label = $form_state->get('field_config')->label(); - $form['#title'] = $field_label; $form['#prefix'] = '

' . $this->t('These settings apply to the %field field everywhere it is used. Some also impact the way that data is stored and cannot be changed once data has been created.', ['%field' => $field_label]) . '

'; // Add settings provided by the field module. The field module is @@ -85,7 +108,13 @@ public function form(array $form, FormStateInterface $form_state) { 'entity_id' => NULL, ]; $entity = _field_create_entity_from_ids($ids); - $items = $entity->get($this->entity->getName()); + if (!$this->entity->isNew()) { + $items = $entity->get($this->entity->getName()); + } + else { + $field_config = $form_state->get('field_config'); + $items = $this->typedDataManager->create($field_config, name: $this->entity->getName(), parent: EntityAdapter::createFromEntity($entity)); + } $item = $items->first() ?: $items->appendItem(); $form['settings'] += $item->storageSettingsForm($form, $form_state, $this->entity->hasData()); @@ -126,7 +155,7 @@ protected function getCardinalityForm() { $form['cardinality'] = ['#markup' => $markup]; } else { - $form['#element_validate'][] = '::validateCardinality'; + $form['#element_validate'][] = [$this, 'validateCardinality']; $cardinality = $this->entity->getCardinality(); $form['cardinality'] = [ '#type' => 'select', @@ -147,10 +176,10 @@ protected function getCardinalityForm() { '#size' => 2, '#states' => [ 'visible' => [ - ':input[name="cardinality"]' => ['value' => 'number'], + ':input[name="field_storage[subform][cardinality]"]' => ['value' => 'number'], ], 'disabled' => [ - ':input[name="cardinality"]' => ['value' => FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED], + ':input[id="field_storage[subform][cardinality]"]' => ['value' => FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED], ], ], ]; @@ -164,7 +193,7 @@ protected function getCardinalityForm() { */ protected function actions(array $form, FormStateInterface $form_state) { $elements = parent::actions($form, $form_state); - $elements['submit']['#value'] = $this->t('Save field settings'); + $elements['submit']['#value'] = $this->entity->isNew() ? $this->t('Continue') : $this->t('Save'); return $elements; } @@ -180,24 +209,33 @@ protected function actions(array $form, FormStateInterface $form_state) { public function validateCardinality(array &$element, FormStateInterface $form_state) { $field_storage_definitions = \Drupal::service('entity_field.manager')->getFieldStorageDefinitions($this->entity->getTargetEntityTypeId()); + $cardinality = $form_state->getValue([ + ...$element['#parents'], + 'cardinality', + ]); + $cardinality_number = $form_state->getValue([ + ...$element['#parents'], + 'cardinality_number', + ]); + // Validate field cardinality. - if ($form_state->getValue('cardinality') === 'number' && !$form_state->getValue('cardinality_number')) { + if ($cardinality === 'number' && !$cardinality_number) { $form_state->setError($element['cardinality_number'], $this->t('Number of values is required.')); } // If a specific cardinality is used, validate that there are no entities // with a higher delta. - elseif (!$this->entity->isNew() && isset($field_storage_definitions[$this->entity->getName()]) && $form_state->getValue('cardinality') != FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED) { + elseif (!$this->entity->isNew() && isset($field_storage_definitions[$this->entity->getName()]) && $cardinality != FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED) { // Get a count of entities that have a value in a delta higher than the // one selected. Deltas start with 0, so the selected value does not // need to be incremented. $entities_with_higher_delta = \Drupal::entityQuery($this->entity->getTargetEntityTypeId()) ->accessCheck(FALSE) - ->condition($this->entity->getName() . '.%delta', $form_state->getValue('cardinality')) + ->condition($this->entity->getName() . '.%delta', $cardinality_number) ->count() ->execute(); if ($entities_with_higher_delta) { - $form_state->setError($element['cardinality_number'], $this->formatPlural($entities_with_higher_delta, 'There is @count entity with @delta or more values in this field, so the allowed number of values cannot be set to @allowed.', 'There are @count entities with @delta or more values in this field, so the allowed number of values cannot be set to @allowed.', ['@delta' => $form_state->getValue('cardinality') + 1, '@allowed' => $form_state->getValue('cardinality')])); + $form_state->setError($element['cardinality_number'], $this->formatPlural($entities_with_higher_delta, 'There is @count entity with @delta or more values in this field, so the allowed number of values cannot be set to @allowed.', 'There are @count entities with @delta or more values in this field, so the allowed number of values cannot be set to @allowed.', ['@delta' => $cardinality_number + 1, '@allowed' => $cardinality_number])); } } } @@ -208,7 +246,7 @@ public function validateCardinality(array &$element, FormStateInterface $form_st public function buildEntity(array $form, FormStateInterface $form_state) { // Save field cardinality. if (!$this->getEnforcedCardinality() && $form_state->getValue('cardinality') === 'number' && $form_state->getValue('cardinality_number')) { - $form_state->setValue('cardinality', $form_state->getValue('cardinality_number')); + $form_state->setValue('cardinality', (int) $form_state->getValue('cardinality_number')); } return parent::buildEntity($form, $form_state); @@ -218,22 +256,7 @@ public function buildEntity(array $form, FormStateInterface $form_state) { * {@inheritdoc} */ public function save(array $form, FormStateInterface $form_state) { - $field_label = $form_state->get('field_config')->label(); - try { - $this->entity->save(); - $this->messenger()->addStatus($this->t('Updated field %label field settings.', ['%label' => $field_label])); - $request = $this->getRequest(); - if (($destinations = $request->query->all('destinations')) && $next_destination = FieldUI::getNextDestination($destinations)) { - $request->query->remove('destinations'); - $form_state->setRedirectUrl($next_destination); - } - else { - $form_state->setRedirectUrl(FieldUI::getOverviewRouteInfo($form_state->get('entity_type_id'), $form_state->get('bundle'))); - } - } - catch (\Exception $e) { - $this->messenger()->addStatus($this->t('Attempt to update field %label failed: %message.', ['%label' => $field_label, '%message' => $e->getMessage()])); - } + $this->entity->save(); } /** diff --git a/core/modules/field_ui/src/Plugin/Derivative/FieldUiLocalTask.php b/core/modules/field_ui/src/Plugin/Derivative/FieldUiLocalTask.php index deff201664..af4c5674d1 100644 --- a/core/modules/field_ui/src/Plugin/Derivative/FieldUiLocalTask.php +++ b/core/modules/field_ui/src/Plugin/Derivative/FieldUiLocalTask.php @@ -107,13 +107,6 @@ public function getDerivativeDefinitions($base_plugin_definition) { 'base_route' => "entity.field_config.{$entity_type_id}_field_edit_form", ]; - // Field settings tab. - $this->derivatives["field_storage_$entity_type_id"] = [ - 'route_name' => "entity.field_config.{$entity_type_id}_storage_edit_form", - 'title' => $this->t('Field settings'), - 'base_route' => "entity.field_config.{$entity_type_id}_field_edit_form", - ]; - // View and form modes secondary tabs. // The same base $path for the menu item (with a placeholder) can be // used for all bundles of a given entity type; but depending on diff --git a/core/modules/field_ui/src/Routing/RouteSubscriber.php b/core/modules/field_ui/src/Routing/RouteSubscriber.php index a84dde3181..52968245e7 100644 --- a/core/modules/field_ui/src/Routing/RouteSubscriber.php +++ b/core/modules/field_ui/src/Routing/RouteSubscriber.php @@ -5,6 +5,7 @@ use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Routing\RouteSubscriberBase; use Drupal\Core\Routing\RoutingEvents; +use Drupal\field_ui\Controller\FieldConfigAddController; use Symfony\Component\Routing\Route; use Symfony\Component\Routing\RouteCollection; @@ -71,14 +72,6 @@ protected function alterRoutes(RouteCollection $collection) { ); $collection->add("entity.field_config.{$entity_type_id}_field_edit_form", $route); - $route = new Route( - "$path/fields/{field_config}/storage", - ['_entity_form' => 'field_storage_config.edit'] + $defaults, - ['_permission' => 'administer ' . $entity_type_id . ' fields'], - $options - ); - $collection->add("entity.field_config.{$entity_type_id}_storage_edit_form", $route); - $route = new Route( "$path/fields/{field_config}/delete", ['_entity_form' => 'field_config.delete'] + $defaults, @@ -109,6 +102,17 @@ protected function alterRoutes(RouteCollection $collection) { ); $collection->add("field_ui.field_storage_config_add_$entity_type_id", $route); + $route = new Route( + "$path/add-field/{entity_type}/{field_name}", + [ + '_controller' => FieldConfigAddController::class . '::fieldConfigAddConfigureForm', + '_title' => 'Add field', + ] + $defaults, + ['_permission' => 'administer ' . $entity_type_id . ' fields'], + $options + ); + $collection->add("field_ui.field_add_$entity_type_id", $route); + $route = new Route( "$path/fields/reuse", [ diff --git a/core/modules/field_ui/templates/form-element--new-storage-type.html.twig b/core/modules/field_ui/templates/form-element--new-storage-type.html.twig new file mode 100644 index 0000000000..be1821fa26 --- /dev/null +++ b/core/modules/field_ui/templates/form-element--new-storage-type.html.twig @@ -0,0 +1,46 @@ +{# +/** + * @file + * Default theme implementation for a storage type option form element. + * + * Available variables: + * - attributes: HTML attributes for the containing element. + * - errors: (optional) Any errors for this form element, may not be set. + * - label: A rendered label element. + * - description: (optional) A list of description properties containing: + * - content: A description of the form element, may not be set. + * - attributes: (optional) A list of HTML attributes to apply to the + * description content wrapper. Will only be set when description is set. + * - variant: specifies option type. Typically 'field-option' or 'field-suboption'. + * + * @see template_preprocess_form_element() + * + * @ingroup themeable + */ +#} +{% + set classes = [ + errors ? 'form-item--error', + variant ? variant ~ '__item' + ] +%} + + {% if variant == 'field-option' %} + {{ label }} + + {{ description.content }} + + {% endif %} + {{ children }} + {% if variant == 'field-suboption' %} + {{ label }} + + {{ description.content }} + + {% endif %} + {% if errors %} +
+ {{ errors }} +
+ {% endif %} + diff --git a/core/modules/field_ui/tests/src/Functional/FieldTypeCategoriesIntegrationTest.php b/core/modules/field_ui/tests/src/Functional/FieldTypeCategoriesIntegrationTest.php new file mode 100644 index 0000000000..1490c2e841 --- /dev/null +++ b/core/modules/field_ui/tests/src/Functional/FieldTypeCategoriesIntegrationTest.php @@ -0,0 +1,60 @@ +drupalCreateUser(['administer node fields']); + $this->drupalLogin($admin_user); + } + + /** + * Tests if the libraries are loaded on FieldStorageAddForm. + */ + public function testLibrariesLoaded() { + $this->drupalGet('admin/structure/types/manage/' . $this->drupalCreateContentType()->id() . '/fields/add-field'); + $page_content = $this->getSession()->getPage()->getContent(); + $css_libraries = [ + 'drupal.file-icon', + 'drupal.text-icon', + 'drupal.options-icon', + 'drupal.comment-icon', + 'drupal.link-icon', + ]; + foreach ($css_libraries as $css_library) { + // Check if the library asset is present in the rendered HTML. + $this->assertStringContainsString($css_library, $page_content); + } + } + +} diff --git a/core/modules/field_ui/tests/src/Functional/ManageFieldsFunctionalTest.php b/core/modules/field_ui/tests/src/Functional/ManageFieldsFunctionalTest.php index 5235a3e457..c06a80303d 100644 --- a/core/modules/field_ui/tests/src/Functional/ManageFieldsFunctionalTest.php +++ b/core/modules/field_ui/tests/src/Functional/ManageFieldsFunctionalTest.php @@ -2,6 +2,7 @@ namespace Drupal\Tests\field_ui\Functional; +use Behat\Mink\Exception\ElementNotFoundException; use Drupal\Core\Entity\Entity\EntityFormDisplay; use Drupal\Core\Entity\Entity\EntityFormMode; use Drupal\Core\Entity\Entity\EntityViewDisplay; @@ -187,7 +188,7 @@ public function manageFieldsPage($type = '') { $this->assertSession()->linkExists('Create a new field'); // Assert entity operations for all fields. - $number_of_links = 3; + $number_of_links = 2; $number_of_links_found = 0; $operation_links = $this->xpath('//ul[@class = "dropbutton"]/li/a'); $url = base_path() . "admin/structure/types/manage/$type/fields/node.$type.body"; @@ -199,11 +200,6 @@ public function manageFieldsPage($type = '') { $number_of_links_found++; break; - case 'Edit storage settings.': - $this->assertSame("$url/storage", $link->getAttribute('href')); - $number_of_links_found++; - break; - case 'Delete field.': $this->assertSame("$url/delete", $link->getAttribute('href')); $number_of_links_found++; @@ -231,20 +227,15 @@ public function createField() { public function updateField() { $field_id = 'node.' . $this->contentType . '.' . $this->fieldName; // Go to the field edit page. - $this->drupalGet('admin/structure/types/manage/' . $this->contentType . '/fields/' . $field_id . '/storage'); + $this->drupalGet('admin/structure/types/manage/' . $this->contentType . '/fields/' . $field_id); $this->assertSession()->assertEscaped($this->fieldLabel); // Populate the field settings with new settings. $string = 'updated dummy test string'; - $edit = [ - 'settings[test_field_storage_setting]' => $string, - ]; - $this->submitForm($edit, 'Save field settings'); - // Go to the field edit page. - $this->drupalGet('admin/structure/types/manage/' . $this->contentType . '/fields/' . $field_id); $edit = [ 'settings[test_field_setting]' => $string, + 'field_storage[subform][settings][test_field_storage_setting]' => $string, ]; $this->assertSession()->pageTextContains('Default value'); $this->submitForm($edit, 'Save settings'); @@ -281,35 +272,29 @@ public function addExistingField() { * numeric value. That is tested already in FormTest::testNumber(). */ public function cardinalitySettings() { - $field_edit_path = 'admin/structure/types/manage/article/fields/node.article.body/storage'; + $field_edit_path = 'admin/structure/types/manage/article/fields/node.article.body'; // Assert the cardinality other field cannot be empty when cardinality is // set to 'number'. $edit = [ - 'cardinality' => 'number', - 'cardinality_number' => '', + 'field_storage[subform][cardinality]' => 'number', + 'field_storage[subform][cardinality_number]' => '', ]; $this->drupalGet($field_edit_path); - $this->submitForm($edit, 'Save field settings'); + $this->submitForm($edit, 'Update settings'); $this->assertSession()->pageTextContains('Number of values is required.'); // Submit a custom number. $edit = [ - 'cardinality' => 'number', - 'cardinality_number' => 6, + 'field_storage[subform][cardinality]' => 'number', + 'field_storage[subform][cardinality_number]' => 6, ]; $this->drupalGet($field_edit_path); - $this->submitForm($edit, 'Save field settings'); - $this->assertSession()->pageTextContains('Updated field Body field settings.'); + $this->submitForm($edit, 'Update settings'); + $this->submitForm([], 'Save settings'); $this->drupalGet($field_edit_path); - $this->assertSession()->fieldValueEquals('cardinality', 'number'); - $this->assertSession()->fieldValueEquals('cardinality_number', 6); - - // Check that tabs displayed. - $this->assertSession()->linkExists('Edit'); - $this->assertSession()->linkByHrefExists('admin/structure/types/manage/article/fields/node.article.body'); - $this->assertSession()->linkExists('Field settings'); - $this->assertSession()->linkByHrefExists($field_edit_path); + $this->assertSession()->fieldValueEquals('field_storage[subform][cardinality]', 'number'); + $this->assertSession()->fieldValueEquals('field_storage[subform][cardinality_number]', 6); // Add two entries in the body. $edit = ['title[0][value]' => 'Cardinality', 'body[0][value]' => 'Body 1', 'body[1][value]' => 'Body 2']; @@ -319,11 +304,11 @@ public function cardinalitySettings() { // Assert that you can't set the cardinality to a lower number than the // highest delta of this field. $edit = [ - 'cardinality' => 'number', - 'cardinality_number' => 1, + 'field_storage[subform][cardinality]' => 'number', + 'field_storage[subform][cardinality_number]' => 1, ]; $this->drupalGet($field_edit_path); - $this->submitForm($edit, 'Save field settings'); + $this->submitForm($edit, 'Update settings'); $this->assertSession()->pageTextContains("There is 1 entity with 2 or more values in this field"); // Create a second entity with three values. @@ -333,48 +318,48 @@ public function cardinalitySettings() { // Set to unlimited. $edit = [ - 'cardinality' => FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED, + 'field_storage[subform][cardinality]' => FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED, ]; $this->drupalGet($field_edit_path); - $this->submitForm($edit, 'Save field settings'); - $this->assertSession()->pageTextContains('Updated field Body field settings.'); + $this->submitForm($edit, 'Update settings'); + $this->submitForm([], 'Save settings'); $this->drupalGet($field_edit_path); - $this->assertSession()->fieldValueEquals('cardinality', FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED); - $this->assertSession()->fieldValueEquals('cardinality_number', 1); + $this->assertSession()->fieldValueEquals('field_storage[subform][cardinality]', FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED); + $this->assertSession()->fieldValueEquals('field_storage[subform][cardinality_number]', 1); // Assert that you can't set the cardinality to a lower number then the // highest delta of this field but can set it to the same. $edit = [ - 'cardinality' => 'number', - 'cardinality_number' => 1, + 'field_storage[subform][cardinality]' => 'number', + 'field_storage[subform][cardinality_number]' => 1, ]; $this->drupalGet($field_edit_path); - $this->submitForm($edit, 'Save field settings'); + $this->submitForm($edit, 'Update settings'); $this->assertSession()->pageTextContains("There are 2 entities with 2 or more values in this field"); $edit = [ - 'cardinality' => 'number', - 'cardinality_number' => 2, + 'field_storage[subform][cardinality]' => 'number', + 'field_storage[subform][cardinality_number]' => 2, ]; $this->drupalGet($field_edit_path); - $this->submitForm($edit, 'Save field settings'); + $this->submitForm($edit, 'Update settings'); $this->assertSession()->pageTextContains("There is 1 entity with 3 or more values in this field"); $edit = [ - 'cardinality' => 'number', - 'cardinality_number' => 3, + 'field_storage[subform][cardinality]' => 'number', + 'field_storage[subform][cardinality_number]' => 3, ]; $this->drupalGet($field_edit_path); - $this->submitForm($edit, 'Save field settings'); + $this->submitForm($edit, 'Update settings'); // Test the cardinality validation is not access sensitive. // Remove the cardinality limit 4 so we can add a node the user doesn't have access to. $edit = [ - 'cardinality' => (string) FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED, + 'field_storage[subform][cardinality]' => (string) FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED, ]; $this->drupalGet($field_edit_path); - $this->submitForm($edit, 'Save field settings'); + $this->submitForm($edit, 'Update settings'); $node = $this->drupalCreateNode([ 'private' => TRUE, 'uid' => 0, @@ -391,25 +376,25 @@ public function cardinalitySettings() { // set it to the same. $this->drupalGet($field_edit_path); $edit = [ - 'cardinality' => 'number', - 'cardinality_number' => 2, + 'field_storage[subform][cardinality]' => 'number', + 'field_storage[subform][cardinality_number]' => 2, ]; $this->drupalGet($field_edit_path); - $this->submitForm($edit, 'Save field settings'); + $this->submitForm($edit, 'Update settings'); $this->assertSession()->pageTextContains("There are 2 entities with 3 or more values in this field"); $edit = [ - 'cardinality' => 'number', - 'cardinality_number' => 3, + 'field_storage[subform][cardinality]' => 'number', + 'field_storage[subform][cardinality_number]' => 3, ]; $this->drupalGet($field_edit_path); - $this->submitForm($edit, 'Save field settings'); + $this->submitForm($edit, 'Update settings'); $this->assertSession()->pageTextContains("There is 1 entity with 4 or more values in this field"); $edit = [ - 'cardinality' => 'number', - 'cardinality_number' => 4, + 'field_storage[subform][cardinality]' => 'number', + 'field_storage[subform][cardinality_number]' => 4, ]; $this->drupalGet($field_edit_path); - $this->submitForm($edit, 'Save field settings'); + $this->submitForm($edit, 'Update settings'); } /** @@ -489,7 +474,7 @@ public function testFieldPrefix() { 'field_name' => $field_exceed_max_length_input, ]; $this->drupalGet('admin/structure/types/manage/' . $this->contentType . '/fields/add-field'); - $this->submitForm($edit, 'Save and continue'); + $this->submitForm($edit, 'Continue'); $this->assertSession()->pageTextContains('Machine-readable name cannot be longer than 22 characters but is currently 23 characters long.'); // Create a valid field. @@ -641,14 +626,14 @@ public function testDisallowedFieldNames() { $edit['field_name'] = 'title'; $bundle_path = 'admin/structure/types/manage/' . $this->contentType; $this->drupalGet("{$bundle_path}/fields/add-field"); - $this->submitForm($edit, 'Save and continue'); + $this->submitForm($edit, 'Continue'); $this->assertSession()->pageTextContains('The machine-readable name is already in use. It must be unique.'); // Try with a base field. $edit['field_name'] = 'sticky'; $bundle_path = 'admin/structure/types/manage/' . $this->contentType; $this->drupalGet("{$bundle_path}/fields/add-field"); - $this->submitForm($edit, 'Save and continue'); + $this->submitForm($edit, 'Continue'); $this->assertSession()->pageTextContains('The machine-readable name is already in use. It must be unique.'); } @@ -692,8 +677,8 @@ public function testLockedField() { public function testHiddenFields() { // Check that the field type is not available in the 'add new field' row. $this->drupalGet('admin/structure/types/manage/' . $this->contentType . '/fields/add-field'); - $this->assertSession()->optionNotExists('edit-new-storage-type', 'hidden_test_field'); - $this->assertSession()->optionExists('edit-new-storage-type', 'shape'); + $this->assertSession()->elementNotExists('css', "[name='new_storage_type'][value='hidden_test_field']"); + $this->assertSession()->elementExists('css', "[name='new_storage_type'][value='shape']"); // Create a field storage and a field programmatically. $field_name = 'hidden_test_field'; @@ -731,10 +716,19 @@ public function testHiddenFields() { $this->drupalGet('admin/structure/types/manage/page/fields/add-field'); foreach ($field_types as $field_type => $definition) { if (empty($definition['no_ui'])) { - $this->assertSession()->optionExists('edit-new-storage-type', $field_type); + try { + $this->assertSession() + ->elementExists('css', "[name='new_storage_type'][value='$field_type']"); + } + catch (ElementNotFoundException) { + if ($this->getFieldFromGroup($field_type)) { + $this->assertSession() + ->elementExists('css', "[name='group_field_options_wrapper'][value='$field_type']"); + } + } } else { - $this->assertSession()->optionNotExists('edit-new-storage-type', $field_type); + $this->assertSession()->elementNotExists('css', "[name='new_storage_type'][value='$field_type']"); } } } @@ -745,14 +739,14 @@ public function testHiddenFields() { public function testDuplicateFieldName() { // field_tags already exists, so we're expecting an error when trying to // create a new field with the same name. - $edit = [ - 'field_name' => 'tags', - 'label' => $this->randomMachineName(), - 'new_storage_type' => 'entity_reference', - ]; $url = 'admin/structure/types/manage/' . $this->contentType . '/fields/add-field'; $this->drupalGet($url); - $this->submitForm($edit, 'Save and continue'); + $edit = [ + 'label' => $this->randomMachineName(), + 'field_name' => 'tags', + 'new_storage_type' => 'boolean', + ]; + $this->submitForm($edit, 'Continue'); $this->assertSession()->pageTextContains('The machine-readable name is already in use. It must be unique.'); $this->assertSession()->addressEquals($url); @@ -765,10 +759,10 @@ public function testExternalDestinations() { $options = [ 'query' => ['destinations' => ['http://example.com']], ]; - $this->drupalGet('admin/structure/types/manage/article/fields/node.article.body/storage', $options); - $this->submitForm([], 'Save field settings'); + $this->drupalGet('admin/structure/types/manage/article/fields/node.article.body', $options); + $this->submitForm([], 'Save settings'); // The external redirect should not fire. - $this->assertSession()->addressEquals('admin/structure/types/manage/article/fields/node.article.body/storage?destinations%5B0%5D=http%3A//example.com'); + $this->assertSession()->addressEquals('admin/structure/types/manage/article/fields/node.article.body?destinations%5B0%5D=http%3A//example.com'); $this->assertSession()->statusCodeEquals(200); $this->assertSession()->responseContains('Attempt to update field Body failed: The internal path component 'http://example.com' is external. You are not allowed to specify an external URL together with internal:/..'); } @@ -855,8 +849,8 @@ public function testPreconfiguredFields() { // Check that the preconfigured field option exist alongside the regular // field type option. - $this->assertSession()->optionExists('edit-new-storage-type', 'field_ui:test_field_with_preconfigured_options:custom_options'); - $this->assertSession()->optionExists('edit-new-storage-type', 'test_field_with_preconfigured_options'); + $this->assertSession()->elementExists('css', "[name='new_storage_type'][value='field_ui:test_field_with_preconfigured_options:custom_options']"); + $this->assertSession()->elementExists('css', "[name='new_storage_type'][value='test_field_with_preconfigured_options']"); // Add a field with every possible preconfigured value. $this->fieldUIAddNewField(NULL, 'test_custom_options', 'Test label', 'field_ui:test_field_with_preconfigured_options:custom_options'); @@ -886,9 +880,6 @@ public function testNonExistentFieldUrls() { $this->drupalGet('admin/structure/types/manage/' . $this->contentType . '/fields/' . $field_id); $this->assertSession()->statusCodeEquals(404); - - $this->drupalGet('admin/structure/types/manage/' . $this->contentType . '/fields/' . $field_id . '/storage'); - $this->assertSession()->statusCodeEquals(404); } /** diff --git a/core/modules/field_ui/tests/src/Functional/ManageFieldsTest.php b/core/modules/field_ui/tests/src/Functional/ManageFieldsTest.php index 7a97872b0f..4b51a417ae 100644 --- a/core/modules/field_ui/tests/src/Functional/ManageFieldsTest.php +++ b/core/modules/field_ui/tests/src/Functional/ManageFieldsTest.php @@ -2,7 +2,11 @@ namespace Drupal\Tests\field_ui\Functional; +use Drupal\field\Entity\FieldConfig; +use Drupal\field\Entity\FieldStorageConfig; use Drupal\Tests\BrowserTestBase; +use Drupal\Tests\field_ui\Traits\FieldUiTestTrait; +use Drupal\user\Entity\User; // cSpell:ignore downlander @@ -13,10 +17,12 @@ */ class ManageFieldsTest extends BrowserTestBase { + use FieldUiTestTrait; /** * {@inheritdoc} */ protected static $modules = [ + 'field_test', 'field_ui', 'field_ui_test', 'node', @@ -28,13 +34,20 @@ class ManageFieldsTest extends BrowserTestBase { */ protected $defaultTheme = 'stark'; + /** + * A user with permission to administer node fields, etc. + * + * @var \Drupal\user\UserInterface + */ + protected $adminUser; + /** * {@inheritdoc} */ protected function setUp(): void { parent::setUp(); - $account = $this->drupalCreateUser(['administer node fields']); - $this->drupalLogin($account); + $this->adminUser = $this->drupalCreateUser(['administer node fields']); + $this->drupalLogin($this->adminUser); $this->config('system.logging') ->set('error_level', ERROR_REPORTING_DISPLAY_ALL) ->save(); @@ -112,4 +125,175 @@ public function testFieldDropButtonOperations() { $this->assertStringContainsString($allowed_bundles_text, $element->getText()); } + /** + * Tests adding a field. + */ + public function testAddField() { + $page = $this->getSession()->getPage(); + $type = $this->drupalCreateContentType([ + 'name' => 'Article', + 'type' => 'article', + ]); + + // Create a new field without actually saving it. + $this->fieldUIAddNewField('admin/structure/types/manage/' . $type->id(), 'test_field', 'Test field', 'test_field', [], [], FALSE); + // Assert that the field was not created. + $this->assertNull(FieldStorageConfig::loadByName('node', "field_test_field")); + + $this->drupalGet('/admin/structure/types/manage/' . $type->id() . '/fields/add-field'); + $edit = [ + 'label' => 'Test field', + 'field_name' => 'test_field', + 'new_storage_type' => 'test_field', + ]; + $this->submitForm($edit, 'Continue'); + $this->assertSession()->statusMessageNotContains('Saved'); + + // Change the storage form values. + $edit = ['field_storage[subform][cardinality_number]' => 5]; + $this->submitForm($edit, 'Update settings'); + $this->assertSession()->statusMessageNotContains('Saved'); + + // Assert that the form values persist. + $this->assertEquals(5, $page->findField('field_storage[subform][cardinality_number]')->getValue()); + + // Try creating a field with the same machine name. + $this->drupalGet('/admin/structure/types/manage/' . $type->id() . '/fields/add-field'); + $edit = [ + 'label' => 'Test field', + 'field_name' => 'test_field', + 'new_storage_type' => 'test_field', + ]; + $this->submitForm($edit, 'Continue'); + // Assert that the values in the field storage form are reset. + $this->assertEquals(1, $page->findField('field_storage[subform][cardinality_number]')->getValue()); + + // Assert that the field is created with the new settings. + $this->submitForm([], 'Update settings'); + $this->assertSession()->statusMessageNotContains('Saved'); + $this->submitForm([], 'Save settings'); + $this->assertSession()->statusMessageContains('Saved'); + + $this->assertEquals(1, FieldStorageConfig::loadByName('node', 'field_test_field')->getCardinality()); + } + + /** + * Tests multiple users adding a field with the same name. + */ + public function testAddFieldWithMultipleUsers() { + $page = $this->getSession()->getPage(); + // Create two users. + $user1 = $this->drupalCreateUser(['administer node fields']); + $user2 = $this->drupalCreateUser(['administer node fields']); + + $node_type = $this->drupalCreateContentType(); + $bundle_path = '/admin/structure/types/manage/' . $node_type->id(); + + // Start adding a field as user 1, stop prior to saving, but keep the URL. + $this->drupalLogin($user1); + $this->drupalGet($bundle_path . '/fields/add-field'); + $edit = [ + 'label' => 'Test field', + 'field_name' => 'test_field', + 'new_storage_type' => 'test_field', + ]; + $this->submitForm($edit, 'Continue'); + // Make changes to the storage form. + $edit = ['field_storage[subform][cardinality_number]' => 5]; + $storage_form_url = $this->getUrl(); + $this->submitForm($edit, 'Update settings'); + $this->drupalLogout(); + + // Actually add a field as user 2. + $this->drupalLogin($user2); + $this->drupalGet($bundle_path . '/fields/add-field'); + $edit = [ + 'label' => 'Test field', + 'field_name' => 'test_field', + 'new_storage_type' => 'test_field', + ]; + $this->submitForm($edit, 'Continue'); + $allowed_no_of_values = $page->findField('field_storage[subform][cardinality_number]')->getValue(); + // Assert that the changes made by any user do not affect other users until + // the field is saved. + $this->assertEquals(1, $allowed_no_of_values); + $this->submitForm(['field_storage[subform][cardinality_number]' => 2], 'Update settings'); + $this->submitForm([], 'Save settings'); + $this->assertSession()->pageTextContains("Saved Test field configuration."); + $this->drupalLogout(); + + // Continue adding a field as user 1, using the URL saved previously. + $this->drupalLogin($user1); + $this->drupalGet($storage_form_url); + // Assert that the user can go on with configuring a field with a machine + // that is already taken. + $this->assertSession()->pageTextNotContains('error'); + $this->submitForm([], 'Save settings'); + // An error is thrown only after the final 'Save'. + $this->assertSession()->statusMessageContains("An error occurred while saving the field: 'field_storage_config' entity with ID 'node.field_test_field' already exists."); + } + + /** + * Tests editing field when the field exists in temp store. + */ + public function testEditFieldWithLeftOverFieldInTempStore() { + $user = $this->drupalCreateUser(['administer node fields']); + + $node_type = $this->drupalCreateContentType(); + $bundle_path = '/admin/structure/types/manage/' . $node_type->id(); + + // Start adding a field but stop prior to saving. + $this->drupalLogin($user); + $this->drupalGet($bundle_path . '/fields/add-field'); + $edit = [ + 'label' => 'Test field', + 'field_name' => 'test_field', + 'new_storage_type' => 'test_field', + ]; + $this->submitForm($edit, 'Continue'); + + /** @var \Drupal\field\FieldStorageConfigInterface $storage */ + $storage = $this->container->get('entity_type.manager') + ->getStorage('field_storage_config') + ->create([ + 'type' => 'test_field', + 'field_name' => 'test_field', + 'entity_type' => 'node', + ]); + $storage->save(); + + $this->container->get('entity_type.manager') + ->getStorage('field_config') + ->create([ + 'field_storage' => $storage, + 'bundle' => $node_type->id(), + 'entity_type' => 'node', + ]) + ->save(); + + $this->drupalGet("$bundle_path/fields/node.{$node_type->id()}.test_field"); + $this->submitForm([], 'Save settings'); + $this->assertSession()->statusMessageContains('Saved test_field configuration.', 'status'); + } + + /** + * Tests creating entity reference field to non-bundleable entity type. + */ + public function testEntityReferenceToNonBundleableEntity() { + $type = $this->drupalCreateContentType([ + 'name' => 'kittens', + 'type' => 'kittens', + ]); + $bundle_path = 'admin/structure/types/manage/' . $type->id(); + $field_name = 'field_user_reference'; + + $field_edit = [ + 'set_default_value' => '1', + "default_value_input[$field_name][0][target_id]" => $this->adminUser->label() . ' (' . $this->adminUser->id() . ')', + ]; + $this->fieldUIAddNewField($bundle_path, 'user_reference', NULL, 'field_ui:entity_reference:user', [], $field_edit); + $field = FieldConfig::loadByName('node', 'kittens', $field_name); + $this->assertEquals([['target_id' => $this->adminUser->id()]], $field->getDefaultValue(User::create(['name' => '1337']))); + } + } diff --git a/core/modules/field_ui/tests/src/FunctionalJavascript/ManageFieldsTest.php b/core/modules/field_ui/tests/src/FunctionalJavascript/ManageFieldsTest.php index e32c1b4188..4be9ffcf2f 100644 --- a/core/modules/field_ui/tests/src/FunctionalJavascript/ManageFieldsTest.php +++ b/core/modules/field_ui/tests/src/FunctionalJavascript/ManageFieldsTest.php @@ -2,6 +2,7 @@ namespace Drupal\Tests\field_ui\FunctionalJavascript; +use Drupal\field\Entity\FieldStorageConfig; use Drupal\FunctionalJavascriptTests\WebDriverTestBase; use Drupal\Tests\field_ui\Traits\FieldUiJSTestTrait; @@ -24,6 +25,7 @@ class ManageFieldsTest extends WebDriverTestBase { 'field_ui', 'field_test', 'block', + 'options', ]; /** @@ -166,4 +168,161 @@ public function testFieldDelete() { $assert_session->waitForText('The field Body has been deleted from the Article content type.'); } + /** + * Tests field add. + */ + public function testAddField() { + $page = $this->getSession()->getPage(); + $assert_session = $this->assertSession(); + + $this->drupalGet('admin/structure/types/manage/article/fields/add-field'); + $field_name = 'test_field_1'; + $page->fillField('label', $field_name); + + // Test validation. + $page->pressButton('Continue'); + $assert_session->pageTextContains('You need to select a field type.'); + $assert_session->elementExists('css', '[name="new_storage_type"].error'); + $assert_session->pageTextNotContains('Choose an option below'); + + $this->assertNotEmpty($number_field = $page->find('xpath', '//*[text() = "Number"]')->getParent()); + $number_field->click(); + $assert_session->assertWaitOnAjaxRequest(); + $this->assertTrue($assert_session->elementExists('css', '[name="new_storage_type"][value="number"]')->isSelected()); + $assert_session->pageTextContains('Choose an option below'); + $page->pressButton('Continue'); + $assert_session->pageTextContains('You need to select a field type.'); + $assert_session->elementNotExists('css', '[name="new_storage_type"].error'); + $assert_session->elementExists('css', '[name="group_field_options_wrapper"].error'); + + // Try adding a field using a grouped field type. + $this->assertNotEmpty($email_field = $page->find('xpath', '//*[text() = "Email"]')->getParent()); + $email_field->click(); + $assert_session->assertWaitOnAjaxRequest(); + $this->assertTrue($assert_session->elementExists('css', '[name="new_storage_type"][value="email"]')->isSelected()); + $assert_session->pageTextNotContains('Choose an option below'); + + $this->assertNotEmpty($text = $page->find('xpath', '//*[text() = "Plain text"]')->getParent()); + $text->click(); + $assert_session->assertWaitOnAjaxRequest(); + $this->assertTrue($assert_session->elementExists('css', '[name="new_storage_type"][value="plain_text"]')->isSelected()); + $assert_session->pageTextContains('Choose an option below'); + + $this->assertNotEmpty($text_plain = $page->find('xpath', '//*[text() = "Text (plain)"]')->getParent()); + $text_plain->click(); + $this->assertTrue($assert_session->elementExists('css', '[name="group_field_options_wrapper"][value="string"]')->isSelected()); + $page->pressButton('Continue'); + + $this->assertMatchesRegularExpression('/.*article\/add-field\/node\/field_test_field_1.*/', $this->getUrl()); + + // Ensure the default value is reloaded when the field storage settings + // are changed. + $default_input_1_name = 'default_value_input[field_test_field_1][0][value]'; + $default_input_1 = $assert_session->fieldExists($default_input_1_name); + $this->assertFalse($default_input_1->isVisible()); + + $default_value = $assert_session->fieldExists('set_default_value'); + $default_value->check(); + $assert_session->waitForElementVisible('xpath', $default_value->getXpath()); + $default_input_1->setValue('There can be only one!'); + $default_input_2_name = 'default_value_input[field_test_field_1][1][value]'; + $assert_session->fieldNotExists($default_input_2_name); + $cardinality = $assert_session->fieldExists('field_storage[subform][cardinality_number]'); + $cardinality->setValue(2); + $default_input_2 = $assert_session->waitForField($default_input_2_name); + // Ensure the default value for first input is retained. + $assert_session->fieldValueEquals($default_input_1_name, 'There can be only one!'); + $page->findField($default_input_2_name)->setValue('But maybe also two?'); + $cardinality->setValue('1'); + $assert_session->assertWaitOnAjaxRequest(); + $assert_session->waitForElementRemoved('xpath', $default_input_2->getXpath()); + // Ensure the first input retains its value. + $assert_session->fieldValueEquals($default_input_1_name, 'There can be only one!'); + $cardinality->setValue(2); + $assert_session->waitForField($default_input_2_name); + // Ensure when the second input is added again it does not retain its value. + $assert_session->fieldValueEquals($default_input_2_name, ''); + + // Ensure changing the max length input will also reload the form. + $max_length_input = $assert_session->fieldExists('field_storage[subform][settings][max_length]'); + $this->assertSame('255', $max_length_input->getValue()); + $this->assertSame('255', $default_input_1->getAttribute('maxlength')); + $max_length_input->setValue('5'); + $page->waitFor(5, function () use ($default_input_1) { + return $default_input_1->getAttribute('maxlength') === '5'; + }); + $this->assertSame('5', $default_input_1->getAttribute('maxlength')); + // Set a default value that is under the new limit. + $default_input_1->setValue('Five!'); + + $page->pressButton('Save settings'); + $assert_session->pageTextContains('Saved ' . $field_name . ' configuration.'); + $this->assertNotNull($field_storage = FieldStorageConfig::loadByName('node', "field_$field_name")); + $this->assertEquals('string', $field_storage->getType()); + + // Try adding a field using a non-grouped field type. + $this->drupalGet('admin/structure/types/manage/article/fields/add-field'); + $field_name = 'test_field_2'; + $page->fillField('label', $field_name); + + $this->assertNotEmpty($number_field = $page->find('xpath', '//*[text() = "Number"]')->getParent()); + $number_field->click(); + $assert_session->assertWaitOnAjaxRequest(); + $this->assertTrue($assert_session->elementExists('css', '[name="new_storage_type"][value="number"]')->isSelected()); + $assert_session->pageTextContains('Choose an option below'); + $this->assertNotEmpty($number_integer = $page->find('xpath', '//*[text() = "Number (integer)"]')->getParent()); + $number_integer->click(); + $this->assertTrue($assert_session->elementExists('css', '[name="group_field_options_wrapper"][value="integer"]')->isSelected()); + + $this->assertNotEmpty($test_field = $page->find('xpath', '//*[text() = "Test field"]')->getParent()); + $test_field->click(); + $assert_session->assertWaitOnAjaxRequest(); + $this->assertTrue($assert_session->elementExists('css', '[name="new_storage_type"][value="test_field"]')->isSelected()); + $assert_session->pageTextNotContains('Choose an option below'); + + $page->pressButton('Continue'); + $this->assertMatchesRegularExpression('/.*article\/add-field\/node\/field_test_field_2.*/', $this->getUrl()); + $page->pressButton('Save settings'); + $assert_session->pageTextContains('Saved ' . $field_name . ' configuration.'); + $this->assertNotNull($field_storage = FieldStorageConfig::loadByName('node', "field_$field_name")); + $this->assertEquals('test_field', $field_storage->getType()); + } + + /** + * Tests the order in which the field types appear in the form. + */ + public function testFieldTypeOrder() { + $this->drupalget('admin/structure/types/manage/article/fields/add-field'); + $page = $this->getSession()->getPage(); + $field_type_categories = [ + 'selection_list', + 'number', + ]; + foreach ($field_type_categories as $field_type_category) { + // Select the group card. + $group_field_card = $page->find('css', "[name='new_storage_type'][value='$field_type_category']")->getParent(); + $group_field_card->click(); + $this->assertSession()->assertWaitOnAjaxRequest(); + $field_types = $page->findAll('css', '.subfield-option .option'); + $field_type_labels = []; + foreach ($field_types as $field_type) { + $field_type_labels[] = $field_type->getText(); + } + $expected_field_types = match ($field_type_category) { + 'selection_list' => [ + 'List (text)', + 'List (integer)', + 'List (float)', + ], + 'number' => [ + 'Number (integer)', + 'Number (decimal)', + 'Number (float)', + ], + }; + // Assert that the field type options are displayed as per their weights. + $this->assertSame($expected_field_types, $field_type_labels); + } + } + } diff --git a/core/modules/field_ui/tests/src/Traits/FieldUiJSTestTrait.php b/core/modules/field_ui/tests/src/Traits/FieldUiJSTestTrait.php index c544421f29..2630b36815 100644 --- a/core/modules/field_ui/tests/src/Traits/FieldUiJSTestTrait.php +++ b/core/modules/field_ui/tests/src/Traits/FieldUiJSTestTrait.php @@ -19,8 +19,13 @@ trait FieldUiJSTestTrait { * @param string $field_type * (optional) The field type of the new field storage. Defaults to * 'test_field'. + * @param bool $save_settings + * (optional) Parameter for conditional execution of second and third step + * (Saving the storage settings and field settings). Defaults to 'TRUE'. + * + * @throws \Behat\Mink\Exception\ElementNotFoundException */ - public function fieldUIAddNewFieldJS(?string $bundle_path, string $field_name, ?string $label = NULL, string $field_type = 'test_field'): void { + public function fieldUIAddNewFieldJS(?string $bundle_path, string $field_name, ?string $label = NULL, string $field_type = 'test_field', bool $save_settings = TRUE): void { $label = $label ?: $field_name; // Allow the caller to set a NULL path in case they navigated to the right @@ -36,13 +41,19 @@ public function fieldUIAddNewFieldJS(?string $bundle_path, string $field_name, ? $page = $session->getPage(); $assert_session = $this->assertSession(); - $field_new_storage_type = $page->findField('new_storage_type'); - $field_new_storage_type->setValue($field_type); - - $field_label = $page->findField('label'); + if ($assert_session->waitForElementVisible('css', "[name='new_storage_type'][value='$field_type']")) { + $page = $this->getSession()->getPage(); + $field_card = $page->find('css', "[name='new_storage_type'][value='$field_type']")->getParent(); + } + else { + $field_card = $this->getFieldFromGroupJS($field_type); + } + $field_card?->click(); + $field_label = $page->findField('edit-label'); $this->assertTrue($field_label->isVisible()); + $field_label = $page->find('css', 'input[data-drupal-selector="edit-label"]'); $field_label->setValue($label); - $machine_name = $assert_session->waitForElementVisible('css', '[name="label"] + * .machine-name-value'); + $machine_name = $assert_session->waitForElementVisible('css', '[data-drupal-selector="edit-label"] + * .machine-name-value'); $this->assertNotEmpty($machine_name); $page->findButton('Edit')->press(); @@ -50,25 +61,17 @@ public function fieldUIAddNewFieldJS(?string $bundle_path, string $field_name, ? $this->assertTrue($field_field_name->isVisible()); $field_field_name->setValue($field_name); - $page->findButton('Save and continue')->click(); - - $assert_session->pageTextContains("These settings apply to the $label field everywhere it is used."); - $breadcrumb_link = $page->findLink($label); - - // Test breadcrumb. - $this->assertTrue($breadcrumb_link->isVisible()); - - // Second step: 'Storage settings' form. - $page->findButton('Save field settings')->click(); - $assert_session->pageTextContains("Updated field $label field settings."); - - // Third step: 'Field settings' form. - $page->findButton('Save settings')->click(); - $assert_session->pageTextContains("Saved $label configuration."); - - // Check that the field appears in the overview form. - $row = $page->find('css', '#field-' . $field_name); - $this->assertNotEmpty($row, 'Field was created and appears in the overview page.'); + $page->findButton('Continue')->click(); + $assert_session->waitForText("These settings apply to the $label field everywhere it is used."); + if ($save_settings) { + // Second step: Save field settings. + $page->findButton('Save settings')->click(); + $assert_session->pageTextContains("Saved $label configuration."); + + // Check that the field appears in the overview form. + $row = $page->find('css', '#field-' . $field_name); + $this->assertNotEmpty($row, 'Field was created and appears in the overview page.'); + } } /** @@ -116,4 +119,32 @@ public function fieldUIAddExistingFieldJS(string $bundle_path, string $existing_ $this->assertSession()->elementExists('xpath', $xpath); } + /** + * Helper function that returns the field card element if it is in a group. + * + * @param string $field_type + * The name of the field type. + * + * @return \Behat\Mink\Element\NodeElement|false|mixed|null + * Field card element within a group. + */ + public function getFieldFromGroupJS($field_type) { + $group_elements = $this->getSession()->getPage()->findAll('css', '.field-option-radio'); + $groups = []; + foreach ($group_elements as $group_element) { + $groups[] = $group_element->getAttribute('value'); + } + $field_card = NULL; + foreach ($groups as $group) { + $group_field_card = $this->getSession()->getPage()->find('css', "[name='new_storage_type'][value='$group']")->getParent(); + $group_field_card->click(); + $this->assertSession()->assertWaitOnAjaxRequest(); + $field_card = $this->getSession()->getPage()->find('css', "[name='group_field_options_wrapper'][value='$field_type']"); + if ($field_card) { + break; + } + } + return $field_card->getParent(); + } + } diff --git a/core/modules/field_ui/tests/src/Traits/FieldUiTestTrait.php b/core/modules/field_ui/tests/src/Traits/FieldUiTestTrait.php index f77481fe4b..c73388645f 100644 --- a/core/modules/field_ui/tests/src/Traits/FieldUiTestTrait.php +++ b/core/modules/field_ui/tests/src/Traits/FieldUiTestTrait.php @@ -2,6 +2,8 @@ namespace Drupal\Tests\field_ui\Traits; +use Behat\Mink\Exception\ElementNotFoundException; + /** * Provides common functionality for the Field UI test classes. */ @@ -25,46 +27,73 @@ trait FieldUiTestTrait { * @param array $field_edit * (optional) $edit parameter for submitForm() on the third step ('Field * settings' form). + * @param bool $save_settings + * (optional) Parameter for conditional execution of second and third step + * (Saving the storage settings and field settings). Defaults to 'TRUE'. */ - public function fieldUIAddNewField($bundle_path, $field_name, $label = NULL, $field_type = 'test_field', array $storage_edit = [], array $field_edit = []) { + public function fieldUIAddNewField($bundle_path, $field_name, $label = NULL, $field_type = 'test_field', array $storage_edit = [], array $field_edit = [], bool $save_settings = TRUE) { // Generate a label containing only letters and numbers to prevent random // test failure. // See https://www.drupal.org/project/drupal/issues/3030902 $label = $label ?: $this->randomMachineName(); - $initial_edit = [ - 'new_storage_type' => $field_type, - 'label' => $label, - 'field_name' => $field_name, - ]; + $initial_edit = []; // 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/add-field"; - } - - // First step: 'Add field' page. - if ($bundle_path !== NULL) { + // First step: 'Add field' page. $this->drupalGet($bundle_path); } - $this->submitForm($initial_edit, 'Save and continue'); - $this->assertSession()->pageTextContains("These settings apply to the $label field everywhere it is used."); - // Test Breadcrumbs. - $this->assertSession()->linkExists($label, 0, 'Field label is correct in the breadcrumb of the storage settings page.'); + else { + $bundle_path = $this->getUrl(); + } - // Second step: 'Storage settings' form. - $this->submitForm($storage_edit, 'Save field settings'); - $this->assertSession()->pageTextContains("Updated field $label field settings."); + try { + // First check if the passed in field type is not part of a group. + $this->assertSession()->elementExists('css', "[name='new_storage_type'][value='$field_type']"); + // If the element exists then we can add it to our object. + $initial_edit = [ + 'new_storage_type' => $field_type, + 'label' => $label, + 'field_name' => $field_name, + ]; + } + // If the element could not be found then it is probably in a group. + catch (ElementNotFoundException) { + // Call the helper function to confirm it is in a group. + $field_group = $this->getFieldFromGroup($field_type); + if ($field_group) { + // Pass in the group name as the new storage type. + $selected_group = [ + 'new_storage_type' => $field_group, + ]; + $this->submitForm($selected_group, 'Change field group'); + $initial_edit = [ + 'group_field_options_wrapper' => $field_type, + 'label' => $label, + 'field_name' => $field_name, + ]; + } + } + $this->submitForm($initial_edit, 'Continue'); + // Assert that the field is not created. + $this->assertFieldDoesNotExist($bundle_path, $label); + if ($save_settings) { + $this->assertSession()->pageTextContains("These settings apply to the $label field everywhere it is used."); + // Test Breadcrumbs. + $this->getSession()->getPage()->findLink($label); - // Third step: 'Field settings' form. - $this->submitForm($field_edit, 'Save settings'); - $this->assertSession()->pageTextContains("Saved $label configuration."); + // Second step: 'Storage settings' form. + $this->submitForm($storage_edit, 'Update settings'); - // Check that the field appears in the overview form. - $xpath = $this->assertSession()->buildXPathQuery("//table[@id=\"field-overview\"]//tr/td[1 and text() = :label]", [ - ':label' => $label, - ]); - $this->assertSession()->elementExists('xpath', $xpath); + // Third step: 'Field settings' form. + $this->submitForm($field_edit, 'Save settings'); + $this->assertSession()->pageTextContains("Saved $label configuration."); + + // Check that the field appears in the overview form. + $this->assertFieldExistsOnOverview($label); + } } /** @@ -141,4 +170,84 @@ public function fieldUIDeleteField($bundle_path, $field_name, $label, $bundle_la $this->assertSession()->elementNotExists('xpath', $xpath); } + /** + * Helper function that returns the name of the group that a field is in. + * + * @param string $field_type + * The name of the field type. + * + * @returns string + * Group name + */ + public function getFieldFromGroup($field_type) { + $group_elements = $this->getSession()->getPage()->findAll('css', '.field-option-radio'); + $groups = []; + foreach ($group_elements as $group_element) { + $groups[] = $group_element->getAttribute('value'); + } + foreach ($groups as $group) { + $test = [ + 'new_storage_type' => $group, + ]; + $this->submitForm($test, 'Change field group'); + try { + $this->assertSession()->elementExists('css', "[name='group_field_options_wrapper'][value='$field_type']"); + return $group; + } + catch (ElementNotFoundException) { + continue; + } + } + return NULL; + } + + /** + * Asserts that the field doesn't exist in the overview form. + * + * @param string $bundle_path + * The bundle path. + * @param string $label + * The field label. + */ + protected function assertFieldDoesNotExist(string $bundle_path, string $label) { + $original_url = $this->getUrl(); + $this->drupalGet(explode('/fields', $bundle_path)[0] . '/fields'); + $this->assertFieldDoesNotExistOnOverview($label); + $this->drupalGet($original_url); + } + + /** + * Asserts that the field appears on the overview form. + * + * @param string $label + * The field label. + * + * @throws \Behat\Mink\Exception\ElementNotFoundException + */ + protected function assertFieldExistsOnOverview(string $label) { + $xpath = $this->assertSession() + ->buildXPathQuery("//table[@id=\"field-overview\"]//tr/td[1 and text() = :label]", [ + ':label' => $label, + ]); + $element = $this->getSession()->getPage()->find('xpath', $xpath); + if ($element === NULL) { + throw new ElementNotFoundException($this->getSession()->getDriver(), 'form field', 'label', $label); + } + } + + /** + * Asserts that the field does not appear on the overview form. + * + * @param string $label + * The field label. + */ + protected function assertFieldDoesNotExistOnOverview(string $label) { + $xpath = $this->assertSession() + ->buildXPathQuery("//table[@id=\"field-overview\"]//tr/td[1 and text() = :label]", [ + ':label' => $label, + ]); + $element = $this->getSession()->getPage()->find('xpath', $xpath); + $this->assertSession()->assert($element === NULL, sprintf('A field "%s" appears on this page, but it should not.', $label)); + } + } diff --git a/core/modules/field_ui/tests/src/Unit/FieldConfigEditFormTest.php b/core/modules/field_ui/tests/src/Unit/FieldConfigEditFormTest.php index 00267079bc..650baff4c6 100644 --- a/core/modules/field_ui/tests/src/Unit/FieldConfigEditFormTest.php +++ b/core/modules/field_ui/tests/src/Unit/FieldConfigEditFormTest.php @@ -2,6 +2,8 @@ namespace Drupal\Tests\field_ui\Unit; +use Drupal\Core\Entity\EntityDisplayRepositoryInterface; +use Drupal\Core\TempStore\PrivateTempStore; use Drupal\field_ui\Form\FieldConfigEditForm; use Drupal\Tests\UnitTestCase; @@ -27,7 +29,9 @@ protected function setUp(): void { $entity_type_bundle_info = $this->createMock('\Drupal\Core\Entity\EntityTypeBundleInfoInterface'); $typed_data = $this->createMock('\Drupal\Core\TypedData\TypedDataManagerInterface'); - $this->fieldConfigEditForm = new FieldConfigEditForm($entity_type_bundle_info, $typed_data); + $temp_store = $this->createMock(PrivateTempStore::class); + $entity_display_repository = $this->createMock(EntityDisplayRepositoryInterface::class); + $this->fieldConfigEditForm = new FieldConfigEditForm($entity_type_bundle_info, $typed_data, $entity_display_repository, $temp_store); } /** diff --git a/core/modules/file/css/file.icon.theme.css b/core/modules/file/css/file.icon.theme.css new file mode 100644 index 0000000000..cde89c79dc --- /dev/null +++ b/core/modules/file/css/file.icon.theme.css @@ -0,0 +1,9 @@ +/* + * DO NOT EDIT THIS FILE. + * See the following change record for more information, + * https://www.drupal.org/node/3084859 + * @preserve + */ +.field-icon-file_upload { + background-image: url("data:image/svg+xml,%3csvg height='36' viewBox='0 0 36 36' width='36' xmlns='http://www.w3.org/2000/svg'%3e%3cpath d='m3.87 1.163c-.361.17-.581.394-.745.757-.122.269-.122.303-.123 16.08l-.001 15.81.141.303c.166.355.54.699.87.797.173.052 3.612.07 13.532.07 14.778 0 13.513.037 13.978-.408.128-.122.282-.33.344-.462.107-.232.111-.689.113-12.93l.001-12.69-3.735-3.735-3.735-3.735-10.17.001h-10.17zm19.11 5.857v3h6v21.96h-22.98v-27.96h16.98zm-9.215 11.981-3.703 3.949 1.959.016 1.959.016v5.998h7.02v-5.998l1.969-.016 1.968-.016-3.684-3.93c-2.027-2.162-3.707-3.938-3.734-3.949-.028-.01-1.717 1.759-3.754 3.93' fill='%2355565b'/%3e%3c/svg%3e"); +} diff --git a/core/modules/file/css/file.icon.theme.pcss.css b/core/modules/file/css/file.icon.theme.pcss.css new file mode 100644 index 0000000000..1133d5843e --- /dev/null +++ b/core/modules/file/css/file.icon.theme.pcss.css @@ -0,0 +1,3 @@ +.field-icon-file_upload { + background-image: url(../../../misc/icons/55565b/file_upload.svg); +} diff --git a/core/modules/file/file.field_type_categories.yml b/core/modules/file/file.field_type_categories.yml new file mode 100644 index 0000000000..8f96134e8f --- /dev/null +++ b/core/modules/file/file.field_type_categories.yml @@ -0,0 +1,6 @@ +file_upload: + label: 'File upload' + description: 'Field to upload any type of files.' + weight: -15 + libraries: + - file/drupal.file-icon diff --git a/core/modules/file/file.libraries.yml b/core/modules/file/file.libraries.yml index 81c9324ea5..50904d18f5 100644 --- a/core/modules/file/file.libraries.yml +++ b/core/modules/file/file.libraries.yml @@ -7,3 +7,11 @@ drupal.file: - core/once - core/drupal - core/drupalSettings + +drupal.file-icon: + version: VERSION + css: + theme: + css/file.icon.theme.css: {} + dependencies: + - field_ui/drupal.field_ui.manage_fields diff --git a/core/modules/file/src/Plugin/Field/FieldType/FileItem.php b/core/modules/file/src/Plugin/Field/FieldType/FileItem.php index 3b97851e98..a52f3d9190 100644 --- a/core/modules/file/src/Plugin/Field/FieldType/FileItem.php +++ b/core/modules/file/src/Plugin/Field/FieldType/FileItem.php @@ -21,8 +21,11 @@ * @FieldType( * id = "file", * label = @Translation("File"), - * description = @Translation("This field stores the ID of a file as an integer value."), - * category = @Translation("Reference"), + * description = { + * @Translation("For uploading files"), + * @Translation("Can be configured with options such as allowed file extensions and maximum upload size"), + * }, + * category = "file_upload", * default_widget = "file_generic", * default_formatter = "file_default", * list_class = "\Drupal\file\Plugin\Field\FieldType\FileFieldItemList", diff --git a/core/modules/file/tests/src/Functional/FileFieldDisplayTest.php b/core/modules/file/tests/src/Functional/FileFieldDisplayTest.php index 844df162bf..43dfa4fc23 100644 --- a/core/modules/file/tests/src/Functional/FileFieldDisplayTest.php +++ b/core/modules/file/tests/src/Functional/FileFieldDisplayTest.php @@ -5,6 +5,7 @@ use Drupal\Core\Field\FieldStorageDefinitionInterface; use Drupal\file\Entity\File; use Drupal\node\Entity\Node; +use Drupal\Tests\field_ui\Traits\FieldUiTestTrait; /** * Tests the display of file fields in node and views. @@ -13,6 +14,8 @@ */ class FileFieldDisplayTest extends FileFieldTestBase { + use FieldUiTestTrait; + /** * {@inheritdoc} */ @@ -165,20 +168,10 @@ public function testDescToggle() { ]; $this->drupalGet('admin/structure/types/add'); $this->submitForm($edit, 'Save and manage fields'); - $edit = [ - 'new_storage_type' => $field_type, - 'field_name' => $field_name, - 'label' => $this->randomString(), - ]; - $this->drupalGet('/admin/structure/types/manage/' . $type_name . '/fields/add-field'); - $this->submitForm($edit, 'Save and continue'); - $this->submitForm([], 'Save field settings'); - // Ensure the description field is selected on the field instance settings - // form. That's what this test is all about. - $edit = [ + $field_edit = [ 'settings[description_field]' => TRUE, ]; - $this->submitForm($edit, 'Save settings'); + $this->fieldUIAddNewField('/admin/structure/types/manage/' . $type_name, $field_name, $this->randomString(), $field_type, [], $field_edit); // Add a node of our new type and upload a file to it. $file = current($this->drupalGetTestFiles('text')); $title = $this->randomString(); diff --git a/core/modules/file/tests/src/Functional/FileFieldWidgetTest.php b/core/modules/file/tests/src/Functional/FileFieldWidgetTest.php index 4f9051979a..b9e1dabf3e 100644 --- a/core/modules/file/tests/src/Functional/FileFieldWidgetTest.php +++ b/core/modules/file/tests/src/Functional/FileFieldWidgetTest.php @@ -258,9 +258,9 @@ public function testPrivateFileSetting() { $test_file = $this->getTestFile('text'); // Change the field setting to make its files private, and upload a file. - $edit = ['settings[uri_scheme]' => 'private']; - $this->drupalGet("admin/structure/types/manage/{$type_name}/fields/{$field_id}/storage"); - $this->submitForm($edit, 'Save field settings'); + $edit = ['field_storage[subform][settings][uri_scheme]' => 'private']; + $this->drupalGet("admin/structure/types/manage/{$type_name}/fields/{$field_id}"); + $this->submitForm($edit, 'Save'); $nid = $this->uploadNodeFile($test_file, $field_name, $type_name); $node = $node_storage->loadUnchanged($nid); $node_file = File::load($node->{$field_name}->target_id); @@ -272,13 +272,13 @@ public function testPrivateFileSetting() { // Ensure we can't change 'uri_scheme' field settings while there are some // entities with uploaded files. - $this->drupalGet("admin/structure/types/manage/$type_name/fields/$field_id/storage"); - $this->assertSession()->fieldDisabled("edit-settings-uri-scheme-public"); + $this->drupalGet("admin/structure/types/manage/$type_name/fields/$field_id"); + $this->assertSession()->fieldDisabled("edit-field-storage-subform-settings-uri-scheme-public"); // Delete node and confirm that setting could be changed. $node->delete(); - $this->drupalGet("admin/structure/types/manage/$type_name/fields/$field_id/storage"); - $this->assertSession()->fieldEnabled("edit-settings-uri-scheme-public"); + $this->drupalGet("admin/structure/types/manage/$type_name/fields/$field_id"); + $this->assertSession()->fieldEnabled("edit-field-storage-subform-settings-uri-scheme-public"); } /** @@ -301,7 +301,7 @@ public function testPrivateFileComment() { $name = strtolower($this->randomMachineName()); $label = $this->randomMachineName(); - $storage_edit = ['settings[uri_scheme]' => 'private']; + $storage_edit = ['field_storage[subform][settings][uri_scheme]' => 'private']; $this->fieldUIAddNewField('admin/structure/comment/manage/comment', $name, $label, 'file', $storage_edit); // Manually clear cache on the tester side. diff --git a/core/modules/image/src/Plugin/Field/FieldType/ImageItem.php b/core/modules/image/src/Plugin/Field/FieldType/ImageItem.php index 5324e09a45..c126ae13eb 100644 --- a/core/modules/image/src/Plugin/Field/FieldType/ImageItem.php +++ b/core/modules/image/src/Plugin/Field/FieldType/ImageItem.php @@ -21,8 +21,12 @@ * @FieldType( * id = "image", * label = @Translation("Image"), - * description = @Translation("This field stores the ID of an image file as an integer value."), - * category = @Translation("Reference"), + * description = { + * @Translation("For uploading images"), + * @Translation("Allows a user to upload an image with configurable extensions, image resolutions, upload size"), + * @Translation("Can be configured with options such as allowed file extensions, maximum upload size and image resolution minimums/maximums"), + * }, + * category = "file_upload", * default_widget = "image_image", * default_formatter = "image", * column_groups = { diff --git a/core/modules/image/tests/modules/image_module_test/src/Plugin/Field/FieldType/DummyAjaxItem.php b/core/modules/image/tests/modules/image_module_test/src/Plugin/Field/FieldType/DummyAjaxItem.php index 50b77bd1c9..5876612345 100644 --- a/core/modules/image/tests/modules/image_module_test/src/Plugin/Field/FieldType/DummyAjaxItem.php +++ b/core/modules/image/tests/modules/image_module_test/src/Plugin/Field/FieldType/DummyAjaxItem.php @@ -14,7 +14,6 @@ * id = "image_module_test_dummy_ajax", * label = @Translation("Dummy AJAX"), * description = @Translation("A field containing an AJAX handler."), - * category = @Translation("Field"), * default_widget = "image_module_test_dummy_ajax_widget", * default_formatter = "image_module_test_dummy_ajax_formatter" * ) diff --git a/core/modules/image/tests/src/Functional/ImageFieldDefaultImagesTest.php b/core/modules/image/tests/src/Functional/ImageFieldDefaultImagesTest.php index adac206549..bbb42adea6 100644 --- a/core/modules/image/tests/src/Functional/ImageFieldDefaultImagesTest.php +++ b/core/modules/image/tests/src/Functional/ImageFieldDefaultImagesTest.php @@ -126,17 +126,17 @@ public function testDefaultImages() { ->save(); // Confirm the defaults are present on the article field storage settings - // form. + // sub-form. $field_id = $field->id(); - $this->drupalGet("admin/structure/types/manage/article/fields/$field_id/storage"); - $this->assertSession()->hiddenFieldValueEquals('settings[default_image][uuid][fids]', $default_images['field_storage']->id()); + $this->drupalGet("admin/structure/types/manage/article/fields/$field_id"); + $this->assertSession()->hiddenFieldValueEquals('field_storage[subform][settings][default_image][uuid][fids]', $default_images['field_storage']->id()); // Confirm the defaults are present on the article field edit form. $this->drupalGet("admin/structure/types/manage/article/fields/$field_id"); $this->assertSession()->hiddenFieldValueEquals('settings[default_image][uuid][fids]', $default_images['field']->id()); // Confirm the defaults are present on the page field storage settings form. - $this->drupalGet("admin/structure/types/manage/page/fields/$field_id/storage"); - $this->assertSession()->hiddenFieldValueEquals('settings[default_image][uuid][fids]', $default_images['field_storage']->id()); + $this->drupalGet("admin/structure/types/manage/page/fields/$field_id"); + $this->assertSession()->hiddenFieldValueEquals('field_storage[subform][settings][default_image][uuid][fids]', $default_images['field_storage']->id()); // Confirm the defaults are present on the page field edit form. $field2_id = $field2->id(); $this->drupalGet("admin/structure/types/manage/page/fields/$field2_id"); @@ -167,8 +167,8 @@ public function testDefaultImages() { // Confirm that the new default is used on the article field storage // settings form. - $this->drupalGet("admin/structure/types/manage/article/fields/$field_id/storage"); - $this->assertSession()->hiddenFieldValueEquals('settings[default_image][uuid][fids]', $default_images['field_storage_new']->id()); + $this->drupalGet("admin/structure/types/manage/article/fields/$field_id"); + $this->assertSession()->hiddenFieldValueEquals('field_storage[subform][settings][default_image][uuid][fids]', $default_images['field_storage_new']->id()); // Reload the nodes and confirm the field defaults are used. $node_storage->resetCache([$article->id(), $page->id()]); @@ -240,9 +240,9 @@ public function testDefaultImages() { $field_storage->save(); // Confirm that the new default is used on the article field storage - // settings form. - $this->drupalGet("admin/structure/types/manage/article/fields/$field_id/storage"); - $this->assertSession()->hiddenFieldValueEquals('settings[default_image][uuid][fids]', $default_images['field_storage_private']->id()); + // settings sub-form. + $this->drupalGet("admin/structure/types/manage/article/fields/$field_id"); + $this->assertSession()->hiddenFieldValueEquals('field_storage[subform][settings][default_image][uuid][fids]', $default_images['field_storage_private']->id()); // Upload a new default for the article's field after setting the field // storage upload destination to 'private'. diff --git a/core/modules/image/tests/src/Functional/ImageFieldDisplayTest.php b/core/modules/image/tests/src/Functional/ImageFieldDisplayTest.php index 7502129a79..7a6b24fe27 100644 --- a/core/modules/image/tests/src/Functional/ImageFieldDisplayTest.php +++ b/core/modules/image/tests/src/Functional/ImageFieldDisplayTest.php @@ -334,10 +334,10 @@ public function testImageFieldSettings() { // 1, so we need to make sure the file widget prevents these notices by // providing all settings, even if they are not used. // @see FileWidget::formMultipleElements(). - $this->drupalGet('admin/structure/types/manage/article/fields/node.article.' . $field_name . '/storage'); + $this->drupalGet('admin/structure/types/manage/article/fields/node.article.' . $field_name); $this->submitForm([ - 'cardinality' => FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED, - ], 'Save field settings'); + 'field_storage[subform][cardinality]' => FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED, + ], 'Save'); $edit = [ 'files[' . $field_name . '_1][]' => \Drupal::service('file_system')->realpath($test_image->uri), ]; @@ -499,12 +499,12 @@ public function testImageFieldDefaultImage() { $title = $this->randomString(1024); $edit = [ // Get the path of the 'image-test.png' file. - 'files[settings_default_image_uuid]' => \Drupal::service('file_system')->realpath($images[0]->uri), - 'settings[default_image][alt]' => $alt, - 'settings[default_image][title]' => $title, + 'files[field_storage_subform_settings_default_image_uuid]' => \Drupal::service('file_system')->realpath($images[0]->uri), + 'field_storage[subform][settings][default_image][alt]' => $alt, + 'field_storage[subform][settings][default_image][title]' => $title, ]; - $this->drupalGet("admin/structure/types/manage/article/fields/node.article.{$field_name}/storage"); - $this->submitForm($edit, 'Save field settings'); + $this->drupalGet("admin/structure/types/manage/article/fields/node.article.{$field_name}"); + $this->submitForm($edit, 'Save'); // Clear field definition cache so the new default image is detected. \Drupal::service('entity_field.manager')->clearCachedFieldDefinitions(); $field_storage = FieldStorageConfig::loadByName('node', $field_name); @@ -558,9 +558,9 @@ public function testImageFieldDefaultImage() { // Remove default image from the field and make sure it is no longer used. // Can't use fillField cause Mink can't fill hidden fields. - $this->drupalGet("admin/structure/types/manage/article/fields/node.article.$field_name/storage"); - $this->getSession()->getPage()->find('css', 'input[name="settings[default_image][uuid][fids]"]')->setValue(0); - $this->getSession()->getPage()->pressButton('Save field settings'); + $this->drupalGet("admin/structure/types/manage/article/fields/node.article.$field_name"); + $this->getSession()->getPage()->find('css', 'input[name="field_storage[subform][settings][default_image][uuid][fids]"]')->setValue(0); + $this->getSession()->getPage()->pressButton('Save'); // Clear field definition cache so the new default image is detected. \Drupal::service('entity_field.manager')->clearCachedFieldDefinitions(); @@ -574,12 +574,12 @@ public function testImageFieldDefaultImage() { // Add a default image to the new field. $edit = [ // Get the path of the 'image-test.gif' file. - 'files[settings_default_image_uuid]' => \Drupal::service('file_system')->realpath($images[2]->uri), - 'settings[default_image][alt]' => $alt, - 'settings[default_image][title]' => $title, + 'files[field_storage_subform_settings_default_image_uuid]' => \Drupal::service('file_system')->realpath($images[2]->uri), + 'field_storage[subform][settings][default_image][alt]' => $alt, + 'field_storage[subform][settings][default_image][title]' => $title, ]; - $this->drupalGet('admin/structure/types/manage/article/fields/node.article.' . $private_field_name . '/storage'); - $this->submitForm($edit, 'Save field settings'); + $this->drupalGet('admin/structure/types/manage/article/fields/node.article.' . $private_field_name); + $this->submitForm($edit, 'Save'); // Clear field definition cache so the new default image is detected. \Drupal::service('entity_field.manager')->clearCachedFieldDefinitions(); diff --git a/core/modules/layout_builder/tests/src/Functional/LayoutBuilderTest.php b/core/modules/layout_builder/tests/src/Functional/LayoutBuilderTest.php index 6a8382bf6e..3002e6a339 100644 --- a/core/modules/layout_builder/tests/src/Functional/LayoutBuilderTest.php +++ b/core/modules/layout_builder/tests/src/Functional/LayoutBuilderTest.php @@ -6,6 +6,7 @@ use Drupal\layout_builder\Section; use Drupal\node\Entity\Node; use Drupal\Tests\BrowserTestBase; +use Drupal\Tests\field_ui\Traits\FieldUiTestTrait; use Drupal\views\Entity\View; /** @@ -15,6 +16,8 @@ */ class LayoutBuilderTest extends BrowserTestBase { + use FieldUiTestTrait; + /** * {@inheritdoc} */ @@ -444,15 +447,7 @@ public function testLayoutBuilderUi() { $assert_session->linkNotExists('Layout'); // Add a new field. - $edit = [ - 'new_storage_type' => 'string', - 'label' => 'My text field', - 'field_name' => 'my_text', - ]; - $this->drupalGet("{$field_ui_prefix}/fields/add-field"); - $this->submitForm($edit, 'Save and continue'); - $page->pressButton('Save field settings'); - $page->pressButton('Save settings'); + $this->fieldUIAddNewField($field_ui_prefix, 'my_text', 'My text field', 'string'); $this->drupalGet("$field_ui_prefix/display/default/layout"); $assert_session->pageTextContains('My text field'); $assert_session->elementExists('css', '.field--name-field-my-text'); diff --git a/core/modules/link/css/link.icon.theme.css b/core/modules/link/css/link.icon.theme.css new file mode 100644 index 0000000000..8924b5a534 --- /dev/null +++ b/core/modules/link/css/link.icon.theme.css @@ -0,0 +1,9 @@ +/* + * DO NOT EDIT THIS FILE. + * See the following change record for more information, + * https://www.drupal.org/node/3084859 + * @preserve + */ +.field-icon-link { + background-image: url("data:image/svg+xml,%3csvg height='38' viewBox='0 0 38 38' width='38' xmlns='http://www.w3.org/2000/svg'%3e%3cpath d='m25.84 2.37c-1.972.311-3.605 1.099-5.162 2.493-1.157 1.035-3.04 2.995-3.221 3.353a1.34 1.34 0 0 0 -.018 1.252c.193.415.819.997 1.219 1.133.798.272 1.01.143 2.997-1.818.891-.88 1.734-1.688 1.873-1.796.678-.523 1.686-.971 2.555-1.135.663-.125 2.055-.089 2.622.068 1.75.484 3.119 1.611 3.911 3.219.493.999.595 1.459.593 2.673-.002 1.102-.093 1.593-.444 2.395-.437.998-.716 1.326-3.528 4.137-1.468 1.467-2.844 2.795-3.058 2.951-1.167.846-2.819 1.293-4.162 1.126a6.606 6.606 0 0 1 -2.194-.674c-.836-.445-1.081-.654-2.231-1.909-.385-.42-.706-.585-1.139-.584-.431.001-.898.215-1.313.604-.579.541-.721 1.135-.423 1.773.157.339.916 1.282 1.378 1.712a9.753 9.753 0 0 0 3.617 2.108c.98.314 1.471.395 2.613.432.91.03 1.195.015 1.842-.096a9.098 9.098 0 0 0 2.767-.918c1.263-.639 1.688-1.007 4.862-4.201 2.382-2.397 2.954-3.006 3.28-3.496 1.732-2.599 2.122-5.727 1.075-8.622-1.126-3.113-3.765-5.388-7.049-6.079-.818-.172-2.484-.224-3.262-.101m-10.64 10.783c-1.249.2-2.102.463-3.071.946-1.308.651-1.648.941-4.727 4.012-1.669 1.666-2.97 3.018-3.178 3.302-.899 1.23-1.444 2.426-1.758 3.857-.168.763-.233 2.364-.128 3.113.583 4.136 3.564 7.335 7.605 8.161 2.581.528 5.344-.096 7.537-1.7.261-.191 1.234-1.1 2.162-2.02 1.865-1.851 2.043-2.083 2.047-2.677.003-.427-.133-.719-.538-1.163-.35-.383-.785-.62-1.212-.661-.581-.056-.836.131-2.744 2.013-1.74 1.715-2.089 2.001-2.908 2.379-.895.414-1.338.502-2.507.499-.947-.002-1.096-.018-1.592-.171-.737-.227-1.185-.431-1.713-.783-1.1-.731-1.953-1.812-2.37-3.006-.489-1.401-.452-3.071.097-4.364.449-1.056.614-1.252 3.451-4.107 1.466-1.475 2.829-2.809 3.03-2.964 1.652-1.284 3.976-1.616 5.891-.842 1.036.419 1.703.931 2.81 2.16.537.595 1.024.749 1.675.527.388-.132.966-.601 1.17-.951.338-.576.258-1.146-.258-1.835-1.526-2.036-3.759-3.341-6.333-3.703-.425-.06-2.108-.075-2.438-.022' fill='%2355565b'/%3e%3c/svg%3e"); +} diff --git a/core/modules/link/css/link.icon.theme.pcss.css b/core/modules/link/css/link.icon.theme.pcss.css new file mode 100644 index 0000000000..733eedbcf5 --- /dev/null +++ b/core/modules/link/css/link.icon.theme.pcss.css @@ -0,0 +1,3 @@ +.field-icon-link { + background-image: url(../../../misc/icons/55565b/link.svg); +} diff --git a/core/modules/link/link.libraries.yml b/core/modules/link/link.libraries.yml new file mode 100644 index 0000000000..d192cb3211 --- /dev/null +++ b/core/modules/link/link.libraries.yml @@ -0,0 +1,7 @@ +drupal.link-icon: + version: VERSION + css: + theme: + css/link.icon.theme.css: {} + dependencies: + - field_ui/drupal.field_ui.manage_fields diff --git a/core/modules/link/link.module b/core/modules/link/link.module index fafa734f7d..f05fe1c238 100644 --- a/core/modules/link/link.module +++ b/core/modules/link/link.module @@ -5,6 +5,7 @@ * Defines simple link field types. */ +use Drupal\Core\Field\FieldTypeCategoryManagerInterface; use Drupal\Core\Link; use Drupal\Core\Url; use Drupal\Core\Routing\RouteMatchInterface; @@ -65,3 +66,14 @@ function link_theme() { function template_preprocess_link_formatter_link_separate(&$variables) { $variables['link'] = Link::fromTextAndUrl($variables['url_title'], $variables['url'])->toString(); } + +/** + * Implements hook_field_type_category_info_alter(). + */ +function link_field_type_category_info_alter(&$definitions) { + // TRICKY: the `link` field type belongs in the `general` category, so the + // libraries need to be attached using an alter hook. + if (array_key_exists(FieldTypeCategoryManagerInterface::FALLBACK_CATEGORY, $definitions)) { + $definitions[FieldTypeCategoryManagerInterface::FALLBACK_CATEGORY]['libraries'][] = 'link/drupal.link-icon'; + } +} diff --git a/core/modules/link/tests/src/Functional/LinkFieldUITest.php b/core/modules/link/tests/src/Functional/LinkFieldUITest.php index 2c40adac32..b435e2d3e9 100644 --- a/core/modules/link/tests/src/Functional/LinkFieldUITest.php +++ b/core/modules/link/tests/src/Functional/LinkFieldUITest.php @@ -165,7 +165,7 @@ public function runFieldUIItem($cardinality, $link_type, $title, $label, $field_ $field_edit['default_value_input[field_' . $field_name . '][0][title]'] = 'Default title'; } $storage_edit = [ - 'cardinality_number' => $cardinality, + 'field_storage[subform][cardinality_number]' => $cardinality, ]; $this->fieldUIAddNewField($type_path, $field_name, $label, 'link', $storage_edit, $field_edit); diff --git a/core/modules/media/css/media.icon.theme.css b/core/modules/media/css/media.icon.theme.css new file mode 100644 index 0000000000..5d4e7d6dec --- /dev/null +++ b/core/modules/media/css/media.icon.theme.css @@ -0,0 +1,10 @@ +/* + * DO NOT EDIT THIS FILE. + * See the following change record for more information, + * https://www.drupal.org/node/3084859 + * @preserve + */ +/* cspell:ignore uientity referencemedia */ +.field-icon-field-uientity-referencemedia { + background-image: url("data:image/svg+xml,%3csvg height='36' viewBox='0 0 36 36' width='36' xmlns='http://www.w3.org/2000/svg'%3e%3cpath d='m1.02 11.52v10.5h17.46v5.203l-.495-.198a4.21 4.21 0 0 0 -2.493-.23c-.951.194-1.625.559-2.324 1.258-.989.988-1.371 2.274-1.049 3.531.171.672.441 1.137.96 1.658.74.741 1.733 1.138 2.846 1.138 2.017-.002 3.781-1.311 4.26-3.162.082-.317.095-.954.095-4.781v-4.416l2.355-.015 2.355-.016.016-3.06.015-3.06 3.855-.858c2.12-.472 3.874-.857 3.899-.855.05.003.063 11.043.013 11.043-.018 0-.161-.066-.319-.146-.669-.339-1.648-.461-2.5-.313a4.415 4.415 0 0 0 -2.489 1.3c-1.49 1.513-1.514 3.723-.058 5.169.774.768 1.682 1.127 2.853 1.129 1.701.003 3.245-.922 3.957-2.369.4-.812.375-.108.409-11.535.029-9.661.024-10.395-.069-10.395-.055 0-2.21.472-4.79 1.05-2.579.578-4.706 1.05-4.726 1.05s-.036-2.052-.036-4.56v-4.56h-24v10.5m21.96-4.715v3.805l-.555.125-2.265.511-1.71.385-.15-.133-.707-.634c-.306-.275-.565-.49-.575-.477s-1.309 2.03-2.887 4.483l-2.87 4.46-.905-.89-1.85-1.818-.944-.929-.256.243-4.081 3.857-.225.211v-17.004h19.98zm-15.69-2.415c-.545.102-1.263.499-1.703.94-.696.699-1.027 1.507-1.027 2.51 0 1.932 1.531 3.45 3.48 3.45a3.453 3.453 0 0 0 3.479-3.472c.001-.606-.09-.989-.378-1.578-.682-1.399-2.248-2.151-3.851-1.85' fill='%2355565b'/%3e%3c/svg%3e"); +} diff --git a/core/modules/media/css/media.icon.theme.pcss.css b/core/modules/media/css/media.icon.theme.pcss.css new file mode 100644 index 0000000000..b987efbdfa --- /dev/null +++ b/core/modules/media/css/media.icon.theme.pcss.css @@ -0,0 +1,4 @@ +/* cspell:ignore uientity referencemedia */ +.field-icon-field-uientity-referencemedia { + background-image: url(../../../misc/icons/55565b/media.svg); +} diff --git a/core/modules/media/media.libraries.yml b/core/modules/media/media.libraries.yml index f8febc4dfc..41fc310fbb 100644 --- a/core/modules/media/media.libraries.yml +++ b/core/modules/media/media.libraries.yml @@ -40,3 +40,11 @@ media_embed_ckeditor_theme: js/media_embed_ckeditor.theme.js: {} dependencies: - core/drupal + +drupal.media-icon: + version: VERSION + css: + theme: + css/media.icon.theme.css: {} + dependencies: + - field_ui/drupal.field_ui.manage_fields diff --git a/core/modules/media/media.module b/core/modules/media/media.module index 83884fe45f..205d7801e6 100644 --- a/core/modules/media/media.module +++ b/core/modules/media/media.module @@ -9,6 +9,7 @@ use Drupal\Core\Access\AccessResult; use Drupal\Core\Entity\Display\EntityViewDisplayInterface; use Drupal\Core\Entity\EntityInterface; +use Drupal\Core\Field\FieldTypeCategoryManagerInterface; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Render\Element; use Drupal\Core\Render\Element\RenderElement; @@ -184,6 +185,7 @@ function media_field_ui_preconfigured_options_alter(array &$options, $field_type // Set the default formatter for media in entity reference fields to be the // "Rendered entity" formatter. if (!empty($options['media'])) { + $options['media']['category'] = FieldTypeCategoryManagerInterface::FALLBACK_CATEGORY; $options['media']['entity_view_display']['type'] = 'entity_reference_entity_view'; } } @@ -200,27 +202,21 @@ function media_form_field_ui_field_storage_add_form_alter(&$form, FormStateInter '@help_url' => Url::fromRoute('help.page', ['name' => 'media'])->toString(), ]); } - $form['add']['description_wrapper'] = [ - '#type' => 'container', - ]; $field_types = [ - 'file', - 'image', + 'file_upload', 'field_ui:entity_reference:media', ]; - foreach ($field_types as $field_name) { - $form['add']['description_wrapper']["description_{$field_name}"] = [ + if (in_array($form_state->getValue('selected_field_type'), $field_types)) { + $form['group_field_options_wrapper']['description_wrapper'] = [ '#type' => 'item', '#markup' => $description_text, - '#states' => [ - 'visible' => [ - ':input[name="new_storage_type"]' => ['value' => $field_name], - ], - ], ]; } - $form['add']['new_storage_type']['#weight'] = 0; - $form['add']['description_wrapper']['#weight'] = 1; + + // Remove Media from the Reference group since it already exists as a field. + if ($form_state->getValue('selected_field_type') === 'reference') { + unset($form['group_field_options_wrapper']['fields']['field_ui:entity_reference:media']); + } } /** @@ -539,3 +535,14 @@ function media_views_query_substitutions(ViewExecutable $view) { '***ADMINISTER_MEDIA***' => (int) $account->hasPermission('administer media'), ]; } + +/** + * Implements hook_field_type_category_info_alter(). + */ +function media_field_type_category_info_alter(&$definitions) { + // TRICKY: the `media` field type belongs in the `general` category, so the + // libraries need to be attached using an alter hook. + if (array_key_exists(FieldTypeCategoryManagerInterface::FALLBACK_CATEGORY, $definitions)) { + $definitions[FieldTypeCategoryManagerInterface::FALLBACK_CATEGORY]['libraries'][] = 'media/drupal.media-icon'; + } +} diff --git a/core/modules/media/tests/src/Functional/MediaUiFunctionalTest.php b/core/modules/media/tests/src/Functional/MediaUiFunctionalTest.php index 362467b380..e085215d42 100644 --- a/core/modules/media/tests/src/Functional/MediaUiFunctionalTest.php +++ b/core/modules/media/tests/src/Functional/MediaUiFunctionalTest.php @@ -7,6 +7,7 @@ use Drupal\Core\Url; use Drupal\field\Entity\FieldConfig; use Drupal\field\Entity\FieldStorageConfig; +use Drupal\Tests\field_ui\Traits\FieldUiTestTrait; /** * Ensures that media UI works correctly. @@ -15,6 +16,8 @@ */ class MediaUiFunctionalTest extends MediaFunctionalTestBase { + use FieldUiTestTrait; + /** * Modules to enable. * @@ -23,6 +26,8 @@ class MediaUiFunctionalTest extends MediaFunctionalTestBase { protected static $modules = [ 'block', 'media_test_source', + 'media', + 'media_library', ]; /** @@ -189,15 +194,11 @@ public function testMediaWithMultipleMediaTypes() { * Tests that media in ER fields use the Rendered Entity formatter by default. */ public function testRenderedEntityReferencedMedia() { - $page = $this->getSession()->getPage(); $assert_session = $this->assertSession(); $this->drupalCreateContentType(['type' => 'page', 'name' => 'Page']); - $this->drupalGet('/admin/structure/types/manage/page/fields/add-field'); - $page->selectFieldOption('new_storage_type', 'field_ui:entity_reference:media'); - $page->fillField('label', 'Foo field'); - $page->fillField('field_name', 'foo_field'); - $page->pressButton('Save and continue'); + $this->createMediaType('image', ['id' => 'image', 'new_revision' => TRUE]); + $this->fieldUIAddNewField('/admin/structure/types/manage/page', 'foo_field', 'Foo field', 'field_ui:entity_reference:media', [], ['settings[handler_settings][target_bundles][image]' => TRUE]); $this->drupalGet('/admin/structure/types/manage/page/display'); $assert_session->fieldValueEquals('fields[field_foo_field][type]', 'entity_reference_entity_view'); } @@ -341,19 +342,11 @@ public function testMediaReferenceWidget($cardinality, array $media_type_create_ // settings form. // Using submitForm() to avoid dealing with JavaScript on the previous // page in the field creation. - $edit = [ - 'new_storage_type' => 'field_ui:entity_reference:media', - 'label' => "Media (cardinality $cardinality)", - 'field_name' => 'media_reference', - ]; - $this->drupalGet("admin/structure/types/manage/{$content_type->id()}/fields/add-field"); - $this->submitForm($edit, 'Save and continue'); - $edit = []; + $field_edit = []; foreach ($media_types as $type) { - $edit["settings[handler_settings][target_bundles][$type]"] = TRUE; + $field_edit["settings[handler_settings][target_bundles][$type]"] = TRUE; } - $this->drupalGet("admin/structure/types/manage/{$content_type->id()}/fields/node.{$content_type->id()}.field_media_reference"); - $this->submitForm($edit, "Save settings"); + $this->fieldUIAddNewField("admin/structure/types/manage/{$content_type->id()}", 'media_reference', "Media (cardinality $cardinality)", 'field_ui:entity_reference:media', [], $field_edit); \Drupal::entityTypeManager() ->getStorage('entity_form_display') ->load('node.' . $content_type->id() . '.default') diff --git a/core/modules/media/tests/src/FunctionalJavascript/MediaReferenceFieldHelpTest.php b/core/modules/media/tests/src/FunctionalJavascript/MediaReferenceFieldHelpTest.php index 74d4237bfd..277a68aa26 100644 --- a/core/modules/media/tests/src/FunctionalJavascript/MediaReferenceFieldHelpTest.php +++ b/core/modules/media/tests/src/FunctionalJavascript/MediaReferenceFieldHelpTest.php @@ -2,8 +2,6 @@ namespace Drupal\Tests\media\FunctionalJavascript; -use Drupal\Component\Utility\Html; - /** * Tests related to media reference fields. * @@ -16,6 +14,14 @@ class MediaReferenceFieldHelpTest extends MediaJavascriptTestBase { */ protected $defaultTheme = 'stark'; + /** + * {@inheritdoc} + */ + protected static $modules = [ + 'media', + 'media_library', + ]; + /** * Tests our custom help texts when creating a field. * @@ -30,31 +36,28 @@ public function testFieldCreationHelpText() { ]); $this->drupalGet("/admin/structure/types/manage/{$type->id()}/fields/add-field"); - $field_types = [ - 'file', - 'image', + $field_groups = [ + 'file_upload', 'field_ui:entity_reference:media', ]; - $description_ids = array_map(function ($item) { - return '#edit-description-' . Html::cleanCssIdentifier($item); - }, $field_types); + + $help_text = 'Use Media reference fields for most files, images, audio, videos, and remote media. Use File or Image reference fields when creating your own media types, or for legacy files and images created before enabling the Media module.'; // Choose a boolean field, none of the description containers should be // visible. - $assert_session->optionExists('edit-new-storage-type', 'boolean'); - $page->selectFieldOption('edit-new-storage-type', 'boolean'); - foreach ($description_ids as $description_id) { - $this->assertFalse($assert_session->elementExists('css', $description_id)->isVisible()); - } - // Select each of the file, image, and media fields and verify their + $assert_session->elementExists('css', "[name='new_storage_type'][value='boolean']"); + $page->find('css', "[name='new_storage_type'][value='boolean']")->getParent()->click(); + $assert_session->assertWaitOnAjaxRequest(); + $assert_session->pageTextNotContains($help_text); + + // Select each of the Reference, File upload field groups and verify their // descriptions are now visible and match the expected text. - $help_text = 'Use Media reference fields for most files, images, audio, videos, and remote media. Use File or Image reference fields when creating your own media types, or for legacy files and images created before enabling the Media module.'; - foreach ($field_types as $field_name) { - $assert_session->optionExists('edit-new-storage-type', $field_name); - $page->selectFieldOption('edit-new-storage-type', $field_name); - $field_description_element = $assert_session->elementExists('css', '#edit-description-' . Html::cleanCssIdentifier($field_name)); - $this->assertTrue($field_description_element->isVisible()); - $this->assertSame($help_text, $field_description_element->getText()); + foreach ($field_groups as $field_group) { + $assert_session->elementExists('css', "[name='new_storage_type'][value='$field_group']"); + $page->find('css', "[name='new_storage_type'][value='$field_group']")->getParent()->click(); + + $assert_session->assertWaitOnAjaxRequest(); + $assert_session->pageTextContains($help_text); } } diff --git a/core/modules/media_library/media_library.module b/core/modules/media_library/media_library.module index b858450094..82966b0870 100644 --- a/core/modules/media_library/media_library.module +++ b/core/modules/media_library/media_library.module @@ -346,6 +346,8 @@ function media_library_field_ui_preconfigured_options_alter(array &$options, $fi // Set the default field widget for media to be the Media library. if (!empty($options['media'])) { $options['media']['entity_form_display']['type'] = 'media_library_widget'; + $options['media']['description'] = t('Field to reference media. Allows uploading and selecting from uploaded media.'); + $options['media']['weight'] = -25; } } diff --git a/core/modules/media_library/tests/modules/media_library_test/src/Plugin/Field/FieldType/EntityReferenceItemSubclass.php b/core/modules/media_library/tests/modules/media_library_test/src/Plugin/Field/FieldType/EntityReferenceItemSubclass.php index e065d3aed4..9fd286d8af 100644 --- a/core/modules/media_library/tests/modules/media_library_test/src/Plugin/Field/FieldType/EntityReferenceItemSubclass.php +++ b/core/modules/media_library/tests/modules/media_library_test/src/Plugin/Field/FieldType/EntityReferenceItemSubclass.php @@ -11,10 +11,10 @@ * id = "entity_reference_subclass", * label = @Translation("Entity reference subclass"), * description = @Translation("An entity field containing an entity reference."), - * category = @Translation("Reference"), + * category = "reference", * default_widget = "entity_reference_autocomplete", * default_formatter = "entity_reference_label", - * list_class = "\Drupal\Core\Field\EntityReferenceFieldItemList" + * list_class = "\Drupal\Core\Field\EntityReferenceFieldItemList", * ) */ class EntityReferenceItemSubclass extends EntityReferenceItem { diff --git a/core/modules/media_library/tests/src/FunctionalJavascript/FieldUiIntegrationTest.php b/core/modules/media_library/tests/src/FunctionalJavascript/FieldUiIntegrationTest.php index c0f5a9cec7..170bd7192f 100644 --- a/core/modules/media_library/tests/src/FunctionalJavascript/FieldUiIntegrationTest.php +++ b/core/modules/media_library/tests/src/FunctionalJavascript/FieldUiIntegrationTest.php @@ -66,12 +66,12 @@ public function testFieldUiIntegration() { $this->drupalLogin($user); $this->drupalGet('/admin/structure/types/manage/article/fields/add-field'); - $page->selectFieldOption('new_storage_type', 'field_ui:entity_reference:media'); + $page->find('css', "[name='new_storage_type'][value='field_ui:entity_reference:media']")->getParent()->click(); $this->assertNotNull($assert_session->waitForField('label')); $page->fillField('label', 'Shatner'); $this->waitForText('field_shatner'); - $page->pressButton('Save and continue'); - $page->pressButton('Save field settings'); + $page->pressButton('Continue'); + $this->assertMatchesRegularExpression('/.*article\/add-field\/node\/field_shatner.*/', $this->getUrl()); $assert_session->pageTextNotContains('Undefined index: target_bundles'); $this->waitForFieldExists('Type One')->check(); $this->assertElementExistsAfterWait('css', '[name="settings[handler_settings][target_bundles][type_one]"][checked="checked"]'); diff --git a/core/modules/node/tests/src/Functional/NodeTranslationUITest.php b/core/modules/node/tests/src/Functional/NodeTranslationUITest.php index efbef09f13..f40a0f7964 100644 --- a/core/modules/node/tests/src/Functional/NodeTranslationUITest.php +++ b/core/modules/node/tests/src/Functional/NodeTranslationUITest.php @@ -558,9 +558,9 @@ public function testDetailsTitleIsNotEscaped() { $this->drupalLogin($this->administrator); // Make the image field a multi-value field in order to display a // details form element. - $edit = ['cardinality_number' => 2]; - $this->drupalGet('admin/structure/types/manage/article/fields/node.article.field_image/storage'); - $this->submitForm($edit, 'Save field settings'); + $edit = ['field_storage[subform][cardinality_number]' => 2]; + $this->drupalGet('admin/structure/types/manage/article/fields/node.article.field_image'); + $this->submitForm($edit, 'Save'); // Make the image field non-translatable. $edit = ['settings[node][article][fields][field_image]' => FALSE]; diff --git a/core/modules/node/tests/src/Functional/NodeTypeTranslationTest.php b/core/modules/node/tests/src/Functional/NodeTypeTranslationTest.php index 931c884bbf..69f2a140f7 100644 --- a/core/modules/node/tests/src/Functional/NodeTypeTranslationTest.php +++ b/core/modules/node/tests/src/Functional/NodeTypeTranslationTest.php @@ -172,8 +172,8 @@ public function testNodeTypeTitleLabelTranslation() { 'new_storage_type' => 'email', 'label' => 'Email', 'field_name' => 'email', - ], 'Save and continue'); - $this->submitForm([], 'Save field settings'); + ], 'Continue'); + $this->submitForm([], 'Update settings'); $this->submitForm([], 'Save settings'); $type = mb_strtolower($this->randomMachineName(16)); diff --git a/core/modules/options/css/options.icon.theme.css b/core/modules/options/css/options.icon.theme.css new file mode 100644 index 0000000000..0fc18882d3 --- /dev/null +++ b/core/modules/options/css/options.icon.theme.css @@ -0,0 +1,9 @@ +/* + * DO NOT EDIT THIS FILE. + * See the following change record for more information, + * https://www.drupal.org/node/3084859 + * @preserve + */ +.field-icon-selection_list { + background-image: url("data:image/svg+xml,%3csvg height='36' viewBox='0 0 36 36' width='36' xmlns='http://www.w3.org/2000/svg'%3e%3cpath d='m4.98 9.51v2.49h5.04v-4.98h-5.04zm7.02 0v1.47h19.98v-2.94h-19.98zm-7.02 9v2.49h5.04v-4.98h-5.04zm7.02-.03v1.5h19.98v-3h-19.98zm-7.02 9.03v2.49h5.04v-4.98h-5.04zm7.02 0v1.47h19.98v-2.94h-19.98z' fill='%2355565b'/%3e%3c/svg%3e"); +} diff --git a/core/modules/options/css/options.icon.theme.pcss.css b/core/modules/options/css/options.icon.theme.pcss.css new file mode 100644 index 0000000000..cc748ad631 --- /dev/null +++ b/core/modules/options/css/options.icon.theme.pcss.css @@ -0,0 +1,3 @@ +.field-icon-selection_list { + background-image: url(../../../misc/icons/55565b/selection_list.svg); +} diff --git a/core/modules/options/options.field_type_categories.yml b/core/modules/options/options.field_type_categories.yml new file mode 100644 index 0000000000..339cc386c8 --- /dev/null +++ b/core/modules/options/options.field_type_categories.yml @@ -0,0 +1,6 @@ +selection_list: + label: 'Selection list' + description: 'Field to select from predefined options.' + weight: -15 + libraries: + - options/drupal.options-icon diff --git a/core/modules/options/options.libraries.yml b/core/modules/options/options.libraries.yml new file mode 100644 index 0000000000..a95f9154b3 --- /dev/null +++ b/core/modules/options/options.libraries.yml @@ -0,0 +1,7 @@ +drupal.options-icon: + version: VERSION + css: + theme: + css/options.icon.theme.css: {} + dependencies: + - field_ui/drupal.field_ui.manage_fields diff --git a/core/modules/options/options.module b/core/modules/options/options.module index 30aefdd33c..074be4a0fe 100644 --- a/core/modules/options/options.module +++ b/core/modules/options/options.module @@ -5,6 +5,8 @@ * Defines selection, check box and radio button widgets for text and numeric fields. */ +use Drupal\Component\Utility\NestedArray; +use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Url; use Drupal\Core\Entity\FieldableEntityInterface; use Drupal\Core\Entity\Exception\FieldStorageDefinitionUpdateForbiddenException; @@ -136,3 +138,30 @@ function _options_values_in_use($entity_type, $field_name, $values) { return FALSE; } + +/** + * Implements hook_form_FORM_ID_alter() for field_config_edit_form. + */ +function options_form_field_config_edit_form_alter(&$form, FormStateInterface $form_state) { + $entity = $form_state->getFormObject()->getEntity(); + if ($entity->getFieldStorageDefinition()->getTypeProvider() === 'options') { + $form['field_storage']['subform']['field_storage_submit']['#submit'][] = 'options_field_config_edit_form_submit'; + $form['field_storage']['subform']['field_storage_submit']['#limit_validation_errors'] = []; + + $table = &NestedArray::getValue($form, ['field_storage', 'subform', 'settings', 'allowed_values', 'table']); + if (!$table) { + return; + } + + // @see \Drupal\options\Plugin\Field\FieldType\ListItemBase::storageSettingsForm + $form['#attached']['library'][] = 'field_ui/drupal.field_ui'; + $table['#attributes']['class'][] = 'allowed-values-table'; + } +} + +/** + * Form submission handler for field_config_edit_form. + */ +function options_field_config_edit_form_submit($form, FormStateInterface $form_state) { + drupal_static_reset('options_allowed_values'); +} diff --git a/core/modules/options/src/Plugin/Field/FieldType/ListFloatItem.php b/core/modules/options/src/Plugin/Field/FieldType/ListFloatItem.php index 0799b5fa6a..f17f28fd32 100644 --- a/core/modules/options/src/Plugin/Field/FieldType/ListFloatItem.php +++ b/core/modules/options/src/Plugin/Field/FieldType/ListFloatItem.php @@ -13,8 +13,12 @@ * @FieldType( * id = "list_float", * label = @Translation("List (float)"), - * description = @Translation("This field stores float values from a list of allowed 'value => label' pairs, i.e. 'Fraction': 0 => 0, .25 => 1/4, .75 => 3/4, 1 => 1."), - * category = @Translation("Number"), + * description = { + * @Translation("Values stored are floating-point numbers"), + * @Translation("For example, 'Fraction': 0 => 0, .25 => 1/4, .75 => 3/4, 1 => 1"), + * }, + * category = "selection_list", + * weight = -10, * default_widget = "options_select", * default_formatter = "list_default", * ) diff --git a/core/modules/options/src/Plugin/Field/FieldType/ListIntegerItem.php b/core/modules/options/src/Plugin/Field/FieldType/ListIntegerItem.php index eabec6f022..ff7c7fc959 100644 --- a/core/modules/options/src/Plugin/Field/FieldType/ListIntegerItem.php +++ b/core/modules/options/src/Plugin/Field/FieldType/ListIntegerItem.php @@ -13,8 +13,12 @@ * @FieldType( * id = "list_integer", * label = @Translation("List (integer)"), - * description = @Translation("This field stores integer values from a list of allowed 'value => label' pairs, i.e. 'Lifetime in days': 1 => 1 day, 7 => 1 week, 31 => 1 month."), - * category = @Translation("Number"), + * description = { + * @Translation("Values stored are numbers without decimals"), + * @Translation("For example, 'Lifetime in days': 1 => 1 day, 7 => 1 week, 31 => 1 month"), + * }, + * category = "selection_list", + * weight = -30, * default_widget = "options_select", * default_formatter = "list_default", * ) diff --git a/core/modules/options/src/Plugin/Field/FieldType/ListStringItem.php b/core/modules/options/src/Plugin/Field/FieldType/ListStringItem.php index acd86c0fa2..4b6a1f6004 100644 --- a/core/modules/options/src/Plugin/Field/FieldType/ListStringItem.php +++ b/core/modules/options/src/Plugin/Field/FieldType/ListStringItem.php @@ -13,8 +13,12 @@ * @FieldType( * id = "list_string", * label = @Translation("List (text)"), - * description = @Translation("This field stores text values from a list of allowed 'value => label' pairs, i.e. 'US States': IL => Illinois, IA => Iowa, IN => Indiana."), - * category = @Translation("Text"), + * description = { + * @Translation("Values stored are text values"), + * @Translation("For example, 'US States': IL => Illinois, IA => Iowa, IN => Indiana"), + * }, + * category = "selection_list", + * weight = -50, * default_widget = "options_select", * default_formatter = "list_default", * ) diff --git a/core/modules/options/tests/src/Functional/OptionsFieldUITest.php b/core/modules/options/tests/src/Functional/OptionsFieldUITest.php index 3ab68bf635..105fe9be76 100644 --- a/core/modules/options/tests/src/Functional/OptionsFieldUITest.php +++ b/core/modules/options/tests/src/Functional/OptionsFieldUITest.php @@ -303,7 +303,7 @@ protected function createOptionsField($type) { ->setComponent($this->fieldName) ->save(); - $this->adminPath = 'admin/structure/types/manage/' . $this->type . '/fields/node.' . $this->type . '.' . $this->fieldName . '/storage'; + $this->adminPath = 'admin/structure/types/manage/' . $this->type . '/fields/node.' . $this->type . '.' . $this->fieldName; } /** @@ -321,9 +321,9 @@ protected function createOptionsField($type) { * @internal */ public function assertAllowedValuesInput(string $input_string, $result, string $message): void { - $edit = ['settings[allowed_values]' => $input_string]; + $edit = ['field_storage[subform][settings][allowed_values]' => $input_string]; $this->drupalGet($this->adminPath); - $this->submitForm($edit, 'Save field settings'); + $this->submitForm($edit, 'Save'); // Verify that the page does not have double escaped HTML tags. $this->assertSession()->responseNotContains('<'); @@ -347,12 +347,12 @@ public function testNodeDisplay() { $on = $this->randomMachineName(); $off = $this->randomMachineName(); $edit = [ - 'settings[allowed_values]' => "1|$on" . PHP_EOL . "0|$off", + 'field_storage[subform][settings][allowed_values]' => "1|$on" . PHP_EOL . "0|$off", ]; $this->drupalGet($this->adminPath); - $this->submitForm($edit, 'Save field settings'); - $this->assertSession()->pageTextContains('Updated field ' . $this->fieldName . ' field settings.'); + $this->submitForm($edit, 'Save'); + $this->assertSession()->statusMessageContains("Saved {$this->fieldName} configuration.", 'status'); // Select a default value. $edit = [ @@ -400,7 +400,7 @@ public function testRequiredPropertyForAllowedValuesList() { // Try to proceed without entering any value. $this->drupalGet($this->adminPath); - $this->submitForm([], 'Save field settings'); + $this->submitForm([], 'Save'); // Confirmation message that this is a required field. $this->assertSession()->pageTextContains('Allowed values list field is required.'); diff --git a/core/modules/options/tests/src/Functional/OptionsFloatFieldImportTest.php b/core/modules/options/tests/src/Functional/OptionsFloatFieldImportTest.php index 8365d3f164..93be2be729 100644 --- a/core/modules/options/tests/src/Functional/OptionsFloatFieldImportTest.php +++ b/core/modules/options/tests/src/Functional/OptionsFloatFieldImportTest.php @@ -65,15 +65,15 @@ public function testImport() { $field_storage = FieldStorageConfig::loadByName('node', $field_name); $this->assertSame($array = ['0' => 'Zero', '0.5' => 'Point five'], $field_storage->getSetting('allowed_values')); - $admin_path = 'admin/structure/types/manage/' . $type . '/fields/node.' . $type . '.' . $field_name . '/storage'; + $admin_path = 'admin/structure/types/manage/' . $type . '/fields/node.' . $type . '.' . $field_name; // Export active config to sync. $this->copyConfig($this->container->get('config.storage'), $this->container->get('config.storage.sync')); // Set the active to not use dots in the allowed values key names. - $edit = ['settings[allowed_values]' => "0|Zero\n1|One"]; + $edit = ['field_storage[subform][settings][allowed_values]' => "0|Zero\n1|One"]; $this->drupalGet($admin_path); - $this->submitForm($edit, 'Save field settings'); + $this->submitForm($edit, 'Save'); $field_storage = FieldStorageConfig::loadByName('node', $field_name); $this->assertSame($array = ['0' => 'Zero', '1' => 'One'], $field_storage->getSetting('allowed_values')); diff --git a/core/modules/responsive_image/tests/src/FunctionalJavascript/ResponsiveImageFieldUiTest.php b/core/modules/responsive_image/tests/src/FunctionalJavascript/ResponsiveImageFieldUiTest.php index 5a2d5cf723..4409ec2747 100644 --- a/core/modules/responsive_image/tests/src/FunctionalJavascript/ResponsiveImageFieldUiTest.php +++ b/core/modules/responsive_image/tests/src/FunctionalJavascript/ResponsiveImageFieldUiTest.php @@ -4,6 +4,7 @@ use Drupal\FunctionalJavascriptTests\WebDriverTestBase; use Drupal\responsive_image\Entity\ResponsiveImageStyle; +use Drupal\Tests\field_ui\Traits\FieldUiJSTestTrait; /** * Tests the responsive image field UI. @@ -12,6 +13,8 @@ */ class ResponsiveImageFieldUiTest extends WebDriverTestBase { + use FieldUiJSTestTrait; + /** * {@inheritdoc} */ @@ -69,28 +72,11 @@ protected function setUp(): void { */ public function testResponsiveImageFormatterUi() { $manage = 'admin/structure/types/manage/' . $this->type; - $add_field = $manage . '/fields/add-field'; $manage_display = $manage . '/display'; + /** @var \Drupal\FunctionalJavascriptTests\JSWebAssert $assert_session */ $assert_session = $this->assertSession(); - // Create a field, and a node with some data for the field. - // Create the field. - $this->drupalGet($add_field); - - $page = $this->getSession()->getPage(); - $storage_type = $page->findField('edit-new-storage-type'); - $storage_type->setValue('image'); - - // Set the label. - $label = $page->findField('edit-label'); - $label->setValue('Image'); - - // Wait for the machine name. - $assert_session->waitForElementVisible('css', '[name="label"] + * .machine-name-value'); - - // Save the current page. - $save_button = $page->findButton('Save and continue'); - $save_button->click(); + $this->fieldUIAddNewFieldJS('admin/structure/types/manage/' . $this->type, 'image', 'Image', 'image'); // Display the "Manage display" page. $this->drupalGet($manage_display); diff --git a/core/modules/search/tests/src/Functional/SearchPageCacheTagsTest.php b/core/modules/search/tests/src/Functional/SearchPageCacheTagsTest.php index 940807e30c..1d8c15306f 100644 --- a/core/modules/search/tests/src/Functional/SearchPageCacheTagsTest.php +++ b/core/modules/search/tests/src/Functional/SearchPageCacheTagsTest.php @@ -4,6 +4,7 @@ use Drupal\Core\Cache\Cache; use Drupal\Tests\BrowserTestBase; +use Drupal\Tests\field_ui\Traits\FieldUiTestTrait; use Drupal\Tests\system\Functional\Cache\AssertPageCacheContextsAndTagsTrait; /** @@ -14,6 +15,7 @@ class SearchPageCacheTagsTest extends BrowserTestBase { use AssertPageCacheContextsAndTagsTrait; + use FieldUiTestTrait; /** * {@inheritdoc} @@ -155,17 +157,7 @@ public function testSearchTagsBubbling() { ]); $this->drupalLogin($admin_user); - // First step: 'Add new field' on the 'Manage fields' page. - $this->drupalGet($bundle_path . '/fields/add-field'); - $this->submitForm([ - 'label' => 'Test label', - 'field_name' => 'test__ref', - 'new_storage_type' => 'entity_reference', - ], 'Save and continue'); - - // Second step: 'Field settings' form. - $this->submitForm([], 'Save field settings'); - + $this->fieldUIAddNewField($bundle_path, 'test__ref', 'Test label', 'entity_reference', [], ['settings[handler_settings][target_bundles][page]' => TRUE]); // Create a new node of our newly created node type and fill in the entity // reference field. $edit = [ diff --git a/core/modules/system/tests/modules/entity_test/src/Plugin/Field/FieldType/AutoIncrementingTestItem.php b/core/modules/system/tests/modules/entity_test/src/Plugin/Field/FieldType/AutoIncrementingTestItem.php index d4b53defe9..f0d6beacef 100644 --- a/core/modules/system/tests/modules/entity_test/src/Plugin/Field/FieldType/AutoIncrementingTestItem.php +++ b/core/modules/system/tests/modules/entity_test/src/Plugin/Field/FieldType/AutoIncrementingTestItem.php @@ -11,7 +11,6 @@ * id = "auto_incrementing_test", * label = @Translation("Auto incrementing test field item"), * description = @Translation("An entity field designed to test the field method invocation order."), - * category = @Translation("Number"), * no_ui = TRUE, * ) */ diff --git a/core/modules/system/tests/modules/entity_test/src/Plugin/Field/FieldType/ComputedTestCacheableStringItem.php b/core/modules/system/tests/modules/entity_test/src/Plugin/Field/FieldType/ComputedTestCacheableStringItem.php index 06da5ea3ab..2ee17c143a 100644 --- a/core/modules/system/tests/modules/entity_test/src/Plugin/Field/FieldType/ComputedTestCacheableStringItem.php +++ b/core/modules/system/tests/modules/entity_test/src/Plugin/Field/FieldType/ComputedTestCacheableStringItem.php @@ -14,7 +14,6 @@ * id = "computed_test_cacheable_string_item", * label = @Translation("Test Text (plain string with cacheability)"), * description = @Translation("A test field containing a plain string value and cacheability metadata."), - * category = @Translation("Text"), * no_ui = TRUE, * default_widget = "string_textfield", * default_formatter = "string" diff --git a/core/modules/system/tests/modules/entity_test/src/Plugin/Field/FieldType/FieldTestItem.php b/core/modules/system/tests/modules/entity_test/src/Plugin/Field/FieldType/FieldTestItem.php index 1549978b42..43e43c1a82 100644 --- a/core/modules/system/tests/modules/entity_test/src/Plugin/Field/FieldType/FieldTestItem.php +++ b/core/modules/system/tests/modules/entity_test/src/Plugin/Field/FieldType/FieldTestItem.php @@ -16,7 +16,6 @@ * id = "field_test", * label = @Translation("Test field item"), * description = @Translation("A field containing a plain string value."), - * category = @Translation("Field"), * ) */ class FieldTestItem extends FieldItemBase { diff --git a/core/modules/system/tests/modules/entity_test/src/Plugin/Field/FieldType/InternalPropertyTestFieldItem.php b/core/modules/system/tests/modules/entity_test/src/Plugin/Field/FieldType/InternalPropertyTestFieldItem.php index 2bc8154cf9..647598d988 100644 --- a/core/modules/system/tests/modules/entity_test/src/Plugin/Field/FieldType/InternalPropertyTestFieldItem.php +++ b/core/modules/system/tests/modules/entity_test/src/Plugin/Field/FieldType/InternalPropertyTestFieldItem.php @@ -15,7 +15,6 @@ * id = "internal_property_test", * label = @Translation("Internal Property (test)"), * description = @Translation("A field containing one string, from which two strings are computed (one internal, one not)."), - * category = @Translation("Test"), * default_widget = "string_textfield", * default_formatter = "string" * ) diff --git a/core/modules/system/tests/modules/entity_test/src/Plugin/Field/FieldType/SerializedItem.php b/core/modules/system/tests/modules/entity_test/src/Plugin/Field/FieldType/SerializedItem.php index d460cb2026..f9606918f5 100644 --- a/core/modules/system/tests/modules/entity_test/src/Plugin/Field/FieldType/SerializedItem.php +++ b/core/modules/system/tests/modules/entity_test/src/Plugin/Field/FieldType/SerializedItem.php @@ -14,7 +14,6 @@ * id = "serialized_item_test", * label = @Translation("Test serialized field item"), * description = @Translation("A field containing a serialized string value."), - * category = @Translation("Field"), * ) */ class SerializedItem extends FieldItemBase { diff --git a/core/modules/system/tests/modules/entity_test/src/Plugin/Field/FieldType/SerializedPropertyItem.php b/core/modules/system/tests/modules/entity_test/src/Plugin/Field/FieldType/SerializedPropertyItem.php index c9b067fe42..9f5a674c5a 100644 --- a/core/modules/system/tests/modules/entity_test/src/Plugin/Field/FieldType/SerializedPropertyItem.php +++ b/core/modules/system/tests/modules/entity_test/src/Plugin/Field/FieldType/SerializedPropertyItem.php @@ -14,7 +14,6 @@ * id = "serialized_property_item_test", * label = @Translation("Test serialized property field item"), * description = @Translation("A field containing a string representing serialized data."), - * category = @Translation("Field"), * serialized_property_names = { * "value" * } diff --git a/core/modules/system/tests/modules/entity_test/src/Plugin/Field/FieldType/SingleInternalPropertyTestFieldItem.php b/core/modules/system/tests/modules/entity_test/src/Plugin/Field/FieldType/SingleInternalPropertyTestFieldItem.php index 30add62df1..8d3d25e1f4 100644 --- a/core/modules/system/tests/modules/entity_test/src/Plugin/Field/FieldType/SingleInternalPropertyTestFieldItem.php +++ b/core/modules/system/tests/modules/entity_test/src/Plugin/Field/FieldType/SingleInternalPropertyTestFieldItem.php @@ -21,7 +21,6 @@ * id = "single_internal_property_test", * label = @Translation("Single Internal Property (test)"), * description = @Translation("A field containing one string, from which one internal string is computed."), - * category = @Translation("Test"), * default_widget = "string_textfield", * default_formatter = "string" * ) diff --git a/core/modules/system/tests/src/Functional/Entity/EntityReferenceFieldCreationTest.php b/core/modules/system/tests/src/Functional/Entity/EntityReferenceFieldCreationTest.php index 9062c5cf81..89a1857042 100644 --- a/core/modules/system/tests/src/Functional/Entity/EntityReferenceFieldCreationTest.php +++ b/core/modules/system/tests/src/Functional/Entity/EntityReferenceFieldCreationTest.php @@ -4,6 +4,7 @@ use Drupal\Tests\BrowserTestBase; use Drupal\Tests\field\Traits\EntityReferenceTestTrait; +use Drupal\Tests\field_ui\Traits\FieldUiTestTrait; /** * Tests creating entity reference fields in the UI. @@ -13,6 +14,7 @@ class EntityReferenceFieldCreationTest extends BrowserTestBase { use EntityReferenceTestTrait; + use FieldUiTestTrait; /** * {@inheritdoc} @@ -33,14 +35,8 @@ public function testAddReferenceFieldTargetingEntityTypeWithoutId() { // Entity types without an ID key should not be presented as options when // creating an entity reference field in the UI. - $this->drupalGet("/admin/structure/types/manage/$node_type/fields/add-field"); - $edit = [ - 'new_storage_type' => 'entity_reference', - 'label' => 'Test Field', - 'field_name' => 'test_reference_field', - ]; - $this->submitForm($edit, 'Save and continue'); - $this->assertSession()->optionNotExists('settings[target_type]', 'entity_test_no_id'); + $this->fieldUIAddNewField("/admin/structure/types/manage/$node_type", 'test_reference_field', 'Test Field', 'entity_reference', [], [], FALSE); + $this->assertSession()->optionNotExists('field_storage[subform][settings][target_type]', 'entity_test_no_id'); // Trying to do it programmatically should raise an exception. $this->expectException('\Drupal\Core\Field\FieldException'); diff --git a/core/modules/system/tests/src/Functional/System/DateTimeTest.php b/core/modules/system/tests/src/Functional/System/DateTimeTest.php index 5ba3732053..00c3a38d87 100644 --- a/core/modules/system/tests/src/Functional/System/DateTimeTest.php +++ b/core/modules/system/tests/src/Functional/System/DateTimeTest.php @@ -5,6 +5,7 @@ use Drupal\Core\Datetime\Entity\DateFormat; use Drupal\Core\Url; use Drupal\Tests\BrowserTestBase; +use Drupal\Tests\field_ui\Traits\FieldUiTestTrait; /** * Test date formatting and time zone handling, including daylight saving time. @@ -13,6 +14,8 @@ */ class DateTimeTest extends BrowserTestBase { + use FieldUiTestTrait; + /** * Modules to enable. * @@ -211,26 +214,12 @@ public function testEnteringDateTimeViaSelectors() { $this->drupalGet('admin/structure/types/manage/page_with_date'); $this->assertSession()->statusCodeEquals(200); - $this->drupalGet('admin/structure/types/manage/page_with_date/fields/add-field'); - $edit = [ - 'new_storage_type' => 'datetime', - 'label' => 'dt', - 'field_name' => 'dt', + $storage_edit = [ + 'field_storage[subform][settings][datetime_type]' => 'datetime', + 'field_storage[subform][cardinality]' => 'number', + 'field_storage[subform][cardinality_number]' => '1', ]; - $this->drupalGet('admin/structure/types/manage/page_with_date/fields/add-field'); - $this->submitForm($edit, 'Save and continue'); - // Check that the new datetime field was created, and process is now set - // to continue for configuration. - $this->assertSession()->pageTextContains('These settings apply to the'); - - $this->drupalGet('admin/structure/types/manage/page_with_date/fields/node.page_with_date.field_dt/storage'); - $edit = [ - 'settings[datetime_type]' => 'datetime', - 'cardinality' => 'number', - 'cardinality_number' => '1', - ]; - $this->drupalGet('admin/structure/types/manage/page_with_date/fields/node.page_with_date.field_dt/storage'); - $this->submitForm($edit, 'Save field settings'); + $this->fieldUIAddNewField('admin/structure/types/manage/page_with_date', 'dt', 'dt', 'datetime', $storage_edit); $this->drupalGet('admin/structure/types/manage/page_with_date/fields'); $this->assertSession()->pageTextContains('field_dt'); diff --git a/core/modules/telephone/css/telephone.icon.theme.css b/core/modules/telephone/css/telephone.icon.theme.css new file mode 100644 index 0000000000..7d9335e3fe --- /dev/null +++ b/core/modules/telephone/css/telephone.icon.theme.css @@ -0,0 +1,9 @@ +/* + * DO NOT EDIT THIS FILE. + * See the following change record for more information, + * https://www.drupal.org/node/3084859 + * @preserve + */ +.field-icon-telephone { + background-image: url("data:image/svg+xml,%3csvg height='36' viewBox='0 0 36 36' width='36' xmlns='http://www.w3.org/2000/svg'%3e%3cpath d='m9.82246 14.9148c2.20964 4.9719 6.27314 8.9558 11.32454 11.0949.8278-.5984 1.2952-1.6427 1.853-2.4896.4167-.4122 1.25-1.2162 2.0833-.8243 2.0728.9746 3.0622 1.3152 5.4167 1.6486.2314.0328.8678.2463 1.25.2844.5749.0574 1.25.54 1.25 1.7046v4.6058c0 .8243-.6765 1.9233-1.5492 1.9844-.7289.0509-1.3237.0764-1.7841.0764-14.7917 0-26.6667-12.2065-26.6667-26.70265 0-.45554.02574-1.0438.07726-1.7649.0617-.86328 1.17274-1.53245 2.00607-1.53245h4.65592c1.17745 0 1.66525.66786 1.72325 1.23651.0386.37783.2543 1.00757.2875 1.23651.3371 2.32911.6814 3.30781 1.6667 5.35818.3961.8244-.4167 1.6487-.8334 2.0609-.9004.5803-2.2133 1.1-2.76084 2.0227z' fill='%2355565b'/%3e%3c/svg%3e"); +} diff --git a/core/modules/telephone/css/telephone.icon.theme.pcss.css b/core/modules/telephone/css/telephone.icon.theme.pcss.css new file mode 100644 index 0000000000..d92cd6c1a4 --- /dev/null +++ b/core/modules/telephone/css/telephone.icon.theme.pcss.css @@ -0,0 +1,3 @@ +.field-icon-telephone { + background-image: url(../../../misc/icons/55565b/telephone.svg); +} diff --git a/core/modules/telephone/src/Plugin/Field/FieldType/TelephoneItem.php b/core/modules/telephone/src/Plugin/Field/FieldType/TelephoneItem.php index 3cec358056..d0f0c17f05 100644 --- a/core/modules/telephone/src/Plugin/Field/FieldType/TelephoneItem.php +++ b/core/modules/telephone/src/Plugin/Field/FieldType/TelephoneItem.php @@ -14,8 +14,7 @@ * @FieldType( * id = "telephone", * label = @Translation("Telephone number"), - * description = @Translation("This field stores a telephone number in the database."), - * category = @Translation("Number"), + * description = @Translation("This field stores a telephone number."), * default_widget = "telephone_default", * default_formatter = "basic_string" * ) diff --git a/core/modules/telephone/telephone.libraries.yml b/core/modules/telephone/telephone.libraries.yml new file mode 100644 index 0000000000..21ebb918f1 --- /dev/null +++ b/core/modules/telephone/telephone.libraries.yml @@ -0,0 +1,7 @@ +drupal.telephone-icon: + version: VERSION + css: + theme: + css/telephone.icon.theme.css: {} + dependencies: + - field_ui/drupal.field_ui.manage_fields diff --git a/core/modules/telephone/telephone.module b/core/modules/telephone/telephone.module index 65a9b1c713..34899b4728 100644 --- a/core/modules/telephone/telephone.module +++ b/core/modules/telephone/telephone.module @@ -5,6 +5,7 @@ * Defines a simple telephone number field type. */ +use Drupal\Core\Field\FieldTypeCategoryManagerInterface; use Drupal\Core\Url; use Drupal\Core\Routing\RouteMatchInterface; @@ -34,3 +35,14 @@ function telephone_help($route_name, RouteMatchInterface $route_match) { function telephone_field_formatter_info_alter(&$info) { $info['string']['field_types'][] = 'telephone'; } + +/** + * Implements hook_field_type_category_info_alter(). + */ +function telephone_field_type_category_info_alter(&$definitions) { + // TRICKY: the `telephone` field type belongs in the `general` category, so + // the libraries need to be attached using an alter hook. + if (array_key_exists(FieldTypeCategoryManagerInterface::FALLBACK_CATEGORY, $definitions)) { + $definitions[FieldTypeCategoryManagerInterface::FALLBACK_CATEGORY]['libraries'][] = 'telephone/drupal.telephone-icon'; + } +} diff --git a/core/modules/text/css/text.icon.theme.css b/core/modules/text/css/text.icon.theme.css new file mode 100644 index 0000000000..bca6e90092 --- /dev/null +++ b/core/modules/text/css/text.icon.theme.css @@ -0,0 +1,9 @@ +/* + * DO NOT EDIT THIS FILE. + * See the following change record for more information, + * https://www.drupal.org/node/3084859 + * @preserve + */ +.field-icon-formatted_text { + background-image: url("data:image/svg+xml,%3csvg height='36' viewBox='0 0 36 36' width='36' xmlns='http://www.w3.org/2000/svg'%3e%3cpath d='m21 9.48v1.5h15v-3h-15zm-20.236 1.425c-.388 1.576-.705 2.913-.704 2.972 0 .097.062.105.698.09l.699-.017.459-.99c.567-1.222.894-1.777 1.322-2.241.813-.88 1.883-1.239 3.696-1.239h.579l-.04.165c-.449 1.841-4.58 17.152-4.673 17.32a1.696 1.696 0 0 1 -.362.42l-.231.185-1.104-.017-1.103-.016v1.443h10.5v-.716c0-.696-.004-.716-.135-.738-.074-.012-.676-.01-1.337.005-1.038.022-1.228.012-1.395-.074-.191-.099-.193-.105-.193-.542 0-.641.125-1.135 2.45-9.695l2.095-7.71.892.006c1.115.008 1.444.091 1.871.474.782.703 1.239 1.865 1.362 3.465l.041.525h1.849v-5.94h-16.532zm15.736 7.605v1.47h19.5v-2.94h-19.5zm-3 9v1.47h22.5v-2.94h-22.5z' fill='%2355565b'/%3e%3c/svg%3e"); +} diff --git a/core/modules/text/css/text.icon.theme.pcss.css b/core/modules/text/css/text.icon.theme.pcss.css new file mode 100644 index 0000000000..bc60992882 --- /dev/null +++ b/core/modules/text/css/text.icon.theme.pcss.css @@ -0,0 +1,3 @@ +.field-icon-formatted_text { + background-image: url(../../../misc/icons/55565b/formatted_text.svg); +} diff --git a/core/modules/text/src/Plugin/Field/FieldType/TextItem.php b/core/modules/text/src/Plugin/Field/FieldType/TextItem.php index 8926a4acb6..5492544db2 100644 --- a/core/modules/text/src/Plugin/Field/FieldType/TextItem.php +++ b/core/modules/text/src/Plugin/Field/FieldType/TextItem.php @@ -11,8 +11,13 @@ * @FieldType( * id = "text", * label = @Translation("Text (formatted)"), - * description = @Translation("This field stores a text with a text format."), - * category = @Translation("Text"), + * description = { + * @Translation("Ideal for titles and names that need to support markup such as bold, italics or links"), + * @Translation("Efficient storage for short text"), + * @Translation("Requires specifying a maximum length"), + * @Translation("Good for fields with known or predictable lengths"), + * }, + * category = "formatted_text", * default_widget = "text_textfield", * default_formatter = "text_default", * list_class = "\Drupal\text\Plugin\Field\FieldType\TextFieldItemList" diff --git a/core/modules/text/src/Plugin/Field/FieldType/TextLongItem.php b/core/modules/text/src/Plugin/Field/FieldType/TextLongItem.php index ce35873326..3656d05e4f 100644 --- a/core/modules/text/src/Plugin/Field/FieldType/TextLongItem.php +++ b/core/modules/text/src/Plugin/Field/FieldType/TextLongItem.php @@ -10,8 +10,12 @@ * @FieldType( * id = "text_long", * label = @Translation("Text (formatted, long)"), - * description = @Translation("This field stores a long text with a text format."), - * category = @Translation("Text"), + * description = { + * @Translation("Ideal for longer texts, like body or description without a summary"), + * @Translation("Supports long text without specifying a maximum length"), + * @Translation("May use more storage and be slower for searching and sorting"), + * }, + * category = "formatted_text", * default_widget = "text_textarea", * default_formatter = "text_default", * list_class = "\Drupal\text\Plugin\Field\FieldType\TextFieldItemList" diff --git a/core/modules/text/src/Plugin/Field/FieldType/TextWithSummaryItem.php b/core/modules/text/src/Plugin/Field/FieldType/TextWithSummaryItem.php index 088986d576..9d43920520 100644 --- a/core/modules/text/src/Plugin/Field/FieldType/TextWithSummaryItem.php +++ b/core/modules/text/src/Plugin/Field/FieldType/TextWithSummaryItem.php @@ -13,8 +13,13 @@ * @FieldType( * id = "text_with_summary", * label = @Translation("Text (formatted, long, with summary)"), - * description = @Translation("This field stores long text with a format and an optional summary."), - * category = @Translation("Text"), + * description = { + * @Translation("Ideal for longer texts, like body or description with a summary"), + * @Translation("Allows specifying a summary for the text"), + * @Translation("Supports long text without specifying a maximum length"), + * @Translation("May use more storage and be slower for searching and sorting"), + * }, + * category = "formatted_text", * default_widget = "text_textarea_with_summary", * default_formatter = "text_default", * list_class = "\Drupal\text\Plugin\Field\FieldType\TextFieldItemList" diff --git a/core/modules/text/text.field_type_categories.yml b/core/modules/text/text.field_type_categories.yml new file mode 100644 index 0000000000..c819e4b83b --- /dev/null +++ b/core/modules/text/text.field_type_categories.yml @@ -0,0 +1,6 @@ +formatted_text: + label: 'Formatted text' + description: 'Text field with markup support and optional editor.' + weight: -45 + libraries: + - text/drupal.text-icon diff --git a/core/modules/text/text.libraries.yml b/core/modules/text/text.libraries.yml index ebcfd3ab88..91df5cac84 100644 --- a/core/modules/text/text.libraries.yml +++ b/core/modules/text/text.libraries.yml @@ -6,3 +6,11 @@ drupal.text: - core/jquery - core/once - core/drupal + +drupal.text-icon: + version: VERSION + css: + theme: + css/text.icon.theme.css: {} + dependencies: + - field_ui/drupal.field_ui.manage_fields diff --git a/core/modules/workspaces/tests/src/Functional/WorkspaceTest.php b/core/modules/workspaces/tests/src/Functional/WorkspaceTest.php index 65e800c83d..4e9d012bf0 100644 --- a/core/modules/workspaces/tests/src/Functional/WorkspaceTest.php +++ b/core/modules/workspaces/tests/src/Functional/WorkspaceTest.php @@ -3,6 +3,7 @@ namespace Drupal\Tests\workspaces\Functional; use Drupal\Tests\BrowserTestBase; +use Drupal\Tests\field_ui\Traits\FieldUiTestTrait; use Drupal\Tests\node\Traits\ContentTypeCreationTrait; use Drupal\Tests\taxonomy\Traits\TaxonomyTestTrait; @@ -16,6 +17,7 @@ class WorkspaceTest extends BrowserTestBase { use WorkspaceTestUtilities; use ContentTypeCreationTrait; use TaxonomyTestTrait; + use FieldUiTestTrait; /** * {@inheritdoc} @@ -226,16 +228,7 @@ public function testWorkspaceFieldUi() { // Create a new filed. $field_name = mb_strtolower($this->randomMachineName()); $field_label = $this->randomMachineName(); - $edit = [ - 'new_storage_type' => 'string', - 'label' => $field_label, - 'field_name' => $field_name, - ]; - $this->drupalGet("admin/config/workflow/workspaces/fields/add-field"); - $this->submitForm($edit, 'Save and continue'); - $page = $this->getSession()->getPage(); - $page->pressButton('Save field settings'); - $page->pressButton('Save settings'); + $this->fieldUIAddNewField('admin/config/workflow/workspaces', $field_name, $field_label, 'string'); // Check that the field is displayed on the manage form display page. $this->drupalGet('admin/config/workflow/workspaces/form-display'); diff --git a/core/phpstan-baseline.neon b/core/phpstan-baseline.neon index cf0e157801..4fe2802420 100644 --- a/core/phpstan-baseline.neon +++ b/core/phpstan-baseline.neon @@ -475,16 +475,6 @@ parameters: count: 1 path: lib/Drupal/Core/Field/FieldItemList.php - - - message: "#^Call to method getDefinitions\\(\\) on an unknown class Drupal\\\\Core\\\\Plugin\\\\CategorizingPluginManagerTrait\\.$#" - count: 1 - path: lib/Drupal/Core/Field/FieldTypePluginManager.php - - - - message: "#^Call to method getSortedDefinitions\\(\\) on an unknown class Drupal\\\\Core\\\\Plugin\\\\CategorizingPluginManagerTrait\\.$#" - count: 1 - path: lib/Drupal/Core/Field/FieldTypePluginManager.php - - message: "#^Call to deprecated constant REQUEST_TIME\\: Deprecated in drupal\\:8\\.3\\.0 and is removed from drupal\\:11\\.0\\.0\\. Use \\\\Drupal\\:\\:time\\(\\)\\-\\>getRequestTime\\(\\); $#" count: 1 diff --git a/core/tests/Drupal/KernelTests/Core/Field/FieldSettingsTest.php b/core/tests/Drupal/KernelTests/Core/Field/FieldSettingsTest.php index 1ba196ce58..877566d4ef 100644 --- a/core/tests/Drupal/KernelTests/Core/Field/FieldSettingsTest.php +++ b/core/tests/Drupal/KernelTests/Core/Field/FieldSettingsTest.php @@ -111,14 +111,21 @@ public function testConfigurableFieldSettings() { 'entity_type' => 'entity_test', 'type' => 'test_field', ]); + + $field_storage->save(); + $expected_settings = [ + 'test_field_storage_setting' => 'dummy test string', + 'changeable' => 'a changeable field storage setting', + 'unchangeable' => 'an unchangeable field storage setting', + 'translatable_storage_setting' => 'a translatable field storage setting', + 'storage_setting_from_config_data' => 'TRUE', + ]; + $this->assertEquals($expected_settings, $field_storage->getSettings()); + $field = FieldConfig::create([ 'field_storage' => $field_storage, 'bundle' => 'entity_test', ]); - // Note: FieldConfig does not populate default settings until the config - // is saved. - // @todo Remove once https://www.drupal.org/node/2327883 is fixed. - $field->save(); // Check that the default settings have been populated. Note: getSettings() // returns both storage and field settings. @@ -129,7 +136,7 @@ public function testConfigurableFieldSettings() { 'translatable_storage_setting' => 'a translatable field storage setting', 'test_field_setting' => 'dummy test string', 'translatable_field_setting' => 'a translatable field setting', - 'field_setting_from_config_data' => 'TRUE', + 'storage_setting_from_config_data' => 'TRUE', ]; $this->assertEquals($expected_settings, $field->getSettings()); @@ -138,6 +145,20 @@ public function testConfigurableFieldSettings() { $expected_settings['test_field_setting'] = 'another test string'; $field->setSettings(['test_field_setting' => $expected_settings['test_field_setting']]); $this->assertEquals($expected_settings, $field->getSettings()); + + // Save the field and check the expected settings. + $field->save(); + + $expected_settings['field_setting_from_config_data'] = 'TRUE'; + $this->assertEquals($expected_settings, $field->getSettings()); + + $field = FieldConfig::loadByName('entity_test', 'entity_test', 'test_field'); + + $this->assertEquals($expected_settings, $field->getSettings()); + + $expected_settings['test_field_setting'] = 'yet another test string'; + $field->setSettings(['test_field_setting' => $expected_settings['test_field_setting']]); + $this->assertEquals($expected_settings, $field->getSettings()); } } diff --git a/core/tests/Drupal/KernelTests/Core/Theme/Stable9LibraryOverrideTest.php b/core/tests/Drupal/KernelTests/Core/Theme/Stable9LibraryOverrideTest.php index e140b481c4..d4361352f7 100644 --- a/core/tests/Drupal/KernelTests/Core/Theme/Stable9LibraryOverrideTest.php +++ b/core/tests/Drupal/KernelTests/Core/Theme/Stable9LibraryOverrideTest.php @@ -18,6 +18,15 @@ class Stable9LibraryOverrideTest extends StableLibraryOverrideTestBase { 'core/drupal.dialog.off_canvas', 'layout_builder/drupal.layout_builder', 'views/views.responsive-grid', + 'field_ui/drupal.field_ui.manage_fields', + 'comment/drupal.comment-icon', + 'file/drupal.file-icon', + 'text/drupal.text-icon', + 'link/drupal.link-icon', + 'media/drupal.media-icon', + 'options/drupal.options-icon', + 'telephone/drupal.telephone-icon', + 'datetime_range/drupal.datetime_range-icon', ]; /** diff --git a/core/tests/Drupal/Tests/Core/Field/BaseFieldDefinitionTestBase.php b/core/tests/Drupal/Tests/Core/Field/BaseFieldDefinitionTestBase.php index c2f45ae311..3398baf9f8 100644 --- a/core/tests/Drupal/Tests/Core/Field/BaseFieldDefinitionTestBase.php +++ b/core/tests/Drupal/Tests/Core/Field/BaseFieldDefinitionTestBase.php @@ -4,6 +4,7 @@ use Drupal\Core\DependencyInjection\ContainerBuilder; use Drupal\Core\Field\BaseFieldDefinition; +use Drupal\Core\Field\FieldTypeCategoryManagerInterface; use Drupal\Core\Field\FieldTypePluginManager; use Drupal\Core\TypedData\TypedDataManagerInterface; use Drupal\Tests\UnitTestCase; @@ -38,11 +39,13 @@ protected function setUp(): void { ->with($module_name) ->willReturn(TRUE); $typed_data_manager = $this->createMock(TypedDataManagerInterface::class); + $field_type_category_manager = $this->createMock(FieldTypeCategoryManagerInterface::class); $plugin_manager = new FieldTypePluginManager( $namespaces, $this->createMock('Drupal\Core\Cache\CacheBackendInterface'), $module_handler, - $typed_data_manager + $typed_data_manager, + $field_type_category_manager, ); $container = new ContainerBuilder(); diff --git a/core/tests/Drupal/Tests/Core/Field/FieldTypePluginManagerTest.php b/core/tests/Drupal/Tests/Core/Field/FieldTypePluginManagerTest.php new file mode 100644 index 0000000000..ac286f2456 --- /dev/null +++ b/core/tests/Drupal/Tests/Core/Field/FieldTypePluginManagerTest.php @@ -0,0 +1,182 @@ +prophesize(AccountInterface::class); + $container->set('current_user', $current_user->reveal()); + $container->set('string_translation', $this->getStringTranslationStub()); + \Drupal::setContainer($container); + + $cache_backend = $this->prophesize(CacheBackendInterface::class); + $this->moduleHandler = $this->prophesize(ModuleHandlerInterface::class); + $this->moduleHandler->alter('field_info', Argument::any())->willReturn(NULL); + $typed_data_manager = $this->prophesize(TypedDataManager::class); + $this->fieldTypeCategoryManager = $this->prophesize(FieldTypeCategoryManagerInterface::class); + + $this->fieldTypeManager = new FieldTypePluginManager(new \ArrayObject(), $cache_backend->reveal(), $this->moduleHandler->reveal(), $typed_data_manager->reveal(), $this->fieldTypeCategoryManager->reveal()); + $this->fieldTypeManager->setStringTranslation($this->getStringTranslationStub()); + + $this->discovery = $this->prophesize(DiscoveryInterface::class); + $property = new \ReflectionProperty(FieldTypePluginManager::class, 'discovery'); + $property->setAccessible(TRUE); + $property->setValue($this->fieldTypeManager, $this->discovery->reveal()); + } + + /** + * @covers ::getGroupedDefinitions + */ + public function testGetGroupedDefinitions() { + $this->discovery->getDefinitions()->willReturn([ + 'telephone' => [ + 'category' => 'general', + 'label' => 'Telephone', + 'id' => 'telephone', + ], + 'string' => [ + 'category' => 'text', + 'label' => 'Text (plain)', + 'id' => 'string', + ], + 'integer' => [ + 'category' => 'number', + 'label' => 'Number (integer)', + 'id' => 'integer', + ], + 'float' => [ + 'id' => 'float', + 'label' => 'Number (float)', + 'category' => 'number', + ], + ]); + + $this->fieldTypeCategoryManager->getDefinitions()->willReturn([ + 'general' => [ + 'label' => 'General', + 'id' => 'general', + ], + 'number' => [ + 'label' => 'Number 🦥', + 'id' => 'number', + ], + 'text' => [ + 'label' => 'Text 🐈', + 'id' => 'text', + ], + 'empty_group' => [ + 'label' => 'Empty 🦗', + 'id' => 'empty_group', + ], + ]); + + $grouped_definitions = $this->fieldTypeManager->getGroupedDefinitions(); + $this->assertEquals(['General', 'Number 🦥', 'Text 🐈'], array_keys($grouped_definitions)); + + $grouped_definitions = $this->fieldTypeManager->getGroupedDefinitions(NULL, 'label', 'id'); + $this->assertEquals(['general', 'number', 'text'], array_keys($grouped_definitions)); + } + + /** + * @covers ::getGroupedDefinitions + */ + public function testGetGroupedDefinitionsInvalid() { + $this->discovery->getDefinitions()->willReturn([ + 'string' => [ + 'category' => 'text', + 'label' => 'Text (plain)', + 'id' => 'string', + ], + ]); + + $this->fieldTypeCategoryManager->getDefinitions()->willReturn([ + 'general' => [ + 'label' => 'General', + 'id' => 'general', + ], + ]); + + $zend_assertions_default = ini_get('zend.assertions'); + $assert_active_default = assert_options(ASSERT_ACTIVE); + + // Test behavior when assertions are not enabled. + ini_set('zend.assertions', 0); + assert_options(ASSERT_ACTIVE, 0); + $grouped_definitions = $this->fieldTypeManager->getGroupedDefinitions(); + $this->assertEquals(['General'], array_keys($grouped_definitions)); + + // Test behavior when assertions are enabled. + ini_set('zend.assertions', 1); + assert_options(ASSERT_ACTIVE, 1); + $this->expectException(\AssertionError::class); + try { + $this->fieldTypeManager->getGroupedDefinitions(); + } + catch (\Exception $e) { + // Reset the original assert values. + ini_set('zend.assertions', $zend_assertions_default); + assert_options(ASSERT_ACTIVE, $assert_active_default); + + throw $e; + } + } + + /** + * @covers ::getGroupedDefinitions + */ + public function testGetGroupedDefinitionsEmpty() { + $this->fieldTypeCategoryManager->getDefinitions()->willReturn([]); + $this->assertEquals([], $this->fieldTypeManager->getGroupedDefinitions([])); + } + +} diff --git a/core/themes/stable9/templates/admin/form-element--new-storage-type.html.twig b/core/themes/stable9/templates/admin/form-element--new-storage-type.html.twig new file mode 100644 index 0000000000..e2f7e3a714 --- /dev/null +++ b/core/themes/stable9/templates/admin/form-element--new-storage-type.html.twig @@ -0,0 +1,46 @@ +{# +/** + * @file + * Theme override for a storage type option form element. + * + * Available variables: + * - attributes: HTML attributes for the containing element. + * - errors: (optional) Any errors for this form element, may not be set. + * - label: A rendered label element. + * - description: (optional) A list of description properties containing: + * - content: A description of the form element, may not be set. + * - attributes: (optional) A list of HTML attributes to apply to the + * description content wrapper. Will only be set when description is set. + * - variant: specifies option type. Typically 'field-option' or 'field-suboption'. + * + * @see template_preprocess_form_element() + * + * @ingroup themeable + */ +#} +{% + set classes = [ + errors ? 'form-item--error', + variant ? variant ~ '__item' +] +%} + + {% if variant == 'field-option' %} + {{ label }} + + {{ description.content }} + + {% endif %} + {{ children }} + {% if variant == 'field-suboption' %} + {{ label }} + + {{ description.content }} + + {% endif %} + {% if errors %} +
+ {{ errors }} +
+ {% endif %} +