- <%= turbo_stream_from @project, :samples, data: { "refresh-target": "source" } %>
- <%= viral_alert(message: "Page has updated", data: { "refresh-target": "notice" }, classes: "hidden", aria: {
- live: "assertive",
- }) do %>
- <%= link_to "refresh" %>
- <% end %>
-
+ <%= render RefreshNoticeComponent.new(streamable: @project, stream_name: :samples, message: t(".refresh_notice.message")) %>
<% if @has_samples %>
diff --git a/config/locales/en.yml b/config/locales/en.yml
index 11505b31a6..ebde27068c 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -320,6 +320,10 @@ en:
expiration: Expiration
username: Username
never: Never
+ components:
+ refresh_notice:
+ default_link_text: Refresh
+ default_message: New data available
concerns:
attachment_actions:
destroy:
@@ -1562,6 +1566,8 @@ en:
deselect_all_tooltip: Deselect all samples in this project
no_associated_samples: There are no samples associated with this project.
no_samples: No Samples
+ refresh_notice:
+ message: Samples table is out of date. Please refresh to view latest changes.
select_all_button: Select All
select_all_tooltip: Select all samples in this project
title: Samples
diff --git a/config/locales/fr.yml b/config/locales/fr.yml
index c88a355ec7..cc3a969b56 100644
--- a/config/locales/fr.yml
+++ b/config/locales/fr.yml
@@ -320,6 +320,10 @@ fr:
expiration: Expiration
username: Nom d’utilisateur
never: Jamais
+ components:
+ refresh_notice:
+ default_link_text: Actualiser
+ default_message: Nouvelles données disponibles
concerns:
attachment_actions:
destroy:
@@ -1566,6 +1570,8 @@ fr:
deselect_all_tooltip: Désélectionner tous les échantillons de ce projet
no_associated_samples: Il n’y a aucun échantillon associé à ce projet.
no_samples: Aucun échantillon
+ refresh_notice:
+ message: Le tableau des échantillons est obsolète. Veuillez actualiser pour voir les dernières modifications.
select_all_button: Tout sélectionner
select_all_tooltip: Sélectionner tous les échantillons de ce projet
title: Échantillons
diff --git a/test/components/refresh_notice_component_test.rb b/test/components/refresh_notice_component_test.rb
new file mode 100644
index 0000000000..c44884a20b
--- /dev/null
+++ b/test/components/refresh_notice_component_test.rb
@@ -0,0 +1,151 @@
+# frozen_string_literal: true
+
+require 'view_component_test_case'
+
+class RefreshNoticeComponentTest < ViewComponentTestCase
+ def setup
+ @project = namespaces_project_namespaces(:project1)
+ end
+
+ # 🔄 BASIC RENDERING - Ensure component renders with required elements
+
+ test 'renders with refresh controller and turbo stream' do
+ render_inline(RefreshNoticeComponent.new(streamable: @project, stream_name: :samples))
+
+ assert_selector '[data-controller="refresh"]', count: 1
+ assert_selector 'turbo-cable-stream-source', count: 1
+ assert_selector '[data-refresh-target="source"]', count: 1
+ end
+
+ test 'renders with viral alert component' do
+ render_inline(RefreshNoticeComponent.new(streamable: @project, stream_name: :samples))
+
+ assert_selector '[data-refresh-target="notice"]', count: 1
+ assert_selector '.hidden', count: 1
+ assert_selector '[aria-live="assertive"]', count: 1
+ end
+
+ test 'uses default message when not provided' do
+ render_inline(RefreshNoticeComponent.new(streamable: @project, stream_name: :samples))
+
+ assert_text I18n.t('components.refresh_notice.default_message')
+ end
+
+ test 'uses default link text when not provided' do
+ render_inline(RefreshNoticeComponent.new(streamable: @project, stream_name: :samples))
+
+ assert_text I18n.t('components.refresh_notice.default_link_text')
+ end
+
+ # 🎯 CUSTOM CONTENT - Test custom messages and links
+
+ test 'renders with custom message' do
+ custom_message = 'New data available'
+ render_inline(RefreshNoticeComponent.new(
+ streamable: @project,
+ stream_name: :samples,
+ message: custom_message
+ ))
+
+ assert_text custom_message
+ end
+
+ test 'renders with custom link text' do
+ custom_link_text = 'Load new data'
+ render_inline(RefreshNoticeComponent.new(
+ streamable: @project,
+ stream_name: :samples,
+ link_text: custom_link_text
+ ))
+
+ assert_text custom_link_text
+ end
+
+ test 'renders with both custom message and link text' do
+ custom_message = 'Updates are ready'
+ custom_link_text = 'Reload page'
+ render_inline(RefreshNoticeComponent.new(
+ streamable: @project,
+ stream_name: :samples,
+ message: custom_message,
+ link_text: custom_link_text
+ ))
+
+ assert_text custom_message
+ assert_text custom_link_text
+ end
+
+ # 🔌 TURBO STREAM CONFIGURATION - Test stream setup
+
+ test 'configures turbo stream with correct streamable and stream name' do
+ render_inline(RefreshNoticeComponent.new(streamable: @project, stream_name: :samples))
+
+ turbo_source = page.find('turbo-cable-stream-source')
+ # The channel attribute should include the project and stream name
+ assert turbo_source['channel'].present?
+ end
+
+ test 'works with different stream names' do
+ %i[samples members attachments].each do |stream_name|
+ render_inline(RefreshNoticeComponent.new(streamable: @project, stream_name: stream_name))
+
+ assert_selector '[data-controller="refresh"]', count: 1
+ assert_selector 'turbo-cable-stream-source', count: 1
+ end
+ end
+
+ # 🎨 SYSTEM ARGUMENTS - Test custom styling and attributes
+
+ test 'accepts and applies additional system arguments' do
+ render_inline(RefreshNoticeComponent.new(
+ streamable: @project,
+ stream_name: :samples,
+ class: 'custom-class',
+ id: 'custom-id'
+ ))
+
+ assert_selector '#custom-id', count: 1
+ assert_selector '.custom-class', count: 1
+ assert_selector '[data-controller="refresh"]', count: 1
+ end
+
+ # 🔧 ACCESSIBILITY - Test accessibility attributes
+
+ test 'alert has proper accessibility attributes' do
+ render_inline(RefreshNoticeComponent.new(streamable: @project, stream_name: :samples))
+
+ assert_selector '[aria-live="assertive"]', count: 1
+ end
+
+ test 'alert is initially hidden' do
+ render_inline(RefreshNoticeComponent.new(streamable: @project, stream_name: :samples))
+
+ # The alert should have the hidden class initially
+ assert_selector '.hidden', count: 1
+ end
+
+ # 🧪 INTEGRATION - Test component structure
+
+ test 'complete component structure is correct' do
+ render_inline(RefreshNoticeComponent.new(
+ streamable: @project,
+ stream_name: :samples,
+ message: 'Test message',
+ link_text: 'Test link'
+ ))
+
+ # Wrapper with refresh controller
+ assert_selector '[data-controller="refresh"]', count: 1
+
+ # Turbo stream source
+ assert_selector 'turbo-cable-stream-source[data-refresh-target="source"]', count: 1
+
+ # Alert notice
+ assert_selector '[data-refresh-target="notice"]', count: 1
+ assert_selector '[aria-live="assertive"]', count: 1
+
+ # Content
+ assert_text 'Test message'
+ assert_text 'Test link'
+ end
+end
From 35d8a15e33424915fc6dbcfa662d65a671151170 Mon Sep 17 00:00:00 2001
From: Josh Adam
Date: Thu, 16 Oct 2025 14:00:07 -0500
Subject: [PATCH 8/8] Adds refresh notice component
Implements a refresh notice component to inform users when data is out of date and provide a button to refresh.
The component utilizes Turbo Streams for real-time updates and includes logic to prevent redundant refreshes triggered by editable cells. It also uses a stimulus controller to handle dismissing the notice and reloading the page. The component is now rendered in the samples index view.
---
.../refresh_notice_component.html.erb | 49 ++++++++++---------
app/components/samples/table_component.rb | 3 +-
.../controllers/editable_cell_controller.js | 13 +++++
.../controllers/refresh_controller.js | 47 +++++++++++++++---
app/views/groups/samples/index.html.erb | 13 ++---
config/locales/en.yml | 2 +
config/locales/fr.yml | 2 +
7 files changed, 92 insertions(+), 37 deletions(-)
diff --git a/app/components/refresh_notice_component.html.erb b/app/components/refresh_notice_component.html.erb
index eff96e630f..68189ebfbc 100644
--- a/app/components/refresh_notice_component.html.erb
+++ b/app/components/refresh_notice_component.html.erb
@@ -1,34 +1,40 @@
>
- <%= helpers.turbo_stream_from streamable, stream_name, data: { "refresh-target": "source" } %>
+ <%= helpers.turbo_stream_from streamable,
+ stream_name,
+ data: {
+ "refresh-target": "source",
+ } %>
-
-
+
-
- <%= helpers.pathogen_icon(
- :info,
- color: :blue,
- size: :md,
- ) %>
+
+ <%= helpers.pathogen_icon(:info, color: :blue, size: :md) %>
-
<%= message %>
-
@@ -37,21 +43,20 @@
data-action="click->refresh#refresh"
class="
px-3 py-1.5 text-sm font-medium text-primary-600 dark:text-primary-400
- hover:text-primary-800 dark:hover:text-primary-300
- transition-colors duration-200 cursor-pointer
+ hover:text-primary-800 dark:hover:text-primary-300 transition-colors
+ duration-200 cursor-pointer
"
>
<%= link_text %>
-