Code Coverage
 
Lines
Branches
Paths
Functions and Methods
Classes and Traits
Total
6.38% covered (danger)
6.38%
19 / 298
3.97% covered (danger)
3.97%
5 / 126
1.74% covered (danger)
1.74%
2 / 115
8.70% covered (danger)
8.70%
2 / 23
CRAP
0.00% covered (danger)
0.00%
0 / 1
ApiController
6.38% covered (danger)
6.38%
19 / 298
3.97% covered (danger)
3.97%
5 / 126
1.74% covered (danger)
1.74%
2 / 115
8.70% covered (danger)
8.70%
2 / 23
4990.21
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
n/a
0 / 0
n/a
0 / 0
100.00% covered (success)
100.00%
1 / 1
1
 attachToRoot
0.00% covered (danger)
0.00%
0 / 25
0.00% covered (danger)
0.00%
0 / 19
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
72
 attachToSlot
40.62% covered (danger)
40.62%
13 / 32
21.05% covered (danger)
21.05%
4 / 19
6.67% covered (danger)
6.67%
1 / 15
0.00% covered (danger)
0.00%
0 / 1
60.03
 get
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
 update
0.00% covered (danger)
0.00%
0 / 41
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 19
0.00% covered (danger)
0.00%
0 / 1
72
 thirdPartySettingsUpdate
0.00% covered (danger)
0.00%
0 / 29
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
 paste
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
20
 delete
0.00% covered (danger)
0.00%
0 / 11
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
 saveAsPreset
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
6
 save
0.00% covered (danger)
0.00%
0 / 7
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
 restore
0.00% covered (danger)
0.00%
0 / 4
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
 revert
0.00% covered (danger)
0.00%
0 / 23
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
30
 undo
0.00% covered (danger)
0.00%
0 / 4
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
 redo
0.00% covered (danger)
0.00%
0 / 4
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
 clear
0.00% covered (danger)
0.00%
0 / 4
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
 attachPresetToRoot
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
20
 attachPresetToSlot
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
20
 dispatchDisplayBuilderEvent
0.00% covered (danger)
0.00%
0 / 2
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
 dispatchDisplayBuilderEventWithRenderApi
0.00% covered (danger)
0.00%
0 / 3
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
 validateIslandForm
0.00% covered (danger)
0.00%
0 / 24
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
56
 responseMessageError
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 recursiveRefreshNodeId
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
20
 cleanNodeId
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
30
1<?php
2
3declare(strict_types=1);
4
5namespace Drupal\display_builder\Controller;
6
7use Drupal\Component\Datetime\TimeInterface;
8use Drupal\Core\Entity\FieldableEntityInterface;
9use Drupal\Core\Form\FormAjaxException;
10use Drupal\Core\Form\FormState;
11use Drupal\Core\Form\FormStateInterface;
12use Drupal\Core\Plugin\Context\ContextInterface;
13use Drupal\Core\Render\BareHtmlPageRendererInterface;
14use Drupal\Core\Render\HtmlResponse;
15use Drupal\Core\Render\RendererInterface;
16use Drupal\Core\StringTranslation\TranslatableMarkup;
17use Drupal\Core\TempStore\SharedTempStoreFactory;
18use Drupal\display_builder\Event\DisplayBuilderEvents;
19use Drupal\display_builder\InstanceInterface;
20use Drupal\display_builder\IslandPluginManagerInterface;
21use Drupal\display_builder\Plugin\display_builder\Island\ContextualFormPanel;
22use Drupal\display_builder\RenderableBuilderTrait;
23use Drupal\display_builder_entity_view\Field\DisplayBuilderItemList;
24use Symfony\Component\DependencyInjection\Attribute\Autowire;
25use Symfony\Component\HttpFoundation\Request;
26use Symfony\Component\HttpFoundation\Session\SessionInterface;
27use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
28
29/**
30 * Returns responses for Display builder routes.
31 */
32class ApiController extends ApiControllerBase implements ApiControllerInterface {
33
34  use RenderableBuilderTrait;
35
36  public function __construct(
37    protected EventDispatcherInterface $eventDispatcher,
38    protected RendererInterface $renderer,
39    protected TimeInterface $time,
40    #[Autowire(service: 'tempstore.shared')]
41    protected SharedTempStoreFactory $sharedTempStoreFactory,
42    protected SessionInterface $session,
43    private IslandPluginManagerInterface $islandPluginManager,
44    private BareHtmlPageRendererInterface $bareHtmlPageRenderer,
45  ) {
46    parent::__construct($eventDispatcher, $renderer, $time, $sharedTempStoreFactory, $session);
47  }
48
49  /**
50   * {@inheritdoc}
51   */
52  public function attachToRoot(Request $request, InstanceInterface $display_builder_instance): HtmlResponse {
53    $position = (int) $request->request->get('position', 0);
54
55    if ($request->request->has('preset_id')) {
56      $preset_id = (string) $request->request->get('preset_id');
57
58      return $this->attachPresetToRoot($display_builder_instance, $preset_id, $position);
59    }
60
61    $is_move = FALSE;
62
63    if ($request->request->has('node_id')) {
64      $node_id = (string) $request->request->get('node_id');
65
66      if (!$display_builder_instance->moveToRoot($node_id, $position)) {
67        $message = $this->t('[attachToRoot] moveToRoot failed with invalid data');
68
69        return $this->responseMessageError((string) $display_builder_instance->id(), $message, $request->request->all());
70      }
71
72      $is_move = TRUE;
73    }
74    elseif ($request->request->has('source_id')) {
75      $source_id = (string) $request->request->get('source_id');
76      $data = $request->request->has('source') ? \json_decode((string) $request->request->get('source'), TRUE) : [];
77      $node_id = $display_builder_instance->attachToRoot($position, $source_id, $data);
78    }
79    else {
80      $message = '[attachToRoot] Missing content (source_id, node_id or preset_id)';
81
82      return $this->responseMessageError((string) $display_builder_instance->id(), $message, $request->request->all());
83    }
84    $display_builder_instance->save();
85
86    $this->builder = $display_builder_instance;
87    // Let's refresh when we add new source to get the placeholder replacement.
88    $this->islandId = $is_move ? (string) $request->query->get('from', NULL) : NULL;
89
90    return $this->dispatchDisplayBuilderEvent(
91      $is_move ? DisplayBuilderEvents::ON_MOVE : DisplayBuilderEvents::ON_ATTACH_TO_ROOT,
92      NULL,
93      $node_id,
94    );
95  }
96
97  /**
98   * {@inheritdoc}
99   */
100  public function attachToSlot(Request $request, InstanceInterface $display_builder_instance, string $node_id, string $slot): HtmlResponse {
101    $parent_id = $node_id;
102    $position = (int) $request->request->get('position', 0);
103
104    if ($request->request->has('preset_id')) {
105      $preset_id = (string) $request->request->get('preset_id');
106
107      return $this->attachPresetToSlot($display_builder_instance, $preset_id, $parent_id, $slot, $position);
108    }
109
110    $is_move = FALSE;
111
112    // First, we update the data state.
113    if ($request->request->has('node_id')) {
114      $node_id = (string) $request->request->get('node_id');
115
116      if (!$display_builder_instance->moveToSlot($node_id, $parent_id, $slot, $position)) {
117        $message = $this->t('[attachToRoot] moveToRoot failed with invalid data');
118
119        return $this->responseMessageError((string) $display_builder_instance->id(), $message, $request->request->all());
120      }
121
122      $is_move = TRUE;
123    }
124    elseif ($request->request->has('source_id')) {
125      $source_id = (string) $request->request->get('source_id');
126      $data = $request->request->has('source') ? \json_decode((string) $request->request->get('source'), TRUE) : [];
127      $node_id = $display_builder_instance->attachToSlot($parent_id, $slot, $position, $source_id, $data);
128    }
129    else {
130      $message = $this->t('[attachToSlot] Missing content (component_id, block_id or node_id)');
131      $debug = [
132        'node_id' => $node_id,
133        'slot' => $slot,
134        'request' => $request->request->all(),
135      ];
136
137      return $this->responseMessageError((string) $display_builder_instance->id(), $message, $debug);
138    }
139    $display_builder_instance->save();
140
141    $this->builder = $display_builder_instance;
142    // Let's refresh when we add new source to get the placeholder replacement.
143    $this->islandId = $is_move ? (string) $request->query->get('from', NULL) : NULL;
144
145    return $this->dispatchDisplayBuilderEvent(
146      $is_move ? DisplayBuilderEvents::ON_MOVE : DisplayBuilderEvents::ON_ATTACH_TO_ROOT,
147      NULL,
148      $node_id,
149      $parent_id,
150    );
151  }
152
153  /**
154   * {@inheritdoc}
155   */
156  public function get(Request $request, InstanceInterface $display_builder_instance, string $node_id): array {
157    $this->builder = $display_builder_instance;
158
159    return $this->dispatchDisplayBuilderEventWithRenderApi(
160      DisplayBuilderEvents::ON_ACTIVE,
161      $display_builder_instance->get($node_id),
162    );
163  }
164
165  /**
166   * {@inheritdoc}
167   */
168  public function update(Request $request, InstanceInterface $display_builder_instance, string $node_id): HtmlResponse {
169    $this->builder = $display_builder_instance;
170    $body = $request->getPayload()->all();
171
172    if (!isset($body['form_id'])) {
173      $message = $this->t('[update] Missing payload!');
174
175      return $this->responseMessageError((string) $display_builder_instance->id(), $message, $body);
176    }
177
178    // Load the instance to properly alter the form data into config data.
179    $instance = $display_builder_instance->get($node_id);
180
181    if (isset($body['source']['form_build_id'])) {
182      unset($body['source']['form_build_id'], $body['source']['form_token'], $body['source']['form_id']);
183    }
184
185    if (isset($body['form_build_id'])) {
186      unset($body['form_build_id'], $body['form_token'], $body['form_id']);
187    }
188    $form_state = new FormState();
189    // Default values are the existing values from the state.
190    $form_state->addBuildInfo('args', [
191      [
192        'island_id' => 'contextual_form',
193        'builder_id' => (string) $display_builder_instance->id(),
194        'instance' => $instance,
195      ],
196      $display_builder_instance->getContexts(),
197    ]);
198    $form_state->setTemporaryValue('gathered_contexts', $display_builder_instance->getContexts());
199    // The body received corresponds to raw form values.
200    // We need to set them in the form state to properly
201    // take them into account.
202    $form_state->setValues($body);
203
204    $formClass = ContextualFormPanel::getFormClass();
205    $data = [];
206
207    try {
208      $values = $this->validateIslandForm($formClass, $form_state);
209      $data['source'] = $values;
210    }
211    catch (FormAjaxException $e) {
212      throw $e;
213    }
214    catch (\Exception $e) {
215      return $this->responseMessageError((string) $display_builder_instance->id(), $e->getMessage(), []);
216    }
217
218    if (isset($instance['source']['component']['slots'], $data['source']['component'])
219      && ($data['source']['component']['component_id'] === $instance['source']['component']['component_id'])) {
220      // We keep the slots.
221      $data['source']['component']['slots'] = $instance['source']['component']['slots'];
222    }
223
224    $display_builder_instance->setSource($node_id, $instance['source_id'], $data['source']);
225    $display_builder_instance->save();
226
227    $this->builder = $display_builder_instance;
228    $this->islandId = (string) $request->query->get('from', NULL);
229
230    return $this->dispatchDisplayBuilderEvent(
231      DisplayBuilderEvents::ON_UPDATE,
232      NULL,
233      $node_id,
234    );
235  }
236
237  /**
238   * {@inheritdoc}
239   */
240  public function thirdPartySettingsUpdate(Request $request, InstanceInterface $display_builder_instance, string $node_id, string $island_id): HtmlResponse {
241    $body = $request->getPayload()->all();
242
243    if (!isset($body['form_id'])) {
244      $message = $this->t('[thirdPartySettingsUpdate] Missing payload!');
245
246      return $this->responseMessageError((string) $display_builder_instance->id(), $message, $body);
247    }
248
249    $islandDefinition = $this->islandPluginManager->getDefinition($island_id);
250    // Load the instance to properly alter the form data into config data.
251    $instance = $display_builder_instance->get($node_id);
252    unset($body['form_build_id'], $body['form_token'], $body['form_id']);
253
254    $form_state = new FormState();
255    // Default values are the existing values from the state.
256    $form_state->addBuildInfo('args', [
257      [
258        'island_id' => $island_id,
259        'builder_id' => (string) $display_builder_instance->id(),
260        'instance' => $instance,
261      ],
262      [],
263    ]);
264
265    // phpcs:disable Drupal.Files.LineLength.TooLong
266    // @todo should context be injected for third party settings?
267    // $form_state->setTemporaryValue('gathered_contexts', $display_builder_instance->getContexts());
268    // phpcs:enable Drupal.Files.LineLength.TooLong
269    // The body received corresponds to raw form values.
270    // We need to set them in the form state to properly
271    // take them into account.
272    $form_state->setValues($body);
273
274    $formClass = ($islandDefinition['class'])::getFormClass();
275    $values = $this->validateIslandForm($formClass, $form_state);
276    // We update the state with the new data.
277    $display_builder_instance->setThirdPartySettings($node_id, $island_id, $values);
278    $display_builder_instance->save();
279
280    $this->builder = $display_builder_instance;
281    $this->islandId = $island_id;
282
283    return $this->dispatchDisplayBuilderEvent(
284      DisplayBuilderEvents::ON_UPDATE,
285      NULL,
286      $node_id,
287      NULL,
288    );
289  }
290
291  /**
292   * {@inheritdoc}
293   */
294  public function paste(Request $request, InstanceInterface $display_builder_instance, string $node_id, string $parent_id, string $slot_id, string $slot_position): HtmlResponse {
295    $this->builder = $display_builder_instance;
296    $dataToCopy = $display_builder_instance->get($node_id);
297
298    // Keep flag for move or attach to root.
299    $is_paste_root = FALSE;
300
301    if (isset($dataToCopy['source_id'], $dataToCopy['source'])) {
302      $source_id = $dataToCopy['source_id'];
303      $data = $dataToCopy['source'];
304
305      self::recursiveRefreshNodeId($data);
306
307      // If no parent we are on root.
308      // @todo for duplicate and not parent root seems not detected and copy is inside the slot.
309      if ($parent_id === '__root__') {
310        $is_paste_root = TRUE;
311        $display_builder_instance->attachToRoot(0, $source_id, $data, $dataToCopy['_third_party_settings'] ?? []);
312      }
313      else {
314        $display_builder_instance->attachToSlot($parent_id, $slot_id, (int) $slot_position, $source_id, $data, $dataToCopy['_third_party_settings'] ?? []);
315      }
316    }
317    $display_builder_instance->save();
318
319    $this->builder = $display_builder_instance;
320
321    return $this->dispatchDisplayBuilderEvent(
322      $is_paste_root ? DisplayBuilderEvents::ON_MOVE : DisplayBuilderEvents::ON_ATTACH_TO_ROOT,
323      NULL,
324      $parent_id,
325    );
326  }
327
328  /**
329   * {@inheritdoc}
330   */
331  public function delete(Request $request, InstanceInterface $display_builder_instance, string $node_id): HtmlResponse {
332    $current = $display_builder_instance->getCurrentState();
333    $parent_id = $display_builder_instance->getParentId($current, $node_id);
334    $display_builder_instance->remove($node_id);
335    $display_builder_instance->save();
336
337    $this->builder = $display_builder_instance;
338
339    return $this->dispatchDisplayBuilderEvent(
340      DisplayBuilderEvents::ON_DELETE,
341      NULL,
342      $node_id,
343      $parent_id
344    );
345  }
346
347  /**
348   * {@inheritdoc}
349   */
350  public function saveAsPreset(Request $request, InstanceInterface $display_builder_instance, string $node_id): HtmlResponse {
351    $label = (string) $this->t('New preset');
352    $data = $display_builder_instance->get($node_id);
353    self::cleanNodeId($data);
354
355    $preset_storage = $this->entityTypeManager()->getStorage('pattern_preset');
356    $preset = $preset_storage->create([
357      'id' => \uniqid(),
358      'label' => $request->headers->get('hx-prompt', $label) ?: $label,
359      'status' => TRUE,
360      'group' => '',
361      'description' => '',
362      'sources' => $data,
363    ]);
364    $preset->save();
365
366    $this->builder = $display_builder_instance;
367
368    return $this->dispatchDisplayBuilderEvent(DisplayBuilderEvents::ON_PRESET_SAVE);
369  }
370
371  /**
372   * {@inheritdoc}
373   */
374  public function save(Request $request, InstanceInterface $display_builder_instance): HtmlResponse {
375    $display_builder_instance->setSave($display_builder_instance->getCurrentState());
376    $display_builder_instance->save();
377
378    $this->builder = $display_builder_instance;
379
380    return $this->dispatchDisplayBuilderEvent(
381      DisplayBuilderEvents::ON_SAVE,
382      $display_builder_instance->getContexts()
383    );
384  }
385
386  /**
387   * {@inheritdoc}
388   */
389  public function restore(Request $request, InstanceInterface $display_builder_instance): HtmlResponse {
390    $display_builder_instance->restore();
391    $display_builder_instance->save();
392
393    $this->builder = $display_builder_instance;
394
395    // @todo on history change is closest to a data change that we need here
396    // without any instance id. Perhaps we need a new event?
397    return $this->dispatchDisplayBuilderEvent(DisplayBuilderEvents::ON_HISTORY_CHANGE);
398  }
399
400  /**
401   * {@inheritdoc}
402   */
403  public function revert(Request $request, InstanceInterface $display_builder_instance): HtmlResponse {
404    $instanceInfos = DisplayBuilderItemList::checkInstanceId((string) $display_builder_instance->id());
405
406    if (isset($instanceInfos['entity_type_id'], $instanceInfos['entity_id'], $instanceInfos['field_name'])) {
407      // Do not get the profile entity ID from Instance context because the
408      // data stored there is not reliable yet.
409      // See: https://www.drupal.org/project/display_builder/issues/3544545
410      $entity = $this->entityTypeManager()->getStorage($instanceInfos['entity_type_id'])
411        ->load($instanceInfos['entity_id']);
412
413      if ($entity instanceof FieldableEntityInterface) {
414        // Remove the saved state as the field values will be deleted.
415        $display_builder_instance->setNewPresent([], 'Revert 1/2: clear overridden data and save');
416        $display_builder_instance->save();
417        $display_builder_instance->setSave($display_builder_instance->getCurrentState());
418
419        // Clear field value.
420        $field = $entity->get($instanceInfos['field_name']);
421        $field->setValue(NULL);
422        $entity->save();
423
424        // Repopulate the Instance entity from the entity view display config.
425        $data = $display_builder_instance->toArray();
426
427        if (isset($data['contexts']['view_mode'])
428          && $data['contexts']['view_mode'] instanceof ContextInterface
429        ) {
430          $viewMode = $data['contexts']['view_mode']->getContextValue();
431          $display_id = "{$instanceInfos['entity_type_id']}.{$entity->bundle()}.{$viewMode}";
432
433          /** @var \Drupal\display_builder\DisplayBuildableInterface|null $display */
434          $display = $this->entityTypeManager()->getStorage('entity_view_display')
435            ->load($display_id);
436
437          $sources = $display->getSources();
438          $display_builder_instance->setNewPresent($sources, 'Revert 2/2: retrieve existing data from config');
439          $display_builder_instance->save();
440        }
441      }
442    }
443
444    $this->builder = $display_builder_instance;
445
446    // @todo on history change is closest to a data change that we need here
447    // without any instance id. Perhaps we need a new event?
448    return $this->dispatchDisplayBuilderEvent(DisplayBuilderEvents::ON_HISTORY_CHANGE);
449  }
450
451  /**
452   * {@inheritdoc}
453   */
454  public function undo(Request $request, InstanceInterface $display_builder_instance): HtmlResponse {
455    $display_builder_instance->undo();
456    $display_builder_instance->save();
457
458    $this->builder = $display_builder_instance;
459
460    return $this->dispatchDisplayBuilderEvent(DisplayBuilderEvents::ON_HISTORY_CHANGE);
461  }
462
463  /**
464   * {@inheritdoc}
465   */
466  public function redo(Request $request, InstanceInterface $display_builder_instance): HtmlResponse {
467    $display_builder_instance->redo();
468    $display_builder_instance->save();
469
470    $this->builder = $display_builder_instance;
471
472    return $this->dispatchDisplayBuilderEvent(DisplayBuilderEvents::ON_HISTORY_CHANGE);
473  }
474
475  /**
476   * {@inheritdoc}
477   */
478  public function clear(Request $request, InstanceInterface $display_builder_instance): HtmlResponse {
479    $display_builder_instance->clear();
480    $display_builder_instance->save();
481
482    $this->builder = $display_builder_instance;
483
484    return $this->dispatchDisplayBuilderEvent(DisplayBuilderEvents::ON_HISTORY_CHANGE);
485  }
486
487  /**
488   * Attach a pattern preset to root.
489   *
490   * Presets are "resolved" after attachment, so they are never moved around.
491   *
492   * @param \Drupal\display_builder\InstanceInterface $display_builder_instance
493   *   Display builder instance.
494   * @param string $preset_id
495   *   Pattern preset ID.
496   * @param int $position
497   *   Position.
498   *
499   * @return \Drupal\Core\Render\HtmlResponse
500   *   The HTML response.
501   */
502  protected function attachPresetToRoot(InstanceInterface $display_builder_instance, string $preset_id, int $position): HtmlResponse {
503    $presetStorage = $this->entityTypeManager()->getStorage('pattern_preset');
504
505    /** @var \Drupal\display_builder\PatternPresetInterface $preset */
506    $preset = $presetStorage->load($preset_id);
507    $data = $preset->getSources();
508
509    if (!isset($data['source_id']) || !isset($data['source'])) {
510      $message = $this->t('[attachToRoot] Missing preset source_id data');
511
512      return $this->responseMessageError((string) $display_builder_instance->id(), $message, $data);
513    }
514    $node_id = $display_builder_instance->attachToRoot($position, $data['source_id'], $data['source']);
515
516    foreach ($data['_third_party_settings'] ?? [] as $provider => $settings) {
517      $display_builder_instance->setThirdPartySettings($node_id, $provider, $settings ?? []);
518    }
519    $this->builder = $display_builder_instance;
520
521    return $this->dispatchDisplayBuilderEvent(
522      DisplayBuilderEvents::ON_ATTACH_TO_ROOT,
523      NULL,
524      $node_id,
525    );
526  }
527
528  /**
529   * Attach a pattern preset to a slot .
530   *
531   * Presets are "resolved" after attachment, so they are never moved around.
532   *
533   * @param \Drupal\display_builder\InstanceInterface $display_builder_instance
534   *   Display builder instance.
535   * @param string $preset_id
536   *   Pattern preset ID.
537   * @param string $parent_id
538   *   Parent instance ID.
539   * @param string $slot
540   *   Slot.
541   * @param int $position
542   *   Position.
543   *
544   * @return \Drupal\Core\Render\HtmlResponse
545   *   The HTML response.
546   */
547  protected function attachPresetToSlot(InstanceInterface $display_builder_instance, string $preset_id, string $parent_id, string $slot, int $position): HtmlResponse {
548    $presetStorage = $this->entityTypeManager()->getStorage('pattern_preset');
549
550    /** @var \Drupal\display_builder\PatternPresetInterface $preset */
551    $preset = $presetStorage->load($preset_id);
552    $data = $preset->getSources();
553
554    if (!isset($data['source_id']) || !isset($data['source'])) {
555      $message = $this->t('[attachToSlot] Missing preset source_id data');
556
557      return $this->responseMessageError((string) $display_builder_instance->id(), $message, $data);
558    }
559    $node_id = $display_builder_instance->attachToSlot($parent_id, $slot, $position, $data['source_id'], $data['source']);
560
561    foreach ($data['_third_party_settings'] ?? [] as $provider => $settings) {
562      $display_builder_instance->setThirdPartySettings($node_id, $provider, $settings ?? []);
563    }
564
565    $this->builder = $display_builder_instance;
566
567    return $this->dispatchDisplayBuilderEvent(
568      DisplayBuilderEvents::ON_ATTACH_TO_ROOT,
569      NULL,
570      $node_id,
571    );
572  }
573
574  /**
575   * Dispatches a display builder event.
576   *
577   * @param string $event_id
578   *   The event ID.
579   * @param array|null $data
580   *   The data.
581   * @param string|null $node_id
582   *   Optional instance ID.
583   * @param string|null $parent_id
584   *   Optional parent ID.
585   *
586   * @return \Drupal\Core\Render\HtmlResponse
587   *   The HTML response.
588   */
589  protected function dispatchDisplayBuilderEvent(
590    string $event_id,
591    ?array $data = NULL,
592    ?string $node_id = NULL,
593    ?string $parent_id = NULL,
594  ): HtmlResponse {
595    $result = $this->dispatchDisplayBuilderEventWithRenderApi($event_id, $data, $node_id, $parent_id);
596
597    return $this->bareHtmlPageRenderer->renderBarePage($result, '', 'markup');
598  }
599
600  /**
601   * Dispatches a display builder event with render API.
602   *
603   * @param string $event_id
604   *   The event ID.
605   * @param array|null $data
606   *   The data.
607   * @param string|null $node_id
608   *   Optional instance ID.
609   * @param string|null $parent_id
610   *   Optional parent ID.
611   *
612   * @return array
613   *   The render array result of the event.
614   */
615  protected function dispatchDisplayBuilderEventWithRenderApi(
616    string $event_id,
617    ?array $data = NULL,
618    ?string $node_id = NULL,
619    ?string $parent_id = NULL,
620  ): array {
621    $event = $this->createEventWithEnabledIsland($event_id, $data, $node_id, $parent_id);
622    $this->saveSseData($event_id);
623
624    return $event->getResult();
625  }
626
627  /**
628   * Validates an island form.
629   *
630   * @param string $formClass
631   *   The form class.
632   * @param \Drupal\Core\Form\FormStateInterface $form_state
633   *   The form state.
634   *
635   * @return array
636   *   The validated values.
637   */
638  private function validateIslandForm(string $formClass, FormStateInterface $form_state): array {
639    /** @var \Drupal\Core\Form\FormBuilder $formBuilder */
640    $formBuilder = $this->formBuilder();
641
642    try {
643      $triggering_element = $form_state->getTriggeringElement();
644
645      if (!$triggering_element && !isset($form_state->getValues()['_triggering_element_name'])) {
646        // We set a fake triggering element to avoid form API error.
647        $form_state->setTriggeringElement([
648          '#type' => 'submit',
649          '#limit_validation_errors' => FALSE,
650          '#value' => (string) $this->t('Submit'),
651        ]);
652      }
653      $form = $formBuilder->buildForm($formClass, $form_state);
654      $formErrors = $form_state->getErrors();
655
656      if (!empty($formErrors)) {
657        $first_error = \reset($formErrors);
658
659        throw new \Exception((string) $first_error);
660      }
661      $formBuilder->validateForm($formClass, $form, $form_state);
662      $formErrors = $form_state->getErrors();
663
664      if (!empty($formErrors)) {
665        $first_error = \reset($formErrors);
666
667        throw new \Exception((string) $first_error);
668      }
669    }
670    catch (FormAjaxException $e) {
671      throw $e;
672    }
673    // Those values are the validated values, produced by the form.
674    // with all Form API processing.
675    $values = $form_state->getValues();
676
677    // We clean the values from form API keys.
678    if (isset($values['form_build_id'])) {
679      unset($values['form_build_id'], $values['form_token'], $values['form_id']);
680    }
681
682    return $values;
683  }
684
685  /**
686   * Render an error message in the display builder.
687   *
688   * @param string $display_builder_instance_id
689   *   The builder ID.
690   * @param string|\Drupal\Core\StringTranslation\TranslatableMarkup $message
691   *   The error message.
692   * @param array $debug
693   *   The debug code.
694   *
695   * @return \Drupal\Core\Render\HtmlResponse
696   *   The response with the component.
697   */
698  private function responseMessageError(
699    string $display_builder_instance_id,
700    string|TranslatableMarkup $message,
701    array $debug,
702  ): HtmlResponse {
703    $build = $this->buildError($display_builder_instance_id, $message, \print_r($debug, TRUE), NULL, TRUE);
704
705    $html = $this->renderer->renderInIsolation($build);
706    $response = new HtmlResponse();
707    $response->setContent($html);
708
709    return $response;
710  }
711
712  /**
713   * Recursively regenerate the node_id key.
714   *
715   * @param array $array
716   *   The array reference.
717   */
718  private static function recursiveRefreshNodeId(array &$array): void {
719    if (isset($array['node_id'])) {
720      $array['node_id'] = \uniqid();
721    }
722
723    foreach ($array as &$value) {
724      if (\is_array($value)) {
725        self::recursiveRefreshNodeId($value);
726      }
727    }
728  }
729
730  /**
731   * Recursively regenerate the node_id key.
732   *
733   * @param array $array
734   *   The array reference.
735   *
736   * @todo set as utils because clone in ExportForm.php?
737   */
738  private static function cleanNodeId(array &$array): void {
739    unset($array['node_id']);
740
741    foreach ($array as $key => &$value) {
742      if (\is_array($value)) {
743        self::cleanNodeId($value);
744
745        if (isset($value['source_id'], $value['source']['value']) && empty($value['source']['value'])) {
746          unset($array[$key]);
747        }
748      }
749    }
750  }
751
752}