From d8f04ad1157a07bbd21cff285f697cb808fa74ae Mon Sep 17 00:00:00 2001 From: Ryan Arakawa Date: Thu, 26 Feb 2026 13:59:39 -0600 Subject: [PATCH] CP-10379 ordered signing (#59) * add named signing order values and defer to template signing order * the enum changes and the default in submission.rb don't REALLY matter since almost all of our changes in future commits defer to templates. * add template methods to know how many actual submitters there are and add complex default logic based on when fields are added or removed. For example: If only 1 employee field it's single sided. If we add a manager field it automatically changes to employee_then_manager unless manually changed to a different dual sided. If either field is removed, it automatically switches back to single_sided * enforce new signing order logic - replace submitters_order_preserved? with signing_order_enforced? in send_signature_requests - add manager_then_employee branch to send_signature_requests to send to second submitter first, while we don't send out emails with Docuseal, there are changes further down the line required - skip submitters without fields for single_sided in create_from_submitters, this is mostly necessary for single_sided manager forms - refactor current_submitter_order? to reverse submitter_items for manager_then_employee instead of special-casing index * wire up named signing order through controllers * when saving a template, check if preferences have changed, if it has changed, fire webhook event. * changes in templates_controller.rb are for automatic updates based on field types. So if only 1 field type (employee fields only) this automatically updates * template_preferences_controller.rb handles manual updates to signing order from user * add signing order UI - add SigningOrderModal component for selecting signing order from within the template builder - show signing order button in builder toolbar only when template has 2+ submitter fields * add template.preferences_updated webhook job * add template.preferences_updated webhook support - add template.preferences_updated to account default webhook events - guard account create_careerplug_webhook against missing CAREERPLUG_WEBHOOK_URL env var - create partnership-scoped webhook for template.preferences_updated on partnership creation - add template.preferences_updated to WebhookUrl::EVENTS - update PARTNERSHIP_EVENTS to only include template.preferences_updated - return WebhookUrl.none instead of raising for templates with neither account nor partnership - extend webhooks:setup_development rake task to create partnership webhooks * rubocop and rspec fixes * erb_lint violation fixes * harden webhooks with account_id and partnership_id in payload * we're requiring two points of contact in the db for multitenancy * use external account id to match correctly in webhook payload * PR comments * handle submitter UUID not matching correctly with flash alert that surfaces to user * add more testing for simultaneous and single sided orders * add comment for skipping Devise auth for Iframe auth * refactor template webhook enqueue to a shared concern * use safe navigation for first_party name * make default submitters_order value consistent between `lib/submissions.rb` and `submission.rb` * more descriptive error message for signing order error * update to non-predicate method for rubocop we used to just return true or false, but we are using nil to signify that the submitter uuid is not found for the controller so the error can be surfaced to the user. * erb_lint formatting fix * PR comment changes * change current_submitter_order to validate_submitter_order for clarity * add translations --- app/controllers/api/submissions_controller.rb | 4 +- app/controllers/concerns/template_webhooks.rb | 24 +++ app/controllers/submissions_controller.rb | 2 +- app/controllers/submit_form_controller.rb | 14 +- app/controllers/templates_controller.rb | 27 ++-- .../templates_preferences_controller.rb | 24 +++ .../templates_uploads_controller.rb | 9 +- app/javascript/template_builder/builder.vue | 34 +++- app/javascript/template_builder/i18n.js | 4 + .../template_builder/signing_order_modal.vue | 123 ++++++++++++++ app/jobs/process_submitter_completion_job.rb | 2 +- ...preferences_updated_webhook_request_job.rb | 36 +++++ app/models/account.rb | 4 +- app/models/partnership.rb | 12 ++ app/models/submission.rb | 16 +- app/models/template.rb | 29 ++++ app/models/webhook_url.rb | 4 +- app/views/layouts/form.html.erb | 1 + app/views/shared/_flash.html.erb | 2 +- app/views/templates_preferences/show.html.erb | 56 ++++++- config/locales/i18n.yml | 7 + lib/params/submission_create_validator.rb | 8 +- lib/submissions.rb | 17 +- lib/submissions/create_from_submitters.rb | 23 ++- lib/submissions/serialize_for_api.rb | 3 +- lib/submitters.rb | 17 +- lib/submitters/serialize_for_webhook.rb | 7 +- lib/tasks/webhooks.rake | 27 +++- lib/webhook_urls.rb | 6 +- ...rences_updated_webhook_request_job_spec.rb | 131 +++++++++++++++ .../create_from_submitters_spec.rb | 68 ++++++++ spec/lib/submissions_spec.rb | 58 +++++++ spec/lib/submitters_spec.rb | 95 +++++++++++ spec/lib/webhook_urls_spec.rb | 10 +- .../account_create_careerplug_webhook_spec.rb | 65 -------- spec/models/account_spec.rb | 35 ++++ spec/models/partnership_spec.rb | 29 ++++ spec/models/submission_spec.rb | 47 ++++++ spec/models/template_spec.rb | 153 ++++++++++++++++++ spec/requests/submissions_spec.rb | 8 +- spec/requests/templates_spec.rb | 3 +- 41 files changed, 1099 insertions(+), 145 deletions(-) create mode 100644 app/controllers/concerns/template_webhooks.rb create mode 100644 app/javascript/template_builder/signing_order_modal.vue create mode 100644 app/jobs/send_template_preferences_updated_webhook_request_job.rb create mode 100644 spec/jobs/send_template_preferences_updated_webhook_request_job_spec.rb create mode 100644 spec/lib/submissions/create_from_submitters_spec.rb create mode 100644 spec/lib/submissions_spec.rb create mode 100644 spec/lib/submitters_spec.rb delete mode 100644 spec/models/account_create_careerplug_webhook_spec.rb create mode 100644 spec/models/submission_spec.rb create mode 100644 spec/models/template_spec.rb diff --git a/app/controllers/api/submissions_controller.rb b/app/controllers/api/submissions_controller.rb index f1b515ea..788fcc7e 100644 --- a/app/controllers/api/submissions_controller.rb +++ b/app/controllers/api/submissions_controller.rb @@ -13,7 +13,7 @@ module Api submissions = Submissions.search(current_user, @submissions, params[:q]) submissions = filter_submissions(submissions, params) - submissions = paginate(submissions.preload(:created_by_user, :submitters, + submissions = paginate(submissions.preload(:account, :created_by_user, :submitters, template: :folder, combined_document_attachment: :blob, audit_trail_attachment: :blob)) @@ -167,7 +167,7 @@ module Api template:, user: current_user, source: :api, - submitters_order: params[:submitters_order] || params[:order] || 'preserved', + submitters_order: params[:submitters_order] || params[:order] || template.effective_submitters_order, submissions_attrs:, params: ) diff --git a/app/controllers/concerns/template_webhooks.rb b/app/controllers/concerns/template_webhooks.rb new file mode 100644 index 00000000..ffb0619e --- /dev/null +++ b/app/controllers/concerns/template_webhooks.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +module TemplateWebhooks + def enqueue_template_created_webhooks(template) + WebhookUrls.for_template(template, 'template.created').each do |webhook_url| + SendTemplateCreatedWebhookRequestJob.perform_async('template_id' => template.id, + 'webhook_url_id' => webhook_url.id) + end + end + + def enqueue_template_updated_webhooks(template) + WebhookUrls.for_template(template, 'template.updated').each do |webhook_url| + SendTemplateUpdatedWebhookRequestJob.perform_async('template_id' => template.id, + 'webhook_url_id' => webhook_url.id) + end + end + + def enqueue_template_preferences_updated_webhooks(template) + WebhookUrls.for_template(template, 'template.preferences_updated').each do |webhook_url| + SendTemplatePreferencesUpdatedWebhookRequestJob.perform_async('template_id' => template.id, + 'webhook_url_id' => webhook_url.id) + end + end +end diff --git a/app/controllers/submissions_controller.rb b/app/controllers/submissions_controller.rb index 66940486..76cd0a43 100644 --- a/app/controllers/submissions_controller.rb +++ b/app/controllers/submissions_controller.rb @@ -56,7 +56,7 @@ class SubmissionsController < ApplicationController Submissions.create_from_submitters(template: @template, user: current_user, source: :invite, - submitters_order: params[:preserve_order] == '1' ? 'preserved' : 'random', + submitters_order: @template.effective_submitters_order, submissions_attrs: submissions_params[:submission].to_h.values, params: params.merge('send_completed_email' => true)) end diff --git a/app/controllers/submit_form_controller.rb b/app/controllers/submit_form_controller.rb index 1e0e8e0e..2588c711 100644 --- a/app/controllers/submit_form_controller.rb +++ b/app/controllers/submit_form_controller.rb @@ -20,9 +20,17 @@ class SubmitFormController < ApplicationController @form_configs = Submitters::FormConfigs.call(@submitter, CONFIG_KEYS) - return render :awaiting if (@form_configs[:enforce_signing_order] || - submission.template&.preferences&.dig('submitters_order') == 'preserved') && - !Submitters.current_submitter_order?(@submitter) + if @form_configs[:enforce_signing_order] || + submission.template_signing_order.in?(%w[employee_then_manager manager_then_employee]) + signing_order = Submitters.validate_submitter_order(@submitter) + + if signing_order.nil? + flash.now[:alert] = I18n.t('user_id_did_not_match_please_try_again_or_contact_support') + return render :awaiting + end + + return render :awaiting unless signing_order + end Submissions.preload_with_pages(submission) diff --git a/app/controllers/templates_controller.rb b/app/controllers/templates_controller.rb index c98836d1..1de0cab7 100644 --- a/app/controllers/templates_controller.rb +++ b/app/controllers/templates_controller.rb @@ -4,6 +4,7 @@ class TemplatesController < ApplicationController include PrefillFieldsHelper include IframeAuthentication include PartnershipContext + include TemplateWebhooks skip_before_action :verify_authenticity_token skip_before_action :authenticate_via_token!, only: [:update] @@ -99,8 +100,10 @@ class TemplatesController < ApplicationController end def update - @template.assign_attributes(template_params) + # Capture current submitters_order before any changes + old_submitters_order = @template.preferences['submitters_order'] + @template.assign_attributes(template_params) is_name_changed = @template.name_changed? @template.save! @@ -109,7 +112,13 @@ class TemplatesController < ApplicationController enqueue_template_updated_webhooks(@template) - head :ok + # If submitters_order changed (e.g., fields removed making it single_sided), fire preferences webhook + new_submitters_order = @template.preferences['submitters_order'] + if old_submitters_order != new_submitters_order && new_submitters_order.present? + enqueue_template_preferences_updated_webhooks(@template) + end + + render json: { preferences: @template.preferences } end def destroy @@ -159,20 +168,6 @@ class TemplatesController < ApplicationController end end - def enqueue_template_created_webhooks(template) - WebhookUrls.for_template(template, 'template.created').each do |webhook_url| - SendTemplateCreatedWebhookRequestJob.perform_async('template_id' => template.id, - 'webhook_url_id' => webhook_url.id) - end - end - - def enqueue_template_updated_webhooks(template) - WebhookUrls.for_template(template, 'template.updated').each do |webhook_url| - SendTemplateUpdatedWebhookRequestJob.perform_async('template_id' => template.id, - 'webhook_url_id' => webhook_url.id) - end - end - def handle_account_override return unless authorized_clone_account_id?(params[:account_id]) diff --git a/app/controllers/templates_preferences_controller.rb b/app/controllers/templates_preferences_controller.rb index e2ec9ee3..dc9b1e8d 100644 --- a/app/controllers/templates_preferences_controller.rb +++ b/app/controllers/templates_preferences_controller.rb @@ -1,6 +1,17 @@ # frozen_string_literal: true class TemplatesPreferencesController < ApplicationController + include IframeAuthentication + include PartnershipContext + include TemplateWebhooks + + # We use IframeAuthentication#authenticate_from_referer to authenticate the user. + # These are holdovers from legacy Docuseal that uses an actual login system + # and will be removed in a future ticket. + skip_before_action :verify_authenticity_token + skip_before_action :authenticate_via_token! + + before_action :authenticate_from_referer load_and_authorize_resource :template def show; end @@ -8,10 +19,23 @@ class TemplatesPreferencesController < ApplicationController def create authorize!(:update, @template) + old_submitters_order = @template.preferences['submitters_order'] @template.preferences = @template.preferences.merge(template_params[:preferences]) @template.preferences = @template.preferences.reject { |_, v| (v.is_a?(String) || v.is_a?(Hash)) && v.blank? } + + # Handle single_sided case (when template has < 2 unique submitters) + if @template.unique_submitter_uuids.size < 2 && @template.preferences['submitters_order'].present? + @template.preferences['submitters_order'] = 'single_sided' + end + @template.save! + # Enqueue webhook if submitters_order changed + new_submitters_order = @template.preferences['submitters_order'] + if old_submitters_order != new_submitters_order && new_submitters_order.present? + enqueue_template_preferences_updated_webhooks(@template) + end + head :ok end diff --git a/app/controllers/templates_uploads_controller.rb b/app/controllers/templates_uploads_controller.rb index f50dbfa7..7991a74a 100644 --- a/app/controllers/templates_uploads_controller.rb +++ b/app/controllers/templates_uploads_controller.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true class TemplatesUploadsController < ApplicationController + include TemplateWebhooks + skip_before_action :verify_authenticity_token, only: [:create] load_and_authorize_resource :template, parent: false @@ -70,11 +72,4 @@ class TemplatesUploadsController < ApplicationController { files: [file] } end - - def enqueue_template_created_webhooks(template) - WebhookUrls.for_template(template, 'template.created').each do |webhook_url| - SendTemplateCreatedWebhookRequestJob.perform_async('template_id' => template.id, - 'webhook_url_id' => webhook_url.id) - end - end end diff --git a/app/javascript/template_builder/builder.vue b/app/javascript/template_builder/builder.vue index e8024ddc..f576ffc8 100644 --- a/app/javascript/template_builder/builder.vue +++ b/app/javascript/template_builder/builder.vue @@ -63,6 +63,14 @@ name="buttons" /> @@ -348,6 +364,7 @@ import DocumentPreview from './preview' import DocumentControls from './controls' import MobileFields from './mobile_fields' import FieldSubmitter from './field_submitter' +import SigningOrderModal from './signing_order_modal' import { IconPlus, IconUsersPlus, IconDeviceFloppy, IconChevronDown, IconEye, IconWritingSign, IconInnerShadowTop, IconInfoCircle, IconAdjustments } from '@tabler/icons-vue' import { v4 } from 'uuid' import { ref, computed, toRaw, watch } from 'vue' @@ -376,7 +393,8 @@ export default { IconChevronDown, IconAdjustments, IconEye, - IconDeviceFloppy + IconDeviceFloppy, + SigningOrderModal }, provide () { return { @@ -387,6 +405,7 @@ export default { currencies: this.currencies, locale: this.locale, baseFetch: this.baseFetch, + authenticityToken: this.authenticityToken, fieldTypes: this.fieldTypes, backgroundColor: this.backgroundColor, withPhone: this.withPhone, @@ -636,13 +655,18 @@ export default { drawFieldType: null, drawOption: null, dragField: null, - isDragFile: false + isDragFile: false, + isShowSigningOrderModal: false } }, computed: { submitterDefaultNames: FieldSubmitter.computed.names, selectedAreaRef: () => ref(), fieldsDragFieldRef: () => ref(), + hasMultipleSubmitterFields () { + const submitterUuids = new Set(this.template.fields.map((f) => f.submitter_uuid).filter(Boolean)) + return submitterUuids.size >= 2 + }, language () { return this.locale.split('-')[0].toLowerCase() }, @@ -1823,7 +1847,11 @@ export default { } }), headers: { 'Content-Type': 'application/json' } - }).then(() => { + }).then((response) => response.json()).then((data) => { + if (data.preferences) { + this.template.preferences = data.preferences + } + if (this.onSave) { this.onSave(this.template) } diff --git a/app/javascript/template_builder/i18n.js b/app/javascript/template_builder/i18n.js index db5f8983..489e2257 100644 --- a/app/javascript/template_builder/i18n.js +++ b/app/javascript/template_builder/i18n.js @@ -79,6 +79,10 @@ const en = { condition: 'Condition', first_party: 'Employee', second_party: 'Manager', + signing_order: 'Signing Order', + select_signing_order: 'Select Signing Order', + simultaneous_signing_description: 'Both parties may complete the form at the same time', + failed_to_save_signing_order_please_try_again_or_contact_support: 'Failed to save signing order. Please try again or contact support for assistance.', draw: 'Draw', add: 'Add', or_add_field_without_drawing: 'Or add field without drawing', diff --git a/app/javascript/template_builder/signing_order_modal.vue b/app/javascript/template_builder/signing_order_modal.vue new file mode 100644 index 00000000..8cb15efc --- /dev/null +++ b/app/javascript/template_builder/signing_order_modal.vue @@ -0,0 +1,123 @@ + + + diff --git a/app/jobs/process_submitter_completion_job.rb b/app/jobs/process_submitter_completion_job.rb index 26367c5f..832448d7 100644 --- a/app/jobs/process_submitter_completion_job.rb +++ b/app/jobs/process_submitter_completion_job.rb @@ -24,7 +24,7 @@ class ProcessSubmitterCompletionJob create_completed_documents!(submitter) - if !is_all_completed && submitter.submission.submitters_order_preserved? && params['send_invitation_email'] != false + if !is_all_completed && submitter.submission.signing_order_enforced? && params['send_invitation_email'] != false enqueue_next_submitter_request_notification(submitter) end diff --git a/app/jobs/send_template_preferences_updated_webhook_request_job.rb b/app/jobs/send_template_preferences_updated_webhook_request_job.rb new file mode 100644 index 00000000..457d3967 --- /dev/null +++ b/app/jobs/send_template_preferences_updated_webhook_request_job.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +class SendTemplatePreferencesUpdatedWebhookRequestJob + include Sidekiq::Job + + sidekiq_options queue: :webhooks + + def perform(params = {}) + template = Template.find(params['template_id']) + webhook_url = WebhookUrl.find(params['webhook_url_id']) + + attempt = params['attempt'].to_i + + return if webhook_url.url.blank? || webhook_url.events.exclude?('template.preferences_updated') + + data = { + id: template.id, + external_account_id: template.account&.external_account_id, + external_partnership_id: template.partnership&.external_partnership_id, + external_id: template.external_id, + application_key: template.application_key, + submitters_order: template.preferences['submitters_order'] + } + + resp = SendWebhookRequest.call(webhook_url, event_type: 'template.preferences_updated', data:) + + return unless WebhookRetryLogic.should_retry?(response: resp, attempt: attempt, record: template) + + SendTemplatePreferencesUpdatedWebhookRequestJob.perform_in((2**attempt).minutes, { + 'template_id' => template.id, + 'webhook_url_id' => webhook_url.id, + 'attempt' => attempt + 1, + 'last_status' => resp&.status.to_i + }) + end +end diff --git a/app/models/account.rb b/app/models/account.rb index e9acfa12..8ea0e893 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -78,11 +78,11 @@ class Account < ApplicationRecord private def create_careerplug_webhook - return if ENV['CAREERPLUG_WEBHOOK_SECRET'].blank? + return if ENV['CAREERPLUG_WEBHOOK_SECRET'].blank? || ENV['CAREERPLUG_WEBHOOK_URL'].blank? webhook_urls.create!( url: ENV.fetch('CAREERPLUG_WEBHOOK_URL'), - events: %w[form.viewed form.started form.completed form.declined], + events: %w[form.viewed form.started form.completed form.declined template.preferences_updated], secret: { 'X-CareerPlug-Secret' => ENV.fetch('CAREERPLUG_WEBHOOK_SECRET') } ) end diff --git a/app/models/partnership.rb b/app/models/partnership.rb index 782317fe..63a4851f 100644 --- a/app/models/partnership.rb +++ b/app/models/partnership.rb @@ -22,6 +22,8 @@ class Partnership < ApplicationRecord validates :external_partnership_id, presence: true, uniqueness: true validates :name, presence: true + after_commit :create_careerplug_webhook, on: :create + def self.find_or_create_by_external_id(external_id, name, attributes = {}) find_by(external_partnership_id: external_id) || create!(attributes.merge(external_partnership_id: external_id, name: name)) @@ -34,4 +36,14 @@ class Partnership < ApplicationRecord template_folders.create!(name: TemplateFolder::DEFAULT_NAME, author: author) end + + def create_careerplug_webhook + return if ENV['CAREERPLUG_WEBHOOK_SECRET'].blank? || ENV['CAREERPLUG_WEBHOOK_URL'].blank? + + webhook_urls.create!( + url: ENV.fetch('CAREERPLUG_WEBHOOK_URL'), + events: %w[template.preferences_updated], + secret: { 'X-CareerPlug-Secret' => ENV.fetch('CAREERPLUG_WEBHOOK_SECRET') } + ) + end end diff --git a/app/models/submission.rb b/app/models/submission.rb index 2abfbb5b..a70df4c8 100644 --- a/app/models/submission.rb +++ b/app/models/submission.rb @@ -53,7 +53,7 @@ class Submission < ApplicationRecord serialize :preferences, coder: JSON attribute :source, :string, default: 'link' - attribute :submitters_order, :string, default: 'random' + attribute :submitters_order, :string, default: 'single_sided' attribute :slug, :string, default: -> { SecureRandom.base58(14) } @@ -94,10 +94,16 @@ class Submission < ApplicationRecord }, scope: false, prefix: true enum :submitters_order, { - random: 'random', - preserved: 'preserved' + single_sided: 'single_sided', + employee_then_manager: 'employee_then_manager', + manager_then_employee: 'manager_then_employee', + simultaneous: 'simultaneous' }, scope: false, prefix: true + def signing_order_enforced? + template_signing_order.in?(%w[employee_then_manager manager_then_employee]) + end + def expired? expire_at && expire_at <= Time.current end @@ -106,6 +112,10 @@ class Submission < ApplicationRecord submitters.where.not(completed_at: nil).order(:completed_at).last end + def template_signing_order + template&.preferences&.dig('submitters_order') + end + def schema_documents if template_id? template_schema_documents diff --git a/app/models/template.rb b/app/models/template.rb index e7399124..18976a6a 100644 --- a/app/models/template.rb +++ b/app/models/template.rb @@ -54,6 +54,7 @@ class Template < ApplicationRecord has_one :search_entry, as: :record, inverse_of: :record, dependent: :destroy before_validation :maybe_set_default_folder, on: :create + before_save :update_submitters_order, if: :fields_changed? attribute :preferences, :string, default: -> { {} } attribute :fields, :string, default: -> { [] } @@ -87,6 +88,15 @@ class Template < ApplicationRecord external_id end + def unique_submitter_uuids + fields.filter_map { |f| f['submitter_uuid'] }.uniq + end + + def effective_submitters_order + preferences['submitters_order'].presence || + (unique_submitter_uuids.size < 2 ? 'single_sided' : 'employee_then_manager') + end + private def maybe_set_default_folder @@ -96,4 +106,23 @@ class Template < ApplicationRecord self.folder ||= partnership.default_template_folder(author) end end + + def update_submitters_order + submitter_count = unique_submitter_uuids.size + current_order = preferences['submitters_order'] + + if submitter_count < 2 + # Always set to single_sided for templates with 0 or 1 submitter + preferences['submitters_order'] = 'single_sided' + elsif submitter_count == 2 + # Set to employee_then_manager when there are exactly 2 submitters + # Only set if not already configured to something else + if current_order.blank? || current_order == 'single_sided' + preferences['submitters_order'] = 'employee_then_manager' + end + elsif current_order == 'single_sided' + # Clear single_sided if template now has 3+ submitters + preferences.delete('submitters_order') + end + end end diff --git a/app/models/webhook_url.rb b/app/models/webhook_url.rb index 430b9e23..3a495755 100644 --- a/app/models/webhook_url.rb +++ b/app/models/webhook_url.rb @@ -38,12 +38,12 @@ class WebhookUrl < ApplicationRecord submission.archived template.created template.updated + template.preferences_updated ].freeze # Partnership webhooks can only use template events since partnerships don't have submissions/submitters PARTNERSHIP_EVENTS = %w[ - template.created - template.updated + template.preferences_updated ].freeze belongs_to :account, optional: true diff --git a/app/views/layouts/form.html.erb b/app/views/layouts/form.html.erb index 64b4a537..d298ef4d 100644 --- a/app/views/layouts/form.html.erb +++ b/app/views/layouts/form.html.erb @@ -15,6 +15,7 @@ <%= render 'shared/posthog' if ENV['POSTHOG_TOKEN'] %> + <% if flash.present? %><%= render 'shared/flash' %><% end %> <%= yield %> diff --git a/app/views/shared/_flash.html.erb b/app/views/shared/_flash.html.erb index 5eee11bd..234ce214 100644 --- a/app/views/shared/_flash.html.erb +++ b/app/views/shared/_flash.html.erb @@ -1,5 +1,5 @@