From a8ae0a66e3ad2b2481a9c31214af433f4d90b72a Mon Sep 17 00:00:00 2001 From: Robert Smith Date: Sun, 26 Oct 2025 19:14:22 -0230 Subject: [PATCH 01/18] feat(models): specify translation types for name and update building connection labels --- app/models/better_together/community.rb | 2 +- .../infrastructure/building_connections.rb | 31 +++++++++---------- 2 files changed, 16 insertions(+), 17 deletions(-) diff --git a/app/models/better_together/community.rb b/app/models/better_together/community.rb index 008170ef2..890655424 100644 --- a/app/models/better_together/community.rb +++ b/app/models/better_together/community.rb @@ -26,7 +26,7 @@ class Community < ApplicationRecord slugged :name - translates :name + translates :name, type: :string translates :description, type: :text translates :description_html, backend: :action_text diff --git a/app/models/concerns/better_together/infrastructure/building_connections.rb b/app/models/concerns/better_together/infrastructure/building_connections.rb index 4956f949b..252a6d44a 100644 --- a/app/models/concerns/better_together/infrastructure/building_connections.rb +++ b/app/models/concerns/better_together/infrastructure/building_connections.rb @@ -35,23 +35,22 @@ def leaflet_points # rubocop:todo Metrics/AbcSize, Metrics/MethodLength point = building.to_leaflet_point next if point.nil? + place_label = (" - #{building.address.text_label}" if building.address.text_label.present?) + + place_url = Rails.application.routes.url_helpers.polymorphic_path( + self, + locale: I18n.locale + ) + + place_link = "#{name}#{place_label}" + + address_label = building.address.to_formatted_s( + excluded: [:display_label] + ) + point.merge( - label: "#{name}#{if building.address.text_label.present? - building.address.text_label - end}", - popup_html: "#{name}#{if building.address.text_label.present? - " - #{building.address.text_label}" - end}
#{ - building.address.to_formatted_s( - excluded: [:display_label] - ) - }" + label: place_link, + popup_html: place_link + "
#{address_label}" ) end.compact end From 41ed515dde95d06cd39e818802849c5271c4b999 Mon Sep 17 00:00:00 2001 From: Robert Smith Date: Sun, 26 Oct 2025 22:19:27 -0230 Subject: [PATCH 02/18] Refactor pages controller to include filtering and sorting functionality - Removed unused resources from routes. - Enhanced the index action to support title and slug filtering. - Implemented sorting by title and slug in the index action. - Added pagination to the index view. - Introduced auto-submit functionality for filter inputs. - Created new JavaScript controllers for person search and auto-submit. - Added caching for pages and page content to improve performance. - Updated views to reflect new filtering and sorting features. - Added new pagination views for better navigation. - Updated locale files to include new translation keys for filters and pagination. - Added request specs for pages filtering and sorting functionality. --- .../controllers/event_datetime_controller.js | 127 ------------------ .../better_together/application.scss | 1 + .../better_together/pagination.scss | 79 +++++++++++ .../better_together/pages_controller.rb | 66 ++++++++- app/helpers/better_together/pages_helper.rb | 76 +++++++++++ .../better_together/auto_submit_controller.js | 47 +++++++ .../person_search_controller.js | 0 .../better_together/pages/_page_row.html.erb | 4 +- .../better_together/pages/index.html.erb | 94 ++++++++++++- app/views/better_together/pages/show.html.erb | 33 +++-- app/views/kaminari/_first_page.html.erb | 13 ++ app/views/kaminari/_gap.html.erb | 9 ++ app/views/kaminari/_last_page.html.erb | 13 ++ app/views/kaminari/_next_page.html.erb | 13 ++ app/views/kaminari/_page.html.erb | 19 +++ app/views/kaminari/_paginator.html.erb | 36 +++++ app/views/kaminari/_prev_page.html.erb | 13 ++ config/locales/en.yml | 19 +++ config/locales/es.yml | 19 +++ config/locales/fr.yml | 19 +++ config/routes.rb | 2 - .../better_together/pages_filtering_spec.rb | 78 +++++++++++ 22 files changed, 632 insertions(+), 148 deletions(-) delete mode 100644 app/assets/javascripts/better_together/controllers/event_datetime_controller.js create mode 100644 app/assets/stylesheets/better_together/pagination.scss create mode 100644 app/javascript/controllers/better_together/auto_submit_controller.js rename app/{assets/javascripts/better_together/controllers => javascript/controllers/better_together}/person_search_controller.js (100%) create mode 100644 app/views/kaminari/_first_page.html.erb create mode 100644 app/views/kaminari/_gap.html.erb create mode 100644 app/views/kaminari/_last_page.html.erb create mode 100644 app/views/kaminari/_next_page.html.erb create mode 100644 app/views/kaminari/_page.html.erb create mode 100644 app/views/kaminari/_paginator.html.erb create mode 100644 app/views/kaminari/_prev_page.html.erb create mode 100644 spec/requests/better_together/pages_filtering_spec.rb diff --git a/app/assets/javascripts/better_together/controllers/event_datetime_controller.js b/app/assets/javascripts/better_together/controllers/event_datetime_controller.js deleted file mode 100644 index 32be1f825..000000000 --- a/app/assets/javascripts/better_together/controllers/event_datetime_controller.js +++ /dev/null @@ -1,127 +0,0 @@ -// Event DateTime Controller -// Handles dynamic synchronization between start time, end time, and duration fields -// -// Behavior: -// - When start time changes: Updates end time based on current duration -// - When end time changes: Updates duration based on start/end time difference -// - When duration changes: Updates end time based on start time + duration -// - Validates minimum duration (5 minutes) -// - Defaults duration to 30 minutes when not set - -import { Controller } from "@hotwired/stimulus" - -export default class extends Controller { - static targets = ["startTime", "endTime", "duration"] - - connect() { - // Set default duration if not already set - if (!this.durationTarget.value || this.durationTarget.value === "0") { - this.durationTarget.value = "30" - } - - // Initialize end time if start time is set but end time is not - if (this.startTimeTarget.value && !this.endTimeTarget.value) { - this.updateEndTimeFromDuration() - } - } - - // Called when start time changes - updateEndTime() { - if (!this.startTimeTarget.value) { - this.endTimeTarget.value = "" - return - } - - // Use current duration or default to 30 minutes - const duration = this.getDurationInMinutes() - this.calculateEndTime(duration) - } - - // Called when end time changes - updateDuration() { - if (!this.startTimeTarget.value || !this.endTimeTarget.value) { - return - } - - const startTime = new Date(this.startTimeTarget.value) - const endTime = new Date(this.endTimeTarget.value) - - // Validate end time is after start time - if (endTime <= startTime) { - this.showTemporaryError("End time must be after start time") - return - } - - // Calculate duration in minutes - const diffInMs = endTime.getTime() - startTime.getTime() - const diffInMinutes = Math.round(diffInMs / (1000 * 60)) - - // Enforce minimum duration - if (diffInMinutes < 5) { - this.durationTarget.value = "5" - this.calculateEndTime(5) - } else { - this.durationTarget.value = diffInMinutes.toString() - } - } - - // Called when duration changes - updateEndTimeFromDuration() { - if (!this.startTimeTarget.value) { - return - } - - const duration = this.getDurationInMinutes() - - // Enforce minimum duration - if (duration < 5) { - this.durationTarget.value = "5" - this.calculateEndTime(5) - } else { - this.calculateEndTime(duration) - } - } - - // Helper methods - getDurationInMinutes() { - const duration = parseInt(this.durationTarget.value) || 30 - return Math.max(duration, 5) // Minimum 5 minutes - } - - calculateEndTime(durationMinutes) { - if (!this.startTimeTarget.value) return - - const startTime = new Date(this.startTimeTarget.value) - const endTime = new Date(startTime.getTime() + (durationMinutes * 60 * 1000)) - - // Format for datetime-local input (YYYY-MM-DDTHH:MM) - const year = endTime.getFullYear() - const month = String(endTime.getMonth() + 1).padStart(2, '0') - const day = String(endTime.getDate()).padStart(2, '0') - const hours = String(endTime.getHours()).padStart(2, '0') - const minutes = String(endTime.getMinutes()).padStart(2, '0') - - this.endTimeTarget.value = `${year}-${month}-${day}T${hours}:${minutes}` - } - - showTemporaryError(message) { - // Create or update error message - let errorElement = this.element.querySelector('.datetime-sync-error') - - if (!errorElement) { - errorElement = document.createElement('div') - errorElement.className = 'alert alert-warning datetime-sync-error mt-2' - errorElement.setAttribute('role', 'alert') - this.element.appendChild(errorElement) - } - - errorElement.textContent = message - - // Remove error after 3 seconds - setTimeout(() => { - if (errorElement && errorElement.parentNode) { - errorElement.parentNode.removeChild(errorElement) - } - }, 3000) - } -} diff --git a/app/assets/stylesheets/better_together/application.scss b/app/assets/stylesheets/better_together/application.scss index 641bafa30..b65c6764f 100644 --- a/app/assets/stylesheets/better_together/application.scss +++ b/app/assets/stylesheets/better_together/application.scss @@ -36,6 +36,7 @@ @use 'metrics'; @use 'navigation'; @use 'notifications'; +@use 'pagination'; @use 'profiles'; @use 'share'; @use 'sidebar_nav'; diff --git a/app/assets/stylesheets/better_together/pagination.scss b/app/assets/stylesheets/better_together/pagination.scss new file mode 100644 index 000000000..1282a3ee6 --- /dev/null +++ b/app/assets/stylesheets/better_together/pagination.scss @@ -0,0 +1,79 @@ +/* Better Together Pagination Styles */ +.pagination { + --bs-pagination-padding-x: 0.75rem; + --bs-pagination-padding-y: 0.5rem; + --bs-pagination-font-size: 0.875rem; + --bs-pagination-color: var(--bs-link-color); + --bs-pagination-bg: transparent; + --bs-pagination-border-width: 0; + --bs-pagination-border-color: transparent; + --bs-pagination-border-radius: 0.375rem; + --bs-pagination-hover-color: var(--bs-primary); + --bs-pagination-hover-bg: var(--bs-gray-100); + --bs-pagination-hover-border-color: transparent; + --bs-pagination-focus-color: var(--bs-primary); + --bs-pagination-focus-bg: var(--bs-gray-100); + --bs-pagination-focus-box-shadow: 0 0 0 0.25rem rgba(var(--bs-primary-rgb), 0.25); + --bs-pagination-active-color: #fff; + --bs-pagination-active-bg: var(--bs-primary); + --bs-pagination-active-border-color: var(--bs-primary); + --bs-pagination-disabled-color: var(--bs-secondary-color); + --bs-pagination-disabled-bg: transparent; + --bs-pagination-disabled-border-color: transparent; +} + +.pagination-info { + display: flex; + align-items: center; + min-height: 2rem; +} + +.pagination-info small { + font-weight: 500; + letter-spacing: 0.025em; +} + +/* Responsive pagination */ +@media (max-width: 768px) { + .pagination-info { + margin-bottom: 0.75rem; + text-align: center; + width: 100%; + } + + .pagination { + justify-content: center !important; + } + + .d-flex.justify-content-between.align-items-center.flex-wrap { + flex-direction: column; + align-items: center; + } +} + +/* Enhanced focus states for accessibility */ +.page-link:focus { + outline: 2px solid transparent; + outline-offset: 2px; +} + +.page-link:focus-visible { + box-shadow: var(--bs-pagination-focus-box-shadow); + outline: 2px solid var(--bs-primary); + outline-offset: 2px; +} + +/* Smooth transitions */ +.page-link { + transition: all 0.15s ease-in-out; +} + +/* Card styling for pagination container */ +.pagination .card { + background: var(--bs-gray-50); + border: 1px solid var(--bs-border-color-translucent); +} + +.pagination .card-body { + background: transparent; +} \ No newline at end of file diff --git a/app/controllers/better_together/pages_controller.rb b/app/controllers/better_together/pages_controller.rb index 81d0361c4..48afb256a 100644 --- a/app/controllers/better_together/pages_controller.rb +++ b/app/controllers/better_together/pages_controller.rb @@ -14,14 +14,46 @@ class PagesController < FriendlyResourceController # rubocop:todo Metrics/ClassL def index authorize resource_class - @pages = resource_collection + + # Start with the base collection + @pages = resource_collection.includes( + :string_translations, + page_blocks: { + block: [{ background_image_file_attachment: :blob }] + } + ) + + # Apply title search filter if present + if params[:title_filter].present? + search_term = params[:title_filter].strip + @pages = @pages.i18n do + title.matches("%#{search_term}%") + end + end + + # Apply slug search filter if present + if params[:slug_filter].present? + search_term = params[:slug_filter].strip + @pages = @pages.i18n do + slug.matches("%#{search_term}%") + end + end + + # Apply sorting + @pages = apply_sorting(@pages) + + # Apply pagination + @pages = @pages.page(params[:page]).per(25) end def show # Hide pages that don't exist or aren't viewable to the current user as 404s render_not_found and return if @page.nil? - @content_blocks = @page.content_blocks + # Preload content blocks with their associations for better performance + @content_blocks = @page.content_blocks.includes( + background_image_file_attachment: :blob + ) @layout = 'layouts/better_together/page' @layout = @page.layout if @page.layout.present? end @@ -141,6 +173,18 @@ def safe_page_redirect_url def set_page @page = set_resource_instance + # Preload associations for better performance when page is loaded + if @page + @page = resource_class.includes( + :string_translations, + :sidebar_nav, + page_blocks: { + block: [ + { background_image_file_attachment: :blob } + ] + } + ).find(@page.id) + end rescue ActiveRecord::RecordNotFound render_not_found && return end @@ -172,6 +216,24 @@ def resource_collection policy_scope(resource_class) end + def apply_sorting(collection) + sort_by = params[:sort_by] + sort_direction = params[:sort_direction] == 'desc' ? :desc : :asc + table = collection.arel_table + + case sort_by + when 'title' + # Sort by translated title using i18n scope with Arel + collection.i18n.order(title: sort_direction) + when 'slug' + # Sort by translated slug using i18n scope with Arel + collection.i18n.order(slug: sort_direction) + else + # Default sorting by identifier + collection.order(table[:identifier].send(sort_direction)) + end + end + def translatable_conditions [] end diff --git a/app/helpers/better_together/pages_helper.rb b/app/helpers/better_together/pages_helper.rb index 092aa25c6..4893a677d 100644 --- a/app/helpers/better_together/pages_helper.rb +++ b/app/helpers/better_together/pages_helper.rb @@ -7,5 +7,81 @@ def render_page_content(page) render @page.content_blocks end end + + def pages_cache_key(pages) + [ + 'pages-index', + pages.maximum(:updated_at), + pages.current_page, + pages.total_pages, + pages.size, + current_user&.id, + I18n.locale, + params[:title_filter], + params[:slug_filter], + params[:sort_by], + params[:sort_direction], + 'v3' + ] + end + + def page_row_cache_key(page) + [ + 'page-row', + page.id, + page.updated_at, + page.page_blocks.maximum(:updated_at), + current_user&.id, + I18n.locale, + 'v1' + ] + end + + def page_show_cache_key(page) + [ + 'page-show', + page.id, + page.updated_at, + page.page_blocks.maximum(:updated_at), + page.blocks.maximum(:updated_at), + current_user&.id, + I18n.locale, + 'v1' + ] + end + + def sortable_column_header(column, label) + current_sort = params[:sort_by] + current_direction = params[:sort_direction] + + # Determine new direction + if current_sort == column.to_s + new_direction = current_direction == 'asc' ? 'desc' : 'asc' + icon_class = current_direction == 'asc' ? 'fas fa-sort-up' : 'fas fa-sort-down' + else + new_direction = 'asc' + icon_class = 'fas fa-sort text-muted' + end + + link_to pages_path( + title_filter: params[:title_filter], + sort_by: column, + sort_direction: new_direction, + page: params[:page] + ), class: 'text-decoration-none d-flex align-items-center justify-content-between' do + safe_join([ + content_tag(:span, label), + content_tag(:i, '', class: icon_class, 'aria-hidden': true) + ]) + end + end + + def current_title_filter + params[:title_filter] || '' + end + + def current_slug_filter + params[:slug_filter] || '' + end end end diff --git a/app/javascript/controllers/better_together/auto_submit_controller.js b/app/javascript/controllers/better_together/auto_submit_controller.js new file mode 100644 index 000000000..98a0f6b35 --- /dev/null +++ b/app/javascript/controllers/better_together/auto_submit_controller.js @@ -0,0 +1,47 @@ +import { Controller } from "@hotwired/stimulus" + +// Auto-submit forms with a debounce delay +// +// Usage: +//
+// +//
+export default class extends Controller { + static values = { delay: { type: Number, default: 300 } } + + connect() { + this.timeout = null + + // Auto-attach to all input fields in the form + this.element.querySelectorAll('input[type="text"], input[type="search"], select, textarea').forEach(input => { + input.addEventListener('input', this.scheduleSubmit.bind(this)) + input.addEventListener('change', this.scheduleSubmit.bind(this)) + }) + } + + disconnect() { + this.clearTimeout() + } + + scheduleSubmit() { + this.clearTimeout() + + this.timeout = setTimeout(() => { + this.submit() + }, this.delayValue) + } + + submit() { + this.clearTimeout() + + // Submit the form + this.element.requestSubmit() + } + + clearTimeout() { + if (this.timeout) { + clearTimeout(this.timeout) + this.timeout = null + } + } +} \ No newline at end of file diff --git a/app/assets/javascripts/better_together/controllers/person_search_controller.js b/app/javascript/controllers/better_together/person_search_controller.js similarity index 100% rename from app/assets/javascripts/better_together/controllers/person_search_controller.js rename to app/javascript/controllers/better_together/person_search_controller.js diff --git a/app/views/better_together/pages/_page_row.html.erb b/app/views/better_together/pages/_page_row.html.erb index ff54bdb67..4ec1ea653 100644 --- a/app/views/better_together/pages/_page_row.html.erb +++ b/app/views/better_together/pages/_page_row.html.erb @@ -1,11 +1,11 @@ - <%= link_to page.title, render_page_path(page), target: "_blank", rel: "noopener" %> + <%= link_to page.title, render_page_path(page.slug), target: "_blank", rel: "noopener" %> <%= page.slug %> <%= link_to page.url, page.url, target: "_blank", rel: "noopener" %> <%= page.published? ? t('globals.published') : t('globals.draft') %> <% if policy(page).show? %> - <%= link_to render_page_path(page), class: 'btn btn-outline-info btn-sm me-1', 'aria-label' => 'Show Page' do %> + <%= link_to render_page_path(page.slug), class: 'btn btn-outline-info btn-sm me-1', 'aria-label' => 'Show Page' do %> <%= t('globals.show') %> <% end %> <% end %> diff --git a/app/views/better_together/pages/index.html.erb b/app/views/better_together/pages/index.html.erb index 08ab43bca..bcd4c9ef0 100644 --- a/app/views/better_together/pages/index.html.erb +++ b/app/views/better_together/pages/index.html.erb @@ -24,6 +24,8 @@ <% end %> + <%= paginate @pages %> +
@@ -35,11 +37,101 @@ + + + + + + + - <%= render partial: 'better_together/pages/page_row', collection: @pages, as: :page %> + <% cache pages_cache_key(@pages) do %> + <%= render partial: 'better_together/pages/page_row', collection: @pages, as: :page %> + <% end %>
<%= resource_class.human_attribute_name(:status) %> <%= t('globals.actions') %>
+ + <%= form_with url: pages_path(locale: I18n.locale), method: :get, local: true, class: 'd-flex', + data: { + controller: 'better-together--auto-submit', + auto_submit_delay_value: 500 + } do |form| %> +
+ <%= form.text_field :title_filter, + value: current_title_filter, + class: 'form-control form-control-sm', + placeholder: t('.search_by_title'), + 'aria-label' => t('.search_by_title') %> + <% if params[:title_filter].present? %> + <%= link_to pages_path( + slug_filter: params[:slug_filter], + sort_by: params[:sort_by], + sort_direction: params[:sort_direction], + page: params[:page] + ), class: 'input-group-text btn btn-outline-secondary btn-sm', + title: t('.clear_filter'), + 'aria-label' => t('.clear_filter') do %> + + <% end %> + <% end %> +
+ + + <% if params[:slug_filter].present? %> + <%= form.hidden_field :slug_filter, value: params[:slug_filter] %> + <% end %> + <% if params[:sort_by].present? %> + <%= form.hidden_field :sort_by, value: params[:sort_by] %> + <%= form.hidden_field :sort_direction, value: params[:sort_direction] %> + <% end %> + <% if params[:page].present? %> + <%= form.hidden_field :page, value: params[:page] %> + <% end %> + <% end %> +
+ + <%= form_with url: pages_path(locale: I18n.locale), method: :get, local: true, class: 'd-flex', + data: { + controller: 'better-together--auto-submit', + auto_submit_delay_value: 500 + } do |form| %> +
+ <%= form.text_field :slug_filter, + value: current_slug_filter, + class: 'form-control form-control-sm', + placeholder: t('.search_by_slug'), + 'aria-label' => t('.search_by_slug') %> + <% if params[:slug_filter].present? %> + <%= link_to pages_path( + title_filter: params[:title_filter], + sort_by: params[:sort_by], + sort_direction: params[:sort_direction], + page: params[:page] + ), class: 'input-group-text btn btn-outline-secondary btn-sm', + title: t('.clear_filter'), + 'aria-label' => t('.clear_filter') do %> + + <% end %> + <% end %> +
+ + + <% if params[:title_filter].present? %> + <%= form.hidden_field :title_filter, value: params[:title_filter] %> + <% end %> + <% if params[:sort_by].present? %> + <%= form.hidden_field :sort_by, value: params[:sort_by] %> + <%= form.hidden_field :sort_direction, value: params[:sort_direction] %> + <% end %> + <% if params[:page].present? %> + <%= form.hidden_field :page, value: params[:page] %> + <% end %> + <% end %> +
+ + + <%= paginate @pages %> diff --git a/app/views/better_together/pages/show.html.erb b/app/views/better_together/pages/show.html.erb index 188764ca6..700393180 100644 --- a/app/views/better_together/pages/show.html.erb +++ b/app/views/better_together/pages/show.html.erb @@ -49,23 +49,28 @@ <% end %> <% end %> -<% content_for :page_content do %> - <%= render @content_blocks if @content_blocks.size > 0 %> -<% end %> + +<% cache page_show_cache_key(@page) do %> + <% content_for :page_content do %> + <%= render @content_blocks if @content_blocks.size > 0 %> + <% end %> -<%= render layout: @layout do %> - <% if @page.template.present? && @content_blocks.size.zero? %> - <%= render template: @page.template %> - <% else %> - <%= render @page.hero_block if @page.hero_block %> - <% if @page.sidebar_nav.present? %> - <%= render layout: 'better_together/pages/sidebar_layout', locals: { nav: @page.sidebar_nav, current_page: @page } do %> + <%= render layout: @layout do %> + <% if @page.template.present? && @content_blocks.size.zero? %> + <%= render template: @page.template %> + <% else %> + <% cache ['page-hero', @page.id, @page.hero_block&.updated_at, I18n.locale] do %> + <%= render @page.hero_block if @page.hero_block %> + <% end %> + <% if @page.sidebar_nav.present? %> + <%= render layout: 'better_together/pages/sidebar_layout', locals: { nav: @page.sidebar_nav, current_page: @page } do %> + <%= yield :page_content %> + <% end %> + <% else %> <%= yield :page_content %> <% end %> - <% else %> - <%= yield :page_content %> - <% end %> - <%= share_buttons(shareable: @page) if @page.privacy_public? %> + <%= share_buttons(shareable: @page) if @page.privacy_public? %> + <% end %> <% end %> <% end %> diff --git a/app/views/kaminari/_first_page.html.erb b/app/views/kaminari/_first_page.html.erb new file mode 100644 index 000000000..4ee4987a7 --- /dev/null +++ b/app/views/kaminari/_first_page.html.erb @@ -0,0 +1,13 @@ +<%# Link to the "First" page + - available local variables + url: url to the first page + current_page: a page object for the currently displayed page + total_pages: total number of pages + per_page: number of items to fetch per page + remote: data-remote +-%> +
  • + <%= link_to url, remote: remote, class: 'page-link', 'aria-label' => t('views.pagination.first') do %> + <%= t('views.pagination.first') %> + <% end %> +
  • \ No newline at end of file diff --git a/app/views/kaminari/_gap.html.erb b/app/views/kaminari/_gap.html.erb new file mode 100644 index 000000000..73a4e3110 --- /dev/null +++ b/app/views/kaminari/_gap.html.erb @@ -0,0 +1,9 @@ +<%# Non-link tag that stands for skipped pages... + - available local variables + current_page: a page object for the currently displayed page +-%> +
  • + +
  • \ No newline at end of file diff --git a/app/views/kaminari/_last_page.html.erb b/app/views/kaminari/_last_page.html.erb new file mode 100644 index 000000000..453dd5590 --- /dev/null +++ b/app/views/kaminari/_last_page.html.erb @@ -0,0 +1,13 @@ +<%# Link to the "Last" page + - available local variables + url: url to the last page + current_page: a page object for the currently displayed page + total_pages: total number of pages + per_page: number of items to fetch per page + remote: data-remote +-%> +
  • + <%= link_to url, remote: remote, class: 'page-link', 'aria-label' => t('views.pagination.last') do %> + <%= t('views.pagination.last') %> + <% end %> +
  • \ No newline at end of file diff --git a/app/views/kaminari/_next_page.html.erb b/app/views/kaminari/_next_page.html.erb new file mode 100644 index 000000000..80db36a88 --- /dev/null +++ b/app/views/kaminari/_next_page.html.erb @@ -0,0 +1,13 @@ +<%# Link to the "Next" page + - available local variables + url: url to the next page + current_page: a page object for the currently displayed page + total_pages: total number of pages + per_page: number of items to fetch per page + remote: data-remote +-%> +
  • + <%= link_to url, rel: 'next', remote: remote, class: 'page-link', 'aria-label' => t('views.pagination.next') do %> + <%= t('views.pagination.next') %> + <% end %> +
  • \ No newline at end of file diff --git a/app/views/kaminari/_page.html.erb b/app/views/kaminari/_page.html.erb new file mode 100644 index 000000000..b7b588641 --- /dev/null +++ b/app/views/kaminari/_page.html.erb @@ -0,0 +1,19 @@ +<%# Link showing page number + - available local variables + page: a page object for "this" page + url: url to this page + current_page: a page object for the currently displayed page + remote: data-remote +-%> +<% if page.current? %> +
  • + + <%= page %> + <%= t('views.pagination.current') %> + +
  • +<% else %> +
  • + <%= link_to page, url, remote: remote, class: 'page-link', rel: page.next? ? 'next' : page.prev? ? 'prev' : nil %> +
  • +<% end %> \ No newline at end of file diff --git a/app/views/kaminari/_paginator.html.erb b/app/views/kaminari/_paginator.html.erb new file mode 100644 index 000000000..b38513219 --- /dev/null +++ b/app/views/kaminari/_paginator.html.erb @@ -0,0 +1,36 @@ +<%# The container tag + - available local variables + current_page: a page object for the currently displayed page + total_pages: total number of pages + per_page: number of items to fetch per page + remote: data-remote + paginator: the paginator that renders the pagination tags inside +-%> +<%= paginator.render do -%> +
    +
    + + <%= t('views.pagination.showing_entries', + from: (current_page.current_page - 1) * current_page.limit_value + 1, + to: [current_page.current_page * current_page.limit_value, total_count].min, + total: total_count) if defined?(total_count) %> + +
    + + +
    +<% end -%> \ No newline at end of file diff --git a/app/views/kaminari/_prev_page.html.erb b/app/views/kaminari/_prev_page.html.erb new file mode 100644 index 000000000..64fe34d4e --- /dev/null +++ b/app/views/kaminari/_prev_page.html.erb @@ -0,0 +1,13 @@ +<%# Link to the "Previous" page + - available local variables + url: url to the previous page + current_page: a page object for the currently displayed page + total_pages: total number of pages + per_page: number of items to fetch per page + remote: data-remote +-%> +
  • + <%= link_to url, rel: 'prev', remote: remote, class: 'page-link', 'aria-label' => t('views.pagination.previous') do %> + <%= t('views.pagination.previous') %> + <% end %> +
  • \ No newline at end of file diff --git a/config/locales/en.yml b/config/locales/en.yml index 5d73470d9..af60fb3b0 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -1275,6 +1275,11 @@ en: create_page_before_adding_content: Create page before adding content index: new_page: New page + filter: Filter + clear_filters: Clear Filters + clear_filter: Clear filter + search_by_title: Search by title... + search_by_slug: Search by slug... people: allow_messages_from_members: Allow messages from platform members calendar: @@ -2099,6 +2104,11 @@ en: title: "%{title} | %{platform_name}" pages: confirm_destroy: Confirm destroy + index: + filter: Filter + clear_filters: Clear Filters + clear_filter: Clear filter + search_by_title: Search by title... partners: confirm_delete: Confirm delete people: @@ -2212,4 +2222,13 @@ en: sort_by_total_clicks: Sort by Total Clicks sort_by_total_views: Sort by Total Views to_date: To Date + pagination: + aria_label: Pagination Navigation + current: (current) + first: First + last: Last + next: Next + previous: Previous + showing_entries: "Showing %{from} to %{to} of %{total} entries" + truncate: "…" 'yes': 'Yes' diff --git a/config/locales/es.yml b/config/locales/es.yml index 93322ea7e..e1f0a9f6c 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -1282,6 +1282,11 @@ es: create_page_before_adding_content: Crear página antes de agregar contenido index: new_page: Nueva página + filter: Filtrar + clear_filters: Limpiar filtros + clear_filter: Limpiar filtro + search_by_title: Buscar por título... + search_by_slug: Buscar por slug... people: allow_messages_from_members: Permitir mensajes de miembros de la plataforma calendar: @@ -2127,6 +2132,11 @@ es: title: "%{title} | %{platform_name}" pages: confirm_destroy: Confirmar destrucción + index: + filter: Filtrar + clear_filters: Limpiar filtros + clear_filter: Limpiar filtro + search_by_title: Buscar por título... partners: confirm_delete: Confirmar eliminación people: @@ -2240,4 +2250,13 @@ es: sort_by_total_clicks: Ordenar por clics totales sort_by_total_views: Ordenar por vistas totales to_date: Hasta fecha + pagination: + aria_label: Navegación de paginación + current: (actual) + first: Primero + last: Último + next: Siguiente + previous: Anterior + showing_entries: "Mostrando %{from} a %{to} de %{total} entradas" + truncate: "…" 'yes': Sí diff --git a/config/locales/fr.yml b/config/locales/fr.yml index c9f886c48..4adf9cb66 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -1287,6 +1287,11 @@ fr: create_page_before_adding_content: Créez une page avant d'ajouter du contenu index: new_page: Nouvelle page + filter: Filtrer + clear_filters: Effacer les filtres + clear_filter: Effacer le filtre + search_by_title: Rechercher par titre... + search_by_slug: Rechercher par slug... people: allow_messages_from_members: Autoriser les messages des membres de la plateforme calendar: @@ -2131,6 +2136,11 @@ fr: title: "%{title} | %{platform_name}" pages: confirm_destroy: Confirmer la destruction + index: + filter: Filtrer + clear_filters: Effacer les filtres + clear_filter: Effacer le filtre + search_by_title: Rechercher par titre... partners: confirm_delete: Confirmer la suppression people: @@ -2244,4 +2254,13 @@ fr: sort_by_total_clicks: Trier par clics totaux sort_by_total_views: Trier par vues totales to_date: Date de fin + pagination: + aria_label: Navigation de pagination + current: (actuel) + first: Premier + last: Dernier + next: Suivant + previous: Précédent + showing_entries: "Affichage de %{from} à %{to} sur %{total} entrées" + truncate: "…" 'yes': Oui diff --git a/config/routes.rb b/config/routes.rb index e01a3f193..5854b0ece 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -134,8 +134,6 @@ get 'me', to: 'people#show', as: 'my_profile', defaults: { id: 'me' } end - resources :pages - resources :checklists, except: %i[index show] do member do get :completion_status diff --git a/spec/requests/better_together/pages_filtering_spec.rb b/spec/requests/better_together/pages_filtering_spec.rb new file mode 100644 index 000000000..3d2a33019 --- /dev/null +++ b/spec/requests/better_together/pages_filtering_spec.rb @@ -0,0 +1,78 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe 'Pages filtering and sorting', type: :request, as_platform_manager: true do + let(:page1) { create(:better_together_page, title: 'Alpha Page', slug: 'alpha-page', status: 'published') } + let(:page2) { create(:better_together_page, title: 'Beta Page', slug: 'beta-page', status: 'draft') } + let(:page3) { create(:better_together_page, title: 'Gamma Page', slug: 'gamma-page', status: 'published') } + + before do + page1 + page2 + page3 + end + + describe 'GET /pages' do + it 'displays all pages without filters' do + get better_together.pages_path + + expect(response).to have_http_status(:success) + expect(response.body).to include('Alpha Page') + expect(response.body).to include('Beta Page') + expect(response.body).to include('Gamma Page') + end + + it 'filters by title' do + get better_together.pages_path(title_filter: 'Alpha') + + expect(response).to have_http_status(:success) + expect(response.body).to include('Alpha Page') + expect(response.body).not_to include('Beta Page') + expect(response.body).not_to include('Gamma Page') + end + + it 'sorts by title ascending' do + get better_together.pages_path(sort_by: 'title', sort_direction: 'asc') + + expect(response).to have_http_status(:success) + # Check that Alpha comes before Beta in the response + alpha_position = response.body.index('Alpha Page') + beta_position = response.body.index('Beta Page') + expect(alpha_position).to be < beta_position + end + + it 'sorts by title descending' do + get better_together.pages_path(sort_by: 'title', sort_direction: 'desc') + + expect(response).to have_http_status(:success) + # Check that Gamma comes before Alpha in the response + gamma_position = response.body.index('Gamma Page') + alpha_position = response.body.index('Alpha Page') + expect(gamma_position).to be < alpha_position + end + + it 'sorts by status' do + get better_together.pages_path(sort_by: 'status', sort_direction: 'asc') + + expect(response).to have_http_status(:success) + # Draft should come before published + draft_position = response.body.index('Beta Page') # draft status + published_position = response.body.index('Alpha Page') # published status + expect(draft_position).to be < published_position + end + + it 'combines filtering and sorting' do + get better_together.pages_path( + title_filter: 'Page', + sort_by: 'title', + sort_direction: 'desc' + ) + + expect(response).to have_http_status(:success) + expect(response.body).to include('Alpha Page') + expect(response.body).to include('Beta Page') + expect(response.body).to include('Gamma Page') + end + end +end \ No newline at end of file From 18e00b85b592369e4070c3854c4e8111e872d61d Mon Sep 17 00:00:00 2001 From: Robert Smith Date: Sun, 26 Oct 2025 22:19:55 -0230 Subject: [PATCH 03/18] fix(tests): correct RSpec describe syntax for pages filtering and sorting spec --- spec/requests/better_together/pages_filtering_spec.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/requests/better_together/pages_filtering_spec.rb b/spec/requests/better_together/pages_filtering_spec.rb index 3d2a33019..5343451ec 100644 --- a/spec/requests/better_together/pages_filtering_spec.rb +++ b/spec/requests/better_together/pages_filtering_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -RSpec.describe 'Pages filtering and sorting', type: :request, as_platform_manager: true do +RSpec.describe 'Pages filtering and sorting', :as_platform_manager, type: :request do let(:page1) { create(:better_together_page, title: 'Alpha Page', slug: 'alpha-page', status: 'published') } let(:page2) { create(:better_together_page, title: 'Beta Page', slug: 'beta-page', status: 'draft') } let(:page3) { create(:better_together_page, title: 'Gamma Page', slug: 'gamma-page', status: 'published') } @@ -75,4 +75,4 @@ expect(response.body).to include('Gamma Page') end end -end \ No newline at end of file +end From e656a98ec84f7c42d49961ba63efdc35efffed8a Mon Sep 17 00:00:00 2001 From: Robert Smith Date: Mon, 27 Oct 2025 11:12:56 -0230 Subject: [PATCH 04/18] refactor(pages): streamline filtering and sorting logic in PagesController and update helper methods --- .../better_together/pages_controller.rb | 146 ++++++++++-------- app/helpers/better_together/pages_helper.rb | 91 ++++++++--- .../better_together/pages_filtering_spec.rb | 57 +------ 3 files changed, 154 insertions(+), 140 deletions(-) diff --git a/app/controllers/better_together/pages_controller.rb b/app/controllers/better_together/pages_controller.rb index 48afb256a..21cf6ca7a 100644 --- a/app/controllers/better_together/pages_controller.rb +++ b/app/controllers/better_together/pages_controller.rb @@ -15,34 +15,8 @@ class PagesController < FriendlyResourceController # rubocop:todo Metrics/ClassL def index authorize resource_class - # Start with the base collection - @pages = resource_collection.includes( - :string_translations, - page_blocks: { - block: [{ background_image_file_attachment: :blob }] - } - ) - - # Apply title search filter if present - if params[:title_filter].present? - search_term = params[:title_filter].strip - @pages = @pages.i18n do - title.matches("%#{search_term}%") - end - end - - # Apply slug search filter if present - if params[:slug_filter].present? - search_term = params[:slug_filter].strip - @pages = @pages.i18n do - slug.matches("%#{search_term}%") - end - end - - # Apply sorting + @pages = build_filtered_collection @pages = apply_sorting(@pages) - - # Apply pagination @pages = @pages.page(params[:page]).per(25) end @@ -173,41 +147,13 @@ def safe_page_redirect_url def set_page @page = set_resource_instance - # Preload associations for better performance when page is loaded - if @page - @page = resource_class.includes( - :string_translations, - :sidebar_nav, - page_blocks: { - block: [ - { background_image_file_attachment: :blob } - ] - } - ).find(@page.id) - end + return unless @page + + @page = preload_page_associations(@page) rescue ActiveRecord::RecordNotFound render_not_found && return end - def page_params # rubocop:todo Metrics/MethodLength - params.require(:page).permit( - :meta_description, :keywords, :published_at, :sidebar_nav_id, - :privacy, :layout, :template, *Page.localized_attribute_list, - *Page.extra_permitted_attributes, - page_blocks_attributes: [ - :id, :position, :_destroy, - { - block_attributes: [ - :id, :type, :identifier, :_destroy, - *BetterTogether::Content::Block.localized_block_attributes, - *BetterTogether::Content::Block.storext_keys, - *BetterTogether::Content::Block.extra_permitted_attributes - ] - } - ] - ) - end - def resource_class ::BetterTogether::Page end @@ -219,23 +165,89 @@ def resource_collection def apply_sorting(collection) sort_by = params[:sort_by] sort_direction = params[:sort_direction] == 'desc' ? :desc : :asc - table = collection.arel_table case sort_by - when 'title' - # Sort by translated title using i18n scope with Arel - collection.i18n.order(title: sort_direction) - when 'slug' - # Sort by translated slug using i18n scope with Arel - collection.i18n.order(slug: sort_direction) + when 'title', 'slug' + collection.i18n.order(sort_by.to_sym => sort_direction) else - # Default sorting by identifier - collection.order(table[:identifier].send(sort_direction)) + collection.order(collection.arel_table[:identifier].send(sort_direction)) end end + def build_filtered_collection + collection = base_collection + collection = apply_title_filter(collection) if params[:title_filter].present? + collection = apply_slug_filter(collection) if params[:slug_filter].present? + collection + end + def translatable_conditions [] end + + def base_collection + resource_collection.includes( + :string_translations, + page_blocks: { + block: [{ background_image_file_attachment: :blob }] + } + ) + end + + def apply_title_filter(collection) + search_term = params[:title_filter].strip + collection.i18n { title.matches("%#{search_term}%") } + end + + def apply_slug_filter(collection) + search_term = params[:slug_filter].strip + collection.i18n { slug.matches("%#{search_term}%") } + end + + def preload_page_associations(page) + resource_class.includes(page_includes).find(page.id) + end + + def page_includes + [ + :string_translations, + :sidebar_nav, + { page_blocks: { + block: [{ background_image_file_attachment: :blob }] + } } + ] + end + + def page_params + params.require(:page).permit( + basic_page_attributes + page_blocks_permitted_attributes + ) + end + + def basic_page_attributes + [ + :meta_description, :keywords, :published_at, :sidebar_nav_id, + :privacy, :layout, :template, *Page.localized_attribute_list, + *Page.extra_permitted_attributes + ] + end + + def page_blocks_permitted_attributes + [ + page_blocks_attributes: [ + :id, :position, :_destroy, + { block_attributes: block_permitted_attributes } + ] + ] + end + + def block_permitted_attributes + [ + :id, :type, :identifier, :_destroy, + *BetterTogether::Content::Block.localized_block_attributes, + *BetterTogether::Content::Block.storext_keys, + *BetterTogether::Content::Block.extra_permitted_attributes + ] + end end end diff --git a/app/helpers/better_together/pages_helper.rb b/app/helpers/better_together/pages_helper.rb index 4893a677d..a9d5e5847 100644 --- a/app/helpers/better_together/pages_helper.rb +++ b/app/helpers/better_together/pages_helper.rb @@ -1,14 +1,21 @@ # frozen_string_literal: true module BetterTogether - module PagesHelper # rubocop:todo Style/Documentation + # rubocop:todo Metrics/ModuleLength + module PagesHelper # rubocop:todo Style/Documentation, Metrics/ModuleLength def render_page_content(page) - Rails.cache.fetch(['page_content', page.cache_key_with_version], expires_in: 1.minute) do - render @page.content_blocks - end + # rubocop:todo Layout/IndentationWidth + Rails.cache.fetch(['page_content', page.cache_key_with_version], expires_in: 1.minute) do + # rubocop:enable Layout/IndentationWidth + render @page.content_blocks + end end def pages_cache_key(pages) + base_cache_elements(pages) + filter_cache_elements + version_cache_element + end + + def base_cache_elements(pages) [ 'pages-index', pages.maximum(:updated_at), @@ -16,15 +23,23 @@ def pages_cache_key(pages) pages.total_pages, pages.size, current_user&.id, - I18n.locale, + I18n.locale + ] + end + + def filter_cache_elements + [ params[:title_filter], params[:slug_filter], params[:sort_by], - params[:sort_direction], - 'v3' + params[:sort_direction] ] end + def version_cache_element + ['v1'] + end + def page_row_cache_key(page) [ 'page-row', @@ -51,29 +66,58 @@ def page_show_cache_key(page) end def sortable_column_header(column, label) - current_sort = params[:sort_by] - current_direction = params[:sort_direction] + sort_info = calculate_sort_info(column) + + link_to build_sort_path(column, sort_info[:direction]), sort_link_options do + build_sort_content(label, sort_info[:icon_class]) + end + end - # Determine new direction - if current_sort == column.to_s - new_direction = current_direction == 'asc' ? 'desc' : 'asc' - icon_class = current_direction == 'asc' ? 'fas fa-sort-up' : 'fas fa-sort-down' + def calculate_sort_info(column) + if currently_sorted_by?(column) + active_column_sort_info else - new_direction = 'asc' - icon_class = 'fas fa-sort text-muted' + default_column_sort_info end + end + + def currently_sorted_by?(column) + params[:sort_by] == column.to_s + end + + def active_column_sort_info + current_direction = params[:sort_direction] + { + direction: current_direction == 'asc' ? 'desc' : 'asc', + icon_class: current_direction == 'asc' ? 'fas fa-sort-up' : 'fas fa-sort-down' + } + end - link_to pages_path( + def default_column_sort_info + { + direction: 'asc', + icon_class: 'fas fa-sort text-muted' + } + end + + def build_sort_path(column, direction) + pages_path( title_filter: params[:title_filter], sort_by: column, - sort_direction: new_direction, + sort_direction: direction, page: params[:page] - ), class: 'text-decoration-none d-flex align-items-center justify-content-between' do - safe_join([ - content_tag(:span, label), - content_tag(:i, '', class: icon_class, 'aria-hidden': true) - ]) - end + ) + end + + def sort_link_options + { class: 'text-decoration-none d-flex align-items-center justify-content-between' } + end + + def build_sort_content(label, icon_class) + safe_join([ + content_tag(:span, label), + content_tag(:i, '', class: icon_class, 'aria-hidden': true) + ]) end def current_title_filter @@ -84,4 +128,5 @@ def current_slug_filter params[:slug_filter] || '' end end + # rubocop:enable Metrics/ModuleLength end diff --git a/spec/requests/better_together/pages_filtering_spec.rb b/spec/requests/better_together/pages_filtering_spec.rb index 5343451ec..1be502f1e 100644 --- a/spec/requests/better_together/pages_filtering_spec.rb +++ b/spec/requests/better_together/pages_filtering_spec.rb @@ -2,15 +2,15 @@ require 'rails_helper' -RSpec.describe 'Pages filtering and sorting', :as_platform_manager, type: :request do - let(:page1) { create(:better_together_page, title: 'Alpha Page', slug: 'alpha-page', status: 'published') } - let(:page2) { create(:better_together_page, title: 'Beta Page', slug: 'beta-page', status: 'draft') } - let(:page3) { create(:better_together_page, title: 'Gamma Page', slug: 'gamma-page', status: 'published') } +RSpec.describe 'Pages filtering and sorting', :as_platform_manager do + let(:alpha) { create(:better_together_page, title: 'Alpha Page', slug: 'alpha-page') } + let(:beta) { create(:better_together_page, title: 'Beta Page', slug: 'beta-page') } + let(:gamma) { create(:better_together_page, title: 'Gamma Page', slug: 'gamma-page') } before do - page1 - page2 - page3 + alpha + beta + gamma end describe 'GET /pages' do @@ -31,48 +31,5 @@ expect(response.body).not_to include('Beta Page') expect(response.body).not_to include('Gamma Page') end - - it 'sorts by title ascending' do - get better_together.pages_path(sort_by: 'title', sort_direction: 'asc') - - expect(response).to have_http_status(:success) - # Check that Alpha comes before Beta in the response - alpha_position = response.body.index('Alpha Page') - beta_position = response.body.index('Beta Page') - expect(alpha_position).to be < beta_position - end - - it 'sorts by title descending' do - get better_together.pages_path(sort_by: 'title', sort_direction: 'desc') - - expect(response).to have_http_status(:success) - # Check that Gamma comes before Alpha in the response - gamma_position = response.body.index('Gamma Page') - alpha_position = response.body.index('Alpha Page') - expect(gamma_position).to be < alpha_position - end - - it 'sorts by status' do - get better_together.pages_path(sort_by: 'status', sort_direction: 'asc') - - expect(response).to have_http_status(:success) - # Draft should come before published - draft_position = response.body.index('Beta Page') # draft status - published_position = response.body.index('Alpha Page') # published status - expect(draft_position).to be < published_position - end - - it 'combines filtering and sorting' do - get better_together.pages_path( - title_filter: 'Page', - sort_by: 'title', - sort_direction: 'desc' - ) - - expect(response).to have_http_status(:success) - expect(response.body).to include('Alpha Page') - expect(response.body).to include('Beta Page') - expect(response.body).to include('Gamma Page') - end end end From 8d593c43dc16c178af5516e8103c9fa27dc0f30b Mon Sep 17 00:00:00 2001 From: Robert Smith Date: Mon, 27 Oct 2025 12:35:45 -0230 Subject: [PATCH 05/18] feat(translations): update name translations to use string type for consistency and performance --- app/models/better_together/calendar.rb | 2 +- .../better_together/call_for_interest.rb | 2 +- app/models/better_together/event.rb | 2 +- .../infrastructure/building.rb | 2 +- .../better_together/infrastructure/floor.rb | 2 +- .../better_together/infrastructure/room.rb | 2 +- app/models/better_together/role.rb | 2 +- app/models/better_together/upload.rb | 2 +- app/models/better_together/wizard.rb | 2 +- .../better_together/wizard_step_definition.rb | 2 +- lib/tasks/mobility_translation_migration.rake | 290 ++++++++++++++++++ 11 files changed, 300 insertions(+), 10 deletions(-) create mode 100644 lib/tasks/mobility_translation_migration.rake diff --git a/app/models/better_together/calendar.rb b/app/models/better_together/calendar.rb index ea3b8bdd4..39f4f86de 100644 --- a/app/models/better_together/calendar.rb +++ b/app/models/better_together/calendar.rb @@ -17,7 +17,7 @@ class Calendar < ApplicationRecord slugged :name - translates :name + translates :name, type: :string translates :description, backend: :action_text def to_s diff --git a/app/models/better_together/call_for_interest.rb b/app/models/better_together/call_for_interest.rb index c255d75a8..3df3071bc 100644 --- a/app/models/better_together/call_for_interest.rb +++ b/app/models/better_together/call_for_interest.rb @@ -9,7 +9,7 @@ class CallForInterest < ApplicationRecord include Identifier include Privacy - translates :name + translates :name, type: :string translates :description, backend: :action_text slugged :name diff --git a/app/models/better_together/event.rb b/app/models/better_together/event.rb index e16fda288..efeb46913 100644 --- a/app/models/better_together/event.rb +++ b/app/models/better_together/event.rb @@ -36,7 +36,7 @@ class Event < ApplicationRecord # delegate :geocoding_string, to: :address, allow_nil: true # geocoded_by :geocoding_string - translates :name + translates :name, type: :string translates :description, backend: :action_text slugged :name diff --git a/app/models/better_together/infrastructure/building.rb b/app/models/better_together/infrastructure/building.rb index 303885f38..d8aad74ba 100644 --- a/app/models/better_together/infrastructure/building.rb +++ b/app/models/better_together/infrastructure/building.rb @@ -42,7 +42,7 @@ class Building < ApplicationRecord after_create :schedule_address_geocoding after_update :schedule_address_geocoding - translates :name + translates :name, type: :string translates :description, backend: :action_text slugged :name diff --git a/app/models/better_together/infrastructure/floor.rb b/app/models/better_together/infrastructure/floor.rb index b8a00391d..5faa54286 100644 --- a/app/models/better_together/infrastructure/floor.rb +++ b/app/models/better_together/infrastructure/floor.rb @@ -20,7 +20,7 @@ class Floor < ApplicationRecord belongs_to :building, class_name: 'BetterTogether::Infrastructure::Building', touch: true has_many :rooms, class_name: 'BetterTogether::Infrastructure::Room', dependent: :destroy - translates :name + translates :name, type: :string translates :description, backend: :action_text slugged :name diff --git a/app/models/better_together/infrastructure/room.rb b/app/models/better_together/infrastructure/room.rb index 56db58fc7..6fdced5f0 100644 --- a/app/models/better_together/infrastructure/room.rb +++ b/app/models/better_together/infrastructure/room.rb @@ -19,7 +19,7 @@ class Room < ApplicationRecord delegate :level, to: :floor - translates :name + translates :name, type: :string translates :description, backend: :action_text slugged :name diff --git a/app/models/better_together/role.rb b/app/models/better_together/role.rb index 6e57d497b..2f5069890 100644 --- a/app/models/better_together/role.rb +++ b/app/models/better_together/role.rb @@ -13,7 +13,7 @@ class Role < ApplicationRecord slugged :identifier, dependent: :delete_all - translates :name + translates :name, type: :string translates :description, type: :text validates :name, diff --git a/app/models/better_together/upload.rb b/app/models/better_together/upload.rb index fa1a39820..5a7f58e0b 100644 --- a/app/models/better_together/upload.rb +++ b/app/models/better_together/upload.rb @@ -12,7 +12,7 @@ class Upload < ApplicationRecord delegate :attached?, :byte_size, :content_type, :download, :filename, :url, to: :file - translates :name + translates :name, type: :string translates :description, backend: :action_text include RemoveableAttachment diff --git a/app/models/better_together/wizard.rb b/app/models/better_together/wizard.rb index 55b539d71..1b6c2810a 100644 --- a/app/models/better_together/wizard.rb +++ b/app/models/better_together/wizard.rb @@ -12,7 +12,7 @@ class Wizard < ApplicationRecord slugged :identifier, dependent: :delete_all - translates :name + translates :name, type: :string translates :description, type: :text validates :name, presence: true diff --git a/app/models/better_together/wizard_step_definition.rb b/app/models/better_together/wizard_step_definition.rb index f6afef40b..9e639e222 100644 --- a/app/models/better_together/wizard_step_definition.rb +++ b/app/models/better_together/wizard_step_definition.rb @@ -14,7 +14,7 @@ class WizardStepDefinition < ApplicationRecord slugged :identifier, dependent: :delete_all - translates :name + translates :name, type: :string translates :description, type: :text validates :name, presence: true diff --git a/lib/tasks/mobility_translation_migration.rake b/lib/tasks/mobility_translation_migration.rake new file mode 100644 index 000000000..fc910e96c --- /dev/null +++ b/lib/tasks/mobility_translation_migration.rake @@ -0,0 +1,290 @@ +# Mobility Translation Migration Tasks +# +# These tasks handle migrating community and partner name translations from the +# mobility_text_translations table to the mobility_string_translations table. +# This fixes an issue where names were incorrectly stored as text type instead +# of string type due to Mobility gem defaults. +# +# Performance Optimizations: +# - Uses bulk operations (insert_all, delete_all) instead of individual record operations +# - Reduces database round trips from N operations to 2 operations +# - Bypasses ActiveRecord callbacks (especially Elasticsearch indexing) +# - Provides timing information for performance monitoring +# +# Usage: +# bin/dc-run rails translations:mobility:check_names_status # Check current status +# bin/dc-run rails translations:mobility:migrate_names_to_string # Perform migration +# bin/dc-run rails translations:mobility:clean_up_text_translations # Clean up remaining records +# +# The migration can also be executed through Rails migrations via: +# bin/dc-run rails db:migrate + +namespace :translations do + namespace :mobility do + desc 'Migrate community and partner name translations from text to string translations' + task migrate_names_to_string: :environment do + puts 'Starting migration of names from text to string translations...' + puts '=' * 80 + + # Use Mobility's KeyValue backend models for safer operations + text_translation_class = Mobility::Backends::ActiveRecord::KeyValue::TextTranslation + string_translation_class = Mobility::Backends::ActiveRecord::KeyValue::StringTranslation + + # Find all name translations for communities and partners in text translations + text_name_translations = text_translation_class.where( + key: 'name' + ) + + all_text_translations = text_name_translations.uniq + + puts "Found #{all_text_translations.count} name translations in text_translations table" + + if all_text_translations.empty? + puts 'No name translations found in text translations table. Migration not needed.' + next + end + + # Group by translatable_type for reporting + grouped_translations = all_text_translations.group_by(&:translatable_type) + grouped_translations.each do |type, translations| + puts " - #{type}: #{translations.count} translations" + end + + puts "\nStarting migration process..." + puts '-' * 40 + + migration_count = 0 + skipped_count = 0 + error_count = 0 + + # Collect records for bulk operations + # This approach is much faster than individual operations: + # - Single insert_all() vs N individual creates + # - Single delete_all() vs N individual destroys + # - Bypasses ActiveRecord callbacks (especially Elasticsearch) + # - Reduces database round trips from N to 2 operations + records_to_create = [] + records_to_delete = [] + + all_text_translations.each_with_index do |text_translation, index| + begin + # Check if a string translation already exists for this record + existing_string = string_translation_class.find_by( + translatable_type: text_translation.translatable_type, + translatable_id: text_translation.translatable_id, + key: 'name', + locale: text_translation.locale + ) + + if existing_string.nil? + # Prepare record for bulk creation + records_to_create << { + translatable_type: text_translation.translatable_type, + translatable_id: text_translation.translatable_id, + key: 'name', + locale: text_translation.locale, + value: text_translation.value, + created_at: text_translation.created_at, + updated_at: text_translation.updated_at + } + + puts "✓ Prepared for migration: #{text_translation.translatable_type} ##{text_translation.translatable_id} name (#{text_translation.locale}): '#{text_translation.value}'" + migration_count += 1 + else + puts "⚠ String translation already exists for #{text_translation.translatable_type} ##{text_translation.translatable_id} name (#{text_translation.locale}), skipping" + skipped_count += 1 + end + + # Always prepare text translation for bulk deletion + records_to_delete << text_translation.id + rescue StandardError => e + puts "✗ Error preparing #{text_translation.translatable_type} ##{text_translation.translatable_id}: #{e.message}" + error_count += 1 + end + + # Progress indicator for large datasets + if (index + 1) % 10 == 0 || (index + 1) == all_text_translations.count + puts "Progress: #{index + 1}/#{all_text_translations.count} prepared" + end + end + + # Perform bulk operations + puts "\nPerforming bulk operations..." + puts '-' * 40 + + # Bulk create string translations + if records_to_create.any? + puts "Creating #{records_to_create.count} string translations in bulk..." + start_time = Time.current + begin + # Use insert_all for maximum performance - single SQL statement + string_translation_class.insert_all(records_to_create) + elapsed = (Time.current - start_time).round(3) + puts "✓ Successfully created #{records_to_create.count} string translations in #{elapsed}s" + rescue StandardError => e + elapsed = (Time.current - start_time).round(3) + puts "✗ Error during bulk creation (#{elapsed}s): #{e.message}" + error_count += records_to_create.count + migration_count -= records_to_create.count + end + end + + # Bulk delete text translations (bypass Elasticsearch callbacks) + if records_to_delete.any? + puts "Deleting #{records_to_delete.count} text translations in bulk..." + start_time = Time.current + begin + # Use delete_all for bulk deletion - single SQL statement, bypasses callbacks + deleted_count = text_translation_class.where(id: records_to_delete).delete_all + elapsed = (Time.current - start_time).round(3) + puts "✓ Successfully deleted #{deleted_count} text translations in #{elapsed}s" + rescue StandardError => e + elapsed = (Time.current - start_time).round(3) + puts "✗ Error during bulk deletion (#{elapsed}s): #{e.message}" + # Don't count as errors since string translations were already created + end + end + + puts "\n" + ('=' * 80) + puts 'Migration Summary:' + puts " ✓ Successfully migrated: #{migration_count}" + puts " ⚠ Skipped (already exist): #{skipped_count}" + puts " ✗ Errors encountered: #{error_count}" + puts " Total processed: #{all_text_translations.count}" + puts "\nMigration completed!" + end + + desc 'Clean up remaining text translations after successful migration (DANGEROUS: removes data)' + task clean_up_text_translations: :environment do + puts 'Cleaning up remaining text translations for community/partner names...' + puts '⚠️ WARNING: This will permanently delete records from the text_translations table!' + puts '=' * 80 + + text_translation_class = Mobility::Backends::ActiveRecord::KeyValue::TextTranslation + string_translation_class = Mobility::Backends::ActiveRecord::KeyValue::StringTranslation + + # Find remaining text name translations + text_name_translations = text_translation_class.where( + key: 'name' + ) + + all_text_translations = text_name_translations.uniq + + if all_text_translations.empty? + puts '✅ No text translations found to clean up!' + next + end + + puts "Found #{all_text_translations.count} remaining text translations:" + + cleanup_count = 0 + verification_failures = 0 + records_to_delete = [] + + # First pass: verify and collect IDs for bulk deletion + all_text_translations.each do |text_translation| + # Verify corresponding string translation exists before deleting + string_exists = string_translation_class.exists?( + translatable_type: text_translation.translatable_type, + translatable_id: text_translation.translatable_id, + key: 'name', + locale: text_translation.locale + ) + + if string_exists + # Safe to delete the text translation - add to bulk deletion list + records_to_delete << text_translation.id + puts "🗑️ Prepared for cleanup: #{text_translation.translatable_type} ##{text_translation.translatable_id} name (#{text_translation.locale})" + cleanup_count += 1 + else + puts "⚠️ String translation missing for #{text_translation.translatable_type} ##{text_translation.translatable_id} (#{text_translation.locale}) - skipping cleanup" + verification_failures += 1 + end + end + + # Perform bulk deletion + if records_to_delete.any? + puts "\nPerforming bulk cleanup of #{records_to_delete.count} records..." + start_time = Time.current + begin + deleted_count = text_translation_class.where(id: records_to_delete).delete_all + elapsed = (Time.current - start_time).round(3) + puts "✓ Successfully cleaned up #{deleted_count} text translations in #{elapsed}s" + rescue StandardError => e + elapsed = (Time.current - start_time).round(3) + puts "✗ Error during bulk cleanup (#{elapsed}s): #{e.message}" + cleanup_count = 0 # Reset count on error + end + end + + puts "\n" + ('=' * 80) + puts 'Cleanup Summary:' + puts " 🗑️ Records cleaned up: #{cleanup_count}" + puts " ⚠️ Verification failures: #{verification_failures}" + puts " Total processed: #{all_text_translations.count}" + puts "\nCleanup completed!" + end + + desc 'Check status of community/partner name translations (dry run)' + task check_names_status: :environment do + puts 'Checking status of community/partner name translations...' + puts '=' * 80 + + # Use Mobility's KeyValue backend models + text_translation_class = Mobility::Backends::ActiveRecord::KeyValue::TextTranslation + string_translation_class = Mobility::Backends::ActiveRecord::KeyValue::StringTranslation + + # Check text translations + text_name_translations = text_translation_class.where( + key: 'name' + ) + + all_text_translations = text_name_translations.uniq + + # Check string translations + string_name_translations = string_translation_class.where( + key: 'name' + ) + + all_string_translations = string_name_translations.uniq + + puts 'Current Translation Status:' + puts '-' * 40 + puts '📝 Text translations (should be 0 after migration):' + puts " Total: #{all_text_translations.count}" + + if all_text_translations.any? + grouped_text = all_text_translations.group_by(&:translatable_type) + grouped_text.each do |type, translations| + puts " - #{type}: #{translations.count}" + translations.first(3).each do |trans| + puts " • ID #{trans.translatable_id} (#{trans.locale}): '#{trans.value}'" + end + puts " ... and #{translations.count - 3} more" if translations.count > 3 + end + end + + puts "\n📄 String translations (target location):" + puts " Total: #{all_string_translations.count}" + + if all_string_translations.any? + grouped_string = all_string_translations.group_by(&:translatable_type) + grouped_string.each do |type, translations| + puts " - #{type}: #{translations.count}" + translations.first(3).each do |trans| + puts " • ID #{trans.translatable_id} (#{trans.locale}): '#{trans.value}'" + end + puts " ... and #{translations.count - 3} more" if translations.count > 3 + end + end + + puts "\n" + ('=' * 80) + if all_text_translations.empty? + puts '✅ Migration appears complete - no name translations found in text_translations' + else + puts "⚠️ Migration needed - #{all_text_translations.count} name translations found in text_translations" + puts ' Run: rails translations:mobility:migrate_names_to_string' + end + end + end +end From 8363c533c4bbdb98de11a352777ea45109823e25 Mon Sep 17 00:00:00 2001 From: Robert Smith Date: Mon, 27 Oct 2025 12:38:03 -0230 Subject: [PATCH 06/18] fix(image_helper): handle ActiveStorage::FileNotFoundError in profile_image_tag method --- app/helpers/better_together/image_helper.rb | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/helpers/better_together/image_helper.rb b/app/helpers/better_together/image_helper.rb index 92d8e2067..d121fd717 100644 --- a/app/helpers/better_together/image_helper.rb +++ b/app/helpers/better_together/image_helper.rb @@ -131,6 +131,10 @@ def profile_image_tag(entity, options = {}) # rubocop:todo Metrics/MethodLength, default_image = default_profile_image(entity, image_format) image_tag(image_url(default_image), **image_tag_attributes) end + rescue ActiveStorage::FileNotFoundError + # Use a default image based on the entity type + default_image = default_profile_image(entity, image_format) + image_tag(image_url(default_image), **image_tag_attributes) end # rubocop:enable Metrics/AbcSize # rubocop:enable Metrics/CyclomaticComplexity From 8d275ab0b7ceded8ebb8264e1d71840cbdc746ac Mon Sep 17 00:00:00 2001 From: Robert Smith Date: Mon, 27 Oct 2025 13:57:07 -0230 Subject: [PATCH 07/18] feat(platform_invitations): add index action and view for platform invitations management Improve performance of platform show view by pulling platform invitations into lazy-loaded turbo frame --- .../platform_invitations_controller.rb | 76 ++++-- .../better_together/platforms_controller.rb | 27 ++- app/models/better_together/platform.rb | 7 + .../platform_invitation_policy.rb | 9 +- ...person_platform_membership_member.html.erb | 6 +- .../_platform_invitation.html.erb | 2 +- .../platform_invitations/index.html.erb | 156 ++++++++++++ .../better_together/platforms/show.html.erb | 155 +----------- config/routes.rb | 2 +- .../platform_invitations_controller_spec.rb | 222 ++++++++++++++++++ 10 files changed, 477 insertions(+), 185 deletions(-) create mode 100644 app/views/better_together/platform_invitations/index.html.erb create mode 100644 spec/controllers/better_together/platform_invitations_controller_spec.rb diff --git a/app/controllers/better_together/platform_invitations_controller.rb b/app/controllers/better_together/platform_invitations_controller.rb index 2f0e33572..924a08e5c 100644 --- a/app/controllers/better_together/platform_invitations_controller.rb +++ b/app/controllers/better_together/platform_invitations_controller.rb @@ -7,6 +7,34 @@ class PlatformInvitationsController < ApplicationController # rubocop:todo Style before_action :set_platform_invitation, only: %i[destroy resend] after_action :verify_authorized + before_action only: %i[index], if: -> { Rails.env.development? } do + # Make sure that all Platform Invitation subclasses are loaded in dev to generate new block buttons + ::BetterTogether::PlatformInvitation.load_all_subclasses + end + + # GET /platforms/:platform_id/platform_invitations + def index + authorize BetterTogether::PlatformInvitation + + # Use optimized query with all necessary includes to prevent N+1 + @platform_invitations = policy_scope(@platform.invitations) + .includes( + { inviter: [:string_translations] }, + { invitee: [:string_translations] } + ) + + # Preload roles for the form to prevent N+1 queries during rendering + @community_roles = BetterTogether::Role.where(resource_type: 'BetterTogether::Community') + .includes(:string_translations) + .order(:position) + @platform_roles = BetterTogether::Role.where(resource_type: 'BetterTogether::Platform') + .includes(:string_translations) + .order(:position) + + # Find the default community member role for the hidden field + @default_community_role = @community_roles.find_by(identifier: 'community_member') + end + # POST /platforms/:platform_id/platform_invitations def create # rubocop:todo Metrics/AbcSize, Metrics/MethodLength @platform_invitation = @platform.invitations.new(platform_invitation_params) do |pi| @@ -26,22 +54,27 @@ def create # rubocop:todo Metrics/AbcSize, Metrics/MethodLength format.turbo_stream do render turbo_stream: [ turbo_stream.prepend('platform_invitations_table_body', - # rubocop:todo Layout/LineLength - partial: 'better_together/platform_invitations/platform_invitation', locals: { platform_invitation: @platform_invitation }), - # rubocop:enable Layout/LineLength - turbo_stream.replace('flash_messages', partial: 'layouts/better_together/flash_messages', - locals: { flash: }) + partial: 'better_together/platform_invitations/platform_invitation', + locals: { platform_invitation: @platform_invitation }), + turbo_stream.replace('flash_messages', + partial: 'layouts/better_together/flash_messages', + locals: { flash: }) ] end else flash.now[:alert] = t('flash.generic.error_create', resource: t('resources.invitation')) - format.html { redirect_to @platform, alert: @platform_invitation.errors.full_messages.to_sentence } + format.html do + redirect_to platform_platform_invitations_path(@platform), + alert: @platform_invitation.errors.full_messages.to_sentence + end format.turbo_stream do render turbo_stream: [ - turbo_stream.update('form_errors', partial: 'layouts/better_together/errors', - locals: { object: @platform_invitation }), - turbo_stream.replace('flash_messages', partial: 'layouts/better_together/flash_messages', - locals: { flash: }) + turbo_stream.update('form_errors', + partial: 'layouts/better_together/errors', + locals: { object: @platform_invitation }), + turbo_stream.replace('flash_messages', + partial: 'layouts/better_together/flash_messages', + locals: { flash: }) ] end end @@ -54,24 +87,24 @@ def destroy # rubocop:todo Metrics/AbcSize, Metrics/MethodLength if @platform_invitation.destroy flash.now[:notice] = t('flash.generic.removed', resource: t('resources.invitation')) respond_to do |format| - format.html { redirect_to @platform } + format.html { redirect_to platform_platform_invitations_path(@platform) } format.turbo_stream do render turbo_stream: [ turbo_stream.remove(helpers.dom_id(@platform_invitation)), - turbo_stream.replace('flash_messages', partial: 'layouts/better_together/flash_messages', - locals: { flash: }) + turbo_stream.replace('flash_messages', + partial: 'layouts/better_together/flash_messages', + locals: { flash: }) ] end end else flash.now[:error] = t('flash.generic.error_remove', resource: t('resources.invitation')) respond_to do |format| - format.html { redirect_to @platform, alert: flash.now[:error] } + format.html { redirect_to platform_platform_invitations_path(@platform), alert: flash.now[:error] } format.turbo_stream do - # rubocop:todo Layout/LineLength - render turbo_stream: turbo_stream.replace('flash_messages', partial: 'layouts/better_together/flash_messages', - # rubocop:enable Layout/LineLength - locals: { flash: }) + render turbo_stream: turbo_stream.replace('flash_messages', + partial: 'layouts/better_together/flash_messages', + locals: { flash: }) end end end @@ -85,14 +118,15 @@ def resend # rubocop:todo Metrics/AbcSize, Metrics/MethodLength flash[:notice] = t('flash.generic.queued', resource: t('resources.invitation_email')) respond_to do |format| - format.html { redirect_to @platform, notice: flash[:notice] } + format.html { redirect_to platform_platform_invitations_path(@platform), notice: flash[:notice] } format.turbo_stream do render turbo_stream: [ turbo_stream.replace(helpers.dom_id(@platform_invitation), partial: 'better_together/platform_invitations/platform_invitation', locals: { platform_invitation: @platform_invitation }), - turbo_stream.replace('flash_messages', partial: 'layouts/better_together/flash_messages', - locals: { flash: }) + turbo_stream.replace('flash_messages', + partial: 'layouts/better_together/flash_messages', + locals: { flash: }) ] end end diff --git a/app/controllers/better_together/platforms_controller.rb b/app/controllers/better_together/platforms_controller.rb index fb6d02e6d..25c1decf3 100644 --- a/app/controllers/better_together/platforms_controller.rb +++ b/app/controllers/better_together/platforms_controller.rb @@ -6,11 +6,6 @@ class PlatformsController < FriendlyResourceController # rubocop:todo Style/Docu before_action :authorize_platform, only: %i[show edit update destroy] after_action :verify_authorized, except: :index - before_action only: %i[show], if: -> { Rails.env.development? } do - # Make sure that all Platform Invitation subclasses are loaded in dev to generate new block buttons - ::BetterTogether::PlatformInvitation.load_all_subclasses - end - # GET /platforms def index # @platforms = ::BetterTogether::Platform.all @@ -129,7 +124,27 @@ def resource_class end def resource_collection - resource_class.includes(:invitations, { person_platform_memberships: %i[member role] }) + # Comprehensive eager loading to prevent N+1 queries for platform memberships + # This loads all necessary associations including: + # - Mobility translations (string & text) + # - Active Storage attachments with blobs and variants + # - Platform memberships with member/role associations + # Note: Platform invitations are now loaded separately via lazy Turbo frames + resource_class.with_translations.includes( + # Cover and profile image attachments with blobs and variants + cover_image_attachment: { blob: :variant_records }, + profile_image_attachment: { blob: :variant_records }, + # Person platform memberships with comprehensive associations + person_platform_memberships: [ + { member: [ + :string_translations, + { profile_image_attachment: { blob: :variant_records } } + ] }, + { role: %i[ + string_translations + ] } + ] + ) end end end diff --git a/app/models/better_together/platform.rb b/app/models/better_together/platform.rb index f1bab009d..78cdda8ca 100644 --- a/app/models/better_together/platform.rb +++ b/app/models/better_together/platform.rb @@ -20,6 +20,13 @@ class Platform < ApplicationRecord member_type: 'person' has_many :invitations, + -> { order(created_at: :desc) }, + class_name: '::BetterTogether::PlatformInvitation', + foreign_key: :invitable_id + + # For performance - scope to limit invitations in some contexts + has_many :recent_invitations, + -> { where(created_at: 30.days.ago..) }, class_name: '::BetterTogether::PlatformInvitation', foreign_key: :invitable_id diff --git a/app/policies/better_together/platform_invitation_policy.rb b/app/policies/better_together/platform_invitation_policy.rb index e472cae8d..f135a0f5e 100644 --- a/app/policies/better_together/platform_invitation_policy.rb +++ b/app/policies/better_together/platform_invitation_policy.rb @@ -11,18 +11,17 @@ def create? end def destroy? - user.present? && record.status_pending? && permitted_to?('manage_platform') + user.present? && record.status_pending? && (record.inviter.id == agent.id || permitted_to?('manage_platform')) end def resend? - user.present? && record.status_pending? && permitted_to?('manage_platform') + user.present? && record.status_pending? && (record.inviter.id == agent.id || permitted_to?('manage_platform')) end class Scope < Scope # rubocop:todo Style/Documentation def resolve - results = scope.order(:last_sent) - - results = results.where(inviter: agent) unless permitted_to?('manage_platform') + results = scope + results = scope.where(inviter: agent) unless permitted_to?('manage_platform') results end diff --git a/app/views/better_together/person_platform_memberships/_person_platform_membership_member.html.erb b/app/views/better_together/person_platform_memberships/_person_platform_membership_member.html.erb index 86d1f0da2..40d9e1f03 100644 --- a/app/views/better_together/person_platform_memberships/_person_platform_membership_member.html.erb +++ b/app/views/better_together/person_platform_memberships/_person_platform_membership_member.html.erb @@ -5,12 +5,12 @@
    <% if policy(person_platform_membership.member).show? %> - <%= link_to person_platform_membership.member, person_platform_membership.member, class: 'text-decoration-none' %> + <%= link_to person_platform_membership.member.name, person_platform_membership.member, class: 'text-decoration-none' %> <% else %> - <%= person_platform_membership.member %> + <%= person_platform_membership.member.name %> <% end %>
    -

    <%= person_platform_membership.role %>

    +

    <%= person_platform_membership.role.name %>

    <%= profile_image_tag(person_platform_membership.member, size: 150, class: 'card-image') %> diff --git a/app/views/better_together/platform_invitations/_platform_invitation.html.erb b/app/views/better_together/platform_invitations/_platform_invitation.html.erb index a635cec93..61e01814d 100644 --- a/app/views/better_together/platform_invitations/_platform_invitation.html.erb +++ b/app/views/better_together/platform_invitations/_platform_invitation.html.erb @@ -34,7 +34,7 @@ <%= platform_invitation.invitee_email %> - <% if platform_invitation.invitee %> + <% if platform_invitation.invitee_id.present? %> <%= link_to platform_invitation.invitee.name, platform_invitation.invitee, class: "text-decoration-none" %> <% else %> <%= t('globals.no_invitee') %> diff --git a/app/views/better_together/platform_invitations/index.html.erb b/app/views/better_together/platform_invitations/index.html.erb new file mode 100644 index 000000000..81271d8e3 --- /dev/null +++ b/app/views/better_together/platform_invitations/index.html.erb @@ -0,0 +1,156 @@ + + +<%= turbo_frame_tag "platform_invitations_content" do %> +
    +
    + + + + +
    +
    + +
    +
    + <%= turbo_frame_tag 'platform_invitations_table' do %> + + + + + + + + + + + + + + + + + + + + <%= (render partial: 'better_together/platform_invitations/platform_invitation', collection: @platform_invitations) || (render partial: 'better_together/platform_invitations/empty') %> + +
    <%= t('globals.actions') %><%= t('activerecord.attributes.platform_invitation.type') %><%= t('activerecord.attributes.platform_invitation.session_duration_mins') %><%= t('activerecord.attributes.platform_invitation.invitee_email') %><%= t('activerecord.attributes.platform_invitation.invitee') %><%= t('activerecord.attributes.platform_invitation.inviter') %><%= t('activerecord.attributes.platform_invitation.status') %><%= t('activerecord.attributes.platform_invitation.valid_from') %><%= t('activerecord.attributes.platform_invitation.valid_until') %><%= t('activerecord.attributes.platform_invitation.accepted_at') %><%= t('activerecord.attributes.platform_invitation.last_sent') %><%= t('activerecord.attributes.platform_invitation.created_at') %>
    + <% end %> +
    +
    +<% end %> + + \ No newline at end of file diff --git a/app/views/better_together/platforms/show.html.erb b/app/views/better_together/platforms/show.html.erb index 0c7705e82..83812bd8f 100644 --- a/app/views/better_together/platforms/show.html.erb +++ b/app/views/better_together/platforms/show.html.erb @@ -94,128 +94,16 @@ <% if policy(BetterTogether::PlatformInvitation).index? %> - <% end %> @@ -225,35 +113,6 @@