admin_toolbar_content-1.0.0/src/Plugin/AdminToolbarContent/AdminToolbarContentContentPlugin.php
src/Plugin/AdminToolbarContent/AdminToolbarContentContentPlugin.php
<?php
namespace Drupal\admin_toolbar_content\Plugin\AdminToolbarContent;
use Drupal\admin_toolbar_content\AdminToolbarContentPluginBase;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Menu\MenuLinkDefault;
use Drupal\views\Views;
/**
* An AdminToolbarContentPlugin for altering the system content menu.
*
* @see \Drupal\admin_toolbar_content\Plugin\Derivative\AdminToolbarContentMenuLinks.
*
* @AdminToolbarContentPlugin(
* id = "content",
* name = @Translation("Content"),
* description = @Translation("Alters the system content menu to provide links for each content type."),
* entity_type = "node_type"
* )
*/
class AdminToolbarContentContentPlugin extends AdminToolbarContentPluginBase {
/**
* {@inheritdoc}
*/
public function buildConfigForm(array &$form, FormStateInterface $form_state): array {
$elements = parent::buildConfigForm($form, $form_state);
$elements += $this->buildConfigFormRecentItems($form, $form_state);
$elements['hide_non_content_items'] = [
'#type' => 'checkbox',
'#title' => $this->t('Hide non content items'),
'#description' => $this->t('Hides items under "content" not directly related to content types.'),
'#default_value' => $this->config->get("plugins." . $this->getPluginId() . '.hide_non_content_items') ?? FALSE,
];
$elements['hide_content_type_items'] = [
'#type' => 'checkboxes',
'#title' => $this->t('Hide content type items'),
'#description' => $this->t('If one of these conditions is met, the relevant content type item will be hidden.'),
'#options' => [
'content_view' => $this->t("If the content type is not available in the 'content' view 'type' filter."),
'admin_permissions' => $this->t("If the user doesn't have administrative permissions."),
],
'#default_value' => $this->config->get("plugins." . $this->getPluginId() . '.hide_content_type_items') ?? [],
];
return $elements;
}
/**
* Add config fields to a plugin config form when it is using recent items.
*
* @param array $form
* The complete config form.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The form state.
*
* @return array
* An array of elements for configuring the recent items.
*/
protected function buildConfigFormRecentItems(array &$form, FormStateInterface $form_state): array {
$elements = [];
$elements['recent_items'] = [
'#type' => 'details',
'#title' => $this->t('Recent content'),
];
$elements['recent_items']['number_of_items'] = [
'#type' => 'textfield',
'#title' => $this->t('Number of items'),
'#description' => $this->t('The amount of items to show. Set to 0 or leave empty to disable recent items.'),
'#default_value' => $this->config->get("plugins." . $this->getPluginId() . '.recent_items.number_of_items') ?? 5,
];
$elements['recent_items']['hide_empty_list'] = [
'#type' => 'checkbox',
'#title' => $this->t('Hide empty list'),
'#description' => $this->t('Hide recent content list if non are available.'),
'#default_value' => $this->config->get("plugins." . $this->getPluginId() . '.recent_items.hide_empty_list') ?? FALSE,
];
$recent_items_link_options = [
'default' => $this->t('Edit form'),
'view' => $this->t('View'),
];
// Add Layout Builder option only if layout builder is enabled.
if ($this->moduleHandler->moduleExists('layout_builder')) {
$recent_items_link_options['layout_builder'] = $this->t('Layout Builder');
}
$elements['recent_items']['link'] = [
'#type' => 'radios',
'#title' => $this->t('Link'),
'#description' => $this->t('Choose where recent items should be linked to.'),
'#options' => $recent_items_link_options,
'#default_value' => $this->config->get("plugins." . $this->getPluginId() . '.link') ?? 'default',
];
return $elements;
}
/**
* {@inheritdoc}
*/
public function alterDiscoveredMenuLinks(array &$links): void {
$hide_non_content_items = $this->config->get("plugins." . $this->getPluginId() . '.hide_non_content_items') ?? 0;
if ($hide_non_content_items) {
$parents = ['system.admin_content'];
while (!empty($parents)) {
$removed = [];
foreach ($links as $name => $link) {
if (isset($link['parent']) && in_array($link['parent'], $parents)) {
if (!str_starts_with($name, 'admin_toolbar_content')) {
unset($links[$name]);
$removed[] = $name;
}
}
}
$parents = $removed;
}
}
// Unset the original "add content" menu item and it's children.
// These are replaced with the links from createMenuLinkItems.
unset($links['admin_toolbar_tools.extra_links:node.add']);
$contentTypes = $this->getItems();
foreach ($contentTypes as $contentType) {
unset($links['admin_toolbar_tools.extra_links:node.add.' . $contentType->id()]);
}
}
/**
* {@inheritdoc}
*/
public function needsMenuLinkRebuild(EntityInterface $entity): bool {
return (bool) (
$entity->getEntityTypeId() === 'node' &&
$this->config->get("plugins." . $this->getPluginId() . '.recent_items.number_of_items') ?? 0
);
}
/**
* {@inheritdoc}
*/
public function createMenuLinkItems(): void {
$this->createCollectionLinks('system.admin_content');
$this->createItemLinks('system.admin_content', 'type');
$this->createItemAddLinks('node.add');
$this->createItemRecentContentLinks('node');
}
/**
* {@inheritdoc}
*/
protected function createCollectionLink(array $collection, $route_name, $route_parameters = []): void {
parent::createCollectionLink($collection, $route_name, $route_parameters);
if (isset($this->links[$collection['id']])) {
// Because we don't have a custom root item, we add collections to the
// existing system content menu item.
if ($collection['parent'] == $this->getPluginId()) {
$this->links[$collection['id']]['parent'] = 'system.admin_content';
}
}
}
/**
* {@inheritdoc}
*/
protected function createItemLink(mixed $item, string $route_name, string $route_item_parameter): void {
parent::createItemLink($item, $route_name, $route_item_parameter);
$collection = $this->getItemCollection($item);
if (isset($this->links[$collection['id'] . '.' . $item->id()])) {
// Because we don't have a custom root item, we add items without a
// collection to the existing system content menu item.
if ($collection['id'] == $this->getPluginId()) {
$this->links[$collection['id'] . '.' . $item->id()]['parent'] = 'system.admin_content';
}
}
}
/**
* {@inheritdoc}
*/
protected function filterMenuItem(array &$item, &$parent = NULL): bool {
// Only fetch this once, so stick it in a static variable.
static $view_definition = NULL;
/** @var \Drupal\Core\Url $url */
$url = $item['url'];
if ($url->isRouted() && $url->getRouteName() == 'system.admin_content') {
// Determine if we need to hide content type menu items.
$params = $url->getRouteParameters();
$content_type = $params['type'] ?? '';
if (empty($content_type)) {
return FALSE;
}
$hide_content_type_items = $this->config->get("plugins." . $this->getPluginId() . '.hide_content_type_items') ?? [];
if (!empty($hide_content_type_items['content_view'])) {
// If the content type is not set in the content view type filter
// selection, then don't show the menu item.
if (!$view_definition) {
// Check if there is a content type specific view, else fall back to
// the normal content view of core.
$view = Views::getView('content_' . $content_type) ?? Views::getView('content');
if ($view) {
// Look for the page_1 display (set by core by default). Or fallback
// to the default settings.
/** @var \Drupal\views\Plugin\views\ViewsHandlerInterface $handler */
$view_definition = $view->getHandler('page_1', 'filter', 'type')
?? $view->getHandler('default', 'filter', 'type')
?? [];
}
}
if (!empty($view_definition['value']) && !in_array($content_type, $view_definition['value'])) {
return TRUE;
}
}
if (!empty($hide_content_type_items['admin_permissions']) && $this->currentUser->id() > 1) {
// We need at least permission to see the overview page.
if (!$this->currentUser->hasPermission('access content overview')) {
return TRUE;
}
// If the user doesn't have at least one of these administrative
// permissions on the content type, then don't show the menu item.
$admin_permissions = FALSE;
$permissions = [
// General node permissions.
'bypass node access',
'administer nodes',
'view own unpublished content',
'view all revisions',
'revert all revisions',
'delete all revisions',
// Content type specific permissions.
"create $content_type content",
"edit own $content_type content",
"edit any $content_type content",
"delete own $content_type content",
"delete any $content_type content",
"view $content_type revisions",
"revert $content_type revisions",
"delete $content_type revisions",
];
foreach ($permissions as $permission) {
$admin_permissions |= $this->currentUser->hasPermission($permission);
if ($admin_permissions) {
// We know enough, stop checking.
break;
}
}
if (!$admin_permissions) {
return TRUE;
}
}
}
// Filter out "recent items" or "more" children if there are no recent menu
// children left.
if (!empty($item['below'])) {
$recent_items_index = null;
$more_items_index = null;
$count_recent_items = 0;
foreach ($item['below'] as $index => $child) {
$attributes = $child['url']->getOption('attributes') ?? [];
if (in_array('admin-toolbar-content--recent-items', $attributes['class'])) {
$recent_items_index = $index;
} elseif (in_array('admin-toolbar-content--more-recent-items', $attributes['class'])) {
$more_items_index = $index;
} elseif (in_array('admin-toolbar-content--recent-item', $attributes['class'])) {
// Remove the child if it is still an empty placeholder
// (has no parameter entity id filled in).
if (empty($child['url']->getRouteParameters()['node'])) {
unset($item['below'][$index]);
}
else {
$count_recent_items++;
}
}
}
if (!$count_recent_items && ($this->config->get("plugins." . $this->getPluginId() . '.recent_items.hide_empty_list') ?? 0)) {
unset($item['below'][$recent_items_index]);
unset($item['below'][$more_items_index]);
}
// If less then the desired count items remain, just remove the more link.
$count = $this->config->get("plugins." . $this->getPluginId() . '.recent_items.number_of_items') ?? 0;
if ($count_recent_items < $count) {
unset($item['below'][$more_items_index]);
}
}
return parent::filterMenuItem($item, $parent);
}
/**
* Creates the recent content links for items.
*
* @param string $type
* The type.
*/
protected function createItemRecentContentLinks(string $type): void {
$count = $this->config->get("plugins." . $this->getPluginId() . '.recent_items.number_of_items') ?? 0;
if (empty($count)) {
return;
}
$items = $this->getItems();
$recent_items_link = $this->config->get("plugins." . $this->getPluginId() . '.recent_items.link') ?? 'default';
$route_name = [
'default' => 'entity.node.edit_form',
'view' => 'entity.node.canonical',
'layout_builder' => 'layout_builder.overrides.' . $type . '.view',
][$recent_items_link];
$route_parameters = [];
foreach ($items as $item) {
$this->createItemRecentContentEditLinks($item, $type, $count, $route_name, $route_parameters);
}
}
/**
* Create the edit links for the recent items in the content.
*
* @param mixed $item
* The item.
* @param string $entity_type
* The entity type.
* @param int $count
* How many items to generate.
* @param string $route_name
* The route name.
* @param array $route_parameters
* The route parameters.
*/
protected function createItemRecentContentEditLinks(mixed $item, string $entity_type, int $count, string $route_name, array $route_parameters = []): void {
$collection = $this->getItemCollection($item);
// If the item link is not there, we can't add recent items.
if (!isset($this->links[$collection['id'] . '.' . $item->id()])) {
return;
}
$type = $this->entityTypeManager->getDefinition($entity_type);
/** @var \Drupal\node\NodeStorageInterface $entity_storage */
$entity_storage = $this->entityTypeManager->getStorage($entity_type);
// Check if we have items of this type.
$ids = $entity_storage->getQuery()
// We don't do the access check here, we let menu handle that.
// Otherwise, the check is performed on the user doing the rebuild.
->accessCheck(FALSE)
->condition($type->getKey('bundle'), $item->id())
->range(0, $count + 1)
->sort('changed', 'DESC')
->sort($type->getKey('id'), 'DESC')
->execute();
if (empty($ids)) {
return;
}
// Add a "Recent items" empty menu item, to create a visual divider
// in the menu.
$this->links[$collection['id'] . '.' . $item->id() . '.recent'] = [
'title' => $this->t('Recent items'),
'route_name' => '<none>',
'parent' => $this->baseMenuLinkPluginDefinition['id'] . ':' . $collection['id'] . '.' . $item->id(),
'weight' => 0,
'options' => [
'attributes' => [
'class' => [
'admin-toolbar-content--recent-items',
'admin-toolbar-content--recent-items--' . $item->id(),
],
],
],
// We need to override admin_toolbar_tools MenuLinkEntity::getTitle().
'class' => "Drupal\Core\Menu\MenuLinkDefault",
] + $this->links[$collection['id'] . '.' . $item->id()];
// Create menu item placeholders. Each user will have a different list
// of recent menu items. So we create menu items with out custom Menu link
// entity that will replace the placeholder with a linke to the entities
// the user is entitled to administer.
for ($counter = 0; $counter < $count; $counter++) {
// Use a placeholder entity id (0).
$route_parameters = [
$entity_type => 0,
] + $route_parameters;
if ($this->isRouteAvailable($route_name)) {
$this->createItemRecentContentEditLink($collection, $item, $route_name, $route_parameters, $counter);
}
}
// Add a "More" menu item, linking to the content overview.
if (count($ids) > $count) {
$this->links[$collection['id'] . '.' . $item->id() . '.more'] = [
'title' => $this->t('More'),
'weight' => $counter + 2,
'parent' => $this->baseMenuLinkPluginDefinition['id'] . ':' . $collection['id'] . '.' . $item->id(),
'options' => [
'attributes' => [
'class' => [
'admin-toolbar-content--more-recent-items',
'admin-toolbar-content--more-recent-items--' . $item->id(),
],
],
],
// We need to override admin_toolbar_tools MenuLinkEntity::getTitle().
'class' => "Drupal\Core\Menu\MenuLinkDefault",
] + $this->links[$collection['id'] . '.' . $item->id()];
}
}
/**
* Create the edit link for the recent item in the content.
*
* @param array $collection
* The collections.
* @param mixed $item
* The item to create the link for.
* @param string $route_name
* The route name.
* @param array $route_parameters
* The route parameters.
* @param int $weight
* The weight.
*/
protected function createItemRecentContentEditLink(array $collection, mixed $item, string $route_name, array $route_parameters, int $count): void {
$this->links[$collection['id'] . '.' . $item->id() . '.recent.entity.' . $count] = [
'class' => 'Drupal\admin_toolbar_content\Plugin\Menu\RecentMenuLinkEntity',
'route_name' => $route_name,
'route_parameters' => $route_parameters,
'menu_name' => 'admin',
'weight' => $count + 1,
'parent' => $this->baseMenuLinkPluginDefinition['id'] . ':' . $collection['id'] . '.' . $item->id(),
'options' => [
'attributes' => [
'class' => [
'admin-toolbar-content--recent-item',
'admin-toolbar-content--recent-item--' . $item->id(),
],
],
],
'metadata' => [
// See RecentMenuLinkEntity class how these are used.
'entity_type' => $item->getEntityType()->getBundleOf(),
'entity_bundle' => $item->id(),
'entity_count' => $count,
],
] + $this->baseMenuLinkPluginDefinition;
}
}
