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

Merge pull request #2398 from weseek/improvement/initialize-client

Improvement/initialize client
Yuki Takei 5 лет назад
Родитель
Сommit
66de33729f

+ 1 - 1
CHANGES.md

@@ -2,7 +2,7 @@
 
 ## v4.0.7-RC
 
-* 
+* Fix: Styles are not applyed on installer
 
 ## v4.0.6
 

+ 2 - 1
config/logger/config.dev.js

@@ -30,7 +30,8 @@ module.exports = {
   /*
    * configure level for client
    */
-  'growi:app': 'debug',
+  'growi:cli:bootstrap': 'debug',
+  'growi:cli:app': 'debug',
   'growi:services:*': 'debug',
   // 'growi:StaffCredit': 'debug',
   // 'growi:TableOfContents': 'debug',

+ 6 - 2
config/webpack.common.js

@@ -20,6 +20,7 @@ module.exports = (options) => {
   return {
     mode: options.mode,
     entry: Object.assign({
+      'js/boot':                      './src/client/js/boot',
       'js/app':                       './src/client/js/app',
       'js/admin':                     './src/client/js/admin',
       'js/nologin':                   './src/client/js/nologin',
@@ -165,7 +166,10 @@ module.exports = (options) => {
           },
           commons: {
             test: /(src|resource)[\\/].*\.(js|jsx|json)$/,
-            chunks: 'initial',
+            chunks: (chunk) => {
+              // ignore patterns
+              return chunk.name != null && !chunk.name.match(/boot/);
+            },
             name: 'js/commons',
             minChunks: 2,
             minSize: 1,
@@ -175,7 +179,7 @@ module.exports = (options) => {
             test: /node_modules[\\/].*\.(js|jsx|json)$/,
             chunks: (chunk) => {
               // ignore patterns
-              return chunk.name != null && !chunk.name.match(/legacy-presentation|ie11-polyfill|hackmd-/);
+              return chunk.name != null && !chunk.name.match(/boot|legacy-presentation|ie11-polyfill|hackmd-/);
             },
             name: 'js/vendors',
             minSize: 1,

+ 7 - 1
src/client/js/admin.jsx

@@ -23,6 +23,8 @@ import ExportArchiveDataPage from './components/Admin/ExportArchiveDataPage';
 import FullTextSearchManagement from './components/Admin/FullTextSearchManagement';
 import AdminNavigation from './components/Admin/Common/AdminNavigation';
 
+import NavigationContainer from './services/NavigationContainer';
+
 import AdminHomeContainer from './services/AdminHomeContainer';
 import AdminCustomizeContainer from './services/AdminCustomizeContainer';
 import AdminUserGroupDetailContainer from './services/AdminUserGroupDetailContainer';
@@ -41,14 +43,17 @@ import AdminGitHubSecurityContainer from './services/AdminGitHubSecurityContaine
 import AdminTwitterSecurityContainer from './services/AdminTwitterSecurityContainer';
 import AdminNotificationContainer from './services/AdminNotificationContainer';
 
-import { appContainer, componentMappings } from './bootstrap';
+import { appContainer, componentMappings } from './base';
 
 const logger = loggerFactory('growi:admin');
 
+appContainer.initContents();
+
 const { i18n } = appContainer;
 const websocketContainer = appContainer.getContainer('WebsocketContainer');
 
 // create unstated container instance
+const navigationContainer = new NavigationContainer(appContainer);
 const adminAppContainer = new AdminAppContainer(appContainer);
 const adminHomeContainer = new AdminHomeContainer(appContainer);
 const adminCustomizeContainer = new AdminCustomizeContainer(appContainer);
@@ -60,6 +65,7 @@ const adminUserGroupDetailContainer = new AdminUserGroupDetailContainer(appConta
 const injectableContainers = [
   appContainer,
   websocketContainer,
+  navigationContainer,
   adminAppContainer,
   adminHomeContainer,
   adminCustomizeContainer,

+ 22 - 9
src/client/js/app.jsx

@@ -32,6 +32,7 @@ import LikerList from './components/User/LikerList';
 import TableOfContents from './components/TableOfContents';
 
 import PersonalSettings from './components/Me/PersonalSettings';
+import NavigationContainer from './services/NavigationContainer';
 import PageContainer from './services/PageContainer';
 import CommentContainer from './services/CommentContainer';
 import EditorContainer from './services/EditorContainer';
@@ -40,21 +41,24 @@ import GrowiSubNavigation from './components/Navbar/GrowiSubNavigation';
 import GrowiSubNavigationForUserPage from './components/Navbar/GrowiSubNavigationForUserPage';
 import PersonalContainer from './services/PersonalContainer';
 
-import { appContainer, componentMappings } from './bootstrap';
+import { appContainer, componentMappings } from './base';
 
-const logger = loggerFactory('growi:app');
+const logger = loggerFactory('growi:cli:app');
+
+appContainer.initContents();
 
 const { i18n } = appContainer;
 const websocketContainer = appContainer.getContainer('WebsocketContainer');
 
 // create unstated container instance
+const navigationContainer = new NavigationContainer(appContainer);
 const pageContainer = new PageContainer(appContainer);
 const commentContainer = new CommentContainer(appContainer);
 const editorContainer = new EditorContainer(appContainer, defaultEditorOptions, defaultPreviewOptions);
 const tagContainer = new TagContainer(appContainer);
 const personalContainer = new PersonalContainer(appContainer);
 const injectableContainers = [
-  appContainer, websocketContainer, pageContainer, commentContainer, editorContainer, tagContainer, personalContainer,
+  appContainer, websocketContainer, navigationContainer, pageContainer, commentContainer, editorContainer, tagContainer, personalContainer,
 ];
 
 logger.info('unstated containers have been initialized');
@@ -70,11 +74,7 @@ Object.assign(componentMappings, {
   // 'revision-history': <PageHistory pageId={pageId} />,
   'tags-page': <TagsList crowi={appContainer} />,
 
-  'page-editor': <PageEditor />,
-  'page-editor-path-nav': <PagePathNavForEditor />,
-  'page-editor-options-selector': <OptionsSelector crowi={appContainer} />,
   'page-status-alert': <PageStatusAlert />,
-  'save-page-controls': <SavePageControls />,
 
   'trash-page-alert': <TrashPageAlert />,
 
@@ -86,10 +86,9 @@ Object.assign(componentMappings, {
 // additional definitions if data exists
 if (pageContainer.state.pageId != null) {
   Object.assign(componentMappings, {
-    'page-editor-with-hackmd': <PageEditorByHackmd />,
     'page-comments-list': <PageComments />,
-    'page-attachment': <PageAttachment />,
     'page-comment-write': <CommentEditorLazyRenderer />,
+    'page-attachment': <PageAttachment />,
     'page-management': <PageManagement />,
 
     'revision-toc': <TableOfContents />,
@@ -108,6 +107,20 @@ if (pageContainer.state.path != null) {
     'grw-subnav-for-user-page': <GrowiSubNavigationForUserPage />,
   });
 }
+// additional definitions if user is logged in
+if (appContainer.currentUser != null) {
+  Object.assign(componentMappings, {
+    'page-editor': <PageEditor />,
+    'page-editor-path-nav': <PagePathNavForEditor />,
+    'page-editor-options-selector': <OptionsSelector crowi={appContainer} />,
+    'save-page-controls': <SavePageControls />,
+  });
+  if (pageContainer.state.pageId != null) {
+    Object.assign(componentMappings, {
+      'page-editor-with-hackmd': <PageEditorByHackmd />,
+    });
+  }
+}
 
 Object.keys(componentMappings).forEach((key) => {
   const elem = document.getElementById(key);

+ 3 - 4
src/client/js/bootstrap.jsx → src/client/js/base.jsx

@@ -14,7 +14,7 @@ import WebsocketContainer from './services/WebsocketContainer';
 import PageCreateButton from './components/Navbar/PageCreateButton';
 import PageCreateModal from './components/PageCreateModal';
 
-const logger = loggerFactory('growi:app');
+const logger = loggerFactory('growi:cli:app');
 
 if (!window) {
   window = {};
@@ -29,10 +29,9 @@ const appContainer = new AppContainer();
 // eslint-disable-next-line no-unused-vars
 const websocketContainer = new WebsocketContainer(appContainer);
 
-logger.info('unstated containers have been initialized');
+appContainer.initApp();
 
-appContainer.init();
-appContainer.injectToWindow();
+logger.info('AppContainer has been initialized');
 
 /**
  * define components

+ 5 - 0
src/client/js/boot.js

@@ -0,0 +1,5 @@
+import {
+  applyColorScheme,
+} from './util/color-scheme';
+
+applyColorScheme();

+ 13 - 12
src/client/js/components/Drawio.jsx

@@ -3,6 +3,8 @@ import PropTypes from 'prop-types';
 
 import { withTranslation } from 'react-i18next';
 
+import NotAvailableForGuest from './NotAvailableForGuest';
+
 class Drawio extends React.Component {
 
   constructor(props) {
@@ -23,11 +25,9 @@ class Drawio extends React.Component {
   }
 
   onEdit() {
-    if (window.crowi != null) {
-      window.crowi.launchDrawioModal('page',
-        this.props.rangeLineNumberOfMarkdown.beginLineNumber,
-        this.props.rangeLineNumberOfMarkdown.endLineNumber);
-    }
+    const { appContainer, rangeLineNumberOfMarkdown } = this.props;
+    const { beginLineNumber, endLineNumber } = rangeLineNumberOfMarkdown;
+    appContainer.launchDrawioModal('page', beginLineNumber, endLineNumber);
   }
 
   componentDidMount() {
@@ -53,13 +53,13 @@ class Drawio extends React.Component {
   render() {
     return (
       <div className="editable-with-drawio position-relative">
-        { !this.isPreview
-          && (
-          <button type="button" className="drawio-iframe-trigger position-absolute btn btn-outline-secondary" onClick={this.onEdit}>
-            <i className="icon-note mr-1"></i>{this.props.t('Edit')}
-          </button>
-          )
-        }
+        { !this.isPreview && (
+          <NotAvailableForGuest>
+            <button type="button" className="drawio-iframe-trigger position-absolute btn btn-outline-secondary" onClick={this.onEdit}>
+              <i className="icon-note mr-1"></i>{this.props.t('Edit')}
+            </button>
+          </NotAvailableForGuest>
+        ) }
         <div
           className="drawio"
           style={this.style}
@@ -77,6 +77,7 @@ class Drawio extends React.Component {
 Drawio.propTypes = {
   t: PropTypes.func.isRequired, // i18next
   appContainer: PropTypes.object.isRequired,
+
   drawioContent: PropTypes.any.isRequired,
   isPreview: PropTypes.bool,
   rangeLineNumberOfMarkdown: PropTypes.object.isRequired,

+ 9 - 8
src/client/js/components/LoginForm.jsx

@@ -4,8 +4,8 @@ import ReactCardFlip from 'react-card-flip';
 
 import { withTranslation } from 'react-i18next';
 
+import AppContainer from '../services/AppContainer';
 import { withUnstatedContainers } from './UnstatedUtils';
-import NoLoginContainer from '../services/NoLoginContainer';
 
 class LoginForm extends React.Component {
 
@@ -35,12 +35,12 @@ class LoginForm extends React.Component {
 
   handleLoginWithExternalAuth(e) {
     const auth = e.currentTarget.id;
-    const csrf = this.props.noLoginContainer.csrfToken;
+    const { csrf } = this.props.appContainer;
     window.location.href = `/passport/${auth}?_csrf=${csrf}`;
   }
 
   renderLocalOrLdapLoginForm() {
-    const { t, noLoginContainer, isLdapStrategySetup } = this.props;
+    const { t, appContainer, isLdapStrategySetup } = this.props;
 
     return (
       <form role="form" action="/login" method="post">
@@ -70,7 +70,7 @@ class LoginForm extends React.Component {
         </div>
 
         <div className="input-group my-4">
-          <input type="hidden" name="_csrf" value={noLoginContainer.csrfToken} />
+          <input type="hidden" name="_csrf" value={appContainer.csrfToken} />
           <button type="submit" id="login" className="btn btn-fill rounded-0 login mx-auto">
             <div className="eff"></div>
             <span className="btn-label">
@@ -147,10 +147,10 @@ class LoginForm extends React.Component {
   renderRegisterForm() {
     const {
       t,
+      appContainer,
       username,
       name,
       email,
-      noLoginContainer,
       registrationMode,
       registrationWhiteList,
     } = this.props;
@@ -220,7 +220,7 @@ class LoginForm extends React.Component {
           </div>
 
           <div className="input-group justify-content-center my-4">
-            <input type="hidden" name="_csrf" value={noLoginContainer.csrfToken} />
+            <input type="hidden" name="_csrf" value={appContainer.csrfToken} />
             <button type="submit" className="btn btn-fill rounded-0" id="register">
               <div className="eff"></div>
               <span className="btn-label">
@@ -293,12 +293,13 @@ class LoginForm extends React.Component {
 /**
  * Wrapper component for using unstated
  */
-const LoginFormWrapper = withUnstatedContainers(LoginForm, [NoLoginContainer]);
+const LoginFormWrapper = withUnstatedContainers(LoginForm, [AppContainer]);
 
 LoginForm.propTypes = {
   // i18next
   t: PropTypes.func.isRequired,
-  noLoginContainer: PropTypes.instanceOf(NoLoginContainer).isRequired,
+  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
+
   isRegistering: PropTypes.bool,
   username: PropTypes.string,
   name: PropTypes.string,

+ 5 - 5
src/client/js/components/Navbar/NavbarToggler.jsx

@@ -4,14 +4,14 @@ import PropTypes from 'prop-types';
 import { withTranslation } from 'react-i18next';
 
 import { withUnstatedContainers } from '../UnstatedUtils';
-import AppContainer from '../../services/AppContainer';
+import NavigationContainer from '../../services/NavigationContainer';
 
 const NavbarToggler = (props) => {
 
-  const { appContainer } = props;
+  const { navigationContainer } = props;
 
   const clickHandler = () => {
-    appContainer.toggleDrawer();
+    navigationContainer.toggleDrawer();
   };
 
   return (
@@ -31,12 +31,12 @@ const NavbarToggler = (props) => {
 /**
  * Wrapper component for using unstated
  */
-const NavbarTogglerWrapper = withUnstatedContainers(NavbarToggler, [AppContainer]);
+const NavbarTogglerWrapper = withUnstatedContainers(NavbarToggler, [NavigationContainer]);
 
 
 NavbarToggler.propTypes = {
   t: PropTypes.func.isRequired, //  i18next
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
+  navigationContainer: PropTypes.instanceOf(NavigationContainer).isRequired,
 };
 
 export default withTranslation()(NavbarTogglerWrapper);

+ 6 - 6
src/client/js/components/Navbar/PageCreateButton.jsx

@@ -4,21 +4,21 @@ import PropTypes from 'prop-types';
 import { withTranslation } from 'react-i18next';
 
 import { withUnstatedContainers } from '../UnstatedUtils';
-import AppContainer from '../../services/AppContainer';
+import NavigationContainer from '../../services/NavigationContainer';
 
 const PageCreateButton = (props) => {
-  const { t, appContainer, isIcon } = props;
+  const { t, navigationContainer, isIcon } = props;
 
   if (isIcon) {
     return (
-      <button className="btn btn-lg btn-primary rounded-circle waves-effect waves-light" type="button" onClick={appContainer.openPageCreateModal}>
+      <button className="btn btn-lg btn-primary rounded-circle waves-effect waves-light" type="button" onClick={navigationContainer.openPageCreateModal}>
         <i className="icon-pencil"></i>
       </button>
     );
   }
 
   return (
-    <button className="px-md-2 nav-link create-page border-0 bg-transparent" type="button" onClick={appContainer.openPageCreateModal}>
+    <button className="px-md-2 nav-link 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>
@@ -28,12 +28,12 @@ const PageCreateButton = (props) => {
 /**
  * Wrapper component for using unstated
  */
-const PageCreateButtonWrapper = withUnstatedContainers(PageCreateButton, [AppContainer]);
+const PageCreateButtonWrapper = withUnstatedContainers(PageCreateButton, [NavigationContainer]);
 
 
 PageCreateButton.propTypes = {
   t: PropTypes.func.isRequired, //  i18next
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
+  navigationContainer: PropTypes.instanceOf(NavigationContainer).isRequired,
 
   isIcon: PropTypes.bool,
 };

+ 7 - 5
src/client/js/components/Navbar/PersonalDropdown.jsx

@@ -7,12 +7,13 @@ import { UncontrolledTooltip } from 'reactstrap';
 
 import { withUnstatedContainers } from '../UnstatedUtils';
 import AppContainer from '../../services/AppContainer';
+import NavigationContainer from '../../services/NavigationContainer';
 
 import UserPicture from '../User/UserPicture';
 
 const PersonalDropdown = (props) => {
 
-  const { t, appContainer } = props;
+  const { t, appContainer, navigationContainer } = props;
   const user = appContainer.currentUser || {};
 
   const logoutHandler = () => {
@@ -28,11 +29,11 @@ const PersonalDropdown = (props) => {
   };
 
   const preferDrawerModeSwitchModifiedHandler = (bool) => {
-    appContainer.setDrawerModePreference(bool);
+    navigationContainer.setDrawerModePreference(bool);
   };
 
   const preferDrawerModeOnEditSwitchModifiedHandler = (bool) => {
-    appContainer.setDrawerModePreferenceOnEdit(bool);
+    navigationContainer.setDrawerModePreferenceOnEdit(bool);
   };
 
   const followOsCheckboxModifiedHandler = (bool) => {
@@ -56,7 +57,7 @@ const PersonalDropdown = (props) => {
    */
   const {
     preferDarkModeByMediaQuery, preferDarkModeByUser, preferDrawerModeByUser, preferDrawerModeOnEditByUser,
-  } = appContainer.state;
+  } = navigationContainer.state;
   const isUserPreferenceExists = preferDarkModeByUser != null;
   const isDarkMode = () => {
     if (isUserPreferenceExists) {
@@ -205,12 +206,13 @@ const PersonalDropdown = (props) => {
 /**
  * Wrapper component for using unstated
  */
-const PersonalDropdownWrapper = withUnstatedContainers(PersonalDropdown, [AppContainer]);
+const PersonalDropdownWrapper = withUnstatedContainers(PersonalDropdown, [AppContainer, NavigationContainer]);
 
 
 PersonalDropdown.propTypes = {
   t: PropTypes.func.isRequired, //  i18next
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
+  navigationContainer: PropTypes.instanceOf(NavigationContainer).isRequired,
 };
 
 export default withTranslation()(PersonalDropdownWrapper);

+ 4 - 2
src/client/js/components/Navbar/SearchTop.jsx

@@ -4,6 +4,7 @@ import { withTranslation } from 'react-i18next';
 
 import { withUnstatedContainers } from '../UnstatedUtils';
 import AppContainer from '../../services/AppContainer';
+import NavigationContainer from '../../services/NavigationContainer';
 
 import SearchForm from '../SearchForm';
 
@@ -51,7 +52,7 @@ class SearchTop extends React.Component {
   }
 
   Root = ({ children }) => {
-    const { isDeviceSmallerThanMd: isCollapsed } = this.props.appContainer.state;
+    const { isDeviceSmallerThanMd: isCollapsed } = this.props.navigationContainer.state;
 
     return isCollapsed
       ? (
@@ -116,11 +117,12 @@ class SearchTop extends React.Component {
 SearchTop.propTypes = {
   t: PropTypes.func.isRequired, // i18next
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
+  navigationContainer: PropTypes.instanceOf(NavigationContainer).isRequired,
 };
 
 /**
  * Wrapper component for using unstated
  */
-const SearchTopWrapper = withUnstatedContainers(SearchTop, [AppContainer]);
+const SearchTopWrapper = withUnstatedContainers(SearchTop, [AppContainer, NavigationContainer]);
 
 export default withTranslation()(SearchTopWrapper);

+ 30 - 0
src/client/js/components/NotAvailableForGuest.jsx

@@ -0,0 +1,30 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+
+import { UncontrolledTooltip } from 'reactstrap';
+
+const NotAvailableForGuest = ({ children }) => {
+
+  const id = children.props.id || `grw-not-available-for-guest-${Math.random().toString(32).substring(2)}`;
+
+  // clone and add className
+  const clonedChild = React.cloneElement(children, {
+    id,
+    className: `${children.props.className} grw-not-available-for-guest`,
+    onClick: () => { /* do nothing */ },
+  });
+
+  return (
+    <>
+      { clonedChild }
+      <UncontrolledTooltip placement="top" target={id}>Not available for guest</UncontrolledTooltip>
+    </>
+  );
+
+};
+
+NotAvailableForGuest.propTypes = {
+  children: PropTypes.node.isRequired,
+};
+
+export default NotAvailableForGuest;

+ 11 - 4
src/client/js/components/Page.jsx

@@ -126,14 +126,21 @@ class Page extends React.Component {
   }
 
   render() {
-    const isMobile = this.props.appContainer.isMobile;
-    const { markdown } = this.props.pageContainer.state;
+    const { appContainer, pageContainer } = this.props;
+    const isMobile = appContainer.isMobile;
+    const isLoggedIn = appContainer.currentUser != null;
+    const { markdown } = pageContainer.state;
 
     return (
       <div className={isMobile ? 'page-mobile' : ''}>
         <RevisionRenderer growiRenderer={this.growiRenderer} markdown={markdown} />
-        <HandsontableModal ref={this.handsontableModal} onSave={this.saveHandlerForHandsontableModal} />
-        <DrawioModal ref={this.drawioModal} onSave={this.saveHandlerForDrawioModal} />
+
+        { isLoggedIn && (
+          <>
+            <HandsontableModal ref={this.handsontableModal} onSave={this.saveHandlerForHandsontableModal} />
+            <DrawioModal ref={this.drawioModal} onSave={this.saveHandlerForDrawioModal} />
+          </>
+        )}
       </div>
     );
   }

+ 6 - 4
src/client/js/components/Page/TagLabels.jsx

@@ -6,6 +6,7 @@ import * as toastr from 'toastr';
 
 import { withUnstatedContainers } from '../UnstatedUtils';
 import AppContainer from '../../services/AppContainer';
+import NavigationContainer from '../../services/NavigationContainer';
 import PageContainer from '../../services/PageContainer';
 import EditorContainer from '../../services/EditorContainer';
 
@@ -30,7 +31,7 @@ class TagLabels extends React.Component {
    *   2. editorContainer.state.tags if editorMode is not null
    */
   getEditTargetData() {
-    const { editorMode } = this.props.appContainer.state;
+    const { editorMode } = this.props.navigationContainer.state;
     return (editorMode == null)
       ? this.props.pageContainer.state.tags
       : this.props.editorContainer.state.tags;
@@ -41,8 +42,8 @@ class TagLabels extends React.Component {
   }
 
   async tagsUpdatedHandler(tags) {
-    const { appContainer, editorContainer } = this.props;
-    const { editorMode } = appContainer.state;
+    const { appContainer, navigationContainer, editorContainer } = this.props;
+    const { editorMode } = navigationContainer.state;
 
     // post api request and update tags
     if (editorMode == null) {
@@ -137,12 +138,13 @@ class TagLabels extends React.Component {
 /**
  * Wrapper component for using unstated
  */
-const TagLabelsWrapper = withUnstatedContainers(TagLabels, [AppContainer, PageContainer, EditorContainer]);
+const TagLabelsWrapper = withUnstatedContainers(TagLabels, [AppContainer, NavigationContainer, PageContainer, EditorContainer]);
 
 
 TagLabels.propTypes = {
   t: PropTypes.func.isRequired, // i18next
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
+  navigationContainer: PropTypes.instanceOf(NavigationContainer).isRequired,
   pageContainer: PropTypes.instanceOf(PageContainer).isRequired,
   editorContainer: PropTypes.instanceOf(EditorContainer).isRequired,
 };

+ 10 - 7
src/client/js/components/PageComment/CommentEditor.jsx

@@ -20,6 +20,7 @@ import Editor from '../PageEditor/Editor';
 import SlackNotification from '../SlackNotification';
 
 import CommentPreview from './CommentPreview';
+import NotAvailableForGuest from '../NotAvailableForGuest';
 
 /**
  *
@@ -242,13 +243,15 @@ class CommentEditor extends React.Component {
   renderBeforeReady() {
     return (
       <div className="text-center">
-        <button
-          type="button"
-          className="btn btn-lg btn-link"
-          onClick={() => this.setState({ isReadyToUse: true })}
-        >
-          <i className="icon-bubble"></i> Add Comment
-        </button>
+        <NotAvailableForGuest>
+          <button
+            type="button"
+            className="btn btn-lg btn-link"
+            onClick={() => this.setState({ isReadyToUse: true })}
+          >
+            <i className="icon-bubble"></i> Add Comment
+          </button>
+        </NotAvailableForGuest>
       </div>
     );
   }

+ 1 - 1
src/client/js/components/PageComments.jsx

@@ -174,7 +174,7 @@ class PageComments extends React.Component {
             </Button>
           </div>
         )}
-        { showEditor && isLoggedIn && (
+        { showEditor && (
           <div className="page-comment-reply-form ml-4 ml-sm-5 mr-3">
             <CommentEditor
               growiRenderer={this.growiRenderer}

+ 9 - 5
src/client/js/components/PageCreateModal.jsx

@@ -10,13 +10,15 @@ import urljoin from 'url-join';
 
 import { userPageRoot } from '@commons/util/path-utils';
 import { pathUtils } from 'growi-commons';
-import { withUnstatedContainers } from './UnstatedUtils';
 
 import AppContainer from '../services/AppContainer';
+import NavigationContainer from '../services/NavigationContainer';
+import { withUnstatedContainers } from './UnstatedUtils';
+
 import PagePathAutoComplete from './PagePathAutoComplete';
 
 const PageCreateModal = (props) => {
-  const { t, appContainer } = props;
+  const { t, appContainer, navigationContainer } = props;
 
   const config = appContainer.getConfig();
   const isReachable = config.isSearchServiceReachable;
@@ -240,9 +242,10 @@ const PageCreateModal = (props) => {
       </div>
     );
   }
+
   return (
-    <Modal size="lg" isOpen={appContainer.state.isPageCreateModalShown} toggle={appContainer.closePageCreateModal} className="grw-create-page">
-      <ModalHeader tag="h4" toggle={appContainer.closePageCreateModal} className="bg-primary text-light">
+    <Modal size="lg" isOpen={navigationContainer.state.isPageCreateModalShown} toggle={navigationContainer.closePageCreateModal} className="grw-create-page">
+      <ModalHeader tag="h4" toggle={navigationContainer.closePageCreateModal} className="bg-primary text-light">
         { t('New Page') }
       </ModalHeader>
       <ModalBody>
@@ -259,12 +262,13 @@ const PageCreateModal = (props) => {
 /**
  * Wrapper component for using unstated
  */
-const ModalControlWrapper = withUnstatedContainers(PageCreateModal, [AppContainer]);
+const ModalControlWrapper = withUnstatedContainers(PageCreateModal, [AppContainer, NavigationContainer]);
 
 
 PageCreateModal.propTypes = {
   t: PropTypes.func.isRequired, //  i18next
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
+  navigationContainer: PropTypes.instanceOf(NavigationContainer).isRequired,
 };
 
 export default withTranslation()(ModalControlWrapper);

+ 9 - 6
src/client/js/components/Sidebar.jsx

@@ -10,6 +10,7 @@ import {
 
 import { withUnstatedContainers } from './UnstatedUtils';
 import AppContainer from '../services/AppContainer';
+import NavigationContainer from '../services/NavigationContainer';
 
 import SidebarNav from './Sidebar/SidebarNav';
 import RecentChanges from './Sidebar/RecentChanges';
@@ -22,6 +23,7 @@ class Sidebar extends React.Component {
 
   static propTypes = {
     appContainer: PropTypes.instanceOf(AppContainer).isRequired,
+    navigationContainer: PropTypes.instanceOf(NavigationContainer).isRequired,
     navigationUIController: PropTypes.any.isRequired,
     isDrawerModeOnInit: PropTypes.bool,
   };
@@ -58,7 +60,7 @@ class Sidebar extends React.Component {
    * return whether drawer mode or not
    */
   get isDrawerMode() {
-    let isDrawerMode = this.props.appContainer.state.isDrawerMode;
+    let isDrawerMode = this.props.navigationContainer.state.isDrawerMode;
     if (isDrawerMode == null) {
       isDrawerMode = this.props.isDrawerModeOnInit;
     }
@@ -120,8 +122,8 @@ class Sidebar extends React.Component {
   }
 
   backdropClickedHandler = () => {
-    const { appContainer } = this.props;
-    appContainer.setState({ isDrawerOpened: false });
+    const { navigationContainer } = this.props;
+    navigationContainer.setState({ isDrawerOpened: false });
   }
 
   itemSelectedHandler = (contentsId) => {
@@ -156,7 +158,7 @@ class Sidebar extends React.Component {
   }
 
   render() {
-    const { isDrawerOpened } = this.props.appContainer.state;
+    const { isDrawerOpened } = this.props.navigationContainer.state;
 
     return (
       <>
@@ -199,7 +201,7 @@ const SidebarWithNavigationUIController = withNavigationUIController(Sidebar);
  */
 
 const SidebarWithNavigation = (props) => {
-  const { preferDrawerModeByUser: isDrawerModeOnInit } = props.appContainer.state;
+  const { preferDrawerModeByUser: isDrawerModeOnInit } = props.navigationContainer.state;
 
   const initUICForDrawerMode = isDrawerModeOnInit
     // generate initialUIController for Drawer mode
@@ -220,6 +222,7 @@ const SidebarWithNavigation = (props) => {
 
 SidebarWithNavigation.propTypes = {
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
+  navigationContainer: PropTypes.instanceOf(NavigationContainer).isRequired,
 };
 
-export default withUnstatedContainers(SidebarWithNavigation, [AppContainer]);
+export default withUnstatedContainers(SidebarWithNavigation, [AppContainer, NavigationContainer]);

+ 10 - 5
src/client/js/legacy/crowi.js

@@ -240,10 +240,12 @@ $(() => {
 
   // tab changing handling
   $('a[href="#revision-body"]').on('show.bs.tab', () => {
-    appContainer.setEditorMode(null);
+    const navigationContainer = appContainer.getContainer('NavigationContainer');
+    navigationContainer.setEditorMode(null);
   });
   $('a[href="#edit"]').on('show.bs.tab', () => {
-    appContainer.setEditorMode('builtin');
+    const navigationContainer = appContainer.getContainer('NavigationContainer');
+    navigationContainer.setEditorMode('builtin');
     $('body').addClass('on-edit');
     $('body').addClass('builtin-editor');
   });
@@ -252,7 +254,8 @@ $(() => {
     $('body').removeClass('builtin-editor');
   });
   $('a[href="#hackmd"]').on('show.bs.tab', () => {
-    appContainer.setEditorMode('hackmd');
+    const navigationContainer = appContainer.getContainer('NavigationContainer');
+    navigationContainer.setEditorMode('hackmd');
     $('body').addClass('on-edit');
     $('body').addClass('hackmd');
   });
@@ -317,8 +320,10 @@ window.addEventListener('load', (e) => {
 
   // hash on page
   if (window.location.hash) {
+    const navigationContainer = appContainer.getContainer('NavigationContainer');
+
     if ((window.location.hash === '#edit' || window.location.hash === '#edit-form') && $('.tab-pane#edit').length > 0) {
-      appContainer.setState({ editorMode: 'builtin' });
+      navigationContainer.setEditorMode('builtin');
 
       $('a[data-toggle="tab"][href="#edit"]').tab('show');
       $('body').addClass('on-edit');
@@ -328,7 +333,7 @@ window.addEventListener('load', (e) => {
       Crowi.setCaretLineAndFocusToEditor();
     }
     else if (window.location.hash === '#hackmd' && $('.tab-pane#hackmd').length > 0) {
-      appContainer.setState({ editorMode: 'hackmd' });
+      navigationContainer.setEditorMode('hackmd');
 
       $('a[data-toggle="tab"][href="#hackmd"]').tab('show');
       $('body').addClass('on-edit');

+ 2 - 4
src/client/js/nologin.jsx

@@ -5,7 +5,6 @@ import { I18nextProvider } from 'react-i18next';
 
 import i18nFactory from './util/i18n';
 
-import NoLoginContainer from './services/NoLoginContainer';
 import AppContainer from './services/AppContainer';
 
 import InstallerForm from './components/InstallerForm';
@@ -31,9 +30,8 @@ if (installerFormElem) {
 // render loginForm
 const loginFormElem = document.getElementById('login-form');
 if (loginFormElem) {
-  const noLoginContainer = new NoLoginContainer();
   const appContainer = new AppContainer();
-  appContainer.init();
+  appContainer.initApp();
 
   const username = loginFormElem.dataset.username;
   const name = loginFormElem.dataset.name;
@@ -62,7 +60,7 @@ if (loginFormElem) {
 
   ReactDOM.render(
     <I18nextProvider i18n={i18n}>
-      <Provider inject={[noLoginContainer, appContainer]}>
+      <Provider inject={[appContainer]}>
         <LoginForm
           username={username}
           name={name}

+ 46 - 191
src/client/js/services/AppContainer.js

@@ -8,6 +8,11 @@ import InterceptorManager from '@commons/service/interceptor-manager';
 import emojiStrategy from '../util/emojione/emoji_strategy_shrinked.json';
 import GrowiRenderer from '../util/GrowiRenderer';
 
+import {
+  mediaQueryListForDarkMode,
+  applyColorScheme,
+  savePreferenceByUser,
+} from '../util/color-scheme';
 import Apiv1ErrorHandler from '../util/apiv1ErrorHandler';
 
 import {
@@ -31,63 +36,27 @@ export default class AppContainer extends Container {
   constructor() {
     super();
 
-    const { localStorage } = window;
-
     this.state = {
-      editorMode: null,
-      isDeviceSmallerThanMd: null,
-      preferDarkModeByMediaQuery: false,
-      preferDarkModeByUser: localStorage.preferDarkModeByUser === 'true',
-      preferDrawerModeByUser: localStorage.preferDrawerModeByUser === 'true',
-      preferDrawerModeOnEditByUser: // default: true
-        localStorage.preferDrawerModeOnEditByUser == null || localStorage.preferDrawerModeOnEditByUser === 'true',
-      isDrawerMode: null,
-      isDrawerOpened: false,
-
-      isPageCreateModalShown: false,
-
+      // stetes for contents
       recentlyUpdatedPages: [],
     };
 
     const body = document.querySelector('body');
 
-    this.isAdmin = body.dataset.isAdmin === 'true';
     this.csrfToken = body.dataset.csrftoken;
-    this.isPluginEnabled = body.dataset.pluginEnabled === 'true';
-    this.isLoggedin = document.querySelector('body.nologin') == null;
 
     this.config = JSON.parse(document.getElementById('growi-context-hydrate').textContent || '{}');
 
-    const currentUserElem = document.getElementById('growi-current-user');
-    if (currentUserElem != null) {
-      this.currentUser = JSON.parse(currentUserElem.textContent);
-    }
-
     const userAgent = window.navigator.userAgent.toLowerCase();
     this.isMobile = /iphone|ipad|android/.test(userAgent);
 
-    this.isDocSaved = true;
-
-    this.originRenderer = new GrowiRenderer(this);
-
-    this.interceptorManager = new InterceptorManager();
-    this.interceptorManager.addInterceptor(new DetachCodeBlockInterceptor(this), 10); // process as soon as possible
-    this.interceptorManager.addInterceptor(new DrawioInterceptor(this), 20);
-    this.interceptorManager.addInterceptor(new RestoreCodeBlockInterceptor(this), 900); // process as late as possible
-
     const userlang = body.dataset.userlang;
     this.i18n = i18nFactory(userlang);
 
-    if (this.isLoggedin) {
-      // remove old user cache
-      this.removeOldUserCache();
-    }
-
     this.containerInstances = {};
     this.componentInstances = {};
     this.rendererInstances = {};
 
-    this.removeOldUserCache = this.removeOldUserCache.bind(this);
     this.apiGet = this.apiGet.bind(this);
     this.apiPost = this.apiPost.bind(this);
     this.apiDelete = this.apiDelete.bind(this);
@@ -100,26 +69,6 @@ export default class AppContainer extends Container {
       put: this.apiv3Put.bind(this),
       delete: this.apiv3Delete.bind(this),
     };
-
-    this.openPageCreateModal = this.openPageCreateModal.bind(this);
-    this.closePageCreateModal = this.closePageCreateModal.bind(this);
-
-    window.addEventListener('keydown', (event) => {
-      const target = event.target;
-
-      // ignore when target dom is input
-      const inputPattern = /^input|textinput|textarea$/i;
-      if (inputPattern.test(target.tagName) || target.isContentEditable) {
-        return;
-      }
-
-      if (event.key === 'c') {
-        // don't fire when not needed
-        if (!event.ctrlKey && !event.metaKey && !event.altKey && !event.shiftKey) {
-          this.setState({ isPageCreateModalShown: true });
-        }
-      }
-    });
   }
 
   /**
@@ -129,53 +78,59 @@ export default class AppContainer extends Container {
     return 'AppContainer';
   }
 
-  init() {
-    this.initDeviceSize();
-    this.initColorScheme();
-    this.initPlugins();
+  initApp() {
+    this.initMediaQueryForColorScheme();
+
+    this.injectToWindow();
   }
 
-  initDeviceSize() {
-    const mdOrAvobeHandler = async(mql) => {
-      let isDeviceSmallerThanMd;
+  initContents() {
+    const body = document.querySelector('body');
 
-      // sm -> md
-      if (mql.matches) {
-        isDeviceSmallerThanMd = false;
-      }
-      // md -> sm
-      else {
-        isDeviceSmallerThanMd = true;
-      }
+    const currentUserElem = document.getElementById('growi-current-user');
+    if (currentUserElem != null) {
+      this.currentUser = JSON.parse(currentUserElem.textContent);
+    }
 
-      this.setState({ isDeviceSmallerThanMd });
-      this.updateDrawerMode({ ...this.state, isDeviceSmallerThanMd }); // generate newest state object
-    };
+    this.isAdmin = body.dataset.isAdmin === 'true';
+
+    this.isDocSaved = true;
+
+    this.originRenderer = new GrowiRenderer(this);
 
-    this.addBreakpointListener('md', mdOrAvobeHandler, true);
+    this.interceptorManager = new InterceptorManager();
+    this.interceptorManager.addInterceptor(new DetachCodeBlockInterceptor(this), 10); // process as soon as possible
+    this.interceptorManager.addInterceptor(new DrawioInterceptor(this), 20);
+    this.interceptorManager.addInterceptor(new RestoreCodeBlockInterceptor(this), 900); // process as late as possible
+
+    if (this.currentUser != null) {
+      // remove old user cache
+      this.removeOldUserCache();
+    }
+
+    const isPluginEnabled = body.dataset.pluginEnabled === 'true';
+    if (isPluginEnabled) {
+      this.initPlugins();
+    }
+
+    this.injectToWindow();
   }
 
-  async initColorScheme() {
+  async initMediaQueryForColorScheme() {
     const switchStateByMediaQuery = async(mql) => {
       const preferDarkMode = mql.matches;
-      await this.setState({ preferDarkModeByMediaQuery: preferDarkMode });
+      this.setState({ preferDarkModeByMediaQuery: preferDarkMode });
 
-      this.applyColorScheme();
+      applyColorScheme();
     };
 
-    const mqlForDarkMode = window.matchMedia('(prefers-color-scheme: dark)');
     // add event listener
-    mqlForDarkMode.addListener(switchStateByMediaQuery);
-
-    // initialize: check media query
-    switchStateByMediaQuery(mqlForDarkMode);
+    mediaQueryListForDarkMode.addListener(switchStateByMediaQuery);
   }
 
   initPlugins() {
-    if (this.isPluginEnabled) {
-      const growiPlugin = window.growiPlugin;
-      growiPlugin.installAll(this, this.originRenderer);
-    }
+    const growiPlugin = window.growiPlugin;
+    growiPlugin.installAll(this, this.originRenderer);
   }
 
   injectToWindow() {
@@ -334,16 +289,6 @@ export default class AppContainer extends Container {
     this.setState({ recentlyUpdatedPages: data.pages });
   }
 
-  setEditorMode(editorMode) {
-    this.setState({ editorMode });
-    this.updateDrawerMode({ ...this.state, editorMode }); // generate newest state object
-  }
-
-  toggleDrawer() {
-    const { isDrawerOpened } = this.state;
-    this.setState({ isDrawerOpened: !isDrawerOpened });
-  }
-
   launchHandsontableModal(componentKind, beginLineNumber, endLineNumber) {
     let targetComponent;
     switch (componentKind) {
@@ -364,96 +309,14 @@ export default class AppContainer extends Container {
     targetComponent.launchDrawioModal(beginLineNumber, endLineNumber);
   }
 
-  /**
-   * 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;
-  }
-
-  /**
-   * Update drawer related state by specified 'newState' object
-   * @param {object} newState A newest state object
-   *
-   * Specify 'newState' like following code:
-   *
-   *   { ...this.state, overwriteParam: overwriteValue }
-   *
-   * 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 != null ? preferDrawerModeOnEditByUser : preferDrawerModeByUser;
-
-    const isDrawerMode = isDeviceSmallerThanMd || preferDrawerMode;
-    const isDrawerOpened = false; // close Drawer anyway
-
-    this.setState({ isDrawerMode, isDrawerOpened });
-  }
-
   /**
    * Set color scheme preference by user
    * @param {boolean} isDarkMode
    */
   async setColorSchemePreference(isDarkMode) {
-    await this.setState({ preferDarkModeByUser: isDarkMode });
-
-    // store settings to localStorage
-    const { localStorage } = window;
-    if (isDarkMode == null) {
-      delete localStorage.removeItem('preferDarkModeByUser');
-    }
-    else {
-      localStorage.preferDarkModeByUser = isDarkMode;
-    }
-
-    this.applyColorScheme();
-  }
-
-  /**
-   * Apply color scheme as 'dark' attribute of <html></html>
-   */
-  applyColorScheme() {
-    const { preferDarkModeByMediaQuery, preferDarkModeByUser } = this.state;
-
-    let isDarkMode = preferDarkModeByMediaQuery;
-    if (preferDarkModeByUser != null) {
-      isDarkMode = preferDarkModeByUser;
-    }
-
-    // switch to dark mode
-    if (isDarkMode) {
-      document.documentElement.removeAttribute('light');
-      document.documentElement.setAttribute('dark', 'true');
-    }
-    // switch to light mode
-    else {
-      document.documentElement.setAttribute('light', 'true');
-      document.documentElement.removeAttribute('dark');
-    }
+    this.setState({ preferDarkModeByUser: isDarkMode });
+    savePreferenceByUser(isDarkMode);
+    applyColorScheme();
   }
 
   async apiGet(path, params) {
@@ -530,12 +393,4 @@ export default class AppContainer extends Container {
     return this.apiv3Request('delete', path, { params });
   }
 
-  openPageCreateModal() {
-    this.setState({ isPageCreateModalShown: true });
-  }
-
-  closePageCreateModal() {
-    this.setState({ isPageCreateModalShown: false });
-  }
-
 }

+ 151 - 0
src/client/js/services/NavigationContainer.js

@@ -0,0 +1,151 @@
+import { Container } from 'unstated';
+
+/**
+ * Service container related to options for Application
+ * @extends {Container} unstated Container
+ */
+export default class NavigationContainer extends Container {
+
+  constructor(appContainer) {
+    super();
+
+    this.appContainer = appContainer;
+    this.appContainer.registerContainer(this);
+
+    const { localStorage } = window;
+
+    this.state = {
+      editorMode: null,
+
+      isDeviceSmallerThanMd: null,
+      preferDrawerModeByUser: localStorage.preferDrawerModeByUser === 'true',
+      preferDrawerModeOnEditByUser: // default: true
+        localStorage.preferDrawerModeOnEditByUser == null || localStorage.preferDrawerModeOnEditByUser === 'true',
+      isDrawerMode: null,
+      isDrawerOpened: false,
+
+      isPageCreateModalShown: false,
+    };
+
+    this.openPageCreateModal = this.openPageCreateModal.bind(this);
+    this.closePageCreateModal = this.closePageCreateModal.bind(this);
+
+    this.initHotkeys();
+    this.initDeviceSize();
+  }
+
+  /**
+   * Workaround for the mangling in production build to break constructor.name
+   */
+  static getClassName() {
+    return 'NavigationContainer';
+  }
+
+  initHotkeys() {
+    window.addEventListener('keydown', (event) => {
+      const target = event.target;
+
+      // ignore when target dom is input
+      const inputPattern = /^input|textinput|textarea$/i;
+      if (inputPattern.test(target.tagName) || target.isContentEditable) {
+        return;
+      }
+
+      if (event.key === 'c') {
+        // don't fire when not needed
+        if (!event.ctrlKey && !event.metaKey && !event.altKey && !event.shiftKey) {
+          this.setState({ isPageCreateModalShown: true });
+        }
+      }
+    });
+  }
+
+  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);
+  }
+
+  setEditorMode(editorMode) {
+    this.setState({ editorMode });
+    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;
+  }
+
+  /**
+   * Update drawer related state by specified 'newState' object
+   * @param {object} newState A newest state object
+   *
+   * Specify 'newState' like following code:
+   *
+   *   { ...this.state, overwriteParam: overwriteValue }
+   *
+   * 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 != null ? preferDrawerModeOnEditByUser : preferDrawerModeByUser;
+
+    const isDrawerMode = isDeviceSmallerThanMd || preferDrawerMode;
+    const isDrawerOpened = false; // close Drawer anyway
+
+    this.setState({ isDrawerMode, isDrawerOpened });
+  }
+
+  openPageCreateModal() {
+    this.setState({ isPageCreateModalShown: true });
+  }
+
+  closePageCreateModal() {
+    this.setState({ isPageCreateModalShown: false });
+  }
+
+}

+ 0 - 23
src/client/js/services/NoLoginContainer.js

@@ -1,23 +0,0 @@
-import { Container } from 'unstated';
-
-/**
- * Service container related to Nologin (installer, login)
- * @extends {Container} unstated Container
- */
-export default class NoLoginContainer extends Container {
-
-  constructor() {
-    super();
-
-    const body = document.querySelector('body');
-    this.csrfToken = body.dataset.csrftoken;
-  }
-
-  /**
-   * Workaround for the mangling in production build to break constructor.name
-   */
-  static getClassName() {
-    return 'NoLoginContainer';
-  }
-
-}

+ 7 - 3
src/client/js/services/PageContainer.js

@@ -179,6 +179,10 @@ export default class PageContainer extends Container {
     }
   }
 
+  get navigationContainer() {
+    return this.appContainer.getContainer('NavigationContainer');
+  }
+
   setLatestRemotePageData(page, user) {
     this.setState({
       remoteRevisionId: page.revision._id,
@@ -199,7 +203,7 @@ export default class PageContainer extends Container {
    * @param {Array[Tag]} tags Array of Tag
    */
   updateStateAfterSave(page, tags) {
-    const { editorMode } = this.appContainer.state;
+    const { editorMode } = this.navigationContainer.state;
 
     // update state of PageContainer
     const newState = {
@@ -243,7 +247,7 @@ export default class PageContainer extends Container {
    * @return {object} { page: Page, tags: Tag[] }
    */
   async save(markdown, optionsToSave = {}) {
-    const { editorMode } = this.appContainer.state;
+    const { editorMode } = this.navigationContainer.state;
 
     const { pageId, path } = this.state;
     let { revisionId } = this.state;
@@ -274,7 +278,7 @@ export default class PageContainer extends Container {
       throw new Error(msg);
     }
 
-    const { editorMode } = this.appContainer.state;
+    const { editorMode } = this.navigationContainer.state;
     if (editorMode == null) {
       logger.warn('\'saveAndReload\' requires the \'errorMode\' param');
       return;

+ 45 - 0
src/client/js/util/color-scheme.js

@@ -0,0 +1,45 @@
+const mediaQueryListForDarkMode = window.matchMedia('(prefers-color-scheme: dark)');
+
+/**
+ * Apply color scheme as 'dark' attribute of <html></html>
+ */
+function applyColorScheme() {
+  const { preferDarkModeByUser } = localStorage;
+
+  let isDarkMode = mediaQueryListForDarkMode.matches;
+  if (preferDarkModeByUser != null) {
+    isDarkMode = preferDarkModeByUser === 'true';
+  }
+
+  // switch to dark mode
+  if (isDarkMode) {
+    document.documentElement.removeAttribute('light');
+    document.documentElement.setAttribute('dark', 'true');
+  }
+  // switch to light mode
+  else {
+    document.documentElement.setAttribute('light', 'true');
+    document.documentElement.removeAttribute('dark');
+  }
+}
+
+/**
+ * Set color scheme preference by user
+ * @param {boolean} isDarkMode
+ */
+function savePreferenceByUser(isDarkMode) {
+  // store settings to localStorage
+  const { localStorage } = window;
+  if (isDarkMode == null) {
+    delete localStorage.removeItem('preferDarkModeByUser');
+  }
+  else {
+    localStorage.preferDarkModeByUser = isDarkMode;
+  }
+}
+
+export {
+  mediaQueryListForDarkMode,
+  applyColorScheme,
+  savePreferenceByUser,
+};

+ 6 - 0
src/client/styles/scss/style-app.scss

@@ -63,14 +63,20 @@
 /*
  * for Guest User Mode
  */
+// TODO: reactify and replace with `grw-not-available-for-guest`
 .dropdown-toggle.dropdown-toggle-disabled {
   cursor: not-allowed;
 }
 
+// TODO: reactify and replace with `grw-not-available-for-guest`
 .edit-button.edit-button-disabled {
   cursor: not-allowed;
 }
 
+.grw-not-available-for-guest {
+  cursor: not-allowed !important;
+}
+
 /*
  * Helper Classes
  */

+ 2 - 0
src/server/views/installer.html

@@ -19,6 +19,8 @@
 
   {% include './widget/headers/scripts-for-dev.html' %}
 
+  <script src="{{ webpack_asset('js/boot.js') }}"></script>
+
   <script src="{{ webpack_asset('js/vendors.js') }}" defer></script>
   <script src="{{ webpack_asset('js/commons.js') }}" defer></script>
 

+ 2 - 0
src/server/views/layout/layout.html

@@ -25,6 +25,8 @@
 
   {% include '../widget/headers/scripts-for-dev.html' %}
 
+  <script src="{{ webpack_asset('js/boot.js') }}"></script>
+
   <script src="{{ webpack_asset('js/vendors.js') }}" defer></script>
   <script src="{{ webpack_asset('js/commons.js') }}" defer></script>
   {% if getConfig('crowi', 'plugin:isEnabledPlugins') %}