reviewer-1.2.x-dev/modules/reviewer_ui/src/Form/Results.php
modules/reviewer_ui/src/Form/Results.php
<?php
declare(strict_types=1);
namespace Drupal\reviewer_ui\Form;
use Drupal\Component\Serialization\Json;
use Drupal\Component\Utility\Html;
use Drupal\Core\DependencyInjection\AutowireTrait;
use Drupal\Core\Form\ConfirmFormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\Core\Url;
use Drupal\reviewer\Reviewer\Action;
use Drupal\reviewer\Reviewer\IgnorerInterface;
use Drupal\reviewer\Reviewer\Result\IndividualResultInterface;
use Drupal\reviewer\Reviewer\Review\ReviewRunnerInterface;
use Drupal\reviewer\Reviewer\Status\Status;
use Drupal\reviewer\Reviewer\Status\StatusEvaluatorInterface;
/**
* Form for viewing, ignoring, and fixing results.
*/
final class Results extends ConfirmFormBase {
use AutowireTrait;
/**
* Fixes, keyed by ID with label values.
*
* @var array<string, string>
*/
private array $fixes = [];
// phpcs:ignore Drupal.Commenting.FunctionComment.Missing
public function __construct(
private readonly IgnorerInterface $ignorer,
private readonly ReviewRunnerInterface $runner,
private readonly StatusEvaluatorInterface $evaluator,
) {}
/**
* {@inheritdoc}
*/
public function getFormId(): string {
return 'reviewer_ui_reviews';
}
/**
* {@inheritdoc}
*/
public function getQuestion(): TranslatableMarkup {
return $this->t('Let Reviewer fix configuration issues?');
}
/**
* {@inheritdoc}
*/
public function getDescription(): TranslatableMarkup {
return $this->t('Fixing issus will alter configuration on your site, and cannot be undone.');
}
/**
* {@inheritdoc}
*/
public function getCancelUrl(): Url {
return Url::fromRoute('reviewer.ui.results');
}
/**
* {@inheritdoc}
*
* @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
* @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
*/
public function buildForm(
array $form,
FormStateInterface $form_state,
): array {
if ($this->fixes) {
$form = parent::buildForm($form, $form_state);
$form['message'] = [
'#type' => 'html_tag',
'#tag' => 'p',
'#value' => $this->t('Fixes will be run for the following reviews:'),
];
$form['review_list'] = [
'#type' => 'html_tag',
'#tag' => 'ul',
'reviews' => array_map(
fn(string $review): array => ['#type' => 'html_tag', '#tag' => 'li', '#value' => $review],
$this->fixes,
),
];
return $form;
}
$form = [
'filters' => [
'#type' => 'fieldset',
'#title' => $this->t('Filter Results'),
'show' => [
'#type' => 'checkboxes',
'#title' => $this->t('Show:'),
'#options' => [
'all' => $this->t('All Results'),
$this->stateFromStatus(Status::Fail) => $this->t('Failed Results'),
$this->stateFromStatus(Status::IgnoredFailure) => $this->t('Ignored Results'),
$this->stateFromStatus(Status::Pass) => $this->t('Passed Results'),
$this->stateFromStatus(Status::NotRun) => $this->t('Not Run'),
],
'#default_value' => array_values(array_filter(
(array) $this->getRequest()->getSession()->get('reviewer_ui.show', []),
fn(mixed $value) => !\is_null($value),
)) ?: ['failed'],
],
],
'#attached' => [
'library' => [
'core/drupal.states',
'reviewer_ui/admin',
],
],
];
foreach ($this->runner->runPlugins(Action::Check) as $review) {
$status_label = (string) $review->getStatus()->label();
$review_label = $review->getLabel();
$status_class = $this->classFromStatus($review->getStatus());
$form[$review->getId()] = [
'#type' => 'details',
'#title' => "<span>$status_label</span> $review_label",
'#attributes' => [
'class' => [
'js-form-wrapper',
'reviewer-details',
"reviewer--$status_class",
],
],
'#summary_attributes' => [
'class' => [
'reviewer-summary',
],
],
];
$failures = \count(array_filter(
$review->getResults()->getIndividualResults(),
fn(IndividualResultInterface $result): bool => $this->evaluator->isFailure($result->getStatus()),
)) !== 0;
$fixable = \count(array_filter(
$review->getResults()->getIndividualResults(),
fn(IndividualResultInterface $result): bool => $result->isFixable(),
)) !== 0;
$form[$review->getId()]['fix'] = [
'#type' => 'checkbox',
'#name' => "fix[{$review->getId()}]",
'#title' => $this->t('Fix Issues'),
'#description' => !$failures
? $this->t('There are no issues identified by this review.')
: ($fixable
? $this->t('Attempt to fix all issues identified by this review.')
: $this->t('No issues identified by this review are fixable.')
),
'#attributes' => [
'disabled' => !$failures || !$fixable,
],
];
$contained_statuses = [];
foreach ($review->getResults()->getIndividualResults() as $result) {
if (!\in_array($result->getStatus(), $contained_statuses, TRUE)) {
$contained_statuses[] = $result->getStatus();
}
}
$states = [];
foreach ($contained_statuses as $contained_status) {
$states[] = 'or';
$states[] = ["[name='show[{$this->stateFromStatus($contained_status)}]']" => ['checked' => TRUE]];
}
array_shift($states);
$form[$review->getId()]['#attributes']['data-drupal-states'] = Json::encode([
'visible' => $states,
]);
$form[$review->getId()]['table'] = [
'#type' => 'table',
'#header' => [
'status' => $this->t('Status'),
'id' => $this->t('ID'),
'message' => $this->t('Message'),
'ignore' => $this->t('Ignore Result'),
],
'#rows' => $this->createRows($review->getResults()->getIndividualResults()),
'#attributes' => [
'class' => [
'reviewer-results-table',
],
],
];
}
$form['actions'] = [
'#type' => 'actions',
'ignore' => [
'#type' => 'submit',
'#button_type' => 'primary',
'#value' => $this->t('Update Ignored'),
'#name' => 'ignore',
],
'unignore_all' => [
'#type' => 'submit',
'#button_type' => 'danger',
'#value' => $this->t('Unignore All'),
'#name' => 'unignore_all',
],
'fix_issues' => [
'#type' => 'submit',
'#button_type' => 'fix',
'#value' => $this->t('Fix Issues'),
'#name' => 'fix_issues',
'#attributes' => [
'class' => ['use-ajax'],
'data-dialog-type' => 'use-modal',
],
'#attached' => [
'library' => [
'core/drupal.ajax',
],
],
],
];
return $form;
}
/**
* {@inheritdoc}
*
* @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
* @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
* @throws \Drupal\reviewer\Exception\NoChecklistsException
*/
public function submitForm(
array &$form,
FormStateInterface $form_state,
): void {
$form_state->cleanValues();
if ($show = $form_state->getUserInput()['show'] ?? []) {
$this->getRequest()->getSession()->set('reviewer_ui.show', $show);
}
$trigger = $form_state->getTriggeringElement()['#name'] ?? '';
if ($trigger === 'fix_issues') {
$fixes = [];
foreach (array_map(strval(...), array_keys($form_state->getUserInput()['fix'] ?? [])) as $fix) {
// Injected services are not available here as the form must be
// serialized.
// @phpstan-ignore-next-line
$reviews = \Drupal::service('reviewer.builder.review')->fromId(explode('.', $fix)[0]);
$fixes[$fix] = $reviews[$fix]->getLabel();
}
$form_state->set('fixes', $this->fixes = $fixes);
$form_state->setRebuild();
return;
}
if ($form_state->has('fixes')) {
// Injected services are not available here as the form must be
// serialized.
// @phpstan-ignore-next-line
$runner = \Drupal::service('reviewer.review_runner');
/** @var array<string, string> $fixes */
$fixes = $form_state->get('fixes');
foreach ($fixes as $fix => $label) {
[$id, $bundle] = explode('.', $fix);
$runner->runIds(Action::Fix, [$id], (array) $bundle);
}
}
if ($trigger === 'ignore') {
$reasons = $form_state->getUserInput()['reasons'] ?? [];
$ignored = [];
foreach ($form_state->getUserInput()['ignored'] ?? [] as $id => $value) {
$ignored[] = $id;
$this->ignorer->ignore($id, $reasons[$id]);
}
// Checkboxes only submit values when they are checked, so get all the
// config ignored result IDs and filter them against the submitted IDs to
// figure out what ignored results should be removed.
$unignored = array_map(
fn(array $unignore) => $unignore['id'],
$this->ignorer->getConfigIgnored(),
);
$unignored = array_filter(
$unignored,
fn(string $id) => !\in_array($id, $ignored, TRUE),
);
$this->ignorer->unignore($unignored);
}
if ($trigger === 'unignore_all') {
$this->ignorer->unignoreAll();
}
}
/**
* Generate table rows for a result.
*
* @param \Drupal\reviewer\Reviewer\Result\IndividualResultInterface[] $results
*
* @return array<string, mixed>
*/
private function createRows(array $results): array {
$rows = [];
foreach ($results as $result) {
$status_class = $this->classFromStatus($result->getStatus());
$status_state = $this->stateFromStatus($result->getStatus());
$rows[$result->getId()] = [
'class' => [
'reviewer-result',
"reviewer--$status_class",
'js-form-wrapper',
],
'data-reviewer-status' => $status_class,
'data-drupal-states' => Json::encode([
'visible' => [
["[name='show[$status_state]']" => ['checked' => TRUE]],
],
]),
'data' => [
'status' => [
'data' => [
'#type' => 'html_tag',
'#tag' => 'span',
'#value' => (string) $result->getStatus()->label(),
],
],
'id' => $result->getId(),
'message' => $result->getMessage(),
'ignore' => '',
],
];
if (
!$this->evaluator->isPass($result->getStatus())
&& !$this->evaluator->isNotRun($result->getStatus())
) {
$result_id = $result->getId();
$rows[$result_id]['data']['ignore'] = [
'data' => [
'#type' => 'checkbox',
'#name' => "ignored[$result_id]",
'#title' => $this->t('Ignore'),
'#title_display' => 'invisible',
'#attributes' => [
'checked' => $this->ignorer->isIgnored($result),
'disabled' => $this->ignorer->isCodeIgnored($result),
],
],
];
$rows["{$result_id}_reason"] = [
'class' => [
'js-form-wrapper',
],
'data-drupal-states' => Json::encode([
'visible' => [
["[name='show[{$this->stateFromStatus($result->getStatus())}]']" => ['checked' => TRUE]],
],
]),
'data' => [
'reason' => [
'data' => [
'#type' => 'textfield',
'#name' => "reasons[$result_id]",
'#title' => $this->t('Reason'),
'#title_display' => 'invisible',
'#maxlength' => 255,
'#value' => $this->ignorer->isIgnored($result)
? $this->ignorer->ignoredReason($result)
: '',
'#attributes' => [
'disabled' => $this->ignorer->isCodeIgnored($result),
],
],
'colspan' => 4,
'class' => [
'js-form-wrapper',
],
'data-drupal-states' => Json::encode([
'visible' => [
"[name='ignored[$result_id]']" => ['checked' => TRUE],
],
]),
],
],
];
}
}
return $rows;
}
/**
* Get a class string from a status.
*/
private function classFromStatus(Status $status): string {
return Html::cleanCssIdentifier(match ($status) {
Status::NotRun => 'not-run',
Status::Pass, Status::Fixed => 'pass',
Status::IgnoredFailure => 'ignored-failure',
Status::IgnoredError => 'ignored-error',
Status::Fail => 'fail',
Status::Error => 'error',
});
}
/**
* Get the state string from a status.
*/
private function stateFromStatus(Status $status): string {
return match ($status) {
Status::NotRun => 'not-run',
Status::IgnoredFailure, Status::IgnoredError => 'ignored',
Status::Fail, Status::Error => 'failed',
default => 'passed',
};
}
}
