first commit

This commit is contained in:
2024-07-15 12:33:27 +02:00
commit ce50ae282b
22084 changed files with 2623791 additions and 0 deletions

View File

@@ -0,0 +1,68 @@
/**
* @file
* Stylesheet for the Field UI module.
*/
/* 'Manage fields' and 'Manage display' overviews */
.field-ui-overview .region-title td {
font-weight: bold;
}
.field-ui-overview .region-message td {
font-style: italic;
}
.field-settings-summary-cell li,
.storage-settings-summary-cell li {
margin: 0;
list-style-type: none;
}
.field-settings-summary-cell li {
font-size: 0.9em;
}
.field-settings-summary-cell li:first-child {
font-size: 1em;
}
.allowed-values-table .form-item:where(:not(.hidden)) {
display: inline-table;
}
/* 'Manage form display' and 'Manage display' overview */
.field-ui-overview .field-plugin-summary-cell {
line-height: 1em;
}
.field-ui-overview .field-plugin-summary {
float: left; /* LTR */
font-size: 0.9em;
}
[dir="rtl"] .field-ui-overview .field-plugin-summary {
float: right;
}
.field-ui-overview .field-plugin-summary-cell .warning {
display: block;
float: left; /* LTR */
margin-right: 0.5em;
}
[dir="rtl"] .field-ui-overview .field-plugin-summary-cell .warning {
float: right;
}
.field-ui-overview .field-plugin-settings-edit-wrapper {
float: right; /* LTR */
}
[dir="rtl"] .field-ui-overview .field-plugin-settings-edit-wrapper {
float: left;
}
.field-ui-overview .field-plugin-settings-edit {
float: right; /* LTR */
}
[dir="rtl"] .field-ui-overview .field-plugin-settings-edit {
float: left;
}
.field-ui-overview .field-plugin-settings-editing td {
vertical-align: top;
}
.field-ui-overview .field-plugin-settings-editing .field-plugin-type {
display: none;
}
.field-ui-overview .field-plugin-settings-edit-form .plugin-name {
font-weight: bold;
}

View File

@@ -0,0 +1,33 @@
/*
* DO NOT EDIT THIS FILE.
* See the following change record for more information,
* https://www.drupal.org/node/3084859
* @preserve
*/
/* cspell:ignore cacbd */
.field-option__icon {
position: relative;
height: 100%;
background-image: url("data:image/svg+xml,%3csvg fill='none' height='36' viewBox='0 0 36 36' width='36' xmlns='http://www.w3.org/2000/svg'%3e%3cpath d='m5 7.71798 8.7937-.00001c-.7328-5.48304 7.3282-7.049628 7.3282 0l7.3281.00001v7.83292c5.8625-.7833 8.7937 9.3995 0 9.3995v7.0496h-23.45v-11.7494-14.02745' stroke='%23cacbd2' stroke-width='3'/%3e%3c/svg%3e");
background-repeat: no-repeat;
background-position: center;
background-size: 36px;
}
.field-icon-boolean {
background-image: url("data:image/svg+xml,%3csvg height='36' viewBox='0 0 36 36' width='36' xmlns='http://www.w3.org/2000/svg'%3e%3cpath d='m11.02 6.064c-2.287.164-4.788 1.165-6.58 2.634-1.741 1.427-3.084 3.366-3.786 5.466-.852 2.547-.853 5.12-.006 7.656 1.506 4.503 5.535 7.679 10.302 8.119.884.082 13.216.082 14.1 0 5.287-.488 9.574-4.301 10.683-9.502.649-3.043.026-6.328-1.707-8.989a11.927 11.927 0 0 0 -9.157-5.386c-.977-.071-12.861-.069-13.849.002m14.422 2.542c4.167.683 7.319 3.848 7.953 7.984.165 1.079.088 2.688-.182 3.75-.944 3.727-4.045 6.501-7.923 7.088-.789.12-13.787.12-14.58.001-3.514-.53-6.376-2.828-7.627-6.126-.631-1.664-.746-3.857-.295-5.645.918-3.647 3.936-6.404 7.712-7.047.692-.118 14.227-.122 14.942-.005m-2.702 2.548c-2.256.498-3.999 2.206-4.569 4.476-.156.618-.219 2.389-.115 3.18.4 3.027 2.917 5.25 5.944 5.25a5.87 5.87 0 0 0 4.37-1.894 6.1 6.1 0 0 0 1.576-3.415c.1-.847.038-2.503-.117-3.121-.446-1.782-1.586-3.196-3.219-3.994-.879-.43-1.377-.546-2.46-.573-.72-.017-1.002.001-1.41.091' fill='%2355565b'/%3e%3c/svg%3e");
}
.field-icon-plain_text {
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='36' height='36' viewBox='0 0 36 36'%3e %3cpath fill='%2355565B' d='M7 7.60001V4H29V7.60001H19.8333V31H16.1667V7.60001H7Z'/%3e%3c/svg%3e");
}
.field-icon-date_time {
background-image: url("data:image/svg+xml,%3csvg height='33' viewBox='0 0 36 33' width='36' xmlns='http://www.w3.org/2000/svg'%3e%3cpath d='m7.08 1.44v1.44h-5.976l-.318.158a1.326 1.326 0 0 0 -.726.941c-.048.224-.061 3.678-.048 12.311l.018 12 .131.246c.073.135.228.329.345.432.448.393-.104.373 9.978.372l9.14-.002.288.346c.479.574 1.348 1.362 1.936 1.755a9.006 9.006 0 0 0 8.182.98c4.629-1.704 7.072-6.881 5.452-11.555-.939-2.711-3.044-4.81-5.725-5.709l-.627-.211-.03-5.537-.03-5.537-.133-.249c-.162-.303-.513-.603-.797-.682-.125-.035-1.57-.058-3.555-.059h-3.345v-2.88h-2.82v2.88h-8.52v-2.88h-2.82zm18.84 10.912v2.391l-.342.041c-.542.063-1.317.269-1.969.521-2.825 1.095-4.943 3.609-5.613 6.664-.235 1.07-.219 2.683.039 3.936l.04.195h-14.835v-16.14h22.68zm1.185 2.332a2.601 2.601 0 0 1 -.45 0c-.124-.013-.022-.024.225-.024s.349.011.225.024m1.332 3.012c.586.148 1.445.539 1.976.899a6.322 6.322 0 0 1 2.746 5.525c-.079 1.624-.71 3.058-1.845 4.194a5.756 5.756 0 0 1 -1.756 1.24c-.918.435-1.576.581-2.618.583-.585.001-1.008-.03-1.292-.094-2.621-.594-4.532-2.609-4.95-5.219-.107-.664-.045-1.976.121-2.594.636-2.361 2.568-4.177 4.912-4.62.665-.125 2.042-.081 2.706.086m-2.563 5.119.016 3.255 2.415.016 2.415.015v-1.859l-1.605-.016-1.605-.016-.016-2.325-.015-2.325h-1.62z' fill='%2355565b'/%3e%3c/svg%3e");
}
.field-icon-email {
background-image: url("data:image/svg+xml,%3csvg height='36' viewBox='0 0 36 36' width='36' xmlns='http://www.w3.org/2000/svg'%3e%3cpath d='m18.905 3.0034c-4.8818-.0181-9.89368 2.11996-12.75421 6.14588-2.97685 4.12852-3.72724 9.57882-2.74182 14.50242.69215 3.2862 2.61415 6.3919 5.55652 8.109 3.35621 2.0297 7.44861 2.3335 11.27361 1.8957 1.9198-.2327 3.8097-.7456 5.5656-1.549 0-1.2133 0-2.4267 0-3.64-3.9461 1.6362-8.4574 2.4333-12.6514 1.2587-2.9465-.8305-5.34152-3.3703-5.98961-6.3624-.77366-3.0458-.58571-6.3211.39477-9.2927 1.05002-2.9697 3.32674-5.53882 6.31624-6.61934 2.9829-1.16767 6.4097-1.27462 9.4541-.26651 2.7133.99524 4.9245 3.33755 5.6015 6.14525.7038 2.5698.6228 5.4088-.3714 7.8826-.4383 1.0424-1.4289 2.1055-2.6643 1.867-.6836-.1102-1.2174-.6898-1.2841-1.374-.3646-1.7236.0832-3.4856.0543-5.2278.0939-1.7622.1876-3.5244.2846-5.2865-2.7816-.8329-5.7863-1.36856-8.6563-.6962-2.9057.7966-5.1346 3.4114-5.6209 6.3736-.4246 2.2055-.2402 4.5877.7799 6.5936.9431 1.7193 2.7689 2.9433 4.7485 3.0192 2.1842.205 4.5109-.7068 5.752-2.5513.808 1.8442 2.9703 2.8932 4.9355 2.5197 2.3445-.3217 4.2363-2.1564 5.0624-4.3086 1.3658-3.1906 1.3042-6.8642.3573-10.1616-1.129-3.63941-3.9388-6.75356-7.5656-8.02092-1.8577-.69892-3.8521-.9948-5.8372-.95578zm-.2305 10.5789c.7719-.0025 1.547.0602 2.296.2236-.2194 2.5144.0496 5.147-.9169 7.5287-.4626 1.006-1.4737 1.788-2.6009 1.773-1.18.1157-2.4907-.5777-2.7663-1.7944-.5272-1.6144-.3716-3.4013.1106-5.0038.5405-1.4722 1.9158-2.6924 3.5363-2.7087.1134-.0098.2273-.016.3412-.0184z' fill='%2355565b'/%3e%3c/svg%3e");
}
.field-icon-number {
background-image: url("data:image/svg+xml,%3csvg height='36' viewBox='0 0 36 36' width='36' xmlns='http://www.w3.org/2000/svg'%3e%3cpath d='m15.497 8.045c-1.353.129-2.556.713-3.559 1.727-1.08 1.092-1.675 2.516-1.677 4.013l-.001.555h2.683l.032-.585c.064-1.137.567-1.998 1.515-2.591.955-.598 2.344-.579 3.304.046 1.046.68 1.594 1.871 1.419 3.085-.134.93-.07.844-4.719 6.377l-4.231 5.038-.002.825-.001.825h11.7v-2.699l-3.627-.016-3.628-.015 2.974-3.54c1.635-1.947 3.08-3.689 3.212-3.87.322-.446.668-1.176.866-1.83.148-.487.164-.634.168-1.5.003-.82-.016-1.035-.133-1.47-.174-.647-.634-1.595-1.02-2.104-1.223-1.611-3.215-2.469-5.275-2.271m-12.872 1.184-2.625.635v1.338c0 .736.01 1.338.023 1.338.012 0 .91-.213 1.995-.473 1.085-.261 2.06-.492 2.167-.515l.195-.042v15.85h2.94v-18.78l-1.035.007-1.035.007zm21.495.701v1.35h3.3c1.815 0 3.3.013 3.3.028 0 .023-4.162 5.318-4.411 5.612-.064.075-.004.224.366.9.243.445.45.832.46.859.01.028.233-.06.496-.195 1.06-.541 1.997-.569 3.012-.087.814.387 1.449 1.12 1.781 2.06.161.457.181.589.181 1.203.001.492-.03.793-.108 1.05-.534 1.778-2.246 2.891-3.886 2.527-1.343-.299-2.334-1.279-2.686-2.655-.082-.322-.129-.41-.211-.403-.058.005-.602.14-1.209.3-.864.228-1.105.312-1.105.389 0 .214.317 1.188.538 1.654.833 1.753 2.35 2.971 4.166 3.345.74.153 1.734.13 2.465-.055a5.578 5.578 0 0 0 2.596-1.435c3.055-2.897 2.51-8.072-1.077-10.218a6 6 0 0 0 -.9-.424c-.257-.091-.467-.179-.467-.195s.905-1.175 2.01-2.574l2.009-2.544v-1.842h-10.62z' fill='%2355565b'/%3e%3c/svg%3e");
}
.field-icon-reference {
background-image: url("data:image/svg+xml,%3csvg height='36' viewBox='0 0 36 36' width='36' xmlns='http://www.w3.org/2000/svg'%3e%3cpath d='m10.98 9v4.98h5.52v3.06h-9v6h-5.52v9.96h14.04v-9.96h-5.52v-3.06h15v3.06h-5.52v9.96h14.04v-9.96h-5.52v-6h-9v-3.06h5.52v-9.96h-14.04zm11.026.015-.016 1.995h-7.98l-.016-1.995-.016-1.995h8.044zm-8.986 18.975v2.01h-8.04v-4.02h8.04zm18 0v2.01h-8.04v-4.02h8.04z' fill='%2355565b'/%3e%3c/svg%3e");
}

View File

@@ -0,0 +1,33 @@
/* cspell:ignore cacbd */
.field-option__icon {
position: relative;
height: 100%;
background-image: url(../../../misc/icons/cacbd2/puzzle_piece_placeholder.svg);
background-repeat: no-repeat;
background-position: center;
background-size: 36px;
}
.field-icon-boolean {
background-image: url(../../../misc/icons/55565b/boolean.svg);
}
.field-icon-plain_text {
background-image: url(../../../misc/icons/55565b/plain_text.svg);
}
.field-icon-date_time {
background-image: url(../../../misc/icons/55565b/date_and_time.svg);
}
.field-icon-email {
background-image: url(../../../misc/icons/55565b/email.svg);
}
.field-icon-number {
background-image: url(../../../misc/icons/55565b/number.svg);
}
.field-icon-reference {
background-image: url(../../../misc/icons/55565b/reference.svg);
}

View File

@@ -0,0 +1,153 @@
/*
* DO NOT EDIT THIS FILE.
* See the following change record for more information,
* https://www.drupal.org/node/3084859
* @preserve
*/
/**
* @file field_ui_add_field.module.css
*/
.field-ui-field-storage-add-form {
--thumb-size: 4.5rem;
--color-focus: #26a769;
--color-gray: #232429;
--input-fg-color: var(--color-gray);
--color-blue: #003ecc;
--color-red: #dc2323;
--details-box-shadow: 0 2px 0.25rem rgba(0, 0, 0, 0.1);
}
.field-ui-new-storage-wrapper {
margin-bottom: 0.75rem;
}
.group-field-options-wrapper {
margin-block: 1.5em;
}
.add-field-container,
.group-field-options {
display: grid;
gap: 0.75rem 1.25rem;
margin-block: 0.625rem;
}
@media (min-width: 45rem) {
.add-field-container,
.group-field-options {
grid-template-columns: repeat(2, 1fr);
}
}
@media (min-width: 75rem) {
.add-field-container {
grid-template-columns: repeat(3, 1fr);
}
}
@media (min-width: 87.5rem) {
.add-field-container {
grid-template-columns: repeat(4, 1fr);
}
}
.field-option {
display: grid;
grid-template-columns: var(--thumb-size) auto;
align-items: center;
padding: 0.25rem;
padding-inline-end: 0.75rem;
border: 1px solid #dedfe4;
gap: 0.75rem;
border-radius: 0.25rem;
}
.field-option__item {
display: grid;
grid-template-rows: auto 2fr;
grid-template-columns: auto 1.1875rem;
align-items: center;
width: 100%;
margin: 0;
column-gap: 1.25rem;
padding-block: 0.25rem;
}
.field-option__item > *:not(input) {
grid-column: 1;
}
.field-option__item > input {
grid-row: 1 / -1;
grid-column: 2;
}
.field-option__thumb {
height: 100%;
min-height: var(--thumb-size);
background-color: #f3f4f9;
}
.subfield-option {
margin-block: 0.625rem;
padding: 1rem;
padding-inline-end: 2rem;
border: 1px solid #d3d4d9;
border-radius: 0.25rem;
}
.subfield-option .field-option-radio {
margin-inline-end: 0.4375rem;
}
.subfield-option .item-list ul {
margin-inline: 0;
}
.field-option,
.subfield-option {
cursor: pointer;
}
.field-option.focus,
.subfield-option.focus {
outline: 3px solid var(--color-focus);
outline-offset: 2px;
}
.field-option.hover,
.subfield-option.hover {
border-color: var(--color-gray);
box-shadow:
inset 0 0 0 1px var(--color-gray),
var(--details-box-shadow);
}
.field-option:not(.selected, .error):hover .form-boolean,
.subfield-option:not(.selected, .error):hover .form-boolean {
border-color: var(--input-fg-color);
box-shadow: inset 0 0 0 1px var(--input-fg-color);
}
.field-option.selected,
.subfield-option.selected {
border-color: var(--color-blue);
box-shadow:
inset 0 0 0 2px var(--color-blue),
var(--details-box-shadow);
}
.field-option.error,
.subfield-option.error {
border-color: var(--color-red);
box-shadow:
inset 0 0 0 1px var(--color-red),
var(--details-box-shadow);
}
.field-option .form-item__label.has-error,
.subfield-option .form-item__label.has-error {
color: currentColor;
}

View File

@@ -0,0 +1,129 @@
/**
* @file field_ui_add_field.module.css
*/
.field-ui-field-storage-add-form {
--thumb-size: 72px;
--color-focus: #26a769;
--color-gray: #232429;
--input-fg-color: var(--color-gray);
--color-blue: #003ecc;
--color-red: #dc2323;
--details-box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.field-ui-new-storage-wrapper {
margin-bottom: 0.75rem;
}
.group-field-options-wrapper {
margin-block: 1.5em;
}
.add-field-container,
.group-field-options {
display: grid;
gap: 0.75rem 1.25rem;
margin-block: 0.625rem;
@media (min-width: 45rem) {
grid-template-columns: repeat(2, 1fr);
}
}
.add-field-container {
@media (min-width: 75rem) {
grid-template-columns: repeat(3, 1fr);
}
@media (min-width: 87.5rem) {
grid-template-columns: repeat(4, 1fr);
}
}
.field-option {
display: grid;
grid-template-columns: var(--thumb-size) auto;
align-items: center;
padding: 0.25rem;
padding-inline-end: 0.75rem;
border: 1px solid #dedfe4;
gap: 0.75rem;
border-radius: 4px;
}
.field-option__item {
display: grid;
grid-template-rows: auto 2fr;
grid-template-columns: auto 1.1875rem;
align-items: center;
width: 100%;
margin: 0;
column-gap: 1.25rem;
padding-block: 0.25rem;
> *:not(input) {
grid-column: 1;
}
> input {
grid-row: 1 / -1;
grid-column: 2;
}
}
.field-option__thumb {
height: 100%;
min-height: var(--thumb-size);
background-color: #f3f4f9;
}
.subfield-option {
margin-block: 0.625rem;
padding: 1rem;
padding-inline-end: 2rem;
border: 1px solid #d3d4d9;
border-radius: 4px;
.field-option-radio {
margin-inline-end: 0.4375rem;
}
.item-list ul {
margin-inline: 0;
}
}
.field-option,
.subfield-option {
cursor: pointer;
&.focus {
outline: 3px solid var(--color-focus);
outline-offset: 2px;
}
&.hover {
border-color: var(--color-gray);
box-shadow:
inset 0 0 0 1px var(--color-gray),
var(--details-box-shadow);
}
&:not(.selected, .error):hover .form-boolean {
border-color: var(--input-fg-color);
box-shadow: inset 0 0 0 1px var(--input-fg-color);
}
&.selected {
border-color: var(--color-blue);
box-shadow:
inset 0 0 0 2px var(--color-blue),
var(--details-box-shadow);
}
&.error {
border-color: var(--color-red);
box-shadow:
inset 0 0 0 1px var(--color-red),
var(--details-box-shadow);
}
& .form-item__label.has-error {
color: currentColor;
}
}

View File

@@ -0,0 +1,21 @@
/**
* @file field_ui_add_field.theme.css
*/
.field-option .form-item__label,
.subfield-option.subfield-option .form-item__label {
margin: 0;
font-size: 16px;
font-weight: 400;
line-height: 1.5;
}
.subfield-option.subfield-option .form-item__label {
padding-bottom: 10px;
}
.field-option .field-option__description,
.subfield-option .description .item-list {
color: #75767b;
font-size: 14px;
line-height: 1.2;
}

View File

@@ -0,0 +1,13 @@
.display-mode-table th:first-child {
width: 30%;
}
.display-mode-table th:nth-child(2) {
width: 50%;
}
.display-mode-table th:last-child {
width: 20%;
}
.display-mode-table td {
max-width: 50px;
word-wrap: break-word;
}

View File

@@ -0,0 +1,127 @@
<?php
/**
* @file
* Hooks provided by the Field UI module.
*/
/**
* @addtogroup field_types
* @{
*/
/**
* Allow modules to add settings to field formatters provided by other modules.
*
* @param \Drupal\Core\Field\FormatterInterface $plugin
* The instantiated field formatter plugin.
* @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition
* The field definition.
* @param string $view_mode
* The entity view mode.
* @param array $form
* The (entire) configuration form array.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The form state.
*
* @return array
* Returns the form array to be built.
*
* @see \Drupal\field_ui\Form\EntityViewDisplayEditForm::thirdPartySettingsForm()
*/
function hook_field_formatter_third_party_settings_form(\Drupal\Core\Field\FormatterInterface $plugin, \Drupal\Core\Field\FieldDefinitionInterface $field_definition, $view_mode, array $form, \Drupal\Core\Form\FormStateInterface $form_state) {
$element = [];
// Add a 'my_setting' checkbox to the settings form for 'foo_formatter' field
// formatters.
if ($plugin->getPluginId() == 'foo_formatter') {
$element['my_setting'] = [
'#type' => 'checkbox',
'#title' => t('My setting'),
'#default_value' => $plugin->getThirdPartySetting('my_module', 'my_setting'),
];
}
return $element;
}
/**
* Allow modules to add settings to field widgets provided by other modules.
*
* @param \Drupal\Core\Field\WidgetInterface $plugin
* The instantiated field widget plugin.
* @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition
* The field definition.
* @param string $form_mode
* The entity form mode.
* @param array $form
* The (entire) configuration form array.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The form state.
*
* @return array
* Returns the form array to be built.
*
* @see \Drupal\field_ui\Form\EntityFormDisplayEditForm::thirdPartySettingsForm()
*/
function hook_field_widget_third_party_settings_form(\Drupal\Core\Field\WidgetInterface $plugin, \Drupal\Core\Field\FieldDefinitionInterface $field_definition, $form_mode, array $form, \Drupal\Core\Form\FormStateInterface $form_state) {
$element = [];
// Add a 'my_setting' checkbox to the settings form for 'foo_widget' field
// widgets.
if ($plugin->getPluginId() == 'foo_widget') {
$element['my_setting'] = [
'#type' => 'checkbox',
'#title' => t('My setting'),
'#default_value' => $plugin->getThirdPartySetting('my_module', 'my_setting'),
];
}
return $element;
}
/**
* Alters the field formatter settings summary.
*
* @param array $summary
* An array of summary messages.
* @param array $context
* An associative array with the following elements:
* - formatter: The formatter plugin.
* - field_definition: The field definition.
* - view_mode: The view mode being configured.
*
* @see \Drupal\field_ui\Form\EntityViewDisplayEditForm::alterSettingsSummary()
*/
function hook_field_formatter_settings_summary_alter(array &$summary, array $context) {
// Append a message to the summary when an instance of foo_formatter has
// my_setting set to TRUE for the current view mode.
if ($context['formatter']->getPluginId() == 'foo_formatter') {
if ($context['formatter']->getThirdPartySetting('my_module', 'my_setting')) {
$summary[] = t('My setting enabled.');
}
}
}
/**
* Alters the field widget settings summary.
*
* @param array $summary
* An array of summary messages.
* @param array $context
* An associative array with the following elements:
* - widget: The widget object.
* - field_definition: The field definition.
* - form_mode: The form mode being configured.
*
* @see \Drupal\field_ui\Form\EntityFormDisplayEditForm::alterSettingsSummary()
*/
function hook_field_widget_settings_summary_alter(array &$summary, array $context) {
// Append a message to the summary when an instance of foo_widget has
// my_setting set to TRUE for the current view mode.
if ($context['widget']->getPluginId() == 'foo_widget') {
if ($context['widget']->getThirdPartySetting('my_module', 'my_setting')) {
$summary[] = t('My setting enabled.');
}
}
}
/**
* @} End of "addtogroup field_types".
*/

View File

@@ -0,0 +1,12 @@
name: 'Field UI'
type: module
description: 'Provides a user interface for the Field module.'
package: Core
# version: VERSION
dependencies:
- drupal:field
# Information added by Drupal.org packaging script on 2024-07-04
version: '10.3.1'
project: 'drupal'
datestamp: 1720094222

View File

@@ -0,0 +1,29 @@
drupal.field_ui:
version: VERSION
js:
js/field_ui.js: {}
css:
theme:
css/field_ui.admin.css: {}
dependencies:
- core/jquery
- core/drupal
- core/drupalSettings
- core/once
- core/drupal.ajax
- core/drupal.dialog
drupal.field_ui.manage_fields:
version: VERSION
css:
component:
css/field_ui_add_field.module.css: {}
theme:
css/field_ui_add_field.theme.css: {}
css/field_ui.icons.theme.css: {}
drupal.field_ui_table:
version: VERSION
css:
theme:
css/field_ui_display_mode_table.css: {}

View File

@@ -0,0 +1,19 @@
field_ui.entity_view_mode_add:
route_name: field_ui.entity_view_mode_add
title: 'Add view mode'
weight: 1
class: \Drupal\field_ui\DisplayModeLocalAction
appears_on:
- entity.entity_view_mode.collection
field_ui.entity_form_mode_add:
route_name: field_ui.entity_form_mode_add
title: 'Add form mode'
weight: 1
class: \Drupal\field_ui\DisplayModeLocalAction
appears_on:
- entity.entity_form_mode.collection
field_ui.field_storage_config_add:
class: \Drupal\Core\Menu\LocalActionDefault
deriver: \Drupal\field_ui\Plugin\Derivative\FieldUiLocalAction

View File

@@ -0,0 +1,23 @@
entity.field_storage_config.collection:
title: 'Field list'
description: 'Overview of fields on all entity types.'
route_name: entity.field_storage_config.collection
parent: system.admin_reports
field_ui.display_mode:
title: 'Display modes'
description: 'Configure what displays are available for your content and forms.'
route_name: field_ui.display_mode
parent: system.admin_structure
entity.entity_view_mode.collection:
title: 'View modes'
description: 'Manage custom view modes.'
route_name: entity.entity_view_mode.collection
parent: field_ui.display_mode
entity.entity_form_mode.collection:
title: 'Form modes'
description: 'Manage custom form modes.'
route_name: entity.entity_form_mode.collection
parent: field_ui.display_mode

View File

@@ -0,0 +1,28 @@
entity.field_storage_config.collection:
title: Entities
route_name: entity.field_storage_config.collection
base_route: entity.field_storage_config.collection
field_ui.fields:
class: \Drupal\Core\Menu\LocalTaskDefault
deriver: \Drupal\field_ui\Plugin\Derivative\FieldUiLocalTask
entity.entity_view_mode.edit_form:
title: 'Edit'
route_name: entity.entity_view_mode.edit_form
base_route: entity.entity_view_mode.edit_form
entity.entity_form_mode.edit_form:
title: 'Edit'
route_name: entity.entity_form_mode.edit_form
base_route: entity.entity_form_mode.edit_form
entity.entity_view_mode.collection:
title: List
route_name: entity.entity_view_mode.collection
base_route: entity.entity_view_mode.collection
entity.entity_form_mode.collection:
title: List
route_name: entity.entity_form_mode.collection
base_route: entity.entity_form_mode.collection

View File

@@ -0,0 +1,303 @@
<?php
/**
* @file
* Allows administrators to attach custom fields to fieldable types.
*/
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Form\SubformState;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Entity\EntityViewModeInterface;
use Drupal\Core\Entity\EntityFormModeInterface;
use Drupal\Core\Url;
use Drupal\field\FieldConfigInterface;
use Drupal\field_ui\FieldUI;
use Drupal\field_ui\Form\FieldConfigEditForm;
use Drupal\field_ui\Form\FieldStorageConfigEditForm;
use Drupal\field_ui\Plugin\Derivative\FieldUiLocalTask;
/**
* Implements hook_help().
*/
function field_ui_help($route_name, RouteMatchInterface $route_match) {
switch ($route_name) {
case 'help.page.field_ui':
$output = '';
$output .= '<h2>' . t('About') . '</h2>';
$output .= '<p>' . t('The Field UI module provides an administrative user interface (UI) for managing and displaying fields. Fields can be attached to most content entity sub-types. Different field types, widgets, and formatters are provided by the modules installed on your site, and managed by the Field module. For background information and terminology related to fields and entities, see the <a href=":field">Field module help page</a>. For more information about the Field UI, see the <a href=":field_ui_docs">online documentation for the Field UI module</a>.', [':field' => Url::fromRoute('help.page', ['name' => 'field'])->toString(), ':field_ui_docs' => 'https://www.drupal.org/docs/8/core/modules/field-ui']) . '</p>';
$output .= '<h2>' . t('Uses') . '</h2>';
$output .= '<dl>';
$output .= '<dt>' . t('Creating a field') . '</dt>';
$output .= '<dd>' . t('On the <em>Manage fields</em> page for your entity type or sub-type, you can add, configure, and delete fields for that entity type or sub-type. Each field has a <em>machine name</em>, which is used internally to identify the field and must be unique across an entity type; once a field is created, you cannot change the machine name. Most fields have two types of settings. The field-level settings depend on the field type, and affect how the data in the field is stored. Once they are set, they can no longer be changed; examples include how many data values are allowed for the field and where files are stored. The sub-type-level settings are specific to each entity sub-type the field is used on, and they can be changed later; examples include the field label, help text, default value, and whether the field is required or not. You can return to these settings by choosing the <em>Edit</em> link for the field from the <em>Manage fields</em> page.');
$output .= '<dt>' . t('Re-using fields') . '</dt>';
$output .= '<dd>' . t('Once you have created a field, you can use it again in other sub-types of the same entity type. For instance, if you create a field for the article content type, you can also use it for the page content type, but you cannot use it for content blocks or taxonomy terms. If there are fields available for re-use, after clicking <em>Add field</em> from the <em>Manage fields</em> page, you will see a list of available fields for re-use. After selecting a field for re-use, you can configure the sub-type-level settings.') . '</dd>';
$output .= '<dt>' . t('Configuring field editing') . '</dt>';
$output .= '<dd>' . t('On the <em>Manage form display</em> page of your entity type or sub-type, you can configure how the field data is edited by default and in each form mode. If your entity type has multiple form modes (on most sites, most entities do not), you can toggle between the form modes at the top of the page, and you can toggle whether each form mode uses the default settings or custom settings in the <em>Custom display settings</em> section. For each field in each form mode, you can select the widget to use for editing; some widgets have additional configuration options, such as the size for a text field, and these can be edited using the Edit button (which looks like a wheel). You can also change the order of the fields on the form. You can exclude a field from a form by choosing <em>Hidden</em> from the widget drop-down list, or by dragging it into the <em>Disabled</em> section.') . '</dd>';
$output .= '<dt>' . t('Configuring field display') . '</dt>';
$output .= '<dd>' . t('On the <em>Manage display</em> page of your entity type or sub-type, you can configure how each field is displayed by default and in each view mode. If your entity type has multiple view modes, you can toggle between the view modes at the top of the page, and you can toggle whether each view mode uses the default settings or custom settings in the <em>Custom display settings</em> section. For each field in each view mode, you can choose whether and how to display the label of the field from the <em>Label</em> drop-down list. You can also select the formatter to use for display; some formatters have configuration options, which you can edit using the Edit button (which looks like a wheel). You can also change the display order of fields. You can exclude a field from a specific view mode by choosing <em>Hidden</em> from the formatter drop-down list, or by dragging it into the <em>Disabled</em> section.') . '</dd>';
$output .= '<dt>' . t('Configuring view and form modes') . '</dt>';
$output .= '<dd>' . t('You can add, edit, and delete view modes for entities on the <a href=":view_modes">View modes page</a>, and you can add, edit, and delete form modes for entities on the <a href=":form_modes">Form modes page</a>. Once you have defined a view mode or form mode for an entity type, it will be available on the Manage display or Manage form display page for each sub-type of that entity.', [':view_modes' => Url::fromRoute('entity.entity_view_mode.collection')->toString(), ':form_modes' => Url::fromRoute('entity.entity_form_mode.collection')->toString()]) . '</dd>';
$output .= '<dt>' . t('Listing fields') . '</dt>';
$output .= '<dd>' . t('There are two reports available that list the fields defined on your site. The <a href=":entity-list" title="Entities field list report">Entities</a> report lists all your fields, showing the field machine names, types, and the entity types or sub-types they are used on (each sub-type links to the Manage fields page). If the <a href=":views">Views</a> and <a href=":views-ui">Views UI</a> modules are installed, the <a href=":views-list" title="Used in views field list report">Used in views</a> report lists each field that is used in a view, with a link to edit that view.', [':entity-list' => Url::fromRoute('entity.field_storage_config.collection')->toString(), ':views-list' => (\Drupal::moduleHandler()->moduleExists('views_ui')) ? Url::fromRoute('views_ui.reports_fields')->toString() : '#', ':views' => (\Drupal::moduleHandler()->moduleExists('views')) ? Url::fromRoute('help.page', ['name' => 'views'])->toString() : '#', ':views-ui' => (\Drupal::moduleHandler()->moduleExists('views_ui')) ? Url::fromRoute('help.page', ['name' => 'views_ui'])->toString() : '#']) . '</dd>';
$output .= '</dl>';
return $output;
case 'entity.field_storage_config.collection':
return '<p>' . t('This list shows all fields currently in use for easy reference.') . '</p>';
}
}
/**
* Implements hook_theme().
*/
function field_ui_theme() {
return [
'field_ui_table' => [
'variables' => [
'header' => NULL,
'rows' => NULL,
'footer' => NULL,
'attributes' => [],
'caption' => NULL,
'colgroups' => [],
'sticky' => FALSE,
'responsive' => TRUE,
'empty' => '',
],
],
// Provide a dedicated template for new storage options as their styling
// is quite different from a typical form element, so it works best to not
// include default form element classes.
'form_element__new_storage_type' => [
'base hook' => 'form_element',
'render element' => 'element',
],
];
}
/**
* Implements hook_entity_type_build().
*/
function field_ui_entity_type_build(array &$entity_types) {
/** @var \Drupal\Core\Entity\EntityTypeInterface[] $entity_types */
$entity_types['field_config']->setFormClass('edit', 'Drupal\field_ui\Form\FieldConfigEditForm');
$entity_types['field_config']->setFormClass('default', FieldConfigEditForm::class);
$entity_types['field_config']->setFormClass('delete', 'Drupal\field_ui\Form\FieldConfigDeleteForm');
$entity_types['field_config']->setListBuilderClass('Drupal\field_ui\FieldConfigListBuilder');
$entity_types['field_storage_config']->setFormClass('edit', 'Drupal\field_ui\Form\FieldStorageConfigEditForm');
$entity_types['field_storage_config']->setFormClass('default', FieldStorageConfigEditForm::class);
$entity_types['field_storage_config']->setListBuilderClass('Drupal\field_ui\FieldStorageConfigListBuilder');
$entity_types['field_storage_config']->setLinkTemplate('collection', '/admin/reports/fields');
$entity_types['entity_form_display']->setFormClass('edit', 'Drupal\field_ui\Form\EntityFormDisplayEditForm');
$entity_types['entity_view_display']->setFormClass('edit', 'Drupal\field_ui\Form\EntityViewDisplayEditForm');
$form_mode = $entity_types['entity_form_mode'];
$form_mode->setListBuilderClass('Drupal\field_ui\EntityFormModeListBuilder');
$form_mode->setFormClass('add', 'Drupal\field_ui\Form\EntityFormModeAddForm');
$form_mode->setFormClass('edit', 'Drupal\field_ui\Form\EntityDisplayModeEditForm');
$form_mode->setFormClass('delete', 'Drupal\field_ui\Form\EntityDisplayModeDeleteForm');
$form_mode->set('admin_permission', 'administer display modes');
$form_mode->setLinkTemplate('delete-form', '/admin/structure/display-modes/form/manage/{entity_form_mode}/delete');
$form_mode->setLinkTemplate('edit-form', '/admin/structure/display-modes/form/manage/{entity_form_mode}');
$form_mode->setLinkTemplate('add-form', '/admin/structure/display-modes/form/add/{entity_type_id}');
$form_mode->setLinkTemplate('collection', '/admin/structure/display-modes/form');
$view_mode = $entity_types['entity_view_mode'];
$view_mode->setListBuilderClass('Drupal\field_ui\EntityDisplayModeListBuilder');
$view_mode->setFormClass('add', 'Drupal\field_ui\Form\EntityDisplayModeAddForm');
$view_mode->setFormClass('edit', 'Drupal\field_ui\Form\EntityDisplayModeEditForm');
$view_mode->setFormClass('delete', 'Drupal\field_ui\Form\EntityDisplayModeDeleteForm');
$view_mode->set('admin_permission', 'administer display modes');
$view_mode->setLinkTemplate('delete-form', '/admin/structure/display-modes/view/manage/{entity_view_mode}/delete');
$view_mode->setLinkTemplate('edit-form', '/admin/structure/display-modes/view/manage/{entity_view_mode}');
$view_mode->setLinkTemplate('add-form', '/admin/structure/display-modes/view/add/{entity_type_id}');
$view_mode->setLinkTemplate('collection', '/admin/structure/display-modes/view');
}
/**
* Implements hook_entity_bundle_create().
*/
function field_ui_entity_bundle_create($entity_type, $bundle) {
// When a new bundle is created, the menu needs to be rebuilt to add our
// menu item tabs.
\Drupal::service('router.builder')->setRebuildNeeded();
}
/**
* Implements hook_entity_operation().
*/
function field_ui_entity_operation(EntityInterface $entity) {
$operations = [];
$info = $entity->getEntityType();
// Add manage fields and display links if this entity type is the bundle
// of another and that type has field UI enabled.
if (($bundle_of = $info->getBundleOf()) && \Drupal::entityTypeManager()->getDefinition($bundle_of)->get('field_ui_base_route')) {
$account = \Drupal::currentUser();
if ($account->hasPermission('administer ' . $bundle_of . ' fields')) {
$operations['manage-fields'] = [
'title' => t('Manage fields'),
'weight' => 15,
'url' => Url::fromRoute("entity.{$bundle_of}.field_ui_fields", [
$entity->getEntityTypeId() => $entity->id(),
]),
];
}
if ($account->hasPermission('administer ' . $bundle_of . ' form display')) {
$operations['manage-form-display'] = [
'title' => t('Manage form display'),
'weight' => 20,
'url' => Url::fromRoute("entity.entity_form_display.{$bundle_of}.default", [
$entity->getEntityTypeId() => $entity->id(),
]),
];
}
if ($account->hasPermission('administer ' . $bundle_of . ' display')) {
$operations['manage-display'] = [
'title' => t('Manage display'),
'weight' => 25,
'url' => Url::fromRoute("entity.entity_view_display.$bundle_of.default", [
$entity->getEntityTypeId() => $entity->id(),
]),
];
}
}
return $operations;
}
/**
* Implements hook_entity_view_mode_presave().
*/
function field_ui_entity_view_mode_presave(EntityViewModeInterface $view_mode) {
\Drupal::service('router.builder')->setRebuildNeeded();
}
/**
* Implements hook_entity_form_mode_presave().
*/
function field_ui_entity_form_mode_presave(EntityFormModeInterface $form_mode) {
\Drupal::service('router.builder')->setRebuildNeeded();
}
/**
* Implements hook_entity_view_mode_delete().
*/
function field_ui_entity_view_mode_delete(EntityViewModeInterface $view_mode) {
\Drupal::service('router.builder')->setRebuildNeeded();
}
/**
* Implements hook_entity_form_mode_delete().
*/
function field_ui_entity_form_mode_delete(EntityFormModeInterface $form_mode) {
\Drupal::service('router.builder')->setRebuildNeeded();
}
/**
* Prepares variables for field UI overview table templates.
*
* Default template: field-ui-table.html.twig.
*
* @param array $variables
* An associative array containing:
* - elements: An associative array containing a Form API structure to be
* rendered as a table.
*/
function template_preprocess_field_ui_table(&$variables) {
template_preprocess_table($variables);
}
/**
* Implements hook_local_tasks_alter().
*/
function field_ui_local_tasks_alter(&$local_tasks) {
$container = \Drupal::getContainer();
$local_task = FieldUiLocalTask::create($container, 'field_ui.fields');
$local_task->alterLocalTasks($local_tasks);
}
/**
* Implements hook_form_FORM_ID_alter() for 'field_ui_field_storage_add_form'.
*/
function field_ui_form_field_ui_field_storage_add_form_alter(array &$form) {
$optgroup = (string) t('Reference');
// Move the "Entity reference" option to the end of the list and rename it to
// "Other".
unset($form['add']['new_storage_type']['#options'][$optgroup]['entity_reference']);
$form['add']['new_storage_type']['#options'][$optgroup]['entity_reference'] = t('Other…');
}
/**
* Implements hook_preprocess_HOOK().
*/
function field_ui_preprocess_form_element__new_storage_type(&$variables) {
// Add support for a variant string so radios in the add field form can be
// programmatically distinguished.
$variables['variant'] = $variables['element']['#variant'] ?? NULL;
}
/**
* Implements hook_form_alter().
*
* Adds a button 'Save and manage fields' to forms.
*
* @see \Drupal\node\NodeTypeForm
* @see \Drupal\comment\CommentTypeForm
* @see \Drupal\media\MediaTypeForm
* @see \Drupal\block_content\BlockContentTypeForm
* @see field_ui_form_manage_field_form_submit()
*/
function field_ui_form_alter(&$form, FormStateInterface $form_state, $form_id) {
$forms = [
'node_type_add_form',
'comment_type_add_form',
'media_type_add_form',
'block_content_type_add_form',
];
if (!in_array($form_id, $forms)) {
return;
}
if ($form_state->getFormObject()->getEntity()->isNew()) {
$form['actions']['save_continue'] = $form['actions']['submit'];
unset($form['actions']['submit']['#button_type']);
$form['actions']['save_continue']['#value'] = t('Save and manage fields');
$form['actions']['save_continue']['#weight'] = $form['actions']['save_continue']['#weight'] - 5;
$form['actions']['save_continue']['#submit'][] = 'field_ui_form_manage_field_form_submit';
}
}
/**
* Form submission handler for the 'Save and manage fields' button.
*
* @see field_ui_form_alter()
*/
function field_ui_form_manage_field_form_submit($form, FormStateInterface $form_state) {
$provider = $form_state->getFormObject()->getEntity()->getEntityType()->getProvider();
$id = $form_state->getFormObject()->getEntity()->id();
if ($form_state->getTriggeringElement()['#parents'][0] === 'save_continue' && $route_info = FieldUI::getOverviewRouteInfo($provider, $id)) {
$form_state->setRedirectUrl($route_info);
}
}
/**
* Implements hook_form_FORM_ID_alter() for field_config_edit_form.
*/
function field_ui_form_field_config_edit_form_alter(&$form, FormStateInterface $form_state) {
$field_config = $form_state->getFormObject()->getEntity();
assert($field_config instanceof FieldConfigInterface);
$form_id = 'field_storage_config_edit_form';
$hook = 'form_' . $form_id;
$field_storage_form = \Drupal::entityTypeManager()->getFormObject('field_storage_config', $form_state->getFormObject()->getOperation());
$field_storage_form->setEntity($field_config->getFieldStorageDefinition());
$subform_state = SubformState::createForSubform($form['field_storage']['subform'], $form, $form_state, $field_storage_form);
\Drupal::moduleHandler()->alterDeprecated('Use hook_form_field_config_edit_form_alter() instead. See https://www.drupal.org/node/3386675.', $hook, $form['field_storage']['subform'], $subform_state, $form_id);
\Drupal::theme()->alter($hook, $form['field_storage']['subform'], $subform_state, $form_id);
}

View File

@@ -0,0 +1,5 @@
administer display modes:
title: 'Add, edit, and delete custom display modes.'
permission_callbacks:
- Drupal\field_ui\FieldUiPermissions::fieldPermissions

View File

@@ -0,0 +1,95 @@
entity.field_storage_config.collection:
path: '/admin/reports/fields'
defaults:
_entity_list: 'field_storage_config'
_title: 'Field list'
requirements:
_permission: 'administer content types'
field_ui.display_mode:
path: '/admin/structure/display-modes'
defaults:
_controller: '\Drupal\system\Controller\SystemController::systemAdminMenuBlockPage'
_title: 'Display modes'
requirements:
_permission: 'administer display modes'
entity.entity_view_mode.collection:
path: '/admin/structure/display-modes/view'
defaults:
_entity_list: 'entity_view_mode'
_title: 'View modes'
requirements:
_permission: 'administer display modes'
field_ui.entity_view_mode_add:
path: '/admin/structure/display-modes/view/add'
defaults:
_controller: '\Drupal\field_ui\Controller\EntityDisplayModeController::viewModeTypeSelection'
_title: 'Choose view mode entity type'
requirements:
_permission: 'administer display modes'
entity.entity_view_mode.add_form:
path: '/admin/structure/display-modes/view/add/{entity_type_id}'
defaults:
_entity_form: 'entity_view_mode.add'
_title: 'Add view mode'
requirements:
_permission: 'administer display modes'
entity.entity_view_mode.edit_form:
path: '/admin/structure/display-modes/view/manage/{entity_view_mode}'
defaults:
_entity_form: 'entity_view_mode.edit'
_title: 'Edit view mode'
requirements:
_entity_access: 'entity_view_mode.update'
entity.entity_view_mode.delete_form:
path: '/admin/structure/display-modes/view/manage/{entity_view_mode}/delete'
defaults:
_entity_form: 'entity_view_mode.delete'
_title: 'Delete view mode'
requirements:
_entity_access: 'entity_view_mode.delete'
entity.entity_form_mode.collection:
path: '/admin/structure/display-modes/form'
defaults:
_entity_list: 'entity_form_mode'
_title: 'Form modes'
requirements:
_permission: 'administer display modes'
field_ui.entity_form_mode_add:
path: '/admin/structure/display-modes/form/add'
defaults:
_controller: '\Drupal\field_ui\Controller\EntityDisplayModeController::formModeTypeSelection'
_title: 'Choose form mode entity type'
requirements:
_permission: 'administer display modes'
entity.entity_form_mode.add_form:
path: '/admin/structure/display-modes/form/add/{entity_type_id}'
defaults:
_entity_form: 'entity_form_mode.add'
_title: 'Add form mode'
requirements:
_permission: 'administer display modes'
entity.entity_form_mode.edit_form:
path: '/admin/structure/display-modes/form/manage/{entity_form_mode}'
defaults:
_entity_form: 'entity_form_mode.edit'
_title: 'Edit form mode'
requirements:
_entity_access: 'entity_form_mode.update'
entity.entity_form_mode.delete_form:
path: '/admin/structure/display-modes/form/manage/{entity_form_mode}/delete'
defaults:
_entity_form: 'entity_form_mode.delete'
_title: 'Delete form mode'
requirements:
_entity_access: 'entity_form_mode.delete'

View File

@@ -0,0 +1,21 @@
services:
_defaults:
autoconfigure: true
field_ui.subscriber:
class: Drupal\field_ui\Routing\RouteSubscriber
arguments: ['@entity_type.manager']
access_check.field_ui.view_mode:
class: Drupal\field_ui\Access\ViewModeAccessCheck
arguments: ['@entity_type.manager']
tags:
- { name: access_check, applies_to: _field_ui_view_mode_access }
access_check.field_ui.form_mode:
class: Drupal\field_ui\Access\FormModeAccessCheck
arguments: ['@entity_type.manager']
tags:
- { name: access_check, applies_to: _field_ui_form_mode_access }
access_check.field_ui.field_reuse:
class: Drupal\field_ui\Access\FieldReuseAccessCheck
autowire: true
tags:
- { name: access_check, applies_to: _field_ui_field_reuse_access }

View File

@@ -0,0 +1,24 @@
---
label: 'Adding a field to an entity sub-type'
related:
- core.content_structure
- field_ui.manage_display
- field_ui.manage_form
---
{% set content_types_link_text %}{% trans %}Content types{% endtrans %}{% endset %}
{% set content_types_link = render_var(help_route_link(content_types_link_text, 'entity.node_type.collection')) %}
{% set content_structure_topic = render_var(help_topic_link('core.content_structure')) %}
<h2>{% trans %}Goal{% endtrans %}</h2>
<p>{% trans %}Add a field to an entity sub-type; see {{ content_structure_topic }} for an overview of entity types and sub-types, as well as an overview of field types.{% endtrans %}</p>
<h2>{% trans %}Steps{% endtrans %}</h2>
<ol>
<li>{% trans %}Navigate to the page for managing the entity sub-type you want to add the field to. For example, to add a field to a content type, in the <em>Manage</em> administrative menu, navigate to <em>Structure</em> &gt; <em>{{ content_types_link }}</em>.{% endtrans %}</li>
<li>{% trans %}Find the particular sub-type that you want to add the field to, and click <em>Manage fields</em>.{% endtrans %}</li>
<li>{% trans %}Click <em>Add field</em>.{% endtrans %}</li>
<li>{% trans %}In <em>Add a new field</em>, select the type of field you want to add; see {{ content_structure_topic }} for an overview of field types.{% endtrans %}</li>
<li>{% trans %}The <em>Label</em> field should now be visible; enter a label for the field, which is used as the field label for both content editing and content display.{% endtrans %}</li>
<li>{% trans %}Click <em>Save and continue</em>.{% endtrans %}</li>
<li>{% trans %}On the next screen, enter a value for <em>Allowed number of values</em>. You can limit the field to one value per entity item, a set number of values, or set it to have unlimited values. Click <em>Save field settings</em>.{% endtrans %}</li>
<li>{% trans %}On the next screen, optionally edit the settings for the field, which vary depending on what field type you are creating. For all fields, you can edit the <em>Label</em>, <em>Help text</em> (text to be displayed below the field on the content editing page), and <em>Required field</em> (to make it so a value must be entered in order to save the content when editing). You can also configure a default value for the field.{% endtrans %}</li>
<li>{% trans %}Click <em>Save settings</em>. You should be returned to the <em>Manage fields</em> page, with your new field in the list.{% endtrans %}</li>
</ol>

View File

@@ -0,0 +1,25 @@
---
label: 'Configuring field display for an entity sub-type'
related:
- core.content_structure
- field_ui.add_field
- field_ui.manage_form
- core.ui_accessibility
---
{% set content_types_link_text %}{% trans %}Content types{% endtrans %}{% endset %}
{% set content_types_link = render_var(help_route_link(content_types_link_text, 'entity.node_type.collection')) %}
{% set content_structure_topic = render_var(help_topic_link('core.content_structure')) %}
<h2>{% trans %}Goal{% endtrans %}</h2>
<p>{% trans %}Configure the <em>formatters</em> used to display the fields of an entity sub-type, their order in the display, and the formatter settings. See {{ content_structure_topic }} for background information.{% endtrans %}</p>
<h2>{% trans %}Steps{% endtrans %}</h2>
<ol>
<li>{% trans %}Navigate to the page for managing the entity type you want to add the field to. For example, to add a field to a content type, in the <em>Manage</em> administrative menu, navigate to <em>Structure</em> &gt; <em>{{ content_types_link }}</em>.{% endtrans %}</li>
<li>{% trans %}Find the particular sub-type that you want to configure the display of, and click <em>Manage display</em> in the <em>Operations</em> list.{% endtrans %}</li>
<li>{% trans %}Use the drag arrows to order the fields in your preferred order.{% endtrans %}</li>
<li>{% trans %}Drag any fields that you do not wish to see in the display to the <em>Disabled</em> section.{% endtrans %}</li>
<li>{% trans %}In the <em>Label</em> column, select the position for each field label in the display, or <em>- Hidden -</em> to hide a label. You can also choose <em>- Visually Hidden-</em> if you want the label's text to appear in the HTML page, so that screen readers and search engines can read it, but it will not be visible.{% endtrans %}</li>
<li>{% trans %}In the <em>Format</em> column, select the formatter for displaying each field.{% endtrans %}</li>
<li>{% trans %}After selecting the desired formatters, click the settings gear in each row to change the settings for the formatter.{% endtrans %}</li>
<li>{% trans %}When you are done making changes, click <em>Save</em>.{% endtrans %}</li>
<li>{% trans %}Test the display for your entity sub-type by viewing an entity. If needed, return to these steps to further refine the display.{% endtrans %}</li>
</ol>

View File

@@ -0,0 +1,24 @@
---
label: 'Configuring the edit form for an entity sub-type'
related:
- core.content_structure
- field_ui.add_field
- field_ui.manage_display
- core.ui_accessibility
---
{% set content_types_link_text %}{% trans %}Content types{% endtrans %}{% endset %}
{% set content_types_link = render_var(help_route_link(content_types_link_text, 'entity.node_type.collection')) %}
{% set content_structure_topic = render_var(help_topic_link('core.content_structure')) %}
<h2>{% trans %}Goal{% endtrans %}</h2>
<p>{% trans %}Configure the <em>widgets</em> used to edit the fields of an entity sub-type, their order on the form, and the widget settings. See {{ content_structure_topic }} for background information.{% endtrans %}</p>
<h2>{% trans %}Steps{% endtrans %}</h2>
<ol>
<li>{% trans %}Navigate to the page for managing the entity type you want to add the field to. For example, to add a field to a content type, in the <em>Manage</em> administrative menu, navigate to <em>Structure</em> &gt; <em>{{ content_types_link }}</em>.{% endtrans %}</li>
<li>{% trans %}Find the particular sub-type that you want to configure the editing form for, and click <em>Manage form display</em> in the <em>Operations</em> list.{% endtrans %}</li>
<li>{% trans %}Use the drag arrows to order the fields in your preferred order.{% endtrans %}</li>
<li>{% trans %}Drag any fields that you do not wish to see on the editing form to the <em>Disabled</em> section.{% endtrans %}</li>
<li>{% trans %}In the <em>Widget</em> column, select the widget for editing each field.{% endtrans %}</li>
<li>{% trans %}After selecting the desired widgets, click the settings gear in each row to change the settings for the widget.{% endtrans %}</li>
<li>{% trans %}When you are done making changes, click <em>Save</em>.{% endtrans %}</li>
<li>{% trans %}Test the editing form for your entity sub-type by editing or creating an entity. If needed, return to these steps to further refine the form.{% endtrans %}</li>
</ol>

View File

@@ -0,0 +1,26 @@
---
label: 'Adding a reference field to an entity sub-type'
related:
- core.content_structure
- field_ui.add_field
- field_ui.manage_display
- field_ui.manage_form
---
{% set content_structure_topic = render_var(help_topic_link('core.content_structure')) %}
{% set content_types_link_text %}{% trans %}Content types{% endtrans %}{% endset %}
{% set content_types_link = render_var(help_route_link(content_types_link_text, 'entity.node_type.collection')) %}
<h2>{% trans %}Goal{% endtrans %}</h2>
<p>{% trans %}Add an entity reference field to an entity sub-type; see {{ content_structure_topic }} for more information on entities and reference fields.{% endtrans %}</p>
<h2>{% trans %}Steps{% endtrans %}</h2>
<ol>
<li>{% trans %}Navigate to the page for managing the entity sub-type you want to add the field to. For example, to add a field to a content type, in the <em>Manage</em> administrative menu, navigate to <em>Structure</em> &gt; <em>{{ content_types_link }}</em>.{% endtrans %}</li>
<li>{% trans %}Find the particular sub-type that you want to add the field to, and click <em>Manage fields</em>.{% endtrans %}</li>
<li>{% trans %}Click <em>Add field</em>.{% endtrans %}</li>
<li>{% trans %}In <em>Add a new field</em>, select the type of reference field you want to add. The <em>Reference</em> section of the select list shows the most common types of reference field; choose <em>Other...</em> if the entity type you want to reference is not listed.{% endtrans %}</li>
<li>{% trans %}The <em>Label</em> field should now be visible; enter a label for the field, which is used as the field label for both content editing and content display.{% endtrans %}</li>
<li>{% trans %}Click <em>Save and continue</em>.{% endtrans %}</li>
<li>{% trans %}On the next screen, verify that the type of entity you want to reference is shown in <em>Type of item to reference</em>, or select it if not. Enter a value for <em>Allowed number of values</em>. You can limit the field to one value per entity item, a set number of values, or set it to have unlimited values. Click <em>Save field settings</em>.{% endtrans %}</li>
<li>{% trans %}On the next screen, optionally edit the settings for <em>Label</em>, <em>Help text</em> (text to be displayed below the field on the content editing page), and <em>Required field</em> (to make it so a value must be entered in order to save the content when editing).{% endtrans %}</li>
<li>{% trans %}In the <em>Reference type</em> section, you will usually want to limit the entity sub-types that can be referenced; for example, if you are creating a <em>Content</em> reference, you can check one or two <em>Content type</em> choices. The choices will be easier for content editors to scan if you also choose a sort value (normally the entity title or label field).{% endtrans %}</li>
<li>{% trans %}Click <em>Save settings</em>. You should be returned to the <em>Manage fields</em> page, with your new field in the list.{% endtrans %}</li>
</ol>

View File

@@ -0,0 +1,553 @@
/**
* @file
* Attaches the behaviors for the Field UI module.
*/
(function ($, Drupal, drupalSettings, debounce) {
/**
* @type {Drupal~behavior}
*
* @prop {Drupal~behaviorAttach} attach
* Adds behaviors to the field storage add form.
*/
Drupal.behaviors.fieldUIFieldStorageAddForm = {
attach(context) {
const form = once(
'field_ui_add',
'[data-drupal-selector="field-ui-field-storage-add-form"]',
context,
);
if (form.length) {
const $form = $(form);
// Add a few 'js-form-required' and 'form-required' css classes here.
// We can not use the Form API '#required' property because both label
// elements for "add new" and "re-use existing" can never be filled and
// submitted at the same time. The actual validation will happen
// server-side.
$form
.find(
'.js-form-item-label label,' +
'.js-form-item-field-name label,' +
'.js-form-item-existing-storage-label label',
)
.addClass('js-form-required form-required');
}
},
};
/**
* Attaches the fieldUIOverview behavior.
*
* @type {Drupal~behavior}
*
* @prop {Drupal~behaviorAttach} attach
* Attaches the fieldUIOverview behavior.
*
* @see Drupal.fieldUIOverview.attach
*/
Drupal.behaviors.fieldUIDisplayOverview = {
attach(context, settings) {
once(
'field-display-overview',
'table#field-display-overview',
context,
).forEach((overview) => {
Drupal.fieldUIOverview.attach(
overview,
settings.fieldUIRowsData,
Drupal.fieldUIDisplayOverview,
);
});
},
};
// Override the beforeSend method to disable the submit button until
// the AJAX request is completed. This is done to avoid the race
// condition that is being caused by change event listener that is
// attached to every form element inside field storage config edit
// form to update the field config form based on changes made to the
// storage settings.
const originalAjaxBeforeSend = Drupal.Ajax.prototype.beforeSend;
// eslint-disable-next-line func-names
Drupal.Ajax.prototype.beforeSend = function () {
// Disable the submit button on AJAX request initiation.
$('.field-config-edit-form [data-drupal-selector="edit-submit"]').prop(
'disabled',
true,
);
// eslint-disable-next-line prefer-rest-params
return originalAjaxBeforeSend.apply(this, arguments);
};
// Re-enable the submit button after AJAX request is completed.
// eslint-disable-next-line
$(document).on('ajaxComplete', () => {
$('.field-config-edit-form [data-drupal-selector="edit-submit"]').prop(
'disabled',
false,
);
});
/**
* Namespace for the field UI overview.
*
* @namespace
*/
Drupal.fieldUIOverview = {
/**
* Attaches the fieldUIOverview behavior.
*
* @param {HTMLTableElement} table
* The table element for the overview.
* @param {object} rowsData
* The data of the rows in the table.
* @param {object} rowHandlers
* Handlers to be added to the rows.
*/
attach(table, rowsData, rowHandlers) {
const tableDrag = Drupal.tableDrag[table.id];
// Add custom tabledrag callbacks.
tableDrag.onDrop = this.onDrop;
tableDrag.row.prototype.onSwap = this.onSwap;
// Create row handlers.
$(table)
.find('tr.draggable')
.each(function () {
// Extract server-side data for the row.
const row = this;
if (row.id in rowsData) {
const data = rowsData[row.id];
data.tableDrag = tableDrag;
// Create the row handler, make it accessible from the DOM row
// element.
const rowHandler = new rowHandlers[data.rowHandler](row, data);
$(row).data('fieldUIRowHandler', rowHandler);
}
});
},
/**
* Event handler to be attached to form inputs triggering a region change.
*/
onChange() {
const $trigger = $(this);
const $row = $trigger.closest('tr');
// Do not fire change listeners for items within forms that have their
// own AJAX callbacks to process a change.
if ($trigger.closest('.ajax-new-content').length !== 0) {
return;
}
const rowHandler = $row.data('fieldUIRowHandler');
const refreshRows = {};
refreshRows[rowHandler.name] = $trigger.get(0);
// Handle region change.
const region = rowHandler.getRegion();
if (region !== rowHandler.region) {
const $fieldParent = $row.find('select.js-field-parent');
if ($fieldParent.length) {
// Remove parenting.
$fieldParent[0].value = '';
}
// Let the row handler deal with the region change.
$.extend(refreshRows, rowHandler.regionChange(region));
// Update the row region.
rowHandler.region = region;
}
// Fields inside `.tabledrag-hide` are typically hidden. They can be
// visible when "Show row weights" are enabled. If their value is changed
// while visible, the row should be marked as changed, but they should not
// be processed via AJAXRefreshRows as they are intended to be fields AJAX
// updates the value of.
if ($trigger.closest('.tabledrag-hide').length) {
const thisTableDrag = Drupal.tableDrag['field-display-overview'];
// eslint-disable-next-line new-cap
const rowObject = new thisTableDrag.row(
$row[0],
'',
thisTableDrag.indentEnabled,
thisTableDrag.maxDepth,
true,
);
rowObject.markChanged();
rowObject.addChangedWarning();
} else {
// Ajax-update the rows.
Drupal.fieldUIOverview.AJAXRefreshRows(refreshRows);
}
},
/**
* Lets row handlers react when a row is dropped into a new region.
*/
onDrop() {
const dragObject = this;
const row = dragObject.rowObject.element;
const $row = $(row);
const rowHandler = $row.data('fieldUIRowHandler');
if (typeof rowHandler !== 'undefined') {
const regionRow = $row.prevAll('tr.region-message').get(0);
const region = regionRow.className.replace(
/([^ ]+[ ]+)*region-([^ ]+)-message([ ]+[^ ]+)*/,
'$2',
);
if (region !== rowHandler.region) {
// Let the row handler deal with the region change.
const refreshRows = rowHandler.regionChange(region);
// Update the row region.
rowHandler.region = region;
// Ajax-update the rows.
Drupal.fieldUIOverview.AJAXRefreshRows(refreshRows);
}
}
},
/**
* Refreshes placeholder rows in empty regions while a row is being dragged.
*
* Copied from block.js.
*
* @param {HTMLElement} draggedRow
* The tableDrag rowObject for the row being dragged.
*/
onSwap(draggedRow) {
const rowObject = this;
$(rowObject.table)
.find('tr.region-message')
.each(function () {
const $this = $(this);
// If the dragged row is in this region, but above the message row, swap
// it down one space.
if (
$this.prev('tr').get(0) ===
rowObject.group[rowObject.group.length - 1]
) {
// Prevent a recursion problem when using the keyboard to move rows
// up.
if (
rowObject.method !== 'keyboard' ||
rowObject.direction === 'down'
) {
rowObject.swap('after', this);
}
}
// This region has become empty.
if (
$this.next('tr').length === 0 ||
!$this.next('tr')[0].matches('.draggable')
) {
$this.removeClass('region-populated').addClass('region-empty');
}
// This region has become populated.
else if (this.matches('.region-empty')) {
$this.removeClass('region-empty').addClass('region-populated');
}
});
},
/**
* Triggers Ajax refresh of selected rows.
*
* The 'format type' selects can trigger a series of changes in child rows.
* The #ajax behavior is therefore not attached directly to the selects, but
* triggered manually through a hidden #ajax 'Refresh' button.
*
* @param {object} rows
* A hash object, whose keys are the names of the rows to refresh (they
* will receive the 'ajax-new-content' effect on the server side), and
* whose values are the DOM element in the row that should get an Ajax
* throbber.
*/
AJAXRefreshRows(rows) {
// Separate keys and values.
const rowNames = [];
const ajaxElements = [];
Object.keys(rows || {}).forEach((rowName) => {
rowNames.push(rowName);
ajaxElements.push(rows[rowName]);
});
if (rowNames.length) {
// Add a throbber next each of the ajaxElements.
$(ajaxElements).after(Drupal.theme.ajaxProgressThrobber());
const $refreshRows = $('input[name=refresh_rows]');
if ($refreshRows.length) {
// Fire the Ajax update.
$refreshRows[0].value = rowNames.join(' ');
}
once(
'edit-refresh',
'input[data-drupal-selector="edit-refresh"]',
).forEach((input) => {
// Keep track of the element that was focused prior to triggering the
// mousedown event on the hidden submit button.
let returnFocus = {
drupalSelector: null,
scrollY: null,
};
// Use jQuery on to listen as the mousedown event is propagated by
// jQuery trigger().
$(input).on('mousedown', () => {
returnFocus = {
drupalSelector: document.activeElement.hasAttribute(
'data-drupal-selector',
)
? document.activeElement.getAttribute('data-drupal-selector')
: false,
scrollY: window.scrollY,
};
});
input.addEventListener('focus', () => {
if (returnFocus.drupalSelector) {
// Refocus the element that lost focus due to this hidden submit
// button being triggered by a mousedown event.
document
.querySelector(
`[data-drupal-selector="${returnFocus.drupalSelector}"]`,
)
.focus();
}
// Ensure the scroll position is the same as when the input was
// initially changed.
window.scrollTo({
top: returnFocus.scrollY,
});
returnFocus = {};
});
});
$('input[data-drupal-selector="edit-refresh"]').trigger('mousedown');
// Disabled elements do not appear in POST ajax data, so we mark the
// elements disabled only after firing the request.
$(ajaxElements).prop('disabled', true);
}
},
};
/**
* Row handlers for the 'Manage display' screen.
*
* @namespace
*/
Drupal.fieldUIDisplayOverview = {};
/**
* Constructor for a 'field' row handler.
*
* This handler is used for both fields and 'extra fields' rows.
*
* @constructor
*
* @param {HTMLTableRowElement} row
* The row DOM element.
* @param {object} data
* Additional data to be populated in the constructed object.
*
* @return {Drupal.fieldUIDisplayOverview.field}
* The field row handler constructed.
*/
Drupal.fieldUIDisplayOverview.field = function (row, data) {
this.row = row;
this.name = data.name;
this.region = data.region;
this.tableDrag = data.tableDrag;
this.defaultPlugin = data.defaultPlugin;
this.$pluginSelect = $(row).find('.field-plugin-type');
this.$regionSelect = $(row).find('select.field-region');
// Attach change listeners to select and input elements in the row.
$(row).find('select, input').on('change', Drupal.fieldUIOverview.onChange);
return this;
};
Drupal.fieldUIDisplayOverview.field.prototype = {
/**
* Returns the region corresponding to the current form values of the row.
*
* @return {string}
* Either 'hidden' or 'content'.
*/
getRegion() {
if (this.$regionSelect.length) {
return this.$regionSelect[0].value;
}
},
/**
* Reacts to a row being changed regions.
*
* This function is called when the row is moved to a different region, as
* a
* result of either :
* - a drag-and-drop action (the row's form elements then probably need to
* be updated accordingly)
* - user input in one of the form elements watched by the
* {@link Drupal.fieldUIOverview.onChange} change listener.
*
* @param {string} region
* The name of the new region for the row.
*
* @return {object}
* A hash object indicating which rows should be Ajax-updated as a result
* of the change, in the format expected by
* {@link Drupal.fieldUIOverview.AJAXRefreshRows}.
*/
regionChange(region) {
// Replace dashes with underscores.
region = region.replace(/-/g, '_');
if (this.$regionSelect.length) {
// Set the region of the select list.
this.$regionSelect[0].value = region;
}
// Restore the formatter back to the default formatter only if it was
// disabled previously. Pseudo-fields do not have default formatters,
// we just return to 'visible' for those.
if (this.region === 'hidden') {
const pluginSelect =
typeof this.$pluginSelect.find('option')[0] !== 'undefined'
? this.$pluginSelect.find('option')[0].value
: undefined;
const value =
typeof this.defaultPlugin !== 'undefined'
? this.defaultPlugin
: pluginSelect;
if (typeof value !== 'undefined') {
if (this.$pluginSelect.length) {
this.$pluginSelect[0].value = value;
}
}
}
const refreshRows = {};
refreshRows[this.name] = this.$pluginSelect.get(0);
return refreshRows;
},
};
/**
* Filters the existing fields table by the field name or field type.
*
* @type {Drupal~behavior}
*/
Drupal.behaviors.tableFilterByText = {
attach() {
const [input] = once('table-filter-text', '.js-table-filter-text');
if (!input) {
return;
}
const $table = $(input.getAttribute('data-table'));
let $rows;
let searching = false;
function filterRows(e) {
const query = e.target.value;
function showRow(index, row) {
const sources = row.querySelectorAll('.form-item');
let sourcesConcat = '';
// Concatenate the textContent of the elements in the row, with a
// space in between.
sources.forEach((item) => {
sourcesConcat += ` ${item.textContent}`;
});
// Make it case-insensitive.
const textMatch = sourcesConcat
.toLowerCase()
.includes(query.toLowerCase());
$(row).closest('tr').toggle(textMatch);
}
// Filter if the length of the query is at least 1 character.
if (query.length > 0) {
searching = true;
$rows.each(showRow);
} else if (searching) {
searching = false;
$rows.show();
}
}
function preventEnterKey(event) {
if (event.key === 'Enter') {
event.preventDefault();
event.stopPropagation();
}
}
if ($table.length) {
$rows = $table.find('tbody tr');
$(input).on({
keyup: debounce(filterRows, 200),
keydown: preventEnterKey,
});
}
},
};
/**
* Allows users to select an element which checks a radio button and
* adds a class used for css styling on different states.
*
* @type {Drupal~behavior}
*
* @prop {Drupal~behaviorAttach} attach
* Attaches behavior for selecting field.
*/
Drupal.behaviors.clickToSelect = {
attach(context) {
once('field-click-to-select', '.js-click-to-select', context).forEach(
(clickToSelectEl) => {
const input = clickToSelectEl.querySelector('input');
if (input) {
Drupal.behaviors.clickToSelect.clickHandler(clickToSelectEl, input);
}
if (input.classList.contains('error')) {
clickToSelectEl.classList.add('error');
}
if (input.checked) {
this.selectHandler(clickToSelectEl, input);
}
},
);
},
// Adds click event listener to the field card.
clickHandler(clickToSelectEl, input) {
$(clickToSelectEl).on('click', (event) => {
const clickToSelect = event.target.closest('.js-click-to-select');
this.selectHandler(clickToSelect, input);
$(input).trigger('updateOptions');
});
},
// Handles adding and removing classes for the different states.
selectHandler(clickToSelect, input) {
$(input).on('focus', () => clickToSelect.classList.add('focus'));
$(input).on('blur', () => clickToSelect.classList.remove('focus'));
input.checked = true;
document
.querySelectorAll('.js-click-to-select.selected')
.forEach((item) => {
item.classList.remove('selected');
});
clickToSelect.classList.add('selected');
// Ensure focus is added at the end of the process so wrap in
// a timeout.
setTimeout(() => {
// Remove the disabled attribute added by Drupal ajax so the
// element is focusable. This is safe as clicking the element
// multiple times causes no problems.
input.removeAttribute('disabled');
input.focus();
}, 0);
},
};
})(jQuery, Drupal, drupalSettings, Drupal.debounce);

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;
}
}

View File

@@ -0,0 +1,47 @@
{#
/**
* @file
* Default theme implementation to display a Field UI table.
*
* Available variables:
* - attributes: HTML attributes to apply to the <table> tag.
* - caption: A localized string for the <caption> tag.
* - colgroups: Column groups. Each group contains the following properties:
* - attributes: HTML attributes to apply to the <col> tag.
* Note: Drupal currently supports only one table header row, see
* https://www.drupal.org/node/893530 and
* http://api.drupal.org/api/drupal/includes!theme.inc/function/theme_table/7#comment-5109.
* - header: Table header cells. Each cell contains the following properties:
* - tag: The HTML tag name to use; either 'th' or 'td'.
* - attributes: HTML attributes to apply to the tag.
* - content: A localized string for the title of the column.
* - field: Field name (required for column sorting).
* - sort: Default sort order for this column ("asc" or "desc").
* - sticky: A flag indicating whether to use a "sticky" table header.
* - rows: Table rows. Each row contains the following properties:
* - attributes: HTML attributes to apply to the <tr> tag.
* - data: Table cells.
* - no_striping: A flag indicating that the row should receive no
* 'even / odd' styling. Defaults to FALSE.
* - cells: Table cells of the row. Each cell contains the following keys:
* - tag: The HTML tag name to use; either 'th' or 'td'.
* - attributes: Any HTML attributes, such as "colspan", to apply to the
* table cell.
* - content: The string to display in the table cell.
* - active_table_sort: A boolean indicating whether the cell is the active
table sort.
* - footer: Table footer rows, in the same format as the rows variable.
* - empty: The message to display in an extra row if table does not have
* any rows.
* - no_striping: A boolean indicating that the row should receive no striping.
* - header_columns: The number of columns in the header.
*
* @see template_preprocess_field_ui_table()
*
* @ingroup themeable
*/
#}
{# Add Ajax wrapper. #}
<div id="field-display-overview-wrapper">
{% include 'table.html.twig' %}
</div>

View File

@@ -0,0 +1,46 @@
{#
/**
* @file
* Default theme implementation for a storage type option form element.
*
* Available variables:
* - attributes: HTML attributes for the containing element.
* - errors: (optional) Any errors for this form element, may not be set.
* - label: A rendered label element.
* - description: (optional) A list of description properties containing:
* - content: A description of the form element, may not be set.
* - attributes: (optional) A list of HTML attributes to apply to the
* description content wrapper. Will only be set when description is set.
* - variant: specifies option type. Typically 'field-option' or 'field-suboption'.
*
* @see template_preprocess_form_element()
*
* @ingroup themeable
*/
#}
{%
set classes = [
errors ? 'form-item--error',
variant ? variant ~ '__item'
]
%}
<div{{ attributes.addClass(classes) }}>
{% if variant == 'field-option' %}
{{ label }}
<div{{ description.attributes }}>
{{ description.content }}
</div>
{% endif %}
{{ children }}
{% if variant == 'field-suboption' %}
{{ label }}
<div{{ description.attributes.addClass(description_classes) }}>
{{ description.content }}
</div>
{% endif %}
{% if errors %}
<div class="form-item--error-message">
{{ errors }}
</div>
{% endif %}
</div>

View File

@@ -0,0 +1,10 @@
name: 'Field UI test'
type: module
description: 'Support module for Field UI tests.'
package: Testing
# version: VERSION
# Information added by Drupal.org packaging script on 2024-07-04
version: '10.3.1'
project: 'drupal'
datestamp: 1720094222

View File

@@ -0,0 +1,68 @@
<?php
/**
* @file
* Field UI test module.
*/
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Render\Element;
use Drupal\Core\Field\FieldConfigInterface;
/**
* Implements hook_ENTITY_TYPE_access().
*/
function field_ui_test_field_config_access(FieldConfigInterface $field) {
return AccessResult::forbiddenIf($field->getName() == 'highlander');
}
/**
* Implements hook_form_FORM_BASE_ID_alter().
*/
function field_ui_test_form_entity_view_display_edit_form_alter(&$form, FormStateInterface $form_state) {
$table = &$form['fields'];
foreach (Element::children($table) as $name) {
$table[$name]['parent_wrapper']['parent']['#options'] = ['indent' => 'Indent'];
$table[$name]['parent_wrapper']['parent']['#default_value'] = 'indent';
}
$table['indent'] = [
'#attributes' => ['class' => ['draggable', 'field-group'], 'id' => 'indent-id'],
'#row_type' => 'group',
'#region_callback' => 'field_ui_test_region_callback',
'#js_settings' => ['rowHandler' => 'group'],
'human_name' => [
'#markup' => 'Indent',
'#prefix' => '<span class="group-label">',
'#suffix' => '</span>',
],
'weight' => [
'#type' => 'textfield',
'#default_value' => 0,
'#size' => 3,
'#attributes' => ['class' => ['field-weight']],
],
'parent_wrapper' => [
'parent' => [
'#type' => 'select',
'#options' => ['indent' => 'Indent'],
'#empty_value' => '',
'#default_value' => '',
'#attributes' => ['class' => ['field-parent']],
'#parents' => ['fields', 'indent', 'parent'],
],
'hidden_name' => [
'#type' => 'hidden',
'#default_value' => 'indent',
'#attributes' => ['class' => ['field-name']],
],
],
];
}
function field_ui_test_region_callback($row) {
return 'content';
}

View File

@@ -0,0 +1,10 @@
name: 'Field UI test deprecated'
type: module
description: 'Support module for testing deprecated Field UI functionality.'
package: Testing
# version: VERSION
# Information added by Drupal.org packaging script on 2024-07-04
version: '10.3.1'
project: 'drupal'
datestamp: 1720094222

View File

@@ -0,0 +1,29 @@
<?php
/**
* @file
* Field UI test module.
*/
use Drupal\Core\Form\FormStateInterface;
use Drupal\field\FieldStorageConfigInterface;
use Drupal\field_ui\Form\FieldStorageConfigEditForm;
/**
* Implements hook_form_FORM_ID_alter() for field_storage_config_edit_form.
*/
function field_ui_test_deprecated_form_field_storage_config_edit_form_alter(&$form, FormStateInterface $form_state) {
if (!($form_state->getFormObject() instanceof FieldStorageConfigEditForm)) {
throw new \LogicException('field_storage_config_edit_form() expects to get access to the field storage config entity edit form.');
}
if (!($form_state->getFormObject()->getEntity() instanceof FieldStorageConfigInterface)) {
throw new \LogicException('field_storage_config_edit_form() expects to get access to the field storage config entity.');
}
if (!isset($form['cardinality_container']['cardinality'])) {
throw new \LogicException('field_storage_config_edit_form() expects to that the cardinality container with the cardinality form element exists.');
}
$form['cardinality_container']['hello'] = [
'#markup' => 'Greetings from the field_storage_config_edit_form() alter.',
];
}

View File

@@ -0,0 +1,77 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\field_ui\Functional;
use Drupal\field\Entity\FieldConfig;
use Drupal\field\Entity\FieldStorageConfig;
use Drupal\Tests\BrowserTestBase;
/**
* Tests the UI for configuring entity displays.
*
* @group field_ui
*/
class EntityDisplayFormBaseTest extends BrowserTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['field_ui', 'entity_test', 'field_test'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
foreach (entity_test_entity_types() as $entity_type) {
// Auto-create fields for testing.
FieldStorageConfig::create([
'entity_type' => $entity_type,
'field_name' => 'field_test_no_plugin',
'type' => 'field_test',
'cardinality' => 1,
])->save();
FieldConfig::create([
'entity_type' => $entity_type,
'field_name' => 'field_test_no_plugin',
'bundle' => $entity_type,
'label' => 'Test field with no plugin',
'translatable' => FALSE,
])->save();
\Drupal::service('entity_display.repository')
->getFormDisplay($entity_type, $entity_type)
->setComponent('field_test_no_plugin', [
'type' => 'test_field_widget',
])
->save();
}
$this->drupalLogin($this->drupalCreateUser([
'administer entity_test form display',
]));
}
/**
* Ensures the entity is not affected when there are no applicable formatters.
*/
public function testNoApplicableFormatters(): void {
$storage = $this->container->get('entity_type.manager')->getStorage('entity_form_display');
$id = 'entity_test.entity_test.default';
$entity_before = $storage->load($id);
$this->drupalGet('entity_test/structure/entity_test/form-display');
$entity_after = $storage->load($id);
$this->assertSame($entity_before->toArray(), $entity_after->toArray());
}
}

View File

@@ -0,0 +1,221 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\field_ui\Functional;
use Drupal\Core\Entity\Entity\EntityFormMode;
use Drupal\Core\Entity\Entity\EntityViewMode;
use Drupal\Core\Url;
use Drupal\Tests\BrowserTestBase;
/**
* Tests the entity display modes UI.
*
* @group field_ui
*/
class EntityDisplayModeTest extends BrowserTestBase {
/**
* Modules to enable.
*
* @var string[]
*/
protected static $modules = ['block', 'entity_test', 'field_ui', 'node'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
// Create a node type.
$this->drupalCreateContentType([
'type' => 'article',
'name' => 'Article',
]);
$this->drupalPlaceBlock('local_actions_block');
$this->drupalPlaceBlock('page_title_block');
}
/**
* Tests the EntityViewMode user interface.
*/
public function testEntityViewModeUI(): void {
// Test the listing page.
$this->drupalGet('admin/structure/display-modes/view');
$this->assertSession()->statusCodeEquals(403);
$this->drupalLogin($this->drupalCreateUser(['administer display modes']));
$this->drupalGet('admin/structure/display-modes/view');
$this->assertSession()->statusCodeEquals(200);
$this->assertSession()->pageTextContains('Add view mode');
$this->assertSession()->linkByHrefExists('admin/structure/display-modes/view/add');
$this->assertSession()->linkByHrefExists('admin/structure/display-modes/view/add/entity_test');
$this->drupalGet('admin/structure/display-modes/view/add/entity_test_mulrev');
$this->assertSession()->statusCodeEquals(404);
$this->drupalGet('admin/structure/display-modes/view/add');
$this->assertSession()->linkNotExists('Test entity - revisions and data table', 'An entity type with no view builder cannot have view modes.');
// Test adding a view mode including dots in machine_name.
$this->clickLink('Test entity');
// Check if 'Name' field is required.
$this->assertTrue($this->getSession()->getPage()->findField('label')->hasClass('required'));
$edit = [
'id' => $this->randomMachineName() . '.' . $this->randomMachineName(),
'label' => $this->randomString(),
];
$this->submitForm($edit, 'Save');
$this->assertSession()->pageTextContains('The machine-readable name must contain only lowercase letters, numbers, and underscores.');
// Test adding a view mode.
$edit = [
'id' => $this->randomMachineName(),
'label' => $this->randomString(),
];
$this->submitForm($edit, 'Save');
$this->assertSession()->pageTextContains("Saved the {$edit['label']} view mode.");
// Test editing the view mode.
$this->drupalGet('admin/structure/display-modes/view/manage/entity_test.' . $edit['id']);
// Test that the link templates added by field_ui_entity_type_build() are
// generating valid routes.
$view_mode = EntityViewMode::load('entity_test.' . $edit['id']);
$this->assertEquals(Url::fromRoute('entity.entity_view_mode.collection')->toString(), $view_mode->toUrl('collection')->toString());
$this->assertEquals(Url::fromRoute('entity.entity_view_mode.add_form', ['entity_type_id' => $view_mode->getTargetType()])->toString(), $view_mode->toUrl('add-form')->toString());
$this->assertEquals(Url::fromRoute('entity.entity_view_mode.edit_form', ['entity_view_mode' => $view_mode->id()])->toString(), $view_mode->toUrl('edit-form')->toString());
$this->assertEquals(Url::fromRoute('entity.entity_view_mode.delete_form', ['entity_view_mode' => $view_mode->id()])->toString(), $view_mode->toUrl('delete-form')->toString());
// Test deleting the view mode.
$this->clickLink('Delete');
$this->assertSession()->pageTextContains("Are you sure you want to delete the view mode {$edit['label']}?");
$this->submitForm([], 'Delete');
$this->assertSession()->pageTextContains("The view mode {$edit['label']} has been deleted.");
}
/**
* Tests the EntityFormMode user interface.
*/
public function testEntityFormModeUI(): void {
// Test the listing page.
$this->drupalGet('admin/structure/display-modes/form');
$this->assertSession()->statusCodeEquals(403);
$this->drupalLogin($this->drupalCreateUser(['administer display modes']));
$this->drupalGet('admin/structure/display-modes/form');
$this->assertSession()->statusCodeEquals(200);
$this->assertSession()->pageTextContains('Add form mode');
$this->assertSession()->linkByHrefExists('admin/structure/display-modes/form/add');
$this->drupalGet('admin/structure/display-modes/form/add/entity_test_no_label');
$this->assertSession()->statusCodeEquals(404);
$this->drupalGet('admin/structure/display-modes/form/add');
$this->assertSession()->linkNotExists('Entity Test without label', 'An entity type with no form cannot have form modes.');
// Test adding a view mode including dots in machine_name.
$this->clickLink('Test entity');
// Check if 'Name' field is required.
$this->assertTrue($this->getSession()->getPage()->findField('label')->hasClass('required'));
$edit = [
'id' => $this->randomMachineName() . '.' . $this->randomMachineName(),
'label' => $this->randomString(),
];
$this->submitForm($edit, 'Save');
$this->assertSession()->pageTextContains('The machine-readable name must contain only lowercase letters, numbers, and underscores.');
// Test adding a form mode.
$edit = [
'id' => $this->randomMachineName(),
'label' => $this->randomString(),
];
$this->submitForm($edit, 'Save');
$this->assertSession()->pageTextContains("Saved the {$edit['label']} form mode.");
// Test editing the form mode.
$this->drupalGet('admin/structure/display-modes/form/manage/entity_test.' . $edit['id']);
// Test that the link templates added by field_ui_entity_type_build() are
// generating valid routes.
$form_mode = EntityFormMode::load('entity_test.' . $edit['id']);
$this->assertEquals(Url::fromRoute('entity.entity_form_mode.collection')->toString(), $form_mode->toUrl('collection')->toString());
$this->assertEquals(Url::fromRoute('entity.entity_form_mode.add_form', ['entity_type_id' => $form_mode->getTargetType()])->toString(), $form_mode->toUrl('add-form')->toString());
$this->assertEquals(Url::fromRoute('entity.entity_form_mode.edit_form', ['entity_form_mode' => $form_mode->id()])->toString(), $form_mode->toUrl('edit-form')->toString());
$this->assertEquals(Url::fromRoute('entity.entity_form_mode.delete_form', ['entity_form_mode' => $form_mode->id()])->toString(), $form_mode->toUrl('delete-form')->toString());
// Test deleting the form mode.
$this->clickLink('Delete');
$this->assertSession()->pageTextContains("Are you sure you want to delete the form mode {$edit['label']}?");
$this->submitForm([], 'Delete');
$this->assertSession()->pageTextContains("The form mode {$edit['label']} has been deleted.");
}
/**
* Tests if view modes appear in alphabetical order by visible name.
*
* The machine name should not be used for sorting.
*
* @see https://www.drupal.org/node/2858569
*/
public function testAlphabeticalDisplaySettings(): void {
$this->drupalLogin($this->drupalCreateUser([
'access administration pages',
'administer content types',
'administer display modes',
'administer nodes',
'administer node fields',
'administer node display',
'administer node form display',
'view the administration theme',
]));
$this->drupalGet('admin/structure/types/manage/article/display');
// Verify that the order of view modes is alphabetical by visible label.
// Since the default view modes all have machine names which coincide with
// the English labels, they should appear in alphabetical order, by default
// if viewing the site in English and if no changes have been made. We will
// verify this first.
$page_text = $this->getTextContent();
$start = strpos($page_text, 'view modes');
$pos = $start;
$list = ['Full content', 'RSS', 'Search index', 'Search result', 'Teaser'];
// Verify that the order of the view modes is correct on the page.
foreach ($list as $name) {
$new_pos = strpos($page_text, $name, $start);
$this->assertGreaterThan($pos, $new_pos);
$pos = $new_pos;
}
// Now that we have verified the original display order, we can change the
// label for one of the view modes. If we rename "Teaser" to "Breezier", it
// should appear as the first of the listed view modes:
// Set new values and enable test plugins.
$edit = [
'label' => 'Breezier',
];
$this->drupalGet('admin/structure/display-modes/view/manage/node.teaser');
$this->submitForm($edit, 'Save');
$this->assertSession()->pageTextContains('Saved the Breezier view mode.');
// Re-open the display settings for the article content type and verify
// that changing "Teaser" to "Breezier" makes it appear before "Full
// content".
$this->drupalGet('admin/structure/types/manage/article/display');
$page_text = $this->getTextContent();
$start = strpos($page_text, 'view modes');
$pos = $start;
$list = ['Breezier', 'Full content'];
// Verify that the order of the view modes is correct on the page.
foreach ($list as $name) {
$new_pos = strpos($page_text, $name, $start);
$this->assertGreaterThan($pos, $new_pos);
$pos = $new_pos;
}
}
}

View File

@@ -0,0 +1,53 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\field_ui\Functional;
use Drupal\Tests\BrowserTestBase;
/**
* Tests the UI for entity displays.
*
* @group field_ui
*/
class EntityDisplayTest extends BrowserTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['field_ui', 'entity_test'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->drupalLogin($this->drupalCreateUser([
'administer entity_test display',
]));
}
/**
* Tests the use of regions for entity view displays.
*/
public function testEntityView(): void {
$this->drupalGet('entity_test/structure/entity_test/display');
$this->assertSession()->elementExists('css', '.region-content-message.region-empty');
$this->assertTrue($this->assertSession()->optionExists('fields[field_test_text][region]', 'hidden')->isSelected());
$this->getSession()->getPage()->selectFieldOption('fields[field_test_text][region]', 'content');
$this->assertTrue($this->assertSession()->optionExists('fields[field_test_text][region]', 'content')->isSelected());
$this->submitForm([], 'Save');
$this->assertSession()->pageTextContains('Your settings have been saved.');
$this->assertTrue($this->assertSession()->optionExists('fields[field_test_text][region]', 'content')->isSelected());
}
}

View File

@@ -0,0 +1,62 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\field_ui\Functional;
use Drupal\Tests\BrowserTestBase;
/**
* Tests field UI integration with field type categories for loading libraries.
*
* @group field_ui
*/
class FieldTypeCategoriesIntegrationTest extends BrowserTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = [
'node',
'file',
'field_ui',
'options',
'comment',
'link',
];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
// Create a test user.
$admin_user = $this->drupalCreateUser(['administer node fields']);
$this->drupalLogin($admin_user);
}
/**
* Tests if the libraries are loaded on FieldStorageAddForm.
*/
public function testLibrariesLoaded(): void {
$this->drupalGet('admin/structure/types/manage/' . $this->drupalCreateContentType()->id() . '/fields/add-field');
$settings = $this->getDrupalSettings();
$css_libraries = [
'file/drupal.file-icon',
'text/drupal.text-icon',
'options/drupal.options-icon',
'comment/drupal.comment-icon',
'link/drupal.link-icon',
];
$libraries = explode(',', $settings['ajaxPageState']['libraries']);
foreach ($css_libraries as $css_library) {
$this->assertContains($css_library, $libraries);
}
}
}

View File

@@ -0,0 +1,150 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\field_ui\Functional;
use Drupal\field\Entity\FieldConfig;
use Drupal\field\Entity\FieldStorageConfig;
use Drupal\Tests\BrowserTestBase;
use Drupal\Tests\field_ui\Traits\FieldUiTestTrait;
use Drupal\views\Entity\View;
use Drupal\views\Tests\ViewTestData;
/**
* Tests deletion of a field and their dependencies in the UI.
*
* @group field_ui
*/
class FieldUIDeleteTest extends BrowserTestBase {
use FieldUiTestTrait;
/**
* Modules to install.
*
* @var array
*/
protected static $modules = [
'node',
'field_ui',
'field_test',
'block',
'field_test_views',
];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* Test views to enable.
*
* @var string[]
*/
public static $testViews = ['test_view_field_delete'];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->drupalPlaceBlock('system_breadcrumb_block');
$this->drupalPlaceBlock('local_tasks_block');
$this->drupalPlaceBlock('page_title_block');
$this->drupalPlaceBlock('local_actions_block');
// Create a test user.
$admin_user = $this->drupalCreateUser([
'access content',
'administer content types',
'administer node fields',
'administer node form display',
'administer node display',
'administer users',
'administer account settings',
'administer user display',
'bypass node access',
]);
$this->drupalLogin($admin_user);
}
/**
* Tests that deletion removes field storages and fields as expected.
*/
public function testDeleteField(): void {
$field_label = $this->randomMachineName();
$field_name_input = 'test';
$field_name = 'field_test';
// Create an additional node type.
$type_name1 = $this->randomMachineName(8) . '_test';
$type1 = $this->drupalCreateContentType(['name' => $type_name1, 'type' => $type_name1]);
$type_name1 = $type1->id();
// Create a new field.
$bundle_path1 = 'admin/structure/types/manage/' . $type_name1;
$this->fieldUIAddNewField($bundle_path1, $field_name_input, $field_label);
// Create an additional node type.
$type_name2 = $this->randomMachineName(8) . '_test';
$type2 = $this->drupalCreateContentType(['name' => $type_name2, 'type' => $type_name2]);
$type_name2 = $type2->id();
// Add a field to the second node type.
$bundle_path2 = 'admin/structure/types/manage/' . $type_name2;
$this->fieldUIAddExistingField($bundle_path2, $field_name, $field_label);
\Drupal::service('module_installer')->install(['views']);
ViewTestData::createTestViews(static::class, ['field_test_views']);
$view = View::load('test_view_field_delete');
$this->assertNotNull($view);
$this->assertTrue($view->status());
// Test that the View depends on the field.
$dependencies = $view->getDependencies() + ['config' => []];
$this->assertContains("field.storage.node.$field_name", $dependencies['config']);
// Check the config dependencies of the first field, the field storage must
// not be shown as being deleted yet.
$this->drupalGet("$bundle_path1/fields/node.$type_name1.$field_name/delete");
$this->assertSession()->pageTextNotContains('The listed configuration will be deleted.');
$this->assertSession()->elementNotExists('xpath', '//ul[@data-drupal-selector="edit-view"]');
$this->assertSession()->pageTextNotContains('test_view_field_delete');
// Delete the first field.
$this->fieldUIDeleteField($bundle_path1, "node.$type_name1.$field_name", $field_label, $type_name1, 'content type');
// Check that the field was deleted.
$this->assertNull(FieldConfig::loadByName('node', $type_name1, $field_name), 'Field was deleted.');
// Check that the field storage was not deleted.
$this->assertNotNull(FieldStorageConfig::loadByName('node', $field_name), 'Field storage was not deleted.');
// Check the config dependencies of the first field.
$this->drupalGet("$bundle_path2/fields/node.$type_name2.$field_name/delete");
$this->assertSession()->pageTextContains('The listed configuration will be updated.');
$this->assertSession()->elementTextEquals('xpath', '//ul[@data-drupal-selector="edit-view"]', 'test_view_field_delete');
// Test that nothing is scheduled for deletion.
$this->assertSession()->elementNotExists('css', '#edit-entity-deletes');
// Delete the second field.
$this->fieldUIDeleteField($bundle_path2, "node.$type_name2.$field_name", $field_label, $type_name2, 'content type');
// Check that the field was deleted.
$this->assertNull(FieldConfig::loadByName('node', $type_name2, $field_name), 'Field was deleted.');
// Check that the field storage was deleted too.
$this->assertNull(FieldStorageConfig::loadByName('node', $field_name), 'Field storage was deleted.');
// Test that the View isn't deleted and has been disabled.
$view = View::load('test_view_field_delete');
$this->assertNotNull($view);
$this->assertFalse($view->status());
// Test that the View no longer depends on the deleted field.
$dependencies = $view->getDependencies() + ['config' => []];
$this->assertNotContains("field.storage.node.$field_name", $dependencies['config']);
}
}

View File

@@ -0,0 +1,52 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\field_ui\Functional;
use Drupal\Tests\BrowserTestBase;
/**
* Tests indentation on Field UI.
*
* @group field_ui
*/
class FieldUIIndentationTest extends BrowserTestBase {
/**
* Modules to install.
*
* @var array
*/
protected static $modules = ['node', 'field_ui', 'field_ui_test'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
// Create a test user.
$admin_user = $this->drupalCreateUser([
'access content',
'administer content types',
'administer node display',
]);
$this->drupalLogin($admin_user);
// Create Basic page node type.
$this->drupalCreateContentType(['type' => 'page', 'name' => 'Basic page']);
}
public function testIndentation(): void {
$this->drupalGet('admin/structure/types/manage/page/display');
$this->assertSession()->responseContains('js-indentation indentation');
}
}

View File

@@ -0,0 +1,140 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\field_ui\Functional;
use Drupal\Core\Entity\Entity\EntityFormMode;
use Drupal\Core\Entity\Entity\EntityViewMode;
use Drupal\Tests\BrowserTestBase;
/**
* Tests the functionality of the Field UI route subscriber.
*
* @group field_ui
*/
class FieldUIRouteTest extends BrowserTestBase {
/**
* Modules to install.
*
* @var string[]
*/
protected static $modules = ['block', 'entity_test', 'field_ui'];
/**
* {@inheritdoc}
*
* @todo Remove and fix test to not rely on super user.
* @see https://www.drupal.org/project/drupal/issues/3437620
*/
protected bool $usesSuperUserAccessPolicy = TRUE;
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->drupalLogin($this->rootUser);
$this->drupalPlaceBlock('local_tasks_block');
}
/**
* Ensures that entity types with bundles do not break following entity types.
*/
public function testFieldUIRoutes(): void {
$this->drupalGet('entity_test_no_id/structure/entity_test/fields');
$this->assertSession()->pageTextContains('No fields are present yet.');
$this->drupalGet('admin/config/people/accounts/fields');
$this->assertSession()->titleEquals('Manage fields | Drupal');
$this->assertLocalTasks();
// Test manage display tabs and titles.
$this->drupalGet('admin/config/people/accounts/display/compact');
$this->assertSession()->statusCodeEquals(403);
$this->drupalGet('admin/config/people/accounts/display');
$this->assertSession()->titleEquals('Manage display | Drupal');
$this->assertLocalTasks();
$edit = ['display_modes_custom[compact]' => TRUE];
$this->submitForm($edit, 'Save');
$this->drupalGet('admin/config/people/accounts/display/compact');
$this->assertSession()->titleEquals('Manage display | Drupal');
$this->assertLocalTasks();
// Test manage form display tabs and titles.
$this->drupalGet('admin/config/people/accounts/form-display/register');
$this->assertSession()->statusCodeEquals(403);
$this->drupalGet('admin/config/people/accounts/form-display');
$this->assertSession()->titleEquals('Manage form display | Drupal');
$this->assertLocalTasks();
$edit = ['display_modes_custom[register]' => TRUE];
$this->submitForm($edit, 'Save');
$this->assertSession()->statusCodeEquals(200);
$this->drupalGet('admin/config/people/accounts/form-display/register');
$this->assertSession()->titleEquals('Manage form display | Drupal');
$this->assertLocalTasks();
// Test that default secondary tab is in first position.
$this->assertSession()->elementsCount('xpath', "//ul/li[1]/a[contains(text(), 'Default')]", 1);
// Create new view mode and verify it's available on the Manage Display
// screen after enabling it.
EntityViewMode::create([
'id' => 'user.test',
'label' => 'Test',
'targetEntityType' => 'user',
])->save();
$this->container->get('router.builder')->rebuildIfNeeded();
$edit = ['display_modes_custom[test]' => TRUE];
$this->drupalGet('admin/config/people/accounts/display');
$this->submitForm($edit, 'Save');
$this->assertSession()->linkExists('Test');
// Create new form mode and verify it's available on the Manage Form
// Display screen after enabling it.
EntityFormMode::create([
'id' => 'user.test',
'label' => 'Test',
'targetEntityType' => 'user',
])->save();
$this->container->get('router.builder')->rebuildIfNeeded();
$edit = ['display_modes_custom[test]' => TRUE];
$this->drupalGet('admin/config/people/accounts/form-display');
$this->submitForm($edit, 'Save');
$this->assertSession()->linkExists('Test');
}
/**
* Asserts that local tasks exists.
*
* @internal
*/
public function assertLocalTasks(): void {
$this->assertSession()->linkExists('Settings');
$this->assertSession()->linkExists('Manage fields');
$this->assertSession()->linkExists('Manage display');
$this->assertSession()->linkExists('Manage form display');
}
/**
* Asserts that admin routes are correctly marked as such.
*/
public function testAdminRoute(): void {
$route = \Drupal::service('router.route_provider')->getRouteByName('entity.entity_test.field_ui_fields');
$is_admin = \Drupal::service('router.admin_context')->isAdminRoute($route);
$this->assertTrue($is_admin, 'Admin route correctly marked for "Manage fields" page.');
}
}

View File

@@ -0,0 +1,14 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\field_ui\Functional;
use Drupal\Tests\system\Functional\Module\GenericModuleTestBase;
/**
* Generic module test for field_ui.
*
* @group field_ui
*/
class GenericTest extends GenericModuleTestBase {}

View File

@@ -0,0 +1,441 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\field_ui\Functional;
use Behat\Mink\Exception\ExpectationException;
use Drupal\Core\Entity\Entity\EntityFormMode;
use Drupal\Core\Url;
use Behat\Mink\Element\NodeElement;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Language\LanguageInterface;
use Drupal\node\Entity\NodeType;
use Drupal\taxonomy\Entity\Vocabulary;
use Drupal\Tests\BrowserTestBase;
use Drupal\Tests\field_ui\Traits\FieldUiTestTrait;
/**
* Tests the Field UI "Manage display" and "Manage form display" screens.
*
* @group field_ui
* @group #slow
*/
class ManageDisplayTest extends BrowserTestBase {
use FieldUiTestTrait;
/**
* Modules to install.
*
* @var array
*/
protected static $modules = [
'node',
'field_ui',
'taxonomy',
'search',
'field_test',
'field_third_party_test',
'block',
];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* @var string
*/
private string $type;
/**
* @var string
*/
private string $vocabulary;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->drupalPlaceBlock('system_breadcrumb_block');
$this->drupalPlaceBlock('local_tasks_block');
// Create a test user.
$admin_user = $this->drupalCreateUser([
'access content',
'administer content types',
'administer display modes',
'administer node fields',
'administer node form display',
'administer node display',
'administer taxonomy',
'administer taxonomy_term fields',
'administer taxonomy_term display',
'administer users',
'administer account settings',
'administer user display',
'bypass node access',
]);
$this->drupalLogin($admin_user);
// Create content type, with underscores.
$type_name = $this->randomMachineName(8) . '_test';
$type = $this->drupalCreateContentType(['name' => $type_name, 'type' => $type_name]);
$this->type = $type->id();
// Create a default vocabulary.
$vocabulary = Vocabulary::create([
'name' => $this->randomMachineName(),
'description' => $this->randomMachineName(),
'vid' => $this->randomMachineName(),
'langcode' => LanguageInterface::LANGCODE_NOT_SPECIFIED,
'help' => '',
'nodes' => ['article' => 'article'],
'weight' => mt_rand(0, 10),
]);
$vocabulary->save();
$this->vocabulary = $vocabulary->id();
}
/**
* Tests switching view modes to use custom or 'default' settings'.
*/
public function testViewModeCustom(): void {
// Create a field, and a node with some data for the field.
$this->fieldUIAddNewField('admin/structure/types/manage/' . $this->type, 'test', 'Test field');
\Drupal::service('entity_field.manager')->clearCachedFieldDefinitions();
// For this test, use a formatter setting value that is an integer unlikely
// to appear in a rendered node other than as part of the field being tested
// (for example, unlikely to be part of the "Submitted by ... on ..." line).
$value = '12345';
$settings = [
'type' => $this->type,
'field_test' => [['value' => $value]],
];
$node = $this->drupalCreateNode($settings);
// Gather expected output values with the various formatters.
$formatter_plugin_manager = \Drupal::service('plugin.manager.field.formatter');
$field_test_default_settings = $formatter_plugin_manager->getDefaultSettings('field_test_default');
$field_test_with_prepare_view_settings = $formatter_plugin_manager->getDefaultSettings('field_test_with_prepare_view');
$output = [
'field_test_default' => $field_test_default_settings['test_formatter_setting'] . '|' . $value,
'field_test_with_prepare_view' => $field_test_with_prepare_view_settings['test_formatter_setting_additional'] . '|' . $value . '|' . ($value + 1),
];
// Check that the field is displayed with the default formatter in 'rss'
// mode (uses 'default'), and hidden in 'teaser' mode (uses custom settings).
$this->assertNodeViewText($node, 'rss', $output['field_test_default'], "The field is displayed as expected in view modes that use 'default' settings.");
$this->assertNodeViewNoText($node, 'teaser', $value, "The field is hidden in view modes that use custom settings.");
// Change formatter for 'default' mode, check that the field is displayed
// accordingly in 'rss' mode.
$edit = [
'fields[field_test][type]' => 'field_test_with_prepare_view',
'fields[field_test][region]' => 'content',
];
$this->drupalGet('admin/structure/types/manage/' . $this->type . '/display');
$this->submitForm($edit, 'Save');
$this->assertNodeViewText($node, 'rss', $output['field_test_with_prepare_view'], "The field is displayed as expected in view modes that use 'default' settings.");
// Specialize the 'rss' mode, check that the field is displayed the same.
$edit = [
"display_modes_custom[rss]" => TRUE,
];
$this->drupalGet('admin/structure/types/manage/' . $this->type . '/display');
$this->submitForm($edit, 'Save');
$this->assertNodeViewText($node, 'rss', $output['field_test_with_prepare_view'], "The field is displayed as expected in newly specialized 'rss' mode.");
// Set the field to 'hidden' in the view mode, check that the field is
// hidden.
$edit = [
'fields[field_test][region]' => 'hidden',
];
$this->drupalGet('admin/structure/types/manage/' . $this->type . '/display/rss');
$this->submitForm($edit, 'Save');
$this->assertNodeViewNoText($node, 'rss', $value, "The field is hidden in 'rss' mode.");
// Set the view mode back to 'default', check that the field is displayed
// accordingly.
$edit = [
"display_modes_custom[rss]" => FALSE,
];
$this->drupalGet('admin/structure/types/manage/' . $this->type . '/display');
$this->submitForm($edit, 'Save');
$this->assertNodeViewText($node, 'rss', $output['field_test_with_prepare_view'], "The field is displayed as expected when 'rss' mode is set back to 'default' settings.");
// Specialize the view mode again.
$edit = [
"display_modes_custom[rss]" => TRUE,
];
$this->drupalGet('admin/structure/types/manage/' . $this->type . '/display');
$this->submitForm($edit, 'Save');
// Check that the previous settings for the view mode have been kept.
$this->assertNodeViewNoText($node, 'rss', $value, "The previous settings are kept when 'rss' mode is specialized again.");
}
/**
* Tests the local tasks are displayed correctly for view modes.
*/
public function testViewModeLocalTasks(): void {
$manage_display = 'admin/structure/types/manage/' . $this->type . '/display';
$this->drupalGet($manage_display);
$this->assertSession()->linkNotExists('Full content');
$this->assertSession()->linkExists('Teaser');
$this->drupalGet($manage_display . '/teaser');
$this->assertSession()->linkNotExists('Full content');
$this->assertSession()->linkExists('Default');
}
/**
* Tests that fields with no explicit display settings do not break.
*/
public function testNonInitializedFields(): void {
// Create a test field.
$this->fieldUIAddNewField('admin/structure/types/manage/' . $this->type, 'test', 'Test');
// Check that the field appears as 'hidden' on the 'Manage display' page
// for the 'teaser' mode.
$this->drupalGet('admin/structure/types/manage/' . $this->type . '/display/teaser');
$this->assertSession()->fieldValueEquals('fields[field_test][region]', 'hidden');
}
/**
* Tests hiding the view modes fieldset when there's only one available.
*/
public function testSingleViewMode(): void {
$this->drupalGet('admin/structure/taxonomy/manage/' . $this->vocabulary . '/display');
$this->assertSession()->pageTextNotContains('Use custom display settings for the following view modes');
// This may not trigger a notice when 'view_modes_custom' isn't available.
$this->drupalGet('admin/structure/taxonomy/manage/' . $this->vocabulary . '/overview/display');
$this->submitForm([], 'Save');
}
/**
* Tests that a message is shown when there are no fields.
*/
public function testNoFieldsDisplayOverview(): void {
// Create a fresh content type without any fields.
NodeType::create([
'type' => 'no_fields',
'name' => 'No fields',
])->save();
$this->drupalGet('admin/structure/types/manage/no_fields/display');
$this->assertSession()->pageTextContains("There are no fields yet added. You can add new fields on the Manage fields page.");
$this->assertSession()->linkByHrefExists(Url::fromRoute('entity.node.field_ui_fields', ['node_type' => 'no_fields'])->toString());
}
/**
* Tests if display mode local tasks appear in alphabetical order by label.
*/
public function testViewModeLocalTasksOrder(): void {
$manage_display = 'admin/structure/types/manage/' . $this->type . '/display';
// Specify the 'rss' mode, check that the field is displayed the same.
$edit = [
'display_modes_custom[rss]' => TRUE,
'display_modes_custom[teaser]' => TRUE,
];
$this->drupalGet($manage_display);
$this->submitForm($edit, 'Save');
$this->assertOrderInPage(['RSS', 'Teaser']);
$edit = [
'label' => 'Breezier',
];
$this->drupalGet('admin/structure/display-modes/view/manage/node.teaser');
$this->submitForm($edit, 'Save');
$this->assertOrderInPage(['Breezier', 'RSS']);
}
/**
* Tests if form mode local tasks appear in alphabetical order by label.
*/
public function testFormModeLocalTasksOrder(): void {
EntityFormMode::create([
'id' => 'node.big',
'label' => 'Big Form',
'targetEntityType' => 'node',
])->save();
EntityFormMode::create([
'id' => 'node.little',
'label' => 'Little Form',
'targetEntityType' => 'node',
])->save();
$manage_form = 'admin/structure/types/manage/' . $this->type . '/form-display';
$this->drupalGet($manage_form);
$this->assertOrderInPage(['Big Form', 'Little Form']);
$edit = [
'label' => 'Ultimate Form',
];
$this->drupalGet('admin/structure/display-modes/form/manage/node.big');
$this->submitForm($edit, 'Save');
$this->drupalGet($manage_form);
$this->assertOrderInPage(['Little Form', 'Ultimate Form']);
}
/**
* Asserts that a string is found in the rendered node in a view mode.
*
* @param \Drupal\Core\Entity\EntityInterface $node
* The node.
* @param string $view_mode
* The view mode in which the node should be displayed.
* @param string $text
* Plain text to look for.
* @param string $message
* Message to display.
*
* @internal
*/
public function assertNodeViewText(EntityInterface $node, string $view_mode, string $text, string $message): void {
$this->assertNodeViewTextHelper($node, $view_mode, $text, $message, FALSE);
}
/**
* Asserts that a string is not found in the rendered node in a view mode.
*
* @param \Drupal\Core\Entity\EntityInterface $node
* The node.
* @param string $view_mode
* The view mode in which the node should be displayed.
* @param string $text
* Plain text to look for.
* @param string $message
* Message to display.
*
* @internal
*/
public function assertNodeViewNoText(EntityInterface $node, string $view_mode, string $text, string $message): void {
$this->assertNodeViewTextHelper($node, $view_mode, $text, $message, TRUE);
}
/**
* Asserts that a string is (not) found in the rendered node in a view mode.
*
* This helper function is used by assertNodeViewText() and
* assertNodeViewNoText().
*
* @param \Drupal\Core\Entity\EntityInterface $node
* The node.
* @param string $view_mode
* The view mode in which the node should be displayed.
* @param string $text
* Plain text to look for.
* @param string $message
* Message to display.
* @param bool $not_exists
* TRUE if this text should not exist, FALSE if it should.
*
* @internal
*/
public function assertNodeViewTextHelper(EntityInterface $node, string $view_mode, string $text, string $message, bool $not_exists): void {
// Make sure caches on the tester side are refreshed after changes
// submitted on the tested side.
\Drupal::service('entity_field.manager')->clearCachedFieldDefinitions();
// Render a cloned node, so that we do not alter the original.
$clone = clone $node;
$element = \Drupal::entityTypeManager()
->getViewBuilder('node')
->view($clone, $view_mode);
$output = (string) \Drupal::service('renderer')->renderRoot($element);
if ($not_exists) {
$this->assertStringNotContainsString((string) $text, $output, $message);
}
else {
$this->assertStringContainsString((string) $text, $output, $message);
}
}
/**
* Checks if a select element contains the specified options.
*
* @param string $name
* The field name.
* @param array $expected_options
* An array of expected options.
*
* @internal
*/
protected function assertFieldSelectOptions(string $name, array $expected_options): void {
$xpath = $this->assertSession()->buildXPathQuery('//select[@name=:name]', [':name' => $name]);
$fields = $this->xpath($xpath);
if ($fields) {
$field = $fields[0];
$options = $this->getAllOptionsList($field);
sort($options);
sort($expected_options);
$this->assertSame($expected_options, $options);
}
else {
$this->fail('Unable to find field ' . $name);
}
}
/**
* Extracts all options from a select element.
*
* @param \Behat\Mink\Element\NodeElement $element
* The select element field information.
*
* @return array
* An array of option values as strings.
*/
protected function getAllOptionsList(NodeElement $element) {
$options = [];
// Add all options items.
foreach ($element->option as $option) {
$options[] = $option->getValue();
}
// Loops trough all the option groups
foreach ($element->optgroup as $optgroup) {
$options = array_merge($this->getAllOptionsList($optgroup), $options);
}
return $options;
}
/**
* Asserts that several pieces of markup are in a given order in the page.
*
* @param string[] $items
* An ordered list of strings.
*
* @throws \Behat\Mink\Exception\ExpectationException
* When any of the given string is not found.
*
* @internal
*
* @todo Remove this once https://www.drupal.org/node/2817657 is committed.
*/
protected function assertOrderInPage(array $items): void {
$session = $this->getSession();
$text = $session->getPage()->getHtml();
$strings = [];
foreach ($items as $item) {
if (($pos = strpos($text, $item)) === FALSE) {
throw new ExpectationException("Cannot find '$item' in the page", $session->getDriver());
}
$strings[$pos] = $item;
}
ksort($strings);
$ordered = implode(', ', array_map(function ($item) {
return "'$item'";
}, $items));
$this->assertSame($items, array_values($strings), "Found strings, ordered as: $ordered.");
}
}

View File

@@ -0,0 +1,440 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\field_ui\Functional;
use Behat\Mink\Exception\ElementNotFoundException;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
use Drupal\field\Entity\FieldConfig;
use Drupal\field\Entity\FieldStorageConfig;
/**
* Tests the Field UI "Manage fields" screen.
*
* @group field_ui
* @group #slow
*/
class ManageFieldsFunctionalTest extends ManageFieldsFunctionalTestBase {
/**
* Tests that default value is correctly validated and saved.
*/
public function testDefaultValue(): void {
// Create a test field storage and field.
$field_name = 'test';
FieldStorageConfig::create([
'field_name' => $field_name,
'entity_type' => 'node',
'type' => 'test_field',
])->save();
$field = FieldConfig::create([
'field_name' => $field_name,
'entity_type' => 'node',
'bundle' => $this->contentType,
]);
$field->save();
/** @var \Drupal\Core\Entity\EntityDisplayRepositoryInterface $display_repository */
$display_repository = \Drupal::service('entity_display.repository');
$display_repository->getFormDisplay('node', $this->contentType)
->setComponent($field_name)
->save();
$admin_path = 'admin/structure/types/manage/' . $this->contentType . '/fields/' . $field->id();
$element_id = "edit-default-value-input-$field_name-0-value";
$element_name = "default_value_input[{$field_name}][0][value]";
$this->drupalGet($admin_path);
$this->assertSession()->fieldValueEquals($element_id, '');
// Check that invalid default values are rejected.
$edit = [$element_name => '-1', 'set_default_value' => '1'];
$this->drupalGet($admin_path);
$this->submitForm($edit, 'Save settings');
$this->assertSession()->pageTextContains("$field_name does not accept the value -1");
// Check that the default value is saved.
$edit = [$element_name => '1', 'set_default_value' => '1'];
$this->drupalGet($admin_path);
$this->submitForm($edit, 'Save settings');
$this->assertSession()->pageTextContains("Saved $field_name configuration");
$field = FieldConfig::loadByName('node', $this->contentType, $field_name);
$this->assertEquals([['value' => 1]], $field->getDefaultValueLiteral(), 'The default value was correctly saved.');
// Check that the default value shows up in the form.
$this->drupalGet($admin_path);
$this->assertSession()->fieldValueEquals($element_id, '1');
// Check that the default value is left empty when "Set default value"
// checkbox is not checked.
$edit = [$element_name => '1', 'set_default_value' => '0'];
$this->drupalGet($admin_path);
$this->submitForm($edit, 'Save settings');
$this->assertSession()->pageTextContains("Saved $field_name configuration");
$field = FieldConfig::loadByName('node', $this->contentType, $field_name);
$this->assertEquals([], $field->getDefaultValueLiteral(), 'The default value was removed.');
// Check that the default value can be emptied.
$this->drupalGet($admin_path);
$edit = [$element_name => ''];
$this->submitForm($edit, 'Save settings');
$this->assertSession()->pageTextContains("Saved $field_name configuration");
$field = FieldConfig::loadByName('node', $this->contentType, $field_name);
$this->assertEquals([], $field->getDefaultValueLiteral(), 'The default value was correctly saved.');
// Check that the default value can be empty when the field is marked as
// required and can store unlimited values.
$field_storage = FieldStorageConfig::loadByName('node', $field_name);
$field_storage->setCardinality(FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED);
$field_storage->save();
$this->drupalGet($admin_path);
$edit = [
'required' => 1,
];
$this->submitForm($edit, 'Save settings');
$this->drupalGet($admin_path);
$this->submitForm([], 'Save settings');
$this->assertSession()->pageTextContains("Saved $field_name configuration");
$field = FieldConfig::loadByName('node', $this->contentType, $field_name);
$this->assertEquals([], $field->getDefaultValueLiteral(), 'The default value was correctly saved.');
// Check that the default widget is used when the field is hidden.
$display_repository->getFormDisplay($field->getTargetEntityTypeId(), $field->getTargetBundle())
->removeComponent($field_name)
->save();
$this->drupalGet($admin_path);
$this->assertSession()->fieldValueEquals($element_id, '');
}
/**
* Tests that Field UI respects disallowed field names.
*/
public function testDisallowedFieldNames(): void {
// Reset the field prefix so we can test properly.
$this->config('field_ui.settings')->set('field_prefix', '')->save();
$label = 'Disallowed field';
$edit1 = [
'new_storage_type' => 'test_field',
];
$edit2 = [
'label' => $label,
];
// Try with an entity key.
$edit2['field_name'] = 'title';
$bundle_path = 'admin/structure/types/manage/' . $this->contentType;
$this->drupalGet("{$bundle_path}/fields/add-field");
$this->submitForm($edit1, 'Continue');
$this->submitForm($edit2, 'Continue');
$this->assertSession()->pageTextContains('The machine-readable name is already in use. It must be unique.');
// Try with a base field.
$edit2['field_name'] = 'sticky';
$bundle_path = 'admin/structure/types/manage/' . $this->contentType;
$this->drupalGet("{$bundle_path}/fields/add-field");
$this->submitForm($edit1, 'Continue');
$this->submitForm($edit2, 'Continue');
$this->assertSession()->pageTextContains('The machine-readable name is already in use. It must be unique.');
}
/**
* Tests that Field UI respects locked fields.
*/
public function testLockedField(): void {
// Create a locked field and attach it to a bundle. We need to do this
// programmatically as there's no way to create a locked field through UI.
$field_name = $this->randomMachineName(8);
$field_storage = FieldStorageConfig::create([
'field_name' => $field_name,
'entity_type' => 'node',
'type' => 'test_field',
'cardinality' => 1,
'locked' => TRUE,
]);
$field_storage->save();
FieldConfig::create([
'field_storage' => $field_storage,
'bundle' => $this->contentType,
])->save();
\Drupal::service('entity_display.repository')
->getFormDisplay('node', $this->contentType)
->setComponent($field_name, [
'type' => 'test_field_widget',
])
->save();
// Check that the links for edit and delete are not present.
$this->drupalGet('admin/structure/types/manage/' . $this->contentType . '/fields');
$locked = $this->xpath('//tr[@id=:field_name]/td[4]', [':field_name' => $field_name]);
$this->assertSame('Locked', $locked[0]->getHtml(), 'Field is marked as Locked in the UI');
$this->drupalGet('admin/structure/types/manage/' . $this->contentType . '/fields/node.' . $this->contentType . '.' . $field_name . '/delete');
$this->assertSession()->statusCodeEquals(403);
}
/**
* Tests that Field UI respects the 'no_ui' flag in the field type definition.
*/
public function testHiddenFields(): void {
// Check that the field type is not available in the 'add new field' row.
$this->drupalGet('admin/structure/types/manage/' . $this->contentType . '/fields/add-field');
$this->assertSession()->elementNotExists('css', "[name='new_storage_type'][value='hidden_test_field']");
$this->assertSession()->elementExists('css', "[name='new_storage_type'][value='shape']");
// Create a field storage and a field programmatically.
$field_name = 'hidden_test_field';
FieldStorageConfig::create([
'field_name' => $field_name,
'entity_type' => 'node',
'type' => $field_name,
])->save();
$field = [
'field_name' => $field_name,
'bundle' => $this->contentType,
'entity_type' => 'node',
'label' => 'Hidden field',
];
FieldConfig::create($field)->save();
\Drupal::service('entity_display.repository')
->getFormDisplay('node', $this->contentType)
->setComponent($field_name)
->save();
$this->assertInstanceOf(FieldConfig::class, FieldConfig::load('node.' . $this->contentType . '.' . $field_name));
// Check that the newly added field appears on the 'Manage Fields'
// screen.
$this->drupalGet('admin/structure/types/manage/' . $this->contentType . '/fields');
$this->assertSession()->elementTextContains('xpath', '//table[@id="field-overview"]//tr[@id="hidden-test-field"]//td[1]', $field['label']);
// Check that the field does not appear in the 're-use existing field' row
// on other bundles.
$this->drupalGet('admin/structure/types/manage/page/fields/reuse');
$this->assertSession()->elementNotExists('css', ".js-reuse-table [data-field-id='{$field_name}']");
$this->assertSession()->elementExists('css', '.js-reuse-table [data-field-id="field_tags"]');
// Check that non-configurable fields are not available.
$field_types = \Drupal::service('plugin.manager.field.field_type')->getDefinitions();
$this->drupalGet('admin/structure/types/manage/page/fields/add-field');
foreach ($field_types as $field_type => $definition) {
if (empty($definition['no_ui'])) {
try {
$this->assertSession()
->elementExists('css', "[name='new_storage_type'][value='$field_type']");
}
catch (ElementNotFoundException) {
if ($group = $this->getFieldFromGroup($field_type)) {
$this->assertSession()
->elementExists('css', "[name='new_storage_type'][value='$group']");
$this->submitForm(['new_storage_type' => $group], 'Continue');
$this->assertSession()
->elementExists('css', "[name='group_field_options_wrapper'][value='$field_type']");
$this->submitForm([], 'Back');
}
}
}
else {
$this->assertSession()->elementNotExists('css', "[name='new_storage_type'][value='$field_type']");
}
}
}
/**
* Tests that a duplicate field name is caught by validation.
*/
public function testDuplicateFieldName(): void {
// field_tags already exists, so we're expecting an error when trying to
// create a new field with the same name.
$url = 'admin/structure/types/manage/' . $this->contentType . '/fields/add-field';
$this->drupalGet($url);
$edit = [
'new_storage_type' => 'boolean',
];
$this->submitForm($edit, 'Continue');
$edit = [
'label' => $this->randomMachineName(),
'field_name' => 'tags',
];
$this->submitForm($edit, 'Continue');
$this->assertSession()->pageTextContains('The machine-readable name is already in use. It must be unique.');
$this->assertSession()->addressEquals($url);
}
/**
* Tests that external URLs in the 'destinations' query parameter are blocked.
*/
public function testExternalDestinations(): void {
$options = [
'query' => ['destinations' => ['http://example.com']],
];
$this->drupalGet('admin/structure/types/manage/article/fields/node.article.body', $options);
$this->submitForm([], 'Save settings');
// The external redirect should not fire.
$this->assertSession()->addressEquals('admin/structure/types/manage/article/fields/node.article.body?destinations%5B0%5D=http%3A//example.com');
$this->assertSession()->statusCodeEquals(200);
$this->assertSession()->responseContains('Attempt to update field <em class="placeholder">Body</em> failed: <em class="placeholder">The internal path component &#039;http://example.com&#039; is external. You are not allowed to specify an external URL together with internal:/.</em>.');
}
/**
* Tests that deletion removes field storages and fields as expected for a term.
*/
public function testDeleteTaxonomyField(): void {
// Create a new field.
$bundle_path = 'admin/structure/taxonomy/manage/tags/overview';
$this->fieldUIAddNewField($bundle_path, $this->fieldNameInput, $this->fieldLabel);
// Delete the field.
$this->fieldUIDeleteField($bundle_path, "taxonomy_term.tags.$this->fieldName", $this->fieldLabel, 'Tags', 'taxonomy vocabulary');
// Check that the field was deleted.
$this->assertNull(FieldConfig::loadByName('taxonomy_term', 'tags', $this->fieldName), 'Field was deleted.');
// Check that the field storage was deleted too.
$this->assertNull(FieldStorageConfig::loadByName('taxonomy_term', $this->fieldName), 'Field storage was deleted.');
}
/**
* Tests that help descriptions render valid HTML.
*/
public function testHelpDescriptions(): void {
// Create an image field.
FieldStorageConfig::create([
'field_name' => 'field_image',
'entity_type' => 'node',
'type' => 'image',
])->save();
FieldConfig::create([
'field_name' => 'field_image',
'entity_type' => 'node',
'label' => 'Image',
'bundle' => 'article',
])->save();
\Drupal::service('entity_display.repository')
->getFormDisplay('node', 'article')
->setComponent('field_image')
->save();
$edit = [
'description' => '<strong>Test with an upload field.',
];
$this->drupalGet('admin/structure/types/manage/article/fields/node.article.field_image');
$this->submitForm($edit, 'Save settings');
// Check that hook_field_widget_single_element_form_alter() does believe
// this is the default value form.
$this->drupalGet('admin/structure/types/manage/article/fields/node.article.field_tags');
$this->assertSession()->pageTextContains('From hook_field_widget_single_element_form_alter(): Default form is true.');
$edit = [
'description' => '<em>Test with a non upload field.',
];
$this->drupalGet('admin/structure/types/manage/article/fields/node.article.field_tags');
$this->submitForm($edit, 'Save settings');
$this->drupalGet('node/add/article');
$this->assertSession()->responseContains('<strong>Test with an upload field.</strong>');
$this->assertSession()->responseContains('<em>Test with a non upload field.</em>');
}
/**
* Tests the "preconfigured field" functionality.
*
* @see \Drupal\Core\Field\PreconfiguredFieldUiOptionsInterface
*/
public function testPreconfiguredFields(): void {
$this->drupalGet('admin/structure/types/manage/article/fields/add-field');
// Check that the preconfigured field option exist alongside the regular
// field type option.
$this->assertSession()->elementExists('css', "[name='new_storage_type'][value='field_ui:test_field_with_preconfigured_options:custom_options']");
$this->assertSession()->elementExists('css', "[name='new_storage_type'][value='test_field_with_preconfigured_options']");
// Add a field with every possible preconfigured value.
$this->fieldUIAddNewField(NULL, 'test_custom_options', 'Test label', 'field_ui:test_field_with_preconfigured_options:custom_options');
$field_storage = FieldStorageConfig::loadByName('node', 'field_test_custom_options');
$this->assertEquals(FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED, $field_storage->getCardinality());
$this->assertEquals('preconfigured_storage_setting', $field_storage->getSetting('test_field_storage_setting'));
$field = FieldConfig::loadByName('node', 'article', 'field_test_custom_options');
$this->assertTrue($field->isRequired());
$this->assertEquals('preconfigured_field_setting', $field->getSetting('test_field_setting'));
/** @var \Drupal\Core\Entity\EntityDisplayRepositoryInterface $display_repository */
$display_repository = \Drupal::service('entity_display.repository');
$form_display = $display_repository->getFormDisplay('node', 'article');
$this->assertEquals('test_field_widget_multiple', $form_display->getComponent('field_test_custom_options')['type']);
$view_display = $display_repository->getViewDisplay('node', 'article');
$this->assertEquals('field_test_multiple', $view_display->getComponent('field_test_custom_options')['type']);
$this->assertEquals('altered dummy test string', $view_display->getComponent('field_test_custom_options')['settings']['test_formatter_setting_multiple']);
}
/**
* Tests the access to non-existent field URLs.
*/
public function testNonExistentFieldUrls(): void {
$field_id = 'node.foo.bar';
$this->drupalGet('admin/structure/types/manage/' . $this->contentType . '/fields/' . $field_id);
$this->assertSession()->statusCodeEquals(404);
}
/**
* Tests that the 'field_prefix' setting works on Field UI.
*/
public function testFieldPrefix(): void {
// Change default field prefix.
$field_prefix = $this->randomMachineName(10);
$this->config('field_ui.settings')->set('field_prefix', $field_prefix)->save();
// Create a field input and label exceeding the new maxlength, which is 22.
$field_exceed_max_length_label = $this->randomString(23);
$field_exceed_max_length_input = $this->randomMachineName(23);
// Try to create the field.
$edit1 = [
'new_storage_type' => 'test_field',
];
$edit2 = [
'label' => $field_exceed_max_length_label,
'field_name' => $field_exceed_max_length_input,
];
$this->drupalGet('admin/structure/types/manage/' . $this->contentType . '/fields/add-field');
$this->submitForm($edit1, 'Continue');
$this->submitForm($edit2, 'Continue');
$this->assertSession()->pageTextContains('Machine-readable name cannot be longer than 22 characters but is currently 23 characters long.');
// Create a valid field.
$this->fieldUIAddNewField('admin/structure/types/manage/' . $this->contentType, $this->fieldNameInput, $this->fieldLabel);
$this->drupalGet('admin/structure/types/manage/' . $this->contentType . '/fields/node.' . $this->contentType . '.' . $field_prefix . $this->fieldNameInput);
$this->assertSession()->pageTextContains($this->fieldLabel . ' settings for ' . $this->contentType);
}
/**
* Test translation defaults.
*/
public function testTranslationDefaults(): void {
$this->fieldUIAddNewField('admin/structure/types/manage/' . $this->contentType, $this->fieldNameInput, $this->fieldLabel);
$field_storage = FieldStorageConfig::loadByName('node', 'field_' . $this->fieldNameInput);
$this->assertTrue($field_storage->isTranslatable(), 'Field storage translatable.');
$field = FieldConfig::loadByName('node', $this->contentType, 'field_' . $this->fieldNameInput);
$this->assertFalse($field->isTranslatable(), 'Field instance should not be translatable by default.');
// Add a new field based on an existing field.
$this->drupalCreateContentType(['type' => 'additional', 'name' => 'Additional type']);
$this->fieldUIAddExistingField("admin/structure/types/manage/additional", $this->fieldName, 'Additional type');
$field_storage = FieldStorageConfig::loadByName('node', 'field_' . $this->fieldNameInput);
$this->assertTrue($field_storage->isTranslatable(), 'Field storage translatable.');
$field = FieldConfig::loadByName('node', 'additional', 'field_' . $this->fieldNameInput);
$this->assertFalse($field->isTranslatable(), 'Field instance should not be translatable by default.');
}
}

View File

@@ -0,0 +1,146 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\field_ui\Functional;
use Drupal\Core\Language\LanguageInterface;
use Drupal\node\Entity\NodeType;
use Drupal\taxonomy\Entity\Vocabulary;
use Drupal\Tests\BrowserTestBase;
use Drupal\Tests\field\Traits\EntityReferenceFieldCreationTrait;
use Drupal\Tests\field_ui\Traits\FieldUiTestTrait;
/**
* Tests the Field UI "Manage fields" screen.
*/
class ManageFieldsFunctionalTestBase extends BrowserTestBase {
use FieldUiTestTrait;
use EntityReferenceFieldCreationTrait;
/**
* Modules to install.
*
* @var array
*/
protected static $modules = [
'node',
'field_ui',
'field_test',
'taxonomy',
'image',
'block',
'node_access_test',
];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* The ID of the custom content type created for testing.
*
* @var string
*/
protected $contentType;
/**
* The label for a random field to be created for testing.
*
* @var string
*/
protected $fieldLabel;
/**
* The input name of a random field to be created for testing.
*
* @var string
*/
protected $fieldNameInput;
/**
* The name of a random field to be created for testing.
*
* @var string
*/
protected $fieldName;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->drupalPlaceBlock('system_breadcrumb_block');
$this->drupalPlaceBlock('local_actions_block');
$this->drupalPlaceBlock('local_tasks_block');
$this->drupalPlaceBlock('page_title_block');
// Create a test user.
$admin_user = $this->drupalCreateUser([
'access content',
'administer content types',
'bypass node access',
'administer node fields',
'administer node form display',
'administer node display',
'administer taxonomy',
'administer taxonomy_term fields',
'administer taxonomy_term display',
'administer users',
'administer account settings',
'administer user display',
]);
$this->drupalLogin($admin_user);
// Create content type, with underscores.
$type_name = $this->randomMachineName(8) . '_test';
$type = $this->drupalCreateContentType(['name' => $type_name, 'type' => $type_name]);
$this->contentType = $type->id();
// Create random field name with markup to test escaping.
$this->fieldLabel = '<em>' . $this->randomMachineName(8) . '</em>';
$this->fieldNameInput = $this->randomMachineName(8);
$this->fieldName = 'field_' . $this->fieldNameInput;
// Create Basic page and Article node types.
$this->drupalCreateContentType(['type' => 'page', 'name' => 'Basic page']);
$this->drupalCreateContentType(['type' => 'article', 'name' => 'Article']);
// Create a vocabulary named "Tags".
$vocabulary = Vocabulary::create([
'name' => 'Tags',
'vid' => 'tags',
'langcode' => LanguageInterface::LANGCODE_NOT_SPECIFIED,
]);
$vocabulary->save();
// Create a vocabulary named "Kittens".
Vocabulary::create([
'name' => 'Kittens',
'vid' => 'kittens',
'langcode' => LanguageInterface::LANGCODE_NOT_SPECIFIED,
])->save();
$handler_settings = [
'target_bundles' => [
$vocabulary->id() => $vocabulary->id(),
],
];
$this->createEntityReferenceField('node', 'article', 'field_' . $vocabulary->id(), 'Tags', 'taxonomy_term', 'default', $handler_settings);
\Drupal::service('entity_display.repository')
->getFormDisplay('node', 'article')
->setComponent('field_' . $vocabulary->id())
->save();
// Setup node access testing.
node_access_rebuild();
node_access_test_add_field(NodeType::load('article'));
\Drupal::state()->set('node_access_test.private', TRUE);
}
}

View File

@@ -0,0 +1,333 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\field_ui\Functional;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
use Drupal\field\Entity\FieldConfig;
use Drupal\field\Entity\FieldStorageConfig;
/**
* Tests the Field UI "Manage fields" screen.
*
* @group field_ui
* @group #slow
*/
class ManageFieldsLifecycleTest extends ManageFieldsFunctionalTestBase {
/**
* Runs the field CRUD tests.
*
* In order to act on the same fields, and not create the fields over and over
* again the following tests create, update and delete the same fields.
*/
public function testCRUDFields(): void {
$this->manageFieldsPage();
$this->createField();
$this->updateField();
$this->addExistingField();
$this->cardinalitySettings();
$this->fieldListAdminPage();
$this->deleteField();
$this->addPersistentFieldStorage();
}
/**
* Tests the manage fields page.
*
* @param string $type
* (optional) The name of a content type.
*/
protected function manageFieldsPage($type = '') {
$type = empty($type) ? $this->contentType : $type;
$this->drupalGet('admin/structure/types/manage/' . $type . '/fields');
// Check all table columns.
$table_headers = ['Label', 'Machine name', 'Field type', 'Operations'];
foreach ($table_headers as $table_header) {
// We check that the label appear in the table headings.
$this->assertSession()->responseContains($table_header . '</th>');
}
// Test the "Create a new field" action link.
$this->assertSession()->linkExists('Create a new field');
// Assert entity operations for all fields.
$number_of_links = 2;
$number_of_links_found = 0;
$operation_links = $this->xpath('//ul[@class = "dropbutton"]/li/a');
$url = base_path() . "admin/structure/types/manage/$type/fields/node.$type.body";
foreach ($operation_links as $link) {
switch ($link->getAttribute('title')) {
case 'Edit field settings.':
$this->assertSame($url, $link->getAttribute('href'));
$number_of_links_found++;
break;
case 'Delete field.':
$this->assertSame("$url/delete", $link->getAttribute('href'));
$number_of_links_found++;
break;
}
}
$this->assertEquals($number_of_links, $number_of_links_found);
}
/**
* Tests adding a new field.
*
* @todo Assert properties can be set in the form and read back in
* $field_storage and $fields.
*/
protected function createField() {
// Create a test field.
$this->fieldUIAddNewField('admin/structure/types/manage/' . $this->contentType, $this->fieldNameInput, $this->fieldLabel);
}
/**
* Tests editing an existing field.
*/
protected function updateField() {
$field_id = 'node.' . $this->contentType . '.' . $this->fieldName;
// Go to the field edit page.
$this->drupalGet('admin/structure/types/manage/' . $this->contentType . '/fields/' . $field_id);
$this->assertSession()->assertEscaped($this->fieldLabel);
// Populate the field settings with new settings.
$string = 'updated dummy test string';
$edit = [
'settings[test_field_setting]' => $string,
'field_storage[subform][settings][test_field_storage_setting]' => $string,
];
$this->assertSession()->pageTextContains('Default value');
$this->submitForm($edit, 'Save settings');
// Assert the field settings are correct.
$this->assertFieldSettings($this->contentType, $this->fieldName, $string);
// Assert redirection back to the "manage fields" page.
$this->assertSession()->addressEquals('admin/structure/types/manage/' . $this->contentType . '/fields');
}
/**
* Tests adding an existing field in another content type.
*/
protected function addExistingField() {
// Check "Re-use existing field" appears.
$this->drupalGet('admin/structure/types/manage/page/fields');
$this->assertSession()->pageTextContains('Re-use an existing field');
$this->clickLink('Re-use an existing field');
// Check that fields of other entity types (here, the 'comment_body' field)
// do not show up in the "Re-use existing field" list.
$this->assertSession()->elementNotExists('css', '.js-reuse-table [data-field-id="comment_body"]');
// Validate the FALSE assertion above by also testing a valid one.
$this->assertSession()->elementExists('css', ".js-reuse-table [data-field-id='{$this->fieldName}']");
$new_label = $this->fieldLabel . '_2';
// Add a new field based on an existing field.
$this->fieldUIAddExistingField("admin/structure/types/manage/page", $this->fieldName, $new_label);
}
/**
* Tests the cardinality settings of a field.
*
* We do not test if the number can be submitted with anything else than a
* numeric value. That is tested already in FormTest::testNumber().
*/
protected function cardinalitySettings() {
$field_edit_path = 'admin/structure/types/manage/article/fields/node.article.body';
// Assert the cardinality other field cannot be empty when cardinality is
// set to 'number'.
$edit = [
'field_storage[subform][cardinality]' => 'number',
'field_storage[subform][cardinality_number]' => '',
];
$this->drupalGet($field_edit_path);
$this->submitForm($edit, 'Update settings');
$this->assertSession()->pageTextContains('Number of values is required.');
// Submit a custom number.
$edit = [
'field_storage[subform][cardinality]' => 'number',
'field_storage[subform][cardinality_number]' => 6,
];
$this->submitForm($edit, 'Update settings');
$this->submitForm([], 'Save settings');
$this->drupalGet($field_edit_path);
$this->assertSession()->fieldValueEquals('field_storage[subform][cardinality]', 'number');
$this->assertSession()->fieldValueEquals('field_storage[subform][cardinality_number]', 6);
// Add two entries in the body.
$edit = ['title[0][value]' => 'Cardinality', 'body[0][value]' => 'Body 1', 'body[1][value]' => 'Body 2'];
$this->drupalGet('node/add/article');
$this->submitForm($edit, 'Save');
// Assert that you can't set the cardinality to a lower number than the
// highest delta of this field.
$edit = [
'field_storage[subform][cardinality]' => 'number',
'field_storage[subform][cardinality_number]' => 1,
];
$this->drupalGet($field_edit_path);
$this->submitForm($edit, 'Update settings');
$this->assertSession()->pageTextContains("There is 1 entity with 2 or more values in this field");
// Create a second entity with three values.
$edit = ['title[0][value]' => 'Cardinality 3', 'body[0][value]' => 'Body 1', 'body[1][value]' => 'Body 2', 'body[2][value]' => 'Body 3'];
$this->drupalGet('node/add/article');
$this->submitForm($edit, 'Save');
// Set to unlimited.
$edit = [
'field_storage[subform][cardinality]' => FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED,
];
$this->drupalGet($field_edit_path);
$this->submitForm($edit, 'Update settings');
$this->submitForm([], 'Save settings');
$this->drupalGet($field_edit_path);
$this->assertSession()->fieldValueEquals('field_storage[subform][cardinality]', FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED);
$this->assertSession()->fieldValueEquals('field_storage[subform][cardinality_number]', 1);
// Assert that you can't set the cardinality to a lower number then the
// highest delta of this field but can set it to the same.
$edit = [
'field_storage[subform][cardinality]' => 'number',
'field_storage[subform][cardinality_number]' => 1,
];
$this->drupalGet($field_edit_path);
$this->submitForm($edit, 'Update settings');
$this->submitForm([], 'Save settings');
$this->assertSession()->pageTextContains("There are 2 entities with 2 or more values in this field");
$edit = [
'field_storage[subform][cardinality]' => 'number',
'field_storage[subform][cardinality_number]' => 2,
];
$this->drupalGet($field_edit_path);
$this->submitForm($edit, 'Update settings');
$this->assertSession()->pageTextContains("There is 1 entity with 3 or more values in this field");
$edit = [
'field_storage[subform][cardinality]' => 'number',
'field_storage[subform][cardinality_number]' => 3,
];
$this->drupalGet($field_edit_path);
$this->submitForm($edit, 'Update settings');
// Test the cardinality validation is not access sensitive.
// Remove the cardinality limit 4 so we can add a node the user doesn't have access to.
$edit = [
'field_storage[subform][cardinality]' => (string) FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED,
];
$this->drupalGet($field_edit_path);
$this->submitForm($edit, 'Update settings');
$node = $this->drupalCreateNode([
'private' => TRUE,
'uid' => 0,
'type' => 'article',
]);
$node->body->appendItem('body 1');
$node->body->appendItem('body 2');
$node->body->appendItem('body 3');
$node->body->appendItem('body 4');
$node->save();
// Assert that you can't set the cardinality to a lower number then the
// highest delta of this field (including inaccessible entities) but can
// set it to the same.
$this->drupalGet($field_edit_path);
$edit = [
'field_storage[subform][cardinality]' => 'number',
'field_storage[subform][cardinality_number]' => 2,
];
$this->drupalGet($field_edit_path);
$this->submitForm($edit, 'Update settings');
$this->assertSession()->pageTextContains("There are 2 entities with 3 or more values in this field");
$edit = [
'field_storage[subform][cardinality]' => 'number',
'field_storage[subform][cardinality_number]' => 3,
];
$this->drupalGet($field_edit_path);
$this->submitForm($edit, 'Update settings');
$this->assertSession()->pageTextContains("There is 1 entity with 4 or more values in this field");
$edit = [
'field_storage[subform][cardinality]' => 'number',
'field_storage[subform][cardinality_number]' => 4,
];
$this->drupalGet($field_edit_path);
$this->submitForm($edit, 'Update settings');
$this->submitForm([], 'Save settings');
}
/**
* Tests deleting a field from the field edit form.
*/
protected function deleteField() {
// Delete the field.
$field_id = 'node.' . $this->contentType . '.' . $this->fieldName;
$this->drupalGet('admin/structure/types/manage/' . $this->contentType . '/fields/' . $field_id);
$this->clickLink('Delete');
$this->assertSession()->statusCodeEquals(200);
}
/**
* Tests that persistent field storage appears in the field UI.
*/
protected function addPersistentFieldStorage() {
$field_storage = FieldStorageConfig::loadByName('node', $this->fieldName);
// Persist the field storage even if there are no fields.
$field_storage->set('persist_with_no_fields', TRUE)->save();
// Delete all instances of the field.
foreach ($field_storage->getBundles() as $node_type) {
// Delete all the body field instances.
$this->drupalGet('admin/structure/types/manage/' . $node_type . '/fields/node.' . $node_type . '.' . $this->fieldName);
$this->clickLink('Delete');
$this->submitForm([], 'Delete');
}
// Check "Re-use existing field" appears.
$this->drupalGet('admin/structure/types/manage/page/fields');
$this->assertSession()->pageTextContains('Re-use an existing field');
// Ensure that we test with a label that contains HTML.
$label = $this->randomString(4) . '<br/>' . $this->randomString(4);
// Add a new field for the orphaned storage.
$this->fieldUIAddExistingField("admin/structure/types/manage/page", $this->fieldName, $label);
}
/**
* Asserts field settings are as expected.
*
* @param string $bundle
* The bundle name for the field.
* @param string $field_name
* The field name for the field.
* @param string $string
* The settings text.
* @param string $entity_type
* The entity type for the field.
*
* @internal
*/
protected function assertFieldSettings(string $bundle, string $field_name, string $string = 'dummy test string', string $entity_type = 'node'): void {
// Assert field storage settings.
$field_storage = FieldStorageConfig::loadByName($entity_type, $field_name);
$this->assertSame($string, $field_storage->getSetting('test_field_storage_setting'), 'Field storage settings were found.');
// Assert field settings.
$field = FieldConfig::loadByName($entity_type, $bundle, $field_name);
$this->assertSame($string, $field->getSetting('test_field_setting'), 'Field settings were found.');
}
/**
* Tests that the field list administration page operates correctly.
*/
protected function fieldListAdminPage() {
$this->drupalGet('admin/reports/fields');
$this->assertSession()->pageTextContains($this->fieldName);
$this->assertSession()->linkByHrefExists('admin/structure/types/manage/' . $this->contentType . '/fields');
}
}

View File

@@ -0,0 +1,170 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\field_ui\Functional;
use Drupal\Core\Entity\Entity\EntityFormDisplay;
use Drupal\Core\Entity\Entity\EntityFormMode;
use Drupal\Core\Entity\Entity\EntityViewDisplay;
use Drupal\Core\Entity\Entity\EntityViewMode;
use Drupal\field\Entity\FieldConfig;
/**
* Tests the Field UI "Manage fields" screen.
*
* @group field_ui
* @group #slow
*/
class ManageFieldsMultipleTypesTest extends ManageFieldsFunctionalTestBase {
/**
* Tests that options are copied over when reusing a field.
*
* @dataProvider entityTypesProvider
*/
public function testReuseField($entity_type, $bundle1, $bundle2): void {
$field_name = 'test_reuse';
$label = $this->randomMachineName();
// Create field with pre-configured options.
$this->drupalGet($bundle1['path'] . "/fields/add-field");
$this->fieldUIAddNewField(NULL, $field_name, $label, 'field_ui:test_field_with_preconfigured_options:custom_options');
$new_label = $this->randomMachineName();
$this->fieldUIAddExistingField($bundle2['path'], "field_{$field_name}", $new_label);
$field = FieldConfig::loadByName($entity_type, $bundle2['id'], "field_{$field_name}");
$this->assertTrue($field->isRequired());
$this->assertEquals($new_label, $field->label());
$this->assertEquals('preconfigured_field_setting', $field->getSetting('test_field_setting'));
/** @var \Drupal\Core\Entity\EntityDisplayRepositoryInterface $display_repository */
$display_repository = \Drupal::service('entity_display.repository');
$form_display = $display_repository->getFormDisplay($entity_type, $bundle2['id']);
$this->assertEquals('test_field_widget_multiple', $form_display->getComponent("field_{$field_name}")['type']);
$view_display = $display_repository->getViewDisplay($entity_type, $bundle2['id']);
$this->assertEquals('field_test_multiple', $view_display->getComponent("field_{$field_name}")['type']);
$this->assertEquals('altered dummy test string', $view_display->getComponent("field_{$field_name}")['settings']['test_formatter_setting_multiple']);
}
/**
* Tests that options are copied over when reusing a field.
*
* @dataProvider entityTypesProvider
*/
public function testReuseFieldMultipleDisplay($entity_type, $bundle1, $bundle2): void {
// Create additional form mode and enable it on both bundles.
EntityFormMode::create([
'id' => "{$entity_type}.little",
'label' => 'Little Form',
'targetEntityType' => $entity_type,
])->save();
$form_display = EntityFormDisplay::create([
'id' => "{$entity_type}.{$bundle1['id']}.little",
'targetEntityType' => $entity_type,
'status' => TRUE,
'bundle' => $bundle1['id'],
'mode' => 'little',
]);
$form_display->save();
EntityFormDisplay::create([
'id' => "{$entity_type}.{$bundle2['id']}.little",
'targetEntityType' => $entity_type,
'status' => TRUE,
'bundle' => $bundle2['id'],
'mode' => 'little',
])->save();
// Create additional view mode and enable it on both bundles.
EntityViewMode::create([
'id' => "{$entity_type}.little",
'targetEntityType' => $entity_type,
'status' => TRUE,
'enabled' => TRUE,
'label' => 'Little View Mode',
])->save();
$view_display = EntityViewDisplay::create([
'id' => "{$entity_type}.{$bundle1['id']}.little",
'targetEntityType' => $entity_type,
'status' => TRUE,
'bundle' => $bundle1['id'],
'mode' => 'little',
]);
$view_display->save();
EntityViewDisplay::create([
'id' => "{$entity_type}.{$bundle2['id']}.little",
'targetEntityType' => $entity_type,
'status' => TRUE,
'bundle' => $bundle2['id'],
'mode' => 'little',
])->save();
$field_name = 'test_reuse';
$label = $this->randomMachineName();
// Create field with pre-configured options.
$this->drupalGet($bundle1['path'] . "/fields/add-field");
$this->fieldUIAddNewField(NULL, $field_name, $label, 'field_ui:test_field_with_preconfigured_options:custom_options');
$view_display->setComponent("field_{$field_name}", [
'type' => 'field_test_default',
'region' => 'content',
])->save();
$form_display->setComponent("field_{$field_name}", [
'type' => 'test_field_widget',
'region' => 'content',
])->save();
$new_label = $this->randomMachineName();
$this->fieldUIAddExistingField($bundle2['path'], "field_{$field_name}", $new_label);
$field = FieldConfig::loadByName($entity_type, $bundle2['id'], "field_{$field_name}");
$this->assertTrue($field->isRequired());
$this->assertEquals($new_label, $field->label());
$this->assertEquals('preconfigured_field_setting', $field->getSetting('test_field_setting'));
/** @var \Drupal\Core\Entity\EntityDisplayRepositoryInterface $display_repository */
$display_repository = \Drupal::service('entity_display.repository');
// Ensure that the additional form display has correct settings.
$form_display = $display_repository->getFormDisplay($entity_type, $bundle2['id'], $form_display->getMode());
$this->assertEquals('test_field_widget', $form_display->getComponent("field_{$field_name}")['type']);
// Ensure that the additional view display has correct settings.
$view_display = $display_repository->getViewDisplay($entity_type, $bundle2['id'], $view_display->getMode());
$this->assertEquals('field_test_default', $view_display->getComponent("field_{$field_name}")['type']);
}
/**
* Data provider for testing Field UI with multiple entity types.
*
* @return array
* Test cases.
*/
public static function entityTypesProvider() {
return [
'node' => [
'entity_type' => 'node',
'bundle1' => [
'id' => 'article',
'path' => 'admin/structure/types/manage/article',
],
'bundle2' => [
'id' => 'page',
'path' => 'admin/structure/types/manage/page',
],
],
'taxonomy' => [
'entity_type' => 'taxonomy_term',
'bundle1' => [
'id' => 'tags',
'path' => 'admin/structure/taxonomy/manage/tags/overview',
],
'bundle2' => [
'id' => 'kittens',
'path' => 'admin/structure/taxonomy/manage/kittens/overview',
],
],
];
}
}

View File

@@ -0,0 +1,400 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\field_ui\Functional;
use Drupal\field\Entity\FieldConfig;
use Drupal\field\Entity\FieldStorageConfig;
use Drupal\Tests\BrowserTestBase;
use Drupal\Tests\field_ui\Traits\FieldUiTestTrait;
use Drupal\user\Entity\User;
// cSpell:ignore downlander
/**
* Tests the Manage Display page of a fieldable entity type.
*
* @group field_ui
* @group #slow
*/
class ManageFieldsTest extends BrowserTestBase {
use FieldUiTestTrait;
/**
* {@inheritdoc}
*/
protected static $modules = [
'field_test',
'field_ui',
'field_ui_test',
'node',
'text',
];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* A user with permission to administer node fields, etc.
*
* @var \Drupal\user\UserInterface
*/
protected $adminUser;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->adminUser = $this->drupalCreateUser(['administer node fields']);
$this->drupalLogin($this->adminUser);
$this->config('system.logging')
->set('error_level', ERROR_REPORTING_DISPLAY_ALL)
->save();
}
/**
* Tests drop button operations on the manage fields page.
*/
public function testFieldDropButtonOperations(): void {
$assert_session = $this->assertSession();
$node_type = $this->drupalCreateContentType();
$bundle = $node_type->id();
/** @var \Drupal\field\FieldStorageConfigInterface $storage */
$storage = $this->container->get('entity_type.manager')
->getStorage('field_storage_config')
->create([
'type' => 'string',
'field_name' => 'highlander',
'entity_type' => 'node',
]);
$storage->save();
$this->container->get('entity_type.manager')
->getStorage('field_config')
->create([
'field_storage' => $storage,
'bundle' => $bundle,
])
->save();
$this->drupalGet("/admin/structure/types/manage/{$bundle}/fields");
// Check that the summary element for the string field type exists and has
// the correct text (which comes from the FieldItemBase class).
$element = $assert_session->elementExists('css', '#highlander');
$summary = $assert_session->elementExists('css', '.field-settings-summary-cell > ul > li', $element);
$field_label = $this->container->get('plugin.manager.field.field_type')->getDefinitions()['string']['label'];
$this->assertEquals($field_label, $summary->getText());
// Add an entity reference field, and check that its summary is custom.
/** @var \Drupal\field\FieldStorageConfigInterface $storage */
$storage = $this->container->get('entity_type.manager')
->getStorage('field_storage_config')
->create([
'type' => 'entity_reference',
'field_name' => 'downlander',
'entity_type' => 'node',
'settings' => [
'target_type' => 'node',
],
]);
$storage->save();
$this->container->get('entity_type.manager')
->getStorage('field_config')
->create([
'field_storage' => $storage,
'bundle' => $bundle,
'entity_type' => 'node',
'settings' => [
'handler_settings' => [
'target_bundles' => [$bundle => $bundle],
],
],
])
->save();
$this->drupalGet("/admin/structure/types/manage/{$bundle}/fields");
$element = $assert_session->elementExists('css', '#downlander');
$custom_summary_text = 'Reference type: Content';
$allowed_bundles_text = "Content type: $bundle";
$this->assertStringContainsString($custom_summary_text, $element->getText());
$this->assertStringContainsString($allowed_bundles_text, $element->getText());
}
/**
* Tests adding a field.
*/
public function testAddField(): void {
$page = $this->getSession()->getPage();
$type = $this->drupalCreateContentType([
'name' => 'Article',
'type' => 'article',
]);
// Make sure field descriptions appear, both 1 line and multiple lines.
$this->drupalGet('/admin/structure/types/manage/' . $type->id() . '/fields/add-field');
$edit = [
'new_storage_type' => 'field_test_descriptions',
];
$this->submitForm($edit, 'Continue');
$this->assertSession()->pageTextContains('This one-line field description is important for testing');
$this->assertSession()->pageTextContains('This multiple line description needs to use an array');
$this->assertSession()->pageTextContains('This second line contains important information');
// Create a new field without actually saving it.
$this->fieldUIAddNewField('admin/structure/types/manage/' . $type->id(), 'test_field', 'Test field', 'test_field', [], [], FALSE);
// Assert that the field was not created.
$this->assertNull(FieldStorageConfig::loadByName('node', "field_test_field"));
$this->drupalGet('/admin/structure/types/manage/' . $type->id() . '/fields/add-field');
$edit = [
'new_storage_type' => 'test_field',
];
$this->submitForm($edit, 'Continue');
$edit = [
'label' => 'Test field',
'field_name' => 'test_field',
];
$this->submitForm($edit, 'Continue');
$this->assertSession()->statusMessageNotContains('Saved');
// Change the storage form values.
$edit = ['field_storage[subform][cardinality_number]' => 5];
$this->submitForm($edit, 'Update settings');
$this->assertSession()->statusMessageNotContains('Saved');
// Assert that the form values persist.
$this->assertEquals(5, $page->findField('field_storage[subform][cardinality_number]')->getValue());
// Try creating a field with the same machine name.
$this->drupalGet('/admin/structure/types/manage/' . $type->id() . '/fields/add-field');
$edit = [
'new_storage_type' => 'test_field',
];
$this->submitForm($edit, 'Continue');
$edit = [
'label' => 'Test field',
'field_name' => 'test_field',
];
$this->submitForm($edit, 'Continue');
// Assert that the values in the field storage form are reset.
$this->assertEquals(1, $page->findField('field_storage[subform][cardinality_number]')->getValue());
// Assert that the field is created with the new settings.
$this->submitForm([], 'Update settings');
$this->assertSession()->statusMessageNotContains('Saved');
$this->submitForm([], 'Save settings');
$this->assertSession()->statusMessageContains('Saved');
$this->assertEquals(1, FieldStorageConfig::loadByName('node', 'field_test_field')->getCardinality());
}
/**
* Tests multiple users adding a field with the same name.
*/
public function testAddFieldWithMultipleUsers(): void {
$page = $this->getSession()->getPage();
// Create two users.
$user1 = $this->drupalCreateUser(['administer node fields']);
$user2 = $this->drupalCreateUser(['administer node fields']);
$node_type = $this->drupalCreateContentType();
$bundle_path = '/admin/structure/types/manage/' . $node_type->id();
// Start adding a field as user 1, stop prior to saving, but keep the URL.
$this->drupalLogin($user1);
$this->drupalGet($bundle_path . '/fields/add-field');
$edit = [
'new_storage_type' => 'test_field',
];
$this->submitForm($edit, 'Continue');
$edit = [
'label' => 'Test field',
'field_name' => 'test_field',
];
$this->submitForm($edit, 'Continue');
// Make changes to the storage form.
$edit = ['field_storage[subform][cardinality_number]' => 5];
$storage_form_url = $this->getUrl();
$this->submitForm($edit, 'Update settings');
$this->drupalLogout();
// Actually add a field as user 2.
$this->drupalLogin($user2);
$this->drupalGet($bundle_path . '/fields/add-field');
$edit = [
'new_storage_type' => 'test_field',
];
$this->submitForm($edit, 'Continue');
$edit = [
'label' => 'Test field',
'field_name' => 'test_field',
];
$this->submitForm($edit, 'Continue');
$allowed_no_of_values = $page->findField('field_storage[subform][cardinality_number]')->getValue();
// Assert that the changes made by any user do not affect other users until
// the field is saved.
$this->assertEquals(1, $allowed_no_of_values);
$this->submitForm(['field_storage[subform][cardinality_number]' => 2], 'Update settings');
$this->submitForm([], 'Save settings');
$this->assertSession()->pageTextContains("Saved Test field configuration.");
$this->drupalLogout();
// Continue adding a field as user 1, using the URL saved previously.
$this->drupalLogin($user1);
$this->drupalGet($storage_form_url);
// Assert that the user can go on with configuring a field with a machine
// that is already taken.
$this->assertSession()->pageTextNotContains('error');
$this->submitForm([], 'Save settings');
// An error is thrown only after the final 'Save'.
$this->assertSession()->statusMessageContains("An error occurred while saving the field: 'field_storage_config' entity with ID 'node.field_test_field' already exists.");
}
/**
* Tests editing field when the field exists in temp store.
*/
public function testEditFieldWithLeftOverFieldInTempStore(): void {
$user = $this->drupalCreateUser(['administer node fields']);
$node_type = $this->drupalCreateContentType();
$bundle_path = '/admin/structure/types/manage/' . $node_type->id();
// Start adding a field but stop prior to saving.
$this->drupalLogin($user);
$this->drupalGet($bundle_path . '/fields/add-field');
$edit = [
'new_storage_type' => 'test_field',
];
$this->submitForm($edit, 'Continue');
$edit = [
'label' => 'Test field',
'field_name' => 'test_field',
];
$this->submitForm($edit, 'Continue');
/** @var \Drupal\field\FieldStorageConfigInterface $storage */
$storage = $this->container->get('entity_type.manager')
->getStorage('field_storage_config')
->create([
'type' => 'test_field',
'field_name' => 'test_field',
'entity_type' => 'node',
]);
$storage->save();
$this->container->get('entity_type.manager')
->getStorage('field_config')
->create([
'field_storage' => $storage,
'bundle' => $node_type->id(),
'entity_type' => 'node',
])
->save();
$this->drupalGet("$bundle_path/fields/node.{$node_type->id()}.test_field");
$this->submitForm([], 'Save settings');
$this->assertSession()->statusMessageContains('Saved test_field configuration.', 'status');
}
/**
* Tests creating entity reference field to non-bundleable entity type.
*/
public function testEntityReferenceToNonBundleableEntity(): void {
$type = $this->drupalCreateContentType([
'name' => 'kittens',
'type' => 'kittens',
]);
$bundle_path = 'admin/structure/types/manage/' . $type->id();
$field_name = 'field_user_reference';
$field_edit = [
'set_default_value' => '1',
"default_value_input[$field_name][0][target_id]" => $this->adminUser->label() . ' (' . $this->adminUser->id() . ')',
];
$this->fieldUIAddNewField($bundle_path, 'user_reference', NULL, 'field_ui:entity_reference:user', [], $field_edit);
$field = FieldConfig::loadByName('node', 'kittens', $field_name);
$this->assertEquals([['target_id' => $this->adminUser->id()]], $field->getDefaultValue(User::create(['name' => '1337'])));
}
/**
* Tests hook_form_field_storage_config_form_edit_alter().
*
* @group legacy
*/
public function testFieldStorageFormAlter(): void {
$this->container->get('module_installer')->install(['field_ui_test_deprecated']);
$this->rebuildContainer();
$node_type = $this->drupalCreateContentType();
$bundle = $node_type->id();
$this->expectDeprecation('The deprecated alter hook hook_form_field_storage_config_edit_form_alter() is implemented in these functions: field_ui_test_deprecated_form_field_storage_config_edit_form_alter. Use hook_form_field_config_edit_form_alter() instead. See https://www.drupal.org/node/3386675.');
$this->drupalGet("/admin/structure/types/manage/$bundle/fields/node.$bundle.body");
$this->assertSession()->elementTextContains('css', '#edit-field-storage', 'Greetings from the field_storage_config_edit_form() alter.');
}
/**
* Tests hook_form_field_storage_config_form_edit_alter().
*
* @group legacy
*/
public function testFieldTypeCardinalityAlter(): void {
$node_type = $this->drupalCreateContentType();
$bundle = $node_type->id();
/** @var \Drupal\field\FieldStorageConfigInterface $storage */
$storage = $this->container->get('entity_type.manager')
->getStorage('field_storage_config')
->create([
'type' => 'test_field',
'field_name' => 'field_test_field',
'entity_type' => 'node',
]);
$storage->save();
$this->container->get('entity_type.manager')
->getStorage('field_config')
->create([
'field_storage' => $storage,
'bundle' => $bundle,
'entity_type' => 'node',
])
->save();
$this->drupalGet("/admin/structure/types/manage/$bundle/fields/node.$bundle.field_test_field");
$this->assertSession()->elementTextContains('css', '#edit-field-storage', 'Greetings from Drupal\field_test\Plugin\Field\FieldType\TestItem::storageSettingsForm');
}
/**
* Tests hook_field_info_entity_type_ui_definitions_alter().
*/
public function testFieldUiDefinitionsAlter(): void {
$user = $this->drupalCreateUser(['administer node fields']);
$node_type = $this->drupalCreateContentType();
$this->drupalLogin($user);
$this->drupalGet('/admin/structure/types/manage/' . $node_type->id() . '/fields/add-field');
$this->assertSession()->pageTextContains('Boolean (overridden by alter)');
}
/**
* Ensure field category fallback works for field types without a description.
*/
public function testFieldCategoryFallbackWithoutDescription(): void {
$user = $this->drupalCreateUser(['administer node fields']);
$node_type = $this->drupalCreateContentType();
$this->drupalLogin($user);
$this->drupalGet('/admin/structure/types/manage/' . $node_type->id() . '/fields/add-field');
$field_type = $this->assertSession()->elementExists('xpath', '//label[text()="Test field"]');
$description_container = $field_type->getParent()->find('css', '.field-option__description');
$this->assertNotNull($description_container);
$this->assertEquals('', $description_container->getText());
}
}

View File

@@ -0,0 +1,86 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\field_ui\FunctionalJavascript;
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
use Drupal\Tests\field_ui\Traits\FieldUiJSTestTrait;
use Drupal\Tests\taxonomy\Traits\TaxonomyTestTrait;
/**
* Tests the default value widget in Field UI.
*
* @group field_ui
*/
class DefaultValueWidgetTest extends WebDriverTestBase {
use TaxonomyTestTrait;
use FieldUiJSTestTrait;
/**
* {@inheritdoc}
*/
protected static $modules = [
'node',
'field_ui',
'taxonomy',
];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
// Create a Content type and two test nodes.
$this->createContentType(['type' => 'test_content']);
$user = $this->drupalCreateUser([
'access content',
'administer content types',
'administer node fields',
]);
$this->drupalLogin($user);
}
/**
* Tests default value options on field config change.
*/
public function testDefaultValueOptionsForChangingBundles(): void {
$vocab_1 = $this->createVocabulary(['name' => 'Colors']);
$this->createTerm($vocab_1, ['name' => 'red']);
$this->createTerm($vocab_1, ['name' => 'green']);
$vocab_2 = $this->createVocabulary(['name' => 'Tags']);
$this->createTerm($vocab_2, ['name' => 'random tag 1']);
$this->createTerm($vocab_2, ['name' => 'random tag 2']);
$field_name = 'test_field';
$this->fieldUIAddNewFieldJS('admin/structure/types/manage/test_content', $field_name, $field_name, 'entity_reference', FALSE);
$page = $this->getSession()->getPage();
$page->findField('field_storage[subform][settings][target_type]')->selectOption('taxonomy_term');
$this->assertSession()->assertWaitOnAjaxRequest();
$page->findField('settings[handler_settings][target_bundles][' . $vocab_1->id() . ']')->check();
$this->assertSession()->assertWaitOnAjaxRequest();
$page->findField('set_default_value')->check();
$default_value_field = $page->findField('default_value_input[field_' . $field_name . '][0][target_id]');
$default_value_field->setValue('r');
$this->getSession()->getDriver()->keyDown($default_value_field->getXpath(), ' ');
$this->assertSession()->waitOnAutocomplete();
// Check the autocomplete results.
$results = $page->findAll('css', '.ui-autocomplete li');
$this->assertCount(2, $results);
$this->assertSession()->elementTextNotContains('css', '.ui-autocomplete li', 'random tag 1');
$this->assertSession()->elementTextContains('css', '.ui-autocomplete li', 'green');
}
}

View File

@@ -0,0 +1,153 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\field_ui\FunctionalJavascript;
use Drupal\Core\Entity\Entity\EntityFormMode;
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
/**
* Tests the bundle selection for view & form display modes.
*
* @group field_ui
*/
class DisplayModeBundleSelectionTest extends WebDriverTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = [
'node',
'field_ui',
'block',
];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->drupalCreateContentType([
'name' => 'Article',
'type' => 'article',
]);
$this->drupalCreateContentType([
'name' => 'Page',
'type' => 'page',
]);
$this->drupalPlaceBlock('local_actions_block');
$user = $this->drupalCreateUser([
'administer display modes',
'administer node display',
'administer node form display',
]);
// Create a new form mode 'foobar' for content.
EntityFormMode::create([
'id' => 'node.foobar',
'targetEntityType' => 'node',
'label' => 'Foobar',
])->save();
$this->drupalLogin($user);
}
/**
* Tests the bundle selection.
*
* @param string $display_mode
* View or Form display mode.
* @param string $path
* Display mode path.
* @param string $custom_mode
* Custom mode to test.
*
* @dataProvider providerBundleSelection
*/
public function testBundleSelection($display_mode, $path, $custom_mode): void {
$page = $this->getSession()->getPage();
$assert_session = $this->assertSession();
// Add new display mode for content.
$this->drupalGet("/admin/structure/display-modes/$display_mode");
$this->assertNotEmpty($assert_session->waitForText("Add $display_mode mode"));
$this->clickLink("Add $display_mode mode for Content");
$this->assertNotEmpty($assert_session->waitForText("Add new Content $display_mode mode"));
$page->find('css', '[data-drupal-selector="edit-label"]')->setValue('test');
$page->find('css', '[data-drupal-selector="edit-bundles-by-entity-article"]')->check();
$page->find('css', '.ui-dialog-buttonset')->pressButton('Save');
// Verify that test display mode is selected for article content type.
$this->drupalGet("/admin/structure/types/manage/article/$path");
$page->find('css', '[data-drupal-selector="edit-modes"]')->pressButton('Custom display settings');
$checkbox = $page->find('css', '[data-drupal-selector="edit-display-modes-custom-test"]');
$this->assertTrue($checkbox->isChecked());
// Verify that test display mode is not selected for page content type.
$this->drupalGet("/admin/structure/types/manage/page/$path");
$page->find('css', '[data-drupal-selector="edit-modes"]')->pressButton('Custom display settings');
$checkbox = $page->find('css', '[data-drupal-selector="edit-display-modes-custom-test"]');
$this->assertFalse($checkbox->isChecked());
// Click Add view/form display mode button.
$this->drupalGet("/admin/structure/display-modes/$display_mode");
$this->assertNotEmpty($assert_session->waitForText("Add $display_mode mode"));
$this->clickLink("Add $display_mode mode");
$this->assertNotEmpty($assert_session->waitForText("Choose $display_mode mode entity type"));
// Add new view/form display mode for content.
$this->clickLink('Content');
$this->assertNotEmpty($assert_session->waitForText("Add new Content $display_mode mode"));
$page->find('css', '[data-drupal-selector="edit-label"]')->setValue('test2');
$page->find('css', '[data-drupal-selector="edit-bundles-by-entity-article"]')->check();
$page->find('css', '.ui-dialog-buttonset')->pressButton('Save');
// Verify that test2 display mode is selected for article content type.
$this->drupalGet("/admin/structure/types/manage/article/$path");
$page->find('css', '[data-drupal-selector="edit-modes"]')->pressButton('Custom display settings');
$checkbox = $page->find('css', '[data-drupal-selector="edit-display-modes-custom-test2"]');
$this->assertTrue($checkbox->isChecked());
// Verify that test2 display mode is not selected for page content type.
$this->drupalGet("/admin/structure/types/manage/page/$path");
$page->find('css', '[data-drupal-selector="edit-modes"]')->pressButton('Custom display settings');
$checkbox = $page->find('css', '[data-drupal-selector="edit-display-modes-custom-test2"]');
$this->assertFalse($checkbox->isChecked());
// Verify that display mode is not selected on article content type.
$this->drupalGet("/admin/structure/types/manage/article/$path");
$page->find('css', '[data-drupal-selector="edit-modes"]')->pressButton('Custom display settings');
$checkbox = $page->find('css', "[data-drupal-selector='edit-display-modes-custom-$custom_mode']");
$this->assertFalse($checkbox->isChecked());
// Edit existing display mode and enable it for article content type.
$this->drupalGet("/admin/structure/display-modes/$display_mode");
$this->assertNotEmpty($assert_session->waitForText("Add $display_mode mode"));
$page->find('xpath', '//ul[@class = "dropbutton"]/li[1]/a')->click();
$this->assertNotEmpty($assert_session->waitForText("This $display_mode mode will still be available for the rest of the Content types if not checked here, but it will not be enabled by default."));
$page->find('css', '[data-drupal-selector="edit-bundles-by-entity-article"]')->check();
$page->find('css', '.ui-dialog-buttonset')->pressButton('Save');
// Verify that display mode is selected on article content type.
$this->drupalGet("/admin/structure/types/manage/article/$path");
$page->find('css', '[data-drupal-selector="edit-modes"]')->pressButton('Custom display settings');
$checkbox = $page->find('css', "[data-drupal-selector='edit-display-modes-custom-$custom_mode']");
$this->assertTrue($checkbox->isChecked());
}
/**
* Data provider for testBundleSelection().
*/
public static function providerBundleSelection() {
return [
'view display' => ['view', 'display', 'full'],
'form display' => ['form', 'form-display', 'foobar'],
];
}
}

View File

@@ -0,0 +1,118 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\field_ui\FunctionalJavascript;
use Drupal\entity_test\Entity\EntityTest;
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
/**
* Tests the UI for entity displays.
*
* @group field_ui
*/
class EntityDisplayTest extends WebDriverTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['field_ui', 'entity_test'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$entity = EntityTest::create([
'name' => 'The name for this entity',
'field_test_text' => [
['value' => 'The field test text value'],
],
]);
$entity->save();
$this->drupalLogin($this->drupalCreateUser([
'access administration pages',
'view test entity',
'administer entity_test content',
'administer entity_test fields',
'administer entity_test display',
'administer entity_test form display',
'view the administration theme',
]));
}
/**
* Tests the use of regions for entity form displays.
*/
public function testEntityForm(): void {
$this->drupalGet('entity_test/manage/1/edit');
$this->assertSession()->fieldExists('field_test_text[0][value]');
$this->drupalGet('entity_test/structure/entity_test/form-display');
$this->assertTrue($this->assertSession()->optionExists('fields[field_test_text][region]', 'content')->isSelected());
$this->getSession()->getPage()->pressButton('Show row weights');
$this->assertSession()->waitForElementVisible('css', '[name="fields[field_test_text][region]"]');
$this->getSession()->getPage()->selectFieldOption('fields[field_test_text][region]', 'hidden');
$this->assertTrue($this->assertSession()->optionExists('fields[field_test_text][region]', 'hidden')->isSelected());
$this->submitForm([], 'Save');
$this->assertSession()->pageTextContains('Your settings have been saved.');
$this->assertTrue($this->assertSession()->optionExists('fields[field_test_text][region]', 'hidden')->isSelected());
$this->drupalGet('entity_test/manage/1/edit');
$this->assertSession()->fieldNotExists('field_test_text[0][value]');
}
/**
* Tests the use of regions for entity view displays.
*/
public function testEntityView(): void {
$this->drupalGet('entity_test/1');
$this->assertSession()->pageTextNotContains('The field test text value');
$this->drupalGet('entity_test/structure/entity_test/display');
$this->assertSession()->elementExists('css', '.region-content-message.region-empty');
$this->getSession()->getPage()->pressButton('Show row weights');
$this->assertSession()->waitForElementVisible('css', '[name="fields[field_test_text][region]"]');
$this->assertTrue($this->assertSession()->optionExists('fields[field_test_text][region]', 'hidden')->isSelected());
$this->getSession()->getPage()->selectFieldOption('fields[field_test_text][region]', 'content');
$this->assertTrue($this->assertSession()->optionExists('fields[field_test_text][region]', 'content')->isSelected());
$this->submitForm([], 'Save');
$this->assertSession()->pageTextContains('Your settings have been saved.');
$this->assertTrue($this->assertSession()->optionExists('fields[field_test_text][region]', 'content')->isSelected());
$this->drupalGet('entity_test/1');
$this->assertSession()->pageTextContains('The field test text value');
}
/**
* Tests extra fields.
*/
public function testExtraFields(): void {
entity_test_create_bundle('bundle_with_extra_fields');
$this->drupalGet('entity_test/structure/bundle_with_extra_fields/display');
$this->assertSession()->waitForElement('css', '.tabledrag-handle');
$id = $this->getSession()->getPage()->find('css', '[name="form_build_id"]')->getValue();
$extra_field_row = $this->getSession()->getPage()->find('css', '#display-extra-field');
$disabled_region_row = $this->getSession()->getPage()->find('css', '.region-hidden-title');
$extra_field_row->find('css', '.handle')->dragTo($disabled_region_row);
$this->assertSession()->assertWaitOnAjaxRequest();
$this->assertSession()
->waitForElement('css', "[name='form_build_id']:not([value='$id'])");
$this->submitForm([], 'Save');
$this->assertSession()->pageTextContains('Your settings have been saved.');
}
}

View File

@@ -0,0 +1,505 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\field_ui\FunctionalJavascript;
use Behat\Mink\Element\NodeElement;
use Drupal\Core\Entity\Entity\EntityFormDisplay;
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
use Drupal\Tests\field_ui\Traits\FieldUiJSTestTrait;
use Drupal\Tests\field_ui\Traits\FieldUiTestTrait;
/**
* Tests the Field UI "Manage display" and "Manage form display" screens.
*
* @group field_ui
*/
class ManageDisplayTest extends WebDriverTestBase {
use FieldUiTestTrait;
use FieldUiJSTestTrait;
/**
* {@inheritdoc}
*/
protected static $modules = [
'node',
'field_ui',
'field_test',
'field_third_party_test',
'block',
];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* @var string
*/
protected $type;
/**
* @var \Drupal\Core\Entity\entityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* @var \Drupal\Core\Entity\EntityStorageInterface
*/
protected $displayStorage;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->drupalPlaceBlock('system_breadcrumb_block');
// Create a test user.
$admin_user = $this->drupalCreateUser([
'access content',
'administer content types',
'administer node fields',
'administer node form display',
'administer node display',
'administer users',
'administer account settings',
'administer user display',
'bypass node access',
]);
$this->drupalLogin($admin_user);
// Create content type, with underscores.
$type_name = $this->randomMachineName(8) . '_test';
$type = $this->drupalCreateContentType(['name' => $type_name, 'type' => $type_name]);
$this->type = $type->id();
$this->entityTypeManager = $this->container->get('entity_type.manager');
}
/**
* Tests formatter settings.
*/
public function testFormatterUI(): void {
$manage_fields = 'admin/structure/types/manage/' . $this->type;
$manage_display = $manage_fields . '/display';
// Create a field, and a node with some data for the field.
$this->fieldUIAddNewFieldJS($manage_fields, 'test', 'Test field');
$display_id = 'node.' . $this->type . '.default';
$displayStorage = $this->entityTypeManager->getStorage('entity_view_display');
// Get the display options (formatter and settings) that were automatically
// assigned for the 'default' display.
/** @var \Drupal\Core\Entity\Display\EntityViewDisplayInterface $display */
$display = $displayStorage->loadUnchanged($display_id);
$display_options = $display->getComponent('field_test');
$format = $display_options['type'];
$default_settings = \Drupal::service('plugin.manager.field.formatter')->getDefaultSettings($format);
$setting_name = key($default_settings);
$setting_value = $display_options['settings'][$setting_name];
// Display the "Manage display" screen and check that the expected formatter
// is selected.
$this->drupalGet($manage_display);
$session = $this->getSession();
$assert_session = $this->assertSession();
$page = $session->getPage();
// Find commonly used elements in this test.
$button_save = $page->findButton('Save');
$field_test_format_type = $page->findField('fields[field_test][type]');
$field_test_drag_handle = $page->find('css', '#field-test .tabledrag-handle');
$field_test_settings = $page->find('css', 'input[name="field_test_settings_edit"]');
$weight_toggle = $page->find('css', '.tabledrag-toggle-weight');
// Assert the format type field is visible and contains the expected
// formatter.
$this->assertTrue($field_test_format_type->isVisible());
$this->assertEquals($format, $field_test_format_type->getValue());
$assert_session->responseContains("$setting_name: $setting_value");
// Validate the selectbox.
$this->assertFieldSelectOptions($field_test_format_type, [
'field_no_settings',
'field_empty_test',
'field_empty_setting',
'field_test_default',
'field_test_multiple',
'field_test_with_prepare_view',
'field_test_applicable',
]);
// Ensure that fields can be hidden directly by dragging the element.
$target = $page->find('css', '.region-hidden-message');
$field_test_drag_handle->dragTo($target);
$assert_session->assertExpectedAjaxRequest(1);
$button_save->click();
// Validate the changed display settings on the server.
/** @var \Drupal\Core\Entity\Display\EntityViewDisplayInterface $display */
$display = $displayStorage->loadUnchanged($display_id);
$this->assertNull($display->getComponent('field_test'));
// Switch to manual mode.
$weight_toggle->click();
$field_region = $page->findField('fields[field_test][region]');
// Change the region to content using the region field.
$this->assertEquals('hidden', $field_region->getValue());
$field_region->setValue('content');
// Confirm the region element retains focus after the AJAX update completes.
$this->assertJsCondition('document.activeElement === document.querySelector("[name=\'fields[field_test][region]\']")');
$button_save->click();
// Change the format for the test field.
$field_test_format_type->setValue('field_test_multiple');
$assert_session->assertExpectedAjaxRequest(1);
// Confirm the format element retains focus after the AJAX update completes.
$this->assertJsCondition('document.activeElement === document.querySelector("[name=\'fields[field_test][type]\']")');
$plugin_summary = $page->find('css', '#field-test .field-plugin-summary');
$this->assertStringContainsString("test_formatter_setting_multiple: dummy test string", $plugin_summary->getText(), 'The expected summary is displayed.');
// Submit the form and assert that
// hook_field_formatter_settings_summary_alter() is called.
$button_save->click();
$assert_session->responseContains('field_test_field_formatter_settings_summary_alter');
// Open the settings form for the test field.
$field_test_settings->click();
$assert_session->assertExpectedAjaxRequest(1);
// Assert that the field added in
// field_test_field_formatter_third_party_settings_form() is present.
$field_third_party = $page->findField('fields[field_test][settings_edit_form][third_party_settings][field_third_party_test][field_test_field_formatter_third_party_settings_form]');
$this->assertNotEmpty($field_third_party, 'The field added in hook_field_formatter_third_party_settings_form() is present on the settings form.');
// Change the value and submit the form to save the third party settings.
$field_third_party->setValue('foo');
$page->findButton('Update')->click();
$assert_session->assertExpectedAjaxRequest(2);
$button_save->click();
// Assert the third party settings.
\Drupal::service('entity_field.manager')->clearCachedFieldDefinitions();
$this->drupalGet($manage_display);
$id = 'node.' . $this->type . '.default';
/** @var \Drupal\Core\Entity\Display\EntityViewDisplayInterface $display */
$display = $displayStorage->loadUnchanged($id);
$this->assertEquals('foo', $display->getRenderer('field_test')->getThirdPartySetting('field_third_party_test', 'field_test_field_formatter_third_party_settings_form'));
$this->assertContains('field_third_party_test', $display->calculateDependencies()->getDependencies()['module'], 'The display has a dependency on field_third_party_test module.');
// Change the formatter to an empty setting and validate it's initialized
// correctly.
$field_test_format_type = $page->findField('fields[field_test][type]');
$field_test_format_type->setValue('field_empty_setting');
$assert_session->assertExpectedAjaxRequest(1);
$assert_session->responseNotContains('Default empty setting now has a value.');
$this->assertTrue($field_test_settings->isVisible());
// Set the empty_setting option to a non-empty value again and validate
// the formatting summary now display's this correctly.
$field_test_settings->click();
$assert_session->assertExpectedAjaxRequest(2);
$field_empty_setting = $page->findField('fields[field_test][settings_edit_form][settings][field_empty_setting]');
$field_empty_setting->setValue('non empty setting');
$page->findButton('Update')->click();
$assert_session->assertExpectedAjaxRequest(3);
$assert_session->responseContains('Default empty setting now has a value.');
// Test the settings form behavior. An edit button should be present since
// there are third party settings to configure.
$field_test_format_type->setValue('field_no_settings');
$this->assertTrue($field_test_settings->isVisible());
// Make sure we can save the third party settings when there are no settings
// available.
$field_test_settings->click();
$assert_session->assertExpectedAjaxRequest(4);
$page->findButton('Update')->click();
// When a module providing third-party settings to a formatter (or widget)
// is uninstalled, the formatter remains enabled but the provided settings,
// together with the corresponding form elements, are removed from the
// display component.
\Drupal::service('module_installer')->uninstall(['field_third_party_test']);
// Ensure the button is still there after the module has been disabled.
$this->drupalGet($manage_display);
$this->assertTrue($field_test_settings->isVisible());
// Ensure that third-party form elements are not present anymore.
$field_test_settings->click();
$assert_session->assertExpectedAjaxRequest(1);
$field_third_party = $page->findField('fields[field_test][settings_edit_form][third_party_settings][field_third_party_test][field_test_field_formatter_third_party_settings_form]');
$this->assertEmpty($field_third_party);
// Ensure that third-party settings were removed from the formatter.
/** @var \Drupal\Core\Entity\Display\EntityViewDisplayInterface $display */
$display = $displayStorage->loadUnchanged($display_id);
$component = $display->getComponent('field_test');
$this->assertArrayNotHasKey('field_third_party_test', $component['third_party_settings']);
}
/**
* Tests widget settings.
*/
public function testWidgetUI(): void {
// Admin Manage Fields page.
$manage_fields = 'admin/structure/types/manage/' . $this->type;
// Admin Manage Display page.
$manage_display = $manage_fields . '/form-display';
$form_storage = $this->entityTypeManager->getStorage('entity_form_display');
// Creates a new field that can be used with multiple formatters.
// Reference: Drupal\field_test\Plugin\Field\FieldWidget\TestFieldWidgetMultiple::isApplicable().
$this->fieldUIAddNewFieldJS($manage_fields, 'test', 'Test field');
// Get the display options (formatter and settings) that were automatically
// assigned for the 'default' display.
/** @var \Drupal\Core\Entity\Display\EntityFormDisplayInterface $display */
$display = $form_storage->loadUnchanged("node.{$this->type}.default");
$display_options = $display->getComponent('field_test');
$widget_type = $display_options['type'];
$default_settings = \Drupal::service('plugin.manager.field.widget')->getDefaultSettings($widget_type);
$setting_name = key($default_settings);
$setting_value = $display_options['settings'][$setting_name];
// Display the "Manage form display" screen and check if the expected
// widget is selected.
$this->drupalGet($manage_display);
$session = $this->getSession();
$assert_session = $this->assertSession();
$page = $session->getPage();
$field_test_settings = $page->find('css', 'input[name="field_test_settings_edit"]');
$field_test_type = $page->findField('fields[field_test][type]');
$button_save = $page->findButton('Save');
$this->assertEquals($widget_type, $field_test_type->getValue(), 'The expected widget is selected.');
$assert_session->responseContains("$setting_name: $setting_value");
// Check whether widget weights are respected.
$this->assertFieldSelectOptions($field_test_type, [
'test_field_widget',
'test_field_widget_multilingual',
'test_field_widget_multiple',
]);
$field_test_type->setValue('test_field_widget_multiple');
$assert_session->assertExpectedAjaxRequest(1);
$button_save->click();
$this->drupalGet($manage_display);
$widget_type = 'test_field_widget_multiple';
$default_settings = \Drupal::service('plugin.manager.field.widget')->getDefaultSettings($widget_type);
$setting_name = key($default_settings);
$setting_value = $default_settings[$setting_name];
$this->assertEquals($widget_type, $field_test_type->getValue(), 'The expected widget is selected.');
$assert_session->responseContains("$setting_name: $setting_value");
$button_save->click();
/** @var \Drupal\Core\Entity\Display\EntityFormDisplayInterface $display */
$display = $form_storage->loadUnchanged("node.{$this->type}.default");
$display_options = $display->getComponent('field_test');
$current_widget = $display_options['type'];
$current_setting_value = $display_options['settings'][$setting_name];
$this->assertEquals($current_widget, $widget_type, 'The widget was updated.');
$this->assertEquals($current_setting_value, $setting_value, 'The setting was updated.');
// Assert that hook_field_widget_settings_summary_alter() is called.
$assert_session->responseContains('field_test_field_widget_settings_summary_alter');
$field_test_settings->click();
$assert_session->assertExpectedAjaxRequest(1);
// Assert that the field added in
// field_test_field_widget_third_party_settings_form() is present.
$field_third_party_test = $page->findField('fields[field_test][settings_edit_form][third_party_settings][field_third_party_test][field_test_widget_third_party_settings_form]');
$this->assertNotEmpty($field_third_party_test, 'The field added in hook_field_widget_third_party_settings_form() is present on the settings form.');
$field_third_party_test->setValue('foo');
$page->findButton('Update')->click();
$assert_session->assertWaitOnAjaxRequest();
$button_save->click();
$this->drupalGet($manage_display);
// Assert the third party settings.
\Drupal::service('entity_field.manager')->clearCachedFieldDefinitions();
/** @var \Drupal\Core\Entity\Display\EntityFormDisplayInterface $display */
$display = $form_storage->loadUnchanged('node.' . $this->type . '.default');
$this->assertEquals('foo', $display->getRenderer('field_test')->getThirdPartySetting('field_third_party_test', 'field_test_widget_third_party_settings_form'));
$this->assertContains('field_third_party_test', $display->calculateDependencies()->getDependencies()['module'], 'Form display does not have a dependency on field_third_party_test module.');
// Creates a new field that can not be used with the multiple formatter.
// Reference: Drupal\field_test\Plugin\Field\FieldWidget\TestFieldWidgetMultiple::isApplicable().
$this->fieldUIAddNewFieldJS($manage_fields, 'onewidgetfield', 'One Widget Field');
// Go to the Manage Form Display.
$this->drupalGet($manage_display);
$field_onewidgetfield_type = $page->findField('fields[field_onewidgetfield][type]');
$field_test_drag_handle = $page->find('css', '#field-test .tabledrag-handle');
$field_region = $page->findField('fields[field_test][region]');
$weight_toggle = $page->find('css', '.tabledrag-toggle-weight');
$target = $page->find('css', '.region-hidden-message');
// Checks if the select elements contain the specified options.
$this->assertFieldSelectOptions($field_test_type, [
'test_field_widget',
'test_field_widget_multilingual',
'test_field_widget_multiple',
]);
$this->assertFieldSelectOptions($field_onewidgetfield_type, [
'test_field_widget',
'test_field_widget_multilingual',
]);
$field_test_drag_handle->dragTo($target);
$assert_session->assertWaitOnAjaxRequest();
$button_save->click();
// Validate the changed display settings on the server.
/** @var \Drupal\Core\Entity\Display\EntityFormDisplayInterface $display */
$display = $form_storage->loadUnchanged("node.{$this->type}.default");
$this->assertNull($display->getComponent('field_test'));
// Switch to manual mode.
$weight_toggle->click();
// Change the region to content using the region field.
$this->assertEquals('hidden', $field_region->getValue());
$field_region->setValue('content');
$button_save->click();
// Validate the change on the server.
$this->drupalGet($manage_display);
$display = EntityFormDisplay::load("node.{$this->type}.default");
$this->assertNotNull($display->getComponent('field_test'));
}
/**
* Checks if a select element contains the specified options.
*
* @param \Behat\Mink\Element\NodeElement $field
* The select field to validate.
* @param array $expected_options
* An array of expected options.
* @param string|null $selected
* The default value to validate.
*
* @internal
*/
protected function assertFieldSelectOptions(NodeElement $field, array $expected_options, ?string $selected = NULL): void {
/** @var \Behat\Mink\Element\NodeElement[] $select_options */
$select_options = $field->findAll('xpath', 'option');
// Validate the number of options.
$this->assertSameSize($expected_options, $select_options);
// Validate the options and expected order.
foreach ($select_options as $key => $option) {
$this->assertEquals($option->getAttribute('value'), $expected_options[$key]);
}
// Validate the default value if passed.
if (!is_null($selected)) {
$this->assertEquals($selected, $field->getValue());
}
}
/**
* Confirms that notifications to save appear when necessary.
*/
public function testNotAppliedUntilSavedWarning(): void {
$assert_session = $this->assertSession();
$page = $this->getSession()->getPage();
// Admin Manage Fields page.
$manage_fields = 'admin/structure/types/manage/' . $this->type;
$this->fieldUIAddNewFieldJS($manage_fields, 'test', 'Test field');
$manage_display = 'admin/structure/types/manage/' . $this->type . '/display';
$manage_form = 'admin/structure/types/manage/' . $this->type . '/form-display';
// Form display, change widget type.
$this->drupalGet($manage_form);
$assert_session->elementNotExists('css', '.tabledrag-changed-warning');
$assert_session->elementNotExists('css', 'abbr.tabledrag-changed');
$page->selectFieldOption('fields[uid][type]', 'options_buttons');
$this->assertNotNull($changed_warning = $assert_session->waitForElementVisible('css', '.tabledrag-changed-warning'));
$this->assertNotNull($assert_session->waitForElementVisible('css', ' #uid abbr.tabledrag-changed'));
$this->assertSame('* You have unsaved changes.', $changed_warning->getText());
// Form display, change widget settings.
$this->drupalGet($manage_form);
$edit_widget_button = $assert_session->waitForElementVisible('css', '[data-drupal-selector="edit-fields-uid-settings-edit"]');
$edit_widget_button->press();
$assert_session->waitForText('3rd party formatter settings form');
// Confirm the AJAX operation of opening the form does not result in the row
// being set as changed. New settings must be submitted for that to happen.
$assert_session->elementNotExists('css', 'abbr.tabledrag-changed');
$cancel_button = $assert_session->waitForElementVisible('css', '[data-drupal-selector="edit-fields-uid-settings-edit-form-actions-cancel-settings"]');
$cancel_button->press();
$assert_session->assertNoElementAfterWait('css', '[data-drupal-selector="edit-fields-uid-settings-edit-form-actions-cancel-settings"]');
$assert_session->elementNotExists('css', '.tabledrag-changed-warning');
$assert_session->elementNotExists('css', 'abbr.tabledrag-changed');
$edit_widget_button = $assert_session->waitForElementVisible('css', '[data-drupal-selector="edit-fields-uid-settings-edit"]');
$edit_widget_button->press();
$widget_field = $assert_session->waitForField('fields[uid][settings_edit_form][third_party_settings][field_third_party_test][field_test_widget_third_party_settings_form]');
$widget_field->setValue('honk');
$update_button = $assert_session->waitForElementVisible('css', '[data-drupal-selector="edit-fields-uid-settings-edit-form-actions-save-settings"]');
$update_button->press();
$assert_session->assertNoElementAfterWait('css', '[data-drupal-selector="edit-fields-field-test-settings-edit-form-actions-cancel-settings"]');
$this->assertNotNull($changed_warning = $assert_session->waitForElementVisible('css', '.tabledrag-changed-warning'));
$this->assertNotNull($assert_session->waitForElementVisible('css', ' #uid abbr.tabledrag-changed'));
$this->assertSame('* You have unsaved changes.', $changed_warning->getText());
// Content display, change formatter type.
$this->drupalGet($manage_display);
$assert_session->elementNotExists('css', '.tabledrag-changed-warning');
$assert_session->elementNotExists('css', 'abbr.tabledrag-changed');
$page->selectFieldOption('edit-fields-field-test-label', 'inline');
$this->assertNotNull($changed_warning = $assert_session->waitForElementVisible('css', '.tabledrag-changed-warning'));
$this->assertNotNull($assert_session->waitForElementVisible('css', ' #field-test abbr.tabledrag-changed'));
$this->assertSame('* You have unsaved changes.', $changed_warning->getText());
// Content display, change formatter settings.
$this->drupalGet($manage_display);
$assert_session->elementNotExists('css', '.tabledrag-changed-warning');
$assert_session->elementNotExists('css', 'abbr.tabledrag-changed');
$edit_formatter_button = $assert_session->waitForElementVisible('css', '[data-drupal-selector="edit-fields-field-test-settings-edit"]');
$edit_formatter_button->press();
$assert_session->waitForText('3rd party formatter settings form');
$cancel_button = $assert_session->waitForElementVisible('css', '[data-drupal-selector="edit-fields-field-test-settings-edit-form-actions-cancel-settings"]');
$cancel_button->press();
$assert_session->assertNoElementAfterWait('css', '[data-drupal-selector="edit-fields-field-test-settings-edit-form-actions-cancel-settings"]');
$assert_session->elementNotExists('css', '.tabledrag-changed-warning');
$assert_session->elementNotExists('css', 'abbr.tabledrag-changed');
$edit_formatter_button = $assert_session->waitForElementVisible('css', '[data-drupal-selector="edit-fields-field-test-settings-edit"]');
$edit_formatter_button->press();
$formatter_field = $assert_session->waitForField('fields[field_test][settings_edit_form][third_party_settings][field_third_party_test][field_test_field_formatter_third_party_settings_form]');
$formatter_field->setValue('honk');
$update_button = $assert_session->waitForElementVisible('css', '[data-drupal-selector="edit-fields-field-test-settings-edit-form-actions-save-settings"]');
$update_button->press();
$assert_session->assertNoElementAfterWait('css', '[data-drupal-selector="edit-fields-field-test-settings-edit-form-actions-cancel-settings"]');
$this->assertNotNull($changed_warning = $assert_session->waitForElementVisible('css', '.tabledrag-changed-warning'));
$this->assertNotNull($assert_session->waitForElementVisible('css', ' #field-test abbr.tabledrag-changed'));
$this->assertSame('* You have unsaved changes.', $changed_warning->getText());
}
}

View File

@@ -0,0 +1,376 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\field_ui\FunctionalJavascript;
use Drupal\field\Entity\FieldConfig;
use Drupal\field\Entity\FieldStorageConfig;
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
use Drupal\Tests\field_ui\Traits\FieldUiJSTestTrait;
// cspell:ignore horserad
/**
* Tests the Field UI "Manage Fields" screens.
*
* @group field_ui
* @group #slow
*/
class ManageFieldsTest extends WebDriverTestBase {
use FieldUiJSTestTrait;
/**
* {@inheritdoc}
*/
protected static $modules = [
'node',
'field_ui',
'field_test',
'block',
'options',
];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* @var string
*/
protected $type;
/**
* @var string
*/
protected $type2;
/**
* @var \Drupal\Core\Entity\entityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->drupalPlaceBlock('system_breadcrumb_block');
$this->drupalPlaceBlock('local_tasks_block');
$this->drupalPlaceBlock('local_actions_block');
// Create a test user.
$admin_user = $this->drupalCreateUser([
'access content',
'administer content types',
'administer node fields',
]);
$this->drupalLogin($admin_user);
$type = $this->drupalCreateContentType([
'name' => 'Article',
'type' => 'article',
]);
$this->type = $type->id();
$type2 = $this->drupalCreateContentType([
'name' => 'Basic Page',
'type' => 'page',
]);
$this->type2 = $type2->id();
$this->entityTypeManager = $this->container->get('entity_type.manager');
}
/**
* Tests re-using an existing field and the visibility of the re-use button.
*/
public function testReuseExistingField(): void {
$path = 'admin/structure/types/manage/article';
$path2 = 'admin/structure/types/manage/page';
$this->drupalGet($path2 . '/fields');
// The button should not be visible without any re-usable fields.
$this->assertSession()->linkNotExists('Re-use an existing field');
$field_label = 'Test field';
// Create a field, and a node with some data for the field.
$this->fieldUIAddNewFieldJS($path, 'test', $field_label);
// Add an existing field.
$this->fieldUIAddExistingFieldJS($path2, 'field_test', $field_label);
// Confirm the button is no longer visible after re-using the field.
$this->assertSession()->linkNotExists('Re-use an existing field');
}
/**
* Tests filter results in the re-use form.
*/
public function testFilterInReuseForm(): void {
$session = $this->getSession();
$page = $session->getPage();
$path = 'admin/structure/types/manage/article';
$path2 = 'admin/structure/types/manage/page';
$this->fieldUIAddNewFieldJS($path, 'horse', 'Horse');
$this->fieldUIAddNewFieldJS($path, 'horseradish', 'Horseradish', 'text');
$this->fieldUIAddNewFieldJS($path, 'carrot', 'Carrot', 'text');
$this->drupalGet($path2 . '/fields');
$this->assertSession()->linkExists('Re-use an existing field');
$this->clickLink('Re-use an existing field');
$this->assertSession()->waitForElementVisible('css', '#drupal-modal');
$filter = $this->assertSession()->waitForElementVisible('css', 'input[name="search"]');
$horse_field_row = $page->find('css', '.js-reuse-table tr[data-field-id="field_horse"]');
$horseradish_field_row = $page->find('css', '.js-reuse-table tr[data-field-id="field_horseradish"]');
$carrot_field_row = $page->find('css', '.js-reuse-table tr[data-field-id="field_carrot"]');
// Confirm every field is visible first.
$this->assertTrue($horse_field_row->isVisible());
$this->assertTrue($horseradish_field_row->isVisible());
$this->assertTrue($carrot_field_row->isVisible());
// Filter by 'horse' field name.
$filter->setValue('horse');
$session->wait(1000, "jQuery('[data-field-id=\"field_carrot\"]:visible').length == 0");
$this->assertTrue($horse_field_row->isVisible());
$this->assertTrue($horseradish_field_row->isVisible());
$this->assertFalse($carrot_field_row->isVisible());
// Filter even more so only 'horseradish' is visible.
$filter->setValue('horserad');
$session->wait(1000, "jQuery('[data-field-id=\"field_horse\"]:visible').length == 0");
$this->assertFalse($horse_field_row->isVisible());
$this->assertTrue($horseradish_field_row->isVisible());
$this->assertFalse($carrot_field_row->isVisible());
// Filter by field type but search with 'ext' instead of 'text' to
// confirm that contains-based search works.
$filter->setValue('ext');
$session->wait(1000, "jQuery('[data-field-id=\"field_horse\"]:visible').length == 0");
$session->wait(1000, "jQuery('[data-field-id=\"field_carrot\"]:visible').length == 1");
$this->assertFalse($horse_field_row->isVisible());
$this->assertTrue($horseradish_field_row->isVisible());
$this->assertTrue($carrot_field_row->isVisible());
// Ensure clearing brings all the results back.
$filter->setValue('');
$session->wait(1000, "jQuery('[data-field-id=\"field_horse\"]:visible').length == 1");
$this->assertTrue($horse_field_row->isVisible());
$this->assertTrue($horseradish_field_row->isVisible());
$this->assertTrue($carrot_field_row->isVisible());
}
/**
* Tests that field delete operation opens in modal.
*/
public function testFieldDelete(): void {
$page = $this->getSession()->getPage();
$assert_session = $this->assertSession();
$this->drupalGet('admin/structure/types/manage/article/fields');
$page->find('css', '.dropbutton-toggle button')->click();
$page->clickLink('Delete');
// Asserts a dialog opens with the expected text.
$this->assertEquals('Are you sure you want to delete the field Body?', $assert_session->waitForElement('css', '.ui-dialog-title')->getText());
$page->find('css', '.ui-dialog-buttonset')->pressButton('Delete');
$assert_session->waitForText('The field Body has been deleted from the Article content type.');
}
/**
* Tests field add.
*/
public function testAddField(): void {
$page = $this->getSession()->getPage();
$assert_session = $this->assertSession();
$this->drupalGet('admin/structure/types/manage/article/fields/add-field');
// Test validation.
$page->pressButton('Continue');
$assert_session->pageTextContains('You need to select a field type.');
$assert_session->pageTextNotContains('Choose an option below');
$this->assertNotEmpty($number_field = $page->find('xpath', '//*[text() = "Number"]')->getParent());
$number_field->click();
$this->assertTrue($assert_session->elementExists('css', '[name="new_storage_type"][value="number"]')->isSelected());
$page->pressButton('Continue');
$assert_session->pageTextContains('Choose an option below');
$field_name = 'test_field_1';
$page->fillField('label', $field_name);
$page->pressButton('Continue');
$assert_session->pageTextContains('You need to choose an option.');
$assert_session->elementNotExists('css', '[name="new_storage_type"].error');
$assert_session->elementExists('css', '[name="group_field_options_wrapper"].error');
$page->pressButton('Back');
// Try adding a field using a grouped field type.
$this->assertNotEmpty($email_field = $page->find('xpath', '//*[text() = "Email"]')->getParent());
$email_field->click();
$this->assertTrue($assert_session->elementExists('css', '[name="new_storage_type"][value="email"]')->isSelected());
$page->pressButton('Continue');
$assert_session->pageTextNotContains('Choose an option below');
$page->pressButton('Back');
$this->assertNotEmpty($text = $page->find('xpath', '//*[text() = "Plain text"]')->getParent());
$text->click();
$this->assertTrue($assert_session->elementExists('css', '[name="new_storage_type"][value="plain_text"]')->isSelected());
$page->pressButton('Continue');
$assert_session->pageTextContains('Choose an option below');
$page->fillField('label', $field_name);
$this->assertNotEmpty($text_plain = $page->find('xpath', '//*[text() = "Text (plain)"]')->getParent());
$text_plain->click();
$this->assertTrue($assert_session->elementExists('css', '[name="group_field_options_wrapper"][value="string"]')->isSelected());
$page->pressButton('Continue');
$this->assertMatchesRegularExpression('/.*article\/add-field\/node\/field_test_field_1.*/', $this->getUrl());
// Ensure the default value is reloaded when the field storage settings
// are changed.
$default_input_1_name = 'default_value_input[field_test_field_1][0][value]';
$default_input_1 = $assert_session->fieldExists($default_input_1_name);
$this->assertFalse($default_input_1->isVisible());
$default_value = $assert_session->fieldExists('set_default_value');
$default_value->check();
$assert_session->waitForElementVisible('xpath', $default_value->getXpath());
$default_input_1->setValue('There can be only one!');
$default_input_2_name = 'default_value_input[field_test_field_1][1][value]';
$assert_session->fieldNotExists($default_input_2_name);
$cardinality = $assert_session->fieldExists('field_storage[subform][cardinality_number]');
$cardinality->setValue(2);
$default_input_2 = $assert_session->waitForField($default_input_2_name);
// Ensure the default value for first input is retained.
$assert_session->fieldValueEquals($default_input_1_name, 'There can be only one!');
$page->findField($default_input_2_name)->setValue('But maybe also two?');
$cardinality->setValue('1');
$assert_session->assertWaitOnAjaxRequest();
$assert_session->waitForElementRemoved('xpath', $default_input_2->getXpath());
// Ensure the first input retains its value.
$assert_session->fieldValueEquals($default_input_1_name, 'There can be only one!');
$cardinality->setValue(2);
$assert_session->waitForField($default_input_2_name);
// Ensure when the second input is added again it does not retain its value.
$assert_session->fieldValueEquals($default_input_2_name, '');
// Ensure changing the max length input will also reload the form.
$max_length_input = $assert_session->fieldExists('field_storage[subform][settings][max_length]');
$this->assertSame('255', $max_length_input->getValue());
$this->assertSame('255', $default_input_1->getAttribute('maxlength'));
$max_length_input->setValue('5');
$page->waitFor(5, function () use ($default_input_1) {
return $default_input_1->getAttribute('maxlength') === '5';
});
$this->assertSame('5', $default_input_1->getAttribute('maxlength'));
// Set a default value that is under the new limit.
$default_input_1->setValue('Five!');
$page->pressButton('Save settings');
$assert_session->pageTextContains('Saved ' . $field_name . ' configuration.');
$this->assertNotNull($field_storage = FieldStorageConfig::loadByName('node', "field_$field_name"));
$this->assertEquals('string', $field_storage->getType());
// Try adding a field using a non-grouped field type.
$this->drupalGet('admin/structure/types/manage/article/fields/add-field');
$this->assertNotEmpty($number_field = $page->find('xpath', '//*[text() = "Number"]')->getParent());
$number_field->click();
$this->assertTrue($assert_session->elementExists('css', '[name="new_storage_type"][value="number"]')->isSelected());
$page->pressButton('Continue');
$assert_session->pageTextContains('Choose an option below');
$this->assertNotEmpty($number_integer = $page->find('xpath', '//*[text() = "Number (integer)"]')->getParent());
$number_integer->click();
$this->assertTrue($assert_session->elementExists('css', '[name="group_field_options_wrapper"][value="integer"]')->isSelected());
$page->pressButton('Back');
$this->assertNotEmpty($test_field = $page->find('xpath', '//*[text() = "Test field"]')->getParent());
$test_field->click();
$this->assertTrue($assert_session->elementExists('css', '[name="new_storage_type"][value="test_field"]')->isSelected());
$page->pressButton('Continue');
$field_name = 'test_field_2';
$page->fillField('label', $field_name);
$assert_session->pageTextNotContains('Choose an option below');
$page->pressButton('Continue');
$this->assertMatchesRegularExpression('/.*article\/add-field\/node\/field_test_field_2.*/', $this->getUrl());
$page->pressButton('Save settings');
$assert_session->pageTextContains('Saved ' . $field_name . ' configuration.');
$this->assertNotNull($field_storage = FieldStorageConfig::loadByName('node', "field_$field_name"));
$this->assertEquals('test_field', $field_storage->getType());
}
/**
* Tests the order in which the field types appear in the form.
*/
public function testFieldTypeOrder(): void {
$this->drupalGet('admin/structure/types/manage/article/fields/add-field');
$page = $this->getSession()->getPage();
$field_type_categories = [
'selection_list',
'number',
];
foreach ($field_type_categories as $field_type_category) {
// Select the group card.
$group_field_card = $page->find('css', "[name='new_storage_type'][value='$field_type_category']")->getParent();
$group_field_card->click();
$page->pressButton('Continue');
$field_types = $page->findAll('css', '.subfield-option .option');
$field_type_labels = [];
foreach ($field_types as $field_type) {
$field_type_labels[] = $field_type->getText();
}
$expected_field_types = match ($field_type_category) {
'selection_list' => [
'List (text)',
'List (integer)',
'List (float)',
],
'number' => [
'Number (integer)',
'Number (decimal)',
'Number (float)',
],
};
// Assert that the field type options are displayed as per their weights.
$this->assertSame($expected_field_types, $field_type_labels);
// Return to the first step of the form.
$page->pressButton('Back');
}
}
/**
* Tests the form validation for allowed values field.
*/
public function testAllowedValuesFormValidation(): void {
FieldStorageConfig::create([
'field_name' => 'field_text',
'entity_type' => 'node',
'type' => 'text',
])->save();
FieldConfig::create([
'field_name' => 'field_text',
'entity_type' => 'node',
'bundle' => 'article',
])->save();
$this->drupalGet('/admin/structure/types/manage/article/fields/node.article.field_text');
$page = $this->getSession()->getPage();
$page->findField('edit-field-storage-subform-cardinality-number')->setValue('-11');
$this->assertSession()->assertExpectedAjaxRequest(1);
$page->findButton('Save settings')->click();
$this->assertSession()->pageTextContains('Limit must be higher than or equal to 1.');
}
/**
* Tests the form validation for label field.
*/
public function testLabelFieldFormValidation(): void {
$this->drupalGet('/admin/structure/types/manage/article/fields/add-field');
$page = $this->getSession()->getPage();
$page->findButton('Continue')->click();
$this->assertSession()->pageTextContains('You need to select a field type.');
$this->assertNotEmpty($boolean_field = $page->find('xpath', '//*[text() = "Boolean (overridden by alter)"]')->getParent());
$boolean_field->click();
$page->findButton('Continue')->click();
$page->findButton('Continue')->click();
$this->assertSession()->pageTextContains('Add new field: you need to provide a label.');
}
}

View File

@@ -0,0 +1,712 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\field_ui\Kernel;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Database\Database;
use Drupal\Core\Entity\Display\EntityDisplayInterface;
use Drupal\Core\Entity\Display\EntityViewDisplayInterface;
use Drupal\Core\Entity\Entity\EntityFormDisplay;
use Drupal\Core\Entity\Entity\EntityViewDisplay;
use Drupal\Core\Entity\Entity\EntityViewMode;
use Drupal\field\Entity\FieldConfig;
use Drupal\field\Entity\FieldStorageConfig;
use Drupal\node\Entity\NodeType;
use Drupal\KernelTests\KernelTestBase;
use Drupal\user\Entity\Role;
/**
* Tests the entity display configuration entities.
*
* @group field_ui
*/
class EntityDisplayTest extends KernelTestBase {
/**
* Modules to install.
*
* @var string[]
*/
protected static $modules = [
'field_ui',
'field',
'entity_test',
'user',
'text',
'field_test',
'node',
'system',
];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->installEntitySchema('entity_test');
$this->installEntitySchema('node');
$this->installEntitySchema('user');
$this->installConfig(['field', 'node', 'user']);
}
/**
* Tests basic CRUD operations on entity display objects.
*/
public function testEntityDisplayCRUD(): void {
$display = EntityViewDisplay::create([
'targetEntityType' => 'entity_test',
'bundle' => 'entity_test',
'mode' => 'default',
]);
$expected = [];
// Check that providing no 'weight' results in the highest current weight
// being assigned. The 'name' field's formatter has weight -5, therefore
// these follow.
$expected['component_1'] = ['weight' => -4, 'settings' => [], 'third_party_settings' => []];
$expected['component_2'] = ['weight' => -3, 'settings' => [], 'third_party_settings' => []];
$display->setComponent('component_1');
$display->setComponent('component_2');
$this->assertEquals($expected['component_1'], $display->getComponent('component_1'));
$this->assertEquals($expected['component_2'], $display->getComponent('component_2'));
// Check that arbitrary options are correctly stored.
$expected['component_3'] = ['weight' => 10, 'third_party_settings' => ['field_test' => ['foo' => 'bar']], 'settings' => []];
$display->setComponent('component_3', $expected['component_3']);
$this->assertEquals($expected['component_3'], $display->getComponent('component_3'));
// Check that the display can be properly saved and read back.
$display->save();
$display = EntityViewDisplay::load($display->id());
foreach (['component_1', 'component_2', 'component_3'] as $name) {
$expected[$name]['region'] = 'content';
$this->assertEquals($expected[$name], $display->getComponent($name));
}
// Ensure that third party settings were added to the config entity.
// These are added by entity_test_entity_presave() implemented in
// entity_test module.
$this->assertEquals('bar', $display->getThirdPartySetting('entity_test', 'foo'), 'Third party settings were added to the entity view display.');
// Check that getComponents() returns options for all components.
$expected['name'] = [
'label' => 'hidden',
'type' => 'string',
'weight' => -5,
'settings' => [
'link_to_entity' => FALSE,
],
'third_party_settings' => [],
'region' => 'content',
];
$this->assertEquals($expected, $display->getComponents());
// Check that a component can be removed.
$display->removeComponent('component_3');
$this->assertNULL($display->getComponent('component_3'));
// Check that the removal is correctly persisted.
$display->save();
$display = EntityViewDisplay::load($display->id());
$this->assertNULL($display->getComponent('component_3'));
// Check that createCopy() creates a new component that can be correctly
// saved.
EntityViewMode::create([
'id' => $display->getTargetEntityTypeId() . '.other_view_mode',
'label' => 'Other',
'targetEntityType' => $display->getTargetEntityTypeId(),
])->save();
$new_display = $display->createCopy('other_view_mode');
$new_display->save();
$new_display = EntityViewDisplay::load($new_display->id());
$dependencies = $new_display->calculateDependencies()->getDependencies();
$this->assertEquals(['config' => ['core.entity_view_mode.entity_test.other_view_mode'], 'module' => ['entity_test']], $dependencies);
$this->assertEquals($display->getTargetEntityTypeId(), $new_display->getTargetEntityTypeId());
$this->assertEquals($display->getTargetBundle(), $new_display->getTargetBundle());
$this->assertEquals('other_view_mode', $new_display->getMode());
$this->assertEquals($display->getComponents(), $new_display->getComponents());
}
/**
* Tests sorting of components by name on basic CRUD operations.
*/
public function testEntityDisplayCRUDSort(): void {
$display = EntityViewDisplay::create([
'targetEntityType' => 'entity_test',
'bundle' => 'entity_test',
'mode' => 'default',
]);
$display->setComponent('component_3');
$display->setComponent('component_1');
$display->setComponent('component_2');
$display->save();
$components = array_keys($display->getComponents());
// The name field is not configurable so will be added automatically.
$expected = [0 => 'component_1', 1 => 'component_2', 2 => 'component_3', 'name'];
$this->assertSame($expected, $components);
}
/**
* @covers \Drupal\Core\Entity\EntityDisplayRepository::getViewDisplay
*/
public function testEntityGetDisplay(): void {
$display_repository = $this->container->get('entity_display.repository');
// Check that getViewDisplay() returns a fresh object when no configuration
// entry exists.
$display = $display_repository->getViewDisplay('entity_test', 'entity_test');
$this->assertTrue($display->isNew());
// Add some components and save the display.
$display->setComponent('component_1', ['weight' => 10, 'settings' => []])
->save();
// Check that getViewDisplay() returns the correct object.
$display = $display_repository->getViewDisplay('entity_test', 'entity_test');
$this->assertFalse($display->isNew());
$this->assertEquals('entity_test.entity_test.default', $display->id());
$this->assertEquals(['weight' => 10, 'settings' => [], 'third_party_settings' => [], 'region' => 'content'], $display->getComponent('component_1'));
}
/**
* Tests the behavior of a field component within an entity display object.
*/
public function testExtraFieldComponent(): void {
entity_test_create_bundle('bundle_with_extra_fields');
$display = EntityViewDisplay::create([
'targetEntityType' => 'entity_test',
'bundle' => 'bundle_with_extra_fields',
'mode' => 'default',
]);
// Check that the default visibility taken into account for extra fields
// unknown in the display.
$this->assertEquals(['weight' => 5, 'region' => 'content', 'settings' => [], 'third_party_settings' => []], $display->getComponent('display_extra_field'));
$this->assertNull($display->getComponent('display_extra_field_hidden'));
// Check that setting explicit options overrides the defaults.
$display->removeComponent('display_extra_field');
$display->setComponent('display_extra_field_hidden', ['weight' => 10]);
$this->assertNull($display->getComponent('display_extra_field'));
$this->assertEquals(['weight' => 10, 'settings' => [], 'third_party_settings' => []], $display->getComponent('display_extra_field_hidden'));
}
/**
* Tests the behavior of an extra field component with initial invalid values.
*/
public function testExtraFieldComponentInitialInvalidConfig(): void {
entity_test_create_bundle('bundle_with_extra_fields');
$display = EntityViewDisplay::create([
'targetEntityType' => 'entity_test',
'bundle' => 'bundle_with_extra_fields',
'mode' => 'default',
// Add the extra field to the initial config, without a 'type'.
'content' => [
'display_extra_field' => [
'weight' => 5,
],
],
]);
// Check that the default visibility taken into account for extra fields
// unknown in the display that were included in the initial config.
$this->assertEquals(['weight' => 5, 'region' => 'content'], $display->getComponent('display_extra_field'));
$this->assertNull($display->getComponent('display_extra_field_hidden'));
// Check that setting explicit options overrides the defaults.
$display->removeComponent('display_extra_field');
$display->setComponent('display_extra_field_hidden', ['weight' => 10]);
$this->assertNull($display->getComponent('display_extra_field'));
$this->assertEquals(['weight' => 10, 'settings' => [], 'third_party_settings' => []], $display->getComponent('display_extra_field_hidden'));
}
/**
* Tests the behavior of a field component within an entity display object.
*/
public function testFieldComponent(): void {
$field_name = 'test_field';
// Create a field storage and a field.
$field_storage = FieldStorageConfig::create([
'field_name' => $field_name,
'entity_type' => 'entity_test',
'type' => 'test_field',
]);
$field_storage->save();
$field = FieldConfig::create([
'field_storage' => $field_storage,
'bundle' => 'entity_test',
]);
$field->save();
$display = EntityViewDisplay::create([
'targetEntityType' => 'entity_test',
'bundle' => 'entity_test',
'mode' => 'default',
]);
// Check that providing no options results in default values being used.
$display->setComponent($field_name);
$field_type_info = \Drupal::service('plugin.manager.field.field_type')->getDefinition($field_storage->getType());
$default_formatter = $field_type_info['default_formatter'];
$formatter_settings = \Drupal::service('plugin.manager.field.formatter')->getDefaultSettings($default_formatter);
$expected = [
'weight' => -4,
'label' => 'above',
'type' => $default_formatter,
'settings' => $formatter_settings,
'third_party_settings' => [],
];
$this->assertEquals($expected, $display->getComponent($field_name));
// Check that the getFormatter() method returns the correct formatter plugin.
$formatter = $display->getRenderer($field_name);
$this->assertEquals($default_formatter, $formatter->getPluginId());
$this->assertEquals($formatter_settings, $formatter->getSettings());
// Check that the formatter is statically persisted.
$this->assertSame($formatter, $display->getRenderer($field_name));
// Check that changing the definition creates a new formatter.
$display->setComponent($field_name, [
'type' => 'field_test_multiple',
]);
$renderer = $display->getRenderer($field_name);
$this->assertEquals('field_test_multiple', $renderer->getPluginId());
$this->assertNotSame($formatter, $renderer);
// Check that the display has dependencies on the field and the module that
// provides the formatter.
$dependencies = $display->calculateDependencies()->getDependencies();
$this->assertEquals(['config' => ['field.field.entity_test.entity_test.test_field'], 'module' => ['entity_test', 'field_test']], $dependencies);
}
/**
* Tests the behavior of a field component for a base field.
*/
public function testBaseFieldComponent(): void {
$display = EntityViewDisplay::create([
'targetEntityType' => 'entity_test_base_field_display',
'bundle' => 'entity_test_base_field_display',
'mode' => 'default',
]);
// Check that default options are correctly filled in.
$formatter_settings = \Drupal::service('plugin.manager.field.formatter')->getDefaultSettings('text_default');
$expected = [
'test_no_display' => NULL,
'test_display_configurable' => [
'label' => 'above',
'type' => 'text_default',
'settings' => $formatter_settings,
'third_party_settings' => [],
'weight' => 10,
'region' => 'content',
],
'test_display_non_configurable' => [
'label' => 'above',
'type' => 'text_default',
'settings' => $formatter_settings,
'third_party_settings' => [],
'weight' => 11,
'region' => 'content',
],
];
foreach ($expected as $field_name => $options) {
$this->assertEquals($options, $display->getComponent($field_name));
}
// Check that saving the display only writes data for fields whose display
// is configurable.
$display->save();
$config = $this->config('core.entity_view_display.' . $display->id());
$data = $config->get();
$this->assertFalse(isset($data['content']['test_no_display']));
$this->assertFalse(isset($data['hidden']['test_no_display']));
$this->assertEquals($expected['test_display_configurable'], $data['content']['test_display_configurable']);
$this->assertFalse(isset($data['content']['test_display_non_configurable']));
$this->assertFalse(isset($data['hidden']['test_display_non_configurable']));
// Check that defaults are correctly filled when loading the display.
$display = EntityViewDisplay::load($display->id());
foreach ($expected as $field_name => $options) {
$this->assertEquals($options, $display->getComponent($field_name));
}
// Check that data manually written for fields whose display is not
// configurable is discarded when loading the display.
$data['content']['test_display_non_configurable'] = $expected['test_display_non_configurable'];
$data['content']['test_display_non_configurable']['weight']++;
$config->setData($data)->save();
$display = EntityViewDisplay::load($display->id());
foreach ($expected as $field_name => $options) {
$this->assertEquals($options, $display->getComponent($field_name));
}
}
/**
* Tests deleting a bundle.
*/
public function testDeleteBundle(): void {
// Create a node bundle, display and form display object.
$type = NodeType::create([
'type' => 'article',
'name' => 'Article',
]);
$type->save();
node_add_body_field($type);
/** @var \Drupal\Core\Entity\EntityDisplayRepositoryInterface $display_repository */
$display_repository = \Drupal::service('entity_display.repository');
$display_repository->getViewDisplay('node', 'article')->save();
$display_repository->getFormDisplay('node', 'article')->save();
// Delete the bundle.
$type->delete();
$display = EntityViewDisplay::load('node.article.default');
$this->assertFalse((bool) $display);
$form_display = EntityFormDisplay::load('node.article.default');
$this->assertFalse((bool) $form_display);
}
/**
* Tests deleting field.
*/
public function testDeleteField(): void {
$field_name = 'test_field';
// Create a field storage and a field.
$field_storage = FieldStorageConfig::create([
'field_name' => $field_name,
'entity_type' => 'entity_test',
'type' => 'test_field',
]);
$field_storage->save();
$field = FieldConfig::create([
'field_storage' => $field_storage,
'bundle' => 'entity_test',
]);
$field->save();
// Create default and teaser entity display.
EntityViewMode::create([
'id' => 'entity_test.teaser',
'label' => 'Teaser',
'targetEntityType' => 'entity_test',
])->save();
EntityViewDisplay::create([
'targetEntityType' => 'entity_test',
'bundle' => 'entity_test',
'mode' => 'default',
])->setComponent($field_name)->save();
EntityViewDisplay::create([
'targetEntityType' => 'entity_test',
'bundle' => 'entity_test',
'mode' => 'teaser',
])->setComponent($field_name)->save();
/** @var \Drupal\Core\Entity\EntityDisplayRepositoryInterface $display_repository */
$display_repository = \Drupal::service('entity_display.repository');
// Check the component exists.
$display = $display_repository->getViewDisplay('entity_test', 'entity_test');
$this->assertNotEmpty($display->getComponent($field_name));
$display = $display_repository->getViewDisplay('entity_test', 'entity_test', 'teaser');
$this->assertNotEmpty($display->getComponent($field_name));
// Delete the field.
$field->delete();
// Check that the component has been removed from the entity displays.
$display = $display_repository->getViewDisplay('entity_test', 'entity_test');
$this->assertNull($display->getComponent($field_name));
$display = $display_repository->getViewDisplay('entity_test', 'entity_test', 'teaser');
$this->assertNull($display->getComponent($field_name));
}
/**
* Tests \Drupal\Core\Entity\EntityDisplayBase::onDependencyRemoval().
*/
public function testOnDependencyRemoval(): void {
$this->enableModules(['field_plugins_test']);
$field_name = 'test_field';
// Create a field.
$field_storage = FieldStorageConfig::create([
'field_name' => $field_name,
'entity_type' => 'entity_test',
'type' => 'text',
]);
$field_storage->save();
$field = FieldConfig::create([
'field_storage' => $field_storage,
'bundle' => 'entity_test',
]);
$field->save();
EntityViewDisplay::create([
'targetEntityType' => 'entity_test',
'bundle' => 'entity_test',
'mode' => 'default',
])->setComponent($field_name, ['type' => 'field_plugins_test_text_formatter'])->save();
/** @var \Drupal\Core\Entity\EntityDisplayRepositoryInterface $display_repository */
$display_repository = \Drupal::service('entity_display.repository');
// Check the component exists and is of the correct type.
$display = $display_repository->getViewDisplay('entity_test', 'entity_test');
$this->assertEquals('field_plugins_test_text_formatter', $display->getComponent($field_name)['type']);
// Removing the field_plugins_test module should change the component to use
// the default formatter for test fields.
\Drupal::service('config.manager')->uninstall('module', 'field_plugins_test');
$display = $display_repository->getViewDisplay('entity_test', 'entity_test');
$this->assertEquals('text_default', $display->getComponent($field_name)['type']);
// Removing the text module should remove the field from the view display.
\Drupal::service('config.manager')->uninstall('module', 'text');
$display = $display_repository->getViewDisplay('entity_test', 'entity_test');
$this->assertNull($display->getComponent($field_name));
}
/**
* Ensure that entity view display changes invalidates cache tags.
*/
public function testEntityDisplayInvalidateCacheTags(): void {
$cache = \Drupal::cache();
$cache->set('cid', 'kittens', Cache::PERMANENT, ['config:entity_view_display_list']);
$display = EntityViewDisplay::create([
'targetEntityType' => 'entity_test',
'bundle' => 'entity_test',
'mode' => 'default',
]);
$display->setComponent('kitten');
$display->save();
$this->assertFalse($cache->get('cid'));
}
/**
* Tests getDisplayModeOptions().
*/
public function testGetDisplayModeOptions(): void {
NodeType::create([
'type' => 'article',
'name' => 'Article',
])->save();
EntityViewDisplay::create([
'targetEntityType' => 'node',
'bundle' => 'article',
'mode' => 'default',
])->setStatus(TRUE)->save();
$display_teaser = EntityViewDisplay::create([
'targetEntityType' => 'node',
'bundle' => 'article',
'mode' => 'teaser',
]);
$display_teaser->save();
EntityFormDisplay::create([
'targetEntityType' => 'user',
'bundle' => 'user',
'mode' => 'default',
])->setStatus(TRUE)->save();
$form_display_teaser = EntityFormDisplay::create([
'targetEntityType' => 'user',
'bundle' => 'user',
'mode' => 'register',
]);
$form_display_teaser->save();
// Test getViewModeOptionsByBundle().
$view_modes = \Drupal::service('entity_display.repository')->getViewModeOptionsByBundle('node', 'article');
$this->assertEquals(['default' => 'Default'], $view_modes);
$display_teaser->setStatus(TRUE)->save();
$view_modes = \Drupal::service('entity_display.repository')->getViewModeOptionsByBundle('node', 'article');
$this->assertEquals(['default' => 'Default', 'teaser' => 'Teaser'], $view_modes);
// Test getFormModeOptionsByBundle().
$form_modes = \Drupal::service('entity_display.repository')->getFormModeOptionsByBundle('user', 'user');
$this->assertEquals(['default' => 'Default'], $form_modes);
$form_display_teaser->setStatus(TRUE)->save();
$form_modes = \Drupal::service('entity_display.repository')->getFormModeOptionsByBundle('user', 'user');
$this->assertEquals(['default' => 'Default', 'register' => 'Register'], $form_modes);
}
/**
* Tests components dependencies additions.
*/
public function testComponentDependencies(): void {
$this->enableModules(['dblog', 'help']);
$this->installSchema('dblog', ['watchdog']);
$this->installEntitySchema('user');
/** @var \Drupal\user\RoleInterface[] $roles */
$roles = [];
// Create two arbitrary user roles.
for ($i = 0; $i < 2; $i++) {
$roles[$i] = Role::create([
'id' => $this->randomMachineName(),
'label' => $this->randomString(),
]);
$roles[$i]->save();
}
// Create a field of type 'test_field' attached to 'entity_test'.
$field_name = $this->randomMachineName();
FieldStorageConfig::create([
'field_name' => $field_name,
'entity_type' => 'entity_test',
'type' => 'test_field',
])->save();
FieldConfig::create([
'field_name' => $field_name,
'entity_type' => 'entity_test',
'bundle' => 'entity_test',
])->save();
// Create a new form display without components.
/** @var \Drupal\Core\Entity\Display\EntityFormDisplayInterface $form_display */
$form_display = EntityFormDisplay::create([
'targetEntityType' => 'entity_test',
'bundle' => 'entity_test',
'mode' => 'default',
]);
$form_display->save();
$dependencies = ['user.role.' . $roles[0]->id(), 'user.role.' . $roles[1]->id()];
// The config object should not depend on none of the two $roles.
$this->assertNoDependency('config', $dependencies[0], $form_display);
$this->assertNoDependency('config', $dependencies[1], $form_display);
// Add a widget of type 'test_field_widget'.
$component = [
'type' => 'test_field_widget',
'settings' => [
'test_widget_setting' => $this->randomString(),
'role' => $roles[0]->id(),
'role2' => $roles[1]->id(),
],
'third_party_settings' => [
'help' => ['foo' => 'bar'],
],
];
$form_display->setComponent($field_name, $component);
$form_display->save();
// Now, the form display should depend on both user roles $roles.
$this->assertDependency('config', $dependencies[0], $form_display);
$this->assertDependency('config', $dependencies[1], $form_display);
// The form display should depend on 'help' module.
$this->assertDependency('module', 'help', $form_display);
// Delete the first user role entity.
$roles[0]->delete();
// Reload the form display.
$form_display = EntityFormDisplay::load($form_display->id());
// The display exists.
$this->assertNotEmpty($form_display);
// The form display should not depend on $role[0] anymore.
$this->assertNoDependency('config', $dependencies[0], $form_display);
// The form display should depend on 'anonymous' user role.
$this->assertDependency('config', 'user.role.anonymous', $form_display);
// The form display should depend on 'help' module.
$this->assertDependency('module', 'help', $form_display);
// Manually trigger the removal of configuration belonging to the module
// because KernelTestBase::disableModules() is not aware of this.
$this->container->get('config.manager')->uninstall('module', 'help');
// Uninstall 'help' module.
$this->disableModules(['help']);
// Reload the form display.
$form_display = EntityFormDisplay::load($form_display->id());
// The display exists.
$this->assertNotEmpty($form_display);
// The component is still enabled.
$this->assertNotNull($form_display->getComponent($field_name));
// The form display should not depend on 'help' module anymore.
$this->assertNoDependency('module', 'help', $form_display);
// Delete the 2nd user role entity.
$roles[1]->delete();
// Reload the form display.
$form_display = EntityFormDisplay::load($form_display->id());
// The display exists.
$this->assertNotEmpty($form_display);
// The component has been disabled.
$this->assertNull($form_display->getComponent($field_name));
$this->assertTrue($form_display->get('hidden')[$field_name]);
// The correct warning message has been logged.
$arguments = ['@display' => 'Entity form display', '@id' => $form_display->id(), '@name' => $field_name];
$variables = Database::getConnection()->select('watchdog', 'w')
->fields('w', ['variables'])
->condition('type', 'system')
->condition('message', "@display '@id': Component '@name' was disabled because its settings depend on removed dependencies.")
->execute()
->fetchField();
$this->assertEquals($arguments, unserialize($variables));
}
/**
* Asserts that $key is a $type type dependency of $display config entity.
*
* @param string $type
* The dependency type: 'config', 'content', 'module' or 'theme'.
* @param string $key
* The string to be checked.
* @param \Drupal\Core\Entity\Display\EntityDisplayInterface $display
* The entity display object to get dependencies from.
*
* @internal
*/
protected function assertDependency(string $type, string $key, EntityDisplayInterface $display): void {
$this->assertDependencyHelper(TRUE, $type, $key, $display);
}
/**
* Asserts that $key is not a $type type dependency of $display config entity.
*
* @param string $type
* The dependency type: 'config', 'content', 'module' or 'theme'.
* @param string $key
* The string to be checked.
* @param \Drupal\Core\Entity\Display\EntityDisplayInterface $display
* The entity display object to get dependencies from.
*
* @internal
*/
protected function assertNoDependency(string $type, string $key, EntityDisplayInterface $display): void {
$this->assertDependencyHelper(FALSE, $type, $key, $display);
}
/**
* Provides a helper for dependency assertions.
*
* @param bool $assertion
* Assertion: positive or negative.
* @param string $type
* The dependency type: 'config', 'content', 'module' or 'theme'.
* @param string $key
* The string to be checked.
* @param \Drupal\Core\Entity\Display\EntityDisplayInterface $display
* The entity display object to get dependencies from.
*
* @internal
*/
protected function assertDependencyHelper(bool $assertion, string $type, string $key, EntityDisplayInterface $display): void {
$all_dependencies = $display->getDependencies();
$dependencies = !empty($all_dependencies[$type]) ? $all_dependencies[$type] : [];
$context = $display instanceof EntityViewDisplayInterface ? 'View' : 'Form';
$value = $assertion ? in_array($key, $dependencies) : !in_array($key, $dependencies);
$display_id = $display->id();
$message = $assertion ? "$context display '$display_id' depends on $type '$key'." : "$context display '$display_id' do not depend on $type '$key'.";
$this->assertTrue($value, $message);
}
}

View File

@@ -0,0 +1,313 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\field_ui\Kernel;
use Drupal\Core\Entity\Entity\EntityFormDisplay;
use Drupal\Core\Entity\Entity\EntityFormMode;
use Drupal\field\Entity\FieldConfig;
use Drupal\field\Entity\FieldStorageConfig;
use Drupal\KernelTests\KernelTestBase;
/**
* Tests the entity display configuration entities.
*
* @group field_ui
*/
class EntityFormDisplayTest extends KernelTestBase {
/**
* Modules to install.
*
* @var string[]
*/
protected static $modules = [
'field_ui',
'field',
'entity_test',
'field_test',
'system',
'text',
'user',
];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->installEntitySchema('action');
$this->installConfig('user');
$this->installEntitySchema('entity_test');
}
/**
* @covers \Drupal\Core\Entity\EntityDisplayRepository::getFormDisplay
*/
public function testEntityGetFromDisplay(): void {
/** @var \Drupal\Core\Entity\EntityDisplayRepositoryInterface $display_repository */
$display_repository = \Drupal::service('entity_display.repository');
// Check that EntityDisplayRepositoryInterface::getFormDisplay() returns a
// fresh object when no configuration entry exists.
$form_display = $display_repository->getFormDisplay('entity_test', 'entity_test');
$this->assertTrue($form_display->isNew());
// Add some components and save the display.
$form_display->setComponent('component_1', ['weight' => 10])
->save();
// Check that EntityDisplayRepositoryInterface::getFormDisplay() returns the
// correct object.
$form_display = $display_repository->getFormDisplay('entity_test', 'entity_test');
$this->assertFalse($form_display->isNew());
$this->assertEquals('entity_test.entity_test.default', $form_display->id());
$this->assertEquals(['weight' => 10, 'settings' => [], 'third_party_settings' => [], 'region' => 'content'], $form_display->getComponent('component_1'));
}
/**
* Tests the behavior of a field component within an EntityFormDisplay object.
*/
public function testFieldComponent(): void {
// Create a field storage and a field.
$field_name = 'test_field';
$field_storage = FieldStorageConfig::create([
'field_name' => $field_name,
'entity_type' => 'entity_test',
'type' => 'test_field',
]);
$field_storage->save();
$field = FieldConfig::create([
'field_storage' => $field_storage,
'bundle' => 'entity_test',
]);
$field->save();
$form_display = EntityFormDisplay::create([
'targetEntityType' => 'entity_test',
'bundle' => 'entity_test',
'mode' => 'default',
]);
// Check that providing no options results in default values being used.
$form_display->setComponent($field_name);
$field_type_info = \Drupal::service('plugin.manager.field.field_type')->getDefinition($field_storage->getType());
$default_widget = $field_type_info['default_widget'];
$widget_settings = \Drupal::service('plugin.manager.field.widget')->getDefaultSettings($default_widget);
$expected = [
'weight' => 3,
'type' => $default_widget,
'settings' => $widget_settings,
'third_party_settings' => [],
];
$this->assertEquals($expected, $form_display->getComponent($field_name));
// Check that the getWidget() method returns the correct widget plugin.
$widget = $form_display->getRenderer($field_name);
$this->assertEquals($default_widget, $widget->getPluginId());
$this->assertEquals($widget_settings, $widget->getSettings());
// Check that the widget is statically persisted.
$this->assertSame($widget, $form_display->getRenderer($field_name));
// Check that changing the definition creates a new widget.
$form_display->setComponent($field_name, [
'type' => 'field_test_multiple',
]);
$renderer = $form_display->getRenderer($field_name);
$this->assertEquals('test_field_widget', $renderer->getPluginId());
$this->assertNotSame($widget, $renderer);
// Check that specifying an unknown widget (e.g. case of a disabled module)
// gets stored as is in the display, but results in the default widget being
// used.
$form_display->setComponent($field_name, [
'type' => 'unknown_widget',
]);
$options = $form_display->getComponent($field_name);
$this->assertEquals('unknown_widget', $options['type']);
$widget = $form_display->getRenderer($field_name);
$this->assertEquals($default_widget, $widget->getPluginId());
}
/**
* Tests the behavior of a field component for a base field.
*/
public function testBaseFieldComponent(): void {
$display = EntityFormDisplay::create([
'targetEntityType' => 'entity_test_base_field_display',
'bundle' => 'entity_test_base_field_display',
'mode' => 'default',
]);
// Check that default options are correctly filled in.
$formatter_settings = \Drupal::service('plugin.manager.field.widget')->getDefaultSettings('text_textfield');
$expected = [
'test_no_display' => NULL,
'test_display_configurable' => [
'type' => 'text_textfield',
'settings' => $formatter_settings,
'third_party_settings' => [],
'weight' => 10,
'region' => 'content',
],
'test_display_non_configurable' => [
'type' => 'text_textfield',
'settings' => $formatter_settings,
'third_party_settings' => [],
'weight' => 11,
'region' => 'content',
],
];
foreach ($expected as $field_name => $options) {
$this->assertEquals($options, $display->getComponent($field_name));
}
// Check that saving the display only writes data for fields whose display
// is configurable.
$display->save();
$config = $this->config('core.entity_form_display.' . $display->id());
$data = $config->get();
$this->assertFalse(isset($data['content']['test_no_display']));
$this->assertFalse(isset($data['hidden']['test_no_display']));
$this->assertEquals($expected['test_display_configurable'], $data['content']['test_display_configurable']);
$this->assertFalse(isset($data['content']['test_display_non_configurable']));
$this->assertFalse(isset($data['hidden']['test_display_non_configurable']));
// Check that defaults are correctly filled when loading the display.
$display = EntityFormDisplay::load($display->id());
foreach ($expected as $field_name => $options) {
$this->assertEquals($options, $display->getComponent($field_name));
}
// Check that data manually written for fields whose display is not
// configurable is discarded when loading the display.
$data['content']['test_display_non_configurable'] = $expected['test_display_non_configurable'];
$data['content']['test_display_non_configurable']['weight']++;
$config->setData($data)->save();
$display = EntityFormDisplay::load($display->id());
foreach ($expected as $field_name => $options) {
$this->assertEquals($options, $display->getComponent($field_name));
}
}
/**
* Tests deleting field.
*/
public function testDeleteField(): void {
$field_name = 'test_field';
// Create a field storage and a field.
$field_storage = FieldStorageConfig::create([
'field_name' => $field_name,
'entity_type' => 'entity_test',
'type' => 'test_field',
]);
$field_storage->save();
$field = FieldConfig::create([
'field_storage' => $field_storage,
'bundle' => 'entity_test',
]);
$field->save();
// Create default and compact entity display.
EntityFormMode::create([
'id' => 'entity_test.compact',
'label' => 'Compact',
'targetEntityType' => 'entity_test',
])->save();
EntityFormDisplay::create([
'targetEntityType' => 'entity_test',
'bundle' => 'entity_test',
'mode' => 'default',
])->setComponent($field_name)->save();
EntityFormDisplay::create([
'targetEntityType' => 'entity_test',
'bundle' => 'entity_test',
'mode' => 'compact',
])->setComponent($field_name)->save();
/** @var \Drupal\Core\Entity\EntityDisplayRepositoryInterface $display_repository */
$display_repository = \Drupal::service('entity_display.repository');
// Check the component exists.
$display = $display_repository->getFormDisplay('entity_test', 'entity_test');
$this->assertNotEmpty($display->getComponent($field_name));
$display = $display_repository->getFormDisplay('entity_test', 'entity_test', 'compact');
$this->assertNotEmpty($display->getComponent($field_name));
// Delete the field.
$field->delete();
// Check that the component has been removed from the entity displays.
$display = $display_repository->getFormDisplay('entity_test', 'entity_test');
$this->assertNull($display->getComponent($field_name));
$display = $display_repository->getFormDisplay('entity_test', 'entity_test', 'compact');
$this->assertNull($display->getComponent($field_name));
}
/**
* Tests \Drupal\Core\Entity\EntityDisplayBase::onDependencyRemoval().
*/
public function testOnDependencyRemoval(): void {
$this->enableModules(['field_plugins_test']);
$field_name = 'test_field';
// Create a field.
$field_storage = FieldStorageConfig::create([
'field_name' => $field_name,
'entity_type' => 'entity_test',
'type' => 'text',
]);
$field_storage->save();
$field = FieldConfig::create([
'field_storage' => $field_storage,
'bundle' => 'entity_test',
]);
$field->save();
EntityFormDisplay::create([
'targetEntityType' => 'entity_test',
'bundle' => 'entity_test',
'mode' => 'default',
])->setComponent($field_name, ['type' => 'field_plugins_test_text_widget'])->save();
/** @var \Drupal\Core\Entity\EntityDisplayRepositoryInterface $display_repository */
$display_repository = \Drupal::service('entity_display.repository');
// Check the component exists and is of the correct type.
$display = $display_repository->getFormDisplay('entity_test', 'entity_test');
$this->assertEquals('field_plugins_test_text_widget', $display->getComponent($field_name)['type']);
// Removing the field_plugins_test module should change the component to use
// the default widget for test fields.
\Drupal::service('config.manager')->uninstall('module', 'field_plugins_test');
$display = $display_repository->getFormDisplay('entity_test', 'entity_test');
$this->assertEquals('text_textfield', $display->getComponent($field_name)['type']);
// Removing the text module should remove the field from the form display.
\Drupal::service('config.manager')->uninstall('module', 'text');
$display = $display_repository->getFormDisplay('entity_test', 'entity_test');
$this->assertNull($display->getComponent($field_name));
}
/**
* Tests the serialization and unserialization of the class.
*/
public function testSerialization(): void {
/** @var \Drupal\Core\Entity\EntityDisplayRepositoryInterface $display_repository */
$display_repository = \Drupal::service('entity_display.repository');
$form_display = $display_repository->getFormDisplay('entity_test', 'entity_test');
// Make sure the langcode base field is visible in the original form
// display.
$this->assertNotEmpty($form_display->getComponent('langcode'));
// Remove the langcode.
$form_display->removeComponent('langcode');
$unserialized = unserialize(serialize($form_display));
// Verify that components are retained upon unserialization.
$this->assertEquals($form_display->getComponents(), $unserialized->getComponents());
}
}

View File

@@ -0,0 +1,154 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\field_ui\Traits;
/**
* Provides common functionality for the Field UI tests that depend on JS.
*/
trait FieldUiJSTestTrait {
/**
* Creates a new field through the Field UI.
*
* @param string|null $bundle_path
* Admin path of the bundle that the new field is to be attached to.
* @param string $field_name
* The field name of the new field storage.
* @param string|null $label
* (optional) The label of the new field. Defaults to a random string.
* @param string $field_type
* (optional) The field type of the new field storage. Defaults to
* 'test_field'.
* @param bool $save_settings
* (optional) Parameter for conditional execution of second and third step
* (Saving the storage settings and field settings). Defaults to 'TRUE'.
*
* @throws \Behat\Mink\Exception\ElementNotFoundException
*/
public function fieldUIAddNewFieldJS(?string $bundle_path, string $field_name, ?string $label = NULL, string $field_type = 'test_field', bool $save_settings = TRUE): void {
$label = $label ?: $field_name;
// Allow the caller to set a NULL path in case they navigated to the right
// page before calling this method.
if ($bundle_path !== NULL) {
$bundle_path = "$bundle_path/fields/add-field";
$this->drupalGet($bundle_path);
}
// First step: 'Add field' page.
$session = $this->getSession();
$page = $session->getPage();
$assert_session = $this->assertSession();
if ($assert_session->waitForElementVisible('css', "[name='new_storage_type'][value='$field_type']")) {
$page = $this->getSession()->getPage();
$field_card = $page->find('css', "[name='new_storage_type'][value='$field_type']")->getParent();
}
else {
$field_card = $this->getFieldFromGroupJS($field_type);
}
$field_card?->click();
$page->findButton('Continue')->click();
$field_label = $page->findField('edit-label');
$this->assertTrue($field_label->isVisible());
$field_label = $page->find('css', 'input[data-drupal-selector="edit-label"]');
$field_label->setValue($label);
$machine_name = $assert_session->waitForElementVisible('css', '[data-drupal-selector="edit-label"] + * .machine-name-value');
$this->assertNotEmpty($machine_name);
$page->findButton('Edit')->press();
$field_field_name = $page->findField('field_name');
$this->assertTrue($field_field_name->isVisible());
$field_field_name->setValue($field_name);
$page->findButton('Continue')->click();
$assert_session->waitForText("These settings apply to the $label field everywhere it is used.");
if ($save_settings) {
// Second step: Save field settings.
$page->findButton('Save settings')->click();
$assert_session->pageTextContains("Saved $label configuration.");
// Check that the field appears in the overview form.
$row = $page->find('css', '#field-' . $field_name);
$this->assertNotEmpty($row, 'Field was created and appears in the overview page.');
}
}
/**
* Adds an existing field through the Field UI.
*
* @param string $bundle_path
* Admin path of the bundle that the field is to be attached to.
* @param string $existing_storage_name
* The name of the existing field storage for which we want to add a new
* field.
* @param string|null $label
* (optional) The label of the new field. Defaults to a random string.
* @param array $field_edit
* (optional) $edit parameter for submitForm() on the second step
* ('Field settings' form).
*/
public function fieldUIAddExistingFieldJS(string $bundle_path, string $existing_storage_name, ?string $label = NULL, array $field_edit = []): void {
$label = $label ?: $this->randomMachineName();
$field_edit['edit-label'] = $label;
// First step: navigate to the re-use field page.
$this->drupalGet("{$bundle_path}/fields/");
// Confirm that the local action is visible.
$this->assertSession()->linkExists('Re-use an existing field');
$this->clickLink('Re-use an existing field');
// Wait for the modal to open.
$this->assertSession()->waitForElementVisible('css', '#drupal-modal');
$this->assertSession()->elementExists('css', "input[value=Re-use][name=$existing_storage_name]");
$this->click("input[value=Re-use][name=$existing_storage_name]");
// Set the main content to only the content region because the label can
// contain HTML which will be auto-escaped by Twig.
$this->assertSession()->responseContains('field-config-edit-form');
// Check that the page does not have double escaped HTML tags.
$this->assertSession()->responseNotContains('&amp;lt;');
// Second step: 'Field settings' form.
$this->submitForm($field_edit, 'Save settings');
$this->assertSession()->pageTextContains("Saved $label configuration.");
// Check that the field appears in the overview form.
$xpath = $this->assertSession()->buildXPathQuery("//table[@id=\"field-overview\"]//tr/td[1 and text() = :label]", [
':label' => $label,
]);
$this->assertSession()->elementExists('xpath', $xpath);
}
/**
* Helper function that returns the field card element if it is in a group.
*
* @param string $field_type
* The name of the field type.
*
* @return \Behat\Mink\Element\NodeElement|false|mixed|null
* Field card element within a group.
*/
public function getFieldFromGroupJS($field_type) {
$group_elements = $this->getSession()->getPage()->findAll('css', '.field-option-radio');
$groups = [];
foreach ($group_elements as $group_element) {
$groups[] = $group_element->getAttribute('value');
}
$field_card = NULL;
foreach ($groups as $group) {
$group_field_card = $this->getSession()->getPage()->find('css', "[name='new_storage_type'][value='$group']")->getParent();
$group_field_card->click();
$this->getSession()->getPage()->pressButton('Continue');
$field_card = $this->getSession()->getPage()->find('css', "[name='group_field_options_wrapper'][value='$field_type']");
if ($field_card) {
break;
}
$this->getSession()->getPage()->pressButton('Back');
}
return $field_card->getParent();
}
}

View File

@@ -0,0 +1,269 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\field_ui\Traits;
use Behat\Mink\Exception\ElementNotFoundException;
/**
* Provides common functionality for the Field UI test classes.
*/
trait FieldUiTestTrait {
/**
* Creates a new field through the Field UI.
*
* @param string $bundle_path
* Admin path of the bundle that the new field is to be attached to.
* @param string $field_name
* The field name of the new field storage.
* @param string $label
* (optional) The label of the new field. Defaults to a random string.
* @param string $field_type
* (optional) The field type of the new field storage. Defaults to
* 'test_field'.
* @param array $storage_edit
* (optional) $edit parameter for submitForm() on the second step
* ('Storage settings' form).
* @param array $field_edit
* (optional) $edit parameter for submitForm() on the third step ('Field
* settings' form).
* @param bool $save_settings
* (optional) Parameter for conditional execution of second and third step
* (Saving the storage settings and field settings). Defaults to 'TRUE'.
*/
public function fieldUIAddNewField($bundle_path, $field_name, $label = NULL, $field_type = 'test_field', array $storage_edit = [], array $field_edit = [], bool $save_settings = TRUE) {
// Generate a label containing only letters and numbers to prevent random
// test failure.
// See https://www.drupal.org/project/drupal/issues/3030902
$label = $label ?: $this->randomMachineName();
$initial_edit = [
'new_storage_type' => $field_type,
];
$second_edit = [
'label' => $label,
'field_name' => $field_name,
];
// Allow the caller to set a NULL path in case they navigated to the right
// page before calling this method.
if ($bundle_path !== NULL) {
$bundle_path = "$bundle_path/fields/add-field";
// First step: 'Add field' page.
$this->drupalGet($bundle_path);
}
else {
$bundle_path = $this->getUrl();
}
try {
// First check if the passed in field type is not part of a group.
$this->assertSession()->elementExists('css', "[name='new_storage_type'][value='$field_type']");
}
// If the element could not be found then it is probably in a group.
catch (ElementNotFoundException) {
// Call the helper function to confirm it is in a group.
$field_group = $this->getFieldFromGroup($field_type);
if ($field_group) {
// Pass in the group name as the new storage type.
$initial_edit['new_storage_type'] = $field_group;
$second_edit['group_field_options_wrapper'] = $field_type;
$this->drupalGet($bundle_path);
}
}
$this->submitForm($initial_edit, 'Continue');
$this->submitForm($second_edit, 'Continue');
// Assert that the field is not created.
$this->assertFieldDoesNotExist($bundle_path, $label);
if ($save_settings) {
$this->assertSession()->pageTextContains("These settings apply to the $label field everywhere it is used.");
// Test Breadcrumbs.
$this->getSession()->getPage()->findLink($label);
// Ensure that each array key in $storage_edit is prefixed with field_storage.
$prefixed_storage_edit = [];
foreach ($storage_edit as $key => $value) {
if (str_starts_with($key, 'field_storage')) {
$prefixed_storage_edit[$key] = $value;
continue;
}
// If the key starts with settings, it needs to be prefixed differently.
if (str_starts_with($key, 'settings[')) {
$prefixed_storage_edit[str_replace('settings[', 'field_storage[subform][settings][', $key)] = $value;
continue;
}
$prefixed_storage_edit['field_storage[subform][' . $key . ']'] = $value;
}
// Second step: 'Storage settings' form.
$this->submitForm($prefixed_storage_edit, 'Update settings');
// Third step: 'Field settings' form.
$this->submitForm($field_edit, 'Save settings');
$this->assertSession()->pageTextContains("Saved $label configuration.");
// Check that the field appears in the overview form.
$this->assertFieldExistsOnOverview($label);
}
}
/**
* Adds an existing field through the Field UI.
*
* @param string $bundle_path
* Admin path of the bundle that the field is to be attached to.
* @param string $existing_storage_name
* The name of the existing field storage for which we want to add a new
* field.
* @param string $label
* (optional) The label of the new field. Defaults to a random string.
* @param array $field_edit
* (optional) $edit parameter for submitForm() on the second step
* ('Field settings' form).
*/
public function fieldUIAddExistingField($bundle_path, $existing_storage_name, $label = NULL, array $field_edit = []) {
$label = $label ?: $this->randomMachineName();
$field_edit['edit-label'] = $label;
// First step: navigate to the re-use field page.
$this->drupalGet("{$bundle_path}/fields/");
// Confirm that the local action is visible.
$this->assertSession()->linkExists('Re-use an existing field');
$this->clickLink('Re-use an existing field');
$this->assertSession()->elementExists('css', "input[value=Re-use][name=$existing_storage_name]");
$this->click("input[value=Re-use][name=$existing_storage_name]");
// Set the main content to only the content region because the label can
// contain HTML which will be auto-escaped by Twig.
$this->assertSession()->responseContains('field-config-edit-form');
// Check that the page does not have double escaped HTML tags.
$this->assertSession()->responseNotContains('&amp;lt;');
// Second step: 'Field settings' form.
$this->submitForm($field_edit, 'Save settings');
$this->assertSession()->pageTextContains("Saved $label configuration.");
// Check that the field appears in the overview form.
$xpath = $this->assertSession()->buildXPathQuery("//table[@id=\"field-overview\"]//tr/td[1 and text() = :label]", [
':label' => $label,
]);
$this->assertSession()->elementExists('xpath', $xpath);
}
/**
* Deletes a field through the Field UI.
*
* @param string $bundle_path
* Admin path of the bundle that the field is to be deleted from.
* @param string $field_name
* The name of the field.
* @param string $label
* The label of the field.
* @param string $bundle_label
* The label of the bundle.
* @param string $source_label
* (optional) The label of the source entity type bundle.
*/
public function fieldUIDeleteField($bundle_path, $field_name, $label, $bundle_label, string $source_label = '') {
// Display confirmation form.
$this->drupalGet("$bundle_path/fields/$field_name/delete");
$this->assertSession()->pageTextContains("Are you sure you want to delete the field $label");
// Test Breadcrumbs.
$this->assertSession()->linkExists($label, 0, 'Field label is correct in the breadcrumb of the field delete page.');
// Submit confirmation form.
$this->submitForm([], 'Delete');
$this->assertSession()->pageTextContains("The field $label has been deleted from the $bundle_label $source_label");
// Check that the field does not appear in the overview form.
$xpath = $this->assertSession()->buildXPathQuery('//table[@id="field-overview"]//span[@class="label-field" and text()= :label]', [
':label' => $label,
]);
$this->assertSession()->elementNotExists('xpath', $xpath);
}
/**
* Helper function that returns the name of the group that a field is in.
*
* @param string $field_type
* The name of the field type.
*
* @return string
* Group name
*/
public function getFieldFromGroup($field_type) {
$group_elements = $this->getSession()->getPage()->findAll('css', '.field-option-radio');
$groups = [];
foreach ($group_elements as $group_element) {
$groups[] = $group_element->getAttribute('value');
}
foreach ($groups as $group) {
$test = [
'new_storage_type' => $group,
];
$this->submitForm($test, 'Continue');
try {
$this->assertSession()->elementExists('css', "[name='group_field_options_wrapper'][value='$field_type']");
$this->submitForm([], 'Back');
return $group;
}
catch (ElementNotFoundException) {
$this->submitForm([], 'Back');
continue;
}
}
return NULL;
}
/**
* Asserts that the field doesn't exist in the overview form.
*
* @param string $bundle_path
* The bundle path.
* @param string $label
* The field label.
*/
protected function assertFieldDoesNotExist(string $bundle_path, string $label) {
$original_url = $this->getUrl();
$this->drupalGet(explode('/fields', $bundle_path)[0] . '/fields');
$this->assertFieldDoesNotExistOnOverview($label);
$this->drupalGet($original_url);
}
/**
* Asserts that the field appears on the overview form.
*
* @param string $label
* The field label.
*
* @throws \Behat\Mink\Exception\ElementNotFoundException
*/
protected function assertFieldExistsOnOverview(string $label) {
$xpath = $this->assertSession()
->buildXPathQuery("//table[@id=\"field-overview\"]//tr/td[1 and text() = :label]", [
':label' => $label,
]);
$element = $this->getSession()->getPage()->find('xpath', $xpath);
if ($element === NULL) {
throw new ElementNotFoundException($this->getSession()->getDriver(), 'form field', 'label', $label);
}
}
/**
* Asserts that the field does not appear on the overview form.
*
* @param string $label
* The field label.
*/
protected function assertFieldDoesNotExistOnOverview(string $label) {
$xpath = $this->assertSession()
->buildXPathQuery("//table[@id=\"field-overview\"]//tr/td[1 and text() = :label]", [
':label' => $label,
]);
$element = $this->getSession()->getPage()->find('xpath', $xpath);
$this->assertSession()->assert($element === NULL, sprintf('A field "%s" appears on this page, but it should not.', $label));
}
}

View File

@@ -0,0 +1,82 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\field_ui\Unit;
use Drupal\Core\Entity\EntityDisplayRepositoryInterface;
use Drupal\Core\Render\ElementInfoManagerInterface;
use Drupal\Core\TempStore\PrivateTempStore;
use Drupal\field_ui\Form\FieldConfigEditForm;
use Drupal\Tests\UnitTestCase;
/**
* @coversDefaultClass \Drupal\field_ui\Form\FieldConfigEditForm
*
* @group field_ui
*/
class FieldConfigEditFormTest extends UnitTestCase {
/**
* The field config edit form.
*
* @var \Drupal\field_ui\Form\FieldConfigEditForm|\PHPUnit\Framework\MockObject\MockObject
*/
protected $fieldConfigEditForm;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$entity_type_bundle_info = $this->createMock('\Drupal\Core\Entity\EntityTypeBundleInfoInterface');
$typed_data = $this->createMock('\Drupal\Core\TypedData\TypedDataManagerInterface');
$temp_store = $this->createMock(PrivateTempStore::class);
$element_info_manager = $this->createMock(ElementInfoManagerInterface::class);
$entity_display_repository = $this->createMock(EntityDisplayRepositoryInterface::class);
$this->fieldConfigEditForm = new FieldConfigEditForm($entity_type_bundle_info, $typed_data, $entity_display_repository, $temp_store, $element_info_manager);
}
/**
* @covers ::hasAnyRequired
*
* @dataProvider providerRequired
*/
public function testHasAnyRequired(array $element, bool $result): void {
$reflection = new \ReflectionClass('\Drupal\field_ui\Form\FieldConfigEditForm');
$method = $reflection->getMethod('hasAnyRequired');
$this->assertEquals($result, $method->invoke($this->fieldConfigEditForm, $element));
}
/**
* Provides test cases with required and optional elements.
*/
public static function providerRequired(): \Generator {
yield 'required' => [
[['#required' => TRUE]],
TRUE,
];
yield 'optional' => [
[['#required' => FALSE]],
FALSE,
];
yield 'required and optional' => [
[['#required' => TRUE], ['#required' => FALSE]],
TRUE,
];
yield 'empty' => [
[[], []],
FALSE,
];
yield 'multiple required' => [
[[['#required' => TRUE]], [['#required' => TRUE]]],
TRUE,
];
yield 'multiple optional' => [
[[['#required' => FALSE]], [['#required' => FALSE]]],
FALSE,
];
}
}

View File

@@ -0,0 +1,92 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\field_ui\Unit;
use Drupal\Tests\UnitTestCase;
/**
* @coversDefaultClass \Drupal\field_ui\Element\FieldUiTable
*
* @group field_ui
*/
class FieldUiTableTest extends UnitTestCase {
/**
* @covers ::reduceOrder
*
* @dataProvider providerTestReduceOrder
*/
public function testReduceOrder($array, $expected): void {
$this->assertSame($expected, array_reduce($array, ['Drupal\field_ui\Element\FieldUiTable', 'reduceOrder']));
}
/**
* Provides test data for testReduceOrder().
*/
public static function providerTestReduceOrder() {
return [
'Flat' => [
'array' => [
[
'name' => 'foo',
],
[
'name' => 'bar',
],
[
'name' => 'baz',
],
],
'expected' => ['foo', 'bar', 'baz'],
],
'Nested' => [
'array' => [
[
'name' => 'foo',
'children' => [
[
'name' => 'bar',
'weight' => 0,
],
[
'name' => 'baz',
'weight' => -1,
],
],
],
[
'name' => 'biz',
],
],
'expected' => ['foo', 'baz', 'bar', 'biz'],
],
'Nested no name key' => [
'array' => [
[
'children' => [
[
'name' => 'foo',
'weight' => -1,
],
[
'name' => 'bar',
'weight' => 1,
],
[
'name' => 'baz',
'weight' => 0,
],
],
],
[
'name' => 'biz',
],
],
'expected' => ['foo', 'baz', 'bar', 'biz'],
],
];
}
}

View File

@@ -0,0 +1,74 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\field_ui\Unit;
use Drupal\Core\DependencyInjection\ContainerBuilder;
use Drupal\field_ui\FieldUI;
use Drupal\Tests\UnitTestCase;
/**
* @coversDefaultClass \Drupal\field_ui\FieldUI
*
* @group field_ui
*/
class FieldUiTest extends UnitTestCase {
/**
* The path validator.
*
* @var \Drupal\Core\Path\PathValidatorInterface|\PHPUnit\Framework\MockObject\MockObject
*/
protected $pathValidator;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->pathValidator = $this->createMock('Drupal\Core\Path\PathValidatorInterface');
$container = new ContainerBuilder();
$container->set('path.validator', $this->pathValidator);
\Drupal::setContainer($container);
}
/**
* @covers ::getNextDestination
*/
public function testGetNextDestination(): void {
$destinations = ['admin', 'admin/content'];
$expected_uri = 'base:admin';
$expected_query = [
'destinations' => ['admin/content'],
];
$actual = FieldUI::getNextDestination($destinations);
$this->assertSame($expected_uri, $actual->getUri());
$this->assertSame($expected_query, $actual->getOption('query'));
}
/**
* @covers ::getNextDestination
*/
public function testGetNextDestinationEmpty(): void {
$destinations = [];
$actual = FieldUI::getNextDestination($destinations);
$this->assertNull($actual);
}
/**
* @covers ::getNextDestination
*/
public function testGetNextDestinationRouteName(): void {
$destinations = [['route_name' => 'system.admin'], ['route_name' => 'system.admin_content']];
$expected_route_name = 'system.admin';
$expected_query = [
'destinations' => [['route_name' => 'system.admin_content']],
];
$actual = FieldUI::getNextDestination($destinations);
$this->assertSame($expected_route_name, $actual->getRouteName());
$this->assertSame($expected_query, $actual->getOption('query'));
}
}