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 ]
def index
@rules = Current.family.rules
@rules = Current.family.rules.order(created_at: :desc)
render layout: "settings"
end

View file

@ -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 `<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);
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 `<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");
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));
}
}

View file

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

View file

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

View file

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

View file

@ -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

View file

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

View file

@ -4,10 +4,16 @@
<% rule = condition.rule %>
<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 :operator, condition.operators, {}, data: { id: "operator-select" } %>
<%= form.text_field :value, data: { id: "value-input" } %>
<%= form.select :operator, condition.operators, {}, data: { rule_target: "operatorField" } %>
<% 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"
data-action="rule#removeCondition"
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>
<%= render "rules/form", rule: @rule %>

View file

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

View file

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