mirror of
https://github.com/we-promise/sure.git
synced 2026-09-09 00:24:15 +00:00
* feat(rules): support multiple tags in the set transaction tags action Fixes #3353. Reuses the existing DS::TagSelect multi-select tag picker (made generic via attribute:/show_label:) instead of a native <select multiple>, so the UX matches the rest of the app. Multiple tag ids are stored as a comma-separated string in the existing value column, keeping single-tag rows backward compatible with no migration. Also closes a read-modify-write race in SetTransactionTags#execute via with_lock. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * fix(rules): address automated review findings on multi-tag actions - Fix data export/import: multi-tag actions were exported/imported as one opaque comma string, losing all but a bogus combined tag on restore. Each tag id is now resolved/reconstructed independently, with a backward-compatible scalar value_ref for single-tag actions. - Fix N+1 in Rule::Action#value_display (options queried once per tag). - Add aria-label to DS::TagSelect's trigger button when show_label is false, so the control keeps an accessible name. - Localize the "to" label in rule action rows (rules.actions.to_label). - Use a monotonic counter instead of Date.now() for nested form indices, closing a same-millisecond collision window. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * fix(rules): batch tag lookups in multi-tag import resolution Avoids one find_by query per tag name when reconstructing multi-tag rule actions during import. * fix(rules): resolve jjmata review findings on multi-tag action - rules_controller.js: prefix the JS-side nested-form index counter with "new_" so it can never collide with the numeric indexes Rails assigns to already-persisted conditions/actions on an edit form. - data_exporter.rb: key the value_ref scalar/array decision off the number of tag ids on the action, not the number that still resolve, so a partially-orphaned multi-tag action keeps round-tripping as an array. - rule_import.rb: split comma-separated set_transaction_tags values into individual tag names during CSV rule import, matching the batched resolution already used by Family::DataImporter. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * fix(rules): CSV-quote multi-tag names so commas don't split them CodeRabbit flagged that a tag name containing a comma (e.g. "Food, Dining") would be silently split into two tags when round-tripped through the comma-separated multi-tag value/CSV formats used by Family::DataExporter, Family::DataImporter, and RuleImport. Add Rule::Action.encode_multi_value_names/.decode_multi_value_names, backed by Ruby's CSV line quoting, and use them at all three call sites instead of a plain join(",")/split(","). A single name without a comma round-trips byte-identical to before, so existing exports and CSV rule templates are unaffected. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --------- Co-authored-by: GFR <248542187+gfr-free@users.noreply.github.com> Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
83 lines
3.1 KiB
Ruby
83 lines
3.1 KiB
Ruby
class Rule::Action < ApplicationRecord
|
|
belongs_to :rule, touch: true
|
|
|
|
validates :action_type, presence: true
|
|
|
|
# Pre-seed (watermark): when a send_email_notification action is created — on a
|
|
# new rule OR added to an existing one — record all currently-matching
|
|
# transactions as already-delivered WITHOUT sending, so the rule only ever
|
|
# emails about transactions that appear AFTER the action exists.
|
|
#
|
|
# Uses after_create_commit (not after_create): nested children persist before
|
|
# the parent rule commits, and the pre-seed reads the rule's conditions, which
|
|
# must be committed first.
|
|
#
|
|
# after_update_commit covers the edit flow: the action_type select is editable
|
|
# for persisted actions (see rules_controller#rule_params), so an existing
|
|
# action can be CHANGED to send_email_notification. Without re-seeding, the
|
|
# next apply/sync would email every historical match. Guard on the type change
|
|
# so we only watermark when an action actually becomes email-notify.
|
|
after_create_commit :seed_notification_baseline
|
|
after_update_commit :seed_notification_baseline, if: :saved_change_to_action_type?
|
|
|
|
# Accepts an Array (e.g. from a multi-select tag input) and stores it as a
|
|
# comma-separated string in the existing `value` column, so multi-value
|
|
# actions don't require a schema change. A single scalar value round-trips
|
|
# unchanged, which keeps existing single-value rows backward compatible.
|
|
def value=(val)
|
|
val = val.reject(&:blank?).join(",") if val.is_a?(Array)
|
|
super(val)
|
|
end
|
|
|
|
# Encodes a list of tag (or other) names as a single comma-separated string
|
|
# for the portable `value`/CSV-import representation, CSV-quoting any name
|
|
# that itself contains a comma (e.g. "Food, Dining") so it round-trips as
|
|
# one name instead of being split into two on import.
|
|
def self.encode_multi_value_names(names)
|
|
CSV.generate_line(names, row_sep: "")
|
|
end
|
|
|
|
# Inverse of .encode_multi_value_names. Also accepts a plain unquoted
|
|
# comma-separated string (the format used before quoting was introduced),
|
|
# which CSV parses the same way as long as no name contains a comma.
|
|
def self.decode_multi_value_names(str)
|
|
CSV.parse_line(str.to_s) || []
|
|
end
|
|
|
|
def apply(resource_scope, ignore_attribute_locks: false, rule_run: nil)
|
|
executor.execute(resource_scope, value: execution_value, ignore_attribute_locks: ignore_attribute_locks, rule_run: rule_run) || 0
|
|
end
|
|
|
|
def options
|
|
executor.options
|
|
end
|
|
|
|
def value_display
|
|
return "" if value.blank?
|
|
|
|
cached_options = options
|
|
return "" if cached_options.blank?
|
|
|
|
labels_by_id = cached_options.to_h { |label, id| [ id.to_s, label ] }
|
|
Array(execution_value).filter_map { |v| labels_by_id[v] }.join(", ")
|
|
end
|
|
|
|
def executor
|
|
rule.registry.get_executor!(action_type)
|
|
end
|
|
|
|
private
|
|
def execution_value
|
|
executor.type == "multi_select" ? value.to_s.split(",") : value
|
|
end
|
|
|
|
def seed_notification_baseline
|
|
return unless action_type == "send_email_notification"
|
|
|
|
NotificationDelivery.record_for(
|
|
rule_id: rule_id,
|
|
transaction_ids: rule.matching_transaction_ids
|
|
)
|
|
end
|
|
end
|