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