Code Coverage
 
Lines
Branches
Paths
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 183
n/a
0 / 0
n/a
0 / 0
0.00% covered (danger)
0.00%
0 / 11
CRAP
0.00% covered (danger)
0.00%
0 / 1
EntityViewDisplayFormTrait
0.00% covered (danger)
0.00%
0 / 183
n/a
0 / 0
n/a
0 / 0
0.00% covered (danger)
0.00%
0 / 11
1806
0.00% covered (danger)
0.00%
0 / 1
 submitForm
0.00% covered (danger)
0.00%
0 / 27
n/a
0 / 0
n/a
0 / 0
0.00% covered (danger)
0.00%
0 / 1
30
 entityViewDisplayForm
0.00% covered (danger)
0.00%
0 / 27
n/a
0 / 0
n/a
0 / 0
0.00% covered (danger)
0.00%
0 / 1
20
 buildOverridesForm
0.00% covered (danger)
0.00%
0 / 49
n/a
0 / 0
n/a
0 / 0
0.00% covered (danger)
0.00%
0 / 1
20
 getAlreadyMappedFields
0.00% covered (danger)
0.00%
0 / 11
n/a
0 / 0
n/a
0 / 0
0.00% covered (danger)
0.00%
0 / 1
30
 getSourceFieldAsOptions
0.00% covered (danger)
0.00%
0 / 17
n/a
0 / 0
n/a
0 / 0
0.00% covered (danger)
0.00%
0 / 1
56
 buildExtraFieldRow
0.00% covered (danger)
0.00%
0 / 3
n/a
0 / 0
n/a
0 / 0
0.00% covered (danger)
0.00%
0 / 1
6
 buildFieldRow
0.00% covered (danger)
0.00%
0 / 3
n/a
0 / 0
n/a
0 / 0
0.00% covered (danger)
0.00%
0 / 1
6
 copyFormValuesToEntity
0.00% covered (danger)
0.00%
0 / 6
n/a
0 / 0
n/a
0 / 0
0.00% covered (danger)
0.00%
0 / 1
12
 displayBuildable
0.00% covered (danger)
0.00%
0 / 2
n/a
0 / 0
n/a
0 / 0
0.00% covered (danger)
0.00%
0 / 1
2
 createOverrideField
0.00% covered (danger)
0.00%
0 / 31
n/a
0 / 0
n/a
0 / 0
0.00% covered (danger)
0.00%
0 / 1
42
 setOverrideFieldLocked
0.00% covered (danger)
0.00%
0 / 7
n/a
0 / 0
n/a
0 / 0
0.00% covered (danger)
0.00%
0 / 1
12
1<?php
2
3declare(strict_types=1);
4
5namespace Drupal\display_builder_entity_view\Form;
6
7use Drupal\Core\Entity\Display\EntityViewDisplayInterface;
8use Drupal\Core\Entity\EntityInterface;
9use Drupal\Core\Field\FieldDefinitionInterface;
10use Drupal\Core\Form\FormStateInterface;
11use Drupal\Core\StringTranslation\StringTranslationTrait;
12use Drupal\Core\StringTranslation\TranslatableMarkup;
13use Drupal\display_builder\DisplayBuildableInterface;
14use Drupal\display_builder_entity_view\Entity\DisplayBuilderEntityDisplayInterface;
15use Drupal\display_builder_entity_view\Entity\DisplayBuilderOverridableInterface;
16use Drupal\field\Entity\FieldConfig;
17use Drupal\field\Entity\FieldStorageConfig;
18
19/**
20 * Common methods for entity view display form.
21 */
22trait EntityViewDisplayFormTrait {
23
24  use StringTranslationTrait;
25
26  /**
27   * Form submission handler.
28   *
29   * @param array $form
30   *   An associative array containing the structure of the form.
31   * @param \Drupal\Core\Form\FormStateInterface $form_state
32   *   The current state of the form.
33   */
34  public function submitForm(array &$form, FormStateInterface $form_state): void {
35    parent::submitForm($form, $form_state);
36
37    $profile = $form_state->getValue(DisplayBuildableInterface::PROFILE_PROPERTY, '');
38    $override_status = (bool) $form_state->getValue('override_status', FALSE);
39
40    // Empty means disable main and override.
41    if (empty($profile)) {
42      $this->setOverrideFieldLocked(FALSE);
43      $this->entity->unsetThirdPartySetting('display_builder', DisplayBuildableInterface::PROFILE_PROPERTY);
44      $this->entity->unsetThirdPartySetting('display_builder', DisplayBuildableInterface::OVERRIDE_FIELD_PROPERTY);
45      $this->entity->unsetThirdPartySetting('display_builder', DisplayBuildableInterface::OVERRIDE_PROFILE_PROPERTY);
46    }
47    else {
48      $this->entity->setThirdPartySetting('display_builder', DisplayBuildableInterface::PROFILE_PROPERTY, $profile);
49    }
50
51    if ($override_status) {
52      $profile_override = $form_state->getValue(DisplayBuildableInterface::OVERRIDE_PROFILE_PROPERTY, NULL);
53
54      if ($profile_override) {
55        $this->entity->setThirdPartySetting('display_builder', DisplayBuildableInterface::OVERRIDE_PROFILE_PROPERTY, $profile_override);
56      }
57
58      $override_field = $form_state->getValue(DisplayBuildableInterface::OVERRIDE_FIELD_PROPERTY, NULL);
59
60      if (!$override_field) {
61        $field_name = \sprintf('display_%s', $this->entity->getMode());
62        $field_name = $this->createOverrideField($field_name);
63      }
64      else {
65        $field_name = $override_field;
66      }
67      // In case of field change, need to unlock the previous field.
68      $this->setOverrideFieldLocked(FALSE);
69      $this->entity->setThirdPartySetting('display_builder', DisplayBuildableInterface::OVERRIDE_FIELD_PROPERTY, $field_name);
70      $this->setOverrideFieldLocked(TRUE);
71    }
72    else {
73      $this->setOverrideFieldLocked(FALSE);
74      $this->entity->unsetThirdPartySetting('display_builder', DisplayBuildableInterface::OVERRIDE_FIELD_PROPERTY);
75      $this->entity->unsetThirdPartySetting('display_builder', DisplayBuildableInterface::OVERRIDE_PROFILE_PROPERTY);
76    }
77
78    $this->entity->save();
79    $this->localTaskManager->clearCachedDefinitions();
80    $this->routeBuilder->rebuild();
81  }
82
83  /**
84   * Provides form elements to enable Display Builder.
85   *
86   * @param array $form
87   *   The form structure.
88   *
89   * @return array
90   *   The modified form.
91   */
92  protected function entityViewDisplayForm(array $form): array {
93    $is_display_builder_enabled = $this->entity->isDisplayBuilderEnabled();
94
95    if ($is_display_builder_enabled) {
96      // Hide the table of fields.
97      $form['fields']['#access'] = FALSE;
98      $form['#fields'] = [];
99      $form['#extra'] = [];
100    }
101
102    $form['manage_display_builder'] = [
103      '#type' => 'link',
104      '#title' => $this->t('Display builder'),
105      '#weight' => -11,
106      '#attributes' => ['class' => ['button']],
107      '#url' => $this->displayBuildable()->getBuilderUrl(),
108      '#access' => $is_display_builder_enabled,
109    ];
110
111    if (isset($form['modes'])) {
112      $form['modes']['#weight'] = 0;
113    }
114
115    $form['display_builder_wrapper'] = [
116      '#type' => 'details',
117      '#open' => TRUE,
118      '#title' => $this->t('Display builder'),
119      '#weight' => 1,
120    ];
121
122    $title = new TranslatableMarkup('Enable with profile');
123    $form['display_builder_wrapper'][DisplayBuildableInterface::PROFILE_PROPERTY] = $this->displayBuildable()->buildInstanceForm(FALSE, $title, FALSE);
124
125    /** @var \Drupal\display_builder_entity_view\Entity\DisplayBuilderEntityDisplayInterface $entity */
126    $entity = $this->getEntity();
127
128    if ($entity instanceof DisplayBuilderOverridableInterface) {
129      $form['display_builder_wrapper'][DisplayBuildableInterface::PROFILE_PROPERTY]['override_form'] = $this->buildOverridesForm($entity);
130    }
131
132    return $form;
133  }
134
135  /**
136   * Build the form for entity display overrides per content.
137   *
138   * @param \Drupal\display_builder_entity_view\Entity\DisplayBuilderEntityDisplayInterface $entity
139   *   The entity.
140   *
141   * @return array
142   *   The renderable form array.
143   */
144  protected function buildOverridesForm(DisplayBuilderEntityDisplayInterface|DisplayBuilderOverridableInterface $entity): array {
145    /** @var \Drupal\display_builder_entity_view\Entity\DisplayBuilderOverridableInterface $overridable */
146    $overridable = $entity;
147
148    $options = $this->getSourceFieldAsOptions();
149    $overrideFieldIsSet = $overridable->getDisplayBuilderOverrideField();
150
151    $form = [
152      'override' => [
153        '#type' => 'container',
154        '#title' => $this->t('Content Override'),
155        '#states' => [
156          'visible' => [
157            ':input[name="profile"]' => ['!value' => ''],
158          ],
159        ],
160      ],
161    ];
162
163    $form['override']['override_status'] = [
164      '#type' => 'checkbox',
165      '#title' => $this->t('Enable content overrides'),
166      '#description' => $this->t('Each content will have the option to define a custom display.'),
167      '#default_value' => $overrideFieldIsSet ? TRUE : FALSE,
168    ];
169
170    $form['override']['settings'] = [
171      '#type' => 'container',
172      '#title' => $this->t('Content Override'),
173      '#states' => [
174        'visible' => [
175          ':input[name="override_status"]' => ['checked' => TRUE],
176        ],
177      ],
178    ];
179
180    $form['override']['settings'][DisplayBuildableInterface::OVERRIDE_PROFILE_PROPERTY] = [
181      '#type' => 'select',
182      '#title' => $this->t('Profile for overrides'),
183      '#description' => $this->t('The profile used for content overrides. Can be changed at any time.'),
184      '#options' => $this->displayBuildable()->getAllowedProfiles(),
185      '#default_value' => $overridable->getDisplayBuilderOverrideProfile()?->id() ?? 'default',
186    ];
187
188    if (!empty($options)) {
189      $form['override']['settings'][DisplayBuildableInterface::OVERRIDE_FIELD_PROPERTY] = [
190        '#type' => 'select',
191        '#title' => $this->t('Display Override Field'),
192        '#description' => $this->t('The field where per-content display overrides are stored.'),
193        '#options' => $options,
194        '#default_value' => $overrideFieldIsSet,
195      ];
196    }
197
198    if (!$this->displayBuildable()->isAllowed()) {
199      $form['override']['override_status']['#disabled'] = TRUE;
200      $form['override']['settings'][DisplayBuildableInterface::OVERRIDE_PROFILE_PROPERTY]['#disabled'] = TRUE;
201      $form['override']['settings'][DisplayBuildableInterface::OVERRIDE_FIELD_PROPERTY]['#disabled'] = TRUE;
202    }
203
204    return $form;
205  }
206
207  /**
208   * Returns an array of UI Patterns Source fields which are already mapped.
209   *
210   * @param \Drupal\Core\Entity\Display\EntityViewDisplayInterface $current_display
211   *   The current display.
212   *
213   * @return array
214   *   An array of field names that are already mapped to the current display.
215   */
216  protected function getAlreadyMappedFields(EntityViewDisplayInterface $current_display): array {
217    /** @var \Drupal\display_builder_entity_view\Entity\DisplayBuilderEntityDisplayInterface[] $displays */
218    $displays = $this->entityTypeManager->getStorage('entity_view_display')->loadByProperties([
219      'targetEntityType' => $current_display->getTargetEntityTypeId(),
220      'bundle' => $current_display->getTargetBundle(),
221    ]);
222    $field_names = [];
223
224    foreach ($displays as $display) {
225      if ($display instanceof DisplayBuilderOverridableInterface) {
226        if ($display->isDisplayBuilderOverridable()
227          && $current_display->id() !== $display->id()) {
228          $field_names[] = $display->getDisplayBuilderOverrideField();
229        }
230      }
231    }
232
233    return $field_names;
234  }
235
236  /**
237   * Returns UI Patterns source fields as options.
238   *
239   * @return array
240   *   An associative array of field names and labels.
241   */
242  protected function getSourceFieldAsOptions(): array {
243    /** @var \Drupal\display_builder_entity_view\Entity\DisplayBuilderEntityDisplayInterface $display */
244    $display = $this->getEntity();
245    $fields = [];
246    // Load field instance definitions.
247    $field_storage_definitions = $this->entityFieldManager->getFieldDefinitions(
248      $display->getTargetEntityTypeId(),
249      $display->getTargetBundle(),
250    );
251
252    if ($display instanceof DisplayBuilderOverridableInterface
253      && $display instanceof EntityViewDisplayInterface
254    ) {
255      $already_mapped = $this->getAlreadyMappedFields($display);
256
257      foreach ($field_storage_definitions as $field_name => $field_definition) {
258        if ($field_definition->getType() !== 'ui_patterns_source') {
259          continue;
260        }
261
262        if (\in_array($field_name, $already_mapped, TRUE)) {
263          continue;
264        }
265        $field = FieldConfig::loadByName($this->entity->getTargetEntityTypeId(), $this->entity->getTargetBundle(), $field_name);
266        $fields[$field_name] = $field ? $field->label() : $field_name;
267      }
268    }
269
270    return $fields;
271  }
272
273  /**
274   * Builds the table row structure for a single extra field.
275   *
276   * @param string $field_id
277   *   The field ID.
278   * @param array $extra_field
279   *   The pseudo-field element.
280   *
281   * @return array
282   *   A table row array.
283   */
284  protected function buildExtraFieldRow($field_id, $extra_field): array {
285    if ($this->entity->isDisplayBuilderEnabled()) {
286      return [];
287    }
288
289    return parent::buildExtraFieldRow($field_id, $extra_field);
290  }
291
292  /**
293   * Builds the table row structure for a single field.
294   *
295   * @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition
296   *   The field definition.
297   * @param array $form
298   *   An associative array containing the structure of the form.
299   * @param \Drupal\Core\Form\FormStateInterface $form_state
300   *   The current state of the form.
301   *
302   * @return array
303   *   A table row array.
304   */
305  protected function buildFieldRow(FieldDefinitionInterface $field_definition, array $form, FormStateInterface $form_state): array {
306    if ($this->entity->isDisplayBuilderEnabled()) {
307      return [];
308    }
309
310    return parent::buildFieldRow($field_definition, $form, $form_state);
311  }
312
313  /**
314   * Copies top-level form values to entity properties.
315   *
316   * This should not change existing entity properties that are not being edited
317   * by this form.
318   *
319   * @param \Drupal\Core\Entity\EntityInterface $entity
320   *   The entity the current form should operate upon.
321   * @param array $form
322   *   A nested array of form elements comprising the form.
323   * @param \Drupal\Core\Form\FormStateInterface $form_state
324   *   The current state of the form.
325   *
326   * @see \Drupal\Core\Form\ConfigFormBase::copyFormValuesToConfig()
327   */
328  protected function copyFormValuesToEntity(EntityInterface $entity, array $form, FormStateInterface $form_state): void {
329    /** @var \Drupal\display_builder_entity_view\Entity\DisplayBuilderEntityDisplayInterface $entity */
330    // Do not process field values if Display Builder is or will be enabled.
331    $set_enabled = (bool) $form_state->getValue(['display_builder', 'enabled'], FALSE);
332    $already_enabled = $entity->isDisplayBuilderEnabled();
333
334    if ($already_enabled || $set_enabled) {
335      $form['#fields'] = [];
336      $form['#extra'] = [];
337    }
338
339    parent::copyFormValuesToEntity($entity, $form, $form_state);
340  }
341
342  /**
343   * Gets the display buildable manager.
344   *
345   * @return \Drupal\display_builder\DisplayBuildableInterface
346   *   The manager for display buildable.
347   */
348  protected function displayBuildable(): DisplayBuildableInterface {
349    /** @var \Drupal\display_builder\DisplayBuildableInterface $buildable */
350    $buildable = $this->displayBuildableManager->createInstance('entity_view', ['entity' => $this->getEntity()]);
351
352    return $buildable;
353  }
354
355  /**
356   * Create a field based on a name.
357   *
358   * @param string $field_name
359   *   The field name, field prefix will be added.
360   */
361  private function createOverrideField(string $field_name): string {
362    try {
363      $field_prefix = $this->configFactory()->get('field_ui.settings')->get('field_prefix');
364    }
365    catch (\Throwable $th) {
366      $field_prefix = 'field_';
367    }
368
369    $field_name = $field_prefix . $field_name;
370
371    if (\strlen($field_name) > FieldStorageConfig::NAME_MAX_LENGTH) {
372      $field_name = \substr($field_name, 0, FieldStorageConfig::NAME_MAX_LENGTH);
373    }
374    $field_storage = FieldStorageConfig::loadByName($this->entity->getTargetEntityTypeId(), $field_name);
375
376    if (!$field_storage) {
377      $field_storage = FieldStorageConfig::create([
378        'field_name' => $field_name,
379        'entity_type' => $this->entity->getTargetEntityTypeId(),
380        'type' => 'ui_patterns_source',
381      ]);
382      $field_storage->setTranslatable(TRUE);
383      $field_storage->setCardinality(-1);
384      $field_storage->save();
385    }
386
387    // Add the field prefix to the field name and cut to max size if needed.
388    $field_definition = FieldConfig::loadByName($this->entity->getTargetEntityTypeId(), $this->entity->getTargetBundle(), $field_name);
389
390    if (!$field_definition) {
391      $view_mode_id = $this->entity->getMode();
392      $view_mode_id = ($view_mode_id === 'default') ? 'full' : $view_mode_id;
393      $view_mode_id = \sprintf('%s.%s', $this->entity->getTargetEntityTypeId(), $view_mode_id);
394      $view_mode = $this->entityTypeManager->getStorage('entity_view_mode')->load($view_mode_id);
395      $field_definition = FieldConfig::create([
396        'field_storage' => $field_storage,
397        'bundle' => $this->entity->getTargetBundle(),
398        'field_name' => $field_name,
399        'label' => $this->t('@display display override', ['@display' => $view_mode->label()]),
400      ]);
401      $field_definition->setTranslatable(TRUE);
402      $field_definition->save();
403    }
404
405    return $field_name;
406  }
407
408  /**
409   * Set the lock status of the override field if it exists.
410   *
411   * @param bool $locked
412   *   Whether to lock or unlock the field.
413   */
414  private function setOverrideFieldLocked(bool $locked): void {
415    $field_name = $this->entity->getDisplayBuilderOverrideField();
416
417    if (!$field_name) {
418      return;
419    }
420    $field_storage = FieldStorageConfig::loadByName($this->entity->getTargetEntityTypeId(), $field_name);
421
422    if ($field_storage) {
423      $field_storage->setLocked($locked);
424      $field_storage->save();
425    }
426  }
427
428}