mirror of
https://github.com/maybe-finance/maybe.git
synced 2025-07-19 13:19:39 +02:00
Allow CSV file upload in import flow (#986)
* Add .tool-versions to gitignore * Add dropzone js for drag and drop file uploads * UI for csv file uploads for import * dropzone controller and use lucide_icon instead of svg * Preview for file chosen * File upload * Remove dropzone * Normalize I18n keys and fix lint issues * Add system tests * Cleanup * Remove unwanted
This commit is contained in:
parent
41f9e23f8c
commit
cdbca5aff3
13 changed files with 307 additions and 41 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -51,6 +51,9 @@
|
||||||
# Ignore .devcontainer files
|
# Ignore .devcontainer files
|
||||||
compose-dev.yaml
|
compose-dev.yaml
|
||||||
|
|
||||||
|
# Ignore asdf ruby version file
|
||||||
|
.tool-versions
|
||||||
|
|
||||||
# Ignore GCP keyfile
|
# Ignore GCP keyfile
|
||||||
gcp-storage-keyfile.json
|
gcp-storage-keyfile.json
|
||||||
|
|
||||||
|
|
|
@ -38,6 +38,21 @@ class ImportsController < ApplicationController
|
||||||
def load
|
def load
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def upload_csv
|
||||||
|
begin
|
||||||
|
@import.raw_csv_str = import_params[:raw_csv_str].read
|
||||||
|
rescue NoMethodError
|
||||||
|
flash.now[:error] = "Please select a file to upload"
|
||||||
|
render :load, status: :unprocessable_entity and return
|
||||||
|
end
|
||||||
|
if @import.save
|
||||||
|
redirect_to configure_import_path(@import), notice: t(".import_loaded")
|
||||||
|
else
|
||||||
|
flash.now[:error] = @import.errors.full_messages.to_sentence
|
||||||
|
render :load, status: :unprocessable_entity
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def load_csv
|
def load_csv
|
||||||
if @import.update(import_params)
|
if @import.update(import_params)
|
||||||
redirect_to configure_import_path(@import), notice: t(".import_loaded")
|
redirect_to configure_import_path(@import), notice: t(".import_loaded")
|
||||||
|
|
94
app/javascript/controllers/csv_upload_controller.js
Normal file
94
app/javascript/controllers/csv_upload_controller.js
Normal file
|
@ -0,0 +1,94 @@
|
||||||
|
import { Controller } from "@hotwired/stimulus"
|
||||||
|
|
||||||
|
export default class extends Controller {
|
||||||
|
static targets = ["input", "preview", "submit", "filename", "filesize"]
|
||||||
|
|
||||||
|
connect() {
|
||||||
|
this.submitTarget.disabled = true
|
||||||
|
}
|
||||||
|
|
||||||
|
addFile(event) {
|
||||||
|
const file = event.target.files[0]
|
||||||
|
this._fileAdded(file)
|
||||||
|
}
|
||||||
|
|
||||||
|
dragover(event) {
|
||||||
|
event.preventDefault()
|
||||||
|
event.stopPropagation()
|
||||||
|
event.currentTarget.classList.add("bg-gray-100")
|
||||||
|
}
|
||||||
|
|
||||||
|
dragleave(event) {
|
||||||
|
event.preventDefault()
|
||||||
|
event.stopPropagation()
|
||||||
|
event.currentTarget.classList.remove("bg-gray-100")
|
||||||
|
}
|
||||||
|
|
||||||
|
drop(event) {
|
||||||
|
event.preventDefault()
|
||||||
|
event.stopPropagation()
|
||||||
|
event.currentTarget.classList.remove("bg-gray-100")
|
||||||
|
|
||||||
|
const file = event.dataTransfer.files[0]
|
||||||
|
if (file && this._isCSVFile(file)) {
|
||||||
|
this._setFileInput(file);
|
||||||
|
this._fileAdded(file)
|
||||||
|
} else {
|
||||||
|
this.previewTarget.classList.add("text-red-500")
|
||||||
|
this.previewTarget.textContent = "Only CSV files are allowed."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Private
|
||||||
|
|
||||||
|
_fetchFileSize(size) {
|
||||||
|
let fileSize = '';
|
||||||
|
if (size < 1024 * 1024) {
|
||||||
|
fileSize = (size / 1024).toFixed(2) + ' KB'; // Convert bytes to KB
|
||||||
|
} else {
|
||||||
|
fileSize = (size / (1024 * 1024)).toFixed(2) + ' MB'; // Convert bytes to MB
|
||||||
|
}
|
||||||
|
return fileSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
_fileAdded(file) {
|
||||||
|
const fileSizeLimit = 5 * 1024 * 1024 // 5MB
|
||||||
|
|
||||||
|
if (file) {
|
||||||
|
if (file.size > fileSizeLimit) {
|
||||||
|
this.previewTarget.classList.add("text-red-500")
|
||||||
|
this.previewTarget.textContent = "File size exceeds the limit of 5MB"
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.submitTarget.classList.remove([
|
||||||
|
"bg-alpha-black-25",
|
||||||
|
"text-gray",
|
||||||
|
"cursor-not-allowed",
|
||||||
|
]);
|
||||||
|
this.submitTarget.classList.add(
|
||||||
|
"bg-gray-900",
|
||||||
|
"text-white",
|
||||||
|
"cursor-pointer",
|
||||||
|
);
|
||||||
|
this.submitTarget.disabled = false;
|
||||||
|
this.previewTarget.innerHTML = document.querySelector("#template-preview").innerHTML;
|
||||||
|
this.previewTarget.classList.remove("text-red-500")
|
||||||
|
this.previewTarget.classList.add("text-gray-900")
|
||||||
|
this.filenameTarget.textContent = file.name;
|
||||||
|
this.filesizeTarget.textContent = this._fetchFileSize(file.size);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_isCSVFile(file) {
|
||||||
|
const acceptedTypes = ["text/csv", "application/csv", ".csv"]
|
||||||
|
const extension = file.name.split('.').pop().toLowerCase()
|
||||||
|
return acceptedTypes.includes(file.type) || extension === "csv"
|
||||||
|
}
|
||||||
|
|
||||||
|
_setFileInput(file) {
|
||||||
|
const dataTransfer = new DataTransfer();
|
||||||
|
dataTransfer.items.add(file);
|
||||||
|
this.inputTarget.files = dataTransfer.files;
|
||||||
|
}
|
||||||
|
}
|
27
app/views/imports/_csv_paste.html.erb
Normal file
27
app/views/imports/_csv_paste.html.erb
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
<%= form_with model: @import, url: load_import_path(@import) do |form| %>
|
||||||
|
<div>
|
||||||
|
<%= form.text_area :raw_csv_str,
|
||||||
|
rows: 10,
|
||||||
|
required: true,
|
||||||
|
placeholder: "Paste your CSV file contents here",
|
||||||
|
class: "rounded-md w-full border text-sm border-alpha-black-100 bg-white placeholder:text-gray-400 p-4" %>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<%= form.submit t(".next"), class: "px-4 py-2 mb-4 block w-full rounded-lg bg-gray-900 text-white text-sm font-medium", data: { turbo_confirm: (@import.raw_csv_str? ? { title: t(".confirm_title"), body: t(".confirm_body"), accept: t(".confirm_accept") } : nil) } %>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<div class="bg-alpha-black-25 rounded-xl p-1 mt-5">
|
||||||
|
<div class="text-gray-500 p-2 mb-2">
|
||||||
|
<div class="flex gap-2 mb-2">
|
||||||
|
<%= lucide_icon("info", class: "w-5 h-5 shrink-0") %>
|
||||||
|
<p class="text-sm"><%= t(".instructions") %></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ul class="list-disc text-sm pl-10">
|
||||||
|
<li><%= t(".requirement1") %></li>
|
||||||
|
<li><%= t(".requirement2") %></li>
|
||||||
|
<li><%= t(".requirement3") %></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<%= render partial: "imports/sample_table" %>
|
||||||
|
</div>
|
39
app/views/imports/_csv_upload.html.erb
Normal file
39
app/views/imports/_csv_upload.html.erb
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
<%= form_with model: @import, url: upload_import_path(@import), class: "dropzone", data: { controller: "csv-upload" }, method: :patch, multipart: true do |form| %>
|
||||||
|
<div class="flex items-center justify-center w-full">
|
||||||
|
<label for="import_raw_csv_str" class="csv-drop-box flex flex-col items-center justify-center w-full h-64 border-2 border-gray-300 border-dashed rounded-lg cursor-pointer bg-gray-50" data-action="dragover->csv-upload#dragover dragleave->csv-upload#dragleave drop->csv-upload#drop">
|
||||||
|
<div class="flex flex-col items-center justify-center pt-5 pb-6">
|
||||||
|
<%= lucide_icon "plus", class: "w-5 h-5 text-gray-500" %>
|
||||||
|
<%= form.file_field :raw_csv_str, class: "hidden", direct_upload: false, accept: "text/csv,.csv,application/csv", data: { csv_upload_target: "input", action: "change->csv-upload#addFile" } %>
|
||||||
|
<p class="mb-2 text-sm text-gray-500 mt-3">Drag and drop your csv file here or <span class="text-black">click to browse</span></p>
|
||||||
|
<p class="text-xs text-gray-500">CSV (Max. 5MB)</p>
|
||||||
|
<div class="csv-preview" data-csv-upload-target="preview"></div>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<%= form.submit t(".next"), class: "px-4 py-2 mb-4 block w-full rounded-lg bg-alpha-black-25 text-gray text-sm font-medium", data: { csv_upload_target: "submit", turbo_confirm: (@import.raw_csv_str? ? { title: t(".confirm_title"), body: t(".confirm_body"), accept: t(".confirm_accept") } : nil) } %>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<div id="template-preview" class="hidden">
|
||||||
|
<div class="flex flex-col items-center justify-center">
|
||||||
|
<%= lucide_icon "file-text", class: "w-10 h-10 pt-2 text-black" %>
|
||||||
|
<div class="flex flex-row items-center justify-center gap-0.5">
|
||||||
|
<div><span data-csv-upload-target="filename"></span></div>
|
||||||
|
<div><span data-csv-upload-target="filesize" class="font-semibold"></span></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-alpha-black-25 rounded-xl p-1 mt-5">
|
||||||
|
<div class="text-gray-500 p-2 mb-2">
|
||||||
|
<div class="flex gap-2 mb-2">
|
||||||
|
<%= lucide_icon("info", class: "w-5 h-5 shrink-0") %>
|
||||||
|
<p class="text-sm">
|
||||||
|
<%= t(".instructions") %>
|
||||||
|
<span class="text-black underline">
|
||||||
|
<%= link_to "download this template", "/transactions.csv", download: "" %>
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<%= render partial: "imports/sample_table" %>
|
||||||
|
</div>
|
|
@ -8,33 +8,18 @@
|
||||||
<p class="text-gray-500 text-sm"><%= t(".description") %></p>
|
<p class="text-gray-500 text-sm"><%= t(".description") %></p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<%= form_with model: @import, url: load_import_path(@import) do |form| %>
|
<div data-controller="tabs" data-tabs-active-class="bg-white" data-tabs-default-tab-value="csv-upload-tab">
|
||||||
<div>
|
<div class="flex justify-center mb-4">
|
||||||
<%= form.text_area :raw_csv_str,
|
<div class="bg-gray-50 rounded-lg inline-flex p-1 space-x-2 text-sm text-gray-900 font-medium">
|
||||||
rows: 10,
|
<button data-id="csv-upload-tab" class="p-2 rounded-lg" data-tabs-target="btn" data-action="click->tabs#select">Upload CSV</button>
|
||||||
required: true,
|
<button data-id="csv-paste-tab" class="p-2 rounded-lg" data-tabs-target="btn" data-action="click->tabs#select">Copy & Paste</button>
|
||||||
placeholder: "Paste your CSV file contents here",
|
|
||||||
class: "rounded-md w-full border text-sm border-alpha-black-100 bg-white placeholder:text-gray-400 p-4" %>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<%= form.submit t(".next"), class: "px-4 py-2 block w-full rounded-lg bg-gray-900 text-white text-sm font-medium cursor-pointer hover:bg-gray-700", data: { turbo_confirm: (@import.raw_csv_str? ? { title: t(".confirm_title"), body: t(".confirm_body"), accept: t(".confirm_accept") } : nil) } %>
|
|
||||||
<% end %>
|
|
||||||
|
|
||||||
<div class="bg-alpha-black-25 rounded-xl p-1">
|
|
||||||
<div class="text-gray-500 p-2 mb-2">
|
|
||||||
<div class="flex gap-2 mb-2">
|
|
||||||
<%= lucide_icon("info", class: "w-5 h-5 shrink-0") %>
|
|
||||||
<p class="text-sm"><%= t(".instructions") %></p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ul class="list-disc text-sm pl-10">
|
|
||||||
<li><%= t(".requirement1") %></li>
|
|
||||||
<li><%= t(".requirement2") %></li>
|
|
||||||
<li><%= t(".requirement3") %></li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div data-tabs-target="tab" id="csv-upload-tab">
|
||||||
<%= render partial: "imports/sample_table" %>
|
<%= render partial: "imports/csv_upload", locals: { import: @import } %>
|
||||||
|
</div>
|
||||||
|
<div data-tabs-target="tab" id="csv-paste-tab" class="hidden">
|
||||||
|
<%= render partial: "imports/csv_paste", locals: { import: @import } %>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -27,6 +27,25 @@ en:
|
||||||
invalid_data: You have invalid data, please fix before continuing
|
invalid_data: You have invalid data, please fix before continuing
|
||||||
create:
|
create:
|
||||||
import_created: Import created
|
import_created: Import created
|
||||||
|
csv_paste:
|
||||||
|
confirm_accept: Yep, start over!
|
||||||
|
confirm_body: This will reset your import. Any changes you have made to the
|
||||||
|
CSV will be erased.
|
||||||
|
confirm_title: Are you sure?
|
||||||
|
instructions: Your CSV should have the following columns and formats for the
|
||||||
|
best import experience.
|
||||||
|
next: Next
|
||||||
|
requirement1: Dates must be in ISO 8601 format (YYYY-MM-DD)
|
||||||
|
requirement2: Negative transaction is an "outflow" (expense), positive is an
|
||||||
|
"inflow" (income)
|
||||||
|
requirement3: Can have 0 or more tags separated by |
|
||||||
|
csv_upload:
|
||||||
|
confirm_accept: Yep, start over!
|
||||||
|
confirm_body: This will reset your import. Any changes you have made to the
|
||||||
|
CSV will be erased.
|
||||||
|
confirm_title: Are you sure?
|
||||||
|
instructions: The csv file must be in the format below. You can also reuse and
|
||||||
|
next: Next
|
||||||
destroy:
|
destroy:
|
||||||
import_destroyed: Import destroyed
|
import_destroyed: Import destroyed
|
||||||
edit:
|
edit:
|
||||||
|
@ -56,20 +75,9 @@ en:
|
||||||
new: New import
|
new: New import
|
||||||
title: Imports
|
title: Imports
|
||||||
load:
|
load:
|
||||||
confirm_accept: Yep, start over!
|
|
||||||
confirm_body: This will reset your import. Any changes you have made to the
|
|
||||||
CSV will be erased.
|
|
||||||
confirm_title: Are you sure?
|
|
||||||
description: Create a spreadsheet or upload an exported CSV from your financial
|
description: Create a spreadsheet or upload an exported CSV from your financial
|
||||||
institution.
|
institution.
|
||||||
instructions: Your CSV should have the following columns and formats for the
|
|
||||||
best import experience.
|
|
||||||
load_title: Load import
|
load_title: Load import
|
||||||
next: Next
|
|
||||||
requirement1: Dates must be in ISO 8601 format (YYYY-MM-DD)
|
|
||||||
requirement2: Negative transaction is an "outflow" (expense), positive is an
|
|
||||||
"inflow" (income)
|
|
||||||
requirement3: Can have 0 or more tags separated by |
|
|
||||||
subtitle: Import your transactions
|
subtitle: Import your transactions
|
||||||
load_csv:
|
load_csv:
|
||||||
import_loaded: Import CSV loaded
|
import_loaded: Import CSV loaded
|
||||||
|
@ -95,3 +103,5 @@ en:
|
||||||
import_updated: Import updated
|
import_updated: Import updated
|
||||||
update_mappings:
|
update_mappings:
|
||||||
column_mappings_saved: Column mappings saved
|
column_mappings_saved: Column mappings saved
|
||||||
|
upload_csv:
|
||||||
|
import_loaded: CSV File loaded
|
||||||
|
|
|
@ -25,6 +25,7 @@ Rails.application.routes.draw do
|
||||||
member do
|
member do
|
||||||
get "load"
|
get "load"
|
||||||
patch "load" => "imports#load_csv"
|
patch "load" => "imports#load_csv"
|
||||||
|
patch "upload" => "imports#upload_csv"
|
||||||
|
|
||||||
get "configure"
|
get "configure"
|
||||||
patch "configure" => "imports#update_mappings"
|
patch "configure" => "imports#update_mappings"
|
||||||
|
|
4
public/transactions.csv
Normal file
4
public/transactions.csv
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
date,name,category,tags,amount
|
||||||
|
2024-01-01,Amazon,Shopping,Tag1|Tag2,-24.99
|
||||||
|
2024-03-01,Spotify,,,-16.32
|
||||||
|
2023-01-06,Acme,Income,Tag3,151.22
|
|
|
@ -65,6 +65,18 @@ class ImportsControllerTest < ActionDispatch::IntegrationTest
|
||||||
assert_equal "Import CSV loaded", flash[:notice]
|
assert_equal "Import CSV loaded", flash[:notice]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "should upload CSV file if valid" do
|
||||||
|
Tempfile.open([ "transactions.csv", ".csv" ]) do |temp|
|
||||||
|
CSV.open(temp, "wb", headers: true) do |csv|
|
||||||
|
valid_csv_str.split("\n").each { |row| csv << row.split(",") }
|
||||||
|
end
|
||||||
|
|
||||||
|
patch upload_import_url(@empty_import), params: { import: { raw_csv_str: Rack::Test::UploadedFile.new(temp, ".csv") } }
|
||||||
|
assert_redirected_to configure_import_path(@empty_import)
|
||||||
|
assert_equal "CSV File loaded", flash[:notice]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
test "should flash error message if invalid CSV input" do
|
test "should flash error message if invalid CSV input" do
|
||||||
patch load_import_url(@empty_import), params: { import: { raw_csv_str: malformed_csv_str } }
|
patch load_import_url(@empty_import), params: { import: { raw_csv_str: malformed_csv_str } }
|
||||||
|
|
||||||
|
@ -72,6 +84,23 @@ class ImportsControllerTest < ActionDispatch::IntegrationTest
|
||||||
assert_equal "Raw csv str is not a valid CSV format", flash[:error]
|
assert_equal "Raw csv str is not a valid CSV format", flash[:error]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "should flash error message if invalid CSV file upload" do
|
||||||
|
Tempfile.open([ "transactions.csv", ".csv" ]) do |temp|
|
||||||
|
temp.write(malformed_csv_str)
|
||||||
|
temp.rewind
|
||||||
|
|
||||||
|
patch upload_import_url(@empty_import), params: { import: { raw_csv_str: Rack::Test::UploadedFile.new(temp, ".csv") } }
|
||||||
|
assert_response :unprocessable_entity
|
||||||
|
assert_equal "Raw csv str is not a valid CSV format", flash[:error]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
test "should flash error message if no fileprovided for upload" do
|
||||||
|
patch upload_import_url(@empty_import), params: { import: { raw_csv_str: nil } }
|
||||||
|
assert_response :unprocessable_entity
|
||||||
|
assert_equal "Please select a file to upload", flash[:error]
|
||||||
|
end
|
||||||
|
|
||||||
test "should get configure" do
|
test "should get configure" do
|
||||||
get configure_import_url(@loaded_import)
|
get configure_import_url(@loaded_import)
|
||||||
assert_response :success
|
assert_response :success
|
||||||
|
|
3
test/fixtures/files/transactions.csv
vendored
Normal file
3
test/fixtures/files/transactions.csv
vendored
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
date,Custom Name Column,category,amount
|
||||||
|
invalid_date,Starbucks drink,Food,-20.50
|
||||||
|
2024-01-01,Amazon purchase,Shopping,-89.50
|
|
|
@ -1,6 +1,6 @@
|
||||||
module ImportTestHelper
|
module ImportTestHelper
|
||||||
def valid_csv_str
|
def valid_csv_str
|
||||||
<<-ROWS
|
<<~ROWS
|
||||||
date,name,category,tags,amount
|
date,name,category,tags,amount
|
||||||
2024-01-01,Starbucks drink,Food & Drink,Tag1|Tag2,-8.55
|
2024-01-01,Starbucks drink,Food & Drink,Tag1|Tag2,-8.55
|
||||||
2024-01-01,Etsy,Shopping,Tag1,-80.98
|
2024-01-01,Etsy,Shopping,Tag1,-80.98
|
||||||
|
@ -10,14 +10,14 @@ module ImportTestHelper
|
||||||
end
|
end
|
||||||
|
|
||||||
def valid_csv_with_invalid_values
|
def valid_csv_with_invalid_values
|
||||||
<<-ROWS
|
<<~ROWS
|
||||||
date,name,category,tags,amount
|
date,name,category,tags,amount
|
||||||
invalid_date,Starbucks drink,Food,,invalid_amount
|
invalid_date,Starbucks drink,Food,,invalid_amount
|
||||||
ROWS
|
ROWS
|
||||||
end
|
end
|
||||||
|
|
||||||
def valid_csv_with_missing_data
|
def valid_csv_with_missing_data
|
||||||
<<-ROWS
|
<<~ROWS
|
||||||
date,name,category,"optional id",amount
|
date,name,category,"optional id",amount
|
||||||
2024-01-01,Drink,Food,1234,-200
|
2024-01-01,Drink,Food,1234,-200
|
||||||
2024-01-02,,,,-100
|
2024-01-02,,,,-100
|
||||||
|
@ -25,7 +25,7 @@ module ImportTestHelper
|
||||||
end
|
end
|
||||||
|
|
||||||
def malformed_csv_str
|
def malformed_csv_str
|
||||||
<<-ROWS
|
<<~ROWS
|
||||||
name,age
|
name,age
|
||||||
"John Doe,23
|
"John Doe,23
|
||||||
"Jane Doe",25
|
"Jane Doe",25
|
||||||
|
|
|
@ -51,6 +51,8 @@ class ImportsTest < ApplicationSystemTestCase
|
||||||
|
|
||||||
click_button "Next"
|
click_button "Next"
|
||||||
|
|
||||||
|
click_button "Copy & Paste"
|
||||||
|
|
||||||
# 2) Load Step
|
# 2) Load Step
|
||||||
assert_selector "h1", text: "Load import"
|
assert_selector "h1", text: "Load import"
|
||||||
|
|
||||||
|
@ -94,6 +96,60 @@ class ImportsTest < ApplicationSystemTestCase
|
||||||
assert_selector "h1", text: "Imports"
|
assert_selector "h1", text: "Imports"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "can perform import by CSV upload" do
|
||||||
|
trigger_import_from_settings
|
||||||
|
verify_import_modal
|
||||||
|
|
||||||
|
within "#modal" do
|
||||||
|
click_link "New import from CSV"
|
||||||
|
end
|
||||||
|
|
||||||
|
# 1) Create import step
|
||||||
|
assert_selector "h1", text: "New import"
|
||||||
|
|
||||||
|
within "form" do
|
||||||
|
select "Checking Account", from: "import_account_id"
|
||||||
|
end
|
||||||
|
|
||||||
|
click_button "Next"
|
||||||
|
|
||||||
|
click_button "Upload CSV"
|
||||||
|
|
||||||
|
find(".csv-drop-box").drop File.join(file_fixture_path, "transactions.csv")
|
||||||
|
assert_selector "div.csv-preview", text: "transactions.csv"
|
||||||
|
|
||||||
|
click_button "Next"
|
||||||
|
|
||||||
|
# 3) Configure step
|
||||||
|
assert_selector "h1", text: "Configure import"
|
||||||
|
|
||||||
|
within "form" do
|
||||||
|
select "Custom Name Column", from: "import_column_mappings_name"
|
||||||
|
end
|
||||||
|
|
||||||
|
click_button "Next"
|
||||||
|
|
||||||
|
# 4) Clean step
|
||||||
|
assert_selector "h1", text: "Clean import"
|
||||||
|
|
||||||
|
# We have an invalid value, so user cannot click next yet
|
||||||
|
assert_no_text "Next"
|
||||||
|
|
||||||
|
# Replace invalid date with valid date
|
||||||
|
fill_in "cell-0-0", with: "2024-01-02"
|
||||||
|
|
||||||
|
# Trigger blur event so value saves
|
||||||
|
find("body").click
|
||||||
|
|
||||||
|
click_link "Next"
|
||||||
|
|
||||||
|
# 5) Confirm step
|
||||||
|
assert_selector "h1", text: "Confirm import"
|
||||||
|
assert_selector "#new_account_entry", count: 2
|
||||||
|
click_button "Import 2 transactions"
|
||||||
|
assert_selector "h1", text: "Imports"
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def trigger_import_from_settings
|
def trigger_import_from_settings
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue