From 63adc01acaf00b86e55dc443284a988b2e25a080 Mon Sep 17 00:00:00 2001 From: Francesco Sardara Date: Fri, 19 Jul 2024 19:31:54 +0200 Subject: [PATCH 1/3] OEL-2632: Split code for status badge and progress. --- .../oe_whitelabel_extra_project.module | 47 +++++++++++++------ 1 file changed, 32 insertions(+), 15 deletions(-) diff --git a/modules/oe_whitelabel_extra_project/oe_whitelabel_extra_project.module b/modules/oe_whitelabel_extra_project/oe_whitelabel_extra_project.module index 3f0c451b..75fce0c2 100644 --- a/modules/oe_whitelabel_extra_project/oe_whitelabel_extra_project.module +++ b/modules/oe_whitelabel_extra_project/oe_whitelabel_extra_project.module @@ -66,6 +66,7 @@ function oe_whitelabel_extra_project_preprocess_node__oe_project__oe_w_content_b */ function oe_whitelabel_extra_project_preprocess_node__oe_project__teaser(array &$variables): void { _oe_whitelabel_extra_project_preprocess_featured_media($variables); + _oe_whitelabel_extra_project_preprocess_status($variables); } /** @@ -168,7 +169,7 @@ function _oe_whitelabel_extra_project_preprocess_inpage_nav(array &$variables): * @param array $variables * Variables from hook_preprocess_node(). */ -function _oe_whitelabel_extra_project_preprocess_status_and_progress(array &$variables): void { +function _oe_whitelabel_extra_project_preprocess_status(array &$variables): void { /** @var \Drupal\node\NodeInterface $node */ $node = $variables['node']; /** @var \Drupal\datetime_range\Plugin\Field\FieldType\DateRangeItem|null $date_range_item */ @@ -203,6 +204,34 @@ function _oe_whitelabel_extra_project_preprocess_status_and_progress(array &$var return; } + $status_labels = [t('Planned'), t('Ongoing'), t('Closed')]; + + $variables['project_status_badge_args'] = [ + 'label' => '…', + 'outline' => FALSE, + 'attributes' => new Attribute([ + 'data-start-timestamp' => $t_start, + 'data-end-timestamp' => $t_end, + 'data-status-labels' => implode('|', $status_labels), + 'class' => [ + // Hide for non-js users, to avoid showing wrong/outdated information. + 'd-none', + // Add a class to be able to target it with the JS library. + 'oe-wt-project__status', + ], + ]), + ]; +} + +/** + * Adds variables for the project status and progress component. + * + * @param array $variables + * Variables from hook_preprocess_node(). + */ +function _oe_whitelabel_extra_project_preprocess_status_and_progress(array &$variables): void { + _oe_whitelabel_extra_project_preprocess_status($variables); + // Use the formatted field values for start / end date. $element = $variables['elements']['oe_project_dates'][0] ?? []; if ($element['#theme'] ?? NULL === 'time') { @@ -220,11 +249,7 @@ function _oe_whitelabel_extra_project_preprocess_status_and_progress(array &$var return; } - $status_labels = [t('Planned'), t('Ongoing'), t('Closed')]; - // Values for the 'bcl-project-status' component. - // Some values contain placeholders that will be updated with javascript. - // This makes sure that tests will fail if js does not run. $variables['project_status_args'] = [ // Placeholder value. 'status' => 'planned', @@ -233,18 +258,10 @@ function _oe_whitelabel_extra_project_preprocess_status_and_progress(array &$var 'end_date' => $end_date_element, 'end_label' => t('End'), 'label' => t('Status'), - // Placeholder value. - 'badge' => '&ellipsis;', + 'badge' => '…', // Placeholder value, identical to 'planned'. 'progress' => 0, - 'attributes' => new Attribute([ - 'data-start-timestamp' => $t_start, - 'data-end-timestamp' => $t_end, - 'data-status-labels' => implode('|', $status_labels), - // Hide for non-js users, to avoid showing wrong/outdated information. - 'class' => ['d-none'], - ]), - ]; + ] + $variables['project_status_badge_args']; } /** From 26eb2b51b418fb5400e22b94f7d71229640e0eec Mon Sep 17 00:00:00 2001 From: Francesco Sardara Date: Fri, 19 Jul 2024 19:32:40 +0200 Subject: [PATCH 2/3] OEL-2632: Allow JS code to run on teaser and full view mode scenarios. --- resources/js/oe_whitelabel.project_status.js | 100 ++++++++++++------- 1 file changed, 63 insertions(+), 37 deletions(-) diff --git a/resources/js/oe_whitelabel.project_status.js b/resources/js/oe_whitelabel.project_status.js index c9a7fe32..83ddea75 100644 --- a/resources/js/oe_whitelabel.project_status.js +++ b/resources/js/oe_whitelabel.project_status.js @@ -2,8 +2,13 @@ * @file * Attaches behaviors for the project status element. */ -(function (bootstrap, Drupal, once) { +(function (Drupal, once) { + /** + * List of background classes. + * + * @type {string[]} + */ const colorClasses = [ 'bg-secondary', 'bg-info', @@ -20,43 +25,64 @@ */ Drupal.behaviors.projectStatus = { attach: function (context) { - const bclProjectStatus = once('bcl-project-status', '.bcl-project-status', context); - - bclProjectStatus.forEach(function (element) { - var msBegin = element.dataset.startTimestamp * 1000; - var msEnd = element.dataset.endTimestamp * 1000; - var statusLabels = element.dataset.statusLabels.split('|'); - var msNow = Date.now(); - // Calculate a status id: planned = 0, ongoing = 1, closed = 2. - var status = (msNow >= msBegin) + (msNow > msEnd); - // Calculate a progress: planned = 0, ongoing = 0..1, closed = 1. - var progress01 = Math.max(0, Math.min(1, (msNow - msBegin) / (msEnd - msBegin))); - // Convert to percent: planned = 0%, ongoing = 0%..100%, closed = 100%. - // Round to 1%, to avoid overwhelming float digits in aria attributes. - var percent = Math.round(progress01 * 100); - - // Process the status label. - var badges = element.getElementsByClassName('badge'); - Array.from(badges).forEach(function(badge) { - badge.classList.remove(...colorClasses); - badge.classList.add(colorClasses[status]); - badge.innerHTML = statusLabels[status]; - }); - - // Process the progress bar. - var progressBars = element.getElementsByClassName('progress-bar'); - Array.from(progressBars).forEach(function(progressBar) { - progressBar.classList.remove(...colorClasses); - progressBar.classList.add(colorClasses[status]); - progressBar.style.width = percent + '%'; - progressBar.setAttribute('aria-valuenow', percent); - progressBar.setAttribute('aria-label', percent); - }); - - // Reveal the entire section. - element.classList.remove('d-none'); + const statusComponents = once('oe-wt-project-status', '.bcl-project-status', context); + const statusBadges = once('oe-wt-project-status', '.badge.oe-wt-project__status', context); + + statusComponents.forEach(function (wrapper) { + const badges = wrapper.getElementsByClassName('badge'); + const progressBars = wrapper.getElementsByClassName('progress-bar'); + + calculateProjectStatusAndProgress(wrapper, badges, progressBars); + }); + + statusBadges.forEach(function (wrapper) { + // Status badges don't have the progress element. + calculateProjectStatusAndProgress(wrapper, [wrapper], []); }); } }; -})(bootstrap, Drupal, once); + /** + * Calculates the values for the project status and progress elements. + * + * @param {Element} wrapper + * The element that holds the project data. + * @param {array.} badges + * A list of badges to process. + * @param {array.} progressBars + * A list of progress bars to process. + */ + function calculateProjectStatusAndProgress(wrapper, badges, progressBars) { + const msBegin = wrapper.dataset.startTimestamp * 1000; + const msEnd = wrapper.dataset.endTimestamp * 1000; + const statusLabels = wrapper.dataset.statusLabels.split('|'); + const msNow = Date.now(); + // Calculate a status id: planned = 0, ongoing = 1, closed = 2. + const status = (msNow >= msBegin) + (msNow > msEnd); + // Calculate a progress: planned = 0, ongoing = 0..1, closed = 1. + const progress01 = Math.max(0, Math.min(1, (msNow - msBegin) / (msEnd - msBegin))); + // Convert to percent: planned = 0%, ongoing = 0%..100%, closed = 100%. + // Round to 1%, to avoid overwhelming float digits in aria attributes. + const percent = Math.round(progress01 * 100); + + // Process the status label. + Array.from(badges).forEach(function (badge) { + badge.classList.remove(...colorClasses); + badge.classList.add(colorClasses[status]); + badge.innerHTML = statusLabels[status]; + }); + + // Process the progress bar. + Array.from(progressBars).forEach(function (progressBar) { + progressBar.classList.remove(...colorClasses); + progressBar.classList.add(colorClasses[status]); + progressBar.style.width = percent + '%'; + progressBar.setAttribute('aria-valuenow', percent); + progressBar.setAttribute('aria-label', percent); + }); + + // Reveal the wrapper element. + wrapper.classList.remove('d-none'); + } + +})(Drupal, once); From 849a602d24fed668802bbe1e86c2afcf483d9bf0 Mon Sep 17 00:00:00 2001 From: Francesco Sardara Date: Fri, 19 Jul 2024 19:33:03 +0200 Subject: [PATCH 3/3] OEL-2632: Render the status badge in the teaser view mode. --- .../node--oe-project--teaser.html.twig | 4 + .../ContentProjectRenderTest.php | 74 +++++++++++++++++-- tests/src/Kernel/ProjectRenderTest.php | 6 +- 3 files changed, 77 insertions(+), 7 deletions(-) diff --git a/templates/content/node--oe-project--teaser.html.twig b/templates/content/node--oe-project--teaser.html.twig index dc13a497..d35e0c9b 100644 --- a/templates/content/node--oe-project--teaser.html.twig +++ b/templates/content/node--oe-project--teaser.html.twig @@ -9,6 +9,10 @@ {% endset %} {% set _badges = [] %} {% set _meta = [] %} +{% if project_status_badge_args is not empty %} + {{ attach_library('oe_whitelabel/project_status') }} + {% set _badges = _badges|merge([project_status_badge_args]) %} +{% endif %} {% for _item in content.oe_subject|field_value %} {% set _badges = _badges|merge([{ label: _item, diff --git a/tests/src/FunctionalJavascript/ContentProjectRenderTest.php b/tests/src/FunctionalJavascript/ContentProjectRenderTest.php index 9d334f2f..96ed566c 100644 --- a/tests/src/FunctionalJavascript/ContentProjectRenderTest.php +++ b/tests/src/FunctionalJavascript/ContentProjectRenderTest.php @@ -4,6 +4,7 @@ namespace Drupal\Tests\oe_whitelabel\FunctionalJavascript; +use Behat\Mink\Element\NodeElement; use Drupal\Core\Datetime\DrupalDateTime; use Drupal\Core\Entity\EntityStorageInterface; use Drupal\Core\Url; @@ -15,7 +16,7 @@ use Drupal\Tests\oe_bootstrap_theme\PatternAssertion\DescriptionListAssert; use Drupal\Tests\oe_bootstrap_theme\PatternAssertion\GalleryPatternAssert; use Drupal\Tests\oe_bootstrap_theme\PatternAssertion\InPageNavigationAssert; -use Drupal\Tests\oe_whitelabel\Traits\MediaCreationTrait; +use Drupal\Tests\oe_whitelabel\Traits\NodeCreationTrait; use Drupal\Tests\sparql_entity_storage\Traits\SparqlConnectionTrait; use Drupal\Tests\TestFileCreationTrait; use Drupal\user\Entity\Role; @@ -26,7 +27,7 @@ */ class ContentProjectRenderTest extends WebDriverTestBase { - use MediaCreationTrait; + use NodeCreationTrait; use SparqlConnectionTrait; use TestFileCreationTrait; @@ -377,6 +378,48 @@ public function testProjectRendering(): void { ], $gallery_container->getOuterHtml()); } + /** + * Tests the status badge rendering for the teaser view mode. + */ + public function testTeaserStatusBadge(): void { + // Create a project with dates set in the past. + $this->createProjectNode([ + 'oe_project_dates' => [ + 'value' => '2000-05-10', + 'end_value' => '2010-05-15', + ], + ]); + + // In order to render a teaser, we use the default node list view. + $this->drupalGet('/node'); + + $teasers = $this->getSession()->getPage()->findAll('css', 'article.listing-item'); + $this->assertCount(1, $teasers); + $this->assertTeaserStatusBadge($teasers[0], 'Closed', 'bg-dark'); + + // Create an ongoing project. + $this->createProjectNode([ + 'oe_project_dates' => [ + 'value' => '2010-05-10', + 'end_value' => '2100-05-15', + ], + ]); + // And a planned project. + $this->createProjectNode([ + 'oe_project_dates' => [ + 'value' => '2100-05-10', + 'end_value' => '2200-05-15', + ], + ]); + + $this->drupalGet('/node'); + $teasers = $this->getSession()->getPage()->findAll('css', 'article.listing-item'); + $this->assertCount(3, $teasers); + $this->assertTeaserStatusBadge($teasers[0], 'Planned', 'bg-secondary'); + $this->assertTeaserStatusBadge($teasers[1], 'Ongoing', 'bg-info'); + $this->assertTeaserStatusBadge($teasers[2], 'Closed', 'bg-dark'); + } + /** * Creates a stakeholder organisation entity. * @@ -430,10 +473,12 @@ protected function getStorage(string $entity_type_id): EntityStorageInterface { * End date string. */ protected function setProjectDateRange(NodeInterface $node, string $begin, string $end): void { - $node->oe_project_dates = [ - 'value' => (new DrupalDateTime($begin, 'Europe/Brussels'))->format('Y-m-d'), - 'end_value' => (new DrupalDateTime($end, 'Europe/Brussels'))->format('Y-m-d'), - ]; + $node->set('oe_project_dates', [ + [ + 'value' => (new DrupalDateTime($begin, 'Europe/Brussels'))->format('Y-m-d'), + 'end_value' => (new DrupalDateTime($end, 'Europe/Brussels'))->format('Y-m-d'), + ], + ]); } /** @@ -531,4 +576,21 @@ protected function assertProjectDates(string $expected_start_date, string $expec $this->assertEquals($expected_end_date, trim($end_element->getText())); } + /** + * Asserts the teaser status badge. + * + * @param \Behat\Mink\Element\NodeElement $wrapper + * The teaser wrapper element. + * @param string $status_label + * The expected status label. + * @param string $status_class + * The expected status class. + */ + protected function assertTeaserStatusBadge(NodeElement $wrapper, string $status_label, string $status_class): void { + $badges = $wrapper->findAll('css', '.badge'); + $this->assertCount(2, $badges); + $this->assertEquals($status_label, $badges[0]->getText()); + $this->assertTrue($badges[0]->hasClass($status_class)); + } + } diff --git a/tests/src/Kernel/ProjectRenderTest.php b/tests/src/Kernel/ProjectRenderTest.php index 5510a8de..f70f503b 100644 --- a/tests/src/Kernel/ProjectRenderTest.php +++ b/tests/src/Kernel/ProjectRenderTest.php @@ -100,7 +100,11 @@ public function testProjectTeaser(): void { 'title' => 'Project 1', 'url' => '/node/1', 'description' => 'The teaser text', - 'badges' => ['EU financing'], + 'badges' => [ + // The project status is only calculated when JavaScript is executed. + '…', + 'EU financing', + ], 'image' => [ 'src' => 'example_1.jpeg', 'alt' => 'Alternative text',