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

Branches

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.

Collaboration->alterRenderable
169  public function alterRenderable(InstanceInterface $instance, array $build): array {
170    $build['#attributes'] = [
171      'hx-ext' => 'sse',
172      'sse-connect' => Url::fromRoute('display_builder.api_sse', ['display_builder_instance' => (string) $instance->id()])->toString(),
173    ];
174    // We don't attach it from the ::build() because the island doesn't
175    // always render something in the toolbar.
176    $build['#attached']['library'][] = 'display_builder/htmx_sse';
177
178    return $build;
179  }
Collaboration->build
136  public function build(InstanceInterface $builder, array $data = [], array $options = []): array {
137    $this->builder = $builder;
138    $users = $builder->getUsers();
139    $current_user = $this->currentUser->id();
140    $users = $this->removeInactiveUsers($users);
141
142    // If no users (this situation must not happen), don't show anything.
143    if (\count($users) === 0) {
144      return [];
147    if (\array_key_exists($current_user, $users)) {
149      if (\count($users) === 1) {
150        return [];
153      $users = [$current_user => $users[$current_user]] + $users;
154
155      return $this->buildRenderable($users);
159    if (\count($users) === 1) {
160      $users = [$current_user => NULL] + $users;
161    }
162
163    return $this->buildRenderable($users);
163    return $this->buildRenderable($users);
164  }
Collaboration->buildConfigurationForm
83  public function buildConfigurationForm(array $form, FormStateInterface $form_state): array {
84    $options = [
85      '' => $this->t('- None -'),
86    ];
87    $configuration = $this->getConfiguration();
88
89    $fields = $this->entityFieldManager->getFieldDefinitions('user', 'user');
90
91    foreach ($fields as $field_id => $field) {
91    foreach ($fields as $field_id => $field) {
91    foreach ($fields as $field_id => $field) {
92      if ($field->getType() === 'image') {
91    foreach ($fields as $field_id => $field) {
92      if ($field->getType() === 'image') {
93        $options[$field_id] = $field->getLabel();
91    foreach ($fields as $field_id => $field) {
91    foreach ($fields as $field_id => $field) {
92      if ($field->getType() === 'image') {
93        $options[$field_id] = $field->getLabel();
94      }
95    }
96    $form['image_field'] = [
97      '#title' => $this->t('Image field'),
98      '#type' => 'select',
99      '#default_value' => $configuration['image_field'],
100      '#options' => $options,
101    ];
102
103    $styles = $this->entityTypeManager->getStorage('image_style')->loadMultiple();
104    $options = [
105      '' => $this->t('- None -'),
106    ];
107
108    foreach ($styles as $name => $style) {
108    foreach ($styles as $name => $style) {
108    foreach ($styles as $name => $style) {
108    foreach ($styles as $name => $style) {
109      $options[$name] = $style->label();
110    }
111    $form['image_style'] = [
112      '#title' => $this->t('Image style'),
113      '#type' => 'select',
114      '#default_value' => $configuration['image_style'],
115      '#options' => $options,
116    ];
117
118    return $form;
119  }
Collaboration->buildRenderable
209  protected function buildRenderable(array $users): array {
210    $avatars = [];
211    $configuration = $this->getConfiguration();
212
213    foreach ($users as $user_id => $time) {
213    foreach ($users as $user_id => $time) {
213    foreach ($users as $user_id => $time) {
214      /** @var \Drupal\user\UserInterface $user */
215      $user = $this->entityTypeManager->getStorage('user')->load($user_id);
216
217      if (!$user) {
219        continue;
222        '#type' => 'component',
223        '#component' => 'display_builder:avatar',
224        '#props' => [
225          'name' => $user->getDisplayName(),
226        ],
227        '#attributes' => [
228          'style' => '--size: 38px',
229        ],
230      ];
231
232      if ($time) {
236        $time = $this->dateFormatter->format($time, 'custom', 'G:i');
237        $avatar['#props']['name'] .= ', ' . $this->t('at @time', ['@time' => $time]);
238      }
239
240      if (isset($configuration['image_field']) && $user->hasField($configuration['image_field'])) {
240      if (isset($configuration['image_field']) && $user->hasField($configuration['image_field'])) {
240      if (isset($configuration['image_field']) && $user->hasField($configuration['image_field'])) {
240      if (isset($configuration['image_field']) && $user->hasField($configuration['image_field'])) {
241        $image = $user->get($configuration['image_field'])->entity;
242
243        if ($image instanceof FileInterface) {
244          $image_style = $configuration['image_style'];
245          $style = $this->entityTypeManager->getStorage('image_style')->load($image_style);
246          $avatar['#props']['image'] = $style ? $style->buildUri($image->getFileUri()) : $image->getFileUri();
246          $avatar['#props']['image'] = $style ? $style->buildUri($image->getFileUri()) : $image->getFileUri();
246          $avatar['#props']['image'] = $style ? $style->buildUri($image->getFileUri()) : $image->getFileUri();
246          $avatar['#props']['image'] = $style ? $style->buildUri($image->getFileUri()) : $image->getFileUri();
247        }
248      }
249      $avatars[] = $avatar;
213    foreach ($users as $user_id => $time) {
214      /** @var \Drupal\user\UserInterface $user */
215      $user = $this->entityTypeManager->getStorage('user')->load($user_id);
216
217      if (!$user) {
218        // For example, if the user was deleted.
219        continue;
220      }
221      $avatar = [
222        '#type' => 'component',
223        '#component' => 'display_builder:avatar',
224        '#props' => [
225          'name' => $user->getDisplayName(),
226        ],
227        '#attributes' => [
228          'style' => '--size: 38px',
229        ],
230      ];
231
232      if ($time) {
233        // We can't use DateFormatterInterface::formatTimeDiffSince() because
234        // the displayed value will become obsolete if the island is not updated
235        // for a while.
236        $time = $this->dateFormatter->format($time, 'custom', 'G:i');
237        $avatar['#props']['name'] .= ', ' . $this->t('at @time', ['@time' => $time]);
238      }
239
240      if (isset($configuration['image_field']) && $user->hasField($configuration['image_field'])) {
241        $image = $user->get($configuration['image_field'])->entity;
242
243        if ($image instanceof FileInterface) {
244          $image_style = $configuration['image_style'];
245          $style = $this->entityTypeManager->getStorage('image_style')->load($image_style);
246          $avatar['#props']['image'] = $style ? $style->buildUri($image->getFileUri()) : $image->getFileUri();
247        }
248      }
249      $avatars[] = $avatar;
213    foreach ($users as $user_id => $time) {
214      /** @var \Drupal\user\UserInterface $user */
215      $user = $this->entityTypeManager->getStorage('user')->load($user_id);
216
217      if (!$user) {
218        // For example, if the user was deleted.
219        continue;
220      }
221      $avatar = [
222        '#type' => 'component',
223        '#component' => 'display_builder:avatar',
224        '#props' => [
225          'name' => $user->getDisplayName(),
226        ],
227        '#attributes' => [
228          'style' => '--size: 38px',
229        ],
230      ];
231
232      if ($time) {
233        // We can't use DateFormatterInterface::formatTimeDiffSince() because
234        // the displayed value will become obsolete if the island is not updated
235        // for a while.
236        $time = $this->dateFormatter->format($time, 'custom', 'G:i');
237        $avatar['#props']['name'] .= ', ' . $this->t('at @time', ['@time' => $time]);
238      }
239
240      if (isset($configuration['image_field']) && $user->hasField($configuration['image_field'])) {
241        $image = $user->get($configuration['image_field'])->entity;
242
243        if ($image instanceof FileInterface) {
244          $image_style = $configuration['image_style'];
245          $style = $this->entityTypeManager->getStorage('image_style')->load($image_style);
246          $avatar['#props']['image'] = $style ? $style->buildUri($image->getFileUri()) : $image->getFileUri();
247        }
248      }
249      $avatars[] = $avatar;
250    }
251
252    if (\count($avatars) === 0) {
253      return [];
257      '#type' => 'html_tag',
258      '#tag' => 'span',
259      '#attributes' => [
260        'class' => 'sl-avatar-group',
261      ],
262      'avatars' => $avatars,
263    ];
264  }
Collaboration->configurationSummary
125    $conf = $this->getConfiguration();
126    $with_picture_text = $conf['image_style'] ? $this->t('With @style picture.', ['@style' => $conf['image_style']]) : $this->t('With picture');
126    $with_picture_text = $conf['image_style'] ? $this->t('With @style picture.', ['@style' => $conf['image_style']]) : $this->t('With picture');
126    $with_picture_text = $conf['image_style'] ? $this->t('With @style picture.', ['@style' => $conf['image_style']]) : $this->t('With picture');
126    $with_picture_text = $conf['image_style'] ? $this->t('With @style picture.', ['@style' => $conf['image_style']]) : $this->t('With picture');
127
128    return [
129      $conf['image_field'] ? $with_picture_text : $this->t('Without picture.'),
129      $conf['image_field'] ? $with_picture_text : $this->t('Without picture.'),
129      $conf['image_field'] ? $with_picture_text : $this->t('Without picture.'),
129      $conf['image_field'] ? $with_picture_text : $this->t('Without picture.'),
130    ];
131  }
Collaboration->create
61  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition): static {
62    $instance = parent::create($container, $configuration, $plugin_id, $plugin_definition);
63    $instance->currentUser = $container->get('current_user');
64    $instance->dateFormatter = $container->get('date.formatter');
65    $instance->entityFieldManager = $container->get('entity_field.manager');
66
67    return $instance;
68  }
Collaboration->defaultConfiguration
75      'image_field' => 'user_picture',
76      'image_style' => 'thumbnail',
77    ];
78  }
Collaboration->removeInactiveUsers
190  protected function removeInactiveUsers(array $users): array {
191    foreach ($users as $user_id => $time) {
191    foreach ($users as $user_id => $time) {
191    foreach ($users as $user_id => $time) {
192      if (\time() - $time > self::SECONDS_IN_15_MINUTES) {
191    foreach ($users as $user_id => $time) {
192      if (\time() - $time > self::SECONDS_IN_15_MINUTES) {
193        unset($users[$user_id]);
191    foreach ($users as $user_id => $time) {
191    foreach ($users as $user_id => $time) {
192      if (\time() - $time > self::SECONDS_IN_15_MINUTES) {
193        unset($users[$user_id]);
194      }
195    }
196
197    return $users;
198  }