1
0
Fork 0
mirror of https://github.com/maybe-finance/maybe.git synced 2025-08-09 15:35:22 +02:00

Form UI improvements, ignore attribute locks on manual rule application

This commit is contained in:
Zach Gollwitzer 2025-04-15 12:09:07 -04:00
parent 1016d4f7cf
commit bc7b96863f
14 changed files with 47 additions and 32 deletions

View file

@ -31,7 +31,7 @@ class AccountsController < ApplicationController
family.sync_later
end
redirect_to accounts_path
redirect_back_or_to accounts_path
end
private

View file

@ -38,7 +38,7 @@ class RulesController < ApplicationController
def apply
@rule.update!(active: true)
@rule.apply_later
@rule.apply_later(ignore_attribute_locks: true)
redirect_back_or_to rules_path, notice: "#{@rule.resource_type.humanize} rule activated"
end

View file

@ -15,14 +15,17 @@ class TransactionCategoriesController < ApplicationController
}
end
transaction.lock_saved_attributes!
@entry.lock_saved_attributes!
respond_to do |format|
format.html { redirect_back_or_to transaction_path(@entry) }
format.turbo_stream do
render turbo_stream: [
turbo_stream.replace(
dom_id(@entry, :category_menu),
dom_id(transaction, :category_menu),
partial: "categories/menu",
locals: { transaction: @entry.transaction }
locals: { transaction: transaction }
),
*flash_notification_stream_items
]

View file

@ -54,8 +54,8 @@ export default class extends Controller {
for (const operator of conditionFilter.operators) {
const optionEl = document.createElement("option");
optionEl.value = operator;
optionEl.textContent = operator;
optionEl.value = operator[1];
optionEl.textContent = operator[0];
this.operatorSelectTarget.appendChild(optionEl);
}
}

View file

@ -1,7 +1,7 @@
class RuleJob < ApplicationJob
queue_as :default
def perform(rule)
rule.apply
def perform(rule, ignore_attribute_locks: false)
rule.apply(ignore_attribute_locks: ignore_attribute_locks)
end
end

View file

@ -40,14 +40,14 @@ class Rule < ApplicationRecord
matching_resources_scope.count
end
def apply
def apply(ignore_attribute_locks: false)
actions.each do |action|
action.apply(matching_resources_scope)
action.apply(matching_resources_scope, ignore_attribute_locks: ignore_attribute_locks)
end
end
def apply_later
RuleJob.perform_later(self)
def apply_later(ignore_attribute_locks: false)
RuleJob.perform_later(self, ignore_attribute_locks: ignore_attribute_locks)
end
private

View file

@ -4,9 +4,9 @@ class Rule::ConditionFilter
TYPES = [ "text", "number", "select" ]
OPERATORS_MAP = {
"text" => [ "like", "=" ],
"number" => [ ">", ">=", "<", "<=", "=" ],
"select" => [ "=" ]
"text" => [ [ "Contains", "like" ], [ "Equal to", "=" ] ],
"number" => [ [ "Greater than", ">" ], [ "Greater or equal to", ">=" ], [ "Less than", "<" ], [ "Less than or equal to", "<=" ], [ "Is equal to", "=" ] ],
"select" => [ [ "Equal to", "=" ] ]
}
def initialize(rule)
@ -70,7 +70,7 @@ class Rule::ConditionFilter
end
def sanitize_operator(operator)
raise UnsupportedOperatorError, "Unsupported operator: #{operator} for type: #{type}" unless operators.include?(operator)
raise UnsupportedOperatorError, "Unsupported operator: #{operator} for type: #{type}" unless operators.map(&:last).include?(operator)
if operator == "like"
"ILIKE"

View file

@ -8,9 +8,11 @@
<%= form.hidden_field :_destroy, value: false, data: { rule__actions_target: "destroyField" } %>
<div class="grow flex gap-2 items-center h-full">
<%= form.select :action_type, rule.action_executors.map { |executor| [ executor.label, executor.key ] }, {}, data: { action: "rule--actions#handleActionTypeChange" } %>
<div class="grow shrink-0">
<%= form.select :action_type, rule.action_executors.map { |executor| [ executor.label, executor.key ] }, {}, data: { action: "rule--actions#handleActionTypeChange" } %>
</div>
<%= tag.div class: class_names("flex items-center gap-2", "hidden" => !needs_value),
<%= tag.div class: class_names("min-w-1/2 flex items-center gap-2", "hidden" => !needs_value),
data: { rule__actions_target: "actionValue" } do %>
<span class="font-medium uppercase text-xs">to</span>
<%= form.select :value, action.options, {}, disabled: !needs_value %>

View file

@ -13,15 +13,21 @@
<div class="grow flex gap-2 items-center h-full">
<%= form.hidden_field :_destroy, value: false, data: { rule__conditions_target: "destroyField" } %>
<%= form.select :condition_type, rule.condition_filters.map { |filter| [ filter.label, filter.key ] }, {}, data: { action: "rule--conditions#handleConditionTypeChange" } %>
<div class="w-2/5 shrink-0">
<%= form.select :condition_type, rule.condition_filters.map { |filter| [ filter.label, filter.key ] }, {}, data: { action: "rule--conditions#handleConditionTypeChange" } %>
</div>
<%= form.select :operator, condition.operators, { container_class: "w-fit min-w-16" }, data: { rule__conditions_target: "operatorSelect" } %>
<%= form.select :operator, condition.operators, { container_class: "w-fit min-w-36" }, data: { rule__conditions_target: "operatorSelect" } %>
<div data-rule--conditions-target="filterValue">
<div data-rule--conditions-target="filterValue" class="grow">
<% if condition.filter.type == "select" %>
<%= form.select :value, condition.options, {} %>
<% else %>
<%= form.text_field :value, placeholder: "Enter a value" %>
<% if condition.filter.type == "number" %>
<%= form.number_field :value, placeholder: "10" %>
<% else %>
<%= form.text_field :value, placeholder: "Enter a value" %>
<% end %>
<% end %>
</div>
</div>

View file

@ -1,6 +1,6 @@
<%# locals: (rule:) %>
<%= styled_form_with model: rule, class: "space-y-4",
<%= styled_form_with model: rule, class: "space-y-4 w-[550px]",
data: { controller: "rules", rule_registry_value: rule.registry.to_json } do |f| %>
<%= f.hidden_field :resource_type, value: rule.resource_type %>
@ -80,13 +80,13 @@
<div class="space-y-2">
<div class="flex items-center gap-2">
<%= f.radio_button :effective_date_enabled, false, checked: rule.effective_date.nil?, data: { action: "rules#clearEffectiveDate" } %>
<%= f.label :effective_date_enabled_false, "To all past and future #{rule.resource_type}s" %>
<%= f.label :effective_date_enabled_false, "To all past and future #{rule.resource_type}s", class: "text-sm" %>
</div>
<div class="flex items-center gap-2">
<div class="flex items-center gap-2">
<%= f.radio_button :effective_date_enabled, true, checked: rule.effective_date.present? %>
<%= f.label :effective_date_enabled_true, "Starting from" %>
<%= f.label :effective_date_enabled_true, "Starting from", class: "text-sm" %>
</div>
<%= f.date_field :effective_date, container_class: "w-fit", data: { rules_target: "effectiveDateInput" } %>

View file

@ -2,7 +2,6 @@
<div class="flex justify-between items-center gap-4 bg-white shadow-border-xs rounded-md p-4">
<div class="text-sm space-y-1.5">
<p class="flex items-center flex-wrap gap-1.5">
<span class="px-2 py-1 border border-alpha-black-200 rounded-full">
<%= rule.actions.first.executor.label %>

View file

@ -4,6 +4,11 @@
<div class="flex items-center gap-5">
<div class="flex items-center gap-2">
<%= contextual_menu do %>
<% if Rails.env.development? %>
<%= button_to "Dev only: Sync all", sync_all_accounts_path, class: "btn btn--ghost w-full" %>
<% end %>
<%= contextual_menu_item "New rule", url: new_rule_path(resource_type: "transaction"), icon: "plus", turbo_frame: :modal %>
<%= contextual_menu_item "Edit rules", url: rules_path, icon: "git-branch", turbo_frame: :_top %>
<%= contextual_menu_modal_action_item t(".edit_categories"), categories_path, icon: "shapes", turbo_frame: :_top %>
<%= contextual_menu_modal_action_item t(".edit_tags"), tags_path, icon: "tags", turbo_frame: :_top %>
<%= contextual_menu_modal_action_item t(".edit_merchants"), family_merchants_path, icon: "store", turbo_frame: :_top %>

View file

@ -1,4 +1,4 @@
class CreateDataEnrichments < ActiveRecord::Migration[7.2]
class DataEnrichmentsAndLocks < ActiveRecord::Migration[7.2]
def change
create_table :data_enrichments, id: :uuid do |t|
t.references :enrichable, polymorphic: true, null: false, type: :uuid
@ -12,10 +12,10 @@ class CreateDataEnrichments < ActiveRecord::Migration[7.2]
add_index :data_enrichments, [ :enrichable_id, :enrichable_type, :source, :attribute_name ], unique: true
# Entries
add_column :account_entries, :locked_attributes, :jsonb, default: {}
add_column :account_transactions, :locked_attributes, :jsonb, default: {}
add_column :account_trades, :locked_attributes, :jsonb, default: {}
add_column :account_valuations, :locked_attributes, :jsonb, default: {}
add_column :entries, :locked_attributes, :jsonb, default: {}
add_column :transactions, :locked_attributes, :jsonb, default: {}
add_column :trades, :locked_attributes, :jsonb, default: {}
add_column :valuations, :locked_attributes, :jsonb, default: {}
# Accounts
add_column :accounts, :locked_attributes, :jsonb, default: {}

2
db/schema.rb generated
View file

@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[7.2].define(version: 2025_04_13_141446) do
ActiveRecord::Schema[7.2].define(version: 2025_04_15_125256) do
# These are extensions that must be enabled in order to support this database
enable_extension "pgcrypto"
enable_extension "plpgsql"