From fe069806d19a84a77dc8d54369982521ac4d3b1d Mon Sep 17 00:00:00 2001 From: TudorOrban <130213626+TudorOrban@users.noreply.github.com> Date: Wed, 27 Mar 2024 23:34:31 +0200 Subject: [PATCH] Add reusable view for searching and selecting for users --- .../controller/AddNewMembersController.java | 15 +- ...blicUsersSearchAndSelectionController.java | 167 ++++++++++++++++++ .../core/user/dto/UserSearchResultDTO.java | 16 ++ .../core/user/service/UserService.java | 5 + .../core/user/service/UserServiceImpl.java | 41 ++++- .../core/organization/AddNewMembersView.fxml | 9 +- .../PublicUsersSearchAndSelectionView.fxml | 28 +++ 7 files changed, 269 insertions(+), 12 deletions(-) create mode 100644 src/main/java/org/chainoptim/desktop/core/user/controller/PublicUsersSearchAndSelectionController.java create mode 100644 src/main/java/org/chainoptim/desktop/core/user/dto/UserSearchResultDTO.java create mode 100644 src/main/resources/org/chainoptim/desktop/core/user/PublicUsersSearchAndSelectionView.fxml diff --git a/src/main/java/org/chainoptim/desktop/core/organization/controller/AddNewMembersController.java b/src/main/java/org/chainoptim/desktop/core/organization/controller/AddNewMembersController.java index d5670219..afae4f43 100644 --- a/src/main/java/org/chainoptim/desktop/core/organization/controller/AddNewMembersController.java +++ b/src/main/java/org/chainoptim/desktop/core/organization/controller/AddNewMembersController.java @@ -28,7 +28,7 @@ public class AddNewMembersController implements Initializable { private Integer organizationId; @FXML - private VBox contentVBox; + private StackPane usersSelectionContainer; @FXML private StackPane fallbackContainer; @@ -50,6 +50,7 @@ public AddNewMembersController(OrganizationService organizationService, @Override public void initialize(URL location, ResourceBundle resourceBundle) { loadFallbackManager(); + loadUsersSearch(); setupListeners(); Integer receivedOrganizationId = currentSelectionService.getSelectedId(); @@ -70,10 +71,18 @@ private void loadFallbackManager() { fallbackContainer.getChildren().add(fallbackView); } + private void loadUsersSearch() { + Node usersSearchView = fxmlLoaderService.loadView( + "/org/chainoptim/desktop/core/user/PublicUsersSearchAndSelectionView.fxml", + controllerFactory::createController + ); + usersSelectionContainer.getChildren().add(usersSearchView); + } + private void setupListeners() { fallbackManager.isEmptyProperty().addListener((observable, oldValue, newValue) -> { - contentVBox.setVisible(newValue); - contentVBox.setManaged(newValue); + usersSelectionContainer.setVisible(newValue); + usersSelectionContainer.setManaged(newValue); fallbackContainer.setVisible(!newValue); fallbackContainer.setManaged(!newValue); }); diff --git a/src/main/java/org/chainoptim/desktop/core/user/controller/PublicUsersSearchAndSelectionController.java b/src/main/java/org/chainoptim/desktop/core/user/controller/PublicUsersSearchAndSelectionController.java new file mode 100644 index 00000000..b3e01fc2 --- /dev/null +++ b/src/main/java/org/chainoptim/desktop/core/user/controller/PublicUsersSearchAndSelectionController.java @@ -0,0 +1,167 @@ +package org.chainoptim.desktop.core.user.controller; + +import org.chainoptim.desktop.core.user.dto.UserSearchResultDTO; +import org.chainoptim.desktop.core.user.service.UserService; +import org.chainoptim.desktop.shared.search.model.PaginatedResults; +import com.google.inject.Inject; +import javafx.application.Platform; +import javafx.beans.property.SimpleIntegerProperty; +import javafx.fxml.FXML; +import javafx.fxml.Initializable; +import javafx.geometry.Pos; +import javafx.scene.control.Button; +import javafx.scene.control.Label; +import javafx.scene.control.TextField; +import javafx.scene.effect.ColorAdjust; +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; +import javafx.scene.layout.HBox; +import javafx.scene.layout.VBox; +import lombok.Getter; + +import java.net.URL; +import java.util.*; + +public class PublicUsersSearchAndSelectionController implements Initializable { + + private final UserService userService; + + // State + private final SimpleIntegerProperty currentPage = new SimpleIntegerProperty(1); + private int totalCount = 0; + @Getter + private List selectedUsers = new ArrayList<>(); + + // Constants + private static final int PAGE_SIZE = 1; + + @FXML + private VBox userResultsVBox; + @FXML + private TextField searchInput; + @FXML + private Button searchButton; + @FXML + private VBox selectedUsersVBox; + + // Icons + private Image removeIcon; + + @Inject + public PublicUsersSearchAndSelectionController(UserService userService) { + this.userService = userService; + } + + @Override + public void initialize(URL url, ResourceBundle resourceBundle) { + setupListeners(); + initializeUI(); + searchForUsers(); + } + + private void setupListeners() { + currentPage.addListener((observable, oldValue, newValue) -> searchForUsers()); + searchInput.setOnAction(event -> { + currentPage.set(1); + searchForUsers(); + }); + } + + private void initializeUI() { + // Search Icon + Image image = new Image(Objects.requireNonNull(getClass().getResourceAsStream("/img/search.png"))); + ImageView searchIconView = new ImageView(image); + ColorAdjust colorAdjust = new ColorAdjust(); + colorAdjust.setBrightness(1); + searchIconView.setEffect(colorAdjust); + searchButton.setGraphic(searchIconView); + searchButton.getStyleClass().add("search-button"); + searchButton.setOnAction(event -> { + currentPage.set(1); + searchForUsers(); + }); + + // Remove icon + removeIcon = new Image(Objects.requireNonNull(getClass().getResourceAsStream("/img/xmark-solid.png"))); + } + + private void searchForUsers() { + this.userService.searchPublicUsers(searchInput.getText(), currentPage.get(), PAGE_SIZE) + .thenApply(this::handleSearchResponse) + .exceptionally(this::handleSearchException); + } + + private Optional> handleSearchResponse(Optional> optionalPaginatedResults) { + Platform.runLater(() -> { + if (optionalPaginatedResults.isEmpty()) { + return; + } + PaginatedResults paginatedResults = optionalPaginatedResults.get(); + totalCount = (int) paginatedResults.getTotalCount(); + + // Render Users List + Next Page Button + userResultsVBox.getChildren().clear(); + + if (paginatedResults.getResults().isEmpty()) { + Label noResultsLabel = new Label("No results found."); + noResultsLabel.getStyleClass().add("general-label"); + userResultsVBox.getChildren().add(noResultsLabel); + return; + } + + for (UserSearchResultDTO user : paginatedResults.getResults()) { + Button userButton = new Button(user.getUsername()); + userButton.getStyleClass().add("general-label"); + userButton.setOnAction(event -> selectUser(user)); + userResultsVBox.getChildren().add(userButton); + } + + Button nextPageButton = new Button("Load More"); + nextPageButton.getStyleClass().add("pseudo-link"); + nextPageButton.setStyle("-fx-font-weight: bold; -fx-padding: 10px 0px;"); + nextPageButton.setOnAction(event -> currentPage.set(currentPage.get() + 1)); + userResultsVBox.getChildren().add(nextPageButton); + if (isAtTheEndOfResults()) { + nextPageButton.setDisable(true); + nextPageButton.setVisible(false); + } + }); + return optionalPaginatedResults; + } + + private Optional> handleSearchException(Throwable throwable) { + System.out.println("Error searching for users: " + throwable.getMessage()); + return Optional.empty(); + } + + private void selectUser(UserSearchResultDTO user) { + if (selectedUsers.stream().map(UserSearchResultDTO::getId).toList().contains(user.getId())) { + return; + } + + HBox selectedUserHBox = new HBox(); + Label selectedUserLabel = new Label(user.getUsername()); + selectedUserLabel.getStyleClass().add("general-label"); + + Button removeButton = new Button(); + ImageView removeIconView = new ImageView(removeIcon); + removeIconView.setFitWidth(12); + removeIconView.setFitHeight(12); + removeButton.setGraphic(removeIconView); + removeButton.getStyleClass().add("cancel-edit-button"); + removeButton.setOnAction(event -> { + selectedUsersVBox.getChildren().remove(selectedUserHBox); + selectedUsers.remove(user); + }); + + selectedUserHBox.getChildren().addAll(selectedUserLabel, removeButton); + selectedUserHBox.setAlignment(Pos.CENTER_LEFT); + selectedUserHBox.setSpacing(8); + selectedUsers.add(user); + selectedUsersVBox.getChildren().add(selectedUserHBox); + } + + private boolean isAtTheEndOfResults() { + return currentPage.get() >= Math.ceil((double) totalCount / PAGE_SIZE); + } +} diff --git a/src/main/java/org/chainoptim/desktop/core/user/dto/UserSearchResultDTO.java b/src/main/java/org/chainoptim/desktop/core/user/dto/UserSearchResultDTO.java new file mode 100644 index 00000000..b4116406 --- /dev/null +++ b/src/main/java/org/chainoptim/desktop/core/user/dto/UserSearchResultDTO.java @@ -0,0 +1,16 @@ +package org.chainoptim.desktop.core.user.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@Setter +@AllArgsConstructor +@NoArgsConstructor +public class UserSearchResultDTO { + private String id; + private String username; + private String email; +} diff --git a/src/main/java/org/chainoptim/desktop/core/user/service/UserService.java b/src/main/java/org/chainoptim/desktop/core/user/service/UserService.java index dd883f88..7d99a282 100644 --- a/src/main/java/org/chainoptim/desktop/core/user/service/UserService.java +++ b/src/main/java/org/chainoptim/desktop/core/user/service/UserService.java @@ -1,14 +1,19 @@ package org.chainoptim.desktop.core.user.service; +import org.chainoptim.desktop.core.user.dto.UserSearchResultDTO; import org.chainoptim.desktop.core.user.model.User; +import org.chainoptim.desktop.shared.search.model.PaginatedResults; import java.util.List; import java.util.Optional; import java.util.concurrent.CompletableFuture; public interface UserService { + // Read CompletableFuture> getUserByUsername(String username); CompletableFuture>> getUsersByCustomRoleId(Integer customRoleId); + CompletableFuture>> searchPublicUsers(String searchQuery, int page, int itemsPerPage); + // Write CompletableFuture> assignBasicRoleToUser(String userId, User.Role role); CompletableFuture> assignCustomRoleToUser(String userId, Integer roleId); CompletableFuture> removeUserFromOrganization(String userId, Integer organizationId); diff --git a/src/main/java/org/chainoptim/desktop/core/user/service/UserServiceImpl.java b/src/main/java/org/chainoptim/desktop/core/user/service/UserServiceImpl.java index 7eee4a04..76d02c1e 100644 --- a/src/main/java/org/chainoptim/desktop/core/user/service/UserServiceImpl.java +++ b/src/main/java/org/chainoptim/desktop/core/user/service/UserServiceImpl.java @@ -2,7 +2,9 @@ import org.chainoptim.desktop.core.user.dto.AssignBasicRoleDTO; import org.chainoptim.desktop.core.user.dto.AssignCustomRoleDTO; +import org.chainoptim.desktop.core.user.dto.UserSearchResultDTO; import org.chainoptim.desktop.core.user.model.User; +import org.chainoptim.desktop.shared.search.model.PaginatedResults; import org.chainoptim.desktop.shared.util.JsonUtil; import org.chainoptim.desktop.core.user.util.TokenManager; @@ -25,10 +27,11 @@ public class UserServiceImpl implements UserService { private static final String HEADER_KEY = "Authorization"; private static final String HEADER_VALUE_PREFIX = "Bearer "; + private static final String BASE_PATH = "http://localhost:8080/api/v1/users"; public CompletableFuture> getUserByUsername(String username) { String encodedUsername = URLEncoder.encode(username, StandardCharsets.UTF_8); - String routeAddress = "http://localhost:8080/api/v1/users/username/" + encodedUsername; + String routeAddress = BASE_PATH + "/username/" + encodedUsername; String jwtToken = TokenManager.getToken(); if (jwtToken == null) return new CompletableFuture<>(); @@ -55,7 +58,7 @@ public CompletableFuture> getUserByUsername(String username) { } public CompletableFuture>> getUsersByCustomRoleId(Integer customRoleId) { - String routeAddress = "http://localhost:8080/api/v1/users/search/custom-role/" + customRoleId; + String routeAddress = BASE_PATH + "/search/custom-role/" + customRoleId; String jwtToken = TokenManager.getToken(); if (jwtToken == null) return new CompletableFuture<>(); @@ -83,8 +86,36 @@ public CompletableFuture>> getUsersByCustomRoleId(Integer cu }); } + public CompletableFuture>> searchPublicUsers(String searchQuery, int page, int itemsPerPage) { + String encodedSearchQuery = URLEncoder.encode(searchQuery, StandardCharsets.UTF_8); + String routeAddress = BASE_PATH + "/search/public?searchQuery=" + encodedSearchQuery + "&page=" + page + "&itemsPerPage=" + itemsPerPage; + + String jwtToken = TokenManager.getToken(); + if (jwtToken == null) return new CompletableFuture<>(); + String headerValue = HEADER_VALUE_PREFIX + jwtToken; + + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(routeAddress)) + .headers(HEADER_KEY, headerValue) + .GET() + .build(); + + return client.sendAsync(request, HttpResponse.BodyHandlers.ofString()) + .thenApply(response -> { + if (response.statusCode() != HttpURLConnection.HTTP_OK) return Optional.>empty(); + try { + PaginatedResults userSearchResultDTO = JsonUtil.getObjectMapper().readValue(response.body(), new TypeReference>() {}); + return Optional.of(userSearchResultDTO); + } catch (Exception e) { + e.printStackTrace(); + } + return Optional.>empty(); + }); + } + + // Write public CompletableFuture> assignBasicRoleToUser(String userId, User.Role role) { - String routeAddress = "http://localhost:8080/api/v1/users/" + userId + "/assign-basic-role"; + String routeAddress = BASE_PATH + "/" + userId + "/assign-basic-role"; String jwtToken = TokenManager.getToken(); if (jwtToken == null) return new CompletableFuture<>(); @@ -120,7 +151,7 @@ public CompletableFuture> assignBasicRoleToUser(String userId, Us } public CompletableFuture> assignCustomRoleToUser(String userId, Integer roleId) { - String routeAddress = "http://localhost:8080/api/v1/users/" + userId + "/assign-custom-role"; + String routeAddress = BASE_PATH + "/" + userId + "/assign-custom-role"; String jwtToken = TokenManager.getToken(); if (jwtToken == null) return new CompletableFuture<>(); @@ -156,7 +187,7 @@ public CompletableFuture> assignCustomRoleToUser(String userId, I } public CompletableFuture> removeUserFromOrganization(String userId, Integer organizationId) { - String routeAddress = "http://localhost:8080/api/v1/users/" + userId + "/remove-from-organization/" + organizationId.toString(); + String routeAddress = BASE_PATH + "/" + userId + "/remove-from-organization/" + organizationId.toString(); String jwtToken = TokenManager.getToken(); if (jwtToken == null) return new CompletableFuture<>(); diff --git a/src/main/resources/org/chainoptim/desktop/core/organization/AddNewMembersView.fxml b/src/main/resources/org/chainoptim/desktop/core/organization/AddNewMembersView.fxml index 99a3cf75..259322b8 100644 --- a/src/main/resources/org/chainoptim/desktop/core/organization/AddNewMembersView.fxml +++ b/src/main/resources/org/chainoptim/desktop/core/organization/AddNewMembersView.fxml @@ -5,12 +5,13 @@ + prefHeight="400.0" prefWidth="600.0" styleClass="form-container"> - - + + + diff --git a/src/main/resources/org/chainoptim/desktop/core/user/PublicUsersSearchAndSelectionView.fxml b/src/main/resources/org/chainoptim/desktop/core/user/PublicUsersSearchAndSelectionView.fxml new file mode 100644 index 00000000..3ad70c34 --- /dev/null +++ b/src/main/resources/org/chainoptim/desktop/core/user/PublicUsersSearchAndSelectionView.fxml @@ -0,0 +1,28 @@ + + + + + + + + +