mirror of
https://gitlab.unicamp.br/infimecc_drupal11_modules/structural_pages.git
synced 2026-05-05 18:35:29 -03:00
Add field_redirect_page to site_sections terms and fix root page hierarchy
- Add field_redirect_page to site_sections taxonomy terms, with a custom EntityReferenceSelection plugin that filters content_page nodes by section - Redirect taxonomy term canonical URLs to the configured node (301) - Fix root page detection in MenuBlock and views to also match nodes whose field_parent_page points to a taxonomy_term (not only empty parent) - Move root taxonomy-term option to the top of the parent-page dropdown - Add breadcrumb workaround for Gavias notech theme separator rendering - Add imecc_menu_helper submodule - Translate config labels and default terms from English to Portuguese Co-Authored-By: Henrique Bauer <henrique@webcontent.com.br> Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -16,7 +16,8 @@ use Drupal\structural_pages\ParentEntityHandler\ParentEntityHandlerManagerInterf
|
||||
/**
|
||||
* Provides a breadcrumb builder for section_page and content_page content types.
|
||||
*/
|
||||
class SectionBreadcrumbBuilder implements BreadcrumbBuilderInterface {
|
||||
class SectionBreadcrumbBuilder implements BreadcrumbBuilderInterface
|
||||
{
|
||||
|
||||
use StringTranslationTrait;
|
||||
|
||||
@@ -60,7 +61,8 @@ class SectionBreadcrumbBuilder implements BreadcrumbBuilderInterface {
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function applies(RouteMatchInterface $route_match): bool {
|
||||
public function applies(RouteMatchInterface $route_match): bool
|
||||
{
|
||||
$node = $route_match->getParameter('node');
|
||||
|
||||
if ($node instanceof NodeInterface) {
|
||||
@@ -73,7 +75,8 @@ class SectionBreadcrumbBuilder implements BreadcrumbBuilderInterface {
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function build(RouteMatchInterface $route_match): Breadcrumb {
|
||||
public function build(RouteMatchInterface $route_match): Breadcrumb
|
||||
{
|
||||
$breadcrumb = new Breadcrumb();
|
||||
$breadcrumb->addCacheContexts(['route']);
|
||||
|
||||
@@ -94,8 +97,7 @@ class SectionBreadcrumbBuilder implements BreadcrumbBuilderInterface {
|
||||
if ($context) {
|
||||
// Use the handler manager to build breadcrumbs for the context entity.
|
||||
$this->handlerManager->buildBreadcrumbForEntity($breadcrumb, $context['entity']);
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
// No parent context, check for site section directly.
|
||||
if ($node->hasField('field_site_section') && !$node->get('field_site_section')->isEmpty()) {
|
||||
$term_id = $node->get('field_site_section')->target_id;
|
||||
@@ -108,6 +110,12 @@ class SectionBreadcrumbBuilder implements BreadcrumbBuilderInterface {
|
||||
$this->addParentBreadcrumbs($breadcrumb, $node);
|
||||
}
|
||||
|
||||
// Gavias notech theme bug bypass: the theme requires an empty element
|
||||
// before the title to correctly iterate and render '/' separators.
|
||||
$breadcrumb->addLink(Link::createFromRoute('', '<none>'));
|
||||
// Append the active page title, as the theme's title resolver fails for nodes.
|
||||
$breadcrumb->addLink(Link::createFromRoute($node->getTitle(), '<none>'));
|
||||
|
||||
return $breadcrumb;
|
||||
}
|
||||
|
||||
@@ -122,7 +130,8 @@ class SectionBreadcrumbBuilder implements BreadcrumbBuilderInterface {
|
||||
* @return array|null
|
||||
* An array with 'type' and 'entity' keys, or NULL if no context.
|
||||
*/
|
||||
protected function getParentContext(NodeInterface $node): ?array {
|
||||
protected function getParentContext(NodeInterface $node): ?array
|
||||
{
|
||||
if ($node->bundle() !== 'content_page') {
|
||||
return NULL;
|
||||
}
|
||||
@@ -130,9 +139,11 @@ class SectionBreadcrumbBuilder implements BreadcrumbBuilderInterface {
|
||||
$visited = [];
|
||||
$current = $node;
|
||||
|
||||
while ($current instanceof NodeInterface &&
|
||||
$current->hasField('field_parent_page') &&
|
||||
!$current->get('field_parent_page')->isEmpty()) {
|
||||
while (
|
||||
$current instanceof NodeInterface &&
|
||||
$current->hasField('field_parent_page') &&
|
||||
!$current->get('field_parent_page')->isEmpty()
|
||||
) {
|
||||
|
||||
$parent_field = $current->get('field_parent_page')->first();
|
||||
if (!$parent_field) {
|
||||
@@ -180,8 +191,7 @@ class SectionBreadcrumbBuilder implements BreadcrumbBuilderInterface {
|
||||
// If parent is a node, continue traversing.
|
||||
if ($parent instanceof NodeInterface) {
|
||||
$current = $parent;
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -197,7 +207,8 @@ class SectionBreadcrumbBuilder implements BreadcrumbBuilderInterface {
|
||||
* @param int|string $term_id
|
||||
* The taxonomy term ID.
|
||||
*/
|
||||
protected function addTaxonomyBreadcrumbs(Breadcrumb $breadcrumb, int|string $term_id): void {
|
||||
protected function addTaxonomyBreadcrumbs(Breadcrumb $breadcrumb, int|string $term_id): void
|
||||
{
|
||||
$term_storage = $this->entityTypeManager->getStorage('taxonomy_term');
|
||||
$ancestors = $term_storage->loadAllParents($term_id);
|
||||
$ancestors = array_reverse($ancestors);
|
||||
@@ -219,7 +230,8 @@ class SectionBreadcrumbBuilder implements BreadcrumbBuilderInterface {
|
||||
* @param \Drupal\node\NodeInterface $node
|
||||
* The current node.
|
||||
*/
|
||||
protected function addParentBreadcrumbs(Breadcrumb $breadcrumb, NodeInterface $node): void {
|
||||
protected function addParentBreadcrumbs(Breadcrumb $breadcrumb, NodeInterface $node): void
|
||||
{
|
||||
$parents = $this->getNodeParents($node);
|
||||
$parents = array_reverse($parents);
|
||||
|
||||
@@ -240,14 +252,17 @@ class SectionBreadcrumbBuilder implements BreadcrumbBuilderInterface {
|
||||
* @return \Drupal\node\NodeInterface[]
|
||||
* Array of parent nodes, from closest to farthest.
|
||||
*/
|
||||
protected function getNodeParents(NodeInterface $node): array {
|
||||
protected function getNodeParents(NodeInterface $node): array
|
||||
{
|
||||
$parents = [];
|
||||
$visited = [];
|
||||
$current = $node;
|
||||
|
||||
while ($current instanceof NodeInterface &&
|
||||
$current->hasField('field_parent_page') &&
|
||||
!$current->get('field_parent_page')->isEmpty()) {
|
||||
while (
|
||||
$current instanceof NodeInterface &&
|
||||
$current->hasField('field_parent_page') &&
|
||||
!$current->get('field_parent_page')->isEmpty()
|
||||
) {
|
||||
|
||||
$parent_field = $current->get('field_parent_page')->first();
|
||||
if (!$parent_field) {
|
||||
|
||||
@@ -73,6 +73,26 @@ class StructuralPagesRedirectSubscriber implements EventSubscriberInterface
|
||||
}
|
||||
}
|
||||
}
|
||||
} elseif ($route_name === 'entity.taxonomy_term.canonical') {
|
||||
$term = $this->routeMatch->getParameter('taxonomy_term');
|
||||
if ($term instanceof \Drupal\taxonomy\TermInterface && $term->bundle() === 'site_sections') {
|
||||
if ($term->hasField('field_redirect_page') && !$term->get('field_redirect_page')->isEmpty()) {
|
||||
try {
|
||||
$target_node = $term->get('field_redirect_page')->entity;
|
||||
if ($target_node && $target_node->access('view')) {
|
||||
$url = $target_node->toUrl()->toString();
|
||||
|
||||
// Cacheable redirect response.
|
||||
$response = new TrustedRedirectResponse($url, 301);
|
||||
$response->addCacheableDependency($term);
|
||||
$response->addCacheableDependency($target_node);
|
||||
$event->setResponse($response);
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
// If invalid, proceed normally.
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,8 @@ use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
/**
|
||||
* Configuration form for Structural Pages module.
|
||||
*/
|
||||
class StructuralPagesSettingsForm extends ConfigFormBase {
|
||||
class StructuralPagesSettingsForm extends ConfigFormBase
|
||||
{
|
||||
|
||||
/**
|
||||
* The entity type manager.
|
||||
@@ -69,7 +70,8 @@ class StructuralPagesSettingsForm extends ConfigFormBase {
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function create(ContainerInterface $container): static {
|
||||
public static function create(ContainerInterface $container): static
|
||||
{
|
||||
return new static(
|
||||
$container->get('config.factory'),
|
||||
$container->get('config.typed'),
|
||||
@@ -82,21 +84,24 @@ class StructuralPagesSettingsForm extends ConfigFormBase {
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getFormId(): string {
|
||||
public function getFormId(): string
|
||||
{
|
||||
return 'structural_pages_settings_form';
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getEditableConfigNames(): array {
|
||||
protected function getEditableConfigNames(): array
|
||||
{
|
||||
return ['structural_pages.settings'];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function buildForm(array $form, FormStateInterface $form_state): array {
|
||||
public function buildForm(array $form, FormStateInterface $form_state): array
|
||||
{
|
||||
$config = $this->config('structural_pages.settings');
|
||||
$allowed_targets = $config->get('allowed_parent_targets') ?? [];
|
||||
|
||||
@@ -116,8 +121,8 @@ class StructuralPagesSettingsForm extends ConfigFormBase {
|
||||
|
||||
$form['site_section_vocabulary'] = [
|
||||
'#type' => 'select',
|
||||
'#title' => $this->t('Site section vocabulary'),
|
||||
'#description' => $this->t('Select the taxonomy vocabulary used for site sections. Content pages and section pages will reference terms from this vocabulary.'),
|
||||
'#title' => $this->t('Vocabulário de Seções do Site'),
|
||||
'#description' => $this->t('Selecione o vocabulário de taxonomia usado para as seções do site. As páginas de conteúdo e páginas de seção farão referência aos termos deste vocabulário.'),
|
||||
'#options' => $vocabulary_options,
|
||||
'#default_value' => $config->get('site_section_vocabulary') ?? 'site_sections',
|
||||
'#required' => TRUE,
|
||||
@@ -194,7 +199,8 @@ class StructuralPagesSettingsForm extends ConfigFormBase {
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function submitForm(array &$form, FormStateInterface $form_state): void {
|
||||
public function submitForm(array &$form, FormStateInterface $form_state): void
|
||||
{
|
||||
$vocabulary = $form_state->getValue('site_section_vocabulary');
|
||||
$values = $form_state->getValue('allowed_parent_targets') ?? [];
|
||||
$supportedEntityTypes = $this->handlerManager->getSupportedEntityTypes();
|
||||
@@ -258,7 +264,8 @@ class StructuralPagesSettingsForm extends ConfigFormBase {
|
||||
* @param array $targets
|
||||
* The allowed targets.
|
||||
*/
|
||||
protected function updateFieldConfiguration(array $targets): void {
|
||||
protected function updateFieldConfiguration(array $targets): void
|
||||
{
|
||||
$field_config = $this->entityTypeManager
|
||||
->getStorage('field_config')
|
||||
->load('node.content_page.field_parent_page');
|
||||
@@ -300,8 +307,7 @@ class StructuralPagesSettingsForm extends ConfigFormBase {
|
||||
// Handle wildcard bundles.
|
||||
if ($bundle === '*') {
|
||||
$entity_type_settings[$entity_type]['handler_settings']['target_bundles'] = NULL;
|
||||
}
|
||||
elseif ($entity_type_settings[$entity_type]['handler_settings']['target_bundles'] !== NULL) {
|
||||
} elseif ($entity_type_settings[$entity_type]['handler_settings']['target_bundles'] !== NULL) {
|
||||
$entity_type_settings[$entity_type]['handler_settings']['target_bundles'][$bundle] = $bundle;
|
||||
}
|
||||
}
|
||||
@@ -323,7 +329,8 @@ class StructuralPagesSettingsForm extends ConfigFormBase {
|
||||
* @param string $vocabulary
|
||||
* The vocabulary machine name to set as target bundle.
|
||||
*/
|
||||
protected function updateSiteSectionFieldConfiguration(string $vocabulary): void {
|
||||
protected function updateSiteSectionFieldConfiguration(string $vocabulary): void
|
||||
{
|
||||
$field_names = [
|
||||
'node.section_page.field_site_section',
|
||||
'node.content_page.field_site_section',
|
||||
|
||||
@@ -388,8 +388,11 @@ class StructuralPagesMenuBlock extends BlockBase implements ContainerFactoryPlug
|
||||
->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');
|
||||
// Root pages could have an empty parent page OR explicitly point to the taxonomy term.
|
||||
$or_group = $query->orConditionGroup()
|
||||
->notExists('field_parent_page')
|
||||
->condition('field_parent_page.target_type', 'taxonomy_term');
|
||||
$query->condition($or_group);
|
||||
$query->condition('field_site_section.target_id', $parent_id);
|
||||
} else {
|
||||
// Child pages belong to a specific parent entity (node).
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\structural_pages\Plugin\EntityReferenceSelection;
|
||||
|
||||
use Drupal\node\Plugin\EntityReferenceSelection\NodeSelection;
|
||||
|
||||
/**
|
||||
* Filter Content Pages by the currently edited Site Section term.
|
||||
*
|
||||
* @EntityReferenceSelection(
|
||||
* id = "default:node_site_section_redirect",
|
||||
* label = @Translation("Site Section Redirect Pages"),
|
||||
* entity_types = {"node"},
|
||||
* group = "default",
|
||||
* weight = 10
|
||||
* )
|
||||
*/
|
||||
class SiteSectionRedirectSelection extends NodeSelection
|
||||
{
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function buildEntityQuery($match = NULL, $match_operator = 'CONTAINS')
|
||||
{
|
||||
$query = parent::buildEntityQuery($match, $match_operator);
|
||||
|
||||
// Get the taxonomy term being edited.
|
||||
$term = $this->configuration['entity'] ?? NULL;
|
||||
if ($term && $term->getEntityTypeId() === 'taxonomy_term' && $term->bundle() === 'site_sections') {
|
||||
if ($term->id()) {
|
||||
$query->condition('field_site_section', $term->id());
|
||||
} else {
|
||||
// If it's a newly created term, there are no pages for it yet to redirect to.
|
||||
$query->condition('nid', 0);
|
||||
}
|
||||
}
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user