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