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

Merge pull request #2714 from weseek/feat/bulk-export-pages-for-merge

Feat/bulk export pages for merge
Yuki Takei 5 лет назад
Родитель
Сommit
994bf5f539

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

@@ -50,9 +50,13 @@
   "Not available for guest": "Not available for guest",
   "Not available for guest": "Not available for guest",
   "Create Archive Page": "Create Archive Page",
   "Create Archive Page": "Create Archive Page",
   "File type": "File type",
   "File type": "File type",
+  "Target page": "Target page",
   "Include Attachment File": "Include Attachment File",
   "Include Attachment File": "Include Attachment File",
   "Include Comment": "Include Comment",
   "Include Comment": "Include Comment",
   "Include Subordinated Page": "Include Subordinated Page",
   "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",
   "username": "Username",
   "Created": "Created",
   "Created": "Created",
   "Last updated": "Updated",
   "Last updated": "Updated",
@@ -718,6 +722,7 @@
   },
   },
   "export_bulk": {
   "export_bulk": {
     "failed_to_export": "Failed to export",
     "failed_to_export": "Failed to export",
+    "failed_to_count_pages": "Failed to count pages",
     "export_page_markdown": "Export page as Markdown",
     "export_page_markdown": "Export page as Markdown",
     "export_page_pdf": "Export page as PDF"
     "export_page_pdf": "Export page as PDF"
   },
   },

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

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

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

@@ -50,7 +50,16 @@
 	"History": "历史",
 	"History": "历史",
 	"Presentation Mode": "演示文稿",
 	"Presentation Mode": "演示文稿",
   "Not available for guest": "Not available for guest",
   "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": "创建",
 	"Created": "创建",
 	"Last updated": "上次更新",
 	"Last updated": "上次更新",
   "Last_Login": "上次登录",
   "Last_Login": "上次登录",
@@ -715,6 +724,12 @@
 		"Registration successful": "注册成功",
 		"Registration successful": "注册成功",
 		"Setup": "安装程序"
 		"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": {
 	"message": {
 		"successfully_connected": "连接成功!",
 		"successfully_connected": "连接成功!",
 		"fail_to_save_access_token": "无法保存访问令牌。请再试一次。",
 		"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);

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

@@ -9,13 +9,23 @@ import AppContainer from '../../services/AppContainer';
 import PageContainer from '../../services/PageContainer';
 import PageContainer from '../../services/PageContainer';
 import OutsideShareLinkModal from '../OutsideShareLinkModal';
 import OutsideShareLinkModal from '../OutsideShareLinkModal';
 
 
+// TODO GW-2746 bulk export pages
+// import ArchiveCreateModal from '../ArchiveCreateModal';
+
 const PageShareManagement = (props) => {
 const PageShareManagement = (props) => {
   const { t, appContainer, pageContainer } = 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 { currentUser } = appContainer;
 
 
   const [isOutsideShareLinkModalShown, setIsOutsideShareLinkModalShown] = useState(false);
   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() {
   function openOutsideShareLinkModalHandler() {
     setIsOutsideShareLinkModalShown(true);
     setIsOutsideShareLinkModalShown(true);
@@ -25,6 +35,17 @@ const PageShareManagement = (props) => {
     setIsOutsideShareLinkModalShown(false);
     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) {
   async function exportPageHandler(format) {
     const { pageId, revisionId } = pageContainer.state;
     const { pageId, revisionId } = pageContainer.state;
     const url = new URL(urljoin(window.location.origin, '_api/v3/page/export', pageId));
     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;
     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() {
   function renderModals() {
     if (currentUser == null) {
     if (currentUser == null) {
       return null;
       return null;
@@ -44,6 +77,15 @@ const PageShareManagement = (props) => {
           isOpen={isOutsideShareLinkModalShown}
           isOpen={isOutsideShareLinkModalShown}
           onClose={closeOutsideShareLinkModalHandler}
           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') }}>
         <button type="button" className="dropdown-item" onClick={() => { exportPageHandler('md') }}>
           <span>{t('export_bulk.export_page_markdown')}</span>
           <span>{t('export_bulk.export_page_markdown')}</span>
         </button>
         </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>
       </div>
       {renderModals()}
       {renderModals()}
     </>
     </>

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

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

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

@@ -129,6 +129,15 @@ module.exports = (crowi) => {
       query('format').isString().isIn(['md', 'pdf']),
       query('format').isString().isIn(['md', 'pdf']),
       query('revisionId').isString(),
       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);
     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;
   return router;
 };
 };