mirror of
https://github.com/maybe-finance/maybe.git
synced 2025-07-19 13:19:39 +02:00
Add rule option to change transaction name (#2175)
* Add change name rule for transaction * Use HTML template in the ERB, clone and inject those templates from the stimulus controller * Put back the ai_enabled check * Update docs * Example of what no case statement would look like * Remove action_type and needs_value now that controller is injecting templates/hiding action target * add "to" to template, improve no-option selection, ensure text box is cleared
This commit is contained in:
parent
c0267d5665
commit
60c3a04a48
6 changed files with 129 additions and 29 deletions
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
class Rule::ActionExecutor
|
||||
TYPES = [ "select", "function" ]
|
||||
TYPES = [ "select", "function", "text" ]
|
||||
|
||||
def initialize(rule)
|
||||
@rule = rule
|
||||
|
|
29
app/models/rule/action_executor/set_transaction_name.rb
Normal file
29
app/models/rule/action_executor/set_transaction_name.rb
Normal file
|
@ -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
|
|
@ -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?
|
||||
|
|
|
@ -2,7 +2,6 @@
|
|||
|
||||
<% action = form.object %>
|
||||
<% rule = action.rule %>
|
||||
<% needs_value = action.executor.type == "select" %>
|
||||
|
||||
<li data-controller="rule--actions" data-rule--actions-action-executors-value="<%= rule.action_executors.to_json %>" class="flex items-center gap-3">
|
||||
<%= form.hidden_field :_destroy, value: false, data: { rule__actions_target: "destroyField" } %>
|
||||
|
@ -12,10 +11,13 @@
|
|||
<%= form.select :action_type, rule.action_executors.map { |executor| [ executor.label, executor.key ] }, {}, data: { action: "rule--actions#handleActionTypeChange" } %>
|
||||
</div>
|
||||
|
||||
<%= tag.div class: class_names("min-w-1/2 flex items-center gap-2", "hidden" => !needs_value),
|
||||
<%= tag.div class: class_names("min-w-1/2 flex items-center gap-2"),
|
||||
data: { rule__actions_target: "actionValue" } do %>
|
||||
<%# Initial rendering based on rule.action_executors.first from the rule form. %>
|
||||
<%# This is currently always SetTransactionCategory from transaction_resource.rb, which is a select type. %>
|
||||
<%# Subsequent renders are injected by the Stimulus controller, which uses the templates from below. %>
|
||||
<span class="font-medium uppercase text-xs">to</span>
|
||||
<%= form.select :value, action.options || [], {}, disabled: !needs_value %>
|
||||
<%= form.select :value, action.options || [], {} %>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
|
@ -24,4 +26,17 @@
|
|||
size: "sm",
|
||||
as_button: true,
|
||||
data: { action: "rule--actions#remove", rule__actions_destroy_param: action.persisted? }) %>
|
||||
|
||||
<%# Templates for different input types - these will be cloned and used by the Stimulus controller %>
|
||||
<template data-rule--actions-target="selectTemplate">
|
||||
<span class="font-medium uppercase text-xs">to</span>
|
||||
<%= form.select :value, [], {} %>
|
||||
</template>
|
||||
|
||||
<template data-rule--actions-target="textTemplate">
|
||||
<span class="font-medium uppercase text-xs">to</span>
|
||||
<%= form.text_field :value, placeholder: "Enter a value" %>
|
||||
</template>
|
||||
|
||||
<%# The function type doesn't need an input, so no template is required.%>
|
||||
</li>
|
||||
|
|
|
@ -79,4 +79,25 @@ class Rule::ActionTest < ActiveSupport::TestCase
|
|||
assert_equal merchant.id, transaction.reload.merchant_id
|
||||
end
|
||||
end
|
||||
|
||||
test "set_transaction_name" do
|
||||
new_name = "Renamed Transaction"
|
||||
|
||||
# Does not modify transactions that are locked (user edited them)
|
||||
@txn1.lock!(:name)
|
||||
|
||||
action = Rule::Action.new(
|
||||
rule: @transaction_rule,
|
||||
action_type: "set_transaction_name",
|
||||
value: new_name
|
||||
)
|
||||
|
||||
action.apply(@rule_scope)
|
||||
|
||||
assert_not_equal new_name, @txn1.reload.entry.name
|
||||
|
||||
[ @txn2, @txn3 ].each do |transaction|
|
||||
assert_equal new_name, transaction.reload.entry.name
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue