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

@@ -212,6 +212,7 @@ function structural_pages_theme(): array {
'structural_pages_menu' => [
'variables' => [
'ancestor' => NULL,
'ancestor_url' => '',
'tree' => [],
'active_trail' => [],
'show_ancestor_title' => TRUE,
@@ -332,3 +333,285 @@ function _structural_pages_get_term_hierarchy_path(TermInterface $term): string
return implode('/', $path_parts);
}
/**
* Implements hook_form_FORM_ID_alter() for node_content_page_form.
*/
function structural_pages_form_node_content_page_form_alter(&$form, \Drupal\Core\Form\FormStateInterface $form_state, $form_id) {
_structural_pages_alter_parent_page_form($form, $form_state);
}
/**
* Implements hook_form_FORM_ID_alter() for node_content_page_edit_form.
*/
function structural_pages_form_node_content_page_edit_form_alter(&$form, \Drupal\Core\Form\FormStateInterface $form_state, $form_id) {
_structural_pages_alter_parent_page_form($form, $form_state);
}
/**
* Helper to alter the content_page forms for parent page filtering.
*/
function _structural_pages_alter_parent_page_form(&$form, \Drupal\Core\Form\FormStateInterface $form_state) {
// Hide the native Drupal menu settings tab, since this module generates
// the structural pages menu automatically.
if (isset($form['menu'])) {
$form['menu']['#access'] = FALSE;
}
if (isset($form['field_site_section']) && isset($form['field_parent_page'])) {
// 1. Add AJAX behavior to update parent page options when section changes.
if (isset($form['field_site_section']['widget'])) {
$form['field_site_section']['widget']['#ajax'] = [
'callback' => 'structural_pages_parent_page_ajax_callback',
'wrapper' => 'parent-page-wrapper',
];
}
// Wrap the container where our custom select will go.
$form['structural_pages_wrapper'] = [
'#type' => 'container',
'#attributes' => ['id' => 'parent-page-wrapper'],
'#weight' => $form['field_parent_page']['#weight'] ?? 1,
'#attached' => [
'library' => [
'structural_pages/select2',
],
],
];
// 3. Retrieve the currently selected site_section ID.
$site_section_id = NULL;
// From AJAX state (user just changed it).
$site_section_value = $form_state->getValue('field_site_section');
if (!empty($site_section_value[0]['target_id'])) {
$site_section_id = $site_section_value[0]['target_id'];
}
// From initial form load.
elseif (isset($form['field_site_section']['widget']['#default_value'])) {
$default = $form['field_site_section']['widget']['#default_value'];
if (is_array($default) && !empty($default[0])) {
$site_section_id = $default[0];
}
elseif (is_scalar($default)) {
$site_section_id = $default;
}
}
$current_entity = $form_state->getFormObject()->getEntity();
$current_nid = $current_entity ? $current_entity->id() : NULL;
if ($site_section_id) {
$options = _structural_pages_build_parent_tree_options($site_section_id, $current_nid);
$default_parent = '';
if ($current_entity && !$current_entity->isNew() && !$current_entity->get('field_parent_page')->isEmpty()) {
$default_parent = $current_entity->get('field_parent_page')->target_type . ':' . $current_entity->get('field_parent_page')->target_id;
}
// Hide the original field.
$form['field_parent_page']['#access'] = FALSE;
// Create a new visual field.
$form['structural_pages_wrapper']['custom_parent_page'] = [
'#type' => 'select',
'#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.'),
// Attach a select2/chosen class if available in standard themes
'#attributes' => [
'class' => ['select2-widget', 'chosen-enable'],
'data-placeholder' => t('Search for a parent page...'),
],
];
// We must add a custom submit/validate handler to map our select back to field_parent_page.
array_unshift($form['#validate'], 'structural_pages_custom_parent_validate');
} else {
// If no section chosen yet, hide parent page completely so they pick a section first.
$form['field_parent_page']['#access'] = FALSE;
$form['structural_pages_wrapper']['custom_parent_page'] = [
'#type' => 'markup',
'#markup' => '<p><em>' . t('Por favor, selecione uma Site Section primeiro.') . '</em></p>',
];
}
}
}
/**
* Validation callback to map the select list back into dynamic_entity_reference.
*/
function structural_pages_custom_parent_validate(&$form, \Drupal\Core\Form\FormStateInterface $form_state) {
$custom_val = $form_state->getValue('custom_parent_page');
if (!empty($custom_val)) {
list($type, $id) = explode(':', $custom_val);
$form_state->setValue('field_parent_page', [
['target_id' => $id, 'target_type' => $type]
]);
} else {
$form_state->setValue('field_parent_page', []);
}
}
/**
* AJAX callback to replace the parent page wrapper.
*/
function structural_pages_parent_page_ajax_callback(array &$form, \Drupal\Core\Form\FormStateInterface $form_state) {
return $form['structural_pages_wrapper'];
}
/**
* Builds a hierarchical tree array of content_pages for a given section.
*/
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')
->condition('field_site_section', $site_section_id)
->accessCheck(TRUE)
->sort('title', 'ASC');
$nids = $query->execute();
if (empty($nids)) {
return $options;
}
$nodes = \Drupal::entityTypeManager()->getStorage('node')->loadMultiple($nids);
$children = [];
$roots = [];
foreach ($nodes as $nid => $node) {
if ($nid == $current_node_id) {
continue; // Skip self
}
$parent_id = NULL;
$parent_type = NULL;
if (!$node->get('field_parent_page')->isEmpty()) {
$parent_id = $node->get('field_parent_page')->target_id;
$parent_type = $node->get('field_parent_page')->target_type;
}
// If the parent is another content_page in our list, it's a child.
if ($parent_type === 'node' && isset($nodes[$parent_id])) {
$children[$parent_id][] = $nid;
} else {
// It's a root (parent is a taxonomy term, a section_page, or empty)
$roots[] = $nid;
}
}
// Try to sort roots alphabetically
usort($roots, function($a, $b) use ($nodes) {
return strcmp($nodes[$a]->getTitle(), $nodes[$b]->getTitle());
});
$build_options = function($nids, $depth) use (&$build_options, &$options, $nodes, $children) {
// We use non-breaking spaces and hyphens for a tree view look
$prefix = str_repeat('— ', $depth);
foreach ($nids as $nid) {
$options['node:' . $nid] = $prefix . $nodes[$nid]->getTitle();
if (isset($children[$nid])) {
// Sort children alphabetically too
$child_nids = $children[$nid];
usort($child_nids, function($a, $b) use ($nodes) {
return strcmp($nodes[$a]->getTitle(), $nodes[$b]->getTitle());
});
$build_options($child_nids, $depth + 1);
}
}
};
$build_options($roots, 0);
return $options;
}
/**
* Implements hook_preprocess_node().
*/
function structural_pages_preprocess_node(array &$variables): void {
// Forcefully remove the "submitted by" author and date information
// for content pages, regardless of theme settings.
if (isset($variables['node']) && $variables['node']->bundle() === 'content_page') {
$variables['display_submitted'] = FALSE;
}
}
/**
* Implements hook_views_post_execute().
*/
function structural_pages_views_post_execute(\Drupal\views\ViewExecutable $view) {
// Fallback for child_pages view: if empty, show sibling pages instead.
if ($view->id() === 'child_pages' && empty($view->result)) {
$args = $view->args;
if (!empty($args[0]) && is_numeric($args[0])) {
$node_storage = \Drupal::entityTypeManager()->getStorage('node');
$current_node = $node_storage->load($args[0]);
if ($current_node instanceof \Drupal\node\NodeInterface && $current_node->bundle() === 'content_page') {
$query = \Drupal::entityQuery('node')
->condition('type', 'content_page')
->condition('status', 1)
->accessCheck(TRUE)
->sort('title', 'ASC');
$parent_id = NULL;
if (!$current_node->get('field_parent_page')->isEmpty()) {
$parent_field = $current_node->get('field_parent_page')->first();
if (($parent_field->target_type ?? '') === 'node') {
$parent_id = $parent_field->target_id;
}
}
if ($parent_id) {
// Has a node parent. Siblings are nodes with the same parent.
$query->condition('field_parent_page.target_id', $parent_id);
$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');
if (!$current_node->get('field_site_section')->isEmpty()) {
$query->condition('field_site_section.target_id', $current_node->get('field_site_section')->target_id);
}
}
$nids = $query->execute();
if (!empty($nids)) {
// Load the sibling nodes
$siblings = $node_storage->loadMultiple($nids);
$index = 0;
$view->result = [];
foreach ($siblings as $nid => $sibling) {
$row = new \Drupal\views\ResultRow();
$row->_entity = $sibling;
$row->nid = $nid;
$row->index = $index++;
$view->result[] = $row;
}
$view->total_rows = count($view->result);
// Optionally alter the title to indicate these are siblings
$view->setTitle(t('Páginas do mesmo nível'));
// Remove empty text since we now have results
$view->empty = [];
}
}
}
}
}