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

Merge pull request #949 from weseek/imprv/handle-privatepages-ongroupdeletion

Imprv/handle privatepages ongroupdeletion
Yuki Takei 6 лет назад
Родитель
Сommit
9ea022f91d

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

@@ -670,6 +670,19 @@
     "current users": "Current users:"
     "current users": "Current users:"
   },
   },
 
 
+  "user_group_management": {
+    "is_loading_data": "Loading data...",
+    "choose_action": "Choose an action for private pages",
+    "delete_group": "Delete Group",
+    "group_name": "Group Name",
+    "group_and_pages_not_retrievable": "Once deleted, the deleted group and its private pages cannot be retrieved",
+    "publish_pages": "Publish All",
+    "delete_pages": "Delete All",
+    "transfer_pages": "Transfer to another group",
+    "select_group": "Select a group",
+    "no_groups": "No groups to select"
+  },
+
   "importer_management": {
   "importer_management": {
     "import_from": "Import from %s",
     "import_from": "Import from %s",
     "esa_settings": {
     "esa_settings": {

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

@@ -699,6 +699,19 @@
     "current users": "現在のユーザー数:"
     "current users": "現在のユーザー数:"
   },
   },
 
 
+  "user_group_management": {
+    "is_loading_data": "データを取得中です...",
+    "choose_action": "削除するグループの限定公開ページの処理を選択してください",
+    "delete_group": "グループの削除",
+    "group_name": "グループ名",
+    "group_and_pages_not_retrievable": "グループ及び限定公開のページの削除を行うと元に戻すことはできませんのでご注意ください。",
+    "publish_pages": "全て公開する",
+    "delete_pages": "全て削除する",
+    "transfer_pages": "全て他のグループに移譲する",
+    "select_group": "グループを選択してください",
+    "no_groups": "グループがありません"
+  },
+
   "importer_management": {
   "importer_management": {
     "import_from": "%s からインポート",
     "import_from": "%s からインポート",
     "esa_settings": {
     "esa_settings": {

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

@@ -44,6 +44,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 GroupDeleteModal from './components/GroupDeleteModal/GroupDeleteModal';
 
 
 
 
 const logger = loggerFactory('growi:app');
 const logger = loggerFactory('growi:app');
@@ -590,6 +591,17 @@ if (adminRebuildSearchElem != null) {
     adminRebuildSearchElem,
     adminRebuildSearchElem,
   );
   );
 }
 }
+const adminGrantSelectorElem = document.getElementById('admin-delete-user-group-modal');
+if (adminGrantSelectorElem != null) {
+  ReactDOM.render(
+    <I18nextProvider i18n={i18n}>
+      <GroupDeleteModal
+        crowi={crowi}
+      />
+    </I18nextProvider>,
+    adminGrantSelectorElem,
+  );
+}
 
 
 // notification from websocket
 // notification from websocket
 function updatePageStatusAlert(page, user) {
 function updatePageStatusAlert(page, user) {

+ 260 - 0
src/client/js/components/GroupDeleteModal/GroupDeleteModal.jsx

@@ -0,0 +1,260 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { withTranslation } from 'react-i18next';
+
+import * as toastr from 'toastr';
+
+/**
+ * Delete User Group Select component
+ *
+ * @export
+ * @class GrantSelector
+ * @extends {React.Component}
+ */
+class GroupDeleteModal extends React.Component {
+
+  constructor(props) {
+    super(props);
+
+    const { t } = this.props;
+
+    // actionName master constants
+    this.actionForPages = {
+      public: 'public',
+      delete: 'delete',
+      transfer: 'transfer',
+    };
+
+    this.availableOptions = [
+      {
+        id: 1, actionForPages: this.actionForPages.public, iconClass: 'icon-people', styleClass: '', label: t('user_group_management.publish_pages'),
+      },
+      {
+        id: 2, actionForPages: this.actionForPages.delete, iconClass: 'icon-trash', styleClass: 'text-danger', label: t('user_group_management.delete_pages'),
+      },
+      {
+        id: 3, actionForPages: this.actionForPages.transfer, iconClass: 'icon-options', styleClass: '', label: t('user_group_management.transfer_pages'),
+      },
+    ];
+
+    this.initialState = {
+      deleteGroupId: '',
+      deleteGroupName: '',
+      groups: [],
+      actionName: '',
+      selectedGroupId: '',
+      isFetching: false,
+    };
+
+    this.state = this.initialState;
+
+    // logger
+    this.logger = require('@alias/logger')('growi:GroupDeleteModal:GroupDeleteModal');
+
+    // retrieve xss library from window
+    this.xss = window.xss;
+
+    this.getGroupName = this.getGroupName.bind(this);
+    this.changeActionHandler = this.changeActionHandler.bind(this);
+    this.changeGroupHandler = this.changeGroupHandler.bind(this);
+    this.renderPageActionSelector = this.renderPageActionSelector.bind(this);
+    this.renderGroupSelector = this.renderGroupSelector.bind(this);
+    this.validateForm = this.validateForm.bind(this);
+  }
+
+  componentDidMount() {
+    // bootstrap and this jQuery opens/hides the modal.
+    // let React handle it in the future.
+    $('#admin-delete-user-group-modal').on('show.bs.modal', async(button) => {
+      this.setState({ isFetching: true });
+
+      const groups = await this.fetchAllGroups();
+
+      const data = $(button.relatedTarget);
+      const deleteGroupId = data.data('user-group-id');
+      const deleteGroupName = data.data('user-group-name');
+
+      this.setState({
+        groups,
+        deleteGroupId,
+        deleteGroupName,
+        isFetching: false,
+      });
+    });
+
+    $('#admin-delete-user-group-modal').on('hide.bs.modal', (button) => {
+      this.setState(this.initialState);
+    });
+  }
+
+  getGroupName(group) {
+    return this.xss.process(group.name);
+  }
+
+  async fetchAllGroups() {
+    let groups = [];
+
+    try {
+      const res = await this.props.crowi.apiGet('/admin/user-groups');
+      if (res.ok) {
+        groups = res.userGroups;
+      }
+      else {
+        throw new Error('Unable to fetch groups from server');
+      }
+    }
+    catch (err) {
+      this.handleError(err);
+    }
+
+    return groups;
+  }
+
+  handleError(err) {
+    this.logger.error(err);
+    toastr.error(err, 'Error occured', {
+      closeButton: true,
+      progressBar: true,
+      newestOnTop: false,
+      showDuration: '100',
+      hideDuration: '100',
+      timeOut: '3000',
+    });
+  }
+
+  changeActionHandler(e) {
+    const actionName = e.target.value;
+    this.setState({ actionName });
+  }
+
+  changeGroupHandler(e) {
+    const selectedGroupId = e.target.value;
+    this.setState({ selectedGroupId });
+  }
+
+  renderPageActionSelector() {
+    const { t } = this.props;
+
+    const optoins = this.availableOptions.map((opt) => {
+      const dataContent = `<i class="icon icon-fw ${opt.iconClass} ${opt.styleClass}"></i> <span class="action-name ${opt.styleClass}">${t(opt.label)}</span>`;
+      return <option key={opt.id} value={opt.actionForPages} data-content={dataContent}>{t(opt.label)}</option>;
+    });
+
+    return (
+      <select
+        name="actionName"
+        className="form-control"
+        placeholder="select"
+        value={this.state.actionName}
+        onChange={this.changeActionHandler}
+      >
+        <option value="" disabled>{t('user_group_management.choose_action')}</option>
+        {optoins}
+      </select>
+    );
+  }
+
+  renderGroupSelector() {
+    const { t } = this.props;
+
+    const groups = this.state.groups.filter((group) => {
+      return group._id !== this.state.deleteGroupId;
+    });
+
+    const options = groups.map((group) => {
+      const dataContent = `<i class="icon icon-fw icon-organization"></i> ${this.getGroupName(group)}`;
+      return <option key={group._id} value={group._id} data-content={dataContent}>{this.getGroupName(group)}</option>;
+    });
+
+    const defaultOptionText = groups.length === 0 ? t('user_group_management.no_groups') : t('user_group_management.select_group');
+
+    return (
+      <select
+        name="selectedGroupId"
+        className={`form-control ${this.state.actionName === this.actionForPages.transfer ? '' : 'd-none'}`}
+        value={this.state.selectedGroupId}
+        onChange={this.changeGroupHandler}
+      >
+        <option value="" disabled>{defaultOptionText}</option>
+        {options}
+      </select>
+    );
+  }
+
+  validateForm() {
+    let isValid = true;
+
+    if (this.state.actionName === '') {
+      isValid = false;
+    }
+    else if (this.state.actionName === this.actionForPages.transfer) {
+      isValid = this.state.selectedGroupId !== '';
+    }
+
+    return isValid;
+  }
+
+  render() {
+    const { t } = this.props;
+
+    return (
+      <div className="modal-dialog">
+        <div className="modal-content">
+          <div className="modal-header bg-danger">
+            <button type="button" className="close" data-dismiss="modal" aria-hidden="true">&times;</button>
+            <div className="modal-title">
+              <i className="icon icon-fire"></i> {t('user_group_management.delete_group')}
+            </div>
+          </div>
+
+          <div className="modal-body">
+            <div>
+              <span className="font-weight-bold">{t('user_group_management.group_name')}</span> : &quot;{this.state.deleteGroupName}&quot;
+            </div>
+            {this.state.isFetching
+              ? (
+                <div className="mt-5">
+                  {t('user_group_management.is_loading_data')}
+                </div>
+              )
+              : (
+                <div className="text-danger mt-5">
+                  {t('user_group_management.group_and_pages_not_retrievable')}
+                </div>
+              )
+            }
+          </div>
+
+          {this.state.isFetching
+            ? (
+              null
+            )
+            : (
+              <div className="modal-footer">
+                <form action="/admin/user-group.remove" method="post" id="admin-user-groups-delete" className="d-flex justify-content-between">
+                  <div className="d-flex">
+                    {this.renderPageActionSelector()}
+                    {this.renderGroupSelector()}
+                  </div>
+                  <input type="hidden" id="deleteGroupId" name="deleteGroupId" value={this.state.deleteGroupId} onChange={() => {}} />
+                  <input type="hidden" name="_csrf" defaultValue={this.props.crowi.csrfToken} />
+                  <button type="submit" value="" className="btn btn-sm btn-danger" disabled={!this.validateForm()}>
+                    <i className="icon icon-fire"></i> {t('Delete')}
+                  </button>
+                </form>
+              </div>
+            )
+          }
+        </div>
+      </div>
+    );
+  }
+
+}
+
+GroupDeleteModal.propTypes = {
+  t: PropTypes.func.isRequired, // i18next
+  crowi: PropTypes.object.isRequired,
+};
+
+export default withTranslation()(GroupDeleteModal);

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

@@ -61,15 +61,6 @@ $(() => {
     return false;
     return false;
   });
   });
 
 
-  $('#admin-delete-user-group-modal').on('show.bs.modal', (button) => {
-    const data = $(button.relatedTarget);
-    const userGroupId = data.data('user-group-id');
-    const userGroupName = data.data('user-group-name');
-
-    $('#admin-delete-user-group-name').text(userGroupName);
-    $('#admin-user-groups-delete input[name=user_group_id]').val(userGroupId);
-  });
-
   $('form#user-group-relation-create').on('submit', function(e) {
   $('form#user-group-relation-create').on('submit', function(e) {
     $.post('/admin/user-group-relation/create', $(this).serialize(), (res) => {
     $.post('/admin/user-group-relation/create', $(this).serialize(), (res) => {
       $('#admin-add-user-group-relation-modal').modal('hide');
       $('#admin-add-user-group-relation-modal').modal('hide');

+ 46 - 0
src/server/models/page.js

@@ -1311,6 +1311,52 @@ module.exports = function(crowi) {
     return pageData;
     return pageData;
   };
   };
 
 
+  pageSchema.statics.handlePrivatePagesForDeletedGroup = async function(deletedGroup, action, selectedGroupId) {
+    const Page = mongoose.model('Page');
+
+    const pages = await this.find({ grantedGroup: deletedGroup });
+
+    switch (action) {
+      case 'public':
+        await Promise.all(pages.map((page) => {
+          return Page.publicizePage(page);
+        }));
+        break;
+      case 'delete':
+        await Promise.all(pages.map((page) => {
+          return Page.completelyDeletePage(page);
+        }));
+        break;
+      case 'transfer':
+        await Promise.all(pages.map((page) => {
+          return Page.transferPageToGroup(page, selectedGroupId);
+        }));
+        break;
+      default:
+        throw new Error('Unknown action for private pages');
+    }
+  };
+
+  pageSchema.statics.publicizePage = async function(page) {
+    page.grantedGroup = null;
+    page.grant = GRANT_PUBLIC;
+    await page.save();
+  };
+
+  pageSchema.statics.transferPageToGroup = async function(page, selectedGroupId) {
+    const UserGroup = mongoose.model('UserGroup');
+
+    // check page existence
+    const isExist = await UserGroup.count({ _id: selectedGroupId }) > 0;
+    if (isExist) {
+      page.grantedGroup = selectedGroupId;
+      await page.save();
+    }
+    else {
+      throw new Error('Cannot find the group to which private pages belong to. _id: ', selectedGroupId);
+    }
+  };
+
   /**
   /**
    * associate GROWI page and HackMD page
    * associate GROWI page and HackMD page
    * @param {Page} pageData
    * @param {Page} pageData

+ 15 - 22
src/server/models/user-group.js

@@ -90,31 +90,24 @@ class UserGroup {
   }
   }
 
 
   // グループの完全削除
   // グループの完全削除
-  static removeCompletelyById(id) {
+  static async removeCompletelyById(deleteGroupId, action, selectedGroupId) {
     const PageGroupRelation = mongoose.model('PageGroupRelation');
     const PageGroupRelation = mongoose.model('PageGroupRelation');
     const UserGroupRelation = mongoose.model('UserGroupRelation');
     const UserGroupRelation = mongoose.model('UserGroupRelation');
+    const Page = mongoose.model('Page');
 
 
-    let removed;
-    return this.findById(id)
-      .then((userGroupData) => {
-        if (userGroupData == null) {
-          throw new Error('UserGroup data is not exists. id:', id);
-        }
-        return userGroupData.remove();
-      })
-      .then((removedUserGroupData) => {
-        removed = removedUserGroupData;
-      })
-      // remove relations
-      .then(() => {
-        return Promise.all([
-          UserGroupRelation.removeAllByUserGroup(removed),
-          PageGroupRelation.removeAllByUserGroup(removed),
-        ]);
-      })
-      .then(() => {
-        return removed;
-      });
+    const groupToDelete = await this.findById(deleteGroupId);
+    if (groupToDelete == null) {
+      throw new Error('UserGroup data is not exists. id:', deleteGroupId);
+    }
+    const deletedGroup = await groupToDelete.remove();
+
+    await Promise.all([
+      UserGroupRelation.removeAllByUserGroup(deletedGroup),
+      PageGroupRelation.removeAllByUserGroup(deletedGroup),
+      Page.handlePrivatePagesForDeletedGroup(deletedGroup, action, selectedGroupId),
+    ]);
+
+    return deletedGroup;
   }
   }
 
 
   // グループ生成(名前が要る)
   // グループ生成(名前が要る)

+ 23 - 12
src/server/routes/admin.js

@@ -780,19 +780,19 @@ module.exports = function(crowi, app) {
 
 
 
 
   // app.post('/_api/admin/user-group/delete' , admin.userGroup.removeCompletely);
   // app.post('/_api/admin/user-group/delete' , admin.userGroup.removeCompletely);
-  actions.userGroup.removeCompletely = function(req, res) {
-    const id = req.body.user_group_id;
+  actions.userGroup.removeCompletely = async(req, res) => {
+    const { deleteGroupId, actionName, selectedGroupId } = req.body;
 
 
-    UserGroup.removeCompletelyById(id)
-      .then(() => {
-        req.flash('successMessage', '削除しました');
-        return res.redirect('/admin/user-groups');
-      })
-      .catch((err) => {
-        debug('Error while removing userGroup.', err, id);
-        req.flash('errorMessage', '完全な削除に失敗しました。');
-        return res.redirect('/admin/user-groups');
-      });
+    try {
+      await UserGroup.removeCompletelyById(deleteGroupId, actionName, selectedGroupId);
+      req.flash('successMessage', '削除しました');
+    }
+    catch (err) {
+      debug('Error while removing userGroup.', err, deleteGroupId);
+      req.flash('errorMessage', '完全な削除に失敗しました。');
+    }
+
+    return res.redirect('/admin/user-groups');
   };
   };
 
 
   actions.userGroupRelation = {};
   actions.userGroupRelation = {};
@@ -1327,6 +1327,17 @@ module.exports = function(crowi, app) {
     return res.json(ApiResponse.success());
     return res.json(ApiResponse.success());
   };
   };
 
 
+  actions.api.userGroups = async(req, res) => {
+    try {
+      const userGroups = await UserGroup.find();
+      return res.json(ApiResponse.success({ userGroups }));
+    }
+    catch (err) {
+      logger.error('Error', err);
+      return res.json(ApiResponse.error('Error'));
+    }
+  };
+
   /**
   /**
    * save settings, update config cache, and response json
    * save settings, update config cache, and response json
    *
    *

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

@@ -143,6 +143,7 @@ module.exports = function(crowi, app) {
   app.post('/admin/user-group/create'      , form.admin.userGroupCreate, loginRequired(crowi, app), middleware.adminRequired(), csrf, admin.userGroup.create);
   app.post('/admin/user-group/create'      , form.admin.userGroupCreate, loginRequired(crowi, app), middleware.adminRequired(), csrf, admin.userGroup.create);
   app.post('/admin/user-group/:userGroupId/update', loginRequired(crowi, app), middleware.adminRequired(), csrf, admin.userGroup.update);
   app.post('/admin/user-group/:userGroupId/update', loginRequired(crowi, app), middleware.adminRequired(), csrf, admin.userGroup.update);
   app.post('/admin/user-group.remove' , loginRequired(crowi, app), middleware.adminRequired(), csrf, admin.userGroup.removeCompletely);
   app.post('/admin/user-group.remove' , loginRequired(crowi, app), middleware.adminRequired(), csrf, admin.userGroup.removeCompletely);
+  app.get('/_api/admin/user-groups', loginRequired(crowi, app), middleware.adminRequired(), admin.api.userGroups);
 
 
   // user-group-relations admin
   // user-group-relations admin
   app.post('/admin/user-group-relation/create', loginRequired(crowi, app), middleware.adminRequired(), csrf, admin.userGroupRelation.create);
   app.post('/admin/user-group-relation/create', loginRequired(crowi, app), middleware.adminRequired(), csrf, admin.userGroupRelation.create);

+ 1 - 34
src/server/views/admin/user-groups.html

@@ -74,40 +74,7 @@
       </div><!-- /.modal -->
       </div><!-- /.modal -->
       {% endif %}
       {% endif %}
 
 
-      <div class="modal fade" id="admin-delete-user-group-modal">
-        <div class="modal-dialog">
-          <div class="modal-content">
-            <div class="modal-header bg-danger">
-              <button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
-              <div class="modal-title">
-                <i class="icon icon-fire"></i> グループの削除
-              </div>
-            </div>
-
-            <div class="modal-body">
-              <dl>
-                <dt>グループ名</dt>
-                <dd><span id="admin-delete-user-group-name"></span></dd>
-              </dl>
-              <span class="text-danger">
-                グループの削除を行うと元に戻すことはできませんのでご注意ください。
-              </span>
-            </div>
-            <div class="modal-footer">
-              <form action="/admin/user-group.remove" method="post" id="admin-user-groups-delete" class="text-right">
-                <input type="hidden" name="user_group_id" value="">
-                <input type="hidden" name="_csrf" value="{{ csrf() }}">
-                <button type="submit" value="" class="btn btn-sm btn-danger">
-                  <i class="icon icon-fire"></i> 削除
-                </button>
-              </form>
-            </div>
-
-          </div>
-          <!-- /.modal-content -->
-        </div>
-        <!-- /.modal-dialog -->
-      </div>
+      <div class="modal fade" id="admin-delete-user-group-modal"></div>
 
 
       <h2>グループ一覧</h2>
       <h2>グループ一覧</h2>