Skip to content

Commit

Permalink
Merge pull request #4937 from danielabar/4472-partner-profile-files-d…
Browse files Browse the repository at this point in the history
…irect-upload

[#4472] Persist file uploads through validation errors
  • Loading branch information
cielf authored Jan 31, 2025
2 parents 7427d29 + 66636b4 commit e4f2a01
Show file tree
Hide file tree
Showing 16 changed files with 231 additions and 524 deletions.
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" %>

<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

0 comments on commit e4f2a01

Please sign in to comment.