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