Преглед изворни кода

Merge branch 'master' into feat/GW-3658-can-use-GCP-from-db-vars

# Conflicts:
#	resource/locales/zh_CN/translation.json
#	src/client/js/components/BookmarkButton.jsx
#	src/client/js/components/Navbar/GrowiSubNavigation.jsx
#	src/client/styles/scss/_subnav.scss
#	src/server/routes/apiv3/bookmarks.js
yusuketk пре 5 година
родитељ
комит
3faa4896b9
79 измењених фајлова са 912 додато и 610 уклоњено
  1. 11 1
      resource/locales/en_US/translation.json
  2. 11 1
      resource/locales/ja_JP/translation.json
  3. 13 3
      resource/locales/zh_CN/translation.json
  4. 11 12
      src/client/js/app.jsx
  5. 1 0
      src/client/js/components/Admin/ManageExternalAccount.jsx
  6. 1 1
      src/client/js/components/Admin/Security/ShareLinkSetting.jsx
  7. 1 0
      src/client/js/components/Admin/UserGroup/UserGroupPage.jsx
  8. 1 0
      src/client/js/components/Admin/UserGroupDetail/UserGroupPageList.jsx
  9. 1 0
      src/client/js/components/Admin/UserManagement.jsx
  10. 16 32
      src/client/js/components/BookmarkButton.jsx
  11. 83 0
      src/client/js/components/CustomNavigation.jsx
  12. 0 2
      src/client/js/components/Hotkeys/Subscribers/EditPage.jsx
  13. 12 14
      src/client/js/components/LikeButton.jsx
  14. 2 1
      src/client/js/components/MyDraftList/MyDraftList.jsx
  15. 40 28
      src/client/js/components/Navbar/GrowiSubNavigation.jsx
  16. 49 15
      src/client/js/components/Navbar/ThreeStrandedButton.jsx
  17. 39 0
      src/client/js/components/NotFoundPage.jsx
  18. 2 2
      src/client/js/components/Page.jsx
  19. 43 0
      src/client/js/components/Page/DisplaySwitcher.jsx
  20. 40 0
      src/client/js/components/Page/NotFoundAlert.jsx
  21. 4 4
      src/client/js/components/Page/PageManagement.jsx
  22. 1 9
      src/client/js/components/Page/RevisionPathControls.jsx
  23. 7 18
      src/client/js/components/Page/TagLabels.jsx
  24. 0 48
      src/client/js/components/PageEditor/PagePathNavForEditor.jsx
  25. 20 2
      src/client/js/components/PageEditorByHackmd.jsx
  26. 16 4
      src/client/js/components/PageList.jsx
  27. 1 1
      src/client/js/components/PageTimeline.jsx
  28. 3 2
      src/client/js/components/PaginationWrapper.jsx
  29. 2 1
      src/client/js/components/RecentCreated/RecentCreated.jsx
  30. 1 0
      src/client/js/components/TagsList.jsx
  31. 1 0
      src/client/js/components/TopOfTableContents.jsx
  32. 32 0
      src/client/js/components/TrashPageList.jsx
  33. 2 2
      src/client/js/components/User/SeenUserList.jsx
  34. 12 97
      src/client/js/legacy/crowi.js
  35. 24 2
      src/client/js/services/NavigationContainer.js
  36. 38 14
      src/client/js/services/PageContainer.js
  37. 2 0
      src/client/styles/scss/_layout.scss
  38. 74 7
      src/client/styles/scss/_mixins.scss
  39. 12 0
      src/client/styles/scss/_navbar.scss
  40. 46 41
      src/client/styles/scss/_on-edit.scss
  41. 1 0
      src/client/styles/scss/_override-bootstrap-variables.scss
  42. 17 4
      src/client/styles/scss/_subnav.scss
  43. 0 2
      src/client/styles/scss/_toc.scss
  44. 5 0
      src/client/styles/scss/_variables.scss
  45. 6 0
      src/client/styles/scss/atoms/_buttons.scss
  46. 19 1
      src/client/styles/scss/theme/_apply-colors.scss
  47. 4 0
      src/client/styles/scss/theme/_reboot-bootstrap-border-colors.scss
  48. 7 0
      src/client/styles/scss/theme/antarctic.scss
  49. 7 0
      src/client/styles/scss/theme/christmas.scss
  50. 14 0
      src/client/styles/scss/theme/default.scss
  51. 7 0
      src/client/styles/scss/theme/future.scss
  52. 7 0
      src/client/styles/scss/theme/halloween.scss
  53. 7 0
      src/client/styles/scss/theme/island.scss
  54. 6 0
      src/client/styles/scss/theme/kibela.scss
  55. 13 0
      src/client/styles/scss/theme/mono-blue.scss
  56. 7 0
      src/client/styles/scss/theme/nature.scss
  57. 7 0
      src/client/styles/scss/theme/spring.scss
  58. 7 0
      src/client/styles/scss/theme/wood.scss
  59. 1 1
      src/lib/components/PagePathHierarchicalLink.jsx
  60. 0 8
      src/server/form/admin/importerEsa.js
  61. 0 8
      src/server/form/admin/importerQiita.js
  62. 0 15
      src/server/form/comment.js
  63. 0 2
      src/server/form/index.js
  64. 0 15
      src/server/form/revision.js
  65. 29 3
      src/server/routes/apiv3/bookmarks.js
  66. 21 1
      src/server/routes/apiv3/page.js
  67. 15 3
      src/server/routes/apiv3/pages.js
  68. 0 14
      src/server/views/_form.html
  69. 2 2
      src/server/views/layout-growi/base/layout.html
  70. 3 0
      src/server/views/layout-growi/page_list.html
  71. 7 0
      src/server/views/layout-growi/widget/liker-and-seenusers.html
  72. 2 2
      src/server/views/widget/forbidden_content.html
  73. 2 22
      src/server/views/widget/not_creatable_content.html
  74. 2 32
      src/server/views/widget/not_found_content.html
  75. 0 20
      src/server/views/widget/not_found_tabs.html
  76. 3 0
      src/server/views/widget/page_alerts.html
  77. 8 34
      src/server/views/widget/page_content.html
  78. 1 1
      src/server/views/widget/page_list.html
  79. 0 56
      src/server/views/widget/page_tabs.html

+ 11 - 1
resource/locales/en_US/translation.json

@@ -155,6 +155,13 @@
     "required": "%s is required",
     "required": "%s is required",
     "invalid_syntax": "The syntax of %s is invalid."
     "invalid_syntax": "The syntax of %s is invalid."
   },
   },
+  "not_found_page": {
+    "Create Page": "Create Page",
+    "page_not_exist_alert": "This page does not exist. Please create a new page."
+  },
+  "custom_navigation": {
+    "no_page_list": "There are no pages under <a href='{{path}}'><strong>{{ path }}</strong></a>."
+  },
   "installer": {
   "installer": {
     "setup": "Setup",
     "setup": "Setup",
     "create_initial_account": "Create an initial account",
     "create_initial_account": "Create an initial account",
@@ -433,6 +440,7 @@
   },
   },
   "hackmd": {
   "hackmd": {
     "not_set_up": "HackMD is not set up.",
     "not_set_up": "HackMD is not set up.",
+    "used_for_not_found": "Can not use HackMD to a page that does not exist.",
     "start_to_edit": "Start to edit with HackMD",
     "start_to_edit": "Start to edit with HackMD",
     "clone_page_content": "Click to clone page content and start to edit.",
     "clone_page_content": "Click to clone page content and start to edit.",
     "unsaved_draft": "HackMD has unsaved draft.",
     "unsaved_draft": "HackMD has unsaved draft.",
@@ -446,7 +454,9 @@
     "check_configuration": "Check your configuration following <a href='https://docs.growi.org/guide/admin-cookbook/integrate-with-hackmd.html'>the manual</a>.",
     "check_configuration": "Check your configuration following <a href='https://docs.growi.org/guide/admin-cookbook/integrate-with-hackmd.html'>the manual</a>.",
     "not_initialized": "HackmdEditor component has not initialized",
     "not_initialized": "HackmdEditor component has not initialized",
     "someone_editing": "Someone editing this page on HackMD",
     "someone_editing": "Someone editing this page on HackMD",
-    "this_page_has_draft": "This page has a draft on HackMD"
+    "this_page_has_draft": "This page has a draft on HackMD",
+    "need_to_associate_with_growi_to_use_hackmd_refer_to_this": "To use HackMD for simultaneous multi-person editing, need to associate HackMD with GROWI.Please refer to <a href='https://docs.growi.org/en/admin-guide/admin-cookbook/integrate-with-hackmd.html'>here</a>.",
+    "need_to_make_page": "To use HackMD, please make a new page from the <a href='#edit'>built-in editor.</a>"
   },
   },
   "slack_notification": {
   "slack_notification": {
     "popover_title": "Slack Notification",
     "popover_title": "Slack Notification",

+ 11 - 1
resource/locales/ja_JP/translation.json

@@ -158,6 +158,13 @@
     "required": "%sに値を入力してください",
     "required": "%sに値を入力してください",
     "invalid_syntax": "%sの構文が不正です"
     "invalid_syntax": "%sの構文が不正です"
   },
   },
+  "not_found_page": {
+    "Create Page": "ページを作成する",
+    "page_not_exist_alert": "このページは存在しません。新たに作成する必要があります。"
+  },
+  "custom_navigation": {
+    "no_page_list": "<a href='{{path}}'><strong>{{ path }}</strong></a>の配下にはページが存在しません。"
+  },
   "installer": {
   "installer": {
     "setup": "セットアップ",
     "setup": "セットアップ",
     "create_initial_account": "最初のアカウントの作成",
     "create_initial_account": "最初のアカウントの作成",
@@ -435,6 +442,7 @@
   },
   },
   "hackmd":{
   "hackmd":{
     "not_set_up": "HackMD はセットアップされていません",
     "not_set_up": "HackMD はセットアップされていません",
+    "used_for_not_found": "HackMD は新しいページの作成には利用できません",
     "start_to_edit": "HackMD を開始する",
     "start_to_edit": "HackMD を開始する",
     "clone_page_content": "ページを複製して編集を開始します",
     "clone_page_content": "ページを複製して編集を開始します",
     "unsaved_draft": "HackMD のドラフトが保存されていません",
     "unsaved_draft": "HackMD のドラフトが保存されていません",
@@ -448,7 +456,9 @@
     "check_configuration": "<a href='https://docs.growi.org/ja/admin-guide/admin-cookbook/integrate-with-hackmd.html'>こちらのマニュアル</a>から設定を確認してください",
     "check_configuration": "<a href='https://docs.growi.org/ja/admin-guide/admin-cookbook/integrate-with-hackmd.html'>こちらのマニュアル</a>から設定を確認してください",
     "not_initialized": "HackMD コンポーネントは初期化されていません",
     "not_initialized": "HackMD コンポーネントは初期化されていません",
     "someone_editing": "このページは、HackMD で編集されています。",
     "someone_editing": "このページは、HackMD で編集されています。",
-    "this_page_has_draft": "このページは、HackMD のドラフトがあります。"
+    "this_page_has_draft": "このページは、HackMD のドラフトがあります。",
+    "need_to_associate_with_growi_to_use_hackmd_refer_to_this": "HackMD を利用して同時多人数編集を行うには、HackMD と GROWI を連携する必要があります。<a href='https://docs.growi.org/ja/admin-guide/admin-cookbook/integrate-with-hackmd.html'>こちら</a>を参照してください。",
+    "need_to_make_page": "HackMD を利用するためには、<a href='#edit'>ビルトインエディタ</a>で新しいページを作成してください。"
   },
   },
   "slack_notification": {
   "slack_notification": {
     "popover_title": "Slack 通知",
     "popover_title": "Slack 通知",

+ 13 - 3
resource/locales/zh_CN/translation.json

@@ -154,7 +154,14 @@
 		"error_message": "有些值不正确",
 		"error_message": "有些值不正确",
 		"required": "%s 是必需的",
 		"required": "%s 是必需的",
 		"invalid_syntax": "%s的语法无效。"
 		"invalid_syntax": "%s的语法无效。"
-	},
+  },
+  "not_found_page": {
+    "Create Page": "创建页面",
+    "page_not_exist_alert": "该页面不存在,请创建一个新页面"
+  },
+  "custom_navigation": {
+    "no_page_list": "There are no pages under <a href='{{path}}'><strong>{{ path }}</strong></a>."
+  },
 	"installer": {
 	"installer": {
 		"setup": "安装",
 		"setup": "安装",
 		"create_initial_account": "创建初始用户",
 		"create_initial_account": "创建初始用户",
@@ -408,7 +415,8 @@
 		"open_sandbox": "开放式沙箱"
 		"open_sandbox": "开放式沙箱"
 	},
 	},
 	"hackmd": {
 	"hackmd": {
-		"not_set_up": "HackMD is not set up.",
+    "not_set_up": "HackMD is not set up.",
+    "used_for_not_found": "Can not use HackMD to a page that does not exist.",
 		"start_to_edit": "Start to edit with HackMD",
 		"start_to_edit": "Start to edit with HackMD",
 		"clone_page_content": "Click to clone page content and start to edit.",
 		"clone_page_content": "Click to clone page content and start to edit.",
 		"unsaved_draft": "HackMD has unsaved draft.",
 		"unsaved_draft": "HackMD has unsaved draft.",
@@ -422,7 +430,9 @@
 		"check_configuration": "Check your configuration following <a href='https://docs.growi.org/guide/admin-cookbook/integrate-with-hackmd.html'>the manual</a>.",
 		"check_configuration": "Check your configuration following <a href='https://docs.growi.org/guide/admin-cookbook/integrate-with-hackmd.html'>the manual</a>.",
 		"not_initialized": "HackmdEditor component has not initialized",
 		"not_initialized": "HackmdEditor component has not initialized",
 		"someone_editing": "Someone editing this page on HackMD",
 		"someone_editing": "Someone editing this page on HackMD",
-		"this_page_has_draft": "This page has a draft on HackMD"
+    "this_page_has_draft": "This page has a draft on HackMD",
+    "need_to_associate_with_growi_to_use_hackmd_refer_to_this": "若要使用HackMD的多人同时编辑功能,请先关联HackMD和GROWI。详情请参考<a href='https://docs.growi.org/cn/admin-guide/admin-cookbook/integrate-with-hackmd.html'>这里</a>。",
+    "need_to_make_page": "To use HackMD, please make a new page from the <a href='#edit'>built-in editor.</a>"
   },
   },
   "slack_notification": {
   "slack_notification": {
     "popover_title": "Slack Notification",
     "popover_title": "Slack Notification",

+ 11 - 12
src/client/js/app.jsx

@@ -8,17 +8,17 @@ import loggerFactory from '@alias/logger';
 import ErrorBoundary from './components/ErrorBoudary';
 import ErrorBoundary from './components/ErrorBoudary';
 import SearchPage from './components/SearchPage';
 import SearchPage from './components/SearchPage';
 import TagsList from './components/TagsList';
 import TagsList from './components/TagsList';
-import PageEditor from './components/PageEditor';
-import PagePathNavForEditor from './components/PageEditor/PagePathNavForEditor';
-import EditorNavbarBottom from './components/PageEditor/EditorNavbarBottom';
+import DisplaySwitcher from './components/Page/DisplaySwitcher';
 import { defaultEditorOptions, defaultPreviewOptions } from './components/PageEditor/OptionsSelector';
 import { defaultEditorOptions, defaultPreviewOptions } from './components/PageEditor/OptionsSelector';
-import PageEditorByHackmd from './components/PageEditorByHackmd';
 import Page from './components/Page';
 import Page from './components/Page';
 import PageComments from './components/PageComments';
 import PageComments from './components/PageComments';
 import PageTimeline from './components/PageTimeline';
 import PageTimeline from './components/PageTimeline';
 import CommentEditorLazyRenderer from './components/PageComment/CommentEditorLazyRenderer';
 import CommentEditorLazyRenderer from './components/PageComment/CommentEditorLazyRenderer';
 import PageManagement from './components/Page/PageManagement';
 import PageManagement from './components/Page/PageManagement';
+import TrashPageList from './components/TrashPageList';
 import TrashPageAlert from './components/Page/TrashPageAlert';
 import TrashPageAlert from './components/Page/TrashPageAlert';
+import NotFoundPage from './components/NotFoundPage';
+import NotFoundAlert from './components/Page/NotFoundAlert';
 import PageStatusAlert from './components/PageStatusAlert';
 import PageStatusAlert from './components/PageStatusAlert';
 import RecentCreated from './components/RecentCreated/RecentCreated';
 import RecentCreated from './components/RecentCreated/RecentCreated';
 import MyBookmarkList from './components/MyBookmarkList/MyBookmarkList';
 import MyBookmarkList from './components/MyBookmarkList/MyBookmarkList';
@@ -76,6 +76,12 @@ Object.assign(componentMappings, {
 
 
   'trash-page-alert': <TrashPageAlert />,
   'trash-page-alert': <TrashPageAlert />,
 
 
+  'trash-page-list': <TrashPageList />,
+
+  'not-found-page': <NotFoundPage />,
+
+  'not-found-alert': <NotFoundAlert onPageCreateClicked={navigationContainer.setEditorMode} />,
+
   'page-timeline': <PageTimeline />,
   'page-timeline': <PageTimeline />,
 
 
   'personal-setting': <PersonalSettings crowi={personalContainer} />,
   'personal-setting': <PersonalSettings crowi={personalContainer} />,
@@ -114,15 +120,8 @@ if (pageContainer.state.path != null) {
 // additional definitions if user is logged in
 // additional definitions if user is logged in
 if (appContainer.currentUser != null) {
 if (appContainer.currentUser != null) {
   Object.assign(componentMappings, {
   Object.assign(componentMappings, {
-    'page-editor': <PageEditor />,
-    'page-editor-path-nav': <PagePathNavForEditor />,
-    'page-editor-navbar-bottom-container': <EditorNavbarBottom />,
+    'display-switcher': <DisplaySwitcher />,
   });
   });
-  if (pageContainer.state.pageId != null) {
-    Object.assign(componentMappings, {
-      'page-editor-with-hackmd': <PageEditorByHackmd />,
-    });
-  }
 }
 }
 
 
 Object.keys(componentMappings).forEach((key) => {
 Object.keys(componentMappings).forEach((key) => {

+ 1 - 0
src/client/js/components/Admin/ManageExternalAccount.jsx

@@ -43,6 +43,7 @@ class ManageExternalAccount extends React.Component {
         totalItemsCount={adminExternalAccountsContainer.state.totalAccounts}
         totalItemsCount={adminExternalAccountsContainer.state.totalAccounts}
         pagingLimit={adminExternalAccountsContainer.state.pagingLimit}
         pagingLimit={adminExternalAccountsContainer.state.pagingLimit}
         align="right"
         align="right"
+        size="sm"
       />
       />
 
 
     );
     );

+ 1 - 1
src/client/js/components/Admin/Security/ShareLinkSetting.jsx

@@ -25,6 +25,7 @@ const Pager = (props) => {
       totalItemsCount={props.totalLinks}
       totalItemsCount={props.totalLinks}
       pagingLimit={props.limit}
       pagingLimit={props.limit}
       align="right"
       align="right"
+      size="sm"
     />
     />
   );
   );
 };
 };
@@ -111,7 +112,6 @@ class ShareLinkSetting extends React.Component {
       shareLinks, shareLinksActivePage, totalshareLinks, shareLinksPagingLimit,
       shareLinks, shareLinksActivePage, totalshareLinks, shareLinksPagingLimit,
     } = adminGeneralSecurityContainer.state;
     } = adminGeneralSecurityContainer.state;
 
 
-
     return (
     return (
       <Fragment>
       <Fragment>
         <div className="mb-3">
         <div className="mb-3">

+ 1 - 0
src/client/js/components/Admin/UserGroup/UserGroupPage.jsx

@@ -157,6 +157,7 @@ class UserGroupPage extends React.Component {
           changePage={this.handlePage}
           changePage={this.handlePage}
           totalItemsCount={this.state.totalUserGroups}
           totalItemsCount={this.state.totalUserGroups}
           pagingLimit={this.state.pagingLimit}
           pagingLimit={this.state.pagingLimit}
+          size="sm"
         />
         />
         <UserGroupDeleteModal
         <UserGroupDeleteModal
           userGroups={this.state.userGroups}
           userGroups={this.state.userGroups}

+ 1 - 0
src/client/js/components/Admin/UserGroupDetail/UserGroupPageList.jsx

@@ -64,6 +64,7 @@ class UserGroupPageList extends React.Component {
           changePage={this.handlePageChange}
           changePage={this.handlePageChange}
           totalItemsCount={this.state.total}
           totalItemsCount={this.state.total}
           pagingLimit={this.state.pagingLimit}
           pagingLimit={this.state.pagingLimit}
+          size="sm"
         />
         />
       </Fragment>
       </Fragment>
     );
     );

+ 1 - 0
src/client/js/components/Admin/UserManagement.jsx

@@ -121,6 +121,7 @@ class UserManagement extends React.Component {
           totalItemsCount={adminUsersContainer.state.totalUsers}
           totalItemsCount={adminUsersContainer.state.totalUsers}
           pagingLimit={adminUsersContainer.state.pagingLimit}
           pagingLimit={adminUsersContainer.state.pagingLimit}
           align="right"
           align="right"
+          size="sm"
         />
         />
       </div>
       </div>
     );
     );

+ 16 - 32
src/client/js/components/BookmarkButton.jsx

@@ -2,46 +2,22 @@ import React from 'react';
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
 
 
 import { toastError } from '../util/apiNotification';
 import { toastError } from '../util/apiNotification';
+import { withUnstatedContainers } from './UnstatedUtils';
+import PageContainer from '../services/PageContainer';
 
 
 class BookmarkButton extends React.Component {
 class BookmarkButton extends React.Component {
 
 
   constructor(props) {
   constructor(props) {
     super(props);
     super(props);
 
 
-    this.state = {
-      isBookmarked: false,
-    };
-
     this.handleClick = this.handleClick.bind(this);
     this.handleClick = this.handleClick.bind(this);
   }
   }
 
 
-  async componentDidMount() {
-    const { pageId, crowi } = this.props;
-    // if guest user
-    if (!this.isUserLoggedIn()) {
-      // do nothing
-      return;
-    }
-
-    try {
-      const response = await crowi.apiv3.get('/bookmarks', { pageId });
-      if (response.data.bookmark != null) {
-        this.setState({ isBookmarked: true });
-      }
-    }
-    catch (err) {
-      toastError(err);
-    }
-
-  }
-
   async handleClick() {
   async handleClick() {
-    const { crowi, pageId } = this.props;
-    const bool = !this.state.isBookmarked;
+    const { pageContainer } = this.props;
 
 
     try {
     try {
-      await crowi.apiv3.put('/bookmarks', { pageId, bool });
-      this.setState({ isBookmarked: bool });
+      pageContainer.toggleBookmark();
     }
     }
     catch (err) {
     catch (err) {
       toastError(err);
       toastError(err);
@@ -53,6 +29,7 @@ class BookmarkButton extends React.Component {
   }
   }
 
 
   render() {
   render() {
+    const { pageContainer } = this.props;
     // if guest user
     // if guest user
     if (!this.isUserLoggedIn()) {
     if (!this.isUserLoggedIn()) {
       return <div></div>;
       return <div></div>;
@@ -64,18 +41,25 @@ class BookmarkButton extends React.Component {
         href="#"
         href="#"
         title="Bookmark"
         title="Bookmark"
         onClick={this.handleClick}
         onClick={this.handleClick}
-        className={`btn rounded-circle btn-bookmark border-0 d-edit-none
+        className={`btn btn-bookmark border-0
           ${`btn-${this.props.size}`}
           ${`btn-${this.props.size}`}
-          ${this.state.isBookmarked ? 'active' : ''}`}
+          ${pageContainer.state.isBookmarked ? 'active' : ''}`}
       >
       >
-        <i className="icon-star"></i>
+        <i className="icon-star mr-3"></i>
+        <span className="total-bookmarks">
+          {pageContainer.state.sumOfBookmarks}
+        </span>
       </button>
       </button>
     );
     );
   }
   }
 
 
 }
 }
 
 
+const BookmarkButtonWrapper = withUnstatedContainers(BookmarkButton, [PageContainer]);
+
 BookmarkButton.propTypes = {
 BookmarkButton.propTypes = {
+  pageContainer: PropTypes.instanceOf(PageContainer).isRequired,
+
   pageId: PropTypes.string,
   pageId: PropTypes.string,
   crowi: PropTypes.object.isRequired,
   crowi: PropTypes.object.isRequired,
   size: PropTypes.string,
   size: PropTypes.string,
@@ -85,4 +69,4 @@ BookmarkButton.defaultProps = {
   size: 'md',
   size: 'md',
 };
 };
 
 
-export default BookmarkButton;
+export default BookmarkButtonWrapper;

+ 83 - 0
src/client/js/components/CustomNavigation.jsx

@@ -0,0 +1,83 @@
+import React, { useEffect, useState } from 'react';
+import PropTypes from 'prop-types';
+import {
+  Nav, NavItem, NavLink, TabContent, TabPane,
+} from 'reactstrap';
+
+
+const CustomNavigation = (props) => {
+  const [activeTab, setActiveTab] = useState('');
+  // [TODO: set default active tab by gw4079]
+  const [sliderWidth, setSliderWidth] = useState(null);
+  const [sliderMarginLeft, setSliderMarginLeft] = useState(null);
+
+  function switchActiveTab(activeTab) {
+    setActiveTab(activeTab);
+  }
+
+  // Might make this dynamic for px, %, pt, em
+  function getPercentage(min, max) {
+    return min / max * 100;
+  }
+
+  useEffect(() => {
+    if (activeTab === '') {
+      return;
+    }
+
+    const navBar = document.getElementById('grw-custom-navbar');
+    const navTabs = document.querySelectorAll('ul.grw-custom-navbar > li.grw-custom-navtab');
+
+    if (navBar == null || navTabs == null) {
+      return;
+    }
+
+    let tempML = 0;
+
+    const styles = [].map.call(navTabs, (el) => {
+      const width = getPercentage(el.offsetWidth, navBar.offsetWidth);
+      const marginLeft = tempML;
+      tempML += width;
+      return { width, marginLeft };
+    });
+    const { width, marginLeft } = styles[props.navTabMapping[activeTab].index];
+
+    setSliderWidth(width);
+    setSliderMarginLeft(marginLeft);
+
+  }, [activeTab]);
+
+
+  return (
+    <React.Fragment>
+      <Nav className="nav-title grw-custom-navbar" id="grw-custom-navbar">
+        {Object.entries(props.navTabMapping).map(([key, value]) => {
+          return (
+            <NavItem key={key} type="button" className={`p-0 grw-custom-navtab ${activeTab === key && 'active'}`}>
+              <NavLink onClick={() => { switchActiveTab(key) }}>
+                {value.icon}
+                {value.i18n}
+              </NavLink>
+            </NavItem>
+          );
+        })}
+      </Nav>
+      <hr className="my-0 grw-nav-slide-hr border-none" style={{ width: `${sliderWidth}%`, marginLeft: `${sliderMarginLeft}%` }} />
+      <TabContent activeTab={activeTab} className="p-4">
+        {Object.entries(props.navTabMapping).map(([key, value]) => {
+          return (
+            <TabPane key={key} tabId={key}>
+              {value.tabContent}
+            </TabPane>
+          );
+        })}
+      </TabContent>
+    </React.Fragment>
+  );
+};
+
+CustomNavigation.propTypes = {
+  navTabMapping: PropTypes.object,
+};
+
+export default CustomNavigation;

+ 0 - 2
src/client/js/components/Hotkeys/Subscribers/EditPage.jsx

@@ -9,8 +9,6 @@ const EditPage = (props) => {
     if (document.getElementsByClassName('modal in').length > 0) {
     if (document.getElementsByClassName('modal in').length > 0) {
       return;
       return;
     }
     }
-    // show editor
-    $('a[data-toggle="tab"][href="#edit"]').tab('show');
 
 
     // remove this
     // remove this
     props.onDeleteRender(this);
     props.onDeleteRender(this);

+ 12 - 14
src/client/js/components/LikeButton.jsx

@@ -4,25 +4,20 @@ import PropTypes from 'prop-types';
 import { toastError } from '../util/apiNotification';
 import { toastError } from '../util/apiNotification';
 import { withUnstatedContainers } from './UnstatedUtils';
 import { withUnstatedContainers } from './UnstatedUtils';
 import AppContainer from '../services/AppContainer';
 import AppContainer from '../services/AppContainer';
+import PageContainer from '../services/PageContainer';
 
 
 class LikeButton extends React.Component {
 class LikeButton extends React.Component {
 
 
   constructor(props) {
   constructor(props) {
     super(props);
     super(props);
 
 
-    this.state = {
-      isLiked: props.isLiked,
-    };
-
     this.handleClick = this.handleClick.bind(this);
     this.handleClick = this.handleClick.bind(this);
   }
   }
 
 
   async handleClick() {
   async handleClick() {
-    const { appContainer, pageId } = this.props;
-    const bool = !this.state.isLiked;
+    const { pageContainer } = this.props;
     try {
     try {
-      await appContainer.apiv3.put('/page/likes', { pageId, bool });
-      this.setState({ isLiked: bool });
+      pageContainer.toggleLike();
     }
     }
     catch (err) {
     catch (err) {
       toastError(err);
       toastError(err);
@@ -34,6 +29,7 @@ class LikeButton extends React.Component {
   }
   }
 
 
   render() {
   render() {
+    const { pageContainer } = this.props;
     // if guest user
     // if guest user
     if (!this.isUserLoggedIn()) {
     if (!this.isUserLoggedIn()) {
       return <div></div>;
       return <div></div>;
@@ -43,10 +39,13 @@ class LikeButton extends React.Component {
       <button
       <button
         type="button"
         type="button"
         onClick={this.handleClick}
         onClick={this.handleClick}
-        className={`btn rounded-circle btn-like border-0 d-edit-none
-        ${this.state.isLiked ? 'active' : ''}`}
+        className={`btn btn-like border-0 d-edit-none
+        ${pageContainer.state.isLiked ? 'active' : ''}`}
       >
       >
-        <i className="icon-like"></i>
+        <i className="icon-like mr-3"></i>
+        <span className="total-likes">
+          {pageContainer.state.sumOfLikers}
+        </span>
       </button>
       </button>
     );
     );
   }
   }
@@ -56,13 +55,12 @@ class LikeButton extends React.Component {
 /**
 /**
  * Wrapper component for using unstated
  * Wrapper component for using unstated
  */
  */
-const LikeButtonWrapper = withUnstatedContainers(LikeButton, [AppContainer]);
+const LikeButtonWrapper = withUnstatedContainers(LikeButton, [AppContainer, PageContainer]);
 
 
 LikeButton.propTypes = {
 LikeButton.propTypes = {
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
+  pageContainer: PropTypes.instanceOf(PageContainer).isRequired,
 
 
-  pageId: PropTypes.string,
-  isLiked: PropTypes.bool,
   size: PropTypes.string,
   size: PropTypes.string,
 };
 };
 
 

+ 2 - 1
src/client/js/components/MyDraftList/MyDraftList.jsx

@@ -134,7 +134,7 @@ class MyDraftList extends React.Component {
     const totalCount = this.state.totalDrafts;
     const totalCount = this.state.totalDrafts;
 
 
     return (
     return (
-      <div className="page-list-container-create">
+      <div>
 
 
         { totalCount === 0
         { totalCount === 0
           && <span>No drafts yet.</span>
           && <span>No drafts yet.</span>
@@ -160,6 +160,7 @@ class MyDraftList extends React.Component {
               changePage={this.handlePage}
               changePage={this.handlePage}
               totalItemsCount={this.state.totalDrafts}
               totalItemsCount={this.state.totalDrafts}
               pagingLimit={this.state.pagingLimit}
               pagingLimit={this.state.pagingLimit}
+              size="sm"
             />
             />
           </React.Fragment>
           </React.Fragment>
         ) }
         ) }

+ 40 - 28
src/client/js/components/Navbar/GrowiSubNavigation.jsx

@@ -24,6 +24,8 @@ import AuthorInfo from './AuthorInfo';
 import DrawerToggler from './DrawerToggler';
 import DrawerToggler from './DrawerToggler';
 import UserPicture from '../User/UserPicture';
 import UserPicture from '../User/UserPicture';
 
 
+import PageManagement from '../Page/PageManagement';
+
 
 
 // eslint-disable-next-line react/prop-types
 // eslint-disable-next-line react/prop-types
 const PagePathNav = ({ pageId, pagePath, isPageForbidden }) => {
 const PagePathNav = ({ pageId, pagePath, isPageForbidden }) => {
@@ -49,13 +51,15 @@ const PagePathNav = ({ pageId, pagePath, isPageForbidden }) => {
   return (
   return (
     <div className="grw-page-path-nav">
     <div className="grw-page-path-nav">
       {formerLink}
       {formerLink}
-      <span className="d-flex align-items-center flex-wrap">
+      <span className="d-flex align-items-center">
         <h1 className="m-0">{latterLink}</h1>
         <h1 className="m-0">{latterLink}</h1>
-        <RevisionPathControls
-          pageId={pageId}
-          pagePath={pagePath}
-          isPageForbidden={isPageForbidden}
-        />
+        <div className="mx-2">
+          <RevisionPathControls
+            pageId={pageId}
+            pagePath={pagePath}
+            isPageForbidden={isPageForbidden}
+          />
+        </div>
       </span>
       </span>
     </div>
     </div>
   );
   );
@@ -70,10 +74,12 @@ const UserPagePathNav = ({ pageId, pagePath }) => {
     <div className="grw-page-path-nav">
     <div className="grw-page-path-nav">
       <span className="d-flex align-items-center flex-wrap">
       <span className="d-flex align-items-center flex-wrap">
         <h4 className="grw-user-page-path">{latterLink}</h4>
         <h4 className="grw-user-page-path">{latterLink}</h4>
-        <RevisionPathControls
-          pageId={pageId}
-          pagePath={pagePath}
-        />
+        <div className="mx-2">
+          <RevisionPathControls
+            pageId={pageId}
+            pagePath={pagePath}
+          />
+        </div>
       </span>
       </span>
     </div>
     </div>
   );
   );
@@ -82,7 +88,7 @@ const UserPagePathNav = ({ pageId, pagePath }) => {
 /* eslint-disable react/prop-types */
 /* eslint-disable react/prop-types */
 const UserInfo = ({ pageUser }) => {
 const UserInfo = ({ pageUser }) => {
   return (
   return (
-    <div className="grw-users-info d-flex align-items-center d-edit-none">
+    <div className="grw-users-info d-flex align-items-center">
       <UserPicture user={pageUser} />
       <UserPicture user={pageUser} />
 
 
       <div className="users-meta">
       <div className="users-meta">
@@ -107,7 +113,9 @@ const UserInfo = ({ pageUser }) => {
 /* eslint-disable react/prop-types */
 /* eslint-disable react/prop-types */
 const PageReactionButtons = ({ appContainer, pageContainer }) => {
 const PageReactionButtons = ({ appContainer, pageContainer }) => {
 
 
-  const { pageId, isLiked, pageUser } = pageContainer.state;
+  const {
+    pageId, isLiked, pageUser,
+  } = pageContainer.state;
 
 
   return (
   return (
     <>
     <>
@@ -116,7 +124,7 @@ const PageReactionButtons = ({ appContainer, pageContainer }) => {
         <LikeButton pageId={pageId} isLiked={isLiked} />
         <LikeButton pageId={pageId} isLiked={isLiked} />
       </span>
       </span>
       )}
       )}
-      <span className="mr-2">
+      <span>
         <BookmarkButton pageId={pageId} crowi={appContainer} />
         <BookmarkButton pageId={pageId} crowi={appContainer} />
       </span>
       </span>
     </>
     </>
@@ -128,37 +136,33 @@ const GrowiSubNavigation = (props) => {
   const {
   const {
     appContainer, navigationContainer, pageContainer, isCompactMode,
     appContainer, navigationContainer, pageContainer, isCompactMode,
   } = props;
   } = props;
-  const { isDrawerMode } = navigationContainer.state;
+  const { isDrawerMode, editorMode } = navigationContainer.state;
   const {
   const {
     pageId, path, createdAt, creator, updatedAt, revisionAuthor,
     pageId, path, createdAt, creator, updatedAt, revisionAuthor,
-    isForbidden: isPageForbidden, pageUser,
+    isForbidden: isPageForbidden, pageUser, isCreatable,
   } = pageContainer.state;
   } = pageContainer.state;
 
 
+  const { currentUser } = appContainer;
   const isPageNotFound = pageId == null;
   const isPageNotFound = pageId == null;
   const isUserPage = pageUser != null;
   const isUserPage = pageUser != null;
   const isPageInTrash = isTrashPage(path);
   const isPageInTrash = isTrashPage(path);
 
 
-  // Display only the RevisionPath
-  if (isPageNotFound || isPageForbidden) {
-    return (
-      <div className="grw-subnav d-flex align-items-center justify-content-between">
-        <PagePathNav pageId={pageId} pagePath={path} isPageForbidden={isPageForbidden} />
-      </div>
-    );
+  function onThreeStrandedButtonClicked(viewType) {
+    navigationContainer.setEditorMode(viewType);
   }
   }
 
 
   return (
   return (
     <div className={`grw-subnav d-flex align-items-center justify-content-between ${isCompactMode ? 'grw-subnav-compact d-print-none' : ''}`}>
     <div className={`grw-subnav d-flex align-items-center justify-content-between ${isCompactMode ? 'grw-subnav-compact d-print-none' : ''}`}>
 
 
       {/* Left side */}
       {/* Left side */}
-      <div className="d-flex">
+      <div className="d-flex grw-subnav-left-side">
         { isDrawerMode && (
         { isDrawerMode && (
           <div className="d-none d-md-flex align-items-center border-right mr-3 pr-3">
           <div className="d-none d-md-flex align-items-center border-right mr-3 pr-3">
             <DrawerToggler />
             <DrawerToggler />
           </div>
           </div>
         ) }
         ) }
 
 
-        <div>
+        <div className="grw-path-nav-container">
           { !isCompactMode && !isPageNotFound && !isPageForbidden && !isUserPage && (
           { !isCompactMode && !isPageNotFound && !isPageForbidden && !isUserPage && (
             <div className="mb-2">
             <div className="mb-2">
               <TagLabels />
               <TagLabels />
@@ -183,17 +187,25 @@ const GrowiSubNavigation = (props) => {
       {/* Right side */}
       {/* Right side */}
       <div className="d-flex">
       <div className="d-flex">
 
 
-        <div className="d-flex flex-column align-items-end justify-content-center">
+        <div className="d-flex flex-column align-items-end">
           <div className="d-flex">
           <div className="d-flex">
-            { !isPageInTrash && <PageReactionButtons appContainer={appContainer} pageContainer={pageContainer} /> }
+            { !isPageInTrash && !isPageNotFound && !isPageForbidden && <PageReactionButtons appContainer={appContainer} pageContainer={pageContainer} /> }
+            { !isPageNotFound && !isPageForbidden && <PageManagement /> }
           </div>
           </div>
           <div className="mt-2">
           <div className="mt-2">
-            <ThreeStrandedButton />
+            { !isCreatable && !isPageInTrash
+            && (
+            <ThreeStrandedButton
+              onThreeStrandedButtonClicked={onThreeStrandedButtonClicked}
+              isBtnDisabled={currentUser == null}
+              editorMode={editorMode}
+            />
+)}
           </div>
           </div>
         </div>
         </div>
 
 
         {/* Page Authors */}
         {/* Page Authors */}
-        { (!isCompactMode && !isUserPage) && (
+        { (!isCompactMode && !isUserPage && !isPageNotFound && !isPageForbidden) && (
           <ul className="authors text-nowrap border-left d-none d-lg-block d-edit-none">
           <ul className="authors text-nowrap border-left d-none d-lg-block d-edit-none">
             <li className="pb-1">
             <li className="pb-1">
               <AuthorInfo user={creator} date={createdAt} />
               <AuthorInfo user={creator} date={createdAt} />

+ 49 - 15
src/client/js/components/Navbar/ThreeStrandedButton.jsx

@@ -1,32 +1,60 @@
 import React from 'react';
 import React from 'react';
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
 import { withTranslation } from 'react-i18next';
 import { withTranslation } from 'react-i18next';
+import { UncontrolledTooltip } from 'reactstrap';
 
 
 const ThreeStrandedButton = (props) => {
 const ThreeStrandedButton = (props) => {
+  const { t, isBtnDisabled, editorMode } = props;
 
 
-  const { t } = props;
 
 
   function threeStrandedButtonClickedHandler(viewType) {
   function threeStrandedButtonClickedHandler(viewType) {
+    if (isBtnDisabled) {
+      return;
+    }
     if (props.onThreeStrandedButtonClicked != null) {
     if (props.onThreeStrandedButtonClicked != null) {
       props.onThreeStrandedButtonClicked(viewType);
       props.onThreeStrandedButtonClicked(viewType);
     }
     }
   }
   }
 
 
   return (
   return (
-    <div className="btn-group grw-three-stranded-button" role="group " aria-label="three-stranded-button">
-      <button type="button" className="btn btn-outline-primary view-button" onClick={() => { threeStrandedButtonClickedHandler('view') }}>
-        <i className="icon-control-play icon-fw" />
-        { t('view') }
-      </button>
-      <button type="button" className="btn btn-outline-primary edit-button" onClick={() => { threeStrandedButtonClickedHandler('edit') }}>
-        <i className="icon-note icon-fw" />
-        { t('Edit') }
-      </button>
-      <button type="button" className="btn btn-outline-primary hackmd-button" onClick={() => { threeStrandedButtonClickedHandler('hackmd') }}>
-        <i className="fa fa-fw fa-file-text-o" />
-        { t('hackmd.hack_md') }
-      </button>
-    </div>
+    <>
+      <div
+        className="btn-group grw-three-stranded-button"
+        role="group"
+        aria-label="three-stranded-button"
+        id="grw-three-stranded-button"
+      >
+        <button
+          type="button"
+          className={`btn btn-outline-primary view-button ${editorMode === 'view' && 'active'} ${isBtnDisabled && 'disabled'}`}
+          onClick={() => { threeStrandedButtonClickedHandler('view') }}
+        >
+          <i className="icon-control-play icon-fw grw-three-stranded-button-icon" />
+          { t('view') }
+        </button>
+        <button
+          type="button"
+          className={`btn btn-outline-primary edit-button ${editorMode === 'edit' && 'active'} ${isBtnDisabled && 'disabled'}`}
+          onClick={() => { threeStrandedButtonClickedHandler('edit') }}
+        >
+          <i className="icon-note icon-fw grw-three-stranded-button-icon" />
+          { t('Edit') }
+        </button>
+        <button
+          type="button"
+          className={`btn btn-outline-primary hackmd-button ${editorMode === 'hackmd' && 'active'} ${isBtnDisabled && 'disabled'}`}
+          onClick={() => { threeStrandedButtonClickedHandler('hackmd') }}
+        >
+          <i className="fa fa-fw fa-file-text-o grw-three-stranded-button-icon" />
+          { t('hackmd.hack_md') }
+        </button>
+      </div>
+      {isBtnDisabled && (
+        <UncontrolledTooltip placement="top" target="grw-three-stranded-button" fade={false}>
+          {t('Not available for guest')}
+        </UncontrolledTooltip>
+      )}
+    </>
   );
   );
 
 
 };
 };
@@ -34,6 +62,12 @@ const ThreeStrandedButton = (props) => {
 ThreeStrandedButton.propTypes = {
 ThreeStrandedButton.propTypes = {
   t: PropTypes.func.isRequired, //  i18next
   t: PropTypes.func.isRequired, //  i18next
   onThreeStrandedButtonClicked: PropTypes.func,
   onThreeStrandedButtonClicked: PropTypes.func,
+  isBtnDisabled: PropTypes.bool,
+  editorMode: PropTypes.string,
+};
+
+ThreeStrandedButton.defaultProps = {
+  isBtnDisabled: false,
 };
 };
 
 
 export default withTranslation()(ThreeStrandedButton);
 export default withTranslation()(ThreeStrandedButton);

+ 39 - 0
src/client/js/components/NotFoundPage.jsx

@@ -0,0 +1,39 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { withTranslation } from 'react-i18next';
+import PageListIcon from './Icons/PageListIcon';
+import TimeLineIcon from './Icons/TimeLineIcon';
+import CustomNavigation from './CustomNavigation';
+import PageList from './PageList';
+import PageTimeline from './PageTimeline';
+
+const NotFoundPage = (props) => {
+  const { t } = props;
+
+  const navTabMapping = {
+    pagelist: {
+      icon: <PageListIcon />,
+      i18n: t('page_list'),
+      tabContent: <PageList />,
+      index: 0,
+    },
+    timeLine: {
+      icon: <TimeLineIcon />,
+      i18n: t('Timeline View'),
+      tabContent: <PageTimeline />,
+      index: 1,
+    },
+  };
+
+  return (
+    <div className="grw-custom-navigation mt-5 on-edit">
+      <CustomNavigation navTabMapping={navTabMapping} />
+    </div>
+  );
+};
+
+NotFoundPage.propTypes = {
+  t: PropTypes.func.isRequired, //  i18next
+};
+
+export default withTranslation()(NotFoundPage);

+ 2 - 2
src/client/js/components/Page.jsx

@@ -129,12 +129,12 @@ class Page extends React.Component {
 
 
   render() {
   render() {
     const { appContainer, pageContainer } = this.props;
     const { appContainer, pageContainer } = this.props;
-    const isMobile = appContainer.isMobile;
+    const { isMobile } = appContainer;
     const isLoggedIn = appContainer.currentUser != null;
     const isLoggedIn = appContainer.currentUser != null;
     const { markdown } = pageContainer.state;
     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} />
 
 
         { isLoggedIn && (
         { isLoggedIn && (

+ 43 - 0
src/client/js/components/Page/DisplaySwitcher.jsx

@@ -0,0 +1,43 @@
+import React from 'react';
+import { TabContent, TabPane } from 'reactstrap';
+import propTypes from 'prop-types';
+import { withUnstatedContainers } from '../UnstatedUtils';
+import NavigationContainer from '../../services/NavigationContainer';
+import Editor from '../PageEditor';
+import Page from '../Page';
+import PageEditorByHackmd from '../PageEditorByHackmd';
+import EditorNavbarBottom from '../PageEditor/EditorNavbarBottom';
+
+
+const DisplaySwitcher = (props) => {
+  const { navigationContainer } = props;
+  const { editorMode } = navigationContainer.state;
+
+  return (
+    <>
+      <TabContent activeTab={editorMode}>
+        <TabPane tabId="view">
+          <Page />
+        </TabPane>
+        <TabPane tabId="edit">
+          <div id="page-editor">
+            <Editor />
+          </div>
+        </TabPane>
+        <TabPane tabId="hackmd">
+          <div id="page-editor-with-hackmd">
+            <PageEditorByHackmd />
+          </div>
+        </TabPane>
+      </TabContent>
+      {editorMode !== 'view' && <EditorNavbarBottom /> }
+    </>
+  );
+};
+
+DisplaySwitcher.propTypes = {
+  navigationContainer: propTypes.instanceOf(NavigationContainer).isRequired,
+};
+
+
+export default withUnstatedContainers(DisplaySwitcher, [NavigationContainer]);

+ 40 - 0
src/client/js/components/Page/NotFoundAlert.jsx

@@ -0,0 +1,40 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { withTranslation } from 'react-i18next';
+
+const NotFoundAlert = (props) => {
+  const { t } = props;
+  function clickHandler(viewType) {
+    if (props.onPageCreateClicked === null) {
+      return;
+    }
+    props.onPageCreateClicked(viewType);
+  }
+
+  return (
+    <div className="border border-info m-4 p-3">
+      <div className="col-md-12 p-0">
+        <h2 className="text-info lead">
+          <i className="icon-info pr-2 font-weight-bold" aria-hidden="true"></i>
+          {t('not_found_page.page_not_exist_alert')}
+        </h2>
+        <button
+          type="button"
+          className="m-1 pl-3 pr-3 btn bg-info text-white"
+          onClick={() => { clickHandler('edit') }}
+        >
+          <i className="icon-note icon-fw" />
+          {t('not_found_page.Create Page')}
+        </button>
+      </div>
+    </div>
+  );
+};
+
+
+NotFoundAlert.propTypes = {
+  t: PropTypes.func.isRequired, // i18next
+  onPageCreateClicked: PropTypes.func,
+};
+
+export default withTranslation()(NotFoundAlert);

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

@@ -175,10 +175,10 @@ const PageManagement = (props) => {
       <>
       <>
         <button
         <button
           type="button"
           type="button"
-          className="btn-link nav-link bg-transparent dropdown-toggle dropdown-toggle-no-caret"
+          className="btn-link nav-link dropdown-toggle dropdown-toggle-no-caret border-0 rounded grw-btn-page-management"
           data-toggle="dropdown"
           data-toggle="dropdown"
         >
         >
-          <i className="icon-options-vertical"></i>
+          <i className="icon-options"></i>
         </button>
         </button>
       </>
       </>
     );
     );
@@ -192,9 +192,9 @@ const PageManagement = (props) => {
           className="btn nav-link bg-transparent dropdown-toggle dropdown-toggle-no-caret disabled"
           className="btn nav-link bg-transparent dropdown-toggle dropdown-toggle-no-caret disabled"
           id="icon-options-guest-tltips"
           id="icon-options-guest-tltips"
         >
         >
-          <i className="icon-options-vertical"></i>
+          <i className="icon-options"></i>
         </button>
         </button>
-        <UncontrolledTooltip placement="top" target="icon-options-guest-tltips">
+        <UncontrolledTooltip placement="top" target="icon-options-guest-tltips" fade={false}>
           {t('Not available for guest')}
           {t('Not available for guest')}
         </UncontrolledTooltip>
         </UncontrolledTooltip>
       </>
       </>

+ 1 - 9
src/client/js/components/Page/RevisionPathControls.jsx

@@ -3,8 +3,6 @@ import PropTypes from 'prop-types';
 
 
 import { withTranslation } from 'react-i18next';
 import { withTranslation } from 'react-i18next';
 
 
-import { isTrashPage } from '@commons/util/path-utils';
-
 import CopyDropdown from './CopyDropdown';
 import CopyDropdown from './CopyDropdown';
 
 
 const RevisionPathControls = (props) => {
 const RevisionPathControls = (props) => {
@@ -15,19 +13,13 @@ const RevisionPathControls = (props) => {
   };
   };
 
 
   const {
   const {
-    pagePath, pageId, isPageForbidden,
+    pagePath, pageId,
   } = props;
   } = props;
 
 
-  const isPageInTrash = isTrashPage(pagePath);
 
 
   return (
   return (
     <>
     <>
       <CopyDropdown pagePath={pagePath} pageId={pageId} buttonStyle={buttonStyle} />
       <CopyDropdown pagePath={pagePath} pageId={pageId} buttonStyle={buttonStyle} />
-      { !isPageInTrash && !isPageForbidden && (
-        <a href="#edit" className="d-edit-none text-muted btn btn-secondary bg-transparent btn-edit border-0" style={buttonStyle}>
-          <i className="icon-note" />
-        </a>
-      ) }
     </>
     </>
   );
   );
 };
 };

+ 7 - 18
src/client/js/components/Page/TagLabels.jsx

@@ -28,12 +28,11 @@ class TagLabels extends React.Component {
 
 
   /**
   /**
    * @return tags data
    * @return tags data
-   *   1. pageContainer.state.tags if isEditorMode is false
-   *   2. editorContainer.state.tags if isEditorMode is true
+   *   1. pageContainer.state.tags if pageId is not null
+   *   2. editorContainer.state.tags if pageId is null
    */
    */
   getEditTargetData() {
   getEditTargetData() {
-    const { isEditorMode } = this.props;
-    return (isEditorMode) ? this.props.editorContainer.state.tags : this.props.pageContainer.state.tags;
+    return (this.props.editorContainer.state.pageId != null) ? this.props.editorContainer.state.tags : this.props.pageContainer.state.tags;
   }
   }
 
 
   openEditorModal() {
   openEditorModal() {
@@ -45,24 +44,19 @@ class TagLabels extends React.Component {
   }
   }
 
 
   async tagsUpdatedHandler(tags) {
   async tagsUpdatedHandler(tags) {
-    const { appContainer, editorContainer, isEditorMode } = this.props;
+    const { appContainer, editorContainer, pageContainer } = this.props;
+    const { pageId } = pageContainer.state;
 
 
-    // only update tags in editorContainer
-    if (isEditorMode) {
+    // only update tags in editorContainer when new page
+    if (pageId != null) {
       return editorContainer.setState({ tags });
       return editorContainer.setState({ tags });
     }
     }
 
 
-    // post api request and update tags
-    const { pageContainer } = this.props;
-
     try {
     try {
-      const { pageId } = pageContainer.state;
       await appContainer.apiPost('/tags.update', { pageId, tags });
       await appContainer.apiPost('/tags.update', { pageId, tags });
 
 
       // update pageContainer.state
       // update pageContainer.state
       pageContainer.setState({ tags });
       pageContainer.setState({ tags });
-      editorContainer.setState({ tags });
-
       toastSuccess('updated tags successfully');
       toastSuccess('updated tags successfully');
     }
     }
     catch (err) {
     catch (err) {
@@ -113,11 +107,6 @@ TagLabels.propTypes = {
   pageContainer: PropTypes.instanceOf(PageContainer).isRequired,
   pageContainer: PropTypes.instanceOf(PageContainer).isRequired,
   editorContainer: PropTypes.instanceOf(EditorContainer).isRequired,
   editorContainer: PropTypes.instanceOf(EditorContainer).isRequired,
 
 
-  isEditorMode: PropTypes.bool,
-};
-
-TagLabels.defaultProps = {
-  isEditorMode: false,
 };
 };
 
 
 export default withTranslation()(TagLabelsWrapper);
 export default withTranslation()(TagLabelsWrapper);

+ 0 - 48
src/client/js/components/PageEditor/PagePathNavForEditor.jsx

@@ -1,48 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-
-import { withTranslation } from 'react-i18next';
-
-import LinkedPagePath from '@commons/models/linked-page-path';
-import PagePathHierarchicalLink from '@commons/components/PagePathHierarchicalLink';
-
-import { withUnstatedContainers } from '../UnstatedUtils';
-import AppContainer from '../../services/AppContainer';
-import PageContainer from '../../services/PageContainer';
-
-import RevisionPathControls from '../Page/RevisionPathControls';
-import TagLabels from '../Page/TagLabels';
-
-const PagePathNavForEditor = (props) => {
-  const { pageId, path } = props.pageContainer.state;
-
-  const linkedPagePath = new LinkedPagePath(path);
-  const pagePathHierarchicalLink = <PagePathHierarchicalLink linkedPagePath={linkedPagePath} />;
-
-  return (
-    <div className="grw-page-path-nav-for-edit">
-      <span className="d-flex align-items-center flex-wrap">
-        <h3 className="mb-0 grw-page-path-link">{pagePathHierarchicalLink}</h3>
-        <RevisionPathControls
-          pageId={pageId}
-          pagePath={path}
-        />
-      </span>
-      <TagLabels isEditorMode />
-    </div>
-  );
-};
-
-/**
- * Wrapper component for using unstated
- */
-const PagePathNavForEditorWrapper = withUnstatedContainers(PagePathNavForEditor, [AppContainer, PageContainer]);
-
-
-PagePathNavForEditor.propTypes = {
-  t: PropTypes.func.isRequired, //  i18next
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
-  pageContainer: PropTypes.instanceOf(PageContainer).isRequired,
-};
-
-export default withTranslation()(PagePathNavForEditorWrapper);

+ 20 - 2
src/client/js/components/PageEditorByHackmd.jsx

@@ -230,9 +230,9 @@ class PageEditorByHackmd extends React.Component {
     const hackmdUri = this.getHackmdUri();
     const hackmdUri = this.getHackmdUri();
     const { pageContainer, t } = this.props;
     const { pageContainer, t } = this.props;
     const {
     const {
-      revisionId, revisionIdHackmdSynced, remoteRevisionId,
+      revisionId, revisionIdHackmdSynced, remoteRevisionId, pageId,
     } = pageContainer.state;
     } = pageContainer.state;
-
+    const isPageNotFound = pageId == null;
 
 
     let content;
     let content;
 
 
@@ -243,6 +243,24 @@ class PageEditorByHackmd extends React.Component {
       content = (
       content = (
         <div>
         <div>
           <p className="text-center hackmd-status-label"><i className="fa fa-file-text"></i> { t('hackmd.not_set_up')}</p>
           <p className="text-center hackmd-status-label"><i className="fa fa-file-text"></i> { t('hackmd.not_set_up')}</p>
+          {/* eslint-disable-next-line react/no-danger */}
+          <p dangerouslySetInnerHTML={{ __html: t('hackmd.need_to_associate_with_growi_to_use_hackmd_refer_to_this') }} />
+        </div>
+      );
+    }
+
+    /*
+    * used HackMD from NotFound Page
+    */
+    else if (isPageNotFound) {
+      content = (
+        <div className="text-center">
+          <p className="hackmd-status-label">
+            <i className="fa fa-file-text mr-2" />
+            { t('hackmd.used_for_not_found') }
+          </p>
+          {/* eslint-disable-next-line react/no-danger */}
+          <p dangerouslySetInnerHTML={{ __html: t('hackmd.need_to_make_page') }} />
         </div>
         </div>
       );
       );
     }
     }

+ 16 - 4
src/client/js/components/PageList.jsx

@@ -1,5 +1,6 @@
 import React, { useEffect, useCallback, useState } from 'react';
 import React, { useEffect, useCallback, useState } from 'react';
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
+import { withTranslation } from 'react-i18next';
 
 
 import Page from './PageList/Page';
 import Page from './PageList/Page';
 import { withUnstatedContainers } from './UnstatedUtils';
 import { withUnstatedContainers } from './UnstatedUtils';
@@ -11,7 +12,7 @@ import PaginationWrapper from './PaginationWrapper';
 
 
 
 
 const PageList = (props) => {
 const PageList = (props) => {
-  const { appContainer, pageContainer } = props;
+  const { appContainer, pageContainer, t } = props;
   const { path } = pageContainer.state;
   const { path } = pageContainer.state;
   const [pages, setPages] = useState(null);
   const [pages, setPages] = useState(null);
   const [isLoading, setIsLoading] = useState(false);
   const [isLoading, setIsLoading] = useState(false);
@@ -54,10 +55,18 @@ const PageList = (props) => {
       <Page page={page} />
       <Page page={page} />
     </li>
     </li>
   ));
   ));
+  if (pageList.length === 0) {
+    return (
+      <div className="mt-2">
+        {/* eslint-disable-next-line react/no-danger */}
+        <p dangerouslySetInnerHTML={{ __html: t('custom_navigation.no_page_list', { path }) }} />
+      </div>
+    );
+  }
 
 
   return (
   return (
-    <div className="page-list-container-create">
-      <ul className="page-list-ul page-list-ul-flat ml-n4">
+    <div className="page-list">
+      <ul className="page-list-ul page-list-ul-flat">
         {pageList}
         {pageList}
       </ul>
       </ul>
       <PaginationWrapper
       <PaginationWrapper
@@ -75,10 +84,13 @@ const PageList = (props) => {
 
 
 const PageListWrapper = withUnstatedContainers(PageList, [AppContainer, PageContainer]);
 const PageListWrapper = withUnstatedContainers(PageList, [AppContainer, PageContainer]);
 
 
+const PageListTranslation = withTranslation()(PageListWrapper);
+
 
 
 PageList.propTypes = {
 PageList.propTypes = {
+  t: PropTypes.func.isRequired, // i18next
   appContainer: PropTypes.instanceOf(AppContainer),
   appContainer: PropTypes.instanceOf(AppContainer),
   pageContainer: PropTypes.instanceOf(PageContainer),
   pageContainer: PropTypes.instanceOf(PageContainer),
 };
 };
 
 
-export default PageListWrapper;
+export default PageListTranslation;

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

@@ -61,7 +61,7 @@ class PageTimeline extends React.Component {
 
 
   render() {
   render() {
     const { pages } = this.state;
     const { pages } = this.state;
-    if (pages == null) {
+    if (pages == null || pages.length === 0) {
       return <React.Fragment></React.Fragment>;
       return <React.Fragment></React.Fragment>;
     }
     }
 
 

+ 3 - 2
src/client/js/components/PaginationWrapper.jsx

@@ -173,7 +173,7 @@ class PaginationWrapper extends React.Component {
 
 
     return (
     return (
       <React.Fragment>
       <React.Fragment>
-        <Pagination size="sm" listClassName={this.getListClassName()}>{paginationItems}</Pagination>
+        <Pagination size={this.props.size} listClassName={this.getListClassName()}>{paginationItems}</Pagination>
       </React.Fragment>
       </React.Fragment>
     );
     );
   }
   }
@@ -191,11 +191,12 @@ PaginationWrapper.propTypes = {
   totalItemsCount: PropTypes.number.isRequired,
   totalItemsCount: PropTypes.number.isRequired,
   pagingLimit: PropTypes.number,
   pagingLimit: PropTypes.number,
   align: PropTypes.string,
   align: PropTypes.string,
+  size: PropTypes.string,
 };
 };
 PaginationWrapper.defaultProps = {
 PaginationWrapper.defaultProps = {
   align: 'left',
   align: 'left',
+  size: 'md',
   pagingLimit: PropTypes.number,
   pagingLimit: PropTypes.number,
-
 };
 };
 
 
 export default withTranslation()(PaginationWrappered);
 export default withTranslation()(PaginationWrappered);

+ 2 - 1
src/client/js/components/RecentCreated/RecentCreated.jsx

@@ -68,7 +68,7 @@ class RecentCreated extends React.Component {
     const pageList = this.generatePageList(this.state.pages);
     const pageList = this.generatePageList(this.state.pages);
 
 
     return (
     return (
-      <div className="page-list-container-create">
+      <div>
         <ul className="page-list-ul page-list-ul-flat mb-3">
         <ul className="page-list-ul page-list-ul-flat mb-3">
           {pageList}
           {pageList}
         </ul>
         </ul>
@@ -77,6 +77,7 @@ class RecentCreated extends React.Component {
           changePage={this.handlePage}
           changePage={this.handlePage}
           totalItemsCount={this.state.totalPages}
           totalItemsCount={this.state.totalPages}
           pagingLimit={this.state.pagingLimit}
           pagingLimit={this.state.pagingLimit}
+          size="sm"
         />
         />
       </div>
       </div>
     );
     );

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

@@ -80,6 +80,7 @@ class TagsList extends React.Component {
             changePage={this.handlePage}
             changePage={this.handlePage}
             totalItemsCount={this.state.totalTags}
             totalItemsCount={this.state.totalTags}
             pagingLimit={this.state.pagingLimit}
             pagingLimit={this.state.pagingLimit}
+            size="sm"
           />
           />
         </div>
         </div>
       </div>
       </div>

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

@@ -76,6 +76,7 @@ const TopOfTableContents = (props) => {
           id="seen-user-list"
           id="seen-user-list"
           data-user-ids-str="{{ page.seenUsers|slice(-15)|default([])|reverse|join(',') }}"
           data-user-ids-str="{{ page.seenUsers|slice(-15)|default([])|reverse|join(',') }}"
           data-sum-of-seen-users="{{ page.seenUsers.length|default(0) }}"
           data-sum-of-seen-users="{{ page.seenUsers.length|default(0) }}"
+          className="grw-seen-user-list ml-1 pl-1"
         >
         >
         </div>
         </div>
       </div>
       </div>

+ 32 - 0
src/client/js/components/TrashPageList.jsx

@@ -0,0 +1,32 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { withTranslation } from 'react-i18next';
+import PageListIcon from './Icons/PageListIcon';
+import CustomNavigation from './CustomNavigation';
+import PageList from './PageList';
+
+
+const TrashPageList = (props) => {
+  const { t } = props;
+
+  const navTabMapping = {
+    pagelist: {
+      icon: <PageListIcon />,
+      i18n: t('page_list'),
+      tabContent: <PageList />,
+      index: 0,
+    },
+  };
+
+  return (
+    <div className="grw-custom-navigation mt-5">
+      <CustomNavigation navTabMapping={navTabMapping} />
+    </div>
+  );
+};
+
+TrashPageList.propTypes = {
+  t: PropTypes.func.isRequired, //  i18next
+};
+
+export default withTranslation()(TrashPageList);

+ 2 - 2
src/client/js/components/User/SeenUserList.jsx

@@ -20,7 +20,7 @@ const SeenUserList = (props) => {
   const toggle = () => setPopoverOpen(!popoverOpen);
   const toggle = () => setPopoverOpen(!popoverOpen);
   const { pageContainer } = props;
   const { pageContainer } = props;
   return (
   return (
-    <div className="grw-seen-user-list pl-2 ml-2">
+    <>
       <Button id="po-seen-user" color="link" className="px-2">
       <Button id="po-seen-user" color="link" className="px-2">
         <span className="mr-1 footstamp-icon"><FootstampIcon /></span>
         <span className="mr-1 footstamp-icon"><FootstampIcon /></span>
         <span className="seen-user-count">{pageContainer.state.countOfSeenUsers}</span>
         <span className="seen-user-count">{pageContainer.state.countOfSeenUsers}</span>
@@ -32,7 +32,7 @@ const SeenUserList = (props) => {
           </div>
           </div>
         </PopoverBody>
         </PopoverBody>
       </Popover>
       </Popover>
-    </div>
+    </>
   );
   );
 };
 };
 
 

+ 12 - 97
src/client/js/legacy/crowi.js

@@ -15,6 +15,9 @@ window.Crowi = Crowi;
  * @param {number} line
  * @param {number} line
  */
  */
 Crowi.setCaretLineData = function(line) {
 Crowi.setCaretLineData = function(line) {
+  const { appContainer } = window;
+  const navigationContainer = appContainer.getContainer('NavigationContainer');
+  navigationContainer.setEditorMode('edit');
   const pageEditorDom = document.querySelector('#page-editor');
   const pageEditorDom = document.querySelector('#page-editor');
   pageEditorDom.setAttribute('data-caret-line', line);
   pageEditorDom.setAttribute('data-caret-line', line);
 };
 };
@@ -152,15 +155,11 @@ Crowi.highlightSelectedSection = function(hash) {
 };
 };
 
 
 $(() => {
 $(() => {
-  const appContainer = window.appContainer;
-  const config = appContainer.getConfig();
-
   const pageId = $('#content-main').data('page-id');
   const pageId = $('#content-main').data('page-id');
   // const revisionId = $('#content-main').data('page-revision-id');
   // const revisionId = $('#content-main').data('page-revision-id');
   // const revisionCreatedAt = $('#content-main').data('page-revision-created');
   // const revisionCreatedAt = $('#content-main').data('page-revision-created');
   // const currentUser = $('#content-main').data('current-user');
   // const currentUser = $('#content-main').data('current-user');
   const isSeen = $('#content-main').data('page-is-seen');
   const isSeen = $('#content-main').data('page-is-seen');
-  const isSavedStatesOfTabChanges = config.isSavedStatesOfTabChanges;
 
 
   $('[data-toggle="popover"]').popover();
   $('[data-toggle="popover"]').popover();
   $('[data-toggle="tooltip"]').tooltip();
   $('[data-toggle="tooltip"]').tooltip();
@@ -198,77 +197,6 @@ $(() => {
       });
       });
     }
     }
   } // end if pageId
   } // end if pageId
-
-  // tab changing handling
-  $('a[href="#revision-body"]').on('show.bs.tab', () => {
-    const navigationContainer = appContainer.getContainer('NavigationContainer');
-    navigationContainer.setEditorMode(null);
-  });
-  $('a[href="#edit"]').on('show.bs.tab', () => {
-    const navigationContainer = appContainer.getContainer('NavigationContainer');
-    navigationContainer.setEditorMode('builtin');
-    $('body').addClass('on-edit');
-    $('body').addClass('builtin-editor');
-  });
-  $('a[href="#edit"]').on('hide.bs.tab', () => {
-    $('body').removeClass('on-edit');
-    $('body').removeClass('builtin-editor');
-  });
-  $('a[href="#hackmd"]').on('show.bs.tab', () => {
-    const navigationContainer = appContainer.getContainer('NavigationContainer');
-    navigationContainer.setEditorMode('hackmd');
-    $('body').addClass('on-edit');
-    $('body').addClass('hackmd');
-  });
-
-  $('a[href="#hackmd"]').on('hide.bs.tab', () => {
-    $('body').removeClass('on-edit');
-    $('body').removeClass('hackmd');
-  });
-
-  // hash handling
-  if (isSavedStatesOfTabChanges) {
-    $('a[data-toggle="tab"][href="#revision-history"]').on('show.bs.tab', () => {
-      window.location.hash = '#revision-history';
-      window.history.replaceState('', 'History', '#revision-history');
-    });
-    $('a[data-toggle="tab"][href="#edit"]').on('show.bs.tab', () => {
-      window.location.hash = '#edit';
-      window.history.replaceState('', 'Edit', '#edit');
-    });
-    $('a[data-toggle="tab"][href="#hackmd"]').on('show.bs.tab', () => {
-      window.location.hash = '#hackmd';
-      window.history.replaceState('', 'HackMD', '#hackmd');
-    });
-    $('a[data-toggle="tab"][href="#revision-body"]').on('show.bs.tab', () => {
-      // couln't solve https://github.com/weseek/crowi-plus/issues/119 completely -- 2017.07.03 Yuki Takei
-      window.location.hash = '#';
-      window.history.replaceState('', '', window.location.href);
-    });
-  }
-  else {
-    $('a[data-toggle="tab"][href="#revision-history"]').on('show.bs.tab', () => {
-      window.history.replaceState('', 'History', '#revision-history');
-    });
-    $('a[data-toggle="tab"][href="#edit"]').on('show.bs.tab', () => {
-      window.history.replaceState('', 'Edit', '#edit');
-    });
-    $('a[data-toggle="tab"][href="#hackmd"]').on('show.bs.tab', () => {
-      window.history.replaceState('', 'HackMD', '#hackmd');
-    });
-    $('a[data-toggle="tab"][href="#revision-body"]').on('show.bs.tab', () => {
-      window.history.replaceState('', '', window.location.href.replace(window.location.hash, ''));
-    });
-    // replace all href="#edit" link behaviors
-    $(document).on('click', 'a[href="#edit"]', () => {
-      window.location.replace('#edit');
-    });
-  }
-
-  // focus to editor when 'shown.bs.tab' event fired
-  $('a[href="#edit"]').on('shown.bs.tab', (e) => {
-    Crowi.setCaretLineAndFocusToEditor();
-  });
 });
 });
 
 
 window.addEventListener('load', (e) => {
 window.addEventListener('load', (e) => {
@@ -283,25 +211,14 @@ window.addEventListener('load', (e) => {
   if (window.location.hash) {
   if (window.location.hash) {
     const navigationContainer = appContainer.getContainer('NavigationContainer');
     const navigationContainer = appContainer.getContainer('NavigationContainer');
 
 
-    if ((window.location.hash === '#edit' || window.location.hash === '#edit-form') && $('.tab-pane#edit').length > 0) {
-      navigationContainer.setEditorMode('builtin');
-
-      $('a[data-toggle="tab"][href="#edit"]').tab('show');
-      $('body').addClass('on-edit');
-      $('body').addClass('builtin-editor');
+    if (window.location.hash === '#edit') {
+      navigationContainer.setEditorMode('edit');
 
 
       // focus
       // focus
       Crowi.setCaretLineAndFocusToEditor();
       Crowi.setCaretLineAndFocusToEditor();
     }
     }
-    else if (window.location.hash === '#hackmd' && $('.tab-pane#hackmd').length > 0) {
+    else if (window.location.hash === '#hackmd') {
       navigationContainer.setEditorMode('hackmd');
       navigationContainer.setEditorMode('hackmd');
-
-      $('a[data-toggle="tab"][href="#hackmd"]').tab('show');
-      $('body').addClass('on-edit');
-      $('body').addClass('hackmd');
-    }
-    else if (window.location.hash === '#revision-history' && $('.tab-pane#revision-history').length > 0) {
-      $('a[data-toggle="tab"][href="#revision-history"]').tab('show');
     }
     }
   }
   }
 });
 });
@@ -353,22 +270,20 @@ window.addEventListener('hashchange', (e) => {
   Crowi.unhighlightSelectedSection(Crowi.findHashFromUrl(e.oldURL));
   Crowi.unhighlightSelectedSection(Crowi.findHashFromUrl(e.oldURL));
   Crowi.highlightSelectedSection(Crowi.findHashFromUrl(e.newURL));
   Crowi.highlightSelectedSection(Crowi.findHashFromUrl(e.newURL));
   Crowi.modifyScrollTop();
   Crowi.modifyScrollTop();
+  const { appContainer } = window;
+  const navigationContainer = appContainer.getContainer('NavigationContainer');
+
 
 
   // hash on page
   // hash on page
   if (window.location.hash) {
   if (window.location.hash) {
     if (window.location.hash === '#edit') {
     if (window.location.hash === '#edit') {
-      $('a[data-toggle="tab"][href="#edit"]').tab('show');
+      navigationContainer.setEditorMode('edit');
+      Crowi.setCaretLineAndFocusToEditor();
     }
     }
     else if (window.location.hash === '#hackmd') {
     else if (window.location.hash === '#hackmd') {
-      $('a[data-toggle="tab"][href="#hackmd"]').tab('show');
-    }
-    else if (window.location.hash === '#revision-history') {
-      $('a[data-toggle="tab"][href="#revision-history"]').tab('show');
+      navigationContainer.setEditorMode('hackmd');
     }
     }
   }
   }
-  else {
-    $('a[data-toggle="tab"][href="#revision-body"]').tab('show');
-  }
 });
 });
 
 
 // adjust min-height of page for print temporarily
 // adjust min-height of page for print temporarily

+ 24 - 2
src/client/js/services/NavigationContainer.js

@@ -19,7 +19,7 @@ export default class NavigationContainer extends Container {
     const { localStorage } = window;
     const { localStorage } = window;
 
 
     this.state = {
     this.state = {
-      editorMode: null,
+      editorMode: 'view',
 
 
       isDeviceSmallerThanMd: null,
       isDeviceSmallerThanMd: null,
       preferDrawerModeByUser: localStorage.preferDrawerModeByUser === 'true',
       preferDrawerModeByUser: localStorage.preferDrawerModeByUser === 'true',
@@ -37,6 +37,7 @@ export default class NavigationContainer extends Container {
 
 
     this.openPageCreateModal = this.openPageCreateModal.bind(this);
     this.openPageCreateModal = this.openPageCreateModal.bind(this);
     this.closePageCreateModal = this.closePageCreateModal.bind(this);
     this.closePageCreateModal = this.closePageCreateModal.bind(this);
+    this.setEditorMode = this.setEditorMode.bind(this);
     this.initDeviceSize();
     this.initDeviceSize();
     this.initScrollEvent();
     this.initScrollEvent();
   }
   }
@@ -86,6 +87,27 @@ export default class NavigationContainer extends Container {
 
 
   setEditorMode(editorMode) {
   setEditorMode(editorMode) {
     this.setState({ editorMode });
     this.setState({ editorMode });
+    if (editorMode === 'view') {
+      $('body').removeClass('on-edit');
+      $('body').removeClass('builtin-editor');
+      $('body').removeClass('hackmd');
+      window.history.replaceState(null, '', window.location.pathname);
+    }
+
+    if (editorMode === 'edit') {
+      $('body').addClass('on-edit');
+      $('body').addClass('builtin-editor');
+      window.location.hash = '#edit';
+    }
+
+    if (editorMode === 'hackmd') {
+      $('body').addClass('on-edit');
+      $('body').addClass('hackmd');
+      $('body').removeClass('builtin-editor');
+      window.location.hash = '#hackmd';
+
+    }
+
     this.updateDrawerMode({ ...this.state, editorMode }); // generate newest state object
     this.updateDrawerMode({ ...this.state, editorMode }); // generate newest state object
   }
   }
 
 
@@ -136,7 +158,7 @@ export default class NavigationContainer extends Container {
     } = newState;
     } = newState;
 
 
     // get preference on view or edit
     // get preference on view or edit
-    const preferDrawerMode = editorMode != null ? preferDrawerModeOnEditByUser : preferDrawerModeByUser;
+    const preferDrawerMode = editorMode !== 'view' ? preferDrawerModeOnEditByUser : preferDrawerModeByUser;
 
 
     const isDrawerMode = isDeviceSmallerThanMd || preferDrawerMode;
     const isDrawerMode = isDeviceSmallerThanMd || preferDrawerMode;
     const isDrawerOpened = false; // close Drawer anyway
     const isDrawerOpened = false; // close Drawer anyway

+ 38 - 14
src/client/js/services/PageContainer.js

@@ -39,6 +39,7 @@ export default class PageContainer extends Container {
 
 
     const revisionId = mainContent.getAttribute('data-page-revision-id');
     const revisionId = mainContent.getAttribute('data-page-revision-id');
     const path = decodeURI(mainContent.getAttribute('data-path'));
     const path = decodeURI(mainContent.getAttribute('data-path'));
+
     this.state = {
     this.state = {
       // local page data
       // local page data
       markdown: null, // will be initialized after initStateMarkdown()
       markdown: null, // will be initialized after initStateMarkdown()
@@ -47,20 +48,20 @@ export default class PageContainer extends Container {
       revisionCreatedAt: +mainContent.getAttribute('data-page-revision-created'),
       revisionCreatedAt: +mainContent.getAttribute('data-page-revision-created'),
       path,
       path,
       tocHtml: '',
       tocHtml: '',
-      isLiked: JSON.parse(mainContent.getAttribute('data-page-is-liked')),
-
-      seenUserIds: mainContent.getAttribute('data-page-ids-of-seen-users'),
+      isLiked: false,
+      isBookmarked: false,
       seenUsers: [],
       seenUsers: [],
       countOfSeenUsers: mainContent.getAttribute('data-page-count-of-seen-users'),
       countOfSeenUsers: mainContent.getAttribute('data-page-count-of-seen-users'),
 
 
       likerUsers: [],
       likerUsers: [],
       sumOfLikers: 0,
       sumOfLikers: 0,
-
+      sumOfBookmarks: 0,
       createdAt: mainContent.getAttribute('data-page-created-at'),
       createdAt: mainContent.getAttribute('data-page-created-at'),
       updatedAt: mainContent.getAttribute('data-page-updated-at'),
       updatedAt: mainContent.getAttribute('data-page-updated-at'),
       isForbidden:  JSON.parse(mainContent.getAttribute('data-page-is-forbidden')),
       isForbidden:  JSON.parse(mainContent.getAttribute('data-page-is-forbidden')),
       isDeleted:  JSON.parse(mainContent.getAttribute('data-page-is-deleted')),
       isDeleted:  JSON.parse(mainContent.getAttribute('data-page-is-deleted')),
       isDeletable:  JSON.parse(mainContent.getAttribute('data-page-is-deletable')),
       isDeletable:  JSON.parse(mainContent.getAttribute('data-page-is-deletable')),
+      isCreatable: JSON.parse(mainContent.getAttribute('data-page-is-creatable')),
       isAbleToDeleteCompletely:  JSON.parse(mainContent.getAttribute('data-page-is-able-to-delete-completely')),
       isAbleToDeleteCompletely:  JSON.parse(mainContent.getAttribute('data-page-is-able-to-delete-completely')),
       pageUser: JSON.parse(mainContent.getAttribute('data-page-user')),
       pageUser: JSON.parse(mainContent.getAttribute('data-page-user')),
       tags: null,
       tags: null,
@@ -153,20 +154,43 @@ export default class PageContainer extends Container {
 
 
   async initStateOthers() {
   async initStateOthers() {
 
 
-    const likerListElem = document.getElementById('liker-list');
-    if (likerListElem != null) {
-      const { userIdsStr, sumOfLikers } = likerListElem.dataset;
-      this.setState({ sumOfLikers });
+    this.retrieveLikeInfo();
+    this.retrieveBookmarkInfo();
+    this.checkAndUpdateImageUrlCached(this.state.likerUsers);
+  }
 
 
-      if (userIdsStr === '') {
-        return;
-      }
+  async retrieveLikeInfo() {
+    const like = await this.appContainer.apiv3Get('/page/like-info', { _id: this.state.pageId });
+    this.setState({
+      sumOfLikers: like.data.sumOfLikers,
+      likerUsers: like.data.users.liker,
+      isLiked: like.data.isLiked,
+    });
+  }
+
+  async toggleLike() {
+    const bool = !this.state.isLiked;
+    await this.appContainer.apiv3Put('/page/likes', { pageId: this.state.pageId, bool });
+    this.setState({ isLiked: bool });
 
 
-      const { users } = await this.appContainer.apiGet('/users.list', { user_ids: userIdsStr });
-      this.setState({ likerUsers: users });
+    return this.retrieveLikeInfo();
+  }
 
 
-      this.checkAndUpdateImageUrlCached(users);
+  async retrieveBookmarkInfo() {
+    const response = await this.appContainer.apiv3Get('/bookmarks', { pageId: this.state.pageId });
+    if (response.data.bookmarks != null) {
+      this.setState({ isBookmarked: true });
     }
     }
+    else {
+      this.setState({ isBookmarked: false });
+    }
+    this.setState({ sumOfBookmarks: response.data.sumOfBookmarks });
+  }
+
+  async toggleBookmark() {
+    const bool = !this.state.isBookmarked;
+    await this.appContainer.apiv3Put('/bookmarks', { pageId: this.state.pageId, bool });
+    return this.retrieveBookmarkInfo();
   }
   }
 
 
   async checkAndUpdateImageUrlCached(users) {
   async checkAndUpdateImageUrlCached(users) {

+ 2 - 0
src/client/styles/scss/_layout.scss

@@ -29,6 +29,8 @@ body {
 }
 }
 
 
 .main {
 .main {
+  padding-right: 15px;
+  padding-left: 15px;
   margin-top: 1rem;
   margin-top: 1rem;
 }
 }
 
 

+ 74 - 7
src/client/styles/scss/_mixins.scss

@@ -15,9 +15,8 @@
   }
   }
 }
 }
 
 
-@mixin expand-editor($editor-header-plus-footer, $navbar-height-adjustment: 0px) {
-  $navbar-height: $grw-navbar-border-width + $navbar-height-adjustment;
-  $header-plus-footer: $navbar-height + $editor-header-plus-footer + 2px; // add .main padding-top
+@mixin expand-editor($editor-margin-top) {
+  $header-plus-footer: $editor-margin-top + $grw-editor-navbar-bottom-height;
 
 
   $editor-margin: $header-plus-footer //
   $editor-margin: $header-plus-footer //
     + 25px //   add .btn-open-dropzone height
     + 25px //   add .btn-open-dropzone height
@@ -25,8 +24,7 @@
 
 
   .main {
   .main {
     width: 100%;
     width: 100%;
-    height: calc(100vh - #{$navbar-height});
-    padding-top: 2px;
+    height: calc(100vh - #{$editor-margin-top});
     margin-top: 0px !important;
     margin-top: 0px !important;
 
 
     &,
     &,
@@ -36,8 +34,7 @@
       flex: 1;
       flex: 1;
       flex-direction: column;
       flex-direction: column;
 
 
-      .tab-pane#edit,
-      .tab-pane#hackmd {
+      .tab-pane {
         height: calc(100vh - #{$header-plus-footer});
         height: calc(100vh - #{$header-plus-footer});
         min-height: calc(100vh - #{$header-plus-footer}); // for IE11
         min-height: calc(100vh - #{$header-plus-footer}); // for IE11
       }
       }
@@ -223,3 +220,73 @@
   transition-timing-function: cubic-bezier(0.25, 1, 0.5, 1);
   transition-timing-function: cubic-bezier(0.25, 1, 0.5, 1);
   transition-duration: 300ms;
   transition-duration: 300ms;
 }
 }
+
+@mixin border-vertical($beforeOrAfter, $borderColor, $borderLength, $zIndex: initial, $isBtnGroup: false) {
+  position: relative;
+  @if $isBtnGroup {
+    &:not(:first-child) {
+      margin-left: 0;
+      border-left: none;
+    }
+    &:not(:last-child) {
+      border-right: none;
+    }
+  }
+  &:not(:first-child) {
+    &::#{$beforeOrAfter} {
+      position: absolute;
+      top: calc((100% - #{$borderLength}) / 2);
+      left: 0;
+      z-index: $zIndex;
+      width: 100%;
+      height: $borderLength;
+      margin-left: -0.5px;
+      content: '';
+      border-left: 1px solid $borderColor;
+      transition: border-color 0.15s ease-in-out;
+    }
+  }
+}
+
+@mixin three-stranded-button($textColor, $borderColor, $bgColorHoverAndActive, $bgColor: white) {
+  display: inline-flex;
+  align-items: center;
+  justify-content: center;
+  width: 70px;
+  padding-right: 0;
+  padding-left: 0;
+  color: $textColor;
+  white-space: nowrap;
+  background-color: $bgColor;
+  border-color: $borderColor;
+
+  @include border-vertical('before', $borderColor, 70%, 1, true);
+
+  &.view-button,
+  &.edit-button {
+    .grw-three-stranded-button-icon {
+      margin-right: -0.25rem;
+    }
+  }
+  &.hackmd-button {
+    font-size: 12px;
+    letter-spacing: -0.6px;
+
+    .grw-three-stranded-button-icon {
+      margin-right: -0.1rem;
+    }
+  }
+  &:hover,
+  &:active,
+  &.active {
+    color: $textColor;
+    background-color: $bgColorHoverAndActive;
+    border-color: $borderColor;
+    &::after {
+      border-color: $bgColorHoverAndActive;
+    }
+  }
+  &:not(:disabled):not(.disabled):focus {
+    box-shadow: none;
+  }
+}

+ 12 - 0
src/client/styles/scss/_navbar.scss

@@ -75,3 +75,15 @@
     bottom: -$grw-navbar-bottom-height;
     bottom: -$grw-navbar-bottom-height;
   }
   }
 }
 }
+
+.grw-custom-navigation {
+  .grw-nav-slide-hr {
+    border-bottom: 2px solid;
+    transition: 0.3s ease-in-out;
+  }
+  .nav-link svg {
+    width: 17px;
+    height: 17px;
+    margin-right: 5px;
+  }
+}

+ 46 - 41
src/client/styles/scss/_on-edit.scss

@@ -11,11 +11,24 @@ body:not(.on-edit) {
 body.on-edit {
 body.on-edit {
   overflow-y: hidden !important;
   overflow-y: hidden !important;
 
 
+  .container {
+    max-width: 100%;
+  }
+
   .grw-navbar {
   .grw-navbar {
     position: fixed !important;
     position: fixed !important;
     width: 100vw;
     width: 100vw;
   }
   }
 
 
+  // restrict height of subnav
+  .grw-subnav {
+    max-height: $grw-subnav-max-height-on-edit;
+
+    @include media-breakpoint-up(md) {
+      max-height: $grw-subnav-max-height-md-on-edit;
+    }
+  }
+
   .page-wrapper {
   .page-wrapper {
     position: relative;
     position: relative;
     top: $grw-navbar-border-width;
     top: $grw-navbar-border-width;
@@ -23,11 +36,14 @@ body.on-edit {
   }
   }
 
 
   // calculate margin
   // calculate margin
-  $editor-header-plus-footer: 42px //               .nav-tabs height
-    + 1px //                                        .page-editor-footer border-top
-    + $grw-editor-navbar-bottom-height !default; // .EditorNavbarBottom min-height
+  $editor-margin-top: $grw-navbar-border-width + $grw-subnav-max-height-on-edit;
+  @include expand-editor($editor-margin-top);
 
 
-  @include expand-editor($editor-header-plus-footer);
+  @include media-breakpoint-up(md) {
+    // calculate margin
+    $editor-margin-top: $grw-navbar-border-width + $grw-subnav-max-height-md-on-edit;
+    @include expand-editor($editor-margin-top);
+  }
 
 
   // for growi layout
   // for growi layout
   .main {
   .main {
@@ -47,6 +63,11 @@ body.on-edit {
     }
     }
   }
   }
 
 
+  // hide when Editor
+  .grw-custom-navigation {
+    display: none;
+  }
+
   // hide unnecessary elements
   // hide unnecessary elements
   .d-edit-none {
   .d-edit-none {
     display: none !important;
     display: none !important;
@@ -82,43 +103,6 @@ body.on-edit {
     padding-bottom: 0;
     padding-bottom: 0;
   }
   }
 
 
-  .row.grw-subnav {
-    $left-margin: $grw-nav-main-left-tab-width * 2 + 25px; // width of .grw-nav-main-left-tab x 2 + some margin
-    $right-margin: 128px + 94px + 46px; //                    width of all of grw-nav-main-right-tab
-
-    position: absolute;
-    left: $left-margin;
-    z-index: 7; // forward than .CodeMirror-vscrollbar
-    width: calc(100% - #{$left-margin} - #{$right-margin});
-    padding-top: 3px;
-    pointer-events: none; // disable pointer-events because it becomes an obstacle
-
-    background: none;
-
-    > .grw-subnav-container {
-      width: 100%; //   for crowi layout
-      padding: 0; //    for crowi layout
-      pointer-events: initial; // enable pointer-events
-    }
-  }
-
-  .grw-page-path-nav-for-edit {
-    position: absolute;
-
-    .grw-page-path-link {
-      font-size: 20px;
-      line-height: 1em;
-    }
-    .separator {
-      margin-right: 0.1em;
-      margin-left: 0.1em;
-    }
-  }
-
-  .tag-labels {
-    line-height: 1em;
-  }
-
   .grw-editor-navbar-bottom {
   .grw-editor-navbar-bottom {
     height: $grw-editor-navbar-bottom-height;
     height: $grw-editor-navbar-bottom-height;
 
 
@@ -159,6 +143,27 @@ body.on-edit {
   /*********************
   /*********************
    * Navigation styles
    * Navigation styles
    */
    */
+  // ellipsis .grw-page-path-hierarchical-link
+  .grw-subnav-left-side {
+    overflow: hidden;
+    .grw-path-nav-container {
+      overflow: hidden;
+      .grw-page-path-nav {
+        white-space: nowrap;
+
+        .grw-page-path-hierarchical-link {
+          width: 100%;
+          overflow: hidden;
+          text-overflow: ellipsis;
+        }
+
+        h1 {
+          overflow: hidden;
+        }
+      }
+    }
+  }
+
   .nav:hover {
   .nav:hover {
     .btn-copy,
     .btn-copy,
     .btn-edit,
     .btn-edit,

+ 1 - 0
src/client/styles/scss/_override-bootstrap-variables.scss

@@ -39,6 +39,7 @@ $line-height-base: 1.42857;
 $text-muted: $gray-500;
 $text-muted: $gray-500;
 $blockquote-small-color: $gray-500;
 $blockquote-small-color: $gray-500;
 
 
+
 //== Components
 //== Components
 //
 //
 $border-radius:               .15rem;
 $border-radius:               .15rem;

+ 17 - 4
src/client/styles/scss/_subnav.scss

@@ -1,8 +1,9 @@
 .grw-subnav {
 .grw-subnav {
-  padding: 10px 15px;
+  min-height: $grw-subnav-min-height;
+  padding: 8px 15px;
 
 
   @include media-breakpoint-up(md) {
   @include media-breakpoint-up(md) {
-    min-height: 115px;
+    min-height: $grw-subnav-min-height-md;
   }
   }
 
 
   &:hover {
   &:hover {
@@ -34,9 +35,15 @@
 
 
   .btn-like,
   .btn-like,
   .btn-bookmark {
   .btn-bookmark {
-    width: 40px;
     height: 40px;
     height: 40px;
     font-size: 20px;
     font-size: 20px;
+    border-radius: $border-radius-xl;
+  }
+
+  .total-likes,
+  .total-bookmarks {
+    font-size: 17px;
+    font-weight: $font-weight-bold;
   }
   }
 
 
   ul.authors {
   ul.authors {
@@ -79,9 +86,15 @@
     .btn-bookmark {
     .btn-bookmark {
       @extend .btn-sm;
       @extend .btn-sm;
 
 
-      width: 30px;
       height: 30px;
       height: 30px;
       font-size: 15px !important;
       font-size: 15px !important;
+      border-radius: $border-radius-xl;
+    }
+
+    .total-likes,
+    .total-bookmarks {
+      height: 12px;
+      font-size: 12px;
     }
     }
   }
   }
 }
 }

+ 0 - 2
src/client/styles/scss/_toc.scss

@@ -13,8 +13,6 @@
     font-weight: bolder;
     font-weight: bolder;
   }
   }
   .grw-seen-user-list {
   .grw-seen-user-list {
-    border-left: 1px solid;
-
     .btn {
     .btn {
       white-space: nowrap;
       white-space: nowrap;
     }
     }

+ 5 - 0
src/client/styles/scss/_variables.scss

@@ -9,6 +9,11 @@ $font-family-monospace-not-strictly: Monaco, Menlo, Consolas, 'Courier New', Mei
 $grw-navbar-height: 52px;
 $grw-navbar-height: 52px;
 $grw-navbar-border-width: 3.3333px;
 $grw-navbar-border-width: 3.3333px;
 
 
+$grw-subnav-min-height: 95px;
+$grw-subnav-min-height-md: 115px;
+$grw-subnav-max-height-on-edit: 95px;
+$grw-subnav-max-height-md-on-edit: 115px;
+
 $grw-navbar-bottom-height: 48px;
 $grw-navbar-bottom-height: 48px;
 $grw-editor-navbar-bottom-height: 48px;
 $grw-editor-navbar-bottom-height: 48px;
 
 

+ 6 - 0
src/client/styles/scss/atoms/_buttons.scss

@@ -85,3 +85,9 @@
     }
     }
   }
   }
 }
 }
+
+// Page Management Dropdown icon
+.grw-btn-page-management {
+  background-color: transparent;
+  transition: 0.3s;
+}

+ 19 - 1
src/client/styles/scss/theme/_apply-colors.scss

@@ -274,7 +274,7 @@ pre:not(.hljs):not(.CodeMirror-line) {
     fill: $color-link;
     fill: $color-link;
   }
   }
   .grw-seen-user-list {
   .grw-seen-user-list {
-    border-color: $bordercolor-toc;
+    @include border-vertical('before', $bordercolor-toc, 70%);
 
 
     .btn {
     .btn {
       color: $color-seen-user;
       color: $color-seen-user;
@@ -288,6 +288,18 @@ pre:not(.hljs):not(.CodeMirror-line) {
   }
   }
 }
 }
 
 
+.grw-custom-navigation {
+  .nav-title {
+    color: $color-link;
+  }
+  .nav-link svg {
+    fill: $color-link;
+  }
+  .grw-nav-slide-hr {
+    border-color: $color-link;
+  }
+}
+
 .grw-page-accessories-modal {
 .grw-page-accessories-modal {
   .nav-title {
   .nav-title {
     color: $color-link;
     color: $color-link;
@@ -506,3 +518,9 @@ mark.rbt-highlight-text {
     fill: $gray-900;
     fill: $gray-900;
   }
   }
 }
 }
+
+// Page Management Dropdown icon
+.grw-btn-page-management:hover,
+.grw-btn-page-management:focus {
+  background-color: rgba($color-link, 0.15);
+}

+ 4 - 0
src/client/styles/scss/theme/_reboot-bootstrap-border-colors.scss

@@ -21,3 +21,7 @@
 .border-left {
 .border-left {
   border-left: $border-width solid $border-color !important;
   border-left: $border-width solid $border-color !important;
 }
 }
+
+.border-info {
+  border-color: $info !important;
+}

+ 7 - 0
src/client/styles/scss/theme/antarctic.scss

@@ -112,6 +112,13 @@ html[dark] {
   @import 'apply-colors';
   @import 'apply-colors';
   @import 'apply-colors-light';
   @import 'apply-colors-light';
 
 
+  //Button
+  .btn-group.grw-three-stranded-button {
+    .btn.btn-outline-primary {
+      @include three-stranded-button(darken($primary, 10%), lighten($primary, 55%), lighten($primary, 60%));
+    }
+  }
+
   .table {
   .table {
     background-color: $themelight;
     background-color: $themelight;
   }
   }

+ 7 - 0
src/client/styles/scss/theme/christmas.scss

@@ -182,4 +182,11 @@ html[dark] {
   .grw-navbar {
   .grw-navbar {
     background-image: url('/images/themes/christmas/christmas-navbar.jpg');
     background-image: url('/images/themes/christmas/christmas-navbar.jpg');
   }
   }
+
+  // Button
+  .grw-three-stranded-button {
+    .btn.btn-outline-primary {
+      @include three-stranded-button(darken($subthemecolor, 15%), lighten($subthemecolor, 35%), lighten($subthemecolor, 45%));
+    }
+  }
 }
 }

+ 14 - 0
src/client/styles/scss/theme/default.scss

@@ -103,6 +103,13 @@ html[light] {
 
 
   @import 'apply-colors';
   @import 'apply-colors';
   @import 'apply-colors-light';
   @import 'apply-colors-light';
+
+  // Button
+  .btn-group.grw-three-stranded-button {
+    .btn.btn-outline-primary {
+      @include three-stranded-button($primary, lighten($primary, 65%), lighten($primary, 70%));
+    }
+  }
 }
 }
 
 
 //== Dark Mode
 //== Dark Mode
@@ -196,4 +203,11 @@ html[dark] {
 
 
   @import 'apply-colors';
   @import 'apply-colors';
   @import 'apply-colors-dark';
   @import 'apply-colors-dark';
+
+  //Button
+  .btn-group.grw-three-stranded-button {
+    .btn.btn-outline-primary {
+      @include three-stranded-button(lighten($primary, 30%), lighten($primary, 20%), $primary, darken($primary, 20%));
+    }
+  }
 }
 }

+ 7 - 0
src/client/styles/scss/theme/future.scss

@@ -89,6 +89,13 @@ html[dark] {
   @import 'apply-colors';
   @import 'apply-colors';
   @import 'apply-colors-dark';
   @import 'apply-colors-dark';
 
 
+  //Button
+  .btn-group.grw-three-stranded-button {
+    .btn.btn-outline-primary {
+      @include three-stranded-button(lighten($primary, 10%), $primary, darken($primary, 10%), darken($primary, 20%));
+    }
+  }
+
   // headers
   // headers
   @for $i from 1 through 6 {
   @for $i from 1 through 6 {
     h#{$i} {
     h#{$i} {

+ 7 - 0
src/client/styles/scss/theme/halloween.scss

@@ -107,6 +107,13 @@ html[dark] {
   @import 'apply-colors';
   @import 'apply-colors';
   @import 'apply-colors-dark';
   @import 'apply-colors-dark';
 
 
+  //Button
+  .btn-group.grw-three-stranded-button {
+    .btn.btn-outline-primary {
+      @include three-stranded-button(lighten($primary, 35%), $primary, lighten($primary, 5%), darken($primary, 20%));
+    }
+  }
+
   // Table
   // Table
   .table {
   .table {
     color: $color-global;
     color: $color-global;

+ 7 - 0
src/client/styles/scss/theme/island.scss

@@ -108,4 +108,11 @@ html[dark] {
       }
       }
     }
     }
   }
   }
+
+  // Button
+  .btn-group.grw-three-stranded-button {
+    .btn.btn-outline-primary {
+      @include three-stranded-button(darken($primary, 50%), lighten($primary, 5%), darken($primary, 5%));
+    }
+  }
 }
 }

+ 6 - 0
src/client/styles/scss/theme/kibela.scss

@@ -108,4 +108,10 @@ html[dark] {
 
 
   @import 'apply-colors';
   @import 'apply-colors';
   @import 'apply-colors-light';
   @import 'apply-colors-light';
+  //Button
+  .grw-three-stranded-button {
+    .btn.btn-outline-primary {
+      @include three-stranded-button(darken($primary, 15%), lighten($primary, 45%), lighten($primary, 50%));
+    }
+  }
 }
 }

+ 13 - 0
src/client/styles/scss/theme/mono-blue.scss

@@ -88,6 +88,12 @@ html[light] {
       }
       }
     }
     }
   }
   }
+  // Button
+  .btn-group.grw-three-stranded-button {
+    .btn.btn-outline-primary {
+      @include three-stranded-button($primary, lighten($primary, 65%), lighten($primary, 70%));
+    }
+  }
 }
 }
 
 
 html[dark] {
 html[dark] {
@@ -189,4 +195,11 @@ html[dark] {
   .table {
   .table {
     color: white;
     color: white;
   }
   }
+
+  // Button
+  .btn-group.grw-three-stranded-button {
+    .btn.btn-outline-primary {
+      @include three-stranded-button(lighten($primary, 30%), $primary, darken($primary, 10%), darken($primary, 20%));
+    }
+  }
 }
 }

+ 7 - 0
src/client/styles/scss/theme/nature.scss

@@ -110,4 +110,11 @@ html[dark] {
       color: $color-link-hover !important;
       color: $color-link-hover !important;
     }
     }
   }
   }
+
+  // Button
+  .btn-group.grw-three-stranded-button {
+    .btn.btn-outline-primary {
+      @include three-stranded-button($bgcolor-navbar, lighten($bgcolor-navbar, 65%), lighten($bgcolor-navbar, 70%));
+    }
+  }
 }
 }

+ 7 - 0
src/client/styles/scss/theme/spring.scss

@@ -93,6 +93,13 @@ html[dark] {
   @import 'apply-colors';
   @import 'apply-colors';
   @import 'apply-colors-light';
   @import 'apply-colors-light';
 
 
+  //Button
+  .btn-group.grw-three-stranded-button {
+    .btn.btn-outline-primary {
+      @include three-stranded-button(darken($primary, 50%), lighten($primary, 5%), lighten($primary, 10%));
+    }
+  }
+
   .growi:not(.login-page) {
   .growi:not(.login-page) {
     // add background-image
     // add background-image
     #page-wrapper,
     #page-wrapper,

+ 7 - 0
src/client/styles/scss/theme/wood.scss

@@ -160,4 +160,11 @@ html[dark] {
       border-color: #aaa !important;
       border-color: #aaa !important;
     }
     }
   }
   }
+
+  // Button
+  .btn-group.grw-three-stranded-button {
+    .btn.btn-outline-primary {
+      @include three-stranded-button(darken($primary, 30%), lighten($primary, 15%), lighten($primary, 25%));
+    }
+  }
 }
 }

+ 1 - 1
src/lib/components/PagePathHierarchicalLink.jsx

@@ -46,7 +46,7 @@ const PagePathHierarchicalLink = (props) => {
   const RootElm = ({ children }) => {
   const RootElm = ({ children }) => {
     return props.isInnerElem
     return props.isInnerElem
       ? <>{children}</>
       ? <>{children}</>
-      : <span className="grw-page-path-hierarchical-link text-break">{children}</span>;
+      : <span className="grw-page-path-hierarchical-link d-inline-block text-break">{children}</span>;
   };
   };
 
 
   return (
   return (

+ 0 - 8
src/server/form/admin/importerEsa.js

@@ -1,8 +0,0 @@
-const form = require('express-form');
-
-const field = form.field;
-
-module.exports = form(
-  field('settingForm[importer:esa:access_token]').required(),
-  field('settingForm[importer:esa:team_name]').required(),
-);

+ 0 - 8
src/server/form/admin/importerQiita.js

@@ -1,8 +0,0 @@
-const form = require('express-form');
-
-const field = form.field;
-
-module.exports = form(
-  field('settingForm[importer:qiita:access_token]').required(),
-  field('settingForm[importer:qiita:team_name]').required(),
-);

+ 0 - 15
src/server/form/comment.js

@@ -1,15 +0,0 @@
-const form = require('express-form');
-
-const field = form.field;
-
-module.exports = form(
-  field('commentForm.page_id').trim().required(),
-  field('commentForm.revision_id').trim().required(),
-  field('commentForm.comment').trim().required(),
-  field('commentForm.comment_position').trim().toInt(),
-  field('commentForm.is_markdown').trim().toBooleanStrict(),
-  field('commentForm.replyTo').trim(),
-
-  field('slackNotificationForm.isSlackEnabled').trim().toBooleanStrict().required(),
-  field('slackNotificationForm.slackChannels').trim(),
-);

+ 0 - 2
src/server/form/index.js

@@ -2,8 +2,6 @@ module.exports = {
   login: require('./login'),
   login: require('./login'),
   register: require('./register'),
   register: require('./register'),
   invited: require('./invited'),
   invited: require('./invited'),
-  revision: require('./revision'),
-  comment: require('./comment'),
   admin: {
   admin: {
     userGroupCreate: require('./admin/userGroupCreate'),
     userGroupCreate: require('./admin/userGroupCreate'),
   },
   },

+ 0 - 15
src/server/form/revision.js

@@ -1,15 +0,0 @@
-const form = require('express-form');
-
-const field = form.field;
-
-module.exports = form(
-  field('pageForm.path').required(),
-  field('pageForm.body').required().custom((value) => {
-    // see https://github.com/weseek/growi/issues/463
-    return value.replace(/\r\n?/g, '\n');
-  }),
-  field('pageForm.currentRevision'),
-  field('pageForm.grant').toInt().required(),
-  field('pageForm.grantUserGroupId'),
-  field('pageForm.notify'),
-);

+ 29 - 3
src/server/routes/apiv3/bookmarks.js

@@ -65,6 +65,9 @@ module.exports = (crowi) => {
       body('pageId').isString(),
       body('pageId').isString(),
       body('bool').isBoolean(),
       body('bool').isBoolean(),
     ],
     ],
+    bookmarkInfo: [
+      query('pageId').isMongoId(),
+    ],
   };
   };
 
 
   /**
   /**
@@ -90,12 +93,13 @@ module.exports = (crowi) => {
    *                schema:
    *                schema:
    *                  $ref: '#/components/schemas/Bookmark'
    *                  $ref: '#/components/schemas/Bookmark'
    */
    */
-  router.get('/', accessTokenParser, loginRequired, async(req, res) => {
+  router.get('/', accessTokenParser, loginRequired, validator.bookmarkInfo, async(req, res) => {
     const { pageId } = req.query;
     const { pageId } = req.query;
 
 
     try {
     try {
-      const bookmark = await Bookmark.findByPageIdAndUserId(pageId, req.user);
-      return res.apiv3({ bookmark });
+      const bookmarks = await Bookmark.findByPageIdAndUserId(pageId, req.user);
+      const sumOfBookmarks = await Bookmark.countByPageId(pageId);
+      return res.apiv3({ bookmarks, sumOfBookmarks });
     }
     }
     catch (err) {
     catch (err) {
       logger.error('get-bookmark-failed', err);
       logger.error('get-bookmark-failed', err);
@@ -236,5 +240,27 @@ module.exports = (crowi) => {
     return res.apiv3({ bookmark });
     return res.apiv3({ bookmark });
   });
   });
 
 
+  /**
+   * @swagger
+   *
+   *    /count-bookmarks:
+   *      get:
+   *        tags: [Bookmarks]
+   *        summary: /bookmarks
+   *        description: Count bookmsrks
+   *        requestBody:
+   *          content:
+   *            application/json:
+   *              schema:
+   *                $ref: '#/components/schemas/BookmarkParams'
+   *        responses:
+   *          200:
+   *            description: Succeeded to count bookmarks.
+   *            content:
+   *              application/json:
+   *                schema:
+   *                  $ref: '#/components/schemas/Bookmark'
+   */
+
   return router;
   return router;
 };
 };

+ 21 - 1
src/server/routes/apiv3/page.js

@@ -117,7 +117,7 @@ module.exports = (crowi) => {
   const apiV3FormValidator = require('../../middlewares/apiv3-form-validator')(crowi);
   const apiV3FormValidator = require('../../middlewares/apiv3-form-validator')(crowi);
 
 
   const globalNotificationService = crowi.getGlobalNotificationService();
   const globalNotificationService = crowi.getGlobalNotificationService();
-  const { Page, GlobalNotificationSetting } = crowi.models;
+  const { Page, GlobalNotificationSetting, User } = crowi.models;
   const { exportService } = crowi;
   const { exportService } = crowi;
 
 
   const validator = {
   const validator = {
@@ -125,6 +125,9 @@ module.exports = (crowi) => {
       body('pageId').isString(),
       body('pageId').isString(),
       body('bool').isBoolean(),
       body('bool').isBoolean(),
     ],
     ],
+    likeInfo: [
+      query('_id').isMongoId(),
+    ],
     export: [
     export: [
       query('format').isString().isIn(['md', 'pdf']),
       query('format').isString().isIn(['md', 'pdf']),
       query('revisionId').isString(),
       query('revisionId').isString(),
@@ -196,6 +199,23 @@ module.exports = (crowi) => {
     return res.apiv3({ result });
     return res.apiv3({ result });
   });
   });
 
 
+  router.get('/like-info', loginRequired, validator.likeInfo, async(req, res) => {
+    const pageId = req.query._id;
+    const userId = req.user._id;
+    try {
+      const page = await Page.findById(pageId);
+      const users = await Page.findById(pageId).populate('liker', User.USER_PUBLIC_FIELDS);
+      const sumOfLikers = page.liker.length;
+      const isLiked = page.liker.includes(userId);
+
+      return res.apiv3({ users, sumOfLikers, isLiked });
+    }
+    catch (err) {
+      logger.error('error like info', err);
+      return res.apiv3Err(err, 500);
+    }
+  });
+
   /**
   /**
   * @swagger
   * @swagger
   *
   *

+ 15 - 3
src/server/routes/apiv3/pages.js

@@ -91,12 +91,24 @@ module.exports = (crowi) => {
   ];
   ];
 
 
   router.get('/list', accessTokenParser, loginRequired, validator.displayList, apiV3FormValidator, async(req, res) => {
   router.get('/list', accessTokenParser, loginRequired, validator.displayList, apiV3FormValidator, async(req, res) => {
-    const limit = parseInt(req.query.limit) || await crowi.configManager.getConfig('crowi', 'customize:showPageLimitationS') || 10;
+    const { isTrashPage } = require('@commons/util/path-utils');
+
     const { path } = req.query;
     const { path } = req.query;
-    const page = req.query.page;
+    const limit = parseInt(req.query.limit) || await crowi.configManager.getConfig('crowi', 'customize:showPageLimitationS') || 10;
+    const page = req.query.page || 1;
     const offset = (page - 1) * limit;
     const offset = (page - 1) * limit;
 
 
-    const queryOptions = { offset, limit };
+    let includeTrashed = false;
+
+    if (isTrashPage(path)) {
+      includeTrashed = true;
+    }
+
+    const queryOptions = {
+      offset,
+      limit,
+      includeTrashed,
+    };
 
 
     try {
     try {
       const result = await Page.findListWithDescendants(path, req.user, queryOptions);
       const result = await Page.findListWithDescendants(path, req.user, queryOptions);

+ 0 - 14
src/server/views/_form.html

@@ -1,14 +0,0 @@
-{% if req.form.errors %}
-<div class="alert alert-danger">
-  <ul>
-  {% for error in req.form.errors %}
-    <li>{{ t(error) }}</li>
-  {% endfor %}
-
-  </ul>
-</div>
-{% endif %}
-
-<div id="page-editor-navbar-bottom-container" class="d-none d-edit-block"></div>
-
-<div class="file-module hidden"></div>

+ 2 - 2
src/server/views/layout-growi/base/layout.html

@@ -11,7 +11,7 @@
 {% block content_header_wrapper %}
 {% block content_header_wrapper %}
 <header class="py-0">
 <header class="py-0">
   {% block content_header %}
   {% block content_header %}
-    <div id="grw-subnav-container" class="d-edit-none"></div>
+    <div id="grw-subnav-container"></div>
   {% endblock %}
   {% endblock %}
 </header>
 </header>
 <div id="grw-subnav-switcher-container" class="d-edit-none"></div>
 <div id="grw-subnav-switcher-container" class="d-edit-none"></div>
@@ -19,7 +19,7 @@
 <div id="grw-fav-sticky-trigger" class="sticky-top"></div>
 <div id="grw-fav-sticky-trigger" class="sticky-top"></div>
 {% endblock %}
 {% endblock %}
 
 
-<div id="main" class="main container-fluid {% if page %}{{ css.grant(page) }}{% endif %} {% block main_css_class %}{% endblock %}">
+<div id="main" class="main {% if page %}{{ css.grant(page) }}{% endif %} {% block main_css_class %}{% endblock %}">
   {% block content_main_before %}
   {% block content_main_before %}
   {% endblock %}
   {% endblock %}
 
 

+ 3 - 0
src/server/views/layout-growi/page_list.html

@@ -26,6 +26,9 @@
 
 
 
 
 {% block content_main_after %}
 {% block content_main_after %}
+  {% if isTrashPage() %}
+    <div id="trash-page-list"></div>
+  {% endif %}
   {% if page %}
   {% if page %}
     {% include '../widget/page_attachments.html' %}
     {% include '../widget/page_attachments.html' %}
   {% endif %}
   {% endif %}

+ 7 - 0
src/server/views/layout-growi/widget/liker-and-seenusers.html

@@ -0,0 +1,7 @@
+<div class="liker-and-seenusers">
+  <div
+    id="seen-user-list"
+    data-user-ids-str="{{ page.seenUsers|slice(-15)|default([])|reverse|join(',') }}"
+    data-sum-of-seen-users="{{ page.seenUsers.length|default(0) }}"
+  ></div>
+</div>

+ 2 - 2
src/server/views/widget/forbidden_content.html

@@ -22,7 +22,7 @@
 
 
   <ul class="nav nav-tabs d-print-none" role="tablist">
   <ul class="nav nav-tabs d-print-none" role="tablist">
     <li class="nav-item grw-nav-main-left-tab">
     <li class="nav-item grw-nav-main-left-tab">
-      <a class="nav-link active" role="tab" href="#revision-body" data-toggle="tab">
+      <a class="nav-link active">
         <i class="icon-notebook"></i> List
         <i class="icon-notebook"></i> List
       </a>
       </a>
     </li>
     </li>
@@ -30,7 +30,7 @@
 
 
   <div class="tab-content">
   <div class="tab-content">
     {# list view #}
     {# list view #}
-    <div class="pt-2 active tab-pane page-list-container" id="revision-body">
+    <div class="pt-2 active tab-pane page-list-container">
       {% if pages.length == 0 %}
       {% if pages.length == 0 %}
         <div class="mt-2">
         <div class="mt-2">
           There are no pages under <strong>{{ path | preventXss }}</strong>.
           There are no pages under <strong>{{ path | preventXss }}</strong>.

+ 2 - 22
src/server/views/widget/not_creatable_content.html

@@ -10,27 +10,7 @@
 <div id="content-main" class="content-main page-list"
 <div id="content-main" class="content-main page-list"
   data-path="{{ encodeURI(path) }}"
   data-path="{{ encodeURI(path) }}"
   data-current-user="{% if user %}{{ user._id.toString() }}{% endif %}"
   data-current-user="{% if user %}{{ user._id.toString() }}{% endif %}"
-  >
+  data-page-is-creatable="true"
+  ></div>
 
 
-  <ul class="nav nav-tabs d-print-none" role="tablist">
-    <li class="nav-item grw-nav-main-left-tab active" role="presentation">
-      <a class="nav-link active" role="tab" href="#revision-body" data-toggle="tab">
-        <i class="icon-notebook"></i> List
-      </a>
-    </li>
-  </ul>
-
-  <div class="tab-content">
-    {# list view #}
-    <div class="pt-2 active tab-pane page-list-container" id="revision-body">
-      {% if pages.length == 0 %}
-        <div class="mt-2">
-          There are no pages under <strong>{{ path | preventXss }}</strong>.
-        </div>
-      {% endif  %}
-
-      {% include '../widget/page_list.html' with { pages: pages, pager: pager, viewConfig: viewConfig } %}
-    </div>
-
-  </div>
 </div>
 </div>

+ 2 - 32
src/server/views/widget/not_found_content.html

@@ -1,12 +1,3 @@
-<div class="row not-found-message-row mb-4 d-edit-none">
-  <div class="col-md-12">
-    <h2 class="text-muted">
-      <i class="icon-info" aria-hidden="true"></i>
-      Page is not found
-    </h2>
-  </div>
-</div>
-
 <div id="content-main" class="content-main page-list"
 <div id="content-main" class="content-main page-list"
   data-path="{{ encodeURI(path) }}"
   data-path="{{ encodeURI(path) }}"
   data-current-user="{% if user %}{{ user._id.toString() }}{% endif %}"
   data-current-user="{% if user %}{{ user._id.toString() }}{% endif %}"
@@ -14,9 +5,8 @@
     data-template-tags="{{ templateTags }}"
     data-template-tags="{{ templateTags }}"
   {% endif %}
   {% endif %}
   >
   >
-
-  {% include 'not_found_tabs.html' %}
-
+  <div id="display-switcher"></div>
+  <div id="not-found-page"></div>
   <div class="tab-content">
   <div class="tab-content">
 
 
 
 
@@ -35,26 +25,6 @@
     {% endif %}
     {% endif %}
 
 
     {# TODO: should be removed and transplanted to PageContainer.initStateMarkdown ------ to here ------ #}
     {# TODO: should be removed and transplanted to PageContainer.initStateMarkdown ------ to here ------ #}
-
-
-
-    {# list view #}
-    <div class="pt-2 active tab-pane page-list-container" id="revision-body">
-      {% if pages.length == 0 %}
-        <div class="mt-2">
-          There are no pages under <strong>{{ path | preventXss }}</strong>.
-        </div>
-      {% endif  %}
-
-      {% include '../widget/page_list.html' with { pages: pages, pager: pager, viewConfig: viewConfig } %}
-    </div>
-
-    {# edit view #}
-    <div class="tab-pane" id="edit">
-      <div id="page-editor">{% if pageForm.body %}{{ pageForm.body }}{% endif %}</div>
-    </div>
-    {% include '../_form.html' %}
-
   </div>
   </div>
 
 
   <div id="grw-page-status-alert-container"></div>
   <div id="grw-page-status-alert-container"></div>

+ 0 - 20
src/server/views/widget/not_found_tabs.html

@@ -1,20 +0,0 @@
-<ul class="nav nav-tabs d-print-none" role="tablist">
-  <li class="nav-item grw-nav-main-left-tab">
-    <a class="nav-link active" role="tab" href="#revision-body" data-toggle="tab">
-      <i class="icon-notebook"></i> List
-    </a>
-  </li>
-
-  {% if !isTrashPage() and !page.isDeleted() %}
-  <li class="nav-item grw-nav-main-left-tab">
-    <a
-      {% if user %} href="#edit" role="tab" data-toggle="tab" class="nav-link edit-button" {% endif %}
-      {% if not user %} class="nav-link edit-button edit-button-disabled" data-toggle="tooltip" data-placement="top" data-container="body" title="{{ t('Not available for guest') }}" {% endif %}
-    >
-      <i class="icon-note"></i> {{ t('Create') }}
-    </a>
-  </li>
-
-  <div id="page-editor-path-nav" class="d-none d-edit-sm-block ml-2"></div>
-  {% endif %}
-</ul>

+ 3 - 0
src/server/views/widget/page_alerts.html

@@ -80,5 +80,8 @@
     {% if isTrashPage() %}
     {% if isTrashPage() %}
       <div id="trash-page-alert"></div>
       <div id="trash-page-alert"></div>
     {% endif %}
     {% endif %}
+    {% if !page %}
+      <div id="not-found-alert"></div>
+    {% endif %}
   </div>
   </div>
 </div>
 </div>

+ 8 - 34
src/server/views/widget/page_content.html

@@ -1,5 +1,5 @@
 {% if page %}
 {% if page %}
-<div id="content-main" class="content-main"
+<div id="content-main" class="content-main container"
   data-path="{{ encodeURI(page.path) }}"
   data-path="{{ encodeURI(page.path) }}"
   data-current-user="{% if user %}{{ user._id.toString() }}{% endif %}"
   data-current-user="{% if user %}{{ user._id.toString() }}{% endif %}"
   data-page-id="{% if page %}{{ page._id.toString() }}{% endif %}"
   data-page-id="{% if page %}{{ page._id.toString() }}{% endif %}"
@@ -17,6 +17,7 @@
   data-page-is-forbidden="{% if forbidden %}true{% else %}false{% endif %}"
   data-page-is-forbidden="{% if forbidden %}true{% else %}false{% endif %}"
   data-page-is-deleted="{% if page.isDeleted() %}true{% else %}false{% endif %}"
   data-page-is-deleted="{% if page.isDeleted() %}true{% else %}false{% endif %}"
   data-page-is-deletable="{% if isDeletablePage() %}true{% else %}false{% endif %}"
   data-page-is-deletable="{% if isDeletablePage() %}true{% else %}false{% endif %}"
+  data-page-is-creatable="false"
   data-page-is-able-to-delete-completely="{% if user.canDeleteCompletely(page.creator._id) %}true{% else %}false{% endif %}"
   data-page-is-able-to-delete-completely="{% if user.canDeleteCompletely(page.creator._id) %}true{% else %}false{% endif %}"
   data-slack-channels="{{ slack|default('') }}"
   data-slack-channels="{{ slack|default('') }}"
   data-page-created-at="{% if page %}{{ page.createdAt|datetz('Y/m/d H:i:s') }}{% endif %}"
   data-page-created-at="{% if page %}{{ page.createdAt|datetz('Y/m/d H:i:s') }}{% endif %}"
@@ -42,38 +43,11 @@
 
 
   {% include 'page_alerts.html' %}
   {% include 'page_alerts.html' %}
 
 
-  {% include 'page_tabs.html' %}
-
-  <div class="tab-content">
-
-    {% if page %}
-      <script type="text/template" id="raw-text-original">{{ revision.body.toString() | encodeHTML }}</script>
-
-      {# formatted text #}
-      <div class="tab-pane active" id="revision-body">
-        <div id="page" class="mt-4"></div>
-      </div>
-    {% endif %}
-
-    {% if !isTrashPage() %}
-      {# edit form #}
-      <div class="tab-pane" id="edit">
-        <div id="page-editor">{% if pageForm.body %}{{ pageForm.body }}{% endif %}</div>
-      </div>
-      <div class="tab-pane" id="hackmd">
-        <div id="page-editor-with-hackmd"></div>
-      </div>
-      {% include '../_form.html' %}
-    {% endif %}
-
-    {# raw revision history #}
-    {% if not page %}
-    {% else %}
-    <div class="tab-pane revision-history" id="revision-history">
-    </div>
-    {% endif %}
-
-  </div>
+<div id="display-switcher">
+  <script type="text/template" id="raw-text-original">{{ revision.body.toString() | encodeHTML }}</script>
+</div>
+<div id="page-editor-navbar-bottom-container" class="d-none d-edit-block"></div>
+<div id="grw-page-status-alert-container"></div>
 
 
-  <div id="grw-page-status-alert-container"></div>
 </div>
 </div>
+

+ 1 - 1
src/server/views/widget/page_list.html

@@ -37,7 +37,7 @@
     <span class="page-list-seer" data-count="{{ listPage.seenUsers.length }}">
     <span class="page-list-seer" data-count="{{ listPage.seenUsers.length }}">
       <i class="fa fa-paw"></i>{{ listPage.seenUsers.length }}
       <i class="fa fa-paw"></i>{{ listPage.seenUsers.length }}
     </span>
     </span>
-    {% endif  %}
+    {% endif %}
 
 
     {% if !listPage.isPublic() %}
     {% if !listPage.isPublic() %}
     <span>
     <span>

+ 0 - 56
src/server/views/widget/page_tabs.html

@@ -1,56 +0,0 @@
-{% if page %}
-<ul class="nav nav-tabs d-print-none" role="tablist">
-
-  {#
-    Left Tabs
-  #}
-  <li class="nav-item grw-main-nav-item-left">
-    <a class="nav-link active" href="#revision-body" role="tab" data-toggle="tab">
-      <i class="icon-control-play icon-fw"></i><span class="d-none d-md-inline">View</span>
-    </a>
-  </li>
-
-  {% if !isTrashPage() %}
-    <li class="nav-item grw-main-nav-item-left grw-nav-item-edit">
-      <a
-        {% if user %} href="#edit" data-toggle="tab" class="nav-link edit-button" {% endif %}
-        {% if not user %}
-          class="nav-link edit-button edit-button-disabled"
-          data-toggle="tooltip" data-placement="top" data-container="body" title="{{ t('Not available for guest') }}"
-        {% endif %}
-      >
-        <i class="icon-note icon-fw"></i><span class="d-none d-md-inline">{{ t('Edit') }}</span>
-      </a>
-    </li>
-
-    {% if isHackmdSetup() %}
-    <li class="nav-item grw-main-nav-item-left grw-nav-tab-hackmd">
-      <a
-        {% if user %} href="#hackmd" data-toggle="tab" class="nav-link edit-button" {% endif %}
-        {% if not user %}
-          class="nav-link edit-button edit-button-disabled"
-          data-toggle="tooltip" data-placement="top" data-container="body" title="{{ t('Not available for guest') }}"
-        {% endif %}
-      >
-        <i class="fa fa-fw fa-file-text-o"></i><span class="d-none d-md-inline">{{ t('HackMD') }}</span>
-      </a>
-    </li>
-    {% endif %}
-
-    <div id="page-editor-path-nav" class="d-none d-edit-sm-block ml-2"></div>
-  {% endif %}
-
-  {#
-    Right Tabs
-  #}
-
-  {# to place right side #}
-  <div class="mr-auto"></div>
-
-  <!-- icon-options-vertical -->
-  {% if !isTrashPage() %}
-    <li id="page-management" class="nav-item dropdown d-edit-none"></li>
-  {% endif %}
-</ul>
-
-{% endif %}