Skip to content

Commit 0d7c254

Browse files
committed
Update platform invitations views for better performance
- Created a new partial `_filter_hidden_fields.html.erb` to preserve filter parameters across form submissions in the platform invitations. - Updated `_platform_invitation.html.erb` to use caching for table rows. - Refactored `index.html.erb` to include the new filter hidden fields partial and improved the form structure for filtering and pagination. - Enhanced localization files (`en.yml`, `es.yml`, `fr.yml`) to include new translations for platform invitation attributes. - Updated controller specs to reflect changes in the platform invitation model and added tests for filtering, searching, and sorting functionality. - Adjusted factories for platform invitations and roles to ensure proper associations and unique invitee emails.
1 parent 8d275ab commit 0d7c254

File tree

13 files changed

+808
-134
lines changed

13 files changed

+808
-134
lines changed

app/controllers/better_together/platform_invitations_controller.rb

Lines changed: 123 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,10 @@ class PlatformInvitationsController < ApplicationController # rubocop:todo Style
1616
def index
1717
authorize BetterTogether::PlatformInvitation
1818

19-
# Use optimized query with all necessary includes to prevent N+1
20-
@platform_invitations = policy_scope(@platform.invitations)
21-
.includes(
22-
{ inviter: [:string_translations] },
23-
{ invitee: [:string_translations] }
24-
)
19+
# Build filtered and sorted collection with pagination
20+
@platform_invitations = build_filtered_collection
21+
@platform_invitations = apply_sorting(@platform_invitations)
22+
@platform_invitations = @platform_invitations.page(params[:page]).per(25)
2523

2624
# Preload roles for the form to prevent N+1 queries during rendering
2725
@community_roles = BetterTogether::Role.where(resource_type: 'BetterTogether::Community')
@@ -64,8 +62,8 @@ def create # rubocop:todo Metrics/AbcSize, Metrics/MethodLength
6462
else
6563
flash.now[:alert] = t('flash.generic.error_create', resource: t('resources.invitation'))
6664
format.html do
67-
redirect_to platform_platform_invitations_path(@platform),
68-
alert: @platform_invitation.errors.full_messages.to_sentence
65+
index
66+
render :index, status: :unprocessable_entity
6967
end
7068
format.turbo_stream do
7169
render turbo_stream: [
@@ -147,6 +145,119 @@ def set_platform_invitation
147145
@platform_invitation = @platform.invitations.find(params[:id])
148146
end
149147

148+
def build_filtered_collection
149+
collection = base_collection
150+
collection = apply_status_filter(collection) if filter_params[:status].present? || params[:status].present?
151+
collection = apply_email_filter(collection) if filter_params[:search].present? || params[:search].present?
152+
collection = apply_valid_from_filter(collection) if filter_params[:valid_from].present?
153+
collection = apply_valid_until_filter(collection) if filter_params[:valid_until].present?
154+
collection = apply_accepted_at_filter(collection) if filter_params[:accepted_at].present?
155+
collection = apply_last_sent_filter(collection) if filter_params[:last_sent].present?
156+
collection
157+
end
158+
159+
def apply_sorting(collection)
160+
sort_by = params[:sort_by]
161+
sort_direction = params[:sort_direction] == 'asc' ? :asc : :desc
162+
163+
default_sort = { created_at: sort_direction }
164+
165+
case sort_by
166+
when 'invitee_email'
167+
collection.order({ invitee_email: sort_direction }.merge(default_sort))
168+
when 'status'
169+
collection.order({ status: sort_direction }.merge(default_sort))
170+
when 'created_at'
171+
collection.order({ created_at: sort_direction }.merge(default_sort))
172+
when 'valid_from'
173+
collection.order({ valid_from: sort_direction }.merge(default_sort))
174+
when 'valid_until'
175+
collection.order({ valid_until: sort_direction }.merge(default_sort))
176+
when 'accepted_at'
177+
collection.order({ accepted_at: sort_direction }.merge(default_sort))
178+
when 'last_sent'
179+
collection.order({ last_sent: sort_direction }.merge(default_sort))
180+
else
181+
# Default sort by created_at (newest first)
182+
collection.order(created_at: :desc)
183+
end
184+
end
185+
186+
def base_collection
187+
policy_scope(@platform.invitations).includes(
188+
{ inviter: [:string_translations] },
189+
{ invitee: [:string_translations] }
190+
)
191+
end
192+
193+
def apply_status_filter(collection)
194+
status = filter_params[:status] || params[:status]
195+
collection.where(status: status) if status.present?
196+
end
197+
198+
def apply_email_filter(collection)
199+
search_term = filter_params[:search] || params[:search]
200+
return collection unless search_term.present?
201+
202+
collection.where('invitee_email ILIKE ?', "%#{search_term.strip}%")
203+
end
204+
205+
def apply_valid_from_filter(collection)
206+
date_filter = filter_params[:valid_from]
207+
return collection unless date_filter.present?
208+
209+
apply_datetime_filter(collection, :valid_from, date_filter)
210+
end
211+
212+
def apply_valid_until_filter(collection)
213+
date_filter = filter_params[:valid_until]
214+
return collection unless date_filter.present?
215+
216+
apply_datetime_filter(collection, :valid_until, date_filter)
217+
end
218+
219+
def apply_accepted_at_filter(collection)
220+
date_filter = filter_params[:accepted_at]
221+
return collection unless date_filter.present?
222+
223+
apply_datetime_filter(collection, :accepted_at, date_filter)
224+
end
225+
226+
def apply_last_sent_filter(collection)
227+
date_filter = filter_params[:last_sent]
228+
return collection unless date_filter.present?
229+
230+
apply_datetime_filter(collection, :last_sent, date_filter)
231+
end
232+
233+
def apply_datetime_filter(collection, column, date_filter)
234+
return collection unless date_filter.is_a?(Hash)
235+
236+
if date_filter[:from].present?
237+
from_date = parse_date(date_filter[:from])
238+
collection = collection.where("#{column} >= ?", from_date.beginning_of_day) if from_date
239+
end
240+
241+
if date_filter[:to].present?
242+
to_date = parse_date(date_filter[:to])
243+
collection = collection.where("#{column} <= ?", to_date.end_of_day) if to_date
244+
end
245+
246+
collection
247+
end
248+
249+
def parse_date(date_string)
250+
return nil unless date_string.present?
251+
252+
Date.parse(date_string.to_s)
253+
rescue ArgumentError
254+
nil
255+
end
256+
257+
def filter_params
258+
params[:filters] || {}
259+
end
260+
150261
def platform_invitation_params
151262
params.require(:platform_invitation).permit(
152263
:invitee_email, :platform_role_id, :community_role_id, :locale,
@@ -156,11 +267,13 @@ def platform_invitation_params
156267
end
157268

158269
def param_invitation_class
159-
param_type = params[:platform_invitation][:type]
270+
param_type = params[:platform_invitation]&.[](:type)
160271

161272
Rails.application.eager_load! unless Rails.env.production? # Ensure all models are loaded
162273
valid_types = [BetterTogether::PlatformInvitation, *BetterTogether::PlatformInvitation.descendants]
163-
valid_types.find { |klass| klass.to_s == param_type }
274+
found_class = valid_types.find { |klass| klass.to_s == param_type }
275+
276+
found_class || BetterTogether::PlatformInvitation
164277
end
165278
end
166279
# rubocop:enable Metrics/ClassLength
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
# frozen_string_literal: true
2+
3+
module BetterTogether
4+
# Helper methods for platform invitations views
5+
module PlatformInvitationsHelper
6+
def sortable_column_header_for_invitations(column, label, platform)
7+
sort_info = calculate_sort_info_for_invitations(column)
8+
9+
link_to build_sort_path_for_invitations(column, sort_info[:direction], platform),
10+
sort_link_options_for_invitations do
11+
build_sort_content_for_invitations(label, sort_info[:icon_class])
12+
end
13+
end
14+
15+
def calculate_sort_info_for_invitations(column)
16+
if currently_sorted_by_invitations?(column)
17+
active_column_sort_info_for_invitations
18+
else
19+
default_column_sort_info_for_invitations
20+
end
21+
end
22+
23+
def currently_sorted_by_invitations?(column)
24+
params[:sort_by] == column.to_s
25+
end
26+
27+
def active_column_sort_info_for_invitations
28+
current_direction = params[:sort_direction]
29+
{
30+
direction: current_direction == 'asc' ? 'desc' : 'asc',
31+
icon_class: current_direction == 'asc' ? 'fas fa-sort-up' : 'fas fa-sort-down'
32+
}
33+
end
34+
35+
def default_column_sort_info_for_invitations
36+
{
37+
direction: 'asc',
38+
icon_class: 'fas fa-sort text-muted'
39+
}
40+
end
41+
42+
def build_sort_path_for_invitations(column, direction, platform)
43+
platform_platform_invitations_path(platform,
44+
filters: {
45+
search: current_search_filter_for_invitations,
46+
status: current_status_filter_for_invitations,
47+
valid_from: current_valid_from_filter_for_invitations.presence,
48+
valid_until: current_valid_until_filter_for_invitations.presence,
49+
accepted_at: current_accepted_at_filter_for_invitations.presence,
50+
last_sent: current_last_sent_filter_for_invitations.presence
51+
}.compact_blank,
52+
sort_by: column,
53+
sort_direction: direction,
54+
page: params[:page])
55+
end
56+
57+
def sort_link_options_for_invitations
58+
{
59+
class: 'text-decoration-none d-flex align-items-center justify-content-between',
60+
data: {
61+
turbo_frame: 'platform_invitations_content',
62+
turbo_prefetch: false # disable Turbo prefetch for these links
63+
}
64+
}
65+
end
66+
67+
def build_sort_content_for_invitations(label, icon_class)
68+
safe_join([
69+
content_tag(:span, label),
70+
content_tag(:i, '', class: icon_class, 'aria-hidden': true)
71+
])
72+
end
73+
74+
def current_search_filter_for_invitations
75+
filter_params[:search] || params[:search]
76+
end
77+
78+
def current_status_filter_for_invitations
79+
filter_params[:status] || params[:status]
80+
end
81+
82+
def current_valid_from_filter_for_invitations
83+
filter = filter_params[:valid_from] || {}
84+
{
85+
from: filter[:from].presence,
86+
to: filter[:to].presence
87+
}.compact
88+
end
89+
90+
def current_valid_until_filter_for_invitations
91+
filter = filter_params[:valid_until] || {}
92+
{
93+
from: filter[:from].presence,
94+
to: filter[:to].presence
95+
}.compact
96+
end
97+
98+
def current_accepted_at_filter_for_invitations
99+
filter = filter_params[:accepted_at] || {}
100+
{
101+
from: filter[:from].presence,
102+
to: filter[:to].presence
103+
}.compact
104+
end
105+
106+
def current_last_sent_filter_for_invitations
107+
filter = filter_params[:last_sent] || {}
108+
{
109+
from: filter[:from].presence,
110+
to: filter[:to].presence
111+
}.compact
112+
end
113+
114+
private
115+
116+
def filter_params
117+
params[:filters] || {}
118+
end
119+
end
120+
end

app/models/better_together/platform.rb

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ class Platform < ApplicationRecord
2020
member_type: 'person'
2121

2222
has_many :invitations,
23-
-> { order(created_at: :desc) },
2423
class_name: '::BetterTogether::PlatformInvitation',
2524
foreign_key: :invitable_id
2625

app/models/better_together/platform_invitation.rb

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ class PlatformInvitation < ApplicationRecord
3232

3333
has_rich_text :greeting, encrypted: true
3434

35-
validates :invitee_email, uniqueness: { scope: :invitable_id, allow_nil: true }
35+
validates :invitee_email, presence: true, format: { with: URI::MailTo::EMAIL_REGEXP }
3636
validates :invitee_email, uniqueness: { scope: :invitable_id, allow_nil: true, allow_blank: true }
3737
validates :locale, presence: true, inclusion: { in: I18n.available_locales.map(&:to_s) }
3838
validates :status, presence: true, inclusion: { in: STATUS_VALUES.values }
@@ -84,6 +84,14 @@ def to_s
8484
"[#{self.class.model_name.human}] - #{id}"
8585
end
8686

87+
# Attributes permitted for strong parameters
88+
def self.permitted_attributes(id: false, destroy: false)
89+
super + %i[
90+
invitee_email platform_role_id community_role_id locale
91+
valid_from valid_until greeting session_duration_mins
92+
]
93+
end
94+
8795
private
8896

8997
def set_accepted_timestamp
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
<%
2+
# Partial to preserve filter parameters across form submissions
3+
# Params:
4+
# - form: the form builder
5+
# - exclude: array of filter types to exclude (symbols)
6+
7+
exclude_fields = local_assigns[:exclude] || []
8+
%>
9+
10+
<% unless exclude_fields.include?(:search) %>
11+
<% if current_search_filter_for_invitations.present? %>
12+
<%= form.hidden_field 'filters[search]', value: current_search_filter_for_invitations %>
13+
<% end %>
14+
<% end %>
15+
16+
<% unless exclude_fields.include?(:status) %>
17+
<% if current_status_filter_for_invitations.present? %>
18+
<%= form.hidden_field 'filters[status]', value: current_status_filter_for_invitations %>
19+
<% end %>
20+
<% end %>
21+
22+
<% unless exclude_fields.include?(:valid_from) %>
23+
<% if current_valid_from_filter_for_invitations.any? %>
24+
<% current_valid_from_filter_for_invitations.each do |key, value| %>
25+
<%= form.hidden_field "filters[valid_from][#{key}]", value: value %>
26+
<% end %>
27+
<% end %>
28+
<% end %>
29+
30+
<% unless exclude_fields.include?(:valid_until) %>
31+
<% if current_valid_until_filter_for_invitations.any? %>
32+
<% current_valid_until_filter_for_invitations.each do |key, value| %>
33+
<%= form.hidden_field "filters[valid_until][#{key}]", value: value %>
34+
<% end %>
35+
<% end %>
36+
<% end %>
37+
38+
<% unless exclude_fields.include?(:accepted_at) %>
39+
<% if current_accepted_at_filter_for_invitations.any? %>
40+
<% current_accepted_at_filter_for_invitations.each do |key, value| %>
41+
<%= form.hidden_field "filters[accepted_at][#{key}]", value: value %>
42+
<% end %>
43+
<% end %>
44+
<% end %>
45+
46+
<% unless exclude_fields.include?(:last_sent) %>
47+
<% if current_last_sent_filter_for_invitations.any? %>
48+
<% current_last_sent_filter_for_invitations.each do |key, value| %>
49+
<%= form.hidden_field "filters[last_sent][#{key}]", value: value %>
50+
<% end %>
51+
<% end %>
52+
<% end %>
53+
54+
<!-- Preserve sort and pagination parameters -->
55+
<%= form.hidden_field :sort_by, value: params[:sort_by] if params[:sort_by].present? %>
56+
<%= form.hidden_field :sort_direction, value: params[:sort_direction] if params[:sort_direction].present? %>
57+
<%= form.hidden_field :page, value: params[:page] if params[:page].present? %>

0 commit comments

Comments
 (0)