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'),
|
require.resolve('react-dev-utils/webpackHotDevClient'),
|
||||||
paths.appSrc + "/view-file-unknown.js",
|
paths.appSrc + "/view-file-unknown.js",
|
||||||
],
|
],
|
||||||
|
settings: [
|
||||||
|
require.resolve('./polyfills'),
|
||||||
|
require.resolve('react-dev-utils/webpackHotDevClient'),
|
||||||
|
paths.appSrc + "/settings.js",
|
||||||
|
],
|
||||||
orgAdmin: [
|
orgAdmin: [
|
||||||
require.resolve('./polyfills'),
|
require.resolve('./polyfills'),
|
||||||
require.resolve('react-dev-utils/webpackHotDevClient'),
|
require.resolve('react-dev-utils/webpackHotDevClient'),
|
||||||
|
@@ -87,6 +87,7 @@ module.exports = {
|
|||||||
viewFileSVG: [require.resolve('./polyfills'), paths.appSrc + "/view-file-svg.js"],
|
viewFileSVG: [require.resolve('./polyfills'), paths.appSrc + "/view-file-svg.js"],
|
||||||
viewFileAudio: [require.resolve('./polyfills'), paths.appSrc + "/view-file-audio.js"],
|
viewFileAudio: [require.resolve('./polyfills'), paths.appSrc + "/view-file-audio.js"],
|
||||||
viewFileUnknown: [require.resolve('./polyfills'), paths.appSrc + "/view-file-unknown.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"],
|
orgAdmin: [require.resolve('./polyfills'), paths.appSrc + "/pages/org-admin"],
|
||||||
sysAdmin: [require.resolve('./polyfills'), paths.appSrc + "/pages/sys-admin"],
|
sysAdmin: [require.resolve('./polyfills'), paths.appSrc + "/pages/sys-admin"],
|
||||||
viewDataGrid: [require.resolve('./polyfills'), paths.appSrc + "/view-file-ctable.js"],
|
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['default_device'] = default_device(request.user)
|
||||||
resp_dict['backup_tokens'] = backup_tokens
|
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
|
@login_required
|
||||||
def user_profile(request, username):
|
def user_profile(request, username):
|
||||||
|
Reference in New Issue
Block a user