diff --git a/frontend/src/components/dialog/manage-members-dialog.js b/frontend/src/components/dialog/manage-members-dialog.js
index 3885013b3d..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 } 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,187 +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
+ 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);
- });
- }
-
- 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 ?
- :
-
- }
-
- {
- 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') : (
-
-
-
- |
- {gettext('Name')} |
- {gettext('Role')} |
- |
-
-
-
- {
- this.state.groupMembers.length > 0 &&
- this.state.groupMembers.map((item, index) => {
- return (
-
- );
- })
- }
-
-
- {hasNextPage && }
+
+ {gettext('Search group members')}
- )}
-
+ )
+ }
+
+
+ {currentMode == MANAGEMENT_MODE.LIST_AND_ADD ?
+ :
+
+ }
-
+
);
@@ -212,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 (
+
+
+
+ |
+ {gettext('Name')} |
+ {gettext('Role')} |
+ |
+
+
+
+ {groupMembers.map((item, index) => {
+ return (
+
+ );
+ })
+ }
+
+
+ );
+ }
+}
+
+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..ee607f201f
--- /dev/null
+++ b/frontend/src/components/list-and-add-group-members.js
@@ -0,0 +1,188 @@
+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 265f31bdd2..1220f5ff71 100644
--- a/frontend/src/css/manage-members-dialog.css
+++ b/frontend/src/css/manage-members-dialog.css
@@ -1,33 +1,13 @@
.manage-members {
max-height: 300px;
overflow-y: scroll;
- margin: 1rem 0 2rem;
+ margin: 10px 0 2rem;
}
.manage-members-table th,
.manage-members-table td {
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;
@@ -46,4 +26,20 @@
}
.group-error {
margin-top: 10px;
-}
\ No newline at end of file
+}
+
+.group-manage-members-dialog .back-icon {
+ color: #999;
+ cursor: pointer;
+}
+
+.group-manage-members-dialog .search-group-members-btn {
+ font-size: 14px;
+ font-weight: normal;
+ background: #f1f1f1;
+ margin: 12px 0 0;
+}
+
+.group-manage-members-dialog .search-group-members-btn:hover {
+ background: #dbdbdb;
+}
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();
+ }
+ }
+
};
diff --git a/seahub/api2/endpoints/group_members.py b/seahub/api2/endpoints/group_members.py
index 725a1fe4d5..b5e0726a30 100644
--- a/seahub/api2/endpoints/group_members.py
+++ b/seahub/api2/endpoints/group_members.py
@@ -133,10 +133,42 @@ class GroupMembers(APIView):
return Response(member_info, status=status.HTTP_201_CREATED)
+class GroupSearchMember(APIView):
+
+ authentication_classes = (TokenAuthentication, SessionAuthentication)
+ permission_classes = (IsAuthenticated,)
+ throttle_classes = (UserRateThrottle,)
+
+ @api_check_group
+ def get(self, request, group_id, format=None):
+ """
+ Search group member by email.
+ """
+
+ q = request.GET.get('q', '')
+ if not q:
+ error_msg = 'q invalid.'
+ return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
+
+ if not is_group_member(group_id, request.user.username):
+ error_msg = 'Permission denied.'
+ return api_error(status.HTTP_403_FORBIDDEN, error_msg)
+
+ group_members = []
+ members = ccnet_api.search_group_members(group_id, q)
+ for member in members:
+
+ member_info = get_group_member_info(request, group_id, member.user_name)
+
+ group_members.append(member_info)
+
+ return Response(group_members)
+
+
class GroupMember(APIView):
authentication_classes = (TokenAuthentication, SessionAuthentication)
permission_classes = (IsAuthenticated,)
- throttle_classes = (UserRateThrottle, )
+ throttle_classes = (UserRateThrottle,)
@api_check_group
def get(self, request, group_id, email):
diff --git a/seahub/urls.py b/seahub/urls.py
index ac687a49a1..fbe6f7b747 100644
--- a/seahub/urls.py
+++ b/seahub/urls.py
@@ -36,7 +36,7 @@ from seahub.api2.endpoints.group_owned_libraries import GroupOwnedLibraries, \
from seahub.api2.endpoints.address_book.groups import AddressBookGroupsSubGroups
from seahub.api2.endpoints.address_book.members import AddressBookGroupsSearchMember
-from seahub.api2.endpoints.group_members import GroupMembers, GroupMember, \
+from seahub.api2.endpoints.group_members import GroupMembers, GroupSearchMember, GroupMember, \
GroupMembersBulk, GroupMembersImport, GroupMembersImportExample
from seahub.api2.endpoints.search_group import SearchGroup
from seahub.api2.endpoints.share_links import ShareLinks, ShareLink, \
@@ -308,6 +308,7 @@ urlpatterns = [
url(r'^api/v2.1/groups/(?P\d+)/group-owned-libraries/$', GroupOwnedLibraries.as_view(), name='api-v2.1-group-owned-libraries'),
url(r'^api/v2.1/groups/(?P\d+)/group-owned-libraries/(?P[-0-9a-f]{36})/$', GroupOwnedLibrary.as_view(), name='api-v2.1-owned-group-library'),
url(r'^api/v2.1/groups/(?P\d+)/members/$', GroupMembers.as_view(), name='api-v2.1-group-members'),
+ url(r'^api/v2.1/groups/(?P\d+)/search-member/$', GroupSearchMember.as_view(), name='api-v2.1-group-search-member'),
url(r'^api/v2.1/groups/(?P\d+)/members/bulk/$', GroupMembersBulk.as_view(), name='api-v2.1-group-members-bulk'),
url(r'^api/v2.1/groups/(?P\d+)/members/import/$', GroupMembersImport.as_view(), name='api-v2.1-group-members-import'),
url(r'^api/v2.1/group-members-import-example/$', GroupMembersImportExample.as_view(), name='api-v2.1-group-members-import-example'),