1
0
mirror of https://github.com/haiwen/seahub.git synced 2025-09-02 23:48:47 +00:00

[share dialog] share link / upload link: enable user to choose expira… (#4557)

* [share dialog] share link / upload link: enable user to choose expiration time

* fixup & improvement for 'share link / upload link'

* fixup for pages in which there is no 'share'

* for pages in which the variables are not returned

* [date-and-time picker] fixed UI.

* updated version of '@seafile/seafile-calendar' to fix its 'selected time' UI problem

* [share dialog] expiration time: improved 'disabledDate'

* update create share/upload link api

handle expiration_time parameter

* update

* update

* update

Co-authored-by: lian <lian@seafile.com>
This commit is contained in:
llj
2020-05-21 11:32:02 +08:00
committed by GitHub
parent 0a2d61c24b
commit 1f08158663
12 changed files with 453 additions and 153 deletions

View File

@@ -6,7 +6,7 @@
"@reach/router": "^1.2.0", "@reach/router": "^1.2.0",
"@seafile/react-image-lightbox": "0.0.1", "@seafile/react-image-lightbox": "0.0.1",
"@seafile/resumablejs": "^1.1.15", "@seafile/resumablejs": "^1.1.15",
"@seafile/seafile-calendar": "0.0.8", "@seafile/seafile-calendar": "0.0.12",
"@seafile/seafile-editor": "^0.2.84", "@seafile/seafile-editor": "^0.2.84",
"MD5": "^1.3.0", "MD5": "^1.3.0",
"autoprefixer": "7.1.6", "autoprefixer": "7.1.6",

View File

@@ -0,0 +1,78 @@
import React from 'react';
import PropTypes from 'prop-types';
import moment from 'moment';
import Calendar from '@seafile/seafile-calendar';
import DatePicker from '@seafile/seafile-calendar/lib/Picker';
import { translateCalendar } from '../utils/date-format-utils';
import '@seafile/seafile-calendar/assets/index.css';
import '../css/date-and-time-picker.css';
const propsTypes = {
disabledDate: PropTypes.func.isRequired,
value: PropTypes.object,
onChange: PropTypes.func.isRequired
};
const FORMAT = 'YYYY-MM-DD HH:mm';
class Picker extends React.Component {
constructor(props) {
super(props);
this.defaultCalendarValue = null;
this.calendarContainerRef = React.createRef();
}
componentDidMount() {
let lang = window.app.config.lang;
this.defaultCalendarValue = moment().locale(lang).clone();
}
getCalendarContainer = () => {
return this.calendarContainerRef.current;
}
render() {
const props = this.props;
const calendar = (<Calendar
defaultValue={this.defaultCalendarValue}
disabledDate={props.disabledDate}
format={FORMAT}
locale={translateCalendar()}
showHourAndMinute={true}
/>);
return (
<DatePicker
disabled={props.disabled}
getCalendarContainer={this.getCalendarContainer}
calendar={calendar}
value={props.value}
onChange={props.onChange}
>
{
({value}) => {
return (
<div>
<input
placeholder={FORMAT}
style={{ width: props.inputWidth || 250 }}
tabIndex="-1"
disabled={props.disabled}
readOnly
value={value && value.format(FORMAT) || ''}
className="form-control"
/>
<div ref={this.calendarContainerRef} />
</div>
);
}
}
</DatePicker>
);
}
}
Picker.propsTypes = propsTypes;
export default Picker;

View File

@@ -2,7 +2,7 @@ import React, { Fragment } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import moment from 'moment'; import moment from 'moment';
import copy from 'copy-to-clipboard'; import copy from 'copy-to-clipboard';
import { Button, Form, FormGroup, Label, Input, InputGroup, InputGroupAddon, Alert } from 'reactstrap'; import { Button, Form, FormGroup, Label, Input, InputGroup, InputGroupAddon, InputGroupText, Alert, FormText } from 'reactstrap';
import { isPro, gettext, shareLinkExpireDaysMin, shareLinkExpireDaysMax, shareLinkExpireDaysDefault, shareLinkPasswordMinLength, canSendShareLinkEmail } from '../../utils/constants'; import { isPro, gettext, shareLinkExpireDaysMin, shareLinkExpireDaysMax, shareLinkExpireDaysDefault, shareLinkPasswordMinLength, canSendShareLinkEmail } from '../../utils/constants';
import ShareLinkPermissionEditor from '../../components/select-editor/share-link-permission-editor'; import ShareLinkPermissionEditor from '../../components/select-editor/share-link-permission-editor';
import { seafileAPI } from '../../utils/seafile-api'; import { seafileAPI } from '../../utils/seafile-api';
@@ -11,6 +11,7 @@ import ShareLink from '../../models/share-link';
import toaster from '../toast'; import toaster from '../toast';
import Loading from '../loading'; import Loading from '../loading';
import SendLink from '../send-link'; import SendLink from '../send-link';
import DateTimePicker from '../date-and-time-picker';
const propTypes = { const propTypes = {
itemPath: PropTypes.string.isRequired, itemPath: PropTypes.string.isRequired,
@@ -18,23 +19,41 @@ const propTypes = {
closeShareDialog: PropTypes.func.isRequired, closeShareDialog: PropTypes.func.isRequired,
}; };
const inputWidth = Utils.isDesktop() ? 250 : 210;
class GenerateShareLink extends React.Component { class GenerateShareLink extends React.Component {
constructor(props) { constructor(props) {
super(props); super(props);
this.isExpireDaysNoLimit = (parseInt(shareLinkExpireDaysMin) === 0 && parseInt(shareLinkExpireDaysMax) === 0 && shareLinkExpireDaysDefault == 0); this.isExpireDaysNoLimit = (shareLinkExpireDaysMin === 0 && shareLinkExpireDaysMax === 0 && shareLinkExpireDaysDefault == 0);
this.defaultExpireDays = this.isExpireDaysNoLimit ? '' : shareLinkExpireDaysDefault; this.defaultExpireDays = this.isExpireDaysNoLimit ? '' : shareLinkExpireDaysDefault;
let expirationLimitTip = '';
if (shareLinkExpireDaysMin !== 0 && shareLinkExpireDaysMax !== 0) {
expirationLimitTip = gettext('{minDays_placeholder} - {maxDays_placeholder} days')
.replace('{minDays_placeholder}', shareLinkExpireDaysMin)
.replace('{maxDays_placeholder}', shareLinkExpireDaysMax);
} else if (shareLinkExpireDaysMin !== 0 && shareLinkExpireDaysMax === 0) {
expirationLimitTip = gettext('Greater than or equal to {minDays_placeholder} days')
.replace('{minDays_placeholder}', shareLinkExpireDaysMin);
} else if (shareLinkExpireDaysMin === 0 && shareLinkExpireDaysMax !== 0) {
expirationLimitTip = gettext('Less than or equal to {maxDays_placeholder} days')
.replace('{maxDays_placeholder}', shareLinkExpireDaysMax);
}
this.expirationLimitTip = expirationLimitTip;
this.state = { this.state = {
isOpIconShown: false, isOpIconShown: false,
isValidate: false, isValidate: false,
isShowPasswordInput: false, isShowPasswordInput: false,
isPasswordVisible: false, isPasswordVisible: false,
isExpireChecked: !this.isExpireDaysNoLimit, isExpireChecked: !this.isExpireDaysNoLimit,
setExp: 'by-days',
expireDays: this.defaultExpireDays,
expDate: null,
password: '', password: '',
passwdnew: '', passwdnew: '',
expireDays: this.defaultExpireDays,
errorInfo: '', errorInfo: '',
sharedLinkInfo: null, sharedLinkInfo: null,
isNoticeMessageShow: false, isNoticeMessageShow: false,
@@ -93,6 +112,39 @@ class GenerateShareLink extends React.Component {
} }
} }
setExp = (e) => {
this.setState({
setExp: e.target.value
});
}
disabledDate = (current) => {
if (!current) {
// allow empty select
return false;
}
if (this.isExpireDaysNoLimit) {
return current.isBefore(moment(), 'day');
}
const startDay = moment().add(shareLinkExpireDaysMin, 'days');
const endDay = moment().add(shareLinkExpireDaysMax, 'days');
if (shareLinkExpireDaysMin !== 0 && shareLinkExpireDaysMax !== 0) {
return current.isBefore(startDay, 'day') || current.isAfter(endDay, 'day');
} else if (shareLinkExpireDaysMin !== 0 && shareLinkExpireDaysMax === 0) {
return current.isBefore(startDay, 'day');
} else if (shareLinkExpireDaysMin === 0 && shareLinkExpireDaysMax !== 0) {
return current.isBefore(moment(), 'day') || current.isAfter(endDay, 'day');
}
}
onExpDateChanged = (value) => {
this.setState({
expDate: value
});
}
onPasswordInputChecked = () => { onPasswordInputChecked = () => {
this.setState({ this.setState({
isShowPasswordInput: !this.state.isShowPasswordInput, isShowPasswordInput: !this.state.isShowPasswordInput,
@@ -135,14 +187,21 @@ class GenerateShareLink extends React.Component {
if (isValid) { if (isValid) {
this.setState({errorInfo: ''}); this.setState({errorInfo: ''});
let { itemPath, repoID } = this.props; let { itemPath, repoID } = this.props;
let { password, isExpireChecked, expireDays } = this.state; let { password, isExpireChecked, setExp, expireDays, expDate } = this.state;
let permissions; let permissions;
if (isPro) { if (isPro) {
const permissionDetails = Utils.getShareLinkPermissionObject(this.state.currentPermission).permissionDetails; const permissionDetails = Utils.getShareLinkPermissionObject(this.state.currentPermission).permissionDetails;
permissions = JSON.stringify(permissionDetails); permissions = JSON.stringify(permissionDetails);
} }
const expireDaysSent = isExpireChecked ? expireDays : ''; let expirationTime = '';
seafileAPI.createShareLink(repoID, itemPath, password, expireDaysSent, permissions).then((res) => { if (isExpireChecked) {
if (setExp == 'by-days') {
expirationTime = moment().add(parseInt(expireDays), 'days').format();
} else {
expirationTime = expDate.format();
}
}
seafileAPI.createShareLink(repoID, itemPath, password, expirationTime, permissions).then((res) => {
let sharedLinkInfo = new ShareLink(res.data); let sharedLinkInfo = new ShareLink(res.data);
this.setState({sharedLinkInfo: sharedLinkInfo}); this.setState({sharedLinkInfo: sharedLinkInfo});
}).catch((error) => { }).catch((error) => {
@@ -195,7 +254,8 @@ class GenerateShareLink extends React.Component {
} }
validateParamsInput = () => { validateParamsInput = () => {
let { isShowPasswordInput , password, passwdnew, isExpireChecked, expireDays } = this.state; let { isShowPasswordInput, password, passwdnew, isExpireChecked, setExp, expireDays, expDate } = this.state;
// validate password // validate password
if (isShowPasswordInput) { if (isShowPasswordInput) {
if (password.length === 0) { if (password.length === 0) {
@@ -212,22 +272,17 @@ class GenerateShareLink extends React.Component {
} }
} }
// validate days if (isExpireChecked) {
// no limit if (setExp == 'by-date') {
let reg = /^\d+$/; if (!expDate) {
if (this.isExpireDaysNoLimit) { this.setState({errorInfo: 'Please select an expiration time'});
if (isExpireChecked) {
if (!expireDays) {
this.setState({errorInfo: 'Please enter days'});
return false; return false;
} }
if (!reg.test(expireDays)) { return true;
this.setState({errorInfo: 'Please enter a non-negative integer'});
return false;
}
this.setState({expireDays: parseInt(expireDays)});
} }
} else {
// by days
let reg = /^\d+$/;
if (!expireDays) { if (!expireDays) {
this.setState({errorInfo: 'Please enter days'}); this.setState({errorInfo: 'Please enter days'});
return false; return false;
@@ -238,10 +293,10 @@ class GenerateShareLink extends React.Component {
} }
expireDays = parseInt(expireDays); expireDays = parseInt(expireDays);
let minDays = parseInt(shareLinkExpireDaysMin); let minDays = shareLinkExpireDaysMin;
let maxDays = parseInt(shareLinkExpireDaysMax); let maxDays = shareLinkExpireDaysMax;
if (minDays !== 0 && maxDays !== maxDays) { if (minDays !== 0 && maxDays == 0) {
if (expireDays < minDays) { if (expireDays < minDays) {
this.setState({errorInfo: 'Please enter valid days'}); this.setState({errorInfo: 'Please enter valid days'});
return false; return false;
@@ -261,6 +316,7 @@ class GenerateShareLink extends React.Component {
return false; return false;
} }
} }
this.setState({expireDays: expireDays}); this.setState({expireDays: expireDays});
} }
@@ -385,80 +441,92 @@ class GenerateShareLink extends React.Component {
<Form className="generate-share-link"> <Form className="generate-share-link">
<FormGroup check> <FormGroup check>
<Label check> <Label check>
<Input type="checkbox" onChange={this.onPasswordInputChecked}/>{' '}{gettext('Add password protection')} <Input type="checkbox" onChange={this.onPasswordInputChecked} />
<span>{gettext('Add password protection')}</span>
</Label> </Label>
</FormGroup> {this.state.isShowPasswordInput &&
{this.state.isShowPasswordInput && <div className="ml-4">
<FormGroup className="link-operation-content" check> <FormGroup>
<Label className="font-weight-bold">{gettext('Password')}</Label>{' '}<span className="tip">{passwordLengthTip}</span> <Label for="passwd">{gettext('Password')}</Label>
<InputGroup className="passwd"> <span className="tip">{passwordLengthTip}</span>
<Input type={this.state.isPasswordVisible ? 'text' : 'password'} value={this.state.password || ''} onChange={this.inputPassword}/> <InputGroup style={{width: inputWidth}}>
<InputGroupAddon addonType="append"> <Input id="passwd" type={this.state.isPasswordVisible ? 'text' : 'password'} value={this.state.password || ''} onChange={this.inputPassword} />
<Button onClick={this.togglePasswordVisible}><i className={`link-operation-icon fas ${this.state.isPasswordVisible ? 'fa-eye': 'fa-eye-slash'}`}></i></Button> <InputGroupAddon addonType="append">
<Button onClick={this.generatePassword}><i className="link-operation-icon fas fa-magic"></i></Button> <Button onClick={this.togglePasswordVisible}><i className={`link-operation-icon fas ${this.state.isPasswordVisible ? 'fa-eye': 'fa-eye-slash'}`}></i></Button>
</InputGroupAddon> <Button onClick={this.generatePassword}><i className="link-operation-icon fas fa-magic"></i></Button>
</InputGroup> </InputGroupAddon>
<Label className="font-weight-bold">{gettext('Password again')}</Label> </InputGroup>
<Input className="passwd" type={this.state.isPasswordVisible ? 'text' : 'password'} value={this.state.passwdnew || ''} onChange={this.inputPasswordNew} />
</FormGroup>
}
{this.isExpireDaysNoLimit && (
<Fragment>
<FormGroup check>
<Label check>
<Input className="expire-checkbox" type="checkbox" onChange={this.onExpireChecked} />{' '}{gettext('Add auto expiration')}
</Label>
</FormGroup> </FormGroup>
{this.state.isExpireChecked && <FormGroup>
<Label for="passwd-again">{gettext('Password again')}</Label>
<Input id="passwd-again" style={{width: inputWidth}} type={this.state.isPasswordVisible ? 'text' : 'password'} value={this.state.passwdnew || ''} onChange={this.inputPasswordNew} />
</FormGroup>
</div>
}
</FormGroup>
<FormGroup check>
<Label check>
{this.isExpireDaysNoLimit ? (
<Input type="checkbox" onChange={this.onExpireChecked} />
) : (
<Input type="checkbox" checked readOnly disabled />
)}
<span>{gettext('Add auto expiration')}</span>
</Label>
{this.state.isExpireChecked &&
<div className="ml-4">
<FormGroup check> <FormGroup check>
<Label check> <Label check>
<Input className="expire-input expire-input-border" type="text" value={this.state.expireDays} onChange={this.onExpireDaysChanged} readOnly={!this.state.isExpireChecked} /><span className="expir-span">{gettext('days')}</span> <Input type="radio" name="set-exp" value="by-days" checked={this.state.setExp == 'by-days'} onChange={this.setExp} className="mr-1" />
<span>{gettext('Expiration days')}</span>
</Label> </Label>
{this.state.setExp == 'by-days' && (
<Fragment>
<InputGroup style={{width: inputWidth}}>
<Input type="text" value={this.state.expireDays} onChange={this.onExpireDaysChanged} />
<InputGroupAddon addonType="append">
<InputGroupText>{gettext('days')}</InputGroupText>
</InputGroupAddon>
</InputGroup>
{!this.state.isExpireDaysNoLimit && (
<FormText color="muted">{this.expirationLimitTip}</FormText>
)}
</Fragment>
)}
</FormGroup> </FormGroup>
} <FormGroup check>
</Fragment> <Label check>
)} <Input type="radio" name="set-exp" value="by-date" checked={this.state.setExp == 'by-date'} onChange={this.setExp} className="mr-1" />
{!this.isExpireDaysNoLimit && ( <span>{gettext('Expiration time')}</span>
<Fragment> </Label>
<FormGroup check> {this.state.setExp == 'by-date' && (
<Label check> <DateTimePicker
<Input className="expire-checkbox" type="checkbox" onChange={this.onExpireChecked} checked readOnly disabled/>{' '}{gettext('Add auto expiration')} inputWidth={inputWidth}
</Label> disabledDate={this.disabledDate}
</FormGroup> value={this.state.expDate}
<FormGroup check> onChange={this.onExpDateChanged}
<Label check> />
<Input className="expire-input expire-input-border" type="text" value={this.state.expireDays} onChange={this.onExpireDaysChanged} /><span className="expir-span">{gettext('days')}</span>
{(parseInt(shareLinkExpireDaysMin) !== 0 && parseInt(shareLinkExpireDaysMax) !== 0) && (
<span className="d-inline-block ml-7">({shareLinkExpireDaysMin} - {shareLinkExpireDaysMax}{' '}{gettext('days')})</span>
)} )}
{(parseInt(shareLinkExpireDaysMin) !== 0 && parseInt(shareLinkExpireDaysMax) === 0) && ( </FormGroup>
<span className="d-inline-block ml-7">({gettext('Greater than or equal to')} {shareLinkExpireDaysMin}{' '}{gettext('days')})</span> </div>
)} }
{(parseInt(shareLinkExpireDaysMin) === 0 && parseInt(shareLinkExpireDaysMax) !== 0) && ( </FormGroup>
<span className="d-inline-block ml-7">({gettext('Less than or equal to')} {shareLinkExpireDaysMax}{' '}{gettext('days')})</span>
)}
</Label>
</FormGroup>
</Fragment>
)}
{isPro && ( {isPro && (
<Fragment> <FormGroup check>
<FormGroup check> <Label check>
<Label check> <span>{gettext('Set permission')}</span>
<span>{gettext('Set permission')}</span> </Label>
</Label>
</FormGroup>
{this.state.permissionOptions.map((item, index) => { {this.state.permissionOptions.map((item, index) => {
return ( return (
<FormGroup check className="permission" key={index}> <FormGroup check className="ml-4" key={index}>
<Label className="form-check-label"> <Label check>
<Input type="radio" name="permission" value={item} checked={this.state.currentPermission == item} onChange={this.setPermission} className="mr-1" /> <Input type="radio" name="permission" value={item} checked={this.state.currentPermission == item} onChange={this.setPermission} className="mr-1" />
{Utils.getShareLinkPermissionObject(item).text} {Utils.getShareLinkPermissionObject(item).text}
</Label> </Label>
</FormGroup> </FormGroup>
); );
})} })}
</Fragment> </FormGroup>
)} )}
{this.state.errorInfo && <Alert color="danger" className="mt-2">{gettext(this.state.errorInfo)}</Alert>} {this.state.errorInfo && <Alert color="danger" className="mt-2">{gettext(this.state.errorInfo)}</Alert>}
<Button onClick={this.generateShareLink} className="mt-2">{gettext('Generate')}</Button> <Button onClick={this.generateShareLink} className="mt-2">{gettext('Generate')}</Button>

View File

@@ -2,13 +2,14 @@ import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import copy from 'copy-to-clipboard'; import copy from 'copy-to-clipboard';
import moment from 'moment'; import moment from 'moment';
import { Button, Form, FormGroup, Label, Input, InputGroup, InputGroupAddon, Alert } from 'reactstrap'; import { Button, Form, FormGroup, Label, Input, InputGroup, InputGroupAddon, InputGroupText, Alert } from 'reactstrap';
import { gettext, shareLinkPasswordMinLength, canSendShareLinkEmail } from '../../utils/constants'; import { gettext, shareLinkPasswordMinLength, canSendShareLinkEmail } from '../../utils/constants';
import { seafileAPI } from '../../utils/seafile-api'; import { seafileAPI } from '../../utils/seafile-api';
import { Utils } from '../../utils/utils'; import { Utils } from '../../utils/utils';
import UploadLink from '../../models/upload-link'; import UploadLink from '../../models/upload-link';
import toaster from '../toast'; import toaster from '../toast';
import SendLink from '../send-link'; import SendLink from '../send-link';
import DateTimePicker from '../date-and-time-picker';
const propTypes = { const propTypes = {
itemPath: PropTypes.string.isRequired, itemPath: PropTypes.string.isRequired,
@@ -16,6 +17,8 @@ const propTypes = {
closeShareDialog: PropTypes.func.isRequired, closeShareDialog: PropTypes.func.isRequired,
}; };
const inputWidth = Utils.isDesktop() ? 250 : 210;
class GenerateUploadLink extends React.Component { class GenerateUploadLink extends React.Component {
constructor(props) { constructor(props) {
super(props); super(props);
@@ -27,7 +30,9 @@ class GenerateUploadLink extends React.Component {
sharedUploadInfo: null, sharedUploadInfo: null,
isSendLinkShown: false, isSendLinkShown: false,
isExpireChecked: false, isExpireChecked: false,
expireDays: 0, setExp: 'by-days',
expireDays: '',
expDate: null
}; };
} }
@@ -88,13 +93,23 @@ class GenerateUploadLink extends React.Component {
} }
generateUploadLink = () => { generateUploadLink = () => {
let path = this.props.itemPath;
let repoID = this.props.repoID;
let { password, expireDays } = this.state;
let isValid = this.validateParamsInput(); let isValid = this.validateParamsInput();
if (isValid) { if (isValid) {
seafileAPI.createUploadLink(repoID, path, password, expireDays).then((res) => { this.setState({errorInfo: ''});
let { itemPath, repoID } = this.props;
let { password, isExpireChecked, setExp, expireDays, expDate } = this.state;
let expirationTime = '';
if (isExpireChecked) {
if (setExp == 'by-days') {
expirationTime = moment().add(parseInt(expireDays), 'days').format();
} else {
expirationTime = expDate.format();
}
}
seafileAPI.createUploadLink(repoID, itemPath, password, expirationTime).then((res) => {
let sharedUploadInfo = new UploadLink(res.data); let sharedUploadInfo = new UploadLink(res.data);
this.setState({sharedUploadInfo: sharedUploadInfo}); this.setState({sharedUploadInfo: sharedUploadInfo});
}).catch(error => { }).catch(error => {
@@ -105,7 +120,7 @@ class GenerateUploadLink extends React.Component {
} }
validateParamsInput = () => { validateParamsInput = () => {
let { showPasswordInput , password, passwordnew, isExpireChecked, expireDays } = this.state; let { showPasswordInput, password, passwordnew, isExpireChecked, setExp, expireDays, expDate } = this.state;
// check password params // check password params
if (showPasswordInput) { if (showPasswordInput) {
@@ -123,9 +138,16 @@ class GenerateUploadLink extends React.Component {
} }
} }
// check expire day params
let reg = /^\d+$/;
if (isExpireChecked) { if (isExpireChecked) {
if (setExp == 'by-date') {
if (!expDate) {
this.setState({errorInfo: 'Please select an expiration time'});
return false;
}
return true;
}
let reg = /^\d+$/;
if (!expireDays) { if (!expireDays) {
this.setState({errorInfo: gettext('Please enter days')}); this.setState({errorInfo: gettext('Please enter days')});
return false; return false;
@@ -143,6 +165,27 @@ class GenerateUploadLink extends React.Component {
this.setState({isExpireChecked: e.target.checked}); this.setState({isExpireChecked: e.target.checked});
} }
setExp = (e) => {
this.setState({
setExp: e.target.value
});
}
disabledDate = (current) => {
if (!current) {
// allow empty select
return false;
}
return current.isBefore(moment(), 'day');
}
onExpDateChanged = (value) => {
this.setState({
expDate: value
});
}
onExpireDaysChanged = (e) => { onExpireDaysChanged = (e) => {
let day = e.target.value.trim(); let day = e.target.value.trim();
this.setState({expireDays: day}); this.setState({expireDays: day});
@@ -218,36 +261,67 @@ class GenerateUploadLink extends React.Component {
<Form className="generate-upload-link"> <Form className="generate-upload-link">
<FormGroup check> <FormGroup check>
<Label check> <Label check>
<Input type="checkbox" onChange={this.addPassword}/>{' '}{gettext('Add password protection')} <Input type="checkbox" onChange={this.addPassword} />
<span>{gettext('Add password protection')}</span>
</Label> </Label>
{this.state.showPasswordInput &&
<div className="ml-4">
<FormGroup>
<Label for="passwd">{gettext('Password')}</Label>
<span className="tip">{passwordLengthTip}</span>
<InputGroup style={{width: inputWidth}}>
<Input id="passwd" type={this.state.passwordVisible ? 'text':'password'} value={this.state.password || ''} onChange={this.inputPassword} />
<InputGroupAddon addonType="append">
<Button onClick={this.togglePasswordVisible}><i className={`link-operation-icon fas ${this.state.passwordVisible ? 'fa-eye': 'fa-eye-slash'}`}></i></Button>
<Button onClick={this.generatePassword}><i className="link-operation-icon fas fa-magic"></i></Button>
</InputGroupAddon>
</InputGroup>
</FormGroup>
<FormGroup>
<Label for="passwd-again">{gettext('Password again')}</Label>
<Input id="passwd-again" style={{width: inputWidth}} type={this.state.passwordVisible ? 'text' : 'password'} value={this.state.passwordnew || ''} onChange={this.inputPasswordNew} />
</FormGroup>
</div>
}
</FormGroup> </FormGroup>
{this.state.showPasswordInput &&
<FormGroup className="link-operation-content">
{/* todo translate */}
<Label className="font-weight-bold">{gettext('Password')}</Label>{' '}<span className="tip">{passwordLengthTip}</span>
<InputGroup className="passwd">
<Input type={this.state.passwordVisible ? 'text':'password'} value={this.state.password || ''} onChange={this.inputPassword}/>
<InputGroupAddon addonType="append">
<Button onClick={this.togglePasswordVisible}><i className={`link-operation-icon fas ${this.state.passwordVisible ? 'fa-eye': 'fa-eye-slash'}`}></i></Button>
<Button onClick={this.generatePassword}><i className="link-operation-icon fas fa-magic"></i></Button>
</InputGroupAddon>
</InputGroup>
<Label className="font-weight-bold">{gettext('Password again')}</Label>
<Input className="passwd" type={this.state.passwordVisible ? 'text' : 'password'} value={this.state.passwordnew || ''} onChange={this.inputPasswordNew} />
</FormGroup>
}
<FormGroup check> <FormGroup check>
<Label check> <Label check>
<Input className="expire-checkbox" type="checkbox" onChange={this.onExpireChecked}/>{' '}{gettext('Add auto expiration')} <Input type="checkbox" onChange={this.onExpireChecked} />
<span>{gettext('Add auto expiration')}</span>
</Label> </Label>
{this.state.isExpireChecked &&
<div className="ml-4">
<FormGroup check>
<Label check>
<Input type="radio" name="set-exp" value="by-days" checked={this.state.setExp == 'by-days'} onChange={this.setExp} className="mr-1" />
<span>{gettext('Expiration days')}</span>
</Label>
{this.state.setExp == 'by-days' && (
<InputGroup style={{width: inputWidth}}>
<Input type="text" value={this.state.expireDays} onChange={this.onExpireDaysChanged} />
<InputGroupAddon addonType="append">
<InputGroupText>{gettext('days')}</InputGroupText>
</InputGroupAddon>
</InputGroup>
)}
</FormGroup>
<FormGroup check>
<Label check>
<Input type="radio" name="set-exp" value="by-date" checked={this.state.setExp == 'by-date'} onChange={this.setExp} className="mr-1" />
<span>{gettext('Expiration time')}</span>
</Label>
{this.state.setExp == 'by-date' && (
<DateTimePicker
inputWidth={inputWidth}
disabledDate={this.disabledDate}
value={this.state.expDate}
onChange={this.onExpDateChanged}
/>
)}
</FormGroup>
</div>
}
</FormGroup> </FormGroup>
{this.state.isExpireChecked &&
<FormGroup check>
<Label check>
<Input className="expire-input expire-input-border" type="text" value={this.state.expireDays} onChange={this.onExpireDaysChanged} readOnly={!this.state.isExpireChecked}/><span className="expir-span">{gettext('days')}</span>
</Label>
</FormGroup>
}
{this.state.errorInfo && <Alert color="danger" className="mt-2">{this.state.errorInfo}</Alert>} {this.state.errorInfo && <Alert color="danger" className="mt-2">{this.state.errorInfo}</Alert>}
<Button className="generate-link-btn" onClick={this.generateUploadLink}>{gettext('Generate')}</Button> <Button className="generate-link-btn" onClick={this.generateUploadLink}>{gettext('Generate')}</Button>
</Form> </Form>

View File

@@ -0,0 +1,13 @@
/* to overwrite styles from seahub_react.css */
.rc-calendar-table {
table-layout: auto;
}
.rc-calendar-table tbody tr {
height: auto;
}
/* overwrite some styles */
/* for 'markdown file view -> share -> picker' */
.rc-calendar-input:focus {
border-color: transparent;
}

View File

@@ -69,8 +69,8 @@ import FileScanRecords from './file-scan-records';
import VirusScanRecords from './virus-scan-records'; import VirusScanRecords from './virus-scan-records';
import WorkWeixinDepartments from './work-weixin-departments'; import WorkWeixinDepartments from './work-weixin-departments';
import DingtalkDepartments from './dingtalk-departments'; import DingtalkDepartments from './dingtalk-departments';
import Invitations from './invitations/invitations'; import Invitations from './invitations/invitations';
import StatisticFile from './statistic/statistic-file'; import StatisticFile from './statistic/statistic-file';
import StatisticStorage from './statistic/statistic-storage'; import StatisticStorage from './statistic/statistic-storage';
import StatisticTraffic from './statistic/statistic-traffic'; import StatisticTraffic from './statistic/statistic-traffic';

View File

@@ -2,9 +2,11 @@
import os import os
import stat import stat
import json import json
import pytz
import logging import logging
import posixpath import posixpath
from constance import config from constance import config
from datetime import datetime
from dateutil.relativedelta import relativedelta from dateutil.relativedelta import relativedelta
from rest_framework.authentication import SessionAuthentication from rest_framework.authentication import SessionAuthentication
@@ -244,33 +246,71 @@ class ShareLinks(APIView):
error_msg = _('Password is too short.') error_msg = _('Password is too short.')
return api_error(status.HTTP_400_BAD_REQUEST, error_msg) return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
try: expire_days = request.data.get('expire_days', '')
expire_days = int(request.data.get('expire_days', 0)) expiration_time = request.data.get('expiration_time', '')
except ValueError: if expire_days and expiration_time:
error_msg = 'expire_days invalid.' error_msg = 'Can not pass expire_days and expiration_time at the same time.'
return api_error(status.HTTP_400_BAD_REQUEST, error_msg) return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
if expire_days <= 0: expire_date = None
if SHARE_LINK_EXPIRE_DAYS_DEFAULT > 0: if expire_days:
expire_days = SHARE_LINK_EXPIRE_DAYS_DEFAULT try:
expire_days = int(expire_days)
if SHARE_LINK_EXPIRE_DAYS_MIN > 0: except ValueError:
if expire_days < SHARE_LINK_EXPIRE_DAYS_MIN: error_msg = 'expire_days invalid.'
error_msg = _('Expire days should be greater or equal to %s') % \
SHARE_LINK_EXPIRE_DAYS_MIN
return api_error(status.HTTP_400_BAD_REQUEST, error_msg) return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
if SHARE_LINK_EXPIRE_DAYS_MAX > 0: if expire_days <= 0:
if expire_days > SHARE_LINK_EXPIRE_DAYS_MAX: error_msg = 'expire_days invalid.'
error_msg = _('Expire days should be less than or equal to %s') % \
SHARE_LINK_EXPIRE_DAYS_MAX
return api_error(status.HTTP_400_BAD_REQUEST, error_msg) return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
if expire_days <= 0: if SHARE_LINK_EXPIRE_DAYS_MIN > 0:
expire_date = None if expire_days < SHARE_LINK_EXPIRE_DAYS_MIN:
else: error_msg = _('Expire days should be greater or equal to %s') % \
SHARE_LINK_EXPIRE_DAYS_MIN
return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
if SHARE_LINK_EXPIRE_DAYS_MAX > 0:
if expire_days > SHARE_LINK_EXPIRE_DAYS_MAX:
error_msg = _('Expire days should be less than or equal to %s') % \
SHARE_LINK_EXPIRE_DAYS_MAX
return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
expire_date = timezone.now() + relativedelta(days=expire_days) expire_date = timezone.now() + relativedelta(days=expire_days)
elif expiration_time:
try:
expire_date = datetime.fromisoformat(expiration_time)
except Exception as e:
logger.error(e)
error_msg = 'expiration_time invalid, should be iso format, for example: 2020-05-17T10:26:22+08:00'
return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
expire_date = expire_date.astimezone(pytz.UTC).replace(tzinfo=None)
if SHARE_LINK_EXPIRE_DAYS_MIN > 0:
expire_date_min_limit = timezone.now() + relativedelta(days=SHARE_LINK_EXPIRE_DAYS_MIN)
expire_date_min_limit = expire_date_min_limit.replace(hour=0).replace(minute=0).replace(second=0)
if expire_date < expire_date_min_limit:
error_msg = _('Expiration time should be later than %s') % \
expire_date_min_limit.strftime("%Y-%m-%d %H:%M:%S")
return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
if SHARE_LINK_EXPIRE_DAYS_MAX > 0:
expire_date_max_limit = timezone.now() + relativedelta(days=SHARE_LINK_EXPIRE_DAYS_MAX)
expire_date_max_limit = expire_date_max_limit.replace(hour=23).replace(minute=59).replace(second=59)
if expire_date > expire_date_max_limit:
error_msg = _('Expiration time should be earlier than %s') % \
expire_date_max_limit.strftime("%Y-%m-%d %H:%M:%S")
return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
else:
if SHARE_LINK_EXPIRE_DAYS_DEFAULT > 0:
expire_date = timezone.now() + relativedelta(days=SHARE_LINK_EXPIRE_DAYS_DEFAULT)
try: try:
perm = check_permissions_arg(request) perm = check_permissions_arg(request)
except Exception: except Exception:

View File

@@ -1,8 +1,10 @@
# Copyright (c) 2012-2016 Seafile Ltd. # Copyright (c) 2012-2016 Seafile Ltd.
import os import os
import json import json
import pytz
import logging import logging
from constance import config from constance import config
from datetime import datetime
from dateutil.relativedelta import relativedelta from dateutil.relativedelta import relativedelta
from rest_framework.authentication import SessionAuthentication from rest_framework.authentication import SessionAuthentication
@@ -153,12 +155,37 @@ class UploadLinks(APIView):
error_msg = _('Password is too short') error_msg = _('Password is too short')
return api_error(status.HTTP_400_BAD_REQUEST, error_msg) return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
try: expire_days = request.data.get('expire_days', '')
expire_days = int(request.data.get('expire_days', 0)) expiration_time = request.data.get('expiration_time', '')
except ValueError: if expire_days and expiration_time:
error_msg = 'expire_days invalid.' error_msg = 'Can not pass expire_days and expiration_time at the same time.'
return api_error(status.HTTP_400_BAD_REQUEST, error_msg) return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
expire_date = None
if expire_days:
try:
expire_days = int(expire_days)
except ValueError:
error_msg = 'expire_days invalid.'
return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
if expire_days <= 0:
error_msg = 'expire_days invalid.'
return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
expire_date = timezone.now() + relativedelta(days=expire_days)
elif expiration_time:
try:
expire_date = datetime.fromisoformat(expiration_time)
except Exception as e:
logger.error(e)
error_msg = 'expiration_time invalid, should be iso format, for example: 2020-05-17T10:26:22+08:00'
return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
expire_date = expire_date.astimezone(pytz.UTC).replace(tzinfo=None)
# resource check # resource check
repo = seafile_api.get_repo(repo_id) repo = seafile_api.get_repo(repo_id)
if not repo: if not repo:
@@ -180,11 +207,6 @@ class UploadLinks(APIView):
error_msg = 'Permission denied.' error_msg = 'Permission denied.'
return api_error(status.HTTP_403_FORBIDDEN, error_msg) return api_error(status.HTTP_403_FORBIDDEN, error_msg)
if expire_days <= 0:
expire_date = None
else:
expire_date = timezone.now() + relativedelta(days=expire_days)
username = request.user.username username = request.user.username
uls = UploadLinkShare.objects.get_upload_link_by_path(username, repo_id, path) uls = UploadLinkShare.objects.get_upload_link_by_path(username, repo_id, path)
if not uls: if not uls:

View File

@@ -83,9 +83,6 @@
})(), })(),
enableRepoSnapshotLabel: {% if enable_repo_snapshot_label %} true {% else %} false {% endif %}, enableRepoSnapshotLabel: {% if enable_repo_snapshot_label %} true {% else %} false {% endif %},
shareLinkPasswordMinLength: {{ share_link_password_min_length }}, shareLinkPasswordMinLength: {{ share_link_password_min_length }},
shareLinkExpireDaysDefault: {% if share_link_expire_days_default %} {{ share_link_expire_days_default }} {% else %} 0 {% endif %},
shareLinkExpireDaysMin: "{{ share_link_expire_days_min }}",
shareLinkExpireDaysMax: "{{ share_link_expire_days_max }}",
sideNavFooterCustomHtml: "{{ side_nav_footer_custom_html|safe|escapejs }}", sideNavFooterCustomHtml: "{{ side_nav_footer_custom_html|safe|escapejs }}",
maxFileName: "{{ max_file_name }}", maxFileName: "{{ max_file_name }}",
canPublishRepo: {% if user.permissions.can_publish_repo %} true {% else %} false {% endif %}, canPublishRepo: {% if user.permissions.can_publish_repo %} true {% else %} false {% endif %},

View File

@@ -28,6 +28,7 @@
serviceUrl: '{{ serviceUrl }}', serviceUrl: '{{ serviceUrl }}',
isPro: '{{ is_pro }}', isPro: '{{ is_pro }}',
isDocs: '{{ is_docs }}', isDocs: '{{ is_docs }}',
lang: '{{ LANGUAGE_CODE }}',
seafileCollabServer: '{{ seafile_collab_server}}', seafileCollabServer: '{{ seafile_collab_server}}',
}, },
pageOptions: { pageOptions: {
@@ -44,10 +45,10 @@
hasDraft: '{{ has_draft }}' === 'True', hasDraft: '{{ has_draft }}' === 'True',
draftFilePath: '{{ draft_file_path }}', draftFilePath: '{{ draft_file_path }}',
draftOriginFilePath: '{{ draft_origin_file_path }}', draftOriginFilePath: '{{ draft_origin_file_path }}',
shareLinkPasswordMinLength: '{{ share_link_password_min_length }}', shareLinkPasswordMinLength: {{ share_link_password_min_length }},
shareLinkExpireDaysDefault: '{{ share_link_expire_days_default }}', shareLinkExpireDaysDefault: {{ share_link_expire_days_default }},
shareLinkExpireDaysMin: '{{ share_link_expire_days_min }}', shareLinkExpireDaysMin: {{ share_link_expire_days_min }},
shareLinkExpireDaysMax: '{{ share_link_expire_days_max }}', shareLinkExpireDaysMax: {{ share_link_expire_days_max }},
canGenerateShareLink: {% if user.permissions.can_generate_share_link %} true {% else %} false {% endif %}, canGenerateShareLink: {% if user.permissions.can_generate_share_link %} true {% else %} false {% endif %},
canSendShareLinkEmail: {% if user.permissions.can_send_share_link_mail %} true {% else %} false {% endif %}, canSendShareLinkEmail: {% if user.permissions.can_send_share_link_mail %} true {% else %} false {% endif %},
isLocked: {% if file_locked %}true{% else %}false{% endif %}, isLocked: {% if file_locked %}true{% else %}false{% endif %},

View File

@@ -6,5 +6,12 @@
{% endblock %} {% endblock %}
{% block extra_script %} {% block extra_script %}
<script type="text/javascript">
Object.assign(app.pageOptions, {
shareLinkExpireDaysDefault: {{ share_link_expire_days_default }},
shareLinkExpireDaysMin: {{ share_link_expire_days_min }},
shareLinkExpireDaysMax: {{ share_link_expire_days_max }}
});
</script>
{% render_bundle 'app' 'js' %} {% render_bundle 'app' 'js' %}
{% endblock %} {% endblock %}

View File

@@ -1150,7 +1150,7 @@ def react_fake_view(request, **kwargs):
'enable_repo_snapshot_label': settings.ENABLE_REPO_SNAPSHOT_LABEL, 'enable_repo_snapshot_label': settings.ENABLE_REPO_SNAPSHOT_LABEL,
'resumable_upload_file_block_size': settings.RESUMABLE_UPLOAD_FILE_BLOCK_SIZE, 'resumable_upload_file_block_size': settings.RESUMABLE_UPLOAD_FILE_BLOCK_SIZE,
'max_number_of_files_for_fileupload': settings.MAX_NUMBER_OF_FILES_FOR_FILEUPLOAD, 'max_number_of_files_for_fileupload': settings.MAX_NUMBER_OF_FILES_FOR_FILEUPLOAD,
'share_link_expire_days_default': settings.SHARE_LINK_EXPIRE_DAYS_DEFAULT, 'share_link_expire_days_default': SHARE_LINK_EXPIRE_DAYS_DEFAULT,
'share_link_expire_days_min': SHARE_LINK_EXPIRE_DAYS_MIN, 'share_link_expire_days_min': SHARE_LINK_EXPIRE_DAYS_MIN,
'share_link_expire_days_max': SHARE_LINK_EXPIRE_DAYS_MAX, 'share_link_expire_days_max': SHARE_LINK_EXPIRE_DAYS_MAX,
'enable_encrypted_library': config.ENABLE_ENCRYPTED_LIBRARY, 'enable_encrypted_library': config.ENABLE_ENCRYPTED_LIBRARY,