diff --git a/pom.xml b/pom.xml index 43ff4d1..3d5d903 100644 --- a/pom.xml +++ b/pom.xml @@ -1,5 +1,5 @@ - 4.0.0 @@ -78,11 +78,6 @@ com.h2database h2 - - org.springframework.boot - spring-boot-starter-test - test - org.flywaydb @@ -101,6 +96,38 @@ sonar-maven-plugin 3.9.1.2184 + + + + + org.springframework.boot + spring-boot-starter-test + 3.0.3 + test + + + + org.testfx + testfx-core + 4.0.16-alpha + test + + + + org.testfx + testfx-junit5 + 4.0.16-alpha + test + + + + org.testfx + openjfx-monocle + jdk-11+26 + test + + + org.hamcrest hamcrest-library @@ -172,6 +199,20 @@ false + + org.apache.maven.plugins + maven-surefire-plugin + 3.0.0-M9 + + + + glass + Monocle + Headless + true + + + - \ No newline at end of file + diff --git a/src/main/java/de/doubleslash/keeptask/common/Resources.java b/src/main/java/de/doubleslash/keeptask/common/Resources.java index 5928114..609c894 100644 --- a/src/main/java/de/doubleslash/keeptask/common/Resources.java +++ b/src/main/java/de/doubleslash/keeptask/common/Resources.java @@ -24,6 +24,10 @@ private Resources() { throw new IllegalStateException("Utility class"); } + public static URL getResource(final RESOURCE resource) { + return Resources.class.getResource(resource.getResourceLocation()); + } + public enum RESOURCE { /** * FONTS @@ -35,16 +39,16 @@ public enum RESOURCE { * LAYOUTS **/ // main - FXML_VIEW_LAYOUT("/layouts/ViewLayout.fxml"), + FXML_VIEW_LAYOUT("/layouts/MainWindowLayout.fxml"), FXML_EDIT_WORKITEM_LAYOUT("/layouts/EditWorkItemDialog.fxml"), FXML_FILTER_LAYOUT("/layouts/FiltersLayout.fxml"), + FXML_SORTING_LAYOUT("/layouts/SortingLayout.fxml"), SVG_TRASH_ICON("/svgs/trash-can.svg"), SVG_PENCIL_ICON("/svgs/pencil.svg"), - ICON_MAIN("/icons/icon.png") - ; + ICON_MAIN("/icons/icon.png"); String resourceLocation; @@ -56,8 +60,4 @@ public String getResourceLocation() { return resourceLocation; } } - - public static URL getResource(final RESOURCE resource) { - return Resources.class.getResource(resource.getResourceLocation()); - } } diff --git a/src/main/java/de/doubleslash/keeptask/controller/Controller.java b/src/main/java/de/doubleslash/keeptask/controller/Controller.java index fa966ae..4c56084 100644 --- a/src/main/java/de/doubleslash/keeptask/controller/Controller.java +++ b/src/main/java/de/doubleslash/keeptask/controller/Controller.java @@ -16,12 +16,7 @@ package de.doubleslash.keeptask.controller; -import java.time.LocalDateTime; -import java.util.List; -import java.util.function.Predicate; - -import javax.annotation.PreDestroy; - +import de.doubleslash.keeptask.model.Model; import de.doubleslash.keeptask.model.WorkItem; import de.doubleslash.keeptask.model.repos.WorkItemRepository; import org.slf4j.Logger; @@ -29,7 +24,9 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; -import de.doubleslash.keeptask.model.Model; +import javax.annotation.PreDestroy; +import java.time.LocalDateTime; +import java.util.List; @Service public class Controller { @@ -95,11 +92,6 @@ public void shutdown() { LOG.info("Controller shutdown"); } - public void setFilterPredicate(Predicate filterPredicate) { - LOG.debug("Filters were changed"); - model.getWorkFilteredList().setPredicate(filterPredicate); - } - public void setLatestSelectedProject(String projectName) { model.setLatestSelectedProject(projectName); } diff --git a/src/main/java/de/doubleslash/keeptask/controller/SortingController.java b/src/main/java/de/doubleslash/keeptask/controller/SortingController.java new file mode 100644 index 0000000..0071608 --- /dev/null +++ b/src/main/java/de/doubleslash/keeptask/controller/SortingController.java @@ -0,0 +1,93 @@ +package de.doubleslash.keeptask.controller; + +import de.doubleslash.keeptask.model.Sorting.DueDate; +import de.doubleslash.keeptask.model.Sorting.Priority; +import de.doubleslash.keeptask.model.Sorting.SortingCriteria; +import de.doubleslash.keeptask.model.Sorting.SortingDirection; +import de.doubleslash.keeptask.model.WorkItem; +import javafx.collections.FXCollections; +import javafx.collections.ListChangeListener; +import javafx.collections.ObservableList; +import javafx.collections.transformation.SortedList; + +import java.util.Arrays; +import java.util.Comparator; +import java.util.List; +import java.util.stream.Collectors; + +public class SortingController { + final ObservableList selectedSortingCriteriaList = FXCollections.observableArrayList(); + private List possibleSortingCriteriaList; + private SortedList sortedWorkItems; + + public SortingController() { + possibleSortingCriteriaList = Arrays.asList( + new Priority(), + new DueDate() + ); + selectedSortingCriteriaList.addListener((ListChangeListener) change -> updateComparator()); + } + + private Comparator getSortingComparator() { + Comparator comparator = null; + SortingCriteria sortingCriteria; + + if (selectedSortingCriteriaList.size() > 0) { + sortingCriteria = selectedSortingCriteriaList.get(0); + comparator = Comparator.comparing(sortingCriteria.getOrderFunction(), Comparator.nullsLast(getComparatorBySortingDirection(sortingCriteria.getSortingDirection()))); + } + + for (int i = 1; i < selectedSortingCriteriaList.size(); i++) { + sortingCriteria = selectedSortingCriteriaList.get(i); + comparator = comparator.thenComparing(sortingCriteria.getOrderFunction(), Comparator.nullsLast(getComparatorBySortingDirection(sortingCriteria.getSortingDirection()))); + } + return comparator; + } + + private Comparator getComparatorBySortingDirection(final SortingDirection sortingDirection) { + Comparator comparator = null; + + switch (sortingDirection) { + case Ascending: + comparator = Comparator.naturalOrder(); + break; + + case Descending: + comparator = Comparator.reverseOrder(); + break; + + default: + throw new RuntimeException("The selected sorting direction could not be mapped to a comparator."); + } + + return comparator; + } + + private void updateComparator() { + Comparator comparator = getSortingComparator(); + sortedWorkItems.setComparator(comparator); + } + + public void setWorkItemsToSort(ObservableList workItemsToSort) { + sortedWorkItems = new SortedList<>(workItemsToSort); + updateComparator(); + } + + public SortedList getSortedWorkItems() { + return sortedWorkItems; + } + + public void addSortingCriteriaByString(String sortingCriteriaString) { + SortingCriteria foundSortingCriteria = possibleSortingCriteriaList.stream().filter(sortingCriteria -> sortingCriteria.getName() == sortingCriteriaString).findFirst().get(); + selectedSortingCriteriaList.add(foundSortingCriteria); + } + + public void removeSortingCriteriaByString(String sortingCriteriaString) { + SortingCriteria foundSortingCriteria = possibleSortingCriteriaList.stream().filter(sortingCriteria -> sortingCriteria.getName() == sortingCriteriaString).findFirst().get(); + selectedSortingCriteriaList.remove(foundSortingCriteria); + } + + public List getSortingCriterias() { + return possibleSortingCriteriaList.stream().map(SortingCriteria::getName).collect(Collectors.toList()); + } +} diff --git a/src/main/java/de/doubleslash/keeptask/model/Model.java b/src/main/java/de/doubleslash/keeptask/model/Model.java index 584986f..5d9ab01 100644 --- a/src/main/java/de/doubleslash/keeptask/model/Model.java +++ b/src/main/java/de/doubleslash/keeptask/model/Model.java @@ -16,35 +16,26 @@ package de.doubleslash.keeptask.model; -import de.doubleslash.keeptask.model.repos.WorkItemRepository; -import javafx.beans.InvalidationListener; import javafx.beans.property.ObjectProperty; import javafx.beans.property.SimpleObjectProperty; import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.StringProperty; -import javafx.beans.value.ChangeListener; -import javafx.beans.value.ObservableValue; import javafx.collections.FXCollections; import javafx.collections.ObservableList; -import javafx.collections.transformation.FilteredList; +import javafx.scene.paint.Color; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; -import javafx.scene.paint.Color; - import java.util.List; @Component public class Model { - public static final Color ORIGINAL_DEFAULT_BACKGROUND_COLOR = Color.WHITE; public final ObjectProperty defaultBackgroundColor = new SimpleObjectProperty<>( ORIGINAL_DEFAULT_BACKGROUND_COLOR); private ObservableList workItems = FXCollections.observableArrayList(); - private FilteredList workFilteredItems = new FilteredList(workItems); - private StringProperty latestSelectedProject = new SimpleStringProperty(); @Autowired @@ -52,21 +43,15 @@ public Model() { super(); } - public void setWorkItems(List workItems) { - this.workItems.clear(); - this.workItems.addAll(workItems); - } - public ObservableList getWorkItems() { return FXCollections.unmodifiableObservableList(workItems); } - public ObservableList getWorkFilteredItems() { - return FXCollections.unmodifiableObservableList(workFilteredItems); + public void setWorkItems(List workItems) { + this.workItems.clear(); + this.workItems.addAll(workItems); } - public FilteredList getWorkFilteredList(){return workFilteredItems;} - public StringProperty latestSelectedProjectProperty() { return latestSelectedProject; } diff --git a/src/main/java/de/doubleslash/keeptask/model/Sorting/DueDate.java b/src/main/java/de/doubleslash/keeptask/model/Sorting/DueDate.java new file mode 100644 index 0000000..36600b7 --- /dev/null +++ b/src/main/java/de/doubleslash/keeptask/model/Sorting/DueDate.java @@ -0,0 +1,17 @@ +package de.doubleslash.keeptask.model.Sorting; + +import de.doubleslash.keeptask.model.WorkItem; + +import java.util.function.Function; + +public class DueDate extends SortingCriteria { + public DueDate() { + name = "Due Date"; + sortingDirection = SortingDirection.Ascending; + } + + @Override + public Function getOrderFunction() { + return WorkItem::getDueDateTime; + } +} diff --git a/src/main/java/de/doubleslash/keeptask/model/Sorting/Priority.java b/src/main/java/de/doubleslash/keeptask/model/Sorting/Priority.java new file mode 100644 index 0000000..6358f3d --- /dev/null +++ b/src/main/java/de/doubleslash/keeptask/model/Sorting/Priority.java @@ -0,0 +1,17 @@ +package de.doubleslash.keeptask.model.Sorting; + +import de.doubleslash.keeptask.model.WorkItem; + +import java.util.function.Function; + +public class Priority extends SortingCriteria { + public Priority() { + name = "Priority"; + sortingDirection = SortingDirection.Descending; + } + + @Override + public Function getOrderFunction() { + return WorkItem::getPriority; + } +} diff --git a/src/main/java/de/doubleslash/keeptask/model/Sorting/SortingCriteria.java b/src/main/java/de/doubleslash/keeptask/model/Sorting/SortingCriteria.java new file mode 100644 index 0000000..fee0e7f --- /dev/null +++ b/src/main/java/de/doubleslash/keeptask/model/Sorting/SortingCriteria.java @@ -0,0 +1,24 @@ +package de.doubleslash.keeptask.model.Sorting; + +import de.doubleslash.keeptask.model.WorkItem; + +import java.util.function.Function; + +public abstract class SortingCriteria { + protected String name = null; + protected SortingDirection sortingDirection = SortingDirection.Ascending; + + public String getName() { + return name; + } + + public SortingDirection getSortingDirection() { + return sortingDirection; + } + + public void setSortingDirection(SortingDirection sortingDirection) { + this.sortingDirection = sortingDirection; + } + + public abstract Function getOrderFunction(); +} diff --git a/src/main/java/de/doubleslash/keeptask/model/Sorting/SortingDirection.java b/src/main/java/de/doubleslash/keeptask/model/Sorting/SortingDirection.java new file mode 100644 index 0000000..a05d49f --- /dev/null +++ b/src/main/java/de/doubleslash/keeptask/model/Sorting/SortingDirection.java @@ -0,0 +1,5 @@ +package de.doubleslash.keeptask.model.Sorting; + +public enum SortingDirection { + Ascending, Descending +} diff --git a/src/main/java/de/doubleslash/keeptask/model/WorkItem.java b/src/main/java/de/doubleslash/keeptask/model/WorkItem.java index ce59c7b..8a13413 100644 --- a/src/main/java/de/doubleslash/keeptask/model/WorkItem.java +++ b/src/main/java/de/doubleslash/keeptask/model/WorkItem.java @@ -13,7 +13,9 @@ public class WorkItem { private long id; private String project; - private String priority; + + @Enumerated(EnumType.STRING) + private Priority priority; private String todo; private LocalDateTime createdDateTime; private LocalDateTime dueDateTime; @@ -21,38 +23,11 @@ public class WorkItem { private boolean finished; private String note; - public void setProject(String project) { - this.project = project; - } - - public void setTodo(String todo) { - this.todo = todo; - } - - public void setCreatedDateTime(LocalDateTime createdDateTime) { - this.createdDateTime = createdDateTime; - } - - public void setDueDateTime(LocalDateTime dueDateTime) { - this.dueDateTime = dueDateTime; - } - - public void setCompletedDateTime(LocalDateTime completedDateTime) { - this.completedDateTime = completedDateTime; - } - - public void setFinished(boolean finished) { - this.finished = finished; - } - - public void setNote(String note) { - this.note = note; - } - - public WorkItem(){ + public WorkItem() { // needed for hibernate } - public WorkItem(String project, String priority, String todo, LocalDateTime createdDateTime, LocalDateTime dueDateTime, LocalDateTime completedDateTime, boolean finished, String note) { + + public WorkItem(String project, Priority priority, String todo, LocalDateTime createdDateTime, LocalDateTime dueDateTime, LocalDateTime completedDateTime, boolean finished, String note) { this.project = project; this.priority = priority; this.todo = todo; @@ -67,42 +42,75 @@ public long getId() { return id; } + public void setId(long id) { + this.id = id; + } + public String getProject() { return project; } + public void setProject(String project) { + this.project = project; + } + public String getTodo() { return todo; } + public void setTodo(String todo) { + this.todo = todo; + } + public LocalDateTime getCreatedDateTime() { return createdDateTime; } + public void setCreatedDateTime(LocalDateTime createdDateTime) { + this.createdDateTime = createdDateTime; + } + public LocalDateTime getDueDateTime() { return dueDateTime; } + public void setDueDateTime(LocalDateTime dueDateTime) { + this.dueDateTime = dueDateTime; + } + public LocalDateTime getCompletedDateTime() { return completedDateTime; } + public void setCompletedDateTime(LocalDateTime completedDateTime) { + this.completedDateTime = completedDateTime; + } + public boolean isFinished() { return finished; } + public void setFinished(boolean finished) { + this.finished = finished; + } + public String getNote() { return note; } - public String getPriority() { + + public void setNote(String note) { + this.note = note; + } + + public Priority getPriority() { return priority; } - public void setPriority(String priority) { + public void setPriority(Priority priority) { this.priority = priority; } - public void setId(long id) { - this.id = id; + public enum Priority { + Low, Medium, High } } diff --git a/src/main/java/de/doubleslash/keeptask/model/WorkItemBuilder.java b/src/main/java/de/doubleslash/keeptask/model/WorkItemBuilder.java new file mode 100644 index 0000000..4d2e614 --- /dev/null +++ b/src/main/java/de/doubleslash/keeptask/model/WorkItemBuilder.java @@ -0,0 +1,58 @@ +package de.doubleslash.keeptask.model; + +import java.time.LocalDateTime; + +public class WorkItemBuilder { + private String project; + private WorkItem.Priority priority; + private String todo; + private LocalDateTime createdDateTime; + private LocalDateTime dueDateTime; + private LocalDateTime completedDateTime; + private boolean finished; + private String note; + + public WorkItemBuilder setProject(String project) { + this.project = project; + return this; + } + + public WorkItemBuilder setPriority(WorkItem.Priority priority) { + this.priority = priority; + return this; + } + + public WorkItemBuilder setTodo(String todo) { + this.todo = todo; + return this; + } + + public WorkItemBuilder setCreatedDateTime(LocalDateTime createdDateTime) { + this.createdDateTime = createdDateTime; + return this; + } + + public WorkItemBuilder setDueDateTime(LocalDateTime dueDateTime) { + this.dueDateTime = dueDateTime; + return this; + } + + public WorkItemBuilder setCompletedDateTime(LocalDateTime completedDateTime) { + this.completedDateTime = completedDateTime; + return this; + } + + public WorkItemBuilder setFinished(boolean finished) { + this.finished = finished; + return this; + } + + public WorkItemBuilder setNote(String note) { + this.note = note; + return this; + } + + public WorkItem createWorkItem() { + return new WorkItem(project, priority, todo, createdDateTime, dueDateTime, completedDateTime, finished, note); + } +} \ No newline at end of file diff --git a/src/main/java/de/doubleslash/keeptask/view/EditWorkItemController.java b/src/main/java/de/doubleslash/keeptask/view/EditWorkItemController.java index 7c408ae..6824124 100644 --- a/src/main/java/de/doubleslash/keeptask/view/EditWorkItemController.java +++ b/src/main/java/de/doubleslash/keeptask/view/EditWorkItemController.java @@ -49,7 +49,7 @@ public void initializeWith(final WorkItem workItem) { todoTextInput.setText(workItem.getTodo()); if (workItem.getDueDateTime() != null) dueDateDatePicker.setValue(workItem.getDueDateTime().toLocalDate()); - priorityTextInput.setText(workItem.getPriority()); + priorityTextInput.setText(workItem.getPriority().toString()); projectTextInput.setText(workItem.getProject()); if (workItem.getCreatedDateTime() != null) createdDateDatePicker.setValue(workItem.getCreatedDateTime().toLocalDate()); @@ -64,7 +64,7 @@ public WorkItem getWorkItemFromUserInput() { workItem.setTodo(todoTextInput.getText()); if (dueDateDatePicker.getValue() != null) workItem.setDueDateTime(dueDateDatePicker.getValue().atStartOfDay()); - workItem.setPriority(priorityTextInput.getText()); + workItem.setPriority(WorkItem.Priority.valueOf(priorityTextInput.getText())); workItem.setProject(projectTextInput.getText()); if (createdDateDatePicker.getValue() != null) workItem.setCreatedDateTime(createdDateDatePicker.getValue().atStartOfDay()); diff --git a/src/main/java/de/doubleslash/keeptask/view/FilterController.java b/src/main/java/de/doubleslash/keeptask/view/FilterController.java index 2def301..36b4eb7 100644 --- a/src/main/java/de/doubleslash/keeptask/view/FilterController.java +++ b/src/main/java/de/doubleslash/keeptask/view/FilterController.java @@ -4,6 +4,7 @@ import de.doubleslash.keeptask.model.Model; import de.doubleslash.keeptask.model.WorkItem; import javafx.collections.ListChangeListener; +import javafx.collections.transformation.FilteredList; import javafx.event.ActionEvent; import javafx.event.EventHandler; import javafx.fxml.FXML; @@ -20,35 +21,28 @@ import java.util.Set; import java.util.function.Predicate; import java.util.stream.Collectors; -import java.util.stream.Stream; @Component public class FilterController { + private final Model model; + private final Controller controller; + private final List> timeFilters = new ArrayList<>(); + private final List projectNameFilters = new ArrayList<>(); @FXML private TextField searchTextInput; - @FXML private ToggleButton todayToggleButton; - @FXML private ToggleButton tomorrowToggleButton; - @FXML private ToggleButton expiredToggleButton; - @FXML private CheckBox alsoCompletedCheckbox; - @FXML private HBox projectFilterHbox; - private final Model model; - private final Controller controller; - - private final List> timeFilters = new ArrayList<>(); - private final List projectNameFilters = new ArrayList<>(); - + private FilteredList filteredWorkItems; @Autowired public FilterController(final Model model, final Controller controller) { @@ -56,8 +50,14 @@ public FilterController(final Model model, final Controller controller) { this.controller = controller; } + public FilteredList getFilteredWorkItems() { + return filteredWorkItems; + } + @FXML private void initialize() { + filteredWorkItems = new FilteredList<>(model.getWorkItems()); + model.getWorkItems().addListener((ListChangeListener) change -> { updateProjectFilterButtons(); }); @@ -124,9 +124,10 @@ private void updateProjectFilterButtons() { private void updateFilters() { Predicate filterPredicate = generateFilterPredicate(); - controller.setFilterPredicate(filterPredicate); + filteredWorkItems.setPredicate(filterPredicate); } + private Predicate generateFilterPredicate() { Predicate filterPredicate = (workItem) -> { boolean timeFilterMatches = timeFilters.isEmpty() ? true : timeFilters.stream().reduce(x -> false, Predicate::or).test(workItem); diff --git a/src/main/java/de/doubleslash/keeptask/view/MainWindowController.java b/src/main/java/de/doubleslash/keeptask/view/MainWindowController.java index b0cbb31..5fe392a 100644 --- a/src/main/java/de/doubleslash/keeptask/view/MainWindowController.java +++ b/src/main/java/de/doubleslash/keeptask/view/MainWindowController.java @@ -17,35 +17,40 @@ package de.doubleslash.keeptask.view; import de.doubleslash.keeptask.common.*; +import de.doubleslash.keeptask.controller.Controller; +import de.doubleslash.keeptask.controller.SortingController; import de.doubleslash.keeptask.exceptions.FXMLLoaderException; +import de.doubleslash.keeptask.model.Model; import de.doubleslash.keeptask.model.TodoPart; import de.doubleslash.keeptask.model.WorkItem; +import de.doubleslash.keeptask.model.WorkItemBuilder; +import javafx.collections.FXCollections; import javafx.collections.ListChangeListener; import javafx.collections.ObservableList; -import javafx.collections.transformation.SortedList; +import javafx.fxml.FXML; import javafx.fxml.FXMLLoader; import javafx.scene.Node; import javafx.scene.control.*; import javafx.scene.image.Image; import javafx.scene.layout.GridPane; import javafx.scene.layout.HBox; +import javafx.scene.layout.Pane; import javafx.scene.layout.VBox; +import javafx.scene.paint.Color; +import javafx.stage.Stage; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; -import de.doubleslash.keeptask.controller.Controller; -import de.doubleslash.keeptask.model.Model; -import javafx.fxml.FXML; -import javafx.scene.layout.Pane; -import javafx.scene.paint.Color; -import javafx.stage.Stage; - import java.io.IOException; import java.time.LocalDate; import java.time.LocalDateTime; -import java.util.*; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + @Component public class MainWindowController { @@ -53,46 +58,35 @@ public class MainWindowController { private final Model model; private final Controller controller; - + private FilterController filterController; + private SortingController sortingController; private Point dragDelta = new Point(0, 0); private Stage mainStage; - @FXML private Pane pane; - @FXML private Button minimizeButton; - @FXML private Button closeButton; - - @FXML private VBox filterVBox; - // TODO extract new ToDo-section into own controller @FXML - private TextField prioTextInput; - + private ComboBox prioComboBox; @FXML private TextField projectTextInput; - @FXML private TextField todoTextInput; - @FXML private DatePicker dueDatePicker; - @FXML private Button addTodoButton; - + @FXML + private VBox sortingVBox; // TODO extract TODO ListView to own controller @FXML private VBox workItemVBox; - SortedList sortedWorkItems; - - @Autowired public MainWindowController(final Model model, final Controller controller) { this.model = model; @@ -101,14 +95,9 @@ public MainWindowController(final Model model, final Controller controller) { @FXML private void initialize() { - // TODO make sorting configurable - sortedWorkItems = new SortedList<>(model.getWorkFilteredItems()); - Comparator comparing = Comparator.comparing(workItem -> workItem.getDueDateTime() != null ? workItem.getDueDateTime() : LocalDateTime.MIN); - comparing = comparing.reversed(); - sortedWorkItems.setComparator(comparing); - - + prioComboBox.setItems(FXCollections.observableArrayList(Arrays.stream(WorkItem.Priority.values()).map(priority -> priority.toString()).collect(Collectors.toList()))); loadFiltersLayout(); + loadSortingLayout(); closeButton.setOnAction(ae -> openConfirmationWindow()); minimizeButton.setOnAction(ae -> mainStage.setIconified(true)); @@ -135,14 +124,16 @@ private void initialize() { if (dueDate != null) { dueDateTime = dueDate.atStartOfDay(); } - WorkItem newItem = new WorkItem(projectTextInput.getText(), prioTextInput.getText(), todoTextInput.getText(), LocalDateTime.now(), dueDateTime, null, false, ""); + WorkItem newItem = new WorkItemBuilder().setProject(projectTextInput.getText()).setPriority(WorkItem.Priority.valueOf(prioComboBox.getSelectionModel().getSelectedItem())).setTodo(todoTextInput.getText()).setCreatedDateTime(LocalDateTime.now()).setDueDateTime(dueDateTime).setCompletedDateTime(null).setFinished(false).setNote("").createWorkItem(); controller.addWorkItem(newItem); todoTextInput.clear(); dueDatePicker.setValue(null); }); - model.getWorkFilteredItems().addListener((ListChangeListener) change -> { + model.getWorkItems().addListener((ListChangeListener) change -> { + if (!change.next()) return; + refreshTodos(); }); @@ -164,20 +155,42 @@ private void loadFiltersLayout() { throw new RuntimeException(e); } this.filterVBox.getChildren().addAll(filterVBox.getChildren()); // TODO losses original vbox attributes - FilterController filterController = loader.getController(); + filterController = loader.getController(); + } + + private void loadSortingLayout() { + final FXMLLoader loader = FxmlLayout.createLoaderFor(Resources.RESOURCE.FXML_SORTING_LAYOUT); + + final VBox sortingVBox; + try { + sortingVBox = loader.load(); + } catch (IOException e) { + LOG.error("Could not load sorting layout", e); + throw new RuntimeException(e); + } + this.sortingVBox.getChildren().addAll(sortingVBox.getChildren()); + SortingViewController sortingViewController = loader.getController(); + sortingController = sortingViewController.getSortingController(); + sortingController.setWorkItemsToSort(filterController.getFilteredWorkItems()); + sortingController.getSortedWorkItems().addListener((ListChangeListener) change -> { + if (!change.next()) return; + + refreshTodos(); + }); } private void refreshTodos() { ObservableList children = workItemVBox.getChildren(); children.clear(); - for (WorkItem workItem : sortedWorkItems) { + List sortedFilteredItems = sortingController.getSortedWorkItems(); + + for (WorkItem workItem : sortedFilteredItems) { Node todoNode = createTodoNode(workItem); children.add(todoNode); } - if (mainStage != null) - mainStage.sizeToScene(); + if (mainStage != null) mainStage.sizeToScene(); } private Node createTodoNode(WorkItem workItem) { @@ -194,13 +207,10 @@ private Node createTodoNode(WorkItem workItem) { hbox2.setSpacing(10); hbox2.disableProperty().bind(completedCheckBox.selectedProperty()); ObservableList children1 = hbox2.getChildren(); - Label prioLabel = new Label(workItem.getPriority()); - if (workItem.getPriority().equalsIgnoreCase("High")) - prioLabel.setTextFill(Color.RED); - if (workItem.getPriority().equalsIgnoreCase("Medium")) - prioLabel.setTextFill(Color.ORANGE); - if (!workItem.getPriority().isEmpty()) - children1.add(prioLabel); + Label prioLabel = new Label(workItem.getPriority().toString()); + if (workItem.getPriority() == WorkItem.Priority.High) prioLabel.setTextFill(Color.RED); + if (workItem.getPriority() == WorkItem.Priority.Medium) prioLabel.setTextFill(Color.ORANGE); + if (!workItem.getPriority().toString().isEmpty()) children1.add(prioLabel); Label projectLabel = new Label(workItem.getProject()); children1.add(projectLabel); @@ -224,12 +234,10 @@ private Node createTodoNode(WorkItem workItem) { children1.add(todoHbox); Label dueDateTimeLabel = new Label("Due: " + workItem.getDueDateTime()); - if (workItem.getDueDateTime() != null) - children1.add(dueDateTimeLabel); + if (workItem.getDueDateTime() != null) children1.add(dueDateTimeLabel); Label noteLabel = new Label("Note: " + workItem.getNote()); - if (!workItem.getNote().isEmpty()) - children1.add(noteLabel); + if (!workItem.getNote().isEmpty()) children1.add(noteLabel); Label completedDateTimeLabel = new Label("Completed: " + workItem.getCompletedDateTime()); if (workItem.isFinished()) // TODO this is only working on rerender @@ -237,16 +245,14 @@ private Node createTodoNode(WorkItem workItem) { children.add(hbox2); - Button editButton = new Button("", - SvgNodeProvider.getSvgNodeWithScale(Resources.RESOURCE.SVG_PENCIL_ICON, 0.03, 0.03)); + Button editButton = new Button("", SvgNodeProvider.getSvgNodeWithScale(Resources.RESOURCE.SVG_PENCIL_ICON, 0.03, 0.03)); editButton.setMaxSize(20, 18); editButton.setMinSize(20, 18); editButton.setContentDisplay(ContentDisplay.GRAPHIC_ONLY); children.add(editButton); editButton.setOnAction((actionEvent) -> editTodoClicked(workItem)); - Button deleteButton = new Button("", - SvgNodeProvider.getSvgNodeWithScale(Resources.RESOURCE.SVG_TRASH_ICON, 0.03, 0.03)); + Button deleteButton = new Button("", SvgNodeProvider.getSvgNodeWithScale(Resources.RESOURCE.SVG_TRASH_ICON, 0.03, 0.03)); deleteButton.setMaxSize(20, 18); deleteButton.setMinSize(20, 18); deleteButton.setContentDisplay(ContentDisplay.GRAPHIC_ONLY); @@ -305,8 +311,7 @@ private void editTodoClicked(WorkItem workItem) { private void runUpdateMainBackgroundColor() { Color color = model.defaultBackgroundColor.get(); - String style = StyleUtils.changeStyleAttribute(pane.getStyle(), "fx-background-color", - "rgba(" + ColorHelper.colorToCssRgba(color) + ")"); + String style = StyleUtils.changeStyleAttribute(pane.getStyle(), "fx-background-color", "rgba(" + ColorHelper.colorToCssRgba(color) + ")"); pane.setStyle(style); } @@ -330,5 +335,4 @@ public void setMainStage(Stage mainStage) { this.mainStage = mainStage; mainStage.sizeToScene(); } - } diff --git a/src/main/java/de/doubleslash/keeptask/view/SortingViewController.java b/src/main/java/de/doubleslash/keeptask/view/SortingViewController.java new file mode 100644 index 0000000..9e4032d --- /dev/null +++ b/src/main/java/de/doubleslash/keeptask/view/SortingViewController.java @@ -0,0 +1,60 @@ +package de.doubleslash.keeptask.view; + +import de.doubleslash.keeptask.controller.SortingController; +import javafx.collections.FXCollections; +import javafx.fxml.FXML; +import javafx.scene.control.Button; +import javafx.scene.control.ComboBox; +import javafx.scene.control.Tooltip; +import javafx.scene.layout.HBox; +import org.springframework.stereotype.Component; + +@Component +public class SortingViewController { + private SortingController sortingController; + @FXML + private HBox sortingCriteriaHBox; + @FXML + private ComboBox addSortingCriteriaCbx; + + SortingController getSortingController() { + return sortingController; + } + + HBox getSortingCriteriaHBox() { + return sortingCriteriaHBox; + } + + ComboBox getAddSortingCriteriaCbx() { + return addSortingCriteriaCbx; + } + + @FXML + private void initialize() { + sortingController = new SortingController(); + + addSortingCriteriaCbx.setItems(FXCollections.observableArrayList(sortingController.getSortingCriterias())); + addSortingCriteriaCbx.getSelectionModel().selectedItemProperty().addListener((observable, oldValue, newValue) -> { + if (newValue == null) return; + + sortingController.addSortingCriteriaByString(newValue.toString()); + addSortingCriteriaButton(newValue.toString()); + addSortingCriteriaCbx.getSelectionModel().clearSelection(); + addSortingCriteriaCbx.getItems().remove(newValue); + }); + } + + private void addSortingCriteriaButton(String sortingCriteria) { + Button sortingCriteriaButton = new Button(); + sortingCriteriaButton.setText(sortingCriteria); + Tooltip tooltip = new Tooltip(); + tooltip.setText("Click to remove " + sortingCriteria + " sorting criteria"); + sortingCriteriaButton.setTooltip(tooltip); + sortingCriteriaButton.setOnAction(event -> { + sortingController.removeSortingCriteriaByString(sortingCriteria); + addSortingCriteriaCbx.getItems().add(sortingCriteria); + sortingCriteriaHBox.getChildren().remove(sortingCriteriaButton); + }); + sortingCriteriaHBox.getChildren().add(sortingCriteriaHBox.getChildren().size() - 1, sortingCriteriaButton); + } +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 33323bd..f5a9bcb 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -21,5 +21,6 @@ spring.datasource.driver-class-name=org.h2.Driver spring.flyway.baselineOnMigrate=true spring.flyway.baselineVersion=0.0.0 +spring.flyway.validate-migration-naming=true -spring.jpa.hibernate.ddl-auto=update \ No newline at end of file +spring.jpa.hibernate.ddl-auto=update diff --git a/src/main/resources/db/migration/V2023_03_03_14_00__11-change_priority_data_type.sql b/src/main/resources/db/migration/V2023_03_03_14_00__11-change_priority_data_type.sql new file mode 100644 index 0000000..8339087 --- /dev/null +++ b/src/main/resources/db/migration/V2023_03_03_14_00__11-change_priority_data_type.sql @@ -0,0 +1 @@ +UPDATE WORK_ITEM SET PRIORITY='Low' WHERE PRIORITY<>'Low' AND PRIORITY<>'Medium' AND PRIORITY<>'High'; diff --git a/src/main/resources/layouts/ViewLayout.fxml b/src/main/resources/layouts/MainWindowLayout.fxml similarity index 59% rename from src/main/resources/layouts/ViewLayout.fxml rename to src/main/resources/layouts/MainWindowLayout.fxml index 5d4bdaf..3cfb070 100644 --- a/src/main/resources/layouts/ViewLayout.fxml +++ b/src/main/resources/layouts/MainWindowLayout.fxml @@ -1,14 +1,9 @@ - - - - - - - + + - + @@ -24,7 +19,7 @@ + diff --git a/src/main/resources/layouts/SortingLayout.fxml b/src/main/resources/layouts/SortingLayout.fxml new file mode 100644 index 0000000..99d0d37 --- /dev/null +++ b/src/main/resources/layouts/SortingLayout.fxml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/test/java/de/doubleslash/keeptask/TestUtils.java b/src/test/java/de/doubleslash/keeptask/TestUtils.java new file mode 100644 index 0000000..5211733 --- /dev/null +++ b/src/test/java/de/doubleslash/keeptask/TestUtils.java @@ -0,0 +1,14 @@ +package de.doubleslash.keeptask; + +import de.doubleslash.keeptask.model.WorkItem; + +import java.util.ArrayList; +import java.util.Collections; + +public class TestUtils { + public static ArrayList getArrayListReverted(ArrayList arrayListToRevert) { + ArrayList revertedArrayList = (ArrayList) arrayListToRevert.clone(); + Collections.reverse(revertedArrayList); + return revertedArrayList; + } +} diff --git a/src/test/java/de/doubleslash/keeptask/controller/SortingControllerTest.java b/src/test/java/de/doubleslash/keeptask/controller/SortingControllerTest.java new file mode 100644 index 0000000..7e2abfe --- /dev/null +++ b/src/test/java/de/doubleslash/keeptask/controller/SortingControllerTest.java @@ -0,0 +1,174 @@ +package de.doubleslash.keeptask.controller; + +import de.doubleslash.keeptask.model.Sorting.DueDate; +import de.doubleslash.keeptask.model.Sorting.Priority; +import de.doubleslash.keeptask.model.Sorting.SortingCriteria; +import de.doubleslash.keeptask.model.Sorting.SortingDirection; +import de.doubleslash.keeptask.model.WorkItem; +import de.doubleslash.keeptask.model.WorkItemBuilder; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; + +import static de.doubleslash.keeptask.TestUtils.getArrayListReverted; +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +class SortingControllerTest { + private SortingController sortingController; + + @BeforeEach + void setup() { + sortingController = new SortingController(); + } + + @Test + void shouldSortWorkItemsDescendingByPriorityByDefaultWhenSettingPriorityAsSortingCriteria() { + ArrayList expectedSortedWorkItems = new ArrayList<>(List.of( + new WorkItemBuilder().setPriority(WorkItem.Priority.High).createWorkItem(), + new WorkItemBuilder().setPriority(WorkItem.Priority.Medium).createWorkItem(), + new WorkItemBuilder().setPriority(WorkItem.Priority.Low).createWorkItem() + )); + + // GIVEN + ObservableList workItemsToBeSorted = FXCollections.observableArrayList(getArrayListReverted(expectedSortedWorkItems)); + sortingController.setWorkItemsToSort(workItemsToBeSorted); + + // WHEN + sortingController.selectedSortingCriteriaList.add(new Priority()); + + // THEN + assertThat(sortingController.getSortedWorkItems()).isEqualTo(expectedSortedWorkItems); + } + + @Test + void shouldSortWorkItemsCorrectlyByDueDateWhenSettingDueDateAsSortingCriteria() { + ArrayList expectedSortedWorkItems = new ArrayList<>(List.of( + new WorkItemBuilder().setDueDateTime(LocalDateTime.now().plusDays(1)).createWorkItem(), + new WorkItemBuilder().setDueDateTime(LocalDateTime.now().plusDays(2)).createWorkItem(), + new WorkItemBuilder().setDueDateTime(LocalDateTime.now().plusDays(3)).createWorkItem() + )); + + // GIVEN + ObservableList workItemsToBeSorted = FXCollections.observableArrayList(getArrayListReverted(expectedSortedWorkItems)); + sortingController.setWorkItemsToSort(workItemsToBeSorted); + + // WHEN + sortingController.selectedSortingCriteriaList.add(new DueDate()); + + // THEN + assertThat(sortingController.getSortedWorkItems()).isEqualTo(expectedSortedWorkItems); + } + + @Test + void shouldPutWorkItemToBottomWhenUsedSortingCriteriaNotSetOnWorkItem() { + ArrayList expectedSortedWorkItems = new ArrayList<>(List.of( + new WorkItemBuilder().setPriority(WorkItem.Priority.High).createWorkItem(), + new WorkItemBuilder().setPriority(WorkItem.Priority.Medium).createWorkItem(), + new WorkItemBuilder().setPriority(null).createWorkItem() + )); + + // GIVEN + ObservableList workItemsToBeSorted = FXCollections.observableArrayList(getArrayListReverted(expectedSortedWorkItems)); + sortingController.setWorkItemsToSort(workItemsToBeSorted); + + // WHEN + sortingController.selectedSortingCriteriaList.add(new Priority()); + + // THEN + assertThat(sortingController.getSortedWorkItems()).isEqualTo(expectedSortedWorkItems); + } + + @Test + void shouldSortWorkItemsCorrectlyByPriorityAndDueDateWhenFirstSortingByPriorityAndThenByDueDate() { + ArrayList expectedSortedWorkItems = new ArrayList<>(List.of( + new WorkItemBuilder().setPriority(WorkItem.Priority.High).setDueDateTime(LocalDateTime.now().plusDays(1)).createWorkItem(), + new WorkItemBuilder().setPriority(WorkItem.Priority.High).setDueDateTime(LocalDateTime.now().plusDays(2)).createWorkItem(), + new WorkItemBuilder().setPriority(WorkItem.Priority.Medium).setDueDateTime(LocalDateTime.now().plusDays(1)).createWorkItem(), + new WorkItemBuilder().setPriority(WorkItem.Priority.Medium).setDueDateTime(LocalDateTime.now().plusDays(2)).createWorkItem(), + new WorkItemBuilder().setPriority(WorkItem.Priority.Low).setDueDateTime(LocalDateTime.now().plusDays(1)).createWorkItem(), + new WorkItemBuilder().setPriority(WorkItem.Priority.Low).setDueDateTime(LocalDateTime.now().plusDays(2)).createWorkItem() + )); + + // GIVEN + ObservableList workItemsToBeSorted = FXCollections.observableArrayList(getArrayListReverted(expectedSortedWorkItems)); + sortingController.setWorkItemsToSort(workItemsToBeSorted); + + // WHEN + sortingController.selectedSortingCriteriaList.add(new Priority()); + sortingController.selectedSortingCriteriaList.add(new DueDate()); + + // THEN + assertThat(sortingController.getSortedWorkItems()).isEqualTo(expectedSortedWorkItems); + } + + @Test + void shouldSortWorkItemsCorrectlyByPriorityAndDueDateWhenFirstSortingByPriorityAndThenByDueDateIncludingNullValues() { + ArrayList expectedSortedWorkItems = new ArrayList<>(List.of( + new WorkItemBuilder().setPriority(WorkItem.Priority.High).setDueDateTime(LocalDateTime.now()).createWorkItem(), + new WorkItemBuilder().setPriority(WorkItem.Priority.High).setDueDateTime(LocalDateTime.now().plusDays(1)).createWorkItem(), + new WorkItemBuilder().setPriority(WorkItem.Priority.Medium).setDueDateTime(LocalDateTime.now()).createWorkItem(), + new WorkItemBuilder().setPriority(WorkItem.Priority.Medium).setDueDateTime(LocalDateTime.now().plusDays(1)).createWorkItem(), + new WorkItemBuilder().setPriority(WorkItem.Priority.Medium).setDueDateTime(null).createWorkItem(), + new WorkItemBuilder().setPriority(WorkItem.Priority.Low).setDueDateTime(LocalDateTime.now()).createWorkItem(), + new WorkItemBuilder().setPriority(WorkItem.Priority.Low).setDueDateTime(LocalDateTime.now().plusDays(1)).createWorkItem(), + new WorkItemBuilder().setPriority(null).setDueDateTime(LocalDateTime.now()).createWorkItem(), + new WorkItemBuilder().setPriority(null).setDueDateTime(null).createWorkItem() + )); + + // GIVEN + ObservableList workItemsToBeSorted = FXCollections.observableArrayList(getArrayListReverted(expectedSortedWorkItems)); + sortingController.setWorkItemsToSort(workItemsToBeSorted); + + // WHEN + sortingController.selectedSortingCriteriaList.add(new Priority()); + sortingController.selectedSortingCriteriaList.add(new DueDate()); + + // THEN + assertThat(sortingController.getSortedWorkItems()).isEqualTo(expectedSortedWorkItems); + } + + @Test + void shouldSortWorkItemsDescendingByPriorityWhenSettingPriorityAsSortingCriteriaAndDescendingOrder() { + ArrayList expectedSortedWorkItems = new ArrayList<>(List.of( + new WorkItemBuilder().setPriority(WorkItem.Priority.High).createWorkItem(), + new WorkItemBuilder().setPriority(WorkItem.Priority.Medium).createWorkItem(), + new WorkItemBuilder().setPriority(WorkItem.Priority.Low).createWorkItem() + )); + + // GIVEN + ObservableList workItemsToBeSorted = FXCollections.observableArrayList(getArrayListReverted(expectedSortedWorkItems)); + sortingController.setWorkItemsToSort(workItemsToBeSorted); + + // WHEN + sortingController.selectedSortingCriteriaList.add(new Priority()); + + // THEN + assertThat(sortingController.getSortedWorkItems()).isEqualTo(expectedSortedWorkItems); + } + + @Test + void shouldSortWorkItemsAscendingByPriorityWhenSettingPriorityAsSortingCriteriaAndAscendingOrder() { + ArrayList expectedSortedWorkItems = new ArrayList<>(List.of( + new WorkItemBuilder().setPriority(WorkItem.Priority.Low).createWorkItem(), + new WorkItemBuilder().setPriority(WorkItem.Priority.Medium).createWorkItem(), + new WorkItemBuilder().setPriority(WorkItem.Priority.High).createWorkItem() + )); + + // GIVEN + ObservableList workItemsToBeSorted = FXCollections.observableArrayList(getArrayListReverted(expectedSortedWorkItems)); + sortingController.setWorkItemsToSort(workItemsToBeSorted); + + // WHEN + SortingCriteria priority = new Priority(); + priority.setSortingDirection(SortingDirection.Ascending); + sortingController.selectedSortingCriteriaList.add(priority); + + // THEN + assertThat(sortingController.getSortedWorkItems()).isEqualTo(expectedSortedWorkItems); + } +} diff --git a/src/test/java/de/doubleslash/keeptask/view/SortingViewControllerTest.java b/src/test/java/de/doubleslash/keeptask/view/SortingViewControllerTest.java new file mode 100644 index 0000000..d7c5229 --- /dev/null +++ b/src/test/java/de/doubleslash/keeptask/view/SortingViewControllerTest.java @@ -0,0 +1,61 @@ +package de.doubleslash.keeptask.view; + + +import de.doubleslash.keeptask.common.Resources; +import de.doubleslash.keeptask.model.WorkItem; +import de.doubleslash.keeptask.model.WorkItemBuilder; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.fxml.FXMLLoader; +import javafx.scene.Node; +import javafx.scene.control.Button; +import javafx.stage.Stage; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.testfx.framework.junit5.ApplicationExtension; +import org.testfx.framework.junit5.Start; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +import static de.doubleslash.keeptask.TestUtils.getArrayListReverted; +import static org.assertj.core.api.Assertions.assertThat; + +@ExtendWith(ApplicationExtension.class) +class SortingViewControllerTest { + SortingViewController sortingViewController; + + @Start + private void start(Stage stage) throws IOException { + final FXMLLoader loader = new FXMLLoader(); + loader.setLocation(Resources.getResource(Resources.RESOURCE.FXML_SORTING_LAYOUT)); + loader.load(); + sortingViewController = loader.getController(); + } + + @Test + void shouldAddSortingCriteriaWhenSelectedViaComboBox() { + String expectedSelectedSortingCriteria = "Priority"; + ArrayList expectedSortedWorkItems = new ArrayList<>(List.of( + new WorkItemBuilder().setPriority(WorkItem.Priority.High).createWorkItem(), + new WorkItemBuilder().setPriority(WorkItem.Priority.Medium).createWorkItem(), + new WorkItemBuilder().setPriority(WorkItem.Priority.Low).createWorkItem() + )); + + // GIVEN + ObservableList workItemsToBeSorted = FXCollections.observableArrayList(getArrayListReverted(expectedSortedWorkItems)); + sortingViewController.getSortingController().setWorkItemsToSort(workItemsToBeSorted); + + // WHEN + sortingViewController.getAddSortingCriteriaCbx().getSelectionModel().select(expectedSelectedSortingCriteria); + + // THEN + List buttonsList = sortingViewController.getSortingCriteriaHBox().getChildren().stream().filter(node -> node instanceof Button).collect(Collectors.toList()); + assertThat(buttonsList).hasSize(1); + Button button = (Button) buttonsList.get(0); + assertThat(button.getText()).isEqualTo(expectedSelectedSortingCriteria); + assertThat(sortingViewController.getSortingController().getSortedWorkItems()).isEqualTo(expectedSortedWorkItems); + } +}