diff --git a/src/actions/users.js b/src/actions/users.js index 708ddd1f..92e54cc1 100644 --- a/src/actions/users.js +++ b/src/actions/users.js @@ -5,7 +5,10 @@ import { fetchMemberProjects } from '../services/projects' import { LOAD_ALL_USER_PROJECTS_PENDING, LOAD_ALL_USER_PROJECTS_SUCCESS, - LOAD_ALL_USER_PROJECTS_FAILURE + LOAD_ALL_USER_PROJECTS_FAILURE, + SEARCH_USER_PROJECTS_PENDING, + SEARCH_USER_PROJECTS_SUCCESS, + SEARCH_USER_PROJECTS_FAILURE } from '../config/constants' /** @@ -33,3 +36,41 @@ export function loadAllUserProjects (isAdmin = true) { })) } } + +/** + * Filter projects of the authenticated user + * + * @param {bool} isAdmin is admin + * @param {string} keyword search keyword + */ +export function searchUserProjects (isAdmin = true, keyword) { + return (dispatch) => { + if (!keyword) { + dispatch({ + type: SEARCH_USER_PROJECTS_SUCCESS, + projects: [] + }) + return + } + dispatch({ + type: SEARCH_USER_PROJECTS_PENDING + }) + + const filters = { + sort: 'updatedAt desc', + perPage: 20, + page: 1, + keyword + } + if (!isAdmin) { + filters['memberOnly'] = true + } + + fetchMemberProjects(filters).then(projects => dispatch({ + type: SEARCH_USER_PROJECTS_SUCCESS, + projects + })).catch(() => dispatch({ + type: SEARCH_USER_PROJECTS_FAILURE + })) + } +} diff --git a/src/components/Users/index.js b/src/components/Users/index.js index 1f9e7846..07d862dc 100644 --- a/src/components/Users/index.js +++ b/src/components/Users/index.js @@ -8,7 +8,7 @@ import UserCard from '../UserCard' import PrimaryButton from '../Buttons/PrimaryButton' import Modal from '../Modal' import SelectUserAutocomplete from '../SelectUserAutocomplete' -import { PROJECT_ROLES } from '../../config/constants' +import { PROJECT_ROLES, AUTOCOMPLETE_DEBOUNCE_TIME_MS } from '../../config/constants' import { checkAdmin } from '../../util/tc' import { addUserToProject, removeUserFromProject } from '../../services/projects' import ConfirmationModal from '../Modal/ConfirmationModal' @@ -31,7 +31,8 @@ class Users extends Component { isRemoving: false, removeError: null, showRemoveConfirmationModal: false, - userToRemove: null + userToRemove: null, + searchKey: '' } this.setProjectOption = this.setProjectOption.bind(this) this.onAddUserClick = this.onAddUserClick.bind(this) @@ -42,6 +43,9 @@ class Users extends Component { this.onRemoveClick = this.onRemoveClick.bind(this) this.resetRemoveUserState = this.resetRemoveUserState.bind(this) this.onRemoveConfirmClick = this.onRemoveConfirmClick.bind(this) + this.onInputChange = this.onInputChange.bind(this) + + this.debouncedOnInputChange = _.debounce(this.onInputChange, AUTOCOMPLETE_DEBOUNCE_TIME_MS) } setProjectOption (projectOption) { @@ -190,9 +194,31 @@ class Users extends Component { } } + /** + * Handler for the input which calls API for getting project suggestions + */ + onInputChange (inputValue, a, b, c) { + const { searchUserProjects } = this.props + const preparedValue = inputValue.trim() + searchUserProjects(preparedValue) + this.setState({ + searchKey: preparedValue + }) + } + render () { - const { projects, projectMembers, updateProjectNember, isEditable } = this.props - const projectOptions = projects.map(p => { + const { + projects, + projectMembers, + updateProjectNember, + isEditable, + isSearchingUserProjects, + resultSearchUserProjects + } = this.props + const { + searchKey + } = this.state + const projectOptions = ((searchKey ? resultSearchUserProjects : projects) || []).map(p => { return { label: p.name, value: p.id @@ -218,6 +244,10 @@ class Users extends Component { placeholder='Select a project' value={this.state.projectOption} onChange={(e) => { this.setProjectOption(e) }} + onInputChange={this.debouncedOnInputChange} + isLoading={isSearchingUserProjects} + filterOption={() => true} + noOptionsMessage={() => isSearchingUserProjects ? 'Searching...' : 'No options'} /> @@ -423,8 +453,11 @@ Users.propTypes = { addNewProjectMember: PropTypes.func.isRequired, auth: PropTypes.object, isEditable: PropTypes.bool, + isSearchingUserProjects: PropTypes.bool, projects: PropTypes.arrayOf(PropTypes.object), - projectMembers: PropTypes.arrayOf(PropTypes.object) + projectMembers: PropTypes.arrayOf(PropTypes.object), + searchUserProjects: PropTypes.func.isRequired, + resultSearchUserProjects: PropTypes.arrayOf(PropTypes.object) } export default Users diff --git a/src/config/constants.js b/src/config/constants.js index 6aff40cf..0bb1db0f 100644 --- a/src/config/constants.js +++ b/src/config/constants.js @@ -61,6 +61,10 @@ export const LOAD_ALL_USER_PROJECTS_SUCCESS = 'LOAD_ALL_USER_PROJECTS_SUCCESS' export const LOAD_ALL_USER_PROJECTS_PENDING = 'LOAD_ALL_USER_PROJECTS_PENDING' export const LOAD_ALL_USER_PROJECTS_FAILURE = 'LOAD_ALL_USER_PROJECTS_FAILURE' +export const SEARCH_USER_PROJECTS_SUCCESS = 'SEARCH_USER_PROJECTS_SUCCESS' +export const SEARCH_USER_PROJECTS_PENDING = 'SEARCH_USER_PROJECTS_PENDING' +export const SEARCH_USER_PROJECTS_FAILURE = 'SEARCH_USER_PROJECTS_FAILURE' + // project billingAccount export const LOAD_PROJECT_BILLING_ACCOUNT = 'LOAD_PROJECT_BILLING_ACCOUNT' export const LOAD_PROJECT_BILLING_ACCOUNT_PENDING = 'LOAD_PROJECT_BILLING_ACCOUNT_PENDING' diff --git a/src/containers/Users/index.js b/src/containers/Users/index.js index 8e45f089..7a1ecf3f 100644 --- a/src/containers/Users/index.js +++ b/src/containers/Users/index.js @@ -8,7 +8,8 @@ import { fetchProjectById } from '../../services/projects' import { checkAdmin } from '../../util/tc' import { - loadAllUserProjects + loadAllUserProjects, + searchUserProjects } from '../../actions/users' class Users extends Component { @@ -17,7 +18,8 @@ class Users extends Component { this.state = { loginUserRoleInProject: '', - projectMembers: null + projectMembers: null, + isAdmin: false } this.loadProject = this.loadProject.bind(this) this.updateProjectNember = this.updateProjectNember.bind(this) @@ -30,6 +32,9 @@ class Users extends Component { if (!isLoading) { const isAdmin = checkAdmin(token) loadAllUserProjects(isAdmin) + this.setState({ + isAdmin + }) } } @@ -108,10 +113,14 @@ class Users extends Component { render () { const { projects, - auth + auth, + searchUserProjects, + resultSearchUserProjects, + isSearchingUserProjects } = this.props const { - projectMembers + projectMembers, + isAdmin } = this.state return ( { + searchUserProjects(isAdmin, key) + }} /> ) } @@ -132,6 +147,8 @@ const mapStateToProps = ({ users, auth }) => { return { projects: users.allUserProjects, isLoading: users.isLoadingAllUserProjects, + resultSearchUserProjects: users.searchUserProjects, + isSearchingUserProjects: users.isSearchingUserProjects, auth, loggedInUser: auth.user, token: auth.token @@ -140,15 +157,19 @@ const mapStateToProps = ({ users, auth }) => { Users.propTypes = { projects: PT.arrayOf(PT.object), + resultSearchUserProjects: PT.arrayOf(PT.object), auth: PT.object, loggedInUser: PT.object, token: PT.string, isLoading: PT.bool, - loadAllUserProjects: PT.func.isRequired + isSearchingUserProjects: PT.bool, + loadAllUserProjects: PT.func.isRequired, + searchUserProjects: PT.func.isRequired } const mapDispatchToProps = { - loadAllUserProjects + loadAllUserProjects, + searchUserProjects } export default connect(mapStateToProps, mapDispatchToProps)(Users) diff --git a/src/reducers/users.js b/src/reducers/users.js index ad5e7192..8dc18247 100644 --- a/src/reducers/users.js +++ b/src/reducers/users.js @@ -4,22 +4,50 @@ import { LOAD_ALL_USER_PROJECTS_PENDING, LOAD_ALL_USER_PROJECTS_SUCCESS, - LOAD_ALL_USER_PROJECTS_FAILURE + LOAD_ALL_USER_PROJECTS_FAILURE, + SEARCH_USER_PROJECTS_PENDING, + SEARCH_USER_PROJECTS_SUCCESS, + SEARCH_USER_PROJECTS_FAILURE } from '../config/constants' const initialState = { allUserProjects: [], - isLoadProjectsSuccess: false + isLoadingAllUserProjects: false, + searchUserProjects: [], + isSearchingUserProjects: false } export default function (state = initialState, action) { switch (action.type) { case LOAD_ALL_USER_PROJECTS_SUCCESS: - return { ...state, allUserProjects: action.projects, isLoadingAllUserProjects: false } + return { + ...state, + allUserProjects: action.projects, + isLoadingAllUserProjects: false + } case LOAD_ALL_USER_PROJECTS_PENDING: return { ...state, isLoadingAllUserProjects: true } case LOAD_ALL_USER_PROJECTS_FAILURE: return { ...state, isLoadingAllUserProjects: false } + + case SEARCH_USER_PROJECTS_SUCCESS: + return { + ...state, + searchUserProjects: action.projects, + isSearchingUserProjects: false + } + case SEARCH_USER_PROJECTS_PENDING: + return { + ...state, + searchUserProjects: [], + isSearchingUserProjects: true + } + case SEARCH_USER_PROJECTS_FAILURE: + return { + ...state, + searchUserProjects: [], + isSearchingUserProjects: false + } default: return state }