1
0
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:
Ranjiwei
2024-07-19 18:12:35 +08:00
committed by GitHub
parent 69eb6263cf
commit add4689b02
25 changed files with 1621 additions and 291 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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