From 123c3c979a391e445fae2a384a61196072e6a5bc Mon Sep 17 00:00:00 2001
From: Robert Goldmann <deadlocker@gmx.de>
Date: Sun, 12 Jun 2022 12:23:17 +0200
Subject: [PATCH] #696 - save personalization settings

---
 .../settings/SettingsController.java          | 55 ++++++++++++----
 .../PersonalizationSettingsContainer.java     | 65 +++++++++++++++++++
 .../containers/SecuritySettingsContainer.java | 21 +-----
 .../resources/languages/base_de.properties    |  2 +
 .../resources/languages/base_en.properties    |  2 +
 .../resources/static/js/settingsContainers.js |  3 +
 .../containers/settingsPersonalization.ftl    | 65 +++++++++++++++++++
 .../resources/templates/settings/settings.ftl | 56 ++--------------
 8 files changed, 184 insertions(+), 85 deletions(-)
 create mode 100644 BudgetMasterServer/src/main/java/de/deadlocker8/budgetmaster/settings/containers/PersonalizationSettingsContainer.java
 create mode 100644 BudgetMasterServer/src/main/resources/templates/settings/containers/settingsPersonalization.ftl

diff --git a/BudgetMasterServer/src/main/java/de/deadlocker8/budgetmaster/settings/SettingsController.java b/BudgetMasterServer/src/main/java/de/deadlocker8/budgetmaster/settings/SettingsController.java
index ffab9b429..2fa7b1975 100644
--- a/BudgetMasterServer/src/main/java/de/deadlocker8/budgetmaster/settings/SettingsController.java
+++ b/BudgetMasterServer/src/main/java/de/deadlocker8/budgetmaster/settings/SettingsController.java
@@ -2,7 +2,6 @@ package de.deadlocker8.budgetmaster.settings;
 
 import com.google.gson.JsonObject;
 import de.deadlocker8.budgetmaster.Build;
-import de.deadlocker8.budgetmaster.accounts.AccountService;
 import de.deadlocker8.budgetmaster.backup.*;
 import de.deadlocker8.budgetmaster.categories.CategoryService;
 import de.deadlocker8.budgetmaster.controller.BaseController;
@@ -12,9 +11,9 @@ import de.deadlocker8.budgetmaster.database.InternalDatabase;
 import de.deadlocker8.budgetmaster.database.model.BackupDatabase;
 import de.deadlocker8.budgetmaster.services.ImportResultItem;
 import de.deadlocker8.budgetmaster.services.ImportService;
+import de.deadlocker8.budgetmaster.settings.containers.PersonalizationSettingsContainer;
 import de.deadlocker8.budgetmaster.settings.containers.SecuritySettingsContainer;
 import de.deadlocker8.budgetmaster.update.BudgetMasterUpdateService;
-import de.deadlocker8.budgetmaster.utils.LanguageType;
 import de.deadlocker8.budgetmaster.utils.Mappings;
 import de.deadlocker8.budgetmaster.utils.WebRequestUtils;
 import de.deadlocker8.budgetmaster.utils.notification.Notification;
@@ -79,6 +78,7 @@ public class SettingsController extends BaseController
 		public static final String IMPORT_DATABASE_STEP_1 = "settings/importStepOne";
 		public static final String IMPORT_DATABASE_RESULT = "settings/importResult";
 		public static final String CONTAINER_SECURITY = "settings/containers/settingsSecurity";
+		public static final String CONTAINER_PERSONALIZATION = "settings/containers/settingsPersonalization";
 	}
 
 	private static class RequestAttributeNames
@@ -139,7 +139,7 @@ public class SettingsController extends BaseController
 			return ReturnValues.CONTAINER_SECURITY;
 		}
 
-		final String password = securitySettingsContainer.getPassword();
+		final String password = securitySettingsContainer.password();
 		if(password.equals(PASSWORD_PLACEHOLDER))
 		{
 			final JsonObject toastContent = getToastContent("notification.settings.security.warning", NotificationType.WARNING);
@@ -154,6 +154,45 @@ public class SettingsController extends BaseController
 		return ReturnValues.CONTAINER_SECURITY;
 	}
 
+	@PostMapping(value = "/save/personalization")
+	public String saveContainerPersonalization(Model model,
+										@ModelAttribute("PersonalizationSettingsContainer") PersonalizationSettingsContainer personalizationSettingsContainer,
+										BindingResult bindingResult)
+	{
+		personalizationSettingsContainer.fixBooleans();
+
+		final Settings settings = settingsService.getSettings();
+
+		if(bindingResult.hasErrors())
+		{
+			model.addAttribute(ModelAttributes.ERROR, bindingResult);
+
+			final JsonObject toastContent = getToastContent("notification.settings.personalization.error", NotificationType.ERROR);
+			model.addAttribute(ModelAttributes.TOAST_CONTENT, toastContent);
+			model.addAttribute(ModelAttributes.SETTINGS, settings);
+			model.addAttribute(ModelAttributes.SEARCH_RESULTS_PER_PAGE, SEARCH_RESULTS_PER_PAGE_OPTIONS);
+			return ReturnValues.CONTAINER_PERSONALIZATION;
+		}
+
+		// update settings
+		settings.setLanguage(personalizationSettingsContainer.getLanguageType());
+		settings.setCurrency(personalizationSettingsContainer.currency());
+		settings.setUseDarkTheme(personalizationSettingsContainer.useDarkTheme());
+		settings.setShowCategoriesAsCircles(personalizationSettingsContainer.showCategoriesAsCircles());
+		settings.setSearchItemsPerPage(personalizationSettingsContainer.searchItemsPerPage());
+		settingsService.updateSettings(settings);
+
+		// reload localization
+		Localization.load();
+		categoryService.localizeDefaultCategories();
+
+		final JsonObject toastContent = getToastContent("notification.settings.personalization.saved", NotificationType.SUCCESS);
+		model.addAttribute(ModelAttributes.TOAST_CONTENT, toastContent);
+		model.addAttribute(ModelAttributes.SETTINGS, settings);
+		model.addAttribute(ModelAttributes.SEARCH_RESULTS_PER_PAGE, SEARCH_RESULTS_PER_PAGE_OPTIONS);
+		return ReturnValues.CONTAINER_PERSONALIZATION;
+	}
+
 	private JsonObject getToastContent(String localizationKey, NotificationType notificationType)
 	{
 		final JsonObject toastContent = new JsonObject();
@@ -170,11 +209,9 @@ public class SettingsController extends BaseController
 	@PostMapping(value = "/save")
 	public String post(WebRequest request, Model model,
 					   @ModelAttribute("Settings") Settings settings, BindingResult bindingResult,
-					   @RequestParam(value = "languageType") String languageType,
 					   @RequestParam(value = "autoBackupStrategyType", required = false) String autoBackupStrategyType,
 					   @RequestParam(value = "runBackup", required = false) Boolean runBackup)
 	{
-		settings.setLanguage(LanguageType.fromName(languageType));
 		if(autoBackupStrategyType == null)
 		{
 			settings.setAutoBackupStrategy(AutoBackupStrategy.NONE);
@@ -253,11 +290,6 @@ public class SettingsController extends BaseController
 			settings.setAutoBackupGitUserName(defaultSettings.getAutoBackupGitUserName());
 			settings.setAutoBackupGitToken(defaultSettings.getAutoBackupGitToken());
 		}
-
-		if(settings.getShowCategoriesAsCircles() == null)
-		{
-			settings.setShowCategoriesAsCircles(false);
-		}
 	}
 
 	public void updateSettings(Settings settings)
@@ -275,9 +307,6 @@ public class SettingsController extends BaseController
 			final String cron = backupService.computeCron(settings.getAutoBackupTime(), settings.getAutoBackupDays());
 			backupTaskOptional.ifPresent(runnable -> backupService.startBackupCron(cron, runnable));
 		}
-
-		Localization.load();
-		categoryService.localizeDefaultCategories();
 	}
 
 
diff --git a/BudgetMasterServer/src/main/java/de/deadlocker8/budgetmaster/settings/containers/PersonalizationSettingsContainer.java b/BudgetMasterServer/src/main/java/de/deadlocker8/budgetmaster/settings/containers/PersonalizationSettingsContainer.java
new file mode 100644
index 000000000..722be8225
--- /dev/null
+++ b/BudgetMasterServer/src/main/java/de/deadlocker8/budgetmaster/settings/containers/PersonalizationSettingsContainer.java
@@ -0,0 +1,65 @@
+package de.deadlocker8.budgetmaster.settings.containers;
+
+import de.deadlocker8.budgetmaster.utils.LanguageType;
+
+public final class PersonalizationSettingsContainer
+{
+	private final String language;
+	private final String currency;
+	private Boolean useDarkTheme;
+	private Boolean showCategoriesAsCircles;
+	private final Integer searchItemsPerPage;
+
+	public PersonalizationSettingsContainer(String language, String currency, Boolean useDarkTheme,
+											Boolean showCategoriesAsCircles, Integer searchItemsPerPage)
+	{
+		this.language = language;
+		this.currency = currency;
+		this.useDarkTheme = useDarkTheme;
+		this.showCategoriesAsCircles = showCategoriesAsCircles;
+		this.searchItemsPerPage = searchItemsPerPage;
+	}
+
+	public LanguageType getLanguageType()
+	{
+		return LanguageType.fromName(language);
+	}
+
+	public String language()
+	{
+		return language;
+	}
+
+	public String currency()
+	{
+		return currency;
+	}
+
+	public Boolean useDarkTheme()
+	{
+		return useDarkTheme;
+	}
+
+	public Boolean showCategoriesAsCircles()
+	{
+		return showCategoriesAsCircles;
+	}
+
+	public Integer searchItemsPerPage()
+	{
+		return searchItemsPerPage;
+	}
+
+	public void fixBooleans()
+	{
+		if(useDarkTheme == null)
+		{
+			useDarkTheme = false;
+		}
+
+		if(showCategoriesAsCircles == null)
+		{
+			showCategoriesAsCircles = false;
+		}
+	}
+}
diff --git a/BudgetMasterServer/src/main/java/de/deadlocker8/budgetmaster/settings/containers/SecuritySettingsContainer.java b/BudgetMasterServer/src/main/java/de/deadlocker8/budgetmaster/settings/containers/SecuritySettingsContainer.java
index 308fdbf81..85f14e0a8 100644
--- a/BudgetMasterServer/src/main/java/de/deadlocker8/budgetmaster/settings/containers/SecuritySettingsContainer.java
+++ b/BudgetMasterServer/src/main/java/de/deadlocker8/budgetmaster/settings/containers/SecuritySettingsContainer.java
@@ -5,27 +5,8 @@ import org.springframework.validation.FieldError;
 
 import java.util.Optional;
 
-public class SecuritySettingsContainer
+public record SecuritySettingsContainer(String password, String passwordConfirmation)
 {
-	private final String password;
-	private final String passwordConfirmation;
-
-	public SecuritySettingsContainer(String password, String passwordConfirmation)
-	{
-		this.password = password;
-		this.passwordConfirmation = passwordConfirmation;
-	}
-
-	public String getPassword()
-	{
-		return password;
-	}
-
-	public String getPasswordConfirmation()
-	{
-		return passwordConfirmation;
-	}
-
 	public Optional<FieldError> validate()
 	{
 		if(password == null || password.equals(""))
diff --git a/BudgetMasterServer/src/main/resources/languages/base_de.properties b/BudgetMasterServer/src/main/resources/languages/base_de.properties
index 974fd17c8..fda2f2b71 100644
--- a/BudgetMasterServer/src/main/resources/languages/base_de.properties
+++ b/BudgetMasterServer/src/main/resources/languages/base_de.properties
@@ -171,6 +171,8 @@ notification.settings.saved=Einstellungen gespeichert
 notification.settings.security.saved=Passwort gespeichert
 notification.settings.security.warning=Passwort entspricht bereits gespeicherten Passwort
 notification.settings.security.error=Fehler beim Speichern des Passworts
+notification.settings.personalization.saved=Personalisierung gespeichert
+notification.settings.personalization.error=Fehler beim Speichern der Personalisierung
 notification.settings.update.available=BudgetMaster Update "{0}" verfügbar
 notification.settings.database.delete.success=Datenbank erfolgreich gelöscht
 notification.settings.backup.run.success=Backup erfolgreich
diff --git a/BudgetMasterServer/src/main/resources/languages/base_en.properties b/BudgetMasterServer/src/main/resources/languages/base_en.properties
index aba7f6bf9..ddb208bb4 100644
--- a/BudgetMasterServer/src/main/resources/languages/base_en.properties
+++ b/BudgetMasterServer/src/main/resources/languages/base_en.properties
@@ -172,6 +172,8 @@ notification.settings.saved=Settings saved
 notification.settings.security.saved=Password saved
 notification.settings.security.warning=Password equals already saved password
 notification.settings.security.error=Error saving password
+notification.settings.personalization.saved=Personalization settings saved
+notification.settings.personalization.error=Error saving personalization settings
 notification.settings.update.available=BudgetMaster update "{0}" available
 notification.settings.database.delete.success=Successfully deleted database
 notification.settings.backup.run.success=Backup successful
diff --git a/BudgetMasterServer/src/main/resources/static/js/settingsContainers.js b/BudgetMasterServer/src/main/resources/static/js/settingsContainers.js
index 164c146d7..1dd8cbf45 100644
--- a/BudgetMasterServer/src/main/resources/static/js/settingsContainers.js
+++ b/BudgetMasterServer/src/main/resources/static/js/settingsContainers.js
@@ -13,7 +13,10 @@ function initSettingsContainer(formName, containerId)
             success: function(response)
             {
                 $('#' + containerId).html(response);
+
+                // re-init materialize components
                 $('.tooltipped').tooltip();
+                $('select').formSelect();
 
                 let toastContent = document.querySelector('#' + containerId + ' .securityContainerToastContent').innerHTML.trim();
                 if(toastContent)
diff --git a/BudgetMasterServer/src/main/resources/templates/settings/containers/settingsPersonalization.ftl b/BudgetMasterServer/src/main/resources/templates/settings/containers/settingsPersonalization.ftl
new file mode 100644
index 000000000..5d930720a
--- /dev/null
+++ b/BudgetMasterServer/src/main/resources/templates/settings/containers/settingsPersonalization.ftl
@@ -0,0 +1,65 @@
+<#import "/spring.ftl" as s>
+<#import "../../helpers/validation.ftl" as validation>
+<#import "../../helpers/header.ftl" as header>
+<@header.globals/>
+
+<#import "settingsContainer.ftl" as settingsContainerMacros>
+<#import "../settingsMacros.ftl" as settingsMacros>
+
+<#macro personalizationSettingsContainer settings>
+    <@settingsContainerMacros.settingsContainer 'PersonalizationSettingsContainer' 'personalizationSettingsContainer'>
+        <#-- language -->
+        <div class="row">
+            <div class="input-field col s12 m12 l8 offset-l2">
+                <i class="material-icons prefix">translate</i>
+                <select id="settings-language" name="language" <@validation.validation "language"/>>
+                    <#list helpers.getAvailableLanguages() as language>
+                        <#if settings.getLanguage() == language>
+                            <option selected value="${language.getName()}">${language.getName()}</option>
+                        <#else>
+                            <option value="${language.getName()}">${language.getName()}</option>
+                        </#if>
+                    </#list>
+                </select>
+                <label for="settings-language">${locale.getString("settings.language")}</label>
+            </div>
+        </div>
+
+        <#-- currency -->
+        <div class="row">
+            <div class="input-field col s12 m12 l8 offset-l2">
+                <i class="material-icons prefix">euro</i>
+                <input id="settings-currency" type="text" name="currency" <@validation.validation "currency"/> value="<#if settings.getCurrency()??>${settings.getCurrency()}</#if>">
+                <label for="settings-currency">${locale.getString("settings.currency")}</label>
+            </div>
+        </div>
+
+        <#-- dark theme and category style -->
+        <@settingsMacros.switches settings/>
+
+        <#-- search items per page -->
+        <div class="row">
+            <div class="input-field col s12 m12 l8 offset-l2">
+                <i class="material-icons prefix">search</i>
+                <select id="settings-search-items-per-page" name="searchItemsPerPage" <@validation.validation "searchItemsPerPage"/>>
+                    <#list searchResultsPerPageOptions as number>
+                        <#if settings.getSearchItemsPerPage() == number>
+                            <option selected value="${number}">${number}</option>
+                        <#else>
+                            <option value="${number}">${number}</option>
+                        </#if>
+                    </#list>
+                </select>
+                <label for="settings-search-items-per-page">${locale.getString("settings.search.itemsPerPage")}</label>
+            </div>
+        </div>
+
+        <div class="row">
+            <div class="col s12 center-align">
+                <@header.buttonSubmit name='action' icon='save' localizationKey='save' color='background-green' formaction='/settings/save/personalization'/>
+            </div>
+        </div>
+    </@settingsContainerMacros.settingsContainer>
+</#macro>
+
+<@personalizationSettingsContainer settings/>
\ No newline at end of file
diff --git a/BudgetMasterServer/src/main/resources/templates/settings/settings.ftl b/BudgetMasterServer/src/main/resources/templates/settings/settings.ftl
index d2d9605cd..667380853 100644
--- a/BudgetMasterServer/src/main/resources/templates/settings/settings.ftl
+++ b/BudgetMasterServer/src/main/resources/templates/settings/settings.ftl
@@ -15,6 +15,7 @@
         <#import "settingsMacros.ftl" as settingsMacros>
 
         <#import "containers/settingsSecurity.ftl" as settingsSecurityMacros>
+        <#import "containers/settingsPersonalization.ftl" as settingsPersonalizationMacros>
 
 
         <main>
@@ -35,58 +36,8 @@
                                         <@settingsSecurityMacros.securitySettingsContainer/>
                                     </@settingsMacros.settingsCollapsibleItem>
 
-                                    <@settingsMacros.settingsCollapsibleItem "" "format_paint" locale.getString("settings.appearance")>
-                                        <#-- language -->
-                                        <div class="row">
-                                            <div class="input-field col s12 m12 l8 offset-l2">
-                                                <i class="material-icons prefix">translate</i>
-                                                <select id="settings-language" name="languageType" <@validation.validation "language"/>>
-                                                    <#list helpers.getAvailableLanguages() as language>
-                                                        <#if settings.getLanguage() == language>
-                                                            <option selected value="${language.getName()}">${language.getName()}</option>
-                                                        <#else>
-                                                            <option value="${language.getName()}">${language.getName()}</option>
-                                                        </#if>
-                                                    </#list>
-                                                </select>
-                                                <label for="settings-language">${locale.getString("settings.language")}</label>
-                                            </div>
-                                        </div>
-
-                                        <#-- currency -->
-                                        <div class="row">
-                                            <div class="input-field col s12 m12 l8 offset-l2">
-                                                <i class="material-icons prefix">euro</i>
-                                                <input id="settings-currency" type="text" name="currency" <@validation.validation "currency"/> value="<#if settings.getCurrency()??>${settings.getCurrency()}</#if>">
-                                                <label for="settings-currency">${locale.getString("settings.currency")}</label>
-                                            </div>
-                                        </div>
-
-                                        <#-- rest, dark theme and category style -->
-                                        <@settingsMacros.switches settings/>
-
-                                        <#-- search items per page -->
-                                        <div class="row">
-                                            <div class="input-field col s12 m12 l8 offset-l2">
-                                                <i class="material-icons prefix">search</i>
-                                                <select id="settings-search-items-per-page" name="searchItemsPerPage" <@validation.validation "searchItemsPerPage"/>>
-                                                    <#list searchResultsPerPageOptions as number>
-                                                        <#if settings.getSearchItemsPerPage() == number>
-                                                            <option selected value="${number}">${number}</option>
-                                                        <#else>
-                                                            <option value="${number}">${number}</option>
-                                                        </#if>
-                                                    </#list>
-                                                </select>
-                                                <label for="settings-search-items-per-page">${locale.getString("settings.search.itemsPerPage")}</label>
-                                            </div>
-                                        </div>
-
-                                        <div class="row">
-                                            <div class="col s12 center-align">
-                                                <@header.buttonSubmit name='action' icon='save' localizationKey='save' color='background-green'/>
-                                            </div>
-                                        </div>
+                                    <@settingsMacros.settingsCollapsibleItem "personalizationSettingsContainer" "format_paint" locale.getString("settings.appearance")>
+                                        <@settingsPersonalizationMacros.personalizationSettingsContainer settings/>
                                     </@settingsMacros.settingsCollapsibleItem>
 
                                     <@settingsMacros.settingsCollapsibleItem "" "list" "Transactions">
@@ -290,6 +241,7 @@
 
         <script>
             initSettingsContainer('SecuritySettingsContainer', 'securitySettingsContainer');
+            initSettingsContainer('PersonalizationSettingsContainer', 'personalizationSettingsContainer');
         </script>
     </@header.body>
 </html>
-- 
GitLab