ソースを参照

Merge branch 'master' into feat/display-plugin-cards-dynamically

jam411 3 年 前
コミット
85cafb6d6b

+ 1 - 4
packages/app/src/client/services/AdminMarkDownContainer.js

@@ -27,7 +27,7 @@ export default class AdminMarkDownContainer extends Container {
       pageBreakSeparator: 1,
       pageBreakSeparator: 1,
       pageBreakCustomSeparator: '',
       pageBreakCustomSeparator: '',
       isEnabledXss: false,
       isEnabledXss: false,
-      xssOption: 1,
+      xssOption: '',
       tagWhiteList: '',
       tagWhiteList: '',
       attrWhiteList: '',
       attrWhiteList: '',
     };
     };
@@ -86,9 +86,6 @@ export default class AdminMarkDownContainer extends Container {
    * Switch enableXss
    * Switch enableXss
    */
    */
   switchEnableXss() {
   switchEnableXss() {
-    if (this.state.isEnabledXss) {
-      this.setState({ xssOption: null });
-    }
     this.setState({ isEnabledXss: !this.state.isEnabledXss });
     this.setState({ isEnabledXss: !this.state.isEnabledXss });
   }
   }
 
 

+ 12 - 5
packages/app/src/client/services/page-operation.ts

@@ -3,8 +3,8 @@ import urljoin from 'url-join';
 
 
 import { OptionsToSave } from '~/interfaces/page-operation';
 import { OptionsToSave } from '~/interfaces/page-operation';
 import { useCurrentPageId } from '~/stores/context';
 import { useCurrentPageId } from '~/stores/context';
-import { useIsEnabledUnsavedWarning } from '~/stores/editor';
-import { useSWRxCurrentPage } from '~/stores/page';
+import { useIsEnabledUnsavedWarning, usePageTagsForEditors } from '~/stores/editor';
+import { useSWRxCurrentPage, useSWRxTagsInfo } from '~/stores/page';
 import { useSetRemoteLatestPageData } from '~/stores/remote-latest-page';
 import { useSetRemoteLatestPageData } from '~/stores/remote-latest-page';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
@@ -175,16 +175,23 @@ export const useSaveOrUpdate = (): SaveOrUpdateFunction => {
   };
   };
 };
 };
 
 
-export const useUpdateStateAfterSave = () => {
+export const useUpdateStateAfterSave = (pageId: string|undefined|null): (() => Promise<void>) | undefined => {
   const { mutate: mutateCurrentPageId } = useCurrentPageId();
   const { mutate: mutateCurrentPageId } = useCurrentPageId();
   const { mutate: mutateCurrentPage } = useSWRxCurrentPage();
   const { mutate: mutateCurrentPage } = useSWRxCurrentPage();
   const { setRemoteLatestPageData } = useSetRemoteLatestPageData();
   const { setRemoteLatestPageData } = useSetRemoteLatestPageData();
+  const { mutate: mutateTagsInfo } = useSWRxTagsInfo(pageId);
+  const { sync: syncTagsInfoForEditor } = usePageTagsForEditors(pageId);
+
+  if (pageId == null) { return }
 
 
   // update swr 'currentPageId', 'currentPage', remote states
   // update swr 'currentPageId', 'currentPage', remote states
-  return async(pageId: string) => {
+  return async() => {
     await mutateCurrentPageId(pageId);
     await mutateCurrentPageId(pageId);
     const updatedPage = await mutateCurrentPage();
     const updatedPage = await mutateCurrentPage();
 
 
+    await mutateTagsInfo(); // get from DB
+    syncTagsInfoForEditor(); // sync global state for client
+
     if (updatedPage == null) { return }
     if (updatedPage == null) { return }
 
 
     const remoterevisionData = {
     const remoterevisionData = {
@@ -192,7 +199,7 @@ export const useUpdateStateAfterSave = () => {
       remoteRevisionBody: updatedPage.revision.body,
       remoteRevisionBody: updatedPage.revision.body,
       remoteRevisionLastUpdateUser: updatedPage.lastUpdateUser,
       remoteRevisionLastUpdateUser: updatedPage.lastUpdateUser,
       remoteRevisionLastUpdatedAt: updatedPage.updatedAt,
       remoteRevisionLastUpdatedAt: updatedPage.updatedAt,
-      revisionIdHackmdSynced: updatedPage.revisionHackmdSynced.toString(),
+      revisionIdHackmdSynced: updatedPage.revisionHackmdSynced?.toString(),
       hasDraftOnHackmd: updatedPage.hasDraftOnHackmd,
       hasDraftOnHackmd: updatedPage.hasDraftOnHackmd,
     };
     };
 
 

+ 9 - 8
packages/app/src/components/Admin/MarkdownSetting/XssForm.jsx

@@ -5,6 +5,7 @@ import PropTypes from 'prop-types';
 
 
 import AdminMarkDownContainer from '~/client/services/AdminMarkDownContainer';
 import AdminMarkDownContainer from '~/client/services/AdminMarkDownContainer';
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
+import { RehypeSanitizeOption } from '~/interfaces/rehype';
 import { tags, attrs } from '~/services/xss/recommended-whitelist';
 import { tags, attrs } from '~/services/xss/recommended-whitelist';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
@@ -49,12 +50,12 @@ class XssForm extends React.Component {
               <input
               <input
                 type="radio"
                 type="radio"
                 className="custom-control-input"
                 className="custom-control-input"
-                id="xssOption2"
+                id="xssOption1"
                 name="XssOption"
                 name="XssOption"
-                checked={xssOption === 2}
-                onChange={() => { adminMarkDownContainer.setState({ xssOption: 2 }) }}
+                checked={xssOption === RehypeSanitizeOption.RECOMMENDED}
+                onChange={() => { adminMarkDownContainer.setState({ xssOption: RehypeSanitizeOption.RECOMMENDED }) }}
               />
               />
-              <label className="custom-control-label w-100" htmlFor="xssOption2">
+              <label className="custom-control-label w-100" htmlFor="xssOption1">
                 <p className="font-weight-bold">{t('markdown_settings.xss_options.recommended_setting')}</p>
                 <p className="font-weight-bold">{t('markdown_settings.xss_options.recommended_setting')}</p>
                 <div className="mt-4">
                 <div className="mt-4">
                   <div className="d-flex justify-content-between">
                   <div className="d-flex justify-content-between">
@@ -91,12 +92,12 @@ class XssForm extends React.Component {
               <input
               <input
                 type="radio"
                 type="radio"
                 className="custom-control-input"
                 className="custom-control-input"
-                id="xssOption3"
+                id="xssOption2"
                 name="XssOption"
                 name="XssOption"
-                checked={xssOption === 3}
-                onChange={() => { adminMarkDownContainer.setState({ xssOption: 3 }) }}
+                checked={xssOption === RehypeSanitizeOption.CUSTOM}
+                onChange={() => { adminMarkDownContainer.setState({ xssOption: RehypeSanitizeOption.CUSTOM }) }}
               />
               />
-              <label className="custom-control-label w-100" htmlFor="xssOption3">
+              <label className="custom-control-label w-100" htmlFor="xssOption2">
                 <p className="font-weight-bold">{t('markdown_settings.xss_options.custom_whitelist')}</p>
                 <p className="font-weight-bold">{t('markdown_settings.xss_options.custom_whitelist')}</p>
                 <WhiteListInput customizable />
                 <WhiteListInput customizable />
               </label>
               </label>

+ 1 - 1
packages/app/src/components/Layout/ShareLinkLayout.tsx

@@ -30,7 +30,7 @@ export const ShareLinkLayout = ({
 
 
   return (
   return (
     <RawLayout title={title} className={myClassName}>
     <RawLayout title={title} className={myClassName}>
-      <GrowiNavbar />
+      <GrowiNavbar isGlobalSearchHidden={true} />
 
 
       <div className="page-wrapper d-flex d-print-block">
       <div className="page-wrapper d-flex d-print-block">
         <div className="flex-fill mw-0" style={{ position: 'relative' }}>
         <div className="flex-fill mw-0" style={{ position: 'relative' }}>

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

@@ -188,7 +188,8 @@ const GrowiContextualSubNavigation = (props: GrowiContextualSubNavigationProps):
 
 
   const router = useRouter();
   const router = useRouter();
 
 
-  const { data: currentPage, mutate: mutateCurrentPage } = useSWRxCurrentPage();
+  const { data: shareLinkId } = useShareLinkId();
+  const { data: currentPage, mutate: mutateCurrentPage } = useSWRxCurrentPage(shareLinkId ?? undefined);
 
 
   const revision = currentPage?.revision;
   const revision = currentPage?.revision;
   const revisionId = (revision != null && isPopulated(revision)) ? revision._id : undefined;
   const revisionId = (revision != null && isPopulated(revision)) ? revision._id : undefined;
@@ -201,7 +202,6 @@ const GrowiContextualSubNavigation = (props: GrowiContextualSubNavigationProps):
   const { data: isNotFound } = useIsNotFound();
   const { data: isNotFound } = useIsNotFound();
   const { data: isGuestUser } = useIsGuestUser();
   const { data: isGuestUser } = useIsGuestUser();
   const { data: isSharedUser } = useIsSharedUser();
   const { data: isSharedUser } = useIsSharedUser();
-  const { data: shareLinkId } = useShareLinkId();
   const { data: isContainerFluid } = useIsContainerFluid();
   const { data: isContainerFluid } = useIsContainerFluid();
 
 
   const { data: isAbleToShowPageManagement } = useIsAbleToShowPageManagement();
   const { data: isAbleToShowPageManagement } = useIsAbleToShowPageManagement();

+ 28 - 39
packages/app/src/components/PageEditor.tsx

@@ -69,7 +69,7 @@ const PageEditor = React.memo((): JSX.Element => {
   const router = useRouter();
   const router = useRouter();
 
 
   const { data: isNotFound } = useIsNotFound();
   const { data: isNotFound } = useIsNotFound();
-  const { data: pageId, mutate: mutateCurrentPageId } = useCurrentPageId();
+  const { data: pageId } = useCurrentPageId();
   const { data: currentPagePath } = useCurrentPagePath();
   const { data: currentPagePath } = useCurrentPagePath();
   const { data: currentPathname } = useCurrentPathname();
   const { data: currentPathname } = useCurrentPathname();
   const { data: currentPage, mutate: mutateCurrentPage } = useSWRxCurrentPage();
   const { data: currentPage, mutate: mutateCurrentPage } = useSWRxCurrentPage();
@@ -82,7 +82,6 @@ const PageEditor = React.memo((): JSX.Element => {
   const { data: isEditable } = useIsEditable();
   const { data: isEditable } = useIsEditable();
   const { data: editorMode, mutate: mutateEditorMode } = useEditorMode();
   const { data: editorMode, mutate: mutateEditorMode } = useEditorMode();
   const { data: isSlackEnabled } = useIsSlackEnabled();
   const { data: isSlackEnabled } = useIsSlackEnabled();
-  const { data: slackChannelsData } = useSWRxSlackChannels(currentPagePath);
   const { data: isTextlintEnabled } = useIsTextlintEnabled();
   const { data: isTextlintEnabled } = useIsTextlintEnabled();
   const { data: isIndentSizeForced } = useIsIndentSizeForced();
   const { data: isIndentSizeForced } = useIsIndentSizeForced();
   const { data: currentIndentSize, mutate: mutateCurrentIndentSize } = useCurrentIndentSize();
   const { data: currentIndentSize, mutate: mutateCurrentIndentSize } = useCurrentIndentSize();
@@ -94,7 +93,7 @@ const PageEditor = React.memo((): JSX.Element => {
   const { mutate: mutateIsEnabledUnsavedWarning } = useIsEnabledUnsavedWarning();
   const { mutate: mutateIsEnabledUnsavedWarning } = useIsEnabledUnsavedWarning();
   const saveOrUpdate = useSaveOrUpdate();
   const saveOrUpdate = useSaveOrUpdate();
 
 
-  const updateStateAfterSave = useUpdateStateAfterSave();
+  const updateStateAfterSave = useUpdateStateAfterSave(pageId);
 
 
   const currentRevisionId = currentPage?.revision?._id;
   const currentRevisionId = currentPage?.revision?._id;
 
 
@@ -117,8 +116,6 @@ const PageEditor = React.memo((): JSX.Element => {
   const markdownToSave = useRef<string>(initialValue);
   const markdownToSave = useRef<string>(initialValue);
   const [markdownToPreview, setMarkdownToPreview] = useState<string>(initialValue);
   const [markdownToPreview, setMarkdownToPreview] = useState<string>(initialValue);
 
 
-  const slackChannels = useMemo(() => (slackChannelsData ? slackChannelsData.toString() : ''), [slackChannelsData]);
-
   const { data: socket } = useGlobalSocket();
   const { data: socket } = useGlobalSocket();
 
 
   const { mutate: mutateIsConflict } = useIsConflict();
   const { mutate: mutateIsConflict } = useIsConflict();
@@ -152,18 +149,21 @@ const PageEditor = React.memo((): JSX.Element => {
 
 
   }, [socket, checkIsConflict]);
   }, [socket, checkIsConflict]);
 
 
-  // const optionsToSave = useMemo(() => {
-  //   if (grantData == null) {
-  //     return;
-  //   }
-  //   const slackChannels = slackChannelsData ? slackChannelsData.toString() : '';
-  //   const optionsToSave = getOptionsToSave(
-  //     isSlackEnabled ?? false, slackChannels,
-  //     grantData.grant, grantData.grantedGroup?.id, grantData.grantedGroup?.name,
-  //     pageTags || [],
-  //   );
-  //   return optionsToSave;
-  // }, [grantData, isSlackEnabled, pageTags, slackChannelsData]);
+  const optionsToSave = useMemo((): OptionsToSave | undefined => {
+    if (grantData == null) {
+      return;
+    }
+    const optionsToSave = {
+      isSlackEnabled: isSlackEnabled ?? false,
+      slackChannels: '', // set in save method by opts in SavePageControlls.tsx
+      grant: grantData.grant,
+      pageTags: pageTags ?? [],
+      grantUserGroupId: grantData.grantedGroup?.id,
+      grantUserGroupName: grantData.grantedGroup?.name,
+    };
+    return optionsToSave;
+  }, [grantData, isSlackEnabled, pageTags]);
+
   // register to facade
   // register to facade
   useEffect(() => {
   useEffect(() => {
     // for markdownRenderer
     // for markdownRenderer
@@ -189,30 +189,19 @@ const PageEditor = React.memo((): JSX.Element => {
     setMarkdownWithDebounce(value, isClean);
     setMarkdownWithDebounce(value, isClean);
   }, [setMarkdownWithDebounce]);
   }, [setMarkdownWithDebounce]);
 
 
-  const save = useCallback(async(opts?: {overwriteScopesOfDescendants: boolean}): Promise<IPageHasId | null> => {
-    if (grantData == null || isSlackEnabled == null || currentPathname == null) {
+  const save = useCallback(async(opts?: {slackChannels: string, overwriteScopesOfDescendants?: boolean}): Promise<IPageHasId | null> => {
+    if (currentPathname == null || optionsToSave == 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');
     }
     }
 
 
-    const grant = grantData.grant || PageGrant.GRANT_PUBLIC;
-    const grantedGroup = grantData?.grantedGroup;
-
-    const optionsToSave: OptionsToSave = {
-      isSlackEnabled,
-      slackChannels,
-      grant: grant || 1,
-      pageTags: pageTags || [],
-      grantUserGroupId: grantedGroup?.id,
-      grantUserGroupName: grantedGroup?.name,
-      ...opts,
-    };
+    const options = Object.assign(optionsToSave, opts);
 
 
     try {
     try {
       const { page } = await saveOrUpdate(
       const { page } = await saveOrUpdate(
         markdownToSave.current,
         markdownToSave.current,
         { pageId, path: currentPagePath || currentPathname, revisionId: currentRevisionId },
         { pageId, path: currentPagePath || currentPathname, revisionId: currentRevisionId },
-        optionsToSave,
+        options,
       );
       );
 
 
       return page;
       return page;
@@ -232,9 +221,9 @@ const PageEditor = React.memo((): JSX.Element => {
     }
     }
 
 
   // eslint-disable-next-line max-len
   // eslint-disable-next-line max-len
-  }, [grantData, isSlackEnabled, currentPathname, slackChannels, pageTags, saveOrUpdate, pageId, currentPagePath, currentRevisionId]);
+  }, [currentPathname, optionsToSave, grantData, isSlackEnabled, saveOrUpdate, pageId, currentPagePath, currentRevisionId]);
 
 
-  const saveAndReturnToViewHandler = useCallback(async(opts?: {overwriteScopesOfDescendants: boolean}) => {
+  const saveAndReturnToViewHandler = useCallback(async(opts: {slackChannels: string, overwriteScopesOfDescendants?: boolean}) => {
     if (editorMode !== EditorMode.Editor) {
     if (editorMode !== EditorMode.Editor) {
       return;
       return;
     }
     }
@@ -248,10 +237,10 @@ const PageEditor = React.memo((): JSX.Element => {
       await router.push(`/${page._id}`);
       await router.push(`/${page._id}`);
     }
     }
     else {
     else {
-      updateStateAfterSave(page._id);
+      updateStateAfterSave?.();
     }
     }
     mutateEditorMode(EditorMode.View);
     mutateEditorMode(EditorMode.View);
-  }, [editorMode, save, isNotFound, mutateEditorMode, router, useUpdateStateAfterSave]);
+  }, [editorMode, save, isNotFound, mutateEditorMode, router, updateStateAfterSave]);
 
 
   const saveWithShortcut = useCallback(async() => {
   const saveWithShortcut = useCallback(async() => {
     if (editorMode !== EditorMode.Editor) {
     if (editorMode !== EditorMode.Editor) {
@@ -260,10 +249,10 @@ const PageEditor = React.memo((): JSX.Element => {
 
 
     const page = await save();
     const page = await save();
     if (page != null) {
     if (page != null) {
-      updateStateAfterSave(page._id);
+      updateStateAfterSave?.();
       toastSuccess(t('toaster.save_succeeded'));
       toastSuccess(t('toaster.save_succeeded'));
     }
     }
-  }, [editorMode, save, t, useUpdateStateAfterSave]);
+  }, [editorMode, save, t, updateStateAfterSave]);
 
 
 
 
   /**
   /**
@@ -540,7 +529,7 @@ const PageEditor = React.memo((): JSX.Element => {
         isOpen={conflictDiffModalStatus?.isOpened}
         isOpen={conflictDiffModalStatus?.isOpened}
         onClose={() => closeConflictDiffModal()}
         onClose={() => closeConflictDiffModal()}
         markdownOnEdit={markdownToPreview}
         markdownOnEdit={markdownToPreview}
-        optionsToSave={undefined} // replace undefined
+        optionsToSave={optionsToSave}
         afterResolvedHandler={afterResolvedHandler}
         afterResolvedHandler={afterResolvedHandler}
       />
       />
     </div>
     </div>

+ 0 - 112
packages/app/src/components/PageEditor/Cheatsheet.jsx

@@ -1,112 +0,0 @@
-/* eslint-disable max-len */
-
-import React from 'react';
-
-import PropTypes from 'prop-types';
-import { useTranslation } from 'next-i18next';
-
-class Cheatsheet extends React.Component {
-
-  render() {
-    const { t } = this.props;
-
-    return (
-      <div className="row small">
-        <div className="col-sm-6">
-          <h4>{t('sandbox.header')}</h4>
-          <ul className="hljs">
-            <li><code># </code>{t('sandbox.header_x', { index: '1' })}</li>
-            <li><code>## </code>{t('sandbox.header_x', { index: '2' })}</li>
-            <li><code>### </code>{t('sandbox.header_x', { index: '3' })}</li>
-          </ul>
-          <h4>{t('sandbox.block')}</h4>
-          <p className="mb-1"><code>[{t('sandbox.empty_line')}]</code>{t('sandbox.block_detail')}</p>
-          <ul className="hljs">
-            <li>text</li>
-            <li></li>
-            <li>text</li>
-          </ul>
-          <h4>{t('sandbox.line_break')}</h4>
-          <p className="mb-1"><code>[ ][ ]</code> {t('sandbox.line_break_detail')}</p>
-          <ul className="hljs">
-            <li>text&nbsp;&nbsp;</li>
-            <li>text</li>
-          </ul>
-          <h4>{t('sandbox.typography')}</h4>
-          <ul className="hljs">
-            <li><i>*{t('sandbox.italics')}*</i></li>
-            <li><b>**{t('sandbox.bold')}**</b></li>
-            <li><i><b>***{t('sandbox.italic_bold')}***</b></i></li>
-            <li>~~{t('sandbox.strikethrough')}~~ =&lt; <s>{t('sandbox.strikethrough')}</s></li>
-          </ul>
-          <h4>{t('sandbox.link')}</h4>
-          <ul className="hljs">
-            <li>[Google](https://www.google.co.jp/)</li>
-            <li>[/Page1/ChildPage1]</li>
-          </ul>
-          <h4>{t('sandbox.code_highlight')}</h4>
-          <ul className="hljs">
-            <li>```javascript:index.js</li>
-            <li>writeCode();</li>
-            <li>```</li>
-          </ul>
-        </div>
-        <div className="col-sm-6">
-          <h4>{t('sandbox.list')}</h4>
-          <ul className="hljs">
-            <li>- {t('sandbox.unordered_list_x', { index: '1' })}</li>
-            <li>&nbsp;&nbsp;- {t('sandbox.unordered_list_x', { index: '1.1' })}</li>
-            <li>- {t('sandbox.unordered_list_x', { index: '2' })}</li>
-          </ul>
-          <ul className="hljs">
-            <li>1. {t('sandbox.ordered_list_x', { index: '1' })}</li>
-            <li>1. {t('sandbox.ordered_list_x', { index: '2' })}</li>
-          </ul>
-          <ul className="hljs">
-            <li>- [ ] {t('sandbox.task')}({t('sandbox.task_unchecked')})</li>
-            <li>- [x] {t('sandbox.task')}({t('sandbox.task_checked')})</li>
-          </ul>
-          <h4>{t('sandbox.quote')}</h4>
-          <ul className="hljs">
-            <li>&gt; {t('sandbox.quote1')}</li>
-            <li>&gt; {t('sandbox.quote2')}</li>
-          </ul>
-          <ul className="hljs">
-            <li>&gt;&gt; {t('sandbox.quote_nested')}</li>
-            <li>&gt;&gt;&gt; {t('sandbox.quote_nested')}</li>
-            <li>&gt;&gt;&gt;&gt; {t('sandbox.quote_nested')}</li>
-          </ul>
-          <h4>{t('sandbox.table')}</h4>
-          <pre className="border-0">
-            |Left&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;|&nbsp;&nbsp;&nbsp;&nbsp;Mid&nbsp;&nbsp;&nbsp;&nbsp;|&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Right|<br />
-            |:----------|:---------:|----------:|<br />
-            |col 1&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;|&nbsp;&nbsp;&nbsp;col 2&nbsp;&nbsp;&nbsp;|&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;col 3|<br />
-            |col 1&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;|&nbsp;&nbsp;&nbsp;col 2&nbsp;&nbsp;&nbsp;|&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;col 3|<br />
-          </pre>
-          <h4>{t('sandbox.image')}</h4>
-          <p className="mb-1"><code> ![{t('sandbox.alt_text')}](URL)</code> {t('sandbox.insert_image')}</p>
-          <ul className="hljs">
-            <li>![ex](https://example.com/image.png)</li>
-          </ul>
-
-          <hr />
-          <a href="/Sandbox" className="btn btn-info btn-block" target="_blank">
-            <i className="icon-share-alt" /> {t('sandbox.open_sandbox')}
-          </a>
-        </div>
-      </div>
-    );
-  }
-
-}
-
-Cheatsheet.propTypes = {
-  t: PropTypes.func.isRequired, // i18next
-};
-
-const CheatsheetWrapperFC = (props) => {
-  const { t } = useTranslation();
-  return <Cheatsheet t={t} {...props} />;
-};
-
-export default CheatsheetWrapperFC;

+ 114 - 0
packages/app/src/components/PageEditor/Cheatsheet.tsx

@@ -0,0 +1,114 @@
+/* eslint-disable max-len */
+
+import React from 'react';
+
+import { useTranslation } from 'next-i18next';
+import { PrismAsyncLight } from 'react-syntax-highlighter';
+import { oneDark } from 'react-syntax-highlighter/dist/cjs/styles/prism';
+
+export const Cheatsheet = (): JSX.Element => {
+  const { t } = useTranslation();
+
+  /*
+  * Each Element
+  */
+  // Left Side
+  const codeStr = `# ${t('sandbox.header_x', { index: '1' })}\n## ${t('sandbox.header_x', { index: '2' })}\n### ${t('sandbox.header_x', { index: '3' })}`;
+  const codeBlockStr = 'text\n\ntext';
+  const lineBlockStr = 'text\ntext';
+  const typographyStr = `*${t('sandbox.italics')}*\n**${t('sandbox.bold')}**\n***${t('sandbox.italic_bold')}***\n~~${t('sandbox.strikethrough')}~~`;
+  const linkStr = '[Google](https://www.google.co.jp/)\n[/Page1/ChildPage1]';
+  const codeHighlightStr = '```javascript:index.js\nwriteCode();\n```';
+
+  // Right Side
+  const codeListStr = `- ${t('sandbox.unordered_list_x', { index: '1' })}
+    - ${t('sandbox.unordered_list_x', { index: '1.1' })}
+    - ${t('sandbox.unordered_list_x', { index: '1.2' })}`;
+  const orderedListStr = `1. ${t('sandbox.ordered_list_x', { index: '1' })}\n1. ${t('sandbox.ordered_list_x', { index: '2' })}`;
+  const taskStr = `- [ ] ${t('sandbox.task')}(${t('sandbox.task_unchecked')})\n- [x] ${t('sandbox.task')}(${t('sandbox.task_checked')})`;
+  const quoteStr = `> ${t('sandbox.quote1')}\n> ${t('sandbox.quote2')}`;
+  const nestedQuoteStr = `>> ${t('sandbox.quote_nested')}\n>>> ${t('sandbox.quote_nested')}\n>>>> ${t('sandbox.quote_nested')}`;
+  const tableStr = '|Left       |    Mid    |      Right|\n|:----------|:---------:|----------:|\n|col 1      |   col 2   |      col 3|\n|col 1      |   col 2   |      col 3|';
+  const imageStr = '![ex](https://example.com/image.png)';
+
+
+  const renderCheetSheetElm = (CheetSheetElm: string) => {
+    return (
+      <PrismAsyncLight
+        className="code-highlighted"
+        PreTag="div"
+        style={oneDark}
+        language={'text'}
+      >
+        {String(CheetSheetElm).replace(/\n$/, '')}
+      </PrismAsyncLight>
+    );
+  };
+
+
+  return (
+    <div className="row small">
+      <div className="col-sm-6">
+
+        {/* Header */}
+        <h4>{t('sandbox.header')}</h4>
+        {renderCheetSheetElm(codeStr)}
+
+        {/* Block */}
+        <h4>{t('sandbox.block')}</h4>
+        <p className="mb-1"><code>[{t('sandbox.empty_line')}]</code>{t('sandbox.block_detail')}</p>
+        {renderCheetSheetElm(codeBlockStr)}
+
+        {/* Line Break */}
+        <h4>{t('sandbox.line_break')}</h4>
+        <p className="mb-1"><code>[ ][ ]</code> {t('sandbox.line_break_detail')}</p>
+        {renderCheetSheetElm(lineBlockStr)}
+
+
+        {/* Typography */}
+        <h4>{t('sandbox.typography')}</h4>
+        {renderCheetSheetElm(typographyStr)}
+
+        {/* Link */}
+        <h4>{t('sandbox.link')}</h4>
+        {renderCheetSheetElm(linkStr)}
+
+        {/* CodeHhighlight */}
+        <h4>{t('sandbox.code_highlight')}</h4>
+        {renderCheetSheetElm(codeHighlightStr)}
+      </div>
+
+      <div className="col-sm-6">
+        {/* List */}
+        <h4>{t('sandbox.list')}</h4>
+        {renderCheetSheetElm(codeListStr)}
+
+        {renderCheetSheetElm(orderedListStr)}
+
+        {renderCheetSheetElm(taskStr)}
+
+        {/* Quote */}
+        <h4>{t('sandbox.quote')}</h4>
+        {renderCheetSheetElm(quoteStr)}
+
+        {renderCheetSheetElm(nestedQuoteStr)}
+
+
+        {/* Table */}
+        <h4>{t('sandbox.table')}</h4>
+        {renderCheetSheetElm(tableStr)}
+
+        {/* Image */}
+        <h4>{t('sandbox.image')}</h4>
+        <p className="mb-1"><code> ![{t('sandbox.alt_text')}](URL)</code> {t('sandbox.insert_image')}</p>
+        {renderCheetSheetElm(imageStr)}
+
+        <hr />
+        <a href="/Sandbox" className="btn btn-info btn-block" target="_blank">
+          <i className="icon-share-alt" /> {t('sandbox.open_sandbox')}
+        </a>
+      </div>
+    </div>
+  );
+
+};

+ 0 - 8
packages/app/src/components/PageEditor/Editor.module.scss

@@ -154,11 +154,3 @@
     }
     }
   }
   }
 }
 }
-
-.modal-gfm-cheatsheet :global {
-  .modal-body {
-    .hljs {
-      font-family: var(--font-family-monospace);
-    }
-  }
-}

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

@@ -18,7 +18,7 @@ import { useIsMobile } from '~/stores/ui';
 import { IEditorMethods } from '../../interfaces/editor-methods';
 import { IEditorMethods } from '../../interfaces/editor-methods';
 
 
 import AbstractEditor from './AbstractEditor';
 import AbstractEditor from './AbstractEditor';
-import Cheatsheet from './Cheatsheet';
+import { Cheatsheet } from './Cheatsheet';
 import CodeMirrorEditor from './CodeMirrorEditor';
 import CodeMirrorEditor from './CodeMirrorEditor';
 import pasteHelper from './PasteHelper';
 import pasteHelper from './PasteHelper';
 import TextAreaEditor from './TextAreaEditor';
 import TextAreaEditor from './TextAreaEditor';
@@ -246,7 +246,7 @@ const Editor: ForwardRefRenderFunction<IEditorMethods, EditorPropsType> = (props
     };
     };
 
 
     return (
     return (
-      <Modal isOpen={isCheatsheetModalShown} toggle={hideCheatsheetModal} className={`${styles['modal-gfm-cheatsheet']}`} >
+      <Modal isOpen={isCheatsheetModalShown} toggle={hideCheatsheetModal} className={`modal-gfm-cheatsheet ${styles['modal-gfm-cheatsheet']}`} >
         <ModalHeader tag="h4" toggle={hideCheatsheetModal} className="bg-primary text-light">
         <ModalHeader tag="h4" toggle={hideCheatsheetModal} className="bg-primary text-light">
           <i className="icon-fw icon-question" />Markdown help
           <i className="icon-fw icon-question" />Markdown help
         </ModalHeader>
         </ModalHeader>

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

@@ -4,6 +4,7 @@ import dynamic from 'next/dynamic';
 import { Collapse, Button } from 'reactstrap';
 import { Collapse, Button } from 'reactstrap';
 
 
 
 
+import { SavePageControlsProps } from '~/components/SavePageControls';
 import { useIsSlackConfigured } from '~/stores/context';
 import { useIsSlackConfigured } from '~/stores/context';
 import { useSWRxSlackChannels, useIsSlackEnabled } from '~/stores/editor';
 import { useSWRxSlackChannels, useIsSlackEnabled } from '~/stores/editor';
 import { useCurrentPagePath } from '~/stores/page';
 import { useCurrentPagePath } from '~/stores/page';
@@ -12,7 +13,7 @@ import {
 } from '~/stores/ui';
 } from '~/stores/ui';
 
 
 
 
-const SavePageControls = dynamic(() => import('~/components/SavePageControls').then(mod => mod.SavePageControls), { ssr: false });
+const SavePageControls = dynamic<SavePageControlsProps>(() => import('~/components/SavePageControls').then(mod => mod.SavePageControls), { ssr: false });
 const SlackLogo = dynamic(() => import('~/components/SlackLogo').then(mod => mod.SlackLogo), { ssr: false });
 const SlackLogo = dynamic(() => import('~/components/SlackLogo').then(mod => mod.SlackLogo), { ssr: false });
 const SlackNotification = dynamic(() => import('~/components/SlackNotification').then(mod => mod.SlackNotification), { ssr: false });
 const SlackNotification = dynamic(() => import('~/components/SlackNotification').then(mod => mod.SlackNotification), { ssr: false });
 const OptionsSelector = dynamic(() => import('~/components/PageEditor/OptionsSelector').then(mod => mod.OptionsSelector), { ssr: false });
 const OptionsSelector = dynamic(() => import('~/components/PageEditor/OptionsSelector').then(mod => mod.OptionsSelector), { ssr: false });
@@ -129,7 +130,7 @@ const EditorNavbarBottom = (): JSX.Element => {
               )}
               )}
             </div>
             </div>
           ))}
           ))}
-          <SavePageControls />
+          <SavePageControls slackChannels={slackChannelsStr} />
           { isCollapsedOptionsSelectorEnabled && renderExpandButton() }
           { isCollapsedOptionsSelectorEnabled && renderExpandButton() }
         </form>
         </form>
       </div>
       </div>

+ 38 - 41
packages/app/src/components/PageEditorByHackmd.tsx

@@ -1,5 +1,5 @@
 import React, {
 import React, {
-  useCallback, useRef, useState, useEffect,
+  useCallback, useRef, useState, useEffect, useMemo,
 } from 'react';
 } from 'react';
 
 
 import EventEmitter from 'events';
 import EventEmitter from 'events';
@@ -19,13 +19,13 @@ import {
   useCurrentPageId, useCurrentPathname, useHackmdUri, useIsNotFound,
   useCurrentPageId, useCurrentPathname, useHackmdUri, useIsNotFound,
 } from '~/stores/context';
 } from '~/stores/context';
 import {
 import {
-  useSWRxSlackChannels, useIsSlackEnabled, usePageTagsForEditors, useIsEnabledUnsavedWarning,
+  useIsSlackEnabled, usePageTagsForEditors, useIsEnabledUnsavedWarning,
 } from '~/stores/editor';
 } from '~/stores/editor';
 import {
 import {
   usePageIdOnHackmd, useHasDraftOnHackmd, useRevisionIdHackmdSynced, useIsHackmdDraftUpdatingInRealtime,
   usePageIdOnHackmd, useHasDraftOnHackmd, useRevisionIdHackmdSynced, useIsHackmdDraftUpdatingInRealtime,
 } from '~/stores/hackmd';
 } from '~/stores/hackmd';
 import { useCurrentPagePath, useSWRxCurrentPage, useSWRxTagsInfo } from '~/stores/page';
 import { useCurrentPagePath, useSWRxCurrentPage, useSWRxTagsInfo } from '~/stores/page';
-import { useRemoteRevisionId, useSetRemoteLatestPageData } from '~/stores/remote-latest-page';
+import { useRemoteRevisionId } from '~/stores/remote-latest-page';
 import {
 import {
   EditorMode,
   EditorMode,
   useEditorMode, useSelectedGrant,
   useEditorMode, useSelectedGrant,
@@ -56,12 +56,11 @@ export const PageEditorByHackmd = (): JSX.Element => {
   const { data: editorMode, mutate: mutateEditorMode } = useEditorMode();
   const { data: editorMode, mutate: mutateEditorMode } = useEditorMode();
   const { data: currentPagePath } = useCurrentPagePath();
   const { data: currentPagePath } = useCurrentPagePath();
   const { data: currentPathname } = useCurrentPathname();
   const { data: currentPathname } = useCurrentPathname();
-  const { data: slackChannelsData } = useSWRxSlackChannels(currentPagePath);
   const { data: isSlackEnabled } = useIsSlackEnabled();
   const { data: isSlackEnabled } = useIsSlackEnabled();
-  const { data: pageId, mutate: mutateCurrentPageId } = useCurrentPageId();
+  const { data: pageId } = useCurrentPageId();
   const { data: pageTags } = usePageTagsForEditors(pageId);
   const { data: pageTags } = usePageTagsForEditors(pageId);
   const { mutate: mutateTagsInfo } = useSWRxTagsInfo(pageId);
   const { mutate: mutateTagsInfo } = useSWRxTagsInfo(pageId);
-  const { data: grant } = useSelectedGrant();
+  const { data: grantData } = useSelectedGrant();
   const { data: hackmdUri } = useHackmdUri();
   const { data: hackmdUri } = useHackmdUri();
   const saveOrUpdate = useSaveOrUpdate();
   const saveOrUpdate = useSaveOrUpdate();
 
 
@@ -71,8 +70,6 @@ export const PageEditorByHackmd = (): JSX.Element => {
   const { data: pageData, mutate: mutatePageData } = useSWRxCurrentPage();
   const { data: pageData, mutate: mutatePageData } = useSWRxCurrentPage();
   const revision = pageData?.revision;
   const revision = pageData?.revision;
 
 
-  const slackChannels = slackChannelsData?.toString();
-
   const [isInitialized, setIsInitialized] = useState(false);
   const [isInitialized, setIsInitialized] = useState(false);
   const [isInitializing, setIsInitializing] = useState(false);
   const [isInitializing, setIsInitializing] = useState(false);
   // for error
   // for error
@@ -88,33 +85,38 @@ export const PageEditorByHackmd = (): JSX.Element => {
   const { data: isHackmdDraftUpdatingInRealtime, mutate: mutateIsHackmdDraftUpdatingInRealtime } = useIsHackmdDraftUpdatingInRealtime();
   const { data: isHackmdDraftUpdatingInRealtime, mutate: mutateIsHackmdDraftUpdatingInRealtime } = useIsHackmdDraftUpdatingInRealtime();
   const { data: remoteRevisionId, mutate: mutateRemoteRevisionId } = useRemoteRevisionId();
   const { data: remoteRevisionId, mutate: mutateRemoteRevisionId } = useRemoteRevisionId();
 
 
-  const updateStateAfterSave = useUpdateStateAfterSave();
+  const updateStateAfterSave = useUpdateStateAfterSave(pageId);
 
 
   const hackmdEditorRef = useRef<HackEditorRef>(null);
   const hackmdEditorRef = useRef<HackEditorRef>(null);
 
 
+  const optionsToSave = useMemo((): OptionsToSave | undefined => {
+    if (grantData == null) {
+      return;
+    }
+    const optionsToSave = {
+      isSlackEnabled: isSlackEnabled ?? false,
+      slackChannels: '', // set in save method by opts in SavePageControlls.tsx
+      grant: grantData.grant,
+      pageTags: pageTags ?? [],
+      grantUserGroupId: grantData.grantedGroup?.id,
+      grantUserGroupName: grantData.grantedGroup?.name,
+    };
+    return optionsToSave;
+  }, [grantData, isSlackEnabled, pageTags]);
+
   const saveAndReturnToViewHandler = useCallback(async(opts?: {overwriteScopesOfDescendants: boolean}) => {
   const saveAndReturnToViewHandler = useCallback(async(opts?: {overwriteScopesOfDescendants: boolean}) => {
     if (editorMode !== EditorMode.HackMD) { return }
     if (editorMode !== EditorMode.HackMD) { return }
 
 
     try {
     try {
-      if (isSlackEnabled == null || currentPathname == null || slackChannels == null || grant == null
-          || revision == null || hackmdEditorRef.current == null || revisionIdHackmdSynced == null) {
+      if (currentPathname == null || revision == null || hackmdEditorRef.current == null || revisionIdHackmdSynced == null || optionsToSave == null) {
         throw new Error('Some materials to save are invalid');
         throw new Error('Some materials to save are invalid');
       }
       }
 
 
-      const optionsToSave: OptionsToSave = {
-        isSlackEnabled,
-        slackChannels,
-        grant: grant.grant,
-        grantUserGroupId: grant.grantedGroup?.id,
-        grantUserGroupName: grant.grantedGroup?.name,
-        pageTags: pageTags ?? [],
-        isSyncRevisionToHackmd: true,
-        ...opts,
-      };
+      const options = Object.assign(optionsToSave, opts, { isSyncRevisionToHackmd: true });
 
 
       const markdown = await hackmdEditorRef.current.getValue();
       const markdown = await hackmdEditorRef.current.getValue();
 
 
-      const { page } = await saveOrUpdate(markdown, { pageId, path: currentPagePath || currentPathname, revisionId: revisionIdHackmdSynced }, optionsToSave);
+      const { page } = await saveOrUpdate(markdown, { pageId, path: currentPagePath || currentPathname, revisionId: revisionIdHackmdSynced }, options);
       await mutatePageData();
       await mutatePageData();
       await mutateTagsInfo();
       await mutateTagsInfo();
 
 
@@ -125,7 +127,7 @@ export const PageEditorByHackmd = (): JSX.Element => {
         await router.push(`/${page._id}`);
         await router.push(`/${page._id}`);
       }
       }
       else {
       else {
-        updateStateAfterSave(page._id);
+        updateStateAfterSave?.();
       }
       }
       setIsInitialized(false);
       setIsInitialized(false);
       mutateEditorMode(EditorMode.View);
       mutateEditorMode(EditorMode.View);
@@ -135,7 +137,7 @@ export const PageEditorByHackmd = (): JSX.Element => {
       toastError(error.message);
       toastError(error.message);
     }
     }
   // eslint-disable-next-line max-len
   // eslint-disable-next-line max-len
-  }, [editorMode, isSlackEnabled, currentPathname, slackChannels, grant, revision, revisionIdHackmdSynced, pageTags, saveOrUpdate, pageId, currentPagePath, mutatePageData, mutateTagsInfo, isNotFound, mutateEditorMode, router, useUpdateStateAfterSave]);
+  }, [editorMode, currentPathname, revision, revisionIdHackmdSynced, optionsToSave, saveOrUpdate, pageId, currentPagePath, mutatePageData, mutateTagsInfo, isNotFound, mutateEditorMode, router, updateStateAfterSave]);
 
 
   // set handler to save and reload Page
   // set handler to save and reload Page
   useEffect(() => {
   useEffect(() => {
@@ -160,6 +162,11 @@ export const PageEditorByHackmd = (): JSX.Element => {
     };
     };
   }, [resetInitializedStatusHandler]);
   }, [resetInitializedStatusHandler]);
 
 
+  useEffect(() => {
+    // for page translation: https://github.com/weseek/growi/pull/7100
+    setIsInitialized(false);
+  }, [pageId]);
+
 
 
   const isResume = useCallback(() => {
   const isResume = useCallback(() => {
     const isPageExistsOnHackmd = (pageIdOnHackmd != null);
     const isPageExistsOnHackmd = (pageIdOnHackmd != null);
@@ -238,25 +245,18 @@ export const PageEditorByHackmd = (): JSX.Element => {
     try {
     try {
       const currentPagePathOrPathname = currentPagePath || currentPathname;
       const currentPagePathOrPathname = currentPagePath || currentPathname;
       if (
       if (
-        isSlackEnabled == null || grant == null || slackChannels == null || pageId == null
-        || revisionIdHackmdSynced == null || currentPagePathOrPathname == null
+        pageId == null || revisionIdHackmdSynced == null || currentPagePathOrPathname == null || optionsToSave == null
       ) { throw new Error('Some materials to save are invalid') }
       ) { throw new Error('Some materials to save are invalid') }
-      const optionsToSave = {
-        isSlackEnabled,
-        slackChannels,
-        grant: grant.grant,
-        grantUserGroupId: grant.grantedGroup?.id,
-        grantUserGroupName: grant.grantedGroup?.name,
-        pageTags: pageTags ?? [],
-        isSyncRevisionToHackmd: true,
-      };
-      const res = await saveOrUpdate(markdown, { pageId, path: currentPagePathOrPathname, revisionId: revisionIdHackmdSynced }, optionsToSave);
+
+      const options = Object.assign(optionsToSave, { isSyncRevisionToHackmd: true });
+
+      const res = await saveOrUpdate(markdown, { pageId, path: currentPagePathOrPathname, revisionId: revisionIdHackmdSynced }, options);
 
 
       // update pageData
       // update pageData
       mutatePageData(res);
       mutatePageData(res);
 
 
       // set updated data
       // set updated data
-      updateStateAfterSave(res._id);
+      updateStateAfterSave?.();
       mutateTagsInfo();
       mutateTagsInfo();
 
 
       logger.debug('success to save');
       logger.debug('success to save');
@@ -267,10 +267,7 @@ export const PageEditorByHackmd = (): JSX.Element => {
       logger.error('failed to save', error);
       logger.error('failed to save', error);
       toastError(error.message);
       toastError(error.message);
     }
     }
-  }, [
-    currentPagePath, currentPathname, isSlackEnabled, grant, slackChannels, pageId, revisionIdHackmdSynced,
-    pageTags, saveOrUpdate, mutatePageData, useUpdateStateAfterSave, mutateTagsInfo, t,
-  ]);
+  }, [currentPagePath, currentPathname, pageId, revisionIdHackmdSynced, optionsToSave, saveOrUpdate, mutatePageData, updateStateAfterSave, mutateTagsInfo, t]);
 
 
   /**
   /**
    * onChange event of HackmdEditor handler
    * onChange event of HackmdEditor handler

+ 5 - 3
packages/app/src/components/PageStatusAlert.tsx

@@ -10,6 +10,7 @@ import {
 import { useConflictDiffModal } from '~/stores/modal';
 import { useConflictDiffModal } from '~/stores/modal';
 import { useSWRxCurrentPage } from '~/stores/page';
 import { useSWRxCurrentPage } from '~/stores/page';
 import { useRemoteRevisionId, useRemoteRevisionLastUpdateUser } from '~/stores/remote-latest-page';
 import { useRemoteRevisionId, useRemoteRevisionLastUpdateUser } from '~/stores/remote-latest-page';
+import { EditorMode, useEditorMode } from '~/stores/ui';
 
 
 import { Username } from './User/Username';
 import { Username } from './User/Username';
 
 
@@ -29,6 +30,7 @@ export const PageStatusAlert = (): JSX.Element => {
   const { data: isConflict } = useIsConflict();
   const { data: isConflict } = useIsConflict();
   const { mutate: mutateEditingMarkdown } = useEditingMarkdown();
   const { mutate: mutateEditingMarkdown } = useEditingMarkdown();
   const { open: openConflictDiffModal } = useConflictDiffModal();
   const { open: openConflictDiffModal } = useConflictDiffModal();
+  const { mutate: mutateEditorMode } = useEditorMode();
 
 
   // store remote latest page data
   // store remote latest page data
   const { data: revisionIdHackmdSynced } = useRevisionIdHackmdSynced();
   const { data: revisionIdHackmdSynced } = useRevisionIdHackmdSynced();
@@ -72,12 +74,12 @@ export const PageStatusAlert = (): JSX.Element => {
           {t('hackmd.this_page_has_draft')}
           {t('hackmd.this_page_has_draft')}
         </>,
         </>,
       btn:
       btn:
-        <a href="#hackmd" key="btnOpenHackmdPageHasDraft" className="btn btn-outline-white">
+        <button onClick={() => mutateEditorMode(EditorMode.HackMD)} className="btn btn-outline-white">
           <i className="fa fa-fw fa-file-text-o mr-1"></i>
           <i className="fa fa-fw fa-file-text-o mr-1"></i>
           Open HackMD Editor
           Open HackMD Editor
-        </a>,
+        </button>,
     };
     };
-  }, [t]);
+  }, [mutateEditorMode, t]);
 
 
   const getContentsForUpdatedAlert = useCallback((): AlertComponentContents => {
   const getContentsForUpdatedAlert = useCallback((): AlertComponentContents => {
 
 

+ 10 - 5
packages/app/src/components/SavePageControls.tsx

@@ -30,7 +30,12 @@ const logger = loggerFactory('growi:SavePageControls');
 
 
 const { isTopPage } = pagePathUtils;
 const { isTopPage } = pagePathUtils;
 
 
-export const SavePageControls = (): JSX.Element | null => {
+export type SavePageControlsProps = {
+  slackChannels: string
+}
+
+export const SavePageControls = (props: SavePageControlsProps): JSX.Element | null => {
+  const { slackChannels } = props;
   const { t } = useTranslation();
   const { t } = useTranslation();
   const { data: currentPagePath } = useCurrentPagePath();
   const { data: currentPagePath } = useCurrentPagePath();
   const { data: isEditable } = useIsEditable();
   const { data: isEditable } = useIsEditable();
@@ -45,13 +50,13 @@ export const SavePageControls = (): JSX.Element | null => {
 
 
   const save = useCallback(async(): Promise<void> => {
   const save = useCallback(async(): Promise<void> => {
     // save
     // save
-    globalEmitter.emit('saveAndReturnToView');
-  }, []);
+    globalEmitter.emit('saveAndReturnToView', { slackChannels });
+  }, [slackChannels]);
 
 
   const saveAndOverwriteScopesOfDescendants = useCallback(() => {
   const saveAndOverwriteScopesOfDescendants = useCallback(() => {
     // save
     // save
-    globalEmitter.emit('saveAndReturnToView', { overwriteScopesOfDescendants: true });
-  }, []);
+    globalEmitter.emit('saveAndReturnToView', { overwriteScopesOfDescendants: true, slackChannels });
+  }, [slackChannels]);
 
 
 
 
   if (isEditable == null || isAclEnabled == null || grantData == null) {
   if (isEditable == null || isAclEnabled == null || grantData == null) {

+ 5 - 0
packages/app/src/interfaces/rehype.ts

@@ -4,3 +4,8 @@ export const RehypeSanitizeOption = {
 } as const;
 } as const;
 
 
 export type RehypeSanitizeOption = typeof RehypeSanitizeOption[keyof typeof RehypeSanitizeOption];
 export type RehypeSanitizeOption = typeof RehypeSanitizeOption[keyof typeof RehypeSanitizeOption];
+
+export type RehypeSanitizeOptionConfig = {
+  isEnabledXssPrevention: boolean,
+  // Todo add types for custom sanitize option at https://redmine.weseek.co.jp/issues/109763
+}

+ 3 - 1
packages/app/src/interfaces/services/renderer.ts

@@ -1,5 +1,7 @@
 import { XssOptionConfig } from '~/services/xss/xssOption';
 import { XssOptionConfig } from '~/services/xss/xssOption';
 
 
+import { RehypeSanitizeOptionConfig } from '../rehype';
+
 
 
 export type RendererConfig = {
 export type RendererConfig = {
   isEnabledLinebreaks: boolean,
   isEnabledLinebreaks: boolean,
@@ -10,4 +12,4 @@ export type RendererConfig = {
 
 
   plantumlUri: string | null,
   plantumlUri: string | null,
   blockdiagUri: string | null,
   blockdiagUri: string | null,
-} & XssOptionConfig;
+} & XssOptionConfig & RehypeSanitizeOptionConfig;

+ 4 - 2
packages/app/src/pages/[[...path]].page.tsx

@@ -262,7 +262,7 @@ const GrowiPage: NextPage<Props> = (props: Props) => {
   useRevisionIdHackmdSynced(pageWithMeta?.data.revisionHackmdSynced);
   useRevisionIdHackmdSynced(pageWithMeta?.data.revisionHackmdSynced);
   useRemoteRevisionId(pageWithMeta?.data.revision._id);
   useRemoteRevisionId(pageWithMeta?.data.revision._id);
   usePageIdOnHackmd(pageWithMeta?.data.pageIdOnHackmd);
   usePageIdOnHackmd(pageWithMeta?.data.pageIdOnHackmd);
-  useHasDraftOnHackmd(pageWithMeta?.data.hasDraftOnHackmd);
+  useHasDraftOnHackmd(pageWithMeta?.data.hasDraftOnHackmd ?? false);
   // useIsNotCreatable(props.isForbidden || !isCreatablePage(pagePath)); // TODO: need to include props.isIdentical
   // useIsNotCreatable(props.isForbidden || !isCreatablePage(pagePath)); // TODO: need to include props.isIdentical
   useCurrentPathname(props.currentPathname);
   useCurrentPathname(props.currentPathname);
 
 
@@ -571,10 +571,12 @@ function injectServerConfigurations(context: GetServerSidePropsContext, props: P
     blockdiagUri: process.env.BLOCKDIAG_URI ?? null,
     blockdiagUri: process.env.BLOCKDIAG_URI ?? null,
 
 
     // XSS Options
     // XSS Options
-    isEnabledXssPrevention: configManager.getConfig('markdown', 'markdown:xss:isEnabledPrevention'),
     attrWhiteList: crowi.xssService.getAttrWhiteList(),
     attrWhiteList: crowi.xssService.getAttrWhiteList(),
     tagWhiteList: crowi.xssService.getTagWhiteList(),
     tagWhiteList: crowi.xssService.getTagWhiteList(),
     highlightJsStyleBorder: crowi.configManager.getConfig('crowi', 'customize:highlightJsStyleBorder'),
     highlightJsStyleBorder: crowi.configManager.getConfig('crowi', 'customize:highlightJsStyleBorder'),
+
+    // XSS: rehype-sanitize options
+    isEnabledXssPrevention: configManager.getConfig('markdown', 'markdown:rehypeSanitize:isEnabledPrevention'),
   };
   };
 
 
   props.sidebarConfig = {
   props.sidebarConfig = {

+ 0 - 2
packages/app/src/server/models/config.ts

@@ -150,8 +150,6 @@ export const defaultCrowiConfigs: { [key: string]: any } = {
 };
 };
 
 
 export const defaultMarkdownConfigs: { [key: string]: any } = {
 export const defaultMarkdownConfigs: { [key: string]: any } = {
-  'markdown:xss:isEnabledPrevention': true,
-  'markdown:xss:option': 2,
   'markdown:xss:tagWhiteList': [],
   'markdown:xss:tagWhiteList': [],
   'markdown:xss:attrWhiteList': [],
   'markdown:xss:attrWhiteList': [],
   'markdown:rehypeSanitize:isEnabledPrevention': true,
   'markdown:rehypeSanitize:isEnabledPrevention': true,

+ 8 - 8
packages/app/src/server/routes/apiv3/markdown-setting.js

@@ -125,8 +125,8 @@ module.exports = (crowi) => {
       isIndentSizeForced: await crowi.configManager.getConfig('markdown', 'markdown:isIndentSizeForced'),
       isIndentSizeForced: await crowi.configManager.getConfig('markdown', 'markdown:isIndentSizeForced'),
       pageBreakSeparator: await crowi.configManager.getConfig('markdown', 'markdown:presentation:pageBreakSeparator'),
       pageBreakSeparator: await crowi.configManager.getConfig('markdown', 'markdown:presentation:pageBreakSeparator'),
       pageBreakCustomSeparator: await crowi.configManager.getConfig('markdown', 'markdown:presentation:pageBreakCustomSeparator'),
       pageBreakCustomSeparator: await crowi.configManager.getConfig('markdown', 'markdown:presentation:pageBreakCustomSeparator'),
-      isEnabledXss: await crowi.configManager.getConfig('markdown', 'markdown:xss:isEnabledPrevention'),
-      xssOption: await crowi.configManager.getConfig('markdown', 'markdown:xss:option'),
+      isEnabledXss: await crowi.configManager.getConfig('markdown', 'markdown:rehypeSanitize:isEnabledPrevention'),
+      xssOption: await crowi.configManager.getConfig('markdown', 'markdown:rehypeSanitize:option'),
       tagWhiteList: await crowi.configManager.getConfig('markdown', 'markdown:xss:tagWhiteList'),
       tagWhiteList: await crowi.configManager.getConfig('markdown', 'markdown:xss:tagWhiteList'),
       attrWhiteList: await crowi.configManager.getConfig('markdown', 'markdown:xss:attrWhiteList'),
       attrWhiteList: await crowi.configManager.getConfig('markdown', 'markdown:xss:attrWhiteList'),
     };
     };
@@ -293,17 +293,17 @@ module.exports = (crowi) => {
     }
     }
 
 
     const reqestXssParams = {
     const reqestXssParams = {
-      'markdown:xss:isEnabledPrevention': req.body.isEnabledXss,
-      'markdown:xss:option': req.body.xssOption,
-      'markdown:xss:tagWhiteList': req.body.tagWhiteList,
-      'markdown:xss:attrWhiteList': req.body.attrWhiteList,
+      'markdown:rehypeSanitize:isEnabledPrevention': req.body.isEnabledXss,
+      'markdown:rehypeSanitize:option': req.body.xssOption,
+      'markdown:xss:tagWhiteList': req.body.tagWhiteList, // Todo: need to be changed at https://redmine.weseek.co.jp/issues/109763
+      'markdown:xss:attrWhiteList': req.body.attrWhiteList, // Todo: need to be changed at https://redmine.weseek.co.jp/issues/109763
     };
     };
 
 
     try {
     try {
       await crowi.configManager.updateConfigsInTheSameNamespace('markdown', reqestXssParams);
       await crowi.configManager.updateConfigsInTheSameNamespace('markdown', reqestXssParams);
       const xssParams = {
       const xssParams = {
-        isEnabledXss: await crowi.configManager.getConfig('markdown', 'markdown:xss:isEnabledPrevention'),
-        xssOption: await crowi.configManager.getConfig('markdown', 'markdown:xss:option'),
+        isEnabledXss: await crowi.configManager.getConfig('markdown', 'markdown:rehypeSanitize:isEnabledPrevention'),
+        xssOption: await crowi.configManager.getConfig('markdown', 'markdown:rehypeSanitize:option'),
         tagWhiteList: await crowi.configManager.getConfig('markdown', 'markdown:xss:tagWhiteList'),
         tagWhiteList: await crowi.configManager.getConfig('markdown', 'markdown:xss:tagWhiteList'),
         attrWhiteList: await crowi.configManager.getConfig('markdown', 'markdown:xss:attrWhiteList'),
         attrWhiteList: await crowi.configManager.getConfig('markdown', 'markdown:xss:attrWhiteList'),
       };
       };