Repository navigation
Keep download buttons enabled after the file downloads (Resolves #5691) #5725
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
costajohnt
wants to merge
8
commits into
rubyforgood:main
Choose a base branch
from
costajohnt:5691-download-button-stuck-please-wait
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
a56c4e0
Keep download buttons enabled after the file downloads
costajohnt a5d049f
Retrigger CI
costajohnt 384c1c5
Retrigger CI
costajohnt 8060d82
Keep the download debounce and re-enable the button after a delay
costajohnt 88a9517
Merge remote-tracking branch 'upstream/main' into 5691-download-butto…
costajohnt a2d940f
Use one download controller for every download button
costajohnt af162fb
Make the download controller opt-in with remote: true
costajohnt 24c6bbc
Merge remote-tracking branch 'upstream/main' into 5691-download-butto…
costajohnt File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file was deleted.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,77 @@ | ||
| import { Controller } from "@hotwired/stimulus" | ||
| import Rails from "@rails/ujs" | ||
|
|
||
| // Connects to data-controller="download" | ||
| // Fetches a file in the background and hands it to the browser as a download, so a download | ||
| // button can stay disabled until the file actually arrives (#5691). rails-ujs only re-enables | ||
| // a disabled link on the next page load, and a file download never triggers one. | ||
| // | ||
| // Usage: | ||
| // <a href="/donations.csv" data-controller="download" data-action="download#download"> | ||
| // <div data-controller="download" data-download-url-value="/some/path.csv"></div> (downloads on connect) | ||
| // | ||
| export default class extends Controller { | ||
| static values = { url: String } | ||
|
|
||
| connect() { | ||
| if (this.hasUrlValue) this.fetchFile(this.urlValue) | ||
| } | ||
|
|
||
| async download(event) { | ||
| // Let new-tab clicks through to the browser. These are the same ones rails-ujs skips, so the | ||
| // link is never left disabled. | ||
| if (event.metaKey || event.ctrlKey || event.button !== 0) return | ||
| event.preventDefault() | ||
| if (this.busy) return | ||
|
|
||
| this.busy = true | ||
| Rails.disableElement(this.element) | ||
| try { | ||
| await this.fetchFile(this.element.href) | ||
| } finally { | ||
| Rails.enableElement(this.element) | ||
| this.busy = false | ||
| } | ||
| } | ||
|
|
||
| async fetchFile(url) { | ||
| try { | ||
| // fetch never exposes a 3xx to JS: it either follows it or, in manual mode, returns an | ||
| // opaque response with no Location. Following it here would also use up the flash that | ||
| // export redirects (handle_csv_export) rely on, so let the browser make the request instead. | ||
| // That means a redirecting endpoint is requested twice, so it must not do work before redirecting. | ||
| const response = await fetch(url, { redirect: "manual", headers: { "X-Requested-With": "XMLHttpRequest" } }) | ||
| if (response.type === "opaqueredirect") { | ||
| window.location.href = url | ||
| return | ||
| } | ||
| // Signed out: reloading the page sends the user to the login screen. | ||
| if (response.status === 401) { | ||
| window.location.reload() | ||
| return | ||
| } | ||
| if (!response.ok) throw new Error(`Download failed with status ${response.status}`) | ||
|
|
||
| const filename = this._extractFilename(response.headers.get("Content-Disposition")) | ||
| const objectUrl = URL.createObjectURL(await response.blob()) | ||
| const anchor = document.createElement("a") | ||
| anchor.href = objectUrl | ||
| anchor.download = filename || new URL(url, window.location.href).pathname.split("/").pop() | ||
| anchor.click() | ||
| // Revoking in the same tick can cancel the download in Safari. | ||
| setTimeout(() => URL.revokeObjectURL(objectUrl), 1000) | ||
| } catch (error) { | ||
| console.error(error) | ||
| window.toastr.error("Sorry, the download failed. Please try again.", "", { timeOut: 5000 }) | ||
| } | ||
| } | ||
|
|
||
| _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 | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,7 +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 %>" | ||
| <div data-controller="toast download" | ||
| data-download-url-value="<%= csv_url %>" | ||
| data-toast-message-value="Your CSV export is downloading!" | ||
| class="d-none"></div> | ||
| <% end %> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Have you validated that this is the case across the board? Nothing ever does work before redirecting?
If this is not a straight no-risk change, then we need to change this so it doesn't alter the existing
download_button_tobut adds a new version (e.g. withremote: true) that we can opt into rather than doing an across-the-board change.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I went through all 20
download_button_tocall sites and the action behind each one. 19 of them answer withsend_data/send_filedirectly (no redirect), so there's only ever one request. Distributions is the only one that redirects, andhandle_csv_exportonly setsflash[:trigger_csv_download]before redirecting, so hitting it twice is harmless. The annual reportshow/rangeendpoints do write throughReports.retrieve_report, but they also respond withsend_data, so they still run once.That said, it isn't a straight no-risk change: every export button moves from a normal navigation to fetch + blob, which means the whole file sits in browser memory before it's saved (that matters most for the bigger annual reports), and I haven't tested very large files in a browser. So I'm happy to switch to the opt-in approach: leave
download_button_toas it was, add an opt-in version, and use it only on the annual report buttons from #5691. Want me to do that?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Went ahead with the opt-in version since it isn't a straight no-risk change.
download_button_tois back to its original behavior, and it only wires up the download controller when you passremote: true. Right now that's just the two annual report export buttons from #5691. I also merged in main.