mirror of
https://github.com/maybe-finance/maybe.git
synced 2025-08-10 07:55:21 +02:00
Complete dynamic rule form, split Stimulus controllers by resource
This commit is contained in:
parent
3b464b97aa
commit
eabacb2d01
14 changed files with 362 additions and 239 deletions
|
@ -348,7 +348,7 @@
|
||||||
@layer components {
|
@layer components {
|
||||||
/* Buttons */
|
/* Buttons */
|
||||||
.btn {
|
.btn {
|
||||||
@apply px-3 py-2 rounded-lg text-sm font-medium cursor-pointer disabled:cursor-not-allowed focus:outline-gray-500;
|
@apply inline-flex items-center justify-center gap-2 px-3 py-2 rounded-lg text-sm font-medium cursor-pointer disabled:cursor-not-allowed focus:outline-gray-500;
|
||||||
@apply transition-all duration-300;
|
@apply transition-all duration-300;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -365,7 +365,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn--ghost {
|
.btn--ghost {
|
||||||
@apply border border-transparent text-gray-900 hover:bg-gray-100;
|
@apply border border-transparent text-secondary hover:border hover:border-alpha-black-200 hover:bg-gray-50;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn--outline-destructive {
|
.btn--outline-destructive {
|
||||||
|
|
|
@ -9,13 +9,21 @@ class RulesController < ApplicationController
|
||||||
end
|
end
|
||||||
|
|
||||||
def new
|
def new
|
||||||
actions = []
|
@rule = Current.family.rules.build(
|
||||||
|
resource_type: params[:resource_type] || "transaction",
|
||||||
if params[:action_type]
|
conditions: [
|
||||||
actions << Rule::Action.new(action_type: params[:action_type], value: params[:action_value])
|
Rule::Condition.new(
|
||||||
end
|
condition_type: params[:condition_type] || "transaction_amount",
|
||||||
|
value: params[:condition_value]
|
||||||
@rule = Current.family.rules.build(resource_type: params[:resource_type] || "transaction", actions: actions)
|
)
|
||||||
|
],
|
||||||
|
actions: [
|
||||||
|
Rule::Action.new(
|
||||||
|
action_type: params[:action_type] || "set_transaction_category",
|
||||||
|
value: params[:action_value]
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
def create
|
def create
|
||||||
|
|
55
app/javascript/controllers/rule/actions_controller.js
Normal file
55
app/javascript/controllers/rule/actions_controller.js
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
import { Controller } from "@hotwired/stimulus";
|
||||||
|
|
||||||
|
// Connects to data-controller="rule--actions"
|
||||||
|
export default class extends Controller {
|
||||||
|
static values = { actionExecutors: Array };
|
||||||
|
static targets = ["destroyField", "actionValue"];
|
||||||
|
|
||||||
|
remove(e) {
|
||||||
|
if (e.params.destroy) {
|
||||||
|
this.destroyFieldTarget.value = true;
|
||||||
|
} else {
|
||||||
|
this.element.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleActionTypeChange(e) {
|
||||||
|
const actionExecutor = this.actionExecutorsValue.find(
|
||||||
|
(executor) => executor.key === e.target.value,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (actionExecutor.type === "select") {
|
||||||
|
this.#updateValueSelectFor(actionExecutor);
|
||||||
|
this.#showAndEnableValueSelect();
|
||||||
|
} else {
|
||||||
|
this.#hideAndDisableValueSelect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get valueSelectEl() {
|
||||||
|
return this.actionValueTarget.querySelector("select");
|
||||||
|
}
|
||||||
|
|
||||||
|
#showAndEnableValueSelect() {
|
||||||
|
this.actionValueTarget.classList.remove("hidden");
|
||||||
|
this.valueSelectEl.disabled = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
#hideAndDisableValueSelect() {
|
||||||
|
this.actionValueTarget.classList.add("hidden");
|
||||||
|
this.valueSelectEl.disabled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
#updateValueSelectFor(actionExecutor) {
|
||||||
|
// Clear existing options
|
||||||
|
this.valueSelectEl.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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
100
app/javascript/controllers/rule/conditions_controller.js
Normal file
100
app/javascript/controllers/rule/conditions_controller.js
Normal file
|
@ -0,0 +1,100 @@
|
||||||
|
import { Controller } from "@hotwired/stimulus";
|
||||||
|
|
||||||
|
// Connects to data-controller="rule--conditions"
|
||||||
|
export default class extends Controller {
|
||||||
|
static values = { conditionFilters: Array };
|
||||||
|
static targets = [
|
||||||
|
"destroyField",
|
||||||
|
"filterValue",
|
||||||
|
"operatorSelect",
|
||||||
|
"subConditionTemplate",
|
||||||
|
"subConditionsList",
|
||||||
|
];
|
||||||
|
|
||||||
|
addSubCondition() {
|
||||||
|
const html = this.subConditionTemplateTarget.innerHTML.replaceAll(
|
||||||
|
"IDX_PLACEHOLDER",
|
||||||
|
this.#uniqueKey(),
|
||||||
|
);
|
||||||
|
|
||||||
|
this.subConditionsListTarget.insertAdjacentHTML("beforeend", html);
|
||||||
|
}
|
||||||
|
|
||||||
|
remove(e) {
|
||||||
|
if (e.params.destroy) {
|
||||||
|
this.destroyFieldTarget.value = true;
|
||||||
|
} else {
|
||||||
|
this.element.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleConditionTypeChange(e) {
|
||||||
|
const conditionFilter = this.conditionFiltersValue.find(
|
||||||
|
(filter) => filter.key === e.target.value,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (conditionFilter.type === "select") {
|
||||||
|
this.#buildSelectFor(conditionFilter);
|
||||||
|
} else {
|
||||||
|
this.#buildTextInputFor(conditionFilter);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.#updateOperatorsField(conditionFilter);
|
||||||
|
}
|
||||||
|
|
||||||
|
get valueInputEl() {
|
||||||
|
const textInput = this.filterValueTarget.querySelector("input");
|
||||||
|
const selectInput = this.filterValueTarget.querySelector("select");
|
||||||
|
|
||||||
|
return textInput || selectInput;
|
||||||
|
}
|
||||||
|
|
||||||
|
#updateOperatorsField(conditionFilter) {
|
||||||
|
this.operatorSelectTarget.innerHTML = "";
|
||||||
|
|
||||||
|
for (const operator of conditionFilter.operators) {
|
||||||
|
const optionEl = document.createElement("option");
|
||||||
|
optionEl.value = operator;
|
||||||
|
optionEl.textContent = operator;
|
||||||
|
this.operatorSelectTarget.appendChild(optionEl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#buildSelectFor(conditionFilter) {
|
||||||
|
const selectEl = this.#convertFormFieldTo("select", this.valueInputEl);
|
||||||
|
|
||||||
|
for (const option of conditionFilter.options) {
|
||||||
|
const optionEl = document.createElement("option");
|
||||||
|
optionEl.value = option[1];
|
||||||
|
optionEl.textContent = option[0];
|
||||||
|
selectEl.appendChild(optionEl);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.valueInputEl.replaceWith(selectEl);
|
||||||
|
}
|
||||||
|
|
||||||
|
#buildTextInputFor(conditionFilter) {
|
||||||
|
const textInput = this.#convertFormFieldTo("input", this.valueInputEl);
|
||||||
|
textInput.placeholder = "Enter a value";
|
||||||
|
textInput.type = conditionFilter.type; // "text" || "number"
|
||||||
|
|
||||||
|
this.valueInputEl.replaceWith(textInput);
|
||||||
|
}
|
||||||
|
|
||||||
|
#convertFormFieldTo(type, el) {
|
||||||
|
const priorClasses = el.classList;
|
||||||
|
const priorId = el.id;
|
||||||
|
const priorName = el.name;
|
||||||
|
|
||||||
|
const newFormField = document.createElement(type);
|
||||||
|
newFormField.classList.add(...priorClasses);
|
||||||
|
newFormField.id = priorId;
|
||||||
|
newFormField.name = priorName;
|
||||||
|
|
||||||
|
return newFormField;
|
||||||
|
}
|
||||||
|
|
||||||
|
#uniqueKey() {
|
||||||
|
return Math.random().toString(36).substring(2, 15);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,169 +0,0 @@
|
||||||
import { Controller } from "@hotwired/stimulus";
|
|
||||||
|
|
||||||
// Connects to data-controller="rule"
|
|
||||||
export default class extends Controller {
|
|
||||||
static values = {
|
|
||||||
registry: Object,
|
|
||||||
};
|
|
||||||
|
|
||||||
static targets = [
|
|
||||||
"newConditionTemplate",
|
|
||||||
"newActionTemplate",
|
|
||||||
"conditionsList",
|
|
||||||
"condition",
|
|
||||||
"actionsList",
|
|
||||||
"action",
|
|
||||||
"destroyField",
|
|
||||||
"operatorField",
|
|
||||||
"valueField",
|
|
||||||
];
|
|
||||||
|
|
||||||
initialize() {
|
|
||||||
console.log(this.registryValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
addCondition() {
|
|
||||||
const html = this.newConditionTemplateTarget.innerHTML.replaceAll(
|
|
||||||
"IDX_PLACEHOLDER",
|
|
||||||
this.#uniqueKey(),
|
|
||||||
);
|
|
||||||
|
|
||||||
this.conditionsListTarget.insertAdjacentHTML("beforeend", html);
|
|
||||||
}
|
|
||||||
|
|
||||||
handleConditionTypeChange(e) {
|
|
||||||
const definition = this.#getConditionFilterDefinition(e.target.value);
|
|
||||||
const conditionEl = this.#getEventConditionEl(e.target);
|
|
||||||
const valueFieldEl = this.#getFieldEl(this.valueFieldTargets, conditionEl);
|
|
||||||
|
|
||||||
this.#updateOperatorsField(definition, conditionEl);
|
|
||||||
|
|
||||||
if (definition.type === "select") {
|
|
||||||
const selectEl = this.#buildSelectInput(definition, valueFieldEl);
|
|
||||||
valueFieldEl.replaceWith(selectEl);
|
|
||||||
} else {
|
|
||||||
const inputEl = this.#buildTextInput(definition, valueFieldEl);
|
|
||||||
valueFieldEl.replaceWith(inputEl);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
addAction() {
|
|
||||||
const html = this.newActionTemplateTarget.innerHTML.replaceAll(
|
|
||||||
"IDX_PLACEHOLDER",
|
|
||||||
this.#uniqueKey(),
|
|
||||||
);
|
|
||||||
|
|
||||||
this.actionsListTarget.insertAdjacentHTML("beforeend", html);
|
|
||||||
}
|
|
||||||
|
|
||||||
handleActionTypeChange(e) {
|
|
||||||
const definition = this.#getActionExecutorDefinition(e.target.value);
|
|
||||||
const actionEl = this.#getEventActionEl(e.target);
|
|
||||||
const valueFieldEl = this.#getFieldEl(this.valueFieldTargets, actionEl);
|
|
||||||
|
|
||||||
if (definition.type === "select") {
|
|
||||||
const selectEl = this.#buildSelectInput(definition, valueFieldEl);
|
|
||||||
valueFieldEl.replaceWith(selectEl);
|
|
||||||
} else {
|
|
||||||
valueFieldEl.classList.add("hidden");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
removeCondition(e) {
|
|
||||||
const conditionEl = this.conditionTargets.find((el) => {
|
|
||||||
return el.contains(e.target);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (e.params.destroy) {
|
|
||||||
this.#destroyRuleItem(conditionEl);
|
|
||||||
} else {
|
|
||||||
conditionEl.remove();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
removeAction(e) {
|
|
||||||
const actionEl = this.actionTargets.find((el) => {
|
|
||||||
return el.contains(e.target);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (e.params.destroy) {
|
|
||||||
this.#destroyRuleItem(actionEl);
|
|
||||||
} else {
|
|
||||||
actionEl.remove();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#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");
|
|
||||||
destroyFieldEl.value = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
#uniqueKey() {
|
|
||||||
return Date.now();
|
|
||||||
}
|
|
||||||
|
|
||||||
#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));
|
|
||||||
}
|
|
||||||
}
|
|
50
app/javascript/controllers/rules_controller.js
Normal file
50
app/javascript/controllers/rules_controller.js
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
import { Controller } from "@hotwired/stimulus";
|
||||||
|
|
||||||
|
// Connects to data-controller="rules"
|
||||||
|
export default class extends Controller {
|
||||||
|
static targets = [
|
||||||
|
"conditionTemplate",
|
||||||
|
"conditionGroupTemplate",
|
||||||
|
"actionTemplate",
|
||||||
|
"conditionsList",
|
||||||
|
"actionsList",
|
||||||
|
"effectiveDateInput",
|
||||||
|
];
|
||||||
|
|
||||||
|
addConditionGroup() {
|
||||||
|
this.#appendTemplate(
|
||||||
|
this.conditionGroupTemplateTarget,
|
||||||
|
this.conditionsListTarget,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
addCondition() {
|
||||||
|
this.#appendTemplate(
|
||||||
|
this.conditionTemplateTarget,
|
||||||
|
this.conditionsListTarget,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
addAction() {
|
||||||
|
this.#appendTemplate(this.actionTemplateTarget, this.actionsListTarget);
|
||||||
|
}
|
||||||
|
|
||||||
|
clearEffectiveDate() {
|
||||||
|
this.effectiveDateInputTarget.value = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
enableEffectiveDate() {}
|
||||||
|
|
||||||
|
#appendTemplate(templateEl, listEl) {
|
||||||
|
const html = templateEl.innerHTML.replaceAll(
|
||||||
|
"IDX_PLACEHOLDER",
|
||||||
|
this.#uniqueKey(),
|
||||||
|
);
|
||||||
|
|
||||||
|
listEl.insertAdjacentHTML("beforeend", html);
|
||||||
|
}
|
||||||
|
|
||||||
|
#uniqueKey() {
|
||||||
|
return Date.now();
|
||||||
|
}
|
||||||
|
}
|
|
@ -5,6 +5,8 @@ class Rule::Condition < ApplicationRecord
|
||||||
has_many :sub_conditions, class_name: "Rule::Condition", foreign_key: :parent_id, dependent: :destroy, inverse_of: :parent
|
has_many :sub_conditions, class_name: "Rule::Condition", foreign_key: :parent_id, dependent: :destroy, inverse_of: :parent
|
||||||
|
|
||||||
validates :condition_type, presence: true
|
validates :condition_type, presence: true
|
||||||
|
validates :operator, presence: true
|
||||||
|
validates :value, presence: true, unless: -> { compound? }
|
||||||
|
|
||||||
accepts_nested_attributes_for :sub_conditions, allow_destroy: true
|
accepts_nested_attributes_for :sub_conditions, allow_destroy: true
|
||||||
|
|
||||||
|
|
|
@ -2,21 +2,24 @@
|
||||||
|
|
||||||
<% action = form.object %>
|
<% action = form.object %>
|
||||||
<% rule = action.rule %>
|
<% rule = action.rule %>
|
||||||
|
<% needs_value = action.executor.type == "select" %>
|
||||||
|
|
||||||
<li data-rule-target="action">
|
<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_target: "destroyField" } %>
|
<%= form.hidden_field :_destroy, value: false, data: { rule__actions_target: "destroyField" } %>
|
||||||
<%= form.select :action_type, rule.action_executors.map { |executor| [ executor.label, executor.key ] }, {}, data: { action: "rule#handleActionTypeChange" } %>
|
|
||||||
|
|
||||||
<% if action.executor.type == "select" %>
|
<div class="grow flex gap-2 items-center h-full">
|
||||||
<span>to</span>
|
<%= form.select :action_type, rule.action_executors.map { |executor| [ executor.label, executor.key ] }, {}, data: { action: "rule--actions#handleActionTypeChange" } %>
|
||||||
<%= form.select :value, action.options, {}, data: { rule_target: "valueField" } %>
|
|
||||||
<% else %>
|
<%= tag.div class: class_names("flex items-center gap-2", "hidden" => !needs_value),
|
||||||
<%= form.hidden_field :value, data: { rule_target: "valueField" } %>
|
data: { rule__actions_target: "actionValue" } do %>
|
||||||
<% end %>
|
<span class="font-medium uppercase text-xs">to</span>
|
||||||
|
<%= form.select :value, action.options, {}, disabled: !needs_value %>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
|
||||||
<button type="button"
|
<button type="button"
|
||||||
data-action="rule#removeAction"
|
data-action="rule--actions#remove"
|
||||||
data-rule-destroy-param="<%= action.persisted? %>">
|
data-rule--actions-destroy-param="<%= action.persisted? %>">
|
||||||
Remove
|
<%= icon("trash-2", color: "gray", size: "sm") %>
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
|
|
|
@ -1,22 +1,34 @@
|
||||||
<%# locals: (form:) %>
|
<%# locals: (form:, show_prefix: true) %>
|
||||||
|
|
||||||
<% condition = form.object %>
|
<% condition = form.object %>
|
||||||
<% rule = condition.rule %>
|
<% rule = condition.rule %>
|
||||||
|
|
||||||
<li data-rule-target="condition">
|
<li data-controller="rule--conditions" data-rule--conditions-condition-filters-value="<%= rule.condition_filters.to_json %>" class="flex items-center gap-3">
|
||||||
<%= form.hidden_field :_destroy, value: false, data: { rule_target: "destroyField" } %>
|
<% if form.index.to_i > 0 && show_prefix %>
|
||||||
<%= form.select :condition_type, rule.condition_filters.map { |filter| [ filter.label, filter.key ] }, {}, data: { action: "rule#handleConditionTypeChange" } %>
|
<div class="pl-4">
|
||||||
<%= form.select :operator, condition.operators, {}, data: { rule_target: "operatorField" } %>
|
<span class="font-medium uppercase text-xs">and</span>
|
||||||
|
</div>
|
||||||
<% if condition.filter.type == "select" %>
|
|
||||||
<%= form.select :value, condition.options, {}, data: { rule_target: "valueField" } %>
|
|
||||||
<% else %>
|
|
||||||
<%= form.text_field :value, data: { rule_target: "valueField" } %>
|
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
|
<div class="grow flex gap-2 items-center h-full">
|
||||||
|
<%= form.hidden_field :_destroy, value: false, data: { rule__conditions_target: "destroyField" } %>
|
||||||
|
|
||||||
|
<%= form.select :condition_type, rule.condition_filters.map { |filter| [ filter.label, filter.key ] }, {}, data: { action: "rule--conditions#handleConditionTypeChange" } %>
|
||||||
|
|
||||||
|
<%= form.select :operator, condition.operators, { container_class: "w-fit min-w-16" }, data: { rule__conditions_target: "operatorSelect" } %>
|
||||||
|
|
||||||
|
<div data-rule--conditions-target="filterValue">
|
||||||
|
<% if condition.filter.type == "select" %>
|
||||||
|
<%= form.select :value, condition.options, {} %>
|
||||||
|
<% else %>
|
||||||
|
<%= form.text_field :value, placeholder: "Enter a value" %>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<button type="button"
|
<button type="button"
|
||||||
data-action="rule#removeCondition"
|
data-action="rule--conditions#remove"
|
||||||
data-rule-destroy-param="<%= condition.persisted? %>">
|
data-rule--conditions-destroy-param="<%= condition.persisted? %>">
|
||||||
Remove
|
<%= icon("trash-2", color: "gray", size: "sm") %>
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
|
|
|
@ -1,15 +1,44 @@
|
||||||
<%# locals: (form:) %>
|
<%# locals: (form:) %>
|
||||||
|
|
||||||
<% condition = form.object %>
|
<% condition = form.object %>
|
||||||
|
<% rule = condition.rule %>
|
||||||
|
|
||||||
<li>
|
<li data-controller="rule--conditions element-removal" class="border border-alpha-black-100 rounded-md p-4 space-y-3">
|
||||||
<% if condition.compound? %>
|
|
||||||
<ul class="border border-gray-200 p-2">
|
<%= form.hidden_field :condition_type, value: "compound" %>
|
||||||
<%= form.fields_for :sub_conditions do |scf| %>
|
|
||||||
<%= render "rule/conditions/condition_group", form: scf %>
|
<div class="flex items-center justify-between gap-2">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<% unless form.index == 0 %>
|
||||||
|
<div class="pl-2">
|
||||||
|
<span class="font-medium uppercase text-xs">and</span>
|
||||||
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
</ul>
|
<p class="text-sm text-secondary">match</p>
|
||||||
<% else %>
|
<%= form.select :operator, [["all", "and"], ["any", "or"]], { container_class: "w-fit" }, data: { rules_target: "operatorField" } %>
|
||||||
<%= render "rule/conditions/condition", form: form %>
|
<p class="text-sm text-secondary">of the following conditions</p>
|
||||||
<% end %>
|
</div>
|
||||||
|
|
||||||
|
<button type="button" data-action="element-removal#remove">
|
||||||
|
<%= icon("trash-2", color: "gray", size: "sm") %>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<%# Sub-condition template, used by Stimulus controller to add new sub-conditions dynamically %>
|
||||||
|
<template data-rule--conditions-target="subConditionTemplate">
|
||||||
|
<%= form.fields_for :sub_conditions, Rule::Condition.new(parent: condition, condition_type: rule.condition_filters.first.key), child_index: "IDX_PLACEHOLDER" do |scf| %>
|
||||||
|
<%= render "rule/conditions/condition", form: scf %>
|
||||||
|
<% end %>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<ul data-rule--conditions-target="subConditionsList" class="space-y-3">
|
||||||
|
<%= form.fields_for :sub_conditions do |scf| %>
|
||||||
|
<%= render "rule/conditions/condition", form: scf, show_prefix: false %>
|
||||||
|
<% end %>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<button type="button" class="btn btn--ghost" data-action="rule--conditions#addSubCondition">
|
||||||
|
<%= icon("plus", color: "gray", size: "sm") %>
|
||||||
|
<span>Add condition</span>
|
||||||
|
</button>
|
||||||
</li>
|
</li>
|
||||||
|
|
|
@ -1,11 +1,7 @@
|
||||||
<%# locals: (rule:) %>
|
<%# locals: (rule:) %>
|
||||||
|
|
||||||
<%= form_with model: rule,
|
<%= styled_form_with model: rule, class: "space-y-4",
|
||||||
class: "p-4 space-y-8 min-w-[600px]",
|
data: { controller: "rules", rule_registry_value: rule.registry.to_json } do |f| %>
|
||||||
data: {
|
|
||||||
controller: "rule",
|
|
||||||
rule_registry_value: rule.registry.to_json
|
|
||||||
} do |f| %>
|
|
||||||
|
|
||||||
<%= f.hidden_field :resource_type, value: rule.resource_type %>
|
<%= f.hidden_field :resource_type, value: rule.resource_type %>
|
||||||
|
|
||||||
|
@ -14,37 +10,56 @@
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
<section class="space-y-4">
|
<section class="space-y-4">
|
||||||
<h3 class="mb-4 font-bold">Conditions</h3>
|
<h3 class="text-sm font-medium">If <%= rule.resource_type %></h3>
|
||||||
<hr>
|
|
||||||
|
|
||||||
<template data-rule-target="newConditionTemplate">
|
<%# Condition template, used by Stimulus controller to add new conditions dynamically %>
|
||||||
<%= f.fields_for :conditions, Rule::Condition.new(rule: rule, condition_type: rule.condition_filters.first.key), child_index: "IDX_PLACEHOLDER" do |cf| %>
|
<template data-rules-target="conditionGroupTemplate">
|
||||||
|
<%= f.fields_for :conditions, Rule::Condition.new(rule: rule, condition_type: "compound", operator: "and"), child_index: "IDX_PLACEHOLDER" do |cf| %>
|
||||||
<%= render "rule/conditions/condition_group", form: cf %>
|
<%= render "rule/conditions/condition_group", form: cf %>
|
||||||
<% end %>
|
<% end %>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<ul data-rule-target="conditionsList">
|
<%# Condition template, used by Stimulus controller to add new conditions dynamically %>
|
||||||
<span>When</span>
|
<template data-rules-target="conditionTemplate">
|
||||||
|
<%= f.fields_for :conditions, Rule::Condition.new(rule: rule, condition_type: rule.condition_filters.first.key), child_index: "IDX_PLACEHOLDER" do |cf| %>
|
||||||
|
<%= render "rule/conditions/condition", form: cf %>
|
||||||
|
<% end %>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<ul data-rules-target="conditionsList" class="space-y-3 mb-4">
|
||||||
<%= f.fields_for :conditions do |cf| %>
|
<%= f.fields_for :conditions do |cf| %>
|
||||||
<%= render "rule/conditions/condition_group", form: cf %>
|
<% if cf.object.compound? %>
|
||||||
|
<%= render "rule/conditions/condition_group", form: cf %>
|
||||||
|
<% else %>
|
||||||
|
<%= render "rule/conditions/condition", form: cf %>
|
||||||
|
<% end %>
|
||||||
<% end %>
|
<% end %>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<button type="button" data-action="rule#addCondition">Add condition</button>
|
<div class="flex items-center gap-2">
|
||||||
|
<button type="button" data-action="rules#addCondition" class="btn btn--ghost">
|
||||||
|
<%= icon("plus") %>
|
||||||
|
<span>Add condition</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button type="button" data-action="rules#addConditionGroup" class="btn btn--ghost">
|
||||||
|
<%= icon("boxes") %>
|
||||||
|
<span>Add condition group</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="space-y-4">
|
<section class="space-y-4">
|
||||||
<h3 class="mb-4 font-bold">Actions</h3>
|
<h3 class="text-sm font-medium">Then</h3>
|
||||||
<hr>
|
|
||||||
|
|
||||||
<template data-rule-target="newActionTemplate">
|
<%# Action template, used by Stimulus controller to add new actions dynamically %>
|
||||||
|
<template data-rules-target="actionTemplate">
|
||||||
<%= f.fields_for :actions, Rule::Action.new(rule: rule, action_type: rule.action_executors.first.key), child_index: "IDX_PLACEHOLDER" do |af| %>
|
<%= f.fields_for :actions, Rule::Action.new(rule: rule, action_type: rule.action_executors.first.key), child_index: "IDX_PLACEHOLDER" do |af| %>
|
||||||
<%= render "rule/actions/action", form: af %>
|
<%= render "rule/actions/action", form: af %>
|
||||||
<% end %>
|
<% end %>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<ul data-rule-target="actionsList">
|
<ul data-rules-target="actionsList" class="space-y-3">
|
||||||
<%= f.fields_for :actions do |af| %>
|
<%= f.fields_for :actions do |af| %>
|
||||||
<%= render "rule/actions/action", form: af %>
|
<%= render "rule/actions/action", form: af %>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
@ -52,10 +67,32 @@
|
||||||
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
data-action="rule#addAction">
|
data-action="rules#addAction"
|
||||||
Add action
|
class="btn btn--ghost">
|
||||||
|
<%= icon("plus") %>
|
||||||
|
<span>Add action</span>
|
||||||
</button>
|
</button>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<section class="space-y-4">
|
||||||
|
<h3 class="text-sm font-medium">Apply this</h3>
|
||||||
|
|
||||||
|
<div class="space-y-2">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<%= f.radio_button :effective_date_enabled, false, checked: rule.effective_date.nil?, data: { action: "rules#clearEffectiveDate" } %>
|
||||||
|
<%= f.label :effective_date_enabled_false, "To all past and future #{rule.resource_type}s" %>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<%= f.radio_button :effective_date_enabled, true, checked: rule.effective_date.present? %>
|
||||||
|
<%= f.label :effective_date_enabled_true, "Starting from" %>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<%= f.date_field :effective_date, container_class: "w-fit", data: { rules_target: "effectiveDateInput" } %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<%= f.submit %>
|
<%= f.submit %>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
<%= link_to "Back to rules", rules_path %>
|
<%= link_to "Back to rules", rules_path %>
|
||||||
|
|
||||||
<%= modal do %>
|
<%= modal_form_wrapper title: "Edit #{@rule.resource_type} rule" do %>
|
||||||
<h2>Edit <%= @rule.resource_type %> rule</h2>
|
|
||||||
|
|
||||||
<%= render "rules/form", rule: @rule %>
|
<%= render "rules/form", rule: @rule %>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
<%= link_to "Back to rules", rules_path %>
|
<%= link_to "Back to rules", rules_path %>
|
||||||
|
|
||||||
<%= modal do %>
|
<%= modal_form_wrapper title: "New #{@rule.resource_type} rule" do %>
|
||||||
<h2>New <%= @rule.resource_type %> rule</h2>
|
|
||||||
|
|
||||||
<%= render "rules/form", rule: @rule %>
|
<%= render "rules/form", rule: @rule %>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<%# locals: (content:, classes:) -%>
|
<%# locals: (content:, classes:) -%>
|
||||||
<%= turbo_frame_tag "modal" do %>
|
<%= turbo_frame_tag "modal" do %>
|
||||||
<dialog class="focus:outline-none m-auto bg-white shadow-border-xs rounded-2xl max-w-[580px] w-min-content h-fit overflow-visible <%= classes %>" data-controller="modal" data-action="mousedown->modal#clickOutside">
|
<dialog class="focus:outline-none m-auto bg-white shadow-border-xs rounded-2xl max-w-[580px] w-min-content h-fit overflow-auto <%= classes %>" data-controller="modal" data-action="mousedown->modal#clickOutside">
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
<%= content %>
|
<%= content %>
|
||||||
</div>
|
</div>
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue