diff options
| author | erdgeist <erdgeist@erdgeist.org> | 2026-07-10 18:07:15 +0200 |
|---|---|---|
| committer | erdgeist <erdgeist@erdgeist.org> | 2026-07-10 18:07:15 +0200 |
| commit | c9401e45433ea45b46f9a8faf1e7e537e4683244 (patch) | |
| tree | c2fabb1942a257f64b4be0c3ab5a5d5bff717d25 | |
| parent | ed9e846d1d85fa326389982e16bbc8799f2001c0 (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
</> entities, corrupting anything containing a literal < or >.
| -rw-r--r-- | app/helpers/events_helper.rb | 11 | ||||
| -rw-r--r-- | app/views/events/index.html.erb | 8 | ||||
| -rw-r--r-- | public/stylesheets/admin.css | 15 | ||||
| -rw-r--r-- | test/models/helpers/events_helper_test.rb | 21 |
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 @@ | |||
| 1 | require 'cgi' | ||
| 2 | |||
| 1 | module EventsHelper | 3 | module 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 | ||
| 2 | end | 13 | end |
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 @@ | |||
| 1 | require 'test_helper' | 1 | require 'test_helper' |
| 2 | 2 | ||
| 3 | class EventsHelperTest < ActionView::TestCase | 3 | class 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 "<script>", 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 | ||
| 4 | end | 25 | end |
