Code Coverage
 
Lines
Branches
Paths
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 87
0.00% covered (danger)
0.00%
0 / 45
0.00% covered (danger)
0.00%
0 / 32
0.00% covered (danger)
0.00%
0 / 14
CRAP
0.00% covered (danger)
0.00%
0 / 1
ViewDisplay
0.00% covered (danger)
0.00%
0 / 83
0.00% covered (danger)
0.00%
0 / 45
0.00% covered (danger)
0.00%
0 / 32
0.00% covered (danger)
0.00%
0 / 14
812
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 11
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
 getBuilderUrl
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
 checkInstanceId
0.00% covered (danger)
0.00%
0 / 7
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
 checkAccess
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
 getUrlFromInstanceId
0.00% covered (danger)
0.00%
0 / 4
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
 getDisplayUrlFromInstanceId
0.00% covered (danger)
0.00%
0 / 4
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
 getProfile
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
20
 getSources
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
 saveSources
0.00% covered (danger)
0.00%
0 / 8
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
 getInstanceId
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
 collectInstances
0.00% covered (danger)
0.00%
0 / 20
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
20
 getRuntimeContexts
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
 getInitializationMessage
0.00% covered (danger)
0.00%
0 / 3
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
 getInitialSources
0.00% covered (danger)
0.00%
0 / 5
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_views\Plugin\display_builder\Buildable;
6
7use Drupal\Core\Access\AccessResult;
8use Drupal\Core\Access\AccessResultInterface;
9use Drupal\Core\Plugin\Context\Context;
10use Drupal\Core\Plugin\Context\ContextDefinition;
11use Drupal\Core\Plugin\Context\EntityContext;
12use Drupal\Core\Session\AccountInterface;
13use Drupal\Core\StringTranslation\TranslatableMarkup;
14use Drupal\Core\Url;
15use Drupal\display_builder\Attribute\DisplayBuildable;
16use Drupal\display_builder\DisplayBuildableInterface;
17use Drupal\display_builder\DisplayBuildablePluginBase;
18use Drupal\display_builder\DisplayBuilderHelpers;
19use Drupal\display_builder\Entity\ProfileInterface;
20use Drupal\ui_patterns\Plugin\Context\RequirementsContext;
21use Drupal\views\Entity\View;
22use Drupal\views\Plugin\views\PluginBase;
23
24/**
25 * Plugin implementation of the display_buildable.
26 */
27#[DisplayBuildable(
28  id: 'view_display',
29  label: new TranslatableMarkup('Views'),
30  instance_prefix: 'views__',
31)]
32final class ViewDisplay extends DisplayBuildablePluginBase {
33
34  /**
35   * View display extender plugin.
36   */
37  protected PluginBase $extender;
38
39  /**
40   * {@inheritdoc}
41   */
42  public function __construct(array $configuration, $plugin_id, $plugin_definition) {
43    parent::__construct($configuration, $plugin_id, $plugin_definition);
44
45    // It is better to take the plugin from configuration when available in
46    // order to manipulate the view "executable" from the tempstore.  So, we
47    // have access to the state not yet saved in config.
48    if (isset($configuration['extender'])) {
49      $this->extender = $configuration['extender'];
50      // Configuration to store in the Instance entity.
51      unset($this->configuration['extender']);
52      $this->configuration['view_id'] = $this->extender->view->id();
53      $this->configuration['view_display'] = $this->extender->view->current_display;
54
55      return;
56    }
57
58    // If extender plugin is not directly passed, we can get it from the
59    // configuration data (as stored in Instance entity):
60    // - view_id (string)
61    // - view_display (string)
62    // However, this is loading a "real" View entity, as stored in config. So
63    // it may miss unsaved parameters.
64    $view = View::load($configuration['view_id'] ?? '')?->getExecutable() ?? NULL;
65
66    if ($view) {
67      $view->setDisplay($configuration['view_display'] ?? '');
68      $this->extender = $view->getDisplay()->getExtenders()['display_builder'];
69    }
70  }
71
72  /**
73   * {@inheritdoc}
74   */
75  public function getBuilderUrl(): Url {
76    $params = [
77      'view' => $this->extender->view->id(),
78      'display' => $this->extender->view->current_display,
79    ];
80
81    return Url::fromRoute('display_builder_views.views.manage', $params);
82  }
83
84  /**
85   * {@inheritdoc}
86   */
87  public static function checkInstanceId(string $instance_id): ?array {
88    if (!\str_starts_with($instance_id, self::getPrefix())) {
89      return NULL;
90    }
91    [, $view, $display] = \explode('__', $instance_id);
92
93    return [
94      'view' => $view,
95      'display' => $display,
96    ];
97  }
98
99  /**
100   * {@inheritdoc}
101   */
102  public static function checkAccess(string $instance_id, AccountInterface $account): AccessResultInterface {
103    return $account->hasPermission('administer views') ? AccessResult::allowed() : AccessResult::forbidden();
104  }
105
106  /**
107   * {@inheritdoc}
108   */
109  public static function getUrlFromInstanceId(string $instance_id): Url {
110    $params = self::checkInstanceId($instance_id);
111
112    if (!$params) {
113      // Fallback to the list of instances.
114      return Url::fromRoute('entity.display_builder_instance.collection');
115    }
116
117    return Url::fromRoute('display_builder_views.views.manage', $params);
118  }
119
120  /**
121   * {@inheritdoc}
122   */
123  public static function getDisplayUrlFromInstanceId(string $instance_id): Url {
124    $params = self::checkInstanceId($instance_id);
125
126    if (!$params) {
127      // Fallback to the list of instances.
128      return Url::fromRoute('entity.display_builder_instance.collection');
129    }
130
131    return Url::fromRoute('entity.view.edit_form', $params);
132  }
133
134  /**
135   * {@inheritdoc}
136   */
137  public function getProfile(): ?ProfileInterface {
138    $display_builder_id = $this->extender->options[DisplayBuildableInterface::PROFILE_PROPERTY] ?? NULL;
139
140    if ($display_builder_id === NULL && $this->getInstance()) {
141      return $this->getInstance()->getProfile();
142    }
143
144    if (empty($display_builder_id)) {
145      return NULL;
146    }
147    $storage = $this->entityTypeManager->getStorage('display_builder_profile');
148
149    /** @var \Drupal\display_builder\Entity\ProfileInterface $display_builder */
150    $display_builder = $storage->load($display_builder_id);
151
152    return $display_builder;
153  }
154
155  /**
156   * {@inheritdoc}
157   */
158  public function getSources(): array {
159    return $this->extender->options[DisplayBuildableInterface::SOURCES_PROPERTY] ?? [];
160  }
161
162  /**
163   * {@inheritdoc}
164   */
165  public function saveSources(): void {
166    $sources = $this->getInstance()->getCurrentState();
167    // First, we save in the "live" object.
168    $this->extender->options[DisplayBuildableInterface::SOURCES_PROPERTY] = $sources;
169    // Then, we save in the permanent storage.
170    $displays = $this->extender->view->storage->get('display');
171    $display_id = $this->extender->view->current_display;
172    // It is risky to alter a View like that. We need to be careful to not
173    // break the storage integrity, but we didn't find a better way.
174    $displays[$display_id]['display_options']['display_extenders']['display_builder'][DisplayBuildableInterface::SOURCES_PROPERTY] = $sources;
175    $this->extender->view->storage->set('display', $displays);
176    $this->extender->view->storage->save();
177    // @todo Test if we still need to invalidate the cache manually here.
178    $this->extender->view->storage->invalidateCaches();
179  }
180
181  /**
182   * {@inheritdoc}
183   */
184  public function getInstanceId(): string {
185    return \sprintf('%s%s__%s', self::getPrefix(), $this->extender->view->id(), $this->extender->view->current_display);
186  }
187
188  /**
189   * {@inheritdoc}
190   */
191  public static function collectInstances(): array {
192    $entityTypeManager = \Drupal::service('entity_type.manager');
193    $displayBuildableManager = \Drupal::service('plugin.manager.display_buildable');
194    $storage = $entityTypeManager->getStorage('view');
195    $instances = [];
196
197    foreach ($storage->loadMultiple() as $view) {
198      // @phpstan-ignore-next-line
199      foreach ($view->display as $display_id => $display) {
200        $profile = $display['display_options']['display_extenders']['display_builder'][DisplayBuildableInterface::PROFILE_PROPERTY] ?? NULL;
201
202        if (!$profile) {
203          continue;
204        }
205        $instance_id = \sprintf('%s%s__%s', self::getPrefix(), $view->id(), $display_id);
206        /** @var \Drupal\display_builder\DisplayBuildableInterface $buildable */
207        $buildable = $displayBuildableManager->createInstance('view_display',
208          [
209            'view_id' => $view->id(),
210            'view_display' => $display_id,
211          ]
212        );
213        $buildable->initInstanceIfMissing();
214        $instance_id = $buildable->getInstanceId();
215        $instances[$instance_id] = $buildable->getInstance();
216      }
217    }
218
219    return $instances;
220  }
221
222  /**
223   * {@inheritdoc}
224   */
225  public function getRuntimeContexts(array $unqualified_context_ids): array {
226    // Contexts needed by UI Patterns Source can be added by each Display
227    // Buildable plugin by overriding this method.
228    // @todo filter by $unqualified_context_ids.
229    $contexts = [];
230    $contexts['ui_patterns_views:view_entity'] = EntityContext::fromEntity($this->extender->view->storage);
231    // Needed by ui_patterns_views's ViewRowsSource.
232    // Will be filled by \Drupal\display_builder_views\Hook\PreprocessViewsView.
233    $contexts['ui_patterns_views:rows'] = new Context(new ContextDefinition('any'), []);
234    $contexts = RequirementsContext::addToContext(['views:style'], $contexts);
235
236    return $contexts;
237  }
238
239  /**
240   * {@inheritdoc}
241   */
242  protected function getInitializationMessage(): TranslatableMarkup {
243    if ($this->initialDataSource === 'fixture') {
244      return $this->t('Initialize display from default configuration');
245    }
246
247    return $this->t('Initialize display from existing View configuration');
248  }
249
250  /**
251   * {@inheritdoc}
252   */
253  protected function getInitialSources(): array {
254    // Get the sources stored in config.
255    $sources = $this->getSources();
256
257    if (empty($sources)) {
258      // Fallback to a fixture mimicking the standard view layout.
259      $sources = DisplayBuilderHelpers::getFixtureDataFromExtension('display_builder_views', 'default_view');
260      $this->initialDataSource = 'fixture';
261    }
262
263    return $sources;
264  }
265
266}