1
0
Fork 0
mirror of https://github.com/maybe-finance/maybe.git synced 2025-08-10 07:55:21 +02:00

Clean up rules stimulus controller

This commit is contained in:
Zach Gollwitzer 2025-04-08 14:51:29 -04:00
parent 8796450308
commit fe8008e5ed
11 changed files with 118 additions and 85 deletions

View file

@ -2,7 +2,7 @@ class RulesController < ApplicationController
before_action :set_rule, only: [ :show, :edit, :update, :destroy ] before_action :set_rule, only: [ :show, :edit, :update, :destroy ]
def index def index
@rules = Current.family.rules @rules = Current.family.rules.order(created_at: :desc)
render layout: "settings" render layout: "settings"
end end

View file

@ -3,8 +3,7 @@ import { Controller } from "@hotwired/stimulus";
// Connects to data-controller="rule" // Connects to data-controller="rule"
export default class extends Controller { export default class extends Controller {
static values = { static values = {
conditionsRegistry: Array, registry: Object,
actionsRegistry: Array,
}; };
static targets = [ static targets = [
@ -14,12 +13,13 @@ export default class extends Controller {
"condition", "condition",
"actionsList", "actionsList",
"action", "action",
"destroyInput", "destroyField",
"operatorField",
"valueField",
]; ];
initialize() { initialize() {
console.log(this.conditionsRegistryValue); console.log(this.registryValue);
console.log(this.actionsRegistryValue);
} }
addCondition() { addCondition() {
@ -32,53 +32,18 @@ export default class extends Controller {
} }
handleConditionTypeChange(e) { handleConditionTypeChange(e) {
const definition = this.conditionsRegistryValue.find((def) => { const definition = this.#getConditionFilterDefinition(e.target.value);
return def.condition_type === e.target.value; const conditionEl = this.#getEventConditionEl(e.target);
}); const valueFieldEl = this.#getFieldEl(this.valueFieldTargets, conditionEl);
const conditionEl = this.conditionTargets.find((t) => { this.#updateOperatorsField(definition, conditionEl);
return t.contains(e.target);
});
const operatorSelectEl = conditionEl.querySelector( if (definition.type === "select") {
"select[data-id='operator-select']", const selectEl = this.#buildSelectInput(definition, valueFieldEl);
); valueFieldEl.replaceWith(selectEl);
operatorSelectEl.innerHTML = definition.operators
.map((operator) => {
return `<option value="${operator}">${operator}</option>`;
})
.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);
} else { } else {
// Text input const inputEl = this.#buildTextInput(definition, valueFieldEl);
const inputEl = document.createElement("input"); valueFieldEl.replaceWith(inputEl);
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);
} }
} }
@ -92,32 +57,15 @@ export default class extends Controller {
} }
handleActionTypeChange(e) { handleActionTypeChange(e) {
const definition = this.actionsRegistryValue.find((def) => { const definition = this.#getActionExecutorDefinition(e.target.value);
return def.action_type === e.target.value; const actionEl = this.#getEventActionEl(e.target);
}); const valueFieldEl = this.#getFieldEl(this.valueFieldTargets, actionEl);
const actionEl = this.actionTargets.find((t) => { if (definition.type === "select") {
return t.contains(e.target); const selectEl = this.#buildSelectInput(definition, valueFieldEl);
}); valueFieldEl.replaceWith(selectEl);
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);
} else { } else {
valueInputEl.classList.add("hidden"); valueFieldEl.classList.add("hidden");
} }
} }
@ -145,16 +93,77 @@ export default class extends Controller {
} }
} }
#destroyRuleItem(itemEl) { #updateOperatorsField(definition, conditionEl) {
const destroyInputEl = this.destroyInputTargets.find((el) => { const operatorFieldEl = this.#getFieldEl(
return itemEl.contains(el); this.operatorFieldTargets,
conditionEl,
);
operatorFieldEl.innerHTML = definition.operators
.map((operator) => {
return `<option value="${operator}">${operator}</option>`;
})
.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"); itemEl.classList.add("hidden");
destroyInputEl.value = true; destroyFieldEl.value = true;
} }
#uniqueKey() { #uniqueKey() {
return `${Date.now()}_${Math.floor(Math.random() * 100000)}`; 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));
}
} }

View file

@ -29,7 +29,8 @@ class Rule::ActionExecutor
{ {
type: type, type: type,
key: key, key: key,
label: label label: label,
options: options
} }
end end

View file

@ -1,4 +1,8 @@
class Rule::ActionExecutor::SetTransactionCategory < Rule::ActionExecutor class Rule::ActionExecutor::SetTransactionCategory < Rule::ActionExecutor
def type
"select"
end
def options def options
family.categories.pluck(:name, :id) family.categories.pluck(:name, :id)
end end

View file

@ -1,4 +1,8 @@
class Rule::ActionExecutor::SetTransactionTags < Rule::ActionExecutor class Rule::ActionExecutor::SetTransactionTags < Rule::ActionExecutor
def type
"select"
end
def options def options
family.tags.pluck(:name, :id) family.tags.pluck(:name, :id)
end end

View file

@ -47,13 +47,19 @@ class Rule::ConditionFilter
{ {
type: type, type: type,
key: key, key: key,
label: label label: label,
operators: operators,
options: options
} }
end end
private private
attr_reader :rule attr_reader :rule
def family
rule.family
end
def build_sanitized_where_condition(field, operator, value) def build_sanitized_where_condition(field, operator, value)
sanitized_value = operator == "like" ? ActiveRecord::Base.sanitize_sql_like(value) : value sanitized_value = operator == "like" ? ActiveRecord::Base.sanitize_sql_like(value) : value

View file

@ -4,10 +4,10 @@
<% rule = action.rule %> <% rule = action.rule %>
<li data-rule-target="action"> <li data-rule-target="action">
<%= 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" } %> <%= form.select :action_type, rule.action_types, {}, data: { action: "rule#handleActionTypeChange" } %>
<span>to</span> <span>to</span>
<%= form.select :value, action.options, {}, data: { id: "value-input" } %> <%= form.select :value, action.options, {}, data: { rule_target: "valueField" } %>
<button type="button" <button type="button"
data-action="rule#removeAction" data-action="rule#removeAction"
data-rule-destroy-param="<%= action.persisted? %>"> data-rule-destroy-param="<%= action.persisted? %>">

View file

@ -4,10 +4,16 @@
<% rule = condition.rule %> <% rule = condition.rule %>
<li data-rule-target="condition"> <li data-rule-target="condition">
<%= form.hidden_field :_destroy, value: false, data: { rule_target: "destroyInput" } %> <%= form.hidden_field :_destroy, value: false, data: { rule_target: "destroyField" } %>
<%= form.select :condition_type, rule.condition_types, {}, data: { action: "rule#handleConditionTypeChange" } %> <%= form.select :condition_type, rule.condition_types, {}, data: { action: "rule#handleConditionTypeChange" } %>
<%= form.select :operator, condition.operators, {}, data: { id: "operator-select" } %> <%= form.select :operator, condition.operators, {}, data: { rule_target: "operatorField" } %>
<%= form.text_field :value, data: { id: "value-input" } %>
<% if condition.filter.type == "select" %>
<%= form.select :value, condition.options, {}, data: { rule_target: "valueField" } %>
<% else %>
<%= form.text_field :value, data: { rule_target: "valueField" } %>
<% end %>
<button type="button" <button type="button"
data-action="rule#removeCondition" data-action="rule#removeCondition"
data-rule-destroy-param="<%= condition.persisted? %>"> data-rule-destroy-param="<%= condition.persisted? %>">

View file

@ -1,3 +1,4 @@
<%= link_to "Back to rules", rules_path %>
<h2>Edit <%= @rule.resource_type %> rule</h2> <h2>Edit <%= @rule.resource_type %> rule</h2>
<%= render "rules/form", rule: @rule %> <%= render "rules/form", rule: @rule %>

View file

@ -1,6 +1,7 @@
<% content_for :page_title, "Rules" %> <% content_for :page_title, "Rules" %>
<div> <div>
<%= link_to "New rule", new_rule_path %>
<ul> <ul>
<% @rules.each do |rule| %> <% @rules.each do |rule| %>
<li> <li>

View file

@ -1,3 +1,4 @@
<%= link_to "Back to rules", rules_path %>
<h2>New <%= @rule.resource_type %> rule</h2> <h2>New <%= @rule.resource_type %> rule</h2>
<%= render "rules/form", rule: @rule %> <%= render "rules/form", rule: @rule %>