clean install

This commit is contained in:
2024-12-10 15:08:16 +01:00
commit e14eb2d8fd
31193 changed files with 3555714 additions and 0 deletions

View File

@@ -0,0 +1,89 @@
<?php
namespace Drupal\field_ui\Access;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Access\AccessResultInterface;
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));
}
}
$access->addCacheableDependency($this->entityFieldManager);
}
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,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 ? [] : $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 $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 $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,210 @@
<?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.'),
],
];
}
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,74 @@
<?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
*/
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,939 @@
<?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,
],
'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'],
],
'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,301 @@
<?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\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),
};
}
}

View File

@@ -0,0 +1,150 @@
<?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'),
$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] = $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,185 @@
<?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'),
$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] = $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,146 @@
<?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 {
/**
* The entity type bundle info service.
*
* @var \Drupal\Core\Entity\EntityTypeBundleInfoInterface
*/
protected $entityTypeBundleInfo;
/**
* Constructs a new FieldConfigDeleteForm object.
*
* @param \Drupal\Core\Entity\EntityTypeBundleInfoInterface $entity_type_bundle_info
* The entity type bundle info service.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface|null $entity_type_manager
* The entity type manager service.
*/
public function __construct(EntityTypeBundleInfoInterface $entity_type_bundle_info, ?EntityTypeManagerInterface $entity_type_manager = NULL) {
$this->entityTypeBundleInfo = $entity_type_bundle_info;
if (!$entity_type_manager) {
@trigger_error('Calling ' . __METHOD__ . '() without the $entity_type_manager argument is deprecated in drupal:10.2.0 and is removed from drupal:11.0.0. See https://www.drupal.org/node/3396525', E_USER_DEPRECATED);
$entity_type_manager = \Drupal::service('entity_type.manager');
}
$this->entityTypeManager = $entity_type_manager;
}
/**
* {@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,563 @@
<?php
namespace Drupal\field_ui\Form;
use Drupal\Core\Entity\EntityDisplayRepositoryInterface;
use Drupal\Core\Entity\EntityForm;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityStorageException;
use Drupal\Core\Entity\EntityTypeBundleInfoInterface;
use Drupal\Core\Entity\FieldableEntityInterface;
use Drupal\Core\Entity\Plugin\DataType\EntityAdapter;
use Drupal\Core\Field\FieldFilteredMarkup;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Form\SubformState;
use Drupal\Core\Render\Element;
use Drupal\Core\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 entity type bundle info service.
*
* @var \Drupal\Core\Entity\EntityTypeBundleInfoInterface
*/
protected $entityTypeBundleInfo;
/**
* The name of the entity type.
*
* @var string
*/
protected string $entityTypeId;
/**
* The entity bundle.
*
* @var string
*/
protected string $bundle;
/**
* Constructs a new FieldConfigDeleteForm object.
*
* @param \Drupal\Core\Entity\EntityTypeBundleInfoInterface $entity_type_bundle_info
* The entity type bundle info service.
* @param \Drupal\Core\TypedData\TypedDataManagerInterface $typedDataManager
* The type data manger.
* @param \Drupal\Core\Entity\EntityDisplayRepositoryInterface|null $entityDisplayRepository
* The entity display repository.
* @param \Drupal\Core\TempStore\PrivateTempStore|null $tempStore
* The private tempstore.
* @param \Drupal\Core\Render\ElementInfoManagerInterface|null $elementInfo
* The element info manager.
*/
public function __construct(
EntityTypeBundleInfoInterface $entity_type_bundle_info,
protected TypedDataManagerInterface $typedDataManager,
protected ?EntityDisplayRepositoryInterface $entityDisplayRepository = NULL,
protected ?PrivateTempStore $tempStore = NULL,
protected ?ElementInfoManagerInterface $elementInfo = NULL,
) {
$this->entityTypeBundleInfo = $entity_type_bundle_info;
if ($this->entityDisplayRepository === NULL) {
@trigger_error('Calling FieldConfigEditForm::__construct() without the $entityDisplayRepository argument is deprecated in drupal:10.2.0 and will be required in drupal:11.0.0. See https://www.drupal.org/node/3383771', E_USER_DEPRECATED);
$this->entityDisplayRepository = \Drupal::service('entity_display.repository');
}
if ($this->tempStore === NULL) {
@trigger_error('Calling FieldConfigEditForm::__construct() without the $tempStore argument is deprecated in drupal:10.2.0 and will be required in drupal:11.0.0. See https://www.drupal.org/node/3383771', E_USER_DEPRECATED);
$this->tempStore = \Drupal::service('tempstore.private')->get('field_ui');
}
if ($this->elementInfo === NULL) {
@trigger_error('Calling FieldConfigEditForm::__construct() without the $elementInfo argument is deprecated in drupal:10.2.0 and will be required in drupal:11.0.0. See https://www.drupal.org/node/3383771', E_USER_DEPRECATED);
$this->elementInfo = \Drupal::service('plugin.manager.element_info');
}
}
/**
* {@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
*/
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->t('Save settings');
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);
if ($this->getRequest()->query->has('destination')) {
$query = $url->getOption('query');
$query['destination'] = $this->getRequest()->query->get('destination');
$url->setOption('query', $query);
}
$actions['delete'] = [
'#type' => 'link',
'#title' => $this->t('Delete'),
'#url' => $url,
'#access' => $this->entity->access('delete'),
'#attributes' => [
'class' => ['button', 'button--danger'],
],
];
}
return $actions;
}
/**
* {@inheritdoc}
*/
public function validateForm(array &$form, FormStateInterface $form_state) {
parent::validateForm($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->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
*/
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,620 @@
<?php
namespace Drupal\field_ui\Form;
use Drupal\Component\Utility\Html;
use Drupal\Component\Utility\SortArray;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Entity\EntityFieldManagerInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Field\FallbackFieldTypeCategory;
use Drupal\Core\Field\FieldTypeCategoryManagerInterface;
use Drupal\Core\Field\FieldTypePluginManagerInterface;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\TempStore\PrivateTempStore;
use Drupal\field\Entity\FieldStorageConfig;
use Drupal\field_ui\FieldUI;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides a form for the "field storage" add page.
*
* @internal
*/
class FieldStorageAddForm extends FormBase {
/**
* The name of the entity type.
*
* @var string
*/
protected $entityTypeId;
/**
* The entity bundle.
*
* @var string
*/
protected $bundle;
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* The entity field manager.
*
* @var \Drupal\Core\Entity\EntityFieldManagerInterface
*/
protected $entityFieldManager;
/**
* The field type plugin manager.
*
* @var \Drupal\Core\Field\FieldTypePluginManagerInterface
*/
protected $fieldTypePluginManager;
/**
* The configuration factory.
*
* @var \Drupal\Core\Config\ConfigFactoryInterface
*/
protected $configFactory;
/**
* Constructs a new FieldStorageAddForm object.
*
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
* @param \Drupal\Core\Field\FieldTypePluginManagerInterface $field_type_plugin_manager
* The field type plugin manager.
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The configuration factory.
* @param \Drupal\Core\Entity\EntityFieldManagerInterface $entity_field_manager
* (optional) The entity field manager.
* @param \Drupal\Core\TempStore\PrivateTempStore|null $tempStore
* The private tempstore.
* @param \Drupal\Core\Field\FieldTypeCategoryManagerInterface|null $fieldTypeCategoryManager
* The field type category plugin manager.
*/
public function __construct(EntityTypeManagerInterface $entity_type_manager, FieldTypePluginManagerInterface $field_type_plugin_manager, ConfigFactoryInterface $config_factory, EntityFieldManagerInterface $entity_field_manager, protected ?PrivateTempStore $tempStore = NULL, protected ?FieldTypeCategoryManagerInterface $fieldTypeCategoryManager = NULL) {
$this->entityTypeManager = $entity_type_manager;
$this->fieldTypePluginManager = $field_type_plugin_manager;
$this->configFactory = $config_factory;
$this->entityFieldManager = $entity_field_manager;
if ($this->tempStore === NULL) {
@trigger_error('Calling FieldStorageAddForm::__construct() without the $tempStore argument is deprecated in drupal:10.2.0 and will be required in drupal:11.0.0. See https://www.drupal.org/node/3383719', E_USER_DEPRECATED);
$this->tempStore = \Drupal::service('tempstore.private')->get('field_ui');
}
if ($this->fieldTypeCategoryManager === NULL) {
@trigger_error('Calling FieldStorageAddForm::__construct() without the $fieldTypeCategoryManager argument is deprecated in drupal:10.2.0 and will be required in drupal:11.0.0. See https://www.drupal.org/node/3375740', E_USER_DEPRECATED);
$this->fieldTypeCategoryManager = \Drupal::service('plugin.manager.field.field_type_category');
}
}
/**
* {@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) {
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');
if (!$form_state->has('field_type_options') || !$form_state->has('unique_definitions')) {
$this->processFieldDefinitions($form_state);
}
// 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['actions'] = ['#type' => 'actions'];
$form['actions']['submit'] = [
'#type' => 'submit',
'#value' => $this->t('Continue'),
'#button_type' => 'primary',
];
$form['#attached']['library'] = [
'field_ui/drupal.field_ui',
'field_ui/drupal.field_ui.manage_fields',
'core/drupal.ajax',
];
if ($form_state->hasValue('new_storage_type')) {
// A group is already selected. Show field types for that group.
$this->addFieldOptionsForGroup($form, $form_state);
}
else {
// Show options for groups and ungrouped field types.
$this->addGroupFieldOptions($form, $form_state);
}
return $form;
}
/**
* Save field type definitions and categories in the form state.
*
* Get all field type definitions and store each one twice:
* - field_type_options: each field type is indexed by its category plugin ID
* or its label.
* - unique_definitions: each field type is indexed by its category and name.
*
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current state of the form.
*/
protected function processFieldDefinitions(FormStateInterface $form_state): void {
$field_type_options = $unique_definitions = [];
$grouped_definitions = $this->fieldTypePluginManager
->getGroupedDefinitions($this->fieldTypePluginManager->getEntityTypeUiDefinitions($this->entityTypeId), 'label', 'id');
foreach ($grouped_definitions as $category => $field_types) {
foreach ($field_types as $name => $field_type) {
$definition = ['unique_identifier' => $name] + $field_type;
$category_info = $this->fieldTypeCategoryManager
->createInstance($field_type['category'], $definition);
$definition['display_as_group'] = !($category_info instanceof FallbackFieldTypeCategory);
$id = $this->fieldTypeCategoryManager->hasDefinition($category)
? $category_info->getPluginId()
: (string) $field_type['label'];
$field_type_options[$id] = $definition;
$unique_definitions[$category][$name] = $definition;
}
}
$form_state->set('field_type_options', $field_type_options);
$form_state->set('unique_definitions', $unique_definitions);
}
/**
* Adds ungrouped field types and field type groups to the form.
*
* When a group is selected, the related fields are shown when the form is
* rebuilt.
*
* @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.
*/
protected function addGroupFieldOptions(array &$form, FormStateInterface $form_state): void {
$field_type_options_radios = [];
foreach ($form_state->get('field_type_options') as $id => $field_type) {
/** @var \Drupal\Core\Field\FieldTypeCategoryInterface $category_info */
$category_info = $this->fieldTypeCategoryManager
->createInstance($field_type['category'], $field_type);
$display_as_group = $field_type['display_as_group'];
$cleaned_class_name = Html::getClass($field_type['unique_identifier']);
$field_type_options_radios[$id] = [
'#type' => 'container',
'#attributes' => [
'class' => ['field-option', 'js-click-to-select'],
],
'#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",
],
],
],
],
'radio' => [
'#type' => 'radio',
'#title' => $category_info->getLabel(),
'#parents' => ['new_storage_type'],
'#title_display' => 'before',
'#description_display' => 'before',
'#theme_wrappers' => ['form_element__new_storage_type'],
// If it is a category, set return value as the category label.
// Otherwise, set it as the field type id.
'#return_value' => $display_as_group ? $field_type['category'] : $field_type['unique_identifier'],
'#attributes' => [
'class' => ['field-option-radio'],
],
'#description' => [
'#type' => 'container',
'#attributes' => [
'class' => ['field-option__description'],
],
'#markup' => $category_info->getDescription(),
],
'#variant' => 'field-option',
],
];
if ($libraries = $category_info->getLibraries()) {
$field_type_options_radios[$id]['#attached']['library'] = $libraries;
}
}
uasort($field_type_options_radios, [SortArray::class, 'sortByWeightProperty']);
$form['add-label'] = [
'#type' => 'label',
'#title' => $this->t('Choose a type of field'),
'#required' => TRUE,
];
$form['add'] = [
'#type' => 'container',
'#attributes' => [
'class' => 'add-field-container',
],
];
$form['add']['new_storage_type'] = $field_type_options_radios;
$form['actions']['submit']['#validate'][] = '::validateGroupOrField';
$form['actions']['submit']['#submit'][] = '::rebuildWithOptions';
}
/**
* Adds field types for the selected group to the form.
*
* @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.
*/
protected function addFieldOptionsForGroup(array &$form, FormStateInterface $form_state): void {
// Field label and field_name.
$form['new_storage_wrapper'] = [
'#type' => 'container',
'#attributes' => [
'class' => ['field-ui-new-storage-wrapper'],
],
];
$form['new_storage_wrapper']['label'] = [
'#type' => 'textfield',
'#title' => $this->t('Label'),
'#size' => 30,
];
$field_prefix = $this->config('field_ui.settings')->get('field_prefix');
$form['new_storage_wrapper']['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' => ['new_storage_wrapper', 'label'],
'exists' => [$this, 'fieldNameExists'],
],
'#required' => FALSE,
];
$form['actions']['submit']['#validate'][] = '::validateFieldType';
$form['actions']['back'] = [
'#type' => 'submit',
'#value' => $this->t('Back'),
'#submit' => ['::startOver'],
];
$field_type_options = $form_state->get('field_type_options');
$new_storage_type = $form_state->getValue('new_storage_type');
$form['new_storage_type'] = [
'#type' => 'value',
'#value' => $new_storage_type,
];
if (!isset($new_storage_type) || !$field_type_options[$new_storage_type]['display_as_group']) {
return;
}
// Create a wrapper for all the field options to be provided.
$form['group_field_options_wrapper'] = [
'#prefix' => '<div id="group-field-options-wrapper" class="group-field-options-wrapper">',
'#suffix' => '</div>',
];
$form['group_field_options_wrapper']['label'] = [
'#type' => 'label',
'#title' => $this->t('Choose an option below'),
'#required' => TRUE,
];
$form['group_field_options_wrapper']['fields'] = [
'#type' => 'container',
'#attributes' => [
'class' => ['group-field-options'],
],
];
$unique_definitions = $form_state->get('unique_definitions')[$new_storage_type] ?? [];
$group_field_options = [];
foreach ($unique_definitions as $option) {
$identifier = $option['unique_identifier'];
// If the field type plugin's annotation defines description as an
// array, render it as an item_list.
$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' => $identifier,
'#weight' => $option['weight'],
'#parents' => ['group_field_options_wrapper'],
'#attributes' => [
'class' => ['field-option-radio'],
'data-once' => 'field-click-to-select',
],
'#wrapper_attributes' => [
'class' => ['js-click-to-select', 'subfield-option'],
],
'#variant' => 'field-suboption',
'#return_value' => $identifier,
];
if ($identifier === 'entity_reference') {
$radio_element['#title'] = 'Other';
$radio_element['#weight'] = 10;
}
$group_field_options[$identifier] = $radio_element;
}
uasort($group_field_options, [SortArray::class, 'sortByWeightProperty']);
$form['group_field_options_wrapper']['fields'] += $group_field_options;
}
/**
* Validates the first step of the form.
*
* @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 validateGroupOrField(array &$form, FormStateInterface $form_state) {
if (!$form_state->getValue('new_storage_type')) {
$form_state->setErrorByName('add', $this->t('You need to select a field type.'));
}
}
/**
* Validates the second step (field storage selection and label) of the form.
*
* @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 validateFieldType(array $form, FormStateInterface $form_state) {
// Missing label.
if (!$form_state->getValue('label')) {
$form_state->setErrorByName('label', $this->t('Add new field: you need to provide a label.'));
}
// Missing field name.
if (!$form_state->getValue('field_name')) {
$form_state->setErrorByName('field_name', $this->t('Add new field: you need to provide a machine name for the field.'));
}
// Field name validation.
else {
$field_name = $form_state->getValue('field_name');
// Add the field prefix.
$field_name = $this->configFactory->get('field_ui.settings')->get('field_prefix') . $field_name;
$form_state->setValueForElement($form['new_storage_wrapper']['field_name'], $field_name);
}
if (isset($form['group_field_options_wrapper']['fields']) && !$form_state->getValue('group_field_options_wrapper')) {
$form_state->setErrorByName('group_field_options_wrapper', $this->t('You need to choose an option.'));
}
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
$values = $form_state->getValues();
$entity_type = $this->entityTypeManager->getDefinition($this->entityTypeId);
$field_storage_type = $values['group_field_options_wrapper'] ?? $values['new_storage_type'];
$field_name = $values['field_name'];
$field_values = [
'entity_type' => $this->entityTypeId,
'bundle' => $this->bundle,
];
// Check if we're dealing with a preconfigured field.
if (str_starts_with($field_storage_type, 'field_ui:')) {
[, $field_type, $preset_key] = explode(':', $field_storage_type, 3);
$default_options = $this->getNewFieldDefaults($field_type, $preset_key);
}
else {
$field_type = $field_storage_type;
$default_options = [];
}
$field_values += [
...$default_options['field_config'] ?? [],
'field_name' => $field_name,
'label' => $values['label'],
// Field translatability should be explicitly enabled by the users.
'translatable' => FALSE,
];
$field_storage_values = [
...$default_options['field_storage_config'] ?? [],
'field_name' => $field_name,
'type' => $field_type,
'entity_type' => $this->entityTypeId,
'translatable' => $values['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 %label: @message', ['%label' => $values['label'], '@message' => $e->getMessage()]));
return;
}
// Save field and field storage values in tempstore.
$this->tempStore->set($this->entityTypeId . ':' . $field_name, [
'field_storage' => $field_storage_entity,
'field_config_values' => $field_values,
'default_options' => $default_options,
]);
// Configure next steps in the multi-part form.
$destinations = [];
$route_parameters = [
'entity_type' => $this->entityTypeId,
'field_name' => $field_name,
] + FieldUI::getRouteBundleParameter($entity_type, $this->bundle);
$destinations[] = [
'route_name' => "field_ui.field_add_{$this->entityTypeId}",
'route_parameters' => $route_parameters,
];
$destinations[] = [
'route_name' => "entity.{$this->entityTypeId}.field_ui_fields",
'route_parameters' => $route_parameters,
];
$destination = $this->getDestinationArray();
$destinations[] = $destination['destination'];
$form_state->setRedirectUrl(
FieldUI::getNextDestination($destinations)
);
// Store new field information for any additional submit handlers.
$form_state->set(['fields_added', '_add_new_field'], $field_name);
}
/**
* 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;
}
/**
* 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,282 @@
<?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\Form\SubformStateInterface;
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) {
if ($form_state instanceof SubformStateInterface) {
return [];
}
$elements = parent::actions($form, $form_state);
$elements['submit']['#value'] = $this->entity->isNew() ? $this->t('Continue') : $this->t('Save');
@trigger_error('Rendering ' . __CLASS__ . ' outside of a subform is deprecated in drupal:10.2.0 and is removed in drupal:11.0.0. See https://www.drupal.org/node/3391538', E_USER_DEPRECATED);
return $elements;
}
/**
* 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
*/
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,92 @@
<?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"],
];
$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' => '85vw',
]),
],
],
];
}
}
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,185 @@
<?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 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/add-field",
[
'_form' => '\Drupal\field_ui\Form\FieldStorageAddForm',
'_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/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;
}
}