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}

Branches

Below are the source code lines that represent each code branch as identified by Xdebug. Please note a branch is not necessarily coterminous with a line, a line may contain multiple branches and therefore show up more than once. Please also be aware that some branches may be implicit rather than explicit, e.g. an if statement always has an else as part of its logical flow even if you didn't write one.

ApiController->attachPresetToRoot
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'])) {
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);
514    $node_id = $display_builder_instance->attachToRoot($position, $data['source_id'], $data['source']);
515
516    foreach ($data['_third_party_settings'] ?? [] as $provider => $settings) {
516    foreach ($data['_third_party_settings'] ?? [] as $provider => $settings) {
516    foreach ($data['_third_party_settings'] ?? [] as $provider => $settings) {
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,
ApiController->attachPresetToSlot
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'])) {
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);
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) {
561    foreach ($data['_third_party_settings'] ?? [] as $provider => $settings) {
561    foreach ($data['_third_party_settings'] ?? [] as $provider => $settings) {
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,
ApiController->attachToRoot
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);
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());
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;
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) : [];
76      $data = $request->request->has('source') ? \json_decode((string) $request->request->get('source'), TRUE) : [];
76      $data = $request->request->has('source') ? \json_decode((string) $request->request->get('source'), TRUE) : [];
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) : [];
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());
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;
88    $this->islandId = $is_move ? (string) $request->query->get('from', NULL) : NULL;
88    $this->islandId = $is_move ? (string) $request->query->get('from', NULL) : NULL;
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,
91      $is_move ? DisplayBuilderEvents::ON_MOVE : DisplayBuilderEvents::ON_ATTACH_TO_ROOT,
91      $is_move ? DisplayBuilderEvents::ON_MOVE : DisplayBuilderEvents::ON_ATTACH_TO_ROOT,
91      $is_move ? DisplayBuilderEvents::ON_MOVE : DisplayBuilderEvents::ON_ATTACH_TO_ROOT,
ApiController->attachToSlot
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);
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());
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;
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) : [];
126      $data = $request->request->has('source') ? \json_decode((string) $request->request->get('source'), TRUE) : [];
126      $data = $request->request->has('source') ? \json_decode((string) $request->request->get('source'), TRUE) : [];
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) : [];
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);
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;
143    $this->islandId = $is_move ? (string) $request->query->get('from', NULL) : NULL;
143    $this->islandId = $is_move ? (string) $request->query->get('from', NULL) : NULL;
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,
146      $is_move ? DisplayBuilderEvents::ON_MOVE : DisplayBuilderEvents::ON_ATTACH_TO_ROOT,
146      $is_move ? DisplayBuilderEvents::ON_MOVE : DisplayBuilderEvents::ON_ATTACH_TO_ROOT,
146      $is_move ? DisplayBuilderEvents::ON_MOVE : DisplayBuilderEvents::ON_ATTACH_TO_ROOT,
ApiController->cleanNodeId
738  private static function cleanNodeId(array &$array): void {
739    unset($array['node_id']);
740
741    foreach ($array as $key => &$value) {
741    foreach ($array as $key => &$value) {
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'])) {
745        if (isset($value['source_id'], $value['source']['value']) && empty($value['source']['value'])) {
745        if (isset($value['source_id'], $value['source']['value']) && empty($value['source']['value'])) {
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]);
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  }
ApiController->clear
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);
ApiController->delete
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,
ApiController->dispatchDisplayBuilderEvent
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');
ApiController->dispatchDisplayBuilderEventWithRenderApi
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();
ApiController->get
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),
ApiController->paste
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'])) {
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__') {
309      if ($parent_id === '__root__') {
310        $is_paste_root = TRUE;
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();
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,
322      $is_paste_root ? DisplayBuilderEvents::ON_MOVE : DisplayBuilderEvents::ON_ATTACH_TO_ROOT,
322      $is_paste_root ? DisplayBuilderEvents::ON_MOVE : DisplayBuilderEvents::ON_ATTACH_TO_ROOT,
322      $is_paste_root ? DisplayBuilderEvents::ON_MOVE : DisplayBuilderEvents::ON_ATTACH_TO_ROOT,
ApiController->recursiveRefreshNodeId
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) {
723    foreach ($array as &$value) {
723    foreach ($array as &$value) {
724      if (\is_array($value)) {
723    foreach ($array as &$value) {
724      if (\is_array($value)) {
725        self::recursiveRefreshNodeId($value);
723    foreach ($array as &$value) {
724      if (\is_array($value)) {
725        self::recursiveRefreshNodeId($value);
726      }
727    }
728  }
ApiController->redo
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);
ApiController->responseMessageError
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;
ApiController->restore
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);
ApiController->revert
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'])) {
406    if (isset($instanceInfos['entity_type_id'], $instanceInfos['entity_id'], $instanceInfos['field_name'])) {
406    if (isset($instanceInfos['entity_type_id'], $instanceInfos['entity_id'], $instanceInfos['field_name'])) {
410      $entity = $this->entityTypeManager()->getStorage($instanceInfos['entity_type_id'])
411        ->load($instanceInfos['entity_id']);
412
413      if ($entity instanceof FieldableEntityInterface) {
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
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;
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);
ApiController->save
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()
ApiController->saveAsPreset
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);
ApiController->thirdPartySettingsUpdate
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);
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,
ApiController->undo
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);
ApiController->update
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);
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'])) {
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();
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;
211    catch (FormAjaxException $e) {
212      throw $e;
214    catch (\Exception $e) {
215      return $this->responseMessageError((string) $display_builder_instance->id(), $e->getMessage(), []);
218    if (isset($instance['source']['component']['slots'], $data['source']['component'])
218    if (isset($instance['source']['component']['slots'], $data['source']['component'])
219      && ($data['source']['component']['component_id'] === $instance['source']['component']['component_id'])) {
221      $data['source']['component']['slots'] = $instance['source']['component']['slots'];
222    }
223
224    $display_builder_instance->setSource($node_id, $instance['source_id'], $data['source']);
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,
ApiController->validateIslandForm
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'])) {
645      if (!$triggering_element && !isset($form_state->getValues()['_triggering_element_name'])) {
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);
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);
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);
670    catch (FormAjaxException $e) {
671      throw $e;
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;
682    return $values;