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

Merge branch 'imprv/reactify-admin' into reactify-admin-external-account

# Conflicts:
#	src/client/js/app.jsx
WESEEK Kaito 6 лет назад
Родитель
Сommit
27f618e50f

+ 30 - 5
src/client/js/app.jsx

@@ -40,9 +40,8 @@ import CustomScriptEditor from './components/Admin/CustomScriptEditor';
 import CustomHeaderEditor from './components/Admin/CustomHeaderEditor';
 import AdminRebuildSearch from './components/Admin/AdminRebuildSearch';
 import MarkdownSetting from './components/Admin/MarkdownSetting/MarkDownSetting';
-import UserPage from './components/Admin/Users/Users';
+import Users from './components/Admin/Users/Users';
 import UserGroupPage from './components/Admin/UserGroup/UserGroupPage';
-import ManageExternalAccount from './components/Admin/Users/ManageExternalAccount';
 import Customize from './components/Admin/Customize/Customize';
 import Importer from './components/Admin/Importer';
 import FullTextSearchManagement from './components/Admin/FullTextSearchManagement/FullTextSearchPage';
@@ -54,7 +53,9 @@ import CommentContainer from './services/CommentContainer';
 import EditorContainer from './services/EditorContainer';
 import TagContainer from './services/TagContainer';
 import UserGroupDetailContainer from './services/UserGroupDetailContainer';
+import AdminUsersContainer from './services/AdminUsersContainer';
 import WebsocketContainer from './services/WebsocketContainer';
+import MarkDownSettingContainer from './services/MarkDownSettingContainer';
 
 const logger = loggerFactory('growi:app');
 
@@ -107,9 +108,6 @@ let componentMappings = {
   'user-created-list': <RecentCreated />,
   'user-draft-list': <MyDraftList />,
 
-  'admin-markdown-setting': <MarkdownSetting />,
-  'admin-user-page': <UserPage />,
-  'admin-external-account-setting': <ManageExternalAccount />,
   'admin-full-text-search-management': <FullTextSearchManagement />,
   'admin-customize': <Customize />,
 
@@ -161,6 +159,19 @@ Object.keys(componentMappings).forEach((key) => {
 });
 
 // render for admin
+const adminUsersElem = document.getElementById('admin-user-page');
+if (adminUsersElem != null) {
+  const adminUsersContainer = new AdminUsersContainer(appContainer);
+  ReactDOM.render(
+    <Provider inject={[injectableContainers, adminUsersContainer]}>
+      <I18nextProvider i18n={i18n}>
+        <Users />
+      </I18nextProvider>
+    </Provider>,
+    adminUsersElem,
+  );
+}
+
 const adminUserGroupDetailElem = document.getElementById('admin-user-group-detail');
 if (adminUserGroupDetailElem != null) {
   const userGroupDetailContainer = new UserGroupDetailContainer(appContainer);
@@ -173,6 +184,20 @@ if (adminUserGroupDetailElem != null) {
     adminUserGroupDetailElem,
   );
 }
+
+const adminMarkDownSettingElem = document.getElementById('admin-markdown-setting');
+if (adminMarkDownSettingElem != null) {
+  const markDownSettingContainer = new MarkDownSettingContainer(appContainer);
+  ReactDOM.render(
+    <Provider inject={[injectableContainers, markDownSettingContainer]}>
+      <I18nextProvider i18n={i18n}>
+        <MarkdownSetting />
+      </I18nextProvider>
+    </Provider>,
+    adminMarkDownSettingElem,
+  );
+}
+
 const customCssEditorElem = document.getElementById('custom-css-editor');
 if (customCssEditorElem != null) {
   // get input[type=hidden] element

+ 3 - 4
src/client/js/components/Admin/MarkdownSetting/MarkDownSetting.jsx

@@ -37,6 +37,7 @@ class MarkdownSetting extends React.Component {
     const { t } = this.props;
 
     return (
+      // TODO GW-322 adjust layout
       <React.Fragment>
         <div>
           {/* Line Break Setting */}
@@ -99,10 +100,8 @@ class MarkdownSetting extends React.Component {
         </div>
         {/* XSS Setting */}
         <div className="row my-3">
-          <div className="form-group">
-            <legend>{ t('markdown_setting.XSS_setting') }</legend>
-            <p className="well">{ t('markdown_setting.XSS_setting_desc') }</p>
-          </div>
+          <h2>{ t('markdown_setting.XSS_setting') }</h2>
+          <p className="well">{ t('markdown_setting.XSS_setting_desc') }</p>
           <XssForm />
         </div>
       </React.Fragment>

+ 43 - 10
src/client/js/components/Admin/MarkdownSetting/WhiteListInput.jsx

@@ -1,11 +1,12 @@
-/* eslint-disable max-len */
 import React from 'react';
 import PropTypes from 'prop-types';
 import { withTranslation } from 'react-i18next';
 
 import { createSubscribedElement } from '../../UnstatedUtils';
+import { tags, attrs } from '../../../../../lib/service/xss/recommended-whitelist';
 
 import AppContainer from '../../../services/AppContainer';
+import MarkDownSettingContainer from '../../../services/MarkDownSettingContainer';
 
 class WhiteListInput extends React.Component {
 
@@ -19,9 +20,28 @@ class WhiteListInput extends React.Component {
     );
   }
 
+  renderTagValue() {
+    const { customizable, markDownSettingContainer } = this.props;
+
+    if (customizable) {
+      return markDownSettingContainer.state.tagWhiteList;
+    }
+
+    return tags;
+  }
+
+  renderAttrValue() {
+    const { customizable, markDownSettingContainer } = this.props;
+
+    if (customizable) {
+      return markDownSettingContainer.state.attrWhiteList;
+    }
+
+    return attrs;
+  }
+
   render() {
-    const { t, customizable } = this.props;
-    const { onChangeTagWhiteList, onChangeAttrWhiteList } = this.props;
+    const { t, customizable, markDownSettingContainer } = this.props;
 
     return (
       <>
@@ -30,16 +50,30 @@ class WhiteListInput extends React.Component {
             { t('markdown_setting.Tag names') }
             {customizable && this.renderRecommendBtn()}
           </div>
-          {/* TODO GW-304 fetch correct defaultValue */}
-          <textarea className="form-control xss-list" name="recommendedTags" rows="6" cols="40" readOnly={!customizable} defaultValue="recommendedWhitelist.tags" onChange={(e) => { onChangeTagWhiteList(e.target.value) }} />
+          <textarea
+            className="form-control xss-list"
+            name="recommendedTags"
+            rows="6"
+            cols="40"
+            readOnly={!customizable}
+            value={this.renderTagValue()}
+            onChange={(e) => { markDownSettingContainer.setState({ tagWhiteList: e.target.value }) }}
+          />
         </div>
         <div className="m-t-15">
           <div className="d-flex justify-content-between">
             { t('markdown_setting.Tag attributes') }
             {customizable && this.renderRecommendBtn()}
           </div>
-          {/* TODO GW-304 fetch correct defaultValue */}
-          <textarea className="form-control xss-list" name="recommendedAttrs" rows="6" cols="40" readOnly={!customizable} defaultValue="recommendedWhitelist.attrs" onChange={(e) => { onChangeAttrWhiteList(e.target.value) }} />
+          <textarea
+            className="form-control xss-list"
+            name="recommendedAttrs"
+            rows="6"
+            cols="40"
+            readOnly={!customizable}
+            value={this.renderAttrValue()}
+            onChange={(e) => { markDownSettingContainer.setState({ attrWhiteList: e.target.value }) }}
+          />
         </div>
       </>
     );
@@ -48,16 +82,15 @@ class WhiteListInput extends React.Component {
 }
 
 const WhiteListWrapper = (props) => {
-  return createSubscribedElement(WhiteListInput, props, [AppContainer]);
+  return createSubscribedElement(WhiteListInput, props, [AppContainer, MarkDownSettingContainer]);
 };
 
 WhiteListInput.propTypes = {
   t: PropTypes.func.isRequired, // i18next
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
+  markDownSettingContainer: PropTypes.instanceOf(MarkDownSettingContainer).isRequired,
 
   customizable: PropTypes.bool.isRequired,
-  onChangeTagWhiteList: PropTypes.func,
-  onChangeAttrWhiteList: PropTypes.func,
 };
 
 export default withTranslation()(WhiteListWrapper);

+ 33 - 51
src/client/js/components/Admin/MarkdownSetting/XssForm.jsx

@@ -1,5 +1,3 @@
-/* eslint-disable react/no-unused-state */
-/* eslint-disable max-len */
 import React from 'react';
 import PropTypes from 'prop-types';
 import { withTranslation } from 'react-i18next';
@@ -7,6 +5,8 @@ import { withTranslation } from 'react-i18next';
 import { createSubscribedElement } from '../../UnstatedUtils';
 
 import AppContainer from '../../../services/AppContainer';
+import MarkDownSettingContainer from '../../../services/MarkDownSettingContainer';
+
 import WhiteListInput from './WhiteListInput';
 
 class XssForm extends React.Component {
@@ -14,58 +14,27 @@ class XssForm extends React.Component {
   constructor(props) {
     super(props);
 
-    const { appContainer } = this.props;
-
-    this.state = {
-      // TODO GW-304 fetch correct value
-      isEnabledXss: false,
-      XssOption: 1,
-      tagWhiteList: appContainer.config.tagWhiteList,
-      attrWhiteList: '',
-    };
-
-    this.onChangeEnableXss = this.onChangeEnableXss.bind(this);
-    this.onChangeXssOption = this.onChangeXssOption.bind(this);
-    this.onChangeTagWhiteList = this.onChangeTagWhiteList.bind(this);
-    this.onChangeAttrWhiteList = this.onChangeAttrWhiteList.bind(this);
     this.onClickSubmit = this.onClickSubmit.bind(this);
   }
 
-  onChangeEnableXss() {
-    this.setState({ isEnabledXss: !this.state.isEnabledXss });
-  }
-
-  onChangeXssOption(value) {
-    this.setState({ XssOption: value });
-  }
-
-  onChangeTagWhiteList(value) {
-    this.setState({ tagWhiteList: value });
-  }
-
-  onChangeAttrWhiteList(value) {
-    this.setState({ attrWhiteList: value });
-  }
-
-  async componentDidMount() {
-    await this.syncXssSettings();
-  }
-
   async onClickSubmit() {
     // TODO GW-303 create apiV3 of update setting
   }
 
-  async syncXssSettings() {
-    // TODO GW-304 createApiV3
-  }
-
   xssOptions() {
-    const { t } = this.props;
+    const { t, markDownSettingContainer } = this.props;
+    const { xssOption } = markDownSettingContainer.state;
 
     return (
       <fieldset className="form-group col-xs-12 my-3">
         <div className="col-xs-4 radio radio-primary">
-          <input type="radio" id="xssOption1" name="XssOption" onChange={() => { this.onChangeXssOption(1) }} />
+          <input
+            type="radio"
+            id="xssOption1"
+            name="XssOption"
+            checked={xssOption === 1}
+            onChange={() => { markDownSettingContainer.setState({ xssOption: 1 }) }}
+          />
           <label htmlFor="xssOption1">
             <p className="font-weight-bold">{ t('markdown_setting.Ignore all tags') }</p>
             <div className="m-t-15">
@@ -75,7 +44,13 @@ class XssForm extends React.Component {
         </div>
 
         <div className="col-xs-4 radio radio-primary">
-          <input type="radio" id="xssOption2" name="XssOption" onChange={() => { this.onChangeXssOption(2) }} />
+          <input
+            type="radio"
+            id="xssOption2"
+            name="XssOption"
+            checked={xssOption === 2}
+            onChange={() => { markDownSettingContainer.setState({ xssOption: 2 }) }}
+          />
           <label htmlFor="xssOption2">
             <p className="font-weight-bold">{ t('markdown_setting.Recommended setting') }</p>
             <WhiteListInput customizable={false} />
@@ -83,10 +58,16 @@ class XssForm extends React.Component {
         </div>
 
         <div className="col-xs-4 radio radio-primary">
-          <input type="radio" id="xssOption3" name="XssOption" onChange={() => { this.onChangeXssOption(3) }} />
+          <input
+            type="radio"
+            id="xssOption3"
+            name="XssOption"
+            checked={xssOption === 3}
+            onChange={() => { markDownSettingContainer.setState({ xssOption: 3 }) }}
+          />
           <label htmlFor="xssOption3">
             <p className="font-weight-bold">{ t('markdown_setting.Custom Whitelist') }</p>
-            <WhiteListInput customizable onChangeTagWhiteList={this.onChangeTagWhiteList} onChangeAttrWhiteList={this.onChangeAttrWhiteList} />
+            <WhiteListInput customizable />
           </label>
         </div>
       </fieldset>
@@ -94,21 +75,22 @@ class XssForm extends React.Component {
   }
 
   render() {
-    const { t } = this.props;
+    const { t, markDownSettingContainer } = this.props;
+    const { isEnabledXss } = markDownSettingContainer.state;
 
     return (
       <React.Fragment>
         <form className="row">
           <div className="form-group">
             <div className="col-xs-4 text-right">
-              <div className="checkbox checkbox-success" onChange={this.onChangeEnableXss}>
-                <input type="checkbox" id="XssEnable" className="form-check-input" name="isEnabledXss" checked={this.state.isEnabledXss} />
+              <div className="checkbox checkbox-success" onChange={markDownSettingContainer.switchEnableXss}>
+                <input type="checkbox" id="XssEnable" className="form-check-input" name="isEnabledXss" checked={isEnabledXss} />
                 <label htmlFor="XssEnable">
                   { t('markdown_setting.Enable XSS prevention') }
                 </label>
               </div>
             </div>
-            {this.state.isEnabledXss && this.xssOptions()}
+            {isEnabledXss && this.xssOptions()}
           </div>
           <div className="form-group my-3">
             <div className="col-xs-offset-4 col-xs-5">
@@ -123,13 +105,13 @@ class XssForm extends React.Component {
 }
 
 const XssFormWrapper = (props) => {
-  return createSubscribedElement(XssForm, props, [AppContainer]);
+  return createSubscribedElement(XssForm, props, [AppContainer, MarkDownSettingContainer]);
 };
 
 XssForm.propTypes = {
   t: PropTypes.func.isRequired, // i18next
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
-
+  markDownSettingContainer: PropTypes.instanceOf(MarkDownSettingContainer).isRequired,
 };
 
 export default withTranslation()(XssFormWrapper);

+ 6 - 24
src/client/js/components/Admin/Users/InviteUserControl.jsx

@@ -4,39 +4,20 @@ import { withTranslation } from 'react-i18next';
 
 import { createSubscribedElement } from '../../UnstatedUtils';
 import AppContainer from '../../../services/AppContainer';
+import AdminUsersContainer from '../../../services/AdminUsersContainer';
 import UserInviteModal from './UserInviteModal';
 
 class InviteUserControl extends React.Component {
 
-  constructor(props) {
-    super(props);
-
-    this.state = {
-      isUserInviteModalShown: false,
-    };
-
-    this.toggleUserInviteModal = this.toggleUserInviteModal.bind(this);
-  }
-
-  /**
-   * user招待モーダルを開閉する
-   */
-  toggleUserInviteModal() {
-    this.setState({ isUserInviteModalShown: !this.state.isUserInviteModalShown });
-  }
-
   render() {
-    const { t } = this.props;
+    const { t, adminUsersContainer } = this.props;
 
     return (
       <Fragment>
-        <button type="button" className="btn btn-default" onClick={this.toggleUserInviteModal}>
+        <button type="button" className="btn btn-default" onClick={adminUsersContainer.toggleUserInviteModal}>
           { t('user_management.invite_users') }
         </button>
-        <UserInviteModal
-          show={this.state.isUserInviteModalShown}
-          onToggleModal={this.toggleUserInviteModal}
-        />
+        <UserInviteModal />
       </Fragment>
     );
   }
@@ -44,12 +25,13 @@ class InviteUserControl extends React.Component {
 }
 
 const InviteUserControlWrapper = (props) => {
-  return createSubscribedElement(InviteUserControl, props, [AppContainer]);
+  return createSubscribedElement(InviteUserControl, props, [AppContainer, AdminUsersContainer]);
 };
 
 InviteUserControl.propTypes = {
   t: PropTypes.func.isRequired, // i18next
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
+  adminUsersContainer: PropTypes.instanceOf(AdminUsersContainer).isRequired,
 };
 
 export default withTranslation()(InviteUserControlWrapper);

+ 45 - 50
src/client/js/components/Admin/Users/PasswordResetModal.jsx

@@ -7,6 +7,7 @@ import Modal from 'react-bootstrap/es/Modal';
 import { toastError } from '../../../util/apiNotification';
 import { createSubscribedElement } from '../../UnstatedUtils';
 import AppContainer from '../../../services/AppContainer';
+import AdminUsersContainer from '../../../services/AdminUsersContainer';
 
 class PasswordResetModal extends React.Component {
 
@@ -18,13 +19,12 @@ class PasswordResetModal extends React.Component {
       isPasswordResetDone: false,
     };
 
-    this.returnModalBody = this.returnModalBody.bind(this);
-    this.returnModalFooter = this.returnModalFooter.bind(this);
     this.resetPassword = this.resetPassword.bind(this);
   }
 
   async resetPassword() {
-    const { appContainer, user } = this.props;
+    const { appContainer, adminUsersContainer } = this.props;
+    const user = adminUsersContainer.state.userForPasswordResetModal;
 
     const res = await appContainer.apiPost('/admin/users.resetPassword', { user_id: user._id });
     if (res.ok) {
@@ -35,71 +35,69 @@ class PasswordResetModal extends React.Component {
     }
   }
 
-  returnModalBody() {
-    const { t, user } = this.props;
+  renderModalBodyBeforeReset() {
+    const { t, adminUsersContainer } = this.props;
+    const user = adminUsersContainer.state.userForPasswordResetModal;
+
+    return (
+      <div>
+        <p className="alert alert-danger">{ t('user_management.password_reset_message') }</p>
+        <p>
+          { t('user_management.target_user') }: <code>{ user.email }</code>
+        </p>
+        <p>
+          { t('user_management.new_password') }: <code>{ this.state.temporaryPassword }</code>
+        </p>
+      </div>
+    );
+  }
+
+  returnModalBodyAfterReset() {
+    const { t, adminUsersContainer } = this.props;
+    const user = adminUsersContainer.state.userForPasswordResetModal;
+
     return (
-      this.state.isPasswordResetDone
-        ? (
-          <div>
-            <p className="alert alert-danger">{ t('user_management.password_reset_message') }</p>
-            <p>
-              { t('user_management.target_user') }: <code>{ user.email }</code>
-            </p>
-            <p>
-              { t('user_management.new_password') }: <code>{ this.state.temporaryPassword }</code>
-            </p>
-          </div>
-        )
-        : (
-          <div>
-            <p>
-              { t('user_management.password_never_seen') }<br />
-              <span className="text-danger">{ t('user_management.send_new_password') }</span>
-            </p>
-            <p>
-              { t('user_management.target_user') }: <code>{ user.email }</code>
-            </p>
-            <button type="submit" className="btn btn-primary" onClick={this.resetPassword}>
-              { t('user_management.reset_password')}
-            </button>
-          </div>
-        )
+      <div>
+        <p>
+          { t('user_management.password_never_seen') }<br />
+          <span className="text-danger">{ t('user_management.send_new_password') }</span>
+        </p>
+        <p>
+          { t('user_management.target_user') }: <code>{ user.email }</code>
+        </p>
+        <button type="submit" className="btn btn-primary" onClick={this.resetPassword}>
+          { t('user_management.reset_password')}
+        </button>
+      </div>
     );
   }
 
   returnModalFooter() {
     return (
-      this.state.isPasswordResetDone
-        ? (
-          <div>
-            <button type="submit" className="btn btn-primary" onClick={this.props.onHideModal}>OK</button>
-          </div>
-        )
-        : (
-          ''
-        )
+      <div>
+        <button type="submit" className="btn btn-primary" onClick={this.props.adminUsersContainer.hidePasswordResetModal}>OK</button>
+      </div>
     );
   }
 
 
   render() {
-    const { t } = this.props;
+    const { t, adminUsersContainer } = this.props;
 
     return (
-      <Modal show={this.props.show} onHide={this.props.onHideModal}>
+      <Modal show={adminUsersContainer.state.isPasswordResetModalShown} onHide={adminUsersContainer.hidePasswordResetModal}>
         <Modal.Header className="modal-header" closeButton>
           <Modal.Title>
             { t('user_management.reset_password') }
           </Modal.Title>
         </Modal.Header>
         <Modal.Body>
-          {this.returnModalBody()}
+          {this.state.isPasswordResetDone ? this.renderModalBodyBeforeReset() : this.returnModalBodyAfterReset()}
         </Modal.Body>
         <Modal.Footer>
-          {this.returnModalFooter()}
+          {this.state.isPasswordResetDone && this.returnModalFooter()}
         </Modal.Footer>
       </Modal>
-
     );
   }
 
@@ -109,16 +107,13 @@ class PasswordResetModal extends React.Component {
  * Wrapper component for using unstated
  */
 const PasswordResetModalWrapper = (props) => {
-  return createSubscribedElement(PasswordResetModal, props, [AppContainer]);
+  return createSubscribedElement(PasswordResetModal, props, [AppContainer, AdminUsersContainer]);
 };
 
 PasswordResetModal.propTypes = {
   t: PropTypes.func.isRequired, // i18next
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
-
-  user: PropTypes.object.isRequired,
-  show: PropTypes.bool.isRequired,
-  onHideModal: PropTypes.func.isRequired,
+  adminUsersContainer: PropTypes.instanceOf(AdminUsersContainer).isRequired,
 };
 
 export default withTranslation()(PasswordResetModalWrapper);

+ 6 - 7
src/client/js/components/Admin/Users/UserInviteModal.jsx

@@ -11,6 +11,7 @@ import { toastSuccess, toastError } from '../../../util/apiNotification';
 
 import { createSubscribedElement } from '../../UnstatedUtils';
 import AppContainer from '../../../services/AppContainer';
+import AdminUsersContainer from '../../../services/AdminUsersContainer';
 
 class UserInviteModal extends React.Component {
 
@@ -30,7 +31,7 @@ class UserInviteModal extends React.Component {
   }
 
   onToggleModal() {
-    this.props.onToggleModal();
+    this.props.adminUsersContainer.toggleUserInviteModal();
     this.setState({ invitedEmailList: null });
   }
 
@@ -185,11 +186,11 @@ class UserInviteModal extends React.Component {
   }
 
   render() {
-    const { t } = this.props;
+    const { t, adminUsersContainer } = this.props;
     const { invitedEmailList } = this.state;
 
     return (
-      <Modal show={this.props.show} onHide={this.onToggleModal}>
+      <Modal show={adminUsersContainer.state.isUserInviteModalShown} onHide={this.onToggleModal}>
         <Modal.Header className="modal-header" closeButton>
           <Modal.Title>
             { t('user_management.invite_users') }
@@ -213,16 +214,14 @@ class UserInviteModal extends React.Component {
  * Wrapper component for using unstated
  */
 const UserInviteModalWrapper = (props) => {
-  return createSubscribedElement(UserInviteModal, props, [AppContainer]);
+  return createSubscribedElement(UserInviteModal, props, [AppContainer, AdminUsersContainer]);
 };
 
 
 UserInviteModal.propTypes = {
   t: PropTypes.func.isRequired, // i18next
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
-
-  show: PropTypes.bool.isRequired,
-  onToggleModal: PropTypes.func.isRequired,
+  adminUsersContainer: PropTypes.instanceOf(AdminUsersContainer).isRequired,
 };
 
 export default withTranslation()(UserInviteModalWrapper);

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

@@ -10,6 +10,7 @@ import GiveAdminForm from './GiveAdminForm';
 
 import { createSubscribedElement } from '../../UnstatedUtils';
 import AppContainer from '../../../services/AppContainer';
+import AdminUsersContainer from '../../../services/AdminUsersContainer';
 
 class UserMenu extends React.Component {
 
@@ -24,7 +25,7 @@ class UserMenu extends React.Component {
   }
 
   onPasswordResetClicked() {
-    this.props.onPasswordResetClicked(this.props.user);
+    this.props.adminUsersContainer.showPasswordResetModal(this.props.user);
   }
 
   render() {
@@ -48,7 +49,7 @@ class UserMenu extends React.Component {
             <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} removeUser={this.props.removeUser} />}
+              {(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>
@@ -65,16 +66,15 @@ class UserMenu extends React.Component {
 }
 
 const UserMenuWrapper = (props) => {
-  return createSubscribedElement(UserMenu, props, [AppContainer]);
+  return createSubscribedElement(UserMenu, props, [AppContainer, AdminUsersContainer]);
 };
 
 UserMenu.propTypes = {
   t: PropTypes.func.isRequired, // i18next
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
+  adminUsersContainer: PropTypes.instanceOf(AdminUsersContainer).isRequired,
 
   user: PropTypes.object.isRequired,
-  removeUser: PropTypes.func.isRequired,
-  onPasswordResetClicked: PropTypes.func.isRequired,
 };
 
 export default withTranslation()(UserMenuWrapper);

+ 12 - 4
src/client/js/components/Admin/Users/UserRemoveButton.jsx

@@ -4,6 +4,8 @@ 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 UserRemoveButton extends React.Component {
 
@@ -13,8 +15,14 @@ class UserRemoveButton extends React.Component {
     this.onClickDeleteBtn = this.onClickDeleteBtn.bind(this);
   }
 
-  onClickDeleteBtn() {
-    this.props.removeUser(this.props.user);
+  async onClickDeleteBtn() {
+    try {
+      const username = await this.props.adminUsersContainer.removeUser(this.props.user._id);
+      toastSuccess(`Delete ${username} success`);
+    }
+    catch (err) {
+      toastError(err);
+    }
   }
 
   render() {
@@ -33,15 +41,15 @@ class UserRemoveButton extends React.Component {
  * Wrapper component for using unstated
  */
 const UserRemoveButtonWrapper = (props) => {
-  return createSubscribedElement(UserRemoveButton, props, [AppContainer]);
+  return createSubscribedElement(UserRemoveButton, props, [AppContainer, AdminUsersContainer]);
 };
 
 UserRemoveButton.propTypes = {
   t: PropTypes.func.isRequired, // i18next
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
+  adminUsersContainer: PropTypes.instanceOf(AdminUsersContainer).isRequired,
 
   user: PropTypes.object.isRequired,
-  removeUser: PropTypes.func.isRequired,
 };
 
 export default withTranslation()(UserRemoveButtonWrapper);

+ 6 - 7
src/client/js/components/Admin/Users/UserTable.jsx

@@ -8,6 +8,7 @@ import UserMenu from './UserMenu';
 
 import { createSubscribedElement } from '../../UnstatedUtils';
 import AppContainer from '../../../services/AppContainer';
+import AdminUsersContainer from '../../../services/AdminUsersContainer';
 
 class UserTable extends React.Component {
 
@@ -61,7 +62,7 @@ class UserTable extends React.Component {
   }
 
   render() {
-    const { t } = this.props;
+    const { t, adminUsersContainer } = this.props;
 
     return (
       <Fragment>
@@ -81,7 +82,7 @@ class UserTable extends React.Component {
             </tr>
           </thead>
           <tbody>
-            {this.props.users.map((user) => {
+            {adminUsersContainer.state.users.map((user) => {
               return (
                 <tr key={user._id}>
                   <td>
@@ -99,7 +100,7 @@ class UserTable extends React.Component {
                     { user.lastLoginAt && <span>{dateFnsFormat(new Date(user.lastLoginAt), 'yyyy-MM-dd HH:mm')}</span> }
                   </td>
                   <td>
-                    <UserMenu user={user} onPasswordResetClicked={this.props.onPasswordResetClicked} removeUser={this.props.removeUser} />
+                    <UserMenu user={user} />
                   </td>
                 </tr>
               );
@@ -113,16 +114,14 @@ class UserTable extends React.Component {
 }
 
 const UserTableWrapper = (props) => {
-  return createSubscribedElement(UserTable, props, [AppContainer]);
+  return createSubscribedElement(UserTable, props, [AppContainer, AdminUsersContainer]);
 };
 
 UserTable.propTypes = {
   t: PropTypes.func.isRequired, // i18next
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
+  adminUsersContainer: PropTypes.instanceOf(AdminUsersContainer).isRequired,
 
-  users: PropTypes.array.isRequired,
-  removeUser: PropTypes.func.isRequired,
-  onPasswordResetClicked: PropTypes.func.isRequired,
 };
 
 export default withTranslation()(UserTableWrapper);

+ 7 - 64
src/client/js/components/Admin/Users/Users.jsx

@@ -2,8 +2,6 @@ import React, { Fragment } from 'react';
 import PropTypes from 'prop-types';
 import { withTranslation } from 'react-i18next';
 
-import { toastSuccess, toastError } from '../../../util/apiNotification';
-
 import PasswordResetModal from './PasswordResetModal';
 import PaginationWrapper from '../../PaginationWrapper';
 import InviteUserControl from './InviteUserControl';
@@ -11,6 +9,7 @@ import UserTable from './UserTable';
 
 import { createSubscribedElement } from '../../UnstatedUtils';
 import AppContainer from '../../../services/AppContainer';
+import AdminUsersContainer from '../../../services/AdminUsersContainer';
 
 class UserPage extends React.Component {
 
@@ -18,71 +17,18 @@ class UserPage extends React.Component {
     super();
 
     this.state = {
-      userForPasswordResetModal: null,
-      users: [],
       activePage: 1,
       pagingLimit: Infinity,
-      isPasswordResetModalShown: false,
     };
 
-    this.removeUser = this.removeUser.bind(this);
-    this.showPasswordResetModal = this.showPasswordResetModal.bind(this);
-    this.hidePasswordResetModal = this.hidePasswordResetModal.bind(this);
-  }
-
-  // TODO unstatedContainerを作ってそこにリファクタすべき
-  componentDidMount() {
-    const data = document.getElementById('admin-user-page');
-    const users = JSON.parse(data.getAttribute('users'));
-
-    this.setState({
-      users,
-    });
-  }
-
-  async removeUser(user) {
-
-    const { appContainer } = this.props;
-
-    try {
-      const response = await appContainer.apiv3.delete(`/users/${user._id}/remove`);
-      const { username } = response.data.userData;
-      toastSuccess(`Delete ${username} success`);
-    }
-    catch (err) {
-      toastError(err);
-    }
-  }
-
-  /**
-   * passwordリセットモーダルが開き、userが渡される
-   * @param {object} user
-   *
-   */
-  showPasswordResetModal(user) {
-    this.setState({
-      isPasswordResetModalShown: true,
-      userForPasswordResetModal: user,
-    });
   }
 
-  hidePasswordResetModal() {
-    this.setState({ isPasswordResetModalShown: false });
-  }
-
-
   render() {
-    const { t } = this.props;
+    const { t, adminUsersContainer } = this.props;
 
     return (
       <Fragment>
-        { this.state.userForPasswordResetModal && (
-          <PasswordResetModal
-            user={this.state.userForPasswordResetModal}
-            show={this.state.isPasswordResetModalShown}
-            onHideModal={this.hidePasswordResetModal}
-          />
-        ) }
+        {adminUsersContainer.state.userForPasswordResetModal && <PasswordResetModal />}
         <p>
           <InviteUserControl />
           <a className="btn btn-default btn-outline ml-2" href="/admin/users/external-accounts">
@@ -90,15 +36,11 @@ class UserPage extends React.Component {
             { t('user_management.external_account') }
           </a>
         </p>
-        <UserTable
-          users={this.state.users}
-          onPasswordResetClicked={this.showPasswordResetModal}
-          removeUser={this.removeUser}
-        />
+        <UserTable />
         <PaginationWrapper
           activePage={this.state.activePage}
           changePage={this.handlePage} // / TODO GW-314 create function
-          totalItemsCount={this.state.users.length} // TODO GW-314 props.userTotalCount
+          totalItemsCount={adminUsersContainer.state.users.length}
           pagingLimit={this.state.pagingLimit}
         />
       </Fragment>
@@ -108,12 +50,13 @@ class UserPage extends React.Component {
 }
 
 const UserPageWrapper = (props) => {
-  return createSubscribedElement(UserPage, props, [AppContainer]);
+  return createSubscribedElement(UserPage, props, [AppContainer, AdminUsersContainer]);
 };
 
 UserPage.propTypes = {
   t: PropTypes.func.isRequired, // i18next
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
+  adminUsersContainer: PropTypes.instanceOf(AdminUsersContainer).isRequired,
 
 };
 

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

@@ -0,0 +1,78 @@
+import { Container } from 'unstated';
+
+import loggerFactory from '@alias/logger';
+
+// eslint-disable-next-line no-unused-vars
+const logger = loggerFactory('growi:services:UserGroupDetailContainer');
+
+/**
+ * Service container for admin users page (Users.jsx)
+ * @extends {Container} unstated Container
+ */
+export default class AdminUsersContainer extends Container {
+
+  constructor(appContainer) {
+    super();
+
+    this.appContainer = appContainer;
+
+    this.state = {
+      users: JSON.parse(document.getElementById('admin-user-page').getAttribute('users')) || [],
+      isPasswordResetModalShown: false,
+      isUserInviteModalShown: false,
+      userForPasswordResetModal: null,
+    };
+
+    this.showPasswordResetModal = this.showPasswordResetModal.bind(this);
+    this.hidePasswordResetModal = this.hidePasswordResetModal.bind(this);
+    this.toggleUserInviteModal = this.toggleUserInviteModal.bind(this);
+  }
+
+  /**
+   * Workaround for the mangling in production build to break constructor.name
+   */
+  static getClassName() {
+    return 'AdminUsersContainer';
+  }
+
+  /**
+   * open reset password modal, and props user
+   * @memberOf AdminUsersContainer
+   * @param {object} user
+   */
+  async showPasswordResetModal(user) {
+    await this.setState({
+      isPasswordResetModalShown: true,
+      userForPasswordResetModal: user,
+    });
+  }
+
+  /**
+   * close reset password modal
+   * @memberOf AdminUsersContainer
+   */
+  async hidePasswordResetModal() {
+    await this.setState({ isPasswordResetModalShown: false });
+  }
+
+  /**
+   * toggle user invite modal
+   * @memberOf AdminUsersContainer
+   */
+  async toggleUserInviteModal() {
+    await this.setState({ isUserInviteModalShown: !this.state.isUserInviteModalShown });
+  }
+
+  /**
+   * remove user
+   * @memberOf AdminUsersContainer
+   * @param {string} userId
+   * @return {string} username
+   */
+  async removeUser(userId) {
+    const response = await this.appContainer.apiv3.delete(`/users/${userId}/remove`);
+    const { username } = response.data.userData;
+    return username;
+  }
+
+}

+ 41 - 0
src/client/js/services/MarkDownSettingContainer.js

@@ -0,0 +1,41 @@
+import { Container } from 'unstated';
+
+/**
+ * Service container for admin markdown setting page (MarkDownSetting.jsx)
+ * @extends {Container} unstated Container
+ */
+export default class MarkDownSettingContainer extends Container {
+
+  constructor(appContainer) {
+    super();
+
+    this.appContainer = appContainer;
+
+    this.state = {
+      isEnabledXss: (appContainer.config.xssOption != null),
+      xssOption: appContainer.config.xssOption,
+      tagWhiteList: appContainer.config.tagWhiteList || '',
+      attrWhiteList: appContainer.config.attrWhiteList || '',
+    };
+
+    this.switchEnableXss = this.switchEnableXss.bind(this);
+  }
+
+  /**
+   * Workaround for the mangling in production build to break constructor.name
+   */
+  static getClassName() {
+    return 'MarkDownSettingContainer';
+  }
+
+  /**
+   * Switch enableXss
+   */
+  switchEnableXss() {
+    if (this.state.isEnabledXss) {
+      this.setState({ xssOption: null });
+    }
+    this.setState({ isEnabledXss: !this.state.isEnabledXss });
+  }
+
+}

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

@@ -13,6 +13,8 @@ module.exports = (crowi) => {
 
   router.use('/healthcheck', require('./healthcheck')(crowi));
 
+  router.use('/markdown-setting', require('./markdown-setting')(crowi));
+
   router.use('/users', require('./users')(crowi));
 
   router.use('/user-groups', require('./user-group')(crowi));

+ 26 - 0
src/server/routes/apiv3/markdown-setting.js

@@ -0,0 +1,26 @@
+/* eslint-disable no-unused-vars */
+const loggerFactory = require('@alias/logger');
+
+const logger = loggerFactory('growi:routes:apiv3:user-group');
+
+const express = require('express');
+
+const router = express.Router();
+
+/**
+ * @swagger
+ *  tags:
+ *    name: MarkDownSetting
+ */
+
+module.exports = (crowi) => {
+  const loginRequiredStrictly = require('../../middleware/login-required')(crowi);
+  const adminRequired = require('../../middleware/admin-required')(crowi);
+
+  const {
+    ErrorV3,
+    Config,
+  } = crowi.models;
+
+  return router;
+};

+ 1 - 1
src/server/routes/index.js

@@ -91,7 +91,7 @@ module.exports = function(crowi, app) {
   app.post('/passport/saml/callback'              , loginPassport.loginPassportSamlCallback);
 
   // markdown admin
-  app.get('/admin/markdown'                   , loginRequiredStrictly , adminRequired , admin.markdown.index);
+  app.get('/admin/markdown'                   , loginRequiredStrictly , adminRequired , admin.markdown.index); // TODO delete
   app.post('/_api/admin/markdown/lineBreaksSetting', loginRequiredStrictly , adminRequired , csrf, form.admin.markdown, admin.markdown.lineBreaksSetting); // change form name
   app.post('/admin/markdown/xss-setting'      , loginRequiredStrictly , adminRequired , csrf, form.admin.markdownXss, admin.markdown.xssSetting);
   app.post('/admin/markdown/presentationSetting', loginRequiredStrictly , adminRequired , csrf, form.admin.markdownPresentation, admin.markdown.presentationSetting);