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

Merge pull request #5298 from weseek/feat/87943-create-page-from-page-tree

feat: Create page from PageTree
Yuki Takei 4 лет назад
Родитель
Сommit
848e17348b

+ 2 - 0
packages/app/resource/locales/en_US/translation.json

@@ -167,6 +167,8 @@
   "new_path":"New path",
   "duplicated_path":"duplicated_path",
   "Link sharing is disabled": "Link sharing is disabled",
+  "successfully_saved_the_page": "Successfully saved the page",
+  "you_can_not_create_page_with_this_name": "You can not create page with this name",
   "personal_dropdown": {
     "home": "Home",
     "settings": "Settings",

+ 2 - 0
packages/app/resource/locales/ja_JP/translation.json

@@ -169,6 +169,8 @@
   "new_path":"新しいパス",
   "duplicated_path":"重複したパス",
   "Link sharing is disabled": "リンクのシェアは無効化されています",
+  "successfully_saved_the_page": "ページが正常に保存されました",
+  "you_can_not_create_page_with_this_name": "この名前でページを作成することはできません",
   "personal_dropdown": {
     "home": "ホーム",
     "settings": "設定",

+ 2 - 0
packages/app/resource/locales/zh_CN/translation.json

@@ -175,6 +175,8 @@
   "new_path":"New path",
   "duplicated_path":"duplicated_path",
   "Link sharing is disabled": "你不允许分享该链接",
+  "successfully_saved_the_page": "成功地保存了该页面",
+  "you_can_not_create_page_with_this_name": "您无法使用此名称创建页面",
 	"form_validation": {
 		"error_message": "有些值不正确",
 		"required": "%s 是必需的",

+ 1 - 1
packages/app/src/components/Common/ClosableTextInput.tsx

@@ -107,7 +107,7 @@ const ClosableTextInput: FC<ClosableTextInputProps> = memo((props: ClosableTextI
   return (
     <div className={props.isShown ? 'd-block' : 'd-none'}>
       <input
-        value={inputText}
+        value={inputText || ''}
         ref={inputRef}
         type="text"
         className="form-control"

+ 31 - 5
packages/app/src/components/Sidebar/PageTree/Item.tsx

@@ -8,13 +8,13 @@ import { useDrag, useDrop } from 'react-dnd';
 
 import nodePath from 'path';
 
-import { pathUtils } from '@growi/core';
+import { pathUtils, pagePathUtils } from '@growi/core';
 
 import { toastWarning, toastError, toastSuccess } from '~/client/util/apiNotification';
 
 import { useSWRxPageChildren } from '~/stores/page-listing';
+import { apiv3Put, apiv3Post } from '~/client/util/apiv3-client';
 import { IPageForPageDeleteModal } from '~/stores/modal';
-import { apiv3Put } from '~/client/util/apiv3-client';
 
 import TriangleIcon from '~/components/Icons/TriangleIcon';
 import { bookmark, unbookmark } from '~/client/services/page-operation';
@@ -257,12 +257,38 @@ const Item: FC<ItemProps> = (props: ItemProps) => {
     onClickDeleteMenuItem(pageToDelete);
   }, [page, onClickDeleteMenuItem]);
 
-  const onPressEnterForCreateHandler = (inputText: string) => {
+  const onPressEnterForCreateHandler = async(inputText: string) => {
     setNewPageInputShown(false);
     const parentPath = pathUtils.addTrailingSlash(page.path as string);
     const newPagePath = `${parentPath}${inputText}`;
-    console.log(newPagePath);
-    // TODO: https://redmine.weseek.co.jp/issues/87943
+    const isCreatable = pagePathUtils.isCreatablePage(newPagePath);
+
+    if (!isCreatable) {
+      toastWarning(t('you_can_not_create_page_with_this_name'));
+      return;
+    }
+
+    // TODO 88261: Get the isEnabledAttachTitleHeader by SWR
+    // const initBody = '';
+    // const { isEnabledAttachTitleHeader } = props.appContainer.getConfig();
+    // if (isEnabledAttachTitleHeader) {
+    //   initBody = pathUtils.attachTitleHeader(newPagePath);
+    // }
+
+    try {
+      await apiv3Post('/pages/', {
+        path: newPagePath,
+        body: '',
+        grant: page.grant,
+        grantUserGroupId: page.grantedGroup,
+        createFromPageTree: true,
+      });
+      mutateChildren();
+      toastSuccess(t('successfully_saved_the_page'));
+    }
+    catch (err) {
+      toastError(err);
+    }
   };
 
   const inputValidator = (title: string | null): AlertInfo | null => {

+ 3 - 0
packages/app/src/server/models/revision.js

@@ -10,6 +10,9 @@ module.exports = function(crowi) {
   const mongoose = require('mongoose');
   const mongoosePaginate = require('mongoose-paginate-v2');
 
+  // allow empty strings
+  mongoose.Schema.Types.String.checkRequired(v => v != null);
+
   const ObjectId = mongoose.Schema.Types.ObjectId;
   const revisionSchema = new mongoose.Schema({
     // OBSOLETE path: { type: String, required: true, index: true }

+ 6 - 2
packages/app/src/server/routes/apiv3/pages.js

@@ -159,8 +159,8 @@ module.exports = (crowi) => {
 
   const validator = {
     createPage: [
-      body('body').exists().not().isEmpty({ ignore_whitespace: true })
-        .withMessage('body is required'),
+      body('body').exists()
+        .withMessage('body is re quired but an empty string is allowed'),
       body('path').exists().not().isEmpty({ ignore_whitespace: true })
         .withMessage('path is required'),
       body('grant').if(value => value != null).isInt({ min: 0, max: 5 }).withMessage('grant must be integer from 1 to 5'),
@@ -168,6 +168,7 @@ module.exports = (crowi) => {
       body('isSlackEnabled').if(value => value != null).isBoolean().withMessage('isSlackEnabled must be boolean'),
       body('slackChannels').if(value => value != null).isString().withMessage('slackChannels must be string'),
       body('pageTags').if(value => value != null).isArray().withMessage('pageTags must be array'),
+      body('createFromPageTree').optional().isBoolean().withMessage('createFromPageTree must be boolean'),
     ],
     renamePage: [
       body('pageId').isMongoId().withMessage('pageId is required'),
@@ -244,6 +245,9 @@ module.exports = (crowi) => {
    *                    type: array
    *                    items:
    *                      $ref: '#/components/schemas/Tag'
+   *                  createFromPageTree:
+   *                    type: boolean
+   *                    description: Whether the page was created from the page tree or not
    *                required:
    *                  - body
    *                  - path

+ 1 - 1
packages/app/src/server/routes/page.js

@@ -900,7 +900,7 @@ module.exports = function(crowi, app) {
    * - If revision_id is not specified => force update by the new contents.
    */
   api.update = async function(req, res) {
-    const pageBody = req.body.body || null;
+    const pageBody = body ?? null;
     const pageId = req.body.page_id || null;
     const revisionId = req.body.revision_id || null;
     const grant = req.body.grant || null;

+ 4 - 0
packages/app/src/server/util/swigFunctions.js

@@ -171,6 +171,10 @@ module.exports = function(crowi, req, locals) {
     });
   };
 
+  locals.attachTitleHeader = function(path) {
+    return pathUtils.attachTitleHeader(path);
+  };
+
   locals.css = {
     grant(pageData) {
       if (!pageData) {

+ 2 - 2
packages/app/src/server/views/widget/not_found_content.html

@@ -17,9 +17,9 @@
 
     {% if getConfig('crowi', 'customize:isEnabledAttachTitleHeader') %}
     {% if template %}
-    <script type="text/template" id="raw-text-original"># {{ path | path2name | preventXss }}&NewLine;{{ template }}</script>
+    <script type="text/template" id="raw-text-original">{{ attachTitleHeader(path | path2name | preventXss) }}&NewLine;{{ template }}</script>
     {% else %}
-    <script type="text/template" id="raw-text-original"># {{ path | path2name | preventXss }}</script>
+    <script type="text/template" id="raw-text-original">{{ attachTitleHeader(path | path2name | preventXss) }}</script>
     {% endif %}
     {% else %}
     {% if template %}

+ 11 - 0
packages/core/src/utils/path-utils.js

@@ -106,3 +106,14 @@ export function normalizePath(path) {
   }
   return `/${match[3]}`;
 }
+
+
+/**
+ *
+ * @param {string} path
+ * @returns {string}
+ * @memberof pathUtils
+ */
+export function attachTitleHeader(path) {
+  return `# ${path}`;
+}