diff --git a/frontend/config/webpack.config.dev.js b/frontend/config/webpack.config.dev.js
index f4b090b052..b15617522c 100644
--- a/frontend/config/webpack.config.dev.js
+++ b/frontend/config/webpack.config.dev.js
@@ -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'),
diff --git a/frontend/config/webpack.config.prod.js b/frontend/config/webpack.config.prod.js
index 6fbe09b931..a41ff61334 100644
--- a/frontend/config/webpack.config.prod.js
+++ b/frontend/config/webpack.config.prod.js
@@ -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"],
diff --git a/frontend/src/components/dialog/confirm-delete-account.js b/frontend/src/components/dialog/confirm-delete-account.js
new file mode 100644
index 0000000000..e65dd24727
--- /dev/null
+++ b/frontend/src/components/dialog/confirm-delete-account.js
@@ -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 (
+
+ {gettext('Delete Account')}
+
+ {gettext('Really want to delete your account?')}
+
+
+
+ {gettext('Cancel')}
+ {gettext('Delete')}
+
+
+ );
+ }
+}
+
+ConfirmDeleteAccount.propTypes = propTypes;
+
+export default ConfirmDeleteAccount;
diff --git a/frontend/src/components/dialog/confirm-disconnect-wechat.js b/frontend/src/components/dialog/confirm-disconnect-wechat.js
new file mode 100644
index 0000000000..f04a3b27e3
--- /dev/null
+++ b/frontend/src/components/dialog/confirm-disconnect-wechat.js
@@ -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 (
+
+ {gettext('Disconnect')}
+
+ {gettext('Are you sure you want to disconnect?')}
+
+
+
+ {gettext('Cancel')}
+ {gettext('Disconnect')}
+
+
+ );
+ }
+}
+
+ConfirmDisconnectWechat.propTypes = propTypes;
+
+export default ConfirmDisconnectWechat;
diff --git a/frontend/src/components/user-settings/delete-account.js b/frontend/src/components/user-settings/delete-account.js
new file mode 100644
index 0000000000..db7e2ed808
--- /dev/null
+++ b/frontend/src/components/user-settings/delete-account.js
@@ -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 (
+
+
+
{gettext('Delete Account')}
+
{gettext('This operation will not be reverted. Please think twice!')}
+
{gettext('Delete')}
+
+ {this.state.isConfirmDialogOpen && (
+
+
+
+ )}
+
+ );
+ }
+}
+
+export default DeleteAccount;
diff --git a/frontend/src/components/user-settings/email-notice.js b/frontend/src/components/user-settings/email-notice.js
new file mode 100644
index 0000000000..8c9701aa16
--- /dev/null
+++ b/frontend/src/components/user-settings/email-notice.js
@@ -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 (
+
+
{gettext('Email Notification of File Changes')}
+
+
+ );
+ }
+}
+
+export default EmailNotice;
diff --git a/frontend/src/components/user-settings/language-setting.js b/frontend/src/components/user-settings/language-setting.js
new file mode 100644
index 0000000000..0fcb543bf8
--- /dev/null
+++ b/frontend/src/components/user-settings/language-setting.js
@@ -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 (
+
+
{gettext('Language Setting')}
+
+
+ {currentLang}
+
+
+ {langList.map((item, index) => {
+ return (
+
+
+ {item.langName}
+
+
+ );
+ })}
+
+
+
+ );
+ }
+}
+
+export default LanguageSetting;
diff --git a/frontend/src/components/user-settings/list-in-address-book.js b/frontend/src/components/user-settings/list-in-address-book.js
new file mode 100644
index 0000000000..b81ab43730
--- /dev/null
+++ b/frontend/src/components/user-settings/list-in-address-book.js
@@ -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 (
+
+
{gettext('Global Address Book')}
+
+
+ {gettext('List your account in global address book, so that others can find you by typing your name.')}
+
+
+ );
+ }
+}
+
+export default ListInAddressBook;
diff --git a/frontend/src/components/user-settings/side-nav.js b/frontend/src/components/user-settings/side-nav.js
new file mode 100644
index 0000000000..ca62462922
--- /dev/null
+++ b/frontend/src/components/user-settings/side-nav.js
@@ -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 (
+
+ );
+ }
+}
+
+export default SideNav;
diff --git a/frontend/src/components/user-settings/social-login.js b/frontend/src/components/user-settings/social-login.js
new file mode 100644
index 0000000000..565fc17c12
--- /dev/null
+++ b/frontend/src/components/user-settings/social-login.js
@@ -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 (
+
+
+ {this.state.isConfirmDialogOpen && (
+
+
+
+ )}
+
+ );
+ }
+}
+
+export default SocialLogin;
diff --git a/frontend/src/components/user-settings/two-factor-auth.js b/frontend/src/components/user-settings/two-factor-auth.js
new file mode 100644
index 0000000000..12fa123afd
--- /dev/null
+++ b/frontend/src/components/user-settings/two-factor-auth.js
@@ -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 (
+
+ {gettext('Status: enabled')}
+
+ {gettext('Disable Two-Factor Authentication')}
+
+ {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)}
+
+ {gettext('Show Codes')}
+
+ );
+ }
+
+ renderDisabled = () => {
+ return (
+
+ {gettext('Two-factor authentication is not enabled for your account. Enable two-factor authentication for enhanced account security.')}
+
+ {gettext('Enable Two-Factor Authentication')}
+
+ );
+ }
+
+ render() {
+ return (
+
+
{gettext('Two-Factor Authentication')}
+ {defaultDevice ? this.renderEnabled() : this.renderDisabled()}
+
+ );
+ }
+}
+
+export default TwoFactorAuthentication;
diff --git a/frontend/src/components/user-settings/user-avatar-form.js b/frontend/src/components/user-settings/user-avatar-form.js
new file mode 100644
index 0000000000..685b7e0abf
--- /dev/null
+++ b/frontend/src/components/user-settings/user-avatar-form.js
@@ -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 (
+
+ );
+ }
+}
+
+export default UserAvatarForm;
diff --git a/frontend/src/components/user-settings/user-basic-info-form.js b/frontend/src/components/user-settings/user-basic-info-form.js
new file mode 100644
index 0000000000..0cd094981a
--- /dev/null
+++ b/frontend/src/components/user-settings/user-basic-info-form.js
@@ -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 (
+
+ );
+ }
+}
+
+export default UserBasicInfoForm;
diff --git a/frontend/src/components/user-settings/webdav-password.js b/frontend/src/components/user-settings/webdav-password.js
new file mode 100644
index 0000000000..5415fe4b9a
--- /dev/null
+++ b/frontend/src/components/user-settings/webdav-password.js
@@ -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 (
+
+
{gettext('WebDav Password')}
+
{gettext('Password')}
+
+
+
+
+
+
+
+
+
+
{gettext('Update')}
+
{gettext('Delete')}
+
+ );
+ }
+}
+
+export default WebdavPassword;
diff --git a/frontend/src/css/user-settings.css b/frontend/src/css/user-settings.css
new file mode 100644
index 0000000000..124f92e0ad
--- /dev/null
+++ b/frontend/src/css/user-settings.css
@@ -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;
+}
diff --git a/frontend/src/settings.js b/frontend/src/settings.js
new file mode 100644
index 0000000000..a64b426483
--- /dev/null
+++ b/frontend/src/settings.js
@@ -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 (
+
+
+
+
+
+
+
+
+
{gettext('Settings')}
+
+
+
{gettext('Profile Setting')}
+
+ {this.state.userInfo && }
+
+ {canUpdatePassword &&
+
+ }
+ {enableWebdavSecret &&
}
+ {enableAddressBook && this.state.userInfo &&
+
}
+
+ {isPro &&
}
+ {twoFactorAuthEnabled &&
}
+ {enableWechatWork &&
}
+ {enableDeleteAccount &&
}
+
+
+
+
+
+ );
+ }
+}
+
+ReactDOM.render(
+ ,
+ document.getElementById('wrapper')
+);
diff --git a/seahub/profile/templates/profile/set_profile_react.html b/seahub/profile/templates/profile/set_profile_react.html
new file mode 100644
index 0000000000..d07d710c6c
--- /dev/null
+++ b/seahub/profile/templates/profile/set_profile_react.html
@@ -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 %}
+
+{% render_bundle 'settings' 'js' %}
+{% endblock %}
diff --git a/seahub/profile/views.py b/seahub/profile/views.py
index 8fd44fda12..35e69f47e4 100644
--- a/seahub/profile/views.py
+++ b/seahub/profile/views.py
@@ -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):
{gettext('Social Login')}
+{langCode == 'zh-cn' ? '企业微信': 'WeChat Work'}
+ {socialConnected ? + {gettext('Disconnect')} : + {gettext('Connect')} + } +