feat(rules): add not-equal, does-not-contain, and is-not-empty condition operators (#2529)

* feat(rules): add not-equal, does-not-contain, is-not-empty condition operators

Extend transaction rule conditions beyond "equal to" / "is empty":

- text:   add "does not contain" (not_like), "not equal to" (!=), "is not empty" (is_not_null)
- number: add "not equal to" (!=)
- select: add "not equal to" (!=), "is not empty" (is_not_null)

NULL handling is inclusive so the operators match user intent:
- "!=" uses IS DISTINCT FROM, so e.g. "category not equal to X" also matches
  uncategorized (NULL) transactions
- "does not contain" also matches rows where the field is NULL

transaction_type keeps its custom operator set, and transaction_details is
pinned to the original operators since its JSONB apply only supports
contains/equals/empty semantics.

The conditions Stimulus controller hides the value field for both valueless
operators (is_null and is_not_null).

* refactor(rules): address PR review feedback on condition operators

- Pass VALUELESS_OPERATORS from Ruby to JS via Stimulus value attribute
  instead of duplicating the list as a static class property, so there
  is a single source of truth for which operators suppress the value field
- Clarify IS DISTINCT FROM comment to note the NULL-inclusion behaviour
  is intentional for select-type fields (merchant_id, category_id) and
  not applicable to number fields where NULL is impossible at the DB level
- Add test that exercises the OR IS NULL branch of not_like by using
  transaction_notes (entries.notes is nullable, unlike entries.name)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(rules): localize condition operator labels via i18n

Moves all Rule::ConditionFilter operator labels (including ones that
predate this PR) out of OPERATORS_MAP and into config/locales, so
operators() resolves them through t() per request instead of hardcoded
English strings.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GvxjdTgH34cPoJenQAqnpN

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Ivan Kostiashov
2026-09-02 03:12:27 +02:00
committed by GitHub
co-authored by Claude Sonnet 5
parent 7ffe877531
commit 9714612bb1
7 changed files with 179 additions and 15 deletions
@@ -2,7 +2,7 @@ import { Controller } from "@hotwired/stimulus";
// Connects to data-controller="rule--conditions"
export default class extends Controller {
static values = { conditionFilters: Array };
static values = { conditionFilters: Array, valuelessOperators: Array };
static targets = [
"destroyField",
"filterValue",
@@ -12,7 +12,7 @@ export default class extends Controller {
];
connect() {
// Hide value field on initial load if operator is "is_null"
// Hide value field on initial load for valueless operators (e.g. "is_null")
this.#toggleValueFieldVisibility();
}
@@ -129,7 +129,7 @@ export default class extends Controller {
#toggleValueFieldVisibility() {
const operator = this.operatorSelectTarget.value;
if (operator === "is_null") {
if (this.valuelessOperatorsValue.includes(operator)) {
this.filterValueTarget.classList.add("hidden");
// Clear the value since it's not needed
if (this.valueInputEl) {