Skip to content
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

[#4472] Persist file uploads through validation errors #4937

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 2 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
6 changes: 5 additions & 1 deletion app/javascript/application.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,11 @@ import 'utils/purchases'

import Rails from "@rails/ujs"
Rails.start()

// Initialize Active Storage
import * as ActiveStorage from "@rails/activestorage";
ActiveStorage.start();

// Disable turbo by default to avoid issues with turbolinks
Turbo.session.drive = false

Expand Down Expand Up @@ -107,4 +112,3 @@ $(document).ready(function(){
});
picker.setDateRange(startDate, endDate);
});

49 changes: 49 additions & 0 deletions app/javascript/controllers/file_input_label_controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { Controller } from "@hotwired/stimulus"

// Connects to data-controller="file-input-label"
//
// Reproduces the native browser behavior of updating a file input label
// to show the selected file's name. This is necessary when using a custom
// file input, such as with Bootstrap, that does not update automatically.
//
// Key Features:
// 1. Handles initial display of a default label text (e.g., "Choose file..." or
// the previously selected file name if present).
// 2. Updates the label dynamically when a new file is selected.
//
// How it works:
// - When a file is selected, the `fileSelected` method updates the text of the
// label to reflect the name of the selected file.
// - On page load, the `connect` method ensures the label is initialized to the
// correct state (default text or file name, if a file was previously selected).
//
// This controller is used in coordination with direct uploads in Active Storage.
// When a validation error occurs, previously selected files persist on the server
// (via direct upload), and the file name can be displayed to the user.
export default class extends Controller {
static targets = ["input", "label"];
static values = {
defaultText: { type: String, default: 'Choose file...' }
}

connect() {
this.updateLabel();
}

updateLabel() {
const input = this.inputTarget;
const label = this.labelTarget;

// Check if the file input has a file selected
if (input.files.length > 0) {
label.textContent = input.files[0].name;
} else {
label.textContent = this.defaultTextValue;
}
}

// Update the label when a file is selected
fileSelected() {
this.updateLabel();
}
}
22 changes: 8 additions & 14 deletions app/views/partners/profiles/edit/_agency_information.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -10,20 +10,14 @@
<%= form.input :name, label: "Agency Name", class: "form-control", wrapper: :input_group %>
<%= profile_form.input :agency_type, collection: Partner::AGENCY_TYPES.values, label: "Agency Type", class: "form-control", wrapper: :input_group %>
<%= profile_form.input :other_agency_type, label: "Other Agency Type", class: "form-control", wrapper: :input_group %>
<div class="form-group row">
<label class="control-label col-md-3">501(c)(3) IRS Determination Letter or other Proof of Agency Status</label>
<% if profile.proof_of_partner_status.attached? %>
<div class="col-md-8">
Attached
file: <%= link_to profile.proof_of_partner_status.blob['filename'], rails_blob_path(profile.proof_of_partner_status), class: "font-weight-bold" %>
<%= profile_form.file_field :proof_of_partner_status, class: "form-control-file form-control" %>
</div>
<% else %>
<div class="col-md-8">
<%= profile_form.file_field :proof_of_partner_status, class: "form-control-file" %>
</div>
<% end %>
</div>

<%= render "shared/custom_file_input",
form_builder: profile_form,
attachment: profile.proof_of_partner_status,
attachment_name: :proof_of_partner_status,
label_for: "partner_profile_proof_of_partner_status",
label_text: "501(c)(3) IRS Determination Letter or other Proof of Agency Status" %>

<%= profile_form.input :agency_mission, label: "Agency Mission", class: "form-control", wrapper: :input_group %>
<%= profile_form.input :address1, label: "Address (line 1)", class: "form-control", wrapper: :input_group %>
<%= profile_form.input :address2, label: "Address (line 2)", class: "form-control", wrapper: :input_group %>
Expand Down
20 changes: 8 additions & 12 deletions app/views/partners/profiles/edit/_agency_stability.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,14 @@
<%= form.input :founded, label: "Year Founded", class: "form-control", wrapper: :input_group %>
<%= form.input :form_990, label: "Form 990 Filed", as: :radio_buttons, class: "form-control",
wrapper: :input_group, wrapper_html: {class: "form-yesno"}, input_html: {class: "radio-yesno"} %>
<label class="control-label col-md-3">Form 990</label>
<% if profile.proof_of_form_990.attached? %>
<div class="col-md-8">
Attached
file: <%= link_to profile.proof_of_form_990.blob['filename'], rails_blob_path(profile.proof_of_form_990), class: "font-weight-bold" %>
<%= form.file_field :proof_of_form_990, class: "form-control-file form-control" %>
</div>
<% else %>
<div class="col-md-8">
<%= form.file_field :proof_of_form_990, class: "form-control-file" %>
</div>
<% end %>

<%= render "shared/custom_file_input",
form_builder: form,
attachment: profile.proof_of_form_990,
attachment_name: :proof_of_form_990,
label_for: "partner_profile_proof_of_form_990",
label_text: "Form 990" %>

<%= form.input :program_name, label: "Program Name(s)", class: "form-control", wrapper: :input_group %>
<%= form.input :program_description, label: "Program Description(s)", class: "form-control", wrapper: :input_group %>
<%= form.input :program_age, label: "Agency Age", class: "form-control", wrapper: :input_group %>
Expand Down
19 changes: 6 additions & 13 deletions app/views/partners/profiles/step/_agency_information_form.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,12 @@
<%= pf.input :other_agency_type, label: "Other Agency Type", class: "form-control" %>
</div>

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

<div class="form-group">
<%= pf.input :agency_mission, as: :text, label: "Agency Mission", class: "form-control" %>
Expand Down
16 changes: 6 additions & 10 deletions app/views/partners/profiles/step/_agency_stability_form.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,12 @@
<%= pf.input :form_990, label: "Form 990 Filed", as: :radio_buttons, class: "form-control" %>
</div>

<% if profile.proof_of_form_990.attached? %>
<div class="form-group">
<label>Attached file: </label>
<%= link_to profile.proof_of_form_990.blob['filename'], rails_blob_path(profile.proof_of_form_990), class: "font-weight-bold" %>
</div>
<% end %>

<div class="form-group">
<%= pf.file_field :proof_of_form_990, class: "form-control-file" %>
</div>
<%= render "shared/custom_file_input",
form_builder: pf,
attachment: profile.proof_of_form_990,
attachment_name: :proof_of_form_990,
label_for: "partner_profile_proof_of_form_990",
label_text: "Form 990 Filed" %>

<div class="form-group">
<%= pf.input :program_name, label: "Program Name(s)", class: "form-control" %>
Expand Down
41 changes: 41 additions & 0 deletions app/views/shared/_custom_file_input.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<%# locals: (form_builder:, label_for:, label_text:, attachment:, attachment_name:) %>

<%# Creates a custom file input field with the following features: %>
<%# - Displays the name of a previously selected file (even after validation errors). %>
<%# - Integrates with Active Storage direct uploads to persist the file on the server, %>
<%# even if form submission fails. %>
<%# - Uses a Stimulus controller (`file_input_label`) to dynamically update the label %>
<%# text when a new file is selected. %>
<%# - Styled with Bootstrap's custom file input classes. %>
<%# %>
<%# Arguments: %>
<%# - form_builder: The form builder object. %>
<%# - label_for: The ID of the file input (used for the label `for` attribute). %>
<%# - label_text: The text to display for the file input's label. %>
<%# - attachment: The Active Storage attachment object (used to check for existing files). %>
<%# - attachment_name: The name of the attachment field (e.g., `:proof_of_form_990`). %>

<div class="form-group">
<label class="control-label"><%= label_text %></label>

<% if attachment.persisted? %>
Attached file: <%= link_to attachment.blob.filename, rails_blob_path(attachment), class: "font-weight-bold" %>
<% elsif attachment.attached? %>
<%= form_builder.hidden_field attachment_name, value: attachment.signed_id %>
<% end %>

<div class="col-md-12"
data-controller="file-input-label"
data-file-input-label-default-text-value="<%= attachment.attached? ? attachment.blob.filename : 'Choose file...' %>">
<%= form_builder.file_field attachment_name,
direct_upload: true,
class: "custom-file-input",
data: {
action: "change->file-input-label#fileSelected",
file_input_label_target: "input"
} %>
<label class="custom-file-label"
for="<%= label_for %>"
data-file-input-label-target="label">Choose file...</label>
</div>
</div>
1 change: 1 addition & 0 deletions config/importmap.rb
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,4 @@
pin "filterrific", to: "filterrific.js"
pin "bootstrap-select", to: "https://ga.jspm.io/npm:[email protected]/dist/js/bootstrap-select.js"
pin "jquery-ui", to: "https://ga.jspm.io/npm:[email protected]/ui/widget.js"
pin "@rails/activestorage", to: "@rails--activestorage.js" # @8.0.100
124 changes: 5 additions & 119 deletions public/403.html
Original file line number Diff line number Diff line change
Expand Up @@ -11,132 +11,18 @@
<link href="https://cdn.jsdelivr.net/npm/[email protected]/build/toastr.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/@fortawesome/[email protected]/css/all.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/@fortawesome/[email protected]/css/v4-shims.css" rel="stylesheet">

<link rel="stylesheet" href="/assets/application.css" media="all" />
<script type="importmap" data-turbo-track="reload">{
"imports": {
"jquery": "https://ga.jspm.io/npm:[email protected]/dist/jquery.js",
"admin-lte": "/assets/adminlte.js",
"application": "/assets/application.js",
"startup": "/assets/startup.js",
"@hotwired/turbo-rails": "/assets/turbo.min.js",
"@hotwired/stimulus": "/assets/stimulus.min.js",
"@hotwired/stimulus-loading": "/assets/stimulusloading.js",
"bootstrap": "/assets/bootstrap.min.js",
"popper": "/assets/popper.js",
"highcharts": "https://ga.jspm.io/npm:[email protected]/highcharts.js",
"select2": "https://cdn.jsdelivr.net/npm/[email protected]/dist/js/select2.min.js",
"trix": "https://ga.jspm.io/npm:[email protected]/dist/trix.esm.min.js",
"@rails/actiontext": "https://ga.jspm.io/npm:@rails/[email protected]/app/assets/javascripts/actiontext.js",
"luxon": "https://ga.jspm.io/npm:[email protected]/build/cjs-browser/luxon.js",
"litepicker": "https://cdn.jsdelivr.net/npm/litepicker/dist/litepicker.js",
"litepicker/ranges": "https://cdn.jsdelivr.net/npm/litepicker/dist/plugins/ranges.js",
"toastr": "https://ga.jspm.io/npm:[email protected]/toastr.js",
"@fullcalendar/core": "https://ga.jspm.io/npm:@fullcalendar/[email protected]/index.js",
"preact": "https://ga.jspm.io/npm:[email protected]/dist/preact.module.js",
"preact/compat": "https://ga.jspm.io/npm:[email protected]/compat/dist/compat.module.js",
"preact/hooks": "https://ga.jspm.io/npm:[email protected]/hooks/dist/hooks.module.js",
"@fullcalendar/luxon": "https://ga.jspm.io/npm:@fullcalendar/[email protected]/index.js",
"@fullcalendar/core/": "https://ga.jspm.io/npm:@fullcalendar/[email protected]/",
"@fullcalendar/daygrid": "https://ga.jspm.io/npm:@fullcalendar/[email protected]/index.js",
"@fullcalendar/list": "https://ga.jspm.io/npm:@fullcalendar/[email protected]/index.js",
"quagga": "https://ga.jspm.io/npm:[email protected]/dist/quagga.min.js",
"@rails/ujs": "https://ga.jspm.io/npm:@rails/[email protected]/lib/assets/compiled/rails-ujs.js",
"filterrific": "/assets/filterrific.js",
"bootstrap-select": "https://ga.jspm.io/npm:[email protected]/dist/js/bootstrap-select.js",
"jquery-ui": "https://ga.jspm.io/npm:[email protected]/ui/widget.js",
"controllers/application": "/assets/controllers/application.js",
"controllers/area_served_controller": "/assets/controllers/area_served_controller.js",
"controllers/checkbox_with_nested_element_controller": "/assets/controllers/checkbox_with_nested_element_controller.js",
"controllers/confirmation_controller": "/assets/controllers/confirmation_controller.js",
"controllers/distribution_delivery_controller": "/assets/controllers/distribution_delivery_controller.js",
"controllers/double_select_controller": "/assets/controllers/double_select_controller.js",
"controllers/form_input_controller": "/assets/controllers/form_input_controller.js",
"controllers/highchart_controller": "/assets/controllers/highchart_controller.js",
"controllers": "/assets/controllers/index.js",
"controllers/item_units_controller": "/assets/controllers/item_units_controller.js",
"controllers/password_visibility_controller": "/assets/controllers/password_visibility_controller.js",
"controllers/select2_controller": "/assets/controllers/select2_controller.js",
"controllers/served_area_controller": "/assets/controllers/served_area_controller.js",
"controllers/turbo_controller": "/assets/controllers/turbo_controller.js",
"utils/barcode_items": "/assets/utils/barcode_items.js",
"utils/barcode_scan": "/assets/utils/barcode_scan.js",
"utils/deadline_day_pickers": "/assets/utils/deadline_day_pickers.js",
"utils/distributions_and_transfers": "/assets/utils/distributions_and_transfers.js",
"utils/donations": "/assets/utils/donations.js",
"utils/purchases": "/assets/utils/purchases.js"
}
}</script>
<link rel="modulepreload" href="https://ga.jspm.io/npm:[email protected]/dist/jquery.js">
<link rel="modulepreload" href="/assets/adminlte.js">
<link rel="modulepreload" href="/assets/application.js">
<link rel="modulepreload" href="/assets/startup.js">
<link rel="modulepreload" href="/assets/turbo.min.js">
<link rel="modulepreload" href="/assets/stimulus.min.js">
<link rel="modulepreload" href="/assets/stimulusloading.js">
<link rel="modulepreload" href="/assets/bootstrap.min.js">
<link rel="modulepreload" href="/assets/popper.js">
<link rel="modulepreload" href="https://ga.jspm.io/npm:[email protected]/highcharts.js">
<link rel="modulepreload" href="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/select2.min.js">
<link rel="modulepreload" href="https://ga.jspm.io/npm:[email protected]/dist/trix.esm.min.js">
<link rel="modulepreload" href="https://ga.jspm.io/npm:@rails/[email protected]/app/assets/javascripts/actiontext.js">
<link rel="modulepreload" href="https://ga.jspm.io/npm:[email protected]/build/cjs-browser/luxon.js">
<link rel="modulepreload" href="https://cdn.jsdelivr.net/npm/litepicker/dist/litepicker.js">
<link rel="modulepreload" href="https://cdn.jsdelivr.net/npm/litepicker/dist/plugins/ranges.js">
<link rel="modulepreload" href="https://ga.jspm.io/npm:[email protected]/toastr.js">
<link rel="modulepreload" href="https://ga.jspm.io/npm:@fullcalendar/[email protected]/index.js">
<link rel="modulepreload" href="https://ga.jspm.io/npm:[email protected]/dist/preact.module.js">
<link rel="modulepreload" href="https://ga.jspm.io/npm:[email protected]/compat/dist/compat.module.js">
<link rel="modulepreload" href="https://ga.jspm.io/npm:[email protected]/hooks/dist/hooks.module.js">
<link rel="modulepreload" href="https://ga.jspm.io/npm:@fullcalendar/[email protected]/index.js">
<link rel="modulepreload" href="https://ga.jspm.io/npm:@fullcalendar/[email protected]/">
<link rel="modulepreload" href="https://ga.jspm.io/npm:@fullcalendar/[email protected]/index.js">
<link rel="modulepreload" href="https://ga.jspm.io/npm:@fullcalendar/[email protected]/index.js">
<link rel="modulepreload" href="https://ga.jspm.io/npm:[email protected]/dist/quagga.min.js">
<link rel="modulepreload" href="https://ga.jspm.io/npm:@rails/[email protected]/lib/assets/compiled/rails-ujs.js">
<link rel="modulepreload" href="/assets/filterrific.js">
<link rel="modulepreload" href="https://ga.jspm.io/npm:[email protected]/dist/js/bootstrap-select.js">
<link rel="modulepreload" href="https://ga.jspm.io/npm:[email protected]/ui/widget.js">
<link rel="modulepreload" href="/assets/controllers/application.js">
<link rel="modulepreload" href="/assets/controllers/area_served_controller.js">
<link rel="modulepreload" href="/assets/controllers/checkbox_with_nested_element_controller.js">
<link rel="modulepreload" href="/assets/controllers/confirmation_controller.js">
<link rel="modulepreload" href="/assets/controllers/distribution_delivery_controller.js">
<link rel="modulepreload" href="/assets/controllers/double_select_controller.js">
<link rel="modulepreload" href="/assets/controllers/form_input_controller.js">
<link rel="modulepreload" href="/assets/controllers/highchart_controller.js">
<link rel="modulepreload" href="/assets/controllers/index.js">
<link rel="modulepreload" href="/assets/controllers/item_units_controller.js">
<link rel="modulepreload" href="/assets/controllers/password_visibility_controller.js">
<link rel="modulepreload" href="/assets/controllers/select2_controller.js">
<link rel="modulepreload" href="/assets/controllers/served_area_controller.js">
<link rel="modulepreload" href="/assets/controllers/turbo_controller.js">
<link rel="modulepreload" href="/assets/utils/barcode_items.js">
<link rel="modulepreload" href="/assets/utils/barcode_scan.js">
<link rel="modulepreload" href="/assets/utils/deadline_day_pickers.js">
<link rel="modulepreload" href="/assets/utils/distributions_and_transfers.js">
<link rel="modulepreload" href="/assets/utils/donations.js">
<link rel="modulepreload" href="/assets/utils/purchases.js">
<script type="module">import "application"</script>

<script type="esms-options">
{
"noLoadEventRetriggers": true
}
</script>

<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
<link rel="manifest" href="/site.webmanifest">
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#5bbad5">
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Source+Sans+Pro:300,400,600,700,300italic,400italic,600italic">

<meta name="turbo-visit-control" content="reload">
<meta name="turbo-cache-control" content="no-cache">
<meta name="turbo-visit-control" content="reload">
<meta name="turbo-cache-control" content="no-cache">
</head>
<body data-turbo="" data-controller='turbo'
id="errors" class="not_found hold-transition sidebar-mini layout-fixed">

<body id="errors" class="not_found hold-transition sidebar-mini layout-fixed">
<!-- Site wrapper -->
<div class="wrapper">
<nav class="main-header navbar navbar-expand navbar-white navbar-light">
Expand Down Expand Up @@ -188,7 +74,7 @@ <h1>403 Error Page</h1>
<!-- Main content -->
<section class="content">
<div class="error-page">
<h2 class="headline text-warning"> 403</h2>
<h2 class="headline text-warning">403</h2>
<br>
<div class="error-content">
<h3><i class="fas fa-exclamation-triangle text-warning"></i> Oops! The page you were looking for is forbidden.</h3>
Expand Down
Loading
Loading