From 8ef999f3b8f75420c8158cbb54f42ab8859ec2f1 Mon Sep 17 00:00:00 2001
From: Robert Goldmann <deadlocker@gmx.de>
Date: Sat, 1 Jul 2023 12:05:11 +0200
Subject: [PATCH] #751 - transactions are no longer saved multiple times during
 csv import:

- remove listeners before registering new ones
- form can now be submitted even if already sent (move form tag inside <td>)
---
 .../resources/static/js/transactionImport.js  | 84 +++++++++++--------
 .../templates/helpers/customSelectMacros.ftl  |  8 +-
 .../resources/templates/helpers/header.ftl    |  4 +-
 .../transactions/transactionImportMacros.ftl  | 16 ++--
 .../integration/selenium/CsvImportTest.java   | 12 +--
 5 files changed, 71 insertions(+), 53 deletions(-)

diff --git a/BudgetMasterServer/src/main/resources/static/js/transactionImport.js b/BudgetMasterServer/src/main/resources/static/js/transactionImport.js
index cc29ec50a..80fcbb31a 100644
--- a/BudgetMasterServer/src/main/resources/static/js/transactionImport.js
+++ b/BudgetMasterServer/src/main/resources/static/js/transactionImport.js
@@ -9,12 +9,12 @@ $(document).ready(function()
 
     $('#table-transaction-rows').DataTable({
         paging: false,
-        order: [[1, 'desc']],
+        order: [[2, 'desc']],
         info: false,
         scrollX: true,
         scrollY: false,
         columnDefs: [
-            { orderable: false, targets:  5}
+            { orderable: false, targets:  6}
         ],
         language: { search: '' , searchPlaceholder: localizedSearch},
     });
@@ -56,34 +56,42 @@ function initCsvTransactionForms()
     for(let i = 0; i < forms.length; i++)
     {
         let form = forms[i];
-        $(form).submit(function(event)
+        console.log(i)
+        console.log(form)
+        // form.removeEventListener('submit', submitTransactionInPlaceForm);
+        form.addEventListener('submit', submitTransactionInPlaceForm);
+    }
+}
+
+function submitTransactionInPlaceForm(event)
+{
+    const form = event.target;
+    console.log('form ' + form)
+    const csvTransactionId = form.dataset.index;
+            console.log('go')
+
+    $.ajax({
+        type: 'POST',
+        url: $(this).attr('action'),
+        data: new FormData(form),
+        processData: false,
+        contentType: false,
+        success: function(response)
+        {
+            $('#transaction-import-row-' + csvTransactionId).replaceWith(response);
+            initCsvTransactions();
+        },
+        error: function(response)
         {
-            const csvTransactionId = form.dataset.index;
-
-            $.ajax({
-                type: 'POST',
-                url: $(this).attr('action'),
-                data: new FormData(form),
-                processData: false,
-                contentType: false,
-                success: function(response)
-                {
-                    $('#transaction-import-row-' + csvTransactionId).replaceWith(response);
-                    initCsvTransactions();
-                },
-                error: function(response)
-                {
-                    M.toast({
-                        html: "Error saving transaction",
-                        classes: 'red'
-                    });
-                    console.error(response);
-                }
+            M.toast({
+                html: "Error saving transaction",
+                classes: 'red'
             });
+            console.error(response);
+        }
+    });
 
-            event.preventDefault();
-        });
-    }
+    event.preventDefault();
 }
 
 function initCsvTransactionButtons()
@@ -92,23 +100,29 @@ function initCsvTransactionButtons()
     for(let i = 0; i < buttonsSkip.length; i++)
     {
         const button = buttonsSkip[i];
-        button.addEventListener('click', function()
-        {
-            performCsvTransactionGetRequestWithoutReload(button, 'Error skipping transaction');
-        });
+        button.removeEventListener('click', skipRow);
+        button.addEventListener('click', skipRow);
     }
 
     const buttonsUndoSkip = document.getElementsByClassName('button-request-transaction-import-undo-skip');
     for(let i = 0; i < buttonsUndoSkip.length; i++)
     {
         const button = buttonsUndoSkip[i];
-        button.addEventListener('click', function()
-        {
-            performCsvTransactionGetRequestWithoutReload(button, 'Error undo skip transaction');
-        });
+        button.removeEventListener('click', undoSkipRow);
+        button.addEventListener('click', undoSkipRow);
     }
 }
 
+function skipRow(event)
+{
+    performCsvTransactionGetRequestWithoutReload(event.currentTarget, 'Error skipping transaction');
+}
+
+function undoSkipRow(event)
+{
+    performCsvTransactionGetRequestWithoutReload(event.currentTarget, 'Error undo skip transaction');
+}
+
 function performCsvTransactionGetRequestWithoutReload(button, errorMessage)
 {
     const url = button.dataset.url;
diff --git a/BudgetMasterServer/src/main/resources/templates/helpers/customSelectMacros.ftl b/BudgetMasterServer/src/main/resources/templates/helpers/customSelectMacros.ftl
index 78cce0aed..ad57d9ac3 100644
--- a/BudgetMasterServer/src/main/resources/templates/helpers/customSelectMacros.ftl
+++ b/BudgetMasterServer/src/main/resources/templates/helpers/customSelectMacros.ftl
@@ -13,14 +13,14 @@
                 </div>
 </#macro>
 
-<#macro customSelectEnd inputName selectedItem>
-                <input type="hidden" name="${inputName}" class="hidden-input-custom-select" <#if selectedItem??>value="${selectedItem.getID()?c}"</#if>/>
+<#macro customSelectEnd inputName selectedItem form="">
+                <input type="hidden" name="${inputName}" class="hidden-input-custom-select" <#if selectedItem??>value="${selectedItem.getID()?c}"</#if> <#if form?has_content>form="${form}"</#if>/>
             </div>
         </div>
     </div>
 </#macro>
 
-<#macro customCategorySelect categories selectedCategory inputClasses labelText id showName=true rowClasses="" disabled=false>
+<#macro customCategorySelect categories selectedCategory inputClasses labelText id showName=true rowClasses="" disabled=false form="">
     <@customSelectStart "category-select-wrapper" categories inputClasses labelText id "label" disabled rowClasses>
         <div class="custom-select-trigger" tabindex="0">
             <div class="custom-select-selected-item">
@@ -52,7 +52,7 @@
             </#list>
         </div>
     </@customSelectStart>
-    <@customSelectEnd "category" selectedCategory/>
+    <@customSelectEnd "category" selectedCategory form/>
 </#macro>
 
 <#macro customAccountSelect selector inputName accounts selectedAccount inputClasses labelText id disabled=false>
diff --git a/BudgetMasterServer/src/main/resources/templates/helpers/header.ftl b/BudgetMasterServer/src/main/resources/templates/helpers/header.ftl
index 4b8ee3807..808412c4e 100644
--- a/BudgetMasterServer/src/main/resources/templates/helpers/header.ftl
+++ b/BudgetMasterServer/src/main/resources/templates/helpers/header.ftl
@@ -130,8 +130,8 @@
     </a>
 </#macro>
 
-<#macro buttonSubmit name icon localizationKey id="" color="background-blue" classes="" disabled=false formaction="" value="" onclick="">
-    <button id="${id}" class="btn waves-effect waves-light ${color} ${classes}" type="submit" name="${name}" <#if disabled>disabled</#if> <#if formaction?has_content>formaction="<@s.url formaction/>"</#if> <#if value?has_content>value="${value}"</#if> <#if onclick?has_content>onclick="${onclick}"</#if>>
+<#macro buttonSubmit name icon localizationKey id="" color="background-blue" classes="" disabled=false formaction="" value="" onclick="" form="">
+    <button <#if id?has_content>id="${id}"</#if> class="btn waves-effect waves-light ${color} ${classes}" type="submit" name="${name}" <#if disabled>disabled</#if> <#if formaction?has_content>formaction="<@s.url formaction/>"</#if> <#if value?has_content>value="${value}"</#if> <#if onclick?has_content>onclick="${onclick}" </#if> <#if form?has_content>form="${form}" </#if>>
         <i class="material-icons left <#if !localizationKey?has_content>no-margin</#if>">${icon}</i><#if localizationKey?has_content>${locale.getString(localizationKey)}</#if>
     </button>
 </#macro>
diff --git a/BudgetMasterServer/src/main/resources/templates/transactions/transactionImportMacros.ftl b/BudgetMasterServer/src/main/resources/templates/transactions/transactionImportMacros.ftl
index cf03a21ed..86fdd0c0d 100644
--- a/BudgetMasterServer/src/main/resources/templates/transactions/transactionImportMacros.ftl
+++ b/BudgetMasterServer/src/main/resources/templates/transactions/transactionImportMacros.ftl
@@ -168,6 +168,7 @@
         <table class="bordered centered" id="table-transaction-rows" style="width:100%">
             <thead>
                 <tr>
+                    <td class="hidden"></td>
                     <td class="bold">${locale.getString("transactions.import.status")}</td>
                     <td class="bold">${locale.getString("transaction.new.label.date")}</td>
                     <td class="bold">${locale.getString("transaction.new.label.category")}</td>
@@ -189,21 +190,24 @@
 
 <#macro renderCsvTransaction csvTransaction index>
     <tr class="transaction-import-row <#if csvTransaction.getStatus().name() == 'SKIPPED'>transaction-import-row-skipped</#if>" id="transaction-import-row-${index}">
-        <form name="NewTransactionInPlace" method="POST" action="<@s.url '/transactionImport/' + index + '/newTransactionInPlace'/>" data-index="${index}">
-            <input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/>
+            <td class="hidden">
+                <form name="NewTransactionInPlace" id="newTransactionInPlace_${index}" method="POST" action="<@s.url '/transactionImport/' + index + '/newTransactionInPlace'/>" data-index="${index}">
+                    <input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/>
+                </form>
+            </td>
             <td data-order="${locale.getString(csvTransaction.getStatus().getLocalizationKey())}" data-search="${locale.getString(csvTransaction.getStatus().getLocalizationKey())}"><@statusBanner csvTransaction.getStatus()/></td>
             <td data-order="${csvTransaction.getDate()}" data-search="${csvTransaction.getDate()}">${csvTransaction.getDate()}</td>
             <td data-order="${csvTransaction.getCategory().getName()}" data-search="${csvTransaction.getCategory().getName()}">
-                <@customSelectMacros.customCategorySelect categories csvTransaction.getCategory() "left-align no-margin-top no-margin-bottom" "" "csvTransaction-category-${index}" false "no-margin-bottom" csvTransaction.getStatus().name() == 'SKIPPED'/>
+                <@customSelectMacros.customCategorySelect categories csvTransaction.getCategory() "left-align no-margin-top no-margin-bottom" "" "csvTransaction-category-${index}" false "no-margin-bottom" csvTransaction.getStatus().name() == 'SKIPPED' 'newTransactionInPlace_${index}'/>
             </td>
             <td data-order="${csvTransaction.getName()}" data-search="${csvTransaction.getName()}">
                 <div class="input-field no-margin-top no-margin-bottom">
-                    <input class="no-margin-bottom autocomplete" type="text" name="name" autocomplete="off" required value="${csvTransaction.getName()}" <#if csvTransaction.getStatus().name() == 'SKIPPED'>disabled</#if>>
+                    <input form="newTransactionInPlace_${index}" class="no-margin-bottom autocomplete" type="text" name="name" autocomplete="off" required value="${csvTransaction.getName()}" <#if csvTransaction.getStatus().name() == 'SKIPPED'>disabled</#if>>
                 </div>
             </td>
             <td data-order="${csvTransaction.getDescription()}" data-search="${csvTransaction.getDescription()}">
                 <div class="input-field no-margin-top no-margin-bottom">
-                    <input class="no-margin-bottom" type="text" name="description" value="${csvTransaction.getDescription()}" <#if csvTransaction.getStatus().name() == 'SKIPPED'>disabled</#if>>
+                    <input form="newTransactionInPlace_${index}" class="no-margin-bottom" type="text" name="description" value="${csvTransaction.getDescription()}" <#if csvTransaction.getStatus().name() == 'SKIPPED'>disabled</#if>>
                 </div>
             </td>
             <td data-order="${currencyService.getCurrencyString(csvTransaction.getAmount())}" data-search="${currencyService.getCurrencyString(csvTransaction.getAmount())}">${currencyService.getCurrencyString(csvTransaction.getAmount())}</td>
@@ -211,7 +215,7 @@
                 <#if csvTransaction.getStatus().name() == 'SKIPPED'>
                     <@header.buttonFlat url='/transactionImport/' + index + '/undoSkip' isDataUrl=true icon='do_disturb_off' localizationKey='' classes="no-padding text-default button-request-transaction-import-undo-skip" datasetIndex=index/>
                 <#else>
-                    <@header.buttonSubmit name='action' icon='save' localizationKey='' classes='text-white'/>&nbsp;
+                    <@header.buttonSubmit name='action' icon='save' localizationKey='' classes='text-white' form='newTransactionInPlace_${index}'/>&nbsp;
                     <div class="fixed-action-btn edit-transaction-button">
                         <a class="btn-floating btn-flat waves-effect waves-light no-padding text-default edit-transaction-button-link">
                             <i class="material-icons text-default">edit</i>
diff --git a/BudgetMasterServer/src/test/java/de/deadlocker8/budgetmaster/integration/selenium/CsvImportTest.java b/BudgetMasterServer/src/test/java/de/deadlocker8/budgetmaster/integration/selenium/CsvImportTest.java
index 70ea4e380..28fec6532 100644
--- a/BudgetMasterServer/src/test/java/de/deadlocker8/budgetmaster/integration/selenium/CsvImportTest.java
+++ b/BudgetMasterServer/src/test/java/de/deadlocker8/budgetmaster/integration/selenium/CsvImportTest.java
@@ -642,23 +642,23 @@ class CsvImportTest extends SeleniumTestBase
 	{
 		final List<WebElement> columns = row.findElements(By.tagName("td"));
 
-		assertThat(columns.get(0).findElements(By.cssSelector(".banner.background-" + statusColor)))
+		assertThat(columns.get(1).findElements(By.cssSelector(".banner.background-" + statusColor)))
 				.hasSize(1);
 
-		assertThat(columns.get(1).getText())
+		assertThat(columns.get(2).getText())
 				.isEqualTo(date);
 
-		final WebElement categoryCircle = columns.get(2).findElement(By.className("category-circle"));
+		final WebElement categoryCircle = columns.get(3).findElement(By.className("category-circle"));
 		categoryName = categoryName.substring(0, 1).toUpperCase();
 		assertThat(categoryCircle.findElement(By.tagName("span"))).hasFieldOrPropertyWithValue("text", categoryName);
 
-		assertThat(columns.get(3).findElement(By.name("name")).getAttribute("value"))
+		assertThat(columns.get(4).findElement(By.name("name")).getAttribute("value"))
 				.isEqualTo(name);
 
-		assertThat(columns.get(4).findElement(By.name("description")).getAttribute("value"))
+		assertThat(columns.get(5).findElement(By.name("description")).getAttribute("value"))
 				.isEqualTo(description);
 
-		assertThat(columns.get(5).getText())
+		assertThat(columns.get(6).getText())
 				.isEqualTo(amount);
 	}
 }
\ No newline at end of file
-- 
GitLab