Skip to content
Snippets Groups Projects
Commit 693ced9c authored by Ben Mullins's avatar Ben Mullins
Browse files

Issue #2521800 by srishtiiee, bnjmnm, tim.plunkett, narendraR, lauriii,...

Issue #2521800 by srishtiiee, bnjmnm, tim.plunkett, narendraR, lauriii, Utkarsh_33, hooroomoo, LewisNyman, smustgrave, Wim Leers: List key|label entry field is textarea, which doesn't give guidance towards the expected input
parent 0f2e209b
No related branches found
No related tags found
46 merge requests!12227Issue #3181946 by jonmcl, mglaman,!54479.5.x SF update,!5014Issue #3071143: Table Render Array Example Is Incorrect,!4868Issue #1428520: Improve menu parent link selection,!4594Applying patch for Views Global Text area field to allow extra HTML tags. As video, source and iframe tag is not rendering. Due to which Media embedded video and remote-video not rendering in Views Global Text area field.,!4289Issue #1344552 by marcingy, Niklas Fiekas, Ravi.J, aleevas, Eduardo Morales...,!3878Removed unused condition head title for views,!38582585169-10.1.x,!3818Issue #2140179: $entity->original gets stale between updates,!3742Issue #3328429: Create item list field formatter for displaying ordered and unordered lists,!3731Claro: role=button on status report items,!3668Resolve #3347842 "Deprecate the trusted",!3651Issue #3347736: Create new SDC component for Olivero (header-search),!3546refactored dialog.pcss file,!3531Issue #3336994: StringFormatter always displays links to entity even if the user in context does not have access,!3502Issue #3335308: Confusing behavior with FormState::setFormState and FormState::setMethod,!3478Issue #3337882: Deleted menus are not removed from content type config,!3452Issue #3332701: Refactor Claro's tablesort-indicator stylesheet,!3451Issue #2410579: Allows setting the current language programmatically.,!3355Issue #3209129: Scrolling problems when adding a block via layout builder,!3226Issue #2987537: Custom menu link entity type should not declare "bundle" entity key,!3154Fixes #2987987 - CSRF token validation broken on routes with optional parameters.,!3147Issue #3328457: Replace most substr($a, $i) where $i is negative with str_ends_with(),!3146Issue #3328456: Replace substr($a, 0, $i) with str_starts_with(),!3133core/modules/system/css/components/hidden.module.css,!31312878513-10.1.x,!2964Issue #2865710 : Dependencies from only one instance of a widget are used in display modes,!2812Issue #3312049: [Followup] Fix Drupal.Commenting.FunctionComment.MissingReturnType returns for NULL,!2614Issue #2981326: Replace non-test usages of \Drupal::logger() with IoC injection,!2378Issue #2875033: Optimize joins and table selection in SQL entity query implementation,!2334Issue #3228209: Add hasRole() method to AccountInterface,!2062Issue #3246454: Add weekly granularity to views date sort,!1591Issue #3199697: Add JSON:API Translation experimental module,!1255Issue #3238922: Refactor (if feasible) uses of the jQuery serialize function to use vanillaJS,!1105Issue #3025039: New non translatable field on translatable content throws error,!1073issue #3191727: Focus states on mobile second level navigation items fixed,!10223132456: Fix issue where views instances are emptied before an ajax request is complete,!877Issue #2708101: Default value for link text is not saved,!844Resolve #3036010 "Updaters",!673Issue #3214208: FinishResponseSubscriber could create duplicate headers,!617Issue #3043725: Provide a Entity Handler for user cancelation,!579Issue #2230909: Simple decimals fail to pass validation,!560Move callback classRemove outside of the loop,!555Issue #3202493,!485Sets the autocomplete attribute for username/password input field on login form.,!30Issue #3182188: Updates composer usage to point at ./vendor/bin/composer
Showing
with 767 additions and 168 deletions
......@@ -22,6 +22,10 @@
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;
......
......@@ -5,10 +5,12 @@
* Defines selection, check box and radio button widgets for text and numeric fields.
*/
use Drupal\Component\Utility\NestedArray;
use Drupal\Core\Url;
use Drupal\Core\Entity\FieldableEntityInterface;
use Drupal\Core\Entity\Exception\FieldStorageDefinitionUpdateForbiddenException;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\field\FieldStorageConfigInterface;
......@@ -136,3 +138,20 @@ function _options_values_in_use($entity_type, $field_name, $values) {
return FALSE;
}
/**
* Implements hook_form_FORM_ID_alter().
*
* Add additional classes to enable styling for list field types.
*
* @see \Drupal\options\Plugin\Field\FieldType\ListItemBase::storageSettingsForm
*/
function options_form_field_storage_config_edit_form_alter(&$form, FormStateInterface $form_state) {
$table = &NestedArray::getValue($form, ['settings', 'allowed_values', 'table']);
if (!$table) {
return;
}
$form['#attached']['library'][] = 'field_ui/drupal.field_ui';
$table['#attributes']['class'][] = 'allowed-values-table';
}
......@@ -4,6 +4,8 @@
use Drupal\Core\Field\FieldFilteredMarkup;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Render\Element;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\Core\TypedData\DataDefinition;
......@@ -52,11 +54,7 @@ public static function schema(FieldStorageDefinitionInterface $field_definition)
* {@inheritdoc}
*/
protected function allowedValuesDescription() {
$description = '<p>' . $this->t('The possible values this field can contain. Enter one value per line, in the format key|label.');
$description .= '<br/>' . $this->t('The key is the stored value, and must be numeric. The label will be used in displayed values and edit forms.');
$description .= '<br/>' . $this->t('The label is optional: if a line contains a single number, it will be used as key and label.');
$description .= '<br/>' . $this->t('Lists of labels are also accepted (one label per line), only if the field does not hold any values yet. Numeric keys will be automatically generated from the positions in the list.');
$description .= '</p>';
$description = '<p>' . $this->t('The name will be used in displayed options and edit forms. The value is the stored value, and must be numeric.') . '</p>';
$description .= '<p>' . $this->t('Allowed HTML tags in labels: @tags', ['@tags' => FieldFilteredMarkup::displayAllowedTags()]) . '</p>';
return $description;
}
......@@ -115,4 +113,20 @@ protected static function castAllowedValue($value) {
return (float) $value;
}
/**
* {@inheritdoc}
*/
public function storageSettingsForm(array &$form, FormStateInterface $form_state, $has_data) {
$element = parent::storageSettingsForm($form, $form_state, $has_data);
foreach (Element::children($element['allowed_values']['table']) as $delta => $row) {
// @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/number
// @see \Drupal\Core\Field\Plugin\Field\FieldWidget\NumberWidget::formElement()
$element['allowed_values']['table'][$delta]['item']['key']['#step'] = 'any';
$element['allowed_values']['table'][$delta]['item']['key']['#type'] = 'number';
}
return $element;
}
}
......@@ -4,6 +4,8 @@
use Drupal\Core\Field\FieldFilteredMarkup;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Render\Element;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\Core\TypedData\DataDefinition;
......@@ -52,11 +54,7 @@ public static function schema(FieldStorageDefinitionInterface $field_definition)
* {@inheritdoc}
*/
protected function allowedValuesDescription() {
$description = '<p>' . $this->t('The possible values this field can contain. Enter one value per line, in the format key|label.');
$description .= '<br/>' . $this->t('The key is the stored value, and must be numeric. The label will be used in displayed values and edit forms.');
$description .= '<br/>' . $this->t('The label is optional: if a line contains a single number, it will be used as key and label.');
$description .= '<br/>' . $this->t('Lists of labels are also accepted (one label per line), only if the field does not hold any values yet. Numeric keys will be automatically generated from the positions in the list.');
$description .= '</p>';
$description = '<p>' . $this->t('The name will be used in displayed options and edit forms. The value is the stored value, and must be numeric.') . '</p>';
$description .= '<p>' . $this->t('Allowed HTML tags in labels: @tags', ['@tags' => FieldFilteredMarkup::displayAllowedTags()]) . '</p>';
return $description;
}
......@@ -77,4 +75,19 @@ protected static function castAllowedValue($value) {
return (int) $value;
}
/**
* {@inheritdoc}
*/
public function storageSettingsForm(array &$form, FormStateInterface $form_state, $has_data) {
$element = parent::storageSettingsForm($form, $form_state, $has_data);
foreach (Element::children($element['allowed_values']['table']) as $delta => $row) {
// @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/number
// @see \Drupal\Core\Field\Plugin\Field\FieldWidget\NumberWidget::formElement()
$element['allowed_values']['table'][$delta]['item']['key']['#type'] = 'number';
}
return $element;
}
}
......@@ -2,10 +2,13 @@
namespace Drupal\options\Plugin\Field\FieldType;
use Drupal\Component\Utility\Html;
use Drupal\Component\Utility\NestedArray;
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\Field\FieldItemBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Form\OptGroup;
use Drupal\Core\Render\Element;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\Core\TypedData\OptionsProviderInterface;
......@@ -84,24 +87,131 @@ public function isEmpty() {
* {@inheritdoc}
*/
public function storageSettingsForm(array &$form, FormStateInterface $form_state, $has_data) {
$allowed_values = $this->getSetting('allowed_values');
if (!array_key_exists('allowed_values', $form_state->getStorage())) {
$form_state->set('allowed_values', $this->getFieldDefinition()->getSetting('allowed_values'));
}
$allowed_values = $form_state->getStorage()['allowed_values'];
$allowed_values_function = $this->getSetting('allowed_values_function');
if (!$form_state->get('items_count')) {
$form_state->set('items_count', max(count($allowed_values), 0));
}
$wrapper_id = Html::getUniqueId('allowed-values-wrapper');
$element['allowed_values'] = [
'#type' => 'textarea',
'#title' => $this->t('Allowed values list'),
'#default_value' => $this->allowedValuesString($allowed_values),
'#rows' => 10,
'#access' => empty($allowed_values_function),
'#element_validate' => [[static::class, 'validateAllowedValues']],
'#field_has_data' => $has_data,
'#field_name' => $this->getFieldDefinition()->getName(),
'#entity_type' => $this->getEntity()->getEntityTypeId(),
'#allowed_values' => $allowed_values,
'#required' => TRUE,
'#prefix' => '<div id="' . $wrapper_id . '">',
'#suffix' => '</div>',
'#access' => empty($allowed_values_function),
'help_text' => ['#markup' => $this->allowedValuesDescription()],
];
$element['allowed_values']['table'] = [
'#type' => 'table',
'#header' => [
$this->t('Allowed values'),
$this->t('Delete'),
$this->t('Weight'),
],
'#attributes' => [
'id' => 'allowed-values-order',
],
'#tabledrag' => [
[
'action' => 'order',
'relationship' => 'sibling',
'group' => 'weight',
],
],
];
$element['allowed_values']['#description'] = $this->allowedValuesDescription();
$max = $form_state->get('items_count');
$entity_type_id = $this->getFieldDefinition()->getTargetEntityTypeId();
$field_name = $this->getFieldDefinition()->getName();
$current_keys = array_keys($allowed_values);
for ($delta = 0; $delta <= $max; $delta++) {
$element['allowed_values']['table'][$delta] = [
'#attributes' => [
'class' => ['draggable'],
],
'#weight' => $delta,
];
$element['allowed_values']['table'][$delta]['item'] = [
'label' => [
'#type' => 'textfield',
'#title' => $this->t('Name'),
'#weight' => -30,
'#default_value' => isset($current_keys[$delta]) ? $allowed_values[$current_keys[$delta]] : '',
'#required' => $delta === 0,
],
'key' => [
'#type' => 'textfield',
'#maxlength' => 255,
'#title' => $this->t('Value'),
'#default_value' => $current_keys[$delta] ?? '',
'#weight' => -20,
'#required' => $delta === 0,
],
];
$element['allowed_values']['table'][$delta]['delete'] = [
'#type' => 'submit',
'#value' => $this->t('Remove'),
'#name' => "remove_row_button__$delta",
'#id' => "remove_row_button__$delta",
'#delta' => $delta,
'#submit' => [[static::class, 'deleteSubmit']],
'#limit_validation_errors' => [],
'#ajax' => [
'callback' => [static::class, 'deleteAjax'],
'wrapper' => $wrapper_id,
'effect' => 'fade',
],
];
$element['allowed_values']['table'][$delta]['weight'] = [
'#type' => 'weight',
'#title' => $this->t('Weight for row @number', ['@number' => $delta + 1]),
'#title_display' => 'invisible',
'#delta' => 50,
'#default_value' => 0,
'#attributes' => ['class' => ['weight']],
];
if ($delta < count($allowed_values)) {
$query = \Drupal::entityQuery($entity_type_id)
->accessCheck(FALSE)
->condition($field_name, $current_keys[$delta]);
$entity_ids = $query->execute();
if (!empty($entity_ids)) {
$element['allowed_values']['table'][$delta]['item']['key']['#attributes']['disabled'] = 'disabled';
$element['allowed_values']['table'][$delta]['delete']['#attributes']['disabled'] = 'disabled';
$element['allowed_values']['table'][$delta]['delete'] += [
'message' => [
'#type' => 'item',
'#markup' => $this->t('Cannot be removed: option in use.'),
],
];
}
}
}
$element['allowed_values']['table']['#max_delta'] = $max;
$element['allowed_values']['add_more_allowed_values'] = [
'#type' => 'submit',
'#name' => 'add_more_allowed_values',
'#value' => $this->t('Add another item'),
'#attributes' => ['class' => ['field-add-more-submit']],
// Allow users to add another row without requiring existing rows to have
// values.
'#limit_validation_errors' => [],
'#submit' => [[static::class, 'addMoreSubmit']],
'#ajax' => [
'callback' => [static::class, 'addMoreAjax'],
'wrapper' => $wrapper_id,
'effect' => 'fade',
],
];
$element['allowed_values_function'] = [
'#type' => 'item',
......@@ -114,6 +224,71 @@ public function storageSettingsForm(array &$form, FormStateInterface $form_state
return $element;
}
/**
* Adds a new option.
*
* @param array $form
* The form array to add elements to.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current state of the form.
*/
public static function addMoreSubmit(array $form, FormStateInterface $form_state) {
$form_state->set('items_count', $form_state->get('items_count') + 1);
$form_state->setRebuild();
}
/**
* Ajax callback for the "Add another item" button.
*/
public static function addMoreAjax(array $form, FormStateInterface $form_state) {
$button = $form_state->getTriggeringElement();
// Go one level up in the form.
$element = NestedArray::getValue($form, array_slice($button['#array_parents'], 0, -1));
$delta = $element['table']['#max_delta'];
$element['table'][$delta]['item']['#prefix'] = '<div class="ajax-new-content">' . ($element['table'][$delta]['item']['#prefix'] ?? '');
$element['table'][$delta]['item']['#suffix'] = ($element['table'][$delta]['item']['#suffix'] ?? '') . '</div>';
return $element;
}
/**
* Deletes a row/option.
*
* @param array $form
* The form array to add elements to.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current state of the form.
*/
public static function deleteSubmit(array $form, FormStateInterface $form_state) {
$allowed_values = $form_state->getStorage()['allowed_values'];
$button = $form_state->getTriggeringElement();
$element = NestedArray::getValue($form, array_slice($button['#array_parents'], 0, -1));
$item_to_be_removed = $element['item']['label']['#default_value'];
$remaining_allowed_values = array_diff($allowed_values, [$item_to_be_removed]);
$form_state->set('allowed_values', $remaining_allowed_values);
$delta = $button['#delta'];
$user_input = $form_state->getUserInput();
// The user input is directly modified to preserve the rest of the data on
// the page as it cannot be rebuilt from a fresh form state.
unset($user_input['settings']['allowed_values']['table'][$delta]);
$user_input['settings']['allowed_values']['table'] = array_values($user_input['settings']['allowed_values']['table']);
$form_state->setUserInput($user_input);
$form_state->set('items_count', $form_state->get('items_count') - 1);
$form_state->setRebuild();
}
/**
* Ajax callback for per row delete button.
*/
public static function deleteAjax(array $form, FormStateInterface $form_state) {
$button = $form_state->getTriggeringElement();
return NestedArray::getValue($form, array_slice($button['#array_parents'], 0, -3));
}
/**
* Provides the field type specific allowed values form element #description.
*
......@@ -134,7 +309,30 @@ abstract protected function allowedValuesDescription();
* @see \Drupal\Core\Render\Element\FormElement::processPattern()
*/
public static function validateAllowedValues($element, FormStateInterface $form_state) {
$values = static::extractAllowedValues($element['#value'], $element['#field_has_data']);
$items = array_filter(array_map(function ($item) use ($element) {
$current_element = $element['table'][$item];
if ($current_element['item']['key']['#value'] !== NULL && $current_element['item']['label']['#value']) {
return $current_element['item']['key']['#value'] . '|' . $current_element['item']['label']['#value'];
}
elseif ($current_element['item']['key']['#value']) {
return $current_element['item']['key']['#value'];
}
elseif ($current_element['item']['label']['#value']) {
return $current_element['item']['label']['#value'];
}
return NULL;
}, Element::children($element['table'])), function ($item) {
return $item;
});
if ($reordered_items = $form_state->getValue(['settings', 'allowed_values', 'table'])) {
uksort($items, function ($a, $b) use ($reordered_items) {
$a_weight = $reordered_items[$a]['weight'] ?? 0;
$b_weight = $reordered_items[$b]['weight'] ?? 0;
return $a_weight <=> $b_weight;
});
}
$values = static::extractAllowedValues($items, $element['#field_has_data']);
if (!is_array($values)) {
$form_state->setError($element, new TranslatableMarkup('Allowed values list: invalid input.'));
......@@ -148,14 +346,6 @@ public static function validateAllowedValues($element, FormStateInterface $form_
}
}
// Prevent removing values currently in use.
if ($element['#field_has_data']) {
$lost_keys = array_keys(array_diff_key($element['#allowed_values'], $values));
if (_options_values_in_use($element['#entity_type'], $element['#field_name'], $lost_keys)) {
$form_state->setError($element, new TranslatableMarkup('Allowed values list: some values are being removed while currently in use.'));
}
}
$form_state->setValueForElement($element, $values);
}
}
......@@ -163,8 +353,8 @@ public static function validateAllowedValues($element, FormStateInterface $form_
/**
* Extracts the allowed values array from the allowed_values element.
*
* @param string $string
* The raw string to extract values from.
* @param string|array $list
* The raw string or array to extract values from.
* @param bool $has_data
* The current field already has data inserted or not.
*
......@@ -173,12 +363,15 @@ public static function validateAllowedValues($element, FormStateInterface $form_
*
* @see \Drupal\options\Plugin\Field\FieldType\ListItemBase::allowedValuesString()
*/
protected static function extractAllowedValues($string, $has_data) {
protected static function extractAllowedValues($list, $has_data) {
$values = [];
$list = explode("\n", $string);
$list = array_map('trim', $list);
$list = array_filter($list, 'strlen');
if (is_string($list)) {
trigger_error('Passing a string to ' . __METHOD__ . '() is deprecated in drupal:10.2.0 and will be removed from drupal:11.0.0. Please use an array instead.', E_USER_DEPRECATED);
$list = explode("\n", $list);
$list = array_map('trim', $list);
$list = array_filter($list, 'strlen');
}
$generated_keys = $explicit_keys = FALSE;
foreach ($list as $position => $text) {
......
......@@ -4,6 +4,8 @@
use Drupal\Core\Field\FieldFilteredMarkup;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Render\Element;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\Core\TypedData\DataDefinition;
......@@ -54,9 +56,8 @@ public static function schema(FieldStorageDefinitionInterface $field_definition)
* {@inheritdoc}
*/
protected function allowedValuesDescription() {
$description = '<p>' . $this->t('The possible values this field can contain. Enter one value per line, in the format key|label.');
$description .= '<br/>' . $this->t('The key is the stored value. The label will be used in displayed values and edit forms.');
$description .= '<br/>' . $this->t('The label is optional: if a line contains a single string, it will be used as key and label.');
$description = '<p>' . $this->t('The name will be used in displayed options and edit forms.');
$description .= '<br/>' . $this->t('The value is automatically generated machine name of the name provided and will be the stored value.');
$description .= '</p>';
$description .= '<p>' . $this->t('Allowed HTML tags in labels: @tags', ['@tags' => FieldFilteredMarkup::displayAllowedTags()]) . '</p>';
return $description;
......@@ -78,4 +79,46 @@ protected static function castAllowedValue($value) {
return (string) $value;
}
/**
* {@inheritdoc}
*/
public function storageSettingsForm(array &$form, FormStateInterface $form_state, $has_data) {
$element = parent::storageSettingsForm($form, $form_state, $has_data);
// Improve user experience by using an automatically generated machine name.
foreach (Element::children($element['allowed_values']['table']) as $delta => $row) {
$element['allowed_values']['table'][$delta]['item']['key']['#type'] = 'machine_name';
$element['allowed_values']['table'][$delta]['item']['key']['#machine_name'] = [
'exists' => [static::class, 'exists'],
];
$element['allowed_values']['table'][$delta]['item']['key']['#process'] = array_merge(
[[static::class, 'processAllowedValuesKey']],
// Workaround for https://drupal.org/i/1300290#comment-12873635.
\Drupal::service('plugin.manager.element_info')->getInfoProperty('machine_name', '#process', []),
);
}
return $element;
}
/**
* Sets the machine name source to be the label.
*/
public static function processAllowedValuesKey(array &$element): array {
$parents = $element['#parents'];
array_pop($parents);
$parents[] = 'label';
$element['#machine_name']['source'] = $parents;
return $element;
}
/**
* Checks for existing keys for allowed values.
*/
public static function exists(): bool {
// Without access to the current form state, we cannot know if a given key
// is in use. Return FALSE in all cases.
return FALSE;
}
}
......@@ -71,7 +71,12 @@ public function testImport() {
$this->copyConfig($this->container->get('config.storage'), $this->container->get('config.storage.sync'));
// Set the active to not use dots in the allowed values key names.
$edit = ['settings[allowed_values]' => "0|Zero\n1|One"];
$edit = [
'settings[allowed_values][table][0][item][key]' => 0,
'settings[allowed_values][table][0][item][label]' => 'Zero',
'settings[allowed_values][table][1][item][key]' => 1,
'settings[allowed_values][table][1][item][label]' => 'One',
];
$this->drupalGet($admin_path);
$this->submitForm($edit, 'Save field settings');
$field_storage = FieldStorageConfig::loadByName('node', $field_name);
......
<?php
namespace Drupal\Tests\options\FunctionalJavascript;
use Drupal\field\Entity\FieldConfig;
use Drupal\field\Entity\FieldStorageConfig;
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
/**
* Tests the Options field UI functionality.
*
* @group options
*/
class OptionsFieldUITest extends WebDriverTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = [
'node',
'options',
'field_ui',
];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* Machine name of the created content type.
*
* @var string
*/
protected $type;
/**
* Name of the option field.
*
* @var string
*/
protected $fieldName;
/**
* Admin path to manage field storage settings.
*
* @var string
*/
protected $adminPath;
/**
* Node form path for created content type.
*
* @var string
*/
protected $nodeFormPath;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
// Create test user.
$admin_user = $this->drupalCreateUser([
'bypass node access',
'administer node fields',
'administer node display',
]);
$this->drupalLogin($admin_user);
$type = $this->drupalCreateContentType(['type' => 'plan']);
$this->type = $type->id();
$this->nodeFormPath = 'node/add/' . $this->type;
}
/**
* Tests option types allowed values.
*
* @dataProvider providerTestOptionsAllowedValues
*/
public function testOptionsAllowedValues($option_type, $options, $is_string_option) {
$this->fieldName = 'field_options_text';
$this->createOptionsField($option_type);
$page = $this->getSession()->getPage();
$this->drupalGet($this->adminPath);
$i = 0;
foreach ($options as $option_key => $option_label) {
$page->fillField("settings[allowed_values][table][$i][item][label]", $option_label);
// Add keys if not string option list.
if (!$is_string_option) {
$page->fillField("settings[allowed_values][table][$i][item][key]", $option_key);
}
$page->pressButton('Add another item');
$i++;
$this->assertSession()->waitForElementVisible('css', "[name='settings[allowed_values][table][$i][item][label]']");
}
$page->pressButton('Save field settings');
// Test the order of the option list on node form.
$this->drupalGet($this->nodeFormPath);
$this->assertNodeFormOrder(['- None -', 'First', 'Second', 'Third']);
// Test the order of the option list on admin path.
$this->drupalGet($this->adminPath);
$this->assertOrder(['First', 'Second', 'Third', ''], $is_string_option);
$drag_handle = $page->find('css', '[data-drupal-selector="edit-settings-allowed-values-table-0"] .tabledrag-handle');
$target = $page->find('css', '[data-drupal-selector="edit-settings-allowed-values-table-2"]');
// Change the order the items appear.
$drag_handle->dragTo($target);
$this->assertOrder(['Second', 'Third', 'First', ''], $is_string_option);
$page->pressButton('Save field settings');
$this->drupalGet($this->nodeFormPath);
$this->assertNodeFormOrder(['- None -', 'Second', 'Third', 'First']);
$this->drupalGet($this->adminPath);
// Confirm the change in order was saved.
$this->assertOrder(['Second', 'Third', 'First', ''], $is_string_option);
// Delete an item.
$page->pressButton('remove_row_button__1');
$this->assertSession()->assertWaitOnAjaxRequest();
$this->assertOrder(['Second', 'First', ''], $is_string_option);
$page->pressButton('Save field settings');
$this->drupalGet($this->nodeFormPath);
$this->assertNodeFormOrder(['- None -', 'Second', 'First']);
$this->drupalGet($this->adminPath);
// Confirm the item removal was saved.
$this->assertOrder(['Second', 'First', ''], $is_string_option);
}
/**
* Asserts the order of provided option list on admin path.
*
* @param array $expected
* Expected order.
* @param bool $is_string_option
* Whether the request is for string option list.
*/
protected function assertOrder($expected, $is_string_option) {
$page = $this->getSession()->getPage();
if ($is_string_option) {
$inputs = $page->findAll('css', '.draggable .form-text.machine-name-source');
}
else {
$inputs = $page->findAll('css', '.draggable .form-text');
}
foreach ($expected as $step => $expected_input_value) {
$value = $inputs[$step]->getValue();
$this->assertSame($expected_input_value, $value, "Item $step should be $expected_input_value, but got $value");
}
}
/**
* Asserts the order of provided option list on node form.
*
* @param array $expected
* Expected order.
*/
protected function assertNodeFormOrder($expected) {
$elements = $this->assertSession()->selectExists('field_options_text')->findAll('css', 'option');
$elements = array_map(function ($element) {
return $element->getText();
}, $elements);
$this->assertSame($expected, $elements);
}
/**
* Helper function to create list field of a given type.
*
* @param string $type
* One of 'list_integer', 'list_float' or 'list_string'.
*/
protected function createOptionsField($type) {
// Create a field.
FieldStorageConfig::create([
'field_name' => $this->fieldName,
'entity_type' => 'node',
'type' => $type,
])->save();
FieldConfig::create([
'field_name' => $this->fieldName,
'entity_type' => 'node',
'bundle' => $this->type,
])->save();
\Drupal::service('entity_display.repository')
->getFormDisplay('node', $this->type)
->setComponent($this->fieldName)
->save();
$this->adminPath = 'admin/structure/types/manage/' . $this->type . '/fields/node.' . $this->type . '.' . $this->fieldName . '/storage';
}
/**
* Data provider for testOptionsAllowedValues().
*
* @return array
* Array of arrays with the following elements:
* - Option type.
* - Array of option type values.
* - Whether option type is string type or not.
*/
public function providerTestOptionsAllowedValues() {
return [
'List integer' => [
'list_integer',
[1 => 'First', 2 => 'Second', 3 => 'Third'],
FALSE,
],
'List float' => [
'list_float',
['0.1' => 'First', '0.2' => 'Second', '0.3' => 'Third'],
FALSE,
],
'List string' => [
'list_string',
['first' => 'First', 'second' => 'Second', 'third' => 'Third'],
TRUE,
],
];
}
}
......@@ -94,3 +94,7 @@
.field-settings-summary-cell li:first-child {
font-size: 1em;
}
.allowed-values-table .form-item:where(:not(.hidden)) {
display: inline-table;
}
......@@ -75,3 +75,7 @@
.field-settings-summary-cell li:first-child {
font-size: 1em;
}
.allowed-values-table .form-item:where(:not(.hidden)) {
display: inline-table;
}
......@@ -22,6 +22,10 @@
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;
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment