Procházet zdrojové kódy

Merge branch 'adjust-admin-usrs' into reactify-admin/markDownSettings

itizawa před 6 roky
rodič
revize
ed0e030729

+ 2 - 1
CHANGES.md

@@ -2,7 +2,8 @@
 
 ## 3.5.17-RC
 
-* 
+* Fix: Use HTTP PlantUML URL in default
+    * Introduced by 3.5.12
 
 ## 3.5.16
 

+ 6 - 1
resource/locales/en-US/translation.json

@@ -714,7 +714,12 @@
     "cannot_invite_maximum_users": "Can not invite more than the maximum number of users.",
     "current_users": "Current users:",
     "valid_email": "Valid email address is required",
-    "existing_email": "The following emails already exist"
+    "existing_email": "The following emails already exist",
+    "give_user_admin": "Give {{username}} admin success",
+    "remove_user_admin": "Remove {{username}} admin success",
+    "activate_user_success": "Activating {{username}} success",
+    "deactivate_user_success": "Deactivating {{username}} success",
+    "remove_user_success": "Removing {{username}} success"
   },
 
   "user_group_management": {

+ 6 - 1
resource/locales/ja/translation.json

@@ -698,7 +698,12 @@
     "cannot_invite_maximum_users": "ユーザーが上限に達したため招待できません。",
     "current_users": "現在のユーザー数:",
     "valid_email": "メールアドレスを入力してください。",
-    "existing_email": "以下のEmailはすでに存在しています。"
+    "existing_email": "以下のEmailはすでに存在しています。",
+    "give_user_admin": "{{username}}を管理者に設定しました",
+    "remove_user_admin": "{{username}}を管理者から外しました",
+    "activate_user_success": "{{username}}を有効化しました",
+    "deactivate_user_success": "{{username}}を無効化しました",
+    "remove_user_success": "{{username}}を削除しました"
   },
 
   "user_group_management": {

+ 57 - 0
src/client/js/components/Admin/Users/GiveAdminButton.jsx

@@ -0,0 +1,57 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { withTranslation } from 'react-i18next';
+
+import { createSubscribedElement } from '../../UnstatedUtils';
+import AppContainer from '../../../services/AppContainer';
+import { toastSuccess, toastError } from '../../../util/apiNotification';
+import AdminUsersContainer from '../../../services/AdminUsersContainer';
+
+class GiveAdminButton extends React.Component {
+
+  constructor(props) {
+    super(props);
+
+    this.onClickGiveAdminBtn = this.onClickGiveAdminBtn.bind(this);
+  }
+
+  async onClickGiveAdminBtn() {
+    const { t } = this.props;
+
+    try {
+      const username = await this.props.adminUsersContainer.giveUserAdmin(this.props.user._id);
+      toastSuccess(t('user_management.give_user_admin', { username }));
+    }
+    catch (err) {
+      toastError(err);
+    }
+  }
+
+  render() {
+    const { t } = this.props;
+
+    return (
+      <a className="px-4" onClick={() => { this.onClickGiveAdminBtn() }}>
+        <i className="icon-fw icon-user-following"></i> { t('user_management.give_admin_access') }
+      </a>
+    );
+  }
+
+}
+
+/**
+ * Wrapper component for using unstated
+ */
+const GiveAdminButtonWrapper = (props) => {
+  return createSubscribedElement(GiveAdminButton, props, [AppContainer, AdminUsersContainer]);
+};
+
+GiveAdminButton.propTypes = {
+  t: PropTypes.func.isRequired, // i18next
+  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
+  adminUsersContainer: PropTypes.instanceOf(AdminUsersContainer).isRequired,
+
+  user: PropTypes.object.isRequired,
+};
+
+export default withTranslation()(GiveAdminButtonWrapper);

+ 0 - 56
src/client/js/components/Admin/Users/GiveAdminForm.jsx

@@ -1,56 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import { withTranslation } from 'react-i18next';
-
-import { createSubscribedElement } from '../../UnstatedUtils';
-import AppContainer from '../../../services/AppContainer';
-
-class AdminMenuForm extends React.Component {
-
-  constructor(props) {
-    super(props);
-
-    this.state = {
-
-    };
-
-    this.handleSubmit = this.handleSubmit.bind(this);
-  }
-
-  // これは将来的にapiにするので。あとボタンにするとデザインがよくなかったので。
-  handleSubmit(event) {
-    $(event.currentTarget).parent().submit();
-  }
-
-  render() {
-    const { t, appContainer, user } = this.props;
-
-    return (
-      <a className="px-4">
-        <form action={`/admin/user/${user._id}/makeAdmin`} method="post">
-          <input type="hidden" name="_csrf" value={appContainer.csrfToken} />
-          <span onClick={this.handleSubmit}>
-            <i className="icon-fw icon-magic-wand"></i>{ t('user_management.give_admin_access') }
-          </span>
-        </form>
-      </a>
-    );
-  }
-
-}
-
-/**
- * Wrapper component for using unstated
- */
-const AdminMenuFormWrapper = (props) => {
-  return createSubscribedElement(AdminMenuForm, props, [AppContainer]);
-};
-
-AdminMenuForm.propTypes = {
-  t: PropTypes.func.isRequired, // i18next
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
-
-  user: PropTypes.object.isRequired,
-};
-
-export default withTranslation()(AdminMenuFormWrapper);

+ 81 - 0
src/client/js/components/Admin/Users/RemoveAdminButton.jsx

@@ -0,0 +1,81 @@
+import React, { Fragment } from 'react';
+import PropTypes from 'prop-types';
+import { withTranslation } from 'react-i18next';
+
+import { createSubscribedElement } from '../../UnstatedUtils';
+import AppContainer from '../../../services/AppContainer';
+import { toastSuccess, toastError } from '../../../util/apiNotification';
+import AdminUsersContainer from '../../../services/AdminUsersContainer';
+
+class RemoveAdminButton extends React.Component {
+
+  constructor(props) {
+    super(props);
+
+    this.onClickRemoveAdminBtn = this.onClickRemoveAdminBtn.bind(this);
+  }
+
+  async onClickRemoveAdminBtn() {
+    const { t } = this.props;
+
+    try {
+      const username = await this.props.adminUsersContainer.removeUserAdmin(this.props.user._id);
+      toastSuccess(t('user_management.remove_user_admin', { username }));
+    }
+    catch (err) {
+      toastError(err);
+    }
+  }
+
+
+  renderRemoveAdminBtn() {
+    const { t } = this.props;
+
+    return (
+      <a className="px-4" onClick={() => { this.onClickRemoveAdminBtn() }}>
+        <i className="icon-fw icon-user-unfollow"></i> { t('user_management.remove_admin_access') }
+      </a>
+    );
+  }
+
+  renderRemoveAdminAlert() {
+    const { t } = this.props;
+
+    return (
+      <div className="px-4">
+        <i className="icon-fw icon-user-unfollow mb-2"></i>{ t('user_management.remove_admin_access') }
+        <p className="alert alert-danger">{ t('user_management.cannot_remove') }</p>
+      </div>
+    );
+  }
+
+  render() {
+    const { user } = this.props;
+    const me = this.props.appContainer.me;
+
+    return (
+      <Fragment>
+        {user.username !== me ? this.renderRemoveAdminBtn()
+          : this.renderRemoveAdminAlert()}
+      </Fragment>
+    );
+  }
+
+}
+
+/**
+* Wrapper component for using unstated
+*/
+const RemoveAdminButtonWrapper = (props) => {
+  return createSubscribedElement(RemoveAdminButton, props, [AppContainer, AdminUsersContainer]);
+};
+
+RemoveAdminButton.propTypes = {
+  t: PropTypes.func.isRequired, // i18next
+  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
+  adminUsersContainer: PropTypes.instanceOf(AdminUsersContainer).isRequired,
+
+  user: PropTypes.object.isRequired,
+};
+
+export default withTranslation()(RemoveAdminButtonWrapper);

+ 0 - 68
src/client/js/components/Admin/Users/RemoveAdminForm.jsx

@@ -1,68 +0,0 @@
-import React, { Fragment } from 'react';
-import PropTypes from 'prop-types';
-import { withTranslation } from 'react-i18next';
-
-import { createSubscribedElement } from '../../UnstatedUtils';
-import AppContainer from '../../../services/AppContainer';
-
-class RemoveAdminForm extends React.Component {
-
-  constructor(props) {
-    super(props);
-
-    this.state = {
-    };
-
-    this.handleSubmit = this.handleSubmit.bind(this);
-  }
-
-  // これは将来的にapiにするので。あとボタンにするとデザインがよくなかったので。
-  handleSubmit(event) {
-    $(event.currentTarget).parent().submit();
-  }
-
-  render() {
-    const { t, user } = this.props;
-    const me = this.props.appContainer.me;
-
-    return (
-      <Fragment>
-        {user.username !== me
-          ? (
-            <a>
-              <form action={`/admin/user/${user._id}/removeFromAdmin`} method="post">
-                <input type="hidden" />
-                <span onClick={this.handleSubmit}>
-                  <i className="icon-fw icon-user-unfollow mb-2"></i>{ t('user_management.remove_admin_access') }
-                </span>
-              </form>
-            </a>
-          )
-          : (
-            <div className="px-4">
-              <i className="icon-fw icon-user-unfollow mb-2"></i>{ t('user_management.remove_admin_access') }
-              <p className="alert alert-danger">{ t('user_management.cannot_remove') }</p>
-            </div>
-          )
-        }
-      </Fragment>
-    );
-  }
-
-}
-
-/**
-* Wrapper component for using unstated
-*/
-const RemoveAdminFormWrapper = (props) => {
-  return createSubscribedElement(RemoveAdminForm, props, [AppContainer]);
-};
-
-RemoveAdminForm.propTypes = {
-  t: PropTypes.func.isRequired, // i18next
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
-
-  user: PropTypes.object.isRequired,
-};
-
-export default withTranslation()(RemoveAdminFormWrapper);

+ 57 - 0
src/client/js/components/Admin/Users/StatusActivateButton.jsx

@@ -0,0 +1,57 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { withTranslation } from 'react-i18next';
+
+import { createSubscribedElement } from '../../UnstatedUtils';
+import AppContainer from '../../../services/AppContainer';
+import AdminUsersContainer from '../../../services/AdminUsersContainer';
+import { toastSuccess, toastError } from '../../../util/apiNotification';
+
+class StatusActivateButton extends React.Component {
+
+  constructor(props) {
+    super(props);
+
+    this.onClickAcceptBtn = this.onClickAcceptBtn.bind(this);
+  }
+
+  async onClickAcceptBtn() {
+    const { t } = this.props;
+
+    try {
+      const username = await this.props.adminUsersContainer.activateUser(this.props.user._id);
+      toastSuccess(t('user_management.activate_user_success', { username }));
+    }
+    catch (err) {
+      toastError(err);
+    }
+  }
+
+  render() {
+    const { t } = this.props;
+
+    return (
+      <a className="px-4" onClick={() => { this.onClickAcceptBtn() }}>
+        <i className="icon-fw icon-user-following"></i> { t('user_management.accept') }
+      </a>
+    );
+  }
+
+}
+
+/**
+ * Wrapper component for using unstated
+ */
+const StatusActivateFormWrapper = (props) => {
+  return createSubscribedElement(StatusActivateButton, props, [AppContainer, AdminUsersContainer]);
+};
+
+StatusActivateButton.propTypes = {
+  t: PropTypes.func.isRequired, // i18next
+  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
+  adminUsersContainer: PropTypes.instanceOf(AdminUsersContainer).isRequired,
+
+  user: PropTypes.object.isRequired,
+};
+
+export default withTranslation()(StatusActivateFormWrapper);

+ 0 - 72
src/client/js/components/Admin/Users/StatusActivateForm.jsx

@@ -1,72 +0,0 @@
-import React, { Fragment } from 'react';
-import PropTypes from 'prop-types';
-import { withTranslation } from 'react-i18next';
-
-import { createSubscribedElement } from '../../UnstatedUtils';
-import AppContainer from '../../../services/AppContainer';
-
-class StatusActivateForm extends React.Component {
-
-  constructor(props) {
-    super(props);
-
-    this.state = {
-
-    };
-
-    this.handleSubmit = this.handleSubmit.bind(this);
-  }
-
-  // これは将来的にapiにするので。あとボタンにするとデザインがよくなかったので。
-  handleSubmit(event) {
-    $(event.currentTarget).parent().submit();
-  }
-
-  render() {
-    const { t, user, appContainer } = this.props;
-
-    return (
-      <Fragment>
-        {user.status === 1
-          ? (
-            <a>
-              <form action={`/admin/user/${user._id}/activate`} method="post">
-                <input type="hidden" name="_csrf" value={appContainer.csrfToken} />
-                <span onClick={this.handleSubmit}>
-                  <i className="icon-fw icon-user-following"></i> { t('user_management.accept') }
-                </span>
-              </form>
-            </a>
-          )
-          : (
-            <a className="px-4">
-              <form action={`/admin/user/${user._id}/activate`} method="post">
-                <input type="hidden" />
-                <span onClick={this.handleSubmit}>
-                  <i className="icon-fw icon-user-following"></i> { t('user_management.accept') }
-                </span>
-              </form>
-            </a>
-          )
-        }
-      </Fragment>
-    );
-  }
-
-}
-
-/**
- * Wrapper component for using unstated
- */
-const StatusActivateFormWrapper = (props) => {
-  return createSubscribedElement(StatusActivateForm, props, [AppContainer]);
-};
-
-StatusActivateForm.propTypes = {
-  t: PropTypes.func.isRequired, // i18next
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
-
-  user: PropTypes.object.isRequired,
-};
-
-export default withTranslation()(StatusActivateFormWrapper);

+ 80 - 0
src/client/js/components/Admin/Users/StatusSuspendedButton.jsx

@@ -0,0 +1,80 @@
+import React, { Fragment } from 'react';
+import PropTypes from 'prop-types';
+import { withTranslation } from 'react-i18next';
+
+import { createSubscribedElement } from '../../UnstatedUtils';
+import AppContainer from '../../../services/AppContainer';
+import { toastSuccess, toastError } from '../../../util/apiNotification';
+import AdminUsersContainer from '../../../services/AdminUsersContainer';
+
+class StatusSuspendedButton extends React.Component {
+
+  constructor(props) {
+    super(props);
+
+    this.onClickDeactiveBtn = this.onClickDeactiveBtn.bind(this);
+  }
+
+  async onClickDeactiveBtn() {
+    const { t } = this.props;
+
+    try {
+      const username = await this.props.adminUsersContainer.deactivateUser(this.props.user._id);
+      toastSuccess(t('user_management.deactivate_user_success', { username }));
+    }
+    catch (err) {
+      toastError(err);
+    }
+  }
+
+  renderSuspendedBtn() {
+    const { t } = this.props;
+
+    return (
+      <a className="px-4" onClick={() => { this.onClickDeactiveBtn() }}>
+        <i className="icon-fw icon-ban"></i> { t('user_management.deactivate_account') }
+      </a>
+    );
+  }
+
+  renderSuspendedAlert() {
+    const { t } = this.props;
+
+    return (
+      <div className="px-4">
+        <i className="icon-fw icon-ban mb-2"></i>{ t('user_management.deactivate_account') }
+        <p className="alert alert-danger">{ t('user_management.your_own') }</p>
+      </div>
+    );
+  }
+
+  render() {
+    const { user } = this.props;
+    const me = this.props.appContainer.me;
+
+    return (
+      <Fragment>
+        {user.username !== me ? this.renderSuspendedBtn()
+          : this.renderSuspendedAlert()}
+      </Fragment>
+    );
+  }
+
+}
+
+/**
+ * Wrapper component for using unstated
+ */
+const StatusSuspendedFormWrapper = (props) => {
+  return createSubscribedElement(StatusSuspendedButton, props, [AppContainer, AdminUsersContainer]);
+};
+
+StatusSuspendedButton.propTypes = {
+  t: PropTypes.func.isRequired, // i18next
+  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
+  adminUsersContainer: PropTypes.instanceOf(AdminUsersContainer).isRequired,
+
+  user: PropTypes.object.isRequired,
+};
+
+export default withTranslation()(StatusSuspendedFormWrapper);

+ 0 - 69
src/client/js/components/Admin/Users/StatusSuspendedForm.jsx

@@ -1,69 +0,0 @@
-import React, { Fragment } from 'react';
-import PropTypes from 'prop-types';
-import { withTranslation } from 'react-i18next';
-
-import { createSubscribedElement } from '../../UnstatedUtils';
-import AppContainer from '../../../services/AppContainer';
-
-class StatusSuspendedForm extends React.Component {
-
-  constructor(props) {
-    super(props);
-
-    this.state = {
-
-    };
-
-    this.handleSubmit = this.handleSubmit.bind(this);
-  }
-
-  // これは将来的にapiにするので。あとボタンにするとデザインがよくなかったので。
-  handleSubmit(event) {
-    $(event.currentTarget).parent().submit();
-  }
-
-  render() {
-    const { t, user } = this.props;
-    const me = this.props.appContainer.me;
-
-    return (
-      <Fragment>
-        {user.username !== me
-          ? (
-            <a>
-              <form action={`/admin/user/${user._id}/suspend`} method="post">
-                <input type="hidden" name="_csrf" value={this.props.appContainer.csrfToken} />
-                <span onClick={this.handleSubmit}>
-                  <i className="icon-fw icon-ban"></i>{ t('user_management.deactivate_account') }
-                </span>
-              </form>
-            </a>
-          )
-          : (
-            <div className="px-4">
-              <i className="icon-fw icon-ban mb-2"></i>{ t('user_management.deactivate_account') }
-              <p className="alert alert-danger">{ t('user_management.your_own') }</p>
-            </div>
-          )
-        }
-      </Fragment>
-    );
-  }
-
-}
-
-/**
- * Wrapper component for using unstated
- */
-const StatusSuspendedFormWrapper = (props) => {
-  return createSubscribedElement(StatusSuspendedForm, props, [AppContainer]);
-};
-
-StatusSuspendedForm.propTypes = {
-  t: PropTypes.func.isRequired, // i18next
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
-
-  user: PropTypes.object.isRequired,
-};
-
-export default withTranslation()(StatusSuspendedFormWrapper);

+ 10 - 15
src/client/js/components/Admin/Users/UserInviteModal.jsx

@@ -76,14 +76,12 @@ class UserInviteModal extends React.Component {
 
     return (
       <>
-        <label className="mr-3 text-left" style={{ flex: 1 }}>
-          <input
-            type="checkbox"
-            defaultChecked={this.state.sendEmail}
-            onChange={this.handleCheckBox}
-          />
-          <span className="ml-2">{ t('user_management.invite_thru_email') }</span>
-        </label>
+        <div className="checkbox checkbox-success text-left" onChange={this.handleCheckBox} style={{ flex: 1 }}>
+          <input type="checkbox" id="sendEmail" className="form-check-input" name="sendEmail" defaultChecked={this.state.sendEmail} />
+          <label htmlFor="sendEmail">
+            { t('user_management.invite_thru_email') }
+          </label>
+        </div>
         <div>
           <Button bsStyle="danger" className="fcbtn btn btn-xs btn-danger btn-outline btn-rounded" onClick={this.onToggleModal}>
           Cancel
@@ -126,7 +124,7 @@ class UserInviteModal extends React.Component {
         {userList.map((user) => {
           const copyText = `Email:${user.email} Password:${user.password} `;
           return (
-            <CopyToClipboard text={copyText} onCopy={this.showToaster}>
+            <CopyToClipboard key={user.email} text={copyText} onCopy={this.showToaster}>
               <li key={user.email} className="btn">Email: <strong className="mr-3">{user.email}</strong> Password: <strong>{user.password}</strong></li>
             </CopyToClipboard>
           );
@@ -157,19 +155,16 @@ class UserInviteModal extends React.Component {
   }
 
   async handleSubmit() {
-    const { appContainer } = this.props;
+    const { adminUsersContainer } = this.props;
 
     const array = this.state.emailInputValue.split('\n');
     const emailList = array.filter((element) => { return element.match(/.+@.+\..+/) });
     const shapedEmailList = emailList.map((email) => { return email.trim() });
 
     try {
-      const response = await appContainer.apiv3.post('/users/invite', {
-        shapedEmailList,
-        sendEmail: this.state.sendEmail,
-      });
+      const emailList = await adminUsersContainer.createUserInvited(shapedEmailList, this.state.sendEmail);
       this.setState({ emailInputValue: '' });
-      this.setState({ invitedEmailList: response.data.emailList });
+      this.setState({ invitedEmailList: emailList });
       toastSuccess('Inviting user success');
     }
     catch (err) {

+ 54 - 24
src/client/js/components/Admin/Users/UserMenu.jsx

@@ -2,11 +2,11 @@ import React, { Fragment } from 'react';
 import PropTypes from 'prop-types';
 import { withTranslation } from 'react-i18next';
 
-import StatusActivateForm from './StatusActivateForm';
-import StatusSuspendedForm from './StatusSuspendedForm';
+import StatusActivateButton from './StatusActivateButton';
+import StatusSuspendedButton from './StatusSuspendedButton';
 import RemoveUserButton from './UserRemoveButton';
-import RemoveAdminForm from './RemoveAdminForm';
-import GiveAdminForm from './GiveAdminForm';
+import RemoveAdminButton from './RemoveAdminButton';
+import GiveAdminButton from './GiveAdminButton';
 
 import { createSubscribedElement } from '../../UnstatedUtils';
 import AppContainer from '../../../services/AppContainer';
@@ -28,9 +28,55 @@ class UserMenu extends React.Component {
     this.props.adminUsersContainer.showPasswordResetModal(this.props.user);
   }
 
-  render() {
+  renderEditMenu() {
+    const { t } = this.props;
+
+    return (
+      <Fragment>
+        <li className="dropdown-header">{ t('user_management.edit_menu') }</li>
+        <li onClick={this.onPasswordResetClicked}>
+          <a>
+            <i className="icon-fw icon-key"></i>{ t('user_management.reset_password') }
+          </a>
+        </li>
+      </Fragment>
+    );
+  }
+
+  renderStatusMenu() {
+    const { t, user } = this.props;
+
+    return (
+      <Fragment>
+        <li className="divider"></li>
+        <li className="dropdown-header">{ t('status') }</li>
+        <li>
+          {(user.status === 1 || user.status === 3) && <StatusActivateButton user={user} />}
+          {user.status === 2 && <StatusSuspendedButton user={user} />}
+          {(user.status === 1 || user.status === 3 || user.status === 5) && <RemoveUserButton user={user} />}
+        </li>
+      </Fragment>
+    );
+  }
+
+  renderAdminMenu() {
     const { t, user } = this.props;
 
+    return (
+      <Fragment>
+        <li className="divider pl-0"></li>
+        <li className="dropdown-header">{ t('user_management.administrator_menu') }</li>
+        <li>
+          {user.admin === true && <RemoveAdminButton user={user} />}
+          {user.admin === false && <GiveAdminButton user={user} />}
+        </li>
+      </Fragment>
+    );
+  }
+
+  render() {
+    const { user } = this.props;
+
     return (
       <Fragment>
         <div className="btn-group admin-user-menu">
@@ -38,25 +84,9 @@ class UserMenu extends React.Component {
             <i className="icon-settings"></i> <span className="caret"></span>
           </button>
           <ul className="dropdown-menu" role="menu">
-            <li className="dropdown-header">{ t('user_management.edit_menu') }</li>
-            <li onClick={this.onPasswordResetClicked}>
-              <a>
-                <i className="icon-fw icon-key"></i>{ t('user_management.reset_password') }
-              </a>
-            </li>
-            <li className="divider"></li>
-            <li className="dropdown-header">{ t('status') }</li>
-            <li>
-              {(user.status === 1 || user.status === 3) && <StatusActivateForm user={user} />}
-              {user.status === 2 && <StatusSuspendedForm user={user} />}
-              {(user.status === 1 || user.status === 3 || user.status === 5) && <RemoveUserButton user={user} />}
-            </li>
-            <li className="divider pl-0"></li>
-            <li className="dropdown-header">{ t('user_management.administrator_menu') }</li>
-            <li>
-              {user.status === 2 && user.admin === true && <RemoveAdminForm user={user} />}
-              {user.status === 2 && user.admin === false && <GiveAdminForm user={user} />}
-            </li>
+            {this.renderEditMenu()}
+            {user.status !== 4 && this.renderStatusMenu()}
+            {user.status === 2 && this.renderAdminMenu()}
           </ul>
         </div>
       </Fragment>

+ 3 - 1
src/client/js/components/Admin/Users/UserRemoveButton.jsx

@@ -16,9 +16,11 @@ class UserRemoveButton extends React.Component {
   }
 
   async onClickDeleteBtn() {
+    const { t } = this.props;
+
     try {
       const username = await this.props.adminUsersContainer.removeUser(this.props.user._id);
-      toastSuccess(`Delete ${username} success`);
+      toastSuccess(t('user_management.remove_user_success', { username }));
     }
     catch (err) {
       toastError(err);

+ 15 - 8
src/client/js/components/Admin/Users/Users.jsx

@@ -8,6 +8,8 @@ import InviteUserControl from './InviteUserControl';
 import UserTable from './UserTable';
 
 import { createSubscribedElement } from '../../UnstatedUtils';
+import { toastError } from '../../../util/apiNotification';
+
 import AppContainer from '../../../services/AppContainer';
 import AdminUsersContainer from '../../../services/AdminUsersContainer';
 
@@ -16,11 +18,16 @@ class UserPage extends React.Component {
   constructor(props) {
     super();
 
-    this.state = {
-      activePage: 1,
-      pagingLimit: Infinity,
-    };
+    this.handlePage = this.handlePage.bind(this);
+  }
 
+  async handlePage(selectedPage) {
+    try {
+      await this.props.adminUsersContainer.retrieveUsersByPagingNum(selectedPage);
+    }
+    catch (err) {
+      toastError(err);
+    }
   }
 
   render() {
@@ -38,10 +45,10 @@ class UserPage extends React.Component {
         </p>
         <UserTable />
         <PaginationWrapper
-          activePage={this.state.activePage}
-          changePage={this.handlePage} // / TODO GW-314 create function
-          totalItemsCount={adminUsersContainer.state.users.length}
-          pagingLimit={this.state.pagingLimit}
+          activePage={adminUsersContainer.state.activePage}
+          changePage={this.handlePage}
+          totalItemsCount={adminUsersContainer.state.totalUsers}
+          pagingLimit={adminUsersContainer.state.pagingLimit}
         />
       </Fragment>
     );

+ 89 - 0
src/client/js/services/AdminUsersContainer.js

@@ -21,6 +21,9 @@ export default class AdminUsersContainer extends Container {
       isPasswordResetModalShown: false,
       isUserInviteModalShown: false,
       userForPasswordResetModal: null,
+      totalUsers: 0,
+      activePage: 1,
+      pagingLimit: Infinity,
     };
 
     this.showPasswordResetModal = this.showPasswordResetModal.bind(this);
@@ -35,6 +38,44 @@ export default class AdminUsersContainer extends Container {
     return 'AdminUsersContainer';
   }
 
+  /**
+   * syncUsers of selectedPage
+   * @memberOf AdminUsersContainer
+   * @param {number} selectedPage
+   */
+  async retrieveUsersByPagingNum(selectedPage) {
+
+    const params = { page: selectedPage };
+    const response = await this.appContainer.apiv3.get('/users', params);
+
+    const users = response.data.users;
+    const totalUsers = response.data.totalUsers;
+    const pagingLimit = response.data.pagingLimit;
+
+    this.setState({
+      users,
+      totalUsers,
+      pagingLimit,
+      activePage: selectedPage,
+    });
+
+  }
+
+  /**
+   * create user invited
+   * @memberOf AdminUsersContainer
+   * @param {object} shapedEmailList
+   * @param {bool} sendEmail
+   */
+  async createUserInvited(shapedEmailList, sendEmail) {
+    const response = await this.appContainer.apiv3.post('/users/invite', {
+      shapedEmailList,
+      sendEmail,
+    });
+    const { emailList } = response.data;
+    return emailList;
+  }
+
   /**
    * open reset password modal, and props user
    * @memberOf AdminUsersContainer
@@ -63,6 +104,54 @@ export default class AdminUsersContainer extends Container {
     await this.setState({ isUserInviteModalShown: !this.state.isUserInviteModalShown });
   }
 
+  /**
+   * Give user admin
+   * @memberOf AdminUsersContainer
+   * @param {string} userId
+   * @return {string} username
+   */
+  async giveUserAdmin(userId) {
+    const response = await this.appContainer.apiv3.put(`/users/${userId}/giveAdmin`);
+    const { username } = response.data.userData;
+    return username;
+  }
+
+  /**
+   * Remove user admin
+   * @memberOf AdminUsersContainer
+   * @param {string} userId
+   * @return {string} username
+   */
+  async removeUserAdmin(userId) {
+    const response = await this.appContainer.apiv3.put(`/users/${userId}/removeAdmin`);
+    const { username } = response.data.userData;
+    return username;
+  }
+
+  /**
+   * Activate user
+   * @memberOf AdminUsersContainer
+   * @param {string} userId
+   * @return {string} username
+   */
+  async activateUser(userId) {
+    const response = await this.appContainer.apiv3.put(`/users/${userId}/activate`);
+    const { username } = response.data.userData;
+    return username;
+  }
+
+  /**
+   * Deactivate user
+   * @memberOf AdminUsersContainer
+   * @param {string} userId
+   * @return {string} username
+   */
+  async deactivateUser(userId) {
+    const response = await this.appContainer.apiv3.put(`/users/${userId}/deactivate`);
+    const { username } = response.data.userData;
+    return username;
+  }
+
   /**
    * remove user
    * @memberOf AdminUsersContainer

+ 2 - 1
src/client/js/util/markdown-it/plantuml.js

@@ -7,7 +7,8 @@ export default class PlantUMLConfigurer {
     this.crowi = crowi;
     const config = crowi.getConfig();
 
-    this.serverUrl = config.env.PLANTUML_URI || 'https://plantuml.com/plantuml';
+    // Do NOT use HTTPS URL because plantuml.com refuse request except from members
+    this.serverUrl = config.env.PLANTUML_URI || 'http://plantuml.com/plantuml';
 
     this.generateSource = this.generateSource.bind(this);
   }

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

@@ -285,20 +285,16 @@ module.exports = function(crowi) {
     });
   };
 
-  userSchema.methods.removeFromAdmin = function(callback) {
+  userSchema.methods.removeFromAdmin = async function() {
     debug('Remove from admin', this);
     this.admin = 0;
-    this.save((err, userData) => {
-      return callback(err, userData);
-    });
+    return this.save();
   };
 
-  userSchema.methods.makeAdmin = function(callback) {
+  userSchema.methods.makeAdmin = async function() {
     debug('Admin', this);
     this.admin = 1;
-    this.save((err, userData) => {
-      return callback(err, userData);
-    });
+    return this.save();
   };
 
   userSchema.methods.asyncMakeAdmin = async function(callback) {
@@ -306,16 +302,14 @@ module.exports = function(crowi) {
     return this.save();
   };
 
-  userSchema.methods.statusActivate = function(callback) {
+  userSchema.methods.statusActivate = async function() {
     debug('Activate User', this);
     this.status = STATUS_ACTIVE;
-    this.save((err, userData) => {
-      userEvent.emit('activated', userData);
-      return callback(err, userData);
-    });
+    const userData = await this.save();
+    return userEvent.emit('activated', userData);
   };
 
-  userSchema.methods.statusSuspend = function(callback) {
+  userSchema.methods.statusSuspend = async function() {
     debug('Suspend User', this);
     this.status = STATUS_SUSPENDED;
     if (this.email === undefined || this.email === null) { // migrate old data
@@ -327,9 +321,7 @@ module.exports = function(crowi) {
     if (this.username === undefined || this.usename === null) { // migrate old data
       this.username = '-';
     }
-    this.save((err, userData) => {
-      return callback(err, userData);
-    });
+    return this.save();
   };
 
   userSchema.methods.statusDelete = async function() {

+ 0 - 72
src/server/routes/admin.js

@@ -439,78 +439,6 @@ module.exports = function(crowi, app) {
     });
   };
 
-  actions.user.makeAdmin = function(req, res) {
-    const id = req.params.id;
-    User.findById(id, (err, userData) => {
-      userData.makeAdmin((err, userData) => {
-        if (err === null) {
-          req.flash('successMessage', `${userData.name}さんのアカウントを管理者に設定しました。`);
-        }
-        else {
-          req.flash('errorMessage', '更新に失敗しました。');
-          debug(err, userData);
-        }
-        return res.redirect('/admin/users');
-      });
-    });
-  };
-
-  actions.user.removeFromAdmin = function(req, res) {
-    const id = req.params.id;
-    User.findById(id, (err, userData) => {
-      userData.removeFromAdmin((err, userData) => {
-        if (err === null) {
-          req.flash('successMessage', `${userData.name}さんのアカウントを管理者から外しました。`);
-        }
-        else {
-          req.flash('errorMessage', '更新に失敗しました。');
-          debug(err, userData);
-        }
-        return res.redirect('/admin/users');
-      });
-    });
-  };
-
-  actions.user.activate = async function(req, res) {
-    // check user upper limit
-    const isUserCountExceedsUpperLimit = await User.isUserCountExceedsUpperLimit();
-    if (isUserCountExceedsUpperLimit) {
-      req.flash('errorMessage', 'ユーザーが上限に達したため有効化できません。');
-      return res.redirect('/admin/users');
-    }
-
-    const id = req.params.id;
-    User.findById(id, (err, userData) => {
-      userData.statusActivate((err, userData) => {
-        if (err === null) {
-          req.flash('successMessage', `${userData.name}さんのアカウントを有効化しました`);
-        }
-        else {
-          req.flash('errorMessage', '更新に失敗しました。');
-          debug(err, userData);
-        }
-        return res.redirect('/admin/users');
-      });
-    });
-  };
-
-  actions.user.suspend = function(req, res) {
-    const id = req.params.id;
-
-    User.findById(id, (err, userData) => {
-      userData.statusSuspend((err, userData) => {
-        if (err === null) {
-          req.flash('successMessage', `${userData.name}さんのアカウントを利用停止にしました`);
-        }
-        else {
-          req.flash('errorMessage', '更新に失敗しました。');
-          debug(err, userData);
-        }
-        return res.redirect('/admin/users');
-      });
-    });
-  };
-
   // これやったときの relation の挙動未確認
   actions.user.removeCompletely = function(req, res) {
     // ユーザーの物理削除

+ 2 - 0
src/server/routes/apiv3/index.js

@@ -27,5 +27,7 @@ module.exports = (crowi) => {
 
   router.use('/import', require('./import')(crowi));
 
+  router.use('/statistics', require('./statistics')(crowi));
+
   return router;
 };

+ 93 - 0
src/server/routes/apiv3/statistics.js

@@ -0,0 +1,93 @@
+const loggerFactory = require('@alias/logger');
+
+const logger = loggerFactory('growi:routes:apiv3:healthcheck'); // eslint-disable-line no-unused-vars
+
+const express = require('express');
+
+const router = express.Router();
+
+const helmet = require('helmet');
+
+const util = require('util');
+
+const USER_STATUS_MASTER = {
+  1: 'registered',
+  2: 'active',
+  3: 'suspended',
+  4: 'deleted',
+  5: 'invited',
+};
+
+
+/**
+ * @swagger
+ *  tags:
+ *    name: Statistics
+ */
+module.exports = (crowi) => {
+
+  const models = crowi.models;
+  const User = models.User;
+
+  /**
+   * @swagger
+   *
+   *  /statistics/user:
+   *    get:
+   *      tags: [Statistics]
+   *      description: Get statistics for user
+   *      responses:
+   *        200:
+   *          description: Statistics for user
+   *          content:
+   *            application/json:
+   *              schema:
+   *                properties:
+   *                  data:
+   *                    type: object
+   *                    description: Statistics for all user
+   */
+  router.get('/user', helmet.noCache(), async(req, res) => {
+    const userCountGroupByStatus = await User.aggregate().group({
+      _id: '$status',
+      totalCount: { $sum: 1 },
+    });
+
+    // Initialize userCountResults with 0
+    const userCountResults = {};
+    Object.values(USER_STATUS_MASTER).forEach((status) => {
+      userCountResults[status] = 0;
+    });
+
+    userCountGroupByStatus.forEach((userCount) => {
+      const key = USER_STATUS_MASTER[userCount._id];
+      userCountResults[key] = userCount.totalCount;
+    });
+    const activeUserCount = userCountResults.active;
+
+    // Use userCountResults for inactive users, so delete unnecessary active
+    delete userCountResults.active;
+
+    // Calculate the total number of inactive users
+    const inactiveUserTotal = userCountResults.invited + userCountResults.deleted + userCountResults.suspended + userCountResults.registered;
+
+    // Get admin users
+    const findAdmins = util.promisify(User.findAdmins).bind(User);
+    const adminUsers = await findAdmins();
+
+    const data = {
+      total: activeUserCount + userCountResults.total,
+      active: {
+        total: activeUserCount,
+        admin: adminUsers.length,
+      },
+      inactive: {
+        total: inactiveUserTotal,
+        ...userCountResults,
+      },
+    };
+    res.status(200).send({ data });
+  });
+
+  return router;
+};

+ 197 - 0
src/server/routes/apiv3/users.js

@@ -32,6 +32,39 @@ module.exports = (crowi) => {
 
   const { ApiV3FormValidator } = crowi.middlewares;
 
+  /**
+   * @swagger
+   *
+   *  paths:
+   *    /_api/v3/users:
+   *      get:
+   *        tags: [Users]
+   *        description: Get users
+   *        responses:
+   *          200:
+   *            description: users are fetched
+   *            content:
+   *              application/json:
+   *                schema:
+   *                  properties:
+   *                    users:
+   *                      type: object
+   *                      description: a result of `Users.find`
+   */
+  router.get('/', loginRequiredStrictly, adminRequired, async(req, res) => {
+    try {
+      const page = parseInt(req.query.page) || 1;
+      const result = await User.findUsersWithPagination({ page });
+      const { docs: users, total: totalUsers, limit: pagingLimit } = result;
+      return res.apiv3({ users, totalUsers, pagingLimit });
+    }
+    catch (err) {
+      const msg = 'Error occurred in fetching user group list';
+      logger.error('Error', err);
+      return res.apiv3Err(new ErrorV3(msg, 'user-group-list-fetch-failed'));
+    }
+  });
+
   validator.inviteEmail = [
     // isEmail prevents line breaks, so use isString
     body('shapedEmailList').custom((value) => {
@@ -86,6 +119,170 @@ module.exports = (crowi) => {
       return res.apiv3Err(new ErrorV3(err));
     }
   });
+  /**
+   * @swagger
+   *
+   *  paths:
+   *    /_api/v3/users/{id}/giveAdmin:
+   *      put:
+   *        tags: [Users]
+   *        description: Give user admin
+   *        parameters:
+   *          - name: id
+   *            in: path
+   *            required: true
+   *            description: id of user for admin
+   *            schema:
+   *              type: string
+   *        responses:
+   *          200:
+   *            description: Give user admin success
+   *            content:
+   *              application/json:
+   *                schema:
+   *                  properties:
+   *                    userData:
+   *                      type: object
+   *                      description: data of admin user
+   */
+  router.put('/:id/giveAdmin', loginRequiredStrictly, adminRequired, csrf, async(req, res) => {
+    const { id } = req.params;
+
+    try {
+      const userData = await User.findById(id);
+      await userData.makeAdmin();
+      return res.apiv3({ userData });
+    }
+    catch (err) {
+      logger.error('Error', err);
+      return res.apiv3Err(new ErrorV3(err));
+    }
+  });
+  /**
+   * @swagger
+   *
+   *  paths:
+   *    /_api/v3/users/{id}/removeAdmin:
+   *      put:
+   *        tags: [Users]
+   *        description: Remove user admin
+   *        parameters:
+   *          - name: id
+   *            in: path
+   *            required: true
+   *            description: id of user for removing admin
+   *            schema:
+   *              type: string
+   *        responses:
+   *          200:
+   *            description: Remove user admin success
+   *            content:
+   *              application/json:
+   *                schema:
+   *                  properties:
+   *                    userData:
+   *                      type: object
+   *                      description: data of removed admin user
+   */
+  router.put('/:id/removeAdmin', loginRequiredStrictly, adminRequired, csrf, async(req, res) => {
+    const { id } = req.params;
+
+    try {
+      const userData = await User.findById(id);
+      await userData.removeFromAdmin();
+      return res.apiv3({ userData });
+    }
+    catch (err) {
+      logger.error('Error', err);
+      return res.apiv3Err(new ErrorV3(err));
+    }
+  });
+  /**
+   * @swagger
+   *
+   *  paths:
+   *    /_api/v3/users/{id}/activate:
+   *      put:
+   *        tags: [Users]
+   *        description: Activate user
+   *        parameters:
+   *          - name: id
+   *            in: path
+   *            required: true
+   *            description: id of activate user
+   *            schema:
+   *              type: string
+   *        responses:
+   *          200:
+   *            description: Activationg user success
+   *            content:
+   *              application/json:
+   *                schema:
+   *                  properties:
+   *                    userData:
+   *                      type: object
+   *                      description: data of activate user
+   */
+  router.put('/:id/activate', loginRequiredStrictly, adminRequired, csrf, async(req, res) => {
+    // check user upper limit
+    const isUserCountExceedsUpperLimit = await User.isUserCountExceedsUpperLimit();
+    if (isUserCountExceedsUpperLimit) {
+      const msg = 'Unable to activate because user has reached limit';
+      logger.error('Error', msg);
+      return res.apiv3Err(new ErrorV3(msg));
+    }
+
+    const { id } = req.params;
+
+    try {
+      const userData = await User.findById(id);
+      await userData.statusActivate();
+      return res.apiv3({ userData });
+    }
+    catch (err) {
+      logger.error('Error', err);
+      return res.apiv3Err(new ErrorV3(err));
+    }
+  });
+  /**
+   * @swagger
+   *
+   *  paths:
+   *    /_api/v3/users/{id}/deactivate:
+   *      put:
+   *        tags: [Users]
+   *        description: Deactivate user
+   *        parameters:
+   *          - name: id
+   *            in: path
+   *            required: true
+   *            description: id of deactivate user
+   *            schema:
+   *              type: string
+   *        responses:
+   *          200:
+   *            description: Deactivationg user success
+   *            content:
+   *              application/json:
+   *                schema:
+   *                  properties:
+   *                    userData:
+   *                      type: object
+   *                      description: data of deactivate user
+   */
+  router.put('/:id/deactivate', loginRequiredStrictly, adminRequired, csrf, async(req, res) => {
+    const { id } = req.params;
+
+    try {
+      const userData = await User.findById(id);
+      await userData.statusSuspend();
+      return res.apiv3({ userData });
+    }
+    catch (err) {
+      logger.error('Error', err);
+      return res.apiv3Err(new ErrorV3(err));
+    }
+  });
   /**
    * @swagger
    *

+ 2 - 1
src/server/routes/attachment.js

@@ -203,7 +203,8 @@ module.exports = function(crowi, app) {
    * @apiGroup Attachment
    */
   api.limit = async function(req, res) {
-    return res.json(ApiResponse.success(await fileUploader.checkLimit(req.query.fileSize)));
+    const fileSize = Number(req.query.fileSize);
+    return res.json(ApiResponse.success(await fileUploader.checkLimit(fileSize)));
   };
 
   /**

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

@@ -128,10 +128,6 @@ module.exports = function(crowi, app) {
   app.post('/admin/global-notification/:id/remove', loginRequiredStrictly , adminRequired , admin.globalNotification.remove);
 
   app.get('/admin/users'                , loginRequiredStrictly , adminRequired , admin.user.index);
-  app.post('/admin/user/:id/makeAdmin'  , loginRequiredStrictly , adminRequired , csrf, admin.user.makeAdmin);
-  app.post('/admin/user/:id/removeFromAdmin', loginRequiredStrictly , adminRequired , admin.user.removeFromAdmin);
-  app.post('/admin/user/:id/activate'   , loginRequiredStrictly , adminRequired , csrf, admin.user.activate);
-  app.post('/admin/user/:id/suspend'    , loginRequiredStrictly , adminRequired , csrf, admin.user.suspend);
   app.post('/admin/user/:id/removeCompletely' , loginRequiredStrictly , adminRequired , csrf, admin.user.removeCompletely);
   // new route patterns from here:
   app.post('/_api/admin/users.resetPassword'  , loginRequiredStrictly , adminRequired , csrf, admin.user.resetPassword);