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

Merge pull request #6535 from weseek/feat/edit-and-save

feat: Edit and save
Yuki Takei 3 лет назад
Родитель
Сommit
c0c5aca798

+ 0 - 88
packages/app/src/client/services/EditorContainer.js

@@ -1,88 +0,0 @@
-import { Container } from 'unstated';
-
-import loggerFactory from '~/utils/logger';
-
-const logger = loggerFactory('growi:services:EditorContainer');
-
-
-/*
-* TODO: Omit Unstated: EditorContainer
-* => https://redmine.weseek.co.jp/issues/103246
-*/
-
-
-/**
- * Service container related to options for Editor/Preview
- * @extends {Container} unstated Container
- */
-export default class EditorContainer extends Container {
-
-  constructor(appContainer) {
-    super();
-
-    this.appContainer = appContainer;
-    this.appContainer.registerContainer(this);
-
-    this.state = {
-      tags: null,
-    };
-
-    this.initDrafts();
-
-  }
-
-  /**
-   * Workaround for the mangling in production build to break constructor.name
-   */
-  static getClassName() {
-    return 'EditorContainer';
-  }
-
-  /**
-   * initialize state for drafts
-   */
-  initDrafts() {
-    this.drafts = {};
-
-    // restore data from localStorage
-    const contents = window.localStorage.drafts;
-    if (contents != null) {
-      try {
-        this.drafts = JSON.parse(contents);
-      }
-      catch (e) {
-        window.localStorage.removeItem('drafts');
-      }
-    }
-
-    if (this.state.pageId == null) {
-      const draft = this.findDraft(this.state.path);
-      if (draft != null) {
-        this.state.markdown = draft;
-      }
-    }
-  }
-
-  clearDraft(path) {
-    delete this.drafts[path];
-    window.localStorage.setItem('drafts', JSON.stringify(this.drafts));
-  }
-
-  clearAllDrafts() {
-    window.localStorage.removeItem('drafts');
-  }
-
-  saveDraft(path, body) {
-    this.drafts[path] = body;
-    window.localStorage.setItem('drafts', JSON.stringify(this.drafts));
-  }
-
-  findDraft(path) {
-    if (this.drafts != null && this.drafts[path]) {
-      return this.drafts[path];
-    }
-
-    return null;
-  }
-
-}

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

@@ -97,8 +97,8 @@ export const resumeRenameOperation = async(pageId: string): Promise<void> => {
   await apiv3Post('/pages/resume-rename', { pageId });
   await apiv3Post('/pages/resume-rename', { pageId });
 };
 };
 
 
-
-export const createPage = async(pagePath: string, markdown: string, tmpParams: OptionsToSave) => {
+// TODO: define return type
+const createPage = async(pagePath: string, markdown: string, tmpParams: OptionsToSave) => {
   // clone
   // clone
   const params = Object.assign(tmpParams, {
   const params = Object.assign(tmpParams, {
     path: pagePath,
     path: pagePath,
@@ -111,7 +111,8 @@ export const createPage = async(pagePath: string, markdown: string, tmpParams: O
   return { page, tags, revision };
   return { page, tags, revision };
 };
 };
 
 
-export const updatePage = async(pageId: string, revisionId: string, markdown: string, tmpParams: OptionsToSave) => {
+// TODO: define return type
+const updatePage = async(pageId: string, revisionId: string, markdown: string, tmpParams: OptionsToSave) => {
   // clone
   // clone
   const params = Object.assign(tmpParams, {
   const params = Object.assign(tmpParams, {
     page_id: pageId,
     page_id: pageId,
@@ -132,8 +133,8 @@ type PageInfo= {
   revisionId: Nullable<string>,
   revisionId: Nullable<string>,
 }
 }
 
 
-
-export const saveAndReload = async(optionsToSave: OptionsToSave, pageInfo: PageInfo, markdown: string) => {
+// TODO: define return type
+export const saveOrUpdate = async(optionsToSave: OptionsToSave, pageInfo: PageInfo, markdown: string) => {
   const { path, pageId, revisionId } = pageInfo;
   const { path, pageId, revisionId } = pageInfo;
 
 
   const options = Object.assign({}, optionsToSave);
   const options = Object.assign({}, optionsToSave);
@@ -168,12 +169,5 @@ export const saveAndReload = async(optionsToSave: OptionsToSave, pageInfo: PageI
     res = await updatePage(pageId, revisionId, markdown, options);
     res = await updatePage(pageId, revisionId, markdown, options);
   }
   }
 
 
-  /*
-  * TODO: implement Draft function => https://redmine.weseek.co.jp/issues/103246
-  */
-  // const editorContainer = this.appContainer.getContainer('EditorContainer');
-  // editorContainer.clearDraft(path);
-  window.location.href = path;
-
   return res;
   return res;
 };
 };

+ 7 - 6
packages/app/src/components/MyDraftList/MyDraftList.jsx

@@ -3,7 +3,6 @@ import React from 'react';
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
 
 
-import EditorContainer from '~/client/services/EditorContainer';
 import PageContainer from '~/client/services/PageContainer';
 import PageContainer from '~/client/services/PageContainer';
 import { apiGet } from '~/client/util/apiv1-client';
 import { apiGet } from '~/client/util/apiv1-client';
 
 
@@ -105,7 +104,7 @@ class MyDraftList extends React.Component {
   }
   }
 
 
   clearDraft(path) {
   clearDraft(path) {
-    this.props.editorContainer.clearDraft(path);
+    // this.props.editorContainer.clearDraft(path);
 
 
     this.setState((prevState) => {
     this.setState((prevState) => {
       return {
       return {
@@ -116,7 +115,7 @@ class MyDraftList extends React.Component {
   }
   }
 
 
   clearAllDrafts() {
   clearAllDrafts() {
-    this.props.editorContainer.clearAllDrafts();
+    // this.props.editorContainer.clearAllDrafts();
 
 
     this.setState({
     this.setState({
       drafts: [],
       drafts: [],
@@ -175,7 +174,7 @@ MyDraftList.propTypes = {
   t: PropTypes.func.isRequired, // i18next
   t: PropTypes.func.isRequired, // i18next
 
 
   pageContainer: PropTypes.instanceOf(PageContainer).isRequired,
   pageContainer: PropTypes.instanceOf(PageContainer).isRequired,
-  editorContainer: PropTypes.instanceOf(EditorContainer).isRequired,
+  // editorContainer: PropTypes.instanceOf(EditorContainer).isRequired,
 };
 };
 
 
 const MyDraftListWrapperFC = (props) => {
 const MyDraftListWrapperFC = (props) => {
@@ -183,9 +182,11 @@ const MyDraftListWrapperFC = (props) => {
   return <MyDraftList t={t} {...props} />;
   return <MyDraftList t={t} {...props} />;
 };
 };
 
 
+export default MyDraftListWrapperFC;
+
 /**
 /**
  * Wrapper component for using unstated
  * Wrapper component for using unstated
  */
  */
-const MyDraftListWrapper = withUnstatedContainers(MyDraftListWrapperFC, [PageContainer, EditorContainer]);
+// const MyDraftListWrapper = withUnstatedContainers(MyDraftListWrapperFC, [PageContainer, EditorContainer]);
 
 
-export default MyDraftListWrapper;
+// export default MyDraftListWrapper;

+ 85 - 136
packages/app/src/components/PageEditor.tsx

@@ -8,10 +8,7 @@ import { envUtils, PageGrant } from '@growi/core';
 import detectIndent from 'detect-indent';
 import detectIndent from 'detect-indent';
 import { throttle, debounce } from 'throttle-debounce';
 import { throttle, debounce } from 'throttle-debounce';
 
 
-import { saveAndReload } from '~/client/services/page-operation';
-
-// import EditorContainer from '~/client/services/EditorContainer';
-// import PageContainer from '~/client/services/PageContainer';
+import { saveOrUpdate } from '~/client/services/page-operation';
 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 {
 import {
@@ -34,10 +31,7 @@ import loggerFactory from '~/utils/logger';
 import Editor from './PageEditor/Editor';
 import Editor from './PageEditor/Editor';
 import Preview from './PageEditor/Preview';
 import Preview from './PageEditor/Preview';
 import scrollSyncHelper from './PageEditor/ScrollSyncHelper';
 import scrollSyncHelper from './PageEditor/ScrollSyncHelper';
-// import { withUnstatedContainers } from './UnstatedUtils';
-
 
 
-// TODO: remove this when omitting unstated is completed
 
 
 const logger = loggerFactory('growi:PageEditor');
 const logger = loggerFactory('growi:PageEditor');
 
 
@@ -55,7 +49,6 @@ type EditorRef = {
 
 
 type Props = {
 type Props = {
   // pageContainer: PageContainer,
   // pageContainer: PageContainer,
-  // editorContainer: EditorContainer,
 
 
   // isEditable: boolean,
   // isEditable: boolean,
 
 
@@ -82,37 +75,34 @@ let isOriginOfScrollSyncPreview = false;
 
 
 const PageEditor = React.memo((props: Props): JSX.Element => {
 const PageEditor = React.memo((props: Props): JSX.Element => {
   // const {
   // const {
-  //   pageContainer, editorContainer,
+  //   pageContainer,
   // } = props;
   // } = props;
 
 
-  const { data: isEditable } = useIsEditable();
-  const { data: editorMode } = useEditorMode();
-  const { data: isMobile } = useIsMobile();
-  const { data: isSlackEnabled } = useIsSlackEnabled();
   const { data: pageId } = useCurrentPageId();
   const { data: pageId } = useCurrentPageId();
-  const { data: pageTags } = usePageTagsForEditors(pageId);
   const { data: currentPagePath } = useCurrentPagePath();
   const { data: currentPagePath } = useCurrentPagePath();
   const { data: currentPathname } = useCurrentPathname();
   const { data: currentPathname } = useCurrentPathname();
-  const { data: slackChannelsData } = useSWRxSlackChannels(currentPagePath);
+  const { data: currentPage, mutate: mutateCurrentPage } = useSWRxCurrentPage();
   const { data: grantData, mutate: mutateGrant } = useSelectedGrant();
   const { data: grantData, mutate: mutateGrant } = useSelectedGrant();
+  const { data: pageTags } = usePageTagsForEditors(pageId);
+
+  const { data: isEditable } = useIsEditable();
+  const { data: editorMode, mutate: mutateEditorMode } = useEditorMode();
+  const { data: isMobile } = useIsMobile();
+  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: indentSize, mutate: mutateCurrentIndentSize } = useCurrentIndentSize();
   const { data: indentSize, mutate: mutateCurrentIndentSize } = useCurrentIndentSize();
-  const { mutate: mutateIsEnabledUnsavedWarning } = useIsEnabledUnsavedWarning();
+  const { data: isEnabledUnsavedWarning, mutate: mutateIsEnabledUnsavedWarning } = useIsEnabledUnsavedWarning();
   const { data: isUploadableFile } = useIsUploadableFile();
   const { data: isUploadableFile } = useIsUploadableFile();
   const { data: isUploadableImage } = useIsUploadableImage();
   const { data: isUploadableImage } = useIsUploadableImage();
-  const { data: currentPage } = useSWRxCurrentPage();
 
 
   const { data: rendererOptions } = usePreviewOptions();
   const { data: rendererOptions } = usePreviewOptions();
 
 
-  const [markdown, setMarkdown] = useState<string>('');
-
+  const currentRevisionId = currentPage?.revision?._id;
+  const initialValue = currentPage?.revision?.body;
 
 
-  useEffect(() => {
-    if (currentPage != null) {
-      setMarkdown(currentPage.revision?.body);
-    }
-  }, [currentPage, currentPage?.revision?.body]);
+  const [markdown, setMarkdown] = useState<string>(initialValue ?? '');
 
 
   const slackChannels = useMemo(() => (slackChannelsData ? slackChannelsData.toString() : ''), []);
   const slackChannels = useMemo(() => (slackChannelsData ? slackChannelsData.toString() : ''), []);
 
 
@@ -121,49 +111,66 @@ const PageEditor = React.memo((props: Props): JSX.Element => {
   const previewRef = useRef<HTMLDivElement>(null);
   const previewRef = useRef<HTMLDivElement>(null);
 
 
   const setMarkdownWithDebounce = useMemo(() => debounce(50, throttle(100, value => setMarkdown(value))), []);
   const setMarkdownWithDebounce = useMemo(() => debounce(50, throttle(100, value => setMarkdown(value))), []);
-  // const saveDraftWithDebounce = useMemo(() => debounce(800, () => {
-  //   editorContainer.saveDraft(pageContainer.state.path, markdown);
-  // }), [editorContainer, markdown, pageContainer.state.path]);
+
 
 
   const markdownChangedHandler = useCallback((value: string): void => {
   const markdownChangedHandler = useCallback((value: string): void => {
     setMarkdownWithDebounce(value);
     setMarkdownWithDebounce(value);
-    // only when the first time to edit
-    // if (!pageContainer.state.revisionId) {
-    //   saveDraftWithDebounce();
-    // }
-  // }, [pageContainer.state.revisionId, saveDraftWithDebounce, setMarkdownWithDebounce]);
   }, [setMarkdownWithDebounce]);
   }, [setMarkdownWithDebounce]);
 
 
-
-  const saveWithShortcut = useCallback(async() => {
-    if (grantData == null) {
-      return;
+  const save = useCallback(async(opts?: {overwriteScopesOfDescendants: boolean}) => {
+    if (grantData == null || isSlackEnabled == null || currentPathname == null) {
+      logger.error('Some materials to save are invalid', { grantData, isSlackEnabled, currentPathname });
+      throw new Error('Some materials to save are invalid');
     }
     }
 
 
-    const optionsToSave = getOptionsToSave(
-      isSlackEnabled ?? false, slackChannels,
-      grantData.grant, grantData.grantedGroup?.id, grantData.grantedGroup?.name,
-      pageTags || [],
+    const grant = grantData.grant || PageGrant.GRANT_PUBLIC;
+    const grantedGroup = grantData?.grantedGroup;
+
+    const optionsToSave = Object.assign(
+      getOptionsToSave(isSlackEnabled, slackChannels, grant || 1, grantedGroup?.id, grantedGroup?.name, pageTags || []),
+      { ...opts },
     );
     );
 
 
     try {
     try {
-      // disable unsaved warning
+      await saveOrUpdate(optionsToSave, { pageId, path: currentPagePath || currentPathname, revisionId: currentRevisionId }, markdown);
+      await mutateCurrentPage();
       mutateIsEnabledUnsavedWarning(false);
       mutateIsEnabledUnsavedWarning(false);
-
-      // eslint-disable-next-line no-unused-vars
-      // const { tags } = await pageContainer.save(markdown, editorMode, optionsToSave);
-      logger.debug('success to save');
-
-      // pageContainer.showSuccessToastr();
-
-      // update state of EditorContainer
-      // editorContainer.setState({ tags });
     }
     }
     catch (error) {
     catch (error) {
       logger.error('failed to save', error);
       logger.error('failed to save', error);
       // pageContainer.showErrorToastr(error);
       // pageContainer.showErrorToastr(error);
+      if (error.code === 'conflict') {
+        // pageContainer.setState({
+        //   remoteRevisionId: error.data.revisionId,
+        //   remoteRevisionBody: error.data.revisionBody,
+        //   remoteRevisionUpdateAt: error.data.createdAt,
+        //   lastUpdateUser: error.data.user,
+        // });
+      }
     }
     }
-  }, [grantData, isSlackEnabled, slackChannels, pageTags, mutateIsEnabledUnsavedWarning]);
+
+  // eslint-disable-next-line max-len
+  }, [grantData, isSlackEnabled, currentPathname, slackChannels, pageTags, pageId, currentPagePath, currentRevisionId, markdown, mutateCurrentPage, mutateIsEnabledUnsavedWarning]);
+
+  const saveAndReturnToViewHandler = useCallback(async(opts?: {overwriteScopesOfDescendants: boolean}) => {
+    if (editorMode !== EditorMode.Editor) {
+      return;
+    }
+
+    await save(opts);
+    mutateEditorMode(EditorMode.View);
+  }, [editorMode, save, mutateEditorMode]);
+
+  const saveWithShortcut = useCallback(async() => {
+    if (editorMode !== EditorMode.Editor) {
+      return;
+    }
+
+    await save();
+
+    // TODO: show toastr
+    // pageContainer.showErrorToastr(error);
+  }, [editorMode, save]);
 
 
 
 
   /**
   /**
@@ -221,7 +228,6 @@ const PageEditor = React.memo((props: Props): JSX.Element => {
     finally {
     finally {
       editorRef.current.terminateUploadingState();
       editorRef.current.terminateUploadingState();
     }
     }
-  // }, [editorMode, mutateGrant, pageContainer]);
   }, [currentPagePath, mutateGrant, pageId]);
   }, [currentPagePath, mutateGrant, pageId]);
 
 
 
 
@@ -342,68 +348,14 @@ const PageEditor = React.memo((props: Props): JSX.Element => {
     };
     };
   }, []);
   }, []);
 
 
-
-  const saveAndReloadHandler = useCallback(async(opts?: {overwriteScopesOfDescendants: boolean}) => {
-    if (editorMode !== EditorMode.Editor) {
-      return;
-    }
-
-    const grant = grantData?.grant || PageGrant.GRANT_PUBLIC;
-    const grantedGroup = grantData?.grantedGroup;
-
-    if (isSlackEnabled == null || currentPathname == null) {
-      return;
-    }
-
-    let optionsToSave;
-
-    const currentOptionsToSave = getOptionsToSave(isSlackEnabled, slackChannels, grant || 1, grantedGroup?.id, grantedGroup?.name, pageTags || []);
-
-    if (opts != null) {
-      optionsToSave = Object.assign(currentOptionsToSave, {
-        ...opts,
-      });
-    }
-    else {
-      optionsToSave = currentOptionsToSave;
-    }
-
-    try {
-      await saveAndReload(optionsToSave, { pageId, path: currentPagePath || currentPathname, revisionId: currentPage?.revision?._id }, markdown);
-    }
-    catch (error) {
-      logger.error('failed to save', error);
-      // pageContainer.showErrorToastr(error);
-      if (error.code === 'conflict') {
-        // pageContainer.setState({
-        //   remoteRevisionId: error.data.revisionId,
-        //   remoteRevisionBody: error.data.revisionBody,
-        //   remoteRevisionUpdateAt: error.data.createdAt,
-        //   lastUpdateUser: error.data.user,
-        // });
-      }
-    }
-  }, [currentPage?.revision?._id,
-      currentPagePath,
-      currentPathname,
-      editorMode,
-      grantData?.grant,
-      grantData?.grantedGroup,
-      isSlackEnabled,
-      markdown,
-      pageId,
-      pageTags,
-      slackChannels,
-  ]);
-
-  // set handler to save and reload Page
+  // set handler to save and return to View
   useEffect(() => {
   useEffect(() => {
-    globalEmitter.on('saveAndReload', saveAndReloadHandler);
+    globalEmitter.on('saveAndReturnToView', saveAndReturnToViewHandler);
 
 
     return function cleanup() {
     return function cleanup() {
-      globalEmitter.removeListener('saveAndReload', saveAndReloadHandler);
+      globalEmitter.removeListener('saveAndReturnToView', saveAndReturnToViewHandler);
     };
     };
-  }, [saveAndReloadHandler]);
+  }, [saveAndReturnToViewHandler]);
 
 
   // set handler to focus
   // set handler to focus
   useEffect(() => {
   useEffect(() => {
@@ -412,27 +364,31 @@ const PageEditor = React.memo((props: Props): JSX.Element => {
     }
     }
   }, [editorMode]);
   }, [editorMode]);
 
 
+  // Unnecessary code. Delete after PageEditor and PageEditorByHackmd implementation has completed. -- 2022.09.06 Yuki Takei
+  //
   // set handler to update editor value
   // set handler to update editor value
-  useEffect(() => {
-    const handler = (markdown) => {
-      if (editorRef.current != null) {
-        editorRef.current.setValue(markdown);
-      }
-    };
-    globalEmitter.on('updateEditorValue', handler);
+  // useEffect(() => {
+  //   const handler = (markdown) => {
+  //     if (editorRef.current != null) {
+  //       editorRef.current.setValue(markdown);
+  //     }
+  //   };
+  //   globalEmitter.on('updateEditorValue', handler);
 
 
-    return function cleanup() {
-      globalEmitter.removeListener('updateEditorValue', handler);
-    };
-  }, []);
+  //   return function cleanup() {
+  //     globalEmitter.removeListener('updateEditorValue', handler);
+  //   };
+  // }, []);
 
 
-  // Displays an alert if there is a difference with pageContainer's markdown
-  // useEffect(() => {
-  //   // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-  //   if (pageContainer.state.markdown! !== markdown) {
-  //     mutateIsEnabledUnsavedWarning(true);
-  //   }
-  // }, [editorContainer, markdown, mutateIsEnabledUnsavedWarning, pageContainer.state.markdown]);
+  // Displays an alert if there is a difference with original markdown body
+  useEffect(() => {
+    if (initialValue == null || isEnabledUnsavedWarning) {
+      return;
+    }
+    if (initialValue !== markdown) {
+      mutateIsEnabledUnsavedWarning(true);
+    }
+  }, [initialValue, isEnabledUnsavedWarning, markdown, mutateIsEnabledUnsavedWarning]);
 
 
   // Detect indent size from contents (only when users are allowed to change it)
   // Detect indent size from contents (only when users are allowed to change it)
   // useEffect(() => {
   // useEffect(() => {
@@ -456,13 +412,12 @@ const PageEditor = React.memo((props: Props): JSX.Element => {
 
 
   const isUploadable = isUploadableImage || isUploadableFile;
   const isUploadable = isUploadableImage || isUploadableFile;
 
 
-
   return (
   return (
     <div className="d-flex flex-wrap">
     <div className="d-flex flex-wrap">
       <div className="page-editor-editor-container flex-grow-1 flex-basis-0 mw-0">
       <div className="page-editor-editor-container flex-grow-1 flex-basis-0 mw-0">
         <Editor
         <Editor
           ref={editorRef}
           ref={editorRef}
-          value={markdown}
+          value={initialValue}
           isUploadable={isUploadable}
           isUploadable={isUploadable}
           isUploadableFile={isUploadableFile}
           isUploadableFile={isUploadableFile}
           isTextlintEnabled={isTextlintEnabled}
           isTextlintEnabled={isTextlintEnabled}
@@ -495,10 +450,4 @@ const PageEditor = React.memo((props: Props): JSX.Element => {
 });
 });
 PageEditor.displayName = 'PageEditor';
 PageEditor.displayName = 'PageEditor';
 
 
-/**
-   * Wrapper component for using unstated
-   */
-// const PageEditorWrapper = withUnstatedContainers(PageEditor, [PageContainer, EditorContainer]);
-
-// export default PageEditorWrapper;
 export default PageEditor;
 export default PageEditor;

+ 1 - 0
packages/app/src/components/PageEditor/AbstractEditor.tsx

@@ -1,5 +1,6 @@
 /* eslint-disable @typescript-eslint/no-unused-vars */
 /* eslint-disable @typescript-eslint/no-unused-vars */
 import React from 'react';
 import React from 'react';
+
 import { ICodeMirror } from 'react-codemirror2';
 import { ICodeMirror } from 'react-codemirror2';
 
 
 
 

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

@@ -1,7 +1,7 @@
 import React from 'react';
 import React from 'react';
 
 
 import { createValidator } from '@growi/codemirror-textlint';
 import { createValidator } from '@growi/codemirror-textlint';
-import * as codemirror from 'codemirror';
+import { commands } from 'codemirror';
 import { JSHINT } from 'jshint';
 import { JSHINT } from 'jshint';
 import * as loadCssSync from 'load-css-file';
 import * as loadCssSync from 'load-css-file';
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
@@ -39,14 +39,6 @@ import styles from './CodeMirrorEditor.module.scss';
 window.JSHINT = JSHINT;
 window.JSHINT = JSHINT;
 window.kuromojin = { dicPath: '/static/dict' };
 window.kuromojin = { dicPath: '/static/dict' };
 
 
-// set save handler
-codemirror.commands.save = (instance) => {
-  if (instance.codeMirrorEditor != null) {
-    instance.codeMirrorEditor.dispatchSave();
-  }
-};
-// set CodeMirror instance as 'CodeMirror' so that CDN addons can reference
-window.CodeMirror = require('codemirror');
 require('codemirror/addon/display/placeholder');
 require('codemirror/addon/display/placeholder');
 require('codemirror/addon/edit/matchbrackets');
 require('codemirror/addon/edit/matchbrackets');
 require('codemirror/addon/edit/matchtags');
 require('codemirror/addon/edit/matchtags');
@@ -107,7 +99,6 @@ class CodeMirrorEditor extends AbstractEditor {
     this.logger = loggerFactory('growi:PageEditor:CodeMirrorEditor');
     this.logger = loggerFactory('growi:PageEditor:CodeMirrorEditor');
 
 
     this.state = {
     this.state = {
-      value: this.props.value,
       isGfmMode: this.props.isGfmMode,
       isGfmMode: this.props.isGfmMode,
       isLoadingKeymap: false,
       isLoadingKeymap: false,
       isSimpleCheatsheetShown: this.props.isGfmMode && this.props.value.length === 0,
       isSimpleCheatsheetShown: this.props.isGfmMode && this.props.value.length === 0,
@@ -252,7 +243,6 @@ class CodeMirrorEditor extends AbstractEditor {
    * @inheritDoc
    * @inheritDoc
    */
    */
   setValue(newValue) {
   setValue(newValue) {
-    this.setState({ value: newValue });
     this.getCodeMirror().getDoc().setValue(newValue);
     this.getCodeMirror().getDoc().setValue(newValue);
   }
   }
 
 
@@ -508,7 +498,7 @@ class CodeMirrorEditor extends AbstractEditor {
    */
    */
   handleEnterKey() {
   handleEnterKey() {
     if (!this.state.isGfmMode) {
     if (!this.state.isGfmMode) {
-      codemirror.commands.newlineAndIndent(this.getCodeMirror());
+      commands.newlineAndIndent(this.getCodeMirror());
       return;
       return;
     }
     }
 
 
@@ -1002,8 +992,6 @@ class CodeMirrorEditor extends AbstractEditor {
           //   editor.on('paste', this.pasteHandler);
           //   editor.on('paste', this.pasteHandler);
           //   editor.on('scrollCursorIntoView', this.scrollCursorIntoViewHandler);
           //   editor.on('scrollCursorIntoView', this.scrollCursorIntoViewHandler);
           // }}
           // }}
-          // temporary set props.value
-          // value={this.state.value}
           value={this.props.value}
           value={this.props.value}
           options={{
           options={{
             indentUnit: this.props.indentSize,
             indentUnit: this.props.indentSize,

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

@@ -48,14 +48,14 @@ export const SavePageControls = (props: Props): JSX.Element | null => {
     mutateIsEnabledUnsavedWarning(false);
     mutateIsEnabledUnsavedWarning(false);
 
 
     // save
     // save
-    (window as CustomWindow).globalEmitter.emit('saveAndReload');
+    (window as CustomWindow).globalEmitter.emit('saveAndReturnToView');
   }, [mutateIsEnabledUnsavedWarning]);
   }, [mutateIsEnabledUnsavedWarning]);
 
 
   const saveAndOverwriteScopesOfDescendants = useCallback(() => {
   const saveAndOverwriteScopesOfDescendants = useCallback(() => {
     // disable unsaved warning
     // disable unsaved warning
     mutateIsEnabledUnsavedWarning(false);
     mutateIsEnabledUnsavedWarning(false);
     // save
     // save
-    (window as CustomWindow).globalEmitter.emit('saveAndReload', { overwriteScopesOfDescendants: true });
+    (window as CustomWindow).globalEmitter.emit('saveAndReturnToView', { overwriteScopesOfDescendants: true });
   }, [mutateIsEnabledUnsavedWarning]);
   }, [mutateIsEnabledUnsavedWarning]);
 
 
 
 

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

@@ -99,7 +99,7 @@ export const SidebarNav: FC<Props> = (props: Props) => {
       </div>
       </div>
       <div className="grw-sidebar-nav-secondary-container">
       <div className="grw-sidebar-nav-secondary-container">
         {isAdmin && <SecondaryItem label="Admin" iconName="settings" href="/admin" />}
         {isAdmin && <SecondaryItem label="Admin" iconName="settings" href="/admin" />}
-        <SecondaryItem label="Draft" iconName="file_copy" href="/me/drafts" />
+        {/* <SecondaryItem label="Draft" iconName="file_copy" href="/me/drafts" /> */}
         <SecondaryItem label="Help" iconName="help" href="https://docs.growi.org" isBlank />
         <SecondaryItem label="Help" iconName="help" href="https://docs.growi.org" isBlank />
         <SecondaryItem label="Trash" iconName="delete" href="/trash" />
         <SecondaryItem label="Trash" iconName="delete" href="/trash" />
       </div>
       </div>

+ 62 - 54
packages/app/src/components/UncontrolledCodeMirror.tsx

@@ -1,81 +1,89 @@
 import React, {
 import React, {
-  forwardRef, ReactNode, Ref,
+  useCallback, useRef, MutableRefObject,
 } from 'react';
 } from 'react';
 
 
-import { Editor } from 'codemirror';
+import { commands, Editor } from 'codemirror';
 import { ICodeMirror, UnControlled as CodeMirror } from 'react-codemirror2';
 import { ICodeMirror, UnControlled as CodeMirror } from 'react-codemirror2';
 
 
-import AbstractEditor, { AbstractEditorProps } from '~/components/PageEditor/AbstractEditor';
+// set save handler
+// CommandActions in @types/codemirror does not include 'save' but actualy exists
+// https://codemirror.net/5/doc/manual.html#commands
+(commands as any).save = (instance) => {
+  if (instance.codeMirrorEditor != null) {
+    instance.codeMirrorEditor.dispatchSave();
+  }
+};
 
 
 window.CodeMirror = require('codemirror');
 window.CodeMirror = require('codemirror');
 require('codemirror/addon/display/placeholder');
 require('codemirror/addon/display/placeholder');
 require('~/client/util/codemirror/gfm-growi.mode');
 require('~/client/util/codemirror/gfm-growi.mode');
 
 
-export interface UncontrolledCodeMirrorProps extends AbstractEditorProps {
+export interface UncontrolledCodeMirrorProps extends ICodeMirror {
   value: string;
   value: string;
-  options?: ICodeMirror['options'];
   isGfmMode?: boolean;
   isGfmMode?: boolean;
   lineNumbers?: boolean;
   lineNumbers?: boolean;
+  onScrollCursorIntoView?: (line: number) => void;
+  onSave?: () => Promise<void>;
+  onPasteFiles?: (event: Event) => void;
+  onCtrlEnter?: (event: Event) => void;
 }
 }
 
 
-interface UncontrolledCodeMirrorCoreProps extends UncontrolledCodeMirrorProps {
-  forwardedRef: Ref<UncontrolledCodeMirrorCore>;
-}
-
-export class UncontrolledCodeMirrorCore extends AbstractEditor<UncontrolledCodeMirrorCoreProps> {
+export const UncontrolledCodeMirror = React.forwardRef<CodeMirror|null, UncontrolledCodeMirrorProps>((props, forwardedRef): JSX.Element => {
 
 
-  editor: Editor;
+  const wrapperRef = useRef<CodeMirror|null>();
 
 
-  // wrapperRef: RefObject<any>;
+  const editorRef = useRef<Editor>();
 
 
-  constructor(props: UncontrolledCodeMirrorCoreProps) {
-    super(props);
-    this.editorDidMount = this.editorDidMount.bind(this);
-    this.editorWillUnmount = this.editorWillUnmount.bind(this);
-  }
+  const editorDidMountHandler = useCallback((editor: Editor): void => {
+    editorRef.current = editor;
+  }, []);
 
 
-  editorDidMount(e: Editor): void {
-    this.editor = e;
-  }
-
-  editorWillUnmount(): void {
+  const editorWillUnmountHandler = useCallback((): void => {
     // workaround to fix editor duplicating by https://github.com/scniro/react-codemirror2/issues/284#issuecomment-1155928554
     // workaround to fix editor duplicating by https://github.com/scniro/react-codemirror2/issues/284#issuecomment-1155928554
-    (this.editor as any).display.wrapper.remove();
-  }
+    if (editorRef.current != null) {
+      (editorRef.current as any).display.wrapper.remove();
+    }
+    if (wrapperRef.current != null) {
+      (wrapperRef.current as any).hydrated = false;
+    }
+  }, []);
+
+  const {
+    value, lineNumbers, options,
+    ...rest
+  } = props;
+
+  // default true
+  const isGfmMode = rest.isGfmMode ?? true;
 
 
-  override render(): ReactNode {
-
-    const {
-      value, isGfmMode, lineNumbers, options, forwardedRef,
-      ...rest
-    } = this.props;
-
-    return (
-      <CodeMirror
-        ref={forwardedRef}
-        value={value}
-        options={{
-          lineNumbers: lineNumbers ?? true,
-          mode: isGfmMode ? 'gfm-growi' : undefined,
-          tabSize: 4,
-          ...options,
-        }}
-        editorDidMount={this.editorDidMount}
-        editorWillUnmount={this.editorWillUnmount}
-        {...rest}
-      />
-    );
-  }
-
-}
-
-export const UncontrolledCodeMirror = forwardRef<UncontrolledCodeMirrorCore, UncontrolledCodeMirrorProps>((props, ref) => {
   return (
   return (
-    <UncontrolledCodeMirrorCore
-      {...props}
-      forwardedRef={ref}
+    <CodeMirror
+      ref={(elem) => {
+        // register to wrapperRef
+        wrapperRef.current = elem;
+        // register to forwardedRef
+        if (forwardedRef != null) {
+          if (typeof forwardedRef === 'function') {
+            forwardedRef(elem);
+          }
+          else {
+            (forwardedRef as MutableRefObject<CodeMirror|null>).current = elem;
+          }
+        }
+      }}
+      value={value}
+      options={{
+        lineNumbers: lineNumbers ?? true,
+        mode: isGfmMode ? 'gfm-growi' : undefined,
+        tabSize: 4,
+        ...options,
+      }}
+      editorDidMount={editorDidMountHandler}
+      editorWillUnmount={editorWillUnmountHandler}
+      {...rest}
     />
     />
   );
   );
+
 });
 });
 
 
 UncontrolledCodeMirror.displayName = 'UncontrolledCodeMirror';
 UncontrolledCodeMirror.displayName = 'UncontrolledCodeMirror';

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

@@ -49,7 +49,7 @@ type Props = CommonProps & {
 };
 };
 
 
 const PersonalSettings = dynamic(() => import('~/components/Me/PersonalSettings'), { ssr: false });
 const PersonalSettings = dynamic(() => import('~/components/Me/PersonalSettings'), { ssr: false });
-const MyDraftList = dynamic(() => import('~/components/MyDraftList/MyDraftList'), { ssr: false });
+// const MyDraftList = dynamic(() => import('~/components/MyDraftList/MyDraftList'), { ssr: false });
 const InAppNotificationPage = dynamic(
 const InAppNotificationPage = dynamic(
   () => import('~/components/InAppNotification/InAppNotificationPage').then(mod => mod.InAppNotificationPage), { ssr: false },
   () => import('~/components/InAppNotification/InAppNotificationPage').then(mod => mod.InAppNotificationPage), { ssr: false },
 );
 );
@@ -66,10 +66,10 @@ const MePage: NextPage<Props> = (props: Props) => {
         title: t('User Settings'),
         title: t('User Settings'),
         component: <PersonalSettings />,
         component: <PersonalSettings />,
       },
       },
-      drafts: {
-        title: t('My Drafts'),
-        component: <MyDraftList />,
-      },
+      // drafts: {
+      //   title: t('My Drafts'),
+      //   component: <MyDraftList />,
+      // },
       'all-in-app-notifications': {
       'all-in-app-notifications': {
         title: t('in_app_notification.notification_list'),
         title: t('in_app_notification.notification_list'),
         component: <InAppNotificationPage />,
         component: <InAppNotificationPage />,