Code Coverage
 
Lines
Branches
Paths
Functions and Methods
Classes and Traits
Total
91.49% covered (success)
91.49%
215 / 235
81.29% covered (warning)
81.29%
126 / 155
49.61% covered (danger)
49.61%
64 / 129
73.17% covered (warning)
73.17%
30 / 41
CRAP
0.00% covered (danger)
0.00%
0 / 1
Instance
91.49% covered (success)
91.49%
215 / 235
81.29% covered (warning)
81.29%
126 / 155
49.61% covered (danger)
49.61%
64 / 129
78.05% covered (warning)
78.05%
32 / 41
1150.38
0.00% covered (danger)
0.00%
0 / 1
 __construct
60.00% covered (warning)
60.00%
3 / 5
50.00% covered (danger)
50.00%
3 / 6
25.00% covered (danger)
25.00%
1 / 4
0.00% covered (danger)
0.00%
0 / 1
6.80
 baseFieldDefinitions
100.00% covered (success)
100.00%
9 / 9
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
 isNew
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 label
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 toArray
100.00% covered (success)
100.00%
9 / 9
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
 postCreate
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 getProfile
100.00% covered (success)
100.00%
3 / 3
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
 setProfile
100.00% covered (success)
100.00%
1 / 1
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
 moveToRoot
77.78% covered (warning)
77.78%
7 / 9
60.00% covered (warning)
60.00%
3 / 5
33.33% covered (danger)
33.33%
1 / 3
0.00% covered (danger)
0.00%
0 / 1
5.67
 moveToSlot
85.71% covered (warning)
85.71%
12 / 14
62.50% covered (warning)
62.50%
5 / 8
25.00% covered (danger)
25.00%
1 / 4
0.00% covered (danger)
0.00%
0 / 1
10.75
 attachToRoot
77.78% covered (warning)
77.78%
7 / 9
33.33% covered (danger)
33.33%
2 / 6
25.00% covered (danger)
25.00%
1 / 4
0.00% covered (danger)
0.00%
0 / 1
6.80
 attachToSlot
87.50% covered (warning)
87.50%
14 / 16
54.55% covered (warning)
54.55%
6 / 11
22.22% covered (danger)
22.22%
2 / 9
0.00% covered (danger)
0.00%
0 / 1
16.76
 getNode
100.00% covered (success)
100.00%
1 / 1
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
 getParentId
100.00% covered (success)
100.00%
1 / 1
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
 setSource
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 setThirdPartySettings
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 remove
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
9 / 9
60.00% covered (warning)
60.00%
3 / 5
100.00% covered (success)
100.00%
1 / 1
5.02
 getContexts
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 setSave
100.00% covered (success)
100.00%
4 / 4
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
 getCurrentState
100.00% covered (success)
100.00%
1 / 1
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
 restore
100.00% covered (success)
100.00%
2 / 2
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
 undo
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 redo
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 clear
100.00% covered (success)
100.00%
2 / 2
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
 getPast
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
4 / 4
66.67% covered (warning)
66.67%
2 / 3
100.00% covered (success)
100.00%
1 / 1
2.15
 getFuture
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
4 / 4
66.67% covered (warning)
66.67%
2 / 3
100.00% covered (success)
100.00%
1 / 1
2.15
 getUsers
88.89% covered (warning)
88.89%
8 / 9
83.33% covered (warning)
83.33%
10 / 12
11.11% covered (danger)
11.11%
1 / 9
0.00% covered (danger)
0.00%
0 / 1
31.28
 isPublishable
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
5 / 5
50.00% covered (danger)
50.00%
2 / 4
100.00% covered (success)
100.00%
1 / 1
4.12
 hasSaveContextsRequirement
100.00% covered (success)
100.00%
6 / 6
90.00% covered (success)
90.00%
9 / 10
18.75% covered (danger)
18.75%
3 / 16
100.00% covered (success)
100.00%
1 / 1
18.41
 isPublished
100.00% covered (success)
100.00%
1 / 1
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
 isPublishedPresent
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
7 / 7
33.33% covered (danger)
33.33%
2 / 6
100.00% covered (success)
100.00%
1 / 1
8.74
 getPathIndex
100.00% covered (success)
100.00%
1 / 1
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
 setNewPresent
100.00% covered (success)
100.00%
19 / 19
100.00% covered (success)
100.00%
11 / 11
56.25% covered (warning)
56.25%
9 / 16
100.00% covered (success)
100.00%
1 / 1
9.01
 getCurrent
100.00% covered (success)
100.00%
2 / 2
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
 getUniqId
100.00% covered (success)
100.00%
1 / 1
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
 getSourceTree
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 sampleEntityGenerator
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 slotSourceProxy
100.00% covered (success)
100.00%
1 / 1
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
 nodeLabel
100.00% covered (success)
100.00%
2 / 2
75.00% covered (warning)
75.00%
3 / 4
50.00% covered (danger)
50.00%
1 / 2
100.00% covered (success)
100.00%
1 / 1
2.50
 currentUser
100.00% covered (success)
100.00%
1 / 1
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
 refreshContexts
30.00% covered (danger)
30.00%
3 / 10
45.45% covered (danger)
45.45%
5 / 11
14.29% covered (danger)
14.29%
1 / 7
0.00% covered (danger)
0.00%
0 / 1
20.74
1<?php
2
3declare(strict_types=1);
4
5namespace Drupal\display_builder\Entity;
6
7use Drupal\Core\Entity\Attribute\ContentEntityType;
8use Drupal\Core\Entity\ContentEntityBase;
9use Drupal\Core\Entity\EntityStorageInterface;
10use Drupal\Core\Entity\EntityTypeInterface;
11use Drupal\Core\Entity\EntityTypeManagerInterface;
12use Drupal\Core\Field\BaseFieldDefinition;
13use Drupal\Core\Plugin\Context\EntityContext;
14use Drupal\Core\Session\AccountInterface;
15use Drupal\Core\StringTranslation\TranslatableMarkup;
16use Drupal\display_builder\Exception\InvalidNodeException;
17use Drupal\display_builder\InstanceAccessControlHandler;
18use Drupal\display_builder\InstanceInterface;
19use Drupal\display_builder\InstanceStorage;
20use Drupal\display_builder\Plugin\Field\FieldType\HistoryStep;
21use Drupal\display_builder\SlotSourceProxy;
22use Drupal\display_builder\SourceTree;
23use Drupal\display_builder_ui\InstanceListBuilder;
24use Drupal\ui_patterns\Entity\SampleEntityGeneratorInterface;
25use Drupal\ui_patterns\Plugin\Context\RequirementsContext;
26
27/**
28 * Defines the display builder instance entity class.
29 */
30#[ContentEntityType(
31  id: 'display_builder_instance',
32  label: new TranslatableMarkup('Display Builder instance'),
33  label_collection: new TranslatableMarkup('Display builder instances'),
34  label_singular: new TranslatableMarkup('display builder instance'),
35  label_plural: new TranslatableMarkup('display builder instances'),
36  entity_keys: [
37    'id' => 'id',
38  ],
39  handlers: [
40    'access' => InstanceAccessControlHandler::class,
41    'storage' => InstanceStorage::class,
42    // Managed by display_builder_ui.
43    'list_builder' => InstanceListBuilder::class,
44  ],
45  links: [
46    // Managed by display_builder_ui.
47    'collection' => '/admin/structure/display-builder/instances',
48  ],
49  label_count: [
50    'singular' => '@count instance',
51    'plural' => '@count instances',
52  ],
53)]
54class Instance extends ContentEntityBase implements InstanceInterface {
55
56  private const MAX_HISTORY = 10;
57
58  /**
59   * Current user.
60   */
61  public AccountInterface $currentUser;
62
63  /**
64   * Path index.
65   *
66   * A mapping where each key is an slot source node ID and each value has
67   * two properties:
68   * - path: the path
69   * - parent: the node ID of the parent. This is necessary because not every
70   *   SourceWithSlotsInterface implementations has the same "deepness". For
71   *   example, ComponentSource has 4 levels (component, slots, slot_id,
72   *   'sources), LayoutSource has 2 levels (regions, slot_id), etc.
73   */
74  protected array $pathIndex = [];
75
76  /**
77   * Entity type manager.
78   */
79  protected EntityTypeManagerInterface $entityTypeManager;
80
81  /**
82   * Sample entity generator.
83   */
84  protected SampleEntityGeneratorInterface $sampleEntityGenerator;
85
86  /**
87   * Slot source proxy for resolving node labels.
88   */
89  private SlotSourceProxy $slotSourceProxy;
90
91  /**
92   * Cached normalized source tree for the current present state.
93   *
94   * Stays valid after mutations (index=FALSE path) and is cleared on undo/redo
95   * when the present pointer jumps to a different history step.
96   */
97  private ?SourceTree $sourceTree = NULL;
98
99  /**
100   * {@inheritdoc}
101   */
102  public function __construct(array $values, mixed $entity_type, mixed $bundle = FALSE, mixed $translations = []) {
103    parent::__construct($values, $entity_type, $bundle, $translations);
104    $fields = $this->fieldDefinitions;
105
106    foreach ($values as $key => $value) {
107      if (isset($fields[$key])) {
108        $this->set($key, $value);
109      }
110    }
111  }
112
113  /**
114   * {@inheritdoc}
115   */
116  public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
117    $fields = parent::baseFieldDefinitions($entity_type);
118    // Override from ContentEntityBase.
119    $fields['id'] = BaseFieldDefinition::create('string');
120    // @todo replace by entity_reference.
121    $fields['profileId'] = BaseFieldDefinition::create('string');
122    $fields['contexts'] = BaseFieldDefinition::create('map');
123    // @todo replace by Revisions API.
124    $fields['present'] = BaseFieldDefinition::create('step');
125    $fields['past'] = BaseFieldDefinition::create('step')->setCardinality(-1);
126    $fields['future'] = BaseFieldDefinition::create('step')->setCardinality(-1);
127    $fields['save'] = BaseFieldDefinition::create('step');
128
129    return $fields;
130  }
131
132  /**
133   * {@inheritdoc}
134   */
135  public function isNew(): bool {
136    // We don't support enforceIsNew property because we have no practical
137    // use of it and because it seems to break the invalidation of
138    // ::getCacheTags().
139    return !$this->id();
140  }
141
142  /**
143   * {@inheritdoc}
144   */
145  public function label() {
146    // Extract a human readable name from an instance id.
147    // Example: "provider__my_display" -> "My display".
148    $parts = \explode('__', (string) $this->id());
149
150    if (\count($parts) > 1) {
151      \array_shift($parts);
152
153      return \ucfirst(\implode(' ', \str_replace('_', ' ', $parts)));
154    }
155
156    return (string) $this->id();
157  }
158
159  /**
160   * {@inheritdoc}
161   *
162   * @see \Drupal\Core\Entity\EntityInterface
163   */
164  public function toArray(): array {
165    return [
166      'id' => $this->id(),
167      'profileId' => $this->get('profileId')->getString(),
168      'contexts' => $this->get('contexts')->first()?->getValue(),
169      'past' => $this->get('past')->getValue(),
170      'present' => $this->get('present')->first()?->getValue(),
171      'future' => $this->get('future')->getValue(),
172      'save' => $this->get('save')->first()?->getValue(),
173    ];
174  }
175
176  /**
177   * {@inheritdoc}
178   *
179   * @see \Drupal\Core\Entity\EntityInterface
180   */
181  public function postCreate(EntityStorageInterface $storage): void {
182    if ($this->get('present')->isEmpty()) {
183      return;
184    }
185    /** @var \Drupal\display_builder\Plugin\Field\FieldType\HistoryStep $present */
186    $present = $this->get('present')->first();
187    $this->sourceTree = new SourceTree($present->getData() ?? []);
188    $indexed = $this->sourceTree->getTree();
189    $hash = self::getUniqId($indexed);
190
191    $this->set('present', [
192      'data' => $indexed,
193      'hash' => $hash,
194      'log' => $present->getLog(),
195      'time' => $present->getTime(),
196      'user' => $present->getUser(),
197    ]);
198  }
199
200  /**
201   * {@inheritdoc}
202   */
203  public function getProfile(): ?ProfileInterface {
204    $profile_id = $this->get('profileId')->getString();
205    /** @var \Drupal\display_builder\Entity\ProfileInterface $profile */
206    $profile = $this->entityTypeManager()->getStorage('display_builder_profile')->load($profile_id);
207
208    return $profile;
209  }
210
211  /**
212   * {@inheritdoc}
213   */
214  public function setProfile(string $profile_id): void {
215    $this->set('profileId', $profile_id);
216  }
217
218  /**
219   * {@inheritdoc}
220   */
221  public function moveToRoot(string $node_id, int $position): bool {
222    $tree = $this->getSourceTree();
223    $data = $tree->getNodeData($node_id);
224
225    if (!$data) {
226      return FALSE;
227    }
228
229    if (!$tree->moveToRoot($node_id, $position)) {
230      return FALSE;
231    }
232
233    $log = new TranslatableMarkup('@label moved to root', ['@label' => $this->nodeLabel($data)]);
234    $this->setNewPresent($tree->getTree(), $log, TRUE, FALSE);
235
236    return TRUE;
237  }
238
239  /**
240   * {@inheritdoc}
241   */
242  public function moveToSlot(string $node_id, string $parent_id, string $slot_id, int $position): bool {
243    $tree = $this->getSourceTree();
244    $data = $tree->getNodeData($node_id);
245
246    if (!$data) {
247      return FALSE;
248    }
249
250    if (!$tree->moveToSlot($node_id, $parent_id, $slot_id, $position)) {
251      return FALSE;
252    }
253
254    $parentData = $tree->getNodeData($parent_id);
255    $log = new TranslatableMarkup('@label moved to slot @slot_id in @parent_label', [
256      '@label' => $this->nodeLabel($data),
257      '@slot_id' => $slot_id,
258      '@parent_label' => $parentData ? $this->nodeLabel($parentData) : $parent_id,
259    ]);
260
261    $this->setNewPresent($tree->getTree(), $log, TRUE, FALSE);
262
263    return TRUE;
264  }
265
266  /**
267   * {@inheritdoc}
268   */
269  public function attachToRoot(int $position, string $source_id, array $data, array $third_party_settings = []): string {
270    $tree = $this->getSourceTree();
271    $node_id = $tree->attachToRoot($position, $source_id, $data);
272
273    if ($third_party_settings) {
274      foreach ($third_party_settings as $island_id => $settings) {
275        $tree->setThirdPartySettings($node_id, $island_id, $settings);
276      }
277    }
278
279    $nodeData = $tree->getNodeData($node_id);
280    $log = new TranslatableMarkup('@label attached to root', ['@label' => $this->nodeLabel($nodeData ?? [])]);
281    $this->setNewPresent($tree->getTree(), $log, FALSE, FALSE);
282
283    return $node_id;
284  }
285
286  /**
287   * {@inheritdoc}
288   */
289  public function attachToSlot(string $parent_id, string $slot_id, int $position, string $source_id, array $data, array $third_party_settings = []): string {
290    $tree = $this->getSourceTree();
291    $node_id = $tree->attachToSlot($parent_id, $slot_id, $position, $source_id, $data);
292
293    if (!$node_id) {
294      throw new InvalidNodeException('Parent or slot not found');
295    }
296
297    if ($third_party_settings) {
298      foreach ($third_party_settings as $island_id => $settings) {
299        $tree->setThirdPartySettings($node_id, $island_id, $settings);
300      }
301    }
302
303    $nodeData = $tree->getNodeData($node_id);
304    $parentData = $tree->getNodeData($parent_id);
305    $log = new TranslatableMarkup('@label attached to slot @slot_id in @parent_label', [
306      '@label' => $this->nodeLabel($nodeData ?? []),
307      '@slot_id' => $slot_id,
308      '@parent_label' => $parentData ? $this->nodeLabel($parentData) : $parent_id,
309    ]);
310    $this->setNewPresent($tree->getTree(), $log, TRUE, FALSE);
311
312    return $node_id;
313  }
314
315  /**
316   * {@inheritdoc}
317   */
318  public function getNode(string $node_id): array {
319    return $this->getSourceTree()->getNode($node_id) ?? [];
320  }
321
322  /**
323   * {@inheritdoc}
324   */
325  public function getParentId(string $node_id): ?string {
326    return $this->getPathIndex()[$node_id]['parent'] ?? NULL;
327  }
328
329  /**
330   * {@inheritdoc}
331   */
332  public function setSource(string $node_id, string $source_id, array $data): void {
333    $tree = $this->getSourceTree();
334
335    if (!$tree->setSource($node_id, $source_id, $data)) {
336      throw new InvalidNodeException('Internal node ID mismatch');
337    }
338
339    $nodeData = $tree->getNodeData($node_id);
340    $log = new TranslatableMarkup('@label updated config', ['@label' => $this->nodeLabel($nodeData ?? [])]);
341    $this->setNewPresent($tree->getTree(), $log, TRUE, FALSE);
342  }
343
344  /**
345   * {@inheritdoc}
346   */
347  public function setThirdPartySettings(string $node_id, string $island_id, array $data): void {
348    $tree = $this->getSourceTree();
349    $nodeData = $tree->getNodeData($node_id);
350
351    if (!$tree->setThirdPartySettings($node_id, $island_id, $data)) {
352      return;
353    }
354
355    $log = new TranslatableMarkup('@label settings updated by @island_id', [
356      '@label' => $this->nodeLabel($nodeData),
357      '@island_id' => $island_id,
358    ]);
359    $this->setNewPresent($tree->getTree(), $log, TRUE, FALSE);
360  }
361
362  /**
363   * {@inheritdoc}
364   */
365  public function remove(string $node_id): void {
366    $tree = $this->getSourceTree();
367    $data = $tree->getNodeData($node_id);
368
369    if (!$data) {
370      return;
371    }
372    $parent_id = $tree->getParentId($node_id);
373
374    $tree->remove($node_id);
375
376    $parentData = $parent_id === NULL ? NULL : $tree->getNodeData($parent_id);
377    $log = new TranslatableMarkup('@label removed from @parent_label', [
378      '@label' => $this->nodeLabel($data),
379      '@parent_label' => $parentData ? $this->nodeLabel($parentData) : 'root',
380    ]);
381    $this->setNewPresent($tree->getTree(), $log, FALSE, FALSE);
382  }
383
384  /**
385   * {@inheritdoc}
386   */
387  public function getContexts(): array {
388    if ($this->get('contexts')->isEmpty()) {
389      return [];
390    }
391
392    return $this->refreshContexts($this->get('contexts')->first()->getValue());
393  }
394
395  /**
396   * {@inheritdoc}
397   */
398  public function setSave(array $save_data): void {
399    $tree = new SourceTree($save_data);
400    $indexed = $tree->getTree();
401    $hash = self::getUniqId($indexed);
402    $this->set('save', ['data' => $indexed, 'hash' => $hash, 'log' => NULL, 'time' => \time(), 'user' => NULL]);
403  }
404
405  /**
406   * {@inheritdoc}
407   *
408   * @see \Drupal\display_builder\HistoryInterface
409   */
410  public function getCurrentState(): array {
411    return $this->getCurrent()?->getData() ?? [];
412  }
413
414  /**
415   * {@inheritdoc}
416   */
417  public function restore(): void {
418    /** @var \Drupal\display_builder\Plugin\Field\FieldType\HistoryStep|null $first */
419    $first = $this->get('save')->first();
420    $this->setNewPresent($first->getData(), new TranslatableMarkup('Back to saved data.'));
421  }
422
423  /**
424   * {@inheritdoc}
425   *
426   * @see \Drupal\display_builder\HistoryInterface
427   */
428  public function undo(): void {
429    $past = $this->get('past');
430
431    if ($past->isEmpty()) {
432      return;
433    }
434
435    $present_values = $this->get('present')->getValue();
436    \assert(\array_is_list($present_values));
437
438    // Remove the last element from the past.
439    $past_values = $past->getValue();
440    \assert(\array_is_list($past_values));
441    $last = \array_pop($past_values);
442    \assert(!\array_is_list($last));
443    $this->set('past', $past_values);
444
445    // Set the present to the element we removed in the previous step.
446    $this->set('present', $last);
447    // Insert the old present state at the beginning of the future.
448    $this->set('future', \array_merge($present_values, $this->get('future')->getValue()));
449    $this->sourceTree = NULL;
450  }
451
452  /**
453   * {@inheritdoc}
454   *
455   * @see \Drupal\display_builder\HistoryInterface
456   */
457  public function redo(): void {
458    $future = $this->get('future');
459
460    if ($future->isEmpty()) {
461      return;
462    }
463
464    // Remove the first element from the future.
465    $first = $future->first()->getValue();
466    \assert(!\array_is_list($first));
467    $future->removeItem(0);
468    // Insert the old present state at the end of the past.
469    $this->get('past')->appendItem($this->get('present')->first());
470    // Set the present to the element we removed in the previous step.
471    $this->set('present', $first);
472    $this->set('future', $future->getValue());
473    $this->sourceTree = NULL;
474  }
475
476  /**
477   * {@inheritdoc}
478   */
479  public function clear(): void {
480    $this->set('past', NULL);
481    $this->set('future', NULL);
482  }
483
484  /**
485   * {@inheritdoc}
486   *
487   * @see \Drupal\display_builder\HistoryInterface
488   */
489  public function getPast(): array {
490    $past = [];
491
492    foreach ($this->get('past') as $step) {
493      $past[] = $step;
494    }
495
496    return $past;
497  }
498
499  /**
500   * {@inheritdoc}
501   *
502   * @see \Drupal\display_builder\HistoryInterface
503   */
504  public function getFuture(): array {
505    $future = [];
506
507    foreach ($this->get('future') as $step) {
508      $future[] = $step;
509    }
510
511    return $future;
512  }
513
514  /**
515   * {@inheritdoc}
516   */
517  public function getUsers(): array {
518    $users = [];
519    $steps = \array_merge($this->get('past')->getValue(), $this->get('present')->getValue(), $this->get('future')->getValue());
520
521    foreach ($steps as $step) {
522      if ($step === NULL) {
523        continue;
524      }
525      $user_id = $step['user'];
526
527      if ($user_id !== NULL && (!isset($users[$user_id]) || $step['time'] > $users[$user_id])) {
528        $users[$user_id] = $step['time'];
529      }
530    }
531
532    return $users;
533  }
534
535  /**
536   * {@inheritdoc}
537   */
538  public function isPublishable(): bool {
539    $contexts = $this->getContexts();
540
541    if (!\array_key_exists('context_requirements', $contexts)
542      || !($contexts['context_requirements'] instanceof RequirementsContext)) {
543      return FALSE;
544    }
545
546    return TRUE;
547  }
548
549  /**
550   * {@inheritdoc}
551   */
552  public function hasSaveContextsRequirement(string $key, array $contexts = []): bool {
553    $contexts = empty($contexts) ? $this->getContexts() : $contexts;
554
555    if (!\array_key_exists('context_requirements', $contexts)
556      || !($contexts['context_requirements'] instanceof RequirementsContext)
557      || !$contexts['context_requirements']->hasValue($key)) {
558      return FALSE;
559    }
560
561    return TRUE;
562  }
563
564  /**
565   * {@inheritdoc}
566   */
567  public function isPublished(): bool {
568    return !$this->get('save')->isEmpty();
569  }
570
571  /**
572   * {@inheritdoc}
573   */
574  public function isPublishedPresent(): bool {
575    $present = $this->get('present');
576    $save = $this->get('save');
577
578    // If either present or save is null, they can't be equal unless both are
579    // null.
580    if ($present->isEmpty() || $save->isEmpty()) {
581      return $present->isEmpty() && $save->isEmpty();
582    }
583
584    /** @var \Drupal\display_builder\Plugin\Field\FieldType\HistoryStep|null $present */
585    $present = $present->first();
586    /** @var \Drupal\display_builder\Plugin\Field\FieldType\HistoryStep|null $save */
587    $save = $save->first();
588
589    return $present->getHash() === $save->getHash();
590  }
591
592  /**
593   * {@inheritdoc}
594   */
595  public function getPathIndex(): array {
596    return $this->getSourceTree()->getPathIndex();
597  }
598
599  /**
600   * {@inheritdoc}
601   *
602   * @see \Drupal\display_builder\HistoryInterface
603   */
604  public function setNewPresent(array $data, string|\Stringable $log_message = '', bool $check_hash = TRUE, bool $index = TRUE): void {
605    if ($index) {
606      // Raw data needs normalizing; build tree and keep it as the new cache.
607      $this->sourceTree = new SourceTree($data);
608      $data = $this->sourceTree->getTree();
609    }
610    // When index=FALSE the data was produced by the cached tree's getTree(),
611    // so $this->sourceTree already reflects the new state â€” no invalidation.
612    $hash = self::getUniqId($data);
613
614    if (!$this->get('present')->isEmpty()) {
615      /** @var \Drupal\display_builder\Plugin\Field\FieldType\HistoryStep|null $present */
616      $present = $this->get('present')->first();
617
618      // Check if this present is the same to avoid duplicates, for example move
619      // to the same place.
620      if ($check_hash && $hash === $present->getHash()) {
621        return;
622      }
623
624      // 1. Insert the present at the end of the past.
625      // If it's the very first action, we want a NULL in the past to be able to
626      // undo to initial empty state.
627      $this->get('past')->appendItem($present->getValue());
628    }
629
630    // Keep only the last x history.
631    if ($this->get('past')->count() > self::MAX_HISTORY) {
632      $this->get('past')->removeItem(0);
633    }
634
635    // 2. Set the present to the new state.
636    $this->set('present', [
637      'data' => $data,
638      'hash' => $hash,
639      'log' => $log_message,
640      'time' => \time(),
641      'user' => (int) $this->currentUser()->id(),
642    ]);
643
644    // 3. Clear the future.
645    $this->set('future', []);
646  }
647
648  /**
649   * {@inheritdoc}
650   *
651   * @see \Drupal\display_builder\HistoryInterface
652   */
653  public function getCurrent(): ?HistoryStep {
654    /** @var \Drupal\display_builder\Plugin\Field\FieldType\HistoryStep|null $step */
655    $step = $this->get('present')->first();
656
657    return $step;
658  }
659
660  /**
661   * {@inheritdoc}
662   */
663  public static function getUniqId(array $data): int {
664    return \crc32((string) \serialize($data));
665  }
666
667  /**
668   * Get or create the cached source tree for the current present state.
669   *
670   * The cache is populated lazily on first access and remains valid until
671   * undo() or redo() changes the present pointer to a different history step.
672   * Mutations (index=FALSE path) keep it alive since the tree is the source
673   * of the new present data. The index=TRUE path in setNewPresent() replaces
674   * it with a freshly normalized tree.
675   *
676   * @return \Drupal\display_builder\SourceTree
677   *   The source tree for the current state.
678   */
679  private function getSourceTree(): SourceTree {
680    if ($this->sourceTree === NULL) {
681      $this->sourceTree = new SourceTree($this->getCurrentState());
682    }
683
684    return $this->sourceTree;
685  }
686
687  /**
688   * Sample entity generator.
689   */
690  private function sampleEntityGenerator(): SampleEntityGeneratorInterface {
691    return $this->sampleEntityGenerator ??= \Drupal::service('ui_patterns.sample_entity_generator');
692  }
693
694  /**
695   * Slot source proxy lazy loader.
696   */
697  private function slotSourceProxy(): SlotSourceProxy {
698    return $this->slotSourceProxy ??= \Drupal::service('display_builder.slot_sources_proxy');
699  }
700
701  /**
702   * Returns the human-readable label for a node, falling back to source_id.
703   */
704  private function nodeLabel(array $data): string {
705    $label = $this->slotSourceProxy()->getLabelWithSummary($data, [], TRUE)['label'];
706
707    return $label !== '' ? $label : ($data['source_id'] ?? '');
708  }
709
710  /**
711   * Slot source proxy.
712   */
713  private function currentUser(): AccountInterface {
714    return $this->currentUser ??= \Drupal::service('current_user');
715  }
716
717  /**
718   * Refresh contexts after loaded from storage.
719   *
720   * @param \Drupal\Core\Plugin\Context\ContextInterface[] $contexts
721   *   The contexts.
722   *
723   * @throws \Drupal\Component\Plugin\Exception\ContextException
724   * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
725   * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
726   *
727   * @return array
728   *   The refreshed contexts or NULL if no context.
729   */
730  private function refreshContexts(array $contexts): array {
731    foreach ($contexts as &$context) {
732      if ($context instanceof EntityContext) {
733        // @todo We should use cache entries here
734        // with the corresponding cache contexts in it.
735        // This may avoid some unnecessary entity loads or generation.
736        $entity = $context->getContextValue();
737
738        // Check if sample entity.
739        if ($entity->id()) {
740          $entity = $this->entityTypeManager()->getStorage($entity->getEntityTypeId())->load($entity->id());
741        }
742        else {
743          $entity = $this->sampleEntityGenerator()->get($entity->getEntityTypeId(), $entity->bundle());
744        }
745
746        // Edge case when the parent entity is deleted but not the builder
747        // instance.
748        if (!$entity) {
749          return $contexts;
750        }
751        $context = (\get_class($context))::fromEntity($entity);
752      }
753    }
754
755    return $contexts;
756  }
757
758}

Paths

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

Instance->__construct
102  public function __construct(array $values, mixed $entity_type, mixed $bundle = FALSE, mixed $translations = []) {
103    parent::__construct($values, $entity_type, $bundle, $translations);
104    $fields = $this->fieldDefinitions;
105
106    foreach ($values as $key => $value) {
 
106    foreach ($values as $key => $value) {
 
106    foreach ($values as $key => $value) {
107      if (isset($fields[$key])) {
 
106    foreach ($values as $key => $value) {
107      if (isset($fields[$key])) {
108        $this->set($key, $value);
 
106    foreach ($values as $key => $value) {
 
106    foreach ($values as $key => $value) {
 
106    foreach ($values as $key => $value) {
107      if (isset($fields[$key])) {
108        $this->set($key, $value);
109      }
110    }
111  }
102  public function __construct(array $values, mixed $entity_type, mixed $bundle = FALSE, mixed $translations = []) {
103    parent::__construct($values, $entity_type, $bundle, $translations);
104    $fields = $this->fieldDefinitions;
105
106    foreach ($values as $key => $value) {
 
106    foreach ($values as $key => $value) {
 
106    foreach ($values as $key => $value) {
107      if (isset($fields[$key])) {
 
106    foreach ($values as $key => $value) {
 
106    foreach ($values as $key => $value) {
 
106    foreach ($values as $key => $value) {
107      if (isset($fields[$key])) {
108        $this->set($key, $value);
109      }
110    }
111  }
102  public function __construct(array $values, mixed $entity_type, mixed $bundle = FALSE, mixed $translations = []) {
103    parent::__construct($values, $entity_type, $bundle, $translations);
104    $fields = $this->fieldDefinitions;
105
106    foreach ($values as $key => $value) {
 
106    foreach ($values as $key => $value) {
 
106    foreach ($values as $key => $value) {
107      if (isset($fields[$key])) {
108        $this->set($key, $value);
109      }
110    }
111  }
102  public function __construct(array $values, mixed $entity_type, mixed $bundle = FALSE, mixed $translations = []) {
103    parent::__construct($values, $entity_type, $bundle, $translations);
104    $fields = $this->fieldDefinitions;
105
106    foreach ($values as $key => $value) {
 
106    foreach ($values as $key => $value) {
107      if (isset($fields[$key])) {
108        $this->set($key, $value);
109      }
110    }
111  }
Instance->attachToRoot
269  public function attachToRoot(int $position, string $source_id, array $data, array $third_party_settings = []): string {
270    $tree = $this->getSourceTree();
271    $node_id = $tree->attachToRoot($position, $source_id, $data);
272
273    if ($third_party_settings) {
 
274      foreach ($third_party_settings as $island_id => $settings) {
 
274      foreach ($third_party_settings as $island_id => $settings) {
 
274      foreach ($third_party_settings as $island_id => $settings) {
 
274      foreach ($third_party_settings as $island_id => $settings) {
 
274      foreach ($third_party_settings as $island_id => $settings) {
275        $tree->setThirdPartySettings($node_id, $island_id, $settings);
276      }
277    }
278
279    $nodeData = $tree->getNodeData($node_id);
 
279    $nodeData = $tree->getNodeData($node_id);
280    $log = new TranslatableMarkup('@label attached to root', ['@label' => $this->nodeLabel($nodeData ?? [])]);
281    $this->setNewPresent($tree->getTree(), $log, FALSE, FALSE);
282
283    return $node_id;
284  }
269  public function attachToRoot(int $position, string $source_id, array $data, array $third_party_settings = []): string {
270    $tree = $this->getSourceTree();
271    $node_id = $tree->attachToRoot($position, $source_id, $data);
272
273    if ($third_party_settings) {
 
274      foreach ($third_party_settings as $island_id => $settings) {
 
274      foreach ($third_party_settings as $island_id => $settings) {
 
274      foreach ($third_party_settings as $island_id => $settings) {
275        $tree->setThirdPartySettings($node_id, $island_id, $settings);
276      }
277    }
278
279    $nodeData = $tree->getNodeData($node_id);
 
279    $nodeData = $tree->getNodeData($node_id);
280    $log = new TranslatableMarkup('@label attached to root', ['@label' => $this->nodeLabel($nodeData ?? [])]);
281    $this->setNewPresent($tree->getTree(), $log, FALSE, FALSE);
282
283    return $node_id;
284  }
269  public function attachToRoot(int $position, string $source_id, array $data, array $third_party_settings = []): string {
270    $tree = $this->getSourceTree();
271    $node_id = $tree->attachToRoot($position, $source_id, $data);
272
273    if ($third_party_settings) {
 
274      foreach ($third_party_settings as $island_id => $settings) {
 
274      foreach ($third_party_settings as $island_id => $settings) {
275        $tree->setThirdPartySettings($node_id, $island_id, $settings);
276      }
277    }
278
279    $nodeData = $tree->getNodeData($node_id);
 
279    $nodeData = $tree->getNodeData($node_id);
280    $log = new TranslatableMarkup('@label attached to root', ['@label' => $this->nodeLabel($nodeData ?? [])]);
281    $this->setNewPresent($tree->getTree(), $log, FALSE, FALSE);
282
283    return $node_id;
284  }
269  public function attachToRoot(int $position, string $source_id, array $data, array $third_party_settings = []): string {
270    $tree = $this->getSourceTree();
271    $node_id = $tree->attachToRoot($position, $source_id, $data);
272
273    if ($third_party_settings) {
 
279    $nodeData = $tree->getNodeData($node_id);
280    $log = new TranslatableMarkup('@label attached to root', ['@label' => $this->nodeLabel($nodeData ?? [])]);
281    $this->setNewPresent($tree->getTree(), $log, FALSE, FALSE);
282
283    return $node_id;
284  }
Instance->attachToSlot
289  public function attachToSlot(string $parent_id, string $slot_id, int $position, string $source_id, array $data, array $third_party_settings = []): string {
290    $tree = $this->getSourceTree();
291    $node_id = $tree->attachToSlot($parent_id, $slot_id, $position, $source_id, $data);
292
293    if (!$node_id) {
 
294      throw new InvalidNodeException('Parent or slot not found');
289  public function attachToSlot(string $parent_id, string $slot_id, int $position, string $source_id, array $data, array $third_party_settings = []): string {
290    $tree = $this->getSourceTree();
291    $node_id = $tree->attachToSlot($parent_id, $slot_id, $position, $source_id, $data);
292
293    if (!$node_id) {
 
297    if ($third_party_settings) {
 
298      foreach ($third_party_settings as $island_id => $settings) {
 
298      foreach ($third_party_settings as $island_id => $settings) {
 
298      foreach ($third_party_settings as $island_id => $settings) {
 
298      foreach ($third_party_settings as $island_id => $settings) {
 
298      foreach ($third_party_settings as $island_id => $settings) {
299        $tree->setThirdPartySettings($node_id, $island_id, $settings);
300      }
301    }
302
303    $nodeData = $tree->getNodeData($node_id);
 
303    $nodeData = $tree->getNodeData($node_id);
304    $parentData = $tree->getNodeData($parent_id);
305    $log = new TranslatableMarkup('@label attached to slot @slot_id in @parent_label', [
306      '@label' => $this->nodeLabel($nodeData ?? []),
307      '@slot_id' => $slot_id,
308      '@parent_label' => $parentData ? $this->nodeLabel($parentData) : $parent_id,
 
308      '@parent_label' => $parentData ? $this->nodeLabel($parentData) : $parent_id,
 
308      '@parent_label' => $parentData ? $this->nodeLabel($parentData) : $parent_id,
309    ]);
310    $this->setNewPresent($tree->getTree(), $log, TRUE, FALSE);
311
312    return $node_id;
313  }
289  public function attachToSlot(string $parent_id, string $slot_id, int $position, string $source_id, array $data, array $third_party_settings = []): string {
290    $tree = $this->getSourceTree();
291    $node_id = $tree->attachToSlot($parent_id, $slot_id, $position, $source_id, $data);
292
293    if (!$node_id) {
 
297    if ($third_party_settings) {
 
298      foreach ($third_party_settings as $island_id => $settings) {
 
298      foreach ($third_party_settings as $island_id => $settings) {
 
298      foreach ($third_party_settings as $island_id => $settings) {
 
298      foreach ($third_party_settings as $island_id => $settings) {
 
298      foreach ($third_party_settings as $island_id => $settings) {
299        $tree->setThirdPartySettings($node_id, $island_id, $settings);
300      }
301    }
302
303    $nodeData = $tree->getNodeData($node_id);
 
303    $nodeData = $tree->getNodeData($node_id);
304    $parentData = $tree->getNodeData($parent_id);
305    $log = new TranslatableMarkup('@label attached to slot @slot_id in @parent_label', [
306      '@label' => $this->nodeLabel($nodeData ?? []),
307      '@slot_id' => $slot_id,
308      '@parent_label' => $parentData ? $this->nodeLabel($parentData) : $parent_id,
 
308      '@parent_label' => $parentData ? $this->nodeLabel($parentData) : $parent_id,
 
308      '@parent_label' => $parentData ? $this->nodeLabel($parentData) : $parent_id,
309    ]);
310    $this->setNewPresent($tree->getTree(), $log, TRUE, FALSE);
311
312    return $node_id;
313  }
289  public function attachToSlot(string $parent_id, string $slot_id, int $position, string $source_id, array $data, array $third_party_settings = []): string {
290    $tree = $this->getSourceTree();
291    $node_id = $tree->attachToSlot($parent_id, $slot_id, $position, $source_id, $data);
292
293    if (!$node_id) {
 
297    if ($third_party_settings) {
 
298      foreach ($third_party_settings as $island_id => $settings) {
 
298      foreach ($third_party_settings as $island_id => $settings) {
 
298      foreach ($third_party_settings as $island_id => $settings) {
299        $tree->setThirdPartySettings($node_id, $island_id, $settings);
300      }
301    }
302
303    $nodeData = $tree->getNodeData($node_id);
 
303    $nodeData = $tree->getNodeData($node_id);
304    $parentData = $tree->getNodeData($parent_id);
305    $log = new TranslatableMarkup('@label attached to slot @slot_id in @parent_label', [
306      '@label' => $this->nodeLabel($nodeData ?? []),
307      '@slot_id' => $slot_id,
308      '@parent_label' => $parentData ? $this->nodeLabel($parentData) : $parent_id,
 
308      '@parent_label' => $parentData ? $this->nodeLabel($parentData) : $parent_id,
 
308      '@parent_label' => $parentData ? $this->nodeLabel($parentData) : $parent_id,
309    ]);
310    $this->setNewPresent($tree->getTree(), $log, TRUE, FALSE);
311
312    return $node_id;
313  }
289  public function attachToSlot(string $parent_id, string $slot_id, int $position, string $source_id, array $data, array $third_party_settings = []): string {
290    $tree = $this->getSourceTree();
291    $node_id = $tree->attachToSlot($parent_id, $slot_id, $position, $source_id, $data);
292
293    if (!$node_id) {
 
297    if ($third_party_settings) {
 
298      foreach ($third_party_settings as $island_id => $settings) {
 
298      foreach ($third_party_settings as $island_id => $settings) {
 
298      foreach ($third_party_settings as $island_id => $settings) {
299        $tree->setThirdPartySettings($node_id, $island_id, $settings);
300      }
301    }
302
303    $nodeData = $tree->getNodeData($node_id);
 
303    $nodeData = $tree->getNodeData($node_id);
304    $parentData = $tree->getNodeData($parent_id);
305    $log = new TranslatableMarkup('@label attached to slot @slot_id in @parent_label', [
306      '@label' => $this->nodeLabel($nodeData ?? []),
307      '@slot_id' => $slot_id,
308      '@parent_label' => $parentData ? $this->nodeLabel($parentData) : $parent_id,
 
308      '@parent_label' => $parentData ? $this->nodeLabel($parentData) : $parent_id,
 
308      '@parent_label' => $parentData ? $this->nodeLabel($parentData) : $parent_id,
309    ]);
310    $this->setNewPresent($tree->getTree(), $log, TRUE, FALSE);
311
312    return $node_id;
313  }
289  public function attachToSlot(string $parent_id, string $slot_id, int $position, string $source_id, array $data, array $third_party_settings = []): string {
290    $tree = $this->getSourceTree();
291    $node_id = $tree->attachToSlot($parent_id, $slot_id, $position, $source_id, $data);
292
293    if (!$node_id) {
 
297    if ($third_party_settings) {
 
298      foreach ($third_party_settings as $island_id => $settings) {
 
298      foreach ($third_party_settings as $island_id => $settings) {
299        $tree->setThirdPartySettings($node_id, $island_id, $settings);
300      }
301    }
302
303    $nodeData = $tree->getNodeData($node_id);
 
303    $nodeData = $tree->getNodeData($node_id);
304    $parentData = $tree->getNodeData($parent_id);
305    $log = new TranslatableMarkup('@label attached to slot @slot_id in @parent_label', [
306      '@label' => $this->nodeLabel($nodeData ?? []),
307      '@slot_id' => $slot_id,
308      '@parent_label' => $parentData ? $this->nodeLabel($parentData) : $parent_id,
 
308      '@parent_label' => $parentData ? $this->nodeLabel($parentData) : $parent_id,
 
308      '@parent_label' => $parentData ? $this->nodeLabel($parentData) : $parent_id,
309    ]);
310    $this->setNewPresent($tree->getTree(), $log, TRUE, FALSE);
311
312    return $node_id;
313  }
289  public function attachToSlot(string $parent_id, string $slot_id, int $position, string $source_id, array $data, array $third_party_settings = []): string {
290    $tree = $this->getSourceTree();
291    $node_id = $tree->attachToSlot($parent_id, $slot_id, $position, $source_id, $data);
292
293    if (!$node_id) {
 
297    if ($third_party_settings) {
 
298      foreach ($third_party_settings as $island_id => $settings) {
 
298      foreach ($third_party_settings as $island_id => $settings) {
299        $tree->setThirdPartySettings($node_id, $island_id, $settings);
300      }
301    }
302
303    $nodeData = $tree->getNodeData($node_id);
 
303    $nodeData = $tree->getNodeData($node_id);
304    $parentData = $tree->getNodeData($parent_id);
305    $log = new TranslatableMarkup('@label attached to slot @slot_id in @parent_label', [
306      '@label' => $this->nodeLabel($nodeData ?? []),
307      '@slot_id' => $slot_id,
308      '@parent_label' => $parentData ? $this->nodeLabel($parentData) : $parent_id,
 
308      '@parent_label' => $parentData ? $this->nodeLabel($parentData) : $parent_id,
 
308      '@parent_label' => $parentData ? $this->nodeLabel($parentData) : $parent_id,
309    ]);
310    $this->setNewPresent($tree->getTree(), $log, TRUE, FALSE);
311
312    return $node_id;
313  }
289  public function attachToSlot(string $parent_id, string $slot_id, int $position, string $source_id, array $data, array $third_party_settings = []): string {
290    $tree = $this->getSourceTree();
291    $node_id = $tree->attachToSlot($parent_id, $slot_id, $position, $source_id, $data);
292
293    if (!$node_id) {
 
297    if ($third_party_settings) {
 
303    $nodeData = $tree->getNodeData($node_id);
304    $parentData = $tree->getNodeData($parent_id);
305    $log = new TranslatableMarkup('@label attached to slot @slot_id in @parent_label', [
306      '@label' => $this->nodeLabel($nodeData ?? []),
307      '@slot_id' => $slot_id,
308      '@parent_label' => $parentData ? $this->nodeLabel($parentData) : $parent_id,
 
308      '@parent_label' => $parentData ? $this->nodeLabel($parentData) : $parent_id,
 
308      '@parent_label' => $parentData ? $this->nodeLabel($parentData) : $parent_id,
309    ]);
310    $this->setNewPresent($tree->getTree(), $log, TRUE, FALSE);
311
312    return $node_id;
313  }
289  public function attachToSlot(string $parent_id, string $slot_id, int $position, string $source_id, array $data, array $third_party_settings = []): string {
290    $tree = $this->getSourceTree();
291    $node_id = $tree->attachToSlot($parent_id, $slot_id, $position, $source_id, $data);
292
293    if (!$node_id) {
 
297    if ($third_party_settings) {
 
303    $nodeData = $tree->getNodeData($node_id);
304    $parentData = $tree->getNodeData($parent_id);
305    $log = new TranslatableMarkup('@label attached to slot @slot_id in @parent_label', [
306      '@label' => $this->nodeLabel($nodeData ?? []),
307      '@slot_id' => $slot_id,
308      '@parent_label' => $parentData ? $this->nodeLabel($parentData) : $parent_id,
 
308      '@parent_label' => $parentData ? $this->nodeLabel($parentData) : $parent_id,
 
308      '@parent_label' => $parentData ? $this->nodeLabel($parentData) : $parent_id,
309    ]);
310    $this->setNewPresent($tree->getTree(), $log, TRUE, FALSE);
311
312    return $node_id;
313  }
Instance->baseFieldDefinitions
116  public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
117    $fields = parent::baseFieldDefinitions($entity_type);
118    // Override from ContentEntityBase.
119    $fields['id'] = BaseFieldDefinition::create('string');
120    // @todo replace by entity_reference.
121    $fields['profileId'] = BaseFieldDefinition::create('string');
122    $fields['contexts'] = BaseFieldDefinition::create('map');
123    // @todo replace by Revisions API.
124    $fields['present'] = BaseFieldDefinition::create('step');
125    $fields['past'] = BaseFieldDefinition::create('step')->setCardinality(-1);
126    $fields['future'] = BaseFieldDefinition::create('step')->setCardinality(-1);
127    $fields['save'] = BaseFieldDefinition::create('step');
128
129    return $fields;
130  }
Instance->clear
480    $this->set('past', NULL);
481    $this->set('future', NULL);
482  }
Instance->currentUser
714    return $this->currentUser ??= \Drupal::service('current_user');
715  }
Instance->getContexts
388    if ($this->get('contexts')->isEmpty()) {
 
389      return [];
388    if ($this->get('contexts')->isEmpty()) {
 
392    return $this->refreshContexts($this->get('contexts')->first()->getValue());
393  }
Instance->getCurrent
655    $step = $this->get('present')->first();
656
657    return $step;
658  }
Instance->getCurrentState
411    return $this->getCurrent()?->getData() ?? [];
412  }
Instance->getFuture
505    $future = [];
506
507    foreach ($this->get('future') as $step) {
 
507    foreach ($this->get('future') as $step) {
 
507    foreach ($this->get('future') as $step) {
508      $future[] = $step;
 
507    foreach ($this->get('future') as $step) {
 
507    foreach ($this->get('future') as $step) {
508      $future[] = $step;
509    }
510
511    return $future;
512  }
505    $future = [];
506
507    foreach ($this->get('future') as $step) {
 
507    foreach ($this->get('future') as $step) {
 
507    foreach ($this->get('future') as $step) {
508      $future[] = $step;
509    }
510
511    return $future;
512  }
505    $future = [];
506
507    foreach ($this->get('future') as $step) {
 
507    foreach ($this->get('future') as $step) {
508      $future[] = $step;
509    }
510
511    return $future;
512  }
Instance->getNode
318  public function getNode(string $node_id): array {
319    return $this->getSourceTree()->getNode($node_id) ?? [];
320  }
Instance->getParentId
325  public function getParentId(string $node_id): ?string {
326    return $this->getPathIndex()[$node_id]['parent'] ?? NULL;
327  }
Instance->getPast
490    $past = [];
491
492    foreach ($this->get('past') as $step) {
 
492    foreach ($this->get('past') as $step) {
 
492    foreach ($this->get('past') as $step) {
493      $past[] = $step;
 
492    foreach ($this->get('past') as $step) {
 
492    foreach ($this->get('past') as $step) {
493      $past[] = $step;
494    }
495
496    return $past;
497  }
490    $past = [];
491
492    foreach ($this->get('past') as $step) {
 
492    foreach ($this->get('past') as $step) {
 
492    foreach ($this->get('past') as $step) {
493      $past[] = $step;
494    }
495
496    return $past;
497  }
490    $past = [];
491
492    foreach ($this->get('past') as $step) {
 
492    foreach ($this->get('past') as $step) {
493      $past[] = $step;
494    }
495
496    return $past;
497  }
Instance->getPathIndex
596    return $this->getSourceTree()->getPathIndex();
597  }
Instance->getProfile
204    $profile_id = $this->get('profileId')->getString();
205    /** @var \Drupal\display_builder\Entity\ProfileInterface $profile */
206    $profile = $this->entityTypeManager()->getStorage('display_builder_profile')->load($profile_id);
207
208    return $profile;
209  }
Instance->getSourceTree
680    if ($this->sourceTree === NULL) {
 
681      $this->sourceTree = new SourceTree($this->getCurrentState());
682    }
683
684    return $this->sourceTree;
 
684    return $this->sourceTree;
685  }
680    if ($this->sourceTree === NULL) {
 
684    return $this->sourceTree;
685  }
Instance->getUniqId
663  public static function getUniqId(array $data): int {
664    return \crc32((string) \serialize($data));
665  }
Instance->getUsers
518    $users = [];
519    $steps = \array_merge($this->get('past')->getValue(), $this->get('present')->getValue(), $this->get('future')->getValue());
520
521    foreach ($steps as $step) {
 
521    foreach ($steps as $step) {
 
522      if ($step === NULL) {
 
523        continue;
 
521    foreach ($steps as $step) {
 
521    foreach ($steps as $step) {
522      if ($step === NULL) {
523        continue;
524      }
525      $user_id = $step['user'];
526
527      if ($user_id !== NULL && (!isset($users[$user_id]) || $step['time'] > $users[$user_id])) {
528        $users[$user_id] = $step['time'];
529      }
530    }
531
532    return $users;
533  }
518    $users = [];
519    $steps = \array_merge($this->get('past')->getValue(), $this->get('present')->getValue(), $this->get('future')->getValue());
520
521    foreach ($steps as $step) {
 
521    foreach ($steps as $step) {
 
522      if ($step === NULL) {
 
525      $user_id = $step['user'];
526
527      if ($user_id !== NULL && (!isset($users[$user_id]) || $step['time'] > $users[$user_id])) {
 
527      if ($user_id !== NULL && (!isset($users[$user_id]) || $step['time'] > $users[$user_id])) {
 
527      if ($user_id !== NULL && (!isset($users[$user_id]) || $step['time'] > $users[$user_id])) {
 
527      if ($user_id !== NULL && (!isset($users[$user_id]) || $step['time'] > $users[$user_id])) {
 
527      if ($user_id !== NULL && (!isset($users[$user_id]) || $step['time'] > $users[$user_id])) {
 
521    foreach ($steps as $step) {
522      if ($step === NULL) {
523        continue;
524      }
525      $user_id = $step['user'];
526
527      if ($user_id !== NULL && (!isset($users[$user_id]) || $step['time'] > $users[$user_id])) {
528        $users[$user_id] = $step['time'];
 
521    foreach ($steps as $step) {
 
521    foreach ($steps as $step) {
 
521    foreach ($steps as $step) {
522      if ($step === NULL) {
523        continue;
524      }
525      $user_id = $step['user'];
526
527      if ($user_id !== NULL && (!isset($users[$user_id]) || $step['time'] > $users[$user_id])) {
528        $users[$user_id] = $step['time'];
529      }
530    }
531
532    return $users;
533  }
518    $users = [];
519    $steps = \array_merge($this->get('past')->getValue(), $this->get('present')->getValue(), $this->get('future')->getValue());
520
521    foreach ($steps as $step) {
 
521    foreach ($steps as $step) {
 
522      if ($step === NULL) {
 
525      $user_id = $step['user'];
526
527      if ($user_id !== NULL && (!isset($users[$user_id]) || $step['time'] > $users[$user_id])) {
 
527      if ($user_id !== NULL && (!isset($users[$user_id]) || $step['time'] > $users[$user_id])) {
 
527      if ($user_id !== NULL && (!isset($users[$user_id]) || $step['time'] > $users[$user_id])) {
 
527      if ($user_id !== NULL && (!isset($users[$user_id]) || $step['time'] > $users[$user_id])) {
 
527      if ($user_id !== NULL && (!isset($users[$user_id]) || $step['time'] > $users[$user_id])) {
 
521    foreach ($steps as $step) {
 
521    foreach ($steps as $step) {
 
521    foreach ($steps as $step) {
522      if ($step === NULL) {
523        continue;
524      }
525      $user_id = $step['user'];
526
527      if ($user_id !== NULL && (!isset($users[$user_id]) || $step['time'] > $users[$user_id])) {
528        $users[$user_id] = $step['time'];
529      }
530    }
531
532    return $users;
533  }
518    $users = [];
519    $steps = \array_merge($this->get('past')->getValue(), $this->get('present')->getValue(), $this->get('future')->getValue());
520
521    foreach ($steps as $step) {
 
521    foreach ($steps as $step) {
 
522      if ($step === NULL) {
 
525      $user_id = $step['user'];
526
527      if ($user_id !== NULL && (!isset($users[$user_id]) || $step['time'] > $users[$user_id])) {
 
527      if ($user_id !== NULL && (!isset($users[$user_id]) || $step['time'] > $users[$user_id])) {
 
527      if ($user_id !== NULL && (!isset($users[$user_id]) || $step['time'] > $users[$user_id])) {
 
527      if ($user_id !== NULL && (!isset($users[$user_id]) || $step['time'] > $users[$user_id])) {
 
521    foreach ($steps as $step) {
522      if ($step === NULL) {
523        continue;
524      }
525      $user_id = $step['user'];
526
527      if ($user_id !== NULL && (!isset($users[$user_id]) || $step['time'] > $users[$user_id])) {
528        $users[$user_id] = $step['time'];
 
521    foreach ($steps as $step) {
 
521    foreach ($steps as $step) {
 
521    foreach ($steps as $step) {
522      if ($step === NULL) {
523        continue;
524      }
525      $user_id = $step['user'];
526
527      if ($user_id !== NULL && (!isset($users[$user_id]) || $step['time'] > $users[$user_id])) {
528        $users[$user_id] = $step['time'];
529      }
530    }
531
532    return $users;
533  }
518    $users = [];
519    $steps = \array_merge($this->get('past')->getValue(), $this->get('present')->getValue(), $this->get('future')->getValue());
520
521    foreach ($steps as $step) {
 
521    foreach ($steps as $step) {
 
522      if ($step === NULL) {
 
525      $user_id = $step['user'];
526
527      if ($user_id !== NULL && (!isset($users[$user_id]) || $step['time'] > $users[$user_id])) {
 
527      if ($user_id !== NULL && (!isset($users[$user_id]) || $step['time'] > $users[$user_id])) {
 
527      if ($user_id !== NULL && (!isset($users[$user_id]) || $step['time'] > $users[$user_id])) {
 
527      if ($user_id !== NULL && (!isset($users[$user_id]) || $step['time'] > $users[$user_id])) {
 
521    foreach ($steps as $step) {
 
521    foreach ($steps as $step) {
 
521    foreach ($steps as $step) {
522      if ($step === NULL) {
523        continue;
524      }
525      $user_id = $step['user'];
526
527      if ($user_id !== NULL && (!isset($users[$user_id]) || $step['time'] > $users[$user_id])) {
528        $users[$user_id] = $step['time'];
529      }
530    }
531
532    return $users;
533  }
518    $users = [];
519    $steps = \array_merge($this->get('past')->getValue(), $this->get('present')->getValue(), $this->get('future')->getValue());
520
521    foreach ($steps as $step) {
 
521    foreach ($steps as $step) {
 
522      if ($step === NULL) {
 
525      $user_id = $step['user'];
526
527      if ($user_id !== NULL && (!isset($users[$user_id]) || $step['time'] > $users[$user_id])) {
 
527      if ($user_id !== NULL && (!isset($users[$user_id]) || $step['time'] > $users[$user_id])) {
 
521    foreach ($steps as $step) {
522      if ($step === NULL) {
523        continue;
524      }
525      $user_id = $step['user'];
526
527      if ($user_id !== NULL && (!isset($users[$user_id]) || $step['time'] > $users[$user_id])) {
528        $users[$user_id] = $step['time'];
 
521    foreach ($steps as $step) {
 
521    foreach ($steps as $step) {
 
521    foreach ($steps as $step) {
522      if ($step === NULL) {
523        continue;
524      }
525      $user_id = $step['user'];
526
527      if ($user_id !== NULL && (!isset($users[$user_id]) || $step['time'] > $users[$user_id])) {
528        $users[$user_id] = $step['time'];
529      }
530    }
531
532    return $users;
533  }
518    $users = [];
519    $steps = \array_merge($this->get('past')->getValue(), $this->get('present')->getValue(), $this->get('future')->getValue());
520
521    foreach ($steps as $step) {
 
521    foreach ($steps as $step) {
 
522      if ($step === NULL) {
 
525      $user_id = $step['user'];
526
527      if ($user_id !== NULL && (!isset($users[$user_id]) || $step['time'] > $users[$user_id])) {
 
527      if ($user_id !== NULL && (!isset($users[$user_id]) || $step['time'] > $users[$user_id])) {
 
521    foreach ($steps as $step) {
 
521    foreach ($steps as $step) {
 
521    foreach ($steps as $step) {
522      if ($step === NULL) {
523        continue;
524      }
525      $user_id = $step['user'];
526
527      if ($user_id !== NULL && (!isset($users[$user_id]) || $step['time'] > $users[$user_id])) {
528        $users[$user_id] = $step['time'];
529      }
530    }
531
532    return $users;
533  }
518    $users = [];
519    $steps = \array_merge($this->get('past')->getValue(), $this->get('present')->getValue(), $this->get('future')->getValue());
520
521    foreach ($steps as $step) {
 
521    foreach ($steps as $step) {
 
521    foreach ($steps as $step) {
522      if ($step === NULL) {
523        continue;
524      }
525      $user_id = $step['user'];
526
527      if ($user_id !== NULL && (!isset($users[$user_id]) || $step['time'] > $users[$user_id])) {
528        $users[$user_id] = $step['time'];
529      }
530    }
531
532    return $users;
533  }
518    $users = [];
519    $steps = \array_merge($this->get('past')->getValue(), $this->get('present')->getValue(), $this->get('future')->getValue());
520
521    foreach ($steps as $step) {
 
521    foreach ($steps as $step) {
522      if ($step === NULL) {
523        continue;
524      }
525      $user_id = $step['user'];
526
527      if ($user_id !== NULL && (!isset($users[$user_id]) || $step['time'] > $users[$user_id])) {
528        $users[$user_id] = $step['time'];
529      }
530    }
531
532    return $users;
533  }
Instance->hasSaveContextsRequirement
552  public function hasSaveContextsRequirement(string $key, array $contexts = []): bool {
553    $contexts = empty($contexts) ? $this->getContexts() : $contexts;
 
553    $contexts = empty($contexts) ? $this->getContexts() : $contexts;
 
553    $contexts = empty($contexts) ? $this->getContexts() : $contexts;
554
555    if (!\array_key_exists('context_requirements', $contexts)
 
556      || !($contexts['context_requirements'] instanceof RequirementsContext)
 
556      || !($contexts['context_requirements'] instanceof RequirementsContext)
 
557      || !$contexts['context_requirements']->hasValue($key)) {
 
557      || !$contexts['context_requirements']->hasValue($key)) {
 
558      return FALSE;
552  public function hasSaveContextsRequirement(string $key, array $contexts = []): bool {
553    $contexts = empty($contexts) ? $this->getContexts() : $contexts;
 
553    $contexts = empty($contexts) ? $this->getContexts() : $contexts;
 
553    $contexts = empty($contexts) ? $this->getContexts() : $contexts;
554
555    if (!\array_key_exists('context_requirements', $contexts)
 
556      || !($contexts['context_requirements'] instanceof RequirementsContext)
 
556      || !($contexts['context_requirements'] instanceof RequirementsContext)
 
557      || !$contexts['context_requirements']->hasValue($key)) {
 
557      || !$contexts['context_requirements']->hasValue($key)) {
 
561    return TRUE;
562  }
552  public function hasSaveContextsRequirement(string $key, array $contexts = []): bool {
553    $contexts = empty($contexts) ? $this->getContexts() : $contexts;
 
553    $contexts = empty($contexts) ? $this->getContexts() : $contexts;
 
553    $contexts = empty($contexts) ? $this->getContexts() : $contexts;
554
555    if (!\array_key_exists('context_requirements', $contexts)
 
556      || !($contexts['context_requirements'] instanceof RequirementsContext)
 
556      || !($contexts['context_requirements'] instanceof RequirementsContext)
 
557      || !$contexts['context_requirements']->hasValue($key)) {
 
558      return FALSE;
552  public function hasSaveContextsRequirement(string $key, array $contexts = []): bool {
553    $contexts = empty($contexts) ? $this->getContexts() : $contexts;
 
553    $contexts = empty($contexts) ? $this->getContexts() : $contexts;
 
553    $contexts = empty($contexts) ? $this->getContexts() : $contexts;
554
555    if (!\array_key_exists('context_requirements', $contexts)
 
556      || !($contexts['context_requirements'] instanceof RequirementsContext)
 
556      || !($contexts['context_requirements'] instanceof RequirementsContext)
 
557      || !$contexts['context_requirements']->hasValue($key)) {
 
561    return TRUE;
562  }
552  public function hasSaveContextsRequirement(string $key, array $contexts = []): bool {
553    $contexts = empty($contexts) ? $this->getContexts() : $contexts;
 
553    $contexts = empty($contexts) ? $this->getContexts() : $contexts;
 
553    $contexts = empty($contexts) ? $this->getContexts() : $contexts;
554
555    if (!\array_key_exists('context_requirements', $contexts)
 
556      || !($contexts['context_requirements'] instanceof RequirementsContext)
 
557      || !$contexts['context_requirements']->hasValue($key)) {
 
557      || !$contexts['context_requirements']->hasValue($key)) {
 
558      return FALSE;
552  public function hasSaveContextsRequirement(string $key, array $contexts = []): bool {
553    $contexts = empty($contexts) ? $this->getContexts() : $contexts;
 
553    $contexts = empty($contexts) ? $this->getContexts() : $contexts;
 
553    $contexts = empty($contexts) ? $this->getContexts() : $contexts;
554
555    if (!\array_key_exists('context_requirements', $contexts)
 
556      || !($contexts['context_requirements'] instanceof RequirementsContext)
 
557      || !$contexts['context_requirements']->hasValue($key)) {
 
557      || !$contexts['context_requirements']->hasValue($key)) {
 
561    return TRUE;
562  }
552  public function hasSaveContextsRequirement(string $key, array $contexts = []): bool {
553    $contexts = empty($contexts) ? $this->getContexts() : $contexts;
 
553    $contexts = empty($contexts) ? $this->getContexts() : $contexts;
 
553    $contexts = empty($contexts) ? $this->getContexts() : $contexts;
554
555    if (!\array_key_exists('context_requirements', $contexts)
 
556      || !($contexts['context_requirements'] instanceof RequirementsContext)
 
557      || !$contexts['context_requirements']->hasValue($key)) {
 
558      return FALSE;
552  public function hasSaveContextsRequirement(string $key, array $contexts = []): bool {
553    $contexts = empty($contexts) ? $this->getContexts() : $contexts;
 
553    $contexts = empty($contexts) ? $this->getContexts() : $contexts;
 
553    $contexts = empty($contexts) ? $this->getContexts() : $contexts;
554
555    if (!\array_key_exists('context_requirements', $contexts)
 
556      || !($contexts['context_requirements'] instanceof RequirementsContext)
 
557      || !$contexts['context_requirements']->hasValue($key)) {
 
561    return TRUE;
562  }
552  public function hasSaveContextsRequirement(string $key, array $contexts = []): bool {
553    $contexts = empty($contexts) ? $this->getContexts() : $contexts;
 
553    $contexts = empty($contexts) ? $this->getContexts() : $contexts;
 
553    $contexts = empty($contexts) ? $this->getContexts() : $contexts;
554
555    if (!\array_key_exists('context_requirements', $contexts)
 
556      || !($contexts['context_requirements'] instanceof RequirementsContext)
 
556      || !($contexts['context_requirements'] instanceof RequirementsContext)
 
557      || !$contexts['context_requirements']->hasValue($key)) {
 
557      || !$contexts['context_requirements']->hasValue($key)) {
 
558      return FALSE;
552  public function hasSaveContextsRequirement(string $key, array $contexts = []): bool {
553    $contexts = empty($contexts) ? $this->getContexts() : $contexts;
 
553    $contexts = empty($contexts) ? $this->getContexts() : $contexts;
 
553    $contexts = empty($contexts) ? $this->getContexts() : $contexts;
554
555    if (!\array_key_exists('context_requirements', $contexts)
 
556      || !($contexts['context_requirements'] instanceof RequirementsContext)
 
556      || !($contexts['context_requirements'] instanceof RequirementsContext)
 
557      || !$contexts['context_requirements']->hasValue($key)) {
 
557      || !$contexts['context_requirements']->hasValue($key)) {
 
561    return TRUE;
562  }
552  public function hasSaveContextsRequirement(string $key, array $contexts = []): bool {
553    $contexts = empty($contexts) ? $this->getContexts() : $contexts;
 
553    $contexts = empty($contexts) ? $this->getContexts() : $contexts;
 
553    $contexts = empty($contexts) ? $this->getContexts() : $contexts;
554
555    if (!\array_key_exists('context_requirements', $contexts)
 
556      || !($contexts['context_requirements'] instanceof RequirementsContext)
 
556      || !($contexts['context_requirements'] instanceof RequirementsContext)
 
557      || !$contexts['context_requirements']->hasValue($key)) {
 
558      return FALSE;
552  public function hasSaveContextsRequirement(string $key, array $contexts = []): bool {
553    $contexts = empty($contexts) ? $this->getContexts() : $contexts;
 
553    $contexts = empty($contexts) ? $this->getContexts() : $contexts;
 
553    $contexts = empty($contexts) ? $this->getContexts() : $contexts;
554
555    if (!\array_key_exists('context_requirements', $contexts)
 
556      || !($contexts['context_requirements'] instanceof RequirementsContext)
 
556      || !($contexts['context_requirements'] instanceof RequirementsContext)
 
557      || !$contexts['context_requirements']->hasValue($key)) {
 
561    return TRUE;
562  }
552  public function hasSaveContextsRequirement(string $key, array $contexts = []): bool {
553    $contexts = empty($contexts) ? $this->getContexts() : $contexts;
 
553    $contexts = empty($contexts) ? $this->getContexts() : $contexts;
 
553    $contexts = empty($contexts) ? $this->getContexts() : $contexts;
554
555    if (!\array_key_exists('context_requirements', $contexts)
 
556      || !($contexts['context_requirements'] instanceof RequirementsContext)
 
557      || !$contexts['context_requirements']->hasValue($key)) {
 
557      || !$contexts['context_requirements']->hasValue($key)) {
 
558      return FALSE;
552  public function hasSaveContextsRequirement(string $key, array $contexts = []): bool {
553    $contexts = empty($contexts) ? $this->getContexts() : $contexts;
 
553    $contexts = empty($contexts) ? $this->getContexts() : $contexts;
 
553    $contexts = empty($contexts) ? $this->getContexts() : $contexts;
554
555    if (!\array_key_exists('context_requirements', $contexts)
 
556      || !($contexts['context_requirements'] instanceof RequirementsContext)
 
557      || !$contexts['context_requirements']->hasValue($key)) {
 
557      || !$contexts['context_requirements']->hasValue($key)) {
 
561    return TRUE;
562  }
552  public function hasSaveContextsRequirement(string $key, array $contexts = []): bool {
553    $contexts = empty($contexts) ? $this->getContexts() : $contexts;
 
553    $contexts = empty($contexts) ? $this->getContexts() : $contexts;
 
553    $contexts = empty($contexts) ? $this->getContexts() : $contexts;
554
555    if (!\array_key_exists('context_requirements', $contexts)
 
556      || !($contexts['context_requirements'] instanceof RequirementsContext)
 
557      || !$contexts['context_requirements']->hasValue($key)) {
 
558      return FALSE;
552  public function hasSaveContextsRequirement(string $key, array $contexts = []): bool {
553    $contexts = empty($contexts) ? $this->getContexts() : $contexts;
 
553    $contexts = empty($contexts) ? $this->getContexts() : $contexts;
 
553    $contexts = empty($contexts) ? $this->getContexts() : $contexts;
554
555    if (!\array_key_exists('context_requirements', $contexts)
 
556      || !($contexts['context_requirements'] instanceof RequirementsContext)
 
557      || !$contexts['context_requirements']->hasValue($key)) {
 
561    return TRUE;
562  }
Instance->isNew
139    return !$this->id();
140  }
Instance->isPublishable
539    $contexts = $this->getContexts();
540
541    if (!\array_key_exists('context_requirements', $contexts)
 
542      || !($contexts['context_requirements'] instanceof RequirementsContext)) {
 
542      || !($contexts['context_requirements'] instanceof RequirementsContext)) {
 
543      return FALSE;
539    $contexts = $this->getContexts();
540
541    if (!\array_key_exists('context_requirements', $contexts)
 
542      || !($contexts['context_requirements'] instanceof RequirementsContext)) {
 
542      || !($contexts['context_requirements'] instanceof RequirementsContext)) {
 
546    return TRUE;
547  }
539    $contexts = $this->getContexts();
540
541    if (!\array_key_exists('context_requirements', $contexts)
 
542      || !($contexts['context_requirements'] instanceof RequirementsContext)) {
 
543      return FALSE;
539    $contexts = $this->getContexts();
540
541    if (!\array_key_exists('context_requirements', $contexts)
 
542      || !($contexts['context_requirements'] instanceof RequirementsContext)) {
 
546    return TRUE;
547  }
Instance->isPublished
568    return !$this->get('save')->isEmpty();
569  }
Instance->isPublishedPresent
575    $present = $this->get('present');
576    $save = $this->get('save');
577
578    // If either present or save is null, they can't be equal unless both are
579    // null.
580    if ($present->isEmpty() || $save->isEmpty()) {
 
580    if ($present->isEmpty() || $save->isEmpty()) {
 
580    if ($present->isEmpty() || $save->isEmpty()) {
 
581      return $present->isEmpty() && $save->isEmpty();
 
581      return $present->isEmpty() && $save->isEmpty();
 
581      return $present->isEmpty() && $save->isEmpty();
575    $present = $this->get('present');
576    $save = $this->get('save');
577
578    // If either present or save is null, they can't be equal unless both are
579    // null.
580    if ($present->isEmpty() || $save->isEmpty()) {
 
580    if ($present->isEmpty() || $save->isEmpty()) {
 
580    if ($present->isEmpty() || $save->isEmpty()) {
 
581      return $present->isEmpty() && $save->isEmpty();
 
581      return $present->isEmpty() && $save->isEmpty();
575    $present = $this->get('present');
576    $save = $this->get('save');
577
578    // If either present or save is null, they can't be equal unless both are
579    // null.
580    if ($present->isEmpty() || $save->isEmpty()) {
 
580    if ($present->isEmpty() || $save->isEmpty()) {
 
580    if ($present->isEmpty() || $save->isEmpty()) {
 
585    $present = $present->first();
586    /** @var \Drupal\display_builder\Plugin\Field\FieldType\HistoryStep|null $save */
587    $save = $save->first();
588
589    return $present->getHash() === $save->getHash();
590  }
575    $present = $this->get('present');
576    $save = $this->get('save');
577
578    // If either present or save is null, they can't be equal unless both are
579    // null.
580    if ($present->isEmpty() || $save->isEmpty()) {
 
580    if ($present->isEmpty() || $save->isEmpty()) {
 
581      return $present->isEmpty() && $save->isEmpty();
 
581      return $present->isEmpty() && $save->isEmpty();
 
581      return $present->isEmpty() && $save->isEmpty();
575    $present = $this->get('present');
576    $save = $this->get('save');
577
578    // If either present or save is null, they can't be equal unless both are
579    // null.
580    if ($present->isEmpty() || $save->isEmpty()) {
 
580    if ($present->isEmpty() || $save->isEmpty()) {
 
581      return $present->isEmpty() && $save->isEmpty();
 
581      return $present->isEmpty() && $save->isEmpty();
575    $present = $this->get('present');
576    $save = $this->get('save');
577
578    // If either present or save is null, they can't be equal unless both are
579    // null.
580    if ($present->isEmpty() || $save->isEmpty()) {
 
580    if ($present->isEmpty() || $save->isEmpty()) {
 
585    $present = $present->first();
586    /** @var \Drupal\display_builder\Plugin\Field\FieldType\HistoryStep|null $save */
587    $save = $save->first();
588
589    return $present->getHash() === $save->getHash();
590  }
Instance->label
148    $parts = \explode('__', (string) $this->id());
149
150    if (\count($parts) > 1) {
 
151      \array_shift($parts);
152
153      return \ucfirst(\implode(' ', \str_replace('_', ' ', $parts)));
148    $parts = \explode('__', (string) $this->id());
149
150    if (\count($parts) > 1) {
 
156    return (string) $this->id();
157  }
Instance->moveToRoot
221  public function moveToRoot(string $node_id, int $position): bool {
222    $tree = $this->getSourceTree();
223    $data = $tree->getNodeData($node_id);
224
225    if (!$data) {
 
226      return FALSE;
221  public function moveToRoot(string $node_id, int $position): bool {
222    $tree = $this->getSourceTree();
223    $data = $tree->getNodeData($node_id);
224
225    if (!$data) {
 
229    if (!$tree->moveToRoot($node_id, $position)) {
 
230      return FALSE;
221  public function moveToRoot(string $node_id, int $position): bool {
222    $tree = $this->getSourceTree();
223    $data = $tree->getNodeData($node_id);
224
225    if (!$data) {
 
229    if (!$tree->moveToRoot($node_id, $position)) {
 
233    $log = new TranslatableMarkup('@label moved to root', ['@label' => $this->nodeLabel($data)]);
234    $this->setNewPresent($tree->getTree(), $log, TRUE, FALSE);
235
236    return TRUE;
237  }
Instance->moveToSlot
242  public function moveToSlot(string $node_id, string $parent_id, string $slot_id, int $position): bool {
243    $tree = $this->getSourceTree();
244    $data = $tree->getNodeData($node_id);
245
246    if (!$data) {
 
247      return FALSE;
242  public function moveToSlot(string $node_id, string $parent_id, string $slot_id, int $position): bool {
243    $tree = $this->getSourceTree();
244    $data = $tree->getNodeData($node_id);
245
246    if (!$data) {
 
250    if (!$tree->moveToSlot($node_id, $parent_id, $slot_id, $position)) {
 
251      return FALSE;
242  public function moveToSlot(string $node_id, string $parent_id, string $slot_id, int $position): bool {
243    $tree = $this->getSourceTree();
244    $data = $tree->getNodeData($node_id);
245
246    if (!$data) {
 
250    if (!$tree->moveToSlot($node_id, $parent_id, $slot_id, $position)) {
 
254    $parentData = $tree->getNodeData($parent_id);
255    $log = new TranslatableMarkup('@label moved to slot @slot_id in @parent_label', [
256      '@label' => $this->nodeLabel($data),
257      '@slot_id' => $slot_id,
258      '@parent_label' => $parentData ? $this->nodeLabel($parentData) : $parent_id,
 
258      '@parent_label' => $parentData ? $this->nodeLabel($parentData) : $parent_id,
 
258      '@parent_label' => $parentData ? $this->nodeLabel($parentData) : $parent_id,
259    ]);
260
261    $this->setNewPresent($tree->getTree(), $log, TRUE, FALSE);
262
263    return TRUE;
264  }
242  public function moveToSlot(string $node_id, string $parent_id, string $slot_id, int $position): bool {
243    $tree = $this->getSourceTree();
244    $data = $tree->getNodeData($node_id);
245
246    if (!$data) {
 
250    if (!$tree->moveToSlot($node_id, $parent_id, $slot_id, $position)) {
 
254    $parentData = $tree->getNodeData($parent_id);
255    $log = new TranslatableMarkup('@label moved to slot @slot_id in @parent_label', [
256      '@label' => $this->nodeLabel($data),
257      '@slot_id' => $slot_id,
258      '@parent_label' => $parentData ? $this->nodeLabel($parentData) : $parent_id,
 
258      '@parent_label' => $parentData ? $this->nodeLabel($parentData) : $parent_id,
 
258      '@parent_label' => $parentData ? $this->nodeLabel($parentData) : $parent_id,
259    ]);
260
261    $this->setNewPresent($tree->getTree(), $log, TRUE, FALSE);
262
263    return TRUE;
264  }
Instance->nodeLabel
704  private function nodeLabel(array $data): string {
705    $label = $this->slotSourceProxy()->getLabelWithSummary($data, [], TRUE)['label'];
706
707    return $label !== '' ? $label : ($data['source_id'] ?? '');
 
707    return $label !== '' ? $label : ($data['source_id'] ?? '');
 
707    return $label !== '' ? $label : ($data['source_id'] ?? '');
708  }
704  private function nodeLabel(array $data): string {
705    $label = $this->slotSourceProxy()->getLabelWithSummary($data, [], TRUE)['label'];
706
707    return $label !== '' ? $label : ($data['source_id'] ?? '');
 
707    return $label !== '' ? $label : ($data['source_id'] ?? '');
 
707    return $label !== '' ? $label : ($data['source_id'] ?? '');
708  }
Instance->postCreate
181  public function postCreate(EntityStorageInterface $storage): void {
182    if ($this->get('present')->isEmpty()) {
 
183      return;
181  public function postCreate(EntityStorageInterface $storage): void {
182    if ($this->get('present')->isEmpty()) {
 
186    $present = $this->get('present')->first();
187    $this->sourceTree = new SourceTree($present->getData() ?? []);
188    $indexed = $this->sourceTree->getTree();
189    $hash = self::getUniqId($indexed);
190
191    $this->set('present', [
192      'data' => $indexed,
193      'hash' => $hash,
194      'log' => $present->getLog(),
195      'time' => $present->getTime(),
196      'user' => $present->getUser(),
197    ]);
198  }
Instance->redo
458    $future = $this->get('future');
459
460    if ($future->isEmpty()) {
 
461      return;
458    $future = $this->get('future');
459
460    if ($future->isEmpty()) {
 
465    $first = $future->first()->getValue();
466    \assert(!\array_is_list($first));
467    $future->removeItem(0);
468    // Insert the old present state at the end of the past.
469    $this->get('past')->appendItem($this->get('present')->first());
470    // Set the present to the element we removed in the previous step.
471    $this->set('present', $first);
472    $this->set('future', $future->getValue());
473    $this->sourceTree = NULL;
474  }
Instance->refreshContexts
730  private function refreshContexts(array $contexts): array {
731    foreach ($contexts as &$context) {
 
731    foreach ($contexts as &$context) {
 
732      if ($context instanceof EntityContext) {
 
736        $entity = $context->getContextValue();
737
738        // Check if sample entity.
739        if ($entity->id()) {
 
739        if ($entity->id()) {
740          $entity = $this->entityTypeManager()->getStorage($entity->getEntityTypeId())->load($entity->id());
 
748        if (!$entity) {
 
749          return $contexts;
730  private function refreshContexts(array $contexts): array {
731    foreach ($contexts as &$context) {
 
731    foreach ($contexts as &$context) {
 
732      if ($context instanceof EntityContext) {
 
736        $entity = $context->getContextValue();
737
738        // Check if sample entity.
739        if ($entity->id()) {
 
739        if ($entity->id()) {
740          $entity = $this->entityTypeManager()->getStorage($entity->getEntityTypeId())->load($entity->id());
 
748        if (!$entity) {
 
731    foreach ($contexts as &$context) {
732      if ($context instanceof EntityContext) {
733        // @todo We should use cache entries here
734        // with the corresponding cache contexts in it.
735        // This may avoid some unnecessary entity loads or generation.
736        $entity = $context->getContextValue();
737
738        // Check if sample entity.
739        if ($entity->id()) {
740          $entity = $this->entityTypeManager()->getStorage($entity->getEntityTypeId())->load($entity->id());
741        }
742        else {
743          $entity = $this->sampleEntityGenerator()->get($entity->getEntityTypeId(), $entity->bundle());
744        }
745
746        // Edge case when the parent entity is deleted but not the builder
747        // instance.
748        if (!$entity) {
749          return $contexts;
750        }
751        $context = (\get_class($context))::fromEntity($entity);
 
731    foreach ($contexts as &$context) {
 
731    foreach ($contexts as &$context) {
 
731    foreach ($contexts as &$context) {
732      if ($context instanceof EntityContext) {
733        // @todo We should use cache entries here
734        // with the corresponding cache contexts in it.
735        // This may avoid some unnecessary entity loads or generation.
736        $entity = $context->getContextValue();
737
738        // Check if sample entity.
739        if ($entity->id()) {
740          $entity = $this->entityTypeManager()->getStorage($entity->getEntityTypeId())->load($entity->id());
741        }
742        else {
743          $entity = $this->sampleEntityGenerator()->get($entity->getEntityTypeId(), $entity->bundle());
744        }
745
746        // Edge case when the parent entity is deleted but not the builder
747        // instance.
748        if (!$entity) {
749          return $contexts;
750        }
751        $context = (\get_class($context))::fromEntity($entity);
752      }
753    }
754
755    return $contexts;
756  }
730  private function refreshContexts(array $contexts): array {
731    foreach ($contexts as &$context) {
 
731    foreach ($contexts as &$context) {
 
732      if ($context instanceof EntityContext) {
 
736        $entity = $context->getContextValue();
737
738        // Check if sample entity.
739        if ($entity->id()) {
 
743          $entity = $this->sampleEntityGenerator()->get($entity->getEntityTypeId(), $entity->bundle());
744        }
745
746        // Edge case when the parent entity is deleted but not the builder
747        // instance.
748        if (!$entity) {
 
748        if (!$entity) {
 
749          return $contexts;
730  private function refreshContexts(array $contexts): array {
731    foreach ($contexts as &$context) {
 
731    foreach ($contexts as &$context) {
 
732      if ($context instanceof EntityContext) {
 
736        $entity = $context->getContextValue();
737
738        // Check if sample entity.
739        if ($entity->id()) {
 
743          $entity = $this->sampleEntityGenerator()->get($entity->getEntityTypeId(), $entity->bundle());
744        }
745
746        // Edge case when the parent entity is deleted but not the builder
747        // instance.
748        if (!$entity) {
 
748        if (!$entity) {
 
731    foreach ($contexts as &$context) {
732      if ($context instanceof EntityContext) {
733        // @todo We should use cache entries here
734        // with the corresponding cache contexts in it.
735        // This may avoid some unnecessary entity loads or generation.
736        $entity = $context->getContextValue();
737
738        // Check if sample entity.
739        if ($entity->id()) {
740          $entity = $this->entityTypeManager()->getStorage($entity->getEntityTypeId())->load($entity->id());
741        }
742        else {
743          $entity = $this->sampleEntityGenerator()->get($entity->getEntityTypeId(), $entity->bundle());
744        }
745
746        // Edge case when the parent entity is deleted but not the builder
747        // instance.
748        if (!$entity) {
749          return $contexts;
750        }
751        $context = (\get_class($context))::fromEntity($entity);
 
731    foreach ($contexts as &$context) {
 
731    foreach ($contexts as &$context) {
 
731    foreach ($contexts as &$context) {
732      if ($context instanceof EntityContext) {
733        // @todo We should use cache entries here
734        // with the corresponding cache contexts in it.
735        // This may avoid some unnecessary entity loads or generation.
736        $entity = $context->getContextValue();
737
738        // Check if sample entity.
739        if ($entity->id()) {
740          $entity = $this->entityTypeManager()->getStorage($entity->getEntityTypeId())->load($entity->id());
741        }
742        else {
743          $entity = $this->sampleEntityGenerator()->get($entity->getEntityTypeId(), $entity->bundle());
744        }
745
746        // Edge case when the parent entity is deleted but not the builder
747        // instance.
748        if (!$entity) {
749          return $contexts;
750        }
751        $context = (\get_class($context))::fromEntity($entity);
752      }
753    }
754
755    return $contexts;
756  }
730  private function refreshContexts(array $contexts): array {
731    foreach ($contexts as &$context) {
 
731    foreach ($contexts as &$context) {
 
732      if ($context instanceof EntityContext) {
 
731    foreach ($contexts as &$context) {
 
731    foreach ($contexts as &$context) {
 
731    foreach ($contexts as &$context) {
732      if ($context instanceof EntityContext) {
733        // @todo We should use cache entries here
734        // with the corresponding cache contexts in it.
735        // This may avoid some unnecessary entity loads or generation.
736        $entity = $context->getContextValue();
737
738        // Check if sample entity.
739        if ($entity->id()) {
740          $entity = $this->entityTypeManager()->getStorage($entity->getEntityTypeId())->load($entity->id());
741        }
742        else {
743          $entity = $this->sampleEntityGenerator()->get($entity->getEntityTypeId(), $entity->bundle());
744        }
745
746        // Edge case when the parent entity is deleted but not the builder
747        // instance.
748        if (!$entity) {
749          return $contexts;
750        }
751        $context = (\get_class($context))::fromEntity($entity);
752      }
753    }
754
755    return $contexts;
756  }
730  private function refreshContexts(array $contexts): array {
731    foreach ($contexts as &$context) {
 
731    foreach ($contexts as &$context) {
 
731    foreach ($contexts as &$context) {
732      if ($context instanceof EntityContext) {
733        // @todo We should use cache entries here
734        // with the corresponding cache contexts in it.
735        // This may avoid some unnecessary entity loads or generation.
736        $entity = $context->getContextValue();
737
738        // Check if sample entity.
739        if ($entity->id()) {
740          $entity = $this->entityTypeManager()->getStorage($entity->getEntityTypeId())->load($entity->id());
741        }
742        else {
743          $entity = $this->sampleEntityGenerator()->get($entity->getEntityTypeId(), $entity->bundle());
744        }
745
746        // Edge case when the parent entity is deleted but not the builder
747        // instance.
748        if (!$entity) {
749          return $contexts;
750        }
751        $context = (\get_class($context))::fromEntity($entity);
752      }
753    }
754
755    return $contexts;
756  }
730  private function refreshContexts(array $contexts): array {
731    foreach ($contexts as &$context) {
 
731    foreach ($contexts as &$context) {
732      if ($context instanceof EntityContext) {
733        // @todo We should use cache entries here
734        // with the corresponding cache contexts in it.
735        // This may avoid some unnecessary entity loads or generation.
736        $entity = $context->getContextValue();
737
738        // Check if sample entity.
739        if ($entity->id()) {
740          $entity = $this->entityTypeManager()->getStorage($entity->getEntityTypeId())->load($entity->id());
741        }
742        else {
743          $entity = $this->sampleEntityGenerator()->get($entity->getEntityTypeId(), $entity->bundle());
744        }
745
746        // Edge case when the parent entity is deleted but not the builder
747        // instance.
748        if (!$entity) {
749          return $contexts;
750        }
751        $context = (\get_class($context))::fromEntity($entity);
752      }
753    }
754
755    return $contexts;
756  }
Instance->remove
365  public function remove(string $node_id): void {
366    $tree = $this->getSourceTree();
367    $data = $tree->getNodeData($node_id);
368
369    if (!$data) {
 
370      return;
365  public function remove(string $node_id): void {
366    $tree = $this->getSourceTree();
367    $data = $tree->getNodeData($node_id);
368
369    if (!$data) {
 
372    $parent_id = $tree->getParentId($node_id);
373
374    $tree->remove($node_id);
375
376    $parentData = $parent_id === NULL ? NULL : $tree->getNodeData($parent_id);
 
376    $parentData = $parent_id === NULL ? NULL : $tree->getNodeData($parent_id);
 
376    $parentData = $parent_id === NULL ? NULL : $tree->getNodeData($parent_id);
377    $log = new TranslatableMarkup('@label removed from @parent_label', [
378      '@label' => $this->nodeLabel($data),
379      '@parent_label' => $parentData ? $this->nodeLabel($parentData) : 'root',
 
379      '@parent_label' => $parentData ? $this->nodeLabel($parentData) : 'root',
 
379      '@parent_label' => $parentData ? $this->nodeLabel($parentData) : 'root',
380    ]);
381    $this->setNewPresent($tree->getTree(), $log, FALSE, FALSE);
382  }
365  public function remove(string $node_id): void {
366    $tree = $this->getSourceTree();
367    $data = $tree->getNodeData($node_id);
368
369    if (!$data) {
 
372    $parent_id = $tree->getParentId($node_id);
373
374    $tree->remove($node_id);
375
376    $parentData = $parent_id === NULL ? NULL : $tree->getNodeData($parent_id);
 
376    $parentData = $parent_id === NULL ? NULL : $tree->getNodeData($parent_id);
 
376    $parentData = $parent_id === NULL ? NULL : $tree->getNodeData($parent_id);
377    $log = new TranslatableMarkup('@label removed from @parent_label', [
378      '@label' => $this->nodeLabel($data),
379      '@parent_label' => $parentData ? $this->nodeLabel($parentData) : 'root',
 
379      '@parent_label' => $parentData ? $this->nodeLabel($parentData) : 'root',
 
379      '@parent_label' => $parentData ? $this->nodeLabel($parentData) : 'root',
380    ]);
381    $this->setNewPresent($tree->getTree(), $log, FALSE, FALSE);
382  }
365  public function remove(string $node_id): void {
366    $tree = $this->getSourceTree();
367    $data = $tree->getNodeData($node_id);
368
369    if (!$data) {
 
372    $parent_id = $tree->getParentId($node_id);
373
374    $tree->remove($node_id);
375
376    $parentData = $parent_id === NULL ? NULL : $tree->getNodeData($parent_id);
 
376    $parentData = $parent_id === NULL ? NULL : $tree->getNodeData($parent_id);
 
376    $parentData = $parent_id === NULL ? NULL : $tree->getNodeData($parent_id);
377    $log = new TranslatableMarkup('@label removed from @parent_label', [
378      '@label' => $this->nodeLabel($data),
379      '@parent_label' => $parentData ? $this->nodeLabel($parentData) : 'root',
 
379      '@parent_label' => $parentData ? $this->nodeLabel($parentData) : 'root',
 
379      '@parent_label' => $parentData ? $this->nodeLabel($parentData) : 'root',
380    ]);
381    $this->setNewPresent($tree->getTree(), $log, FALSE, FALSE);
382  }
365  public function remove(string $node_id): void {
366    $tree = $this->getSourceTree();
367    $data = $tree->getNodeData($node_id);
368
369    if (!$data) {
 
372    $parent_id = $tree->getParentId($node_id);
373
374    $tree->remove($node_id);
375
376    $parentData = $parent_id === NULL ? NULL : $tree->getNodeData($parent_id);
 
376    $parentData = $parent_id === NULL ? NULL : $tree->getNodeData($parent_id);
 
376    $parentData = $parent_id === NULL ? NULL : $tree->getNodeData($parent_id);
377    $log = new TranslatableMarkup('@label removed from @parent_label', [
378      '@label' => $this->nodeLabel($data),
379      '@parent_label' => $parentData ? $this->nodeLabel($parentData) : 'root',
 
379      '@parent_label' => $parentData ? $this->nodeLabel($parentData) : 'root',
 
379      '@parent_label' => $parentData ? $this->nodeLabel($parentData) : 'root',
380    ]);
381    $this->setNewPresent($tree->getTree(), $log, FALSE, FALSE);
382  }
Instance->restore
419    $first = $this->get('save')->first();
420    $this->setNewPresent($first->getData(), new TranslatableMarkup('Back to saved data.'));
421  }
Instance->sampleEntityGenerator
691    return $this->sampleEntityGenerator ??= \Drupal::service('ui_patterns.sample_entity_generator');
692  }
Instance->setNewPresent
604  public function setNewPresent(array $data, string|\Stringable $log_message = '', bool $check_hash = TRUE, bool $index = TRUE): void {
605    if ($index) {
 
607      $this->sourceTree = new SourceTree($data);
608      $data = $this->sourceTree->getTree();
609    }
610    // When index=FALSE the data was produced by the cached tree's getTree(),
611    // so $this->sourceTree already reflects the new state â€” no invalidation.
612    $hash = self::getUniqId($data);
 
612    $hash = self::getUniqId($data);
613
614    if (!$this->get('present')->isEmpty()) {
 
616      $present = $this->get('present')->first();
617
618      // Check if this present is the same to avoid duplicates, for example move
619      // to the same place.
620      if ($check_hash && $hash === $present->getHash()) {
 
620      if ($check_hash && $hash === $present->getHash()) {
 
620      if ($check_hash && $hash === $present->getHash()) {
 
621        return;
604  public function setNewPresent(array $data, string|\Stringable $log_message = '', bool $check_hash = TRUE, bool $index = TRUE): void {
605    if ($index) {
 
607      $this->sourceTree = new SourceTree($data);
608      $data = $this->sourceTree->getTree();
609    }
610    // When index=FALSE the data was produced by the cached tree's getTree(),
611    // so $this->sourceTree already reflects the new state â€” no invalidation.
612    $hash = self::getUniqId($data);
 
612    $hash = self::getUniqId($data);
613
614    if (!$this->get('present')->isEmpty()) {
 
616      $present = $this->get('present')->first();
617
618      // Check if this present is the same to avoid duplicates, for example move
619      // to the same place.
620      if ($check_hash && $hash === $present->getHash()) {
 
620      if ($check_hash && $hash === $present->getHash()) {
 
620      if ($check_hash && $hash === $present->getHash()) {
 
627      $this->get('past')->appendItem($present->getValue());
628    }
629
630    // Keep only the last x history.
631    if ($this->get('past')->count() > self::MAX_HISTORY) {
 
631    if ($this->get('past')->count() > self::MAX_HISTORY) {
 
632      $this->get('past')->removeItem(0);
633    }
634
635    // 2. Set the present to the new state.
636    $this->set('present', [
 
636    $this->set('present', [
637      'data' => $data,
638      'hash' => $hash,
639      'log' => $log_message,
640      'time' => \time(),
641      'user' => (int) $this->currentUser()->id(),
642    ]);
643
644    // 3. Clear the future.
645    $this->set('future', []);
646  }
604  public function setNewPresent(array $data, string|\Stringable $log_message = '', bool $check_hash = TRUE, bool $index = TRUE): void {
605    if ($index) {
 
607      $this->sourceTree = new SourceTree($data);
608      $data = $this->sourceTree->getTree();
609    }
610    // When index=FALSE the data was produced by the cached tree's getTree(),
611    // so $this->sourceTree already reflects the new state â€” no invalidation.
612    $hash = self::getUniqId($data);
 
612    $hash = self::getUniqId($data);
613
614    if (!$this->get('present')->isEmpty()) {
 
616      $present = $this->get('present')->first();
617
618      // Check if this present is the same to avoid duplicates, for example move
619      // to the same place.
620      if ($check_hash && $hash === $present->getHash()) {
 
620      if ($check_hash && $hash === $present->getHash()) {
 
620      if ($check_hash && $hash === $present->getHash()) {
 
627      $this->get('past')->appendItem($present->getValue());
628    }
629
630    // Keep only the last x history.
631    if ($this->get('past')->count() > self::MAX_HISTORY) {
 
631    if ($this->get('past')->count() > self::MAX_HISTORY) {
 
636    $this->set('present', [
637      'data' => $data,
638      'hash' => $hash,
639      'log' => $log_message,
640      'time' => \time(),
641      'user' => (int) $this->currentUser()->id(),
642    ]);
643
644    // 3. Clear the future.
645    $this->set('future', []);
646  }
604  public function setNewPresent(array $data, string|\Stringable $log_message = '', bool $check_hash = TRUE, bool $index = TRUE): void {
605    if ($index) {
 
607      $this->sourceTree = new SourceTree($data);
608      $data = $this->sourceTree->getTree();
609    }
610    // When index=FALSE the data was produced by the cached tree's getTree(),
611    // so $this->sourceTree already reflects the new state â€” no invalidation.
612    $hash = self::getUniqId($data);
 
612    $hash = self::getUniqId($data);
613
614    if (!$this->get('present')->isEmpty()) {
 
616      $present = $this->get('present')->first();
617
618      // Check if this present is the same to avoid duplicates, for example move
619      // to the same place.
620      if ($check_hash && $hash === $present->getHash()) {
 
620      if ($check_hash && $hash === $present->getHash()) {
 
621        return;
604  public function setNewPresent(array $data, string|\Stringable $log_message = '', bool $check_hash = TRUE, bool $index = TRUE): void {
605    if ($index) {
 
607      $this->sourceTree = new SourceTree($data);
608      $data = $this->sourceTree->getTree();
609    }
610    // When index=FALSE the data was produced by the cached tree's getTree(),
611    // so $this->sourceTree already reflects the new state â€” no invalidation.
612    $hash = self::getUniqId($data);
 
612    $hash = self::getUniqId($data);
613
614    if (!$this->get('present')->isEmpty()) {
 
616      $present = $this->get('present')->first();
617
618      // Check if this present is the same to avoid duplicates, for example move
619      // to the same place.
620      if ($check_hash && $hash === $present->getHash()) {
 
620      if ($check_hash && $hash === $present->getHash()) {
 
627      $this->get('past')->appendItem($present->getValue());
628    }
629
630    // Keep only the last x history.
631    if ($this->get('past')->count() > self::MAX_HISTORY) {
 
631    if ($this->get('past')->count() > self::MAX_HISTORY) {
 
632      $this->get('past')->removeItem(0);
633    }
634
635    // 2. Set the present to the new state.
636    $this->set('present', [
 
636    $this->set('present', [
637      'data' => $data,
638      'hash' => $hash,
639      'log' => $log_message,
640      'time' => \time(),
641      'user' => (int) $this->currentUser()->id(),
642    ]);
643
644    // 3. Clear the future.
645    $this->set('future', []);
646  }
604  public function setNewPresent(array $data, string|\Stringable $log_message = '', bool $check_hash = TRUE, bool $index = TRUE): void {
605    if ($index) {
 
607      $this->sourceTree = new SourceTree($data);
608      $data = $this->sourceTree->getTree();
609    }
610    // When index=FALSE the data was produced by the cached tree's getTree(),
611    // so $this->sourceTree already reflects the new state â€” no invalidation.
612    $hash = self::getUniqId($data);
 
612    $hash = self::getUniqId($data);
613
614    if (!$this->get('present')->isEmpty()) {
 
616      $present = $this->get('present')->first();
617
618      // Check if this present is the same to avoid duplicates, for example move
619      // to the same place.
620      if ($check_hash && $hash === $present->getHash()) {
 
620      if ($check_hash && $hash === $present->getHash()) {
 
627      $this->get('past')->appendItem($present->getValue());
628    }
629
630    // Keep only the last x history.
631    if ($this->get('past')->count() > self::MAX_HISTORY) {
 
631    if ($this->get('past')->count() > self::MAX_HISTORY) {
 
636    $this->set('present', [
637      'data' => $data,
638      'hash' => $hash,
639      'log' => $log_message,
640      'time' => \time(),
641      'user' => (int) $this->currentUser()->id(),
642    ]);
643
644    // 3. Clear the future.
645    $this->set('future', []);
646  }
604  public function setNewPresent(array $data, string|\Stringable $log_message = '', bool $check_hash = TRUE, bool $index = TRUE): void {
605    if ($index) {
 
607      $this->sourceTree = new SourceTree($data);
608      $data = $this->sourceTree->getTree();
609    }
610    // When index=FALSE the data was produced by the cached tree's getTree(),
611    // so $this->sourceTree already reflects the new state â€” no invalidation.
612    $hash = self::getUniqId($data);
 
612    $hash = self::getUniqId($data);
613
614    if (!$this->get('present')->isEmpty()) {
 
631    if ($this->get('past')->count() > self::MAX_HISTORY) {
 
632      $this->get('past')->removeItem(0);
633    }
634
635    // 2. Set the present to the new state.
636    $this->set('present', [
 
636    $this->set('present', [
637      'data' => $data,
638      'hash' => $hash,
639      'log' => $log_message,
640      'time' => \time(),
641      'user' => (int) $this->currentUser()->id(),
642    ]);
643
644    // 3. Clear the future.
645    $this->set('future', []);
646  }
604  public function setNewPresent(array $data, string|\Stringable $log_message = '', bool $check_hash = TRUE, bool $index = TRUE): void {
605    if ($index) {
 
607      $this->sourceTree = new SourceTree($data);
608      $data = $this->sourceTree->getTree();
609    }
610    // When index=FALSE the data was produced by the cached tree's getTree(),
611    // so $this->sourceTree already reflects the new state â€” no invalidation.
612    $hash = self::getUniqId($data);
 
612    $hash = self::getUniqId($data);
613
614    if (!$this->get('present')->isEmpty()) {
 
631    if ($this->get('past')->count() > self::MAX_HISTORY) {
 
636    $this->set('present', [
637      'data' => $data,
638      'hash' => $hash,
639      'log' => $log_message,
640      'time' => \time(),
641      'user' => (int) $this->currentUser()->id(),
642    ]);
643
644    // 3. Clear the future.
645    $this->set('future', []);
646  }
604  public function setNewPresent(array $data, string|\Stringable $log_message = '', bool $check_hash = TRUE, bool $index = TRUE): void {
605    if ($index) {
 
612    $hash = self::getUniqId($data);
613
614    if (!$this->get('present')->isEmpty()) {
 
616      $present = $this->get('present')->first();
617
618      // Check if this present is the same to avoid duplicates, for example move
619      // to the same place.
620      if ($check_hash && $hash === $present->getHash()) {
 
620      if ($check_hash && $hash === $present->getHash()) {
 
620      if ($check_hash && $hash === $present->getHash()) {
 
621        return;
604  public function setNewPresent(array $data, string|\Stringable $log_message = '', bool $check_hash = TRUE, bool $index = TRUE): void {
605    if ($index) {
 
612    $hash = self::getUniqId($data);
613
614    if (!$this->get('present')->isEmpty()) {
 
616      $present = $this->get('present')->first();
617
618      // Check if this present is the same to avoid duplicates, for example move
619      // to the same place.
620      if ($check_hash && $hash === $present->getHash()) {
 
620      if ($check_hash && $hash === $present->getHash()) {
 
620      if ($check_hash && $hash === $present->getHash()) {
 
627      $this->get('past')->appendItem($present->getValue());
628    }
629
630    // Keep only the last x history.
631    if ($this->get('past')->count() > self::MAX_HISTORY) {
 
631    if ($this->get('past')->count() > self::MAX_HISTORY) {
 
632      $this->get('past')->removeItem(0);
633    }
634
635    // 2. Set the present to the new state.
636    $this->set('present', [
 
636    $this->set('present', [
637      'data' => $data,
638      'hash' => $hash,
639      'log' => $log_message,
640      'time' => \time(),
641      'user' => (int) $this->currentUser()->id(),
642    ]);
643
644    // 3. Clear the future.
645    $this->set('future', []);
646  }
604  public function setNewPresent(array $data, string|\Stringable $log_message = '', bool $check_hash = TRUE, bool $index = TRUE): void {
605    if ($index) {
 
612    $hash = self::getUniqId($data);
613
614    if (!$this->get('present')->isEmpty()) {
 
616      $present = $this->get('present')->first();
617
618      // Check if this present is the same to avoid duplicates, for example move
619      // to the same place.
620      if ($check_hash && $hash === $present->getHash()) {
 
620      if ($check_hash && $hash === $present->getHash()) {
 
620      if ($check_hash && $hash === $present->getHash()) {
 
627      $this->get('past')->appendItem($present->getValue());
628    }
629
630    // Keep only the last x history.
631    if ($this->get('past')->count() > self::MAX_HISTORY) {
 
631    if ($this->get('past')->count() > self::MAX_HISTORY) {
 
636    $this->set('present', [
637      'data' => $data,
638      'hash' => $hash,
639      'log' => $log_message,
640      'time' => \time(),
641      'user' => (int) $this->currentUser()->id(),
642    ]);
643
644    // 3. Clear the future.
645    $this->set('future', []);
646  }
604  public function setNewPresent(array $data, string|\Stringable $log_message = '', bool $check_hash = TRUE, bool $index = TRUE): void {
605    if ($index) {
 
612    $hash = self::getUniqId($data);
613
614    if (!$this->get('present')->isEmpty()) {
 
616      $present = $this->get('present')->first();
617
618      // Check if this present is the same to avoid duplicates, for example move
619      // to the same place.
620      if ($check_hash && $hash === $present->getHash()) {
 
620      if ($check_hash && $hash === $present->getHash()) {
 
621        return;
604  public function setNewPresent(array $data, string|\Stringable $log_message = '', bool $check_hash = TRUE, bool $index = TRUE): void {
605    if ($index) {
 
612    $hash = self::getUniqId($data);
613
614    if (!$this->get('present')->isEmpty()) {
 
616      $present = $this->get('present')->first();
617
618      // Check if this present is the same to avoid duplicates, for example move
619      // to the same place.
620      if ($check_hash && $hash === $present->getHash()) {
 
620      if ($check_hash && $hash === $present->getHash()) {
 
627      $this->get('past')->appendItem($present->getValue());
628    }
629
630    // Keep only the last x history.
631    if ($this->get('past')->count() > self::MAX_HISTORY) {
 
631    if ($this->get('past')->count() > self::MAX_HISTORY) {
 
632      $this->get('past')->removeItem(0);
633    }
634
635    // 2. Set the present to the new state.
636    $this->set('present', [
 
636    $this->set('present', [
637      'data' => $data,
638      'hash' => $hash,
639      'log' => $log_message,
640      'time' => \time(),
641      'user' => (int) $this->currentUser()->id(),
642    ]);
643
644    // 3. Clear the future.
645    $this->set('future', []);
646  }
604  public function setNewPresent(array $data, string|\Stringable $log_message = '', bool $check_hash = TRUE, bool $index = TRUE): void {
605    if ($index) {
 
612    $hash = self::getUniqId($data);
613
614    if (!$this->get('present')->isEmpty()) {
 
616      $present = $this->get('present')->first();
617
618      // Check if this present is the same to avoid duplicates, for example move
619      // to the same place.
620      if ($check_hash && $hash === $present->getHash()) {
 
620      if ($check_hash && $hash === $present->getHash()) {
 
627      $this->get('past')->appendItem($present->getValue());
628    }
629
630    // Keep only the last x history.
631    if ($this->get('past')->count() > self::MAX_HISTORY) {
 
631    if ($this->get('past')->count() > self::MAX_HISTORY) {
 
636    $this->set('present', [
637      'data' => $data,
638      'hash' => $hash,
639      'log' => $log_message,
640      'time' => \time(),
641      'user' => (int) $this->currentUser()->id(),
642    ]);
643
644    // 3. Clear the future.
645    $this->set('future', []);
646  }
604  public function setNewPresent(array $data, string|\Stringable $log_message = '', bool $check_hash = TRUE, bool $index = TRUE): void {
605    if ($index) {
 
612    $hash = self::getUniqId($data);
613
614    if (!$this->get('present')->isEmpty()) {
 
631    if ($this->get('past')->count() > self::MAX_HISTORY) {
 
632      $this->get('past')->removeItem(0);
633    }
634
635    // 2. Set the present to the new state.
636    $this->set('present', [
 
636    $this->set('present', [
637      'data' => $data,
638      'hash' => $hash,
639      'log' => $log_message,
640      'time' => \time(),
641      'user' => (int) $this->currentUser()->id(),
642    ]);
643
644    // 3. Clear the future.
645    $this->set('future', []);
646  }
604  public function setNewPresent(array $data, string|\Stringable $log_message = '', bool $check_hash = TRUE, bool $index = TRUE): void {
605    if ($index) {
 
612    $hash = self::getUniqId($data);
613
614    if (!$this->get('present')->isEmpty()) {
 
631    if ($this->get('past')->count() > self::MAX_HISTORY) {
 
636    $this->set('present', [
637      'data' => $data,
638      'hash' => $hash,
639      'log' => $log_message,
640      'time' => \time(),
641      'user' => (int) $this->currentUser()->id(),
642    ]);
643
644    // 3. Clear the future.
645    $this->set('future', []);
646  }
Instance->setProfile
214  public function setProfile(string $profile_id): void {
215    $this->set('profileId', $profile_id);
216  }
Instance->setSave
398  public function setSave(array $save_data): void {
399    $tree = new SourceTree($save_data);
400    $indexed = $tree->getTree();
401    $hash = self::getUniqId($indexed);
402    $this->set('save', ['data' => $indexed, 'hash' => $hash, 'log' => NULL, 'time' => \time(), 'user' => NULL]);
403  }
Instance->setSource
332  public function setSource(string $node_id, string $source_id, array $data): void {
333    $tree = $this->getSourceTree();
334
335    if (!$tree->setSource($node_id, $source_id, $data)) {
 
336      throw new InvalidNodeException('Internal node ID mismatch');
332  public function setSource(string $node_id, string $source_id, array $data): void {
333    $tree = $this->getSourceTree();
334
335    if (!$tree->setSource($node_id, $source_id, $data)) {
 
339    $nodeData = $tree->getNodeData($node_id);
340    $log = new TranslatableMarkup('@label updated config', ['@label' => $this->nodeLabel($nodeData ?? [])]);
341    $this->setNewPresent($tree->getTree(), $log, TRUE, FALSE);
342  }
Instance->setThirdPartySettings
347  public function setThirdPartySettings(string $node_id, string $island_id, array $data): void {
348    $tree = $this->getSourceTree();
349    $nodeData = $tree->getNodeData($node_id);
350
351    if (!$tree->setThirdPartySettings($node_id, $island_id, $data)) {
 
352      return;
347  public function setThirdPartySettings(string $node_id, string $island_id, array $data): void {
348    $tree = $this->getSourceTree();
349    $nodeData = $tree->getNodeData($node_id);
350
351    if (!$tree->setThirdPartySettings($node_id, $island_id, $data)) {
 
355    $log = new TranslatableMarkup('@label settings updated by @island_id', [
356      '@label' => $this->nodeLabel($nodeData),
357      '@island_id' => $island_id,
358    ]);
359    $this->setNewPresent($tree->getTree(), $log, TRUE, FALSE);
360  }
Instance->slotSourceProxy
698    return $this->slotSourceProxy ??= \Drupal::service('display_builder.slot_sources_proxy');
699  }
Instance->toArray
166      'id' => $this->id(),
167      'profileId' => $this->get('profileId')->getString(),
168      'contexts' => $this->get('contexts')->first()?->getValue(),
169      'past' => $this->get('past')->getValue(),
170      'present' => $this->get('present')->first()?->getValue(),
171      'future' => $this->get('future')->getValue(),
172      'save' => $this->get('save')->first()?->getValue(),
173    ];
174  }
Instance->undo
429    $past = $this->get('past');
430
431    if ($past->isEmpty()) {
 
432      return;
429    $past = $this->get('past');
430
431    if ($past->isEmpty()) {
 
435    $present_values = $this->get('present')->getValue();
436    \assert(\array_is_list($present_values));
437
438    // Remove the last element from the past.
439    $past_values = $past->getValue();
440    \assert(\array_is_list($past_values));
441    $last = \array_pop($past_values);
442    \assert(!\array_is_list($last));
443    $this->set('past', $past_values);
444
445    // Set the present to the element we removed in the previous step.
446    $this->set('present', $last);
447    // Insert the old present state at the beginning of the future.
448    $this->set('future', \array_merge($present_values, $this->get('future')->getValue()));
449    $this->sourceTree = NULL;
450  }