You are here

class JobItemForm in Translation Management Tool 8

Form controller for the job item edit forms.

Hierarchy

Expanded class hierarchy of JobItemForm

Related topics

File

src/Form/JobItemForm.php, line 27

Namespace

Drupal\tmgmt\Form
View source
class JobItemForm extends TmgmtFormBase {

  /**
   * @var \Drupal\tmgmt\JobItemInterface
   */
  protected $entity;

  /**
   * Overrides Drupal\Core\Entity\EntityForm::form().
   */
  public function form(array $form, FormStateInterface $form_state) {
    $form = parent::form($form, $form_state);
    $item = $this->entity;
    $form['#title'] = $this
      ->t('Job item @source_label', array(
      '@source_label' => $item
        ->getSourceLabel(),
    ));
    $form['info'] = array(
      '#type' => 'container',
      '#attributes' => array(
        'class' => array(
          'tmgmt-ui-job-info',
          'clearfix',
        ),
      ),
      '#weight' => 0,
    );
    $url = $item
      ->getSourceUrl();
    $form['info']['source'] = array(
      '#type' => 'item',
      '#title' => t('Source'),
      '#markup' => $url ? Link::fromTextAndUrl($item
        ->getSourceLabel(), $url)
        ->toString() : $item
        ->getSourceLabel(),
      '#prefix' => '<div class="tmgmt-ui-source tmgmt-ui-info-item">',
      '#suffix' => '</div>',
    );
    $form['info']['sourcetype'] = array(
      '#type' => 'item',
      '#title' => t('Source type'),
      '#markup' => $item
        ->getSourceType(),
      '#prefix' => '<div class="tmgmt-ui-source-type tmgmt-ui-info-item">',
      '#suffix' => '</div>',
    );
    $form['info']['source_language'] = array(
      '#type' => 'item',
      '#title' => t('Source language'),
      '#markup' => $item
        ->getJob()
        ->getSourceLanguage()
        ->getName(),
      '#prefix' => '<div class="tmgmt-ui-source-language tmgmt-ui-info-item">',
      '#suffix' => '</div>',
    );
    $form['info']['target_language'] = array(
      '#type' => 'item',
      '#title' => t('Target language'),
      '#markup' => $item
        ->getJob()
        ->getTargetLanguage()
        ->getName(),
      '#prefix' => '<div class="tmgmt-ui-target-language tmgmt-ui-info-item">',
      '#suffix' => '</div>',
    );
    $form['info']['changed'] = array(
      '#type' => 'item',
      '#title' => t('Last change'),
      '#value' => $item
        ->getChangedTime(),
      '#markup' => $this->dateFormatter
        ->format($item
        ->getChangedTime()),
      '#prefix' => '<div class="tmgmt-ui-changed tmgmt-ui-info-item">',
      '#suffix' => '</div>',
    );
    $states = JobItem::getStates();
    $form['info']['state'] = array(
      '#type' => 'item',
      '#title' => t('State'),
      '#markup' => $states[$item
        ->getState()],
      '#prefix' => '<div class="tmgmt-ui-item-state tmgmt-ui-info-item">',
      '#suffix' => '</div>',
      '#value' => $item
        ->getState(),
    );
    $job = $item
      ->getJob();
    $form['info']['job'] = array(
      '#type' => 'item',
      '#title' => t('Job'),
      '#markup' => $job
        ->toLink()
        ->toString(),
      '#prefix' => '<div class="tmgmt-ui-job tmgmt-ui-info-item">',
      '#suffix' => '</div>',
    );

    // Display selected translator for already submitted jobs.
    if (!$item
      ->getJob()
      ->isSubmittable()) {
      $form['info']['translator'] = array(
        '#type' => 'item',
        '#title' => t('Provider'),
        '#markup' => $job
          ->getTranslatorLabel(),
        '#prefix' => '<div class="tmgmt-ui-translator tmgmt-ui-info-item">',
        '#suffix' => '</div>',
      );
    }

    // Actually build the review form elements...
    $form['review'] = array(
      '#type' => 'container',
    );

    // Build the review form.
    $data = $item
      ->getData();
    $this
      ->trackChangedSource(\Drupal::service('tmgmt.data')
      ->flatten($data), $form_state);
    $form_state
      ->set('has_preliminary_items', FALSE);
    $form_state
      ->set('all_preliminary', TRUE);

    // Need to keep the first hierarchy. So flatten must take place inside
    // of the foreach loop.
    foreach (Element::children($data) as $key) {
      $review_element = $this
        ->reviewFormElement($form_state, \Drupal::service('tmgmt.data')
        ->flatten($data[$key], $key), $key);
      if ($review_element) {
        $form['review'][$key] = $review_element;
      }
    }
    if ($form_state
      ->get('has_preliminary_items')) {
      $form['translation_changes'] = array(
        '#type' => 'container',
        '#markup' => $this
          ->t('The translations below are in preliminary state and can not be changed.'),
        '#attributes' => array(
          'class' => array(
            'messages',
            'messages--warning',
          ),
        ),
        '#weight' => -50,
      );
    }
    if ($view = View::load('tmgmt_job_item_messages')) {
      $form['messages'] = array(
        '#type' => 'details',
        '#title' => $view
          ->label(),
        '#open' => FALSE,
        '#weight' => 50,
      );
      $form['messages']['view'] = $view
        ->getExecutable()
        ->preview('block', array(
        $item
          ->id(),
      ));
    }
    $form['#attached']['library'][] = 'tmgmt/admin';

    // The reject functionality has to be implement by the translator plugin as
    // that process is completely unique and custom for each translation service.
    // Give the source ui controller a chance to affect the review form.
    $source = $this->sourceManager
      ->createUIInstance($item
      ->getPlugin());
    $form = $source
      ->reviewForm($form, $form_state, $item);

    // Give the translator ui controller a chance to affect the review form.
    if ($item
      ->getTranslator()) {
      $plugin_ui = $this->translatorManager
        ->createUIInstance($item
        ->getTranslator()
        ->getPluginId());
      $form = $plugin_ui
        ->reviewForm($form, $form_state, $item);
    }
    $form['footer'] = tmgmt_color_review_legend();
    return $form;
  }
  protected function actions(array $form, FormStateInterface $form_state) {
    $item = $this->entity;

    // Add the form actions as well.
    $actions['accept'] = array(
      '#type' => 'submit',
      '#button_type' => 'primary',
      '#value' => t('Save as completed'),
      '#access' => $item
        ->isNeedsReview() && !$form_state
        ->has('accept_item'),
      '#validate' => array(
        '::validateForm',
        '::validateJobItem',
      ),
      '#submit' => array(
        '::submitForm',
        '::save',
      ),
    );
    $actions['save'] = array(
      '#type' => 'submit',
      '#value' => t('Save'),
      '#access' => !$item
        ->isAccepted() && !$form_state
        ->get('all_preliminary'),
      '#submit' => array(
        '::submitForm',
        '::save',
      ),
    );
    if ($item
      ->isActive()) {
      $actions['save']['#button_type'] = 'primary';
    }
    $actions['validate'] = array(
      '#type' => 'submit',
      '#value' => t('Validate'),
      '#access' => !$item
        ->isAccepted(),
      '#validate' => array(
        '::validateForm',
        '::validateJobItem',
      ),
      '#submit' => array(
        '::submitForm',
        '::submitRebuild',
      ),
    );
    $actions['validate_html'] = array(
      '#type' => 'submit',
      '#value' => t('Validate HTML tags'),
      '#access' => !$item
        ->isAccepted(),
      '#validate' => [
        '::validateTags',
      ],
      '#submit' => [
        '::submitForm',
      ],
    );
    if ($item
      ->getSourcePlugin() instanceof SourcePreviewInterface && $item
      ->getSourcePlugin()
      ->getPreviewUrl($item)) {
      $actions['preview'] = [
        '#type' => 'link',
        '#title' => t('Preview'),
        '#url' => $item
          ->getSourcePlugin()
          ->getPreviewUrl($item),
        '#attributes' => [
          'target' => '_blank',
        ],
      ];
    }
    $actions['abort_job_item'] = [
      '#type' => 'link',
      '#title' => t('Abort'),
      '#access' => $item
        ->isAbortable(),
      '#url' => Url::fromRoute('entity.tmgmt_job_item.abort_form', [
        'tmgmt_job_item' => $item
          ->id(),
      ]),
      '#weight' => 40,
      '#attributes' => [
        'class' => [
          'button',
          'button--danger',
        ],
      ],
    ];
    return $actions;
  }

  /**
   * Gets the translatable fields of a given job item.
   *
   * @param array $form
   *   The form array.
   *
   * @return array $fields
   *   Returns the translatable fields of the job item.
   */
  private function getTranslatableFields(array $form) {
    $fields = [];
    foreach (Element::children($form['review']) as $group_key) {
      foreach (Element::children($form['review'][$group_key]) as $parent_key) {
        foreach ($form['review'][$group_key][$parent_key] as $key => $data) {
          if (isset($data['translation'])) {
            $fields[$key] = [
              'parent_key' => $parent_key,
              'group_key' => $group_key,
              'data' => $data,
            ];
          }
        }
      }
    }
    return $fields;
  }

  /**
   * {@inheritdoc}
   */
  public function validateForm(array &$form, FormStateInterface $form_state) {
    parent::validateForm($form, $form_state);

    /** @var JobItem $item */
    $item = $this
      ->buildEntity($form, $form_state);

    // First invoke the validation method on the source controller.
    $source_ui = $this->sourceManager
      ->createUIInstance($item
      ->getPlugin());
    $source_ui
      ->reviewFormValidate($form, $form_state, $item);

    // Invoke the validation method on the translator controller (if available).
    if ($item
      ->hasTranslator()) {
      $translator_ui = $this->translatorManager
        ->createUIInstance($item
        ->getTranslator()
        ->getPluginId());
      $translator_ui
        ->reviewFormValidate($form, $form_state, $item);
    }
  }

  /**
   * Form validate callback to validate the job item.
   */
  public function validateJobItem(array &$form, FormStateInterface $form_state) {
    foreach ($this
      ->getTranslatableFields($form) as $key => $value) {
      $parent_key = $value['parent_key'];
      $group_key = $value['group_key'];

      // If has HTML tags will be an array.
      if (isset($value['data']['translation']['value'])) {
        $translation_text = $value['data']['translation']['value']['#value'];
      }
      else {
        $translation_text = $value['data']['translation']['#value'];
      }

      // Validate that is not empty.
      if (empty($translation_text)) {
        $form_state
          ->setError($form['review'][$group_key][$parent_key][$key]['translation'], $this
          ->t('The field is empty.'));
        continue;
      }

      /** @var \Drupal\tmgmt\SegmenterInterface $segmenter */
      $segmenter = \Drupal::service('tmgmt.segmenter');
      $segmenter
        ->validateFormTranslation($form_state, $form['review'][$group_key][$parent_key][$key]['translation'], $this
        ->getEntity());
    }
    if (!$form_state
      ->hasAnyErrors()) {
      $this
        ->messenger()
        ->addStatus(t('Validation completed successfully.'));
    }
  }

  /**
   * Validate that the element is not longer than the max length.
   *
   * @param array $element
   *   The input element to validate.
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   The form state.
   */
  public function validateMaxLength(array $element, FormStateInterface &$form_state) {
    if (isset($element['#max_length']) && $element['#max_length'] < strlen($element['#value'])) {
      $form_state
        ->setError($element, $this
        ->t('The field has @size characters while the limit is @limit.', [
        '@size' => strlen($element['#value']),
        '@limit' => $element['#max_length'],
      ]));
    }
  }

  /**
   * Overrides Drupal\Core\Entity\EntityForm::save().
   */
  public function save(array $form, FormStateInterface $form_state) {
    $item = $this->entity;

    // First invoke the submit method on the source controller.
    $source_ui = $this->sourceManager
      ->createUIInstance($item
      ->getPlugin());
    $source_ui
      ->reviewFormSubmit($form, $form_state, $item);

    // Invoke the submit method on the translator controller (if available).
    if ($item
      ->getTranslator()) {
      $translator_ui = $this->translatorManager
        ->createUIInstance($item
        ->getTranslator()
        ->getPluginId());
      $translator_ui
        ->reviewFormSubmit($form, $form_state, $item);
    }

    // Write changes back to item.
    $data_service = \Drupal::service('tmgmt.data');
    foreach ($form_state
      ->getValues() as $key => $value) {
      if (is_array($value) && isset($value['translation'])) {

        // Update the translation, this will only update the translation in case
        // it has changed. We have two different cases, the first is for nested
        // texts.
        $text = is_array($value['translation']) ? $value['translation']['value'] : $value['translation'];

        // Unmask the translation's HTML tags.
        $data_item = $item
          ->getData($data_service
          ->ensureArrayKey($key));
        $contexts = [
          'data_item' => $data_item,
          'job_item' => $this->entity,
        ];
        \Drupal::moduleHandler()
          ->alter('tmgmt_data_item_text_input', $text, $contexts);
        $data = [
          '#text' => $text,
          '#origin' => 'local',
        ];
        if ($data['#text'] == '' && $item
          ->isActive() && $form_state
          ->getTriggeringElement()['#value'] != '✓') {
          $data = NULL;
          continue;
        }
        $current_data_status = $data_item['#status'];
        $item
          ->addTranslatedData($data, $key, $current_data_status);
      }
    }

    // Check if the user clicked on 'Accept', 'Submit' or 'Reject'.
    if (!empty($form['actions']['accept']) && $form_state
      ->getTriggeringElement()['#value'] == $form['actions']['accept']['#value']) {
      $item
        ->acceptTranslation();

      // Print all messages that have been saved while accepting the reviewed
      // translation.
      foreach ($item
        ->getMessagesSince() as $message) {

        // Ignore debug messages.
        if ($message
          ->getType() == 'debug') {
          continue;
        }
        if ($text = $message
          ->getMessage()) {
          $this
            ->messenger()
            ->addMessage(new FormattableMarkup($text, []), $message
            ->getType());
        }
      }
    }
    if ($form_state
      ->getTriggeringElement()['#value'] == $form['actions']['save']['#value'] && isset($data)) {
      if ($item
        ->getSourceUrl()) {
        $message = t('The translation for <a href=:job>@job_title</a> has been saved successfully.', [
          ':job' => $item
            ->getSourceUrl()
            ->toString(),
          '@job_title' => $item
            ->label(),
        ]);
      }
      else {
        $message = t('The translation has been saved successfully.');
      }
      $this
        ->messenger()
        ->addStatus($message);
    }
    $item
      ->save();
    $item
      ->getJob()
      ->isContinuous() ? $form_state
      ->setRedirect('entity.tmgmt_job_item.canonical', [
      'tmgmt_job_item' => $item
        ->id(),
    ]) : $form_state
      ->setRedirectUrl($item
      ->getJob()
      ->toUrl());
  }

  /**
   * Build form elements for the review form using flattened data items.
   *
   * @todo Mention in the api documentation that the char '|' is not allowed in
   * field names.
   *
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   The current state of the form.
   * @param array $data
   *   Flattened array of translation data items.
   * @param string $parent_key
   *   The key for $data.
   *
   * @return array|NULL
   *   Render array with the form element, or NULL if the text is not set.
   */
  function reviewFormElement(FormStateInterface $form_state, $data, $parent_key) {
    $review_element = NULL;
    foreach (Element::children($data) as $key) {
      $data_item = $data[$key];
      if (isset($data_item['#text']) && \Drupal::service('tmgmt.data')
        ->filterData($data_item)) {

        // The char sequence '][' confuses the form API so we need to replace
        // it when using it for the form field name.
        $field_name = str_replace('][', '|', $key);

        // Ensure that the review form structure is initialized.
        $review_element['#theme'] = 'tmgmt_data_items_form';
        $review_element['#ajaxid'] = $ajax_id = tmgmt_review_form_element_ajaxid($parent_key);
        $review_element['#top_label'] = array_shift($data_item['#parent_label']);
        $leave_label = array_pop($data_item['#parent_label']);

        // Data items are grouped based on their key hierarchy, calculate the
        // group key and ensure that the group is initialized.
        $group_name = substr($field_name, 0, strrpos($field_name, '|'));
        if (empty($group_name)) {
          $group_name = '_none';
        }
        if (!isset($review_element[$group_name])) {
          $review_element[$group_name] = [
            '#group_label' => $data_item['#parent_label'],
          ];
        }

        // Initialize the form element for the given data item and make it
        // available as $element.
        $review_element[$group_name][$field_name] = array(
          '#tree' => TRUE,
        );
        $item_element =& $review_element[$group_name][$field_name];
        $item_element['label']['#markup'] = $leave_label;
        $item_element['status'] = $this
          ->buildStatusRenderArray($this->entity
          ->isAccepted() ? TMGMT_DATA_ITEM_STATE_ACCEPTED : $data_item['#status']);
        $is_preliminary = $data[$key]['#status'] == TMGMT_DATA_ITEM_STATE_PRELIMINARY;
        if ($is_preliminary) {
          $form_state
            ->set('has_preliminary_items', $is_preliminary);
        }
        else {
          $form_state
            ->set('all_preliminary', FALSE);
        }
        $item_element['actions'] = array(
          '#type' => 'container',
          '#access' => !$is_preliminary,
        );
        $item_element['below_actions'] = [
          '#type' => 'container',
        ];

        // Check if the field has a text format attached and check access.
        if (!empty($data_item['#format'])) {
          $format_id = $data_item['#format'];

          /** @var \Drupal\filter\Entity\FilterFormat $format */
          $format = FilterFormat::load($format_id);
          if (!$format || !$format
            ->access('use')) {
            $item_element['actions']['#access'] = FALSE;
            $form_state
              ->set('accept_item', FALSE);
          }
        }
        $item_element['actions'] += $this
          ->buildActions($data_item, $key, $field_name, $ajax_id);

        // Manage the height of the textareas, depending on the length of the
        // description. The minimum number of rows is 3 and the maximum is 15.
        $rows = ceil(strlen($data_item['#text']) / 100);
        $rows = min($rows, 15);
        $rows = max($rows, 3);

        // Allow other modules to change the source and translation texts,
        // for example to mask HTML-tags.
        $source_text = $data_item['#text'];
        $translation_text = isset($data_item['#translation']['#text']) ? $data_item['#translation']['#text'] : '';
        $contexts = [
          'data_item' => $data_item,
          'job_item' => $this->entity,
        ];
        \Drupal::moduleHandler()
          ->alter('tmgmt_data_item_text_output', $source_text, $translation_text, $contexts);

        // Build source and translation areas.
        $item_element = $this
          ->buildSource($item_element, $source_text, $data_item, $rows, $form_state);
        $item_element = $this
          ->buildTranslation($item_element, $translation_text, $data_item, $rows, $form_state, $is_preliminary);
        $item_element = $this
          ->buildChangedSource($item_element, $form_state, $field_name, $key, $ajax_id);
        if (isset($form_state
          ->get('validation_messages')[$field_name])) {
          $item_element['below']['validation'] = [
            '#type' => 'container',
            '#attributes' => [
              'class' => [
                'tmgmt_validation_message',
                'messages',
                'messages--warning',
              ],
            ],
            'message' => [
              '#markup' => Html::escape($form_state
                ->get('validation_messages')[$field_name]),
            ],
          ];
        }

        // Give the translator UI controller a chance to affect the data item element.
        if ($this->entity
          ->hasTranslator()) {
          $item_element = \Drupal::service('plugin.manager.tmgmt.translator')
            ->createUIInstance($this->entity
            ->getTranslator()
            ->getPluginId())
            ->reviewDataItemElement($item_element, $form_state, $key, $parent_key, $data_item, $this->entity);

          // Give the source ui controller a chance to affect the data item element.
          $item_element = \Drupal::service('plugin.manager.tmgmt.source')
            ->createUIInstance($this->entity
            ->getPlugin())
            ->reviewDataItemElement($item_element, $form_state, $key, $parent_key, $data_item, $this->entity);
        }
      }
    }
    return $review_element;
  }

  /**
   * Builds the render array for the status icon.
   *
   * @param int $status
   *   Data item status.
   *
   * @return array
   *   The render array for the status icon.
   */
  protected function buildStatusRenderArray($status) {
    $classes = array();
    $classes[] = 'tmgmt-ui-icon';

    // Icon size 32px square.
    $classes[] = 'tmgmt-ui-icon-32';
    switch ($status) {
      case TMGMT_DATA_ITEM_STATE_ACCEPTED:
        $title = t('Accepted');
        $icon = 'core/misc/icons/73b355/check.svg';
        break;
      case TMGMT_DATA_ITEM_STATE_REVIEWED:
        $title = t('Reviewed');
        $icon = drupal_get_path('module', 'tmgmt') . '/icons/gray-check.svg';
        break;
      case TMGMT_DATA_ITEM_STATE_TRANSLATED:
        $title = t('Translated');
        $icon = drupal_get_path('module', 'tmgmt') . '/icons/ready.svg';
        break;
      case TMGMT_DATA_ITEM_STATE_PENDING:
      default:
        $title = t('Pending');
        $icon = drupal_get_path('module', 'tmgmt') . '/icons/hourglass.svg';
        break;
    }
    return [
      '#type' => 'container',
      '#attributes' => [
        'class' => $classes,
      ],
      'icon' => [
        '#theme' => 'image',
        '#uri' => $icon,
        '#title' => $title,
        '#alt' => $title,
      ],
    ];
  }

  /**
   * Ajax callback for the job item review form.
   */
  function ajaxReviewForm(array $form, FormStateInterface $form_state) {
    $key = array_slice($form_state
      ->getTriggeringElement()['#array_parents'], 0, 2);
    $render_data = NestedArray::getValue($form, $key);
    tmgmt_write_request_messages($form_state
      ->getFormObject()
      ->getEntity());
    return $render_data;
  }

  /**
   * Submit handler for the HTML tag validation.
   */
  function validateTags(array $form, FormStateInterface $form_state) {
    $validation_messages = array();
    $field_count = 0;
    foreach ($form_state
      ->getValues() as $field => $value) {
      if (is_array($value) && isset($value['translation'])) {
        if (!empty($value['translation'])) {
          $tags_validated = $this
            ->compareHTMLTags($value['source'], $value['translation']);
          if ($tags_validated) {
            $validation_messages[$field] = $tags_validated;
            $field_count++;
          }
        }
      }
    }
    if ($field_count > 0) {
      $this
        ->messenger()
        ->addError(t('HTML tag validation failed for @count field(s).', array(
        '@count' => $field_count,
      )));
    }
    else {
      $this
        ->messenger()
        ->addStatus(t('Validation completed successfully.'));
    }
    $form_state
      ->set('validation_messages', $validation_messages);
    $request = \Drupal::request();
    $url = $this->entity
      ->toUrl('canonical');
    if ($request->query
      ->has('destination')) {
      $destination = $request->query
        ->get('destination');
      $request->query
        ->remove('destination');
      $url
        ->setOption('query', array(
        'destination' => $destination,
      ));
    }
    $form_state
      ->setRedirectUrl($url);
    $form_state
      ->setRebuild();
  }

  /**
   * Submit rebuild.
   */
  function submitRebuild(array $form, FormStateInterface $form_state) {
    $form_state
      ->setRebuild();
  }

  /**
   * Compare the HTML tags of source and translation.
   * @param string $source
   *  Source text.
   * @param string $translation
   *  Translated text.
   */
  function compareHTMLTags($source, $translation) {
    $pattern = "/\\<(.*?)\\>/";
    if (is_array($source) && isset($source['value'])) {
      $source = $source['value'];
    }
    if (is_array($translation) && isset($translation['value'])) {
      $translation = $translation['value'];
    }
    preg_match_all($pattern, $source, $source_tags);
    preg_match_all($pattern, $translation, $translation_tags);
    $message = '';
    if ($source_tags != $translation_tags) {
      if (count($source_tags[0]) == count($translation_tags[0])) {
        $message .= 'Order of the HTML tags are incorrect. ';
      }
      else {
        $tags = implode(',', array_diff($source_tags[0], $translation_tags[0]));
        if (!empty($tags)) {
          $message .= 'Expected tags ' . $tags . ' not found. ';
        }
        $source_tags_count = $this
          ->htmlTagCount($source_tags[0]);
        $translation_tags_count = $this
          ->htmlTagCount($translation_tags[0]);
        $difference = array_diff_assoc($source_tags_count, $translation_tags_count);
        foreach ($difference as $tag => $count) {
          if (!isset($translation_tags_count[$tag])) {
            $translation_tags_count[$tag] = 0;
          }
          $message .= $tag . ' expected ' . $count . ', found ' . $translation_tags_count[$tag] . '.';
        }
        $unexpected_tags = array_diff_key($translation_tags_count, $source_tags_count);
        foreach ($unexpected_tags as $tag => $count) {
          if (!isset($translation_tags_count[$tag])) {
            $translation_tags_count[$tag] = 0;
          }
          $message .= $count . ' unexpected ' . $tag . ' tag(s), found.';
        }
      }
    }
    return $message;
  }

  /**
   * Compare the HTML tags of source and translation.
   * @param array $tags
   *  array containing all the HTML tags.
   */
  function htmlTagCount($tags) {
    $counted_tags = array();
    foreach ($tags as $tag) {
      if (in_array($tag, array_keys($counted_tags))) {
        $counted_tags[$tag]++;
      }
      else {
        $counted_tags[$tag] = 1;
      }
    }
    return $counted_tags;
  }

  /**
   * Detect source changes and persist on $form_state.
   *
   * @param array $data
   *   The data items.
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   The form state.
   */
  public function trackChangedSource(array $data, FormStateInterface $form_state) {
    $item = $this->entity;
    $source_changed = [];
    $source_removed = [];
    foreach ($data as $key => $value) {
      if (is_array($value) && isset($value['#translate']) && $value['#translate']) {
        $key_array = \Drupal::service('tmgmt.data')
          ->ensureArrayKey($key);
        try {
          $new_data = \Drupal::service('tmgmt.data')
            ->flatten($item
            ->getSourceData());
        } catch (TMGMTException $e) {
          $this
            ->messenger()
            ->addError(t('The source does not exist any more.'));
          return;
        }
        $current_data = $item
          ->getData($key_array);
        if (!isset($new_data[$key])) {
          $source_changed[$key] = t('This data item has been removed from the source.');
          $source_removed[$key] = TRUE;
        }
        elseif ($current_data['#text'] != $new_data[$key]['#text']) {
          $source_changed[$key] = t('The source has changed.');
        }
      }
    }
    $form_state
      ->set('source_changed', $source_changed);
    $form_state
      ->set('source_removed', $source_removed);
  }

  /**
   * Submit handler to show the diff table of the source.
   */
  public function showDiff(array $form, FormStateInterface $form_state) {
    $key = $form_state
      ->getTriggeringElement()['#data_item_key'];
    $form_state
      ->set('show_diff:' . $key, TRUE);
    $form_state
      ->setRebuild();
  }

  /**
   * Submit handler to resolve the diff updating the Job Item source.
   */
  public function resolveDiff(array $form, FormStateInterface $form_state) {
    $item = $this->entity;
    $key = $form_state
      ->getTriggeringElement()['#data_item_key'];
    $array_key = \Drupal::service('tmgmt.data')
      ->ensureArrayKey($key);
    $first_key = reset($array_key);
    $source_data = $item
      ->getSourceData();
    $new_data = \Drupal::service('tmgmt.data')
      ->flatten($source_data)[$key];
    $item
      ->updateData($key, $new_data);
    if (isset($source_data[$first_key]['#label'])) {
      $item
        ->addMessage('The conflict in the data item source "@data_item" has been resolved.', [
        '@data_item' => $source_data[$first_key]['#label'],
      ]);
    }
    else {
      $item
        ->addMessage('The conflict in the data item source has been resolved.');
    }
    $item
      ->save();
    $form_state
      ->set('show_diff:' . $key, FALSE);
    $form_state
      ->setRebuild();
  }

  /**
   * Builds the actions for a data item.
   *
   * @param array $data_item
   *   The data item.
   * @param string $key
   *   The data item key for the given structure.
   * @param string $field_name
   *   The name of the form element.
   * @param string $ajax_id
   *   The ID used for ajax replacements.
   *
   * @return array
   *   A list of action form elements.
   */
  protected function buildActions($data_item, $key, $field_name, $ajax_id) {
    $actions = [];
    if (!$this->entity
      ->isAccepted()) {
      if ($data_item['#status'] != TMGMT_DATA_ITEM_STATE_REVIEWED) {
        $actions['reviewed'] = array(
          '#type' => 'submit',
          // Unicode character &#x2713 CHECK MARK
          '#value' => '✓',
          '#attributes' => array(
            'title' => t('Reviewed'),
          ),
          '#name' => 'reviewed-' . $field_name,
          '#submit' => [
            '::save',
            'tmgmt_translation_review_form_update_state',
          ],
          '#limit_validation_errors' => array(
            array(
              $ajax_id,
            ),
            array(
              $field_name,
            ),
          ),
          '#ajax' => array(
            'callback' => array(
              $this,
              'ajaxReviewForm',
            ),
            'wrapper' => $ajax_id,
          ),
        );
      }
      else {
        $actions['unreviewed'] = array(
          '#type' => 'submit',
          // Unicode character &#x2713 CHECK MARK
          '#value' => '✓',
          '#attributes' => array(
            'title' => t('Not reviewed'),
            'class' => array(
              'unreviewed',
            ),
          ),
          '#name' => 'unreviewed-' . $field_name,
          '#submit' => [
            '::save',
            'tmgmt_translation_review_form_update_state',
          ],
          '#limit_validation_errors' => array(
            array(
              $ajax_id,
            ),
            array(
              $field_name,
            ),
          ),
          '#ajax' => array(
            'callback' => array(
              $this,
              'ajaxReviewForm',
            ),
            'wrapper' => $ajax_id,
          ),
        );
      }
      if ($this->entity
        ->hasTranslator() && $this->entity
        ->getTranslatorPlugin() instanceof TranslatorRejectDataInterface && $data_item['#status'] != TMGMT_DATA_ITEM_STATE_PENDING) {
        $actions['reject'] = array(
          '#type' => 'submit',
          // Unicode character &#x2717 BALLOT X
          '#value' => '✗',
          '#attributes' => array(
            'title' => t('Reject'),
          ),
          '#name' => 'reject-' . $field_name,
          '#submit' => [
            '::save',
            'tmgmt_translation_review_form_update_state',
          ],
        );
      }
      if (!empty($data_item['#translation']['#text_revisions'])) {
        $actions['revert'] = array(
          '#type' => 'submit',
          // Unicode character U+21B6 ANTICLOCKWISE TOP SEMICIRCLE ARROW
          '#value' => '↶',
          '#attributes' => array(
            'title' => t('Revert to previous revision'),
            'class' => array(
              'reset-above',
            ),
          ),
          '#name' => 'revert-' . $field_name,
          '#data_item_key' => $key,
          '#submit' => array(
            'tmgmt_translation_review_form_revert',
          ),
          '#ajax' => array(
            'callback' => array(
              $this,
              'ajaxReviewForm',
            ),
            'wrapper' => $ajax_id,
          ),
        );
        $actions['reviewed']['#attributes'] = array(
          'class' => array(
            'reviewed-below',
          ),
        );
      }
    }
    return $actions;
  }

  /**
   * Builds the notification and diff for source changes for a data item.
   *
   * @param array $item_element
   *   The form element for the data item.
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   The form state.
   * @param string $field_name
   *   The name of the form element.
   * @param string $key
   *   The data item key for the given structure.
   * @param string $ajax_id
   *   The ID used for ajax replacements.
   *
   * @return array
   *   The form element for the data item.
   */
  protected function buildChangedSource($item_element, FormStateInterface $form_state, $field_name, $key, $ajax_id) {

    // Check for source changes and offer actions.
    if (isset($form_state
      ->get('source_changed')[$key])) {

      // Show diff if requested.
      if ($form_state
        ->get('show_diff:' . $key)) {
        $keys = \Drupal::service('tmgmt.data')
          ->ensureArrayKey($field_name);
        try {
          $new_data = \Drupal::service('tmgmt.data')
            ->flatten($this->entity
            ->getSourceData());
        } catch (TMGMTException $e) {
          $new_data = [];
        }
        $current_data = $this->entity
          ->getData($keys);
        $diff_header = [
          '',
          t('Current text'),
          '',
          t('New text'),
        ];
        $current_lines = explode("\n", $current_data['#text']);
        $new_lines = explode("\n", isset($new_data[$key]) ? $new_data[$key]['#text'] : '');
        $diff_formatter = new DiffFormatter($this
          ->configFactory());
        $diff = new Diff($current_lines, $new_lines);
        $diff_rows = $diff_formatter
          ->format($diff);

        // Unset start block.
        unset($diff_rows[0]);
        $item_element['below']['source_changed']['diff'] = [
          '#type' => 'table',
          '#header' => $diff_header,
          '#rows' => $diff_rows,
          '#empty' => $this
            ->t('No visible changes'),
          '#attributes' => [
            'class' => [
              'diff',
            ],
          ],
        ];
        $item_element['below']['source_changed']['#attached']['library'][] = 'system/diff';
        $item_element['below_actions']['resolve-diff'] = [
          '#type' => 'submit',
          '#value' => t('Resolve'),
          '#attributes' => [
            'title' => t('Apply the changes of the source.'),
          ],
          '#name' => 'resolve-diff-' . $field_name,
          '#data_item_key' => $key,
          '#submit' => [
            '::resolveDiff',
          ],
          '#ajax' => [
            'callback' => '::ajaxReviewForm',
            'wrapper' => $ajax_id,
          ],
        ];
      }
      else {
        $item_element['below']['source_changed'] = [
          '#type' => 'container',
          '#attributes' => [
            'class' => [
              'tmgmt_source_changed',
              'messages',
              'messages--warning',
            ],
          ],
        ];

        // Display changed message.
        $item_element['below']['source_changed']['message'] = [
          '#markup' => '<span>' . $form_state
            ->get('source_changed')[$key] . '</span>',
          '#attributes' => [
            'class' => [
              'tmgmt-review-message-inline',
            ],
          ],
        ];
        if (!isset($form_state
          ->get('source_removed')[$key])) {

          // Offer diff action.
          $item_element['below']['source_changed']['diff_button'] = [
            '#type' => 'submit',
            '#value' => t('Show change'),
            '#name' => 'diff-button-' . $field_name,
            '#data_item_key' => $key,
            '#submit' => [
              '::showDiff',
            ],
            '#attributes' => [
              'class' => [
                'tmgmt-review-message-inline',
              ],
            ],
            '#ajax' => [
              'callback' => '::ajaxReviewForm',
              'wrapper' => $ajax_id,
            ],
          ];
        }
      }
    }
    return $item_element;
  }

  /**
   * Builds the translation form element for a data item.
   *
   * @param array $item_element
   *   The form element for the data item.
   * @param string $translation_text
   *   The translation's text to display in the item element.
   * @param array $data_item
   *   The data item.
   * @param int $rows
   *   The number of rows that should be displayed.
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   The form state.
   * @param bool $is_preliminary
   *   TRUE is the data item is in the PRELIMINARY STATE, FALSE otherwise.
   *
   * @return array
   *   The form element for the data item.
   */
  protected function buildTranslation($item_element, $translation_text, $data_item, $rows, FormStateInterface $form_state, $is_preliminary) {
    if (!empty($data_item['#format']) && $this
      ->config('tmgmt.settings')
      ->get('respect_text_format') && !$form_state
      ->has('accept_item')) {
      $item_element['translation'] = array(
        '#type' => 'text_format',
        '#default_value' => $translation_text,
        '#title' => t('Translation'),
        '#disabled' => $this->entity
          ->isAccepted() || $is_preliminary,
        '#rows' => $rows,
        '#allowed_formats' => array(
          $data_item['#format'],
        ),
      );
    }
    elseif ($form_state
      ->has('accept_item')) {
      $item_element['translation'] = array(
        '#type' => 'textarea',
        '#title' => t('Translation'),
        '#value' => t('This field has been disabled because you do not have sufficient permissions to edit it. It is not possible to review or accept this job item.'),
        '#disabled' => TRUE,
        '#rows' => $rows,
      );
    }
    else {
      $item_element['translation'] = array(
        '#type' => 'textarea',
        '#default_value' => $translation_text,
        '#title' => t('Translation'),
        '#disabled' => $this->entity
          ->isAccepted() || $is_preliminary,
        '#rows' => $rows,
      );
      if (!empty($data_item['#max_length'])) {
        $item_element['translation']['#max_length'] = $data_item['#max_length'];
        $item_element['translation']['#element_validate'] = [
          '::validateMaxLength',
        ];
      }
    }
    if (!empty($data_item['#translation']['#text_revisions'])) {
      $revisions = array();
      foreach ($data_item['#translation']['#text_revisions'] as $revision) {
        $revisions[] = t('Origin: %origin, Created: %created<br />%text', array(
          '%origin' => $revision['#origin'],
          '%created' => $this->dateFormatter
            ->format($revision['#timestamp']),
          '%text' => Xss::filter($revision['#text']),
        ));
      }
      $item_element['below']['revisions_wrapper'] = array(
        '#type' => 'details',
        '#title' => t('Translation revisions'),
        '#open' => TRUE,
      );
      $item_element['below']['revisions_wrapper']['revisions'] = array(
        '#theme' => 'item_list',
        '#items' => $revisions,
      );
    }
    return $item_element;
  }

  /**
   * Builds the source form elements for a data item.
   *
   * @param array $item_element
   *   The form element for the data item.
   * @param string $source_text
   *   The source's text to display in the item element.
   * @param array $data_item
   *   The data item.
   * @param int $rows
   *   The number of rows that should be displayed.
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   The form state.
   *
   * @return array
   *   The form element for the data item.
   */
  protected function buildSource($item_element, $source_text, $data_item, $rows, FormStateInterface $form_state) {
    if (!empty($data_item['#format']) && $this
      ->config('tmgmt.settings')
      ->get('respect_text_format') && !$form_state
      ->has('accept_item')) {
      $item_element['source'] = array(
        '#type' => 'text_format',
        '#default_value' => $source_text,
        '#title' => t('Source'),
        '#disabled' => TRUE,
        '#rows' => $rows,
        '#allowed_formats' => array(
          $data_item['#format'],
        ),
      );
    }
    elseif ($form_state
      ->has('accept_item')) {
      $item_element['source'] = array(
        '#type' => 'textarea',
        '#title' => t('Source'),
        '#value' => t('This field has been disabled because you do not have sufficient permissions to edit it. It is not possible to review or accept this job item.'),
        '#disabled' => TRUE,
        '#rows' => $rows,
      );
    }
    else {
      $item_element['source'] = array(
        '#type' => 'textarea',
        '#default_value' => $source_text,
        '#title' => t('Source'),
        '#disabled' => TRUE,
        '#rows' => $rows,
      );
    }
    return $item_element;
  }

}

Members

Namesort descending Modifiers Type Description Overrides
ContentEntityForm::$entityRepository protected property The entity repository service.
ContentEntityForm::$entityTypeBundleInfo protected property The entity type bundle info service.
ContentEntityForm::$time protected property The time service.
ContentEntityForm::addRevisionableFormFields protected function Add revision form fields if the entity enabled the UI.
ContentEntityForm::buildEntity public function Builds an updated entity object based upon the submitted form values. Overrides EntityForm::buildEntity 3
ContentEntityForm::copyFormValuesToEntity protected function Copies top-level form values to entity properties Overrides EntityForm::copyFormValuesToEntity
ContentEntityForm::flagViolations protected function Flags violations for the current form. 4
ContentEntityForm::getBundleEntity protected function Returns the bundle entity of the entity, or NULL if there is none.
ContentEntityForm::getEditedFieldNames protected function Gets the names of all fields edited in the form. 4
ContentEntityForm::getFormDisplay public function Gets the form display. Overrides ContentEntityFormInterface::getFormDisplay
ContentEntityForm::getFormLangcode public function Gets the code identifying the active form language. Overrides ContentEntityFormInterface::getFormLangcode
ContentEntityForm::getNewRevisionDefault protected function Should new revisions created on default.
ContentEntityForm::init protected function Initializes the form state and the entity before the first form build. Overrides EntityForm::init 1
ContentEntityForm::initFormLangcodes protected function Initializes form language code values.
ContentEntityForm::isDefaultFormLangcode public function Checks whether the current form language matches the entity one. Overrides ContentEntityFormInterface::isDefaultFormLangcode
ContentEntityForm::prepareEntity protected function Prepares the entity object before the form is built first. Overrides EntityForm::prepareEntity 1
ContentEntityForm::setFormDisplay public function Sets the form display. Overrides ContentEntityFormInterface::setFormDisplay
ContentEntityForm::showRevisionUi protected function Checks whether the revision form fields should be added to the form.
ContentEntityForm::submitForm public function This is the default entity object builder function. It is called before any other submit handler to build the new entity object to be used by the following submit handlers. At this point of the form workflow the entity is validated and the form state… Overrides EntityForm::submitForm 3
ContentEntityForm::updateChangedTime public function Updates the changed time of the entity.
ContentEntityForm::updateFormLangcode public function Updates the form language to reflect any change to the entity language.
ContentEntityForm::__construct public function Constructs a ContentEntityForm object. 9
DependencySerializationTrait::$_entityStorages protected property An array of entity type IDs keyed by the property name of their storages.
DependencySerializationTrait::$_serviceIds protected property An array of service IDs keyed by property name used for serialization.
DependencySerializationTrait::__sleep public function 1
DependencySerializationTrait::__wakeup public function 2
EntityForm::$entityTypeManager protected property The entity type manager. 3
EntityForm::$moduleHandler protected property The module handler service.
EntityForm::$operation protected property The name of the current operation.
EntityForm::$privateEntityManager private property The entity manager.
EntityForm::actionsElement protected function Returns the action form element for the current entity form.
EntityForm::afterBuild public function Form element #after_build callback: Updates the entity with submitted data.
EntityForm::buildForm public function Form constructor. Overrides FormInterface::buildForm 10
EntityForm::getBaseFormId public function Returns a string identifying the base form. Overrides BaseFormIdInterface::getBaseFormId 5
EntityForm::getEntity public function Gets the form entity. Overrides EntityFormInterface::getEntity
EntityForm::getEntityFromRouteMatch public function Determines which entity will be used by this form from a RouteMatch object. Overrides EntityFormInterface::getEntityFromRouteMatch 1
EntityForm::getFormId public function Returns a unique string identifying the form. Overrides FormInterface::getFormId 10
EntityForm::getOperation public function Gets the operation identifying the form. Overrides EntityFormInterface::getOperation
EntityForm::prepareInvokeAll protected function Invokes the specified prepare hook variant.
EntityForm::processForm public function Process callback: assigns weights and hides extra fields.
EntityForm::setEntity public function Sets the form entity. Overrides EntityFormInterface::setEntity
EntityForm::setEntityManager public function Sets the entity manager for this form. Overrides EntityFormInterface::setEntityManager
EntityForm::setEntityTypeManager public function Sets the entity type manager for this form. Overrides EntityFormInterface::setEntityTypeManager
EntityForm::setModuleHandler public function Sets the module handler for this form. Overrides EntityFormInterface::setModuleHandler
EntityForm::setOperation public function Sets the operation for this form. Overrides EntityFormInterface::setOperation
EntityForm::__get public function
EntityForm::__set public function
FormBase::$configFactory protected property The config factory. 1
FormBase::$requestStack protected property The request stack. 1
FormBase::$routeMatch protected property The route match.
FormBase::config protected function Retrieves a configuration object.
FormBase::configFactory protected function Gets the config factory for this form. 1
FormBase::container private function Returns the service container.
FormBase::currentUser protected function Gets the current user.
FormBase::getRequest protected function Gets the request object.
FormBase::getRouteMatch protected function Gets the route match.
FormBase::logger protected function Gets the logger for a specific channel.
FormBase::redirect protected function Returns a redirect response object for the specified route. Overrides UrlGeneratorTrait::redirect
FormBase::resetConfigFactory public function Resets the configuration factory.
FormBase::setConfigFactory public function Sets the config factory for this form.
FormBase::setRequestStack public function Sets the request stack object to use.
JobItemForm::$entity protected property Overrides ContentEntityForm::$entity
JobItemForm::actions protected function Returns an array of supported actions for the current entity form. Overrides EntityForm::actions
JobItemForm::ajaxReviewForm function Ajax callback for the job item review form.
JobItemForm::buildActions protected function Builds the actions for a data item.
JobItemForm::buildChangedSource protected function Builds the notification and diff for source changes for a data item.
JobItemForm::buildSource protected function Builds the source form elements for a data item.
JobItemForm::buildStatusRenderArray protected function Builds the render array for the status icon.
JobItemForm::buildTranslation protected function Builds the translation form element for a data item.
JobItemForm::compareHTMLTags function Compare the HTML tags of source and translation.
JobItemForm::form public function Overrides Drupal\Core\Entity\EntityForm::form(). Overrides ContentEntityForm::form
JobItemForm::getTranslatableFields private function Gets the translatable fields of a given job item.
JobItemForm::htmlTagCount function Compare the HTML tags of source and translation.
JobItemForm::resolveDiff public function Submit handler to resolve the diff updating the Job Item source.
JobItemForm::reviewFormElement function Build form elements for the review form using flattened data items.
JobItemForm::save public function Overrides Drupal\Core\Entity\EntityForm::save(). Overrides EntityForm::save
JobItemForm::showDiff public function Submit handler to show the diff table of the source.
JobItemForm::submitRebuild function Submit rebuild.
JobItemForm::trackChangedSource public function Detect source changes and persist on $form_state.
JobItemForm::validateForm public function Button-level validation handlers are highly discouraged for entity forms, as they will prevent entity validation from running. If the entity is going to be saved during the form submission, this method should be manually invoked from the button-level… Overrides ContentEntityForm::validateForm
JobItemForm::validateJobItem public function Form validate callback to validate the job item.
JobItemForm::validateMaxLength public function Validate that the element is not longer than the max length.
JobItemForm::validateTags function Submit handler for the HTML tag validation.
LinkGeneratorTrait::$linkGenerator protected property The link generator. 1
LinkGeneratorTrait::getLinkGenerator Deprecated protected function Returns the link generator.
LinkGeneratorTrait::l Deprecated protected function Renders a link to a route given a route name and its parameters.
LinkGeneratorTrait::setLinkGenerator Deprecated public function Sets the link generator service.
LoggerChannelTrait::$loggerFactory protected property The logger channel factory service.
LoggerChannelTrait::getLogger protected function Gets the logger for a specific channel.
LoggerChannelTrait::setLoggerFactory public function Injects the logger channel factory.
MessengerTrait::$messenger protected property The messenger. 29
MessengerTrait::messenger public function Gets the messenger. 29
MessengerTrait::setMessenger public function Sets the messenger.
RedirectDestinationTrait::$redirectDestination protected property The redirect destination service. 1
RedirectDestinationTrait::getDestinationArray protected function Prepares a 'destination' URL query parameter for use with \Drupal\Core\Url.
RedirectDestinationTrait::getRedirectDestination protected function Returns the redirect destination service.
RedirectDestinationTrait::setRedirectDestination public function Sets the redirect destination service.
StringTranslationTrait::$stringTranslation protected property The string translation service. 1
StringTranslationTrait::formatPlural protected function Formats a string containing a count of items.
StringTranslationTrait::getNumberOfPlurals protected function Returns the number of plurals supported by a given language.
StringTranslationTrait::getStringTranslation protected function Gets the string translation service.
StringTranslationTrait::setStringTranslation public function Sets the string translation service to use. 2
StringTranslationTrait::t protected function Translates a string to the current language or to a given language.
TmgmtFormBase::$dateFormatter protected property The date formatter.
TmgmtFormBase::$renderer protected property Renderer.
TmgmtFormBase::$sourceManager protected property Source plugin manager.
TmgmtFormBase::$translatorManager protected property Translator plugin manager.
TmgmtFormBase::create public static function Instantiates a new instance of this class. Overrides ContentEntityForm::create 1
UrlGeneratorTrait::$urlGenerator protected property The url generator.
UrlGeneratorTrait::getUrlGenerator Deprecated protected function Returns the URL generator service.
UrlGeneratorTrait::setUrlGenerator Deprecated public function Sets the URL generator service.
UrlGeneratorTrait::url Deprecated protected function Generates a URL or path for a specific route based on the given parameters.