Files
sure/app/views/imports/new.html.erb
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

147 lines
6.5 KiB
Plaintext

<%= render DS::Dialog.new do |dialog| %>
<% dialog.with_header(title: t(".title"), subtitle: t(".description")) %>
<% dialog.with_body do %>
<div class="rounded-xl bg-container-inset p-1">
<h3 class="uppercase text-secondary text-xs font-medium px-3 py-1.5"><%= t(".sources") %></h3>
<ul class="bg-container shadow-border-xs rounded-lg">
<li>
<% if @pending_import.present? && (params[:type].nil? || params[:type] == @pending_import.type) %>
<%= link_to import_path(@pending_import), class: "flex items-center justify-between p-4 group cursor-pointer", data: { turbo: false } do %>
<div class="flex items-center gap-2">
<div class="bg-orange-500/5 rounded-md w-8 h-8 flex items-center justify-center">
<span class="text-orange-500">
<%= icon("loader", color: "current") %>
</span>
</div>
<span class="text-sm text-primary group-hover:text-secondary">
<%= t(".resume", type: @pending_import.type.titleize) %>
</span>
</div>
<%= icon("chevron-right") %>
<% end %>
<%= render "shared/ruler" %>
</li>
<% end %>
<% if Current.family.accounts.any? && (params[:type].nil? || params[:type] == "TransactionImport") %>
<li>
<%= button_to imports_path(import: { type: "TransactionImport" }), class: "flex items-center justify-between p-4 group cursor-pointer w-full", data: { turbo: false } do %>
<div class="flex items-center gap-2">
<div class="bg-indigo-500/5 rounded-md w-8 h-8 flex items-center justify-center">
<span class="text-indigo-500">
<%= icon("file-spreadsheet", color: "current") %>
</span>
</div>
<span class="text-sm text-primary group-hover:text-secondary">
<%= t(".import_transactions") %>
</span>
</div>
<%= icon("chevron-right") %>
<% end %>
<%= render "shared/ruler" %>
</li>
<% end %>
<% if Current.family.accounts.any? && (params[:type].nil? || params[:type] == "TradeImport") %>
<li>
<%= button_to imports_path(import: { type: "TradeImport" }), class: "flex items-center justify-between p-4 group cursor-pointer w-full", data: { turbo: false } do %>
<div class="flex items-center gap-2">
<div class="bg-yellow-500/5 rounded-md w-8 h-8 flex items-center justify-center">
<span class="text-yellow-500">
<%= icon("square-percent", color: "current") %>
</span>
</div>
<span class="text-sm text-primary group-hover:text-secondary">
<%= t(".import_portfolio") %>
</span>
</div>
<%= icon("chevron-right") %>
<% end %>
<%= render "shared/ruler" %>
</li>
<% end %>
<% if params[:type].nil? || params[:type] == "AccountImport" %>
<li>
<%= button_to imports_path(import: { type: "AccountImport" }), class: "flex items-center justify-between p-4 group cursor-pointer w-full", data: { turbo: false } do %>
<div class="flex items-center gap-2">
<div class="bg-violet-500/5 rounded-md w-8 h-8 flex items-center justify-center">
<span class="text-violet-500">
<%= icon("building", color: "current") %>
</span>
</div>
<span class="text-sm text-primary group-hover:text-secondary">
<%= t(".import_accounts") %>
</span>
</div>
<%= icon("chevron-right") %>
<% end %>
<%= render "shared/ruler" %>
</li>
<% end %>
<% if params[:type].nil? || params[:type] == "CategoryImport" %>
<li>
<%= button_to imports_path(import: { type: "CategoryImport" }), class: "flex items-center justify-between p-4 group cursor-pointer w-full", data: { turbo: false } do %>
<div class="flex items-center gap-2">
<div class="bg-blue-500/5 rounded-md w-8 h-8 flex items-center justify-center">
<span class="text-blue-500">
<%= icon("shapes", color: "current") %>
</span>
</div>
<span class="text-sm text-primary group-hover:text-secondary">
<%= t(".import_categories") %>
</span>
</div>
<%= icon("chevron-right") %>
<% end %>
<%= render "shared/ruler" %>
</li>
<% end %>
<% if params[:type].nil? || params[:type] == "RuleImport" %>
<li>
<%= button_to imports_path(import: { type: "RuleImport" }), class: "flex items-center justify-between p-4 group cursor-pointer w-full", data: { turbo: false } do %>
<div class="flex items-center gap-2">
<div class="bg-green-500/5 rounded-md w-8 h-8 flex items-center justify-center">
<span class="text-green-500">
<%= icon("workflow", color: "current") %>
</span>
</div>
<span class="text-sm text-primary group-hover:text-secondary">
<%= t(".import_rules") %>
</span>
</div>
<%= icon("chevron-right") %>
<% end %>
<%= render "shared/ruler" %>
</li>
<% end %>
<% if Current.family.accounts.any? && (params[:type].nil? || params[:type] == "MintImport" || params[:type] == "TransactionImport") %>
<li>
<%= button_to imports_path(import: { type: "MintImport" }), class: "flex items-center justify-between p-4 group w-full", data: { turbo: false } do %>
<div class="flex items-center gap-2">
<%= image_tag("mint-logo.jpeg", alt: "Mint logo", class: "w-8 h-8 rounded-md") %>
<span class="text-sm text-primary">
<%= t(".import_mint") %>
</span>
</div>
<%= icon("chevron-right") %>
<% end %>
<%= render "shared/ruler" %>
</li>
<% end %>
</ul>
</div>
<% end %>
<% end %>