mirror of
https://github.com/haiwen/seahub.git
synced 2025-09-19 18:29:23 +00:00
add-share-link-authentication (#6201)
* add-share-link-authentication * [share link - link creation] 'set scope': redesigned it * update * Update share_link_auth.py * Update share_link_auth.py * Update share_link_auth.py * Update share_link_auth.py * Update share_link_auth.py * Update share_link_auth.py * [share link - link details] redesigned the panel * [share dialog - share link] 'authenticated users/emails' panels: redesigned them * [share dialog - share link'] UI details, UX, and code improvements for 'link detais, authenticated users/emails' panels * [share dialog - share link] updated the 'submit' handler for the 'authenticated emails/users' panels; fixup for 'set scope' in the 'link creation' panel' * [share dialog - share link] deleted 'share-link-api.js', moved api modification to seafile-js; fixed 'change scope' & etc. in 'link details' * [share dialog - share link] link authenticated users: update 'submit' handler according to the python API update * [share dialog - share link] added 'share-link-api.js' back & used it * [share dialog - share link] handled eslint warnings and etc. --------- Co-authored-by: llj <lingjun.li1@gmail.com>
This commit is contained in:
@@ -0,0 +1,50 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import SelectEditor from './select-editor';
|
||||
import { gettext, isEmailConfigured } from '../../utils/constants';
|
||||
|
||||
const propTypes = {
|
||||
isTextMode: PropTypes.bool.isRequired,
|
||||
isEditIconShow: PropTypes.bool.isRequired,
|
||||
currentScope: PropTypes.string.isRequired,
|
||||
onScopeChanged: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
class ShareLinkScopeEditor extends React.Component {
|
||||
|
||||
translateScope = (scope) => {
|
||||
if (scope === 'all_users') {
|
||||
return gettext('Anyone with the link');
|
||||
}
|
||||
|
||||
if (scope === 'specific_users') {
|
||||
return gettext('Specific users in the team');
|
||||
}
|
||||
|
||||
if (scope === 'specific_emails') {
|
||||
return gettext('Specific people with email address');
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
render() {
|
||||
let scopeOptions = ['all_users', 'specific_users'];
|
||||
if (isEmailConfigured) {
|
||||
scopeOptions.push('specific_emails');
|
||||
}
|
||||
return (
|
||||
<SelectEditor
|
||||
isTextMode={this.props.isTextMode}
|
||||
isEditIconShow={this.props.isEditIconShow}
|
||||
options={scopeOptions}
|
||||
currentOption={this.props.currentScope}
|
||||
onOptionChanged={this.props.onScopeChanged}
|
||||
translateOption={this.translateScope}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ShareLinkScopeEditor.propTypes = propTypes;
|
||||
|
||||
export default ShareLinkScopeEditor;
|
@@ -9,6 +9,8 @@ import Loading from '../loading';
|
||||
import LinkDetails from './link-details';
|
||||
import LinkCreation from './link-creation';
|
||||
import LinkList from './link-list';
|
||||
import LinkAuthenticatedUsers from './link-authenticated-users';
|
||||
import LinkAuthenticatedEmails from './link-authenticated-emails';
|
||||
|
||||
const propTypes = {
|
||||
itemPath: PropTypes.string.isRequired,
|
||||
@@ -240,6 +242,7 @@ class ShareLinkPanel extends React.Component {
|
||||
updateLink={this.updateLink}
|
||||
deleteLink={this.deleteLink}
|
||||
closeShareDialog={this.props.closeShareDialog}
|
||||
setMode={this.setMode}
|
||||
/>
|
||||
);
|
||||
case 'singleLinkCreation':
|
||||
@@ -268,6 +271,24 @@ class ShareLinkPanel extends React.Component {
|
||||
updateAfterCreation={this.updateAfterCreation}
|
||||
/>
|
||||
);
|
||||
case 'linkAuthenticatedUsers':
|
||||
return (
|
||||
<LinkAuthenticatedUsers
|
||||
repoID={repoID}
|
||||
linkToken={sharedLinkInfo.token}
|
||||
setMode={this.setMode}
|
||||
path={itemPath}
|
||||
/>
|
||||
);
|
||||
case 'linkAuthenticatedEmails':
|
||||
return (
|
||||
<LinkAuthenticatedEmails
|
||||
repoID={repoID}
|
||||
linkToken={sharedLinkInfo.token}
|
||||
setMode={this.setMode}
|
||||
path={itemPath}
|
||||
/>
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<LinkList
|
||||
|
@@ -0,0 +1,229 @@
|
||||
import React, { Fragment } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { gettext } from '../../utils/constants';
|
||||
import { Button } from 'reactstrap';
|
||||
import { Utils } from '../../utils/utils';
|
||||
import Loading from '../loading';
|
||||
import toaster from '../toast';
|
||||
import { shareLinkAPI } from '../../utils/share-link-api';
|
||||
|
||||
class EmailItem extends React.Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
isHighlighted: false,
|
||||
isOperationShow: false,
|
||||
};
|
||||
}
|
||||
|
||||
onMouseEnter = () => {
|
||||
this.setState({
|
||||
isHighlighted: true,
|
||||
isOperationShow: true
|
||||
});
|
||||
};
|
||||
|
||||
onMouseLeave = () => {
|
||||
this.setState({
|
||||
isHighlighted: false,
|
||||
isOperationShow: false
|
||||
});
|
||||
};
|
||||
|
||||
deleteItem = () => {
|
||||
const { item } = this.props;
|
||||
this.props.deleteItem(item);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { item } = this.props;
|
||||
return (
|
||||
<tr
|
||||
className={this.state.isHighlighted ? 'tr-highlight' : ''}
|
||||
onMouseEnter={this.onMouseEnter}
|
||||
onMouseLeave={this.onMouseLeave}
|
||||
onFocus={this.onMouseEnter}
|
||||
tabIndex="0"
|
||||
>
|
||||
<td>{item}</td>
|
||||
<td>
|
||||
<span
|
||||
tabIndex="0"
|
||||
role="button"
|
||||
className={`sf2-icon-x3 action-icon ${this.state.isOperationShow ? '' : 'hide'}`}
|
||||
onClick={this.deleteItem}
|
||||
onKeyDown={Utils.onKeyDown}
|
||||
title={gettext('Delete')}
|
||||
aria-label={gettext('Delete')}
|
||||
>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
EmailItem.propTypes = {
|
||||
repoID: PropTypes.string.isRequired,
|
||||
item: PropTypes.string.isRequired,
|
||||
deleteItem: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
const propTypes = {
|
||||
repoID: PropTypes.string.isRequired,
|
||||
linkToken: PropTypes.string,
|
||||
setMode: PropTypes.func,
|
||||
path: PropTypes.string,
|
||||
};
|
||||
|
||||
class LinkAuthenticatedEmails extends React.Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
inputEmails: '',
|
||||
authEmails: [],
|
||||
isSubmitting: false
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.getItems();
|
||||
}
|
||||
|
||||
getItems = () => {
|
||||
const { linkToken, path } = this.props;
|
||||
shareLinkAPI.listShareLinkAuthEmails(linkToken, path).then(res => {
|
||||
this.setState({ authEmails: res.data.auth_list });
|
||||
}).catch(error => {
|
||||
let errMessage = Utils.getErrorMsg(error);
|
||||
toaster.danger(errMessage);
|
||||
});
|
||||
};
|
||||
|
||||
onSubmit = () => {
|
||||
const { linkToken, path } = this.props;
|
||||
const { inputEmails, authEmails } = this.state;
|
||||
this.setState({
|
||||
isSubmitting: true
|
||||
});
|
||||
shareLinkAPI.addShareLinkAuthEmails(linkToken, inputEmails, path).then(res => {
|
||||
const { success, failed } = res.data;
|
||||
let newEmails = [];
|
||||
if (success.length) {
|
||||
newEmails = success.map(item => item.email);
|
||||
let msg = gettext('Successfully added %s.').replace('%s', newEmails.join(', '));
|
||||
toaster.success(msg);
|
||||
}
|
||||
if (failed.length) {
|
||||
failed.forEach(item => {
|
||||
let msg = `${item.email}: ${item.error_msg}`;
|
||||
toaster.danger(msg);
|
||||
});
|
||||
}
|
||||
this.setState({
|
||||
authEmails: newEmails.concat(authEmails),
|
||||
inputEmails: '',
|
||||
isSubmitting: false
|
||||
});
|
||||
}).catch(error => {
|
||||
let errMessage = Utils.getErrorMsg(error);
|
||||
toaster.danger(errMessage);
|
||||
this.setState({
|
||||
isSubmitting: false
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
deleteItem = (email) => {
|
||||
const { linkToken, path } = this.props;
|
||||
let emails = [email,];
|
||||
shareLinkAPI.deleteShareLinkAuthEmails(linkToken, emails, path).then(res => {
|
||||
let authEmails = this.state.authEmails.filter(e => {
|
||||
return e !== email;
|
||||
});
|
||||
this.setState({
|
||||
authEmails: authEmails
|
||||
});
|
||||
}).catch(error => {
|
||||
let errMessage = Utils.getErrorMsg(error);
|
||||
toaster.danger(errMessage);
|
||||
});
|
||||
};
|
||||
|
||||
goBack = () => {
|
||||
this.props.setMode('displayLinkDetails');
|
||||
};
|
||||
|
||||
handleInputChange = (e) => {
|
||||
this.setState({
|
||||
inputEmails: e.target.value
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
const { authEmails, inputEmails, isSubmitting } = this.state;
|
||||
const btnDisabled = !inputEmails.trim() || isSubmitting;
|
||||
const thead = (
|
||||
<thead>
|
||||
<tr>
|
||||
<th width="82%"></th>
|
||||
<th width="18%"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
);
|
||||
return (
|
||||
<Fragment>
|
||||
<div className="d-flex align-items-center pb-2 border-bottom">
|
||||
<h6 className="font-weight-normal m-0">
|
||||
<button
|
||||
className="sf3-font sf3-font-arrow rotate-180 d-inline-block back-icon border-0 bg-transparent text-secondary p-0 mr-2"
|
||||
onClick={this.goBack}
|
||||
title={gettext('Back')}
|
||||
aria-label={gettext('Back')}
|
||||
>
|
||||
</button>
|
||||
{gettext('Authenticated emails')}
|
||||
</h6>
|
||||
</div>
|
||||
<table className="table-thead-hidden w-xs-200">
|
||||
{thead}
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<input type="text" className="form-control" value={inputEmails} onChange={this.handleInputChange} placeholder={gettext('Emails, separated by \',\'')} />
|
||||
</td>
|
||||
<td>
|
||||
<Button disabled={btnDisabled} onClick={this.onSubmit}>
|
||||
{isSubmitting ? <Loading /> : gettext('Submit')}
|
||||
</Button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div className="share-list-container">
|
||||
<table className="table-thead-hidden w-xs-200">
|
||||
{thead}
|
||||
<tbody>
|
||||
{authEmails.map((item, index) => {
|
||||
return (
|
||||
<EmailItem
|
||||
key={index}
|
||||
item={item}
|
||||
repoID={this.props.repoID}
|
||||
deleteItem={this.deleteItem}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
LinkAuthenticatedEmails.propTypes = propTypes;
|
||||
|
||||
export default LinkAuthenticatedEmails;
|
@@ -0,0 +1,231 @@
|
||||
import React, { Fragment } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { gettext } from '../../utils/constants';
|
||||
import { Button } from 'reactstrap';
|
||||
import { shareLinkAPI } from '../../utils/share-link-api';
|
||||
import { Utils } from '../../utils/utils';
|
||||
import UserSelect from '../user-select';
|
||||
import toaster from '../toast';
|
||||
|
||||
class UserItem extends React.Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
isHighlighted: false,
|
||||
isOperationShow: false
|
||||
};
|
||||
}
|
||||
|
||||
onMouseEnter = () => {
|
||||
this.setState({
|
||||
isHighlighted: true,
|
||||
isOperationShow: true
|
||||
});
|
||||
};
|
||||
|
||||
onMouseLeave = () => {
|
||||
this.setState({
|
||||
isHighlighted: false,
|
||||
isOperationShow: false
|
||||
});
|
||||
};
|
||||
|
||||
deleteItem = () => {
|
||||
const { item } = this.props;
|
||||
this.props.deleteItem(item.username);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { item } = this.props;
|
||||
const { isHighlighted } = this.state;
|
||||
return (
|
||||
<tr
|
||||
className={isHighlighted ? 'tr-highlight' : ''}
|
||||
onMouseEnter={this.onMouseEnter}
|
||||
onMouseLeave={this.onMouseLeave}
|
||||
onFocus={this.onMouseEnter}
|
||||
tabIndex="0"
|
||||
>
|
||||
<td>
|
||||
<div className="d-flex align-items-center" title={item.contact_email}>
|
||||
<img src={item.avatar_url} width="24" alt={item.name} className="rounded-circle mr-2 cursor-pointer" />
|
||||
<span>{item.name}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<span
|
||||
tabIndex="0"
|
||||
role="button"
|
||||
className={`sf2-icon-x3 action-icon ${this.state.isOperationShow ? '' : 'hide'}`}
|
||||
onClick={this.deleteItem}
|
||||
onKeyDown={Utils.onKeyDown}
|
||||
title={gettext('Delete')}
|
||||
aria-label={gettext('Delete')}
|
||||
>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
UserItem.propTypes = {
|
||||
repoID: PropTypes.string.isRequired,
|
||||
item: PropTypes.object.isRequired,
|
||||
deleteItem: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
const propTypes = {
|
||||
repoID: PropTypes.string.isRequired,
|
||||
linkToken: PropTypes.string,
|
||||
setMode: PropTypes.func,
|
||||
path: PropTypes.string,
|
||||
};
|
||||
|
||||
class LinkAuthenticatedUsers extends React.Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
selectedOption: null,
|
||||
authUsers: []
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.listLinkAuthUsers();
|
||||
}
|
||||
|
||||
listLinkAuthUsers = () => {
|
||||
const { linkToken, path } = this.props;
|
||||
shareLinkAPI.listShareLinkAuthUsers(linkToken, path).then(res => {
|
||||
this.setState({ authUsers: res.data.auth_list });
|
||||
}).catch(error => {
|
||||
let errMessage = Utils.getErrorMsg(error);
|
||||
toaster.danger(errMessage);
|
||||
});
|
||||
};
|
||||
|
||||
addLinkAuthUsers = () => {
|
||||
const { linkToken, path } = this.props;
|
||||
const { selectedOption, authUsers } = this.state;
|
||||
if (!selectedOption || !selectedOption.length ) {
|
||||
return false;
|
||||
}
|
||||
const users = selectedOption.map((item, index) => item.email);
|
||||
shareLinkAPI.addShareLinkAuthUsers(linkToken, users, path).then(res => {
|
||||
const { success, failed } = res.data;
|
||||
if (success.length) {
|
||||
let newNames = success.map(item => item.name);
|
||||
let msg = gettext('Successfully added %s.').replace('%s', newNames.join(', '));
|
||||
toaster.success(msg);
|
||||
}
|
||||
if (failed.length) {
|
||||
failed.forEach(item => {
|
||||
let msg = `${item.name}: ${item.error_msg}`;
|
||||
toaster.danger(msg);
|
||||
});
|
||||
}
|
||||
this.setState({
|
||||
authUsers: success.concat(authUsers),
|
||||
selectedOption: null
|
||||
});
|
||||
this.refs.userSelect.clearSelect();
|
||||
}).catch(error => {
|
||||
let errMessage = Utils.getErrorMsg(error);
|
||||
toaster.danger(errMessage);
|
||||
});
|
||||
};
|
||||
|
||||
deleteItem = (username) => {
|
||||
const { linkToken, path } = this.props;
|
||||
let users = [username,];
|
||||
shareLinkAPI.deleteShareLinkAuthUsers(linkToken, users, path).then(res => {
|
||||
let authUsers = this.state.authUsers.filter(user => {
|
||||
return user.username !== username;
|
||||
});
|
||||
this.setState({
|
||||
authUsers: authUsers
|
||||
});
|
||||
}).catch(error => {
|
||||
let errMessage = Utils.getErrorMsg(error);
|
||||
toaster.danger(errMessage);
|
||||
});
|
||||
};
|
||||
|
||||
goBack = () => {
|
||||
this.props.setMode('displayLinkDetails');
|
||||
};
|
||||
|
||||
handleSelectChange = (option) => {
|
||||
this.setState({ selectedOption: option });
|
||||
};
|
||||
|
||||
render() {
|
||||
let { authUsers } = this.state;
|
||||
const thead = (
|
||||
<thead>
|
||||
<tr>
|
||||
<th width="82%"></th>
|
||||
<th width="18%"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
);
|
||||
return (
|
||||
<Fragment>
|
||||
<div className="d-flex align-items-center pb-2 border-bottom">
|
||||
<h6 className="font-weight-normal m-0">
|
||||
<button
|
||||
className="sf3-font sf3-font-arrow rotate-180 d-inline-block back-icon border-0 bg-transparent text-secondary p-0 mr-2"
|
||||
onClick={this.goBack}
|
||||
title={gettext('Back')}
|
||||
aria-label={gettext('Back')}
|
||||
>
|
||||
</button>
|
||||
{gettext('Authenticated users')}
|
||||
</h6>
|
||||
</div>
|
||||
<table className="table-thead-hidden w-xs-200">
|
||||
{thead}
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<UserSelect
|
||||
ref="userSelect"
|
||||
isMulti={true}
|
||||
placeholder={gettext('Search users')}
|
||||
onSelectChange={this.handleSelectChange}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<Button onClick={this.addLinkAuthUsers}>{gettext('Submit')}</Button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div className="share-list-container">
|
||||
<table className="table-thead-hidden w-xs-200">
|
||||
{thead}
|
||||
<tbody>
|
||||
{authUsers.map((item, index) => {
|
||||
return (
|
||||
<UserItem
|
||||
key={index}
|
||||
item={item}
|
||||
repoID={this.props.repoID}
|
||||
deleteItem={this.deleteItem}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
LinkAuthenticatedUsers.propTypes = propTypes;
|
||||
|
||||
export default LinkAuthenticatedUsers;
|
@@ -2,12 +2,14 @@ import React, { Fragment } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import moment from 'moment';
|
||||
import { Button, Form, FormGroup, Label, Input, InputGroup, InputGroupAddon, Alert } from 'reactstrap';
|
||||
import { gettext, shareLinkExpireDaysMin, shareLinkExpireDaysMax, shareLinkExpireDaysDefault, shareLinkForceUsePassword, shareLinkPasswordMinLength, shareLinkPasswordStrengthLevel } from '../../utils/constants';
|
||||
import { gettext, shareLinkExpireDaysMin, shareLinkExpireDaysMax, shareLinkExpireDaysDefault, shareLinkForceUsePassword, shareLinkPasswordMinLength, shareLinkPasswordStrengthLevel, isEmailConfigured } from '../../utils/constants';
|
||||
import { seafileAPI } from '../../utils/seafile-api';
|
||||
import { shareLinkAPI } from '../../utils/share-link-api';
|
||||
import { Utils } from '../../utils/utils';
|
||||
import ShareLink from '../../models/share-link';
|
||||
import toaster from '../toast';
|
||||
import SetLinkExpiration from '../set-link-expiration';
|
||||
import UserSelect from '../user-select';
|
||||
|
||||
const propTypes = {
|
||||
itemPath: PropTypes.string.isRequired,
|
||||
@@ -42,7 +44,11 @@ class LinkCreation extends React.Component {
|
||||
password: '',
|
||||
passwdnew: '',
|
||||
errorInfo: '',
|
||||
currentPermission: props.currentPermission
|
||||
currentPermission: props.currentPermission,
|
||||
|
||||
currentScope: 'all_users',
|
||||
selectedOption: null,
|
||||
inputEmails: ''
|
||||
};
|
||||
}
|
||||
|
||||
@@ -108,7 +114,7 @@ class LinkCreation extends React.Component {
|
||||
|
||||
let expirationTime = '';
|
||||
if (isExpireChecked) {
|
||||
if (expType == 'by-days') {
|
||||
if (expType === 'by-days') {
|
||||
expirationTime = moment().add(parseInt(expireDays), 'days').format();
|
||||
} else {
|
||||
expirationTime = expDate.format();
|
||||
@@ -116,15 +122,23 @@ class LinkCreation extends React.Component {
|
||||
}
|
||||
|
||||
let request;
|
||||
if (type == 'batch') {
|
||||
let users;
|
||||
if (type === 'batch') {
|
||||
const autoGeneratePassword = shareLinkForceUsePassword || isShowPasswordInput;
|
||||
request = seafileAPI.batchCreateMultiShareLink(repoID, itemPath, linkAmount, autoGeneratePassword, expirationTime, permissions);
|
||||
} else {
|
||||
request = seafileAPI.createMultiShareLink(repoID, itemPath, password, expirationTime, permissions);
|
||||
const { currentScope, selectedOption, inputEmails } = this.state;
|
||||
if ( currentScope === 'specific_users' && selectedOption ) {
|
||||
users = selectedOption.map((item, index) => item.email);
|
||||
}
|
||||
if (currentScope === 'specific_emails' && inputEmails) {
|
||||
users = inputEmails;
|
||||
}
|
||||
request = shareLinkAPI.createMultiShareLink(repoID, itemPath, password, expirationTime, permissions, currentScope, users);
|
||||
}
|
||||
|
||||
request.then((res) => {
|
||||
if (type == 'batch') {
|
||||
if (type === 'batch') {
|
||||
const newLinks = res.data.map(item => new ShareLink(item));
|
||||
this.props.updateAfterCreation(newLinks);
|
||||
} else {
|
||||
@@ -157,7 +171,7 @@ class LinkCreation extends React.Component {
|
||||
const { type } = this.props;
|
||||
let { linkAmount, isShowPasswordInput, password, passwdnew, isExpireChecked, expType, expireDays, expDate } = this.state;
|
||||
|
||||
if (type == 'batch') {
|
||||
if (type === 'batch') {
|
||||
if (!Number.isInteger(parseInt(linkAmount)) || parseInt(linkAmount) <= 1) {
|
||||
this.setState({ errorInfo: gettext('Please enter an integer bigger than 1 as number of links.') });
|
||||
return false;
|
||||
@@ -168,7 +182,7 @@ class LinkCreation extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
if (type == 'single' && isShowPasswordInput) {
|
||||
if (type === 'single' && isShowPasswordInput) {
|
||||
if (password.length === 0) {
|
||||
this.setState({ errorInfo: gettext('Please enter a password.') });
|
||||
return false;
|
||||
@@ -188,7 +202,7 @@ class LinkCreation extends React.Component {
|
||||
}
|
||||
|
||||
if (isExpireChecked) {
|
||||
if (expType == 'by-date') {
|
||||
if (expType === 'by-date') {
|
||||
if (!expDate) {
|
||||
this.setState({ errorInfo: gettext('Please select an expiration time') });
|
||||
return false;
|
||||
@@ -211,7 +225,7 @@ class LinkCreation extends React.Component {
|
||||
let minDays = shareLinkExpireDaysMin;
|
||||
let maxDays = shareLinkExpireDaysMax;
|
||||
|
||||
if (minDays !== 0 && maxDays == 0) {
|
||||
if (minDays !== 0 && maxDays === 0) {
|
||||
if (expireDays < minDays) {
|
||||
this.setState({ errorInfo: 'Please enter valid days' });
|
||||
return false;
|
||||
@@ -248,6 +262,20 @@ class LinkCreation extends React.Component {
|
||||
this.props.setMode('');
|
||||
};
|
||||
|
||||
setScope = (e) => {
|
||||
this.setState({ currentScope: e.target.value, selectedOption: null, inputEmails: '' });
|
||||
};
|
||||
|
||||
handleSelectChange = (option) => {
|
||||
this.setState({ selectedOption: option });
|
||||
};
|
||||
|
||||
handleInputChange = (e) => {
|
||||
this.setState({
|
||||
inputEmails: e.target.value
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
const { userPerm, type, permissionOptions } = this.props;
|
||||
const { isCustomPermission } = Utils.getUserPermission(userPerm);
|
||||
@@ -257,11 +285,11 @@ class LinkCreation extends React.Component {
|
||||
<div className="d-flex align-items-center pb-2 border-bottom">
|
||||
<h6 className="font-weight-normal m-0">
|
||||
<button className="sf3-font sf3-font-arrow rotate-180 d-inline-block back-icon border-0 bg-transparent text-secondary p-0 mr-2" onClick={this.goBack} title={gettext('Back')} aria-label={gettext('Back')}></button>
|
||||
{type == 'batch' ? gettext('Generate links in batch') : gettext('Generate Link')}
|
||||
{type === 'batch' ? gettext('Generate links in batch') : gettext('Generate Link')}
|
||||
</h6>
|
||||
</div>
|
||||
<Form className="pt-4">
|
||||
{type == 'batch' && (
|
||||
{type === 'batch' && (
|
||||
<FormGroup>
|
||||
<Label for="link-number" className="p-0">{gettext('Number of links')}</Label>
|
||||
<Input type="number" id="link-number" value={this.state.linkAmount} onChange={this.onLinkAmountChange} style={{ width: inputWidth }} />
|
||||
@@ -279,7 +307,7 @@ class LinkCreation extends React.Component {
|
||||
<span>{gettext('Add password protection')}</span>
|
||||
</Label>
|
||||
)}
|
||||
{type != 'batch' && this.state.isShowPasswordInput &&
|
||||
{type !== 'batch' && this.state.isShowPasswordInput &&
|
||||
<div className="ml-4">
|
||||
<FormGroup>
|
||||
<Label for="passwd">{gettext('Password')}</Label>
|
||||
@@ -345,6 +373,44 @@ class LinkCreation extends React.Component {
|
||||
})}
|
||||
</FormGroup>
|
||||
)}
|
||||
{type !== 'batch' && (
|
||||
<FormGroup check>
|
||||
<Label check>
|
||||
<span>{gettext('Set scope')}</span>
|
||||
</Label>
|
||||
<FormGroup check className="ml-4">
|
||||
<Label check>
|
||||
<Input type="radio" name='scope' value={'all_users'} checked={this.state.currentScope === 'all_users'} onChange={this.setScope} className="mr-1" />
|
||||
{gettext('Anyone with the link')}
|
||||
</Label>
|
||||
</FormGroup>
|
||||
<FormGroup check className="ml-4">
|
||||
<Label check>
|
||||
<Input type="radio" name='scope' value={'specific_users'} checked={this.state.currentScope === 'specific_users'} onChange={this.setScope} className="mr-1" />
|
||||
{gettext('Specific users in the team')}
|
||||
</Label>
|
||||
{this.state.currentScope === 'specific_users' &&
|
||||
<UserSelect
|
||||
ref="userSelect"
|
||||
isMulti={true}
|
||||
placeholder={gettext('Search users')}
|
||||
onSelectChange={this.handleSelectChange}
|
||||
/>
|
||||
}
|
||||
</FormGroup>
|
||||
{isEmailConfigured && (
|
||||
<FormGroup check className="ml-4">
|
||||
<Label check>
|
||||
<Input type="radio" name='scope' value={'specific_emails'} checked={this.state.currentScope === 'specific_emails'} onChange={this.setScope} className="mr-1" />
|
||||
{gettext('Specific people with email address')}
|
||||
</Label>
|
||||
{this.state.currentScope === 'specific_emails' &&
|
||||
<input type="text" className="form-control" value={this.state.inputEmails} onChange={this.handleInputChange} placeholder={gettext('Emails, separated by \',\'')}/>
|
||||
}
|
||||
</FormGroup>
|
||||
)}
|
||||
</FormGroup>
|
||||
)}
|
||||
{this.state.errorInfo && <Alert color="danger" className="mt-2">{gettext(this.state.errorInfo)}</Alert>}
|
||||
<Button color="primary" onClick={this.generateShareLink} className="mt-2 ml-1 mb-1">{gettext('Generate')}</Button>
|
||||
</Form>
|
||||
|
@@ -2,17 +2,19 @@ import React, { Fragment } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import moment from 'moment';
|
||||
import copy from 'copy-to-clipboard';
|
||||
import { Button } from 'reactstrap';
|
||||
import { Button, Input, InputGroup, InputGroupAddon } from 'reactstrap';
|
||||
import { gettext, shareLinkExpireDaysMin, shareLinkExpireDaysMax, shareLinkExpireDaysDefault, canSendShareLinkEmail } from '../../utils/constants';
|
||||
import Selector from '../../components/single-selector';
|
||||
import CommonOperationConfirmationDialog from '../../components/dialog/common-operation-confirmation-dialog';
|
||||
import { seafileAPI } from '../../utils/seafile-api';
|
||||
import { shareLinkAPI } from '../../utils/share-link-api';
|
||||
import { Utils } from '../../utils/utils';
|
||||
import ShareLink from '../../models/share-link';
|
||||
import toaster from '../toast';
|
||||
import SendLink from '../send-link';
|
||||
import SharedLink from '../shared-link';
|
||||
import SetLinkExpiration from '../set-link-expiration';
|
||||
import ShareLinkScopeEditor from '../select-editor/share-link-scope-editor';
|
||||
import SelectEditor from '../select-editor/select-editor';
|
||||
|
||||
const propTypes = {
|
||||
sharedLinkInfo: PropTypes.object.isRequired,
|
||||
@@ -24,7 +26,8 @@ const propTypes = {
|
||||
showLinkDetails: PropTypes.func.isRequired,
|
||||
updateLink: PropTypes.func.isRequired,
|
||||
deleteLink: PropTypes.func.isRequired,
|
||||
closeShareDialog: PropTypes.func.isRequired
|
||||
closeShareDialog: PropTypes.func.isRequired,
|
||||
setMode: PropTypes.func,
|
||||
};
|
||||
|
||||
class LinkDetails extends React.Component {
|
||||
@@ -34,11 +37,9 @@ class LinkDetails extends React.Component {
|
||||
this.state = {
|
||||
storedPasswordVisible: false,
|
||||
isEditingExpiration: false,
|
||||
isExpirationEditIconShow: false,
|
||||
expType: 'by-days',
|
||||
expireDays: this.props.defaultExpireDays,
|
||||
expDate: null,
|
||||
isOpIconShown: false,
|
||||
isLinkDeleteDialogOpen: false,
|
||||
isSendLinkShown: false
|
||||
};
|
||||
@@ -62,14 +63,6 @@ class LinkDetails extends React.Component {
|
||||
});
|
||||
};
|
||||
|
||||
handleMouseOverExpirationEditIcon = () => {
|
||||
this.setState({ isExpirationEditIconShow: true });
|
||||
};
|
||||
|
||||
handleMouseOutExpirationEditIcon = () => {
|
||||
this.setState({ isExpirationEditIconShow: false });
|
||||
};
|
||||
|
||||
editingExpirationToggle = () => {
|
||||
this.setState({ isEditingExpiration: !this.state.isEditingExpiration });
|
||||
};
|
||||
@@ -95,7 +88,7 @@ class LinkDetails extends React.Component {
|
||||
const { sharedLinkInfo } = this.props;
|
||||
const { expType, expireDays, expDate } = this.state;
|
||||
let expirationTime = '';
|
||||
if (expType == 'by-days') {
|
||||
if (expType === 'by-days') {
|
||||
expirationTime = moment().add(parseInt(expireDays), 'days').format();
|
||||
} else {
|
||||
expirationTime = expDate.format();
|
||||
@@ -111,17 +104,9 @@ class LinkDetails extends React.Component {
|
||||
});
|
||||
};
|
||||
|
||||
handleMouseOver = () => {
|
||||
this.setState({ isOpIconShown: true });
|
||||
};
|
||||
|
||||
handleMouseOut = () => {
|
||||
this.setState({ isOpIconShown: false });
|
||||
};
|
||||
|
||||
changePerm = (permOption) => {
|
||||
const { sharedLinkInfo } = this.props;
|
||||
const { permissionDetails } = Utils.getShareLinkPermissionObject(permOption.value);
|
||||
const { permissionDetails } = Utils.getShareLinkPermissionObject(permOption);
|
||||
seafileAPI.updateShareLink(sharedLinkInfo.token, JSON.stringify(permissionDetails)).then((res) => {
|
||||
this.props.updateLink(new ShareLink(res.data));
|
||||
}).catch((error) => {
|
||||
@@ -148,24 +133,43 @@ class LinkDetails extends React.Component {
|
||||
this.props.showLinkDetails(null);
|
||||
};
|
||||
|
||||
changeScope = (scope) => {
|
||||
const { sharedLinkInfo } = this.props;
|
||||
const { token } = sharedLinkInfo;
|
||||
shareLinkAPI.updateShareLink(token, '', '', scope).then((res) => {
|
||||
this.props.updateLink(new ShareLink(res.data));
|
||||
}).catch((error) => {
|
||||
let errMessage = Utils.getErrorMsg(error);
|
||||
toaster.danger(errMessage);
|
||||
});
|
||||
};
|
||||
|
||||
onUserAuth = () => {
|
||||
this.props.setMode('linkAuthenticatedUsers');
|
||||
};
|
||||
|
||||
onEmailAuth = () => {
|
||||
this.props.setMode('linkAuthenticatedEmails');
|
||||
};
|
||||
|
||||
getPermissionText = (perm) => {
|
||||
return Utils.getShareLinkPermissionObject(perm).text;
|
||||
};
|
||||
|
||||
render() {
|
||||
const { sharedLinkInfo, permissionOptions } = this.props;
|
||||
const { isOpIconShown } = this.state;
|
||||
const { user_scope: currentScope } = sharedLinkInfo;
|
||||
const currentPermission = Utils.getShareLinkPermissionStr(sharedLinkInfo.permissions);
|
||||
this.permOptions = permissionOptions.map(item => {
|
||||
return {
|
||||
value: item,
|
||||
text: Utils.getShareLinkPermissionObject(item).text,
|
||||
isSelected: item == currentPermission
|
||||
};
|
||||
});
|
||||
const currentSelectedPermOption = this.permOptions.filter(item => item.isSelected)[0];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<button className="sf3-font sf3-font-arrow rotate-180 d-inline-block back-icon border-0 bg-transparent text-secondary p-0" onClick={this.goBack} title={gettext('Back')} aria-label={gettext('Back')}></button>
|
||||
<div className="d-flex align-items-center pb-2 border-bottom">
|
||||
<h6 className="font-weight-normal m-0">
|
||||
<button className="sf3-font sf3-font-arrow rotate-180 d-inline-block back-icon border-0 bg-transparent text-secondary p-0 mr-2" onClick={this.goBack} title={gettext('Back')} aria-label={gettext('Back')}></button>
|
||||
{gettext('Link')}
|
||||
</h6>
|
||||
</div>
|
||||
<dl>
|
||||
<dt className="text-secondary font-weight-normal">{gettext('Link:')}</dt>
|
||||
<dt className="text-secondary font-weight-normal">{gettext('Link')}</dt>
|
||||
<dd>
|
||||
<SharedLink
|
||||
link={sharedLinkInfo.link}
|
||||
@@ -175,7 +179,7 @@ class LinkDetails extends React.Component {
|
||||
</dd>
|
||||
{!sharedLinkInfo.is_dir && sharedLinkInfo.permissions.can_download && ( // just for file
|
||||
<>
|
||||
<dt className="text-secondary font-weight-normal">{gettext('Direct Download Link:')}</dt>
|
||||
<dt className="text-secondary font-weight-normal">{gettext('Direct Download Link')}</dt>
|
||||
<dd>
|
||||
<SharedLink
|
||||
link={`${sharedLinkInfo.link}?dl=1`}
|
||||
@@ -187,66 +191,100 @@ class LinkDetails extends React.Component {
|
||||
)}
|
||||
{sharedLinkInfo.password && (
|
||||
<>
|
||||
<dt className="text-secondary font-weight-normal">{gettext('Password:')}</dt>
|
||||
<dd className="d-flex align-items-center">
|
||||
<span className="mr-1">{this.state.storedPasswordVisible ? sharedLinkInfo.password : '***************'}</span>
|
||||
<span tabIndex="0" role="button" aria-label={this.state.storedPasswordVisible ? gettext('Hide') : gettext('Show')} onKeyDown={this.onIconKeyDown} onClick={this.toggleStoredPasswordVisible} className={`eye-icon sf3-font sf3-font-eye${this.state.storedPasswordVisible ? '' : '-slash'}`}></span>
|
||||
<dt className="text-secondary font-weight-normal">{gettext('Password')}</dt>
|
||||
<dd>
|
||||
<InputGroup className="w-50">
|
||||
{this.state.storedPasswordVisible ?
|
||||
<Input type="text" readOnly={true} value={sharedLinkInfo.password} /> :
|
||||
<Input type="text" readOnly={true} value={'***************'} />
|
||||
}
|
||||
<InputGroupAddon addonType="append">
|
||||
<Button
|
||||
aria-label={this.state.storedPasswordVisible ? gettext('Hide') : gettext('Show')}
|
||||
onClick={this.toggleStoredPasswordVisible}
|
||||
className={`link-operation-icon eye-icon sf3-font sf3-font-eye${this.state.storedPasswordVisible ? '' : '-slash'}`}
|
||||
>
|
||||
</Button>
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
</dd>
|
||||
</>
|
||||
)}
|
||||
{sharedLinkInfo.expire_date && (
|
||||
<>
|
||||
<dt className="text-secondary font-weight-normal">{gettext('Expiration Date:')}</dt>
|
||||
{!this.state.isEditingExpiration &&
|
||||
<dd style={{ width: '250px' }} onMouseEnter={this.handleMouseOverExpirationEditIcon} onMouseLeave={this.handleMouseOutExpirationEditIcon}>
|
||||
{moment(sharedLinkInfo.expire_date).format('YYYY-MM-DD HH:mm:ss')}
|
||||
{this.state.isExpirationEditIconShow && (
|
||||
<a href="#"
|
||||
role="button"
|
||||
aria-label={gettext('Edit')}
|
||||
title={gettext('Edit')}
|
||||
className="sf3-font sf3-font-rename attr-action-icon"
|
||||
onClick={this.editingExpirationToggle}>
|
||||
</a>
|
||||
<dt className="text-secondary font-weight-normal">{gettext('Expiration Date')}</dt>
|
||||
<dd>
|
||||
{this.state.isEditingExpiration ? (
|
||||
<div className="ml-4">
|
||||
<SetLinkExpiration
|
||||
minDays={shareLinkExpireDaysMin}
|
||||
maxDays={shareLinkExpireDaysMax}
|
||||
defaultDays={shareLinkExpireDaysDefault}
|
||||
expType={this.state.expType}
|
||||
setExpType={this.setExpType}
|
||||
expireDays={this.state.expireDays}
|
||||
onExpireDaysChanged={this.onExpireDaysChanged}
|
||||
expDate={this.state.expDate}
|
||||
onExpDateChanged={this.onExpDateChanged}
|
||||
/>
|
||||
<div className={this.state.expType === 'by-days' ? 'mt-2' : 'mt-3'}>
|
||||
<button className="btn btn-primary mr-2" onClick={this.updateExpiration}>{gettext('Update')}</button>
|
||||
<button className="btn btn-secondary" onClick={this.editingExpirationToggle}>{gettext('Cancel')}</button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<InputGroup className="w-50">
|
||||
<Input type="text" readOnly={true} value={moment(sharedLinkInfo.expire_date).format('YYYY-MM-DD HH:mm:ss')} />
|
||||
<InputGroupAddon addonType="append">
|
||||
<Button
|
||||
aria-label={gettext('Edit')}
|
||||
title={gettext('Edit')}
|
||||
className="link-operation-icon sf3-font sf3-font-rename"
|
||||
onClick={this.editingExpirationToggle}
|
||||
>
|
||||
</Button>
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
)}
|
||||
</dd>
|
||||
}
|
||||
{this.state.isEditingExpiration &&
|
||||
<dd>
|
||||
<div className="ml-4">
|
||||
<SetLinkExpiration
|
||||
minDays={shareLinkExpireDaysMin}
|
||||
maxDays={shareLinkExpireDaysMax}
|
||||
defaultDays={shareLinkExpireDaysDefault}
|
||||
expType={this.state.expType}
|
||||
setExpType={this.setExpType}
|
||||
expireDays={this.state.expireDays}
|
||||
onExpireDaysChanged={this.onExpireDaysChanged}
|
||||
expDate={this.state.expDate}
|
||||
onExpDateChanged={this.onExpDateChanged}
|
||||
/>
|
||||
<div className={this.state.expType == 'by-days' ? 'mt-2' : 'mt-3'}>
|
||||
<button className="btn btn-primary mr-2" onClick={this.updateExpiration}>{gettext('Update')}</button>
|
||||
<button className="btn btn-secondary" onClick={this.editingExpirationToggle}>{gettext('Cancel')}</button>
|
||||
</div>
|
||||
</div>
|
||||
</dd>
|
||||
}
|
||||
</>
|
||||
)}
|
||||
{sharedLinkInfo.permissions && (
|
||||
<>
|
||||
<dt className="text-secondary font-weight-normal">{gettext('Permission:')}</dt>
|
||||
<dd style={{ width: '250px' }} onMouseEnter={this.handleMouseOver} onMouseLeave={this.handleMouseOut}>
|
||||
<Selector
|
||||
isDropdownToggleShown={isOpIconShown && !sharedLinkInfo.is_expired}
|
||||
currentSelectedOption={currentSelectedPermOption}
|
||||
options={this.permOptions}
|
||||
selectOption={this.changePerm}
|
||||
/>
|
||||
<dd>
|
||||
<div className="w-50">
|
||||
<SelectEditor
|
||||
isTextMode={false}
|
||||
isEditIconShow={false}
|
||||
options={permissionOptions}
|
||||
currentOption={currentPermission}
|
||||
onOptionChanged={this.changePerm}
|
||||
translateOption={this.getPermissionText}
|
||||
/>
|
||||
</div>
|
||||
</dd>
|
||||
</>
|
||||
)}
|
||||
<>
|
||||
<dt className="text-secondary font-weight-normal">{gettext('Scope')}</dt>
|
||||
<dd className="d-flex align-items-center">
|
||||
<div className="w-50 mr-2">
|
||||
<ShareLinkScopeEditor
|
||||
isTextMode={false}
|
||||
isEditIconShow={false}
|
||||
currentScope={currentScope}
|
||||
onScopeChanged={this.changeScope}
|
||||
/>
|
||||
</div>
|
||||
{currentScope === 'specific_users' &&
|
||||
<Button color="primary" outline={true} className="border-0 p-0 link-authenticated-op" onClick={this.onUserAuth}>{gettext('Authenticated users')}</Button>
|
||||
}
|
||||
{currentScope === 'specific_emails' &&
|
||||
<Button color="primary" outline={true} className="border-0 p-0 link-authenticated-op" onClick={this.onEmailAuth}>{gettext('Authenticated emails')}</Button>
|
||||
}
|
||||
</dd>
|
||||
</>
|
||||
</dl>
|
||||
{(canSendShareLinkEmail && !this.state.isSendLinkShown) &&
|
||||
<Button onClick={this.toggleSendLink} className='mr-2'>{gettext('Send')}</Button>
|
||||
|
@@ -77,7 +77,7 @@ class LinkList extends React.Component {
|
||||
<p className="text-secondary">{gettext('No share links')}</p>
|
||||
</EmptyTip>
|
||||
) : (
|
||||
<div className='share-list-container share-link'>
|
||||
<div className='share-list-container share-link-list'>
|
||||
<table className="table-place-header">
|
||||
<thead>
|
||||
<tr>
|
||||
|
Reference in New Issue
Block a user