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

Merge branch 'imprv/reactify-admin' into imprv/reactify-admin-user-groups-detail

mizozobu 6 лет назад
Родитель
Сommit
c0dcd580ab

+ 2 - 2
package.json

@@ -32,8 +32,8 @@
     "clean:report": "rimraf -- report",
     "clean": "npm-run-all -p clean:*",
     "heroku-postbuild": "sh bin/heroku/install-plugins.sh && npm run build:prod",
-    "lint:js:fix": "eslint **/*.{js,jsx} --fix",
-    "lint:js": "eslint **/*.{js,jsx}",
+    "lint:js:fix": "eslint '**/*.{js,jsx}' --fix",
+    "lint:js": "eslint '**/*.{js,jsx}'",
     "lint:styles:fix": "prettier-stylelint --quiet --write src/client/styles/scss/**/*.scss",
     "lint:styles": "stylelint src/client/styles/scss/**/*.scss",
     "lint": "npm-run-all -p lint:js lint:styles",

+ 8 - 0
resource/locales/en-US/translation.json

@@ -736,5 +736,13 @@
     "import": "Import",
     "page_skip": "Pages with a name that already exists on GROWI are not imported",
     "Directory_hierarchy_tag": "Directory Hierarchy Tag"
+  },
+
+  "full_text_search_management":{
+    "elasticsearch_management":"Elasticsearch Management",
+    "build_button":"Rebuild Index",
+    "rebuild_description_1":"Force rebuild index.",
+    "rebuild_description_2":"Click 'Build Now' to delete and create mapping file and add all pages.",
+    "rebuild_description_3":"This may take a while."
   }
 }

+ 8 - 0
resource/locales/ja/translation.json

@@ -721,5 +721,13 @@
     "import": "インポート",
     "page_skip": "既に GROWI 側に同名のページが存在する場合、そのページはスキップされます",
     "Directory_hierarchy_tag": "ディレクトリ階層タグ"
+  },
+
+  "full_text_search_management":{
+    "elasticsearch_management":"Elasticsearch 管理",
+    "build_button":"インデックスのリビルド",
+    "rebuild_description_1":"Build Now ボタンを押すと全てのページのインデックスを削除し、作り直します。",
+    "rebuild_description_2":"この作業には数秒かかります。",
+    "rebuild_description_3":""
   }
 }

+ 15 - 0
src/client/js/app.js

@@ -39,7 +39,10 @@ import CustomCssEditor from './components/Admin/CustomCssEditor';
 import CustomScriptEditor from './components/Admin/CustomScriptEditor';
 import CustomHeaderEditor from './components/Admin/CustomHeaderEditor';
 import AdminRebuildSearch from './components/Admin/AdminRebuildSearch';
+import UserPage from './components/Admin/Users/Users';
 import UserGroupPage from './components/Admin/UserGroup/UserGroupPage';
+import Importer from './components/Admin/Importer';
+import FullTextSearchManagement from './components/Admin/FullTextSearchManagement/FullTextSearchPage';
 
 import AppContainer from './services/AppContainer';
 import PageContainer from './services/PageContainer';
@@ -99,6 +102,9 @@ let componentMappings = {
   'user-created-list': <RecentCreated />,
   'user-draft-list': <MyDraftList />,
 
+  'admin-user-page': <UserPage />,
+  'admin-full-text-search-management': <FullTextSearchManagement />,
+
   'staff-credit': <StaffCredit />,
 };
 
@@ -205,6 +211,15 @@ if (adminUserGroupPageElem != null) {
   );
 }
 
+const adminImporterElem = document.getElementById('admin-importer');
+if (adminImporterElem != null) {
+  ReactDOM.render(
+    <Importer />,
+    adminImporterElem,
+  );
+}
+
+
 // うわーもうー (commented by Crowi team -- 2018.03.23 Yuki Takei)
 $('a[data-toggle="tab"][href="#revision-history"]').on('show.bs.tab', () => {
   ReactDOM.render(

+ 3 - 3
src/client/js/components/Admin/AdminRebuildSearch.jsx

@@ -2,7 +2,7 @@ import React from 'react';
 import PropTypes from 'prop-types';
 
 import { createSubscribedElement } from '../UnstatedUtils';
-import WebsocketContainer from '../../services/AppContainer';
+import WebsocketContainer from '../../services/WebsocketContainer';
 
 class AdminRebuildSearch extends React.Component {
 
@@ -18,7 +18,7 @@ class AdminRebuildSearch extends React.Component {
   }
 
   componentDidMount() {
-    const socket = this.props.webspcketContainer.getWebSocket();
+    const socket = this.props.websocketContainer.getWebSocket();
 
     socket.on('admin:addPageProgress', (data) => {
       const newStates = Object.assign(data, { isCompleted: false });
@@ -76,7 +76,7 @@ const AdminRebuildSearchWrapper = (props) => {
 };
 
 AdminRebuildSearch.propTypes = {
-  webspcketContainer: PropTypes.instanceOf(WebsocketContainer).isRequired,
+  websocketContainer: PropTypes.instanceOf(WebsocketContainer).isRequired,
 };
 
 export default AdminRebuildSearchWrapper;

+ 75 - 0
src/client/js/components/Admin/FullTextSearchManagement/FullTextSearchPage.jsx

@@ -0,0 +1,75 @@
+import React, { Fragment } from 'react';
+import PropTypes from 'prop-types';
+import { withTranslation } from 'react-i18next';
+
+import AdminRebuildSearch from '../AdminRebuildSearch';
+import AppContainer from '../../../services/AppContainer';
+
+import { createSubscribedElement } from '../../UnstatedUtils';
+import { toastSuccess, toastError } from '../../../util/apiNotification';
+
+
+class FullTextSearchManagement extends React.Component {
+
+  constructor(props) {
+    super(props);
+
+    this.buildIndex = this.buildIndex.bind(this);
+  }
+
+  async buildIndex() {
+
+    const { appContainer } = this.props;
+    const pageId = this.pageId;
+
+    try {
+      const res = await appContainer.apiPost('/admin/search/build', { page_id: pageId });
+      if (!res.ok) {
+        throw new Error(res.message);
+      }
+      else {
+        toastSuccess('Building request is successfully posted.');
+      }
+    }
+    catch (e) {
+      toastError(e, (new Error('エラーが発生しました')));
+    }
+  }
+
+  render() {
+    const { t } = this.props;
+
+    return (
+      <Fragment>
+        <fieldset>
+          <legend> { t('full_text_search_management.elasticsearch_management') } </legend>
+          <div className="form-group form-horizontal">
+            <div className="col-xs-3 control-label"></div>
+            <div className="col-xs-7">
+              <button type="submit" className="btn btn-inverse" onClick={this.buildIndex}>{ t('full_text_search_management.build_button') }</button>
+              <p className="help-block">
+                { t('full_text_search_management.rebuild_description_1') }<br />
+                { t('full_text_search_management.rebuild_description_2') }<br />
+                { t('full_text_search_management.rebuild_description_3') }<br />
+              </p>
+            </div>
+          </div>
+        </fieldset>
+
+        <AdminRebuildSearch />
+      </Fragment>
+    );
+  }
+
+}
+
+const FullTextSearchManagementWrapper = (props) => {
+  return createSubscribedElement(FullTextSearchManagement, props, [AppContainer]);
+};
+
+FullTextSearchManagement.propTypes = {
+  t: PropTypes.func.isRequired, // i18next
+  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
+};
+
+export default withTranslation()(FullTextSearchManagementWrapper);

+ 19 - 0
src/client/js/components/Admin/Importer.jsx

@@ -0,0 +1,19 @@
+import React, { Fragment } from 'react';
+
+class Importer extends React.Component {
+
+  constructor(props) {
+    super();
+  }
+
+  render() {
+    return (
+      <Fragment>
+        <h1>連打</h1>
+      </Fragment>
+    );
+  }
+
+}
+
+export default Importer;

+ 27 - 3
src/client/js/components/Admin/UserGroup/UserGroupPage.jsx

@@ -1,6 +1,7 @@
 import React, { Fragment } from 'react';
 import PropTypes from 'prop-types';
 
+import PaginationWrapper from '../../PaginationWrapper';
 import UserGroupTable from './UserGroupTable';
 import UserGroupCreateForm from './UserGroupCreateForm';
 import UserGroupDeleteModal from './UserGroupDeleteModal';
@@ -19,10 +20,14 @@ class UserGroupPage extends React.Component {
       userGroupRelations: {},
       selectedUserGroup: undefined, // not null but undefined (to use defaultProps in UserGroupDeleteModal)
       isDeleteModalShow: false,
+      activePage: 1,
+      totalUserGroups: 0,
+      pagingLimit: Infinity,
     };
 
     this.xss = window.xss;
 
+    this.handlePage = this.handlePage.bind(this);
     this.showDeleteModal = this.showDeleteModal.bind(this);
     this.hideDeleteModal = this.hideDeleteModal.bind(this);
     this.addUserGroup = this.addUserGroup.bind(this);
@@ -96,23 +101,35 @@ class UserGroupPage extends React.Component {
     }
   }
 
+  async handlePage(selectedPage) {
+    await this.setState({ activePage: selectedPage });
+    await this.syncUserGroupAndRelations();
+  }
+
   async syncUserGroupAndRelations() {
     let userGroups = [];
     let userGroupRelations = {};
+    let totalUserGroups = 0;
+    let pagingLimit = Infinity;
 
     try {
+      const params = { page: this.state.activePage };
       const responses = await Promise.all([
-        this.props.appContainer.apiv3.get('/user-groups'),
-        this.props.appContainer.apiv3.get('/user-group-relations'),
+        this.props.appContainer.apiv3.get('/user-groups', params),
+        this.props.appContainer.apiv3.get('/user-group-relations', params),
       ]);
 
       const [userGroupsRes, userGroupRelationsRes] = responses;
       userGroups = userGroupsRes.data.userGroups;
+      totalUserGroups = userGroupsRes.data.totalUserGroups;
+      pagingLimit = userGroupsRes.data.pagingLimit;
       userGroupRelations = userGroupRelationsRes.data.userGroupRelations;
 
       this.setState({
         userGroups,
         userGroupRelations,
+        totalUserGroups,
+        pagingLimit,
       });
     }
     catch (err) {
@@ -129,10 +146,17 @@ class UserGroupPage extends React.Component {
         />
         <UserGroupTable
           userGroups={this.state.userGroups}
-          userGroupRelations={this.state.userGroupRelations}
           isAclEnabled={this.props.isAclEnabled}
           onDelete={this.showDeleteModal}
+          userGroupRelations={this.state.userGroupRelations}
         />
+        <PaginationWrapper
+          activePage={this.state.activePage}
+          changePage={this.handlePage}
+          totalItemsCount={this.state.totalUserGroups}
+          pagingLimit={this.state.pagingLimit}
+        >
+        </PaginationWrapper>
         <UserGroupDeleteModal
           userGroups={this.state.userGroups}
           deleteUserGroup={this.state.selectedUserGroup}

+ 15 - 3
src/client/js/components/Admin/UserGroup/UserGroupTable.jsx

@@ -13,13 +13,25 @@ class UserGroupTable extends React.Component {
 
     this.xss = window.xss;
 
+    this.state = {
+      userGroups: this.props.userGroups,
+      userGroupRelations: this.props.userGroupRelations,
+    };
+
     this.onDelete = this.onDelete.bind(this);
   }
 
+  componentWillReceiveProps(nextProps) {
+    this.setState({
+      userGroups: nextProps.userGroups,
+      userGroupRelations: nextProps.userGroupRelations,
+    });
+  }
+
   onDelete(e) {
     const { target } = e;
     const groupId = target.getAttribute('data-user-group-id');
-    const group = this.props.userGroups.find((group) => {
+    const group = this.state.userGroups.find((group) => {
       return group._id === groupId;
     });
 
@@ -43,7 +55,7 @@ class UserGroupTable extends React.Component {
             </tr>
           </thead>
           <tbody>
-            {this.props.userGroups.map((group) => {
+            {this.state.userGroups.map((group) => {
               return (
                 <tr key={group._id}>
                   {this.props.isAclEnabled
@@ -56,7 +68,7 @@ class UserGroupTable extends React.Component {
                   }
                   <td>
                     <ul className="list-inline">
-                      {this.props.userGroupRelations[group._id].map((user) => {
+                      {this.state.userGroupRelations[group._id].map((user) => {
                         return <li key={user._id} className="list-inline-item badge badge-primary">{this.xss.process(user.username)}</li>;
                       })}
                     </ul>

+ 33 - 0
src/client/js/components/Admin/Users/InviteUserControl.jsx

@@ -0,0 +1,33 @@
+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 InviteUserControl extends React.Component {
+
+  render() {
+    const { t } = this.props;
+
+    return (
+      <Fragment>
+        <button type="button" data-toggle="collapse" className="btn btn-default">
+          { t('user_management.invite_users') }
+        </button>
+      </Fragment>
+    );
+  }
+
+}
+
+const InviteUserControlWrapper = (props) => {
+  return createSubscribedElement(InviteUserControl, props, [AppContainer]);
+};
+
+InviteUserControl.propTypes = {
+  t: PropTypes.func.isRequired, // i18next
+  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
+};
+
+export default withTranslation()(InviteUserControlWrapper);

+ 47 - 0
src/client/js/components/Admin/Users/UserTable.jsx

@@ -0,0 +1,47 @@
+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 UserTable extends React.Component {
+
+
+  render() {
+    const { t } = this.props;
+
+    return (
+      <Fragment>
+        <h2>{ t('User_Management') }</h2>
+
+        <table className="table table-default table-bordered table-user-list">
+          <thead>
+            <tr>
+              <th width="100px">#</th>
+              <th>{ t('status') }</th>
+              <th><code>{ t('User') }</code></th>
+              <th>{ t('Name') }</th>
+              <th>{ t('Email') }</th>
+              <th width="100px">{ t('Created') }</th>
+              <th width="150px">{ t('Last_Login') }</th>
+              <th width="70px"></th>
+            </tr>
+          </thead>
+        </table>
+      </Fragment>
+    );
+  }
+
+}
+
+const UserTableWrapper = (props) => {
+  return createSubscribedElement(UserTable, props, [AppContainer]);
+};
+
+UserTable.propTypes = {
+  t: PropTypes.func.isRequired, // i18next
+  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
+};
+
+export default withTranslation()(UserTableWrapper);

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

@@ -0,0 +1,46 @@
+import React, { Fragment } from 'react';
+import PropTypes from 'prop-types';
+import { withTranslation } from 'react-i18next';
+
+import InviteUserControl from './InviteUserControl';
+import UserTable from './UserTable';
+
+import AppContainer from '../../../services/AppContainer';
+import { createSubscribedElement } from '../../UnstatedUtils';
+
+class UserPage extends React.Component {
+
+  constructor(props) {
+    super();
+
+  }
+
+  render() {
+    const { t } = this.props;
+
+    return (
+      <Fragment>
+        <p>
+          <InviteUserControl />
+          <a className="btn btn-default btn-outline ml-2" href="/admin/users/external-accounts">
+            <i className="icon-user-follow" aria-hidden="true"></i>
+            { t('user_management.external_account') }
+          </a>
+        </p>
+        <UserTable />
+      </Fragment>
+    );
+  }
+
+}
+
+const UserPageWrapper = (props) => {
+  return createSubscribedElement(UserPage, props, [AppContainer]);
+};
+
+UserPage.propTypes = {
+  t: PropTypes.func.isRequired, // i18next
+  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
+};
+
+export default withTranslation()(UserPageWrapper);

+ 23 - 121
src/client/js/components/MyDraftList/MyDraftList.jsx

@@ -3,13 +3,13 @@ import PropTypes from 'prop-types';
 
 import { withTranslation } from 'react-i18next';
 
-import Pagination from 'react-bootstrap/lib/Pagination';
-
 import { createSubscribedElement } from '../UnstatedUtils';
 import AppContainer from '../../services/AppContainer';
 import PageContainer from '../../services/PageContainer';
 import EditorContainer from '../../services/EditorContainer';
 
+import PaginationWrapper from '../PaginationWrapper';
+
 import Draft from './Draft';
 
 class MyDraftList extends React.Component {
@@ -21,14 +21,15 @@ class MyDraftList extends React.Component {
       drafts: [],
       currentDrafts: [],
       activePage: 1,
-      paginationNumbers: {},
+      totalDrafts: 0,
+      pagingLimit: Infinity,
     };
 
+    this.handlePage = this.handlePage.bind(this);
     this.getDraftsFromLocalStorage = this.getDraftsFromLocalStorage.bind(this);
     this.getCurrentDrafts = this.getCurrentDrafts.bind(this);
     this.clearDraft = this.clearDraft.bind(this);
     this.clearAllDrafts = this.clearAllDrafts.bind(this);
-    this.calculatePagination = this.calculatePagination.bind(this);
   }
 
   async componentWillMount() {
@@ -36,6 +37,11 @@ class MyDraftList extends React.Component {
     this.getCurrentDrafts(1);
   }
 
+  async handlePage(selectedPage) {
+    await this.getDraftsFromLocalStorage();
+    await this.getCurrentDrafts(selectedPage);
+  }
+
   async getDraftsFromLocalStorage() {
     const draftsAsObj = this.props.editorContainer.drafts;
 
@@ -53,7 +59,7 @@ class MyDraftList extends React.Component {
       };
     });
 
-    this.setState({ drafts });
+    this.setState({ drafts, totalDrafts: drafts.length });
   }
 
   getCurrentDrafts(selectPageNumber) {
@@ -61,16 +67,16 @@ class MyDraftList extends React.Component {
 
     const limit = appContainer.getConfig().recentCreatedLimit;
 
-    const totalCount = this.state.drafts.length;
+    const totalDrafts = this.state.drafts.length;
     const activePage = selectPageNumber;
-    const paginationNumbers = this.calculatePagination(limit, totalCount, activePage);
 
     const currentDrafts = this.state.drafts.slice((activePage - 1) * limit, activePage * limit);
 
     this.setState({
       currentDrafts,
       activePage,
-      paginationNumbers,
+      totalDrafts,
+      pagingLimit: limit,
     });
   }
 
@@ -112,125 +118,16 @@ class MyDraftList extends React.Component {
       drafts: [],
       currentDrafts: [],
       activePage: 1,
-      paginationNumbers: {},
+      totalDrafts: 0,
+      pagingLimit: Infinity,
     });
   }
 
-  calculatePagination(limit, totalCount, activePage) {
-    // calc totalPageNumber
-    const totalPage = Math.floor(totalCount / limit) + (totalCount % limit === 0 ? 0 : 1);
-
-    let paginationStart = activePage - 2;
-    let maxViewPageNum = activePage + 2;
-    // pagiNation Number area size = 5 , pageNuber calculate in here
-    // activePage Position calculate ex. 4 5 [6] 7 8 (Page8 over is Max), 3 4 5 [6] 7 (Page7 is Max)
-    if (paginationStart < 1) {
-      const diff = 1 - paginationStart;
-      paginationStart += diff;
-      maxViewPageNum = Math.min(totalPage, maxViewPageNum + diff);
-    }
-    if (maxViewPageNum > totalPage) {
-      const diff = maxViewPageNum - totalPage;
-      maxViewPageNum -= diff;
-      paginationStart = Math.max(1, paginationStart - diff);
-    }
-
-    return {
-      totalPage,
-      paginationStart,
-      maxViewPageNum,
-    };
-  }
-
-  /**
-   * generate Elements of Pagination First Prev
-   * ex.  <<   <   1  2  3  >  >>
-   * this function set << & <
-   */
-  generateFirstPrev(activePage) {
-    const paginationItems = [];
-    if (activePage !== 1) {
-      paginationItems.push(
-        <Pagination.First key="first" onClick={() => { return this.getCurrentDrafts(1) }} />,
-      );
-      paginationItems.push(
-        <Pagination.Prev key="prev" onClick={() => { return this.getCurrentDrafts(this.state.activePage - 1) }} />,
-      );
-    }
-    else {
-      paginationItems.push(
-        <Pagination.First key="first" disabled />,
-      );
-      paginationItems.push(
-        <Pagination.Prev key="prev" disabled />,
-      );
-
-    }
-    return paginationItems;
-  }
-
-  /**
-   * generate Elements of Pagination First Prev
-   *  ex. << < 4 5 6 7 8 > >>, << < 1 2 3 4 > >>
-   * this function set  numbers
-   */
-  generatePaginations(activePage, paginationStart, maxViewPageNum) {
-    const paginationItems = [];
-    for (let number = paginationStart; number <= maxViewPageNum; number++) {
-      paginationItems.push(
-        <Pagination.Item key={number} active={number === activePage} onClick={() => { return this.getCurrentDrafts(number) }}>{number}</Pagination.Item>,
-      );
-    }
-    return paginationItems;
-  }
-
-  /**
-   * generate Elements of Pagination First Prev
-   * ex.  <<   <   1  2  3  >  >>
-   * this function set > & >>
-   */
-  generateNextLast(activePage, totalPage) {
-    const paginationItems = [];
-    if (totalPage !== activePage) {
-      paginationItems.push(
-        <Pagination.Next key="next" onClick={() => { return this.getCurrentDrafts(this.state.activePage + 1) }} />,
-      );
-      paginationItems.push(
-        <Pagination.Last key="last" onClick={() => { return this.getCurrentDrafts(totalPage) }} />,
-      );
-    }
-    else {
-      paginationItems.push(
-        <Pagination.Next key="next" disabled />,
-      );
-      paginationItems.push(
-        <Pagination.Last key="last" disabled />,
-      );
-
-    }
-    return paginationItems;
-
-  }
-
   render() {
     const { t } = this.props;
 
     const draftList = this.generateDraftList(this.state.currentDrafts);
-
-    const paginationItems = [];
-
-    const totalCount = this.state.drafts.length;
-
-    const activePage = this.state.activePage;
-    const totalPage = this.state.paginationNumbers.totalPage;
-    const paginationStart = this.state.paginationNumbers.paginationStart;
-    const maxViewPageNum = this.state.paginationNumbers.maxViewPageNum;
-    const firstPrevItems = this.generateFirstPrev(activePage);
-    paginationItems.push(firstPrevItems);
-    const paginations = this.generatePaginations(activePage, paginationStart, maxViewPageNum);
-    paginationItems.push(paginations);
-    const nextLastItems = this.generateNextLast(activePage, totalPage);
-    paginationItems.push(nextLastItems);
+    const totalCount = this.state.totalDrafts;
 
     return (
       <div className="page-list-container-create">
@@ -254,7 +151,12 @@ class MyDraftList extends React.Component {
             <div className="tab-pane m-t-30 accordion" id="draft-list">
               {draftList}
             </div>
-            <Pagination bsSize="small">{paginationItems}</Pagination>
+            <PaginationWrapper
+              activePage={this.state.activePage}
+              changePage={this.handlePage}
+              totalItemsCount={this.state.totalDrafts}
+              pagingLimit={this.state.pagingLimit}
+            />
           </React.Fragment>
         ) }
 

+ 174 - 0
src/client/js/components/PaginationWrapper.jsx

@@ -0,0 +1,174 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+
+import { withTranslation } from 'react-i18next';
+
+import Pagination from 'react-bootstrap/lib/Pagination';
+import { createSubscribedElement } from './UnstatedUtils';
+import AppContainer from '../services/AppContainer';
+
+class PaginationWrapper extends React.Component {
+
+  constructor(props) {
+    super(props);
+
+    this.state = {
+      totalItemsCount: 0,
+      activePage: 1,
+      paginationNumbers: {},
+      limit: Infinity,
+    };
+
+    this.calculatePagination = this.calculatePagination.bind(this);
+  }
+
+  componentWillReceiveProps(nextProps) {
+    this.setState({
+      activePage: nextProps.activePage,
+      totalItemsCount: nextProps.totalItemsCount,
+      limit: nextProps.pagingLimit,
+    }, () => {
+      const activePage = this.state.activePage;
+      const totalCount = this.state.totalItemsCount;
+      const limit = this.state.limit;
+      const paginationNumbers = this.calculatePagination(limit, totalCount, activePage);
+      this.setState({ paginationNumbers });
+    });
+  }
+
+  calculatePagination(limit, totalCount, activePage) {
+    // calc totalPageNumber
+    const totalPage = Math.floor(totalCount / limit) + (totalCount % limit === 0 ? 0 : 1);
+
+    let paginationStart = activePage - 2;
+    let maxViewPageNum = activePage + 2;
+    // pagiNation Number area size = 5 , pageNuber calculate in here
+    // activePage Position calculate ex. 4 5 [6] 7 8 (Page8 over is Max), 3 4 5 [6] 7 (Page7 is Max)
+    if (paginationStart < 1) {
+      const diff = 1 - paginationStart;
+      paginationStart += diff;
+      maxViewPageNum = Math.min(totalPage, maxViewPageNum + diff);
+    }
+    if (maxViewPageNum > totalPage) {
+      const diff = maxViewPageNum - totalPage;
+      maxViewPageNum -= diff;
+      paginationStart = Math.max(1, paginationStart - diff);
+    }
+
+    return {
+      totalPage,
+      paginationStart,
+      maxViewPageNum,
+    };
+  }
+
+  /**
+    * generate Elements of Pagination First Prev
+    * ex.  <<   <   1  2  3  >  >>
+    * this function set << & <
+    */
+  generateFirstPrev(activePage) {
+    const paginationItems = [];
+    if (activePage !== 1) {
+      paginationItems.push(
+        <Pagination.First key="first" onClick={() => { return this.props.changePage(1) }} />,
+      );
+      paginationItems.push(
+        <Pagination.Prev key="prev" onClick={() => { return this.props.changePage(activePage - 1) }} />,
+      );
+    }
+    else {
+      paginationItems.push(
+        <Pagination.First key="first" disabled />,
+      );
+      paginationItems.push(
+        <Pagination.Prev key="prev" disabled />,
+      );
+
+    }
+    return paginationItems;
+  }
+
+  /**
+   * generate Elements of Pagination First Prev
+   *  ex. << < 4 5 6 7 8 > >>, << < 1 2 3 4 > >>
+   * this function set  numbers
+   */
+  generatePaginations(activePage, paginationStart, maxViewPageNum) {
+    const paginationItems = [];
+    for (let number = paginationStart; number <= maxViewPageNum; number++) {
+      paginationItems.push(
+        <Pagination.Item key={number} active={number === activePage} onClick={() => { return this.props.changePage(number) }}>{number}</Pagination.Item>,
+      );
+    }
+    return paginationItems;
+  }
+
+  /**
+   * generate Elements of Pagination First Prev
+   * ex.  <<   <   1  2  3  >  >>
+   * this function set > & >>
+   */
+  generateNextLast(activePage, totalPage) {
+    const paginationItems = [];
+    if (totalPage !== activePage) {
+      paginationItems.push(
+        <Pagination.Next key="next" onClick={() => { return this.props.changePage(activePage + 1) }} />,
+      );
+      paginationItems.push(
+        <Pagination.Last key="last" onClick={() => { return this.props.changePage(totalPage) }} />,
+      );
+    }
+    else {
+      paginationItems.push(
+        <Pagination.Next key="next" disabled />,
+      );
+      paginationItems.push(
+        <Pagination.Last key="last" disabled />,
+      );
+
+    }
+    return paginationItems;
+
+  }
+
+  render() {
+    const paginationItems = [];
+
+    const activePage = this.state.activePage;
+    const totalPage = this.state.paginationNumbers.totalPage;
+    const paginationStart = this.state.paginationNumbers.paginationStart;
+    const maxViewPageNum = this.state.paginationNumbers.maxViewPageNum;
+    const firstPrevItems = this.generateFirstPrev(activePage);
+    paginationItems.push(firstPrevItems);
+    const paginations = this.generatePaginations(activePage, paginationStart, maxViewPageNum);
+    paginationItems.push(paginations);
+    const nextLastItems = this.generateNextLast(activePage, totalPage);
+    paginationItems.push(nextLastItems);
+
+    return (
+      <React.Fragment>
+        <div>
+          <Pagination bsSize="small">{paginationItems}</Pagination>
+        </div>
+      </React.Fragment>
+    );
+  }
+
+
+}
+
+const PaginationWrappered = (props) => {
+  return createSubscribedElement(PaginationWrapper, props, [AppContainer]);
+};
+
+PaginationWrapper.propTypes = {
+  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
+
+  activePage: PropTypes.number.isRequired,
+  changePage: PropTypes.func.isRequired,
+  totalItemsCount: PropTypes.number.isRequired,
+  pagingLimit: PropTypes.number.isRequired,
+};
+
+export default withTranslation()(PaginationWrappered);

+ 19 - 118
src/client/js/components/RecentCreated/RecentCreated.jsx

@@ -1,12 +1,12 @@
 import React from 'react';
 import PropTypes from 'prop-types';
 
-import Pagination from 'react-bootstrap/lib/Pagination';
-
 import { createSubscribedElement } from '../UnstatedUtils';
 import AppContainer from '../../services/AppContainer';
 import PageContainer from '../../services/PageContainer';
 
+import PaginationWrapper from '../PaginationWrapper';
+
 import Page from '../PageList/Page';
 
 class RecentCreated extends React.Component {
@@ -17,9 +17,11 @@ class RecentCreated extends React.Component {
     this.state = {
       pages: [],
       activePage: 1,
-      paginationNumbers: {},
+      totalPages: 0,
+      pagingLimit: Infinity,
     };
-    this.calculatePagination = this.calculatePagination.bind(this);
+
+    this.handlePage = this.handlePage.bind(this);
   }
 
 
@@ -27,6 +29,10 @@ class RecentCreated extends React.Component {
     this.getRecentCreatedList(1);
   }
 
+  async handlePage(selectedPage) {
+    await this.getRecentCreatedList(selectedPage);
+  }
+
   getRecentCreatedList(selectPageNumber) {
     const { appContainer, pageContainer } = this.props;
     const { pageId } = pageContainer.state;
@@ -40,45 +46,18 @@ class RecentCreated extends React.Component {
       page_id: pageId, user: userId, limit, offset,
     })
       .then((res) => {
-        const totalCount = res.totalCount;
+        const totalPages = res.totalCount;
         const pages = res.pages;
         const activePage = selectPageNumber;
-        // pagiNation calculate function call
-        const paginationNumbers = this.calculatePagination(limit, totalCount, activePage);
         this.setState({
           pages,
           activePage,
-          paginationNumbers,
+          totalPages,
+          pagingLimit: limit,
         });
       });
   }
 
-  calculatePagination(limit, totalCount, activePage) {
-    // calc totalPageNumber
-    const totalPage = Math.floor(totalCount / limit) + (totalCount % limit === 0 ? 0 : 1);
-
-    let paginationStart = activePage - 2;
-    let maxViewPageNum = activePage + 2;
-    // pagiNation Number area size = 5 , pageNuber calculate in here
-    // activePage Position calculate ex. 4 5 [6] 7 8 (Page8 over is Max), 3 4 5 [6] 7 (Page7 is Max)
-    if (paginationStart < 1) {
-      const diff = 1 - paginationStart;
-      paginationStart += diff;
-      maxViewPageNum = Math.min(totalPage, maxViewPageNum + diff);
-    }
-    if (maxViewPageNum > totalPage) {
-      const diff = maxViewPageNum - totalPage;
-      maxViewPageNum -= diff;
-      paginationStart = Math.max(1, paginationStart - diff);
-    }
-
-    return {
-      totalPage,
-      paginationStart,
-      maxViewPageNum,
-    };
-  }
-
   /**
    * generate Elements of Page
    *
@@ -92,98 +71,20 @@ class RecentCreated extends React.Component {
 
   }
 
-  /**
-   * generate Elements of Pagination First Prev
-   * ex.  <<   <   1  2  3  >  >>
-   * this function set << & <
-   */
-  generateFirstPrev(activePage) {
-    const paginationItems = [];
-    if (activePage !== 1) {
-      paginationItems.push(
-        <Pagination.First key="first" onClick={() => { return this.getRecentCreatedList(1) }} />,
-      );
-      paginationItems.push(
-        <Pagination.Prev key="prev" onClick={() => { return this.getRecentCreatedList(this.state.activePage - 1) }} />,
-      );
-    }
-    else {
-      paginationItems.push(
-        <Pagination.First key="first" disabled />,
-      );
-      paginationItems.push(
-        <Pagination.Prev key="prev" disabled />,
-      );
-
-    }
-    return paginationItems;
-  }
-
-  /**
-   * generate Elements of Pagination First Prev
-   *  ex. << < 4 5 6 7 8 > >>, << < 1 2 3 4 > >>
-   * this function set  numbers
-   */
-  generatePaginations(activePage, paginationStart, maxViewPageNum) {
-    const paginationItems = [];
-    for (let number = paginationStart; number <= maxViewPageNum; number++) {
-      paginationItems.push(
-        <Pagination.Item key={number} active={number === activePage} onClick={() => { return this.getRecentCreatedList(number) }}>{number}</Pagination.Item>,
-      );
-    }
-    return paginationItems;
-  }
-
-  /**
-   * generate Elements of Pagination First Prev
-   * ex.  <<   <   1  2  3  >  >>
-   * this function set > & >>
-   */
-  generateNextLast(activePage, totalPage) {
-    const paginationItems = [];
-    if (totalPage !== activePage) {
-      paginationItems.push(
-        <Pagination.Next key="next" onClick={() => { return this.getRecentCreatedList(this.state.activePage + 1) }} />,
-      );
-      paginationItems.push(
-        <Pagination.Last key="last" onClick={() => { return this.getRecentCreatedList(totalPage) }} />,
-      );
-    }
-    else {
-      paginationItems.push(
-        <Pagination.Next key="next" disabled />,
-      );
-      paginationItems.push(
-        <Pagination.Last key="last" disabled />,
-      );
-
-    }
-    return paginationItems;
-
-  }
-
   render() {
     const pageList = this.generatePageList(this.state.pages);
 
-    const paginationItems = [];
-
-    const activePage = this.state.activePage;
-    const totalPage = this.state.paginationNumbers.totalPage;
-    const paginationStart = this.state.paginationNumbers.paginationStart;
-    const maxViewPageNum = this.state.paginationNumbers.maxViewPageNum;
-    const firstPrevItems = this.generateFirstPrev(activePage);
-    paginationItems.push(firstPrevItems);
-    const paginations = this.generatePaginations(activePage, paginationStart, maxViewPageNum);
-    paginationItems.push(paginations);
-    const nextLastItems = this.generateNextLast(activePage, totalPage);
-    paginationItems.push(nextLastItems);
-
     return (
       <div className="page-list-container-create">
         <ul className="page-list-ul page-list-ul-flat">
           {pageList}
         </ul>
-        <Pagination bsSize="small">{paginationItems}</Pagination>
+        <PaginationWrapper
+          activePage={this.state.activePage}
+          changePage={this.handlePage}
+          totalItemsCount={this.state.totalPages}
+          pagingLimit={this.state.pagingLimit}
+        />
       </div>
     );
   }

+ 19 - 117
src/client/js/components/TagsList.jsx

@@ -2,7 +2,8 @@ import React from 'react';
 import PropTypes from 'prop-types';
 
 import { withTranslation } from 'react-i18next';
-import Pagination from 'react-bootstrap/lib/Pagination';
+
+import PaginationWrapper from './PaginationWrapper';
 
 class TagsList extends React.Component {
 
@@ -12,59 +13,38 @@ class TagsList extends React.Component {
     this.state = {
       tagData: [],
       activePage: 1,
-      paginationNumbers: {},
+      totalTags: 0,
+      pagingLimit: 10,
     };
 
-    this.calculatePagination = this.calculatePagination.bind(this);
+    this.handlePage = this.handlePage.bind(this);
+    this.getTagList = this.getTagList.bind(this);
   }
 
   async componentWillMount() {
     await this.getTagList(1);
   }
 
+  async handlePage(selectedPage) {
+    await this.getTagList(selectedPage);
+  }
+
   async getTagList(selectPageNumber) {
-    const limit = 10;
+    const limit = this.state.pagingLimit;
     const offset = (selectPageNumber - 1) * limit;
     const res = await this.props.crowi.apiGet('/tags.list', { limit, offset });
 
-    const totalCount = res.totalCount;
+    const totalTags = res.totalCount;
     const tagData = res.data;
     const activePage = selectPageNumber;
-    const paginationNumbers = this.calculatePagination(limit, totalCount, activePage);
 
     this.setState({
       tagData,
       activePage,
-      paginationNumbers,
+      totalTags,
     });
   }
 
-  calculatePagination(limit, totalCount, activePage) {
-    // calc totalPageNumber
-    const totalPage = Math.floor(totalCount / limit) + (totalCount % limit === 0 ? 0 : 1);
-
-    let paginationStart = activePage - 2;
-    let maxViewPageNum = activePage + 2;
-    // pagination Number area size = 5 , pageNumber calculate in here
-    // activePage Position calculate ex. 4 5 [6] 7 8 (Page8 over is Max), 3 4 5 [6] 7 (Page7 is Max)
-    if (paginationStart < 1) {
-      const diff = 1 - paginationStart;
-      paginationStart += diff;
-      maxViewPageNum = Math.min(totalPage, maxViewPageNum + diff);
-    }
-    if (maxViewPageNum > totalPage) {
-      const diff = maxViewPageNum - totalPage;
-      maxViewPageNum -= diff;
-      paginationStart = Math.max(1, paginationStart - diff);
-    }
-
-    return {
-      totalPage,
-      paginationStart,
-      maxViewPageNum,
-    };
-  }
-
   /**
    * generate Elements of Tag
    *
@@ -82,93 +62,10 @@ class TagsList extends React.Component {
     });
   }
 
-  /**
-   * generate Elements of Pagination First Prev
-   * ex.  <<   <   1  2  3  >  >>
-   * this function set << & <
-   */
-  generateFirstPrev(activePage) {
-    const paginationItems = [];
-    if (activePage !== 1) {
-      paginationItems.push(
-        <Pagination.First key="first" onClick={() => { return this.getTagList(1) }} />,
-      );
-      paginationItems.push(
-        <Pagination.Prev key="prev" onClick={() => { return this.getTagList(this.state.activePage - 1) }} />,
-      );
-    }
-    else {
-      paginationItems.push(
-        <Pagination.First key="first" disabled />,
-      );
-      paginationItems.push(
-        <Pagination.Prev key="prev" disabled />,
-      );
-
-    }
-    return paginationItems;
-  }
-
-  /**
-   * generate Elements of Pagination First Prev
-   *  ex. << < 4 5 6 7 8 > >>, << < 1 2 3 4 > >>
-   * this function set  numbers
-   */
-  generatePaginations(activePage, paginationStart, maxViewPageNum) {
-    const paginationItems = [];
-    for (let number = paginationStart; number <= maxViewPageNum; number++) {
-      paginationItems.push(
-        <Pagination.Item key={number} active={number === activePage} onClick={() => { return this.getTagList(number) }}>{number}</Pagination.Item>,
-      );
-    }
-    return paginationItems;
-  }
-
-  /**
-   * generate Elements of Pagination First Prev
-   * ex.  <<   <   1  2  3  >  >>
-   * this function set > & >>
-   */
-  generateNextLast(activePage, totalPage) {
-    const paginationItems = [];
-    if (totalPage !== activePage) {
-      paginationItems.push(
-        <Pagination.Next key="next" onClick={() => { return this.getTagList(this.state.activePage + 1) }} />,
-      );
-      paginationItems.push(
-        <Pagination.Last key="last" onClick={() => { return this.getTagList(totalPage) }} />,
-      );
-    }
-    else {
-      paginationItems.push(
-        <Pagination.Next key="next" disabled />,
-      );
-      paginationItems.push(
-        <Pagination.Last key="last" disabled />,
-      );
-
-    }
-    return paginationItems;
-  }
-
   render() {
     const { t } = this.props;
     const messageForNoTag = this.state.tagData.length ? null : <h3>{ t('You have no tag, You can set tags on pages') }</h3>;
 
-    const paginationItems = [];
-
-    const activePage = this.state.activePage;
-    const totalPage = this.state.paginationNumbers.totalPage;
-    const paginationStart = this.state.paginationNumbers.paginationStart;
-    const maxViewPageNum = this.state.paginationNumbers.maxViewPageNum;
-    const firstPrevItems = this.generateFirstPrev(activePage);
-    paginationItems.push(firstPrevItems);
-    const paginations = this.generatePaginations(activePage, paginationStart, maxViewPageNum);
-    paginationItems.push(paginations);
-    const nextLastItems = this.generateNextLast(activePage, totalPage);
-    paginationItems.push(nextLastItems);
-    const pagination = this.state.tagData.length ? <Pagination>{paginationItems}</Pagination> : null;
-
     return (
       <div className="text-center">
         <div className="tag-list">
@@ -178,7 +75,12 @@ class TagsList extends React.Component {
           {messageForNoTag}
         </div>
         <div className="tag-list-pagination">
-          {pagination}
+          <PaginationWrapper
+            activePage={this.state.activePage}
+            changePage={this.handlePage}
+            totalItemsCount={this.state.totalTags}
+            pagingLimit={this.state.pagingLimit}
+          />
         </div>
       </div>
     );

+ 4 - 0
src/server/models/user-group.js

@@ -108,6 +108,10 @@ class UserGroup {
     return deletedGroup;
   }
 
+  static countUserGroups() {
+    return this.estimatedDocumentCount();
+  }
+
   // グループ生成(名前が要る)
   static createGroupByName(name) {
     return this.create({ name });

+ 4 - 2
src/server/routes/apiv3/user-group.js

@@ -50,8 +50,10 @@ module.exports = (crowi) => {
   router.get('/', loginRequired(), adminRequired, async(req, res) => {
     // TODO: filter with querystring
     try {
-      const userGroups = await UserGroup.find();
-      return res.apiv3({ userGroups });
+      const page = parseInt(req.query.page) || 1;
+      const result = await UserGroup.findUserGroupsWithPagination({ page });
+      const { docs: userGroups, total: totalUserGroups, limit: pagingLimit } = result;
+      return res.apiv3({ userGroups, totalUserGroups, pagingLimit });
     }
     catch (err) {
       const msg = 'Error occurred in fetching user group list';

+ 321 - 0
src/server/views/admin/Users_reserve.html

@@ -0,0 +1,321 @@
+{% extends '../layout/admin.html' %}
+
+{% block html_title %}{{ customizeService.generateCustomTitle(t('User_Management')) }}{% endblock %}
+
+{% block content_header %}
+<div class="header-wrap">
+  <header id="page-header">
+    <h1 id="admin-title" class="title">{{ t('User_Management') }}</h1>
+  </header>
+</div>
+{% endblock %}
+
+{% block content_main %}
+<div class="content-main">
+  {% set smessage = req.flash('successMessage') %}
+  {% if smessage.length %}
+  <div class="alert alert-success">
+    {{ smessage }}
+  </div>
+  {% endif %}
+
+  {% set emessage = req.flash('errorMessage') %}
+  {% if emessage.length %}
+  <div class="alert alert-danger">
+    {{ emessage }}
+  </div>
+  {% endif %}
+
+  <div class="row">
+    <div class="col-md-3">
+      {% include './widget/menu.html' with {current: 'user'} %}
+    </div>
+
+    <div class="col-md-9">
+      <p>
+        <button data-toggle="collapse" class="btn btn-default" href="#inviteUserForm" {% if isUserCountExceedsUpperLimit %}disabled{% endif %}>
+          {{ t("user_management.invite_users") }}
+        </button>
+        <a class="btn btn-default btn-outline" href="/admin/users/external-accounts">
+          <i class="icon-user-follow" aria-hidden="true"></i>
+          {{ t("user_management.external_account") }}
+        </a>
+      </p>
+      <form role="form" action="/admin/user/invite" method="post">
+        <div id="inviteUserForm" class="collapse">
+          <div class="form-group">
+            <label for="inviteForm[emailList]">{{ t('user_management.emails') }}</label>
+            <textarea class="form-control" name="inviteForm[emailList]" placeholder="{{ t('eg') }} user@growi.org"></textarea>
+          </div>
+          <div class="checkbox checkbox-info">
+            <input type="checkbox" id="inviteWithEmail" name="inviteForm[sendEmail]" checked>
+            <label for="inviteWithEmail">{{ t('user_management.invite_thru_email') }}</label>
+          </div>
+          <button type="submit" class="btn btn-primary">{{ t('user_management.invite') }}</button>
+        </div>
+        <input type="hidden" name="_csrf" value="{{ csrf() }}">
+      </form>
+
+      {% if isUserCountExceedsUpperLimit === true %}
+      <label>{{ t('user_management.cannot_invite_maximum_users') }}</label>
+      {% endif %}
+      {% if userUpperLimit !== 0 %}
+      <label>{{ t('user_management.current_users') }}{{ activeUsers }}</label>
+      {% endif %}
+
+      {% set createdUser = req.flash('createdUser') %}
+      {% if createdUser.length %}
+      <div class="modal fade in" id="createdUserModal">
+        <div class="modal-dialog">
+          <div class="modal-content">
+
+            <div class="modal-header">
+              <button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
+              <div class="modal-title">{{ t('user_management.invited') }}</div>
+            </div>
+
+            <div class="modal-body">
+              <p>
+                {{ t('user_management.temporary_password') }}<br>
+                {{ t('user_management.password_never_seen') }}<span class="text-danger">{{ t('user_management.send_temporary_password') }}</span>
+              </p>
+
+              <pre>{% for cUser in createdUser %}{% if cUser.user %}{{ cUser.email }} {{ cUser.password }}<br>{% else %}{{ cUser.email }} 作成失敗<br>{% endif %}{% endfor %}</pre>
+            </div>
+
+          </div><!-- /.modal-content -->
+        </div><!-- /.modal-dialog -->
+      </div><!-- /.modal -->
+      {% endif %}
+
+      {# FIXME とりあえずクソ実装。React化はやくしたいなー(チラッチラッ #}
+      <div class="modal fade" id="admin-password-reset-modal">
+        <div class="modal-dialog">
+          <div class="modal-content">
+            <div class="modal-header">
+              <button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
+              <div class="modal-title">{{ t('user_management.reset_password')}}</div>
+            </div>
+
+            <div class="modal-body">
+              <p>
+                {{ t('user_management.password_never_seen') }}<br>
+              <span class="text-danger">{{ t('user_management.send_new_password') }}</span>
+              </p>
+              <p>
+              {{ t('user_management.target_user') }}: <code id="admin-password-reset-user"></code>
+              </p>
+
+              <form method="post" id="admin-users-reset-password">
+                <input type="hidden" name="user_id" value="">
+                <input type="hidden" name="_csrf" value="{{ csrf() }}">
+                <button type="submit" value="" class="btn btn-primary">
+                  {{ t('user_management.reset_password')}}
+                </button>
+              </form>
+
+            </div>
+
+          </div><!-- /.modal-content -->
+        </div>/.modal-dialog
+      </div>
+      <div class="modal fade" id="admin-password-reset-modal-done">
+        <div class="modal-dialog">
+          <div class="modal-content">
+
+            <div class="modal-header">
+              <button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
+              <div class="modal-title">{{ t('user_management.reset_password') }}</div>
+            </div>
+
+            <div class="modal-body">
+              <p class="alert alert-danger">Let the user know the new password below and strongly recommend to change another one immediately. </p>
+              <p>
+              Reset user: <code id="admin-password-reset-done-user"></code>
+              </p>
+              <p>
+              New password: <code id="admin-password-reset-done-password"></code>
+              </p>
+            </div>
+            <div class="modal-footer">
+              <button class="btn btn-primary" data-dismiss="modal">OK</button>
+            </div>
+          </div><!-- /.modal-content -->
+        </div><!-- /.modal-dialog -->
+      </div>
+
+      <h2>{{ t("User_Management") }}</h2>
+
+      <table class="table table-default table-bordered table-user-list">
+        <thead>
+          <tr>
+            <th width="100px">#</th>
+            <th>{{ t('status') }}</th>
+            <th><code>{{ t('User') }}</code></th>
+            <th>{{ t('Name') }}</th>
+            <th>{{ t('Email') }}</th>
+            <th width="100px">{{ t('Created') }}</th>
+            <th width="150px">{{ t('Last_Login') }}</th>
+            <th width="70px"></th>
+          </tr>
+        </thead>
+        <tbody>
+          {% for sUser in users %}
+          {% set sUserId = sUser._id.toString() %}
+          <tr>
+            <td>
+              <img src="{{ sUser|picture }}" class="picture img-circle" />
+              {% if sUser.admin %}
+              <span class="label label-inverse label-admin">
+              {{ t('administrator') }}
+              </span>
+              {% endif %}
+            </td>
+            <td>
+              <span class="label {{ css.userStatus(sUser) }}">
+                {{ consts.userStatus[sUser.status] }}
+              </span>
+            </td>
+            <td>
+              <strong>{{ sUser.username }}</strong>
+            </td>
+            <td>{{ sUser.name }}</td>
+            <td>{{ sUser.email }}</td>
+            <td>{{ sUser.createdAt|date('Y-m-d', sUser.createdAt.getTimezoneOffset()) }}</td>
+            <td>
+              {% if sUser.lastLoginAt %}
+                {{ sUser.lastLoginAt|date('Y-m-d H:i', sUser.createdAt.getTimezoneOffset()) }}
+              {% endif %}
+            </td>
+            <td>
+              <div class="btn-group admin-user-menu">
+                <button type="button" class="btn btn-sm btn-default dropdown-toggle" data-toggle="dropdown">
+                  <i class="icon-settings"></i> <span class="caret"></span>
+                </button>
+                <ul class="dropdown-menu" role="menu">
+                  <li class="dropdown-header">{{ t('user_management.edit_menu') }}</li>
+                  <li>
+                    <a href="#"
+                        data-user-id="{{ sUserId }}"
+                        data-user-email="{{ sUser.email }}"
+                        data-target="#admin-password-reset-modal"
+                        data-toggle="modal">
+                      <i class="icon-fw icon-key"></i>
+                      {{ t('user_management.reset_password') }}
+                    </a>
+                  </li>
+                  <li class="divider"></li>
+                  <li class="dropdown-header">{{ t('status') }}</li>
+
+                  {% if sUser.status == 1 %}
+                  <form id="form_activate_{{ sUserId }}" action="/admin/user/{{ sUserId }}/activate" method="post">
+                    <input type="hidden" name="_csrf" value="{{ csrf() }}">
+                  </form>
+                  <li>
+                    <a href="javascript:form_activate_{{ sUserId }}.submit()">
+                      <i class="icon-fw icon-user-following"></i> {{ t('user_management.accept') }}
+                    </a>
+                  </li>
+                  {% endif  %}
+
+                  {% if sUser.status == 2 %}
+                  <form id="form_suspend_{{ sUserId }}" action="/admin/user/{{ sUserId }}/suspend" method="post">
+                    <input type="hidden" name="_csrf" value="{{ csrf() }}">
+                  </form>
+                  <li>
+                    {% if sUser.username != user.username %}
+                    <a href="javascript:form_suspend_{{ sUserId }}.submit()">
+                      <i class="icon-fw icon-ban"></i>
+                      {{ t('user_management.deactivate_account') }}
+                    </a>
+                    {% else %}
+                    <a disabled>
+                      <i class="icon-fw icon-ban"></i>
+                      {{ t('user_management.deactivate_account') }}
+                    </a>
+                    <p class="alert alert-danger m-l-10 m-r-10 p-10">{{ t("user_management.your_own") }}</p>
+                    {% endif %}
+                  </li>
+                  {% endif %}
+
+                  {% if sUser.status == 3 %}
+                  <form id="form_activate_{{ sUserId }}" action="/admin/user/{{ sUserId }}/activate" method="post">
+                    <input type="hidden" name="_csrf" value="{{ csrf() }}">
+                  </form>
+                  <form id="form_remove_{{ sUserId }}" action="/admin/user/{{ sUserId }}/remove" method="post">
+                    <input type="hidden" name="_csrf" value="{{ csrf() }}">
+                  </form>
+                  <li>
+                    <a href="javascript:form_activate_{{ sUserId }}.submit()">
+                      <i class="icon-fw icon-action-redo"></i> {{ t('Undo') }}
+                    </a>
+                  </li>
+                  <li>
+                    {# label は同じだけど、こっちは論理削除 #}
+                    <a href="javascript:form_remove_{{ sUserId }}.submit()">
+                      <i class="icon-fw icon-fire text-danger"></i> {{ t('Delete') }}
+                    </a>
+                  </li>
+                  {% endif %}
+
+                  {% if sUser.status == 1 || sUser.status == 5 %}
+                  <form id="form_removeCompletely_{{ sUserId }}" action="/admin/user/{{ sUser._id.toString() }}/removeCompletely" method="post">
+                    <input type="hidden" name="_csrf" value="{{ csrf() }}">
+                  </form>
+                  <li class="dropdown-button">
+                    {# label は同じだけど、こっちは物理削除 #}
+                    <a href="javascript:form_removeCompletely_{{ sUserId }}.submit()">
+                      <i class="icon-fw icon-fire text-danger"></i> {{ t('Delete') }}
+                    </a>
+                  </li>
+                  {% endif %}
+
+                  {% if sUser.status == 2 %} {# activated な人だけこのメニューを表示 #}
+                  <li class="divider"></li>
+                  <li class="dropdown-header">{{ t('user_management.administrator_menu') }}</li>
+
+                  {% if sUser.admin %}
+                  <form id="form_removeFromAdmin_{{ sUserId }}" action="/admin/user/{{ sUser._id.toString() }}/removeFromAdmin" method="post">
+                    <input type="hidden" name="_csrf" value="{{ csrf() }}">
+                  </form>
+                  <li>
+                    {% if sUser.username != user.username %}
+                      <a href="javascript:form_removeFromAdmin_{{ sUserId }}.submit()">
+                        <i class="icon-fw icon-user-unfollow"></i> {{ t("user_management.remove_admin_access") }}
+                      </a>
+                    {% else %}
+                      <a disabled>
+                        <i class="icon-fw icon-user-unfollow"></i> {{ t("user_management.remove_admin_access") }}
+                      </a>
+                      <p class="alert alert-danger m-l-10 m-r-10 p-10">{{ t("user_management.cannot_remove") }}</p>
+                    {% endif %}
+                  </li>
+                  {% else %}
+                  <form id="form_makeAdmin_{{ sUserId }}" action="/admin/user/{{ sUser._id.toString() }}/makeAdmin" method="post">
+                    <input type="hidden" name="_csrf" value="{{ csrf() }}">
+                  </form>
+                  <li>
+                    <a href="javascript:form_makeAdmin_{{ sUserId }}.submit()">
+                      <i class="icon-fw icon-magic-wand"></i> {{ t("user_management.give_admin_access") }}
+                    </a>
+                  </li>
+                  {% endif %}
+
+                  {% endif %}
+                </ul>
+              </div>
+            </td>
+          </tr>
+          {% endfor %}
+        </tbody>
+      </table>
+
+      {% include '../widget/pager.html' with {path: "/admin/users", pager: pager} %}
+
+    </div>
+  </div>
+</div>
+{% endblock content_main %}
+
+{% block content_footer %}
+{% endblock content_footer %} -->

+ 2 - 259
src/server/views/admin/importer.html

@@ -1,3 +1,4 @@
+
 {% extends '../layout/admin.html' %}
 
 {% block html_title %}{{ customizeService.generateCustomTitle(t('Import Data')) }}{% endblock %}
@@ -11,265 +12,7 @@
 {% endblock %}
 
 {% block content_main %}
-<div class="content-main admin-importer">
-
-  <div class="row">
-    <div class="col-md-3">
-      {% include './widget/menu.html' with {current: 'importer'} %}
-    </div>
-    <div class="col-lg-7 col-md-9">
-
-      <!-- Flash message for success -->
-      {% set smessage = req.flash('successMessage') %}
-      {% if smessage.length %}
-      <div class="alert alert-success">
-        {% for e in smessage %}
-          {{ e }}<br>
-        {% endfor %}
-      </div>
-      {% endif %}
-
-      <!-- Flash message for error -->
-      {% set emessage = req.flash('errorMessage') %}
-      {% if emessage.length %}
-      <div class="alert alert-danger">
-        {% for e in emessage %}
-        {{ e }}<br>
-        {% endfor %}
-      </div>
-      {% endif %}
-
-      <!-- esa Importer management forms -->
-      <form action="/_api/admin/settings/importerEsa" method="post" class="form-horizontal" id="importerSettingFormEsa" role="form"
-          data-success-messaage="{{ ('Updated') }}">
-        <fieldset>
-          <legend>{{ t('importer_management.import_from', 'esa.io') }}</legend>
-          <table class="table table-bordered table-mapping">
-            <thead>
-              <tr>
-                <th width="45%">esa.io</th>
-                <th width="10%"></th>
-                <th>GROWI</th>
-              </tr>
-            </thead>
-            <tbody>
-              <tr>
-                <th>{{ t('Article') }}</th>
-                <th><i class="icon-arrow-right-circle text-success"></i></th>
-                <th>{{ t('Page') }}</th>
-              </tr>
-              <tr>
-                <th>{{ t('Category') }}</th>
-                <th><i class="icon-arrow-right-circle text-success"></i></th>
-                <th>{{ t('Page Path') }}</th>
-              </tr>
-              <tr>
-                <th>{{ t('User') }}</th>
-                <th></th>
-                <th>(TBD)</th>
-              </tr>
-            </tbody>
-          </table>
-          <div class="well well-sm mb-0 small">
-            <ul>
-              <li>{{ t("importer_management.page_skip") }}</li>
-            </ul>
-          </div>
-          <div class="form-group">
-            <input type="password" name="dummypass" style="display:none; top: -100px; left: -100px;" />
-          </div>
-          <div class="form-group">
-            <label for="settingForm[importer:esa:team_name]" class="col-xs-3 control-label">{{ t('importer_management.esa_settings.team_name') }}</label>
-            <div class="col-xs-6">
-              <input class="form-control" type="text" name="settingForm[importer:esa:team_name]" value="{{ settingForm['importer:esa:team_name'] | default('') }}">
-            </div>
-          </div>
-          <div class="form-group">
-            <label for="settingForm[importer:esa:access_token]" class="col-xs-3 control-label">{{ t('importer_management.esa_settings.access_token') }}</label>
-            <div class="col-xs-6">
-              <input class="form-control" type="password" name="settingForm[importer:esa:access_token]" value="{{ settingForm['importer:esa:access_token'] | default('') }}">
-            </div>
-          </div>
-          <div class="form-group">
-            <input type="hidden" name="_csrf" value="{{ csrf() }}" />
-            <div class="col-xs-offset-3 col-xs-6">
-              <button id="testConnectionToEsa" type="button" class="btn btn-primary btn-esa" data-action="/_api/admin/import/esa" name="Esa"
-                  data-success-message="Import posts from esa success." data-error-message="Error occurred in importing pages from esa.io">
-                {{ t("importer_management.import") }}
-              </button>
-              <button type="submit" class="btn btn-secondary">{# the first element is the default button to submit #}
-                {{ t('Update') }}
-              </button>
-              <span class="col-xs-offset-1">
-                <button id="importFromEsa" type="button" class="btn btn-default btn-esa" data-action="/_api/admin/import/testEsaAPI" name="Esa"
-                    data-success-message="Test connection to esa success." data-error-message="Test connection to esa failed.">
-                  {{ t("importer_management.esa_settings.test_connection") }}
-                </button>
-              </span>
-            </div>
-          </div>
-        </fieldset>
-      </form>
-
-
-      <!-- qiita:team Importer management forms -->
-      <form action="/_api/admin/settings/importerQiita" method="post" class="form-horizontal mt-5" id="importerSettingFormQiita" role="form"
-          data-success-messaage="Updated">
-        <fieldset>
-          <legend>{{ t('importer_management.import_from', 'Qiita:Team') }}</legend>
-          <table class="table table-bordered table-mapping">
-            <thead>
-              <tr>
-                <th width="45%">Qiita:Team</th>
-                <th width="10%"></th>
-                <th>GROWI</th>
-              </tr>
-            </thead>
-            <tbody>
-              <tr>
-                <th>{{ t('Article') }}</th>
-                <th><i class="icon-arrow-right-circle text-success"></i></th>
-                <th>{{ t('Page') }}</th>
-              </tr>
-              <tr>
-                <th>{{ t('Tag')}}</th>
-                <th></th>
-                <th>-</th>
-              </tr>
-              <tr>
-                <th>{{ t("importer_management.Directory_hierarchy_tag") }}</th>
-                <th></th>
-                <th>(TBD)</th>
-              </tr>
-              <tr>
-                <th>{{ t('User') }}</th>
-                <th></th>
-                <th>(TBD)</th>
-              </tr>
-            </tbody>
-          </table>
-          <div class="well well-sm mb-0 small">
-            <ul>
-              <li>{{ t("importer_management.page_skip") }}</li>
-            </ul>
-          </div>
-          <div class="form-group">
-            <input type="password" name="dummypass" style="display: none; top: -100px; left: -100px;" />
-          </div>
-          <div class="form-group">
-            <label for="settingForm[importer:qiita:team_name]" class="col-xs-3 control-label">{{ t('importer_management.qiita_settings.team_name') }}</label>
-            <div class="col-xs-6">
-              <input class="form-control" type="text" name="settingForm[importer:qiita:team_name]" value="{{ settingForm['importer:qiita:team_name'] | default('') }}">
-            </div>
-          </div>
-          <div class="form-group">
-            <label for="settingForm[importer:qiita:access_token]" class="col-xs-3 control-label">{{ t('importer_management.qiita_settings.access_token') }}</label>
-            <div class="col-xs-6">
-              <input class="form-control" type="password" name="settingForm[importer:qiita:access_token]" value="{{ settingForm['importer:qiita:access_token'] | default('') }}">
-            </div>
-          </div>
-          <div class="form-group">
-            <input type="hidden" name="_csrf" value="{{ csrf() }}" />
-            <div class="col-xs-offset-3 col-xs-6">
-              <button id="testConnectionToQiita" type="button" class="btn btn-primary btn-qiita" data-action="/_api/admin/import/qiita" name="Qiita"
-                  data-success-message="Import posts from qiita:team success." data-error-message="Error occurred in importing pages from qiita:team">
-                {{ t("importer_management.import") }}
-              </button>
-              <button type="submit" class="btn btn-secondary">{# the first element is the default button to submit #}
-                {{ t('Update') }}
-              </button>
-              <span class="col-xs-offset-1">
-                <button id="importFromQiita" type="button" class="btn btn-default btn-qiita" data-action="/_api/admin/import/testQiitaAPI" name="Qiita"
-                    data-success-message="Test connection to qiita:team success." data-error-message="Test connection to qiita:team failed.">
-                  {{ t("importer_management.qiita_settings.test_connection") }}
-                </button>
-              </span>
-            </div>
-          </div>
-        </fieldset>
-      </form>
-    </div>
-  </div>
-
-</div>
-
-<script>
-  /**
-   * show flash message
-   */
-  function showMessage(formId, msg, status) {
-    $('#' + formId + ' .alert').remove();
-
-    if (!status) {
-      status = 'success';
-    }
-    var $message = $('<p class="alert"></p>');
-    $message.addClass('alert-' + status);
-    $message.html(msg.replace(/\n/g, '<br>'));
-    $message.insertAfter('#' + formId + ' legend');
-
-    if (status == 'success') {
-      setTimeout(function()
-      {
-        $message.fadeOut({
-          complete: function() {
-            $message.remove();
-          }
-        });
-      }, 5000);
-    }
-  }
-
-  /**
-   * Post form data and process UI
-   */
-  function postData(form, button, action, success_msg = "Success", error_msg = " {{ t('Error occurred') }} " ) {
-    var id = form.attr('id');
-    button.attr('disabled', 'disabled');
-    var jqxhr = $.post(action, form.serialize(), function(data)
-      {
-        if (!data.status) {
-          showMessage(id, `${error_msg} ${data.message}`, 'danger');
-        }
-        else {
-          showMessage(id, success_msg);
-        }
-      })
-      .fail(function() {
-        showMessage(id, "{{ t('Error occurred') }}", 'danger');
-      })
-      .always(function() {
-        button.prop('disabled', false);
-      });
-    return false;
-  }
-
-  /**
-   * Handle button esa
-   */
-  $('.btn-esa, .btn-qiita').each(function() {
-    var $form = $('#importerSettingForm' + $(this).attr('name'));
-    var $button = $(this);
-    var $action = $button.attr('data-action');
-    var $success_msg = $button.attr('data-success-message');
-    var $error_msg = $button.attr('data-error-message');
-    $button.click(function() { return postData($form, $button, $action, $success_msg, $error_msg) });
-  });
-
-  /**
-   * Handle submit button esa
-   */
-  $('#importerSettingFormEsa, #importerSettingFormQiita').each(function() {
-    var $form = $(this);
-    var $button = $("#importerSettingForm" + $(this).attr('name') + " button[type='submit']");
-    var $action = $form.attr('action');
-    var $success_msg = $button.attr('data-success-message');
-    var $error_msg = $button.attr('data-error-message');
-    $form.submit(function() { return postData($form, $button, $action, $success_msg, $error_msg) });
-  });
-
-</script>
-
+<div id ="admin-importer"></div>
 {% endblock content_main %}
 
 {% block content_footer %}

+ 7 - 114
src/server/views/admin/search.html

@@ -17,125 +17,18 @@
     <div class="col-md-3">
       {% include './widget/menu.html' with {current: 'search'} %}
     </div>
-    <div class="col-md-9">
-
-      {% if !searchConfigured() %}
-        <div class="col-md-12">
-          <div class="alert alert-warning">
-            <strong><i class="icon-fw icon-exclamation"></i> Full Text Search is not configured</strong>
-          </div>
-          <p>Check whether the env var <code>ELASTICSEARCH_URI</code> is set.</p>
-        </div>
-      {% else %}
-        {% set smessage = req.flash('successMessage') %}
-        {% if smessage.length %}
-        <div class="alert alert-success">
-          {% for e in smessage %}
-            {{ e }}<br>
-          {% endfor %}
-        </div>
-        {% endif %}
-
-        {% set emessage = req.flash('errorMessage') %}
-        {% if emessage.length %}
-        <div class="alert alert-danger">
-          {% for e in emessage %}
-          {{ e }}<br>
-          {% endfor %}
-        </div>
-        {% endif %}
-
-        <form action="/_api/admin/search/build" method="post" class="form-horizontal" id="buildIndexForm" role="form">
-          <fieldset>
-            <legend>Index Build</legend>
-            <div class="form-group">
-              <label for="" class="col-xs-3 control-label">Index Build</label>
-              <div class="col-xs-6">
-
-                <div id="admin-rebuild-search">
-                </div>
-
-                <button type="submit" class="btn btn-inverse">Build Now</button>
-                <p class="help-block">
-                  Force rebuild index.<br>
-                  Click "Build Now" to delete and create mapping file and add all pages.<br>
-                  This may take a while.
-                </p>
-              </div>
-            </div>
-          </fieldset>
-          <input type="hidden" name="_csrf" value="{{ csrf() }}">
-        </form>
-      {% endif %}
+    <div
+      class="col-md-9"
+      id ="admin-full-text-search-management"
+    >
+      <!-- Reactify Paginator start -->
+      <!-- {% include '../widget/pager.html' with {path: "/admin/search", pager: pager} %} -->
+      <!-- Reactify Paginator end -->
     </div>
   </div>
 
 </div>
 
-<script>
-  /**
-   * show flash message
-   */
-  function showMessage(formId, msg, status) {
-    $('#' + formId + ' .alert').remove();
-
-    if (!status) {
-      status = 'success';
-    }
-    var $message = $('<p class="alert"></p>');
-    $message.addClass('alert-' + status);
-    $message.html(msg.replace(/\n/g, '<br>'));
-    $message.insertAfter('#' + formId + ' legend');
-
-    if (status == 'success') {
-      setTimeout(function()
-      {
-        $message.fadeOut({
-          complete: function() {
-            $message.remove();
-          }
-        });
-      }, 5000);
-    }
-  }
-
-  /**
-   * Post form data and process UI
-   */
-  function postData(form, button, action) {
-    var id = form.attr('id');
-    button.attr('disabled', 'disabled');
-    var jqxhr = $.post(action, form.serialize(), function(res)
-      {
-        if (!res.ok) {
-          showMessage(id, `Error: ${res.message}`, 'danger');
-        }
-        else {
-          showMessage(id, 'Building request is successfully posted.');
-        }
-      })
-      .fail(function() {
-        showMessage(id, "エラーが発生しました", 'danger');
-      })
-      .always(function() {
-        button.prop('disabled', false);
-      });
-    return false;
-  }
-
-  /**
-   * Handle submit button esa
-   */
-  $('#buildIndexForm').each(function() {
-    var $form = $(this);
-    var $button = $("#buildIndexForm" + $(this).attr('name') + " button[type='submit']");
-    var $action = $form.attr('action');
-    var $success_msg = $button.attr('data-success-message');
-    var $error_msg = $button.attr('data-error-message');
-    $form.submit(function() { return postData($form, $button, $action, $success_msg, $error_msg) });
-  });
-
-</script>
 
 {% endblock content_main %}
 

+ 6 - 300
src/server/views/admin/users.html

@@ -12,307 +12,13 @@
 
 {% block content_main %}
 <div class="content-main">
-  {% set smessage = req.flash('successMessage') %}
-  {% if smessage.length %}
-  <div class="alert alert-success">
-    {{ smessage }}
+  <div class="col-md-3">
+    {% include './widget/menu.html' with {current: 'user'} %}
   </div>
-  {% endif %}
-
-  {% set emessage = req.flash('errorMessage') %}
-  {% if emessage.length %}
-  <div class="alert alert-danger">
-    {{ emessage }}
-  </div>
-  {% endif %}
-
-  <div class="row">
-    <div class="col-md-3">
-      {% include './widget/menu.html' with {current: 'user'} %}
-    </div>
-
-    <div class="col-md-9">
-      <p>
-        <button data-toggle="collapse" class="btn btn-default" href="#inviteUserForm" {% if isUserCountExceedsUpperLimit %}disabled{% endif %}>
-          {{ t("user_management.invite_users") }}
-        </button>
-        <a class="btn btn-default btn-outline" href="/admin/users/external-accounts">
-          <i class="icon-user-follow" aria-hidden="true"></i>
-          {{ t("user_management.external_account") }}
-        </a>
-      </p>
-      <form role="form" action="/admin/user/invite" method="post">
-        <div id="inviteUserForm" class="collapse">
-          <div class="form-group">
-            <label for="inviteForm[emailList]">{{ t('user_management.emails') }}</label>
-            <textarea class="form-control" name="inviteForm[emailList]" placeholder="{{ t('eg') }} user@growi.org"></textarea>
-          </div>
-          <div class="checkbox checkbox-info">
-            <input type="checkbox" id="inviteWithEmail" name="inviteForm[sendEmail]" checked>
-            <label for="inviteWithEmail">{{ t('user_management.invite_thru_email') }}</label>
-          </div>
-          <button type="submit" class="btn btn-primary">{{ t('user_management.invite') }}</button>
-        </div>
-        <input type="hidden" name="_csrf" value="{{ csrf() }}">
-      </form>
-
-      {% if isUserCountExceedsUpperLimit === true %}
-      <label>{{ t('user_management.cannot_invite_maximum_users') }}</label>
-      {% endif %}
-      {% if userUpperLimit !== 0 %}
-      <label>{{ t('user_management.current_users') }}{{ activeUsers }}</label>
-      {% endif %}
-
-      {% set createdUser = req.flash('createdUser') %}
-      {% if createdUser.length %}
-      <div class="modal fade in" id="createdUserModal">
-        <div class="modal-dialog">
-          <div class="modal-content">
-
-            <div class="modal-header">
-              <button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
-              <div class="modal-title">{{ t('user_management.invited') }}</div>
-            </div>
-
-            <div class="modal-body">
-              <p>
-                {{ t('user_management.temporary_password') }}<br>
-                {{ t('user_management.password_never_seen') }}<span class="text-danger">{{ t('user_management.send_temporary_password') }}</span>
-              </p>
-
-              <pre>{% for cUser in createdUser %}{% if cUser.user %}{{ cUser.email }} {{ cUser.password }}<br>{% else %}{{ cUser.email }} 作成失敗<br>{% endif %}{% endfor %}</pre>
-            </div>
-
-          </div><!-- /.modal-content -->
-        </div><!-- /.modal-dialog -->
-      </div><!-- /.modal -->
-      {% endif %}
-
-      {# FIXME とりあえずクソ実装。React化はやくしたいなー(チラッチラッ #}
-      <div class="modal fade" id="admin-password-reset-modal">
-        <div class="modal-dialog">
-          <div class="modal-content">
-            <div class="modal-header">
-              <button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
-              <div class="modal-title">{{ t('user_management.reset_password')}}</div>
-            </div>
-
-            <div class="modal-body">
-              <p>
-                {{ t('user_management.password_never_seen') }}<br>
-              <span class="text-danger">{{ t('user_management.send_new_password') }}</span>
-              </p>
-              <p>
-              {{ t('user_management.target_user') }}: <code id="admin-password-reset-user"></code>
-              </p>
-
-              <form method="post" id="admin-users-reset-password">
-                <input type="hidden" name="user_id" value="">
-                <input type="hidden" name="_csrf" value="{{ csrf() }}">
-                <button type="submit" value="" class="btn btn-primary">
-                  {{ t('user_management.reset_password')}}
-                </button>
-              </form>
-
-            </div>
-
-          </div><!-- /.modal-content -->
-        </div><!-- /.modal-dialog -->
-      </div>
-      <div class="modal fade" id="admin-password-reset-modal-done">
-        <div class="modal-dialog">
-          <div class="modal-content">
-
-            <div class="modal-header">
-              <button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
-              <div class="modal-title">{{ t('user_management.reset_password') }}</div>
-            </div>
-
-            <div class="modal-body">
-              <p class="alert alert-danger">Let the user know the new password below and strongly recommend to change another one immediately. </p>
-              <p>
-              Reset user: <code id="admin-password-reset-done-user"></code>
-              </p>
-              <p>
-              New password: <code id="admin-password-reset-done-password"></code>
-              </p>
-            </div>
-            <div class="modal-footer">
-              <button class="btn btn-primary" data-dismiss="modal">OK</button>
-            </div>
-          </div><!-- /.modal-content -->
-        </div><!-- /.modal-dialog -->
-      </div>
-
-      <h2>{{ t("User_Management") }}</h2>
-
-      <table class="table table-default table-bordered table-user-list">
-        <thead>
-          <tr>
-            <th width="100px">#</th>
-            <th>{{ t('status') }}</th>
-            <th><code>{{ t('User') }}</code></th>
-            <th>{{ t('Name') }}</th>
-            <th>{{ t('Email') }}</th>
-            <th width="100px">{{ t('Created') }}</th>
-            <th width="150px">{{ t('Last_Login') }}</th>
-            <th width="70px"></th>
-          </tr>
-        </thead>
-        <tbody>
-          {% for sUser in users %}
-          {% set sUserId = sUser._id.toString() %}
-          <tr>
-            <td>
-              <img src="{{ sUser|picture }}" class="picture img-circle" />
-              {% if sUser.admin %}
-              <span class="label label-inverse label-admin">
-              {{ t('administrator') }}
-              </span>
-              {% endif %}
-            </td>
-            <td>
-              <span class="label {{ css.userStatus(sUser) }}">
-                {{ consts.userStatus[sUser.status] }}
-              </span>
-            </td>
-            <td>
-              <strong>{{ sUser.username }}</strong>
-            </td>
-            <td>{{ sUser.name }}</td>
-            <td>{{ sUser.email }}</td>
-            <td>{{ sUser.createdAt|date('Y-m-d', sUser.createdAt.getTimezoneOffset()) }}</td>
-            <td>
-              {% if sUser.lastLoginAt %}
-                {{ sUser.lastLoginAt|date('Y-m-d H:i', sUser.createdAt.getTimezoneOffset()) }}
-              {% endif %}
-            </td>
-            <td>
-              <div class="btn-group admin-user-menu">
-                <button type="button" class="btn btn-sm btn-default dropdown-toggle" data-toggle="dropdown">
-                  <i class="icon-settings"></i> <span class="caret"></span>
-                </button>
-                <ul class="dropdown-menu" role="menu">
-                  <li class="dropdown-header">{{ t('user_management.edit_menu') }}</li>
-                  <li>
-                    <a href="#"
-                        data-user-id="{{ sUserId }}"
-                        data-user-email="{{ sUser.email }}"
-                        data-target="#admin-password-reset-modal"
-                        data-toggle="modal">
-                      <i class="icon-fw icon-key"></i>
-                      {{ t('user_management.reset_password') }}
-                    </a>
-                  </li>
-                  <li class="divider"></li>
-                  <li class="dropdown-header">{{ t('status') }}</li>
-
-                  {% if sUser.status == 1 %}
-                  <form id="form_activate_{{ sUserId }}" action="/admin/user/{{ sUserId }}/activate" method="post">
-                    <input type="hidden" name="_csrf" value="{{ csrf() }}">
-                  </form>
-                  <li>
-                    <a href="javascript:form_activate_{{ sUserId }}.submit()">
-                      <i class="icon-fw icon-user-following"></i> {{ t('user_management.accept') }}
-                    </a>
-                  </li>
-                  {% endif  %}
-
-                  {% if sUser.status == 2 %}
-                  <form id="form_suspend_{{ sUserId }}" action="/admin/user/{{ sUserId }}/suspend" method="post">
-                    <input type="hidden" name="_csrf" value="{{ csrf() }}">
-                  </form>
-                  <li>
-                    {% if sUser.username != user.username %}
-                    <a href="javascript:form_suspend_{{ sUserId }}.submit()">
-                      <i class="icon-fw icon-ban"></i>
-                      {{ t('user_management.deactivate_account') }}
-                    </a>
-                    {% else %}
-                    <a disabled>
-                      <i class="icon-fw icon-ban"></i>
-                      {{ t('user_management.deactivate_account') }}
-                    </a>
-                    <p class="alert alert-danger m-l-10 m-r-10 p-10">{{ t("user_management.your_own") }}</p>
-                    {% endif %}
-                  </li>
-                  {% endif %}
-
-                  {% if sUser.status == 3 %}
-                  <form id="form_activate_{{ sUserId }}" action="/admin/user/{{ sUserId }}/activate" method="post">
-                    <input type="hidden" name="_csrf" value="{{ csrf() }}">
-                  </form>
-                  <form id="form_remove_{{ sUserId }}" action="/admin/user/{{ sUserId }}/remove" method="post">
-                    <input type="hidden" name="_csrf" value="{{ csrf() }}">
-                  </form>
-                  <li>
-                    <a href="javascript:form_activate_{{ sUserId }}.submit()">
-                      <i class="icon-fw icon-action-redo"></i> {{ t('Undo') }}
-                    </a>
-                  </li>
-                  <li>
-                    {# label は同じだけど、こっちは論理削除 #}
-                    <a href="javascript:form_remove_{{ sUserId }}.submit()">
-                      <i class="icon-fw icon-fire text-danger"></i> {{ t('Delete') }}
-                    </a>
-                  </li>
-                  {% endif %}
-
-                  {% if sUser.status == 1 || sUser.status == 5 %}
-                  <form id="form_removeCompletely_{{ sUserId }}" action="/admin/user/{{ sUser._id.toString() }}/removeCompletely" method="post">
-                    <input type="hidden" name="_csrf" value="{{ csrf() }}">
-                  </form>
-                  <li class="dropdown-button">
-                    {# label は同じだけど、こっちは物理削除 #}
-                    <a href="javascript:form_removeCompletely_{{ sUserId }}.submit()">
-                      <i class="icon-fw icon-fire text-danger"></i> {{ t('Delete') }}
-                    </a>
-                  </li>
-                  {% endif %}
-
-                  {% if sUser.status == 2 %} {# activated な人だけこのメニューを表示 #}
-                  <li class="divider"></li>
-                  <li class="dropdown-header">{{ t('user_management.administrator_menu') }}</li>
-
-                  {% if sUser.admin %}
-                  <form id="form_removeFromAdmin_{{ sUserId }}" action="/admin/user/{{ sUser._id.toString() }}/removeFromAdmin" method="post">
-                    <input type="hidden" name="_csrf" value="{{ csrf() }}">
-                  </form>
-                  <li>
-                    {% if sUser.username != user.username %}
-                      <a href="javascript:form_removeFromAdmin_{{ sUserId }}.submit()">
-                        <i class="icon-fw icon-user-unfollow"></i> {{ t("user_management.remove_admin_access") }}
-                      </a>
-                    {% else %}
-                      <a disabled>
-                        <i class="icon-fw icon-user-unfollow"></i> {{ t("user_management.remove_admin_access") }}
-                      </a>
-                      <p class="alert alert-danger m-l-10 m-r-10 p-10">{{ t("user_management.cannot_remove") }}</p>
-                    {% endif %}
-                  </li>
-                  {% else %}
-                  <form id="form_makeAdmin_{{ sUserId }}" action="/admin/user/{{ sUser._id.toString() }}/makeAdmin" method="post">
-                    <input type="hidden" name="_csrf" value="{{ csrf() }}">
-                  </form>
-                  <li>
-                    <a href="javascript:form_makeAdmin_{{ sUserId }}.submit()">
-                      <i class="icon-fw icon-magic-wand"></i> {{ t("user_management.give_admin_access") }}
-                    </a>
-                  </li>
-                  {% endif %}
-
-                  {% endif %}
-                </ul>
-              </div>
-            </td>
-          </tr>
-          {% endfor %}
-        </tbody>
-      </table>
-
-      {% include '../widget/pager.html' with {path: "/admin/users", pager: pager} %}
-
-    </div>
+  <div
+  class="col-md-9"
+  id ="admin-user-page"
+  >
   </div>
 </div>
 {% endblock content_main %}