diff --git a/app/javascript/controllers/rule/actions_controller.js b/app/javascript/controllers/rule/actions_controller.js index f4de40eb..0ca8a252 100644 --- a/app/javascript/controllers/rule/actions_controller.js +++ b/app/javascript/controllers/rule/actions_controller.js @@ -3,7 +3,12 @@ import { Controller } from "@hotwired/stimulus"; // Connects to data-controller="rule--actions" export default class extends Controller { static values = { actionExecutors: Array }; - static targets = ["destroyField", "actionValue"]; + static targets = [ + "destroyField", + "actionValue", + "selectTemplate", + "textTemplate" + ]; remove(e) { if (e.params.destroy) { @@ -19,38 +24,67 @@ export default class extends Controller { (executor) => executor.key === e.target.value, ); + // Clear any existing input elements first + this.#clearFormFields(); + if (actionExecutor.type === "select") { - this.#updateValueSelectFor(actionExecutor); - this.#showAndEnableValueSelect(); + this.#buildSelectFor(actionExecutor); + } else if (actionExecutor.type === "text") { + this.#buildTextInputFor(); } else { - this.#hideAndDisableValueSelect(); + // Hide for any type that doesn't need a value (e.g. function) + this.#hideActionValue(); } } - get valueSelectEl() { - return this.actionValueTarget.querySelector("select"); - } - - #showAndEnableValueSelect() { - this.actionValueTarget.classList.remove("hidden"); - this.valueSelectEl.disabled = false; - } - - #hideAndDisableValueSelect() { + #hideActionValue() { this.actionValueTarget.classList.add("hidden"); - this.valueSelectEl.disabled = true; } - #updateValueSelectFor(actionExecutor) { - // Clear existing options - this.valueSelectEl.innerHTML = ""; + #clearFormFields() { + // Remove all children from actionValueTarget + this.actionValueTarget.innerHTML = ""; + } - // Add new options - for (const option of actionExecutor.options) { - const optionEl = document.createElement("option"); - optionEl.value = option[1]; - optionEl.textContent = option[0]; - this.valueSelectEl.appendChild(optionEl); + #buildSelectFor(actionExecutor) { + // Clone the select template + const template = this.selectTemplateTarget.content.cloneNode(true); + const selectEl = template.querySelector("select"); + + // Add options to the select element + if (selectEl) { + selectEl.innerHTML = ""; + if (!actionExecutor.options || actionExecutor.options.length === 0) { + selectEl.disabled = true; + const optionEl = document.createElement("option"); + optionEl.textContent = "(none)"; + selectEl.appendChild(optionEl); + } else { + selectEl.disabled = false; + for (const option of actionExecutor.options) { + const optionEl = document.createElement("option"); + optionEl.value = option[1]; + optionEl.textContent = option[0]; + selectEl.appendChild(optionEl); + } + } } + + // Add the template content to the actionValue target and ensure it's visible + this.actionValueTarget.appendChild(template); + this.actionValueTarget.classList.remove("hidden"); + } + + #buildTextInputFor() { + // Clone the text template + const template = this.textTemplateTarget.content.cloneNode(true); + + // Ensure the input is always empty + const inputEl = template.querySelector("input"); + if (inputEl) inputEl.value = ""; + + // Add the template content to the actionValue target and ensure it's visible + this.actionValueTarget.appendChild(template); + this.actionValueTarget.classList.remove("hidden"); } } diff --git a/app/models/rule/action_executor.rb b/app/models/rule/action_executor.rb index 807e6cea..4a9a1185 100644 --- a/app/models/rule/action_executor.rb +++ b/app/models/rule/action_executor.rb @@ -1,5 +1,5 @@ class Rule::ActionExecutor - TYPES = [ "select", "function" ] + TYPES = [ "select", "function", "text" ] def initialize(rule) @rule = rule diff --git a/app/models/rule/action_executor/set_transaction_name.rb b/app/models/rule/action_executor/set_transaction_name.rb new file mode 100644 index 00000000..39f3ee26 --- /dev/null +++ b/app/models/rule/action_executor/set_transaction_name.rb @@ -0,0 +1,29 @@ +class Rule::ActionExecutor::SetTransactionName < Rule::ActionExecutor + def type + "text" + end + + def options + nil + end + + def execute(transaction_scope, value: nil, ignore_attribute_locks: false) + return if value.blank? + + scope = transaction_scope + unless ignore_attribute_locks + scope = scope.enrichable(:name) + end + + scope.each do |txn| + Rule.transaction do + txn.entry.log_enrichment!( + attribute_name: "name", + attribute_value: value, + source: "rule" + ) + txn.entry.update!(name: value) + end + end + end +end diff --git a/app/models/rule/registry/transaction_resource.rb b/app/models/rule/registry/transaction_resource.rb index 628d8cde..15aae288 100644 --- a/app/models/rule/registry/transaction_resource.rb +++ b/app/models/rule/registry/transaction_resource.rb @@ -15,7 +15,8 @@ class Rule::Registry::TransactionResource < Rule::Registry enabled_executors = [ Rule::ActionExecutor::SetTransactionCategory.new(rule), Rule::ActionExecutor::SetTransactionTags.new(rule), - Rule::ActionExecutor::SetTransactionMerchant.new(rule) + Rule::ActionExecutor::SetTransactionMerchant.new(rule), + Rule::ActionExecutor::SetTransactionName.new(rule) ] if ai_enabled? diff --git a/app/views/rule/actions/_action.html.erb b/app/views/rule/actions/_action.html.erb index f41e4857..ed126cfd 100644 --- a/app/views/rule/actions/_action.html.erb +++ b/app/views/rule/actions/_action.html.erb @@ -2,7 +2,6 @@ <% action = form.object %> <% rule = action.rule %> -<% needs_value = action.executor.type == "select" %>