Skip to content

Commit e4f2a01

Browse files
authored
Merge pull request #4937 from danielabar/4472-partner-profile-files-direct-upload
[#4472] Persist file uploads through validation errors
2 parents 7427d29 + 66636b4 commit e4f2a01

File tree

16 files changed

+231
-524
lines changed

16 files changed

+231
-524
lines changed

app/javascript/application.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,11 @@ import 'utils/purchases'
3535

3636
import Rails from "@rails/ujs"
3737
Rails.start()
38+
39+
// Initialize Active Storage
40+
import * as ActiveStorage from "@rails/activestorage";
41+
ActiveStorage.start();
42+
3843
// Disable turbo by default to avoid issues with turbolinks
3944
Turbo.session.drive = false
4045

@@ -107,4 +112,3 @@ $(document).ready(function(){
107112
});
108113
picker.setDateRange(startDate, endDate);
109114
});
110-
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { Controller } from "@hotwired/stimulus"
2+
3+
// Connects to data-controller="file-input-label"
4+
//
5+
// Reproduces the native browser behavior of updating a file input label
6+
// to show the selected file's name. This is necessary when using a custom
7+
// file input, such as with Bootstrap, that does not update automatically.
8+
//
9+
// Key Features:
10+
// 1. Handles initial display of a default label text (e.g., "Choose file..." or
11+
// the previously selected file name if present).
12+
// 2. Updates the label dynamically when a new file is selected.
13+
//
14+
// How it works:
15+
// - When a file is selected, the `fileSelected` method updates the text of the
16+
// label to reflect the name of the selected file.
17+
// - On page load, the `connect` method ensures the label is initialized to the
18+
// correct state (default text or file name, if a file was previously selected).
19+
//
20+
// This controller is used in coordination with direct uploads in Active Storage.
21+
// When a validation error occurs, previously selected files persist on the server
22+
// (via direct upload), and the file name can be displayed to the user.
23+
export default class extends Controller {
24+
static targets = ["input", "label"];
25+
static values = {
26+
defaultText: { type: String, default: 'Choose file...' }
27+
}
28+
29+
connect() {
30+
this.updateLabel();
31+
}
32+
33+
updateLabel() {
34+
const input = this.inputTarget;
35+
const label = this.labelTarget;
36+
37+
// Check if the file input has a file selected
38+
if (input.files.length > 0) {
39+
label.textContent = input.files[0].name;
40+
} else {
41+
label.textContent = this.defaultTextValue;
42+
}
43+
}
44+
45+
// Update the label when a file is selected
46+
fileSelected() {
47+
this.updateLabel();
48+
}
49+
}

app/views/partners/profiles/edit/_agency_information.html.erb

Lines changed: 8 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -10,20 +10,14 @@
1010
<%= form.input :name, label: "Agency Name", class: "form-control", wrapper: :input_group %>
1111
<%= profile_form.input :agency_type, collection: Partner::AGENCY_TYPES.values, label: "Agency Type", class: "form-control", wrapper: :input_group %>
1212
<%= profile_form.input :other_agency_type, label: "Other Agency Type", class: "form-control", wrapper: :input_group %>
13-
<div class="form-group row">
14-
<label class="control-label col-md-3">501(c)(3) IRS Determination Letter or other Proof of Agency Status</label>
15-
<% if profile.proof_of_partner_status.attached? %>
16-
<div class="col-md-8">
17-
Attached
18-
file: <%= link_to profile.proof_of_partner_status.blob['filename'], rails_blob_path(profile.proof_of_partner_status), class: "font-weight-bold" %>
19-
<%= profile_form.file_field :proof_of_partner_status, class: "form-control-file form-control" %>
20-
</div>
21-
<% else %>
22-
<div class="col-md-8">
23-
<%= profile_form.file_field :proof_of_partner_status, class: "form-control-file" %>
24-
</div>
25-
<% end %>
26-
</div>
13+
14+
<%= render "shared/custom_file_input",
15+
form_builder: profile_form,
16+
attachment: profile.proof_of_partner_status,
17+
attachment_name: :proof_of_partner_status,
18+
label_for: "partner_profile_proof_of_partner_status",
19+
label_text: "501(c)(3) IRS Determination Letter or other Proof of Agency Status" %>
20+
2721
<%= profile_form.input :agency_mission, label: "Agency Mission", class: "form-control", wrapper: :input_group %>
2822
<%= profile_form.input :address1, label: "Address (line 1)", class: "form-control", wrapper: :input_group %>
2923
<%= profile_form.input :address2, label: "Address (line 2)", class: "form-control", wrapper: :input_group %>

app/views/partners/profiles/edit/_agency_stability.html.erb

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,18 +10,14 @@
1010
<%= form.input :founded, label: "Year Founded", class: "form-control", wrapper: :input_group %>
1111
<%= form.input :form_990, label: "Form 990 Filed", as: :radio_buttons, class: "form-control",
1212
wrapper: :input_group, wrapper_html: {class: "form-yesno"}, input_html: {class: "radio-yesno"} %>
13-
<label class="control-label col-md-3">Form 990</label>
14-
<% if profile.proof_of_form_990.attached? %>
15-
<div class="col-md-8">
16-
Attached
17-
file: <%= link_to profile.proof_of_form_990.blob['filename'], rails_blob_path(profile.proof_of_form_990), class: "font-weight-bold" %>
18-
<%= form.file_field :proof_of_form_990, class: "form-control-file form-control" %>
19-
</div>
20-
<% else %>
21-
<div class="col-md-8">
22-
<%= form.file_field :proof_of_form_990, class: "form-control-file" %>
23-
</div>
24-
<% end %>
13+
14+
<%= render "shared/custom_file_input",
15+
form_builder: form,
16+
attachment: profile.proof_of_form_990,
17+
attachment_name: :proof_of_form_990,
18+
label_for: "partner_profile_proof_of_form_990",
19+
label_text: "Form 990" %>
20+
2521
<%= form.input :program_name, label: "Program Name(s)", class: "form-control", wrapper: :input_group %>
2622
<%= form.input :program_description, label: "Program Description(s)", class: "form-control", wrapper: :input_group %>
2723
<%= form.input :program_age, label: "Agency Age", class: "form-control", wrapper: :input_group %>

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

Lines changed: 6 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -11,19 +11,12 @@
1111
<%= pf.input :other_agency_type, label: "Other Agency Type", class: "form-control" %>
1212
</div>
1313

14-
<div class="form-group row">
15-
<label class="control-label col-md-3">501(c)(3) IRS Determination Letter or other Proof of Agency Status</label>
16-
<% if profile.proof_of_partner_status.attached? %>
17-
<div class="col-md-8">
18-
Attached file: <%= link_to profile.proof_of_partner_status.blob['filename'], rails_blob_path(profile.proof_of_partner_status), class: "font-weight-bold" %>
19-
<%= pf.file_field :proof_of_partner_status, class: "form-control-file" %>
20-
</div>
21-
<% else %>
22-
<div class="col-md-8">
23-
<%= pf.file_field :proof_of_partner_status, class: "form-control-file" %>
24-
</div>
25-
<% end %>
26-
</div>
14+
<%= render "shared/custom_file_input",
15+
form_builder: pf,
16+
attachment: profile.proof_of_partner_status,
17+
attachment_name: :proof_of_partner_status,
18+
label_for: "partner_profile_proof_of_partner_status",
19+
label_text: "501(c)(3) IRS Determination Letter or other Proof of Agency Status" %>
2720

2821
<div class="form-group">
2922
<%= pf.input :agency_mission, as: :text, label: "Agency Mission", class: "form-control" %>

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

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,12 @@
77
<%= pf.input :form_990, label: "Form 990 Filed", as: :radio_buttons, class: "form-control" %>
88
</div>
99

10-
<% if profile.proof_of_form_990.attached? %>
11-
<div class="form-group">
12-
<label>Attached file: </label>
13-
<%= link_to profile.proof_of_form_990.blob['filename'], rails_blob_path(profile.proof_of_form_990), class: "font-weight-bold" %>
14-
</div>
15-
<% end %>
16-
17-
<div class="form-group">
18-
<%= pf.file_field :proof_of_form_990, class: "form-control-file" %>
19-
</div>
10+
<%= render "shared/custom_file_input",
11+
form_builder: pf,
12+
attachment: profile.proof_of_form_990,
13+
attachment_name: :proof_of_form_990,
14+
label_for: "partner_profile_proof_of_form_990",
15+
label_text: "Form 990" %>
2016

2117
<div class="form-group">
2218
<%= pf.input :program_name, label: "Program Name(s)", class: "form-control" %>
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
<%# locals: (form_builder:, label_for:, label_text:, attachment:, attachment_name:) %>
2+
3+
<%# Creates a custom file input field with the following features: %>
4+
<%# - Displays the name of a previously selected file (even after validation errors). %>
5+
<%# - Integrates with Active Storage direct uploads to persist the file on the server, %>
6+
<%# even if form submission fails. %>
7+
<%# - Uses a Stimulus controller (`file_input_label`) to dynamically update the label %>
8+
<%# text when a new file is selected. %>
9+
<%# - Styled with Bootstrap's custom file input classes. %>
10+
<%# %>
11+
<%# Arguments: %>
12+
<%# - form_builder: The form builder object. %>
13+
<%# - label_for: The ID of the file input (used for the label `for` attribute). %>
14+
<%# - label_text: The text to display for the file input's label. %>
15+
<%# - attachment: The Active Storage attachment object (used to check for existing files). %>
16+
<%# - attachment_name: The name of the attachment field (e.g., `:proof_of_form_990`). %>
17+
18+
<div class="form-group">
19+
<label class="control-label"><%= label_text %></label>
20+
21+
<% if attachment.persisted? %>
22+
Attached file: <%= link_to attachment.blob.filename, rails_blob_path(attachment), class: "font-weight-bold" %>
23+
<% elsif attachment.attached? %>
24+
<%= form_builder.hidden_field attachment_name, value: attachment.signed_id %>
25+
<% end %>
26+
27+
<div class="col-md-12"
28+
data-controller="file-input-label"
29+
data-file-input-label-default-text-value="<%= attachment.attached? ? attachment.blob.filename : 'Choose file...' %>">
30+
<%= form_builder.file_field attachment_name,
31+
direct_upload: true,
32+
class: "custom-file-input",
33+
data: {
34+
action: "change->file-input-label#fileSelected",
35+
file_input_label_target: "input"
36+
} %>
37+
<label class="custom-file-label"
38+
for="<%= label_for %>"
39+
data-file-input-label-target="label">Choose file...</label>
40+
</div>
41+
</div>

config/importmap.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,4 @@
3333
pin "filterrific", to: "filterrific.js"
3434
pin "bootstrap-select", to: "https://ga.jspm.io/npm:[email protected]/dist/js/bootstrap-select.js"
3535
pin "jquery-ui", to: "https://ga.jspm.io/npm:[email protected]/ui/widget.js"
36+
pin "@rails/activestorage", to: "@rails--activestorage.js" # @8.0.100

public/403.html

Lines changed: 5 additions & 119 deletions
Original file line numberDiff line numberDiff line change
@@ -11,132 +11,18 @@
1111
<link href="https://cdn.jsdelivr.net/npm/[email protected]/build/toastr.css" rel="stylesheet">
1212
<link href="https://cdn.jsdelivr.net/npm/@fortawesome/[email protected]/css/all.min.css" rel="stylesheet">
1313
<link href="https://cdn.jsdelivr.net/npm/@fortawesome/[email protected]/css/v4-shims.css" rel="stylesheet">
14-
1514
<link rel="stylesheet" href="/assets/application.css" media="all" />
16-
<script type="importmap" data-turbo-track="reload">{
17-
"imports": {
18-
"jquery": "https://ga.jspm.io/npm:[email protected]/dist/jquery.js",
19-
"admin-lte": "/assets/adminlte.js",
20-
"application": "/assets/application.js",
21-
"startup": "/assets/startup.js",
22-
"@hotwired/turbo-rails": "/assets/turbo.min.js",
23-
"@hotwired/stimulus": "/assets/stimulus.min.js",
24-
"@hotwired/stimulus-loading": "/assets/stimulusloading.js",
25-
"bootstrap": "/assets/bootstrap.min.js",
26-
"popper": "/assets/popper.js",
27-
"highcharts": "https://ga.jspm.io/npm:[email protected]/highcharts.js",
28-
"select2": "https://cdn.jsdelivr.net/npm/[email protected]/dist/js/select2.min.js",
29-
"trix": "https://ga.jspm.io/npm:[email protected]/dist/trix.esm.min.js",
30-
"@rails/actiontext": "https://ga.jspm.io/npm:@rails/[email protected]/app/assets/javascripts/actiontext.js",
31-
"luxon": "https://ga.jspm.io/npm:[email protected]/build/cjs-browser/luxon.js",
32-
"litepicker": "https://cdn.jsdelivr.net/npm/litepicker/dist/litepicker.js",
33-
"litepicker/ranges": "https://cdn.jsdelivr.net/npm/litepicker/dist/plugins/ranges.js",
34-
"toastr": "https://ga.jspm.io/npm:[email protected]/toastr.js",
35-
"@fullcalendar/core": "https://ga.jspm.io/npm:@fullcalendar/[email protected]/index.js",
36-
"preact": "https://ga.jspm.io/npm:[email protected]/dist/preact.module.js",
37-
"preact/compat": "https://ga.jspm.io/npm:[email protected]/compat/dist/compat.module.js",
38-
"preact/hooks": "https://ga.jspm.io/npm:[email protected]/hooks/dist/hooks.module.js",
39-
"@fullcalendar/luxon": "https://ga.jspm.io/npm:@fullcalendar/[email protected]/index.js",
40-
"@fullcalendar/core/": "https://ga.jspm.io/npm:@fullcalendar/[email protected]/",
41-
"@fullcalendar/daygrid": "https://ga.jspm.io/npm:@fullcalendar/[email protected]/index.js",
42-
"@fullcalendar/list": "https://ga.jspm.io/npm:@fullcalendar/[email protected]/index.js",
43-
"quagga": "https://ga.jspm.io/npm:[email protected]/dist/quagga.min.js",
44-
"@rails/ujs": "https://ga.jspm.io/npm:@rails/[email protected]/lib/assets/compiled/rails-ujs.js",
45-
"filterrific": "/assets/filterrific.js",
46-
"bootstrap-select": "https://ga.jspm.io/npm:[email protected]/dist/js/bootstrap-select.js",
47-
"jquery-ui": "https://ga.jspm.io/npm:[email protected]/ui/widget.js",
48-
"controllers/application": "/assets/controllers/application.js",
49-
"controllers/area_served_controller": "/assets/controllers/area_served_controller.js",
50-
"controllers/checkbox_with_nested_element_controller": "/assets/controllers/checkbox_with_nested_element_controller.js",
51-
"controllers/confirmation_controller": "/assets/controllers/confirmation_controller.js",
52-
"controllers/distribution_delivery_controller": "/assets/controllers/distribution_delivery_controller.js",
53-
"controllers/double_select_controller": "/assets/controllers/double_select_controller.js",
54-
"controllers/form_input_controller": "/assets/controllers/form_input_controller.js",
55-
"controllers/highchart_controller": "/assets/controllers/highchart_controller.js",
56-
"controllers": "/assets/controllers/index.js",
57-
"controllers/item_units_controller": "/assets/controllers/item_units_controller.js",
58-
"controllers/password_visibility_controller": "/assets/controllers/password_visibility_controller.js",
59-
"controllers/select2_controller": "/assets/controllers/select2_controller.js",
60-
"controllers/served_area_controller": "/assets/controllers/served_area_controller.js",
61-
"controllers/turbo_controller": "/assets/controllers/turbo_controller.js",
62-
"utils/barcode_items": "/assets/utils/barcode_items.js",
63-
"utils/barcode_scan": "/assets/utils/barcode_scan.js",
64-
"utils/deadline_day_pickers": "/assets/utils/deadline_day_pickers.js",
65-
"utils/distributions_and_transfers": "/assets/utils/distributions_and_transfers.js",
66-
"utils/donations": "/assets/utils/donations.js",
67-
"utils/purchases": "/assets/utils/purchases.js"
68-
}
69-
}</script>
70-
<link rel="modulepreload" href="https://ga.jspm.io/npm:[email protected]/dist/jquery.js">
71-
<link rel="modulepreload" href="/assets/adminlte.js">
72-
<link rel="modulepreload" href="/assets/application.js">
73-
<link rel="modulepreload" href="/assets/startup.js">
74-
<link rel="modulepreload" href="/assets/turbo.min.js">
75-
<link rel="modulepreload" href="/assets/stimulus.min.js">
76-
<link rel="modulepreload" href="/assets/stimulusloading.js">
77-
<link rel="modulepreload" href="/assets/bootstrap.min.js">
78-
<link rel="modulepreload" href="/assets/popper.js">
79-
<link rel="modulepreload" href="https://ga.jspm.io/npm:[email protected]/highcharts.js">
80-
<link rel="modulepreload" href="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/select2.min.js">
81-
<link rel="modulepreload" href="https://ga.jspm.io/npm:[email protected]/dist/trix.esm.min.js">
82-
<link rel="modulepreload" href="https://ga.jspm.io/npm:@rails/[email protected]/app/assets/javascripts/actiontext.js">
83-
<link rel="modulepreload" href="https://ga.jspm.io/npm:[email protected]/build/cjs-browser/luxon.js">
84-
<link rel="modulepreload" href="https://cdn.jsdelivr.net/npm/litepicker/dist/litepicker.js">
85-
<link rel="modulepreload" href="https://cdn.jsdelivr.net/npm/litepicker/dist/plugins/ranges.js">
86-
<link rel="modulepreload" href="https://ga.jspm.io/npm:[email protected]/toastr.js">
87-
<link rel="modulepreload" href="https://ga.jspm.io/npm:@fullcalendar/[email protected]/index.js">
88-
<link rel="modulepreload" href="https://ga.jspm.io/npm:[email protected]/dist/preact.module.js">
89-
<link rel="modulepreload" href="https://ga.jspm.io/npm:[email protected]/compat/dist/compat.module.js">
90-
<link rel="modulepreload" href="https://ga.jspm.io/npm:[email protected]/hooks/dist/hooks.module.js">
91-
<link rel="modulepreload" href="https://ga.jspm.io/npm:@fullcalendar/[email protected]/index.js">
92-
<link rel="modulepreload" href="https://ga.jspm.io/npm:@fullcalendar/[email protected]/">
93-
<link rel="modulepreload" href="https://ga.jspm.io/npm:@fullcalendar/[email protected]/index.js">
94-
<link rel="modulepreload" href="https://ga.jspm.io/npm:@fullcalendar/[email protected]/index.js">
95-
<link rel="modulepreload" href="https://ga.jspm.io/npm:[email protected]/dist/quagga.min.js">
96-
<link rel="modulepreload" href="https://ga.jspm.io/npm:@rails/[email protected]/lib/assets/compiled/rails-ujs.js">
97-
<link rel="modulepreload" href="/assets/filterrific.js">
98-
<link rel="modulepreload" href="https://ga.jspm.io/npm:[email protected]/dist/js/bootstrap-select.js">
99-
<link rel="modulepreload" href="https://ga.jspm.io/npm:[email protected]/ui/widget.js">
100-
<link rel="modulepreload" href="/assets/controllers/application.js">
101-
<link rel="modulepreload" href="/assets/controllers/area_served_controller.js">
102-
<link rel="modulepreload" href="/assets/controllers/checkbox_with_nested_element_controller.js">
103-
<link rel="modulepreload" href="/assets/controllers/confirmation_controller.js">
104-
<link rel="modulepreload" href="/assets/controllers/distribution_delivery_controller.js">
105-
<link rel="modulepreload" href="/assets/controllers/double_select_controller.js">
106-
<link rel="modulepreload" href="/assets/controllers/form_input_controller.js">
107-
<link rel="modulepreload" href="/assets/controllers/highchart_controller.js">
108-
<link rel="modulepreload" href="/assets/controllers/index.js">
109-
<link rel="modulepreload" href="/assets/controllers/item_units_controller.js">
110-
<link rel="modulepreload" href="/assets/controllers/password_visibility_controller.js">
111-
<link rel="modulepreload" href="/assets/controllers/select2_controller.js">
112-
<link rel="modulepreload" href="/assets/controllers/served_area_controller.js">
113-
<link rel="modulepreload" href="/assets/controllers/turbo_controller.js">
114-
<link rel="modulepreload" href="/assets/utils/barcode_items.js">
115-
<link rel="modulepreload" href="/assets/utils/barcode_scan.js">
116-
<link rel="modulepreload" href="/assets/utils/deadline_day_pickers.js">
117-
<link rel="modulepreload" href="/assets/utils/distributions_and_transfers.js">
118-
<link rel="modulepreload" href="/assets/utils/donations.js">
119-
<link rel="modulepreload" href="/assets/utils/purchases.js">
120-
<script type="module">import "application"</script>
121-
122-
<script type="esms-options">
123-
{
124-
"noLoadEventRetriggers": true
125-
}
126-
</script>
127-
12815
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
12916
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
13017
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
13118
<link rel="manifest" href="/site.webmanifest">
13219
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#5bbad5">
13320
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Source+Sans+Pro:300,400,600,700,300italic,400italic,600italic">
134-
135-
<meta name="turbo-visit-control" content="reload">
136-
<meta name="turbo-cache-control" content="no-cache">
21+
<meta name="turbo-visit-control" content="reload">
22+
<meta name="turbo-cache-control" content="no-cache">
13723
</head>
138-
<body data-turbo="" data-controller='turbo'
139-
id="errors" class="not_found hold-transition sidebar-mini layout-fixed">
24+
25+
<body id="errors" class="not_found hold-transition sidebar-mini layout-fixed">
14026
<!-- Site wrapper -->
14127
<div class="wrapper">
14228
<nav class="main-header navbar navbar-expand navbar-white navbar-light">
@@ -188,7 +74,7 @@ <h1>403 Error Page</h1>
18874
<!-- Main content -->
18975
<section class="content">
19076
<div class="error-page">
191-
<h2 class="headline text-warning"> 403</h2>
77+
<h2 class="headline text-warning">403</h2>
19278
<br>
19379
<div class="error-content">
19480
<h3><i class="fas fa-exclamation-triangle text-warning"></i> Oops! The page you were looking for is forbidden.</h3>

0 commit comments

Comments
 (0)