Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Expand the commerce_profile_select form element with the ability to reuse profiles #760

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -33,20 +33,14 @@ public function buildPaneSummary() {
*/
public function buildPaneForm(array $pane_form, FormStateInterface $form_state, array &$complete_form) {
$store = $this->order->getStore();
$billing_profile = $this->order->getBillingProfile();
if (!$billing_profile) {
$profile_storage = $this->entityTypeManager->getStorage('profile');
$billing_profile = $profile_storage->create([
'type' => 'customer',
'uid' => $this->order->getCustomerId(),
]);
}

$pane_form['profile'] = [
'#type' => 'commerce_profile_select',
'#default_value' => $billing_profile,
'#title' => $this->t('Select an address'),
'#default_value' => $this->order->getBillingProfile(),
'#default_country' => $store->getAddress()->getCountryCode(),
'#available_countries' => $store->getBillingCountries(),
'#profile_type' => 'customer',
'#owner_uid' => $this->order->getCustomerId(),
];

return $pane_form;
Expand All @@ -56,7 +50,8 @@ public function buildPaneForm(array $pane_form, FormStateInterface $form_state,
* {@inheritdoc}
*/
public function submitPaneForm(array &$pane_form, FormStateInterface $form_state, array &$complete_form) {
$this->order->setBillingProfile($pane_form['profile']['#profile']);
$values = $form_state->getValue($pane_form['#parents']);
$this->order->setBillingProfile($values['profile']);
}

}
219 changes: 187 additions & 32 deletions modules/order/src/Element/ProfileSelect.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@
namespace Drupal\commerce_order\Element;

use Drupal\commerce\Element\CommerceElementTrait;
use Drupal\Component\Utility\Html;
use Drupal\Component\Utility\NestedArray;
use Drupal\Core\Entity\Entity\EntityFormDisplay;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Render\Element\RenderElement;
use Drupal\Core\Render\Element\FormElement;
use Drupal\profile\Entity\ProfileInterface;

/**
Expand All @@ -18,15 +20,14 @@
* '#default_value' => $profile,
* '#default_country' => 'FR',
* '#available_countries' => ['US', 'FR'],
* '#profile_type' => 'customer',
* '#owner_uid' => \Drupal::currentUser()->id(),
* ];
* @endcode
* To access the profile in validation or submission callbacks, use
* $form['billing_profile']['#profile']. Due to Drupal core limitations the
* profile can't be accessed via $form_state->getValue('billing_profile').
*
* @RenderElement("commerce_profile_select")
* @FormElement("commerce_profile_select")
*/
class ProfileSelect extends RenderElement {
class ProfileSelect extends FormElement {
Copy link
Contributor Author

@bojanz bojanz Jul 23, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is reverting the fix from #2857051.

Might have started by accident, since this PR is older than that fix.


use CommerceElementTrait;

Expand All @@ -40,9 +41,14 @@ public function getInfo() {
'#default_country' => NULL,
// A list of country codes. If empty, all countries will be available.
'#available_countries' => [],
'#title' => t('Select a profile'),
'#create_title' => t('+ Enter a new profile'),

// The profile entity operated on. Required.
'#default_value' => NULL,
'#default_value' => '_new',
'#owner_uid' => 0,
// Provide default to not break contrib which have outdated elements.
'#profile_type' => 'customer',
'#process' => [
[$class, 'attachElementSubmit'],
[$class, 'processForm'],
Expand All @@ -58,6 +64,26 @@ public function getInfo() {
];
}

/**
* {@inheritdoc}
*/
public static function valueCallback(&$element, $input, FormStateInterface $form_state) {
if (!empty($input['profile_selection'])) {
$value = $input['profile_selection'];
}
elseif ($element['#default_value'] instanceof ProfileInterface) {
$value = $element['#default_value']->id();
}
elseif (!empty($element['#default_value'])) {
$value = $element['#default_value'];
}
else {
$value = '_new';
}

return $value;
}

/**
* Builds the element form.
*
Expand All @@ -76,30 +102,115 @@ public function getInfo() {
* The processed form element.
*/
public static function processForm(array $element, FormStateInterface $form_state, array &$complete_form) {
if (empty($element['#default_value'])) {
throw new \InvalidArgumentException('The commerce_profile_select element requires the #default_value property.');
}
elseif (isset($element['#default_value']) && !($element['#default_value'] instanceof ProfileInterface)) {
throw new \InvalidArgumentException('The commerce_profile_select #default_value property must be a profile entity.');
}
if (!is_array($element['#available_countries'])) {
throw new \InvalidArgumentException('The commerce_profile_select #available_countries property must be an array.');
}
if (empty($element['#profile_type'])) {
throw new \InvalidArgumentException('The commerce_profile_select #profile_type property must be provided.');
}
$entity_type_manager = \Drupal::entityTypeManager();
/** @var \Drupal\profile\ProfileStorageInterface $profile_storage */
$profile_storage = $entity_type_manager->getStorage('profile');
/** @var \Drupal\profile\Entity\ProfileTypeInterface $profile_type */
$profile_type = $entity_type_manager->getStorage('profile_type')->load($element['#profile_type']);

$user_profiles = [];
/** @var \Drupal\user\UserInterface $user */
$user = $entity_type_manager->getStorage('user')->load($element['#owner_uid']);

if (!$user->isAnonymous()) {
// If the user exists, attempt to load other profiles for selection.
foreach ($profile_storage->loadMultipleByUser($user, $profile_type->id(), TRUE) as $existing_profile) {
$user_profiles[$existing_profile->id()] = $existing_profile->label();

$element['#profile'] = $element['#default_value'];
$form_display = EntityFormDisplay::collectRenderDisplay($element['#profile'], 'default');
$form_display->buildForm($element['#profile'], $element, $form_state);
if (!empty($element['address']['widget'][0])) {
$widget_element = &$element['address']['widget'][0];
// Remove the details wrapper from the address widget.
$widget_element['#type'] = 'container';
// Provide a default country.
if (!empty($element['#default_country']) && empty($widget_element['address']['#default_value']['country_code'])) {
$widget_element['address']['#default_value']['country_code'] = $element['#default_country'];
// If this is the first form build, set the element's value based on
// the user's default profile.
if (!$form_state->isProcessingInput() && $existing_profile->isDefault()) {
$element['#value'] = $existing_profile->id();
}
}
// Limit the available countries.
if (!empty($element['#available_countries'])) {
$widget_element['address']['#available_countries'] = $element['#available_countries'];
}

$id_prefix = implode('-', $element['#parents']);
$wrapper_id = Html::getUniqueId($id_prefix . '-ajax-wrapper');
$element = [
'#tree' => TRUE,
'#prefix' => '<div id="' . $wrapper_id . '">',
'#suffix' => '</div>',
// Pass the id along to other methods.
'#wrapper_id' => $wrapper_id,
'#element_mode' => $form_state->get('element_mode') ?: 'view',

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the multiple ProfileSelect form elements exist in the same form (eg. in checkout page, the shipping info and billing info), the element_mode in $form_state should be separated by the form parents.

For example, commerce_shipping module is installed, and there are two form elements in the same checkout form page. Once a edit_button within shipping info (or payment info) is clicked, the same element_mode value in $form_state is set into edit by ajaxSubmit(). Therefore, #element_mode in shipping info and payment info are edit. It caused another form element in payment info doesn't contain edit_button.

Perhaps, the element mode name should be with the form parents.

$form_state->get('element_mode-' . $id_prefix . '-edit_button')

] + $element;

if (!empty($user_profiles)) {
$element['profile_selection'] = [
'#title' => $element['#title'],
'#options' => $user_profiles + ['_new' => $element['#create_title']],
'#type' => 'select',
'#weight' => -5,
'#default_value' => $element['#value'],
'#ajax' => [
'callback' => [get_called_class(), 'ajaxRefresh'],
'wrapper' => $wrapper_id,
],
'#element_mode' => 'view',
];
}
else {
$element['profile_selection'] = [
'#type' => 'value',
'#value' => '_new',
'#element_mode' => 'create',
];
}

/** @var \Drupal\profile\Entity\ProfileInterface $element_profile */
if ($element['#value'] == '_new') {
$element_profile = $profile_storage->create([
'type' => $profile_type->id(),
'uid' => $user->id(),
]);
$element['#element_mode'] = 'create';
}
else {
$element_profile = $profile_storage->load($element['#value']);
}

// Viewing a profile.
if (!$element_profile->isNew() && $element['#element_mode'] == 'view') {
$view_builder = $entity_type_manager->getViewBuilder('profile');
$element['rendered_profile'] = $view_builder->view($element_profile, 'default');

$element['edit_button'] = [
'#type' => 'submit',
'#value' => t('Edit'),
'#limit_validation_errors' => [],
'#ajax' => [
'callback' => [get_called_class(), 'ajaxRefresh'],
'wrapper' => $wrapper_id,
],
'#submit' => [[get_called_class(), 'ajaxSubmit']],
'#name' => 'edit_profile',

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm testing the patch when enabling commerce_shipping module.

Because, the commerce_shipping also uses the ProfileSelect element, in checkout page, there are multiple submit buttons with same #name. If the edit_button element is clicked, the trigger element will be wrong.

In my testing, when I clicked the "Edit" button, the trigger element was the "Continue to review" button. However, the expected and correct trigger element should be "Edit" button. The problem may result from the same value of #name and more detail is in issue #2546700.

In order to prevent the situation, we should have a workaround for the issue to make the value of #name unique.
My rough implementation:

$form_name = (isset($element['#parents'])) ? implode('_', $element['#parents']) . '_' : '';
$form_name .= 'edit_profile';

And the value of the #name is $form_name.

'#element_mode' => 'edit',
];
}
else {
$form_display = EntityFormDisplay::collectRenderDisplay($element_profile, 'default');
$form_display->buildForm($element_profile, $element, $form_state);

// @todo Loop over all possible address fields.
if (!empty($element['address']['widget'][0])) {
$widget_element = &$element['address']['widget'][0];
// Remove the details wrapper from the address widget.
$widget_element['#type'] = 'container';
// Provide a default country.
if (!empty($element['#default_country']) && empty($widget_element['address']['#default_value']['country_code'])) {
$widget_element['address']['#default_value']['country_code'] = $element['#default_country'];
}
// Limit the available countries.
if (!empty($element['#available_countries'])) {
$widget_element['address']['#available_countries'] = $element['#available_countries'];
}
}
}

Expand All @@ -119,9 +230,29 @@ public static function processForm(array $element, FormStateInterface $form_stat
* form, as a protection against buggy behavior.
*/
public static function validateForm(array &$element, FormStateInterface $form_state) {
$form_display = EntityFormDisplay::collectRenderDisplay($element['#profile'], 'default');
$form_display->extractFormValues($element['#profile'], $element, $form_state);
$form_display->validateFormValues($element['#profile'], $element, $form_state);
$value = $form_state->getValue($element['#parents']);

$entity_type_manager = \Drupal::entityTypeManager();
/** @var \Drupal\profile\ProfileStorageInterface $profile_storage */
$profile_storage = $entity_type_manager->getStorage('profile');
/** @var \Drupal\profile\Entity\ProfileInterface $element_profile */
if ($value['profile_selection'] == '_new') {
$element_profile = $profile_storage->create([
'type' => $element['#profile_type'],
'uid' => $element['#owner_uid'],
]);
}
else {
$element_profile = $profile_storage->load($value['profile_selection']);
}

if ($element['#element_mode'] != 'view' && $form_state->isSubmitted()) {
$form_display = EntityFormDisplay::collectRenderDisplay($element_profile, 'default');
$form_display->extractFormValues($element_profile, $element, $form_state);
$form_display->validateFormValues($element_profile, $element, $form_state);
}

$form_state->setValueForElement($element, $element_profile);
}

/**
Expand All @@ -133,9 +264,33 @@ public static function validateForm(array &$element, FormStateInterface $form_st
* The current state of the form.
*/
public static function submitForm(array &$element, FormStateInterface $form_state) {
$form_display = EntityFormDisplay::collectRenderDisplay($element['#profile'], 'default');
$form_display->extractFormValues($element['#profile'], $element, $form_state);
$element['#profile']->save();
$element_profile = $form_state->getValue($element['#parents']);

if ($element['#element_mode'] != 'view' && $form_state->isSubmitted()) {
$form_display = EntityFormDisplay::collectRenderDisplay($element_profile, 'default');
$form_display->extractFormValues($element_profile, $element, $form_state);
$element_profile->save();
}

$form_state->setValueForElement($element, $element_profile);
}

/**
* Ajax callback.
*/
public static function ajaxRefresh(array &$form, FormStateInterface $form_state) {
$triggering_element = $form_state->getTriggeringElement();
$element = NestedArray::getValue($form, array_slice($triggering_element['#array_parents'], 0, -1));
return $element;
}

/**
* Ajax submit callback.
*/
public static function ajaxSubmit(array &$form, FormStateInterface $form_state) {
$triggering_element = $form_state->getTriggeringElement();
$form_state->set('element_mode', $triggering_element['#element_mode']);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As previous mention, the element_mode key in form state should be changed into:

    $parents = implode('-', $triggering_element['#parents']);
    $element_mode_name = 'element_mode-' . $parents;

    $form_state->set($element_mode_name, $triggering_element['#element_mode']);

$form_state->setRebuild();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

namespace Drupal\commerce_order\Plugin\Field\FieldWidget;

use Drupal\Component\Utility\NestedArray;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\Field\FieldItemListInterface;
Expand Down Expand Up @@ -75,22 +74,16 @@ public function formElement(FieldItemListInterface $items, $delta, array $elemen
$order = $items[$delta]->getEntity();
$store = $order->getStore();

if (!$items[$delta]->isEmpty()) {
$profile = $items[$delta]->entity;
}
else {
$profile = $this->entityTypeManager->getStorage('profile')->create([
'type' => 'customer',
'uid' => $order->getCustomerId(),
]);
}

$element['#type'] = 'fieldset';
$element['profile'] = [
'#type' => 'commerce_profile_select',
'#default_value' => $profile,
'#title' => $this->t('Select an address'),
'#create_title' => t('+ Enter a new address'),
'#default_value' => $profile = $items[$delta]->entity,
'#default_country' => $store->getAddress()->getCountryCode(),
'#available_countries' => $store->getBillingCountries(),
'#profile_type' => 'customer',
'#owner_uid' => $order->getCustomerId(),
];
// Workaround for massageFormValues() not getting $element.
$element['array_parents'] = [
Expand All @@ -107,8 +100,7 @@ public function formElement(FieldItemListInterface $items, $delta, array $elemen
public function massageFormValues(array $values, array $form, FormStateInterface $form_state) {
$new_values = [];
foreach ($values as $delta => $value) {
$element = NestedArray::getValue($form, $value['array_parents']);
$new_values[$delta]['entity'] = $element['profile']['#profile'];
$new_values[$delta]['entity'] = $value['profile'];
}
return $new_values;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
commerce_order_test.profile_select_form:
path: '/commerce_order_test/profile_select_form'
defaults:
_form: '\Drupal\commerce_order_test\Form\ProfileSelectTestForm'
_title: 'Profile select test form'
requirements:
_access: 'TRUE'
options:
no_cache: TRUE