mirror of
https://github.com/maybe-finance/maybe.git
synced 2025-07-19 05:09:38 +02:00
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
This commit is contained in:
parent
47523f64c2
commit
e51806b98b
50 changed files with 268 additions and 488 deletions
|
@ -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;
|
||||
|
|
|
@ -19,6 +19,7 @@ class AccountsController < ApplicationController
|
|||
end
|
||||
|
||||
def list
|
||||
render layout: false
|
||||
end
|
||||
|
||||
def new
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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")
|
||||
|
|
55
app/helpers/styled_form_builder.rb
Normal file
55
app/helpers/styled_form_builder.rb
Normal file
|
@ -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
|
|
@ -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;
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 %>
|
||||
<button type="button" data-bulk-select-scope-param="bulk_delete" data-action="bulk-select#submitBulkRequest" class="p-1.5 group hover:bg-gray-700 flex items-center justify-center rounded-md" title="Delete">
|
||||
<%= lucide_icon "trash-2", class: "w-5 group-hover:text-white" %>
|
||||
</button>
|
||||
|
|
|
@ -27,8 +27,7 @@
|
|||
</summary>
|
||||
|
||||
<div class="pb-6">
|
||||
<%= form_with model: [account, entry], url: account_entry_path(account, entry), html: { data: { controller: "auto-submit-form" } } do |f| %>
|
||||
<div class="space-y-2">
|
||||
<%= 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? %>
|
||||
<div class="flex space-x-2">
|
||||
|
@ -44,13 +43,12 @@
|
|||
|
||||
<%= 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" %>
|
||||
<%= 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" } %>
|
||||
</div>
|
||||
<%= 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 %>
|
||||
</div>
|
||||
</details>
|
||||
|
@ -61,12 +59,12 @@
|
|||
<%= lucide_icon "chevron-down", class: "group-open:transform group-open:rotate-180 text-gray-500 w-5" %>
|
||||
</summary>
|
||||
|
||||
<div class="pb-6 space-y-2">
|
||||
<%= form_with model: [account, entry], url: account_entry_path(account, entry), html: { data: { controller: "auto-submit-form" } } do |f| %>
|
||||
<div class="pb-6">
|
||||
<%= 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 @@
|
|||
</summary>
|
||||
|
||||
<div class="pb-6">
|
||||
<%= 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| %>
|
||||
<div class="flex cursor-pointer items-center gap-2 justify-between">
|
||||
<div class="text-sm space-y-1">
|
||||
|
|
|
@ -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"),
|
||||
|
|
|
@ -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| %>
|
||||
<div class="grid grid-cols-10 p-4 items-center">
|
||||
<div class="col-span-7 flex items-center gap-4">
|
||||
<div class="w-8 h-8 rounded-full p-1.5 flex items-center justify-center bg-gray-500/5">
|
||||
|
|
|
@ -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| %>
|
||||
<section>
|
||||
<fieldset class="bg-gray-50 rounded-lg p-1 grid grid-flow-col justify-stretch gap-x-2">
|
||||
<%= 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 %>
|
||||
</section>
|
||||
|
||||
|
|
|
@ -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| %>
|
||||
<div class="relative inline-block select-none">
|
||||
<%= form.check_box :is_active, { class: "sr-only peer", data: { "auto-submit-form-target": "auto" } } %>
|
||||
|
|
|
@ -5,9 +5,9 @@
|
|||
<%= lucide_icon "x", class: "w-5 h-5 text-gray-500", data: { action: "click->modal#close" } %>
|
||||
</header>
|
||||
|
||||
<%= 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 %>
|
||||
|
||||
<div class="relative">
|
||||
<%= f.collection_select :institution_id, Current.family.institutions.alphabetically, :id, :name, { include_blank: t(".ungrouped"), label: t(".institution") } %>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<turbo-frame id="account-list" target="_top">
|
||||
<%= turbo_frame_tag "account-list" do %>
|
||||
<% account_groups(period: @period).each do |group| %>
|
||||
<%= render "accounts/account_list", group: group %>
|
||||
<% end %>
|
||||
</turbo-frame>
|
||||
<% end %>
|
||||
|
|
|
@ -73,13 +73,13 @@
|
|||
<% end %>
|
||||
<span>Add <%= @account.accountable.model_name.human.downcase %></span>
|
||||
</div>
|
||||
<%= 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| %>
|
||||
<div class="space-y-4 grow">
|
||||
<%= 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" %>
|
||||
|
||||
<div>
|
||||
<%= check_box_tag :add_start_values, class: "maybe-checkbox maybe-checkbox--light peer mb-1" %>
|
||||
|
|
|
@ -69,8 +69,8 @@
|
|||
<%= tag.span period_label(@period), class: "text-gray-500" %>
|
||||
</div>
|
||||
</div>
|
||||
<%= 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 %>
|
||||
</div>
|
||||
<div class="h-96 flex items-center justify-center text-2xl font-bold">
|
||||
|
|
|
@ -48,8 +48,8 @@
|
|||
<%= lucide_icon("plus", class: "w-5 h-5 text-gray-500") %>
|
||||
<p><%= t(".new") %></p>
|
||||
<% 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 %>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -64,8 +64,8 @@
|
|||
<%= lucide_icon("plus", class: "w-5 h-5 text-gray-500") %>
|
||||
<p><%= t(".new") %></p>
|
||||
<% 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 %>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
<%= form_with model: category, data: { turbo: false } do |form| %>
|
||||
<%= styled_form_with model: category, data: { turbo: false } do |form| %>
|
||||
<div class="flex flex-col space-y-4 w-96" data-controller="color-select" data-color-select-selection-value="<%= category.color %>">
|
||||
<fieldset class="relative">
|
||||
<span data-color-select-target="decoration" class="pointer-events-none absolute inset-y-3.5 left-3 flex items-center pl-1 block w-1 rounded-lg"></span>
|
||||
|
|
|
@ -11,7 +11,8 @@
|
|||
</p>
|
||||
</div>
|
||||
|
||||
<%= form_with url: category_deletions_path(@category),
|
||||
<%= styled_form_with url: category_deletions_path(@category),
|
||||
class: "space-y-4",
|
||||
data: {
|
||||
turbo: false,
|
||||
controller: "deletion",
|
||||
|
|
|
@ -1,11 +1,9 @@
|
|||
<%= form_with model: @import, url: load_import_path(@import) do |form| %>
|
||||
<div>
|
||||
<%= 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" %>
|
||||
</div>
|
||||
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 %>
|
||||
|
|
|
@ -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| %>
|
||||
<div class="flex items-center justify-center w-full">
|
||||
<label for="import_raw_csv_str" class="csv-drop-box flex flex-col items-center justify-center w-full h-64 border-2 border-gray-300 border-dashed rounded-lg cursor-pointer bg-gray-50" data-action="dragover->csv-upload#dragover dragleave->csv-upload#dragleave drop->csv-upload#drop">
|
||||
<div class="flex flex-col items-center justify-center pt-5 pb-6">
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
<%= form_with model: @import do |form| %>
|
||||
<%= styled_form_with model: @import do |form| %>
|
||||
<div class="mb-4">
|
||||
<%= form.collection_select :account_id, Current.family.accounts.alphabetically, :id, :name, { prompt: t(".select_account"), label: t(".account"), required: true } %>
|
||||
</div>
|
||||
|
|
|
@ -24,7 +24,6 @@
|
|||
style="grid-template-columns: repeat(<%= @import.expected_fields.size %>, 1fr);">
|
||||
<% row.fields.each_with_index do |value, col_index| %>
|
||||
<%= form_with model: @import,
|
||||
builder: ActionView::Helpers::FormBuilder,
|
||||
url: clean_import_url(@import),
|
||||
method: :patch,
|
||||
data: { turbo: false, controller: "auto-submit-form", "auto-submit-form-trigger-event-value" => "blur" } do |form| %>
|
||||
|
|
|
@ -8,8 +8,7 @@
|
|||
<p class="text-gray-500 text-sm"><%= t(".configure_description") %></p>
|
||||
</div>
|
||||
|
||||
<%= form_with model: @import, url: configure_import_path(@import) do |form| %>
|
||||
<div class="mb-4 space-y-4">
|
||||
<%= styled_form_with model: @import, url: configure_import_path(@import), class: "space-y-4" do |form| %>
|
||||
<%= form.fields_for :column_mappings do |mappings| %>
|
||||
<% @import.expected_fields.each do |field| %>
|
||||
<%= mappings.select field.key,
|
||||
|
@ -18,7 +17,6 @@
|
|||
include_blank: field.optional? ? t(".optional") : false %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<%= form.submit t(".next"), class: "px-4 py-2 block w-full rounded-lg bg-gray-900 text-white text-sm font-medium cursor-pointer hover:bg-gray-700", data: { turbo_confirm: (@import.column_mappings? ? { title: t(".confirm_title"), body: t(".confirm_body"), accept: t(".confirm_accept") } : nil) } %>
|
||||
<% end %>
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
<%= form_with model: institution, data: { turbo_frame: "_top", controller: "profile-image-preview" } do |f| %>
|
||||
|
||||
<%= styled_form_with model: institution, class: "space-y-4", data: { turbo_frame: "_top", controller: "profile-image-preview" } do |f| %>
|
||||
<div class="flex justify-center items-center py-4">
|
||||
<%= f.label :logo do %>
|
||||
<div class="relative cursor-pointer hover:opacity-80 w-16 h-16 rounded-full bg-gray-50">
|
||||
|
|
|
@ -91,17 +91,18 @@
|
|||
<%= t(".portfolio") %>
|
||||
<% end %>
|
||||
<span class="font-bold tracking-wide">•</span>
|
||||
<%= form_with url: list_accounts_path, method: :get, class: "flex items-center gap-4", data: { controller: "auto-submit-form", turbo_frame: "account-list" } do %>
|
||||
<%= render partial: "shared/period_select", locals: { button_class: "flex items-center gap-1 w-full cursor-pointer font-bold tracking-wide" } %>
|
||||
<%= form_with url: list_accounts_path, method: :get, data: { controller: "auto-submit-form", turbo_frame: "account-list" } do |form| %>
|
||||
<%= period_select form: form, selected: "last_7_days", classes: "w-full border-none pl-2 pr-7 text-xs bg-transparent gap-1 cursor-pointer font-semibold tracking-wide focus:outline-none focus:ring-0" %>
|
||||
<% end %>
|
||||
</div>
|
||||
<%= link_to new_account_path, id: "sidebar-new-account", class: "block hover:bg-gray-100 p-2 text-sm font-semibold text-gray-900 flex items-center rounded", title: t(".new_account"), data: { turbo_frame: "modal" } do %>
|
||||
<%= link_to new_account_path, id: "sidebar-new-account", class: "block hover:bg-gray-100 font-semibold text-gray-900 flex items-center rounded", title: t(".new_account"), data: { turbo_frame: "modal" } do %>
|
||||
<%= lucide_icon("plus", class: "w-5 h-5 text-gray-500") %>
|
||||
<% end %>
|
||||
</div>
|
||||
<turbo-frame id="account-list" target="_top">
|
||||
|
||||
<%= turbo_frame_tag "account-list", target: "_top" do %>
|
||||
<% account_groups.each do |group| %>
|
||||
<%= render "accounts/account_list", group: group %>
|
||||
<% end %>
|
||||
</turbo-frame>
|
||||
<% end %>
|
||||
</div>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<% is_editing = @merchant.id.present? %>
|
||||
<div data-controller="merchant-avatar">
|
||||
<%= 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| %>
|
||||
<section class="space-y-4">
|
||||
<div class="w-fit m-auto">
|
||||
<%= render partial: "merchants/avatar", locals: { merchant: } %>
|
||||
|
|
|
@ -26,8 +26,8 @@
|
|||
trend: @net_worth_series.trend
|
||||
} %>
|
||||
</div>
|
||||
<%= 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 %>
|
||||
</div>
|
||||
<%= render partial: "pages/dashboard/net_worth_chart", locals: { series: @net_worth_series } %>
|
||||
|
|
|
@ -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 %>
|
||||
|
||||
<div class="relative border border-gray-100 bg-gray-25 rounded-xl focus-within:bg-white focus-within:shadow focus-within:opacity-100">
|
||||
|
|
|
@ -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" %>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<h1><% t(".title") %></h1>
|
||||
|
||||
<%= 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 %>
|
||||
|
||||
<div>
|
||||
|
|
|
@ -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 %>
|
||||
|
|
|
@ -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 %>
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
<div class="space-y-4">
|
||||
<h1 class="text-gray-900 text-xl font-medium mb-4"><%= t(".page_title") %></h1>
|
||||
<%= 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" %>
|
||||
<div>
|
||||
|
|
|
@ -5,16 +5,16 @@
|
|||
<h1 class="text-gray-900 text-xl font-medium mb-4"><%= t(".page_title") %></h1>
|
||||
<%= settings_section title: t(".general_title"), subtitle: t(".general_subtitle") do %>
|
||||
<div>
|
||||
<%= 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 %>
|
||||
</div>
|
||||
<% end %>
|
||||
<%= settings_section title: t(".theme_title"), subtitle: t(".theme_subtitle") do %>
|
||||
<div>
|
||||
<%= 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| %>
|
||||
<div class="text-center">
|
||||
<%= image_tag("light-mode-preview.png", alt: "Light Theme Preview", class: "h-44 mb-4") %>
|
||||
<div class="flex justify-center items-center gap-2">
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
<h1 class="text-gray-900 text-xl font-medium mb-4"><%= t(".page_title") %></h1>
|
||||
<div class="space-y-4">
|
||||
<%= 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| %>
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="relative flex justify-center items-center bg-gray-50 w-24 h-24 rounded-full border border-alpha-black-25">
|
||||
<div data-profile-image-preview-target="imagePreview" class="h-full w-full flex justify-center items-center">
|
||||
|
@ -43,7 +43,7 @@
|
|||
<% end %>
|
||||
<%= settings_section title: t(".household_title"), subtitle: t(".household_subtitle") do %>
|
||||
<div class="space-y-4">
|
||||
<%= 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 %>
|
||||
|
|
24
app/views/shared/_money_field.html.erb
Normal file
24
app/views/shared/_money_field.html.erb
Normal file
|
@ -0,0 +1,24 @@
|
|||
<%# locals: (form:, money_method:, default_currency: "USD", disable_currency: false, hide_currency: false, label: nil) %>
|
||||
<% fallback_label = t(".money-label") %>
|
||||
<div class="form-field pr-0" data-controller="money-field">
|
||||
<%= form.label label || fallback_label, { class: "form-field__label" } %>
|
||||
|
||||
<div class="flex items-center gap-1">
|
||||
<div class="flex items-center grow gap-1">
|
||||
<span class="text-gray-500 text-sm" data-money-field-target="symbol">$</span>
|
||||
<%= money_field form, money_method, { inline: true, "data-money-field-target" => "amount", default_currency: default_currency } %>
|
||||
</div>
|
||||
<% unless hide_currency %>
|
||||
<div>
|
||||
<%= 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"
|
||||
}
|
||||
} %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
|
@ -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"]] %>
|
||||
<div data-controller="select" data-select-active-class="bg-alpha-black-50" class="relative" data-select-selected-value="<%= value %>">
|
||||
<%=
|
||||
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
|
||||
%>
|
||||
<span data-select-target="buttonText"><%= options.find { |option| option[1] == value }[0] %></span>
|
||||
<%= lucide_icon("chevron-down", class: "w-5 h-5 text-gray-500") %>
|
||||
<% end %>
|
||||
<input type="hidden" name="period" data-select-target="input" data-auto-submit-form-target="auto">
|
||||
<ul data-select-target="list" class="hidden absolute z-10 top-[100%] right-0 border border-alpha-black-25 bg-white rounded-lg shadow-xs">
|
||||
<% options.each do |label, value| %>
|
||||
<li tabindex="0" data-select-target="option" data-action="click->select#selectOption" data-value="<%= value %>" class="text-sm text-gray-900 rounded-lg cursor-pointer hover:bg-alpha-black-50 px-5 py-1">
|
||||
<%= label %>
|
||||
</li>
|
||||
<% end %>
|
||||
</ul>
|
||||
</div>
|
|
@ -11,7 +11,8 @@
|
|||
</p>
|
||||
</div>
|
||||
|
||||
<%= form_with url: tag_deletions_path(@tag),
|
||||
<%= styled_form_with url: tag_deletions_path(@tag),
|
||||
class: "space-y-4",
|
||||
data: {
|
||||
turbo: false,
|
||||
controller: "deletion",
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
<%= form_with model: tag, data: { turbo: false } do |form| %>
|
||||
<%= styled_form_with model: tag, data: { turbo: false } do |form| %>
|
||||
<div class="flex flex-col space-y-4 w-96" data-controller="color-select" data-color-select-selection-value="<%= tag.color %>">
|
||||
<fieldset class="relative">
|
||||
<span data-color-select-target="decoration" class="pointer-events-none absolute inset-y-3.5 left-3 flex items-center pl-1 block w-1 rounded-lg"></span>
|
||||
|
|
|
@ -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| %>
|
||||
<section>
|
||||
<fieldset class="bg-gray-50 rounded-lg p-1 grid grid-flow-col justify-stretch gap-x-2">
|
||||
<%= 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 @@
|
|||
<section class="space-y-2">
|
||||
<%= 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") } %>
|
||||
|
|
|
@ -41,7 +41,6 @@
|
|||
</div>
|
||||
<div class="flex items-center gap-4">
|
||||
<%= form_with url: transactions_path,
|
||||
builder: ActionView::Helpers::FormBuilder,
|
||||
method: :get,
|
||||
class: "flex items-center gap-4",
|
||||
data: { controller: "auto-submit-form" } do |f| %>
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
<dialog data-controller="modal"
|
||||
data-action="click->modal#clickOutside"
|
||||
class="bg-white border border-alpha-black-25 rounded-2xl max-h-[calc(100vh-32px)] max-w-[480px] w-full shadow-xs h-full mt-4 mr-4">
|
||||
<%= 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| %>
|
||||
<div class="flex h-full flex-col justify-between p-4">
|
||||
<div>
|
||||
<div class="flex h-9 items-center justify-end">
|
||||
|
|
|
@ -5,11 +5,11 @@
|
|||
data: { controller: "auto-submit-form" } do |form| %>
|
||||
<div class="flex gap-2 mb-4">
|
||||
<div class="grow">
|
||||
<div class="relative flex items-center bg-white border border-gray-200 rounded-lg">
|
||||
<div class="relative flex items-center bg-white border border-alpha-black-200 rounded-lg focus-within:border-alpha-black-500">
|
||||
<%= 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") %>
|
||||
</div>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -6,6 +6,8 @@ en:
|
|||
body_html: "<p>You will not be able to undo this decision</p>"
|
||||
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.
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue