Skip to content

Instantly share code, notes, and snippets.

@hudri
Created August 24, 2020 11:04
Show Gist options
  • Save hudri/4cb05bd631ef53fa28ca80341f73b8da to your computer and use it in GitHub Desktop.
Save hudri/4cb05bd631ef53fa28ca80341f73b8da to your computer and use it in GitHub Desktop.
Drupal Form with multiple submit buttons in a table
<?php
namespace Drupal\wt_pricetable\Form;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\paragraphs\Entity\Paragraph;
use Drupal\Component\Render\FormattableMarkup;
use Drupal\Core\Url;
use Drupal\wt_svgicons\SvgIconsHelperService;
/**
* Provides a bulk edit form for pricetable paragraphs accross mulitple wt_room nodes
*/
class BulkEditForm extends FormBase {
/**
* @var EntityTypeManagerInterface $entityTypeManager
*/
protected $entityTypeManager;
/**
* @var SvgIconsHelperService $iconHelper
*/
protected $iconHelper;
/**
* Class constructor.
*/
public function __construct(EntityTypeManagerInterface $entityTypeManager, SvgIconsHelperService $iconHelper) {
$this->entityTypeManager = $entityTypeManager;
$this->iconHelper = $iconHelper;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('entity_type.manager'),
$container->get('wt_svgicons.helper')
);
}
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'wt_pricetable_bulk_edit';
}
public function cbSubmitCopy(array &$form, FormStateInterface $form_state) {
$form_state->set('num_pricerows', $form_state->get('num_pricerows') + 1);
$form_state->setRebuild();
}
public function cbCopyRow(array &$form, FormStateInterface $form_state) {
// using Ajax seems pointless, but necessary due Drupal bug
// @see https://www.drupal.org/project/drupal/issues/2546700#comment-10801702
return $form['pt'];
}
public function cbSubmitAdd(array &$form, FormStateInterface $form_state) {
$form_state->set('num_pricerows', $form_state->get('num_pricerows') + 1);
$form_state->setRebuild();
}
public function cbAddRow(array &$form, FormStateInterface $form_state) {
// using Ajax seems pointless, but necessary due Drupal bug
// @see https://www.drupal.org/project/drupal/issues/2546700#comment-10801702
return $form['pt'];
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
$form['#attached']['library'][] = 'wt_pricetable/admin';
$query = $this->entityTypeManager->getStorage('node')->getQuery();
$query->condition('type', 'wt_room');
$query->sort('field_weight', 'ASC');
$query->sort('title', 'ASC', \Drupal::languageManager()->getDefaultLanguage()->getId());
$query->sort('nid', 'ASC');
$roomIds = $query->execute();
$rooms = $this->entityTypeManager->getStorage('node')->loadMultiple($roomIds);
if (count($roomIds) < 1) {
$this->messenger()->addWarning($this->t('You cannot edit prices because there are no rooms. <a href=":link">Create a room here.</a>', [':link' => Url::fromRoute('node.add', ['node_type' => 'wt_room'])->toString()]));
return $form;
}
$query = $this->entityTypeManager->getStorage('taxonomy_term')->getQuery();
$query->condition('vid', 'rate');
$query->sort('weight', 'ASC');
$rateIds = $query->execute();
$rates = $this->entityTypeManager->getStorage('taxonomy_term')->loadMultiple($rateIds);
if (count($rateIds) == 0) {
$this->messenger()->addWarning($this->t('You cannot edit prices because there are no rates. <a href=":link">Create a rate here.</a>', [':link' => Url::fromRoute('entity.taxonomy_term.add_form', ['taxonomy_vocabulary' => 'rate'])->toString()]));
return $form;
}
$query = $this->entityTypeManager->getStorage('paragraph')->getQuery();
$query->condition('type', 'pricetable');
$query->condition('parent_id', array_keys($rooms), 'IN');
$query->sort('field_pt_from', 'ASC');
$query->sort('field_pt_to', 'ASC');
$query->sort('field_pt_rate', 'ASC');
$priceIds = $query->execute();
$prices = $this->entityTypeManager->getStorage('paragraph')->loadMultiple($priceIds);
$initialDefaultPrices = [];
foreach ($prices as $price) {
$key = $price->get('field_pt_from')->value . '___' . $price->get('field_pt_to')->value . '___' . $price->get('field_pt_rate')->target_id;
if (!array_key_exists($key, $initialDefaultPrices)) {
$initialDefaultPrices[$key] = [
'from' => $price->get('field_pt_from')->value,
'to' => $price->get('field_pt_to')->value,
'rate' => $price->get('field_pt_rate')->target_id
];
}
$initialDefaultPrices[$key]['room-'.$price->get('parent_id')->value] = $price->get('field_pt_price')->value;
}
$initialDefaultPrices = array_values($initialDefaultPrices);
$form['#tree'] = TRUE;
$form['intro'] = [
'#markup' => '<p><ul>' .
'<li>' . $this->t('The combination of <em>From</em>, <em>To</em> and <em>Rate</em> must be unique.') . '</li>' .
'<li>' . $this->t('<del>Strikethrough rows</del> will be deleted after clicking the button <em>Update all prices</em>.') . '</li>' .
'</ul></p>'
];
$form['pt'] = [
'#type' => 'container',
'#prefix' => '<div id="pt-ajax-wrapper">',
'#suffix' => '</div>',
];
$form['pt']['table'] = [
'#type' => 'container',
'#prefix' => '<table class="pt">',
'#suffix' => '</table>',
];
$th = '<tr class="pt__head">' .
'<th class="text-align-center">' . $this->iconHelper->getSvgIcon('trash', 'far') . '</th>' .
'<th class="text-align-center">' . $this->iconHelper->getSvgIcon('copy', 'far') . '</i></th>' .
'<th>' . $this->t('From', [], ['context' => 'time']) . '</th>' .
'<th>' . $this->t('To', [], ['context' => 'time']) . '</th>'.
'<th>' . $this->t('Rate', [], ['context' => 'price']) . '</th>';
foreach ($rooms as $room) {
$th .= '<th>' . $room->get('title')->value . '</th>';
}
$form['pt']['table']['head'] = [
'#type' => 'inline_template',
'#template' => '<thead><tr>' . $th . '</tr></thead>',
];
if ($form_state->get('num_pricerows') === NULL) {
$form_state->set('num_pricerows', count($initialDefaultPrices));
}
$copySource = false;
if (isset($form_state->getTriggeringElement()['#parents'][4]) && $form_state->getTriggeringElement()['#parents'][4] == 'copy') {
$copySource = $form_state->getTriggeringElement()['#parents'][3];
}
$form['pt']['table']['rows'] = [
'#type' => 'container',
'#prefix' => '<tbody>',
'#suffix' => '</tbody>',
];
for ($i=0; $i<$form_state->get('num_pricerows'); $i++) {
$form['pt']['table']['rows'][$i] = [
'#type' => 'container',
'#prefix' => '<tr class="pt__row pt__row--'. $i .'">',
'#suffix' => '</tr>',
];
$form['pt']['table']['rows'][$i]['delete'] = [
'#type' => 'checkbox',
'#title' => $this->t('Delete row'),
'#title_display' => 'invisible',
'#prefix' => '<td class="text-align-center">',
'#suffix' => '</td>',
];
$form['pt']['table']['rows'][$i]['copy'] = [
'#type' => 'submit',
'#value' => html_entity_decode('&#xf0c5;'),
'#copy_source' => $i,
'#name' => 'pt[table][rows]['.$i.'][copy]', // @see https://www.drupal.org/node/2546700 - must be unqiue for $form_state->getTriggeringElement()
'#submit' => ['::cbSubmitCopy'],
'#ajax' => [ // @see https://www.drupal.org/project/drupal/issues/2546700#comment-10801702 - must use Ajax for $form_state->getTriggeringElement()
'callback' => '::cbCopyRow',
'wrapper' => 'pt-ajax-wrapper',
],
'#attributes' => ['class' => ['far']],
'#prefix' => '<td class="text-align-center">',
'#suffix' => '</td>',
'#use_button_template' => true,
'#children' => [
'#type' => 'inline_template',
'#template' => $this->iconHelper->getSvgIcon('copy', 'far')
]
];
$form['pt']['table']['rows'][$i]['from'] = [
'#type' => 'date',
'#title' => $this->t('From', [], ['context' => 'time']),
'#title_display' => 'invisible',
'#prefix' => '<td>',
'#suffix' => '</td>',
'#default_value' => is_numeric($copySource) ? $form_state->getValue(['pt', 'table', 'rows', $copySource, 'from']) : @$initialDefaultPrices[$i]['from'],
];
$form['pt']['table']['rows'][$i]['to'] = [
'#type' => 'date',
'#title' => $this->t('To', [], ['context' => 'time']),
'#title_display' => 'invisible',
'#prefix' => '<td>',
'#suffix' => '</td>',
'#default_value' => is_numeric($copySource) ? $form_state->getValue(['pt', 'table', 'rows', $copySource, 'to']) : @$initialDefaultPrices[$i]['to'],
];
$selOptions = [];
foreach ($rates as $id => $rate) {
$selOptions[$id] = $rate->getName();
}
$form['pt']['table']['rows'][$i]['rate'] = [
'#type' => 'select',
'#title_display' => 'invisible',
'#title' => $this->t('Rate', [], ['context' => 'price']),
'#options' => $selOptions,
'#prefix' => '<td>',
'#suffix' => '</td>',
'#default_value' => is_numeric($copySource) ? $form_state->getValue(['pt', 'table', 'rows', $copySource, 'rate']) : @$initialDefaultPrices[$i]['rate'],
];
foreach ($rooms as $room) {
$form['pt']['table']['rows'][$i]['room-' . $room->id()] = [
'#type' => 'number',
'#title_display' => 'invisible',
'#title' => $this->t('Price'),
'#min' => 0,
'#step' => 0.01,
'#prefix' => '<td>',
'#suffix' => '</td>',
'#default_value' => is_numeric($copySource) ? $form_state->getValue(['pt', 'table', 'rows', $copySource, 'room-'.$room->id()]) : @$initialDefaultPrices[$i]['room-'.$room->id()],
];
}
};
$form['add_pricerow'] = [
'#type' => 'submit',
'#value' => t('Add empty row'),
'#submit' => ['::cbSubmitAdd'],
'#ajax' => [
'callback' => '::cbAddRow',
'wrapper' => 'pt-ajax-wrapper',
],
];
$form['submit'] = [
'#value' => t('Update all prices'),
'#type' => 'submit',
'#attributes' => ['class' => ['button', 'button--primary']],
];
return $form;
}
/**
* {@inheritdoc}
*/
public function validateForm(array &$form, FormStateInterface $form_state) {
$uniqueActiveRows = [];
foreach (($form_state->getValue(['pt', 'table', 'rows']) ?? []) as $i => $row) {
$isEmptyRow = TRUE;
foreach ($form_state->getValue(['pt', 'table', 'rows', $i]) as $key => $value) {
if (!in_array($key, ['delete', 'rate', 'copy'])) {
$isEmptyRow = $isEmptyRow && !$value;
}
}
if (!$isEmptyRow && $form_state->getValue(['pt', 'table', 'rows', $i, 'delete']) != "1") {
foreach ($form_state->getValue(['pt', 'table', 'rows', $i]) as $key => $value) {
if ($key != 'delete' && $value == "") {
$form_state->setErrorByName('pt][table][rows]['.$i.']['.$key, $this->t('Required field'));
}
}
if ($form_state->getValue(['pt', 'table', 'rows', $i, 'from']) >= $form_state->getValue(['pt', 'table', 'rows', $i, 'to'])) {
$form_state->setErrorByName('pt][table][rows]['.$i.'][to', $this->t('<em>To</em> must always be greater than <em>From</em>'));
}
if ($form_state->getValue(['pt', 'table', 'rows', $i, 'from']) && $form_state->getValue(['pt', 'table', 'rows', $i, 'to']) && $form_state->getValue(['pt', 'table', 'rows', $i, 'rate'])) {
$uniqueKey = $form_state->getValue(['pt', 'table', 'rows', $i, 'from']) .'___'. $form_state->getValue(['pt', 'table', 'rows', $i, 'to']) .'___'. $form_state->getValue(['pt', 'table', 'rows', $i, 'rate']);
if (in_array($uniqueKey, $uniqueActiveRows)) {
$form_state->setErrorByName('pt][table][rows]['.$i.'][from', $this->t('Row @ROWNUMBER_1 is a duplicate of row @ROWNUMBER_2', ['@ROWNUMBER_1' => $i+1, '@ROWNUMBER_2' => array_search($uniqueKey, $uniqueActiveRows)+1]));
$form_state->setErrorByName('pt][table][rows]['.$i.'][to', $this->t('Row @ROWNUMBER_1 is a duplicate of row @ROWNUMBER_2', ['@ROWNUMBER_1' => $i+1, '@ROWNUMBER_2' => array_search($uniqueKey, $uniqueActiveRows)+1]));
$form_state->setErrorByName('pt][table][rows]['.$i.'][rate', $this->t('Row @ROWNUMBER_1 is a duplicate of row @ROWNUMBER_2', ['@ROWNUMBER_1' => $i+1, '@ROWNUMBER_2' => array_search($uniqueKey, $uniqueActiveRows)+1]));
}
else {
$uniqueActiveRows[$i] = $uniqueKey;
}
}
}
}
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
$query = $this->entityTypeManager->getStorage('node')->getQuery();
$query->condition('type', 'wt_room');
$query->sort('title', 'ASC');
$roomIds = $query->execute();
$rooms = $this->entityTypeManager->getStorage('node')->loadMultiple($roomIds);
$query = $this->entityTypeManager->getStorage('taxonomy_term')->getQuery();
$query->condition('vid', 'rate');
$query->sort('weight', 'ASC');
$rateIds = $query->execute();
$rates = $this->entityTypeManager->getStorage('taxonomy_term')->loadMultiple($rateIds);
$ignoreMinprice = [];
foreach ($rates as $rate) {
$ignoreMinprice[$rate->id()] = $rate->get('field_ignore_minprice')->value ?? '0';
}
$query = $this->entityTypeManager->getStorage('paragraph')->getQuery();
$query->condition('type', 'pricetable');
$query->condition('parent_id', array_keys($rooms), 'IN');
$priceIds = $query->execute();
$prices = $this->entityTypeManager->getStorage('paragraph')->loadMultiple($priceIds);
$this->entityTypeManager->getStorage('paragraph')->delete($prices);
$today = date('Y-m-d');
foreach ($rooms as $room) {
$priceHostField=[];
$minPrice = NULL;
foreach ($form_state->getValue(['pt', 'table', 'rows']) as $i => $row) {
$from = $form_state->getValue(['pt', 'table', 'rows', $i, 'from']);
$to = $form_state->getValue(['pt', 'table', 'rows', $i, 'to']);
$rate = $form_state->getValue(['pt', 'table', 'rows', $i, 'rate']);
$price = $form_state->getValue(['pt', 'table', 'rows', $i, 'room-'.$room->id()]);
if (
$form_state->getValue(['pt', 'table', 'rows', $i, 'delete']) != "1" &&
$from && $to && is_numeric($price) && $rate
) {
$paragraph = Paragraph::create([
'type' => 'pricetable',
'field_pt_from' => $from,
'field_pt_to' => $to,
'field_pt_rate' => ['target_id' => $rate],
'field_pt_price' => $price,
]);
$paragraph->save();
if ($ignoreMinprice[$rate] != 1 && $to >= $today && $price > 0) {
if (is_null($minPrice) || $price < $minPrice)
$minPrice = $price;
}
$priceHostField[] = [
'target_id' => $paragraph->id(),
'target_revision_id' => $paragraph->getRevisionId(),
];
}
}
if (\Drupal::configFactory()->getEditable('wt_pricetable.settings')->get('update_minprice') == 1) {
$room->set('field_minprice', $minPrice);
}
$room->set('field_pricetable', $priceHostField);
$room->save();
}
if (\Drupal::configFactory()->getEditable('wt_pricetable.settings')->get('update_minprice') == 1) {
$this->messenger()->addStatus($this->t('All prices tables updated and <em>from price</em> in all rooms adjusted.'));
}
else {
$this->messenger()->addStatus($this->t('All prices updated.'));
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment