Code Coverage |
||||||||||||||||
Lines |
Branches |
Paths |
Functions and Methods |
Classes and Traits |
||||||||||||
| Total | |
0.00% |
0 / 40 |
|
0.00% |
0 / 17 |
|
0.00% |
0 / 16 |
|
0.00% |
0 / 13 |
CRAP | |
0.00% |
0 / 1 |
| SaveStatus | |
0.00% |
0 / 33 |
|
0.00% |
0 / 17 |
|
0.00% |
0 / 16 |
|
0.00% |
0 / 13 |
240 | |
0.00% |
0 / 1 |
| build | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| onAttachToRoot | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| onAttachToSlot | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| onMove | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| onUpdate | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| onDelete | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| onHistoryChange | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| onPublish | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| onRestore | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| onRevert | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| reloadWithStatus | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| restingStatus | |
0.00% |
0 / 3 |
|
0.00% |
0 / 5 |
|
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
12 | |||
| statusLabels | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| 1 | <?php |
| 2 | |
| 3 | declare(strict_types=1); |
| 4 | |
| 5 | namespace Drupal\display_builder\Plugin\display_builder\Island; |
| 6 | |
| 7 | use Drupal\Core\StringTranslation\TranslatableMarkup; |
| 8 | use Drupal\display_builder\Attribute\Island; |
| 9 | use Drupal\display_builder\DisplayBuilderHtmx; |
| 10 | use Drupal\display_builder\InstanceInterface; |
| 11 | use Drupal\display_builder\Island\IslandPluginBase; |
| 12 | use 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 | )] |
| 46 | class 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 | } |
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.
| 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 | } |
| 99 | public function onAttachToRoot(InstanceInterface $instance, string $node_id): array { |
| 100 | return $this->reloadWithStatus($instance, self::STATUS_SAVED); |
| 101 | } |
| 106 | public function onAttachToSlot(InstanceInterface $instance, string $node_id, string $parent_id): array { |
| 107 | return $this->reloadWithStatus($instance, self::STATUS_SAVED); |
| 108 | } |
| 127 | public function onDelete(InstanceInterface $instance, ?string $parent_id): array { |
| 128 | return $this->reloadWithStatus($instance, self::STATUS_SAVED); |
| 129 | } |
| 134 | public function onHistoryChange(InstanceInterface $instance): array { |
| 135 | return $this->reloadWithStatus($instance, self::STATUS_SAVED); |
| 136 | } |
| 113 | public function onMove(InstanceInterface $instance, string $node_id): array { |
| 114 | return $this->reloadWithStatus($instance, self::STATUS_SAVED); |
| 115 | } |
| 141 | public function onPublish(InstanceInterface $instance): array { |
| 142 | return $this->reloadWithStatus($instance, self::STATUS_PUBLISHED); |
| 143 | } |
| 148 | public function onRestore(InstanceInterface $instance): array { |
| 149 | return $this->reloadWithStatus($instance, self::STATUS_RESTORED); |
| 150 | } |
| 155 | public function onRevert(InstanceInterface $instance): array { |
| 156 | return $this->reloadWithStatus($instance, self::STATUS_REVERTED); |
| 157 | } |
| 120 | public function onUpdate(InstanceInterface $instance, string $node_id): array { |
| 121 | return $this->reloadWithStatus($instance, self::STATUS_SAVED); |
| 122 | } |
| 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 | } |
| 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 | } |
| 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 | } |