Skip to content

Commit

Permalink
Add reusable view for searching and selecting for users
Browse files Browse the repository at this point in the history
  • Loading branch information
TudorOrban committed Mar 27, 2024
1 parent b1d3ce7 commit fe06980
Show file tree
Hide file tree
Showing 7 changed files with 269 additions and 12 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ public class AddNewMembersController implements Initializable {
private Integer organizationId;

@FXML
private VBox contentVBox;
private StackPane usersSelectionContainer;
@FXML
private StackPane fallbackContainer;

Expand All @@ -50,6 +50,7 @@ public AddNewMembersController(OrganizationService organizationService,
@Override
public void initialize(URL location, ResourceBundle resourceBundle) {
loadFallbackManager();
loadUsersSearch();
setupListeners();

Integer receivedOrganizationId = currentSelectionService.getSelectedId();
Expand All @@ -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);
});
Expand Down
Original file line number Diff line number Diff line change
@@ -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<UserSearchResultDTO> 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<PaginatedResults<UserSearchResultDTO>> handleSearchResponse(Optional<PaginatedResults<UserSearchResultDTO>> optionalPaginatedResults) {
Platform.runLater(() -> {
if (optionalPaginatedResults.isEmpty()) {
return;
}
PaginatedResults<UserSearchResultDTO> 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<PaginatedResults<UserSearchResultDTO>> 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);
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
@@ -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<Optional<User>> getUserByUsername(String username);
CompletableFuture<Optional<List<User>>> getUsersByCustomRoleId(Integer customRoleId);
CompletableFuture<Optional<PaginatedResults<UserSearchResultDTO>>> searchPublicUsers(String searchQuery, int page, int itemsPerPage);
// Write
CompletableFuture<Optional<User>> assignBasicRoleToUser(String userId, User.Role role);
CompletableFuture<Optional<User>> assignCustomRoleToUser(String userId, Integer roleId);
CompletableFuture<Optional<User>> removeUserFromOrganization(String userId, Integer organizationId);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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<Optional<User>> 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<>();
Expand All @@ -55,7 +58,7 @@ public CompletableFuture<Optional<User>> getUserByUsername(String username) {
}

public CompletableFuture<Optional<List<User>>> 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<>();
Expand Down Expand Up @@ -83,8 +86,36 @@ public CompletableFuture<Optional<List<User>>> getUsersByCustomRoleId(Integer cu
});
}

public CompletableFuture<Optional<PaginatedResults<UserSearchResultDTO>>> 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.<PaginatedResults<UserSearchResultDTO>>empty();
try {
PaginatedResults<UserSearchResultDTO> userSearchResultDTO = JsonUtil.getObjectMapper().readValue(response.body(), new TypeReference<PaginatedResults<UserSearchResultDTO>>() {});
return Optional.of(userSearchResultDTO);
} catch (Exception e) {
e.printStackTrace();
}
return Optional.<PaginatedResults<UserSearchResultDTO>>empty();
});
}

// Write
public CompletableFuture<Optional<User>> 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<>();
Expand Down Expand Up @@ -120,7 +151,7 @@ public CompletableFuture<Optional<User>> assignBasicRoleToUser(String userId, Us
}

public CompletableFuture<Optional<User>> 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<>();
Expand Down Expand Up @@ -156,7 +187,7 @@ public CompletableFuture<Optional<User>> assignCustomRoleToUser(String userId, I
}

public CompletableFuture<Optional<User>> 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<>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,13 @@
<VBox xmlns="http://javafx.com/javafx"
xmlns:fx="http://javafx.com/fxml"
fx:controller="org.chainoptim.desktop.core.organization.controller.AddNewMembersController"
prefHeight="400.0" prefWidth="600.0">
prefHeight="400.0" prefWidth="600.0" styleClass="form-container">

<VBox fx:id="contentVBox">
<Label text="Add New Members" />
<Label text="Add New Members" styleClass="form-label"/>

</VBox>
<StackPane fx:id="usersSelectionContainer">

</StackPane>

<StackPane fx:id="fallbackContainer"/>
</VBox>
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.layout.*?>

<?import javafx.scene.control.TextField?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.Label?>
<VBox xmlns="http://javafx.com/javafx"
xmlns:fx="http://javafx.com/fxml"
fx:controller="org.chainoptim.desktop.core.user.controller.PublicUsersSearchAndSelectionController"
prefHeight="360.0" prefWidth="240.0" spacing="4">

<Label text="Invite Existing Users" styleClass="general-label"/>

<HBox>
<TextField promptText="Search for Existing Users" fx:id="searchInput" HBox.hgrow="ALWAYS" style="-fx-pref-height: 32px; -fx-pref-width: 240px;"/>
<Button fx:id="searchButton" />
</HBox>

<VBox fx:id="userResultsVBox" style="-fx-padding: 10px 0px;">

</VBox>

<Label text="Selected Users" styleClass="general-label"/>

<VBox fx:id="selectedUsersVBox" spacing="4">

</VBox>
</VBox>

0 comments on commit fe06980

Please sign in to comment.