From e51806b98b1cbde4b2d3b35d4fc5973eceaf0f7a Mon Sep 17 00:00:00 2001 From: Zach Gollwitzer Date: Tue, 16 Jul 2024 14:08:24 -0400 Subject: [PATCH] More composable forms (#989) * Make forms more composable, opt-in to form builder * Remove unused method * Simpler money input controls * Add in new form styling to imports * Lint fixes * Small tweak of multi select styles --- .../stylesheets/application.tailwind.css | 19 +- app/controllers/accounts_controller.rb | 1 + app/controllers/application_controller.rb | 2 - app/controllers/currencies_controller.rb | 4 +- app/helpers/application_form_builder.rb | 149 --------------- app/helpers/forms_helper.rb | 59 +++++- app/helpers/styled_form_builder.rb | 55 ++++++ .../controllers/money_field_controller.js | 18 +- .../controllers/select_controller.js | 179 ------------------ .../account/entries/_selection_bar.html.erb | 3 +- .../entryables/transaction/_show.html.erb | 46 +++-- .../transaction/_transaction.html.erb | 2 +- .../entryables/valuation/_form.html.erb | 3 +- app/views/account/transfers/_form.html.erb | 5 +- app/views/accounts/_account.html.erb | 1 - app/views/accounts/edit.html.erb | 4 +- app/views/accounts/list.html.erb | 4 +- app/views/accounts/new.html.erb | 4 +- app/views/accounts/show.html.erb | 4 +- app/views/accounts/summary.html.erb | 26 +-- app/views/categories/_form.html.erb | 2 +- app/views/category/deletions/new.html.erb | 23 +-- app/views/imports/_csv_paste.html.erb | 6 +- app/views/imports/_csv_upload.html.erb | 2 +- app/views/imports/_form.html.erb | 2 +- app/views/imports/clean.html.erb | 1 - app/views/imports/configure.html.erb | 4 +- app/views/institutions/_form.html.erb | 3 +- app/views/layouts/_sidebar.html.erb | 11 +- app/views/merchants/_form.html.erb | 2 +- app/views/pages/dashboard.html.erb | 4 +- app/views/password_resets/edit.html.erb | 2 +- app/views/password_resets/new.html.erb | 2 +- app/views/passwords/edit.html.erb | 2 +- app/views/registrations/new.html.erb | 2 +- app/views/sessions/new.html.erb | 4 +- app/views/settings/hostings/show.html.erb | 2 +- app/views/settings/preferences/show.html.erb | 8 +- app/views/settings/profiles/show.html.erb | 4 +- app/views/shared/_money_field.html.erb | 24 +++ app/views/shared/_period_select.html.erb | 22 --- app/views/tag/deletions/new.html.erb | 17 +- app/views/tags/_form.html.erb | 2 +- app/views/transactions/_form.html.erb | 4 +- app/views/transactions/_pagination.html.erb | 1 - app/views/transactions/bulk_edit.html.erb | 2 +- .../transactions/searches/_form.html.erb | 4 +- config/locales/views/accounts/en.yml | 3 - config/locales/views/sessions/en.yml | 1 + config/locales/views/shared/en.yml | 2 + 50 files changed, 268 insertions(+), 488 deletions(-) delete mode 100644 app/helpers/application_form_builder.rb create mode 100644 app/helpers/styled_form_builder.rb delete mode 100644 app/javascript/controllers/select_controller.js create mode 100644 app/views/shared/_money_field.html.erb delete mode 100644 app/views/shared/_period_select.html.erb diff --git a/app/assets/stylesheets/application.tailwind.css b/app/assets/stylesheets/application.tailwind.css index 00fb5d95..357724d9 100644 --- a/app/assets/stylesheets/application.tailwind.css +++ b/app/assets/stylesheets/application.tailwind.css @@ -15,16 +15,16 @@ @layer components { .form-field { - @apply relative rounded-md border bg-white border-alpha-black-100 shadow-xs; + @apply flex flex-col gap-1 relative px-3 py-2 rounded-md border bg-white border-alpha-black-100 shadow-xs w-full; @apply focus-within:border-gray-900 focus-within:shadow-none focus-within:ring-4 focus-within:ring-gray-100; } .form-field__label { - @apply block px-3 pt-2 pb-0 text-xs text-gray-500; + @apply block text-xs text-gray-500; } .form-field__input { - @apply w-full border-none bg-transparent px-3 pt-1 pb-2 text-sm opacity-100; + @apply border-none bg-transparent text-sm opacity-100 w-full p-0; @apply focus:opacity-100 focus:outline-none focus:ring-0; @apply placeholder-shown:opacity-50; @apply disabled:opacity-50; @@ -58,6 +58,19 @@ background-image: url("data:image/svg+xml,%3csvg viewBox='0 0 16 16' fill='111827' xmlns='http://www.w3.org/2000/svg'%3e%3cpath d='M12.207 4.793a1 1 0 010 1.414l-5 5a1 1 0 01-1.414 0l-2-2a1 1 0 011.414-1.414L6.5 9.086l4.293-4.293a1 1 0 011.414 0z'/%3e%3c/svg%3e"); } + select[multiple="multiple"] { + @apply py-2 pr-2 space-y-0.5; + } + + select[multiple="multiple"] option { + @apply p-2 rounded-md; + } + + select[multiple="multiple"] option:checked { + @apply bg-gray-50; + @apply after:content-['\2713'] after:float-right after:text-gray-500; + } + .maybe-switch { @apply block bg-gray-100 w-9 h-5 rounded-full cursor-pointer; @apply after:content-[''] after:block after:absolute after:top-0.5 after:left-0.5 after:bg-white after:w-4 after:h-4 after:rounded-full after:transition-transform after:duration-300 after:ease-in-out; diff --git a/app/controllers/accounts_controller.rb b/app/controllers/accounts_controller.rb index e703dca4..9eaada46 100644 --- a/app/controllers/accounts_controller.rb +++ b/app/controllers/accounts_controller.rb @@ -19,6 +19,7 @@ class AccountsController < ApplicationController end def list + render layout: false end def new diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 500432af..84f8a8fe 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -2,8 +2,6 @@ class ApplicationController < ActionController::Base include Authentication, Invitable, SelfHostable include Pagy::Backend - default_form_builder ApplicationFormBuilder - # Only allow modern browsers supporting webp images, web push, badges, import maps, CSS nesting, and CSS :has. allow_browser versions: :modern end diff --git a/app/controllers/currencies_controller.rb b/app/controllers/currencies_controller.rb index d12f835a..71e085b7 100644 --- a/app/controllers/currencies_controller.rb +++ b/app/controllers/currencies_controller.rb @@ -1,6 +1,6 @@ class CurrenciesController < ApplicationController def show - @currency = Money::Currency.all_instances.find { |currency| currency.iso_code == params[:id] } - render json: { step: @currency.step, placeholder: Money.new(0, @currency).format } + currency = Money::Currency.all_instances.find { |currency| currency.iso_code == params[:id] } + render json: currency.as_json.merge({ step: currency.step }) end end diff --git a/app/helpers/application_form_builder.rb b/app/helpers/application_form_builder.rb deleted file mode 100644 index 5c0fb8b7..00000000 --- a/app/helpers/application_form_builder.rb +++ /dev/null @@ -1,149 +0,0 @@ -class ApplicationFormBuilder < ActionView::Helpers::FormBuilder - def initialize(object_name, object, template, options) - options[:html] ||= {} - options[:html][:class] ||= "space-y-4" - - super(object_name, object, template, options) - end - - (field_helpers - [ :label, :check_box, :radio_button, :fields_for, :fields, :hidden_field, :file_field ]).each do |selector| - class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1 - def #{selector}(method, options = {}) - default_options = { class: "form-field__input" } - merged_options = default_options.merge(options) - - return super(method, merged_options) unless options[:label] - - @template.form_field_tag do - label(method, *label_args(options)) + - super(method, merged_options.except(:label)) - end - end - RUBY_EVAL - end - - # See `Monetizable` concern, which adds a _money suffix to the attribute name - # For a monetized field, the setter will always be the attribute name without the _money suffix - def money_field(method, options = {}) - money = @object && @object.respond_to?(method) ? @object.send(method) : nil - raise ArgumentError, "The value of #{method} is not a Money object" unless money.is_a?(Money) || money.nil? - - money_amount_method = method.to_s.chomp("_money").to_sym - money_currency_method = :currency - - readonly_currency = options[:readonly_currency] || false - - currency = money&.currency || Money::Currency.new(Current.family.currency) || Money.default_currency - default_options = { - class: "form-field__input", - value: money&.amount, - "data-money-field-target" => "amount", - placeholder: Money.new(0, currency).format, - min: -99999999999999, - max: 99999999999999, - step: currency.step - } - - merged_options = default_options.merge(options) - - grouped_options = currency_options_for_select - selected_currency = money&.currency&.iso_code || currency.iso_code - - @template.form_field_tag data: { controller: "money-field" } do - (label(method, *label_args(options)).to_s if options[:label]) + - @template.tag.div(class: "flex items-center") do - number_field(money_amount_method, merged_options.except(:label)) + - grouped_select(money_currency_method, grouped_options, { selected: selected_currency }, disabled: readonly_currency, class: "ml-auto form-field__input w-fit pr-8", data: { "money-field-target" => "currency", action: "change->money-field#handleCurrencyChange" }) - end - end - end - - def radio_button(method, tag_value, options = {}) - default_options = { class: "form-field__radio" } - merged_options = default_options.merge(options) - super(method, tag_value, merged_options) - end - - def grouped_select(method, grouped_choices, options = {}, html_options = {}) - default_options = { class: "form-field__input" } - merged_html_options = default_options.merge(html_options) - - label_html = label(method, *label_args(options)).to_s if options[:label] - select_html = @template.grouped_collection_select(@object_name, method, grouped_choices, :last, :first, :last, :first, options, merged_html_options) - - @template.content_tag(:div, class: "flex items-center") do - label_html.to_s.html_safe + select_html - end - end - - def currency_select(method, options = {}, html_options = {}) - default_options = { class: "form-field__input" } - merged_options = default_options.merge(html_options) - - choices = currency_options_for_select - - return @template.grouped_collection_select(@object_name, method, choices, :last, :first, :last, :first, options, merged_options) unless options[:label] - - @template.form_field_tag do - label(method, *label_args(options)) + - @template.grouped_collection_select(@object_name, method, choices, :last, :first, :last, :first, options, merged_options.except(:label)) - end - end - - def select(method, choices, options = {}, html_options = {}) - default_options = { class: "form-field__input" } - merged_options = default_options.merge(html_options) - - return super(method, choices, options, merged_options) unless options[:label] - - @template.form_field_tag do - label(method, *label_args(options)) + - super(method, choices, options, merged_options.except(:label)) - end - end - - def collection_select(method, collection, value_method, text_method, options = {}, html_options = {}) - default_options = { class: "form-field__input" } - merged_options = default_options.merge(html_options) - - return super(method, collection, value_method, text_method, options, merged_options) unless options[:label] - - @template.form_field_tag do - label(method, *label_args(options)) + - super(method, collection, value_method, text_method, options, merged_options.except(:label)) - end - end - - def submit(value = nil, options = {}) - value, options = nil, value if value.is_a?(Hash) - default_options = { class: "form-field__submit" } - merged_options = default_options.merge(options) - super(value, merged_options) - end - - private - - def currency_options_for_select - popular_currencies = Money::Currency.popular.map { |currency| [ currency.iso_code, currency.iso_code ] } - all_currencies = Money::Currency.all_instances.map { |currency| [ currency.iso_code, currency.iso_code ] } - all_other_currencies = all_currencies.reject { |c| popular_currencies.map(&:last).include?(c.last) }.sort_by(&:last) - - { - I18n.t("accounts.new.currency.popular") => popular_currencies, - I18n.t("accounts.new.currency.all_others") => all_other_currencies - } - end - - def label_args(options) - case options[:label] - when Array - options[:label] - when String - [ options[:label], { class: "form-field__label" } ] - when Hash - [ nil, options[:label] ] - else - [ nil, { class: "form-field__label" } ] - end - end -end diff --git a/app/helpers/forms_helper.rb b/app/helpers/forms_helper.rb index 86968e02..356f928c 100644 --- a/app/helpers/forms_helper.rb +++ b/app/helpers/forms_helper.rb @@ -1,7 +1,12 @@ module FormsHelper + def styled_form_with(**options, &block) + options[:builder] = StyledFormBuilder + form_with(**options, &block) + end + def form_field_tag(options = {}, &block) options[:class] = [ "form-field", options[:class] ].compact.join(" ") - tag.div **options, &block + tag.div(**options, &block) end def radio_tab_tag(form:, name:, value:, label:, icon:, checked: false, disabled: false) @@ -11,23 +16,59 @@ module FormsHelper end end - def selectable_categories - Current.family.categories.alphabetically + def period_select(form:, selected:, classes: "border border-alpha-black-100 shadow-xs rounded-lg text-sm pr-7 cursor-pointer text-gray-900 focus:outline-none focus:ring-0") + periods_for_select = [ [ "7D", "last_7_days" ], [ "1M", "last_30_days" ], [ "1Y", "last_365_days" ], [ "All", "all" ] ] + form.select(:period, periods_for_select, { selected: selected }, class: classes, data: { "auto-submit-form-target": "auto" }) end - def selectable_merchants - Current.family.merchants.alphabetically + def money_with_currency_field(form, money_method, options = {}) + render partial: "shared/money_field", locals: { + form: form, + money_method: money_method, + disable_currency: options[:disable_currency] || false, + hide_currency: options[:hide_currency] || false, + label: options[:label] || "Amount" + } end - def selectable_accounts - Current.family.accounts.alphabetically + def money_field(form, method, options = {}) + value = form.object.send(method) + + currency = value&.currency || Money::Currency.new(options[:default_currency] || "USD") + + # See "Monetizable" concern + money_amount_method = method.to_s.chomp("_money").to_sym + + money_options = { + value: value&.amount, + placeholder: 100, + min: -99999999999999, + max: 99999999999999, + step: currency.step + } + + merged_options = options.merge(money_options) + + form.number_field money_amount_method, merged_options end - def selectable_tags - Current.family.tags.alphabetically.pluck(:name, :id) + def currency_select_full(form, method, options = {}, html_options = {}, &block) + choices = currencies_for_select.map { |currency| [ "#{currency.name} (#{currency.iso_code})", currency.iso_code ] } + form.select method, choices, options, html_options, &block + end + + def currency_select(form, method, options = {}, html_options = {}, &block) + choices = currencies_for_select.map(&:iso_code) + form.select method, choices, options, html_options, &block end private + + def currencies_for_select + Money::Currency.all_instances + .sort_by(&:priority) + end + def radio_tab_contents(label:, icon:) tag.div(class: "flex px-4 py-1 rounded-lg items-center space-x-2 justify-center text-gray-400 group-has-[:checked]:bg-white group-has-[:checked]:text-gray-800 group-has-[:checked]:shadow-sm") do concat lucide_icon(icon, class: "w-5 h-5") diff --git a/app/helpers/styled_form_builder.rb b/app/helpers/styled_form_builder.rb new file mode 100644 index 00000000..01bf22e0 --- /dev/null +++ b/app/helpers/styled_form_builder.rb @@ -0,0 +1,55 @@ +class StyledFormBuilder < ActionView::Helpers::FormBuilder + # Fields that visually inherit from "text field" + class_attribute :text_field_helpers, default: field_helpers - [ :label, :check_box, :radio_button, :fields_for, :fields, :hidden_field, :file_field ] + + # Wraps "text" inputs with custom structure + base styles + text_field_helpers.each do |selector| + class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1 + def #{selector}(method, options = {}) + input_html = label_html(method, options) + super(method, merged_options(options)) + input_html = apply_form_field_wrapper(input_html) unless options[:inline] + input_html + end + RUBY_EVAL + end + + def radio_button(method, tag_value, options = {}) + super(method, tag_value, merged_options(options, "form-field__radio")) + end + + def select(method, choices, options = {}, html_options = {}) + input_html = label_html(method, options) + super(method, choices, options, merged_options(html_options)) + input_html = apply_form_field_wrapper(input_html, class: "pr-0") unless options[:inline] + input_html + end + + def collection_select(method, collection, value_method, text_method, options = {}, html_options = {}) + input_html = label_html(method, options) + super(method, collection, value_method, text_method, options, merged_options(html_options)) + input_html = apply_form_field_wrapper(input_html, class: "pr-0") unless options[:inline] + input_html + end + + def submit(value = nil, options = {}) + value, options = nil, value if value.is_a?(Hash) + super(value, merged_options(options, "form-field__submit")) + end + + private + + def apply_form_field_wrapper(input_html, **options) + @template.form_field_tag(**options) do + input_html + end + end + + def merged_options(options, default_class = "form-field__input") + combined_classes = options.fetch(:class, "") + " #{default_class}" + style_options = { class: combined_classes } + non_custom_options = options.except(:class, :label, :inline) + style_options.merge(non_custom_options) + end + + def label_html(method, options) + options[:label] ? label(method, options[:label], class: "form-field__label") : "".html_safe + end +end diff --git a/app/javascript/controllers/money_field_controller.js b/app/javascript/controllers/money_field_controller.js index 09b6b67a..26219681 100644 --- a/app/javascript/controllers/money_field_controller.js +++ b/app/javascript/controllers/money_field_controller.js @@ -4,17 +4,23 @@ import { CurrenciesService } from "services/currencies_service"; // Connects to data-controller="money-field" // when currency select change, update the input value with the correct placeholder and step export default class extends Controller { - static targets = ["amount", "currency"]; + static targets = ["amount", "currency", "symbol"]; - handleCurrencyChange() { - const selectedCurrency = event.target.value; + handleCurrencyChange(e) { + const selectedCurrency = e.target.value; this.updateAmount(selectedCurrency); } updateAmount(currency) { - (new CurrenciesService).get(currency).then((data) => { - this.amountTarget.placeholder = data.placeholder; - this.amountTarget.step = data.step; + (new CurrenciesService).get(currency).then((currency) => { + console.log(currency) + this.amountTarget.step = currency.step; + + if (isFinite(this.amountTarget.value)) { + this.amountTarget.value = parseFloat(this.amountTarget.value).toFixed(currency.default_precision) + } + + this.symbolTarget.innerText = currency.symbol; }); } } \ No newline at end of file diff --git a/app/javascript/controllers/select_controller.js b/app/javascript/controllers/select_controller.js deleted file mode 100644 index 4d8bb1a8..00000000 --- a/app/javascript/controllers/select_controller.js +++ /dev/null @@ -1,179 +0,0 @@ -import { Controller } from "@hotwired/stimulus"; - -/** - * A custom "select" element that follows accessibility patterns of a native select element. - * - * - If you need to display arbitrary content including non-clickable items, links, buttons, and forms, use the "popover" controller instead. - */ -export default class extends Controller { - static classes = ["active"]; - static targets = ["option", "button", "list", "input", "buttonText"]; - static values = { selected: String }; - - initialize() { - this.show = false; - - const selectedElement = this.optionTargets.find( - (option) => option.dataset.value === this.selectedValue - ); - if (selectedElement) { - this.updateAriaAttributesAndClasses(selectedElement); - this.syncButtonTextWithInput(); - } - } - - connect() { - this.syncButtonTextWithInput(); - if (this.hasButtonTarget) { - this.buttonTarget.addEventListener("click", this.toggleList); - } - this.element.addEventListener("keydown", this.handleKeydown); - document.addEventListener("click", this.handleOutsideClick); - this.element.addEventListener("turbo:load", this.handleTurboLoad); - } - - disconnect() { - this.element.removeEventListener("keydown", this.handleKeydown); - document.removeEventListener("click", this.handleOutsideClick); - this.element.removeEventListener("turbo:load", this.handleTurboLoad); - - if (this.hasButtonTarget) { - this.buttonTarget.removeEventListener("click", this.toggleList); - } - } - - selectedValueChanged() { - this.syncButtonTextWithInput(); - } - - handleOutsideClick = (event) => { - if (this.show && !this.element.contains(event.target)) { - this.close(); - } - }; - - handleTurboLoad = () => { - this.close(); - this.syncButtonTextWithInput(); - }; - - handleKeydown = (event) => { - switch (event.key) { - case " ": - case "Enter": - event.preventDefault(); // Prevent the default action to avoid scrolling - if ( - this.hasButtonTarget && - document.activeElement === this.buttonTarget - ) { - this.toggleList(); - } else { - this.selectOption(event); - } - break; - case "ArrowDown": - event.preventDefault(); // Prevent the default action to avoid scrolling - this.focusNextOption(); - break; - case "ArrowUp": - event.preventDefault(); // Prevent the default action to avoid scrolling - this.focusPreviousOption(); - break; - case "Escape": - this.close(); - if (this.hasButtonTarget) { - this.buttonTarget.focus(); // Bring focus back to the button - } - break; - case "Tab": - this.close(); - break; - } - }; - - focusNextOption() { - this.focusOptionInDirection(1); - } - - focusPreviousOption() { - this.focusOptionInDirection(-1); - } - - focusOptionInDirection(direction) { - const currentFocusedIndex = this.optionTargets.findIndex( - (option) => option === document.activeElement - ); - const optionsCount = this.optionTargets.length; - const nextIndex = - (currentFocusedIndex + direction + optionsCount) % optionsCount; - this.optionTargets[nextIndex].focus(); - } - - toggleList = () => { - if (!this.hasButtonTarget) return; // Ensure button target is present before toggling - - this.show = !this.show; - this.listTarget.classList.toggle("hidden", !this.show); - this.buttonTarget.setAttribute("aria-expanded", this.show.toString()); - - if (this.show) { - // Focus the first option or the selected option when the list is shown - const selectedOption = this.optionTargets.find( - (option) => option.getAttribute("aria-selected") === "true" - ); - (selectedOption || this.optionTargets[0]).focus(); - } - }; - - close() { - if (this.hasButtonTarget) { - this.show = false; - this.listTarget.classList.add("hidden"); - this.buttonTarget.setAttribute("aria-expanded", "false"); - } - } - - selectOption(event) { - const selectedOption = - event.type === "keydown" ? document.activeElement : event.currentTarget; - this.updateAriaAttributesAndClasses(selectedOption); - if (this.inputTarget.value !== selectedOption.getAttribute("data-value")) { - this.updateInputValueAndEmitEvent(selectedOption); - } - this.close(); // Close the list after selection - } - - updateAriaAttributesAndClasses(selectedOption) { - this.optionTargets.forEach((option) => { - option.setAttribute("aria-selected", "false"); - option.setAttribute("tabindex", "-1"); - option.classList.remove(...this.activeClasses); - }); - selectedOption.classList.add(...this.activeClasses); - selectedOption.setAttribute("aria-selected", "true"); - selectedOption.focus(); - } - - updateInputValueAndEmitEvent(selectedOption) { - // Update the hidden input's value - const selectedValue = selectedOption.getAttribute("data-value"); - this.inputTarget.value = selectedValue; - this.syncButtonTextWithInput(); - - // Emit an input event for auto-submit functionality - const inputEvent = new Event("input", { - bubbles: true, - cancelable: true, - }); - this.inputTarget.dispatchEvent(inputEvent); - } - - syncButtonTextWithInput() { - const matchingOption = this.optionTargets.find( - (option) => option.getAttribute("data-value") === this.inputTarget.value - ); - if (matchingOption && this.hasButtonTextTarget) { - this.buttonTextTarget.textContent = matchingOption.textContent.trim(); - } - } -} diff --git a/app/views/account/entries/_selection_bar.html.erb b/app/views/account/entries/_selection_bar.html.erb index e3003813..3f72eaab 100644 --- a/app/views/account/entries/_selection_bar.html.erb +++ b/app/views/account/entries/_selection_bar.html.erb @@ -9,7 +9,6 @@ <%= turbo_frame_tag "bulk_transaction_edit_drawer" %> <%= form_with url: mark_transfers_transactions_path, - builder: ActionView::Helpers::FormBuilder, scope: "bulk_update", data: { turbo_frame: "_top", @@ -36,7 +35,7 @@ <%= lucide_icon "pencil-line", class: "w-5 group-hover:text-white" %> <% end %> - <%= form_with url: bulk_delete_transactions_path, builder: ActionView::Helpers::FormBuilder, data: { turbo_confirm: true, turbo_frame: "_top" } do %> + <%= form_with url: bulk_delete_transactions_path, data: { turbo_confirm: true, turbo_frame: "_top" } do %> diff --git a/app/views/account/entries/entryables/transaction/_show.html.erb b/app/views/account/entries/entryables/transaction/_show.html.erb index 1ff9d7ba..ebf603ba 100644 --- a/app/views/account/entries/entryables/transaction/_show.html.erb +++ b/app/views/account/entries/entryables/transaction/_show.html.erb @@ -27,30 +27,28 @@
- <%= form_with model: [account, entry], url: account_entry_path(account, entry), html: { data: { controller: "auto-submit-form" } } do |f| %> -
- <%= f.text_field :name, label: t(".name_label"), "data-auto-submit-form-target": "auto" %> - <% unless entry.marked_as_transfer? %> -
-
- <%= f.select :nature, [["Expense", "expense"], ["Income", "income"]], { label: t(".nature"), selected: entry.amount.negative? ? "income" : "expense" }, "data-auto-submit-form-target": "auto" %> -
-
- <%= f.number_field :amount, value: entry.amount.abs, label: t(".amount"), step: "0.01", "data-auto-submit-form-target": "auto", "data-autosubmit-trigger-event": "change" %> -
+ <%= styled_form_with model: [account, entry], url: account_entry_path(account, entry), class: "space-y-2", data: { controller: "auto-submit-form" } do |f| %> + <%= f.text_field :name, label: t(".name_label"), "data-auto-submit-form-target": "auto" %> + <% unless entry.marked_as_transfer? %> +
+
+ <%= f.select :nature, [["Expense", "expense"], ["Income", "income"]], { label: t(".nature"), selected: entry.amount.negative? ? "income" : "expense" }, "data-auto-submit-form-target": "auto" %>
- <% end %> - <%= f.date_field :date, label: t(".date_label"), max: Date.current, "data-auto-submit-form-target": "auto" %> +
+ <%= f.number_field :amount, value: entry.amount.abs, label: t(".amount"), step: "0.01", "data-auto-submit-form-target": "auto", "data-autosubmit-trigger-event": "change" %> +
+
+ <% end %> + <%= f.date_field :date, label: t(".date_label"), max: Date.current, "data-auto-submit-form-target": "auto" %> - <%= f.fields_for :entryable do |ef| %> - <% unless entry.marked_as_transfer? %> - <%= ef.collection_select :category_id, selectable_categories, :id, :name, { prompt: t(".category_placeholder"), label: t(".category_label"), class: "text-gray-400" }, "data-auto-submit-form-target": "auto" %> - <%= ef.collection_select :merchant_id, selectable_merchants, :id, :name, { prompt: t(".merchant_placeholder"), label: t(".merchant_label"), class: "text-gray-400" }, "data-auto-submit-form-target": "auto" %> - <% end %> + <%= f.fields_for :entryable do |ef| %> + <% unless entry.marked_as_transfer? %> + <%= ef.collection_select :category_id, Current.family.categories.alphabetically, :id, :name, { prompt: t(".category_placeholder"), label: t(".category_label"), class: "text-gray-400" }, "data-auto-submit-form-target": "auto" %> + <%= ef.collection_select :merchant_id, Current.family.merchants.alphabetically, :id, :name, { prompt: t(".merchant_placeholder"), label: t(".merchant_label"), class: "text-gray-400" }, "data-auto-submit-form-target": "auto" %> <% end %> + <% end %> - <%= f.collection_select :account_id, selectable_accounts, :id, :name, { prompt: t(".account_placeholder"), label: t(".account_label"), class: "text-gray-500" }, { class: "form-field__input cursor-not-allowed text-gray-400", disabled: "disabled" } %> -
+ <%= f.collection_select :account_id, Current.family.accounts.alphabetically, :id, :name, { prompt: t(".account_placeholder"), label: t(".account_label"), class: "text-gray-500" }, { class: "form-field__input cursor-not-allowed text-gray-400", disabled: "disabled" } %> <% end %>
@@ -61,12 +59,12 @@ <%= lucide_icon "chevron-down", class: "group-open:transform group-open:rotate-180 text-gray-500 w-5" %> -
- <%= form_with model: [account, entry], url: account_entry_path(account, entry), html: { data: { controller: "auto-submit-form" } } do |f| %> +
+ <%= styled_form_with model: [account, entry], url: account_entry_path(account, entry), class: "space-y-2", data: { controller: "auto-submit-form" } do |f| %> <%= f.fields_for :entryable do |ef| %> <%= ef.select :tag_ids, - options_for_select(selectable_tags, transaction.tag_ids), + options_for_select(Current.family.tags.alphabetically.pluck(:name, :id), transaction.tag_ids), { multiple: true, label: t(".tags_label"), @@ -86,7 +84,7 @@
- <%= form_with model: [account, entry], url: account_entry_path(account, entry), html: { class: "p-3 space-y-3", data: { controller: "auto-submit-form" } } do |f| %> + <%= styled_form_with model: [account, entry], url: account_entry_path(account, entry), class: "p-3 space-y-3", data: { controller: "auto-submit-form" } do |f| %> <%= f.fields_for :entryable do |ef| %>
diff --git a/app/views/account/entries/entryables/transaction/_transaction.html.erb b/app/views/account/entries/entryables/transaction/_transaction.html.erb index 0c87eac2..851af438 100644 --- a/app/views/account/entries/entryables/transaction/_transaction.html.erb +++ b/app/views/account/entries/entryables/transaction/_transaction.html.erb @@ -31,7 +31,7 @@ <% if unconfirmed_transfer?(entry) %> <% if editable %> - <%= form_with url: unmark_transfers_transactions_path, builder: ActionView::Helpers::FormBuilder, class: "flex items-center", data: { + <%= form_with url: unmark_transfers_transactions_path, class: "flex items-center", data: { turbo_confirm: { title: t(".remove_transfer"), body: t(".remove_transfer_body"), diff --git a/app/views/account/entries/entryables/valuation/_form.html.erb b/app/views/account/entries/entryables/valuation/_form.html.erb index 0123e61d..c7b3abff 100644 --- a/app/views/account/entries/entryables/valuation/_form.html.erb +++ b/app/views/account/entries/entryables/valuation/_form.html.erb @@ -1,8 +1,7 @@ <%# locals: (entry:) %> <%= form_with model: [entry.account, entry], data: { turbo_frame: "_top" }, - url: entry.new_record? ? account_entries_path(entry.account) : account_entry_path(entry.account, entry), - builder: ActionView::Helpers::FormBuilder do |f| %> + url: entry.new_record? ? account_entries_path(entry.account) : account_entry_path(entry.account, entry) do |f| %>
diff --git a/app/views/account/transfers/_form.html.erb b/app/views/account/transfers/_form.html.erb index ddd89c79..2f58b08e 100644 --- a/app/views/account/transfers/_form.html.erb +++ b/app/views/account/transfers/_form.html.erb @@ -1,4 +1,4 @@ -<%= form_with model: transfer, data: { turbo_frame: "_top" } do |f| %> +<%= styled_form_with model: transfer, class: "space-y-4", data: { turbo_frame: "_top" } do |f| %>
<%= link_to new_transaction_path(nature: "expense"), data: { turbo_frame: :modal }, class: "flex px-4 py-1 rounded-lg items-center space-x-2 justify-center text-gray-400" do %> @@ -22,7 +22,8 @@ <%= f.text_field :name, value: transfer.name, label: t(".description"), placeholder: t(".description_placeholder"), required: true %> <%= f.collection_select :from_account_id, Current.family.accounts.alphabetically, :id, :name, { prompt: t(".select_account"), label: t(".from") }, required: true %> <%= f.collection_select :to_account_id, Current.family.accounts.alphabetically, :id, :name, { prompt: t(".select_account"), label: t(".to") }, required: true %> - <%= f.money_field :amount_money, label: t(".amount"), required: true %> + <%= money_field f, :amount_money, label: t(".amount"), required: true %> + <%= f.hidden_field :currency, value: Current.family.currency %> <%= f.date_field :date, value: transfer.date, label: t(".date"), required: true, max: Date.current %>
diff --git a/app/views/accounts/_account.html.erb b/app/views/accounts/_account.html.erb index 715570c7..8a343360 100644 --- a/app/views/accounts/_account.html.erb +++ b/app/views/accounts/_account.html.erb @@ -17,7 +17,6 @@ <%= form_with model: account, namespace: account.id, - builder: ActionView::Helpers::FormBuilder, data: { controller: "auto-submit-form", turbo_frame: "_top" } do |form| %>
<%= form.check_box :is_active, { class: "sr-only peer", data: { "auto-submit-form-target": "auto" } } %> diff --git a/app/views/accounts/edit.html.erb b/app/views/accounts/edit.html.erb index 3660aa3f..0b14157e 100644 --- a/app/views/accounts/edit.html.erb +++ b/app/views/accounts/edit.html.erb @@ -5,9 +5,9 @@ <%= lucide_icon "x", class: "w-5 h-5 text-gray-500", data: { action: "click->modal#close" } %> - <%= form_with model: @account, data: { turbo_frame: "_top" } do |f| %> + <%= styled_form_with model: @account, class: "space-y-4", data: { turbo_frame: "_top" } do |f| %> <%= f.text_field :name, label: t(".name") %> - <%= f.money_field :balance_money, label: t(".balance"), readonly_currency: true %> + <%= money_with_currency_field f, :balance_money, label: t(".balance"), disable_currency: true %>
<%= f.collection_select :institution_id, Current.family.institutions.alphabetically, :id, :name, { include_blank: t(".ungrouped"), label: t(".institution") } %> diff --git a/app/views/accounts/list.html.erb b/app/views/accounts/list.html.erb index fe2a7d33..6075fd14 100644 --- a/app/views/accounts/list.html.erb +++ b/app/views/accounts/list.html.erb @@ -1,5 +1,5 @@ - +<%= turbo_frame_tag "account-list" do %> <% account_groups(period: @period).each do |group| %> <%= render "accounts/account_list", group: group %> <% end %> - +<% end %> diff --git a/app/views/accounts/new.html.erb b/app/views/accounts/new.html.erb index 1e42a5d6..04590414 100644 --- a/app/views/accounts/new.html.erb +++ b/app/views/accounts/new.html.erb @@ -73,13 +73,13 @@ <% end %> Add <%= @account.accountable.model_name.human.downcase %>
- <%= form_with model: @account, url: accounts_path, scope: :account, html: { class: "m-5 mt-1 flex flex-col justify-between grow", data: { turbo: false } } do |f| %> + <%= styled_form_with model: @account, url: accounts_path, scope: :account, class: "m-5 mt-1 flex flex-col justify-between grow", data: { turbo: false } do |f| %>
<%= f.hidden_field :accountable_type %> <%= f.text_field :name, placeholder: t(".name.placeholder"), required: "required", label: t(".name.label"), autofocus: true %> <%= f.collection_select :institution_id, Current.family.institutions.alphabetically, :id, :name, { include_blank: t(".ungrouped"), label: t(".institution") } %> <%= render "accounts/accountables/#{permitted_accountable_partial(@account.accountable_type)}", f: f %> - <%= f.money_field :balance_money, label: t(".balance"), required: "required" %> + <%= money_with_currency_field f, :balance_money, label: t(".balance"), required: "required" %>
<%= check_box_tag :add_start_values, class: "maybe-checkbox maybe-checkbox--light peer mb-1" %> diff --git a/app/views/accounts/show.html.erb b/app/views/accounts/show.html.erb index e1712d53..de2a57e1 100644 --- a/app/views/accounts/show.html.erb +++ b/app/views/accounts/show.html.erb @@ -69,8 +69,8 @@ <%= tag.span period_label(@period), class: "text-gray-500" %>
- <%= form_with url: account_path(@account), method: :get, class: "flex items-center gap-4", data: { controller: "auto-submit-form" } do %> - <%= render partial: "shared/period_select", locals: { value: @period.name } %> + <%= form_with url: account_path(@account), method: :get, data: { controller: "auto-submit-form" } do |form| %> + <%= period_select form: form, selected: @period.name %> <% end %>
diff --git a/app/views/accounts/summary.html.erb b/app/views/accounts/summary.html.erb index e8603f8d..4dc83eaf 100644 --- a/app/views/accounts/summary.html.erb +++ b/app/views/accounts/summary.html.erb @@ -10,10 +10,10 @@
<%= render partial: "shared/value_heading", locals: { label: "Assets", - period: @period, - value: Current.family.assets, - trend: @asset_series.trend - } %> + period: @period, + value: Current.family.assets, + trend: @asset_series.trend + } %>
<%= render partial: "shared/value_heading", locals: { label: "Liabilities", - period: @period, - size: "md", - value: Current.family.liabilities, - trend: @liability_series.trend - } %> + period: @period, + size: "md", + value: Current.family.liabilities, + trend: @liability_series.trend + } %>

<%= t(".new") %>

<% end %> - <%= form_with url: summary_accounts_path, method: :get, class: "flex items-center gap-4", data: { controller: "auto-submit-form" } do %> - <%= render partial: "shared/period_select", locals: { value: @period.name } %> + <%= form_with url: summary_accounts_path, method: :get, data: { controller: "auto-submit-form" } do |form| %> + <%= period_select form: form, selected: @period.name %> <% end %>
@@ -64,8 +64,8 @@ <%= lucide_icon("plus", class: "w-5 h-5 text-gray-500") %>

<%= t(".new") %>

<% end %> - <%= form_with url: summary_accounts_path, method: :get, class: "flex items-center gap-4", data: { controller: "auto-submit-form" } do %> - <%= render partial: "shared/period_select", locals: { value: @period.name } %> + <%= form_with url: summary_accounts_path, method: :get, data: { controller: "auto-submit-form" } do |form| %> + <%= period_select form: form, selected: @period.name %> <% end %>
diff --git a/app/views/categories/_form.html.erb b/app/views/categories/_form.html.erb index b5fba349..0321b2bd 100644 --- a/app/views/categories/_form.html.erb +++ b/app/views/categories/_form.html.erb @@ -1,4 +1,4 @@ -<%= form_with model: category, data: { turbo: false } do |form| %> +<%= styled_form_with model: category, data: { turbo: false } do |form| %>
diff --git a/app/views/category/deletions/new.html.erb b/app/views/category/deletions/new.html.erb index 53981162..de800518 100644 --- a/app/views/category/deletions/new.html.erb +++ b/app/views/category/deletions/new.html.erb @@ -11,22 +11,23 @@

- <%= form_with url: category_deletions_path(@category), - data: { - turbo: false, - controller: "deletion", - deletion_dangerous_action_class: "form-field__submit bg-white text-red-600 border hover:bg-red-50", - deletion_safe_action_class: "form-field__submit border border-transparent", - deletion_submit_text_when_not_replacing_value: t(".delete_and_leave_uncategorized", category_name: @category.name), - deletion_submit_text_when_replacing_value: t(".delete_and_recategorize", category_name: @category.name) } do |f| %> + <%= styled_form_with url: category_deletions_path(@category), + class: "space-y-4", + data: { + turbo: false, + controller: "deletion", + deletion_dangerous_action_class: "form-field__submit bg-white text-red-600 border hover:bg-red-50", + deletion_safe_action_class: "form-field__submit border border-transparent", + deletion_submit_text_when_not_replacing_value: t(".delete_and_leave_uncategorized", category_name: @category.name), + deletion_submit_text_when_replacing_value: t(".delete_and_recategorize", category_name: @category.name) } do |f| %> <%= f.collection_select :replacement_category_id, Current.family.categories.alphabetically.without(@category), - :id, :name, - { prompt: t(".replacement_category_prompt"), label: t(".category") }, + :id, :name, + { prompt: t(".replacement_category_prompt"), label: t(".category") }, { data: { deletion_target: "replacementField", action: "deletion#updateSubmitButton" } } %> <%= f.submit t(".delete_and_leave_uncategorized", category_name: @category.name), - class: "form-field__submit bg-white text-red-600 border hover:bg-red-50", + class: "form-field__submit bg-white text-red-600 border hover:bg-red-50", data: { deletion_target: "submitButton" } %> <% end %> diff --git a/app/views/imports/_csv_paste.html.erb b/app/views/imports/_csv_paste.html.erb index 6006e874..59257173 100644 --- a/app/views/imports/_csv_paste.html.erb +++ b/app/views/imports/_csv_paste.html.erb @@ -1,11 +1,9 @@ -<%= form_with model: @import, url: load_import_path(@import) do |form| %> -
+<%= styled_form_with model: @import, url: load_import_path(@import), class: "space-y-4" do |form| %> <%= form.text_area :raw_csv_str, rows: 10, required: true, placeholder: "Paste your CSV file contents here", - class: "rounded-md w-full border text-sm border-alpha-black-100 bg-white placeholder:text-gray-400 p-4" %> -
+ class: "rounded-md w-full border text-sm border-alpha-black-100 bg-white placeholder:text-gray-400" %> <%= form.submit t(".next"), class: "px-4 py-2 mb-4 block w-full rounded-lg bg-gray-900 text-white text-sm font-medium", data: { turbo_confirm: (@import.raw_csv_str? ? { title: t(".confirm_title"), body: t(".confirm_body"), accept: t(".confirm_accept") } : nil) } %> <% end %> diff --git a/app/views/imports/_csv_upload.html.erb b/app/views/imports/_csv_upload.html.erb index 69d598ef..ec23db31 100644 --- a/app/views/imports/_csv_upload.html.erb +++ b/app/views/imports/_csv_upload.html.erb @@ -1,4 +1,4 @@ -<%= form_with model: @import, url: upload_import_path(@import), class: "dropzone", data: { controller: "csv-upload" }, method: :patch, multipart: true do |form| %> +<%= styled_form_with model: @import, url: upload_import_path(@import), class: "dropzone space-y-4", data: { controller: "csv-upload" }, method: :patch, multipart: true do |form| %>
diff --git a/app/views/merchants/_form.html.erb b/app/views/merchants/_form.html.erb index aebeef4e..9e78369c 100644 --- a/app/views/merchants/_form.html.erb +++ b/app/views/merchants/_form.html.erb @@ -1,6 +1,6 @@ <% is_editing = @merchant.id.present? %>
- <%= form_with model: @merchant, url: is_editing ? merchant_path(@merchant) : merchants_path, method: is_editing ? :patch : :post, scope: :merchant, data: { turbo: false } do |f| %> + <%= styled_form_with model: @merchant, url: is_editing ? merchant_path(@merchant) : merchants_path, method: is_editing ? :patch : :post, scope: :merchant, class: "space-y-4", data: { turbo: false } do |f| %>
<%= render partial: "merchants/avatar", locals: { merchant: } %> diff --git a/app/views/pages/dashboard.html.erb b/app/views/pages/dashboard.html.erb index 66b24c9d..27bf0c57 100644 --- a/app/views/pages/dashboard.html.erb +++ b/app/views/pages/dashboard.html.erb @@ -26,8 +26,8 @@ trend: @net_worth_series.trend } %>
- <%= form_with url: root_path, method: :get, class: "flex items-center gap-4", data: { controller: "auto-submit-form" } do %> - <%= render partial: "shared/period_select", locals: { value: @period.name } %> + <%= form_with url: root_path, method: :get, class: "flex items-center gap-4", data: { controller: "auto-submit-form" } do |form| %> + <%= period_select form: form, selected: @period.name %> <% end %>
<%= render partial: "pages/dashboard/net_worth_chart", locals: { series: @net_worth_series } %> diff --git a/app/views/password_resets/edit.html.erb b/app/views/password_resets/edit.html.erb index 1a4b6260..42c87ffe 100644 --- a/app/views/password_resets/edit.html.erb +++ b/app/views/password_resets/edit.html.erb @@ -2,7 +2,7 @@ header_title t(".title") %> -<%= form_with model: @user, url: password_reset_path(token: params[:token]), method: :patch, html: {class: "space-y-6"} do |form| %> +<%= styled_form_with model: @user, url: password_reset_path(token: params[:token]), method: :patch, class: "space-y-4" do |form| %> <%= auth_messages form %>
diff --git a/app/views/password_resets/new.html.erb b/app/views/password_resets/new.html.erb index 2974c80e..f2b455a0 100644 --- a/app/views/password_resets/new.html.erb +++ b/app/views/password_resets/new.html.erb @@ -2,7 +2,7 @@ header_title t(".title") %> -<%= form_with url: password_reset_path do |form| %> +<%= styled_form_with url: password_reset_path, class: "space-y-4" do |form| %> <%= auth_messages form %> <%= form.email_field :email, label: true, autofocus: false, autocomplete: "email", required: "required", placeholder: "you@example.com" %> diff --git a/app/views/passwords/edit.html.erb b/app/views/passwords/edit.html.erb index 0bbfa827..a89cabde 100644 --- a/app/views/passwords/edit.html.erb +++ b/app/views/passwords/edit.html.erb @@ -1,6 +1,6 @@

<% t(".title") %>

-<%= form_with model: Current.user, url: password_path do |form| %> +<%= styled_form_with model: Current.user, url: password_path, class: "space-y-4" do |form| %> <%= auth_messages form %>
diff --git a/app/views/registrations/new.html.erb b/app/views/registrations/new.html.erb index bfd7289f..a5714243 100644 --- a/app/views/registrations/new.html.erb +++ b/app/views/registrations/new.html.erb @@ -1,7 +1,7 @@ <% header_title t(".title") %> -<%= form_with model: @user, url: registration_path do |form| %> +<%= styled_form_with model: @user, url: registration_path, class: "space-y-4" do |form| %> <%= auth_messages form %> <%= form.email_field :email, autofocus: false, autocomplete: "email", required: "required", placeholder: "you@example.com", label: true %> <%= form.password_field :password, autocomplete: "new-password", required: "required", label: true %> diff --git a/app/views/sessions/new.html.erb b/app/views/sessions/new.html.erb index da0a3264..67a9d7ed 100644 --- a/app/views/sessions/new.html.erb +++ b/app/views/sessions/new.html.erb @@ -2,12 +2,12 @@ header_title t(".title") %> -<%= form_with url: session_path do |form| %> +<%= styled_form_with url: session_path, class: "space-y-4" do |form| %> <%= auth_messages form %> <%= form.email_field :email, label: t(".email"), autofocus: false, autocomplete: "email", required: "required", placeholder: t(".email_placeholder") %> - <%= form.password_field :password, label: true, required: "required" %> + <%= form.password_field :password, label: t(".password"), required: "required" %> <%= form.submit t(".submit") %> <% end %> diff --git a/app/views/settings/hostings/show.html.erb b/app/views/settings/hostings/show.html.erb index 37c1f3f6..4b9dff76 100644 --- a/app/views/settings/hostings/show.html.erb +++ b/app/views/settings/hostings/show.html.erb @@ -4,7 +4,7 @@

<%= t(".page_title") %>

<%= settings_section title: t(".general_settings_title") do %> - <%= form_with model: Setting.new, url: settings_hosting_path, method: :patch, local: true, html: { class: "space-y-6", data: { controller: "auto-submit-form", "auto-submit-form-trigger-event-value" => "blur" } } do |form| %> + <%= styled_form_with model: Setting.new, url: settings_hosting_path, method: :patch, local: true, class: "space-y-6", data: { controller: "auto-submit-form", "auto-submit-form-trigger-event-value" => "blur" } do |form| %> <% if ENV["HOSTING_PLATFORM"] == "render" %>
diff --git a/app/views/settings/preferences/show.html.erb b/app/views/settings/preferences/show.html.erb index 734190e7..0a6609ca 100644 --- a/app/views/settings/preferences/show.html.erb +++ b/app/views/settings/preferences/show.html.erb @@ -5,16 +5,16 @@

<%= t(".page_title") %>

<%= settings_section title: t(".general_title"), subtitle: t(".general_subtitle") do %>
- <%= form_with model: Current.user, url: settings_preferences_path, html: { class: "space-y-4", data: { controller: "auto-submit-form" } } do |form| %> - <%= form.fields_for :family_attributes do |family_fields| %> - <%= family_fields.currency_select :currency, { selected: Current.family.currency, label: "Currency" }, { data: { auto_submit_form_target: "auto" } } %> + <%= styled_form_with model: Current.user, url: settings_preferences_path, class: "space-y-4", data: { controller: "auto-submit-form" } do |form| %> + <%= form.fields_for :family_attributes do |family_form| %> + <%= currency_select_full family_form, :currency, { label: "Currency", selected: Current.family.currency }, { data: { auto_submit_form_target: "auto" } } %> <% end %> <% end %>
<% end %> <%= settings_section title: t(".theme_title"), subtitle: t(".theme_subtitle") do %>
- <%= form_with model: Current.user, url: settings_preferences_path, local: true, html: { class: "flex justify-between items-center" } do |form| %> + <%= styled_form_with model: Current.user, url: settings_preferences_path, local: true, class: "flex justify-between items-center" do |form| %>
<%= image_tag("light-mode-preview.png", alt: "Light Theme Preview", class: "h-44 mb-4") %>
diff --git a/app/views/settings/profiles/show.html.erb b/app/views/settings/profiles/show.html.erb index e829761b..edcab295 100644 --- a/app/views/settings/profiles/show.html.erb +++ b/app/views/settings/profiles/show.html.erb @@ -5,7 +5,7 @@

<%= t(".page_title") %>

<%= settings_section title: t(".profile_title"), subtitle: t(".profile_subtitle") do %> - <%= form_with model: Current.user, url: settings_profile_path, html: {data: { controller: "profile-image-preview" }} do |form| %> + <%= styled_form_with model: Current.user, url: settings_profile_path, class: "space-y-4", data: { controller: "profile-image-preview" } do |form| %>
@@ -43,7 +43,7 @@ <% end %> <%= settings_section title: t(".household_title"), subtitle: t(".household_subtitle") do %>
- <%= form_with model: Current.user, url: settings_profile_path, html: { class: "space-y-4", data: { controller: "auto-submit-form", "auto-submit-form-trigger-event-value": "blur" } } do |form| %> + <%= styled_form_with model: Current.user, url: settings_profile_path, class: "space-y-4", data: { controller: "auto-submit-form", "auto-submit-form-trigger-event-value": "blur" } do |form| %> <%= form.fields_for :family_attributes do |family_fields| %> <%= family_fields.text_field :name, placeholder: t(".household_form_input_placeholder"), value: Current.family.name, label: t(".household_form_label"), disabled: !Current.user.admin?, "data-auto-submit-form-target": "auto" %> <% end %> diff --git a/app/views/shared/_money_field.html.erb b/app/views/shared/_money_field.html.erb new file mode 100644 index 00000000..7b3288b0 --- /dev/null +++ b/app/views/shared/_money_field.html.erb @@ -0,0 +1,24 @@ +<%# locals: (form:, money_method:, default_currency: "USD", disable_currency: false, hide_currency: false, label: nil) %> +<% fallback_label = t(".money-label") %> +
+ <%= form.label label || fallback_label, { class: "form-field__label" } %> + +
+
+ $ + <%= money_field form, money_method, { inline: true, "data-money-field-target" => "amount", default_currency: default_currency } %> +
+ <% unless hide_currency %> +
+ <%= currency_select form, :currency, { inline: true }, { + class: "form-field__input text-right pr-8 disabled:text-gray-500", + disabled: disable_currency, + data: { + "money-field-target" => "currency", + action: "money-field#handleCurrencyChange" + } + } %> +
+ <% end %> +
+
diff --git a/app/views/shared/_period_select.html.erb b/app/views/shared/_period_select.html.erb deleted file mode 100644 index 8b935a72..00000000 --- a/app/views/shared/_period_select.html.erb +++ /dev/null @@ -1,22 +0,0 @@ -<%# locals: (value: 'last_30_days', button_class: '') -%> -<% options = [["7D", "last_7_days"], ["1M", "last_30_days"], ["1Y", "last_365_days"], ["All", "all"]] %> -
- <%= - tag.button( - type: "button", - data: { "select-target": "button" }, - class: button_class.presence || "flex items-center gap-1 w-full border border-alpha-black-100 shadow-xs rounded-lg text-sm p-2 cursor-pointer text-gray-900 text-sm" - ) do - %> - <%= options.find { |option| option[1] == value }[0] %> - <%= lucide_icon("chevron-down", class: "w-5 h-5 text-gray-500") %> - <% end %> - - -
diff --git a/app/views/tag/deletions/new.html.erb b/app/views/tag/deletions/new.html.erb index 240df1f2..48c3562d 100644 --- a/app/views/tag/deletions/new.html.erb +++ b/app/views/tag/deletions/new.html.erb @@ -11,14 +11,15 @@

- <%= form_with url: tag_deletions_path(@tag), - data: { - turbo: false, - controller: "deletion", - deletion_dangerous_action_class: "form-field__submit bg-white text-red-600 border hover:bg-red-50", - deletion_safe_action_class: "form-field__submit border border-transparent", - deletion_submit_text_when_not_replacing_value: t(".delete_and_leave_uncategorized", tag_name: @tag.name), - deletion_submit_text_when_replacing_value: t(".delete_and_recategorize", tag_name: @tag.name) } do |f| %> + <%= styled_form_with url: tag_deletions_path(@tag), + class: "space-y-4", + data: { + turbo: false, + controller: "deletion", + deletion_dangerous_action_class: "form-field__submit bg-white text-red-600 border hover:bg-red-50", + deletion_safe_action_class: "form-field__submit border border-transparent", + deletion_submit_text_when_not_replacing_value: t(".delete_and_leave_uncategorized", tag_name: @tag.name), + deletion_submit_text_when_replacing_value: t(".delete_and_recategorize", tag_name: @tag.name) } do |f| %> <%= f.collection_select :replacement_tag_id, Current.family.tags.alphabetically.without(@tag), :id, :name, diff --git a/app/views/tags/_form.html.erb b/app/views/tags/_form.html.erb index 4965b305..4ee675d5 100644 --- a/app/views/tags/_form.html.erb +++ b/app/views/tags/_form.html.erb @@ -1,4 +1,4 @@ -<%= form_with model: tag, data: { turbo: false } do |form| %> +<%= styled_form_with model: tag, data: { turbo: false } do |form| %>
diff --git a/app/views/transactions/_form.html.erb b/app/views/transactions/_form.html.erb index dcd4c08c..501fd3ac 100644 --- a/app/views/transactions/_form.html.erb +++ b/app/views/transactions/_form.html.erb @@ -1,4 +1,4 @@ -<%= form_with model: @entry, url: transactions_path, data: { turbo_frame: "_top" } do |f| %> +<%= styled_form_with model: @entry, url: transactions_path, class: "space-y-4", data: { turbo_frame: "_top" } do |f| %>
<%= radio_tab_tag form: f, name: :nature, value: :expense, label: t(".expense"), icon: "minus-circle", checked: params[:nature] == "expense" || params[:nature].nil? %> @@ -13,7 +13,7 @@
<%= f.text_field :name, label: t(".description"), placeholder: t(".description_placeholder"), required: true %> <%= f.collection_select :account_id, Current.family.accounts.alphabetically, :id, :name, { prompt: t(".account_prompt"), label: t(".account") }, required: true %> - <%= f.money_field :amount_money, label: t(".amount"), required: true %> + <%= money_with_currency_field f, :amount_money, label: t(".amount"), required: true %> <%= f.hidden_field :entryable_type, value: "Account::Transaction" %> <%= f.fields_for :entryable do |ef| %> <%= ef.collection_select :category_id, Current.family.categories.alphabetically, :id, :name, { prompt: t(".category_prompt"), label: t(".category") } %> diff --git a/app/views/transactions/_pagination.html.erb b/app/views/transactions/_pagination.html.erb index c588bfa4..dcc8ec0f 100644 --- a/app/views/transactions/_pagination.html.erb +++ b/app/views/transactions/_pagination.html.erb @@ -41,7 +41,6 @@
<%= form_with url: transactions_path, - builder: ActionView::Helpers::FormBuilder, method: :get, class: "flex items-center gap-4", data: { controller: "auto-submit-form" } do |f| %> diff --git a/app/views/transactions/bulk_edit.html.erb b/app/views/transactions/bulk_edit.html.erb index 07c21e1e..dc4ec2a1 100644 --- a/app/views/transactions/bulk_edit.html.erb +++ b/app/views/transactions/bulk_edit.html.erb @@ -2,7 +2,7 @@ - <%= form_with url: bulk_update_transactions_path, scope: "bulk_update", html: { class: "h-full" }, data: { turbo_frame: "_top" } do |form| %> + <%= styled_form_with url: bulk_update_transactions_path, scope: "bulk_update", class: "h-full", data: { turbo_frame: "_top" } do |form| %>
diff --git a/app/views/transactions/searches/_form.html.erb b/app/views/transactions/searches/_form.html.erb index 3c539e64..0cfd8a1a 100644 --- a/app/views/transactions/searches/_form.html.erb +++ b/app/views/transactions/searches/_form.html.erb @@ -5,11 +5,11 @@ data: { controller: "auto-submit-form" } do |form| %>
-
+
<%= form.text_field :search, placeholder: "Search transactions by name", value: @q[:search], - class: "placeholder:text-sm placeholder:text-gray-500 relative pl-10 w-full border-none rounded-lg", + class: "placeholder:text-sm placeholder:text-gray-500 relative pl-10 w-full border-none rounded-lg focus:outline-none focus:ring-0", "data-auto-submit-form-target": "auto" %> <%= lucide_icon("search", class: "w-5 h-5 text-gray-500 ml-2 absolute inset-0 transform top-1/2 -translate-y-1/2") %>
diff --git a/config/locales/views/accounts/en.yml b/config/locales/views/accounts/en.yml index 686ebd8f..f838f961 100644 --- a/config/locales/views/accounts/en.yml +++ b/config/locales/views/accounts/en.yml @@ -37,9 +37,6 @@ en: other_accounts: Other accounts new: balance: Current balance - currency: - all_others: All Others - popular: Popular institution: Financial institution name: label: Account name diff --git a/config/locales/views/sessions/en.yml b/config/locales/views/sessions/en.yml index 9853e089..dd9169fe 100644 --- a/config/locales/views/sessions/en.yml +++ b/config/locales/views/sessions/en.yml @@ -9,6 +9,7 @@ en: email: Email address email_placeholder: you@example.com forgot_password: Forgot your password? + password: Password reset_password: Reset it submit: Log in title: Sign in to your account diff --git a/config/locales/views/shared/en.yml b/config/locales/views/shared/en.yml index 1e22ea1e..5df96bb3 100644 --- a/config/locales/views/shared/en.yml +++ b/config/locales/views/shared/en.yml @@ -6,6 +6,8 @@ en: body_html: "

You will not be able to undo this decision

" cancel: Cancel title: Are you sure? + money_field: + money-label: Amount no_account_empty_state: new_account: New account no_account_subtitle: Since no accounts have been added, there's no data to display.