Skip to content

Commit

Permalink
I18N-1291 - Mojito user management table should be searchable (#155)
Browse files Browse the repository at this point in the history
Made user management table searchable
  • Loading branch information
DarKhaos authored Oct 11, 2024
1 parent 99bca45 commit adaecde
Show file tree
Hide file tree
Showing 15 changed files with 428 additions and 61 deletions.
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
package com.box.l10n.mojito.rest.security;

import static com.box.l10n.mojito.rest.security.UserSpecification.enabledEquals;
import static com.box.l10n.mojito.rest.security.UserSpecification.usernameEquals;
import static com.box.l10n.mojito.specification.Specifications.ifParamNotNull;
import static org.slf4j.LoggerFactory.getLogger;
import static org.springframework.data.jpa.domain.Specification.where;

import com.box.l10n.mojito.entity.security.user.Authority;
import com.box.l10n.mojito.entity.security.user.User;
Expand Down Expand Up @@ -48,11 +44,9 @@ public class UserWS {
@RequestMapping(value = "/api/users", method = RequestMethod.GET)
public Page<User> getUsers(
@RequestParam(value = "username", required = false) String username,
@RequestParam(value = "search", required = false) String search,
@PageableDefault(sort = "username", direction = Sort.Direction.ASC) Pageable pageable) {
Page<User> users =
userService.findAll(
where(ifParamNotNull(usernameEquals(username))).and(enabledEquals(true)), pageable);
return users;
return userService.findByUsernameOrName(username, search, pageable);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
import org.springframework.data.jpa.repository.EntityGraph.EntityGraphType;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.data.rest.core.annotation.RepositoryRestResource;

/**
Expand All @@ -37,4 +39,21 @@ public interface UserRepository extends JpaRepository<User, Long>, JpaSpecificat
@Override
@EntityGraph(value = "User.legacy", type = EntityGraphType.FETCH)
Optional<User> findById(Long aLong);

@Query(
"""
select u
from User u
where (:username is null or u.username = :username)
and ((:search is null or :search = '')
or (lower(u.username) like lower(concat('%', :search, '%'))
or (u.commonName is not null
and u.commonName <> ''
and lower(u.commonName) like lower(concat('%', :search, '%')))
or ((u.commonName is null or u.commonName = '')
and lower(concat(u.givenName, ' ', u.surname)) like lower(concat('%', :search, '%')))))
and u.enabled = true
""")
Page<User> findByUsernameOrName(
@Param("username") String username, @Param("search") String search, Pageable pageable);
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Component;
Expand Down Expand Up @@ -367,11 +366,12 @@ public User getOrCreateOrUpdateBasicUser(
* Cannot use an EntityGraph with pagination as it triggers the following warning: HHH90003004:
* firstResult/maxResults specified with collection fetch; applying in memory
*
* @param spec
* @param username
* @param search
* @param pageable
*/
public Page<User> findAll(Specification<User> spec, Pageable pageable) {
final Page<User> users = userRepository.findAll(spec, pageable);
public Page<User> findByUsernameOrName(String username, String search, Pageable pageable) {
final Page<User> users = userRepository.findByUsernameOrName(username, search, pageable);
users.forEach(
u -> {
Hibernate.initialize(u.getAuthorities());
Expand Down
3 changes: 3 additions & 0 deletions webapp/src/main/resources/properties/en.properties
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,9 @@ branches.searchstatusDropdown.owner.onlyMyBranches=Only my branches
# Branches search input placeholder, shown when no text is entered in the search field. To search branches by name or by username
branches.searchtext=Branch name or username...

# User search input placeholder, shown when no text is entered in the search field. To search users by username or by name
user.searchtext=Username or name...

# Label displayed as the title of the modal popup used to upload a screenshot
branches.screenshotUploadModal.title=Upload screenshot for selected textunits

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,6 @@ class UserActions {
"checkUsernameTaken",
"checkUsernameTakenSuccess",
"checkUsernameTakenError",
"getAllUsers",
"getAllUsersSuccess",
"getAllUsersError",
"deleteRequest",
"deleteRequestSuccess",
"deleteRequestError",
Expand All @@ -26,6 +23,9 @@ class UserActions {
"saveEditRequest",
"saveEditRequestSuccess",
"saveEditRequestError",
"getUsers",
"getUsersSuccess",
"getUsersError",
);
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,20 @@
import UserClient from "../../sdk/UserClient";
import UserActions from "./UserActions";
import UserModalActions from "./UserModalActions";
import UserSearcherParameters from "../../sdk/UserSearcherParameters";
import UserSearchParamStore from "../../stores/users/UserSearchParamStore";

const UserDataSource = {
getAllUsers: {
getUsers: {
remote(userStoreState, pageRequestParams) {
return UserClient.getUsers(pageRequestParams.page, pageRequestParams.size);
const userSearcherParameters = new UserSearcherParameters();
const { searchText } = UserSearchParamStore.getState();
const { page, size } = pageRequestParams;
userSearcherParameters.search(searchText).page(page).size(size);
return UserClient.getUsers(userSearcherParameters);
},

success: UserActions.getAllUsersSuccess,
error: UserActions.getAllUsersError
success: UserActions.getUsersSuccess,
error: UserActions.getUsersError
},

checkUsernameTakenRequest: {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import alt from "../../alt";

class UserSearchParamActions {
constructor() {
this.generateActions(
"changeSearchText",
"resetUserSearchParams",
);
}
}

export default alt.createActions(UserSearchParamActions);
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import React from "react";
import {injectIntl} from "react-intl";
import PropTypes from "prop-types";
import keycode from "keycode";
import {Button, FormControl, FormGroup, Glyphicon, InputGroup} from "react-bootstrap";

class SearchText extends React.Component {
static propTypes = {
"searchText": PropTypes.string.isRequired,
"isSpinnerShown": PropTypes.bool.isRequired,
"onSearchTextChanged": PropTypes.func.isRequired,
"onPerformSearch": PropTypes.func.isRequired,
"placeholderTextId": PropTypes.string.isRequired
}

onKeyDownOnSearchText(e) {
e.stopPropagation();
if (e.keyCode === keycode("enter")) {
this.props.onPerformSearch();
}
}

onSearchButtonClicked() {
this.props.onPerformSearch();
}

renderSearchButton() {
return (
<Button onClick={() => this.onSearchButtonClicked()}>
<Glyphicon glyph='glyphicon glyphicon-search'/>
</Button>
);
}

render() {
return (
<div className="search-text">
<FormGroup>
<InputGroup>
<FormControl type='text' value={this.props.searchText}
onChange={(e) => this.props.onSearchTextChanged(e.target.value)}
placeholder={this.props.intl.formatMessage({id: this.props.placeholderTextId})}
onKeyDown={(e) => this.onKeyDownOnSearchText(e)}/>
<InputGroup>
{this.props.isSpinnerShown && (<span className="glyphicon glyphicon-refresh spinning"/>)}
</InputGroup>
<InputGroup.Button>{this.renderSearchButton()}</InputGroup.Button>
</InputGroup>
</FormGroup>
</div>
);
}
}
export default injectIntl(SearchText);
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
import React from "react";
import {FormattedMessage, FormattedDate, FormattedTime, injectIntl, __esModule} from "react-intl";
import {FormattedMessage, FormattedDate, FormattedTime, injectIntl} from "react-intl";
import {Table, Button, OverlayTrigger, Popover, DropdownButton, MenuItem} from "react-bootstrap";
import UserActions from "../../actions/users/UserActions";
import UserStore from "../../stores/users/UserStore";
import User from "../../sdk/entity/User";
import UserModal from "./UserModal";
import UserDeleteModal from "./UserDeleteModal";
import { UserRole } from "./UserRole";
import UserErrorModal from "./UserErrorModal";
import AltContainer from "alt-container";
import UserModalStore from "../../stores/users/UserModalStore";
import UserModalActions from "../../actions/users/UserModalActions";
import SearchText from "../common/SearchText";
import UserSearchParamActions from "../../actions/users/UserSearchParamActions";
import UserSearchParamStore from "../../stores/users/UserSearchParamStore";

class UserMainPage extends React.Component {

Expand Down Expand Up @@ -168,6 +170,13 @@ class UserMainPage extends React.Component {
<div style={{gridArea: "count", justifySelf: "start"}} className="mbl">
<h4><FormattedMessage values={{numUsers: this.numUsers()}} id="users.count" /></h4>
</div>
<div style={{gridArea: "search", justifySelf: "center", width: "100%"}}>
<AltContainer store={UserSearchParamStore}>
<SearchText onSearchTextChanged={(text) => UserSearchParamActions.changeSearchText(text)}
onPerformSearch={() => UserActions.getUsers()}
placeholderTextId="user.searchtext" />
</AltContainer>
</div>
{this.renderPageBar()}
{this.renderUsersTable()}
<AltContainer stores={{user: UserStore, modal: UserModalStore}}>
Expand Down
17 changes: 2 additions & 15 deletions webapp/src/main/resources/public/js/sdk/UserClient.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,8 @@ import User from "./entity/User";
import UserPage from "./UsersPage";

class UserClient extends BaseClient {

/**
* @param {Number} page
* @param {Number} size
* @returns {UserPage}
*/
getUsers(page = 0, size = 5) {
let promise = this.get(this.getUrl(), {
"page": page,
"size": size
});

return promise.then(function (result) {
return UserPage.toUserPage(result);
});
getUsers(userSearcherParameters) {
return this.get(this.getUrl(), userSearcherParameters.getParams());
}

checkUsernameTaken(username) {
Expand Down
24 changes: 24 additions & 0 deletions webapp/src/main/resources/public/js/sdk/UserSearcherParameters.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
export default class UserSearcherParameters {
constructor() {
this.params = {};
}

search(searchText) {
this.params.search = searchText;
return this;
}

page(page) {
this.params.page = page;
return this;
}

size(size) {
this.params.size = size;
return this;
}

getParams() {
return this.params;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import alt from "../../alt";
import UserSearchParamActions from "../../actions/users/UserSearchParamActions";
import UserActions from "../../actions/users/UserActions";

class UserSearchParamStore {
constructor() {
this.setDefaultState();

this.bindActions(UserSearchParamActions);
this.bindActions(UserActions);
}

setDefaultState() {
this.searchText = "";
this.isSpinnerShown = false;
}

changeSearchText(text) {
this.searchText = text;
}

resetUserSearchParams() {
this.setDefaultState();
}

getUsers() {
this.isSpinnerShown = true;
}

getUsersSuccess() {
this.isSpinnerShown = false;
}

getUsersError() {
this.isSpinnerShown = false;
}
}

export default alt.createStore(UserSearchParamStore, "UserSearchParamStore");
Loading

0 comments on commit adaecde

Please sign in to comment.