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'
+ ]
+%}
+