Code Coverage
 
Lines
Branches
Paths
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 225
0.00% covered (danger)
0.00%
0 / 57
0.00% covered (danger)
0.00%
0 / 70
0.00% covered (danger)
0.00%
0 / 7
CRAP
0.00% covered (danger)
0.00%
0 / 1
ProfileForm
0.00% covered (danger)
0.00%
0 / 225
0.00% covered (danger)
0.00%
0 / 57
0.00% covered (danger)
0.00%
0 / 70
0.00% covered (danger)
0.00%
0 / 7
930
0.00% covered (danger)
0.00%
0 / 1
 form
0.00% covered (danger)
0.00%
0 / 92
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
20
 submitForm
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
12
 save
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
42
 buildIslandTypeTable
0.00% covered (danger)
0.00%
0 / 29
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
 buildIslandRow
0.00% covered (danger)
0.00%
0 / 73
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 30
0.00% covered (danger)
0.00%
0 / 1
72
 copyFormValuesToEntity
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
20
 moduleExtensionList
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
1<?php
2
3declare(strict_types=1);
4
5namespace Drupal\display_builder\Form;
6
7use Drupal\Component\Utility\Html;
8use Drupal\Component\Utility\NestedArray;
9use Drupal\Core\DependencyInjection\AutowireTrait;
10use Drupal\Core\Entity\EntityForm;
11use Drupal\Core\Entity\EntityInterface;
12use Drupal\Core\Entity\EntityWithPluginCollectionInterface;
13use Drupal\Core\Extension\ModuleExtensionList;
14use Drupal\Core\Form\FormStateInterface;
15use Drupal\Core\Plugin\PluginFormInterface;
16use Drupal\display_builder\Entity\Profile;
17use Drupal\display_builder\IslandInterface;
18use Drupal\display_builder\IslandType;
19use Drupal\display_builder\IslandTypeViewDisplay;
20use Drupal\display_builder\ProfileInterface;
21use Drupal\user\RoleInterface;
22
23/**
24 * Display builder form.
25 */
26final class ProfileForm extends EntityForm {
27
28  use AutowireTrait;
29
30  /**
31   * Module extension list.
32   */
33  protected ModuleExtensionList $moduleExtensionList;
34
35  /**
36   * {@inheritdoc}
37   */
38  public function form(array $form, FormStateInterface $form_state): array {
39    $form = parent::form($form, $form_state);
40    /** @var \Drupal\display_builder\ProfileInterface $entity */
41    $entity = $this->entity;
42
43    $form['label'] = [
44      '#type' => 'textfield',
45      '#title' => $this->t('Label'),
46      '#maxlength' => 255,
47      '#default_value' => $entity->label(),
48      '#required' => TRUE,
49    ];
50
51    $form['id'] = [
52      '#type' => 'machine_name',
53      '#default_value' => $this->entity->id(),
54      '#machine_name' => [
55        'exists' => [Profile::class, 'load'],
56      ],
57      '#disabled' => !$entity->isNew(),
58    ];
59
60    $form['description'] = [
61      '#type' => 'textarea',
62      '#title' => $this->t('Description'),
63      '#default_value' => $entity->get('description'),
64    ];
65
66    // Add user role access selection. Not available at creation because the
67    // permissions are not set yet by ProfilePermissions.
68    if (!$entity->isNew()) {
69      $roles = $this->entityTypeManager->getStorage('user_role')->loadMultiple();
70      \ksort($roles);
71      $form['roles'] = [
72        '#type' => 'checkboxes',
73        '#title' => $this->t('Roles'),
74        '#options' => \array_map(static fn (RoleInterface $role) => Html::escape((string) $role->label()), $roles),
75        '#default_value' => \array_keys($entity->getRoles()),
76      ];
77    }
78
79    // Inform on two time save for the island specific configurations.
80    if ($this->entity->isNew()) {
81      $form['islands_notice'] = [
82        '#prefix' => '<div class="messages messages--warning">',
83        '#markup' => $this->t('Island configuration will be available only after saving this form.'),
84        '#suffix' => '</div>',
85      ];
86    }
87
88    $path = $this->moduleExtensionList()->getPath('display_builder');
89    $form['islands_intro'] = [
90      [
91        '#type' => 'html_tag',
92        '#tag' => 'label',
93        '#value' => $this->t('Islands'),
94        '#attributes' => [
95          'class' => ['form-item__label'],
96        ],
97      ],
98      [
99        '#type' => 'html_tag',
100        '#tag' => 'img',
101        '#attributes' => [
102          'src' => base_path() . $path . '/assets/images/islands-regions.png',
103          'width' => '1200',
104        ],
105        '#prefix' => '<div style="text-align: center;">',
106        '#suffix' => '</div>',
107      ],
108    ];
109
110    $form['islands'] = [
111      '#type' => 'vertical_tabs',
112    ];
113
114    $island_configuration = $entity->get('islands') ?? [];
115
116    /** @var \Drupal\display_builder\IslandPluginManagerInterface $islandPluginManager */
117    $islandPluginManager = \Drupal::service('plugin.manager.db_island'); // phpcs:ignore
118    $island_by_types = $islandPluginManager->getIslandsByTypes();
119    $labels = [
120      'view' => $this->t('View panels'),
121      'button' => $this->t('Toolbar buttons'),
122      'contextual' => $this->t('Contextual panels'),
123      'library' => $this->t('Library panels'),
124      'menu' => $this->t('Menu items'),
125    ];
126    // Sort the types according to the labels.
127    $island_by_types = \array_merge($labels, $island_by_types);
128
129    foreach ($island_by_types as $type => $islands) {
130      $form['islands'][$type] = [
131        '#type' => 'details',
132        '#title' => $labels[$type] ?? $type,
133        '#description' => IslandType::description($type),
134        '#group' => 'islands',
135        'content' => $this->buildIslandTypeTable(IslandType::from($type), $islands, $island_configuration),
136      ];
137    }
138
139    $form['debug'] = [
140      '#type' => 'checkbox',
141      '#title' => $this->t('Debug mode'),
142      '#description' => $this->t('Enable verbose JavaScript and error logs.'),
143      '#default_value' => $entity->get('debug'),
144    ];
145
146    $form['status'] = [
147      '#type' => 'checkbox',
148      '#title' => $this->t('Enabled'),
149      '#default_value' => $entity->status(),
150    ];
151
152    return $form;
153  }
154
155  /**
156   * {@inheritdoc}
157   */
158  public function submitForm(array &$form, FormStateInterface $form_state): ProfileInterface {
159    parent::submitForm($form, $form_state);
160
161    // Save user permissions.
162    /** @var \Drupal\display_builder\ProfileInterface $entity */
163    $entity = $this->entity;
164
165    if ($permission = $entity->getPermissionName()) {
166      foreach ($form_state->getValue('roles') ?? [] as $rid => $enabled) {
167        user_role_change_permissions($rid, [$permission => $enabled]);
168      }
169    }
170
171    return $entity;
172  }
173
174  /**
175   * {@inheritdoc}
176   */
177  public function save(array $form, FormStateInterface $form_state): int {
178    $result = parent::save($form, $form_state);
179
180    // Clear the plugin cache so changes are applied on front theme builder.
181    /** @var \Drupal\Core\Plugin\CachedDiscoveryClearerInterface $pluginCacheClearer */
182    $pluginCacheClearer = \Drupal::service('plugin.cache_clearer'); // phpcs:ignore
183    $pluginCacheClearer->clearCachedDefinitions();
184
185    $message_args = ['%label' => $this->entity->label()];
186    $this->messenger()->addStatus(
187      match ($result) {
188        SAVED_NEW => $this->t('Created new display builder config %label.', $message_args),
189        SAVED_UPDATED => $this->t('Updated display builder config %label.', $message_args),
190        default => '',
191      }
192    );
193
194    // Set the initial default configuration and stay on the form to allow
195    // islands configuration.
196    if ($result === SAVED_NEW) {
197      $form_state->setRedirect('entity.display_builder_profile.edit_form', ['display_builder_profile' => $this->entity->id()]);
198    }
199    elseif ($result === SAVED_UPDATED) {
200      $form_state->setRedirect('entity.display_builder_profile.collection');
201    }
202
203    return $result;
204  }
205
206  /**
207   * Build island type table.
208   *
209   * @param \Drupal\display_builder\IslandType $type
210   *   Island type from IslandType enum.
211   * @param array $islands
212   *   List of island plugins.
213   * @param array $configuration
214   *   Configuration of all islands from this type.
215   *
216   * @return array
217   *   A renderable array.
218   */
219  protected function buildIslandTypeTable(IslandType $type, array $islands, array $configuration): array {
220    $type = $type->value;
221    $table = [
222      '#type' => 'table',
223      '#header' => [
224        'drag' => '',
225        'status' => $this->t('Enabled'),
226        'name' => $this->t('Island'),
227        'summary' => $this->t('Configuration'),
228        'region' => ($type === IslandType::View->value) ? $this->t('Region') : '',
229        'actions' => $this->t('Actions'),
230        'weight' => $this->t('Weight'),
231      ],
232      '#attributes' => ['id' => 'db-islands-' . $type],
233      '#tabledrag' => [
234        [
235          'action' => 'order',
236          'relationship' => 'sibling',
237          'group' => 'draggable-weight-' . $type,
238        ],
239      ],
240      // We don't want to submit the island type level. We already know the
241      // type of each islands thanks to IslandInterface::getTypeId() so let's
242      // keep the storage flat.
243      '#parents' => ['islands'],
244    ];
245
246    foreach ($islands as $id => $island) {
247      $table[$id] = $this->buildIslandRow($island, $configuration[$id] ?? []);
248    }
249
250    // Order rows by weight.
251    \uasort($table, static function ($a, $b) {
252      if (isset($a['#weight'], $b['#weight'])) {
253        return (int) $a['#weight'] - (int) $b['#weight'];
254      }
255    });
256
257    return $table;
258  }
259
260  /**
261   * Build island row.
262   *
263   * @param \Drupal\display_builder\IslandInterface $island
264   *   Island plugin.
265   * @param array $configuration
266   *   Configuration of this specific island.
267   *
268   * @return array
269   *   A renderable array.
270   */
271  protected function buildIslandRow(IslandInterface $island, array $configuration): array {
272    $id = $island->getPluginId();
273    $definition = (array) $island->getPluginDefinition();
274    $type = $island->getTypeId();
275    /** @var \Drupal\display_builder\IslandPluginManagerInterface $islandPluginManager */
276    $islandPluginManager = \Drupal::service('plugin.manager.db_island'); // phpcs:ignore
277    /** @var \Drupal\display_builder\IslandConfigurationFormInterface $instance */
278    $instance = $islandPluginManager->createInstance($id, $configuration);
279    $weight = isset($configuration['weight']) ? (string) $configuration['weight'] : '0';
280
281    $row = [];
282    $row['#attributes']['class'] = ['draggable'];
283    $row['#weight'] = (int) $weight;
284
285    $row[''] = [];
286    $row['status'] = [
287      '#type' => 'checkbox',
288      '#title' => $this->t('Enabled'),
289      '#title_display' => 'invisible',
290      '#default_value' => $configuration['status'] ?? $definition['enabled_by_default'] ?? FALSE,
291    ];
292    $row['name'] = [
293      '#type' => 'inline_template',
294      '#template' => '<strong >{{ name }}</strong><br>{{ description }}',
295      '#context' => [
296        'name' => $definition['label'] ?? '',
297        'description' => $definition['description'] ?? '',
298      ],
299    ];
300    $row['summary'] = [
301      '#markup' => \implode('<br>', $instance->configurationSummary()),
302    ];
303
304    if ($type === IslandType::View->value) {
305      // If new, only library is on sidebar by default.
306      // @todo move this position option to Island configuration.
307      if ($id !== 'library' && !isset($configuration['region']) && isset($definition['enabled_by_default'])) {
308        $region = IslandTypeViewDisplay::Main->value;
309      }
310      else {
311        $region = $configuration['region'] ?? NULL;
312      }
313      $row['region'] = [
314        '#type' => 'radios',
315        '#title' => $this->t('Display'),
316        '#title_display' => 'invisible',
317        '#options' => IslandTypeViewDisplay::regions(),
318        '#default_value' => $region,
319      ];
320    }
321    else {
322      $row['region'] = [];
323    }
324
325    if ($island instanceof PluginFormInterface && !$this->entity->isNew()) {
326      $row['actions'] = [
327        '#type' => 'link',
328        '#title' => $this->t('Configure'),
329        '#url' => $this->entity->toUrl('edit-plugin-form', [
330          'island_id' => $id,
331          'query' => [
332            'destination' => $this->entity->toUrl()->toString(),
333          ],
334        ]),
335        '#attributes' => [
336          'class' => ['use-ajax', 'button', 'button--small'],
337          'data-dialog-type' => 'modal',
338          'data-dialog-options' => \json_encode([
339            'width' => 700,
340          ]),
341        ],
342        '#states' => [
343          'visible' => [
344            'input[name="islands[' . $id . '][status]"]' => ['checked' => TRUE],
345          ],
346        ],
347      ];
348    }
349    else {
350      $row['actions'] = ['#markup' => ''];
351    }
352
353    $row['weight'] = [
354      '#type' => 'weight',
355      '#default_value' => $weight,
356      '#title' => $this->t('Weight'),
357      '#title_display' => 'invisible',
358      '#attributes' => [
359        'class' => ['draggable-weight-' . $type],
360      ],
361    ];
362
363    return $row;
364  }
365
366  /**
367   * {@inheritdoc}
368   */
369  protected function copyFormValuesToEntity(EntityInterface $entity, array $form, FormStateInterface $form_state): void {
370    $values = $form_state->getValues();
371
372    /** @var \Drupal\Core\Config\Entity\ConfigEntityInterface $entity */
373    $entity = $entity;
374
375    if ($this->entity instanceof EntityWithPluginCollectionInterface) {
376      // Do not manually update values represented by plugin collections.
377      $values = \array_diff_key($values, $this->entity->getPluginCollections());
378    }
379
380    foreach ($values as $key => $value) {
381      if ($key === 'islands') {
382        $value = NestedArray::mergeDeep($entity->get('islands'), $value);
383      }
384      $entity->set($key, $value);
385    }
386  }
387
388  /**
389   * Wraps the module extension list service repository service.
390   *
391   * @return \Drupal\Core\Extension\ModuleExtensionList
392   *   The module extension list service.
393   */
394  protected function moduleExtensionList(): ModuleExtensionList {
395    return $this->moduleExtensionList ??= \Drupal::service('extension.list.module'); // phpcs:ignore
396  }
397
398}