Merge conflict fix
This commit is contained in:
commit
92c1c0eb15
34 changed files with 1161 additions and 164 deletions
131
client/src/main/java/client/Ingredient/IngredientController.java
Normal file
131
client/src/main/java/client/Ingredient/IngredientController.java
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
package client.Ingredient;
|
||||
|
||||
import commons.Ingredient;
|
||||
import javafx.scene.control.Alert;
|
||||
import javafx.scene.control.ButtonType;
|
||||
import javafx.scene.control.Alert.AlertType;
|
||||
import javafx.event.ActionEvent;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.scene.control.ListView;
|
||||
import javafx.scene.control.Button;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.http.HttpStatus;
|
||||
|
||||
public class IngredientController {
|
||||
|
||||
@FXML
|
||||
private ListView<Ingredient> ingredientListView;
|
||||
|
||||
@FXML
|
||||
private Button deleteButton;
|
||||
|
||||
private final RestTemplate restTemplate = new RestTemplate(); // Simplified REST client
|
||||
|
||||
@FXML
|
||||
private void handleDeleteIngredient(ActionEvent event) {
|
||||
// Get selected ingredient
|
||||
Ingredient selectedIngredient = ingredientListView.getSelectionModel().getSelectedItem();
|
||||
if (selectedIngredient == null) {
|
||||
showError("No ingredient selected", "Please select an ingredient to delete.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the ingredient is used in any recipe
|
||||
checkIngredientUsage(selectedIngredient);
|
||||
}
|
||||
|
||||
private void checkIngredientUsage(Ingredient ingredient) {
|
||||
String url = "http://localhost:8080/api/ingredients/" + ingredient.getId() + "/usage";
|
||||
|
||||
ResponseEntity<IngredientUsageResponse> response = restTemplate.getForEntity(url, IngredientUsageResponse.class);
|
||||
|
||||
if (response.getStatusCode() == HttpStatus.NOT_FOUND) {
|
||||
showError("Ingredient not found", "The ingredient does not exist.");
|
||||
return;
|
||||
}
|
||||
|
||||
IngredientUsageResponse usageResponse = response.getBody();
|
||||
long usedInRecipes = usageResponse != null ? usageResponse.getUsedInRecipes() : 0;
|
||||
|
||||
if (usedInRecipes > 0) {
|
||||
// If ingredient is in use, show warning to the user
|
||||
showWarningDialog(ingredient, usedInRecipes);
|
||||
} else {
|
||||
// delete if not in use
|
||||
deleteIngredient(ingredient);
|
||||
}
|
||||
}
|
||||
|
||||
private void showWarningDialog(Ingredient ingredient, long usedInRecipes) {
|
||||
Alert alert = new Alert(AlertType.WARNING);
|
||||
alert.setTitle("Warning");
|
||||
alert.setHeaderText("Ingredient in Use");
|
||||
alert.setContentText("The ingredient '" + ingredient.getName() + "' is used in " + usedInRecipes + " recipe(s). Are you sure you want to delete it?");
|
||||
|
||||
ButtonType deleteButton = new ButtonType("Delete Anyway");
|
||||
ButtonType cancelButton = new ButtonType("Cancel");
|
||||
|
||||
alert.getButtonTypes().setAll(deleteButton, cancelButton);
|
||||
|
||||
alert.showAndWait().ifPresent(response -> {
|
||||
if (response == deleteButton) {
|
||||
// delete if the user confirms
|
||||
deleteIngredient(ingredient);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void deleteIngredient(Ingredient ingredient) {
|
||||
String url = "http://localhost:8080/api/ingredients/" + ingredient.getId();
|
||||
|
||||
restTemplate.delete(url);
|
||||
showConfirmation("Deletion Successful", "The ingredient '" + ingredient.getName() + "' has been successfully deleted.");
|
||||
|
||||
// refresh
|
||||
refreshIngredientList();
|
||||
}
|
||||
|
||||
private void showError(String title, String message) {
|
||||
Alert alert = new Alert(AlertType.ERROR);
|
||||
alert.setTitle(title);
|
||||
alert.setHeaderText(null);
|
||||
alert.setContentText(message);
|
||||
alert.showAndWait();
|
||||
}
|
||||
|
||||
private void showConfirmation(String title, String message) {
|
||||
Alert alert = new Alert(AlertType.INFORMATION);
|
||||
alert.setTitle(title);
|
||||
alert.setHeaderText(null);
|
||||
alert.setContentText(message);
|
||||
alert.showAndWait();
|
||||
}
|
||||
|
||||
private void refreshIngredientList() {
|
||||
// Refresh
|
||||
ingredientListView.getItems().clear();
|
||||
}
|
||||
|
||||
// Inner class for usage response
|
||||
public static class IngredientUsageResponse {
|
||||
private long ingredientId;
|
||||
private long usedInRecipes;
|
||||
|
||||
public long getIngredientId() {
|
||||
return ingredientId;
|
||||
}
|
||||
|
||||
public void setIngredientId(long ingredientId) {
|
||||
this.ingredientId = ingredientId;
|
||||
}
|
||||
|
||||
public long getUsedInRecipes() {
|
||||
return usedInRecipes;
|
||||
}
|
||||
|
||||
public void setUsedInRecipes(long usedInRecipes) {
|
||||
this.usedInRecipes = usedInRecipes;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -26,4 +26,8 @@ public class UI extends Application {
|
|||
var mainCtrl = INJECTOR.getInstance(MainCtrl.class);
|
||||
mainCtrl.setup(primaryStage, foodpal);
|
||||
}
|
||||
|
||||
public static MyFXML getFXML() {
|
||||
return FXML;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import java.util.logging.Logger;
|
|||
import java.util.stream.Collectors;
|
||||
|
||||
import client.exception.InvalidModificationException;
|
||||
import client.scenes.recipe.IngredientsPopupCtrl;
|
||||
import client.scenes.recipe.RecipeDetailCtrl;
|
||||
|
||||
import client.utils.Config;
|
||||
|
|
@ -20,6 +21,7 @@ import client.utils.LocaleManager;
|
|||
import client.utils.ServerUtils;
|
||||
import client.utils.WebSocketDataService;
|
||||
import client.utils.WebSocketUtils;
|
||||
import commons.Ingredient;
|
||||
import commons.Recipe;
|
||||
|
||||
import commons.ws.Topics;
|
||||
|
|
@ -36,17 +38,20 @@ import javafx.scene.control.Button;
|
|||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.ListCell;
|
||||
import javafx.scene.control.ListView;
|
||||
import javafx.scene.control.TextInputDialog;
|
||||
import javafx.scene.control.ToggleButton;
|
||||
import org.apache.commons.lang3.NotImplementedException;
|
||||
import org.apache.commons.lang3.tuple.ImmutablePair;
|
||||
import org.apache.commons.lang3.tuple.Pair;
|
||||
|
||||
|
||||
public class FoodpalApplicationCtrl implements LocaleAware {
|
||||
private final ServerUtils server;
|
||||
private final WebSocketUtils webSocketUtils;
|
||||
private final LocaleManager localeManager;
|
||||
private final WebSocketDataService<Long, Recipe> dataService;
|
||||
private final Logger logger = Logger.getLogger(FoodpalApplicationCtrl.class.getName());
|
||||
|
||||
@FXML
|
||||
private RecipeDetailCtrl recipeDetailController;
|
||||
|
||||
|
|
@ -60,6 +65,9 @@ public class FoodpalApplicationCtrl implements LocaleAware {
|
|||
@FXML
|
||||
public ListView<Recipe> recipeList;
|
||||
|
||||
@FXML
|
||||
private ListView<Ingredient> ingredientListView;
|
||||
|
||||
@FXML
|
||||
private Button addRecipeButton;
|
||||
|
||||
|
|
@ -93,6 +101,7 @@ public class FoodpalApplicationCtrl implements LocaleAware {
|
|||
|
||||
this.configService = configService;
|
||||
this.dataService = recipeDataService;
|
||||
|
||||
setupDataService();
|
||||
logger.info("WebSocket processor initialized.");
|
||||
initializeWebSocket();
|
||||
|
|
@ -268,7 +277,10 @@ public class FoodpalApplicationCtrl implements LocaleAware {
|
|||
public void refresh() {
|
||||
List<Recipe> recipes;
|
||||
try {
|
||||
recipes = server.getRecipesFiltered(searchBarController.getFilter());
|
||||
recipes = server.getRecipesFiltered(
|
||||
searchBarController.getFilter(),
|
||||
this.configService.getConfig().getRecipeLanguages()
|
||||
);
|
||||
} catch (IOException | InterruptedException e) {
|
||||
recipes = Collections.emptyList();
|
||||
String msg = "Failed to load recipes: " + e.getMessage();
|
||||
|
|
@ -400,6 +412,139 @@ public class FoodpalApplicationCtrl implements LocaleAware {
|
|||
this.recipeDetailController.refreshFavouriteButton();
|
||||
this.recipeDetailController.setVisible(!recipeList.getItems().isEmpty());
|
||||
}
|
||||
|
||||
//Delete Ingredient button click
|
||||
@FXML
|
||||
private void handleDeleteIngredient() {
|
||||
// Get selected ingredient
|
||||
Ingredient selectedIngredient = ingredientListView.getSelectionModel().getSelectedItem();
|
||||
|
||||
if (selectedIngredient == null) {
|
||||
// Show an error message if no ingredient is selected
|
||||
showError("No ingredient selected", "Please select an ingredient to delete.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the ingredient is used in any recipe
|
||||
checkIngredientUsage(selectedIngredient);
|
||||
}
|
||||
|
||||
// Check if ingredient is used in any recipe before deleting
|
||||
private void checkIngredientUsage(Ingredient ingredient) {
|
||||
try {
|
||||
long usageCount = server.getIngredientUsage(ingredient.getId()); // Check ingredient usage via ServerUtils
|
||||
|
||||
if (usageCount > 0) {
|
||||
// If ingredient is used, show a warning dialog
|
||||
showWarningDialog(ingredient, usageCount);
|
||||
} else {
|
||||
// If not used, delete
|
||||
deleteIngredient(ingredient);
|
||||
}
|
||||
} catch (IOException | InterruptedException e) {
|
||||
showError("Error", "Failed to check ingredient usage: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void deleteIngredient(Ingredient ingredient) {
|
||||
try {
|
||||
server.deleteIngredient(ingredient.getId()); // Call ServerUtils to delete the ingredient
|
||||
showConfirmation("Success", "Ingredient '" + ingredient.getName() + "' has been deleted.");
|
||||
refreshIngredientList(); // refresh the ingredient list
|
||||
} catch (IOException | InterruptedException e) {
|
||||
showError("Error", "Failed to delete ingredient: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void showWarningDialog(Ingredient ingredient, long usedInRecipes) {
|
||||
Alert alert = new Alert(Alert.AlertType.WARNING);
|
||||
}
|
||||
|
||||
private void showError(String title, String message) {
|
||||
Alert alert = new Alert(Alert.AlertType.ERROR);
|
||||
alert.setTitle(title);
|
||||
alert.setHeaderText(null);
|
||||
alert.setContentText(message);
|
||||
alert.showAndWait();
|
||||
}
|
||||
|
||||
private void showConfirmation(String title, String message) {
|
||||
Alert alert = new Alert(Alert.AlertType.INFORMATION);
|
||||
alert.setTitle(title);
|
||||
alert.setHeaderText(null);
|
||||
alert.setContentText(message);
|
||||
alert.showAndWait();
|
||||
}
|
||||
|
||||
private void refreshIngredientList() {
|
||||
// Refresh
|
||||
ingredientListView.getItems().clear();
|
||||
|
||||
}
|
||||
|
||||
|
||||
@FXML
|
||||
private void handleAddIngredient() {
|
||||
//ask the user for the ingredient name
|
||||
TextInputDialog dialog = new TextInputDialog();
|
||||
dialog.setTitle("Add Ingredient");
|
||||
dialog.setHeaderText("Enter the ingredient name:");
|
||||
dialog.setContentText("Ingredient:");
|
||||
|
||||
// Wait for the user to enter a value
|
||||
Optional<String> result = dialog.showAndWait();
|
||||
|
||||
result.ifPresent(name -> {
|
||||
// Create a new Ingredient object
|
||||
Ingredient newIngredient = new Ingredient();
|
||||
newIngredient.setName(name);
|
||||
|
||||
// Add the new ingredient to the ListView
|
||||
ingredientListView.getItems().add(newIngredient);
|
||||
});
|
||||
}
|
||||
|
||||
// display ingredient name
|
||||
@FXML
|
||||
private void initializeIngredients() {
|
||||
ingredientListView.setCellFactory(list -> new ListCell<Ingredient>() {
|
||||
@Override
|
||||
protected void updateItem(Ingredient item, boolean empty) {
|
||||
super.updateItem(item, empty);
|
||||
if (empty || item == null) {
|
||||
setText(null);
|
||||
} else {
|
||||
setText(item.getName()); // Display the ingredient name in the ListView
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@FXML
|
||||
private void openIngredientsPopup() {
|
||||
try {
|
||||
var pair = client.UI.getFXML().load(
|
||||
IngredientsPopupCtrl.class,
|
||||
"client", "scenes", "recipe", "IngredientsPopup.fxml"
|
||||
);
|
||||
|
||||
var root = pair.getValue();
|
||||
|
||||
var stage = new javafx.stage.Stage();
|
||||
stage.setTitle("Ingredients");
|
||||
stage.initModality(javafx.stage.Modality.APPLICATION_MODAL);
|
||||
stage.setScene(new javafx.scene.Scene(root));
|
||||
stage.showAndWait();
|
||||
} catch (Exception e) {
|
||||
var alert = new Alert(Alert.AlertType.ERROR);
|
||||
alert.setTitle("Error");
|
||||
alert.setHeaderText("Failed to open ingredients popup");
|
||||
alert.setContentText(e.getMessage());
|
||||
alert.showAndWait();
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -410,3 +555,8 @@ public class FoodpalApplicationCtrl implements LocaleAware {
|
|||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
87
client/src/main/java/client/scenes/LanguageFilterCtrl.java
Normal file
87
client/src/main/java/client/scenes/LanguageFilterCtrl.java
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
package client.scenes;
|
||||
|
||||
import client.utils.ConfigService;
|
||||
import client.utils.LocaleAware;
|
||||
import client.utils.LocaleManager;
|
||||
import com.google.inject.Inject;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.scene.control.CheckMenuItem;
|
||||
import javafx.scene.control.MenuButton;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class LanguageFilterCtrl implements LocaleAware {
|
||||
private final LocaleManager manager;
|
||||
private final ConfigService configService;
|
||||
private final FoodpalApplicationCtrl appCtrl;
|
||||
|
||||
@Inject
|
||||
public LanguageFilterCtrl(LocaleManager manager, ConfigService configService, FoodpalApplicationCtrl appCtrl) {
|
||||
this.manager = manager;
|
||||
this.configService = configService;
|
||||
this.appCtrl = appCtrl;
|
||||
}
|
||||
|
||||
@FXML
|
||||
MenuButton langFilterMenu;
|
||||
|
||||
List<String> selectedLanguages = new ArrayList<>();
|
||||
|
||||
private String getSelectedLanguagesDisplay() {
|
||||
String joined = String.join(", ", selectedLanguages);
|
||||
if (joined.isEmpty()) {
|
||||
return "none";
|
||||
} else {
|
||||
return joined;
|
||||
}
|
||||
}
|
||||
|
||||
private void updateMenuButtonDisplay() {
|
||||
langFilterMenu.setText(getLocaleString("menu.label.selected-langs") + ": " + this.getSelectedLanguagesDisplay());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateText() {
|
||||
this.updateMenuButtonDisplay();
|
||||
}
|
||||
|
||||
@Override
|
||||
public LocaleManager getLocaleManager() {
|
||||
return this.manager;
|
||||
}
|
||||
|
||||
public void initializeComponents() {
|
||||
var items = this.langFilterMenu.getItems();
|
||||
|
||||
final List<String> languages = List.of("en", "nl", "pl");
|
||||
this.selectedLanguages = this.configService.getConfig().getRecipeLanguages();
|
||||
this.updateMenuButtonDisplay();
|
||||
|
||||
items.clear();
|
||||
|
||||
languages.forEach(lang -> {
|
||||
CheckMenuItem it = new CheckMenuItem(lang);
|
||||
|
||||
if (selectedLanguages.contains(it.getText())) {
|
||||
it.setSelected(true);
|
||||
}
|
||||
|
||||
it.selectedProperty().addListener((observable, _, value) -> {
|
||||
if (value) {
|
||||
selectedLanguages.add(it.getText());
|
||||
} else {
|
||||
selectedLanguages.remove(it.getText());
|
||||
}
|
||||
|
||||
configService.save();
|
||||
selectedLanguages.sort(String::compareTo);
|
||||
appCtrl.refresh();
|
||||
|
||||
this.updateMenuButtonDisplay();
|
||||
});
|
||||
|
||||
items.add(it);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
package client.scenes;
|
||||
|
||||
import client.utils.ConfigService;
|
||||
import client.utils.LocaleAware;
|
||||
import client.utils.LocaleManager;
|
||||
import client.utils.ServerUtils;
|
||||
|
|
@ -28,6 +29,7 @@ public class SearchBarCtrl implements LocaleAware {
|
|||
|
||||
private final LocaleManager localeManager;
|
||||
private final ServerUtils serverUtils;
|
||||
private final ConfigService configService;
|
||||
|
||||
private Consumer<List<Recipe>> onSearchCallback;
|
||||
|
||||
|
|
@ -41,9 +43,10 @@ public class SearchBarCtrl implements LocaleAware {
|
|||
private Task<List<Recipe>> currentSearchTask = null;
|
||||
|
||||
@Inject
|
||||
public SearchBarCtrl(LocaleManager localeManager, ServerUtils serverUtils) {
|
||||
public SearchBarCtrl(LocaleManager localeManager, ServerUtils serverUtils, ConfigService configService) {
|
||||
this.localeManager = localeManager;
|
||||
this.serverUtils = serverUtils;
|
||||
this.configService = configService;
|
||||
}
|
||||
|
||||
@FXML
|
||||
|
|
@ -96,7 +99,7 @@ public class SearchBarCtrl implements LocaleAware {
|
|||
currentSearchTask = new Task<>() {
|
||||
@Override
|
||||
protected List<Recipe> call() throws IOException, InterruptedException {
|
||||
return serverUtils.getRecipesFiltered(filter);
|
||||
return serverUtils.getRecipesFiltered(filter, configService.getConfig().getRecipeLanguages());
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -75,6 +75,10 @@ public class IngredientListCell extends OrderedEditableListCell<RecipeIngredient
|
|||
String name = nameInput.getText();
|
||||
if (unit == null || !unit.isFormal()) {
|
||||
String desc = amountInput.getText();
|
||||
if (desc.isEmpty() || name.isEmpty()) {
|
||||
// TODO printError() integration
|
||||
return; // The user is forced to kindly try again until something valid comes up.
|
||||
}
|
||||
Ingredient newIngredient = new Ingredient(name, 0., 0., 0.);
|
||||
commitEdit(new VagueIngredient(newIngredient, desc));
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,136 @@
|
|||
package client.scenes.recipe;
|
||||
|
||||
import client.utils.ServerUtils;
|
||||
import commons.Ingredient;
|
||||
import jakarta.inject.Inject;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.scene.control.Alert;
|
||||
import javafx.scene.control.ListCell;
|
||||
import javafx.scene.control.ListView;
|
||||
import javafx.scene.control.TextInputDialog;
|
||||
import javafx.stage.Stage;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
//TODO and check for capital letter milk and MILK are seen as different
|
||||
|
||||
|
||||
public class IngredientsPopupCtrl {
|
||||
|
||||
private final ServerUtils server;
|
||||
|
||||
@FXML
|
||||
private ListView<Ingredient> ingredientListView;
|
||||
|
||||
@Inject
|
||||
public IngredientsPopupCtrl(ServerUtils server) {
|
||||
this.server = server;
|
||||
}
|
||||
|
||||
@FXML
|
||||
public void initialize() {
|
||||
ingredientListView.setCellFactory(list -> new ListCell<>() {
|
||||
@Override
|
||||
protected void updateItem(Ingredient item, boolean empty) {
|
||||
super.updateItem(item, empty);
|
||||
if (empty || item == null) {
|
||||
setText(null);
|
||||
} else {
|
||||
setText(item.getName());
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
refresh();
|
||||
}
|
||||
@FXML
|
||||
private void addIngredient() {
|
||||
TextInputDialog dialog = new TextInputDialog();
|
||||
dialog.setTitle("Add Ingredient");
|
||||
dialog.setHeaderText("Create a new ingredient");
|
||||
dialog.setContentText("Name:");
|
||||
|
||||
Optional<String> result = dialog.showAndWait();
|
||||
if (result.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
String name = result.get().trim();
|
||||
if (name.isEmpty()) {
|
||||
showError("Ingredient name cannot be empty.");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
server.createIngredient(name); // calls POST /api/ingredients
|
||||
refresh(); // reload list from server
|
||||
} catch (IOException | InterruptedException e) {
|
||||
showError("Failed to create ingredient: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@FXML
|
||||
private void refresh() {
|
||||
try {
|
||||
List<Ingredient> ingredients = server.getIngredients();
|
||||
ingredientListView.getItems().setAll(ingredients);
|
||||
} catch (IOException | InterruptedException e) {
|
||||
showError("Failed to load ingredients: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@FXML
|
||||
private void deleteSelected() {
|
||||
Ingredient selected = ingredientListView.getSelectionModel().getSelectedItem();
|
||||
if (selected == null) {
|
||||
showError("No ingredient selected.");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
long usageCount = server.getIngredientUsage(selected.getId());
|
||||
if (usageCount > 0) {
|
||||
boolean proceed = confirmDeleteUsed(selected.getName(), usageCount);
|
||||
if (!proceed) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
server.deleteIngredient(selected.getId());
|
||||
refresh();
|
||||
} catch (IOException | InterruptedException e) {
|
||||
showError("Failed to delete ingredient: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@FXML
|
||||
private void close() {
|
||||
Stage stage = (Stage) ingredientListView.getScene().getWindow();
|
||||
stage.close();
|
||||
}
|
||||
|
||||
private boolean confirmDeleteUsed(String name, long usedInRecipes) {
|
||||
Alert alert = new Alert(Alert.AlertType.WARNING);
|
||||
alert.setTitle("Warning");
|
||||
alert.setHeaderText("Ingredient in use");
|
||||
alert.setContentText("Ingredient '" + name + "' is used in " + usedInRecipes
|
||||
+ " recipe(s). Delete anyway?");
|
||||
|
||||
var delete = new javafx.scene.control.ButtonType("Delete Anyway");
|
||||
var cancel = new javafx.scene.control.ButtonType("Cancel");
|
||||
alert.getButtonTypes().setAll(delete, cancel);
|
||||
|
||||
return alert.showAndWait().orElse(cancel) == delete;
|
||||
}
|
||||
|
||||
private void showError(String msg) {
|
||||
Alert alert = new Alert(Alert.AlertType.ERROR);
|
||||
alert.setTitle("Error");
|
||||
alert.setHeaderText(null);
|
||||
alert.setContentText(msg);
|
||||
alert.showAndWait();
|
||||
}
|
||||
}
|
||||
|
|
@ -19,6 +19,7 @@ import javafx.scene.control.Button;
|
|||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.ListView;
|
||||
import javafx.scene.control.TextField;
|
||||
import javafx.scene.control.ComboBox;
|
||||
import javafx.scene.input.KeyCode;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.VBox;
|
||||
|
|
@ -77,6 +78,9 @@ public class RecipeDetailCtrl implements LocaleAware {
|
|||
@FXML
|
||||
private Button favouriteButton;
|
||||
|
||||
@FXML
|
||||
private ComboBox<String> langSelector;
|
||||
|
||||
private ListView<Recipe> getParentRecipeList() {
|
||||
return this.appCtrl.recipeList;
|
||||
}
|
||||
|
|
@ -138,6 +142,7 @@ public class RecipeDetailCtrl implements LocaleAware {
|
|||
this.showName(recipe.getName());
|
||||
this.ingredientListController.refetchFromRecipe(recipe);
|
||||
this.stepListController.refetchFromRecipe(recipe);
|
||||
this.langSelector.setValue(recipe.getLocale());
|
||||
this.refreshFavouriteButton();
|
||||
}
|
||||
|
||||
|
|
@ -294,6 +299,20 @@ public class RecipeDetailCtrl implements LocaleAware {
|
|||
editableTitleArea.getChildren().add(nameLabel);
|
||||
}
|
||||
|
||||
/**
|
||||
* Switch the recipe's language.
|
||||
*/
|
||||
@FXML
|
||||
void changeLanguage() {
|
||||
recipe.setLocale(this.langSelector.getValue());
|
||||
|
||||
try {
|
||||
server.updateRecipe(this.recipe);
|
||||
} catch (IOException | InterruptedException e) {
|
||||
throw new UpdateException("Error occurred when updating recipe locale!");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateText() {
|
||||
editRecipeTitleButton.setText(getLocaleString("menu.button.edit"));
|
||||
|
|
@ -309,5 +328,7 @@ public class RecipeDetailCtrl implements LocaleAware {
|
|||
@Override
|
||||
public void initializeComponents() {
|
||||
initStepsIngredientsList();
|
||||
|
||||
langSelector.getItems().addAll("en", "nl", "pl");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ import java.util.List;
|
|||
public class Config {
|
||||
private String language;
|
||||
private String serverUrl;
|
||||
|
||||
private List<Long> favourites;
|
||||
private List<String> shoppingList;
|
||||
|
||||
|
|
@ -68,5 +67,28 @@ public class Config {
|
|||
public void removeFavourite(long recipeId) {
|
||||
getFavourites().remove(recipeId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a list of languages that should filter the displayed recipes.
|
||||
*
|
||||
* @return The desired languages the user would like to see.
|
||||
*/
|
||||
public List<String> getRecipeLanguages() {
|
||||
return this.recipeLanguages;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a language to the list of filtering languages.
|
||||
*/
|
||||
public void addRecipeLanguage(String lang) {
|
||||
this.recipeLanguages.add(lang);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a language from the list of filtering languages.
|
||||
*/
|
||||
public void removeRecipeLanguage(String lang) {
|
||||
this.recipeLanguages.remove(lang);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ public class DefaultValueFactory {
|
|||
return new Recipe(
|
||||
null,
|
||||
"Untitled recipe",
|
||||
"en",
|
||||
List.of(),
|
||||
List.of());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package client.utils;
|
|||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.google.inject.Inject;
|
||||
import commons.Ingredient;
|
||||
import commons.Recipe;
|
||||
import commons.RecipeIngredient;
|
||||
import jakarta.ws.rs.ProcessingException;
|
||||
|
|
@ -39,9 +40,16 @@ public class ServerUtils {
|
|||
* Gets all the recipes from the backend.
|
||||
* @return a JSON string with all the recipes
|
||||
*/
|
||||
public List<Recipe> getRecipes() throws IOException, InterruptedException {
|
||||
public List<Recipe> getRecipes(List<String> locales) throws IOException, InterruptedException {
|
||||
|
||||
String uri =
|
||||
SERVER +
|
||||
"/recipes" +
|
||||
"?locales=" +
|
||||
String.join(",", locales);
|
||||
|
||||
HttpRequest request = HttpRequest.newBuilder()
|
||||
.uri(URI.create(SERVER + "/recipes"))
|
||||
.uri(URI.create(uri))
|
||||
.GET()
|
||||
.build();
|
||||
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
|
||||
|
|
@ -56,9 +64,9 @@ public class ServerUtils {
|
|||
return list; // JSON string-> List<Recipe> (Jackson)
|
||||
}
|
||||
|
||||
public List<Recipe> getRecipesFiltered(String filter) throws IOException, InterruptedException {
|
||||
public List<Recipe> getRecipesFiltered(String filter, List<String> locales) throws IOException, InterruptedException {
|
||||
// TODO: implement filtering on server side
|
||||
return this.getRecipes();
|
||||
return this.getRecipes(locales);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -87,7 +95,7 @@ public class ServerUtils {
|
|||
*/
|
||||
public Recipe addRecipe(Recipe newRecipe) throws IOException, InterruptedException {
|
||||
//Make sure the name of the newRecipe is unique
|
||||
List<Recipe> allRecipes = getRecipes();
|
||||
List<Recipe> allRecipes = getRecipes(List.of());
|
||||
newRecipe.setId(null); // otherwise the id is the same as the original, and that's wrong
|
||||
// now that each recipeIngredient has its own ID in the database,
|
||||
// we set that to null too to force a new persist value on the server
|
||||
|
|
@ -207,4 +215,84 @@ public class ServerUtils {
|
|||
|
||||
updateRecipe(recipe);
|
||||
}
|
||||
|
||||
|
||||
// how many ingredients are getting used in recipes
|
||||
|
||||
public long getIngredientUsage(long ingredientId) throws IOException, InterruptedException {
|
||||
HttpRequest request = HttpRequest.newBuilder()
|
||||
.uri(URI.create(SERVER + "/ingredients/" + ingredientId + "/usage"))
|
||||
.GET()
|
||||
.build();
|
||||
|
||||
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
|
||||
if (response.statusCode() != statusOK) {
|
||||
throw new IOException("Failed to get usage for ingredient with id: " + ingredientId
|
||||
+ " body: " + response.body());
|
||||
}
|
||||
|
||||
record IngredientUsageResponse(long ingredientId, long usedInRecipes) {}
|
||||
IngredientUsageResponse usage =
|
||||
objectMapper.readValue(response.body(), IngredientUsageResponse.class);
|
||||
|
||||
return usage.usedInRecipes();
|
||||
}
|
||||
|
||||
|
||||
public void deleteIngredient(long ingredientId) throws IOException, InterruptedException {
|
||||
// Send delete request to remove the ingredient
|
||||
HttpRequest request = HttpRequest.newBuilder()
|
||||
.uri(URI.create(SERVER + "/ingredients/" + ingredientId))
|
||||
.DELETE()
|
||||
.build();
|
||||
|
||||
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
|
||||
|
||||
if (response.statusCode() != statusOK) {
|
||||
throw new IOException("Failed to delete ingredient with id: " + ingredientId + " body: " + response.body());
|
||||
}
|
||||
|
||||
logger.info("Successfully deleted ingredient with id: " + ingredientId);
|
||||
}
|
||||
|
||||
|
||||
//retrieves the list of ingredients saved to backend
|
||||
|
||||
public List<Ingredient> getIngredients() throws IOException, InterruptedException {
|
||||
HttpRequest request = HttpRequest.newBuilder()
|
||||
.uri(URI.create(SERVER + "/ingredients"))
|
||||
.GET()
|
||||
.build();
|
||||
|
||||
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
|
||||
if (response.statusCode() != statusOK) {
|
||||
throw new IOException("Failed to fetch ingredients. Server responds with: " + response.body());
|
||||
}
|
||||
|
||||
return objectMapper.readValue(response.body(), new com.fasterxml.jackson.core.type.TypeReference<List<Ingredient>>() {});
|
||||
}
|
||||
|
||||
//creates new ingredients in the ingredient list
|
||||
|
||||
public Ingredient createIngredient(String name) throws IOException, InterruptedException {
|
||||
Ingredient ingredient = new Ingredient(name, 0.0, 0.0, 0.0);
|
||||
String json = objectMapper.writeValueAsString(ingredient);
|
||||
|
||||
HttpRequest request = HttpRequest.newBuilder()
|
||||
.uri(URI.create(SERVER + "/ingredients"))
|
||||
.header("Content-Type", "application/json")
|
||||
.POST(HttpRequest.BodyPublishers.ofString(json))
|
||||
.build();
|
||||
|
||||
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
|
||||
if (response.statusCode() != statusOK) {
|
||||
throw new IOException("Failed to create ingredient. Server responds with: " + response.body());
|
||||
}
|
||||
|
||||
return objectMapper.readValue(response.body(), Ingredient.class);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@
|
|||
<Font name="System Bold" size="29.0" />
|
||||
</font>
|
||||
</Label>
|
||||
</HBox>
|
||||
</HBox>
|
||||
</top>
|
||||
|
||||
<!-- LEFT: RECIPE LIST -->
|
||||
|
|
@ -44,18 +44,25 @@
|
|||
<Label fx:id="recipesLabel" text="Recipes">
|
||||
<font>
|
||||
<Font name="System Bold" size="15.0" />
|
||||
</font></Label>
|
||||
</font>
|
||||
</Label>
|
||||
|
||||
<fx:include source="SearchBar.fxml" fx:id="searchBar" />
|
||||
<fx:include source="LanguageFilter.fxml" fx:id="langFilter" />
|
||||
|
||||
<ListView fx:id="recipeList" />
|
||||
|
||||
<HBox spacing="10">
|
||||
<Button fx:id="addRecipeButton" onAction="#addRecipe" text="Add Recipe" />
|
||||
<Button fx:id="removeRecipeButton" onAction="#removeSelectedRecipe" text="Remove Recipe" />
|
||||
<Button fx:id= "cloneRecipeButton" mnemonicParsing="false" onAction="#cloneRecipe" text="Clone" />
|
||||
<ToggleButton fx:id="favouritesOnlyToggle" text="Favourites" onAction="#toggleFavouritesView"/>
|
||||
<Button fx:id="cloneRecipeButton" mnemonicParsing="false" onAction="#cloneRecipe" text="Clone" />
|
||||
<ToggleButton fx:id="favouritesOnlyToggle" text="Favourites" onAction="#toggleFavouritesView" />
|
||||
</HBox>
|
||||
|
||||
<Button fx:id="manageIngredientsButton"
|
||||
onAction="#openIngredientsPopup"
|
||||
text="Ingredients..." />
|
||||
|
||||
</VBox>
|
||||
</left>
|
||||
|
||||
|
|
|
|||
12
client/src/main/resources/client/scenes/LanguageFilter.fxml
Normal file
12
client/src/main/resources/client/scenes/LanguageFilter.fxml
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<?import javafx.scene.control.MenuButton?>
|
||||
|
||||
<MenuButton
|
||||
mnemonicParsing="false"
|
||||
text="Languages"
|
||||
xmlns:fx="http://javafx.com/fxml/1"
|
||||
xmlns="http://javafx.com/javafx/25"
|
||||
fx:controller="client.scenes.LanguageFilterCtrl"
|
||||
fx:id="langFilterMenu"
|
||||
/>
|
||||
|
|
@ -9,33 +9,32 @@
|
|||
<?import javafx.scene.layout.VBox?>
|
||||
<?import javafx.scene.text.Font?>
|
||||
|
||||
<AnchorPane xmlns="http://javafx.com/javafx/25" xmlns:fx="http://javafx.com/fxml/1" fx:controller="client.scenes.recipe.IngredientListCtrl">
|
||||
|
||||
<VBox xmlns="http://javafx.com/javafx/25" xmlns:fx="http://javafx.com/fxml/1"
|
||||
fx:controller="client.scenes.recipe.IngredientListCtrl"
|
||||
minHeight="200.0" minWidth="200.0" maxWidth="Infinity">
|
||||
<children>
|
||||
<VBox minHeight="200.0" minWidth="200.0">
|
||||
<HBox alignment="CENTER_LEFT" spacing="20.0">
|
||||
<children>
|
||||
<HBox alignment="CENTER_LEFT" spacing="20.0">
|
||||
<children>
|
||||
<Label fx:id="ingredientsLabel" text="Ingredients">
|
||||
<padding>
|
||||
<Insets bottom="5.0" top="5.0" />
|
||||
</padding>
|
||||
<font>
|
||||
<Font name="System Bold" size="16.0" />
|
||||
</font>
|
||||
</Label>
|
||||
<HBox alignment="CENTER" spacing="10.0">
|
||||
<children>
|
||||
<Button fx:id="addIngredientButton" mnemonicParsing="false" text="Add" />
|
||||
<Button fx:id="deleteIngredientButton" mnemonicParsing="false" text="Delete" />
|
||||
</children>
|
||||
</HBox>
|
||||
</children>
|
||||
<Label fx:id="ingredientsLabel" text="Ingredients">
|
||||
<padding>
|
||||
<Insets left="10.0" right="10.0" />
|
||||
<Insets bottom="5.0" top="5.0" />
|
||||
</padding>
|
||||
<font>
|
||||
<Font name="System Bold" size="16.0" />
|
||||
</font>
|
||||
</Label>
|
||||
<HBox alignment="CENTER" spacing="10.0">
|
||||
<children>
|
||||
<Button fx:id="addIngredientButton" mnemonicParsing="false" text="Add" />
|
||||
<Button fx:id="deleteIngredientButton" mnemonicParsing="false" text="Delete" />
|
||||
</children>
|
||||
</HBox>
|
||||
<ListView fx:id="ingredientListView" prefHeight="200.0" prefWidth="200.0" />
|
||||
</children>
|
||||
</VBox>
|
||||
<padding>
|
||||
<Insets left="10.0" right="10.0" />
|
||||
</padding>
|
||||
</HBox>
|
||||
<ListView fx:id="ingredientListView" prefHeight="200.0" prefWidth="200.0" />
|
||||
</children>
|
||||
</AnchorPane>
|
||||
</VBox>
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<?import javafx.geometry.Insets?>
|
||||
<?import javafx.scene.control.Button?>
|
||||
<?import javafx.scene.control.ButtonBar?>
|
||||
<?import javafx.scene.control.Label?>
|
||||
<?import javafx.scene.control.ListView?>
|
||||
<?import javafx.scene.layout.VBox?>
|
||||
|
||||
<VBox xmlns="http://javafx.com/javafx/25" xmlns:fx="http://javafx.com/fxml/1"
|
||||
fx:controller="client.scenes.recipe.IngredientsPopupCtrl"
|
||||
spacing="10" prefWidth="420" prefHeight="520">
|
||||
|
||||
<padding>
|
||||
<Insets top="12" right="12" bottom="12" left="12"/>
|
||||
</padding>
|
||||
|
||||
<Label text="Ingredients" style="-fx-font-size: 18px; -fx-font-weight: bold;"/>
|
||||
|
||||
<ListView fx:id="ingredientListView" VBox.vgrow="ALWAYS"/>
|
||||
|
||||
<ButtonBar>
|
||||
<buttons>
|
||||
<Button text="Add" onAction="#addIngredient"/>
|
||||
<Button text="Refresh" onAction="#refresh"/>
|
||||
<Button text="Delete" onAction="#deleteSelected"/>
|
||||
<Button text="Close" onAction="#close"/>
|
||||
</buttons>
|
||||
</ButtonBar>
|
||||
</VBox>
|
||||
|
|
@ -27,9 +27,13 @@
|
|||
<Button fx:id="favouriteButton" onAction="#toggleFavourite" text="☆" />
|
||||
</HBox>
|
||||
|
||||
<ComboBox fx:id="langSelector" onAction="#changeLanguage" />
|
||||
|
||||
<!-- Ingredients -->
|
||||
<fx:include source="IngredientList.fxml" fx:id="ingredientList" />
|
||||
<fx:include source="IngredientList.fxml" fx:id="ingredientList"
|
||||
VBox.vgrow="ALWAYS" maxWidth="Infinity" />
|
||||
|
||||
<!-- Preparation -->
|
||||
<fx:include source="RecipeStepList.fxml" fx:id="stepList" />
|
||||
<fx:include source="RecipeStepList.fxml" fx:id="stepList"
|
||||
VBox.vgrow="ALWAYS" maxWidth="Infinity" />
|
||||
</VBox>
|
||||
|
|
|
|||
|
|
@ -9,33 +9,31 @@
|
|||
<?import javafx.scene.layout.VBox?>
|
||||
<?import javafx.scene.text.Font?>
|
||||
|
||||
<AnchorPane xmlns="http://javafx.com/javafx/25" xmlns:fx="http://javafx.com/fxml/1" fx:controller="client.scenes.recipe.RecipeStepListCtrl">
|
||||
<VBox xmlns="http://javafx.com/javafx/25" xmlns:fx="http://javafx.com/fxml/1"
|
||||
fx:controller="client.scenes.recipe.RecipeStepListCtrl"
|
||||
minHeight="200.0" minWidth="200.0" maxWidth="Infinity">
|
||||
<children>
|
||||
<VBox minHeight="200.0" minWidth="200.0">
|
||||
<HBox alignment="CENTER_LEFT" spacing="20.0">
|
||||
<children>
|
||||
<HBox alignment="CENTER_LEFT" spacing="20.0">
|
||||
<children>
|
||||
<Label fx:id="stepsLabel" text="Steps">
|
||||
<padding>
|
||||
<Insets bottom="5.0" top="5.0" />
|
||||
</padding>
|
||||
<font>
|
||||
<Font name="System Bold" size="16.0" />
|
||||
</font>
|
||||
</Label>
|
||||
<HBox alignment="CENTER" spacing="10.0">
|
||||
<children>
|
||||
<Button fx:id="addStepButton" mnemonicParsing="false" text="Add" />
|
||||
<Button fx:id="deleteStepButton" mnemonicParsing="false" text="Delete" />
|
||||
</children>
|
||||
</HBox>
|
||||
</children>
|
||||
<Label fx:id="stepsLabel" text="Steps">
|
||||
<padding>
|
||||
<Insets left="10.0" right="10.0" />
|
||||
<Insets bottom="5.0" top="5.0" />
|
||||
</padding>
|
||||
<font>
|
||||
<Font name="System Bold" size="16.0" />
|
||||
</font>
|
||||
</Label>
|
||||
<HBox alignment="CENTER" spacing="10.0">
|
||||
<children>
|
||||
<Button fx:id="addStepButton" mnemonicParsing="false" text="Add" />
|
||||
<Button fx:id="deleteStepButton" mnemonicParsing="false" text="Delete" />
|
||||
</children>
|
||||
</HBox>
|
||||
<ListView fx:id="recipeStepListView" prefHeight="200.0" prefWidth="200.0" />
|
||||
</children>
|
||||
</VBox>
|
||||
<padding>
|
||||
<Insets left="10.0" right="10.0" />
|
||||
</padding>
|
||||
</HBox>
|
||||
<ListView fx:id="recipeStepListView" prefHeight="200.0" prefWidth="200.0" />
|
||||
</children>
|
||||
</AnchorPane>
|
||||
</VBox>
|
||||
|
|
@ -25,6 +25,8 @@ menu.button.edit=Edit
|
|||
menu.button.clone=Clone
|
||||
menu.button.print=Print recipe
|
||||
|
||||
menu.label.selected-langs=Languages
|
||||
|
||||
lang.en.display=English
|
||||
lang.nl.display=Dutch
|
||||
lang.pl.display=Polish
|
||||
|
|
@ -26,6 +26,9 @@ menu.button.clone=Clone
|
|||
menu.button.print=Print recipe
|
||||
|
||||
menu.search=Search...
|
||||
|
||||
menu.label.selected-langs=Languages
|
||||
|
||||
lang.en.display=English
|
||||
lang.nl.display=Nederlands
|
||||
lang.pl.display=Polski
|
||||
|
|
|
|||
|
|
@ -25,6 +25,8 @@ menu.button.edit=Bewerken
|
|||
menu.button.clone=Dupliceren
|
||||
menu.button.print=Recept afdrukken
|
||||
|
||||
menu.label.selected-langs=Talen
|
||||
|
||||
menu.search=Zoeken...
|
||||
lang.en.display=English
|
||||
lang.nl.display=Nederlands
|
||||
|
|
|
|||
|
|
@ -26,6 +26,9 @@ menu.button.clone=Duplikuj
|
|||
menu.button.print=Drukuj przepis
|
||||
|
||||
menu.search=Szukaj...
|
||||
|
||||
menu.label.selected-langs=J?zyki
|
||||
|
||||
lang.en.display=English
|
||||
lang.nl.display=Nederlands
|
||||
lang.pl.display=Polski
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ class ServerUtilsTest {
|
|||
void tearDown() throws IOException, InterruptedException {
|
||||
// Not applicable in pipeline testing
|
||||
Assumptions.assumeTrue(dv.isServerAvailable(), "Server not available");
|
||||
dv.getRecipes().stream().map(Recipe::getId).forEach(id -> {
|
||||
dv.getRecipes(List.of()).stream().map(Recipe::getId).forEach(id -> {
|
||||
try {
|
||||
dv.deleteRecipe(id);
|
||||
} catch (Exception ex) {
|
||||
|
|
@ -77,7 +77,7 @@ class ServerUtilsTest {
|
|||
|
||||
@Test
|
||||
void getAllRecipesTest() throws IOException, InterruptedException {
|
||||
List<Recipe> recipes = dv.getRecipes();
|
||||
List<Recipe> recipes = dv.getRecipes(List.of());
|
||||
|
||||
assertNotNull(recipes, "The list should not be null");
|
||||
assertTrue(recipes.size() >= 0, "The list should be 0 (when no recipes), or more");
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ public class PrintExportTest {
|
|||
List<String> preparationSteps = new ArrayList<>();
|
||||
preparationSteps.add("Mix Ingredients");
|
||||
preparationSteps.add("Heat in Oven");
|
||||
Recipe recipe1 = new Recipe(testRecipeId, "Banana Bread", ingredients, preparationSteps);
|
||||
Recipe recipe1 = new Recipe(testRecipeId, "Banana Bread", "en", ingredients, preparationSteps);
|
||||
|
||||
assertEquals("""
|
||||
Title: Banana Bread
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue