Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
204 changes: 137 additions & 67 deletions src/main/java/de/doubleslash/keeptime/controller/HeimatController.java

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ public record JWTTokenAttributes(
String header,
String payload,
LocalDateTime expiration
) {}
) { }


public static JWTTokenAttributes parse(String bearerToken) {
Expand All @@ -57,6 +57,10 @@ public static JWTTokenAttributes parse(String bearerToken) {
return new JWTTokenAttributes(header, payload, expiration);
}

public static boolean isExpired(JWTTokenAttributes token, LocalDateTime localDateTimeNow) {
return token.expiration.isAfter(localDateTimeNow);
}

private static String removeBearerPrefix(String token) {
return token.startsWith("Bearer ") ? token.substring(7) : token;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
import de.doubleslash.keeptime.model.Project;
import de.doubleslash.keeptime.rest.integration.heimat.model.ExistingAndInvalidMappings;
import de.doubleslash.keeptime.rest.integration.heimat.model.HeimatTask;
import javafx.application.Platform;
import de.doubleslash.keeptime.viewpopup.SearchCombobox;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
Expand All @@ -35,6 +35,8 @@
import javafx.fxml.FXML;
import javafx.scene.control.*;
import javafx.scene.control.cell.CheckBoxTableCell;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import org.slf4j.Logger;
Expand Down Expand Up @@ -77,24 +79,43 @@ public ExternalProjectsMapController(final Model model, Controller controller, H
this.heimatController = heimatController;
}

private ExistingAndInvalidMappings existingAndInvalidMappings;
private ObservableList<HeimatController.ProjectMapping> newProjectMappings;

public void setStage(final Stage thisStage) {
this.thisStage = thisStage;
// Show invalid mappings dialog when stage is shown
thisStage.setOnShown(e -> {
if (existingAndInvalidMappings != null && newProjectMappings != null) {
List<String> warnings = existingAndInvalidMappings.invalidMappingsAsString();
if (!warnings.isEmpty()) {
if (showInvalidMappingsDialog(warnings)) {
newProjectMappings.stream()
.filter(HeimatController.ProjectMapping::isPendingRemoval)
.forEach(pm -> pm.setHeimatTask(null));
mappingTableView.refresh();
}
}
}
});
}

@FXML
private void initialize() {
tasksForDateDatePicker.setValue(LocalDate.now());
tasksForDateDatePicker.setDisable(true);
// TODO add listener on this thing
// but what happens with mapped projects not existing at that date? but actually not related to this feature alone

final List<HeimatTask> externalProjects = heimatController.getTasks(tasksForDateDatePicker.getValue());
final ExistingAndInvalidMappings existingAndInvalidMappings = heimatController.getExistingProjectMappings(
final List<HeimatTask> externalProjects = heimatController.getAllKnownHeimatTasks(tasksForDateDatePicker.getValue());

existingAndInvalidMappings = heimatController.getExistingProjectMappings(
externalProjects);
final List<HeimatController.ProjectMapping> previousProjectMappings = existingAndInvalidMappings.validMappings();

final ObservableList<HeimatController.ProjectMapping> newProjectMappings = FXCollections.observableArrayList(

final List<HeimatController.ProjectMapping> previousProjectMappings = existingAndInvalidMappings.validMappings();
newProjectMappings = FXCollections.observableArrayList(
previousProjectMappings);

final FilteredList<HeimatController.ProjectMapping> value = new FilteredList<>(newProjectMappings,
pm -> pm.getProject().isWork());
mappingTableView.setItems(value);
Expand All @@ -103,58 +124,56 @@ private void initialize() {
TableColumn<HeimatController.ProjectMapping, String> keepTimeColumn = new TableColumn<>("KeepTime project");
keepTimeColumn.setCellValueFactory(data -> new SimpleStringProperty(data.getValue().getProject().getName()));

keepTimeColumn.setCellFactory(col -> new TableCell<>() {
@Override
protected void updateItem(String item, boolean empty) {
super.updateItem(item, empty);
if (empty || item == null) {
setText(null);
setTooltip(null);
} else {
setText(item);
Tooltip tooltip = new Tooltip(item);
setTooltip(tooltip);
}
}
});

// External Project column with dropdown
final ObservableList<HeimatTask> externalProjectsObservableList = FXCollections.observableArrayList(
externalProjects);
externalProjectsObservableList.add(0, null); // option to clear selection

TableColumn<HeimatController.ProjectMapping, HeimatTask> externalColumn = new TableColumn<>("HEIMAT project");
externalColumn.setCellValueFactory(data -> new SimpleObjectProperty<>(data.getValue().getHeimatTask()));
externalColumn.setCellFactory(col -> new TableCell<>() {
// TODO search in box would be nice
private final ComboBox<HeimatTask> comboBox = new ComboBox<>(externalProjectsObservableList);
private final SearchCombobox<HeimatTask> searchPopup = new SearchCombobox<>(externalProjectsObservableList);

{
searchPopup.setDisplayTextFunction(ht -> ht == null ? "" : ht.taskHolderName() + " - " + ht.name());
searchPopup.setClearFieldAfterSelection(false);
searchPopup.setPromptText("Search Project...");
searchPopup.setOnItemSelected((selectedTask, popup) -> {
HeimatController.ProjectMapping mapping = getTableView().getItems().get(getIndex());
mapping.setHeimatTask(selectedTask);
searchPopup.setComboBoxTooltip(selectedTask.name() + " - " + selectedTask.id());
updateItem(selectedTask, false);
});
}

@Override
protected void updateItem(HeimatTask item, boolean empty) {
super.updateItem(item, empty);
// selected item
comboBox.setButtonCell(new ListCell<>() {
@Override
protected void updateItem(HeimatTask item, boolean empty) {
super.updateItem(item, empty);
if (empty || item == null) {
setText(null);
} else {
setText(item.taskHolderName() + " - " + item.name());
}
}
});

// Dropdown
comboBox.setCellFactory(param -> new ListCell<>() {
@Override
protected void updateItem(HeimatTask item, boolean empty) {
super.updateItem(item, empty);
if (item == null || empty) {
setGraphic(null);
setText(null);
} else {
// TODO maybe show if the project was already mapped
setText(item.taskHolderName() + " - " + item.name());
}
}
});

if (empty) {
setGraphic(null);
setText(null);
} else {
comboBox.setValue(getTableView().getItems().get(getIndex()).getHeimatTask());
comboBox.setOnAction(e -> {
HeimatController.ProjectMapping mapping = getTableView().getItems().get(getIndex());
mapping.setHeimatTask(comboBox.getValue());
});
setGraphic(comboBox);
searchPopup.setSelectedItem(item);
if (item != null) {
searchPopup.setComboBoxTooltip(item.name() + " - " + item.id());
} else {
searchPopup.setComboBoxTooltip("");
}
setGraphic(searchPopup.getComboBox());
setText(null);
}
}
Expand All @@ -179,7 +198,7 @@ protected void updateItem(HeimatTask item, boolean empty) {
final Project project = controller.addNewProject(
new Project(toBeCreatedHeimatTask.name() + " - " + toBeCreatedHeimatTask.taskHolderName(),
toBeCreatedHeimatTask.bookingHint(), ColorHelper.randomColor(), true, sortIndex));
newProjectMappings.add(new HeimatController.ProjectMapping(project, toBeCreatedHeimatTask));
newProjectMappings.add(new HeimatController.ProjectMapping(project, toBeCreatedHeimatTask, false));
}
});

Expand All @@ -189,11 +208,6 @@ protected void updateItem(HeimatTask item, boolean empty) {
});

cancelButton.setOnAction(ae -> thisStage.close());

List<String> warnings = existingAndInvalidMappings.invalidMappingsAsString();
if (!warnings.isEmpty()) {
Platform.runLater(() -> showInvalidMappingsDialog(warnings));
}
}

private List<HeimatTask> showMultiSelectDialog(final List<HeimatTask> externalProjects,
Expand All @@ -210,6 +224,11 @@ private List<HeimatTask> showMultiSelectDialog(final List<HeimatTask> externalPr
ButtonType cancelButtonType = new ButtonType("Cancel", ButtonBar.ButtonData.CANCEL_CLOSE);
dialog.getDialogPane().getButtonTypes().addAll(okButtonType, cancelButtonType);

// Observable and filtered list
ObservableList<HeimatTask> baseList = FXCollections.observableArrayList(externalProjects);
FilteredList<HeimatTask> filteredList = new FilteredList<>(baseList, t -> true);

// Name Column
TableView<HeimatTask> tableView = new TableView<>();
TableColumn<HeimatTask, HeimatTask> nameColumn = new TableColumn<>("HEIMAT project");
nameColumn.setCellValueFactory(data -> new SimpleObjectProperty<>(data.getValue()));
Expand Down Expand Up @@ -238,9 +257,10 @@ protected void updateItem(HeimatTask item, boolean empty) {
tableView.setEditable(false);

tableView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
tableView.setItems(FXCollections.observableArrayList(externalProjects));
tableView.setItems(filteredList);

Button selectAllUnmappedButton = new Button("Select unmapped projects (" + unmappedHeimatTasks.size() + ")");
Button selectAllUnmappedButton = new Button("Select unmapped projects ("
+ unmappedHeimatTasks.size() + ")");
selectAllUnmappedButton.getStyleClass().add("secondary-button");
selectAllUnmappedButton.setOnAction(e -> {
tableView.getSelectionModel().clearSelection();
Expand All @@ -250,7 +270,27 @@ protected void updateItem(HeimatTask item, boolean empty) {
tableView.requestFocus();
});

VBox content = new VBox(10, selectAllUnmappedButton, tableView);
TextField searchField = new TextField();
searchField.setPromptText("Search...");
searchField.textProperty().addListener((obs, oldText, newText) -> {
String filter = newText == null ? "" : newText.trim().toLowerCase();
filteredList.setPredicate(task -> {
if (filter.isEmpty()) return true;
return task.taskHolderName().toLowerCase().contains(filter)
|| task.name().toLowerCase().contains(filter);
});

long visibleUnmapped = filteredList.stream().filter(unmappedHeimatTasks::contains).count();
selectAllUnmappedButton.setText("Select unmapped projects ("
+ visibleUnmapped + ")");
});
searchField.getStyleClass().add("text-field");
searchField.setMaxWidth(Double.MAX_VALUE);
HBox.setHgrow(searchField, Priority.ALWAYS);

HBox headContent = new HBox(50, selectAllUnmappedButton, searchField);

VBox content = new VBox(10, headContent, tableView);
dialog.getDialogPane().setContent(content);
final List<HeimatTask> emptyList = List.of();
dialog.setResultConverter(dialogButton -> {
Expand Down Expand Up @@ -279,11 +319,17 @@ protected void updateItem(HeimatTask item, boolean empty) {
return result.orElse(emptyList);
}

private void showInvalidMappingsDialog(final List<String> warnings) {
Dialog<Void> dialog = new Dialog<>();
private boolean showInvalidMappingsDialog(final List<String> warnings) {
Dialog<ButtonType> dialog = new Dialog<>();

dialog.initOwner(this.thisStage);

Stage dialogStage = (Stage) dialog.getDialogPane().getScene().getWindow();
dialogStage.getIcons().addAll(this.thisStage.getIcons());

dialog.setTitle("Invalid mappings");
dialog.setHeaderText("Please note to following issue:");
dialog.setHeaderText("The following projects are no longer available.\n"
+ "Would you like to remove them from your mapping list?");

VBox warningBox = new VBox(10);
for (String warning : warnings) {
Expand All @@ -299,10 +345,11 @@ private void showInvalidMappingsDialog(final List<String> warnings) {
dialog.getDialogPane().setContent(scrollPane);
dialog.getDialogPane().setMinWidth(400);

// Add OK button
ButtonType okButton = new ButtonType("OK", ButtonBar.ButtonData.OK_DONE);
dialog.getDialogPane().getButtonTypes().add(okButton);
ButtonType removeButton = new ButtonType("Remove", ButtonBar.ButtonData.YES);
ButtonType keepButton = new ButtonType("Keep", ButtonBar.ButtonData.NO);
dialog.getDialogPane().getButtonTypes().setAll(removeButton, keepButton);

dialog.showAndWait();
Optional<ButtonType> result = dialog.showAndWait();
return result.isPresent() && result.get() == removeButton;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@ public class ExternalProjectsSyncController {

private LocalDate currentReportDate;
private Stage thisStage;
private Timeline closingTimeline;
private final HeimatController heimatController;
private final RotateTransition loadingSpinnerAnimation = new RotateTransition(Duration.seconds(1),
syncingIconRegion);
Expand Down Expand Up @@ -229,8 +230,10 @@ public void initForDate(LocalDate currentReportDate, List<Work> currentWorkItems
itemsForBindings.add(addedRow); // add new row also to items2 - as it is not added automatically :(
mappingTableView.scrollTo(items.size() - 1); // scroll to newly added row
});
heimatTaskSearchCombobox.setClearFieldAfterSelection(true);

heimatTaskSearchCombobox.setClearFieldAfterSelection(true);
heimatTaskSearchCombobox.setMaxSuggestionHeight(220);
heimatTaskSearchCombobox.setPromptText("Select Project...");
heimatTaskSearchContainer.getChildren().add(heimatTaskSearchCombobox.getComboBox());
HBox.setHgrow(heimatTaskSearchCombobox.getComboBox(), Priority.ALWAYS);
}
Expand Down Expand Up @@ -506,10 +509,14 @@ protected List<HeimatController.HeimatErrors> call() {
loadingSuccess);
}

if (closingTimeline != null) {
closingTimeline.stop();
}

final AtomicInteger remainingSeconds = new AtomicInteger(closingSeconds);
loadingClosingMessage.setText("Closing in " + remainingSeconds + " seconds...");
loadingClosingMessage.setVisible(true);
Timeline timeline = new Timeline(new KeyFrame(Duration.seconds(1), event -> {
closingTimeline = new Timeline(new KeyFrame(Duration.seconds(1), event -> {
remainingSeconds.getAndDecrement();
loadingClosingMessage.setText("Closing in " + remainingSeconds + " seconds...");
if (remainingSeconds.get() <= 0) {
Expand All @@ -518,8 +525,8 @@ protected List<HeimatController.HeimatErrors> call() {
loadingClosingMessage.setVisible(false);
}
}));
timeline.setCycleCount(remainingSeconds.get());
timeline.play();
closingTimeline.setCycleCount(remainingSeconds.get());
closingTimeline.play();
});

task.setOnFailed(e -> {
Expand Down Expand Up @@ -702,6 +709,14 @@ public static LocalTime decrementToNextHour(LocalTime time) {
public void setStage(final Stage thisStage) {
this.thisStage = thisStage;


thisStage.setOnCloseRequest(e -> {
if (closingTimeline != null) {
closingTimeline.stop();
closingTimeline = null;
}
});

registerKeyEventListenersForSpinners(thisStage);
}

Expand Down
Loading
Loading