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

Merge pull request #6569 from weseek/master

Release v5.1.5
Yuki Takei 3 лет назад
Родитель
Сommit
9725485e97
45 измененных файлов с 511 добавлено и 220 удалено
  1. 1 1
      lerna.json
  2. 1 1
      package.json
  3. 2 2
      packages/app/bin/github-actions/update-readme.sh
  4. 2 2
      packages/app/docker/README.md
  5. 8 8
      packages/app/package.json
  6. 5 1
      packages/app/public/static/locales/en_US/admin/admin.json
  7. 1 0
      packages/app/public/static/locales/en_US/translation.json
  8. 5 1
      packages/app/public/static/locales/ja_JP/admin/admin.json
  9. 3 2
      packages/app/public/static/locales/ja_JP/translation.json
  10. 5 1
      packages/app/public/static/locales/zh_CN/admin/admin.json
  11. 1 0
      packages/app/public/static/locales/zh_CN/translation.json
  12. 4 5
      packages/app/src/components/Admin/Customize/CustomizeLogoSetting.tsx
  13. 6 1
      packages/app/src/components/Common/Dropdown/PageItemControl.tsx
  14. 54 13
      packages/app/src/components/Common/ImageCropModal.tsx
  15. 3 4
      packages/app/src/components/Me/ProfileImageSettings.tsx
  16. 15 13
      packages/app/src/components/Navbar/GrowiContextualSubNavigation.tsx
  17. 1 1
      packages/app/src/components/Page/TagLabels.tsx
  18. 101 29
      packages/app/src/components/PageEditor/CodeMirrorEditor.jsx
  19. 9 13
      packages/app/src/components/PageEditor/EmojiPicker.tsx
  20. 51 62
      packages/app/src/components/PageEditor/EmojiPickerHelper.ts
  21. 1 0
      packages/app/src/components/PageList/PageListItemL.tsx
  22. 1 1
      packages/app/src/components/PrivateLegacyPages.tsx
  23. 1 1
      packages/app/src/components/SavePageControls.jsx
  24. 1 1
      packages/app/src/components/Sidebar/RecentChanges.tsx
  25. 1 1
      packages/app/src/interfaces/activity.ts
  26. 1 1
      packages/app/src/server/routes/index.js
  27. 16 2
      packages/app/src/server/service/installer.ts
  28. 9 0
      packages/app/test/cypress/integration/20-basic-features/access-to-page.spec.ts
  29. 11 4
      packages/app/test/cypress/integration/20-basic-features/access-to-pagelist.spec.ts
  30. 82 10
      packages/app/test/cypress/integration/20-basic-features/use-tools.spec.ts
  31. 14 10
      packages/app/test/cypress/integration/21-basic-features-for-guest/access-to-page.spec.ts
  32. 53 12
      packages/app/test/cypress/integration/30-search/search.spec.ts
  33. 2 0
      packages/app/test/cypress/integration/40-admin/access-to-admin-page.spec.ts
  34. 21 3
      packages/app/test/cypress/integration/50-sidebar/access-to-side-bar.spec.ts
  35. 7 2
      packages/app/test/cypress/integration/50-sidebar/switching-sidebar-mode.spec.ts
  36. 2 2
      packages/app/test/cypress/support/commands.ts
  37. 1 1
      packages/app/test/cypress/support/index.ts
  38. 1 1
      packages/codemirror-textlint/package.json
  39. 1 1
      packages/core/package.json
  40. 1 1
      packages/plugin-attachment-refs/package.json
  41. 1 1
      packages/plugin-lsx/package.json
  42. 1 1
      packages/plugin-pukiwiki-like-linker/package.json
  43. 1 1
      packages/slack/package.json
  44. 2 2
      packages/slackbot-proxy/package.json
  45. 1 1
      packages/ui/package.json

+ 1 - 1
lerna.json

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

+ 1 - 1
package.json

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

+ 2 - 2
packages/app/bin/github-actions/update-readme.sh

@@ -2,5 +2,5 @@
 
 cd docker
 
-sed -i -e "s/^\([*] \[\`\)[^\`]\+\(\`, \`5\.1\`, .\+\]\)\(.\+\/blob\/v\).\+\(\/docker\/Dockerfile.\+\)$/\1${RELEASED_VERSION}\2\3${RELEASED_VERSION}\4/" README.md
-sed -i -e "s/^\([*] \[\`\)[^\`]\+\(\`, \`5\.1-nocdn\`, .\+\]\)\(.\+\/blob\/v\).\+\(\/docker\/Dockerfile.\+\)$/\1${RELEASED_VERSION}-nocdn\2\3${RELEASED_VERSION}\4/" README.md
+sed -i -e "s/^\([*] \[\`\)[^\`]\+\(\`, \`5\.1\`, .\+\]\)\(.\+\/blob\/v\).\+\(\/packages\/app\/docker\/Dockerfile.\+\)$/\1${RELEASED_VERSION}\2\3${RELEASED_VERSION}\4/" README.md
+sed -i -e "s/^\([*] \[\`\)[^\`]\+\(\`, \`5\.1-nocdn\`, .\+\]\)\(.\+\/blob\/v\).\+\(\/packages\/app\/docker\/Dockerfile.\+\)$/\1${RELEASED_VERSION}-nocdn\2\3${RELEASED_VERSION}\4/" README.md

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

@@ -10,8 +10,8 @@ GROWI Official docker image
 Supported tags and respective Dockerfile links
 ------------------------------------------------
 
-* [`5.1.4`, `5.1`, `5`, `latest` (Dockerfile)](https://github.com/weseek/growi/blob/v5.1.4/docker/Dockerfile)
-* [`5.1.4-nocdn`, `5.1-nocdn`, `5-nocdn`, `latest-nocdn` (Dockerfile)](https://github.com/weseek/growi/blob/v5.1.4/docker/Dockerfile)
+* [`5.1.4`, `5.1`, `5`, `latest` (Dockerfile)](https://github.com/weseek/growi/blob/v5.1.4/packages/app/docker/Dockerfile)
+* [`5.1.4-nocdn`, `5.1-nocdn`, `5-nocdn`, `latest-nocdn` (Dockerfile)](https://github.com/weseek/growi/blob/v5.1.4/packages/app/docker/Dockerfile)
 * [`5.0.11`, `5.0` (Dockerfile)](https://github.com/weseek/growi/blob/v5.0.11/packages/app/docker/Dockerfile)
 * [`5.0.11-nocdn`, `5.0-nocdn` (Dockerfile)](https://github.com/weseek/growi/blob/v5.0.11/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)

+ 8 - 8
packages/app/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/app",
-  "version": "5.1.4",
+  "version": "5.1.5-RC.0",
   "license": "MIT",
   "scripts": {
     "//// for production": "",
@@ -64,12 +64,12 @@
     "@elastic/elasticsearch7": "npm:@elastic/elasticsearch@^7.17.0",
     "@godaddy/terminus": "^4.9.0",
     "@google-cloud/storage": "^5.8.5",
-    "@growi/codemirror-textlint": "^5.1.4",
-    "@growi/core": "^5.1.4",
-    "@growi/plugin-attachment-refs": "^5.1.4",
-    "@growi/plugin-lsx": "^5.1.4",
-    "@growi/plugin-pukiwiki-like-linker": "^5.1.4",
-    "@growi/slack": "^5.1.4",
+    "@growi/codemirror-textlint": "^5.1.5-RC.0",
+    "@growi/core": "^5.1.5-RC.0",
+    "@growi/plugin-attachment-refs": "^5.1.5-RC.0",
+    "@growi/plugin-lsx": "^5.1.5-RC.0",
+    "@growi/plugin-pukiwiki-like-linker": "^5.1.5-RC.0",
+    "@growi/slack": "^5.1.5-RC.0",
     "@promster/express": "^7.0.2",
     "@promster/server": "^7.0.4",
     "@slack/events-api": "^3.0.0",
@@ -172,7 +172,7 @@
   },
   "devDependencies": {
     "@alienfast/i18next-loader": "^1.1.4",
-    "@growi/ui": "^5.1.4",
+    "@growi/ui": "^5.1.5-RC.0",
     "@handsontable/react": "=2.1.0",
     "@types/compression": "^1.7.0",
     "@types/express": "^4.17.11",

+ 5 - 1
packages/app/public/static/locales/en_US/admin/admin.json

@@ -598,6 +598,10 @@
     "PAGE_DELETE_COMPLETELY": "Delete completely page",
     "PAGE_REVERT": "Revert page",
     "PAGE_EMPTY_TRASH": "Empty trash",
+    "PAGE_RECURSIVELY_RENAME": "Recursive page rename",
+    "PAGE_RECURSIVELY_DELETE": "Recursive page delete",
+    "PAGE_RECURSIVELY_DELETE_COMPLETELY": "Recursive page delete completely",
+    "PAGE_RECURSIVELY_REVERT": "Recursive page revert",
     "PAGE_SUBSCRIBE": "Subscribe page",
     "PAGE_UNSUBSCRIBE": "Unsubscribe page",
     "PAGE_EXPORT": "Export page",
@@ -615,7 +619,7 @@
     "SHARE_LINK_NOT_FOUND": "Page view (Not found share link)",
     "ATTACHMENT_ADD": "Add Attachment",
     "ATTACHMENT_REMOVE": "Delete Attachment",
-    "ACTION_ATTACHMENT_DOWNLOAD": "Download Attachment",
+    "ATTACHMENT_DOWNLOAD": "Download Attachment",
     "SEARCH_PAGE": "Page Search",
     "SEARCH_PAGE_VIEW": "Page view(Search results page)",
     "ADMIN_APP_SETTING_UPDATE": "Update App Settings",

+ 1 - 0
packages/app/public/static/locales/en_US/translation.json

@@ -1088,6 +1088,7 @@
   "crop_image_modal": {
     "image_crop": "Image Crop",
     "crop": "Crop",
+    "save": "Save",
     "reset": "Reset",
     "cancel": "Cancel"
   },

+ 5 - 1
packages/app/public/static/locales/ja_JP/admin/admin.json

@@ -597,6 +597,10 @@
     "PAGE_DELETE_COMPLETELY": "ページの完全削除",
     "PAGE_REVERT": "ページを元に戻す",
     "PAGE_EMPTY_TRASH": "ゴミ箱を空にする",
+    "PAGE_RECURSIVELY_RENAME": "再帰的なページのリネーム",
+    "PAGE_RECURSIVELY_DELETE": "再帰的なページの削除",
+    "PAGE_RECURSIVELY_DELETE_COMPLETELY": "再起的なページの完全削除",
+    "PAGE_RECURSIVELY_REVERT": "再起的なページの復元",
     "PAGE_SUBSCRIBE": "ページをサブスクライブ",
     "PAGE_UNSUBSCRIBE": "ページをアンサブスクライブ",
     "PAGE_EXPORT": "マークダウン形式でページをエクスポート",
@@ -614,7 +618,7 @@
     "SHARE_LINK_NOT_FOUND": "ページ閲覧(存在しない共有リンク)",
     "ATTACHMENT_ADD": "添付データの追加",
     "ATTACHMENT_REMOVE": "添付データの削除",
-    "ACTION_ATTACHMENT_DOWNLOAD": "添付データのダウンロード",
+    "ATTACHMENT_DOWNLOAD": "添付データのダウンロード",
     "SEARCH_PAGE": "ページの検索",
     "SEARCH_PAGE_VIEW": "ページ閲覧(検索結果ページ)",
     "ADMIN_APP_SETTING_UPDATE": "アプリ設定の更新",

+ 3 - 2
packages/app/public/static/locales/ja_JP/translation.json

@@ -882,8 +882,8 @@
       "attrMapId": "ID",
       "attrMapUsername": "ユーザー名",
       "attrMapMail": "メールアドレス",
-      "attrMapFirstName": "",
-      "attrMapLastName": "",
+      "attrMapFirstName": "",
+      "attrMapLastName": "",
       "ABLCRule": "ルール"
     }
   },
@@ -1081,6 +1081,7 @@
   "crop_image_modal": {
     "image_crop": "画像の切り抜き",
     "crop": "トリミング",
+    "save": "保存",
     "reset": "リセット",
     "cancel": "キャンセル"
   },

+ 5 - 1
packages/app/public/static/locales/zh_CN/admin/admin.json

@@ -608,6 +608,10 @@
     "PAGE_DELETE_COMPLETELY": "彻底删除页面",
     "PAGE_REVERT": "还原页面",
     "PAGE_EMPTY_TRASH": "清空垃圾箱",
+    "PAGE_RECURSIVELY_RENAME": "递归页面重命名",
+    "PAGE_RECURSIVELY_DELETE": "递归页面删除",
+    "PAGE_RECURSIVELY_DELETE_COMPLETELY": "递归页面完全删除",
+    "PAGE_RECURSIVELY_REVERT": "递归页面还原",
     "PAGE_SUBSCRIBE": "订阅页面",
     "PAGE_UNSUBSCRIBE": "退订页面",
     "PAGE_EXPORT": "导出页面",
@@ -625,7 +629,7 @@
     "SHARE_LINK_NOT_FOUND": "页面浏览量(未找到分享链接)",
     "ATTACHMENT_ADD": "添加附件",
     "ATTACHMENT_REMOVE": "删除附件",
-    "ACTION_ATTACHMENT_DOWNLOAD": "下载附件",
+    "ATTACHMENT_DOWNLOAD": "下载附件",
     "SEARCH_PAGE": "页面搜索",
     "SEARCH_PAGE_VIEW": "页面浏览量(搜索结果页面)",
     "ADMIN_APP_SETTING_UPDATE": "更新应用设置",

+ 1 - 0
packages/app/public/static/locales/zh_CN/translation.json

@@ -1091,6 +1091,7 @@
   "crop_image_modal": {
     "image_crop": "图像裁剪",
     "crop": "修剪",
+    "save": "节省",
     "reset": "重启",
     "cancel": "取消"
   },

+ 4 - 5
packages/app/src/components/Admin/Customize/CustomizeLogoSetting.tsx

@@ -66,7 +66,6 @@ const CustomizeLogoSetting = (): JSX.Element => {
     }
   }, [t, isDefaultLogo, customizedLogoSrc]);
 
-
   const onClickDeleteBtn = useCallback(async() => {
     try {
       await apiv3Delete('/customize-setting/delete-brand-logo');
@@ -80,7 +79,8 @@ const CustomizeLogoSetting = (): JSX.Element => {
     }
   }, [t]);
 
-  const onCropCompleted = useCallback(async(croppedImage) => {
+
+  const processImageCompletedHandler = useCallback(async(croppedImage) => {
     try {
       const formData = new FormData();
       formData.append('file', croppedImage);
@@ -93,10 +93,8 @@ const CustomizeLogoSetting = (): JSX.Element => {
       setRetrieveError(err);
       throw new Error('Failed to upload brand logo');
     }
-    setIsImageCropModalShow(false);
   }, [t]);
 
-
   return (
     <React.Fragment>
       <div className="row">
@@ -172,8 +170,9 @@ const CustomizeLogoSetting = (): JSX.Element => {
         isShow={isImageCropModalShow}
         src={uploadLogoSrc}
         onModalClose={() => setIsImageCropModalShow(false)}
-        onCropCompleted={onCropCompleted}
+        onImageProcessCompleted={processImageCompletedHandler}
         isCircular={false}
+        showCropOption={false}
       />
     </React.Fragment>
   );

+ 6 - 1
packages/app/src/components/Common/Dropdown/PageItemControl.tsx

@@ -245,7 +245,12 @@ const PageItemControlDropdownMenu = React.memo((props: DropdownMenuProps): JSX.E
   }
 
   return (
-    <DropdownMenu positionFixed modifiers={{ preventOverflow: { boundariesElement: undefined } }} right={alignRight}>
+    <DropdownMenu
+      data-testid="page-item-control-menu"
+      positionFixed
+      modifiers={{ preventOverflow: { boundariesElement: undefined } }}
+      right={alignRight}
+    >
       {contents}
     </DropdownMenu>
   );

+ 54 - 13
packages/app/src/components/Common/ImageCropModal.tsx

@@ -33,17 +33,19 @@ type Props = {
   isShow: boolean,
   src: string | ArrayBuffer | null,
   onModalClose: () => void,
-  onCropCompleted: (res: any) => void,
+  onImageProcessCompleted: (res: any) => void,
   isCircular: boolean,
+  showCropOption: boolean
 }
 const ImageCropModal: FC<Props> = (props: Props) => {
 
   const {
-    isShow, src, onModalClose, onCropCompleted, isCircular,
+    isShow, src, onModalClose, onImageProcessCompleted, isCircular, showCropOption,
   } = props;
 
-  const [imageRef, setImageRef] = useState<HTMLImageElement>();
+  const [imageRef, setImageRef] = useState<HTMLImageElement | null>(null);
   const [cropOptions, setCropOtions] = useState<CropOptions>(null);
+  const [isCropImage, setIsCropImage] = useState<boolean>(true);
   const { t } = useTranslation();
   const reset = useCallback(() => {
     if (imageRef) {
@@ -93,31 +95,70 @@ const ImageCropModal: FC<Props> = (props: Props) => {
     }
   };
 
-  const crop = async() => {
-    // crop immages
+  // Convert base64 Image to blob
+  const convertBase64ToBlob = async(base64Image: string) => {
+    const base64Response = await fetch(base64Image);
+    return base64Response.blob();
+  };
+
+
+  // Clear image and set isImageCrop true on modal close
+  const onModalCloseHandler = async() => {
+    setImageRef(null);
+    setIsCropImage(true);
+    onModalClose();
+  };
+
+  // Process and save image
+  // Cropping image is optional
+  // If crop is active , the saved image is cropped image (png). Otherwise, the original image will be saved (Original size and file type)
+  const processAndSaveImage = async() => {
     if (imageRef && cropOptions?.width && cropOptions.height) {
-      const result = await getCroppedImg(imageRef, cropOptions);
-      onCropCompleted(result);
+      const processedImage = isCropImage ? await getCroppedImg(imageRef, cropOptions) : await convertBase64ToBlob(imageRef.src);
+      // Save image to database
+      onImageProcessCompleted(processedImage);
     }
+    onModalCloseHandler();
   };
 
   return (
-    <Modal isOpen={isShow} toggle={onModalClose}>
-      <ModalHeader tag="h4" toggle={onModalClose} className="bg-info text-light">
+    <Modal isOpen={isShow} toggle={onModalCloseHandler}>
+      <ModalHeader tag="h4" toggle={onModalCloseHandler} className="bg-info text-light">
         {t('crop_image_modal.image_crop')}
       </ModalHeader>
       <ModalBody className="my-4">
-        <ReactCrop src={src} crop={cropOptions} onImageLoaded={onImageLoaded} onChange={onCropChange} circularCrop={isCircular} />
+        {
+          isCropImage
+            ? (<ReactCrop src={src} crop={cropOptions} onImageLoaded={onImageLoaded} onChange={onCropChange} circularCrop={isCircular} />)
+            : (<img style={{ maxWidth: imageRef?.width }} src={imageRef?.src} />)
+        }
       </ModalBody>
       <ModalFooter>
         <button type="button" className="btn btn-outline-danger rounded-pill mr-auto" onClick={reset}>
           {t('crop_image_modal.reset')}
         </button>
-        <button type="button" className="btn btn-outline-secondary rounded-pill mr-2" onClick={onModalClose}>
+        { !showCropOption && (
+          <div className="mr-auto">
+            <div className="custom-control custom-switch ">
+              <input
+                id="cropImageOption"
+                className="custom-control-input mr-auto"
+                type="checkbox"
+                checked={isCropImage}
+                onChange={() => { setIsCropImage(!isCropImage) }}
+              />
+              <label className="custom-control-label" htmlFor="cropImageOption">
+                { t('crop_image_modal.image_crop') }
+              </label>
+            </div>
+          </div>
+        )
+        }
+        <button type="button" className="btn btn-outline-secondary rounded-pill mr-2" onClick={onModalCloseHandler}>
           {t('crop_image_modal.cancel')}
         </button>
-        <button type="button" className="btn btn-outline-primary rounded-pill" onClick={crop}>
-          {t('crop_image_modal.crop')}
+        <button type="button" className="btn btn-outline-primary rounded-pill" onClick={processAndSaveImage}>
+          { isCropImage ? t('crop_image_modal.crop') : t('crop_image_modal.save') }
         </button>
       </ModalFooter>
     </Modal>

+ 3 - 4
packages/app/src/components/Me/ProfileImageSettings.tsx

@@ -42,7 +42,7 @@ const ProfileImageSettings = (): JSX.Element => {
     setShowImageCropModal(true);
   }, []);
 
-  const cropCompletedHandler = useCallback(async(croppedImage) => {
+  const processImageCompletedHandler = useCallback(async(croppedImage) => {
     try {
       const formData = new FormData();
       formData.append('file', croppedImage);
@@ -53,8 +53,6 @@ const ProfileImageSettings = (): JSX.Element => {
       // eslint-disable-next-line @typescript-eslint/no-explicit-any
       setUploadedPictureSrc((response as any).attachment.filePathProxied);
 
-      // close modal
-      setShowImageCropModal(false);
     }
     catch (err) {
       toastError(err);
@@ -158,8 +156,9 @@ const ProfileImageSettings = (): JSX.Element => {
         isShow={showImageCropModal}
         src={imageCropSrc}
         onModalClose={() => setShowImageCropModal(false)}
-        onCropCompleted={cropCompletedHandler}
+        onImageProcessCompleted={processImageCompletedHandler}
         isCircular
+        showCropOption
       />
 
       <div className="row my-3">

+ 15 - 13
packages/app/src/components/Navbar/GrowiContextualSubNavigation.tsx

@@ -360,19 +360,21 @@ const GrowiContextualSubNavigation = (props) => {
   };
 
   return (
-    <GrowiSubNavigation
-      page={currentPage}
-      showDrawerToggler={isDrawerMode}
-      showTagLabel={isAbleToShowTagLabel}
-      showPageAuthors={isAbleToShowPageAuthors}
-      isGuestUser={isGuestUser}
-      isDrawerMode={isDrawerMode}
-      isCompactMode={isCompactMode}
-      tags={isViewMode ? tagsInfoData?.tags : tagsForEditors}
-      tagsUpdatedHandler={isViewMode ? tagsUpdatedHandlerForViewMode : tagsUpdatedHandlerForEditMode}
-      controls={ControlComponents}
-      additionalClasses={['container-fluid']}
-    />
+    <div data-testid="grw-contextual-sub-nav">
+      <GrowiSubNavigation
+        page={currentPage}
+        showDrawerToggler={isDrawerMode}
+        showTagLabel={isAbleToShowTagLabel}
+        showPageAuthors={isAbleToShowPageAuthors}
+        isGuestUser={isGuestUser}
+        isDrawerMode={isDrawerMode}
+        isCompactMode={isCompactMode}
+        tags={isViewMode ? tagsInfoData?.tags : tagsForEditors}
+        tagsUpdatedHandler={isViewMode ? tagsUpdatedHandlerForViewMode : tagsUpdatedHandlerForEditMode}
+        controls={ControlComponents}
+        additionalClasses={['container-fluid']}
+      />
+    </div>
   );
 };
 

+ 1 - 1
packages/app/src/components/Page/TagLabels.tsx

@@ -25,7 +25,7 @@ const TagLabels:FC<Props> = (props: Props) => {
 
   return (
     <>
-      <form className="grw-tag-labels form-inline">
+      <form className="grw-tag-labels form-inline" data-testid="grw-tag-labels">
         <i className="tag-icon icon-tag mr-2"></i>
         { tags == null
           ? (

+ 101 - 29
packages/app/src/components/PageEditor/CodeMirrorEditor.jsx

@@ -7,6 +7,7 @@ import * as loadCssSync from 'load-css-file';
 import PropTypes from 'prop-types';
 import { Button } from 'reactstrap';
 import * as loadScript from 'simple-load-script';
+import { throttle, debounce } from 'throttle-debounce';
 import urljoin from 'url-join';
 
 import InterceptorManager from '~/services/interceptor-manager';
@@ -115,7 +116,9 @@ class CodeMirrorEditor extends AbstractEditor {
       isCheatsheetModalShown: false,
       additionalClassSet: new Set(),
       isEmojiPickerShown: false,
-      emojiSearchText: null,
+      emojiSearchText: '',
+      startPosWithEmojiPickerModeTurnedOn: null,
+      isEmojiPickerMode: false,
     };
 
     this.gridEditModal = React.createRef();
@@ -140,7 +143,16 @@ class CodeMirrorEditor extends AbstractEditor {
     this.pasteHandler = this.pasteHandler.bind(this);
     this.cursorHandler = this.cursorHandler.bind(this);
     this.changeHandler = this.changeHandler.bind(this);
-    this.keyUpHandler = this.keyUpHandler.bind(this);
+    this.turnOnEmojiPickerMode = this.turnOnEmojiPickerMode.bind(this);
+    this.turnOffEmojiPickerMode = this.turnOffEmojiPickerMode.bind(this);
+    this.windowClickHandler = this.windowClickHandler.bind(this);
+    this.keyDownHandler = this.keyDownHandler.bind(this);
+    this.keyDownHandlerForEmojiPicker = this.keyDownHandlerForEmojiPicker.bind(this);
+    this.keyDownHandlerForEmojiPickerThrottled = throttle(400, this.keyDownHandlerForEmojiPicker);
+    this.showEmojiPicker = this.showEmojiPicker.bind(this);
+    this.keyPressHandlerForEmojiPicker = this.keyPressHandlerForEmojiPicker.bind(this);
+    this.keyPressHandlerForEmojiPickerThrottled = debounce(50, throttle(200, this.keyPressHandlerForEmojiPicker));
+    this.keyPressHandler = this.keyPressHandler.bind(this);
 
     this.updateCheatsheetStates = this.updateCheatsheetStates.bind(this);
 
@@ -155,7 +167,6 @@ class CodeMirrorEditor extends AbstractEditor {
 
     this.foldDrawioSection = this.foldDrawioSection.bind(this);
     this.onSaveForDrawio = this.onSaveForDrawio.bind(this);
-    this.checkWhetherEmojiPickerShouldBeShown = this.checkWhetherEmojiPickerShouldBeShown.bind(this);
 
   }
 
@@ -186,6 +197,13 @@ class CodeMirrorEditor extends AbstractEditor {
     }
     this.emojiPickerHelper = new EmojiPickerHelper(this.getCodeMirror());
 
+    // HACKME: Find a better way to handle onClick for Editor
+    document.addEventListener('click', this.windowClickHandler);
+  }
+
+  componentWillUnmount() {
+    // HACKME: Find a better way to handle onClick for Editor
+    document.removeEventListener('click', this.windowClickHandler);
   }
 
   componentWillReceiveProps(nextProps) {
@@ -586,10 +604,82 @@ class CodeMirrorEditor extends AbstractEditor {
 
   }
 
-  keyUpHandler(editor, event) {
-    if (event.key !== 'Backspace') {
-      this.checkWhetherEmojiPickerShouldBeShown();
+  turnOnEmojiPickerMode(pos) {
+    this.setState({
+      isEmojiPickerMode: true,
+      startPosWithEmojiPickerModeTurnedOn: pos,
+    });
+  }
+
+  turnOffEmojiPickerMode() {
+    this.setState({
+      isEmojiPickerMode: false,
+    });
+  }
+
+  showEmojiPicker(initialSearchingText) {
+    // show emoji picker with a stored word
+    this.setState({
+      isEmojiPickerShown: true,
+      emojiSearchText: initialSearchingText ?? '',
+    });
+
+    const resetStartPos = initialSearchingText == null;
+    if (resetStartPos) {
+      this.setState({ startPosWithEmojiPickerModeTurnedOn: null });
     }
+
+    this.turnOffEmojiPickerMode();
+  }
+
+  keyPressHandlerForEmojiPicker(editor, event) {
+    const char = event.key;
+    const isEmojiPickerMode = this.state.isEmojiPickerMode;
+
+    // evaluate whether emoji picker mode to be turned on
+    if (!isEmojiPickerMode) {
+      const startPos = this.emojiPickerHelper.shouldModeTurnOn(char);
+      if (startPos == null) {
+        return;
+      }
+
+      this.turnOnEmojiPickerMode(startPos);
+      return;
+    }
+
+    // evaluate whether EmojiPicker to be opened
+    const startPos = this.state.startPosWithEmojiPickerModeTurnedOn;
+    if (this.emojiPickerHelper.shouldOpen(startPos)) {
+      const initialSearchingText = this.emojiPickerHelper.getInitialSearchingText(startPos);
+      this.showEmojiPicker(initialSearchingText);
+      return;
+    }
+
+    this.turnOffEmojiPickerMode();
+  }
+
+  keyPressHandler(editor, event) {
+    this.keyPressHandlerForEmojiPickerThrottled(editor, event);
+  }
+
+  keyDownHandlerForEmojiPicker(editor, event) {
+    const key = event.key;
+
+    if (!this.state.isEmojiPickerMode) {
+      return;
+    }
+
+    if (['ArrowRight', 'ArrowLeft', 'ArrowUp', 'ArrowDown', 'BackSpace'].includes(key)) {
+      this.turnOffEmojiPickerMode();
+    }
+  }
+
+  keyDownHandler(editor, event) {
+    this.keyDownHandlerForEmojiPickerThrottled(editor, event);
+  }
+
+  windowClickHandler() {
+    this.turnOffEmojiPickerMode();
   }
 
   /**
@@ -613,26 +703,6 @@ class CodeMirrorEditor extends AbstractEditor {
 
   }
 
-  /**
-   * Show emoji picker component when emoji pattern (`:` + searchWord ) found
-   * eg `:a`, `:ap`
-   */
-  checkWhetherEmojiPickerShouldBeShown() {
-    const searchWord = this.emojiPickerHelper.getEmoji();
-
-    if (searchWord == null) {
-      this.setState({ isEmojiPickerShown: false });
-      this.setState({ emojiSearchText: null });
-    }
-    else {
-      this.setState({ emojiSearchText: searchWord });
-      // Show emoji picker after user stop typing
-      setTimeout(() => {
-        this.setState({ isEmojiPickerShown: true });
-      }, 700);
-    }
-  }
-
   /**
    * update states which related to cheatsheet
    * @param {boolean} isGfmModeTmp (use state.isGfmMode if null is set)
@@ -712,7 +782,8 @@ class CodeMirrorEditor extends AbstractEditor {
         <div className="text-left">
           <div className="mb-2 d-none d-md-block">
             <EmojiPicker
-              onClose={() => this.setState({ isEmojiPickerShown: false, emojiSearchText: null })}
+              onClose={() => this.setState({ isEmojiPickerShown: false })}
+              onSelected={emoji => this.emojiPickerHelper.addEmoji(emoji, this.state.startPosWithEmojiPickerModeTurnedOn)}
               emojiSearchText={emojiSearchText}
               emojiPickerHelper={this.emojiPickerHelper}
               isOpen={this.state.isEmojiPickerShown}
@@ -966,7 +1037,7 @@ class CodeMirrorEditor extends AbstractEditor {
         color={null}
         bssize="small"
         title="Emoji"
-        onClick={() => this.setState({ isEmojiPickerShown: true })}
+        onClick={() => this.showEmojiPicker()}
       >
         <EditorIcon icon="Emoji" />
       </Button>,
@@ -1045,7 +1116,8 @@ class CodeMirrorEditor extends AbstractEditor {
               this.props.onDragEnter(event);
             }
           }}
-          onKeyUp={this.keyUpHandler}
+          onKeyPress={this.keyPressHandler}
+          onKeyDown={this.keyDownHandler}
         />
 
         { this.renderLoadingKeymapOverlay() }

+ 9 - 13
packages/app/src/components/PageEditor/EmojiPicker.tsx

@@ -1,4 +1,4 @@
-import React, { FC } from 'react';
+import React, { FC, useCallback } from 'react';
 
 import { Picker } from 'emoji-mart';
 import { Modal } from 'reactstrap';
@@ -9,6 +9,7 @@ import EmojiPickerHelper, { getEmojiTranslation } from './EmojiPickerHelper';
 
 type Props = {
   onClose: () => void,
+  onSelected: (emoji: string) => void,
   emojiSearchText: string,
   emojiPickerHelper: EmojiPickerHelper,
   isOpen: boolean
@@ -17,28 +18,23 @@ type Props = {
 const EmojiPicker: FC<Props> = (props: Props) => {
 
   const {
-    onClose, emojiSearchText, emojiPickerHelper, isOpen,
+    onClose, onSelected, emojiSearchText, emojiPickerHelper, isOpen,
   } = props;
 
   // Set search emoji input and trigger search
-  const searchEmoji = () => {
+  const searchEmoji = useCallback(() => {
     const input = window.document.querySelector('[id^="emoji-mart-search"]') as HTMLInputElement;
     const valueSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value')?.set;
     valueSetter?.call(input, emojiSearchText);
     const event = new Event('input', { bubbles: true });
     input.dispatchEvent(event);
     input.focus();
-  };
-
-  const selectEmoji = (emoji) => {
-    if (emojiSearchText !== null) {
-      emojiPickerHelper.addEmojiOnSearch(emoji);
-    }
-    else {
-      emojiPickerHelper.addEmoji(emoji);
-    }
+  }, [emojiSearchText]);
+
+  const selectEmoji = useCallback((emoji) => {
+    onSelected(emoji);
     onClose();
-  };
+  }, [onClose, onSelected]);
 
 
   const translation = getEmojiTranslation();

+ 51 - 62
packages/app/src/components/PageEditor/EmojiPickerHelper.ts

@@ -1,89 +1,78 @@
 import { CSSProperties } from 'react';
 
+import { Position } from 'codemirror';
 import i18n from 'i18next';
 
-// https://regex101.com/r/Gqhor8/1
-const EMOJI_PATTERN = new RegExp(/\B:[^:\s]+/);
+// https://regex101.com/r/x5LbOZ/1
+const EMOJI_PATTERN = new RegExp(/^:[a-z0-9-+_]+$/);
 
 export default class EmojiPickerHelper {
 
-editor;
+  editor;
 
-pattern: RegExp;
+  pattern: string;
 
-constructor(editor) {
-  this.editor = editor;
-  this.pattern = EMOJI_PATTERN;
-}
+  constructor(editor) {
+    this.editor = editor;
+  }
 
-setStyle = ():CSSProperties => {
-  const offset = 20;
-  const emojiPickerHeight = 420;
-  const cursorPos = this.editor.cursorCoords(true);
-  const editorPos = this.editor.getWrapperElement().getBoundingClientRect();
-  // Emoji Picker bottom position exceed editor's bottom position
-  if (cursorPos.bottom + emojiPickerHeight > editorPos.bottom) {
+  setStyle = (): CSSProperties => {
+    const offset = 20;
+    const emojiPickerHeight = 420;
+    const cursorPos = this.editor.cursorCoords(true);
+    const editorPos = this.editor.getWrapperElement().getBoundingClientRect();
+    // Emoji Picker bottom position exceed editor's bottom position
+    if (cursorPos.bottom + emojiPickerHeight > editorPos.bottom) {
+      return {
+        top: editorPos.bottom - emojiPickerHeight,
+        left: cursorPos.left + offset,
+        position: 'fixed',
+      };
+    }
     return {
-      top: editorPos.bottom - emojiPickerHeight,
+      top: cursorPos.top + offset,
       left: cursorPos.left + offset,
       position: 'fixed',
     };
-  }
-  return {
-    top: cursorPos.top + offset,
-    left: cursorPos.left + offset,
-    position: 'fixed',
   };
-}
 
-getSearchCursor =() => {
-  const currentPos = this.editor.getCursor();
-  const sc = this.editor.getSearchCursor(this.pattern, currentPos, { multiline: false });
-  return sc;
-}
+  shouldModeTurnOn = (char: string): Position | null | undefined => {
+    if (char !== ':') {
+      return null;
+    }
 
-// Add emoji when triggered by search
-addEmojiOnSearch = (emoji) => {
-  const currentPos = this.editor.getCursor();
-  const sc = this.getSearchCursor();
-  if (sc.findPrevious()) {
-    sc.replace(`${emoji.colons} `, this.editor.getTokenAt(currentPos).string);
-    this.editor.focus();
-    this.editor.refresh();
+    const currentPos = this.editor.getCursor();
+    const sc = this.editor.getSearchCursor(':', currentPos, { multiline: false });
+    if (sc.findPrevious()) {
+      return sc.pos.from;
+    }
   }
-}
 
+  shouldOpen = (startPos: Position): boolean => {
+    const currentPos = this.editor.getCursor();
+    const rangeStr = this.editor.getRange(startPos, currentPos);
 
-// Add emoji when triggered by click emoji icon on top of editor
-addEmoji = (emoji) => {
-  const currentPos = this.editor.getCursor();
-  const doc = this.editor.getDoc();
-  doc.replaceRange(`${emoji.colons} `, currentPos);
-  this.editor.focus();
-  this.editor.refresh();
-}
+    return EMOJI_PATTERN.test(rangeStr);
+  }
 
-getEmoji = () => {
-  const sc = this.getSearchCursor();
-  const currentPos = this.editor.getCursor();
+  getInitialSearchingText = (startPos: Position): void => {
+    const currentPos = this.editor.getCursor();
+    const rangeStr = this.editor.getRange(startPos, currentPos);
 
-  if (sc.findPrevious()) {
-    const isInputtingEmoji = (currentPos.line === sc.to().line && currentPos.ch === sc.to().ch);
-    // current search cursor position
-    if (!isInputtingEmoji) {
-      return;
-    }
-    const pos = {
-      line: sc.to().line,
-      ch: sc.to().ch,
-    };
-    const currentSearchText = sc.matches(true, pos).match[0];
-    const searchWord = currentSearchText.replace(':', '');
-    return searchWord;
+    return rangeStr.slice(1); // return without the heading ':'
   }
 
-  return;
-}
+  addEmoji = (emoji: { colons: string }, startPosToReplace: Position|null): void => {
+    const currentPos = this.editor.getCursor();
+
+    const from = startPosToReplace ?? currentPos;
+    const to = currentPos;
+
+    const doc = this.editor.getDoc();
+    doc.replaceRange(`${emoji.colons} `, from, to);
+    this.editor.focus();
+    this.editor.refresh();
+  };
 
 }
 

+ 1 - 0
packages/app/src/components/PageList/PageListItemL.tsx

@@ -163,6 +163,7 @@ const PageListItemLSubstance: ForwardRefRenderFunction<ISelectable, Props> = (pr
     <li
       key={pageData._id}
       className={`list-group-item d-flex align-items-center px-3 px-md-1 ${styleListGroupItem} ${styleActive}`}
+      data-testid="page-list-item-L"
       onClick={clickHandler}
     >
       <div className="text-break w-100">

+ 1 - 1
packages/app/src/components/PrivateLegacyPages.tsx

@@ -75,7 +75,7 @@ const SearchResultListHead = React.memo((props: SearchResultListHeadProps): JSX.
 
   if (isSuccess) {
     return (
-      <div className="card border-success mt-3">
+      <div className="card border-success mt-3" data-testid="search-result-private-legacy-pages">
         <div className="card-body">
           <h2 className="card-title text-success">{t('private_legacy_pages.nopages_title')}</h2>
           <p className="card-text">

+ 1 - 1
packages/app/src/components/SavePageControls.jsx

@@ -117,7 +117,7 @@ class SavePageControls extends React.Component {
         }
 
         <UncontrolledButtonDropdown direction="up">
-          <Button id="caret" color="primary" className="btn-submit" onClick={this.save}>{labelSubmitButton}</Button>
+          <Button data-testid="save-page-btn" id="caret" color="primary" className="btn-submit" onClick={this.save}>{labelSubmitButton}</Button>
           <DropdownToggle caret color="primary" />
           <DropdownMenu right>
             <DropdownItem onClick={this.saveAndOverwriteScopesOfDescendants}>

+ 1 - 1
packages/app/src/components/Sidebar/RecentChanges.tsx

@@ -30,7 +30,7 @@ function PageItemLower({ page }) {
         <div className="icon-bubble mr-1 d-inline-block"></div>
         <div className="mr-2 grw-list-counts d-inline-block">{page.commentCount}</div>
       </div>
-      <div className="grw-formatted-distance-date small mt-auto">
+      <div className="grw-formatted-distance-date small mt-auto" data-hide-in-vrt>
         <FormattedDistanceDate id={page._id} date={page.updatedAt} />
       </div>
     </div>

+ 1 - 1
packages/app/src/interfaces/activity.ts

@@ -67,7 +67,7 @@ const ACTION_SHARE_LINK_EXPIRED_PAGE_VIEW = 'SHARE_LINK_EXPIRED_PAGE_VIEW';
 const ACTION_SHARE_LINK_NOT_FOUND = 'SHARE_LINK_NOT_FOUND';
 const ACTION_ATTACHMENT_ADD = 'ATTACHMENT_ADD';
 const ACTION_ATTACHMENT_REMOVE = 'ATTACHMENT_REMOVE';
-const ACTION_ATTACHMENT_DOWNLOAD = 'ACTION_ATTACHMENT_DOWNLOAD';
+const ACTION_ATTACHMENT_DOWNLOAD = 'ATTACHMENT_DOWNLOAD';
 const ACTION_SEARCH_PAGE = 'SEARCH_PAGE';
 const ACTION_SEARCH_PAGE_VIEW = 'SEARCH_PAGE_VIEW';
 const ACTION_ADMIN_APP_SETTINGS_UPDATE = 'ADMIN_APP_SETTING_UPDATE';

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

@@ -200,7 +200,7 @@ module.exports = function(crowi, app) {
   app.get('/me'                                 , loginRequiredStrictly, injectUserUISettings, me.index);
   // external-accounts
   // my in-app-notifications
-  app.get('/me/all-in-app-notifications'   , loginRequiredStrictly, allInAppNotifications.list);
+  app.get('/me/all-in-app-notifications'   , loginRequiredStrictly, injectUserUISettings, allInAppNotifications.list);
   app.get('/me/external-accounts'               , loginRequiredStrictly, injectUserUISettings, me.externalAccounts.list);
   // my drafts
   app.get('/me/drafts'                          , loginRequiredStrictly, injectUserUISettings, me.drafts.list);

+ 16 - 2
packages/app/src/server/service/installer.ts

@@ -1,5 +1,6 @@
 import path from 'path';
 
+import { addSeconds } from 'date-fns';
 import ExtensibleCustomError from 'extensible-custom-error';
 import fs from 'graceful-fs';
 import mongoose from 'mongoose';
@@ -79,7 +80,20 @@ export class InstallerService {
         // TODO typescriptize models/user.js and remove eslint-disable-next-line
         // eslint-disable-next-line @typescript-eslint/no-explicit-any
         const Page = mongoose.model('Page') as any;
-        await Page.updateMany({}, { createdAt: initialPagesCreatedAt, updatedAt: initialPagesCreatedAt });
+
+        // Increment timestamp to avoid difference for order in VRT
+        const pagePaths = ['/Sandbox', '/Sandbox/Bootstrap4', '/Sandbox/Diagrams', '/Sandbox/Math'];
+        const promises = pagePaths.map(async(path: string, idx: number) => {
+          const date = addSeconds(initialPagesCreatedAt, idx);
+          return Page.update(
+            { path },
+            {
+              createdAt: date,
+              updatedAt: date,
+            },
+          );
+        });
+        await Promise.all(promises);
       }
       catch (err) {
         logger.error('Failed to update createdAt', err);
@@ -145,7 +159,7 @@ export class InstallerService {
     const rootRevision = await Revision.findOne({ path: '/' });
     rootPage.creator = adminUser._id;
     rootPage.lastUpdateUser = adminUser._id;
-    rootRevision.creator = adminUser._id;
+    rootRevision.author = adminUser._id;
     await Promise.all([rootPage.save(), rootRevision.save()]);
 
     // create initial pages

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

@@ -21,6 +21,11 @@ context('Access to page', () => {
     // hide fab
     cy.getByTestid('grw-fab-container').invoke('attr', 'style', 'display: none');
 
+    // remove animation for screenshot
+    // remove 'blink' class because ::after element cannot be operated
+    // https://stackoverflow.com/questions/5041494/selecting-and-manipulating-css-pseudo-elements-such-as-before-and-after-usin/21709814#21709814
+    cy.get('#Headers').invoke('removeClass', 'blink');
+
     cy.screenshot(`${ssPrefix}-sandbox-headers`);
   });
 
@@ -28,6 +33,8 @@ context('Access to page', () => {
     cy.visit('/Sandbox/Math');
 
     cy.get('mjx-container').should('be.visible');
+    // eslint-disable-next-line cypress/no-unnecessary-waiting
+    cy.wait(2000); // wait for 2 seconds for MathJax.typesetPromise();
 
     cy.screenshot(`${ssPrefix}-sandbox-math`);
   });
@@ -59,6 +66,8 @@ context('Access to /me page', () => {
 
   it('/me is successfully loaded', () => {
     cy.visit('/me', {  });
+    // eslint-disable-next-line cypress/no-unnecessary-waiting
+    cy.wait(500); // wait loading image
     cy.screenshot(`${ssPrefix}-me`);
   });
 

+ 11 - 4
packages/app/test/cypress/integration/20-basic-features/access-to-pagelist.spec.ts

@@ -12,8 +12,7 @@ context('Access to pagelist', () => {
   it('Page list modal is successfully opened ', () => {
     cy.visit('/');
     cy.getByTestid('pageListButton').click({force: true});
-    cy.getByTestid('page-accessories-modal').parent().should('have.class','show');
-    cy.screenshot(`${ssPrefix}1-open-pagelist-modal`);
+    cy.getByTestid('page-accessories-modal').should('be.visible').screenshot(`${ssPrefix}1-open-pagelist-modal`);
   });
 
   it('Successfully duplicate a page from page list', () => {
@@ -21,8 +20,10 @@ context('Access to pagelist', () => {
     cy.getByTestid('pageListButton').click({force: true});
     cy.getByTestid('page-accessories-modal').parent().should('have.class','show').within(() => {
       cy.getByTestid('open-page-item-control-btn').first().click();
-      cy.screenshot(`${ssPrefix}2-click-on-three-dots-menu`);
-      cy.get('.dropdown-menu').should('have.class', 'show').first().within(() => {
+      cy.getByTestid('page-item-control-menu').should('have.class', 'show').first().within(() => {
+        // eslint-disable-next-line cypress/no-unnecessary-waiting
+        cy.wait(300);
+        cy.screenshot(`${ssPrefix}2-open-page-item-control-menu`);
         cy.getByTestid('open-page-duplicate-modal-btn').click();
       });
     });
@@ -50,6 +51,7 @@ context('Access to pagelist', () => {
     cy.getByTestid('page-accessories-modal').parent().should('have.class','show').within(() => {
       cy.get('button.close').eq(0).click();
     });
+
     cy.screenshot(`${ssPrefix}7-page-list-modal-size-fullscreen`, {capture: 'viewport'});
 
     cy.getByTestid('page-accessories-modal').parent().should('have.class','show').within(() => {
@@ -76,6 +78,8 @@ context('Access to timeline', () => {
     cy.getByTestid('page-accessories-modal').parent().should('have.class','show').within(() => {
       cy.get('.nav-title > li').eq(1).find('a').click();
     });
+    // eslint-disable-next-line cypress/no-unnecessary-waiting
+    cy.wait(500); // wait for loading wiki
     cy.screenshot(`${ssPrefix}1-timeline-list`, {capture: 'viewport'});
   });
 
@@ -86,6 +90,9 @@ context('Access to timeline', () => {
       cy.get('.nav-title > li').eq(1).find('a').click();
       cy.get('button.close').eq(0).click();
     });
+    cy.get('.modal').should('be.visible').scrollTo('top');
+    // eslint-disable-next-line cypress/no-unnecessary-waiting
+    cy.wait(500); // wait for loading wiki
     cy.screenshot(`${ssPrefix}2-timeline-list-fullscreen`, {capture: 'viewport'});
     cy.getByTestid('page-accessories-modal').parent().should('have.class','show').within(() => {
       cy.get('button.close').eq(1).click();

+ 82 - 10
packages/app/test/cypress/integration/20-basic-features/use-tools.spec.ts

@@ -9,6 +9,7 @@ context('Switch Sidebar content', () => {
   });
 
   it('PageTree is successfully shown', () => {
+    cy.collapseSidebar(false);
     cy.visit('/page');
     cy.getByTestid('grw-sidebar-nav-primary-page-tree').click();
     // eslint-disable-next-line cypress/no-unnecessary-waiting
@@ -28,20 +29,75 @@ context('Modal for page operation', () => {
     cy.fixture("user-admin.json").then(user => {
       cy.login(user.username, user.password);
     });
+    cy.collapseSidebar(true);
   });
+  it("PageCreateModal is shown and closed successfully", () => {
+    cy.visit('/');
+    cy.getByTestid('newPageBtn').click();
 
-  it("PageCreateModal is shown successfully", () => {
-    cy.visit('/me');
+    cy.getByTestid('page-create-modal').should('be.visible').within(() => {
+      cy.screenshot(`${ssPrefix}new-page-modal-opened`);
+      cy.get('button.close').click();
 
+    });
+    cy.screenshot(`${ssPrefix}page-create-modal-closed`, {capture: 'viewport'});
+  });
+  it("Successfully Create Today's page", () => {
+    const pageName = "Today's page";
+    cy.visit('/');
     cy.getByTestid('newPageBtn').click();
 
-    cy.getByTestid('page-create-modal').should('be.visible').screenshot(`${ssPrefix}-open`);
+    cy.getByTestid('page-create-modal').should('be.visible').within(() => {
+      cy.get('.page-today-input2').type(pageName);
+      cy.screenshot(`${ssPrefix}today-add-page-name`);
+      cy.getByTestid('btn-create-memo').click();
+    });
+    cy.getByTestid('page-editor').should('be.visible');
+    cy.getByTestid('save-page-btn').click();
+    cy.get('body').should('not.have.class', 'on-edit');
 
-    cy.getByTestid('row-create-page-under-below').find('input.form-control').clear().type('/new-page');
-    cy.getByTestid('btn-create-page-under-below').click();
+    cy.getByTestid('grw-contextual-sub-nav').should('be.visible');
+    cy.screenshot(`${ssPrefix}create-today-page`);
+  });
+  it('Successfully create page under specific path', () => {
+    const pageName = 'child';
 
+    cy.visit('/SandBox');
+    cy.getByTestid('newPageBtn').click();
+
+    cy.getByTestid('page-create-modal').should('be.visible').within(() => {
+      cy.get('.rbt-input-main').type(pageName);
+      cy.screenshot(`${ssPrefix}under-path-add-page-name`);
+      cy.getByTestid('btn-create-page-under-below').click();
+    });
     cy.getByTestid('page-editor').should('be.visible');
-    cy.screenshot(`${ssPrefix}-create-clicked`, {capture: 'viewport'});
+    cy.getByTestid('save-page-btn').click();
+    cy.get('body').should('not.have.class', 'on-edit');
+
+    cy.getByTestid('grw-contextual-sub-nav').should('be.visible');
+    cy.screenshot(`${ssPrefix}create-page-under-specific-page`);
+  });
+
+  it('Trying to create template page under the root page fail', () => {
+    cy.visit('/');
+    cy.getByTestid('newPageBtn').click();
+
+    cy.getByTestid('page-create-modal').should('be.visible').within(() => {
+      cy.get('#template-type').click();
+      cy.get('#template-type').next().find('button:eq(0)').click({force: true});
+      cy.get('#dd-template-type').next().find('button').click({force: true});
+    });
+    cy.get('.toast-error').should('be.visible').invoke('attr', 'style', 'opacity: 1');
+    cy.screenshot(`${ssPrefix}create-template-for-children-error`, {capture: 'viewport'});
+    cy.get('.toast-error').should('be.visible').click();
+
+    cy.getByTestid('page-create-modal').should('be.visible').within(() => {
+      cy.get('#template-type').click();
+      cy.get('#template-type').next().find('button:eq(1)').click({force: true});
+      cy.get('#dd-template-type').next().find('button').click({force: true});
+    });
+    cy.get('.toast-error').should('be.visible').invoke('attr', 'style', 'opacity: 1');
+    cy.screenshot(`${ssPrefix}create-template-for-descendants-error`, {capture: 'viewport'});
   });
 
   it('PageDeleteModal is shown successfully', () => {
@@ -89,6 +145,7 @@ context('Open presentation modal', () => {
     cy.fixture("user-admin.json").then(user => {
       cy.login(user.username, user.password);
     });
+    cy.collapseSidebar(true);
   });
 
   it('PresentationModal for "/" is shown successfully', () => {
@@ -115,6 +172,7 @@ context('Page Accessories Modal', () => {
     cy.fixture("user-admin.json").then(user => {
       cy.login(user.username, user.password);
     });
+    cy.collapseSidebar(true);
   });
 
   it('Page History is shown successfully', () => {
@@ -160,6 +218,7 @@ context('Tag Oprations', () =>{
     cy.fixture("user-admin.json").then(user => {
       cy.login(user.username, user.password);
     });
+    cy.collapseSidebar(true);
   });
 
   it('Successfully add new tag', () => {
@@ -188,8 +247,10 @@ context('Tag Oprations', () =>{
       cy.get('div.modal-footer > button').click();
     });
 
+    cy.get('.toast').should('be.visible').trigger('mouseover');
     cy.get('.grw-taglabels-container > form > a').contains(tag).should('exist');
-
+    /* eslint-disable cypress/no-unnecessary-waiting */
+    cy.wait(150); // wait for toastr to change its color occured by mouseover
     cy.screenshot(`${ssPrefix}4-click-done`, {capture: 'viewport'});
 
   });
@@ -203,9 +264,14 @@ context('Tag Oprations', () =>{
     cy.getByTestid('search-result-base').should('be.visible');
     cy.getByTestid('search-result-list').should('be.visible');
     cy.getByTestid('search-result-content').should('be.visible');
+    cy.get('#wiki').should('be.visible');
+    // force to add 'active' to pass VRT: https://github.com/weseek/growi/pull/6603
+    cy.getByTestid('page-list-item-L').first().invoke('addClass', 'active');
     cy.screenshot(`${ssPrefix}1-click-tag-name`, {capture: 'viewport'});
 
     cy.getByTestid('open-page-item-control-btn').first().click({force: true});
+    // eslint-disable-next-line cypress/no-unnecessary-waiting
+    cy.wait(1500); // for wait rendering pagelist info
     cy.screenshot(`${ssPrefix}2-click-three-dots-menu`, {capture: 'viewport'});
 
     cy.getByTestid('open-page-duplicate-modal-btn').first().click({force: true});
@@ -233,24 +299,29 @@ context('Tag Oprations', () =>{
     cy.getByTestid('search-result-base').should('be.visible');
     cy.getByTestid('search-result-list').should('be.visible');
     cy.getByTestid('search-result-content').should('be.visible');
+    cy.get('#wiki').should('be.visible');
     cy.screenshot(`${ssPrefix}1-click-tag-name`, {capture: 'viewport'});
 
     cy.getByTestid('search-result-list').within(() => {
       cy.get('.list-group-item').each(($row) => {
         if($row.find('a').text() === oldPageName){
           cy.wrap($row).within(() => {
-            cy.getByTestid('open-page-item-control-btn').click();
+            cy.getByTestid('open-page-item-control-btn').first().click();
+            cy.getByTestid('page-item-control-menu').should('have.class', 'show').first().within(() => {
+            // eslint-disable-next-line cypress/no-unnecessary-waiting
+            cy.wait(300);
+            cy.screenshot(`${ssPrefix}2-open-page-item-control-menu`);
+            })
           });
         }
       });
     });
-    cy.screenshot(`${ssPrefix}2-click-three-dots-menu`, {capture: 'viewport'});
 
     cy.getByTestid('search-result-list').within(() => {
       cy.get('.list-group-item').each(($row) => {
         if($row.find('a').text() === oldPageName){
           cy.wrap($row).within(() => {
-            cy.getByTestid('open-page-move-rename-modal-btn').click();
+            cy.getByTestid('open-page-move-rename-modal-btn').click({force: true});
           });
         }
       });
@@ -265,6 +336,7 @@ context('Tag Oprations', () =>{
     });
 
     cy.visit(`/${newPageName}`);
+    cy.getByTestid('grw-tag-labels').should('be.visible');
     cy.screenshot(`${ssPrefix}4-new-page-name-applied`, {capture: 'viewport'});
   });
 

+ 14 - 10
packages/app/test/cypress/integration/21-basic-features-for-guest/access-to-page.spec.ts

@@ -1,18 +1,18 @@
 context('Access to page by guest', () => {
   const ssPrefix = 'access-to-page-by-guest-';
 
-  beforeEach(() => {
-    // collapse sidebar
-    cy.collapseSidebar(true);
-  });
-
   it('/Sandbox is successfully loaded', () => {
     cy.visit('/Sandbox', {  });
+    cy.collapseSidebar(true, true);
     cy.screenshot(`${ssPrefix}-sandbox`);
   });
 
   it('/Sandbox with anchor hash is successfully loaded', () => {
     cy.visit('/Sandbox#Headers');
+    cy.collapseSidebar(true, true);
+
+    // eslint-disable-next-line cypress/no-unnecessary-waiting
+    cy.wait(500);
 
     // hide fab
     cy.getByTestid('grw-fab-container').invoke('attr', 'style', 'display: none');
@@ -22,14 +22,22 @@ context('Access to page by guest', () => {
 
   it('/Sandbox/Math is successfully loaded', () => {
     cy.visit('/Sandbox/Math');
+    cy.collapseSidebar(true, true);
 
     cy.get('mjx-container').should('be.visible');
+    // eslint-disable-next-line cypress/no-unnecessary-waiting
+    cy.wait(2000); // wait for 2 seconds for MathJax.typesetPromise();
 
     cy.screenshot(`${ssPrefix}-sandbox-math`);
   });
 
   it('/Sandbox with edit is successfully loaded', () => {
     cy.visit('/Sandbox#edit');
+    cy.collapseSidebar(true, true);
+
+    // eslint-disable-next-line cypress/no-unnecessary-waiting
+    cy.wait(1000);
+
     cy.screenshot(`${ssPrefix}-sandbox-edit-page`);
   })
 
@@ -55,13 +63,9 @@ context('Access to /me page', () => {
 context('Access to special pages by guest', () => {
   const ssPrefix = 'access-to-special-pages-by-guest-';
 
-  beforeEach(() => {
-    // collapse sidebar
-    cy.collapseSidebar(true);
-  });
-
   it('/trash is successfully loaded', () => {
     cy.visit('/trash', {  });
+    cy.collapseSidebar(true, true);
     cy.getByTestid('trash-page-list').should('be.visible');
     cy.screenshot(`${ssPrefix}-trash`);
   });

+ 53 - 12
packages/app/test/cypress/integration/30-search/search.spec.ts

@@ -16,8 +16,10 @@ context('Access to search result page', () => {
     cy.getByTestid('search-result-base').should('be.visible');
     cy.getByTestid('search-result-list').should('be.visible');
     cy.getByTestid('search-result-content').should('be.visible');
-
-    cy.screenshot(`${ssPrefix}-with-q`);
+    cy.get('#wiki').should('be.visible');
+    // for avoid mismatch by auto scrolling
+    cy.get('.search-result-content-body-container').scrollTo('top');
+    cy.screenshot(`${ssPrefix}with-q`);
   });
 
   it('checkboxes behaviors', () => {
@@ -26,27 +28,31 @@ context('Access to search result page', () => {
     cy.getByTestid('search-result-base').should('be.visible');
     cy.getByTestid('search-result-list').should('be.visible');
     cy.getByTestid('search-result-content').should('be.visible');
+    cy.get('#wiki').should('be.visible');
+    // for avoid mismatch by auto scrolling
+    cy.get('.search-result-content-body-container').scrollTo('top');
+
+    // force to add 'active' to pass VRT: https://github.com/weseek/growi/pull/6603
+    cy.getByTestid('page-list-item-L').first().invoke('addClass', 'active');
 
     cy.getByTestid('cb-select').first().click({force: true});
-    cy.screenshot(`${ssPrefix}-the-first-checkbox-on`);
+    cy.screenshot(`${ssPrefix}the-first-checkbox-on`);
     cy.getByTestid('cb-select').first().click({force: true});
-    cy.screenshot(`${ssPrefix}-the-first-checkbox-off`);
+    cy.screenshot(`${ssPrefix}the-first-checkbox-off`);
 
     // click select all checkbox
     cy.getByTestid('cb-select-all').click({force: true});
-    cy.screenshot(`${ssPrefix}-the-select-all-checkbox-1`);
+    cy.screenshot(`${ssPrefix}the-select-all-checkbox-1`);
     cy.getByTestid('cb-select').first().click({force: true});
-    cy.screenshot(`${ssPrefix}-the-select-all-checkbox-2`);
+    cy.screenshot(`${ssPrefix}the-select-all-checkbox-2`);
     cy.getByTestid('cb-select').first().click({force: true});
-    cy.screenshot(`${ssPrefix}-the-select-all-checkbox-3`);
+    cy.screenshot(`${ssPrefix}the-select-all-checkbox-3`);
     cy.getByTestid('cb-select-all').click({force: true});
-    cy.screenshot(`${ssPrefix}-the-select-all-checkbox-4`);
+    cy.screenshot(`${ssPrefix}the-select-all-checkbox-4`);
   });
 
 });
 
-
-
 context('Access to legacy private pages', () => {
   const ssPrefix = 'access-to-legacy-private-pages-directly-';
 
@@ -63,8 +69,9 @@ context('Access to legacy private pages', () => {
     cy.visit('/_private-legacy-pages');
 
     cy.getByTestid('search-result-base').should('be.visible');
+    cy.getByTestid('search-result-private-legacy-pages').should('be.visible');
 
-    cy.screenshot(`${ssPrefix}-shown`);
+    cy.screenshot(`${ssPrefix}shown`);
   });
 
 });
@@ -98,9 +105,20 @@ context('Search all pages', () => {
     cy.getByTestid('search-result-base').should('be.visible');
     cy.getByTestid('search-result-list').should('be.visible');
     cy.getByTestid('search-result-content').should('be.visible');
+    cy.get('#wiki').should('be.visible');
+    // force to add 'active' to pass VRT: https://github.com/weseek/growi/pull/6603
+    cy.getByTestid('page-list-item-L').first().invoke('addClass', 'active');
+    // for avoid mismatch by auto scrolling
+    cy.get('.search-result-content-body-container').scrollTo('top');
+    // eslint-disable-next-line cypress/no-unnecessary-waiting
+    cy.wait(1500);
     cy.screenshot(`${ssPrefix}3-search-page-results`, { capture: 'viewport'});
 
     cy.getByTestid('open-page-item-control-btn').eq(1).click();
+    cy.getByTestid('search-result-content').should('be.visible');
+    cy.get('#wiki').should('be.visible');
+    // for avoid mismatch by auto scrolling
+    cy.get('.search-result-content-body-container').scrollTo('top');
     cy.screenshot(`${ssPrefix}4-click-three-dots-menu`, {capture: 'viewport'});
 
     //Add bookmark
@@ -158,12 +176,18 @@ context('Search all pages', () => {
     cy.getByTestid('search-result-base').should('be.visible');
     cy.getByTestid('search-result-list').should('be.visible');
     cy.getByTestid('search-result-content').should('be.visible');
-
+    cy.get('#wiki').should('be.visible');
+    // force to add 'active' to pass VRT: https://github.com/weseek/growi/pull/6603
+    cy.getByTestid('page-list-item-L').first().invoke('addClass', 'active');
     cy.screenshot(`${ssPrefix}2-search-with-tag-result`, {capture: 'viewport'});
+
     cy.getByTestid('open-page-item-control-btn').first().click();
+    cy.getByTestid('search-result-content').should('be.visible');
+    cy.get('#wiki').should('be.visible');
     cy.screenshot(`${ssPrefix}3-click-three-dots-menu-search-with-tag`, {capture: 'viewport'});
 
   });
+
   it('Successfully order page search results by tag', () => {
     const tag = 'help';
 
@@ -172,6 +196,9 @@ context('Search all pages', () => {
     cy.getByTestid('search-result-base').should('be.visible');
     cy.getByTestid('search-result-list').should('be.visible');
     cy.getByTestid('search-result-content').should('be.visible');
+    cy.get('#wiki').should('be.visible');
+    // force to add 'active' to pass VRT: https://github.com/weseek/growi/pull/6603
+    cy.getByTestid('page-list-item-L').first().invoke('addClass', 'active');
     cy.screenshot(`${ssPrefix}1-tag-order-click-tag-name`, {capture: 'viewport'});
 
     cy.get('.grw-search-page-nav').within(() => {
@@ -182,6 +209,7 @@ context('Search all pages', () => {
     cy.getByTestid('search-result-base').should('be.visible');
     cy.getByTestid('search-result-list').should('be.visible');
     cy.getByTestid('search-result-content').should('be.visible');
+    cy.get('#wiki').should('be.visible');
     cy.screenshot(`${ssPrefix}2-tag-order-by-relevance`);
 
     cy.get('.grw-search-page-nav').within(() => {
@@ -192,6 +220,7 @@ context('Search all pages', () => {
     cy.getByTestid('search-result-base').should('be.visible');
     cy.getByTestid('search-result-list').should('be.visible');
     cy.getByTestid('search-result-content').should('be.visible');
+    cy.get('#wiki').should('be.visible');
     cy.screenshot(`${ssPrefix}3-tag-order-by-creation-date`);
 
     cy.get('.grw-search-page-nav').within(() => {
@@ -202,6 +231,7 @@ context('Search all pages', () => {
     cy.getByTestid('search-result-base').should('be.visible');
     cy.getByTestid('search-result-list').should('be.visible');
     cy.getByTestid('search-result-content').should('be.visible');
+    cy.get('#wiki').should('be.visible');
     cy.screenshot(`${ssPrefix}4-tag-order-by-last-update-date`);
   });
 
@@ -235,9 +265,20 @@ context('Search current tree with "prefix":', () => {
     cy.getByTestid('search-result-base').should('be.visible');
     cy.getByTestid('search-result-list').should('be.visible');
     cy.getByTestid('search-result-content').should('be.visible');
+    cy.get('#wiki').should('be.visible');
+    // force to add 'active' to pass VRT: https://github.com/weseek/growi/pull/6603
+    cy.getByTestid('page-list-item-L').first().invoke('addClass', 'active');
+    // for avoid mismatch by auto scrolling
+    cy.get('.search-result-content-body-container').scrollTo('top');
+    // eslint-disable-next-line cypress/no-unnecessary-waiting
+    cy.wait(1500);
     cy.screenshot(`${ssPrefix}3-search-page-results`, { capture: 'viewport'});
 
     cy.getByTestid('open-page-item-control-btn').first().click();
+    cy.getByTestid('search-result-content').should('be.visible');
+    cy.get('#wiki').should('be.visible');
+    // for avoid mismatch by auto scrolling
+    cy.get('.search-result-content-body-container').scrollTo('top');
     cy.screenshot(`${ssPrefix}4-click-three-dots-menu`, {capture: 'viewport'});
   });
 

+ 2 - 0
packages/app/test/cypress/integration/40-admin/access-to-admin-page.spec.ts

@@ -51,6 +51,8 @@ context('Access to Admin page', () => {
   it('/admin/customize is successfully loaded', () => {
     cy.visit('/admin/customize');
     cy.getByTestid('admin-customize').should('be.visible');
+    /* eslint-disable cypress/no-unnecessary-waiting */
+    cy.wait(500); // wait for loading layout image
     cy.screenshot(`${ssPrefix}-admin-customize`);
   });
 

+ 21 - 3
packages/app/test/cypress/integration/50-sidebar/access-to-side-bar.spec.ts

@@ -27,13 +27,20 @@ context('Access to sidebar', () => {
     });
 
     cy.getByTestid('grw-recent-changes').should('be.visible');
+    cy.get('.list-group-item').should('be.visible');
 
-    cy.getByTestid('grw-contextual-navigation-sub').screenshot(`${ssPrefix}recent-changes-1-page-list`);
+    // Avoid blackout misalignment
+    cy.scrollTo('center');
+    cy.screenshot(`${ssPrefix}recent-changes-1-page-list`);
 
     cy.get('#grw-sidebar-contents-wrapper').within(() => {
       cy.get('#recentChangesResize').click({force: true});
-      cy.screenshot(`${ssPrefix}recent-changes-2-switch-sidebar-size`);
+      cy.get('.list-group-item').should('be.visible');
     });
+
+    // Avoid blackout misalignment
+    cy.scrollTo('center');
+    cy.screenshot(`${ssPrefix}recent-changes-2-switch-sidebar-size`);
   });
 
   it('Successfully create a custom sidebar page', () => {
@@ -52,8 +59,17 @@ context('Access to sidebar', () => {
     cy.get('.grw-sidebar-content-header.h5').find('a').click();
     cy.get('.CodeMirror textarea').type(content, {force: true});
     cy.screenshot(`${ssPrefix}custom-sidebar-2-custom-sidebar-editor`);
-    cy.get('.dropup > .btn-submit').click();
+    cy.getByTestid('save-page-btn').click();
     cy.get('body').should('not.have.class', 'on-edit');
+
+    // What to do when UserUISettings is not saved in time
+    cy.getByTestid('grw-sidebar-nav-primary-custom-sidebar').then(($el) => {
+      if (!$el.hasClass('active')) {
+        cy.wrap($el).click();
+      }
+    });
+
+    cy.get('.grw-custom-sidebar-content').should('be.visible');
     cy.getByTestid('grw-contextual-navigation-sub').screenshot(`${ssPrefix}custom-sidebar-3-custom-sidebar-created`);
   });
 
@@ -142,6 +158,7 @@ context('Access to sidebar', () => {
 
   it('Successfully access to My Drafts page', () => {
     cy.visit('/');
+    cy.collapseSidebar(true);
     cy.get('.grw-sidebar-nav-secondary-container').within(() => {
       cy.get('a[href*="/me/drafts"]').click();
     });
@@ -159,6 +176,7 @@ context('Access to sidebar', () => {
 
   it('Successfully access to trash page', () => {
     cy.visit('/');
+    cy.collapseSidebar(true);
     cy.get('.grw-sidebar-nav-secondary-container').within(() => {
       cy.get('a[href*="/trash"]').click();
     });

+ 7 - 2
packages/app/test/cypress/integration/50-sidebar/switching-sidebar-mode.spec.ts

@@ -21,13 +21,18 @@ context('Switch sidebar mode', () => {
 
   it('Switching sidebar mode', () => {
     cy.visit('/');
+    cy.collapseSidebar(true, true)
     cy.get('.grw-personal-dropdown').first().click();
 
     cy.get('[for="swSidebarMode"]').click({force: true});
-    cy.screenshot(`${ssPrefix}-switch-sidebar-mode`, { capture: 'viewport' });
+    cy.screenshot(`${ssPrefix}-switch-sidebar-mode`, {
+      blackout: ['#revision-toc', '[data-hide-in-vrt=true]'],
+    })
 
     cy.get('[for="swSidebarMode"]').click({force: true});
-    cy.screenshot(`${ssPrefix}-switch-sidebar-mode-back`, { capture: 'viewport' });
+    cy.screenshot(`${ssPrefix}-switch-sidebar-mode-back`, {
+      blackout: ['#revision-toc', '[data-hide-in-vrt=true]'],
+    })
   });
 
 });

+ 2 - 2
packages/app/test/cypress/support/commands.ts

@@ -40,9 +40,9 @@ Cypress.Commands.add('login', (username, password) => {
 
 let isSidebarCollapsed: boolean | undefined;
 
-Cypress.Commands.add('collapseSidebar', (isCollapsed) => {
+Cypress.Commands.add('collapseSidebar', (isCollapsed, force=false) => {
 
-  if (isSidebarCollapsed === isCollapsed) {
+  if (!force && isSidebarCollapsed === isCollapsed) {
     return;
   }
 

+ 1 - 1
packages/app/test/cypress/support/index.ts

@@ -36,7 +36,7 @@ declare global {
     interface Chainable {
        getByTestid(selector: string, options?: Partial<Loggable & Timeoutable & Withinable & Shadow>): Chainable<JQuery<Element>>,
        login(username: string, password: string): Chainable<void>,
-       collapseSidebar(isCollapsed: boolean): Chainable<void>,
+       collapseSidebar(isCollapsed: boolean, force?: boolean): Chainable<void>,
     }
   }
 }

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

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

+ 1 - 1
packages/core/package.json

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

+ 1 - 1
packages/plugin-attachment-refs/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/plugin-attachment-refs",
-  "version": "5.1.4",
+  "version": "5.1.5-RC.0",
   "description": "GROWI Plugin to add ref/refimg/refs/refsimg tags",
   "license": "MIT",
   "keywords": [

+ 1 - 1
packages/plugin-lsx/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/plugin-lsx",
-  "version": "5.1.4",
+  "version": "5.1.5-RC.0",
   "description": "GROWI plugin to list pages",
   "license": "MIT",
   "keywords": [

+ 1 - 1
packages/plugin-pukiwiki-like-linker/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/plugin-pukiwiki-like-linker",
-  "version": "5.1.4",
+  "version": "5.1.5-RC.0",
   "description": "GROWI plugin to add PukiwikiLikeLinker",
   "license": "MIT",
   "keywords": [

+ 1 - 1
packages/slack/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/slack",
-  "version": "5.1.4",
+  "version": "5.1.5-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": "5.1.4",
+  "version": "5.1.5-slackbot-proxy.0",
   "license": "MIT",
   "scripts": {
     "build": "yarn tsc && tsc-alias -p tsconfig.build.json",
@@ -25,7 +25,7 @@
   },
   "dependencies": {
     "@godaddy/terminus": "^4.9.0",
-    "@growi/slack": "^5.1.4",
+    "@growi/slack": "^5.1.5-RC.0",
     "@slack/oauth": "^2.0.1",
     "@slack/web-api": "^6.2.4",
     "@tsed/common": "^6.43.0",

+ 1 - 1
packages/ui/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/ui",
-  "version": "5.1.4",
+  "version": "5.1.5-RC.0",
   "description": "GROWI UI Libraries",
   "license": "MIT",
   "keywords": [