Skip to content

Commit 1f7505f

Browse files
committed
PTBAS-741: MappingDialog tooltips, search, and mapping deletion options
* Added tooltip * Enabled search in comboboxes * Expired token now shown in red * Fixed SyncDialog auto-close * Made auto-deletion of obsolete mappings optional (Keep/Remove)
1 parent 8254c79 commit 1f7505f

File tree

9 files changed

+396
-211
lines changed

9 files changed

+396
-211
lines changed

src/main/java/de/doubleslash/keeptime/controller/HeimatController.java

Lines changed: 166 additions & 73 deletions
Large diffs are not rendered by default.

src/main/java/de/doubleslash/keeptime/rest/integration/heimat/JwtDecoder.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ public record JWTTokenAttributes(
3030
String header,
3131
String payload,
3232
LocalDateTime expiration
33-
) {}
33+
) { }
3434

3535

3636
public static JWTTokenAttributes parse(String bearerToken) {
@@ -57,6 +57,10 @@ public static JWTTokenAttributes parse(String bearerToken) {
5757
return new JWTTokenAttributes(header, payload, expiration);
5858
}
5959

60+
public static boolean isExpired(JWTTokenAttributes token, LocalDateTime localDateTimeNow) {
61+
return token.expiration.isAfter(localDateTimeNow);
62+
}
63+
6064
private static String removeBearerPrefix(String token) {
6165
return token.startsWith("Bearer ") ? token.substring(7) : token;
6266
}

src/main/java/de/doubleslash/keeptime/view/ExternalProjectsMapController.java

Lines changed: 99 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import de.doubleslash.keeptime.model.Project;
2525
import de.doubleslash.keeptime.rest.integration.heimat.model.ExistingAndInvalidMappings;
2626
import de.doubleslash.keeptime.rest.integration.heimat.model.HeimatTask;
27+
import de.doubleslash.keeptime.viewpopup.SearchPopup;
2728
import javafx.application.Platform;
2829
import javafx.beans.property.SimpleBooleanProperty;
2930
import javafx.beans.property.SimpleObjectProperty;
@@ -35,6 +36,8 @@
3536
import javafx.fxml.FXML;
3637
import javafx.scene.control.*;
3738
import javafx.scene.control.cell.CheckBoxTableCell;
39+
import javafx.scene.layout.HBox;
40+
import javafx.scene.layout.Priority;
3841
import javafx.scene.layout.VBox;
3942
import javafx.stage.Stage;
4043
import org.slf4j.Logger;
@@ -86,15 +89,29 @@ private void initialize() {
8689
tasksForDateDatePicker.setValue(LocalDate.now());
8790
tasksForDateDatePicker.setDisable(true);
8891
// TODO add listener on this thing
89-
// but what happens with mapped projects not existing at that date? but actually not related to this feature alone
9092

91-
final List<HeimatTask> externalProjects = heimatController.getTasks(tasksForDateDatePicker.getValue());
93+
final List<HeimatTask> externalProjects = heimatController.getAllKnownHeimatTasks(tasksForDateDatePicker.getValue());
94+
9295
final ExistingAndInvalidMappings existingAndInvalidMappings = heimatController.getExistingProjectMappings(
9396
externalProjects);
94-
final List<HeimatController.ProjectMapping> previousProjectMappings = existingAndInvalidMappings.validMappings();
9597

98+
99+
final List<HeimatController.ProjectMapping> previousProjectMappings = existingAndInvalidMappings.validMappings();
96100
final ObservableList<HeimatController.ProjectMapping> newProjectMappings = FXCollections.observableArrayList(
97101
previousProjectMappings);
102+
103+
Platform.runLater(() -> {
104+
List<String> warnings = existingAndInvalidMappings.invalidMappingsAsString();
105+
if (!warnings.isEmpty()) {
106+
if (showInvalidMappingsDialog(warnings)) {
107+
newProjectMappings.stream()
108+
.filter(HeimatController.ProjectMapping::isPendingRemoval)
109+
.forEach(pm -> pm.setHeimatTask(null));
110+
mappingTableView.refresh();
111+
}
112+
}
113+
});
114+
98115
final FilteredList<HeimatController.ProjectMapping> value = new FilteredList<>(newProjectMappings,
99116
pm -> pm.getProject().isWork());
100117
mappingTableView.setItems(value);
@@ -103,58 +120,56 @@ private void initialize() {
103120
TableColumn<HeimatController.ProjectMapping, String> keepTimeColumn = new TableColumn<>("KeepTime project");
104121
keepTimeColumn.setCellValueFactory(data -> new SimpleStringProperty(data.getValue().getProject().getName()));
105122

123+
keepTimeColumn.setCellFactory(col -> new TableCell<>() {
124+
@Override
125+
protected void updateItem(String item, boolean empty) {
126+
super.updateItem(item, empty);
127+
if (empty || item == null) {
128+
setText(null);
129+
setTooltip(null);
130+
} else {
131+
setText(item);
132+
Tooltip tooltip = new Tooltip(item);
133+
setTooltip(tooltip);
134+
}
135+
}
136+
});
137+
106138
// External Project column with dropdown
107139
final ObservableList<HeimatTask> externalProjectsObservableList = FXCollections.observableArrayList(
108140
externalProjects);
109-
externalProjectsObservableList.add(0, null); // option to clear selection
110141

111142
TableColumn<HeimatController.ProjectMapping, HeimatTask> externalColumn = new TableColumn<>("HEIMAT project");
112143
externalColumn.setCellValueFactory(data -> new SimpleObjectProperty<>(data.getValue().getHeimatTask()));
113144
externalColumn.setCellFactory(col -> new TableCell<>() {
114-
// TODO search in box would be nice
115-
private final ComboBox<HeimatTask> comboBox = new ComboBox<>(externalProjectsObservableList);
145+
private final SearchPopup<HeimatTask> searchPopup = new SearchPopup<>(externalProjectsObservableList);
146+
147+
{
148+
searchPopup.setDisplayTextFunction(ht -> ht == null ? "" : ht.taskHolderName() + " - " + ht.name());
149+
searchPopup.setClearFieldAfterSelection(false);
150+
searchPopup.setPromptText("Search Project...");
151+
searchPopup.setOnItemSelected((selectedTask, popup) -> {
152+
HeimatController.ProjectMapping mapping = getTableView().getItems().get(getIndex());
153+
mapping.setHeimatTask(selectedTask);
154+
searchPopup.setComboBoxTooltip(selectedTask.name() + " - " + selectedTask.id());
155+
updateItem(selectedTask, false);
156+
});
157+
}
116158

117159
@Override
118160
protected void updateItem(HeimatTask item, boolean empty) {
119161
super.updateItem(item, empty);
120-
// selected item
121-
comboBox.setButtonCell(new ListCell<>() {
122-
@Override
123-
protected void updateItem(HeimatTask item, boolean empty) {
124-
super.updateItem(item, empty);
125-
if (empty || item == null) {
126-
setText(null);
127-
} else {
128-
setText(item.taskHolderName() + " - " + item.name());
129-
}
130-
}
131-
});
132-
133-
// Dropdown
134-
comboBox.setCellFactory(param -> new ListCell<>() {
135-
@Override
136-
protected void updateItem(HeimatTask item, boolean empty) {
137-
super.updateItem(item, empty);
138-
if (item == null || empty) {
139-
setGraphic(null);
140-
setText(null);
141-
} else {
142-
// TODO maybe show if the project was already mapped
143-
setText(item.taskHolderName() + " - " + item.name());
144-
}
145-
}
146-
});
147-
148162
if (empty) {
149163
setGraphic(null);
150164
setText(null);
151165
} else {
152-
comboBox.setValue(getTableView().getItems().get(getIndex()).getHeimatTask());
153-
comboBox.setOnAction(e -> {
154-
HeimatController.ProjectMapping mapping = getTableView().getItems().get(getIndex());
155-
mapping.setHeimatTask(comboBox.getValue());
156-
});
157-
setGraphic(comboBox);
166+
searchPopup.setSelectedItem(item);
167+
if (item != null) {
168+
searchPopup.setComboBoxTooltip(item.name() + " - " + item.id());
169+
} else {
170+
searchPopup.setComboBoxTooltip("");
171+
}
172+
setGraphic(searchPopup.getComboBox());
158173
setText(null);
159174
}
160175
}
@@ -179,7 +194,7 @@ protected void updateItem(HeimatTask item, boolean empty) {
179194
final Project project = controller.addNewProject(
180195
new Project(toBeCreatedHeimatTask.name() + " - " + toBeCreatedHeimatTask.taskHolderName(),
181196
toBeCreatedHeimatTask.bookingHint(), ColorHelper.randomColor(), true, sortIndex));
182-
newProjectMappings.add(new HeimatController.ProjectMapping(project, toBeCreatedHeimatTask));
197+
newProjectMappings.add(new HeimatController.ProjectMapping(project, toBeCreatedHeimatTask, false));
183198
}
184199
});
185200

@@ -189,11 +204,6 @@ protected void updateItem(HeimatTask item, boolean empty) {
189204
});
190205

191206
cancelButton.setOnAction(ae -> thisStage.close());
192-
193-
List<String> warnings = existingAndInvalidMappings.invalidMappingsAsString();
194-
if (!warnings.isEmpty()) {
195-
Platform.runLater(() -> showInvalidMappingsDialog(warnings));
196-
}
197207
}
198208

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

223+
// Observable and filtered list
224+
ObservableList<HeimatTask> baseList = FXCollections.observableArrayList(externalProjects);
225+
FilteredList<HeimatTask> filteredList = new FilteredList<>(baseList, t -> true);
226+
227+
// Name Column
213228
TableView<HeimatTask> tableView = new TableView<>();
214229
TableColumn<HeimatTask, HeimatTask> nameColumn = new TableColumn<>("HEIMAT project");
215230
nameColumn.setCellValueFactory(data -> new SimpleObjectProperty<>(data.getValue()));
@@ -238,9 +253,10 @@ protected void updateItem(HeimatTask item, boolean empty) {
238253
tableView.setEditable(false);
239254

240255
tableView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
241-
tableView.setItems(FXCollections.observableArrayList(externalProjects));
256+
tableView.setItems(filteredList);
242257

243-
Button selectAllUnmappedButton = new Button("Select unmapped projects (" + unmappedHeimatTasks.size() + ")");
258+
Button selectAllUnmappedButton = new Button("Select unmapped projects ("
259+
+ unmappedHeimatTasks.size() + ")");
244260
selectAllUnmappedButton.getStyleClass().add("secondary-button");
245261
selectAllUnmappedButton.setOnAction(e -> {
246262
tableView.getSelectionModel().clearSelection();
@@ -250,7 +266,27 @@ protected void updateItem(HeimatTask item, boolean empty) {
250266
tableView.requestFocus();
251267
});
252268

253-
VBox content = new VBox(10, selectAllUnmappedButton, tableView);
269+
TextField searchField = new TextField();
270+
searchField.setPromptText("Search...");
271+
searchField.textProperty().addListener((obs, oldText, newText) -> {
272+
String filter = newText == null ? "" : newText.trim().toLowerCase();
273+
filteredList.setPredicate(task -> {
274+
if (filter.isEmpty()) return true;
275+
return task.taskHolderName().toLowerCase().contains(filter)
276+
|| task.name().toLowerCase().contains(filter);
277+
});
278+
279+
long visibleUnmapped = filteredList.stream().filter(unmappedHeimatTasks::contains).count();
280+
selectAllUnmappedButton.setText("Select unmapped projects ("
281+
+ visibleUnmapped + ")");
282+
});
283+
searchField.getStyleClass().add("text-field");
284+
searchField.setMaxWidth(Double.MAX_VALUE);
285+
HBox.setHgrow(searchField, Priority.ALWAYS);
286+
287+
HBox headContent = new HBox(50, selectAllUnmappedButton, searchField);
288+
289+
VBox content = new VBox(10, headContent, tableView);
254290
dialog.getDialogPane().setContent(content);
255291
final List<HeimatTask> emptyList = List.of();
256292
dialog.setResultConverter(dialogButton -> {
@@ -279,11 +315,17 @@ protected void updateItem(HeimatTask item, boolean empty) {
279315
return result.orElse(emptyList);
280316
}
281317

282-
private void showInvalidMappingsDialog(final List<String> warnings) {
283-
Dialog<Void> dialog = new Dialog<>();
318+
private boolean showInvalidMappingsDialog(final List<String> warnings) {
319+
Dialog<ButtonType> dialog = new Dialog<>();
320+
284321
dialog.initOwner(this.thisStage);
322+
323+
Stage dialogStage = (Stage) dialog.getDialogPane().getScene().getWindow();
324+
dialogStage.getIcons().addAll(this.thisStage.getIcons());
325+
285326
dialog.setTitle("Invalid mappings");
286-
dialog.setHeaderText("Please note to following issue:");
327+
dialog.setHeaderText("The following projects are no longer available.\n"
328+
+ "Would you like to remove them from your mapping list?");
287329

288330
VBox warningBox = new VBox(10);
289331
for (String warning : warnings) {
@@ -299,10 +341,11 @@ private void showInvalidMappingsDialog(final List<String> warnings) {
299341
dialog.getDialogPane().setContent(scrollPane);
300342
dialog.getDialogPane().setMinWidth(400);
301343

302-
// Add OK button
303-
ButtonType okButton = new ButtonType("OK", ButtonBar.ButtonData.OK_DONE);
304-
dialog.getDialogPane().getButtonTypes().add(okButton);
344+
ButtonType removeButton = new ButtonType("Remove", ButtonBar.ButtonData.YES);
345+
ButtonType keepButton = new ButtonType("Keep", ButtonBar.ButtonData.NO);
346+
dialog.getDialogPane().getButtonTypes().setAll(removeButton, keepButton);
305347

306-
dialog.showAndWait();
348+
Optional<ButtonType> result = dialog.showAndWait();
349+
return result.isPresent() && result.get() == removeButton;
307350
}
308351
}

src/main/java/de/doubleslash/keeptime/view/ExternalProjectsSyncController.java

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,7 @@ public class ExternalProjectsSyncController {
137137

138138
private LocalDate currentReportDate;
139139
private Stage thisStage;
140+
private Timeline closingTimeline;
140141
private final HeimatController heimatController;
141142
private final RotateTransition loadingSpinnerAnimation = new RotateTransition(Duration.seconds(1),
142143
syncingIconRegion);
@@ -236,7 +237,8 @@ public void initForDate(LocalDate currentReportDate, List<Work> currentWorkItems
236237
mappingTableView.scrollTo(items.size() - 1);
237238
});
238239
heimatTaskSearchPopup.setClearFieldAfterSelection(true);
239-
240+
heimatTaskSearchPopup.setMaxSuggestionHeight(220);
241+
heimatTaskSearchPopup.setPromptText("Select Project...");
240242
heimatTaskSearchContainer.getChildren().add(heimatTaskSearchPopup.getComboBox());
241243
HBox.setHgrow(heimatTaskSearchPopup.getComboBox(), Priority.ALWAYS);
242244
}
@@ -523,10 +525,14 @@ protected List<HeimatController.HeimatErrors> call() {
523525
loadingSuccess);
524526
}
525527

528+
if (closingTimeline != null) {
529+
closingTimeline.stop();
530+
}
531+
526532
final AtomicInteger remainingSeconds = new AtomicInteger(closingSeconds);
527533
loadingClosingMessage.setText("Closing in " + remainingSeconds + " seconds...");
528534
loadingClosingMessage.setVisible(true);
529-
Timeline timeline = new Timeline(new KeyFrame(Duration.seconds(1), event -> {
535+
closingTimeline = new Timeline(new KeyFrame(Duration.seconds(1), event -> {
530536
remainingSeconds.getAndDecrement();
531537
loadingClosingMessage.setText("Closing in " + remainingSeconds + " seconds...");
532538
if (remainingSeconds.get() <= 0) {
@@ -535,8 +541,8 @@ protected List<HeimatController.HeimatErrors> call() {
535541
loadingClosingMessage.setVisible(false);
536542
}
537543
}));
538-
timeline.setCycleCount(remainingSeconds.get());
539-
timeline.play();
544+
closingTimeline.setCycleCount(remainingSeconds.get());
545+
closingTimeline.play();
540546
});
541547

542548
task.setOnFailed(e -> {
@@ -735,6 +741,13 @@ public static LocalTime decrementToNextHour(LocalTime time) {
735741

736742
public void setStage(final Stage thisStage) {
737743
this.thisStage = thisStage;
744+
745+
thisStage.setOnCloseRequest(e -> {
746+
if (closingTimeline != null) {
747+
closingTimeline.stop();
748+
closingTimeline = null;
749+
}
750+
});
738751
}
739752

740753
public static class TableRow {

src/main/java/de/doubleslash/keeptime/view/SettingsController.java

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import java.io.InputStream;
2424
import java.nio.file.Paths;
2525
import java.sql.SQLException;
26+
import java.time.LocalDateTime;
2627
import java.util.Comparator;
2728
import java.util.HashMap;
2829
import java.util.Map;
@@ -215,6 +216,9 @@ public class SettingsController {
215216
@FXML
216217
private Label heimatExpiresLabel;
217218

219+
@FXML
220+
private Label expirationDateLabel;
221+
218222
@FXML
219223
private Button heimatValidateConnectionButton;
220224

@@ -411,9 +415,21 @@ private void initializeHeimat() {
411415
heimatPatTextField.textProperty().addListener((observable, oldValue, newValue)->{
412416
try{
413417
final JwtDecoder.JWTTokenAttributes jwt = JwtDecoder.parse(newValue);
414-
heimatExpiresLabel.setText(jwt.expiration().toString());
418+
if (!JwtDecoder.isExpired(jwt, LocalDateTime.now())) {
419+
heimatExpiresLabel.setText("Expired:");
420+
heimatExpiresLabel.setTextFill(Color.RED);
421+
expirationDateLabel.setTextFill(Color.RED);
422+
} else {
423+
heimatExpiresLabel.setText("Expires:");
424+
heimatExpiresLabel.setTextFill(Color.BLACK);
425+
expirationDateLabel.setTextFill(Color.BLACK);
426+
}
427+
428+
expirationDateLabel.setText(jwt.expiration().toString());
429+
415430
} catch(Exception e){
416-
heimatExpiresLabel.setText("Does not seem to be valid");
431+
heimatExpiresLabel.setText("");
432+
expirationDateLabel.setText("Does not seem to be valid");
417433
}
418434
});
419435
heimatValidateConnectionLabel.setText("Not validated.");

0 commit comments

Comments
 (0)