Explorar el Código

Merge branch 'feat/bulk-export-pages-for-merge' into feat/define-archive-model

# Conflicts:
#	src/client/js/components/ArchiveCreateModal.jsx
#	src/server/routes/apiv3/page.js
yusuketk hace 5 años
padre
commit
015e5a6708

+ 5 - 3
package.json

@@ -54,8 +54,9 @@
     "preserver:prod": "npm run migrate",
     "prestart": "npm run build:prod",
     "resource": "node bin/download-cdn-resources.js",
-    "translations": "rs-i18n -lan zh_CN -t",
-    "i18n-json-merge:noTran": "rs-i18n -lan zh_CN",
+    "i18n-json-merge:withTran": "rs-i18n -lan -- -t",
+    "i18n-json-merge:noTran": "rs-i18n -lan --",
+    "i18n-json-merge": "npm run i18n-json-merge:withTran --",
     "server:nolazy": "env-cmd -f config/env.dev.js node-dev --nolazy --inspect src/server/app.js",
     "server:dev": "env-cmd -f config/env.dev.js node-dev --inspect src/server/app.js",
     "server:prod:ci": "npm run server:prod -- --ci",
@@ -183,10 +184,11 @@
     "diff2html": "^3.1.2",
     "eazy-logger": "^3.0.2",
     "eslint": "^6.0.1",
-    "eslint-config-weseek": "^1.0.3",
+    "eslint-config-weseek": "^1.0.4",
     "eslint-plugin-import": "^2.18.0",
     "eslint-plugin-jest": "^23.0.3",
     "eslint-plugin-react": "^7.14.2",
+    "eslint-plugin-react-hooks": "^4.0.4",
     "file-loader": "^5.0.2",
     "handsontable": "=6.2.2",
     "hard-source-webpack-plugin": "^0.13.1",

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

@@ -54,6 +54,9 @@
   "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",

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

@@ -54,6 +54,9 @@
   "Include Attachment File": "添付ファイルも含める",
   "Include Comment": "コメントも含める",
   "Include Subordinated Page": "配下ページも含める",
+  "All Subordinated Page": "全ての配下ページ",
+  "Specify Hierarchy": "階層の深さを指定",
+  "Submitted the request to create the archive": "アーカイブ作成のリクエストを正常に送信しました",
   "username": "ユーザー名",
   "Created": "作成日",
   "Last updated": "最終更新",

+ 3 - 3
resource/locales/zh_CN/meta.json

@@ -1,4 +1,4 @@
 {
-  "id": "zh_CN",
-  "displayName": "简体中文"
-}
+	"id": "zh_CN",
+	"displayName": "简体中文"
+}

+ 24 - 0
resource/locales/zh_CN/translation.json

@@ -698,5 +698,29 @@
 		"Sign in error": "登录错误",
 		"Registration successful": "注册成功",
 		"Setup": "安装程序"
+	},
+	"message": {
+		"successfully_connected": "连接成功!",
+		"fail_to_save_access_token": "无法保存访问令牌。请再试一次。",
+		"fail_to_fetch_access_token": "无法获取访问令牌。请重新连接。",
+		"successfully_disconnected": "成功断开连接!",
+		"strategy_has_not_been_set_up": "{{strategy}尚未设置",
+		"maximum_number_of_users": "注册的用户数不能超过最大值。",
+		"database_error": "发生数据库服务器错误",
+		"sign_in_failure": "登录失败。",
+		"aws_sttings_required": "使用此功能所需的AWS设置。请询问管理员。",
+		"application_already_installed": "应用程序已安装。",
+		"email_address_could_not_be_used": "无法使用此电子邮件地址。(确保允许的电子邮件地址)",
+		"user_id_is_not_available.": "此用户ID不可用。",
+		"email_address_is_already_registered": "此电子邮件地址已注册。",
+		"can_not_register_maximum_number_of_users": "注册的用户数不能超过最大值。",
+		"failed_to_register": "注册失败。",
+		"successfully_created": "已成功创建用户{{username}。",
+		"can_not_activate_maximum_number_of_users": "无法激活超过最大用户数的用户。",
+		"failed_to_activate": "无法激活。",
+		"unable_to_use_this_user": "无法使用此用户。",
+		"complete_to_install1": "完成安装GROWI!请以管理员帐户登录。",
+		"complete_to_install2": "完成安装GROWI!请先检查此页上的每个设置。",
+		"failed_to_create_admin_user": "无法创建管理用户。{{errMessage}"
 	}
 }

+ 96 - 15
src/client/js/components/ArchiveCreateModal.jsx

@@ -6,22 +6,24 @@ import {
 } from 'reactstrap';
 import AppContainer from '../services/AppContainer';
 import { withUnstatedContainers } from './UnstatedUtils';
+import { toastSuccess, toastError } from '../util/apiNotification';
 
 
 const ArchiveCreateModal = (props) => {
-  const { t } = props;
+  const { t, appContainer } = props;
   const [isCommentDownload, setIsCommentDownload] = useState(false);
-  const [isFileDownload, setIsFileDownload] = 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 changeIsFileDownloadHandler() {
-    setIsFileDownload(!isFileDownload);
+
+  function changeIsAttachmentFileDownloadHandler() {
+    setIsAttachmentFileDownload(!isAttachmentFileDownload);
   }
 
   function changeIsSubordinatedPageDownloadHandler() {
@@ -43,12 +45,34 @@ const ArchiveCreateModal = (props) => {
     [],
   );
 
-  async function submit() {
-    await props.appContainer.apiv3Post('page/archive');
+  function handleChangeSubordinatedType(hierarchyType) {
+    setHierarchyType(hierarchyType);
+  }
+
+  function handleHierarchyDepth(hierarchyValue) {
+    setHierarchyValue(hierarchyValue);
+  }
+
+  async function done() {
+
+    try {
+      await appContainer.apiv3Post('/page/archive', {
+        isCommentDownload,
+        isAttachmentFileDownload,
+        isSubordinatedPageDownload,
+        fileType,
+        hierarchyType,
+        hierarchyValue,
+      });
+      toastSuccess(t('Submitted the request to create the archive'));
+    }
+    catch (e) {
+      toastError(e);
+    }
   }
 
   return (
-    <Modal size="lg" isOpen={props.isOpen} toggle={closeModalHandler}>
+    <Modal isOpen={props.isOpen} toggle={closeModalHandler}>
       <ModalHeader tag="h4" toggle={closeModalHandler} className="bg-primary text-white">
         {t('Create Archive Page')}
       </ModalHeader>
@@ -98,7 +122,7 @@ const ArchiveCreateModal = (props) => {
           </div>
         </div>
 
-        <div className="custom-control custom-checkbox custom-checkbox-warning">
+        <div className="my-1 custom-control custom-checkbox custom-checkbox-info">
           <input
             className="custom-control-input"
             name="comment"
@@ -111,19 +135,19 @@ const ArchiveCreateModal = (props) => {
             {t('Include Comment')}
           </label>
         </div>
-        <div className="custom-control custom-checkbox custom-checkbox-warning">
+        <div className="my-1 custom-control custom-checkbox custom-checkbox-info">
           <input
             className="custom-control-input"
             id="downloadFile"
             type="checkbox"
-            checked={isFileDownload}
-            onChange={changeIsFileDownloadHandler}
+            checked={isAttachmentFileDownload}
+            onChange={changeIsAttachmentFileDownloadHandler}
           />
           <label className="custom-control-label" htmlFor="downloadFile">
             {t('Include Attachment File')}
           </label>
         </div>
-        <div className="custom-control custom-checkbox custom-checkbox-warning">
+        <div className="my-1 custom-control custom-checkbox custom-checkbox-info">
           <input
             className="custom-control-input"
             id="subordinatedFile"
@@ -134,10 +158,65 @@ const ArchiveCreateModal = (props) => {
           <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="0"
+                  max="10"
+                  disabled={hierarchyType === 'allSubordinatedPage'}
+                  value={hierarchyValue}
+                  placeholder="1"
+                  onChange={(e) => {
+                    handleHierarchyDepth(e.target.value);
+                  }}
+                />
+              </div>
+            </>
+          )}
         </div>
       </ModalBody>
       <ModalFooter>
-        <button type="button" className="btn btn-primary" onClick={submit}>
+        <button type="button" className="btn btn-primary" onClick={done}>
           Done
         </button>
       </ModalFooter>
@@ -145,9 +224,11 @@ const ArchiveCreateModal = (props) => {
   );
 };
 
+const ArchiveCreateModalWrapper = withUnstatedContainers(ArchiveCreateModal, [AppContainer]);
 
 ArchiveCreateModal.propTypes = {
   t: PropTypes.func.isRequired, //  i18next
+  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
   isOpen: PropTypes.bool.isRequired,
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
   onClose: PropTypes.func,

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

@@ -77,7 +77,7 @@ class SearchTop extends React.Component {
     const isReachable = config.isSearchServiceReachable;
 
     return (
-      <div className={`form-group mb-0 ${isReachable ? '' : 'has-error'}`}>
+      <div className={`form-group mb-0 d-print-none ${isReachable ? '' : 'has-error'}`}>
         <div className="input-group flex-nowrap">
           <div className="input-group-prepend">
             <button className="btn btn-secondary dropdown-toggle py-0" type="button" data-toggle="dropdown" aria-haspopup="true">

+ 2 - 0
src/client/js/components/PageEditor.jsx

@@ -21,6 +21,8 @@ class PageEditor extends React.Component {
   constructor(props) {
     super(props);
 
+    this.previewElement = React.createRef();
+
     const config = this.props.appContainer.getConfig();
     const isUploadable = config.upload.image || config.upload.file;
     const isUploadableFile = config.upload.file;

+ 4 - 2
src/client/js/components/PageEditor/Preview.jsx

@@ -82,7 +82,9 @@ class Preview extends React.PureComponent {
             className="page-editor-preview-body"
             ref={(elm) => {
                 this.previewElement = elm;
-                this.props.inputRef(elm);
+                if (this.props.inputRef != null) {
+                  this.props.inputRef(elm);
+                }
               }}
             onScroll={(event) => {
                 if (this.props.onScroll != null) {
@@ -112,7 +114,7 @@ Preview.propTypes = {
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
 
   markdown: PropTypes.string,
-  inputRef: PropTypes.func.isRequired, // for getting div element
+  inputRef: PropTypes.func,
   isMathJaxEnabled: PropTypes.bool,
   renderMathJaxOnInit: PropTypes.bool,
   onScroll: PropTypes.func,

+ 1 - 1
src/client/js/components/Sidebar.jsx

@@ -167,7 +167,7 @@ class Sidebar extends React.Component {
 
     return (
       <>
-        <div className={`grw-sidebar ${this.isDrawerMode ? 'grw-sidebar-drawer' : ''} ${isDrawerOpened ? 'open' : ''}`}>
+        <div className={`grw-sidebar d-print-none ${this.isDrawerMode ? 'grw-sidebar-drawer' : ''} ${isDrawerOpened ? 'open' : ''}`}>
           <ThemeProvider
             theme={theme => ({
               ...theme,

+ 6 - 6
src/client/js/components/StickyStretchableScroller.jsx

@@ -91,7 +91,7 @@ const StickyStretchableScroller = (props) => {
     if (contentsHeight < viewHeight) {
       $(scrollTargetSelector).slimScroll({ destroy: true });
     }
-  }, [contentsElemSelector, calcViewHeightFunc, calcContentsHeightFunc]);
+  }, [contentsElemSelector, calcViewHeightFunc, calcContentsHeightFunc, scrollTargetSelector]);
 
   const resetScrollbarDebounced = debounce(100, resetScrollbar);
 
@@ -99,7 +99,7 @@ const StickyStretchableScroller = (props) => {
   const stickyChangeHandler = useCallback((event) => {
     logger.debug('StickyEvents.CHANGE detected');
     resetScrollbar();
-  });
+  }, [resetScrollbar]);
 
   // setup effect by sticky event
   useEffect(() => {
@@ -118,7 +118,7 @@ const StickyStretchableScroller = (props) => {
     return () => {
       elem.removeEventListener(StickyEvents.CHANGE, stickyChangeHandler);
     };
-  }, []);
+  }, [stickyElemSelector, stickyChangeHandler]);
 
   // setup effect by resizing event
   useEffect(() => {
@@ -132,19 +132,19 @@ const StickyStretchableScroller = (props) => {
     return () => {
       window.removeEventListener('resize', resizeHandler);
     };
-  }, []);
+  }, [resetScrollbarDebounced]);
 
   // setup effect by isScrollTop
   useEffect(() => {
     if (navigationContainer.state.isScrollTop) {
       resetScrollbar();
     }
-  }, [navigationContainer.state.isScrollTop]);
+  }, [navigationContainer.state.isScrollTop, resetScrollbar]);
 
   // setup effect by update props
   useEffect(() => {
     resetScrollbarDebounced();
-  });
+  }, [resetScrollbarDebounced]);
 
   return (
     <>

+ 1 - 1
src/client/js/components/TableOfContents.jsx

@@ -27,7 +27,7 @@ const TableOfContents = (props) => {
 
     // window height - revisionToc top - .system-version height
     return window.innerHeight - containerTop - 20;
-  });
+  }, []);
 
   const { tocHtml } = pageContainer.state;
 

+ 4 - 0
src/client/styles/scss/_layout.scss

@@ -75,6 +75,10 @@ body {
       }
     }
 
+    .row {
+      display: block !important;
+    }
+
     .revision-toc {
       float: none;
       max-width: 100%;

+ 1 - 0
src/server/routes/apiv3/docs.js

@@ -9,6 +9,7 @@ const router = express.Router();
 // paths to scan
 const APIS = [
   'src/server/routes/apiv3/**/*.js',
+  'src/server/models/**/*.js',
 ];
 
 module.exports = (crowi) => {

+ 20 - 1
src/server/routes/apiv3/page.js

@@ -124,6 +124,15 @@ module.exports = (crowi) => {
       body('pageId').isString(),
       body('bool').isBoolean(),
     ],
+
+    archive: [
+      body('isCommentDownload').isBoolean(),
+      body('isAttachmentFileDownload').isBoolean(),
+      body('isSubordinatedPageDownload').isBoolean(),
+      body('fileType').isString().isIn(['pdf', 'markDown']),
+      body('hierarchyType').isString().isIn(['allSubordinatedPage', 'decideHierarchy']),
+      body('hierarchyValue').isNumeric(),
+    ],
   };
 
   /**
@@ -205,7 +214,17 @@ module.exports = (crowi) => {
    *                schema:
    *                  $ref: '#/components/schemas/Page'
    */
-  router.post('/archive', accessTokenParser, loginRequired, async(req, res) => {
+  router.post('/archive', accessTokenParser, loginRequired, csrf, validator.archive, apiV3FormValidator, async(req, res) => {
+
+    const {
+      isCommentDownload,
+      isAttachmentFileDownload,
+      isSubordinatedPageDownload,
+      fileType,
+      hierarchyType,
+      hierarchyValue,
+    } = req.body;
+
     const PageArchive = crowi.model('PageArchive');
     const filePath = 'path';
     const creator = req.user._id;

+ 2 - 2
src/server/views/layout-kibela/base/layout.html

@@ -9,10 +9,10 @@
 {% block layout_main %}
 <div class="container-fluid p-0">
 
-  <div class="row body m-0 p-0">
+  <div class="row body m-0 p-0 d-print-block">
 
     <div id="main" class="main col-12 kibela-block round-corner {% if page %}{{ css.grant(page) }}{% endif %}{% block main_css_class %}{% endblock %}">
-      <div class="row grw-subnav d-edit-none">
+      <div class="row grw-subnav d-edit-none d-print-block">
         <div class="col-12 col-xl-9 col-lg-8 px-0 mx-0 bg-white kibela-border-top round-corner">
           {% block content_header %} {% endblock %}
         </div>

+ 1 - 1
src/server/views/layout/layout.html

@@ -79,7 +79,7 @@
 
   {% block head_warn_breaking_changes %}{% include '../widget/alert_breaking_changes.html' %}{% endblock %}
 
-  <div id="page-wrapper" class="page-wrapper d-flex">
+  <div id="page-wrapper" class="page-wrapper d-flex d-print-block">
     {# Sidebar #}
     <div id="grw-sidebar-wrapper"></div>
 

+ 1 - 1
src/server/views/widget/system-version.html

@@ -1,4 +1,4 @@
-<div class="system-version d-none d-md-block">
+<div class="system-version d-none d-md-block d-print-none">
   <span>
     <a href="https://growi.org">GROWI</a> {{ growiVersion() }}
   </span>

+ 9 - 4
yarn.lock

@@ -5474,10 +5474,10 @@ eslint-config-airbnb@^17.1.0:
     object.assign "^4.1.0"
     object.entries "^1.0.4"
 
-eslint-config-weseek@^1.0.3:
-  version "1.0.3"
-  resolved "https://registry.yarnpkg.com/eslint-config-weseek/-/eslint-config-weseek-1.0.3.tgz#420f583371447def71af11a78baf39d65a3d9e4a"
-  integrity sha512-AXOuaZomA/h34EHMT+CmhU7TTDbsyqCl702yaY0PGt6wype/YWra9phTbPyHjuI+Uh8gh9eKX2tAhnruKK3Ivw==
+eslint-config-weseek@^1.0.4:
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/eslint-config-weseek/-/eslint-config-weseek-1.0.4.tgz#26f8dc5ce6e791f9eb379308f0fbac4902b25439"
+  integrity sha512-PkWrFlNDs5X+cyCDEZ6JFoHly0LQhDuPVbEiSxchguk9A8OvrUD+KBgIOWbQZSjI0YJV582zyGSTU5J1lwtpUw==
   dependencies:
     eslint-config-airbnb "^17.1.0"
 
@@ -5520,6 +5520,11 @@ eslint-plugin-jest@^23.0.3:
   dependencies:
     "@typescript-eslint/experimental-utils" "^2.5.0"
 
+eslint-plugin-react-hooks@^4.0.4:
+  version "4.0.4"
+  resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.0.4.tgz#aed33b4254a41b045818cacb047b81e6df27fa58"
+  integrity sha512-equAdEIsUETLFNCmmCkiCGq6rkSK5MoJhXFPFYeUebcjKgBmWWcgVOqZyQC8Bv1BwVCnTq9tBxgJFgAJTWoJtA==
+
 eslint-plugin-react@^7.14.2:
   version "7.14.2"
   resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.14.2.tgz#94c193cc77a899ac0ecbb2766fbef88685b7ecc1"