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') : (
-
-
-
- |
- {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 ?
+ :
+
+ }
-
+
);
@@ -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 (
+
+
+
+ |
+ {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..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();
+ }
+ }
+
};