Skip to content

Commit 372d34a

Browse files
authored
Merge pull request #186 from doubleSlashde/feature/PTBAS-741_mappingDialogAdjustment
PTBAS-741: Adjust Mapping Dialog * mapping dialog * - user can now choose to remove invalid mappings, or keep them * - invalid mappings are also highlighted in ui * - added filter possibility * sync dialog * - fixed issue which closed sync dialog 'directly' after it was openend * - fixed issue when same project was shown twice in the sync dialog * setting dialog * - highlights heimat token expiration in red when expired
2 parents 07419a7 + 31eff20 commit 372d34a

10 files changed

Lines changed: 620 additions & 242 deletions

File tree

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

Lines changed: 101 additions & 79 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 localDateTimeNow.isAfter(token.expiration);
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: 122 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,11 @@
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.SearchCombobox;
28+
import java.time.LocalDate;
29+
import java.util.List;
30+
import java.util.Optional;
31+
2732
import javafx.application.Platform;
2833
import javafx.beans.property.SimpleBooleanProperty;
2934
import javafx.beans.property.SimpleObjectProperty;
@@ -33,18 +38,28 @@
3338
import javafx.collections.ObservableList;
3439
import javafx.collections.transformation.FilteredList;
3540
import javafx.fxml.FXML;
36-
import javafx.scene.control.*;
41+
import javafx.scene.control.Button;
42+
import javafx.scene.control.ButtonBar;
43+
import javafx.scene.control.ButtonType;
44+
import javafx.scene.control.DatePicker;
45+
import javafx.scene.control.Dialog;
46+
import javafx.scene.control.Label;
47+
import javafx.scene.control.ScrollPane;
48+
import javafx.scene.control.SelectionMode;
49+
import javafx.scene.control.TableCell;
50+
import javafx.scene.control.TableColumn;
51+
import javafx.scene.control.TableView;
52+
import javafx.scene.control.TextField;
53+
import javafx.scene.control.Tooltip;
3754
import javafx.scene.control.cell.CheckBoxTableCell;
55+
import javafx.scene.layout.HBox;
56+
import javafx.scene.layout.Priority;
3857
import javafx.scene.layout.VBox;
3958
import javafx.stage.Stage;
4059
import org.slf4j.Logger;
4160
import org.slf4j.LoggerFactory;
4261
import org.springframework.stereotype.Component;
4362

44-
import java.time.LocalDate;
45-
import java.util.List;
46-
import java.util.Optional;
47-
4863
@Component
4964
public class ExternalProjectsMapController {
5065

@@ -86,15 +101,27 @@ private void initialize() {
86101
tasksForDateDatePicker.setValue(LocalDate.now());
87102
tasksForDateDatePicker.setDisable(true);
88103
// 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
90104

91105
final List<HeimatTask> externalProjects = heimatController.getTasks(tasksForDateDatePicker.getValue());
92-
final ExistingAndInvalidMappings existingAndInvalidMappings = heimatController.getExistingProjectMappings(
93-
externalProjects);
94-
final List<HeimatController.ProjectMapping> previousProjectMappings = existingAndInvalidMappings.validMappings();
95106

107+
final ExistingAndInvalidMappings existingAndInvalidMappings = heimatController.getExistingProjectMappings(externalProjects);
108+
109+
final List<HeimatController.ProjectMapping> previousProjectMappings = existingAndInvalidMappings.validMappings();
96110
final ObservableList<HeimatController.ProjectMapping> newProjectMappings = FXCollections.observableArrayList(
97111
previousProjectMappings);
112+
113+
Platform.runLater(() -> {
114+
List<String> warnings = existingAndInvalidMappings.invalidMappingsAsString();
115+
if (!warnings.isEmpty()) {
116+
if (showInvalidMappingsDialog(warnings)) {
117+
newProjectMappings.stream()
118+
.filter(HeimatController.ProjectMapping::isPendingRemoval)
119+
.forEach(pm -> pm.setHeimatTask(null));
120+
mappingTableView.refresh();
121+
}
122+
}
123+
});
124+
98125
final FilteredList<HeimatController.ProjectMapping> value = new FilteredList<>(newProjectMappings,
99126
pm -> pm.getProject().isWork());
100127
mappingTableView.setItems(value);
@@ -103,58 +130,62 @@ private void initialize() {
103130
TableColumn<HeimatController.ProjectMapping, String> keepTimeColumn = new TableColumn<>("KeepTime project");
104131
keepTimeColumn.setCellValueFactory(data -> new SimpleStringProperty(data.getValue().getProject().getName()));
105132

133+
keepTimeColumn.setCellFactory(col -> new TableCell<>() {
134+
@Override
135+
protected void updateItem(String item, boolean empty) {
136+
super.updateItem(item, empty);
137+
if (empty || item == null) {
138+
setText(null);
139+
setTooltip(null);
140+
} else {
141+
setText(item);
142+
Tooltip tooltip = new Tooltip(item);
143+
setTooltip(tooltip);
144+
}
145+
}
146+
});
147+
106148
// External Project column with dropdown
107149
final ObservableList<HeimatTask> externalProjectsObservableList = FXCollections.observableArrayList(
108150
externalProjects);
109-
externalProjectsObservableList.add(0, null); // option to clear selection
110-
151+
externalProjectsObservableList.add(0,null);
111152
TableColumn<HeimatController.ProjectMapping, HeimatTask> externalColumn = new TableColumn<>("Heimat project");
112153
externalColumn.setCellValueFactory(data -> new SimpleObjectProperty<>(data.getValue().getHeimatTask()));
113154
externalColumn.setCellFactory(col -> new TableCell<>() {
114-
// TODO search in box would be nice
115-
private final ComboBox<HeimatTask> comboBox = new ComboBox<>(externalProjectsObservableList);
155+
private final SearchCombobox<HeimatTask> searchPopup = new SearchCombobox<>(externalProjectsObservableList);
156+
157+
{
158+
searchPopup.setDisplayTextFunction(ht -> ht == null ? "" : ht.taskHolderName() + " - " + ht.name());
159+
searchPopup.setClearFieldAfterSelection(false);
160+
searchPopup.setPromptText("Search Project...");
161+
searchPopup.setOnItemSelected((selectedTask, popup) -> {
162+
HeimatController.ProjectMapping mapping = getTableView().getItems().get(getIndex());
163+
mapping.setHeimatTask(selectedTask);
164+
if(selectedTask != null)
165+
searchPopup.setComboBoxTooltip(selectedTask.name() + " - " + selectedTask.id());
166+
updateItem(selectedTask, false);
167+
});
168+
}
116169

117170
@Override
118171
protected void updateItem(HeimatTask item, boolean empty) {
119172
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-
148173
if (empty) {
149174
setGraphic(null);
150175
setText(null);
176+
setStyle(null);
151177
} 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);
178+
searchPopup.setSelectedItem(item);
179+
if (item != null) {
180+
searchPopup.setComboBoxTooltip(item.name() + " - " + item.id());
181+
} else {
182+
searchPopup.setComboBoxTooltip("");
183+
}
184+
185+
// highlight mappings which do not exist anymore
186+
final String highlightStyle = item != null && !externalProjects.contains(item) ? "-fx-background-color: lightsalmon;" : null;
187+
setStyle(highlightStyle);
188+
setGraphic(searchPopup.getComboBox());
158189
setText(null);
159190
}
160191
}
@@ -179,7 +210,7 @@ protected void updateItem(HeimatTask item, boolean empty) {
179210
final Project project = controller.addNewProject(
180211
new Project(toBeCreatedHeimatTask.name() + " - " + toBeCreatedHeimatTask.taskHolderName(),
181212
toBeCreatedHeimatTask.bookingHint(), ColorHelper.randomColor(), true, sortIndex));
182-
newProjectMappings.add(new HeimatController.ProjectMapping(project, toBeCreatedHeimatTask));
213+
newProjectMappings.add(new HeimatController.ProjectMapping(project, toBeCreatedHeimatTask, false));
183214
}
184215
});
185216

@@ -189,11 +220,6 @@ protected void updateItem(HeimatTask item, boolean empty) {
189220
});
190221

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

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

239+
// Observable and filtered list
240+
ObservableList<HeimatTask> baseList = FXCollections.observableArrayList(externalProjects);
241+
FilteredList<HeimatTask> filteredList = new FilteredList<>(baseList, t -> true);
242+
243+
// Name Column
213244
TableView<HeimatTask> tableView = new TableView<>();
214245
TableColumn<HeimatTask, HeimatTask> nameColumn = new TableColumn<>("Heimat project");
215246
nameColumn.setCellValueFactory(data -> new SimpleObjectProperty<>(data.getValue()));
@@ -238,9 +269,10 @@ protected void updateItem(HeimatTask item, boolean empty) {
238269
tableView.setEditable(false);
239270

240271
tableView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
241-
tableView.setItems(FXCollections.observableArrayList(externalProjects));
272+
tableView.setItems(filteredList);
242273

243-
Button selectAllUnmappedButton = new Button("Select unmapped projects (" + unmappedHeimatTasks.size() + ")");
274+
Button selectAllUnmappedButton = new Button("Select unmapped projects ("
275+
+ unmappedHeimatTasks.size() + ")");
244276
selectAllUnmappedButton.getStyleClass().add("secondary-button");
245277
selectAllUnmappedButton.setOnAction(e -> {
246278
tableView.getSelectionModel().clearSelection();
@@ -250,7 +282,27 @@ protected void updateItem(HeimatTask item, boolean empty) {
250282
tableView.requestFocus();
251283
});
252284

253-
VBox content = new VBox(10, selectAllUnmappedButton, tableView);
285+
TextField searchField = new TextField();
286+
searchField.setPromptText("Search...");
287+
searchField.textProperty().addListener((obs, oldText, newText) -> {
288+
String filter = newText == null ? "" : newText.trim().toLowerCase();
289+
filteredList.setPredicate(task -> {
290+
if (filter.isEmpty()) return true;
291+
return task.taskHolderName().toLowerCase().contains(filter)
292+
|| task.name().toLowerCase().contains(filter);
293+
});
294+
295+
long visibleUnmapped = filteredList.stream().filter(unmappedHeimatTasks::contains).count();
296+
selectAllUnmappedButton.setText("Select unmapped projects ("
297+
+ visibleUnmapped + ")");
298+
});
299+
searchField.getStyleClass().add("text-field");
300+
searchField.setMaxWidth(Double.MAX_VALUE);
301+
HBox.setHgrow(searchField, Priority.ALWAYS);
302+
303+
HBox headContent = new HBox(50, selectAllUnmappedButton, searchField);
304+
305+
VBox content = new VBox(10, headContent, tableView);
254306
dialog.getDialogPane().setContent(content);
255307
final List<HeimatTask> emptyList = List.of();
256308
dialog.setResultConverter(dialogButton -> {
@@ -279,11 +331,17 @@ protected void updateItem(HeimatTask item, boolean empty) {
279331
return result.orElse(emptyList);
280332
}
281333

282-
private void showInvalidMappingsDialog(final List<String> warnings) {
283-
Dialog<Void> dialog = new Dialog<>();
334+
private boolean showInvalidMappingsDialog(final List<String> warnings) {
335+
Dialog<ButtonType> dialog = new Dialog<>();
336+
284337
dialog.initOwner(this.thisStage);
338+
339+
Stage dialogStage = (Stage) dialog.getDialogPane().getScene().getWindow();
340+
dialogStage.getIcons().addAll(this.thisStage.getIcons());
341+
285342
dialog.setTitle("Invalid mappings");
286-
dialog.setHeaderText("Please note to following issue:");
343+
dialog.setHeaderText("The following projects are no longer available.\n"
344+
+ "Would you like to remove them from your mapping list?");
287345

288346
VBox warningBox = new VBox(10);
289347
for (String warning : warnings) {
@@ -299,10 +357,11 @@ private void showInvalidMappingsDialog(final List<String> warnings) {
299357
dialog.getDialogPane().setContent(scrollPane);
300358
dialog.getDialogPane().setMinWidth(400);
301359

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

306-
dialog.showAndWait();
364+
Optional<ButtonType> result = dialog.showAndWait();
365+
return result.isPresent() && result.get() == removeButton;
307366
}
308367
}

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

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,7 @@ public class ExternalProjectsSyncController {
138138

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

234+
heimatTaskSearchCombobox.setClearFieldAfterSelection(true);
235+
heimatTaskSearchCombobox.setMaxSuggestionHeight(220);
236+
heimatTaskSearchCombobox.setPromptText("Select Project...");
234237
heimatTaskSearchContainer.getChildren().add(heimatTaskSearchCombobox.getComboBox());
235238
HBox.setHgrow(heimatTaskSearchCombobox.getComboBox(), Priority.ALWAYS);
236239
}
@@ -506,10 +509,14 @@ protected List<HeimatController.HeimatErrors> call() {
506509
loadingSuccess);
507510
}
508511

512+
if (closingTimeline != null) {
513+
closingTimeline.stop();
514+
}
515+
509516
final AtomicInteger remainingSeconds = new AtomicInteger(closingSeconds);
510517
loadingClosingMessage.setText("Closing in " + remainingSeconds + " seconds...");
511518
loadingClosingMessage.setVisible(true);
512-
Timeline timeline = new Timeline(new KeyFrame(Duration.seconds(1), event -> {
519+
closingTimeline = new Timeline(new KeyFrame(Duration.seconds(1), event -> {
513520
remainingSeconds.getAndDecrement();
514521
loadingClosingMessage.setText("Closing in " + remainingSeconds + " seconds...");
515522
if (remainingSeconds.get() <= 0) {
@@ -518,8 +525,8 @@ protected List<HeimatController.HeimatErrors> call() {
518525
loadingClosingMessage.setVisible(false);
519526
}
520527
}));
521-
timeline.setCycleCount(remainingSeconds.get());
522-
timeline.play();
528+
closingTimeline.setCycleCount(remainingSeconds.get());
529+
closingTimeline.play();
523530
});
524531

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

712+
713+
thisStage.setOnCloseRequest(e -> {
714+
if (closingTimeline != null) {
715+
closingTimeline.stop();
716+
closingTimeline = null;
717+
}
718+
});
719+
705720
registerKeyEventListenersForSpinners(thisStage);
706721
}
707722

0 commit comments

Comments
 (0)