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

Merge branch 'feat/pt-dev-4' into feat/pt-dev-master

Taichi Masuyama 4 лет назад
Родитель
Сommit
bb2e1920ac
38 измененных файлов с 473 добавлено и 432 удалено
  1. 1 0
      packages/app/package.json
  2. 2 6
      packages/app/src/client/app.jsx
  3. 31 31
      packages/app/src/client/legacy/crowi.js
  4. 26 9
      packages/app/src/client/services/ContextExtractor.tsx
  5. 55 137
      packages/app/src/client/services/NavigationContainer.js
  6. 5 5
      packages/app/src/client/services/PageContainer.js
  7. 0 0
      packages/app/src/client/services/user-ui-settings.ts
  8. 5 1
      packages/app/src/components/Fab.jsx
  9. 9 11
      packages/app/src/components/Hotkeys/Subscribers/CreatePage.jsx
  10. 4 2
      packages/app/src/components/Hotkeys/Subscribers/EditPage.jsx
  11. 3 3
      packages/app/src/components/Icons/GrowiLogo.jsx
  12. 0 115
      packages/app/src/components/Navbar/GrowiNavbar.jsx
  13. 128 0
      packages/app/src/components/Navbar/GrowiNavbar.tsx
  14. 8 4
      packages/app/src/components/Navbar/GrowiNavbarBottom.jsx
  15. 13 12
      packages/app/src/components/Navbar/GrowiSubNavigation.jsx
  16. 17 15
      packages/app/src/components/Navbar/PageEditorModeManager.jsx
  17. 4 2
      packages/app/src/components/Navbar/PersonalDropdown.jsx
  18. 3 3
      packages/app/src/components/Navbar/SubNavButtons.jsx
  19. 13 9
      packages/app/src/components/Page/DisplaySwitcher.jsx
  20. 13 13
      packages/app/src/components/Page/NotFoundAlert.jsx
  21. 8 7
      packages/app/src/components/PageCreateModal.jsx
  22. 15 9
      packages/app/src/components/PageEditor/EditorNavbarBottom.jsx
  23. 4 3
      packages/app/src/components/Sidebar.tsx
  24. 1 1
      packages/app/src/components/Sidebar/PageTree/PrivateLegacyPages.tsx
  25. 1 1
      packages/app/src/components/Sidebar/SidebarNav.tsx
  26. 6 9
      packages/app/src/components/StickyStretchableScroller.jsx
  27. 2 1
      packages/app/src/interfaces/user-ui-settings.ts
  28. 2 2
      packages/app/src/server/models/page.ts
  29. 4 0
      packages/app/src/server/routes/index.js
  30. 1 1
      packages/app/src/server/routes/page.js
  31. 7 0
      packages/app/src/server/routes/private-legacy-pages.ts
  32. 1 1
      packages/app/src/server/routes/search.js
  33. 1 1
      packages/app/src/server/service/page.js
  34. 4 7
      packages/app/src/server/service/search.ts
  35. 16 0
      packages/app/src/server/views/private-legacy-pages.html
  36. 39 10
      packages/app/src/stores/ui.tsx
  37. 9 1
      packages/app/src/styles/_sidebar.scss
  38. 12 0
      yarn.lock

+ 1 - 0
packages/app/package.json

@@ -161,6 +161,7 @@
     "@handsontable/react": "=2.1.0",
     "@handsontable/react": "=2.1.0",
     "@types/compression": "^1.7.0",
     "@types/compression": "^1.7.0",
     "@types/express": "^4.17.11",
     "@types/express": "^4.17.11",
+    "@types/jquery": "^3.5.8",
     "@types/multer": "^1.4.5",
     "@types/multer": "^1.4.5",
     "@types/react-dom": "^17.0.9",
     "@types/react-dom": "^17.0.9",
     "autoprefixer": "^9.0.0",
     "autoprefixer": "^9.0.0",

+ 2 - 6
packages/app/src/client/app.jsx

@@ -41,6 +41,7 @@ import PersonalSettings from '../components/Me/PersonalSettings';
 import GrowiSubNavigation from '../components/Navbar/GrowiSubNavigation';
 import GrowiSubNavigation from '../components/Navbar/GrowiSubNavigation';
 import GrowiSubNavigationSwitcher from '../components/Navbar/GrowiSubNavigationSwitcher';
 import GrowiSubNavigationSwitcher from '../components/Navbar/GrowiSubNavigationSwitcher';
 
 
+import ContextExtractor from '~/client/services/ContextExtractor';
 import NavigationContainer from '~/client/services/NavigationContainer';
 import NavigationContainer from '~/client/services/NavigationContainer';
 import PageContainer from '~/client/services/PageContainer';
 import PageContainer from '~/client/services/PageContainer';
 import PageHistoryContainer from '~/client/services/PageHistoryContainer';
 import PageHistoryContainer from '~/client/services/PageHistoryContainer';
@@ -50,7 +51,6 @@ import EditorContainer from '~/client/services/EditorContainer';
 import TagContainer from '~/client/services/TagContainer';
 import TagContainer from '~/client/services/TagContainer';
 import PersonalContainer from '~/client/services/PersonalContainer';
 import PersonalContainer from '~/client/services/PersonalContainer';
 import PageAccessoriesContainer from '~/client/services/PageAccessoriesContainer';
 import PageAccessoriesContainer from '~/client/services/PageAccessoriesContainer';
-import ContextExtractor from '~/client/services/ContextExtractor';
 
 
 import { appContainer, componentMappings } from './base';
 import { appContainer, componentMappings } from './base';
 
 
@@ -99,7 +99,6 @@ Object.assign(componentMappings, {
 
 
   'not-found-page': <NotFoundPage />,
   'not-found-page': <NotFoundPage />,
   'not-found-alert': <NotFoundAlert
   'not-found-alert': <NotFoundAlert
-    onPageCreateClicked={navigationContainer.setEditorMode}
     isGuestUserMode={appContainer.isGuestUser}
     isGuestUserMode={appContainer.isGuestUser}
     isHidden={pageContainer.state.isNotCreatable || pageContainer.state.isTrashPage}
     isHidden={pageContainer.state.isNotCreatable || pageContainer.state.isTrashPage}
   />,
   />,
@@ -118,8 +117,6 @@ Object.assign(componentMappings, {
   'duplicated-alert': <DuplicatedAlert />,
   'duplicated-alert': <DuplicatedAlert />,
   'redirected-alert': <RedirectedAlert />,
   'redirected-alert': <RedirectedAlert />,
   'renamed-alert': <RenamedAlert />,
   'renamed-alert': <RenamedAlert />,
-
-  'growi-context-extractor': <ContextExtractor />, // use static swr
 });
 });
 
 
 // additional definitions if data exists
 // additional definitions if data exists
@@ -181,7 +178,7 @@ const elem = document.getElementById('growi-context-extractor');
 if (elem != null) {
 if (elem != null) {
   ReactDOM.render(
   ReactDOM.render(
     <SWRConfig value={swrGlobalConfiguration}>
     <SWRConfig value={swrGlobalConfiguration}>
-      {componentMappings['growi-context-extractor']}
+      <ContextExtractor></ContextExtractor>
     </SWRConfig>,
     </SWRConfig>,
     elem,
     elem,
     renderMainComponents,
     renderMainComponents,
@@ -191,6 +188,5 @@ else {
   renderMainComponents();
   renderMainComponents();
 }
 }
 
 
-
 // initialize scrollpos-styler
 // initialize scrollpos-styler
 ScrollPosStyler.init();
 ScrollPosStyler.init();

+ 31 - 31
packages/app/src/client/legacy/crowi.js

@@ -17,7 +17,7 @@ window.Crowi = Crowi;
 Crowi.setCaretLineData = function(line) {
 Crowi.setCaretLineData = function(line) {
   const { appContainer } = window;
   const { appContainer } = window;
   const navigationContainer = appContainer.getContainer('NavigationContainer');
   const navigationContainer = appContainer.getContainer('NavigationContainer');
-  navigationContainer.setEditorMode('edit');
+  // navigationContainer.setEditorMode('edit');
   const pageEditorDom = document.querySelector('#page-editor');
   const pageEditorDom = document.querySelector('#page-editor');
   pageEditorDom.setAttribute('data-caret-line', line);
   pageEditorDom.setAttribute('data-caret-line', line);
 };
 };
@@ -154,32 +154,32 @@ Crowi.blinkSelectedSection = function(hash) {
   }
   }
 };
 };
 
 
-window.addEventListener('load', () => {
-  const { appContainer } = window;
-  const pageContainer = appContainer.getContainer('PageContainer');
-
-  // Do nothing if the page does not exist
-  // ex.) admin page,login page
-  if (pageContainer == null) {
-    return null;
-  }
-  const { isAbleToOpenPageEditor } = pageContainer;
-
-  // hash on page
-  if (window.location.hash) {
-    const navigationContainer = appContainer.getContainer('NavigationContainer');
-
-    if (window.location.hash === '#edit' && isAbleToOpenPageEditor) {
-      navigationContainer.setEditorMode('edit');
-
-      // focus
-      Crowi.setCaretLineAndFocusToEditor();
-    }
-    else if (window.location.hash === '#hackmd') {
-      navigationContainer.setEditorMode('hackmd');
-    }
-  }
-});
+// window.addEventListener('load', () => {
+//   const { appContainer } = window;
+//   const pageContainer = appContainer.getContainer('PageContainer');
+
+//   // Do nothing if the page does not exist
+//   // ex.) admin page,login page
+//   if (pageContainer == null) {
+//     return null;
+//   }
+//   const { isAbleToOpenPageEditor } = pageContainer;
+
+//   // hash on page
+//   if (window.location.hash) {
+//     const navigationContainer = appContainer.getContainer('NavigationContainer');
+
+//     if (window.location.hash === '#edit' && isAbleToOpenPageEditor) {
+//       navigationContainer.setEditorMode('edit');
+
+//       // focus
+//       Crowi.setCaretLineAndFocusToEditor();
+//     }
+//     else if (window.location.hash === '#hackmd') {
+//       navigationContainer.setEditorMode('hackmd');
+//     }
+//   }
+// });
 
 
 window.addEventListener('load', () => {
 window.addEventListener('load', () => {
   const crowi = window.crowi;
   const crowi = window.crowi;
@@ -228,18 +228,18 @@ window.addEventListener('hashchange', (e) => {
   Crowi.unblinkSelectedSection(Crowi.findHashFromUrl(e.oldURL));
   Crowi.unblinkSelectedSection(Crowi.findHashFromUrl(e.oldURL));
   Crowi.blinkSelectedSection(Crowi.findHashFromUrl(e.newURL));
   Crowi.blinkSelectedSection(Crowi.findHashFromUrl(e.newURL));
   Crowi.modifyScrollTop();
   Crowi.modifyScrollTop();
-  const { appContainer } = window;
-  const navigationContainer = appContainer.getContainer('NavigationContainer');
+  // const { appContainer } = window;
+  // const navigationContainer = appContainer.getContainer('NavigationContainer');
 
 
 
 
   // hash on page
   // hash on page
   if (window.location.hash) {
   if (window.location.hash) {
     if (window.location.hash === '#edit') {
     if (window.location.hash === '#edit') {
-      navigationContainer.setEditorMode('edit');
+      // navigationContainer.setEditorMode('edit');
       Crowi.setCaretLineAndFocusToEditor();
       Crowi.setCaretLineAndFocusToEditor();
     }
     }
     else if (window.location.hash === '#hackmd') {
     else if (window.location.hash === '#hackmd') {
-      navigationContainer.setEditorMode('hackmd');
+      // navigationContainer.setEditorMode('hackmd');
     }
     }
   }
   }
 });
 });

+ 26 - 9
packages/app/src/client/services/ContextExtractor.tsx

@@ -1,4 +1,4 @@
-import React, { FC } from 'react';
+import React, { FC, useEffect, useState } from 'react';
 import { pagePathUtils } from '@growi/core';
 import { pagePathUtils } from '@growi/core';
 
 
 import {
 import {
@@ -9,14 +9,25 @@ import {
 } from '../../stores/context';
 } from '../../stores/context';
 
 
 import {
 import {
-  useEditorMode, useIsDeviceSmallerThanMd, usePreferDrawerModeByUser, usePreferDrawerModeOnEditByUser,
+  EditorMode, useEditorMode, useIsDeviceSmallerThanMd, usePreferDrawerModeByUser, usePreferDrawerModeOnEditByUser,
 } from '~/stores/ui';
 } from '~/stores/ui';
 
 
 const { isTrashPage: _isTrashPage } = pagePathUtils;
 const { isTrashPage: _isTrashPage } = pagePathUtils;
 
 
 const jsonNull = 'null';
 const jsonNull = 'null';
 
 
-const ContextExtractor: FC = () => {
+const getInitialEditorMode = (): EditorMode => {
+  switch (window.location.hash) {
+    case '#edit':
+      return EditorMode.Editor;
+    case '#hackmd':
+      return EditorMode.HackMD;
+    default:
+      return EditorMode.View;
+  }
+};
+
+const ContextExtractorOnce: FC = () => {
 
 
   const mainContent = document.querySelector('#content-main');
   const mainContent = document.querySelector('#content-main');
 
 
@@ -63,7 +74,7 @@ const ContextExtractor: FC = () => {
   useCurrentUser(currentUser);
   useCurrentUser(currentUser);
 
 
   // Navigation
   // Navigation
-  useEditorMode();
+  useEditorMode(getInitialEditorMode());
   usePreferDrawerModeByUser();
   usePreferDrawerModeByUser();
   usePreferDrawerModeOnEditByUser();
   usePreferDrawerModeOnEditByUser();
   useIsDeviceSmallerThanMd();
   useIsDeviceSmallerThanMd();
@@ -97,11 +108,17 @@ const ContextExtractor: FC = () => {
   useRevisionAuthor(revisionAuthor);
   useRevisionAuthor(revisionAuthor);
   useTargetAndAncestors(targetAndAncestors);
   useTargetAndAncestors(targetAndAncestors);
 
 
-  return (
-    <div>
-      {/* Render nothing */}
-    </div>
-  );
+  return null;
 };
 };
 
 
+const ContextExtractor: FC = React.memo(() => {
+  const [isRunOnce, setRunOnce] = useState(false);
+
+  useEffect(() => {
+    setRunOnce(true);
+  }, []);
+
+  return isRunOnce ? null : <ContextExtractorOnce></ContextExtractorOnce>;
+});
+
 export default ContextExtractor;
 export default ContextExtractor;

+ 55 - 137
packages/app/src/client/services/NavigationContainer.js

@@ -22,26 +22,12 @@ export default class NavigationContainer extends Container {
     const { localStorage } = window;
     const { localStorage } = window;
 
 
     this.state = {
     this.state = {
-      editorMode: 'view',
-
-      isDeviceSmallerThanMd: null,
-      preferDrawerModeByUser: localStorage.preferDrawerModeByUser === 'true',
-      preferDrawerModeOnEditByUser: // default: true
-        localStorage.preferDrawerModeOnEditByUser == null || localStorage.preferDrawerModeOnEditByUser === 'true',
-      isDrawerMode: null,
-      isDrawerOpened: false,
-
-      sidebarContentsId: localStorage.sidebarContentsId || 'recent',
+      // editorMode: 'view',
 
 
       isScrollTop: true,
       isScrollTop: true,
-
-      isPageCreateModalShown: false,
     };
     };
 
 
-    this.openPageCreateModal = this.openPageCreateModal.bind(this);
-    this.closePageCreateModal = this.closePageCreateModal.bind(this);
-    this.setEditorMode = this.setEditorMode.bind(this);
-    this.initDeviceSize();
+    // this.setEditorMode = this.setEditorMode.bind(this);
     this.initScrollEvent();
     this.initScrollEvent();
   }
   }
 
 
@@ -56,26 +42,6 @@ export default class NavigationContainer extends Container {
     return this.appContainer.getContainer('PageContainer');
     return this.appContainer.getContainer('PageContainer');
   }
   }
 
 
-  initDeviceSize() {
-    const mdOrAvobeHandler = async(mql) => {
-      let isDeviceSmallerThanMd;
-
-      // sm -> md
-      if (mql.matches) {
-        isDeviceSmallerThanMd = false;
-      }
-      // md -> sm
-      else {
-        isDeviceSmallerThanMd = true;
-      }
-
-      this.setState({ isDeviceSmallerThanMd });
-      this.updateDrawerMode({ ...this.state, isDeviceSmallerThanMd }); // generate newest state object
-    };
-
-    this.appContainer.addBreakpointListener('md', mdOrAvobeHandler, true);
-  }
-
   initScrollEvent() {
   initScrollEvent() {
     window.addEventListener('scroll', () => {
     window.addEventListener('scroll', () => {
       const currentYOffset = window.pageYOffset;
       const currentYOffset = window.pageYOffset;
@@ -91,80 +57,49 @@ export default class NavigationContainer extends Container {
     });
     });
   }
   }
 
 
-  setEditorMode(editorMode) {
-    const { isNotCreatable } = this.getPageContainer().state;
-
-    if (this.appContainer.currentUser == null) {
-      logger.warn('Please login or signup to edit the page or use hackmd.');
-      return;
-    }
-
-    if (isNotCreatable) {
-      logger.warn('This page could not edit.');
-      return;
-    }
-
-    this.setState({ editorMode });
-    if (editorMode === 'view') {
-      $('body').removeClass('on-edit');
-      $('body').removeClass('builtin-editor');
-      $('body').removeClass('hackmd');
-      $('body').removeClass('pathname-sidebar');
-      window.history.replaceState(null, '', window.location.pathname);
-    }
-
-    if (editorMode === 'edit') {
-      $('body').addClass('on-edit');
-      $('body').addClass('builtin-editor');
-      $('body').removeClass('hackmd');
-      // editing /Sidebar
-      if (window.location.pathname === '/Sidebar') {
-        $('body').addClass('pathname-sidebar');
-      }
-      window.location.hash = '#edit';
-    }
-
-    if (editorMode === 'hackmd') {
-      $('body').addClass('on-edit');
-      $('body').addClass('hackmd');
-      $('body').removeClass('builtin-editor');
-      $('body').removeClass('pathname-sidebar');
-      window.location.hash = '#hackmd';
-    }
-
-    this.updateDrawerMode({ ...this.state, editorMode }); // generate newest state object
-  }
-
-  toggleDrawer() {
-    const { isDrawerOpened } = this.state;
-    this.setState({ isDrawerOpened: !isDrawerOpened });
-  }
-
-  /**
-   * Set Sidebar mode preference by user
-   * @param {boolean} preferDockMode
-   */
-  async setDrawerModePreference(bool) {
-    this.setState({ preferDrawerModeByUser: bool });
-    this.updateDrawerMode({ ...this.state, preferDrawerModeByUser: bool }); // generate newest state object
-
-    // store settings to localStorage
-    const { localStorage } = window;
-    localStorage.preferDrawerModeByUser = bool;
-  }
-
-  /**
-   * Set Sidebar mode preference by user
-   * @param {boolean} preferDockMode
-   */
-  async setDrawerModePreferenceOnEdit(bool) {
-    this.setState({ preferDrawerModeOnEditByUser: bool });
-    this.updateDrawerMode({ ...this.state, preferDrawerModeOnEditByUser: bool }); // generate newest state object
-
-    // store settings to localStorage
-    const { localStorage } = window;
-    localStorage.preferDrawerModeOnEditByUser = bool;
-  }
+  // setEditorMode(editorMode) {
+  //   const { isNotCreatable } = this.getPageContainer().state;
+
+  //   if (this.appContainer.currentUser == null) {
+  //     logger.warn('Please login or signup to edit the page or use hackmd.');
+  //     return;
+  //   }
+
+  //   if (isNotCreatable) {
+  //     logger.warn('This page could not edit.');
+  //     return;
+  //   }
+
+  //   this.setState({ editorMode });
+  //   if (editorMode === 'view') {
+  //     $('body').removeClass('on-edit');
+  //     $('body').removeClass('builtin-editor');
+  //     $('body').removeClass('hackmd');
+  //     $('body').removeClass('pathname-sidebar');
+  //     window.history.replaceState(null, '', window.location.pathname);
+  //   }
+
+  //   if (editorMode === 'edit') {
+  //     $('body').addClass('on-edit');
+  //     $('body').addClass('builtin-editor');
+  //     $('body').removeClass('hackmd');
+  //     // editing /Sidebar
+  //     if (window.location.pathname === '/Sidebar') {
+  //       $('body').addClass('pathname-sidebar');
+  //     }
+  //     window.location.hash = '#edit';
+  //   }
+
+  //   if (editorMode === 'hackmd') {
+  //     $('body').addClass('on-edit');
+  //     $('body').addClass('hackmd');
+  //     $('body').removeClass('builtin-editor');
+  //     $('body').removeClass('pathname-sidebar');
+  //     window.location.hash = '#hackmd';
+  //   }
+
+  //   this.updateDrawerMode({ ...this.state, editorMode }); // generate newest state object
+  // }
 
 
   /**
   /**
    * Update drawer related state by specified 'newState' object
    * Update drawer related state by specified 'newState' object
@@ -176,36 +111,19 @@ export default class NavigationContainer extends Container {
    *
    *
    * because updating state of unstated container will be delayed unless you use await
    * because updating state of unstated container will be delayed unless you use await
    */
    */
-  updateDrawerMode(newState) {
-    const {
-      editorMode, isDeviceSmallerThanMd, preferDrawerModeByUser, preferDrawerModeOnEditByUser,
-    } = newState;
-
-    // get preference on view or edit
-    const preferDrawerMode = editorMode !== 'view' ? preferDrawerModeOnEditByUser : preferDrawerModeByUser;
-
-    const isDrawerMode = isDeviceSmallerThanMd || preferDrawerMode;
-    const isDrawerOpened = false; // close Drawer anyway
-
-    this.setState({ isDrawerMode, isDrawerOpened });
-  }
+  // updateDrawerMode(newState) {
+  //   const {
+  //     editorMode, isDeviceSmallerThanMd, preferDrawerModeByUser, preferDrawerModeOnEditByUser,
+  //   } = newState;
 
 
-  selectSidebarContents(contentsId) {
-    window.localStorage.setItem('sidebarContentsId', contentsId);
-    this.setState({ sidebarContentsId: contentsId });
-  }
+  //   // get preference on view or edit
+  //   const preferDrawerMode = editorMode !== 'view' ? preferDrawerModeOnEditByUser : preferDrawerModeByUser;
 
 
-  openPageCreateModal() {
-    if (this.appContainer.currentUser == null) {
-      logger.warn('Please login or signup to create a new page.');
-      return;
-    }
-    this.setState({ isPageCreateModalShown: true });
-  }
+  //   const isDrawerMode = isDeviceSmallerThanMd || preferDrawerMode;
+  //   const isDrawerOpened = false; // close Drawer anyway
 
 
-  closePageCreateModal() {
-    this.setState({ isPageCreateModalShown: false });
-  }
+  //   this.setState({ isDrawerMode, isDrawerOpened });
+  // }
 
 
   /**
   /**
    * Function that implements the click event for realizing smooth scroll
    * Function that implements the click event for realizing smooth scroll

+ 5 - 5
packages/app/src/client/services/PageContainer.js

@@ -162,12 +162,12 @@ export default class PageContainer extends Container {
   }
   }
 
 
 
 
-  get isAbleToOpenPageEditor() {
-    const { isNotCreatable, isTrashPage } = this.state;
-    const { isGuestUser } = this.appContainer;
+  // get isAbleToOpenPageEditor() {
+  //   const { isNotCreatable, isTrashPage } = this.state;
+  //   const { isGuestUser } = this.appContainer;
 
 
-    return (!isNotCreatable && !isTrashPage && !isGuestUser);
-  }
+  //   return (!isNotCreatable && !isTrashPage && !isGuestUser);
+  // }
 
 
   /**
   /**
    * whether to display reaction buttons
    * whether to display reaction buttons

+ 0 - 0
packages/app/src/services/user-ui-settings.ts → packages/app/src/client/services/user-ui-settings.ts


+ 5 - 1
packages/app/src/components/Fab.jsx

@@ -6,6 +6,8 @@ import loggerFactory from '~/utils/logger';
 
 
 import AppContainer from '~/client/services/AppContainer';
 import AppContainer from '~/client/services/AppContainer';
 import NavigationContainer from '~/client/services/NavigationContainer';
 import NavigationContainer from '~/client/services/NavigationContainer';
+import { usePageCreateModalOpened } from '~/stores/ui';
+
 import { withUnstatedContainers } from './UnstatedUtils';
 import { withUnstatedContainers } from './UnstatedUtils';
 import CreatePageIcon from './Icons/CreatePageIcon';
 import CreatePageIcon from './Icons/CreatePageIcon';
 import ReturnTopIcon from './Icons/ReturnTopIcon';
 import ReturnTopIcon from './Icons/ReturnTopIcon';
@@ -16,6 +18,8 @@ const Fab = (props) => {
   const { navigationContainer, appContainer } = props;
   const { navigationContainer, appContainer } = props;
   const { currentUser } = appContainer;
   const { currentUser } = appContainer;
 
 
+  const { mutate: mutatePageCreateModalOpened } = usePageCreateModalOpened();
+
   const [animateClasses, setAnimateClasses] = useState('invisible');
   const [animateClasses, setAnimateClasses] = useState('invisible');
   const [buttonClasses, setButtonClasses] = useState('');
   const [buttonClasses, setButtonClasses] = useState('');
 
 
@@ -52,7 +56,7 @@ const Fab = (props) => {
           <button
           <button
             type="button"
             type="button"
             className={`btn btn-lg btn-create-page btn-primary rounded-circle p-0 waves-effect waves-light ${buttonClasses}`}
             className={`btn btn-lg btn-create-page btn-primary rounded-circle p-0 waves-effect waves-light ${buttonClasses}`}
-            onClick={navigationContainer.openPageCreateModal}
+            onClick={() => mutatePageCreateModalOpened(true)}
           >
           >
             <CreatePageIcon />
             <CreatePageIcon />
           </button>
           </button>

+ 9 - 11
packages/app/src/components/Hotkeys/Subscribers/CreatePage.jsx

@@ -1,31 +1,29 @@
 import React, { useEffect } from 'react';
 import React, { useEffect } from 'react';
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
 
 
-import NavigationContainer from '~/client/services/NavigationContainer';
-import { withUnstatedContainers } from '../../UnstatedUtils';
+import { usePageCreateModalOpened } from '~/stores/ui';
 
 
-const CreatePage = (props) => {
+const CreatePage = React.memo((props) => {
+
+  const { mutate } = usePageCreateModalOpened();
 
 
   // setup effect
   // setup effect
   useEffect(() => {
   useEffect(() => {
-    props.navigationContainer.openPageCreateModal();
+    mutate(true);
 
 
     // remove this
     // remove this
     props.onDeleteRender(this);
     props.onDeleteRender(this);
-  }, [props]);
+  }, [mutate, props]);
 
 
   return <></>;
   return <></>;
-};
+});
 
 
 CreatePage.propTypes = {
 CreatePage.propTypes = {
-  navigationContainer: PropTypes.instanceOf(NavigationContainer).isRequired,
   onDeleteRender: PropTypes.func.isRequired,
   onDeleteRender: PropTypes.func.isRequired,
 };
 };
 
 
-const CreatePageWrapper = withUnstatedContainers(CreatePage, [NavigationContainer]);
-
-CreatePageWrapper.getHotkeyStrokes = () => {
+CreatePage.getHotkeyStrokes = () => {
   return [['c']];
   return [['c']];
 };
 };
 
 
-export default CreatePageWrapper;
+export default CreatePage;

+ 4 - 2
packages/app/src/components/Hotkeys/Subscribers/EditPage.jsx

@@ -3,8 +3,10 @@ import PropTypes from 'prop-types';
 
 
 import NavigationContainer from '~/client/services/NavigationContainer';
 import NavigationContainer from '~/client/services/NavigationContainer';
 import { withUnstatedContainers } from '../../UnstatedUtils';
 import { withUnstatedContainers } from '../../UnstatedUtils';
+import { EditorMode, useEditorMode } from '~/stores/ui';
 
 
 const EditPage = (props) => {
 const EditPage = (props) => {
+  const { mutate: mutateEditorMode } = useEditorMode();
 
 
   // setup effect
   // setup effect
   useEffect(() => {
   useEffect(() => {
@@ -13,11 +15,11 @@ const EditPage = (props) => {
       return;
       return;
     }
     }
 
 
-    props.navigationContainer.setEditorMode('edit');
+    mutateEditorMode(EditorMode.Editor);
 
 
     // remove this
     // remove this
     props.onDeleteRender(this);
     props.onDeleteRender(this);
-  }, [props]);
+  }, [mutateEditorMode, props]);
 
 
   return <></>;
   return <></>;
 };
 };

+ 3 - 3
packages/app/src/components/Icons/GrowiLogo.jsx

@@ -1,6 +1,6 @@
-import React from 'react';
+import React, { memo } from 'react';
 
 
-const GrowiLogo = () => (
+const GrowiLogo = memo(() => (
   <svg
   <svg
     xmlns="http://www.w3.org/2000/svg"
     xmlns="http://www.w3.org/2000/svg"
     width="32"
     width="32"
@@ -29,6 +29,6 @@ const GrowiLogo = () => (
     >
     >
     </path>
     </path>
   </svg>
   </svg>
-);
+));
 
 
 export default GrowiLogo;
 export default GrowiLogo;

+ 0 - 115
packages/app/src/components/Navbar/GrowiNavbar.jsx

@@ -1,115 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-
-import { withTranslation } from 'react-i18next';
-
-import { UncontrolledTooltip } from 'reactstrap';
-import { withUnstatedContainers } from '../UnstatedUtils';
-import NavigationContainer from '~/client/services/NavigationContainer';
-import AppContainer from '~/client/services/AppContainer';
-
-
-import GrowiLogo from '../Icons/GrowiLogo';
-
-import PersonalDropdown from './PersonalDropdown';
-import GlobalSearch from './GlobalSearch';
-
-class GrowiNavbar extends React.Component {
-
-  renderNavbarRight() {
-    const { t, appContainer, navigationContainer } = this.props;
-    const { currentUser } = appContainer;
-
-    // render login button
-    if (currentUser == null) {
-      return <li id="login-user" className="nav-item"><a className="nav-link" href="/login">Login</a></li>;
-    }
-
-    return (
-      <>
-        <li className="nav-item d-none d-md-block">
-          <button className="px-md-2 nav-link btn-create-page border-0 bg-transparent" type="button" onClick={navigationContainer.openPageCreateModal}>
-            <i className="icon-pencil mr-2"></i>
-            <span className="d-none d-lg-block">{ t('New') }</span>
-          </button>
-        </li>
-
-        <li className="grw-personal-dropdown nav-item dropdown dropdown-toggle dropdown-toggle-no-caret">
-          <PersonalDropdown />
-        </li>
-      </>
-    );
-  }
-
-  renderConfidential() {
-    const { appContainer } = this.props;
-    const { crowi } = appContainer.config;
-
-    return (
-      <li className="nav-item confidential text-light">
-        <i id="confidentialTooltip" className="icon-info d-md-none" />
-        <span className="d-none d-md-inline">
-          {crowi.confidential}
-        </span>
-        <UncontrolledTooltip
-          placement="bottom"
-          target="confidentialTooltip"
-          className="d-md-none"
-        >
-          {crowi.confidential}
-        </UncontrolledTooltip>
-      </li>
-    );
-  }
-
-  render() {
-    const { appContainer, navigationContainer } = this.props;
-    const { crowi, isSearchServiceConfigured } = appContainer.config;
-    const { isDeviceSmallerThanMd } = navigationContainer.state;
-
-    return (
-      <>
-
-        {/* Brand Logo  */}
-        <div className="navbar-brand mr-0">
-          <a className="grw-logo d-block" href="/">
-            <GrowiLogo />
-          </a>
-        </div>
-
-        <div className="grw-app-title d-none d-md-block">
-          {crowi.title}
-        </div>
-
-
-        {/* Navbar Right  */}
-        <ul className="navbar-nav ml-auto">
-          {this.renderNavbarRight()}
-          {crowi.confidential != null && this.renderConfidential()}
-        </ul>
-
-        { isSearchServiceConfigured && !isDeviceSmallerThanMd && (
-          <div className="grw-global-search grw-global-search-top position-absolute">
-            <GlobalSearch />
-          </div>
-        ) }
-      </>
-    );
-  }
-
-}
-
-/**
- * Wrapper component for using unstated
- */
-const GrowiNavbarWrapper = withUnstatedContainers(GrowiNavbar, [AppContainer, NavigationContainer]);
-
-
-GrowiNavbar.propTypes = {
-  t: PropTypes.func.isRequired, //  i18next
-
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
-  navigationContainer: PropTypes.instanceOf(NavigationContainer).isRequired,
-};
-
-export default withTranslation()(GrowiNavbarWrapper);

+ 128 - 0
packages/app/src/components/Navbar/GrowiNavbar.tsx

@@ -0,0 +1,128 @@
+import React, { FC, memo } from 'react';
+import PropTypes from 'prop-types';
+
+import { useTranslation } from 'react-i18next';
+
+import { UncontrolledTooltip } from 'reactstrap';
+
+import AppContainer from '~/client/services/AppContainer';
+import { IUser } from '~/interfaces/user';
+import { useIsDeviceSmallerThanMd, usePageCreateModalOpened } from '~/stores/ui';
+
+import { withUnstatedContainers } from '../UnstatedUtils';
+import GrowiLogo from '../Icons/GrowiLogo';
+
+import PersonalDropdown from './PersonalDropdown';
+import GlobalSearch from './GlobalSearch';
+
+type NavbarRightProps = {
+  currentUser: IUser,
+}
+const NavbarRight: FC<NavbarRightProps> = memo((props: NavbarRightProps) => {
+  const { t } = useTranslation();
+  const { mutate: mutatePageCreateModalOpened } = usePageCreateModalOpened();
+
+  const { currentUser } = props;
+
+  // render login button
+  if (currentUser == null) {
+    return <li id="login-user" className="nav-item"><a className="nav-link" href="/login">Login</a></li>;
+  }
+
+  return (
+    <>
+      <li className="nav-item d-none d-md-block">
+        <button
+          className="px-md-2 nav-link btn-create-page border-0 bg-transparent"
+          type="button"
+          onClick={() => mutatePageCreateModalOpened(true)}
+        >
+          <i className="icon-pencil mr-2"></i>
+          <span className="d-none d-lg-block">{ t('New') }</span>
+        </button>
+      </li>
+
+      <li className="grw-personal-dropdown nav-item dropdown dropdown-toggle dropdown-toggle-no-caret">
+        <PersonalDropdown />
+      </li>
+    </>
+  );
+});
+
+type ConfidentialProps = {
+  confidential?: string,
+}
+const Confidential: FC<ConfidentialProps> = memo((props: ConfidentialProps) => {
+  const { confidential } = props;
+
+  if (confidential == null) {
+    return null;
+  }
+
+  return (
+    <li className="nav-item confidential text-light">
+      <i id="confidentialTooltip" className="icon-info d-md-none" />
+      <span className="d-none d-md-inline">
+        {confidential}
+      </span>
+      <UncontrolledTooltip
+        placement="bottom"
+        target="confidentialTooltip"
+        className="d-md-none"
+      >
+        {confidential}
+      </UncontrolledTooltip>
+    </li>
+  );
+});
+
+
+const GrowiNavbar = (props) => {
+
+  const { appContainer } = props;
+  const { currentUser } = appContainer;
+  const { crowi, isSearchServiceConfigured } = appContainer.config;
+
+  const { data: isDeviceSmallerThanMd } = useIsDeviceSmallerThanMd();
+
+  return (
+    <>
+      {/* Brand Logo  */}
+      <div className="navbar-brand mr-0">
+        <a className="grw-logo d-block" href="/">
+          <GrowiLogo />
+        </a>
+      </div>
+
+      <div className="grw-app-title d-none d-md-block">
+        {crowi.title}
+      </div>
+
+
+      {/* Navbar Right  */}
+      <ul className="navbar-nav ml-auto">
+        <NavbarRight currentUser={currentUser}></NavbarRight>
+        <Confidential confidential={crowi.confidential}></Confidential>
+      </ul>
+
+      { isSearchServiceConfigured && !isDeviceSmallerThanMd && (
+        <div className="grw-global-search grw-global-search-top position-absolute">
+          <GlobalSearch />
+        </div>
+      ) }
+    </>
+  );
+
+};
+
+/**
+ * Wrapper component for using unstated
+ */
+const GrowiNavbarWrapper = withUnstatedContainers(GrowiNavbar, [AppContainer]);
+
+
+GrowiNavbar.propTypes = {
+  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
+};
+
+export default GrowiNavbarWrapper;

+ 8 - 4
packages/app/src/components/Navbar/GrowiNavbarBottom.jsx

@@ -2,8 +2,9 @@ import React from 'react';
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
 
 
 import NavigationContainer from '~/client/services/NavigationContainer';
 import NavigationContainer from '~/client/services/NavigationContainer';
-import { withUnstatedContainers } from '../UnstatedUtils';
+import { usePageCreateModalOpened, useIsDeviceSmallerThanMd, useDrawerOpened } from '~/stores/ui';
 
 
+import { withUnstatedContainers } from '../UnstatedUtils';
 import GlobalSearch from './GlobalSearch';
 import GlobalSearch from './GlobalSearch';
 
 
 const GrowiNavbarBottom = (props) => {
 const GrowiNavbarBottom = (props) => {
@@ -11,7 +12,10 @@ const GrowiNavbarBottom = (props) => {
   const {
   const {
     navigationContainer,
     navigationContainer,
   } = props;
   } = props;
-  const { isDrawerOpened, isDeviceSmallerThanMd } = navigationContainer.state;
+
+  const { data: isDrawerOpened, mutate: mutateDrawerOpened } = useDrawerOpened();
+  const { data: isDeviceSmallerThanMd } = useIsDeviceSmallerThanMd();
+  const { mutate: mutatePageCreateModalOpened } = usePageCreateModalOpened();
 
 
   const additionalClasses = ['grw-navbar-bottom'];
   const additionalClasses = ['grw-navbar-bottom'];
   if (isDrawerOpened) {
   if (isDrawerOpened) {
@@ -36,7 +40,7 @@ const GrowiNavbarBottom = (props) => {
             <a
             <a
               role="button"
               role="button"
               className="nav-link btn-lg"
               className="nav-link btn-lg"
-              onClick={() => navigationContainer.toggleDrawer()}
+              onClick={() => mutateDrawerOpened(true)}
             >
             >
               <i className="icon-menu"></i>
               <i className="icon-menu"></i>
             </a>
             </a>
@@ -55,7 +59,7 @@ const GrowiNavbarBottom = (props) => {
             <a
             <a
               role="button"
               role="button"
               className="nav-link btn-lg"
               className="nav-link btn-lg"
-              onClick={() => navigationContainer.openPageCreateModal()}
+              onClick={() => mutatePageCreateModalOpened(true)}
             >
             >
               <i className="icon-pencil"></i>
               <i className="icon-pencil"></i>
             </a>
             </a>

+ 13 - 12
packages/app/src/components/Navbar/GrowiSubNavigation.jsx

@@ -1,16 +1,16 @@
 import React from 'react';
 import React from 'react';
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
 
 
-import { withTranslation } from 'react-i18next';
-
 import { DevidedPagePath } from '@growi/core';
 import { DevidedPagePath } from '@growi/core';
 import PagePathHierarchicalLink from '~/components/PagePathHierarchicalLink';
 import PagePathHierarchicalLink from '~/components/PagePathHierarchicalLink';
 import LinkedPagePath from '~/models/linked-page-path';
 import LinkedPagePath from '~/models/linked-page-path';
 
 
 import { withUnstatedContainers } from '../UnstatedUtils';
 import { withUnstatedContainers } from '../UnstatedUtils';
 import AppContainer from '~/client/services/AppContainer';
 import AppContainer from '~/client/services/AppContainer';
-import NavigationContainer from '~/client/services/NavigationContainer';
 import PageContainer from '~/client/services/PageContainer';
 import PageContainer from '~/client/services/PageContainer';
+import {
+  EditorMode, useDrawerMode, useEditorMode, useIsDeviceSmallerThanMd,
+} from '~/stores/ui';
 
 
 import CopyDropdown from '../Page/CopyDropdown';
 import CopyDropdown from '../Page/CopyDropdown';
 import TagLabels from '../Page/TagLabels';
 import TagLabels from '../Page/TagLabels';
@@ -67,21 +67,24 @@ const PagePathNav = ({
 };
 };
 
 
 const GrowiSubNavigation = (props) => {
 const GrowiSubNavigation = (props) => {
+  const { data: isDeviceSmallerThanMd } = useIsDeviceSmallerThanMd();
+  const { data: isDrawerMode } = useDrawerMode();
+  const { data: editorMode, mutate: mutateEditorMode } = useEditorMode();
+
   const {
   const {
-    appContainer, navigationContainer, pageContainer, isCompactMode,
+    appContainer, pageContainer, isCompactMode,
   } = props;
   } = props;
-  const { isDrawerMode, editorMode, isDeviceSmallerThanMd } = navigationContainer.state;
   const {
   const {
     pageId, path, createdAt, creator, updatedAt, revisionAuthor, isPageExist,
     pageId, path, createdAt, creator, updatedAt, revisionAuthor, isPageExist,
   } = pageContainer.state;
   } = pageContainer.state;
 
 
   const { isGuestUser } = appContainer;
   const { isGuestUser } = appContainer;
-  const isEditorMode = editorMode !== 'view';
+  const isEditorMode = editorMode !== EditorMode.View;
   // Tags cannot be edited while the new page and editorMode is view
   // Tags cannot be edited while the new page and editorMode is view
-  const isTagLabelHidden = (editorMode !== 'edit' && !isPageExist);
+  const isTagLabelHidden = (editorMode !== EditorMode.Editor && !isPageExist);
 
 
   function onPageEditorModeButtonClicked(viewType) {
   function onPageEditorModeButtonClicked(viewType) {
-    navigationContainer.setEditorMode(viewType);
+    mutateEditorMode(viewType);
   }
   }
 
 
   return (
   return (
@@ -145,16 +148,14 @@ const GrowiSubNavigation = (props) => {
 /**
 /**
  * Wrapper component for using unstated
  * Wrapper component for using unstated
  */
  */
-const GrowiSubNavigationWrapper = withUnstatedContainers(GrowiSubNavigation, [AppContainer, NavigationContainer, PageContainer]);
+const GrowiSubNavigationWrapper = withUnstatedContainers(GrowiSubNavigation, [AppContainer, PageContainer]);
 
 
 
 
 GrowiSubNavigation.propTypes = {
 GrowiSubNavigation.propTypes = {
-  t: PropTypes.func.isRequired, //  i18next
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
-  navigationContainer: PropTypes.instanceOf(NavigationContainer).isRequired,
   pageContainer: PropTypes.instanceOf(PageContainer).isRequired,
   pageContainer: PropTypes.instanceOf(PageContainer).isRequired,
 
 
   isCompactMode: PropTypes.bool,
   isCompactMode: PropTypes.bool,
 };
 };
 
 
-export default withTranslation()(GrowiSubNavigationWrapper);
+export default GrowiSubNavigationWrapper;

+ 17 - 15
packages/app/src/components/Navbar/PageEditorModeManager.jsx

@@ -1,10 +1,12 @@
 import React, { useCallback } from 'react';
 import React, { useCallback } from 'react';
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
-import { withTranslation } from 'react-i18next';
+import { useTranslation } from 'react-i18next';
 import { UncontrolledTooltip } from 'reactstrap';
 import { UncontrolledTooltip } from 'reactstrap';
 
 
-import { withUnstatedContainers } from '../UnstatedUtils';
 import AppContainer from '~/client/services/AppContainer';
 import AppContainer from '~/client/services/AppContainer';
+import { EditorMode, useIsDeviceSmallerThanMd } from '~/stores/ui';
+
+import { withUnstatedContainers } from '../UnstatedUtils';
 
 
 /* eslint-disable react/prop-types */
 /* eslint-disable react/prop-types */
 const PageEditorModeButtonWrapper = React.memo(({
 const PageEditorModeButtonWrapper = React.memo(({
@@ -36,14 +38,17 @@ const PageEditorModeButtonWrapper = React.memo(({
 
 
 function PageEditorModeManager(props) {
 function PageEditorModeManager(props) {
   const {
   const {
-    t, appContainer,
-    editorMode, onPageEditorModeButtonClicked, isBtnDisabled, isDeviceSmallerThanMd,
+    appContainer,
+    editorMode, onPageEditorModeButtonClicked, isBtnDisabled,
   } = props;
   } = props;
 
 
+  const { t } = useTranslation();
+  const { data: isDeviceSmallerThanMd } = useIsDeviceSmallerThanMd();
+
   const isAdmin = appContainer.isAdmin;
   const isAdmin = appContainer.isAdmin;
   const isHackmdEnabled = appContainer.config.env.HACKMD_URI != null;
   const isHackmdEnabled = appContainer.config.env.HACKMD_URI != null;
   const showHackmdBtn = isHackmdEnabled || isAdmin;
   const showHackmdBtn = isHackmdEnabled || isAdmin;
-  const showHackmdDisabledTooltip = isAdmin && !isHackmdEnabled && editorMode !== 'hackmd';
+  const showHackmdDisabledTooltip = isAdmin && !isHackmdEnabled && editorMode !== EditorMode.HackMD;
 
 
   const pageEditorModeButtonClickedHandler = useCallback((viewType) => {
   const pageEditorModeButtonClickedHandler = useCallback((viewType) => {
     if (isBtnDisabled) {
     if (isBtnDisabled) {
@@ -62,32 +67,32 @@ function PageEditorModeManager(props) {
         aria-label="page-editor-mode-manager"
         aria-label="page-editor-mode-manager"
         id="grw-page-editor-mode-manager"
         id="grw-page-editor-mode-manager"
       >
       >
-        {(!isDeviceSmallerThanMd || editorMode !== 'view') && (
+        {(!isDeviceSmallerThanMd || editorMode !== EditorMode.View) && (
           <PageEditorModeButtonWrapper
           <PageEditorModeButtonWrapper
             editorMode={editorMode}
             editorMode={editorMode}
             isBtnDisabled={isBtnDisabled}
             isBtnDisabled={isBtnDisabled}
             onClick={pageEditorModeButtonClickedHandler}
             onClick={pageEditorModeButtonClickedHandler}
-            targetMode="view"
+            targetMode={EditorMode.View}
             icon={<i className="icon-control-play" />}
             icon={<i className="icon-control-play" />}
             label={t('view')}
             label={t('view')}
           />
           />
         )}
         )}
-        {(!isDeviceSmallerThanMd || editorMode === 'view') && (
+        {(!isDeviceSmallerThanMd || editorMode === EditorMode.View) && (
           <PageEditorModeButtonWrapper
           <PageEditorModeButtonWrapper
             editorMode={editorMode}
             editorMode={editorMode}
             isBtnDisabled={isBtnDisabled}
             isBtnDisabled={isBtnDisabled}
             onClick={pageEditorModeButtonClickedHandler}
             onClick={pageEditorModeButtonClickedHandler}
-            targetMode="edit"
+            targetMode={EditorMode.Editor}
             icon={<i className="icon-note" />}
             icon={<i className="icon-note" />}
             label={t('Edit')}
             label={t('Edit')}
           />
           />
         )}
         )}
-        {(!isDeviceSmallerThanMd || editorMode === 'view') && showHackmdBtn && (
+        {(!isDeviceSmallerThanMd || editorMode === EditorMode.View) && showHackmdBtn && (
           <PageEditorModeButtonWrapper
           <PageEditorModeButtonWrapper
             editorMode={editorMode}
             editorMode={editorMode}
             isBtnDisabled={isBtnDisabled}
             isBtnDisabled={isBtnDisabled}
             onClick={pageEditorModeButtonClickedHandler}
             onClick={pageEditorModeButtonClickedHandler}
-            targetMode="hackmd"
+            targetMode={EditorMode.HackMD}
             icon={<i className="fa fa-file-text-o" />}
             icon={<i className="fa fa-file-text-o" />}
             label={t('hackmd.hack_md')}
             label={t('hackmd.hack_md')}
             id="grw-page-editor-mode-manager-hackmd-button"
             id="grw-page-editor-mode-manager-hackmd-button"
@@ -110,18 +115,15 @@ function PageEditorModeManager(props) {
 }
 }
 
 
 PageEditorModeManager.propTypes = {
 PageEditorModeManager.propTypes = {
-  t: PropTypes.func.isRequired, //  i18next
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
 
 
   onPageEditorModeButtonClicked: PropTypes.func,
   onPageEditorModeButtonClicked: PropTypes.func,
   isBtnDisabled: PropTypes.bool,
   isBtnDisabled: PropTypes.bool,
   editorMode: PropTypes.string,
   editorMode: PropTypes.string,
-  isDeviceSmallerThanMd: PropTypes.bool,
 };
 };
 
 
 PageEditorModeManager.defaultProps = {
 PageEditorModeManager.defaultProps = {
   isBtnDisabled: false,
   isBtnDisabled: false,
-  isDeviceSmallerThanMd: false,
 };
 };
 
 
 /**
 /**
@@ -129,4 +131,4 @@ PageEditorModeManager.defaultProps = {
  */
  */
 const PageEditorModeManagerWrapper = withUnstatedContainers(PageEditorModeManager, [AppContainer]);
 const PageEditorModeManagerWrapper = withUnstatedContainers(PageEditorModeManager, [AppContainer]);
 
 
-export default withTranslation()(PageEditorModeManagerWrapper);
+export default PageEditorModeManagerWrapper;

+ 4 - 2
packages/app/src/components/Navbar/PersonalDropdown.jsx

@@ -6,11 +6,13 @@ import { withTranslation } from 'react-i18next';
 import { UncontrolledTooltip } from 'reactstrap';
 import { UncontrolledTooltip } from 'reactstrap';
 
 
 import { UserPicture } from '@growi/ui';
 import { UserPicture } from '@growi/ui';
-import { withUnstatedContainers } from '../UnstatedUtils';
+
+import { scheduleToPutUserUISettings } from '~/client/services/user-ui-settings';
 import AppContainer from '~/client/services/AppContainer';
 import AppContainer from '~/client/services/AppContainer';
+
+import { withUnstatedContainers } from '../UnstatedUtils';
 import NavigationContainer from '~/client/services/NavigationContainer';
 import NavigationContainer from '~/client/services/NavigationContainer';
 import { usePreferDrawerModeByUser, usePreferDrawerModeOnEditByUser } from '~/stores/ui';
 import { usePreferDrawerModeByUser, usePreferDrawerModeOnEditByUser } from '~/stores/ui';
-import { scheduleToPutUserUISettings } from '~/services/user-ui-settings';
 
 
 import {
 import {
   isUserPreferenceExists,
   isUserPreferenceExists,

+ 3 - 3
packages/app/src/components/Navbar/SubNavButtons.jsx

@@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
 import AppContainer from '~/client/services/AppContainer';
 import AppContainer from '~/client/services/AppContainer';
 import NavigationContainer from '~/client/services/NavigationContainer';
 import NavigationContainer from '~/client/services/NavigationContainer';
 import PageContainer from '~/client/services/PageContainer';
 import PageContainer from '~/client/services/PageContainer';
+import { EditorMode, useEditorMode } from '~/stores/ui';
 import { withUnstatedContainers } from '../UnstatedUtils';
 import { withUnstatedContainers } from '../UnstatedUtils';
 
 
 import BookmarkButton from '../BookmarkButton';
 import BookmarkButton from '../BookmarkButton';
@@ -14,7 +15,7 @@ const SubnavButtons = (props) => {
     appContainer, navigationContainer, pageContainer, isCompactMode,
     appContainer, navigationContainer, pageContainer, isCompactMode,
   } = props;
   } = props;
 
 
-  /* eslint-enable react/prop-types */
+  const { data: editorMode } = useEditorMode();
 
 
   /* eslint-disable react/prop-types */
   /* eslint-disable react/prop-types */
   const PageReactionButtons = ({ pageContainer }) => {
   const PageReactionButtons = ({ pageContainer }) => {
@@ -34,8 +35,7 @@ const SubnavButtons = (props) => {
   };
   };
   /* eslint-enable react/prop-types */
   /* eslint-enable react/prop-types */
 
 
-  const { editorMode } = navigationContainer.state;
-  const isViewMode = editorMode === 'view';
+  const isViewMode = editorMode === EditorMode.View;
 
 
   return (
   return (
     <>
     <>

+ 13 - 9
packages/app/src/components/Page/DisplaySwitcher.jsx

@@ -1,9 +1,11 @@
 import React from 'react';
 import React from 'react';
 import { TabContent, TabPane } from 'reactstrap';
 import { TabContent, TabPane } from 'reactstrap';
 import propTypes from 'prop-types';
 import propTypes from 'prop-types';
+
 import { withUnstatedContainers } from '../UnstatedUtils';
 import { withUnstatedContainers } from '../UnstatedUtils';
-import NavigationContainer from '~/client/services/NavigationContainer';
 import PageContainer from '~/client/services/PageContainer';
 import PageContainer from '~/client/services/PageContainer';
+import { EditorMode, useEditorMode } from '~/stores/ui';
+
 import Editor from '../PageEditor';
 import Editor from '../PageEditor';
 import Page from '../Page';
 import Page from '../Page';
 import UserInfo from '../User/UserInfo';
 import UserInfo from '../User/UserInfo';
@@ -16,15 +18,18 @@ import EditorNavbarBottom from '../PageEditor/EditorNavbarBottom';
 
 
 const DisplaySwitcher = (props) => {
 const DisplaySwitcher = (props) => {
   const {
   const {
-    navigationContainer, pageContainer,
+    pageContainer,
   } = props;
   } = props;
-  const { editorMode } = navigationContainer.state;
   const { isPageExist, pageUser } = pageContainer.state;
   const { isPageExist, pageUser } = pageContainer.state;
 
 
+  const { data: editorMode } = useEditorMode();
+
+  const isViewMode = editorMode === EditorMode.View;
+
   return (
   return (
     <>
     <>
       <TabContent activeTab={editorMode}>
       <TabContent activeTab={editorMode}>
-        <TabPane tabId="view">
+        <TabPane tabId={EditorMode.View}>
           <div className="d-flex flex-column flex-lg-row-reverse">
           <div className="d-flex flex-column flex-lg-row-reverse">
 
 
             <div className="grw-side-contents-container">
             <div className="grw-side-contents-container">
@@ -49,26 +54,25 @@ const DisplaySwitcher = (props) => {
 
 
           </div>
           </div>
         </TabPane>
         </TabPane>
-        <TabPane tabId="edit">
+        <TabPane tabId={EditorMode.Editor}>
           <div id="page-editor">
           <div id="page-editor">
             <Editor />
             <Editor />
           </div>
           </div>
         </TabPane>
         </TabPane>
-        <TabPane tabId="hackmd">
+        <TabPane tabId={EditorMode.HackMD}>
           <div id="page-editor-with-hackmd">
           <div id="page-editor-with-hackmd">
             <PageEditorByHackmd />
             <PageEditorByHackmd />
           </div>
           </div>
         </TabPane>
         </TabPane>
       </TabContent>
       </TabContent>
-      {editorMode !== 'view' && <EditorNavbarBottom /> }
+      {!isViewMode && <EditorNavbarBottom /> }
     </>
     </>
   );
   );
 };
 };
 
 
 DisplaySwitcher.propTypes = {
 DisplaySwitcher.propTypes = {
-  navigationContainer: propTypes.instanceOf(NavigationContainer).isRequired,
   pageContainer: propTypes.instanceOf(PageContainer).isRequired,
   pageContainer: propTypes.instanceOf(PageContainer).isRequired,
 };
 };
 
 
 
 
-export default withUnstatedContainers(DisplaySwitcher, [NavigationContainer, PageContainer]);
+export default withUnstatedContainers(DisplaySwitcher, [PageContainer]);

+ 13 - 13
packages/app/src/components/Page/NotFoundAlert.jsx

@@ -1,24 +1,26 @@
-import React from 'react';
+import React, { useCallback } from 'react';
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
-import { withTranslation } from 'react-i18next';
+import { useTranslation } from 'react-i18next';
 import { UncontrolledTooltip } from 'reactstrap';
 import { UncontrolledTooltip } from 'reactstrap';
+import { EditorMode, useEditorMode } from '~/stores/ui';
 
 
 
 
 const NotFoundAlert = (props) => {
 const NotFoundAlert = (props) => {
-  const { t, isHidden, isGuestUserMode } = props;
-  function clickHandler(viewType) {
+  const { t } = useTranslation();
+  const { isHidden, isGuestUserMode } = props;
 
 
+  const { mutate: mutateEditorMode } = useEditorMode();
+
+  const clickHandler = useCallback(() => {
     // check guest user,
     // check guest user,
     // disabled of button cannot be used for using tooltip.
     // disabled of button cannot be used for using tooltip.
     if (isGuestUserMode) {
     if (isGuestUserMode) {
       return;
       return;
     }
     }
 
 
-    if (props.onPageCreateClicked === null) {
-      return;
-    }
-    props.onPageCreateClicked(viewType);
-  }
+    mutateEditorMode(EditorMode.Editor);
+
+  }, [isGuestUserMode, mutateEditorMode]);
 
 
   if (isHidden) {
   if (isHidden) {
     return null;
     return null;
@@ -38,7 +40,7 @@ const NotFoundAlert = (props) => {
           <button
           <button
             type="button"
             type="button"
             className={`pl-3 pr-3 btn bg-info text-white ${isGuestUserMode ? 'disabled' : ''}`}
             className={`pl-3 pr-3 btn bg-info text-white ${isGuestUserMode ? 'disabled' : ''}`}
-            onClick={() => { clickHandler('edit') }}
+            onClick={clickHandler}
           >
           >
             <i className="icon-note icon-fw" />
             <i className="icon-note icon-fw" />
             {t('not_found_page.Create Page')}
             {t('not_found_page.Create Page')}
@@ -58,10 +60,8 @@ const NotFoundAlert = (props) => {
 
 
 
 
 NotFoundAlert.propTypes = {
 NotFoundAlert.propTypes = {
-  t: PropTypes.func.isRequired, // i18next
-  onPageCreateClicked: PropTypes.func,
   isHidden: PropTypes.bool.isRequired,
   isHidden: PropTypes.bool.isRequired,
   isGuestUserMode: PropTypes.bool.isRequired,
   isGuestUserMode: PropTypes.bool.isRequired,
 };
 };
 
 
-export default withTranslation()(NotFoundAlert);
+export default NotFoundAlert;

+ 8 - 7
packages/app/src/components/PageCreateModal.jsx

@@ -11,9 +11,9 @@ import { pagePathUtils, pathUtils } from '@growi/core';
 
 
 
 
 import AppContainer from '~/client/services/AppContainer';
 import AppContainer from '~/client/services/AppContainer';
-import NavigationContainer from '~/client/services/NavigationContainer';
 import { withUnstatedContainers } from './UnstatedUtils';
 import { withUnstatedContainers } from './UnstatedUtils';
 import { toastError } from '~/client/util/apiNotification';
 import { toastError } from '~/client/util/apiNotification';
+import { usePageCreateModalOpened } from '~/stores/ui';
 
 
 import PagePathAutoComplete from './PagePathAutoComplete';
 import PagePathAutoComplete from './PagePathAutoComplete';
 
 
@@ -22,7 +22,9 @@ const {
 } = pagePathUtils;
 } = pagePathUtils;
 
 
 const PageCreateModal = (props) => {
 const PageCreateModal = (props) => {
-  const { t, appContainer, navigationContainer } = props;
+  const { t, appContainer } = props;
+
+  const { data: isPageCreateModalOpened, mutate: mutatePageCreateModalOpened } = usePageCreateModalOpened();
 
 
   const config = appContainer.getConfig();
   const config = appContainer.getConfig();
   const isReachable = config.isSearchServiceReachable;
   const isReachable = config.isSearchServiceReachable;
@@ -264,12 +266,12 @@ const PageCreateModal = (props) => {
   return (
   return (
     <Modal
     <Modal
       size="lg"
       size="lg"
-      isOpen={navigationContainer.state.isPageCreateModalShown}
-      toggle={navigationContainer.closePageCreateModal}
+      isOpen={isPageCreateModalOpened}
+      toggle={() => mutatePageCreateModalOpened(false)}
       className="grw-create-page"
       className="grw-create-page"
       autoFocus={false}
       autoFocus={false}
     >
     >
-      <ModalHeader tag="h4" toggle={navigationContainer.closePageCreateModal} className="bg-primary text-light">
+      <ModalHeader tag="h4" toggle={() => mutatePageCreateModalOpened(false)} className="bg-primary text-light">
         {t('New Page')}
         {t('New Page')}
       </ModalHeader>
       </ModalHeader>
       <ModalBody>
       <ModalBody>
@@ -286,13 +288,12 @@ const PageCreateModal = (props) => {
 /**
 /**
  * Wrapper component for using unstated
  * Wrapper component for using unstated
  */
  */
-const ModalControlWrapper = withUnstatedContainers(PageCreateModal, [AppContainer, NavigationContainer]);
+const ModalControlWrapper = withUnstatedContainers(PageCreateModal, [AppContainer]);
 
 
 
 
 PageCreateModal.propTypes = {
 PageCreateModal.propTypes = {
   t: PropTypes.func.isRequired, //  i18next
   t: PropTypes.func.isRequired, //  i18next
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
-  navigationContainer: PropTypes.instanceOf(NavigationContainer).isRequired,
 };
 };
 
 
 export default withTranslation()(ModalControlWrapper);
 export default withTranslation()(ModalControlWrapper);

+ 15 - 9
packages/app/src/components/PageEditor/EditorNavbarBottom.jsx

@@ -3,9 +3,12 @@ import PropTypes from 'prop-types';
 
 
 import { Collapse, Button } from 'reactstrap';
 import { Collapse, Button } from 'reactstrap';
 
 
-import NavigationContainer from '~/client/services/NavigationContainer';
 import EditorContainer from '~/client/services/EditorContainer';
 import EditorContainer from '~/client/services/EditorContainer';
 import AppContainer from '~/client/services/AppContainer';
 import AppContainer from '~/client/services/AppContainer';
+import {
+  EditorMode, useDrawerOpened, useEditorMode, useIsDeviceSmallerThanMd,
+} from '~/stores/ui';
+
 import SlackNotification from '../SlackNotification';
 import SlackNotification from '../SlackNotification';
 import SlackLogo from '../SlackLogo';
 import SlackLogo from '../SlackLogo';
 import { withUnstatedContainers } from '../UnstatedUtils';
 import { withUnstatedContainers } from '../UnstatedUtils';
@@ -16,20 +19,24 @@ import OptionsSelector from './OptionsSelector';
 
 
 const EditorNavbarBottom = (props) => {
 const EditorNavbarBottom = (props) => {
 
 
+  const { data: editorMode } = useEditorMode();
+
   const [isExpanded, setExpanded] = useState(false);
   const [isExpanded, setExpanded] = useState(false);
 
 
   const [isSlackExpanded, setSlackExpanded] = useState(false);
   const [isSlackExpanded, setSlackExpanded] = useState(false);
   const isSlackConfigured = props.appContainer.getConfig().isSlackConfigured;
   const isSlackConfigured = props.appContainer.getConfig().isSlackConfigured;
 
 
-  const {
-    navigationContainer,
-  } = props;
-  const { editorMode, isDeviceSmallerThanMd } = navigationContainer.state;
+  const { mutate: mutateDrawerOpened } = useDrawerOpened();
+  const { data: isDeviceSmallerThanMd } = useIsDeviceSmallerThanMd();
 
 
   const additionalClasses = ['grw-editor-navbar-bottom'];
   const additionalClasses = ['grw-editor-navbar-bottom'];
 
 
   const renderDrawerButton = () => (
   const renderDrawerButton = () => (
-    <button type="button" className="btn btn-outline-secondary border-0" onClick={() => navigationContainer.toggleDrawer()}>
+    <button
+      type="button"
+      className="btn btn-outline-secondary border-0"
+      onClick={() => mutateDrawerOpened(true)}
+    >
       <i className="icon-menu"></i>
       <i className="icon-menu"></i>
     </button>
     </button>
   );
   );
@@ -55,7 +62,7 @@ const EditorNavbarBottom = (props) => {
     </div>
     </div>
   );
   );
 
 
-  const isOptionsSelectorEnabled = editorMode !== 'hackmd';
+  const isOptionsSelectorEnabled = editorMode !== EditorMode.HackMD;
   const isCollapsedOptionsSelectorEnabled = isOptionsSelectorEnabled && isDeviceSmallerThanMd;
   const isCollapsedOptionsSelectorEnabled = isOptionsSelectorEnabled && isDeviceSmallerThanMd;
 
 
   return (
   return (
@@ -127,9 +134,8 @@ const EditorNavbarBottom = (props) => {
 };
 };
 
 
 EditorNavbarBottom.propTypes = {
 EditorNavbarBottom.propTypes = {
-  navigationContainer: PropTypes.instanceOf(NavigationContainer).isRequired,
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
   editorContainer: PropTypes.instanceOf(EditorContainer).isRequired,
   editorContainer: PropTypes.instanceOf(EditorContainer).isRequired,
 };
 };
 
 
-export default withUnstatedContainers(EditorNavbarBottom, [NavigationContainer, EditorContainer, AppContainer]);
+export default withUnstatedContainers(EditorNavbarBottom, [EditorContainer, AppContainer]);

+ 4 - 3
packages/app/src/components/Sidebar.tsx

@@ -2,6 +2,7 @@ import React, {
   FC, useCallback, useEffect, useRef, useState,
   FC, useCallback, useEffect, useRef, useState,
 } from 'react';
 } from 'react';
 
 
+import { scheduleToPutUserUISettings } from '~/client/services/user-ui-settings';
 import {
 import {
   useDrawerMode, useDrawerOpened,
   useDrawerMode, useDrawerOpened,
   useSidebarCollapsed,
   useSidebarCollapsed,
@@ -14,8 +15,8 @@ import DrawerToggler from './Navbar/DrawerToggler';
 
 
 import SidebarNav from './Sidebar/SidebarNav';
 import SidebarNav from './Sidebar/SidebarNav';
 import SidebarContents from './Sidebar/SidebarContents';
 import SidebarContents from './Sidebar/SidebarContents';
-import { scheduleToPutUserUISettings } from '~/services/user-ui-settings';
 import { NavigationResizeHexagon } from './Sidebar/NavigationResizeHexagon';
 import { NavigationResizeHexagon } from './Sidebar/NavigationResizeHexagon';
+import StickyStretchableScroller from './StickyStretchableScroller';
 
 
 const sidebarMinWidth = 240;
 const sidebarMinWidth = 240;
 const sidebarMinimizeWidth = 20;
 const sidebarMinimizeWidth = 20;
@@ -67,12 +68,12 @@ const SidebarContentsWrapper = () => {
 
 
   return (
   return (
     <>
     <>
-      {/* <StickyStretchableScroller
+      <StickyStretchableScroller
         scrollTargetSelector={scrollTargetSelector}
         scrollTargetSelector={scrollTargetSelector}
         contentsElemSelector="#grw-sidebar-content-container"
         contentsElemSelector="#grw-sidebar-content-container"
         stickyElemSelector=".grw-sidebar"
         stickyElemSelector=".grw-sidebar"
         calcViewHeightFunc={calcViewHeight}
         calcViewHeightFunc={calcViewHeight}
-      /> */}
+      />
 
 
       <div id="grw-sidebar-contents-scroll-target">
       <div id="grw-sidebar-contents-scroll-target">
         <div id="grw-sidebar-content-container">
         <div id="grw-sidebar-content-container">

+ 1 - 1
packages/app/src/components/Sidebar/PageTree/PrivateLegacyPages.tsx

@@ -6,7 +6,7 @@ const PrivateLegacyPages: FC = memo(() => {
 
 
   return (
   return (
     <div className="grw-prvt-legacy-pages p-3">
     <div className="grw-prvt-legacy-pages p-3">
-      <a href="#" className="h5">
+      <a href="/private-legacy-pages?q=[nq:PrivateLegacyPages]" className="h5">
         <i className="icon-drawer mr-2"></i> PrivateLegacyPages
         <i className="icon-drawer mr-2"></i> PrivateLegacyPages
       </a>
       </a>
     </div>
     </div>

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

@@ -1,7 +1,7 @@
 import React, { FC, memo, useCallback } from 'react';
 import React, { FC, memo, useCallback } from 'react';
 
 
+import { scheduleToPutUserUISettings } from '~/client/services/user-ui-settings';
 import { SidebarContentsType } from '~/interfaces/ui';
 import { SidebarContentsType } from '~/interfaces/ui';
-import { scheduleToPutUserUISettings } from '~/services/user-ui-settings';
 import { useCurrentUser, useIsSharedUser } from '~/stores/context';
 import { useCurrentUser, useIsSharedUser } from '~/stores/context';
 import { useCurrentSidebarContents } from '~/stores/ui';
 import { useCurrentSidebarContents } from '~/stores/ui';
 
 

+ 6 - 9
packages/app/src/components/StickyStretchableScroller.jsx

@@ -5,7 +5,6 @@ import { debounce } from 'throttle-debounce';
 import StickyEvents from 'sticky-events';
 import StickyEvents from 'sticky-events';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
-import NavigationContainer from '~/client/services/NavigationContainer';
 import { withUnstatedContainers } from './UnstatedUtils';
 import { withUnstatedContainers } from './UnstatedUtils';
 
 
 const logger = loggerFactory('growi:cli:StickyStretchableScroller');
 const logger = loggerFactory('growi:cli:StickyStretchableScroller');
@@ -49,7 +48,6 @@ const StickyStretchableScroller = (props) => {
 
 
   let { scrollTargetSelector } = props;
   let { scrollTargetSelector } = props;
   const {
   const {
-    navigationContainer,
     children, contentsElemSelector, stickyElemSelector,
     children, contentsElemSelector, stickyElemSelector,
     calcViewHeightFunc, calcContentsHeightFunc,
     calcViewHeightFunc, calcContentsHeightFunc,
   } = props;
   } = props;
@@ -142,11 +140,11 @@ const StickyStretchableScroller = (props) => {
   }, [resetScrollbarDebounced]);
   }, [resetScrollbarDebounced]);
 
 
   // setup effect by isScrollTop
   // setup effect by isScrollTop
-  useEffect(() => {
-    if (navigationContainer.state.isScrollTop) {
-      resetScrollbar();
-    }
-  }, [navigationContainer.state.isScrollTop, resetScrollbar]);
+  // useEffect(() => {
+  //   if (navigationContainer.state.isScrollTop) {
+  //     resetScrollbar();
+  //   }
+  // }, [navigationContainer.state.isScrollTop, resetScrollbar]);
 
 
   // setup effect by update props
   // setup effect by update props
   useEffect(() => {
   useEffect(() => {
@@ -161,7 +159,6 @@ const StickyStretchableScroller = (props) => {
 };
 };
 
 
 StickyStretchableScroller.propTypes = {
 StickyStretchableScroller.propTypes = {
-  navigationContainer: PropTypes.instanceOf(NavigationContainer).isRequired,
   contentsElemSelector: PropTypes.string.isRequired,
   contentsElemSelector: PropTypes.string.isRequired,
 
 
   children: PropTypes.node,
   children: PropTypes.node,
@@ -172,4 +169,4 @@ StickyStretchableScroller.propTypes = {
   calcContentsHeightFunc: PropTypes.func,
   calcContentsHeightFunc: PropTypes.func,
 };
 };
 
 
-export default withUnstatedContainers(StickyStretchableScroller, [NavigationContainer]);
+export default StickyStretchableScroller;

+ 2 - 1
packages/app/src/interfaces/user-ui-settings.ts

@@ -1,9 +1,10 @@
 import { IUser } from './user';
 import { IUser } from './user';
 
 
 import { SidebarContentsType } from './ui';
 import { SidebarContentsType } from './ui';
+import { Ref } from './common';
 
 
 export interface IUserUISettings {
 export interface IUserUISettings {
-  userId: IUser | string;
+  user: Ref<IUser> | null;
   isSidebarCollapsed: boolean,
   isSidebarCollapsed: boolean,
   currentSidebarContents: SidebarContentsType,
   currentSidebarContents: SidebarContentsType,
   currentProductNavWidth: number,
   currentProductNavWidth: number,

+ 2 - 2
packages/app/src/server/models/page.ts

@@ -82,8 +82,8 @@ const schema = new Schema<PageDocument, PageModel>({
   pageIdOnHackmd: { type: String },
   pageIdOnHackmd: { type: String },
   revisionHackmdSynced: { type: ObjectId, ref: 'Revision' }, // the revision that is synced to HackMD
   revisionHackmdSynced: { type: ObjectId, ref: 'Revision' }, // the revision that is synced to HackMD
   hasDraftOnHackmd: { type: Boolean }, // set true if revision and revisionHackmdSynced are same but HackMD document has modified
   hasDraftOnHackmd: { type: Boolean }, // set true if revision and revisionHackmdSynced are same but HackMD document has modified
-  createdAt: { type: Date, default: Date.now },
-  updatedAt: { type: Date, default: Date.now },
+  createdAt: { type: Date, default: new Date() },
+  updatedAt: { type: Date, default: new Date() },
   deleteUser: { type: ObjectId, ref: 'User' },
   deleteUser: { type: ObjectId, ref: 'User' },
   deletedAt: { type: Date },
   deletedAt: { type: Date },
 }, {
 }, {

+ 4 - 0
packages/app/src/server/routes/index.js

@@ -3,6 +3,7 @@ import express from 'express';
 import injectResetOrderByTokenMiddleware from '../middlewares/inject-reset-order-by-token-middleware';
 import injectResetOrderByTokenMiddleware from '../middlewares/inject-reset-order-by-token-middleware';
 
 
 import * as forgotPassword from './forgot-password';
 import * as forgotPassword from './forgot-password';
+import * as privateLegacyPages from './private-legacy-pages';
 
 
 const multer = require('multer');
 const multer = require('multer');
 const autoReap = require('multer-autoreap');
 const autoReap = require('multer-autoreap');
@@ -191,6 +192,9 @@ module.exports = function(crowi, app) {
     .get('/:token', apiLimiter, injectResetOrderByTokenMiddleware, forgotPassword.resetPassword)
     .get('/:token', apiLimiter, injectResetOrderByTokenMiddleware, forgotPassword.resetPassword)
     .use(forgotPassword.handleHttpErrosMiddleware));
     .use(forgotPassword.handleHttpErrosMiddleware));
 
 
+  app.use('/private-legacy-pages', express.Router()
+    .get('/', privateLegacyPages.renderPrivateLegacyPages));
+
   app.get('/share/:linkId', page.showSharedPage);
   app.get('/share/:linkId', page.showSharedPage);
 
 
   app.get('/:id([0-9a-z]{24})'       , loginRequired , page.showPage);
   app.get('/:id([0-9a-z]{24})'       , loginRequired , page.showPage);

+ 1 - 1
packages/app/src/server/routes/page.js

@@ -318,7 +318,7 @@ module.exports = function(crowi, app) {
       // add scope variables by ancestor page
       // add scope variables by ancestor page
       const ancestor = await Page.findAncestorByPathAndViewer(path, req.user);
       const ancestor = await Page.findAncestorByPathAndViewer(path, req.user);
       if (ancestor != null) {
       if (ancestor != null) {
-        await ancestor.populate('grantedGroup').execPopulate();
+        await ancestor.populate('grantedGroup');
         addRenderVarsForScope(renderVars, ancestor);
         addRenderVarsForScope(renderVars, ancestor);
       }
       }
     }
     }

+ 7 - 0
packages/app/src/server/routes/private-legacy-pages.ts

@@ -0,0 +1,7 @@
+import {
+  Request, Response,
+} from 'express';
+
+export const renderPrivateLegacyPages = (req: Request, res: Response): void => {
+  return res.render('private-legacy-pages');
+};

+ 1 - 1
packages/app/src/server/routes/search.js

@@ -139,7 +139,7 @@ module.exports = function(crowi, app) {
 
 
     const result = {};
     const result = {};
     try {
     try {
-      const esResult = await searchService.searchKeyword(keyword, user, userGroups, searchOpts);
+      const esResult = await searchService.searchKeyword(keyword, user, userGroups, searchOpts); // TODO: separate when not full-text search
 
 
       // create score map for sorting
       // create score map for sorting
       // key: id , value: score
       // key: id , value: score

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

@@ -911,7 +911,7 @@ class PageService {
       baseAggregation = baseAggregation.limit(Math.floor(total * 0.3));
       baseAggregation = baseAggregation.limit(Math.floor(total * 0.3));
     }
     }
 
 
-    const pagesStream = await baseAggregation.cursor({ batchSize: BATCH_SIZE }).exec();
+    const pagesStream = await baseAggregation.cursor({ batchSize: BATCH_SIZE });
 
 
     // use batch stream
     // use batch stream
     const batchStream = createBatchStream(BATCH_SIZE);
     const batchStream = createBatchStream(BATCH_SIZE);

+ 4 - 7
packages/app/src/server/service/search.ts

@@ -1,9 +1,8 @@
-import mongoose from 'mongoose';
 import RE2 from 're2';
 import RE2 from 're2';
 
 
 import { SearchDelegatorName } from '~/interfaces/named-query';
 import { SearchDelegatorName } from '~/interfaces/named-query';
 
 
-import { NamedQueryModel } from '../models/named-query';
+import NamedQuery from '../models/named-query';
 import {
 import {
   SearchDelegator, SearchQueryParser, SearchResolver, ParsedQuery, Result, MetaData, SearchableData, QueryTerms,
   SearchDelegator, SearchQueryParser, SearchResolver, ParsedQuery, Result, MetaData, SearchableData, QueryTerms,
 } from '../interfaces/search';
 } from '../interfaces/search';
@@ -106,9 +105,9 @@ class SearchService implements SearchQueryParser, SearchResolver {
     tagEvent.on('update', this.fullTextSearchDelegator.syncTagChanged.bind(this.fullTextSearchDelegator));
     tagEvent.on('update', this.fullTextSearchDelegator.syncTagChanged.bind(this.fullTextSearchDelegator));
 
 
     const commentEvent = this.crowi.event('comment');
     const commentEvent = this.crowi.event('comment');
-    commentEvent.on('create', this.delegator.syncCommentChanged.bind(this.delegator));
-    commentEvent.on('update', this.delegator.syncCommentChanged.bind(this.delegator));
-    commentEvent.on('delete', this.delegator.syncCommentChanged.bind(this.delegator));
+    commentEvent.on('create', this.fullTextSearchDelegator.syncCommentChanged.bind(this.fullTextSearchDelegator));
+    commentEvent.on('update', this.fullTextSearchDelegator.syncCommentChanged.bind(this.fullTextSearchDelegator));
+    commentEvent.on('delete', this.fullTextSearchDelegator.syncCommentChanged.bind(this.fullTextSearchDelegator));
   }
   }
 
 
   resetErrorStatus() {
   resetErrorStatus() {
@@ -181,8 +180,6 @@ class SearchService implements SearchQueryParser, SearchResolver {
     }
     }
 
 
     // when Named Query
     // when Named Query
-    const NamedQuery = mongoose.model('NamedQuery') as NamedQueryModel;
-
     const name = queryString.replace(replaceRegexp, '');
     const name = queryString.replace(replaceRegexp, '');
     const nq = await NamedQuery.findOne({ name });
     const nq = await NamedQuery.findOne({ name });
 
 

+ 16 - 0
packages/app/src/server/views/private-legacy-pages.html

@@ -0,0 +1,16 @@
+{% extends 'layout/layout.html' %}
+
+{% block html_additional_headers %}
+  {% parent %}
+  {{ cdnScriptTag('highlight-addons') }}
+  {{ cdnScriptTag('drawio-viewer') }}
+{% endblock %}
+
+{% block html_base_attr %}
+  data-spy="scroll"
+  data-target="#search-result-list"
+{% endblock %}
+
+{% block layout_main %}
+<p>This page is not implemented.</p>
+{% endblock %} {# layout_main #}

+ 39 - 10
packages/app/src/stores/ui.tsx

@@ -59,19 +59,43 @@ export const useIsMobile = (): SWRResponse<boolean|null, Error> => {
   return useStaticSWR(key, null, configuration);
   return useStaticSWR(key, null, configuration);
 };
 };
 
 
-// drawer mode keys
-const IS_DRAWER_MODE: Key = 'isDrawerMode';
 
 
-export const mutateDrawerMode: Middleware = (useSWRNext) => {
+const postChangeEditorModeMiddleware: Middleware = (useSWRNext) => {
   return (...args) => {
   return (...args) => {
-    const { mutate } = useSWRConfig();
+    // -- TODO: https://redmine.weseek.co.jp/issues/81817
     const swrNext = useSWRNext(...args);
     const swrNext = useSWRNext(...args);
     return {
     return {
       ...swrNext,
       ...swrNext,
       mutate: (data, shouldRevalidate) => {
       mutate: (data, shouldRevalidate) => {
         return swrNext.mutate(data, shouldRevalidate)
         return swrNext.mutate(data, shouldRevalidate)
           .then((value) => {
           .then((value) => {
-            mutate(IS_DRAWER_MODE); // mutate isDrawerMode
+            const newEditorMode = value as unknown as EditorMode;
+            switch (newEditorMode) {
+              case EditorMode.View:
+                $('body').removeClass('on-edit');
+                $('body').removeClass('builtin-editor');
+                $('body').removeClass('hackmd');
+                $('body').removeClass('pathname-sidebar');
+                window.history.replaceState(null, '', window.location.pathname);
+                break;
+              case EditorMode.Editor:
+                $('body').addClass('on-edit');
+                $('body').addClass('builtin-editor');
+                $('body').removeClass('hackmd');
+                // editing /Sidebar
+                if (window.location.pathname === '/Sidebar') {
+                  $('body').addClass('pathname-sidebar');
+                }
+                window.location.hash = '#edit';
+                break;
+              case EditorMode.HackMD:
+                $('body').addClass('on-edit');
+                $('body').addClass('hackmd');
+                $('body').removeClass('builtin-editor');
+                $('body').removeClass('pathname-sidebar');
+                window.location.hash = '#hackmd';
+                break;
+            }
             return value;
             return value;
           });
           });
       },
       },
@@ -83,7 +107,7 @@ export const useEditorMode = (editorMode?: EditorMode): SWRResponse<EditorMode,
   const key: Key = 'editorMode';
   const key: Key = 'editorMode';
   const initialData = EditorMode.View;
   const initialData = EditorMode.View;
 
 
-  return useStaticSWR(key, editorMode || null, { fallbackData: initialData, use: [mutateDrawerMode] });
+  return useStaticSWR(key, editorMode || null, { fallbackData: initialData, use: [postChangeEditorModeMiddleware] });
 };
 };
 
 
 export const useIsDeviceSmallerThanMd = (): SWRResponse<boolean|null, Error> => {
 export const useIsDeviceSmallerThanMd = (): SWRResponse<boolean|null, Error> => {
@@ -107,7 +131,7 @@ export const useIsDeviceSmallerThanMd = (): SWRResponse<boolean|null, Error> =>
     }
     }
   }
   }
 
 
-  return useStaticSWR(key, null, { use: [mutateDrawerMode] });
+  return useStaticSWR(key);
 };
 };
 
 
 export const usePreferDrawerModeByUser = (isPrefered?: boolean): SWRResponse<boolean, Error> => {
 export const usePreferDrawerModeByUser = (isPrefered?: boolean): SWRResponse<boolean, Error> => {
@@ -115,7 +139,7 @@ export const usePreferDrawerModeByUser = (isPrefered?: boolean): SWRResponse<boo
   const key: Key = data === undefined ? null : 'preferDrawerModeByUser';
   const key: Key = data === undefined ? null : 'preferDrawerModeByUser';
   const initialData = data?.preferDrawerModeByUser;
   const initialData = data?.preferDrawerModeByUser;
 
 
-  return useStaticSWR(key, isPrefered || null, { fallbackData: initialData, use: [mutateDrawerMode, sessionStorageMiddleware] });
+  return useStaticSWR(key, isPrefered || null, { fallbackData: initialData, use: [sessionStorageMiddleware] });
 };
 };
 
 
 export const usePreferDrawerModeOnEditByUser = (isPrefered?: boolean): SWRResponse<boolean, Error> => {
 export const usePreferDrawerModeOnEditByUser = (isPrefered?: boolean): SWRResponse<boolean, Error> => {
@@ -123,7 +147,7 @@ export const usePreferDrawerModeOnEditByUser = (isPrefered?: boolean): SWRRespon
   const key: Key = data === undefined ? null : 'preferDrawerModeOnEditByUser';
   const key: Key = data === undefined ? null : 'preferDrawerModeOnEditByUser';
   const initialData = data?.preferDrawerModeOnEditByUser;
   const initialData = data?.preferDrawerModeOnEditByUser;
 
 
-  return useStaticSWR(key, isPrefered || null, { fallbackData: initialData, use: [mutateDrawerMode, sessionStorageMiddleware] });
+  return useStaticSWR(key, isPrefered || null, { fallbackData: initialData, use: [sessionStorageMiddleware] });
 };
 };
 
 
 export const useDrawerMode = (): SWRResponse<boolean, Error> => {
 export const useDrawerMode = (): SWRResponse<boolean, Error> => {
@@ -145,7 +169,7 @@ export const useDrawerMode = (): SWRResponse<boolean, Error> => {
   };
   };
 
 
   return useSWR(
   return useSWR(
-    condition ? [IS_DRAWER_MODE, editorMode, preferDrawerModeByUser, preferDrawerModeOnEditByUser, isDeviceSmallerThanMd] : null,
+    condition ? [editorMode, preferDrawerModeByUser, preferDrawerModeOnEditByUser, isDeviceSmallerThanMd] : null,
     calcDrawerMode,
     calcDrawerMode,
     {
     {
       fallback: calcDrawerMode,
       fallback: calcDrawerMode,
@@ -209,3 +233,8 @@ export const useSidebarResizeDisabled = (isDisabled?: boolean): SWRResponse<bool
   const initialData = false;
   const initialData = false;
   return useStaticSWR('isSidebarResizeDisabled', isDisabled || null, { fallbackData: initialData });
   return useStaticSWR('isSidebarResizeDisabled', isDisabled || null, { fallbackData: initialData });
 };
 };
+
+export const usePageCreateModalOpened = (isOpened?: boolean): SWRResponse<boolean, Error> => {
+  const initialData = false;
+  return useStaticSWR('isPageCreateModalOpened', isOpened || null, { fallbackData: initialData });
+};

+ 9 - 1
packages/app/src/styles/_sidebar.scss

@@ -262,7 +262,11 @@
 @mixin drawer() {
 @mixin drawer() {
   z-index: $zindex-fixed + 2;
   z-index: $zindex-fixed + 2;
 
 
-  // override @atlaskit/navigation-next styles
+  .data-layout-container {
+    position: fixed;
+    top: 0;
+    width: 0;
+  }
   div.navigation {
   div.navigation {
     max-width: 80vw;
     max-width: 80vw;
 
 
@@ -286,6 +290,10 @@
     }
     }
   }
   }
 
 
+  .grw-navigation-resize-button {
+    display: none;
+  }
+
   .grw-drawer-toggler {
   .grw-drawer-toggler {
     position: fixed;
     position: fixed;
     right: -15px;
     right: -15px;

+ 12 - 0
yarn.lock

@@ -2790,6 +2790,13 @@
     jest-diff "^26.0.0"
     jest-diff "^26.0.0"
     pretty-format "^26.0.0"
     pretty-format "^26.0.0"
 
 
+"@types/jquery@^3.5.8":
+  version "3.5.8"
+  resolved "https://registry.yarnpkg.com/@types/jquery/-/jquery-3.5.8.tgz#83bfbcdf4e625c5471590f92703c06aadb052a09"
+  integrity sha512-cXk6NwqjDYg+UI9p2l3x0YmPa4m7RrXqmbK4IpVVpRJiYXU/QTo+UZrn54qfE1+9Gao4qpYqUnxm5ZCy2FTXAw==
+  dependencies:
+    "@types/sizzle" "*"
+
 "@types/json-schema@7.0.6":
 "@types/json-schema@7.0.6":
   version "7.0.6"
   version "7.0.6"
   resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.6.tgz#f4c7ec43e81b319a9815115031709f26987891f0"
   resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.6.tgz#f4c7ec43e81b319a9815115031709f26987891f0"
@@ -2963,6 +2970,11 @@
     "@types/mime" "^1"
     "@types/mime" "^1"
     "@types/node" "*"
     "@types/node" "*"
 
 
+"@types/sizzle@*":
+  version "2.3.3"
+  resolved "https://registry.yarnpkg.com/@types/sizzle/-/sizzle-2.3.3.tgz#ff5e2f1902969d305225a047c8a0fd5c915cebef"
+  integrity sha512-JYM8x9EGF163bEyhdJBpR2QX1R5naCJHC8ucJylJ3w9/CVBaskdQ8WqBf8MmQrd1kRvp/a4TS8HJ+bxzR7ZJYQ==
+
 "@types/stack-utils@^2.0.0":
 "@types/stack-utils@^2.0.0":
   version "2.0.1"
   version "2.0.1"
   resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.1.tgz#20f18294f797f2209b5f65c8e3b5c8e8261d127c"
   resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.1.tgz#20f18294f797f2209b5f65c8e3b5c8e8261d127c"