Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 116 |
|
0.00% |
0 / 14 |
CRAP | |
0.00% |
0 / 1 |
Collaboration | |
0.00% |
0 / 111 |
|
0.00% |
0 / 14 |
1190 | |
0.00% |
0 / 1 |
create | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
2 | |||
defaultConfiguration | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
2 | |||
buildConfigurationForm | |
0.00% |
0 / 27 |
|
0.00% |
0 / 1 |
20 | |||
configurationSummary | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
12 | |||
build | |
0.00% |
0 / 16 |
|
0.00% |
0 / 1 |
30 | |||
onAttachToRoot | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
onAttachToSlot | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
onMove | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
onHistoryChange | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
onUpdate | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
onDelete | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
removeInactiveUsers | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
12 | |||
buildRenderable | |
0.00% |
0 / 36 |
|
0.00% |
0 / 1 |
90 | |||
rebuild | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
6 |
1 | <?php |
2 | |
3 | declare(strict_types=1); |
4 | |
5 | namespace Drupal\display_builder\Plugin\display_builder\Island; |
6 | |
7 | use Drupal\Core\Datetime\DateFormatterInterface; |
8 | use Drupal\Core\Entity\EntityFieldManagerInterface; |
9 | use Drupal\Core\Form\FormStateInterface; |
10 | use Drupal\Core\Session\AccountInterface; |
11 | use Drupal\Core\StringTranslation\TranslatableMarkup; |
12 | use Drupal\display_builder\Attribute\Island; |
13 | use Drupal\display_builder\InstanceInterface; |
14 | use Drupal\display_builder\IslandConfigurationFormInterface; |
15 | use Drupal\display_builder\IslandConfigurationFormTrait; |
16 | use Drupal\display_builder\IslandPluginBase; |
17 | use Drupal\display_builder\IslandType; |
18 | use Drupal\file\FileInterface; |
19 | use Symfony\Component\DependencyInjection\ContainerInterface; |
20 | |
21 | /** |
22 | * Real-time collaboration island plugin implementation. |
23 | */ |
24 | #[Island( |
25 | id: 'collaboration', |
26 | label: new TranslatableMarkup('Real-time collaboration'), |
27 | description: new TranslatableMarkup('Allow concurrent editing with multiple users.'), |
28 | type: IslandType::Button, |
29 | )] |
30 | class Collaboration extends IslandPluginBase implements IslandConfigurationFormInterface { |
31 | |
32 | use IslandConfigurationFormTrait; |
33 | |
34 | /** |
35 | * Seconds in 15 minutes. |
36 | */ |
37 | private const SECONDS_IN_15_MINUTES = 900; |
38 | |
39 | /** |
40 | * The current user. |
41 | */ |
42 | protected AccountInterface $currentUser; |
43 | |
44 | /** |
45 | * The date formatter. |
46 | */ |
47 | protected DateFormatterInterface $dateFormatter; |
48 | |
49 | /** |
50 | * The entity field manager. |
51 | */ |
52 | protected EntityFieldManagerInterface $entityFieldManager; |
53 | |
54 | /** |
55 | * {@inheritdoc} |
56 | */ |
57 | public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition): static { |
58 | $instance = parent::create($container, $configuration, $plugin_id, $plugin_definition); |
59 | $instance->currentUser = $container->get('current_user'); |
60 | $instance->dateFormatter = $container->get('date.formatter'); |
61 | $instance->entityFieldManager = $container->get('entity_field.manager'); |
62 | |
63 | return $instance; |
64 | } |
65 | |
66 | /** |
67 | * {@inheritdoc} |
68 | */ |
69 | public function defaultConfiguration(): array { |
70 | return [ |
71 | 'image_field' => 'user_picture', |
72 | 'image_style' => 'thumbnail', |
73 | ]; |
74 | } |
75 | |
76 | /** |
77 | * {@inheritdoc} |
78 | */ |
79 | public function buildConfigurationForm(array $form, FormStateInterface $form_state): array { |
80 | $options = [ |
81 | '' => $this->t('- None -'), |
82 | ]; |
83 | $configuration = $this->getConfiguration(); |
84 | |
85 | $fields = $this->entityFieldManager->getFieldDefinitions('user', 'user'); |
86 | |
87 | foreach ($fields as $field_id => $field) { |
88 | if ($field->getType() === 'image') { |
89 | $options[$field_id] = $field->getLabel(); |
90 | } |
91 | } |
92 | $form['image_field'] = [ |
93 | '#title' => $this->t('Image field'), |
94 | '#type' => 'select', |
95 | '#default_value' => $configuration['image_field'], |
96 | '#options' => $options, |
97 | ]; |
98 | |
99 | $styles = $this->entityTypeManager->getStorage('image_style')->loadMultiple(); |
100 | $options = [ |
101 | '' => $this->t('- None -'), |
102 | ]; |
103 | |
104 | foreach ($styles as $name => $style) { |
105 | $options[$name] = $style->label(); |
106 | } |
107 | $form['image_style'] = [ |
108 | '#title' => $this->t('Image style'), |
109 | '#type' => 'select', |
110 | '#default_value' => $configuration['image_style'], |
111 | '#options' => $options, |
112 | ]; |
113 | |
114 | return $form; |
115 | } |
116 | |
117 | /** |
118 | * {@inheritdoc} |
119 | */ |
120 | public function configurationSummary(): array { |
121 | $conf = $this->getConfiguration(); |
122 | $with_picture_text = $conf['image_style'] ? $this->t('With @style picture.', ['@style' => $conf['image_style']]) : $this->t('With picture'); |
123 | |
124 | return [ |
125 | $conf['image_field'] ? $with_picture_text : $this->t('Without picture.'), |
126 | ]; |
127 | } |
128 | |
129 | /** |
130 | * {@inheritdoc} |
131 | */ |
132 | public function build(InstanceInterface $builder, array $data = [], array $options = []): array { |
133 | $builder_id = (string) $builder->id(); |
134 | // @todo pass \Drupal\display_builder\InstanceInterface object in |
135 | // parameters instead of loading again. |
136 | /** @var \Drupal\display_builder\InstanceInterface $builder */ |
137 | $builder = $this->entityTypeManager->getStorage('display_builder_instance')->load($builder_id); |
138 | $this->builder = $builder; |
139 | $users = $builder->getUsers(); |
140 | $current_user = $this->currentUser->id(); |
141 | $users = $this->removeInactiveUsers($users); |
142 | |
143 | // If no users (this situation must not happen), don't show anything. |
144 | if (\count($users) === 0) { |
145 | return []; |
146 | } |
147 | |
148 | if (\array_key_exists($current_user, $users)) { |
149 | // If the only user is the current user, don't show anything. |
150 | if (\count($users) === 1) { |
151 | return []; |
152 | } |
153 | // Move current user at the beginning of the list. |
154 | $users = [$current_user => $users[$current_user]] + $users; |
155 | |
156 | return $this->buildRenderable($users); |
157 | } |
158 | |
159 | // If the only user is not the current user, add they at the start. |
160 | if (\count($users) === 1) { |
161 | $users = [$current_user => NULL] + $users; |
162 | } |
163 | |
164 | return $this->buildRenderable($users); |
165 | } |
166 | |
167 | /** |
168 | * {@inheritdoc} |
169 | */ |
170 | public function onAttachToRoot(string $builder_id, string $instance_id): array { |
171 | return $this->rebuild($builder_id); |
172 | } |
173 | |
174 | /** |
175 | * {@inheritdoc} |
176 | */ |
177 | public function onAttachToSlot(string $builder_id, string $instance_id, string $parent_id): array { |
178 | return $this->rebuild($builder_id); |
179 | } |
180 | |
181 | /** |
182 | * {@inheritdoc} |
183 | */ |
184 | public function onMove(string $builder_id, string $instance_id): array { |
185 | return $this->rebuild($builder_id); |
186 | } |
187 | |
188 | /** |
189 | * {@inheritdoc} |
190 | */ |
191 | public function onHistoryChange(string $builder_id): array { |
192 | return $this->rebuild($builder_id); |
193 | } |
194 | |
195 | /** |
196 | * {@inheritdoc} |
197 | */ |
198 | public function onUpdate(string $builder_id, string $instance_id): array { |
199 | return $this->rebuild($builder_id); |
200 | } |
201 | |
202 | /** |
203 | * {@inheritdoc} |
204 | */ |
205 | public function onDelete(string $builder_id, string $parent_id): array { |
206 | return $this->rebuild($builder_id); |
207 | } |
208 | |
209 | /** |
210 | * Remove inactive users. |
211 | * |
212 | * @param array $users |
213 | * Each key is an User entity ID, each value is a timestamp. |
214 | * |
215 | * @return array |
216 | * Each key is an User entity ID, each value is a timestamp. |
217 | */ |
218 | protected function removeInactiveUsers(array $users): array { |
219 | foreach ($users as $user_id => $time) { |
220 | if (\time() - $time > self::SECONDS_IN_15_MINUTES) { |
221 | unset($users[$user_id]); |
222 | } |
223 | } |
224 | |
225 | return $users; |
226 | } |
227 | |
228 | /** |
229 | * Build renderable. |
230 | * |
231 | * @param array $users |
232 | * Each key is an User entity ID, each value is a timestamp. |
233 | * |
234 | * @return array |
235 | * A renderable array. |
236 | */ |
237 | protected function buildRenderable(array $users): array { |
238 | $avatars = []; |
239 | $configuration = $this->getConfiguration(); |
240 | |
241 | foreach ($users as $user_id => $time) { |
242 | /** @var \Drupal\user\UserInterface $user */ |
243 | $user = $this->entityTypeManager->getStorage('user')->load($user_id); |
244 | |
245 | if (!$user) { |
246 | // For example, if the user was deleted. |
247 | continue; |
248 | } |
249 | $avatar = [ |
250 | '#type' => 'component', |
251 | '#component' => 'display_builder:avatar', |
252 | '#props' => [ |
253 | 'name' => $user->getDisplayName(), |
254 | ], |
255 | '#attributes' => [ |
256 | 'style' => '--size: 38px', |
257 | ], |
258 | ]; |
259 | |
260 | if ($time) { |
261 | // We can't use DateFormatterInterface::formatTimeDiffSince() because |
262 | // the displayed value will become obsolete if the island is not updated |
263 | // for a while. |
264 | $time = $this->dateFormatter->format($time, 'custom', 'G:i'); |
265 | $avatar['#props']['name'] .= ', ' . $this->t('at @time', ['@time' => $time]); |
266 | } |
267 | |
268 | if (isset($configuration['image_field']) && $user->hasField($configuration['image_field'])) { |
269 | $image = $user->get($configuration['image_field'])->entity; |
270 | |
271 | if ($image instanceof FileInterface) { |
272 | $image_style = $configuration['image_style']; |
273 | $style = $this->entityTypeManager->getStorage('image_style')->load($image_style); |
274 | $avatar['#props']['image'] = $style ? $style->buildUri($image->getFileUri()) : $image->getFileUri(); |
275 | } |
276 | } |
277 | $avatars[] = $avatar; |
278 | } |
279 | |
280 | if (\count($avatars) === 0) { |
281 | return []; |
282 | } |
283 | |
284 | return [ |
285 | '#type' => 'html_tag', |
286 | '#tag' => 'span', |
287 | '#attributes' => [ |
288 | 'class' => 'sl-avatar-group', |
289 | ], |
290 | 'avatars' => $avatars, |
291 | ]; |
292 | } |
293 | |
294 | /** |
295 | * Rebuilds the island with the given builder ID. |
296 | * |
297 | * @param string $builder_id |
298 | * The ID of the builder. |
299 | * |
300 | * @return array |
301 | * The rebuilt island. |
302 | */ |
303 | private function rebuild(string $builder_id): array { |
304 | if (!$this->builder) { |
305 | // @todo pass \Drupal\display_builder\InstanceInterface object in |
306 | // parameters instead of loading again. |
307 | /** @var \Drupal\display_builder\InstanceInterface $builder */ |
308 | $builder = $this->entityTypeManager->getStorage('display_builder_instance')->load($builder_id); |
309 | $this->builder = $builder; |
310 | } |
311 | |
312 | return $this->addOutOfBand( |
313 | $this->build($this->builder), |
314 | '#' . $this->getHtmlId($builder_id), |
315 | 'innerHTML' |
316 | ); |
317 | } |
318 | |
319 | } |