mirror of
https://gitlab.unicamp.br/infimecc_drupal11_modules/structural_pages.git
synced 2026-03-08 01:27:42 -03:00
Initial commit: Site Structure module for Drupal
Drupal module that provides hierarchical site structure management with support for sections, categories, and content items. Includes path aliases with tokens, breadcrumb integration, and admin interface. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,95 @@
|
||||
langcode: en
|
||||
status: true
|
||||
dependencies:
|
||||
config:
|
||||
- field.field.node.content_page.body
|
||||
- field.field.node.content_page.field_parent_page
|
||||
- field.field.node.content_page.field_site_section
|
||||
- node.type.content_page
|
||||
module:
|
||||
- dynamic_entity_reference
|
||||
- path
|
||||
- text
|
||||
id: node.content_page.default
|
||||
targetEntityType: node
|
||||
bundle: content_page
|
||||
mode: default
|
||||
content:
|
||||
body:
|
||||
type: text_textarea_with_summary
|
||||
weight: 2
|
||||
region: content
|
||||
settings:
|
||||
rows: 9
|
||||
summary_rows: 3
|
||||
placeholder: ''
|
||||
show_summary: false
|
||||
third_party_settings: { }
|
||||
created:
|
||||
type: datetime_timestamp
|
||||
weight: 10
|
||||
region: content
|
||||
settings: { }
|
||||
third_party_settings: { }
|
||||
field_parent_page:
|
||||
type: dynamic_entity_reference_default
|
||||
weight: 0
|
||||
region: content
|
||||
settings:
|
||||
match_operator: CONTAINS
|
||||
match_limit: 10
|
||||
size: 60
|
||||
placeholder: ''
|
||||
third_party_settings: { }
|
||||
field_site_section:
|
||||
type: options_select
|
||||
weight: 1
|
||||
region: content
|
||||
settings: { }
|
||||
third_party_settings: { }
|
||||
path:
|
||||
type: path
|
||||
weight: 30
|
||||
region: content
|
||||
settings: { }
|
||||
third_party_settings: { }
|
||||
promote:
|
||||
type: boolean_checkbox
|
||||
weight: 15
|
||||
region: content
|
||||
settings:
|
||||
display_label: true
|
||||
third_party_settings: { }
|
||||
status:
|
||||
type: boolean_checkbox
|
||||
weight: 120
|
||||
region: content
|
||||
settings:
|
||||
display_label: true
|
||||
third_party_settings: { }
|
||||
sticky:
|
||||
type: boolean_checkbox
|
||||
weight: 16
|
||||
region: content
|
||||
settings:
|
||||
display_label: true
|
||||
third_party_settings: { }
|
||||
title:
|
||||
type: string_textfield
|
||||
weight: -5
|
||||
region: content
|
||||
settings:
|
||||
size: 60
|
||||
placeholder: ''
|
||||
third_party_settings: { }
|
||||
uid:
|
||||
type: entity_reference_autocomplete
|
||||
weight: 5
|
||||
region: content
|
||||
settings:
|
||||
match_operator: CONTAINS
|
||||
match_limit: 10
|
||||
size: 60
|
||||
placeholder: ''
|
||||
third_party_settings: { }
|
||||
hidden: { }
|
||||
@@ -0,0 +1,83 @@
|
||||
langcode: en
|
||||
status: true
|
||||
dependencies:
|
||||
config:
|
||||
- field.field.node.section_page.body
|
||||
- field.field.node.section_page.field_site_section
|
||||
- node.type.section_page
|
||||
module:
|
||||
- path
|
||||
- text
|
||||
id: node.section_page.default
|
||||
targetEntityType: node
|
||||
bundle: section_page
|
||||
mode: default
|
||||
content:
|
||||
body:
|
||||
type: text_textarea_with_summary
|
||||
weight: 1
|
||||
region: content
|
||||
settings:
|
||||
rows: 9
|
||||
summary_rows: 3
|
||||
placeholder: ''
|
||||
show_summary: false
|
||||
third_party_settings: { }
|
||||
created:
|
||||
type: datetime_timestamp
|
||||
weight: 10
|
||||
region: content
|
||||
settings: { }
|
||||
third_party_settings: { }
|
||||
field_site_section:
|
||||
type: options_select
|
||||
weight: 0
|
||||
region: content
|
||||
settings: { }
|
||||
third_party_settings: { }
|
||||
path:
|
||||
type: path
|
||||
weight: 30
|
||||
region: content
|
||||
settings: { }
|
||||
third_party_settings: { }
|
||||
promote:
|
||||
type: boolean_checkbox
|
||||
weight: 15
|
||||
region: content
|
||||
settings:
|
||||
display_label: true
|
||||
third_party_settings: { }
|
||||
status:
|
||||
type: boolean_checkbox
|
||||
weight: 120
|
||||
region: content
|
||||
settings:
|
||||
display_label: true
|
||||
third_party_settings: { }
|
||||
sticky:
|
||||
type: boolean_checkbox
|
||||
weight: 16
|
||||
region: content
|
||||
settings:
|
||||
display_label: true
|
||||
third_party_settings: { }
|
||||
title:
|
||||
type: string_textfield
|
||||
weight: -5
|
||||
region: content
|
||||
settings:
|
||||
size: 60
|
||||
placeholder: ''
|
||||
third_party_settings: { }
|
||||
uid:
|
||||
type: entity_reference_autocomplete
|
||||
weight: 5
|
||||
region: content
|
||||
settings:
|
||||
match_operator: CONTAINS
|
||||
match_limit: 10
|
||||
size: 60
|
||||
placeholder: ''
|
||||
third_party_settings: { }
|
||||
hidden: { }
|
||||
@@ -0,0 +1,30 @@
|
||||
langcode: en
|
||||
status: true
|
||||
dependencies:
|
||||
config:
|
||||
- field.field.node.content_page.body
|
||||
- field.field.node.content_page.field_parent_page
|
||||
- field.field.node.content_page.field_site_section
|
||||
- node.type.content_page
|
||||
module:
|
||||
- text
|
||||
id: node.content_page.default
|
||||
targetEntityType: node
|
||||
bundle: content_page
|
||||
mode: default
|
||||
content:
|
||||
body:
|
||||
type: text_default
|
||||
label: hidden
|
||||
settings: { }
|
||||
third_party_settings: { }
|
||||
weight: 0
|
||||
region: content
|
||||
links:
|
||||
settings: { }
|
||||
third_party_settings: { }
|
||||
weight: 100
|
||||
region: content
|
||||
hidden:
|
||||
field_parent_page: true
|
||||
field_site_section: true
|
||||
@@ -0,0 +1,28 @@
|
||||
langcode: en
|
||||
status: true
|
||||
dependencies:
|
||||
config:
|
||||
- field.field.node.section_page.body
|
||||
- field.field.node.section_page.field_site_section
|
||||
- node.type.section_page
|
||||
module:
|
||||
- text
|
||||
id: node.section_page.default
|
||||
targetEntityType: node
|
||||
bundle: section_page
|
||||
mode: default
|
||||
content:
|
||||
body:
|
||||
type: text_default
|
||||
label: hidden
|
||||
settings: { }
|
||||
third_party_settings: { }
|
||||
weight: 0
|
||||
region: content
|
||||
links:
|
||||
settings: { }
|
||||
third_party_settings: { }
|
||||
weight: 100
|
||||
region: content
|
||||
hidden:
|
||||
field_site_section: true
|
||||
22
config/install/field.field.node.content_page.body.yml
Normal file
22
config/install/field.field.node.content_page.body.yml
Normal file
@@ -0,0 +1,22 @@
|
||||
langcode: en
|
||||
status: true
|
||||
dependencies:
|
||||
config:
|
||||
- field.storage.node.body
|
||||
- node.type.content_page
|
||||
module:
|
||||
- text
|
||||
id: node.content_page.body
|
||||
field_name: body
|
||||
entity_type: node
|
||||
bundle: content_page
|
||||
label: Body
|
||||
description: ''
|
||||
required: false
|
||||
translatable: true
|
||||
default_value: { }
|
||||
default_value_callback: ''
|
||||
settings:
|
||||
display_summary: true
|
||||
required_summary: false
|
||||
field_type: text_with_summary
|
||||
@@ -0,0 +1,44 @@
|
||||
langcode: en
|
||||
status: true
|
||||
dependencies:
|
||||
config:
|
||||
- field.storage.node.field_parent_page
|
||||
- node.type.content_page
|
||||
- node.type.section_page
|
||||
- taxonomy.vocabulary.site_section
|
||||
module:
|
||||
- dynamic_entity_reference
|
||||
id: node.content_page.field_parent_page
|
||||
field_name: field_parent_page
|
||||
entity_type: node
|
||||
bundle: content_page
|
||||
label: 'Parent Page'
|
||||
description: 'Select the parent entity. The site section will be inherited automatically based on the parent type.'
|
||||
required: false
|
||||
translatable: false
|
||||
default_value: { }
|
||||
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_section: site_section
|
||||
sort:
|
||||
field: name
|
||||
direction: asc
|
||||
auto_create: false
|
||||
field_type: dynamic_entity_reference
|
||||
@@ -0,0 +1,29 @@
|
||||
langcode: en
|
||||
status: true
|
||||
dependencies:
|
||||
config:
|
||||
- field.storage.node.field_site_section
|
||||
- node.type.content_page
|
||||
- taxonomy.vocabulary.site_section
|
||||
module:
|
||||
- taxonomy
|
||||
id: node.content_page.field_site_section
|
||||
field_name: field_site_section
|
||||
entity_type: node
|
||||
bundle: content_page
|
||||
label: 'Site Section'
|
||||
description: 'For root pages, select the section. For child pages, this field is filled automatically.'
|
||||
required: false
|
||||
translatable: false
|
||||
default_value: { }
|
||||
default_value_callback: ''
|
||||
settings:
|
||||
handler: 'default:taxonomy_term'
|
||||
handler_settings:
|
||||
target_bundles:
|
||||
site_section: site_section
|
||||
sort:
|
||||
field: name
|
||||
direction: asc
|
||||
auto_create: false
|
||||
field_type: entity_reference
|
||||
22
config/install/field.field.node.section_page.body.yml
Normal file
22
config/install/field.field.node.section_page.body.yml
Normal file
@@ -0,0 +1,22 @@
|
||||
langcode: en
|
||||
status: true
|
||||
dependencies:
|
||||
config:
|
||||
- field.storage.node.body
|
||||
- node.type.section_page
|
||||
module:
|
||||
- text
|
||||
id: node.section_page.body
|
||||
field_name: body
|
||||
entity_type: node
|
||||
bundle: section_page
|
||||
label: Body
|
||||
description: ''
|
||||
required: false
|
||||
translatable: true
|
||||
default_value: { }
|
||||
default_value_callback: ''
|
||||
settings:
|
||||
display_summary: true
|
||||
required_summary: false
|
||||
field_type: text_with_summary
|
||||
@@ -0,0 +1,29 @@
|
||||
langcode: en
|
||||
status: true
|
||||
dependencies:
|
||||
config:
|
||||
- field.storage.node.field_site_section
|
||||
- node.type.section_page
|
||||
- taxonomy.vocabulary.site_section
|
||||
module:
|
||||
- taxonomy
|
||||
id: node.section_page.field_site_section
|
||||
field_name: field_site_section
|
||||
entity_type: node
|
||||
bundle: section_page
|
||||
label: 'Site Section'
|
||||
description: 'Select the site section this page belongs to.'
|
||||
required: true
|
||||
translatable: false
|
||||
default_value: { }
|
||||
default_value_callback: ''
|
||||
settings:
|
||||
handler: 'default:taxonomy_term'
|
||||
handler_settings:
|
||||
target_bundles:
|
||||
site_section: site_section
|
||||
sort:
|
||||
field: name
|
||||
direction: asc
|
||||
auto_create: false
|
||||
field_type: entity_reference
|
||||
18
config/install/field.storage.node.field_parent_page.yml
Normal file
18
config/install/field.storage.node.field_parent_page.yml
Normal file
@@ -0,0 +1,18 @@
|
||||
langcode: en
|
||||
status: true
|
||||
dependencies:
|
||||
module:
|
||||
- dynamic_entity_reference
|
||||
- node
|
||||
id: node.field_parent_page
|
||||
field_name: field_parent_page
|
||||
entity_type: node
|
||||
type: dynamic_entity_reference
|
||||
settings: { }
|
||||
module: dynamic_entity_reference
|
||||
locked: false
|
||||
cardinality: 1
|
||||
translatable: true
|
||||
indexes: { }
|
||||
persist_with_no_fields: false
|
||||
custom_storage: false
|
||||
19
config/install/field.storage.node.field_site_section.yml
Normal file
19
config/install/field.storage.node.field_site_section.yml
Normal file
@@ -0,0 +1,19 @@
|
||||
langcode: en
|
||||
status: true
|
||||
dependencies:
|
||||
module:
|
||||
- node
|
||||
- taxonomy
|
||||
id: node.field_site_section
|
||||
field_name: field_site_section
|
||||
entity_type: node
|
||||
type: entity_reference
|
||||
settings:
|
||||
target_type: taxonomy_term
|
||||
module: core
|
||||
locked: false
|
||||
cardinality: 1
|
||||
translatable: true
|
||||
indexes: { }
|
||||
persist_with_no_fields: false
|
||||
custom_storage: false
|
||||
13
config/install/node.type.content_page.yml
Normal file
13
config/install/node.type.content_page.yml
Normal file
@@ -0,0 +1,13 @@
|
||||
langcode: en
|
||||
status: true
|
||||
dependencies: { }
|
||||
third_party_settings:
|
||||
pathauto:
|
||||
enabled: true
|
||||
name: 'Content Page'
|
||||
type: content_page
|
||||
description: 'Pages with hierarchical parent-child structure for content organization.'
|
||||
help: 'For root pages, fill in the Site Section manually. For child pages, select the parent page and the section will be inherited automatically.'
|
||||
new_revision: true
|
||||
preview_mode: 1
|
||||
display_submitted: true
|
||||
13
config/install/node.type.section_page.yml
Normal file
13
config/install/node.type.section_page.yml
Normal file
@@ -0,0 +1,13 @@
|
||||
langcode: en
|
||||
status: true
|
||||
dependencies: { }
|
||||
third_party_settings:
|
||||
pathauto:
|
||||
enabled: true
|
||||
name: 'Section Page'
|
||||
type: section_page
|
||||
description: 'Pages organized by site section, with URLs and breadcrumbs based on taxonomy.'
|
||||
help: ''
|
||||
new_revision: true
|
||||
preview_mode: 1
|
||||
display_submitted: true
|
||||
20
config/install/pathauto.pattern.content_page.yml
Normal file
20
config/install/pathauto.pattern.content_page.yml
Normal file
@@ -0,0 +1,20 @@
|
||||
langcode: en
|
||||
status: true
|
||||
dependencies:
|
||||
module:
|
||||
- node
|
||||
- site_structure
|
||||
id: content_page
|
||||
label: 'Content Page'
|
||||
type: 'canonical_entities:node'
|
||||
pattern: '[node:site-section-path]/[node:title]'
|
||||
selection_logic: and
|
||||
selection_criteria:
|
||||
c4fb8cde-f2d5-5bf5-0e99-303706509174:
|
||||
id: entity_bundle:node
|
||||
negate: false
|
||||
context_mapping:
|
||||
node: node
|
||||
bundles:
|
||||
content_page: content_page
|
||||
weight: 0
|
||||
20
config/install/pathauto.pattern.section_page.yml
Normal file
20
config/install/pathauto.pattern.section_page.yml
Normal file
@@ -0,0 +1,20 @@
|
||||
langcode: en
|
||||
status: true
|
||||
dependencies:
|
||||
module:
|
||||
- node
|
||||
- site_structure
|
||||
id: section_page
|
||||
label: 'Section Page'
|
||||
type: 'canonical_entities:node'
|
||||
pattern: '[node:site-section-path]/[node:title]'
|
||||
selection_logic: and
|
||||
selection_criteria:
|
||||
b3eb7bda-e1c4-4ae4-9d88-292695498063:
|
||||
id: entity_bundle:node
|
||||
negate: false
|
||||
context_mapping:
|
||||
node: node
|
||||
bundles:
|
||||
section_page: section_page
|
||||
weight: 0
|
||||
20
config/install/pathauto.pattern.site_section_term.yml
Normal file
20
config/install/pathauto.pattern.site_section_term.yml
Normal file
@@ -0,0 +1,20 @@
|
||||
langcode: en
|
||||
status: true
|
||||
dependencies:
|
||||
module:
|
||||
- site_structure
|
||||
- taxonomy
|
||||
id: site_section_term
|
||||
label: 'Site Section Term'
|
||||
type: 'canonical_entities:taxonomy_term'
|
||||
pattern: '[term:hierarchy-path]'
|
||||
selection_logic: and
|
||||
selection_criteria:
|
||||
d5fc9eaf-f3e6-6cg6-1f00-414817610285:
|
||||
id: entity_bundle:taxonomy_term
|
||||
negate: false
|
||||
context_mapping:
|
||||
taxonomy_term: taxonomy_term
|
||||
bundles:
|
||||
site_section: site_section
|
||||
weight: 0
|
||||
11
config/install/site_structure.settings.yml
Normal file
11
config/install/site_structure.settings.yml
Normal file
@@ -0,0 +1,11 @@
|
||||
allowed_parent_targets:
|
||||
- entity_type: node
|
||||
bundle: content_page
|
||||
- entity_type: node
|
||||
bundle: section_page
|
||||
- entity_type: taxonomy_term
|
||||
bundle: site_section
|
||||
- entity_type: user
|
||||
bundle: user
|
||||
- entity_type: group
|
||||
bundle: '*'
|
||||
7
config/install/taxonomy.vocabulary.site_section.yml
Normal file
7
config/install/taxonomy.vocabulary.site_section.yml
Normal file
@@ -0,0 +1,7 @@
|
||||
langcode: en
|
||||
status: true
|
||||
dependencies: { }
|
||||
name: 'Site Section'
|
||||
vid: site_section
|
||||
description: 'Main hierarchical structure for site organization.'
|
||||
weight: 0
|
||||
252
config/install/views.view.child_pages.yml
Normal file
252
config/install/views.view.child_pages.yml
Normal file
@@ -0,0 +1,252 @@
|
||||
langcode: en
|
||||
status: true
|
||||
dependencies:
|
||||
config:
|
||||
- node.type.content_page
|
||||
module:
|
||||
- node
|
||||
- user
|
||||
id: child_pages
|
||||
label: 'Child Pages'
|
||||
module: views
|
||||
description: 'Lists the child pages of a content_page.'
|
||||
tag: ''
|
||||
base_table: node_field_data
|
||||
base_field: nid
|
||||
display:
|
||||
default:
|
||||
id: default
|
||||
display_title: Default
|
||||
display_plugin: default
|
||||
position: 0
|
||||
display_options:
|
||||
title: 'Child Pages'
|
||||
fields:
|
||||
title:
|
||||
id: title
|
||||
table: node_field_data
|
||||
field: title
|
||||
relationship: none
|
||||
group_type: group
|
||||
admin_label: ''
|
||||
entity_type: node
|
||||
entity_field: title
|
||||
plugin_id: field
|
||||
label: ''
|
||||
exclude: false
|
||||
alter:
|
||||
alter_text: false
|
||||
text: ''
|
||||
make_link: false
|
||||
path: ''
|
||||
absolute: false
|
||||
external: false
|
||||
replace_spaces: false
|
||||
path_case: none
|
||||
trim_whitespace: false
|
||||
alt: ''
|
||||
rel: ''
|
||||
link_class: ''
|
||||
prefix: ''
|
||||
suffix: ''
|
||||
target: ''
|
||||
nl2br: false
|
||||
max_length: 0
|
||||
word_boundary: true
|
||||
ellipsis: true
|
||||
more_link: false
|
||||
more_link_text: ''
|
||||
more_link_path: ''
|
||||
strip_tags: false
|
||||
trim: false
|
||||
preserve_tags: ''
|
||||
html: false
|
||||
element_type: ''
|
||||
element_class: ''
|
||||
element_label_type: ''
|
||||
element_label_class: ''
|
||||
element_label_colon: false
|
||||
element_wrapper_type: ''
|
||||
element_wrapper_class: ''
|
||||
element_default_classes: true
|
||||
empty: ''
|
||||
hide_empty: false
|
||||
empty_zero: false
|
||||
hide_alter_empty: true
|
||||
click_sort_column: value
|
||||
type: string
|
||||
settings:
|
||||
link_to_entity: true
|
||||
group_column: value
|
||||
group_columns: { }
|
||||
group_rows: true
|
||||
delta_limit: 0
|
||||
delta_offset: 0
|
||||
delta_reversed: false
|
||||
delta_first_last: false
|
||||
multi_type: separator
|
||||
separator: ', '
|
||||
field_api_classes: false
|
||||
pager:
|
||||
type: none
|
||||
options:
|
||||
offset: 0
|
||||
exposed_form:
|
||||
type: basic
|
||||
options:
|
||||
submit_button: Apply
|
||||
reset_button: false
|
||||
reset_button_label: Reset
|
||||
exposed_sorts_label: 'Sort by'
|
||||
expose_sort_order: true
|
||||
sort_asc_label: Asc
|
||||
sort_desc_label: Desc
|
||||
access:
|
||||
type: perm
|
||||
options:
|
||||
perm: 'access content'
|
||||
cache:
|
||||
type: tag
|
||||
options: { }
|
||||
empty:
|
||||
area_text_custom:
|
||||
id: area_text_custom
|
||||
table: views
|
||||
field: area_text_custom
|
||||
relationship: none
|
||||
group_type: group
|
||||
admin_label: ''
|
||||
plugin_id: text_custom
|
||||
empty: true
|
||||
content: 'No child pages found.'
|
||||
tokenize: false
|
||||
sorts:
|
||||
created:
|
||||
id: created
|
||||
table: node_field_data
|
||||
field: created
|
||||
relationship: none
|
||||
group_type: group
|
||||
admin_label: ''
|
||||
entity_type: node
|
||||
entity_field: created
|
||||
plugin_id: date
|
||||
order: ASC
|
||||
expose:
|
||||
label: ''
|
||||
field_identifier: ''
|
||||
exposed: false
|
||||
granularity: second
|
||||
arguments:
|
||||
field_parent_page_target_id:
|
||||
id: field_parent_page_target_id
|
||||
table: node__field_parent_page
|
||||
field: field_parent_page_target_id
|
||||
relationship: none
|
||||
group_type: group
|
||||
admin_label: ''
|
||||
plugin_id: numeric
|
||||
default_action: default
|
||||
exception:
|
||||
value: all
|
||||
title_enable: false
|
||||
title: All
|
||||
title_enable: false
|
||||
title: ''
|
||||
default_argument_type: node
|
||||
default_argument_options: { }
|
||||
summary_options:
|
||||
base_path: ''
|
||||
count: true
|
||||
override: false
|
||||
items_per_page: 25
|
||||
summary:
|
||||
sort_order: asc
|
||||
number_of_records: 0
|
||||
format: default_summary
|
||||
specify_validation: true
|
||||
validate:
|
||||
type: 'entity:node'
|
||||
fail: 'not found'
|
||||
validate_options:
|
||||
bundles:
|
||||
content_page: content_page
|
||||
access: true
|
||||
operation: view
|
||||
multiple: 0
|
||||
break_phrase: false
|
||||
not: false
|
||||
filters:
|
||||
status:
|
||||
id: status
|
||||
table: node_field_data
|
||||
field: status
|
||||
entity_type: node
|
||||
entity_field: status
|
||||
plugin_id: boolean
|
||||
value: '1'
|
||||
group: 1
|
||||
expose:
|
||||
operator: ''
|
||||
type:
|
||||
id: type
|
||||
table: node_field_data
|
||||
field: type
|
||||
entity_type: node
|
||||
entity_field: type
|
||||
plugin_id: bundle
|
||||
value:
|
||||
content_page: content_page
|
||||
group: 1
|
||||
style:
|
||||
type: default
|
||||
options:
|
||||
grouping: { }
|
||||
row_class: ''
|
||||
default_row_class: true
|
||||
row:
|
||||
type: fields
|
||||
options:
|
||||
default_field_elements: true
|
||||
inline: { }
|
||||
separator: ''
|
||||
hide_empty: false
|
||||
query:
|
||||
type: views_query
|
||||
options:
|
||||
query_comment: ''
|
||||
disable_sql_rewrite: false
|
||||
distinct: false
|
||||
replica: false
|
||||
query_tags: { }
|
||||
relationships: { }
|
||||
header: { }
|
||||
footer: { }
|
||||
display_extenders: { }
|
||||
cache_metadata:
|
||||
max-age: -1
|
||||
contexts:
|
||||
- 'languages:language_content'
|
||||
- 'languages:language_interface'
|
||||
- url
|
||||
- 'user.node_grants:view'
|
||||
- user.permissions
|
||||
tags: { }
|
||||
block_child_pages:
|
||||
id: block_child_pages
|
||||
display_title: 'Block: Child Pages'
|
||||
display_plugin: block
|
||||
position: 1
|
||||
display_options:
|
||||
display_extenders: { }
|
||||
block_description: 'Child Pages'
|
||||
block_category: 'Site Structure'
|
||||
cache_metadata:
|
||||
max-age: -1
|
||||
contexts:
|
||||
- 'languages:language_content'
|
||||
- 'languages:language_interface'
|
||||
- url
|
||||
- 'user.node_grants:view'
|
||||
- user.permissions
|
||||
tags: { }
|
||||
16
config/schema/site_structure.schema.yml
Normal file
16
config/schema/site_structure.schema.yml
Normal file
@@ -0,0 +1,16 @@
|
||||
site_structure.settings:
|
||||
type: config_object
|
||||
label: 'Site Structure settings'
|
||||
mapping:
|
||||
allowed_parent_targets:
|
||||
type: sequence
|
||||
label: 'Allowed parent targets'
|
||||
sequence:
|
||||
type: mapping
|
||||
mapping:
|
||||
entity_type:
|
||||
type: string
|
||||
label: 'Entity type'
|
||||
bundle:
|
||||
type: string
|
||||
label: 'Bundle'
|
||||
345
docs/DESIGN.md
Normal file
345
docs/DESIGN.md
Normal file
@@ -0,0 +1,345 @@
|
||||
# Site Structure - Design Document
|
||||
|
||||
## Overview
|
||||
|
||||
The `site_structure` module implements a hierarchical editorial structure and contextual navigation for institutional sites in Drupal 11. The module was designed for higher education institutions but is generic enough for other institutional contexts.
|
||||
|
||||
## Problem
|
||||
|
||||
In Drupal, content is added in a flat manner (non-hierarchical), and menus are traditionally used to organize content into hierarchical structures. This creates user experience problems because content editors typically do not have permission to administer menus.
|
||||
|
||||
## Solution
|
||||
|
||||
The module implements two complementary structures:
|
||||
|
||||
1. **Taxonomic Structure** (primary): Organization based on hierarchical taxonomy vocabulary
|
||||
2. **Parent-Child Structure** (subsidiary): Direct hierarchy between pages for specific cases, supporting multiple entity types as parents
|
||||
|
||||
---
|
||||
|
||||
## Conceptual Model
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────┐
|
||||
│ PRIMARY STRUCTURE: site_section Taxonomy │
|
||||
│ │
|
||||
│ - Governs the overall organization of site information │
|
||||
│ - Defines breadcrumbs and URLs (via Pathauto) │
|
||||
│ - Example: Undergraduate > Courses > Software Engineering │
|
||||
│ - Applied to content type: section_page │
|
||||
├─────────────────────────────────────────────────────────────────────┤
|
||||
│ SUBSIDIARY STRUCTURE: Parent-Child (field_parent_page) │
|
||||
│ │
|
||||
│ - For specific cases: manuals, guides, sequential documentation │
|
||||
│ - Provides contextual navigation (Child pages View) │
|
||||
│ - Automatically inherits site section from parent (node/taxonomy) │
|
||||
│ - For user/group parents, context is the entity itself │
|
||||
│ - Applied to content type: content_page │
|
||||
│ - Supports multiple parent types: content_page, section_page, │
|
||||
│ taxonomy terms, users, groups (configurable) │
|
||||
├─────────────────────────────────────────────────────────────────────┤
|
||||
│ page (core Drupal) │
|
||||
│ │
|
||||
│ - Remains untouched for generic use │
|
||||
└─────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Components
|
||||
|
||||
### 1. Taxonomy Vocabulary: `site_section`
|
||||
|
||||
| Property | Value |
|
||||
|----------|-------|
|
||||
| Machine name | `site_section` |
|
||||
| Name | Site Section |
|
||||
| Hierarchical | Yes |
|
||||
| Description | Main hierarchical structure for site organization |
|
||||
|
||||
**Usage**: This vocabulary defines site sections. Terms can be nested to create hierarchies (e.g., "Undergraduate" > "Courses" > "Engineering").
|
||||
|
||||
### 2. Content Type: `section_page`
|
||||
|
||||
| Property | Value |
|
||||
|----------|-------|
|
||||
| Machine name | `section_page` |
|
||||
| Name | Section Page |
|
||||
| Description | Pages organized by site section |
|
||||
|
||||
**Fields**:
|
||||
|
||||
| Field | Type | Required | Cardinality | Description |
|
||||
|-------|------|----------|-------------|-------------|
|
||||
| `title` | String | Yes | 1 | Page title (core) |
|
||||
| `body` | Text (formatted, long, with summary) | No | 1 | Page content |
|
||||
| `field_site_section` | Entity reference (taxonomy) | Yes | 1 | Site section |
|
||||
|
||||
### 3. Content Type: `content_page`
|
||||
|
||||
| Property | Value |
|
||||
|----------|-------|
|
||||
| Machine name | `content_page` |
|
||||
| Name | Content Page |
|
||||
| Description | Pages with hierarchical parent-child structure |
|
||||
|
||||
**Fields**:
|
||||
|
||||
| Field | Type | Required | Cardinality | Description |
|
||||
|-------|------|----------|-------------|-------------|
|
||||
| `title` | String | Yes | 1 | Page title (core) |
|
||||
| `body` | Text (formatted, long, with summary) | No | 1 | Page content |
|
||||
| `field_parent_page` | Dynamic entity reference | No | 1 | Parent entity (configurable types) |
|
||||
| `field_site_section` | Entity reference (taxonomy) | Conditional | 1 | Site section (inherited or manual) |
|
||||
|
||||
**Rules for `field_site_section` in content_page**:
|
||||
- If `field_parent_page` points to a `taxonomy_term`: uses the term itself as site section
|
||||
- If `field_parent_page` points to a `node`: inherits `field_site_section` from parent
|
||||
- If `field_parent_page` is empty (root page): must be filled manually
|
||||
|
||||
### 4. Dynamic Parent Reference
|
||||
|
||||
The `field_parent_page` uses the Dynamic Entity Reference module to support multiple entity types as parents. This is configurable via the admin interface.
|
||||
|
||||
**Default allowed targets**:
|
||||
- `node:content_page` - Other content pages
|
||||
- `node:section_page` - Section pages
|
||||
- `taxonomy_term:site_section` - Site section terms
|
||||
- `user:user` - User accounts
|
||||
- `group:*` - All group types (requires Group module)
|
||||
|
||||
**Context behavior by parent type**:
|
||||
- **Node/Taxonomy**: Content pages inherit `field_site_section` from the parent
|
||||
- **User**: Content pages are associated with the user profile page (`/user/{id}`). The `field_site_section` is cleared as the context is the user itself.
|
||||
- **Group**: Content pages are associated with the group. The `field_site_section` is cleared as the context is the group itself.
|
||||
|
||||
**Configuration**: `/admin/config/local-modules/site-structure`
|
||||
|
||||
### 5. Inheritance Logic
|
||||
|
||||
Implemented via `hook_entity_presave()`:
|
||||
|
||||
```
|
||||
When a content_page is saved:
|
||||
IF field_parent_page is filled:
|
||||
IF parent is user or group:
|
||||
Clear field_site_section (context is the entity itself)
|
||||
ELSE IF parent is taxonomy_term (site_section):
|
||||
Set field_site_section to the parent term ID
|
||||
ELSE IF parent is node with field_site_section:
|
||||
Copy field_site_section from parent node
|
||||
ELSE:
|
||||
Validate that field_site_section was filled manually
|
||||
```
|
||||
|
||||
**Additional validations**:
|
||||
- Prevent self-reference (page cannot be parent of itself)
|
||||
- Prevent circularity (A → B → C → A) - only for node parents
|
||||
|
||||
### 6. Breadcrumb Builder
|
||||
|
||||
Custom service that overrides Drupal's default breadcrumb for `section_page` and `content_page`.
|
||||
|
||||
**Logic**:
|
||||
1. Determine the root parent context by traversing the parent chain
|
||||
2. Build breadcrumb based on context type:
|
||||
- **User context**: Home > User Name > Parent Nodes > Current Page
|
||||
- **Group context**: Home > Group Name > Parent Nodes > Current Page
|
||||
- **Taxonomy context**: Home > Term Hierarchy > Parent Nodes > Current Page
|
||||
3. For content_page, also add node parents to the breadcrumb
|
||||
|
||||
**Examples**:
|
||||
- Page "Chapter 1" has parent "User Guide" which is a section_page in "Documentation"
|
||||
- Breadcrumb: Home > Documentation > User Guide > Chapter 1
|
||||
- Page "My Notes" has parent user "John Doe"
|
||||
- Breadcrumb: Home > John Doe > My Notes
|
||||
- Page "Meeting Minutes" has parent group "Research Team"
|
||||
- Breadcrumb: Home > Research Team > Meeting Minutes
|
||||
|
||||
### 7. Pathauto Tokens
|
||||
|
||||
Custom token for hierarchical URL generation.
|
||||
|
||||
| Token | Description | Example |
|
||||
|-------|-------------|---------|
|
||||
| `[node:site-section-path]` | Path based on taxonomy hierarchy | `undergraduate/courses` |
|
||||
|
||||
**Suggested URL pattern**: `[node:site-section-path]/[node:title]`
|
||||
|
||||
**Result**: `/undergraduate/courses/software-engineering`
|
||||
|
||||
### 8. View: `child_pages`
|
||||
|
||||
View that lists child pages of the current `content_page`.
|
||||
|
||||
| Property | Value |
|
||||
|----------|-------|
|
||||
| Display | Block |
|
||||
| Filter | `field_parent_page` = Current node ID (contextual) |
|
||||
| Sort | Title (A-Z) or weight (if implemented) |
|
||||
| Usage | Sidebar navigation in guides/manuals |
|
||||
|
||||
### 9. Settings Form
|
||||
|
||||
Configuration form at `/admin/config/local-modules/site-structure` that allows administrators to:
|
||||
- Select which node bundles can be used as parents
|
||||
- Select which taxonomy vocabularies can be used as parents
|
||||
- Changes automatically update the field configuration
|
||||
|
||||
---
|
||||
|
||||
## File Structure
|
||||
|
||||
```
|
||||
site_structure/
|
||||
├── site_structure.info.yml # Metadata and dependencies
|
||||
├── site_structure.module # Hooks (presave, tokens)
|
||||
├── site_structure.install # Installation hooks
|
||||
├── site_structure.services.yml # Service registration
|
||||
├── site_structure.routing.yml # Route definitions
|
||||
├── site_structure.links.menu.yml # Admin menu links
|
||||
│
|
||||
├── config/
|
||||
│ ├── install/
|
||||
│ │ ├── site_structure.settings.yml # Default settings
|
||||
│ │ ├── taxonomy.vocabulary.site_section.yml
|
||||
│ │ │
|
||||
│ │ ├── node.type.section_page.yml
|
||||
│ │ ├── field.storage.node.field_site_section.yml
|
||||
│ │ ├── field.field.node.section_page.field_site_section.yml
|
||||
│ │ ├── core.entity_form_display.node.section_page.default.yml
|
||||
│ │ ├── core.entity_view_display.node.section_page.default.yml
|
||||
│ │ │
|
||||
│ │ ├── node.type.content_page.yml
|
||||
│ │ ├── field.storage.node.field_parent_page.yml # dynamic_entity_reference
|
||||
│ │ ├── field.field.node.content_page.field_parent_page.yml
|
||||
│ │ ├── field.field.node.content_page.field_site_section.yml
|
||||
│ │ ├── core.entity_form_display.node.content_page.default.yml
|
||||
│ │ ├── core.entity_view_display.node.content_page.default.yml
|
||||
│ │ │
|
||||
│ │ ├── pathauto.pattern.section_page.yml
|
||||
│ │ ├── pathauto.pattern.content_page.yml
|
||||
│ │ ├── pathauto.pattern.site_section_term.yml
|
||||
│ │ │
|
||||
│ │ └── views.view.child_pages.yml
|
||||
│ │
|
||||
│ └── schema/
|
||||
│ └── site_structure.schema.yml # Config schema
|
||||
│
|
||||
├── translations/
|
||||
│ └── pt-br.po # Portuguese (Brazil) translation
|
||||
│
|
||||
├── src/
|
||||
│ ├── Breadcrumb/
|
||||
│ │ └── SectionBreadcrumbBuilder.php
|
||||
│ │
|
||||
│ └── Form/
|
||||
│ └── SiteStructureSettingsForm.php
|
||||
│
|
||||
└── docs/
|
||||
└── DESIGN.md # This document
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Dependencies
|
||||
|
||||
### Core Modules (Drupal 11)
|
||||
- `node`
|
||||
- `taxonomy`
|
||||
- `views`
|
||||
- `field`
|
||||
- `text`
|
||||
- `user`
|
||||
|
||||
### Contrib Modules
|
||||
- `token` - For custom tokens
|
||||
- `pathauto` - For automatic URL generation
|
||||
- `dynamic_entity_reference` - For multi-type parent references
|
||||
|
||||
### Optional Modules
|
||||
- `group` - For group entity support as parent type
|
||||
|
||||
---
|
||||
|
||||
## Editor Workflow
|
||||
|
||||
### Creating a Section Page (section_page)
|
||||
|
||||
1. Navigate to Content > Add content > Section Page
|
||||
2. Fill in title and body
|
||||
3. Select the appropriate site section
|
||||
4. Save
|
||||
5. URL and breadcrumb are generated automatically
|
||||
|
||||
### Creating a Content Page (content_page)
|
||||
|
||||
1. **Create root page (anchored to taxonomy term)**:
|
||||
- Add content > Content Page
|
||||
- In "Parent Page", select a site_section term
|
||||
- Site section is set automatically from the term
|
||||
- Save
|
||||
|
||||
2. **Create root page (anchored to section_page)**:
|
||||
- Add content > Content Page
|
||||
- In "Parent Page", select a section_page
|
||||
- Site section is inherited from the section_page
|
||||
- Save
|
||||
|
||||
3. **Create child pages**:
|
||||
- Add content > Content Page
|
||||
- In "Parent Page", select another content_page
|
||||
- Site section is inherited automatically
|
||||
- Save
|
||||
|
||||
4. The child pages View appears automatically on parent pages
|
||||
|
||||
---
|
||||
|
||||
## Administration
|
||||
|
||||
### Configuring Allowed Parent Types
|
||||
|
||||
1. Navigate to `/admin/config/local-modules/site-structure`
|
||||
2. Select which content types (nodes) can be used as parents
|
||||
3. Select which taxonomy vocabularies can be used as parents
|
||||
4. Save configuration
|
||||
5. Field configuration is updated automatically
|
||||
|
||||
---
|
||||
|
||||
## Group Integration
|
||||
|
||||
The module supports the Group module for content organization:
|
||||
|
||||
- Content pages can have groups as parent entities
|
||||
- When a content_page has a group as parent, the context is the group itself
|
||||
- Breadcrumbs show: Home > Group Name > Parent Nodes > Current Page
|
||||
- `field_site_section` is cleared for group-parented content (context is the group)
|
||||
- Per-section permissions can be implemented via Groups
|
||||
|
||||
**Configuration**: Enable group types in the settings form at `/admin/config/local-modules/site-structure`
|
||||
|
||||
---
|
||||
|
||||
## Installation Verification
|
||||
|
||||
1. `drush en site_structure -y`
|
||||
2. Verify vocabulary at `/admin/structure/taxonomy`
|
||||
3. Verify content types at `/admin/structure/types`
|
||||
4. Create test hierarchical terms
|
||||
5. Create `section_page` and verify breadcrumb/URL
|
||||
6. Create root `content_page` with taxonomy term as parent
|
||||
7. Create child `content_page`, verify inheritance
|
||||
8. Verify child pages View
|
||||
9. Test settings form at `/admin/config/local-modules/site-structure`
|
||||
|
||||
---
|
||||
|
||||
## Changelog
|
||||
|
||||
| Version | Date | Description |
|
||||
|---------|------|-------------|
|
||||
| 1.0.0 | - | Initial version |
|
||||
| 1.1.0 | - | Added dynamic_entity_reference support for multi-type parents |
|
||||
| 1.2.0 | - | Added user and group entity support as parent types |
|
||||
15
site_structure.info.yml
Normal file
15
site_structure.info.yml
Normal file
@@ -0,0 +1,15 @@
|
||||
name: 'Site Structure'
|
||||
type: module
|
||||
description: 'Implements hierarchical editorial structure and contextual navigation for institutional sites.'
|
||||
package: Custom
|
||||
core_version_requirement: ^11
|
||||
configure: site_structure.settings
|
||||
dependencies:
|
||||
- drupal:node
|
||||
- drupal:taxonomy
|
||||
- drupal:views
|
||||
- drupal:field
|
||||
- drupal:text
|
||||
- token:token
|
||||
- pathauto:pathauto
|
||||
- dynamic_entity_reference:dynamic_entity_reference
|
||||
206
site_structure.install
Normal file
206
site_structure.install
Normal file
@@ -0,0 +1,206 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Install, update and uninstall functions for the Site Structure module.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Drupal\taxonomy\Entity\Term;
|
||||
|
||||
/**
|
||||
* Implements hook_install().
|
||||
*/
|
||||
function site_structure_install(): void {
|
||||
// Create default terms for site_section vocabulary.
|
||||
_site_structure_create_default_terms();
|
||||
|
||||
// Display success message.
|
||||
\Drupal::messenger()->addStatus(t('Site Structure module installed successfully. Configure Pathauto patterns at /admin/config/search/path/patterns'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates default terms for the site_section vocabulary.
|
||||
*/
|
||||
function _site_structure_create_default_terms(): void {
|
||||
$vocabulary = 'site_section';
|
||||
|
||||
// Check if terms already exist (avoid duplication on reinstall).
|
||||
$existing = \Drupal::entityQuery('taxonomy_term')
|
||||
->accessCheck(FALSE)
|
||||
->condition('vid', $vocabulary)
|
||||
->count()
|
||||
->execute();
|
||||
|
||||
if ($existing > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Structure: name => children.
|
||||
$terms_structure = [
|
||||
'News' => [],
|
||||
'Events' => [],
|
||||
'People' => [],
|
||||
'Institutional' => [
|
||||
'About',
|
||||
'Communication',
|
||||
'Information and Services',
|
||||
'Team',
|
||||
'Management',
|
||||
'Inclusion and Belonging',
|
||||
],
|
||||
'Undergraduate' => [
|
||||
'Statistics',
|
||||
'Mathematics',
|
||||
'Applied Mathematics',
|
||||
'Mathematics Teaching',
|
||||
],
|
||||
'Graduate' => [
|
||||
'Statistics Program',
|
||||
'Mathematics Program',
|
||||
'Applied Mathematics Program',
|
||||
],
|
||||
'Research' => [],
|
||||
'Extension' => [],
|
||||
'Administration' => [],
|
||||
'Departments' => [
|
||||
'Statistics Department',
|
||||
'Mathematics Department',
|
||||
'Applied Mathematics Department',
|
||||
],
|
||||
'Library' => [],
|
||||
'IT Services' => [],
|
||||
];
|
||||
|
||||
$weight = 0;
|
||||
foreach ($terms_structure as $parent_name => $children) {
|
||||
// Create parent term.
|
||||
$parent_term = Term::create([
|
||||
'vid' => $vocabulary,
|
||||
'name' => $parent_name,
|
||||
'weight' => $weight++,
|
||||
]);
|
||||
$parent_term->save();
|
||||
|
||||
// Create child terms.
|
||||
$child_weight = 0;
|
||||
foreach ($children as $child_name) {
|
||||
$child_term = Term::create([
|
||||
'vid' => $vocabulary,
|
||||
'name' => $child_name,
|
||||
'parent' => $parent_term->id(),
|
||||
'weight' => $child_weight++,
|
||||
]);
|
||||
$child_term->save();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_uninstall().
|
||||
*/
|
||||
function site_structure_uninstall(): void {
|
||||
$entity_type_manager = \Drupal::entityTypeManager();
|
||||
|
||||
// Remove nodes from module's content types.
|
||||
foreach (['section_page', 'content_page'] as $bundle) {
|
||||
$nids = \Drupal::entityQuery('node')
|
||||
->accessCheck(FALSE)
|
||||
->condition('type', $bundle)
|
||||
->execute();
|
||||
if ($nids) {
|
||||
$nodes = $entity_type_manager->getStorage('node')->loadMultiple($nids);
|
||||
$entity_type_manager->getStorage('node')->delete($nodes);
|
||||
\Drupal::messenger()->addWarning(t('Deleted @count @bundle nodes.', [
|
||||
'@count' => count($nids),
|
||||
'@bundle' => $bundle,
|
||||
]));
|
||||
}
|
||||
}
|
||||
|
||||
// Remove terms from site_section vocabulary.
|
||||
$tids = \Drupal::entityQuery('taxonomy_term')
|
||||
->accessCheck(FALSE)
|
||||
->condition('vid', 'site_section')
|
||||
->execute();
|
||||
if ($tids) {
|
||||
$terms = $entity_type_manager->getStorage('taxonomy_term')->loadMultiple($tids);
|
||||
$entity_type_manager->getStorage('taxonomy_term')->delete($terms);
|
||||
}
|
||||
|
||||
// Remove configurations in correct order (dependencies first).
|
||||
$configs_to_delete = [
|
||||
// Views.
|
||||
'views.view.child_pages',
|
||||
// Pathauto patterns.
|
||||
'pathauto.pattern.section_page',
|
||||
'pathauto.pattern.content_page',
|
||||
'pathauto.pattern.site_section_term',
|
||||
// Entity displays.
|
||||
'core.entity_form_display.node.section_page.default',
|
||||
'core.entity_view_display.node.section_page.default',
|
||||
'core.entity_form_display.node.content_page.default',
|
||||
'core.entity_view_display.node.content_page.default',
|
||||
// Field instances.
|
||||
'field.field.node.section_page.field_site_section',
|
||||
'field.field.node.section_page.body',
|
||||
'field.field.node.content_page.field_parent_page',
|
||||
'field.field.node.content_page.field_site_section',
|
||||
'field.field.node.content_page.body',
|
||||
// Field storages (only if not used by other bundles).
|
||||
'field.storage.node.field_site_section',
|
||||
'field.storage.node.field_parent_page',
|
||||
// Node types.
|
||||
'node.type.section_page',
|
||||
'node.type.content_page',
|
||||
// Vocabulary.
|
||||
'taxonomy.vocabulary.site_section',
|
||||
];
|
||||
|
||||
$config_factory = \Drupal::configFactory();
|
||||
foreach ($configs_to_delete as $config_name) {
|
||||
$config = $config_factory->getEditable($config_name);
|
||||
if (!$config->isNew()) {
|
||||
$config->delete();
|
||||
}
|
||||
}
|
||||
|
||||
\Drupal::messenger()->addStatus(t('Site Structure module uninstalled successfully.'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_requirements().
|
||||
*/
|
||||
function site_structure_requirements(string $phase): array {
|
||||
$requirements = [];
|
||||
|
||||
if ($phase === 'runtime') {
|
||||
// Check if site_section vocabulary has terms.
|
||||
$term_count = \Drupal::entityQuery('taxonomy_term')
|
||||
->accessCheck(FALSE)
|
||||
->condition('vid', 'site_section')
|
||||
->count()
|
||||
->execute();
|
||||
|
||||
if ($term_count === 0) {
|
||||
$requirements['site_structure_terms'] = [
|
||||
'title' => t('Site Structure'),
|
||||
'value' => t('No terms in site_section vocabulary'),
|
||||
'description' => t('The Site Structure module requires terms in the "Site Section" vocabulary. <a href=":url">Add terms</a>.', [
|
||||
':url' => '/admin/structure/taxonomy/manage/site_section/add',
|
||||
]),
|
||||
'severity' => REQUIREMENT_WARNING,
|
||||
];
|
||||
}
|
||||
else {
|
||||
$requirements['site_structure_terms'] = [
|
||||
'title' => t('Site Structure'),
|
||||
'value' => t('@count terms configured', ['@count' => $term_count]),
|
||||
'severity' => REQUIREMENT_OK,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return $requirements;
|
||||
}
|
||||
6
site_structure.links.menu.yml
Normal file
6
site_structure.links.menu.yml
Normal file
@@ -0,0 +1,6 @@
|
||||
site_structure.settings:
|
||||
title: 'Site Structure'
|
||||
description: 'Configure allowed parent entity types for content pages.'
|
||||
route_name: site_structure.settings
|
||||
parent: site_tools.admin_config
|
||||
weight: 0
|
||||
298
site_structure.module
Normal file
298
site_structure.module
Normal file
@@ -0,0 +1,298 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Primary module hooks for Site Structure module.
|
||||
*/
|
||||
|
||||
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.
|
||||
*/
|
||||
function site_structure_entity_presave(EntityInterface $entity): void {
|
||||
if (!$entity instanceof NodeInterface || $entity->bundle() !== 'content_page') {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if has parent page.
|
||||
if (!$entity->hasField('field_parent_page') || $entity->get('field_parent_page')->isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$parent_field = $entity->get('field_parent_page')->first();
|
||||
if (!$parent_field) {
|
||||
return;
|
||||
}
|
||||
|
||||
$parent_entity_type = $parent_field->target_type ?? NULL;
|
||||
$parent_id = $parent_field->target_id ?? NULL;
|
||||
|
||||
if (!$parent_entity_type || !$parent_id) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Circular reference validation (only for node parents).
|
||||
if ($parent_entity_type === 'node' && !$entity->isNew() && $parent_id) {
|
||||
if (_site_structure_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.
|
||||
// For user and group, we don't set field_site_section - the context is the entity itself.
|
||||
if (in_array($parent_entity_type, ['user', 'group'])) {
|
||||
// Clear field_site_section as the context is the user/group, not a site section.
|
||||
$entity->set('field_site_section', NULL);
|
||||
return;
|
||||
}
|
||||
|
||||
// Inherit field_site_section based on parent type (node or taxonomy_term).
|
||||
$site_section_id = _site_structure_get_section_from_parent($parent_entity_type, $parent_id);
|
||||
if ($site_section_id) {
|
||||
$entity->set('field_site_section', $site_section_id);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 _site_structure_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 site_section vocabulary.
|
||||
$term = $entity_type_manager->getStorage('taxonomy_term')->load($parent_id);
|
||||
if ($term instanceof TermInterface && $term->bundle() === 'site_section') {
|
||||
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 _site_structure_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.
|
||||
*
|
||||
* @param int|string $node_id
|
||||
* The ID of the node being edited.
|
||||
* @param int|string $parent_id
|
||||
* The ID of the potential parent.
|
||||
*
|
||||
* @return bool
|
||||
* TRUE if it would create circular reference, FALSE otherwise.
|
||||
*/
|
||||
function _site_structure_creates_circular_reference(int|string $node_id, int|string $parent_id): bool {
|
||||
$visited = [];
|
||||
$current_id = $parent_id;
|
||||
$node_storage = \Drupal::entityTypeManager()->getStorage('node');
|
||||
|
||||
while ($current_id) {
|
||||
// If we find the original node in the parent chain, it's circular.
|
||||
if ($current_id == $node_id) {
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
// Avoid infinite loops in case of corrupted data.
|
||||
if (isset($visited[$current_id])) {
|
||||
return TRUE;
|
||||
}
|
||||
$visited[$current_id] = TRUE;
|
||||
|
||||
// Load the current node and get its parent.
|
||||
$current_node = $node_storage->load($current_id);
|
||||
if (!$current_node instanceof NodeInterface ||
|
||||
!$current_node->hasField('field_parent_page') ||
|
||||
$current_node->get('field_parent_page')->isEmpty()) {
|
||||
break;
|
||||
}
|
||||
|
||||
$parent_field = $current_node->get('field_parent_page')->first();
|
||||
// Only continue checking if parent is also a node.
|
||||
if (!$parent_field || ($parent_field->target_type ?? NULL) !== 'node') {
|
||||
break;
|
||||
}
|
||||
|
||||
$current_id = $parent_field->target_id;
|
||||
}
|
||||
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_token_info().
|
||||
*/
|
||||
function site_structure_token_info(): array {
|
||||
$info = [];
|
||||
|
||||
$info['tokens']['node']['site-section-path'] = [
|
||||
'name' => t('Site Section Path'),
|
||||
'description' => t('The hierarchical path of the site_section taxonomy (e.g., undergraduate/courses).'),
|
||||
];
|
||||
|
||||
$info['tokens']['term']['hierarchy-path'] = [
|
||||
'name' => t('Hierarchy Path'),
|
||||
'description' => t('The hierarchical path of the term including ancestors (e.g., institutional/news).'),
|
||||
];
|
||||
|
||||
return $info;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_tokens().
|
||||
*/
|
||||
function site_structure_tokens(string $type, array $tokens, array $data, array $options, $bubbleable_metadata): array {
|
||||
$replacements = [];
|
||||
|
||||
if ($type === 'node' && !empty($data['node'])) {
|
||||
$node = $data['node'];
|
||||
foreach ($tokens as $name => $original) {
|
||||
if ($name === 'site-section-path') {
|
||||
$replacements[$original] = _site_structure_get_section_path($node);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($type === 'term' && !empty($data['term'])) {
|
||||
$term = $data['term'];
|
||||
foreach ($tokens as $name => $original) {
|
||||
if ($name === 'hierarchy-path') {
|
||||
$replacements[$original] = _site_structure_get_term_hierarchy_path($term);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $replacements;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the hierarchical path of the site section for a node.
|
||||
*
|
||||
* @param \Drupal\node\NodeInterface $node
|
||||
* The node.
|
||||
*
|
||||
* @return string
|
||||
* The section path (e.g., "undergraduate/courses") or empty string.
|
||||
*/
|
||||
function _site_structure_get_section_path(NodeInterface $node): string {
|
||||
if (!$node->hasField('field_site_section') || $node->get('field_site_section')->isEmpty()) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$term_id = $node->get('field_site_section')->target_id;
|
||||
$term_storage = \Drupal::entityTypeManager()->getStorage('taxonomy_term');
|
||||
$term = $term_storage->load($term_id);
|
||||
|
||||
if (!$term) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Get all ancestors of the term.
|
||||
$ancestors = $term_storage->loadAllParents($term_id);
|
||||
$ancestors = array_reverse($ancestors);
|
||||
|
||||
// Build the path using the term names converted to URL.
|
||||
$path_parts = [];
|
||||
foreach ($ancestors as $ancestor) {
|
||||
$path_parts[] = \Drupal::service('pathauto.alias_cleaner')->cleanString($ancestor->getName());
|
||||
}
|
||||
|
||||
return implode('/', $path_parts);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the hierarchical path of a taxonomy term.
|
||||
*
|
||||
* @param \Drupal\taxonomy\TermInterface $term
|
||||
* The taxonomy term.
|
||||
*
|
||||
* @return string
|
||||
* The hierarchical path (e.g., "institutional/news") or empty string.
|
||||
*/
|
||||
function _site_structure_get_term_hierarchy_path(TermInterface $term): string {
|
||||
$term_storage = \Drupal::entityTypeManager()->getStorage('taxonomy_term');
|
||||
|
||||
// Get all ancestors of the term (including itself).
|
||||
$ancestors = $term_storage->loadAllParents($term->id());
|
||||
$ancestors = array_reverse($ancestors);
|
||||
|
||||
// Build the path using the term names converted to URL.
|
||||
$path_parts = [];
|
||||
foreach ($ancestors as $ancestor) {
|
||||
$path_parts[] = \Drupal::service('pathauto.alias_cleaner')->cleanString($ancestor->getName());
|
||||
}
|
||||
|
||||
return implode('/', $path_parts);
|
||||
}
|
||||
7
site_structure.routing.yml
Normal file
7
site_structure.routing.yml
Normal file
@@ -0,0 +1,7 @@
|
||||
site_structure.settings:
|
||||
path: '/admin/config/local-modules/site-structure'
|
||||
defaults:
|
||||
_form: '\Drupal\site_structure\Form\SiteStructureSettingsForm'
|
||||
_title: 'Site Structure Settings'
|
||||
requirements:
|
||||
_permission: 'administer site configuration'
|
||||
6
site_structure.services.yml
Normal file
6
site_structure.services.yml
Normal file
@@ -0,0 +1,6 @@
|
||||
services:
|
||||
site_structure.breadcrumb.section:
|
||||
class: Drupal\site_structure\Breadcrumb\SectionBreadcrumbBuilder
|
||||
arguments: ['@entity_type.manager']
|
||||
tags:
|
||||
- { name: breadcrumb_builder, priority: 100 }
|
||||
14
site_structure.tokens.inc
Normal file
14
site_structure.tokens.inc
Normal file
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Token integration for Site Structure module.
|
||||
*
|
||||
* Este arquivo existe para compatibilidade, mas os tokens são definidos
|
||||
* diretamente em site_structure.module via hook_token_info() e hook_tokens().
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
// Os tokens são implementados em site_structure.module.
|
||||
// Este arquivo pode ser usado para funções auxiliares adicionais se necessário.
|
||||
320
src/Breadcrumb/SectionBreadcrumbBuilder.php
Normal file
320
src/Breadcrumb/SectionBreadcrumbBuilder.php
Normal file
@@ -0,0 +1,320 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\site_structure\Breadcrumb;
|
||||
|
||||
use Drupal\Core\Breadcrumb\Breadcrumb;
|
||||
use Drupal\Core\Breadcrumb\BreadcrumbBuilderInterface;
|
||||
use Drupal\Core\Entity\EntityInterface;
|
||||
use Drupal\Core\Entity\EntityTypeManagerInterface;
|
||||
use Drupal\Core\Link;
|
||||
use Drupal\Core\Routing\RouteMatchInterface;
|
||||
use Drupal\Core\StringTranslation\StringTranslationTrait;
|
||||
use Drupal\node\NodeInterface;
|
||||
|
||||
/**
|
||||
* Provides a breadcrumb builder for section_page and content_page content types.
|
||||
*/
|
||||
class SectionBreadcrumbBuilder implements BreadcrumbBuilderInterface {
|
||||
|
||||
use StringTranslationTrait;
|
||||
|
||||
/**
|
||||
* The entity type manager.
|
||||
*
|
||||
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
|
||||
*/
|
||||
protected EntityTypeManagerInterface $entityTypeManager;
|
||||
|
||||
/**
|
||||
* Content types that this builder applies to.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected array $applicableBundles = ['section_page', 'content_page'];
|
||||
|
||||
/**
|
||||
* Constructs a SectionBreadcrumbBuilder object.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
|
||||
* The entity type manager.
|
||||
*/
|
||||
public function __construct(EntityTypeManagerInterface $entity_type_manager) {
|
||||
$this->entityTypeManager = $entity_type_manager;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function applies(RouteMatchInterface $route_match): bool {
|
||||
$node = $route_match->getParameter('node');
|
||||
|
||||
if ($node instanceof NodeInterface) {
|
||||
return in_array($node->bundle(), $this->applicableBundles, TRUE);
|
||||
}
|
||||
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function build(RouteMatchInterface $route_match): Breadcrumb {
|
||||
$breadcrumb = new Breadcrumb();
|
||||
$breadcrumb->addCacheContexts(['route']);
|
||||
|
||||
$node = $route_match->getParameter('node');
|
||||
|
||||
if (!$node instanceof NodeInterface) {
|
||||
return $breadcrumb;
|
||||
}
|
||||
|
||||
$breadcrumb->addCacheableDependency($node);
|
||||
|
||||
// Add "Home" as first item.
|
||||
$breadcrumb->addLink(Link::createFromRoute($this->t('Home'), '<front>'));
|
||||
|
||||
// Determine the context type for content_page.
|
||||
$context = $this->getParentContext($node);
|
||||
|
||||
if ($context) {
|
||||
switch ($context['type']) {
|
||||
case 'user':
|
||||
// User context: Home > User Name > ... > Current Page.
|
||||
$this->addUserBreadcrumb($breadcrumb, $context['entity']);
|
||||
break;
|
||||
|
||||
case 'group':
|
||||
// Group context: Home > Group Name > ... > Current Page.
|
||||
$this->addGroupBreadcrumb($breadcrumb, $context['entity']);
|
||||
break;
|
||||
|
||||
default:
|
||||
// Node or taxonomy context: use site section.
|
||||
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);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
// For content_page, add node parent hierarchy.
|
||||
if ($node->bundle() === 'content_page') {
|
||||
$this->addParentBreadcrumbs($breadcrumb, $node);
|
||||
}
|
||||
|
||||
return $breadcrumb;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the root parent context for a content_page.
|
||||
*
|
||||
* Traverses up the parent chain to find the root context (user, group, or taxonomy).
|
||||
*
|
||||
* @param \Drupal\node\NodeInterface $node
|
||||
* The content_page node.
|
||||
*
|
||||
* @return array|null
|
||||
* An array with 'type' and 'entity' keys, or NULL if no context.
|
||||
*/
|
||||
protected function getParentContext(NodeInterface $node): ?array {
|
||||
if ($node->bundle() !== 'content_page') {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
$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;
|
||||
}
|
||||
|
||||
$parent_entity_type = $parent_field->target_type ?? NULL;
|
||||
$parent_id = $parent_field->target_id ?? NULL;
|
||||
|
||||
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 site_section).
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds user breadcrumb.
|
||||
*
|
||||
* @param \Drupal\Core\Breadcrumb\Breadcrumb $breadcrumb
|
||||
* The breadcrumb object.
|
||||
* @param \Drupal\Core\Entity\EntityInterface $user
|
||||
* The user entity.
|
||||
*/
|
||||
protected function addUserBreadcrumb(Breadcrumb $breadcrumb, EntityInterface $user): void {
|
||||
$breadcrumb->addCacheableDependency($user);
|
||||
$breadcrumb->addLink($user->toLink());
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds group breadcrumb.
|
||||
*
|
||||
* @param \Drupal\Core\Breadcrumb\Breadcrumb $breadcrumb
|
||||
* The breadcrumb object.
|
||||
* @param \Drupal\Core\Entity\EntityInterface $group
|
||||
* The group entity.
|
||||
*/
|
||||
protected function addGroupBreadcrumb(Breadcrumb $breadcrumb, EntityInterface $group): void {
|
||||
$breadcrumb->addCacheableDependency($group);
|
||||
$breadcrumb->addLink($group->toLink());
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds breadcrumbs based on taxonomy hierarchy.
|
||||
*
|
||||
* @param \Drupal\Core\Breadcrumb\Breadcrumb $breadcrumb
|
||||
* The breadcrumb object.
|
||||
* @param int|string $term_id
|
||||
* The taxonomy term ID.
|
||||
*/
|
||||
protected function addTaxonomyBreadcrumbs(Breadcrumb $breadcrumb, int|string $term_id): void {
|
||||
$term_storage = $this->entityTypeManager->getStorage('taxonomy_term');
|
||||
$ancestors = $term_storage->loadAllParents($term_id);
|
||||
$ancestors = array_reverse($ancestors);
|
||||
|
||||
foreach ($ancestors as $ancestor) {
|
||||
$breadcrumb->addCacheableDependency($ancestor);
|
||||
$breadcrumb->addLink($ancestor->toLink());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds breadcrumbs based on content_page parent hierarchy.
|
||||
*
|
||||
* Only adds node parents to the breadcrumb, not user/group/taxonomy
|
||||
* which are handled separately.
|
||||
*
|
||||
* @param \Drupal\Core\Breadcrumb\Breadcrumb $breadcrumb
|
||||
* The breadcrumb object.
|
||||
* @param \Drupal\node\NodeInterface $node
|
||||
* The current node.
|
||||
*/
|
||||
protected function addParentBreadcrumbs(Breadcrumb $breadcrumb, NodeInterface $node): void {
|
||||
$parents = $this->getNodeParents($node);
|
||||
$parents = array_reverse($parents);
|
||||
|
||||
foreach ($parents as $parent) {
|
||||
$breadcrumb->addCacheableDependency($parent);
|
||||
$breadcrumb->addLink($parent->toLink());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all node parents of a content_page.
|
||||
*
|
||||
* Stops when encountering a non-node parent (user, group, taxonomy_term).
|
||||
*
|
||||
* @param \Drupal\node\NodeInterface $node
|
||||
* The node.
|
||||
*
|
||||
* @return \Drupal\node\NodeInterface[]
|
||||
* Array of parent nodes, from closest to farthest.
|
||||
*/
|
||||
protected function getNodeParents(NodeInterface $node): array {
|
||||
$parents = [];
|
||||
$visited = [];
|
||||
$current = $node;
|
||||
|
||||
while ($current instanceof NodeInterface &&
|
||||
$current->hasField('field_parent_page') &&
|
||||
!$current->get('field_parent_page')->isEmpty()) {
|
||||
|
||||
$parent_field = $current->get('field_parent_page')->first();
|
||||
if (!$parent_field) {
|
||||
break;
|
||||
}
|
||||
|
||||
$parent_entity_type = $parent_field->target_type ?? NULL;
|
||||
$parent_id = $parent_field->target_id ?? NULL;
|
||||
|
||||
if (!$parent_entity_type || !$parent_id) {
|
||||
break;
|
||||
}
|
||||
|
||||
// Only continue if parent is a node.
|
||||
if ($parent_entity_type !== 'node') {
|
||||
break;
|
||||
}
|
||||
|
||||
// Avoid infinite loops.
|
||||
if (isset($visited[$parent_id])) {
|
||||
break;
|
||||
}
|
||||
$visited[$parent_id] = TRUE;
|
||||
|
||||
$parent = $this->entityTypeManager
|
||||
->getStorage('node')
|
||||
->load($parent_id);
|
||||
|
||||
if (!$parent instanceof NodeInterface) {
|
||||
break;
|
||||
}
|
||||
|
||||
$parents[] = $parent;
|
||||
$current = $parent;
|
||||
}
|
||||
|
||||
return $parents;
|
||||
}
|
||||
|
||||
}
|
||||
319
src/Form/SiteStructureSettingsForm.php
Normal file
319
src/Form/SiteStructureSettingsForm.php
Normal file
@@ -0,0 +1,319 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\site_structure\Form;
|
||||
|
||||
use Drupal\Core\Config\ConfigFactoryInterface;
|
||||
use Drupal\Core\Entity\EntityTypeBundleInfoInterface;
|
||||
use Drupal\Core\Entity\EntityTypeManagerInterface;
|
||||
use Drupal\Core\Extension\ModuleHandlerInterface;
|
||||
use Drupal\Core\Form\ConfigFormBase;
|
||||
use Drupal\Core\Form\FormStateInterface;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
|
||||
/**
|
||||
* Configuration form for Site Structure module.
|
||||
*/
|
||||
class SiteStructureSettingsForm extends ConfigFormBase {
|
||||
|
||||
/**
|
||||
* The entity type manager.
|
||||
*
|
||||
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
|
||||
*/
|
||||
protected EntityTypeManagerInterface $entityTypeManager;
|
||||
|
||||
/**
|
||||
* The entity type bundle info service.
|
||||
*
|
||||
* @var \Drupal\Core\Entity\EntityTypeBundleInfoInterface
|
||||
*/
|
||||
protected EntityTypeBundleInfoInterface $entityTypeBundleInfo;
|
||||
|
||||
/**
|
||||
* The module handler.
|
||||
*
|
||||
* @var \Drupal\Core\Extension\ModuleHandlerInterface
|
||||
*/
|
||||
protected ModuleHandlerInterface $moduleHandler;
|
||||
|
||||
/**
|
||||
* Entity types that can be used as parents.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected array $supportedEntityTypes = [
|
||||
'node' => 'Content Types (node)',
|
||||
'taxonomy_term' => 'Taxonomy Vocabularies (taxonomy_term)',
|
||||
'user' => 'Users (user)',
|
||||
'group' => 'Groups (group)',
|
||||
];
|
||||
|
||||
/**
|
||||
* Constructs a SiteStructureSettingsForm object.
|
||||
*
|
||||
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
|
||||
* The factory for configuration objects.
|
||||
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
|
||||
* The entity type manager.
|
||||
* @param \Drupal\Core\Entity\EntityTypeBundleInfoInterface $entity_type_bundle_info
|
||||
* The entity type bundle info service.
|
||||
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
|
||||
* The module handler.
|
||||
*/
|
||||
public function __construct(
|
||||
ConfigFactoryInterface $config_factory,
|
||||
EntityTypeManagerInterface $entity_type_manager,
|
||||
EntityTypeBundleInfoInterface $entity_type_bundle_info,
|
||||
ModuleHandlerInterface $module_handler,
|
||||
) {
|
||||
parent::__construct($config_factory);
|
||||
$this->entityTypeManager = $entity_type_manager;
|
||||
$this->entityTypeBundleInfo = $entity_type_bundle_info;
|
||||
$this->moduleHandler = $module_handler;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function create(ContainerInterface $container): static {
|
||||
return new static(
|
||||
$container->get('config.factory'),
|
||||
$container->get('entity_type.manager'),
|
||||
$container->get('entity_type.bundle.info'),
|
||||
$container->get('module_handler'),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getFormId(): string {
|
||||
return 'site_structure_settings_form';
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getEditableConfigNames(): array {
|
||||
return ['site_structure.settings'];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function buildForm(array $form, FormStateInterface $form_state): array {
|
||||
$config = $this->config('site_structure.settings');
|
||||
$allowed_targets = $config->get('allowed_parent_targets') ?? [];
|
||||
|
||||
// Build a keyed array for easier lookup.
|
||||
$enabled_targets = [];
|
||||
foreach ($allowed_targets as $target) {
|
||||
$key = $target['entity_type'] . ':' . $target['bundle'];
|
||||
$enabled_targets[$key] = TRUE;
|
||||
// Handle wildcard bundles.
|
||||
if ($target['bundle'] === '*') {
|
||||
$enabled_targets[$target['entity_type'] . ':*'] = TRUE;
|
||||
}
|
||||
}
|
||||
|
||||
$form['description'] = [
|
||||
'#type' => 'markup',
|
||||
'#markup' => '<p>' . $this->t('Select which entity types and bundles can be used as parent for Content Page nodes. This allows creating hierarchical structures where content pages can be children of different entity types.') . '</p>',
|
||||
];
|
||||
|
||||
$form['context_info'] = [
|
||||
'#type' => 'markup',
|
||||
'#markup' => '<p>' . $this->t('<strong>Context behavior:</strong><br>
|
||||
- <em>Node/Taxonomy</em>: Content pages inherit the site section from the parent.<br>
|
||||
- <em>User</em>: Content pages are associated with the user profile page.<br>
|
||||
- <em>Group</em>: Content pages are associated with the group.') . '</p>',
|
||||
];
|
||||
|
||||
$form['allowed_parent_targets'] = [
|
||||
'#type' => 'fieldset',
|
||||
'#title' => $this->t('Allowed Parent Targets'),
|
||||
'#tree' => TRUE,
|
||||
];
|
||||
|
||||
foreach ($this->supportedEntityTypes as $entity_type => $label) {
|
||||
// Check if entity type exists.
|
||||
if (!$this->entityTypeManager->hasDefinition($entity_type)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Special handling for 'group' - check if module is enabled.
|
||||
if ($entity_type === 'group' && !$this->moduleHandler->moduleExists('group')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$form['allowed_parent_targets'][$entity_type] = [
|
||||
'#type' => 'details',
|
||||
'#title' => $this->t($label),
|
||||
'#open' => TRUE,
|
||||
];
|
||||
|
||||
// User entity type typically has only one bundle.
|
||||
if ($entity_type === 'user') {
|
||||
$key = 'user:user';
|
||||
$form['allowed_parent_targets'][$entity_type]['user'] = [
|
||||
'#type' => 'checkbox',
|
||||
'#title' => $this->t('User accounts'),
|
||||
'#description' => $this->t('Allow content pages to be children of user profiles.'),
|
||||
'#default_value' => isset($enabled_targets[$key]),
|
||||
];
|
||||
continue;
|
||||
}
|
||||
|
||||
$bundles = $this->entityTypeBundleInfo->getBundleInfo($entity_type);
|
||||
|
||||
// For groups, add an "all types" option.
|
||||
if ($entity_type === 'group') {
|
||||
$key = 'group:*';
|
||||
$form['allowed_parent_targets'][$entity_type]['_all'] = [
|
||||
'#type' => 'checkbox',
|
||||
'#title' => $this->t('All group types'),
|
||||
'#description' => $this->t('Allow all current and future group types as parents.'),
|
||||
'#default_value' => isset($enabled_targets[$key]),
|
||||
];
|
||||
}
|
||||
|
||||
foreach ($bundles as $bundle_id => $bundle_info) {
|
||||
$key = $entity_type . ':' . $bundle_id;
|
||||
$form['allowed_parent_targets'][$entity_type][$bundle_id] = [
|
||||
'#type' => 'checkbox',
|
||||
'#title' => $bundle_info['label'],
|
||||
'#default_value' => isset($enabled_targets[$key]),
|
||||
];
|
||||
|
||||
// If "all group types" is selected, disable individual checkboxes.
|
||||
if ($entity_type === 'group') {
|
||||
$form['allowed_parent_targets'][$entity_type][$bundle_id]['#states'] = [
|
||||
'disabled' => [
|
||||
':input[name="allowed_parent_targets[group][_all]"]' => ['checked' => TRUE],
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return parent::buildForm($form, $form_state);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function submitForm(array &$form, FormStateInterface $form_state): void {
|
||||
$targets = [];
|
||||
$values = $form_state->getValue('allowed_parent_targets');
|
||||
|
||||
foreach ($this->supportedEntityTypes as $entity_type => $label) {
|
||||
if (empty($values[$entity_type])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Handle "all group types" wildcard.
|
||||
if ($entity_type === 'group' && !empty($values[$entity_type]['_all'])) {
|
||||
$targets[] = [
|
||||
'entity_type' => 'group',
|
||||
'bundle' => '*',
|
||||
];
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($values[$entity_type] as $bundle => $enabled) {
|
||||
// Skip the special "_all" key for groups if not enabled.
|
||||
if ($bundle === '_all') {
|
||||
continue;
|
||||
}
|
||||
if ($enabled) {
|
||||
$targets[] = [
|
||||
'entity_type' => $entity_type,
|
||||
'bundle' => $bundle,
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->config('site_structure.settings')
|
||||
->set('allowed_parent_targets', $targets)
|
||||
->save();
|
||||
|
||||
// Update field configuration to reflect new targets.
|
||||
$this->updateFieldConfiguration($targets);
|
||||
|
||||
parent::submitForm($form, $form_state);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the field_parent_page configuration with new targets.
|
||||
*
|
||||
* @param array $targets
|
||||
* The allowed targets.
|
||||
*/
|
||||
protected function updateFieldConfiguration(array $targets): void {
|
||||
$field_config = $this->entityTypeManager
|
||||
->getStorage('field_config')
|
||||
->load('node.content_page.field_parent_page');
|
||||
|
||||
if (!$field_config) {
|
||||
return;
|
||||
}
|
||||
|
||||
$settings = $field_config->getSettings();
|
||||
|
||||
// Build the entity type settings for dynamic_entity_reference.
|
||||
$entity_type_ids = [];
|
||||
$entity_type_settings = [];
|
||||
|
||||
foreach ($targets as $target) {
|
||||
$entity_type = $target['entity_type'];
|
||||
$bundle = $target['bundle'];
|
||||
|
||||
if (!in_array($entity_type, $entity_type_ids)) {
|
||||
$entity_type_ids[] = $entity_type;
|
||||
}
|
||||
|
||||
if (!isset($entity_type_settings[$entity_type])) {
|
||||
$sort_field = match($entity_type) {
|
||||
'taxonomy_term' => 'name',
|
||||
'user' => 'name',
|
||||
'group' => 'label',
|
||||
default => 'title',
|
||||
};
|
||||
|
||||
$entity_type_settings[$entity_type] = [
|
||||
'handler' => 'default:' . $entity_type,
|
||||
'handler_settings' => [
|
||||
'target_bundles' => [],
|
||||
'sort' => [
|
||||
'field' => $sort_field,
|
||||
'direction' => 'asc',
|
||||
],
|
||||
'auto_create' => FALSE,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
// Handle wildcard bundles.
|
||||
if ($bundle === '*') {
|
||||
$entity_type_settings[$entity_type]['handler_settings']['target_bundles'] = NULL;
|
||||
}
|
||||
elseif ($entity_type_settings[$entity_type]['handler_settings']['target_bundles'] !== NULL) {
|
||||
$entity_type_settings[$entity_type]['handler_settings']['target_bundles'][$bundle] = $bundle;
|
||||
}
|
||||
}
|
||||
|
||||
$settings['entity_type_ids'] = $entity_type_ids;
|
||||
foreach ($entity_type_settings as $entity_type => $type_settings) {
|
||||
$settings[$entity_type] = $type_settings;
|
||||
}
|
||||
|
||||
$field_config->setSettings($settings);
|
||||
$field_config->save();
|
||||
|
||||
$this->messenger()->addStatus($this->t('Field configuration updated successfully.'));
|
||||
}
|
||||
|
||||
}
|
||||
306
translations/pt-br.po
Normal file
306
translations/pt-br.po
Normal file
@@ -0,0 +1,306 @@
|
||||
# Portuguese (Brazil) translation for Site Structure module.
|
||||
# Copyright (C) 2024
|
||||
# This file is distributed under the same license as the Drupal package.
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Site Structure 1.0.0\n"
|
||||
"POT-Creation-Date: 2024-01-01 00:00+0000\n"
|
||||
"PO-Revision-Date: 2024-01-01 00:00+0000\n"
|
||||
"Language-Team: Portuguese, Brazil\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Language: pt-br\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
|
||||
|
||||
#: site_structure.info.yml
|
||||
msgid "Site Structure"
|
||||
msgstr "Estrutura do Site"
|
||||
|
||||
#: site_structure.info.yml
|
||||
msgid "Implements hierarchical editorial structure and contextual navigation for institutional sites."
|
||||
msgstr "Implementa estrutura editorial hierárquica e navegação contextual para sites institucionais."
|
||||
|
||||
#: config/install/taxonomy.vocabulary.site_section.yml
|
||||
msgid "Site Section"
|
||||
msgstr "Seção do Site"
|
||||
|
||||
#: config/install/taxonomy.vocabulary.site_section.yml
|
||||
msgid "Main hierarchical structure for site organization."
|
||||
msgstr "Estrutura principal de organização hierárquica do site."
|
||||
|
||||
#: config/install/node.type.section_page.yml
|
||||
msgid "Section Page"
|
||||
msgstr "Página Institucional"
|
||||
|
||||
#: config/install/node.type.section_page.yml
|
||||
msgid "Pages organized by site section, with URLs and breadcrumbs based on taxonomy."
|
||||
msgstr "Páginas organizadas por seção do site, com URLs e breadcrumbs baseados na taxonomia."
|
||||
|
||||
#: config/install/node.type.content_page.yml
|
||||
msgid "Content Page"
|
||||
msgstr "Página de Conteúdo"
|
||||
|
||||
#: config/install/node.type.content_page.yml
|
||||
msgid "Pages with hierarchical parent-child structure for content organization."
|
||||
msgstr "Páginas com estrutura hierárquica pai-filho para organização de conteúdo."
|
||||
|
||||
#: config/install/node.type.content_page.yml
|
||||
msgid "For root pages, fill in the Site Section manually. For child pages, select the parent page and the section will be inherited automatically."
|
||||
msgstr "Para páginas raiz, preencha a Seção do Site manualmente. Para páginas filhas, selecione a página pai e a seção será herdada automaticamente."
|
||||
|
||||
#: config/install/field.field.node.section_page.field_site_section.yml
|
||||
#: config/install/field.field.node.content_page.field_site_section.yml
|
||||
msgid "Select the site section this page belongs to."
|
||||
msgstr "Selecione a seção do site à qual esta página pertence."
|
||||
|
||||
#: config/install/field.field.node.content_page.field_site_section.yml
|
||||
msgid "For root pages, select the section. For child pages, this field is filled automatically."
|
||||
msgstr "Para páginas raiz, selecione a seção. Para páginas filhas, este campo é preenchido automaticamente."
|
||||
|
||||
#: config/install/field.field.node.content_page.field_parent_page.yml
|
||||
msgid "Parent Page"
|
||||
msgstr "Página Pai"
|
||||
|
||||
#: config/install/field.field.node.content_page.field_parent_page.yml
|
||||
msgid "Select the parent entity. The site section will be inherited automatically based on the parent type."
|
||||
msgstr "Selecione a entidade pai. A seção do site será herdada automaticamente com base no tipo do pai."
|
||||
|
||||
#: config/install/field.field.node.section_page.body.yml
|
||||
#: config/install/field.field.node.content_page.body.yml
|
||||
msgid "Body"
|
||||
msgstr "Corpo"
|
||||
|
||||
#: config/install/pathauto.pattern.site_section_term.yml
|
||||
msgid "Site Section Term"
|
||||
msgstr "Termo de Seção do Site"
|
||||
|
||||
#: config/install/views.view.child_pages.yml
|
||||
msgid "Child Pages"
|
||||
msgstr "Páginas Filhas"
|
||||
|
||||
#: config/install/views.view.child_pages.yml
|
||||
msgid "Lists the child pages of a content_page."
|
||||
msgstr "Lista as páginas filhas de uma página de conteúdo."
|
||||
|
||||
#: config/install/views.view.child_pages.yml
|
||||
msgid "No child pages found."
|
||||
msgstr "Nenhuma página filha encontrada."
|
||||
|
||||
#: config/install/views.view.child_pages.yml
|
||||
msgid "Apply"
|
||||
msgstr "Aplicar"
|
||||
|
||||
#: config/install/views.view.child_pages.yml
|
||||
msgid "Reset"
|
||||
msgstr "Limpar"
|
||||
|
||||
#: config/install/views.view.child_pages.yml
|
||||
msgid "Sort by"
|
||||
msgstr "Ordenar por"
|
||||
|
||||
#: config/install/views.view.child_pages.yml
|
||||
msgid "Asc"
|
||||
msgstr "Crescente"
|
||||
|
||||
#: config/install/views.view.child_pages.yml
|
||||
msgid "Desc"
|
||||
msgstr "Decrescente"
|
||||
|
||||
#: config/install/views.view.child_pages.yml
|
||||
msgid "All"
|
||||
msgstr "Todos"
|
||||
|
||||
#: config/install/views.view.child_pages.yml
|
||||
msgid "Block: Child Pages"
|
||||
msgstr "Bloco: Páginas Filhas"
|
||||
|
||||
#: site_structure.install
|
||||
msgid "Site Structure module installed successfully. Configure Pathauto patterns at /admin/config/search/path/patterns"
|
||||
msgstr "Módulo Site Structure instalado com sucesso. Configure os padrões Pathauto em /admin/config/search/path/patterns"
|
||||
|
||||
#: site_structure.install
|
||||
msgid "Site Structure module uninstalled successfully."
|
||||
msgstr "Módulo Site Structure desinstalado com sucesso."
|
||||
|
||||
#: site_structure.install
|
||||
msgid "Deleted @count @bundle nodes."
|
||||
msgstr "@count nós do tipo @bundle excluídos."
|
||||
|
||||
#: site_structure.install
|
||||
msgid "No terms in site_section vocabulary"
|
||||
msgstr "Nenhum termo no vocabulário site_section"
|
||||
|
||||
#: site_structure.install
|
||||
msgid "The Site Structure module requires terms in the \"Site Section\" vocabulary. <a href=\":url\">Add terms</a>."
|
||||
msgstr "O módulo Site Structure requer termos no vocabulário \"Seção do Site\". <a href=\":url\">Adicionar termos</a>."
|
||||
|
||||
#: site_structure.install
|
||||
msgid "@count terms configured"
|
||||
msgstr "@count termos configurados"
|
||||
|
||||
#: site_structure.module
|
||||
msgid "Circular reference detected. A page cannot be a parent of itself or its ancestors."
|
||||
msgstr "Referência circular detectada. Uma página não pode ser pai de si mesma ou de seus ancestrais."
|
||||
|
||||
#: site_structure.module
|
||||
msgid "Site Section Path"
|
||||
msgstr "Caminho da Seção do Site"
|
||||
|
||||
#: site_structure.module
|
||||
msgid "The hierarchical path of the site_section taxonomy (e.g., undergraduate/courses)."
|
||||
msgstr "O caminho hierárquico da taxonomia site_section (ex: graduacao/cursos)."
|
||||
|
||||
#: site_structure.module
|
||||
msgid "Hierarchy Path"
|
||||
msgstr "Caminho Hierárquico"
|
||||
|
||||
#: site_structure.module
|
||||
msgid "The hierarchical path of the term including ancestors (e.g., institutional/news)."
|
||||
msgstr "O caminho hierárquico do termo incluindo ancestrais (ex: institucional/noticias)."
|
||||
|
||||
#: src/Breadcrumb/SectionBreadcrumbBuilder.php
|
||||
msgid "Home"
|
||||
msgstr "Início"
|
||||
|
||||
#: src/Form/SiteStructureSettingsForm.php
|
||||
msgid "Site Structure Settings"
|
||||
msgstr "Configurações da Estrutura do Site"
|
||||
|
||||
#: src/Form/SiteStructureSettingsForm.php
|
||||
msgid "Select which entity types and bundles can be used as parent for Content Page nodes. This allows creating hierarchical structures where content pages can be children of different entity types."
|
||||
msgstr "Selecione quais tipos de entidade e bundles podem ser usados como pai para nós de Página de Conteúdo. Isso permite criar estruturas hierárquicas onde páginas de conteúdo podem ser filhas de diferentes tipos de entidade."
|
||||
|
||||
#: src/Form/SiteStructureSettingsForm.php
|
||||
msgid "<strong>Context behavior:</strong><br>\n - <em>Node/Taxonomy</em>: Content pages inherit the site section from the parent.<br>\n - <em>User</em>: Content pages are associated with the user profile page.<br>\n - <em>Group</em>: Content pages are associated with the group."
|
||||
msgstr "<strong>Comportamento de contexto:</strong><br>\n - <em>Node/Taxonomia</em>: Páginas de conteúdo herdam a seção do site do pai.<br>\n - <em>Usuário</em>: Páginas de conteúdo são associadas à página de perfil do usuário.<br>\n - <em>Grupo</em>: Páginas de conteúdo são associadas ao grupo."
|
||||
|
||||
#: src/Form/SiteStructureSettingsForm.php
|
||||
msgid "Allowed Parent Targets"
|
||||
msgstr "Alvos de Pai Permitidos"
|
||||
|
||||
#: src/Form/SiteStructureSettingsForm.php
|
||||
msgid "Content Types (node)"
|
||||
msgstr "Tipos de Conteúdo (node)"
|
||||
|
||||
#: src/Form/SiteStructureSettingsForm.php
|
||||
msgid "Taxonomy Vocabularies (taxonomy_term)"
|
||||
msgstr "Vocabulários de Taxonomia (taxonomy_term)"
|
||||
|
||||
#: src/Form/SiteStructureSettingsForm.php
|
||||
msgid "Users (user)"
|
||||
msgstr "Usuários (user)"
|
||||
|
||||
#: src/Form/SiteStructureSettingsForm.php
|
||||
msgid "Groups (group)"
|
||||
msgstr "Grupos (group)"
|
||||
|
||||
#: src/Form/SiteStructureSettingsForm.php
|
||||
msgid "User accounts"
|
||||
msgstr "Contas de usuário"
|
||||
|
||||
#: src/Form/SiteStructureSettingsForm.php
|
||||
msgid "Allow content pages to be children of user profiles."
|
||||
msgstr "Permite que páginas de conteúdo sejam filhas de perfis de usuário."
|
||||
|
||||
#: src/Form/SiteStructureSettingsForm.php
|
||||
msgid "All group types"
|
||||
msgstr "Todos os tipos de grupo"
|
||||
|
||||
#: src/Form/SiteStructureSettingsForm.php
|
||||
msgid "Allow all current and future group types as parents."
|
||||
msgstr "Permite todos os tipos de grupo atuais e futuros como pais."
|
||||
|
||||
#: src/Form/SiteStructureSettingsForm.php
|
||||
msgid "Field configuration updated successfully."
|
||||
msgstr "Configuração do campo atualizada com sucesso."
|
||||
|
||||
#: site_structure.links.menu.yml
|
||||
msgid "Configure allowed parent entity types for content pages."
|
||||
msgstr "Configure os tipos de entidade pai permitidos para páginas de conteúdo."
|
||||
|
||||
#: Default taxonomy terms (site_structure.install)
|
||||
msgid "News"
|
||||
msgstr "Notícias"
|
||||
|
||||
msgid "Events"
|
||||
msgstr "Eventos"
|
||||
|
||||
msgid "People"
|
||||
msgstr "Pessoas"
|
||||
|
||||
msgid "Institutional"
|
||||
msgstr "Institucional"
|
||||
|
||||
msgid "About"
|
||||
msgstr "Sobre"
|
||||
|
||||
msgid "Communication"
|
||||
msgstr "Comunicação"
|
||||
|
||||
msgid "Information and Services"
|
||||
msgstr "Informações e Serviços"
|
||||
|
||||
msgid "Team"
|
||||
msgstr "Equipe"
|
||||
|
||||
msgid "Management"
|
||||
msgstr "Gestão"
|
||||
|
||||
msgid "Inclusion and Belonging"
|
||||
msgstr "Inclusão e Pertencimento"
|
||||
|
||||
msgid "Undergraduate"
|
||||
msgstr "Graduação"
|
||||
|
||||
msgid "Statistics"
|
||||
msgstr "Estatística"
|
||||
|
||||
msgid "Mathematics"
|
||||
msgstr "Matemática"
|
||||
|
||||
msgid "Applied Mathematics"
|
||||
msgstr "Matemática Aplicada"
|
||||
|
||||
msgid "Mathematics Teaching"
|
||||
msgstr "Licenciatura em Matemática"
|
||||
|
||||
msgid "Graduate"
|
||||
msgstr "Pós-Graduação"
|
||||
|
||||
msgid "Statistics Program"
|
||||
msgstr "Programa de Estatística"
|
||||
|
||||
msgid "Mathematics Program"
|
||||
msgstr "Programa de Matemática"
|
||||
|
||||
msgid "Applied Mathematics Program"
|
||||
msgstr "Programa de Matemática Aplicada"
|
||||
|
||||
msgid "Research"
|
||||
msgstr "Pesquisa"
|
||||
|
||||
msgid "Extension"
|
||||
msgstr "Extensão"
|
||||
|
||||
msgid "Administration"
|
||||
msgstr "Administração"
|
||||
|
||||
msgid "Departments"
|
||||
msgstr "Departamentos"
|
||||
|
||||
msgid "Statistics Department"
|
||||
msgstr "Departamento de Estatística"
|
||||
|
||||
msgid "Mathematics Department"
|
||||
msgstr "Departamento de Matemática"
|
||||
|
||||
msgid "Applied Mathematics Department"
|
||||
msgstr "Departamento de Matemática Aplicada"
|
||||
|
||||
msgid "Library"
|
||||
msgstr "Biblioteca"
|
||||
|
||||
msgid "IT Services"
|
||||
msgstr "Informática"
|
||||
Reference in New Issue
Block a user