workflow-8.x-1.x-dev/src/Element/WorkflowTransitionButtons.php
src/Element/WorkflowTransitionButtons.php
<?php
namespace Drupal\workflow\Element;
use Drupal\Component\Utility\Html;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Render\Element;
use Drupal\workflow\Entity\WorkflowTransitionInterface;
/**
* Provides a form element replacement for the Action Buttons/Dropbutton.
*
* Properties:
* - #return_value: The value to return when the checkbox is checked.
*
* @see \Drupal\Core\Render\Element\FormElementBase
* @see https://www.drupal.org/node/169815 "Creating Custom Elements"
*/
class WorkflowTransitionButtons {
/**
* Fetches the first workflow_element from one of the Workflow fields.
*
* @param array $form
* The form.
*
* @return array
* The workflow element, or empty array.
*/
public static function getFirstWorkflowElement($form): array {
// Find the first workflow.
// (So this won't work with multiple workflows per entity.)
$transition = $form['#workflow_transition'] ?? NULL;
if ($transition instanceof WorkflowTransitionInterface) {
// We are on the workflow_transition_form.
return $form;
}
// We are on node edit page. First fetch the field between the others.
$workflow_element = [];
foreach (Element::children($form) as $key) {
// Note: Align ['#parents'] in Widget::form...(), Form::copy..(), ...
$transition = $form[$key]['widget'][0]['#workflow_transition'] ?? NULL;
if ($transition instanceof WorkflowTransitionInterface) {
// Note: If a transition is found, then $key will be $field_name.
$workflow_element = $form[$key]['widget'][0];
break;
}
}
return $workflow_element;
}
/**
* Get the Workflow parameter from the button, pressed by the user.
*
* @param \Drupal\workflow\Entity\WorkflowTransitionInterface $transition
* The workflow transition.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The form state.
* @param array $values
* A list of values.
*
* @return array
* A $field_name => $to_sid array.
*/
public static function getTriggeringButton(WorkflowTransitionInterface $transition, FormStateInterface $form_state, array $values): array {
$field_name = $transition->getFieldName();
$result = ['field_name' => NULL, 'to_sid' => NULL];
if (WorkflowTransitionButtons::useActionButtons()) {
// Add this to avoid error in edge case in FormBuilder::doBuildForm().
// @see https://www.drupal.org/project/workflow/issues/3513418#comment-16049435
// $form_state->setProgrammed(TRUE);
$buttons = $form_state->getButtons();
// Note: $form_state elements must be added to $new_form_state earlier.
if (!$form_state->isProgrammed()
&& !$form_state->getTriggeringElement()
&& !empty($buttons)) {
// $form_state->setTriggeringElement($buttons[0]); // @todo?
}
$triggering_element = $form_state->getTriggeringElement();
if (isset($triggering_element['#workflow'])) {
// This is a Workflow action button/dropbutton.
$result['field_name'] = $triggering_element['#workflow']['field_name'];
$result['to_sid'] = $triggering_element['#workflow']['to_sid'];
}
else {
// Other SubmitForm, or Ajax AddMore/Remove/Upload-widget button.
}
}
elseif ($input = $form_state->getUserInput()) {
// This is a normal Save button or another button like 'File upload'.
// Field_name may not exist due to '#access' = FALSE.
$result['field_name'] = $input['field_name'] ?? NULL;
// 'to_sid' is taken from the Workflow widget, not from the button.
$result['to_sid'] ??= $input['to_sid'] ?? NULL;
}
// Try to get new State ID from a value.
$result['to_sid'] ??= $values['to_sid'][0]['value'] ?? NULL;
// A 3rd party button is hit (e.g., File upload field), get default value.
$result['field_name'] ??= $transition->getFieldName();
$result['to_sid'] ??= $transition->getToSid();
return $result;
}
/**
* Returns the action buttons from the options widget.
*
* @param array $form
* The form. The list of buttons is updated upon return.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The form state.
*
* @return array
* The (updated) action sub-element, just FYI, since $form is updated, too.
*/
public static function addActionButtons(array &$form, FormStateInterface $form_state): array {
// Get the list of default buttons.
$actions = &$form['actions'];
if (!$actions) {
// Sometimes, no actions are defined. Discard this form.
return $actions ?? [];
}
// Use a fast, custom way to check if we need to do this.
// @todo Make this work with multiple workflows per entity.
if (!WorkflowTransitionButtons::useActionButtons()) {
return $actions;
}
// Find the first workflow. Quit if there is no Workflow on this page.
// @todo Support multiple workflows per entity.
$workflow_element = WorkflowTransitionButtons::getFirstWorkflowElement($form);
if (!$workflow_element) {
return $actions;
}
// Get the options. They will be converted to buttons.
// Quit if there are no options / Workflow Action buttons.
// Also show Action buttons if user has only 1 option. The transition label
// overwrites the default 'Save' or 'Update workflow' button label.
// Note: Align ['#parents'] in Widget::form...(), Form::copy..(), ...
$to_sid_widget = $workflow_element['to_sid']['widget'];
$options = $to_sid_widget['#options'] ?? [];
if (count($options) == 0) {
return $actions;
}
/** @var \Drupal\workflow\Entity\WorkflowTransitionInterface $transition */
$transition = $workflow_element['#workflow_transition'];
$field_name = $transition->getFieldName();
$current_sid = $to_sid_widget['#default_value'];
// Find the default submit button and replace with our own action buttons.
$default_submit_action = [];
$default_submit_action = $actions['save'] ?? $default_submit_action;
$default_submit_action = $actions['submit'] ?? $default_submit_action;
// Find the default submit button and add our action buttons before it.
// Get the min weight for our buttons.
$option_weight = $default_submit_action['#weight'] ?? 0;
$option_weight -= count($options);
$min_weight = $option_weight;
// Add the new submit buttons next to/below the default submit buttons.
foreach ($options as $sid => $option_name) {
// Make the workflow button act exactly like the original submit button.
$same_state_button = ($sid == $current_sid);
$workflow_submit_action = $default_submit_action;
// Add target State ID and Field name,
// to set correct value in validate_buttons callback.
$workflow_submit_action['#workflow'] = [
'field_name' => $field_name,
'to_sid' => $sid,
];
// Keep option order. Put current state first.
$workflow_submit_action['#weight'] = ($same_state_button) ? $min_weight : ++$option_weight;
// Add/Overwrite some other settings.
$workflow_submit_action['#value'] = $option_name;
$workflow_submit_action['#access'] = TRUE;
$workflow_submit_action['#button_type'] = '';
// $workflow_submit_action['#executes_submit_callback'] = TRUE;
$workflow_submit_action['#attributes'] = ($same_state_button) ? ['class' => ['form-save-default-button']] : [];
$workflow_submit_action['#attributes']['class'][] = Html::getClass("workflow_button_$option_name");
// Append the form's #validate function, to called upon submit,
// because the workflow buttons have its own #validate.
$workflow_submit_action['#validate'] ??= $form['#validate'] ?? NULL;
// Append the submit-buttons's #submit function,
// or it won't be called upon submit.
$workflow_submit_action['#submit'] ??= $form['#submit'] ?? NULL;
// #3458569 Disable 'gin' Gin Admin theme's 'More actions' button.
$workflow_submit_action['#gin_action_item'] = TRUE;
// Use one drop button, instead of several action buttons.
if ('dropbutton' == WorkflowTransitionButtons::useActionButtons()) {
$workflow_submit_action['#dropbutton'] = 'save';
$workflow_submit_action['#button_type'] = '';
}
// Add the same-state button, hide in some cases.
if ($same_state_button) {
$workflow_submit_action['#button_type'] = ($same_state_button) ? 'primary' : '';
$workflow_submit_action['#attributes'] = ($same_state_button) ? ['class' => ['form-save-default-button']] : [];
$workflow_submit_action['#attributes']['class'][] = Html::getClass("workflow_button_$option_name");
$actions["workflow_$sid"] = $workflow_submit_action;
}
else {
// Add the new state button.
$actions["workflow_$sid"] = $workflow_submit_action;
}
}
unset($actions['submit']);
unset($actions['save']);
return $actions;
}
/**
* Getter/Setter to tell if/which action buttons are used.
*
* @param string $button_type
* If empty, the current button type is returned,
* if not empty, the button type is set to input value.
*
* @return string
* Previous value. If 'dropbutton'||'buttons', action buttons to be created.
*
* @see workflow_form_alter()
* @see WorkflowDefaultWidget::formElement()
*
* Used to save some expensive operations on every form.
*/
public static function useActionButtons($button_type = ''): string {
global $_workflow_action_button_type;
$_workflow_action_button_type = match ($button_type) {
// Getting, not setting.
'' => $_workflow_action_button_type ?? '',
// Setting button type.
'dropbutton' => $button_type,
'buttons' => $button_type,
// Setting any other (non-button) type.
default => '',
};
return $_workflow_action_button_type;
}
}
