Convert Bootstrap 3 horizontal forms to Bootstrap 5

The label-beside-input forms (label col-md-2, input col-md-8) relied on
form-horizontal and form-group acting as a row. Each field wrapper is now a real
.row.mb-3, labels are col-form-label, help text is form-text, and the leftover
form-horizontal and form-actions classes, which do nothing in Bootstrap 5, are
dropped.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Brenda WallaceandClaude Sonnet 5 committed 2026-09-20 18:03:36 +12:00
1 parent a9193dc988
commit 92f3f0cc36
22 files changed
+113 -114

No files matched your search

+2 -2
View File
@@ -5,7 +5,7 @@ jQuery ->
$(".remove-altname-row").css("display", "inline-block")
-$ ->
sci_template = "<div id='sci_template[INDEX]' class='template col-md-12'><div class='col-md-2'><label>Scientific name INDEX:</label></div><div class='col-md-8'><input name='sci_name[INDEX]' class='scientific-name-auto-suggest form-control' id='sci_name[INDEX]' data-source-url='/scientific_names/gbif_suggest')'></input><span class='help-block'>Scientific name of crop</span><input type='text' id='sci_gbif_key[INDEX]' class=''></div><div class='col-md-2'></div></div>"
sci_template = "<div id='sci_template[INDEX]' class='template col-md-12'><div class='col-md-2'><label>Scientific name INDEX:</label></div><div class='col-md-8'><input name='sci_name[INDEX]' class='scientific-name-auto-suggest form-control' id='sci_name[INDEX]' data-source-url='/scientific_names/gbif_suggest')'></input><span class='form-text'>Scientific name of crop</span><input type='text' id='sci_gbif_key[INDEX]' class=''></div><div class='col-md-2'></div></div>"
sci_index = $('#scientific_names .template').length + 1
@@ -21,7 +21,7 @@ jQuery ->
element = document.getElementById(tmp)
element.remove()
alt_template = "<div id='alt_template[INDEX]' class='template col-md-12'><div class='col-md-2'><label>Alternate name INDEX:</label></div><div class='col-md-8'><input name='alt_name[INDEX]' class='form-control' id='alt_name[INDEX]')'></input><span class='help-block'>Alternate name of crop.</span></div><div class='col-md-2'></div></div>"
alt_template = "<div id='alt_template[INDEX]' class='template col-md-12'><div class='col-md-2'><label>Alternate name INDEX:</label></div><div class='col-md-8'><input name='alt_name[INDEX]' class='form-control' id='alt_name[INDEX]')'></input><span class='form-text'>Alternate name of crop.</span></div><div class='col-md-2'></div></div>"
alt_index = $('#alternate_names .template').length + 1
+1 -1
View File
@@ -56,7 +56,7 @@
.row
.col-md-6
= f.check_box :finished, label: t('buttons.mark_as_finished')
%span.help-block= t('.finish_helper')
%span.form-text= t('.finish_helper')
.card-footer
.text-end= f.submit 'Save'
+8 -8
View File
@@ -3,7 +3,7 @@
- if content_for? :title
%h1.h2-responsive.text-center
%strong=yield :title
= form_for @alternate_name, html: { class: 'form-horizontal' } do |f|
= form_for @alternate_name do |f|
- if @alternate_name.errors.any?
#error_explanation
%h2
@@ -14,25 +14,25 @@
%li= msg
%p
%span.help-block
%span.form-text
For detailed crop wrangling guidelines, please consult the
= link_to "crop wrangling guide", "https://github.com/Growstuff/growstuff/wiki/Crop-Wrangling"
on the Growstuff wiki.
.mb-3
= f.label :crop_id, class: 'control-label col-md-2'
.row.mb-3
= f.label :crop_id, class: 'col-form-label col-md-2'
.col-md-8
= select(:alternate_name, :crop_id,
Crop.order(:name).pluck(:name, :id),
{ selected: @alternate_name.crop_id || @crop.id },
class: 'form-control')
.mb-3
= f.label :name, class: 'control-label col-md-2'
.row.mb-3
= f.label :name, class: 'col-form-label col-md-2'
.col-md-8
= f.text_field :name, class: 'form-control'
.col-md-8
= f.select :language, I18nData.languages.map {|code, name| [name.split(";").first, code] }, class: 'form-control'
.mb-3
.form-actions.offset-md-2.col-md-8
.row.mb-3
.offset-md-2.col-md-8
= f.submit 'Save', class: 'btn btn-primary'
+2 -2
View File
@@ -4,7 +4,7 @@
- if content_for? :title
%h1.h2-responsive.text-center
%strong=yield :title
= form_for(@comment, html: { class: "form-horizontal" }) do |f|
= form_for(@comment) do |f|
- if @comment.errors.any?
#error_explanation
%h2
@@ -18,7 +18,7 @@
= f.text_area :body, rows: 6, class: 'form-control md-textarea', autofocus: 'autofocus', required: true, pattern: '\w+'
= f.label :body, "Your comment:"
%span.help-block
%span.form-text
= render partial: "shared/markdown_help"
.actions.text-end
= f.submit 'Post comment', class: 'btn btn-primary'
+19 -19
View File
@@ -17,7 +17,7 @@
-# Handy link to crop wrangling policy/style guide, shown to wranglers only
- if can? :wrangle, @crop
%p
%span.help-block
%span.form-text
For detailed crop wrangling guidelines, please consult the
= link_to "crop wrangling guide", "https://github.com/Growstuff/growstuff/wiki/Crop-Wrangling"
on the Growstuff wiki.
@@ -27,7 +27,7 @@
.mb-3#new_crop
= f.text_field :name, required: true
%span.help-block
%span.form-text
The common name for the crop, in English (required).
- if can? :wrangle, @crop
Wranglers: please ensure this is singular, and capitalize
@@ -36,58 +36,58 @@
%h2 Lifespan
%p
= f.radio_button(:perennial, false, label: 'Annual')
%span.help-block Living and reproducing in a single year or less
%span.form-text Living and reproducing in a single year or less
%p
= f.radio_button(:perennial, true, label: "Perennial")
%span.help-block Living more than two years
%span.form-text Living more than two years
%h2 Data
- if @crop.description.blank? || can?(:wrangle, @crop)
= f.text_area :description, label: 'Description'
- if @crop.parent
%span.help-block Parent: #{@crop.parent.description}
%span.form-text Parent: #{@crop.parent.description}
- if @crop.row_spacing.blank? || can?(:wrangle, @crop)
= f.number_field :row_spacing, label: 'Row Spacing (cm)', min: 0
- if @crop.parent
%span.help-block Parent: #{@crop.parent.row_spacing}
%span.form-text Parent: #{@crop.parent.row_spacing}
- if @crop.spread.blank? || can?(:wrangle, @crop)
= f.number_field :spread, label: 'Spread (cm)', min: 0
- if @crop.parent
%span.help-block Parent: #{@crop.parent.spread}
%span.form-text Parent: #{@crop.parent.spread}
- if @crop.height.blank? || can?(:wrangle, @crop)
= f.number_field :height, label: 'Height (cm)', min: 0
- if @crop.parent
%span.help-block Parent: #{@crop.parent.height}
%span.form-text Parent: #{@crop.parent.height}
- if @crop.sowing_method.blank? || can?(:wrangle, @crop)
= f.text_field :sowing_method
- if @crop.parent
%span.help-block Parent: #{@crop.parent.sowing_method}
%span.form-text Parent: #{@crop.parent.sowing_method}
- if @crop.sun_requirements.blank? || can?(:wrangle, @crop)
= f.text_field :sun_requirements
- if @crop.parent
%span.help-block Parent: #{@crop.parent.sun_requirements}
%span.form-text Parent: #{@crop.parent.sun_requirements}
- if @crop.growing_degree_days.blank? || can?(:wrangle, @crop)
= f.number_field :growing_degree_days, min: 0
- if @crop.parent
%span.help-block Parent: #{@crop.parent.growing_degree_days}
%span.form-text Parent: #{@crop.parent.growing_degree_days}
= f.text_field :public_food_key, label: 'Australian Food Composition Database Public Food Key'
- unless @crop.approved?
= link_to 'Search wikipedia', "https://en.wikipedia.org/w/index.php?search=#{@crop.name}", target: '_blank'
- if @crop.en_wikipedia_url.blank? || can?(:wrangle, @crop)
= f.url_field :en_wikipedia_url, id: "en_wikipedia_url", label: 'Wikipedia URL'
%span.help-block
%span.form-text
Link to the crop's page on the English language Wikipedia (required).
- if @crop.en_youtube_url.blank? || can?(:wrangle, @crop)
= f.url_field :en_youtube_url, label: 'YouTube URL'
%span.help-block
%span.form-text
Link to a YouTube video about the crop in English.
-# Only crop wranglers see the crop hierarchy (for now)
- if can? :wrangle, @crop
= f.select(:parent_id, Crop.order(:name).pluck(:name, :id),
{ include_blank: true, label: 'Parent crop'})
%span.help-block Optional. For setting up crop hierarchies for varieties etc.
%span.form-text Optional. For setting up crop hierarchies for varieties etc.
-# Everyone (wranglers and requesters) gets to add scientific names
@@ -99,12 +99,12 @@
- @crop.scientific_names.each.with_index do |sci, index|
.template.col-12{ id: "sci_template[#{index + 1}]" }
.col-2
= label_tag :scientific_names, "Scientific name #{index + 1}:", class: 'control-label'
= label_tag :scientific_names, "Scientific name #{index + 1}:", class: 'form-label'
.col-8
= text_field_tag "sci_name[#{index + 1}]", sci.name, id: "sci_name[#{index + 1}]",
class: 'scientific-name-auto-suggest form-control',
data: { source_url: gbif_suggest_scientific_names_path }
%span.help-block Searches GBIF to determine scientific name of crop.
%span.form-text Searches GBIF to determine scientific name of crop.
= hidden_field_tag "sci_gbif_key[#{index + 1}]", sci.gbif_key, id: "sci_gbif_key[#{index + 1}]",
class: 'scientific-name-auto-suggest-id'
%h2 Alternate names
@@ -115,10 +115,10 @@
- @crop.alternate_names.each.with_index do |alt, index|
.template.col-12{ id: "alt_template[#{index + 1}]" }
.col-2
= label_tag :alternate_names, "Alternate name #{index + 1}:", class: 'control-label'
= label_tag :alternate_names, "Alternate name #{index + 1}:", class: 'form-label'
.col-8
= text_field_tag "alt_name[#{index + 1}]", alt.name, id: "alt_name[#{index + 1}]", class: 'form-control'
%span.help-block Alternate name of crop.
%span.form-text Alternate name of crop.
.col-2
-# This is used for comments from crop requesters. We need to show it
@@ -141,7 +141,7 @@
= f.select(:reason_for_rejection, @crop.reasons_for_rejection, include_blank: true)
= f.text_area :rejection_notes, rows: 3
%span.help-block
%span.form-text
Please provide additional notes why this crop request was rejected if the above reasons do not apply.
.card-footer
+2 -2
View File
@@ -4,13 +4,13 @@
%h1 Resend confirmation instructions
= form_for(resource, as: resource_name,
url: confirmation_path(resource_name),
html: { method: :post, class: 'form-horizontal', role: 'form' }) do |f|
html: { method: :post, role: 'form' }) do |f|
= render 'devise/shared/error_messages', resource: resource
%p Enter either your login name or your email address to resend the confirmation email.
= f.text_field :login, class: 'form-control', placeholder: 'email or login name'
= f.submit "Resend confirmation instructions", class: 'btn w-100 my-4 btn-primary'
.mb-3
.row.mb-3
.offset-md-2.col-md-8
= render "devise/shared/links"
+7 -7
View File
@@ -2,22 +2,22 @@
= form_for(resource, as: resource_name,
url: password_path(resource_name),
html: { method: :put, class: 'form-horizontal', role: 'form' }) do |f|
html: { method: :put, role: 'form' }) do |f|
= render 'devise/shared/error_messages', resource: resource
= f.hidden_field :reset_password_token
.mb-3
= f.label :password, "New password", class: 'control-label col-md-2'
.row.mb-3
= f.label :password, "New password", class: 'col-form-label col-md-2'
.col-md-8
= f.password_field :password, class: 'form-control'
.mb-3
= f.label :password_confirmation, "Confirm new password", class: 'control-label col-md-2'
.row.mb-3
= f.label :password_confirmation, "Confirm new password", class: 'col-form-label col-md-2'
.col-md-8
= f.password_field :password_confirmation, class: 'form-control'
.mb-3
.form-actions.offset-md-2.col-md-8
.row.mb-3
.offset-md-2.col-md-8
= f.submit "Change my password", class: 'btn btn-primary'
= render "devise/shared/links"
@@ -1,15 +1,15 @@
= form_for(resource, as: resource_name,
url: registration_path(resource_name) + '#delete',
html: { method: :delete, class: 'form-horizontal' }) do |f|
html: { method: :delete }) do |f|
%br/
= render 'devise/shared/error_messages', resource: resource
%p Delete your account from #{ENV['GROWSTUFF_SITE_NAME']}.
.mb-3
= f.label :current_password, "Password required to delete", class: 'control-label col-md-2'
.row.mb-3
= f.label :current_password, "Password required to delete", class: 'col-form-label col-md-2'
.col-md-4
= f.password_field :current_password, class: 'form-control', id: 'current_pw_for_delete'
.mb-3
.form-actions.offset-md-2.col-md-8
.row.mb-3
.offset-md-2.col-md-8
= f.submit "Delete", class: 'btn btn-danger'
@@ -1,6 +1,6 @@
= form_for(resource, as: resource_name,
url: registration_path(resource_name) + '#apps',
html: { method: :put, class: 'form-horizontal' }) do |_f|
html: { method: :put }) do |_f|
%br/
= render 'devise/shared/error_messages', resource: resource
.row
@@ -1,48 +1,48 @@
= form_for(resource, as: resource_name,
url: registration_path(resource_name) + '#email',
html: { method: :put, class: 'form-horizontal' }) do |f|
html: { method: :put }) do |f|
%br/
= render 'devise/shared/error_messages', resource: resource
.mb-3
= f.label :email, class: 'control-label col-md-2'
.row.mb-3
= f.label :email, class: 'col-form-label col-md-2'
.col-md-8
= f.email_field :email, class: 'form-control'
%span.help-block If you change your email address you will have to reconfirm.
%span.form-text If you change your email address you will have to reconfirm.
.mb-3
.row.mb-3
.offset-md-2.col-md-8.checkbox
%label
= f.check_box :show_email
Show email publicly on your profile page.
.mb-3
.row.mb-3
.offset-md-2.col-md-8.checkbox
%label
= f.check_box :send_notification_email
Receive emailed copies of Inbox notifications (eg. private messages).
.mb-3
.row.mb-3
.offset-md-2.col-md-8.checkbox
%label
= f.check_box :send_planting_reminder
Receive regular reminders to track your planting and harvesting.
.mb-3
.row.mb-3
.offset-md-2.col-md-8.checkbox
%label
= f.check_box :send_harvest_reminder
Receive regular reminders of upcoming harvests.
.mb-3
.row.mb-3
.offset-md-2.col-md-8.checkbox
%label
= f.check_box :newsletter
Subscribe to the #{ENV['GROWSTUFF_SITE_NAME']} newsletter
.help-block
.form-text
= render partial: 'newsletter_blurb'
.mb-3
.form-actions.offset-md-2.col-md-8
.row.mb-3
.offset-md-2.col-md-8
= f.submit "Save", class: 'btn btn-primary'
= f.hidden_field(:tos_agreement, value: true)
@@ -1,25 +1,25 @@
= form_for(resource, as: resource_name,
url: registration_path(resource_name) + '#password',
html: { method: :put, class: 'form-horizontal' }) do |f|
html: { method: :put }) do |f|
%br/
= render 'devise/shared/error_messages', resource: resource
.mb-3
= f.label :current_password, class: 'control-label col-md-2'
.row.mb-3
= f.label :current_password, class: 'col-form-label col-md-2'
.col-md-4
= f.password_field :current_password, class: 'form-control'
.mb-3
= f.label :password, "New password", class: 'control-label col-md-2'
.row.mb-3
= f.label :password, "New password", class: 'col-form-label col-md-2'
.col-md-4
= f.password_field :password, autocomplete: "off", class: 'form-control'
.mb-3
= f.label :password_confirmation, class: 'control-label col-md-2'
.row.mb-3
= f.label :password_confirmation, class: 'col-form-label col-md-2'
.col-md-4= f.password_field :password_confirmation, class: 'form-control'
.mb-3
.form-actions.offset-md-2.col-md-8
.row.mb-3
.offset-md-2.col-md-8
= f.submit "Save", class: 'btn btn-primary'
= f.hidden_field(:tos_agreement, value: true)
@@ -1,47 +1,47 @@
= form_for(resource, as: resource_name,
url: registration_path(resource_name) + '#profile',
html: { method: :put, class: 'form-horizontal' }) do |f|
html: { method: :put }) do |f|
%br/
= render 'devise/shared/error_messages', resource: resource
.mb-3
= f.label :location, 'Your location', class: 'control-label col-md-2'
.row.mb-3
= f.label :location, 'Your location', class: 'col-form-label col-md-2'
.col-md-8
= f.text_field :location, autocomplete: "off", class: 'form-control'
%span.help-block This will be displayed on a map. You can be as detailed or vague as you like.
%span.form-text This will be displayed on a map. You can be as detailed or vague as you like.
.mb-3
= f.label :bio, class: 'control-label col-md-2'
.row.mb-3
= f.label :bio, class: 'col-form-label col-md-2'
.col-md-8
= f.text_area :bio, rows: 6, class: 'form-control', placeholder: "I'm am XYZ gardener interested in A, B, C"
.mb-3
= f.label :website_url, 'Website', class: 'control-label col-md-2'
.row.mb-3
= f.label :website_url, 'Website', class: 'col-form-label col-md-2'
.col-md-8
= f.url_field :website_url, class: 'form-control', placeholder: "https://you.example.com/"
.mb-3
= f.label :instagram_handle, 'Instagram', class: 'control-label col-md-2'
.row.mb-3
= f.label :instagram_handle, 'Instagram', class: 'col-form-label col-md-2'
.col-md-8
= f.text_field :instagram_handle, class: 'form-control', placeholder: 'your_handle'
.mb-3
= f.label :facebook_handle, 'Facebook', class: 'control-label col-md-2'
.row.mb-3
= f.label :facebook_handle, 'Facebook', class: 'col-form-label col-md-2'
.col-md-8
= f.text_field :facebook_handle, class: 'form-control', placeholder: 'your_handle'
.mb-3
= f.label :bluesky_handle, 'Bluesky', class: 'control-label col-md-2'
.row.mb-3
= f.label :bluesky_handle, 'Bluesky', class: 'col-form-label col-md-2'
.col-md-8
= f.text_field :bluesky_handle, class: 'form-control', placeholder: 'your_handle'
.mb-3
= f.label :other_url, 'Other URL', class: 'control-label col-md-2'
.row.mb-3
= f.label :other_url, 'Other URL', class: 'col-form-label col-md-2'
.col-md-8
= f.url_field :other_url, class: 'form-control', placeholder: "https://you.example.com/"
.mb-3
%label.control-label.col-md-2
.row.mb-3
%label.form-label.col-md-2
Profile picture
.col-md-8
= render partial: "members/avatar", locals: { member: @member }
@@ -52,7 +52,7 @@
= succeed "." do
= link_to 'gravatar.com', "https://gravatar.com/"
.mb-3
.form-actions.offset-md-2.col-md-8
.row.mb-3
.offset-md-2.col-md-8
= f.submit "Save", class: 'btn btn-primary'
= f.hidden_field(:tos_agreement, value: true)
-1
View File
@@ -4,7 +4,6 @@
= form_for(resource, as: resource_name,
url: unlock_path(resource_name),
html: { method: :post,
class: 'form-horizontal',
role: 'form' }) do |f|
%h2 Resend unlock instructions
+6 -6
View File
@@ -1,4 +1,4 @@
= form_for @garden_type, html: { class: 'form-horizontal' } do |f|
= form_for @garden_type do |f|
- if @garden_type.errors.any?
#error_explanation
%h2= "#{pluralize(@garden_type.errors.count, "error")} prohibited this garden_type from being saved:"
@@ -8,13 +8,13 @@
%h2 Basic information
.mb-3
= f.label :name, class: 'control-label col-md-2'
.row.mb-3
= f.label :name, class: 'col-form-label col-md-2'
.col-md-8
= f.text_field :name, class: 'form-control'
%span.help-block
%span.form-text
The name for the garden_type, i.e. "organic", in English (required).
.mb-3
.form-actions.offset-md-2.col-md-8
.row.mb-3
.offset-md-2.col-md-8
= f.submit 'Save', class: 'btn btn-primary'
+1 -1
View File
@@ -19,7 +19,7 @@
= f.text_field :location,
value: @garden.location || current_member.location,
class: 'form-control', maxlength: 255
%span.help-block
%span.form-text
= t('.location_helper')
- if current_member.location.blank?
= link_to "Set your location now.", edit_member_registration_path
+1 -1
View File
@@ -25,7 +25,7 @@
- else
= auto_suggest @harvest, :crop, class: 'form-control', default: @crop
- unless @planting
%span.help-block.col-md-8
%span.form-text.col-md-8
Can't find what you're looking for?
= link_to "Request new crops.", new_crop_path
+2 -2
View File
@@ -13,13 +13,13 @@
= f.label :login_name
.controls
= f.text_field :login_name, autofocus: true, class: 'form-control input-lg'
%p.help-block Choose a login name
%p.form-text Choose a login name
.mb-3
= f.label :email
.controls
= f.text_field :email, class: 'form-control input-lg', placeholder: 'Example: email@me.com'
%p.help-block Please confirm your email address.
%p.form-text Please confirm your email address.
.mb-3
.offset-md-2.col-md-8.checkbox
+2 -2
View File
@@ -4,10 +4,10 @@
%p
= form_tag(nearby_members_path, method: :get, class: 'form-inline') do
= label_tag :distance, "Find members within", class: 'control-label'
= label_tag :distance, "Find members within", class: 'form-label'
= text_field_tag :distance, @distance, class: 'input-mini'
= select_tag :units, options_for_select({ "miles": :mi, "km": :km }, @units), class: 'input-small'
= label_tag :location, "of", class: 'control-label'
= label_tag :location, "of", class: 'form-label'
= text_field_tag :location, @location
= submit_tag "Search", class: 'btn btn-primary'
+1 -1
View File
@@ -10,7 +10,7 @@
= f.text_field :subject, value: @subject, class: 'form-control', maxlength: 255, required: true
= f.text_area :body, rows: 12, label: "Type your message here", required: true
%span.help-block= render partial: "shared/markdown_help"
%span.form-text= render partial: "shared/markdown_help"
.card-footer
= link_to 'cancel', conversations_path, class: 'btn'
+1 -1
View File
@@ -32,4 +32,4 @@
= f.datetime_field :date_taken
.mb-3
.form-actions= f.submit 'Save', class: 'btn'
.= f.submit 'Save', class: 'btn'
+1 -1
View File
@@ -21,7 +21,7 @@
- else
= label_tag :body, "What's going on in your food garden?"
= f.text_area :body, rows: 12
%span.help-block= render partial: "shared/markdown_help"
%span.form-text= render partial: "shared/markdown_help"
- if @post.forum || @forum
- forum = @post.forum || @forum
+10 -10
View File
@@ -1,4 +1,4 @@
= form_for @scientific_name, html: { class: 'form-horizontal' } do |f|
= form_for @scientific_name do |f|
- if @scientific_name.errors.any?
#error_explanation
%h2
@@ -9,25 +9,25 @@
%li= msg
%p
%span.help-block
%span.form-text
For detailed crop wrangling guidelines, please consult the
= link_to "crop wrangling guide", "https://github.com/Growstuff/growstuff/wiki/Crop-Wrangling"
on the Growstuff wiki.
.mb-3
= f.label :crop_id, class: 'control-label col-md-2'
.row.mb-3
= f.label :crop_id, class: 'col-form-label col-md-2'
.col-md-8
= select(:scientific_name, :crop_id, Crop.order(:name).pluck(:name, :id),
{ selected: @scientific_name.crop_id || @crop.id },
class: 'form-control')
.mb-3
= f.label :name, class: 'control-label col-md-2'
.row.mb-3
= f.label :name, class: 'col-form-label col-md-2'
.col-md-8
= f.text_field :name, class: 'form-control'
.mb-3
= f.label :wikidata_id, "Wikidata ID", class: 'control-label col-md-2'
.row.mb-3
= f.label :wikidata_id, "Wikidata ID", class: 'col-form-label col-md-2'
.col-md-8
= f.text_field :wikidata_id, class: 'form-control', pattern: 'Q[1-9][0-9]*'
.mb-3
.form-actions.offset-md-2.col-md-8
.row.mb-3
.offset-md-2.col-md-8
= f.submit 'Save', class: 'btn btn-primary'