diff --git a/frontend/src/components/dialog/lib-sub-folder-set-user-permission-dialog.js b/frontend/src/components/dialog/lib-sub-folder-set-user-permission-dialog.js index e46b740c8d..fb52e69fd0 100644 --- a/frontend/src/components/dialog/lib-sub-folder-set-user-permission-dialog.js +++ b/frontend/src/components/dialog/lib-sub-folder-set-user-permission-dialog.js @@ -16,7 +16,6 @@ class UserItem extends React.Component { this.state = { isOperationShow: false }; - this.userSelect = React.createRef(); } onMouseEnter = () => { @@ -100,7 +99,7 @@ class LibSubFolderSetUserPermissionDialog extends React.Component { constructor(props) { super(props); this.state = { - selectedUsers: null, + selectedUsers: [], errorMsg: [], permission: 'rw', userFolderPermItems: [], @@ -112,8 +111,6 @@ class LibSubFolderSetUserPermissionDialog extends React.Component { } else { this.permissions = ['r', 'rw', 'cloud-edit', 'preview', 'invisible']; } - - this.userSelect = React.createRef(); } handleUserSelectChange = (option) => { @@ -161,11 +158,10 @@ class LibSubFolderSetUserPermissionDialog extends React.Component { this.setState({ errorMsg: errorMsg, userFolderPermItems: this.state.userFolderPermItems.concat(res.data.success), - selectedUsers: null, + selectedUsers: [], permission: 'rw', folderPath: '', }); - this.userSelect.current.clearSelect(); }).catch(error => { let errMessage = Utils.getErrorMsg(error); toaster.danger(errMessage); @@ -283,11 +279,10 @@ class LibSubFolderSetUserPermissionDialog extends React.Component { {showPath && diff --git a/frontend/src/components/dialog/org-add-admin-dialog.js b/frontend/src/components/dialog/org-add-admin-dialog.js index 7e427f7844..4102f80721 100644 --- a/frontend/src/components/dialog/org-add-admin-dialog.js +++ b/frontend/src/components/dialog/org-add-admin-dialog.js @@ -18,24 +18,23 @@ class AddOrgAdminDialog extends React.Component { constructor(props) { super(props); this.state = { - selectedOption: null, + selectedUsers: [], errMessage: '', }; this.options = []; - this.userSelect = React.createRef(); } handleSelectChange = (option) => { this.setState({ - selectedOption: option, + selectedUsers: option, errMessage: '' }); this.options = []; }; addOrgAdmin = () => { - if (!this.state.selectedOption) return; - const userEmail = this.state.selectedOption[0].email; + if (!this.state.selectedUsers || this.state.selectedUsers.length === 0) return; + const userEmail = this.state.selectedUsers[0].email; orgAdminAPI.orgAdminSetOrgAdmin(orgID, userEmail, true).then(res => { let userInfo = new OrgUserInfo(res.data); this.props.onAddedOrgAdmin(userInfo); @@ -52,13 +51,13 @@ class AddOrgAdminDialog extends React.Component { render() { return ( - {gettext('Add Admins')} + {gettext('Add Admin')} {this.state.errMessage && {this.state.errMessage}} diff --git a/frontend/src/components/dialog/share-to-user.js b/frontend/src/components/dialog/share-to-user.js index 49304aeef8..79c8e8dd74 100644 --- a/frontend/src/components/dialog/share-to-user.js +++ b/frontend/src/components/dialog/share-to-user.js @@ -21,7 +21,6 @@ class UserItem extends React.Component { isOperationShow: false, isUserDetailsPopoverOpen: false }; - this.userSelect = React.createRef(); } onMouseEnter = () => { @@ -225,7 +224,7 @@ class ShareToUser extends React.Component { constructor(props) { super(props); this.state = { - selectedOption: null, + selectedUsers: [], errorMsg: [], permission: 'rw', sharedItems: [], @@ -253,7 +252,7 @@ class ShareToUser extends React.Component { } handleSelectChange = (option) => { - this.setState({ selectedOption: option }); + this.setState({ selectedUsers: option }); this.options = []; }; @@ -287,9 +286,9 @@ class ShareToUser extends React.Component { let users = []; let path = this.props.itemPath; let repoID = this.props.repoID; - if (this.state.selectedOption && this.state.selectedOption.length > 0) { - for (let i = 0; i < this.state.selectedOption.length; i ++) { - users[i] = this.state.selectedOption[i].email; + if (this.state.selectedUsers && this.state.selectedUsers.length > 0) { + for (let i = 0; i < this.state.selectedUsers.length; i ++) { + users[i] = this.state.selectedUsers[i].email; } } if (this.props.isGroupOwnedRepo) { @@ -312,10 +311,9 @@ class ShareToUser extends React.Component { this.setState({ errorMsg: errorMsg, sharedItems: this.state.sharedItems.concat(items), - selectedOption: null, + selectedUsers: [], permission: 'rw', }); - this.userSelect.current.clearSelect(); }).catch(error => { if (error.response) { let message = gettext('Library can not be shared to owner.'); @@ -323,7 +321,7 @@ class ShareToUser extends React.Component { errMessage.push(message); this.setState({ errorMsg: errMessage, - selectedOption: null, + selectedUsers: [], }); } }); @@ -338,10 +336,9 @@ class ShareToUser extends React.Component { this.setState({ errorMsg: errorMsg, sharedItems: this.state.sharedItems.concat(res.data.success), - selectedOption: null, + selectedUsers: [], permission: 'rw', }); - this.userSelect.current.clearSelect(); }).catch(error => { if (error.response) { let message = gettext('Library can not be shared to owner.'); @@ -349,7 +346,7 @@ class ShareToUser extends React.Component { errMessage.push(message); this.setState({ errorMsg: errMessage, - selectedOption: null, + selectedUsers: [], }); } }); @@ -443,10 +440,9 @@ class ShareToUser extends React.Component { this.setState({ errorMsg: errorMsg, sharedItems: this.state.sharedItems.concat(items), - selectedOption: null, + selectedUsers: [], permission: 'rw', }); - this.userSelect.current.clearSelect(); }).catch(error => { if (error.response) { let message = gettext('Library can not be shared to owner.'); @@ -454,7 +450,7 @@ class ShareToUser extends React.Component { errMessage.push(message); this.setState({ errorMsg: errMessage, - selectedOption: null, + selectedUsers: [], }); } }); @@ -469,10 +465,9 @@ class ShareToUser extends React.Component { this.setState({ errorMsg: errorMsg, sharedItems: this.state.sharedItems.concat(res.data.success), - selectedOption: null, + selectedUsers: [], permission: 'rw', }); - this.userSelect.current.clearSelect(); }).catch(error => { if (error.response) { let message = gettext('Library can not be shared to owner.'); @@ -480,7 +475,7 @@ class ShareToUser extends React.Component { errMessage.push(message); this.setState({ errorMsg: errMessage, - selectedOption: null, + selectedUsers: [], }); } }); @@ -526,12 +521,11 @@ class ShareToUser extends React.Component {
{showDeptBtn && { - this.setState({ selectedOptions: options }); + this.setState({ selectedUsers: options }); }; handleSubmit = () => { - const emails = this.state.selectedOptions.map(option => option.email); + const emails = this.state.selectedUsers.map(option => option.email); if (emails.length === 0) return; this.setState({ errMessage: '' }); const { nodeId, orgID } = this.props; @@ -38,7 +38,7 @@ export default class AddDepartMemberV2Dialog extends React.Component { orgAdminAPI.orgAdminAddGroupMember(orgID, nodeId, emails) : systemAdminAPI.sysAdminAddGroupMember(nodeId, emails); req.then((res) => { - this.setState({ selectedOptions: [] }); + this.setState({ selectedUsers: [] }); if (res.data.failed.length > 0) { this.setState({ errMsgs: res.data.failed.map(item => item.error_msg) }); } @@ -62,6 +62,7 @@ export default class AddDepartMemberV2Dialog extends React.Component { placeholder={gettext('Search users')} onSelectChange={this.handleSelectChange} isMulti={true} + selectedUsers={this.state.selectedUsers} /> {errMsgs.length > 0 && (
    diff --git a/frontend/src/components/dialog/sysadmin-dialog/sysadmin-add-institution-member-dialog.js b/frontend/src/components/dialog/sysadmin-dialog/sysadmin-add-institution-member-dialog.js index c898efaa25..f222017c01 100644 --- a/frontend/src/components/dialog/sysadmin-dialog/sysadmin-add-institution-member-dialog.js +++ b/frontend/src/components/dialog/sysadmin-dialog/sysadmin-add-institution-member-dialog.js @@ -15,17 +15,17 @@ class AddMemberDialog extends React.Component { constructor(props) { super(props); this.state = { - selectedOption: [], + selectedUsers: [], }; } handleSelectChange = (option) => { - this.setState({ selectedOption: option }); + this.setState({ selectedUsers: option }); }; handleSubmit = () => { - if (!this.state.selectedOption) return; - const emails = this.state.selectedOption.map(item => item.email); + if (!this.state.selectedUsers) return; + const emails = this.state.selectedUsers.map(item => item.email); this.props.addUser(emails); }; @@ -39,6 +39,7 @@ class AddMemberDialog extends React.Component { onSelectChange={this.handleSelectChange} isMulti={true} className='org-add-member-select' + selectedUsers={this.state.selectedUsers} /> diff --git a/frontend/src/components/dialog/sysadmin-dialog/sysadmin-add-member-dialog.js b/frontend/src/components/dialog/sysadmin-dialog/sysadmin-add-member-dialog.js index 68529ecb5d..df94e50062 100644 --- a/frontend/src/components/dialog/sysadmin-dialog/sysadmin-add-member-dialog.js +++ b/frontend/src/components/dialog/sysadmin-dialog/sysadmin-add-member-dialog.js @@ -19,22 +19,21 @@ class AddMemberDialog extends React.Component { constructor(props) { super(props); this.state = { - selectedOption: null, + selectedUsers: [], errMessage: '', }; } handleSelectChange = (option) => { - this.setState({ selectedOption: option }); + this.setState({ selectedUsers: option }); }; handleSubmit = () => { - if (!this.state.selectedOption) return; - const emails = this.state.selectedOption.map(item => item.email); - this.refs.orgSelect.clearSelect(); + if (!this.state.selectedUsers) return; + const emails = this.state.selectedUsers.map(item => item.email); this.setState({ errMessage: [] }); systemAdminAPI.sysAdminAddGroupMember(this.props.groupID, emails).then((res) => { - this.setState({ selectedOption: null }); + this.setState({ selectedUsers: [] }); if (res.data.failed.length > 0) { this.setState({ errMessage: res.data.failed[0].error_msg }); } @@ -56,9 +55,9 @@ class AddMemberDialog extends React.Component { { this.state.errMessage &&

    {this.state.errMessage}

    } diff --git a/frontend/src/components/dialog/sysadmin-dialog/sysadmin-batch-add-admin-dialog.js b/frontend/src/components/dialog/sysadmin-dialog/sysadmin-batch-add-admin-dialog.js index 2109e6b82e..46cd99d9dc 100644 --- a/frontend/src/components/dialog/sysadmin-dialog/sysadmin-batch-add-admin-dialog.js +++ b/frontend/src/components/dialog/sysadmin-dialog/sysadmin-batch-add-admin-dialog.js @@ -14,7 +14,7 @@ class SysAdminBatchAddAdminDialog extends React.Component { constructor(props) { super(props); this.state = { - options: null, + selectedUsers: [], isSubmitBtnActive: false }; } @@ -23,15 +23,15 @@ class SysAdminBatchAddAdminDialog extends React.Component { this.props.toggle(); }; - handleSelectChange = (options) => { + handleSelectChange = (selectedUsers) => { this.setState({ - options: options, - isSubmitBtnActive: options.length > 0 + selectedUsers: selectedUsers, + isSubmitBtnActive: selectedUsers.length > 0 }); }; handleSubmit = () => { - this.props.addAdminInBatch(this.state.options.map(item => item.email)); + this.props.addAdminInBatch(this.state.selectedUsers.map(item => item.email)); this.toggle(); }; @@ -44,6 +44,7 @@ class SysAdminBatchAddAdminDialog extends React.Component { isMulti={true} placeholder={gettext('Search users')} onSelectChange={this.handleSelectChange} + selectedUsers={this.state.selectedUsers} /> diff --git a/frontend/src/components/dialog/sysadmin-dialog/sysadmin-create-group-dialog.js b/frontend/src/components/dialog/sysadmin-dialog/sysadmin-create-group-dialog.js index 2b57c1271b..a8426287bb 100644 --- a/frontend/src/components/dialog/sysadmin-dialog/sysadmin-create-group-dialog.js +++ b/frontend/src/components/dialog/sysadmin-dialog/sysadmin-create-group-dialog.js @@ -16,7 +16,7 @@ class SysAdminCreateGroupDialog extends React.Component { super(props); this.state = { groupName: '', - ownerEmail: '', + selectedUsers: [], errMessage: '', isSubmitBtnActive: false }; @@ -34,13 +34,12 @@ class SysAdminCreateGroupDialog extends React.Component { handleSubmit = () => { let groupName = this.state.groupName.trim(); - this.props.createGroup(groupName, this.state.ownerEmail); + this.props.createGroup(groupName, this.state.selectedUsers[0].email); }; - handleSelectChange = (option) => { - // option can be `null`, `[{...}]`, or `[]` + handleSelectChange = (selectedUsers) => { this.setState({ - ownerEmail: option && option.length ? option[0].email : '' + selectedUsers: selectedUsers }); }; @@ -72,12 +71,13 @@ class SysAdminCreateGroupDialog extends React.Component { /> diff --git a/frontend/src/components/dialog/sysadmin-dialog/sysadmin-create-repo-dialog.js b/frontend/src/components/dialog/sysadmin-dialog/sysadmin-create-repo-dialog.js index a4e9e0c1cd..b33d52c4a5 100644 --- a/frontend/src/components/dialog/sysadmin-dialog/sysadmin-create-repo-dialog.js +++ b/frontend/src/components/dialog/sysadmin-dialog/sysadmin-create-repo-dialog.js @@ -15,7 +15,7 @@ class SysAdminCreateRepoDialog extends React.Component { super(props); this.state = { repoName: '', - ownerEmail: '', + selectedUsers: [], errMessage: '', isSubmitBtnActive: false }; @@ -30,15 +30,14 @@ class SysAdminCreateRepoDialog extends React.Component { }; handleSubmit = () => { - const { repoName, ownerEmail } = this.state; - this.props.createRepo(repoName.trim(), ownerEmail); + const { repoName, selectedUsers } = this.state; + this.props.createRepo(repoName.trim(), selectedUsers[0].email); this.toggle(); }; - handleSelectChange = (option) => { - // option can be `null`, `[{...}]`, or `[]` + handleSelectChange = (selectedUsers) => { this.setState({ - ownerEmail: option && option.length ? option[0].email : '' + selectedUsers: selectedUsers }); }; @@ -72,13 +71,14 @@ class SysAdminCreateRepoDialog extends React.Component { diff --git a/frontend/src/components/dialog/sysadmin-dialog/sysadmin-group-add-member-dialog.js b/frontend/src/components/dialog/sysadmin-dialog/sysadmin-group-add-member-dialog.js index 23c8ae8938..64a08b939f 100644 --- a/frontend/src/components/dialog/sysadmin-dialog/sysadmin-group-add-member-dialog.js +++ b/frontend/src/components/dialog/sysadmin-dialog/sysadmin-group-add-member-dialog.js @@ -15,21 +15,20 @@ class SysAdminGroupAddMemberDialog extends React.Component { constructor(props) { super(props); this.state = { - selectedOptions: null, + selectedUsers: [], isSubmitBtnDisabled: true }; - this.userSelect = React.createRef(); } handleSelectChange = (options) => { this.setState({ - selectedOptions: options, + selectedUsers: options, isSubmitBtnDisabled: !options.length }); }; addMembers = () => { - let emails = this.state.selectedOptions.map(item => item.email); + let emails = this.state.selectedUsers.map(item => item.email); this.props.addMembers(emails); this.props.toggle(); }; @@ -41,10 +40,10 @@ class SysAdminGroupAddMemberDialog extends React.Component { {gettext('Add Member')} diff --git a/frontend/src/components/dialog/sysadmin-dialog/sysadmin-group-transfer-dialog.js b/frontend/src/components/dialog/sysadmin-dialog/sysadmin-group-transfer-dialog.js index 48ec2f3a1b..0b869e3b7d 100644 --- a/frontend/src/components/dialog/sysadmin-dialog/sysadmin-group-transfer-dialog.js +++ b/frontend/src/components/dialog/sysadmin-dialog/sysadmin-group-transfer-dialog.js @@ -17,22 +17,21 @@ class SysAdminTransferGroupDialog extends React.Component { constructor(props) { super(props); this.state = { - selectedOptions: null, + selectedUsers: [], submitBtnDisabled: true }; - this.userSelect = React.createRef(); } handleSelectChange = (options) => { this.setState({ - selectedOptions: options, + selectedUsers: options, submitBtnDisabled: options == null }); }; submit = () => { - if (this.state.selectedOptions) { - const receiver = this.state.selectedOptions[0].email; + if (this.state.selectedUsers) { + const receiver = this.state.selectedUsers[0].email; this.props.transferGroup(receiver); this.props.toggleDialog(); } @@ -49,10 +48,10 @@ class SysAdminTransferGroupDialog extends React.Component { diff --git a/frontend/src/components/dialog/sysadmin-dialog/sysadmin-repo-transfer-dialog.js b/frontend/src/components/dialog/sysadmin-dialog/sysadmin-repo-transfer-dialog.js deleted file mode 100644 index 19eeb528fc..0000000000 --- a/frontend/src/components/dialog/sysadmin-dialog/sysadmin-repo-transfer-dialog.js +++ /dev/null @@ -1,62 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { Button, Modal, ModalBody, ModalFooter } from 'reactstrap'; -import { gettext } from '../../../utils/constants'; -import UserSelect from '../../user-select'; -import SeahubModalHeader from '@/components/common/seahub-modal-header'; - -const propTypes = { - repoName: PropTypes.string.isRequired, - toggle: PropTypes.func.isRequired, - submit: PropTypes.func.isRequired, -}; - -class SysAdminRepoTransferDialog extends React.Component { - constructor(props) { - super(props); - this.state = { - selectedOption: null, - errorMsg: [], - }; - this.userSelect = React.createRef(); - } - - handleSelectChange = (option) => { - this.setState({ selectedOption: option }); - }; - - submit = () => { - let user = this.state.selectedOption; - this.props.submit(user); - }; - - render() { - const repoName = this.props.repoName; - const innerSpan = '' + repoName + ''; - let msg = gettext('Transfer Library {library_name}'); - let message = msg.replace('{library_name}', innerSpan); - return ( - - -
    - - - - - - - - - - ); - } -} - -SysAdminRepoTransferDialog.propTypes = propTypes; - -export default SysAdminRepoTransferDialog; diff --git a/frontend/src/components/dialog/sysadmin-dialog/sysadmin-share-to-user.js b/frontend/src/components/dialog/sysadmin-dialog/sysadmin-share-to-user.js index 9a610664d4..a8cfc141aa 100644 --- a/frontend/src/components/dialog/sysadmin-dialog/sysadmin-share-to-user.js +++ b/frontend/src/components/dialog/sysadmin-dialog/sysadmin-share-to-user.js @@ -15,7 +15,6 @@ class UserItem extends React.Component { this.state = { isOperationShow: false }; - this.userSelect = React.createRef(); } onMouseEnter = () => { @@ -115,7 +114,7 @@ class SysAdminShareToUser extends React.Component { constructor(props) { super(props); this.state = { - selectedOption: null, + selectedUsers: [], errorMsg: [], permission: 'rw', sharedItems: [] @@ -127,8 +126,8 @@ class SysAdminShareToUser extends React.Component { } } - handleSelectChange = (option) => { - this.setState({ selectedOption: option }); + handleSelectChange = (options) => { + this.setState({ selectedUsers: options }); this.options = []; }; @@ -151,9 +150,9 @@ class SysAdminShareToUser extends React.Component { shareToUser = () => { let users = []; let repoID = this.props.repoID; - if (this.state.selectedOption && this.state.selectedOption.length > 0) { - for (let i = 0; i < this.state.selectedOption.length; i ++) { - users[i] = this.state.selectedOption[i].email; + if (this.state.selectedUsers && this.state.selectedUsers.length > 0) { + for (let i = 0; i < this.state.selectedUsers.length; i ++) { + users[i] = this.state.selectedUsers[i].email; } } systemAdminAPI.sysAdminAddRepoSharedItem(repoID, 'user', users, this.state.permission).then(res => { @@ -167,10 +166,9 @@ class SysAdminShareToUser extends React.Component { this.setState({ errorMsg: errorMsg, sharedItems: this.state.sharedItems.concat(newItems), - selectedOption: null, + selectedUsers: [], permission: 'rw', }); - this.userSelect.current.clearSelect(); }).catch(error => { if (error.response) { let message = gettext('Library can not be shared to owner.'); @@ -178,7 +176,7 @@ class SysAdminShareToUser extends React.Component { errMessage.push(message); this.setState({ errorMsg: errMessage, - selectedOption: null, + selectedUsers: [], }); } }); @@ -236,10 +234,10 @@ class SysAdminShareToUser extends React.Component { diff --git a/frontend/src/components/dialog/transfer-dialog.js b/frontend/src/components/dialog/transfer-dialog.js index e1ddf626d0..67eefaf280 100644 --- a/frontend/src/components/dialog/transfer-dialog.js +++ b/frontend/src/components/dialog/transfer-dialog.js @@ -34,22 +34,26 @@ class TransferDialog extends React.Component { this.state = { options: [], selectedOption: null, + selectedUsers: [], errorMsg: [], transferToUser: true, transferToGroup: false, reshare: false, activeTab: !this.props.isDepAdminTransfer ? TRANS_USER : TRANS_DEPART }; - this.userSelect = React.createRef(); } handleSelectChange = (option) => { this.setState({ selectedOption: option }); }; + onUsersChange = (selectedUsers) => { + this.setState({ selectedUsers }); + }; + submit = () => { - const { activeTab, reshare, selectedOption } = this.state; - const email = activeTab === TRANS_DEPART ? selectedOption.email : selectedOption[0].email; + const { activeTab, reshare, selectedOption, selectedUsers } = this.state; + const email = activeTab === TRANS_DEPART ? selectedOption.email : selectedUsers[0].email; this.props.onTransferRepo(email, reshare); }; @@ -106,6 +110,7 @@ class TransferDialog extends React.Component { activeTab: tab, reshare: false, selectedOption: null, + selectedUsers: [], }); } }; @@ -158,10 +163,10 @@ class TransferDialog extends React.Component { ' + Utils.HTMLescape(repoName) + ''); let buttonDisabled = false; - if (selectedOption === null || (Array.isArray(selectedOption) && selectedOption.length === 0)) { - buttonDisabled = true; + if (activeTab === TRANS_DEPART) { + if (selectedOption === null || (Array.isArray(selectedOption) && selectedOption.length === 0)) { + buttonDisabled = true; + } + } else { + if (this.state.selectedUsers.length === 0) { + buttonDisabled = true; + } } return ( diff --git a/frontend/src/components/dialog/transfer-group-dialog.js b/frontend/src/components/dialog/transfer-group-dialog.js index 145f6415eb..80bcd520a8 100644 --- a/frontend/src/components/dialog/transfer-group-dialog.js +++ b/frontend/src/components/dialog/transfer-group-dialog.js @@ -21,21 +21,21 @@ class TransferGroupDialog extends React.Component { constructor(props) { super(props); this.state = { - selectedOption: null + selectedUsers: [] }; } handleSelectChange = (option) => { this.setState({ - selectedOption: option + selectedUsers: option }); }; transferGroup = () => { - let selectedOption = this.state.selectedOption; + let selectedUsers = this.state.selectedUsers; let email; - if (selectedOption && selectedOption[0]) { - email = selectedOption[0].email; + if (selectedUsers && selectedUsers[0]) { + email = selectedUsers[0].email; } if (!email) { return false; @@ -61,10 +61,10 @@ class TransferGroupDialog extends React.Component {

    {gettext('Transfer group to')}

    diff --git a/frontend/src/components/list-and-add-group-members.js b/frontend/src/components/list-and-add-group-members.js index f2f473087a..4fe9eb7982 100644 --- a/frontend/src/components/list-and-add-group-members.js +++ b/frontend/src/components/list-and-add-group-members.js @@ -27,14 +27,13 @@ class ManageMembersDialog extends React.Component { page: 1, perPage: 100, hasNextPage: false, - selectedOption: null, + selectedUsers: [], errMessage: [], isItemFreezed: false, searchActive: false, keyword: '', membersFound: [] }; - this.userSelect = React.createRef(); } componentDidMount() { @@ -66,23 +65,22 @@ class ManageMembersDialog extends React.Component { onSelectChange = (option) => { this.setState({ - selectedOption: option, + selectedUsers: option, errMessage: [], }); }; addGroupMember = () => { let emails = []; - for (let i = 0; i < this.state.selectedOption.length; i++) { - emails.push(this.state.selectedOption[i].email); + for (let i = 0; i < this.state.selectedUsers.length; i++) { + emails.push(this.state.selectedUsers[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, + selectedUsers: [], }); - this.userSelect.current.clearSelect(); if (res.data.failed.length > 0) { this.setState({ errMessage: res.data.failed @@ -186,14 +184,14 @@ class ManageMembersDialog extends React.Component { {showDeptBtn && } - {this.state.selectedOption ? + {this.state.selectedUsers.length > 0 ? : } diff --git a/frontend/src/components/share-link-panel/link-authenticated-users.js b/frontend/src/components/share-link-panel/link-authenticated-users.js index 6b8168cb7a..add1bc248e 100644 --- a/frontend/src/components/share-link-panel/link-authenticated-users.js +++ b/frontend/src/components/share-link-panel/link-authenticated-users.js @@ -15,7 +15,6 @@ class UserItem extends React.Component { isHighlighted: false, isOperationShow: false }; - this.userSelect = React.createRef(); } onMouseEnter = () => { @@ -89,7 +88,7 @@ class LinkAuthenticatedUsers extends React.Component { constructor(props) { super(props); this.state = { - selectedOption: null, + selectedUsers: [], authUsers: [] }; } @@ -110,11 +109,11 @@ class LinkAuthenticatedUsers extends React.Component { addLinkAuthUsers = () => { const { linkToken, path } = this.props; - const { selectedOption, authUsers } = this.state; - if (!selectedOption || !selectedOption.length) { + const { selectedUsers, authUsers } = this.state; + if (!selectedUsers || !selectedUsers.length) { return false; } - const users = selectedOption.map((item, index) => item.email); + const users = selectedUsers.map((item, index) => item.email); shareLinkAPI.addShareLinkAuthUsers(linkToken, users, path).then(res => { const { success, failed } = res.data; if (success.length) { @@ -130,9 +129,8 @@ class LinkAuthenticatedUsers extends React.Component { } this.setState({ authUsers: success.concat(authUsers), - selectedOption: null + selectedUsers: [] }); - this.userSelect.current.clearSelect(); }).catch(error => { let errMessage = Utils.getErrorMsg(error); toaster.danger(errMessage); @@ -160,7 +158,7 @@ class LinkAuthenticatedUsers extends React.Component { }; handleSelectChange = (option) => { - this.setState({ selectedOption: option }); + this.setState({ selectedUsers: option }); }; render() { @@ -193,10 +191,10 @@ class LinkAuthenticatedUsers extends React.Component { diff --git a/frontend/src/components/share-link-panel/link-creation.js b/frontend/src/components/share-link-panel/link-creation.js index f17bae2e8b..f877630659 100644 --- a/frontend/src/components/share-link-panel/link-creation.js +++ b/frontend/src/components/share-link-panel/link-creation.js @@ -47,10 +47,9 @@ class LinkCreation extends React.Component { currentPermission: props.currentPermission, currentScope: 'all_users', - selectedOption: null, + selectedUsers: [], inputEmails: '' }; - this.userSelect = React.createRef(); } setExpType = (e) => { @@ -128,9 +127,9 @@ class LinkCreation extends React.Component { const autoGeneratePassword = shareLinkForceUsePassword || isShowPasswordInput; request = seafileAPI.batchCreateMultiShareLink(repoID, itemPath, linkAmount, autoGeneratePassword, expirationTime, permissions); } else { - const { currentScope, selectedOption, inputEmails } = this.state; - if (currentScope === 'specific_users' && selectedOption) { - users = selectedOption.map((item, index) => item.email); + const { currentScope, selectedUsers, inputEmails } = this.state; + if (currentScope === 'specific_users' && selectedUsers) { + users = selectedUsers.map((item, index) => item.email); } if (currentScope === 'specific_emails' && inputEmails) { users = inputEmails; @@ -264,11 +263,11 @@ class LinkCreation extends React.Component { }; setScope = (e) => { - this.setState({ currentScope: e.target.value, selectedOption: null, inputEmails: '' }); + this.setState({ currentScope: e.target.value, selectedUsers: [], inputEmails: '' }); }; handleSelectChange = (option) => { - this.setState({ selectedOption: option }); + this.setState({ selectedUsers: option }); }; handleInputChange = (e) => { @@ -390,10 +389,10 @@ class LinkCreation extends React.Component { {this.state.currentScope === 'specific_users' && } diff --git a/frontend/src/components/user-item/index.css b/frontend/src/components/user-item/index.css new file mode 100644 index 0000000000..622c986c64 --- /dev/null +++ b/frontend/src/components/user-item/index.css @@ -0,0 +1,59 @@ +.user-item { + display: inline-flex; + align-items: center; + margin: 2px 10px 2px 0; + padding: 0 8px 0 2px; + height: 20px; + font-size: 13px; + border-radius: 10px; + background: #eaeaea; +} + +.user-item .user-avatar, +.user-item .user-name, +.user-item .user-remove { + height: 20px; + line-height: 20px; +} + +.user-item .user-avatar { + display: flex; + align-items: center; + justify-content: center; + transform: translateY(0); + flex-shrink: 0; +} + +.user-item .user-name { + margin-left: 5px; +} + +.user-item .user-avatar img { + width: 16px; + height: 16px; + border-radius: 50%; +} + +.user-item .user-remove { + display: inline-block; + width: 14px; + margin: 0 -2px 0 2px; +} + +.user-item .user-remove .sf3-font { + display: inline-block; + font-size: 12px; + color: #909090; + transform: scale(.8); + cursor: pointer; +} + +.user-item .user-remove .sf3-font:hover { + color: #666666; + cursor: pointer; +} + +.user-item .user-option-email { + font-size: 12px; + margin-left: 4px; +} diff --git a/frontend/src/components/user-item/index.js b/frontend/src/components/user-item/index.js new file mode 100644 index 0000000000..88cc321baa --- /dev/null +++ b/frontend/src/components/user-item/index.js @@ -0,0 +1,54 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import classnames from 'classnames'; +import { gettext, enableShowContactEmailWhenSearchUser, enableShowLoginIDWhenSearchUser } from '../../utils/constants'; + +import './index.css'; + +const propTypes = { + user: PropTypes.shape({ + name: PropTypes.string.isRequired, + avatar_url: PropTypes.string.isRequired, + email: PropTypes.string, + contact_email: PropTypes.string, + login_id: PropTypes.string, + }), + className: PropTypes.string, + enableDeleteUser: PropTypes.bool, + onDeleteUser: PropTypes.func, +}; + +class UserItem extends React.Component { + + onDeleteUser = (event) => { + event.stopPropagation(); + event && event.nativeEvent.stopImmediatePropagation(); + this.props.onDeleteUser(this.props.user); + }; + + render() { + const { className, user, enableDeleteUser } = this.props; + const { name, avatar_url, contact_email, login_id } = user; + return ( +
    + + {name} + +
    + {name} + {(enableShowContactEmailWhenSearchUser && !enableDeleteUser) && ({contact_email})} + {(enableShowLoginIDWhenSearchUser && !enableDeleteUser) && ({login_id})} +
    + {enableDeleteUser && ( + + + + )} +
    + ); + } +} + +UserItem.propTypes = propTypes; + +export default UserItem; diff --git a/frontend/src/components/user-select.js b/frontend/src/components/user-select.js index abf213ea3b..0edfd8ccc6 100644 --- a/frontend/src/components/user-select.js +++ b/frontend/src/components/user-select.js @@ -1,114 +1,265 @@ import React from 'react'; import PropTypes from 'prop-types'; -import AsyncSelect from 'react-select/async'; +import classnames from 'classnames'; +import { Popover } from 'reactstrap'; import { seafileAPI } from '../utils/seafile-api'; -import { gettext, enableShowContactEmailWhenSearchUser, enableShowLoginIDWhenSearchUser } from '../utils/constants'; +import { gettext } from '../utils/constants'; import { Utils } from '../utils/utils'; import toaster from './toast'; -import { UserSelectStyle, NoOptionsStyle } from './common/select'; +import KeyCodes from '../constants/keyCodes'; +import SearchInput from './search-input'; +import UserItem from '../components/user-item'; +import ClickOutside from './click-outside'; import '../css/user-select.css'; const propTypes = { placeholder: PropTypes.string.isRequired, onSelectChange: PropTypes.func.isRequired, - isMulti: PropTypes.bool.isRequired, + isMulti: PropTypes.bool, className: PropTypes.string, - value: PropTypes.oneOfType([PropTypes.string, PropTypes.array]), }; class UserSelect extends React.Component { constructor(props) { super(props); - this.options = []; - this.finalValue = ''; this.state = { - searchValue: '' + maxItemNum: 0, + itemHeight: 0, + searchedUsers: [], + searchValue: '', + highlightIndex: -1, }; - this.userSelect = React.createRef(); } - onInputChange = (searchValue) => { - if (!this.props.isMulti && searchValue.trim()) { - this.handleSelectChange(null); - this.clearSelect(); - } - this.setState({ searchValue }); - }; - - handleSelectChange = (option) => { - this.options = []; - this.props.onSelectChange(option); - }; - - loadOptions = (input, callback) => { - const value = input.trim(); - this.finalValue = value; - setTimeout(() => { - if (this.finalValue === value && value.length > 0) { - seafileAPI.searchUsers(value).then((res) => { - this.options = []; - for (let i = 0 ; i < res.data.users.length; i++) { - const item = res.data.users[i]; - let obj = {}; - obj.value = item.name; - obj.email = item.email; - obj.label = (enableShowContactEmailWhenSearchUser || enableShowLoginIDWhenSearchUser) ? ( -
    - -
    - {item.name}
    - {enableShowContactEmailWhenSearchUser && {item.contact_email}} - {enableShowLoginIDWhenSearchUser && {item.login_id}} -
    -
    - ) : ( - - - {item.name} - - ); - this.options.push(obj); - } - callback(this.options); - }).catch(error => { - let errMessage = Utils.getErrorMsg(error); - toaster.danger(errMessage); + onValueChanged = (newSearchValue) => { + this.setState({ + searchValue: newSearchValue + }); + const searchValue = newSearchValue.trim(); + if (searchValue.length === 0) { + this.setState({ + searchedUsers: [], + highlightIndex: -1, + }); + } else { + seafileAPI.searchUsers(newSearchValue.trim()).then((res) => { + this.setState({ + searchedUsers: res.data.users, + highlightIndex: res.data.users.length > 0 ? 0 : -1, }); - } - }, 1000); + }).catch(error => { + let errMessage = Utils.getErrorMsg(error); + toaster.danger(errMessage); + }); + } }; - clearSelect = () => { - this.userSelect.current.onChange([], { action: 'clear' }); + componentDidMount() { + if (this.ref) { + const { bottom } = this.ref.getBoundingClientRect(); + if (bottom > window.innerHeight) { + this.ref.style.top = `${window.innerHeight - bottom}px`; + } + } + if (this.container && this.userItem) { + this.setState({ + maxItemNum: this.getMaxItemNum(), + itemHeight: parseInt(getComputedStyle(this.userItem, null).height) + }); + } + document.addEventListener('keydown', this.onHotKey, true); + } + + componentWillUnmount() { + document.removeEventListener('keydown', this.onHotKey, true); + } + + onClickOutside = (e) => { + if (e.target.id !== 'user-select' && this.state.isPopoverOpen) { + this.setState({ + isPopoverOpen: false, + searchedUsers: [], + searchValue: '', + highlightIndex: -1, + }); + } + }; + + getMaxItemNum = () => { + let userContainerStyle = getComputedStyle(this.container, null); + let userItemStyle = getComputedStyle(this.userItem, null); + let maxContainerItemNum = Math.floor(parseInt(userContainerStyle.maxHeight) / parseInt(userItemStyle.height)); + return maxContainerItemNum - 1; + }; + + onHotKey = (e) => { + if (e.keyCode === KeyCodes.Enter) { + this.onEnter(e); + } else if (e.keyCode === KeyCodes.UpArrow) { + this.onUpArrow(e); + } else if (e.keyCode === KeyCodes.DownArrow) { + this.onDownArrow(e); + } else if (e.keyCode === KeyCodes.Escape) { + this.onEsc(e); + } + }; + + onEnter = (e) => { + e.preventDefault(); + let user; + if (this.state.searchedUsers.length === 1) { + user = this.state.searchedUsers[0]; + } else if (this.state.highlightIndex > -1) { + user = this.state.searchedUsers[this.state.highlightIndex]; + } + if (user) { + this.onUserClick(user); + } + }; + + onUpArrow = (e) => { + e.preventDefault(); + e.stopPropagation(); + let { highlightIndex, maxItemNum, itemHeight } = this.state; + if (highlightIndex > 0) { + this.setState({ highlightIndex: highlightIndex - 1 }, () => { + if (highlightIndex < this.state.searchedUsers.length - maxItemNum) { + this.container.scrollTop -= itemHeight; + } + }); + } else { + this.setState({ highlightIndex: this.state.searchedUsers.length - 1 }, () => { + this.container.scrollTop = this.container.scrollHeight; + }); + } + }; + + onDownArrow = (e) => { + e.preventDefault(); + e.stopPropagation(); + let { highlightIndex, maxItemNum, itemHeight } = this.state; + if (highlightIndex < this.state.searchedUsers.length - 1) { + this.setState({ highlightIndex: highlightIndex + 1 }, () => { + if (highlightIndex >= maxItemNum) { + this.container.scrollTop += itemHeight; + } + }); + } else { + this.setState({ highlightIndex: 0 }, () => { + this.container.scrollTop = 0; + }); + } + }; + + onEsc = (e) => { + e.preventDefault(); + e.stopPropagation(); + this.setState({ isPopoverOpen: false }); + }; + + onUserClick = (user) => { + const { isMulti = true } = this.props; + let selectedUsers = this.props.selectedUsers.slice(0); + if (isMulti) { + const index = selectedUsers.findIndex(item => item.email === user.email); + if (index > -1) { + selectedUsers.splice(index, 1); + } else { + selectedUsers.push(user); + } + } else { + selectedUsers = [user]; + } + this.props.onSelectChange(selectedUsers); + }; + + onKeyDown = (e) => { + if (e.keyCode === KeyCodes.LeftArrow || e.keyCode === KeyCodes.RightArrow) { + e.stopPropagation(); + } + }; + + onDeleteSelectedCollaborator = (user) => { + const { selectedUsers = [] } = this.props; + const newSelectedCollaborator = selectedUsers.filter(item => item.email !== user.email); + this.props.onSelectChange(newSelectedCollaborator); + }; + + onTogglePopover = () => { + this.setState({ isPopoverOpen: !this.state.isPopoverOpen }); + if (!this.state.isPopoverOpen) { + this.onValueChanged(this.state.searchValue); + } }; render() { - const searchValue = this.state.searchValue; + const { searchValue, highlightIndex, searchedUsers } = this.state; + const { className = '', selectedUsers = [] } = this.props; return ( - { - return ( -
    - {searchValue ? gettext('User not found') : gettext('Enter characters to start searching')} + + <> +
    + {selectedUsers.map((user, index) => { + return ( + + ); + })} + {selectedUsers.length === 0 && ( +
    + {this.props.placeholder || gettext('Select users')}
    - ); - } - }} - isMulti={true} - loadOptions={this.loadOptions} - onChange={this.handleSelectChange} - onInputChange={this.onInputChange} - placeholder={this.props.placeholder} - className={`user-select ${this.props.className || ''}`} - value={this.props.value} - ref={this.userSelect} - styles={UserSelectStyle} - /> + )} +
    + +
    this.ref = ref} onMouseDown={e => e.stopPropagation()}> +
    + +
    +
    this.container = ref}> + {searchedUsers.length > 0 && ( + searchedUsers.map((user, index) => { + return ( +
    this.userItem = ref} + onClick={this.onUserClick.bind(this, user)} + > + +
    + ); + }) + )} + {searchedUsers.length === 0 && +
    + {searchValue ? gettext('User not found') : gettext('Enter characters to start searching')} +
    + } +
    +
    +
    + +
    ); } } diff --git a/frontend/src/css/user-select.css b/frontend/src/css/user-select.css index 7421482900..0e46b5b12a 100644 --- a/frontend/src/css/user-select.css +++ b/frontend/src/css/user-select.css @@ -1,32 +1,59 @@ -.user-option-name { +.selected-user-item-container { + flex-wrap: wrap; + min-height: 38px; +} + +.selected-user-item-container .user-select-placeholder { + color: #808080; +} + +.user-select-container { + position: relative; +} + +.user-select-container .user-search-container { + padding: 10px 10px 0 10px; +} + +.user-select-container .user-search-container input { + height: 28px; +} + +.user-select-container .user-list-container { + min-height: 160px; + max-height: 200px; + margin: 10px 0; + overflow: auto; +} +.user-select-container .user-list-container .user-item-container { + display: flex; + align-items: center; + justify-content: space-between; + height: 30px; + padding: 0 10px; font-size: 14px; - font-weight: 500; + cursor: pointer; } -.user-option-email { - font-size: 12px; +.user-select-container .user-list-container .user-item-container:hover, +.user-select-container .user-list-container .user-item-container-highlight { + background: #f5f5f5; + cursor: pointer; } -/* dropdown menu avatar is 24*24px, selection box avatar is 16*16px */ -.user-select .true__value-container .select-module.select-module-icon.avatar { - height: 16px; - width: 16px; +.no-search-result { + color: #666666; + font-size: 14px; + padding-left: 10px; + padding: 10px; + overflow: auto; } -.user-select .true__value-container .true__multi-value__label { - padding: 0px; +.user-select-popover .popover { + width: 385px; + max-width: 385px; } -.user-select .true__value-container .true__multi-value__label .select-module.select-module-icon.avatar { - transform: translateY(-2px); -} - -.user-select .true__value-container .select-module.select-module-name { - font-size: 13px; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - flex: 1 1; - line-height: 20px; - margin-left: 5px; +.user-select-popover .popover .user-item { + background: transparent; }