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

+ 9 - 3
src/client/js/components/Admin/UserGroupDetail/UserGroupDetailPage.jsx

@@ -2,7 +2,6 @@ import React from 'react';
 
 import UserGroupEditForm from './UserGroupEditForm';
 import UserGroupUserTable from './UserGroupUserTable';
-import UserGroupUserModal from './UserGroupUserModal';
 import UserGroupPageList from './UserGroupPageList';
 
 class UserGroupDetailPage extends React.Component {
@@ -12,9 +11,13 @@ class UserGroupDetailPage extends React.Component {
 
     const elem = document.getElementById('admin-user-group-detail');
     const userGroup = JSON.parse(elem.getAttribute('data-user-group'));
+    const userGroupRelations = JSON.parse(elem.getAttribute('data-user-group-relations'));
+    const notRelatedUsers = JSON.parse(elem.getAttribute('data-not-related-users'));
 
     this.state = {
       userGroup,
+      userGroupRelations,
+      notRelatedUsers,
     };
   }
 
@@ -29,8 +32,11 @@ class UserGroupDetailPage extends React.Component {
         <UserGroupEditForm
           userGroup={this.state.userGroup}
         />
-        <UserGroupUserTable />
-        <UserGroupUserModal />
+        <UserGroupUserTable
+          userGroupRelations={this.state.userGroupRelations}
+          notRelatedUsers={this.state.notRelatedUsers}
+          userGroup={this.state.userGroup}
+        />
         <UserGroupPageList />
       </div>
     );

+ 117 - 6
src/client/js/components/Admin/UserGroupDetail/UserGroupUserModal.jsx

@@ -1,20 +1,131 @@
-import React from 'react';
-// import PropTypes from 'prop-types';
+import React, { Fragment } from 'react';
+import PropTypes from 'prop-types';
+import { withTranslation } from 'react-i18next';
+import Modal from 'react-bootstrap/es/Modal';
+
+import { createSubscribedElement } from '../../UnstatedUtils';
+import AppContainer from '../../../services/AppContainer';
+import { toastSuccess, toastError } from '../../../util/apiNotification';
 
 class UserGroupUserModal extends React.Component {
 
+  constructor(props) {
+    super(props);
+
+    this.state = {
+      username: '',
+    };
+
+    this.xss = window.xss;
+
+    this.handleChange = this.handleChange.bind(this);
+    this.addUserBySubmit = this.addUserBySubmit.bind(this);
+    this.addUserByClick = this.addUserByClick.bind(this);
+    this.addUser = this.addUser.bind(this);
+  }
+
+  handleChange(e) {
+    this.setState({ username: e.target.value });
+  }
+
+  async addUserBySubmit(e) {
+    e.preventDefault();
+
+    await this.addUser(this.state.username);
+
+    this.setState({ username: '' });
+    this.handlePostAddUser();
+  }
+
+  async addUserByClick(username) {
+    await this.addUser(username);
+
+    this.handlePostAddUser();
+  }
+
+  async addUser(username) {
+    try {
+      await this.props.appContainer.apiv3.post(`/user-groups/${this.props.userGroup._id}/users/${username}`);
+      toastSuccess(`Added "${username}" to "${this.xss.process(this.props.userGroup.name)}"`);
+    }
+    catch (err) {
+      toastError(new Error(`Unable to add "${this.xss.process(username)}" to "${this.xss.process(this.props.userGroup.name)}"`));
+    }
+  }
+
+  handlePostAddUser() {
+    this.props.handleClose();
+  }
+
   render() {
+    const { t } = this.props;
 
     return (
-      <div>
-        UserGroupUserModal
-      </div>
+      <Modal show={this.props.show} onHide={this.props.handleClose}>
+        <Modal.Header closeButton>
+          <Modal.Title>{ t('user_group_management.add_user') }</Modal.Title>
+        </Modal.Header>
+        <Modal.Body>
+          <p>
+            <strong>{ t('Method') }1.</strong> { t('user_group_management.how_to_add1') }
+          </p>
+          <form className="form-inline" onSubmit={this.addUserBySubmit}>
+            <div className="form-group">
+              <input
+                type="text"
+                name="username"
+                className="form-control input-sm"
+                placeholder={t('User Name')}
+                value={this.state.username}
+                onChange={this.handleChange}
+              />
+            </div>
+            <button type="submit" className="btn btn-sm btn-success">{ t('Add') }</button>
+          </form>
+
+          <hr />
+          <p>
+            <strong>{ t('Method') }2.</strong> { t('user_group_management.how_to_add2') }
+          </p>
+
+          <ul className="list-inline">
+            {this.props.notRelatedUsers.map((user) => {
+              return (
+                <li key={user._id}>
+                  <button type="submit" className="btn btn-xs btn-primary" onClick={() => { return this.addUserByClick(user.username) }}>
+                    {user.username}
+                  </button>
+                </li>
+              );
+            })}
+          </ul>
+
+          {this.props.notRelatedUsers.length === 0 ? (
+            <Fragment>
+              No users available.
+            </Fragment>
+          ) : null}
+        </Modal.Body>
+      </Modal>
     );
   }
 
 }
 
 UserGroupUserModal.propTypes = {
+  t: PropTypes.func.isRequired, // i18next
+  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
+  show: PropTypes.bool.isRequired,
+  handleClose: PropTypes.func.isRequired,
+  userGroup: PropTypes.object.isRequired,
+  notRelatedUsers: PropTypes.arrayOf(PropTypes.object).isRequired,
+};
+
+/**
+ * Wrapper component for using unstated
+ */
+const UserGroupUserModalWrapper = (props) => {
+  return createSubscribedElement(UserGroupUserModal, props, [AppContainer]);
 };
 
-export default UserGroupUserModal;
+export default withTranslation()(UserGroupUserModalWrapper);

+ 121 - 6
src/client/js/components/Admin/UserGroupDetail/UserGroupUserTable.jsx

@@ -1,20 +1,135 @@
-import React from 'react';
-// import PropTypes from 'prop-types';
+import React, { Fragment } from 'react';
+import PropTypes from 'prop-types';
+import { withTranslation } from 'react-i18next';
+import dateFnsFormat from 'date-fns/format';
+
+import UserGroupUserModal from './UserGroupUserModal';
+import UserPicture from '../../User/UserPicture';
+import { createSubscribedElement } from '../../UnstatedUtils';
+import AppContainer from '../../../services/AppContainer';
+import { toastSuccess, toastError } from '../../../util/apiNotification';
 
 class UserGroupUserTable extends React.Component {
 
+  constructor(props) {
+    super(props);
+
+    this.state = {
+      isUserGroupUserModalOpen: false,
+    };
+
+    this.openUserGroupUserModal = this.openUserGroupUserModal.bind(this);
+    this.closeUserGroupUserModal = this.closeUserGroupUserModal.bind(this);
+  }
+
+  openUserGroupUserModal() {
+    this.setState({ isUserGroupUserModalOpen: true });
+  }
+
+  closeUserGroupUserModal() {
+    this.setState({ isUserGroupUserModalOpen: false });
+  }
+
   render() {
+    const { t } = this.props;
 
     return (
-      <div>
-        UserGroupUserTable
-      </div>
+      <Fragment>
+        <legend className="m-t-20">{ t('User List') }</legend>
+
+        <table className="table table-bordered table-user-list">
+          <thead>
+            <tr>
+              <th width="100px">#</th>
+              <th>
+                { t('User') }
+              </th>
+              <th>{ t('Name') }</th>
+              <th width="100px">{ t('Created') }</th>
+              <th width="160px">{ t('Last Login')}</th>
+              <th width="70px"></th>
+            </tr>
+          </thead>
+          <tbody>
+            {this.props.userGroupRelations.map((sRelation) => {
+              const { relatedUser } = sRelation;
+
+              return (
+                <tr key={sRelation._id}>
+                  <td>
+                    <UserPicture user={relatedUser} className="picture img-circle" />
+                  </td>
+                  <td>
+                    <strong>{relatedUser.username}</strong>
+                  </td>
+                  <td>{relatedUser.name}</td>
+                  <td>{relatedUser.createdAt ? dateFnsFormat(new Date(relatedUser.createdAt), 'yyyy-MM-dd') : ''}</td>
+                  <td>{relatedUser.lastLoginAt ? dateFnsFormat(new Date(relatedUser.lastLoginAt), 'yyyy-MM-dd HH:mm:ss') : ''}</td>
+                  <td>
+                    <div className="btn-group admin-user-menu">
+                      <button type="button" className="btn btn-default btn-sm dropdown-toggle" data-toggle="dropdown">
+                        <i className="icon-settings"></i> <span className="caret"></span>
+                      </button>
+                      <ul className="dropdown-menu" role="menu">
+                        <form id="form_removeFromGroup_{{ loop.index }}" action="/admin/user-group-relation/{{userGroup._id.toString()}}/remove-relation/{{ sRelation._id.toString() }}" method="post">
+                          <input type="hidden" name="_csrf" value="{{ csrf() }}" />
+                        </form>
+                        <li>
+                          <a href="javascript:form_removeFromGroup_{{ loop.index }}.submit()">
+                            <i className="icon-fw icon-user-unfollow"></i> { t('user_group_management.remove_from_group')}
+                          </a>
+                        </li>
+                      </ul>
+                    </div>
+                  </td>
+                </tr>
+              );
+            })}
+
+            {this.props.userGroupRelations.length === 0 ? (
+              <tr>
+                <td></td>
+                <td className="text-center">
+                  <button className="btn btn-default" type="button" onClick={this.openUserGroupUserModal}>
+                    <i className="ti-plus"></i>
+                  </button>
+                </td>
+                <td></td>
+                <td></td>
+                <td></td>
+                <td></td>
+              </tr>
+            ) : null}
+
+          </tbody>
+        </table>
+
+        <UserGroupUserModal
+          show={this.state.isUserGroupUserModalOpen}
+          handleClose={this.closeUserGroupUserModal}
+          notRelatedUsers={this.props.notRelatedUsers}
+          userGroup={this.props.userGroup}
+        />
+
+      </Fragment>
     );
   }
 
 }
 
 UserGroupUserTable.propTypes = {
+  t: PropTypes.func.isRequired, // i18next
+  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
+  userGroupRelations: PropTypes.arrayOf(PropTypes.object).isRequired,
+  notRelatedUsers: PropTypes.arrayOf(PropTypes.object).isRequired,
+  userGroup: PropTypes.object.isRequired,
+};
+
+/**
+ * Wrapper component for using unstated
+ */
+const UserGroupUserTableWrapper = (props) => {
+  return createSubscribedElement(UserGroupUserTable, props, [AppContainer]);
 };
 
-export default UserGroupUserTable;
+export default withTranslation()(UserGroupUserTableWrapper);

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

@@ -94,9 +94,9 @@ class UserTable extends React.Component {
                   </td>
                   <td>{user.name}</td>
                   <td>{user.email}</td>
-                  <td>{dateFnsFormat(new Date(user.createdAt), 'YYYY-MM-DD')}</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> }
+                    { user.lastLoginAt && <span>{dateFnsFormat(new Date(user.lastLoginAt), 'yyyy-MM-DD HH:mm')}</span> }
                   </td>
                   <td>
                     <UserMenu user={user} onPasswordResetClicked={this.props.onPasswordResetClicked} />

+ 0 - 8
src/client/js/legacy/crowi-admin.js

@@ -61,14 +61,6 @@ $(() => {
     return false;
   });
 
-  $('form#user-group-relation-create').on('submit', function(e) {
-    $.post('/admin/user-group-relation/create', $(this).serialize(), (res) => {
-      $('#admin-add-user-group-relation-modal').modal('hide');
-      return;
-    });
-  });
-
-
   $('#pictureUploadForm input[name=userGroupPicture]').on('change', function() {
     const $form = $('#pictureUploadForm');
     const fd = new FormData($form[0]);

+ 3 - 3
src/client/js/services/AppContainer.js

@@ -332,7 +332,7 @@ export default class AppContainer extends Container {
     return this.apiv3Request('get', path, { params });
   }
 
-  async apiv3Post(path, params) {
+  async apiv3Post(path, params = {}) {
     if (!params._csrf) {
       params._csrf = this.csrfToken;
     }
@@ -340,7 +340,7 @@ export default class AppContainer extends Container {
     return this.apiv3Request('post', path, params);
   }
 
-  async apiv3Put(path, params) {
+  async apiv3Put(path, params = {}) {
     if (!params._csrf) {
       params._csrf = this.csrfToken;
     }
@@ -348,7 +348,7 @@ export default class AppContainer extends Container {
     return this.apiv3Request('put', path, params);
   }
 
-  async apiv3Delete(path, params) {
+  async apiv3Delete(path, params = {}) {
     if (!params._csrf) {
       params._csrf = this.csrfToken;
     }

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

@@ -721,40 +721,6 @@ module.exports = function(crowi, app) {
 
   };
 
-  actions.userGroupRelation.create = function(req, res) {
-    const User = crowi.model('User');
-    const UserGroup = crowi.model('UserGroup');
-    const UserGroupRelation = crowi.model('UserGroupRelation');
-
-    // req params
-    const userName = req.body.user_name;
-    const userGroupId = req.body.user_group_id;
-
-    let user = null;
-    let userGroup = null;
-
-    Promise.all([
-      // ユーザグループをIDで検索
-      UserGroup.findById(userGroupId),
-      // ユーザを名前で検索
-      User.findUserByUsername(userName),
-    ])
-      .then((resolves) => {
-        userGroup = resolves[0];
-        user = resolves[1];
-        // Relation を作成
-        UserGroupRelation.createRelation(userGroup, user);
-      })
-      .then((result) => {
-        return res.redirect(`/admin/user-group-detail/${userGroup.id}`);
-      })
-      .catch((err) => {
-        debug('Error on create user-group relation', err);
-        req.flash('errorMessage', 'Error on create user-group relation');
-        return res.redirect(`/admin/user-group-detail/${userGroup.id}`);
-      });
-  };
-
   actions.userGroupRelation.remove = function(req, res) {
     const UserGroupRelation = crowi.model('UserGroupRelation');
     const userGroupId = req.params.id;

+ 68 - 1
src/server/routes/apiv3/user-group.js

@@ -17,7 +17,12 @@ const validator = {};
  */
 
 module.exports = (crowi) => {
-  const { ErrorV3, UserGroup, UserGroupRelation } = crowi.models;
+  const {
+    ErrorV3,
+    UserGroup,
+    UserGroupRelation,
+    User,
+  } = crowi.models;
   const { ApiV3FormValidator } = crowi.middlewares;
 
   const {
@@ -179,6 +184,8 @@ module.exports = (crowi) => {
     body('name', 'Group name is required').trim().exists(),
   ];
 
+  validator.users = {};
+
   /**
    * @swagger
    *
@@ -283,5 +290,65 @@ module.exports = (crowi) => {
     }
   });
 
+  validator.users.post = [
+    param('id').trim().exists(),
+    param('username').trim().exists({ checkFalsy: true }),
+  ];
+
+  /**
+   * @swagger
+   *
+   *  paths:
+   *    /_api/v3/user-groups/{:id/users}:
+   *      post:
+   *        tags: [UserGroup]
+   *        description: Add a user to the userGroup
+   *        produces:
+   *          - application/json
+   *        parameters:
+   *          - name: id
+   *            in: path
+   *            description: id of userGroup
+   *            schema:
+   *              type: ObjectId
+   *          - name: username
+   *            in: path
+   *            description: id of user
+   *            schema:
+   *              type: string
+   *        responses:
+   *          200:
+   *            description: users are added
+   *            content:
+   *              application/json:
+   *                schema:
+   *                type: object
+   *                  properties:
+   *                    user:
+   *                      type: object
+   *                    userGroup:
+   *                      type: object
+   *                      description: user objects
+   */
+  router.post('/:id/users/:username', loginRequired(), adminRequired, validator.users.post, ApiV3FormValidator, async(req, res) => {
+    const { id, username } = req.params;
+
+    try {
+      const [userGroup, user] = await Promise.all([
+        UserGroup.findById(id),
+        User.findUserByUsername(username),
+      ]);
+
+      await UserGroupRelation.createRelation(userGroup, user);
+
+      return res.apiv3({ userGroup, user });
+    }
+    catch (err) {
+      const msg = `Error occurred in adding an user "${username}" to group "${id}"`;
+      logger.error(msg, err);
+      return res.apiv3Err(new ErrorV3(msg, 'user-group-add-user-failed'));
+    }
+  });
+
   return router;
 };

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

@@ -136,7 +136,6 @@ module.exports = function(crowi, app) {
   app.get('/admin/user-group-detail/:id'          , loginRequired(), adminRequired, admin.userGroup.detail);
 
   // user-group-relations admin
-  app.post('/admin/user-group-relation/create', loginRequired(), adminRequired, csrf, admin.userGroupRelation.create);
   app.post('/admin/user-group-relation/:id/remove-relation/:relationId', loginRequired(), adminRequired, csrf, admin.userGroupRelation.remove);
 
   // importer management for admin

+ 2 - 0
src/server/views/admin/user-group-detail.html

@@ -35,6 +35,8 @@
       id="admin-user-group-detail"
       class="col-md-9"
       data-user-group="{{ userGroup|json }}"
+      data-user-group-relations="{{ userGroupRelations|json }}"
+      data-not-related-users="{{ notRelatedusers|json }}"
     >
       <!-- そのまま start -->
       <a href="/admin/user-groups" class="btn btn-default">