diff --git a/config/schema/archive_tree.schema.yml b/config/schema/archive_tree.schema.yml
new file mode 100644
index 0000000..ac95fad
--- /dev/null
+++ b/config/schema/archive_tree.schema.yml
@@ -0,0 +1,12 @@
+block.settings.archive_tree:
+ type: block_settings
+ label: 'Archive Tree block settings'
+ mapping:
+ content_types:
+ type: sequence
+ label: 'Content types'
+ sequence:
+ type: string
+ expand_years:
+ type: boolean
+ label: 'Expand years by default'
diff --git a/src/Plugin/Block/ArchiveTreeBlock.php b/src/Plugin/Block/ArchiveTreeBlock.php
index ffe219b..710e993 100644
--- a/src/Plugin/Block/ArchiveTreeBlock.php
+++ b/src/Plugin/Block/ArchiveTreeBlock.php
@@ -20,59 +20,26 @@ use Drupal\node\Entity\NodeType;
class ArchiveTreeBlock extends BlockBase {
/**
- * {@inheritdoc}
+ /**
+ * Build the archive tree block content.
+ *
+ * - Gets node IDs with access check for selected content types.
+ * - Fetches only nid and created fields for those nodes (efficient).
+ * - Loops through, increments year and month countersfor display.
+ * - Outputs accessible HTML with details/summary and lists.
+ * - Caches per user, language, and theme for performance.
*/
-
- public function defaultConfiguration() {
- return [
- 'expand_years' => FALSE,
- 'content_types' => [],
- ] + parent::defaultConfiguration();
- }
-
-
- public function blockForm($form, FormStateInterface $form_state) {
- $form = parent::blockForm($form, $form_state);
- $types = NodeType::loadMultiple();
- $options = [];
- foreach ($types as $type) {
- $options[$type->id()] = $type->label();
- }
- $form['content_types'] = [
- '#type' => 'checkboxes',
- '#title' => $this->t('Content types to include'),
- '#options' => $options,
- '#default_value' => isset($this->configuration['content_types']) ? $this->configuration['content_types'] : [],
- '#description' => $this->t('Select one or more content types to include in the archive tree.'),
- '#required' => TRUE,
- ];
- $form['expand_years'] = [
- '#type' => 'checkbox',
- '#title' => $this->t('Expand all years by default'),
- '#default_value' => $this->configuration['expand_years'],
- '#description' => $this->t('If checked, all years will be expanded by default.'),
- ];
- return $form;
- }
-
-
- public function blockSubmit($form, FormStateInterface $form_state) {
- parent::blockSubmit($form, $form_state);
- $this->configuration['expand_years'] = $form_state->getValue('expand_years');
- $selected_types = array_keys(array_filter($form_state->getValue('content_types')));
- $this->configuration['content_types'] = $selected_types;
- }
-
public function build(): array {
$storage = \Drupal::entityTypeManager()->getStorage('node');
$types = array_filter($this->configuration['content_types']);
+ // Show a message if no content types are selected.
if (empty($types)) {
- // If no types selected, return empty block.
return [
'#markup' => $this->t('No content types selected.'),
];
}
- // 1. Get node IDs with access check.
+
+ // 1. Get node IDs with access check (respects permissions).
$query = $storage->getQuery()
->condition('type', $types, 'IN')
->condition('status', 1)
@@ -82,7 +49,7 @@ class ArchiveTreeBlock extends BlockBase {
$tree = [];
if (!empty($nids)) {
- // 2. Fetch only nid and created fields for those nodes.
+ // 2. Fetch only nid and created fields for those nodes (faster than loading full entities).
$connection = \Drupal::database();
$result = $connection->select('node_field_data', 'n')
->fields('n', ['nid', 'created'])
@@ -103,11 +70,13 @@ class ArchiveTreeBlock extends BlockBase {
}
}
- krsort($tree); // Descending years
+ // Sort years and months in descending order for display.
+ krsort($tree);
foreach ($tree as $year => $data) {
- krsort($tree[$year]['months']); // Descending months
+ krsort($tree[$year]['months']);
}
+ // Build accessible HTML output for the archive tree.
$output = '';
$expand = !empty($this->configuration['expand_years']);
$type_arg = implode(',', $types);
@@ -130,6 +99,7 @@ class ArchiveTreeBlock extends BlockBase {
$output .= '';
}
+ // Return render array with proper cache settings and allowed tags.
return [
'#markup' => '
' . $output . '
',
'#allowed_tags' => ['details', 'summary', 'a', 'div', 'ul', 'li'],
@@ -139,18 +109,8 @@ class ArchiveTreeBlock extends BlockBase {
],
],
'#cache' => [
- // Cache per user, language, route, and theme for accuracy.
- 'contexts' => [
- 'user',
- 'languages',
- 'theme',
- ],
- // Invalidate when any node changes or content type config changes.
- 'tags' => [
- 'node_list',
- 'config:node.type',
- ],
- // Cache for 1 hour (3600 seconds) for performance, but not forever.
+ 'contexts' => ['user', 'languages', 'theme'],
+ 'tags' => ['node_list', 'config:node.type'],
'max-age' => 3600,
],
];
diff --git a/tests/src/Kernel/ArchiveTreeBlockKernelTest.php b/tests/src/Kernel/ArchiveTreeBlockKernelTest.php
new file mode 100644
index 0000000..7f177b1
--- /dev/null
+++ b/tests/src/Kernel/ArchiveTreeBlockKernelTest.php
@@ -0,0 +1,155 @@
+installEntitySchema('user');
+ $this->installEntitySchema('node');
+ $this->installConfig(['node']);
+ // Create four content types.
+ NodeType::create(['type' => 'article', 'name' => 'Article'])->save();
+ NodeType::create(['type' => 'book', 'name' => 'Book'])->save();
+ NodeType::create(['type' => 'essay', 'name' => 'Essay'])->save();
+ NodeType::create(['type' => 'poem', 'name' => 'Poem'])->save();
+ }
+
+ /**
+ * Test archive tree block output with mock nodes.
+ */
+ public function testArchiveTreeBlockOutput() {
+ // Sample nodes test a few scenarios:
+ // - Year totals add up correctly (2024 with 5, 2025 with 10).
+ // - Skipped year (2023) doesn't render.
+ // - One item in 2022 renders correctly (one showing in both month and year).
+ // - Month totals add up correctly.
+ // - 2024-01: 2
+ // - 2024-02: 3
+ // - 2025-09: 1
+ // - 2025-10: 4
+ // - 2025-11: 3
+ // - 2025-12: 2
+ // 2022: 1 node (article, Dec)
+ $this->createNode('article', strtotime('2022-12-25'));
+ // 2024: 5 nodes
+ $this->createNode('book', strtotime('2024-01-05'));
+ $this->createNode('poem', strtotime('2024-01-10'));
+ $this->createNode('essay', strtotime('2024-02-01'));
+ $this->createNode('book', strtotime('2024-02-15'));
+ $this->createNode('poem', strtotime('2024-02-20'));
+ // 2025: 10 nodes
+ $this->createNode('article', strtotime('2025-09-01'));
+ $this->createNode('essay', strtotime('2025-10-01'));
+ $this->createNode('essay', strtotime('2025-10-10'));
+ $this->createNode('book', strtotime('2025-10-15'));
+ $this->createNode('poem', strtotime('2025-10-20'));
+ $this->createNode('book', strtotime('2025-11-01'));
+ $this->createNode('poem', strtotime('2025-11-10'));
+ $this->createNode('essay', strtotime('2025-11-20'));
+ $this->createNode('article', strtotime('2025-12-01'));
+ $this->createNode('book', strtotime('2025-12-10'));
+
+ // Place the block with all types selected.
+ $block = Block::create([
+ 'id' => 'archive_tree_test',
+ 'plugin' => 'archive_tree',
+ 'region' => 'sidebar_first',
+ 'theme' => 'stark',
+ 'settings' => [
+ 'content_types' => ['article', 'book', 'essay', 'poem'],
+ 'expand_years' => TRUE,
+ ],
+ 'visibility' => [],
+ 'weight' => 0,
+ 'status' => 1,
+ ]);
+ $block->save();
+
+ // Render the block.
+ $block_plugin = \Drupal::service('plugin.manager.block')->createInstance('archive_tree', [
+ 'content_types' => ['article', 'book', 'essay', 'poem'],
+ 'expand_years' => TRUE,
+ ]);
+ $build = $block_plugin->build();
+ $output = \Drupal::service('renderer')->renderRoot($build);
+
+ // Assert year totals.
+ $this->assertStringContainsString('2022', $output); // 1 node in 2022
+ $this->assertStringContainsString('2024', $output); // 5 nodes in 2024
+ $this->assertStringContainsString('2025', $output); // 10 nodes in 2025
+ $this->assertStringNotContainsString('2023', $output); // 2023 skipped
+
+ // Parse the output to check month counts under the correct year.
+ $dom = new \DOMDocument();
+ @$dom->loadHTML('' . $output);
+ $xpath = new \DOMXPath($dom);
+
+ // Helper to get month/count pairs under a given year.
+ $getMonthsForYear = function($year) use ($xpath) {
+ foreach ($xpath->query('//details') as $details) {
+ $summary = $xpath->query('summary', $details)->item(0);
+ if ($summary && strpos($summary->textContent, (string)$year) !== false) {
+ $months = [];
+ foreach ($xpath->query('.//ul/li', $details) as $li) {
+ $months[] = trim($li->textContent);
+ }
+ return $months;
+ }
+ }
+ return [];
+ };
+
+ // 2024: 01 (2), 02 (3)
+ $months2024 = $getMonthsForYear(2024);
+ $this->assertContains('01 (2)', $months2024);
+ $this->assertContains('02 (3)', $months2024);
+
+ // 2025: 09 (1), 10 (4), 11 (3), 12 (2)
+ $months2025 = $getMonthsForYear(2025);
+ $this->assertContains('09 (1)', $months2025);
+ $this->assertContains('10 (4)', $months2025);
+ $this->assertContains('11 (3)', $months2025);
+ $this->assertContains('12 (2)', $months2025);
+
+ // 2022: 12 (1)
+ $months2022 = $getMonthsForYear(2022);
+ $this->assertContains('12 (1)', $months2022);
+ }
+
+ /**
+ * Helper to create a published node of a type and created date.
+ */
+ protected function createNode($type, $created) {
+ $node = Node::create([
+ 'type' => $type,
+ 'title' => $this->randomString(),
+ 'status' => 1,
+ 'created' => $created,
+ ]);
+ $node->save();
+ return $node;
+ }
+}