summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorerdgeist <erdgeist@erdgeist.org>2026-07-10 18:07:15 +0200
committererdgeist <erdgeist@erdgeist.org>2026-07-10 18:07:15 +0200
commitc9401e45433ea45b46f9a8faf1e7e537e4683244 (patch)
treec2fabb1942a257f64b4be0c3ab5a5d5bff717d25
parented9e846d1d85fa326389982e16bbc8799f2001c0 (diff)
Fix rrule/url column overflow on events#index
Long RRULEs previously overflowed their column with no wrap point; now escaped and rendered with a <wbr> after each semicolon, so a long rule wraps at a clause boundary instead of running off the table. Deliberately not truncated -- a cut-off RRULE's trailing clause (BYDAY, BYMONTH, etc.) is usually the most specific part. The url column is now a real link, truncated with an ellipsis at a fixed width -- deliberately no tooltip, since hovering a real link already shows the full address in the browser's own status bar. rrule_with_break_opportunities splits on the raw string's own semicolons before escaping each piece, not after -- escaping first and searching the result for semicolons also matches the ones inside &lt;/&gt; entities, corrupting anything containing a literal < or >.
-rw-r--r--app/helpers/events_helper.rb11
-rw-r--r--app/views/events/index.html.erb8
-rw-r--r--public/stylesheets/admin.css15
-rw-r--r--test/models/helpers/events_helper_test.rb21
4 files changed, 53 insertions, 2 deletions
diff --git a/app/helpers/events_helper.rb b/app/helpers/events_helper.rb
index 8a9a878..5e84f53 100644
--- a/app/helpers/events_helper.rb
+++ b/app/helpers/events_helper.rb
@@ -1,2 +1,13 @@
1require 'cgi'
2
1module EventsHelper 3module EventsHelper
4 # Insert a zero-width break opportunity after each semicolon, so a long
5 # RRULE can wrap at a clause boundary instead of overflowing its column.
6 # Deliberately <wbr>, not ellipsis -- unlike a URL, an RRULE's trailing
7 # characters (BYDAY, BYMONTH, etc.) are usually the most specific part,
8 # and truncating them would hide exactly the wrong end of the string.
9 def rrule_with_break_opportunities(rrule)
10 return "" if rrule.blank?
11 raw(rrule.split(';', -1).map { |part| CGI.escapeHTML(part) }.join(';<wbr>'))
12 end
2end 13end
diff --git a/app/views/events/index.html.erb b/app/views/events/index.html.erb
index 8127e3a..3d953d5 100644
--- a/app/views/events/index.html.erb
+++ b/app/views/events/index.html.erb
@@ -22,9 +22,13 @@
22 <td><%= link_to event.display_title, event %></td> 22 <td><%= link_to event.display_title, event %></td>
23 <td><%=h event.start_time %></td> 23 <td><%=h event.start_time %></td>
24 <td><%=h event.end_time %></td> 24 <td><%=h event.end_time %></td>
25 <td><%=h event.rrule %></td> 25 <td class="rrule_cell"><span class="rrule_text"><%= rrule_with_break_opportunities(event.rrule) %></span></td>
26 <td><%=h event.allday %></td> 26 <td><%=h event.allday %></td>
27 <td><%=h event.url %></td> 27 <td class="url_cell">
28 <% if event.url.present? %>
29 <span class="truncate"><%= link_to event.url, event.url %></span>
30 <% end %>
31 </td>
28 <td><%= event.node ? link_to(event.node_id, node_path(event.node)) : '' %></td> 32 <td><%= event.node ? link_to(event.node_id, node_path(event.node)) : '' %></td>
29 <td><%= link_to 'edit', edit_event_path(event) %></td> 33 <td><%= link_to 'edit', edit_event_path(event) %></td>
30 </tr> 34 </tr>
diff --git a/public/stylesheets/admin.css b/public/stylesheets/admin.css
index 1a81728..4723f50 100644
--- a/public/stylesheets/admin.css
+++ b/public/stylesheets/admin.css
@@ -519,6 +519,21 @@ table.revisions_table tr:hover {
519 background-color: #f1f1f1; 519 background-color: #f1f1f1;
520} 520}
521 521
522.events_table .rrule_text {
523 display: inline-block;
524 max-width: 300px;
525 overflow-wrap: break-word;
526}
527
528.events_table .truncate {
529 display: inline-block;
530 max-width: 200px;
531 overflow: hidden;
532 text-overflow: ellipsis;
533 white-space: nowrap;
534 vertical-align: bottom;
535}
536
522#diffview del { 537#diffview del {
523 background: #ffd7d5; 538 background: #ffd7d5;
524 color: #82071e; 539 color: #82071e;
diff --git a/test/models/helpers/events_helper_test.rb b/test/models/helpers/events_helper_test.rb
index 2e7567e..0486b3e 100644
--- a/test/models/helpers/events_helper_test.rb
+++ b/test/models/helpers/events_helper_test.rb
@@ -1,4 +1,25 @@
1require 'test_helper' 1require 'test_helper'
2 2
3class EventsHelperTest < ActionView::TestCase 3class EventsHelperTest < ActionView::TestCase
4 test "rrule_with_break_opportunities inserts a break opportunity after each semicolon" do
5 result = rrule_with_break_opportunities("FREQ=MONTHLY;BYMONTH=1,2,3;BYDAY=-1TH")
6 assert_equal "FREQ=MONTHLY;<wbr>BYMONTH=1,2,3;<wbr>BYDAY=-1TH", result
7 assert result.html_safe?
8 end
9
10 test "rrule_with_break_opportunities escapes HTML-significant characters" do
11 result = rrule_with_break_opportunities("FREQ=WEEKLY;BYDAY=<script>")
12 assert_no_match /<script>/, result
13 assert_match "&lt;script&gt;", result
14 end
15
16 test "rrule_with_break_opportunities returns an empty string for blank input" do
17 assert_equal "", rrule_with_break_opportunities(nil)
18 assert_equal "", rrule_with_break_opportunities("")
19 end
20
21 test "rrule_with_break_opportunities preserves a trailing semicolon" do
22 result = rrule_with_break_opportunities("FREQ=WEEKLY;")
23 assert_equal "FREQ=WEEKLY;<wbr>", result
24 end
4end 25end