Files
sure/app/helpers/imports_helper.rb
Juan José Mata e5ed946959 Add rules import/export support (#424)
* Add full import/export support for rules with versioned JSON schema

This commit implements comprehensive import/export functionality for rules,
allowing users to back up and restore their rule definitions.

Key features:
- Export rules to both CSV and NDJSON formats with versioned schema (v1)
- Import rules from CSV with full support for nested conditions and actions
- UUID to name mapping for categories and merchants for portability
- Support for compound conditions with sub-conditions
- Comprehensive test coverage for export and import functionality
- UI integration for rules import in the imports interface

Technical details:
- Extended Family::DataExporter to generate rules.csv and include rules in all.ndjson
- Created RuleImport model following the existing Import STI pattern
- Added migration for rule-specific columns in import_rows table
- Implemented serialization helpers to map UUIDs to human-readable names
- Added i18n support for the new import option
- Included versioning in NDJSON export to support future schema evolution

The implementation ensures rules can be safely exported from one family
and imported into another, even when category/merchant IDs differ,
by mapping between names and IDs during export/import.

* Fix AR migration version

* Mention support for rules export

* Rabbit suggestion

* Fix tests

* Missed schema.rb

* Fix sample CSV download for rule import

* Fix parsing in Rules import

* Fix tests

* Rule import message i18n

* Export tag names, not UUIDs

* Make sure tags are created if needed at import

* Avoid test errors when running in parallel

---------

Co-authored-by: Claude <noreply@anthropic.com>
2025-12-07 13:20:54 +01:00

75 lines
2.4 KiB
Ruby

module ImportsHelper
def mapping_label(mapping_class)
{
"Import::AccountTypeMapping" => "Account Type",
"Import::AccountMapping" => "Account",
"Import::CategoryMapping" => "Category",
"Import::TagMapping" => "Tag"
}.fetch(mapping_class.name)
end
def import_col_label(key)
{
date: "Date",
amount: "Amount",
name: "Name",
currency: "Currency",
category: "Category",
tags: "Tags",
account: "Account",
notes: "Notes",
qty: "Quantity",
ticker: "Ticker",
exchange: "Exchange",
price: "Price",
entity_type: "Type",
category_parent: "Parent category",
category_color: "Color",
category_classification: "Classification",
category_icon: "Lucide icon"
}[key]
end
def dry_run_resource(key)
map = {
transactions: DryRunResource.new(label: "Transactions", icon: "credit-card", text_class: "text-cyan-500", bg_class: "bg-cyan-500/5"),
accounts: DryRunResource.new(label: "Accounts", icon: "layers", text_class: "text-orange-500", bg_class: "bg-orange-500/5"),
categories: DryRunResource.new(label: "Categories", icon: "shapes", text_class: "text-blue-500", bg_class: "bg-blue-500/5"),
tags: DryRunResource.new(label: "Tags", icon: "tags", text_class: "text-violet-500", bg_class: "bg-violet-500/5"),
rules: DryRunResource.new(label: "Rules", icon: "workflow", text_class: "text-green-500", bg_class: "bg-green-500/5")
}
map[key]
end
def permitted_import_configuration_path(import)
if permitted_import_types.include?(import.type.underscore)
"import/configurations/#{import.type.underscore}"
else
raise "Unknown import type: #{import.type}"
end
end
def cell_class(row, field)
base = "bg-container text-sm focus:ring-gray-900 theme-dark:focus:ring-gray-100 focus:border-solid w-full max-w-full disabled:text-subdued"
row.valid? # populate errors
border = row.errors.key?(field) ? "border-destructive" : "border-transparent"
[ base, border ].join(" ")
end
def cell_is_valid?(row, field)
row.valid? # populate errors
!row.errors.key?(field)
end
private
def permitted_import_types
%w[transaction_import trade_import account_import mint_import category_import rule_import]
end
DryRunResource = Struct.new(:label, :icon, :text_class, :bg_class, keyword_init: true)
end