From fe8008e5edc489a33650fabbf1e0ba98788884e7 Mon Sep 17 00:00:00 2001 From: Zach Gollwitzer Date: Tue, 8 Apr 2025 14:51:29 -0400 Subject: [PATCH] Clean up rules stimulus controller --- app/controllers/rules_controller.rb | 2 +- app/javascript/controllers/rule_controller.js | 163 +++++++++--------- app/models/rule/action_executor.rb | 3 +- .../set_transaction_category.rb | 4 + .../action_executor/set_transaction_tags.rb | 4 + app/models/rule/condition_filter.rb | 8 +- app/views/rule/actions/_action.html.erb | 4 +- app/views/rule/conditions/_condition.html.erb | 12 +- app/views/rules/edit.html.erb | 1 + app/views/rules/index.html.erb | 1 + app/views/rules/new.html.erb | 1 + 11 files changed, 118 insertions(+), 85 deletions(-) diff --git a/app/controllers/rules_controller.rb b/app/controllers/rules_controller.rb index 5defb5ff..d85a47db 100644 --- a/app/controllers/rules_controller.rb +++ b/app/controllers/rules_controller.rb @@ -2,7 +2,7 @@ class RulesController < ApplicationController before_action :set_rule, only: [ :show, :edit, :update, :destroy ] def index - @rules = Current.family.rules + @rules = Current.family.rules.order(created_at: :desc) render layout: "settings" end diff --git a/app/javascript/controllers/rule_controller.js b/app/javascript/controllers/rule_controller.js index 7e91c352..5930ae9d 100644 --- a/app/javascript/controllers/rule_controller.js +++ b/app/javascript/controllers/rule_controller.js @@ -3,8 +3,7 @@ import { Controller } from "@hotwired/stimulus"; // Connects to data-controller="rule" export default class extends Controller { static values = { - conditionsRegistry: Array, - actionsRegistry: Array, + registry: Object, }; static targets = [ @@ -14,12 +13,13 @@ export default class extends Controller { "condition", "actionsList", "action", - "destroyInput", + "destroyField", + "operatorField", + "valueField", ]; initialize() { - console.log(this.conditionsRegistryValue); - console.log(this.actionsRegistryValue); + console.log(this.registryValue); } addCondition() { @@ -32,53 +32,18 @@ export default class extends Controller { } handleConditionTypeChange(e) { - const definition = this.conditionsRegistryValue.find((def) => { - return def.condition_type === e.target.value; - }); + const definition = this.#getConditionFilterDefinition(e.target.value); + const conditionEl = this.#getEventConditionEl(e.target); + const valueFieldEl = this.#getFieldEl(this.valueFieldTargets, conditionEl); - const conditionEl = this.conditionTargets.find((t) => { - return t.contains(e.target); - }); + this.#updateOperatorsField(definition, conditionEl); - const operatorSelectEl = conditionEl.querySelector( - "select[data-id='operator-select']", - ); - - operatorSelectEl.innerHTML = definition.operators - .map((operator) => { - return ``; - }) - .join(""); - - const valueInputEl = conditionEl.querySelector("[data-id='value-input']"); - - if (definition.input_type === "select") { - // Select input - const selectEl = document.createElement("select"); - - // Set data-id, name, id - selectEl.setAttribute("data-id", "value-input"); - selectEl.setAttribute("name", valueInputEl.name); - selectEl.setAttribute("id", valueInputEl.id); - - // Populate options - definition.options.forEach((option) => { - const optionEl = document.createElement("option"); - optionEl.value = option[1]; - optionEl.textContent = option[0]; - selectEl.appendChild(optionEl); - }); - - valueInputEl.replaceWith(selectEl); + if (definition.type === "select") { + const selectEl = this.#buildSelectInput(definition, valueFieldEl); + valueFieldEl.replaceWith(selectEl); } else { - // Text input - const inputEl = document.createElement("input"); - inputEl.setAttribute("data-id", "value-input"); - inputEl.setAttribute("name", valueInputEl.name); - inputEl.setAttribute("id", valueInputEl.id); - inputEl.setAttribute("type", definition.input_type); - - valueInputEl.replaceWith(inputEl); + const inputEl = this.#buildTextInput(definition, valueFieldEl); + valueFieldEl.replaceWith(inputEl); } } @@ -92,32 +57,15 @@ export default class extends Controller { } handleActionTypeChange(e) { - const definition = this.actionsRegistryValue.find((def) => { - return def.action_type === e.target.value; - }); + const definition = this.#getActionExecutorDefinition(e.target.value); + const actionEl = this.#getEventActionEl(e.target); + const valueFieldEl = this.#getFieldEl(this.valueFieldTargets, actionEl); - const actionEl = this.actionTargets.find((t) => { - return t.contains(e.target); - }); - - const valueInputEl = actionEl.querySelector("[data-id='value-input']"); - - if (definition.input_type === "select") { - const selectEl = document.createElement("select"); - selectEl.setAttribute("data-id", "value-input"); - selectEl.setAttribute("name", valueInputEl.name); - selectEl.setAttribute("id", valueInputEl.id); - - definition.options.forEach((option) => { - const optionEl = document.createElement("option"); - optionEl.value = option[1]; - optionEl.textContent = option[0]; - selectEl.appendChild(optionEl); - }); - - valueInputEl.replaceWith(selectEl); + if (definition.type === "select") { + const selectEl = this.#buildSelectInput(definition, valueFieldEl); + valueFieldEl.replaceWith(selectEl); } else { - valueInputEl.classList.add("hidden"); + valueFieldEl.classList.add("hidden"); } } @@ -145,16 +93,77 @@ export default class extends Controller { } } - #destroyRuleItem(itemEl) { - const destroyInputEl = this.destroyInputTargets.find((el) => { - return itemEl.contains(el); + #updateOperatorsField(definition, conditionEl) { + const operatorFieldEl = this.#getFieldEl( + this.operatorFieldTargets, + conditionEl, + ); + + operatorFieldEl.innerHTML = definition.operators + .map((operator) => { + return ``; + }) + .join(""); + } + + #buildTextInput(definition, fieldEl) { + const inputEl = document.createElement("input"); + inputEl.setAttribute("data-rule-target", "valueField"); + inputEl.setAttribute("name", fieldEl.name); + inputEl.setAttribute("id", fieldEl.id); + inputEl.setAttribute("type", definition.type); + + return inputEl; + } + + #buildSelectInput(definition, fieldEl) { + const selectEl = document.createElement("select"); + selectEl.setAttribute("data-rule-target", "valueField"); + selectEl.setAttribute("name", fieldEl.name); + selectEl.setAttribute("id", fieldEl.id); + + definition.options.forEach((option) => { + const optionEl = document.createElement("option"); + optionEl.textContent = option[0]; + optionEl.value = option[1]; + selectEl.appendChild(optionEl); }); + return selectEl; + } + + #destroyRuleItem(itemEl) { + const destroyFieldEl = this.#getFieldEl(this.destroyFieldTargets, itemEl); + itemEl.classList.add("hidden"); - destroyInputEl.value = true; + destroyFieldEl.value = true; } #uniqueKey() { return `${Date.now()}_${Math.floor(Math.random() * 100000)}`; } + + #getConditionFilterDefinition(key) { + return this.registryValue.filters.find((filter) => { + return filter.key === key; + }); + } + + #getActionExecutorDefinition(key) { + return this.registryValue.executors.find((executor) => { + return executor.key === key; + }); + } + + #getEventConditionEl(childEl) { + return this.conditionTargets.find((t) => t.contains(childEl)); + } + + #getEventActionEl(childEl) { + return this.actionTargets.find((t) => t.contains(childEl)); + } + + #getFieldEl(targets, containerEl) { + return targets.find((t) => containerEl.contains(t)); + } } diff --git a/app/models/rule/action_executor.rb b/app/models/rule/action_executor.rb index 49936b69..6ac7dab5 100644 --- a/app/models/rule/action_executor.rb +++ b/app/models/rule/action_executor.rb @@ -29,7 +29,8 @@ class Rule::ActionExecutor { type: type, key: key, - label: label + label: label, + options: options } end diff --git a/app/models/rule/action_executor/set_transaction_category.rb b/app/models/rule/action_executor/set_transaction_category.rb index f064d748..2b3382f6 100644 --- a/app/models/rule/action_executor/set_transaction_category.rb +++ b/app/models/rule/action_executor/set_transaction_category.rb @@ -1,4 +1,8 @@ class Rule::ActionExecutor::SetTransactionCategory < Rule::ActionExecutor + def type + "select" + end + def options family.categories.pluck(:name, :id) end diff --git a/app/models/rule/action_executor/set_transaction_tags.rb b/app/models/rule/action_executor/set_transaction_tags.rb index fa46bb39..adee9550 100644 --- a/app/models/rule/action_executor/set_transaction_tags.rb +++ b/app/models/rule/action_executor/set_transaction_tags.rb @@ -1,4 +1,8 @@ class Rule::ActionExecutor::SetTransactionTags < Rule::ActionExecutor + def type + "select" + end + def options family.tags.pluck(:name, :id) end diff --git a/app/models/rule/condition_filter.rb b/app/models/rule/condition_filter.rb index a689bb27..58ad12c5 100644 --- a/app/models/rule/condition_filter.rb +++ b/app/models/rule/condition_filter.rb @@ -47,13 +47,19 @@ class Rule::ConditionFilter { type: type, key: key, - label: label + label: label, + operators: operators, + options: options } end private attr_reader :rule + def family + rule.family + end + def build_sanitized_where_condition(field, operator, value) sanitized_value = operator == "like" ? ActiveRecord::Base.sanitize_sql_like(value) : value diff --git a/app/views/rule/actions/_action.html.erb b/app/views/rule/actions/_action.html.erb index 8906164c..f789876c 100644 --- a/app/views/rule/actions/_action.html.erb +++ b/app/views/rule/actions/_action.html.erb @@ -4,10 +4,10 @@ <% rule = action.rule %>
  • - <%= form.hidden_field :_destroy, value: false, data: { rule_target: "destroyInput" } %> + <%= form.hidden_field :_destroy, value: false, data: { rule_target: "destroyField" } %> <%= form.select :action_type, rule.action_types, {}, data: { action: "rule#handleActionTypeChange" } %> to - <%= form.select :value, action.options, {}, data: { id: "value-input" } %> + <%= form.select :value, action.options, {}, data: { rule_target: "valueField" } %>