mirror of
https://github.com/haiwen/seahub.git
synced 2025-09-07 01:41:39 +00:00
[user settings] rewrote it with react
This commit is contained in:
@@ -194,6 +194,11 @@ module.exports = {
|
||||
require.resolve('react-dev-utils/webpackHotDevClient'),
|
||||
paths.appSrc + "/view-file-unknown.js",
|
||||
],
|
||||
settings: [
|
||||
require.resolve('./polyfills'),
|
||||
require.resolve('react-dev-utils/webpackHotDevClient'),
|
||||
paths.appSrc + "/settings.js",
|
||||
],
|
||||
orgAdmin: [
|
||||
require.resolve('./polyfills'),
|
||||
require.resolve('react-dev-utils/webpackHotDevClient'),
|
||||
|
@@ -87,6 +87,7 @@ module.exports = {
|
||||
viewFileSVG: [require.resolve('./polyfills'), paths.appSrc + "/view-file-svg.js"],
|
||||
viewFileAudio: [require.resolve('./polyfills'), paths.appSrc + "/view-file-audio.js"],
|
||||
viewFileUnknown: [require.resolve('./polyfills'), paths.appSrc + "/view-file-unknown.js"],
|
||||
settings: [require.resolve('./polyfills'), paths.appSrc + "/settings.js"],
|
||||
orgAdmin: [require.resolve('./polyfills'), paths.appSrc + "/pages/org-admin"],
|
||||
sysAdmin: [require.resolve('./polyfills'), paths.appSrc + "/pages/sys-admin"],
|
||||
viewDataGrid: [require.resolve('./polyfills'), paths.appSrc + "/view-file-ctable.js"],
|
||||
|
45
frontend/src/components/dialog/confirm-delete-account.js
Normal file
45
frontend/src/components/dialog/confirm-delete-account.js
Normal file
@@ -0,0 +1,45 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Modal, ModalHeader, ModalBody, ModalFooter, Button } from 'reactstrap';
|
||||
import { gettext } from '../../utils/constants';
|
||||
|
||||
const propTypes = {
|
||||
formActionURL: PropTypes.string.isRequired,
|
||||
csrfToken: PropTypes.string.isRequired,
|
||||
toggle: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
class ConfirmDeleteAccount extends Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.form = React.createRef();
|
||||
}
|
||||
|
||||
action = () => {
|
||||
this.form.current.submit();
|
||||
}
|
||||
|
||||
render() {
|
||||
const {formActionURL, csrfToken, toggle} = this.props;
|
||||
return (
|
||||
<Modal centered={true} isOpen={true} toggle={toggle}>
|
||||
<ModalHeader toggle={toggle}>{gettext('Delete Account')}</ModalHeader>
|
||||
<ModalBody>
|
||||
<p>{gettext('Really want to delete your account?')}</p>
|
||||
<form ref={this.form} className="d-none" method="post" action={formActionURL}>
|
||||
<input type="hidden" name="csrfmiddlewaretoken" value={csrfToken} />
|
||||
</form>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button color="secondary" onClick={toggle}>{gettext('Cancel')}</Button>
|
||||
<Button color="primary" onClick={this.action}>{gettext('Delete')}</Button>
|
||||
</ModalFooter>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ConfirmDeleteAccount.propTypes = propTypes;
|
||||
|
||||
export default ConfirmDeleteAccount;
|
45
frontend/src/components/dialog/confirm-disconnect-wechat.js
Normal file
45
frontend/src/components/dialog/confirm-disconnect-wechat.js
Normal file
@@ -0,0 +1,45 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Modal, ModalHeader, ModalBody, ModalFooter, Button } from 'reactstrap';
|
||||
import { gettext } from '../../utils/constants';
|
||||
|
||||
const propTypes = {
|
||||
formActionURL: PropTypes.string.isRequired,
|
||||
csrfToken: PropTypes.string.isRequired,
|
||||
toggle: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
class ConfirmDisconnectWechat extends Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.form = React.createRef();
|
||||
}
|
||||
|
||||
disconnect = () => {
|
||||
this.form.current.submit();
|
||||
}
|
||||
|
||||
render() {
|
||||
const {formActionURL, csrfToken, toggle} = this.props;
|
||||
return (
|
||||
<Modal centered={true} isOpen={true} toggle={toggle}>
|
||||
<ModalHeader toggle={toggle}>{gettext('Disconnect')}</ModalHeader>
|
||||
<ModalBody>
|
||||
<p>{gettext('Are you sure you want to disconnect?')}</p>
|
||||
<form ref={this.form} className="d-none" method="post" action={formActionURL}>
|
||||
<input type="hidden" name="csrfmiddlewaretoken" value={csrfToken} />
|
||||
</form>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button color="secondary" onClick={toggle}>{gettext('Cancel')}</Button>
|
||||
<Button color="primary" onClick={this.disconnect}>{gettext('Disconnect')}</Button>
|
||||
</ModalFooter>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ConfirmDisconnectWechat.propTypes = propTypes;
|
||||
|
||||
export default ConfirmDisconnectWechat;
|
54
frontend/src/components/user-settings/delete-account.js
Normal file
54
frontend/src/components/user-settings/delete-account.js
Normal file
@@ -0,0 +1,54 @@
|
||||
import React from 'react';
|
||||
import { gettext, siteRoot } from '../../utils/constants';
|
||||
import ModalPortal from '../modal-portal';
|
||||
import ConfirmDeleteAccount from '../dialog/confirm-delete-account';
|
||||
|
||||
const {
|
||||
csrfToken
|
||||
} = window.app.pageOptions;
|
||||
|
||||
class DeleteAccount extends React.Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
isConfirmDialogOpen: false
|
||||
};
|
||||
}
|
||||
|
||||
confirmDelete = (e) => {
|
||||
e.preventDefault();
|
||||
this.setState({
|
||||
isConfirmDialogOpen: true
|
||||
});
|
||||
}
|
||||
|
||||
toggleDialog = () => {
|
||||
this.setState({
|
||||
isConfirmDialogOpen: !this.state.isConfirmDialogOpen
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<div className="setting-item" id="del-account">
|
||||
<h3 className="setting-item-heading">{gettext('Delete Account')}</h3>
|
||||
<p className="mb-2">{gettext('This operation will not be reverted. Please think twice!')}</p>
|
||||
<button className="btn btn-secondary" onClick={this.confirmDelete}>{gettext('Delete')}</button>
|
||||
</div>
|
||||
{this.state.isConfirmDialogOpen && (
|
||||
<ModalPortal>
|
||||
<ConfirmDeleteAccount
|
||||
formActionURL={`${siteRoot}profile/delete/`}
|
||||
csrfToken={csrfToken}
|
||||
toggle={this.toggleDialog}
|
||||
/>
|
||||
</ModalPortal>
|
||||
)}
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default DeleteAccount;
|
78
frontend/src/components/user-settings/email-notice.js
Normal file
78
frontend/src/components/user-settings/email-notice.js
Normal file
@@ -0,0 +1,78 @@
|
||||
import React from 'react';
|
||||
import { gettext } from '../../utils/constants';
|
||||
import { seafileAPI } from '../../utils/seafile-api';
|
||||
import toaster from '../toast';
|
||||
|
||||
const {
|
||||
initialEmailNotificationInterval
|
||||
} = window.app.pageOptions;
|
||||
|
||||
class EmailNotice extends React.Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
// interval: in seconds
|
||||
this.intervalOptions = [
|
||||
{interval: 0, text: gettext('Don\'t send emails')},
|
||||
{interval: 3600, text: gettext('Per hour')},
|
||||
{interval: 14400, text: gettext('Per 4 hours')},
|
||||
{interval: 86400, text: gettext('Per day')},
|
||||
{interval: 604800, text: gettext('Per week')}
|
||||
];
|
||||
|
||||
this.state = {
|
||||
currentInterval: initialEmailNotificationInterval
|
||||
};
|
||||
}
|
||||
|
||||
inputChange = (e) => {
|
||||
if (e.target.checked) {
|
||||
this.setState({
|
||||
currentInterval: e.target.value
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
formSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
const { currentInterval } = this.state;
|
||||
if (currentInterval == initialEmailNotificationInterval) {
|
||||
return;
|
||||
}
|
||||
seafileAPI.updateEmailNotificationInterval(this.state.currentInterval).then((res) => {
|
||||
toaster.success(gettext('Success'));
|
||||
}).catch((error) => {
|
||||
let errorMsg = '';
|
||||
if (error.response) {
|
||||
errorMsg = error.response.data.error_msg || gettext('Error');
|
||||
} else {
|
||||
errorMsg = gettext('Please check the network.');
|
||||
}
|
||||
toaster.danger(errorMsg);
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const { currentInterval } = this.state;
|
||||
return (
|
||||
<div className="setting-item" id="email-notice">
|
||||
<h3 className="setting-item-heading">{gettext('Email Notification of File Changes')}</h3>
|
||||
<form method="post" action="" id="set-email-notice-interval-form" onSubmit={this.formSubmit}>
|
||||
{this.intervalOptions.map((item, index) => {
|
||||
return (
|
||||
<React.Fragment key={index}>
|
||||
<input type="radio" name="interval" value={item.interval} className="align-middle" id={`interval-option${index + 1}`} checked={currentInterval == item.interval} onChange={this.inputChange} />
|
||||
<label className="align-middle m-0 ml-2" htmlFor={`interval-option${index + 1}`}>{item.text}</label>
|
||||
<br />
|
||||
</React.Fragment>
|
||||
);
|
||||
})}
|
||||
<button type="submit" className="btn btn-secondary mt-2">{gettext('Submit')}</button>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default EmailNotice;
|
49
frontend/src/components/user-settings/language-setting.js
Normal file
49
frontend/src/components/user-settings/language-setting.js
Normal file
@@ -0,0 +1,49 @@
|
||||
import React from 'react';
|
||||
import { Dropdown, DropdownToggle, DropdownMenu, DropdownItem } from 'reactstrap';
|
||||
import { gettext, siteRoot } from '../../utils/constants';
|
||||
|
||||
const {
|
||||
currentLang, langList
|
||||
} = window.app.pageOptions;
|
||||
|
||||
class LanguageSetting extends React.Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
dropdownOpen: false,
|
||||
};
|
||||
}
|
||||
|
||||
toggle = () => {
|
||||
this.setState({
|
||||
dropdownOpen: !this.state.dropdownOpen
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="setting-item" id="lang-setting">
|
||||
<h3 className="setting-item-heading">{gettext('Language Setting')}</h3>
|
||||
<Dropdown isOpen={this.state.dropdownOpen} toggle={this.toggle}>
|
||||
<DropdownToggle caret>
|
||||
{currentLang}
|
||||
</DropdownToggle>
|
||||
<DropdownMenu>
|
||||
{langList.map((item, index) => {
|
||||
return (
|
||||
<DropdownItem key={index}>
|
||||
<a href={`${siteRoot}i18n/?lang=${item.langCode}`} className="text-inherit">
|
||||
{item.langName}
|
||||
</a>
|
||||
</DropdownItem>
|
||||
);
|
||||
})}
|
||||
</DropdownMenu>
|
||||
</Dropdown>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default LanguageSetting;
|
@@ -0,0 +1,39 @@
|
||||
import React from 'react';
|
||||
import { gettext } from '../../utils/constants';
|
||||
|
||||
class ListInAddressBook extends React.Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
const { list_in_address_book } = this.props.userInfo;
|
||||
this.state = {
|
||||
inputChecked: list_in_address_book
|
||||
};
|
||||
}
|
||||
|
||||
handleInputChange = (e) => {
|
||||
const checked = e.target.checked;
|
||||
this.setState({
|
||||
inputChecked: checked
|
||||
});
|
||||
this.props.updateUserInfo({
|
||||
list_in_address_book: checked.toString()
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const { inputChecked } = this.state;
|
||||
|
||||
return (
|
||||
<div className="setting-item" id="list-in-address-book">
|
||||
<h3 className="setting-item-heading">{gettext('Global Address Book')}</h3>
|
||||
<div className="d-flex align-items-center">
|
||||
<input type="checkbox" id="list-in-address-book" name="list_in_address_book" className="mr-1" checked={inputChecked} onChange={this.handleInputChange} />
|
||||
<label htmlFor="list-in-address-book" className="m-0">{gettext('List your account in global address book, so that others can find you by typing your name.')}</label>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default ListInAddressBook;
|
50
frontend/src/components/user-settings/side-nav.js
Normal file
50
frontend/src/components/user-settings/side-nav.js
Normal file
@@ -0,0 +1,50 @@
|
||||
import React from 'react';
|
||||
import { isPro, gettext } from '../../utils/constants';
|
||||
|
||||
const {
|
||||
canUpdatePassword,
|
||||
enableAddressBook,
|
||||
enableWebdavSecret,
|
||||
twoFactorAuthEnabled,
|
||||
enableWechatWork,
|
||||
enableDeleteAccount
|
||||
} = window.app.pageOptions;
|
||||
|
||||
class SideNav extends React.Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<ul className="list-group list-group-flush">
|
||||
<li className="list-group-item"><a href="#user-basic-info">{gettext('Profile')}</a></li>
|
||||
{canUpdatePassword &&
|
||||
<li className="list-group-item"><a href="#update-user-passwd">{gettext('Password')}</a></li>
|
||||
}
|
||||
{enableWebdavSecret &&
|
||||
<li className="list-group-item"><a href="#update-webdav-passwd">{gettext('WebDav Password')}</a></li>
|
||||
}
|
||||
{enableAddressBook &&
|
||||
<li className="list-group-item"><a href="#list-in-address-book">{gettext('Global Address Book')}</a></li>
|
||||
}
|
||||
<li className="list-group-item"><a href="#lang-setting">{gettext('Language')}</a></li>
|
||||
{isPro &&
|
||||
<li className="list-group-item"><a href="#email-notice">{gettext('Email Notification')}</a></li>
|
||||
}
|
||||
{twoFactorAuthEnabled &&
|
||||
<li className="list-group-item"><a href="#two-factor-auth">{gettext('Two-Factor Authentication')}</a></li>
|
||||
}
|
||||
{enableWechatWork &&
|
||||
<li className="list-group-item"><a href="#social-auth">{gettext('Social Login')}</a></li>
|
||||
}
|
||||
{enableDeleteAccount &&
|
||||
<li className="list-group-item"><a href="#del-account">{gettext('Delete Account')}</a></li>
|
||||
}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default SideNav;
|
60
frontend/src/components/user-settings/social-login.js
Normal file
60
frontend/src/components/user-settings/social-login.js
Normal file
@@ -0,0 +1,60 @@
|
||||
import React from 'react';
|
||||
import { gettext, siteRoot } from '../../utils/constants';
|
||||
import ModalPortal from '../modal-portal';
|
||||
import ConfirmDisconnectWechat from '../dialog/confirm-disconnect-wechat';
|
||||
|
||||
const {
|
||||
csrfToken,
|
||||
langCode,
|
||||
socialConnected,
|
||||
socialNextPage
|
||||
} = window.app.pageOptions;
|
||||
|
||||
class SocialLogin extends React.Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
isConfirmDialogOpen: false
|
||||
};
|
||||
}
|
||||
|
||||
confirmDisconnect = (e) => {
|
||||
e.preventDefault();
|
||||
this.setState({
|
||||
isConfirmDialogOpen: true
|
||||
});
|
||||
}
|
||||
|
||||
toggleDialog = () => {
|
||||
this.setState({
|
||||
isConfirmDialogOpen: !this.state.isConfirmDialogOpen
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<div className="setting-item" id="social-auth">
|
||||
<h3 className="setting-item-heading">{gettext('Social Login')}</h3>
|
||||
<p className="mb-2">{langCode == 'zh-cn' ? '企业微信': 'WeChat Work'}</p>
|
||||
{socialConnected ?
|
||||
<a href="#" className="btn btn-secondary" onClick={this.confirmDisconnect}>{gettext('Disconnect')}</a> :
|
||||
<a href={`${siteRoot}social/login/weixin-work/?next=${encodeURIComponent(socialNextPage)}`} className="btn btn-secondary">{gettext('Connect')}</a>
|
||||
}
|
||||
</div>
|
||||
{this.state.isConfirmDialogOpen && (
|
||||
<ModalPortal>
|
||||
<ConfirmDisconnectWechat
|
||||
formActionURL={`${siteRoot}social/disconnect/weixin-work/?next=${encodeURIComponent(socialNextPage)}`}
|
||||
csrfToken={csrfToken}
|
||||
toggle={this.toggleDialog}
|
||||
/>
|
||||
</ModalPortal>
|
||||
)}
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default SocialLogin;
|
52
frontend/src/components/user-settings/two-factor-auth.js
Normal file
52
frontend/src/components/user-settings/two-factor-auth.js
Normal file
@@ -0,0 +1,52 @@
|
||||
import React from 'react';
|
||||
import { gettext, siteRoot } from '../../utils/constants';
|
||||
|
||||
const {
|
||||
defaultDevice,
|
||||
backupTokens
|
||||
} = window.app.pageOptions;
|
||||
|
||||
class TwoFactorAuthentication extends React.Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
renderEnabled = () => {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<p className="mb-2">{gettext('Status: enabled')}</p>
|
||||
<a className="btn btn-secondary mb-4" href={`${siteRoot}profile/two_factor_authentication/disable/`}>
|
||||
{gettext('Disable Two-Factor Authentication')}</a>
|
||||
<p className="mb-2">
|
||||
{gettext('If you don\'t have any device with you, you can access your account using backup codes.')}
|
||||
{backupTokens == 1 ? gettext('You have only one backup code remaining.') :
|
||||
gettext('You have {num} backup codes remaining.').replace('{num}', backupTokens)}
|
||||
</p>
|
||||
<a href={`${siteRoot}profile/two_factor_authentication/backup/tokens/`}
|
||||
className="btn btn-secondary">{gettext('Show Codes')}</a>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
renderDisabled = () => {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<p className="mb-2">{gettext('Two-factor authentication is not enabled for your account. Enable two-factor authentication for enhanced account security.')}</p>
|
||||
<a href={`${siteRoot}profile/two_factor_authentication/setup/`} className="btn btn-secondary">
|
||||
{gettext('Enable Two-Factor Authentication')}</a>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="setting-item" id="two-factor-auth">
|
||||
<h3 className="setting-item-heading">{gettext('Two-Factor Authentication')}</h3>
|
||||
{defaultDevice ? this.renderEnabled() : this.renderDisabled()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default TwoFactorAuthentication;
|
74
frontend/src/components/user-settings/user-avatar-form.js
Normal file
74
frontend/src/components/user-settings/user-avatar-form.js
Normal file
@@ -0,0 +1,74 @@
|
||||
import React from 'react';
|
||||
import { gettext, siteRoot } from '../../utils/constants';
|
||||
import toaster from '../toast';
|
||||
|
||||
const { avatarURL, csrfToken } = window.app.pageOptions;
|
||||
|
||||
class UserAvatarForm extends React.Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.fileInput = React.createRef();
|
||||
this.form = React.createRef();
|
||||
this.state = {
|
||||
avatarSrc: avatarURL
|
||||
};
|
||||
}
|
||||
|
||||
fileInputChange = () => {
|
||||
|
||||
// no file selected
|
||||
if (!this.fileInput.current.files.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const file = this.fileInput.current.files[0];
|
||||
const fileName = file.name;
|
||||
|
||||
// no file extension
|
||||
if (fileName.lastIndexOf('.') == -1) {
|
||||
toaster.danger(gettext('Please choose an image file.'), {
|
||||
duration: 5
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
const fileExt = fileName.substr((fileName.lastIndexOf('.') + 1)).toLowerCase();
|
||||
const allowedExt = ['jpg','jpeg', 'png', 'gif'];
|
||||
if (allowedExt.indexOf(fileExt) == -1) {
|
||||
const errorMsg = gettext('File extensions can only be {placeholder}.')
|
||||
.replace('{placeholder}', allowedExt.join(', '));
|
||||
toaster.danger(errorMsg, {duration: 5});
|
||||
return false;
|
||||
}
|
||||
|
||||
// file size should be less than 1MB
|
||||
if (file.size > 1024*1024) {
|
||||
const errorMsg = gettext('The file is too large. Allowed maximum size is 1MB.');
|
||||
toaster.danger(errorMsg, {duration: 5});
|
||||
return false;
|
||||
}
|
||||
|
||||
this.form.current.submit();
|
||||
}
|
||||
|
||||
openFileInput = () => {
|
||||
this.fileInput.current.click();
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<form ref={this.form} className="form-group row" encType="multipart/form-data" method="post" action={`${siteRoot}avatar/add/`}>
|
||||
<input type="hidden" name="csrfmiddlewaretoken" value={csrfToken} />
|
||||
<label className="col-sm-1 col-form-label">{gettext('Avatar:')}</label>
|
||||
<div className="col-sm-11">
|
||||
<img src={avatarURL} width="80" height="80" alt="" className="user-avatar mr-2 align-text-top" />
|
||||
<button type="button" className="btn btn-secondary align-text-top" onClick={this.openFileInput}>{gettext('Change')}</button>
|
||||
<input type="file" name="avatar" className="d-none" onChange={this.fileInputChange} ref={this.fileInput} />
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default UserAvatarForm;
|
107
frontend/src/components/user-settings/user-basic-info-form.js
Normal file
107
frontend/src/components/user-settings/user-basic-info-form.js
Normal file
@@ -0,0 +1,107 @@
|
||||
import React from 'react';
|
||||
import { gettext } from '../../utils/constants';
|
||||
|
||||
const {
|
||||
nameLabel,
|
||||
enableUpdateUserInfo,
|
||||
enableUserSetContactEmail
|
||||
} = window.app.pageOptions;
|
||||
|
||||
class UserBasicInfoForm extends React.Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
const {
|
||||
contact_email,
|
||||
login_id,
|
||||
name,
|
||||
telephone
|
||||
} = this.props.userInfo;
|
||||
this.state = {
|
||||
contactEmail: contact_email,
|
||||
loginID: login_id,
|
||||
name: name,
|
||||
telephone: telephone
|
||||
};
|
||||
}
|
||||
|
||||
handleNameInputChange = (e) => {
|
||||
this.setState({
|
||||
name: e.target.value
|
||||
});
|
||||
}
|
||||
|
||||
handleContactEmailInputChange = (e) => {
|
||||
this.setState({
|
||||
contactEmail: e.target.value
|
||||
});
|
||||
}
|
||||
|
||||
handleTelephoneInputChange = (e) => {
|
||||
this.setState({
|
||||
telephone: e.target.value
|
||||
});
|
||||
}
|
||||
|
||||
handleSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
this.props.updateUserInfo({
|
||||
name: this.state.name,
|
||||
contact_email: this.state.contactEmail,
|
||||
telephone: this.state.telephone
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
contactEmail,
|
||||
loginID,
|
||||
name,
|
||||
telephone
|
||||
} = this.state;
|
||||
|
||||
return (
|
||||
<form action="" method="post" onSubmit={this.handleSubmit}>
|
||||
|
||||
<div className="form-group row">
|
||||
<label className="col-sm-1 col-form-label" htmlFor="name">{nameLabel}</label>
|
||||
<div className="col-sm-5">
|
||||
<input className="form-control" id="name" type="text" name="nickname" value={name} disabled={!enableUpdateUserInfo} onChange={this.handleNameInputChange} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{loginID && (
|
||||
<div className="form-group row">
|
||||
<label className="col-sm-1 col-form-label" htmlFor="user-name">{gettext('Username:')}</label>
|
||||
<div className="col-sm-5">
|
||||
<input className="form-control" id="user-name" type="text" name="username" value={loginID} disabled={true} readOnly={true} />
|
||||
</div>
|
||||
<p className="col-sm-5 m-0">{gettext('You can use this field at login.')}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{(contactEmail || enableUserSetContactEmail) && (
|
||||
<div className="form-group row">
|
||||
<label className="col-sm-1 col-form-label" htmlFor="contact-email">{gettext('Contact Email:')}</label>
|
||||
<div className="col-sm-5">
|
||||
<input className="form-control" id="contact-email" type="text" name="contact_email" value={contactEmail} disabled={!enableUserSetContactEmail} readOnly={!enableUserSetContactEmail} onChange={this.handleContactEmailInputChange} />
|
||||
</div>
|
||||
<p className="col-sm-5 m-0">{gettext('Your notifications will be sent to this email.')}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{(telephone != undefined) && (
|
||||
<div className="form-group row">
|
||||
<label className="col-sm-1 col-form-label" htmlFor="telephone">{gettext('Telephone:')}</label>
|
||||
<div className="col-sm-5">
|
||||
<input className="form-control" id="telephone" type="text" name="telephone" value={telephone} disabled={!enableUpdateUserInfo} readOnly={!enableUpdateUserInfo} onChange={this.handleTelephoneInputChange} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<button type="submit" className="btn btn-secondary offset-sm-1" disabled={!enableUpdateUserInfo}>{gettext('Submit')}</button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default UserBasicInfoForm;
|
92
frontend/src/components/user-settings/webdav-password.js
Normal file
92
frontend/src/components/user-settings/webdav-password.js
Normal file
@@ -0,0 +1,92 @@
|
||||
import React from 'react';
|
||||
import { Button, Input, InputGroup, InputGroupAddon } from 'reactstrap';
|
||||
import { gettext } from '../../utils/constants';
|
||||
import { seafileAPI } from '../../utils/seafile-api';
|
||||
import toaster from '../toast';
|
||||
|
||||
const { webdavPasswd } = window.app.pageOptions;
|
||||
|
||||
class WebdavPassword extends React.Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
isPasswordVisible: false,
|
||||
password: webdavPasswd
|
||||
};
|
||||
}
|
||||
|
||||
handleInputChange = (e) => {
|
||||
let passwd = e.target.value.trim();
|
||||
this.setState({password: passwd});
|
||||
}
|
||||
|
||||
togglePasswordVisible = () => {
|
||||
this.setState({
|
||||
isPasswordVisible: !this.state.isPasswordVisible
|
||||
});
|
||||
}
|
||||
|
||||
generatePassword = () => {
|
||||
let randomPassword = '';
|
||||
const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
||||
for (let i = 0; i < 8; i++) {
|
||||
randomPassword += possible.charAt(Math.floor(Math.random() * possible.length));
|
||||
}
|
||||
this.setState({
|
||||
password: randomPassword,
|
||||
isPasswordVisible: true
|
||||
});
|
||||
}
|
||||
|
||||
updatePassword = (password) => {
|
||||
seafileAPI.updateWebdavSecret(password).then((res) => {
|
||||
toaster.success(gettext('Success'));
|
||||
}).catch((error) => {
|
||||
let errorMsg = '';
|
||||
if (error.response) {
|
||||
if (error.response.data && error.response.data['error_msg']) {
|
||||
errorMsg = error.response.data['error_msg'];
|
||||
} else {
|
||||
errorMsg = gettext('Error');
|
||||
}
|
||||
} else {
|
||||
errorMsg = gettext('Please check the network.');
|
||||
}
|
||||
toaster.danger(errorMsg);
|
||||
});
|
||||
}
|
||||
|
||||
handleUpdate = () => {
|
||||
this.updatePassword(this.state.password);
|
||||
}
|
||||
|
||||
handleDelete = () => {
|
||||
this.setState({
|
||||
password: ''
|
||||
});
|
||||
this.updatePassword('');
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div id="update-webdav-passwd" className="setting-item">
|
||||
<h3 className="setting-item-heading">{gettext('WebDav Password')}</h3>
|
||||
<label>{gettext('Password')}</label>
|
||||
<div className="row mb-2">
|
||||
<InputGroup className="col-sm-5">
|
||||
<Input type={this.state.isPasswordVisible ? 'text' : 'password'} value={this.state.password} onChange={this.handleInputChange} />
|
||||
<InputGroupAddon addonType="append">
|
||||
<Button onClick={this.togglePasswordVisible}><i className={`fas ${this.state.isPasswordVisible ? 'fa-eye': 'fa-eye-slash'}`}></i></Button>
|
||||
<Button onClick={this.generatePassword}><i className="fas fa-magic"></i></Button>
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
</div>
|
||||
<button className="btn btn-secondary mr-1" onClick={this.handleUpdate}>{gettext('Update')}</button>
|
||||
<button className="btn btn-secondary" onClick={this.handleDelete}>{gettext('Delete')}</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default WebdavPassword;
|
48
frontend/src/css/user-settings.css
Normal file
48
frontend/src/css/user-settings.css
Normal file
@@ -0,0 +1,48 @@
|
||||
body {
|
||||
overflow: hidden;
|
||||
}
|
||||
#wrapper {
|
||||
height: 100%;
|
||||
}
|
||||
.top-header {
|
||||
background: #f4f4f7;
|
||||
border-bottom: 1px solid #e8e8e8;
|
||||
padding: 8px 16px 4px;
|
||||
height: 53px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.side-panel {
|
||||
flex: 0 0 22%;
|
||||
padding: 20px;
|
||||
border-right: 1px solid #eee;
|
||||
}
|
||||
.main-panel {
|
||||
flex: 1 0 78%;
|
||||
}
|
||||
.heading {
|
||||
padding: 8px 16px;
|
||||
background: #f9f9f9;
|
||||
font-size: 1rem;
|
||||
color: #322;
|
||||
font-weight: normal;
|
||||
line-height: 1.5;
|
||||
margin:0;
|
||||
}
|
||||
.content {
|
||||
padding: 0rem 1rem;
|
||||
overflow: auto;
|
||||
}
|
||||
.setting-item {
|
||||
margin: 1em 0 2em;
|
||||
}
|
||||
.setting-item-heading {
|
||||
font-size: 1rem;
|
||||
font-weight: normal;
|
||||
padding-bottom: 0.2em;
|
||||
border-bottom: 1px solid #ddd;
|
||||
margin-bottom: 0.7em;
|
||||
}
|
||||
.user-avatar {
|
||||
border-radius: 3px;
|
||||
}
|
132
frontend/src/settings.js
Normal file
132
frontend/src/settings.js
Normal file
@@ -0,0 +1,132 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { navigate } from '@reach/router';
|
||||
import { Utils } from './utils/utils';
|
||||
import { isPro, gettext, siteRoot, mediaUrl, logoPath, logoWidth, logoHeight, siteTitle } from './utils/constants';
|
||||
import { seafileAPI } from './utils/seafile-api';
|
||||
import toaster from './components/toast';
|
||||
import CommonToolbar from './components/toolbar/common-toolbar';
|
||||
import SideNav from './components/user-settings/side-nav';
|
||||
import UserAvatarForm from './components/user-settings/user-avatar-form';
|
||||
import UserBasicInfoForm from './components/user-settings/user-basic-info-form';
|
||||
import WebdavPassword from './components/user-settings/webdav-password';
|
||||
import LanguageSetting from './components/user-settings/language-setting';
|
||||
import ListInAddressBook from './components/user-settings/list-in-address-book';
|
||||
import EmailNotice from './components/user-settings/email-notice';
|
||||
import TwoFactorAuthentication from './components/user-settings/two-factor-auth';
|
||||
import SocialLogin from './components/user-settings/social-login';
|
||||
import DeleteAccount from './components/user-settings/delete-account';
|
||||
|
||||
import './css/toolbar.css';
|
||||
import './css/search.css';
|
||||
|
||||
import './css/user-settings.css';
|
||||
|
||||
const {
|
||||
canUpdatePassword, passwordOperationText,
|
||||
enableAddressBook,
|
||||
enableWebdavSecret,
|
||||
twoFactorAuthEnabled,
|
||||
enableWechatWork,
|
||||
enableDeleteAccount
|
||||
} = window.app.pageOptions;
|
||||
|
||||
class Settings extends React.Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
seafileAPI.getUserInfo().then((res) => {
|
||||
this.setState({
|
||||
userInfo: res.data
|
||||
});
|
||||
}).catch((error) => {
|
||||
// do nothing
|
||||
});
|
||||
}
|
||||
|
||||
updateUserInfo = (data) => {
|
||||
seafileAPI.updateUserInfo(data).then((res) => {
|
||||
this.setState({
|
||||
userInfo: res.data
|
||||
});
|
||||
toaster.success(gettext('Success'));
|
||||
}).catch((error) => {
|
||||
let errorMsg = '';
|
||||
if (error.response) {
|
||||
if (error.response.data && error.response.data['error_msg']) {
|
||||
errorMsg = error.response.data['error_msg'];
|
||||
} else {
|
||||
errorMsg = gettext('Error');
|
||||
}
|
||||
} else {
|
||||
errorMsg = gettext('Please check the network.');
|
||||
}
|
||||
toaster.danger(errorMsg);
|
||||
});
|
||||
}
|
||||
|
||||
onSearchedClick = (selectedItem) => {
|
||||
if (selectedItem.is_dir === true) {
|
||||
let url = siteRoot + 'library/' + selectedItem.repo_id + '/' + selectedItem.repo_name + selectedItem.path;
|
||||
navigate(url, {repalce: true});
|
||||
} else {
|
||||
let url = siteRoot + 'lib/' + selectedItem.repo_id + '/file' + Utils.encodePath(selectedItem.path);
|
||||
let newWindow = window.open('about:blank');
|
||||
newWindow.location.href = url;
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<div className="h-100 d-flex flex-column">
|
||||
<div className="top-header d-flex justify-content-between">
|
||||
<a href={siteRoot}>
|
||||
<img src={mediaUrl + logoPath} height={logoHeight} width={logoWidth} title={siteTitle} alt="logo" />
|
||||
</a>
|
||||
<CommonToolbar onSearchedClick={this.onSearchedClick} />
|
||||
</div>
|
||||
<div className="flex-auto d-flex">
|
||||
<div className="side-panel o-auto">
|
||||
<SideNav />
|
||||
</div>
|
||||
<div className="main-panel d-flex flex-column">
|
||||
<h2 className="heading">{gettext('Settings')}</h2>
|
||||
<div className="content">
|
||||
<div id="user-basic-info" className="setting-item">
|
||||
<h3 className="setting-item-heading">{gettext('Profile Setting')}</h3>
|
||||
<UserAvatarForm />
|
||||
{this.state.userInfo && <UserBasicInfoForm userInfo={this.state.userInfo} updateUserInfo={this.updateUserInfo} />}
|
||||
</div>
|
||||
{canUpdatePassword &&
|
||||
<div id="update-user-passwd" className="setting-item">
|
||||
<h3 className="setting-item-heading">{gettext('Password')}</h3>
|
||||
<a href={`${siteRoot}accounts/password/change/`} className="btn btn-secondary">{passwordOperationText}</a>
|
||||
</div>
|
||||
}
|
||||
{enableWebdavSecret && <WebdavPassword />}
|
||||
{enableAddressBook && this.state.userInfo &&
|
||||
<ListInAddressBook userInfo={this.state.userInfo} updateUserInfo={this.updateUserInfo} />}
|
||||
<LanguageSetting />
|
||||
{isPro && <EmailNotice />}
|
||||
{twoFactorAuthEnabled && <TwoFactorAuthentication />}
|
||||
{enableWechatWork && <SocialLogin />}
|
||||
{enableDeleteAccount && <DeleteAccount />}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ReactDOM.render(
|
||||
<Settings />,
|
||||
document.getElementById('wrapper')
|
||||
);
|
57
seahub/profile/templates/profile/set_profile_react.html
Normal file
57
seahub/profile/templates/profile/set_profile_react.html
Normal file
@@ -0,0 +1,57 @@
|
||||
{% extends 'base_for_react.html' %}
|
||||
{% load seahub_tags avatar_tags i18n %}
|
||||
{% load render_bundle from webpack_loader %}
|
||||
|
||||
{% block sub_title %}{% trans "Settings" %} - {% endblock %}
|
||||
|
||||
{% block extra_style %}
|
||||
{% render_bundle 'settings' 'css' %}
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_script %}
|
||||
<script type="text/javascript">
|
||||
// overwrite the one in base_for_react.html
|
||||
window.app.pageOptions = {
|
||||
avatarURL: '{% avatar_url request.user 160 %}',
|
||||
csrfToken: '{{ csrf_token }}',
|
||||
|
||||
enableUpdateUserInfo: {% if ENABLE_UPDATE_USER_INFO %} true {% else %} false {% endif %},
|
||||
nameLabel: "{% trans "Name:" context "true name" %}",
|
||||
enableUserSetContactEmail: {% if ENABLE_USER_SET_CONTACT_EMAIL %} true {% else %} false {% endif %},
|
||||
|
||||
canUpdatePassword: {% if not is_ldap_user and ENABLE_CHANGE_PASSWORD %} true {% else %} false {% endif %},
|
||||
passwordOperationText: {% if user_unusable_password %}"{% trans "Set Password" %}"{% else %}"{% trans "Update" %}"{% endif %},
|
||||
|
||||
enableWebdavSecret: {% if ENABLE_WEBDAV_SECRET %} true {% else %} false {% endif %},
|
||||
webdavPasswd: '{{ webdav_passwd|escapejs }}',
|
||||
|
||||
enableAddressBook: {% if ENABLE_ADDRESSBOOK_OPT_IN %} true {% else %} false {% endif %},
|
||||
|
||||
currentLang: '{{ LANGUAGE_CODE|language_name_local|capfirst|escapejs }}',
|
||||
langList: (function() {
|
||||
var list = [];
|
||||
{% for LANG in LANGUAGES %}
|
||||
list.push({
|
||||
langCode: '{{LANG.0|escapejs}}',
|
||||
langName: '{{LANG.1|escapejs}}'
|
||||
});
|
||||
{% endfor %}
|
||||
return list;
|
||||
})(),
|
||||
|
||||
initialEmailNotificationInterval: {{ email_notification_interval }},
|
||||
|
||||
twoFactorAuthEnabled: {% if two_factor_auth_enabled %} true {% else %} false {% endif %},
|
||||
defaultDevice: {% if default_device %} true {% else %} false {% endif %},
|
||||
backupTokens: {{backup_tokens}},
|
||||
|
||||
enableWechatWork: {% if enable_wechat_work %} true {% else %} false {% endif %},
|
||||
langCode: "{{LANGUAGE_CODE|escapejs}}",
|
||||
socialConnected: {% if social_connected %} true {% else %} false {% endif %},
|
||||
socialNextPage: "{{ social_next_page|escapejs }}",
|
||||
|
||||
enableDeleteAccount: {% if ENABLE_DELETE_ACCOUNT %} true {% else %} false {% endif %}
|
||||
};
|
||||
</script>
|
||||
{% render_bundle 'settings' 'js' %}
|
||||
{% endblock %}
|
@@ -131,7 +131,9 @@ def edit_profile(request):
|
||||
resp_dict['default_device'] = default_device(request.user)
|
||||
resp_dict['backup_tokens'] = backup_tokens
|
||||
|
||||
return render(request, 'profile/set_profile.html', resp_dict)
|
||||
#template = 'profile/set_profile.html'
|
||||
template = 'profile/set_profile_react.html'
|
||||
return render(request, template, resp_dict)
|
||||
|
||||
@login_required
|
||||
def user_profile(request, username):
|
||||
|
Reference in New Issue
Block a user