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

Merge pull request #1670 from weseek/reactify/profileImage-settings

Reactify/profile image settings
Yuki Takei 6 лет назад
Родитель
Сommit
98ae2e4ef7

+ 118 - 0
src/client/js/components/Me/ProfileImageSettings.jsx

@@ -0,0 +1,118 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { withTranslation } from 'react-i18next';
+import md5 from 'md5';
+
+import { toastSuccess, toastError } from '../../util/apiNotification';
+import { createSubscribedElement } from '../UnstatedUtils';
+
+import AppContainer from '../../services/AppContainer';
+import PersonalContainer from '../../services/PersonalContainer';
+
+class ProfileImageSettings extends React.Component {
+
+  constructor(appContainer) {
+    super();
+
+    this.onClickSubmit = this.onClickSubmit.bind(this);
+  }
+
+  async onClickSubmit() {
+    const { t, personalContainer } = this.props;
+
+    try {
+      await personalContainer.updateProfileImage();
+      toastSuccess(t('toaster.update_successed', { target: t('Set Profile Image') }));
+    }
+    catch (err) {
+      toastError(err);
+    }
+  }
+
+  generateGravatarSrc() {
+    const email = this.props.personalContainer.state.email || '';
+    const hash = md5(email.trim().toLowerCase());
+    return `https://gravatar.com/avatar/${hash}`;
+  }
+
+  render() {
+    const { t, personalContainer } = this.props;
+
+    return (
+      <React.Fragment>
+        <div className="row">
+          <div className="col-md-2 col-sm-offset-1 col-sm-4">
+            <h4>
+              <div className="radio radio-primary">
+                <input
+                  type="radio"
+                  id="radioGravatar"
+                  form="formImageType"
+                  name="imagetypeForm[isGravatarEnabled]"
+                  checked={personalContainer.state.isGravatarEnabled}
+                  onChange={() => { personalContainer.changeIsGravatarEnabled(true) }}
+                />
+                <label htmlFor="radioGravatar">
+                  <img src="https://gravatar.com/avatar/00000000000000000000000000000000?s=24" /> Gravatar
+                </label>
+                <a href="https://gravatar.com/">
+                  <small><i className="icon-arrow-right-circle" aria-hidden="true"></i></small>
+                </a>
+              </div>
+            </h4>
+
+            <img src={this.generateGravatarSrc()} width="64" />
+          </div>
+
+          <div className="col-md-4 col-sm-7">
+            <h4>
+              <div className="radio radio-primary">
+                <input
+                  type="radio"
+                  id="radioUploadPicture"
+                  form="formImageType"
+                  name="imagetypeForm[isGravatarEnabled]"
+                  checked={!personalContainer.state.isGravatarEnabled}
+                  onChange={() => { personalContainer.changeIsGravatarEnabled(false) }}
+                />
+                <label htmlFor="radioUploadPicture">
+                  { t('Upload Image') }
+                </label>
+              </div>
+            </h4>
+            <div className="form-group">
+              <div id="pictureUploadFormMessage"></div>
+              <label className="col-sm-4 control-label">
+                { t('Current Image') }
+              </label>
+              {/* TDOO GW-1198 uproad profile image */}
+            </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} disabled={personalContainer.state.retrieveError != null}>
+              {t('Update')}
+            </button>
+          </div>
+        </div>
+
+      </React.Fragment>
+    );
+  }
+
+}
+
+
+const ProfileImageSettingsWrapper = (props) => {
+  return createSubscribedElement(ProfileImageSettings, props, [AppContainer, PersonalContainer]);
+};
+
+ProfileImageSettings.propTypes = {
+  t: PropTypes.func.isRequired, // i18next
+  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
+  personalContainer: PropTypes.instanceOf(PersonalContainer).isRequired,
+};
+
+export default withTranslation()(ProfileImageSettingsWrapper);

+ 2 - 1
src/client/js/components/Me/UserSettings.jsx

@@ -4,6 +4,7 @@ import PropTypes from 'prop-types';
 import { withTranslation } from 'react-i18next';
 
 import BasicInfoSettings from './BasicInfoSettings';
+import ProfileImageSettings from './ProfileImageSettings';
 
 class UserSettings extends React.Component {
 
@@ -19,7 +20,7 @@ class UserSettings extends React.Component {
 
         <div className="mb-5 container-fluid">
           <h2 className="border-bottom">{t('Set Profile Image')}</h2>
-          {/* TODO GW-1032 create component */}
+          <ProfileImageSettings />
         </div>
 
       </Fragment>

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

@@ -23,6 +23,7 @@ export default class PersonalContainer extends Container {
       registrationWhiteList: this.appContainer.getConfig().registrationWhiteList,
       isEmailPublished: false,
       lang: 'en-US',
+      isGravatarEnabled: false,
       externalAccounts: [],
       isPasswordSet: false,
       apiToken: '',
@@ -50,6 +51,7 @@ export default class PersonalContainer extends Container {
         email: currentUser.email,
         isEmailPublished: currentUser.isEmailPublished,
         lang: currentUser.lang,
+        isGravatarEnabled: currentUser.isGravatarEnabled,
         isPasswordSet: (currentUser.password != null),
         apiToken: currentUser.apiToken,
       });
@@ -106,6 +108,13 @@ export default class PersonalContainer extends Container {
     this.setState({ lang });
   }
 
+  /**
+   * Change isGravatarEnabled
+   */
+  changeIsGravatarEnabled(boolean) {
+    this.setState({ isGravatarEnabled: boolean });
+  }
+
   /**
    * Update basic info
    * @memberOf PersonalContainer
@@ -135,4 +144,25 @@ export default class PersonalContainer extends Container {
     }
   }
 
+  /**
+   * Update profile image
+   * @memberOf PersonalContainer
+   */
+  async updateProfileImage() {
+    try {
+      const response = await this.appContainer.apiv3.put('/personal-setting/image-type', {
+        isGravatarEnabled: this.state.isGravatarEnabled,
+      });
+      const { userData } = response.data;
+      this.setState({
+        isGravatarEnabled: userData.isGravatarEnabled,
+      });
+    }
+    catch (err) {
+      this.setState({ retrieveError: err });
+      logger.error(err);
+      throw new Error('Failed to update profile image');
+    }
+  }
+
 }

+ 0 - 6
src/server/form/index.js

@@ -4,12 +4,6 @@ module.exports = {
   invited: require('./invited'),
   revision: require('./revision'),
   comment: require('./comment'),
-  me: {
-    user: require('./me/user'),
-    password: require('./me/password'),
-    imagetype: require('./me/imagetype'),
-    apiToken: require('./me/apiToken'),
-  },
   admin: {
     securityGeneral: require('./admin/securityGeneral'),
     securityPassportLocal: require('./admin/securityPassportLocal'),

+ 0 - 7
src/server/form/me/apiToken.js

@@ -1,7 +0,0 @@
-const form = require('express-form');
-
-const field = form.field;
-
-module.exports = form(
-  field('apiTokenForm.confirm').required(),
-);

+ 0 - 7
src/server/form/me/imagetype.js

@@ -1,7 +0,0 @@
-const form = require('express-form');
-
-const field = form.field;
-
-module.exports = form(
-  field('imagetypeForm.isGravatarEnabled').required(),
-);

+ 0 - 9
src/server/form/me/password.js

@@ -1,9 +0,0 @@
-const form = require('express-form');
-
-const field = form.field;
-
-module.exports = form(
-  field('mePassword.oldPassword'),
-  field('mePassword.newPassword').required().is(/^[\x20-\x7F]{6,}$/),
-  field('mePassword.newPasswordConfirm').required(),
-);

+ 0 - 10
src/server/form/me/user.js

@@ -1,10 +0,0 @@
-const form = require('express-form');
-
-const field = form.field;
-
-module.exports = form(
-  field('userForm.name').trim().required(),
-  field('userForm.email').trim().isEmail().required(),
-  field('userForm.lang').required(),
-  field('userForm.isEmailPublished').trim().toBooleanStrict().required(),
-);

+ 9 - 25
src/server/models/user.js

@@ -188,25 +188,16 @@ module.exports = function(crowi) {
     });
   };
 
-  userSchema.methods.updateIsGravatarEnabled = function(isGravatarEnabled, callback) {
+  userSchema.methods.updateIsGravatarEnabled = async function(isGravatarEnabled) {
     this.isGravatarEnabled = isGravatarEnabled;
-    this.save((err, userData) => {
-      return callback(err, userData);
-    });
-  };
-
-  userSchema.methods.updateIsEmailPublished = function(isEmailPublished, callback) {
-    this.isEmailPublished = isEmailPublished;
-    this.save((err, userData) => {
-      return callback(err, userData);
-    });
+    const userData = await this.save();
+    return userData;
   };
 
-  userSchema.methods.updatePassword = function(password, callback) {
+  userSchema.methods.updatePassword = async function(password) {
     this.setPassword(password);
-    this.save((err, userData) => {
-      return callback(err, userData);
-    });
+    const userData = await this.save();
+    return userData;
   };
 
   userSchema.methods.canDeleteCompletely = function(creatorId) {
@@ -224,19 +215,12 @@ module.exports = function(crowi) {
     return false;
   };
 
-  userSchema.methods.updateApiToken = function(callback) {
+  userSchema.methods.updateApiToken = async function() {
     const self = this;
 
     self.apiToken = generateApiToken(this);
-    return new Promise(((resolve, reject) => {
-      self.save((err, userData) => {
-        if (err) {
-          return reject(err);
-        }
-
-        return resolve(userData);
-      });
-    }));
+    const userData = await self.save();
+    return userData;
   };
 
   userSchema.methods.updateImage = async function(attachment) {

+ 36 - 0
src/server/routes/apiv3/personal-setting.js

@@ -61,6 +61,9 @@ module.exports = (crowi) => {
       body('lang').isString().isIn(['en-US', 'ja']),
       body('isEmailPublished').isBoolean(),
     ],
+    imageType: [
+      body('isGravatarEnabled').isBoolean(),
+    ],
     password: [
       body('oldPassword').isString(),
       body('newPassword').isString().not().isEmpty()
@@ -144,6 +147,39 @@ module.exports = (crowi) => {
 
   });
 
+  /**
+   * @swagger
+   *
+   *    /personal-setting/image-type:
+   *      put:
+   *        tags: [PersonalSetting]
+   *        operationId: putUserImageType
+   *        summary: /personal-setting/image-type
+   *        description: Update user image type
+   *        responses:
+   *          200:
+   *            description: succeded to update user image type
+   *            content:
+   *              application/json:
+   *                schema:
+   *                  properties:
+   *                    userData:
+   *                      type: object
+   *                      description: user data
+   */
+  router.put('/image-type', accessTokenParser, loginRequiredStrictly, csrf, validator.imageType, ApiV3FormValidator, async(req, res) => {
+    const { isGravatarEnabled } = req.body;
+
+    try {
+      const userData = await req.user.updateIsGravatarEnabled(isGravatarEnabled);
+      return res.apiv3({ userData });
+    }
+    catch (err) {
+      logger.error(err);
+      return res.apiv3Err('update-personal-settings-failed');
+    }
+  });
+
   /**
    * @swagger
    *

+ 0 - 3
src/server/routes/index.js

@@ -128,14 +128,11 @@ module.exports = function(crowi, app) {
   app.get('/admin/export/:fileName'             , loginRequiredStrictly , adminRequired ,admin.export.download);
 
   app.get('/me'                       , loginRequiredStrictly , me.index);
-  app.post('/me'                      , loginRequiredStrictly , csrf , form.me.user , me.index);
   // external-accounts
   app.get('/me/external-accounts'                         , loginRequiredStrictly , me.externalAccounts.list);
   app.post('/me/external-accounts/disassociate'           , loginRequiredStrictly , me.externalAccounts.disassociate);
   app.post('/me/external-accounts/associateLdap'          , loginRequiredStrictly , form.login , me.externalAccounts.associateLdap);
 
-  app.post('/me/imagetype'            , form.me.imagetype         , loginRequiredStrictly , me.imagetype);
-
   app.get('/:id([0-9a-z]{24})'       , loginRequired , page.redirector);
   app.get('/_r/:id([0-9a-z]{24})'    , loginRequired , page.redirector); // alias
   app.get('/attachment/:pageId/:fileName'  , loginRequired, attachment.api.obsoletedGetForMongoDB); // DEPRECATED: remains for backward compatibility for v3.3.x or below

+ 0 - 32
src/server/routes/me.js

@@ -106,38 +106,6 @@ module.exports = function(crowi, app) {
     return res.render('me/index');
   };
 
-  actions.imagetype = function(req, res) {
-    if (req.method !== 'POST') {
-      // do nothing
-      return;
-    }
-    if (!req.form.isValid) {
-      req.flash('errorMessage', req.form.errors.join('\n'));
-      return;
-    }
-
-    const imagetypeForm = req.body.imagetypeForm;
-    const userData = req.user;
-
-    const isGravatarEnabled = imagetypeForm.isGravatarEnabled;
-
-    userData.updateIsGravatarEnabled(isGravatarEnabled, (err, userData) => {
-      if (err) {
-        /* eslint-disable no-restricted-syntax, no-prototype-builtins */
-        for (const e in err.errors) {
-          if (err.errors.hasOwnProperty(e)) {
-            req.form.errors.push(err.errors[e].message);
-          }
-        }
-        /* eslint-enable no-restricted-syntax, no-prototype-builtins */
-        return res.render('me/index', {});
-      }
-
-      req.flash('successMessage', req.t('Updated'));
-      return res.redirect('/me');
-    });
-  };
-
   actions.externalAccounts = {};
   actions.externalAccounts.list = function(req, res) {
     const userData = req.user;

+ 0 - 87
src/server/views/me/api_token.html

@@ -1,87 +0,0 @@
-{% extends '../layout-growi/base/layout.html' %}
-
-
-{% block html_title %}{{ customizeService.generateCustomTitle(t('API Settings')) }}{% endblock %}
-
-
-{% block content_header %}
-<div class="header-wrap">
-  <header id="page-header">
-  <h1 id="admin-title" class="title">{{ t('API Settings') }}</h1>
-  </header>
-</div>
-{% endblock %}
-
-{% block content_main %}
-<div class="content-main">
-
-  <ul class="nav nav-tabs">
-    <li><a href="/me"><i class="icon-user"></i> {{ t('User Information') }}</a></li>
-    <li><a href="/me/external-accounts"><i class="icon-share-alt"></i> {{ t('External Accounts') }}</a></li>
-    <li><a href="/me/password"><i class="icon-lock"></i> {{ t('Password Settings') }}</a></li>
-    <li class="active"><a href="/me/apiToken"><i class="icon-paper-plane"></i> {{ t('API Settings') }}</a></li>
-  </ul>
-
-  <div class="tab-content">
-
-  {% set message = req.flash('successMessage') %}
-  {% if message.length %}
-  <div class="alert alert-success m-t-10">
-    {{ message }}
-  </div>
-  {% endif %}
-
-  {% if req.form.errors.length > 0 %}
-  <div class="alert alert-danger m-t-10">
-    <ul>
-    {% for error in req.form.errors %}
-      <li>{{ error }}</li>
-    {% endfor %}
-    </ul>
-  </div>
-  {% endif %}
-
-  <div class="form-box m-t-20">
-
-    <form action="/me/apiToken" method="post" class="form-horizontal" role="form">
-    <fieldset>
-      <legend>{{ t('API Token Settings') }}</legend>
-      <div class="form-group {% if not user.password %}has-error{% endif %}">
-        <label for="" class="col-xs-3 control-label">{{ t('Current API Token') }}</label>
-        <div class="col-xs-6">
-          {% if user.apiToken %}
-            <input class="form-control" type="text" value="{{ user.apiToken }}">
-          {% else %}
-          <p class="form-control-static">
-            {{ t('page_me_apitoken.notice.apitoken_issued') }}
-          </p>
-          {% endif %}
-        </div>
-      </div>
-
-      <div class="form-group">
-        <div class="col-xs-offset-3 col-xs-9">
-
-          <p class="alert alert-warning">
-            {{ t('page_me_apitoken.notice.update_token1') }}<br>
-            {{ t('page_me_apitoken.notice.update_token2') }}
-          </p>
-
-          <button type="submit" value="1" name="apiTokenForm[confirm]" class="btn btn-primary">{{ t('Update API Token') }}</button>
-        </div>
-      </div>
-
-    </fieldset>
-    </form>
-  </div>
-
-
-  </div>
-</div>
-{% endblock content_main %}
-
-{% block content_footer %}
-{% endblock %}
-
-{% block layout_footer %}
-{% endblock %}