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

Merge branch 'imprv/can-update-SES-setting-in-the-mail-setting-form' into imprv/send-test-email-with-smtp

yusuketk 5 лет назад
Родитель
Сommit
b940c98c4d
27 измененных файлов с 511 добавлено и 30 удалено
  1. 5 0
      resource/locales/en_US/translation.json
  2. 5 0
      resource/locales/ja_JP/translation.json
  3. 16 1
      resource/locales/zh_CN/translation.json
  4. 244 0
      src/client/js/components/ArchiveCreateModal.jsx
  5. 1 1
      src/client/js/components/Navbar/GrowiSubNavigation.jsx
  6. 46 0
      src/client/js/components/Page/PageShareManagement.jsx
  7. 3 3
      src/client/js/components/PageEditor/OptionsSelector.jsx
  8. 15 5
      src/client/styles/scss/theme/_apply-colors-dark.scss
  9. 10 0
      src/client/styles/scss/theme/_apply-colors-light.scss
  10. 6 3
      src/client/styles/scss/theme/_apply-colors.scss
  11. 6 2
      src/client/styles/scss/theme/_reboot-bootstrap-theme-colors.scss
  12. 1 1
      src/server/middlewares/apiv3-form-validator.js
  13. 2 0
      src/server/models/index.js
  14. 22 0
      src/server/models/page-archive.js
  15. 1 1
      src/server/routes/admin.js
  16. 1 1
      src/server/routes/apiv3/app-settings.js
  17. 1 1
      src/server/routes/apiv3/markdown-setting.js
  18. 1 1
      src/server/routes/apiv3/notification-setting.js
  19. 90 0
      src/server/routes/apiv3/page.js
  20. 6 2
      src/server/routes/apiv3/personal-setting.js
  21. 1 1
      src/server/routes/apiv3/revisions.js
  22. 1 1
      src/server/routes/apiv3/security-setting.js
  23. 1 1
      src/server/routes/apiv3/share-links.js
  24. 2 2
      src/server/routes/apiv3/user-group.js
  25. 1 1
      src/server/routes/apiv3/users.js
  26. 2 2
      src/server/routes/comment.js
  27. 21 0
      src/test/util/path-utils.test.js

+ 5 - 0
resource/locales/en_US/translation.json

@@ -50,9 +50,13 @@
   "Not available for guest": "Not available for guest",
   "Create Archive Page": "Create Archive Page",
   "File type": "File type",
+  "Target page": "Target page",
   "Include Attachment File": "Include Attachment File",
   "Include Comment": "Include Comment",
   "Include Subordinated Page": "Include Subordinated Page",
+  "All Subordinated Page": "All Subordinated Page",
+  "Specify Hierarchy": "Specify Hierarchy",
+  "Submitted the request to create the archive": "Submitted the request to create the archive",
   "username": "Username",
   "Created": "Created",
   "Last updated": "Updated",
@@ -718,6 +722,7 @@
   },
   "export_bulk": {
     "failed_to_export": "Failed to export",
+    "failed_to_count_pages": "Failed to count pages",
     "export_page_markdown": "Export page as Markdown",
     "export_page_pdf": "Export page as PDF"
   },

+ 5 - 0
resource/locales/ja_JP/translation.json

@@ -50,10 +50,14 @@
   "Presentation Mode": "プレゼンテーション",
   "Not available for guest": "ゲストユーザーは利用できません",
   "Create Archive Page": "アーカイブページの作成",
+  "Target page": "対象ページ",
   "File type": "ファイル形式",
   "Include Attachment File": "添付ファイルも含める",
   "Include Comment": "コメントも含める",
   "Include Subordinated Page": "配下ページも含める",
+  "All Subordinated Page": "全ての配下ページ",
+  "Specify Hierarchy": "階層の深さを指定",
+  "Submitted the request to create the archive": "アーカイブ作成のリクエストを正常に送信しました",
   "username": "ユーザー名",
   "Created": "作成日",
   "Last updated": "最終更新",
@@ -711,6 +715,7 @@
   },
   "export_bulk": {
     "failed_to_export": "ページのエクスポートに失敗しました",
+    "failed_to_count_pages": "ページ数の取得に失敗しました",
     "export_page_markdown": "マークダウン形式でページをエクスポート",
     "export_page_pdf": "PDF形式でページをエクスポート"
   },

+ 16 - 1
resource/locales/zh_CN/translation.json

@@ -50,7 +50,16 @@
 	"History": "历史",
 	"Presentation Mode": "演示文稿",
   "Not available for guest": "Not available for guest",
-	"username": "用户名",
+  "Create Archive Page": "Create Archive Page",
+  "File type": "File type",
+  "Target page": "Target page",
+  "Include Attachment File": "Include Attachment File",
+  "Include Comment": "Include Comment",
+  "Include Subordinated Page": "Include Subordinated Page",
+  "All Subordinated Page": "All Subordinated Page",
+  "Specify Hierarchy": "Specify Hierarchy",
+  "Submitted the request to create the archive": "Submitted the request to create the archive",
+  "username": "用户名",
 	"Created": "创建",
 	"Last updated": "上次更新",
   "Last_Login": "上次登录",
@@ -715,6 +724,12 @@
 		"Registration successful": "注册成功",
 		"Setup": "安装程序"
 	},
+  "export_bulk": {
+    "failed_to_export": "Failed to export",
+    "failed_to_count_pages": "Failed to count pages",
+    "export_page_markdown": "Export page as Markdown",
+    "export_page_pdf": "Export page as PDF"
+  },
 	"message": {
 		"successfully_connected": "连接成功!",
 		"fail_to_save_access_token": "无法保存访问令牌。请再试一次。",

+ 244 - 0
src/client/js/components/ArchiveCreateModal.jsx

@@ -0,0 +1,244 @@
+import React, { useState, useCallback } from 'react';
+import PropTypes from 'prop-types';
+import { withTranslation } from 'react-i18next';
+import {
+  Modal, ModalHeader, ModalBody, ModalFooter,
+} from 'reactstrap';
+import AppContainer from '../services/AppContainer';
+import { withUnstatedContainers } from './UnstatedUtils';
+import { toastSuccess, toastError } from '../util/apiNotification';
+
+
+const ArchiveCreateModal = (props) => {
+  const { t, appContainer } = props;
+  const [isCommentDownload, setIsCommentDownload] = useState(false);
+  const [isAttachmentFileDownload, setIsAttachmentFileDownload] = useState(false);
+  const [isSubordinatedPageDownload, setIsSubordinatedPageDownload] = useState(false);
+  const [fileType, setFileType] = useState('markdown');
+  const [hierarchyType, setHierarchyType] = useState('allSubordinatedPage');
+  const [hierarchyValue, setHierarchyValue] = useState(1);
+
+  function changeIsCommentDownloadHandler() {
+    setIsCommentDownload(!isCommentDownload);
+  }
+
+  function changeIsAttachmentFileDownloadHandler() {
+    setIsAttachmentFileDownload(!isAttachmentFileDownload);
+  }
+
+  function changeIsSubordinatedPageDownloadHandler() {
+    setIsSubordinatedPageDownload(!isSubordinatedPageDownload);
+  }
+
+  function closeModalHandler() {
+    if (props.onClose == null) {
+      return;
+    }
+
+    props.onClose();
+  }
+
+  const handleChangeFileType = useCallback(
+    (filetype) => {
+      setFileType(filetype);
+    },
+    [],
+  );
+
+  function handleChangeSubordinatedType(hierarchyType) {
+    setHierarchyType(hierarchyType);
+  }
+
+  function handleHierarchyDepth(hierarchyValue) {
+    setHierarchyValue(hierarchyValue);
+  }
+
+
+  async function done() {
+    try {
+      await appContainer.apiv3Post('/page/archive', {
+        rootPagePath: props.path,
+        isCommentDownload,
+        isAttachmentFileDownload,
+        isSubordinatedPageDownload,
+        fileType,
+        hierarchyType,
+        hierarchyValue,
+      });
+      toastSuccess(t('Submitted the request to create the archive'));
+      closeModalHandler();
+    }
+    catch (e) {
+      toastError(e);
+    }
+  }
+
+  return (
+    <Modal isOpen={props.isOpen} toggle={closeModalHandler}>
+      <ModalHeader tag="h4" toggle={closeModalHandler} className="bg-primary text-white">
+        {t('Create Archive Page')}
+      </ModalHeader>
+      <ModalBody>
+        <div className="form-group">
+          <div className="form-group">
+            <label>{t('Target page')}</label>
+            <br />
+            <code>{props.path}</code>
+          </div>
+
+          <div className="custom-control-inline">
+            <label>{t('File type')}: </label>
+          </div>
+          <div className="custom-control custom-radio custom-control-inline ">
+            <input
+              type="radio"
+              className="custom-control-input"
+              id="customRadio1"
+              name="isFileType"
+              value="customRadio1"
+              checked={fileType === 'markdown'}
+              onChange={() => {
+                handleChangeFileType('markdown');
+              }}
+            />
+            <label className="custom-control-label" htmlFor="customRadio1">
+              MarkDown(.md)
+            </label>
+          </div>
+
+          <div className="custom-control custom-radio custom-control-inline ">
+            <input
+              type="radio"
+              className="custom-control-input"
+              id="customRadio2"
+              name="isFileType"
+              value="customRadio2"
+              checked={fileType === 'pdf'}
+              onChange={() => {
+                handleChangeFileType('pdf');
+              }}
+            />
+            <label className="custom-control-label" htmlFor="customRadio2">
+              PDF(.pdf)
+            </label>
+          </div>
+        </div>
+
+        <div className="my-1 custom-control custom-checkbox custom-checkbox-info">
+          <input
+            className="custom-control-input"
+            name="comment"
+            id="commentFile"
+            type="checkbox"
+            checked={isCommentDownload}
+            onChange={changeIsCommentDownloadHandler}
+          />
+          <label className="custom-control-label" htmlFor="commentFile">
+            {t('Include Comment')}
+          </label>
+        </div>
+        <div className="my-1 custom-control custom-checkbox custom-checkbox-info">
+          <input
+            className="custom-control-input"
+            id="downloadFile"
+            type="checkbox"
+            checked={isAttachmentFileDownload}
+            onChange={changeIsAttachmentFileDownloadHandler}
+          />
+          <label className="custom-control-label" htmlFor="downloadFile">
+            {t('Include Attachment File')}
+          </label>
+        </div>
+        <div className="my-1 custom-control custom-checkbox custom-checkbox-info">
+          <input
+            className="custom-control-input"
+            id="subordinatedFile"
+            type="checkbox"
+            checked={isSubordinatedPageDownload}
+            onChange={changeIsSubordinatedPageDownloadHandler}
+          />
+          <label className="custom-control-label" htmlFor="subordinatedFile">
+            {t('Include Subordinated Page')}
+          </label>
+          {isSubordinatedPageDownload && (
+            <>
+              <div className="FormGroup">
+                <div className="my-1 custom-control custom-radio custom-control-inline ">
+                  <input
+                    type="radio"
+                    className="custom-control-input"
+                    id="customRadio3"
+                    name="isSubordinatedType"
+                    value="customRadio3"
+                    disabled={!isSubordinatedPageDownload}
+                    checked={hierarchyType === 'allSubordinatedPage'}
+                    onChange={() => {
+                      handleChangeSubordinatedType('allSubordinatedPage');
+                    }}
+                  />
+                  <label className="custom-control-label" htmlFor="customRadio3">
+                    {t('All Subordinated Page')}
+                  </label>
+                </div>
+              </div>
+              <div className="FormGroup">
+                <div className="my-1 custom-control custom-radio custom-control-inline">
+                  <input
+                    type="radio"
+                    className="custom-control-input"
+                    id="customRadio4"
+                    name="isSubordinatedType"
+                    value="customRadio4"
+                    disabled={!isSubordinatedPageDownload}
+                    checked={hierarchyType === 'decideHierarchy'}
+                    onChange={() => {
+                      handleChangeSubordinatedType('decideHierarchy');
+                    }}
+                  />
+                  <label className="my-1 custom-control-label" htmlFor="customRadio4">
+                    {t('Specify Hierarchy')}
+                  </label>
+                </div>
+              </div>
+              <div className="my-1 custom-control costom-control-inline">
+                <input
+                  type="number"
+                  min="1"
+                  max="10"
+                  disabled={hierarchyType === 'allSubordinatedPage'}
+                  value={hierarchyValue}
+                  placeholder="1"
+                  onChange={(e) => {
+                    handleHierarchyDepth(e.target.value);
+                  }}
+                />
+              </div>
+            </>
+          )}
+        </div>
+      </ModalBody>
+      <ModalFooter>
+        {/* TO DO implement correct number at GW-3053 */}
+        合計{props.totalPages}ページ取得
+        {props.errorMessage}
+        <button type="button" className="btn btn-primary" onClick={done}>
+          Done
+        </button>
+      </ModalFooter>
+    </Modal>
+  );
+};
+
+const ArchiveCreateModalWrapper = withUnstatedContainers(ArchiveCreateModal, [AppContainer]);
+
+ArchiveCreateModal.propTypes = {
+  t: PropTypes.func.isRequired, //  i18next
+  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
+  isOpen: PropTypes.bool.isRequired,
+  onClose: PropTypes.func,
+  path: PropTypes.string.isRequired,
+  totalPages: PropTypes.number,
+  errorMessage: PropTypes.string,
+};
+
+export default withTranslation()(ArchiveCreateModalWrapper);

+ 1 - 1
src/client/js/components/Navbar/GrowiSubNavigation.jsx

@@ -148,7 +148,7 @@ const GrowiSubNavigation = (props) => {
   }
 
   return (
-    <div className={`grw-subnav d-flex align-items-center justify-content-between ${isCompactMode ? 'grw-subnav-compact' : ''}`}>
+    <div className={`grw-subnav d-flex align-items-center justify-content-between ${isCompactMode ? 'grw-subnav-compact d-print-none' : ''}`}>
 
       {/* Left side */}
       <div className="d-flex">

+ 46 - 0
src/client/js/components/Page/PageShareManagement.jsx

@@ -9,13 +9,23 @@ import AppContainer from '../../services/AppContainer';
 import PageContainer from '../../services/PageContainer';
 import OutsideShareLinkModal from '../OutsideShareLinkModal';
 
+// TODO GW-2746 bulk export pages
+// import ArchiveCreateModal from '../ArchiveCreateModal';
+
 const PageShareManagement = (props) => {
   const { t, appContainer, pageContainer } = props;
 
+  // TODO GW-2746 bulk export pages
+  // eslint-disable-next-line no-unused-vars
+  const { path, pageId } = pageContainer.state;
   const { currentUser } = appContainer;
 
   const [isOutsideShareLinkModalShown, setIsOutsideShareLinkModalShown] = useState(false);
 
+  // TODO GW-2746 bulk export pages
+  // const [isArchiveCreateModalShown, setIsArchiveCreateModalShown] = useState(false);
+  // const [totalPages, setTotalPages] = useState(null);
+  // const [errorMessage, setErrorMessage] = useState(null);
 
   function openOutsideShareLinkModalHandler() {
     setIsOutsideShareLinkModalShown(true);
@@ -25,6 +35,17 @@ const PageShareManagement = (props) => {
     setIsOutsideShareLinkModalShown(false);
   }
 
+  // TODO GW-2746 bulk export pages
+  // async function getArchivePageData() {
+  //   try {
+  //     const res = await appContainer.apiv3Get('page/count-children-pages', { pageId });
+  //     setTotalPages(res.data.dummy);
+  //   }
+  //   catch (err) {
+  //     setErrorMessage(t('export_bulk.failed_to_count_pages'));
+  //   }
+  // }
+
   async function exportPageHandler(format) {
     const { pageId, revisionId } = pageContainer.state;
     const url = new URL(urljoin(window.location.origin, '_api/v3/page/export', pageId));
@@ -33,6 +54,18 @@ const PageShareManagement = (props) => {
     window.location.href = url.href;
   }
 
+  // TODO GW-2746 create api to bulk export pages
+  // function openArchiveModalHandler() {
+  //   setIsArchiveCreateModalShown(true);
+  //   getArchivePageData();
+  // }
+
+  // TODO GW-2746 create api to bulk export pages
+  // function closeArchiveCreateModalHandler() {
+  //   setIsArchiveCreateModalShown(false);
+  // }
+
+
   function renderModals() {
     if (currentUser == null) {
       return null;
@@ -44,6 +77,15 @@ const PageShareManagement = (props) => {
           isOpen={isOutsideShareLinkModalShown}
           onClose={closeOutsideShareLinkModalHandler}
         />
+
+        {/* TODO GW-2746 bulk export pages */}
+        {/* <ArchiveCreateModal
+          isOpen={isArchiveCreateModalShown}
+          onClose={closeArchiveCreateModalHandler}
+          path={path}
+          errorMessage={errorMessage}
+          totalPages={totalPages}
+        /> */}
       </>
     );
   }
@@ -91,6 +133,10 @@ const PageShareManagement = (props) => {
         <button type="button" className="dropdown-item" onClick={() => { exportPageHandler('md') }}>
           <span>{t('export_bulk.export_page_markdown')}</span>
         </button>
+        {/* TODO GW-2746 create api to bulk export pages */}
+        {/* <button className="dropdown-item" type="button" onClick={openArchiveModalHandler}>
+          <i className="icon-fw"></i>{t('Create Archive Page')}
+        </button> */}
       </div>
       {renderModals()}
     </>

+ 3 - 3
src/client/js/components/PageEditor/OptionsSelector.jsx

@@ -247,9 +247,9 @@ class OptionsSelector extends React.Component {
   render() {
     return (
       <div className="d-flex flex-row">
-        <span className="ml-2">{this.renderThemeSelector()}</span>
-        <span className="ml-2">{this.renderKeymapModeSelector()}</span>
-        <span className="ml-2">{this.renderConfigurationDropdown()}</span>
+        <span className="ml-3">{this.renderThemeSelector()}</span>
+        <span className="ml-4">{this.renderKeymapModeSelector()}</span>
+        <span className="ml-4">{this.renderConfigurationDropdown()}</span>
       </div>
     );
   }

+ 15 - 5
src/client/styles/scss/theme/_apply-colors-dark.scss

@@ -51,8 +51,8 @@ textarea.form-control {
 }
 
 .input-group > .input-group-prepend > .input-group-text {
-  color: color-yiq(theme-color('dark'));
-  background-color: theme-color('dark');
+  color: theme-color('light');
+  background-color: theme-color('secondary');
   border: 1px solid theme-color('secondary');
   border-right: none;
 }
@@ -64,6 +64,13 @@ textarea.form-control {
   background-color: $bgcolor-global;
 }
 
+.dropdown-item {
+  &:hover {
+    color: $light;
+    background-color: lighten($bgcolor-global, 15%);
+  }
+}
+
 /*
  * Table
  */
@@ -226,9 +233,12 @@ ul.pagination {
 /*
  * GROWI on-edit
  */
-.grw-editor-navbar-bottom {
-  #slack-mark-black {
-    display: none;
+body.on-edit {
+  .grw-editor-navbar-bottom {
+    background-color: $bgcolor-global;
+    #slack-mark-black {
+      display: none;
+    }
   }
 }
 

+ 10 - 0
src/client/styles/scss/theme/_apply-colors-light.scss

@@ -165,6 +165,16 @@ $table-hover-bg: $bgcolor-table-hover;
   #slack-mark-white {
     display: none;
   }
+
+  .input-group-text {
+    margin-right: 1px;
+    color: $secondary;
+    border-color: $light;
+  }
+
+  .btn.btn-outline-secondary {
+    border-color: $border-color;
+  }
 }
 
 /*

+ 6 - 3
src/client/styles/scss/theme/_apply-colors.scss

@@ -90,11 +90,14 @@ pre:not(.hljs):not(.CodeMirror-line) {
 
 .dropdown-item {
   color: $color-global;
-  &.active,
-  &:active {
+  &:active,
+  &.active {
     color: $color-dropdown-link-active;
     background-color: $bgcolor-dropdown-link-active;
   }
+  &:hover {
+    background-color: $light;
+  }
 }
 
 // Form
@@ -339,7 +342,7 @@ body.on-edit {
   }
 
   .grw-editor-navbar-bottom {
-    background-color: darken($bgcolor-global, 2%);
+    background-color: $gray-50;
   }
 }
 

+ 6 - 2
src/client/styles/scss/theme/_reboot-bootstrap-theme-colors.scss

@@ -19,10 +19,14 @@ $theme-colors: map-merge($theme-colors, $colors);
     @include button-variant($value, $value);
   }
 }
-
 @each $color, $value in $theme-colors {
   .btn-outline-#{$color} {
-    @include button-outline-variant($value);
+    @include button-outline-variant($value, $color-hover: $value, $active-background: rgba($value, 0.1), $active-border: $value);
+    &:not(:disabled):not(.disabled):active,
+    &:not(:disabled):not(.disabled).active,
+    .show > &.dropdown-toggle {
+      color: $value;
+    }
   }
 }
 

+ 1 - 1
src/server/middlewares/apiv3-form-validator.js

@@ -1,5 +1,5 @@
 const logger = require('@alias/logger')('growi:middlewares:ApiV3FormValidator');
-const { validationResult } = require('express-validator/check');
+const { validationResult } = require('express-validator');
 
 const ErrorV3 = require('../models/vo/error-apiv3');
 

+ 2 - 0
src/server/models/index.js

@@ -1,6 +1,8 @@
 module.exports = {
   Config: require('./config'),
   Page: require('./page'),
+  // TODO GW-2746 bulk export pages
+  // PageArchive: require('./page-archive'),
   PageTagRelation: require('./page-tag-relation'),
   User: require('./user'),
   ExternalAccount: require('./external-account'),

+ 22 - 0
src/server/models/page-archive.js

@@ -0,0 +1,22 @@
+module.exports = function(crowi) {
+  const mongoose = require('mongoose');
+  const ObjectId = mongoose.Schema.Types.ObjectId;
+
+  const pageArchiveSchema = new mongoose.Schema({
+    owner: {
+      type: ObjectId,
+      ref: 'User',
+      index: true,
+      required: true,
+    },
+    rootPagePath: { type: String, required: true },
+    fileType: { type: String, enum: ['pdf', 'markdown'], required: true },
+    numOfPages: { type: Number, required: true },
+    hasComment: { type: Boolean, required: true },
+    hasAttachment: { type: Boolean, required: true },
+  }, {
+    timestamps: true,
+  });
+
+  return mongoose.model('PageArchive', pageArchiveSchema);
+};

+ 1 - 1
src/server/routes/admin.js

@@ -22,7 +22,7 @@ module.exports = function(crowi, app) {
   const MAX_PAGE_LIST = 50;
   const actions = {};
 
-  const { check } = require('express-validator/check');
+  const { check } = require('express-validator');
 
   const api = {};
 

+ 1 - 1
src/server/routes/apiv3/app-settings.js

@@ -10,7 +10,7 @@ const { listLocaleIds } = require('@commons/util/locale-utils');
 
 const router = express.Router();
 
-const { body } = require('express-validator/check');
+const { body } = require('express-validator');
 const ErrorV3 = require('../../models/vo/error-apiv3');
 
 /**

+ 1 - 1
src/server/routes/apiv3/markdown-setting.js

@@ -6,7 +6,7 @@ const express = require('express');
 
 const router = express.Router();
 
-const { body } = require('express-validator/check');
+const { body } = require('express-validator');
 
 const ErrorV3 = require('../../models/vo/error-apiv3');
 

+ 1 - 1
src/server/routes/apiv3/notification-setting.js

@@ -7,7 +7,7 @@ const express = require('express');
 
 const router = express.Router();
 
-const { body } = require('express-validator/check');
+const { body } = require('express-validator');
 
 const ErrorV3 = require('../../models/vo/error-apiv3');
 const removeNullPropertyFromObject = require('../../../lib/util/removeNullPropertyFromObject');

+ 90 - 0
src/server/routes/apiv3/page.js

@@ -129,6 +129,15 @@ module.exports = (crowi) => {
       query('format').isString().isIn(['md', 'pdf']),
       query('revisionId').isString(),
     ],
+    archive: [
+      body('rootPagePath').isString(),
+      body('isCommentDownload').isBoolean(),
+      body('isAttachmentFileDownload').isBoolean(),
+      body('isSubordinatedPageDownload').isBoolean(),
+      body('fileType').isString().isIn(['pdf', 'markdown']),
+      body('hierarchyType').isString().isIn(['allSubordinatedPage', 'decideHierarchy']),
+      body('hierarchyValue').isNumeric(),
+    ],
   };
 
   /**
@@ -244,5 +253,86 @@ module.exports = (crowi) => {
     return stream.pipe(res);
   });
 
+  // TODO GW-2746 bulk export pages
+  // /**
+  //  * @swagger
+  //  *
+  //  *    /page/archive:
+  //  *      post:
+  //  *        tags: [Page]
+  //  *        summary: /page/archive
+  //  *        description: create page archive
+  //  *        requestBody:
+  //  *          content:
+  //  *            application/json:
+  //  *              schema:
+  //  *                properties:
+  //  *                  rootPagePath:
+  //  *                    type: string
+  //  *                    description: path of the root page
+  //  *                  isCommentDownload:
+  //  *                    type: boolean
+  //  *                    description: whether archive data contains comments
+  //  *                  isAttachmentFileDownload:
+  //  *                    type: boolean
+  //  *                    description: whether archive data contains attachments
+  //  *                  isSubordinatedPageDownload:
+  //  *                    type: boolean
+  //  *                    description: whether archive data children pages
+  //  *                  fileType:
+  //  *                    type: string
+  //  *                    description: file type of archive data(.md, .pdf)
+  //  *                  hierarchyType:
+  //  *                    type: string
+  //  *                    description: method of select children pages archive data contains('allSubordinatedPage', 'decideHierarchy')
+  //  *                  hierarchyValue:
+  //  *                    type: number
+  //  *                    description: depth of hierarchy(use when hierarchyType is 'decideHierarchy')
+  //  *        responses:
+  //  *          200:
+  //  *            description: create page archive
+  //  *            content:
+  //  *              application/json:
+  //  *                schema:
+  //  *                  $ref: '#/components/schemas/Page'
+  //  */
+  // router.post('/archive', accessTokenParser, loginRequired, csrf, validator.archive, apiV3FormValidator, async(req, res) => {
+  //   const PageArchive = crowi.model('PageArchive');
+
+  //   const {
+  //     rootPagePath,
+  //     isCommentDownload,
+  //     isAttachmentFileDownload,
+  //     fileType,
+  //   } = req.body;
+  //   const owner = req.user._id;
+
+  //   const numOfPages = 1; // TODO 最終的にzipファイルに取り込むページ数を入れる
+
+  //   const createdPageArchive = PageArchive.create({
+  //     owner,
+  //     fileType,
+  //     rootPagePath,
+  //     numOfPages,
+  //     hasComment: isCommentDownload,
+  //     hasAttachment: isAttachmentFileDownload,
+  //   });
+
+  //   console.log(createdPageArchive);
+  //   return res.apiv3({ });
+
+  // });
+
+  // router.get('/count-children-pages', accessTokenParser, loginRequired, async(req, res) => {
+
+  //   // TO DO implement correct number at another task
+
+  //   const { pageId } = req.query;
+  //   console.log(pageId);
+
+  //   const dummy = 6;
+  //   return res.apiv3({ dummy });
+  // });
+
   return router;
 };

+ 6 - 2
src/server/routes/apiv3/personal-setting.js

@@ -11,7 +11,7 @@ const { listLocaleIds } = require('@commons/util/locale-utils');
 
 const router = express.Router();
 
-const { body } = require('express-validator/check');
+const { body } = require('express-validator');
 const ErrorV3 = require('../../models/vo/error-apiv3');
 
 /**
@@ -127,9 +127,13 @@ module.exports = (crowi) => {
 
     try {
       const user = await User.findUserByUsername(username);
+
+      // return email whether it's private
+      const { email } = user;
       const currentUser = user.toObject();
-      return res.apiv3({ currentUser });
+      currentUser.email = email;
 
+      return res.apiv3({ currentUser });
     }
     catch (err) {
       logger.error(err);

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

@@ -4,7 +4,7 @@ const logger = loggerFactory('growi:routes:apiv3:pages');
 
 const express = require('express');
 
-const { query, param } = require('express-validator/check');
+const { query, param } = require('express-validator');
 const ErrorV3 = require('../../models/vo/error-apiv3');
 
 const router = express.Router();

+ 1 - 1
src/server/routes/apiv3/security-setting.js

@@ -6,7 +6,7 @@ const express = require('express');
 
 const router = express.Router();
 
-const { body } = require('express-validator/check');
+const { body } = require('express-validator');
 const ErrorV3 = require('../../models/vo/error-apiv3');
 const removeNullPropertyFromObject = require('../../../lib/util/removeNullPropertyFromObject');
 

+ 1 - 1
src/server/routes/apiv3/share-links.js

@@ -8,7 +8,7 @@ const express = require('express');
 
 const router = express.Router();
 
-const { body } = require('express-validator/check');
+const { body } = require('express-validator');
 
 const ErrorV3 = require('../../models/vo/error-apiv3');
 

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

@@ -6,8 +6,8 @@ const express = require('express');
 
 const router = express.Router();
 
-const { body, param, query } = require('express-validator/check');
-const { sanitizeQuery } = require('express-validator/filter');
+const { body, param, query } = require('express-validator');
+const { sanitizeQuery } = require('express-validator');
 
 const mongoose = require('mongoose');
 

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

@@ -6,7 +6,7 @@ const express = require('express');
 
 const router = express.Router();
 
-const { body, query } = require('express-validator/check');
+const { body, query } = require('express-validator');
 const { isEmail } = require('validator');
 
 const ErrorV3 = require('../../models/vo/error-apiv3');

+ 2 - 2
src/server/routes/comment.js

@@ -49,7 +49,7 @@ module.exports = function(crowi, app) {
   const GlobalNotificationSetting = crowi.model('GlobalNotificationSetting');
   const ApiResponse = require('../util/apiResponse');
   const globalNotificationService = crowi.getGlobalNotificationService();
-  const { body } = require('express-validator/check');
+  const { body } = require('express-validator');
   const mongoose = require('mongoose');
   const ObjectId = mongoose.Types.ObjectId;
 
@@ -208,7 +208,7 @@ module.exports = function(crowi, app) {
    */
   api.add = async function(req, res) {
     const { commentForm, slackNotificationForm } = req.body;
-    const { validationResult } = require('express-validator/check');
+    const { validationResult } = require('express-validator');
 
     const errors = validationResult(req.body);
     if (!errors.isEmpty()) {

+ 21 - 0
src/test/util/path-utils.test.js

@@ -0,0 +1,21 @@
+const { isTopPage } = require('../../lib/util/path-utils');
+
+
+describe('TopPage Path test', () => {
+  test('Path is only "/"', () => {
+    const result = isTopPage('/');
+    expect(result).toBe(true);
+  });
+  test('Path is not match string ', () => {
+    const result = isTopPage('/test');
+    expect(result).toBe(false);
+  });
+  test('Path is integer', () => {
+    const result = isTopPage(1);
+    expect(result).toBe(false);
+  });
+  test('Path is null', () => {
+    const result = isTopPage(null);
+    expect(result).toBe(false);
+  });
+});