Code Coverage
 
Lines
Branches
Paths
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 71
0.00% covered (danger)
0.00%
0 / 48
0.00% covered (danger)
0.00%
0 / 91
0.00% covered (danger)
0.00%
0 / 13
CRAP
0.00% covered (danger)
0.00%
0 / 1
LogsPanel
0.00% covered (danger)
0.00%
0 / 64
0.00% covered (danger)
0.00%
0 / 48
0.00% covered (danger)
0.00%
0 / 91
0.00% covered (danger)
0.00%
0 / 13
702
0.00% covered (danger)
0.00%
0 / 1
 create
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 keyboardShortcuts
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 build
0.00% covered (danger)
0.00%
0 / 20
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
12
 onAttachToRoot
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 onAttachToSlot
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 onMove
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 onHistoryChange
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 onUpdate
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 onDelete
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 onSave
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 buildRows
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
12
 buildRow
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 64
0.00% covered (danger)
0.00%
0 / 1
56
 printSaveAlert
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
20
1<?php
2
3declare(strict_types=1);
4
5namespace Drupal\display_builder\Plugin\display_builder\Island;
6
7use Drupal\Core\Datetime\DateFormatterInterface;
8use Drupal\Core\Field\FieldItemListInterface;
9use Drupal\Core\StringTranslation\TranslatableMarkup;
10use Drupal\display_builder\Attribute\Island;
11use Drupal\display_builder\DisplayBuilderHelpers;
12use Drupal\display_builder\InstanceInterface;
13use Drupal\display_builder\IslandPluginBase;
14use Drupal\display_builder\IslandType;
15use Drupal\display_builder\Plugin\Field\FieldType\HistoryStep;
16use Symfony\Component\DependencyInjection\ContainerInterface;
17
18/**
19 * Logs island plugin implementation.
20 */
21#[Island(
22  id: 'logs',
23  label: new TranslatableMarkup('Logs'),
24  description: new TranslatableMarkup('Logs based on changes history.'),
25  type: IslandType::View,
26  default_region: 'main',
27  icon: 'list-columns-reverse',
28)]
29class LogsPanel extends IslandPluginBase {
30
31  /**
32   * The date formatter.
33   */
34  protected DateFormatterInterface $dateFormatter;
35
36  /**
37   * {@inheritdoc}
38   */
39  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition): static {
40    $instance = parent::create($container, $configuration, $plugin_id, $plugin_definition);
41    $instance->dateFormatter = $container->get('date.formatter');
42
43    return $instance;
44  }
45
46  /**
47   * {@inheritdoc}
48   */
49  public static function keyboardShortcuts(): array {
50    return [
51      'key' => 'o',
52      'help' => t('Show the logs'),
53    ];
54  }
55
56  /**
57   * {@inheritdoc}
58   */
59  public function build(InstanceInterface $builder, array $data = [], array $options = []): array {
60    /** @var \Drupal\display_builder\Plugin\Field\FieldType\HistoryStep $present */
61    $present = $builder->get('present')->first();
62
63    if (!$present) {
64      return [];
65    }
66
67    /** @var \Drupal\display_builder\Plugin\Field\FieldType\HistoryStep $save */
68    $save = $builder->get('save')->first() ?? NULL;
69    $rows = $this->buildRows($builder->get('past'), $present, $builder->get('future'), $save);
70    $table = [
71      '#theme' => 'table',
72      '#header' => [
73        ['data' => $this->t('Step')],
74        ['data' => $this->t('Published')],
75        ['data' => $this->t('Time')],
76        ['data' => $this->t('User')],
77        ['data' => $this->t('Message')],
78      ],
79      '#rows' => $rows,
80    ];
81
82    return [
83      $table,
84      $save ? $this->printSaveAlert(\array_merge($builder->get('past')->getValue(), [$present->getValue()], $builder->get('future')->getValue()), $save) : [],
85    ];
86  }
87
88  /**
89   * {@inheritdoc}
90   */
91  public function onAttachToRoot(string $builder_id, string $instance_id): array {
92    return $this->reloadWithGlobalData($builder_id);
93  }
94
95  /**
96   * {@inheritdoc}
97   */
98  public function onAttachToSlot(string $builder_id, string $instance_id, string $parent_id): array {
99    return $this->reloadWithGlobalData($builder_id);
100  }
101
102  /**
103   * {@inheritdoc}
104   */
105  public function onMove(string $builder_id, string $instance_id): array {
106    return $this->reloadWithGlobalData($builder_id);
107  }
108
109  /**
110   * {@inheritdoc}
111   */
112  public function onHistoryChange(string $builder_id): array {
113    return $this->reloadWithGlobalData($builder_id);
114  }
115
116  /**
117   * {@inheritdoc}
118   */
119  public function onUpdate(string $builder_id, string $instance_id): array {
120    return $this->reloadWithGlobalData($builder_id);
121  }
122
123  /**
124   * {@inheritdoc}
125   */
126  public function onDelete(string $builder_id, string $parent_id): array {
127    return $this->reloadWithGlobalData($builder_id);
128  }
129
130  /**
131   * {@inheritdoc}
132   */
133  public function onSave(string $builder_id): array {
134    return $this->reloadWithGlobalData($builder_id);
135  }
136
137  /**
138   * Build rows for the logs table.
139   *
140   * @param \Drupal\Core\Field\FieldItemListInterface $past
141   *   Steps with time and log message.
142   * @param \Drupal\display_builder\Plugin\Field\FieldType\HistoryStep $present
143   *   A step with time and log message.
144   * @param \Drupal\Core\Field\FieldItemListInterface $future
145   *   Steps with time and log message.
146   * @param \Drupal\display_builder\Plugin\Field\FieldType\HistoryStep $save
147   *   Saved state.
148   *
149   * @return array
150   *   A renderable array representing a table row.
151   */
152  protected function buildRows(FieldItemListInterface $past, ?HistoryStep $present, FieldItemListInterface $future, ?HistoryStep $save): array {
153    $rows = [];
154
155    foreach ($past as $index => $step) {
156      /** @var \Drupal\display_builder\Plugin\Field\FieldType\HistoryStep $step */
157      $rows[] = $this->buildRow(-\count($past) + $index, $step, $save);
158    }
159
160    // Present data.
161    $rows[] = $this->buildRow(0, $present, $save);
162
163    foreach ($future as $index => $step) {
164      /** @var \Drupal\display_builder\Plugin\Field\FieldType\HistoryStep $step */
165      $rows[] = $this->buildRow($index + 1, $step, $save);
166    }
167
168    return $rows;
169  }
170
171  /**
172   * Build a single row for the logs table.
173   *
174   * @param int $index
175   *   The row index.
176   * @param \Drupal\display_builder\Plugin\Field\FieldType\HistoryStep $step
177   *   The step data containing time and log message.
178   * @param \Drupal\display_builder\Plugin\Field\FieldType\HistoryStep $save
179   *   Saved state.
180   *
181   * @return array
182   *   A renderable array representing a table row.
183   */
184  private function buildRow(int $index, HistoryStep $step, ?HistoryStep $save): array {
185    $user = !empty($step->getUser()) ? $this->entityTypeManager->getStorage('user')->load($step->getUser()) : NULL;
186
187    return [
188      'hash' => $step->getHash(),
189      'data' => [
190        (string) $index,
191        ($save && $step->getHash() === $save->getHash()) ? '✅' : '',
192        $step->getTime() ? DisplayBuilderHelpers::formatTime($this->dateFormatter, $step->getTime()) : NULL,
193        $user ? $user->getDisplayName() : NULL,
194        $step->getLog() ?? '',
195      ],
196      'style' => ($index === 0) ? 'font-weight: bold;' : '',
197    ];
198  }
199
200  /**
201   * Print an alert if the saved step is not in the history.
202   *
203   * @param array $steps
204   *   All steps: past, present and future.
205   * @param \Drupal\display_builder\Plugin\Field\FieldType\HistoryStep $save
206   *   Saved state.
207   *
208   * @return array
209   *   A renderable array.
210   */
211  private function printSaveAlert(array $steps, HistoryStep $save): array {
212    foreach ($steps as $step) {
213      if ($step && ($step['hash'] === $save->getHash())) {
214        return [];
215      }
216    }
217
218    $params = [
219      '%time' => DisplayBuilderHelpers::formatTime($this->dateFormatter, $save->getTime()),
220    ];
221
222    return [
223      '#type' => 'html_tag',
224      '#tag' => 'p',
225      '#value' => $this->t('Saved at %time but not visible in logs', $params),
226    ];
227  }
228
229}