Code Coverage
 
Lines
Branches
Paths
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 40
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 13
CRAP
0.00% covered (danger)
0.00%
0 / 1
SaveStatus
0.00% covered (danger)
0.00%
0 / 33
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 13
240
0.00% covered (danger)
0.00%
0 / 1
 build
0.00% covered (danger)
0.00%
0 / 10
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
 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
 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
 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
 onPublish
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
 onRestore
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
 onRevert
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
 reloadWithStatus
0.00% covered (danger)
0.00%
0 / 5
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
 restingStatus
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
12
 statusLabels
0.00% covered (danger)
0.00%
0 / 6
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
1<?php
2
3declare(strict_types=1);
4
5namespace Drupal\display_builder\Plugin\display_builder\Island;
6
7use Drupal\Core\StringTranslation\TranslatableMarkup;
8use Drupal\display_builder\Attribute\Island;
9use Drupal\display_builder\DisplayBuilderHtmx;
10use Drupal\display_builder\InstanceInterface;
11use Drupal\display_builder\Island\IslandPluginBase;
12use Drupal\display_builder\Island\IslandType;
13
14/**
15 * Save status island plugin implementation.
16 *
17 * A dot and a word in the toolbar confirming that the last action reached
18 * the stored state. Every mutation already persists a revision before the
19 * response is built, but nothing said so: the undo counter only reports
20 * how much history exists, and the toast stack is reserved for errors -
21 * success toasts on every drag would train people to stop reading the
22 * corner that carries failures.
23 *
24 * The label is state, the pulse is event. build() derives the label from
25 * the instance itself whenever it is called without a status (the initial
26 * page build, or a plain reload()), so the pip is a standing answer to
27 * "where does this display stand", not only a flash. An event handler
28 * passes its own status through $options, which both overrides the label
29 * for that one render and turns the animation on.
30 *
31 * That is why publish, restore and revert are separate statuses rather
32 * than all folded into "saved": they are the three actions whose outcome
33 * a user cannot infer from the canvas, and each carries its own color.
34 * The transient ones (restored, reverted) describe how the display got
35 * here, so decaying to the plain state label on the next render is
36 * correct rather than a bug.
37 */
38#[Island(
39  id: 'save_status',
40  enabled_by_default: TRUE,
41  label: new TranslatableMarkup('Save status'),
42  description: new TranslatableMarkup('A small indicator confirming the last action reached the stored state.'),
43  type: IslandType::Button,
44  default_region: 'end',
45)]
46class SaveStatus extends IslandPluginBase {
47
48  /**
49   * A draft revision was written, the display differs from the published one.
50   */
51  private const string STATUS_SAVED = 'saved';
52
53  /**
54   * The display was published: the current state is the published one.
55   */
56  private const string STATUS_PUBLISHED = 'published';
57
58  /**
59   * The display was restored to the last published version.
60   */
61  private const string STATUS_RESTORED = 'restored';
62
63  /**
64   * An entity display override was reverted to the published entity display.
65   */
66  private const string STATUS_REVERTED = 'reverted';
67
68  /**
69   * Key carrying the status through the $options passed to build().
70   */
71  private const string OPTION_STATUS = 'save_status';
72
73  /**
74   * {@inheritdoc}
75   *
76   * Overrides build() rather than buildContent(), like the other toolbar
77   * islands: IslandPluginBase::build() gates on isApplicable(), which
78   * requires a node context no toolbar island ever has.
79   */
80  public function build(InstanceInterface $builder, array $data = [], array $options = []): array {
81    $status = $options[self::OPTION_STATUS] ?? $this->restingStatus($builder);
82
83    return [
84      '#type' => 'component',
85      '#component' => 'display_builder:save_status',
86      '#props' => [
87        'variant' => $status,
88        'label' => (string) ($this->statusLabels()[$status] ?? $this->statusLabels()[self::STATUS_SAVED]),
89        // Only an action animates. A reload triggered by anything else
90        // rebuilds the same pip silently.
91        'pulse' => isset($options[self::OPTION_STATUS]),
92      ],
93    ];
94  }
95
96  /**
97   * {@inheritdoc}
98   */
99  public function onAttachToRoot(InstanceInterface $instance, string $node_id): array {
100    return $this->reloadWithStatus($instance, self::STATUS_SAVED);
101  }
102
103  /**
104   * {@inheritdoc}
105   */
106  public function onAttachToSlot(InstanceInterface $instance, string $node_id, string $parent_id): array {
107    return $this->reloadWithStatus($instance, self::STATUS_SAVED);
108  }
109
110  /**
111   * {@inheritdoc}
112   */
113  public function onMove(InstanceInterface $instance, string $node_id): array {
114    return $this->reloadWithStatus($instance, self::STATUS_SAVED);
115  }
116
117  /**
118   * {@inheritdoc}
119   */
120  public function onUpdate(InstanceInterface $instance, string $node_id): array {
121    return $this->reloadWithStatus($instance, self::STATUS_SAVED);
122  }
123
124  /**
125   * {@inheritdoc}
126   */
127  public function onDelete(InstanceInterface $instance, ?string $parent_id): array {
128    return $this->reloadWithStatus($instance, self::STATUS_SAVED);
129  }
130
131  /**
132   * {@inheritdoc}
133   */
134  public function onHistoryChange(InstanceInterface $instance): array {
135    return $this->reloadWithStatus($instance, self::STATUS_SAVED);
136  }
137
138  /**
139   * {@inheritdoc}
140   */
141  public function onPublish(InstanceInterface $instance): array {
142    return $this->reloadWithStatus($instance, self::STATUS_PUBLISHED);
143  }
144
145  /**
146   * {@inheritdoc}
147   */
148  public function onRestore(InstanceInterface $instance): array {
149    return $this->reloadWithStatus($instance, self::STATUS_RESTORED);
150  }
151
152  /**
153   * {@inheritdoc}
154   */
155  public function onRevert(InstanceInterface $instance): array {
156    return $this->reloadWithStatus($instance, self::STATUS_REVERTED);
157  }
158
159  /**
160   * Reloads the island showing a given status, with the pulse animation on.
161   *
162   * @param \Drupal\display_builder\InstanceInterface $instance
163   *   The display builder instance.
164   * @param string $status
165   *   One of the self::STATUS_* constants.
166   *
167   * @return array
168   *   Returns a render array with out-of-band commands.
169   */
170  private function reloadWithStatus(InstanceInterface $instance, string $status): array {
171    return DisplayBuilderHtmx::outOfBand(
172      $this->build($instance, $instance->getCurrentState(), [self::OPTION_STATUS => $status]),
173      '#' . $this->getHtmlId((string) $instance->id()),
174      'innerHTML'
175    );
176  }
177
178  /**
179   * The status to show when no action caused this render.
180   *
181   * @param \Drupal\display_builder\InstanceInterface $builder
182   *   The display builder instance.
183   *
184   * @return string
185   *   One of the self::STATUS_* constants.
186   */
187  private function restingStatus(InstanceInterface $builder): string {
188    // isPublishedPresent() resolves the buildable plugin's sources to hash
189    // them, so it is only worth asking where publishing is a thing at all.
190    if ($builder->isPublishable() && $builder->isPublishedPresent()) {
191      return self::STATUS_PUBLISHED;
192    }
193
194    return self::STATUS_SAVED;
195  }
196
197  /**
198   * Human-readable name for each status.
199   *
200   * @return \Drupal\Core\StringTranslation\TranslatableMarkup[]
201   *   Labels keyed by the self::STATUS_* constants.
202   */
203  private function statusLabels(): array {
204    return [
205      self::STATUS_SAVED => $this->t('Saved'),
206      self::STATUS_PUBLISHED => $this->t('Published'),
207      self::STATUS_RESTORED => $this->t('Restored'),
208      self::STATUS_REVERTED => $this->t('Reverted'),
209    ];
210  }
211
212}

Paths

Below are the source code lines that represent each code path as identified by Xdebug. Please note a path is not necessarily coterminous with a line, a line may contain multiple paths and therefore show up more than once. Please also be aware that some paths may include implicit rather than explicit branches, e.g. an if statement always has an else as part of its logical flow even if you didn't write one.

SaveStatus->build
80  public function build(InstanceInterface $builder, array $data = [], array $options = []): array {
81    $status = $options[self::OPTION_STATUS] ?? $this->restingStatus($builder);
82
83    return [
84      '#type' => 'component',
85      '#component' => 'display_builder:save_status',
86      '#props' => [
87        'variant' => $status,
88        'label' => (string) ($this->statusLabels()[$status] ?? $this->statusLabels()[self::STATUS_SAVED]),
89        // Only an action animates. A reload triggered by anything else
90        // rebuilds the same pip silently.
91        'pulse' => isset($options[self::OPTION_STATUS]),
92      ],
93    ];
94  }
SaveStatus->onAttachToRoot
99  public function onAttachToRoot(InstanceInterface $instance, string $node_id): array {
100    return $this->reloadWithStatus($instance, self::STATUS_SAVED);
101  }
SaveStatus->onAttachToSlot
106  public function onAttachToSlot(InstanceInterface $instance, string $node_id, string $parent_id): array {
107    return $this->reloadWithStatus($instance, self::STATUS_SAVED);
108  }
SaveStatus->onDelete
127  public function onDelete(InstanceInterface $instance, ?string $parent_id): array {
128    return $this->reloadWithStatus($instance, self::STATUS_SAVED);
129  }
SaveStatus->onHistoryChange
134  public function onHistoryChange(InstanceInterface $instance): array {
135    return $this->reloadWithStatus($instance, self::STATUS_SAVED);
136  }
SaveStatus->onMove
113  public function onMove(InstanceInterface $instance, string $node_id): array {
114    return $this->reloadWithStatus($instance, self::STATUS_SAVED);
115  }
SaveStatus->onPublish
141  public function onPublish(InstanceInterface $instance): array {
142    return $this->reloadWithStatus($instance, self::STATUS_PUBLISHED);
143  }
SaveStatus->onRestore
148  public function onRestore(InstanceInterface $instance): array {
149    return $this->reloadWithStatus($instance, self::STATUS_RESTORED);
150  }
SaveStatus->onRevert
155  public function onRevert(InstanceInterface $instance): array {
156    return $this->reloadWithStatus($instance, self::STATUS_REVERTED);
157  }
SaveStatus->onUpdate
120  public function onUpdate(InstanceInterface $instance, string $node_id): array {
121    return $this->reloadWithStatus($instance, self::STATUS_SAVED);
122  }
SaveStatus->reloadWithStatus
170  private function reloadWithStatus(InstanceInterface $instance, string $status): array {
171    return DisplayBuilderHtmx::outOfBand(
172      $this->build($instance, $instance->getCurrentState(), [self::OPTION_STATUS => $status]),
173      '#' . $this->getHtmlId((string) $instance->id()),
174      'innerHTML'
175    );
176  }
SaveStatus->restingStatus
187  private function restingStatus(InstanceInterface $builder): string {
188    // isPublishedPresent() resolves the buildable plugin's sources to hash
189    // them, so it is only worth asking where publishing is a thing at all.
190    if ($builder->isPublishable() && $builder->isPublishedPresent()) {
 
190    if ($builder->isPublishable() && $builder->isPublishedPresent()) {
 
190    if ($builder->isPublishable() && $builder->isPublishedPresent()) {
 
191      return self::STATUS_PUBLISHED;
187  private function restingStatus(InstanceInterface $builder): string {
188    // isPublishedPresent() resolves the buildable plugin's sources to hash
189    // them, so it is only worth asking where publishing is a thing at all.
190    if ($builder->isPublishable() && $builder->isPublishedPresent()) {
 
190    if ($builder->isPublishable() && $builder->isPublishedPresent()) {
 
190    if ($builder->isPublishable() && $builder->isPublishedPresent()) {
 
194    return self::STATUS_SAVED;
195  }
187  private function restingStatus(InstanceInterface $builder): string {
188    // isPublishedPresent() resolves the buildable plugin's sources to hash
189    // them, so it is only worth asking where publishing is a thing at all.
190    if ($builder->isPublishable() && $builder->isPublishedPresent()) {
 
190    if ($builder->isPublishable() && $builder->isPublishedPresent()) {
 
191      return self::STATUS_PUBLISHED;
187  private function restingStatus(InstanceInterface $builder): string {
188    // isPublishedPresent() resolves the buildable plugin's sources to hash
189    // them, so it is only worth asking where publishing is a thing at all.
190    if ($builder->isPublishable() && $builder->isPublishedPresent()) {
 
190    if ($builder->isPublishable() && $builder->isPublishedPresent()) {
 
194    return self::STATUS_SAVED;
195  }
SaveStatus->statusLabels
205      self::STATUS_SAVED => $this->t('Saved'),
206      self::STATUS_PUBLISHED => $this->t('Published'),
207      self::STATUS_RESTORED => $this->t('Restored'),
208      self::STATUS_REVERTED => $this->t('Reverted'),
209    ];
210  }