From 6fa7dadefc32abf8c67830255224494ec52d95fc Mon Sep 17 00:00:00 2001 From: bcgov-brwang <87880048+bcgov-brwang@users.noreply.github.com> Date: Tue, 9 Jun 2026 11:31:41 -0700 Subject: [PATCH 1/4] DBC22-6585: highlighted advisory for new updates DBC22-6585: highlighted advisory for new updates 2 DBC22-6585: highlighted bulletin for new updates --- .../Components/advisories/AdvisoriesList.js | 30 ++- .../src/Components/bulletins/BulletinsList.js | 27 ++- src/frontend/src/pages/AdvisoriesListPage.js | 193 +++++++++++++++-- .../src/pages/AdvisoriesListPage.scss | 42 ++++ src/frontend/src/pages/BulletinsListPage.js | 197 ++++++++++++++---- src/frontend/src/pages/BulletinsListPage.scss | 41 ++++ 6 files changed, 462 insertions(+), 68 deletions(-) diff --git a/src/frontend/src/Components/advisories/AdvisoriesList.js b/src/frontend/src/Components/advisories/AdvisoriesList.js index c292cb28c..fabae0f43 100644 --- a/src/frontend/src/Components/advisories/AdvisoriesList.js +++ b/src/frontend/src/Components/advisories/AdvisoriesList.js @@ -1,5 +1,5 @@ // React -import React, { useContext } from 'react'; +import React, { useContext, useRef } from 'react'; // Navigation import { useNavigate } from 'react-router-dom'; @@ -30,7 +30,8 @@ export default function AdvisoriesList(props) { const navigate = useNavigate(); // Props - const { advisories, showDescription, showTimestamp, showPublished, isAdvisoriesListPage, showLoader } = props; + const { advisories, showDescription, showTimestamp, showPublished, isAdvisoriesListPage, showLoader, trackedAdvisories = {}, advisoryRefs } = props; + const { dismissHighlight } = props; function handleClick(advisory, keyEvent) { // Ignore key presses that aren't enter or space @@ -39,6 +40,12 @@ export default function AdvisoriesList(props) { } trackEvent('click', 'advisories-list', 'Advisory', advisory.title, advisory.teaser); + // dismissedHighlightsRef.current.add(advisory.id); + // sessionStorage.setItem( + // 'dismissedHighlights', + // JSON.stringify([...dismissedHighlightsRef.current]) + // ); + dismissHighlight(advisory.id); // use parent's function navigate(`/advisories/${advisory.slug}`); } @@ -50,7 +57,12 @@ export default function AdvisoriesList(props) { {!!sortedAdvisories && sortedAdvisories.map((advisory, index) => { if (isAdvisoriesListPage) { return ( -
+
{ if (advisoryRefs) advisoryRefs.current[advisory.id] = el; }} + >
{advisory.first_published_at != advisory.last_published_at ? "Updated" : "Published" } + {trackedAdvisories[advisory.id]?.highlight && +
Updated
+ }
}
@@ -97,6 +112,9 @@ export default function AdvisoriesList(props) {
{advisory.first_published_at != advisory.last_published_at ? "Updated" : "Published"} + {trackedAdvisories[advisory.id]?.highlight && +
Updated
+ }
} @@ -141,6 +159,9 @@ export default function AdvisoriesList(props) { {!cmsContext.readAdvisories.includes(advisory.id.toString() + '-' + advisory.live_revision.toString()) &&
} {advisory.first_published_at != advisory.last_published_at ? "Updated" : "Published" } + {trackedAdvisories[advisory.id]?.highlight && +
Updated
+ }
} @@ -166,6 +187,9 @@ export default function AdvisoriesList(props) {
{advisory.first_published_at != advisory.last_published_at ? "Updated" : "Published" } + {trackedAdvisories[advisory.id]?.highlight && +
Updated
+ }
}
diff --git a/src/frontend/src/Components/bulletins/BulletinsList.js b/src/frontend/src/Components/bulletins/BulletinsList.js index 792f7e9b4..460584564 100644 --- a/src/frontend/src/Components/bulletins/BulletinsList.js +++ b/src/frontend/src/Components/bulletins/BulletinsList.js @@ -16,15 +16,16 @@ import Skeleton from 'react-loading-skeleton'; // Static files import logo from '../../images/dbc-logo--white.svg'; -export default function Bulletins(props) { - // State, props and context - const { bulletins, showLoader } = props; +export default function BulletinsList(props) { + // Props + const { bulletins, showLoader, trackedBulletins = {}, bulletinRefs, dismissHighlight } = props; // Navigation const navigate = useNavigate(); function handleClick(bulletin) { trackEvent('click', 'bulletins-list', 'Bulletin', bulletin.title, bulletin.teaser); + if (dismissHighlight) dismissHighlight(bulletin.id); navigate(`/bulletins/${bulletin.slug}`); } @@ -33,9 +34,14 @@ export default function Bulletins(props) { // Rendering return (
@@ -57,7 +56,7 @@ export default function Header({ isMaintenance }) { ); } - + const smallScreen = useMediaQuery('only screen and (max-width: 575px)'); // Check current page location @@ -81,6 +80,7 @@ export default function Header({ isMaintenance }) { // Context const { cmsContext } = useContext(CMSContext); + const cmsContextRef = useRef(cmsContext); // States const [advisoriesCount, setAdvisoriesCount] = useState(); @@ -91,17 +91,45 @@ export default function Header({ isMaintenance }) { const [isNavbarCollapsed, setIsNavbarCollapsed] = useState(true); const [isCommercialOpen, setIsCommercialOpen] = useState(false); const commercialDropdownRef = useRef(null); + const isInitialLoad = useRef(true); + const loadRef = useRef(null); + + /* Helpers */ + const getUnreadAdvisoriesCount = (advisoriesData) => { + if (!advisoriesData) return 0; + if (advisoriesData.length !== 0 && advisoriesData[0].live_revision == null) return 0; + + const readAdvisories = advisoriesData.filter(advisory => { + if (!advisory.id || !advisory.live_revision) return true; + return cmsContextRef.current.readAdvisories.includes( + advisory.id.toString() + '-' + advisory.live_revision.toString() + ); + }); + return advisoriesData.length - readAdvisories.length; + } + + const getUnreadBulletinsCount = (bulletinsData) => { + if (!bulletinsData) return 0; + if (bulletinsData.length !== 0 && bulletinsData[0].live_revision == null) return 0; - // Effects + const readBulletins = bulletinsData.filter(bulletin => + cmsContextRef.current.readBulletins.includes( + bulletin.id.toString() + '-' + bulletin.live_revision.toString() + ) + ); + return bulletinsData.length - readBulletins.length; + } + + /* Data loading */ const loadAdvisories = async () => { - // Skip loading if the advisories are already loaded on launch - if (advisories && advisories.length > 0 && advisories[0].live_revision != null) { + // On initial load, use cached data if available + if (advisories && advisories.length > 0 && advisories[0].live_revision != null && isInitialLoad.current) { setAdvisoriesCount(getUnreadAdvisoriesCount(filteredAdvisories)); + isInitialLoad.current = false; return; } const path = window.location.pathname; - let advisoriesData; if (path.includes("preview")) { advisoriesData = await getAdvisoriesPreview(); @@ -109,7 +137,10 @@ export default function Header({ isMaintenance }) { advisoriesData = await getAdvisories(); } - const filteredAdvisoriesData = selectedRoute ? filterAdvisoryByRoute(advisoriesData, selectedRoute) : advisoriesData; + const filteredAdvisoriesData = selectedRoute + ? filterAdvisoryByRoute(advisoriesData, selectedRoute) + : advisoriesData; + dispatch(updateAdvisories({ list: advisoriesData, filteredList: filteredAdvisoriesData, @@ -117,11 +148,12 @@ export default function Header({ isMaintenance }) { })); setAdvisoriesCount(getUnreadAdvisoriesCount(filteredAdvisoriesData)); + isInitialLoad.current = false; } const loadBulletins = async () => { - let bulletinsData; const path = window.location.pathname; + let bulletinsData; if (path.includes("preview")) { bulletinsData = await getBulletinsPreview(); } else { @@ -136,10 +168,28 @@ export default function Header({ isMaintenance }) { setBulletinsCount(getUnreadBulletinsCount(bulletinsData)); } + // Always point loadRef to the latest versions of load functions + loadRef.current = { loadAdvisories, loadBulletins }; + + /* Effects */ + + // Keep cmsContextRef in sync with latest cmsContext useEffect(() => { - loadAdvisories(); - loadBulletins(); - }, [cmsContext, location.pathname]); + cmsContextRef.current = cmsContext; + }, [cmsContext]); + + // Load on mount + pathname change + poll every 30s + useEffect(() => { + loadRef.current.loadAdvisories(); + loadRef.current.loadBulletins(); + + const interval = setInterval(() => { + loadRef.current.loadAdvisories(); + loadRef.current.loadBulletins(); + }, 30000); + + return () => clearInterval(interval); + }, [location.pathname]); useEffect(() => { if (searchLocationFrom.length && searchLocationTo.length) { @@ -154,7 +204,6 @@ export default function Header({ isMaintenance }) { }, [isNavbarCollapsed]); useEffect(() => { - // Close dropdown when clicking outside on xLarge screens const handleClickOutside = (event) => { if ( xLargeScreen && @@ -175,45 +224,6 @@ export default function Header({ isMaintenance }) { }; }, [xLargeScreen, isCommercialOpen]); - /* Helpers */ - const getUnreadAdvisoriesCount = (advisoriesData) => { - if (!advisoriesData) { - return 0; - } - - if (advisoriesData.length !== 0 && advisoriesData[0].live_revision == null) { - return 0; - } - - const readAdvisories = advisoriesData.filter(advisory => { - // Do not count preview items as unread - if (!advisory.id || !advisory.live_revision) { - return true; - } - - return cmsContext.readAdvisories.includes( - advisory.id.toString() + '-' + advisory.live_revision.toString() - ); - }); - - return advisoriesData.length - readAdvisories.length; - } - - const getUnreadBulletinsCount = (bulletinsData) => { - if (!bulletinsData) { - return 0; - } - - if (bulletinsData.length !== 0 && bulletinsData[0].live_revision == null) { - return 0; - } - - const readBulletins = bulletinsData.filter(bulletin => cmsContext.readBulletins.includes( - bulletin.id.toString() + '-' + bulletin.live_revision.toString() - )); - return bulletinsData.length - readBulletins.length; - } - /* Handlers */ const onClickActions = () => { setTimeout(() => setExpanded(false)); @@ -228,7 +238,6 @@ export default function Header({ isMaintenance }) { } /* Rendering */ - // Sub components const getNavLink = (title, count) => { return ( From 4c22703e8176aa0a9791a3fadd009ea95fa510f0 Mon Sep 17 00:00:00 2001 From: bcgov-brwang <87880048+bcgov-brwang@users.noreply.github.com> Date: Wed, 10 Jun 2026 12:15:56 -0700 Subject: [PATCH 3/4] DBC22-6585: added publish minor updates from backend --- ...t_notified_at_bulletin_last_notified_at.py | 23 +++++++ src/backend/apps/cms/models.py | 7 +- src/backend/apps/cms/views.py | 30 ++++++++ src/backend/apps/cms/wagtail_hooks.py | 68 ++++++++++++++++++- .../Components/advisories/AdvisoriesList.js | 17 ++--- .../src/Components/bulletins/BulletinsList.js | 4 +- .../src/Components/data/advisories.js | 15 ++-- src/frontend/src/Components/data/bulletins.js | 15 ++-- .../src/Components/shared/header/Header.js | 41 +++++------ src/frontend/src/pages/AdvisoriesListPage.js | 15 ++-- src/frontend/src/pages/BulletinsListPage.js | 13 ++-- 11 files changed, 184 insertions(+), 64 deletions(-) create mode 100644 src/backend/apps/cms/migrations/0026_advisory_last_notified_at_bulletin_last_notified_at.py diff --git a/src/backend/apps/cms/migrations/0026_advisory_last_notified_at_bulletin_last_notified_at.py b/src/backend/apps/cms/migrations/0026_advisory_last_notified_at_bulletin_last_notified_at.py new file mode 100644 index 000000000..091c4ee84 --- /dev/null +++ b/src/backend/apps/cms/migrations/0026_advisory_last_notified_at_bulletin_last_notified_at.py @@ -0,0 +1,23 @@ +# Generated by Django 5.2.14 on 2026-06-10 17:14 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('cms', '0025_advisoryindexpage_bulletinindexpage_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='advisory', + name='last_notified_at', + field=models.DateTimeField(blank=True, null=True), + ), + migrations.AddField( + model_name='bulletin', + name='last_notified_at', + field=models.DateTimeField(blank=True, null=True), + ), + ] diff --git a/src/backend/apps/cms/models.py b/src/backend/apps/cms/models.py index d32370d63..b6dcb9681 100644 --- a/src/backend/apps/cms/models.py +++ b/src/backend/apps/cms/models.py @@ -196,6 +196,8 @@ def rendered_body(self): # Geo fields geometry = models.MultiPolygonField() + last_notified_at = models.DateTimeField(null=True, blank=True) + # Editor panels configuration content_panels = [ FieldPanel("title", help_text=HelpText.GENERIC_TITLE), @@ -209,7 +211,7 @@ def rendered_body(self): FieldPanel("body", help_text=HelpText.GENERIC_BODY), FieldPanel('created_at', read_only=True, heading="Created"), FieldPanel('first_published_at', read_only=True, heading="Published"), - FieldPanel('last_published_at', read_only=True, heading="Updated"), + FieldPanel('last_notified_at', read_only=True, heading="Updated"), ] promote_panels = [] @@ -252,6 +254,7 @@ class Bulletin(Page, BaseModel): body = StreamField(RichContent()) image = models.ForeignKey(Image, on_delete=models.SET_NULL, null=True, blank=False) image_alt_text = models.CharField(max_length=125, default='', blank=False) + last_notified_at = models.DateTimeField(null=True, blank=True) def rendered_body(self): blocks = [wagtailcore_tags.richtext(block.render()) for block in self.body] @@ -280,7 +283,7 @@ def save(self, *args, **kwargs): FieldPanel("body", help_text=HelpText.GENERIC_BODY), FieldPanel('created_at', read_only=True, heading="Created"), FieldPanel('first_published_at', read_only=True, heading="Published"), - FieldPanel('last_published_at', read_only=True, heading="Updated"), + FieldPanel('last_notified_at', read_only=True, heading="Updated"), ] promote_panels = [] diff --git a/src/backend/apps/cms/views.py b/src/backend/apps/cms/views.py index 7b8c1b80c..4a9b7e793 100644 --- a/src/backend/apps/cms/views.py +++ b/src/backend/apps/cms/views.py @@ -15,6 +15,9 @@ from django.template.loader import render_to_string from django.views.decorators.csrf import csrf_exempt from rest_framework import viewsets +from django.contrib import messages +from django.shortcuts import redirect +from wagtail.models import Page class CMSViewSet(viewsets.ReadOnlyModelViewSet): @@ -146,3 +149,30 @@ def access_denied_idir(request): return render(request, 'wagtailadmin/access_denied.html', context={ "is_non_idir_login": True, }) + + +def publish_minor_update(request, page_id): + if request.method != "POST": + return redirect(f"/drivebc-cms/pages/{page_id}/edit/") + + page = Page.objects.get(pk=page_id).specific + latest_revision = page.get_latest_revision() + + if latest_revision: + # Snapshot the current last_notified_at before publishing + last_notified_at = page.last_notified_at + + # Publish the revision (updates content but triggers after_publish_page) + latest_revision.publish(changed=False) + + # Restore last_notified_at and also revert last_published_at + # so the "Updated" panel field doesn't change + Page.objects.filter(pk=page.pk).update( + last_published_at=last_notified_at + ) + type(page).objects.filter(pk=page.pk).update( + last_notified_at=last_notified_at + ) + + messages.success(request, f'"{page.title}" published as a minor update.') + return redirect(f"/drivebc-cms/pages/{page_id}/edit/") diff --git a/src/backend/apps/cms/wagtail_hooks.py b/src/backend/apps/cms/wagtail_hooks.py index 95cb5383d..abffe6203 100644 --- a/src/backend/apps/cms/wagtail_hooks.py +++ b/src/backend/apps/cms/wagtail_hooks.py @@ -13,7 +13,7 @@ from wagtail_modeladmin.options import ModelAdmin, modeladmin_register from .models import Advisory, Bulletin, SubPage, get_or_create_advisory_index, get_or_create_bulletin_index -from .views import access_requested +from .views import access_requested, publish_minor_update # from wagtail.admin.ui.components import ActionMenuItem from wagtail.admin.action_menu import ActionMenuItem @@ -28,14 +28,21 @@ @hooks.register("after_publish_page") def post_edit_hook(request, page): - # Only process published advisory pages + from django.utils import timezone + if page.specific_class == Advisory: try: + Advisory.objects.filter(pk=page.pk).update(last_notified_at=timezone.now()) send_advisory_notifications(page.id) - except Exception: logger.error(request, 'There was a problem sending an advisory notification') + elif page.specific_class == Bulletin: + try: + Bulletin.objects.filter(pk=page.pk).update(last_notified_at=timezone.now()) + except Exception: + logger.error(request, 'There was a problem updating bulletin notification timestamp') + @hooks.register("insert_global_admin_css") def insert_global_admin_css(): @@ -207,6 +214,43 @@ def render_html(self, parent_context): preview_url, ) +class PublishMinorUpdateMenuItem(ActionMenuItem): + label = "Publish minor update" + name = "publish-minor-update" + icon_name = "upload" + order = 15 + + def render_html(self, parent_context): + context = parent_context.get("context", parent_context) + page = context.get("page") + + if not page: + return "" + + url = reverse("cms-publish-minor-update", args=[page.pk]) + + return format_html( + """ +
  • + +
  • + """, + url, + ) + @hooks.register("register_page_action_menu_item") def register_copy_preview_button(): @@ -281,3 +325,21 @@ def move_new_advisory_to_top(request, page): if page.specific_class == Advisory: parent = page.get_parent() page.move(parent, pos="first-child") + +@hooks.register("register_page_action_menu_item") +def register_publish_minor_update(): + return PublishMinorUpdateMenuItem(order=25) + +@hooks.register("construct_page_action_menu") +def customize_page_action_menu(menu_items, request, context): + for item in menu_items: + if item.name == "action-publish": + item.label = "Publish with notifications" + + +@hooks.register('register_admin_urls') +def add_access_requested_url(): + return [ + path('access-requested', access_requested, name='cms-access-requested'), + path('publish-minor-update//', publish_minor_update, name='cms-publish-minor-update'), + ] \ No newline at end of file diff --git a/src/frontend/src/Components/advisories/AdvisoriesList.js b/src/frontend/src/Components/advisories/AdvisoriesList.js index fabae0f43..f0d86f93d 100644 --- a/src/frontend/src/Components/advisories/AdvisoriesList.js +++ b/src/frontend/src/Components/advisories/AdvisoriesList.js @@ -77,8 +77,8 @@ export default function AdvisoriesList(props) { :
    - {advisory.first_published_at != advisory.last_published_at ? "Updated" : "Published" } - + {advisory.last_notified_at != advisory.first_published_at ? "Updated" : "Published" } + {trackedAdvisories[advisory.id]?.highlight &&
    Updated
    } @@ -110,8 +110,8 @@ export default function AdvisoriesList(props) { :
    - {advisory.first_published_at != advisory.last_published_at ? "Updated" : "Published"} - + {advisory.last_notified_at != advisory.first_published_at ? "Updated" : "Published"} + {trackedAdvisories[advisory.id]?.highlight &&
    Updated
    } @@ -140,7 +140,8 @@ export default function AdvisoriesList(props) { } else { return ( -
    handleClick(advisory)} onKeyDown={(keyEvent) => handleClick(advisory, keyEvent)} @@ -157,8 +158,8 @@ export default function AdvisoriesList(props) { {(showTimestamp && showPublished) &&
    {!cmsContext.readAdvisories.includes(advisory.id.toString() + '-' + advisory.live_revision.toString()) &&
    } - {advisory.first_published_at != advisory.last_published_at ? "Updated" : "Published" } - + {advisory.last_notified_at != advisory.first_published_at ? "Updated" : "Published" } + {trackedAdvisories[advisory.id]?.highlight &&
    Updated
    } @@ -186,7 +187,7 @@ export default function AdvisoriesList(props) { {showTimestamp &&
    {advisory.first_published_at != advisory.last_published_at ? "Updated" : "Published" } - + {trackedAdvisories[advisory.id]?.highlight &&
    Updated
    } diff --git a/src/frontend/src/Components/bulletins/BulletinsList.js b/src/frontend/src/Components/bulletins/BulletinsList.js index 460584564..ec81344d9 100644 --- a/src/frontend/src/Components/bulletins/BulletinsList.js +++ b/src/frontend/src/Components/bulletins/BulletinsList.js @@ -68,9 +68,9 @@ export default function BulletinsList(props) {

    :
    - {bulletin.first_published_at != bulletin.last_published_at ? 'Updated' : 'Published'} + {bulletin.first_published_at != bulletin.last_notified_at ? 'Updated' : 'Published'} - + {trackedBulletins[bulletin.id]?.highlight &&
    Updated
    } diff --git a/src/frontend/src/Components/data/advisories.js b/src/frontend/src/Components/data/advisories.js index 1954addf1..526c725f3 100644 --- a/src/frontend/src/Components/data/advisories.js +++ b/src/frontend/src/Components/data/advisories.js @@ -24,14 +24,11 @@ export const getAdvisoryCounts = (advisories) => { } } -export const markAdvisoriesAsRead = (advisoriesData, cmsContext, setCMSContext) => { - if (advisoriesData && advisoriesData.length > 0 && !advisoriesData[0].live_revision) return; - const advisoriesIds = advisoriesData.map(advisory => advisory.id.toString() + '-' + advisory.live_revision.toString()); +export const markAdvisoriesAsRead = (advisories, cmsContext, setCMSContext) => { + const newRead = advisories + .filter(a => a.id && a.last_notified_at) + .map(a => a.id.toString() + '-' + a.last_notified_at.toString()); - // Combine and remove duplicates - const readAdvisories = Array.from(new Set([...advisoriesIds, ...cmsContext.readAdvisories])); - const updatedContext = {...cmsContext, readAdvisories: readAdvisories}; - - setCMSContext(updatedContext); - localStorage.setItem('cmsContext', JSON.stringify(updatedContext)); + const merged = [...new Set([...cmsContext.readAdvisories, ...newRead])]; + setCMSContext(prev => ({ ...prev, readAdvisories: merged })); } diff --git a/src/frontend/src/Components/data/bulletins.js b/src/frontend/src/Components/data/bulletins.js index 51129f734..bebd2fe2a 100644 --- a/src/frontend/src/Components/data/bulletins.js +++ b/src/frontend/src/Components/data/bulletins.js @@ -17,14 +17,11 @@ export function getBulletinsPreview(id) { }); } -export function markBulletinsAsRead(bulletinsData, cmsContext, setCMSContext) { - if (bulletinsData && bulletinsData.length > 0 && !bulletinsData[0].live_revision) return; - const bulletinsIds = bulletinsData.map(bulletin => bulletin.id.toString() + '-' + bulletin.live_revision.toString()); +export const markBulletinsAsRead = (bulletins, cmsContext, setCMSContext) => { + const newRead = bulletins + .filter(b => b.id && b.last_notified_at) + .map(b => b.id.toString() + '-' + b.last_notified_at.toString()); - // Combine and remove duplicates - const readBulletins = Array.from(new Set([...bulletinsIds, ...cmsContext.readBulletins])); - const updatedContext = {...cmsContext, readBulletins: readBulletins}; - - setCMSContext(updatedContext); - localStorage.setItem('cmsContext', JSON.stringify(updatedContext)); + const merged = [...new Set([...cmsContext.readBulletins, ...newRead])]; + setCMSContext(prev => ({ ...prev, readBulletins: merged })); } diff --git a/src/frontend/src/Components/shared/header/Header.js b/src/frontend/src/Components/shared/header/Header.js index 3d48cd6a0..4b46c95ee 100644 --- a/src/frontend/src/Components/shared/header/Header.js +++ b/src/frontend/src/Components/shared/header/Header.js @@ -96,29 +96,30 @@ export default function Header({ isMaintenance }) { /* Helpers */ const getUnreadAdvisoriesCount = (advisoriesData) => { - if (!advisoriesData) return 0; - if (advisoriesData.length !== 0 && advisoriesData[0].live_revision == null) return 0; - - const readAdvisories = advisoriesData.filter(advisory => { - if (!advisory.id || !advisory.live_revision) return true; - return cmsContextRef.current.readAdvisories.includes( - advisory.id.toString() + '-' + advisory.live_revision.toString() - ); - }); - return advisoriesData.length - readAdvisories.length; - } + if (!advisoriesData) return 0; + if (advisoriesData.length !== 0 && advisoriesData[0].last_notified_at == null) return 0; + + const readAdvisories = advisoriesData.filter(advisory => { + if (!advisory.id || !advisory.last_notified_at) return true; + return cmsContextRef.current.readAdvisories.includes( + advisory.id.toString() + '-' + advisory.last_notified_at.toString() + ); + }); + return advisoriesData.length - readAdvisories.length; +} - const getUnreadBulletinsCount = (bulletinsData) => { - if (!bulletinsData) return 0; - if (bulletinsData.length !== 0 && bulletinsData[0].live_revision == null) return 0; +const getUnreadBulletinsCount = (bulletinsData) => { + if (!bulletinsData) return 0; + if (bulletinsData.length !== 0 && bulletinsData[0].last_notified_at == null) return 0; - const readBulletins = bulletinsData.filter(bulletin => - cmsContextRef.current.readBulletins.includes( - bulletin.id.toString() + '-' + bulletin.live_revision.toString() - ) + const readBulletins = bulletinsData.filter(bulletin => { + if (!bulletin.id || !bulletin.last_notified_at) return true; + return cmsContextRef.current.readBulletins.includes( + bulletin.id.toString() + '-' + bulletin.last_notified_at.toString() ); - return bulletinsData.length - readBulletins.length; - } + }); + return bulletinsData.length - readBulletins.length; +} /* Data loading */ const loadAdvisories = async () => { diff --git a/src/frontend/src/pages/AdvisoriesListPage.js b/src/frontend/src/pages/AdvisoriesListPage.js index 6b429871a..289bf547f 100644 --- a/src/frontend/src/pages/AdvisoriesListPage.js +++ b/src/frontend/src/pages/AdvisoriesListPage.js @@ -94,10 +94,12 @@ export default function AdvisoriesListPage() { const tracked = trackedAdvisoriesRef.current[advisory.id] ?? null; const isDismissed = dismissedHighlightsRef.current.has(advisory.id); const wasHighlighted = highlightedBeforeRefreshRef.current.has(String(advisory.id)); - const revisionChanged = tracked && advisory.live_revision !== tracked.live_revision; - // New revision means it's genuinely updated again — clear dismissal - if (revisionChanged) { + // Use last_notified_at instead of live_revision to detect major updates only + const notificationChanged = tracked && advisory.last_notified_at !== tracked.last_notified_at; + + // Clear dismissal only on major update (notification changed) + if (notificationChanged) { dismissedHighlightsRef.current.delete(String(advisory.id)); sessionStorage.setItem('dismissedHighlights', JSON.stringify([...dismissedHighlightsRef.current])); } @@ -105,10 +107,11 @@ export default function AdvisoriesListPage() { acc[advisory.id] = { highlight: isDismissed ? false - : tracked - ? revisionChanged || tracked.highlight - : wasHighlighted || !isInitialLoad.current, // restore from sessionStorage on refresh + : tracked !== null + ? notificationChanged || tracked.highlight // existing advisory: only highlight on major update + : wasHighlighted, // new advisory: only restore from sessionStorage, never auto-highlight live_revision: advisory.live_revision, + last_notified_at: advisory.last_notified_at, }; return acc; }, {}); diff --git a/src/frontend/src/pages/BulletinsListPage.js b/src/frontend/src/pages/BulletinsListPage.js index 45e412aa9..57da7e165 100644 --- a/src/frontend/src/pages/BulletinsListPage.js +++ b/src/frontend/src/pages/BulletinsListPage.js @@ -84,9 +84,11 @@ export default function BulletinsListPage() { const tracked = trackedBulletinsRef.current[bulletin.id] ?? null; const isDismissed = dismissedHighlightsRef.current.has(String(bulletin.id)); const wasHighlighted = highlightedBeforeRefreshRef.current.has(String(bulletin.id)); - const revisionChanged = tracked && bulletin.live_revision !== tracked.live_revision; + // Use last_notified_at instead of live_revision to detect major updates only + const notificationChanged = tracked !== null && bulletin.last_notified_at !== tracked.last_notified_at; - if (revisionChanged) { + // Clear dismissal only on major update (notification changed) + if (notificationChanged) { dismissedHighlightsRef.current.delete(String(bulletin.id)); sessionStorage.setItem('dismissedBulletinHighlights', JSON.stringify([...dismissedHighlightsRef.current])); } @@ -94,10 +96,11 @@ export default function BulletinsListPage() { acc[bulletin.id] = { highlight: isDismissed ? false - : tracked - ? revisionChanged || tracked.highlight - : wasHighlighted || !isInitialLoad.current, + : tracked !== null + ? notificationChanged || tracked.highlight // existing bulletin: only highlight on major update + : wasHighlighted, // new bulletin: only restore from sessionStorage, never auto-highlight live_revision: bulletin.live_revision, + last_notified_at: bulletin.last_notified_at, }; return acc; }, {}); From 02ff77455bd69528ba83a361a394694ae3628353 Mon Sep 17 00:00:00 2001 From: bcgov-brwang <87880048+bcgov-brwang@users.noreply.github.com> Date: Wed, 10 Jun 2026 15:41:57 -0700 Subject: [PATCH 4/4] DBC22-6585: fix bugs 1 --- src/backend/apps/cms/wagtail_hooks.py | 18 ++- .../src/Components/data/advisories.js | 21 +++- src/frontend/src/Components/data/bulletins.js | 12 +- .../src/Components/shared/header/Header.js | 31 +++-- src/frontend/src/pages/AdvisoriesListPage.js | 109 ++++++++++++------ src/frontend/src/pages/BulletinsListPage.js | 100 ++++++++++++++-- 6 files changed, 225 insertions(+), 66 deletions(-) diff --git a/src/backend/apps/cms/wagtail_hooks.py b/src/backend/apps/cms/wagtail_hooks.py index abffe6203..9f539a6e6 100644 --- a/src/backend/apps/cms/wagtail_hooks.py +++ b/src/backend/apps/cms/wagtail_hooks.py @@ -229,6 +229,13 @@ def render_html(self, parent_context): url = reverse("cms-publish-minor-update", args=[page.pk]) + if isinstance(page.specific, Advisory): + redirect_url = "/drivebc-cms/cms/advisory/" + elif isinstance(page.specific, Bulletin): + redirect_url = "/drivebc-cms/cms/bulletin/" + else: + redirect_url = "/drivebc-cms/" + return format_html( """
  • @@ -236,12 +243,10 @@ def render_html(self, parent_context): type="button" class="action button" style="width: 100%; text-align: left;" - onclick="if(confirm('Publish as minor update? No notifications will be sent and the Updated timestamp will not change.')) {{ - fetch('{}', {{ - method: 'POST', - headers: {{'X-CSRFToken': document.cookie.match(/csrftoken=([^;]+)/)[1]}}, - }}).then(() => window.location.reload()); - }}" + onclick="fetch('{}', {{ + method: 'POST', + headers: {{'X-CSRFToken': document.cookie.match(/csrftoken=([^;]+)/)[1]}}, + }}).then(() => window.location.href = '{}'); return false;" > Publish minor update @@ -249,6 +254,7 @@ def render_html(self, parent_context):
  • """, url, + redirect_url, ) diff --git a/src/frontend/src/Components/data/advisories.js b/src/frontend/src/Components/data/advisories.js index 526c725f3..230c4b10e 100644 --- a/src/frontend/src/Components/data/advisories.js +++ b/src/frontend/src/Components/data/advisories.js @@ -24,11 +24,28 @@ export const getAdvisoryCounts = (advisories) => { } } +// export const markAdvisoriesAsRead = (advisories, cmsContext, setCMSContext) => { +// const newRead = advisories +// .filter(a => a.id && a.last_notified_at) +// .map(a => a.id.toString() + '-' + a.last_notified_at.toString()); + +// const merged = [...new Set([...cmsContext.readAdvisories, ...newRead])]; +// setCMSContext(prev => ({ ...prev, readAdvisories: merged })); +// } + export const markAdvisoriesAsRead = (advisories, cmsContext, setCMSContext) => { + if (!advisories) return; + const newRead = advisories .filter(a => a.id && a.last_notified_at) .map(a => a.id.toString() + '-' + a.last_notified_at.toString()); + if (newRead.length === 0) return; + const merged = [...new Set([...cmsContext.readAdvisories, ...newRead])]; - setCMSContext(prev => ({ ...prev, readAdvisories: merged })); -} + + // Only update if something actually changed + if (merged.length !== cmsContext.readAdvisories.length) { + setCMSContext(prev => ({ ...prev, readAdvisories: merged })); + } +} \ No newline at end of file diff --git a/src/frontend/src/Components/data/bulletins.js b/src/frontend/src/Components/data/bulletins.js index bebd2fe2a..0c025c1b3 100644 --- a/src/frontend/src/Components/data/bulletins.js +++ b/src/frontend/src/Components/data/bulletins.js @@ -18,10 +18,18 @@ export function getBulletinsPreview(id) { } export const markBulletinsAsRead = (bulletins, cmsContext, setCMSContext) => { + if (!bulletins) return; + const newRead = bulletins .filter(b => b.id && b.last_notified_at) .map(b => b.id.toString() + '-' + b.last_notified_at.toString()); + if (newRead.length === 0) return; + const merged = [...new Set([...cmsContext.readBulletins, ...newRead])]; - setCMSContext(prev => ({ ...prev, readBulletins: merged })); -} + + // Only update if something actually changed + if (merged.length !== cmsContext.readBulletins.length) { + setCMSContext(prev => ({ ...prev, readBulletins: merged })); + } +} \ No newline at end of file diff --git a/src/frontend/src/Components/shared/header/Header.js b/src/frontend/src/Components/shared/header/Header.js index 4b46c95ee..f51c2e54b 100644 --- a/src/frontend/src/Components/shared/header/Header.js +++ b/src/frontend/src/Components/shared/header/Header.js @@ -95,37 +95,38 @@ export default function Header({ isMaintenance }) { const loadRef = useRef(null); /* Helpers */ - const getUnreadAdvisoriesCount = (advisoriesData) => { + const getUnreadAdvisoriesCount = (advisoriesData, context) => { if (!advisoriesData) return 0; if (advisoriesData.length !== 0 && advisoriesData[0].last_notified_at == null) return 0; const readAdvisories = advisoriesData.filter(advisory => { if (!advisory.id || !advisory.last_notified_at) return true; - return cmsContextRef.current.readAdvisories.includes( + return context.readAdvisories.includes( advisory.id.toString() + '-' + advisory.last_notified_at.toString() ); }); return advisoriesData.length - readAdvisories.length; } -const getUnreadBulletinsCount = (bulletinsData) => { +const getUnreadBulletinsCount = (bulletinsData, context) => { if (!bulletinsData) return 0; if (bulletinsData.length !== 0 && bulletinsData[0].last_notified_at == null) return 0; const readBulletins = bulletinsData.filter(bulletin => { if (!bulletin.id || !bulletin.last_notified_at) return true; - return cmsContextRef.current.readBulletins.includes( + return context.readBulletins.includes( bulletin.id.toString() + '-' + bulletin.last_notified_at.toString() ); }); return bulletinsData.length - readBulletins.length; } + + /* Data loading */ const loadAdvisories = async () => { - // On initial load, use cached data if available - if (advisories && advisories.length > 0 && advisories[0].live_revision != null && isInitialLoad.current) { - setAdvisoriesCount(getUnreadAdvisoriesCount(filteredAdvisories)); + if (advisories && advisories.length > 0 && advisories[0].last_notified_at != null && isInitialLoad.current) { + setAdvisoriesCount(getUnreadAdvisoriesCount(filteredAdvisories, cmsContextRef.current)); isInitialLoad.current = false; return; } @@ -148,7 +149,7 @@ const getUnreadBulletinsCount = (bulletinsData) => { timeStamp: new Date().getTime() })); - setAdvisoriesCount(getUnreadAdvisoriesCount(filteredAdvisoriesData)); + setAdvisoriesCount(getUnreadAdvisoriesCount(filteredAdvisoriesData, cmsContextRef.current)); isInitialLoad.current = false; } @@ -166,17 +167,25 @@ const getUnreadBulletinsCount = (bulletinsData) => { timeStamp: new Date().getTime() })); - setBulletinsCount(getUnreadBulletinsCount(bulletinsData)); + // setBulletinsCount(getUnreadBulletinsCount(bulletinsData)); + setBulletinsCount(getUnreadBulletinsCount(bulletinsData, cmsContextRef.current)); // <-- + } // Always point loadRef to the latest versions of load functions loadRef.current = { loadAdvisories, loadBulletins }; /* Effects */ - - // Keep cmsContextRef in sync with latest cmsContext useEffect(() => { cmsContextRef.current = cmsContext; + + // Recompute counts immediately when read state changes + if (filteredAdvisories) { + setAdvisoriesCount(getUnreadAdvisoriesCount(filteredAdvisories, cmsContext)); + } + if (bulletins) { + setBulletinsCount(getUnreadBulletinsCount(bulletins, cmsContext)); + } }, [cmsContext]); // Load on mount + pathname change + poll every 30s diff --git a/src/frontend/src/pages/AdvisoriesListPage.js b/src/frontend/src/pages/AdvisoriesListPage.js index 289bf547f..f84e68431 100644 --- a/src/frontend/src/pages/AdvisoriesListPage.js +++ b/src/frontend/src/pages/AdvisoriesListPage.js @@ -1,6 +1,6 @@ // React import React, { useCallback, useContext, useEffect, useRef, useState } from 'react'; -import { useSearchParams } from "react-router-dom"; +import { useSearchParams, useLocation } from "react-router-dom"; // Redux import { useSelector, useDispatch } from 'react-redux' @@ -54,27 +54,48 @@ export default function AdvisoriesListPage() { const [updateCounts, setUpdateCounts] = useState({ above: 0, below: 0 }); const [showNetworkError, setShowNetworkError] = useState(false); const [showServerError, setShowServerError] = useState(false); - const trackedAdvisoriesRef = useRef({}); const dismissedHighlightsRef = useRef( new Set(JSON.parse(sessionStorage.getItem('dismissedHighlights') || '[]')) ); - + const location = useLocation(); + const isFirstMount = useRef(true); const dismissHighlight = (advisoryId) => { dismissedHighlightsRef.current.add(String(advisoryId)); sessionStorage.setItem( 'dismissedHighlights', JSON.stringify([...dismissedHighlightsRef.current]) ); - // Also remove from highlighted set highlightedBeforeRefreshRef.current.delete(String(advisoryId)); - sessionStorage.setItem('highlightedAdvisories', JSON.stringify([...highlightedBeforeRefreshRef.current])); + sessionStorage.setItem( + 'highlightedAdvisories', + JSON.stringify([...highlightedBeforeRefreshRef.current]) + ); + + const advisory = advisoriesRef.current?.find( + a => String(a.id) === String(advisoryId) + ); + if (advisory) { + markAdvisoriesAsRead([advisory], cmsContextRef.current, setCMSContext); + } + + trackedAdvisoriesRef.current = { + ...trackedAdvisoriesRef.current, + [advisoryId]: { + ...trackedAdvisoriesRef.current[advisoryId], + highlight: false, + } }; + setTrackedAdvisories({ ...trackedAdvisoriesRef.current }); + }; const highlightedBeforeRefreshRef = useRef( new Set(JSON.parse(sessionStorage.getItem('highlightedAdvisories') || '[]')) ); + const cmsContextRef = useRef(cmsContext); + const advisoriesRef = useRef(advisories); + // Error handling const displayError = (error) => { if (error instanceof ServerError) { @@ -84,72 +105,84 @@ export default function AdvisoriesListPage() { } } - // Data loading - const loadAdvisories = async () => { - const advisoriesData = await getAdvisories().catch((error) => displayError(error)); - if (!advisoriesData) return; +const loadAdvisories = async () => { + const advisoriesData = await getAdvisories().catch((error) => displayError(error)); + if (!advisoriesData) return; - // Use ref instead of state to avoid stale closure - const trackedDict = advisoriesData.reduce((acc, advisory) => { + // Use the latest context directly from the ref + const currentContext = cmsContextRef.current; + + const trackedDict = advisoriesData.reduce((acc, advisory) => { const tracked = trackedAdvisoriesRef.current[advisory.id] ?? null; - const isDismissed = dismissedHighlightsRef.current.has(advisory.id); + const isDismissed = dismissedHighlightsRef.current.has(String(advisory.id)); const wasHighlighted = highlightedBeforeRefreshRef.current.has(String(advisory.id)); + const notificationChanged = tracked !== null && advisory.last_notified_at !== tracked.last_notified_at; - // Use last_notified_at instead of live_revision to detect major updates only - const notificationChanged = tracked && advisory.last_notified_at !== tracked.last_notified_at; - - // Clear dismissal only on major update (notification changed) if (notificationChanged) { dismissedHighlightsRef.current.delete(String(advisory.id)); sessionStorage.setItem('dismissedHighlights', JSON.stringify([...dismissedHighlightsRef.current])); } + const isUnread = advisory.last_notified_at && + !isDismissed && + !currentContext.readAdvisories.includes( + advisory.id.toString() + '-' + advisory.last_notified_at.toString() + ); + acc[advisory.id] = { highlight: isDismissed ? false : tracked !== null - ? notificationChanged || tracked.highlight // existing advisory: only highlight on major update - : wasHighlighted, // new advisory: only restore from sessionStorage, never auto-highlight + ? notificationChanged || tracked.highlight + : wasHighlighted || Boolean(isUnread), live_revision: advisory.live_revision, last_notified_at: advisory.last_notified_at, }; return acc; }, {}); - // Merge into ref instead of overwriting — preserves dismissed highlights trackedAdvisoriesRef.current = { ...trackedAdvisoriesRef.current, ...trackedDict, }; setTrackedAdvisories({ ...trackedAdvisoriesRef.current }); - // Persist highlighted IDs to sessionStorage const highlightedIds = Object.entries(trackedDict) .filter(([, v]) => v.highlight) .map(([id]) => id); sessionStorage.setItem('highlightedAdvisories', JSON.stringify(highlightedIds)); highlightedBeforeRefreshRef.current = new Set(highlightedIds); - const filteredAdvisoriesData = selectedRoute - ? filterAdvisoryByRoute(advisoriesData, selectedRoute) - : advisoriesData; + const filteredAdvisoriesData = selectedRoute + ? filterAdvisoryByRoute(advisoriesData, selectedRoute) + : advisoriesData; - dispatch(updateAdvisories({ - list: advisoriesData, - filteredList: filteredAdvisoriesData, - timeStamp: new Date().getTime() - })); + dispatch(updateAdvisories({ + list: advisoriesData, + filteredList: filteredAdvisoriesData, + timeStamp: new Date().getTime() + })); - markAdvisoriesAsRead(filteredAdvisoriesData, cmsContext, setCMSContext); + isInitialLoad.current = false; + isInitialMount.current = false; + setShowLoader(false); +} - isInitialLoad.current = false; - isInitialMount.current = false; - setShowLoader(false); - } useEffect(() => { + // Sync ref BEFORE loading so loadAdvisories sees current readAdvisories + cmsContextRef.current = cmsContext; + + if (isFirstMount.current) { + isFirstMount.current = false; loadAdvisories(); - }, []); + return; + } + // User navigated back — reset so highlights are recalculated fresh + trackedAdvisoriesRef.current = {}; + viewedHighlightedAdvisories.current = new Set(); + loadAdvisories(); +}, [location.key, cmsContext]); // Intersection observer — scroll position + highlight clearing (mirrors Delays pattern) useEffect(() => { @@ -196,6 +229,14 @@ export default function AdvisoriesListPage() { return () => observer.disconnect(); }, [advisories, trackedAdvisories]); + useEffect(() => { + cmsContextRef.current = cmsContext; +}, [cmsContext]); + +useEffect(() => { + advisoriesRef.current = advisories; +}, [advisories]); + // Handlers const updateHighlightHandler = (updated) => { setTrackedAdvisories((prev) => ({ diff --git a/src/frontend/src/pages/BulletinsListPage.js b/src/frontend/src/pages/BulletinsListPage.js index 57da7e165..7e4d968f2 100644 --- a/src/frontend/src/pages/BulletinsListPage.js +++ b/src/frontend/src/pages/BulletinsListPage.js @@ -22,6 +22,7 @@ import EmptyBulletin from '../Components/bulletins/EmptyBulletin'; import Footer from '../Footer'; import PageHeader from '../PageHeader'; import PollingComponent from "../Components/shared/PollingComponent"; +import { useLocation } from "react-router-dom"; // Styling import './BulletinsListPage.scss'; @@ -57,6 +58,11 @@ export default function BulletinsListPage() { const [updateCounts, setUpdateCounts] = useState({ above: 0, below: 0 }); const [showNetworkError, setShowNetworkError] = useState(false); const [showServerError, setShowServerError] = useState(false); + const cmsContextRef = useRef(cmsContext); + const bulletinsRef = useRef(bulletins); + + const location = useLocation(); + const isFirstMount = useRef(true); // Error handling const displayError = (error) => { @@ -67,52 +73,111 @@ export default function BulletinsListPage() { } } +// const dismissHighlight = (bulletinId) => { +// dismissedHighlightsRef.current.add(String(bulletinId)); +// sessionStorage.setItem( +// 'dismissedHighlights', +// JSON.stringify([...dismissedHighlightsRef.current]) +// ); +// highlightedBeforeRefreshRef.current.delete(String(bulletinId)); +// sessionStorage.setItem( +// 'highlightedBulletins', +// JSON.stringify([...highlightedBeforeRefreshRef.current]) +// ); + +// // Use refs to avoid stale closure +// const bulletin = bulletinsRef.current?.find( +// b => String(b.id) === String(bulletinId) +// ); +// if (bulletin) { +// markBulletinsAsRead([bulletin], cmsContextRef.current, setCMSContext); +// } + +// // Also clear from trackedBulletins state directly +// trackedBulletinsRef.current = { +// ...trackedBulletinsRef.current, +// [bulletinId]: { +// ...trackedBulletinsRef.current[bulletinId], +// highlight: false, +// } +// }; +// setTrackedBulletins({ ...trackedBulletinsRef.current }); +// }; + const dismissHighlight = (bulletinId) => { dismissedHighlightsRef.current.add(String(bulletinId)); - sessionStorage.setItem('dismissedBulletinHighlights', JSON.stringify([...dismissedHighlightsRef.current])); - + sessionStorage.setItem( + 'dismissedHighlights', + JSON.stringify([...dismissedHighlightsRef.current]) + ); highlightedBeforeRefreshRef.current.delete(String(bulletinId)); - sessionStorage.setItem('highlightedBulletins', JSON.stringify([...highlightedBeforeRefreshRef.current])); + sessionStorage.setItem( + 'highlightedBulletins', + JSON.stringify([...highlightedBeforeRefreshRef.current]) + ); + + const bulletin = bulletinsRef.current?.find( + b => String(b.id) === String(bulletinId) + ); + if (bulletin) { + markBulletinsAsRead([bulletin], cmsContextRef.current, setCMSContext); + } + + trackedBulletinsRef.current = { + ...trackedBulletinsRef.current, + [bulletinId]: { + ...trackedBulletinsRef.current[bulletinId], + highlight: false, + } + }; + setTrackedBulletins({ ...trackedBulletinsRef.current }); }; + + + // Data loading const loadBulletins = async () => { const bulletinsData = await getBulletins().catch((error) => displayError(error)); if (!bulletinsData) return; + const currentContext = cmsContextRef.current; + const trackedDict = bulletinsData.reduce((acc, bulletin) => { const tracked = trackedBulletinsRef.current[bulletin.id] ?? null; const isDismissed = dismissedHighlightsRef.current.has(String(bulletin.id)); const wasHighlighted = highlightedBeforeRefreshRef.current.has(String(bulletin.id)); - // Use last_notified_at instead of live_revision to detect major updates only const notificationChanged = tracked !== null && bulletin.last_notified_at !== tracked.last_notified_at; - // Clear dismissal only on major update (notification changed) if (notificationChanged) { dismissedHighlightsRef.current.delete(String(bulletin.id)); sessionStorage.setItem('dismissedBulletinHighlights', JSON.stringify([...dismissedHighlightsRef.current])); } + const isUnread = bulletin.last_notified_at && + !isDismissed && + !currentContext.readBulletins.includes( + bulletin.id.toString() + '-' + bulletin.last_notified_at.toString() + ); + acc[bulletin.id] = { highlight: isDismissed ? false : tracked !== null - ? notificationChanged || tracked.highlight // existing bulletin: only highlight on major update - : wasHighlighted, // new bulletin: only restore from sessionStorage, never auto-highlight + ? notificationChanged || tracked.highlight + : wasHighlighted || Boolean(isUnread), live_revision: bulletin.live_revision, last_notified_at: bulletin.last_notified_at, }; return acc; }, {}); - // Persist highlighted IDs to sessionStorage const highlightedIds = Object.entries(trackedDict) .filter(([, v]) => v.highlight) .map(([id]) => id); sessionStorage.setItem('highlightedBulletins', JSON.stringify(highlightedIds)); highlightedBeforeRefreshRef.current = new Set(highlightedIds); - // Merge into ref instead of overwriting trackedBulletinsRef.current = { ...trackedBulletinsRef.current, ...trackedDict, @@ -124,15 +189,24 @@ export default function BulletinsListPage() { timeStamp: new Date().getTime() })); - markBulletinsAsRead(bulletinsData, cmsContext, setCMSContext); + // ❌ REMOVED: markBulletinsAsRead(bulletinsData, cmsContext, setCMSContext); isInitialLoad.current = false; setShowLoader(false); } useEffect(() => { + cmsContextRef.current = cmsContext; + + if (isFirstMount.current) { + isFirstMount.current = false; + loadBulletins(); + return; + } + trackedBulletinsRef.current = {}; + viewedHighlightedBulletins.current = new Set(); loadBulletins(); - }, []); + }, [location.key, cmsContext]); // Intersection observer useEffect(() => { @@ -174,6 +248,10 @@ export default function BulletinsListPage() { return () => observer.disconnect(); }, [bulletins, trackedBulletins]); +useEffect(() => { + bulletinsRef.current = bulletins; +}, [bulletins]); + const scrollToNextHighlightedHandler = (direction) => { const offset = 58 + 48; const sortedRefs = Object.entries(bulletinRefs.current)