Adiciona menu 'Adicionar' configurável no menu da conta do usuário

Item pai 'Adicionar' no menu account com subitens derivados dinamicamente
a partir de site_users.settings:add_content_links. O pai fica oculto quando
o usuário não tem acesso a nenhum dos routes configurados.

- Rota site_users.add_content com _custom_access via AddContentAccessCheck
- hook_menu_links_discovered_alter() gera os subitens com IDs estáveis
- Formulário de settings com tabela editável (label, rota, parâmetro, peso)
- CSS do microsite atualizado com dropdown ao hover/focus-within

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-19 14:28:21 -03:00
parent 0ce327026d
commit c96268e09d
12 changed files with 383 additions and 0 deletions

View File

@@ -9,3 +9,4 @@ user_editable_fields:
field_user_social_links: true field_user_social_links: true
field_user_photos: true field_user_photos: true
role_view_modes: { } role_view_modes: { }
add_content_links: [ ]

View File

@@ -30,3 +30,25 @@ site_users.settings:
sequence: sequence:
type: string type: string
label: 'View mode machine name' label: 'View mode machine name'
add_content_links:
type: sequence
label: 'Add content menu items'
sequence:
type: mapping
label: 'Add content menu item'
mapping:
label:
type: label
label: 'Menu item label'
route_name:
type: string
label: 'Route name'
route_parameters:
type: sequence
label: 'Route parameters'
sequence:
type: string
label: 'Parameter value'
weight:
type: integer
label: 'Weight'

View File

@@ -4,3 +4,10 @@ site_users.settings:
route_name: site_users.settings route_name: site_users.settings
parent: site_tools.admin_config parent: site_tools.admin_config
weight: 10 weight: 10
site_users.add_content:
title: 'Adicionar'
route_name: site_users.add_content
menu_name: account
weight: -5
expanded: true

View File

@@ -11,6 +11,7 @@ use Drupal\Core\Entity\Display\EntityViewDisplayInterface;
use Drupal\Core\Field\FieldDefinitionInterface; use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\Field\FieldItemListInterface; use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Menu\MenuLinkDefault;
use Drupal\Core\Session\AccountInterface; use Drupal\Core\Session\AccountInterface;
use Drupal\Core\Url; use Drupal\Core\Url;
use Drupal\field\FieldConfigInterface; use Drupal\field\FieldConfigInterface;
@@ -604,3 +605,44 @@ function site_users_get_default_photo(UserInterface $user): ?MediaInterface {
return NULL; return NULL;
} }
/**
* Implements hook_menu_links_discovered_alter().
*
* Adiciona dinamicamente os subitens do menu "Adicionar" configurados em
* site_users.settings:add_content_links.
*/
function site_users_menu_links_discovered_alter(array &$links): void {
$items = \Drupal::config('site_users.settings')->get('add_content_links') ?? [];
foreach ($items as $item) {
if (empty($item['route_name']) || empty($item['label'])) {
continue;
}
// ID estável derivado da rota e dos parâmetros.
$parts = [preg_replace('/[^a-z0-9]/', '_', strtolower($item['route_name']))];
foreach ($item['route_parameters'] ?? [] as $value) {
$parts[] = preg_replace('/[^a-z0-9]/', '_', strtolower((string) $value));
}
$id = 'site_users.add_content_child.' . implode('_', $parts);
$links[$id] = [
'id' => $id,
'title' => $item['label'],
'route_name' => $item['route_name'],
'route_parameters' => $item['route_parameters'] ?? [],
'menu_name' => 'account',
'parent' => 'site_users.add_content',
'weight' => (int) ($item['weight'] ?? 0),
'provider' => 'site_users',
'class' => MenuLinkDefault::class,
'form_class' => 'Drupal\Core\Menu\Form\MenuLinkDefaultForm',
'metadata' => [],
'options' => [],
'expanded' => FALSE,
'enabled' => TRUE,
'discovered' => TRUE,
];
}
}

View File

@@ -5,3 +5,11 @@ site_users.settings:
_title: 'Site Users Settings' _title: 'Site Users Settings'
requirements: requirements:
_permission: 'administer site_users settings' _permission: 'administer site_users settings'
site_users.add_content:
path: '/add-content'
defaults:
_controller: '\Drupal\site_users\Controller\AddContentController::redirectToFirst'
_title: 'Adicionar'
requirements:
_custom_access: 'site_users.add_content_access::access'

View File

@@ -6,3 +6,9 @@ services:
- '@entity_type.manager' - '@entity_type.manager'
- '@file.repository' - '@file.repository'
- '@file_system' - '@file_system'
site_users.add_content_access:
class: Drupal\site_users\Access\AddContentAccessCheck
arguments:
- '@config.factory'
- '@access_manager'

View File

@@ -0,0 +1,47 @@
<?php
declare(strict_types=1);
namespace Drupal\site_users\Access;
use Drupal\Core\Access\AccessManagerInterface;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Access\AccessResultInterface;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Routing\Access\AccessInterface;
use Drupal\Core\Session\AccountInterface;
/**
* Grants access to the "Adicionar" parent menu route when any child is accessible.
*/
class AddContentAccessCheck implements AccessInterface {
public function __construct(
private readonly ConfigFactoryInterface $configFactory,
private readonly AccessManagerInterface $accessManager,
) {}
/**
* Returns allowed if the user can access at least one configured add route.
*/
public function access(AccountInterface $account): AccessResultInterface {
$items = $this->configFactory
->get('site_users.settings')
->get('add_content_links') ?? [];
foreach ($items as $item) {
if (empty($item['route_name'])) {
continue;
}
$params = $item['route_parameters'] ?? [];
if ($this->accessManager->checkNamedRoute($item['route_name'], $params, $account)) {
return AccessResult::allowed()
->addCacheContexts(['user.permissions', 'user.roles']);
}
}
return AccessResult::forbidden()
->addCacheContexts(['user.permissions', 'user.roles']);
}
}

View File

@@ -0,0 +1,48 @@
<?php
declare(strict_types=1);
namespace Drupal\site_users\Controller;
use Drupal\Core\Access\AccessManagerInterface;
use Drupal\Core\Controller\ControllerBase;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
/**
* Redirects to the first accessible add-content route for the current user.
*/
class AddContentController extends ControllerBase {
public function __construct(
private readonly AccessManagerInterface $accessManager,
) {}
public static function create(ContainerInterface $container): static {
return new static(
$container->get('access_manager'),
);
}
/**
* Redirects to the first accessible add route, or to the front page.
*/
public function redirectToFirst(): RedirectResponse {
$items = $this->config('site_users.settings')
->get('add_content_links') ?? [];
$account = $this->currentUser();
foreach ($items as $item) {
if (empty($item['route_name'])) {
continue;
}
$params = $item['route_parameters'] ?? [];
if ($this->accessManager->checkNamedRoute($item['route_name'], $params, $account)) {
return $this->redirect($item['route_name'], $params);
}
}
return $this->redirect('<front>');
}
}

View File

@@ -177,6 +177,66 @@ class SiteUsersSettingsForm extends ConfigFormBase {
} }
} }
// Fieldset para itens do menu "Adicionar".
$form['add_content_links'] = [
'#type' => 'fieldset',
'#title' => $this->t('Add content menu items'),
'#description' => $this->t('Configure items shown under the "Adicionar" entry in the account menu. Each item points to an entity add route. Leave "Label" empty to remove the row.'),
'#tree' => TRUE,
];
$saved_items = $config->get('add_content_links') ?? [];
// Append one blank row for adding a new item.
$saved_items[] = ['label' => '', 'route_name' => '', 'route_parameters' => [], 'weight' => 0];
$form['add_content_links']['table'] = [
'#type' => 'table',
'#header' => [
$this->t('Label'),
$this->t('Route name'),
$this->t('Parameter name'),
$this->t('Parameter value'),
$this->t('Weight'),
],
'#empty' => $this->t('No items yet. Fill in the row below to add one.'),
];
foreach ($saved_items as $delta => $item) {
$params = $item['route_parameters'] ?? [];
$param_name = array_key_first($params) ?? '';
$param_value = $params[$param_name] ?? '';
$form['add_content_links']['table'][$delta]['label'] = [
'#type' => 'textfield',
'#default_value' => $item['label'] ?? '',
'#size' => 20,
'#placeholder' => $this->t('e.g. Artigo'),
];
$form['add_content_links']['table'][$delta]['route_name'] = [
'#type' => 'textfield',
'#default_value' => $item['route_name'] ?? '',
'#size' => 30,
'#placeholder' => 'e.g. node.add',
];
$form['add_content_links']['table'][$delta]['param_name'] = [
'#type' => 'textfield',
'#default_value' => $param_name,
'#size' => 20,
'#placeholder' => 'e.g. node_type',
];
$form['add_content_links']['table'][$delta]['param_value'] = [
'#type' => 'textfield',
'#default_value' => $param_value,
'#size' => 20,
'#placeholder' => 'e.g. article',
];
$form['add_content_links']['table'][$delta]['weight'] = [
'#type' => 'number',
'#default_value' => (int) ($item['weight'] ?? 0),
'#size' => 4,
];
}
return parent::buildForm($form, $form_state); return parent::buildForm($form, $form_state);
} }
@@ -230,6 +290,32 @@ class SiteUsersSettingsForm extends ConfigFormBase {
} }
} }
// Salvar add_content_links: ignorar linhas sem label ou route_name.
$links_raw = $form_state->getValue(['add_content_links', 'table']) ?? [];
$add_content_links = [];
foreach ($links_raw as $row) {
$label = trim($row['label'] ?? '');
$route_name = trim($row['route_name'] ?? '');
if ($label === '' || $route_name === '') {
continue;
}
$params = [];
$param_name = trim($row['param_name'] ?? '');
$param_value = trim($row['param_value'] ?? '');
if ($param_name !== '') {
$params[$param_name] = $param_value;
}
$add_content_links[] = [
'label' => $label,
'route_name' => $route_name,
'route_parameters' => $params,
'weight' => (int) ($row['weight'] ?? 0),
];
}
// Reordena por weight.
usort($add_content_links, fn($a, $b) => $a['weight'] <=> $b['weight']);
$config->set('add_content_links', $add_content_links);
// Salvar role_view_modes: apenas os valores marcados (filtrar 0). // Salvar role_view_modes: apenas os valores marcados (filtrar 0).
$role_view_modes_raw = $form_state->getValue('role_view_modes') ?? []; $role_view_modes_raw = $form_state->getValue('role_view_modes') ?? [];
$roles = \Drupal\user\Entity\Role::loadMultiple(); $roles = \Drupal\user\Entity\Role::loadMultiple();

View File

@@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace Drupal\site_users\Plugin\Menu;
use Drupal\Core\Menu\MenuLinkDefault;
/**
* Provides derived menu links for add-content actions in the account menu.
*
* @MenuLink(
* id = "site_users.add_content_child",
* deriver = "Drupal\site_users\Plugin\Menu\AddContentMenuLinkDeriver"
* )
*/
class AddContentMenuLink extends MenuLinkDefault {
}

View File

@@ -0,0 +1,64 @@
<?php
declare(strict_types=1);
namespace Drupal\site_users\Plugin\Menu;
use Drupal\Component\Plugin\Derivative\DeriverBase;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Plugin\Discovery\ContainerDeriverInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Generates account menu "Adicionar" child links from site_users.settings.
*/
class AddContentMenuLinkDeriver extends DeriverBase implements ContainerDeriverInterface {
public function __construct(
private readonly ConfigFactoryInterface $configFactory,
) {}
public static function create(ContainerInterface $container, $base_plugin_id): static {
return new static($container->get('config.factory'));
}
/**
* {@inheritdoc}
*/
public function getDerivativeDefinitions($base_plugin_definition): array {
$this->derivatives = [];
$items = $this->configFactory
->get('site_users.settings')
->get('add_content_links') ?? [];
foreach ($items as $item) {
if (empty($item['route_name']) || empty($item['label'])) {
continue;
}
$id = $this->buildId($item);
$this->derivatives[$id] = $base_plugin_definition;
$this->derivatives[$id]['title'] = $item['label'];
$this->derivatives[$id]['route_name'] = $item['route_name'];
$this->derivatives[$id]['route_parameters'] = $item['route_parameters'] ?? [];
$this->derivatives[$id]['weight'] = (int) ($item['weight'] ?? 0);
$this->derivatives[$id]['menu_name'] = 'account';
$this->derivatives[$id]['parent'] = 'site_users.add_content';
}
return $this->derivatives;
}
/**
* Builds a stable derivative ID from the item's route and parameters.
*/
private function buildId(array $item): string {
$parts = [preg_replace('/[^a-z0-9]/', '_', strtolower($item['route_name']))];
foreach ($item['route_parameters'] ?? [] as $value) {
$parts[] = preg_replace('/[^a-z0-9]/', '_', strtolower((string) $value));
}
return implode('_', $parts);
}
}

View File

@@ -310,8 +310,42 @@ body.microsite {
margin-inline-end: 18px; margin-inline-end: 18px;
} }
.microsite-top-bar li.menu__item {
position: relative;
}
.microsite-top-bar ul.menu ul.menu { .microsite-top-bar ul.menu ul.menu {
display: none; display: none;
position: absolute;
top: 100%;
left: 0;
min-width: 160px;
flex-direction: column;
background-color: hsl(201, 15%, 15%);
border-radius: 4px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.35);
z-index: 100;
padding: 4px 0;
}
.microsite-top-bar li.menu__item:hover > ul.menu,
.microsite-top-bar li.menu__item:focus-within > ul.menu {
display: flex;
}
.microsite-top-bar ul.menu ul.menu li.menu__item {
margin-inline-end: 0;
white-space: nowrap;
}
.microsite-top-bar ul.menu ul.menu a.menu__link {
padding: 6px 16px;
width: 100%;
box-sizing: border-box;
}
.microsite-top-bar ul.menu ul.menu a.menu__link:hover {
background-color: hsl(201, 15%, 25%);
} }
.microsite-top-bar a.menu__link { .microsite-top-bar a.menu__link {