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 9d13216..3b02613 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 @@ -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 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 bb29f10..80887d8 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 @@ -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 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 a18c965..efee91b 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 @@ -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 diff --git a/config/install/field.storage.node.field_parent_page.yml b/config/install/field.storage.node.field_parent_page.yml index e2057d1..61236e8 100644 --- a/config/install/field.storage.node.field_parent_page.yml +++ b/config/install/field.storage.node.field_parent_page.yml @@ -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 diff --git a/config/install/field.storage.node.field_site_section.yml b/config/install/field.storage.node.field_site_section.yml index 4417434..baa0a76 100644 --- a/config/install/field.storage.node.field_site_section.yml +++ b/config/install/field.storage.node.field_site_section.yml @@ -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 diff --git a/src/Breadcrumb/SectionBreadcrumbBuilder.php b/src/Breadcrumb/SectionBreadcrumbBuilder.php index c8153ba..9d4b5ed 100644 --- a/src/Breadcrumb/SectionBreadcrumbBuilder.php +++ b/src/Breadcrumb/SectionBreadcrumbBuilder.php @@ -91,17 +91,15 @@ class SectionBreadcrumbBuilder implements BreadcrumbBuilderInterface // Add "Home" as first item. $breadcrumb->addLink(Link::createFromRoute($this->t('Home'), '')); - // 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. * diff --git a/src/Plugin/Block/StructuralPagesMenuBlock.php b/src/Plugin/Block/StructuralPagesMenuBlock.php index 9ab8559..5032d4a 100644 --- a/src/Plugin/Block/StructuralPagesMenuBlock.php +++ b/src/Plugin/Block/StructuralPagesMenuBlock.php @@ -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(); diff --git a/structural_pages.install b/structural_pages.install index 58c58f2..93d9146 100644 --- a/structural_pages.install +++ b/structural_pages.install @@ -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(). */ diff --git a/structural_pages.module b/structural_pages.module index 5b62862..cab2f9a 100644 --- a/structural_pages.module +++ b/structural_pages.module @@ -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=&domain_id= (or the aliases + * ?parent_section_type=&parent_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' => '

' . t('Por favor, selecione uma Site Section primeiro.') . '

', - ]; - } + array_unshift($form['#validate'], 'structural_pages_custom_parent_validate'); + } + else { + $form['structural_pages_wrapper']['custom_parent_page'] = [ + '#type' => 'markup', + '#markup' => '

' . t('Por favor, selecione a Seção do Site primeiro.') . '

', + ]; } } /** - * 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:' → 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:'. + * + * @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:' => 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); } }