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

Merge branch 'imprv/reactify-admin' into reactify-admin-importer

熊谷洸介(Kousuke Kumagai) 6 лет назад
Родитель
Сommit
8f585db078

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

@@ -737,5 +737,13 @@
     "import": "Import",
     "import": "Import",
     "page_skip": "Pages with a name that already exists on GROWI are not imported",
     "page_skip": "Pages with a name that already exists on GROWI are not imported",
     "Directory_hierarchy_tag": "Directory Hierarchy Tag"
     "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

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

+ 4 - 7
src/client/js/app.js

@@ -38,6 +38,7 @@ import CustomCssEditor from './components/Admin/CustomCssEditor';
 import CustomScriptEditor from './components/Admin/CustomScriptEditor';
 import CustomScriptEditor from './components/Admin/CustomScriptEditor';
 import CustomHeaderEditor from './components/Admin/CustomHeaderEditor';
 import CustomHeaderEditor from './components/Admin/CustomHeaderEditor';
 import AdminRebuildSearch from './components/Admin/AdminRebuildSearch';
 import AdminRebuildSearch from './components/Admin/AdminRebuildSearch';
+import UserPage from './components/Admin/Users/Users';
 import UserGroupPage from './components/Admin/UserGroup/UserGroupPage';
 import UserGroupPage from './components/Admin/UserGroup/UserGroupPage';
 import Importer from './components/Admin/Importer';
 import Importer from './components/Admin/Importer';
 import FullTextSearchManagement from './components/Admin/FullTextSearchManagement/FullTextSearchPage';
 import FullTextSearchManagement from './components/Admin/FullTextSearchManagement/FullTextSearchPage';
@@ -100,6 +101,9 @@ let componentMappings = {
   'user-created-list': <RecentCreated />,
   'user-created-list': <RecentCreated />,
   'user-draft-list': <MyDraftList />,
   'user-draft-list': <MyDraftList />,
 
 
+  'admin-user-page': <UserPage />,
+  'admin-full-text-search-management': <FullTextSearchManagement />,
+
   'staff-credit': <StaffCredit />,
   'staff-credit': <StaffCredit />,
   'admin-importer': <Importer />,
   'admin-importer': <Importer />,
 };
 };
@@ -209,13 +213,6 @@ Object.keys(componentMappings).forEach((key) => {
   }
   }
 });
 });
 
 
-const adminFullTextSearchManagementElem = document.getElementById('admin-full-text-search-management');
-if (adminFullTextSearchManagementElem != null) {
-
-  ReactDOM.render(
-    <FullTextSearchManagement />,
-  );
-}
 
 
 // うわーもうー (commented by Crowi team -- 2018.03.23 Yuki Takei)
 // うわーもうー (commented by Crowi team -- 2018.03.23 Yuki Takei)
 $('a[data-toggle="tab"][href="#revision-history"]').on('show.bs.tab', () => {
 $('a[data-toggle="tab"][href="#revision-history"]').on('show.bs.tab', () => {

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

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

+ 59 - 2
src/client/js/components/Admin/FullTextSearchManagement/FullTextSearchPage.jsx

@@ -1,18 +1,75 @@
 import React, { Fragment } from 'react';
 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 {
 class FullTextSearchManagement extends React.Component {
 
 
   constructor(props) {
   constructor(props) {
-    super();
+    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() {
   render() {
+    const { t } = this.props;
+
     return (
     return (
       <Fragment>
       <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>
       </Fragment>
     );
     );
   }
   }
 
 
 }
 }
 
 
-export default FullTextSearchManagement;
+const FullTextSearchManagementWrapper = (props) => {
+  return createSubscribedElement(FullTextSearchManagement, props, [AppContainer]);
+};
+
+FullTextSearchManagement.propTypes = {
+  t: PropTypes.func.isRequired, // i18next
+  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
+};
+
+export default withTranslation()(FullTextSearchManagementWrapper);

+ 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);

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

@@ -0,0 +1,88 @@
+import React, { Fragment } from 'react';
+import PropTypes from 'prop-types';
+import { withTranslation } from 'react-i18next';
+import dateFnsFormat from 'date-fns/format';
+import UserPicture from '../../User/UserPicture';
+
+import { createSubscribedElement } from '../../UnstatedUtils';
+import AppContainer from '../../../services/AppContainer';
+
+class UserTable extends React.Component {
+
+  constructor(props) {
+    super(props);
+
+    this.state = {
+
+    };
+  }
+
+
+  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>
+          <tbody>
+            {this.props.users.map((user) => {
+              return (
+                <tr key={user._id}>
+                  <td>
+                    <UserPicture user={user} className="picture img-circle" />
+                  </td>
+                  <td>{user.admin && <span className="label label-inverse label-admin">{ t('administrator') }</span>}</td>
+                  <td>
+                    <strong>{user.username}</strong>
+                  </td>
+                  <td>{user.name}</td>
+                  <td>{user.email}</td>
+                  <td>{dateFnsFormat(new Date(user.createdAt), 'YYYY-MM-DD')}</td>
+                  <td>
+                    { user.lastLoginAt && <span>{dateFnsFormat(new Date(user.lastLoginAt), 'YYYY-MM-DD HH:mm')}</span> }
+                  </td>
+                  <td>
+                    <div className="btn-group admin-user-menu">
+                      <button type="button" className="btn btn-sm btn-default dropdown-toggle" data-toggle="dropdown">
+                        <i className="icon-settings"></i> <span className="caret"></span>
+                      </button>
+                    </div>
+                  </td>
+                </tr>
+              );
+            })}
+          </tbody>
+        </table>
+      </Fragment>
+    );
+  }
+
+}
+
+const UserTableWrapper = (props) => {
+  return createSubscribedElement(UserTable, props, [AppContainer]);
+};
+
+UserTable.propTypes = {
+  t: PropTypes.func.isRequired, // i18next
+  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
+
+  users: PropTypes.array.isRequired,
+
+};
+
+export default withTranslation()(UserTableWrapper);

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

@@ -0,0 +1,74 @@
+import React, { Fragment } from 'react';
+import PropTypes from 'prop-types';
+import { withTranslation } from 'react-i18next';
+
+import PaginationWrapper from '../../PaginationWrapper';
+import InviteUserControl from './InviteUserControl';
+import UserTable from './UserTable';
+
+import { createSubscribedElement } from '../../UnstatedUtils';
+import AppContainer from '../../../services/AppContainer';
+
+class UserPage extends React.Component {
+
+  constructor(props) {
+    super();
+
+    this.state = {
+      users: [],
+      activePage: 1,
+      pagingLimit: Infinity,
+    };
+
+  }
+
+  // TODO unstatedContainerを作ってそこにリファクタすべき
+  componentDidMount() {
+    const jsonData = document.getElementById('admin-user-page');
+    const users = JSON.parse(jsonData.getAttribute('users'));
+
+    this.setState({
+      users,
+    });
+  }
+
+
+  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
+          users={this.state.users}
+        />
+        <PaginationWrapper
+          activePage={this.state.activePage}
+          changePage={this.handlePage}
+          totalItemsCount={this.state.totalUsers}
+          pagingLimit={this.state.pagingLimit}
+        >
+        </PaginationWrapper>
+      </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);

+ 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 - 1
src/server/views/admin/search.html

@@ -18,7 +18,8 @@
       {% include './widget/menu.html' with {current: 'search'} %}
       {% include './widget/menu.html' with {current: 'search'} %}
     </div>
     </div>
     <div
     <div
-    id ="admin-full-text-search-management"
+      class="col-md-9"
+      id ="admin-full-text-search-management"
     >
     >
       <!-- Reactify Paginator start -->
       <!-- Reactify Paginator start -->
       <!-- {% include '../widget/pager.html' with {path: "/admin/search", pager: pager} %} -->
       <!-- {% include '../widget/pager.html' with {path: "/admin/search", pager: pager} %} -->

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

@@ -12,307 +12,14 @@
 
 
 {% block content_main %}
 {% block content_main %}
 <div class="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>
   </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"
+  users= "{{ users | json }}"
+  >
   </div>
   </div>
 </div>
 </div>
 {% endblock content_main %}
 {% endblock content_main %}