From 276fd028f23a53c232676e300fd03794a167b5b5 Mon Sep 17 00:00:00 2001 From: "Quintino A. G. Souza" Date: Thu, 5 Mar 2026 07:52:49 -0300 Subject: [PATCH] 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 Co-Authored-By: Claude Sonnet 4.6 --- ...ay.taxonomy_term.site_sections.default.yml | 59 +++++ ...ay.taxonomy_term.site_sections.default.yml | 45 ++++ ...ld.node.content_page.field_parent_page.yml | 2 +- ...d.node.content_page.field_site_section.yml | 2 +- ...d.node.section_page.field_site_section.yml | 4 +- ...term.site_sections.field_redirect_page.yml | 23 ++ ...m.site_sections.layout_builder__layout.yml | 21 ++ ...rage.taxonomy_term.field_redirect_page.yml | 19 ++ ...e.taxonomy_term.layout_builder__layout.yml | 19 ++ config/install/node.type.content_page.yml | 2 +- config/install/node.type.section_page.yml | 2 +- .../pathauto.pattern.site_sections_term.yml | 2 +- .../taxonomy.vocabulary.site_sections.yml | 4 +- imecc_menu_helper/imecc_menu_helper.info.yml | 6 + .../imecc_menu_helper.links.menu.yml | 5 + imecc_menu_helper/imecc_menu_helper.module | 202 ++++++++++++++++++ .../imecc_menu_helper.permissions.yml | 3 + .../imecc_menu_helper.routing.yml | 7 + .../src/Form/MenuHelperSettingsForm.php | 79 +++++++ src/Breadcrumb/SectionBreadcrumbBuilder.php | 49 +++-- .../StructuralPagesRedirectSubscriber.php | 20 ++ src/Form/StructuralPagesSettingsForm.php | 31 +-- src/Plugin/Block/StructuralPagesMenuBlock.php | 7 +- .../SiteSectionRedirectSelection.php | 42 ++++ structural_pages.install | 56 ++--- structural_pages.module | 27 ++- 26 files changed, 661 insertions(+), 77 deletions(-) create mode 100644 config/install/core.entity_form_display.taxonomy_term.site_sections.default.yml create mode 100644 config/install/core.entity_view_display.taxonomy_term.site_sections.default.yml create mode 100644 config/install/field.field.taxonomy_term.site_sections.field_redirect_page.yml create mode 100644 config/install/field.field.taxonomy_term.site_sections.layout_builder__layout.yml create mode 100644 config/install/field.storage.taxonomy_term.field_redirect_page.yml create mode 100644 config/install/field.storage.taxonomy_term.layout_builder__layout.yml create mode 100644 imecc_menu_helper/imecc_menu_helper.info.yml create mode 100644 imecc_menu_helper/imecc_menu_helper.links.menu.yml create mode 100644 imecc_menu_helper/imecc_menu_helper.module create mode 100644 imecc_menu_helper/imecc_menu_helper.permissions.yml create mode 100644 imecc_menu_helper/imecc_menu_helper.routing.yml create mode 100644 imecc_menu_helper/src/Form/MenuHelperSettingsForm.php create mode 100644 src/Plugin/EntityReferenceSelection/SiteSectionRedirectSelection.php diff --git a/config/install/core.entity_form_display.taxonomy_term.site_sections.default.yml b/config/install/core.entity_form_display.taxonomy_term.site_sections.default.yml new file mode 100644 index 0000000..d7136e8 --- /dev/null +++ b/config/install/core.entity_form_display.taxonomy_term.site_sections.default.yml @@ -0,0 +1,59 @@ +langcode: pt-br +status: true +dependencies: + config: + - field.field.taxonomy_term.site_sections.field_redirect_page + - field.field.taxonomy_term.site_sections.layout_builder__layout + - taxonomy.vocabulary.site_sections + module: + - path + - text +id: taxonomy_term.site_sections.default +targetEntityType: taxonomy_term +bundle: site_sections +mode: default +content: + description: + type: text_textfield + weight: 0 + region: content + settings: + size: 60 + placeholder: '' + third_party_settings: { } + field_redirect_page: + type: options_select + weight: 10 + region: content + settings: { } + third_party_settings: { } + langcode: + type: language_select + weight: 2 + region: content + settings: + include_locked: true + third_party_settings: { } + name: + type: string_textfield + weight: -5 + region: content + settings: + size: 60 + placeholder: '' + third_party_settings: { } + path: + type: path + weight: 30 + region: content + settings: { } + third_party_settings: { } + status: + type: boolean_checkbox + weight: 100 + region: content + settings: + display_label: true + third_party_settings: { } +hidden: + layout_builder__layout: true diff --git a/config/install/core.entity_view_display.taxonomy_term.site_sections.default.yml b/config/install/core.entity_view_display.taxonomy_term.site_sections.default.yml new file mode 100644 index 0000000..c1b44d3 --- /dev/null +++ b/config/install/core.entity_view_display.taxonomy_term.site_sections.default.yml @@ -0,0 +1,45 @@ +uuid: f6aa2bd0-3f75-4489-b3f9-43429491b548 +langcode: pt-br +status: true +dependencies: + config: + - field.field.taxonomy_term.site_sections.layout_builder__layout + - taxonomy.vocabulary.site_sections + module: + - layout_builder + - layout_discovery +third_party_settings: + layout_builder: + enabled: true + allow_custom: true + sections: + - + layout_id: layout_onecol + layout_settings: + label: '' + components: + 0663ed2f-1401-4e75-8a69-971333b49bc2: + uuid: 0663ed2f-1401-4e75-8a69-971333b49bc2 + region: content + configuration: + id: 'field_block:taxonomy_term:site_sections:description' + label_display: '0' + context_mapping: + entity: layout_builder.entity + formatter: + type: text_default + label: hidden + settings: { } + third_party_settings: { } + weight: 0 + additional: { } + third_party_settings: { } +id: taxonomy_term.site_sections.default +targetEntityType: taxonomy_term +bundle: site_sections +mode: default +content: { } +hidden: + description: true + langcode: true + layout_builder__layout: true diff --git a/config/install/field.field.node.content_page.field_parent_page.yml b/config/install/field.field.node.content_page.field_parent_page.yml index 563d7fd..9d13216 100644 --- a/config/install/field.field.node.content_page.field_parent_page.yml +++ b/config/install/field.field.node.content_page.field_parent_page.yml @@ -13,7 +13,7 @@ field_name: field_parent_page entity_type: node bundle: content_page label: 'Parent Page' -description: 'Select the parent entity. The site section will be inherited automatically based on the parent type.' +description: 'Selecione a entidade pai. A seção do site será herdada automaticamente com base no tipo de pai.' required: false translatable: false default_value: { } diff --git a/config/install/field.field.node.content_page.field_site_section.yml b/config/install/field.field.node.content_page.field_site_section.yml index 1c93b5d..bb29f10 100644 --- a/config/install/field.field.node.content_page.field_site_section.yml +++ b/config/install/field.field.node.content_page.field_site_section.yml @@ -11,7 +11,7 @@ id: node.content_page.field_site_section field_name: field_site_section entity_type: node bundle: content_page -label: 'Site Section' +label: 'Seção do Site' description: 'For root pages, select the section. For child pages, this field is filled automatically.' required: true translatable: false diff --git a/config/install/field.field.node.section_page.field_site_section.yml b/config/install/field.field.node.section_page.field_site_section.yml index a556115..a18c965 100644 --- a/config/install/field.field.node.section_page.field_site_section.yml +++ b/config/install/field.field.node.section_page.field_site_section.yml @@ -11,8 +11,8 @@ id: node.section_page.field_site_section field_name: field_site_section entity_type: node bundle: section_page -label: 'Site Section' -description: 'Select the site section this page belongs to.' +label: 'Seção do Site' +description: 'Selecione a seção do site à qual esta página pertence.' required: true translatable: false default_value: { } diff --git a/config/install/field.field.taxonomy_term.site_sections.field_redirect_page.yml b/config/install/field.field.taxonomy_term.site_sections.field_redirect_page.yml new file mode 100644 index 0000000..1d6b92d --- /dev/null +++ b/config/install/field.field.taxonomy_term.site_sections.field_redirect_page.yml @@ -0,0 +1,23 @@ +langcode: pt-br +status: true +dependencies: + config: + - field.storage.taxonomy_term.field_redirect_page + - node.type.content_page + - taxonomy.vocabulary.site_sections +id: taxonomy_term.site_sections.field_redirect_page +field_name: field_redirect_page +entity_type: taxonomy_term +bundle: site_sections +label: 'Página de Redirecionamento' +description: '' +required: false +translatable: true +default_value: { } +default_value_callback: '' +settings: + handler: 'default:node_site_section_redirect' + handler_settings: + target_bundles: + content_page: content_page +field_type: entity_reference diff --git a/config/install/field.field.taxonomy_term.site_sections.layout_builder__layout.yml b/config/install/field.field.taxonomy_term.site_sections.layout_builder__layout.yml new file mode 100644 index 0000000..75232c9 --- /dev/null +++ b/config/install/field.field.taxonomy_term.site_sections.layout_builder__layout.yml @@ -0,0 +1,21 @@ +uuid: 726b6570-a8df-4518-8542-28446352fe46 +langcode: pt-br +status: true +dependencies: + config: + - field.storage.taxonomy_term.layout_builder__layout + - taxonomy.vocabulary.site_sections + module: + - layout_builder +id: taxonomy_term.site_sections.layout_builder__layout +field_name: layout_builder__layout +entity_type: taxonomy_term +bundle: site_sections +label: Layout +description: '' +required: false +translatable: false +default_value: { } +default_value_callback: '' +settings: { } +field_type: layout_section diff --git a/config/install/field.storage.taxonomy_term.field_redirect_page.yml b/config/install/field.storage.taxonomy_term.field_redirect_page.yml new file mode 100644 index 0000000..bc5841c --- /dev/null +++ b/config/install/field.storage.taxonomy_term.field_redirect_page.yml @@ -0,0 +1,19 @@ +langcode: pt-br +status: true +dependencies: + module: + - node + - taxonomy +id: taxonomy_term.field_redirect_page +field_name: field_redirect_page +entity_type: taxonomy_term +type: entity_reference +settings: + target_type: node +module: core +locked: false +cardinality: 1 +translatable: true +indexes: { } +persist_with_no_fields: false +custom_storage: false diff --git a/config/install/field.storage.taxonomy_term.layout_builder__layout.yml b/config/install/field.storage.taxonomy_term.layout_builder__layout.yml new file mode 100644 index 0000000..6dee228 --- /dev/null +++ b/config/install/field.storage.taxonomy_term.layout_builder__layout.yml @@ -0,0 +1,19 @@ +uuid: 99f94609-3b7c-490e-80a0-96036b03e9d3 +langcode: pt-br +status: true +dependencies: + module: + - layout_builder + - taxonomy +id: taxonomy_term.layout_builder__layout +field_name: layout_builder__layout +entity_type: taxonomy_term +type: layout_section +settings: { } +module: layout_builder +locked: true +cardinality: 1 +translatable: false +indexes: { } +persist_with_no_fields: false +custom_storage: false diff --git a/config/install/node.type.content_page.yml b/config/install/node.type.content_page.yml index 2bf8f7c..69fd48b 100644 --- a/config/install/node.type.content_page.yml +++ b/config/install/node.type.content_page.yml @@ -7,7 +7,7 @@ third_party_settings: name: 'Content Page' type: content_page description: 'Pages with hierarchical parent-child structure for content organization.' -help: 'For root pages, fill in the Site Section manually. For child pages, select the parent page and the section will be inherited automatically.' +help: 'Para páginas raiz, preencha a Seção do Site manualmente. Para páginas filhas, selecione a página pai e a seção será herdada automaticamente.' new_revision: true preview_mode: 1 display_submitted: false diff --git a/config/install/node.type.section_page.yml b/config/install/node.type.section_page.yml index 3f64076..30a2efc 100644 --- a/config/install/node.type.section_page.yml +++ b/config/install/node.type.section_page.yml @@ -6,7 +6,7 @@ third_party_settings: enabled: true name: 'Section Page' type: section_page -description: 'Pages organized by site section, with URLs and breadcrumbs based on taxonomy.' +description: 'Páginas organizadas por seção do site, com URLs e breadcrumbs baseados na taxonomia.' help: '' new_revision: true preview_mode: 1 diff --git a/config/install/pathauto.pattern.site_sections_term.yml b/config/install/pathauto.pattern.site_sections_term.yml index 7a926be..3830c4e 100644 --- a/config/install/pathauto.pattern.site_sections_term.yml +++ b/config/install/pathauto.pattern.site_sections_term.yml @@ -5,7 +5,7 @@ dependencies: - structural_pages - taxonomy id: site_sections_term -label: 'Site Sections Term' +label: 'Termo de Seções do Site' type: 'canonical_entities:taxonomy_term' pattern: '[term:hierarchy-path]' selection_logic: and diff --git a/config/install/taxonomy.vocabulary.site_sections.yml b/config/install/taxonomy.vocabulary.site_sections.yml index c4e459f..81711ca 100644 --- a/config/install/taxonomy.vocabulary.site_sections.yml +++ b/config/install/taxonomy.vocabulary.site_sections.yml @@ -1,7 +1,7 @@ langcode: en status: true dependencies: { } -name: 'Site Sections' +name: 'Seções do Site' vid: site_sections -description: 'Main hierarchical structure for site organization.' +description: 'Estrutura hierárquica principal para organização do site.' weight: 0 diff --git a/imecc_menu_helper/imecc_menu_helper.info.yml b/imecc_menu_helper/imecc_menu_helper.info.yml new file mode 100644 index 0000000..9a9f6e3 --- /dev/null +++ b/imecc_menu_helper/imecc_menu_helper.info.yml @@ -0,0 +1,6 @@ +name: 'IMECC Menu Helper' +type: module +description: 'Automatiza a hierarquia de menus baseada na Taxonomia de Setores.' +core_version_requirement: ^10 +package: Custom +configure: imecc_menu_helper.settings diff --git a/imecc_menu_helper/imecc_menu_helper.links.menu.yml b/imecc_menu_helper/imecc_menu_helper.links.menu.yml new file mode 100644 index 0000000..9d6ae76 --- /dev/null +++ b/imecc_menu_helper/imecc_menu_helper.links.menu.yml @@ -0,0 +1,5 @@ +imecc_menu_helper.settings_link: + title: 'Hierarquia de Menus IMECC' + parent: system.admin_config_content + description: 'Mapear Setores para Itens de Menu.' + route_name: imecc_menu_helper.settings diff --git a/imecc_menu_helper/imecc_menu_helper.module b/imecc_menu_helper/imecc_menu_helper.module new file mode 100644 index 0000000..12514a4 --- /dev/null +++ b/imecc_menu_helper/imecc_menu_helper.module @@ -0,0 +1,202 @@ +getType() != 'pagina') return; + + $field_name = 'field_taxinomia_setor'; + if (!$node->hasField($field_name) || $node->get($field_name)->isEmpty()) return; + + $setor_tid = $node->get($field_name)->target_id; + $config = \Drupal::config('imecc_menu_helper.settings'); + $parent_mapping_string = $config->get('mapping_' . $setor_tid); + + if (empty($parent_mapping_string)) return; + + $current_parent = $node->menu['menu_parent'] ?? 'main:'; + + if ($current_parent != 'main:') { + if ($current_parent != $parent_mapping_string) return; + } + + if (!$node->isNew() && strpos($parent_mapping_string, $node->uuid()) !== false) return; + + $node->menu['enabled'] = 1; + $node->menu['title'] = $node->getTitle(); + $node->menu['menu_parent'] = $parent_mapping_string; +} + +/** + * Implements hook_form_alter(). + */ +function imecc_menu_helper_form_node_form_alter(&$form, FormStateInterface $form_state, $form_id) { + if (!in_array($form_id, ['node_pagina_form', 'node_pagina_edit_form'])) { + return; + } + + // --- LÓGICA DO MENU (AJAX) --- + + // REMOVIDO: O wrapper 'imecc-menu-wrapper' que causava erro. + // Agora atacamos direto o ID nativo 'edit-menu'. + + if (isset($form['field_taxinomia_setor'])) { + $form['field_taxinomia_setor']['widget']['#ajax'] = [ + 'callback' => '_imecc_menu_helper_update_menu_options', + 'wrapper' => 'edit-menu', // <--- CORREÇÃO: Volta para o ID nativo + 'event' => 'change', + 'progress' => [ + 'type' => 'throbber', + 'message' => t('Carregando menu...'), + ], + ]; + } + + // --- LÓGICA DO REDIRECIONAMENTO (Barra Lateral) --- + + $node = $form_state->getFormObject()->getEntity(); + + $should_open = FALSE; + if ($node->hasField('field_redirecionar_para') && !$node->get('field_redirecionar_para')->isEmpty()) { + $should_open = TRUE; + } + + $form['redirection_settings'] = [ + '#type' => 'details', + '#title' => t('Redirecionamento'), + '#group' => 'advanced', + '#open' => $should_open, + '#weight' => -10, + '#attributes' => [ + 'class' => ['node-form-redirection-settings'], + ], + ]; + + if (isset($form['field_redirecionar_para'])) { + $form['field_redirecionar_para']['#group'] = 'redirection_settings'; + } +} + +/** + * Callback AJAX (CORRIGIDO) + */ +function _imecc_menu_helper_update_menu_options(array &$form, FormStateInterface $form_state) { + $values = $form_state->getValue('field_taxinomia_setor'); + $tid = 0; + + if (is_array($values) && isset($values[0]['target_id'])) { + $tid = $values[0]['target_id']; + } elseif (is_array($values) && isset($values['target_id'])) { + $tid = $values['target_id']; + } elseif (is_numeric($values)) { + $tid = $values; + } + + // CORREÇÃO: Garante que o ID do formulário se mantenha estável para não quebrar o AJAX + $form['menu']['#attributes']['id'] = 'edit-menu'; + + if (empty($tid)) return $form['menu']; + + $config = \Drupal::config('imecc_menu_helper.settings'); + $parent_mapping_string = $config->get('mapping_' . $tid); + + if (empty($parent_mapping_string)) return $form['menu']; + + list($menu_name, $root_plugin_id) = explode(':', $parent_mapping_string, 2); + + /** @var \Drupal\Core\Menu\MenuLinkTreeInterface $menu_tree_service */ + $menu_tree_service = \Drupal::service('menu.link_tree'); + + $parameters = new MenuTreeParameters(); + $parameters->setRoot($root_plugin_id); + $parameters->excludeRoot(); + $parameters->setMaxDepth(NULL); + + $tree = $menu_tree_service->load($menu_name, $parameters); + + $allowed_ids = []; + $allowed_ids[$parent_mapping_string] = true; + _imecc_menu_helper_extract_ids($tree, $menu_name, $allowed_ids); + + if (isset($form['menu']['link']['menu_parent']['#options'])) { + $options = $form['menu']['link']['menu_parent']['#options']; + $filtered_options = []; + + foreach ($options as $key => $label) { + if (isset($allowed_ids[$key])) { + $filtered_options[$key] = $label; + } + } + + if (empty($filtered_options) && isset($options[$parent_mapping_string])) { + $filtered_options[$parent_mapping_string] = $options[$parent_mapping_string]; + } + + $form['menu']['link']['menu_parent']['#options'] = $filtered_options; + + $current_val = $form_state->getValue(['menu', 'link', 'menu_parent']); + if (empty($current_val) || !isset($filtered_options[$current_val])) { + $form['menu']['link']['menu_parent']['#value'] = $parent_mapping_string; + } + } + + $form['menu']['#open'] = TRUE; + if (isset($form['menu']['enabled'])) { + $form['menu']['enabled']['#value'] = 1; + } + + // CORREÇÃO: Retorna o array direto, sem envelopar em arrays novos + return $form['menu']; +} + +/** + * Função auxiliar recursiva (Mantida intacta) + */ +function _imecc_menu_helper_extract_ids(array $tree, $menu_name, array &$allowed_ids) { + foreach ($tree as $element) { + $plugin_id = $element->link->getPluginId(); + $key = $menu_name . ':' . $plugin_id; + $allowed_ids[$key] = true; + + if ($element->subtree) { + _imecc_menu_helper_extract_ids($element->subtree, $menu_name, $allowed_ids); + } + } +} + +/** + * Implements hook_node_view(). + * (Mantido intacto) + */ +function imecc_menu_helper_node_view(array &$build, NodeInterface $node, EntityViewDisplayInterface $display, $view_mode) { + if ($view_mode !== 'full') return; + + $route_name = \Drupal::routeMatch()->getRouteName(); + if ($route_name != 'entity.node.canonical') return; + + $field_redirect = 'field_redirecionar_para'; + + if ($node->hasField($field_redirect) && !$node->get($field_redirect)->isEmpty()) { + + $url_object = $node->get($field_redirect)->first()->getUrl(); + $destination = $url_object->toString(); + + $current_url = Url::fromRoute('')->toString(); + + if ($destination == $current_url) return; + + $response = new TrustedRedirectResponse($destination, 302); + $response->addCacheableDependency($node); + $response->send(); + exit; + } +} \ No newline at end of file diff --git a/imecc_menu_helper/imecc_menu_helper.permissions.yml b/imecc_menu_helper/imecc_menu_helper.permissions.yml new file mode 100644 index 0000000..7bfb3a5 --- /dev/null +++ b/imecc_menu_helper/imecc_menu_helper.permissions.yml @@ -0,0 +1,3 @@ +administer imecc menu helper: + title: 'Administrar configuração de menus do IMECC' + description: 'Permite configurar o relacionamento entre Setores e Páginas Pai.' diff --git a/imecc_menu_helper/imecc_menu_helper.routing.yml b/imecc_menu_helper/imecc_menu_helper.routing.yml new file mode 100644 index 0000000..e4c77eb --- /dev/null +++ b/imecc_menu_helper/imecc_menu_helper.routing.yml @@ -0,0 +1,7 @@ +imecc_menu_helper.settings: + path: '/admin/config/imecc/menu-helper' + defaults: + _form: '\Drupal\imecc_menu_helper\Form\MenuHelperSettingsForm' + _title: 'Configuração de Hierarquia IMECC' + requirements: + _permission: 'administer imecc menu helper' diff --git a/imecc_menu_helper/src/Form/MenuHelperSettingsForm.php b/imecc_menu_helper/src/Form/MenuHelperSettingsForm.php new file mode 100644 index 0000000..80c365a --- /dev/null +++ b/imecc_menu_helper/src/Form/MenuHelperSettingsForm.php @@ -0,0 +1,79 @@ +menuParentSelector = $menu_parent_selector; + } + + public static function create(ContainerInterface $container) { + return new static( + $container->get('menu.parent_form_selector') + ); + } + + protected function getEditableConfigNames() { + return ['imecc_menu_helper.settings']; + } + + public function getFormId() { + return 'imecc_menu_helper_settings_form'; + } + + public function buildForm(array $form, FormStateInterface $form_state) { + $config = $this->config('imecc_menu_helper.settings'); + + $form['description'] = [ + '#markup' => '

Mapeie cada Setor (Taxonomia) a um Item de Menu Pai. Quando uma página for criada com este setor, ela será automaticamente movida para dentro deste pai.

', + ]; + + // Carrega todos os termos do vocabulário 'setores'. + $terms = \Drupal::entityTypeManager()->getStorage('taxonomy_term')->loadTree('setores'); + + // Opções de menu (apenas do menu principal). + $menu_options = $this->menuParentSelector->getParentSelectOptions('main:', NULL); + + $form['mappings'] = [ + '#type' => 'details', + '#title' => $this->t('Mapeamento Setor -> Menu Pai'), + '#open' => TRUE, + '#tree' => TRUE, + ]; + + foreach ($terms as $term) { + $default_value = $config->get('mapping_' . $term->tid); + + $form['mappings'][$term->tid] = [ + '#type' => 'select', + '#title' => $term->name . ' (' . $term->tid . ')', + '#options' => $menu_options, + '#default_value' => $default_value, + '#empty_option' => '- Sem automação -', + '#description' => 'Selecione a página pai para o setor ' . $term->name, + ]; + } + + return parent::buildForm($form, $form_state); + } + + public function submitForm(array &$form, FormStateInterface $form_state) { + $config = $this->config('imecc_menu_helper.settings'); + $mappings = $form_state->getValue('mappings'); + + foreach ($mappings as $tid => $menu_parent) { + $config->set('mapping_' . $tid, $menu_parent); + } + + $config->save(); + parent::submitForm($form, $form_state); + } +} diff --git a/src/Breadcrumb/SectionBreadcrumbBuilder.php b/src/Breadcrumb/SectionBreadcrumbBuilder.php index 0939498..c8153ba 100644 --- a/src/Breadcrumb/SectionBreadcrumbBuilder.php +++ b/src/Breadcrumb/SectionBreadcrumbBuilder.php @@ -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('', '')); + // Append the active page title, as the theme's title resolver fails for nodes. + $breadcrumb->addLink(Link::createFromRoute($node->getTitle(), '')); + 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) { diff --git a/src/EventSubscriber/StructuralPagesRedirectSubscriber.php b/src/EventSubscriber/StructuralPagesRedirectSubscriber.php index 082e8b8..9e55a20 100644 --- a/src/EventSubscriber/StructuralPagesRedirectSubscriber.php +++ b/src/EventSubscriber/StructuralPagesRedirectSubscriber.php @@ -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. + } + } + } } } diff --git a/src/Form/StructuralPagesSettingsForm.php b/src/Form/StructuralPagesSettingsForm.php index 42f06b8..add6b52 100644 --- a/src/Form/StructuralPagesSettingsForm.php +++ b/src/Form/StructuralPagesSettingsForm.php @@ -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', diff --git a/src/Plugin/Block/StructuralPagesMenuBlock.php b/src/Plugin/Block/StructuralPagesMenuBlock.php index 6d2fb1c..9ab8559 100644 --- a/src/Plugin/Block/StructuralPagesMenuBlock.php +++ b/src/Plugin/Block/StructuralPagesMenuBlock.php @@ -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). diff --git a/src/Plugin/EntityReferenceSelection/SiteSectionRedirectSelection.php b/src/Plugin/EntityReferenceSelection/SiteSectionRedirectSelection.php new file mode 100644 index 0000000..2551fb3 --- /dev/null +++ b/src/Plugin/EntityReferenceSelection/SiteSectionRedirectSelection.php @@ -0,0 +1,42 @@ +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; + } + +} diff --git a/structural_pages.install b/structural_pages.install index d960d39..73a7717 100644 --- a/structural_pages.install +++ b/structural_pages.install @@ -39,38 +39,38 @@ function _structural_pages_create_default_terms(): void { // Structure: name => children. $terms_structure = [ - 'News' => [], - 'Events' => [], - 'People' => [], - 'Institutional' => [ - 'About', - 'Communication', - 'Information and Services', - 'Team', - 'Management', - 'Inclusion and Belonging', + 'Notícias' => [], + 'Eventos' => [], + 'Pessoas' => [], + 'Institucional' => [ + 'Sobre', + 'Comunicação', + 'Informações e Serviços', + 'Equipe', + 'Gestão', + 'Inclusão e Pertencimento', ], - 'Undergraduate' => [ - 'Statistics', - 'Mathematics', - 'Applied Mathematics', - 'Mathematics Teaching', + 'Graduação' => [ + 'Estatística', + 'Matemática', + 'Matemática Aplicada', + 'Licenciatura em Matemática', ], - 'Graduate' => [ - 'Statistics Program', - 'Mathematics Program', - 'Applied Mathematics Program', + 'Pós-Graduação' => [ + 'Programa de Estatística', + 'Programa de Matemática', + 'Programa de Matemática Aplicada', ], - 'Research' => [], - 'Extension' => [], - 'Administration' => [], - 'Departments' => [ - 'Statistics Department', - 'Mathematics Department', - 'Applied Mathematics Department', + 'Pesquisa' => [], + 'Extensão' => [], + 'Administração' => [], + 'Departamentos' => [ + 'Departamento de Estatística', + 'Departamento de Matemática', + 'Departamento de Matemática Aplicada', ], - 'Library' => [], - 'IT Services' => [], + 'Biblioteca' => [], + 'Informática' => [], ]; $created_terms = []; diff --git a/structural_pages.module b/structural_pages.module index 9772ea2..5b62862 100644 --- a/structural_pages.module +++ b/structural_pages.module @@ -410,6 +410,17 @@ function _structural_pages_alter_parent_page_form(&$form, \Drupal\Core\Form\Form $default_parent = $current_entity->get('field_parent_page')->target_type . ':' . $current_entity->get('field_parent_page')->target_id; } + // Add the Root (Taxonomy term) as the default option at the top of the tree. + $term = \Drupal::entityTypeManager()->getStorage('taxonomy_term')->load($site_section_id); + $term_label = $term ? $term->label() : t('Taxonomia'); + $root_key = 'taxonomy_term:' . $site_section_id; + + $options = [$root_key => '< ' . t('Raiz (@term)', ['@term' => $term_label]) . ' >'] + $options; + + if (empty($default_parent)) { + $default_parent = $root_key; + } + // Hide the original field. $form['field_parent_page']['#access'] = FALSE; @@ -419,8 +430,8 @@ function _structural_pages_alter_parent_page_form(&$form, \Drupal\Core\Form\Form '#title' => t('Parent Page'), '#options' => $options, '#default_value' => $default_parent, - '#empty_option' => t('- Raiz da Seção -'), - '#description' => t('Select the parent page within this site section.'), + '#empty_option' => t('- Selecione a Página Pai -'), + '#description' => t('Select the parent page within this site section. Select Root if this is a top-level page.'), // Attach a select2/chosen class if available in standard themes '#attributes' => [ 'class' => ['select2-widget', 'chosen-enable'], @@ -468,12 +479,6 @@ function structural_pages_parent_page_ajax_callback(array &$form, \Drupal\Core\F */ function _structural_pages_build_parent_tree_options($site_section_id, $current_node_id = NULL) { $options = []; - - // Add the Section's Taxonomy Term as a Root option - $term = \Drupal::entityTypeManager()->getStorage('taxonomy_term')->load($site_section_id); - if ($term) { - $options['taxonomy_term:' . $site_section_id] = '< Raiz (' . $term->getName() . ') >'; - } $query = \Drupal::entityQuery('node') ->condition('type', 'content_page') @@ -581,7 +586,11 @@ function structural_pages_views_post_execute(\Drupal\views\ViewExecutable $view) $query->condition('field_parent_page.target_type', 'node'); } else { // Root page. Siblings are other root pages in the same site section. - $query->notExists('field_parent_page'); + $orGroup = $query->orConditionGroup() + ->notExists('field_parent_page') + ->condition('field_parent_page.target_type', 'taxonomy_term'); + $query->condition($orGroup); + if (!$current_node->get('field_site_section')->isEmpty()) { $query->condition('field_site_section.target_id', $current_node->get('field_site_section')->target_id); }