Code Coverage |
||||||||||||||||
Lines |
Branches |
Paths |
Functions and Methods |
Classes and Traits |
||||||||||||
| Total | |
91.09% |
225 / 247 |
|
77.30% |
109 / 141 |
|
50.00% |
60 / 120 |
|
70.73% |
29 / 41 |
CRAP | |
0.00% |
0 / 1 |
| Instance | |
91.09% |
225 / 247 |
|
77.30% |
109 / 141 |
|
50.00% |
60 / 120 |
|
73.17% |
30 / 41 |
1033.12 | |
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 | |
84.62% |
11 / 13 |
|
60.00% |
3 / 5 |
|
33.33% |
1 / 3 |
|
0.00% |
0 / 1 |
5.67 | |||
| moveToSlot | |
87.50% |
14 / 16 |
|
60.00% |
3 / 5 |
|
33.33% |
1 / 3 |
|
0.00% |
0 / 1 |
5.67 | |||
| attachToRoot | |
84.62% |
11 / 13 |
|
33.33% |
2 / 6 |
|
25.00% |
1 / 4 |
|
0.00% |
0 / 1 |
6.80 | |||
| attachToSlot | |
88.89% |
16 / 18 |
|
50.00% |
4 / 8 |
|
40.00% |
2 / 5 |
|
0.00% |
0 / 1 |
7.46 | |||
| getNode | |
100.00% |
4 / 4 |
|
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% |
8 / 8 |
|
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% |
14 / 14 |
|
100.00% |
6 / 6 |
|
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
3 | |||
| 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% |
12 / 12 |
|
100.00% |
3 / 3 |
|
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
2 | |||
| redo | |
100.00% |
9 / 9 |
|
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 | |||
| isHistoryNew | |
0.00% |
0 / 1 |
|
0.00% |
0 / 5 |
|
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
12 | |||
| getCountPast | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| getCountFuture | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| getUsers | |
88.89% |
8 / 9 |
|
83.33% |
10 / 12 |
|
11.11% |
1 / 9 |
|
0.00% |
0 / 1 |
31.28 | |||
| canSaveContextsRequirement | |
85.71% |
6 / 7 |
|
85.71% |
6 / 7 |
|
40.00% |
2 / 5 |
|
0.00% |
0 / 1 |
7.46 | |||
| hasSaveContextsRequirement | |
100.00% |
7 / 7 |
|
90.00% |
9 / 10 |
|
18.75% |
3 / 16 |
|
100.00% |
1 / 1 |
18.41 | |||
| hasSave | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| saveIsCurrent | |
100.00% |
7 / 7 |
|
100.00% |
7 / 7 |
|
33.33% |
2 / 6 |
|
100.00% |
1 / 1 |
8.74 | |||
| getPathIndex | |
100.00% |
2 / 2 |
|
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 | |||
| 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 | |||
| 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 | |||
| getPath | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| 1 | <?php |
| 2 | |
| 3 | declare(strict_types=1); |
| 4 | |
| 5 | namespace Drupal\display_builder\Entity; |
| 6 | |
| 7 | use Drupal\Component\Render\FormattableMarkup; |
| 8 | use Drupal\Component\Utility\NestedArray; |
| 9 | use Drupal\Core\Entity\Attribute\ContentEntityType; |
| 10 | use Drupal\Core\Entity\ContentEntityBase; |
| 11 | use Drupal\Core\Entity\EntityStorageInterface; |
| 12 | use Drupal\Core\Entity\EntityTypeInterface; |
| 13 | use Drupal\Core\Entity\EntityTypeManagerInterface; |
| 14 | use Drupal\Core\Field\BaseFieldDefinition; |
| 15 | use Drupal\Core\Plugin\Context\EntityContext; |
| 16 | use Drupal\Core\Session\AccountInterface; |
| 17 | use Drupal\Core\StringTranslation\TranslatableMarkup; |
| 18 | use Drupal\display_builder\InstanceAccessControlHandler; |
| 19 | use Drupal\display_builder\InstanceInterface; |
| 20 | use Drupal\display_builder\InstanceStorage; |
| 21 | use Drupal\display_builder\Plugin\Field\FieldType\HistoryStep; |
| 22 | use Drupal\display_builder\ProfileInterface; |
| 23 | use Drupal\display_builder\SlotSourceProxy; |
| 24 | use Drupal\display_builder\SourceTree; |
| 25 | use Drupal\display_builder_ui\InstanceListBuilder; |
| 26 | use Drupal\ui_patterns\Entity\SampleEntityGeneratorInterface; |
| 27 | use Drupal\ui_patterns\Plugin\Context\RequirementsContext; |
| 28 | use 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 | )] |
| 57 | class 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 | } |
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 | } |
| 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) { |
| 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 | } |
| 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'); |
| 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) { |
| 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 | } |
| 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 | } |
| 568 | public function canSaveContextsRequirement(?array $contexts = NULL): bool { |
| 569 | $contexts ??= $this->getContexts(); |
| 570 | |
| 571 | if ($contexts === NULL) { |
| 572 | return FALSE; |
| 575 | if (!\array_key_exists('context_requirements', $contexts) |
| 576 | || !($contexts['context_requirements'] instanceof RequirementsContext)) { |
| 576 | || !($contexts['context_requirements'] instanceof RequirementsContext)) { |
| 577 | return FALSE; |
| 580 | return TRUE; |
| 581 | } |
| 515 | $this->set('past', NULL); |
| 516 | $this->set('future', NULL); |
| 517 | } |
| 720 | return $this->currentUser ??= \Drupal::service('current_user'); |
| 721 | } |
| 425 | if ($this->get('contexts')->isEmpty()) { |
| 426 | return []; |
| 429 | return $this->refreshContexts($this->get('contexts')->first()->getValue()); |
| 430 | } |
| 541 | return $this->get('future')->count(); |
| 542 | } |
| 532 | return $this->get('past')->count(); |
| 533 | } |
| 690 | $step = $this->get('present')->first(); |
| 691 | |
| 692 | return $step; |
| 693 | } |
| 448 | return $this->getCurrent()?->getData() ?? []; |
| 449 | } |
| 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 | } |
| 351 | public function getParentId(string $node_id): string { |
| 352 | return $this->getPathIndex()[$node_id]['parent'] ?? ''; |
| 353 | } |
| 773 | private function getPath(string $node_id): array { |
| 774 | return $this->getPathIndex()[$node_id]['path'] ?? []; |
| 775 | } |
| 632 | $tree = new SourceTree($this->getCurrentState()); |
| 633 | |
| 634 | return $tree->getPathIndex(); |
| 635 | } |
| 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 | } |
| 698 | public static function getUniqId(array $data): int { |
| 699 | return \crc32((string) \serialize($data)); |
| 700 | } |
| 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; |
| 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) { |
| 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 | } |
| 604 | return !$this->get('save')->isEmpty(); |
| 605 | } |
| 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; |
| 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; |
| 597 | return TRUE; |
| 598 | } |
| 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 | } |
| 139 | return !$this->id(); |
| 140 | } |
| 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 = new SourceTree($this->getCurrentState()); |
| 223 | $data = $tree->getNodeData($node_id); |
| 224 | |
| 225 | if (!$data) { |
| 226 | return FALSE; |
| 229 | if (!$tree->moveToRoot($node_id, $position)) { |
| 230 | return FALSE; |
| 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 | } |
| 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; |
| 256 | if (!$tree->moveToSlot($node_id, $parent_id, $slot_id, $position)) { |
| 257 | return FALSE; |
| 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 | } |
| 181 | public function postCreate(EntityStorageInterface $storage): void { |
| 182 | if ($this->get('present')->isEmpty()) { |
| 183 | return; |
| 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 | } |
| 494 | $future = $this->get('future'); |
| 495 | |
| 496 | if ($future->isEmpty()) { |
| 497 | return; |
| 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 | } |
| 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()); |
| 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; |
| 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) { |
| 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 | } |
| 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; |
| 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); |
| 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 | } |
| 456 | $first = $this->get('save')->first(); |
| 457 | $this->setNewPresent($first->getData(), 'Back to saved data.'); |
| 458 | } |
| 706 | return $this->sampleEntityGenerator ??= \Drupal::service('ui_patterns.sample_entity_generator'); |
| 707 | } |
| 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(); |
| 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 | } |
| 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; |
| 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 | } |
| 214 | public function setProfile(string $profile_id): void { |
| 215 | $this->set('profileId', $profile_id); |
| 216 | } |
| 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 | } |
| 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'); |
| 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 | } |
| 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; |
| 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 | } |
| 713 | return $this->slotSourceProxy ??= \Drupal::service('display_builder.slot_sources_proxy'); |
| 714 | } |
| 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 | } |
| 466 | $past = $this->get('past'); |
| 467 | |
| 468 | if ($past->isEmpty()) { |
| 469 | return; |
| 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 | } |