2
0
Эх сурвалжийг харах

Merge branch 'master' into imprv/105915-imprement-updateStateAfterSave

cao 3 жил өмнө
parent
commit
89c035b6e6
37 өөрчлөгдсөн 250 нэмэгдсэн , 195 устгасан
  1. 1 1
      lerna.json
  2. 1 1
      package.json
  3. 0 5
      packages/app/_obsolete/src/client/services/ContextExtractor.tsx
  4. 8 8
      packages/app/package.json
  5. 2 1
      packages/app/public/static/locales/en_US/translation.json
  6. 2 1
      packages/app/public/static/locales/ja_JP/translation.json
  7. 2 1
      packages/app/public/static/locales/zh_CN/translation.json
  8. 18 20
      packages/app/src/components/Admin/Common/AdminNavigation.jsx
  9. 1 1
      packages/app/src/components/Admin/ElasticsearchManagement/ElasticsearchManagement.tsx
  10. 4 2
      packages/app/src/components/LoginForm.tsx
  11. 8 5
      packages/app/src/components/Page.tsx
  12. 14 7
      packages/app/src/components/PageEditor.tsx
  13. 2 2
      packages/app/src/components/PageEditor/CodeMirrorEditor.jsx
  14. 2 1
      packages/app/src/components/PageEditor/Editor.tsx
  15. 4 4
      packages/app/src/components/PageEditor/OptionsSelector.tsx
  16. 1 0
      packages/app/src/components/PrivateLegacyPages.tsx
  17. 1 6
      packages/app/src/components/ReactMarkdownComponents/CodeBlock.module.scss
  18. 9 7
      packages/app/src/components/ReactMarkdownComponents/CodeBlock.tsx
  19. 10 8
      packages/app/src/pages/[[...path]].page.tsx
  20. 75 31
      packages/app/src/pages/share/[[...path]].page.tsx
  21. 1 1
      packages/app/src/stores/context.tsx
  22. 4 5
      packages/app/src/stores/editor.tsx
  23. 34 26
      packages/app/src/stores/ui.tsx
  24. 5 5
      packages/app/src/styles/atoms/_code.scss
  25. 0 15
      packages/app/src/styles/atoms/_mixins.scss
  26. 5 0
      packages/app/src/styles/atoms/mixins/_code.scss
  27. 4 9
      packages/app/src/styles/theme/_apply-colors.scss
  28. 1 1
      packages/codemirror-textlint/package.json
  29. 1 1
      packages/core/package.json
  30. 1 1
      packages/plugin-attachment-refs/package.json
  31. 4 4
      packages/plugin-lsx/package.json
  32. 1 1
      packages/remark-growi-plugin/package.json
  33. 1 1
      packages/slack/package.json
  34. 2 2
      packages/slackbot-proxy/package.json
  35. 2 2
      packages/ui/package.json
  36. 10 1
      packages/ui/src/utils/browser-utils.ts
  37. 9 8
      yarn.lock

+ 1 - 1
lerna.json

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

+ 1 - 1
package.json

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

+ 0 - 5
packages/app/_obsolete/src/client/services/ContextExtractor.tsx

@@ -167,11 +167,6 @@ const ContextExtractorOnce: FC = () => {
   usePreferDrawerModeOnEditByUser();
   usePreferDrawerModeOnEditByUser();
   useIsDeviceSmallerThanMd();
   useIsDeviceSmallerThanMd();
 
 
-  // Navigation
-  usePreferDrawerModeByUser();
-  usePreferDrawerModeOnEditByUser();
-  useIsDeviceSmallerThanMd();
-
   // Editor
   // Editor
   // useSelectedGrant(grant);
   // useSelectedGrant(grant);
   // useSelectedGrantGroupId(grantGroupId);
   // useSelectedGrantGroupId(grantGroupId);

+ 8 - 8
packages/app/package.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "@growi/app",
   "name": "@growi/app",
-  "version": "6.0.0-RC.1",
+  "version": "6.0.0-RC.3",
   "license": "MIT",
   "license": "MIT",
   "scripts": {
   "scripts": {
     "//// for production": "",
     "//// for production": "",
@@ -64,11 +64,11 @@
     "@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.0-RC.1",
-    "@growi/core": "^6.0.0-RC.1",
-    "@growi/plugin-attachment-refs": "^6.0.0-RC.1",
-    "@growi/plugin-lsx": "^6.0.0-RC.1",
-    "@growi/slack": "^6.0.0-RC.1",
+    "@growi/codemirror-textlint": "^6.0.0-RC.3",
+    "@growi/core": "^6.0.0-RC.3",
+    "@growi/plugin-attachment-refs": "^6.0.0-RC.3",
+    "@growi/plugin-lsx": "^6.0.0-RC.3",
+    "@growi/slack": "^6.0.0-RC.3",
     "@promster/express": "^7.0.2",
     "@promster/express": "^7.0.2",
     "@promster/server": "^7.0.4",
     "@promster/server": "^7.0.4",
     "@slack/events-api": "^3.0.0",
     "@slack/events-api": "^3.0.0",
@@ -138,7 +138,7 @@
     "nodemailer-ses-transport": "~1.5.0",
     "nodemailer-ses-transport": "~1.5.0",
     "openid-client": "^5.1.2",
     "openid-client": "^5.1.2",
     "p-retry": "^4.0.0",
     "p-retry": "^4.0.0",
-    "passport": "^0.5.0",
+    "passport": "^0.6.0",
     "passport-github": "^1.1.0",
     "passport-github": "^1.1.0",
     "passport-google-oauth20": "^2.0.0",
     "passport-google-oauth20": "^2.0.0",
     "passport-http": "^0.3.0",
     "passport-http": "^0.3.0",
@@ -203,7 +203,7 @@
   },
   },
   "devDependencies": {
   "devDependencies": {
     "@alienfast/i18next-loader": "^1.1.4",
     "@alienfast/i18next-loader": "^1.1.4",
-    "@growi/ui": "^6.0.0-RC.1",
+    "@growi/ui": "^6.0.0-RC.3",
     "@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": "^12.2.3",
     "@next/bundle-analyzer": "^12.2.3",

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

@@ -540,7 +540,8 @@
     "issue_share_link": "Succeeded to issue new share link",
     "issue_share_link": "Succeeded to issue new share link",
     "remove_share_link": "Succeeded to remove {{count}} share links",
     "remove_share_link": "Succeeded to remove {{count}} share links",
     "switch_disable_link_sharing_success": "Succeeded to update share link setting",
     "switch_disable_link_sharing_success": "Succeeded to update share link setting",
-    "failed_to_reset_password":"Failed to reset password"
+    "failed_to_reset_password":"Failed to reset password",
+    "save_succeeded": "Saved successfully"
   },
   },
   "template": {
   "template": {
     "modal_label": {
     "modal_label": {

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

@@ -531,7 +531,8 @@
     "issue_share_link": "共有リンクを作成しました",
     "issue_share_link": "共有リンクを作成しました",
     "remove_share_link": "共有リンクを{{count}}件削除しました",
     "remove_share_link": "共有リンクを{{count}}件削除しました",
     "switch_disable_link_sharing_success": "共有リンクの設定を変更しました",
     "switch_disable_link_sharing_success": "共有リンクの設定を変更しました",
-    "failed_to_reset_password":"パスワードのリセットに失敗しました"
+    "failed_to_reset_password":"パスワードのリセットに失敗しました",
+    "save_succeeded": "保存に成功しました"
   },
   },
   "template": {
   "template": {
     "modal_label": {
     "modal_label": {

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

@@ -510,7 +510,8 @@
 		"remove_user_success": "Succeeded to removing {{username}} ",
 		"remove_user_success": "Succeeded to removing {{username}} ",
     "remove_external_user_success": "Succeeded to remove {{accountId}} ",
     "remove_external_user_success": "Succeeded to remove {{accountId}} ",
     "switch_disable_link_sharing_success": "成功更新分享链接设置",
     "switch_disable_link_sharing_success": "成功更新分享链接设置",
-    "failed_to_reset_password":"Failed to reset password"
+    "failed_to_reset_password":"Failed to reset password",
+    "save_succeeded": "已成功保存"
   },
   },
 	"template": {
 	"template": {
 		"modal_label": {
 		"modal_label": {

+ 18 - 20
packages/app/src/components/Admin/Common/AdminNavigation.jsx

@@ -22,25 +22,23 @@ const AdminNavigation = (props) => {
   // eslint-disable-next-line react/prop-types
   // eslint-disable-next-line react/prop-types
   const MenuLabel = ({ menu }) => {
   const MenuLabel = ({ menu }) => {
     switch (menu) {
     switch (menu) {
-      /* eslint-disable no-multi-spaces */
-      case 'app':                      return <><i className="icon-fw icon-settings"></i>        { t('app_settings') }</>;
-      case 'security':                 return <><i className="icon-fw icon-shield"></i>          { t('security_settings.security_settings') }</>;
-      case 'markdown':                 return <><i className="icon-fw icon-note"></i>            { t('markdown_settings.markdown_settings') }</>;
-      case 'customize':                return <><i className="icon-fw icon-wrench"></i>          { t('customize_settings.customize_settings') }</>;
-      case 'importer':                 return <><i className="icon-fw icon-cloud-upload"></i>    { t('importer_management.import_data') }</>;
-      case 'export':                   return <><i className="icon-fw icon-cloud-download"></i>  { t('export_archive_data') }</>;
-      case 'notification':             return <><i className="icon-fw icon-bell"></i>            { t('external_notification.external_notification')}</>;
-      case 'slack-integration':        return <><i className="icon-fw icon-shuffle"></i>         { t('slack_integration.slack_integration') }</>;
-      case 'slack-integration-legacy': return <><i className="icon-fw icon-shuffle"></i>         { t('slack_integration_legacy.slack_integration_legacy')}</>;
-      case 'users':                    return <><i className="icon-fw icon-user"></i>            { t('user_management.user_management') }</>;
-      case 'user-groups':              return <><i className="icon-fw icon-people"></i>          { t('user_group_management.user_group_management') }</>;
-      case 'search':                   return <><i className="icon-fw icon-magnifier"></i>
-        { t('full_text_search_management.full_text_search_management') }</>;
-      // TODO: Consider where to place the "AuditLog"
-      case 'audit-log':                return <><i className="icon-fw icon-feed"></i>            { t('audit_log_management.audit_log')}</>;
-      case 'cloud':                    return <><i className="icon-fw icon-share-alt"></i>       { t('to_cloud_settings')} </>;
-      default:                         return <><i className="icon-fw icon-home"></i>            { t('wiki_management_home_page') }</>;
-      /* eslint-enable no-multi-spaces */
+      /* eslint-disable no-multi-spaces, max-len */
+      case 'app':                      return <><i className="mr-1 icon-fw icon-settings"></i>{        t('app_settings') }</>;
+      case 'security':                 return <><i className="mr-1 icon-fw icon-shield"></i>{          t('security_settings.security_settings') }</>;
+      case 'markdown':                 return <><i className="mr-1 icon-fw icon-note"></i>{            t('markdown_settings.markdown_settings') }</>;
+      case 'customize':                return <><i className="mr-1 icon-fw icon-wrench"></i>{          t('customize_settings.customize_settings') }</>;
+      case 'importer':                 return <><i className="mr-1 icon-fw icon-cloud-upload"></i>{    t('importer_management.import_data') }</>;
+      case 'export':                   return <><i className="mr-1 icon-fw icon-cloud-download"></i>{  t('export_management.export_archive_data') }</>;
+      case 'notification':             return <><i className="mr-1 icon-fw icon-bell"></i>{            t('external_notification.external_notification')}</>;
+      case 'slack-integration':        return <><i className="mr-1 icon-fw icon-shuffle"></i>{         t('slack_integration.slack_integration') }</>;
+      case 'slack-integration-legacy': return <><i className="mr-1 icon-fw icon-shuffle"></i>{         t('slack_integration_legacy.slack_integration_legacy')}</>;
+      case 'users':                    return <><i className="mr-1 icon-fw icon-user"></i>{            t('user_management.user_management') }</>;
+      case 'user-groups':              return <><i className="mr-1 icon-fw icon-people"></i>{          t('user_group_management.user_group_management') }</>;
+      case 'search':                   return <><i className="mr-1 icon-fw icon-magnifier"></i>{       t('full_text_search_management.full_text_search_management') }</>;
+      case 'audit-log':                return <><i className="mr-1 icon-fw icon-feed"></i>{            t('audit_log_management.audit_log')}</>;
+      case 'cloud':                    return <><i className="mr-1 icon-fw icon-share-alt"></i>{       t('to_cloud_settings')} </>;
+      default:                         return <><i className="mr-1 icon-fw icon-home"></i>{            t('wiki_management_home_page') }</>;
+      /* eslint-enable no-multi-spaces, max-len */
     }
     }
   };
   };
 
 
@@ -92,8 +90,8 @@ const AdminNavigation = (props) => {
         <MenuLink menu="slack-integration-legacy" isListGroupItems isActive={isActiveMenu('/slack-integration-legacy')} />
         <MenuLink menu="slack-integration-legacy" isListGroupItems isActive={isActiveMenu('/slack-integration-legacy')} />
         <MenuLink menu="users"        isListGroupItems isActive={isActiveMenu('/users')} />
         <MenuLink menu="users"        isListGroupItems isActive={isActiveMenu('/users')} />
         <MenuLink menu="user-groups"  isListGroupItems isActive={isActiveMenu('/user-groups')} />
         <MenuLink menu="user-groups"  isListGroupItems isActive={isActiveMenu('/user-groups')} />
-        <MenuLink menu="search"       isListGroupItems isActive={isActiveMenu('/search')} />
         <MenuLink menu="audit-log"    isListGroupItems isActive={isActiveMenu('/audit-log')} />
         <MenuLink menu="audit-log"    isListGroupItems isActive={isActiveMenu('/audit-log')} />
+        <MenuLink menu="search"       isListGroupItems isActive={isActiveMenu('/search')} />
         {/* {growiCloudUri != null && growiAppIdForGrowiCloud != null
         {/* {growiCloudUri != null && growiAppIdForGrowiCloud != null
           && (
           && (
             <a
             <a

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

@@ -146,7 +146,7 @@ const ElasticsearchManagement = () => {
 
 
   return (
   return (
     <>
     <>
-      <div className="row">
+      <div data-testid="admin-full-text-search" className="row">
         <div className="col-md-12">
         <div className="col-md-12">
           <StatusTable
           <StatusTable
             isInitialized={isInitialized}
             isInitialized={isInitialized}

+ 4 - 2
packages/app/src/components/LoginForm.tsx

@@ -9,6 +9,7 @@ import ReactCardFlip from 'react-card-flip';
 import { apiv3Post } from '~/client/util/apiv3-client';
 import { apiv3Post } from '~/client/util/apiv3-client';
 import { LoginErrorCode } from '~/interfaces/errors/login-error';
 import { LoginErrorCode } from '~/interfaces/errors/login-error';
 import { IErrorV3 } from '~/interfaces/errors/v3-error';
 import { IErrorV3 } from '~/interfaces/errors/v3-error';
+import { toArrayIfNot } from '~/utils/array-utils';
 
 
 type LoginFormProps = {
 type LoginFormProps = {
   username?: string,
   username?: string,
@@ -80,10 +81,11 @@ export const LoginForm = (props: LoginFormProps): JSX.Element => {
     try {
     try {
       const res = await apiv3Post('/login', { loginForm });
       const res = await apiv3Post('/login', { loginForm });
       const { redirectTo } = res.data;
       const { redirectTo } = res.data;
-      router.push(redirectTo);
+      router.push(redirectTo ?? '/');
     }
     }
     catch (err) {
     catch (err) {
-      setLoginErrors(err);
+      const errs = toArrayIfNot(err);
+      setLoginErrors(errs);
     }
     }
     return;
     return;
 
 

+ 8 - 5
packages/app/src/components/Page.tsx

@@ -10,6 +10,7 @@ import dynamic from 'next/dynamic';
 
 
 import { HtmlElementNode } from 'rehype-toc';
 import { HtmlElementNode } from 'rehype-toc';
 
 
+import { toastSuccess, toastError } from '~/client/util/apiNotification';
 import { getOptionsToSave } from '~/client/util/editor';
 import { getOptionsToSave } from '~/client/util/editor';
 import {
 import {
   useIsGuestUser, useCurrentPageTocNode, useShareLinkId,
   useIsGuestUser, useCurrentPageTocNode, useShareLinkId,
@@ -124,11 +125,12 @@ class PageSubstance extends React.Component<PageSubstanceProps> {
     //   const { page, tags } = await pageContainer.save(newMarkdown, this.props.editorMode, optionsToSave);
     //   const { page, tags } = await pageContainer.save(newMarkdown, this.props.editorMode, optionsToSave);
     //   logger.debug('success to save');
     //   logger.debug('success to save');
 
 
-    //   pageContainer.showSuccessToastr();
+    // // Todo: add translation
+    // toastSuccess(t(''));
     // }
     // }
     // catch (error) {
     // catch (error) {
     //   logger.error('failed to save', error);
     //   logger.error('failed to save', error);
-    //   pageContainer.showErrorToastr(error);
+    // toastError(error);
     // }
     // }
     // finally {
     // finally {
     //   this.setState({ currentTargetTableArea: null });
     //   this.setState({ currentTargetTableArea: null });
@@ -156,11 +158,12 @@ class PageSubstance extends React.Component<PageSubstanceProps> {
     //     const { page, tags } = await pageContainer.save(newMarkdown, this.props.editorMode, optionsToSave);
     //     const { page, tags } = await pageContainer.save(newMarkdown, this.props.editorMode, optionsToSave);
     //     logger.debug('success to save');
     //     logger.debug('success to save');
 
 
-  //     pageContainer.showSuccessToastr();
-  //   }
+    // // Todo: add translation
+    //   toastSuccess(t(''));
+    //   }
   //   catch (error) {
   //   catch (error) {
   //     logger.error('failed to save', error);
   //     logger.error('failed to save', error);
-  //     pageContainer.showErrorToastr(error);
+  //     toastError(error);
   //   }
   //   }
   //   finally {
   //   finally {
   //     this.setState({ currentTargetDrawioArea: null });
   //     this.setState({ currentTargetDrawioArea: null });

+ 14 - 7
packages/app/src/components/PageEditor.tsx

@@ -6,9 +6,11 @@ import EventEmitter from 'events';
 
 
 import { envUtils, PageGrant } from '@growi/core';
 import { envUtils, PageGrant } from '@growi/core';
 import detectIndent from 'detect-indent';
 import detectIndent from 'detect-indent';
+import { useTranslation } from 'next-i18next';
 import { throttle, debounce } from 'throttle-debounce';
 import { throttle, debounce } from 'throttle-debounce';
 
 
 import { saveOrUpdate } from '~/client/services/page-operation';
 import { saveOrUpdate } from '~/client/services/page-operation';
+import { toastSuccess, toastError } from '~/client/util/apiNotification';
 import { apiGet, apiPostForm } from '~/client/util/apiv1-client';
 import { apiGet, apiPostForm } from '~/client/util/apiv1-client';
 import { getOptionsToSave } from '~/client/util/editor';
 import { getOptionsToSave } from '~/client/util/editor';
 import { IEditorMethods } from '~/interfaces/editor-methods';
 import { IEditorMethods } from '~/interfaces/editor-methods';
@@ -48,6 +50,7 @@ let isOriginOfScrollSyncPreview = false;
 
 
 const PageEditor = React.memo((): JSX.Element => {
 const PageEditor = React.memo((): JSX.Element => {
 
 
+  const { t } = useTranslation();
   const { data: pageId } = useCurrentPageId();
   const { data: pageId } = useCurrentPageId();
   const { data: currentPagePath } = useCurrentPagePath();
   const { data: currentPagePath } = useCurrentPagePath();
   const { data: currentPathname } = useCurrentPathname();
   const { data: currentPathname } = useCurrentPathname();
@@ -95,7 +98,8 @@ const PageEditor = React.memo((): JSX.Element => {
     setMarkdownWithDebounce(value, isClean);
     setMarkdownWithDebounce(value, isClean);
   }, [setMarkdownWithDebounce]);
   }, [setMarkdownWithDebounce]);
 
 
-  const save = useCallback(async(opts?: {overwriteScopesOfDescendants: boolean}) => {
+  // return true if the save succeeds, otherwise false.
+  const save = useCallback(async(opts?: {overwriteScopesOfDescendants: boolean}): Promise<boolean> => {
     if (grantData == null || isSlackEnabled == null || currentPathname == null) {
     if (grantData == null || isSlackEnabled == null || currentPathname == null) {
       logger.error('Some materials to save are invalid', { grantData, isSlackEnabled, currentPathname });
       logger.error('Some materials to save are invalid', { grantData, isSlackEnabled, currentPathname });
       throw new Error('Some materials to save are invalid');
       throw new Error('Some materials to save are invalid');
@@ -113,10 +117,11 @@ const PageEditor = React.memo((): JSX.Element => {
       await saveOrUpdate(optionsToSave, { pageId, path: currentPagePath || currentPathname, revisionId: currentRevisionId }, markdownToSave.current);
       await saveOrUpdate(optionsToSave, { pageId, path: currentPagePath || currentPathname, revisionId: currentRevisionId }, markdownToSave.current);
       await mutateCurrentPage();
       await mutateCurrentPage();
       mutateIsEnabledUnsavedWarning(false);
       mutateIsEnabledUnsavedWarning(false);
+      return true;
     }
     }
     catch (error) {
     catch (error) {
       logger.error('failed to save', error);
       logger.error('failed to save', error);
-      // pageContainer.showErrorToastr(error);
+      toastError(error);
       if (error.code === 'conflict') {
       if (error.code === 'conflict') {
         // pageContainer.setState({
         // pageContainer.setState({
         //   remoteRevisionId: error.data.revisionId,
         //   remoteRevisionId: error.data.revisionId,
@@ -125,6 +130,7 @@ const PageEditor = React.memo((): JSX.Element => {
         //   lastUpdateUser: error.data.user,
         //   lastUpdateUser: error.data.user,
         // });
         // });
       }
       }
+      return false;
     }
     }
 
 
   // eslint-disable-next-line max-len
   // eslint-disable-next-line max-len
@@ -144,11 +150,12 @@ const PageEditor = React.memo((): JSX.Element => {
       return;
       return;
     }
     }
 
 
-    await save();
+    const isSuccess = await save();
+    if (isSuccess) {
+      toastSuccess(t('toaster.save_succeeded'));
+    }
 
 
-    // TODO: show toastr
-    // pageContainer.showErrorToastr(error);
-  }, [editorMode, save]);
+  }, [editorMode, save, t]);
 
 
 
 
   /**
   /**
@@ -201,7 +208,7 @@ const PageEditor = React.memo((): JSX.Element => {
     }
     }
     catch (e) {
     catch (e) {
       logger.error('failed to upload', e);
       logger.error('failed to upload', e);
-      // pageContainer.showErrorToastr(e);
+      toastError(e);
     }
     }
     finally {
     finally {
       editorRef.current.terminateUploadingState();
       editorRef.current.terminateUploadingState();

+ 2 - 2
packages/app/src/components/PageEditor/CodeMirrorEditor.jsx

@@ -1,4 +1,4 @@
-import React, { useCallback } from 'react';
+import React, { useCallback, memo } from 'react';
 
 
 import { createValidator } from '@growi/codemirror-textlint';
 import { createValidator } from '@growi/codemirror-textlint';
 import { commands } from 'codemirror';
 import { commands } from 'codemirror';
@@ -1163,4 +1163,4 @@ const CodeMirrorEditorFc = React.forwardRef((props, ref) => {
 
 
 CodeMirrorEditorFc.displayName = 'CodeMirrorEditorFc';
 CodeMirrorEditorFc.displayName = 'CodeMirrorEditorFc';
 
 
-export default CodeMirrorEditorFc;
+export default memo(CodeMirrorEditorFc);

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

@@ -1,5 +1,6 @@
 import React, {
 import React, {
   useState, useRef, useImperativeHandle, useCallback, ForwardRefRenderFunction, forwardRef,
   useState, useRef, useImperativeHandle, useCallback, ForwardRefRenderFunction, forwardRef,
+  memo,
 } from 'react';
 } from 'react';
 
 
 import Dropzone from 'react-dropzone';
 import Dropzone from 'react-dropzone';
@@ -347,4 +348,4 @@ const Editor: ForwardRefRenderFunction<IEditorMethods, EditorPropsType> = (props
   );
   );
 };
 };
 
 
-export default forwardRef(Editor);
+export default memo(forwardRef(Editor));

+ 4 - 4
packages/app/src/components/PageEditor/OptionsSelector.tsx

@@ -184,7 +184,7 @@ const ConfigurationDropdown = memo(({ onConfirmEnableTextlint }: ConfigurationDr
 
 
     const iconClasses = ['text-info'];
     const iconClasses = ['text-info'];
     if (isActive) {
     if (isActive) {
-      iconClasses.push('ti-check');
+      iconClasses.push('ti ti-check');
     }
     }
     const iconClassName = iconClasses.join(' ');
     const iconClassName = iconClasses.join(' ');
 
 
@@ -208,7 +208,7 @@ const ConfigurationDropdown = memo(({ onConfirmEnableTextlint }: ConfigurationDr
 
 
     const iconClasses = ['text-info'];
     const iconClasses = ['text-info'];
     if (isActive) {
     if (isActive) {
-      iconClasses.push('ti-check');
+      iconClasses.push('ti ti-check');
     }
     }
     const iconClassName = iconClasses.join(' ');
     const iconClassName = iconClasses.join(' ');
 
 
@@ -232,7 +232,7 @@ const ConfigurationDropdown = memo(({ onConfirmEnableTextlint }: ConfigurationDr
 
 
     const iconClasses = ['text-info'];
     const iconClasses = ['text-info'];
     if (isActive) {
     if (isActive) {
-      iconClasses.push('ti-check');
+      iconClasses.push('ti ti-check');
     }
     }
     const iconClassName = iconClasses.join(' ');
     const iconClassName = iconClasses.join(' ');
 
 
@@ -270,7 +270,7 @@ const ConfigurationDropdown = memo(({ onConfirmEnableTextlint }: ConfigurationDr
 
 
     const iconClasses = ['text-info'];
     const iconClasses = ['text-info'];
     if (isTextlintEnabled) {
     if (isTextlintEnabled) {
-      iconClasses.push('ti-check');
+      iconClasses.push('ti ti-check');
     }
     }
     const iconClassName = iconClasses.join(' ');
     const iconClassName = iconClasses.join(' ');
 
 

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

@@ -455,6 +455,7 @@ const PrivateLegacyPages = (): JSX.Element => {
             toastSuccess(t('private_legacy_pages.by_path_modal.success'));
             toastSuccess(t('private_legacy_pages.by_path_modal.success'));
             setOpenConvertModal(false);
             setOpenConvertModal(false);
             mutate();
             mutate();
+            advancePt();
           }
           }
           catch (errs) {
           catch (errs) {
             if (errs.length === 1) {
             if (errs.length === 1) {

+ 1 - 6
packages/app/src/components/ReactMarkdownComponents/CodeBlock.module.scss

@@ -1,10 +1,5 @@
 @use '~/styles/variables' as var;
 @use '~/styles/variables' as var;
 @use '~/styles/bootstrap/init' as bs;
 @use '~/styles/bootstrap/init' as bs;
-@use '~/styles/atoms/mixins' as atm;
-
-.code-inline {
-  @include atm.code-inline;
-}
 
 
 .code-highlighted-title {
 .code-highlighted-title {
   position: absolute;
   position: absolute;
@@ -16,5 +11,5 @@
   color: bs.$gray-900;
   color: bs.$gray-900;
   background: bs.$gray-300;
   background: bs.$gray-300;
   border-radius: bs.$border-radius;
   border-radius: bs.$border-radius;
-  opacity: 0.6;
+  opacity: 0.8;
 }
 }

+ 9 - 7
packages/app/src/components/ReactMarkdownComponents/CodeBlock.tsx

@@ -1,12 +1,14 @@
-import { CodeComponent } from 'react-markdown/lib/ast-to-react';
-import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
-import { oneLight } from 'react-syntax-highlighter/dist/cjs/styles/prism';
+import type { CodeComponent } from 'react-markdown/lib/ast-to-react';
+import { PrismAsyncLight } from 'react-syntax-highlighter';
+import { oneDark } from 'react-syntax-highlighter/dist/cjs/styles/prism';
 
 
 import styles from './CodeBlock.module.scss';
 import styles from './CodeBlock.module.scss';
 
 
+
 export const CodeBlock: CodeComponent = ({ inline, className, children }) => {
 export const CodeBlock: CodeComponent = ({ inline, className, children }) => {
+
   if (inline) {
   if (inline) {
-    return <code className={`code-inline ${styles['code-inline']} ${className ?? ''}`}>{children}</code>;
+    return <code className={`code-inline ${className ?? ''}`}>{children}</code>;
   }
   }
 
 
   // TODO: set border according to the value of 'customize:highlightJsStyleBorder'
   // TODO: set border according to the value of 'customize:highlightJsStyleBorder'
@@ -20,14 +22,14 @@ export const CodeBlock: CodeComponent = ({ inline, className, children }) => {
       {name != null && (
       {name != null && (
         <cite className={`code-highlighted-title ${styles['code-highlighted-title']}`}>{name}</cite>
         <cite className={`code-highlighted-title ${styles['code-highlighted-title']}`}>{name}</cite>
       )}
       )}
-      <SyntaxHighlighter
+      <PrismAsyncLight
         className="code-highlighted"
         className="code-highlighted"
         PreTag="div"
         PreTag="div"
-        style={oneLight}
+        style={oneDark}
         language={lang}
         language={lang}
       >
       >
         {String(children).replace(/\n$/, '')}
         {String(children).replace(/\n$/, '')}
-      </SyntaxHighlighter>
+      </PrismAsyncLight>
     </>
     </>
   );
   );
 };
 };

+ 10 - 8
packages/app/src/pages/[[...path]].page.tsx

@@ -26,7 +26,6 @@ import { CurrentPageContentFooter } from '~/components/PageContentFooter';
 import { UsersHomePageFooterProps } from '~/components/UsersHomePageFooter';
 import { UsersHomePageFooterProps } from '~/components/UsersHomePageFooter';
 import type { CrowiRequest } from '~/interfaces/crowi-request';
 import type { CrowiRequest } from '~/interfaces/crowi-request';
 // import { renderScriptTagByName, renderHighlightJsStyleTag } from '~/service/cdn-resources-loader';
 // import { renderScriptTagByName, renderHighlightJsStyleTag } from '~/service/cdn-resources-loader';
-// import { useIndentSize } from '~/stores/editor';
 // import { useRendererSettings } from '~/stores/renderer';
 // import { useRendererSettings } from '~/stores/renderer';
 // import { EditorMode, useEditorMode, useIsMobile } from '~/stores/ui';
 // import { EditorMode, useEditorMode, useIsMobile } from '~/stores/ui';
 import type { EditorConfig } from '~/interfaces/editor-settings';
 import type { EditorConfig } from '~/interfaces/editor-settings';
@@ -60,7 +59,7 @@ import {
   useIsForbidden, useIsNotFound, useIsTrashPage, useIsSharedUser,
   useIsForbidden, useIsNotFound, useIsTrashPage, useIsSharedUser,
   useIsEnabledStaleNotification, useIsIdenticalPath,
   useIsEnabledStaleNotification, useIsIdenticalPath,
   useIsSearchServiceConfigured, useIsSearchServiceReachable, useDisableLinkSharing,
   useIsSearchServiceConfigured, useIsSearchServiceReachable, useDisableLinkSharing,
-  useDrawioUri, useHackmdUri,
+  useDrawioUri, useHackmdUri, useDefaultIndentSize, useIsIndentSizeForced,
   useIsAclEnabled, useIsUserPage, useIsSearchPage,
   useIsAclEnabled, useIsUserPage, useIsSearchPage,
   useCsrfToken, useIsSearchScopeChildrenAsDefault, useCurrentPageId, useCurrentPathname,
   useCsrfToken, useIsSearchScopeChildrenAsDefault, useCurrentPageId, useCurrentPathname,
   useIsSlackConfigured, useRendererConfig, useEditingMarkdown,
   useIsSlackConfigured, useRendererConfig, useEditingMarkdown,
@@ -160,8 +159,8 @@ type Props = CommonProps & {
   isEnabledStaleNotification: boolean,
   isEnabledStaleNotification: boolean,
   // isEnabledLinebreaks: boolean,
   // isEnabledLinebreaks: boolean,
   // isEnabledLinebreaksInComments: boolean,
   // isEnabledLinebreaksInComments: boolean,
-  // adminPreferredIndentSize: number,
-  // isIndentSizeForced: boolean,
+  adminPreferredIndentSize: number,
+  isIndentSizeForced: boolean,
   disableLinkSharing: boolean,
   disableLinkSharing: boolean,
 
 
   rendererConfig: RendererConfig,
   rendererConfig: RendererConfig,
@@ -220,7 +219,8 @@ const GrowiPage: NextPage<Props> = (props: Props) => {
   useDrawioUri(props.drawioUri);
   useDrawioUri(props.drawioUri);
   useHackmdUri(props.hackmdUri);
   useHackmdUri(props.hackmdUri);
   // useNoCdn(props.noCdn);
   // useNoCdn(props.noCdn);
-  // useIndentSize(props.adminPreferredIndentSize);
+  useDefaultIndentSize(props.adminPreferredIndentSize);
+  useIsIndentSizeForced(props.isIndentSizeForced);
   useDisableLinkSharing(props.disableLinkSharing);
   useDisableLinkSharing(props.disableLinkSharing);
   useRendererConfig(props.rendererConfig);
   useRendererConfig(props.rendererConfig);
   // useRendererSettings(props.rendererSettingsStr != null ? JSON.parse(props.rendererSettingsStr) : undefined);
   // useRendererSettings(props.rendererSettingsStr != null ? JSON.parse(props.rendererSettingsStr) : undefined);
@@ -305,7 +305,9 @@ const GrowiPage: NextPage<Props> = (props: Props) => {
       <BasicLayout title={useCustomTitle(props, 'GROWI')} className={classNames.join(' ')} expandContainer={isContainerFluid}>
       <BasicLayout title={useCustomTitle(props, 'GROWI')} className={classNames.join(' ')} expandContainer={isContainerFluid}>
         <div className="h-100 d-flex flex-column justify-content-between">
         <div className="h-100 d-flex flex-column justify-content-between">
           <header className="py-0 position-relative">
           <header className="py-0 position-relative">
-            <GrowiContextualSubNavigation isLinkSharingDisabled={props.disableLinkSharing} />
+            <div id="grw-subnav-container">
+              <GrowiContextualSubNavigation isLinkSharingDisabled={props.disableLinkSharing} />
+            </div>
           </header>
           </header>
           <div className="d-edit-none">
           <div className="d-edit-none">
             <GrowiSubNavigationSwitcher />
             <GrowiSubNavigationSwitcher />
@@ -524,8 +526,8 @@ function injectServerConfigurations(context: GetServerSidePropsContext, props: P
       isUploadableImage: crowi.fileUploadService.getIsUploadable(),
       isUploadableImage: crowi.fileUploadService.getIsUploadable(),
     },
     },
   };
   };
-  // props.adminPreferredIndentSize = configManager.getConfig('markdown', 'markdown:adminPreferredIndentSize');
-  // props.isIndentSizeForced = configManager.getConfig('markdown', 'markdown:isIndentSizeForced');
+  props.adminPreferredIndentSize = configManager.getConfig('markdown', 'markdown:adminPreferredIndentSize');
+  props.isIndentSizeForced = configManager.getConfig('markdown', 'markdown:isIndentSizeForced');
 
 
   props.rendererConfig = {
   props.rendererConfig = {
     isEnabledLinebreaks: configManager.getConfig('markdown', 'markdown:isEnabledLinebreaks'),
     isEnabledLinebreaks: configManager.getConfig('markdown', 'markdown:isEnabledLinebreaks'),

+ 75 - 31
packages/app/src/pages/share/[[...path]].page.tsx

@@ -4,12 +4,17 @@ import { IUser, IUserHasId } from '@growi/core';
 import {
 import {
   NextPage, GetServerSideProps, GetServerSidePropsContext,
   NextPage, GetServerSideProps, GetServerSidePropsContext,
 } from 'next';
 } from 'next';
+import { useTranslation } from 'next-i18next';
 import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
 import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
 import dynamic from 'next/dynamic';
 import dynamic from 'next/dynamic';
 
 
+import CountBadge from '~/components/Common/CountBadge';
+import PageListIcon from '~/components/Icons/PageListIcon';
 import { ShareLinkLayout } from '~/components/Layout/ShareLinkLayout';
 import { ShareLinkLayout } from '~/components/Layout/ShareLinkLayout';
 import GrowiContextualSubNavigation from '~/components/Navbar/GrowiContextualSubNavigation';
 import GrowiContextualSubNavigation from '~/components/Navbar/GrowiContextualSubNavigation';
 import { Page } from '~/components/Page';
 import { Page } from '~/components/Page';
+import styles from '~/components/Page/DisplaySwitcher.module.scss'; // for PageList toc style
+import TableOfContents from '~/components/TableOfContents';
 import { SupportedAction, SupportedActionType } from '~/interfaces/activity';
 import { SupportedAction, SupportedActionType } from '~/interfaces/activity';
 import { CrowiRequest } from '~/interfaces/crowi-request';
 import { CrowiRequest } from '~/interfaces/crowi-request';
 import { RendererConfig } from '~/interfaces/services/renderer';
 import { RendererConfig } from '~/interfaces/services/renderer';
@@ -18,6 +23,7 @@ import {
   useCurrentUser, useCurrentPagePath, useCurrentPathname, useCurrentPageId, useRendererConfig, useIsSearchPage,
   useCurrentUser, useCurrentPagePath, useCurrentPathname, useCurrentPageId, useRendererConfig, useIsSearchPage,
   useShareLinkId, useIsSearchServiceConfigured, useIsSearchServiceReachable, useIsSearchScopeChildrenAsDefault,
   useShareLinkId, useIsSearchServiceConfigured, useIsSearchServiceReachable, useIsSearchScopeChildrenAsDefault,
 } from '~/stores/context';
 } from '~/stores/context';
+import { useDescendantsPageListModal } from '~/stores/modal';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
 import {
 import {
@@ -51,9 +57,12 @@ const SharedPage: NextPage<Props> = (props: Props) => {
   useIsSearchServiceConfigured(props.isSearchServiceConfigured);
   useIsSearchServiceConfigured(props.isSearchServiceConfigured);
   useIsSearchServiceReachable(props.isSearchServiceReachable);
   useIsSearchServiceReachable(props.isSearchServiceReachable);
   useIsSearchScopeChildrenAsDefault(props.isSearchScopeChildrenAsDefault);
   useIsSearchScopeChildrenAsDefault(props.isSearchScopeChildrenAsDefault);
+  const { open: openDescendantPageListModal } = useDescendantsPageListModal();
+  const { t } = useTranslation();
 
 
   const isNotFound = props.shareLink == null || props.shareLink.relatedPage == null || props.shareLink.relatedPage.isEmpty;
   const isNotFound = props.shareLink == null || props.shareLink.relatedPage == null || props.shareLink.relatedPage.isEmpty;
   const isShowSharedPage = !props.disableLinkSharing && !isNotFound && !props.isExpired;
   const isShowSharedPage = !props.disableLinkSharing && !isNotFound && !props.isExpired;
+  const shareLink = props.shareLink;
 
 
   return (
   return (
     <ShareLinkLayout title={useCustomTitle(props, 'GROWI')} expandContainer={props.isContainerFluid}>
     <ShareLinkLayout title={useCustomTitle(props, 'GROWI')} expandContainer={props.isContainerFluid}>
@@ -65,37 +74,72 @@ const SharedPage: NextPage<Props> = (props: Props) => {
         <div id="grw-fav-sticky-trigger" className="sticky-top"></div>
         <div id="grw-fav-sticky-trigger" className="sticky-top"></div>
 
 
         <div className="flex-grow-1">
         <div className="flex-grow-1">
-          <div id="content-main" className="content-main grw-container-convertible">
-            { props.disableLinkSharing && (
-              <div className="mt-4">
-                <ForbiddenPage isLinkSharingDisabled={props.disableLinkSharing} />
-              </div>
-            )}
-
-            { (isNotFound && !props.disableLinkSharing) && (
-              <div className="container-lg">
-                <h2 className="text-muted mt-4">
-                  <i className="icon-ban" aria-hidden="true" />
-                  <span> Page is not found</span>
-                </h2>
-              </div>
-            )}
-
-            { (props.isExpired && !props.disableLinkSharing) && (
-              <div className="container-lg">
-                <h2 className="text-muted mt-4">
-                  <i className="icon-ban" aria-hidden="true" />
-                  <span> Page is expired</span>
-                </h2>
-              </div>
-            )}
-
-            {(isShowSharedPage && props.shareLink != null) && (
-              <>
-                <ShareLinkAlert expiredAt={props.shareLink.expiredAt} createdAt={props.shareLink.createdAt} />
-                <Page />
-              </>
-            )}
+          <div id="content-main" className="content-main">
+            <div className="grw-container-convertible">
+              { props.disableLinkSharing && (
+                <div className="mt-4">
+                  <ForbiddenPage isLinkSharingDisabled={props.disableLinkSharing} />
+                </div>
+              )}
+
+              { (isNotFound && !props.disableLinkSharing) && (
+                <div className="container-lg">
+                  <h2 className="text-muted mt-4">
+                    <i className="icon-ban" aria-hidden="true" />
+                    <span> Page is not found</span>
+                  </h2>
+                </div>
+              )}
+
+              { (props.isExpired && !props.disableLinkSharing) && (
+                <div className="container-lg">
+                  <h2 className="text-muted mt-4">
+                    <i className="icon-ban" aria-hidden="true" />
+                    <span> Page is expired</span>
+                  </h2>
+                </div>
+              )}
+
+              {(isShowSharedPage && shareLink != null) && (
+                <>
+                  <ShareLinkAlert expiredAt={shareLink.expiredAt} createdAt={shareLink.createdAt} />
+                  <div className="d-flex flex-column flex-lg-row-reverse">
+
+                    <div className="grw-side-contents-container">
+                      <div className="grw-side-contents-sticky-container">
+
+                        {/* Page list */}
+                        <div className={`grw-page-accessories-control ${styles['grw-page-accessories-control']}`}>
+                          { shareLink.relatedPage.path != null && (
+                            <button
+                              type="button"
+                              className="btn btn-block btn-outline-secondary grw-btn-page-accessories
+                              rounded-pill d-flex justify-content-between align-items-center"
+                              onClick={() => openDescendantPageListModal(shareLink.relatedPage.path)}
+                              data-testid="pageListButton"
+                            >
+                              <div className="grw-page-accessories-control-icon">
+                                <PageListIcon />
+                              </div>
+                              {t('page_list')}
+                              <CountBadge count={shareLink.relatedPage.descendantCount} offset={1} />
+                            </button>
+                          ) }
+                        </div>
+
+                        <div className="d-none d-lg-block">
+                          <TableOfContents />
+                        </div>
+                      </div>
+                    </div>
+
+                    <div className="flex-grow-1 flex-basis-0 mw-0">
+                      <Page />
+                    </div>
+                  </div>
+                </>
+              )}
+            </div>
           </div>
           </div>
         </div>
         </div>
       </div>
       </div>

+ 1 - 1
packages/app/src/stores/context.tsx

@@ -196,7 +196,7 @@ export const useHasParent = (initialData?: boolean) : SWRResponse<boolean, Error
 };
 };
 
 
 export const useIsIndentSizeForced = (initialData?: boolean) : SWRResponse<boolean, Error> => {
 export const useIsIndentSizeForced = (initialData?: boolean) : SWRResponse<boolean, Error> => {
-  return useStaticSWR<boolean, Error>('isIndentSizeForced', initialData);
+  return useStaticSWR<boolean, Error>('isIndentSizeForced', initialData, { fallbackData: false });
 };
 };
 
 
 export const useDefaultIndentSize = (initialData?: number) : SWRResponse<number, Error> => {
 export const useDefaultIndentSize = (initialData?: number) : SWRResponse<number, Error> => {

+ 4 - 5
packages/app/src/stores/editor.tsx

@@ -1,4 +1,4 @@
-import { Nullable } from '@growi/core';
+import { Nullable, withUtils, SWRResponseWithUtils } from '@growi/core';
 import useSWR, { SWRResponse } from 'swr';
 import useSWR, { SWRResponse } from 'swr';
 import useSWRImmutable from 'swr/immutable';
 import useSWRImmutable from 'swr/immutable';
 
 
@@ -20,7 +20,7 @@ type EditorSettingsOperation = {
   turnOffAskingBeforeDownloadLargeFiles: () => void,
   turnOffAskingBeforeDownloadLargeFiles: () => void,
 }
 }
 
 
-export const useEditorSettings = (): SWRResponse<IEditorSettings, Error> & EditorSettingsOperation => {
+export const useEditorSettings = (): SWRResponseWithUtils<EditorSettingsOperation, IEditorSettings, Error> => {
   const { data: currentUser } = useCurrentUser();
   const { data: currentUser } = useCurrentUser();
   const { data: isGuestUser } = useIsGuestUser();
   const { data: isGuestUser } = useIsGuestUser();
 
 
@@ -30,8 +30,7 @@ export const useEditorSettings = (): SWRResponse<IEditorSettings, Error> & Edito
     { use: [localStorageMiddleware] }, // store to localStorage for initialization fastly
     { use: [localStorageMiddleware] }, // store to localStorage for initialization fastly
   );
   );
 
 
-  return {
-    ...swrResult,
+  return withUtils<EditorSettingsOperation, IEditorSettings, Error>(swrResult, {
     update: (updateData) => {
     update: (updateData) => {
       const { data, mutate } = swrResult;
       const { data, mutate } = swrResult;
 
 
@@ -56,7 +55,7 @@ export const useEditorSettings = (): SWRResponse<IEditorSettings, Error> & Edito
       // revalidate
       // revalidate
       mutate();
       mutate();
     },
     },
-  };
+  });
 };
 };
 
 
 export const useIsTextlintEnabled = (): SWRResponse<boolean, Error> => {
 export const useIsTextlintEnabled = (): SWRResponse<boolean, Error> => {

+ 34 - 26
packages/app/src/stores/ui.tsx

@@ -1,10 +1,10 @@
-import { RefObject } from 'react';
+import { RefObject, useEffect } from 'react';
 
 
 import {
 import {
   isClient, isServer, pagePathUtils, Nullable, PageGrant,
   isClient, isServer, pagePathUtils, Nullable, PageGrant,
 } from '@growi/core';
 } from '@growi/core';
 import { withUtils, SWRResponseWithUtils } from '@growi/core/src/utils/with-utils';
 import { withUtils, SWRResponseWithUtils } from '@growi/core/src/utils/with-utils';
-import { Breakpoint, addBreakpointListener } from '@growi/ui';
+import { Breakpoint, addBreakpointListener, cleanupBreakpointListener } from '@growi/ui';
 import SimpleBar from 'simplebar-react';
 import SimpleBar from 'simplebar-react';
 import {
 import {
   useSWRConfig, SWRResponse, Key, Fetcher,
   useSWRConfig, SWRResponse, Key, Fetcher,
@@ -177,21 +177,25 @@ export const useIsDeviceSmallerThanMd = (): SWRResponse<boolean, Error> => {
 
 
   const { cache, mutate } = useSWRConfig();
   const { cache, mutate } = useSWRConfig();
 
 
-  if (isClient()) {
-    const mdOrAvobeHandler = function(this: MediaQueryList): void {
-      // sm -> md: matches will be true
-      // md -> sm: matches will be false
-      mutate(key, !this.matches);
-    };
-    const mql = addBreakpointListener(Breakpoint.MD, mdOrAvobeHandler);
+  useEffect(() => {
+    if (isClient()) {
+      const mdOrAvobeHandler = function(this: MediaQueryList): void {
+        // sm -> md: matches will be true
+        // md -> sm: matches will be false
+        mutate(key, !this.matches);
+      };
+      const mql = addBreakpointListener(Breakpoint.MD, mdOrAvobeHandler);
 
 
-    // initialize
-    if (cache.get(key) == null) {
-      document.addEventListener('DOMContentLoaded', () => {
+      // initialize
+      if (cache.get(key) == null) {
         mutate(key, !mql.matches);
         mutate(key, !mql.matches);
-      });
+      }
+
+      return () => {
+        cleanupBreakpointListener(mql, mdOrAvobeHandler);
+      };
     }
     }
-  }
+  }, [cache, key, mutate]);
 
 
   return useStaticSWR(key);
   return useStaticSWR(key);
 };
 };
@@ -201,21 +205,25 @@ export const useIsDeviceSmallerThanLg = (): SWRResponse<boolean, Error> => {
 
 
   const { cache, mutate } = useSWRConfig();
   const { cache, mutate } = useSWRConfig();
 
 
-  if (isClient()) {
-    const lgOrAvobeHandler = function(this: MediaQueryList): void {
-      // md -> lg: matches will be true
-      // lg -> md: matches will be false
-      mutate(key, !this.matches);
-    };
-    const mql = addBreakpointListener(Breakpoint.LG, lgOrAvobeHandler);
+  useEffect(() => {
+    if (isClient()) {
+      const lgOrAvobeHandler = function(this: MediaQueryList): void {
+        // md -> lg: matches will be true
+        // lg -> md: matches will be false
+        mutate(key, !this.matches);
+      };
+      const mql = addBreakpointListener(Breakpoint.LG, lgOrAvobeHandler);
 
 
-    // initialize
-    if (cache.get(key) == null) {
-      document.addEventListener('DOMContentLoaded', () => {
+      // initialize
+      if (cache.get(key) == null) {
         mutate(key, !mql.matches);
         mutate(key, !mql.matches);
-      });
+      }
+
+      return () => {
+        cleanupBreakpointListener(mql, lgOrAvobeHandler);
+      };
     }
     }
-  }
+  }, [cache, key, mutate]);
 
 
   return useStaticSWR(key);
   return useStaticSWR(key);
 };
 };

+ 5 - 5
packages/app/src/styles/atoms/_code.scss

@@ -1,12 +1,12 @@
 @use '~/styles/variables' as var;
 @use '~/styles/variables' as var;
 @use '~/styles/bootstrap/init' as bs;
 @use '~/styles/bootstrap/init' as bs;
-@use '~/styles/atoms/mixins' as atm;
 
 
 /*
 /*
  * style of inline-code
  * style of inline-code
  */
  */
-:not(pre) {
-  > code {
-    @include atm.code-inline;
-  }
+code:not([class^='language-']) {
+  padding: 2px 4px;
+  font-family: var.$font-family-monospace-not-strictly;
+  border: 1px solid;
+  border-radius: bs.$border-radius;
 }
 }

+ 0 - 15
packages/app/src/styles/atoms/_mixins.scss

@@ -1,15 +0,0 @@
-@use '~/styles/variables' as var;
-@use '~/styles/bootstrap/init' as bs;
-
-@mixin code-inline {
-  padding: 2px 4px;
-  font-family: var.$font-family-monospace-not-strictly;
-  border: 1px solid;
-  border-radius: bs.$border-radius;
-}
-
-@mixin code-inline-color($color-inline-code,$bgcolor-inline-code, $bordercolor-inline-code) {
-  color: $color-inline-code;
-  background-color: $bgcolor-inline-code;
-  border-color: $bordercolor-inline-code;
-}

+ 5 - 0
packages/app/src/styles/atoms/mixins/_code.scss

@@ -0,0 +1,5 @@
+@mixin code-inline-color($color-inline-code,$bgcolor-inline-code, $bordercolor-inline-code) {
+  color: $color-inline-code;
+  background-color: $bgcolor-inline-code;
+  border-color: $bordercolor-inline-code;
+}

+ 4 - 9
packages/app/src/styles/theme/_apply-colors.scss

@@ -2,7 +2,8 @@
 @use '../bootstrap/init' as *;
 @use '../bootstrap/init' as *;
 @use '../mixins';
 @use '../mixins';
 @use './mixins/tables'; // comment out and use _reboot-bootstrap-tables instead -- 2020.05.28 Yuki Takei
 @use './mixins/tables'; // comment out and use _reboot-bootstrap-tables instead -- 2020.05.28 Yuki Takei
-@use '../atoms/mixins' as atm;
+@use '../atoms/mixins/code';
+
 //
 //
 //== Apply to Bootstrap
 //== Apply to Bootstrap
 //
 //
@@ -49,14 +50,8 @@ $theme-colors: map-merge($theme-colors, ( primary: $primary ));
 // determine variables with bootstrap function (These variables can be used after importing bootstrap above)
 // determine variables with bootstrap function (These variables can be used after importing bootstrap above)
 $color-modal-header: color-yiq($primary) !default;
 $color-modal-header: color-yiq($primary) !default;
 
 
-:not(pre) {
-  > code {
-    @include atm.code-inline-color($color-inline-code, $bgcolor-inline-code, $bordercolor-inline-code);
-  }
-}
-
-.code-inline {
-  @include atm.code-inline-color($color-inline-code, $bgcolor-inline-code, $bordercolor-inline-code);
+code:not([class^='language-']) {
+  @include code.code-inline-color($color-inline-code, $bgcolor-inline-code, $bordercolor-inline-code);
 }
 }
 
 
 .code-highlighted {
 .code-highlighted {

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

@@ -1,6 +1,6 @@
 {
 {
   "name": "@growi/codemirror-textlint",
   "name": "@growi/codemirror-textlint",
-  "version": "6.0.0-RC.1",
+  "version": "6.0.0-RC.3",
   "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.0-RC.1",
+  "version": "6.0.0-RC.3",
   "description": "GROWI Core Libraries",
   "description": "GROWI Core Libraries",
   "license": "MIT",
   "license": "MIT",
   "keywords": [
   "keywords": [

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

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

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

@@ -1,6 +1,6 @@
 {
 {
   "name": "@growi/plugin-lsx",
   "name": "@growi/plugin-lsx",
-  "version": "6.0.0-RC.1",
+  "version": "6.0.0-RC.3",
   "description": "GROWI plugin to list pages",
   "description": "GROWI plugin to list pages",
   "license": "MIT",
   "license": "MIT",
   "keywords": ["growi", "growi-plugin"],
   "keywords": ["growi", "growi-plugin"],
@@ -23,9 +23,9 @@
     "test": ""
     "test": ""
   },
   },
   "dependencies": {
   "dependencies": {
-    "@growi/core": "^6.0.0-RC.1",
-    "@growi/remark-growi-plugin": "^6.0.0-RC.1",
-    "@growi/ui": "^6.0.0-RC.1"
+    "@growi/core": "^6.0.0-RC.3",
+    "@growi/remark-growi-plugin": "^6.0.0-RC.3",
+    "@growi/ui": "^6.0.0-RC.3"
   },
   },
   "devDependencies": {
   "devDependencies": {
     "eslint-plugin-regex": "^1.8.0",
     "eslint-plugin-regex": "^1.8.0",

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

@@ -1,6 +1,6 @@
 {
 {
   "name": "@growi/remark-growi-plugin",
   "name": "@growi/remark-growi-plugin",
-  "version": "6.0.0-RC.1",
+  "version": "6.0.0-RC.3",
   "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": [

+ 1 - 1
packages/slack/package.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "@growi/slack",
   "name": "@growi/slack",
-  "version": "6.0.0-RC.1",
+  "version": "6.0.0-RC.3",
   "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.0-slackbot-proxy.1",
+  "version": "6.0.0-RC.3",
   "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.0-RC.1",
+    "@growi/slack": "^6.0.0-RC.3",
     "@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.0-RC.1",
+  "version": "6.0.0-RC.3",
   "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.0-RC.1"
+    "@growi/core": "^6.0.0-RC.3"
   },
   },
   "devDependencies": {
   "devDependencies": {
     "eslint-plugin-regex": "^1.8.0",
     "eslint-plugin-regex": "^1.8.0",

+ 10 - 1
packages/ui/src/utils/browser-utils.ts

@@ -1,5 +1,6 @@
 import { Breakpoint } from '../interfaces/breakpoints';
 import { Breakpoint } from '../interfaces/breakpoints';
 
 
+const EVENT_TYPE_CHANGE = 'change';
 
 
 export const addBreakpointListener = (
 export const addBreakpointListener = (
     breakpoint: Breakpoint,
     breakpoint: Breakpoint,
@@ -12,7 +13,15 @@ export const addBreakpointListener = (
   const mediaQueryList = window.matchMedia(`(min-width: ${breakpointPixel}px)`);
   const mediaQueryList = window.matchMedia(`(min-width: ${breakpointPixel}px)`);
 
 
   // add event listener
   // add event listener
-  mediaQueryList.addEventListener('change', listener);
+  mediaQueryList.addEventListener(EVENT_TYPE_CHANGE, listener);
 
 
   return mediaQueryList;
   return mediaQueryList;
 };
 };
+
+export const cleanupBreakpointListener = (
+    mediaQueryList: MediaQueryList,
+    // eslint-disable-next-line @typescript-eslint/no-explicit-any
+    listener: (this: MediaQueryList, ev: MediaQueryListEvent) => any,
+): void => {
+  mediaQueryList.removeEventListener(EVENT_TYPE_CHANGE, listener);
+};

+ 9 - 8
yarn.lock

@@ -13368,9 +13368,9 @@ jmespath@0.15.0:
   resolved "https://registry.yarnpkg.com/jmespath/-/jmespath-0.15.0.tgz#a3f222a9aae9f966f5d27c796510e28091764217"
   resolved "https://registry.yarnpkg.com/jmespath/-/jmespath-0.15.0.tgz#a3f222a9aae9f966f5d27c796510e28091764217"
 
 
 jose@^4.1.4:
 jose@^4.1.4:
-  version "4.4.0"
-  resolved "https://registry.yarnpkg.com/jose/-/jose-4.4.0.tgz#459954544088244836a361172eb32a704bc55c5f"
-  integrity sha512-3CsqCQWuEUPpNlSLRcLRC8eO/ATFe1tLJMZCtjx2+ma1gkjGQ62HF50oWs3cwtWjLCpM8bdMPpQbxpgc3fhxrQ==
+  version "4.9.3"
+  resolved "https://registry.yarnpkg.com/jose/-/jose-4.9.3.tgz#890abd3f26725fe0f2aa720bc2f7835702b624db"
+  integrity sha512-f8E/z+T3Q0kA9txzH2DKvH/ds2uggcw0m3vVPSB9HrSkrQ7mojjifvS7aR8cw+lQl2Fcmx9npwaHpM/M3GD8UQ==
 
 
 jpeg-js@^0.4.0, jpeg-js@^0.4.2:
 jpeg-js@^0.4.0, jpeg-js@^0.4.2:
   version "0.4.3"
   version "0.4.3"
@@ -17525,13 +17525,14 @@ passport-twitter@^1.0.4:
     passport-oauth1 "1.x.x"
     passport-oauth1 "1.x.x"
     xtraverse "0.1.x"
     xtraverse "0.1.x"
 
 
-passport@^0.5.0:
-  version "0.5.0"
-  resolved "https://registry.yarnpkg.com/passport/-/passport-0.5.0.tgz#7914aaa55844f9dce8c3aa28f7d6b73647ee0169"
-  integrity sha512-ln+ue5YaNDS+fes6O5PCzXKSseY5u8MYhX9H5Co4s+HfYI5oqvnHKoOORLYDUPh+8tHvrxugF2GFcUA1Q1Gqfg==
+passport@^0.6.0:
+  version "0.6.0"
+  resolved "https://registry.yarnpkg.com/passport/-/passport-0.6.0.tgz#e869579fab465b5c0b291e841e6cc95c005fac9d"
+  integrity sha512-0fe+p3ZnrWRW74fe8+SvCyf4a3Pb2/h7gFkQ8yTJpAO50gDzlfjZUZTO1k5Eg9kUct22OxHLqDZoKUWRHOh9ug==
   dependencies:
   dependencies:
     passport-strategy "1.x.x"
     passport-strategy "1.x.x"
     pause "0.0.1"
     pause "0.0.1"
+    utils-merge "^1.0.1"
 
 
 path-case@^3.0.3, path-case@^3.0.4:
 path-case@^3.0.3, path-case@^3.0.4:
   version "3.0.4"
   version "3.0.4"
@@ -24097,7 +24098,7 @@ util.promisify@^1.0.0:
     has-symbols "^1.0.1"
     has-symbols "^1.0.1"
     object.getownpropertydescriptors "^2.1.1"
     object.getownpropertydescriptors "^2.1.1"
 
 
-utils-merge@1.0.1, utils-merge@1.x.x:
+utils-merge@1.0.1, utils-merge@1.x.x, utils-merge@^1.0.1:
   version "1.0.1"
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713"
   resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713"