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