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

Merge branch 'feat/enhanced-link-edit-modal-for-master-merge' into feat/show-link-path-preview

# Conflicts:
#	src/client/js/components/PageEditor/LinkEditModal.jsx
yusuketk 5 лет назад
Родитель
Сommit
ba8860c6fc
47 измененных файлов с 910 добавлено и 493 удалено
  1. 41 0
      .devcontainer/Dockerfile
  2. 38 0
      .devcontainer/devcontainer.json
  3. 88 0
      .devcontainer/docker-compose.yml
  4. 1 0
      .gitignore
  5. 0 21
      .vscode/extensions.json
  6. 21 1
      CHANGES.md
  7. 5 3
      config/env.dev.js
  8. 2 1
      config/logger/config.dev.js
  9. 6 2
      config/webpack.common.js
  10. 2 2
      package.json
  11. 7 1
      src/client/js/admin.jsx
  12. 22 9
      src/client/js/app.jsx
  13. 3 4
      src/client/js/base.jsx
  14. 5 0
      src/client/js/boot.js
  15. 13 12
      src/client/js/components/Drawio.jsx
  16. 9 8
      src/client/js/components/LoginForm.jsx
  17. 5 5
      src/client/js/components/Navbar/NavbarToggler.jsx
  18. 6 6
      src/client/js/components/Navbar/PageCreateButton.jsx
  19. 7 5
      src/client/js/components/Navbar/PersonalDropdown.jsx
  20. 4 2
      src/client/js/components/Navbar/SearchTop.jsx
  21. 30 0
      src/client/js/components/NotAvailableForGuest.jsx
  22. 12 5
      src/client/js/components/Page.jsx
  23. 6 4
      src/client/js/components/Page/TagLabels.jsx
  24. 10 7
      src/client/js/components/PageComment/CommentEditor.jsx
  25. 1 1
      src/client/js/components/PageComments.jsx
  26. 9 6
      src/client/js/components/PageCreateModal.jsx
  27. 0 1
      src/client/js/components/PageDuplicateModal.jsx
  28. 3 2
      src/client/js/components/PageEditor/CodeMirrorEditor.jsx
  29. 43 42
      src/client/js/components/PageEditor/LinkEditModal.jsx
  30. 19 0
      src/client/js/components/PageEditor/MarkdownLinkUtil.js
  31. 3 3
      src/client/js/components/PageEditorByHackmd.jsx
  32. 0 2
      src/client/js/components/PagePathAutoComplete.jsx
  33. 9 6
      src/client/js/components/Sidebar.jsx
  34. 1 1
      src/client/js/components/Sidebar/RecentChanges.jsx
  35. 10 5
      src/client/js/legacy/crowi.js
  36. 2 4
      src/client/js/nologin.jsx
  37. 46 188
      src/client/js/services/AppContainer.js
  38. 151 0
      src/client/js/services/NavigationContainer.js
  39. 0 23
      src/client/js/services/NoLoginContainer.js
  40. 7 3
      src/client/js/services/PageContainer.js
  41. 45 0
      src/client/js/util/color-scheme.js
  42. 2 2
      src/client/styles/scss/_mixins.scss
  43. 6 0
      src/client/styles/scss/style-app.scss
  44. 1 1
      src/lib/util/mongoose-utils.js
  45. 2 0
      src/server/views/installer.html
  46. 2 0
      src/server/views/layout/layout.html
  47. 205 105
      yarn.lock

+ 41 - 0
.devcontainer/Dockerfile

@@ -0,0 +1,41 @@
+#-------------------------------------------------------------------------------------------------------------
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License. See https://go.microsoft.com/fwlink/?linkid=2090316 for license information.
+#-------------------------------------------------------------------------------------------------------------
+
+FROM mcr.microsoft.com/vscode/devcontainers/javascript-node:0-12
+
+# The node image includes a non-root user with sudo access. Use the
+# "remoteUser" property in devcontainer.json to use it. On Linux, update
+# these values to ensure the container user's UID/GID matches your local values.
+# See https://aka.ms/vscode-remote/containers/non-root-user for details.
+ARG USERNAME=node
+ARG USER_UID=1000
+ARG USER_GID=$USER_UID
+
+RUN mkdir -p /workspace/growi/node_modules
+
+# [Optional] Update UID/GID if needed
+RUN if [ "$USER_GID" != "1000" ] || [ "$USER_UID" != "1000" ]; then \
+        groupmod --gid $USER_GID $USERNAME \
+        && usermod --uid $USER_UID --gid $USER_GID $USERNAME; \
+    fi
+RUN chown -R $USER_UID:$USER_GID /home/$USERNAME /workspace;
+
+# *************************************************************
+# * Uncomment this section to use RUN instructions to install *
+# * any needed dependencies after executing "apt-get update". *
+# * See https://docs.docker.com/engine/reference/builder/#run *
+# *************************************************************
+# ENV DEBIAN_FRONTEND=noninteractive
+# RUN apt-get update \
+#    && apt-get -y install --no-install-recommends <your-package-list-here> \
+#    #
+#    # Clean up
+#    && apt-get autoremove -y \
+#    && apt-get clean -y \
+#    && rm -rf /var/lib/apt/lists/*
+# ENV DEBIAN_FRONTEND=dialog
+
+# Uncomment to default to non-root user
+# USER $USER_UID

+ 38 - 0
.devcontainer/devcontainer.json

@@ -0,0 +1,38 @@
+// For format details, see https://aka.ms/vscode-remote/devcontainer.json or this file's README at:
+// https://github.com/microsoft/vscode-dev-containers/tree/v0.117.1/containers/javascript-node-12-mongo
+// If you want to run as a non-root user in the container, see .devcontainer/docker-compose.yml.
+{
+	"name": "GROWI-Dev",
+	"dockerComposeFile": "docker-compose.yml",
+	"service": "node",
+	"workspaceFolder": "/workspace/growi",
+
+	// Set *default* container specific settings.json values on container create.
+	"settings": {
+		"terminal.integrated.shell.linux": "/bin/bash"
+	},
+
+	// Add the IDs of extensions you want installed when the container is created.
+	"extensions": [
+		"dbaeumer.vscode-eslint",
+		"eamodio.gitlens",
+		"msjsdiag.debugger-for-chrome",
+		"firefox-devtools.vscode-firefox-debug",
+		"editorconfig.editorconfig",
+		"esbenp.prettier-vscode",
+		"shinnn.stylelint",
+		"hex-ci.stylelint-plus",
+	],
+
+	// Uncomment the next line if you want start specific services in your Docker Compose config.
+	// "runServices": [],
+
+	// Uncomment the line below if you want to keep your containers running after VS Code shuts down.
+	// "shutdownAction": "none",
+
+	// Use 'postCreateCommand' to run commands after the container is created.
+	// "postCreateCommand": "yarn install",
+
+	// Uncomment to connect as a non-root user. See https://aka.ms/vscode-remote/containers/non-root.
+	"remoteUser": "node"
+}

+ 88 - 0
.devcontainer/docker-compose.yml

@@ -0,0 +1,88 @@
+#-------------------------------------------------------------------------------------------------------------
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License. See https://go.microsoft.com/fwlink/?linkid=2090316 for license information.
+#-------------------------------------------------------------------------------------------------------------
+
+version: '3'
+services:
+  node:
+    # Uncomment the next line to use a non-root user for all processes. You can also
+    # simply use the "remoteUser" property in devcontainer.json if you just want VS Code
+    # and its sub-processes (terminals, tasks, debugging) to execute as the user. On Linux,
+    # you may need to update USER_UID and USER_GID in .devcontainer/Dockerfile to match your
+    # user if not 1000. See https://aka.ms/vscode-remote/containers/non-root for details.
+    user: node
+
+    build:
+      context: .
+      dockerfile: Dockerfile
+
+    ports:
+      - 3000:3000
+      - 3001:3001 # for browser-sync
+
+    volumes:
+      - ..:/workspace/growi:cached
+      - /workspace/growi/node_modules
+      - ../../growi-docker-compose:/workspace/growi-docker-compose:cached
+      - ../../node_modules:/workspace/node_modules:cached
+
+
+    # Overrides default command so things don't shut down after the process ends.
+    command: sleep infinity
+
+    links:
+      - mongo
+      - elasticsearch
+      - hackmd
+
+  mongo:
+    image: mongo:3.6
+    restart: unless-stopped
+    ports:
+      - 27017:27017
+    volumes:
+      - /data/db
+
+  # This container requires '../../growi-docker-compose' repository
+  #   cloned from https://github.com/weseek/growi-docker-compose.git
+  elasticsearch:
+    build:
+      context: ../../growi-docker-compose/elasticsearch
+      dockerfile: ./Dockerfile
+    restart: unless-stopped
+    ports:
+      - 9200:9200
+    environment:
+      - bootstrap.memory_lock=true
+      - "ES_JAVA_OPTS=-Xms256m -Xmx256m"
+    ulimits:
+      memlock:
+        soft: -1
+        hard: -1
+    volumes:
+      - /usr/share/elasticsearch/data
+      - ../../growi-docker-compose/elasticsearch/config/elasticsearch.yml:/usr/share/elasticsearch/config/elasticsearch.yml
+
+  elasticsearch-head:
+    image: tobias74/elasticsearch-head:6
+    restart: unless-stopped
+    ports:
+      - 9100:9100
+
+  # This container requires '../../growi-docker-compose' repository
+  #   cloned from https://github.com/weseek/growi-docker-compose.git
+  hackmd:
+    build:
+      context: ../../growi-docker-compose/hackmd
+    restart: unless-stopped
+    environment:
+      - GROWI_URI=http://localhost:3000
+      # define 'storage' option value
+      # see https://github.com/sequelize/cli/blob/7160d0/src/helpers/config-helper.js#L192
+      - CMD_DB_URL=sqlite://dummyhost/hackmd/sqlite/codimd.db
+      - CMD_CSP_ENABLE=false
+    ports:
+      - 3010:3000
+    volumes:
+      - /files/sqlite

+ 1 - 0
.gitignore

@@ -40,3 +40,4 @@ package-lock.json
 # IDE, dev #
 # IDE, dev #
 .idea
 .idea
 *.orig
 *.orig
+*.code-workspace

+ 0 - 21
.vscode/extensions.json

@@ -1,21 +0,0 @@
-{
-	// See http://go.microsoft.com/fwlink/?LinkId=827846 to learn about workspace recommendations.
-	// Extension identifier format: ${publisher}.${name}. Example: vscode.csharp
-
-	// List of extensions which should be recommended for users of this workspace.
-	"recommendations": [
-    "msjsdiag.debugger-for-chrome",
-    "hbenl.vscode-firefox-debug",
-    "editorconfig.editorconfig",
-    "dbaeumer.vscode-eslint",
-    "eg2.vscode-npm-script",
-    "christian-kohler.npm-intellisense",
-    "esbenp.prettier-vscode",
-    "shinnn.stylelint",
-    "hex-ci.stylelint-plus",
-	],
-	// List of extensions recommended by VS Code that should not be recommended for users of this workspace.
-	"unwantedRecommendations": [
-    "hookyqr.beautify",
-	]
-}

+ 21 - 1
CHANGES.md

@@ -1,8 +1,28 @@
 # CHANGES
 # CHANGES
 
 
+## v4.0.7-RC
+
+* Fix: Styles are not applyed on installer
+
+## v4.0.6
+
+* Fix: Avatar images in Recent Changes are not shown
+* Fix: Full screen modal of Handsontable and Draw.io don't work
+* Fix: Shortcut for creating page respond with modifier key wrongly
+    * Introduced by v4.0.5
+
 ## v4.0.5
 ## v4.0.5
 
 
-* 
+* Improvement: Return pre-defined session id when healthcheck
+* Improvement: Refactor caching for profile image
+* Improvement: Layout for global search help on mobile
+* Improvement: Layout for confidential notation
+* Fix: Shortcut for creating page doesn't work
+* Support: Dev in container
+* Support: Upgrade libs
+    * ldapjs
+    * node-sass
+
 
 
 ## v4.0.4
 ## v4.0.4
 
 

+ 5 - 3
config/env.dev.js

@@ -2,11 +2,13 @@ module.exports = {
   NODE_ENV: 'development',
   NODE_ENV: 'development',
   FILE_UPLOAD: 'mongodb',
   FILE_UPLOAD: 'mongodb',
   // MONGO_GRIDFS_TOTAL_LIMIT: 10485760,   // 10MB
   // MONGO_GRIDFS_TOTAL_LIMIT: 10485760,   // 10MB
-  // MATHJAX: 1,
+  MATHJAX: 1,
   // NO_CDN: true,
   // NO_CDN: true,
-  // REDIS_URI: 'http://localhost:6379',
-  ELASTICSEARCH_URI: 'http://localhost:9200/growi',
+  MONGO_URI: 'mongodb://mongo:27017/growi',
+  // REDIS_URI: 'http://redis:6379',
+  ELASTICSEARCH_URI: 'http://elasticsearch:9200/growi',
   HACKMD_URI: 'http://localhost:3010',
   HACKMD_URI: 'http://localhost:3010',
+  HACKMD_URI_FOR_SERVER: 'http://hackmd:3000',
   // DRAWIO_URI: 'http://localhost:8080/?offline=1&https=0',
   // DRAWIO_URI: 'http://localhost:8080/?offline=1&https=0',
   PLUGIN_NAMES_TOBE_LOADED: [
   PLUGIN_NAMES_TOBE_LOADED: [
     // 'growi-plugin-lsx',
     // 'growi-plugin-lsx',

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

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

+ 6 - 2
config/webpack.common.js

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

+ 2 - 2
package.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "growi",
   "name": "growi",
-  "version": "4.0.5-RC",
+  "version": "4.0.7-RC",
   "description": "Team collaboration software using markdown",
   "description": "Team collaboration software using markdown",
   "tags": [
   "tags": [
     "wiki",
     "wiki",
@@ -211,7 +211,7 @@
     "mini-css-extract-plugin": "^0.9.0",
     "mini-css-extract-plugin": "^0.9.0",
     "morgan": "^1.9.0",
     "morgan": "^1.9.0",
     "node-dev": "^4.0.0",
     "node-dev": "^4.0.0",
-    "node-sass": "^4.12.0",
+    "node-sass": "^4.14.1",
     "normalize-path": "^3.0.0",
     "normalize-path": "^3.0.0",
     "null-loader": "^3.0.0",
     "null-loader": "^3.0.0",
     "on-headers": "^1.0.1",
     "on-headers": "^1.0.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 FullTextSearchManagement from './components/Admin/FullTextSearchManagement';
 import AdminNavigation from './components/Admin/Common/AdminNavigation';
 import AdminNavigation from './components/Admin/Common/AdminNavigation';
 
 
+import NavigationContainer from './services/NavigationContainer';
+
 import AdminHomeContainer from './services/AdminHomeContainer';
 import AdminHomeContainer from './services/AdminHomeContainer';
 import AdminCustomizeContainer from './services/AdminCustomizeContainer';
 import AdminCustomizeContainer from './services/AdminCustomizeContainer';
 import AdminUserGroupDetailContainer from './services/AdminUserGroupDetailContainer';
 import AdminUserGroupDetailContainer from './services/AdminUserGroupDetailContainer';
@@ -41,14 +43,17 @@ import AdminGitHubSecurityContainer from './services/AdminGitHubSecurityContaine
 import AdminTwitterSecurityContainer from './services/AdminTwitterSecurityContainer';
 import AdminTwitterSecurityContainer from './services/AdminTwitterSecurityContainer';
 import AdminNotificationContainer from './services/AdminNotificationContainer';
 import AdminNotificationContainer from './services/AdminNotificationContainer';
 
 
-import { appContainer, componentMappings } from './bootstrap';
+import { appContainer, componentMappings } from './base';
 
 
 const logger = loggerFactory('growi:admin');
 const logger = loggerFactory('growi:admin');
 
 
+appContainer.initContents();
+
 const { i18n } = appContainer;
 const { i18n } = appContainer;
 const websocketContainer = appContainer.getContainer('WebsocketContainer');
 const websocketContainer = appContainer.getContainer('WebsocketContainer');
 
 
 // create unstated container instance
 // create unstated container instance
+const navigationContainer = new NavigationContainer(appContainer);
 const adminAppContainer = new AdminAppContainer(appContainer);
 const adminAppContainer = new AdminAppContainer(appContainer);
 const adminHomeContainer = new AdminHomeContainer(appContainer);
 const adminHomeContainer = new AdminHomeContainer(appContainer);
 const adminCustomizeContainer = new AdminCustomizeContainer(appContainer);
 const adminCustomizeContainer = new AdminCustomizeContainer(appContainer);
@@ -60,6 +65,7 @@ const adminUserGroupDetailContainer = new AdminUserGroupDetailContainer(appConta
 const injectableContainers = [
 const injectableContainers = [
   appContainer,
   appContainer,
   websocketContainer,
   websocketContainer,
+  navigationContainer,
   adminAppContainer,
   adminAppContainer,
   adminHomeContainer,
   adminHomeContainer,
   adminCustomizeContainer,
   adminCustomizeContainer,

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

@@ -32,6 +32,7 @@ import LikerList from './components/User/LikerList';
 import TableOfContents from './components/TableOfContents';
 import TableOfContents from './components/TableOfContents';
 
 
 import PersonalSettings from './components/Me/PersonalSettings';
 import PersonalSettings from './components/Me/PersonalSettings';
+import NavigationContainer from './services/NavigationContainer';
 import PageContainer from './services/PageContainer';
 import PageContainer from './services/PageContainer';
 import CommentContainer from './services/CommentContainer';
 import CommentContainer from './services/CommentContainer';
 import EditorContainer from './services/EditorContainer';
 import EditorContainer from './services/EditorContainer';
@@ -40,21 +41,24 @@ import GrowiSubNavigation from './components/Navbar/GrowiSubNavigation';
 import GrowiSubNavigationForUserPage from './components/Navbar/GrowiSubNavigationForUserPage';
 import GrowiSubNavigationForUserPage from './components/Navbar/GrowiSubNavigationForUserPage';
 import PersonalContainer from './services/PersonalContainer';
 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 { i18n } = appContainer;
 const websocketContainer = appContainer.getContainer('WebsocketContainer');
 const websocketContainer = appContainer.getContainer('WebsocketContainer');
 
 
 // create unstated container instance
 // create unstated container instance
+const navigationContainer = new NavigationContainer(appContainer);
 const pageContainer = new PageContainer(appContainer);
 const pageContainer = new PageContainer(appContainer);
 const commentContainer = new CommentContainer(appContainer);
 const commentContainer = new CommentContainer(appContainer);
 const editorContainer = new EditorContainer(appContainer, defaultEditorOptions, defaultPreviewOptions);
 const editorContainer = new EditorContainer(appContainer, defaultEditorOptions, defaultPreviewOptions);
 const tagContainer = new TagContainer(appContainer);
 const tagContainer = new TagContainer(appContainer);
 const personalContainer = new PersonalContainer(appContainer);
 const personalContainer = new PersonalContainer(appContainer);
 const injectableContainers = [
 const injectableContainers = [
-  appContainer, websocketContainer, pageContainer, commentContainer, editorContainer, tagContainer, personalContainer,
+  appContainer, websocketContainer, navigationContainer, pageContainer, commentContainer, editorContainer, tagContainer, personalContainer,
 ];
 ];
 
 
 logger.info('unstated containers have been initialized');
 logger.info('unstated containers have been initialized');
@@ -70,11 +74,7 @@ Object.assign(componentMappings, {
   // 'revision-history': <PageHistory pageId={pageId} />,
   // 'revision-history': <PageHistory pageId={pageId} />,
   'tags-page': <TagsList crowi={appContainer} />,
   'tags-page': <TagsList crowi={appContainer} />,
 
 
-  'page-editor': <PageEditor />,
-  'page-editor-path-nav': <PagePathNavForEditor />,
-  'page-editor-options-selector': <OptionsSelector crowi={appContainer} />,
   'page-status-alert': <PageStatusAlert />,
   'page-status-alert': <PageStatusAlert />,
-  'save-page-controls': <SavePageControls />,
 
 
   'trash-page-alert': <TrashPageAlert />,
   'trash-page-alert': <TrashPageAlert />,
 
 
@@ -86,10 +86,9 @@ Object.assign(componentMappings, {
 // additional definitions if data exists
 // additional definitions if data exists
 if (pageContainer.state.pageId != null) {
 if (pageContainer.state.pageId != null) {
   Object.assign(componentMappings, {
   Object.assign(componentMappings, {
-    'page-editor-with-hackmd': <PageEditorByHackmd />,
     'page-comments-list': <PageComments />,
     'page-comments-list': <PageComments />,
-    'page-attachment': <PageAttachment />,
     'page-comment-write': <CommentEditorLazyRenderer />,
     'page-comment-write': <CommentEditorLazyRenderer />,
+    'page-attachment': <PageAttachment />,
     'page-management': <PageManagement />,
     'page-management': <PageManagement />,
 
 
     'revision-toc': <TableOfContents />,
     'revision-toc': <TableOfContents />,
@@ -108,6 +107,20 @@ if (pageContainer.state.path != null) {
     'grw-subnav-for-user-page': <GrowiSubNavigationForUserPage />,
     '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) => {
 Object.keys(componentMappings).forEach((key) => {
   const elem = document.getElementById(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 PageCreateButton from './components/Navbar/PageCreateButton';
 import PageCreateModal from './components/PageCreateModal';
 import PageCreateModal from './components/PageCreateModal';
 
 
-const logger = loggerFactory('growi:app');
+const logger = loggerFactory('growi:cli:app');
 
 
 if (!window) {
 if (!window) {
   window = {};
   window = {};
@@ -29,10 +29,9 @@ const appContainer = new AppContainer();
 // eslint-disable-next-line no-unused-vars
 // eslint-disable-next-line no-unused-vars
 const websocketContainer = new WebsocketContainer(appContainer);
 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
  * 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 { withTranslation } from 'react-i18next';
 
 
+import NotAvailableForGuest from './NotAvailableForGuest';
+
 class Drawio extends React.Component {
 class Drawio extends React.Component {
 
 
   constructor(props) {
   constructor(props) {
@@ -23,11 +25,9 @@ class Drawio extends React.Component {
   }
   }
 
 
   onEdit() {
   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() {
   componentDidMount() {
@@ -53,13 +53,13 @@ class Drawio extends React.Component {
   render() {
   render() {
     return (
     return (
       <div className="editable-with-drawio position-relative">
       <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
         <div
           className="drawio"
           className="drawio"
           style={this.style}
           style={this.style}
@@ -77,6 +77,7 @@ class Drawio extends React.Component {
 Drawio.propTypes = {
 Drawio.propTypes = {
   t: PropTypes.func.isRequired, // i18next
   t: PropTypes.func.isRequired, // i18next
   appContainer: PropTypes.object.isRequired,
   appContainer: PropTypes.object.isRequired,
+
   drawioContent: PropTypes.any.isRequired,
   drawioContent: PropTypes.any.isRequired,
   isPreview: PropTypes.bool,
   isPreview: PropTypes.bool,
   rangeLineNumberOfMarkdown: PropTypes.object.isRequired,
   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 { withTranslation } from 'react-i18next';
 
 
+import AppContainer from '../services/AppContainer';
 import { withUnstatedContainers } from './UnstatedUtils';
 import { withUnstatedContainers } from './UnstatedUtils';
-import NoLoginContainer from '../services/NoLoginContainer';
 
 
 class LoginForm extends React.Component {
 class LoginForm extends React.Component {
 
 
@@ -35,12 +35,12 @@ class LoginForm extends React.Component {
 
 
   handleLoginWithExternalAuth(e) {
   handleLoginWithExternalAuth(e) {
     const auth = e.currentTarget.id;
     const auth = e.currentTarget.id;
-    const csrf = this.props.noLoginContainer.csrfToken;
+    const { csrf } = this.props.appContainer;
     window.location.href = `/passport/${auth}?_csrf=${csrf}`;
     window.location.href = `/passport/${auth}?_csrf=${csrf}`;
   }
   }
 
 
   renderLocalOrLdapLoginForm() {
   renderLocalOrLdapLoginForm() {
-    const { t, noLoginContainer, isLdapStrategySetup } = this.props;
+    const { t, appContainer, isLdapStrategySetup } = this.props;
 
 
     return (
     return (
       <form role="form" action="/login" method="post">
       <form role="form" action="/login" method="post">
@@ -70,7 +70,7 @@ class LoginForm extends React.Component {
         </div>
         </div>
 
 
         <div className="input-group my-4">
         <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">
           <button type="submit" id="login" className="btn btn-fill rounded-0 login mx-auto">
             <div className="eff"></div>
             <div className="eff"></div>
             <span className="btn-label">
             <span className="btn-label">
@@ -147,10 +147,10 @@ class LoginForm extends React.Component {
   renderRegisterForm() {
   renderRegisterForm() {
     const {
     const {
       t,
       t,
+      appContainer,
       username,
       username,
       name,
       name,
       email,
       email,
-      noLoginContainer,
       registrationMode,
       registrationMode,
       registrationWhiteList,
       registrationWhiteList,
     } = this.props;
     } = this.props;
@@ -220,7 +220,7 @@ class LoginForm extends React.Component {
           </div>
           </div>
 
 
           <div className="input-group justify-content-center my-4">
           <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">
             <button type="submit" className="btn btn-fill rounded-0" id="register">
               <div className="eff"></div>
               <div className="eff"></div>
               <span className="btn-label">
               <span className="btn-label">
@@ -293,12 +293,13 @@ class LoginForm extends React.Component {
 /**
 /**
  * Wrapper component for using unstated
  * Wrapper component for using unstated
  */
  */
-const LoginFormWrapper = withUnstatedContainers(LoginForm, [NoLoginContainer]);
+const LoginFormWrapper = withUnstatedContainers(LoginForm, [AppContainer]);
 
 
 LoginForm.propTypes = {
 LoginForm.propTypes = {
   // i18next
   // i18next
   t: PropTypes.func.isRequired,
   t: PropTypes.func.isRequired,
-  noLoginContainer: PropTypes.instanceOf(NoLoginContainer).isRequired,
+  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
+
   isRegistering: PropTypes.bool,
   isRegistering: PropTypes.bool,
   username: PropTypes.string,
   username: PropTypes.string,
   name: 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 { withTranslation } from 'react-i18next';
 
 
 import { withUnstatedContainers } from '../UnstatedUtils';
 import { withUnstatedContainers } from '../UnstatedUtils';
-import AppContainer from '../../services/AppContainer';
+import NavigationContainer from '../../services/NavigationContainer';
 
 
 const NavbarToggler = (props) => {
 const NavbarToggler = (props) => {
 
 
-  const { appContainer } = props;
+  const { navigationContainer } = props;
 
 
   const clickHandler = () => {
   const clickHandler = () => {
-    appContainer.toggleDrawer();
+    navigationContainer.toggleDrawer();
   };
   };
 
 
   return (
   return (
@@ -31,12 +31,12 @@ const NavbarToggler = (props) => {
 /**
 /**
  * Wrapper component for using unstated
  * Wrapper component for using unstated
  */
  */
-const NavbarTogglerWrapper = withUnstatedContainers(NavbarToggler, [AppContainer]);
+const NavbarTogglerWrapper = withUnstatedContainers(NavbarToggler, [NavigationContainer]);
 
 
 
 
 NavbarToggler.propTypes = {
 NavbarToggler.propTypes = {
   t: PropTypes.func.isRequired, //  i18next
   t: PropTypes.func.isRequired, //  i18next
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
+  navigationContainer: PropTypes.instanceOf(NavigationContainer).isRequired,
 };
 };
 
 
 export default withTranslation()(NavbarTogglerWrapper);
 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 { withTranslation } from 'react-i18next';
 
 
 import { withUnstatedContainers } from '../UnstatedUtils';
 import { withUnstatedContainers } from '../UnstatedUtils';
-import AppContainer from '../../services/AppContainer';
+import NavigationContainer from '../../services/NavigationContainer';
 
 
 const PageCreateButton = (props) => {
 const PageCreateButton = (props) => {
-  const { t, appContainer, isIcon } = props;
+  const { t, navigationContainer, isIcon } = props;
 
 
   if (isIcon) {
   if (isIcon) {
     return (
     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>
         <i className="icon-pencil"></i>
       </button>
       </button>
     );
     );
   }
   }
 
 
   return (
   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>
       <i className="icon-pencil mr-2"></i>
       <span className="d-none d-lg-block">{ t('New') }</span>
       <span className="d-none d-lg-block">{ t('New') }</span>
     </button>
     </button>
@@ -28,12 +28,12 @@ const PageCreateButton = (props) => {
 /**
 /**
  * Wrapper component for using unstated
  * Wrapper component for using unstated
  */
  */
-const PageCreateButtonWrapper = withUnstatedContainers(PageCreateButton, [AppContainer]);
+const PageCreateButtonWrapper = withUnstatedContainers(PageCreateButton, [NavigationContainer]);
 
 
 
 
 PageCreateButton.propTypes = {
 PageCreateButton.propTypes = {
   t: PropTypes.func.isRequired, //  i18next
   t: PropTypes.func.isRequired, //  i18next
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
+  navigationContainer: PropTypes.instanceOf(NavigationContainer).isRequired,
 
 
   isIcon: PropTypes.bool,
   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 { withUnstatedContainers } from '../UnstatedUtils';
 import AppContainer from '../../services/AppContainer';
 import AppContainer from '../../services/AppContainer';
+import NavigationContainer from '../../services/NavigationContainer';
 
 
 import UserPicture from '../User/UserPicture';
 import UserPicture from '../User/UserPicture';
 
 
 const PersonalDropdown = (props) => {
 const PersonalDropdown = (props) => {
 
 
-  const { t, appContainer } = props;
+  const { t, appContainer, navigationContainer } = props;
   const user = appContainer.currentUser || {};
   const user = appContainer.currentUser || {};
 
 
   const logoutHandler = () => {
   const logoutHandler = () => {
@@ -28,11 +29,11 @@ const PersonalDropdown = (props) => {
   };
   };
 
 
   const preferDrawerModeSwitchModifiedHandler = (bool) => {
   const preferDrawerModeSwitchModifiedHandler = (bool) => {
-    appContainer.setDrawerModePreference(bool);
+    navigationContainer.setDrawerModePreference(bool);
   };
   };
 
 
   const preferDrawerModeOnEditSwitchModifiedHandler = (bool) => {
   const preferDrawerModeOnEditSwitchModifiedHandler = (bool) => {
-    appContainer.setDrawerModePreferenceOnEdit(bool);
+    navigationContainer.setDrawerModePreferenceOnEdit(bool);
   };
   };
 
 
   const followOsCheckboxModifiedHandler = (bool) => {
   const followOsCheckboxModifiedHandler = (bool) => {
@@ -56,7 +57,7 @@ const PersonalDropdown = (props) => {
    */
    */
   const {
   const {
     preferDarkModeByMediaQuery, preferDarkModeByUser, preferDrawerModeByUser, preferDrawerModeOnEditByUser,
     preferDarkModeByMediaQuery, preferDarkModeByUser, preferDrawerModeByUser, preferDrawerModeOnEditByUser,
-  } = appContainer.state;
+  } = navigationContainer.state;
   const isUserPreferenceExists = preferDarkModeByUser != null;
   const isUserPreferenceExists = preferDarkModeByUser != null;
   const isDarkMode = () => {
   const isDarkMode = () => {
     if (isUserPreferenceExists) {
     if (isUserPreferenceExists) {
@@ -205,12 +206,13 @@ const PersonalDropdown = (props) => {
 /**
 /**
  * Wrapper component for using unstated
  * Wrapper component for using unstated
  */
  */
-const PersonalDropdownWrapper = withUnstatedContainers(PersonalDropdown, [AppContainer]);
+const PersonalDropdownWrapper = withUnstatedContainers(PersonalDropdown, [AppContainer, NavigationContainer]);
 
 
 
 
 PersonalDropdown.propTypes = {
 PersonalDropdown.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()(PersonalDropdownWrapper);
 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 { withUnstatedContainers } from '../UnstatedUtils';
 import AppContainer from '../../services/AppContainer';
 import AppContainer from '../../services/AppContainer';
+import NavigationContainer from '../../services/NavigationContainer';
 
 
 import SearchForm from '../SearchForm';
 import SearchForm from '../SearchForm';
 
 
@@ -51,7 +52,7 @@ class SearchTop extends React.Component {
   }
   }
 
 
   Root = ({ children }) => {
   Root = ({ children }) => {
-    const { isDeviceSmallerThanMd: isCollapsed } = this.props.appContainer.state;
+    const { isDeviceSmallerThanMd: isCollapsed } = this.props.navigationContainer.state;
 
 
     return isCollapsed
     return isCollapsed
       ? (
       ? (
@@ -116,11 +117,12 @@ class SearchTop extends React.Component {
 SearchTop.propTypes = {
 SearchTop.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,
 };
 };
 
 
 /**
 /**
  * Wrapper component for using unstated
  * Wrapper component for using unstated
  */
  */
-const SearchTopWrapper = withUnstatedContainers(SearchTop, [AppContainer]);
+const SearchTopWrapper = withUnstatedContainers(SearchTop, [AppContainer, NavigationContainer]);
 
 
 export default withTranslation()(SearchTopWrapper);
 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;

+ 12 - 5
src/client/js/components/Page.jsx

@@ -128,15 +128,22 @@ class Page extends React.Component {
   }
   }
 
 
   render() {
   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 (
     return (
       <div className={isMobile ? 'page-mobile' : ''}>
       <div className={isMobile ? 'page-mobile' : ''}>
         <RevisionRenderer growiRenderer={this.growiRenderer} markdown={markdown} />
         <RevisionRenderer growiRenderer={this.growiRenderer} markdown={markdown} />
-        <LinkEditModal ref={this.LinkEditModal} />
-        <HandsontableModal ref={this.handsontableModal} onSave={this.saveHandlerForHandsontableModal} />
-        <DrawioModal ref={this.drawioModal} onSave={this.saveHandlerForDrawioModal} />
+
+        { isLoggedIn && (
+          <>
+            <LinkEditModal ref={this.LinkEditModal} />
+            <HandsontableModal ref={this.handsontableModal} onSave={this.saveHandlerForHandsontableModal} />
+            <DrawioModal ref={this.drawioModal} onSave={this.saveHandlerForDrawioModal} />
+          </>
+        )}
       </div>
       </div>
     );
     );
   }
   }

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

@@ -6,6 +6,7 @@ import * as toastr from 'toastr';
 
 
 import { withUnstatedContainers } from '../UnstatedUtils';
 import { withUnstatedContainers } from '../UnstatedUtils';
 import AppContainer from '../../services/AppContainer';
 import AppContainer from '../../services/AppContainer';
+import NavigationContainer from '../../services/NavigationContainer';
 import PageContainer from '../../services/PageContainer';
 import PageContainer from '../../services/PageContainer';
 import EditorContainer from '../../services/EditorContainer';
 import EditorContainer from '../../services/EditorContainer';
 
 
@@ -30,7 +31,7 @@ class TagLabels extends React.Component {
    *   2. editorContainer.state.tags if editorMode is not null
    *   2. editorContainer.state.tags if editorMode is not null
    */
    */
   getEditTargetData() {
   getEditTargetData() {
-    const { editorMode } = this.props.appContainer.state;
+    const { editorMode } = this.props.navigationContainer.state;
     return (editorMode == null)
     return (editorMode == null)
       ? this.props.pageContainer.state.tags
       ? this.props.pageContainer.state.tags
       : this.props.editorContainer.state.tags;
       : this.props.editorContainer.state.tags;
@@ -41,8 +42,8 @@ class TagLabels extends React.Component {
   }
   }
 
 
   async tagsUpdatedHandler(tags) {
   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
     // post api request and update tags
     if (editorMode == null) {
     if (editorMode == null) {
@@ -137,12 +138,13 @@ class TagLabels extends React.Component {
 /**
 /**
  * Wrapper component for using unstated
  * Wrapper component for using unstated
  */
  */
-const TagLabelsWrapper = withUnstatedContainers(TagLabels, [AppContainer, PageContainer, EditorContainer]);
+const TagLabelsWrapper = withUnstatedContainers(TagLabels, [AppContainer, NavigationContainer, PageContainer, EditorContainer]);
 
 
 
 
 TagLabels.propTypes = {
 TagLabels.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,
   pageContainer: PropTypes.instanceOf(PageContainer).isRequired,
   pageContainer: PropTypes.instanceOf(PageContainer).isRequired,
   editorContainer: PropTypes.instanceOf(EditorContainer).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 SlackNotification from '../SlackNotification';
 
 
 import CommentPreview from './CommentPreview';
 import CommentPreview from './CommentPreview';
+import NotAvailableForGuest from '../NotAvailableForGuest';
 
 
 /**
 /**
  *
  *
@@ -242,13 +243,15 @@ class CommentEditor extends React.Component {
   renderBeforeReady() {
   renderBeforeReady() {
     return (
     return (
       <div className="text-center">
       <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>
       </div>
     );
     );
   }
   }

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

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

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

@@ -10,13 +10,15 @@ import urljoin from 'url-join';
 
 
 import { userPageRoot } from '@commons/util/path-utils';
 import { userPageRoot } from '@commons/util/path-utils';
 import { pathUtils } from 'growi-commons';
 import { pathUtils } from 'growi-commons';
-import { withUnstatedContainers } from './UnstatedUtils';
 
 
 import AppContainer from '../services/AppContainer';
 import AppContainer from '../services/AppContainer';
+import NavigationContainer from '../services/NavigationContainer';
+import { withUnstatedContainers } from './UnstatedUtils';
+
 import PagePathAutoComplete from './PagePathAutoComplete';
 import PagePathAutoComplete from './PagePathAutoComplete';
 
 
 const PageCreateModal = (props) => {
 const PageCreateModal = (props) => {
-  const { t, appContainer } = props;
+  const { t, appContainer, navigationContainer } = props;
 
 
   const config = appContainer.getConfig();
   const config = appContainer.getConfig();
   const isReachable = config.isSearchServiceReachable;
   const isReachable = config.isSearchServiceReachable;
@@ -160,7 +162,6 @@ const PageCreateModal = (props) => {
               {isReachable
               {isReachable
                 ? (
                 ? (
                   <PagePathAutoComplete
                   <PagePathAutoComplete
-                    crowi={appContainer}
                     initializedPath={pathname}
                     initializedPath={pathname}
                     addTrailingSlash
                     addTrailingSlash
                     onSubmit={ppacSubmitHandler}
                     onSubmit={ppacSubmitHandler}
@@ -240,9 +241,10 @@ const PageCreateModal = (props) => {
       </div>
       </div>
     );
     );
   }
   }
+
   return (
   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') }
         { t('New Page') }
       </ModalHeader>
       </ModalHeader>
       <ModalBody>
       <ModalBody>
@@ -259,12 +261,13 @@ const PageCreateModal = (props) => {
 /**
 /**
  * Wrapper component for using unstated
  * Wrapper component for using unstated
  */
  */
-const ModalControlWrapper = withUnstatedContainers(PageCreateModal, [AppContainer]);
+const ModalControlWrapper = withUnstatedContainers(PageCreateModal, [AppContainer, NavigationContainer]);
 
 
 
 
 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);

+ 0 - 1
src/client/js/components/PageDuplicateModal.jsx

@@ -80,7 +80,6 @@ const PageDuplicateModal = (props) => {
               {isReachable
               {isReachable
               ? (
               ? (
                 <PagePathAutoComplete
                 <PagePathAutoComplete
-                  crowi={appContainer}
                   initializedPath={path}
                   initializedPath={path}
                   onSubmit={ppacSubmitHandler}
                   onSubmit={ppacSubmitHandler}
                   onInputChange={ppacInputChangeHandler}
                   onInputChange={ppacInputChangeHandler}

+ 3 - 2
src/client/js/components/PageEditor/CodeMirrorEditor.jsx

@@ -16,6 +16,7 @@ import pasteHelper from './PasteHelper';
 import EmojiAutoCompleteHelper from './EmojiAutoCompleteHelper';
 import EmojiAutoCompleteHelper from './EmojiAutoCompleteHelper';
 import PreventMarkdownListInterceptor from './PreventMarkdownListInterceptor';
 import PreventMarkdownListInterceptor from './PreventMarkdownListInterceptor';
 import MarkdownTableInterceptor from './MarkdownTableInterceptor';
 import MarkdownTableInterceptor from './MarkdownTableInterceptor';
+import mlu from './MarkdownLinkUtil';
 import mtu from './MarkdownTableUtil';
 import mtu from './MarkdownTableUtil';
 import mdu from './MarkdownDrawioUtil';
 import mdu from './MarkdownDrawioUtil';
 import LinkEditModal from './LinkEditModal';
 import LinkEditModal from './LinkEditModal';
@@ -653,7 +654,7 @@ export default class CodeMirrorEditor extends AbstractEditor {
   }
   }
 
 
   showLinkEditHandler() {
   showLinkEditHandler() {
-    this.linkEditModal.current.show(this.getCodeMirror());
+    this.linkEditModal.current.show(mlu.getSelectedTextInEditor(this.getCodeMirror()));
   }
   }
 
 
   showHandsonTableHandler() {
   showHandsonTableHandler() {
@@ -858,7 +859,7 @@ export default class CodeMirrorEditor extends AbstractEditor {
 
 
         <LinkEditModal
         <LinkEditModal
           ref={this.linkEditModal}
           ref={this.linkEditModal}
-          onSave={(link) => { return this.getCodeMirror().getDoc().replaceSelection(link) }}
+          onSave={(link) => { return mlu.replaceFocusedMarkdownLinkWithEditor(this.getCodeMirror(), link) }}
         />
         />
         <HandsontableModal
         <HandsontableModal
           ref={this.handsontableModal}
           ref={this.handsontableModal}

+ 43 - 42
src/client/js/components/PageEditor/LinkEditModal.jsx

@@ -12,6 +12,8 @@ import {
 
 
 import PageContainer from '../../services/PageContainer';
 import PageContainer from '../../services/PageContainer';
 
 
+import PagePathAutoComplete from '../PagePathAutoComplete';
+
 import { withUnstatedContainers } from '../UnstatedUtils';
 import { withUnstatedContainers } from '../UnstatedUtils';
 
 
 class LinkEditModal extends React.PureComponent {
 class LinkEditModal extends React.PureComponent {
@@ -31,19 +33,19 @@ class LinkEditModal extends React.PureComponent {
     this.show = this.show.bind(this);
     this.show = this.show.bind(this);
     this.hide = this.hide.bind(this);
     this.hide = this.hide.bind(this);
     this.cancel = this.cancel.bind(this);
     this.cancel = this.cancel.bind(this);
+    this.inputChangeHandler = this.inputChangeHandler.bind(this);
+    this.submitHandler = this.submitHandler.bind(this);
     this.toggleIsUseRelativePath = this.toggleIsUseRelativePath.bind(this);
     this.toggleIsUseRelativePath = this.toggleIsUseRelativePath.bind(this);
-    this.handleChangeLinkInput = this.handleChangeLinkInput.bind(this);
     this.handleChangeLabelInput = this.handleChangeLabelInput.bind(this);
     this.handleChangeLabelInput = this.handleChangeLabelInput.bind(this);
     this.handleSelecteLinkerType = this.handleSelecteLinkerType.bind(this);
     this.handleSelecteLinkerType = this.handleSelecteLinkerType.bind(this);
+    this.toggleIsUsePamanentLink = this.toggleIsUsePamanentLink.bind(this);
     this.showLog = this.showLog.bind(this);
     this.showLog = this.showLog.bind(this);
     this.save = this.save.bind(this);
     this.save = this.save.bind(this);
     this.generateLink = this.generateLink.bind(this);
     this.generateLink = this.generateLink.bind(this);
-    this.toggleIsUsePamanentLink = this.toggleIsUsePamanentLink.bind(this);
   }
   }
 
 
-  show(editor) {
-    const selection = editor.getDoc().getSelection();
-    this.setState({ show: true, labelInputValue: selection });
+  show(defaultLabelInputValue = '') {
+    this.setState({ show: true, labelInputValue: defaultLabelInputValue });
   }
   }
 
 
   cancel() {
   cancel() {
@@ -63,6 +65,10 @@ class LinkEditModal extends React.PureComponent {
     this.setState({ isUseRelativePath: !this.state.isUseRelativePath });
     this.setState({ isUseRelativePath: !this.state.isUseRelativePath });
   }
   }
 
 
+  toggleIsUsePamanentLink() {
+    this.setState({ isUsePermanentLink: !this.state.isUsePermanentLink });
+  }
+
   renderPreview() {
   renderPreview() {
     // TODO GW-2658
     // TODO GW-2658
   }
   }
@@ -75,12 +81,8 @@ class LinkEditModal extends React.PureComponent {
     console.log(this.state.linkInputValue);
     console.log(this.state.linkInputValue);
   }
   }
 
 
-  handleChangeLinkInput(linkValue) {
-    this.setState({ linkInputValue: linkValue });
-  }
-
-  handleChangeLabelInput(labelValue) {
-    this.setState({ labelInputValue: labelValue });
+  handleChangeLabelInput(label) {
+    this.setState({ labelInputValue: label });
   }
   }
 
 
   handleSelecteLinkerType(linkerType) {
   handleSelecteLinkerType(linkerType) {
@@ -100,6 +102,16 @@ class LinkEditModal extends React.PureComponent {
     this.hide();
     this.hide();
   }
   }
 
 
+  inputChangeHandler(inputChangeValue) {
+    this.setState({ linkInputValue: inputChangeValue });
+
+  }
+
+  submitHandler(submitValue) {
+    this.setState({ linkInputValue: submitValue });
+  }
+
+
   generateLink() {
   generateLink() {
     const { pageContainer } = this.props;
     const { pageContainer } = this.props;
     const {
     const {
@@ -126,11 +138,6 @@ class LinkEditModal extends React.PureComponent {
     }
     }
   }
   }
 
 
-  toggleIsUsePamanentLink() {
-    // GW-2834
-    this.setState({ isUsePermanentLink: !this.state.isUsePermanentLink });
-  }
-
   render() {
   render() {
     return (
     return (
       <Modal isOpen={this.state.show} toggle={this.cancel} size="lg">
       <Modal isOpen={this.state.show} toggle={this.cancel} size="lg">
@@ -141,25 +148,13 @@ class LinkEditModal extends React.PureComponent {
         <ModalBody className="container">
         <ModalBody className="container">
           <div className="row">
           <div className="row">
             <div className="col-12 col-lg-6">
             <div className="col-12 col-lg-6">
-              <form className="form-group">
-                <div className="form-gorup my-3">
-                  <label htmlFor="linkInput">Link</label>
-                  <div className="input-group">
-                    <input
-                      className="form-control"
-                      id="linkInput"
-                      type="text"
-                      placeholder="URL or page path"
-                      aria-describedby="button-addon"
-                      value={this.state.linkInputValue}
-                      onChange={e => this.handleChangeLinkInput(e.target.value)}
-                    />
-                    <div className="input-group-append">
-                      <button type="button" id="button-addon" className="btn btn-secondary" onClick={this.showLog}>
-                        Preview
-                      </button>
-                    </div>
-                  </div>
+              <div className="form-gorup my-3">
+                <label htmlFor="linkInput">Link</label>
+                <div className="input-group">
+                  <PagePathAutoComplete
+                    onInputChange={this.inputChangeHandler}
+                    onSubmit={this.submitHandler}
+                  />
                 </div>
                 </div>
                 <div className="form-inline">
                 <div className="form-inline">
                   <div className="custom-control custom-checkbox custom-checkbox-info">
                   <div className="custom-control custom-checkbox custom-checkbox-info">
@@ -170,21 +165,16 @@ class LinkEditModal extends React.PureComponent {
                       checked={this.state.isUsePamanentLink}
                       checked={this.state.isUsePamanentLink}
                     />
                     />
                     <label className="custom-control-label" htmlFor="permanentLink" onClick={this.toggleIsUsePamanentLink}>
                     <label className="custom-control-label" htmlFor="permanentLink" onClick={this.toggleIsUsePamanentLink}>
-                      convert into permanent link
+                      Use permanent link
                     </label>
                     </label>
                   </div>
                   </div>
                 </div>
                 </div>
-              </form>
-
-              <div className="d-block d-lg-none">
-                {this.renderPreview}
-                render preview
               </div>
               </div>
 
 
               <div className="card">
               <div className="card">
                 <div className="card-body">
                 <div className="card-body">
                   <form className="form-group">
                   <form className="form-group">
-                    <div className="form-group btn-group w-100" role="group" aria-label="type">
+                    <div className="form-group btn-group d-flex" role="group" aria-label="type">
                       <button
                       <button
                         type="button"
                         type="button"
                         name="mdLink"
                         name="mdLink"
@@ -240,6 +230,17 @@ class LinkEditModal extends React.PureComponent {
                 </div>
                 </div>
               </div>
               </div>
             </div>
             </div>
+
+            <div className="col d-none d-lg-block">
+              {this.renderPreview}
+              render preview
+            </div>
+            <div className="col-12 col-lg-6">
+              <div className="d-block d-lg-none">
+                {this.renderPreview}
+                render preview
+              </div>
+            </div>
           </div>
           </div>
         </ModalBody>
         </ModalBody>
         <ModalFooter>
         <ModalFooter>

+ 19 - 0
src/client/js/components/PageEditor/MarkdownLinkUtil.js

@@ -0,0 +1,19 @@
+/**
+ * Utility for markdown link
+ */
+class MarkdownLinkUtil {
+
+  getSelectedTextInEditor(editor) {
+    return editor.getDoc().getSelection();
+  }
+
+  replaceFocusedMarkdownLinkWithEditor(editor, link) {
+    editor.getDoc().replaceSelection(link);
+  }
+
+}
+
+// singleton pattern
+const instance = new MarkdownLinkUtil();
+Object.freeze(instance);
+export default instance;

+ 3 - 3
src/client/js/components/PageEditorByHackmd.jsx

@@ -286,7 +286,7 @@ class PageEditorByHackmd extends React.Component {
                 disabled={this.state.isInitializing}
                 disabled={this.state.isInitializing}
                 onClick={() => { return this.resumeToEdit() }}
                 onClick={() => { return this.resumeToEdit() }}
               >
               >
-                <span className="btn-label"><i className="icon-control-end"></i></span>
+                <span className="btn-label"><i className="icon-fw icon-control-end"></i></span>
                 <span className="btn-text">{t('hackmd.resume_to_edit')}</span>
                 <span className="btn-text">{t('hackmd.resume_to_edit')}</span>
               </button>
               </button>
             </div>
             </div>
@@ -298,7 +298,7 @@ class PageEditorByHackmd extends React.Component {
               type="button"
               type="button"
               onClick={() => { return this.discardChanges() }}
               onClick={() => { return this.discardChanges() }}
             >
             >
-              <span className="btn-label"><i className="icon-control-start"></i></span>
+              <span className="btn-label"><i className="icon-fw icon-control-start"></i></span>
               <span className="btn-text">{t('hackmd.discard_changes')}</span>
               <span className="btn-text">{t('hackmd.discard_changes')}</span>
             </button>
             </button>
           </div>
           </div>
@@ -322,7 +322,7 @@ class PageEditorByHackmd extends React.Component {
               disabled={isRevisionOutdated || this.state.isInitializing}
               disabled={isRevisionOutdated || this.state.isInitializing}
               onClick={() => { return this.startToEdit() }}
               onClick={() => { return this.startToEdit() }}
             >
             >
-              <span className="btn-label"><i className="icon-paper-plane"></i></span>
+              <span className="btn-label"><i className="icon-fw icon-paper-plane"></i></span>
               {t('hackmd.start_to_edit')}
               {t('hackmd.start_to_edit')}
             </button>
             </button>
           </div>
           </div>

+ 0 - 2
src/client/js/components/PagePathAutoComplete.jsx

@@ -37,7 +37,6 @@ const PagePathAutoComplete = (props) => {
 
 
   return (
   return (
     <SearchTypeahead
     <SearchTypeahead
-      crowi={props.crowi}
       onSubmit={submitHandler}
       onSubmit={submitHandler}
       onChange={inputChangeHandler}
       onChange={inputChangeHandler}
       onInputChange={props.onInputChange}
       onInputChange={props.onInputChange}
@@ -51,7 +50,6 @@ const PagePathAutoComplete = (props) => {
 };
 };
 
 
 PagePathAutoComplete.propTypes = {
 PagePathAutoComplete.propTypes = {
-  crowi:            PropTypes.object.isRequired,
   initializedPath:  PropTypes.string,
   initializedPath:  PropTypes.string,
   addTrailingSlash: PropTypes.bool,
   addTrailingSlash: PropTypes.bool,
 
 

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

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

+ 1 - 1
src/client/js/components/Sidebar/RecentChanges.jsx

@@ -59,7 +59,7 @@ class RecentChanges extends React.Component {
     return (
     return (
       <li className="list-group-item p-2">
       <li className="list-group-item p-2">
         <div className="d-flex w-100">
         <div className="d-flex w-100">
-          <UserPicture user={page.lastUpdatedUser} size="md" />
+          <UserPicture user={page.lastUpdateUser} size="md" />
           <div className="flex-grow-1 ml-2">
           <div className="flex-grow-1 ml-2">
             { !dPagePath.isRoot && <FormerLink /> }
             { !dPagePath.isRoot && <FormerLink /> }
             <h5 className="mb-1">
             <h5 className="mb-1">

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

@@ -240,10 +240,12 @@ $(() => {
 
 
   // tab changing handling
   // tab changing handling
   $('a[href="#revision-body"]').on('show.bs.tab', () => {
   $('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', () => {
   $('a[href="#edit"]').on('show.bs.tab', () => {
-    appContainer.setEditorMode('builtin');
+    const navigationContainer = appContainer.getContainer('NavigationContainer');
+    navigationContainer.setEditorMode('builtin');
     $('body').addClass('on-edit');
     $('body').addClass('on-edit');
     $('body').addClass('builtin-editor');
     $('body').addClass('builtin-editor');
   });
   });
@@ -252,7 +254,8 @@ $(() => {
     $('body').removeClass('builtin-editor');
     $('body').removeClass('builtin-editor');
   });
   });
   $('a[href="#hackmd"]').on('show.bs.tab', () => {
   $('a[href="#hackmd"]').on('show.bs.tab', () => {
-    appContainer.setEditorMode('hackmd');
+    const navigationContainer = appContainer.getContainer('NavigationContainer');
+    navigationContainer.setEditorMode('hackmd');
     $('body').addClass('on-edit');
     $('body').addClass('on-edit');
     $('body').addClass('hackmd');
     $('body').addClass('hackmd');
   });
   });
@@ -317,8 +320,10 @@ window.addEventListener('load', (e) => {
 
 
   // hash on page
   // hash on page
   if (window.location.hash) {
   if (window.location.hash) {
+    const navigationContainer = appContainer.getContainer('NavigationContainer');
+
     if ((window.location.hash === '#edit' || window.location.hash === '#edit-form') && $('.tab-pane#edit').length > 0) {
     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');
       $('a[data-toggle="tab"][href="#edit"]').tab('show');
       $('body').addClass('on-edit');
       $('body').addClass('on-edit');
@@ -328,7 +333,7 @@ window.addEventListener('load', (e) => {
       Crowi.setCaretLineAndFocusToEditor();
       Crowi.setCaretLineAndFocusToEditor();
     }
     }
     else if (window.location.hash === '#hackmd' && $('.tab-pane#hackmd').length > 0) {
     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');
       $('a[data-toggle="tab"][href="#hackmd"]').tab('show');
       $('body').addClass('on-edit');
       $('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 i18nFactory from './util/i18n';
 
 
-import NoLoginContainer from './services/NoLoginContainer';
 import AppContainer from './services/AppContainer';
 import AppContainer from './services/AppContainer';
 
 
 import InstallerForm from './components/InstallerForm';
 import InstallerForm from './components/InstallerForm';
@@ -31,9 +30,8 @@ if (installerFormElem) {
 // render loginForm
 // render loginForm
 const loginFormElem = document.getElementById('login-form');
 const loginFormElem = document.getElementById('login-form');
 if (loginFormElem) {
 if (loginFormElem) {
-  const noLoginContainer = new NoLoginContainer();
   const appContainer = new AppContainer();
   const appContainer = new AppContainer();
-  appContainer.init();
+  appContainer.initApp();
 
 
   const username = loginFormElem.dataset.username;
   const username = loginFormElem.dataset.username;
   const name = loginFormElem.dataset.name;
   const name = loginFormElem.dataset.name;
@@ -62,7 +60,7 @@ if (loginFormElem) {
 
 
   ReactDOM.render(
   ReactDOM.render(
     <I18nextProvider i18n={i18n}>
     <I18nextProvider i18n={i18n}>
-      <Provider inject={[noLoginContainer, appContainer]}>
+      <Provider inject={[appContainer]}>
         <LoginForm
         <LoginForm
           username={username}
           username={username}
           name={name}
           name={name}

+ 46 - 188
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 emojiStrategy from '../util/emojione/emoji_strategy_shrinked.json';
 import GrowiRenderer from '../util/GrowiRenderer';
 import GrowiRenderer from '../util/GrowiRenderer';
 
 
+import {
+  mediaQueryListForDarkMode,
+  applyColorScheme,
+  savePreferenceByUser,
+} from '../util/color-scheme';
 import Apiv1ErrorHandler from '../util/apiv1ErrorHandler';
 import Apiv1ErrorHandler from '../util/apiv1ErrorHandler';
 
 
 import {
 import {
@@ -31,63 +36,27 @@ export default class AppContainer extends Container {
   constructor() {
   constructor() {
     super();
     super();
 
 
-    const { localStorage } = window;
-
     this.state = {
     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: [],
       recentlyUpdatedPages: [],
     };
     };
 
 
     const body = document.querySelector('body');
     const body = document.querySelector('body');
 
 
-    this.isAdmin = body.dataset.isAdmin === 'true';
     this.csrfToken = body.dataset.csrftoken;
     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 || '{}');
     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();
     const userAgent = window.navigator.userAgent.toLowerCase();
     this.isMobile = /iphone|ipad|android/.test(userAgent);
     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;
     const userlang = body.dataset.userlang;
     this.i18n = i18nFactory(userlang);
     this.i18n = i18nFactory(userlang);
 
 
-    if (this.isLoggedin) {
-      // remove old user cache
-      this.removeOldUserCache();
-    }
-
     this.containerInstances = {};
     this.containerInstances = {};
     this.componentInstances = {};
     this.componentInstances = {};
     this.rendererInstances = {};
     this.rendererInstances = {};
 
 
-    this.removeOldUserCache = this.removeOldUserCache.bind(this);
     this.apiGet = this.apiGet.bind(this);
     this.apiGet = this.apiGet.bind(this);
     this.apiPost = this.apiPost.bind(this);
     this.apiPost = this.apiPost.bind(this);
     this.apiDelete = this.apiDelete.bind(this);
     this.apiDelete = this.apiDelete.bind(this);
@@ -100,23 +69,6 @@ export default class AppContainer extends Container {
       put: this.apiv3Put.bind(this),
       put: this.apiv3Put.bind(this),
       delete: this.apiv3Delete.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') {
-        this.setState({ isPageCreateModalShown: true });
-      }
-    });
   }
   }
 
 
   /**
   /**
@@ -126,53 +78,59 @@ export default class AppContainer extends Container {
     return 'AppContainer';
     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 switchStateByMediaQuery = async(mql) => {
       const preferDarkMode = mql.matches;
       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
     // add event listener
-    mqlForDarkMode.addListener(switchStateByMediaQuery);
-
-    // initialize: check media query
-    switchStateByMediaQuery(mqlForDarkMode);
+    mediaQueryListForDarkMode.addListener(switchStateByMediaQuery);
   }
   }
 
 
   initPlugins() {
   initPlugins() {
-    if (this.isPluginEnabled) {
-      const growiPlugin = window.growiPlugin;
-      growiPlugin.installAll(this, this.originRenderer);
-    }
+    const growiPlugin = window.growiPlugin;
+    growiPlugin.installAll(this, this.originRenderer);
   }
   }
 
 
   injectToWindow() {
   injectToWindow() {
@@ -331,16 +289,6 @@ export default class AppContainer extends Container {
     this.setState({ recentlyUpdatedPages: data.pages });
     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) {
   launchHandsontableModal(componentKind, beginLineNumber, endLineNumber) {
     let targetComponent;
     let targetComponent;
     switch (componentKind) {
     switch (componentKind) {
@@ -361,96 +309,14 @@ export default class AppContainer extends Container {
     targetComponent.launchDrawioModal(beginLineNumber, endLineNumber);
     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
    * Set color scheme preference by user
    * @param {boolean} isDarkMode
    * @param {boolean} isDarkMode
    */
    */
   async setColorSchemePreference(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) {
   async apiGet(path, params) {
@@ -527,12 +393,4 @@ export default class AppContainer extends Container {
     return this.apiv3Request('delete', path, { params });
     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) {
   setLatestRemotePageData(page, user) {
     this.setState({
     this.setState({
       remoteRevisionId: page.revision._id,
       remoteRevisionId: page.revision._id,
@@ -199,7 +203,7 @@ export default class PageContainer extends Container {
    * @param {Array[Tag]} tags Array of Tag
    * @param {Array[Tag]} tags Array of Tag
    */
    */
   updateStateAfterSave(page, tags) {
   updateStateAfterSave(page, tags) {
-    const { editorMode } = this.appContainer.state;
+    const { editorMode } = this.navigationContainer.state;
 
 
     // update state of PageContainer
     // update state of PageContainer
     const newState = {
     const newState = {
@@ -243,7 +247,7 @@ export default class PageContainer extends Container {
    * @return {object} { page: Page, tags: Tag[] }
    * @return {object} { page: Page, tags: Tag[] }
    */
    */
   async save(markdown, optionsToSave = {}) {
   async save(markdown, optionsToSave = {}) {
-    const { editorMode } = this.appContainer.state;
+    const { editorMode } = this.navigationContainer.state;
 
 
     const { pageId, path } = this.state;
     const { pageId, path } = this.state;
     let { revisionId } = this.state;
     let { revisionId } = this.state;
@@ -274,7 +278,7 @@ export default class PageContainer extends Container {
       throw new Error(msg);
       throw new Error(msg);
     }
     }
 
 
-    const { editorMode } = this.appContainer.state;
+    const { editorMode } = this.navigationContainer.state;
     if (editorMode == null) {
     if (editorMode == null) {
       logger.warn('\'saveAndReload\' requires the \'errorMode\' param');
       logger.warn('\'saveAndReload\' requires the \'errorMode\' param');
       return;
       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,
+};

+ 2 - 2
src/client/styles/scss/_mixins.scss

@@ -83,9 +83,9 @@
 @mixin expand-modal-fullscreen($hasModalHeader: true, $hasModalFooter: true) {
 @mixin expand-modal-fullscreen($hasModalHeader: true, $hasModalFooter: true) {
   // full-screen modal
   // full-screen modal
   width: auto;
   width: auto;
-  max-width: unset;
+  max-width: unset !important;
   height: calc(100vh - 30px);
   height: calc(100vh - 30px);
-  margin: 15px;
+  margin: 15px !important;
 
 
   .modal-content {
   .modal-content {
     height: calc(100vh - 30px);
     height: calc(100vh - 30px);

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

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

+ 1 - 1
src/lib/util/mongoose-utils.js

@@ -7,7 +7,7 @@ const getMongoUri = () => {
     || env.MONGODB_URI // MONGOLAB changes their env name
     || env.MONGODB_URI // MONGOLAB changes their env name
     || env.MONGOHQ_URL
     || env.MONGOHQ_URL
     || env.MONGO_URI
     || env.MONGO_URI
-    || ((env.NODE_ENV === 'test') ? 'mongodb://localhost/growi_test' : 'mongodb://localhost/growi');
+    || ((env.NODE_ENV === 'test') ? 'mongodb://mongo/growi_test' : 'mongodb://mongo/growi');
 };
 };
 
 
 const getModelSafely = (modelName) => {
 const getModelSafely = (modelName) => {

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

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

Разница между файлами не показана из-за своего большого размера
+ 205 - 105
yarn.lock


Некоторые файлы не были показаны из-за большого количества измененных файлов