From 950b3df52d4d479519738d4c3dc9612abe919d1a Mon Sep 17 00:00:00 2001 From: Hampton Lintorn-Catlin Date: Wed, 22 Apr 2026 12:16:04 -0500 Subject: [PATCH 1/2] Paginate plans index with Turbo Frame infinite scroll - Add :created_by_user to includes to fix N+1 queries on the plans index - Switch unread counts query from subquery to .map(&:id) on loaded records - Paginate plans at 20 per page using LIMIT/OFFSET - Implement infinite scroll via Turbo Frames with loading: :lazy - Extract plan list items into _plan_page partial - Skip filter UI queries (plan_types, onboarding) for Turbo Frame requests Amp-Thread-ID: https://ampcode.com/threads/T-019db090-8247-7319-b0b6-e4cc4ef4b28c Co-authored-by: Amp --- .../controllers/coplan/plans_controller.rb | 28 +++++++++++----- .../views/coplan/plans/_plan_page.html.erb | 33 +++++++++++++++++++ engine/app/views/coplan/plans/index.html.erb | 26 +-------------- 3 files changed, 53 insertions(+), 34 deletions(-) create mode 100644 engine/app/views/coplan/plans/_plan_page.html.erb diff --git a/engine/app/controllers/coplan/plans_controller.rb b/engine/app/controllers/coplan/plans_controller.rb index c4cc31d..faf9e62 100644 --- a/engine/app/controllers/coplan/plans_controller.rb +++ b/engine/app/controllers/coplan/plans_controller.rb @@ -2,25 +2,35 @@ module CoPlan class PlansController < ApplicationController before_action :set_plan, only: [:show, :edit, :update, :update_status, :toggle_checkbox, :history] + PER_PAGE = 20 + def index - @plans = Plan.includes(:plan_type, :tags) + plans = Plan.includes(:plan_type, :tags, :created_by_user) .where.not(status: "brainstorm") .or(Plan.where(created_by_user: current_user)) .order(updated_at: :desc) - @plans = @plans.where(status: params[:status]) if params[:status].present? - @plans = @plans.where(created_by_user: current_user) if params[:scope] == "mine" - @plans = @plans.where(plan_type_id: params[:plan_type]) if params[:plan_type].present? - @plans = @plans.with_tag(params[:tag]) if params[:tag].present? + plans = plans.where(status: params[:status]) if params[:status].present? + plans = plans.where(created_by_user: current_user) if params[:scope] == "mine" + plans = plans.where(plan_type_id: params[:plan_type]) if params[:plan_type].present? + plans = plans.with_tag(params[:tag]) if params[:tag].present? - @plan_types = PlanType.order(:name) + @page = (params[:page] || 1).to_i + @plans = plans.limit(PER_PAGE + 1).offset((@page - 1) * PER_PAGE) + @has_next_page = @plans.size > PER_PAGE + @plans = @plans.first(PER_PAGE) @plan_unread_counts = current_user.notifications.unread - .where(plan_id: @plans.select(:id)) + .where(plan_id: @plans.map(&:id)) .group(:plan_id) .count - @show_onboarding_banner = CoPlan.configuration.onboarding_banner.present? && - !current_user.created_plans.exists? + if turbo_frame_request? + render partial: "coplan/plans/plan_page", locals: { plans: @plans, plan_unread_counts: @plan_unread_counts, page: @page, has_next_page: @has_next_page }, layout: false + else + @plan_types = PlanType.order(:name) + @show_onboarding_banner = CoPlan.configuration.onboarding_banner.present? && + !current_user.created_plans.exists? + end end def show diff --git a/engine/app/views/coplan/plans/_plan_page.html.erb b/engine/app/views/coplan/plans/_plan_page.html.erb new file mode 100644 index 0000000..a976b6e --- /dev/null +++ b/engine/app/views/coplan/plans/_plan_page.html.erb @@ -0,0 +1,33 @@ +<%= turbo_frame_tag "plans-page-#{page}" do %> + <% plans.each do |plan| %> +
+
+ <%= link_to plan.title, plan_path(plan), class: "plans-list__title", data: { turbo_frame: "_top" } %> + <%= plan.status %> + <% if plan.plan_type %> + <%= plan.plan_type.name %> + <% end %> + <% plan_unread = plan_unread_counts[plan.id] || 0 %> + <% if plan_unread > 0 %> + <%= plan_unread %> + <% end %> +
+ <% if plan.tags.any? %> +
+ <% plan.tags.each do |tag| %> + <%= link_to tag.name, plans_path(params.permit(:scope, :status, :plan_type).merge(tag: tag.name)), class: "badge badge--tag #{'badge--tag-active' if params[:tag] == tag.name}", data: { turbo_frame: "_top" } %> + <% end %> +
+ <% end %> +
+ <%= user_avatar(plan.created_by_user) %> <%= plan.created_by_user.name %> · v<%= plan.current_revision %> · updated <%= time_ago_in_words(plan.updated_at) %> ago +
+
+ <% end %> + + <% if has_next_page %> + <%= turbo_frame_tag "plans-page-#{page + 1}", src: plans_path(params.permit(:scope, :status, :plan_type, :tag).merge(page: page + 1)), loading: :lazy do %> +
Loading more plans…
+ <% end %> + <% end %> +<% end %> diff --git a/engine/app/views/coplan/plans/index.html.erb b/engine/app/views/coplan/plans/index.html.erb index 9e271c8..c022475 100644 --- a/engine/app/views/coplan/plans/index.html.erb +++ b/engine/app/views/coplan/plans/index.html.erb @@ -32,31 +32,7 @@ <% if @plans.any? %>
- <% @plans.each do |plan| %> -
-
- <%= link_to plan.title, plan_path(plan), class: "plans-list__title" %> - <%= plan.status %> - <% if plan.plan_type %> - <%= plan.plan_type.name %> - <% end %> - <% plan_unread = @plan_unread_counts[plan.id] || 0 %> - <% if plan_unread > 0 %> - <%= plan_unread %> - <% end %> -
- <% if plan.tags.any? %> -
- <% plan.tags.each do |tag| %> - <%= link_to tag.name, plans_path(params.permit(:scope, :status, :plan_type).merge(tag: tag.name)), class: "badge badge--tag #{'badge--tag-active' if params[:tag] == tag.name}" %> - <% end %> -
- <% end %> -
- <%= user_avatar(plan.created_by_user) %> <%= plan.created_by_user.name %> · v<%= plan.current_revision %> · updated <%= time_ago_in_words(plan.updated_at) %> ago -
-
- <% end %> + <%= render partial: "coplan/plans/plan_page", locals: { plans: @plans, plan_unread_counts: @plan_unread_counts, page: @page, has_next_page: @has_next_page } %>
<% else %>
From 283dc0d043b942e467ab31e56cdaa25958e4270c Mon Sep 17 00:00:00 2001 From: Hampton Lintorn-Catlin Date: Wed, 22 Apr 2026 15:44:50 -0500 Subject: [PATCH 2/2] Add id as secondary sort key for stable offset pagination Amp-Thread-ID: https://ampcode.com/threads/T-019db090-8247-7319-b0b6-e4cc4ef4b28c Co-authored-by: Amp --- engine/app/controllers/coplan/plans_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/engine/app/controllers/coplan/plans_controller.rb b/engine/app/controllers/coplan/plans_controller.rb index faf9e62..54e02c5 100644 --- a/engine/app/controllers/coplan/plans_controller.rb +++ b/engine/app/controllers/coplan/plans_controller.rb @@ -8,7 +8,7 @@ def index plans = Plan.includes(:plan_type, :tags, :created_by_user) .where.not(status: "brainstorm") .or(Plan.where(created_by_user: current_user)) - .order(updated_at: :desc) + .order(updated_at: :desc, id: :desc) plans = plans.where(status: params[:status]) if params[:status].present? plans = plans.where(created_by_user: current_user) if params[:scope] == "mine" plans = plans.where(plan_type_id: params[:plan_type]) if params[:plan_type].present?