Skip to content
Snippets Groups Projects
Commit 7aa5c515 authored by Pieter Frenssen's avatar Pieter Frenssen
Browse files

Issue #3438973 by pfrenssen, ayalon, dulnan: Port WebformElementValidationMultiple to GraphQL 4

parent 958c4c95
Branches
Tags
2 merge requests!513506037: Add help elements.,!29Support elements that can accept multiple values.
Pipeline #140801 passed with warnings
Showing
with 508 additions and 142 deletions
<?php
declare(strict_types=1);
namespace Drupal\graphql_webform\Plugin\GraphQL\DataProducer;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Render\RendererInterface;
use Drupal\graphql\Plugin\GraphQL\DataProducer\DataProducerPluginBase;
use Drupal\webform\Plugin\WebformElementInterface;
use Drupal\webform\Plugin\WebformElementManagerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Returns details regarding multiple value support for a Webform element.
*
* @DataProducer(
* id = "webform_element_multiple_values",
* name = @Translation("Webform element multiple values"),
* description = @Translation("Returns information about multiple value support for a Webform element."),
* produces = @ContextDefinition("any",
* label = @Translation("Any value"),
* required = FALSE
* ),
* consumes = {
* "element" = @ContextDefinition(
* "any",
* label = @Translation("Webform element")
* )
* }
* )
*/
class WebformElementMultipleValues extends DataProducerPluginBase implements ContainerFactoryPluginInterface {
/**
* Maps Webform element property names to GraphQL field names.
*/
protected const PROPERTY_MAPPING = [
'multiple_error' => 'message',
'multiple__header_label' => 'headerLabel',
'multiple__min_items' => 'minItems',
'multiple__empty_items' => 'emptyItems',
'multiple__add_more' => 'addMore',
'multiple__add_more_items' => 'addMoreItems',
'multiple__add_more_button_label' => 'addMoreButtonLabel',
'multiple__add_more_input' => 'addMoreInput',
'multiple__add_more_input_label' => 'addMoreInputLabel',
'multiple__item_label' => 'itemLabel',
'multiple__sorting' => 'sorting',
'multiple__operations' => 'operations',
'multiple__add' => 'add',
'multiple__remove' => 'remove',
];
/**
* Constructs a new WebformElementMultipleValues instance.
*
* @param array $configuration
* The plugin configuration.
* @param string $pluginId
* The plugin ID.
* @param mixed $pluginDefinition
* The plugin definition.
* @param \Drupal\webform\Plugin\WebformElementManagerInterface $elementManager
* The Webform element plugin manager.
* @param \Drupal\Core\Render\RendererInterface $renderer
* The renderer.
*/
public function __construct(
array $configuration,
$pluginId,
$pluginDefinition,
protected WebformElementManagerInterface $elementManager,
protected RendererInterface $renderer,
) {
parent::__construct($configuration, $pluginId, $pluginDefinition);
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('plugin.manager.webform.element'),
$container->get('renderer'),
);
}
/**
* Resolves multiple values information for a Webform element.
*
* @param array $element
* The Webform element for which to return the information.
*
* @return array|null
* The information about multiple values support, or NULL if the element
* does not support multiple values.
*/
public function resolve(array $element): ?array {
$plugin = $this->elementManager->getElementInstance($element);
assert($plugin instanceof WebformElementInterface);
// Contrary to the documentation, this method returns either a boolean or an
// integer.
$limit = $plugin->hasMultipleValues($element);
if ($limit === FALSE) {
return NULL;
}
$resolved = [];
if ($limit) {
// Convert an unlimited limit to -1 so it adheres to the GraphQL schema.
$resolved['limit'] = $limit === TRUE ? -1 : $limit;
// The no items message can be a render array. Convert it to a string.
$resolved['noItemsMessage'] = $plugin->getElementProperty($element, 'multiple__no_items_message');
if (!empty($resolved['noItemsMessage'])) {
$resolved['noItemsMessage'] = (string) match (is_array($resolved['noItemsMessage'])) {
TRUE => $this->renderer->renderPlain($resolved['noItemsMessage']),
default => $resolved['noItemsMessage'],
};
}
foreach (static::PROPERTY_MAPPING as $property => $key) {
$resolved[$key] = $plugin->getElementProperty($element, $property);
}
}
return $resolved;
}
}
......@@ -4,16 +4,19 @@ declare(strict_types=1);
namespace Drupal\graphql_webform\Plugin\GraphQL\DataProducer;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\graphql\Plugin\GraphQL\DataProducer\DataProducerPluginBase;
use Drupal\webform\Plugin\WebformElementManagerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\String\UnicodeString;
/**
* Returns a single property from a webform element.
* Returns a single property from a Webform element.
*
* @DataProducer(
* id = "webform_element_property",
* name = @Translation("Webform element property"),
* description = @Translation("Returns the property of a webform element."),
* description = @Translation("Returns the property of a Webform element."),
* produces = @ContextDefinition("any",
* label = @Translation("Any value"),
* required = FALSE
......@@ -36,13 +39,46 @@ use Symfony\Component\String\UnicodeString;
* }
* )
*/
class WebformElementProperty extends DataProducerPluginBase {
class WebformElementProperty extends DataProducerPluginBase implements ContainerFactoryPluginInterface {
/**
* Resolves a property from a webform element.
* Constructs a new WebformElementProperty instance.
*
* @param array $configuration
* The plugin configuration.
* @param string $pluginId
* The plugin ID.
* @param mixed $pluginDefinition
* The plugin definition.
* @param \Drupal\webform\Plugin\WebformElementManagerInterface $elementManager
* The Webform element plugin manager.
*/
public function __construct(
array $configuration,
$pluginId,
$pluginDefinition,
protected WebformElementManagerInterface $elementManager,
) {
parent::__construct($configuration, $pluginId, $pluginDefinition);
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('plugin.manager.webform.element'),
);
}
/**
* Resolves a property from a Webform element.
*
* @param array $element
* The webform element from which to extract the property.
* The Webform element from which to extract the property.
* @param string $property
* The property to extract.
* @param string $type
......@@ -52,14 +88,29 @@ class WebformElementProperty extends DataProducerPluginBase {
* The value of the property.
*/
public function resolve(array $element, string $property, string $type): mixed {
// Derive the element ID from the property name, by converting it from
// PascalCase to snake_case.
$element_id = (new UnicodeString($property))->snake()->toString();
// Derive the property name from the GraphQL field name, by converting it
// from PascalCase to snake_case.
$property = (new UnicodeString($property))->snake()->toString();
// Try to get the value from the element directly. If it's not there, fall
// back to the "decoded" values. This is the case for the "#description"
// field, which Webform removes from the element.
$value = $element['#' . $element_id] ?? $element['#graphql_element_decoded']['#' . $element_id] ?? NULL;
// Try to get the value from the element directly.
$plugin = $this->elementManager->getElementInstance($element);
$value = $plugin->getElementProperty($element, $property);
// If we didn't find the value and this is an element with multiple values,
// we are looking inside the wrapper. Try getting the value from the first
// actual element instead.
if ($value === NULL && $element['#type'] === 'webform_multiple' && !empty($element['items'][0]['_item_'])) {
$value = $plugin->getElementProperty($element['items'][0]['_item_'], $property);
}
// If it's still not there, fall back to the "decoded" values. This is the
// case for the "#description" field, which Webform removes from the
// element.
// @todo Check if this is actually necessary.
// @see https://www.drupal.org/project/graphql_webform/issues/3439367
if ($value === NULL) {
$value = $element['#graphql_element_decoded']['#' . $property] ?? NULL;
}
if ($value === NULL) {
return NULL;
......
......@@ -9,12 +9,12 @@ use Drupal\graphql\Plugin\GraphQL\DataProducer\DataProducerPluginBase;
use Drupal\webform\WebformInterface;
/**
* Returns the webform elements.
* Returns the Webform elements.
*
* @DataProducer(
* id = "webform_elements",
* name = @Translation("Webform elements"),
* description = @Translation("Returns the webform elements."),
* description = @Translation("Returns the Webform elements."),
* produces = @ContextDefinition("any",
* label = @Translation("Webform elements"),
* required = FALSE
......@@ -34,7 +34,7 @@ use Drupal\webform\WebformInterface;
class WebformElements extends DataProducerPluginBase {
/**
* Resolves the webform elements.
* Resolves the Webform elements.
*
* @param \Drupal\webform\WebformInterface $webform
* The full webform.
......@@ -43,17 +43,17 @@ class WebformElements extends DataProducerPluginBase {
* resolved. If this is not passed, the top-level elements will be resolved.
*
* @return array
* An array of webform render array elements.
* An array of Webform render array elements.
*/
public function resolve(WebformInterface $webform, ?array $parent = NULL): array {
// Retrieve the form elements. First, try to get the elements from the
// parent element. If this is not passed, try to get the elements from the
// base webform.
// base Webform.
$form = $parent ?? $webform->getSubmissionForm() ?? [];
$original_elements = empty($form['elements']) ? $form : $form['elements'];
// Get a list of "decoded" elements. These contain the element properties
// and metadata as configured in the webform.
// and metadata as configured in the Webform.
$decoded_elements = $webform->getElementsDecodedAndFlattened('view');
// Loop over the original elements and store the "decoded" information in
......
<?php
declare(strict_types=1);
namespace Drupal\graphql_webform\Plugin\GraphQL\Fields\Element;
use Drupal\graphql\GraphQL\Execution\ResolveContext;
use Drupal\graphql\Plugin\GraphQL\Fields\FieldPluginBase;
use GraphQL\Type\Definition\ResolveInfo;
/**
* Retrieve the form element machine name.
*
* @GraphQLField(
* secure = true,
* parents = {
* "WebformElementTextBase",
* "WebformElementDateBase",
* "WebformElementOptionsBase",
* "WebformElementManagedFileBase",
* "WebformElementComposite"
* },
* id = "webform_element_multiple",
* name = "multiple",
* type = "WebformElementValidationMultiple",
* )
*/
class WebformElementMultiple extends FieldPluginBase {
/**
* {@inheritdoc}
*/
public function resolveValues($value, array $args, ResolveContext $context, ResolveInfo $info) {
$multiple = $value['plugin']->hasMultipleValues($value);
if ($multiple) {
$response['limit'] = $multiple;
if (isset($value['#multiple_error'])) {
$response['message'] = $value['#multiple_error'];
}
$response['type'] = 'WebformElementValidationMultiple';
yield $response;
}
}
}
<?php
declare(strict_types=1);
namespace Drupal\graphql_webform\Plugin\GraphQL\Fields\Element;
use Drupal\graphql\GraphQL\Execution\ResolveContext;
use Drupal\graphql\Plugin\GraphQL\Fields\FieldPluginBase;
use GraphQL\Type\Definition\ResolveInfo;
/**
* Retrieve the rule of a form validation (e.g. regex pattern).
*
* @GraphQLField(
* secure = true,
* parents = {"WebformElementValidationMultiple"},
* id = "webform_element_validation_limit",
* name = "limit",
* type = "Int",
* )
*/
class WebformElementValidationLimit extends FieldPluginBase {
/**
* {@inheritdoc}
*/
public function resolveValues($value, array $args, ResolveContext $context, ResolveInfo $info) {
if (is_int($value['limit'])) {
yield $value['limit'];
}
else {
yield 0;
}
}
}
......@@ -215,6 +215,12 @@ class WebformExtension extends SdlSchemaExtensionPluginBase {
fn (array $value) => (WebformElementDisplayOn::tryFrom($value['#display_on'] ?? '') ?? WebformElementDisplayOn::DISPLAY_ON_FORM)->name
));
// Expose information about elements that accept multiple values.
$registry->addFieldResolver('WebformElementMultipleValuesBase', 'multiple', $builder
->produce('webform_element_multiple_values')
->map('element', $builder->fromParent())
);
// Resolve the options for checkboxes, radio buttons, etc.
$registry->addFieldResolver('WebformElementOptionsBase', 'options', $builder
->produce('webform_element_options')
......
<?php
declare(strict_types=1);
namespace Drupal\graphql_webform\Plugin\GraphQL\Types;
use Drupal\graphql\GraphQL\Execution\ResolveContext;
use Drupal\graphql\Plugin\GraphQL\Types\TypePluginBase;
use GraphQL\Type\Definition\ResolveInfo;
/**
* A GraphQL type for a value/title option of OptionsBase form item.
*
* @GraphQLType(
* id = "webform_element_validation_multiple",
* name = "WebformElementValidationMultiple",
* )
*/
class WebformElementValidationMultiple extends TypePluginBase {
/**
* {@inheritdoc}
*/
public function applies($object, ResolveContext $context, ResolveInfo $info) {
return $object['type'] == 'WebformElementValidationMultiple';
}
}
......@@ -71,6 +71,83 @@ class WebformSchemaBuilder {
'fields' => $this->getElementDefinition(),
]);
$this->types['WebformElementMultipleValues'] = new ObjectType([
'name' => 'WebformElementMultipleValues',
'fields' => [
'limit' => [
'type' => Type::int(),
'description' => 'The maximum number of values that can be entered. If set to -1, an unlimited number of values can be entered.',
],
'message' => [
'type' => Type::string(),
'description' => 'The error message to display when the maximum number of values is exceeded.',
],
'headerLabel' => [
'type' => Type::string(),
'description' => 'The label for the header of the multiple values fieldset.',
],
'minItems' => [
'type' => Type::int(),
'description' => 'The minimum number of items that must be entered.',
],
'emptyItems' => [
'type' => Type::int(),
'description' => 'The number of empty items to display.',
],
'addMore' => [
'type' => Type::boolean(),
'description' => 'Whether or not to display the "Add more" button.',
],
'addMoreItems' => [
'type' => Type::int(),
'description' => 'The number of items to add when the "Add more" button is clicked.',
],
'addMoreButtonLabel' => [
'type' => Type::string(),
'description' => 'The label for the "Add more" button.',
],
'addMoreInput' => [
'type' => Type::boolean(),
'description' => 'Allow users to input the number of items to be added.',
],
'addMoreInputLabel' => [
'type' => Type::string(),
'description' => 'The label for the input field that allows users to input the number of items to be added.',
],
'itemLabel' => [
'type' => Type::string(),
'description' => 'The label for each item.',
],
'noItemsMessage' => [
'type' => Type::string(),
'description' => 'The message to display when there are no items.',
],
'sorting' => [
'type' => Type::boolean(),
'description' => 'Allow users to sort elements.',
],
'operations' => [
'type' => Type::boolean(),
'description' => 'Allow users to add/remove elements.',
],
'add' => [
'type' => Type::boolean(),
'description' => 'Whether to show the "Add" button.',
],
'remove' => [
'type' => Type::boolean(),
'description' => 'Whether to show the "Remove" button.',
],
],
]);
$this->types['WebformElementMultipleValuesBase'] = new InterfaceType([
'name' => 'WebformElementMultipleValuesBase',
'fields' => [
'multiple' => $this->types['WebformElementMultipleValues'],
],
]);
$this->types['WebformElementOption'] = new ObjectType([
'name' => 'WebformElementOption',
'fields' => [
......@@ -152,6 +229,14 @@ class WebformSchemaBuilder {
];
}
if ($plugin instanceof WebformElementInterface && $plugin->supportsMultipleValues()) {
$interfaces[] = fn () => $this->types['WebformElementMultipleValuesBase'];
$fields = [
...$fields,
'multiple' => fn () => $this->types['WebformElementMultipleValues'],
];
}
// The Webform element for selecting taxonomy terms has an option to limit
// the depth of terms to choose from. Allow this to be overridden in the
// GraphQL query.
......
......@@ -25,10 +25,13 @@ elements: |-
optional_text_field:
'#type': textfield
'#title': 'Optional text field'
'#multiple': 2
checkboxes:
'#type': checkboxes
'#title': Checkboxes
'#description': 'Choose your moons.'
'#multiple': 2
'#multiple_error': "Please don't check more than @count options."
'#description': '<p>Choose your moons.</p>'
'#options':
phobos: 'Phobos -- The inner moon of Mars.'
deimos: 'Deimos -- The outer moon of Mars.'
......@@ -111,6 +114,18 @@ elements: |-
lactose_free: 'Lactose free'
'#empty_option': 'No restrictions'
'#empty_value': no_restrictions
2_years:
'#type': textfield
'#title': 'Minimum 2 years, maximum 6'
'#multiple': 6
'#pattern': '\d{4}'
'#pattern_error': 'Please enter a year between 1000 and 9999.'
'#multiple__header_label': 'At least two years'
'#multiple__item_label': year
'#multiple__no_items_message': '<p>We need some years.</p>'
'#multiple__min_items': 2
'#multiple__add_more_button_label': 'I want more'
'#multiple__add_more_items': 2
actions:
'#type': webform_actions
'#title': 'Submit button(s)'
......
fragment multiple on WebformElementMultipleValues {
limit
message
headerLabel
minItems
emptyItems
addMore
addMoreItems
addMoreButtonLabel
addMoreInput
addMoreInputLabel
itemLabel
noItemsMessage
sorting
operations
add
remove
}
fragment element on WebformElementMultipleValuesBase {
multiple {
...multiple
}
}
query webform($id: String!) {
form: webformById(id: $id) {
title
elements {
...element
}
}
}
......@@ -21,6 +21,8 @@ class WebformElementPropertyTest extends KernelTestBase {
protected static $modules = [
'graphql',
'graphql_webform',
'webform',
'user',
];
/**
......
<?php
declare(strict_types=1);
namespace Drupal\Tests\graphql_webform\Kernel\Element;
use Drupal\Tests\graphql_webform\Kernel\GraphQLWebformKernelTestBase;
/**
* Tests for the elements that accept multiple values.
*
* @group graphql_webform
*/
class MultipleValuesTest extends GraphQLWebformKernelTestBase {
/**
* Tests querying elements that accept multiple values.
*/
public function testMultipleValues(): void {
$query = $this->getQueryFromFile('multiple.gql');
$this->assertResults($query, ['id' => 'graphql_webform_test_form'], [
'form' => [
'title' => 'GraphQL Webform test form',
'elements' => [
0 => ['multiple' => NULL],
1 => [
'multiple' => [
'limit' => 2,
'message' => NULL,
'headerLabel' => '',
'minItems' => NULL,
'emptyItems' => 1,
'addMore' => TRUE,
'addMoreItems' => 1,
'addMoreButtonLabel' => 'Add',
'addMoreInput' => TRUE,
'addMoreInputLabel' => 'more items',
'itemLabel' => 'item',
'noItemsMessage' => '<p>No items entered. Please add items below.</p>',
'sorting' => TRUE,
'operations' => TRUE,
'add' => TRUE,
'remove' => TRUE,
],
],
2 => [
'multiple' => [
'limit' => -1,
'message' => "Please don't check more than @count options.",
'headerLabel' => NULL,
'minItems' => NULL,
'emptyItems' => NULL,
'addMore' => NULL,
'addMoreItems' => NULL,
'addMoreButtonLabel' => NULL,
'addMoreInput' => NULL,
'addMoreInputLabel' => NULL,
'itemLabel' => NULL,
'noItemsMessage' => NULL,
'sorting' => NULL,
'operations' => NULL,
'add' => NULL,
'remove' => NULL,
],
],
4 => ['multiple' => NULL],
6 => [
'multiple' => [
'limit' => -1,
'message' => NULL,
'headerLabel' => NULL,
'minItems' => NULL,
'emptyItems' => NULL,
'addMore' => NULL,
'addMoreItems' => NULL,
'addMoreButtonLabel' => NULL,
'addMoreInput' => NULL,
'addMoreInputLabel' => NULL,
'itemLabel' => NULL,
'noItemsMessage' => NULL,
'sorting' => NULL,
'operations' => NULL,
'add' => NULL,
'remove' => NULL,
],
],
8 => ['multiple' => NULL],
11 => [
'multiple' => [
'limit' => -1,
'message' => NULL,
'headerLabel' => NULL,
'minItems' => NULL,
'emptyItems' => NULL,
'addMore' => NULL,
'addMoreItems' => NULL,
'addMoreButtonLabel' => NULL,
'addMoreInput' => NULL,
'addMoreInputLabel' => NULL,
'itemLabel' => NULL,
'noItemsMessage' => NULL,
'sorting' => NULL,
'operations' => NULL,
'add' => NULL,
'remove' => NULL,
],
],
12 => ['multiple' => NULL],
13 => [
'multiple' => [
'limit' => 6,
'message' => NULL,
'headerLabel' => 'At least two years',
'minItems' => 2,
'emptyItems' => 1,
'addMore' => TRUE,
'addMoreItems' => 2,
'addMoreButtonLabel' => 'I want more',
'addMoreInput' => TRUE,
'addMoreInputLabel' => 'more items',
'itemLabel' => 'year',
'noItemsMessage' => 'We need some years.',
'sorting' => TRUE,
'operations' => TRUE,
'add' => TRUE,
'remove' => TRUE,
],
],
],
],
], $this->defaultCacheMetaData());
}
}
......@@ -7,7 +7,7 @@ namespace Drupal\Tests\graphql_webform\Kernel\Element;
use Drupal\Tests\graphql_webform\Kernel\GraphQLWebformKernelTestBase;
/**
* Tests for the WebformElementTextField type.
* Tests for the WebformElementTextfield type.
*
* @group graphql_webform
*/
......@@ -32,14 +32,14 @@ class TextFieldTest extends GraphQLWebformKernelTestBase {
'required' => TRUE,
'requiredError' => 'This field is required because it is important.',
],
'readonly' => NULL,
'readonly' => FALSE,
'size' => 10,
'minlength' => 1,
'maxlength' => 10,
'placeholder' => NULL,
'autocomplete' => NULL,
'pattern' => NULL,
'patternError' => NULL,
'placeholder' => '',
'autocomplete' => 'on',
'pattern' => '',
'patternError' => '',
'inputMask' => "'casing': 'lower'",
],
1 => [
......@@ -52,15 +52,35 @@ class TextFieldTest extends GraphQLWebformKernelTestBase {
'required' => FALSE,
'requiredError' => NULL,
],
'readonly' => NULL,
'readonly' => FALSE,
'size' => 60,
'minlength' => NULL,
'maxlength' => 255,
'placeholder' => NULL,
'autocomplete' => NULL,
'pattern' => NULL,
'patternError' => NULL,
'inputMask' => NULL,
'placeholder' => '',
'autocomplete' => 'on',
'pattern' => '',
'patternError' => '',
'inputMask' => '',
],
13 => [
'__typename' => 'WebformElementTextfield',
'metadata' => [
'key' => '2_years',
'type' => 'textfield',
'title' => 'Minimum 2 years, maximum 6',
'description' => '',
'required' => FALSE,
'requiredError' => '',
],
'readonly' => FALSE,
'size' => 60,
'minlength' => NULL,
'maxlength' => 255,
'placeholder' => '',
'autocomplete' => 'on',
'pattern' => '',
'patternError' => 'Please enter a year between 1000 and 9999.',
'inputMask' => '',
],
],
],
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment