Skip to content

Commit 03610d9

Browse files
committed
#178: allow to map projects in settings view
1 parent ce597b4 commit 03610d9

File tree

9 files changed

+309
-4
lines changed

9 files changed

+309
-4
lines changed

src/main/java/de/doubleslash/keeptime/common/Resources.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ public enum RESOURCE {
4040

4141
FXML_MANAGE_PROJECT("/layouts/manage-project.fxml"),
4242
FXML_MANAGE_WORK("/layouts/manage-work.fxml"),
43+
FXML_EXT_PROJECT_MAPPING("/layouts/externalProjectMapping.fxml"),
4344

4445
SVG_CALENDAR_DAYS_ICON("/svgs/calendar-days.svg"),
4546

src/main/java/de/doubleslash/keeptime/model/ExternalProjectMapping.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,4 +102,8 @@ public Project getProject() {
102102
public void setProject(Project project) {
103103
this.project = project;
104104
}
105+
106+
public long getDatabaseId() {
107+
return id;
108+
}
105109
}

src/main/java/de/doubleslash/keeptime/model/repos/ExternalProjectsMappingsRepository.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,15 @@
1818
package de.doubleslash.keeptime.model.repos;
1919

2020
import de.doubleslash.keeptime.model.ExternalProjectMapping;
21+
import de.doubleslash.keeptime.model.ExternalSystem;
2122
import de.doubleslash.keeptime.model.Project;
2223
import org.springframework.data.jpa.repository.JpaRepository;
2324
import org.springframework.stereotype.Repository;
2425

26+
import java.util.List;
27+
2528
@Repository
2629
public interface ExternalProjectsMappingsRepository extends JpaRepository<ExternalProjectMapping, Long> {
2730

31+
List<ExternalProjectMapping> findByExternalSystemId(ExternalSystem externalSystem);
2832
}

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,10 @@ public class HeimatAPI {
1717

1818
public HeimatAPI(final String baseUrl, final String bearerToken) {
1919
restClient = RestClient.builder()
20-
.baseUrl(baseUrl)
20+
.baseUrl(baseUrl + "/heimat-core/api/v1/")
2121
.defaultHeader("X-Client-Identifier", "KeepTime")
2222
.defaultHeader("Authorization", "Bearer " + bearerToken)
23+
.defaultHeader("Accept", "application/json")
2324
.build();
2425
}
2526

Lines changed: 226 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,226 @@
1+
// Copyright 2025 doubleSlash Net Business GmbH
2+
//
3+
// This file is part of KeepTime.
4+
// KeepTime is free software: you can redistribute it and/or modify
5+
// it under the terms of the GNU General Public License as published by
6+
// the Free Software Foundation, either version 3 of the License, or
7+
// (at your option) any later version.
8+
//
9+
// This program is distributed in the hope that it will be useful,
10+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
// GNU General Public License for more details.
13+
//
14+
// You should have received a copy of the GNU General Public License
15+
// along with this program. If not, see <http://www.gnu.org/licenses/>.
16+
17+
package de.doubleslash.keeptime.view;
18+
19+
import de.doubleslash.keeptime.model.ExternalProjectMapping;
20+
import de.doubleslash.keeptime.model.ExternalSystem;
21+
import de.doubleslash.keeptime.model.Model;
22+
import de.doubleslash.keeptime.model.Project;
23+
import de.doubleslash.keeptime.model.repos.ExternalProjectsMappingsRepository;
24+
import de.doubleslash.keeptime.model.settings.HeimatSettings;
25+
import de.doubleslash.keeptime.rest.integration.heimat.HeimatAPI;
26+
import de.doubleslash.keeptime.rest.integration.heimat.model.HeimatTask;
27+
import javafx.beans.property.SimpleObjectProperty;
28+
import javafx.beans.property.SimpleStringProperty;
29+
import javafx.collections.FXCollections;
30+
import javafx.collections.transformation.FilteredList;
31+
import javafx.fxml.FXML;
32+
import javafx.scene.control.*;
33+
import org.slf4j.Logger;
34+
import org.slf4j.LoggerFactory;
35+
import org.springframework.stereotype.Component;
36+
37+
import java.util.List;
38+
import java.util.Optional;
39+
40+
@Component
41+
public class MapExternalProjectsController {
42+
43+
private static final Logger LOG = LoggerFactory.getLogger(MapExternalProjectsController.class);
44+
45+
private final Model model;
46+
private final HeimatSettings heimatSettings;
47+
private final ExternalProjectsMappingsRepository externalProjectsMappingsRepository;
48+
49+
@FXML
50+
private TableView<ProjectMapping> mappingTableView;
51+
52+
@FXML
53+
private Button saveButton;
54+
55+
@FXML
56+
private Button cancelButton;
57+
58+
@FXML
59+
private CheckBox filterOnlyWorkCheckBox;
60+
61+
public MapExternalProjectsController(final Model model, HeimatSettings heimatSettings,
62+
ExternalProjectsMappingsRepository externalProjectsMappingsRepository) {
63+
this.model = model;
64+
this.heimatSettings = heimatSettings;
65+
this.externalProjectsMappingsRepository = externalProjectsMappingsRepository;
66+
}
67+
68+
@FXML
69+
private void initialize() {
70+
71+
final HeimatAPI heimatAPI = new HeimatAPI(heimatSettings.getHeimatUrl(), heimatSettings.getHeimatPat());
72+
final List<HeimatTask> externalProjects = heimatAPI.getMyTasks();
73+
74+
final List<ExternalProjectMapping> alreadyMappedProjects = externalProjectsMappingsRepository.findByExternalSystemId(
75+
ExternalSystem.Heimat);
76+
77+
final List<ProjectMapping> projectMappings = model.getSortedAvailableProjects().stream().map(p -> {
78+
final Optional<ExternalProjectMapping> mapping = alreadyMappedProjects.stream()
79+
.filter(mp -> mp.getProject().getId()
80+
== p.getId())
81+
.findAny();
82+
if (mapping.isEmpty()) {
83+
return new ProjectMapping(p, null);
84+
}
85+
final Optional<HeimatTask> any = externalProjects.stream()
86+
.filter(ep -> ep.id() == mapping.get().getExternalTaskId())
87+
.findAny();
88+
if (any.isEmpty()) {
89+
LOG.warn("A mapping exists but task does not exist anymore in HEIMAT! {}.", mapping.get());
90+
return new ProjectMapping(p, null);
91+
}
92+
return new ProjectMapping(p, any.get());
93+
}).toList();
94+
95+
final FilteredList<ProjectMapping> value = new FilteredList<>(FXCollections.observableArrayList(projectMappings));
96+
filterOnlyWorkCheckBox.selectedProperty().addListener(((observable, oldValue, newValue) -> {
97+
if (Boolean.TRUE.equals(newValue))
98+
value.setPredicate(pm -> pm.getProject().isWork());
99+
else
100+
value.setPredicate(null);
101+
}));
102+
filterOnlyWorkCheckBox.setSelected(true);
103+
mappingTableView.setItems(value);
104+
105+
// KeepTime Project column
106+
TableColumn<ProjectMapping, String> keepTimeColumn = new TableColumn<>("KeepTime Project");
107+
keepTimeColumn.setCellValueFactory(data -> new SimpleStringProperty(data.getValue().project.getName()));
108+
keepTimeColumn.setPrefWidth(200);
109+
110+
// External Project column with dropdown
111+
TableColumn<ProjectMapping, HeimatTask> externalColumn = new TableColumn<>("Heimat Project");
112+
externalColumn.setCellValueFactory(data -> new SimpleObjectProperty<>(data.getValue().heimatTask));
113+
externalColumn.setCellFactory(col -> new TableCell<>() {
114+
// TODO search in box would be nice
115+
private final ComboBox<HeimatTask> comboBox = new ComboBox<>(
116+
FXCollections.observableArrayList(externalProjects));
117+
118+
@Override
119+
protected void updateItem(HeimatTask item, boolean empty) {
120+
super.updateItem(item, empty);
121+
// selected item
122+
comboBox.setButtonCell(new ListCell<>() {
123+
@Override
124+
protected void updateItem(HeimatTask item, boolean empty) {
125+
super.updateItem(item, empty);
126+
if (empty || item == null) {
127+
setText(null);
128+
} else {
129+
setText(item.projectName() + " - " + item.name());
130+
}
131+
}
132+
});
133+
134+
// Dropdown
135+
comboBox.setCellFactory(param -> new ListCell<>() {
136+
@Override
137+
protected void updateItem(HeimatTask item, boolean empty) {
138+
super.updateItem(item, empty);
139+
if (item == null || empty) {
140+
setGraphic(null);
141+
setText(null);
142+
} else {
143+
setText(item.projectName() + " - " + item.name());
144+
}
145+
}
146+
});
147+
148+
if (empty) {
149+
setGraphic(null);
150+
setText(null);
151+
} else {
152+
comboBox.setValue(getTableView().getItems().get(getIndex()).getHeimatTask());
153+
comboBox.setOnAction(e -> {
154+
ProjectMapping mapping = getTableView().getItems().get(getIndex());
155+
mapping.setHeimatTask(comboBox.getValue());
156+
});
157+
setGraphic(comboBox);
158+
setText(null);
159+
}
160+
}
161+
});
162+
externalColumn.setPrefWidth(400);
163+
164+
mappingTableView.getColumns().addAll(keepTimeColumn, externalColumn);
165+
166+
saveButton.setOnAction((ae) -> {
167+
LOG.debug("New mappings to be saved '{}'.", projectMappings);
168+
final List<ProjectMapping> newMappings = projectMappings.stream()
169+
.filter(pm -> pm.getHeimatTask() != null)
170+
.toList();
171+
172+
final List<ExternalProjectMapping> list = newMappings.stream().map(projectMapping -> {
173+
final Optional<ExternalProjectMapping> any = alreadyMappedProjects.stream()
174+
.filter(pm -> pm.getProject().getId()
175+
== projectMapping.project.getId())
176+
.findAny();
177+
final HeimatTask heimatTask = projectMapping.getHeimatTask();
178+
if (any.isPresent()) {
179+
final ExternalProjectMapping projectMapping1 = any.get();
180+
projectMapping1.setExternalProjectName(heimatTask.projectName());
181+
projectMapping1.setExternalTaskId(heimatTask.id());
182+
projectMapping1.setExternalTaskName(heimatTask.name());
183+
projectMapping1.setExternalTaskMetadata(heimatTask.toString()); // TODO to json
184+
return projectMapping1;
185+
}
186+
return new ExternalProjectMapping(ExternalSystem.Heimat, heimatTask.projectName(), heimatTask.id(),
187+
heimatTask.name(), heimatTask.toString()// TODO to json
188+
, projectMapping.project);
189+
}).toList();
190+
191+
externalProjectsMappingsRepository.saveAll(list);
192+
// TODO remove mappings which were removed also from database
193+
});
194+
195+
cancelButton.setOnAction(ae -> {
196+
// TODO Close
197+
});
198+
}
199+
200+
public static class ProjectMapping {
201+
Project project;
202+
HeimatTask heimatTask;
203+
204+
public ProjectMapping(final Project project, final HeimatTask heimatTask) {
205+
this.project = project;
206+
this.heimatTask = heimatTask;
207+
}
208+
209+
public Project getProject() {
210+
return project;
211+
}
212+
213+
public void setProject(final Project project) {
214+
this.project = project;
215+
}
216+
217+
public HeimatTask getHeimatTask() {
218+
return heimatTask;
219+
}
220+
221+
public void setHeimatTask(final HeimatTask heimatTask) {
222+
this.heimatTask = heimatTask;
223+
}
224+
}
225+
226+
}

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

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,16 @@
2828
import java.util.Map;
2929
import java.util.Properties;
3030

31+
import de.doubleslash.keeptime.exceptions.FXMLLoaderException;
3132
import de.doubleslash.keeptime.model.settings.HeimatSettings;
3233
import de.doubleslash.keeptime.rest.integration.heimat.HeimatAPI;
3334
import de.doubleslash.keeptime.rest.integration.heimat.JwtDecoder;
3435
import javafx.beans.binding.Bindings;
36+
import javafx.fxml.FXMLLoader;
37+
import javafx.scene.Parent;
38+
import javafx.scene.Scene;
39+
import javafx.scene.input.KeyCode;
40+
import javafx.stage.Modality;
3541
import org.h2.tools.RunScript;
3642
import org.h2.tools.Script;
3743
import org.slf4j.Logger;
@@ -62,6 +68,8 @@
6268
import javafx.stage.FileChooser.ExtensionFilter;
6369
import javafx.stage.Stage;
6470

71+
import static de.doubleslash.keeptime.view.ViewController.createFXMLLoader;
72+
6573
@Component
6674
public class SettingsController {
6775
@FXML
@@ -207,6 +215,9 @@ public class SettingsController {
207215
@FXML
208216
private Label heimatValidateConnectionLabel;
209217

218+
@FXML
219+
private Button heimatMapProjectsButton;
220+
210221
private final String propertiesFilePath = "application.properties";
211222

212223
private static final String GITHUB_PAGE = "https://www.github.com/doubleSlashde/KeepTime";
@@ -397,7 +408,7 @@ private void initializeHeimat() {
397408
}
398409
});
399410
heimatValidateConnectionButton.setOnAction(ae -> {
400-
final HeimatAPI heimatAPI = new HeimatAPI(heimatUrlTextField.getText() + "/heimat-core/api/v1/", heimatPatTextField.getText());
411+
final HeimatAPI heimatAPI = new HeimatAPI(heimatUrlTextField.getText() , heimatPatTextField.getText());
401412
try {
402413
heimatAPI.isLoginValid();
403414
heimatValidateConnectionLabel.setText("Connection is valid");
@@ -416,6 +427,45 @@ private void initializeHeimat() {
416427
heimatActivationCheckbox.setSelected(heimatSettings.isHeimatActive());
417428
heimatUrlTextField.setText(heimatSettings.getHeimatUrl());
418429
heimatPatTextField.setText(heimatSettings.getHeimatPat());
430+
431+
heimatMapProjectsButton.setOnAction((ae) -> {
432+
try {
433+
showMapProjectsStage();
434+
} catch (IOException e) {
435+
throw new RuntimeException(e);
436+
}
437+
});
438+
}
439+
440+
private void showMapProjectsStage() throws IOException {
441+
try{
442+
// Settings stage
443+
final FXMLLoader fxmlLoader2 = createFXMLLoader(RESOURCE.FXML_EXT_PROJECT_MAPPING);
444+
fxmlLoader2.setControllerFactory(model.getSpringContext()::getBean);
445+
final Parent settingsRoot = fxmlLoader2.load();
446+
MapExternalProjectsController settingsController = fxmlLoader2.getController();
447+
Stage settingsStage = new Stage();
448+
//settingsController.setStage(settingsStage);
449+
settingsStage.initModality(Modality.APPLICATION_MODAL);
450+
settingsStage.setTitle("External Project Mappings");
451+
settingsStage.setResizable(false);
452+
settingsStage.getIcons().add(new Image(Resources.getResource(RESOURCE.ICON_MAIN).toString()));
453+
454+
final Scene settingsScene = new Scene(settingsRoot);
455+
settingsScene.setOnKeyPressed(ke -> {
456+
if (ke.getCode() == KeyCode.ESCAPE) {
457+
LOG.info("pressed ESCAPE");
458+
settingsStage.close();
459+
}
460+
});
461+
462+
settingsStage.setScene(settingsScene);
463+
settingsStage.showAndWait();
464+
//settingsStage.setOnHiding(e -> this.mainStage.setAlwaysOnTop(true));
465+
} catch (final IOException e) {
466+
LOG.error("Error while loading sub stage");
467+
throw new FXMLLoaderException(e);
468+
}
419469
}
420470

421471
private static void setRegionSvg(Region region, double requiredWidth, double requiredHeight, RESOURCE resource) {

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -473,7 +473,7 @@ private void loadSubStages() {
473473
}
474474
}
475475

476-
private FXMLLoader createFXMLLoader(final RESOURCE fxmlLayout) {
476+
public static FXMLLoader createFXMLLoader(final RESOURCE fxmlLayout) {
477477
return new FXMLLoader(Resources.getResource(fxmlLayout));
478478
}
479479

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
3+
<?import javafx.scene.control.Button?>
4+
<?import javafx.scene.control.CheckBox?>
5+
<?import javafx.scene.control.Label?>
6+
<?import javafx.scene.control.TableView?>
7+
<?import javafx.scene.layout.AnchorPane?>
8+
9+
10+
<AnchorPane prefHeight="600.0" prefWidth="600.0" xmlns="http://javafx.com/javafx/8.0.171" xmlns:fx="http://javafx.com/fxml/1" fx:controller="de.doubleslash.keeptime.view.MapExternalProjectsController">
11+
<children>
12+
<TableView fx:id="mappingTableView" layoutY="50.0" prefHeight="500.0" prefWidth="600.0" />
13+
<Label text="Map projects" />
14+
<Button fx:id="saveButton" layoutX="429.0" layoutY="555.0" mnemonicParsing="false" text="Save" />
15+
<Button fx:id="cancelButton" layoutX="500.0" layoutY="555.0" mnemonicParsing="false" text="Cancel" />
16+
<CheckBox fx:id="filterOnlyWorkCheckBox" layoutX="11.0" layoutY="29.0" mnemonicParsing="false" text="Only Work items" />
17+
<CheckBox layoutX="334.0" layoutY="29.0" mnemonicParsing="false" text="Only favorites" />
18+
</children>
19+
</AnchorPane>

0 commit comments

Comments
 (0)