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

Merge pull request #7539 from weseek/master

Release v6.0.15
Yuki Takei 3 лет назад
Родитель
Сommit
b261b76c71
28 измененных файлов с 267 добавлено и 152 удалено
  1. 1 1
      lerna.json
  2. 1 1
      package.json
  3. 11 11
      packages/app/package.json
  4. 1 1
      packages/app/src/components/Admin/ExportArchiveDataPage.tsx
  5. 5 4
      packages/app/src/components/PageComment/CommentEditor.tsx
  6. 1 0
      packages/app/src/components/PageEditor/CodeMirrorEditor.jsx
  7. 0 1
      packages/app/src/components/PageEditor/CodeMirrorEditor.module.scss
  8. 15 12
      packages/app/src/components/PageEditor/CommentMentionHelper.ts
  9. 6 5
      packages/app/src/components/PageSideContents.tsx
  10. 35 37
      packages/app/src/components/Sidebar/PageTree/Item.tsx
  11. 0 3
      packages/app/src/components/Sidebar/PageTree/ItemsTree.tsx
  12. 22 5
      packages/app/src/server/routes/apiv3/pages.js
  13. 1 1
      packages/app/src/server/service/page.ts
  14. 1 1
      packages/app/src/styles/bootstrap/_override.scss
  15. 86 51
      packages/app/test/cypress/integration/20-basic-features/20-basic-features--access-to-page.spec.ts
  16. 61 0
      packages/app/test/cypress/integration/20-basic-features/20-basic-features--username-mention.spec.ts
  17. 1 1
      packages/codemirror-textlint/package.json
  18. 1 1
      packages/core/package.json
  19. 1 1
      packages/hackmd/package.json
  20. 2 2
      packages/presentation/package.json
  21. 1 1
      packages/preset-themes/package.json
  22. 1 1
      packages/remark-drawio/package.json
  23. 3 1
      packages/remark-drawio/src/components/DrawioViewer.tsx
  24. 1 1
      packages/remark-growi-directive/package.json
  25. 4 4
      packages/remark-lsx/package.json
  26. 1 1
      packages/slack/package.json
  27. 2 2
      packages/slackbot-proxy/package.json
  28. 2 2
      packages/ui/package.json

+ 1 - 1
lerna.json

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

+ 1 - 1
package.json

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

+ 11 - 11
packages/app/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/app",
-  "version": "6.0.14",
+  "version": "6.0.15-RC.0",
   "license": "MIT",
   "scripts": {
     "//// for production": "",
@@ -66,14 +66,14 @@
     "@elastic/elasticsearch7": "npm:@elastic/elasticsearch@^7.17.0",
     "@godaddy/terminus": "^4.9.0",
     "@google-cloud/storage": "^5.8.5",
-    "@growi/codemirror-textlint": "^6.0.14",
-    "@growi/core": "^6.0.14",
-    "@growi/hackmd": "^6.0.14",
-    "@growi/preset-themes": "^6.0.14",
-    "@growi/remark-drawio": "^6.0.14",
-    "@growi/remark-growi-directive": "^6.0.14",
-    "@growi/remark-lsx": "^6.0.14",
-    "@growi/slack": "^6.0.14",
+    "@growi/codemirror-textlint": "^6.0.15-RC.0",
+    "@growi/core": "^6.0.15-RC.0",
+    "@growi/hackmd": "^6.0.15-RC.0",
+    "@growi/preset-themes": "^6.0.15-RC.0",
+    "@growi/remark-drawio": "^6.0.15-RC.0",
+    "@growi/remark-growi-directive": "^6.0.15-RC.0",
+    "@growi/remark-lsx": "^6.0.15-RC.0",
+    "@growi/slack": "^6.0.15-RC.0",
     "@promster/express": "^7.0.6",
     "@promster/server": "^7.0.8",
     "@slack/web-api": "^6.2.4",
@@ -206,8 +206,8 @@
     "handsontable": "v7.0.0 or above is no loger MIT lisence."
   },
   "devDependencies": {
-    "@growi/presentation": "^6.0.14",
-    "@growi/ui": "^6.0.14",
+    "@growi/presentation": "^6.0.15-RC.0",
+    "@growi/ui": "^6.0.15-RC.0",
     "@handsontable/react": "=2.1.0",
     "@icon/themify-icons": "1.0.1-alpha.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 { data: socket } = useAdminSocket();
-  const { t } = useTranslation();
+  const { t } = useTranslation('admin');
 
   const [collections, setCollections] = 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"
             className="btn btn-lg btn-link"
             onClick={() => setIsReadyToUse(true)}
+            data-testid="open-comment-editor-button"
           >
             <i className="icon-bubble"></i> Add Comment
           </button>
@@ -300,9 +301,9 @@ export const CommentEditor = (props: CommentEditorProps): JSX.Element => {
         <div className="comment-submit">
           <div className="d-flex">
             <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">
                   <SlackNotification
@@ -321,7 +322,7 @@ export const CommentEditor = (props: CommentEditorProps): JSX.Element => {
           </div>
           <div className="d-block d-sm-none mt-2">
             <div className="d-flex justify-content-end">
-              { error && errorMessage }
+              {error && errorMessage}
               <span className="mr-2">{cancelButton}</span><span>{submitButton}</span>
             </div>
           </div>
@@ -337,7 +338,7 @@ export const CommentEditor = (props: CommentEditorProps): JSX.Element => {
           <UserPicture user={currentUser} noLink noTooltip />
         </div>
         <div className="comment-form-main">
-          { isReadyToUse
+          {isReadyToUse
             ? renderReady()
             : renderBeforeReady()
           }

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

@@ -40,6 +40,7 @@ import styles from './CodeMirrorEditor.module.scss';
 window.JSHINT = JSHINT;
 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/edit/matchbrackets');
 require('codemirror/addon/edit/matchtags');

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

@@ -4,7 +4,6 @@
   @import '~codemirror/lib/codemirror';
 
   // addons
-  @import '~codemirror/addon/hint/show-hint';
   @import '~codemirror/addon/fold/foldgutter';
   @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 { apiv3Get } from '~/client/util/apiv3-client';
 
+type UsersListForHints = {
+  text: string
+  displayText: string
+}
 export default class CommentMentionHelper {
 
-  editor;
-
-  pattern: RegExp;
-
+  editor: Editor;
 
-  constructor(editor) {
+  constructor(editor: Editor) {
     this.editor = editor;
   }
 
-  getUsernamHint = () => {
+  getUsenameHint = (): void => {
     // Get word that contains `@` character at the begining
     const currentPos = this.editor.getCursor();
     const wordStart = this.editor.findWordAt(currentPos).anchor.ch - 1;
@@ -32,14 +34,15 @@ export default class CommentMentionHelper {
     }
 
     // Get username after `@` character and search username
-    const mention = searchMention.substr(1);
+    const mention = searchMention.slice(1);
     this.editor.showHint({
       completeSingle: false,
       hint: async() => {
         if (mention.length > 0) {
           const users = await this.getUsersList(mention);
           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,
             to: searchTo,
           };
@@ -48,15 +51,15 @@ export default class CommentMentionHelper {
     });
   };
 
-  getUsersList = async(q: string) => {
+  getUsersList = async(q: string): Promise<UsersListForHints[]> => {
     const limit = 20;
     const { data } = await apiv3Get('/users/usernames', { q, limit });
-    return data.activeUser.usernames.map(username => ({
+    return data.activeUser.usernames.map((username: string) => ({
       text: `@${username} `,
       displayText: username,
     }));
   };
 
-  showUsernameHint = debounce(800, () => this.getUsernamHint());
+  showUsernameHint = debounce(800, () => this.getUsenameHint());
 
 }

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

@@ -37,7 +37,7 @@ export const PageSideContents = (props: PageSideContentsProps): JSX.Element => {
     <>
       {/* Page list */}
       <div className={`grw-page-accessories-control ${styles['grw-page-accessories-control']}`}>
-        { !isSharedUser && (
+        {!isSharedUser && (
           <button
             type="button"
             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')}
             <CountBadge count={page?.descendantCount} offset={1} />
           </button>
-        ) }
+        )}
       </div>
 
       {/* Comments */}
-      { !isTopPagePath && (
+      {!isTopPagePath && (
         <div className={`mt-2 grw-page-accessories-control ${styles['grw-page-accessories-control']}`}>
           <Link to={'page-comments'} offset={-120}>
             <button
               type="button"
               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>
               <span>Comments</span>
@@ -67,11 +68,11 @@ export const PageSideContents = (props: PageSideContentsProps): JSX.Element => {
             </button>
           </Link>
         </div>
-      ) }
+      )}
 
       <div className="d-none d-lg-block">
         <TableOfContents />
-        { isUsersHomePagePath && <ContentLinkButtons author={page?.creator} /> }
+        {isUsersHomePagePath && <ContentLinkButtons author={page?.creator} />}
       </div>
     </>
   );

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

@@ -4,7 +4,9 @@ import React, {
 
 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 Link from 'next/link';
 import { useDrag, useDrop } from 'react-dnd';
@@ -19,7 +21,7 @@ import {
   IPageHasId, IPageInfoAll, IPageToDeleteWithMeta,
 } from '~/interfaces/page';
 import { IPageForPageDuplicateModal } from '~/stores/modal';
-import { useSWRxPageChildren } from '~/stores/page-listing';
+import { mutatePageTree, useSWRxPageChildren } from '~/stores/page-listing';
 import { usePageTreeDescCountMap } from '~/stores/ui';
 import loggerFactory from '~/utils/logger';
 import { shouldRecoverPagePaths } from '~/utils/page-operation';
@@ -39,7 +41,6 @@ interface ItemProps {
   itemNode: ItemNode
   targetPathOrId?: Nullable<string>
   isOpen?: boolean
-  isEnabledAttachTitleHeader?: boolean
   onRenamed?(fromPath: string | undefined, toPath: string): void
   onClickDuplicateMenuItem?(pageToDuplicate: IPageForPageDuplicateModal): void
   onClickDeleteMenuItem?(pageToDelete: IPageToDeleteWithMeta): void
@@ -111,7 +112,7 @@ const NotDraggableForClosableTextInput = (props: NotDraggableProps): JSX.Element
 const Item: FC<ItemProps> = (props: ItemProps) => {
   const { t } = useTranslation();
   const {
-    itemNode, targetPathOrId, isOpen: _isOpen = false, isEnabledAttachTitleHeader,
+    itemNode, targetPathOrId, isOpen: _isOpen = false,
     onRenamed, onClickDuplicateMenuItem, onClickDeleteMenuItem, isEnableActions,
   } = props;
 
@@ -192,6 +193,7 @@ const Item: FC<ItemProps> = (props: ItemProps) => {
         updateMetadata: true,
       });
 
+      await mutatePageTree();
       await mutateChildren();
 
       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 => {
     return currentChildren != null && currentChildren.length > 0;
@@ -333,21 +339,14 @@ const Item: FC<ItemProps> = (props: ItemProps) => {
       return;
     }
 
-    let initBody = '';
-    if (isEnabledAttachTitleHeader) {
-      const pageTitle = nodePath.basename(newPagePath);
-      initBody = pathUtils.attachTitleHeader(pageTitle);
-    }
-
     try {
       setCreating(true);
 
       await apiv3Post('/pages/', {
         path: newPagePath,
-        body: initBody,
+        body: undefined,
         grant: page.grant,
         grantUserGroupId: page.grantedGroup,
-        createFromPageTree: true,
       });
 
       mutateChildren();
@@ -447,7 +446,7 @@ const Item: FC<ItemProps> = (props: ItemProps) => {
             </button>
           )}
         </div>
-        { isRenameInputShown
+        {isRenameInputShown
           ? (
             <div className="flex-fill">
               <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>
                   <UncontrolledTooltip placement="top" target="path-recovery" fade={false}>
@@ -471,7 +470,7 @@ const Item: FC<ItemProps> = (props: ItemProps) => {
                   </UncontrolledTooltip>
                 </>
               )}
-              { page != null && page.path != null && page._id != null && (
+              {page != null && page.path != null && page._id != null && (
                 <Link
                   href={pathUtils.returnPathForURL(page.path, page._id)}
                   className="grw-pagetree-title-anchor flex-grow-1"
@@ -543,12 +542,11 @@ const Item: FC<ItemProps> = (props: ItemProps) => {
               itemNode={node}
               isOpen={false}
               targetPathOrId={targetPathOrId}
-              isEnabledAttachTitleHeader={isEnabledAttachTitleHeader}
               onRenamed={onRenamed}
               onClickDuplicateMenuItem={onClickDuplicateMenuItem}
               onClickDeleteMenuItem={onClickDeleteMenuItem}
             />
-            { isCreating && (currentChildren.length - 1 === index) && (
+            {isCreating && (currentChildren.length - 1 === index) && (
               <div className="text-muted text-center">
                 <i className="fa fa-spinner fa-pulse mr-1"></i>
               </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 { OnDuplicatedFunction, OnDeletedFunction } from '~/interfaces/ui';
 import { SocketEventName, UpdateDescCountData, UpdateDescCountRawData } from '~/interfaces/websocket';
-import { useIsEnabledAttachTitleHeader } from '~/stores/context';
 import {
   IPageForPageDuplicateModal, usePageDuplicateModal, usePageDeleteModal,
 } from '~/stores/modal';
@@ -110,7 +109,6 @@ const ItemsTree = (props: ItemsTreeProps): JSX.Element => {
   const { data: ancestorsChildrenResult, error: error1 } = useSWRxPageAncestorsChildren(targetPath);
   const { data: rootPageResult, error: error2 } = useSWRxRootPage();
   const { data: currentPagePath } = useCurrentPagePath();
-  const { data: isEnabledAttachTitleHeader } = useIsEnabledAttachTitleHeader();
   const { open: openDuplicateModal } = usePageDuplicateModal();
   const { open: openDeleteModal } = usePageDeleteModal();
   const { data: sidebarScrollerRef } = useSidebarScrollerRef();
@@ -279,7 +277,6 @@ const ItemsTree = (props: ItemsTreeProps): JSX.Element => {
           targetPathOrId={targetPathOrId}
           itemNode={initialItemNode}
           isOpen
-          isEnabledAttachTitleHeader={isEnabledAttachTitleHeader}
           isEnableActions={isEnableActions}
           onRenamed={onRenamed}
           onClickDuplicateMenuItem={onClickDuplicateMenuItem}

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

@@ -165,8 +165,8 @@ module.exports = (crowi) => {
 
   const validator = {
     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 })
         .withMessage('path is required'),
       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('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'),
@@ -309,10 +308,28 @@ module.exports = (crowi) => {
       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;
     try {
       createdPage = await createPageAction({
-        path, body, user: req.user, options,
+        path, body: isNoBodyPage ? initialBody : body, user: req.user, options,
       });
     }
     catch (err) {
@@ -320,7 +337,7 @@ module.exports = (crowi) => {
       return res.apiv3Err(err);
     }
 
-    const savedTags = await saveTagsAction({ createdPage, pageTags });
+    const savedTags = await saveTagsAction({ createdPage, pageTags: isNoBodyPage ? initialTags : pageTags });
 
     const result = {
       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
     if (!this.isRenamingToUnderTarget(page.path, newPagePath)) {
-    // remove empty pages at leaf position
+      // remove empty pages at leaf position
       await Page.removeLeafEmptyPagesRecursively(page.parent);
     }
 

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

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

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

@@ -208,7 +208,8 @@ context('Access to special pages', () => {
 });
 
 context('Access to Template Editing Mode', () => {
-  const ssPrefix = 'access-to-modal-';
+  const ssPrefix = 'access-to-template-page-';
+  const templateBody = 'Template for children';
 
   beforeEach(() => {
     // login
@@ -217,58 +218,92 @@ 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 a template page by accessing the editor mode for children's templates", () => {
+    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(templateBody);
+    cy.get('.CodeMirror').contains(templateBody);
+    cy.get('.page-editor-preview-body').contains(templateBody);
+    cy.getByTestid('page-editor').should('be.visible');
+    cy.getByTestid('save-page-btn').click();
+  });
+
+  it('Successfully accessed the editor mode for the descendant template page', () => {
+    cy.visit('/Sandbox/Bootstrap4');
+    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`);
+    });
+  });
+
+  it('Templates are applied to pages created from the PageTree', () => {
+    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 (/Sandbox/template-test-page) 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('template-test-page').type('{enter}');
+    })
+
+    cy.visit('/Sandbox/template-test-page');
+    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', templateBody);
+    })
+
+    cy.screenshot(`${ssPrefix}-page-to-which-template-is-applied`)
+  });
 });
 
 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",
-  "version": "6.0.14",
+  "version": "6.0.15-RC.0",
   "license": "MIT",
   "main": "dist/index.js",
   "scripts": {

+ 1 - 1
packages/core/package.json

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

+ 1 - 1
packages/hackmd/package.json

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

+ 2 - 2
packages/presentation/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/presentation",
-  "version": "6.0.14",
+  "version": "6.0.15-RC.0",
   "description": "GROWI plugin for presentation",
   "license": "MIT",
   "keywords": ["growi", "growi-plugin"],
@@ -15,7 +15,7 @@
     "test": ""
   },
   "dependencies": {
-    "@growi/core": "^6.0.14"
+    "@growi/core": "^6.0.15-RC.0"
   },
   "devDependencies": {
     "@marp-team/marp-core": "^3.4.2",

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

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

+ 1 - 1
packages/remark-drawio/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/remark-drawio",
-  "version": "6.0.14",
+  "version": "6.0.15-RC.0",
   "description": "remark plugin to draw diagrams with draw.io (diagrams.net)",
   "license": "MIT",
   "keywords": [

+ 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
-      ? 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();
 
     let mxgraphData;

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

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

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

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

+ 1 - 1
packages/slack/package.json

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

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

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

+ 2 - 2
packages/ui/package.json

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