Skip to content

Commit ace0ee3

Browse files
authored
Merge pull request #5115 from danielabar/4821-part3-partner-profile-persist-docs-validation-errors
[#4821] Persist profile attached documents through validation errors
2 parents 5c1ddf1 + d675037 commit ace0ee3

File tree

4 files changed

+171
-19
lines changed

4 files changed

+171
-19
lines changed
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
module Partners
2+
module ProfileHelper
3+
# Returns an array of filenames that are attached to the profile but not persisted.
4+
# This is to display to the user that the system remembers their file selections
5+
# even if there was a form validation error.
6+
# The method returns a JSON string (an array of filenames) to be used in a Stimulus controller.
7+
def attached_but_not_persisted_file_names(profile)
8+
filenames = profile.documents.attachments
9+
.select { |att| !att.persisted? }
10+
.map { |att| att.blob.filename.to_s }
11+
12+
filenames.to_json
13+
end
14+
15+
# Returns true if at least one document attachment is actually persisted
16+
def has_persisted_documents?(profile)
17+
profile.documents.attachments.any?(&:persisted?)
18+
end
19+
end
20+
end

app/javascript/controllers/file_input_controller.js

Lines changed: 39 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,9 @@ import { Controller } from "@hotwired/stimulus";
55
*
66
* This controller:
77
* - Listens for file selection on an `<input type="file" multiple="multiple">`
8-
* - Displays selected file names in a custom list **when multiple files are selected
9-
* - Defaults to the browser’s built-in display for a single file selection
8+
* - Displays selected file names in a custom list when multiple files are selected
9+
* - If provided a `filenames` array, displays those file names as if user had just selected them.
10+
* This is useful for displaying previously selected files on page load with validation errors.
1011
*
1112
* Expected HTML structure should have a placeholder div for the selected file names:
1213
*
@@ -20,29 +21,60 @@ import { Controller } from "@hotwired/stimulus";
2021
export default class extends Controller {
2122
static targets = ["input", "list"];
2223

24+
static values = {
25+
filenames: Array
26+
}
27+
2328
connect() {
2429
this.inputTarget.addEventListener("change", () => this.updateFileList());
30+
31+
if (this.hasFilenamesValue && this.filenamesValue.length > 0) {
32+
this.updateFileListFromValue();
33+
}
34+
}
35+
36+
// Opens the hidden file input when "Choose Files" button is clicked
37+
triggerFileSelection() {
38+
this.inputTarget.click();
2539
}
2640

41+
// native file input selection
2742
updateFileList() {
2843
const files = this.inputTarget.files;
29-
this.listTarget.innerHTML = ""; // Clear previous list
3044

31-
// If no files or only one file is selected, let the native UI handle it
32-
if (files.length <= 1) {
45+
if (files.length === 0) {
3346
return;
3447
}
3548

49+
this.renderFileList(Array.from(files).map(file => file.name));
50+
}
51+
52+
updateFileListFromValue() {
53+
this.renderFileList(this.filenamesValue);
54+
}
55+
56+
renderFileList(fileNames) {
57+
// Clear previous list
58+
this.listTarget.innerHTML = "";
59+
60+
// Create subheader
61+
const header = document.createElement("p");
62+
header.textContent = "Selected files:";
63+
header.classList.add("font-weight-bold", "mb-1");
64+
65+
// Create file list
3666
const ul = document.createElement("ul");
3767
ul.classList.add("list-unstyled", "mt-2");
3868

39-
Array.from(files).forEach((file) => {
69+
fileNames.forEach((name) => {
4070
const li = document.createElement("li");
4171
li.classList.add("p-1", "rounded", "mb-1");
42-
li.textContent = file.name;
72+
li.textContent = name;
4373
ul.appendChild(li);
4474
});
4575

76+
// Append header and list to target container
77+
this.listTarget.appendChild(header);
4678
this.listTarget.appendChild(ul);
4779
}
4880
}

app/views/partners/profiles/step/_attached_documents_form.html.erb

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
<%= f.fields_for :profile, profile do |pf| %>
2-
<div class="form-group" data-controller="file-input">
3-
<% if profile.documents.attached? %>
2+
<div class="form-group"
3+
data-controller="file-input"
4+
data-file-input-filenames-value="<%= attached_but_not_persisted_file_names(profile) %>">
5+
6+
<%# Allow user to download and/or remove existing attachments %>
7+
<% if has_persisted_documents?(profile) %>
48
<strong>Attached files:</strong>
59
<ul class="list-unstyled">
610
<% profile.documents.each do |doc| %>
@@ -17,8 +21,27 @@
1721
</ul>
1822
<% end %>
1923

20-
<%# Native file input and placeholder for selected file names %>
21-
<%= pf.file_field :documents, multiple: true, class: "form-control-file", data: { file_input_target: "input" } %>
24+
<%# Submit hidden fields for attachments that are not persisted to preserve them through form validation errors %>
25+
<% profile.documents.attachments.each do |att| %>
26+
<% if !att.persisted? %>
27+
<%= pf.hidden_field :documents, multiple: true, value: att.blob.signed_id %>
28+
<% end %>
29+
<% end %>
30+
31+
<%# Hide native file input to support custom behaviour to display %>
32+
<%# previously selected files when validation error occurs %>
33+
<%= pf.file_field :documents,
34+
multiple: true,
35+
direct_upload: true,
36+
class: "form-control-file d-none",
37+
data: { file_input_target: "input" } %>
38+
39+
<%# Custom button to trigger file selection %>
40+
<button type="button" class="btn btn-outline-primary" data-action="click->file-input#triggerFileSelection">
41+
Choose Files
42+
</button>
43+
44+
<%# Placeholder to display selected file(s) populated by app/javascript/controllers/file_input_controller.js %>
2245
<div data-file-input-target="list" class="mt-2"></div>
2346
</div>
2447
<% end %>

spec/system/partners/profile_edit_system_spec.rb

Lines changed: 85 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -143,22 +143,32 @@
143143
find("button[data-bs-target='#attached_documents']").click
144144
expect(page).to have_css("#attached_documents.accordion-collapse.collapse.show", visible: true)
145145

146-
# Upload two documents - needs to be done individually because Capybara doesn't have attach_files multiple support
147-
# https://github.com/teamcapybara/capybara/issues/37
146+
# Upload multiple documents at once
148147
within "#attached_documents" do
149-
attach_file("partner_profile_documents", Rails.root.join("spec/fixtures/files/document1.md"), make_visible: true)
148+
attach_file("partner_profile_documents", [
149+
Rails.root.join("spec/fixtures/files/document1.md"),
150+
Rails.root.join("spec/fixtures/files/document2.md")
151+
], make_visible: true)
152+
153+
# Verify both documents are displayed in custom selection list
154+
expect(page).to have_text("Selected files:")
155+
expect(page).to have_css("[data-file-input-target='list'] li", text: "document1.md")
156+
expect(page).to have_css("[data-file-input-target='list'] li", text: "document2.md")
150157
end
158+
159+
# Save Progress
151160
all("input[type='submit'][value='Save Progress']").last.click
161+
expect(page).to have_css(".alert-success", text: "Details were successfully updated.")
162+
163+
# Verify both documents persist after page reload
152164
visit edit_partners_profile_path
153165
find("button[data-bs-target='#attached_documents']").click
154166
within "#attached_documents" do
155-
attach_file("partner_profile_documents", Rails.root.join("spec/fixtures/files/document2.md"), make_visible: true)
167+
expect(page).to have_link("document1.md")
168+
expect(page).to have_link("document2.md")
156169
end
157-
all("input[type='submit'][value='Save Progress']").last.click
158170

159171
# Remove the first document
160-
visit edit_partners_profile_path
161-
find("button[data-bs-target='#attached_documents']").click
162172
within "#attached_documents" do
163173
document_name = "document1.md"
164174
document_li = find("li.attached-document", text: document_name)
@@ -170,11 +180,12 @@
170180
all("input[type='submit'][value='Save Progress']").last.click
171181
expect(page).to have_css(".alert-success", text: "Details were successfully updated.")
172182

173-
# Verify only one document is listed
183+
# Verify only one document remains
174184
visit edit_partners_profile_path
175185
find("button[data-bs-target='#attached_documents']").click
176186
within "#attached_documents" do
177187
expect(page).to have_link("document2.md")
188+
expect(page).not_to have_link("document1.md")
178189
end
179190
end
180191

@@ -225,5 +236,71 @@
225236
expect(find("label[for='partner_profile_proof_of_partner_status']")).to have_content("irs_determination_letter.md")
226237
end
227238
end
239+
240+
it "persists multiple file uploads when there are validation errors" do
241+
# Open Pick up person section and fill in 4 email addresses which will generate a validation error
242+
find("button[data-bs-target='#pick_up_person']").click
243+
within "#pick_up_person" do
244+
fill_in "Pick Up Person's Email", with: "[email protected], [email protected], [email protected], [email protected]"
245+
end
246+
247+
# Open attached documents section
248+
find("button[data-bs-target='#attached_documents']").click
249+
expect(page).to have_css("#attached_documents.accordion-collapse.collapse.show", visible: true)
250+
251+
# Upload multiple documents
252+
within "#attached_documents" do
253+
attach_file("partner_profile_documents", [
254+
Rails.root.join("spec/fixtures/files/document1.md"),
255+
Rails.root.join("spec/fixtures/files/document2.md")
256+
], make_visible: true)
257+
258+
# Verify both documents are displayed in custom selection list
259+
expect(page).to have_css("[data-file-input-target='list'] li", text: "document1.md")
260+
expect(page).to have_css("[data-file-input-target='list'] li", text: "document2.md")
261+
end
262+
263+
# Save Progress
264+
all("input[type='submit'][value='Save Progress']").last.click
265+
266+
# Expect an alert-danger message containing validation errors
267+
expect(page).to have_css(".alert-danger", text: /There is a problem/)
268+
269+
# Open attached documents section
270+
find("button[data-bs-target='#attached_documents']").click
271+
expect(page).to have_css("#attached_documents.accordion-collapse.collapse.show", visible: true)
272+
273+
# Expect both documents are still displayed in custom list as selected, but nothing is actually attached
274+
within "#attached_documents" do
275+
expect(page).to have_text("Selected files:")
276+
expect(page).to have_css("[data-file-input-target='list'] li", text: "document1.md")
277+
expect(page).to have_css("[data-file-input-target='list'] li", text: "document2.md")
278+
279+
expect(page).not_to have_text("Attached files:")
280+
expect(page).not_to have_link("document1.md")
281+
expect(page).not_to have_link("document2.md")
282+
end
283+
284+
# Fix validation error in Pick up person section: It's already open due to having a validation error
285+
within "#pick_up_person" do
286+
fill_in "Pick Up Person's Email", with: "[email protected], [email protected], [email protected]"
287+
end
288+
289+
# Save Progress
290+
all("input[type='submit'][value='Save Progress']").last.click
291+
expect(page).to have_css(".alert-success", text: "Details were successfully updated.")
292+
293+
# Open attached documents section
294+
find("button[data-bs-target='#attached_documents']").click
295+
expect(page).to have_css("#attached_documents.accordion-collapse.collapse.show", visible: true)
296+
297+
# Expect both documents are now rendered as downloadable links
298+
# i.e. they've been saved, without user having had to select them again
299+
within "#attached_documents" do
300+
expect(page).to have_text("Attached files:")
301+
expect(page).to have_link("document1.md")
302+
expect(page).to have_link("document2.md")
303+
end
304+
end
228305
end
229306
end

0 commit comments

Comments
 (0)