diff --git a/apps/cms/config/sync/views.view.media.yml b/apps/cms/config/sync/views.view.media.yml index 0e9642ceb..e948542b6 100644 --- a/apps/cms/config/sync/views.view.media.yml +++ b/apps/cms/config/sync/views.view.media.yml @@ -5,6 +5,7 @@ dependencies: config: - image.style.thumbnail module: + - custom - image - media - user @@ -467,6 +468,56 @@ display: multi_type: separator separator: ', ' field_api_classes: false + custom_usage_count: + id: custom_usage_count + table: media + field: custom_usage_count + relationship: none + group_type: group + admin_label: '' + entity_type: media + plugin_id: custom + label: 'Usage count' + exclude: false + alter: + alter_text: true + text: '' + make_link: false + path: '' + absolute: false + external: false + replace_spaces: false + path_case: none + trim_whitespace: false + alt: '' + rel: '' + link_class: '' + prefix: '' + suffix: '' + target: '' + nl2br: false + max_length: 0 + word_boundary: true + ellipsis: true + more_link: false + more_link_text: '' + more_link_path: '' + strip_tags: false + trim: false + preserve_tags: '' + html: false + element_type: '' + element_class: '' + element_label_type: '' + element_label_class: '' + element_label_colon: true + element_wrapper_type: '' + element_wrapper_class: '' + element_default_classes: true + empty: '' + hide_empty: false + empty_zero: false + hide_alter_empty: false operations: id: operations table: media diff --git a/packages/drupal/custom/custom.module b/packages/drupal/custom/custom.module index 6ae54ee93..94d226152 100644 --- a/packages/drupal/custom/custom.module +++ b/packages/drupal/custom/custom.module @@ -1,6 +1,11 @@ createFileUrl(); } - } catch (\Throwable $e) { + } + catch (\Throwable $e) { \Drupal::logger('custom')->error( 'Error turning media (id: "{mediaId}") route into file url. Error: {error}', [ 'mediaId' => $mediaId, 'error' => ErrorUtil::renderExceptionSafe($e), ] - ); + ); } } ); @@ -96,16 +101,18 @@ function custom_silverback_gutenberg_link_processor_outbound_url_alter( /** * Implements hook_silverback_gutenberg_link_processor_inbound_link_alter(). + * * @param \DOMElement $link * @param \Drupal\silverback_gutenberg\LinkProcessor $linkProcessor * * @return void + * * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException */ function custom_silverback_gutenberg_link_processor_inbound_link_alter( \DOMElement $link, - LinkProcessor $linkProcessor + LinkProcessor $linkProcessor, ) { // For inbound links (when data gets saved), if the link points to a public // file, then we want to replace the href value with "media/uuid/edit" and @@ -117,9 +124,9 @@ function custom_silverback_gutenberg_link_processor_inbound_link_alter( // needed so that the entity usage integration works properly (where the // data-id and data-entity-type attributes are checked). $href = $link->getAttribute('href'); - /* @var \Drupal\Core\StreamWrapper\StreamWrapperManager $wrapperManager */ + /** @var \Drupal\Core\StreamWrapper\StreamWrapperManager $wrapperManager */ $wrapperManager = \Drupal::service('stream_wrapper_manager'); - /* @var \Drupal\Core\StreamWrapper\StreamWrapperInterface[] $visibleWrappers */ + /** @var \Drupal\Core\StreamWrapper\StreamWrapperInterface[] $visibleWrappers */ $visibleWrappers = $wrapperManager->getWrappers(StreamWrapperInterface::VISIBLE); foreach ($visibleWrappers as $scheme => $wrapperInfo) { $wrapper = $wrapperManager->getViaScheme($scheme); @@ -167,6 +174,7 @@ function custom_silverback_gutenberg_link_processor_inbound_link_alter( * Implements hook_form_alter(). * * Using global form_alter as it's to be executed at the very late stage. + * * @see custom_heavy_module_implements_alter */ function custom_form_alter(&$form, FormStateInterface $form_state, $form_id) { @@ -245,13 +253,13 @@ function custom_field_widget_language_select_form_alter(&$element, FormStateInte function custom_form_views_exposed_form_alter(&$form, FormStateInterface $form_state, $form_id) { // Remove all the confusing options from the language list. unset( - // Site's default language + // Site's default language. $form['langcode']['#options']['***LANGUAGE_site_default***'], - // Interface text language selected for page + // Interface text language selected for page. $form['langcode']['#options']['***LANGUAGE_language_interface***'], - // Not specified + // Not specified. $form['langcode']['#options']['und'], - // Not applicable + // Not applicable. $form['langcode']['#options']['zxx'], ); // The `excludeIds` filter contains a list of UUIDs, and this might exceed the @@ -272,7 +280,8 @@ function _custom_key_auth_form_access(UserInterface $user): AccessResult { $access = AccessResult::forbidden(); if (\Drupal::currentUser()->id() == 1) { $access = AccessResult::allowed(); - } else { + } + else { $roleIds = \Drupal::currentUser()->getRoles(); if ($roleIds) { foreach (Role::loadMultiple($roleIds) as $role) { @@ -312,6 +321,15 @@ function custom_views_data_alter(array &$data) { 'click sortable' => TRUE, ], ]; + + $data['media']['custom_usage_count'] = [ + 'title' => t('Usage count'), + 'help' => t('Display entity usage count.'), + 'field' => [ + 'id' => 'custom_usage_count', + 'click sortable' => FALSE, + ], + ]; } /** diff --git a/packages/drupal/custom/src/Plugin/views/field/UsageCount.php b/packages/drupal/custom/src/Plugin/views/field/UsageCount.php new file mode 100644 index 000000000..e87d2d7b3 --- /dev/null +++ b/packages/drupal/custom/src/Plugin/views/field/UsageCount.php @@ -0,0 +1,186 @@ + t('Example'), + * 'help' => t('Custom example field.'), + * 'id' => 'foo_example', + * ]; + * } + * @endcode + */ +final class UsageCount extends FieldPluginBase { + + /** + * Constructs a new UsageCount instance. + */ + public function __construct( + array $configuration, + $plugin_id, + $plugin_definition, + private readonly EntityTypeManagerInterface $entityTypeManager, + private readonly EntityFieldManagerInterface $entityFieldManager, + private readonly EntityUsageInterface $entityUsage, + ) { + parent::__construct($configuration, $plugin_id, $plugin_definition); + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition): self { + return new self( + $configuration, + $plugin_id, + $plugin_definition, + $container->get('entity_type.manager'), + $container->get('entity_field.manager'), + entityUsage: $container->get('entity_usage.usage'), + ); + } + + /** + * {@inheritdoc} + */ + public function query(): void { + // For non-existent columns (i.e. computed fields) this method must be + // empty. + } + + /** + * {@inheritdoc} + */ + public function render(ResultRow $values): string|MarkupInterface { + + $count = 0; + $media = $values->_entity; + if ($media instanceof MediaInterface) { + $language = $media->language(); + $all_usages = $this->entityUsage->listSources($media); + foreach ($all_usages as $source_type => $ids) { + $type_storage = $this->entityTypeManager->getStorage($source_type); + foreach ($ids as $source_id => $records) { + $source_entity = $type_storage->load($source_id); + if (!$source_entity) { + // If for some reason this record is broken, just skip it. + continue; + } + + $link = $this->getSourceEntityLink($source_entity); + // If the label is empty it means this usage shouldn't be shown + // on the UI, just skip this count. + if (empty($link)) { + continue; + } + + $count++; + } + } + + $url = Url::fromUserInput("/{$language->getId()}/media/{$media->id()}/edit/usage", []); + } + + return $count ? Markup::create("{$count}") : 0; + } + + /** + * Retrieve a link to the source entity. + * + * Note that some entities are special-cased, since they don't have canonical + * template and aren't expected to be re-usable. For example, if the entity + * passed in is a paragraph or a block content, the link we produce will point + * to this entity's parent (host) entity instead. + * + * @param \Drupal\Core\Entity\EntityInterface $source_entity + * The source entity. + * @param string|null $text + * (optional) The link text for the anchor tag as a translated string. + * If NULL, it will use the entity's label. Defaults to NULL. + * + * @return \Drupal\Core\Link|string|false + * A link to the entity, or its non-linked label, in case it was impossible + * to correctly build a link. Will return FALSE if this item should not be + * shown on the UI (for example when dealing with an orphan paragraph). + */ + protected function getSourceEntityLink(EntityInterface $source_entity, $text = NULL) { + // Note that $paragraph_entity->label() will return a string of type: + // "{parent label} > {parent field}", which is actually OK for us. + $entity_label = $source_entity->access('view label') ? $source_entity->label() : $this->t('- Restricted access -'); + + $rel = NULL; + if ($source_entity->hasLinkTemplate('revision')) { + $rel = 'revision'; + } + elseif ($source_entity->hasLinkTemplate('canonical')) { + $rel = 'canonical'; + } + + // Block content likely used in Layout Builder inline blocks. + if ($source_entity instanceof BlockContentInterface && !$source_entity->isReusable()) { + $rel = NULL; + } + + $link_text = $text ?: $entity_label; + if ($rel) { + // Prevent 404s by exposing the text unlinked if the user has no access + // to view the entity. + return $source_entity->access('view') ? $source_entity->toLink($link_text, $rel) : $link_text; + } + + // Treat paragraph entities in a special manner. Normal paragraph entities + // only exist in the context of their host (parent) entity. For this reason + // we will use the link to the parent's entity label instead. + /** @var \Drupal\paragraphs\ParagraphInterface $source_entity */ + if ($source_entity->getEntityTypeId() == 'paragraph') { + $parent = $source_entity->getParentEntity(); + if ($parent) { + return $this->getSourceEntityLink($parent, $link_text); + } + } + // Treat block_content entities in a special manner. Block content + // relationships are stored as serialized data on the host entity. This + // makes it difficult to query parent data. Instead we look up relationship + // data which may exist in entity_usage tables. This requires site builders + // to set up entity usage on host-entity-type -> block_content manually. + // @todo this could be made more generic to support other entity types with + // difficult to handle parent -> child relationships. + elseif ($source_entity->getEntityTypeId() === 'block_content') { + $sources = $this->entityUsage->listSources($source_entity, FALSE); + $source = reset($sources); + if ($source !== FALSE) { + $parent = $this->entityTypeManager->getStorage($source['source_type'])->load($source['source_id']); + if ($parent) { + return $this->getSourceEntityLink($parent); + } + } + } + + // As a fallback just return a non-linked label. + return $link_text; + } + +}