Code Coverage
 
Lines
Branches
Paths
Functions and Methods
Classes and Traits
Total
91.09% covered (success)
91.09%
225 / 247
77.30% covered (warning)
77.30%
109 / 141
50.00% covered (danger)
50.00%
60 / 120
70.73% covered (warning)
70.73%
29 / 41
CRAP
0.00% covered (danger)
0.00%
0 / 1
Instance
91.09% covered (success)
91.09%
225 / 247
77.30% covered (warning)
77.30%
109 / 141
50.00% covered (danger)
50.00%
60 / 120
73.17% covered (warning)
73.17%
30 / 41
1033.12
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
84.62% covered (warning)
84.62%
11 / 13
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
87.50% covered (warning)
87.50%
14 / 16
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
 attachToRoot
84.62% covered (warning)
84.62%
11 / 13
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
88.89% covered (warning)
88.89%
16 / 18
50.00% covered (danger)
50.00%
4 / 8
40.00% covered (danger)
40.00%
2 / 5
0.00% covered (danger)
0.00%
0 / 1
7.46
 getNode
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
 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%
8 / 8
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%
14 / 14
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
3
 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%
12 / 12
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%
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
 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
 isHistoryNew
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
12
 getCountPast
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
 getCountFuture
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
 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
 canSaveContextsRequirement
85.71% covered (warning)
85.71%
6 / 7
85.71% covered (warning)
85.71%
6 / 7
40.00% covered (danger)
40.00%
2 / 5
0.00% covered (danger)
0.00%
0 / 1
7.46
 hasSaveContextsRequirement
100.00% covered (success)
100.00%
7 / 7
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
 hasSave
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
 saveIsCurrent
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%
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
 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
 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
 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
 getPath
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
1<?php
2
3declare(strict_types=1);
4
5namespace Drupal\display_builder\Entity;
6
7use Drupal\Component\Render\FormattableMarkup;
8use Drupal\Component\Utility\NestedArray;
9use Drupal\Core\Entity\Attribute\ContentEntityType;
10use Drupal\Core\Entity\ContentEntityBase;
11use Drupal\Core\Entity\EntityStorageInterface;
12use Drupal\Core\Entity\EntityTypeInterface;
13use Drupal\Core\Entity\EntityTypeManagerInterface;
14use Drupal\Core\Field\BaseFieldDefinition;
15use Drupal\Core\Plugin\Context\EntityContext;
16use Drupal\Core\Session\AccountInterface;
17use Drupal\Core\StringTranslation\TranslatableMarkup;
18use Drupal\display_builder\InstanceAccessControlHandler;
19use Drupal\display_builder\InstanceInterface;
20use Drupal\display_builder\InstanceStorage;
21use Drupal\display_builder\Plugin\Field\FieldType\HistoryStep;
22use Drupal\display_builder\ProfileInterface;
23use Drupal\display_builder\SlotSourceProxy;
24use Drupal\display_builder\SourceTree;
25use Drupal\display_builder_ui\InstanceListBuilder;
26use Drupal\ui_patterns\Entity\SampleEntityGeneratorInterface;
27use Drupal\ui_patterns\Plugin\Context\RequirementsContext;
28use Drupal\ui_patterns\SourcePluginManager;
29
30/**
31 * Defines the display builder instance entity class.
32 */
33#[ContentEntityType(
34  id: 'display_builder_instance',
35  label: new TranslatableMarkup('Display Builder instance'),
36  label_collection: new TranslatableMarkup('Display builder instances'),
37  label_singular: new TranslatableMarkup('display builder instance'),
38  label_plural: new TranslatableMarkup('display builder instances'),
39  entity_keys: [
40    'id' => 'id',
41  ],
42  handlers: [
43    'access' => InstanceAccessControlHandler::class,
44    'storage' => InstanceStorage::class,
45    // Managed by display_builder_ui.
46    'list_builder' => InstanceListBuilder::class,
47  ],
48  links: [
49    // Managed by display_builder_ui.
50    'collection' => '/admin/structure/display-builder/instances',
51  ],
52  label_count: [
53    'singular' => '@count instance',
54    'plural' => '@count instances',
55  ],
56)]
57class Instance extends ContentEntityBase implements InstanceInterface {
58
59  private const MAX_HISTORY = 10;
60
61  /**
62   * Current user.
63   */
64  public AccountInterface $currentUser;
65
66  /**
67   * Path index.
68   *
69   * A mapping where each key is an slot source node ID and each value has
70   * two properties:
71   * - path: the path
72   * - parent: the node ID of the parent. This is necessary because not every
73   *   SourceWithSlotsInterface implementations has the same "deepness". For
74   *   example, ComponentSource has 4 levels (component, slots, slot_id,
75   *   'sources), LayoutSource has 2 levels (regions, slot_id), etc.
76   */
77  protected array $pathIndex = [];
78
79  /**
80   * Entity type manager.
81   */
82  protected EntityTypeManagerInterface $entityTypeManager;
83
84  /**
85   * Sample entity generator.
86   */
87  protected SampleEntityGeneratorInterface $sampleEntityGenerator;
88
89  /**
90   * Slot source proxy.
91   */
92  protected SlotSourceProxy $slotSourceProxy;
93
94  /**
95   * Source plugin manager.
96   */
97  protected SourcePluginManager $sourceManager;
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    $tree = new SourceTree($present->getData() ?? []);
188    $indexed = $tree->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\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 = new SourceTree($this->getCurrentState());
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    // Get friendly label to display in log instead of ids.
234    $labelWithSummary = $this->slotSourceProxy()->getLabelWithSummary($data, $this->getContexts());
235
236    $log = new FormattableMarkup('%node @thingy has been moved to root', [
237      '%node' => $labelWithSummary['summary'],
238      '@thingy' => $data['source_id'],
239    ]);
240    $this->setNewPresent($tree->getTree(), $log, TRUE, FALSE);
241
242    return TRUE;
243  }
244
245  /**
246   * {@inheritdoc}
247   */
248  public function moveToSlot(string $node_id, string $parent_id, string $slot_id, int $position): bool {
249    $tree = new SourceTree($this->getCurrentState());
250    $data = $tree->getNodeData($node_id);
251
252    if (!$data) {
253      return FALSE;
254    }
255
256    if (!$tree->moveToSlot($node_id, $parent_id, $slot_id, $position)) {
257      return FALSE;
258    }
259
260    // Get friendly label to display in log instead of ids.
261    $labelWithSummary = $this->slotSourceProxy()->getLabelWithSummary($data, $this->getContexts());
262    $labelWithSummaryParent = $this->slotSourceProxy()->getLabelWithSummary($tree->getNodeData($parent_id));
263
264    $log = new FormattableMarkup("%node @thingy has been moved to %parent's @slot_id", [
265      '%node' => $labelWithSummary['summary'],
266      '@thingy' => $data['source_id'],
267      '%parent' => $labelWithSummaryParent['summary'],
268      '@slot_id' => $slot_id,
269    ]);
270
271    $this->setNewPresent($tree->getTree(), $log, TRUE, FALSE);
272
273    return TRUE;
274  }
275
276  /**
277   * {@inheritdoc}
278   */
279  public function attachToRoot(int $position, string $source_id, array $data, array $third_party_settings = []): string {
280    $tree = new SourceTree($this->getCurrentState());
281    $node_id = $tree->attachToRoot($position, $source_id, $data);
282
283    if ($third_party_settings) {
284      foreach ($third_party_settings as $island_id => $settings) {
285        $tree->setThirdPartySettings($node_id, $island_id, $settings);
286      }
287    }
288
289    $new_data = $tree->getNode($node_id);
290
291    // Get friendly label to display in log instead of ids.
292    $labelWithSummary = $this->slotSourceProxy()->getLabelWithSummary($new_data, $this->getContexts() ?? []);
293
294    $log = new FormattableMarkup('%node @source_id has been attached to root', [
295      '%node' => $labelWithSummary['summary'],
296      '@source_id' => $source_id,
297    ]);
298    $this->setNewPresent($tree->getTree(), $log, FALSE, FALSE);
299
300    return $node_id;
301  }
302
303  /**
304   * {@inheritdoc}
305   */
306  public function attachToSlot(string $parent_id, string $slot_id, int $position, string $source_id, array $data, array $third_party_settings = []): string {
307    $tree = new SourceTree($this->getCurrentState());
308    $node_id = $tree->attachToSlot($parent_id, $slot_id, $position, $source_id, $data);
309
310    if (!$node_id) {
311      throw new \Exception('Parent or slot not found');
312    }
313
314    if ($third_party_settings) {
315      foreach ($third_party_settings as $island_id => $settings) {
316        $tree->setThirdPartySettings($node_id, $island_id, $settings);
317      }
318    }
319
320    $new_data = $tree->getNode($node_id);
321
322    // Get friendly label to display in log instead of ids.
323    $labelWithSummary = $this->slotSourceProxy()->getLabelWithSummary($new_data, $this->getContexts() ?? []);
324    $labelWithSummaryParent = $this->slotSourceProxy()->getLabelWithSummary($tree->getNode($parent_id));
325
326    $log = new FormattableMarkup("%node @source_id has been attached to %parent's @slot_id", [
327      '%node' => $labelWithSummary['summary'],
328      '@source_id' => $source_id,
329      '%parent' => $labelWithSummaryParent['summary'],
330      '@slot_id' => $slot_id,
331    ]);
332    $this->setNewPresent($tree->getTree(), $log, TRUE, FALSE);
333
334    return $node_id;
335  }
336
337  /**
338   * {@inheritdoc}
339   */
340  public function getNode(string $node_id): array {
341    $root = $this->getCurrentState();
342    $path = $this->getPath($node_id);
343    $value = NestedArray::getValue($root, $path);
344
345    return $value ?? [];
346  }
347
348  /**
349   * {@inheritdoc}
350   */
351  public function getParentId(string $node_id): string {
352    return $this->getPathIndex()[$node_id]['parent'] ?? '';
353  }
354
355  /**
356   * {@inheritdoc}
357   */
358  public function setSource(string $node_id, string $source_id, array $data): void {
359    $tree = new SourceTree($this->getCurrentState());
360
361    if (!$tree->setSource($node_id, $source_id, $data)) {
362      throw new \Exception('Node ID mismatch');
363    }
364
365    // Get friendly label to display in log instead of ids.
366    $labelWithSummary = $this->slotSourceProxy()->getLabelWithSummary($tree->getNodeData($node_id), $this->getContexts());
367
368    $log = new FormattableMarkup('%source has been updated', [
369      '%source' => $labelWithSummary['summary'],
370    ]);
371    $this->setNewPresent($tree->getTree(), $log, TRUE, FALSE);
372  }
373
374  /**
375   * {@inheritdoc}
376   */
377  public function setThirdPartySettings(string $node_id, string $island_id, array $data): void {
378    $tree = new SourceTree($this->getCurrentState());
379
380    if (!$tree->setThirdPartySettings($node_id, $island_id, $data)) {
381      return;
382    }
383
384    // Get friendly label to display in log instead of ids.
385    $labelWithSummary = $this->slotSourceProxy()->getLabelWithSummary($tree->getNodeData($node_id), $this->getContexts());
386
387    $log = new FormattableMarkup('%source has been updated by @island_id', [
388      '%source' => $labelWithSummary['summary'],
389      '@island_id' => $island_id,
390    ]);
391    $this->setNewPresent($tree->getTree(), $log, TRUE, FALSE);
392  }
393
394  /**
395   * {@inheritdoc}
396   */
397  public function remove(string $node_id): void {
398    $tree = new SourceTree($this->getCurrentState());
399    $data = $tree->getNodeData($node_id);
400
401    if (!$data) {
402      return;
403    }
404    $parent_id = $tree->getParentId($node_id);
405
406    $contexts = $this->getContexts() ?? [];
407
408    // Get friendly label to display in log instead of ids.
409    $labelWithSummary = $this->slotSourceProxy()->getLabelWithSummary($data, $contexts);
410    $labelWithSummaryParent = empty($parent_id) ? ['summary' => 'root'] : $this->slotSourceProxy()->getLabelWithSummary($tree->getNodeData($parent_id), $contexts);
411
412    $tree->remove($node_id);
413
414    $log = new FormattableMarkup('%node has been removed from %parent', [
415      '%node' => $labelWithSummary['summary'],
416      '%parent' => $labelWithSummaryParent['summary'],
417    ]);
418    $this->setNewPresent($tree->getTree(), $log, FALSE, FALSE);
419  }
420
421  /**
422   * {@inheritdoc}
423   */
424  public function getContexts(): ?array {
425    if ($this->get('contexts')->isEmpty()) {
426      return [];
427    }
428
429    return $this->refreshContexts($this->get('contexts')->first()->getValue());
430  }
431
432  /**
433   * {@inheritdoc}
434   */
435  public function setSave(array $save_data): void {
436    $tree = new SourceTree($save_data);
437    $indexed = $tree->getTree();
438    $hash = self::getUniqId($indexed);
439    $this->set('save', ['data' => $indexed, 'hash' => $hash, 'log' => NULL, 'time' => \time(), 'user' => NULL]);
440  }
441
442  /**
443   * {@inheritdoc}
444   *
445   * @see \Drupal\display_builder\HistoryInterface
446   */
447  public function getCurrentState(): array {
448    return $this->getCurrent()?->getData() ?? [];
449  }
450
451  /**
452   * {@inheritdoc}
453   */
454  public function restore(): void {
455    /** @var \Drupal\display_builder\Plugin\Field\FieldType\HistoryStep|null $first */
456    $first = $this->get('save')->first();
457    $this->setNewPresent($first->getData(), 'Back to saved data.');
458  }
459
460  /**
461   * {@inheritdoc}
462   *
463   * @see \Drupal\display_builder\HistoryInterface
464   */
465  public function undo(): void {
466    $past = $this->get('past');
467
468    if ($past->isEmpty()) {
469      return;
470    }
471
472    $present_values = $this->get('present')->getValue();
473    \assert(\array_is_list($present_values));
474
475    // Remove the last element from the past.
476    $past_values = $past->getValue();
477    \assert(\array_is_list($past_values));
478    $last = \array_pop($past_values);
479    \assert(!\array_is_list($last));
480    $this->set('past', $past_values);
481
482    // Set the present to the element we removed in the previous step.
483    $this->set('present', $last);
484    // Insert the old present state at the beginning of the future.
485    $this->set('future', \array_merge($present_values, $this->get('future')->getValue()));
486  }
487
488  /**
489   * {@inheritdoc}
490   *
491   * @see \Drupal\display_builder\HistoryInterface
492   */
493  public function redo(): void {
494    $future = $this->get('future');
495
496    if ($future->isEmpty()) {
497      return;
498    }
499
500    // Remove the first element from the future.
501    $first = $future->first()->getValue();
502    \assert(!\array_is_list($first));
503    $future->removeItem(0);
504    // Insert the old present state at the end of the past.
505    $this->get('past')->appendItem($this->get('present')->first());
506    // Set the present to the element we removed in the previous step.
507    $this->set('present', $first);
508    $this->set('future', $future->getValue());
509  }
510
511  /**
512   * {@inheritdoc}
513   */
514  public function clear(): void {
515    $this->set('past', NULL);
516    $this->set('future', NULL);
517  }
518
519  /**
520   * {@inheritdoc}
521   */
522  public function isHistoryNew(): bool {
523    return $this->present === NULL && empty($this->past) && empty($this->future);
524  }
525
526  /**
527   * {@inheritdoc}
528   *
529   * @see \Drupal\display_builder\HistoryInterface
530   */
531  public function getCountPast(): int {
532    return $this->get('past')->count();
533  }
534
535  /**
536   * {@inheritdoc}
537   *
538   * @see \Drupal\display_builder\HistoryInterface
539   */
540  public function getCountFuture(): int {
541    return $this->get('future')->count();
542  }
543
544  /**
545   * {@inheritdoc}
546   */
547  public function getUsers(): array {
548    $users = [];
549    $steps = \array_merge($this->get('past')->getValue(), $this->get('present')->getValue(), $this->get('future')->getValue());
550
551    foreach ($steps as $step) {
552      if ($step === NULL) {
553        continue;
554      }
555      $user_id = $step['user'];
556
557      if ($user_id !== NULL && (!isset($users[$user_id]) || $step['time'] > $users[$user_id])) {
558        $users[$user_id] = $step['time'];
559      }
560    }
561
562    return $users;
563  }
564
565  /**
566   * {@inheritdoc}
567   */
568  public function canSaveContextsRequirement(?array $contexts = NULL): bool {
569    $contexts ??= $this->getContexts();
570
571    if ($contexts === NULL) {
572      return FALSE;
573    }
574
575    if (!\array_key_exists('context_requirements', $contexts)
576      || !($contexts['context_requirements'] instanceof RequirementsContext)) {
577      return FALSE;
578    }
579
580    return TRUE;
581  }
582
583  /**
584   * {@inheritdoc}
585   */
586  public function hasSaveContextsRequirement(string $key, array $contexts = []): bool {
587    $contexts = empty($contexts) ? $this->getContexts() : $contexts;
588    // Some strange edge cases where context is null.
589    $contexts ??= [];
590
591    if (!\array_key_exists('context_requirements', $contexts)
592      || !($contexts['context_requirements'] instanceof RequirementsContext)
593      || !$contexts['context_requirements']->hasValue($key)) {
594      return FALSE;
595    }
596
597    return TRUE;
598  }
599
600  /**
601   * {@inheritdoc}
602   */
603  public function hasSave(): bool {
604    return !$this->get('save')->isEmpty();
605  }
606
607  /**
608   * {@inheritdoc}
609   */
610  public function saveIsCurrent(): bool {
611    $present = $this->get('present');
612    $save = $this->get('save');
613
614    // If either present or save is null, they can't be equal unless both are
615    // null.
616    if ($present->isEmpty() || $save->isEmpty()) {
617      return $present->isEmpty() && $save->isEmpty();
618    }
619
620    /** @var \Drupal\display_builder\Plugin\Field\FieldType\HistoryStep|null $present */
621    $present = $present->first();
622    /** @var \Drupal\display_builder\Plugin\Field\FieldType\HistoryStep|null $save */
623    $save = $save->first();
624
625    return $present->getHash() === $save->getHash();
626  }
627
628  /**
629   * {@inheritdoc}
630   */
631  public function getPathIndex(): array {
632    $tree = new SourceTree($this->getCurrentState());
633
634    return $tree->getPathIndex();
635  }
636
637  /**
638   * {@inheritdoc}
639   *
640   * @see \Drupal\display_builder\HistoryInterface
641   */
642  public function setNewPresent(array $data, FormattableMarkup|string $log_message = '', bool $check_hash = TRUE, bool $index = TRUE): void {
643    if ($index) {
644      $tree = new SourceTree($data);
645      $data = $tree->getTree();
646    }
647    $hash = self::getUniqId($data);
648
649    if (!$this->get('present')->isEmpty()) {
650      /** @var \Drupal\display_builder\Plugin\Field\FieldType\HistoryStep|null $present */
651      $present = $this->get('present')->first();
652
653      // Check if this present is the same to avoid duplicates, for example move
654      // to the same place.
655      if ($check_hash && $hash === $present->getHash()) {
656        return;
657      }
658
659      // 1. Insert the present at the end of the past.
660      // If it's the very first action, we want a NULL in the past to be able to
661      // undo to initial empty state.
662      $this->get('past')->appendItem($present->getValue());
663    }
664
665    // Keep only the last x history.
666    if ($this->get('past')->count() > self::MAX_HISTORY) {
667      $this->get('past')->removeItem(0);
668    }
669
670    // 2. Set the present to the new state.
671    $this->set('present', [
672      'data' => $data,
673      'hash' => $hash,
674      'log' => $log_message,
675      'time' => \time(),
676      'user' => (int) $this->currentUser()->id(),
677    ]);
678
679    // 3. Clear the future.
680    $this->set('future', []);
681  }
682
683  /**
684   * {@inheritdoc}
685   *
686   * @see \Drupal\display_builder\HistoryInterface
687   */
688  public function getCurrent(): ?HistoryStep {
689    /** @var \Drupal\display_builder\Plugin\Field\FieldType\HistoryStep|null $step */
690    $step = $this->get('present')->first();
691
692    return $step;
693  }
694
695  /**
696   * {@inheritdoc}
697   */
698  public static function getUniqId(array $data): int {
699    return \crc32((string) \serialize($data));
700  }
701
702  /**
703   * Sample entity generator.
704   */
705  private function sampleEntityGenerator(): SampleEntityGeneratorInterface {
706    return $this->sampleEntityGenerator ??= \Drupal::service('ui_patterns.sample_entity_generator');
707  }
708
709  /**
710   * Slot source proxy.
711   */
712  private function slotSourceProxy(): SlotSourceProxy {
713    return $this->slotSourceProxy ??= \Drupal::service('display_builder.slot_sources_proxy');
714  }
715
716  /**
717   * Slot source proxy.
718   */
719  private function currentUser(): AccountInterface {
720    return $this->currentUser ??= \Drupal::service('current_user');
721  }
722
723  /**
724   * Refresh contexts after loaded from storage.
725   *
726   * @param \Drupal\Core\Plugin\Context\ContextInterface[] $contexts
727   *   The contexts.
728   *
729   * @throws \Drupal\Component\Plugin\Exception\ContextException
730   * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
731   * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
732   *
733   * @return array
734   *   The refreshed contexts or NULL if no context.
735   */
736  private function refreshContexts(array $contexts): array {
737    foreach ($contexts as &$context) {
738      if ($context instanceof EntityContext) {
739        // @todo We should use cache entries here
740        // with the corresponding cache contexts in it.
741        // This may avoid some unnecessary entity loads or generation.
742        $entity = $context->getContextValue();
743
744        // Check if sample entity.
745        if ($entity->id()) {
746          $entity = $this->entityTypeManager()->getStorage($entity->getEntityTypeId())->load($entity->id());
747        }
748        else {
749          $entity = $this->sampleEntityGenerator()->get($entity->getEntityTypeId(), $entity->bundle());
750        }
751
752        // Edge case when the parent entity is deleted but not the builder
753        // instance.
754        if (!$entity) {
755          return $contexts;
756        }
757        $context = (\get_class($context))::fromEntity($entity);
758      }
759    }
760
761    return $contexts;
762  }
763
764  /**
765   * Get the path to an source.
766   *
767   * @param string $node_id
768   *   The node id of the source.
769   *
770   * @return array
771   *   The path, one array item by level.
772   */
773  private function getPath(string $node_id): array {
774    return $this->getPathIndex()[$node_id]['path'] ?? [];
775  }
776
777}

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
279  public function attachToRoot(int $position, string $source_id, array $data, array $third_party_settings = []): string {
280    $tree = new SourceTree($this->getCurrentState());
281    $node_id = $tree->attachToRoot($position, $source_id, $data);
282
283    if ($third_party_settings) {
 
284      foreach ($third_party_settings as $island_id => $settings) {
 
284      foreach ($third_party_settings as $island_id => $settings) {
 
284      foreach ($third_party_settings as $island_id => $settings) {
 
284      foreach ($third_party_settings as $island_id => $settings) {
 
284      foreach ($third_party_settings as $island_id => $settings) {
285        $tree->setThirdPartySettings($node_id, $island_id, $settings);
286      }
287    }
288
289    $new_data = $tree->getNode($node_id);
 
289    $new_data = $tree->getNode($node_id);
290
291    // Get friendly label to display in log instead of ids.
292    $labelWithSummary = $this->slotSourceProxy()->getLabelWithSummary($new_data, $this->getContexts() ?? []);
293
294    $log = new FormattableMarkup('%node @source_id has been attached to root', [
295      '%node' => $labelWithSummary['summary'],
296      '@source_id' => $source_id,
297    ]);
298    $this->setNewPresent($tree->getTree(), $log, FALSE, FALSE);
299
300    return $node_id;
301  }
279  public function attachToRoot(int $position, string $source_id, array $data, array $third_party_settings = []): string {
280    $tree = new SourceTree($this->getCurrentState());
281    $node_id = $tree->attachToRoot($position, $source_id, $data);
282
283    if ($third_party_settings) {
 
284      foreach ($third_party_settings as $island_id => $settings) {
 
284      foreach ($third_party_settings as $island_id => $settings) {
 
284      foreach ($third_party_settings as $island_id => $settings) {
285        $tree->setThirdPartySettings($node_id, $island_id, $settings);
286      }
287    }
288
289    $new_data = $tree->getNode($node_id);
 
289    $new_data = $tree->getNode($node_id);
290
291    // Get friendly label to display in log instead of ids.
292    $labelWithSummary = $this->slotSourceProxy()->getLabelWithSummary($new_data, $this->getContexts() ?? []);
293
294    $log = new FormattableMarkup('%node @source_id has been attached to root', [
295      '%node' => $labelWithSummary['summary'],
296      '@source_id' => $source_id,
297    ]);
298    $this->setNewPresent($tree->getTree(), $log, FALSE, FALSE);
299
300    return $node_id;
301  }
279  public function attachToRoot(int $position, string $source_id, array $data, array $third_party_settings = []): string {
280    $tree = new SourceTree($this->getCurrentState());
281    $node_id = $tree->attachToRoot($position, $source_id, $data);
282
283    if ($third_party_settings) {
 
284      foreach ($third_party_settings as $island_id => $settings) {
 
284      foreach ($third_party_settings as $island_id => $settings) {
285        $tree->setThirdPartySettings($node_id, $island_id, $settings);
286      }
287    }
288
289    $new_data = $tree->getNode($node_id);
 
289    $new_data = $tree->getNode($node_id);
290
291    // Get friendly label to display in log instead of ids.
292    $labelWithSummary = $this->slotSourceProxy()->getLabelWithSummary($new_data, $this->getContexts() ?? []);
293
294    $log = new FormattableMarkup('%node @source_id has been attached to root', [
295      '%node' => $labelWithSummary['summary'],
296      '@source_id' => $source_id,
297    ]);
298    $this->setNewPresent($tree->getTree(), $log, FALSE, FALSE);
299
300    return $node_id;
301  }
279  public function attachToRoot(int $position, string $source_id, array $data, array $third_party_settings = []): string {
280    $tree = new SourceTree($this->getCurrentState());
281    $node_id = $tree->attachToRoot($position, $source_id, $data);
282
283    if ($third_party_settings) {
 
289    $new_data = $tree->getNode($node_id);
290
291    // Get friendly label to display in log instead of ids.
292    $labelWithSummary = $this->slotSourceProxy()->getLabelWithSummary($new_data, $this->getContexts() ?? []);
293
294    $log = new FormattableMarkup('%node @source_id has been attached to root', [
295      '%node' => $labelWithSummary['summary'],
296      '@source_id' => $source_id,
297    ]);
298    $this->setNewPresent($tree->getTree(), $log, FALSE, FALSE);
299
300    return $node_id;
301  }
Instance->attachToSlot
306  public function attachToSlot(string $parent_id, string $slot_id, int $position, string $source_id, array $data, array $third_party_settings = []): string {
307    $tree = new SourceTree($this->getCurrentState());
308    $node_id = $tree->attachToSlot($parent_id, $slot_id, $position, $source_id, $data);
309
310    if (!$node_id) {
 
311      throw new \Exception('Parent or slot not found');
306  public function attachToSlot(string $parent_id, string $slot_id, int $position, string $source_id, array $data, array $third_party_settings = []): string {
307    $tree = new SourceTree($this->getCurrentState());
308    $node_id = $tree->attachToSlot($parent_id, $slot_id, $position, $source_id, $data);
309
310    if (!$node_id) {
 
314    if ($third_party_settings) {
 
315      foreach ($third_party_settings as $island_id => $settings) {
 
315      foreach ($third_party_settings as $island_id => $settings) {
 
315      foreach ($third_party_settings as $island_id => $settings) {
 
315      foreach ($third_party_settings as $island_id => $settings) {
 
315      foreach ($third_party_settings as $island_id => $settings) {
316        $tree->setThirdPartySettings($node_id, $island_id, $settings);
317      }
318    }
319
320    $new_data = $tree->getNode($node_id);
 
320    $new_data = $tree->getNode($node_id);
321
322    // Get friendly label to display in log instead of ids.
323    $labelWithSummary = $this->slotSourceProxy()->getLabelWithSummary($new_data, $this->getContexts() ?? []);
324    $labelWithSummaryParent = $this->slotSourceProxy()->getLabelWithSummary($tree->getNode($parent_id));
325
326    $log = new FormattableMarkup("%node @source_id has been attached to %parent's @slot_id", [
327      '%node' => $labelWithSummary['summary'],
328      '@source_id' => $source_id,
329      '%parent' => $labelWithSummaryParent['summary'],
330      '@slot_id' => $slot_id,
331    ]);
332    $this->setNewPresent($tree->getTree(), $log, TRUE, FALSE);
333
334    return $node_id;
335  }
306  public function attachToSlot(string $parent_id, string $slot_id, int $position, string $source_id, array $data, array $third_party_settings = []): string {
307    $tree = new SourceTree($this->getCurrentState());
308    $node_id = $tree->attachToSlot($parent_id, $slot_id, $position, $source_id, $data);
309
310    if (!$node_id) {
 
314    if ($third_party_settings) {
 
315      foreach ($third_party_settings as $island_id => $settings) {
 
315      foreach ($third_party_settings as $island_id => $settings) {
 
315      foreach ($third_party_settings as $island_id => $settings) {
316        $tree->setThirdPartySettings($node_id, $island_id, $settings);
317      }
318    }
319
320    $new_data = $tree->getNode($node_id);
 
320    $new_data = $tree->getNode($node_id);
321
322    // Get friendly label to display in log instead of ids.
323    $labelWithSummary = $this->slotSourceProxy()->getLabelWithSummary($new_data, $this->getContexts() ?? []);
324    $labelWithSummaryParent = $this->slotSourceProxy()->getLabelWithSummary($tree->getNode($parent_id));
325
326    $log = new FormattableMarkup("%node @source_id has been attached to %parent's @slot_id", [
327      '%node' => $labelWithSummary['summary'],
328      '@source_id' => $source_id,
329      '%parent' => $labelWithSummaryParent['summary'],
330      '@slot_id' => $slot_id,
331    ]);
332    $this->setNewPresent($tree->getTree(), $log, TRUE, FALSE);
333
334    return $node_id;
335  }
306  public function attachToSlot(string $parent_id, string $slot_id, int $position, string $source_id, array $data, array $third_party_settings = []): string {
307    $tree = new SourceTree($this->getCurrentState());
308    $node_id = $tree->attachToSlot($parent_id, $slot_id, $position, $source_id, $data);
309
310    if (!$node_id) {
 
314    if ($third_party_settings) {
 
315      foreach ($third_party_settings as $island_id => $settings) {
 
315      foreach ($third_party_settings as $island_id => $settings) {
316        $tree->setThirdPartySettings($node_id, $island_id, $settings);
317      }
318    }
319
320    $new_data = $tree->getNode($node_id);
 
320    $new_data = $tree->getNode($node_id);
321
322    // Get friendly label to display in log instead of ids.
323    $labelWithSummary = $this->slotSourceProxy()->getLabelWithSummary($new_data, $this->getContexts() ?? []);
324    $labelWithSummaryParent = $this->slotSourceProxy()->getLabelWithSummary($tree->getNode($parent_id));
325
326    $log = new FormattableMarkup("%node @source_id has been attached to %parent's @slot_id", [
327      '%node' => $labelWithSummary['summary'],
328      '@source_id' => $source_id,
329      '%parent' => $labelWithSummaryParent['summary'],
330      '@slot_id' => $slot_id,
331    ]);
332    $this->setNewPresent($tree->getTree(), $log, TRUE, FALSE);
333
334    return $node_id;
335  }
306  public function attachToSlot(string $parent_id, string $slot_id, int $position, string $source_id, array $data, array $third_party_settings = []): string {
307    $tree = new SourceTree($this->getCurrentState());
308    $node_id = $tree->attachToSlot($parent_id, $slot_id, $position, $source_id, $data);
309
310    if (!$node_id) {
 
314    if ($third_party_settings) {
 
320    $new_data = $tree->getNode($node_id);
321
322    // Get friendly label to display in log instead of ids.
323    $labelWithSummary = $this->slotSourceProxy()->getLabelWithSummary($new_data, $this->getContexts() ?? []);
324    $labelWithSummaryParent = $this->slotSourceProxy()->getLabelWithSummary($tree->getNode($parent_id));
325
326    $log = new FormattableMarkup("%node @source_id has been attached to %parent's @slot_id", [
327      '%node' => $labelWithSummary['summary'],
328      '@source_id' => $source_id,
329      '%parent' => $labelWithSummaryParent['summary'],
330      '@slot_id' => $slot_id,
331    ]);
332    $this->setNewPresent($tree->getTree(), $log, TRUE, FALSE);
333
334    return $node_id;
335  }
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->canSaveContextsRequirement
568  public function canSaveContextsRequirement(?array $contexts = NULL): bool {
569    $contexts ??= $this->getContexts();
570
571    if ($contexts === NULL) {
 
572      return FALSE;
568  public function canSaveContextsRequirement(?array $contexts = NULL): bool {
569    $contexts ??= $this->getContexts();
570
571    if ($contexts === NULL) {
 
575    if (!\array_key_exists('context_requirements', $contexts)
 
576      || !($contexts['context_requirements'] instanceof RequirementsContext)) {
 
576      || !($contexts['context_requirements'] instanceof RequirementsContext)) {
 
577      return FALSE;
568  public function canSaveContextsRequirement(?array $contexts = NULL): bool {
569    $contexts ??= $this->getContexts();
570
571    if ($contexts === NULL) {
 
575    if (!\array_key_exists('context_requirements', $contexts)
 
576      || !($contexts['context_requirements'] instanceof RequirementsContext)) {
 
576      || !($contexts['context_requirements'] instanceof RequirementsContext)) {
 
580    return TRUE;
581  }
568  public function canSaveContextsRequirement(?array $contexts = NULL): bool {
569    $contexts ??= $this->getContexts();
570
571    if ($contexts === NULL) {
 
575    if (!\array_key_exists('context_requirements', $contexts)
 
576      || !($contexts['context_requirements'] instanceof RequirementsContext)) {
 
577      return FALSE;
568  public function canSaveContextsRequirement(?array $contexts = NULL): bool {
569    $contexts ??= $this->getContexts();
570
571    if ($contexts === NULL) {
 
575    if (!\array_key_exists('context_requirements', $contexts)
 
576      || !($contexts['context_requirements'] instanceof RequirementsContext)) {
 
580    return TRUE;
581  }
Instance->clear
515    $this->set('past', NULL);
516    $this->set('future', NULL);
517  }
Instance->currentUser
720    return $this->currentUser ??= \Drupal::service('current_user');
721  }
Instance->getContexts
425    if ($this->get('contexts')->isEmpty()) {
 
426      return [];
425    if ($this->get('contexts')->isEmpty()) {
 
429    return $this->refreshContexts($this->get('contexts')->first()->getValue());
430  }
Instance->getCountFuture
541    return $this->get('future')->count();
542  }
Instance->getCountPast
532    return $this->get('past')->count();
533  }
Instance->getCurrent
690    $step = $this->get('present')->first();
691
692    return $step;
693  }
Instance->getCurrentState
448    return $this->getCurrent()?->getData() ?? [];
449  }
Instance->getNode
340  public function getNode(string $node_id): array {
341    $root = $this->getCurrentState();
342    $path = $this->getPath($node_id);
343    $value = NestedArray::getValue($root, $path);
344
345    return $value ?? [];
346  }
Instance->getParentId
351  public function getParentId(string $node_id): string {
352    return $this->getPathIndex()[$node_id]['parent'] ?? '';
353  }
Instance->getPath
773  private function getPath(string $node_id): array {
774    return $this->getPathIndex()[$node_id]['path'] ?? [];
775  }
Instance->getPathIndex
632    $tree = new SourceTree($this->getCurrentState());
633
634    return $tree->getPathIndex();
635  }
Instance->getProfile
204    $profile_id = $this->get('profileId')->getString();
205    /** @var \Drupal\display_builder\ProfileInterface $profile */
206    $profile = $this->entityTypeManager()->getStorage('display_builder_profile')->load($profile_id);
207
208    return $profile;
209  }
Instance->getUniqId
698  public static function getUniqId(array $data): int {
699    return \crc32((string) \serialize($data));
700  }
Instance->getUsers
548    $users = [];
549    $steps = \array_merge($this->get('past')->getValue(), $this->get('present')->getValue(), $this->get('future')->getValue());
550
551    foreach ($steps as $step) {
 
551    foreach ($steps as $step) {
 
552      if ($step === NULL) {
 
553        continue;
 
551    foreach ($steps as $step) {
 
551    foreach ($steps as $step) {
552      if ($step === NULL) {
553        continue;
554      }
555      $user_id = $step['user'];
556
557      if ($user_id !== NULL && (!isset($users[$user_id]) || $step['time'] > $users[$user_id])) {
558        $users[$user_id] = $step['time'];
559      }
560    }
561
562    return $users;
563  }
548    $users = [];
549    $steps = \array_merge($this->get('past')->getValue(), $this->get('present')->getValue(), $this->get('future')->getValue());
550
551    foreach ($steps as $step) {
 
551    foreach ($steps as $step) {
 
552      if ($step === NULL) {
 
555      $user_id = $step['user'];
556
557      if ($user_id !== NULL && (!isset($users[$user_id]) || $step['time'] > $users[$user_id])) {
 
557      if ($user_id !== NULL && (!isset($users[$user_id]) || $step['time'] > $users[$user_id])) {
 
557      if ($user_id !== NULL && (!isset($users[$user_id]) || $step['time'] > $users[$user_id])) {
 
557      if ($user_id !== NULL && (!isset($users[$user_id]) || $step['time'] > $users[$user_id])) {
 
557      if ($user_id !== NULL && (!isset($users[$user_id]) || $step['time'] > $users[$user_id])) {
 
551    foreach ($steps as $step) {
552      if ($step === NULL) {
553        continue;
554      }
555      $user_id = $step['user'];
556
557      if ($user_id !== NULL && (!isset($users[$user_id]) || $step['time'] > $users[$user_id])) {
558        $users[$user_id] = $step['time'];
 
551    foreach ($steps as $step) {
 
551    foreach ($steps as $step) {
 
551    foreach ($steps as $step) {
552      if ($step === NULL) {
553        continue;
554      }
555      $user_id = $step['user'];
556
557      if ($user_id !== NULL && (!isset($users[$user_id]) || $step['time'] > $users[$user_id])) {
558        $users[$user_id] = $step['time'];
559      }
560    }
561
562    return $users;
563  }
548    $users = [];
549    $steps = \array_merge($this->get('past')->getValue(), $this->get('present')->getValue(), $this->get('future')->getValue());
550
551    foreach ($steps as $step) {
 
551    foreach ($steps as $step) {
 
552      if ($step === NULL) {
 
555      $user_id = $step['user'];
556
557      if ($user_id !== NULL && (!isset($users[$user_id]) || $step['time'] > $users[$user_id])) {
 
557      if ($user_id !== NULL && (!isset($users[$user_id]) || $step['time'] > $users[$user_id])) {
 
557      if ($user_id !== NULL && (!isset($users[$user_id]) || $step['time'] > $users[$user_id])) {
 
557      if ($user_id !== NULL && (!isset($users[$user_id]) || $step['time'] > $users[$user_id])) {
 
557      if ($user_id !== NULL && (!isset($users[$user_id]) || $step['time'] > $users[$user_id])) {
 
551    foreach ($steps as $step) {
 
551    foreach ($steps as $step) {
 
551    foreach ($steps as $step) {
552      if ($step === NULL) {
553        continue;
554      }
555      $user_id = $step['user'];
556
557      if ($user_id !== NULL && (!isset($users[$user_id]) || $step['time'] > $users[$user_id])) {
558        $users[$user_id] = $step['time'];
559      }
560    }
561
562    return $users;
563  }
548    $users = [];
549    $steps = \array_merge($this->get('past')->getValue(), $this->get('present')->getValue(), $this->get('future')->getValue());
550
551    foreach ($steps as $step) {
 
551    foreach ($steps as $step) {
 
552      if ($step === NULL) {
 
555      $user_id = $step['user'];
556
557      if ($user_id !== NULL && (!isset($users[$user_id]) || $step['time'] > $users[$user_id])) {
 
557      if ($user_id !== NULL && (!isset($users[$user_id]) || $step['time'] > $users[$user_id])) {
 
557      if ($user_id !== NULL && (!isset($users[$user_id]) || $step['time'] > $users[$user_id])) {
 
557      if ($user_id !== NULL && (!isset($users[$user_id]) || $step['time'] > $users[$user_id])) {
 
551    foreach ($steps as $step) {
552      if ($step === NULL) {
553        continue;
554      }
555      $user_id = $step['user'];
556
557      if ($user_id !== NULL && (!isset($users[$user_id]) || $step['time'] > $users[$user_id])) {
558        $users[$user_id] = $step['time'];
 
551    foreach ($steps as $step) {
 
551    foreach ($steps as $step) {
 
551    foreach ($steps as $step) {
552      if ($step === NULL) {
553        continue;
554      }
555      $user_id = $step['user'];
556
557      if ($user_id !== NULL && (!isset($users[$user_id]) || $step['time'] > $users[$user_id])) {
558        $users[$user_id] = $step['time'];
559      }
560    }
561
562    return $users;
563  }
548    $users = [];
549    $steps = \array_merge($this->get('past')->getValue(), $this->get('present')->getValue(), $this->get('future')->getValue());
550
551    foreach ($steps as $step) {
 
551    foreach ($steps as $step) {
 
552      if ($step === NULL) {
 
555      $user_id = $step['user'];
556
557      if ($user_id !== NULL && (!isset($users[$user_id]) || $step['time'] > $users[$user_id])) {
 
557      if ($user_id !== NULL && (!isset($users[$user_id]) || $step['time'] > $users[$user_id])) {
 
557      if ($user_id !== NULL && (!isset($users[$user_id]) || $step['time'] > $users[$user_id])) {
 
557      if ($user_id !== NULL && (!isset($users[$user_id]) || $step['time'] > $users[$user_id])) {
 
551    foreach ($steps as $step) {
 
551    foreach ($steps as $step) {
 
551    foreach ($steps as $step) {
552      if ($step === NULL) {
553        continue;
554      }
555      $user_id = $step['user'];
556
557      if ($user_id !== NULL && (!isset($users[$user_id]) || $step['time'] > $users[$user_id])) {
558        $users[$user_id] = $step['time'];
559      }
560    }
561
562    return $users;
563  }
548    $users = [];
549    $steps = \array_merge($this->get('past')->getValue(), $this->get('present')->getValue(), $this->get('future')->getValue());
550
551    foreach ($steps as $step) {
 
551    foreach ($steps as $step) {
 
552      if ($step === NULL) {
 
555      $user_id = $step['user'];
556
557      if ($user_id !== NULL && (!isset($users[$user_id]) || $step['time'] > $users[$user_id])) {
 
557      if ($user_id !== NULL && (!isset($users[$user_id]) || $step['time'] > $users[$user_id])) {
 
551    foreach ($steps as $step) {
552      if ($step === NULL) {
553        continue;
554      }
555      $user_id = $step['user'];
556
557      if ($user_id !== NULL && (!isset($users[$user_id]) || $step['time'] > $users[$user_id])) {
558        $users[$user_id] = $step['time'];
 
551    foreach ($steps as $step) {
 
551    foreach ($steps as $step) {
 
551    foreach ($steps as $step) {
552      if ($step === NULL) {
553        continue;
554      }
555      $user_id = $step['user'];
556
557      if ($user_id !== NULL && (!isset($users[$user_id]) || $step['time'] > $users[$user_id])) {
558        $users[$user_id] = $step['time'];
559      }
560    }
561
562    return $users;
563  }
548    $users = [];
549    $steps = \array_merge($this->get('past')->getValue(), $this->get('present')->getValue(), $this->get('future')->getValue());
550
551    foreach ($steps as $step) {
 
551    foreach ($steps as $step) {
 
552      if ($step === NULL) {
 
555      $user_id = $step['user'];
556
557      if ($user_id !== NULL && (!isset($users[$user_id]) || $step['time'] > $users[$user_id])) {
 
557      if ($user_id !== NULL && (!isset($users[$user_id]) || $step['time'] > $users[$user_id])) {
 
551    foreach ($steps as $step) {
 
551    foreach ($steps as $step) {
 
551    foreach ($steps as $step) {
552      if ($step === NULL) {
553        continue;
554      }
555      $user_id = $step['user'];
556
557      if ($user_id !== NULL && (!isset($users[$user_id]) || $step['time'] > $users[$user_id])) {
558        $users[$user_id] = $step['time'];
559      }
560    }
561
562    return $users;
563  }
548    $users = [];
549    $steps = \array_merge($this->get('past')->getValue(), $this->get('present')->getValue(), $this->get('future')->getValue());
550
551    foreach ($steps as $step) {
 
551    foreach ($steps as $step) {
 
551    foreach ($steps as $step) {
552      if ($step === NULL) {
553        continue;
554      }
555      $user_id = $step['user'];
556
557      if ($user_id !== NULL && (!isset($users[$user_id]) || $step['time'] > $users[$user_id])) {
558        $users[$user_id] = $step['time'];
559      }
560    }
561
562    return $users;
563  }
548    $users = [];
549    $steps = \array_merge($this->get('past')->getValue(), $this->get('present')->getValue(), $this->get('future')->getValue());
550
551    foreach ($steps as $step) {
 
551    foreach ($steps as $step) {
552      if ($step === NULL) {
553        continue;
554      }
555      $user_id = $step['user'];
556
557      if ($user_id !== NULL && (!isset($users[$user_id]) || $step['time'] > $users[$user_id])) {
558        $users[$user_id] = $step['time'];
559      }
560    }
561
562    return $users;
563  }
Instance->hasSave
604    return !$this->get('save')->isEmpty();
605  }
Instance->hasSaveContextsRequirement
586  public function hasSaveContextsRequirement(string $key, array $contexts = []): bool {
587    $contexts = empty($contexts) ? $this->getContexts() : $contexts;
 
587    $contexts = empty($contexts) ? $this->getContexts() : $contexts;
 
587    $contexts = empty($contexts) ? $this->getContexts() : $contexts;
588    // Some strange edge cases where context is null.
589    $contexts ??= [];
590
591    if (!\array_key_exists('context_requirements', $contexts)
 
592      || !($contexts['context_requirements'] instanceof RequirementsContext)
 
592      || !($contexts['context_requirements'] instanceof RequirementsContext)
 
593      || !$contexts['context_requirements']->hasValue($key)) {
 
593      || !$contexts['context_requirements']->hasValue($key)) {
 
594      return FALSE;
586  public function hasSaveContextsRequirement(string $key, array $contexts = []): bool {
587    $contexts = empty($contexts) ? $this->getContexts() : $contexts;
 
587    $contexts = empty($contexts) ? $this->getContexts() : $contexts;
 
587    $contexts = empty($contexts) ? $this->getContexts() : $contexts;
588    // Some strange edge cases where context is null.
589    $contexts ??= [];
590
591    if (!\array_key_exists('context_requirements', $contexts)
 
592      || !($contexts['context_requirements'] instanceof RequirementsContext)
 
592      || !($contexts['context_requirements'] instanceof RequirementsContext)
 
593      || !$contexts['context_requirements']->hasValue($key)) {
 
593      || !$contexts['context_requirements']->hasValue($key)) {
 
597    return TRUE;
598  }
586  public function hasSaveContextsRequirement(string $key, array $contexts = []): bool {
587    $contexts = empty($contexts) ? $this->getContexts() : $contexts;
 
587    $contexts = empty($contexts) ? $this->getContexts() : $contexts;
 
587    $contexts = empty($contexts) ? $this->getContexts() : $contexts;
588    // Some strange edge cases where context is null.
589    $contexts ??= [];
590
591    if (!\array_key_exists('context_requirements', $contexts)
 
592      || !($contexts['context_requirements'] instanceof RequirementsContext)
 
592      || !($contexts['context_requirements'] instanceof RequirementsContext)
 
593      || !$contexts['context_requirements']->hasValue($key)) {
 
594      return FALSE;
586  public function hasSaveContextsRequirement(string $key, array $contexts = []): bool {
587    $contexts = empty($contexts) ? $this->getContexts() : $contexts;
 
587    $contexts = empty($contexts) ? $this->getContexts() : $contexts;
 
587    $contexts = empty($contexts) ? $this->getContexts() : $contexts;
588    // Some strange edge cases where context is null.
589    $contexts ??= [];
590
591    if (!\array_key_exists('context_requirements', $contexts)
 
592      || !($contexts['context_requirements'] instanceof RequirementsContext)
 
592      || !($contexts['context_requirements'] instanceof RequirementsContext)
 
593      || !$contexts['context_requirements']->hasValue($key)) {
 
597    return TRUE;
598  }
586  public function hasSaveContextsRequirement(string $key, array $contexts = []): bool {
587    $contexts = empty($contexts) ? $this->getContexts() : $contexts;
 
587    $contexts = empty($contexts) ? $this->getContexts() : $contexts;
 
587    $contexts = empty($contexts) ? $this->getContexts() : $contexts;
588    // Some strange edge cases where context is null.
589    $contexts ??= [];
590
591    if (!\array_key_exists('context_requirements', $contexts)
 
592      || !($contexts['context_requirements'] instanceof RequirementsContext)
 
593      || !$contexts['context_requirements']->hasValue($key)) {
 
593      || !$contexts['context_requirements']->hasValue($key)) {
 
594      return FALSE;
586  public function hasSaveContextsRequirement(string $key, array $contexts = []): bool {
587    $contexts = empty($contexts) ? $this->getContexts() : $contexts;
 
587    $contexts = empty($contexts) ? $this->getContexts() : $contexts;
 
587    $contexts = empty($contexts) ? $this->getContexts() : $contexts;
588    // Some strange edge cases where context is null.
589    $contexts ??= [];
590
591    if (!\array_key_exists('context_requirements', $contexts)
 
592      || !($contexts['context_requirements'] instanceof RequirementsContext)
 
593      || !$contexts['context_requirements']->hasValue($key)) {
 
593      || !$contexts['context_requirements']->hasValue($key)) {
 
597    return TRUE;
598  }
586  public function hasSaveContextsRequirement(string $key, array $contexts = []): bool {
587    $contexts = empty($contexts) ? $this->getContexts() : $contexts;
 
587    $contexts = empty($contexts) ? $this->getContexts() : $contexts;
 
587    $contexts = empty($contexts) ? $this->getContexts() : $contexts;
588    // Some strange edge cases where context is null.
589    $contexts ??= [];
590
591    if (!\array_key_exists('context_requirements', $contexts)
 
592      || !($contexts['context_requirements'] instanceof RequirementsContext)
 
593      || !$contexts['context_requirements']->hasValue($key)) {
 
594      return FALSE;
586  public function hasSaveContextsRequirement(string $key, array $contexts = []): bool {
587    $contexts = empty($contexts) ? $this->getContexts() : $contexts;
 
587    $contexts = empty($contexts) ? $this->getContexts() : $contexts;
 
587    $contexts = empty($contexts) ? $this->getContexts() : $contexts;
588    // Some strange edge cases where context is null.
589    $contexts ??= [];
590
591    if (!\array_key_exists('context_requirements', $contexts)
 
592      || !($contexts['context_requirements'] instanceof RequirementsContext)
 
593      || !$contexts['context_requirements']->hasValue($key)) {
 
597    return TRUE;
598  }
586  public function hasSaveContextsRequirement(string $key, array $contexts = []): bool {
587    $contexts = empty($contexts) ? $this->getContexts() : $contexts;
 
587    $contexts = empty($contexts) ? $this->getContexts() : $contexts;
 
587    $contexts = empty($contexts) ? $this->getContexts() : $contexts;
588    // Some strange edge cases where context is null.
589    $contexts ??= [];
590
591    if (!\array_key_exists('context_requirements', $contexts)
 
592      || !($contexts['context_requirements'] instanceof RequirementsContext)
 
592      || !($contexts['context_requirements'] instanceof RequirementsContext)
 
593      || !$contexts['context_requirements']->hasValue($key)) {
 
593      || !$contexts['context_requirements']->hasValue($key)) {
 
594      return FALSE;
586  public function hasSaveContextsRequirement(string $key, array $contexts = []): bool {
587    $contexts = empty($contexts) ? $this->getContexts() : $contexts;
 
587    $contexts = empty($contexts) ? $this->getContexts() : $contexts;
 
587    $contexts = empty($contexts) ? $this->getContexts() : $contexts;
588    // Some strange edge cases where context is null.
589    $contexts ??= [];
590
591    if (!\array_key_exists('context_requirements', $contexts)
 
592      || !($contexts['context_requirements'] instanceof RequirementsContext)
 
592      || !($contexts['context_requirements'] instanceof RequirementsContext)
 
593      || !$contexts['context_requirements']->hasValue($key)) {
 
593      || !$contexts['context_requirements']->hasValue($key)) {
 
597    return TRUE;
598  }
586  public function hasSaveContextsRequirement(string $key, array $contexts = []): bool {
587    $contexts = empty($contexts) ? $this->getContexts() : $contexts;
 
587    $contexts = empty($contexts) ? $this->getContexts() : $contexts;
 
587    $contexts = empty($contexts) ? $this->getContexts() : $contexts;
588    // Some strange edge cases where context is null.
589    $contexts ??= [];
590
591    if (!\array_key_exists('context_requirements', $contexts)
 
592      || !($contexts['context_requirements'] instanceof RequirementsContext)
 
592      || !($contexts['context_requirements'] instanceof RequirementsContext)
 
593      || !$contexts['context_requirements']->hasValue($key)) {
 
594      return FALSE;
586  public function hasSaveContextsRequirement(string $key, array $contexts = []): bool {
587    $contexts = empty($contexts) ? $this->getContexts() : $contexts;
 
587    $contexts = empty($contexts) ? $this->getContexts() : $contexts;
 
587    $contexts = empty($contexts) ? $this->getContexts() : $contexts;
588    // Some strange edge cases where context is null.
589    $contexts ??= [];
590
591    if (!\array_key_exists('context_requirements', $contexts)
 
592      || !($contexts['context_requirements'] instanceof RequirementsContext)
 
592      || !($contexts['context_requirements'] instanceof RequirementsContext)
 
593      || !$contexts['context_requirements']->hasValue($key)) {
 
597    return TRUE;
598  }
586  public function hasSaveContextsRequirement(string $key, array $contexts = []): bool {
587    $contexts = empty($contexts) ? $this->getContexts() : $contexts;
 
587    $contexts = empty($contexts) ? $this->getContexts() : $contexts;
 
587    $contexts = empty($contexts) ? $this->getContexts() : $contexts;
588    // Some strange edge cases where context is null.
589    $contexts ??= [];
590
591    if (!\array_key_exists('context_requirements', $contexts)
 
592      || !($contexts['context_requirements'] instanceof RequirementsContext)
 
593      || !$contexts['context_requirements']->hasValue($key)) {
 
593      || !$contexts['context_requirements']->hasValue($key)) {
 
594      return FALSE;
586  public function hasSaveContextsRequirement(string $key, array $contexts = []): bool {
587    $contexts = empty($contexts) ? $this->getContexts() : $contexts;
 
587    $contexts = empty($contexts) ? $this->getContexts() : $contexts;
 
587    $contexts = empty($contexts) ? $this->getContexts() : $contexts;
588    // Some strange edge cases where context is null.
589    $contexts ??= [];
590
591    if (!\array_key_exists('context_requirements', $contexts)
 
592      || !($contexts['context_requirements'] instanceof RequirementsContext)
 
593      || !$contexts['context_requirements']->hasValue($key)) {
 
593      || !$contexts['context_requirements']->hasValue($key)) {
 
597    return TRUE;
598  }
586  public function hasSaveContextsRequirement(string $key, array $contexts = []): bool {
587    $contexts = empty($contexts) ? $this->getContexts() : $contexts;
 
587    $contexts = empty($contexts) ? $this->getContexts() : $contexts;
 
587    $contexts = empty($contexts) ? $this->getContexts() : $contexts;
588    // Some strange edge cases where context is null.
589    $contexts ??= [];
590
591    if (!\array_key_exists('context_requirements', $contexts)
 
592      || !($contexts['context_requirements'] instanceof RequirementsContext)
 
593      || !$contexts['context_requirements']->hasValue($key)) {
 
594      return FALSE;
586  public function hasSaveContextsRequirement(string $key, array $contexts = []): bool {
587    $contexts = empty($contexts) ? $this->getContexts() : $contexts;
 
587    $contexts = empty($contexts) ? $this->getContexts() : $contexts;
 
587    $contexts = empty($contexts) ? $this->getContexts() : $contexts;
588    // Some strange edge cases where context is null.
589    $contexts ??= [];
590
591    if (!\array_key_exists('context_requirements', $contexts)
 
592      || !($contexts['context_requirements'] instanceof RequirementsContext)
 
593      || !$contexts['context_requirements']->hasValue($key)) {
 
597    return TRUE;
598  }
Instance->isHistoryNew
523    return $this->present === NULL && empty($this->past) && empty($this->future);
 
523    return $this->present === NULL && empty($this->past) && empty($this->future);
 
523    return $this->present === NULL && empty($this->past) && empty($this->future);
 
523    return $this->present === NULL && empty($this->past) && empty($this->future);
 
523    return $this->present === NULL && empty($this->past) && empty($this->future);
524  }
523    return $this->present === NULL && empty($this->past) && empty($this->future);
 
523    return $this->present === NULL && empty($this->past) && empty($this->future);
 
523    return $this->present === NULL && empty($this->past) && empty($this->future);
 
523    return $this->present === NULL && empty($this->past) && empty($this->future);
524  }
523    return $this->present === NULL && empty($this->past) && empty($this->future);
 
523    return $this->present === NULL && empty($this->past) && empty($this->future);
 
523    return $this->present === NULL && empty($this->past) && empty($this->future);
 
523    return $this->present === NULL && empty($this->past) && empty($this->future);
524  }
523    return $this->present === NULL && empty($this->past) && empty($this->future);
 
523    return $this->present === NULL && empty($this->past) && empty($this->future);
 
523    return $this->present === NULL && empty($this->past) && empty($this->future);
524  }
Instance->isNew
139    return !$this->id();
140  }
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 = new SourceTree($this->getCurrentState());
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 = new SourceTree($this->getCurrentState());
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 = new SourceTree($this->getCurrentState());
223    $data = $tree->getNodeData($node_id);
224
225    if (!$data) {
 
229    if (!$tree->moveToRoot($node_id, $position)) {
 
234    $labelWithSummary = $this->slotSourceProxy()->getLabelWithSummary($data, $this->getContexts());
235
236    $log = new FormattableMarkup('%node @thingy has been moved to root', [
237      '%node' => $labelWithSummary['summary'],
238      '@thingy' => $data['source_id'],
239    ]);
240    $this->setNewPresent($tree->getTree(), $log, TRUE, FALSE);
241
242    return TRUE;
243  }
Instance->moveToSlot
248  public function moveToSlot(string $node_id, string $parent_id, string $slot_id, int $position): bool {
249    $tree = new SourceTree($this->getCurrentState());
250    $data = $tree->getNodeData($node_id);
251
252    if (!$data) {
 
253      return FALSE;
248  public function moveToSlot(string $node_id, string $parent_id, string $slot_id, int $position): bool {
249    $tree = new SourceTree($this->getCurrentState());
250    $data = $tree->getNodeData($node_id);
251
252    if (!$data) {
 
256    if (!$tree->moveToSlot($node_id, $parent_id, $slot_id, $position)) {
 
257      return FALSE;
248  public function moveToSlot(string $node_id, string $parent_id, string $slot_id, int $position): bool {
249    $tree = new SourceTree($this->getCurrentState());
250    $data = $tree->getNodeData($node_id);
251
252    if (!$data) {
 
256    if (!$tree->moveToSlot($node_id, $parent_id, $slot_id, $position)) {
 
261    $labelWithSummary = $this->slotSourceProxy()->getLabelWithSummary($data, $this->getContexts());
262    $labelWithSummaryParent = $this->slotSourceProxy()->getLabelWithSummary($tree->getNodeData($parent_id));
263
264    $log = new FormattableMarkup("%node @thingy has been moved to %parent's @slot_id", [
265      '%node' => $labelWithSummary['summary'],
266      '@thingy' => $data['source_id'],
267      '%parent' => $labelWithSummaryParent['summary'],
268      '@slot_id' => $slot_id,
269    ]);
270
271    $this->setNewPresent($tree->getTree(), $log, TRUE, FALSE);
272
273    return TRUE;
274  }
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    $tree = new SourceTree($present->getData() ?? []);
188    $indexed = $tree->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
494    $future = $this->get('future');
495
496    if ($future->isEmpty()) {
 
497      return;
494    $future = $this->get('future');
495
496    if ($future->isEmpty()) {
 
501    $first = $future->first()->getValue();
502    \assert(!\array_is_list($first));
503    $future->removeItem(0);
504    // Insert the old present state at the end of the past.
505    $this->get('past')->appendItem($this->get('present')->first());
506    // Set the present to the element we removed in the previous step.
507    $this->set('present', $first);
508    $this->set('future', $future->getValue());
509  }
Instance->refreshContexts
736  private function refreshContexts(array $contexts): array {
737    foreach ($contexts as &$context) {
 
737    foreach ($contexts as &$context) {
 
738      if ($context instanceof EntityContext) {
 
742        $entity = $context->getContextValue();
743
744        // Check if sample entity.
745        if ($entity->id()) {
 
745        if ($entity->id()) {
746          $entity = $this->entityTypeManager()->getStorage($entity->getEntityTypeId())->load($entity->id());
 
754        if (!$entity) {
 
755          return $contexts;
736  private function refreshContexts(array $contexts): array {
737    foreach ($contexts as &$context) {
 
737    foreach ($contexts as &$context) {
 
738      if ($context instanceof EntityContext) {
 
742        $entity = $context->getContextValue();
743
744        // Check if sample entity.
745        if ($entity->id()) {
 
745        if ($entity->id()) {
746          $entity = $this->entityTypeManager()->getStorage($entity->getEntityTypeId())->load($entity->id());
 
754        if (!$entity) {
 
737    foreach ($contexts as &$context) {
738      if ($context instanceof EntityContext) {
739        // @todo We should use cache entries here
740        // with the corresponding cache contexts in it.
741        // This may avoid some unnecessary entity loads or generation.
742        $entity = $context->getContextValue();
743
744        // Check if sample entity.
745        if ($entity->id()) {
746          $entity = $this->entityTypeManager()->getStorage($entity->getEntityTypeId())->load($entity->id());
747        }
748        else {
749          $entity = $this->sampleEntityGenerator()->get($entity->getEntityTypeId(), $entity->bundle());
750        }
751
752        // Edge case when the parent entity is deleted but not the builder
753        // instance.
754        if (!$entity) {
755          return $contexts;
756        }
757        $context = (\get_class($context))::fromEntity($entity);
 
737    foreach ($contexts as &$context) {
 
737    foreach ($contexts as &$context) {
 
737    foreach ($contexts as &$context) {
738      if ($context instanceof EntityContext) {
739        // @todo We should use cache entries here
740        // with the corresponding cache contexts in it.
741        // This may avoid some unnecessary entity loads or generation.
742        $entity = $context->getContextValue();
743
744        // Check if sample entity.
745        if ($entity->id()) {
746          $entity = $this->entityTypeManager()->getStorage($entity->getEntityTypeId())->load($entity->id());
747        }
748        else {
749          $entity = $this->sampleEntityGenerator()->get($entity->getEntityTypeId(), $entity->bundle());
750        }
751
752        // Edge case when the parent entity is deleted but not the builder
753        // instance.
754        if (!$entity) {
755          return $contexts;
756        }
757        $context = (\get_class($context))::fromEntity($entity);
758      }
759    }
760
761    return $contexts;
762  }
736  private function refreshContexts(array $contexts): array {
737    foreach ($contexts as &$context) {
 
737    foreach ($contexts as &$context) {
 
738      if ($context instanceof EntityContext) {
 
742        $entity = $context->getContextValue();
743
744        // Check if sample entity.
745        if ($entity->id()) {
 
749          $entity = $this->sampleEntityGenerator()->get($entity->getEntityTypeId(), $entity->bundle());
750        }
751
752        // Edge case when the parent entity is deleted but not the builder
753        // instance.
754        if (!$entity) {
 
754        if (!$entity) {
 
755          return $contexts;
736  private function refreshContexts(array $contexts): array {
737    foreach ($contexts as &$context) {
 
737    foreach ($contexts as &$context) {
 
738      if ($context instanceof EntityContext) {
 
742        $entity = $context->getContextValue();
743
744        // Check if sample entity.
745        if ($entity->id()) {
 
749          $entity = $this->sampleEntityGenerator()->get($entity->getEntityTypeId(), $entity->bundle());
750        }
751
752        // Edge case when the parent entity is deleted but not the builder
753        // instance.
754        if (!$entity) {
 
754        if (!$entity) {
 
737    foreach ($contexts as &$context) {
738      if ($context instanceof EntityContext) {
739        // @todo We should use cache entries here
740        // with the corresponding cache contexts in it.
741        // This may avoid some unnecessary entity loads or generation.
742        $entity = $context->getContextValue();
743
744        // Check if sample entity.
745        if ($entity->id()) {
746          $entity = $this->entityTypeManager()->getStorage($entity->getEntityTypeId())->load($entity->id());
747        }
748        else {
749          $entity = $this->sampleEntityGenerator()->get($entity->getEntityTypeId(), $entity->bundle());
750        }
751
752        // Edge case when the parent entity is deleted but not the builder
753        // instance.
754        if (!$entity) {
755          return $contexts;
756        }
757        $context = (\get_class($context))::fromEntity($entity);
 
737    foreach ($contexts as &$context) {
 
737    foreach ($contexts as &$context) {
 
737    foreach ($contexts as &$context) {
738      if ($context instanceof EntityContext) {
739        // @todo We should use cache entries here
740        // with the corresponding cache contexts in it.
741        // This may avoid some unnecessary entity loads or generation.
742        $entity = $context->getContextValue();
743
744        // Check if sample entity.
745        if ($entity->id()) {
746          $entity = $this->entityTypeManager()->getStorage($entity->getEntityTypeId())->load($entity->id());
747        }
748        else {
749          $entity = $this->sampleEntityGenerator()->get($entity->getEntityTypeId(), $entity->bundle());
750        }
751
752        // Edge case when the parent entity is deleted but not the builder
753        // instance.
754        if (!$entity) {
755          return $contexts;
756        }
757        $context = (\get_class($context))::fromEntity($entity);
758      }
759    }
760
761    return $contexts;
762  }
736  private function refreshContexts(array $contexts): array {
737    foreach ($contexts as &$context) {
 
737    foreach ($contexts as &$context) {
 
738      if ($context instanceof EntityContext) {
 
737    foreach ($contexts as &$context) {
 
737    foreach ($contexts as &$context) {
 
737    foreach ($contexts as &$context) {
738      if ($context instanceof EntityContext) {
739        // @todo We should use cache entries here
740        // with the corresponding cache contexts in it.
741        // This may avoid some unnecessary entity loads or generation.
742        $entity = $context->getContextValue();
743
744        // Check if sample entity.
745        if ($entity->id()) {
746          $entity = $this->entityTypeManager()->getStorage($entity->getEntityTypeId())->load($entity->id());
747        }
748        else {
749          $entity = $this->sampleEntityGenerator()->get($entity->getEntityTypeId(), $entity->bundle());
750        }
751
752        // Edge case when the parent entity is deleted but not the builder
753        // instance.
754        if (!$entity) {
755          return $contexts;
756        }
757        $context = (\get_class($context))::fromEntity($entity);
758      }
759    }
760
761    return $contexts;
762  }
736  private function refreshContexts(array $contexts): array {
737    foreach ($contexts as &$context) {
 
737    foreach ($contexts as &$context) {
 
737    foreach ($contexts as &$context) {
738      if ($context instanceof EntityContext) {
739        // @todo We should use cache entries here
740        // with the corresponding cache contexts in it.
741        // This may avoid some unnecessary entity loads or generation.
742        $entity = $context->getContextValue();
743
744        // Check if sample entity.
745        if ($entity->id()) {
746          $entity = $this->entityTypeManager()->getStorage($entity->getEntityTypeId())->load($entity->id());
747        }
748        else {
749          $entity = $this->sampleEntityGenerator()->get($entity->getEntityTypeId(), $entity->bundle());
750        }
751
752        // Edge case when the parent entity is deleted but not the builder
753        // instance.
754        if (!$entity) {
755          return $contexts;
756        }
757        $context = (\get_class($context))::fromEntity($entity);
758      }
759    }
760
761    return $contexts;
762  }
736  private function refreshContexts(array $contexts): array {
737    foreach ($contexts as &$context) {
 
737    foreach ($contexts as &$context) {
738      if ($context instanceof EntityContext) {
739        // @todo We should use cache entries here
740        // with the corresponding cache contexts in it.
741        // This may avoid some unnecessary entity loads or generation.
742        $entity = $context->getContextValue();
743
744        // Check if sample entity.
745        if ($entity->id()) {
746          $entity = $this->entityTypeManager()->getStorage($entity->getEntityTypeId())->load($entity->id());
747        }
748        else {
749          $entity = $this->sampleEntityGenerator()->get($entity->getEntityTypeId(), $entity->bundle());
750        }
751
752        // Edge case when the parent entity is deleted but not the builder
753        // instance.
754        if (!$entity) {
755          return $contexts;
756        }
757        $context = (\get_class($context))::fromEntity($entity);
758      }
759    }
760
761    return $contexts;
762  }
Instance->remove
397  public function remove(string $node_id): void {
398    $tree = new SourceTree($this->getCurrentState());
399    $data = $tree->getNodeData($node_id);
400
401    if (!$data) {
 
402      return;
397  public function remove(string $node_id): void {
398    $tree = new SourceTree($this->getCurrentState());
399    $data = $tree->getNodeData($node_id);
400
401    if (!$data) {
 
404    $parent_id = $tree->getParentId($node_id);
405
406    $contexts = $this->getContexts() ?? [];
407
408    // Get friendly label to display in log instead of ids.
409    $labelWithSummary = $this->slotSourceProxy()->getLabelWithSummary($data, $contexts);
410    $labelWithSummaryParent = empty($parent_id) ? ['summary' => 'root'] : $this->slotSourceProxy()->getLabelWithSummary($tree->getNodeData($parent_id), $contexts);
 
410    $labelWithSummaryParent = empty($parent_id) ? ['summary' => 'root'] : $this->slotSourceProxy()->getLabelWithSummary($tree->getNodeData($parent_id), $contexts);
 
410    $labelWithSummaryParent = empty($parent_id) ? ['summary' => 'root'] : $this->slotSourceProxy()->getLabelWithSummary($tree->getNodeData($parent_id), $contexts);
411
412    $tree->remove($node_id);
413
414    $log = new FormattableMarkup('%node has been removed from %parent', [
415      '%node' => $labelWithSummary['summary'],
416      '%parent' => $labelWithSummaryParent['summary'],
417    ]);
418    $this->setNewPresent($tree->getTree(), $log, FALSE, FALSE);
419  }
397  public function remove(string $node_id): void {
398    $tree = new SourceTree($this->getCurrentState());
399    $data = $tree->getNodeData($node_id);
400
401    if (!$data) {
 
404    $parent_id = $tree->getParentId($node_id);
405
406    $contexts = $this->getContexts() ?? [];
407
408    // Get friendly label to display in log instead of ids.
409    $labelWithSummary = $this->slotSourceProxy()->getLabelWithSummary($data, $contexts);
410    $labelWithSummaryParent = empty($parent_id) ? ['summary' => 'root'] : $this->slotSourceProxy()->getLabelWithSummary($tree->getNodeData($parent_id), $contexts);
 
410    $labelWithSummaryParent = empty($parent_id) ? ['summary' => 'root'] : $this->slotSourceProxy()->getLabelWithSummary($tree->getNodeData($parent_id), $contexts);
 
410    $labelWithSummaryParent = empty($parent_id) ? ['summary' => 'root'] : $this->slotSourceProxy()->getLabelWithSummary($tree->getNodeData($parent_id), $contexts);
411
412    $tree->remove($node_id);
413
414    $log = new FormattableMarkup('%node has been removed from %parent', [
415      '%node' => $labelWithSummary['summary'],
416      '%parent' => $labelWithSummaryParent['summary'],
417    ]);
418    $this->setNewPresent($tree->getTree(), $log, FALSE, FALSE);
419  }
Instance->restore
456    $first = $this->get('save')->first();
457    $this->setNewPresent($first->getData(), 'Back to saved data.');
458  }
Instance->sampleEntityGenerator
706    return $this->sampleEntityGenerator ??= \Drupal::service('ui_patterns.sample_entity_generator');
707  }
Instance->saveIsCurrent
611    $present = $this->get('present');
612    $save = $this->get('save');
613
614    // If either present or save is null, they can't be equal unless both are
615    // null.
616    if ($present->isEmpty() || $save->isEmpty()) {
 
616    if ($present->isEmpty() || $save->isEmpty()) {
 
616    if ($present->isEmpty() || $save->isEmpty()) {
 
617      return $present->isEmpty() && $save->isEmpty();
 
617      return $present->isEmpty() && $save->isEmpty();
 
617      return $present->isEmpty() && $save->isEmpty();
611    $present = $this->get('present');
612    $save = $this->get('save');
613
614    // If either present or save is null, they can't be equal unless both are
615    // null.
616    if ($present->isEmpty() || $save->isEmpty()) {
 
616    if ($present->isEmpty() || $save->isEmpty()) {
 
616    if ($present->isEmpty() || $save->isEmpty()) {
 
617      return $present->isEmpty() && $save->isEmpty();
 
617      return $present->isEmpty() && $save->isEmpty();
611    $present = $this->get('present');
612    $save = $this->get('save');
613
614    // If either present or save is null, they can't be equal unless both are
615    // null.
616    if ($present->isEmpty() || $save->isEmpty()) {
 
616    if ($present->isEmpty() || $save->isEmpty()) {
 
616    if ($present->isEmpty() || $save->isEmpty()) {
 
621    $present = $present->first();
622    /** @var \Drupal\display_builder\Plugin\Field\FieldType\HistoryStep|null $save */
623    $save = $save->first();
624
625    return $present->getHash() === $save->getHash();
626  }
611    $present = $this->get('present');
612    $save = $this->get('save');
613
614    // If either present or save is null, they can't be equal unless both are
615    // null.
616    if ($present->isEmpty() || $save->isEmpty()) {
 
616    if ($present->isEmpty() || $save->isEmpty()) {
 
617      return $present->isEmpty() && $save->isEmpty();
 
617      return $present->isEmpty() && $save->isEmpty();
 
617      return $present->isEmpty() && $save->isEmpty();
611    $present = $this->get('present');
612    $save = $this->get('save');
613
614    // If either present or save is null, they can't be equal unless both are
615    // null.
616    if ($present->isEmpty() || $save->isEmpty()) {
 
616    if ($present->isEmpty() || $save->isEmpty()) {
 
617      return $present->isEmpty() && $save->isEmpty();
 
617      return $present->isEmpty() && $save->isEmpty();
611    $present = $this->get('present');
612    $save = $this->get('save');
613
614    // If either present or save is null, they can't be equal unless both are
615    // null.
616    if ($present->isEmpty() || $save->isEmpty()) {
 
616    if ($present->isEmpty() || $save->isEmpty()) {
 
621    $present = $present->first();
622    /** @var \Drupal\display_builder\Plugin\Field\FieldType\HistoryStep|null $save */
623    $save = $save->first();
624
625    return $present->getHash() === $save->getHash();
626  }
Instance->setNewPresent
642  public function setNewPresent(array $data, FormattableMarkup|string $log_message = '', bool $check_hash = TRUE, bool $index = TRUE): void {
643    if ($index) {
 
644      $tree = new SourceTree($data);
645      $data = $tree->getTree();
646    }
647    $hash = self::getUniqId($data);
 
647    $hash = self::getUniqId($data);
648
649    if (!$this->get('present')->isEmpty()) {
 
651      $present = $this->get('present')->first();
652
653      // Check if this present is the same to avoid duplicates, for example move
654      // to the same place.
655      if ($check_hash && $hash === $present->getHash()) {
 
655      if ($check_hash && $hash === $present->getHash()) {
 
655      if ($check_hash && $hash === $present->getHash()) {
 
656        return;
642  public function setNewPresent(array $data, FormattableMarkup|string $log_message = '', bool $check_hash = TRUE, bool $index = TRUE): void {
643    if ($index) {
 
644      $tree = new SourceTree($data);
645      $data = $tree->getTree();
646    }
647    $hash = self::getUniqId($data);
 
647    $hash = self::getUniqId($data);
648
649    if (!$this->get('present')->isEmpty()) {
 
651      $present = $this->get('present')->first();
652
653      // Check if this present is the same to avoid duplicates, for example move
654      // to the same place.
655      if ($check_hash && $hash === $present->getHash()) {
 
655      if ($check_hash && $hash === $present->getHash()) {
 
655      if ($check_hash && $hash === $present->getHash()) {
 
662      $this->get('past')->appendItem($present->getValue());
663    }
664
665    // Keep only the last x history.
666    if ($this->get('past')->count() > self::MAX_HISTORY) {
 
666    if ($this->get('past')->count() > self::MAX_HISTORY) {
 
667      $this->get('past')->removeItem(0);
668    }
669
670    // 2. Set the present to the new state.
671    $this->set('present', [
 
671    $this->set('present', [
672      'data' => $data,
673      'hash' => $hash,
674      'log' => $log_message,
675      'time' => \time(),
676      'user' => (int) $this->currentUser()->id(),
677    ]);
678
679    // 3. Clear the future.
680    $this->set('future', []);
681  }
642  public function setNewPresent(array $data, FormattableMarkup|string $log_message = '', bool $check_hash = TRUE, bool $index = TRUE): void {
643    if ($index) {
 
644      $tree = new SourceTree($data);
645      $data = $tree->getTree();
646    }
647    $hash = self::getUniqId($data);
 
647    $hash = self::getUniqId($data);
648
649    if (!$this->get('present')->isEmpty()) {
 
651      $present = $this->get('present')->first();
652
653      // Check if this present is the same to avoid duplicates, for example move
654      // to the same place.
655      if ($check_hash && $hash === $present->getHash()) {
 
655      if ($check_hash && $hash === $present->getHash()) {
 
655      if ($check_hash && $hash === $present->getHash()) {
 
662      $this->get('past')->appendItem($present->getValue());
663    }
664
665    // Keep only the last x history.
666    if ($this->get('past')->count() > self::MAX_HISTORY) {
 
666    if ($this->get('past')->count() > self::MAX_HISTORY) {
 
671    $this->set('present', [
672      'data' => $data,
673      'hash' => $hash,
674      'log' => $log_message,
675      'time' => \time(),
676      'user' => (int) $this->currentUser()->id(),
677    ]);
678
679    // 3. Clear the future.
680    $this->set('future', []);
681  }
642  public function setNewPresent(array $data, FormattableMarkup|string $log_message = '', bool $check_hash = TRUE, bool $index = TRUE): void {
643    if ($index) {
 
644      $tree = new SourceTree($data);
645      $data = $tree->getTree();
646    }
647    $hash = self::getUniqId($data);
 
647    $hash = self::getUniqId($data);
648
649    if (!$this->get('present')->isEmpty()) {
 
651      $present = $this->get('present')->first();
652
653      // Check if this present is the same to avoid duplicates, for example move
654      // to the same place.
655      if ($check_hash && $hash === $present->getHash()) {
 
655      if ($check_hash && $hash === $present->getHash()) {
 
656        return;
642  public function setNewPresent(array $data, FormattableMarkup|string $log_message = '', bool $check_hash = TRUE, bool $index = TRUE): void {
643    if ($index) {
 
644      $tree = new SourceTree($data);
645      $data = $tree->getTree();
646    }
647    $hash = self::getUniqId($data);
 
647    $hash = self::getUniqId($data);
648
649    if (!$this->get('present')->isEmpty()) {
 
651      $present = $this->get('present')->first();
652
653      // Check if this present is the same to avoid duplicates, for example move
654      // to the same place.
655      if ($check_hash && $hash === $present->getHash()) {
 
655      if ($check_hash && $hash === $present->getHash()) {
 
662      $this->get('past')->appendItem($present->getValue());
663    }
664
665    // Keep only the last x history.
666    if ($this->get('past')->count() > self::MAX_HISTORY) {
 
666    if ($this->get('past')->count() > self::MAX_HISTORY) {
 
667      $this->get('past')->removeItem(0);
668    }
669
670    // 2. Set the present to the new state.
671    $this->set('present', [
 
671    $this->set('present', [
672      'data' => $data,
673      'hash' => $hash,
674      'log' => $log_message,
675      'time' => \time(),
676      'user' => (int) $this->currentUser()->id(),
677    ]);
678
679    // 3. Clear the future.
680    $this->set('future', []);
681  }
642  public function setNewPresent(array $data, FormattableMarkup|string $log_message = '', bool $check_hash = TRUE, bool $index = TRUE): void {
643    if ($index) {
 
644      $tree = new SourceTree($data);
645      $data = $tree->getTree();
646    }
647    $hash = self::getUniqId($data);
 
647    $hash = self::getUniqId($data);
648
649    if (!$this->get('present')->isEmpty()) {
 
651      $present = $this->get('present')->first();
652
653      // Check if this present is the same to avoid duplicates, for example move
654      // to the same place.
655      if ($check_hash && $hash === $present->getHash()) {
 
655      if ($check_hash && $hash === $present->getHash()) {
 
662      $this->get('past')->appendItem($present->getValue());
663    }
664
665    // Keep only the last x history.
666    if ($this->get('past')->count() > self::MAX_HISTORY) {
 
666    if ($this->get('past')->count() > self::MAX_HISTORY) {
 
671    $this->set('present', [
672      'data' => $data,
673      'hash' => $hash,
674      'log' => $log_message,
675      'time' => \time(),
676      'user' => (int) $this->currentUser()->id(),
677    ]);
678
679    // 3. Clear the future.
680    $this->set('future', []);
681  }
642  public function setNewPresent(array $data, FormattableMarkup|string $log_message = '', bool $check_hash = TRUE, bool $index = TRUE): void {
643    if ($index) {
 
644      $tree = new SourceTree($data);
645      $data = $tree->getTree();
646    }
647    $hash = self::getUniqId($data);
 
647    $hash = self::getUniqId($data);
648
649    if (!$this->get('present')->isEmpty()) {
 
666    if ($this->get('past')->count() > self::MAX_HISTORY) {
 
667      $this->get('past')->removeItem(0);
668    }
669
670    // 2. Set the present to the new state.
671    $this->set('present', [
 
671    $this->set('present', [
672      'data' => $data,
673      'hash' => $hash,
674      'log' => $log_message,
675      'time' => \time(),
676      'user' => (int) $this->currentUser()->id(),
677    ]);
678
679    // 3. Clear the future.
680    $this->set('future', []);
681  }
642  public function setNewPresent(array $data, FormattableMarkup|string $log_message = '', bool $check_hash = TRUE, bool $index = TRUE): void {
643    if ($index) {
 
644      $tree = new SourceTree($data);
645      $data = $tree->getTree();
646    }
647    $hash = self::getUniqId($data);
 
647    $hash = self::getUniqId($data);
648
649    if (!$this->get('present')->isEmpty()) {
 
666    if ($this->get('past')->count() > self::MAX_HISTORY) {
 
671    $this->set('present', [
672      'data' => $data,
673      'hash' => $hash,
674      'log' => $log_message,
675      'time' => \time(),
676      'user' => (int) $this->currentUser()->id(),
677    ]);
678
679    // 3. Clear the future.
680    $this->set('future', []);
681  }
642  public function setNewPresent(array $data, FormattableMarkup|string $log_message = '', bool $check_hash = TRUE, bool $index = TRUE): void {
643    if ($index) {
 
647    $hash = self::getUniqId($data);
648
649    if (!$this->get('present')->isEmpty()) {
 
651      $present = $this->get('present')->first();
652
653      // Check if this present is the same to avoid duplicates, for example move
654      // to the same place.
655      if ($check_hash && $hash === $present->getHash()) {
 
655      if ($check_hash && $hash === $present->getHash()) {
 
655      if ($check_hash && $hash === $present->getHash()) {
 
656        return;
642  public function setNewPresent(array $data, FormattableMarkup|string $log_message = '', bool $check_hash = TRUE, bool $index = TRUE): void {
643    if ($index) {
 
647    $hash = self::getUniqId($data);
648
649    if (!$this->get('present')->isEmpty()) {
 
651      $present = $this->get('present')->first();
652
653      // Check if this present is the same to avoid duplicates, for example move
654      // to the same place.
655      if ($check_hash && $hash === $present->getHash()) {
 
655      if ($check_hash && $hash === $present->getHash()) {
 
655      if ($check_hash && $hash === $present->getHash()) {
 
662      $this->get('past')->appendItem($present->getValue());
663    }
664
665    // Keep only the last x history.
666    if ($this->get('past')->count() > self::MAX_HISTORY) {
 
666    if ($this->get('past')->count() > self::MAX_HISTORY) {
 
667      $this->get('past')->removeItem(0);
668    }
669
670    // 2. Set the present to the new state.
671    $this->set('present', [
 
671    $this->set('present', [
672      'data' => $data,
673      'hash' => $hash,
674      'log' => $log_message,
675      'time' => \time(),
676      'user' => (int) $this->currentUser()->id(),
677    ]);
678
679    // 3. Clear the future.
680    $this->set('future', []);
681  }
642  public function setNewPresent(array $data, FormattableMarkup|string $log_message = '', bool $check_hash = TRUE, bool $index = TRUE): void {
643    if ($index) {
 
647    $hash = self::getUniqId($data);
648
649    if (!$this->get('present')->isEmpty()) {
 
651      $present = $this->get('present')->first();
652
653      // Check if this present is the same to avoid duplicates, for example move
654      // to the same place.
655      if ($check_hash && $hash === $present->getHash()) {
 
655      if ($check_hash && $hash === $present->getHash()) {
 
655      if ($check_hash && $hash === $present->getHash()) {
 
662      $this->get('past')->appendItem($present->getValue());
663    }
664
665    // Keep only the last x history.
666    if ($this->get('past')->count() > self::MAX_HISTORY) {
 
666    if ($this->get('past')->count() > self::MAX_HISTORY) {
 
671    $this->set('present', [
672      'data' => $data,
673      'hash' => $hash,
674      'log' => $log_message,
675      'time' => \time(),
676      'user' => (int) $this->currentUser()->id(),
677    ]);
678
679    // 3. Clear the future.
680    $this->set('future', []);
681  }
642  public function setNewPresent(array $data, FormattableMarkup|string $log_message = '', bool $check_hash = TRUE, bool $index = TRUE): void {
643    if ($index) {
 
647    $hash = self::getUniqId($data);
648
649    if (!$this->get('present')->isEmpty()) {
 
651      $present = $this->get('present')->first();
652
653      // Check if this present is the same to avoid duplicates, for example move
654      // to the same place.
655      if ($check_hash && $hash === $present->getHash()) {
 
655      if ($check_hash && $hash === $present->getHash()) {
 
656        return;
642  public function setNewPresent(array $data, FormattableMarkup|string $log_message = '', bool $check_hash = TRUE, bool $index = TRUE): void {
643    if ($index) {
 
647    $hash = self::getUniqId($data);
648
649    if (!$this->get('present')->isEmpty()) {
 
651      $present = $this->get('present')->first();
652
653      // Check if this present is the same to avoid duplicates, for example move
654      // to the same place.
655      if ($check_hash && $hash === $present->getHash()) {
 
655      if ($check_hash && $hash === $present->getHash()) {
 
662      $this->get('past')->appendItem($present->getValue());
663    }
664
665    // Keep only the last x history.
666    if ($this->get('past')->count() > self::MAX_HISTORY) {
 
666    if ($this->get('past')->count() > self::MAX_HISTORY) {
 
667      $this->get('past')->removeItem(0);
668    }
669
670    // 2. Set the present to the new state.
671    $this->set('present', [
 
671    $this->set('present', [
672      'data' => $data,
673      'hash' => $hash,
674      'log' => $log_message,
675      'time' => \time(),
676      'user' => (int) $this->currentUser()->id(),
677    ]);
678
679    // 3. Clear the future.
680    $this->set('future', []);
681  }
642  public function setNewPresent(array $data, FormattableMarkup|string $log_message = '', bool $check_hash = TRUE, bool $index = TRUE): void {
643    if ($index) {
 
647    $hash = self::getUniqId($data);
648
649    if (!$this->get('present')->isEmpty()) {
 
651      $present = $this->get('present')->first();
652
653      // Check if this present is the same to avoid duplicates, for example move
654      // to the same place.
655      if ($check_hash && $hash === $present->getHash()) {
 
655      if ($check_hash && $hash === $present->getHash()) {
 
662      $this->get('past')->appendItem($present->getValue());
663    }
664
665    // Keep only the last x history.
666    if ($this->get('past')->count() > self::MAX_HISTORY) {
 
666    if ($this->get('past')->count() > self::MAX_HISTORY) {
 
671    $this->set('present', [
672      'data' => $data,
673      'hash' => $hash,
674      'log' => $log_message,
675      'time' => \time(),
676      'user' => (int) $this->currentUser()->id(),
677    ]);
678
679    // 3. Clear the future.
680    $this->set('future', []);
681  }
642  public function setNewPresent(array $data, FormattableMarkup|string $log_message = '', bool $check_hash = TRUE, bool $index = TRUE): void {
643    if ($index) {
 
647    $hash = self::getUniqId($data);
648
649    if (!$this->get('present')->isEmpty()) {
 
666    if ($this->get('past')->count() > self::MAX_HISTORY) {
 
667      $this->get('past')->removeItem(0);
668    }
669
670    // 2. Set the present to the new state.
671    $this->set('present', [
 
671    $this->set('present', [
672      'data' => $data,
673      'hash' => $hash,
674      'log' => $log_message,
675      'time' => \time(),
676      'user' => (int) $this->currentUser()->id(),
677    ]);
678
679    // 3. Clear the future.
680    $this->set('future', []);
681  }
642  public function setNewPresent(array $data, FormattableMarkup|string $log_message = '', bool $check_hash = TRUE, bool $index = TRUE): void {
643    if ($index) {
 
647    $hash = self::getUniqId($data);
648
649    if (!$this->get('present')->isEmpty()) {
 
666    if ($this->get('past')->count() > self::MAX_HISTORY) {
 
671    $this->set('present', [
672      'data' => $data,
673      'hash' => $hash,
674      'log' => $log_message,
675      'time' => \time(),
676      'user' => (int) $this->currentUser()->id(),
677    ]);
678
679    // 3. Clear the future.
680    $this->set('future', []);
681  }
Instance->setProfile
214  public function setProfile(string $profile_id): void {
215    $this->set('profileId', $profile_id);
216  }
Instance->setSave
435  public function setSave(array $save_data): void {
436    $tree = new SourceTree($save_data);
437    $indexed = $tree->getTree();
438    $hash = self::getUniqId($indexed);
439    $this->set('save', ['data' => $indexed, 'hash' => $hash, 'log' => NULL, 'time' => \time(), 'user' => NULL]);
440  }
Instance->setSource
358  public function setSource(string $node_id, string $source_id, array $data): void {
359    $tree = new SourceTree($this->getCurrentState());
360
361    if (!$tree->setSource($node_id, $source_id, $data)) {
 
362      throw new \Exception('Node ID mismatch');
358  public function setSource(string $node_id, string $source_id, array $data): void {
359    $tree = new SourceTree($this->getCurrentState());
360
361    if (!$tree->setSource($node_id, $source_id, $data)) {
 
366    $labelWithSummary = $this->slotSourceProxy()->getLabelWithSummary($tree->getNodeData($node_id), $this->getContexts());
367
368    $log = new FormattableMarkup('%source has been updated', [
369      '%source' => $labelWithSummary['summary'],
370    ]);
371    $this->setNewPresent($tree->getTree(), $log, TRUE, FALSE);
372  }
Instance->setThirdPartySettings
377  public function setThirdPartySettings(string $node_id, string $island_id, array $data): void {
378    $tree = new SourceTree($this->getCurrentState());
379
380    if (!$tree->setThirdPartySettings($node_id, $island_id, $data)) {
 
381      return;
377  public function setThirdPartySettings(string $node_id, string $island_id, array $data): void {
378    $tree = new SourceTree($this->getCurrentState());
379
380    if (!$tree->setThirdPartySettings($node_id, $island_id, $data)) {
 
385    $labelWithSummary = $this->slotSourceProxy()->getLabelWithSummary($tree->getNodeData($node_id), $this->getContexts());
386
387    $log = new FormattableMarkup('%source has been updated by @island_id', [
388      '%source' => $labelWithSummary['summary'],
389      '@island_id' => $island_id,
390    ]);
391    $this->setNewPresent($tree->getTree(), $log, TRUE, FALSE);
392  }
Instance->slotSourceProxy
713    return $this->slotSourceProxy ??= \Drupal::service('display_builder.slot_sources_proxy');
714  }
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
466    $past = $this->get('past');
467
468    if ($past->isEmpty()) {
 
469      return;
466    $past = $this->get('past');
467
468    if ($past->isEmpty()) {
 
472    $present_values = $this->get('present')->getValue();
473    \assert(\array_is_list($present_values));
474
475    // Remove the last element from the past.
476    $past_values = $past->getValue();
477    \assert(\array_is_list($past_values));
478    $last = \array_pop($past_values);
479    \assert(!\array_is_list($last));
480    $this->set('past', $past_values);
481
482    // Set the present to the element we removed in the previous step.
483    $this->set('present', $last);
484    // Insert the old present state at the beginning of the future.
485    $this->set('future', \array_merge($present_values, $this->get('future')->getValue()));
486  }