From ad73361bec3d8fc6ce8a52b6b90a6e191f573bfe Mon Sep 17 00:00:00 2001 From: John Voskuilen <john.voskuilen@sapito.nl> Date: Mon, 25 Mar 2024 16:15:55 +0100 Subject: [PATCH] Issue #2926094: Move to_sid and comment fields to widget --- src/Element/WorkflowTransitionElement.php | 76 +++++++++-------- src/Entity/WorkflowManager.php | 2 +- src/Entity/WorkflowTransition.php | 51 +++++------ src/Plugin/Field/FieldType/WorkflowItem.php | 5 +- .../FieldWidget/WorkflowDefaultWidget.php | 85 +++++++++++++------ workflow.api.php | 24 ++---- 6 files changed, 136 insertions(+), 107 deletions(-) diff --git a/src/Element/WorkflowTransitionElement.php b/src/Element/WorkflowTransitionElement.php index f8f4456d..d40e5455 100644 --- a/src/Element/WorkflowTransitionElement.php +++ b/src/Element/WorkflowTransitionElement.php @@ -177,7 +177,7 @@ class WorkflowTransitionElement extends FormElement { // Save the current value of the entity in the form, for later Workflow-module specific references. // We add prefix, since #tree == FALSE. - $element['workflow_transition'] = [ + $element['_workflow_transition'] = [ '#type' => 'value', '#value' => $transition, ]; @@ -239,7 +239,9 @@ class WorkflowTransitionElement extends FormElement { // It may be replaced later if 'Action buttons' are chosen. // This overrides BaseFieldDefinition. @todo Apply for form and widget. // @todo Repair $workflow->'name_as_title': no container if no details (schedule/comment). - $element['to_sid'] = [ + $attribute_name = 'to_sid'; + $attribute_key = 'target_id'; + $element[$attribute_name]['widget'][0][$attribute_key] = [ // Avoid error with grouped options when workflow not set. '#type' => ($wid) ? $options_type : 'select', '#title' => (!$workflow_settings['name_as_title'] && !$transition->isExecuted()) @@ -249,8 +251,12 @@ class WorkflowTransitionElement extends FormElement { '#access' => TRUE, '#options' => $options, '#default_value' => $default_value, - '#weight' => $field_weight, - ]; + // '#weight' => $field_weight, + // Remove autocomplete settings from BaseFieldDefinitions(). + '#maxlength' => 999, + '#size' => 0, + ] + $element[$attribute_name]['widget'][0][$attribute_key]; + if (_workflow_use_action_buttons($options_type)) { // In WorkflowTransitionForm, a default 'Submit' button is added there. // In Entity Form, workflow_form_alter() adds button per permitted state. @@ -260,29 +266,32 @@ class WorkflowTransitionElement extends FormElement { // It will be replaced by action buttons, but sometimes, the select box // is still shown. // @see workflow_form_alter(). - $element['to_sid']['#type'] = 'select'; - $element['to_sid']['#access'] = FALSE; + $element[$attribute_name]['widget'][0][$attribute_key] = [ + '#type' => 'select', + '#access' => FALSE, + ] + $element[$attribute_name]['widget'][0][$attribute_key]; } // Display scheduling form under certain conditions. $element['timestamp']['widget'][0]['value'] = [ '#type' => 'workflow_transition_timestamp', '#default_value' => $transition, - ] + ($element['timestamp']['widget'][0]['value'] ?? []); + ] + $element['timestamp']['widget'][0]['value']; $element['timestamp']['#weight'] = $field_weight; // Show comment, when both Field and Instance allow this. - // This overrides BaseFieldDefinition. @todo Apply for form and widget. - $element['comment'] = [ - '#type' => 'textarea', + // This overrides BaseFieldDefinition. + $attribute_name = 'comment'; + $attribute_key = 'value'; + $element[$attribute_name]['widget'][0][$attribute_key] = [ + // '#type' => 'textarea', '#title' => t('Comment'), '#description' => t('Briefly describe the changes you have made.'), '#access' => $workflow_settings['comment_log_node'] != '0', // Align with action buttons. '#default_value' => $transition->getComment(), '#required' => $workflow_settings['comment_log_node'] == '2', - '#rows' => 2, - '#weight' => $field_weight, - ]; + '#rows' => 2, //@todo Use correct field setting UI. + ] + $element[$attribute_name]['widget'][0][$attribute_key]; $element['force'] = [ '#type' => 'checkbox', @@ -332,34 +341,33 @@ class WorkflowTransitionElement extends FormElement { */ public static function copyFormValuesToTransition(EntityInterface $transition, array $form, FormStateInterface $form_state, array $item) { /** @var \Drupal\workflow\Entity\WorkflowTransitionInterface $transition */ - // $values = $form_state->getValues(); $values = $item; $input = $form_state->getUserInput(); - if (!isset($values['to_sid'])) { - $entity_id = $transition->getTargetEntityId(); - \Drupal::messenger()->addError(t('Error: content @id has no workflow attached. The data is not saved.', ['@id' => $entity_id])); - // The new state is still the previous state. - return $transition; - } - // @todo #2287057: verify if submit() really is only used for UI. If not, $user must be passed. $user = workflow_current_user(); // Get user input from element. $field_name = $transition->getFieldName(); $uid = $user->id(); - $timestamp = \Drupal::time()->getRequestTime(); $force = FALSE; // Read value from form input, else widget values. - $action_info = _workflow_transition_form_get_triggering_button($form_state); - $to_sid = $action_info['to_sid'] ?? $values['to_sid']; + $action_values = _workflow_transition_form_get_triggering_button($form_state); + $to_sid = $action_values['to_sid'] ?? $values['to_sid'][0]['target_id']; // Note: when editing existing Transition, user may still change comments. - $comment = $input['comment'] ?? $input[$field_name][0]['comment'] ?? ''; - $timestamp_widget = $values['timestamp'][0]['value'] ?? $values['timestamp']['widget'][0]['value']; - $is_scheduled = (bool) $timestamp_widget['scheduled']; - $timestamp = WorkflowTransitionTimestamp::valueCallback($timestamp_widget, $timestamp_widget, $form_state); + $comment = $input['comment'][0]['value'] ?? ''; + $timestamp_values = $values['timestamp'][0]['value']; + $is_scheduled = (bool) $timestamp_values['scheduled']; + $timestamp = WorkflowTransitionTimestamp::valueCallback($timestamp_values, $timestamp_values, $form_state) + ?? \Drupal::time()->getRequestTime(); + + if (!isset($to_sid)) { + $entity_id = $transition->getTargetEntityId(); + \Drupal::messenger()->addError(t('Error: content @id has no workflow attached. The data is not saved.', ['@id' => $entity_id])); + // The new state is still the previous state. + return $transition; + } // @todo D8: add the VBO use case. /* @@ -402,6 +410,10 @@ class WorkflowTransitionElement extends FormElement { $transition->force($force); } + // Also add the transition to the ItemList. + $entity = $transition->getTargetEntity(); + $entity->{$field_name}->__set('_workflow_transition', $transition); + // Add the attached fields. // Caveat: This works automatically on a Workflow Form, // but only with a hack on a widget. @@ -411,15 +423,9 @@ class WorkflowTransitionElement extends FormElement { foreach ($fields as $field_name => $field) { $user_input = $input[$field_name] ?? []; if (isset($values[$field_name])) { - // On Workflow Form (e.g., history tab, block). - // @todo In latest tests, this line seems not necessary. + // @todo This line seems necessary for node edit, not for node view. $transition->{$field_name} = $values[$field_name]; } - elseif ($user_input) { - // On Workflow Widget (e.g., on node, comment). - // @todo Some field types are not supported here. - $transition->{$field_name} = $user_input; - } // #2899025 For each field, let other modules modify the copied values, // as a workaround for not-supported field types. diff --git a/src/Entity/WorkflowManager.php b/src/Entity/WorkflowManager.php index 583fec40..4aae3985 100644 --- a/src/Entity/WorkflowManager.php +++ b/src/Entity/WorkflowManager.php @@ -154,7 +154,7 @@ class WorkflowManager implements WorkflowManagerInterface { foreach ($field_names as $field_name) { /** @var \Drupal\workflow\Entity\WorkflowTransitionInterface $transition */ // @todo Transition is created in widget or WorkflowTransitionForm. - $transition = $entity->{$field_name}->__get('_workflow_transition') ?? NULL; // @todo #default_value. + $transition = $entity->{$field_name}->__get('_workflow_transition') ?? NULL; if (!$transition) { // We come from creating/editing an entity via entity_form, with core widget or hidden Workflow widget. // @todo D8: From an Edit form with hidden widget. diff --git a/src/Entity/WorkflowTransition.php b/src/Entity/WorkflowTransition.php index 2e240e90..47947ff3 100644 --- a/src/Entity/WorkflowTransition.php +++ b/src/Entity/WorkflowTransition.php @@ -148,9 +148,8 @@ class WorkflowTransition extends ContentEntityBase implements WorkflowTransition * * @see entity_create() */ - public function __construct(array $values = [], $entityType = 'workflow_transition', $bundle = FALSE, array $translations = []) { - // Please be aware that $entity_type and $entityType are different things! - parent::__construct($values, $entityType, $bundle, $translations); + public function __construct(array $values = [], $entity_type_id = 'workflow_transition', $bundle = FALSE, array $translations = []) { + parent::__construct($values, $entity_type_id, $bundle, $translations); $this->eventDispatcher = \Drupal::service('event_dispatcher'); // This transition is not scheduled. $this->isScheduled = FALSE; @@ -329,9 +328,9 @@ class WorkflowTransition extends ContentEntityBase implements WorkflowTransition /** * {@inheritdoc} */ - public static function loadByProperties($entity_type, $entity_id, array $revision_ids = [], $field_name = '', $langcode = '', $sort = 'ASC', $transition_type = 'workflow_transition') { + public static function loadByProperties($entity_type_id, $entity_id, array $revision_ids = [], $field_name = '', $langcode = '', $sort = 'ASC', $transition_type = 'workflow_transition') { $limit = 1; - $transitions = self::loadMultipleByProperties($entity_type, [$entity_id], $revision_ids, $field_name, $langcode, $limit, $sort, $transition_type); + $transitions = self::loadMultipleByProperties($entity_type_id, [$entity_id], $revision_ids, $field_name, $langcode, $limit, $sort, $transition_type); if ($transitions) { $transition = reset($transitions); return $transition; @@ -342,11 +341,11 @@ class WorkflowTransition extends ContentEntityBase implements WorkflowTransition /** * {@inheritdoc} */ - public static function loadMultipleByProperties($entity_type, array $entity_ids, array $revision_ids = [], $field_name = '', $langcode = '', $limit = NULL, $sort = 'ASC', $transition_type = 'workflow_transition') { + public static function loadMultipleByProperties($entity_type_id, array $entity_ids, array $revision_ids = [], $field_name = '', $langcode = '', $limit = NULL, $sort = 'ASC', $transition_type = 'workflow_transition') { /** @var \Drupal\Core\Entity\Query\QueryInterface $query */ $query = \Drupal::entityQuery($transition_type) - ->condition('entity_type', $entity_type) + ->condition('entity_type', $entity_type_id) ->accessCheck(FALSE) ->sort('timestamp', $sort) ->addTag($transition_type); @@ -718,9 +717,9 @@ class WorkflowTransition extends ContentEntityBase implements WorkflowTransition // @todo D8: the following line only returns Node, not Term. /* return $this->entity = $this->get('entity_id')->entity; */ - $entity_type = $this->getTargetEntityTypeId(); + $entity_type_id = $this->getTargetEntityTypeId(); if ($id = $this->getTargetEntityId()) { - $this->entity = \Drupal::entityTypeManager()->getStorage($entity_type)->load($id); + $this->entity = \Drupal::entityTypeManager()->getStorage($entity_type_id)->load($id); } return $this->entity; } @@ -853,9 +852,6 @@ class WorkflowTransition extends ContentEntityBase implements WorkflowTransition * {@inheritdoc} */ public function schedule($schedule = TRUE) { - // We do a tricky thing here. The id of the entity is altered, so - // all functions of another subclass are called. - // $this->entityTypeId = ($schedule) ? 'workflow_scheduled_transition' : 'workflow_transition'; $this->isScheduled = $schedule; return $this; } @@ -1034,12 +1030,11 @@ class WorkflowTransition extends ContentEntityBase implements WorkflowTransition ->setLabel(t('To state')) ->setDescription(t('The {workflow_states}.sid the entity transitioned to.')) ->setSetting('target_type', 'workflow_state') -// @todo D8: Activate this. Test with both Form and Widget. -// ->setDisplayOptions('form', [ -// 'type' => 'select', + ->setDisplayOptions('form', [ + 'type' => 'select', // 'weight' => -5, -// ]) -// ->setDisplayConfigurable('form', TRUE) + ]) + ->setDisplayConfigurable('form', TRUE) ->setReadOnly(TRUE); $fields['uid'] = BaseFieldDefinition::create('entity_reference') @@ -1084,7 +1079,7 @@ class WorkflowTransition extends ContentEntityBase implements WorkflowTransition 'type' => 'workflow_transition_timestamp', // 'type' => 'datetime_timestamp', // 'label' => 'hidden', - 'weight' => -100, +// 'weight' => -100, ]) ->setDisplayConfigurable('form', TRUE) ->setRevisionable(TRUE); @@ -1093,16 +1088,16 @@ class WorkflowTransition extends ContentEntityBase implements WorkflowTransition ->setLabel(t('Log message')) ->setDescription(t('The comment explaining this transition.')) ->setRevisionable(TRUE) - ->setTranslatable(TRUE); -// @todo D8: activate this. Test with both Form and Widget. -// ->setDisplayOptions('form', [ -// 'type' => 'string_textarea', -// 'weight' => 25, -// 'settings' => [ -// 'rows' => 4, -// ], -// ]) -// ->setDisplayConfigurable('form', FALSE); + ->setTranslatable(TRUE) + ->setDisplayOptions('form', [ + 'type' => 'textarea', + // 'weight' => 25, + 'settings' => [ + //@todo: Why shows 'Manage fields' 5 rows in the beginning, not 2? + 'rows' => 2, + ], + ]) + ->setDisplayConfigurable('form', TRUE); return $fields; } diff --git a/src/Plugin/Field/FieldType/WorkflowItem.php b/src/Plugin/Field/FieldType/WorkflowItem.php index 334c761b..f604c313 100644 --- a/src/Plugin/Field/FieldType/WorkflowItem.php +++ b/src/Plugin/Field/FieldType/WorkflowItem.php @@ -64,8 +64,9 @@ class WorkflowItem extends ListItemBase { * @var array */ static $propertyDefinitions; - - $definition['settings']['target_type'] = 'workflow_transition'; + // Use underscore to avoid confusion with EntityTypeId. + // @todo Better use 'target_transition'. + $definition['settings']['target_type'] = '_workflow_transition'; // Definitions vary by entity type and bundle, so key them accordingly. $key = $definition['settings']['target_type'] . ':'; $key .= $definition['settings']['target_bundle'] ?? ''; diff --git a/src/Plugin/Field/FieldWidget/WorkflowDefaultWidget.php b/src/Plugin/Field/FieldWidget/WorkflowDefaultWidget.php index ed951328..e1a20132 100644 --- a/src/Plugin/Field/FieldWidget/WorkflowDefaultWidget.php +++ b/src/Plugin/Field/FieldWidget/WorkflowDefaultWidget.php @@ -5,9 +5,11 @@ namespace Drupal\workflow\Plugin\Field\FieldWidget; use Drupal\Core\Field\FieldItemListInterface; use Drupal\Core\Field\WidgetBase; use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Render\Element; use Drupal\workflow\Element\WorkflowTransitionElement; use Drupal\workflow\Entity\Workflow; use Drupal\workflow\Entity\WorkflowManager; +use Drupal\workflow\Entity\WorkflowTransition; /** * Plugin implementation of the 'workflow_default' widget. @@ -56,41 +58,74 @@ class WorkflowDefaultWidget extends WidgetBase { $field_name = $field_storage->getName(); $transition = WorkflowManager::getDefaultTransition($entity, $field_name); - // Here, on entity form, not the $element is added, but the entity form. - // Problem 1: adding the element, does not add added fields. - // Problem 2: adding the form, generates wrong UI. - // Problem 3: does not work on ScheduledTransition. - // - // Step 1: use the Element. - $element['#default_value'] = $transition; - $element = WorkflowTransitionElement::transitionElement($element, $form_state, $form); - - // Step 2: use the Form, in order to get extra fields. + // To prepare the widget, use the Form, in order to get extra fields. $form_state_additions = [ 'input' => $form_state->getUserInput(), 'values' => $form_state->getValues(), 'triggering_element' => $form_state->getTriggeringElement(), ]; - $workflow_form ??= WorkflowManager::getWorkflowTransitionForm($entity, $field_name, $form_state_additions); + $workflow_form = WorkflowManager::getWorkflowTransitionForm($entity, $field_name, $form_state_additions); + // Determine and add the attached fields. - // @todo Option 2: remove above Step 1 and use this form? $attached_fields = WorkflowManager::getAttachedFields('workflow_transition', $wid); - foreach ($attached_fields as $key => $attached_field) { - $workflow_form ??= WorkflowManager::getWorkflowTransitionForm($entity, $field_name, $form_state_additions); - $element[$key] = $workflow_form[$key] ?? NULL; + // Then, remove all form elements, keep widget elements. + $base_fields = WorkflowTransition::baseFieldDefinitions($transition->getEntityType()); + $fields = $attached_fields + $base_fields + [ + '_workflow_transition' => '_workflow_transition', + 'force' => 'force', + ]; + foreach (Element::children($workflow_form) as $attribute_name) { + if (array_key_exists($attribute_name, $fields)) { + $element[$attribute_name] ??= $workflow_form[$attribute_name]; + } } - - // Option 3: use the true Element. - // $form = $this->element($form, $form_state, $transition); - // $element['workflow_transition'] = [ - // '#type' => 'workflow_transition', - // '#title' => $this->t('Workflow transition'), - // '#default_value' => $transition, - // ]; + // The following are not in Element::children. + $element['#default_value'] = $workflow_form['#default_value']; + // Overwrite value set by Form. + $element['#type'] = 'details'; + $element['#collapsible'] = $workflow_form['#collapsible']; + $element['#open'] = $workflow_form['#open']; return $element; } + /** + * {@inheritdoc} + */ + public function extractFormValues(FieldItemListInterface $items, array $form, FormStateInterface $form_state) { + // parent::extractFormValues($items, $form, $form_state); + // Override WidgetBase::extractFormValues() since + // it extracts field values without respecting #tree = TRUE. + // So, the following function massageFormValues has nothing to do. + $field_name = $this->fieldDefinition->getName(); + $transition = $form_state->getValue('_workflow_transition'); + + $values = ['transition' => + // $form_state->getValues() + $form_state->getUserInput() + + ['#default_value' => $transition], + ]; + $key_exists = TRUE; + + if ($key_exists) { + // Let the widget massage the submitted values. + $values = $this->massageFormValues($values, $form, $form_state); + + // Get the new value from an action button if set in the workflow settings. + $action_info = _workflow_transition_form_get_triggering_button($form_state); + if ($field_name == $action_info['field_name']) { + // $values = [0 => $action_info['to_sid']]; + } + + // Assign the values and remove the empty ones. + $items->setValue($values['transition']['value']); + $items->filterEmptyItems(); + // Also add the transition to the ItemList. + $items->__set('_workflow_transition', $transition); + } + } + + /** * {@inheritdoc} * @@ -106,7 +141,6 @@ class WorkflowDefaultWidget extends WidgetBase { * @todo Remove update of {node_form} table. (separate task, because it has features, too.) */ public function massageFormValues(array $values, array $form, FormStateInterface $form_state) { - // @todo #2287057: verify if submit() really is only used for UI. // If not, $user must be passed. $user = workflow_current_user(); @@ -120,7 +154,7 @@ class WorkflowDefaultWidget extends WidgetBase { if (!empty($item)) { // Use a proprietary version of copyFormValuesToEntity(). /** @var \Drupal\workflow\Entity\WorkflowTransitionInterface $transition */ - $transition = $item['workflow_transition']; + $transition = $item['#default_value']; $transition = WorkflowTransitionElement::copyFormValuesToTransition($transition, $form, $form_state, $item); // Try to execute the transition. Return $from_sid when error. @@ -177,7 +211,6 @@ class WorkflowDefaultWidget extends WidgetBase { // - WorkflowDefaultWidget::massageFormValues(); // - WorkflowManager::executeTransition(). // Set the transition back, to be used in hook_entity_update(). - $item['workflow_transition'] = $transition; // Set the value at the proper location. if ($transition && $transition->isScheduled()) { $item['value'] = $from_sid; diff --git a/workflow.api.php b/workflow.api.php index 87196d27..429b7872 100644 --- a/workflow.api.php +++ b/workflow.api.php @@ -247,18 +247,16 @@ function hook_workflow_permitted_state_transitions_alter(array &$transitions, ar * Better use hook_form_workflow_transition_form_alter. */ function hook_field_widget_single_element_workflow_default_form_alter(&$element, FormStateInterface $form_state, $context) { - // workflow_debug(__FILE__, __FUNCTION__, __LINE__, $context['widget']->getPluginId(), ''); // A hook specific for the 'workflow_default' widget. // The name is specified in the annotation of WorkflowDefaultWidget. - workflow_debug(__FILE__, __FUNCTION__, __LINE__, '', ''); // A widget on an entity form. if ('workflow_default' != $context['widget']->getPluginId()) { // This can never happen. return; } - // The Transition object contains all you need. - // You may find it in one of two locations. + + // The $transition object contains all you need. /** @var \Drupal\workflow\Entity\WorkflowTransitionInterface $transition */ $transition = $element['#default_value'] ?? NULL; if (!$transition) { @@ -266,13 +264,13 @@ function hook_field_widget_single_element_workflow_default_form_alter(&$element, } // An example of customizing/overriding the workflow widget. - // Beware, until now, you must do this twice: on the widget and on the form. + // Beware: until now, you must do this twice: on the widget and on the form. $uid = $transition->getOwnerId(); if ($uid == 10) { \Drupal::messenger()->addWarning('(Test/Devel message) For you, user 1, the scheduling is disabled, and commenting is required.'); // Let us prohibit scheduling for user 1. - $element['workflow_scheduling']['#access'] = FALSE; + $element['timestamp']['#access'] = FALSE; // Let us require commenting for user 1. if ($element['comment']['#access'] ?? FALSE) { $element['comment']['#required'] = TRUE; @@ -289,19 +287,15 @@ function hook_field_widget_single_element_workflow_default_form_alter(&$element, * hook_form_alter(). See below for more info. */ function hook_form_workflow_transition_form_alter(&$form, FormStateInterface $form_state, $form_id) { - // workflow_debug(__FILE__, __FUNCTION__, __LINE__, $form_id, ''); - - // The WorkflowTransitionForm (E.g., Workflow History tab, Block). - // It has its own handling. - // @todo Fill WorkflowTransitionForm with Widget, to have 1 way-of-working. - // This object contains all you need. You may find it in one of two locations. + // The $transition object contains all you need. /** @var \Drupal\workflow\Entity\WorkflowTransitionInterface $transition */ - $transition = $form['workflow_transition']['#value']; + $transition = $form['#default_value']; + // An example of customizing/overriding the workflow widget. - // Beware, until now, you must do this twice: on the widget and on the form. + // Beware: until now, you must do this twice: on the widget and on the form. $uid = $transition->getOwnerId(); - if ($uid == 1) { + if ($uid == 10) { \Drupal::messenger()->addWarning('(Test/Devel message) For you, user 1, the scheduling is disabled, and commenting is required.'); // Let us prohibit scheduling for user 1. -- GitLab