mirror of
https://gitlab.unicamp.br/infimecc_drupal11_modules/structural_pages.git
synced 2026-05-04 13:10:41 -03:00
Refatora arquitetura de domínio: field_site_section DER, field_parent_page node-only
- field_site_section migrado de entity_reference para dynamic_entity_reference, permitindo referenciar taxonomy_term, user ou event como domínio raiz da página - field_parent_page restrito a content_page nodes apenas (removidos taxonomy_term/user) - Update hooks 10011–10013: migração de schema/config, limpeza de referências inválidas e correção do tipo do storage field_parent_page para DER - SectionBreadcrumbBuilder reescrito para usar field_site_section como raiz - StructuralPagesMenuBlock: queries de domínio via DB direto (evita bug do entity query com especificador target_type em campos DER) - Formulário content_page: pre-fill de field_site_section via query params ?domain_type/domain_id ou ?parent_section_type/parent_id; atualiza widget #default_value diretamente para refletir o valor na renderização inicial - Parâmetro de URL parent_type renomeado para parent_section_type Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -4,8 +4,6 @@ dependencies:
|
||||
config:
|
||||
- field.storage.node.field_parent_page
|
||||
- node.type.content_page
|
||||
- node.type.section_page
|
||||
- taxonomy.vocabulary.site_sections
|
||||
module:
|
||||
- dynamic_entity_reference
|
||||
id: node.content_page.field_parent_page
|
||||
@@ -13,7 +11,7 @@ field_name: field_parent_page
|
||||
entity_type: node
|
||||
bundle: content_page
|
||||
label: 'Parent Page'
|
||||
description: 'Selecione a entidade pai. A seção do site será herdada automaticamente com base no tipo de pai.'
|
||||
description: 'Selecione a página content_page pai dentro do mesmo domínio. Deixe vazio para criar uma página raiz.'
|
||||
required: false
|
||||
translatable: false
|
||||
default_value: { }
|
||||
@@ -21,24 +19,13 @@ default_value_callback: ''
|
||||
settings:
|
||||
entity_type_ids:
|
||||
- node
|
||||
- taxonomy_term
|
||||
node:
|
||||
handler: 'default:node'
|
||||
handler_settings:
|
||||
target_bundles:
|
||||
content_page: content_page
|
||||
section_page: section_page
|
||||
sort:
|
||||
field: title
|
||||
direction: asc
|
||||
auto_create: false
|
||||
taxonomy_term:
|
||||
handler: 'default:taxonomy_term'
|
||||
handler_settings:
|
||||
target_bundles:
|
||||
site_sections: site_sections
|
||||
sort:
|
||||
field: name
|
||||
direction: asc
|
||||
auto_create: false
|
||||
field_type: dynamic_entity_reference
|
||||
|
||||
@@ -6,24 +6,40 @@ dependencies:
|
||||
- node.type.content_page
|
||||
- taxonomy.vocabulary.site_sections
|
||||
module:
|
||||
- dynamic_entity_reference
|
||||
- taxonomy
|
||||
id: node.content_page.field_site_section
|
||||
field_name: field_site_section
|
||||
entity_type: node
|
||||
bundle: content_page
|
||||
label: 'Seção do Site'
|
||||
description: 'For root pages, select the section. For child pages, this field is filled automatically.'
|
||||
description: 'Para páginas raiz, selecione a seção ou domínio. Para páginas filhas, este campo é preenchido automaticamente a partir do pai.'
|
||||
required: true
|
||||
translatable: false
|
||||
default_value: { }
|
||||
default_value_callback: ''
|
||||
settings:
|
||||
handler: 'default:taxonomy_term'
|
||||
handler_settings:
|
||||
target_bundles:
|
||||
site_sections: site_sections
|
||||
sort:
|
||||
field: name
|
||||
direction: asc
|
||||
auto_create: false
|
||||
field_type: entity_reference
|
||||
entity_type_ids:
|
||||
- taxonomy_term
|
||||
- user
|
||||
taxonomy_term:
|
||||
handler: 'default:taxonomy_term'
|
||||
handler_settings:
|
||||
target_bundles:
|
||||
site_sections: site_sections
|
||||
sort:
|
||||
field: name
|
||||
direction: asc
|
||||
auto_create: false
|
||||
user:
|
||||
handler: 'default:user'
|
||||
handler_settings:
|
||||
include_anonymous: false
|
||||
filter:
|
||||
type: _none
|
||||
target_bundles: null
|
||||
sort:
|
||||
field: name
|
||||
direction: asc
|
||||
auto_create: false
|
||||
field_type: dynamic_entity_reference
|
||||
|
||||
@@ -6,6 +6,7 @@ dependencies:
|
||||
- node.type.section_page
|
||||
- taxonomy.vocabulary.site_sections
|
||||
module:
|
||||
- dynamic_entity_reference
|
||||
- taxonomy
|
||||
id: node.section_page.field_site_section
|
||||
field_name: field_site_section
|
||||
@@ -18,12 +19,15 @@ translatable: false
|
||||
default_value: { }
|
||||
default_value_callback: ''
|
||||
settings:
|
||||
handler: 'default:taxonomy_term'
|
||||
handler_settings:
|
||||
target_bundles:
|
||||
site_sections: site_sections
|
||||
sort:
|
||||
field: name
|
||||
direction: asc
|
||||
auto_create: false
|
||||
field_type: entity_reference
|
||||
entity_type_ids:
|
||||
- taxonomy_term
|
||||
taxonomy_term:
|
||||
handler: 'default:taxonomy_term'
|
||||
handler_settings:
|
||||
target_bundles:
|
||||
site_sections: site_sections
|
||||
sort:
|
||||
field: name
|
||||
direction: asc
|
||||
auto_create: false
|
||||
field_type: dynamic_entity_reference
|
||||
|
||||
@@ -10,7 +10,8 @@ entity_type: node
|
||||
type: dynamic_entity_reference
|
||||
settings:
|
||||
exclude_entity_types: false
|
||||
entity_type_ids: []
|
||||
entity_type_ids:
|
||||
- node
|
||||
module: dynamic_entity_reference
|
||||
locked: false
|
||||
cardinality: 1
|
||||
|
||||
@@ -2,15 +2,19 @@ langcode: en
|
||||
status: true
|
||||
dependencies:
|
||||
module:
|
||||
- dynamic_entity_reference
|
||||
- node
|
||||
- taxonomy
|
||||
id: node.field_site_section
|
||||
field_name: field_site_section
|
||||
entity_type: node
|
||||
type: entity_reference
|
||||
type: dynamic_entity_reference
|
||||
settings:
|
||||
target_type: taxonomy_term
|
||||
module: core
|
||||
exclude_entity_types: false
|
||||
entity_type_ids:
|
||||
- taxonomy_term
|
||||
- user
|
||||
module: dynamic_entity_reference
|
||||
locked: false
|
||||
cardinality: 1
|
||||
translatable: true
|
||||
|
||||
@@ -91,17 +91,15 @@ class SectionBreadcrumbBuilder implements BreadcrumbBuilderInterface
|
||||
// Add "Home" as first item.
|
||||
$breadcrumb->addLink(Link::createFromRoute($this->t('Home'), '<front>'));
|
||||
|
||||
// Determine the context type for content_page.
|
||||
$context = $this->getParentContext($node);
|
||||
// Resolve domain root from field_site_section of this node or its ancestors.
|
||||
$domain = $this->resolveDomain($node);
|
||||
|
||||
if ($context) {
|
||||
// Use the handler manager to build breadcrumbs for the context entity.
|
||||
$this->handlerManager->buildBreadcrumbForEntity($breadcrumb, $context['entity']);
|
||||
} 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;
|
||||
$this->addTaxonomyBreadcrumbs($breadcrumb, $term_id);
|
||||
if ($domain) {
|
||||
if ($domain['type'] === 'taxonomy_term') {
|
||||
$this->addTaxonomyBreadcrumbs($breadcrumb, $domain['entity']->id());
|
||||
}
|
||||
else {
|
||||
$this->handlerManager->buildBreadcrumbForEntity($breadcrumb, $domain['entity']);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -120,85 +118,88 @@ class SectionBreadcrumbBuilder implements BreadcrumbBuilderInterface
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the root parent context for a content_page.
|
||||
* Resolves the domain root for the given node.
|
||||
*
|
||||
* Traverses up the parent chain to find the root context (user, group, or taxonomy).
|
||||
* Walks up the field_parent_page chain to the topmost content_page, then
|
||||
* reads field_site_section (DER) to get the domain entity (taxonomy_term,
|
||||
* user, event, …).
|
||||
*
|
||||
* @param \Drupal\node\NodeInterface $node
|
||||
* The content_page node.
|
||||
* Any node (section_page or content_page).
|
||||
*
|
||||
* @return array|null
|
||||
* An array with 'type' and 'entity' keys, or NULL if no context.
|
||||
* @return array{type: string, entity: \Drupal\Core\Entity\EntityInterface}|null
|
||||
* Array with 'type' and 'entity', or NULL if domain cannot be resolved.
|
||||
*/
|
||||
protected function getParentContext(NodeInterface $node): ?array
|
||||
protected function resolveDomain(NodeInterface $node): ?array
|
||||
{
|
||||
if ($node->bundle() !== 'content_page') {
|
||||
return NULL;
|
||||
}
|
||||
// For section_page or any node with field_site_section, read it directly.
|
||||
if ($node->hasField('field_site_section') && !$node->get('field_site_section')->isEmpty()) {
|
||||
$root = $node;
|
||||
|
||||
$visited = [];
|
||||
$current = $node;
|
||||
|
||||
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) {
|
||||
break;
|
||||
// For content_page, walk up to the topmost node to get the canonical
|
||||
// field_site_section (which should be the same on all nodes in the chain).
|
||||
if ($node->bundle() === 'content_page') {
|
||||
$root = $this->getRootContentPage($node);
|
||||
}
|
||||
|
||||
$parent_entity_type = $parent_field->target_type ?? NULL;
|
||||
$parent_id = $parent_field->target_id ?? NULL;
|
||||
if ($root->hasField('field_site_section') && !$root->get('field_site_section')->isEmpty()) {
|
||||
$fss = $root->get('field_site_section')->first();
|
||||
$domain_type = $fss->target_type ?? 'taxonomy_term';
|
||||
$domain_id = $fss->target_id;
|
||||
|
||||
if (!$parent_entity_type || !$parent_id) {
|
||||
break;
|
||||
}
|
||||
|
||||
// Avoid infinite loops.
|
||||
$visit_key = $parent_entity_type . ':' . $parent_id;
|
||||
if (isset($visited[$visit_key])) {
|
||||
break;
|
||||
}
|
||||
$visited[$visit_key] = TRUE;
|
||||
|
||||
$parent = $this->entityTypeManager
|
||||
->getStorage($parent_entity_type)
|
||||
->load($parent_id);
|
||||
|
||||
if (!$parent) {
|
||||
break;
|
||||
}
|
||||
|
||||
// If parent is user or group, that's our context.
|
||||
if (in_array($parent_entity_type, ['user', 'group'])) {
|
||||
return [
|
||||
'type' => $parent_entity_type,
|
||||
'entity' => $parent,
|
||||
];
|
||||
}
|
||||
|
||||
// If parent is taxonomy_term, that's our context (handled via configured vocabulary).
|
||||
if ($parent_entity_type === 'taxonomy_term') {
|
||||
return [
|
||||
'type' => 'taxonomy_term',
|
||||
'entity' => $parent,
|
||||
];
|
||||
}
|
||||
|
||||
// If parent is a node, continue traversing.
|
||||
if ($parent instanceof NodeInterface) {
|
||||
$current = $parent;
|
||||
} else {
|
||||
break;
|
||||
$domain_entity = $this->entityTypeManager->getStorage($domain_type)->load($domain_id);
|
||||
if ($domain_entity) {
|
||||
$breadcrumb_arg = &$this; // used only to add cache dependency below.
|
||||
return ['type' => $domain_type, 'entity' => $domain_entity];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Walks up field_parent_page to find the topmost content_page.
|
||||
*
|
||||
* @param \Drupal\node\NodeInterface $node
|
||||
* The starting node.
|
||||
*
|
||||
* @return \Drupal\node\NodeInterface
|
||||
* The root content_page (or $node itself if no parent chain found).
|
||||
*/
|
||||
protected function getRootContentPage(NodeInterface $node): NodeInterface
|
||||
{
|
||||
$visited = [];
|
||||
$current = $node;
|
||||
|
||||
while (TRUE) {
|
||||
if (isset($visited[$current->id()])) {
|
||||
break;
|
||||
}
|
||||
$visited[$current->id()] = TRUE;
|
||||
|
||||
if (!$current->hasField('field_parent_page') || $current->get('field_parent_page')->isEmpty()) {
|
||||
return $current;
|
||||
}
|
||||
|
||||
$pf = $current->get('field_parent_page')->first();
|
||||
$parent_id = $pf->target_id ?? NULL;
|
||||
|
||||
if (!$parent_id) {
|
||||
return $current;
|
||||
}
|
||||
|
||||
$parent = $this->entityTypeManager->getStorage('node')->load($parent_id);
|
||||
if (!$parent instanceof NodeInterface) {
|
||||
return $current;
|
||||
}
|
||||
|
||||
$current = $parent;
|
||||
}
|
||||
|
||||
return $node;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds breadcrumbs based on taxonomy hierarchy.
|
||||
*
|
||||
|
||||
@@ -8,6 +8,7 @@ use Drupal\Core\Block\BlockBase;
|
||||
use Drupal\Core\Cache\Cache;
|
||||
use Drupal\Core\Entity\EntityInterface;
|
||||
use Drupal\Core\Entity\EntityTypeManagerInterface;
|
||||
use Drupal\Core\Entity\Query\QueryInterface;
|
||||
use Drupal\Core\Form\FormStateInterface;
|
||||
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
|
||||
use Drupal\Core\Routing\RouteMatchInterface;
|
||||
@@ -252,28 +253,24 @@ class StructuralPagesMenuBlock extends BlockBase implements ContainerFactoryPlug
|
||||
$visited[$current->id()] = TRUE;
|
||||
|
||||
if (!$current->hasField('field_parent_page') || $current->get('field_parent_page')->isEmpty()) {
|
||||
// Node without parent - check if it has a direct section.
|
||||
// Reached the root content_page: domain is in field_site_section (DER).
|
||||
if ($current->hasField('field_site_section') && !$current->get('field_site_section')->isEmpty()) {
|
||||
return $current->get('field_site_section')->entity;
|
||||
$fss = $current->get('field_site_section')->first();
|
||||
$domain_type = $fss->target_type ?? 'taxonomy_term';
|
||||
$domain_id = $fss->target_id;
|
||||
return $this->entityTypeManager->getStorage($domain_type)->load($domain_id);
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
$parent_field = $current->get('field_parent_page')->first();
|
||||
$parent_type = $parent_field->target_type ?? NULL;
|
||||
$parent_id = $parent_field->target_id ?? NULL;
|
||||
$parent_id = $parent_field->target_id ?? NULL;
|
||||
|
||||
if (!$parent_type || !$parent_id) {
|
||||
if (!$parent_id) {
|
||||
break;
|
||||
}
|
||||
|
||||
// If parent is not a node, it's our ancestor.
|
||||
if ($parent_type !== 'node') {
|
||||
$storage = $this->entityTypeManager->getStorage($parent_type);
|
||||
return $storage->load($parent_id);
|
||||
}
|
||||
|
||||
// Parent is a node, continue traversing.
|
||||
// field_parent_page always points to a node; traverse upward.
|
||||
$current = $this->entityTypeManager->getStorage('node')->load($parent_id);
|
||||
}
|
||||
|
||||
@@ -368,6 +365,43 @@ class StructuralPagesMenuBlock extends BlockBase implements ContainerFactoryPlug
|
||||
return $branch;
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies domain conditions to a content_page entity query.
|
||||
*
|
||||
* Handles both entity_reference (legacy) and dynamic_entity_reference (DER)
|
||||
* storage for field_site_section, so queries work before and after the
|
||||
* migration update hook (10011) is applied.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\Query\QueryInterface $query
|
||||
* The entity query to modify.
|
||||
* @param string $domain_type
|
||||
* The domain entity type (e.g. 'taxonomy_term', 'user').
|
||||
* @param int|string $domain_id
|
||||
* The domain entity ID.
|
||||
*/
|
||||
protected function applyDomainCondition(QueryInterface $query, string $domain_type, int|string $domain_id): void {
|
||||
$database = \Drupal::database();
|
||||
$schema = $database->schema();
|
||||
|
||||
if ($schema->fieldExists('node__field_site_section', 'field_site_section_target_type')) {
|
||||
// DER storage: use a direct DB query to avoid DER entity query issues
|
||||
// with the 'target_type' specifier in Drupal\Core\Entity\Query\Sql\Tables.
|
||||
$nids = $database->select('node__field_site_section', 'fss')
|
||||
->fields('fss', ['entity_id'])
|
||||
->condition('fss.field_site_section_target_type', $domain_type)
|
||||
->condition('fss.field_site_section_target_id', $domain_id)
|
||||
->condition('fss.deleted', 0)
|
||||
->execute()
|
||||
->fetchCol();
|
||||
|
||||
$query->condition('nid', empty($nids) ? [0] : $nids, 'IN');
|
||||
}
|
||||
else {
|
||||
// Legacy entity_reference (before update hook 10011).
|
||||
$query->condition('field_site_section', $domain_id);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets child content pages for a given parent entity.
|
||||
*
|
||||
@@ -387,18 +421,15 @@ class StructuralPagesMenuBlock extends BlockBase implements ContainerFactoryPlug
|
||||
->condition('status', 1)
|
||||
->sort('title', 'ASC');
|
||||
|
||||
if ($parent_type === 'taxonomy_term') {
|
||||
// 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).
|
||||
$query->condition('field_parent_page.target_type', $parent_type);
|
||||
if ($parent_type === 'node') {
|
||||
// Child pages: field_parent_page points to this node.
|
||||
$query->condition('field_parent_page.target_id', $parent_id);
|
||||
}
|
||||
else {
|
||||
// Root pages: no parent page, but field_site_section matches the domain.
|
||||
$query->notExists('field_parent_page');
|
||||
$this->applyDomainCondition($query, $parent_type, $parent_id);
|
||||
}
|
||||
|
||||
$nids = $query->execute();
|
||||
|
||||
|
||||
@@ -335,7 +335,7 @@ function structural_pages_update_10006(): string {
|
||||
->getEditable('field.field.node.content_page.field_parent_page');
|
||||
|
||||
if ($config->isNew()) {
|
||||
return t('Campo field_parent_page não encontrado — nada a corrigir.');
|
||||
return 'Campo field_parent_page não encontrado — nada a corrigir.';
|
||||
}
|
||||
|
||||
// Re-aplica as settings completas a partir do YAML de install.
|
||||
@@ -343,14 +343,14 @@ function structural_pages_update_10006(): string {
|
||||
$yaml_file = $module_path . '/config/install/field.field.node.content_page.field_parent_page.yml';
|
||||
|
||||
if (!file_exists($yaml_file)) {
|
||||
return t('Arquivo YAML de install não encontrado.');
|
||||
return 'Arquivo YAML de install não encontrado.';
|
||||
}
|
||||
|
||||
$yaml_settings = \Symfony\Component\Yaml\Yaml::parse(file_get_contents($yaml_file));
|
||||
$config->set('settings', $yaml_settings['settings']);
|
||||
$config->save(TRUE);
|
||||
|
||||
return t('Settings de field_parent_page re-sincronizadas a partir do YAML de install.');
|
||||
return 'Settings de field_parent_page re-sincronizadas a partir do YAML de install.';
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -369,6 +369,446 @@ function structural_pages_update_10007(): string {
|
||||
return 'exclude_entity_types corrigido para FALSE em field_parent_page storage.';
|
||||
}
|
||||
|
||||
/**
|
||||
* Adiciona coluna field_parent_page_target_type ausente no schema do banco.
|
||||
*
|
||||
* O campo foi originalmente criado como entity_reference simples e depois
|
||||
* migrado para dynamic_entity_reference no config, mas a coluna target_type
|
||||
* nunca foi adicionada ao banco. Popula o tipo de cada linha consultando as
|
||||
* tabelas node e taxonomy_term para determinar o tipo correto da referência.
|
||||
*/
|
||||
function structural_pages_update_10008(): string {
|
||||
$database = \Drupal::database();
|
||||
$schema = $database->schema();
|
||||
|
||||
$column_spec = [
|
||||
'type' => 'varchar_ascii',
|
||||
'length' => 128,
|
||||
'not null' => FALSE,
|
||||
'description' => 'The entity type of the field_parent_page reference.',
|
||||
];
|
||||
|
||||
$tables = [
|
||||
'node__field_parent_page',
|
||||
'node_revision__field_parent_page',
|
||||
];
|
||||
|
||||
$updated = [];
|
||||
|
||||
foreach ($tables as $table) {
|
||||
if (!$schema->tableExists($table)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!$schema->fieldExists($table, 'field_parent_page_target_type')) {
|
||||
$schema->addField($table, 'field_parent_page_target_type', $column_spec);
|
||||
}
|
||||
|
||||
// Determina o tipo de cada referência existente comparando o target_id
|
||||
// com os IDs existentes em node e taxonomy_term.
|
||||
$rows = $database->select($table, 't')
|
||||
->fields('t', ['entity_id', 'revision_id', 'field_parent_page_target_id'])
|
||||
->isNull('field_parent_page_target_type')
|
||||
->execute()
|
||||
->fetchAll();
|
||||
|
||||
foreach ($rows as $row) {
|
||||
$target_id = $row->field_parent_page_target_id;
|
||||
|
||||
$is_node = (bool) $database->select('node', 'n')
|
||||
->fields('n', ['nid'])
|
||||
->condition('n.nid', $target_id)
|
||||
->countQuery()
|
||||
->execute()
|
||||
->fetchField();
|
||||
|
||||
$target_type = $is_node ? 'node' : 'taxonomy_term';
|
||||
|
||||
$database->update($table)
|
||||
->fields(['field_parent_page_target_type' => $target_type])
|
||||
->condition('entity_id', $row->entity_id)
|
||||
->condition('revision_id', $row->revision_id)
|
||||
->execute();
|
||||
}
|
||||
|
||||
$updated[] = $table;
|
||||
}
|
||||
|
||||
if (empty($updated)) {
|
||||
return 'Tabelas não encontradas — nada a corrigir.';
|
||||
}
|
||||
|
||||
return 'Coluna field_parent_page_target_type adicionada e populada em: ' . implode(', ', $updated) . '.';
|
||||
}
|
||||
|
||||
/**
|
||||
* Define entity_type_ids no storage do field_parent_page para node e taxonomy_term.
|
||||
*
|
||||
* Com entity_type_ids vazio e exclude_entity_types FALSE, o widget DER não
|
||||
* conseguia determinar quais entity types exibir e deixava o campo em branco.
|
||||
*/
|
||||
function structural_pages_update_10009(): string {
|
||||
$config = \Drupal::configFactory()
|
||||
->getEditable('field.storage.node.field_parent_page');
|
||||
if ($config->isNew()) {
|
||||
return 'field.storage.node.field_parent_page não encontrado.';
|
||||
}
|
||||
$config->set('settings.entity_type_ids', ['node', 'taxonomy_term'])->save(TRUE);
|
||||
return 'entity_type_ids definido como [node, taxonomy_term] em field_parent_page storage.';
|
||||
}
|
||||
|
||||
/**
|
||||
* Adiciona suporte ao entity type user no campo field_parent_page.
|
||||
*
|
||||
* Atualiza o storage (entity_type_ids) e a instância do campo (handler settings
|
||||
* para user) para que o DER module possa referenciar usuários como pai de
|
||||
* content_page nodes.
|
||||
*/
|
||||
function structural_pages_update_10010(): string {
|
||||
$messages = [];
|
||||
|
||||
// 1. Storage: adiciona 'user' a entity_type_ids.
|
||||
$storage_config = \Drupal::configFactory()
|
||||
->getEditable('field.storage.node.field_parent_page');
|
||||
if (!$storage_config->isNew()) {
|
||||
$entity_type_ids = $storage_config->get('settings.entity_type_ids') ?? [];
|
||||
if (!in_array('user', $entity_type_ids, TRUE)) {
|
||||
$entity_type_ids[] = 'user';
|
||||
$storage_config->set('settings.entity_type_ids', $entity_type_ids)->save(TRUE);
|
||||
$messages[] = 'Storage atualizado: user adicionado a entity_type_ids.';
|
||||
}
|
||||
else {
|
||||
$messages[] = 'Storage: user já presente em entity_type_ids.';
|
||||
}
|
||||
}
|
||||
else {
|
||||
$messages[] = 'field.storage.node.field_parent_page não encontrado.';
|
||||
}
|
||||
|
||||
// 2. Instância: adiciona handler settings para user.
|
||||
$field_config = \Drupal::configFactory()
|
||||
->getEditable('field.field.node.content_page.field_parent_page');
|
||||
if (!$field_config->isNew()) {
|
||||
$settings = $field_config->get('settings') ?? [];
|
||||
$type_ids = $settings['entity_type_ids'] ?? [];
|
||||
if (!in_array('user', $type_ids, TRUE)) {
|
||||
$type_ids[] = 'user';
|
||||
$settings['entity_type_ids'] = $type_ids;
|
||||
}
|
||||
if (!isset($settings['user'])) {
|
||||
$settings['user'] = [
|
||||
'handler' => 'default:user',
|
||||
'handler_settings' => [
|
||||
'include_anonymous' => FALSE,
|
||||
'filter' => ['type' => '_none'],
|
||||
'target_bundles' => NULL,
|
||||
'sort' => ['field' => 'name', 'direction' => 'asc'],
|
||||
'auto_create' => FALSE,
|
||||
],
|
||||
];
|
||||
}
|
||||
$field_config->set('settings', $settings)->save(TRUE);
|
||||
$messages[] = 'Instância atualizada: handler settings de user adicionadas.';
|
||||
}
|
||||
else {
|
||||
$messages[] = 'field.field.node.content_page.field_parent_page não encontrado.';
|
||||
}
|
||||
|
||||
return implode(' ', $messages);
|
||||
}
|
||||
|
||||
/**
|
||||
* Migra field_site_section de entity_reference para dynamic_entity_reference.
|
||||
*
|
||||
* O campo precisa armazenar o domínio raiz da página (taxonomy_term OU user),
|
||||
* por isso deve se tornar um campo DER em vez de entity_reference simples.
|
||||
*
|
||||
* Passos:
|
||||
* 1. Adiciona coluna target_type nas tabelas do campo.
|
||||
* 2. Preenche todas as linhas existentes com target_type = 'taxonomy_term'.
|
||||
* 3. Atualiza o storage config para dynamic_entity_reference.
|
||||
* 4. Atualiza as instâncias (content_page e section_page).
|
||||
*/
|
||||
function structural_pages_update_10011(): string {
|
||||
$database = \Drupal::database();
|
||||
$schema = $database->schema();
|
||||
$messages = [];
|
||||
|
||||
$column_spec = [
|
||||
'type' => 'varchar_ascii',
|
||||
'length' => 128,
|
||||
'not null' => FALSE,
|
||||
'description' => 'The entity type of the field_site_section reference.',
|
||||
];
|
||||
|
||||
foreach (['node__field_site_section', 'node_revision__field_site_section'] as $table) {
|
||||
if (!$schema->tableExists($table)) {
|
||||
continue;
|
||||
}
|
||||
if (!$schema->fieldExists($table, 'field_site_section_target_type')) {
|
||||
$schema->addField($table, 'field_site_section_target_type', $column_spec);
|
||||
// Populate existing rows as taxonomy_term.
|
||||
$database->update($table)
|
||||
->fields(['field_site_section_target_type' => 'taxonomy_term'])
|
||||
->isNull('field_site_section_target_type')
|
||||
->execute();
|
||||
$messages[] = "Coluna target_type adicionada em $table.";
|
||||
}
|
||||
else {
|
||||
$messages[] = "Coluna target_type já existe em $table.";
|
||||
}
|
||||
}
|
||||
|
||||
// Update storage config.
|
||||
$storage = \Drupal::configFactory()->getEditable('field.storage.node.field_site_section');
|
||||
if (!$storage->isNew()) {
|
||||
$storage->set('type', 'dynamic_entity_reference');
|
||||
$storage->set('module', 'dynamic_entity_reference');
|
||||
$storage->set('settings', [
|
||||
'exclude_entity_types' => FALSE,
|
||||
'entity_type_ids' => ['taxonomy_term', 'user'],
|
||||
]);
|
||||
$deps = $storage->get('dependencies') ?? [];
|
||||
$mods = $deps['module'] ?? [];
|
||||
if (!in_array('dynamic_entity_reference', $mods, TRUE)) {
|
||||
$mods[] = 'dynamic_entity_reference';
|
||||
}
|
||||
$deps['module'] = $mods;
|
||||
$storage->set('dependencies', $deps);
|
||||
$storage->save(TRUE);
|
||||
$messages[] = 'Storage config atualizado para dynamic_entity_reference.';
|
||||
}
|
||||
|
||||
// Update content_page instance config.
|
||||
$field_cp = \Drupal::configFactory()->getEditable('field.field.node.content_page.field_site_section');
|
||||
if (!$field_cp->isNew()) {
|
||||
$field_cp->set('field_type', 'dynamic_entity_reference');
|
||||
$field_cp->set('settings', [
|
||||
'entity_type_ids' => ['taxonomy_term', 'user'],
|
||||
'taxonomy_term' => [
|
||||
'handler' => 'default:taxonomy_term',
|
||||
'handler_settings' => [
|
||||
'target_bundles' => ['site_sections' => 'site_sections'],
|
||||
'sort' => ['field' => 'name', 'direction' => 'asc'],
|
||||
'auto_create' => FALSE,
|
||||
],
|
||||
],
|
||||
'user' => [
|
||||
'handler' => 'default:user',
|
||||
'handler_settings' => [
|
||||
'include_anonymous' => FALSE,
|
||||
'filter' => ['type' => '_none'],
|
||||
'target_bundles' => NULL,
|
||||
'sort' => ['field' => 'name', 'direction' => 'asc'],
|
||||
'auto_create' => FALSE,
|
||||
],
|
||||
],
|
||||
]);
|
||||
$deps = $field_cp->get('dependencies') ?? [];
|
||||
$mods = $deps['module'] ?? [];
|
||||
if (!in_array('dynamic_entity_reference', $mods, TRUE)) {
|
||||
$mods[] = 'dynamic_entity_reference';
|
||||
}
|
||||
$deps['module'] = $mods;
|
||||
$field_cp->set('dependencies', $deps);
|
||||
$field_cp->save(TRUE);
|
||||
$messages[] = 'Instância content_page atualizada.';
|
||||
}
|
||||
|
||||
// Update section_page instance config (taxonomy_term only).
|
||||
$field_sp = \Drupal::configFactory()->getEditable('field.field.node.section_page.field_site_section');
|
||||
if (!$field_sp->isNew()) {
|
||||
$field_sp->set('field_type', 'dynamic_entity_reference');
|
||||
$field_sp->set('settings', [
|
||||
'entity_type_ids' => ['taxonomy_term'],
|
||||
'taxonomy_term' => [
|
||||
'handler' => 'default:taxonomy_term',
|
||||
'handler_settings' => [
|
||||
'target_bundles' => ['site_sections' => 'site_sections'],
|
||||
'sort' => ['field' => 'name', 'direction' => 'asc'],
|
||||
'auto_create' => FALSE,
|
||||
],
|
||||
],
|
||||
]);
|
||||
$deps = $field_sp->get('dependencies') ?? [];
|
||||
$mods = $deps['module'] ?? [];
|
||||
if (!in_array('dynamic_entity_reference', $mods, TRUE)) {
|
||||
$mods[] = 'dynamic_entity_reference';
|
||||
}
|
||||
$deps['module'] = $mods;
|
||||
$field_sp->set('dependencies', $deps);
|
||||
$field_sp->save(TRUE);
|
||||
$messages[] = 'Instância section_page atualizada.';
|
||||
}
|
||||
|
||||
// Rebuild field definitions cache.
|
||||
\Drupal::service('entity_field.manager')->clearCachedFieldDefinitions();
|
||||
|
||||
return implode(' ', $messages);
|
||||
}
|
||||
|
||||
/**
|
||||
* Restringe field_parent_page a content_page nodes e limpa referências inválidas.
|
||||
*
|
||||
* Na arquitetura anterior, field_parent_page podia apontar para taxonomy_term
|
||||
* ou user (como raiz do domínio). Na nova arquitetura, field_site_section é o
|
||||
* portador do domínio raiz, e field_parent_page aponta somente para
|
||||
* content_page nodes pai (ou é nulo para páginas raiz).
|
||||
*
|
||||
* Passos:
|
||||
* 1. Zera linhas onde target_type != 'node' (eram raízes de domínio).
|
||||
* 2. Atualiza storage e instância para entity_type_ids: [node].
|
||||
*/
|
||||
function structural_pages_update_10012(): string {
|
||||
$database = \Drupal::database();
|
||||
$schema = $database->schema();
|
||||
$messages = [];
|
||||
|
||||
foreach (['node__field_parent_page', 'node_revision__field_parent_page'] as $table) {
|
||||
if (!$schema->tableExists($table)) {
|
||||
continue;
|
||||
}
|
||||
// Clear rows that pointed to non-node entities (taxonomy_term, user, etc.).
|
||||
$deleted = $database->delete($table)
|
||||
->condition('field_parent_page_target_type', 'node', '<>')
|
||||
->execute();
|
||||
if ($deleted) {
|
||||
$messages[] = "Removidas $deleted linhas não-node em $table.";
|
||||
}
|
||||
}
|
||||
|
||||
// Update storage config.
|
||||
$storage = \Drupal::configFactory()->getEditable('field.storage.node.field_parent_page');
|
||||
if (!$storage->isNew()) {
|
||||
$storage->set('settings.entity_type_ids', ['node'])->save(TRUE);
|
||||
$messages[] = 'Storage field_parent_page restrito a [node].';
|
||||
}
|
||||
|
||||
// Update content_page instance config.
|
||||
$field_cp = \Drupal::configFactory()->getEditable('field.field.node.content_page.field_parent_page');
|
||||
if (!$field_cp->isNew()) {
|
||||
$field_cp->set('settings', [
|
||||
'entity_type_ids' => ['node'],
|
||||
'node' => [
|
||||
'handler' => 'default:node',
|
||||
'handler_settings' => [
|
||||
'target_bundles' => ['content_page' => 'content_page'],
|
||||
'sort' => ['field' => 'title', 'direction' => 'asc'],
|
||||
'auto_create' => FALSE,
|
||||
],
|
||||
],
|
||||
]);
|
||||
// Remove dependencies no longer needed (taxonomy, user).
|
||||
$deps = $field_cp->get('dependencies') ?? [];
|
||||
$config_deps = $deps['config'] ?? [];
|
||||
$config_deps = array_filter($config_deps, fn($c) => !str_contains($c, 'taxonomy.vocabulary') && !str_contains($c, 'node.type.section_page'));
|
||||
$deps['config'] = array_values($config_deps);
|
||||
$field_cp->set('dependencies', $deps);
|
||||
$field_cp->save(TRUE);
|
||||
$messages[] = 'Instância field_parent_page restrita a content_page.';
|
||||
}
|
||||
|
||||
\Drupal::service('entity_field.manager')->clearCachedFieldDefinitions();
|
||||
|
||||
return empty($messages) ? 'Nada a migrar.' : implode(' ', $messages);
|
||||
}
|
||||
|
||||
/**
|
||||
* Corrige o tipo do storage field_parent_page para dynamic_entity_reference.
|
||||
*
|
||||
* Os update hooks anteriores (10006–10012) alteraram apenas as settings do
|
||||
* campo (entity_type_ids, exclude_entity_types) sem mudar o campo 'type' de
|
||||
* 'entity_reference' para 'dynamic_entity_reference'. Isso fazia com que o
|
||||
* sistema de entity queries do Drupal rejeitasse o especificador 'target_type'
|
||||
* com "Invalid specifier 'target_type'".
|
||||
*
|
||||
* Além disso, se o módulo event_management estiver instalado, adiciona 'event'
|
||||
* ao entity_type_ids do field_site_section para que content_pages possam
|
||||
* referenciar eventos como domínio raiz.
|
||||
*/
|
||||
function structural_pages_update_10013(): string {
|
||||
$messages = [];
|
||||
|
||||
// 1. Migra field_parent_page de entity_reference para dynamic_entity_reference.
|
||||
$storage = \Drupal::configFactory()->getEditable('field.storage.node.field_parent_page');
|
||||
if (!$storage->isNew() && $storage->get('type') !== 'dynamic_entity_reference') {
|
||||
$database = \Drupal::database();
|
||||
$schema = $database->schema();
|
||||
$col_spec = [
|
||||
'type' => 'varchar_ascii',
|
||||
'length' => 128,
|
||||
'not null' => FALSE,
|
||||
'description' => 'Entity type of the field_parent_page reference.',
|
||||
];
|
||||
foreach (['node__field_parent_page', 'node_revision__field_parent_page'] as $table) {
|
||||
if ($schema->tableExists($table) && !$schema->fieldExists($table, 'field_parent_page_target_type')) {
|
||||
$schema->addField($table, 'field_parent_page_target_type', $col_spec);
|
||||
$database->update($table)
|
||||
->fields(['field_parent_page_target_type' => 'node'])
|
||||
->isNull('field_parent_page_target_type')
|
||||
->execute();
|
||||
}
|
||||
}
|
||||
$storage->set('type', 'dynamic_entity_reference');
|
||||
$storage->set('module', 'dynamic_entity_reference');
|
||||
$deps = $storage->get('dependencies') ?? [];
|
||||
$mods = $deps['module'] ?? [];
|
||||
if (!in_array('dynamic_entity_reference', $mods, TRUE)) {
|
||||
$mods[] = 'dynamic_entity_reference';
|
||||
$deps['module'] = $mods;
|
||||
$storage->set('dependencies', $deps);
|
||||
}
|
||||
$storage->save(TRUE);
|
||||
$messages[] = 'field_parent_page migrado para dynamic_entity_reference.';
|
||||
}
|
||||
else {
|
||||
$messages[] = 'field_parent_page já é dynamic_entity_reference.';
|
||||
}
|
||||
|
||||
// 2. Adiciona 'event' ao field_site_section se event_management estiver instalado.
|
||||
if (\Drupal::moduleHandler()->moduleExists('event_management')) {
|
||||
$fss_storage = \Drupal::configFactory()->getEditable('field.storage.node.field_site_section');
|
||||
if (!$fss_storage->isNew() && $fss_storage->get('type') === 'dynamic_entity_reference') {
|
||||
$ids = $fss_storage->get('settings.entity_type_ids') ?? [];
|
||||
if (!in_array('event', $ids, TRUE)) {
|
||||
$ids[] = 'event';
|
||||
$fss_storage->set('settings.entity_type_ids', $ids)->save(TRUE);
|
||||
$messages[] = "'event' adicionado ao entity_type_ids de field_site_section.";
|
||||
}
|
||||
else {
|
||||
$messages[] = "'event' já presente no entity_type_ids de field_site_section.";
|
||||
}
|
||||
}
|
||||
|
||||
// Adiciona handler settings de event na instância content_page.
|
||||
$field_cp = \Drupal::configFactory()->getEditable('field.field.node.content_page.field_site_section');
|
||||
if (!$field_cp->isNew() && $field_cp->get('field_type') === 'dynamic_entity_reference') {
|
||||
$settings = $field_cp->get('settings') ?? [];
|
||||
$type_ids = $settings['entity_type_ids'] ?? [];
|
||||
if (!in_array('event', $type_ids, TRUE)) {
|
||||
$type_ids[] = 'event';
|
||||
$settings['entity_type_ids'] = $type_ids;
|
||||
}
|
||||
if (!isset($settings['event'])) {
|
||||
$settings['event'] = [
|
||||
'handler' => 'default:event',
|
||||
'handler_settings' => [
|
||||
'filter' => ['type' => '_none'],
|
||||
'sort' => ['field' => 'title', 'direction' => 'asc'],
|
||||
'auto_create' => FALSE,
|
||||
],
|
||||
];
|
||||
}
|
||||
$field_cp->set('settings', $settings)->save(TRUE);
|
||||
$messages[] = "Handler settings de event adicionadas ao field_site_section de content_page.";
|
||||
}
|
||||
}
|
||||
|
||||
\Drupal::service('entity_field.manager')->clearCachedFieldDefinitions();
|
||||
|
||||
return implode(' ', $messages);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_requirements().
|
||||
*/
|
||||
|
||||
@@ -10,20 +10,20 @@ declare(strict_types=1);
|
||||
use Drupal\Core\Entity\EntityInterface;
|
||||
use Drupal\node\NodeInterface;
|
||||
use Drupal\taxonomy\TermInterface;
|
||||
use Drupal\user\UserInterface;
|
||||
|
||||
/**
|
||||
* Implements hook_entity_presave().
|
||||
*
|
||||
* Inherits field_site_section from parent to content_page.
|
||||
* Validates circular reference in field_parent_page.
|
||||
* For content_page nodes with a node parent: inherits field_site_section from
|
||||
* the parent content_page. For root pages (no parent) field_site_section is
|
||||
* set directly by the user in the form and is left untouched here.
|
||||
*/
|
||||
function structural_pages_entity_presave(EntityInterface $entity): void {
|
||||
if (!$entity instanceof NodeInterface || $entity->bundle() !== 'content_page') {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if has parent page.
|
||||
// Root page: field_site_section is managed by the editor, nothing to inherit.
|
||||
if (!$entity->hasField('field_parent_page') || $entity->get('field_parent_page')->isEmpty()) {
|
||||
return;
|
||||
}
|
||||
@@ -33,38 +33,35 @@ function structural_pages_entity_presave(EntityInterface $entity): void {
|
||||
return;
|
||||
}
|
||||
|
||||
$parent_entity_type = $parent_field->target_type ?? NULL;
|
||||
$parent_id = $parent_field->target_id ?? NULL;
|
||||
|
||||
if (!$parent_entity_type || !$parent_id) {
|
||||
if (!$parent_id) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Circular reference validation (only for node parents).
|
||||
if ($parent_entity_type === 'node' && !$entity->isNew() && $parent_id) {
|
||||
// Circular reference validation.
|
||||
if (!$entity->isNew()) {
|
||||
if (_structural_pages_creates_circular_reference($entity->id(), $parent_id)) {
|
||||
\Drupal::messenger()->addError(t('Circular reference detected. A page cannot be a parent of itself or its ancestors.'));
|
||||
// Remove invalid reference.
|
||||
$entity->set('field_parent_page', NULL);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle site section based on parent type.
|
||||
// Some entity types (like user and group) act as context containers themselves.
|
||||
/** @var \Drupal\structural_pages\ParentEntityHandler\ParentEntityHandlerManagerInterface $handler_manager */
|
||||
$handler_manager = \Drupal::service('plugin.manager.parent_entity_handler');
|
||||
if ($handler_manager->clearsSiteSection($parent_entity_type)) {
|
||||
// Clear field_site_section as the context is the parent entity, not a site section.
|
||||
$entity->set('field_site_section', NULL);
|
||||
// Inherit field_site_section (DER value: target_type + target_id) from the
|
||||
// parent content_page so sibling pages always share the same domain root.
|
||||
$parent_node = \Drupal::entityTypeManager()->getStorage('node')->load($parent_id);
|
||||
if (!$parent_node instanceof NodeInterface) {
|
||||
return;
|
||||
}
|
||||
if (!$parent_node->hasField('field_site_section') || $parent_node->get('field_site_section')->isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Inherit field_site_section based on parent type (node or taxonomy_term).
|
||||
$site_section_id = _structural_pages_get_section_from_parent($parent_entity_type, $parent_id);
|
||||
if ($site_section_id) {
|
||||
$entity->set('field_site_section', $site_section_id);
|
||||
}
|
||||
$parent_fss = $parent_node->get('field_site_section')->first();
|
||||
$entity->set('field_site_section', [
|
||||
'target_type' => $parent_fss->target_type ?? 'taxonomy_term',
|
||||
'target_id' => $parent_fss->target_id,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -78,84 +75,6 @@ function _structural_pages_get_vocabulary(): string {
|
||||
->get('site_section_vocabulary') ?? 'site_sections';
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the site section ID from a parent entity.
|
||||
*
|
||||
* @param string $parent_entity_type
|
||||
* The parent entity type (node or taxonomy_term).
|
||||
* @param int|string $parent_id
|
||||
* The parent entity ID.
|
||||
*
|
||||
* @return int|string|null
|
||||
* The site section term ID, or NULL if not found.
|
||||
*/
|
||||
function _structural_pages_get_section_from_parent(string $parent_entity_type, int|string $parent_id): int|string|null {
|
||||
$entity_type_manager = \Drupal::entityTypeManager();
|
||||
|
||||
if ($parent_entity_type === 'taxonomy_term') {
|
||||
// If parent is a taxonomy term, verify it's from the configured vocabulary.
|
||||
$term = $entity_type_manager->getStorage('taxonomy_term')->load($parent_id);
|
||||
if ($term instanceof TermInterface && $term->bundle() === _structural_pages_get_vocabulary()) {
|
||||
return $term->id();
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if ($parent_entity_type === 'node') {
|
||||
$parent_node = $entity_type_manager->getStorage('node')->load($parent_id);
|
||||
if (!$parent_node instanceof NodeInterface) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// If parent has field_site_section, use it.
|
||||
if ($parent_node->hasField('field_site_section') && !$parent_node->get('field_site_section')->isEmpty()) {
|
||||
return $parent_node->get('field_site_section')->target_id;
|
||||
}
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the parent context information for a content_page.
|
||||
*
|
||||
* @param \Drupal\node\NodeInterface $node
|
||||
* The content_page node.
|
||||
*
|
||||
* @return array|null
|
||||
* An array with 'type' and 'entity' keys, or NULL if no parent.
|
||||
*/
|
||||
function _structural_pages_get_parent_context(NodeInterface $node): ?array {
|
||||
if (!$node->hasField('field_parent_page') || $node->get('field_parent_page')->isEmpty()) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
$parent_field = $node->get('field_parent_page')->first();
|
||||
if (!$parent_field) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
$parent_entity_type = $parent_field->target_type ?? NULL;
|
||||
$parent_id = $parent_field->target_id ?? NULL;
|
||||
|
||||
if (!$parent_entity_type || !$parent_id) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
$parent = \Drupal::entityTypeManager()
|
||||
->getStorage($parent_entity_type)
|
||||
->load($parent_id);
|
||||
|
||||
if (!$parent) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
return [
|
||||
'type' => $parent_entity_type,
|
||||
'entity' => $parent,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if setting parent_id as parent of node_id would create circular reference.
|
||||
*
|
||||
@@ -288,7 +207,13 @@ function _structural_pages_get_section_path(NodeInterface $node): string {
|
||||
return '';
|
||||
}
|
||||
|
||||
$term_id = $node->get('field_site_section')->target_id;
|
||||
$fss = $node->get('field_site_section')->first();
|
||||
// Only taxonomy_term domains have a hierarchical term path.
|
||||
if (($fss->target_type ?? 'taxonomy_term') !== 'taxonomy_term') {
|
||||
return '';
|
||||
}
|
||||
|
||||
$term_id = $fss->target_id;
|
||||
$term_storage = \Drupal::entityTypeManager()->getStorage('taxonomy_term');
|
||||
$term = $term_storage->load($term_id);
|
||||
|
||||
@@ -350,121 +275,191 @@ function structural_pages_form_node_content_page_edit_form_alter(&$form, \Drupal
|
||||
|
||||
/**
|
||||
* Helper to alter the content_page forms for parent page filtering.
|
||||
*
|
||||
* Architecture:
|
||||
* - field_site_section (DER): domain root — taxonomy_term, user, or event.
|
||||
* - field_parent_page (DER, node-only): parent content_page within the domain,
|
||||
* or empty for a root page.
|
||||
*
|
||||
* The native field_parent_page widget is replaced by a custom hierarchical
|
||||
* select that lists only content_pages sharing the same field_site_section
|
||||
* value. A sentinel option '__root__' represents an empty parent (root page).
|
||||
* Query params ?domain_type=<type>&domain_id=<id> (or the aliases
|
||||
* ?parent_section_type=<type>&parent_id=<id>) pre-fill field_site_section
|
||||
* when creating a new page from an external domain context (e.g., user).
|
||||
*/
|
||||
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.
|
||||
// Hide the native Drupal menu settings tab.
|
||||
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',
|
||||
];
|
||||
if (!isset($form['field_site_section']) || !isset($form['field_parent_page'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
$current_entity = $form_state->getFormObject()->getEntity();
|
||||
|
||||
// Pre-fill field_site_section for new nodes created from a domain context.
|
||||
if ($current_entity->isNew()) {
|
||||
$request = \Drupal::request();
|
||||
$qp_type = $request->query->get('domain_type') ?? $request->query->get('parent_section_type');
|
||||
$qp_id = $request->query->get('domain_id') ?? $request->query->get('parent_id');
|
||||
if ($qp_type && $qp_id && $qp_type !== 'node') {
|
||||
$domain_entity = \Drupal::entityTypeManager()->getStorage($qp_type)->load($qp_id);
|
||||
if ($domain_entity) {
|
||||
// Set the entity value (used on AJAX rebuild and on save).
|
||||
$current_entity->set('field_site_section', [
|
||||
'target_type' => $qp_type,
|
||||
'target_id' => $qp_id,
|
||||
]);
|
||||
// Also update the widget elements directly: hook_form_alter runs after
|
||||
// the DER widget has already been built from the original (empty) entity,
|
||||
// so #default_value in the rendered elements needs to be set explicitly.
|
||||
if (isset($form['field_site_section']['widget'][0]['target_type'])) {
|
||||
$form['field_site_section']['widget'][0]['target_type']['#default_value'] = $qp_type;
|
||||
}
|
||||
if (isset($form['field_site_section']['widget'][0]['target_id'])) {
|
||||
$form['field_site_section']['widget'][0]['target_id']['#default_value'] = $domain_entity;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add AJAX so changing field_site_section rebuilds the parent page options.
|
||||
$ajax = [
|
||||
'callback' => 'structural_pages_parent_page_ajax_callback',
|
||||
'wrapper' => 'parent-page-wrapper',
|
||||
'event' => 'change',
|
||||
];
|
||||
if (isset($form['field_site_section']['widget'][0]['target_type'])) {
|
||||
$form['field_site_section']['widget'][0]['target_type']['#ajax'] = $ajax;
|
||||
}
|
||||
if (isset($form['field_site_section']['widget'][0]['target_id'])) {
|
||||
$form['field_site_section']['widget'][0]['target_id']['#ajax'] = $ajax;
|
||||
}
|
||||
// Fallback for single-type widgets (entity_reference-style DER or pre-DER).
|
||||
if (!isset($form['field_site_section']['widget'][0]['target_type']) &&
|
||||
!isset($form['field_site_section']['widget'][0]['target_id'])) {
|
||||
$form['field_site_section']['widget']['#ajax'] = $ajax;
|
||||
}
|
||||
|
||||
// Wrapper container replaced by AJAX.
|
||||
$form['structural_pages_wrapper'] = [
|
||||
'#type' => 'container',
|
||||
'#attributes' => ['id' => 'parent-page-wrapper'],
|
||||
'#weight' => $form['field_parent_page']['#weight'] ?? 1,
|
||||
'#attached' => ['library' => ['structural_pages/select2']],
|
||||
];
|
||||
|
||||
// Hide the native DER widget for field_parent_page; we replace it below.
|
||||
$form['field_parent_page']['#access'] = FALSE;
|
||||
|
||||
// Determine the active domain (type + id) from field_site_section.
|
||||
[$domain_type, $domain_id] = _structural_pages_get_form_domain($form, $form_state, $current_entity);
|
||||
|
||||
if ($domain_type && $domain_id) {
|
||||
$domain_entity = \Drupal::entityTypeManager()->getStorage($domain_type)->load($domain_id);
|
||||
$domain_label = $domain_entity ? $domain_entity->label() : t('Domínio');
|
||||
|
||||
$current_nid = (!$current_entity->isNew()) ? (int) $current_entity->id() : NULL;
|
||||
$options = _structural_pages_build_parent_tree_options($domain_type, $domain_id, $current_nid);
|
||||
$options = ['__root__' => '< ' . t('Raiz (@label)', ['@label' => $domain_label]) . ' >'] + $options;
|
||||
|
||||
// Determine default selection.
|
||||
$default_parent = '__root__';
|
||||
if (!$current_entity->isNew() && !$current_entity->get('field_parent_page')->isEmpty()) {
|
||||
$pf = $current_entity->get('field_parent_page')->first();
|
||||
if ($pf && $pf->target_id) {
|
||||
$key = 'node:' . $pf->target_id;
|
||||
if (isset($options[$key])) {
|
||||
$default_parent = $key;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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',
|
||||
],
|
||||
$form['structural_pages_wrapper']['custom_parent_page'] = [
|
||||
'#type' => 'select',
|
||||
'#title' => t('Parent Page'),
|
||||
'#options' => $options,
|
||||
'#default_value' => $default_parent,
|
||||
'#description' => t('Select the parent page within this domain. Select Root to create a top-level page.'),
|
||||
'#attributes' => [
|
||||
'class' => ['select2-widget', 'chosen-enable'],
|
||||
'data-placeholder' => t('Search for a parent page...'),
|
||||
],
|
||||
];
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
// 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;
|
||||
|
||||
// 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('- 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'],
|
||||
'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>',
|
||||
];
|
||||
}
|
||||
array_unshift($form['#validate'], 'structural_pages_custom_parent_validate');
|
||||
}
|
||||
else {
|
||||
$form['structural_pages_wrapper']['custom_parent_page'] = [
|
||||
'#type' => 'markup',
|
||||
'#markup' => '<p><em>' . t('Por favor, selecione a Seção do Site primeiro.') . '</em></p>',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validation callback to map the select list back into dynamic_entity_reference.
|
||||
* Returns the domain [type, id] from the form state or the entity being edited.
|
||||
*
|
||||
* Priority:
|
||||
* 1. AJAX rebuild state (user just changed field_site_section).
|
||||
* 2. Saved field_site_section value on the entity being edited.
|
||||
* 3. Legacy widget #default_value (pre-DER fallback).
|
||||
*
|
||||
* @return array{0: string|null, 1: int|string|null}
|
||||
*/
|
||||
function _structural_pages_get_form_domain(array &$form, \Drupal\Core\Form\FormStateInterface $form_state, \Drupal\Core\Entity\EntityInterface $entity): array {
|
||||
// 1. AJAX state.
|
||||
$fss_value = $form_state->getValue('field_site_section');
|
||||
if (!empty($fss_value[0]['target_id'])) {
|
||||
return [
|
||||
$fss_value[0]['target_type'] ?? 'taxonomy_term',
|
||||
$fss_value[0]['target_id'],
|
||||
];
|
||||
}
|
||||
|
||||
// 2. Saved entity value.
|
||||
if ($entity->hasField('field_site_section') && !$entity->get('field_site_section')->isEmpty()) {
|
||||
$fss = $entity->get('field_site_section')->first();
|
||||
return [
|
||||
$fss->target_type ?? 'taxonomy_term',
|
||||
$fss->target_id,
|
||||
];
|
||||
}
|
||||
|
||||
// 3. Legacy widget default value (entity_reference before DER migration).
|
||||
if (isset($form['field_site_section']['widget']['#default_value'])) {
|
||||
$default = $form['field_site_section']['widget']['#default_value'];
|
||||
if (is_array($default) && !empty($default[0])) {
|
||||
return ['taxonomy_term', $default[0]];
|
||||
}
|
||||
if (is_scalar($default) && $default) {
|
||||
return ['taxonomy_term', $default];
|
||||
}
|
||||
}
|
||||
|
||||
return [NULL, NULL];
|
||||
}
|
||||
|
||||
/**
|
||||
* Validation callback: maps the custom select back to field_parent_page.
|
||||
*
|
||||
* '__root__' or empty → field_parent_page = [] (root page, no parent).
|
||||
* 'node:<id>' → field_parent_page = [{target_id: id, target_type: node}].
|
||||
*/
|
||||
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 {
|
||||
if (empty($custom_val) || $custom_val === '__root__') {
|
||||
$form_state->setValue('field_parent_page', []);
|
||||
}
|
||||
else {
|
||||
[, $id] = explode(':', $custom_val, 2);
|
||||
$form_state->setValue('field_parent_page', [
|
||||
['target_id' => $id, 'target_type' => 'node'],
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -475,74 +470,118 @@ function structural_pages_parent_page_ajax_callback(array &$form, \Drupal\Core\F
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a hierarchical tree array of content_pages for a given section.
|
||||
* Builds a hierarchical tree array of content_pages for a given domain.
|
||||
*
|
||||
* Queries all content_pages with field_site_section matching [$domain_type,
|
||||
* $domain_id], then arranges them in a tree keyed as 'node:<nid>'.
|
||||
*
|
||||
* @param string $domain_type
|
||||
* The domain entity type (e.g. 'taxonomy_term', 'user').
|
||||
* @param int|string $domain_id
|
||||
* The domain entity ID.
|
||||
* @param int|string|null $current_node_id
|
||||
* The node being edited (excluded from options to prevent self-reference).
|
||||
*
|
||||
* @return array
|
||||
* Associative array 'node:<nid>' => indented title.
|
||||
*/
|
||||
function _structural_pages_build_parent_tree_options($site_section_id, $current_node_id = NULL) {
|
||||
function _structural_pages_build_parent_tree_options(string $domain_type, int|string $domain_id, $current_node_id = NULL): array {
|
||||
$options = [];
|
||||
|
||||
$query = \Drupal::entityQuery('node')
|
||||
->condition('type', 'content_page')
|
||||
->condition('field_site_section', $site_section_id)
|
||||
->accessCheck(TRUE)
|
||||
->sort('title', 'ASC');
|
||||
|
||||
_structural_pages_apply_domain_condition($query, $domain_type, $domain_id);
|
||||
|
||||
$nids = $query->execute();
|
||||
|
||||
|
||||
if (empty($nids)) {
|
||||
return $options;
|
||||
}
|
||||
|
||||
|
||||
$nodes = \Drupal::entityTypeManager()->getStorage('node')->loadMultiple($nids);
|
||||
|
||||
|
||||
$children = [];
|
||||
$roots = [];
|
||||
|
||||
$roots = [];
|
||||
|
||||
foreach ($nodes as $nid => $node) {
|
||||
if ($nid == $current_node_id) {
|
||||
continue; // Skip self
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
$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;
|
||||
$parent_id = $node->get('field_parent_page')->target_id;
|
||||
}
|
||||
|
||||
// If the parent is another content_page in our list, it's a child.
|
||||
if ($parent_type === 'node' && isset($nodes[$parent_id])) {
|
||||
|
||||
if ($parent_id && isset($nodes[$parent_id])) {
|
||||
$children[$parent_id][] = $nid;
|
||||
} else {
|
||||
// It's a root (parent is a taxonomy term, a section_page, or empty)
|
||||
}
|
||||
else {
|
||||
$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
|
||||
|
||||
usort($roots, fn($a, $b) => strcmp($nodes[$a]->getTitle(), $nodes[$b]->getTitle()));
|
||||
|
||||
$build_options = function ($nids, $depth) use (&$build_options, &$options, $nodes, $children) {
|
||||
$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());
|
||||
});
|
||||
usort($child_nids, fn($a, $b) => strcmp($nodes[$a]->getTitle(), $nodes[$b]->getTitle()));
|
||||
$build_options($child_nids, $depth + 1);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
$build_options($roots, 0);
|
||||
|
||||
|
||||
return $options;
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies domain conditions to a content_page entity query.
|
||||
*
|
||||
* Uses a direct database subquery on node__field_site_section to filter by
|
||||
* both target_type and target_id. This avoids relying on DER's entity query
|
||||
* integration (which may throw "Invalid specifier 'target_type'" depending on
|
||||
* the installed DER version and Drupal's internal caching).
|
||||
*
|
||||
* @param \Drupal\Core\Entity\Query\QueryInterface $query
|
||||
* The entity query to modify.
|
||||
* @param string $domain_type
|
||||
* The domain entity type (e.g. 'taxonomy_term', 'user').
|
||||
* @param int|string $domain_id
|
||||
* The domain entity ID.
|
||||
*/
|
||||
function _structural_pages_apply_domain_condition(\Drupal\Core\Entity\Query\QueryInterface $query, string $domain_type, int|string $domain_id): void {
|
||||
$database = \Drupal::database();
|
||||
$schema = $database->schema();
|
||||
|
||||
if ($schema->fieldExists('node__field_site_section', 'field_site_section_target_type')) {
|
||||
// DER storage: query the field table directly to filter by both columns.
|
||||
$nids = $database->select('node__field_site_section', 'fss')
|
||||
->fields('fss', ['entity_id'])
|
||||
->condition('fss.field_site_section_target_type', $domain_type)
|
||||
->condition('fss.field_site_section_target_id', $domain_id)
|
||||
->condition('fss.deleted', 0)
|
||||
->execute()
|
||||
->fetchCol();
|
||||
|
||||
// Add a condition that will never match if no results found.
|
||||
$query->condition('nid', empty($nids) ? [0] : $nids, 'IN');
|
||||
}
|
||||
else {
|
||||
// Legacy entity_reference storage (before update hook 10011 runs):
|
||||
// all references are taxonomy_terms, target_id alone is sufficient.
|
||||
$query->condition('field_site_section', $domain_id);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_preprocess_node().
|
||||
*/
|
||||
@@ -582,17 +621,18 @@ function structural_pages_views_post_execute(\Drupal\views\ViewExecutable $view)
|
||||
|
||||
if ($parent_id) {
|
||||
// Has a node parent. Siblings are nodes with the same parent.
|
||||
// field_parent_page only points to nodes (post-migration), so
|
||||
// filtering by target_id alone is sufficient.
|
||||
$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.
|
||||
$orGroup = $query->orConditionGroup()
|
||||
->notExists('field_parent_page')
|
||||
->condition('field_parent_page.target_type', 'taxonomy_term');
|
||||
$query->condition($orGroup);
|
||||
|
||||
$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);
|
||||
$fss = $current_node->get('field_site_section')->first();
|
||||
$domain_type = $fss->target_type ?? 'taxonomy_term';
|
||||
$domain_id = $fss->target_id;
|
||||
_structural_pages_apply_domain_condition($query, $domain_type, $domain_id);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user