Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions app/controllers/application_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,15 @@ def setup_date_range_picker
@selected_date_range_label = helpers.date_range_label
end

def handle_csv_export
return unless params[:export_csv]

flash[:trigger_csv_download] = true
clean_params = request.query_parameters.except("export_csv")
redirect_url = clean_params.any? ? "#{request.path}?#{clean_params.to_query}" : request.path
redirect_to redirect_url
end

def configure_permitted_parameters
devise_parameter_sanitizer.permit(:account_update, keys: [:name])
end
Expand Down
1 change: 1 addition & 0 deletions app/controllers/distributions_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ class DistributionsController < ApplicationController
include Validatable

before_action :enable_turbo!, only: %i[new show]
before_action :handle_csv_export, only: [:index]
skip_before_action :authenticate_user!, only: %i(calendar)
skip_before_action :authorize_user, only: %i(calendar)
skip_before_action :require_organization, only: %i(calendar)
Expand Down
37 changes: 37 additions & 0 deletions app/javascript/controllers/csv_download_controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { Controller } from "@hotwired/stimulus"

// Connects to data-controller="csv-download"
// Fetches a CSV file in the background and triggers a browser download.
//
// Usage:
// <div data-controller="csv-download" data-csv-download-url-value="/some/path.csv"></div>
//
export default class extends Controller {
static values = { url: String }

connect() {
fetch(this.urlValue, { headers: { "X-Requested-With": "XMLHttpRequest" } })
.then(response => {
if (!response.ok) return
const filename = this._extractFilename(response.headers.get("Content-Disposition"))
return response.blob().then(blob => ({ blob, filename }))
})
.then(({ blob, filename }) => {
const objectUrl = URL.createObjectURL(blob)
const anchor = document.createElement("a")
anchor.href = objectUrl
anchor.download = filename || "report.csv"
anchor.click()
URL.revokeObjectURL(objectUrl)
})
}

_extractFilename(disposition) {
if (!disposition) return null

const utf8Match = disposition.match(/filename\*=UTF-8''([^;\n]+)/i)
if (utf8Match) return decodeURIComponent(utf8Match[1])
const plainMatch = disposition.match(/filename="?([^";\n]+)"?/i)
return plainMatch ? plainMatch[1] : null
}
}
26 changes: 26 additions & 0 deletions app/javascript/controllers/toast_controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { Controller } from "@hotwired/stimulus"

// Connects to data-controller="toast"
// Shows a toastr notification when the element is rendered.
//
// Usage:
// <div data-controller="toast" data-toast-message-value="Hello!" data-toast-type-value="info"></div>
//
export default class extends Controller {
static values = {
message: String,
type: { type: String, default: "info" },
timeout: { type: Number, default: 5000 },
position: { type: String, default: "toast-top-center" }
}

connect() {
const previousTimeout = toastr.options.timeOut;
const previousPosition = toastr.options.positionClass;
toastr.options.timeOut = this.timeoutValue;
toastr.options.positionClass = this.positionValue;
toastr[this.typeValue](this.messageValue);
toastr.options.timeOut = previousTimeout;
toastr.options.positionClass = previousPosition;
}
}
4 changes: 3 additions & 1 deletion app/views/distributions/index.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@
<%=
if @distributions.any?
download_button_to(
distributions_path(format: :csv, filters: filter_params.merge(date_range: date_range_params)),
distributions_path(export_csv: true, filters: filter_params.merge(date_range: date_range_params)),
text: "Export Distributions"
)
end
Expand Down Expand Up @@ -169,3 +169,5 @@
</div>
</div>
</section>

<%= render "shared/csv_download" %>
7 changes: 7 additions & 0 deletions app/views/shared/_csv_download.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<% if flash[:trigger_csv_download] %>
<% csv_url = "#{request.path}.csv#{"?#{request.query_string}" if request.query_string.present?}" %>
<div data-controller="toast csv-download"
data-csv-download-url-value="<%= csv_url %>"
data-toast-message-value="Your CSV export is downloading!"
class="d-none"></div>
<% end %>
10 changes: 10 additions & 0 deletions spec/requests/distributions_requests_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,16 @@
expect(response).to be_successful
end

context "with export_csv param" do
it "redirects then renders a csv-download stimulus controller to export CSV" do
get distributions_path(export_csv: true, foo: "bar")
expect(response).to redirect_to(distributions_path(foo: "bar"))
follow_redirect!
expect(response.body).to include("data-controller=\"toast csv-download\"")
expect(response.body).to include("distributions.csv?foo=bar")
end
end

it "sums distribution totals accurately" do
create(:distribution, :with_items, item_quantity: 5, organization: organization)
create(:line_item, :distribution, itemizable_id: distribution.id, quantity: 7)
Expand Down
16 changes: 16 additions & 0 deletions spec/system/distribution_system_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -931,4 +931,20 @@
# will fail (the distribution is already complete) and show this error
expect(page).not_to have_content("Sorry, we encountered an error when trying to mark this distribution as being completed")
end

describe "CSV export", js: true do
before do
create(:distribution, :with_items, organization: organization)
visit distributions_path
end

it "downloads a CSV and shows a toast notification" do
click_on "Export Distributions"

wait_for_download
expect(downloads.length).to eq(1)
expect(download).to match(/Distributions.*\.csv/)
expect(page).to have_text("Your CSV export is downloading!")
end
end
end