Skip to content

Commit

Permalink
feat(slb-457): add usage count field on media list view
Browse files Browse the repository at this point in the history
  • Loading branch information
dspachos committed Sep 18, 2024
1 parent 5a44e5e commit 04b4a5e
Show file tree
Hide file tree
Showing 3 changed files with 267 additions and 11 deletions.
50 changes: 50 additions & 0 deletions apps/cms/config/sync/views.view.media.yml
Original file line number Diff line number Diff line change
Expand Up @@ -518,6 +518,56 @@ display:
empty_zero: false
hide_alter_empty: true
destination: true
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
pager:
type: full
options:
Expand Down
39 changes: 28 additions & 11 deletions packages/drupal/custom/custom.module
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
<?php

/**
* @file
*/

use Drupal\Core\Access\AccessResult;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Form\FormStateInterface;
Expand All @@ -14,8 +18,6 @@ use Drupal\media\Entity\Media;
use Drupal\silverback_gutenberg\LinkProcessor;
use Drupal\user\Entity\Role;
use Drupal\user\UserInterface;
use Drupal\Core\Url;
use Drupal\Core\Link;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;

/**
Expand Down Expand Up @@ -80,14 +82,15 @@ function custom_silverback_gutenberg_link_processor_outbound_url_alter(
$file = File::load($source);
$url = $file->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),
]
);
);
}
}
);
Expand All @@ -96,10 +99,12 @@ 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
*/
Expand All @@ -117,9 +122,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);
Expand Down Expand Up @@ -167,6 +172,7 @@ function custom_silverback_gutenberg_link_processor_inbound_link_alter(
* Implements hook_form_alter().
*
* Executed at the very late stage.
*
* @see custom_heavy_module_implements_alter
*/
function custom_form_alter(&$form, FormStateInterface $form_state, $form_id) {
Expand Down Expand Up @@ -218,13 +224,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
Expand All @@ -245,7 +251,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) {
Expand Down Expand Up @@ -285,6 +292,16 @@ 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,
],
];

}

/**
Expand Down
189 changes: 189 additions & 0 deletions packages/drupal/custom/src/Plugin/views/field/UsageCount.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
<?php

namespace Drupal\custom\Plugin\views\field;

use Drupal\Component\Render\MarkupInterface;
use Drupal\Core\Entity\EntityFieldManagerInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Render\Markup;
use Drupal\Core\Url;
use Drupal\entity_usage\EntityUsageInterface;
use Drupal\media\MediaInterface;
use Drupal\views\Plugin\views\field\FieldPluginBase;
use Drupal\views\ResultRow;
use Symfony\Component\DependencyInjection\ContainerInterface;

/**
* Provides Usage count field handler.
*
* @ViewsField("custom_usage_count")
*
* @DCG
* The plugin needs to be assigned to a specific table column through
* hook_views_data() or hook_views_data_alter().
* Put the following code to custom.views.inc file.
* @code
* function foo_views_data_alter(array &$data): void {
* $data['node']['foo_example']['field'] = [
* 'title' => 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 {
// @todo Modify or replace the rendered value here.
$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) {
// We will show a single row per source entity. If the target is not
// referenced on its default revision on the default language, we will
// just show indicate that in a specific column.
$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 row.
if (empty($link)) {
continue;
}

$count++;
}
}

$url = Url::fromUserInput("/{$language->getId()}/media/{$media->id()}/edit/usage", []);
}

return $count ? Markup::create("<a href='{$url->toString()}'>{$count}</a>") : 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;
}

}

0 comments on commit 04b4a5e

Please sign in to comment.