Browse Source

Merge branch 'master' into feat/rich-attachment

ryoji-s 3 years ago
parent
commit
9039482c15
38 changed files with 338 additions and 220 deletions
  1. 19 1
      CHANGELOG.md
  2. 1 1
      lerna.json
  3. 1 1
      package.json
  4. 1 1
      packages/app/docker/README.md
  5. 12 12
      packages/app/package.json
  6. 1 1
      packages/app/src/components/Admin/ExportArchiveDataPage.tsx
  7. 5 4
      packages/app/src/components/PageComment/CommentEditor.tsx
  8. 1 0
      packages/app/src/components/PageEditor/CodeMirrorEditor.jsx
  9. 0 1
      packages/app/src/components/PageEditor/CodeMirrorEditor.module.scss
  10. 15 12
      packages/app/src/components/PageEditor/CommentMentionHelper.ts
  11. 1 0
      packages/app/src/components/PageEditor/DrawioCommunicationHelper.ts
  12. 3 2
      packages/app/src/components/PageEditor/DrawioModal.tsx
  13. 6 5
      packages/app/src/components/PageSideContents.tsx
  14. 35 37
      packages/app/src/components/Sidebar/PageTree/Item.tsx
  15. 0 3
      packages/app/src/components/Sidebar/PageTree/ItemsTree.tsx
  16. 22 5
      packages/app/src/server/routes/apiv3/pages.js
  17. 1 1
      packages/app/src/server/service/page.ts
  18. 0 20
      packages/app/src/services/renderer/remark-plugins/table.ts
  19. 1 4
      packages/app/src/services/renderer/renderer.tsx
  20. 1 1
      packages/app/src/styles/bootstrap/_override.scss
  21. 117 51
      packages/app/test/cypress/integration/20-basic-features/20-basic-features--access-to-page.spec.ts
  22. 61 0
      packages/app/test/cypress/integration/20-basic-features/20-basic-features--username-mention.spec.ts
  23. 1 1
      packages/codemirror-textlint/package.json
  24. 1 1
      packages/core/package.json
  25. 1 1
      packages/hackmd/package.json
  26. 3 3
      packages/presentation/package.json
  27. 1 1
      packages/preset-themes/package.json
  28. 2 3
      packages/remark-drawio/package.json
  29. 3 1
      packages/remark-drawio/src/components/DrawioViewer.tsx
  30. 1 9
      packages/remark-drawio/src/services/renderer/remark-drawio.ts
  31. 4 4
      packages/remark-drawio/src/utils/embed.ts
  32. 1 1
      packages/remark-growi-directive/package.json
  33. 4 4
      packages/remark-lsx/package.json
  34. 2 11
      packages/remark-lsx/src/services/renderer/lsx.ts
  35. 1 1
      packages/slack/package.json
  36. 2 2
      packages/slackbot-proxy/package.json
  37. 2 2
      packages/ui/package.json
  38. 5 12
      yarn.lock

+ 19 - 1
CHANGELOG.md

@@ -1,9 +1,27 @@
 # Changelog
 # Changelog
 
 
-## [Unreleased](https://github.com/weseek/growi/compare/v6.0.13...HEAD)
+## [Unreleased](https://github.com/weseek/growi/compare/v6.0.15...HEAD)
 
 
 *Please do not manually update this file. We've automated the process.*
 *Please do not manually update this file. We've automated the process.*
 
 
+## [v6.0.15](https://github.com/weseek/growi/compare/v6.0.14...v6.0.15) - 2023-04-10
+
+### 🐛 Bug Fixes
+
+- fix: Templates are not applied when pages are created from PageTree (#7553) @miya
+- fix(drawio): Render uncompressed data when line-breaks option is set (#7555) @yuki-takei
+- fix: Is not working i18n on `/admin/export`  (#7554) @jam411
+- fix: PageTree mutiple DnD unexpected disappear and unexpected tree move if renamed path (#7542) @jam411
+- fix: Username incremental search is not working in comment (#7548) @mudana-grune
+- fix: H6 line-height (#7541) @yukendev
+
+## [v6.0.14](https://github.com/weseek/growi/compare/v6.0.13...v6.0.14) - 2023-04-04
+
+### 🐛 Bug Fixes
+
+- fix(drawio): Set compressXml option (#7536) @yuki-takei
+- fix(drawio): Rendering uncompressed data (#7537) @yuki-takei
+
 ## [v6.0.13](https://github.com/weseek/growi/compare/v6.0.12...v6.0.13) - 2023-04-03
 ## [v6.0.13](https://github.com/weseek/growi/compare/v6.0.12...v6.0.13) - 2023-04-03
 
 
 ### 🐛 Bug Fixes
 ### 🐛 Bug Fixes

+ 1 - 1
lerna.json

@@ -1,7 +1,7 @@
 {
 {
   "npmClient": "yarn",
   "npmClient": "yarn",
   "useWorkspaces": true,
   "useWorkspaces": true,
-  "version": "6.0.14-RC.0",
+  "version": "6.0.16-RC.0",
   "packages": [
   "packages": [
     "packages/*"
     "packages/*"
   ]
   ]

+ 1 - 1
package.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "growi",
   "name": "growi",
-  "version": "6.0.14-RC.0",
+  "version": "6.0.16-RC.0",
   "description": "Team collaboration software using markdown",
   "description": "Team collaboration software using markdown",
   "tags": [
   "tags": [
     "wiki",
     "wiki",

+ 1 - 1
packages/app/docker/README.md

@@ -10,7 +10,7 @@ GROWI Official docker image
 Supported tags and respective Dockerfile links
 Supported tags and respective Dockerfile links
 ------------------------------------------------
 ------------------------------------------------
 
 
-* [`6.0.13`, `6.0`, `6`, `latest` (Dockerfile)](https://github.com/weseek/growi/blob/v6.0.13/packages/app/docker/Dockerfile)
+* [`6.0.15`, `6.0`, `6`, `latest` (Dockerfile)](https://github.com/weseek/growi/blob/v6.0.15/packages/app/docker/Dockerfile)
 * [`5.1.7`, `5.1`, `5`](https://github.com/weseek/growi/blob/v5.1.7/packages/app/docker/Dockerfile)
 * [`5.1.7`, `5.1`, `5`](https://github.com/weseek/growi/blob/v5.1.7/packages/app/docker/Dockerfile)
 * [`5.1.7-nocdn`, `5.1-nocdn`, `5-nocdn`](https://github.com/weseek/growi/blob/v5.1.7/packages/app/docker/Dockerfile)
 * [`5.1.7-nocdn`, `5.1-nocdn`, `5-nocdn`](https://github.com/weseek/growi/blob/v5.1.7/packages/app/docker/Dockerfile)
 * [`4.5.23`, `4.5`, `4`, `latest` (Dockerfile)](https://github.com/weseek/growi/blob/v4.5.23/packages/app/docker/Dockerfile)
 * [`4.5.23`, `4.5`, `4`, `latest` (Dockerfile)](https://github.com/weseek/growi/blob/v4.5.23/packages/app/docker/Dockerfile)

+ 12 - 12
packages/app/package.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "@growi/app",
   "name": "@growi/app",
-  "version": "6.0.14-RC.0",
+  "version": "6.0.16-RC.0",
   "license": "MIT",
   "license": "MIT",
   "scripts": {
   "scripts": {
     "//// for production": "",
     "//// for production": "",
@@ -66,14 +66,14 @@
     "@elastic/elasticsearch7": "npm:@elastic/elasticsearch@^7.17.0",
     "@elastic/elasticsearch7": "npm:@elastic/elasticsearch@^7.17.0",
     "@godaddy/terminus": "^4.9.0",
     "@godaddy/terminus": "^4.9.0",
     "@google-cloud/storage": "^5.8.5",
     "@google-cloud/storage": "^5.8.5",
-    "@growi/codemirror-textlint": "^6.0.14-RC.0",
-    "@growi/core": "^6.0.14-RC.0",
-    "@growi/hackmd": "^6.0.14-RC.0",
-    "@growi/preset-themes": "^6.0.14-RC.0",
-    "@growi/remark-drawio": "^6.0.14-RC.0",
-    "@growi/remark-growi-directive": "^6.0.14-RC.0",
-    "@growi/remark-lsx": "^6.0.14-RC.0",
-    "@growi/slack": "^6.0.14-RC.0",
+    "@growi/codemirror-textlint": "^6.0.16-RC.0",
+    "@growi/core": "^6.0.16-RC.0",
+    "@growi/hackmd": "^6.0.16-RC.0",
+    "@growi/preset-themes": "^6.0.16-RC.0",
+    "@growi/remark-drawio": "^6.0.16-RC.0",
+    "@growi/remark-growi-directive": "^6.0.16-RC.0",
+    "@growi/remark-lsx": "^6.0.16-RC.0",
+    "@growi/slack": "^6.0.16-RC.0",
     "@promster/express": "^7.0.6",
     "@promster/express": "^7.0.6",
     "@promster/server": "^7.0.8",
     "@promster/server": "^7.0.8",
     "@slack/web-api": "^6.2.4",
     "@slack/web-api": "^6.2.4",
@@ -163,7 +163,7 @@
     "react-error-boundary": "^3.1.4",
     "react-error-boundary": "^3.1.4",
     "react-i18next": "^12.2.0",
     "react-i18next": "^12.2.0",
     "react-image-crop": "^8.3.0",
     "react-image-crop": "^8.3.0",
-    "react-markdown": "^8.0.3",
+    "react-markdown": "^8.0.7",
     "react-multiline-clamp": "^2.0.0",
     "react-multiline-clamp": "^2.0.0",
     "react-scroll": "^1.8.7",
     "react-scroll": "^1.8.7",
     "react-syntax-highlighter": "^15.5.0",
     "react-syntax-highlighter": "^15.5.0",
@@ -206,8 +206,8 @@
     "handsontable": "v7.0.0 or above is no loger MIT lisence."
     "handsontable": "v7.0.0 or above is no loger MIT lisence."
   },
   },
   "devDependencies": {
   "devDependencies": {
-    "@growi/presentation": "^6.0.14-RC.0",
-    "@growi/ui": "^6.0.14-RC.0",
+    "@growi/presentation": "^6.0.16-RC.0",
+    "@growi/ui": "^6.0.16-RC.0",
     "@handsontable/react": "=2.1.0",
     "@handsontable/react": "=2.1.0",
     "@icon/themify-icons": "1.0.1-alpha.3",
     "@icon/themify-icons": "1.0.1-alpha.3",
     "@next/bundle-analyzer": "^13.2.3",
     "@next/bundle-analyzer": "^13.2.3",

+ 1 - 1
packages/app/src/components/Admin/ExportArchiveDataPage.tsx

@@ -19,7 +19,7 @@ const IGNORED_COLLECTION_NAMES = [
 
 
 const ExportArchiveDataPage = (): JSX.Element => {
 const ExportArchiveDataPage = (): JSX.Element => {
   const { data: socket } = useAdminSocket();
   const { data: socket } = useAdminSocket();
-  const { t } = useTranslation();
+  const { t } = useTranslation('admin');
 
 
   const [collections, setCollections] = useState<any[]>([]);
   const [collections, setCollections] = useState<any[]>([]);
   const [zipFileStats, setZipFileStats] = useState<any[]>([]);
   const [zipFileStats, setZipFileStats] = useState<any[]>([]);

+ 5 - 4
packages/app/src/components/PageComment/CommentEditor.tsx

@@ -230,6 +230,7 @@ export const CommentEditor = (props: CommentEditorProps): JSX.Element => {
             type="button"
             type="button"
             className="btn btn-lg btn-link"
             className="btn btn-lg btn-link"
             onClick={() => setIsReadyToUse(true)}
             onClick={() => setIsReadyToUse(true)}
+            data-testid="open-comment-editor-button"
           >
           >
             <i className="icon-bubble"></i> Add Comment
             <i className="icon-bubble"></i> Add Comment
           </button>
           </button>
@@ -300,9 +301,9 @@ export const CommentEditor = (props: CommentEditorProps): JSX.Element => {
         <div className="comment-submit">
         <div className="comment-submit">
           <div className="d-flex">
           <div className="d-flex">
             <span className="flex-grow-1" />
             <span className="flex-grow-1" />
-            <span className="d-none d-sm-inline">{ errorMessage && errorMessage }</span>
+            <span className="d-none d-sm-inline">{errorMessage && errorMessage}</span>
 
 
-            { isSlackConfigured && isSlackEnabled != null
+            {isSlackConfigured && isSlackEnabled != null
               && (
               && (
                 <div className="form-inline align-self-center mr-md-2">
                 <div className="form-inline align-self-center mr-md-2">
                   <SlackNotification
                   <SlackNotification
@@ -321,7 +322,7 @@ export const CommentEditor = (props: CommentEditorProps): JSX.Element => {
           </div>
           </div>
           <div className="d-block d-sm-none mt-2">
           <div className="d-block d-sm-none mt-2">
             <div className="d-flex justify-content-end">
             <div className="d-flex justify-content-end">
-              { error && errorMessage }
+              {error && errorMessage}
               <span className="mr-2">{cancelButton}</span><span>{submitButton}</span>
               <span className="mr-2">{cancelButton}</span><span>{submitButton}</span>
             </div>
             </div>
           </div>
           </div>
@@ -337,7 +338,7 @@ export const CommentEditor = (props: CommentEditorProps): JSX.Element => {
           <UserPicture user={currentUser} noLink noTooltip />
           <UserPicture user={currentUser} noLink noTooltip />
         </div>
         </div>
         <div className="comment-form-main">
         <div className="comment-form-main">
-          { isReadyToUse
+          {isReadyToUse
             ? renderReady()
             ? renderReady()
             : renderBeforeReady()
             : renderBeforeReady()
           }
           }

+ 1 - 0
packages/app/src/components/PageEditor/CodeMirrorEditor.jsx

@@ -40,6 +40,7 @@ import styles from './CodeMirrorEditor.module.scss';
 window.JSHINT = JSHINT;
 window.JSHINT = JSHINT;
 window.kuromojin = { dicPath: '/static/dict' };
 window.kuromojin = { dicPath: '/static/dict' };
 
 
+require('codemirror/addon/hint/show-hint.css'); // Import from CodeMirrorEditor.module.scss not working
 require('codemirror/addon/display/placeholder');
 require('codemirror/addon/display/placeholder');
 require('codemirror/addon/edit/matchbrackets');
 require('codemirror/addon/edit/matchbrackets');
 require('codemirror/addon/edit/matchtags');
 require('codemirror/addon/edit/matchtags');

+ 0 - 1
packages/app/src/components/PageEditor/CodeMirrorEditor.module.scss

@@ -4,7 +4,6 @@
   @import '~codemirror/lib/codemirror';
   @import '~codemirror/lib/codemirror';
 
 
   // addons
   // addons
-  @import '~codemirror/addon/hint/show-hint';
   @import '~codemirror/addon/fold/foldgutter';
   @import '~codemirror/addon/fold/foldgutter';
   @import '~codemirror/addon/lint/lint';
   @import '~codemirror/addon/lint/lint';
 
 

+ 15 - 12
packages/app/src/components/PageEditor/CommentMentionHelper.ts

@@ -1,20 +1,22 @@
-import i18n from 'i18next';
+import { Editor } from 'codemirror';
+import { i18n } from 'next-i18next';
 import { debounce } from 'throttle-debounce';
 import { debounce } from 'throttle-debounce';
 
 
 import { apiv3Get } from '~/client/util/apiv3-client';
 import { apiv3Get } from '~/client/util/apiv3-client';
 
 
+type UsersListForHints = {
+  text: string
+  displayText: string
+}
 export default class CommentMentionHelper {
 export default class CommentMentionHelper {
 
 
-  editor;
-
-  pattern: RegExp;
-
+  editor: Editor;
 
 
-  constructor(editor) {
+  constructor(editor: Editor) {
     this.editor = editor;
     this.editor = editor;
   }
   }
 
 
-  getUsernamHint = () => {
+  getUsenameHint = (): void => {
     // Get word that contains `@` character at the begining
     // Get word that contains `@` character at the begining
     const currentPos = this.editor.getCursor();
     const currentPos = this.editor.getCursor();
     const wordStart = this.editor.findWordAt(currentPos).anchor.ch - 1;
     const wordStart = this.editor.findWordAt(currentPos).anchor.ch - 1;
@@ -32,14 +34,15 @@ export default class CommentMentionHelper {
     }
     }
 
 
     // Get username after `@` character and search username
     // Get username after `@` character and search username
-    const mention = searchMention.substr(1);
+    const mention = searchMention.slice(1);
     this.editor.showHint({
     this.editor.showHint({
       completeSingle: false,
       completeSingle: false,
       hint: async() => {
       hint: async() => {
         if (mention.length > 0) {
         if (mention.length > 0) {
           const users = await this.getUsersList(mention);
           const users = await this.getUsersList(mention);
           return {
           return {
-            list: users.length > 0 ? users : [{ text: '', displayText: i18n.t('page_comment.no_user_found') }],
+            // Returns default value if i18n is null because it cannot do early return.
+            list: users.length > 0 ? users : [{ text: '', displayText: i18n != null ? i18n.t('page_comment.no_user_found') : 'No user found' }],
             from: searchFrom,
             from: searchFrom,
             to: searchTo,
             to: searchTo,
           };
           };
@@ -48,15 +51,15 @@ export default class CommentMentionHelper {
     });
     });
   };
   };
 
 
-  getUsersList = async(q: string) => {
+  getUsersList = async(q: string): Promise<UsersListForHints[]> => {
     const limit = 20;
     const limit = 20;
     const { data } = await apiv3Get('/users/usernames', { q, limit });
     const { data } = await apiv3Get('/users/usernames', { q, limit });
-    return data.activeUser.usernames.map(username => ({
+    return data.activeUser.usernames.map((username: string) => ({
       text: `@${username} `,
       text: `@${username} `,
       displayText: username,
       displayText: username,
     }));
     }));
   };
   };
 
 
-  showUsernameHint = debounce(800, () => this.getUsernamHint());
+  showUsernameHint = debounce(800, () => this.getUsenameHint());
 
 
 }
 }

+ 1 - 0
packages/app/src/components/PageEditor/DrawioCommunicationHelper.ts

@@ -6,6 +6,7 @@ const logger = loggerFactory('growi:cli:DrawioCommunicationHelper');
 export type DrawioConfig = {
 export type DrawioConfig = {
   css: string,
   css: string,
   customFonts: string[],
   customFonts: string[],
+  compressXml: boolean,
 }
 }
 
 
 export type DrawioCommunicationCallbackOptions = {
 export type DrawioCommunicationCallbackOptions = {

+ 3 - 2
packages/app/src/components/PageEditor/DrawioModal.tsx

@@ -15,14 +15,14 @@ import { useDrawioModal } from '~/stores/modal';
 import { usePersonalSettings } from '~/stores/personal-settings';
 import { usePersonalSettings } from '~/stores/personal-settings';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
-import { DrawioCommunicationHelper } from './DrawioCommunicationHelper';
+import { type DrawioConfig, DrawioCommunicationHelper } from './DrawioCommunicationHelper';
 
 
 const logger = loggerFactory('growi:components:DrawioModal');
 const logger = loggerFactory('growi:components:DrawioModal');
 
 
 const headerColor = '#334455';
 const headerColor = '#334455';
 const fontFamily = "Lato, -apple-system, BlinkMacSystemFont, 'Hiragino Kaku Gothic ProN', Meiryo, sans-serif";
 const fontFamily = "Lato, -apple-system, BlinkMacSystemFont, 'Hiragino Kaku Gothic ProN', Meiryo, sans-serif";
 
 
-const drawioConfig = {
+const drawioConfig: DrawioConfig = {
   css: `
   css: `
   .geMenubarContainer { background-color: ${headerColor} !important; }
   .geMenubarContainer { background-color: ${headerColor} !important; }
   .geMenubar { background-color: ${headerColor} !important; }
   .geMenubar { background-color: ${headerColor} !important; }
@@ -33,6 +33,7 @@ const drawioConfig = {
   }
   }
   `,
   `,
   customFonts: ['Lato', 'Charter'],
   customFonts: ['Lato', 'Charter'],
+  compressXml: true,
 };
 };
 
 
 
 

+ 6 - 5
packages/app/src/components/PageSideContents.tsx

@@ -37,7 +37,7 @@ export const PageSideContents = (props: PageSideContentsProps): JSX.Element => {
     <>
     <>
       {/* Page list */}
       {/* Page list */}
       <div className={`grw-page-accessories-control ${styles['grw-page-accessories-control']}`}>
       <div className={`grw-page-accessories-control ${styles['grw-page-accessories-control']}`}>
-        { !isSharedUser && (
+        {!isSharedUser && (
           <button
           <button
             type="button"
             type="button"
             className="btn btn-block btn-outline-secondary grw-btn-page-accessories rounded-pill d-flex justify-content-between align-items-center"
             className="btn btn-block btn-outline-secondary grw-btn-page-accessories rounded-pill d-flex justify-content-between align-items-center"
@@ -50,16 +50,17 @@ export const PageSideContents = (props: PageSideContentsProps): JSX.Element => {
             {t('page_list')}
             {t('page_list')}
             <CountBadge count={page?.descendantCount} offset={1} />
             <CountBadge count={page?.descendantCount} offset={1} />
           </button>
           </button>
-        ) }
+        )}
       </div>
       </div>
 
 
       {/* Comments */}
       {/* Comments */}
-      { !isTopPagePath && (
+      {!isTopPagePath && (
         <div className={`mt-2 grw-page-accessories-control ${styles['grw-page-accessories-control']}`}>
         <div className={`mt-2 grw-page-accessories-control ${styles['grw-page-accessories-control']}`}>
           <Link to={'page-comments'} offset={-120}>
           <Link to={'page-comments'} offset={-120}>
             <button
             <button
               type="button"
               type="button"
               className="btn btn-block btn-outline-secondary grw-btn-page-accessories rounded-pill d-flex justify-content-between align-items-center"
               className="btn btn-block btn-outline-secondary grw-btn-page-accessories rounded-pill d-flex justify-content-between align-items-center"
+              data-testid="page-comment-button"
             >
             >
               <i className="icon-fw icon-bubbles grw-page-accessories-control-icon"></i>
               <i className="icon-fw icon-bubbles grw-page-accessories-control-icon"></i>
               <span>Comments</span>
               <span>Comments</span>
@@ -67,11 +68,11 @@ export const PageSideContents = (props: PageSideContentsProps): JSX.Element => {
             </button>
             </button>
           </Link>
           </Link>
         </div>
         </div>
-      ) }
+      )}
 
 
       <div className="d-none d-lg-block">
       <div className="d-none d-lg-block">
         <TableOfContents />
         <TableOfContents />
-        { isUsersHomePagePath && <ContentLinkButtons author={page?.creator} /> }
+        {isUsersHomePagePath && <ContentLinkButtons author={page?.creator} />}
       </div>
       </div>
     </>
     </>
   );
   );

+ 35 - 37
packages/app/src/components/Sidebar/PageTree/Item.tsx

@@ -4,7 +4,9 @@ import React, {
 
 
 import nodePath from 'path';
 import nodePath from 'path';
 
 
-import { pathUtils, pagePathUtils, Nullable } from '@growi/core';
+import {
+  pathUtils, pagePathUtils, Nullable, DevidedPagePath,
+} from '@growi/core';
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 import Link from 'next/link';
 import Link from 'next/link';
 import { useDrag, useDrop } from 'react-dnd';
 import { useDrag, useDrop } from 'react-dnd';
@@ -19,7 +21,7 @@ import {
   IPageHasId, IPageInfoAll, IPageToDeleteWithMeta,
   IPageHasId, IPageInfoAll, IPageToDeleteWithMeta,
 } from '~/interfaces/page';
 } from '~/interfaces/page';
 import { IPageForPageDuplicateModal } from '~/stores/modal';
 import { IPageForPageDuplicateModal } from '~/stores/modal';
-import { useSWRxPageChildren } from '~/stores/page-listing';
+import { mutatePageTree, useSWRxPageChildren } from '~/stores/page-listing';
 import { usePageTreeDescCountMap } from '~/stores/ui';
 import { usePageTreeDescCountMap } from '~/stores/ui';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 import { shouldRecoverPagePaths } from '~/utils/page-operation';
 import { shouldRecoverPagePaths } from '~/utils/page-operation';
@@ -39,7 +41,6 @@ interface ItemProps {
   itemNode: ItemNode
   itemNode: ItemNode
   targetPathOrId?: Nullable<string>
   targetPathOrId?: Nullable<string>
   isOpen?: boolean
   isOpen?: boolean
-  isEnabledAttachTitleHeader?: boolean
   onRenamed?(fromPath: string | undefined, toPath: string): void
   onRenamed?(fromPath: string | undefined, toPath: string): void
   onClickDuplicateMenuItem?(pageToDuplicate: IPageForPageDuplicateModal): void
   onClickDuplicateMenuItem?(pageToDuplicate: IPageForPageDuplicateModal): void
   onClickDeleteMenuItem?(pageToDelete: IPageToDeleteWithMeta): void
   onClickDeleteMenuItem?(pageToDelete: IPageToDeleteWithMeta): void
@@ -111,7 +112,7 @@ const NotDraggableForClosableTextInput = (props: NotDraggableProps): JSX.Element
 const Item: FC<ItemProps> = (props: ItemProps) => {
 const Item: FC<ItemProps> = (props: ItemProps) => {
   const { t } = useTranslation();
   const { t } = useTranslation();
   const {
   const {
-    itemNode, targetPathOrId, isOpen: _isOpen = false, isEnabledAttachTitleHeader,
+    itemNode, targetPathOrId, isOpen: _isOpen = false,
     onRenamed, onClickDuplicateMenuItem, onClickDeleteMenuItem, isEnableActions,
     onRenamed, onClickDuplicateMenuItem, onClickDeleteMenuItem, isEnableActions,
   } = props;
   } = props;
 
 
@@ -192,6 +193,7 @@ const Item: FC<ItemProps> = (props: ItemProps) => {
         updateMetadata: true,
         updateMetadata: true,
       });
       });
 
 
+      await mutatePageTree();
       await mutateChildren();
       await mutateChildren();
 
 
       if (onRenamed != null) {
       if (onRenamed != null) {
@@ -214,27 +216,31 @@ const Item: FC<ItemProps> = (props: ItemProps) => {
     }
     }
   };
   };
 
 
-  const [{ isOver }, drop] = useDrop<ItemNode, Promise<void>, { isOver: boolean }>(() => ({
-    accept: 'PAGE_TREE',
-    drop: pageItemDropHandler,
-    hover: (item, monitor) => {
-      // when a drag item is overlapped more than 1 sec, the drop target item will be opened.
-      if (monitor.isOver()) {
-        setTimeout(() => {
-          if (monitor.isOver()) {
-            setIsOpen(true);
-          }
-        }, 600);
-      }
-    },
-    canDrop: (item) => {
-      const { page: droppedPage } = item;
-      return isDroppable(droppedPage, page);
-    },
-    collect: monitor => ({
-      isOver: monitor.isOver(),
+  const [{ isOver }, drop] = useDrop<ItemNode, Promise<void>, { isOver: boolean }>(
+    () => ({
+      accept: 'PAGE_TREE',
+      drop: pageItemDropHandler,
+      hover: (item, monitor) => {
+        // when a drag item is overlapped more than 1 sec, the drop target item will be opened.
+        if (monitor.isOver()) {
+          setTimeout(() => {
+            if (monitor.isOver()) {
+              setIsOpen(true);
+            }
+          }, 600);
+        }
+      },
+      canDrop: (item) => {
+        const { page: droppedPage } = item;
+        return isDroppable(droppedPage, page);
+      },
+      collect: monitor => ({
+        isOver: monitor.isOver(),
+      }),
     }),
     }),
-  }));
+    [page],
+  );
+
 
 
   const hasChildren = useCallback((): boolean => {
   const hasChildren = useCallback((): boolean => {
     return currentChildren != null && currentChildren.length > 0;
     return currentChildren != null && currentChildren.length > 0;
@@ -333,21 +339,14 @@ const Item: FC<ItemProps> = (props: ItemProps) => {
       return;
       return;
     }
     }
 
 
-    let initBody = '';
-    if (isEnabledAttachTitleHeader) {
-      const pageTitle = nodePath.basename(newPagePath);
-      initBody = pathUtils.attachTitleHeader(pageTitle);
-    }
-
     try {
     try {
       setCreating(true);
       setCreating(true);
 
 
       await apiv3Post('/pages/', {
       await apiv3Post('/pages/', {
         path: newPagePath,
         path: newPagePath,
-        body: initBody,
+        body: undefined,
         grant: page.grant,
         grant: page.grant,
         grantUserGroupId: page.grantedGroup,
         grantUserGroupId: page.grantedGroup,
-        createFromPageTree: true,
       });
       });
 
 
       mutateChildren();
       mutateChildren();
@@ -447,7 +446,7 @@ const Item: FC<ItemProps> = (props: ItemProps) => {
             </button>
             </button>
           )}
           )}
         </div>
         </div>
-        { isRenameInputShown
+        {isRenameInputShown
           ? (
           ? (
             <div className="flex-fill">
             <div className="flex-fill">
               <NotDraggableForClosableTextInput>
               <NotDraggableForClosableTextInput>
@@ -463,7 +462,7 @@ const Item: FC<ItemProps> = (props: ItemProps) => {
           )
           )
           : (
           : (
             <>
             <>
-              { shouldShowAttentionIcon && (
+              {shouldShowAttentionIcon && (
                 <>
                 <>
                   <i id="path-recovery" className="fa fa-warning mr-2 text-warning"></i>
                   <i id="path-recovery" className="fa fa-warning mr-2 text-warning"></i>
                   <UncontrolledTooltip placement="top" target="path-recovery" fade={false}>
                   <UncontrolledTooltip placement="top" target="path-recovery" fade={false}>
@@ -471,7 +470,7 @@ const Item: FC<ItemProps> = (props: ItemProps) => {
                   </UncontrolledTooltip>
                   </UncontrolledTooltip>
                 </>
                 </>
               )}
               )}
-              { page != null && page.path != null && page._id != null && (
+              {page != null && page.path != null && page._id != null && (
                 <Link
                 <Link
                   href={pathUtils.returnPathForURL(page.path, page._id)}
                   href={pathUtils.returnPathForURL(page.path, page._id)}
                   className="grw-pagetree-title-anchor flex-grow-1"
                   className="grw-pagetree-title-anchor flex-grow-1"
@@ -543,12 +542,11 @@ const Item: FC<ItemProps> = (props: ItemProps) => {
               itemNode={node}
               itemNode={node}
               isOpen={false}
               isOpen={false}
               targetPathOrId={targetPathOrId}
               targetPathOrId={targetPathOrId}
-              isEnabledAttachTitleHeader={isEnabledAttachTitleHeader}
               onRenamed={onRenamed}
               onRenamed={onRenamed}
               onClickDuplicateMenuItem={onClickDuplicateMenuItem}
               onClickDuplicateMenuItem={onClickDuplicateMenuItem}
               onClickDeleteMenuItem={onClickDeleteMenuItem}
               onClickDeleteMenuItem={onClickDeleteMenuItem}
             />
             />
-            { isCreating && (currentChildren.length - 1 === index) && (
+            {isCreating && (currentChildren.length - 1 === index) && (
               <div className="text-muted text-center">
               <div className="text-muted text-center">
                 <i className="fa fa-spinner fa-pulse mr-1"></i>
                 <i className="fa fa-spinner fa-pulse mr-1"></i>
               </div>
               </div>

+ 0 - 3
packages/app/src/components/Sidebar/PageTree/ItemsTree.tsx

@@ -14,7 +14,6 @@ import { IPageHasId, IPageToDeleteWithMeta } from '~/interfaces/page';
 import { AncestorsChildrenResult, RootPageResult, TargetAndAncestors } from '~/interfaces/page-listing-results';
 import { AncestorsChildrenResult, RootPageResult, TargetAndAncestors } from '~/interfaces/page-listing-results';
 import { OnDuplicatedFunction, OnDeletedFunction } from '~/interfaces/ui';
 import { OnDuplicatedFunction, OnDeletedFunction } from '~/interfaces/ui';
 import { SocketEventName, UpdateDescCountData, UpdateDescCountRawData } from '~/interfaces/websocket';
 import { SocketEventName, UpdateDescCountData, UpdateDescCountRawData } from '~/interfaces/websocket';
-import { useIsEnabledAttachTitleHeader } from '~/stores/context';
 import {
 import {
   IPageForPageDuplicateModal, usePageDuplicateModal, usePageDeleteModal,
   IPageForPageDuplicateModal, usePageDuplicateModal, usePageDeleteModal,
 } from '~/stores/modal';
 } from '~/stores/modal';
@@ -110,7 +109,6 @@ const ItemsTree = (props: ItemsTreeProps): JSX.Element => {
   const { data: ancestorsChildrenResult, error: error1 } = useSWRxPageAncestorsChildren(targetPath);
   const { data: ancestorsChildrenResult, error: error1 } = useSWRxPageAncestorsChildren(targetPath);
   const { data: rootPageResult, error: error2 } = useSWRxRootPage();
   const { data: rootPageResult, error: error2 } = useSWRxRootPage();
   const { data: currentPagePath } = useCurrentPagePath();
   const { data: currentPagePath } = useCurrentPagePath();
-  const { data: isEnabledAttachTitleHeader } = useIsEnabledAttachTitleHeader();
   const { open: openDuplicateModal } = usePageDuplicateModal();
   const { open: openDuplicateModal } = usePageDuplicateModal();
   const { open: openDeleteModal } = usePageDeleteModal();
   const { open: openDeleteModal } = usePageDeleteModal();
   const { data: sidebarScrollerRef } = useSidebarScrollerRef();
   const { data: sidebarScrollerRef } = useSidebarScrollerRef();
@@ -279,7 +277,6 @@ const ItemsTree = (props: ItemsTreeProps): JSX.Element => {
           targetPathOrId={targetPathOrId}
           targetPathOrId={targetPathOrId}
           itemNode={initialItemNode}
           itemNode={initialItemNode}
           isOpen
           isOpen
-          isEnabledAttachTitleHeader={isEnabledAttachTitleHeader}
           isEnableActions={isEnableActions}
           isEnableActions={isEnableActions}
           onRenamed={onRenamed}
           onRenamed={onRenamed}
           onClickDuplicateMenuItem={onClickDuplicateMenuItem}
           onClickDuplicateMenuItem={onClickDuplicateMenuItem}

+ 22 - 5
packages/app/src/server/routes/apiv3/pages.js

@@ -165,8 +165,8 @@ module.exports = (crowi) => {
 
 
   const validator = {
   const validator = {
     createPage: [
     createPage: [
-      body('body').exists()
-        .withMessage('body is re quired but an empty string is allowed'),
+      body('body').optional().isString()
+        .withMessage('body must be string or undefined'),
       body('path').exists().not().isEmpty({ ignore_whitespace: true })
       body('path').exists().not().isEmpty({ ignore_whitespace: true })
         .withMessage('path is required'),
         .withMessage('path is required'),
       body('grant').if(value => value != null).isInt({ min: 0, max: 5 }).withMessage('grant must be integer from 1 to 5'),
       body('grant').if(value => value != null).isInt({ min: 0, max: 5 }).withMessage('grant must be integer from 1 to 5'),
@@ -174,7 +174,6 @@ module.exports = (crowi) => {
       body('isSlackEnabled').if(value => value != null).isBoolean().withMessage('isSlackEnabled must be boolean'),
       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('slackChannels').if(value => value != null).isString().withMessage('slackChannels must be string'),
       body('pageTags').if(value => value != null).isArray().withMessage('pageTags must be array'),
       body('pageTags').if(value => value != null).isArray().withMessage('pageTags must be array'),
-      body('createFromPageTree').optional().isBoolean().withMessage('createFromPageTree must be boolean'),
     ],
     ],
     renamePage: [
     renamePage: [
       body('pageId').isMongoId().withMessage('pageId is required'),
       body('pageId').isMongoId().withMessage('pageId is required'),
@@ -309,10 +308,28 @@ module.exports = (crowi) => {
       options.grantUserGroupId = grantUserGroupId;
       options.grantUserGroupId = grantUserGroupId;
     }
     }
 
 
+    const isNoBodyPage = body === undefined;
+    let initialTags = [];
+    let initialBody = '';
+    if (isNoBodyPage) {
+      const isEnabledAttachTitleHeader = await crowi.configManager.getConfig('crowi', 'customize:isEnabledAttachTitleHeader');
+      if (isEnabledAttachTitleHeader) {
+        initialBody += `${pathUtils.attachTitleHeader(path)}\n`;
+      }
+
+      const templateData = await Page.findTemplate(path);
+      if (templateData?.templateTags != null) {
+        initialTags = templateData.templateTags;
+      }
+      if (templateData?.templateBody != null) {
+        initialBody += `${templateData.templateBody}\n`;
+      }
+    }
+
     let createdPage;
     let createdPage;
     try {
     try {
       createdPage = await createPageAction({
       createdPage = await createPageAction({
-        path, body, user: req.user, options,
+        path, body: isNoBodyPage ? initialBody : body, user: req.user, options,
       });
       });
     }
     }
     catch (err) {
     catch (err) {
@@ -320,7 +337,7 @@ module.exports = (crowi) => {
       return res.apiv3Err(err);
       return res.apiv3Err(err);
     }
     }
 
 
-    const savedTags = await saveTagsAction({ createdPage, pageTags });
+    const savedTags = await saveTagsAction({ createdPage, pageTags: isNoBodyPage ? initialTags : pageTags });
 
 
     const result = {
     const result = {
       page: serializePageSecurely(createdPage),
       page: serializePageSecurely(createdPage),

+ 1 - 1
packages/app/src/server/service/page.ts

@@ -563,7 +563,7 @@ class PageService {
 
 
     // Remove leaf empty pages if not moving to under the ex-target position
     // Remove leaf empty pages if not moving to under the ex-target position
     if (!this.isRenamingToUnderTarget(page.path, newPagePath)) {
     if (!this.isRenamingToUnderTarget(page.path, newPagePath)) {
-    // remove empty pages at leaf position
+      // remove empty pages at leaf position
       await Page.removeLeafEmptyPagesRecursively(page.parent);
       await Page.removeLeafEmptyPagesRecursively(page.parent);
     }
     }
 
 

+ 0 - 20
packages/app/src/services/renderer/remark-plugins/table.ts

@@ -1,20 +0,0 @@
-import { Plugin } from 'unified';
-import { visit } from 'unist-util-visit';
-
-export const remarkPlugin: Plugin = function() {
-  return (tree) => {
-    visit(tree, (node) => {
-      if (node.type === 'table' || node.type === 'tableCell' || node.type === 'tableRow') {
-
-        // omit position to fix the key regardless of its position
-        // see:
-        //   https://github.com/remarkjs/react-markdown/issues/703
-        //   https://github.com/remarkjs/react-markdown/issues/466
-        //
-        //   https://github.com/remarkjs/react-markdown/blob/a80dfdee2703d84ac2120d28b0e4998a5b417c85/lib/ast-to-react.js#L201-L204
-        //   https://github.com/remarkjs/react-markdown/blob/a80dfdee2703d84ac2120d28b0e4998a5b417c85/lib/ast-to-react.js#L217-L222
-        delete node.position;
-      }
-    });
-  };
-};

+ 1 - 4
packages/app/src/services/renderer/renderer.tsx

@@ -74,7 +74,7 @@ const baseSanitizeSchema = {
     iframe: ['allow', 'referrerpolicy', 'sandbox', 'src', 'srcdoc'],
     iframe: ['allow', 'referrerpolicy', 'sandbox', 'src', 'srcdoc'],
     // The special value 'data*' as a property name can be used to allow all data properties.
     // The special value 'data*' as a property name can be used to allow all data properties.
     // see: https://github.com/syntax-tree/hast-util-sanitize/
     // see: https://github.com/syntax-tree/hast-util-sanitize/
-    '*': ['class', 'className', 'style', 'data*'],
+    '*': ['key', 'class', 'className', 'style', 'data*'],
   },
   },
 };
 };
 
 
@@ -266,7 +266,6 @@ export const generateSimpleViewOptions = (
     drawioPlugin.remarkPlugin,
     drawioPlugin.remarkPlugin,
     xsvToTable.remarkPlugin,
     xsvToTable.remarkPlugin,
     lsxGrowiPlugin.remarkPlugin,
     lsxGrowiPlugin.remarkPlugin,
-    // table.remarkPlugin,
     attachmentPlugin.remarkPlugin,
     attachmentPlugin.remarkPlugin,
   );
   );
 
 
@@ -337,7 +336,6 @@ export const generateSSRViewOptions = (
     math,
     math,
     xsvToTable.remarkPlugin,
     xsvToTable.remarkPlugin,
     lsxGrowiPlugin.remarkPlugin,
     lsxGrowiPlugin.remarkPlugin,
-    // table.remarkPlugin,
     attachmentPlugin.remarkPlugin,
     attachmentPlugin.remarkPlugin,
   );
   );
 
 
@@ -391,7 +389,6 @@ export const generatePreviewOptions = (config: RendererConfig, pagePath: string)
     drawioPlugin.remarkPlugin,
     drawioPlugin.remarkPlugin,
     xsvToTable.remarkPlugin,
     xsvToTable.remarkPlugin,
     lsxGrowiPlugin.remarkPlugin,
     lsxGrowiPlugin.remarkPlugin,
-    // table.remarkPlugin,
     attachmentPlugin.remarkPlugin,
     attachmentPlugin.remarkPlugin,
   );
   );
   if (config.isEnabledLinebreaks) {
   if (config.isEnabledLinebreaks) {

+ 1 - 1
packages/app/src/styles/bootstrap/_override.scss

@@ -45,7 +45,7 @@ h5 {
 
 
 h6 {
 h6 {
   font-size: 12px;
   font-size: 12px;
-  line-height: 14px;
+  line-height: 18px;
 }
 }
 
 
 // Navs
 // Navs

+ 117 - 51
packages/app/test/cypress/integration/20-basic-features/20-basic-features--access-to-page.spec.ts

@@ -208,7 +208,49 @@ context('Access to special pages', () => {
 });
 });
 
 
 context('Access to Template Editing Mode', () => {
 context('Access to Template Editing Mode', () => {
-  const ssPrefix = 'access-to-modal-';
+  const ssPrefix = 'access-to-template-page-';
+  const templateBody1 = 'Template for children';
+  const templateBody2 = 'Template for descendants';
+
+  const createPageFromPageTreeTest = (newPagePath: string, parentPagePath: string, expectedBody: string) => {
+    cy.visit('/');
+    cy.waitUntilSkeletonDisappear();
+
+    // Open sidebar
+    cy.collapseSidebar(false);
+    cy.getByTestid('grw-contextual-navigation-sub').should('be.visible');
+    cy.waitUntilSkeletonDisappear();
+
+    // If PageTree is not active when the sidebar is opened, make it active
+    cy.getByTestid('grw-sidebar-nav-primary-page-tree').should('be.visible')
+      .then($elem => {
+        if (!$elem.hasClass('active')) {
+          cy.getByTestid('grw-sidebar-nav-primary-page-tree').click();
+        }
+      });
+
+    // Create page (/{parentPath}}/{newPagePath}) from PageTree
+    cy.getByTestid('grw-contextual-navigation-sub').within(() => {
+      cy.get('.grw-pagetree-item-children').first().as('pagetreeItem').within(() => {
+        cy.get('#page-create-button-in-page-tree').first().click({force: true})
+      });
+    });
+    cy.get('@pagetreeItem').within(() => {
+      cy.getByTestid('closable-text-input').type(newPagePath).type('{enter}');
+    })
+
+    cy.visit(`/${parentPagePath}/${newPagePath}`);
+    cy.waitUntilSkeletonDisappear();
+    cy.collapseSidebar(true);
+
+    // Check if the template is applied
+    cy.get('.content-main').within(() => {
+      cy.get('.wiki').should('be.visible');
+      cy.get('.wiki').children().first().should('have.text', expectedBody);
+    })
+
+    cy.screenshot(`${ssPrefix}-page(${newPagePath})-to-which-template-is-applied`)
+  }
 
 
   beforeEach(() => {
   beforeEach(() => {
     // login
     // login
@@ -217,58 +259,82 @@ context('Access to Template Editing Mode', () => {
     });
     });
   });
   });
 
 
-  // TODO: 109057
-  // it('Access to Template Editor mode for only child pages successfully', () => {
-  //   cy.visit('/Sandbox/Bootstrap4', {  });
-  //   cy.waitUntilSkeletonDisappear();
-
-  //   cy.get('#grw-subnav-container').within(() => {
-  //     cy.getByTestid('open-page-item-control-btn').should('be.visible');
-  //     cy.getByTestid('open-page-item-control-btn').click();
-  //     cy.getByTestid('open-page-template-modal-btn').should('be.visible');
-  //     cy.getByTestid('open-page-template-modal-btn').click();
-  //   });
-
-  //   cy.getByTestid('page-template-modal').should('be.visible');
-  //   cy.screenshot(`${ssPrefix}-open-page-template-bootstrap4`);
-
-  // Todo: `@`alias may be changed. This code was made in an attempt to solve the error of element being dettached from the dom which couldn't be solved at this time.
-  // Wait for Todo: 109057 is solved and fix or leave the code below for better test code.
-  //   cy.getByTestid('template-button-children').as('template-button-children');
-  //   cy.get('@template-button-children').should('be.visible').click();
-  //   cy.waitUntilSkeletonDisappear();
-
-  //   cy.getByTestid('navbar-editor').should('be.visible').then(()=>{
-  //     cy.url().should('include', '/_template#edit');
-  //     cy.screenshot();
-  //   });
-  // });
+  it("Successfully created template for children", () => {
+    cy.visit('/Sandbox');
+    cy.waitUntilSkeletonDisappear();
 
 
-  // TODO: 109057
-  // it('Access to Template Editor mode including decendants successfully', () => {
-  //   cy.visit('/Sandbox/Bootstrap4', {  });
-  //   cy.waitUntilSkeletonDisappear();
-
-  //   cy.get('#grw-subnav-container').within(() => {
-  //     cy.getByTestid('open-page-item-control-btn').should('be.visible');
-  //     cy.getByTestid('open-page-item-control-btn').click();
-  //     cy.getByTestid('open-page-template-modal-btn').should('be.visible');
-  //     cy.getByTestid('open-page-template-modal-btn').click();
-  //   });
-  //   cy.getByTestid('page-template-modal').should('be.visible');
-
-  // Todo: `@`alias may be changed. This code was made in an attempt to solve the error of element being dettached from the dom which couldn't be solved at this time.
-  // Wait for Todo: 109057 is solved and fix or leave the code below for better test code.
-  //   cy.getByTestid('template-button-decendants').as('template-button-decendants');
-  //   cy.get('@template-button-decendants').should('be.visible').click();
-  //   cy.waitUntilSkeletonDisappear();
-
-  //   cy.getByTestid('navbar-editor').should('be.visible').then(()=>{
-  //     cy.url().should('include', '/__template#edit');
-  //     cy.screenshot();
-  //   });
-  // });
+    cy.get('#grw-subnav-container').within(() => {
+      cy.getByTestid('open-page-item-control-btn').click({force: true});
+      cy.getByTestid('open-page-template-modal-btn').click({force: true});
+    });
+
+    cy.getByTestid('page-template-modal').should('be.visible');
+    cy.screenshot(`${ssPrefix}-open-page-template-modal`);
+
+    cy.getByTestid('template-button-children').click(({force: true}))
+    cy.waitUntilSkeletonDisappear();
+
+    cy.getByTestid('navbar-editor').should('be.visible').then(()=>{
+      cy.url().should('include', '/_template#edit');
+      cy.screenshot(`${ssPrefix}-open-template-page-for-children-in-editor-mode`);
+    });
 
 
+    cy.get('.CodeMirror').type(templateBody1);
+    cy.get('.CodeMirror').contains(templateBody1);
+    cy.get('.page-editor-preview-body').contains(templateBody1);
+    cy.getByTestid('page-editor').should('be.visible');
+    cy.getByTestid('save-page-btn').click();
+  });
+
+  it('Template is applied to pages created from PageTree (template for children 1)', () => {
+    createPageFromPageTreeTest('template-test-page1', '/Sandbox' ,templateBody1);
+  });
+
+  it('Successfully created template for descendants', () => {
+    cy.visit('/Sandbox');
+    cy.waitUntilSkeletonDisappear();
+
+    cy.get('#grw-subnav-container').within(() => {
+      cy.getByTestid('open-page-item-control-btn').click({force: true});
+      cy.getByTestid('open-page-template-modal-btn').click({force: true});
+    });
+
+    cy.getByTestid('page-template-modal').should('be.visible');
+
+    cy.getByTestid('template-button-decendants').click(({force: true}))
+    cy.waitUntilSkeletonDisappear();
+
+    cy.getByTestid('navbar-editor').should('be.visible').then(()=>{
+      cy.url().should('include', '/__template#edit');
+      cy.screenshot(`${ssPrefix}-open-template-page-for-descendants-in-editor-mode`);
+    })
+
+    cy.get('.CodeMirror').type(templateBody2);
+    cy.get('.CodeMirror').contains(templateBody2);
+    cy.get('.page-editor-preview-body').contains(templateBody2);
+    cy.getByTestid('page-editor').should('be.visible');
+    cy.getByTestid('save-page-btn').click();
+  });
+
+  it('Template is applied to pages created from PageTree (template for children 2)', () => {
+    createPageFromPageTreeTest('template-test-page2','Sandbox',templateBody1);
+  });
+
+  it('Template is applied to pages created from PageTree (template for descendants)', () => {
+    // delete /Sandbox/_template
+    cy.visit('/Sandbox/_template');
+    cy.get('#grw-subnav-container').within(() => {
+      cy.getByTestid('open-page-item-control-btn').click({force: true});
+      cy.getByTestid('open-page-delete-modal-btn').click({force: true});
+    });
+    cy.getByTestid('page-delete-modal').should('be.visible').within(() => {
+      cy.intercept('POST', '/_api/pages.remove').as('remove');
+      cy.getByTestid('delete-page-button').click();
+      cy.wait('@remove')
+    });
+
+    createPageFromPageTreeTest('template-test-page3','Sandbox',`${templateBody1}\n${templateBody2}`);
+  })
 });
 });
 
 
 context('Access to /me/all-in-app-notifications', () => {
 context('Access to /me/all-in-app-notifications', () => {

+ 61 - 0
packages/app/test/cypress/integration/20-basic-features/20-basic-features--username-mention.spec.ts

@@ -0,0 +1,61 @@
+context('Mention username in comment', () => {
+  const ssPrefix = 'mention-username-';
+
+  beforeEach(() => {
+    // login
+    cy.fixture("user-admin.json").then(user => {
+      cy.login(user.username, user.password);
+    });
+
+    // Visit /Sandbox
+    cy.visit('/Sandbox');
+    cy.waitUntilSkeletonDisappear();
+
+    cy.collapseSidebar(true, true);
+
+    // Go to comment page
+    cy.getByTestid('page-comment-button').click();
+
+    // Open comment editor
+    cy.waitUntil(() => {
+      // do
+      cy.getByTestid('open-comment-editor-button').click();
+      // wait until
+      return cy.get('.comment-write').then($elem => $elem.is(':visible'));
+    });
+
+  });
+
+  it('Successfully mention username in comment', () => {
+    const username = '@adm';
+
+    cy.waitUntil(() => {
+      // do
+      cy.get('.CodeMirror').type(username);
+      // wait until
+      return cy.get('.CodeMirror-hints').then($elem => $elem.is(':visible'));
+    });
+
+    cy.get('#comments-container').within(() => { cy.screenshot(`${ssPrefix}1-username-found`) });
+    // Click on mentioned username
+    cy.get('.CodeMirror-hints > li').first().click();
+    cy.get('#comments-container').within(() => { cy.screenshot(`${ssPrefix}2-username-mentioned`) });
+  });
+
+  it('Username not found when mention username in comment', () => {
+    const username = '@user';
+
+    cy.waitUntil(() => {
+      // do
+      cy.get('.CodeMirror').type(username);
+      // wait until
+      return cy.get('.CodeMirror-hints').then($elem => $elem.is(':visible'));
+    });
+
+    cy.get('#comments-container').within(() => { cy.screenshot(`${ssPrefix}3-username-not-found`) });
+    // Click on username not found hint
+    cy.get('.CodeMirror-hints > li').first().click();
+    cy.get('#comments-container').within(() => { cy.screenshot(`${ssPrefix}4-no-username-mentioned`) });
+  });
+
+});

+ 1 - 1
packages/codemirror-textlint/package.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "@growi/codemirror-textlint",
   "name": "@growi/codemirror-textlint",
-  "version": "6.0.14-RC.0",
+  "version": "6.0.16-RC.0",
   "license": "MIT",
   "license": "MIT",
   "main": "dist/index.js",
   "main": "dist/index.js",
   "scripts": {
   "scripts": {

+ 1 - 1
packages/core/package.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "@growi/core",
   "name": "@growi/core",
-  "version": "6.0.14-RC.0",
+  "version": "6.0.16-RC.0",
   "description": "GROWI Core Libraries",
   "description": "GROWI Core Libraries",
   "license": "MIT",
   "license": "MIT",
   "keywords": [
   "keywords": [

+ 1 - 1
packages/hackmd/package.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "@growi/hackmd",
   "name": "@growi/hackmd",
-  "version": "6.0.14-RC.0",
+  "version": "6.0.16-RC.0",
   "description": "GROWI js and css files to use hackmd",
   "description": "GROWI js and css files to use hackmd",
   "license": "MIT",
   "license": "MIT",
   "main": "dist/index.js",
   "main": "dist/index.js",

+ 3 - 3
packages/presentation/package.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "@growi/presentation",
   "name": "@growi/presentation",
-  "version": "6.0.14-RC.0",
+  "version": "6.0.16-RC.0",
   "description": "GROWI plugin for presentation",
   "description": "GROWI plugin for presentation",
   "license": "MIT",
   "license": "MIT",
   "keywords": ["growi", "growi-plugin"],
   "keywords": ["growi", "growi-plugin"],
@@ -15,7 +15,7 @@
     "test": ""
     "test": ""
   },
   },
   "dependencies": {
   "dependencies": {
-    "@growi/core": "^6.0.14-RC.0"
+    "@growi/core": "^6.0.16-RC.0"
   },
   },
   "devDependencies": {
   "devDependencies": {
     "@marp-team/marp-core": "^3.4.2",
     "@marp-team/marp-core": "^3.4.2",
@@ -23,7 +23,7 @@
     "eslint-plugin-regex": "^1.8.0",
     "eslint-plugin-regex": "^1.8.0",
     "react": "^18.2.0",
     "react": "^18.2.0",
     "react-dom": "^18.2.0",
     "react-dom": "^18.2.0",
-    "react-markdown": "^8.0.3",
+    "react-markdown": "^8.0.7",
     "reveal.js": "^4.4.0"
     "reveal.js": "^4.4.0"
   }
   }
 }
 }

+ 1 - 1
packages/preset-themes/package.json

@@ -1,7 +1,7 @@
 {
 {
   "name": "@growi/preset-themes",
   "name": "@growi/preset-themes",
   "description": "GROWI preset themes",
   "description": "GROWI preset themes",
-  "version": "6.0.14-RC.0",
+  "version": "6.0.16-RC.0",
   "license": "MIT",
   "license": "MIT",
   "main": "dist/libs/index.js",
   "main": "dist/libs/index.js",
   "files": [
   "files": [

+ 2 - 3
packages/remark-drawio/package.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "@growi/remark-drawio",
   "name": "@growi/remark-drawio",
-  "version": "6.0.14-RC.0",
+  "version": "6.0.16-RC.0",
   "description": "remark plugin to draw diagrams with draw.io (diagrams.net)",
   "description": "remark plugin to draw diagrams with draw.io (diagrams.net)",
   "license": "MIT",
   "license": "MIT",
   "keywords": [
   "keywords": [
@@ -24,8 +24,7 @@
     "test": ""
     "test": ""
   },
   },
   "dependencies": {
   "dependencies": {
-    "pako": "^2.1.0",
-    "xmldoc": "^1.2.0"
+    "pako": "^2.1.0"
   },
   },
   "devDependencies": {
   "devDependencies": {
     "eslint-plugin-regex": "^1.8.0",
     "eslint-plugin-regex": "^1.8.0",

+ 3 - 1
packages/remark-drawio/src/components/DrawioViewer.tsx

@@ -84,7 +84,9 @@ export const DrawioViewer = React.memo((props: DrawioViewerProps): JSX.Element =
     }
     }
 
 
     const code = children instanceof Array
     const code = children instanceof Array
-      ? children.map(e => e?.toString()).join('')
+      ? children
+        .filter(elem => (typeof elem === 'string')) // omit non-string elements (e.g. br element generated by line-breaks option)
+        .join('')
       : children.toString();
       : children.toString();
 
 
     let mxgraphData;
     let mxgraphData;

+ 1 - 9
packages/remark-drawio/src/services/renderer/remark-drawio.ts

@@ -21,6 +21,7 @@ function rewriteNode(node: Node, index: number) {
     diagramIndex: index,
     diagramIndex: index,
     bol: node.position?.start.line,
     bol: node.position?.start.line,
     eol: node.position?.end.line,
     eol: node.position?.end.line,
+    key: `drawio-${index}`,
   };
   };
 }
 }
 
 
@@ -30,15 +31,6 @@ export const remarkPlugin: Plugin = function() {
       if (node.type === 'code') {
       if (node.type === 'code') {
         if (isDrawioBlock(node.lang)) {
         if (isDrawioBlock(node.lang)) {
           rewriteNode(node, index ?? 0);
           rewriteNode(node, index ?? 0);
-
-          // omit position to fix the key regardless of its position
-          // see:
-          //   https://github.com/remarkjs/react-markdown/issues/703
-          //   https://github.com/remarkjs/react-markdown/issues/466
-          //
-          //   https://github.com/remarkjs/react-markdown/blob/a80dfdee2703d84ac2120d28b0e4998a5b417c85/lib/ast-to-react.js#L201-L204
-          //   https://github.com/remarkjs/react-markdown/blob/a80dfdee2703d84ac2120d28b0e4998a5b417c85/lib/ast-to-react.js#L217-L222
-          delete node.position;
         }
         }
       }
       }
     });
     });

+ 4 - 4
packages/remark-drawio/src/utils/embed.ts

@@ -1,9 +1,9 @@
 // transplanted from https://github.com/jgraph/drawio-tools/blob/d46977060ffad70cae5a9059a2cbfcd8bcf420de/tools/convert.html
 // transplanted from https://github.com/jgraph/drawio-tools/blob/d46977060ffad70cae5a9059a2cbfcd8bcf420de/tools/convert.html
 import pako from 'pako';
 import pako from 'pako';
-import xmldoc from 'xmldoc';
 
 
+const unconpressedDataRegexp = new RegExp('<mxGraphModel');
 const validateUncompressedData = (input: string): boolean => {
 const validateUncompressedData = (input: string): boolean => {
-  return new RegExp('/<mxGraphModel/').test(input);
+  return unconpressedDataRegexp.test(input);
 };
 };
 
 
 const validateCompressedData = (input: string): boolean => {
 const validateCompressedData = (input: string): boolean => {
@@ -60,14 +60,14 @@ export const generateMxgraphData = (code: string): string => {
   // Evaluate the code is whether uncompressed data that are generated by v21.1.0 or above
   // Evaluate the code is whether uncompressed data that are generated by v21.1.0 or above
   // see: https://github.com/jgraph/drawio/issues/3106#issuecomment-1479352026
   // see: https://github.com/jgraph/drawio/issues/3106#issuecomment-1479352026
   const isUncompressedData = validateUncompressedData(trimedCode);
   const isUncompressedData = validateUncompressedData(trimedCode);
-  if (isUncompressedData) {
+  if (!isUncompressedData) {
     validateCompressedData(trimedCode);
     validateCompressedData(trimedCode);
   }
   }
 
 
   const xml = `
   const xml = `
     <mxfile version="6.8.9" editor="www.draw.io" type="atlas">
     <mxfile version="6.8.9" editor="www.draw.io" type="atlas">
       <mxAtlasLibraries/>
       <mxAtlasLibraries/>
-      <diagram>${isUncompressedData ? xmldoc.XmlDocument(trimedCode) : trimedCode}</diagram>
+      <diagram>${trimedCode}</diagram>
     </mxfile>
     </mxfile>
   `;
   `;
 
 

+ 1 - 1
packages/remark-growi-directive/package.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "@growi/remark-growi-directive",
   "name": "@growi/remark-growi-directive",
-  "version": "6.0.14-RC.0",
+  "version": "6.0.16-RC.0",
   "description": "remark plugin to support GROWI plugin (forked from remark-directive@2.0.1)",
   "description": "remark plugin to support GROWI plugin (forked from remark-directive@2.0.1)",
   "license": "MIT",
   "license": "MIT",
   "keywords": [
   "keywords": [

+ 4 - 4
packages/remark-lsx/package.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "@growi/remark-lsx",
   "name": "@growi/remark-lsx",
-  "version": "6.0.14-RC.0",
+  "version": "6.0.16-RC.0",
   "description": "GROWI plugin to list pages",
   "description": "GROWI plugin to list pages",
   "license": "MIT",
   "license": "MIT",
   "keywords": ["growi", "growi-plugin"],
   "keywords": ["growi", "growi-plugin"],
@@ -20,9 +20,9 @@
     "test": ""
     "test": ""
   },
   },
   "dependencies": {
   "dependencies": {
-    "@growi/core": "^6.0.14-RC.0",
-    "@growi/remark-growi-directive": "^6.0.14-RC.0",
-    "@growi/ui": "^6.0.14-RC.0",
+    "@growi/core": "^6.0.16-RC.0",
+    "@growi/remark-growi-directive": "^6.0.16-RC.0",
+    "@growi/ui": "^6.0.16-RC.0",
     "swr": "^2.0.3"
     "swr": "^2.0.3"
   },
   },
   "devDependencies": {
   "devDependencies": {

+ 2 - 11
packages/remark-lsx/src/services/renderer/lsx.ts

@@ -11,14 +11,14 @@ import { visit } from 'unist-util-visit';
 const NODE_NAME_PATTERN = new RegExp(/ls|lsx/);
 const NODE_NAME_PATTERN = new RegExp(/ls|lsx/);
 const SUPPORTED_ATTRIBUTES = ['prefix', 'num', 'depth', 'sort', 'reverse', 'filter', 'except', 'isSharedPage'];
 const SUPPORTED_ATTRIBUTES = ['prefix', 'num', 'depth', 'sort', 'reverse', 'filter', 'except', 'isSharedPage'];
 
 
-const { addHeadingSlash, hasHeadingSlash } = pathUtils;
+const { hasHeadingSlash } = pathUtils;
 
 
 type DirectiveAttributes = Record<string, string>
 type DirectiveAttributes = Record<string, string>
 
 
 
 
 export const remarkPlugin: Plugin = function() {
 export const remarkPlugin: Plugin = function() {
   return (tree) => {
   return (tree) => {
-    visit(tree, (node) => {
+    visit(tree, (node, index) => {
       if (node.type === remarkGrowiDirectivePluginType.Text || node.type === remarkGrowiDirectivePluginType.Leaf) {
       if (node.type === remarkGrowiDirectivePluginType.Text || node.type === remarkGrowiDirectivePluginType.Leaf) {
         if (typeof node.name !== 'string') {
         if (typeof node.name !== 'string') {
           return;
           return;
@@ -49,15 +49,6 @@ export const remarkPlugin: Plugin = function() {
 
 
         data.hName = 'lsx';
         data.hName = 'lsx';
         data.hProperties = attributes;
         data.hProperties = attributes;
-
-        // omit position to fix the key regardless of its position
-        // see:
-        //   https://github.com/remarkjs/react-markdown/issues/703
-        //   https://github.com/remarkjs/react-markdown/issues/466
-        //
-        //   https://github.com/remarkjs/react-markdown/blob/a80dfdee2703d84ac2120d28b0e4998a5b417c85/lib/ast-to-react.js#L201-L204
-        //   https://github.com/remarkjs/react-markdown/blob/a80dfdee2703d84ac2120d28b0e4998a5b417c85/lib/ast-to-react.js#L217-L222
-        delete node.position;
       }
       }
     });
     });
   };
   };

+ 1 - 1
packages/slack/package.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "@growi/slack",
   "name": "@growi/slack",
-  "version": "6.0.14-RC.0",
+  "version": "6.0.16-RC.0",
   "license": "MIT",
   "license": "MIT",
   "main": "dist/index.js",
   "main": "dist/index.js",
   "typings": "dist/index.d.ts",
   "typings": "dist/index.d.ts",

+ 2 - 2
packages/slackbot-proxy/package.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "@growi/slackbot-proxy",
   "name": "@growi/slackbot-proxy",
-  "version": "6.0.14-slackbot-proxy.0",
+  "version": "6.0.16-slackbot-proxy.0",
   "license": "MIT",
   "license": "MIT",
   "scripts": {
   "scripts": {
     "build": "yarn tsc && tsc-alias -p tsconfig.build.json",
     "build": "yarn tsc && tsc-alias -p tsconfig.build.json",
@@ -26,7 +26,7 @@
   },
   },
   "dependencies": {
   "dependencies": {
     "@godaddy/terminus": "^4.9.0",
     "@godaddy/terminus": "^4.9.0",
-    "@growi/slack": "^6.0.14-RC.0",
+    "@growi/slack": "^6.0.16-RC.0",
     "@slack/oauth": "^2.0.1",
     "@slack/oauth": "^2.0.1",
     "@slack/web-api": "^6.2.4",
     "@slack/web-api": "^6.2.4",
     "@tsed/common": "^6.43.0",
     "@tsed/common": "^6.43.0",

+ 2 - 2
packages/ui/package.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "@growi/ui",
   "name": "@growi/ui",
-  "version": "6.0.14-RC.0",
+  "version": "6.0.16-RC.0",
   "description": "GROWI UI Libraries",
   "description": "GROWI UI Libraries",
   "license": "MIT",
   "license": "MIT",
   "keywords": ["growi"],
   "keywords": ["growi"],
@@ -17,7 +17,7 @@
     "test": "jest --verbose"
     "test": "jest --verbose"
   },
   },
   "dependencies": {
   "dependencies": {
-    "@growi/core": "^6.0.14-RC.0"
+    "@growi/core": "^6.0.16-RC.0"
   },
   },
   "devDependencies": {
   "devDependencies": {
     "eslint-plugin-regex": "^1.8.0",
     "eslint-plugin-regex": "^1.8.0",

+ 5 - 12
yarn.lock

@@ -17881,10 +17881,10 @@ react-lifecycles-compat@^3.0.4:
   resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362"
   resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362"
   integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==
   integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==
 
 
-react-markdown@^8.0.3:
-  version "8.0.5"
-  resolved "https://registry.yarnpkg.com/react-markdown/-/react-markdown-8.0.5.tgz#c9a70a33ca9aeeafb769c6582e7e38843b9d70ad"
-  integrity sha512-jGJolWWmOWAvzf+xMdB9zwStViODyyFQhNB/bwCerbBKmrTmgmA599CGiOlP58OId1IMoIRsA8UdI1Lod4zb5A==
+react-markdown@^8.0.7:
+  version "8.0.7"
+  resolved "https://registry.yarnpkg.com/react-markdown/-/react-markdown-8.0.7.tgz#c8dbd1b9ba5f1c5e7e5f2a44de465a3caafdf89b"
+  integrity sha512-bvWbzG4MtOU62XqBx3Xx+zB2raaFFsq4mYiAzfjXJMEz2sixgeAfraA3tvzULF02ZdOMUOKTBFFaZJDDrq+BJQ==
   dependencies:
   dependencies:
     "@types/hast" "^2.0.0"
     "@types/hast" "^2.0.0"
     "@types/prop-types" "^15.0.0"
     "@types/prop-types" "^15.0.0"
@@ -19168,7 +19168,7 @@ sax@1.2.1:
   version "1.2.1"
   version "1.2.1"
   resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.1.tgz#7b8e656190b228e81a66aea748480d828cd2d37a"
   resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.1.tgz#7b8e656190b228e81a66aea748480d828cd2d37a"
 
 
-sax@>=0.6.0, sax@^1.2.4:
+sax@>=0.6.0:
   version "1.2.4"
   version "1.2.4"
   resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9"
   resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9"
   integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==
   integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==
@@ -22703,13 +22703,6 @@ xmlbuilder@~9.0.1:
   resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-9.0.7.tgz#132ee63d2ec5565c557e20f4c22df9aca686b10d"
   resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-9.0.7.tgz#132ee63d2ec5565c557e20f4c22df9aca686b10d"
   integrity sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0=
   integrity sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0=
 
 
-xmldoc@^1.2.0:
-  version "1.2.0"
-  resolved "https://registry.yarnpkg.com/xmldoc/-/xmldoc-1.2.0.tgz#7554371bfd8c138287cff01841ae4566d26e5541"
-  integrity sha512-2eN8QhjBsMW2uVj7JHLHkMytpvGHLHxKXBy4J3fAT/HujsEtM6yU84iGjpESYGHg6XwK0Vu4l+KgqQ2dv2cCqg==
-  dependencies:
-    sax "^1.2.4"
-
 xmldom-sre@0.1.31:
 xmldom-sre@0.1.31:
   version "0.1.31"
   version "0.1.31"
   resolved "https://registry.yarnpkg.com/xmldom-sre/-/xmldom-sre-0.1.31.tgz#10860d5bab2c603144597d04bf2c4980e98067f4"
   resolved "https://registry.yarnpkg.com/xmldom-sre/-/xmldom-sre-0.1.31.tgz#10860d5bab2c603144597d04bf2c4980e98067f4"