fix: Handle empty compound conditions on rules index (#965)

* fix: Handle empty compound conditions on rules index

* fix: avoid contradictory rule condition summary on /rules

* refactor: move rules condition display logic from view to model

* fix: localize rule title fallback and preload conditions in rules index
This commit is contained in:
Pluto
2026-02-13 13:53:24 -05:00
committed by GitHub
parent 34afc1f597
commit e99e38a91c
16 changed files with 99 additions and 18 deletions

View File

@@ -11,7 +11,7 @@ class RulesController < ApplicationController
@sort_by = "name" unless allowed_columns.include?(@sort_by) @sort_by = "name" unless allowed_columns.include?(@sort_by)
@direction = "asc" unless [ "asc", "desc" ].include?(@direction) @direction = "asc" unless [ "asc", "desc" ].include?(@direction)
@rules = Current.family.rules.order(@sort_by => @direction) @rules = Current.family.rules.includes(conditions: :sub_conditions).order(@sort_by => @direction)
# Fetch recent rule runs with pagination # Fetch recent rule runs with pagination
recent_runs_scope = RuleRun recent_runs_scope = RuleRun

View File

@@ -86,14 +86,23 @@ class Rule < ApplicationRecord
end end
def primary_condition_title def primary_condition_title
return "No conditions" if conditions.none? condition = displayed_condition
return I18n.t("rules.no_condition") if condition.blank?
first_condition = conditions.first "If #{condition.filter.label.downcase} #{condition.operator} #{condition.value_display}"
if first_condition.compound? && first_condition.sub_conditions.any? end
first_sub_condition = first_condition.sub_conditions.first
"If #{first_sub_condition.filter.label.downcase} #{first_sub_condition.operator} #{first_sub_condition.value_display}" def displayed_condition
else displayable_conditions.first
"If #{first_condition.filter.label.downcase} #{first_condition.operator} #{first_condition.value_display}" end
def additional_displayable_conditions_count
[ displayable_conditions.size - 1, 0 ].max
end
def displayable_conditions
conditions.filter_map do |condition|
condition.compound? ? condition.sub_conditions.first : condition
end end
end end

View File

@@ -6,20 +6,22 @@
<h3 class="font-medium text-md"><%= rule.name %></h3> <h3 class="font-medium text-md"><%= rule.name %></h3>
<% end %> <% end %>
<% if rule.conditions.any? %> <% if rule.conditions.any? %>
<% displayed_condition = rule.displayed_condition %>
<% additional_condition_count = rule.additional_displayable_conditions_count %>
<div class="flex items-center gap-2 mt-1"> <div class="flex items-center gap-2 mt-1">
<div class="flex items-center gap-1 text-secondary w-16 shrink-0"> <div class="flex items-center gap-1 text-secondary w-16 shrink-0">
<span class="font-mono text-xs">IF</span> <span class="font-mono text-xs">IF</span>
</div> </div>
<p class="flex items-center flex-wrap gap-1.5 m-0"> <p class="flex items-center flex-wrap gap-1.5 m-0">
<span class="px-2 py-1 border border-secondary rounded-full"> <span class="px-2 py-1 border border-secondary rounded-full">
<% if rule.conditions.first.compound? %> <% if displayed_condition.present? %>
<%= rule.conditions.first.sub_conditions.first.filter.label %> <%= rule.conditions.first.sub_conditions.first.operator %> <%= rule.conditions.first.sub_conditions.first.value_display %> <%= displayed_condition.filter.label %> <%= displayed_condition.operator %> <%= displayed_condition.value_display %>
<% else %> <% else %>
<%= rule.conditions.first.filter.label %> <%= rule.conditions.first.operator %> <%= rule.conditions.first.value_display %> <%= t("rules.no_condition") %>
<% end %> <% end %>
</span> </span>
<% if rule.conditions.count > 1 %> <% if additional_condition_count.positive? %>
and <%= rule.conditions.count - 1 %> more <%= rule.conditions.count - 1 == 1 ? "condition" : "conditions" %> and <%= additional_condition_count %> more <%= additional_condition_count == 1 ? "condition" : "conditions" %>
<% end %> <% end %>
</p> </p>
</div> </div>

View File

@@ -19,6 +19,7 @@ ca:
success: Totes les regles s'han posat a cua per a execució success: Totes les regles s'han posat a cua per a execució
view_usage: Veure l'historial d'ús view_usage: Veure l'historial d'ús
no_action: Sense acció no_action: Sense acció
no_condition: Sense condició
recent_runs: recent_runs:
columns: columns:
date_time: Data/Hora date_time: Data/Hora

View File

@@ -2,6 +2,7 @@
de: de:
rules: rules:
no_action: Keine Aktion no_action: Keine Aktion
no_condition: Keine Bedingung
recent_runs: recent_runs:
title: Letzte Ausführungen title: Letzte Ausführungen
description: Zeige die Ausführungsgeschichte deiner Regeln einschließlich Erfolgs-/Fehlerstatus und Transaktionsanzahlen. description: Zeige die Ausführungsgeschichte deiner Regeln einschließlich Erfolgs-/Fehlerstatus und Transaktionsanzahlen.
@@ -22,4 +23,3 @@ de:
pending: Ausstehend pending: Ausstehend
success: Erfolgreich success: Erfolgreich
failed: Fehlgeschlagen failed: Fehlgeschlagen

View File

@@ -2,6 +2,7 @@
en: en:
rules: rules:
no_action: No Action no_action: No Action
no_condition: No condition
actions: actions:
value_placeholder: Enter a value value_placeholder: Enter a value
apply_all: apply_all:

View File

@@ -2,6 +2,7 @@
es: es:
rules: rules:
no_action: Sin acción no_action: Sin acción
no_condition: Sin condición
recent_runs: recent_runs:
title: Ejecuciones Recientes title: Ejecuciones Recientes
description: Ver el historial de ejecución de tus reglas incluyendo el estado de éxito/fallo y los conteos de transacciones. description: Ver el historial de ejecución de tus reglas incluyendo el estado de éxito/fallo y los conteos de transacciones.
@@ -22,4 +23,3 @@ es:
pending: Pendiente pending: Pendiente
success: Éxito success: Éxito
failed: Fallido failed: Fallido

View File

@@ -2,6 +2,7 @@
fr: fr:
rules: rules:
no_action: Aucune action no_action: Aucune action
no_condition: Aucune condition
actions: actions:
value_placeholder: Entrez une valeur value_placeholder: Entrez une valeur
apply_all: apply_all:

View File

@@ -2,6 +2,7 @@
nb: nb:
rules: rules:
no_action: Ingen handling no_action: Ingen handling
no_condition: Ingen betingelse
recent_runs: recent_runs:
title: Siste Kjøringer title: Siste Kjøringer
description: Se kjøringsloggen for reglene dine inkludert suksess/feil-status og transaksjonsantall. description: Se kjøringsloggen for reglene dine inkludert suksess/feil-status og transaksjonsantall.
@@ -22,4 +23,3 @@ nb:
pending: Ventende pending: Ventende
success: Vellykket success: Vellykket
failed: Mislyktes failed: Mislyktes

View File

@@ -2,6 +2,7 @@
nl: nl:
rules: rules:
no_action: Geen actie no_action: Geen actie
no_condition: Geen voorwaarde
actions: actions:
value_placeholder: Voer een waarde in value_placeholder: Voer een waarde in
apply_all: apply_all:

View File

@@ -2,6 +2,7 @@
ro: ro:
rules: rules:
no_action: Nicio acțiune no_action: Nicio acțiune
no_condition: Nicio condiție
recent_runs: recent_runs:
title: Rulări Recente title: Rulări Recente
description: Vezi istoricul de execuție al regulilor tale incluzând statusul de succes/eșec și numărul de tranzacții. description: Vezi istoricul de execuție al regulilor tale incluzând statusul de succes/eșec și numărul de tranzacții.
@@ -22,4 +23,3 @@ ro:
pending: În Așteptare pending: În Așteptare
success: Succes success: Succes
failed: Eșuat failed: Eșuat

View File

@@ -2,6 +2,7 @@
tr: tr:
rules: rules:
no_action: İşlem yok no_action: İşlem yok
no_condition: Koşul yok
recent_runs: recent_runs:
title: Son Çalıştırmalar title: Son Çalıştırmalar
description: Başarı/başarısızlık durumu ve işlem sayıları dahil olmak üzere kurallarınızın yürütme geçmişini görüntüleyin. description: Başarı/başarısızlık durumu ve işlem sayıları dahil olmak üzere kurallarınızın yürütme geçmişini görüntüleyin.
@@ -22,4 +23,3 @@ tr:
pending: Beklemede pending: Beklemede
success: Başarılı success: Başarılı
failed: Başarısız failed: Başarısız

View File

@@ -2,6 +2,7 @@
zh-CN: zh-CN:
rules: rules:
no_action: 无操作 no_action: 无操作
no_condition: 无条件
recent_runs: recent_runs:
columns: columns:
date_time: 日期/时间 date_time: 日期/时间

View File

@@ -2,6 +2,7 @@
zh-TW: zh-TW:
rules: rules:
no_action: 無動作 no_action: 無動作
no_condition: 無條件
recent_runs: recent_runs:
title: 最近執行紀錄 title: 最近執行紀錄
description: 查看規則的執行歷史,包括成功/失敗狀態以及交易處理筆數。 description: 查看規則的執行歷史,包括成功/失敗狀態以及交易處理筆數。

View File

@@ -180,6 +180,36 @@ class RulesControllerTest < ActionDispatch::IntegrationTest
assert_redirected_to rules_url assert_redirected_to rules_url
end end
test "index renders when rule has empty compound condition" do
malformed_rule = @user.family.rules.build(resource_type: "transaction")
malformed_rule.conditions.build(condition_type: "compound", operator: "and")
malformed_rule.actions.build(action_type: "exclude_transaction")
malformed_rule.save!
get rules_url
assert_response :success
assert_includes response.body, I18n.t("rules.no_condition")
end
test "index uses next valid condition when first compound condition is empty" do
rule = @user.family.rules.build(resource_type: "transaction")
rule.conditions.build(condition_type: "compound", operator: "and")
rule.conditions.build(condition_type: "transaction_name", operator: "like", value: "edge-case-name")
rule.actions.build(action_type: "exclude_transaction")
rule.save!
get rules_url
assert_response :success
assert_select "##{ActionView::RecordIdentifier.dom_id(rule)}" do
assert_select "span", text: /edge-case-name/
assert_select "span", text: /#{Regexp.escape(I18n.t("rules.no_condition"))}/, count: 0
assert_select "p", text: /and 1 more condition/, count: 0
end
end
test "should get confirm_all" do test "should get confirm_all" do
get confirm_all_rules_url get confirm_all_rules_url
assert_response :success assert_response :success

View File

@@ -138,6 +138,40 @@ class RuleTest < ActiveSupport::TestCase
assert_equal [ "Compound conditions cannot be nested" ], rule.errors.full_messages assert_equal [ "Compound conditions cannot be nested" ], rule.errors.full_messages
end end
test "displayed_condition falls back to next valid condition when first compound condition is empty" do
rule = Rule.new(
family: @family,
resource_type: "transaction",
actions: [ Rule::Action.new(action_type: "exclude_transaction") ],
conditions: [
Rule::Condition.new(condition_type: "compound", operator: "and"),
Rule::Condition.new(condition_type: "transaction_name", operator: "like", value: "starbucks")
]
)
displayed_condition = rule.displayed_condition
assert_not_nil displayed_condition
assert_equal "transaction_name", displayed_condition.condition_type
assert_equal "like", displayed_condition.operator
assert_equal "starbucks", displayed_condition.value
end
test "additional_displayable_conditions_count ignores empty compound conditions" do
rule = Rule.new(
family: @family,
resource_type: "transaction",
actions: [ Rule::Action.new(action_type: "exclude_transaction") ],
conditions: [
Rule::Condition.new(condition_type: "compound", operator: "and"),
Rule::Condition.new(condition_type: "transaction_name", operator: "like", value: "first"),
Rule::Condition.new(condition_type: "transaction_amount", operator: ">", value: 100)
]
)
assert_equal 1, rule.additional_displayable_conditions_count
end
test "rule matching on transaction details" do test "rule matching on transaction details" do
# Create PayPal transaction with underlying merchant in details # Create PayPal transaction with underlying merchant in details
paypal_entry = create_transaction( paypal_entry = create_transaction(