Skip to content
Snippets Groups Projects
Commit 437fb109 authored by Bryan Heisler's avatar Bryan Heisler Committed by Adam Nagy
Browse files

Issue #3418165: Fix bug after Support empty option for Link type select.

parent c667094d
No related branches found
No related tags found
1 merge request!3Issue #3418165: Fix bug after Support empty option for Link type select.
Pipeline #148390 passed with warnings
################
# DrupalCI GitLabCI template
#
# Gitlab-ci.yml to replicate DrupalCI testing for Contrib
#
# With thanks to:
# * The GitLab Acceleration Initiative participants
# * DrupalSpoons
################
################
# Guidelines
#
# This template is designed to give any Contrib maintainer everything they need to test, without requiring modification. It is also designed to keep up to date with Core Development automatically through the use of include files that can be centrally maintained.
#
# However, you can modify this template if you have additional needs for your project.
################
################
# Includes
#
# Additional configuration can be provided through includes.
# One advantage of include files is that if they are updated upstream, the changes affect all pipelines using that include.
#
# Includes can be overridden by re-declaring anything provided in an include, here in gitlab-ci.yml
# https://docs.gitlab.com/ee/ci/yaml/includes.html#override-included-configuration-values
################
include:
################
# DrupalCI includes:
# As long as you include this, any future includes added by the Drupal Association will be accessible to your pipelines automatically.
# View these include files at https://git.drupalcode.org/project/gitlab_templates/
################
- project: $_GITLAB_TEMPLATES_REPO
ref: $_GITLAB_TEMPLATES_REF
file:
- '/includes/include.drupalci.main.yml'
# EXPERIMENTAL: For Drupal 7, remove the above line and uncomment the below.
# - '/includes/include.drupalci.main-d7.yml'
- '/includes/include.drupalci.variables.yml'
- '/includes/include.drupalci.workflows.yml'
################
# Pipeline configuration variables
#
# These are the variables provided to the Run Pipeline form that a user may want to override.
#
# Docs at https://git.drupalcode.org/project/gitlab_templates/-/blob/1.0.x/includes/include.drupalci.variables.yml
################
# variables:
# SKIP_ESLINT: '1'
###################################################################################
#
# *
# /(
# ((((,
# /(((((((
# ((((((((((*
# ,(((((((((((((((
# ,(((((((((((((((((((
# ((((((((((((((((((((((((*
# *(((((((((((((((((((((((((((((
# ((((((((((((((((((((((((((((((((((*
# *(((((((((((((((((( .((((((((((((((((((
# ((((((((((((((((((. /(((((((((((((((((*
# /((((((((((((((((( .(((((((((((((((((,
# ,(((((((((((((((((( ((((((((((((((((((
# .(((((((((((((((((((( .(((((((((((((((((
# ((((((((((((((((((((((( ((((((((((((((((/
# (((((((((((((((((((((((((((/ ,(((((((((((((((*
# .((((((((((((((/ /(((((((((((((. ,(((((((((((((((
# *(((((((((((((( ,(((((((((((((/ *((((((((((((((.
# ((((((((((((((, /(((((((((((((. ((((((((((((((,
# (((((((((((((/ ,(((((((((((((* ,(((((((((((((,
# *((((((((((((( .((((((((((((((( ,(((((((((((((
# ((((((((((((/ /((((((((((((((((((. ,((((((((((((/
# ((((((((((((( *(((((((((((((((((((((((* *((((((((((((
# ((((((((((((( ,(((((((((((((..((((((((((((( *((((((((((((
# ((((((((((((, /((((((((((((* /((((((((((((/ ((((((((((((
# ((((((((((((( /((((((((((((/ (((((((((((((* ((((((((((((
# (((((((((((((/ /(((((((((((( ,((((((((((((, *((((((((((((
# (((((((((((((( *(((((((((((/ *((((((((((((. ((((((((((((/
# *((((((((((((((((((((((((((, /(((((((((((((((((((((((((
# ((((((((((((((((((((((((( ((((((((((((((((((((((((,
# .(((((((((((((((((((((((/ ,(((((((((((((((((((((((
# ((((((((((((((((((((((/ ,(((((((((((((((((((((/
# *((((((((((((((((((((( (((((((((((((((((((((,
# ,(((((((((((((((((((((, ((((((((((((((((((((/
# ,(((((((((((((((((((((* /((((((((((((((((((((
# ((((((((((((((((((((((, ,/((((((((((((((((((((,
# ,(((((((((((((((((((((((((((((((((((((((((((((((((((
# .(((((((((((((((((((((((((((((((((((((((((((((
# .((((((((((((((((((((((((((((((((((((,.
# .,(((((((((((((((((((((((((.
#
###################################################################################
......@@ -24,7 +24,7 @@ class TypedLinkFormatter extends LinkFormatter {
/**
* {@inheritdoc}
*/
public function viewElements(FieldItemListInterface $items, $langcode): array {
public function viewElements(FieldItemListInterface $items, $langcode) {
$elements = parent::viewElements($items, $langcode);
// Only collect allowed options if there are actually items to display.
......
......@@ -23,7 +23,12 @@ use Drupal\options\Plugin\Field\FieldType\ListStringItem;
* description = @Translation("Contains a link and a category so the category can be used for theming purposes."),
* default_widget = "typed_link",
* default_formatter = "typed_link",
* constraints = {"LinkType" = {}, "LinkAccess" = {}, "LinkExternalProtocols" = {}, "LinkNotExistingInternal" = {}}
* constraints = {
* "LinkType" = {},
* "LinkAccess" = {},
* "LinkExternalProtocols" = {},
* "LinkNotExistingInternal" = {},
* }
* )
*/
class TypedLinkItem extends LinkItem implements OptionsProviderInterface {
......@@ -51,7 +56,7 @@ class TypedLinkItem extends LinkItem implements OptionsProviderInterface {
$properties['link_type'] = DataDefinition::create('string')
->setLabel(t('Link Type'))
->addConstraint('Length', ['max' => 255])
->setRequired(FALSE);
->setRequired(TRUE);
return $properties;
}
......
......@@ -4,12 +4,15 @@ namespace Drupal\typed_link\Plugin\Field\FieldWidget;
use Drupal\Component\Utility\Html;
use Drupal\Core\Entity\FieldableEntityInterface;
use Drupal\Core\Extension\ModuleHandler;
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\Field\FieldItemInterface;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Form\OptGroup;
use Drupal\Core\Session\AccountProxyInterface;
use Drupal\link\Plugin\Field\FieldWidget\LinkWidget;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Defines the 'typed_link' field widget.
......@@ -28,16 +31,59 @@ class TypedLinkWidget extends LinkWidget {
*
* @var array
*/
private array $options;
protected array $options;
/**
* Get the column name of the field.
* The current user.
*
* @return string
* The column name.
* @var \Drupal\Core\Session\AccountProxyInterface
*/
private function getColumn(): string {
return $this->fieldDefinition->getFieldStorageDefinition()->getMainPropertyName();
protected $currentUser;
/**
* The module handler.
*
* @var \Drupal\Core\Extension\ModuleHandler
*/
protected $moduleHandler;
/**
* Constructs a new Typed link field widget.
*
* @param string $plugin_id
* The plugin_id for the formatter.
* @param mixed $plugin_definition
* The plugin implementation definition.
* @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition
* The definition of the field to which the formatter is associated.
* @param array $settings
* The widget settings.
* @param array $third_party_settings
* The widget third party settings.
* @param \Drupal\Core\Session\AccountProxyInterface $currentUser
* The current user.
* @param \Drupal\Core\Extension\ModuleHandler $moduleHandler
* The module handler.
*/
public function __construct($plugin_id, $plugin_definition, FieldDefinitionInterface $field_definition, array $settings, array $third_party_settings, AccountProxyInterface $currentUser, ModuleHandler $moduleHandler) {
parent::__construct($plugin_id, $plugin_definition, $field_definition, $settings, $third_party_settings);
$this->currentUser = $currentUser;
$this->moduleHandler = $moduleHandler;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$plugin_id,
$plugin_definition,
$configuration['field_definition'],
$configuration['settings'],
$configuration['third_party_settings'],
$container->get('current_user'),
$container->get('module_handler')
);
}
/**
......@@ -54,18 +100,33 @@ class TypedLinkWidget extends LinkWidget {
'#empty_option' => $this->t('- Select -'),
'#multiple' => FALSE,
'#required' => $element['#required'],
'#states' => [
'required' => [
':input[name="' . $items->getName() . '[' . $delta . '][uri]' . '"]' => [
'filled' => TRUE,
],
],
],
];
// Make the type required on the front-end when URI filled-in.
$parents = $element['#field_parents'];
$parents[] = $this->fieldDefinition->getName();
$selector = $root = array_shift($parents);
if ($parents) {
$selector = $root . '[' . implode('][', $parents) . ']';
}
$element['link_type']['#states']['required'] = [
':input[name="' . $selector . '[' . $delta . '][uri]"]' => ['filled' => TRUE],
];
return $element;
}
/**
* Get the column name of the field.
*
* @return string
* The column name.
*/
protected function getColumn(): string {
return $this->fieldDefinition->getFieldStorageDefinition()->getMainPropertyName();
}
/**
* Returns the array of options for the widget.
*
......@@ -75,20 +136,19 @@ class TypedLinkWidget extends LinkWidget {
* @return array
* The array of options for the widget.
*/
protected function getOptions(FieldableEntityInterface $entity) {
protected function getOptions(FieldableEntityInterface $entity): array {
if (!isset($this->options)) {
// Limit the settable options for the current user account.
$options = $this->fieldDefinition
->getFieldStorageDefinition()
->getOptionsProvider($this->getColumn(), $entity)
->getSettableOptions(\Drupal::currentUser());
->getSettableOptions($this->currentUser);
$module_handler = \Drupal::moduleHandler();
$context = [
'fieldDefinition' => $this->fieldDefinition,
'entity' => $entity,
];
$module_handler->alter('options_list', $options, $context);
$this->moduleHandler->alter('options_list', $options, $context);
array_walk_recursive($options, [$this, 'sanitizeLabel']);
......@@ -105,10 +165,10 @@ class TypedLinkWidget extends LinkWidget {
* @param \Drupal\Core\Field\FieldItemInterface $item
* The field values.
*
* @return string
* The selected option.
* @return string|null
* The selected option or null if there is none.
*/
protected function getSelectedOption(FieldItemInterface $item) {
protected function getSelectedOption(FieldItemInterface $item): ?string {
// We need to check against a flat list of options.
$flat_options = OptGroup::flattenOptions($this->getOptions($item->getEntity()));
......@@ -129,7 +189,7 @@ class TypedLinkWidget extends LinkWidget {
* @param string $label
* The label.
*/
protected function sanitizeLabel(&$label) {
protected function sanitizeLabel(string &$label): void {
// Select form inputs allow unencoded HTML entities, but no HTML tags.
$label = Html::decodeEntities(strip_tags($label));
}
......
<?php
declare(strict_types=1);
namespace Drupal\Tests\typed_link\FunctionalJavascript;
use Behat\Mink\Element\NodeElement;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
use Drupal\field\Entity\FieldConfig;
use Drupal\field\Entity\FieldStorageConfig;
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
use Drupal\Tests\node\Traits\NodeCreationTrait;
/**
* Tests the Typed link field widget.
*/
class TypedLinkFieldWidgetTest extends WebDriverTestBase {
use NodeCreationTrait;
/**
* {@inheritdoc}
*/
protected static $modules = [
'field',
'node',
'user',
'text',
'link',
'options',
'typed_link',
];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->drupalCreateContentType(['type' => 'page', 'name' => 'Page']);
FieldStorageConfig::create([
'field_name' => 'typed_link_field',
'entity_type' => 'node',
'type' => 'typed_link',
'cardinality' => FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED,
'settings' => [
'allowed_values' => [
'type1' => 'Type 1',
'type2' => 'Type 2',
],
],
])->save();
FieldConfig::create([
'field_name' => 'typed_link_field',
'entity_type' => 'node',
'bundle' => 'page',
'label' => 'Typed link',
'required' => TRUE,
'settings' => [],
])->save();
$form_display = $this->container->get('entity_type.manager')
->getStorage('entity_form_display')
->load('node.page.default');
$form_display->setComponent('typed_link_field', [
'weight' => 1,
'region' => 'content',
'type' => 'typed_link',
'settings' => [
'placeholder_url' => '',
'placeholder_title' => '',
],
'third_party_settings' => [],
])->save();
}
/**
* Tests the Typed link field widget.
*/
public function testTypedLinkFieldWidget(): void {
$user = $this->drupalCreateUser([], NULL, TRUE);
$this->drupalLogin($user);
$this->drupalGet('/node/add/page');
$this->getSession()->getPage()->fillField('title[0][value]', 'My page');
$this->getSession()->getPage()->fillField('URL', 'http://example.com');
// Disable the browser required field validation to have form validation.
$this->getSession()->evaluateScript("typeof jQuery === 'undefined' || jQuery(':input[required]').prop('required', false);");
$this->getSession()->getPage()->pressButton('Save');
// We require selecting a link type by Drupal states.
$this->assertSession()->pageTextContainsOnce('Link type field is required.');
$this->assertFieldSelectOptions('Link type', [
'type1',
'type2',
]);
$this->getSession()->getPage()->selectFieldOption('Link type', 'Type 1');
$this->getSession()->getPage()->pressButton('Save');
$this->assertSession()->pageTextContainsOnce('Page My page has been created.');
$node = $this->getNodeByTitle('My page');
$this->drupalGet($node->toUrl('edit-form'));
$this->assertSession()->fieldValueEquals('URL', 'http://example.com');
$this->assertSession()->fieldValueEquals('Link text', '');
$this->assertTrue($this->assertSession()->optionExists('Link type', 'Type 1')->isSelected());
$this->getSession()->getPage()->fillField('Link text', 'Example link');
$this->getSession()->getPage()->pressButton('Save');
$this->assertSession()->pageTextContainsOnce('Page My page has been updated.');
$this->drupalGet($node->toUrl('edit-form'));
$this->assertSession()->fieldValueEquals('URL', 'http://example.com');
$this->assertSession()->fieldValueEquals('Link text', 'Example link');
$this->assertTrue($this->assertSession()->optionExists('Link type', 'Type 1')->isSelected());
}
/**
* Checks if a select element contains the specified options.
*
* @param string $name
* The field name.
* @param array $expected_options
* An array of expected options.
*/
protected function assertFieldSelectOptions(string $name, array $expected_options): void {
$select = $this->getSession()->getPage()->find('named', [
'select',
$name,
]);
if (!$select) {
$this->fail('Unable to find select ' . $name);
}
$options = $select->findAll('css', 'option');
array_walk($options, function (NodeElement &$option) {
$option = $option->getValue();
});
$options = array_filter($options);
sort($options);
sort($expected_options);
$this->assertSame($expected_options, $options);
}
}
<?php
declare(strict_types=1);
namespace Drupal\Tests\typed_link\Kernel;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
use Drupal\field\Entity\FieldConfig;
use Drupal\field\Entity\FieldStorageConfig;
use Drupal\KernelTests\KernelTestBase;
/**
* Tests the Typed link field type and formatter.
*/
class TypedLinkFieldTest extends KernelTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = [
'system',
'node',
'field',
'user',
'text',
'options',
'typed_link',
];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->installConfig([
'system',
'node',
'field',
'user',
]);
$this->installEntitySchema('node');
$this->installEntitySchema('user');
$this->container->get('entity_type.manager')->getStorage('node_type')->create([
'type' => 'page',
'name' => 'Page',
])->save();
FieldStorageConfig::create([
'field_name' => 'typed_link',
'entity_type' => 'node',
'type' => 'typed_link',
'cardinality' => FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED,
'settings' => [
'allowed_values' => [
'type1' => 'Type 1',
'type2' => 'Type 2',
],
],
])->save();
FieldConfig::create([
'entity_type' => 'node',
'field_name' => 'typed_link',
'bundle' => 'page',
])->save();
}
/**
* Tests the field type and formatter.
*/
public function testTypedLinkField(): void {
/** @var \Drupal\node\NodeStorageInterface $node_storage */
$node_storage = $this->container->get('entity_type.manager')->getStorage('node');
$values = [
[
'title' => 'Front page',
'uri' => 'http://drupal.org',
'link_type' => 'type1',
'options' => [],
],
[
'title' => 'Example link',
'uri' => 'http://example.com',
'link_type' => 'type2',
'options' => [],
],
];
/** @var \Drupal\node\NodeInterface $node */
$node = $node_storage->create([
'type' => 'page',
'title' => 'Test page',
'typed_link' => $values,
]);
// Assert values are saved in the field.
$this->assertEquals($values, $node->get('typed_link')->getValue());
// Assert formatter rendering.
$builder = $this->container->get('entity_type.manager')->getViewBuilder('node');
$build = $builder->viewField($node->get('typed_link'));
$output = $this->container->get('renderer')->renderRoot($build);
$this->assertStringContainsString('<div>typed_link</div>', (string) $output);
$this->assertStringContainsString('<div><a href="http://drupal.org">Front page</a>Type 1</div>', (string) $output);
$this->assertStringContainsString('<div><a href="http://example.com">Example link</a>Type 2</div>', (string) $output);
}
}
......@@ -4,4 +4,6 @@ description: A field that takes a link and a type. Used to assign a category to
package: Field types
core_version_requirement: ^8 || ^9 || ^10
dependencies:
- drupal:field
- field:field
- link:link
- options:options
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment