Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SLB-457: add usage count field on media list view #345

Open
wants to merge 2 commits into
base: release
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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),
]
);
);
dspachos marked this conversation as resolved.
Show resolved Hide resolved
}
}
);
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,4 +292,14 @@ 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,
],
];

}
186 changes: 186 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,186 @@
<?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.
dspachos marked this conversation as resolved.
Show resolved Hide resolved
* @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 {

$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("<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;
}

}
Loading