1
0
mirror of https://github.com/haiwen/seahub.git synced 2025-09-25 06:33:48 +00:00

change search and select user (#7782)

* 01 basic function

* 02 add list and add group users

* 03 change style

* 04 other components

* 05 change className
This commit is contained in:
Michael An
2025-04-30 10:04:08 +08:00
committed by GitHub
parent 692e6b7e97
commit b8a74ccebc
22 changed files with 516 additions and 295 deletions

View File

@@ -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 {
<tr>
<td>
<UserSelect
ref={this.userSelect}
isMulti={true}
placeholder={gettext('Search users')}
onSelectChange={this.handleUserSelectChange}
value={this.state.selectedUsers}
selectedUsers={this.state.selectedUsers}
/>
</td>
{showPath &&

View File

@@ -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 (
<Modal isOpen={true} toggle={this.toggle}>
<SeahubModalHeader toggle={this.toggle}>{gettext('Add Admins')}</SeahubModalHeader>
<SeahubModalHeader toggle={this.toggle}>{gettext('Add Admin')}</SeahubModalHeader>
<ModalBody>
<UserSelect
ref={this.userSelect}
isMulti={false}
placeholder={gettext('Select a user as admin')}
onSelectChange={this.handleSelectChange}
selectedUsers={this.state.selectedUsers}
/>
{this.state.errMessage && <Alert color="danger" className="mt-2">{this.state.errMessage}</Alert>}
</ModalBody>

View File

@@ -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 {
<td>
<div className='add-members'>
<UserSelect
ref={this.userSelect}
isMulti={true}
className={classnames('reviewer-select', { 'user-select-right-btn': showDeptBtn })}
placeholder={gettext('Search users...')}
onSelectChange={this.handleSelectChange}
excludeCurrentUser={false}
selectedUsers={this.state.selectedUsers}
/>
{showDeptBtn &&
<span

View File

@@ -20,17 +20,17 @@ export default class AddDepartMemberV2Dialog extends React.Component {
constructor(props) {
super(props);
this.state = {
selectedOptions: [],
selectedUsers: [],
errMsgs: '',
};
}
handleSelectChange = (options) => {
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 && (
<ul className="list-unstyled">

View File

@@ -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}
/>
</ModalBody>
<ModalFooter>

View File

@@ -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 {
<UserSelect
placeholder={gettext('Search users')}
onSelectChange={this.handleSelectChange}
ref="orgSelect"
isMulti={true}
className='org-add-member-select'
selectedUsers={this.state.selectedUsers}
/>
{ this.state.errMessage && <p className="error">{this.state.errMessage}</p> }
</ModalBody>

View File

@@ -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}
/>
</ModalBody>
<ModalFooter>

View File

@@ -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 {
/>
<Label className="mt-2">
{gettext('Owner')}
<span className="small text-secondary">{gettext('(If left blank, owner will be admin)')}</span>
<span className="small text-secondary ml-1">{gettext('(If left blank, owner will be admin)')}</span>
</Label>
<UserSelect
isMulti={false}
placeholder={gettext('Select a user')}
onSelectChange={this.handleSelectChange}
selectedUsers={this.state.selectedUsers}
/>
</FormGroup>
</Form>

View File

@@ -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 {
<FormGroup>
<Label for="userSelect">
{gettext('Owner')}
<span className="small text-secondary">{gettext('(If left blank, owner will be admin)')}</span>
<span className="small text-secondary ml-1">{gettext('(If left blank, owner will be admin)')}</span>
</Label>
<UserSelect
id="userSelect"
isMulti={false}
placeholder={gettext('Select a user')}
onSelectChange={this.handleSelectChange}
selectedUsers={this.state.selectedUsers}
/>
</FormGroup>
</Form>

View File

@@ -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 {
<SeahubModalHeader toggle={this.props.toggle}>{gettext('Add Member')}</SeahubModalHeader>
<ModalBody>
<UserSelect
ref={this.userSelect}
isMulti={true}
placeholder={gettext('Search users')}
onSelectChange={this.handleSelectChange}
selectedUsers={this.state.selectedUsers}
/>
</ModalBody>
<ModalFooter>

View File

@@ -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 {
</SeahubModalHeader>
<ModalBody>
<UserSelect
ref={this.userSelect}
isMulti={false}
placeholder={gettext('Select a user')}
onSelectChange={this.handleSelectChange}
selectedUsers={this.state.selectedUsers}
/>
</ModalBody>
<ModalFooter>

View File

@@ -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 = '<span class="op-target" title=' + repoName + '>' + repoName + '</span>';
let msg = gettext('Transfer Library {library_name}');
let message = msg.replace('{library_name}', innerSpan);
return (
<Modal isOpen={true} toggle={this.props.toggle}>
<SeahubModalHeader toggle={this.props.toggle}>
<div dangerouslySetInnerHTML={{ __html: message }} />
</SeahubModalHeader>
<ModalBody>
<UserSelect
ref={this.userSelect}
isMulti={false}
placeholder={gettext('Search users')}
onSelectChange={this.handleSelectChange}
/>
</ModalBody>
<ModalFooter>
<Button color="secondary" onClick={this.props.toggle}>{gettext('Cancel')}</Button>
<Button color="primary" onClick={this.submit}>{gettext('Submit')}</Button>
</ModalFooter>
</Modal>
);
}
}
SysAdminRepoTransferDialog.propTypes = propTypes;
export default SysAdminRepoTransferDialog;

View File

@@ -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 {
<tr>
<td>
<UserSelect
ref={this.userSelect}
isMulti={true}
placeholder={gettext('Search users')}
onSelectChange={this.handleSelectChange}
selectedUsers={this.state.selectedUsers}
/>
</td>
<td>

View File

@@ -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 {
<TabPane tabId="transUser" role="tabpanel" id="transfer-user-panel">
<Label className='transfer-repo-label'>{gettext('Users')}</Label>
<UserSelect
ref={this.userSelect}
isMulti={false}
placeholder={gettext('Select a user')}
onSelectChange={this.handleSelectChange}
onSelectChange={this.onUsersChange}
selectedUsers={this.state.selectedUsers}
/>
<Switch
checked={reshare}
@@ -207,13 +212,19 @@ class TransferDialog extends React.Component {
};
render() {
const { selectedOption } = this.state;
const { selectedOption, activeTab } = this.state;
const { itemName: repoName } = this.props;
let title = gettext('Transfer Library {library_name}');
title = title.replace('{library_name}', '<span class="op-target text-truncate mx-1">' + Utils.HTMLescape(repoName) + '</span>');
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 (
<Modal isOpen={true} style={{ maxWidth: '720px' }} toggle={this.props.toggleDialog} className="transfer-dialog">

View File

@@ -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 {
<ModalBody>
<p>{gettext('Transfer group to')}</p>
<UserSelect
ref={this.userSelect}
isMulti={false}
placeholder={gettext('Please enter 1 or more character')}
onSelectChange={this.handleSelectChange}
selectedUsers={this.state.selectedUsers}
/>
</ModalBody>
<ModalFooter>

View File

@@ -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 {
<UserSelect
placeholder={gettext('Search users')}
onSelectChange={this.onSelectChange}
ref={this.userSelect}
selectedUsers={this.state.selectedUsers}
isMulti={true}
className="add-members-select"
/>
{showDeptBtn &&
<span onClick={this.onClickDeptBtn} className="sf3-font sf3-font-invite-visitors toggle-detail-btn"></span>
}
{this.state.selectedOption ?
{this.state.selectedUsers.length > 0 ?
<Button color="primary" onClick={this.addGroupMember}>{gettext('Submit')}</Button> :
<Button color="primary" disabled>{gettext('Submit')}</Button>
}

View File

@@ -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 {
<tr>
<td>
<UserSelect
ref={this.userSelect}
isMulti={true}
placeholder={gettext('Search users')}
onSelectChange={this.handleSelectChange}
selectedUsers={this.state.selectedUsers}
/>
</td>
<td>

View File

@@ -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 {
</Label>
{this.state.currentScope === 'specific_users' &&
<UserSelect
ref={this.userSelect}
isMulti={true}
placeholder={gettext('Search users')}
onSelectChange={this.handleSelectChange}
selectedUsers={this.state.selectedUsers}
/>
}
</FormGroup>

View File

@@ -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;
}

View File

@@ -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 (
<div className={classnames('user-item', className)} title={name}>
<span className="user-avatar">
<img className="user-avatar-icon" alt={name} src={avatar_url} />
</span>
<div className="d-flex align-items-center">
<span className="user-name">{name}</span>
{(enableShowContactEmailWhenSearchUser && !enableDeleteUser) && <span className="user-option-email">({contact_email})</span>}
{(enableShowLoginIDWhenSearchUser && !enableDeleteUser) && <span className="user-option-email">({login_id})</span>}
</div>
{enableDeleteUser && (
<span className="user-remove ml-2" onClick={this.onDeleteUser} title={gettext('Remove')}>
<i className="sf3-font sf3-font-x-01" aria-hidden="true"></i>
</span>
)}
</div>
);
}
}
UserItem.propTypes = propTypes;
export default UserItem;

View File

@@ -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) ? (
<div className="d-flex">
<img src={item.avatar_url} className="avatar" width="24" alt="" />
<div className="ml-2">
<span className="user-option-name">{item.name}</span><br />
{enableShowContactEmailWhenSearchUser && <span className="user-option-email">{item.contact_email}</span>}
{enableShowLoginIDWhenSearchUser && <span className="user-option-email">{item.login_id}</span>}
</div>
</div>
) : (
<React.Fragment>
<img src={item.avatar_url} className="select-module select-module-icon avatar" alt=""/>
<span className='select-module select-module-name'>{item.name}</span>
</React.Fragment>
);
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 (
<AsyncSelect
isClearable
classNamePrefix
components={{
NoOptionsMessage: (props) => {
return (
<div {...props.innerProps} style={NoOptionsStyle}>
{searchValue ? gettext('User not found') : gettext('Enter characters to start searching')}
<ClickOutside onClickOutside={this.onClickOutside}>
<>
<div className={classnames('selected-user-item-container form-control d-flex align-items-center', className, { 'focus': this.state.isPopoverOpen })} id="user-select" onClick={this.onTogglePopover}>
{selectedUsers.map((user, index) => {
return (
<UserItem
key={index}
user={user}
enableDeleteUser={true}
onDeleteUser={this.onDeleteSelectedCollaborator}
/>
);
})}
{selectedUsers.length === 0 && (
<div className="user-select-placeholder">
{this.props.placeholder || gettext('Select users')}
</div>
);
}
}}
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}
/>
)}
</div>
<Popover
placement="bottom-start"
isOpen={this.state.isPopoverOpen}
target={'user-select'}
hideArrow={true}
fade={false}
className="user-select-popover"
>
<div className="user-select-container" ref={ref => this.ref = ref} onMouseDown={e => e.stopPropagation()}>
<div className="user-search-container">
<SearchInput
autoFocus={true}
placeholder={this.props.placeholder || gettext('Search users')}
value={searchValue}
onChange={this.onValueChanged}
onKeyDown={this.onKeyDown}
/>
</div>
<div className="user-list-container" ref={ref => this.container = ref}>
{searchedUsers.length > 0 && (
searchedUsers.map((user, index) => {
return (
<div
key={user.email}
className={classnames('user-item-container', { 'user-item-container-highlight': index === highlightIndex })}
ref={ref => this.userItem = ref}
onClick={this.onUserClick.bind(this, user)}
>
<UserItem user={user} enableDeleteUser={false} />
</div>
);
})
)}
{searchedUsers.length === 0 &&
<div className="no-search-result">
{searchValue ? gettext('User not found') : gettext('Enter characters to start searching')}
</div>
}
</div>
</div>
</Popover>
</>
</ClickOutside>
);
}
}

View File

@@ -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;
}