La configuration du dossier d'upload et du fichier des mots de passe sont maintenant dans application.properties

This commit is contained in:
FyloZ 2020-02-22 15:42:21 -05:00
parent cbaa4ea850
commit af58fc47a1
25 changed files with 168 additions and 145 deletions

View File

@ -10,7 +10,7 @@
</parent>
<groupId>dev.fyloz.trial.colorrecipesexplorer</groupId>
<artifactId>ColorRecipesExplorer</artifactId>
<version>1.2.0</version>
<version>1.3.0</version>
<name>Color Recipes Explorer</name>
<properties>

View File

@ -1,74 +1,13 @@
package dev.fyloz.trial.colorrecipesexplorer;
import dev.fyloz.trial.colorrecipesexplorer.core.services.PasswordService;
import dev.fyloz.trial.colorrecipesexplorer.core.services.files.FilesService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.MessageSource;
import java.io.File;
import java.io.IOException;
import java.util.List;
@SpringBootApplication
public class ColorRecipesExplorerApplication {
public static final Logger LOGGER = LoggerFactory.getLogger(ColorRecipesExplorerApplication.class);
public static String UPLOAD_LOCATION;
public static final String USERS_FILE_NAME = "passwords";
public static boolean USE_PORT;
public static ColorRecipesExplorerApplication CREApp;
private MessageSource messageSource;
private FilesService filesService;
public static void main(String[] args) {
UPLOAD_LOCATION = args[0] != null ? args[0] : "./";
SpringApplication.run(ColorRecipesExplorerApplication.class, args);
}
@Autowired
public ColorRecipesExplorerApplication(MessageSource messageSource, FilesService filesService) {
this.messageSource = messageSource;
this.filesService = filesService;
CREApp = this;
LOGGER.info("Le fichier des utilisateurs se situe à: " + new File(UPLOAD_LOCATION + "/" + USERS_FILE_NAME).getAbsolutePath());
loadPasswords();
}
/**
* Charge les mots de passes contenus dans le fichier.
* <p>
* Un mot de passe correspond à une ligne dans le fichier passwords.txt.
*/
private void loadPasswords() {
String filePath = String.format("%s/%s.txt", UPLOAD_LOCATION, USERS_FILE_NAME);
try {
if(filesService.exists(filePath)) filesService.create(filePath);
List<String> fileContent = filesService.readAsStrings(filePath);
if (fileContent.size() < 1) {
LOGGER.warn("Aucun mot de passe trouvé. Il sera impossible d'utiliser certaines fonctionnalités de l'application.");
}
for (String line : fileContent) {
PasswordService.addPassword(line);
}
} catch (IOException e) {
LOGGER.error("Une erreur est survenue lors du chargement du fichier des utilisateurs", e);
LOGGER.warn("Il sera impossible d'utiliser certaines fonctionnalités de l'application.");
}
}
public MessageSource getMessageSource() {
return messageSource;
}
}

View File

@ -0,0 +1,18 @@
package dev.fyloz.trial.colorrecipesexplorer.core;
import org.slf4j.Logger;
import org.springframework.context.MessageSource;
public class Preferences {
public static Logger logger;
public static MessageSource messageSource;
public static boolean urlUsePort;
public static String uploadDirectory;
public static String passwordsFileName;
}

View File

@ -1,6 +1,7 @@
package dev.fyloz.trial.colorrecipesexplorer.core.configuration;
import dev.fyloz.trial.colorrecipesexplorer.ColorRecipesExplorerApplication;
import dev.fyloz.trial.colorrecipesexplorer.core.Preferences;
import dev.fyloz.trial.colorrecipesexplorer.core.exception.model.ModelException;
import dev.fyloz.trial.colorrecipesexplorer.core.model.MaterialType;
import dev.fyloz.trial.colorrecipesexplorer.core.services.model.MaterialTypeService;
@ -34,7 +35,7 @@ public class InitialDataLoader implements ApplicationListener<ApplicationReadyEv
try {
materialTypeService.save(materialType);
} catch (ModelException ex) {
ColorRecipesExplorerApplication.LOGGER.warn(String.format("Échec de la création du type de produit par défaut '%s': %s", materialType.getName(), ex.getMessage()));
Preferences.logger.warn(String.format("Échec de la création du type de produit par défaut '%s': %s", materialType.getName(), ex.getMessage()));
}
}
}

View File

@ -1,37 +0,0 @@
package dev.fyloz.trial.colorrecipesexplorer.core.configuration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.InjectionPoint;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Scope;
import org.springframework.core.MethodParameter;
import java.lang.reflect.Field;
import static java.util.Optional.of;
import static java.util.Optional.ofNullable;
@Configuration
public class LoggingConfiguration {
/**
* Injecte un Logger dans les méthodes qui @Autowired un Logger
*
* @param ip Le point d'injection
* @return Le Logger à injecter.
*/
@Bean
@Scope("prototype")
public Logger logger(final InjectionPoint ip) {
return LoggerFactory.getLogger(of(ip.getMethodParameter())
.<Class>map(MethodParameter::getContainingClass)
.orElseGet(() ->
ofNullable(ip.getField())
.map(Field::getDeclaringClass)
.orElseThrow(IllegalArgumentException::new)
)
);
}
}

View File

@ -1,19 +1,72 @@
package dev.fyloz.trial.colorrecipesexplorer.core.configuration;
import dev.fyloz.trial.colorrecipesexplorer.ColorRecipesExplorerApplication;
import dev.fyloz.trial.colorrecipesexplorer.core.Preferences;
import dev.fyloz.trial.colorrecipesexplorer.core.services.PasswordService;
import dev.fyloz.trial.colorrecipesexplorer.core.services.files.FilesService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.MessageSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.io.IOException;
import java.util.List;
@Configuration
public class SpringConfiguration {
@Value("${response.useport}")
public boolean usePort;
@Value("${url.useport}")
private boolean usePort;
@Value("${server.upload-directory}")
private String uploadDirectory;
@Value("${server.passwords.file-name}")
private String passwordsFileName;
private FilesService filesService;
private MessageSource messageSource;
@Autowired
public SpringConfiguration(FilesService filesService, MessageSource messageSource) {
this.filesService = filesService;
this.messageSource = messageSource;
}
@Bean
public void setUsePort() {
ColorRecipesExplorerApplication.USE_PORT = usePort;
public void setPreferences() {
Preferences.urlUsePort = usePort;
Preferences.uploadDirectory = uploadDirectory;
Preferences.passwordsFileName = passwordsFileName;
Preferences.logger = LoggerFactory.getLogger(ColorRecipesExplorerApplication.class);
Preferences.messageSource = messageSource;
}
@Bean
public void initializePasswords() {
Logger logger = Preferences.logger;
String filePath = filesService.getPath(passwordsFileName);
logger.info("Le fichier des utilisateurs se situe à: " + filesService.getFile(filePath).getAbsolutePath());
try {
if (!filesService.exists(filePath)) filesService.create(filePath);
List<String> fileContent = filesService.readAsStrings(filePath);
if (fileContent.size() < 1) {
logger.warn("Aucun mot de passe trouvé. Il sera impossible d'utiliser certaines fonctionnalités de l'application.");
}
for (String line : fileContent) {
PasswordService.addPassword(line);
}
} catch (IOException e) {
logger.error("Une erreur est survenue lors du chargement du fichier des utilisateurs", e);
logger.warn("Il sera impossible d'utiliser certaines fonctionnalités de l'application.");
}
}
}

View File

@ -1,6 +1,7 @@
package dev.fyloz.trial.colorrecipesexplorer.core.io.file;
import dev.fyloz.trial.colorrecipesexplorer.ColorRecipesExplorerApplication;
import dev.fyloz.trial.colorrecipesexplorer.core.Preferences;
import org.slf4j.Logger;
import java.io.File;
@ -13,7 +14,7 @@ import java.nio.file.Paths;
public class FileHandler {
protected String name;
protected Logger logger = ColorRecipesExplorerApplication.LOGGER;
protected Logger logger = Preferences.logger;
private FileContext context;
private FileExtension extension;
@ -90,7 +91,7 @@ public class FileHandler {
}
public Path getPath() {
return Paths.get(String.format("%s/%s/%s%s", ColorRecipesExplorerApplication.UPLOAD_LOCATION, context.getPath(), name, extension.getExtension()));
return Paths.get(String.format("%s/%s/%s%s", Preferences.uploadDirectory, context.getPath(), name, extension.getExtension()));
}
public File getFile() {

View File

@ -1,6 +1,7 @@
package dev.fyloz.trial.colorrecipesexplorer.core.io.file;
import dev.fyloz.trial.colorrecipesexplorer.ColorRecipesExplorerApplication;
import dev.fyloz.trial.colorrecipesexplorer.core.Preferences;
import dev.fyloz.trial.colorrecipesexplorer.core.model.Recipe;
import dev.fyloz.trial.colorrecipesexplorer.core.services.model.RecipeService;
@ -10,7 +11,7 @@ import java.util.stream.Collectors;
@Deprecated(since = "1.3.0")
public class ImageHandler extends FileHandler {
public static final String IMAGES_LOCATION = ColorRecipesExplorerApplication.UPLOAD_LOCATION + "/images";
public static final String IMAGES_LOCATION = Preferences.uploadDirectory + "/images";
private Recipe recipe;
private int index = 0;

View File

@ -1,6 +1,6 @@
package dev.fyloz.trial.colorrecipesexplorer.core.io.response;
import dev.fyloz.trial.colorrecipesexplorer.ColorRecipesExplorerApplication;
import dev.fyloz.trial.colorrecipesexplorer.core.Preferences;
import org.springframework.context.i18n.LocaleContextHolder;
import java.util.HashMap;
@ -13,7 +13,7 @@ public class JSONResponseBuilder extends ResponseBuilder<JSONResponseBuilder, Ma
Map<String, Object> attributeContent = new HashMap<>();
// Récupère le message depuis le fichier des messages de la bonne language et rajoute ces paramètres.
String message = ColorRecipesExplorerApplication.CREApp.getMessageSource().getMessage(responseMessagePath, parameters, LocaleContextHolder.getLocale());
String message = Preferences.messageSource.getMessage(responseMessagePath, parameters, LocaleContextHolder.getLocale());
attributeContent.put("message", message);
addAttribute(responseCodeType, attributeContent);

View File

@ -1,6 +1,7 @@
package dev.fyloz.trial.colorrecipesexplorer.core.io.response;
import dev.fyloz.trial.colorrecipesexplorer.ColorRecipesExplorerApplication;
import dev.fyloz.trial.colorrecipesexplorer.core.Preferences;
import dev.fyloz.trial.colorrecipesexplorer.core.utils.ControllerUtils;
import javax.validation.constraints.NotNull;
@ -53,7 +54,7 @@ public abstract class ResponseBuilder<T extends ResponseBuilder, R> {
// Avertit s'il y a plus de paramètres que le nombre de paramètres supporté par le template thymeleaf (aussi MAX_PARAMETERS_NUMBER)
int maxParametersNumber = ResponseCode.MAX_PARAMETERS_NUMBER;
if (givenParametersNumber > maxParametersNumber) {
ColorRecipesExplorerApplication.LOGGER.warn(String.format("Trop de paramètres fournis pour le code de réponse %s: %s maximum, %s fournis", responseCode.name(), maxParametersNumber, givenParametersNumber));
Preferences.logger.warn(String.format("Trop de paramètres fournis pour le code de réponse %s: %s maximum, %s fournis", responseCode.name(), maxParametersNumber, givenParametersNumber));
}
}

View File

@ -59,6 +59,7 @@ public class Recipe implements IModel {
*
* @return Les mélanges triés par leur identifiant
*/
@JsonIgnore
public List<Mix> getMixesSortedById() {
List<Mix> sortedMixes = new ArrayList<>(mixes);
sortedMixes.sort(Comparator.comparing(Mix::getId));
@ -70,6 +71,7 @@ public class Recipe implements IModel {
*
* @return Les types de mélange contenus dans la recette
*/
@JsonIgnore
public Collection<MixType> getMixTypes() {
return mixes.stream()
.map(Mix::getMixType)
@ -82,6 +84,7 @@ public class Recipe implements IModel {
* @param mixType Le type de mélange
* @return Si la recette contient le type de mélange
*/
@JsonIgnore
public boolean hasMixType(MixType mixType) {
return getMixTypes().contains(mixType);
}

View File

@ -3,6 +3,7 @@ package dev.fyloz.trial.colorrecipesexplorer.core.services;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import dev.fyloz.trial.colorrecipesexplorer.ColorRecipesExplorerApplication;
import dev.fyloz.trial.colorrecipesexplorer.core.Preferences;
import dev.fyloz.trial.colorrecipesexplorer.core.exception.model.EntityAlreadyExistsException;
import dev.fyloz.trial.colorrecipesexplorer.core.exception.model.EntityNotFoundException;
import dev.fyloz.trial.colorrecipesexplorer.core.exception.model.ModelException;
@ -21,7 +22,7 @@ import java.util.stream.Collectors;
public class GenericService<T extends IModel, R extends JpaRepository<T, Long>> implements IGenericService<T> {
protected Logger logger = ColorRecipesExplorerApplication.LOGGER;
protected Logger logger = Preferences.logger;
protected R dao;
protected Class<T> type;
@ -127,7 +128,7 @@ public class GenericService<T extends IModel, R extends JpaRepository<T, Long>>
try {
return objectMapper.writeValueAsString(obj);
} catch (JsonProcessingException e) {
logger.error("Une erreur est survenue lors de la transformation d'un objet en Json", e);
Preferences.logger.error("Une erreur est survenue lors de la transformation d'un objet en Json", e);
return null;
}
}

View File

@ -1,7 +1,7 @@
package dev.fyloz.trial.colorrecipesexplorer.core.services.files;
import dev.fyloz.trial.colorrecipesexplorer.ColorRecipesExplorerApplication;
import org.slf4j.Logger;
import dev.fyloz.trial.colorrecipesexplorer.core.Preferences;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.ResourceLoader;
import org.springframework.stereotype.Service;
@ -20,7 +20,6 @@ import java.util.List;
public class FilesService {
private ResourceLoader resources;
private Logger logger = ColorRecipesExplorerApplication.LOGGER;
@Autowired
public FilesService(ResourceLoader resources) {
@ -92,7 +91,7 @@ public class FilesService {
multipartFile.transferTo(file.toPath());
return true;
} catch (IOException ex) {
ColorRecipesExplorerApplication.LOGGER.error("Impossible d'écrire un fichier Multipart: " + ex.getMessage());
Preferences.logger.error("Impossible d'écrire un fichier Multipart: " + ex.getMessage());
return false;
}
}
@ -107,19 +106,22 @@ public class FilesService {
public File create(String path) throws IOException {
File file = getFile(path);
if (!file.exists() || file.isDirectory()) {
Files.createDirectories(file.getParentFile().toPath());
Files.createFile(file.toPath());
}
try {
if (!file.exists() || file.isDirectory()) {
Files.createDirectories(file.getParentFile().toPath());
Files.createFile(file.toPath());
}
return file;
return file;
} catch (IOException ex) {
throw new RuntimeException("Impossible de créer un fichier: " + ex.getMessage());
}
}
/**
* Supprime un fichier sur le disque.
*
* @param path Le chemin vers le fichier
* @throws IOException La suppression du fichier échoue
*/
public void delete(String path) {
File file = getFile(path);
@ -127,7 +129,7 @@ public class FilesService {
try {
if (file.exists() && !file.isDirectory()) Files.delete(file.toPath());
} catch (IOException ex) {
logger.error("Impossible de supprimer un fichier: " + ex.getMessage());
throw new RuntimeException("Impossible de supprimer un fichier: " + ex.getMessage());
}
}
@ -143,8 +145,12 @@ public class FilesService {
return file.exists() && !file.isDirectory();
}
private File getFile(String path) {
public File getFile(String path) {
return new File(path);
}
public String getPath(String fileName) {
return String.format("%s/%s", Preferences.uploadDirectory, fileName);
}
}

View File

@ -75,7 +75,7 @@ public class ImagesService {
}
private String getPath(String name) {
return String.format("%s/%s/%s", ColorRecipesExplorerApplication.UPLOAD_LOCATION, IMAGES_DIRECTORY, name);
return filesService.getPath(String.format("%s/%s", IMAGES_DIRECTORY, name));
}
}

View File

@ -115,7 +115,7 @@ public class SimdutService {
* @return Le chemin vers le fichier SIMDUT du produit
*/
private String getPath(Material material) {
return String.format("%s/%s/%s", ColorRecipesExplorerApplication.UPLOAD_LOCATION, SIMDUT_DIRECTORY, getSimdutFileName(material));
return filesService.getPath(String.format("%s/%s", SIMDUT_DIRECTORY, getSimdutFileName(material)));
}
/**

View File

@ -1,4 +1,4 @@
package dev.fyloz.trial.colorrecipesexplorer.core.services;
package dev.fyloz.trial.colorrecipesexplorer.core.services.files;
import dev.fyloz.trial.colorrecipesexplorer.core.utils.PdfBuilder;
import org.springframework.beans.factory.annotation.Autowired;

View File

@ -1,6 +1,7 @@
package dev.fyloz.trial.colorrecipesexplorer.core.services.files;
import dev.fyloz.trial.colorrecipesexplorer.ColorRecipesExplorerApplication;
import dev.fyloz.trial.colorrecipesexplorer.core.Preferences;
import dev.fyloz.trial.colorrecipesexplorer.core.model.Recipe;
import dev.fyloz.trial.colorrecipesexplorer.core.services.model.RecipeService;
import dev.fyloz.trial.colorrecipesexplorer.xlsx.XlsxExporter;
@ -49,7 +50,7 @@ public class XlsService {
* @return Le fichier ZIP contenant tous les fichiers XLS
*/
public byte[] generateForAll() {
ColorRecipesExplorerApplication.LOGGER.info("Exportation de toutes les couleurs en XLS");
Preferences.logger.info("Exportation de toutes les couleurs en XLS");
Collection<Recipe> recipes = recipeService.getAll();
try (ByteArrayOutputStream byteOutput = new ByteArrayOutputStream(); ZipOutputStream zipOutput = new ZipOutputStream(byteOutput)) {

View File

@ -1,6 +1,7 @@
package dev.fyloz.trial.colorrecipesexplorer.core.utils;
import dev.fyloz.trial.colorrecipesexplorer.ColorRecipesExplorerApplication;
import dev.fyloz.trial.colorrecipesexplorer.core.Preferences;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
@ -32,7 +33,7 @@ public class ControllerUtils {
HttpServletRequest request = attributes.getRequest();
String port = ":" + (ColorRecipesExplorerApplication.USE_PORT ? request.getServerPort() : "");
String port = ":" + (Preferences.urlUsePort ? request.getServerPort() : "");
return String.format("%s://%s%s%s", request.getScheme(), request.getServerName(), port, request.getContextPath());
}

View File

@ -2,7 +2,7 @@ package dev.fyloz.trial.colorrecipesexplorer.web.controller.files;
import dev.fyloz.trial.colorrecipesexplorer.core.io.response.ModelResponseBuilder;
import dev.fyloz.trial.colorrecipesexplorer.core.io.response.ResponseDataType;
import dev.fyloz.trial.colorrecipesexplorer.core.services.TouchUpKitService;
import dev.fyloz.trial.colorrecipesexplorer.core.services.files.TouchUpKitService;
import dev.fyloz.trial.colorrecipesexplorer.core.services.model.RecipeService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;

View File

@ -1,6 +1,7 @@
package dev.fyloz.trial.colorrecipesexplorer.xlsx;
import dev.fyloz.trial.colorrecipesexplorer.ColorRecipesExplorerApplication;
import dev.fyloz.trial.colorrecipesexplorer.core.Preferences;
import dev.fyloz.trial.colorrecipesexplorer.core.model.Mix;
import dev.fyloz.trial.colorrecipesexplorer.core.model.MixQuantity;
import dev.fyloz.trial.colorrecipesexplorer.core.model.Recipe;
@ -22,7 +23,7 @@ import java.util.Collection;
public class XlsxExporter {
public byte[] generate(Recipe recipe) {
ColorRecipesExplorerApplication.LOGGER.info(String.format("Génération du XLS de la couleur %s (%s)", recipe.getName(), recipe.getId()));
Preferences.logger.info(String.format("Génération du XLS de la couleur %s (%s)", recipe.getName(), recipe.getId()));
Document document = new Document(recipe.getName());
Sheet sheet = document.getSheet();

View File

@ -1,6 +1,7 @@
package dev.fyloz.trial.colorrecipesexplorer.xlsx.component;
import dev.fyloz.trial.colorrecipesexplorer.ColorRecipesExplorerApplication;
import dev.fyloz.trial.colorrecipesexplorer.core.Preferences;
import dev.fyloz.trial.colorrecipesexplorer.xlsx.builder.SheetBuilder;
import dev.fyloz.trial.colorrecipesexplorer.xlsx.exception.InvalidCellTypeException;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
@ -23,7 +24,7 @@ public class Document extends XSSFWorkbook {
try {
new SheetBuilder(sheet).build();
} catch (InvalidCellTypeException e) {
ColorRecipesExplorerApplication.LOGGER.error("Une erreur est survenue lors de la génération du document: " + e.getLocalizedMessage());
Preferences.logger.error("Une erreur est survenue lors de la génération du document: " + e.getLocalizedMessage());
}
}

View File

@ -0,0 +1,31 @@
spring.datasource.url=jdbc:h2:file:./workdir/recipes
spring.datasource.username=sa
spring.datasource.password=LWK4Y7TvEbNyhu1yCoG3
spring.datasource.driver-class-name=org.h2.Driver
spring.thymeleaf.template-loader-path=classpath:/src/main/java/resources/templates
spring.thymeleaf.suffix=.html
spring.messages.fallback-to-system-locale=true
spring.servlet.multipart.max-file-size=10MB
spring.servlet.multipart.max-request-size=15MB
spring.jpa.hibernate.ddl-auto=update
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
spring.h2.console.path=/dbconsole
spring.h2.console.settings.trace=true
spring.h2.console.settings.web-allow-others=true
server.port=9090
server.http2.enabled=true
server.error.whitelabel.enabled=false
server.upload-directory=./workdir
server.passwords.file-name=passwords.txt
url.useport=true
# DEBUG
spring.jpa.show-sql=true
spring.h2.console.enabled=true

View File

@ -30,5 +30,5 @@ server:
error:
whitelabel:
enabled: false
response:
url:
useport: true

View File

@ -73,16 +73,16 @@ $(() => {
}));
// Imprimante
let src = `${baseUrl}/icons/printerError.svg`;
let title = printErrorTitle;
if ($(".bpac-extension-installed").length) {
src = `${baseUrl}/icons/printer.svg`;
title = printOkTitle;
}
$("#printStatusIcon").attr({
src: src,
title: title
});
let src = `${baseUrl}/icons/printerError.svg`;
let title = printErrorTitle;
if ($(".bpac-extension-installed").length) {
src = `${baseUrl}/icons/printer.svg`;
title = printOkTitle;
}
$("#printStatusIcon").attr({
src: src,
title: title
});
});
function confirmDatabaseExport() {

View File

@ -2,6 +2,7 @@
### Note: Cette mise à jour n'est pas compatible avec les anciennes versions.
### Corrections
* Réusinage des modèles. (Empêche la compatibilité avec les anciennes versions)
* Réusinage des contrôleurs et des services (Améliore la maintenabilité)
# v1.2.0 (Imprimante P-touch)
### Corrections
@ -59,14 +60,14 @@
### Corrections
* Désactivation de l'autocomplétion dans les étapes des recettes (permet d'éviter un bug qui affiche les suggestion par dessus toutes les étapes sur Edge)
* Correction d'un bug qui permettait d'envoyer les formulaires demandant des mots de passe sans donner un mot de passe valide.
* Amélioration des contrôlleurs et du service des mélanges.
* Amélioration des contrôleurs et du service des mélanges.
* Correction d'un bug avec la création des mélanges.
### Ajouts
* L'onglet se ferme automatiquement lorsqu'un utilisateur tente d'accéder à un fichier SIMDUT inexistant.
* Meilleure sélection des produits dans l'éditeur de mélange.
* Retravail de l'affichage de la plupart des tables, les rendant moins chargées.
* Retravail de l'affichage des étapes et des images dans l'explorateur et l'éditeur de recette.
* Amélioration de l'affichage de la plupart des tables, les rendant moins chargées.
* Amélioration de l'affichage des étapes et des images dans l'explorateur et l'éditeur de recette.
* Ajout de la page de l'historique des mises à jour.
* Ajout de la journalisation.