mirror of
https://github.com/dgtlmoon/changedetection.io.git
synced 2026-08-23 14:46:59 +00:00
UI - Fix for Mark all viewed" doesn't update UI until page is manually refreshed #4021
This commit is contained in:
@@ -121,7 +121,11 @@ def construct_blueprint(datastore: ChangeDetectionStore, update_q, queuedWatchMe
|
||||
deals_count=deals_count,
|
||||
unread_count=unread_count,
|
||||
processor_counts=processor_counts,
|
||||
extra_classes=' '.join(filter(None, ['has-queue' if not update_q.empty() else '', 'llm-configured' if llm_configured else ''])),
|
||||
# body classes for app-wide state; realtime.js keeps these in sync live
|
||||
# (has-any-unviewed reveals the "Mark all viewed" button - see _watch_table.scss)
|
||||
extra_classes=' '.join(filter(None, ['has-queue' if not update_q.empty() else '',
|
||||
'llm-configured' if llm_configured else '',
|
||||
'has-any-unviewed' if datastore.unread_changes_count else ''])),
|
||||
form=form,
|
||||
generate_tag_colors=processors.generate_processor_badge_colors,
|
||||
wcag_text_color=processors.wcag_text_color,
|
||||
|
||||
@@ -268,9 +268,11 @@ window.watchOverviewI18n = {
|
||||
</div>
|
||||
{% endif %}
|
||||
<div id="buttons-for-all-watches">
|
||||
{%- if unread_count -%}
|
||||
{# Always rendered, hidden by CSS unless #watch-table-wrapper has .has-unread-changes.
|
||||
It used to be {% if unread_count %}-gated, which meant it could not exist in the DOM
|
||||
of another open tab — so marking viewed (or a new change arriving) in one tab could
|
||||
never show/hide it in the other. Presence in the DOM is what lets realtime toggle it. #}
|
||||
<a id="post-list-mark-views" class="cdio-btn" href="{{ filtered_action_url('ui.mark_all_viewed') }}"><i data-feather="eye"></i>{{ _('Mark all viewed') }}</a>
|
||||
{%- endif -%}
|
||||
<a id="recheck-all" class="cdio-btn" href="{{ filtered_action_url('ui.form_watch_checknow') }}"><i data-feather="refresh-cw"></i>{{ _('Recheck all') }} {% if active_tag_uuid %}{{ _("in '%(title)s'", title=active_tag.title) }}{% endif %}</a>
|
||||
</div>
|
||||
<a title="{{ _('RSS Feed') }}" href="{{ url_for('rss.feed', tag=active_tag_uuid, token=app_rss_token)}}"><img alt="RSS Feed" id="feed-icon" src="{{url_for('static_content', group='images', filename='generic_feed-icon.svg')}}" height="15"></a>
|
||||
|
||||
@@ -242,6 +242,11 @@ $(document).ready(function () {
|
||||
})
|
||||
|
||||
socket.on('general_stats_update', function (general_stats) {
|
||||
// Drives the "Mark all viewed" button, which is always in the DOM and revealed by
|
||||
// this class (see body.has-any-unviewed in _watch_table.scss). Emitted whenever a
|
||||
// worker finishes a watch, and once after bulk ops like mark-all-viewed - so every
|
||||
// open tab shows/hides the button without needing a reload.
|
||||
document.body.classList.toggle('has-any-unviewed', general_stats.unread_changes_count !== 0);
|
||||
$('#watch-table-wrapper').toggleClass("has-unread-changes", general_stats.unread_changes_count !==0)
|
||||
$('#watch-table-wrapper').toggleClass("has-error", general_stats.count_errors !== 0)
|
||||
// NB: the watch-list status .seg counts (unread/errors/deals) are
|
||||
|
||||
@@ -425,6 +425,20 @@ body.watch-selection-active {
|
||||
flex-wrap: wrap;
|
||||
gap: $common-gap;
|
||||
margin: 0;
|
||||
|
||||
/* Always in the DOM so realtime can reveal/hide it in any open tab (it used to be
|
||||
{% if unread_count %}'d away, so another tab could never show it). Revealed by
|
||||
body.has-any-unviewed - a body class rather than a selector tied to this toolbar,
|
||||
so it keeps working if the button moves, and it follows the same convention as
|
||||
body.has-queue / body.is-checking-now. Set server-side via extra_classes and kept
|
||||
live by the general_stats_update socket event. */
|
||||
#post-list-mark-views {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
body.has-any-unviewed #post-list-mark-views {
|
||||
display: inline-flex !important;
|
||||
}
|
||||
|
||||
#watch-table-wrapper {
|
||||
@@ -452,9 +466,11 @@ body.watch-selection-active {
|
||||
}
|
||||
}
|
||||
|
||||
/* #post-list-mark-views is no longer keyed off this class — see body.has-any-unviewed.
|
||||
Two classes tracking one fact is how they drift, so prefer the body class for anything new. */
|
||||
&.has-unread-changes {
|
||||
#list-related-buttons {
|
||||
#post-list-unread, #post-list-mark-views {
|
||||
#post-list-unread {
|
||||
display: inline-flex !important;
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,61 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
"""Re #4021 - the "Mark all viewed" button must be driven by live state, not by render-time presence.
|
||||
|
||||
The button used to be wrapped in {% if unread_count %}, so when nothing was unread it did not
|
||||
exist in the DOM at all. A second tab could therefore never show it after a change arrived, and
|
||||
never hide it after another tab marked everything viewed - no socket event can style an element
|
||||
that isn't there.
|
||||
|
||||
It is now always rendered and revealed by body.has-any-unviewed, which is set server-side via
|
||||
extra_classes and kept live by the general_stats_update socket event. These assertions pin both
|
||||
halves: the element is always present, and the body class tracks the unviewed state.
|
||||
"""
|
||||
|
||||
from flask import url_for
|
||||
|
||||
from .util import set_original_response, set_modified_response, wait_for_all_checks, delete_all_watches
|
||||
|
||||
|
||||
def _body_has_any_unviewed(res):
|
||||
"""The <body> class list is what CSS keys off - realtime.js toggles the same class."""
|
||||
for line in res.data.decode('utf-8').split('\n'):
|
||||
if '<body class="' in line:
|
||||
return 'has-any-unviewed' in line
|
||||
raise AssertionError("no <body class=...> found in response")
|
||||
|
||||
|
||||
def test_mark_all_viewed_button_is_always_in_the_dom(client, live_server, measure_memory_usage, datastore_path):
|
||||
set_original_response(datastore_path=datastore_path)
|
||||
|
||||
client.post(
|
||||
url_for("ui.ui_views.form_quick_watch_add"),
|
||||
data={"url": url_for('test_endpoint', _external=True), "tags": ''},
|
||||
follow_redirects=True
|
||||
)
|
||||
wait_for_all_checks(client)
|
||||
|
||||
# First snapshot only - nothing is unread yet
|
||||
res = client.get(url_for("watchlist.index"))
|
||||
assert b'id="post-list-mark-views"' in res.data, \
|
||||
"Button must be in the DOM even with nothing unread, or another tab can never reveal it"
|
||||
assert not _body_has_any_unviewed(res), "Nothing unread, so the body class must be absent"
|
||||
|
||||
# A real change lands -> unviewed
|
||||
set_modified_response(datastore_path=datastore_path)
|
||||
client.get(url_for("ui.form_watch_checknow"), follow_redirects=True)
|
||||
wait_for_all_checks(client)
|
||||
|
||||
res = client.get(url_for("watchlist.index"))
|
||||
assert b'id="post-list-mark-views"' in res.data
|
||||
assert _body_has_any_unviewed(res), "Unviewed change present, so the body class must be set"
|
||||
|
||||
# Mark all viewed runs synchronously (Re #4021), so the redirect must already reflect it
|
||||
res = client.get(url_for("ui.mark_all_viewed"), follow_redirects=True)
|
||||
assert not _body_has_any_unviewed(res), \
|
||||
"mark_all_viewed must be applied before the redirect renders - it used to run in a " \
|
||||
"background thread and the page rendered mid-marking"
|
||||
assert b'id="post-list-mark-views"' in res.data, "Still in the DOM, just hidden by CSS"
|
||||
assert b'class="has-unread-changes' not in res.data
|
||||
|
||||
delete_all_watches(client)
|
||||
Reference in New Issue
Block a user