Altera lógica de Site Section e adiciona redirect em content_page

- Adiciona campo field_redirect_link (link) no bundle content_page;
  EventSubscriber emite redirect 301 quando o campo está preenchido
- field_site_section passa a ser obrigatório
- Formulário de content_page: AJAX no site section, select hierárquico
  de página pai filtrado por seção, validação customizada
- StructuralPagesMenuBlock: MAX_DEPTH 10→50, nova lógica de raiz via
  field_site_section, variável ancestor_url no render array
- Template do menu: novas classes BEM/Gva, suporte a is_redirect,
  usa ancestor_url em vez de chamada Twig direta
- CSS do menu reescrito com estilos flyout/sidebar; Select2 adicionado
  para o campo de página pai no formulário admin
- display_submitted desabilitado no tipo content_page

Co-Authored-By: Bauer <henrique@webcontent.com.br>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-03 08:04:43 -03:00
parent eff3c0122f
commit 36c3a2e9c0
16 changed files with 746 additions and 102 deletions

View File

@@ -0,0 +1,79 @@
<?php
namespace Drupal\structural_pages\EventSubscriber;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Routing\TrustedRedirectResponse;
use Drupal\node\NodeInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\HttpKernel\KernelEvents;
/**
* Redirects content pages to their configured link if set.
*/
class StructuralPagesRedirectSubscriber implements EventSubscriberInterface
{
/**
* The route match.
*
* @var \Drupal\Core\Routing\RouteMatchInterface
*/
protected $routeMatch;
/**
* Constructs a new StructuralPagesRedirectSubscriber object.
*
* @param \Drupal\Core\Routing\RouteMatchInterface $route_match
* The route match.
*/
public function __construct(RouteMatchInterface $route_match)
{
$this->routeMatch = $route_match;
}
/**
* {@inheritdoc}
*/
public static function getSubscribedEvents()
{
$events[KernelEvents::REQUEST][] = ['onKernelRequest', 30];
return $events;
}
/**
* Redirects if the node has a redirect link.
*
* @param \Symfony\Component\HttpKernel\Event\RequestEvent $event
* The event to process.
*/
public function onKernelRequest(RequestEvent $event)
{
if (!$event->isMainRequest()) {
return;
}
$route_name = $this->routeMatch->getRouteName();
if ($route_name === 'entity.node.canonical') {
$node = $this->routeMatch->getParameter('node');
if ($node instanceof NodeInterface && $node->bundle() === 'content_page') {
if ($node->hasField('field_redirect_link') && !$node->get('field_redirect_link')->isEmpty()) {
try {
/** @var \Drupal\link\LinkItemInterface $link_item */
$link_item = $node->get('field_redirect_link')->first();
$url = $link_item->getUrl()->toString();
// Cacheable redirect response.
$response = new TrustedRedirectResponse($url, 301);
$response->addCacheableDependency($node);
$event->setResponse($response);
} catch (\Exception $e) {
// If the link is invalid, proceed normally.
}
}
}
}
}
}

View File

@@ -27,12 +27,13 @@ use Symfony\Component\DependencyInjection\ContainerInterface;
* }
* )
*/
class StructuralPagesMenuBlock extends BlockBase implements ContainerFactoryPluginInterface {
class StructuralPagesMenuBlock extends BlockBase implements ContainerFactoryPluginInterface
{
/**
* Maximum depth to prevent infinite loops.
* Maximum depth to prevent infinite loops. We set it very high (50) to allow unlimited sublevels.
*/
protected const MAX_DEPTH = 10;
protected const MAX_DEPTH = 50;
/**
* The entity type manager.
@@ -82,7 +83,8 @@ class StructuralPagesMenuBlock extends BlockBase implements ContainerFactoryPlug
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition): static {
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition): static
{
return new static(
$configuration,
$plugin_id,
@@ -96,9 +98,10 @@ class StructuralPagesMenuBlock extends BlockBase implements ContainerFactoryPlug
/**
* {@inheritdoc}
*/
public function defaultConfiguration(): array {
public function defaultConfiguration(): array
{
return [
'max_depth' => 3,
'max_depth' => 50,
'show_ancestor_title' => TRUE,
'expand_active_trail' => TRUE,
] + parent::defaultConfiguration();
@@ -107,7 +110,8 @@ class StructuralPagesMenuBlock extends BlockBase implements ContainerFactoryPlug
/**
* {@inheritdoc}
*/
public function blockForm($form, FormStateInterface $form_state): array {
public function blockForm($form, FormStateInterface $form_state): array
{
$form = parent::blockForm($form, $form_state);
$form['max_depth'] = [
@@ -139,7 +143,8 @@ class StructuralPagesMenuBlock extends BlockBase implements ContainerFactoryPlug
/**
* {@inheritdoc}
*/
public function blockSubmit($form, FormStateInterface $form_state): void {
public function blockSubmit($form, FormStateInterface $form_state): void
{
$this->configuration['max_depth'] = $form_state->getValue('max_depth');
$this->configuration['show_ancestor_title'] = $form_state->getValue('show_ancestor_title');
$this->configuration['expand_active_trail'] = $form_state->getValue('expand_active_trail');
@@ -148,7 +153,8 @@ class StructuralPagesMenuBlock extends BlockBase implements ContainerFactoryPlug
/**
* {@inheritdoc}
*/
public function build(): array {
public function build(): array
{
$this->cacheTags = [];
// Get current context.
@@ -177,6 +183,7 @@ class StructuralPagesMenuBlock extends BlockBase implements ContainerFactoryPlug
$build = [
'#theme' => 'structural_pages_menu',
'#ancestor' => $ancestor,
'#ancestor_url' => $ancestor->hasLinkTemplate('canonical') ? $ancestor->toUrl()->toString() : '',
'#tree' => $tree,
'#active_trail' => $active_trail,
'#show_ancestor_title' => $this->configuration['show_ancestor_title'],
@@ -195,15 +202,15 @@ class StructuralPagesMenuBlock extends BlockBase implements ContainerFactoryPlug
* @return \Drupal\node\NodeInterface|null
* The current node or NULL.
*/
protected function getCurrentNode(): ?NodeInterface {
protected function getCurrentNode(): ?NodeInterface
{
// Try to get from block context first.
try {
$node = $this->getContextValue('node');
if ($node instanceof NodeInterface) {
return $node;
}
}
catch (\Exception $e) {
} catch (\Exception $e) {
// Context not available.
}
@@ -221,7 +228,8 @@ class StructuralPagesMenuBlock extends BlockBase implements ContainerFactoryPlug
* @return \Drupal\Core\Entity\EntityInterface|null
* The ancestor entity (term, user, or group) or NULL.
*/
protected function findAncestor(?NodeInterface $node): ?EntityInterface {
protected function findAncestor(?NodeInterface $node): ?EntityInterface
{
if (!$node) {
// Check if we're on a taxonomy term, user, or group page.
return $this->getAncestorFromRoute();
@@ -278,7 +286,8 @@ class StructuralPagesMenuBlock extends BlockBase implements ContainerFactoryPlug
* @return \Drupal\Core\Entity\EntityInterface|null
* The ancestor entity or NULL.
*/
protected function getAncestorFromRoute(): ?EntityInterface {
protected function getAncestorFromRoute(): ?EntityInterface
{
return $this->handlerManager->getEntityFromRoute($this->routeMatch);
}
@@ -291,7 +300,8 @@ class StructuralPagesMenuBlock extends BlockBase implements ContainerFactoryPlug
* @return array
* The menu tree structure.
*/
protected function buildTree(EntityInterface $ancestor): array {
protected function buildTree(EntityInterface $ancestor): array
{
$root_pages = $this->getChildPages($ancestor->getEntityTypeId(), (string) $ancestor->id());
if (empty($root_pages)) {
@@ -321,14 +331,29 @@ class StructuralPagesMenuBlock extends BlockBase implements ContainerFactoryPlug
* @return array
* The branch structure.
*/
protected function buildBranch(NodeInterface $node, int $current_depth, int $max_depth): array {
protected function buildBranch(NodeInterface $node, int $current_depth, int $max_depth): array
{
$this->cacheTags[] = 'node:' . $node->id();
$url = $node->toUrl()->toString();
$is_redirect = FALSE;
if ($node->hasField('field_redirect_link') && !$node->get('field_redirect_link')->isEmpty()) {
try {
/** @var \Drupal\link\LinkItemInterface $link_item */
$link_item = $node->get('field_redirect_link')->first();
$url = $link_item->getUrl()->toString();
$is_redirect = TRUE;
} catch (\Exception $e) {
// Fallback to node url if the redirect link is invalid.
}
}
$branch = [
'entity' => $node,
'id' => $node->id(),
'title' => $node->label(),
'url' => $node->toUrl()->toString(),
'url' => $url,
'is_redirect' => $is_redirect,
'depth' => $current_depth,
'children' => [],
];
@@ -354,15 +379,24 @@ class StructuralPagesMenuBlock extends BlockBase implements ContainerFactoryPlug
* @return \Drupal\node\NodeInterface[]
* Array of child nodes.
*/
protected function getChildPages(string $parent_type, string $parent_id): array {
protected function getChildPages(string $parent_type, string $parent_id): array
{
$query = $this->entityTypeManager->getStorage('node')->getQuery()
->accessCheck(TRUE)
->condition('type', 'content_page')
->condition('status', 1)
->condition('field_parent_page.target_type', $parent_type)
->condition('field_parent_page.target_id', $parent_id)
->sort('title', 'ASC');
if ($parent_type === 'taxonomy_term') {
// Root pages of a section have no parent page, but belong to the section.
$query->notExists('field_parent_page');
$query->condition('field_site_section.target_id', $parent_id);
} else {
// Child pages belong to a specific parent entity (node).
$query->condition('field_parent_page.target_type', $parent_type);
$query->condition('field_parent_page.target_id', $parent_id);
}
$nids = $query->execute();
if (empty($nids)) {
@@ -386,7 +420,8 @@ class StructuralPagesMenuBlock extends BlockBase implements ContainerFactoryPlug
* @return array
* Array of node IDs in the active trail.
*/
protected function getActiveTrail(NodeInterface $node): array {
protected function getActiveTrail(NodeInterface $node): array
{
$trail = [$node->id()];
$visited = [$node->id() => TRUE];
$current = $node;
@@ -427,14 +462,16 @@ class StructuralPagesMenuBlock extends BlockBase implements ContainerFactoryPlug
/**
* {@inheritdoc}
*/
public function getCacheTags(): array {
public function getCacheTags(): array
{
return Cache::mergeTags(parent::getCacheTags(), ['node_list:content_page']);
}
/**
* {@inheritdoc}
*/
public function getCacheContexts(): array {
public function getCacheContexts(): array
{
return Cache::mergeContexts(parent::getCacheContexts(), ['route', 'url.path']);
}