Code Coverage
 
Lines
Branches
Paths
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 217
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 / 217
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 / 86
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\IslandInterface;
18use Drupal\display_builder\IslandType;
19use Drupal\display_builder\ProfileInterface;
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\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\IslandPluginManagerInterface $islandPluginManager */
116    $islandPluginManager = \Drupal::service('plugin.manager.db_island'); // phpcs:ignore
117    $island_by_types = $islandPluginManager->getIslandsByTypes();
118    $labels = [
119      'view' => $this->t('View panels'),
120      'button' => $this->t('Toolbar buttons'),
121      'contextual' => $this->t('Contextual panels'),
122      'library' => $this->t('Library panels'),
123      'menu' => $this->t('Menu items'),
124    ];
125    // Sort the types according to the labels.
126    $island_by_types = \array_merge($labels, $island_by_types);
127
128    foreach ($island_by_types as $type => $islands) {
129      $form['islands'][$type] = [
130        '#type' => 'details',
131        '#title' => $labels[$type] ?? $type,
132        '#description' => IslandType::description($type),
133        '#group' => 'islands',
134        'content' => $this->buildIslandTypeTable(IslandType::from($type), $islands, $island_configuration),
135      ];
136    }
137
138    $form['status'] = [
139      '#type' => 'checkbox',
140      '#title' => $this->t('Enabled'),
141      '#default_value' => $entity->status(),
142    ];
143
144    return $form;
145  }
146
147  /**
148   * {@inheritdoc}
149   */
150  public function submitForm(array &$form, FormStateInterface $form_state): ProfileInterface {
151    parent::submitForm($form, $form_state);
152
153    // Save user permissions.
154    /** @var \Drupal\display_builder\ProfileInterface $entity */
155    $entity = $this->entity;
156
157    if ($permission = $entity->getPermissionName()) {
158      foreach ($form_state->getValue('roles') ?? [] as $rid => $enabled) {
159        user_role_change_permissions($rid, [$permission => $enabled]);
160      }
161    }
162
163    return $entity;
164  }
165
166  /**
167   * {@inheritdoc}
168   */
169  public function save(array $form, FormStateInterface $form_state): int {
170    $result = parent::save($form, $form_state);
171
172    // Clear the plugin cache so changes are applied on front theme builder.
173    /** @var \Drupal\Core\Plugin\CachedDiscoveryClearerInterface $pluginCacheClearer */
174    $pluginCacheClearer = \Drupal::service('plugin.cache_clearer'); // phpcs:ignore
175    $pluginCacheClearer->clearCachedDefinitions();
176
177    $message_args = ['%label' => $this->entity->label()];
178    $this->messenger()->addStatus(
179      match ($result) {
180        SAVED_NEW => $this->t('Created new display builder config %label.', $message_args),
181        SAVED_UPDATED => $this->t('Updated display builder config %label.', $message_args),
182        default => '',
183      }
184    );
185
186    // Set the initial default configuration and stay on the form to allow
187    // islands configuration.
188    if ($result === SAVED_NEW) {
189      $form_state->setRedirect('entity.display_builder_profile.edit_form', ['display_builder_profile' => $this->entity->id()]);
190    }
191    elseif ($result === SAVED_UPDATED) {
192      $form_state->setRedirect('entity.display_builder_profile.collection');
193    }
194
195    return $result;
196  }
197
198  /**
199   * Build island type table.
200   *
201   * @param \Drupal\display_builder\IslandType $type
202   *   Island type from IslandType enum.
203   * @param array $islands
204   *   List of island plugins.
205   * @param array $configuration
206   *   Configuration of all islands from this type.
207   *
208   * @return array
209   *   A renderable array.
210   */
211  protected function buildIslandTypeTable(IslandType $type, array $islands, array $configuration): array {
212    $type = $type->value;
213    $table = [
214      '#type' => 'table',
215      '#header' => [
216        'drag' => '',
217        'status' => $this->t('Enabled'),
218        'name' => $this->t('Island'),
219        'summary' => $this->t('Configuration'),
220        'region' => empty(IslandType::regions($type)) ? '' : $this->t('Region'),
221        'actions' => $this->t('Actions'),
222        'weight' => $this->t('Weight'),
223      ],
224      '#attributes' => ['id' => 'db-islands-' . $type],
225      '#tabledrag' => [
226        [
227          'action' => 'order',
228          'relationship' => 'sibling',
229          'group' => 'draggable-weight-' . $type,
230        ],
231      ],
232      // We don't want to submit the island type level. We already know the
233      // type of each islands thanks to IslandInterface::getTypeId() so let's
234      // keep the storage flat.
235      '#parents' => ['islands'],
236    ];
237
238    foreach ($islands as $id => $island) {
239      $table[$id] = $this->buildIslandRow($island, $configuration[$id] ?? []);
240    }
241
242    // Order rows by weight.
243    \uasort($table, static function ($a, $b) {
244      if (isset($a['#weight'], $b['#weight'])) {
245        return (int) $a['#weight'] - (int) $b['#weight'];
246      }
247    });
248
249    return $table;
250  }
251
252  /**
253   * Build island row.
254   *
255   * @param \Drupal\display_builder\IslandInterface $island
256   *   Island plugin.
257   * @param array $configuration
258   *   Configuration of this specific island.
259   *
260   * @return array
261   *   A renderable array.
262   */
263  protected function buildIslandRow(IslandInterface $island, array $configuration): array {
264    $id = $island->getPluginId();
265    $definition = (array) $island->getPluginDefinition();
266    $type = $island->getTypeId();
267    /** @var \Drupal\display_builder\IslandPluginManagerInterface $islandPluginManager */
268    $islandPluginManager = \Drupal::service('plugin.manager.db_island'); // phpcs:ignore
269    /** @var \Drupal\display_builder\IslandConfigurationFormInterface $instance */
270    $instance = $islandPluginManager->createInstance($id, $configuration);
271    $weight = isset($configuration['weight']) ? (string) $configuration['weight'] : '0';
272
273    $row = [];
274    $row['#attributes']['class'] = ['draggable'];
275    $row['#weight'] = (int) $weight;
276
277    $row[''] = [];
278    $row['status'] = [
279      '#type' => 'checkbox',
280      '#title' => $this->t('Enabled'),
281      '#title_display' => 'invisible',
282      '#default_value' => $configuration['status'] ?? $definition['enabled_by_default'] ?? FALSE,
283    ];
284    $row['name'] = [
285      '#type' => 'inline_template',
286      '#template' => '<strong >{{ name }}</strong><br>{{ description }}',
287      '#context' => [
288        'name' => $definition['label'] ?? '',
289        'description' => $definition['description'] ?? '',
290      ],
291    ];
292    $row['summary'] = [
293      '#markup' => \implode('<br>', $instance->configurationSummary()),
294    ];
295
296    $regions = IslandType::regions($type);
297
298    if (!empty($regions)) {
299      $row['region'] = [
300        '#type' => 'radios',
301        '#title' => $this->t('Region'),
302        '#title_display' => 'invisible',
303        '#options' => $regions,
304        '#default_value' => $configuration['region'] ?? $definition['default_region'] ?? NULL,
305      ];
306    }
307    else {
308      $row['region'] = [];
309    }
310
311    if ($island instanceof PluginFormInterface && !$this->entity->isNew()) {
312      $row['actions'] = [
313        '#type' => 'link',
314        '#title' => $this->t('Configure'),
315        '#url' => $this->entity->toUrl('edit-plugin-form', [
316          'island_id' => $id,
317          'query' => [
318            'destination' => $this->entity->toUrl()->toString(),
319          ],
320        ]),
321        '#attributes' => [
322          'class' => ['use-ajax', 'button', 'button--small'],
323          'data-dialog-type' => 'modal',
324          'data-dialog-options' => \json_encode([
325            'width' => 700,
326          ]),
327        ],
328        '#states' => [
329          'visible' => [
330            'input[name="islands[' . $id . '][status]"]' => ['checked' => TRUE],
331          ],
332        ],
333      ];
334    }
335    else {
336      $row['actions'] = ['#markup' => ''];
337    }
338
339    $row['weight'] = [
340      '#type' => 'weight',
341      '#default_value' => $weight,
342      '#title' => $this->t('Weight'),
343      '#title_display' => 'invisible',
344      '#attributes' => [
345        'class' => ['draggable-weight-' . $type],
346      ],
347    ];
348
349    return $row;
350  }
351
352  /**
353   * {@inheritdoc}
354   */
355  protected function copyFormValuesToEntity(EntityInterface $entity, array $form, FormStateInterface $form_state): void {
356    $values = $form_state->getValues();
357
358    /** @var \Drupal\Core\Config\Entity\ConfigEntityInterface $entity */
359    $entity = $entity;
360
361    if ($this->entity instanceof EntityWithPluginCollectionInterface) {
362      // Do not manually update values represented by plugin collections.
363      $values = \array_diff_key($values, $this->entity->getPluginCollections());
364    }
365
366    foreach ($values as $key => $value) {
367      if ($key === 'islands') {
368        $value = NestedArray::mergeDeep($entity->get('islands'), $value);
369      }
370      $entity->set($key, $value);
371    }
372  }
373
374  /**
375   * Wraps the module extension list service repository service.
376   *
377   * @return \Drupal\Core\Extension\ModuleExtensionList
378   *   The module extension list service.
379   */
380  protected function moduleExtensionList(): ModuleExtensionList {
381    return $this->moduleExtensionList ??= \Drupal::service('extension.list.module'); // phpcs:ignore
382  }
383
384}