Răsfoiți Sursa

Merge branch 'imprv/refacter-rename-recursively' into imprv/gw-4776

takeru0001 5 ani în urmă
părinte
comite
a4092811a4

+ 25 - 0
src/client/js/components/SearchForm.jsx

@@ -13,10 +13,13 @@ class SearchForm extends React.Component {
 
     this.state = {
       searchError: null,
+      isShownHelp: false,
     };
 
     this.onSearchError = this.onSearchError.bind(this);
     this.onChange = this.onChange.bind(this);
+    this.onBlur = this.onBlur.bind(this);
+    this.onFocus = this.onFocus.bind(this);
   }
 
   componentDidMount() {
@@ -40,12 +43,28 @@ class SearchForm extends React.Component {
     }
   }
 
+  onBlur() {
+    this.setState({
+      isShownHelp: false,
+    });
+
+    this.getHelpElement();
+  }
+
+  onFocus() {
+    this.setState({
+      isShownHelp: true,
+    });
+  }
+
   getHelpElement() {
     const { t, appContainer } = this.props;
+    const { isShownHelp } = this.state;
 
     const config = appContainer.getConfig();
     const isReachable = config.isSearchServiceReachable;
 
+
     if (!isReachable) {
       return (
         <>
@@ -55,6 +74,10 @@ class SearchForm extends React.Component {
       );
     }
 
+    if (!isShownHelp) {
+      return null;
+    }
+
     return (
       <table className="table grw-search-table search-help m-0">
         <caption className="text-left text-primary p-2">
@@ -124,6 +147,8 @@ class SearchForm extends React.Component {
         placeholder={placeholder}
         helpElement={this.getHelpElement()}
         keywordOnInit={this.props.keyword}
+        onBlur={this.onBlur}
+        onFocus={this.onFocus}
       />
     );
   }

+ 4 - 0
src/client/js/components/SearchTypeahead.jsx

@@ -222,6 +222,8 @@ class SearchTypeahead extends React.Component {
           renderMenuItemChildren={this.renderMenuItemChildren}
           caseSensitive={false}
           defaultSelected={defaultSelected}
+          onBlur={this.props.onBlur}
+          onFocus={this.props.onFocus}
         />
         {resetFormButton}
       </div>
@@ -244,6 +246,8 @@ SearchTypeahead.propTypes = {
   onSearchSuccess: PropTypes.func,
   onSearchError:   PropTypes.func,
   onChange:        PropTypes.func,
+  onBlur:          PropTypes.func,
+  onFocus:         PropTypes.func,
   onSubmit:        PropTypes.func,
   onInputChange:   PropTypes.func,
   inputName:       PropTypes.string,

+ 8 - 0
src/client/styles/scss/_on-edit.scss

@@ -134,6 +134,13 @@ body.on-edit {
 
     .grw-taglabels-container {
       margin-bottom: 0;
+
+      // To scroll tags horizontally
+      .grw-tag-labels.form-inline {
+        flex-flow: row nowrap;
+        width: 100%;
+        overflow-x: scroll;
+      }
     }
   }
 
@@ -141,6 +148,7 @@ body.on-edit {
   .grw-subnav-left-side {
     overflow: hidden;
     .grw-path-nav-container {
+      margin-right: 1rem;
       overflow: hidden;
       .grw-page-path-nav {
         white-space: nowrap;

+ 1 - 87
src/server/models/page.js

@@ -1142,30 +1142,6 @@ module.exports = function(crowi) {
     }));
   };
 
-  // TODO: transplant to service/page.js because page deletion affects various models data
-  pageSchema.statics.revertDeletedPage = async function(page, user, options = {}) {
-    const newPath = this.getRevertDeletedPageName(page.path);
-
-    const originPage = await this.findByPath(newPath);
-    if (originPage != null) {
-      // 削除時、元ページの path には必ず redirectTo 付きで、ページが作成される。
-      // そのため、そいつは削除してOK
-      // が、redirectTo ではないページが存在している場合それは何かがおかしい。(データ補正が必要)
-      if (originPage.redirectTo !== page.path) {
-        throw new Error('The new page of to revert is exists and the redirect path of the page is not the deleted page.');
-      }
-
-      await this.completelyDeletePage(originPage, options);
-    }
-
-    page.status = STATUS_PUBLISHED;
-    page.lastUpdateUser = user;
-    debug('Revert deleted the page', page, newPath);
-    const updatedPage = await this.rename(page, newPath, user, {});
-
-    return updatedPage;
-  };
-
   pageSchema.statics.revertDeletedPageRecursively = async function(targetPage, user, options = {}) {
     const findOpts = { includeTrashed: true };
     const pages = await this.findManageableListWithDescendants(targetPage, user, findOpts);
@@ -1173,7 +1149,7 @@ module.exports = function(crowi) {
     let updatedPage = null;
     await Promise.all(pages.map((page) => {
       const isParent = (page.path === targetPage.path);
-      const p = this.revertDeletedPage(page, user, options);
+      const p = crowi.pageService.revertDeletedPage(page, user, options);
       if (isParent) {
         updatedPage = p;
       }
@@ -1183,41 +1159,6 @@ module.exports = function(crowi) {
     return updatedPage;
   };
 
-  /**
-   * This is danger.
-   */
-  // TODO: transplant to service/page.js because page deletion affects various models data
-  pageSchema.statics.completelyDeletePage = async function(pageData, user, options = {}) {
-    validateCrowi();
-
-    const { _id, path } = pageData;
-    const socketClientId = options.socketClientId || null;
-
-    logger.debug('Deleting completely', path);
-
-    await crowi.pageService.deleteCompletely(_id, path);
-
-    if (socketClientId != null) {
-      pageEvent.emit('delete', pageData, user, socketClientId); // update as renamed page
-    }
-    return pageData;
-  };
-
-  /**
-   * Delete Bookmarks, Attachments, Revisions, Pages and emit delete
-   */
-  // TODO: transplant to service/page.js because page deletion affects various models data
-  pageSchema.statics.completelyDeletePageRecursively = async function(targetPage, user, options = {}) {
-    const findOpts = { includeTrashed: true };
-
-    // find manageable descendants (this array does not include GRANT_RESTRICTED)
-    const pages = await this.findManageableListWithDescendants(targetPage, user, findOpts);
-
-    await Promise.all(pages.map((page) => {
-      return this.completelyDeletePage(page, user, options);
-    }));
-  };
-
   pageSchema.statics.removeByPath = function(path) {
     if (path == null) {
       throw new Error('path is required');
@@ -1339,33 +1280,6 @@ module.exports = function(crowi) {
     return await queryBuilder.query.exec();
   };
 
-  // TODO: transplant to service/page.js because page deletion affects various models data
-  pageSchema.statics.handlePrivatePagesForDeletedGroup = async function(deletedGroup, action, transferToUserGroupId) {
-    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, transferToUserGroupId);
-        }));
-        break;
-      default:
-        throw new Error('Unknown action for private pages');
-    }
-  };
-
   pageSchema.statics.publicizePage = async function(page) {
     page.grantedGroup = null;
     page.grant = GRANT_PUBLIC;

+ 1 - 2
src/server/models/user-group.js

@@ -92,7 +92,6 @@ class UserGroup {
   // グループの完全削除
   static async removeCompletelyById(deleteGroupId, action, transferToUserGroupId) {
     const UserGroupRelation = mongoose.model('UserGroupRelation');
-    const Page = mongoose.model('Page');
 
     const groupToDelete = await this.findById(deleteGroupId);
     if (groupToDelete == null) {
@@ -102,7 +101,7 @@ class UserGroup {
 
     await Promise.all([
       UserGroupRelation.removeAllByUserGroup(deletedGroup),
-      Page.handlePrivatePagesForDeletedGroup(deletedGroup, action, transferToUserGroupId),
+      UserGroup.crowi.pageService.handlePrivatePagesForDeletedGroup(deletedGroup, action, transferToUserGroupId),
     ]);
 
     return deletedGroup;

+ 1 - 1
src/server/routes/apiv3/pages.js

@@ -432,7 +432,7 @@ module.exports = (crowi) => {
    */
   router.delete('/empty-trash', loginRequired, adminRequired, csrf, async(req, res) => {
     try {
-      const pages = await Page.completelyDeletePageRecursively({ path: '/trash' }, req.user);
+      const pages = await crowi.pageService.completelyDeletePageRecursively({ path: '/trash' }, req.user);
       return res.apiv3({ pages });
     }
     catch (err) {

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

@@ -181,6 +181,8 @@ module.exports = (crowi) => {
       [sort]: (sortOrder === 'desc') ? -1 : 1,
     };
 
+    //  For more information about the external specification of the User API, see here (https://dev.growi.org/5fd7466a31d89500488248e3)
+
     const orConditions = [
       { name: { $in: searchWord } },
       { username: { $in: searchWord } },

+ 3 - 3
src/server/routes/page.js

@@ -1194,10 +1194,10 @@ module.exports = function(crowi, app) {
           return res.json(ApiResponse.error('You can not delete completely', 'user_not_admin'));
         }
         if (isRecursively) {
-          await Page.completelyDeletePageRecursively(page, req.user, options);
+          await crowi.pageService.completelyDeletePageRecursively(page, req.user, options);
         }
         else {
-          await Page.completelyDeletePage(page, req.user, options);
+          await crowi.pageService.completelyDeletePage(page, req.user, options);
         }
       }
       else {
@@ -1258,7 +1258,7 @@ module.exports = function(crowi, app) {
         page = await Page.revertDeletedPageRecursively(page, req.user, { socketClientId });
       }
       else {
-        page = await Page.revertDeletedPage(page, req.user, { socketClientId });
+        page = await crowi.pageService.revertDeletedPage(page, req.user, { socketClientId });
       }
     }
     catch (err) {

+ 95 - 0
src/server/service/page.js

@@ -1,7 +1,11 @@
 const mongoose = require('mongoose');
 const escapeStringRegexp = require('escape-string-regexp');
+const logger = require('@alias/logger')('growi:models:page');
+const debug = require('debug')('growi:models:page');
 const { serializePageSecurely } = require('../models/serializers/page-serializer');
 
+const STATUS_PUBLISHED = 'published';
+
 class PageService {
 
   constructor(crowi) {
@@ -95,6 +99,97 @@ class PageService {
   }
 
 
+  async completelyDeletePage(pageData, user, options = {}) {
+    this.validateCrowi();
+    let pageEvent;
+    // init event
+    if (this.crowi != null) {
+      pageEvent = this.crowi.event('page');
+      pageEvent.on('create', pageEvent.onCreate);
+      pageEvent.on('update', pageEvent.onUpdate);
+    }
+
+    const { _id, path } = pageData;
+    const socketClientId = options.socketClientId || null;
+
+    logger.debug('Deleting completely', path);
+
+    await this.deleteCompletely(_id, path);
+
+    if (socketClientId != null) {
+      pageEvent.emit('delete', pageData, user, socketClientId); // update as renamed page
+    }
+    return pageData;
+  }
+
+  /**
+   * Delete Bookmarks, Attachments, Revisions, Pages and emit delete
+   */
+  async completelyDeletePageRecursively(targetPage, user, options = {}) {
+    const findOpts = { includeTrashed: true };
+    const Page = this.crowi.model('Page');
+
+    // find manageable descendants (this array does not include GRANT_RESTRICTED)
+    const pages = await Page.findManageableListWithDescendants(targetPage, user, findOpts);
+
+    await Promise.all(pages.map((page) => {
+      return this.completelyDeletePage(page, user, options);
+    }));
+  }
+
+
+  async revertDeletedPage(page, user, options = {}) {
+    const Page = this.crowi.model('Page');
+    const newPath = Page.getRevertDeletedPageName(page.path);
+    const originPage = await Page.findByPath(newPath);
+    if (originPage != null) {
+      // When the page is deleted, it will always be created with "redirectTo" in the path of the original page.
+      // So, it's ok to delete the page
+      // However, If a page exists that is not "redirectTo", something is wrong. (Data correction is needed).
+      if (originPage.redirectTo !== page.path) {
+        throw new Error('The new page of to revert is exists and the redirect path of the page is not the deleted page.');
+      }
+      await this.completelyDeletePage(originPage, options);
+    }
+
+    page.status = STATUS_PUBLISHED;
+    page.lastUpdateUser = user;
+    debug('Revert deleted the page', page, newPath);
+    const updatedPage = await Page.rename(page, newPath, user, {});
+    return updatedPage;
+  }
+
+  async handlePrivatePagesForDeletedGroup(deletedGroup, action, transferToUserGroupId) {
+    const Page = this.crowi.model('Page');
+    const pages = await Page.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 this.completelyDeletePage(page);
+        }));
+        break;
+      case 'transfer':
+        await Promise.all(pages.map((page) => {
+          return Page.transferPageToGroup(page, transferToUserGroupId);
+        }));
+        break;
+      default:
+        throw new Error('Unknown action for private pages');
+    }
+  }
+
+  validateCrowi() {
+    if (this.crowi == null) {
+      throw new Error('"crowi" is null. Init User model with "crowi" argument first.');
+    }
+  }
+
 }
 
 module.exports = PageService;