Przeglądaj źródła

Merge branch 'master' into imprv/enable-browsing-video

Ryoji Shimizu 3 lat temu
rodzic
commit
af3d040106
58 zmienionych plików z 612 dodań i 1673 usunięć
  1. 45 1
      CHANGELOG.md
  2. 1 1
      lerna.json
  3. 1 1
      package.json
  4. 1 1
      packages/app/docker/README.md
  5. 13 13
      packages/app/package.json
  6. 5 8
      packages/app/resource/locales/en_US/sandbox.md
  7. 5 8
      packages/app/resource/locales/ja_JP/sandbox.md
  8. 5 8
      packages/app/resource/locales/zh_CN/sandbox.md
  9. 37 0
      packages/app/src/client/services/side-effects/use-sticky.ts
  10. 1 1
      packages/app/src/components/Admin/ExportArchiveDataPage.tsx
  11. 31 41
      packages/app/src/components/Fab.tsx
  12. 2 2
      packages/app/src/components/Navbar/GrowiContextualSubNavigation.tsx
  13. 5 22
      packages/app/src/components/Navbar/GrowiSubNavigationSwitcher.tsx
  14. 1 1
      packages/app/src/components/PageAccessoriesModal.tsx
  15. 5 4
      packages/app/src/components/PageComment/CommentEditor.tsx
  16. 2 2
      packages/app/src/components/PageEditor.tsx
  17. 1 0
      packages/app/src/components/PageEditor/CodeMirrorEditor.jsx
  18. 0 1
      packages/app/src/components/PageEditor/CodeMirrorEditor.module.scss
  19. 15 12
      packages/app/src/components/PageEditor/CommentMentionHelper.ts
  20. 2 12
      packages/app/src/components/PageEditor/DrawioCommunicationHelper.ts
  21. 3 2
      packages/app/src/components/PageEditor/DrawioModal.tsx
  22. 6 5
      packages/app/src/components/PageSideContents.tsx
  23. 2 2
      packages/app/src/components/ReactMarkdownComponents/DrawioViewerWithEditButton.tsx
  24. 5 1
      packages/app/src/components/Script/DrawioViewerScript.tsx
  25. 35 37
      packages/app/src/components/Sidebar/PageTree/Item.tsx
  26. 0 3
      packages/app/src/components/Sidebar/PageTree/ItemsTree.tsx
  27. 7 21
      packages/app/src/components/StickyStretchableScroller.tsx
  28. 13 5
      packages/app/src/pages/[[...path]].page.tsx
  29. 22 5
      packages/app/src/server/routes/apiv3/pages.js
  30. 1 1
      packages/app/src/server/service/page.ts
  31. 5 5
      packages/app/src/server/service/search-delegator/elasticsearch.ts
  32. 5 5
      packages/app/src/server/service/slack-command-handler/create-page-service.js
  33. 3 4
      packages/app/src/server/service/slack-command-handler/keep.js
  34. 3 1
      packages/app/src/server/service/slack-command-handler/note.js
  35. 0 20
      packages/app/src/services/renderer/remark-plugins/table.ts
  36. 6 8
      packages/app/src/services/renderer/renderer.tsx
  37. 0 8
      packages/app/src/stores/context.tsx
  38. 8 0
      packages/app/src/stores/page.tsx
  39. 1 1
      packages/app/src/styles/bootstrap/_override.scss
  40. 117 51
      packages/app/test/cypress/integration/20-basic-features/20-basic-features--access-to-page.spec.ts
  41. 61 0
      packages/app/test/cypress/integration/20-basic-features/20-basic-features--username-mention.spec.ts
  42. 1 1
      packages/codemirror-textlint/package.json
  43. 1 1
      packages/core/package.json
  44. 1 1
      packages/hackmd/package.json
  45. 3 3
      packages/presentation/package.json
  46. 1 1
      packages/preset-themes/package.json
  47. 2 3
      packages/remark-drawio/package.json
  48. 3 1
      packages/remark-drawio/src/components/DrawioViewer.tsx
  49. 1 9
      packages/remark-drawio/src/services/renderer/remark-drawio.ts
  50. 16 31
      packages/remark-drawio/src/utils/embed.ts
  51. 1 4
      packages/remark-growi-directive/package.json
  52. 4 4
      packages/remark-lsx/package.json
  53. 5 2
      packages/remark-lsx/src/server/routes/lsx.js
  54. 2 11
      packages/remark-lsx/src/services/renderer/lsx.ts
  55. 1 1
      packages/slack/package.json
  56. 2 2
      packages/slackbot-proxy/package.json
  57. 2 2
      packages/ui/package.json
  58. 85 1272
      yarn.lock

+ 45 - 1
CHANGELOG.md

@@ -1,9 +1,53 @@
 # Changelog
 # Changelog
 
 
-## [Unreleased](https://github.com/weseek/growi/compare/v6.0.10...HEAD)
+## [Unreleased](https://github.com/weseek/growi/compare/v6.0.15...HEAD)
 
 
 *Please do not manually update this file. We've automated the process.*
 *Please do not manually update this file. We've automated the process.*
 
 
+## [v6.0.15](https://github.com/weseek/growi/compare/v6.0.14...v6.0.15) - 2023-04-10
+
+### 🐛 Bug Fixes
+
+- fix: Templates are not applied when pages are created from PageTree (#7553) @miya
+- fix(drawio): Render uncompressed data when line-breaks option is set (#7555) @yuki-takei
+- fix: Is not working i18n on `/admin/export`  (#7554) @jam411
+- fix: PageTree mutiple DnD unexpected disappear and unexpected tree move if renamed path (#7542) @jam411
+- fix: Username incremental search is not working in comment (#7548) @mudana-grune
+- fix: H6 line-height (#7541) @yukendev
+
+## [v6.0.14](https://github.com/weseek/growi/compare/v6.0.13...v6.0.14) - 2023-04-04
+
+### 🐛 Bug Fixes
+
+- fix(drawio): Set compressXml option (#7536) @yuki-takei
+- fix(drawio): Rendering uncompressed data (#7537) @yuki-takei
+
+## [v6.0.13](https://github.com/weseek/growi/compare/v6.0.12...v6.0.13) - 2023-04-03
+
+### 🐛 Bug Fixes
+
+- fix: The "note" and "keep" commands of the GROWI bot are not functioning (#7529) @miya
+- fix: The "search" command of the GROWI bot is not functioning (#7525) @miya
+- fix: Lsx filter and except option do not work when the path includes special characters (#7523) @yuki-takei
+
+## [v6.0.12](https://github.com/weseek/growi/compare/v6.0.11...v6.0.12) - 2023-03-30
+
+### 🐛 Bug Fixes
+
+- fix: DrawioViewer script URL (#7518) @yuki-takei
+
+## [v6.0.11](https://github.com/weseek/growi/compare/v6.0.10...v6.0.11) - 2023-03-29
+
+### 🚀 Improvement
+
+- imprv: ToC placeholder (#7506) @yuki-takei
+
+### 🐛 Bug Fixes
+
+- fix: Support draw.io v21.1.0 (support both of compressed/uncompressed data) (#7515) @yuki-takei
+- fix: The same level template page is being applied to lower level pages unintentionally (#7510) @miya
+- fix: Supress `activeTab` prop type error (#7504) @jam411
+
 ## [v6.0.10](https://github.com/weseek/growi/compare/v6.0.9...v6.0.10) - 2023-03-23
 ## [v6.0.10](https://github.com/weseek/growi/compare/v6.0.9...v6.0.10) - 2023-03-23
 
 
 ### 🚀 Improvement
 ### 🚀 Improvement

+ 1 - 1
lerna.json

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

+ 1 - 1
package.json

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

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

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

+ 13 - 13
packages/app/package.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "@growi/app",
   "name": "@growi/app",
-  "version": "6.0.10",
+  "version": "6.0.16-RC.0",
   "license": "MIT",
   "license": "MIT",
   "scripts": {
   "scripts": {
     "//// for production": "",
     "//// for production": "",
@@ -66,14 +66,14 @@
     "@elastic/elasticsearch7": "npm:@elastic/elasticsearch@^7.17.0",
     "@elastic/elasticsearch7": "npm:@elastic/elasticsearch@^7.17.0",
     "@godaddy/terminus": "^4.9.0",
     "@godaddy/terminus": "^4.9.0",
     "@google-cloud/storage": "^5.8.5",
     "@google-cloud/storage": "^5.8.5",
-    "@growi/codemirror-textlint": "^6.0.10",
-    "@growi/core": "^6.0.10",
-    "@growi/hackmd": "^6.0.10",
-    "@growi/preset-themes": "^6.0.10",
-    "@growi/remark-drawio": "^6.0.10",
-    "@growi/remark-growi-directive": "^6.0.10",
-    "@growi/remark-lsx": "^6.0.10",
-    "@growi/slack": "^6.0.10",
+    "@growi/codemirror-textlint": "^6.0.16-RC.0",
+    "@growi/core": "^6.0.16-RC.0",
+    "@growi/hackmd": "^6.0.16-RC.0",
+    "@growi/preset-themes": "^6.0.16-RC.0",
+    "@growi/remark-drawio": "^6.0.16-RC.0",
+    "@growi/remark-growi-directive": "^6.0.16-RC.0",
+    "@growi/remark-lsx": "^6.0.16-RC.0",
+    "@growi/slack": "^6.0.16-RC.0",
     "@promster/express": "^7.0.6",
     "@promster/express": "^7.0.6",
     "@promster/server": "^7.0.8",
     "@promster/server": "^7.0.8",
     "@slack/web-api": "^6.2.4",
     "@slack/web-api": "^6.2.4",
@@ -163,7 +163,7 @@
     "react-error-boundary": "^3.1.4",
     "react-error-boundary": "^3.1.4",
     "react-i18next": "^12.2.0",
     "react-i18next": "^12.2.0",
     "react-image-crop": "^8.3.0",
     "react-image-crop": "^8.3.0",
-    "react-markdown": "^8.0.3",
+    "react-markdown": "^8.0.7",
     "react-multiline-clamp": "^2.0.0",
     "react-multiline-clamp": "^2.0.0",
     "react-scroll": "^1.8.7",
     "react-scroll": "^1.8.7",
     "react-syntax-highlighter": "^15.5.0",
     "react-syntax-highlighter": "^15.5.0",
@@ -181,6 +181,7 @@
     "remark-emoji": "^3.0.2",
     "remark-emoji": "^3.0.2",
     "remark-gfm": "^3.0.1",
     "remark-gfm": "^3.0.1",
     "remark-math": "^5.1.1",
     "remark-math": "^5.1.1",
+    "remark-toc": "^8.0.1",
     "remark-wiki-link": "^1.0.4",
     "remark-wiki-link": "^1.0.4",
     "socket.io": "^4.2.0",
     "socket.io": "^4.2.0",
     "stream-to-promise": "^3.0.0",
     "stream-to-promise": "^3.0.0",
@@ -205,8 +206,8 @@
     "handsontable": "v7.0.0 or above is no loger MIT lisence."
     "handsontable": "v7.0.0 or above is no loger MIT lisence."
   },
   },
   "devDependencies": {
   "devDependencies": {
-    "@growi/presentation": "^6.0.10",
-    "@growi/ui": "^6.0.10",
+    "@growi/presentation": "^6.0.16-RC.0",
+    "@growi/ui": "^6.0.16-RC.0",
     "@handsontable/react": "=2.1.0",
     "@handsontable/react": "=2.1.0",
     "@icon/themify-icons": "1.0.1-alpha.3",
     "@icon/themify-icons": "1.0.1-alpha.3",
     "@next/bundle-analyzer": "^13.2.3",
     "@next/bundle-analyzer": "^13.2.3",
@@ -246,7 +247,6 @@
     "simple-load-script": "^1.0.2",
     "simple-load-script": "^1.0.2",
     "simplebar-react": "^2.3.6",
     "simplebar-react": "^2.3.6",
     "socket.io-client": "^4.2.0",
     "socket.io-client": "^4.2.0",
-    "sticky-events": "^3.4.11",
     "swagger2openapi": "^5.3.1",
     "swagger2openapi": "^5.3.1",
     "ts-node-dev": "^2.0.0",
     "ts-node-dev": "^2.0.0",
     "tsc-alias": "^1.2.9"
     "tsc-alias": "^1.2.9"

+ 5 - 8
packages/app/resource/locales/en_US/sandbox.md

@@ -1,16 +1,13 @@
-<div class="card">
-  <div class="card-body">
+# :memo: Table of Contents
 
 
-# Table of Contents
+Add `ToC` after some `#` signs.
+`Table of Contents` or `Table-of-Contents` is also OK.
 
 
 ```
 ```
-@[toc]
+# ToC
 ```
 ```
 
 
-@[toc]
-
-  </div>
-</div>
+## ToC
 
 
 # :memo: Block Elements
 # :memo: Block Elements
 
 

+ 5 - 8
packages/app/resource/locales/ja_JP/sandbox.md

@@ -1,16 +1,13 @@
-<div class="card">
-  <div class="card-body">
+# :memo: 目次
 
 
-# 目次
+いくつかの `#` 記号に続けて `ToC` という文字列を記述します。  
+`Table of Contents` または `Table-of-Contents` でも構いません。
 
 
 ```
 ```
-@[toc]
+# ToC
 ```
 ```
 
 
-@[toc]
-
-  </div>
-</div>
+## ToC
 
 
 # :memo: Block Elements
 # :memo: Block Elements
 
 

+ 5 - 8
packages/app/resource/locales/zh_CN/sandbox.md

@@ -1,16 +1,13 @@
-<div class="card">
-  <div class="card-body">
+# :memo: Table of Contents
 
 
-# Table of Contents
+Add `ToC` after some `#` signs.
+`Table of Contents` or `Table-of-Contents` is also OK.
 
 
 ```
 ```
-@[toc]
+# ToC
 ```
 ```
 
 
-@[toc]
-
-  </div>
-</div>
+## ToC
 
 
 # :memo: Block Elements
 # :memo: Block Elements
 
 

+ 37 - 0
packages/app/src/client/services/side-effects/use-sticky.ts

@@ -0,0 +1,37 @@
+import { useState, useEffect } from 'react';
+
+// Custom hook that accepts a selector string as an argument
+// and returns a boolean indicating whether the selected element is currently sticky.
+export const useSticky = (selector: string): boolean => {
+  const [isSticky, setIsSticky] = useState<boolean>(false);
+
+  useEffect(() => {
+    // Get element to observe
+    const stickyElement = document.querySelector(selector);
+    // Updates the sticky status based on the current position of the observed element.
+    const observe = () => {
+      // If the observed element is empty or not an instance of Element, return early.
+      if (stickyElement == null || !(stickyElement instanceof Element)) return;
+
+      // Calculate the element's offset from the top of the viewport and the value of its "top" CSS property.
+      const elemOffset = stickyElement.getBoundingClientRect().top;
+      const stickyOffset = parseInt(window.getComputedStyle(stickyElement).top);
+
+      // Update the sticky status based on whether the element's offset is less than or equal to the sticky offset.
+      setIsSticky(elemOffset <= stickyOffset);
+    };
+    // Call the observe function immediately and add it as a listener for scroll and resize events.
+    observe();
+    document.addEventListener('scroll', observe);
+    window.addEventListener('resize', observe);
+
+    // Remove the scroll and resize event listeners when the component unmounts or the selector value changes.
+    return () => {
+      document.removeEventListener('scroll', observe);
+      window.removeEventListener('resize', observe);
+    };
+  }, [selector]);
+
+  // Return the current sticky status.
+  return isSticky;
+};

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

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

+ 31 - 41
packages/app/src/components/Fab.tsx

@@ -4,8 +4,8 @@ import React, {
 
 
 import { animateScroll } from 'react-scroll';
 import { animateScroll } from 'react-scroll';
 import { useRipple } from 'react-use-ripple';
 import { useRipple } from 'react-use-ripple';
-import StickyEvents from 'sticky-events';
 
 
+import { useSticky } from '~/client/services/side-effects/use-sticky';
 import { usePageCreateModal } from '~/stores/modal';
 import { usePageCreateModal } from '~/stores/modal';
 import { useCurrentPagePath } from '~/stores/page';
 import { useCurrentPagePath } from '~/stores/page';
 import { useIsAbleToChangeEditorMode } from '~/stores/ui';
 import { useIsAbleToChangeEditorMode } from '~/stores/ui';
@@ -26,55 +26,45 @@ export const Fab = (): JSX.Element => {
 
 
   const [animateClasses, setAnimateClasses] = useState<string>('invisible');
   const [animateClasses, setAnimateClasses] = useState<string>('invisible');
   const [buttonClasses, setButtonClasses] = useState<string>('');
   const [buttonClasses, setButtonClasses] = useState<string>('');
-  const [isSticky, setIsSticky] = useState<boolean>(false);
+  const [isStickyApplied, setIsStickyApplied] = useState(false);
 
 
   // ripple
   // ripple
   const createBtnRef = useRef(null);
   const createBtnRef = useRef(null);
   useRipple(createBtnRef, { rippleColor: 'rgba(255, 255, 255, 0.3)' });
   useRipple(createBtnRef, { rippleColor: 'rgba(255, 255, 255, 0.3)' });
 
 
-  /**
-   * After the fade animation is finished, fix the button display status.
-   * Prevents the fade animation occurred each time by button components rendered.
-   * Check Fab.module.scss for fade animation time.
-   */
+  // Get sticky status
+  const isSticky = useSticky('#grw-fav-sticky-trigger');
+
+  // check if isSticky is already initialized then save it in isStickyApplied state
   useEffect(() => {
   useEffect(() => {
-    const timer = setTimeout(() => {
-      if (isSticky) {
-        setAnimateClasses('visible');
-        setButtonClasses('');
-      }
-      else {
-        setAnimateClasses('invisible');
-      }
-    }, 500);
-    return () => clearTimeout(timer);
+    if (isSticky) {
+      setIsStickyApplied(true);
+    }
   }, [isSticky]);
   }, [isSticky]);
 
 
-  const stickyChangeHandler = useCallback((event) => {
-    logger.debug('StickyEvents.CHANGE detected');
-
-    const newAnimateClasses = event.detail.isSticky ? 'animated fadeInUp faster' : 'animated fadeOut faster';
-    const newButtonClasses = event.detail.isSticky ? '' : 'disabled grw-pointer-events-none';
-
-    setAnimateClasses(newAnimateClasses);
-    setButtonClasses(newButtonClasses);
-    setIsSticky(event.detail.isSticky);
-  }, []);
-
-  // setup effect by sticky event
+  // Apply new classes if only isSticky is initialized, otherwise no classes have changed
+  // Prevents the Fab button from showing on first load due to the isSticky effect
   useEffect(() => {
   useEffect(() => {
-    // sticky
-    // See: https://github.com/ryanwalters/sticky-events
-    const stickyEvents = new StickyEvents({ stickySelector: '#grw-fav-sticky-trigger' });
-    const { stickySelector } = stickyEvents;
-    const elem = document.querySelector(stickySelector);
-    elem.addEventListener(StickyEvents.CHANGE, stickyChangeHandler);
-
-    // return clean up handler
-    return () => {
-      elem.removeEventListener(StickyEvents.CHANGE, stickyChangeHandler);
-    };
-  }, [stickyChangeHandler]);
+    if (isStickyApplied) {
+      const timer = setTimeout(() => {
+        if (isSticky) {
+          setAnimateClasses('visible');
+          setButtonClasses('');
+        }
+        else {
+          setAnimateClasses('invisible');
+        }
+      }, 500);
+
+      const newAnimateClasses = isSticky ? 'animated fadeInUp faster' : 'animated fadeOut faster';
+      const newButtonClasses = isSticky ? '' : 'disabled grw-pointer-events-none';
+
+      setAnimateClasses(newAnimateClasses);
+      setButtonClasses(newButtonClasses);
+
+      return () => clearTimeout(timer);
+    }
+  }, [isSticky, isStickyApplied]);
 
 
   const PageCreateButton = useCallback(() => {
   const PageCreateButton = useCallback(() => {
     return (
     return (

+ 2 - 2
packages/app/src/components/Navbar/GrowiContextualSubNavigation.tsx

@@ -17,7 +17,7 @@ import {
 import { OnDuplicatedFunction, OnRenamedFunction, OnDeletedFunction } from '~/interfaces/ui';
 import { OnDuplicatedFunction, OnRenamedFunction, OnDeletedFunction } from '~/interfaces/ui';
 import {
 import {
   useCurrentPathname,
   useCurrentPathname,
-  useCurrentUser, useIsGuestUser, useIsSharedUser, useShareLinkId, useTemplateTagData, useIsContainerFluid, useIsIdenticalPath,
+  useCurrentUser, useIsGuestUser, useIsSharedUser, useShareLinkId, useIsContainerFluid, useIsIdenticalPath,
 } from '~/stores/context';
 } from '~/stores/context';
 import { usePageTagsForEditors } from '~/stores/editor';
 import { usePageTagsForEditors } from '~/stores/editor';
 import {
 import {
@@ -25,7 +25,7 @@ import {
   usePageDuplicateModal, usePageRenameModal, usePageDeleteModal, usePagePresentationModal,
   usePageDuplicateModal, usePageRenameModal, usePageDeleteModal, usePagePresentationModal,
 } from '~/stores/modal';
 } from '~/stores/modal';
 import {
 import {
-  useSWRMUTxCurrentPage, useSWRxTagsInfo, useCurrentPageId, useIsNotFound,
+  useSWRMUTxCurrentPage, useSWRxTagsInfo, useCurrentPageId, useIsNotFound, useTemplateTagData,
 } from '~/stores/page';
 } from '~/stores/page';
 import {
 import {
   EditorMode, useDrawerMode, useEditorMode, useIsAbleToShowPageManagement, useIsAbleToShowTagLabel,
   EditorMode, useDrawerMode, useEditorMode, useIsAbleToShowPageManagement, useIsAbleToShowTagLabel,

+ 5 - 22
packages/app/src/components/Navbar/GrowiSubNavigationSwitcher.tsx

@@ -2,9 +2,9 @@ import React, {
   useState, useRef, useEffect, useCallback,
   useState, useRef, useEffect, useCallback,
 } from 'react';
 } from 'react';
 
 
-import StickyEvents from 'sticky-events';
 import { debounce } from 'throttle-debounce';
 import { debounce } from 'throttle-debounce';
 
 
+import { useSticky } from '~/client/services/side-effects/use-sticky';
 import { useSWRxCurrentPage } from '~/stores/page';
 import { useSWRxCurrentPage } from '~/stores/page';
 import { useSidebarCollapsed } from '~/stores/ui';
 import { useSidebarCollapsed } from '~/stores/ui';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
@@ -32,7 +32,6 @@ export const GrowiSubNavigationSwitcher = (props: GrowiSubNavigationSwitcherProp
   const { data: currentPage } = useSWRxCurrentPage();
   const { data: currentPage } = useSWRxCurrentPage();
   const { data: isSidebarCollapsed } = useSidebarCollapsed();
   const { data: isSidebarCollapsed } = useSidebarCollapsed();
 
 
-  const [isVisible, setIsVisible] = useState<boolean>(false);
   const [width, setWidth] = useState<number>(0);
   const [width, setWidth] = useState<number>(0);
 
 
   // use more specific type HTMLDivElement for avoid assertion error.
   // use more specific type HTMLDivElement for avoid assertion error.
@@ -40,6 +39,9 @@ export const GrowiSubNavigationSwitcher = (props: GrowiSubNavigationSwitcherProp
   const fixedContainerRef = useRef<HTMLDivElement>(null);
   const fixedContainerRef = useRef<HTMLDivElement>(null);
   const clientWidth = fixedContainerRef.current?.parentElement?.clientWidth;
   const clientWidth = fixedContainerRef.current?.parentElement?.clientWidth;
 
 
+  // Get sticky status
+  const isSticky = useSticky('#grw-subnav-sticky-trigger');
+
   // Do not use clientWidth as useCallback deps, resizing events will not work in production builds.
   // Do not use clientWidth as useCallback deps, resizing events will not work in production builds.
   const initWidth = useCallback(() => {
   const initWidth = useCallback(() => {
     if (fixedContainerRef.current != null && fixedContainerRef.current.parentElement != null) {
     if (fixedContainerRef.current != null && fixedContainerRef.current.parentElement != null) {
@@ -49,25 +51,6 @@ export const GrowiSubNavigationSwitcher = (props: GrowiSubNavigationSwitcherProp
     }
     }
   }, []);
   }, []);
 
 
-  const stickyChangeHandler = useCallback((event) => {
-    logger.debug('StickyEvents.CHANGE detected');
-    setIsVisible(event.detail.isSticky);
-  }, []);
-
-  // setup effect by sticky-events
-  useEffect(() => {
-    // sticky-events
-    // See: https://github.com/ryanwalters/sticky-events
-    const { stickySelector } = new StickyEvents({ stickySelector: '#grw-subnav-sticky-trigger' });
-    const elem = document.querySelector(stickySelector);
-    elem.addEventListener(StickyEvents.CHANGE, stickyChangeHandler);
-
-    // return clean up handler
-    return () => {
-      elem.removeEventListener(StickyEvents.CHANGE, stickyChangeHandler);
-    };
-  }, [stickyChangeHandler]);
-
   // setup effect by resizing event
   // setup effect by resizing event
   useEffect(() => {
   useEffect(() => {
     const resizeHandler = debounce(100, initWidth);
     const resizeHandler = debounce(100, initWidth);
@@ -100,7 +83,7 @@ export const GrowiSubNavigationSwitcher = (props: GrowiSubNavigationSwitcherProp
   }
   }
 
 
   return (
   return (
-    <div className={`${styles['grw-subnav-switcher']} ${isVisible ? '' : 'grw-subnav-switcher-hidden'}`}>
+    <div className={`${styles['grw-subnav-switcher']} ${isSticky ? '' : 'grw-subnav-switcher-hidden'}`}>
       <div
       <div
         id="grw-subnav-fixed-container"
         id="grw-subnav-fixed-container"
         className={`grw-subnav-fixed-container ${styles['grw-subnav-fixed-container']} position-fixed grw-subnav-append-shadow-container`}
         className={`grw-subnav-fixed-container ${styles['grw-subnav-fixed-container']} position-fixed grw-subnav-append-shadow-container`}

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

@@ -114,7 +114,7 @@ const PageAccessoriesModal = (): JSX.Element => {
     </div>
     </div>
   ), [close, isWindowExpanded]);
   ), [close, isWindowExpanded]);
 
 
-  if (status == null) {
+  if (status == null || activeTab == null) {
     return <></>;
     return <></>;
   }
   }
 
 

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

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

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

@@ -21,7 +21,7 @@ import { IEditorMethods } from '~/interfaces/editor-methods';
 import { OptionsToSave } from '~/interfaces/page-operation';
 import { OptionsToSave } from '~/interfaces/page-operation';
 import { SocketEventName } from '~/interfaces/websocket';
 import { SocketEventName } from '~/interfaces/websocket';
 import {
 import {
-  useCurrentPathname, useIsEnabledAttachTitleHeader, useTemplateBodyData,
+  useCurrentPathname, useIsEnabledAttachTitleHeader,
   useIsEditable, useIsUploadableFile, useIsUploadableImage, useIsIndentSizeForced,
   useIsEditable, useIsUploadableFile, useIsUploadableImage, useIsIndentSizeForced,
 } from '~/stores/context';
 } from '~/stores/context';
 import {
 import {
@@ -32,7 +32,7 @@ import {
 } from '~/stores/editor';
 } from '~/stores/editor';
 import { useConflictDiffModal } from '~/stores/modal';
 import { useConflictDiffModal } from '~/stores/modal';
 import {
 import {
-  useCurrentPagePath, useSWRMUTxCurrentPage, useSWRxCurrentPage, useSWRxTagsInfo, useCurrentPageId, useIsNotFound, useIsLatestRevision,
+  useCurrentPagePath, useSWRMUTxCurrentPage, useSWRxCurrentPage, useSWRxTagsInfo, useCurrentPageId, useIsNotFound, useIsLatestRevision, useTemplateBodyData,
 } from '~/stores/page';
 } from '~/stores/page';
 import { mutatePageTree } from '~/stores/page-listing';
 import { mutatePageTree } from '~/stores/page-listing';
 import {
 import {

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

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

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

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

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

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

+ 2 - 12
packages/app/src/components/PageEditor/DrawioCommunicationHelper.ts

@@ -1,5 +1,3 @@
-import { extractCodeFromMxfile } from '@growi/remark-drawio';
-
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
 
 
@@ -8,6 +6,7 @@ const logger = loggerFactory('growi:cli:DrawioCommunicationHelper');
 export type DrawioConfig = {
 export type DrawioConfig = {
   css: string,
   css: string,
   customFonts: string[],
   customFonts: string[],
+  compressXml: boolean,
 }
 }
 
 
 export type DrawioCommunicationCallbackOptions = {
 export type DrawioCommunicationCallbackOptions = {
@@ -63,16 +62,7 @@ export class DrawioCommunicationHelper {
 
 
     // restore diagram data
     // restore diagram data
     if (event.data === 'ready') {
     if (event.data === 'ready') {
-      let code = drawioMxFile;
-      try {
-        code = extractCodeFromMxfile(drawioMxFile);
-      }
-      // catch error if drawioMxFile is not XML
-      catch (err) {
-        // do nothing because drawioMxFile might be base64 code
-      }
-
-      event.source?.postMessage(code, { targetOrigin: '*' });
+      event.source?.postMessage(drawioMxFile, { targetOrigin: '*' });
       return;
       return;
     }
     }
 
 

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

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

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

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

+ 2 - 2
packages/app/src/components/ReactMarkdownComponents/DrawioViewerWithEditButton.tsx

@@ -4,7 +4,7 @@ import EventEmitter from 'events';
 
 
 import {
 import {
   DrawioEditByViewerProps,
   DrawioEditByViewerProps,
-  DrawioViewer, DrawioViewerProps, extractCodeFromMxfile,
+  DrawioViewer, DrawioViewerProps,
 } from '@growi/remark-drawio';
 } from '@growi/remark-drawio';
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 
 
@@ -33,7 +33,7 @@ export const DrawioViewerWithEditButton = React.memo((props: DrawioViewerProps):
 
 
   const editButtonClickHandler = useCallback(() => {
   const editButtonClickHandler = useCallback(() => {
     const data: DrawioEditByViewerProps = {
     const data: DrawioEditByViewerProps = {
-      bol, eol, drawioMxFile: extractCodeFromMxfile(mxfile),
+      bol, eol, drawioMxFile: mxfile,
     };
     };
     globalEmitter.emit('launchDrawioModal', data);
     globalEmitter.emit('launchDrawioModal', data);
   }, [bol, eol, mxfile]);
   }, [bol, eol, mxfile]);

+ 5 - 1
packages/app/src/components/Script/DrawioViewerScript.tsx

@@ -3,12 +3,16 @@ import { useCallback } from 'react';
 import type { IGraphViewerGlobal } from '@growi/remark-drawio';
 import type { IGraphViewerGlobal } from '@growi/remark-drawio';
 import Script from 'next/script';
 import Script from 'next/script';
 
 
+import { useDrawioUri } from '~/stores/context';
+
 declare global {
 declare global {
   // eslint-disable-next-line vars-on-top, no-var
   // eslint-disable-next-line vars-on-top, no-var
   var GraphViewer: IGraphViewerGlobal;
   var GraphViewer: IGraphViewerGlobal;
 }
 }
 
 
 export const DrawioViewerScript = (): JSX.Element => {
 export const DrawioViewerScript = (): JSX.Element => {
+  const { data: drawioUri } = useDrawioUri();
+
   const loadedHandler = useCallback(() => {
   const loadedHandler = useCallback(() => {
     // disable useResizeSensor and checkVisibleState
     // disable useResizeSensor and checkVisibleState
     //   for preventing resize event by viewer.min.js
     //   for preventing resize event by viewer.min.js
@@ -31,7 +35,7 @@ export const DrawioViewerScript = (): JSX.Element => {
   return (
   return (
     <Script
     <Script
       type="text/javascript"
       type="text/javascript"
-      src="https://www.draw.io/js/viewer.min.js"
+      src={(new URL('/js/viewer.min.js', drawioUri)).toString()}
       onLoad={loadedHandler}
       onLoad={loadedHandler}
     />
     />
   );
   );

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

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

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

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

+ 7 - 21
packages/app/src/components/StickyStretchableScroller.tsx

@@ -1,11 +1,11 @@
 import React, {
 import React, {
-  useEffect, useCallback, FC, useRef, useState, useMemo, RefObject,
+  useEffect, useCallback, useRef, useState, useMemo, RefObject,
 } from 'react';
 } from 'react';
 
 
 import SimpleBar from 'simplebar-react';
 import SimpleBar from 'simplebar-react';
-import StickyEvents from 'sticky-events';
 import { debounce } from 'throttle-debounce';
 import { debounce } from 'throttle-debounce';
 
 
+import { useSticky } from '~/client/services/side-effects/use-sticky';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
 const logger = loggerFactory('growi:cli:StickyStretchableScroller');
 const logger = loggerFactory('growi:cli:StickyStretchableScroller');
@@ -49,6 +49,9 @@ export const StickyStretchableScroller = (props: StickyStretchableScrollerProps)
   const simplebarRef = useRef<SimpleBar>(null);
   const simplebarRef = useRef<SimpleBar>(null);
   const [simplebarMaxHeight, setSimplebarMaxHeight] = useState<number|undefined>();
   const [simplebarMaxHeight, setSimplebarMaxHeight] = useState<number|undefined>();
 
 
+  // Get sticky status
+  const isSticky = useSticky(stickyElemSelector);
+
   /**
   /**
    * Reset scrollbar
    * Reset scrollbar
    */
    */
@@ -70,26 +73,9 @@ export const StickyStretchableScroller = (props: StickyStretchableScrollerProps)
 
 
   const resetScrollbarDebounced = useMemo(() => debounce(100, resetScrollbar), [resetScrollbar]);
   const resetScrollbarDebounced = useMemo(() => debounce(100, resetScrollbar), [resetScrollbar]);
 
 
-  const stickyChangeHandler = useCallback(() => {
-    logger.debug('StickyEvents.CHANGE detected');
-    resetScrollbarDebounced();
-  }, [resetScrollbarDebounced]);
-
-  // setup effect by sticky event
   useEffect(() => {
   useEffect(() => {
-    // sticky
-    // See: https://github.com/ryanwalters/sticky-events
-    const stickyEvents = new StickyEvents({ stickySelector: stickyElemSelector });
-    stickyEvents.enableEvents();
-    const { stickySelector } = stickyEvents;
-    const elem = document.querySelector(stickySelector);
-    elem.addEventListener(StickyEvents.CHANGE, stickyChangeHandler);
-
-    // return clean up handler
-    return () => {
-      elem.removeEventListener(StickyEvents.CHANGE, stickyChangeHandler);
-    };
-  }, [stickyElemSelector, stickyChangeHandler]);
+    resetScrollbarDebounced();
+  }, [isSticky, resetScrollbarDebounced]);
 
 
   // setup effect by resizing event
   // setup effect by resizing event
   useEffect(() => {
   useEffect(() => {

+ 13 - 5
packages/app/src/pages/[[...path]].page.tsx

@@ -34,7 +34,7 @@ import {
   useIsEnabledStaleNotification, useIsIdenticalPath,
   useIsEnabledStaleNotification, useIsIdenticalPath,
   useIsSearchServiceConfigured, useIsSearchServiceReachable, useDisableLinkSharing,
   useIsSearchServiceConfigured, useIsSearchServiceReachable, useDisableLinkSharing,
   useDrawioUri, useHackmdUri, useDefaultIndentSize, useIsIndentSizeForced,
   useDrawioUri, useHackmdUri, useDefaultIndentSize, useIsIndentSizeForced,
-  useIsAclEnabled, useIsSearchPage, useTemplateTagData, useTemplateBodyData, useIsEnabledAttachTitleHeader,
+  useIsAclEnabled, useIsSearchPage, useIsEnabledAttachTitleHeader,
   useCsrfToken, useIsSearchScopeChildrenAsDefault, useCurrentPathname,
   useCsrfToken, useIsSearchScopeChildrenAsDefault, useCurrentPathname,
   useIsSlackConfigured, useRendererConfig,
   useIsSlackConfigured, useRendererConfig,
   useEditorConfig, useIsAllReplyShown, useIsUploadableFile, useIsUploadableImage, useIsContainerFluid, useIsNotCreatable,
   useEditorConfig, useIsAllReplyShown, useIsUploadableFile, useIsUploadableImage, useIsContainerFluid, useIsNotCreatable,
@@ -42,7 +42,7 @@ import {
 import { useEditingMarkdown } from '~/stores/editor';
 import { useEditingMarkdown } from '~/stores/editor';
 import { useHasDraftOnHackmd, usePageIdOnHackmd, useRevisionIdHackmdSynced } from '~/stores/hackmd';
 import { useHasDraftOnHackmd, usePageIdOnHackmd, useRevisionIdHackmdSynced } from '~/stores/hackmd';
 import {
 import {
-  useSWRxCurrentPage, useSWRxIsGrantNormalized, useCurrentPageId, useIsNotFound, useIsLatestRevision,
+  useSWRxCurrentPage, useSWRxIsGrantNormalized, useCurrentPageId, useIsNotFound, useIsLatestRevision, useTemplateTagData, useTemplateBodyData,
 } from '~/stores/page';
 } from '~/stores/page';
 import { useRedirectFrom } from '~/stores/page-redirect';
 import { useRedirectFrom } from '~/stores/page-redirect';
 import { useRemoteRevisionId } from '~/stores/remote-latest-page';
 import { useRemoteRevisionId } from '~/stores/remote-latest-page';
@@ -201,9 +201,6 @@ const Page: NextPageWithLayout<Props> = (props: Props) => {
   useIsEnabledStaleNotification(props.isEnabledStaleNotification);
   useIsEnabledStaleNotification(props.isEnabledStaleNotification);
   useIsSearchPage(false);
   useIsSearchPage(false);
 
 
-  useTemplateTagData(props.templateTagData);
-  useTemplateBodyData(props.templateBodyData);
-
   useIsEnabledAttachTitleHeader(props.isEnabledAttachTitleHeader);
   useIsEnabledAttachTitleHeader(props.isEnabledAttachTitleHeader);
   useIsSearchServiceConfigured(props.isSearchServiceConfigured);
   useIsSearchServiceConfigured(props.isSearchServiceConfigured);
   useIsSearchServiceReachable(props.isSearchServiceReachable);
   useIsSearchServiceReachable(props.isSearchServiceReachable);
@@ -252,6 +249,9 @@ const Page: NextPageWithLayout<Props> = (props: Props) => {
   const { mutate: mutateRemoteRevisionId } = useRemoteRevisionId();
   const { mutate: mutateRemoteRevisionId } = useRemoteRevisionId();
   const { mutate: mutateRevisionIdHackmdSynced } = useRevisionIdHackmdSynced();
   const { mutate: mutateRevisionIdHackmdSynced } = useRevisionIdHackmdSynced();
 
 
+  const { mutate: mutateTemplateTagData } = useTemplateTagData();
+  const { mutate: mutateTemplateBodyData } = useTemplateBodyData();
+
   useSetupGlobalSocket();
   useSetupGlobalSocket();
   useSetupGlobalSocketForPage(pageId);
   useSetupGlobalSocketForPage(pageId);
 
 
@@ -301,6 +301,14 @@ const Page: NextPageWithLayout<Props> = (props: Props) => {
     mutateIsLatestRevision(props.isLatestRevision);
     mutateIsLatestRevision(props.isLatestRevision);
   }, [mutateIsLatestRevision, props.isLatestRevision]);
   }, [mutateIsLatestRevision, props.isLatestRevision]);
 
 
+  useEffect(() => {
+    mutateTemplateTagData(props.templateTagData);
+  }, [props.templateTagData, mutateTemplateTagData]);
+
+  useEffect(() => {
+    mutateTemplateBodyData(props.templateBodyData);
+  }, [props.templateBodyData, mutateTemplateBodyData]);
+
   const title = generateCustomTitleForPage(props, pagePath);
   const title = generateCustomTitleForPage(props, pagePath);
 
 
   return (
   return (

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

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

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

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

+ 5 - 5
packages/app/src/server/service/search-delegator/elasticsearch.ts

@@ -963,17 +963,17 @@ class ElasticsearchDelegator implements SearchDelegator<Data, ESTermsKey, ESQuer
     }
     }
   }
   }
 
 
-  async search(data: SearchableData<ESQueryTerms>, user, userGroups, option): Promise<ISearchResult<unknown>> {
+  async search(data: SearchableData<ESQueryTerms>, user, userGroups, option?): Promise<ISearchResult<unknown>> {
     const { queryString, terms } = data;
     const { queryString, terms } = data;
 
 
     if (terms == null) {
     if (terms == null) {
       throw Error('Cannnot process search since terms is undefined.');
       throw Error('Cannnot process search since terms is undefined.');
     }
     }
 
 
-    const from = option.offset || null;
-    const size = option.limit || null;
-    const sort = option.sort || null;
-    const order = option.order || null;
+    const from = option?.offset ?? null;
+    const size = option?.limit ?? null;
+    const sort = option?.sort ?? null;
+    const order = option?.order ?? null;
     const query = this.createSearchQuery();
     const query = this.createSearchQuery();
     this.appendCriteriaForQueryString(query, terms);
     this.appendCriteriaForQueryString(query, terms);
 
 

+ 5 - 5
packages/app/src/server/service/slack-command-handler/create-page-service.js

@@ -11,17 +11,17 @@ class CreatePageService {
     this.crowi = crowi;
     this.crowi = crowi;
   }
   }
 
 
-  async createPageInGrowi(interactionPayloadAccessor, path, contentsBody, respondUtil, userId) {
-    const Page = this.crowi.model('Page');
+  async createPageInGrowi(interactionPayloadAccessor, path, contentsBody, respondUtil, user) {
     const reshapedContentsBody = reshapeContentsBody(contentsBody);
     const reshapedContentsBody = reshapeContentsBody(contentsBody);
 
 
     // sanitize path
     // sanitize path
     const sanitizedPath = this.crowi.xss.process(path);
     const sanitizedPath = this.crowi.xss.process(path);
     const normalizedPath = pathUtils.normalizePath(sanitizedPath);
     const normalizedPath = pathUtils.normalizePath(sanitizedPath);
 
 
-    // generate a dummy id because Operation to create a page needs ObjectId
-    const dummyObjectIdOfUser = userId != null ? userId : new mongoose.Types.ObjectId();
-    const page = await this.crowi.pageService.create(normalizedPath, reshapedContentsBody, dummyObjectIdOfUser, {});
+    // Since an ObjectId is required for creating a page, if a user does not exist, a dummy user will be generated
+    const userOrDummyUser = user != null ? user : { _id: new mongoose.Types.ObjectId() };
+
+    const page = await this.crowi.pageService.create(normalizedPath, reshapedContentsBody, userOrDummyUser, {});
 
 
     // Send a message when page creation is complete
     // Send a message when page creation is complete
     const growiUri = this.crowi.appService.getSiteUrl();
     const growiUri = this.crowi.appService.getSiteUrl();

+ 3 - 4
packages/app/src/server/service/slack-command-handler/keep.js

@@ -35,7 +35,6 @@ module.exports = (crowi) => {
     const channelId = payload.channel.id; // this must exist since the type is always block_actions
     const channelId = payload.channel.id; // this must exist since the type is always block_actions
     const user = await User.findUserBySlackMemberId(payload.user.id);
     const user = await User.findUserBySlackMemberId(payload.user.id);
 
 
-    const userId = user != null ? user._id : null;
     // validate form
     // validate form
     const { path, oldest, newest } = await this.keepValidateForm(client, payload, interactionPayloadAccessor);
     const { path, oldest, newest } = await this.keepValidateForm(client, payload, interactionPayloadAccessor);
     // get messages
     // get messages
@@ -45,7 +44,7 @@ module.exports = (crowi) => {
 
 
     const contentsBody = cleanedContents.join('');
     const contentsBody = cleanedContents.join('');
     // create and send url message
     // create and send url message
-    await this.keepCreatePageAndSendPreview(client, interactionPayloadAccessor, path, userId, contentsBody, respondUtil);
+    await this.keepCreatePageAndSendPreview(client, interactionPayloadAccessor, path, user, contentsBody, respondUtil);
   };
   };
 
 
   handler.keepValidateForm = async function(client, payload, interactionPayloadAccessor) {
   handler.keepValidateForm = async function(client, payload, interactionPayloadAccessor) {
@@ -197,8 +196,8 @@ module.exports = (crowi) => {
     return cleanedContents;
     return cleanedContents;
   };
   };
 
 
-  handler.keepCreatePageAndSendPreview = async function(client, interactionPayloadAccessor, path, userId, contentsBody, respondUtil) {
-    await createPageService.createPageInGrowi(interactionPayloadAccessor, path, contentsBody, respondUtil, userId);
+  handler.keepCreatePageAndSendPreview = async function(client, interactionPayloadAccessor, path, user, contentsBody, respondUtil) {
+    await createPageService.createPageInGrowi(interactionPayloadAccessor, path, contentsBody, respondUtil, user);
 
 
     // TODO: contentsBody text characters must be less than 3001
     // TODO: contentsBody text characters must be less than 3001
     // send preview to dm
     // send preview to dm

+ 3 - 1
packages/app/src/server/service/slack-command-handler/note.js

@@ -17,6 +17,7 @@ module.exports = (crowi) => {
     type: 'conversations_select',
     type: 'conversations_select',
     default_to_current_conversation: true,
     default_to_current_conversation: true,
   };
   };
+  const { User } = crowi.models;
 
 
   handler.handleCommand = async(growiCommand, client, body, respondUtil) => {
   handler.handleCommand = async(growiCommand, client, body, respondUtil) => {
     await respondUtil.respond({
     await respondUtil.respond({
@@ -44,12 +45,13 @@ module.exports = (crowi) => {
   };
   };
 
 
   handler.createPage = async function(client, interactionPayload, interactionPayloadAccessor, respondUtil) {
   handler.createPage = async function(client, interactionPayload, interactionPayloadAccessor, respondUtil) {
+    const user = await User.findUserBySlackMemberId(interactionPayload.user.id);
     const path = interactionPayloadAccessor.getStateValues()?.path.path_input.value;
     const path = interactionPayloadAccessor.getStateValues()?.path.path_input.value;
     const contentsBody = interactionPayloadAccessor.getStateValues()?.contents.contents_input.value;
     const contentsBody = interactionPayloadAccessor.getStateValues()?.contents.contents_input.value;
     if (path == null || contentsBody == null) {
     if (path == null || contentsBody == null) {
       throw new SlackCommandHandlerError('All parameters are required.');
       throw new SlackCommandHandlerError('All parameters are required.');
     }
     }
-    await createPageService.createPageInGrowi(interactionPayloadAccessor, path, contentsBody, respondUtil);
+    await createPageService.createPageInGrowi(interactionPayloadAccessor, path, contentsBody, respondUtil, user);
     await respondUtil.deleteOriginal();
     await respondUtil.deleteOriginal();
   };
   };
 
 

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

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

+ 6 - 8
packages/app/src/services/renderer/renderer.tsx

@@ -19,6 +19,7 @@ import breaks from 'remark-breaks';
 import emoji from 'remark-emoji';
 import emoji from 'remark-emoji';
 import gfm from 'remark-gfm';
 import gfm from 'remark-gfm';
 import math from 'remark-math';
 import math from 'remark-math';
+import toc from 'remark-toc';
 import deepmerge from 'ts-deepmerge';
 import deepmerge from 'ts-deepmerge';
 import type { PluggableList, Pluggable, PluginTuple } from 'unified';
 import type { PluggableList, Pluggable, PluginTuple } from 'unified';
 
 
@@ -38,10 +39,9 @@ import * as addLineNumberAttribute from './rehype-plugins/add-line-number-attrib
 import * as keywordHighlighter from './rehype-plugins/keyword-highlighter';
 import * as keywordHighlighter from './rehype-plugins/keyword-highlighter';
 import { relativeLinks } from './rehype-plugins/relative-links';
 import { relativeLinks } from './rehype-plugins/relative-links';
 import { relativeLinksByPukiwikiLikeLinker } from './rehype-plugins/relative-links-by-pukiwiki-like-linker';
 import { relativeLinksByPukiwikiLikeLinker } from './rehype-plugins/relative-links-by-pukiwiki-like-linker';
-import * as toc from './rehype-plugins/relocate-toc';
+import * as relocateToc from './rehype-plugins/relocate-toc';
 import * as plantuml from './remark-plugins/plantuml';
 import * as plantuml from './remark-plugins/plantuml';
 import { pukiwikiLikeLinker } from './remark-plugins/pukiwiki-like-linker';
 import { pukiwikiLikeLinker } from './remark-plugins/pukiwiki-like-linker';
-import * as table from './remark-plugins/table';
 import * as xsvToTable from './remark-plugins/xsv-to-table';
 import * as xsvToTable from './remark-plugins/xsv-to-table';
 
 
 // import EasyGrid from './PreProcessor/EasyGrid';
 // import EasyGrid from './PreProcessor/EasyGrid';
@@ -73,7 +73,7 @@ const baseSanitizeSchema = {
     video: ['controls', 'src', 'muted', 'preload', 'width', 'height', 'autoplay'],
     video: ['controls', 'src', 'muted', 'preload', 'width', 'height', 'autoplay'],
     // The special value 'data*' as a property name can be used to allow all data properties.
     // The special value 'data*' as a property name can be used to allow all data properties.
     // see: https://github.com/syntax-tree/hast-util-sanitize/
     // see: https://github.com/syntax-tree/hast-util-sanitize/
-    '*': ['class', 'className', 'style', 'data*'],
+    '*': ['key', 'class', 'className', 'style', 'data*'],
   },
   },
 };
 };
 
 
@@ -125,6 +125,7 @@ const verifySanitizePlugin = (options: RendererOptions, shouldBeTheLastItem = tr
 const generateCommonOptions = (pagePath: string|undefined): RendererOptions => {
 const generateCommonOptions = (pagePath: string|undefined): RendererOptions => {
   return {
   return {
     remarkPlugins: [
     remarkPlugins: [
+      [toc, { maxDepth: 3, tight: true, prefix: 'mdcont-' }],
       gfm,
       gfm,
       emoji,
       emoji,
       pukiwikiLikeLinker,
       pukiwikiLikeLinker,
@@ -189,7 +190,7 @@ export const generateViewOptions = (
     [lsxGrowiPlugin.rehypePlugin, { pagePath, isSharedPage: config.isSharedPage }],
     [lsxGrowiPlugin.rehypePlugin, { pagePath, isSharedPage: config.isSharedPage }],
     rehypeSanitizePlugin,
     rehypeSanitizePlugin,
     katex,
     katex,
-    [toc.rehypePluginStore, { storeTocNode }],
+    [relocateToc.rehypePluginStore, { storeTocNode }],
   );
   );
 
 
   // add components
   // add components
@@ -233,7 +234,7 @@ export const generateTocOptions = (config: RendererConfig, tocNode: HtmlElementN
 
 
   // add rehype plugins
   // add rehype plugins
   rehypePlugins.push(
   rehypePlugins.push(
-    [toc.rehypePluginRestore, { tocNode }],
+    [relocateToc.rehypePluginRestore, { tocNode }],
     rehypeSanitizePlugin,
     rehypeSanitizePlugin,
   );
   );
 
 
@@ -261,7 +262,6 @@ export const generateSimpleViewOptions = (
     drawioPlugin.remarkPlugin,
     drawioPlugin.remarkPlugin,
     xsvToTable.remarkPlugin,
     xsvToTable.remarkPlugin,
     lsxGrowiPlugin.remarkPlugin,
     lsxGrowiPlugin.remarkPlugin,
-    // table.remarkPlugin,
   );
   );
 
 
   const isEnabledLinebreaks = overrideIsEnabledLinebreaks ?? config.isEnabledLinebreaks;
   const isEnabledLinebreaks = overrideIsEnabledLinebreaks ?? config.isEnabledLinebreaks;
@@ -329,7 +329,6 @@ export const generateSSRViewOptions = (
     math,
     math,
     xsvToTable.remarkPlugin,
     xsvToTable.remarkPlugin,
     lsxGrowiPlugin.remarkPlugin,
     lsxGrowiPlugin.remarkPlugin,
-    // table.remarkPlugin,
   );
   );
 
 
   const isEnabledLinebreaks = config.isEnabledLinebreaks;
   const isEnabledLinebreaks = config.isEnabledLinebreaks;
@@ -380,7 +379,6 @@ export const generatePreviewOptions = (config: RendererConfig, pagePath: string)
     drawioPlugin.remarkPlugin,
     drawioPlugin.remarkPlugin,
     xsvToTable.remarkPlugin,
     xsvToTable.remarkPlugin,
     lsxGrowiPlugin.remarkPlugin,
     lsxGrowiPlugin.remarkPlugin,
-    // table.remarkPlugin,
   );
   );
   if (config.isEnabledLinebreaks) {
   if (config.isEnabledLinebreaks) {
     remarkPlugins.push(breaks);
     remarkPlugins.push(breaks);

+ 0 - 8
packages/app/src/stores/context.tsx

@@ -56,14 +56,6 @@ export const useIsNotCreatable = (initialData?: boolean): SWRResponse<boolean, E
   return useContextSWR<boolean, Error>('isNotCreatable', initialData, { fallbackData: false });
   return useContextSWR<boolean, Error>('isNotCreatable', initialData, { fallbackData: false });
 };
 };
 
 
-export const useTemplateTagData = (initialData?: string[]): SWRResponse<string[], Error> => {
-  return useContextSWR<string[], Error>('templateTagData', initialData);
-};
-
-export const useTemplateBodyData = (initialData?: string): SWRResponse<string, Error> => {
-  return useContextSWR<string, Error>('templateBodyData', initialData);
-};
-
 export const useIsSharedUser = (initialData?: boolean): SWRResponse<boolean, Error> => {
 export const useIsSharedUser = (initialData?: boolean): SWRResponse<boolean, Error> => {
   return useContextSWR<boolean, Error>('isSharedUser', initialData);
   return useContextSWR<boolean, Error>('isSharedUser', initialData);
 };
 };

+ 8 - 0
packages/app/src/stores/page.tsx

@@ -37,6 +37,14 @@ export const useIsNotFound = (initialData?: boolean): SWRResponse<boolean, Error
   return useStaticSWR<boolean, Error>('isNotFound', initialData, { fallbackData: false });
   return useStaticSWR<boolean, Error>('isNotFound', initialData, { fallbackData: false });
 };
 };
 
 
+export const useTemplateTagData = (initialData?: string[]): SWRResponse<string[], Error> => {
+  return useStaticSWR<string[], Error>('templateTagData', initialData);
+};
+
+export const useTemplateBodyData = (initialData?: string): SWRResponse<string, Error> => {
+  return useStaticSWR<string, Error>('templateBodyData', initialData);
+};
+
 export const useSWRxCurrentPage = (initialData?: IPagePopulatedToShowRevision|null): SWRResponse<IPagePopulatedToShowRevision|null> => {
 export const useSWRxCurrentPage = (initialData?: IPagePopulatedToShowRevision|null): SWRResponse<IPagePopulatedToShowRevision|null> => {
   const key = 'currentPage';
   const key = 'currentPage';
 
 

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

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

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

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

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

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

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

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

+ 1 - 1
packages/core/package.json

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

+ 1 - 1
packages/hackmd/package.json

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

+ 3 - 3
packages/presentation/package.json

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

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

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

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

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

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

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

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

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

+ 16 - 31
packages/remark-drawio/src/utils/embed.ts

@@ -1,22 +1,14 @@
 // transplanted from https://github.com/jgraph/drawio-tools/blob/d46977060ffad70cae5a9059a2cbfcd8bcf420de/tools/convert.html
 // transplanted from https://github.com/jgraph/drawio-tools/blob/d46977060ffad70cae5a9059a2cbfcd8bcf420de/tools/convert.html
 import pako from 'pako';
 import pako from 'pako';
-import xmldoc from 'xmldoc';
 
 
-export const extractCodeFromMxfile = (input: string): string => {
-  const doc = new xmldoc.XmlDocument(input);
-  return doc.valueWithPath('diagram');
+const unconpressedDataRegexp = new RegExp('<mxGraphModel');
+const validateUncompressedData = (input: string): boolean => {
+  return unconpressedDataRegexp.test(input);
 };
 };
 
 
-const validateInputData = (input: string): boolean => {
+const validateCompressedData = (input: string): boolean => {
   let data = input;
   let data = input;
 
 
-  try {
-    data = extractCodeFromMxfile(data);
-  }
-  catch (e) {
-    // ignore
-  }
-
   try {
   try {
     data = Buffer.from(data, 'base64').toString('binary');
     data = Buffer.from(data, 'base64').toString('binary');
   }
   }
@@ -65,27 +57,20 @@ export const generateMxgraphData = (code: string): string => {
     return '';
     return '';
   }
   }
 
 
-  validateInputData(trimedCode);
-
-  let xml;
-  try {
-    // may be XML Format <mxfile><diagram> ... </diagram></mxfile>
-    const doc = new xmldoc.XmlDocument(trimedCode);
-    const diagram = doc.valueWithPath('diagram');
-    if (diagram) {
-      xml = trimedCode;
-    }
-  }
-  catch (e) {
-    // may be NOT XML Format
-    xml = `
-<mxfile version="6.8.9" editor="www.draw.io" type="atlas">
-  <mxAtlasLibraries/>
-  <diagram>${trimedCode}</diagram>
-</mxfile>
-`;
+  // Evaluate the code is whether uncompressed data that are generated by v21.1.0 or above
+  // see: https://github.com/jgraph/drawio/issues/3106#issuecomment-1479352026
+  const isUncompressedData = validateUncompressedData(trimedCode);
+  if (!isUncompressedData) {
+    validateCompressedData(trimedCode);
   }
   }
 
 
+  const xml = `
+    <mxfile version="6.8.9" editor="www.draw.io" type="atlas">
+      <mxAtlasLibraries/>
+      <diagram>${trimedCode}</diagram>
+    </mxfile>
+  `;
+
   // see options: https://drawio.freshdesk.com/support/solutions/articles/16000042542-embed-html
   // see options: https://drawio.freshdesk.com/support/solutions/articles/16000042542-embed-html
   const mxGraphData = {
   const mxGraphData = {
     editable: false,
     editable: false,

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

@@ -1,6 +1,6 @@
 {
 {
   "name": "@growi/remark-growi-directive",
   "name": "@growi/remark-growi-directive",
-  "version": "6.0.10",
+  "version": "6.0.16-RC.0",
   "description": "remark plugin to support GROWI plugin (forked from remark-directive@2.0.1)",
   "description": "remark plugin to support GROWI plugin (forked from remark-directive@2.0.1)",
   "license": "MIT",
   "license": "MIT",
   "keywords": [
   "keywords": [
@@ -48,10 +48,7 @@
     "is-hidden": "^2.0.0",
     "is-hidden": "^2.0.0",
     "mdast-util-from-markdown": "^1.0.0",
     "mdast-util-from-markdown": "^1.0.0",
     "micromark": "^3.0.0",
     "micromark": "^3.0.0",
-    "micromark-build": "^1.0.0",
     "remark": "^14.0.0",
     "remark": "^14.0.0",
-    "remark-cli": "^10.0.0",
-    "remark-preset-wooorm": "^9.0.0",
     "rimraf": "^3.0.0",
     "rimraf": "^3.0.0",
     "tape": "^5.0.0",
     "tape": "^5.0.0",
     "to-vfile": "^7.0.0",
     "to-vfile": "^7.0.0",

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

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

+ 5 - 2
packages/remark-lsx/src/server/routes/lsx.js

@@ -1,3 +1,4 @@
+import escapeStringRegexp from 'escape-string-regexp';
 import createError, { isHttpError } from 'http-errors';
 import createError, { isHttpError } from 'http-errors';
 
 
 const { pathUtils, pagePathUtils, customTagUtils } = require('@growi/core');
 const { pathUtils, pagePathUtils, customTagUtils } = require('@growi/core');
@@ -103,14 +104,16 @@ class Lsx {
       throw createError(400, 'filter option require value in regular expression.');
       throw createError(400, 'filter option require value in regular expression.');
     }
     }
 
 
+    const pagePathForRegexp = escapeStringRegexp(addTrailingSlash(pagePath));
+
     let filterPath = '';
     let filterPath = '';
     try {
     try {
       if (optionsFilter.charAt(0) === '^') {
       if (optionsFilter.charAt(0) === '^') {
         // move '^' to the first of path
         // move '^' to the first of path
-        filterPath = new RegExp(`^${addTrailingSlash(pagePath)}${optionsFilter.slice(1, optionsFilter.length)}`);
+        filterPath = new RegExp(`^${pagePathForRegexp}${optionsFilter.slice(1, optionsFilter.length)}`);
       }
       }
       else {
       else {
-        filterPath = new RegExp(`^${addTrailingSlash(pagePath)}.*${optionsFilter}`);
+        filterPath = new RegExp(`^${pagePathForRegexp}.*${optionsFilter}`);
       }
       }
     }
     }
     catch (err) {
     catch (err) {

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

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

+ 1 - 1
packages/slack/package.json

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

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

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

+ 2 - 2
packages/ui/package.json

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

Plik diff jest za duży
+ 85 - 1272
yarn.lock


Niektóre pliki nie zostały wyświetlone z powodu dużej ilości zmienionych plików