add infinite scroll

This commit is contained in:
Pete Matsyburka committed 2026-09-23 10:59:54 +03:00
1 parent d3cc2e1ca8
commit 8cb0a7a020
21 files changed
+179 -10

No files matched your search

+5 -1
View File
@@ -13,7 +13,7 @@ class ApplicationController < ActionController::Base
before_action :maybe_redirect_to_setup, unless: :signed_in? before_action :maybe_redirect_to_setup, unless: :signed_in?
before_action :authenticate_user!, unless: :devise_controller? 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, helper_method :button_title,
:current_account, :current_account,
@@ -65,6 +65,10 @@ class ApplicationController < ActionController::Base
private private
def turbo_infinite_scroll?
request.headers['HTTP_X_TURBO_INFINITE_SCROLL'].present?
end
def turbo_native_ios? def turbo_native_ios?
request.user_agent.to_s.include?('Hotwire Native iOS') request.user_agent.to_s.include?('Hotwire Native iOS')
end end
@@ -29,5 +29,17 @@ class SubmissionsArchivedController < ApplicationController
ActiveRecord::Associations::Preloader.new(records: @submissions.filter_map(&:template), ActiveRecord::Associations::Preloader.new(records: @submissions.filter_map(&:template),
associations: :author).call 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
end end
@@ -30,5 +30,17 @@ class SubmissionsDashboardController < ApplicationController
ActiveRecord::Associations::Preloader.new(records: @submissions.filter_map(&:template), ActiveRecord::Associations::Preloader.new(records: @submissions.filter_map(&:template),
associations: :author).call 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
end end
+24 -4
View File
@@ -13,10 +13,7 @@ class TemplateFoldersController < ApplicationController
.where(folder: [@template_folder, *(params[:q].present? ? @template_folder.subfolders : [])]) .where(folder: [@template_folder, *(params[:q].present? ? @template_folder.subfolders : [])])
.preload(:author, :template_accesses) .preload(:author, :template_accesses)
@template_folders = @template_folders = filter_folders(@template_folder)
@template_folder.subfolders.where(id: Template.accessible_by(current_ability).active.select(:folder_id))
@template_folders = TemplateFolders.search(@template_folders, params[:q])
@template_folders = TemplateFolders.sort(@template_folders, current_user, selected_order) @template_folders = TemplateFolders.sort(@template_folders, current_user, selected_order)
if @templates.exists? if @templates.exists?
@@ -40,6 +37,8 @@ class TemplateFoldersController < ApplicationController
@templates = @templates.none @templates = @templates.none
end end
render_infinite_scroll(@pagy, @templates, @template_folders) if turbo_infinite_scroll?
end end
def edit; end def edit; end
@@ -55,6 +54,27 @@ class TemplateFoldersController < ApplicationController
private 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 def selected_order
@selected_order ||= @selected_order ||=
if can?(:manage, :countless) if can?(:manage, :countless)
@@ -12,13 +12,23 @@ class TemplatesArchivedController < ApplicationController
@pagy, @templates = pagy_auto(@templates.select_for_list, limit: 12) @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 end
private 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 def load_related_submissions
related_submissions = related_submissions =
Submission.accessible_by(current_ability) Submission.accessible_by(current_ability)
@@ -16,7 +16,19 @@ class TemplatesArchivedSubmissionsController < ApplicationController
end end
@pagy, @submissions = pagy_auto(@submissions.select_for_list.preload(submitters: :start_form_submission_events)) @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 rescue ActiveRecord::RecordNotFound
redirect_to root_path redirect_to root_path
end 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 end
+10
View File
@@ -21,6 +21,8 @@ class TemplatesController < ApplicationController
@pagy, @submissions = @pagy, @submissions =
pagy_auto(submissions.select_for_list.preload(:template_accesses, submitters: :start_form_submission_events)) 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 rescue ActiveRecord::RecordNotFound
redirect_to root_path redirect_to root_path
end end
@@ -86,6 +88,14 @@ class TemplatesController < ApplicationController
private 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 def template_params
params.require(:template).permit( params.require(:template).permit(
:name, :name,
@@ -43,10 +43,26 @@ class TemplatesDashboardController < ApplicationController
@related_submissions_pagy, @related_submissions = load_related_submissions @related_submissions_pagy, @related_submissions = load_related_submissions
end end
end end
render_infinite_scroll(@pagy, @templates, @template_folders) if turbo_infinite_scroll?
end end
private 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) def load_templates(templates, folders_count, show_shared_inline: false)
templates = templates.preload(:author, :template_accesses) templates = templates.preload(:author, :template_accesses)
+11 -2
View File
@@ -19,13 +19,22 @@ class TemplatesSharedController < ApplicationController
@pagy, @templates = pagy_auto(@templates.select_for_list, limit: 12) @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 end
private 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:) def load_related_submissions(is_archived:)
shared_templates = Templates.shared(current_user) shared_templates = Templates.shared(current_user)
shared_templates = is_archived ? shared_templates.archived : shared_templates.active shared_templates = is_archived ? shared_templates.archived : shared_templates.active
+2
View File
@@ -13,6 +13,7 @@ import NativeAction from './elements/native_action'
import NativeSearch from './elements/native_search' import NativeSearch from './elements/native_search'
import NativeModal from './elements/native_modal' import NativeModal from './elements/native_modal'
import NativeEvent from './elements/native_event' import NativeEvent from './elements/native_event'
import InfiniteScroll from './elements/infinite_scroll'
import ModalButton from './elements/modal_button' import ModalButton from './elements/modal_button'
import FileDropzone from './elements/file_dropzone' import FileDropzone from './elements/file_dropzone'
import MenuActive from './elements/menu_active' import MenuActive from './elements/menu_active'
@@ -127,6 +128,7 @@ safeRegisterElement('native-action', NativeAction)
safeRegisterElement('native-search', NativeSearch) safeRegisterElement('native-search', NativeSearch)
safeRegisterElement('native-modal', NativeModal) safeRegisterElement('native-modal', NativeModal)
safeRegisterElement('native-event', NativeEvent) safeRegisterElement('native-event', NativeEvent)
safeRegisterElement('infinite-scroll', InfiniteScroll)
safeRegisterElement('modal-button', ModalButton) safeRegisterElement('modal-button', ModalButton)
safeRegisterElement('file-dropzone', FileDropzone) safeRegisterElement('file-dropzone', FileDropzone)
safeRegisterElement('menu-active', MenuActive) safeRegisterElement('menu-active', MenuActive)
@@ -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
})
}
}
@@ -0,0 +1,5 @@
<% if pagy.next || pagy.page > 1 %>
<infinite-scroll id="infinite_scroll" class="col-span-full flex justify-center py-2" data-src="<%= url_for(params: request.query_parameters.merge('page' => pagy.next)) if pagy.next %>">
<%= svg_icon('loader', class: 'w-6 h-6 animate-spin') %>
</infinite-scroll>
<% end %>
+7 -1
View File
@@ -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? %>
<div class="mt-2">
<%= local_assigns[:left_additional_html] %>
</div>
<% end %>
<% elsif pagy.pages > 1 %>
<% left_additional_html = local_assigns[:left_additional_html] %> <% left_additional_html = local_assigns[:left_additional_html] %>
<div class="flex flex-col md:flex-row md:items-center <%= left_additional_html.present? ? 'mt-3' : 'mt-6' %> mb-6 md:my-6 justify-center md:justify-between"> <div class="flex flex-col md:flex-row md:items-center <%= left_additional_html.present? ? 'mt-3' : 'mt-6' %> mb-6 md:my-6 justify-center md:justify-between">
<div class="text-sm <%= 'mb-2 md:mb-0' if left_additional_html.present? %>"> <div class="text-sm <%= 'mb-2 md:mb-0' if left_additional_html.present? %>">
@@ -29,6 +29,7 @@
<% if @pagy.count.nil? || @pagy.count > 0 %> <% if @pagy.count.nil? || @pagy.count > 0 %>
<div class="space-y-4"> <div class="space-y-4">
<%= render partial: 'templates/submission', collection: @submissions, locals: { with_template: true, archived: true } %> <%= render partial: 'templates/submission', collection: @submissions, locals: { with_template: true, archived: true } %>
<%= render 'shared/infinite_scroll', pagy: @pagy if turbo_native_app? %>
</div> </div>
<% elsif params[:q].present? || filter_params.present? %> <% elsif params[:q].present? || filter_params.present? %>
<div class="text-center"> <div class="text-center">
@@ -70,6 +70,7 @@
<% if @pagy.count.nil? || @pagy.count > 0 %> <% if @pagy.count.nil? || @pagy.count > 0 %>
<div class="space-y-4"> <div class="space-y-4">
<%= render partial: 'templates/submission', collection: @submissions, locals: { with_template: true } %> <%= render partial: 'templates/submission', collection: @submissions, locals: { with_template: true } %>
<%= render 'shared/infinite_scroll', pagy: @pagy if turbo_native_app? %>
</div> </div>
<% end %> <% end %>
<% if params[:q].blank? && params[:status].blank? && filter_params.blank? && @pagy.count.present? && @pagy.count < 5 %> <% if params[:q].blank? && params[:status].blank? && filter_params.blank? && @pagy.count.present? && @pagy.count < 5 %>
+2
View File
@@ -59,11 +59,13 @@
<% if @template_folders.present? %> <% if @template_folders.present? %>
<div class="grid gap-4 md:grid-cols-3 <%= 'mb-6' if @templates.present? %>"> <div class="grid gap-4 md:grid-cols-3 <%= 'mb-6' if @templates.present? %>">
<%= render partial: 'template_folders/folder', collection: @template_folders, as: :folder %> <%= render partial: 'template_folders/folder', collection: @template_folders, as: :folder %>
<%= render 'shared/infinite_scroll', pagy: @pagy if turbo_native_app? && @templates.blank? %>
</div> </div>
<% end %> <% end %>
<% if @pagy.count.nil? || @pagy.count > 0 %> <% if @pagy.count.nil? || @pagy.count > 0 %>
<div class="grid gap-4 md:grid-cols-3"> <div class="grid gap-4 md:grid-cols-3">
<%= render partial: 'templates/template', collection: @templates %> <%= render partial: 'templates/template', collection: @templates %>
<%= render 'shared/infinite_scroll', pagy: @pagy if turbo_native_app? && @templates.present? %>
</div> </div>
<% templates_order_select_html = capture do %> <% templates_order_select_html = capture do %>
<% if params[:q].blank? && @pagy.pages > 1 && !can?(:manage, :countless) %> <% if params[:q].blank? && @pagy.pages > 1 && !can?(:manage, :countless) %>
+1
View File
@@ -89,6 +89,7 @@
<% if @submissions.present? %> <% if @submissions.present? %>
<div class="space-y-4"> <div class="space-y-4">
<%= render partial: 'submission', collection: @submissions, locals: { template: @template, archived: @template.archived_at? } %> <%= render partial: 'submission', collection: @submissions, locals: { template: @template, archived: @template.archived_at? } %>
<%= render 'shared/infinite_scroll', pagy: @pagy if turbo_native_app? %>
</div> </div>
<% else %> <% else %>
<div class="card bg-base-200"> <div class="card bg-base-200">
@@ -20,6 +20,7 @@
<% if @pagy.count.nil? || @pagy.count > 0 %> <% if @pagy.count.nil? || @pagy.count > 0 %>
<div class="grid gap-4 md:grid-cols-3"> <div class="grid gap-4 md:grid-cols-3">
<%= render partial: 'templates/template', collection: @templates, locals: { with_folder: true } %> <%= render partial: 'templates/template', collection: @templates, locals: { with_folder: true } %>
<%= render 'shared/infinite_scroll', pagy: @pagy if turbo_native_app? %>
</div> </div>
<% elsif params[:q].present? %> <% elsif params[:q].present? %>
<div class="text-center"> <div class="text-center">
@@ -35,6 +35,7 @@
<% if @pagy.count.nil? || @pagy.count > 0 %> <% if @pagy.count.nil? || @pagy.count > 0 %>
<div class="space-y-4"> <div class="space-y-4">
<%= render partial: 'templates/submission', collection: @submissions, locals: { template: @template, archived: true } %> <%= render partial: 'templates/submission', collection: @submissions, locals: { template: @template, archived: true } %>
<%= render 'shared/infinite_scroll', pagy: @pagy if turbo_native_app? %>
</div> </div>
<% elsif params[:q].present? || filter_params.present? %> <% elsif params[:q].present? || filter_params.present? %>
<div class="text-center"> <div class="text-center">
@@ -64,11 +64,13 @@
<%= render 'template_folders/shared_folder' %> <%= render 'template_folders/shared_folder' %>
<% end %> <% end %>
<%= render partial: 'template_folders/folder', collection: @template_folders, as: :folder %> <%= render partial: 'template_folders/folder', collection: @template_folders, as: :folder %>
<%= render 'shared/infinite_scroll', pagy: @pagy if turbo_native_app? && @templates.blank? %>
</div> </div>
<% end %> <% end %>
<% if @templates.present? %> <% if @templates.present? %>
<div class="grid gap-4 md:grid-cols-3"> <div class="grid gap-4 md:grid-cols-3">
<%= render partial: 'templates/template', collection: @templates %> <%= 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' %> <% 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) %> <% 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' %> <% if user_config.new_record? || user_config.value || params[:tour] == 'true' %>
@@ -33,6 +33,7 @@
<% if @pagy.count.nil? || @pagy.count > 0 %> <% if @pagy.count.nil? || @pagy.count > 0 %>
<div class="grid gap-4 md:grid-cols-3"> <div class="grid gap-4 md:grid-cols-3">
<%= render partial: 'templates/template', collection: @templates %> <%= render partial: 'templates/template', collection: @templates %>
<%= render 'shared/infinite_scroll', pagy: @pagy if turbo_native_app? %>
</div> </div>
<% elsif params[:q].present? %> <% elsif params[:q].present? %>
<div class="text-center"> <div class="text-center">