Code Coverage
 
Lines
Branches
Paths
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 103
0.00% covered (danger)
0.00%
0 / 67
0.00% covered (danger)
0.00%
0 / 171
0.00% covered (danger)
0.00%
0 / 16
CRAP
0.00% covered (danger)
0.00%
0 / 1
StateButtons
0.00% covered (danger)
0.00%
0 / 96
0.00% covered (danger)
0.00%
0 / 67
0.00% covered (danger)
0.00%
0 / 171
0.00% covered (danger)
0.00%
0 / 16
1332
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
 build
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
12
 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
 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
 buildStateButtons
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 128
0.00% covered (danger)
0.00%
0 / 1
72
 hasButtons
0.00% covered (danger)
0.00%
0 / 14
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
 isOverridden
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
30
 buildPublishButton
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
12
 buildRestoreButton
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
12
 buildRevertButton
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
12
 rebuild
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
1<?php
2
3declare(strict_types=1);
4
5namespace Drupal\display_builder\Plugin\display_builder\Island;
6
7use Drupal\Core\Entity\FieldableEntityInterface;
8use Drupal\Core\Extension\ModuleHandlerInterface;
9use Drupal\Core\StringTranslation\TranslatableMarkup;
10use Drupal\display_builder\Attribute\Island;
11use Drupal\display_builder\InstanceInterface;
12use Drupal\display_builder\IslandPluginToolbarButtonConfigurationBase;
13use Drupal\display_builder\IslandType;
14use Drupal\display_builder_entity_view\Plugin\display_builder\Buildable\EntityViewOverride;
15use Symfony\Component\DependencyInjection\ContainerInterface;
16
17/**
18 * State buttons island plugin implementation.
19 */
20#[Island(
21  id: 'state',
22  enabled_by_default: TRUE,
23  label: new TranslatableMarkup('State'),
24  description: new TranslatableMarkup('Publish and reset the display.'),
25  type: IslandType::Button,
26  default_region: 'end',
27)]
28class StateButtons extends IslandPluginToolbarButtonConfigurationBase {
29
30  /**
31   * The module handler.
32   */
33  protected ModuleHandlerInterface $moduleHandler;
34
35  /**
36   * {@inheritdoc}
37   */
38  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition): static {
39    $instance = parent::create($container, $configuration, $plugin_id, $plugin_definition);
40    $instance->moduleHandler = $container->get('module_handler');
41
42    return $instance;
43  }
44
45  /**
46   * {@inheritdoc}
47   */
48  public function build(InstanceInterface $builder, array $data = [], array $options = []): array {
49    if (!$builder->canSaveContextsRequirement()) {
50      return [];
51    }
52
53    $buttons = $this->buildStateButtons($builder);
54
55    if (empty($buttons)) {
56      return [];
57    }
58
59    return [
60      '#type' => 'component',
61      '#component' => 'display_builder:button_group',
62      '#slots' => [
63        'buttons' => $buttons,
64      ],
65    ];
66  }
67
68  /**
69   * {@inheritdoc}
70   */
71  public function onSave(string $builder_id): array {
72    return $this->reloadWithGlobalData($builder_id);
73  }
74
75  /**
76   * {@inheritdoc}
77   */
78  public function onAttachToRoot(string $builder_id, string $instance_id): array {
79    return $this->rebuild($builder_id);
80  }
81
82  /**
83   * {@inheritdoc}
84   */
85  public function onAttachToSlot(string $builder_id, string $instance_id, string $parent_id): array {
86    return $this->rebuild($builder_id);
87  }
88
89  /**
90   * {@inheritdoc}
91   */
92  public function onMove(string $builder_id, string $instance_id): array {
93    return $this->rebuild($builder_id);
94  }
95
96  /**
97   * {@inheritdoc}
98   */
99  public function onHistoryChange(string $builder_id): array {
100    return $this->rebuild($builder_id);
101  }
102
103  /**
104   * {@inheritdoc}
105   */
106  public function onUpdate(string $builder_id, string $instance_id): array {
107    return $this->rebuild($builder_id);
108  }
109
110  /**
111   * {@inheritdoc}
112   */
113  public function onDelete(string $builder_id, string $parent_id): array {
114    return $this->rebuild($builder_id);
115  }
116
117  /**
118   * Build state buttons.
119   *
120   * @param \Drupal\display_builder\InstanceInterface $instance
121   *   The current display builder instance.
122   *
123   * @return array
124   *   A renderable array of buttons.
125   */
126  protected function buildStateButtons(InstanceInterface $instance): array {
127    $instance_d = (string) $instance->id();
128    $buttons = [];
129    $hasSave = $instance->hasSave();
130    $saveIsCurrent = $hasSave ? $instance->saveIsCurrent() : FALSE;
131
132    if ($this->isButtonEnabled('publish') && !$saveIsCurrent) {
133      $buttons[] = $this->htmxEvents->onSave($this->buildPublishButton(), $instance_d);
134    }
135
136    if ($this->isButtonEnabled('restore') && !$saveIsCurrent) {
137      $buttons[] = $this->htmxEvents->onReset($this->buildRestoreButton(), $instance_d);
138    }
139
140    if ($this->isButtonEnabled('revert') && $this->isOverridden($instance_d)) {
141      $buttons[] = $this->htmxEvents->onRevert($this->buildRevertButton(), $instance_d);
142    }
143
144    return $buttons;
145  }
146
147  /**
148   * {@inheritdoc}
149   */
150  protected function hasButtons(): array {
151    return [
152      'publish' => [
153        'title' => $this->t('Publish'),
154        'default' => 'label',
155      ],
156      'restore' => [
157        'title' => $this->t('Restore'),
158        'default' => 'icon',
159      ],
160      'revert' => [
161        'title' => $this->t('Revert'),
162        'default' => 'icon',
163      ],
164    ];
165  }
166
167  /**
168   * Check if the display builder is on an entity override.
169   *
170   * @param string $builder_id
171   *   The ID of the builder.
172   *
173   * @return bool
174   *   Returns TRUE if the display builder is on an entity override.
175   */
176  protected function isOverridden(string $builder_id): bool {
177    if (!$this->moduleHandler->moduleExists('display_builder_entity_view')) {
178      return FALSE;
179    }
180
181    $instanceInfos = EntityViewOverride::checkInstanceId($builder_id);
182
183    if (!isset($instanceInfos['entity_type_id'], $instanceInfos['entity_id'], $instanceInfos['field_name'])) {
184      return FALSE;
185    }
186
187    // Do not get the profile entity ID from Instance context because the
188    // data stored there is not reliable yet.
189    // See: https://www.drupal.org/project/display_builder/issues/3544545
190    $entity = $this->entityTypeManager->getStorage($instanceInfos['entity_type_id'])
191      ->load($instanceInfos['entity_id']);
192
193    if (!($entity instanceof FieldableEntityInterface)) {
194      return FALSE;
195    }
196
197    $overriddenField = $entity->get($instanceInfos['field_name']);
198
199    if ($overriddenField->isEmpty()) {
200      return FALSE;
201    }
202
203    return TRUE;
204  }
205
206  /**
207   * Builds the publish button.
208   *
209   * @return array
210   *   The publish button render array.
211   */
212  private function buildPublishButton(): array {
213    $button = $this->buildButton(
214      $this->showLabel('publish') ? $this->t('Publish') : '',
215      'publish',
216      $this->showIcon('publish') ? 'upload' : '',
217      $this->t('Publish this display in current state. (shortcut: P)'), ['P' => $this->t('Publish this display (shift+P)')]
218    );
219    $button['#props']['variant'] = 'primary';
220    $button['#attributes']['outline'] = TRUE;
221
222    return $button;
223  }
224
225  /**
226   * Builds the restore button.
227   *
228   * @return array
229   *   The restore button render array.
230   */
231  private function buildRestoreButton(): array {
232    $button = $this->buildButton(
233      $this->showLabel('restore') ? $this->t('Restore') : '',
234      'restore',
235      $this->showIcon('restore') ? 'arrow-repeat' : '',
236      $this->t('Restore to last saved version')
237    );
238    $button['#props']['variant'] = 'warning';
239    $button['#attributes']['outline'] = TRUE;
240
241    return $button;
242  }
243
244  /**
245   * Builds the revert button.
246   *
247   * @return array
248   *   The revert button render array.
249   */
250  private function buildRevertButton(): array {
251    $button = $this->buildButton(
252      $this->showLabel('revert') ? $this->t('Revert') : '',
253      'revert',
254      $this->showIcon('revert') ? 'box-arrow-in-down' : '',
255      $this->t('Revert to default display (not overridden)')
256    );
257    $button['#props']['variant'] = 'danger';
258    $button['#attributes']['outline'] = TRUE;
259
260    return $button;
261  }
262
263  /**
264   * Rebuilds the island with the given builder ID.
265   *
266   * @param string $builder_id
267   *   The ID of the builder.
268   *
269   * @return array
270   *   The rebuilt island.
271   */
272  private function rebuild(string $builder_id): array {
273    if (!$this->builder) {
274      // @todo pass \Drupal\display_builder\InstanceInterface object in
275      // parameters instead of loading again.
276      /** @var \Drupal\display_builder\InstanceStorage $storage */
277      $storage = $this->entityTypeManager->getStorage('display_builder_instance');
278      /** @var \Drupal\display_builder\InstanceInterface $builder */
279      $builder = $storage->load($builder_id);
280      $this->builder = $builder;
281    }
282
283    return $this->addOutOfBand(
284      $this->build($this->builder),
285      '#' . $this->getHtmlId($builder_id),
286      'innerHTML'
287    );
288  }
289
290}