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

Merge remote-tracking branch 'origin/master' into support/use-turborepo

Yuki Takei 3 лет назад
Родитель
Сommit
0a8899bc70
65 измененных файлов с 510 добавлено и 1593 удалено
  1. 45 1
      CHANGELOG.md
  2. 1 1
      apps/app/docker/README.md
  3. 1 1
      apps/app/next.config.js
  4. 13 12
      apps/app/package.json
  5. 5 8
      apps/app/resource/locales/en_US/sandbox.md
  6. 5 8
      apps/app/resource/locales/ja_JP/sandbox.md
  7. 5 8
      apps/app/resource/locales/zh_CN/sandbox.md
  8. 3 6
      apps/app/src/client/services/renderer/renderer.tsx
  9. 1 1
      apps/app/src/components/Admin/ExportArchiveDataPage.tsx
  10. 2 2
      apps/app/src/components/Navbar/GrowiContextualSubNavigation.tsx
  11. 1 1
      apps/app/src/components/Page/RevisionRenderer.tsx
  12. 1 1
      apps/app/src/components/PageAccessoriesModal.tsx
  13. 5 4
      apps/app/src/components/PageComment/CommentEditor.tsx
  14. 2 2
      apps/app/src/components/PageEditor.tsx
  15. 1 0
      apps/app/src/components/PageEditor/CodeMirrorEditor.jsx
  16. 0 1
      apps/app/src/components/PageEditor/CodeMirrorEditor.module.scss
  17. 15 12
      apps/app/src/components/PageEditor/CommentMentionHelper.ts
  18. 2 12
      apps/app/src/components/PageEditor/DrawioCommunicationHelper.ts
  19. 3 2
      apps/app/src/components/PageEditor/DrawioModal.tsx
  20. 1 1
      apps/app/src/components/PagePresentationModal.tsx
  21. 6 5
      apps/app/src/components/PageSideContents.tsx
  22. 1 1
      apps/app/src/components/ReactMarkdownComponents/CodeBlock.tsx
  23. 2 2
      apps/app/src/components/ReactMarkdownComponents/DrawioViewerWithEditButton.tsx
  24. 1 1
      apps/app/src/components/ReactMarkdownComponents/Header.tsx
  25. 1 1
      apps/app/src/components/ReactMarkdownComponents/TableWithEditButton.tsx
  26. 5 1
      apps/app/src/components/Script/DrawioViewerScript.tsx
  27. 35 37
      apps/app/src/components/Sidebar/PageTree/Item.tsx
  28. 0 3
      apps/app/src/components/Sidebar/PageTree/ItemsTree.tsx
  29. 1 1
      apps/app/src/components/TableOfContents.tsx
  30. 3 3
      apps/app/src/interfaces/renderer-options.ts
  31. 13 5
      apps/app/src/pages/[[...path]].page.tsx
  32. 22 5
      apps/app/src/server/routes/apiv3/pages.js
  33. 1 1
      apps/app/src/server/service/page.ts
  34. 5 5
      apps/app/src/server/service/search-delegator/elasticsearch.ts
  35. 5 5
      apps/app/src/server/service/slack-command-handler/create-page-service.js
  36. 3 4
      apps/app/src/server/service/slack-command-handler/keep.js
  37. 3 1
      apps/app/src/server/service/slack-command-handler/note.js
  38. 0 20
      apps/app/src/services/renderer/remark-plugins/table.ts
  39. 3 2
      apps/app/src/services/renderer/renderer.tsx
  40. 0 8
      apps/app/src/stores/context.tsx
  41. 8 0
      apps/app/src/stores/page.tsx
  42. 1 1
      apps/app/src/styles/bootstrap/_override.scss
  43. 86 51
      apps/app/test/cypress/integration/20-basic-features/20-basic-features--access-to-page.spec.ts
  44. 61 0
      apps/app/test/cypress/integration/20-basic-features/20-basic-features--username-mention.spec.ts
  45. 2 2
      apps/slackbot-proxy/package.json
  46. 1 1
      package.json
  47. 1 1
      packages/codemirror-textlint/package.json
  48. 1 1
      packages/core/package.json
  49. 1 1
      packages/hackmd/package.json
  50. 3 3
      packages/presentation/package.json
  51. 1 1
      packages/presentation/src/components/Slides.tsx
  52. 1 1
      packages/presentation/src/consts/index.ts
  53. 1 1
      packages/presentation/vite.config.ts
  54. 1 1
      packages/preset-themes/package.json
  55. 2 3
      packages/remark-drawio/package.json
  56. 3 1
      packages/remark-drawio/src/components/DrawioViewer.tsx
  57. 1 9
      packages/remark-drawio/src/services/renderer/remark-drawio.ts
  58. 16 31
      packages/remark-drawio/src/utils/embed.ts
  59. 2 4
      packages/remark-growi-directive/package.json
  60. 4 4
      packages/remark-lsx/package.json
  61. 14 5
      packages/remark-lsx/src/server/routes/lsx.ts
  62. 2 10
      packages/remark-lsx/src/services/renderer/lsx.ts
  63. 1 1
      packages/slack/package.json
  64. 2 2
      packages/ui/package.json
  65. 72 1263
      yarn.lock

+ 45 - 1
CHANGELOG.md

@@ -1,9 +1,53 @@
 # 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.*
 
+## [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
 
 ### 🚀 Improvement

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

@@ -10,7 +10,7 @@ GROWI Official docker image
 Supported tags and respective Dockerfile links
 ------------------------------------------------
 
-* [`6.0.9`, `6.0`, `6`, `latest` (Dockerfile)](https://github.com/weseek/growi/blob/v6.0.9/apps/app/docker/Dockerfile)
+* [`6.0.15`, `6.0`, `6`, `latest` (Dockerfile)](https://github.com/weseek/growi/blob/v6.0.15/apps/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)
 * [`4.5.23`, `4.5`, `4`, `latest` (Dockerfile)](https://github.com/weseek/growi/blob/v4.5.23/packages/app/docker/Dockerfile)

+ 1 - 1
apps/app/next.config.js

@@ -16,7 +16,7 @@ const getTranspilePackages = () => {
 
   const packages = [
     // listing ESM packages until experimental.esmExternals works correctly to avoid ERR_REQUIRE_ESM
-    'react-markdown',
+    'react-markdown-customkeyprop',
     'unified',
     'markdown-table',
     'character-entities-html4',

+ 13 - 12
apps/app/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/app",
-  "version": "6.0.11-RC.0",
+  "version": "6.0.16-RC.0",
   "license": "MIT",
   "scripts": {
     "//// for production": "",
@@ -61,14 +61,14 @@
     "@elastic/elasticsearch7": "npm:@elastic/elasticsearch@^7.17.0",
     "@godaddy/terminus": "^4.9.0",
     "@google-cloud/storage": "^5.8.5",
-    "@growi/codemirror-textlint": "^6.0.11-RC.0",
-    "@growi/core": "^6.0.11-RC.0",
-    "@growi/hackmd": "^6.0.11-RC.0",
-    "@growi/preset-themes": "^6.0.11-RC.0",
-    "@growi/remark-drawio": "^6.0.11-RC.0",
-    "@growi/remark-growi-directive": "^6.0.11-RC.0",
-    "@growi/remark-lsx": "^6.0.11-RC.0",
-    "@growi/slack": "^6.0.11-RC.0",
+    "@growi/codemirror-textlint": "^6.0.16-RC.0",
+    "@growi/core": "^6.0.16-RC.0",
+    "@growi/hackmd": "^6.0.16-RC.0",
+    "@growi/preset-themes": "^6.0.16-RC.0",
+    "@growi/remark-drawio": "^6.0.16-RC.0",
+    "@growi/remark-growi-directive": "^6.0.16-RC.0",
+    "@growi/remark-lsx": "^6.0.16-RC.0",
+    "@growi/slack": "^6.0.16-RC.0",
     "@promster/express": "^7.0.6",
     "@promster/server": "^7.0.8",
     "@slack/web-api": "^6.2.4",
@@ -158,7 +158,7 @@
     "react-error-boundary": "^3.1.4",
     "react-i18next": "^12.2.0",
     "react-image-crop": "^8.3.0",
-    "react-markdown": "^8.0.3",
+    "react-markdown-customkeyprop": "^8.0.6-customkeyprop.0",
     "react-multiline-clamp": "^2.0.0",
     "react-scroll": "^1.8.7",
     "react-syntax-highlighter": "^15.5.0",
@@ -176,6 +176,7 @@
     "remark-emoji": "^3.0.2",
     "remark-gfm": "^3.0.1",
     "remark-math": "^5.1.1",
+    "remark-toc": "^8.0.1",
     "remark-wiki-link": "^1.0.4",
     "socket.io": "^4.2.0",
     "stream-to-promise": "^3.0.0",
@@ -200,8 +201,8 @@
     "handsontable": "v7.0.0 or above is no loger MIT lisence."
   },
   "devDependencies": {
-    "@growi/presentation": "^6.0.11-RC.0",
-    "@growi/ui": "^6.0.11-RC.0",
+    "@growi/presentation": "^6.0.16-RC.0",
+    "@growi/ui": "^6.0.16-RC.0",
     "@handsontable/react": "=2.1.0",
     "@icon/themify-icons": "1.0.1-alpha.3",
     "@next/bundle-analyzer": "^13.2.3",

+ 5 - 8
apps/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
 

+ 5 - 8
apps/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
 

+ 5 - 8
apps/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
 

+ 3 - 6
apps/app/src/client/services/renderer/renderer.tsx

@@ -22,9 +22,8 @@ import type { RendererOptions } from '~/interfaces/renderer-options';
 import type { RendererConfig } from '~/interfaces/services/renderer';
 import * as addLineNumberAttribute from '~/services/renderer/rehype-plugins/add-line-number-attribute';
 import * as keywordHighlighter from '~/services/renderer/rehype-plugins/keyword-highlighter';
-import * as toc from '~/services/renderer/rehype-plugins/relocate-toc';
+import * as relocateToc from '~/services/renderer/rehype-plugins/relocate-toc';
 import * as plantuml from '~/services/renderer/remark-plugins/plantuml';
-import * as table from '~/services/renderer/remark-plugins/table';
 import * as xsvToTable from '~/services/renderer/remark-plugins/xsv-to-table';
 import {
   commonSanitizeOption, generateCommonOptions, injectCustomSanitizeOption, verifySanitizePlugin,
@@ -81,7 +80,7 @@ export const generateViewOptions = (
     [lsxGrowiPlugin.rehypePlugin, { pagePath, isSharedPage: config.isSharedPage }],
     rehypeSanitizePlugin,
     katex,
-    [toc.rehypePluginStore, { storeTocNode }],
+    [relocateToc.rehypePluginStore, { storeTocNode }],
   );
 
   // add components
@@ -125,7 +124,7 @@ export const generateTocOptions = (config: RendererConfig, tocNode: HtmlElementN
 
   // add rehype plugins
   rehypePlugins.push(
-    [toc.rehypePluginRestore, { tocNode }],
+    [relocateToc.rehypePluginRestore, { tocNode }],
     rehypeSanitizePlugin,
   );
 
@@ -153,7 +152,6 @@ export const generateSimpleViewOptions = (
     drawioPlugin.remarkPlugin,
     xsvToTable.remarkPlugin,
     lsxGrowiPlugin.remarkPlugin,
-    // table.remarkPlugin,
   );
 
   const isEnabledLinebreaks = overrideIsEnabledLinebreaks ?? config.isEnabledLinebreaks;
@@ -220,7 +218,6 @@ export const generatePreviewOptions = (config: RendererConfig, pagePath: string)
     drawioPlugin.remarkPlugin,
     xsvToTable.remarkPlugin,
     lsxGrowiPlugin.remarkPlugin,
-    // table.remarkPlugin,
   );
   if (config.isEnabledLinebreaks) {
     remarkPlugins.push(breaks);

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

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

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

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

+ 1 - 1
apps/app/src/components/Page/RevisionRenderer.tsx

@@ -1,7 +1,7 @@
 import React from 'react';
 
 import { ErrorBoundary, FallbackProps } from 'react-error-boundary';
-import ReactMarkdown from 'react-markdown';
+import ReactMarkdown from 'react-markdown-customkeyprop';
 
 import type { RendererOptions } from '~/interfaces/renderer-options';
 import loggerFactory from '~/utils/logger';

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

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

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

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

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

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

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

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

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

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

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

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

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

@@ -1,5 +1,3 @@
-import { extractCodeFromMxfile } from '@growi/remark-drawio';
-
 import loggerFactory from '~/utils/logger';
 
 
@@ -8,6 +6,7 @@ const logger = loggerFactory('growi:cli:DrawioCommunicationHelper');
 export type DrawioConfig = {
   css: string,
   customFonts: string[],
+  compressXml: boolean,
 }
 
 export type DrawioCommunicationCallbackOptions = {
@@ -63,16 +62,7 @@ export class DrawioCommunicationHelper {
 
     // restore diagram data
     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;
     }
 

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

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

+ 1 - 1
apps/app/src/components/PagePresentationModal.tsx

@@ -3,7 +3,7 @@ import React, { useCallback } from 'react';
 import type { PresentationProps } from '@growi/presentation';
 import { useFullScreen } from '@growi/ui/dist/utils';
 import dynamic from 'next/dynamic';
-import type { ReactMarkdownOptions } from 'react-markdown/lib/react-markdown';
+import type { ReactMarkdownOptions } from 'react-markdown-customkeyprop/lib/react-markdown';
 import {
   Modal, ModalBody,
 } from 'reactstrap';

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

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

+ 1 - 1
apps/app/src/components/ReactMarkdownComponents/CodeBlock.tsx

@@ -1,6 +1,6 @@
 import { ReactNode } from 'react';
 
-import type { CodeComponent } from 'react-markdown/lib/ast-to-react';
+import type { CodeComponent } from 'react-markdown-customkeyprop/lib/ast-to-react';
 import { PrismAsyncLight } from 'react-syntax-highlighter';
 import { oneDark } from 'react-syntax-highlighter/dist/cjs/styles/prism';
 

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

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

+ 1 - 1
apps/app/src/components/ReactMarkdownComponents/Header.tsx

@@ -3,7 +3,7 @@ import { useCallback, useEffect, useState } from 'react';
 import EventEmitter from 'events';
 
 import { useRouter } from 'next/router';
-import { Element } from 'react-markdown/lib/rehype-filter';
+import { Element } from 'react-markdown-customkeyprop/lib/rehype-filter';
 
 import { useIsGuestUser, useIsSharedUser, useShareLinkId } from '~/stores/context';
 import loggerFactory from '~/utils/logger';

+ 1 - 1
apps/app/src/components/ReactMarkdownComponents/TableWithEditButton.tsx

@@ -2,7 +2,7 @@ import React, { useCallback } from 'react';
 
 import EventEmitter from 'events';
 
-import { Element } from 'react-markdown/lib/rehype-filter';
+import { Element } from 'react-markdown-customkeyprop/lib/rehype-filter';
 
 import { useIsGuestUser, useIsSharedUser, useShareLinkId } from '~/stores/context';
 

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

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

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

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

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

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

+ 1 - 1
apps/app/src/components/TableOfContents.tsx

@@ -1,7 +1,7 @@
 import React, { useCallback } from 'react';
 
 import { pagePathUtils } from '@growi/core';
-import ReactMarkdown from 'react-markdown';
+import ReactMarkdown from 'react-markdown-customkeyprop';
 
 import { useCurrentPagePath } from '~/stores/page';
 import { useTocOptions } from '~/stores/renderer';

+ 3 - 3
apps/app/src/interfaces/renderer-options.ts

@@ -1,8 +1,8 @@
 import type { ComponentType } from 'react';
 
-import type { SpecialComponents } from 'react-markdown/lib/ast-to-react';
-import type { NormalComponents } from 'react-markdown/lib/complex-types';
-import type { ReactMarkdownOptions } from 'react-markdown/lib/react-markdown';
+import type { SpecialComponents } from 'react-markdown-customkeyprop/lib/ast-to-react';
+import type { NormalComponents } from 'react-markdown-customkeyprop/lib/complex-types';
+import type { ReactMarkdownOptions } from 'react-markdown-customkeyprop/lib/react-markdown';
 import type { PluggableList } from 'unified';
 
 export type RendererOptions = Omit<ReactMarkdownOptions, 'remarkPlugins' | 'rehypePlugins' | 'components' | 'children'> & {

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

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

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

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

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

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

+ 5 - 5
apps/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;
 
     if (terms == null) {
       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();
     this.appendCriteriaForQueryString(query, terms);
 

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

@@ -11,17 +11,17 @@ class CreatePageService {
     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);
 
     // sanitize path
     const sanitizedPath = this.crowi.xss.process(path);
     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
     const growiUri = this.crowi.appService.getSiteUrl();

+ 3 - 4
apps/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 user = await User.findUserBySlackMemberId(payload.user.id);
 
-    const userId = user != null ? user._id : null;
     // validate form
     const { path, oldest, newest } = await this.keepValidateForm(client, payload, interactionPayloadAccessor);
     // get messages
@@ -45,7 +44,7 @@ module.exports = (crowi) => {
 
     const contentsBody = cleanedContents.join('');
     // 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) {
@@ -197,8 +196,8 @@ module.exports = (crowi) => {
     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
     // send preview to dm

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

@@ -17,6 +17,7 @@ module.exports = (crowi) => {
     type: 'conversations_select',
     default_to_current_conversation: true,
   };
+  const { User } = crowi.models;
 
   handler.handleCommand = async(growiCommand, client, body, respondUtil) => {
     await respondUtil.respond({
@@ -44,12 +45,13 @@ module.exports = (crowi) => {
   };
 
   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 contentsBody = interactionPayloadAccessor.getStateValues()?.contents.contents_input.value;
     if (path == null || contentsBody == null) {
       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();
   };
 

+ 0 - 20
apps/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;
-      }
-    });
-  };
-};

+ 3 - 2
apps/app/src/services/renderer/renderer.tsx

@@ -8,6 +8,7 @@ import breaks from 'remark-breaks';
 import emoji from 'remark-emoji';
 import gfm from 'remark-gfm';
 import math from 'remark-math';
+import toc from 'remark-toc';
 import deepmerge from 'ts-deepmerge';
 import type { Pluggable, PluginTuple } from 'unified';
 
@@ -40,7 +41,7 @@ const baseSanitizeSchema = {
     iframe: ['allow', 'referrerpolicy', 'sandbox', 'src', 'srcdoc'],
     // 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/
-    '*': ['class', 'className', 'style', 'data*'],
+    '*': ['key', 'class', 'className', 'style', 'data*'],
   },
 };
 
@@ -92,6 +93,7 @@ export const verifySanitizePlugin = (options: RendererOptions, shouldBeTheLastIt
 export const generateCommonOptions = (pagePath: string|undefined): RendererOptions => {
   return {
     remarkPlugins: [
+      [toc, { maxDepth: 3, tight: true, prefix: 'mdcont-' }],
       gfm,
       emoji,
       pukiwikiLikeLinker,
@@ -129,7 +131,6 @@ export const generateSSRViewOptions = (
   remarkPlugins.push(
     math,
     xsvToTable.remarkPlugin,
-    // table.remarkPlugin,
   );
 
   const isEnabledLinebreaks = config.isEnabledLinebreaks;

+ 0 - 8
apps/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 });
 };
 
-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> => {
   return useContextSWR<boolean, Error>('isSharedUser', initialData);
 };

+ 8 - 0
apps/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 });
 };
 
+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> => {
   const key = 'currentPage';
 

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

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

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

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

+ 61 - 0
apps/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`) });
+  });
+
+});

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

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

+ 1 - 1
package.json

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

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

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

+ 1 - 1
packages/core/package.json

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

+ 1 - 1
packages/hackmd/package.json

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

+ 3 - 3
packages/presentation/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/presentation",
-  "version": "6.0.11-RC.0",
+  "version": "6.0.16-RC.0",
   "description": "GROWI plugin for presentation",
   "license": "MIT",
   "keywords": ["growi", "growi-plugin"],
@@ -19,7 +19,7 @@
     "lint": "run-p lint:*"
   },
   "dependencies": {
-    "@growi/core": "^6.0.11-RC.0"
+    "@growi/core": "^6.0.16-RC.0"
   },
   "devDependencies": {
     "@marp-team/marp-core": "^3.4.2",
@@ -31,6 +31,6 @@
     "next": "~12.2",
     "react": "^18.2.0",
     "react-dom": "^18.2.0",
-    "react-markdown": "^8.0.3"
+    "react-markdown-customkeyprop": "^8.0.6-customkeyprop.0"
   }
 }

+ 1 - 1
packages/presentation/src/components/Slides.tsx

@@ -3,7 +3,7 @@ import React from 'react';
 import { Marp } from '@marp-team/marp-core';
 import { Element } from '@marp-team/marpit';
 import Head from 'next/head';
-import ReactMarkdown from 'react-markdown';
+import { ReactMarkdown } from 'react-markdown-customkeyprop/lib/react-markdown';
 
 import type { PresentationOptions } from '../consts';
 import * as extractSections from '../services/renderer/extract-sections';

+ 1 - 1
packages/presentation/src/consts/index.ts

@@ -1,4 +1,4 @@
-import type { ReactMarkdownOptions } from 'react-markdown/lib/react-markdown';
+import type { ReactMarkdownOptions } from 'react-markdown-customkeyprop/lib/react-markdown';
 import type { Options as RevealOptions } from 'reveal.js';
 
 export type PresentationOptions = {

+ 1 - 1
packages/presentation/vite.config.ts

@@ -19,7 +19,7 @@ export default defineConfig({
       external: [
         'react', 'react-dom',
         'next/head',
-        'react-markdown',
+        'react-markdown-customkeyprop',
         '@marp-team/marp-core', '@marp-team/marpit',
         'reveal.js',
       ],

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

@@ -1,7 +1,7 @@
 {
   "name": "@growi/preset-themes",
   "description": "GROWI preset themes",
-  "version": "6.0.11-RC.0",
+  "version": "6.0.16-RC.0",
   "license": "MIT",
   "main": "dist/libs/preset-themes.umd.js",
   "module": "dist/libs/preset-themes.mjs",

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

@@ -1,6 +1,6 @@
 {
   "name": "@growi/remark-drawio",
-  "version": "6.0.11-RC.0",
+  "version": "6.0.16-RC.0",
   "description": "remark plugin to draw diagrams with draw.io (diagrams.net)",
   "license": "MIT",
   "keywords": [
@@ -23,8 +23,7 @@
     "lint": "run-p lint:*"
   },
   "dependencies": {
-    "pako": "^2.1.0",
-    "xmldoc": "^1.2.0"
+    "pako": "^2.1.0"
   },
   "devDependencies": {
     "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
-      ? children.map(e => e?.toString()).join('')
+      ? children
+        .filter(elem => (typeof elem === 'string')) // omit non-string elements (e.g. br element generated by line-breaks option)
+        .join('')
       : children.toString();
 
     let mxgraphData;

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

@@ -21,6 +21,7 @@ function rewriteNode(node: Node, index: number) {
     diagramIndex: index,
     bol: node.position?.start.line,
     eol: node.position?.end.line,
+    key: `drawio-${index}`,
   };
 }
 
@@ -30,15 +31,6 @@ export const remarkPlugin: Plugin = function() {
       if (node.type === 'code') {
         if (isDrawioBlock(node.lang)) {
           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
 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;
 
-  try {
-    data = extractCodeFromMxfile(data);
-  }
-  catch (e) {
-    // ignore
-  }
-
   try {
     data = Buffer.from(data, 'base64').toString('binary');
   }
@@ -65,27 +57,20 @@ export const generateMxgraphData = (code: string): string => {
     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
   const mxGraphData = {
     editable: false,

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

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

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

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

+ 14 - 5
packages/remark-lsx/src/server/routes/lsx.ts

@@ -1,5 +1,7 @@
+
 import { OptionParser } from '@growi/core/dist/plugin';
 import { pathUtils, pagePathUtils } from '@growi/core/dist/utils';
+import escapeStringRegexp from 'escape-string-regexp';
 import createError, { isHttpError } from 'http-errors';
 
 
@@ -111,13 +113,20 @@ class Lsx {
       throw createError(400, 'filter option require value in regular expression.');
     }
 
+    const pagePathForRegexp = escapeStringRegexp(addTrailingSlash(pagePath));
+
     let filterPath;
-    if (optionsFilter.charAt(0) === '^') {
-      // move '^' to the first of path
-      filterPath = new RegExp(`^${addTrailingSlash(pagePath)}${optionsFilter.slice(1, optionsFilter.length)}`);
+    try {
+      if (optionsFilter.charAt(0) === '^') {
+        // move '^' to the first of path
+        filterPath = new RegExp(`^${pagePathForRegexp}${optionsFilter.slice(1, optionsFilter.length)}`);
+      }
+      else {
+        filterPath = new RegExp(`^${pagePathForRegexp}.*${optionsFilter}`);
+      }
     }
-    else {
-      filterPath = new RegExp(`^${addTrailingSlash(pagePath)}.*${optionsFilter}`);
+    catch (err) {
+      throw createError(400, err);
     }
 
     if (isExceptFilter) {

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

@@ -18,7 +18,7 @@ type DirectiveAttributes = Record<string, string>
 
 export const remarkPlugin: Plugin = function() {
   return (tree) => {
-    visit(tree, (node) => {
+    visit(tree, (node, index) => {
       if (node.type === remarkGrowiDirectivePluginType.Text || node.type === remarkGrowiDirectivePluginType.Leaf) {
         if (typeof node.name !== 'string') {
           return;
@@ -46,18 +46,10 @@ export const remarkPlugin: Plugin = function() {
             }
           }
         }
+        attributes.key = `lsx-${index}`;
 
         data.hName = 'lsx';
         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",
-  "version": "6.0.11-RC.0",
+  "version": "6.0.16-RC.0",
   "license": "MIT",
   "main": "dist/slack.umd.js",
   "module": "dist/slack.mjs",

+ 2 - 2
packages/ui/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/ui",
-  "version": "6.0.11-RC.0",
+  "version": "6.0.16-RC.0",
   "description": "GROWI UI Libraries",
   "license": "MIT",
   "keywords": ["growi"],
@@ -17,7 +17,7 @@
     "lint": "npm-run-all -p lint:*"
   },
   "dependencies": {
-    "@growi/core": "^6.0.11-RC.0"
+    "@growi/core": "^6.0.16-RC.0"
   },
   "devDependencies": {
     "react": "^18.2.0"

Разница между файлами не показана из-за своего большого размера
+ 72 - 1263
yarn.lock


Некоторые файлы не были показаны из-за большого количества измененных файлов