diff --git a/frontend/src/components/dialog/manage-members-dialog.js b/frontend/src/components/dialog/manage-members-dialog.js index f1b35f23f7..e96d195bf5 100644 --- a/frontend/src/components/dialog/manage-members-dialog.js +++ b/frontend/src/components/dialog/manage-members-dialog.js @@ -1,21 +1,21 @@ import React, { Fragment } from 'react'; import PropTypes from 'prop-types'; -import { Button, Modal, ModalHeader, ModalBody, ModalFooter, Table, Input, Label, FormGroup } from 'reactstrap'; -import { Utils } from '../../utils/utils'; +import { Button, Modal, ModalHeader, ModalBody, ModalFooter } from 'reactstrap'; import { gettext } from '../../utils/constants'; -import { seafileAPI } from '../../utils/seafile-api'; -import RoleEditor from '../select-editor/role-editor'; -import UserSelect from '../user-select'; -import toaster from '../toast'; -import Loading from '../loading'; +import ListAndAddGroupMembers from '../list-and-add-group-members'; +import SearchGroupMembers from '../search-group-members'; import '../../css/manage-members-dialog.css'; const propTypes = { groupID: PropTypes.string.isRequired, - toggleManageMembersDialog: PropTypes.func.isRequired, - onGroupChanged: PropTypes.func.isRequired, isOwner: PropTypes.bool.isRequired, + toggleManageMembersDialog: PropTypes.func.isRequired +}; + +const MANAGEMENT_MODE= { + LIST_AND_ADD: 'list_and_add', + SEARCH: 'search' }; class ManageMembersDialog extends React.Component { @@ -23,224 +23,44 @@ class ManageMembersDialog extends React.Component { constructor(props) { super(props); this.state = { - isLoading: true, // first loading - isLoadingMore: false, - groupMembers: [], - page: 1, - perPage: 100, - hasNextPage: false, - selectedOption: null, - errMessage: [], - isItemFreezed: false, - searchGroupMemberInputValue: '', + currentMode: MANAGEMENT_MODE.LIST_AND_ADD }; } - componentDidMount() { - this.listGroupMembers(this.state.page); - } - - listGroupMembers = (page) => { - const { groupID } = this.props; - const { perPage, groupMembers } = this.state; - seafileAPI.listGroupMembers(groupID, page, perPage).then((res) => { - const members = res.data; - this.setState({ - isLoading: false, - isLoadingMore: false, - page: page, - hasNextPage: members.length < perPage ? false : true, - groupMembers: groupMembers.concat(members) - }); - }).catch(error => { - let errMessage = Utils.getErrorMsg(error); - toaster.danger(errMessage); - this.setState({ - isLoading: false, - isLoadingMore: false, - hasNextPage: false - }); - }); - } - - onSelectChange = (option) => { + changeMode = () => { this.setState({ - selectedOption: option, - errMessage: [], - }); - } - - addGroupMember = () => { - let emails = []; - for (let i = 0; i < this.state.selectedOption.length; i++) { - emails.push(this.state.selectedOption[i].email); - } - seafileAPI.addGroupMembers(this.props.groupID, emails).then((res) => { - const newMembers = res.data.success; - this.setState({ - groupMembers: [].concat(newMembers, this.state.groupMembers), - selectedOption: null, - }); - this.refs.userSelect.clearSelect(); - if (res.data.failed.length > 0) { - this.setState({ - errMessage: res.data.failed - }); - } - }).catch(error => { - let errMessage = Utils.getErrorMsg(error); - toaster.danger(errMessage); - }); - } - - handleSearchGroupMemberInputChange = (e) => { - this.setState({ - searchGroupMemberInputValue: e.target.value - }); - } - - searchGroupMember = () => { - seafileAPI.searchGroupMember(this.props.groupID, this.state.searchGroupMemberInputValue).then((res) => { - this.setState({ - groupMembers: res.data, - }); - }).catch(error => { - let errMessage = Utils.getErrorMsg(error); - toaster.danger(errMessage); - }); - } - - toggleItemFreezed = (isFreezed) => { - this.setState({ - isItemFreezed: isFreezed - }); - } - - toggle = () => { - this.props.toggleManageMembersDialog(); - } - - handleScroll = (event) => { - // isLoadingMore: to avoid repeated request - const { page, hasNextPage, isLoadingMore } = this.state; - if (hasNextPage && !isLoadingMore) { - const clientHeight = event.target.clientHeight; - const scrollHeight = event.target.scrollHeight; - const scrollTop = event.target.scrollTop; - const isBottom = (clientHeight + scrollTop + 1 >= scrollHeight); - if (isBottom) { // scroll to the bottom - this.setState({isLoadingMore: true}, () => { - this.listGroupMembers(page + 1); - }); - } - } - } - - changeMember = (targetMember) => { - this.setState({ - groupMembers: this.state.groupMembers.map((item) => { - if (item.email == targetMember.email) { - item = targetMember; - } - return item; - }) - }); - } - - deleteMember = (targetMember) => { - const groupMembers = this.state.groupMembers; - groupMembers.splice(groupMembers.indexOf(targetMember), 1); - this.setState({ - groupMembers: groupMembers + currentMode: this.state.currentMode == MANAGEMENT_MODE.LIST_AND_ADD ? + MANAGEMENT_MODE.SEARCH : MANAGEMENT_MODE.LIST_AND_ADD }); } render() { - const { isLoading, hasNextPage } = this.state; + const { currentMode } = this.state; + const { groupID, isOwner, toggleManageMembersDialog: toggle } = this.props; return ( - - {gettext('Manage group members')} - - -

{gettext('Add group member')}

-
- - {this.state.selectedOption ? - : - - } -
-
- -

{gettext('Search group member')}

-
- - {this.state.searchGroupMemberInputValue ? - : - - } -
-
- { - this.state.errMessage.length > 0 && - this.state.errMessage.map((item, index = 0) => { - return ( -
{item.error_msg}
- ); - }) - } -
- {isLoading ? : ( + + + {currentMode == MANAGEMENT_MODE.LIST_AND_ADD ? + gettext('Manage group members') : ( - - - - - - - - - - - { - this.state.groupMembers.length > 0 && - this.state.groupMembers.map((item, index) => { - return ( - - ); - }) - } - -
{gettext('Name')}{gettext('Role')}
- {hasNextPage && } + + {gettext('Search group members')}
- )} -
+ ) + } + + + {currentMode == MANAGEMENT_MODE.LIST_AND_ADD ? + : + + } - +
); @@ -249,107 +69,4 @@ class ManageMembersDialog extends React.Component { ManageMembersDialog.propTypes = propTypes; -const MemberPropTypes = { - item: PropTypes.object.isRequired, - changeMember: PropTypes.func.isRequired, - deleteMember: PropTypes.func.isRequired, - groupID: PropTypes.string.isRequired, - isOwner: PropTypes.bool.isRequired, -}; - -class Member extends React.PureComponent { - - constructor(props) { - super(props); - this.roles = ['Admin', 'Member']; - this.state = ({ - highlight: false, - }); - } - - onChangeUserRole = (role) => { - let isAdmin = role === 'Admin' ? 'True' : 'False'; - seafileAPI.setGroupAdmin(this.props.groupID, this.props.item.email, isAdmin).then((res) => { - this.props.changeMember(res.data); - }).catch(error => { - let errMessage = Utils.getErrorMsg(error); - toaster.danger(errMessage); - }); - } - - deleteMember = () => { - const { item } = this.props; - seafileAPI.deleteGroupMember(this.props.groupID, item.email).then((res) => { - this.props.deleteMember(item); - toaster.success(gettext('Successfully deleted {name}.').replace('{name}', item.name)); - }).catch(error => { - let errMessage = Utils.getErrorMsg(error); - toaster.danger(errMessage); - }); - } - - handleMouseOver = () => { - if (this.props.isItemFreezed) return; - this.setState({ - highlight: true, - }); - } - - handleMouseLeave = () => { - if (this.props.isItemFreezed) return; - this.setState({ - highlight: false, - }); - } - - translateRole = (role) => { - if (role === 'Admin') { - return gettext('Admin'); - } - else if (role === 'Member') { - return gettext('Member'); - } - else if (role === 'Owner') { - return gettext('Owner'); - } - } - - render() { - const { item, isOwner } = this.props; - const deleteAuthority = (item.role !== 'Owner' && isOwner === true) || (item.role === 'Member' && isOwner === false); - return( - - - {item.name} - - {((isOwner === false) || (isOwner === true && item.role === 'Owner')) && - {this.translateRole(item.role)} - } - {(isOwner === true && item.role !== 'Owner') && - - } - - - {(deleteAuthority && !this.props.isItemFreezed) && - - - } - - - ); - } -} - -Member.propTypes = MemberPropTypes; - - export default ManageMembersDialog; diff --git a/frontend/src/components/group-members.js b/frontend/src/components/group-members.js new file mode 100644 index 0000000000..883ba6bb40 --- /dev/null +++ b/frontend/src/components/group-members.js @@ -0,0 +1,165 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Table } from 'reactstrap'; +import { Utils } from '../utils/utils'; +import { gettext } from '../utils/constants'; +import { seafileAPI } from '../utils/seafile-api'; +import RoleEditor from './select-editor/role-editor'; +import toaster from './toast'; +import OpIcon from './op-icon'; + +const propTypes = { + groupMembers: PropTypes.array.isRequired, + groupID: PropTypes.string.isRequired, + isOwner: PropTypes.bool.isRequired, + isItemFreezed: PropTypes.bool.isRequired, + toggleItemFreezed: PropTypes.func.isRequired, + changeMember: PropTypes.func.isRequired, + deleteMember: PropTypes.func.isRequired +}; + +class GroupMembers extends React.Component { + + render() { + const { groupMembers, changeMember, deleteMember, groupID, isOwner, isItemFreezed, toggleItemFreezed } = this.props; + return ( + + + + + + + + + + + {groupMembers.map((item, index) => { + return ( + + ); + }) + } + +
{gettext('Name')}{gettext('Role')}
+ ); + } +} + +GroupMembers.propTypes = propTypes; + +const MemberPropTypes = { + item: PropTypes.object.isRequired, + changeMember: PropTypes.func.isRequired, + deleteMember: PropTypes.func.isRequired, + toggleItemFreezed: PropTypes.func.isRequired, + groupID: PropTypes.string.isRequired, + isOwner: PropTypes.bool.isRequired, + isItemFreezed: PropTypes.bool.isRequired +}; + +class Member extends React.PureComponent { + + constructor(props) { + super(props); + this.roles = ['Admin', 'Member']; + this.state = ({ + highlight: false, + }); + } + + onChangeUserRole = (role) => { + let isAdmin = role === 'Admin' ? 'True' : 'False'; + seafileAPI.setGroupAdmin(this.props.groupID, this.props.item.email, isAdmin).then((res) => { + this.props.changeMember(res.data); + }).catch(error => { + let errMessage = Utils.getErrorMsg(error); + toaster.danger(errMessage); + }); + } + + deleteMember = () => { + const { item } = this.props; + seafileAPI.deleteGroupMember(this.props.groupID, item.email).then((res) => { + this.props.deleteMember(item); + toaster.success(gettext('Successfully deleted {name}.').replace('{name}', item.name)); + }).catch(error => { + let errMessage = Utils.getErrorMsg(error); + toaster.danger(errMessage); + }); + } + + handleMouseOver = () => { + if (this.props.isItemFreezed) return; + this.setState({ + highlight: true, + }); + } + + handleMouseLeave = () => { + if (this.props.isItemFreezed) return; + this.setState({ + highlight: false, + }); + } + + translateRole = (role) => { + if (role === 'Admin') { + return gettext('Admin'); + } + else if (role === 'Member') { + return gettext('Member'); + } + else if (role === 'Owner') { + return gettext('Owner'); + } + } + + render() { + const { item, isOwner } = this.props; + const deleteAuthority = (item.role !== 'Owner' && isOwner === true) || (item.role === 'Member' && isOwner === false); + return( + + + {item.name} + + {((isOwner === false) || (isOwner === true && item.role === 'Owner')) && + {this.translateRole(item.role)} + } + {(isOwner === true && item.role !== 'Owner') && + + } + + + {(deleteAuthority && this.state.highlight) && + + } + + + ); + } +} + +Member.propTypes = MemberPropTypes; + + +export default GroupMembers; diff --git a/frontend/src/components/list-and-add-group-members.js b/frontend/src/components/list-and-add-group-members.js new file mode 100644 index 0000000000..507d541e4f --- /dev/null +++ b/frontend/src/components/list-and-add-group-members.js @@ -0,0 +1,187 @@ +import React, { Fragment } from 'react'; +import PropTypes from 'prop-types'; +import { Button } from 'reactstrap'; +import { Utils } from '../utils/utils'; +import { gettext } from '../utils/constants'; +import { seafileAPI } from '../utils/seafile-api'; +import UserSelect from './user-select'; +import toaster from './toast'; +import Loading from './loading'; +import GroupMembers from './group-members'; + +const propTypes = { + groupID: PropTypes.string.isRequired, + isOwner: PropTypes.bool.isRequired, + changeMode: PropTypes.func.isRequired +}; + +class ManageMembersDialog extends React.Component { + + constructor(props) { + super(props); + this.state = { + isLoading: true, // first loading + isLoadingMore: false, + groupMembers: [], + page: 1, + perPage: 100, + hasNextPage: false, + selectedOption: null, + errMessage: [], + isItemFreezed: false + }; + } + + componentDidMount() { + this.listGroupMembers(this.state.page); + } + + listGroupMembers = (page) => { + const { groupID } = this.props; + const { perPage, groupMembers } = this.state; + seafileAPI.listGroupMembers(groupID, page, perPage).then((res) => { + const members = res.data; + this.setState({ + isLoading: false, + isLoadingMore: false, + page: page, + hasNextPage: members.length < perPage ? false : true, + groupMembers: groupMembers.concat(members) + }); + }).catch(error => { + let errMessage = Utils.getErrorMsg(error); + toaster.danger(errMessage); + this.setState({ + isLoading: false, + isLoadingMore: false, + hasNextPage: false + }); + }); + } + + onSelectChange = (option) => { + this.setState({ + selectedOption: option, + errMessage: [], + }); + } + + addGroupMember = () => { + let emails = []; + for (let i = 0; i < this.state.selectedOption.length; i++) { + emails.push(this.state.selectedOption[i].email); + } + seafileAPI.addGroupMembers(this.props.groupID, emails).then((res) => { + const newMembers = res.data.success; + this.setState({ + groupMembers: [].concat(newMembers, this.state.groupMembers), + selectedOption: null, + }); + this.refs.userSelect.clearSelect(); + if (res.data.failed.length > 0) { + this.setState({ + errMessage: res.data.failed + }); + } + }).catch(error => { + let errMessage = Utils.getErrorMsg(error); + toaster.danger(errMessage); + }); + } + + toggleItemFreezed = (isFreezed) => { + this.setState({ + isItemFreezed: isFreezed + }); + } + + handleScroll = (event) => { + // isLoadingMore: to avoid repeated request + const { page, hasNextPage, isLoadingMore } = this.state; + if (hasNextPage && !isLoadingMore) { + const clientHeight = event.target.clientHeight; + const scrollHeight = event.target.scrollHeight; + const scrollTop = event.target.scrollTop; + const isBottom = (clientHeight + scrollTop + 1 >= scrollHeight); + if (isBottom) { // scroll to the bottom + this.setState({isLoadingMore: true}, () => { + this.listGroupMembers(page + 1); + }); + } + } + } + + changeMember = (targetMember) => { + this.setState({ + groupMembers: this.state.groupMembers.map((item) => { + if (item.email == targetMember.email) { + item = targetMember; + } + return item; + }) + }); + } + + deleteMember = (targetMember) => { + const groupMembers = this.state.groupMembers; + groupMembers.splice(groupMembers.indexOf(targetMember), 1); + this.setState({ + groupMembers: groupMembers + }); + } + + render() { + const { isLoading, hasNextPage, groupMembers } = this.state; + return ( + +

{gettext('Add group member')}

+
+ + {this.state.selectedOption ? + : + + } +
+ { + this.state.errMessage.length > 0 && + this.state.errMessage.map((item, index = 0) => { + return ( +
{item.error_msg}
+ ); + }) + } + {groupMembers.length > 10 && +
+ +
+ } +
+ {isLoading ? : ( + + + {hasNextPage && } + + )} +
+
+ ); + } +} + +ManageMembersDialog.propTypes = propTypes; + +export default ManageMembersDialog; diff --git a/frontend/src/components/op-icon.js b/frontend/src/components/op-icon.js new file mode 100644 index 0000000000..b486bc81dc --- /dev/null +++ b/frontend/src/components/op-icon.js @@ -0,0 +1,29 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Utils } from '../utils/utils'; + +const propTypes = { + className: PropTypes.string.isRequired, + op: PropTypes.func, + title: PropTypes.string.isRequired +}; + +class OpIcon extends React.Component { + + render() { + const { className, op, title } = this.props; + return (); + } +} + +OpIcon.propTypes = propTypes; + +export default OpIcon; diff --git a/frontend/src/components/search-group-members.js b/frontend/src/components/search-group-members.js new file mode 100644 index 0000000000..fb8bf3b6f0 --- /dev/null +++ b/frontend/src/components/search-group-members.js @@ -0,0 +1,130 @@ +import React, { Fragment } from 'react'; +import PropTypes from 'prop-types'; +import { Input, Button } from 'reactstrap'; +import { Utils } from '../utils/utils'; +import { gettext } from '../utils/constants'; +import { seafileAPI } from '../utils/seafile-api'; +import Loading from './loading'; +import GroupMembers from './group-members'; + +const propTypes = { + groupID: PropTypes.string.isRequired, + isOwner: PropTypes.bool.isRequired +}; + +class SearchGroupMembers extends React.Component { + + // pagination is not needed + constructor(props) { + super(props); + this.state = { + isLoading: false, + q: '', // query + groupMembers: [], + errMessage: [], + isItemFreezed: false + }; + } + + toggleItemFreezed = (isFreezed) => { + this.setState({ + isItemFreezed: isFreezed + }); + } + + changeMember = (targetMember) => { + this.setState({ + groupMembers: this.state.groupMembers.map((item) => { + if (item.email == targetMember.email) { + item = targetMember; + } + return item; + }) + }); + } + + deleteMember = (targetMember) => { + const groupMembers = this.state.groupMembers; + groupMembers.splice(groupMembers.indexOf(targetMember), 1); + this.setState({ + groupMembers: groupMembers + }); + } + + submit = () => { + let { q } = this.state; + q = q.trim(); + if (!q) { + return; + } + this.setState({ + isLoading: true + }); + seafileAPI.searchGroupMember(this.props.groupID, q).then((res) => { + this.setState({ + isLoading: false, + groupMembers: res.data, + errorMsg: '' + }); + }).catch(error => { + let errMessage = Utils.getErrorMsg(error); + this.setState({ + isLoading: false, + errorMsg: errMessage + }); + }); + } + + onInputChange = (e) => { + this.setState({ + q: e.target.value + }); + } + + onInputKeyDown = (e) => { + if (e.key == 'Enter') { + this.submit(); + } + } + + render() { + const { isLoading, q, errorMsg, groupMembers } = this.state; + return ( + +
+ + +
+ {errorMsg &&

{errorMsg}

} +
+ {isLoading ? : ( + + {groupMembers.length > 0 && ( + + )} + + )} +
+
+ ); + } +} + +SearchGroupMembers.propTypes = propTypes; + +export default SearchGroupMembers; diff --git a/frontend/src/css/manage-members-dialog.css b/frontend/src/css/manage-members-dialog.css index 8e553c1722..4a30ac72dc 100644 --- a/frontend/src/css/manage-members-dialog.css +++ b/frontend/src/css/manage-members-dialog.css @@ -8,47 +8,27 @@ vertical-align: middle; text-align: left; } -.toggle-group-admin-icon, -.delete-group-member-icon { - opacity: 0; - cursor: pointer; - color: #888; - margin-left: 5px; -} -.manage-members-table tr:hover .toggle-group-admin-icon, -.manage-members-table tr:hover .delete-group-member-icon { - opacity: 1; -} - -.manage-members-table .editing { - background-color: rgba(0, 0, 0, 0.04); -} - -.toggle-group-admin-icon:hover, -.delete-group-member-icon:hover { - color: #333; -} .add-members-select .true__indicator-separator { display: none; } -.add-members, -.search-members { +.add-members { display: flex; justify-content: space-between; } -.add-members .add-members-select, -.search-members .search-members-input { +.add-members .add-members-select { width: 385px; } - -.add-members .btn, -.search-members .btn { +.add-members .btn { width: 75px; } - .group-error { margin-top: 10px; -} \ No newline at end of file +} + +.group-manage-members-dialog .back-icon { + color: #999; + cursor: pointer; +} diff --git a/frontend/src/utils/utils.js b/frontend/src/utils/utils.js index 25ff7edda8..443e5b9855 100644 --- a/frontend/src/utils/utils.js +++ b/frontend/src/utils/utils.js @@ -1382,4 +1382,11 @@ export const Utils = { return level; }, + // for a11y + onKeyDown: function(e) { + if (e.key == 'Enter' || e.key == 'Space') { + e.target.click(); + } + } + };