feat(settings): add pagination to imports and exports pages (#598)

* feat(settings): split imports and exports

* feat(security): sanitize pagination params to prevent abuse

* fix(settings): fix syntax in settings nav

* feat(settings): internationalize family_exports and imports UI strings

* fix(settings): fix coderabbit review

* fix(settings): fix coderabbit review

* fix(settings): fix coderabbit review

* Change default per_page value from 20 to 10

Signed-off-by: Juan José Mata <jjmata@jjmata.com>

* Add `/family_export` to navigation

* Consistency with old defaults

* Align `safe_per_page` even if not DRY

---------

Signed-off-by: Julien Orain <julien.orain@gmail.com>
Signed-off-by: Juan José Mata <jjmata@jjmata.com>
Signed-off-by: Juan José Mata <juanjo.mata@gmail.com>
Co-authored-by: JulienOrain <your-github-email@example.com>
Co-authored-by: Juan José Mata <jjmata@jjmata.com>
Co-authored-by: Juan José Mata <juanjo.mata@gmail.com>
This commit is contained in:
Julien Orain
2026-01-20 00:11:22 +01:00
committed by GitHub
parent 3d91e60a8a
commit 777fbdc4ca
51 changed files with 353 additions and 105 deletions

View File

@@ -38,7 +38,7 @@ class AccountsController < ApplicationController
@q = params.fetch(:q, {}).permit(:search, status: [])
entries = @account.entries.where(excluded: false).search(@q).reverse_chronological
@pagy, @entries = pagy(entries, limit: params[:per_page] || "10")
@pagy, @entries = pagy(entries, limit: safe_per_page)
@activity_feed_data = Account::ActivityFeedData.new(@account, @entries)
end

View File

@@ -1,7 +1,7 @@
class ApplicationController < ActionController::Base
include RestoreLayoutPreferences, Onboardable, Localize, AutoSync, Authentication, Invitable,
SelfHostable, StoreLocation, Impersonatable, Breadcrumbable,
FeatureGuardable, Notifiable
FeatureGuardable, Notifiable, SafePagination
include Pundit::Authorization
include Pagy::Backend

View File

@@ -27,7 +27,7 @@ module AccountableResource
@q = params.fetch(:q, {}).permit(:search)
entries = @account.entries.search(@q).reverse_chronological
@pagy, @entries = pagy(entries, limit: params[:per_page] || "10")
@pagy, @entries = pagy(entries, limit: safe_per_page(10))
end
def edit

View File

@@ -0,0 +1,15 @@
# frozen_string_literal: true
module SafePagination
extend ActiveSupport::Concern
private
def safe_per_page(default = 10)
allowed_values = [ 10, 20, 30, 50, 100 ]
per_page = params[:per_page].to_i
return default if per_page <= 0
allowed_values.include?(per_page) ? per_page : allowed_values.min_by { |v| (v - per_page).abs }
end
end

View File

@@ -13,29 +13,33 @@ class FamilyExportsController < ApplicationController
FamilyDataExportJob.perform_later(@export)
respond_to do |format|
format.html { redirect_to imports_path, notice: "Export started. You'll be able to download it shortly." }
format.html { redirect_to family_exports_path, notice: t("family_exports.create.success") }
format.turbo_stream {
stream_redirect_to imports_path, notice: "Export started. You'll be able to download it shortly."
stream_redirect_to family_exports_path, notice: t("family_exports.create.success")
}
end
end
def index
@exports = Current.family.family_exports.ordered.limit(10)
render layout: false # For turbo frame
@pagy, @exports = pagy(Current.family.family_exports.ordered, limit: safe_per_page)
@breadcrumbs = [
[ t("breadcrumbs.home"), root_path ],
[ t("breadcrumbs.exports"), family_exports_path ]
]
render layout: "settings"
end
def download
if @export.downloadable?
redirect_to @export.export_file, allow_other_host: true
else
redirect_to imports_path, alert: "Export not ready for download"
redirect_to family_exports_path, alert: t("family_exports.export_not_ready")
end
end
def destroy
@export.destroy
redirect_to imports_path, notice: "Export deleted successfully"
redirect_to family_exports_path, notice: t("family_exports.destroy.success")
end
private
@@ -46,7 +50,7 @@ class FamilyExportsController < ApplicationController
def require_admin
unless Current.user.admin?
redirect_to root_path, alert: "Access denied"
redirect_to root_path, alert: t("family_exports.access_denied")
end
end
end

View File

@@ -12,11 +12,10 @@ class ImportsController < ApplicationController
end
def index
@imports = Current.family.imports
@exports = Current.user.admin? ? Current.family.family_exports.ordered.limit(10) : nil
@pagy, @imports = pagy(Current.family.imports.where(type: Import::TYPES).ordered, limit: safe_per_page)
@breadcrumbs = [
[ "Home", root_path ],
[ "Import/Export", imports_path ]
[ t("breadcrumbs.home"), root_path ],
[ t("breadcrumbs.imports"), imports_path ]
]
render layout: "settings"
end
@@ -90,7 +89,7 @@ class ImportsController < ApplicationController
private
def set_import
@import = Current.family.imports.find(params[:id])
@import = Current.family.imports.includes(:account).find(params[:id])
end
def import_params

View File

@@ -20,7 +20,7 @@ class RulesController < ApplicationController
.recent
.includes(:rule)
@pagy, @recent_runs = pagy(recent_runs_scope, limit: params[:per_page] || 20, page_param: :runs_page)
@pagy, @recent_runs = pagy(recent_runs_scope, limit: safe_per_page, page_param: :runs_page)
render layout: "settings"
end

View File

@@ -21,7 +21,7 @@ class TransactionsController < ApplicationController
:transfer_as_inflow, :transfer_as_outflow
)
@pagy, @transactions = pagy(base_scope, limit: per_page)
@pagy, @transactions = pagy(base_scope, limit: safe_per_page)
# Load projected recurring transactions for next month
@projected_recurring = Current.family.recurring_transactions
@@ -281,10 +281,6 @@ class TransactionsController < ApplicationController
end
private
def per_page
params[:per_page].to_i.positive? ? params[:per_page].to_i : 20
end
def needs_rule_notification?(transaction)
return false if Current.user.rule_prompts_disabled