Просмотр исходного кода

Merge pull request #1579 from weseek/reactify/create-user-settings-component

Reactify/create user settings component
itizawa 6 лет назад
Родитель
Сommit
24104624f7

+ 0 - 9
resource/locales/en-US/common/toaster.json

@@ -1,9 +0,0 @@
-{
-  "update_successed": "Succeeded to update {{target}} setting",
-  "give_user_admin": "Succeeded to give {{username}} admin",
-  "remove_user_admin": "Succeeded to remove {{username}} admin ",
-  "activate_user_success": "Succeeded to activating {{username}}",
-  "deactivate_user_success": "Succeeded to deactivate {{username}}",
-  "remove_user_success": "Succeeded to removing {{username}} ",
-  "remove_external_user_success": "Succeeded to remove {{accountId}} "
-}

+ 0 - 9
resource/locales/ja/common/toaster.json

@@ -1,9 +0,0 @@
-{
-  "update_successed": "{{target}}を更新しました",
-  "give_user_admin": "{{username}}を管理者に設定しました",
-  "remove_user_admin": "{{username}}を管理者から外しました",
-  "activate_user_success": "{{username}}を有効化しました",
-  "deactivate_user_success": "{{username}}を無効化しました",
-  "remove_user_success": "{{username}}を削除しました",
-  "remove_external_user_success": "{{accountId}}を削除しました "
-}

+ 17 - 0
src/client/js/app.jsx

@@ -45,6 +45,7 @@ import Customize from './components/Admin/Customize/Customize';
 import ImportDataPage from './components/Admin/ImportDataPage';
 import ExportArchiveDataPage from './components/Admin/ExportArchiveDataPage';
 import FullTextSearchManagement from './components/Admin/FullTextSearchManagement';
+import PersonalSettings from './components/Me/PersonalSettings';
 
 import AppContainer from './services/AppContainer';
 import PageContainer from './services/PageContainer';
@@ -59,6 +60,7 @@ import AdminAppContainer from './services/AdminAppContainer';
 import WebsocketContainer from './services/WebsocketContainer';
 import AdminMarkDownContainer from './services/AdminMarkDownContainer';
 import AdminExternalAccountsContainer from './services/AdminExternalAccountsContainer';
+import PersonalContainer from './services/PersonalContainer';
 
 const logger = loggerFactory('growi:app');
 
@@ -246,6 +248,21 @@ if (adminUserGroupPageElem != null) {
   );
 }
 
+const personalSettingsElem = document.getElementById('personal-setting');
+if (personalSettingsElem != null) {
+  const personalContainer = new PersonalContainer(appContainer);
+  ReactDOM.render(
+    <Provider inject={[injectableContainers, personalContainer]}>
+      <I18nextProvider i18n={i18n}>
+        <PersonalSettings
+          crowi={appContainer}
+        />
+      </I18nextProvider>
+    </Provider>,
+    personalSettingsElem,
+  );
+}
+
 // うわーもうー (commented by Crowi team -- 2018.03.23 Yuki Takei)
 $('a[data-toggle="tab"][href="#revision-history"]').on('show.bs.tab', () => {
   ReactDOM.render(

+ 153 - 0
src/client/js/components/Me/BasicInfoSettings.jsx

@@ -0,0 +1,153 @@
+
+import React, { Fragment } from 'react';
+import PropTypes from 'prop-types';
+import { withTranslation } from 'react-i18next';
+
+import loggerFactory from '@alias/logger';
+
+import { toastSuccess, toastError } from '../../util/apiNotification';
+import { createSubscribedElement } from '../UnstatedUtils';
+
+import AppContainer from '../../services/AppContainer';
+import PersonalContainer from '../../services/PersonalContainer';
+
+const logger = loggerFactory('growi:basicInfoSettings');
+
+class BasicInfoSettings extends React.Component {
+
+  constructor(appContainer) {
+    super();
+
+    this.onClickSubmit = this.onClickSubmit.bind(this);
+  }
+
+  async onClickSubmit() {
+    const { t, personalContainer } = this.props;
+
+    try {
+      await personalContainer.updateBasicInfo();
+      toastSuccess(t('toaster.update_successed', { target: t('Basic Info') }));
+    }
+    catch (err) {
+      toastError(err);
+      logger.error(err);
+    }
+  }
+
+  render() {
+    const { t, personalContainer } = this.props;
+
+    return (
+      <Fragment>
+
+        <div className="row mb-3">
+          <label htmlFor="userForm[name]" className="col-sm-2 text-right">{t('Name')}</label>
+          <div className="col-sm-4 text-left">
+            <input
+              className="form-control"
+              type="text"
+              name="userForm[name]"
+              defaultValue={personalContainer.state.name}
+              onChange={(e) => { personalContainer.changeName(e.target.value) }}
+            />
+          </div>
+        </div>
+
+        <div className="row mb-3">
+          <label htmlFor="userForm[email]" className="col-sm-2 text-right">{t('Email')}</label>
+          <div className="col-sm-4 text-left">
+            <input
+              className="form-control"
+              type="text"
+              name="userForm[email]"
+              defaultValue={personalContainer.state.email}
+              onChange={(e) => { personalContainer.changeEmail(e.target.value) }}
+            />
+          </div>
+          {personalContainer.state.registrationWhiteList.length !== 0 && (
+            <div className="col-sm-offset-2 col-sm-10">
+              <p className="help-block">
+                {t('page_register.form_help.email')}
+                <ul>
+                  <li><code></code></li>
+                </ul>
+              </p>
+            </div>
+          )}
+        </div>
+
+        <div className="row mb-3">
+          <label className="col-xs-2 text-right">{t('Disclose E-mail')}</label>
+          <div className="col-xs-4">
+            <div className="radio radio-primary radio-inline">
+              <input
+                type="radio"
+                id="radioEmailShow"
+                name="userForm[isEmailPublished]"
+                checked={personalContainer.state.isEmailPublished}
+                onChange={() => { personalContainer.changeIsEmailPublished(true) }}
+              />
+              <label htmlFor="radioEmailShow">{t('Show')}</label>
+            </div>
+            <div className="radio radio-primary radio-inline">
+              <input
+                type="radio"
+                id="radioEmailHide"
+                name="userForm[isEmailPublished]"
+                checked={!personalContainer.state.isEmailPublished}
+                onChange={() => { personalContainer.changeIsEmailPublished(false) }}
+              />
+              <label htmlFor="radioEmailHide">{t('Hide')}</label>
+            </div>
+          </div>
+        </div>
+
+        <div className="row mb-3">
+          <label className="col-xs-2 text-right">{t('Language')}</label>
+          <div className="col-xs-4">
+            <div className="radio radio-primary radio-inline">
+              <input
+                type="radio"
+                id="radioLangEn"
+                name="userForm[lang]"
+                checked={personalContainer.state.lang === 'English'}
+                onChange={() => { personalContainer.changeLang('English') }}
+              />
+              <label htmlFor="radioLangEn">{t('English')}</label>
+            </div>
+            <div className="radio radio-primary radio-inline">
+              <input
+                type="radio"
+                id="radioLangJa"
+                name="userForm[lang]"
+                checked={personalContainer.state.lang === 'Japanese'}
+                onChange={() => { personalContainer.changeLang('Japanese') }}
+              />
+              <label htmlFor="radioLangJa">{t('Japanese')}</label>
+            </div>
+          </div>
+        </div>
+
+        <div className="row my-3">
+          <div className="col-xs-offset-4 col-xs-5">
+            <button type="button" className="btn btn-primary" onClick={this.onClickSubmit}>{t('Update')}</button>
+          </div>
+        </div>
+
+      </Fragment>
+    );
+  }
+
+}
+
+const BasicInfoSettingsWrapper = (props) => {
+  return createSubscribedElement(BasicInfoSettings, props, [AppContainer, PersonalContainer]);
+};
+
+BasicInfoSettings.propTypes = {
+  t: PropTypes.func.isRequired, // i18next
+  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
+  personalContainer: PropTypes.instanceOf(PersonalContainer).isRequired,
+};
+
+export default withTranslation()(BasicInfoSettingsWrapper);

+ 58 - 0
src/client/js/components/Me/PersonalSettings.jsx

@@ -0,0 +1,58 @@
+
+import React, { Fragment } from 'react';
+import PropTypes from 'prop-types';
+import { withTranslation } from 'react-i18next';
+
+import UserSettings from './UserSettings';
+
+class PersonalSettings extends React.Component {
+
+  render() {
+    const { t } = this.props;
+
+    return (
+      <Fragment>
+        {/* TODO GW-226 adapt BS4 */}
+        <div className="m-t-10">
+          <div className="personal-settings">
+            <ul className="nav nav-tabs" role="tablist">
+              <li className="active">
+                <a href="#user-settings" data-toggle="tab" role="tab"><i className="icon-user"></i> { t('User Information') }</a>
+              </li>
+              <li>
+                <a href="#external-accounts" data-toggle="tab" role="tab"><i className="icon-share-alt"></i> { t('External Accounts') }</a>
+              </li>
+              <li>
+                <a href="#password-settings" data-toggle="tab" role="tab"><i className="icon-lock"></i> { t('Password Settings') }</a>
+              </li>
+              <li>
+                <a href="#apiToken" data-toggle="tab" role="tab"><i className="icon-paper-plane"></i> { t('API Settings') }</a>
+              </li>
+            </ul>
+            <div className="tab-content p-t-10">
+              <div id="user-settings" className="tab-pane active" role="tabpanel">
+                <UserSettings />
+              </div>
+              <div id="external-accounts" className="tab-pane" role="tabpanel">
+                {/* TODO GW-1029 create component */}
+              </div>
+              <div id="password-settings" className="tab-pane" role="tabpanel">
+                {/* TODO GW-1030 create component */}
+              </div>
+              <div id="apiToken" className="tab-pane" role="tabpanel">
+                {/* TODO GW-1031 create component */}
+              </div>
+            </div>
+          </div>
+        </div>
+      </Fragment>
+    );
+  }
+
+}
+
+PersonalSettings.propTypes = {
+  t: PropTypes.func.isRequired, // i18next
+};
+
+export default withTranslation()(PersonalSettings);

+ 36 - 0
src/client/js/components/Me/UserSettings.jsx

@@ -0,0 +1,36 @@
+
+import React, { Fragment } from 'react';
+import PropTypes from 'prop-types';
+import { withTranslation } from 'react-i18next';
+
+import BasicInfoSettings from './BasicInfoSettings';
+
+class UserSettings extends React.Component {
+
+  render() {
+    const { t } = this.props;
+
+    return (
+      <Fragment>
+        <div className="mb-5 container-fluid">
+          <h2 className="border-bottom">{t('Basic Info')}</h2>
+          <BasicInfoSettings />
+        </div>
+
+        <div className="mb-5 container-fluid">
+          <h2 className="border-bottom">{t('Set Profile Image')}</h2>
+          {/* TODO GW-1032 create component */}
+        </div>
+
+      </Fragment>
+    );
+  }
+
+}
+
+
+UserSettings.propTypes = {
+  t: PropTypes.func.isRequired, // i18next
+};
+
+export default withTranslation()(UserSettings);

+ 88 - 0
src/client/js/services/PersonalContainer.js

@@ -0,0 +1,88 @@
+import { Container } from 'unstated';
+
+import loggerFactory from '@alias/logger';
+
+import { toastError } from '../util/apiNotification';
+
+// eslint-disable-next-line no-unused-vars
+const logger = loggerFactory('growi:services:PersonalContainer');
+
+/**
+ * Service container for personal settings page (PersonalSettings.jsx)
+ * @extends {Container} unstated Container
+ */
+export default class PersonalContainer extends Container {
+
+  constructor(appContainer) {
+    super();
+
+    this.appContainer = appContainer;
+
+    this.state = {
+      name: '',
+      email: '',
+      registrationWhiteList: [],
+      isEmailPublished: false,
+      lang: 'English',
+    };
+
+  }
+
+  /**
+   * Workaround for the mangling in production build to break constructor.name
+   */
+  static getClassName() {
+    return 'PersonalContainer';
+  }
+
+  /**
+   * retrieve personal data
+   */
+  async retrievePersonalData() {
+    try {
+      // TODO GW-1036 retrieve data
+    }
+    catch (err) {
+      logger.error(err);
+      toastError(new Error('Failed to fetch data'));
+    }
+  }
+
+  /**
+   * Change name
+   */
+  changeName(inputValue) {
+    this.setState({ name: inputValue });
+  }
+
+  /**
+   * Change email
+   */
+  changeEmail(inputValue) {
+    this.setState({ email: inputValue });
+  }
+
+  /**
+   * Change isEmailPublished
+   */
+  changeIsEmailPublished(boolean) {
+    this.setState({ isEmailPublished: boolean });
+  }
+
+  /**
+   * Change lang
+   */
+  changeLang(lang) {
+    this.setState({ lang });
+  }
+
+  /**
+   * Update basic info
+   * @memberOf PersonalContainer
+   * @return {Array} basic info
+   */
+  async updateBasicInfo() {
+    // TODO GW-1036 create apiV3
+  }
+
+}

+ 1 - 1
src/server/views/me/index.html

@@ -11,7 +11,7 @@
 {% endblock %}
 
 {% block content_main %}
-<div class="content-main">
+<div class="content-main" id="personal-setting">
 
   <ul class="nav nav-tabs">
     <li class="active"><a href="/me"><i class="icon-user"></i> {{ t('User Information') }}</a></li>