From 8cb0a7a020098565b665e62b61494d65a1014c92 Mon Sep 17 00:00:00 2001 From: Pete Matsyburka Date: Sat, 12 Sep 2026 10:35:04 +0300 Subject: [PATCH] add infinite scroll --- app/controllers/application_controller.rb | 6 ++- .../submissions_archived_controller.rb | 12 ++++++ .../submissions_dashboard_controller.rb | 12 ++++++ .../template_folders_controller.rb | 28 +++++++++++-- .../templates_archived_controller.rb | 14 ++++++- ...mplates_archived_submissions_controller.rb | 12 ++++++ app/controllers/templates_controller.rb | 10 +++++ .../templates_dashboard_controller.rb | 16 ++++++++ .../templates_shared_controller.rb | 13 +++++- app/javascript/application.js | 2 + app/javascript/elements/infinite_scroll.js | 41 +++++++++++++++++++ app/views/shared/_infinite_scroll.html.erb | 5 +++ app/views/shared/_pagination.html.erb | 8 +++- app/views/submissions_archived/index.html.erb | 1 + .../submissions_dashboard/index.html.erb | 1 + app/views/template_folders/show.html.erb | 2 + app/views/templates/show.html.erb | 1 + app/views/templates_archived/index.html.erb | 1 + .../index.html.erb | 1 + app/views/templates_dashboard/index.html.erb | 2 + app/views/templates_shared/index.html.erb | 1 + 21 files changed, 179 insertions(+), 10 deletions(-) create mode 100644 app/javascript/elements/infinite_scroll.js create mode 100644 app/views/shared/_infinite_scroll.html.erb diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 58992a4e5..3e74b38fe 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -13,7 +13,7 @@ class ApplicationController < ActionController::Base before_action :maybe_redirect_to_setup, unless: :signed_in? before_action :authenticate_user!, unless: :devise_controller? - before_action :set_csp, if: -> { request.get? && !request.headers['HTTP_X_TURBO'] } + before_action :set_csp, if: -> { request.get? && !request.headers['HTTP_X_TURBO'] && !turbo_infinite_scroll? } helper_method :button_title, :current_account, @@ -65,6 +65,10 @@ class ApplicationController < ActionController::Base private + def turbo_infinite_scroll? + request.headers['HTTP_X_TURBO_INFINITE_SCROLL'].present? + end + def turbo_native_ios? request.user_agent.to_s.include?('Hotwire Native iOS') end diff --git a/app/controllers/submissions_archived_controller.rb b/app/controllers/submissions_archived_controller.rb index 46fa8278b..d3f1ddfda 100644 --- a/app/controllers/submissions_archived_controller.rb +++ b/app/controllers/submissions_archived_controller.rb @@ -29,5 +29,17 @@ class SubmissionsArchivedController < ApplicationController ActiveRecord::Associations::Preloader.new(records: @submissions.filter_map(&:template), associations: :author).call + + render_infinite_scroll(@pagy, @submissions) if turbo_infinite_scroll? + end + + private + + def render_infinite_scroll(pagy, submissions) + render turbo_stream: [ + turbo_stream.before('infinite_scroll', partial: 'templates/submission', collection: submissions, + locals: { with_template: true, archived: true }), + turbo_stream.replace('infinite_scroll', partial: 'shared/infinite_scroll', locals: { pagy: }) + ] end end diff --git a/app/controllers/submissions_dashboard_controller.rb b/app/controllers/submissions_dashboard_controller.rb index dea9a6f3a..91f6f04b5 100644 --- a/app/controllers/submissions_dashboard_controller.rb +++ b/app/controllers/submissions_dashboard_controller.rb @@ -30,5 +30,17 @@ class SubmissionsDashboardController < ApplicationController ActiveRecord::Associations::Preloader.new(records: @submissions.filter_map(&:template), associations: :author).call + + render_infinite_scroll(@pagy, @submissions) if turbo_infinite_scroll? + end + + private + + def render_infinite_scroll(pagy, submissions) + render turbo_stream: [ + turbo_stream.before('infinite_scroll', partial: 'templates/submission', collection: submissions, + locals: { with_template: true }), + turbo_stream.replace('infinite_scroll', partial: 'shared/infinite_scroll', locals: { pagy: }) + ] end end diff --git a/app/controllers/template_folders_controller.rb b/app/controllers/template_folders_controller.rb index a315b1674..3a305e3b0 100644 --- a/app/controllers/template_folders_controller.rb +++ b/app/controllers/template_folders_controller.rb @@ -13,10 +13,7 @@ class TemplateFoldersController < ApplicationController .where(folder: [@template_folder, *(params[:q].present? ? @template_folder.subfolders : [])]) .preload(:author, :template_accesses) - @template_folders = - @template_folder.subfolders.where(id: Template.accessible_by(current_ability).active.select(:folder_id)) - - @template_folders = TemplateFolders.search(@template_folders, params[:q]) + @template_folders = filter_folders(@template_folder) @template_folders = TemplateFolders.sort(@template_folders, current_user, selected_order) if @templates.exists? @@ -40,6 +37,8 @@ class TemplateFoldersController < ApplicationController @templates = @templates.none end + + render_infinite_scroll(@pagy, @templates, @template_folders) if turbo_infinite_scroll? end def edit; end @@ -55,6 +54,27 @@ class TemplateFoldersController < ApplicationController private + def filter_folders(template_folder) + template_folders = + template_folder.subfolders.where(id: Template.accessible_by(current_ability).active.select(:folder_id)) + + TemplateFolders.search(template_folders, params[:q]) + end + + def render_infinite_scroll(pagy, templates, template_folders) + rendering = + if templates.present? + { partial: 'templates/template', collection: templates } + else + { partial: 'template_folders/folder', collection: template_folders, as: :folder } + end + + render turbo_stream: [ + turbo_stream.before('infinite_scroll', **rendering), + turbo_stream.replace('infinite_scroll', partial: 'shared/infinite_scroll', locals: { pagy: }) + ] + end + def selected_order @selected_order ||= if can?(:manage, :countless) diff --git a/app/controllers/templates_archived_controller.rb b/app/controllers/templates_archived_controller.rb index 84d40074b..7dd078b1e 100644 --- a/app/controllers/templates_archived_controller.rb +++ b/app/controllers/templates_archived_controller.rb @@ -12,13 +12,23 @@ class TemplatesArchivedController < ApplicationController @pagy, @templates = pagy_auto(@templates.select_for_list, limit: 12) - return unless params[:q].present? && @templates.blank? + if params[:q].present? && @templates.blank? + @related_submissions_pagy, @related_submissions = load_related_submissions + end - @related_submissions_pagy, @related_submissions = load_related_submissions + render_infinite_scroll(@pagy, @templates) if turbo_infinite_scroll? end private + def render_infinite_scroll(pagy, templates) + render turbo_stream: [ + turbo_stream.before('infinite_scroll', partial: 'templates/template', collection: templates, + locals: { with_folder: true }), + turbo_stream.replace('infinite_scroll', partial: 'shared/infinite_scroll', locals: { pagy: }) + ] + end + def load_related_submissions related_submissions = Submission.accessible_by(current_ability) diff --git a/app/controllers/templates_archived_submissions_controller.rb b/app/controllers/templates_archived_submissions_controller.rb index 662590d08..10096268a 100644 --- a/app/controllers/templates_archived_submissions_controller.rb +++ b/app/controllers/templates_archived_submissions_controller.rb @@ -16,7 +16,19 @@ class TemplatesArchivedSubmissionsController < ApplicationController end @pagy, @submissions = pagy_auto(@submissions.select_for_list.preload(submitters: :start_form_submission_events)) + + render_infinite_scroll(@pagy, @submissions, @template) if turbo_infinite_scroll? rescue ActiveRecord::RecordNotFound redirect_to root_path end + + private + + def render_infinite_scroll(pagy, submissions, template) + render turbo_stream: [ + turbo_stream.before('infinite_scroll', partial: 'templates/submission', collection: submissions, + locals: { template:, archived: true }), + turbo_stream.replace('infinite_scroll', partial: 'shared/infinite_scroll', locals: { pagy: }) + ] + end end diff --git a/app/controllers/templates_controller.rb b/app/controllers/templates_controller.rb index c45fe6cdc..e8ff9ff4e 100644 --- a/app/controllers/templates_controller.rb +++ b/app/controllers/templates_controller.rb @@ -21,6 +21,8 @@ class TemplatesController < ApplicationController @pagy, @submissions = pagy_auto(submissions.select_for_list.preload(:template_accesses, submitters: :start_form_submission_events)) + + render_infinite_scroll(@pagy, @submissions, @template) if turbo_infinite_scroll? rescue ActiveRecord::RecordNotFound redirect_to root_path end @@ -86,6 +88,14 @@ class TemplatesController < ApplicationController private + def render_infinite_scroll(pagy, submissions, template) + render turbo_stream: [ + turbo_stream.before('infinite_scroll', partial: 'templates/submission', collection: submissions, + locals: { template:, archived: template.archived_at? }), + turbo_stream.replace('infinite_scroll', partial: 'shared/infinite_scroll', locals: { pagy: }) + ] + end + def template_params params.require(:template).permit( :name, diff --git a/app/controllers/templates_dashboard_controller.rb b/app/controllers/templates_dashboard_controller.rb index d9a683c95..b655caaca 100644 --- a/app/controllers/templates_dashboard_controller.rb +++ b/app/controllers/templates_dashboard_controller.rb @@ -43,10 +43,26 @@ class TemplatesDashboardController < ApplicationController @related_submissions_pagy, @related_submissions = load_related_submissions end end + + render_infinite_scroll(@pagy, @templates, @template_folders) if turbo_infinite_scroll? end private + def render_infinite_scroll(pagy, templates, template_folders) + rendering = + if templates.present? + { partial: 'templates/template', collection: templates } + else + { partial: 'template_folders/folder', collection: template_folders, as: :folder } + end + + render turbo_stream: [ + turbo_stream.before('infinite_scroll', **rendering), + turbo_stream.replace('infinite_scroll', partial: 'shared/infinite_scroll', locals: { pagy: }) + ] + end + def load_templates(templates, folders_count, show_shared_inline: false) templates = templates.preload(:author, :template_accesses) diff --git a/app/controllers/templates_shared_controller.rb b/app/controllers/templates_shared_controller.rb index 8855d94de..178f49945 100644 --- a/app/controllers/templates_shared_controller.rb +++ b/app/controllers/templates_shared_controller.rb @@ -19,13 +19,22 @@ class TemplatesSharedController < ApplicationController @pagy, @templates = pagy_auto(@templates.select_for_list, limit: 12) - return unless params[:q].present? && @templates.blank? + if params[:q].present? && @templates.blank? + @related_submissions_pagy, @related_submissions = load_related_submissions(is_archived: @is_archived) + end - @related_submissions_pagy, @related_submissions = load_related_submissions(is_archived: @is_archived) + render_infinite_scroll(@pagy, @templates) if turbo_infinite_scroll? end private + def render_infinite_scroll(pagy, templates) + render turbo_stream: [ + turbo_stream.before('infinite_scroll', partial: 'templates/template', collection: templates), + turbo_stream.replace('infinite_scroll', partial: 'shared/infinite_scroll', locals: { pagy: }) + ] + end + def load_related_submissions(is_archived:) shared_templates = Templates.shared(current_user) shared_templates = is_archived ? shared_templates.archived : shared_templates.active diff --git a/app/javascript/application.js b/app/javascript/application.js index 4e3b73e3f..903220a14 100644 --- a/app/javascript/application.js +++ b/app/javascript/application.js @@ -13,6 +13,7 @@ import NativeAction from './elements/native_action' import NativeSearch from './elements/native_search' import NativeModal from './elements/native_modal' import NativeEvent from './elements/native_event' +import InfiniteScroll from './elements/infinite_scroll' import ModalButton from './elements/modal_button' import FileDropzone from './elements/file_dropzone' import MenuActive from './elements/menu_active' @@ -127,6 +128,7 @@ safeRegisterElement('native-action', NativeAction) safeRegisterElement('native-search', NativeSearch) safeRegisterElement('native-modal', NativeModal) safeRegisterElement('native-event', NativeEvent) +safeRegisterElement('infinite-scroll', InfiniteScroll) safeRegisterElement('modal-button', ModalButton) safeRegisterElement('file-dropzone', FileDropzone) safeRegisterElement('menu-active', MenuActive) diff --git a/app/javascript/elements/infinite_scroll.js b/app/javascript/elements/infinite_scroll.js new file mode 100644 index 000000000..1e0ecee92 --- /dev/null +++ b/app/javascript/elements/infinite_scroll.js @@ -0,0 +1,41 @@ +import { renderStreamMessage } from '@hotwired/turbo' + +export default class extends HTMLElement { + connectedCallback () { + if (!this.dataset.src) { + this.remove() + + return + } + + this.observer = new IntersectionObserver((entries) => { + if (entries.some((entry) => entry.isIntersecting)) { + this.load() + } + }, { rootMargin: '400px' }) + + this.observer.observe(this) + } + + disconnectedCallback () { + this.observer?.disconnect() + } + + load () { + if (this.loading || !this.dataset.src) return + + this.loading = true + + fetch(this.dataset.src, { + headers: { 'X-Turbo-Infinite-Scroll': 'true' } + }).then(async (response) => { + if (!response.ok) return + + delete this.dataset.src + + renderStreamMessage(await response.text()) + }).finally(() => { + this.loading = false + }) + } +} diff --git a/app/views/shared/_infinite_scroll.html.erb b/app/views/shared/_infinite_scroll.html.erb new file mode 100644 index 000000000..09b7fbe7a --- /dev/null +++ b/app/views/shared/_infinite_scroll.html.erb @@ -0,0 +1,5 @@ +<% if pagy.next || pagy.page > 1 %> + + <%= svg_icon('loader', class: 'w-6 h-6 animate-spin') %> + +<% end %> diff --git a/app/views/shared/_pagination.html.erb b/app/views/shared/_pagination.html.erb index 71bcef1cc..0cddaea95 100644 --- a/app/views/shared/_pagination.html.erb +++ b/app/views/shared/_pagination.html.erb @@ -1,4 +1,10 @@ -<% if pagy.pages > 1 %> +<% if turbo_native_app? && local_assigns[:next_page_path].blank? %> + <% if local_assigns[:left_additional_html].present? %> +
+ <%= local_assigns[:left_additional_html] %> +
+ <% end %> +<% elsif pagy.pages > 1 %> <% left_additional_html = local_assigns[:left_additional_html] %>
diff --git a/app/views/submissions_archived/index.html.erb b/app/views/submissions_archived/index.html.erb index 15ed65dde..e7c5a9e67 100644 --- a/app/views/submissions_archived/index.html.erb +++ b/app/views/submissions_archived/index.html.erb @@ -29,6 +29,7 @@ <% if @pagy.count.nil? || @pagy.count > 0 %>
<%= render partial: 'templates/submission', collection: @submissions, locals: { with_template: true, archived: true } %> + <%= render 'shared/infinite_scroll', pagy: @pagy if turbo_native_app? %>
<% elsif params[:q].present? || filter_params.present? %>
diff --git a/app/views/submissions_dashboard/index.html.erb b/app/views/submissions_dashboard/index.html.erb index 9e8cd0401..6be2a73ef 100644 --- a/app/views/submissions_dashboard/index.html.erb +++ b/app/views/submissions_dashboard/index.html.erb @@ -70,6 +70,7 @@ <% if @pagy.count.nil? || @pagy.count > 0 %>
<%= render partial: 'templates/submission', collection: @submissions, locals: { with_template: true } %> + <%= render 'shared/infinite_scroll', pagy: @pagy if turbo_native_app? %>
<% end %> <% if params[:q].blank? && params[:status].blank? && filter_params.blank? && @pagy.count.present? && @pagy.count < 5 %> diff --git a/app/views/template_folders/show.html.erb b/app/views/template_folders/show.html.erb index 35438910c..b84b986f1 100644 --- a/app/views/template_folders/show.html.erb +++ b/app/views/template_folders/show.html.erb @@ -59,11 +59,13 @@ <% if @template_folders.present? %>
<%= render partial: 'template_folders/folder', collection: @template_folders, as: :folder %> + <%= render 'shared/infinite_scroll', pagy: @pagy if turbo_native_app? && @templates.blank? %>
<% end %> <% if @pagy.count.nil? || @pagy.count > 0 %>
<%= render partial: 'templates/template', collection: @templates %> + <%= render 'shared/infinite_scroll', pagy: @pagy if turbo_native_app? && @templates.present? %>
<% templates_order_select_html = capture do %> <% if params[:q].blank? && @pagy.pages > 1 && !can?(:manage, :countless) %> diff --git a/app/views/templates/show.html.erb b/app/views/templates/show.html.erb index ff2f6478b..24cb06b93 100644 --- a/app/views/templates/show.html.erb +++ b/app/views/templates/show.html.erb @@ -89,6 +89,7 @@ <% if @submissions.present? %>
<%= render partial: 'submission', collection: @submissions, locals: { template: @template, archived: @template.archived_at? } %> + <%= render 'shared/infinite_scroll', pagy: @pagy if turbo_native_app? %>
<% else %>
diff --git a/app/views/templates_archived/index.html.erb b/app/views/templates_archived/index.html.erb index 98702cf2f..f02389eaf 100644 --- a/app/views/templates_archived/index.html.erb +++ b/app/views/templates_archived/index.html.erb @@ -20,6 +20,7 @@ <% if @pagy.count.nil? || @pagy.count > 0 %>
<%= render partial: 'templates/template', collection: @templates, locals: { with_folder: true } %> + <%= render 'shared/infinite_scroll', pagy: @pagy if turbo_native_app? %>
<% elsif params[:q].present? %>
diff --git a/app/views/templates_archived_submissions/index.html.erb b/app/views/templates_archived_submissions/index.html.erb index 77529a6b5..271f0bf35 100644 --- a/app/views/templates_archived_submissions/index.html.erb +++ b/app/views/templates_archived_submissions/index.html.erb @@ -35,6 +35,7 @@ <% if @pagy.count.nil? || @pagy.count > 0 %>
<%= render partial: 'templates/submission', collection: @submissions, locals: { template: @template, archived: true } %> + <%= render 'shared/infinite_scroll', pagy: @pagy if turbo_native_app? %>
<% elsif params[:q].present? || filter_params.present? %>
diff --git a/app/views/templates_dashboard/index.html.erb b/app/views/templates_dashboard/index.html.erb index 0cdcc91a9..677acd8db 100644 --- a/app/views/templates_dashboard/index.html.erb +++ b/app/views/templates_dashboard/index.html.erb @@ -64,11 +64,13 @@ <%= render 'template_folders/shared_folder' %> <% end %> <%= render partial: 'template_folders/folder', collection: @template_folders, as: :folder %> + <%= render 'shared/infinite_scroll', pagy: @pagy if turbo_native_app? && @templates.blank? %>
<% end %> <% if @templates.present? %>
<%= render partial: 'templates/template', collection: @templates %> + <%= render 'shared/infinite_scroll', pagy: @pagy if turbo_native_app? %> <% if (show_dropzone && current_user.created_at > 2.weeks.ago && current_user == true_user) || params[:tour] == 'true' %> <% user_config = current_user.user_configs.find_or_initialize_by(key: UserConfig::SHOW_APP_TOUR) %> <% if user_config.new_record? || user_config.value || params[:tour] == 'true' %> diff --git a/app/views/templates_shared/index.html.erb b/app/views/templates_shared/index.html.erb index 26e47d1e6..65b02a7e4 100644 --- a/app/views/templates_shared/index.html.erb +++ b/app/views/templates_shared/index.html.erb @@ -33,6 +33,7 @@ <% if @pagy.count.nil? || @pagy.count > 0 %>
<%= render partial: 'templates/template', collection: @templates %> + <%= render 'shared/infinite_scroll', pagy: @pagy if turbo_native_app? %>
<% elsif params[:q].present? %>