Initial Drupal 11 with DDEV setup

This commit is contained in:
gluebox
2025-10-08 11:39:17 -04:00
commit 89ef74b305
25344 changed files with 2599172 additions and 0 deletions

View File

@ -0,0 +1,97 @@
<?php
namespace Drupal\field_ui\Access;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Access\AccessResultInterface;
use Drupal\Core\Cache\CacheableDependencyInterface;
use Drupal\Core\Entity\EntityFieldManagerInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Field\FieldTypePluginManagerInterface;
use Drupal\Core\Routing\Access\AccessInterface;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\field\FieldStorageConfigInterface;
use Symfony\Component\Routing\Route;
/**
* Defines an access check for the reuse existing fields form.
*/
class FieldReuseAccessCheck implements AccessInterface {
/**
* Creates a new FieldReuseAccessCheck.
*
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager
* The entity type manager.
* @param \Drupal\Core\Field\FieldTypePluginManagerInterface $fieldTypePluginManager
* The field type plugin manager.
* @param \Drupal\Core\Entity\EntityFieldManagerInterface $entityFieldManager
* The entity field manager.
*/
public function __construct(
protected EntityTypeManagerInterface $entityTypeManager,
protected FieldTypePluginManagerInterface $fieldTypePluginManager,
protected EntityFieldManagerInterface $entityFieldManager,
) {}
/**
* Checks access to the reuse existing fields form.
*
* @param \Symfony\Component\Routing\Route $route
* The route to check against.
* @param \Drupal\Core\Routing\RouteMatchInterface $route_match
* The parametrized route.
* @param \Drupal\Core\Session\AccountInterface $account
* The currently logged in account.
* @param string|null $bundle
* (optional) The bundle. Different entity types can have different names
* for their bundle key, so if not specified on the route via a {bundle}
* parameter, the access checker determines the appropriate key name, and
* gets the value from the corresponding request attribute. For example, for
* nodes, the bundle key is "node_type", so the value would be available via
* the {node_type} parameter rather than a {bundle} parameter.
*
* @return \Drupal\Core\Access\AccessResultInterface
* The access result.
*/
public function access(Route $route, RouteMatchInterface $route_match, AccountInterface $account, ?string $bundle = NULL): AccessResultInterface {
$access = AccessResult::neutral();
if ($entity_type_id = $route->getDefault('entity_type_id')) {
if (empty($bundle)) {
$entity_type = $this->entityTypeManager->getDefinition($entity_type_id);
$bundle = $route_match->getRawParameter($entity_type->getBundleEntityType());
}
$field_types = $this->fieldTypePluginManager->getDefinitions();
// Allows access if there are any existing fields and the user
// correct permissions.
foreach ($this->entityFieldManager->getFieldStorageDefinitions($entity_type_id) as $field_storage) {
// Do not include fields with
// - non-configurable field storages,
// - locked field storages,
// - field storages that should not be added via user interface,
// - field storages that already have a field in the bundle.
$field_type = $field_storage->getType();
$access->addCacheableDependency($field_storage);
if ($field_storage instanceof FieldStorageConfigInterface
&& !$field_storage->isLocked()
&& empty($field_types[$field_type]['no_ui'])
&& !in_array($bundle, $field_storage->getBundles(), TRUE)) {
$permission = $route->getRequirement('_field_ui_field_reuse_access');
$access = $access->orIf(AccessResult::allowedIfHasPermission($account, $permission));
}
}
// @todo https://www.drupal.org/project/drupal/issues/3446507 Decide if
// this logic needs to be changed or removed.
if ($this->entityFieldManager instanceof CacheableDependencyInterface) {
$access->addCacheableDependency($this->entityFieldManager);
}
else {
$access->setCacheMaxAge(0);
}
}
return $access;
}
}

View File

@ -0,0 +1,87 @@
<?php
namespace Drupal\field_ui\Access;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Routing\Access\AccessInterface;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Session\AccountInterface;
use Symfony\Component\Routing\Route;
/**
* Defines an access check for entity form mode routes.
*
* @see \Drupal\Core\Entity\Entity\EntityFormMode
*/
class FormModeAccessCheck implements AccessInterface {
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* Creates a new FormModeAccessCheck.
*
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
*/
public function __construct(EntityTypeManagerInterface $entity_type_manager) {
$this->entityTypeManager = $entity_type_manager;
}
/**
* Checks access to the form mode.
*
* @param \Symfony\Component\Routing\Route $route
* The route to check against.
* @param \Drupal\Core\Routing\RouteMatchInterface $route_match
* The parametrized route.
* @param \Drupal\Core\Session\AccountInterface $account
* The currently logged in account.
* @param string $form_mode_name
* (optional) The form mode. Defaults to 'default'.
* @param string $bundle
* (optional) The bundle. Different entity types can have different names
* for their bundle key, so if not specified on the route via a {bundle}
* parameter, the access checker determines the appropriate key name, and
* gets the value from the corresponding request attribute. For example,
* for nodes, the bundle key is "node_type", so the value would be
* available via the {node_type} parameter rather than a {bundle}
* parameter.
*
* @return \Drupal\Core\Access\AccessResultInterface
* The access result.
*/
public function access(Route $route, RouteMatchInterface $route_match, AccountInterface $account, $form_mode_name = 'default', $bundle = NULL) {
$access = AccessResult::neutral();
if ($entity_type_id = $route->getDefault('entity_type_id')) {
if (empty($bundle)) {
$entity_type = $this->entityTypeManager->getDefinition($entity_type_id);
$bundle = $route_match->getRawParameter($entity_type->getBundleEntityType());
}
$visibility = FALSE;
if ($form_mode_name == 'default') {
$visibility = TRUE;
}
elseif ($entity_display = $this->entityTypeManager->getStorage('entity_form_display')->load($entity_type_id . '.' . $bundle . '.' . $form_mode_name)) {
$visibility = $entity_display->status();
}
if ($form_mode_name != 'default' && $entity_display) {
$access->addCacheableDependency($entity_display);
}
if ($visibility) {
$permission = $route->getRequirement('_field_ui_form_mode_access');
$access = $access->orIf(AccessResult::allowedIfHasPermission($account, $permission));
}
}
return $access;
}
}

View File

@ -0,0 +1,88 @@
<?php
namespace Drupal\field_ui\Access;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Routing\Access\AccessInterface;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Session\AccountInterface;
use Symfony\Component\Routing\Route;
/**
* Defines an access check for entity view mode routes.
*
* @see \Drupal\Core\Entity\Entity\EntityViewMode
*/
class ViewModeAccessCheck implements AccessInterface {
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* Creates a new ViewModeAccessCheck.
*
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
*/
public function __construct(EntityTypeManagerInterface $entity_type_manager) {
$this->entityTypeManager = $entity_type_manager;
}
/**
* Checks access to the view mode.
*
* @param \Symfony\Component\Routing\Route $route
* The route to check against.
* @param \Drupal\Core\Routing\RouteMatchInterface $route_match
* The parametrized route.
* @param \Drupal\Core\Session\AccountInterface $account
* The currently logged in account.
* @param string $view_mode_name
* (optional) The view mode. Defaults to 'default'.
* @param string $bundle
* (optional) The bundle. Different entity types can have different names
* for their bundle key, so if not specified on the route via a {bundle}
* parameter, the access checker determines the appropriate key name, and
* gets the value from the corresponding request attribute. For example,
* for nodes, the bundle key is "node_type", so the value would be
* available via the {node_type} parameter rather than a {bundle}
* parameter.
*
* @return \Drupal\Core\Access\AccessResultInterface
* The access result.
*/
public function access(Route $route, RouteMatchInterface $route_match, AccountInterface $account, $view_mode_name = 'default', $bundle = NULL) {
$access = AccessResult::neutral();
if ($entity_type_id = $route->getDefault('entity_type_id')) {
if (empty($bundle)) {
$entity_type = $this->entityTypeManager->getDefinition($entity_type_id);
$bundle = $route_match->getRawParameter($entity_type->getBundleEntityType());
}
$visibility = FALSE;
if ($view_mode_name == 'default') {
$visibility = TRUE;
}
elseif ($entity_display = $this->entityTypeManager->getStorage('entity_view_display')->load($entity_type_id . '.' . $bundle . '.' . $view_mode_name)) {
$visibility = $entity_display->status();
}
if ($view_mode_name != 'default' && $entity_display) {
$access->addCacheableDependency($entity_display);
}
if ($visibility) {
$permission = $route->getRequirement('_field_ui_view_mode_access');
$access = $access->orIf(AccessResult::allowedIfHasPermission($account, $permission));
}
}
return $access;
}
}

View File

@ -0,0 +1,84 @@
<?php
namespace Drupal\field_ui\Controller;
use Drupal\Component\Serialization\Json;
use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\Url;
/**
* Provides methods for entity display mode routes.
*/
class EntityDisplayModeController extends ControllerBase {
/**
* Provides a list of eligible entity types for adding view modes.
*
* @return array
* A list of entity types to add a view mode for.
*/
public function viewModeTypeSelection() {
$entity_types = [];
foreach ($this->entityTypeManager()->getDefinitions() as $entity_type_id => $entity_type) {
if ($entity_type->get('field_ui_base_route') && $entity_type->hasViewBuilderClass()) {
$entity_types[$entity_type_id] = [
'title' => $entity_type->getLabel(),
'url' => Url::fromRoute('entity.entity_view_mode.add_form', ['entity_type_id' => $entity_type_id])->setOption('attributes', [
'class' => ['use-ajax'],
'data-dialog-type' => 'modal',
'data-dialog-options' => Json::encode([
'width' => '880',
]),
]),
];
}
}
// Move content at the top.
array_splice($entity_types, 0, 0, array_splice($entity_types, array_search('node', array_keys($entity_types)), 1));
return [
'#theme' => 'admin_block_content',
'#content' => $entity_types,
'#attached' => [
'library' => [
'core/drupal.dialog.ajax',
],
],
];
}
/**
* Provides a list of eligible entity types for adding form modes.
*
* @return array
* A list of entity types to add a form mode for.
*/
public function formModeTypeSelection() {
$entity_types = [];
foreach ($this->entityTypeManager()->getDefinitions() as $entity_type_id => $entity_type) {
if ($entity_type->get('field_ui_base_route') && $entity_type->hasFormClasses()) {
$entity_types[$entity_type_id] = [
'title' => $entity_type->getLabel(),
'url' => Url::fromRoute('entity.entity_form_mode.add_form', ['entity_type_id' => $entity_type_id])->setOption('attributes', [
'class' => ['use-ajax'],
'data-dialog-type' => 'modal',
'data-dialog-options' => Json::encode([
'width' => '880',
]),
]),
];
}
}
// Move content at the top.
array_splice($entity_types, 0, 0, array_splice($entity_types, array_search('node', array_keys($entity_types)), 1));
return [
'#theme' => 'admin_block_content',
'#content' => $entity_types,
'#attached' => [
'library' => [
'core/drupal.dialog.ajax',
],
],
];
}
}

View File

@ -0,0 +1,67 @@
<?php
declare(strict_types=1);
namespace Drupal\field_ui\Controller;
use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\TempStore\PrivateTempStore;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
/**
* Controller for building the field instance form.
*
* @internal
*/
final class FieldConfigAddController extends ControllerBase {
/**
* FieldConfigAddController constructor.
*
* @param \Drupal\Core\TempStore\PrivateTempStore $tempStore
* The private tempstore.
*/
public function __construct(
protected readonly PrivateTempStore $tempStore,
) {}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->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'],
]);
}
}

View File

@ -0,0 +1,31 @@
<?php
namespace Drupal\field_ui\Controller;
use Drupal\Core\Entity\Controller\EntityListController;
use Drupal\Core\Routing\RouteMatchInterface;
/**
* Defines a controller to list field instances.
*/
class FieldConfigListController extends EntityListController {
/**
* Shows the 'Manage fields' page.
*
* @param string $entity_type_id
* The entity type.
* @param string $bundle
* The entity bundle.
* @param \Drupal\Core\Routing\RouteMatchInterface $route_match
* The current route match.
*
* @return array
* A render array as expected by
* \Drupal\Core\Render\RendererInterface::render().
*/
public function listing($entity_type_id = NULL, $bundle = NULL, ?RouteMatchInterface $route_match = NULL) {
return $this->entityTypeManager()->getListBuilder('field_config')->render($entity_type_id, $bundle);
}
}

View File

@ -0,0 +1,208 @@
<?php
declare(strict_types=1);
namespace Drupal\field_ui\Controller;
use Drupal\Component\Serialization\Json;
use Drupal\Component\Utility\Html;
use Drupal\Component\Utility\SortArray;
use Drupal\Core\Ajax\AjaxHelperTrait;
use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\Field\FallbackFieldTypeCategory;
use Drupal\Core\Field\FieldTypeCategoryManagerInterface;
use Drupal\Core\Field\FieldTypePluginManagerInterface;
use Drupal\Core\TempStore\PrivateTempStore;
use Drupal\Core\Url;
use Drupal\field_ui\FieldUI;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Controller for building the field type links.
*
* @internal
*/
final class FieldStorageAddController extends ControllerBase {
use AjaxHelperTrait;
/**
* The name of the entity type.
*
* @var string
*/
protected $entityTypeId;
/**
* The entity bundle.
*
* @var string
*/
protected $bundle;
/**
* Constructs a new FieldStorageAddController.
*/
public function __construct(
protected FieldTypePluginManagerInterface $fieldTypePluginManager,
protected FieldTypeCategoryManagerInterface $fieldTypeCategoryManager,
protected PrivateTempStore $tempStore,
) {}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('plugin.manager.field.field_type'),
$container->get('plugin.manager.field.field_type_category'),
$container->get('tempstore.private')->get('field_ui'),
);
}
/**
* Deletes stored field data and builds the field selection links.
*
* @param string $entity_type_id
* The name of the entity type.
* @param string $bundle
* The entity bundle.
* @param string $field_name
* The field name.
*
* @return array
* The field selection links.
*/
public function resetField(string $entity_type_id, string $bundle, string $field_name) {
// Delete stored field data in case user changes field type.
$this->tempStore->delete("$entity_type_id:$field_name");
return $this->getFieldSelectionLinks($entity_type_id, $bundle);
}
/**
* Builds the field selection links.
*
* @param string $entity_type_id
* The name of the entity type.
* @param string $bundle
* The entity bundle.
*
* @return array
* The field selection links.
*/
public function getFieldSelectionLinks(string $entity_type_id, string $bundle) {
$build = [];
$this->entityTypeId = $entity_type_id;
$this->bundle = $bundle;
$ui_definitions = $this->fieldTypePluginManager->getEntityTypeUiDefinitions($entity_type_id);
$field_type_options = $unique_definitions = [];
$grouped_definitions = $this->fieldTypePluginManager->getGroupedDefinitions($ui_definitions, '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;
}
}
}
$build['add-label'] = [
'#type' => 'label',
'#title' => $this->t('Choose a type of field'),
'#title_display' => 'before',
'#required' => TRUE,
];
$build['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);
$entity_type = $this->entityTypeManager()->getDefinition($this->entityTypeId);
$display_as_group = !($category_info instanceof FallbackFieldTypeCategory);
$route_parameters = [
'entity_type' => $this->entityTypeId,
'bundle' => $this->bundle,
'display_as_group' => $display_as_group ? 'true' : 'false',
'selected_field_type' => $category_info->getPluginId(),
] + FieldUI::getRouteBundleParameter($entity_type, $this->bundle);
$cleaned_class_name = Html::getClass($field_type['unique_identifier']);
$field_type_options_radios[$id] = [
'#type' => 'html_tag',
'#tag' => 'a',
'#attributes' => [
'class' => ['field-option', 'use-ajax'],
'role' => 'button',
'tabindex' => '0',
'data-dialog-type' => 'modal',
'data-dialog-options' => Json::encode([
'width' => 1100,
'title' => $this->t('Add field: @type', ['@type' => $category_info->getLabel()]),
]),
'href' => Url::fromRoute("field_ui.field_storage_config_add_sub_{$this->entityTypeId}", $route_parameters)->toString(),
],
'#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,
],
'words' => [
'#type' => 'container',
'#attributes' => [
'class' => ['field-option__words'],
],
'label' => [
'#attributes' => [
'class' => ['field-option__label'],
],
'#type' => 'html_tag',
'#tag' => 'span',
'#value' => $category_info->getLabel(),
],
'description' => [
'#type' => 'container',
'#attributes' => [
'class' => ['field-option__description'],
],
'#markup' => $category_info->getDescription(),
],
],
];
if ($libraries = $category_info->getLibraries()) {
$field_type_options_radios[$id]['#attached']['library'] = $libraries;
}
}
uasort($field_type_options_radios, [SortArray::class, 'sortByWeightProperty']);
$build['add']['new_storage_type'] = $field_type_options_radios;
$build['#attached']['library'][] = 'field_ui/drupal.field_ui';
$build['#attached']['library'][] = 'field_ui/drupal.field_ui.manage_fields';
$build['#attached']['library'][] = 'core/drupal.dialog.ajax';
return $build;
}
}

View File

@ -0,0 +1,34 @@
<?php
namespace Drupal\field_ui;
use Drupal\Component\Serialization\Json;
use Drupal\Component\Utility\NestedArray;
use Drupal\Core\Menu\LocalActionDefault;
use Drupal\Core\Routing\RouteMatchInterface;
/**
* Defines a local action plugin with modal dialog.
*/
class DisplayModeLocalAction extends LocalActionDefault {
/**
* {@inheritdoc}
*/
public function getOptions(RouteMatchInterface $route_match) {
$options = parent::getOptions($route_match);
$options = NestedArray::mergeDeepArray([[
'attributes' => [
'class' => ['button', 'use-ajax'],
'data-dialog-type' => 'modal',
'data-dialog-options' => Json::encode([
'width' => '880',
]),
],
], $options,
]);
return $options;
}
}

View File

@ -0,0 +1,242 @@
<?php
namespace Drupal\field_ui\Element;
use Drupal\Component\Utility\Html;
use Drupal\Core\Render\Attribute\RenderElement;
use Drupal\Core\Render\Element;
use Drupal\Core\Render\Element\Table;
/**
* Provides a field_ui table element.
*/
#[RenderElement('field_ui_table')]
class FieldUiTable extends Table {
/**
* {@inheritdoc}
*/
public function getInfo() {
$info = parent::getInfo();
$info['#regions'] = ['' => []];
$info['#theme'] = 'field_ui_table';
// Prepend FieldUiTable's prerender callbacks.
array_unshift($info['#pre_render'], [$this, 'tablePreRender'], [$this, 'preRenderRegionRows']);
return $info;
}
/**
* Performs pre-render tasks on field_ui_table elements.
*
* @param array $elements
* A structured array containing two sub-levels of elements. Properties
* used:
* - #region_callback: A callback that provides the region of the table to
* place the row in.
* - #tabledrag: The value is a list of $options arrays that are passed to
* drupal_attach_tabledrag(). The HTML ID of the table is added to each
* $options array.
*
* @return array
* The $element with prepared variables ready for field-ui-table.html.twig.
*
* @see \Drupal\Core\Render\RendererInterface::render()
* @see \Drupal\Core\Render\Element\Table::preRenderTable()
*/
public static function tablePreRender($elements) {
$js_settings = [];
// For each region, build the tree structure from the weight and parenting
// data contained in the flat form structure, to determine row order and
// indentation.
$regions = $elements['#regions'];
$tree = ['' => ['name' => '', 'children' => []]];
$trees = array_fill_keys(array_keys($regions), $tree);
$parents = [];
$children = Element::children($elements);
$list = array_combine($children, $children);
// Iterate on rows until we can build a known tree path for all of them.
while ($list) {
foreach ($list as $name) {
$row = &$elements[$name];
$parent = $row['parent_wrapper']['parent']['#value'];
// Proceed if parent is known.
if (empty($parent) || isset($parents[$parent])) {
// Grab parent, and remove the row from the next iteration.
$parents[$name] = $parent ? array_merge($parents[$parent], [$parent]) : [];
unset($list[$name]);
// Determine the region for the row.
$region_name = call_user_func_array($row['#region_callback'], [&$row]);
// Add the element in the tree.
// phpcs:ignore DrupalPractice.CodeAnalysis.VariableAnalysis.UnusedVariable
$target = &$trees[$region_name][''];
foreach ($parents[$name] as $key) {
$target = &$target['children'][$key];
}
$target['children'][$name] = ['name' => $name, 'weight' => $row['weight']['#value']];
// Add tabledrag indentation to the first row cell.
if ($depth = count($parents[$name])) {
$children = Element::children($row);
$cell = current($children);
$indentation = [
'#theme' => 'indentation',
'#size' => $depth,
'#suffix' => $row[$cell]['#prefix'] ?? '',
];
$row[$cell]['#prefix'] = \Drupal::service('renderer')->render($indentation);
}
// Add row id and associate JS settings.
$id = Html::getClass($name);
$row['#attributes']['id'] = $id;
if (isset($row['#js_settings'])) {
$row['#js_settings'] += [
'rowHandler' => $row['#row_type'],
'name' => $name,
'region' => $region_name,
];
$js_settings[$id] = $row['#js_settings'];
}
}
}
}
// Determine rendering order from the tree structure.
foreach ($regions as $region_name => $region) {
$elements['#regions'][$region_name]['rows_order'] = array_reduce($trees[$region_name], [static::class, 'reduceOrder']);
}
$elements['#attached']['drupalSettings']['fieldUIRowsData'] = $js_settings;
// If the custom #tabledrag is set and there is an HTML ID, add the table's
// HTML ID to the options and attach the behavior.
// @see \Drupal\Core\Render\Element\Table::preRenderTable()
if (!empty($elements['#tabledrag']) && isset($elements['#attributes']['id'])) {
foreach ($elements['#tabledrag'] as $options) {
$options['table_id'] = $elements['#attributes']['id'];
drupal_attach_tabledrag($elements, $options);
}
}
return $elements;
}
/**
* Performs pre-render to move #regions to rows.
*
* @param array $elements
* A structured array containing two sub-levels of elements. Properties
* used:
* - #tabledrag: The value is a list of $options arrays that are passed to
* drupal_attach_tabledrag(). The HTML ID of the table is added to each
* $options array.
*
* @return array
* The $element with prepared variables ready for field-ui-table.html.twig.
*/
public static function preRenderRegionRows($elements) {
// Determine the colspan to use for region rows, by checking the number of
// columns in the headers.
$columns_count = 0;
foreach ($elements['#header'] as $header) {
$columns_count += (is_array($header) && isset($header['colspan']) ? $header['colspan'] : 1);
}
$rows = [];
foreach (Element::children($elements) as $key) {
$rows[$key] = $elements[$key];
unset($elements[$key]);
}
// Render rows, region by region.
foreach ($elements['#regions'] as $region_name => $region) {
$region_name_class = Html::getClass($region_name);
// Add region rows.
if (isset($region['title']) && empty($region['invisible'])) {
$elements['#rows'][] = [
'class' => [
'region-title',
'region-' . $region_name_class . '-title',
],
'no_striping' => TRUE,
'data' => [
['data' => $region['title'], 'colspan' => $columns_count],
],
];
}
if (isset($region['message'])) {
$class = (empty($region['rows_order']) ? 'region-empty' : 'region-populated');
$elements['#rows'][] = [
'class' => [
'region-message',
'region-' . $region_name_class . '-message', $class,
],
'no_striping' => TRUE,
'data' => [
['data' => $region['message'], 'colspan' => $columns_count],
],
];
}
// Add form rows, in the order determined at pre-render time.
foreach ($region['rows_order'] as $name) {
$element = $rows[$name];
$row = ['data' => []];
if (isset($element['#attributes'])) {
$row += $element['#attributes'];
}
// Render children as table cells.
foreach (Element::children($element) as $cell_key) {
$child = $element[$cell_key];
// Do not render a cell for children of #type 'value'.
if (!(isset($child['#type']) && $child['#type'] == 'value')) {
$cell = ['data' => $child];
if (isset($child['#cell_attributes'])) {
$cell += $child['#cell_attributes'];
}
$row['data'][] = $cell;
}
}
$elements['#rows'][] = $row;
}
}
return $elements;
}
/**
* Determines the rendering order of an array representing a tree.
*
* Callback for array_reduce() within ::tablePreRender().
*
* @param mixed $array
* Holds the return value of the previous iteration; in the case of the
* first iteration it instead holds the value of the initial array.
* @param mixed $a
* Holds the value of the current iteration.
*
* @return array
* Array where rendering order has been determined.
*/
public static function reduceOrder($array, $a) {
$array = $array ?: [];
if (!empty($a['name'])) {
$array[] = $a['name'];
}
if (!empty($a['children'])) {
uasort($a['children'], ['Drupal\Component\Utility\SortArray', 'sortByWeightElement']);
$array = array_merge($array, array_reduce($a['children'], [static::class, 'reduceOrder']));
}
return $array;
}
}

View File

@ -0,0 +1,189 @@
<?php
namespace Drupal\field_ui;
use Drupal\Component\Serialization\Json;
use Drupal\Component\Utility\NestedArray;
use Drupal\Core\Config\Entity\ConfigEntityListBuilder;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Url;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Defines a class to build a listing of view mode entities.
*
* @see \Drupal\Core\Entity\Entity\EntityViewMode
*/
class EntityDisplayModeListBuilder extends ConfigEntityListBuilder {
/**
* All entity types.
*
* @var \Drupal\Core\Entity\EntityTypeInterface[]
*/
protected $entityTypes;
/**
* Constructs a new EntityDisplayModeListBuilder object.
*
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
* The entity type definition.
* @param \Drupal\Core\Entity\EntityStorageInterface $storage
* The entity storage class.
* @param \Drupal\Core\Entity\EntityTypeInterface[] $entity_types
* List of all entity types.
*/
public function __construct(EntityTypeInterface $entity_type, EntityStorageInterface $storage, array $entity_types) {
parent::__construct($entity_type, $storage);
// Override the default limit (50) in order to display all view modes.
$this->limit = FALSE;
$this->entityTypes = $entity_types;
}
/**
* {@inheritdoc}
*/
public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) {
$entity_type_manager = $container->get('entity_type.manager');
return new static(
$entity_type,
$entity_type_manager->getStorage($entity_type->id()),
$entity_type_manager->getDefinitions()
);
}
/**
* {@inheritdoc}
*/
public function buildHeader() {
$header['label'] = $this->t('Name');
$header['description'] = $this->t('Description');
return $header + parent::buildHeader();
}
/**
* {@inheritdoc}
*/
public function buildRow(EntityInterface $entity) {
$row['label'] = $entity->label();
$row['description'] = $entity->getDescription();
return $row + parent::buildRow($entity);
}
/**
* {@inheritdoc}
*/
public function getOperations(EntityInterface $entity) {
// Make the edit form render in a dialog, like the add form.
// The edit form also contains an option to delete the view mode, which
// also spawns a dialog. Rather than have nested dialogs, we allow the
// existing dialog to be replaced, so users will be shown the list again
// if they cancel deleting the view mode.
$operations = parent::getOperations($entity);
if (isset($operations['edit'])) {
$operations['edit'] = NestedArray::mergeDeepArray([[
'attributes' => [
'class' => ['button', 'use-ajax'],
'data-dialog-type' => 'modal',
'data-dialog-options' => Json::encode([
'width' => '880',
]),
],
], $operations['edit'],
]);
}
return $operations;
}
/**
* {@inheritdoc}
*/
public function load() {
$entities = [];
foreach (parent::load() as $entity) {
$entities[$entity->getTargetType()][] = $entity;
}
return $entities;
}
/**
* {@inheritdoc}
*/
public function render() {
$build = [];
foreach ($this->load() as $entity_type => $entities) {
if (!isset($this->entityTypes[$entity_type])) {
continue;
}
// Filter entities.
if (!$this->isValidEntity($entity_type)) {
continue;
}
$table = [
'#prefix' => '<h2>' . $this->entityTypes[$entity_type]->getLabel() . '</h2>',
'#type' => 'table',
'#header' => $this->buildHeader(),
'#rows' => [],
'#attributes' => [
'class' => ['display-mode-table'],
],
];
foreach ($entities as $entity) {
if ($row = $this->buildRow($entity)) {
$table['#rows'][$entity->id()] = $row;
}
}
// Move content at the top.
if ($entity_type == 'node') {
$table['#weight'] = -10;
}
$short_type = str_replace(['entity_', '_mode'], '', $this->entityTypeId);
$table['#rows']['_add_new'][] = [
'data' => [
'#type' => 'link',
'#url' => Url::fromRoute($short_type == 'view' ? 'entity.entity_view_mode.add_form' : 'entity.entity_form_mode.add_form', ['entity_type_id' => $entity_type]),
'#title' => $this->t('Add %label for @entity-type', ['@entity-type' => $this->entityTypes[$entity_type]->getLabel(), '%label' => $this->entityType->getSingularLabel()]),
'#button_type' => 'primary',
'#attributes' => [
'class' => ['button', 'use-ajax', 'button--small'],
'data-dialog-type' => 'modal',
'data-dialog-options' => Json::encode([
'width' => '880',
]),
],
'#attached' => [
'library' => [
'core/drupal.dialog.ajax',
'field_ui/drupal.field_ui_table',
],
],
],
'colspan' => count($table['#header']),
];
$build[$entity_type] = $table;
}
return $build;
}
/**
* Filters entities based on their view builder handlers.
*
* @param string $entity_type
* The entity type of the entity that needs to be validated.
*
* @return bool
* TRUE if the entity has the correct view builder handler, FALSE if the
* entity doesn't have the correct view builder handler.
*/
protected function isValidEntity($entity_type) {
return $this->entityTypes[$entity_type]->get('field_ui_base_route') && $this->entityTypes[$entity_type]->hasViewBuilderClass();
}
}

View File

@ -0,0 +1,25 @@
<?php
namespace Drupal\field_ui;
/**
* Defines a class to build a listing of form mode entities.
*
* @see \Drupal\Core\Entity\Entity\EntityFormMode
*/
class EntityFormModeListBuilder extends EntityDisplayModeListBuilder {
/**
* Filters entities based on their form mode handlers.
*
* @param string $entity_type
* The entity type of the entity that needs to be validated.
*
* @return bool
* TRUE if the entity has any forms, FALSE otherwise.
*/
protected function isValidEntity($entity_type) {
return $this->entityTypes[$entity_type]->hasFormClasses();
}
}

View File

@ -0,0 +1,215 @@
<?php
namespace Drupal\field_ui;
use Drupal\Component\Serialization\Json;
use Drupal\Component\Utility\Html;
use Drupal\Core\Config\Entity\ConfigEntityListBuilder;
use Drupal\Core\Entity\EntityFieldManagerInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Field\FieldTypePluginManagerInterface;
use Drupal\field\FieldConfigInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides lists of field config entities.
*/
class FieldConfigListBuilder extends ConfigEntityListBuilder {
/**
* The name of the entity type the listed fields are attached to.
*
* @var string
*/
protected $targetEntityTypeId;
/**
* The name of the bundle the listed fields are attached to.
*
* @var string
*/
protected $targetBundle;
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* The field type plugin manager.
*
* @var \Drupal\Core\Field\FieldTypePluginManagerInterface
*/
protected $fieldTypeManager;
/**
* The entity field manager.
*
* @var \Drupal\Core\Entity\EntityFieldManagerInterface
*/
protected $entityFieldManager;
/**
* Constructs a new class instance.
*
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
* The entity type definition.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
* @param \Drupal\Core\Field\FieldTypePluginManagerInterface $field_type_manager
* The field type manager.
* @param \Drupal\Core\Entity\EntityFieldManagerInterface|null $entity_field_manager
* The entity field manager.
*/
public function __construct(EntityTypeInterface $entity_type, EntityTypeManagerInterface $entity_type_manager, FieldTypePluginManagerInterface $field_type_manager, EntityFieldManagerInterface $entity_field_manager) {
parent::__construct($entity_type, $entity_type_manager->getStorage($entity_type->id()));
$this->entityTypeManager = $entity_type_manager;
$this->fieldTypeManager = $field_type_manager;
$this->entityFieldManager = $entity_field_manager;
}
/**
* {@inheritdoc}
*/
public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) {
return new static(
$entity_type,
$container->get('entity_type.manager'),
$container->get('plugin.manager.field.field_type'),
$container->get('entity_field.manager')
);
}
/**
* {@inheritdoc}
*/
public function render($target_entity_type_id = NULL, $target_bundle = NULL) {
$this->targetEntityTypeId = $target_entity_type_id;
$this->targetBundle = $target_bundle;
$build = parent::render();
$build['table']['#attributes']['id'] = 'field-overview';
$build['table']['#empty'] = $this->t('No fields are present yet.');
$build['#attached']['library'][] = 'field_ui/drupal.field_ui';
return $build;
}
/**
* {@inheritdoc}
*/
public function load() {
$entities = array_filter($this->entityFieldManager->getFieldDefinitions($this->targetEntityTypeId, $this->targetBundle), function ($field_definition) {
return $field_definition instanceof FieldConfigInterface;
});
// Sort the entities using the entity class's sort() method.
// See \Drupal\Core\Config\Entity\ConfigEntityBase::sort().
uasort($entities, [$this->entityType->getClass(), 'sort']);
return $entities;
}
/**
* {@inheritdoc}
*/
public function buildHeader() {
$header = [
'label' => $this->t('Label'),
'field_name' => [
'data' => $this->t('Machine name'),
'class' => [RESPONSIVE_PRIORITY_MEDIUM],
],
'settings_summary' => $this->t('Field type'),
];
return $header + parent::buildHeader();
}
/**
* {@inheritdoc}
*/
public function buildRow(EntityInterface $field_config) {
/** @var \Drupal\field\FieldConfigInterface $field_config */
$field_storage = $field_config->getFieldStorageDefinition();
$storage_summary = $this->fieldTypeManager->getStorageSettingsSummary($field_storage);
$instance_summary = $this->fieldTypeManager->getFieldSettingsSummary($field_config);
$summary_list = [...$storage_summary, ...$instance_summary];
$settings_summary = [
'data' => [
'#theme' => 'item_list',
'#items' => [
$this->fieldTypeManager->getDefinitions()[$field_storage->getType()]['label'],
...$summary_list,
],
],
'class' => ['field-settings-summary-cell'],
];
$row = [
'id' => Html::getClass($field_config->getName()),
'data' => [
'label' => $field_config->getLabel(),
'field_name' => $field_config->getName(),
'settings_summary' => $settings_summary,
],
];
// Add the operations.
$row['data'] = $row['data'] + parent::buildRow($field_config);
if ($field_storage->isLocked()) {
$row['data']['operations'] = ['data' => ['#markup' => $this->t('Locked')]];
$row['class'][] = 'menu-disabled';
}
return $row;
}
/**
* {@inheritdoc}
*/
public function getDefaultOperations(EntityInterface $entity) {
/** @var \Drupal\field\FieldConfigInterface $entity */
$operations = parent::getDefaultOperations($entity);
if ($entity->access('update') && $entity->hasLinkTemplate("{$entity->getTargetEntityTypeId()}-field-edit-form")) {
$operations['edit'] = [
'title' => $this->t('Edit'),
'weight' => 10,
'url' => $entity->toUrl("{$entity->getTargetEntityTypeId()}-field-edit-form"),
'attributes' => [
'title' => $this->t('Edit field settings.'),
'class' => ['use-ajax'],
'data-dialog-type' => 'modal',
'data-dialog-options' => Json::encode([
'width' => 1100,
]),
],
];
}
if ($entity->access('delete') && $entity->hasLinkTemplate("{$entity->getTargetEntityTypeId()}-field-delete-form")) {
$operations['delete'] = [
'title' => $this->t('Delete'),
'weight' => 100,
'url' => $entity->toUrl("{$entity->getTargetEntityTypeId()}-field-delete-form"),
'attributes' => [
'title' => $this->t('Delete field.'),
'class' => ['use-ajax'],
'data-dialog-type' => 'modal',
'data-dialog-options' => Json::encode([
'width' => 880,
]),
],
];
}
return $operations;
}
}

View File

@ -0,0 +1,152 @@
<?php
namespace Drupal\field_ui;
use Drupal\Core\Config\Entity\ConfigEntityListBuilder;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Entity\EntityTypeBundleInfoInterface;
use Drupal\Core\Field\FieldTypePluginManagerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Drupal\Core\Link;
/**
* Defines a class to build a listing of fields.
*
* @see \Drupal\field\Entity\FieldStorageConfig
* @see field_ui_entity_type_build()
*/
class FieldStorageConfigListBuilder extends ConfigEntityListBuilder {
/**
* An array of information about field types.
*
* @var array
*/
protected $fieldTypes;
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* An array of entity bundle information.
*
* @var array
*/
protected $bundles;
/**
* The field type manager.
*
* @var \Drupal\Core\Field\FieldTypePluginManagerInterface
*/
protected $fieldTypeManager;
/**
* Constructs a new FieldStorageConfigListBuilder object.
*
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
* The entity type definition.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
* @param \Drupal\Core\Field\FieldTypePluginManagerInterface $field_type_manager
* The 'field type' plugin manager.
* @param \Drupal\Core\Entity\EntityTypeBundleInfoInterface $bundle_info_service
* The bundle info service.
*/
public function __construct(EntityTypeInterface $entity_type, EntityTypeManagerInterface $entity_type_manager, FieldTypePluginManagerInterface $field_type_manager, EntityTypeBundleInfoInterface $bundle_info_service) {
parent::__construct($entity_type, $entity_type_manager->getStorage($entity_type->id()));
$this->entityTypeManager = $entity_type_manager;
$this->bundles = $bundle_info_service->getAllBundleInfo();
$this->fieldTypeManager = $field_type_manager;
$this->fieldTypes = $this->fieldTypeManager->getDefinitions();
$this->limit = FALSE;
}
/**
* {@inheritdoc}
*/
public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) {
return new static(
$entity_type,
$container->get('entity_type.manager'),
$container->get('plugin.manager.field.field_type'),
$container->get('entity_type.bundle.info')
);
}
/**
* {@inheritdoc}
*/
public function render() {
$build = parent::render();
$build['#attached']['library'][] = 'field_ui/drupal.field_ui';
return $build;
}
/**
* {@inheritdoc}
*/
public function buildHeader() {
$header['id'] = $this->t('Field name');
$header['entity_type'] = $this->t('Entity type');
$header['type'] = [
'data' => $this->t('Field type'),
'class' => [RESPONSIVE_PRIORITY_MEDIUM],
];
$header['usage'] = $this->t('Used in');
$header['settings_summary'] = $this->t('Summary');
return $header;
}
/**
* {@inheritdoc}
*/
public function buildRow(EntityInterface $field_storage) {
if ($field_storage->isLocked()) {
$row['class'] = ['menu-disabled'];
$row['data']['id'] = $this->t('@field_name (Locked)', ['@field_name' => $field_storage->getName()]);
}
else {
$row['data']['id'] = $field_storage->getName();
}
$entity_type_id = $field_storage->getTargetEntityTypeId();
// Adding the entity type.
$row['data']['entity_type'] = $entity_type_id;
$field_type = $this->fieldTypes[$field_storage->getType()];
$row['data']['type'] = $this->t('@type (module: @module)', ['@type' => $field_type['label'], '@module' => $field_type['provider']]);
$usage = [];
foreach ($field_storage->getBundles() as $bundle) {
if ($route_info = FieldUI::getOverviewRouteInfo($entity_type_id, $bundle)) {
$usage[] = Link::fromTextAndUrl($this->bundles[$entity_type_id][$bundle]['label'], $route_info)->toRenderable();
}
else {
$usage[] = $this->bundles[$entity_type_id][$bundle]['label'];
}
}
$row['data']['usage']['data'] = [
'#theme' => 'item_list',
'#items' => $usage,
'#context' => ['list_style' => 'comma-list'],
];
$summary = $this->fieldTypeManager->getStorageSettingsSummary($field_storage);
$row['data']['settings_summary'] = empty($summary) ? '' : [
'data' => [
'#theme' => 'item_list',
'#items' => $summary,
],
'class' => ['storage-settings-summary-cell'],
];
return $row;
}
}

View File

@ -0,0 +1,83 @@
<?php
namespace Drupal\field_ui;
use Drupal\Component\Utility\UrlHelper;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Url;
/**
* Static service container wrapper for Field UI.
*/
class FieldUI {
/**
* Returns the route info for the field overview of a given entity bundle.
*
* @param string $entity_type_id
* An entity type.
* @param string $bundle
* The entity bundle.
*
* @return \Drupal\Core\Url
* A URL object.
*/
public static function getOverviewRouteInfo($entity_type_id, $bundle) {
$entity_type = \Drupal::entityTypeManager()->getDefinition($entity_type_id);
if ($entity_type->get('field_ui_base_route')) {
return new Url("entity.{$entity_type_id}.field_ui_fields", static::getRouteBundleParameter($entity_type, $bundle));
}
}
/**
* Returns the next redirect path in a multi-page sequence.
*
* @param array $destinations
* An array of destinations to redirect to.
*
* @return \Drupal\Core\Url|null
* The next destination to redirect to.
*/
public static function getNextDestination(array $destinations) {
// If there are no valid destinations left, return here.
if (empty($destinations)) {
return NULL;
}
$next_destination = array_shift($destinations);
if (is_array($next_destination)) {
$next_destination['options']['query']['destinations'] = $destinations;
$next_destination += [
'route_parameters' => [],
];
$next_destination = Url::fromRoute($next_destination['route_name'], $next_destination['route_parameters'], $next_destination['options']);
}
else {
$options = UrlHelper::parse($next_destination);
if ($destinations) {
$options['query']['destinations'] = $destinations;
}
// Redirect to any given path within the same domain.
// @todo Revisit this in https://www.drupal.org/node/2418219.
$next_destination = Url::fromUserInput('/' . $options['path'], $options);
}
return $next_destination;
}
/**
* Gets the route parameter that should be used for Field UI routes.
*
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
* The actual entity type, not the bundle (e.g. the content entity type).
* @param string $bundle
* The bundle name.
*
* @return array
* An array that can be used a route parameter.
*/
public static function getRouteBundleParameter(EntityTypeInterface $entity_type, $bundle) {
$bundle_parameter_key = $entity_type->getBundleEntityType() ?: 'bundle';
return [$bundle_parameter_key => $bundle];
}
}

View File

@ -0,0 +1,75 @@
<?php
namespace Drupal\field_ui;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides dynamic permissions of the field_ui module.
*/
class FieldUiPermissions implements ContainerInjectionInterface {
use StringTranslationTrait;
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* Constructs a new FieldUiPermissions instance.
*
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
*/
public function __construct(EntityTypeManagerInterface $entity_type_manager) {
$this->entityTypeManager = $entity_type_manager;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static($container->get('entity_type.manager'));
}
/**
* Returns an array of field UI permissions.
*
* @return array
* An array of field UI permissions keyed by permission name.
*/
public function fieldPermissions() {
$permissions = [];
foreach ($this->entityTypeManager->getDefinitions() as $entity_type_id => $entity_type) {
if ($entity_type->get('field_ui_base_route')) {
// The permissions depend on the module that provides the entity.
$dependencies = ['module' => [$entity_type->getProvider()]];
// Create a permission for each fieldable entity to manage
// the fields and the display.
$permissions['administer ' . $entity_type_id . ' fields'] = [
'title' => $this->t('%entity_label: Administer fields', ['%entity_label' => $entity_type->getLabel()]),
'restrict access' => TRUE,
'dependencies' => $dependencies,
];
$permissions['administer ' . $entity_type_id . ' form display'] = [
'title' => $this->t('%entity_label: Administer form display', ['%entity_label' => $entity_type->getLabel()]),
'dependencies' => $dependencies,
];
$permissions['administer ' . $entity_type_id . ' display'] = [
'title' => $this->t('%entity_label: Administer display', ['%entity_label' => $entity_type->getLabel()]),
'dependencies' => $dependencies,
];
}
}
return $permissions;
}
}

View File

@ -0,0 +1,947 @@
<?php
namespace Drupal\field_ui\Form;
use Drupal\Component\Plugin\Factory\DefaultFactory;
use Drupal\Component\Plugin\PluginManagerBase;
use Drupal\Component\Utility\Html;
use Drupal\Core\Ajax\AjaxResponse;
use Drupal\Core\Ajax\ReplaceCommand;
use Drupal\Core\Ajax\TabledragWarningCommand;
use Drupal\Core\Entity\EntityFieldManagerInterface;
use Drupal\Core\Entity\EntityForm;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityDisplayRepositoryInterface;
use Drupal\Core\Entity\EntityWithPluginCollectionInterface;
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\Field\FieldTypePluginManagerInterface;
use Drupal\Core\Field\PluginSettingsInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\field_ui\FieldUI;
/**
* Base class for EntityDisplay edit forms.
*/
abstract class EntityDisplayFormBase extends EntityForm {
/**
* The display context. Either 'view' or 'form'.
*
* @var string
*/
protected $displayContext;
/**
* The widget or formatter plugin manager.
*
* @var \Drupal\Component\Plugin\PluginManagerBase
*/
protected $pluginManager;
/**
* The entity display repository.
*
* @var \Drupal\Core\Entity\EntityDisplayRepositoryInterface
*/
protected $entityDisplayRepository;
/**
* The entity field manager.
*
* @var \Drupal\Core\Entity\EntityFieldManagerInterface
*/
protected $entityFieldManager;
/**
* A list of field types.
*
* @var array
*/
protected $fieldTypes;
/**
* The entity being used by this form.
*
* @var \Drupal\Core\Entity\Display\EntityDisplayInterface
*/
protected $entity;
/**
* Constructs a new EntityDisplayFormBase.
*
* @param \Drupal\Core\Field\FieldTypePluginManagerInterface $field_type_manager
* The field type manager.
* @param \Drupal\Component\Plugin\PluginManagerBase $plugin_manager
* The widget or formatter plugin manager.
* @param \Drupal\Core\Entity\EntityDisplayRepositoryInterface|null $entity_display_repository
* (optional) The entity display_repository.
* @param \Drupal\Core\Entity\EntityFieldManagerInterface|null $entity_field_manager
* (optional) The entity field manager.
*/
public function __construct(FieldTypePluginManagerInterface $field_type_manager, PluginManagerBase $plugin_manager, EntityDisplayRepositoryInterface $entity_display_repository, EntityFieldManagerInterface $entity_field_manager) {
$this->fieldTypes = $field_type_manager->getDefinitions();
$this->pluginManager = $plugin_manager;
$this->entityDisplayRepository = $entity_display_repository;
$this->entityFieldManager = $entity_field_manager;
}
/**
* {@inheritdoc}
*/
public function getEntityFromRouteMatch(RouteMatchInterface $route_match, $entity_type_id) {
$route_parameters = $route_match->getParameters()->all();
return $this->getEntityDisplay($route_parameters['entity_type_id'], $route_parameters['bundle'], $route_parameters[$this->displayContext . '_mode_name']);
}
/**
* Get the regions needed to create the overview form.
*
* @return array
* Example usage:
* @code
* return [
* 'content' => [
* // label for the region.
* 'title' => $this->t('Content'),
* // Indicates if the region is visible in the UI.
* 'invisible' => TRUE,
* // A message to indicate that there is nothing to be displayed in
* // the region.
* 'message' => $this->t('No field is displayed.'),
* ],
* ];
* @endcode
*/
public function getRegions() {
return [
'content' => [
'title' => $this->t('Content'),
'invisible' => TRUE,
'message' => $this->t('No field is displayed.'),
],
'hidden' => [
'title' => $this->t('Disabled', [], ['context' => 'Plural']),
'message' => $this->t('No field is hidden.'),
],
];
}
/**
* Returns an associative array of all regions.
*
* @return array
* An array containing the region options.
*/
public function getRegionOptions() {
$options = [];
foreach ($this->getRegions() as $region => $data) {
$options[$region] = $data['title'];
}
return $options;
}
/**
* Collects the definitions of fields whose display is configurable.
*
* @return \Drupal\Core\Field\FieldDefinitionInterface[]
* The array of field definitions
*/
protected function getFieldDefinitions() {
$context = $this->displayContext;
return array_filter($this->entityFieldManager->getFieldDefinitions($this->entity->getTargetEntityTypeId(), $this->entity->getTargetBundle()), function (FieldDefinitionInterface $field_definition) use ($context) {
return $field_definition->isDisplayConfigurable($context);
});
}
/**
* {@inheritdoc}
*/
public function form(array $form, FormStateInterface $form_state) {
$form = parent::form($form, $form_state);
$field_definitions = $this->getFieldDefinitions();
$extra_fields = $this->getExtraFields();
$form += [
'#entity_type' => $this->entity->getTargetEntityTypeId(),
'#bundle' => $this->entity->getTargetBundle(),
'#fields' => array_keys($field_definitions),
'#extra' => array_keys($extra_fields),
];
if (empty($field_definitions) && empty($extra_fields) && $route_info = FieldUI::getOverviewRouteInfo($this->entity->getTargetEntityTypeId(), $this->entity->getTargetBundle())) {
$this->messenger()->addWarning($this->t('There are no fields yet added. You can add new fields on the <a href=":link">Manage fields</a> page.', [':link' => $route_info->toString()]));
return $form;
}
$table = [
'#type' => 'field_ui_table',
'#header' => $this->getTableHeader(),
'#regions' => $this->getRegions(),
'#attributes' => [
'class' => ['field-ui-overview'],
'id' => 'field-display-overview',
],
'#tabledrag' => [
[
'action' => 'order',
'relationship' => 'sibling',
'group' => 'field-weight',
],
[
'action' => 'match',
'relationship' => 'parent',
'group' => 'field-parent',
'subgroup' => 'field-parent',
'source' => 'field-name',
],
[
'action' => 'match',
'relationship' => 'parent',
'group' => 'field-region',
'subgroup' => 'field-region',
'source' => 'field-name',
],
],
];
// Field rows.
foreach ($field_definitions as $field_name => $field_definition) {
$table[$field_name] = $this->buildFieldRow($field_definition, $form, $form_state);
}
// Non-field elements.
foreach ($extra_fields as $field_id => $extra_field) {
$table[$field_id] = $this->buildExtraFieldRow($field_id, $extra_field);
}
$form['fields'] = $table;
// Custom display settings.
if ($this->entity->getMode() == 'default') {
// Only show the settings if there is at least one custom display mode.
$display_mode_options = $this->getDisplayModeOptions();
// Unset default option.
unset($display_mode_options['default']);
if ($display_mode_options) {
$form['modes'] = [
'#type' => 'details',
'#title' => $this->t('Custom display settings'),
];
// Prepare default values for the 'Custom display settings' checkboxes.
$default = [];
if ($enabled_displays = array_filter($this->getDisplayStatuses())) {
$default = array_keys(array_intersect_key($display_mode_options, $enabled_displays));
}
natcasesort($display_mode_options);
$form['modes']['display_modes_custom'] = [
'#type' => 'checkboxes',
'#title' => $this->t('Use custom display settings for the following @display_context modes', ['@display_context' => $this->displayContext]),
'#options' => $display_mode_options,
'#default_value' => $default,
];
// Provide link to manage display modes.
$form['modes']['display_modes_link'] = $this->getDisplayModesLink();
}
}
// In overviews involving nested rows from contributed modules (i.e
// field_group), the 'plugin type' selects can trigger a series of changes
// in child rows. The #ajax behavior is therefore not attached directly to
// the selects, but triggered by the client-side script through a hidden
// #ajax 'Refresh' button. A hidden 'refresh_rows' input tracks the name of
// affected rows.
$form['refresh_rows'] = ['#type' => 'hidden'];
$form['refresh'] = [
'#type' => 'submit',
'#value' => $this->t('Refresh'),
'#op' => 'refresh_table',
'#submit' => ['::multistepSubmit'],
'#ajax' => [
'callback' => '::multistepAjax',
'wrapper' => 'field-display-overview-wrapper',
'effect' => 'fade',
// The button stays hidden, so we hide the Ajax spinner too. Ad-hoc
// spinners will be added manually by the client-side script.
'progress' => 'none',
],
'#attributes' => [
'class' => ['visually-hidden'],
// Ensure the button is not focusable via keyboard navigation.
'tabindex' => '-1',
],
];
$form['actions'] = ['#type' => 'actions'];
$form['actions']['submit'] = [
'#type' => 'submit',
'#button_type' => 'primary',
'#value' => $this->t('Save'),
];
$form['#attached']['library'][] = 'field_ui/drupal.field_ui';
return $form;
}
/**
* Builds the table row structure for a single field.
*
* @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition
* The field definition.
* @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.
*
* @return array
* A table row array.
*/
protected function buildFieldRow(FieldDefinitionInterface $field_definition, array $form, FormStateInterface $form_state) {
$field_name = $field_definition->getName();
$display_options = $this->entity->getComponent($field_name);
$label = $field_definition->getLabel();
// Disable fields without any applicable plugins.
if (empty($this->getApplicablePluginOptions($field_definition))) {
$this->entity->removeComponent($field_name);
$display_options = $this->entity->getComponent($field_name);
}
$regions = array_keys($this->getRegions());
$field_row = [
'#attributes' => ['class' => ['draggable', 'tabledrag-leaf']],
'#row_type' => 'field',
'#region_callback' => [$this, 'getRowRegion'],
'#js_settings' => [
'rowHandler' => 'field',
'defaultPlugin' => $this->getDefaultPlugin($field_definition->getType()),
],
'human_name' => [
'#plain_text' => $label,
],
'machine_name' => [
'#markup' => $field_name,
'#attributes' => ['class' => ['machine-name']],
],
'weight' => [
'#type' => 'textfield',
'#title' => $this->t('Weight for @title', ['@title' => $label]),
'#title_display' => 'invisible',
'#default_value' => $display_options ? $display_options['weight'] : '0',
'#size' => 3,
'#attributes' => ['class' => ['field-weight']],
],
'parent_wrapper' => [
'parent' => [
'#type' => 'select',
'#title' => $this->t('Label display for @title', ['@title' => $label]),
'#title_display' => 'invisible',
'#options' => array_combine($regions, $regions),
'#empty_value' => '',
'#attributes' => ['class' => ['js-field-parent', 'field-parent']],
'#parents' => ['fields', $field_name, 'parent'],
],
'hidden_name' => [
'#type' => 'hidden',
'#default_value' => $field_name,
'#attributes' => ['class' => ['field-name']],
],
],
'region' => [
'#type' => 'select',
'#title' => $this->t('Region for @title', ['@title' => $label]),
'#title_display' => 'invisible',
'#options' => $this->getRegionOptions(),
'#default_value' => $display_options ? $display_options['region'] : 'hidden',
'#attributes' => ['class' => ['field-region']],
],
];
$field_row['plugin'] = [
'type' => [
'#type' => 'select',
'#title' => $this->t('Plugin for @title', ['@title' => $label]),
'#title_display' => 'invisible',
'#options' => $this->getApplicablePluginOptions($field_definition),
'#default_value' => $display_options ? $display_options['type'] : 'hidden',
'#parents' => ['fields', $field_name, 'type'],
'#attributes' => ['class' => ['field-plugin-type']],
],
'settings_edit_form' => [],
];
// Get the corresponding plugin object.
$plugin = $this->entity->getRenderer($field_name);
// Base button element for the various plugin settings actions.
$base_button = [
'#submit' => ['::multistepSubmit'],
'#ajax' => [
'callback' => '::multistepAjax',
'wrapper' => 'field-display-overview-wrapper',
'effect' => 'fade',
],
'#field_name' => $field_name,
];
if ($form_state->get('plugin_settings_edit') == $field_name) {
// We are currently editing this field's plugin settings. Display the
// settings form and submit buttons.
$field_row['plugin']['settings_edit_form'] = [];
if ($plugin) {
// Generate the settings form and allow other modules to alter it.
$settings_form = $plugin->settingsForm($form, $form_state);
$third_party_settings_form = $this->thirdPartySettingsForm($plugin, $field_definition, $form, $form_state);
if ($settings_form || $third_party_settings_form) {
$field_row['plugin']['#cell_attributes'] = ['colspan' => 3];
$field_row['plugin']['settings_edit_form'] = [
'#type' => 'container',
'#attributes' => ['class' => ['field-plugin-settings-edit-form']],
'#parents' => ['fields', $field_name, 'settings_edit_form'],
'label' => [
'#markup' => $this->t('Plugin settings'),
],
'settings' => $settings_form,
'third_party_settings' => $third_party_settings_form,
'actions' => [
'#type' => 'actions',
'save_settings' => $base_button + [
'#type' => 'submit',
'#button_type' => 'primary',
'#name' => $field_name . '_plugin_settings_update',
'#value' => $this->t('Update'),
'#op' => 'update',
],
'cancel_settings' => $base_button + [
'#type' => 'submit',
'#name' => $field_name . '_plugin_settings_cancel',
'#value' => $this->t('Cancel'),
'#op' => 'cancel',
// Do not check errors for the 'Cancel' button, but make sure we
// get the value of the 'plugin type' select.
'#limit_validation_errors' => [['fields', $field_name, 'type']],
],
],
];
$field_row['#attributes']['class'][] = 'field-plugin-settings-editing';
}
}
}
else {
$field_row['settings_summary'] = [];
$field_row['settings_edit'] = [];
if ($plugin) {
// Display a summary of the current plugin settings, and (if the
// summary is not empty) a button to edit them.
$summary = $plugin->settingsSummary();
// Allow other modules to alter the summary.
$this->alterSettingsSummary($summary, $plugin, $field_definition);
if (!empty($summary)) {
$field_row['settings_summary'] = [
'#type' => 'inline_template',
'#template' => '<div class="field-plugin-summary">{{ summary|safe_join("<br />") }}</div>',
'#context' => ['summary' => $summary],
'#cell_attributes' => ['class' => ['field-plugin-summary-cell']],
];
}
// Check selected plugin settings to display edit link or not.
$settings_form = $plugin->settingsForm($form, $form_state);
$third_party_settings_form = $this->thirdPartySettingsForm($plugin, $field_definition, $form, $form_state);
if (!empty($settings_form) || !empty($third_party_settings_form)) {
$field_row['settings_edit'] = $base_button + [
'#type' => 'image_button',
'#name' => $field_name . '_settings_edit',
'#src' => 'core/misc/icons/787878/cog.svg',
'#attributes' => ['class' => ['field-plugin-settings-edit'], 'alt' => $this->t('Edit')],
'#op' => 'edit',
// Do not check errors for the 'Edit' button, but make sure we get
// the value of the 'plugin type' select.
'#limit_validation_errors' => [['fields', $field_name, 'type']],
'#prefix' => '<div class="field-plugin-settings-edit-wrapper">',
'#suffix' => '</div>',
];
}
}
}
return $field_row;
}
/**
* Builds the table row structure for a single extra field.
*
* @param string $field_id
* The field ID.
* @param array $extra_field
* The pseudo-field element.
*
* @return array
* A table row array.
*/
protected function buildExtraFieldRow($field_id, $extra_field) {
$display_options = $this->entity->getComponent($field_id);
$regions = array_keys($this->getRegions());
$extra_field_row = [
'#attributes' => ['class' => ['draggable', 'tabledrag-leaf']],
'#row_type' => 'extra_field',
'#region_callback' => [$this, 'getRowRegion'],
'#js_settings' => ['rowHandler' => 'field'],
'human_name' => [
'#markup' => $extra_field['label'],
],
'machine_name' => [
'#markup' => $field_id,
'#attributes' => ['class' => ['machine-name']],
],
'weight' => [
'#type' => 'textfield',
'#title' => $this->t('Weight for @title', ['@title' => $extra_field['label']]),
'#title_display' => 'invisible',
'#default_value' => $display_options ? $display_options['weight'] : 0,
'#size' => 3,
'#attributes' => ['class' => ['field-weight']],
],
'parent_wrapper' => [
'parent' => [
'#type' => 'select',
'#title' => $this->t('Parents for @title', ['@title' => $extra_field['label']]),
'#title_display' => 'invisible',
'#options' => array_combine($regions, $regions),
'#empty_value' => '',
'#attributes' => ['class' => ['js-field-parent', 'field-parent']],
'#parents' => ['fields', $field_id, 'parent'],
],
'hidden_name' => [
'#type' => 'hidden',
'#default_value' => $field_id,
'#attributes' => ['class' => ['field-name']],
],
],
'region' => [
'#type' => 'select',
'#title' => $this->t('Region for @title', ['@title' => $extra_field['label']]),
'#title_display' => 'invisible',
'#options' => $this->getRegionOptions(),
'#default_value' => $display_options ? $display_options['region'] : 'hidden',
'#attributes' => ['class' => ['field-region']],
],
'plugin' => [
'type' => [
'#type' => 'hidden',
'#value' => $display_options ? 'visible' : 'hidden',
'#parents' => ['fields', $field_id, 'type'],
'#attributes' => ['class' => ['field-plugin-type']],
],
],
'settings_summary' => [],
'settings_edit' => [],
];
return $extra_field_row;
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
// If the main "Save" button was submitted while a field settings subform
// was being edited, update the new incoming settings when rebuilding the
// entity, just as if the subform's "Update" button had been submitted.
if ($edit_field = $form_state->get('plugin_settings_edit')) {
$form_state->set('plugin_settings_update', $edit_field);
}
parent::submitForm($form, $form_state);
$form_values = $form_state->getValues();
// Handle the 'display modes' checkboxes if present.
if ($this->entity->getMode() == 'default' && !empty($form_values['display_modes_custom'])) {
$display_modes = $this->getDisplayModes();
$current_statuses = $this->getDisplayStatuses();
$statuses = [];
foreach ($form_values['display_modes_custom'] as $mode => $value) {
if (!empty($value) && empty($current_statuses[$mode])) {
// If no display exists for the newly enabled view mode, initialize
// it with those from the 'default' view mode, which were used so
// far.
if (!$this->entityTypeManager->getStorage($this->entity->getEntityTypeId())->load($this->entity->getTargetEntityTypeId() . '.' . $this->entity->getTargetBundle() . '.' . $mode)) {
$display = $this->getEntityDisplay($this->entity->getTargetEntityTypeId(), $this->entity->getTargetBundle(), 'default')->createCopy($mode);
$display->save();
}
$display_mode_label = $display_modes[$mode]['label'];
$url = $this->getOverviewUrl($mode);
$this->messenger()->addStatus($this->t('The %display_mode mode now uses custom display settings. You might want to <a href=":url">configure them</a>.', ['%display_mode' => $display_mode_label, ':url' => $url->toString()]));
}
$statuses[$mode] = !empty($value);
}
$this->saveDisplayStatuses($statuses);
}
// The saved message may not be needed in some cases. An example of
// this is in LayoutBuilderEntityViewDisplayForm which can redirect
// the user to a confirmation form before the settings are saved.
if (!$form_state->getRedirect()) {
$this->messenger()->addStatus($this->t('Your settings have been saved.'));
}
}
/**
* {@inheritdoc}
*/
protected function copyFormValuesToEntity(EntityInterface $entity, array $form, FormStateInterface $form_state) {
$form_values = $form_state->getValues();
if ($this->entity instanceof EntityWithPluginCollectionInterface) {
// Do not manually update values represented by plugin collections.
$form_values = array_diff_key($form_values, $this->entity->getPluginCollections());
}
// Collect data for 'regular' fields.
foreach ($form['#fields'] as $field_name) {
$values = $form_values['fields'][$field_name];
if ($values['region'] == 'hidden') {
$entity->removeComponent($field_name);
}
else {
$options = $entity->getComponent($field_name);
// Update field settings only if the submit handler told us to.
if ($form_state->get('plugin_settings_update') === $field_name) {
// Only store settings actually used by the selected plugin.
$default_settings = $this->pluginManager->getDefaultSettings($options['type']);
$options['settings'] = isset($values['settings_edit_form']['settings']) ? array_intersect_key($values['settings_edit_form']['settings'], $default_settings) : [];
$options['third_party_settings'] = $values['settings_edit_form']['third_party_settings'] ?? [];
$form_state->set('plugin_settings_update', NULL);
}
$options['type'] = $values['type'];
$options['weight'] = $values['weight'];
$options['region'] = $values['region'];
// Only formatters have configurable label visibility.
if (isset($values['label'])) {
$options['label'] = $values['label'];
}
$entity->setComponent($field_name, $options);
}
}
// Collect data for 'extra' fields.
foreach ($form['#extra'] as $name) {
if ($form_values['fields'][$name]['region'] == 'hidden') {
$entity->removeComponent($name);
}
else {
$entity->setComponent($name, [
'weight' => $form_values['fields'][$name]['weight'],
'region' => $form_values['fields'][$name]['region'],
]);
}
}
}
/**
* Form submission handler for multistep buttons.
*/
public function multistepSubmit($form, FormStateInterface $form_state) {
$trigger = $form_state->getTriggeringElement();
$op = $trigger['#op'];
switch ($op) {
case 'edit':
// Store the field whose settings are currently being edited.
$field_name = $trigger['#field_name'];
$form_state->set('plugin_settings_edit', $field_name);
break;
case 'update':
// Set the field back to 'non edit' mode, and update $this->entity with
// the new settings fro the next rebuild.
$field_name = $trigger['#field_name'];
$form_state->set('plugin_settings_edit', NULL);
$form_state->set('plugin_settings_update', $field_name);
$this->entity = $this->buildEntity($form, $form_state);
break;
case 'cancel':
// Set the field back to 'non edit' mode.
$form_state->set('plugin_settings_edit', NULL);
break;
case 'refresh_table':
// If the currently edited field is one of the rows to be refreshed, set
// it back to 'non edit' mode.
$updated_rows = explode(' ', $form_state->getValue('refresh_rows'));
$plugin_settings_edit = $form_state->get('plugin_settings_edit');
if ($plugin_settings_edit && in_array($plugin_settings_edit, $updated_rows)) {
$form_state->set('plugin_settings_edit', NULL);
}
break;
}
$form_state->setRebuild();
}
/**
* Ajax handler for multistep buttons.
*/
public function multistepAjax($form, FormStateInterface $form_state) {
$response = new AjaxResponse();
$trigger = $form_state->getTriggeringElement();
$op = $trigger['#op'];
// Pick the elements that need to receive the ajax-new-content effect.
$updated_rows = match ($op) {
'edit' => [$trigger['#field_name']],
'update', 'cancel' => [$trigger['#field_name']],
'refresh_table' => array_values(explode(' ', $form_state->getValue('refresh_rows')))
};
$updated_columns = match ($op) {
'edit' => ['plugin'],
'update', 'cancel' => ['plugin', 'settings_summary', 'settings_edit'],
'refresh_table' => ['settings_summary', 'settings_edit'],
};
foreach ($updated_rows as $name) {
foreach ($updated_columns as $key) {
$element = &$form['fields'][$name][$key];
$element['#prefix'] = '<div class="ajax-new-content">' . ($element['#prefix'] ?? '');
$element['#suffix'] = ($element['#suffix'] ?? '') . '</div>';
}
}
// Replace the whole table.
$response->addCommand(new ReplaceCommand('#field-display-overview-wrapper', $form['fields']));
// Add "row updated" warning after the table has been replaced.
if (!in_array($op, ['cancel', 'edit'])) {
foreach ($updated_rows as $name) {
// The ID of the rendered table row is `$name` processed by getClass().
// @see \Drupal\field_ui\Element\FieldUiTable::tablePreRender
$response->addCommand(new TabledragWarningCommand(Html::getClass($name), 'field-display-overview'));
}
}
return $response;
}
/**
* Returns the extra fields of the entity type and bundle used by this form.
*
* @return array
* An array of extra field info.
*
* @see \Drupal\Core\Entity\EntityFieldManagerInterface::getExtraFields()
*/
protected function getExtraFields() {
$context = $this->displayContext == 'view' ? 'display' : $this->displayContext;
$extra_fields = $this->entityFieldManager->getExtraFields($this->entity->getTargetEntityTypeId(), $this->entity->getTargetBundle());
return $extra_fields[$context] ?? [];
}
/**
* Returns an entity display object to be used by this form.
*
* @param string $entity_type_id
* The target entity type ID of the entity display.
* @param string $bundle
* The target bundle of the entity display.
* @param string $mode
* A view or form mode.
*
* @return \Drupal\Core\Entity\Display\EntityDisplayInterface
* An entity display.
*/
abstract protected function getEntityDisplay($entity_type_id, $bundle, $mode);
/**
* Returns an array of applicable widget or formatter options for a field.
*
* @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition
* The field definition.
*
* @return array
* An array of applicable widget or formatter options.
*/
protected function getApplicablePluginOptions(FieldDefinitionInterface $field_definition) {
$options = $this->pluginManager->getOptions($field_definition->getType());
$applicable_options = [];
foreach ($options as $option => $label) {
$plugin_class = DefaultFactory::getPluginClass($option, $this->pluginManager->getDefinition($option));
if ($plugin_class::isApplicable($field_definition)) {
$applicable_options[$option] = $label;
}
}
return $applicable_options;
}
/**
* Returns the ID of the default widget or formatter plugin for a field type.
*
* @param string $field_type
* The field type.
*
* @return string
* The widget or formatter plugin ID.
*/
abstract protected function getDefaultPlugin($field_type);
/**
* Returns the form or view modes used by this form.
*
* @return array
* An array of form or view mode info.
*/
abstract protected function getDisplayModes();
/**
* Returns an array of form or view mode options.
*
* @return array
* An array of form or view mode options.
*/
abstract protected function getDisplayModeOptions();
/**
* Returns a link to the form or view mode admin page.
*
* @return array
* An array of a form element to be rendered as a link.
*/
abstract protected function getDisplayModesLink();
/**
* Returns the region to which a row in the display overview belongs.
*
* @param array $row
* The row element.
*
* @return string|null
* The region name this row belongs to.
*/
public function getRowRegion(&$row) {
$regions = $this->getRegions();
if (!isset($regions[$row['region']['#value']])) {
$row['region']['#value'] = 'hidden';
}
return $row['region']['#value'];
}
/**
* Returns entity (form) displays for the current entity display type.
*
* @return \Drupal\Core\Entity\Display\EntityDisplayInterface[]
* An array holding entity displays or entity form displays.
*/
protected function getDisplays() {
$load_ids = [];
$display_entity_type = $this->entity->getEntityTypeId();
$entity_type = $this->entityTypeManager->getDefinition($display_entity_type);
$config_prefix = $entity_type->getConfigPrefix();
$ids = $this->configFactory()->listAll($config_prefix . '.' . $this->entity->getTargetEntityTypeId() . '.' . $this->entity->getTargetBundle() . '.');
foreach ($ids as $id) {
$config_id = str_replace($config_prefix . '.', '', $id);
[,, $display_mode] = explode('.', $config_id);
if ($display_mode != 'default') {
$load_ids[] = $config_id;
}
}
return $this->entityTypeManager->getStorage($display_entity_type)->loadMultiple($load_ids);
}
/**
* Returns form or view modes statuses for the bundle used by this form.
*
* @return array
* An array of form or view mode statuses.
*/
protected function getDisplayStatuses() {
$display_statuses = [];
$displays = $this->getDisplays();
foreach ($displays as $display) {
$display_statuses[$display->get('mode')] = $display->status();
}
return $display_statuses;
}
/**
* Saves the updated display mode statuses.
*
* @param array $display_statuses
* An array holding updated form or view mode statuses.
*/
protected function saveDisplayStatuses($display_statuses) {
$displays = $this->getDisplays();
foreach ($displays as $display) {
// Only update the display if the status is changing.
$new_status = $display_statuses[$display->get('mode')];
if ($new_status !== $display->status()) {
$display->set('status', $new_status);
$display->save();
}
}
}
/**
* Returns an array containing the table headers.
*
* @return array
* The table header.
*/
abstract protected function getTableHeader();
/**
* Returns the Url object for a specific entity (form) display edit form.
*
* @param string $mode
* The form or view mode.
*
* @return \Drupal\Core\Url
* A Url object for the overview route.
*/
abstract protected function getOverviewUrl($mode);
/**
* Adds the widget or formatter third party settings forms.
*
* @param \Drupal\Core\Field\PluginSettingsInterface $plugin
* The widget or formatter.
* @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition
* The field definition.
* @param array $form
* The (entire) configuration form array.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The form state.
*
* @return array
* The widget or formatter third party settings form.
*/
abstract protected function thirdPartySettingsForm(PluginSettingsInterface $plugin, FieldDefinitionInterface $field_definition, array $form, FormStateInterface $form_state);
/**
* Alters the widget or formatter settings summary.
*
* @param array $summary
* The widget or formatter settings summary.
* @param \Drupal\Core\Field\PluginSettingsInterface $plugin
* The widget or formatter.
* @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition
* The field definition.
*/
abstract protected function alterSettingsSummary(array &$summary, PluginSettingsInterface $plugin, FieldDefinitionInterface $field_definition);
}

View File

@ -0,0 +1,49 @@
<?php
namespace Drupal\field_ui\Form;
use Drupal\Core\Form\FormStateInterface;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
/**
* Provides the add form for entity display modes.
*
* @internal
*/
class EntityDisplayModeAddForm extends EntityDisplayModeFormBase {
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state, $entity_type_id = NULL) {
$form = parent::buildForm($form, $form_state, $entity_type_id);
// Change replace_pattern to avoid undesired dots.
$form['id']['#machine_name']['replace_pattern'] = '[^a-z0-9_]+';
$definition = $this->entityTypeManager->getDefinition($this->targetEntityTypeId);
$form['#title'] = $this->t('Add new @entity-type %label', ['@entity-type' => $definition->getLabel(), '%label' => $this->entityType->getSingularLabel()]);
return $form;
}
/**
* {@inheritdoc}
*/
public function validateForm(array &$form, FormStateInterface $form_state) {
parent::validateForm($form, $form_state);
$form_state->setValueForElement($form['id'], $this->targetEntityTypeId . '.' . $form_state->getValue('id'));
}
/**
* {@inheritdoc}
*/
protected function prepareEntity() {
$definition = $this->entityTypeManager->getDefinition($this->targetEntityTypeId);
if (!$definition->get('field_ui_base_route') || !$definition->hasViewBuilderClass()) {
throw new NotFoundHttpException();
}
$this->entity->setTargetType($this->targetEntityTypeId);
}
}

View File

@ -0,0 +1,22 @@
<?php
namespace Drupal\field_ui\Form;
use Drupal\Core\Entity\EntityDeleteForm;
/**
* Provides the delete form for entity display modes.
*
* @internal
*/
class EntityDisplayModeDeleteForm extends EntityDeleteForm {
/**
* {@inheritdoc}
*/
public function getDescription() {
$entity_type = $this->entity->getEntityType();
return $this->t('Deleting a @entity-type will cause any output still requesting to use that @entity-type to use the default display settings.', ['@entity-type' => $entity_type->getSingularLabel()]);
}
}

View File

@ -0,0 +1,12 @@
<?php
namespace Drupal\field_ui\Form;
/**
* Provides the edit form for entity display modes.
*
* @internal
*/
class EntityDisplayModeEditForm extends EntityDisplayModeFormBase {
}

View File

@ -0,0 +1,314 @@
<?php
namespace Drupal\field_ui\Form;
use Drupal\Core\Entity\Display\EntityFormDisplayInterface;
use Drupal\Core\Entity\Display\EntityViewDisplayInterface;
use Drupal\Core\Entity\EntityDisplayRepositoryInterface;
use Drupal\Core\Entity\EntityForm;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityTypeBundleInfoInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Url;
use Drupal\field_ui\FieldUI;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides the generic base class for entity display mode forms.
*/
abstract class EntityDisplayModeFormBase extends EntityForm {
/**
* The entity type definition.
*
* @var \Drupal\Core\Entity\EntityTypeInterface
*/
protected $entityType;
/**
* The display context. Either 'view' or 'form'.
*
* @var string
*/
protected string $displayContext;
/**
* The entity type for which the display mode is being created or edited.
*
* @var string|null
*/
protected ?string $targetEntityTypeId;
/**
* {@inheritdoc}
*/
protected function init(FormStateInterface $form_state) {
parent::init($form_state);
$this->entityType = $this->entityTypeManager->getDefinition($this->entity->getEntityTypeId());
$this->displayContext = str_replace(['entity_', '_mode'], '', $this->entityType->id());
}
/**
* Constructs a EntityDisplayModeFormBase object.
*
* @param \Drupal\Core\Entity\EntityTypeBundleInfoInterface $entityTypeBundleInfo
* The entity type bundle service.
* @param \Drupal\Core\Entity\EntityDisplayRepository $entityDisplayRepository
* The entity display repository.
*/
public function __construct(protected EntityTypeBundleInfoInterface $entityTypeBundleInfo, protected EntityDisplayRepositoryInterface $entityDisplayRepository) {}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('entity_type.bundle.info'),
$container->get('entity_display.repository'),
);
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state, $entity_type_id = NULL) {
if (!$entity_type_id && !$this->entity->isNew()) {
$entity_type_id = $this->entity->getTargetType();
}
$this->targetEntityTypeId = $entity_type_id;
return parent::buildForm($form, $form_state);
}
/**
* {@inheritdoc}
*/
public function form(array $form, FormStateInterface $form_state) {
$form['label'] = [
'#type' => 'textfield',
'#title' => $this->t('Name'),
'#maxlength' => 100,
'#default_value' => $this->entity->label(),
'#required' => TRUE,
];
$form['description'] = [
'#title' => $this->t('Description'),
'#type' => 'textarea',
'#default_value' => $this->entity->getDescription(),
'#description' => $this->t('This text will be displayed on the @mode_label list page.', [
'@mode_label' => $this->entity->getEntityType()->getPluralLabel(),
]),
];
$form['id'] = [
'#type' => 'machine_name',
'#description' => $this->t('A unique machine-readable name. Can only contain lowercase letters, numbers, and underscores.'),
'#disabled' => !$this->entity->isNew(),
'#default_value' => $this->entity->id(),
'#field_prefix' => $this->entity->isNew() ? $this->entity->getTargetType() . '.' : '',
'#machine_name' => [
'exists' => [$this, 'exists'],
'replace_pattern' => '[^a-z0-9_.]+',
],
];
$bundle_info_service = $this->entityTypeBundleInfo;
$bundles = $bundle_info_service->getAllBundleInfo();
$definition = $this->entityTypeManager->getDefinition($this->entity->isNew() ? $this->targetEntityTypeId : $this->entity->getTargetType());
$bundles_by_entity = [];
$defaults = [];
foreach (array_keys($bundles[$definition->id()]) as $bundle) {
$bundles_by_entity[$bundle] = $bundles[$definition->id()][$bundle]['label'];
// Determine default display modes.
if (!$this->entity->isNew()) {
[, $display_mode_name] = explode('.', $this->entity->id());
if ($this->getDisplayByContext($bundle, $display_mode_name)) {
$defaults[$bundle] = $bundle;
}
}
}
$form['bundles_by_entity'] = [
'#type' => 'checkboxes',
'#title' => $this->t('Enable this @display-mode for the following @bundle-label types:', ['@display-mode' => $this->entityType->getSingularLabel(), '@bundle-label' => $definition->getLabel()]),
'#description' => $this->t('This @display-mode will still be available for the rest of the @bundle-label types if not checked here, but it will not be enabled by default.', ['@bundle-label' => $definition->getLabel(), '@display-mode' => $this->entityType->getSingularLabel()]),
'#options' => $bundles_by_entity,
'#default_value' => $defaults,
];
return $form;
}
/**
* Determines if the display mode already exists.
*
* @param string|int $entity_id
* The entity ID.
* @param array $element
* The form element.
*
* @return bool
* TRUE if the display mode exists, FALSE otherwise.
*/
public function exists($entity_id, array $element) {
// Do not allow to add internal 'default' view mode.
if ($entity_id == 'default') {
return TRUE;
}
/** @var \Drupal\Core\Config\Entity\ConfigEntityStorageInterface $storage */
$storage = $this->entityTypeManager->getStorage($this->entity->getEntityTypeId());
return (bool) $storage
->getQuery()
->condition('id', $element['#field_prefix'] . $entity_id)
->execute();
}
/**
* {@inheritdoc}
*/
public function save(array $form, FormStateInterface $form_state) {
$this->messenger()->addStatus($this->t('Saved the %label @entity-type.', ['%label' => $this->entity->label(), '@entity-type' => $this->entityType->getSingularLabel()]));
$this->entity->save();
\Drupal::service('entity_field.manager')->clearCachedFieldDefinitions();
$form_state->setRedirectUrl($this->entity->toUrl('collection'));
[, $display_mode_name] = explode('.', $form_state->getValue('id'));
$target_entity_id = $this->targetEntityTypeId;
foreach ($form_state->getValue('bundles_by_entity') as $bundle => $value) {
if (!empty($value)) {
// Add a new entity view/form display if it doesn't already exist.
if (!$this->getDisplayByContext($bundle, $display_mode_name)) {
$display = $this->getEntityDisplay($target_entity_id, $bundle, 'default')->createCopy($display_mode_name);
$display->save();
}
// This message is still helpful, even if the view/form display hasn't
// changed, so we keep it outside the above check.
$url = $this->getOverviewUrl($display_mode_name, $value);
$bundle_info_service = $this->entityTypeBundleInfo;
$bundles = $bundle_info_service->getAllBundleInfo();
$bundle_label = $bundles[$target_entity_id][$bundle]['label'];
$display_mode_label = $form_state->getValue('label');
$this->messenger()->addStatus($this->t('<a href=":url">Configure the %display_mode_label %mode mode for %bundle_label</a>.', ['%mode' => $this->displayContext, '%display_mode_label' => $display_mode_label, '%bundle_label' => $bundle_label, ':url' => $url->toString()]));
}
else {
// The view/form display has been unchecked, so we need to delete this.
// There's no confirmation of deleting the view/form display on the node
// content type forms either, so we match that behavior.
if ($display = $this->getDisplayByContext($bundle, $display_mode_name)) {
$display->delete();
}
}
}
}
/**
* Returns an entity display object to be used by this form.
*
* @param string $entity_type_id
* The target entity type ID of the entity display.
* @param string $bundle
* The target bundle of the entity display.
* @param string $mode
* A view or form mode.
*
* @return \Drupal\Core\Entity\Display\EntityDisplayInterface
* An entity display.
*/
private function getEntityDisplay($entity_type_id, $bundle, $mode) {
return match($this->displayContext) {
'view' => $this->entityDisplayRepository->getViewDisplay($entity_type_id, $bundle, $mode),
'form' => $this->entityDisplayRepository->getFormDisplay($entity_type_id, $bundle, $mode),
};
}
/**
* Returns the Url object for a specific entity (form) display edit form.
*
* @param string $mode
* The form or view mode.
* @param string $bundle
* The entity bundle name.
*
* @return \Drupal\Core\Url
* A Url object for the overview route.
*/
private function getOverviewUrl($mode, $bundle): Url {
$entity_type = $this->entityTypeManager->getDefinition($this->targetEntityTypeId);
return match($this->displayContext) {
'view' => Url::fromRoute('entity.entity_view_display.' . $this->targetEntityTypeId . '.view_mode', [
'view_mode_name' => $mode,
] + FieldUI::getRouteBundleParameter($entity_type, $bundle)),
'form' => Url::fromRoute('entity.entity_form_display.' . $this->targetEntityTypeId . '.form_mode', [
'form_mode_name' => $mode,
] + FieldUI::getRouteBundleParameter($entity_type, $bundle)),
};
}
/**
* Load the view display for a given bundle and view mode name.
*
* @param string $bundle
* The entity bundle to load the view display for.
* @param string $view_mode_name
* The view mode name such as "full_content" to load the view display for.
*
* @return \Drupal\Core\Entity\Display\EntityViewDisplayInterface|null
* Returns the view display, or NULL if one does not exist.
*/
private function getViewDisplay(string $bundle, string $view_mode_name): ?EntityViewDisplayInterface {
$view_mode_id = $this->targetEntityTypeId . '.' . $bundle . '.' . $view_mode_name;
return $this->entityTypeManager->getStorage('entity_view_display')->load($view_mode_id);
}
/**
* Load the form display for a given bundle and form mode name.
*
* @param string $bundle
* The entity bundle to load the form display for.
* @param string $form_mode_name
* The form mode name to load the form display for.
*
* @return \Drupal\Core\Entity\Display\EntityFormDisplayInterface|null
* Returns the form display, or NULL if one does not exist.
*/
private function getFormDisplay(string $bundle, string $form_mode_name): ?EntityFormDisplayInterface {
$form_mode_id = $this->targetEntityTypeId . '.' . $bundle . '.' . $form_mode_name;
return $this->entityTypeManager->getStorage('entity_form_display')->load($form_mode_id);
}
/**
* Returns View or Form display based on display context.
*
* @param string $bundle
* The entity bundle to load the display for.
* @param string $display_mode_name
* The display mode name to load the display for.
*
* @return \Drupal\Core\Entity\Display\EntityFormDisplayInterface|\Drupal\Core\Entity\Display\EntityViewDisplayInterface|null
* Returns the display, or NULL if one does not exist.
*/
private function getDisplayByContext(string $bundle, string $display_mode_name): EntityFormDisplayInterface|EntityViewDisplayInterface|null {
return match($this->displayContext) {
'view' => $this->getViewDisplay($bundle, $display_mode_name),
'form' => $this->getFormDisplay($bundle, $display_mode_name),
};
}
/**
* {@inheritdoc}
*/
protected function copyFormValuesToEntity(EntityInterface $entity, array $form, FormStateInterface $form_state): void {
// Config schema dictates that the description value
// cannot be empty string. So, if it is empty, make it NULL.
if ($form_state->hasValue('description') && trim($form_state->getValue('description')) === '') {
$form_state->setValue('description', NULL);
}
parent::copyFormValuesToEntity($entity, $form, $form_state);
}
}

View File

@ -0,0 +1,154 @@
<?php
namespace Drupal\field_ui\Form;
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\Field\PluginSettingsInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Url;
use Drupal\field_ui\FieldUI;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Edit form for the EntityFormDisplay entity type.
*
* @internal
*/
class EntityFormDisplayEditForm extends EntityDisplayFormBase {
/**
* {@inheritdoc}
*/
protected $displayContext = 'form';
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('plugin.manager.field.field_type'),
$container->get('plugin.manager.field.widget'),
$container->get('entity_display.repository'),
$container->get('entity_field.manager')
);
}
/**
* {@inheritdoc}
*/
protected function buildFieldRow(FieldDefinitionInterface $field_definition, array $form, FormStateInterface $form_state) {
$field_row = parent::buildFieldRow($field_definition, $form, $form_state);
$field_name = $field_definition->getName();
// Update the (invisible) title of the 'plugin' column.
$field_row['plugin']['#title'] = $this->t('Formatter for @title', ['@title' => $field_definition->getLabel()]);
if (!empty($field_row['plugin']['settings_edit_form']) && ($plugin = $this->entity->getRenderer($field_name))) {
$plugin_type_info = $plugin->getPluginDefinition();
$field_row['plugin']['settings_edit_form']['label']['#markup'] = $this->t('Widget settings:') . ' <span class="plugin-name">' . $plugin_type_info['label'] . '</span>';
}
return $field_row;
}
/**
* {@inheritdoc}
*/
protected function getEntityDisplay($entity_type_id, $bundle, $mode) {
return $this->entityDisplayRepository->getFormDisplay($entity_type_id, $bundle, $mode);
}
/**
* {@inheritdoc}
*/
protected function getDefaultPlugin($field_type) {
return $this->fieldTypes[$field_type]['default_widget'] ?? NULL;
}
/**
* {@inheritdoc}
*/
protected function getDisplayModes() {
return $this->entityDisplayRepository->getFormModes($this->entity->getTargetEntityTypeId());
}
/**
* {@inheritdoc}
*/
protected function getDisplayModeOptions() {
return $this->entityDisplayRepository->getFormModeOptions($this->entity->getTargetEntityTypeId());
}
/**
* {@inheritdoc}
*/
protected function getDisplayModesLink() {
return [
'#type' => 'link',
'#title' => $this->t('Manage form modes'),
'#url' => Url::fromRoute('entity.entity_form_mode.collection'),
];
}
/**
* {@inheritdoc}
*/
protected function getTableHeader() {
return [
$this->t('Field'),
[
'data' => $this->t('Machine name'),
'class' => [RESPONSIVE_PRIORITY_MEDIUM, 'machine-name'],
],
$this->t('Weight'),
$this->t('Parent'),
$this->t('Region'),
['data' => $this->t('Widget'), 'colspan' => 3],
];
}
/**
* {@inheritdoc}
*/
protected function getOverviewUrl($mode) {
$entity_type = $this->entityTypeManager->getDefinition($this->entity->getTargetEntityTypeId());
return Url::fromRoute('entity.entity_form_display.' . $this->entity->getTargetEntityTypeId() . '.form_mode', [
'form_mode_name' => $mode,
] + FieldUI::getRouteBundleParameter($entity_type, $this->entity->getTargetBundle()));
}
/**
* {@inheritdoc}
*/
protected function thirdPartySettingsForm(PluginSettingsInterface $plugin, FieldDefinitionInterface $field_definition, array $form, FormStateInterface $form_state) {
$settings_form = [];
// Invoke hook_field_widget_third_party_settings_form(), keying resulting
// subforms by module name.
$this->moduleHandler->invokeAllWith(
'field_widget_third_party_settings_form',
function (callable $hook, string $module) use (&$settings_form, $plugin, $field_definition, &$form, $form_state) {
$settings_form[$module] = ($settings_form[$module] ?? []) + ($hook(
$plugin,
$field_definition,
$this->entity->getMode(),
$form,
$form_state
) ?? []);
}
);
return $settings_form;
}
/**
* {@inheritdoc}
*/
protected function alterSettingsSummary(array &$summary, PluginSettingsInterface $plugin, FieldDefinitionInterface $field_definition) {
$context = [
'widget' => $plugin,
'field_definition' => $field_definition,
'form_mode' => $this->entity->getMode(),
];
$this->moduleHandler->alter('field_widget_settings_summary', $summary, $context);
}
}

View File

@ -0,0 +1,26 @@
<?php
namespace Drupal\field_ui\Form;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
/**
* Provides the add form for entity display modes.
*
* @internal
*/
class EntityFormModeAddForm extends EntityDisplayModeAddForm {
/**
* {@inheritdoc}
*/
protected function prepareEntity() {
$definition = $this->entityTypeManager->getDefinition($this->targetEntityTypeId);
if (!$definition->get('field_ui_base_route') || !$definition->hasFormClasses()) {
throw new NotFoundHttpException();
}
$this->entity->setTargetType($this->targetEntityTypeId);
}
}

View File

@ -0,0 +1,189 @@
<?php
namespace Drupal\field_ui\Form;
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\Field\PluginSettingsInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Url;
use Drupal\field_ui\FieldUI;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Drupal\field\FieldLabelOptionsTrait;
/**
* Edit form for the EntityViewDisplay entity type.
*
* @internal
*/
class EntityViewDisplayEditForm extends EntityDisplayFormBase {
use FieldLabelOptionsTrait;
/**
* {@inheritdoc}
*/
protected $displayContext = 'view';
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('plugin.manager.field.field_type'),
$container->get('plugin.manager.field.formatter'),
$container->get('entity_display.repository'),
$container->get('entity_field.manager')
);
}
/**
* {@inheritdoc}
*/
protected function buildFieldRow(FieldDefinitionInterface $field_definition, array $form, FormStateInterface $form_state) {
$field_row = parent::buildFieldRow($field_definition, $form, $form_state);
$field_name = $field_definition->getName();
$display_options = $this->entity->getComponent($field_name);
// Insert the label column.
$label = [
'label' => [
'#type' => 'select',
'#title' => $this->t('Label display for @title', ['@title' => $field_definition->getLabel()]),
'#title_display' => 'invisible',
'#options' => $this->getFieldLabelOptions(),
'#default_value' => $display_options ? $display_options['label'] : 'above',
],
];
$label_position = array_search('plugin', array_keys($field_row));
$field_row = array_slice($field_row, 0, $label_position, TRUE) + $label + array_slice($field_row, $label_position, count($field_row) - 1, TRUE);
// Update the (invisible) title of the 'plugin' column.
$field_row['plugin']['#title'] = $this->t('Formatter for @title', ['@title' => $field_definition->getLabel()]);
if (!empty($field_row['plugin']['settings_edit_form']) && ($plugin = $this->entity->getRenderer($field_name))) {
$plugin_type_info = $plugin->getPluginDefinition();
$field_row['plugin']['settings_edit_form']['label']['#markup'] = $this->t('Format settings:') . ' <span class="plugin-name">' . $plugin_type_info['label'] . '</span>';
}
return $field_row;
}
/**
* {@inheritdoc}
*/
protected function buildExtraFieldRow($field_id, $extra_field) {
$extra_field_row = parent::buildExtraFieldRow($field_id, $extra_field);
// Insert an empty placeholder for the label column.
$label = [
'empty_cell' => [
'#markup' => '&nbsp;',
],
];
$label_position = array_search('plugin', array_keys($extra_field_row));
$extra_field_row = array_slice($extra_field_row, 0, $label_position, TRUE) + $label + array_slice($extra_field_row, $label_position, count($extra_field_row) - 1, TRUE);
return $extra_field_row;
}
/**
* {@inheritdoc}
*/
protected function getEntityDisplay($entity_type_id, $bundle, $mode) {
return $this->entityDisplayRepository->getViewDisplay($entity_type_id, $bundle, $mode);
}
/**
* {@inheritdoc}
*/
protected function getDefaultPlugin($field_type) {
return $this->fieldTypes[$field_type]['default_formatter'] ?? NULL;
}
/**
* {@inheritdoc}
*/
protected function getDisplayModes() {
return $this->entityDisplayRepository->getViewModes($this->entity->getTargetEntityTypeId());
}
/**
* {@inheritdoc}
*/
protected function getDisplayModeOptions() {
return $this->entityDisplayRepository->getViewModeOptions($this->entity->getTargetEntityTypeId());
}
/**
* {@inheritdoc}
*/
protected function getDisplayModesLink() {
return [
'#type' => 'link',
'#title' => $this->t('Manage view modes'),
'#url' => Url::fromRoute('entity.entity_view_mode.collection'),
];
}
/**
* {@inheritdoc}
*/
protected function getTableHeader() {
return [
$this->t('Field'),
[
'data' => $this->t('Machine name'),
'class' => [RESPONSIVE_PRIORITY_MEDIUM, 'machine-name'],
],
$this->t('Weight'),
$this->t('Parent'),
$this->t('Region'),
$this->t('Label'),
['data' => $this->t('Format'), 'colspan' => 3],
];
}
/**
* {@inheritdoc}
*/
protected function getOverviewUrl($mode) {
$entity_type = $this->entityTypeManager->getDefinition($this->entity->getTargetEntityTypeId());
return Url::fromRoute('entity.entity_view_display.' . $this->entity->getTargetEntityTypeId() . '.view_mode', [
'view_mode_name' => $mode,
] + FieldUI::getRouteBundleParameter($entity_type, $this->entity->getTargetBundle()));
}
/**
* {@inheritdoc}
*/
protected function thirdPartySettingsForm(PluginSettingsInterface $plugin, FieldDefinitionInterface $field_definition, array $form, FormStateInterface $form_state) {
$settings_form = [];
// Invoke hook_field_formatter_third_party_settings_form(), keying resulting
// subforms by module name.
$this->moduleHandler->invokeAllWith(
'field_formatter_third_party_settings_form',
function (callable $hook, string $module) use (&$settings_form, &$plugin, &$field_definition, &$form, &$form_state) {
$settings_form[$module] = ($settings_form[$module] ?? []) + ($hook(
$plugin,
$field_definition,
$this->entity->getMode(),
$form,
$form_state,
)) ?? [];
}
);
return $settings_form;
}
/**
* {@inheritdoc}
*/
protected function alterSettingsSummary(array &$summary, PluginSettingsInterface $plugin, FieldDefinitionInterface $field_definition) {
$context = [
'formatter' => $plugin,
'field_definition' => $field_definition,
'view_mode' => $this->entity->getMode(),
];
$this->moduleHandler->alter('field_formatter_settings_summary', $summary, $context);
}
}

View File

@ -0,0 +1,126 @@
<?php
namespace Drupal\field_ui\Form;
use Drupal\Core\Config\Entity\ConfigEntityInterface;
use Drupal\Core\Entity\EntityDeleteForm;
use Drupal\Core\Entity\EntityTypeBundleInfoInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Render\Element;
use Drupal\field_ui\FieldUI;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides a form for removing a field from a bundle.
*
* @internal
*/
class FieldConfigDeleteForm extends EntityDeleteForm {
public function __construct(protected EntityTypeBundleInfoInterface $entityTypeBundleInfo, EntityTypeManagerInterface $entityTypeManager) {
$this->entityTypeManager = $entityTypeManager;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('entity_type.bundle.info'),
$container->get('entity_type.manager'),
);
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
$form = parent::buildForm($form, $form_state);
// If we are adding the field storage as a dependency to delete, then that
// will list the field as a dependency. That is confusing, so remove it.
// Also remove the entity type and the whole entity deletions details
// element if nothing else is in there.
if (isset($form['entity_deletes']['field_config']['#items']) && isset($form['entity_deletes']['field_config']['#items'][$this->entity->id()])) {
unset($form['entity_deletes']['field_config']['#items'][$this->entity->id()]);
if (empty($form['entity_deletes']['field_config']['#items'])) {
unset($form['entity_deletes']['field_config']);
if (!Element::children($form['entity_deletes'])) {
$form['entity_deletes']['#access'] = FALSE;
}
}
}
return $form;
}
/**
* {@inheritdoc}
*/
protected function getConfigNamesToDelete(ConfigEntityInterface $entity) {
/** @var \Drupal\field\FieldStorageConfigInterface $field_storage */
$field_storage = $entity->getFieldStorageDefinition();
$config_names = [$entity->getConfigDependencyName()];
// If there is only one bundle left for this field storage, it will be
// deleted too, notify the user about dependencies.
if (count($field_storage->getBundles()) <= 1) {
$config_names[] = $field_storage->getConfigDependencyName();
}
return $config_names;
}
/**
* {@inheritdoc}
*/
public function getCancelUrl() {
return FieldUI::getOverviewRouteInfo($this->entity->getTargetEntityTypeId(), $this->entity->getTargetBundle());
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
$field_storage = $this->entity->getFieldStorageDefinition();
$target_entity_type_id = $this->entity->getTargetEntityTypeId();
$target_bundle = $this->entity->getTargetBundle();
$target_entity_definition = $this->entityTypeManager->getDefinition($target_entity_type_id);
$target_entity_bundle_entity_type_id = $target_entity_definition->getBundleEntityType();
if (empty($target_entity_bundle_entity_type_id)) {
$source_label = $this->t('entity type');
}
else {
$target_entity_bundle_entity_type_definition = $this->entityTypeManager->getDefinition($target_entity_bundle_entity_type_id);
$source_label = strtolower($target_entity_bundle_entity_type_definition->getLabel());
}
$bundles = $this->entityTypeBundleInfo->getBundleInfo($target_entity_type_id);
$bundle_label = $bundles[$target_bundle]['label'];
if ($field_storage && !$field_storage->isLocked()) {
$this->entity->delete();
$this->messenger()->addStatus($this->t('The field %field has been deleted from the %type %source_label.', [
'%field' => $this->entity->label(),
'%type' => $bundle_label,
'%source_label' => $source_label,
]));
}
else {
$this->messenger()->addError($this->t('There was a problem removing the %field from the %type %source_label.', [
'%field' => $this->entity->label(),
'%type' => $bundle_label,
'%source_label' => $source_label,
]));
}
$form_state->setRedirectUrl($this->getCancelUrl());
// Fields are purged on cron. However field module prevents disabling
// modules when field types they provided are used in a field until it is
// fully purged. In the case that a field has minimal or no content, a
// single call to field_purge_batch() will remove it from the system. Call
// this with a low batch limit to avoid administrators having to wait for
// cron runs when removing fields that meet this criteria.
field_purge_batch(10);
}
}

View File

@ -0,0 +1,610 @@
<?php
namespace Drupal\field_ui\Form;
use Drupal\Component\Serialization\Json;
use Drupal\Core\Ajax\AjaxResponse;
use Drupal\Core\Ajax\RedirectCommand;
use Drupal\Core\Ajax\ReplaceCommand;
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\Render\ElementInfoManagerInterface;
use Drupal\Core\TempStore\PrivateTempStore;
use Drupal\Core\TypedData\TypedDataInterface;
use Drupal\Core\TypedData\TypedDataManagerInterface;
use Drupal\Core\Url;
use Drupal\field\FieldConfigInterface;
use Drupal\field_ui\FieldUI;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides a form for the field settings form.
*
* @internal
*/
class FieldConfigEditForm extends EntityForm {
use FieldStorageCreationTrait;
/**
* The entity being used by this form.
*
* @var \Drupal\field\FieldConfigInterface
*/
protected $entity;
/**
* The name of the entity type.
*
* @var string
*/
protected string $entityTypeId;
/**
* The entity bundle.
*
* @var string
*/
protected string $bundle;
public function __construct(
protected EntityTypeBundleInfoInterface $entityTypeBundleInfo,
protected TypedDataManagerInterface $typedDataManager,
protected EntityDisplayRepositoryInterface $entityDisplayRepository,
protected PrivateTempStore $tempStore,
protected ElementInfoManagerInterface $elementInfo,
) {}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('entity_type.bundle.info'),
$container->get('typed_data_manager'),
$container->get('entity_display.repository'),
$container->get('tempstore.private')->get('field_ui'),
$container->get('plugin.manager.element_info'),
);
}
/**
* {@inheritdoc}
*/
public function getFormId() {
// Ensure that the form ID remains consistent between both 'default' and
// 'edit' operations. This is needed because historically it was only
// possible to edit the field configuration.
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());
$form_title = $this->t('%field settings for %bundle', [
'%field' => $this->entity->getLabel(),
'%bundle' => $bundles[$this->entity->getTargetBundle()]['label'],
]);
$form['#title'] = $form_title;
if ($field_storage->isLocked()) {
$form['locked'] = [
'#markup' => $this->t('The field %field is locked and cannot be edited.', ['%field' => $this->entity->getLabel()]),
];
return $form;
}
// Build the configurable field values.
$form['label'] = [
'#type' => 'textfield',
'#title' => $this->t('Label'),
'#default_value' => $this->entity->getLabel() ?: $field_storage->getName(),
'#required' => TRUE,
'#maxlength' => 255,
'#weight' => -20,
];
$form['description'] = [
'#type' => 'textarea',
'#title' => $this->t('Help text'),
'#default_value' => $this->entity->getDescription(),
'#rows' => 5,
'#description' => $this->t('Instructions to present to the user below this field on the editing form.<br />Allowed HTML tags: @tags', ['@tags' => FieldFilteredMarkup::displayAllowedTags()]) . '<br />' . $this->t('This field supports tokens.'),
'#weight' => -10,
];
$form['required'] = [
'#type' => 'checkbox',
'#title' => $this->t('Required field'),
'#default_value' => $this->entity->isRequired(),
'#weight' => -5,
];
// Create an arbitrary entity object (used by the 'default value' widget).
$ids = (object) [
'entity_type' => $this->entity->getTargetEntityTypeId(),
'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'],
];
$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'],
];
$field_storage_form = $this->entityTypeManager->getFormObject('field_storage_config', $this->operation);
$field_storage_form->setEntity($field_storage);
$subform_state = SubformState::createForSubform($form['field_storage']['subform'], $form, $form_state, $field_storage_form);
$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($this->entity, $form['#entity']);
$item = $items->first() ?: $items->appendItem();
$this->addAjaxCallbacks($form['field_storage']['subform']);
if (isset($form['field_storage']['subform']['cardinality_container'])) {
$form['field_storage']['subform']['cardinality_container']['#parents'] = [
'field_storage',
'subform',
];
}
// 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'] = [
'#tree' => TRUE,
'#weight' => 10,
];
$form['settings'] += $item->fieldSettingsForm($form, $form_state);
$form['third_party_settings'] = [
'#tree' => TRUE,
'#weight' => 11,
];
// 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($this->entity, $form['#entity']);
// Add handling for default value.
if ($element = $items->defaultValuesForm($form, $form_state)) {
$has_required = $this->hasAnyRequired($element);
$element = array_merge($element, [
'#type' => 'details',
'#title' => $this->t('Default value'),
'#open' => TRUE,
'#tree' => TRUE,
'#description' => $this->t('The default value for this field, used when creating new content.'),
'#weight' => 12,
]);
if (!$has_required) {
$has_default_value = count($this->entity->getDefaultValue($form['#entity'])) > 0;
$element['#states'] = [
'invisible' => [
':input[name="set_default_value"]' => ['checked' => FALSE],
],
];
$form['set_default_value'] = [
'#type' => 'checkbox',
'#title' => $this->t('Set default value'),
'#default_value' => $has_default_value,
'#description' => $this->t('Provide a pre-filled value for the editing form.'),
'#weight' => $element['#weight'],
];
}
$form['default_value'] = $element;
}
$form['#prefix'] = '<div id="field-combined">';
$form['#suffix'] = '</div>';
$form['#attached']['library'][] = 'field_ui/drupal.field_ui';
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.
*
* @param array $element
* An element to check.
*
* @return bool
* TRUE if the element contains any required elements, FALSE otherwise.
*/
private function hasAnyRequired(array $element) {
$has_required = FALSE;
foreach (Element::children($element) as $child) {
if (isset($element[$child]['#required']) && $element[$child]['#required']) {
$has_required = TRUE;
break;
}
if (Element::children($element[$child])) {
return $this->hasAnyRequired($element[$child]);
}
}
return $has_required;
}
/**
* {@inheritdoc}
*/
protected function actions(array $form, FormStateInterface $form_state) {
$actions = parent::actions($form, $form_state);
$actions['submit']['#value'] = $this->entity->isNew() ? $this->t('Save') : $this->t('Save settings');
$actions['submit']['#ajax'] = [
'callback' => '::ajaxSubmit',
];
if ($this->entity->isNew()) {
$entity_type = $this->entity->getTargetEntityTypeId();
$route_parameters = [
'field_name' => $this->entity->getName(),
'entity_type' => $entity_type,
] + FieldUI::getRouteBundleParameter($this->entityTypeManager->getDefinition($entity_type), $this->entity->getTargetBundle());
$actions['back'] = [
'#type' => 'link',
'#weight' => 1,
'#title' => $this->t('Change field type'),
'#limit_validation_errors' => [],
'#attributes' => [
'class' => ['button', 'use-ajax'],
'data-dialog-type' => 'modal',
'data-dialog-options' => Json::encode([
'width' => '1100',
]),
],
'#url' => Url::fromRoute("field_ui.field_storage_config_reset_add_$entity_type", $route_parameters),
];
}
if (!$this->entity->isNew()) {
$target_entity_type = $this->entityTypeManager->getDefinition($this->entity->getTargetEntityTypeId());
$route_parameters = [
'field_config' => $this->entity->id(),
] + FieldUI::getRouteBundleParameter($target_entity_type, $this->entity->getTargetBundle());
$url = new Url('entity.field_config.' . $target_entity_type->id() . '_field_delete_form', $route_parameters);
$actions['delete'] = [
'#type' => 'link',
'#title' => $this->t('Delete'),
'#url' => $url,
'#access' => $this->entity->access('delete'),
'#attributes' => [
'class' => ['button', 'button--danger', 'use-ajax'],
'data-dialog-type' => 'modal',
'data-dialog-options' => Json::encode([
'width' => '1100',
]),
],
];
}
return $actions;
}
/**
* Submit form #ajax callback.
*
* @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.
*
* @return \Drupal\Core\Ajax\AjaxResponse
* An AJAX response that display validation error messages or represents a
* successful submission.
*
* @see \Drupal\Core\Ajax\AjaxFormHelperTrait
*/
public function ajaxSubmit(array &$form, FormStateInterface $form_state): AjaxResponse {
if ($form_state->hasAnyErrors()) {
$form['status_messages'] = [
'#type' => 'status_messages',
'#weight' => -1000,
];
$form['#sorted'] = FALSE;
$response = new AjaxResponse();
$response->addCommand(new ReplaceCommand('#field-combined', $form));
}
else {
$response = $this->successfulAjaxSubmit($form, $form_state);
}
return $response;
}
/**
* Respond to a successful AJAX submission.
*
* @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.
*
* @return \Drupal\Core\Ajax\AjaxResponse
* An AJAX response.
*/
protected function successfulAjaxSubmit(array $form, FormStateInterface $form_state): AjaxResponse {
$response = new AjaxResponse();
$response->addCommand(new RedirectCommand(FieldUI::getOverviewRouteInfo($this->entity->getTargetEntityTypeId(), $this->entity->getTargetBundle())->toString()));
return $response;
}
/**
* {@inheritdoc}
*/
public function validateForm(array &$form, FormStateInterface $form_state) {
parent::validateForm($form, $form_state);
// Additional validation to work when JS is disabled.
if (!$form_state->getValue('label')) {
$form_state->setErrorByName('label', $this->t('Label field is required.'));
}
$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);
$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($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);
}
/**
* {@inheritdoc}
*/
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());
$subform_state = SubformState::createForSubform($form['field_storage']['subform'], $form, $form_state, $field_storage_form);
$field_storage_form->submitForm($form['field_storage']['subform'], $subform_state);
try {
$field_storage_form->save($form['field_storage']['subform'], $subform_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($this->entity, $form['#entity']);
$default_value = $items->defaultValuesFormSubmit($form['default_value'], $form, $form_state);
}
$this->entity->setDefaultValue($default_value);
}
/**
* {@inheritdoc}
*/
public function save(array $form, FormStateInterface $form_state) {
// Save field config.
try {
try {
$this->entity->save();
}
catch (EntityStorageException $exception) {
$this->handleEntityStorageException($form_state, $exception);
return;
}
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'] ?? []);
}
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()));
}
}
catch (\Exception $e) {
$this->messenger()->addError(
$this->t(
'Attempt to update field %label failed: %message.',
[
'%label' => $this->entity->getLabel(),
'%message' => $e->getMessage(),
]
)
);
}
}
/**
* The _title_callback for the field settings form.
*
* @param \Drupal\field\FieldConfigInterface $field_config
* The field.
*
* @return string
* The label of the field.
*/
public function getTitle(FieldConfigInterface $field_config) {
return $field_config->label();
}
/**
* 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
* The typed data object representing the field configuration and its
* default value.
*/
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($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 {
if (isset($form['#type']) && !isset($form['#ajax'])) {
if ($this->elementInfo->getInfoProperty($form['#type'], '#input') && !$this->elementInfo->getInfoProperty($form['#type'], '#is_button')) {
$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()]));
}
}

View File

@ -0,0 +1,495 @@
<?php
namespace Drupal\field_ui\Form;
use Drupal\Component\Serialization\Json;
use Drupal\Component\Utility\Html;
use Drupal\Component\Utility\SortArray;
use Drupal\Core\Ajax\AjaxHelperTrait;
use Drupal\Core\Ajax\AjaxResponse;
use Drupal\Core\Ajax\OpenModalDialogWithUrl;
use Drupal\Core\Ajax\ReplaceCommand;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Entity\EntityFieldManagerInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
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\Core\Url;
use Drupal\field\Entity\FieldStorageConfig;
use Drupal\field_ui\FieldUI;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides a form for the "field storage" add subform.
*
* @internal
*/
class FieldStorageAddForm extends FormBase {
use AjaxHelperTrait;
/**
* The name of the entity type.
*
* @var string
*/
protected $entityTypeId;
/**
* The entity bundle.
*
* @var string
*/
protected $bundle;
public function __construct(protected EntityTypeManagerInterface $entityTypeManager, protected FieldTypePluginManagerInterface $fieldTypePluginManager, ConfigFactoryInterface $configFactory, protected EntityFieldManagerInterface $entityFieldManager, protected PrivateTempStore $tempStore, protected FieldTypeCategoryManagerInterface $fieldTypeCategoryManager) {
$this->setConfigFactory($configFactory);
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('entity_type.manager'),
$container->get('plugin.manager.field.field_type'),
$container->get('config.factory'),
$container->get('entity_field.manager'),
$container->get('tempstore.private')->get('field_ui'),
$container->get('plugin.manager.field.field_type_category'),
);
}
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'field_ui_field_storage_add_form';
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state, $entity_type_id = NULL, $bundle = NULL, $selected_field_type = NULL, $display_as_group = 'false') {
$display_as_group = str_contains($display_as_group, 'true');
if (!$form_state->get('entity_type_id')) {
$form_state->set('entity_type_id', $entity_type_id);
}
if (!$form_state->get('bundle')) {
$form_state->set('bundle', $bundle);
}
if (!$form_state->get('field_type')) {
$form_state->set('field_type', $selected_field_type);
}
if (!$form_state->get('display_as_group')) {
$form_state->set('display_as_group', $display_as_group);
}
$this->entityTypeId = $form_state->get('entity_type_id');
$this->bundle = $form_state->get('bundle');
$unique_definitions = [];
$grouped_definitions = $this->fieldTypePluginManager
->getGroupedDefinitions($this->fieldTypePluginManager->getEntityTypeUiDefinitions($this->entityTypeId), 'label', 'id');
if (array_key_exists($selected_field_type, $grouped_definitions)) {
$field_types = $grouped_definitions[$selected_field_type];
foreach ($field_types as $name => $field_type) {
$unique_definitions[$selected_field_type][$name] = ['unique_identifier' => $name] + $field_type;
}
}
$entity_type = $this->entityTypeManager->getDefinition($this->entityTypeId);
$route_parameters_back = [] + FieldUI::getRouteBundleParameter($entity_type, $this->bundle);
$form['actions'] = ['#type' => 'actions'];
$form['label'] = [
'#type' => 'textfield',
'#title' => $this->t('Label'),
'#size' => 30,
'#required' => TRUE,
'#maxlength' => 255,
'#weight' => -20,
];
$field_prefix = $this->configFactory->get('field_ui.settings')->get('field_prefix');
$form['field_name'] = [
'#type' => 'machine_name',
'#field_prefix' => $field_prefix,
'#size' => 15,
'#description' => $this->t('A unique machine-readable name containing letters, numbers, and underscores.'),
// Calculate characters depending on the length of the field prefix
// setting. Maximum length is 32.
'#maxlength' => FieldStorageConfig::NAME_MAX_LENGTH - strlen($field_prefix),
'#machine_name' => [
'source' => ['label'],
'exists' => [$this, 'fieldNameExists'],
],
'#required' => TRUE,
];
$form['field_options_wrapper'] = [
'#prefix' => '<div class="field-options-wrapper">',
'#suffix' => '</div>',
];
// Set the selected field to the form state by checking
// the checked attribute.
if (isset($selected_field_type)) {
if ($display_as_group) {
$form['field_options_wrapper']['label'] = [
'#type' => 'label',
'#title' => $this->t('Choose a field type'),
'#required' => TRUE,
];
$form['field_options_wrapper']['fields'] = [
'#type' => 'container',
'#attributes' => [
'class' => ['group-field-options'],
],
];
foreach ($unique_definitions[$selected_field_type] as $option_key => $option) {
$description = !is_array($option['description']) ? $option['description'] : [
'#theme' => 'item_list',
'#items' => $option['description'],
];
$radio_element = [
'#type' => 'radio',
'#theme_wrappers' => ['form_element__new_storage_type'],
'#title' => $option['label'],
'#description' => $description,
'#id' => Html::getClass($option['unique_identifier']),
'#weight' => $option['weight'],
'#parents' => ['field_options_wrapper'],
'#attributes' => [
'class' => ['field-option-radio'],
'data-once' => 'field-click-to-select',
'checked' => $this->getRequest()->request->get('field_options_wrapper') !== NULL && $this->getRequest()->request->get('field_options_wrapper') == $option_key,
],
'#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['field_options_wrapper']['fields'] += $group_field_options;
}
$form['actions']['previous'] = [
'#type' => 'link',
'#title' => $this->t('Change field type'),
'#url' => Url::fromRoute("field_ui.field_storage_config_add_$entity_type_id", $route_parameters_back),
'#attributes' => [
'class' => ['button', 'use-ajax'],
'data-dialog-type' => 'modal',
'data-dialog-options' => Json::encode([
'width' => '1100',
]),
],
];
$form['actions']['submit'] = [
'#type' => 'submit',
'#value' => $this->t('Continue'),
'#submit' => ['::submitForm'],
'#attributes' => [
'class' => ['button', 'button--primary'],
'data-dialog-type' => 'modal',
'data-dialog-options' => Json::encode([
'width' => '1100',
]),
],
];
if ($this->isAjax()) {
$form['actions']['submit']['#ajax']['callback'] = '::ajaxSubmit';
}
}
// 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
// translation at field level.
$form['translatable'] = [
'#type' => 'value',
'#value' => TRUE,
];
$form['#prefix'] = '<div id="field-storage-subfield">';
$form['#suffix'] = '</div>';
$form['#attached']['library'] = [
'field_ui/drupal.field_ui',
'field_ui/drupal.field_ui.manage_fields',
'core/drupal.dialog.ajax',
];
return $form;
}
/**
* {@inheritdoc}
*/
public function validateForm(array &$form, FormStateInterface $form_state) {
// Missing subtype.
if (!$form_state->getValue('field_options_wrapper') && isset($form['field_options_wrapper']['fields'])) {
$form_state->setErrorByName('field_options_wrapper', $this->t('You need to select a field type.'));
}
// Additional validation to work when JS is disabled.
if (!$form_state->getValue('label')) {
$form_state->setErrorByName('label', $this->t('Label field is required.'));
}
if (!$form_state->getValue('field_name')) {
$form_state->setErrorByName('label', $this->t('Machine-readable name field is required.'));
}
// Field name validation.
else {
$field_name = $form_state->getValue('field_name');
// Add the field prefix.
$field_name = $this->config('field_ui.settings')->get('field_prefix') . $field_name;
$form_state->setValueForElement($form['field_name'], $field_name);
// Set the temp store here, so we can actually see the error on the modal.
$field_storage_type = $form_state->getValue('field_options_wrapper') ?? $form_state->get('field_type');
$this->setTempStore($this->entityTypeId, $field_storage_type, $this->bundle, $form_state->getValue('label'), $form_state->getValue('field_name'), $form_state->getValue('translatable'));
if (!empty($this->messenger()->messagesByType('error'))) {
$form_state->setErrorByName('drupal-modal', $this->t('There was a problem creating field @label: @message', ['@label' => $form_state->getValue('label'), '@message' => explode(':', $this->messenger()->messagesByType('error')[0])[1]]));
// We need to clear out the messenger so that we just see the message
// on the modal and not on the page when it closes.
$this->messenger()->deleteAll();
}
}
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
$form_state->setRedirectUrl($this->getRedirectUrl($form_state->getValue('field_name')));
}
/**
* Gets the redirect URL.
*
* @param string $field_name
* The field name.
*
* @return \Drupal\Core\Url
* The URL to redirect to.
*
* @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
*/
private function getRedirectUrl(string $field_name): Url {
$route_parameters = [
'field_name' => $field_name,
'entity_type' => $this->entityTypeId,
] + FieldUI::getRouteBundleParameter($this->entityTypeManager->getDefinition($this->entityTypeId), $this->bundle);
return Url::fromRoute("field_ui.field_add_{$this->entityTypeId}", $route_parameters);
}
/**
* Submit form #ajax callback.
*
* @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.
*
* @return \Drupal\Core\Ajax\AjaxResponse
* An AJAX response that display validation error messages or represents a
* successful submission.
*
* @see \Drupal\Core\Ajax\AjaxFormHelperTrait
*/
public function ajaxSubmit(array &$form, FormStateInterface $form_state): AjaxResponse {
if ($form_state->hasAnyErrors()) {
$form['status_messages'] = [
'#type' => 'status_messages',
'#weight' => -1000,
];
$form['#sorted'] = FALSE;
$response = new AjaxResponse();
$response->addCommand(new ReplaceCommand('#field-storage-subfield', $form));
}
else {
$response = $this->successfulAjaxSubmit($form, $form_state);
}
return $response;
}
/**
* Respond to a successful AJAX submission.
*
* @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.
*
* @return \Drupal\Core\Ajax\AjaxResponse
* An AJAX response.
*/
protected function successfulAjaxSubmit(array $form, FormStateInterface $form_state): AjaxResponse {
$response = new AjaxResponse();
$response->addCommand(new OpenModalDialogWithUrl($this->getRedirectUrl($form_state->getValue('field_name'))->toString(), []));
return $response;
}
/**
* Get default options from preconfigured options for a new field.
*
* @param string $field_name
* The machine name of the field.
* @param string $preset_key
* A key in the preconfigured options array for the field.
*
* @return array
* An array of settings with keys 'field_storage_config', 'field_config',
* 'entity_form_display', and 'entity_view_display'.
*
* @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
*
* @see \Drupal\Core\Field\PreconfiguredFieldUiOptionsInterface::getPreconfiguredOptions()
*/
protected function getNewFieldDefaults(string $field_name, string $preset_key): array {
$field_type_definition = $this->fieldTypePluginManager->getDefinition($field_name);
$options = $this->fieldTypePluginManager->getPreconfiguredOptions($field_type_definition['id']);
$field_options = $options[$preset_key] ?? [];
$default_options = [];
// Merge in preconfigured field storage options.
if (isset($field_options['field_storage_config'])) {
foreach (['cardinality', 'settings'] as $key) {
if (isset($field_options['field_storage_config'][$key])) {
$default_options['field_storage_config'][$key] = $field_options['field_storage_config'][$key];
}
}
}
// Merge in preconfigured field options.
if (isset($field_options['field_config'])) {
foreach (['required', 'settings'] as $key) {
if (isset($field_options['field_config'][$key])) {
$default_options['field_config'][$key] = $field_options['field_config'][$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' => []]),
];
}
else {
$default_options[$key] = ['default' => []];
}
}
return $default_options;
}
/**
* Store field information in temp store in order to build the edit form.
*
* @param string $entity_type
* The name of the entity type.
* @param string $field_storage_type
* The machine name of the field storage.
* @param string $bundle
* The entity bundle.
* @param string $field_label
* The label of the field.
* @param string $field_machine_name
* The machine name of the field.
* @param bool $translatable
* TRUE if the field is translatable.
*/
public function setTempStore(string $entity_type, string $field_storage_type, string $bundle, string $field_label, string $field_machine_name, bool $translatable): void {
$field_values = [
'entity_type' => $entity_type,
'bundle' => $bundle,
];
$default_options = [];
// Check if we're dealing with a preconfigured field.
if (strpos($field_storage_type, 'field_ui:') === 0) {
[, $field_type, $preset_key] = explode(':', $field_storage_type, 3);
$default_options = $this->getNewFieldDefaults($field_type, $preset_key);
}
else {
$field_type = $field_storage_type;
}
$field_values += [
...$default_options['field_config'] ?? [],
'field_name' => $field_machine_name,
'label' => $field_label,
// Field translatability should be explicitly enabled by the users.
'translatable' => FALSE,
];
$field_storage_values = [
...$default_options['field_storage_config'] ?? [],
'field_name' => $field_machine_name,
'type' => $field_type,
'entity_type' => $entity_type,
'translatable' => $translatable,
];
try {
$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 :@message', ['@message' => $e->getMessage()]));
return;
}
// Save field and field storage values in tempstore.
$this->tempStore->set($entity_type . ':' . $field_machine_name, [
'field_storage' => $field_storage_entity,
'field_config_values' => $field_values,
'default_options' => $default_options,
]);
}
/**
* Checks if a field machine name is taken.
*
* @param string $value
* The machine name, not prefixed.
* @param array $element
* An array containing the structure of the 'field_name' element.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current state of the form.
*
* @return bool
* Whether or not the field machine name is taken.
*/
public function fieldNameExists($value, $element, FormStateInterface $form_state) {
// Add the field prefix.
$field_name = $this->configFactory->get('field_ui.settings')->get('field_prefix') . $value;
$field_storage_definitions = $this->entityFieldManager->getFieldStorageDefinitions($this->entityTypeId);
return isset($field_storage_definitions[$field_name]);
}
/**
* Submit handler for displaying fields after a group is selected.
*/
public static function rebuildWithOptions($form, FormStateInterface &$form_state) {
$form_state->setRebuild();
}
/**
* Submit handler for resetting the form.
*/
public static function startOver($form, FormStateInterface &$form_state) {
$form_state->unsetValue('new_storage_type');
$form_state->setRebuild();
}
}

View File

@ -0,0 +1,276 @@
<?php
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\FieldConfigInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
/**
* Provides a form for the "field storage" edit page.
*
* @internal
*/
class FieldStorageConfigEditForm extends EntityForm {
/**
* The entity being used by this form.
*
* @var \Drupal\field\FieldStorageConfigInterface
*/
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}
*/
public function getEntityFromRouteMatch(RouteMatchInterface $route_match, $entity_type_id) {
// The URL of this entity form contains only the ID of the field_config
// but we are actually editing a field_storage_config entity.
$field_config = FieldConfig::load($route_match->getRawParameter('field_config'));
if (!$field_config) {
throw new NotFoundHttpException();
}
return $field_config->getFieldStorageDefinition();
}
/**
* {@inheritdoc}
*
* @param array $form
* A nested array form elements comprising the form.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current state of the form.
* @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, FieldConfigInterface|string|null $field_config = NULL) {
if ($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());
$form_state->set('bundle', $field->getTargetBundle());
}
return parent::buildForm($form, $form_state);
}
/**
* {@inheritdoc}
*/
public function form(array $form, FormStateInterface $form_state) {
$form = parent::form($form, $form_state);
$field_label = $form_state->get('field_config')->label();
$form['#prefix'] = '<p>' . $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]) . '</p>';
// Add the cardinality sub-form.
$form['cardinality_container'] = $this->getCardinalityForm();
// Add settings provided by the field module. The field module is
// responsible for not returning settings that cannot be changed if
// the field already has data.
$form['settings'] = [
'#weight' => -10,
'#tree' => TRUE,
];
// Create an arbitrary entity object, so that we can have an instantiated
// FieldItem.
$ids = (object) [
'entity_type' => $form_state->get('entity_type_id'),
'bundle' => $form_state->get('bundle'),
'entity_id' => NULL,
];
$entity = _field_create_entity_from_ids($ids);
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());
return $form;
}
/**
* Builds the cardinality form.
*
* @return array
* The cardinality form render array.
*/
protected function getCardinalityForm() {
$form = [
// Reset #parents so the additional container does not appear.
'#parents' => [],
'#type' => 'fieldset',
'#title' => $this->t('Allowed number of values'),
'#attributes' => [
'class' => [
'container-inline',
'fieldgroup',
'form-composite',
],
],
];
if ($enforced_cardinality = $this->getEnforcedCardinality()) {
if ($enforced_cardinality === FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED) {
$markup = $this->t("This field cardinality is set to unlimited and cannot be configured.");
}
else {
$markup = $this->t("This field cardinality is set to @cardinality and cannot be configured.", ['@cardinality' => $enforced_cardinality]);
}
$form['cardinality'] = ['#markup' => $markup];
}
else {
$form['#element_validate'][] = [$this, 'validateCardinality'];
$cardinality = $this->entity->getCardinality();
$form['cardinality'] = [
'#type' => 'select',
'#title' => $this->t('Allowed number of values'),
'#title_display' => 'invisible',
'#options' => [
'number' => $this->t('Limited'),
FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED => $this->t('Unlimited'),
],
'#default_value' => ($cardinality == FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED) ? FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED : 'number',
];
$form['cardinality_number'] = [
'#type' => 'number',
'#default_value' => $cardinality != FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED ? $cardinality : 1,
'#min' => 1,
'#title' => $this->t('Limit'),
'#title_display' => 'invisible',
'#size' => 2,
'#states' => [
'visible' => [
':input[name="field_storage[subform][cardinality]"]' => ['value' => 'number'],
],
'disabled' => [
':input[id="field_storage[subform][cardinality]"]' => ['value' => FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED],
],
],
];
}
return $form;
}
/**
* {@inheritdoc}
*/
protected function actions(array $form, FormStateInterface $form_state) {
return [];
}
/**
* Validates the cardinality.
*
* @param array $element
* The cardinality form render array.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The 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 ($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()]) && $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', $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' => $cardinality_number + 1, '@allowed' => $cardinality_number]));
}
}
}
/**
* {@inheritdoc}
*/
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', (int) $form_state->getValue('cardinality_number'));
}
return parent::buildEntity($form, $form_state);
}
/**
* {@inheritdoc}
*/
public function save(array $form, FormStateInterface $form_state) {
$this->entity->save();
}
/**
* Returns the cardinality enforced by the field type.
*
* Some field types choose to enforce a fixed cardinality. This method
* returns that cardinality or NULL if no cardinality has been enforced.
*
* @return int|null
* The enforced cardinality as an integer, or NULL if no cardinality is
* enforced.
*/
protected function getEnforcedCardinality() {
/** @var \Drupal\Core\Field\FieldTypePluginManager $field_type_manager */
$field_type_manager = \Drupal::service('plugin.manager.field.field_type');
$definition = $field_type_manager->getDefinition($this->entity->getType());
return $definition['cardinality'] ?? NULL;
}
}

View File

@ -0,0 +1,69 @@
<?php
namespace Drupal\field_ui\Form;
/**
* Provides common functionality for adding or re-using a field.
*/
trait FieldStorageCreationTrait {
/**
* Configures the field for the default form mode.
*
* @param string $field_name
* The field name.
* @param array[] $widget_settings
* (optional) Array of widget settings, keyed by form mode. Defaults to an
* empty array.
*
* @throws \Drupal\Core\Entity\EntityStorageException
*/
protected function configureEntityFormDisplay(string $field_name, array $widget_settings = []) {
// For a new field, only $mode = 'default' should be set. Use the
// preconfigured or default widget and settings. The field will not appear
// in other form modes until it is explicitly configured.
foreach ($widget_settings as $mode => $options) {
$form_display = $this->entityDisplayRepository->getFormDisplay($this->entityTypeId, $this->bundle, $mode);
if ($form_display->status()) {
$form_display->setComponent($field_name, $options)->save();
}
}
if (empty($widget_settings)) {
$this->entityDisplayRepository->getFormDisplay($this->entityTypeId, $this->bundle, 'default')
->setComponent($field_name, [])
->save();
}
}
/**
* Configures the field for the default view mode.
*
* @param string $field_name
* The field name.
* @param array[] $formatter_settings
* (optional) An array of settings, keyed by view mode. Only the 'type' key
* of the inner array is used, and the value should be the plugin ID of a
* formatter. Defaults to an empty array.
*
* @throws \Drupal\Core\Entity\EntityStorageException
*/
protected function configureEntityViewDisplay(string $field_name, array $formatter_settings = []) {
// For a new field, only $mode = 'default' should be set. Use the
// preconfigured or default formatter and settings. The field stays hidden
// for other view modes until it is explicitly configured.
foreach ($formatter_settings as $mode => $options) {
$view_display = $this->entityDisplayRepository->getViewDisplay($this->entityTypeId, $this->bundle, $mode);
if ($view_display->status()) {
$view_display->setComponent($field_name, $options)->save();
}
}
if (empty($formatter_settings)) {
$this->entityDisplayRepository->getViewDisplay($this->entityTypeId, $this->bundle)
->setComponent($field_name, [])
->save();
}
}
}

View File

@ -0,0 +1,352 @@
<?php
namespace Drupal\field_ui\Form;
use Drupal\Core\Entity\EntityDisplayRepositoryInterface;
use Drupal\Core\Entity\EntityFieldManagerInterface;
use Drupal\Core\Entity\EntityTypeBundleInfoInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Field\FieldTypePluginManagerInterface;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\field\FieldStorageConfigInterface;
use Drupal\field_ui\FieldUI;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Drupal\Core\StringTranslation\PluralTranslatableMarkup;
/**
* Provides a form for the "field storage" add page.
*
* @internal
*/
class FieldStorageReuseForm extends FormBase {
use FieldStorageCreationTrait;
/**
* The name of the entity type.
*
* @var string
*/
protected string $entityTypeId;
/**
* The entity bundle.
*
* @var string
*/
protected string $bundle;
/**
* Constructs a new FieldStorageReuseForm object.
*
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager
* The entity type manager.
* @param \Drupal\Core\Field\FieldTypePluginManagerInterface $fieldTypePluginManager
* The field type plugin manager.
* @param \Drupal\Core\Entity\EntityFieldManagerInterface $entityFieldManager
* The entity field manager.
* @param \Drupal\Core\Entity\EntityDisplayRepositoryInterface $entityDisplayRepository
* The entity display repository.
* @param \Drupal\Core\Entity\EntityTypeBundleInfoInterface $bundleInfoService
* The bundle info service.
*/
public function __construct(
protected EntityTypeManagerInterface $entityTypeManager,
protected FieldTypePluginManagerInterface $fieldTypePluginManager,
protected EntityFieldManagerInterface $entityFieldManager,
protected EntityDisplayRepositoryInterface $entityDisplayRepository,
protected EntityTypeBundleInfoInterface $bundleInfoService,
) {}
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'field_ui_field_storage_reuse_form';
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('entity_type.manager'),
$container->get('plugin.manager.field.field_type'),
$container->get('entity_field.manager'),
$container->get('entity_display.repository'),
$container->get('entity_type.bundle.info')
);
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state, $entity_type_id = NULL, $bundle = NULL) {
if (!$form_state->get('entity_type_id')) {
$form_state->set('entity_type_id', $entity_type_id);
}
if (!$form_state->get('bundle')) {
$form_state->set('bundle', $bundle);
}
$this->entityTypeId = $form_state->get('entity_type_id');
$this->bundle = $form_state->get('bundle');
$form['text'] = [
'#plain_text' => $this->t("You can re-use a field from other sub-types of the same entity type. Re-using a field creates another usage of the same field storage."),
];
$form['search'] = [
'#type' => 'search',
'#title' => $this->t('Filter by field or field type'),
'#attributes' => [
'class' => ['js-table-filter-text'],
'data-table' => '.js-reuse-table',
'autocomplete' => 'off',
],
];
$form['add'] = [
'#type' => 'container',
'#attributes' => ['class' => ['form--inline', 'clearfix']],
];
$bundles = $this->bundleInfoService->getAllBundleInfo();
$existing_field_storage_options = $this->getExistingFieldStorageOptions();
$rows = [];
foreach ($existing_field_storage_options as $field) {
$field_bundles = $field['field_storage']->getBundles();
$summary = $this->fieldTypePluginManager->getStorageSettingsSummary($field['field_storage']);
$cardinality = $field['field_storage']->getCardinality();
$readable_cardinality = $cardinality === -1 ? $this->t('Unlimited') : new PluralTranslatableMarkup(1, 'Single value', 'Multiple values: @cardinality', ['@cardinality' => $cardinality]);
// Remove empty values.
$list = array_filter([...$summary, $readable_cardinality]);
$settings_summary = [
'#theme' => 'item_list',
'#items' => $list,
'#attributes' => [
'class' => ['field-settings-summary-cell'],
],
];
$bundle_label_arr = [];
foreach ($field_bundles as $bundle) {
$bundle_label_arr[] = $bundles[$this->entityTypeId][$bundle]['label'];
}
sort($bundle_label_arr);
// Combine bundles to be a single string separated by a comma.
$settings_summary['#items'][] = $this->t('Used in: @list', ['@list' => implode(", ", $bundle_label_arr)]);
$row = [
'#attributes' => [
'data-field-id' => $field["field_name"],
],
'field' => [
'#plain_text' => $field['field_name'],
'#type' => 'item',
],
'field_type' => [
'#plain_text' => $field['field_type'],
'#type' => 'item',
],
'summary' => $settings_summary,
'operations' => [
'#type' => 'submit',
'#name' => $field['field_name'],
'#value' => $this->t('Re-use'),
'#wrapper_attributes' => [
'colspan' => 5,
],
'#attributes' => [
'class' => [
'button',
'button--small',
'use-ajax',
],
'aria-label' => $this->t('Reuse @field_name', ['@field_name' => $field['field_name']]),
'data-dialog-type' => 'modal',
],
'#submit' => [
'callback' => [$this, 'reuseCallback'],
],
],
];
$rows[] = $row;
}
// Sort rows by field name.
ksort($rows);
$form['add']['table'] = [
'#type' => 'table',
'#header' => [
$this->t('Field'),
$this->t('Field Type'),
$this->t('Summary'),
$this->t('Operations'),
],
'#attributes' => [
'class' => ['js-reuse-table'],
],
];
$form['add']['table'] += $rows;
$form['#attached']['library'][] = 'field_ui/drupal.field_ui';
return $form;
}
/**
* Returns an array of existing field storages that can be added to a bundle.
*
* @return array
* An array of existing field storages keyed by name.
*/
protected function getExistingFieldStorageOptions(): array {
$options = [];
// Load the field_storages and build the list of options.
$field_types = $this->fieldTypePluginManager->getDefinitions();
foreach ($this->entityFieldManager->getFieldStorageDefinitions($this->entityTypeId) as $field_name => $field_storage) {
// Do not show:
// - non-configurable field storages,
// - locked field storages,
// - field storages that should not be added via user interface,
// - field storages that already have a field in the bundle.
$field_type = $field_storage->getType();
if ($field_storage instanceof FieldStorageConfigInterface
&& !$field_storage->isLocked()
&& empty($field_types[$field_type]['no_ui'])
&& !in_array($this->bundle, $field_storage->getBundles(), TRUE)) {
$options[$field_name] = [
'field_type' => $field_types[$field_type]['label'],
'field_name' => $field_name,
'field_storage' => $field_storage,
];
}
}
asort($options);
return $options;
}
/**
* Callback function to handle re-using an existing field.
*
* @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.
*
* @throws \Exception
* Thrown when there is an error re-using the field.
*/
public function reuseCallback(array $form, FormStateInterface $form_state) {
$entity_type = $this->entityTypeManager->getDefinition($this->entityTypeId);
$field_name = $form_state->getTriggeringElement()['#name'];
// Get settings from existing configuration.
$default_options = $this->getExistingFieldDefaults($field_name);
$fields = $this->entityTypeManager->getStorage('field_config')->getQuery()
->accessCheck()
->condition('entity_type', $this->entityTypeId)
->condition('field_name', $field_name)
->execute();
$field = $fields ? $this->entityTypeManager->getStorage('field_config')->load(reset($fields)) : NULL;
// Have a default label in case a field storage doesn't have any fields.
$existing_storage_label = $field ? $field->label() : $field_name;
try {
$field = $this->entityTypeManager->getStorage('field_config')->create([
...$default_options['field_config'] ?? [],
'field_name' => $field_name,
'entity_type' => $this->entityTypeId,
'bundle' => $this->bundle,
'label' => $existing_storage_label,
// Field translatability should be explicitly enabled by the users.
'translatable' => FALSE,
]);
$field->save();
// Configure the display modes.
$this->configureEntityFormDisplay($field_name, $default_options['entity_form_display'] ?? []);
$this->configureEntityViewDisplay($field_name, $default_options['entity_view_display'] ?? []);
// Store new field information for any additional submit handlers.
$form_state->set(['fields_added', '_add_existing_field'], $field_name);
$form_state->setRedirect("entity.field_config.{$this->entityTypeId}_field_edit_form", array_merge(FieldUI::getRouteBundleParameter($entity_type, $this->bundle), ['field_config' => "$this->entityTypeId.$this->bundle.$field_name"]));
}
catch (\Exception $e) {
$this->messenger()->addError($this->t('There was a problem reusing field %label: @message', [
'%label' => $existing_storage_label,
'@message' => $e->getMessage(),
]));
}
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
// This is no-op because there is no single submit action on the form. All
// the actions are handled by a callback attached to individual buttons.
// @see \Drupal\field_ui\Form\FieldStorageReuseForm::reuseCallback.
}
/**
* Get default options from an existing field and bundle.
*
* @param string $field_name
* The machine name of the field.
*
* @return array
* An array of settings with keys 'field_config', 'entity_form_display', and
* 'entity_view_display' if these are defined for an existing field
* instance. If the field is not defined for the specified bundle (or for
* any bundle if $existing_bundle is omitted) then return an empty array.
*/
protected function getExistingFieldDefaults(string $field_name): array {
$default_options = [];
$field_map = $this->entityFieldManager->getFieldMap();
if (empty($field_map[$this->entityTypeId][$field_name]['bundles'])) {
return [];
}
$bundles = $field_map[$this->entityTypeId][$field_name]['bundles'];
// Sort bundles to ensure deterministic behavior.
sort($bundles);
$existing_bundle = reset($bundles);
// Copy field configuration.
$existing_field = $this->entityFieldManager->getFieldDefinitions($this->entityTypeId, $existing_bundle)[$field_name];
$default_options['field_config'] = [
'description' => $existing_field->getDescription(),
'settings' => $existing_field->getSettings(),
'required' => $existing_field->isRequired(),
'default_value' => $existing_field->getDefaultValueLiteral(),
'default_value_callback' => $existing_field->getDefaultValueCallback(),
];
// Copy form and view mode configuration.
$properties = [
'targetEntityType' => $this->entityTypeId,
'bundle' => $existing_bundle,
];
/** @var \Drupal\Core\Entity\Display\EntityFormDisplayInterface $existing_forms */
$existing_forms = $this->entityTypeManager->getStorage('entity_form_display')->loadByProperties($properties);
foreach ($existing_forms as $form) {
if ($settings = $form->getComponent($field_name)) {
$default_options['entity_form_display'][$form->getMode()] = $settings;
}
}
/** @var \Drupal\Core\Entity\Display\EntityViewDisplayInterface $existing_views */
$existing_views = $this->entityTypeManager->getStorage('entity_view_display')->loadByProperties($properties);
foreach ($existing_views as $view) {
if ($settings = $view->getComponent($field_name)) {
$default_options['entity_view_display'][$view->getMode()] = $settings;
}
}
return $default_options;
}
}

View File

@ -0,0 +1,278 @@
<?php
namespace Drupal\field_ui\Hook;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\field_ui\Plugin\Derivative\FieldUiLocalTask;
use Drupal\Core\Entity\EntityFormModeInterface;
use Drupal\Core\Entity\EntityViewModeInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\field_ui\Form\FieldStorageConfigEditForm;
use Drupal\field_ui\Form\FieldConfigEditForm;
use Drupal\Core\Url;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Hook\Attribute\Hook;
/**
* Hook implementations for field_ui.
*/
class FieldUiHooks {
use StringTranslationTrait;
/**
* Implements hook_help().
*/
#[Hook('help')]
public function help($route_name, RouteMatchInterface $route_match): ?string {
switch ($route_name) {
case 'help.page.field_ui':
$output = '';
$output .= '<h2>' . $this->t('About') . '</h2>';
$output .= '<p>' . $this->t('The Field UI module provides an administrative user interface (UI) for managing and displaying fields. Fields can be attached to most content entity sub-types. Different field types, widgets, and formatters are provided by the modules installed on your site, and managed by the Field module. For background information and terminology related to fields and entities, see the <a href=":field">Field module help page</a>. For more information about the Field UI, see the <a href=":field_ui_docs">online documentation for the Field UI module</a>.', [
':field' => Url::fromRoute('help.page', [
'name' => 'field',
])->toString(),
':field_ui_docs' => 'https://www.drupal.org/docs/8/core/modules/field-ui',
]) . '</p>';
$output .= '<h2>' . $this->t('Uses') . '</h2>';
$output .= '<dl>';
$output .= '<dt>' . $this->t('Creating a field') . '</dt>';
$output .= '<dd>' . $this->t('On the <em>Manage fields</em> page for your entity type or sub-type, you can add, configure, and delete fields for that entity type or sub-type. Each field has a <em>machine name</em>, which is used internally to identify the field and must be unique across an entity type; once a field is created, you cannot change the machine name. Most fields have two types of settings. The field-level settings depend on the field type, and affect how the data in the field is stored. Once they are set, they can no longer be changed; examples include how many data values are allowed for the field and where files are stored. The sub-type-level settings are specific to each entity sub-type the field is used on, and they can be changed later; examples include the field label, help text, default value, and whether the field is required or not. You can return to these settings by choosing the <em>Edit</em> link for the field from the <em>Manage fields</em> page.');
$output .= '<dt>' . $this->t('Re-using fields') . '</dt>';
$output .= '<dd>' . $this->t('Once you have created a field, you can use it again in other sub-types of the same entity type. For instance, if you create a field for the article content type, you can also use it for the page content type, but you cannot use it for content blocks or taxonomy terms. If there are fields available for re-use, after clicking <em>Add field</em> from the <em>Manage fields</em> page, you will see a list of available fields for re-use. After selecting a field for re-use, you can configure the sub-type-level settings.') . '</dd>';
$output .= '<dt>' . $this->t('Configuring field editing') . '</dt>';
$output .= '<dd>' . $this->t('On the <em>Manage form display</em> page of your entity type or sub-type, you can configure how the field data is edited by default and in each form mode. If your entity type has multiple form modes (on most sites, most entities do not), you can toggle between the form modes at the top of the page, and you can toggle whether each form mode uses the default settings or custom settings in the <em>Custom display settings</em> section. For each field in each form mode, you can select the widget to use for editing; some widgets have additional configuration options, such as the size for a text field, and these can be edited using the Edit button (which looks like a wheel). You can also change the order of the fields on the form. You can exclude a field from a form by choosing <em>Hidden</em> from the widget drop-down list, or by dragging it into the <em>Disabled</em> section.') . '</dd>';
$output .= '<dt>' . $this->t('Configuring field display') . '</dt>';
$output .= '<dd>' . $this->t('On the <em>Manage display</em> page of your entity type or sub-type, you can configure how each field is displayed by default and in each view mode. If your entity type has multiple view modes, you can toggle between the view modes at the top of the page, and you can toggle whether each view mode uses the default settings or custom settings in the <em>Custom display settings</em> section. For each field in each view mode, you can choose whether and how to display the label of the field from the <em>Label</em> drop-down list. You can also select the formatter to use for display; some formatters have configuration options, which you can edit using the Edit button (which looks like a wheel). You can also change the display order of fields. You can exclude a field from a specific view mode by choosing <em>Hidden</em> from the formatter drop-down list, or by dragging it into the <em>Disabled</em> section.') . '</dd>';
$output .= '<dt>' . $this->t('Configuring view and form modes') . '</dt>';
$output .= '<dd>' . $this->t('You can add, edit, and delete view modes for entities on the <a href=":view_modes">View modes page</a>, and you can add, edit, and delete form modes for entities on the <a href=":form_modes">Form modes page</a>. Once you have defined a view mode or form mode for an entity type, it will be available on the Manage display or Manage form display page for each sub-type of that entity.', [
':view_modes' => Url::fromRoute('entity.entity_view_mode.collection')->toString(),
':form_modes' => Url::fromRoute('entity.entity_form_mode.collection')->toString(),
]) . '</dd>';
$output .= '<dt>' . $this->t('Listing fields') . '</dt>';
$output .= '<dd>' . $this->t('There are two reports available that list the fields defined on your site. The <a href=":entity-list" title="Entities field list report">Entities</a> report lists all your fields, showing the field machine names, types, and the entity types or sub-types they are used on (each sub-type links to the Manage fields page). If the <a href=":views">Views</a> and <a href=":views-ui">Views UI</a> modules are installed, the <a href=":views-list" title="Used in views field list report">Used in views</a> report lists each field that is used in a view, with a link to edit that view.', [
':entity-list' => Url::fromRoute('entity.field_storage_config.collection')->toString(),
':views-list' => \Drupal::moduleHandler()->moduleExists('views_ui') ? Url::fromRoute('views_ui.reports_fields')->toString() : '#',
':views' => \Drupal::moduleHandler()->moduleExists('views') ? Url::fromRoute('help.page', [
'name' => 'views',
])->toString() : '#',
':views-ui' => \Drupal::moduleHandler()->moduleExists('views_ui') ? Url::fromRoute('help.page', [
'name' => 'views_ui',
])->toString() : '#',
]) . '</dd>';
$output .= '</dl>';
return $output;
case 'entity.field_storage_config.collection':
return '<p>' . $this->t('This list shows all fields currently in use for easy reference.') . '</p>';
}
return NULL;
}
/**
* Implements hook_theme().
*/
#[Hook('theme')]
public function theme() : array {
return [
'field_ui_table' => [
'variables' => [
'header' => NULL,
'rows' => NULL,
'footer' => NULL,
'attributes' => [],
'caption' => NULL,
'colgroups' => [],
'sticky' => FALSE,
'responsive' => TRUE,
'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',
],
];
}
/**
* Implements hook_entity_type_build().
*/
#[Hook('entity_type_build')]
public function entityTypeBuild(array &$entity_types): void {
/** @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');
$entity_types['entity_form_display']->setFormClass('edit', 'Drupal\field_ui\Form\EntityFormDisplayEditForm');
$entity_types['entity_view_display']->setFormClass('edit', 'Drupal\field_ui\Form\EntityViewDisplayEditForm');
$form_mode = $entity_types['entity_form_mode'];
$form_mode->setListBuilderClass('Drupal\field_ui\EntityFormModeListBuilder');
$form_mode->setFormClass('add', 'Drupal\field_ui\Form\EntityFormModeAddForm');
$form_mode->setFormClass('edit', 'Drupal\field_ui\Form\EntityDisplayModeEditForm');
$form_mode->setFormClass('delete', 'Drupal\field_ui\Form\EntityDisplayModeDeleteForm');
$form_mode->set('admin_permission', 'administer display modes');
$form_mode->setLinkTemplate('delete-form', '/admin/structure/display-modes/form/manage/{entity_form_mode}/delete');
$form_mode->setLinkTemplate('edit-form', '/admin/structure/display-modes/form/manage/{entity_form_mode}');
$form_mode->setLinkTemplate('add-form', '/admin/structure/display-modes/form/add/{entity_type_id}');
$form_mode->setLinkTemplate('collection', '/admin/structure/display-modes/form');
$view_mode = $entity_types['entity_view_mode'];
$view_mode->setListBuilderClass('Drupal\field_ui\EntityDisplayModeListBuilder');
$view_mode->setFormClass('add', 'Drupal\field_ui\Form\EntityDisplayModeAddForm');
$view_mode->setFormClass('edit', 'Drupal\field_ui\Form\EntityDisplayModeEditForm');
$view_mode->setFormClass('delete', 'Drupal\field_ui\Form\EntityDisplayModeDeleteForm');
$view_mode->set('admin_permission', 'administer display modes');
$view_mode->setLinkTemplate('delete-form', '/admin/structure/display-modes/view/manage/{entity_view_mode}/delete');
$view_mode->setLinkTemplate('edit-form', '/admin/structure/display-modes/view/manage/{entity_view_mode}');
$view_mode->setLinkTemplate('add-form', '/admin/structure/display-modes/view/add/{entity_type_id}');
$view_mode->setLinkTemplate('collection', '/admin/structure/display-modes/view');
}
/**
* Implements hook_entity_bundle_create().
*/
#[Hook('entity_bundle_create')]
public function entityBundleCreate($entity_type, $bundle): void {
// When a new bundle is created, the menu needs to be rebuilt to add our
// menu item tabs.
\Drupal::service('router.builder')->setRebuildNeeded();
}
/**
* Implements hook_entity_operation().
*/
#[Hook('entity_operation')]
public function entityOperation(EntityInterface $entity): array {
$operations = [];
$info = $entity->getEntityType();
// Add manage fields and display links if this entity type is the bundle
// of another and that type has field UI enabled.
if (($bundle_of = $info->getBundleOf()) && \Drupal::entityTypeManager()->getDefinition($bundle_of)->get('field_ui_base_route')) {
$account = \Drupal::currentUser();
if ($account->hasPermission('administer ' . $bundle_of . ' fields')) {
$operations['manage-fields'] = [
'title' => $this->t('Manage fields'),
'weight' => 15,
'url' => Url::fromRoute("entity.{$bundle_of}.field_ui_fields", [
$entity->getEntityTypeId() => $entity->id(),
]),
];
}
if ($account->hasPermission('administer ' . $bundle_of . ' form display')) {
$operations['manage-form-display'] = [
'title' => $this->t('Manage form display'),
'weight' => 20,
'url' => Url::fromRoute("entity.entity_form_display.{$bundle_of}.default", [
$entity->getEntityTypeId() => $entity->id(),
]),
];
}
if ($account->hasPermission('administer ' . $bundle_of . ' display')) {
$operations['manage-display'] = [
'title' => $this->t('Manage display'),
'weight' => 25,
'url' => Url::fromRoute("entity.entity_view_display.{$bundle_of}.default", [
$entity->getEntityTypeId() => $entity->id(),
]),
];
}
}
return $operations;
}
/**
* Implements hook_ENTITY_TYPE_presave().
*/
#[Hook('entity_view_mode_presave')]
public function entityViewModePresave(EntityViewModeInterface $view_mode): void {
\Drupal::service('router.builder')->setRebuildNeeded();
}
/**
* Implements hook_ENTITY_TYPE_presave().
*/
#[Hook('entity_form_mode_presave')]
public function entityFormModePresave(EntityFormModeInterface $form_mode): void {
\Drupal::service('router.builder')->setRebuildNeeded();
}
/**
* Implements hook_ENTITY_TYPE_delete().
*/
#[Hook('entity_view_mode_delete')]
public function entityViewModeDelete(EntityViewModeInterface $view_mode): void {
\Drupal::service('router.builder')->setRebuildNeeded();
}
/**
* Implements hook_ENTITY_TYPE_delete().
*/
#[Hook('entity_form_mode_delete')]
public function entityFormModeDelete(EntityFormModeInterface $form_mode): void {
\Drupal::service('router.builder')->setRebuildNeeded();
}
/**
* Implements hook_local_tasks_alter().
*/
#[Hook('local_tasks_alter')]
public function localTasksAlter(&$local_tasks): void {
$container = \Drupal::getContainer();
$local_task = FieldUiLocalTask::create($container, 'field_ui.fields');
$local_task->alterLocalTasks($local_tasks);
}
/**
* Implements hook_form_FORM_ID_alter() for 'field_ui_field_storage_add_form'.
*/
#[Hook('form_field_ui_field_storage_add_form_alter')]
public function formFieldUiFieldStorageAddFormAlter(array &$form) : void {
$optgroup = (string) $this->t('Reference');
// Move the "Entity reference" option to the end of the list and rename it
// to "Other".
unset($form['add']['new_storage_type']['#options'][$optgroup]['entity_reference']);
$form['add']['new_storage_type']['#options'][$optgroup]['entity_reference'] = $this->t('Other…');
}
/**
* Implements hook_form_alter().
*
* Adds a button 'Save and manage fields' to forms.
*
* @see \Drupal\node\Form\NodeTypeForm
* @see \Drupal\comment\CommentTypeForm
* @see \Drupal\media\MediaTypeForm
* @see \Drupal\block_content\BlockContentTypeForm
* @see field_ui_form_manage_field_form_submit()
*/
#[Hook('form_alter')]
public function formAlter(&$form, FormStateInterface $form_state, $form_id) : void {
$forms = [
'node_type_add_form',
'comment_type_add_form',
'media_type_add_form',
'block_content_type_add_form',
];
if (!in_array($form_id, $forms)) {
return;
}
if ($form_state->getFormObject()->getEntity()->isNew()) {
$form['actions']['save_continue'] = $form['actions']['submit'];
unset($form['actions']['submit']['#button_type']);
$form['actions']['save_continue']['#value'] = $this->t('Save and manage fields');
$form['actions']['save_continue']['#weight'] = $form['actions']['save_continue']['#weight'] - 5;
$form['actions']['save_continue']['#submit'][] = 'field_ui_form_manage_field_form_submit';
}
}
}

View File

@ -0,0 +1,101 @@
<?php
namespace Drupal\field_ui\Plugin\Derivative;
use Drupal\Component\Plugin\Derivative\DeriverBase;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Plugin\Discovery\ContainerDeriverInterface;
use Drupal\Core\Routing\RouteProviderInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Drupal\Component\Serialization\Json;
/**
* Provides local action definitions for all entity bundles.
*/
class FieldUiLocalAction extends DeriverBase implements ContainerDeriverInterface {
use StringTranslationTrait;
/**
* The route provider to load routes by name.
*/
protected RouteProviderInterface $routeProvider;
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* Constructs a FieldUiLocalAction object.
*
* @param \Drupal\Core\Routing\RouteProviderInterface $route_provider
* The route provider to load routes by name.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
*/
public function __construct(RouteProviderInterface $route_provider, EntityTypeManagerInterface $entity_type_manager) {
$this->routeProvider = $route_provider;
$this->entityTypeManager = $entity_type_manager;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, $base_plugin_id) {
return new static(
$container->get('router.route_provider'),
$container->get('entity_type.manager')
);
}
/**
* {@inheritdoc}
*/
public function getDerivativeDefinitions($base_plugin_definition) {
$this->derivatives = [];
foreach ($this->entityTypeManager->getDefinitions() as $entity_type_id => $entity_type) {
if ($entity_type->get('field_ui_base_route')) {
$this->derivatives["field_storage_config_add_$entity_type_id"] = [
'route_name' => "field_ui.field_storage_config_add_$entity_type_id",
'title' => $this->t('Create a new field'),
'appears_on' => ["entity.$entity_type_id.field_ui_fields"],
'options' => [
'attributes' => [
'class' => ['use-ajax', 'button'],
'data-dialog-type' => 'modal',
'data-dialog-options' => Json::encode([
'width' => '1100',
]),
],
],
];
$this->derivatives["field_storage_config_reuse_$entity_type_id"] = [
'route_name' => "field_ui.field_storage_config_reuse_$entity_type_id",
'title' => $this->t('Re-use an existing field'),
'appears_on' => ["entity.$entity_type_id.field_ui_fields"],
'options' => [
'attributes' => [
'class' => ['use-ajax', 'button'],
'data-dialog-type' => 'modal',
'data-dialog-options' => Json::encode([
'width' => '1100',
]),
],
],
];
}
}
foreach ($this->derivatives as &$entry) {
$entry += $base_plugin_definition;
}
return $this->derivatives;
}
}

View File

@ -0,0 +1,201 @@
<?php
namespace Drupal\field_ui\Plugin\Derivative;
use Drupal\Core\Entity\EntityDisplayRepositoryInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Component\Plugin\Derivative\DeriverBase;
use Drupal\Core\Plugin\Discovery\ContainerDeriverInterface;
use Drupal\Core\Routing\RouteProviderInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\StringTranslation\TranslationInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides local task definitions for all entity bundles.
*/
class FieldUiLocalTask extends DeriverBase implements ContainerDeriverInterface {
use StringTranslationTrait;
/**
* The route provider.
*
* @var \Drupal\Core\Routing\RouteProviderInterface
*/
protected $routeProvider;
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* The entity display repository.
*
* @var \Drupal\Core\Entity\EntityDisplayRepositoryInterface
*/
protected $entityDisplayRepository;
/**
* Creates a FieldUiLocalTask object.
*
* @param \Drupal\Core\Routing\RouteProviderInterface $route_provider
* The route provider.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
* @param \Drupal\Core\StringTranslation\TranslationInterface $string_translation
* The translation manager.
* @param \Drupal\Core\Entity\EntityDisplayRepositoryInterface $entity_display_repository
* The entity display repository.
*/
public function __construct(RouteProviderInterface $route_provider, EntityTypeManagerInterface $entity_type_manager, TranslationInterface $string_translation, EntityDisplayRepositoryInterface $entity_display_repository) {
$this->routeProvider = $route_provider;
$this->entityTypeManager = $entity_type_manager;
$this->stringTranslation = $string_translation;
$this->entityDisplayRepository = $entity_display_repository;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, $base_plugin_id) {
return new static(
$container->get('router.route_provider'),
$container->get('entity_type.manager'),
$container->get('string_translation'),
$container->get('entity_display.repository')
);
}
/**
* {@inheritdoc}
*/
public function getDerivativeDefinitions($base_plugin_definition) {
$this->derivatives = [];
foreach ($this->entityTypeManager->getDefinitions() as $entity_type_id => $entity_type) {
if ($entity_type->get('field_ui_base_route')) {
$this->derivatives["overview_$entity_type_id"] = [
'route_name' => "entity.$entity_type_id.field_ui_fields",
'weight' => 1,
'title' => $this->t('Manage fields'),
'base_route' => "entity.$entity_type_id.field_ui_fields",
];
// 'Manage form display' tab.
$this->derivatives["form_display_overview_$entity_type_id"] = [
'route_name' => "entity.entity_form_display.$entity_type_id.default",
'weight' => 2,
'title' => $this->t('Manage form display'),
'base_route' => "entity.$entity_type_id.field_ui_fields",
];
// 'Manage display' tab.
$this->derivatives["display_overview_$entity_type_id"] = [
'route_name' => "entity.entity_view_display.$entity_type_id.default",
'weight' => 3,
'title' => $this->t('Manage display'),
'base_route' => "entity.$entity_type_id.field_ui_fields",
];
// Field edit tab.
$this->derivatives["field_edit_$entity_type_id"] = [
'route_name' => "entity.field_config.{$entity_type_id}_field_edit_form",
'title' => $this->t('Edit'),
'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
// administrator settings, each bundle has a different set of view
// modes available for customization. So we define menu items for all
// view modes, and use a route requirement to determine which ones are
// actually visible for a given bundle.
$this->derivatives['field_form_display_default_' . $entity_type_id] = [
'title' => 'Default',
'route_name' => "entity.entity_form_display.$entity_type_id.default",
'parent_id' => "field_ui.fields:form_display_overview_$entity_type_id",
'weight' => -1,
];
$this->derivatives['field_display_default_' . $entity_type_id] = [
'title' => 'Default',
'route_name' => "entity.entity_view_display.$entity_type_id.default",
'parent_id' => "field_ui.fields:display_overview_$entity_type_id",
'weight' => -1,
];
// One local task for each form mode.
$form_modes = $this->entityDisplayRepository->getFormModes($entity_type_id);
// Sort all form modes by title.
$form_modes_titles = array_values(array_map(fn($item) => (string) $item['label'], $form_modes));
sort($form_modes_titles, SORT_NATURAL);
foreach ($form_modes as $form_mode => $form_mode_info) {
$this->derivatives['field_form_display_' . $form_mode . '_' . $entity_type_id] = [
'title' => $form_mode_info['label'],
'route_name' => "entity.entity_form_display.$entity_type_id.form_mode",
'route_parameters' => [
'form_mode_name' => $form_mode,
],
'parent_id' => "field_ui.fields:form_display_overview_$entity_type_id",
'weight' => array_flip($form_modes_titles)[(string) $form_mode_info['label']],
'cache_tags' => $this->entityTypeManager->getDefinition('entity_form_display')->getListCacheTags(),
];
}
// One local task for each view mode.
$view_modes = $this->entityDisplayRepository->getViewModes($entity_type_id);
// Sort all view modes by title.
$view_modes_titles = array_values(array_map(fn($item) => (string) $item['label'], $view_modes));
sort($view_modes_titles, SORT_NATURAL);
foreach ($view_modes as $view_mode => $form_mode_info) {
$this->derivatives['field_display_' . $view_mode . '_' . $entity_type_id] = [
'title' => $form_mode_info['label'],
'route_name' => "entity.entity_view_display.$entity_type_id.view_mode",
'route_parameters' => [
'view_mode_name' => $view_mode,
],
'parent_id' => "field_ui.fields:display_overview_$entity_type_id",
'weight' => array_flip($view_modes_titles)[(string) $form_mode_info['label']],
'cache_tags' => $this->entityTypeManager->getDefinition('entity_view_display')->getListCacheTags(),
];
}
}
}
foreach ($this->derivatives as &$entry) {
$entry += $base_plugin_definition;
}
return $this->derivatives;
}
/**
* Alters the base_route definition for field_ui local tasks.
*
* @param array $local_tasks
* An array of local tasks plugin definitions, keyed by plugin ID.
*/
public function alterLocalTasks(&$local_tasks) {
foreach ($this->entityTypeManager->getDefinitions() as $entity_type_id => $entity_type) {
if ($route_name = $entity_type->get('field_ui_base_route')) {
$local_tasks["field_ui.fields:overview_$entity_type_id"]['base_route'] = $route_name;
$local_tasks["field_ui.fields:form_display_overview_$entity_type_id"]['base_route'] = $route_name;
$local_tasks["field_ui.fields:display_overview_$entity_type_id"]['base_route'] = $route_name;
$local_tasks["field_ui.fields:field_form_display_default_$entity_type_id"]['base_route'] = $route_name;
$local_tasks["field_ui.fields:field_display_default_$entity_type_id"]['base_route'] = $route_name;
foreach ($this->entityDisplayRepository->getFormModes($entity_type_id) as $form_mode => $form_mode_info) {
$local_tasks['field_ui.fields:field_form_display_' . $form_mode . '_' . $entity_type_id]['base_route'] = $route_name;
}
foreach ($this->entityDisplayRepository->getViewModes($entity_type_id) as $view_mode => $form_mode_info) {
$local_tasks['field_ui.fields:field_display_' . $view_mode . '_' . $entity_type_id]['base_route'] = $route_name;
}
}
}
}
}

View File

@ -0,0 +1,211 @@
<?php
namespace Drupal\field_ui\Routing;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Routing\RouteSubscriberBase;
use Drupal\Core\Routing\RoutingEvents;
use Drupal\field_ui\Controller\FieldConfigAddController;
use Drupal\field_ui\Controller\FieldStorageAddController;
use Symfony\Component\Routing\Route;
use Symfony\Component\Routing\RouteCollection;
/**
* Subscriber for Field UI routes.
*/
class RouteSubscriber extends RouteSubscriberBase {
/**
* The entity type manager service.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* Constructs a RouteSubscriber object.
*
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager service.
*/
public function __construct(EntityTypeManagerInterface $entity_type_manager) {
$this->entityTypeManager = $entity_type_manager;
}
/**
* {@inheritdoc}
*/
protected function alterRoutes(RouteCollection $collection) {
foreach ($this->entityTypeManager->getDefinitions() as $entity_type_id => $entity_type) {
if ($route_name = $entity_type->get('field_ui_base_route')) {
// Try to get the route from the current collection.
if (!$entity_route = $collection->get($route_name)) {
continue;
}
$path = $entity_route->getPath();
$options = $entity_route->getOptions();
if ($bundle_entity_type = $entity_type->getBundleEntityType()) {
$options['parameters'][$bundle_entity_type] = [
'type' => 'entity:' . $bundle_entity_type,
];
}
// Special parameter used to easily recognize all Field UI routes.
$options['_field_ui'] = TRUE;
$defaults = [
'entity_type_id' => $entity_type_id,
];
// If the entity type has no bundles and it doesn't use {bundle} in its
// admin path, use the entity type.
if (!str_contains($path, '{bundle}')) {
$defaults['bundle'] = !$entity_type->hasKey('bundle') ? $entity_type_id : '';
}
$route = new Route(
"$path/fields/{field_config}",
[
'_entity_form' => 'field_config.edit',
'_title_callback' => '\Drupal\field_ui\Form\FieldConfigEditForm::getTitle',
] + $defaults,
['_entity_access' => 'field_config.update'],
$options
);
$collection->add("entity.field_config.{$entity_type_id}_field_edit_form", $route);
$route = new Route(
"$path/fields/{field_config}/delete",
['_entity_form' => 'field_config.delete'] + $defaults,
['_entity_access' => 'field_config.delete'],
$options
);
$collection->add("entity.field_config.{$entity_type_id}_field_delete_form", $route);
$route = new Route(
"$path/fields",
[
'_controller' => '\Drupal\field_ui\Controller\FieldConfigListController::listing',
'_title' => 'Manage fields',
] + $defaults,
['_permission' => 'administer ' . $entity_type_id . ' fields'],
$options
);
$collection->add("entity.{$entity_type_id}.field_ui_fields", $route);
$route = new Route(
"$path/fields/reset-add-field/{field_name}",
[
'_controller' => FieldStorageAddController::class . '::resetField',
'_title' => 'Add field',
] + $defaults,
[
'_permission' => 'administer ' . $entity_type_id . ' fields',
'_csrf_token' => 'TRUE',
],
$options
);
$collection->add("field_ui.field_storage_config_reset_add_$entity_type_id", $route);
$route = new Route(
"$path/fields/add-field",
[
'_controller' => FieldStorageAddController::class . '::getFieldSelectionLinks',
'_title' => 'Add field',
] + $defaults,
['_permission' => 'administer ' . $entity_type_id . ' fields'],
$options
);
$collection->add("field_ui.field_storage_config_add_$entity_type_id", $route);
$route = new Route(
"$path/fields/add-field/{selected_field_type}/{display_as_group}",
[
'_form' => '\Drupal\field_ui\Form\FieldStorageAddForm',
'_title' => 'Add Sub-field',
] + $defaults,
['_permission' => 'administer ' . $entity_type_id . ' fields'],
$options
);
$collection->add("field_ui.field_storage_config_add_sub_$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",
[
'_form' => '\Drupal\field_ui\Form\FieldStorageReuseForm',
'_title' => 'Re-use an existing field',
] + $defaults,
['_field_ui_field_reuse_access' => 'administer ' . $entity_type_id . ' fields'],
$options
);
$collection->add("field_ui.field_storage_config_reuse_$entity_type_id", $route);
$route = new Route(
"$path/form-display",
[
'_entity_form' => 'entity_form_display.edit',
'_title' => 'Manage form display',
'form_mode_name' => 'default',
] + $defaults,
['_field_ui_form_mode_access' => 'administer ' . $entity_type_id . ' form display'],
$options
);
$collection->add("entity.entity_form_display.{$entity_type_id}.default", $route);
$route = new Route(
"$path/form-display/{form_mode_name}",
[
'_entity_form' => 'entity_form_display.edit',
'_title' => 'Manage form display',
] + $defaults,
['_field_ui_form_mode_access' => 'administer ' . $entity_type_id . ' form display'],
$options
);
$collection->add("entity.entity_form_display.{$entity_type_id}.form_mode", $route);
$route = new Route(
"$path/display",
[
'_entity_form' => 'entity_view_display.edit',
'_title' => 'Manage display',
'view_mode_name' => 'default',
] + $defaults,
['_field_ui_view_mode_access' => 'administer ' . $entity_type_id . ' display'],
$options
);
$collection->add("entity.entity_view_display.{$entity_type_id}.default", $route);
$route = new Route(
"$path/display/{view_mode_name}",
[
'_entity_form' => 'entity_view_display.edit',
'_title' => 'Manage display',
] + $defaults,
['_field_ui_view_mode_access' => 'administer ' . $entity_type_id . ' display'],
$options
);
$collection->add("entity.entity_view_display.{$entity_type_id}.view_mode", $route);
}
}
}
/**
* {@inheritdoc}
*/
public static function getSubscribedEvents(): array {
$events = parent::getSubscribedEvents();
$events[RoutingEvents::ALTER] = ['onAlterRoutes', -100];
return $events;
}
}