Commit adf3dd2f authored by bojanz's avatar bojanz Committed by bojanz

Issue #2886642 by bojanz: Create a new condition UI

parent 893361c7
conditions:
version: VERSION
js:
js/conditions.js: {}
toolbar:
version: VERSION
css:
......
......@@ -29,6 +29,16 @@ field.value.commerce_remote_id:
type: string
label: 'Remote ID'
field.widget.settings.commerce_conditions:
type: mapping
label: 'Conditions widget settings'
mapping:
entity_types:
type: sequence
label: 'Entity types'
sequence:
type: string
field.widget.settings.commerce_entity_select:
type: mapping
label: 'Entity select widget settings'
......
/**
* @file
* Condition UI behaviors.
*/
(function ($, window, Drupal) {
'use strict';
/**
* Provides the summary information for the condition vertical tabs.
*
* @type {Drupal~behavior}
*
* @prop {Drupal~behaviorAttach} attach
* Attaches the behavior for the condition summaries.
*/
Drupal.behaviors.conditionSummary = {
attach: function () {
$('.vertical-tabs__pane').each(function () {
$(this).drupalSetSummary(function (context) {
if ($(context).find('input.enable:checked').length) {
return Drupal.t('Restricted');
}
else {
return Drupal.t('Not restricted');
}
});
});
}
};
})(jQuery, window, Drupal);
......@@ -556,8 +556,11 @@ class Promotion extends ContentEntityBase implements PromotionInterface {
->setCardinality(BaseFieldDefinition::CARDINALITY_UNLIMITED)
->setRequired(FALSE)
->setDisplayOptions('form', [
'type' => 'commerce_plugin_select',
'type' => 'commerce_conditions',
'weight' => 3,
'settings' => [
'entity_types' => ['commerce_order', 'commerce_order_item'],
],
]);
$fields['coupons'] = BaseFieldDefinition::create('entity_reference')
......
......@@ -48,7 +48,6 @@ class PromotionTest extends CommerceBrowserTestBase {
$this->assertSession()->fieldExists('name[0][value]');
$name = $this->randomMachineName(8);
$this->getSession()->getPage()->fillField('name[0][value]', $name);
$this->getSession()->getPage()->selectFieldOption('offer[0][target_plugin_id]', 'commerce_promotion_product_percentage_off');
$this->waitForAjaxToFinish();
$this->getSession()->getPage()->fillField('offer[0][target_plugin_configuration][commerce_promotion_product_percentage_off][amount]', '10.0');
......@@ -59,9 +58,17 @@ class PromotionTest extends CommerceBrowserTestBase {
$this->assertSession()->fieldValueNotEquals('offer[0][target_plugin_configuration][commerce_promotion_order_percentage_off][amount]', '10.0');
$this->getSession()->getPage()->fillField('offer[0][target_plugin_configuration][commerce_promotion_order_percentage_off][amount]', '10.0');
$this->getSession()->getPage()->selectFieldOption('conditions[0][target_plugin_id]', 'commerce_promotion_order_total_price');
// Confirm the integrity of the conditions UI.
foreach (['order', 'product', 'customer'] as $condition_group) {
$tab_matches = $this->xpath('//a[@href="#edit-conditions-form-' . $condition_group . '"]');
$this->assertNotEmpty($tab_matches);
}
$vertical_tab_elements = $this->xpath('//a[@href="#edit-conditions-form-order"]');
$vertical_tab_element = reset($vertical_tab_elements);
$vertical_tab_element->click();
$this->getSession()->getPage()->checkField('Limit by total price');
$this->waitForAjaxToFinish();
$this->getSession()->getPage()->fillField('conditions[0][target_plugin_configuration][commerce_promotion_order_total_price][amount][number]', '50.00');
$this->getSession()->getPage()->fillField('conditions[form][order][commerce_promotion_order_total_price][configuration][form][amount][number]', '50.00');
// Confirm that the usage limit widget works properly.
$this->getSession()->getPage()->hasCheckedField(' Unlimited');
......@@ -111,11 +118,6 @@ class PromotionTest extends CommerceBrowserTestBase {
'offer[0][target_plugin_configuration][commerce_promotion_order_percentage_off][amount]' => '10.0',
];
$this->getSession()->getPage()->fillField('conditions[0][target_plugin_id]', 'commerce_promotion_order_total_price');
$this->waitForAjaxToFinish();
$edit['conditions[0][target_plugin_configuration][commerce_promotion_order_total_price][amount][number]'] = '50.00';
// Set an end date.
$this->getSession()->getPage()->checkField('end_date[0][has_value]');
$edit['end_date[0][container][value][date]'] = date("Y") + 1 . '-01-01';
......@@ -143,6 +145,17 @@ class PromotionTest extends CommerceBrowserTestBase {
'amount' => '0.10',
],
],
'conditions' => [
[
'target_plugin_id' => 'commerce_promotion_order_total_price',
'target_plugin_configuration' => [
'amount' => [
'number' => '9.10',
'currency_code' => 'USD',
],
],
],
],
]);
/** @var \Drupal\commerce\Plugin\Field\FieldType\PluginItem $offer_field */
......@@ -150,6 +163,10 @@ class PromotionTest extends CommerceBrowserTestBase {
$this->assertEquals('0.10', $offer_field->target_plugin_configuration['amount']);
$this->drupalGet($promotion->toUrl('edit-form'));
$this->assertSession()->pageTextContains('Restricted');
$this->assertSession()->checkboxChecked('Limit by total price');
$this->assertSession()->fieldValueEquals('conditions[form][order][commerce_promotion_order_total_price][configuration][form][amount][number]', '9.10');
$new_promotion_name = $this->randomMachineName(8);
$edit = [
'name[0][value]' => $new_promotion_name,
......
......@@ -2,7 +2,6 @@
namespace Drupal\commerce;
use Drupal\Component\Plugin\CategorizingPluginManagerInterface;
use Drupal\Component\Plugin\Exception\PluginException;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
......@@ -16,7 +15,7 @@ use Drupal\Core\Plugin\DefaultPluginManager;
* @see \Drupal\commerce\Annotation\CommerceCondition
* @see plugin_api
*/
class ConditionManager extends DefaultPluginManager implements CategorizingPluginManagerInterface {
class ConditionManager extends DefaultPluginManager implements ConditionManagerInterface {
use CategorizingPluginManagerTrait;
......@@ -66,4 +65,19 @@ class ConditionManager extends DefaultPluginManager implements CategorizingPlugi
}
}
/**
* {@inheritdoc}
*/
public function getDefinitionsByEntityTypes(array $entity_types) {
$definitions = $this->getDefinitions();
if (!empty($entity_types)) {
// Remove conditions not matching the specified entity types.
$definitions = array_filter($definitions, function ($definition) use ($entity_types) {
return in_array($definition['entity_type'], $entity_types);
});
}
return $definitions;
}
}
<?php
namespace Drupal\commerce;
use Drupal\Component\Plugin\CategorizingPluginManagerInterface;
/**
* Defines the interface for commerce_condition plugin managers.
*/
interface ConditionManagerInterface extends CategorizingPluginManagerInterface {
/**
* Gets the plugin definitions for the given entity types.
*
* @param array $entity_types
* The entity type IDs.
*
* @return array
* The plugin definitions.
*/
public function getDefinitionsByEntityTypes(array $entity_types);
}
<?php
namespace Drupal\commerce\Element;
use Drupal\Component\Utility\Html;
use Drupal\Component\Utility\NestedArray;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Render\Element;
use Drupal\Core\Render\Element\FormElement;
/**
* Provides a form element for configuring conditions.
*
* Usage example:
* @code
* $form['conditions'] = [
* '#type' => 'commerce_conditions',
* '#title' => 'Conditions',
* '#entity_types' => ['commerce_order', 'commerce_order_item'],
* '#default_value' => [
* [
* 'id' => 'order_total_price',
* 'configuration' => [
* 'operator' => '<',
* 'amount' => [
* 'number' => '10.00',
* 'currency_code' => 'USD',
* ],
* ],
* ],
* ],
* ];
* @endcode
*
* @FormElement("commerce_conditions")
*/
class Conditions extends FormElement {
use CommerceElementTrait;
/**
* {@inheritdoc}
*/
public function getInfo() {
$class = get_class($this);
return [
'#input' => TRUE,
'#tree' => TRUE,
'#entity_types' => [],
'#default_value' => [],
'#title' => '',
'#process' => [
[$class, 'attachElementSubmit'],
[$class, 'processConditions'],
[$class, 'processAjaxForm'],
],
'#element_validate' => [
[$class, 'validateElementSubmit'],
],
'#commerce_element_submit' => [
[$class, 'submitConditions'],
],
'#theme_wrappers' => ['container'],
];
}
/**
* Processes the conditions form element.
*
* @param array $element
* The form element to process.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current state of the form.
* @param array $complete_form
* The complete form structure.
*
* @return array
* The processed element.
*
* @throws \InvalidArgumentException
* Thrown for malformed #default_value properties.
*/
public static function processConditions(array &$element, FormStateInterface $form_state, array &$complete_form) {
if (!is_array($element['#default_value'])) {
throw new \InvalidArgumentException('The commerce_conditions #default_value must be an array.');
}
$default_value = array_column($element['#default_value'], 'configuration', 'id');
/** @var \Drupal\commerce\ConditionManagerInterface $plugin_manager */
$plugin_manager = \Drupal::service('plugin.manager.commerce_condition');
$definitions = $plugin_manager->getDefinitionsByEntityTypes($element['#entity_types']);
$grouped_definitions = [];
foreach ($definitions as $plugin_id => $definition) {
$category = (string) $definition['category'];
$grouped_definitions[$category][$plugin_id] = $definition;
}
ksort($grouped_definitions);
$tab_group = implode('][', array_merge($element['#parents'], ['conditions']));
$element['#attached']['library'][] = 'commerce/conditions';
$element['#categories'] = [];
$element['conditions'] = [
'#type' => 'vertical_tabs',
'#title' => $element['#title'],
];
foreach ($grouped_definitions as $category => $definitions) {
$category_id = preg_replace('/[^a-zA-Z\-]/', '_', strtolower($category));
$element['#categories'][] = $category_id;
$element[$category_id] = [
'#type' => 'details',
'#title' => $category,
'#group' => $tab_group,
];
foreach ($definitions as $plugin_id => $definition) {
$category_parents = array_merge($element['#parents'], [$category_id]);
$checkbox_parents = array_merge($category_parents, [$plugin_id, 'enable']);
$checkbox_name = self::buildElementName($checkbox_parents);
$checkbox_input = NestedArray::getValue($form_state->getUserInput(), $checkbox_parents);
$enabled = isset($default_value[$plugin_id]) || !empty($checkbox_input);
$ajax_wrapper_id = Html::getUniqueId('ajax-wrapper-' . $plugin_id);
// Preselect the first vertical tab that has an enabled condition.
if ($enabled && !isset($element['conditions']['#default_tab'])) {
$element['conditions']['#default_tab'] = 'edit-' . implode('-', $category_parents);
}
$element[$category_id][$plugin_id] = [
'#prefix' => '<div id="' . $ajax_wrapper_id . '">',
'#suffix' => '</div>',
];
$element[$category_id][$plugin_id]['enable'] = [
'#type' => 'checkbox',
'#title' => $definition['display_label'],
'#default_value' => $enabled,
'#attributes' => [
'class' => ['enable'],
],
'#ajax' => [
'callback' => [get_called_class(), 'ajaxRefresh'],
'wrapper' => $ajax_wrapper_id,
],
];
if ($enabled) {
$element[$category_id][$plugin_id]['configuration'] = [
'#type' => 'commerce_plugin_configuration',
'#plugin_type' => 'commerce_condition',
'#plugin_id' => $plugin_id,
'#default_value' => isset($default_value[$plugin_id]) ? $default_value[$plugin_id] : [],
'#states' => [
'visible' => [
':input[name="' . $checkbox_name . '"]' => ['checked' => TRUE],
],
],
// The element is already keyed by $plugin_id, no need to do it twice.
'#enforce_unique_parents' => FALSE,
];
}
}
}
return $element;
}
/**
* Builds the element name for the given parents.
*
* @param array $parents
* The parents.
*
* @return string
* The element name.
*/
protected static function buildElementName(array $parents) {
$name = array_shift($parents);
$name .= '[' . implode('][', $parents) . ']';
return $name;
}
/**
* Ajax callback.
*/
public static function ajaxRefresh(&$form, FormStateInterface $form_state) {
$element_parents = array_slice($form_state->getTriggeringElement()['#array_parents'], 0, -1);
return NestedArray::getValue($form, $element_parents);
}
/**
* Submits the conditions.
*
* @param array $element
* An associative array containing the properties of the element.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current state of the form.
*/
public static function submitConditions(array &$element, FormStateInterface $form_state) {
$value = [];
// Transfer the configuration of each enabled plugin.
foreach ($element['#categories'] as $category_id) {
foreach (Element::getVisibleChildren($element[$category_id]) as $plugin_id) {
$plugin_element = $element[$category_id][$plugin_id];
$plugin_value = $form_state->getValue($plugin_element['#parents']);
if ($plugin_value['enable']) {
$value[] = [
'id' => $plugin_id,
'configuration' => $plugin_value['configuration'],
];
}
}
}
$form_state->setValueForElement($element, $value);
}
}
<?php
namespace Drupal\commerce\Plugin\Field\FieldWidget;
use Drupal\commerce\ConditionManagerInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Field\WidgetBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Plugin implementation of the 'commerce_conditions' widget.
*
* @FieldWidget(
* id = "commerce_conditions",
* label = @Translation("Conditions"),
* field_types = {
* "commerce_plugin_item:commerce_condition"
* },
* multiple_values = TRUE
* )
*/
class ConditionsWidget extends WidgetBase implements ContainerFactoryPluginInterface {
/**
* The condition manager.
*
* @var \Drupal\commerce\ConditionManagerInterface
*/
protected $conditionManager;
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* Constructs a ConditionsWidget object.
*
* @param string $plugin_id
* The plugin_id for the widget.
* @param mixed $plugin_definition
* The plugin implementation definition.
* @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition
* The definition of the field to which the widget is associated.
* @param array $settings
* The widget settings.
* @param array $third_party_settings
* Any third party settings.
* @param \Drupal\commerce\ConditionManagerInterface $condition_manager
* The condition manager.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
*/
public function __construct($plugin_id, $plugin_definition, FieldDefinitionInterface $field_definition, array $settings, array $third_party_settings, ConditionManagerInterface $condition_manager, EntityTypeManagerInterface $entity_type_manager) {
parent::__construct($plugin_id, $plugin_definition, $field_definition, $settings, $third_party_settings);
$this->conditionManager = $condition_manager;
$this->entityTypeManager = $entity_type_manager;
}
/**
* {@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('plugin.manager.commerce_condition'),
$container->get('entity_type.manager')
);
}
/**
* {@inheritdoc}
*/
public static function defaultSettings() {
return [
'entity_types' => [],
] + parent::defaultSettings();
}
/**
* {@inheritdoc}
*/
public function settingsForm(array $form, FormStateInterface $formState) {
$entity_types = $this->entityTypeManager->getDefinitions();
// Remove entity types for which there are no conditions.
$condition_entity_types = array_column($this->conditionManager->getDefinitions(), 'entity_type', 'entity_type');
$entity_types = array_filter($entity_types, function ($entity_type) use ($condition_entity_types) {
/** @var \Drupal\Core\Entity\EntityType $entity_type */
return in_array($entity_type->id(), $condition_entity_types);
});
$entity_types = array_map(function ($entity_type) {
/** @var \Drupal\Core\Entity\EntityType $entity_type */
return $entity_type->getLabel();
}, $entity_types);
$element = [];
$element['entity_types'] = [
'#type' => 'checkboxes',
'#title' => $this->t('Entity types'),
'#options' => $entity_types,
'#default_value' => $this->getSetting('entity_types'),
'#description' => $this->t('Only conditions matching the specified entity types will be displayed. Leave empty to show all.'),
];
return $element;
}
/**
* {@inheritdoc}
*/
public function settingsSummary() {
$summary = [];
$selected_entity_types = array_filter($this->getSetting('entity_types'));
if (!empty($selected_entity_types)) {
$entity_types = $this->entityTypeManager->getDefinitions();
$entity_types = array_filter($entity_types, function ($entity_type) use ($selected_entity_types) {
/** @var \Drupal\Core\Entity\EntityType $entity_type */
return in_array($entity_type->id(), $selected_entity_types);
});
$entity_types = array_map(function ($entity_type) {
/** @var \Drupal\Core\Entity\EntityType $entity_type */
return $entity_type->getLabel();
}, $entity_types);
$summary[] = $this->t('Entity types: @entity_types', ['@entity_types' => implode(', ', $entity_types)]);
}
else {
$summary[] = $this->t('No entity type restrictions');
}
return $summary;
}
/**
* {@inheritdoc}
*/
public function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state) {
$values = [];
foreach ($items->getValue() as $value) {
$values[] = [
'id' => $value['target_plugin_id'],
'configuration' => $value['target_plugin_configuration'],
];
}
$element['form'] = [
'#type' => 'commerce_conditions',
'#title' => $this->fieldDefinition->getLabel(),
'#default_value' => $values,
'#entity_types' => array_filter($this->getSetting('entity_types')),
'#required' => $this->fieldDefinition->isRequired(),
];
return $element;
}
/**
* {@inheritdoc}
*/
public function massageFormValues(array $values, array $form, FormStateInterface $form_state) {
$new_values = [];
foreach ($values['form'] as $value) {
if (!isset($value['id'])) {
// This method is invoked during validation with incomplete values.
// The commerce_conditions form element can't set the right values until form submit.
continue;
}
$new_values[] = [
'target_plugin_id' => $value['id'],
'target_plugin_configuration' => $value['configuration'],
];
}
return $new_values;
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment