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

Merge branch 'support/apply-bootstrap4' into support/fix-label-layout

akira-s 6 лет назад
Родитель
Сommit
df2c76438e
90 измененных файлов с 1271 добавлено и 903 удалено
  1. 14 1
      CHANGES.md
  2. 1 2
      package.json
  3. 135 0
      resource/locales/ja/sandbox-bootstrap3.md
  4. 2 2
      resource/locales/ja/translation.json
  5. 2 0
      src/client/js/app.jsx
  6. 1 1
      src/client/js/components/Admin/ExportArchiveData/ArchiveFilesTableMenu.jsx
  7. 3 3
      src/client/js/components/Admin/ExportArchiveData/SelectCollectionsModal.jsx
  8. 1 1
      src/client/js/components/Admin/ExportArchiveDataPage.jsx
  9. 1 1
      src/client/js/components/Admin/ImportData/GrowiArchive/ImportCollectionConfigurationModal.jsx
  10. 1 1
      src/client/js/components/Admin/ImportData/GrowiArchive/ImportCollectionItem.jsx
  11. 3 3
      src/client/js/components/Admin/ImportData/GrowiArchive/ImportForm.jsx
  12. 2 2
      src/client/js/components/Admin/ImportDataPage.jsx
  13. 1 1
      src/client/js/components/Admin/ManageExternalAccount.jsx
  14. 13 3
      src/client/js/components/Admin/MarkdownSetting/PresentationForm.jsx
  15. 1 1
      src/client/js/components/Admin/Notification/GlobalNotification.jsx
  16. 21 19
      src/client/js/components/Admin/Notification/GlobalNotificationList.jsx
  17. 110 81
      src/client/js/components/Admin/Notification/ManageGlobalNotification.jsx
  18. 4 4
      src/client/js/components/Admin/Notification/UserNotificationRow.jsx
  19. 21 13
      src/client/js/components/Admin/Notification/UserTriggerNotification.jsx
  20. 1 1
      src/client/js/components/Admin/Security/GitHubSecuritySetting.jsx
  21. 1 1
      src/client/js/components/Admin/Security/LdapAuthTest.jsx
  22. 7 2
      src/client/js/components/Admin/Security/LdapSecuritySetting.jsx
  23. 1 1
      src/client/js/components/Admin/Security/LocalSecuritySetting.jsx
  24. 2 2
      src/client/js/components/Admin/Security/SecuritySetting.jsx
  25. 1 1
      src/client/js/components/Admin/UserGroup/UserGroupCreateForm.jsx
  26. 2 2
      src/client/js/components/Admin/UserGroup/UserGroupTable.jsx
  27. 1 1
      src/client/js/components/Admin/UserGroupDetail/UserGroupDetailPage.jsx
  28. 2 2
      src/client/js/components/Admin/UserGroupDetail/UserGroupUserTable.jsx
  29. 3 3
      src/client/js/components/Admin/UserManagement.jsx
  30. 1 1
      src/client/js/components/Admin/Users/ExternalAccountTable.jsx
  31. 1 1
      src/client/js/components/Admin/Users/InviteUserControl.jsx
  32. 2 2
      src/client/js/components/Admin/Users/UserMenu.jsx
  33. 1 1
      src/client/js/components/Admin/Users/UserTable.jsx
  34. 31 39
      src/client/js/components/BookmarkButton.jsx
  35. 6 18
      src/client/js/components/LikeButton.jsx
  36. 1 1
      src/client/js/components/Me/DisassociateModal.jsx
  37. 1 1
      src/client/js/components/Me/ExternalAccountLinkedMe.jsx
  38. 1 1
      src/client/js/components/Me/ExternalAccountRow.jsx
  39. 8 12
      src/client/js/components/Me/ImageCropModal.jsx
  40. 1 1
      src/client/js/components/MyDraftList/MyDraftList.jsx
  41. 3 3
      src/client/js/components/Navbar/GrowiSubNavigation.jsx
  42. 85 0
      src/client/js/components/Navbar/GrowiSubNavigationForUserPage.jsx
  43. 13 1
      src/client/js/components/Page/RevisionBody.jsx
  44. 1 1
      src/client/js/components/Page/RevisionPath.jsx
  45. 1 1
      src/client/js/components/PageComment/Comment.jsx
  46. 2 2
      src/client/js/components/PageEditor/Editor.jsx
  47. 2 2
      src/client/js/components/PageEditor/OptionsSelector.jsx
  48. 1 1
      src/client/js/components/PageEditorByHackmd.jsx
  49. 1 1
      src/client/js/components/SearchPage/SearchPageForm.jsx
  50. 2 2
      src/client/js/components/SearchPage/SearchResult.jsx
  51. 1 1
      src/client/js/components/SearchPage/SearchResultList.jsx
  52. 71 59
      src/client/js/components/Sidebar.jsx
  53. 53 0
      src/client/js/components/Sidebar/CustomSidebar.jsx
  54. 53 0
      src/client/js/components/Sidebar/History.jsx
  55. 26 25
      src/client/js/components/Sidebar/SidebarNav.jsx
  56. 2 2
      src/client/js/services/PageContainer.js
  57. 3 1
      src/client/styles/scss/_comment_growi.scss
  58. 1 0
      src/client/styles/scss/_layout_kibela.scss
  59. 1 1
      src/client/styles/scss/_login.scss
  60. 1 1
      src/client/styles/scss/_navbar.scss
  61. 1 0
      src/client/styles/scss/_on-edit.scss
  62. 8 8
      src/client/styles/scss/_override-bootstrap-variables.scss
  63. 9 0
      src/client/styles/scss/_override-bootstrap.scss
  64. 2 0
      src/client/styles/scss/_page_growi.scss
  65. 0 3
      src/client/styles/scss/_vendor.scss
  66. 4 3
      src/server/models/page.js
  67. 157 0
      src/server/routes/apiv3/bookmarks.js
  68. 4 2
      src/server/routes/apiv3/index.js
  69. 187 0
      src/server/routes/apiv3/page.js
  70. 0 218
      src/server/routes/bookmark.js
  71. 0 6
      src/server/routes/index.js
  72. 7 2
      src/server/routes/installer.js
  73. 0 144
      src/server/routes/page.js
  74. 1 1
      src/server/views/layout-crowi/page_list.html
  75. 11 6
      src/server/views/layout-growi/base/layout.html
  76. 1 1
      src/server/views/layout-growi/page.html
  77. 2 2
      src/server/views/layout-growi/page_list.html
  78. 2 2
      src/server/views/layout-growi/user_page.html
  79. 1 1
      src/server/views/layout-kibela/base/layout.html
  80. 1 1
      src/server/views/layout-kibela/user_page.html
  81. 1 1
      src/server/views/layout/layout.html
  82. 69 67
      src/server/views/login.html
  83. 2 2
      src/server/views/modal/create_template.html
  84. 1 1
      src/server/views/modal/duplicate.html
  85. 2 2
      src/server/views/widget/page_alerts.html
  86. 1 1
      src/server/views/widget/page_list.html
  87. 6 6
      src/server/views/widget/page_tabs.html
  88. 49 41
      src/server/views/widget/page_tabs_kibela.html
  89. 0 34
      src/server/views/widget/user_page_header.html
  90. 7 11
      yarn.lock

+ 14 - 1
CHANGES.md

@@ -5,10 +5,23 @@
 * Support: Upgrade libs
     * bootstrap
 
-## v3.7.3-RC
+## v3.7.4-RC
 
 *
 
+## v3.7.3
+
+* Feature: Profile Image Cropping
+* Improvement: Reactify users pages
+* Improvement: Detect language and adjust the order of first and last names when creating accounts in OAuth
+* Fix: Installation is broken when selecting Japanese
+    * Introduced by 3.7.0
+* Fix: Mathjax Rendering is unstable (workaround)
+    * Introduced by 3.7.0
+* Fix: Notification Setting couldn't update without slack token
+    * Introduced by 3.6.6
+* Support: Add GROWI Contributers
+
 ## v3.7.2
 
 * Feature: User Management Filtering/Sort

+ 1 - 2
package.json

@@ -1,6 +1,6 @@
 {
   "name": "growi",
-  "version": "3.7.3-RC",
+  "version": "4.0.0-RC",
   "description": "Team collaboration software using markdown",
   "tags": [
     "wiki",
@@ -208,7 +208,6 @@
     "markdown-it-task-checkbox": "^1.0.6",
     "markdown-it-toc-and-anchor-with-slugid": "^1.1.4",
     "markdown-table": "^1.1.1",
-    "metismenu": "^3.0.3",
     "mini-css-extract-plugin": "^0.9.0",
     "morgan": "^1.9.0",
     "node-dev": "^4.0.0",

+ 135 - 0
resource/locales/ja/sandbox-bootstrap3.md

@@ -0,0 +1,135 @@
+# Labels
+
+<span class="label label-default">Default</span>
+<span class="label label-primary">Primary</span>
+<span class="label label-success">Success</span>
+<span class="label label-info">Info</span>
+<span class="label label-warning">Warning</span>
+<span class="label label-danger">Danger</span>
+
+# Alerts
+
+<div class="alert alert-success" role="alert"><b>Well done!</b> You successfully read this important alert message. </div>
+<div class="alert alert-info" role="alert"><b>Heads up!</b> This alert needs your attention, but it's not super important. </div>
+<div class="alert alert-warning" role="alert"><b>Warning!</b> Better check yourself, you're not looking too good. </div>
+<div class="alert alert-danger" role="alert"><b>Oh snap!</b> Change a few things up and try submitting again. </div>
+
+# Panels
+
+<div class="panel panel-default">
+  <div class="panel-heading">Panel heading without title</div>
+  <div class="panel-body">
+    Panel content
+  </div>
+</div>
+
+<div class="panel panel-primary">
+  <div class="panel-heading">Panel heading without title</div>
+  <div class="panel-body">
+    Panel content
+  </div>
+</div>
+
+<div class="panel panel-success">
+  <div class="panel-heading">Panel heading without title</div>
+  <div class="panel-body">
+    Panel content
+  </div>
+</div>
+
+<div class="panel panel-info">
+  <div class="panel-heading">Panel heading without title</div>
+  <div class="panel-body">
+    Panel content
+  </div>
+</div>
+
+<div class="panel panel-warning">
+  <div class="panel-heading">Panel heading without title</div>
+  <div class="panel-body">
+    Panel content
+  </div>
+</div>
+
+<div class="panel panel-danger">
+  <div class="panel-heading">Panel heading without title</div>
+  <div class="panel-body">
+    Panel content
+  </div>
+</div>
+
+# Wells
+
+## Default well
+
+<div class="well">Look, I'm in a well! </div>
+
+## Optional classes
+
+<div class="well well-lg">Look, I'm in a well! </div>
+
+<div class="well well-sm">Look, I'm in a well! </div>
+
+# Typography
+
+## Lead body copy
+
+<p class="lead">Vivamus sagittis lacus vel augue laoreet rutrum faucibus dolor auctor. Duis mollis, est non commodo luctus.</p>
+
+## Marked text
+
+You can use the mark tag to <mark>highlight</mark> text.
+
+## Small text
+
+<small>This line of text is meant to be treated as fine print.</small>
+
+## Alignment classes
+
+<div class="panel panel-default">
+  <div class="panel-body">
+    <p class="text-left">Left aligned text.</p>
+    <p class="text-center">Center aligned text.</p>
+    <p class="text-right">Right aligned text.</p>
+    <p class="text-justify">Justified text.</p>
+    <p class="text-nowrap">No wrap text.</p>
+  </div>
+</div>
+
+## Transformation classes
+
+<div class="panel panel-default">
+  <div class="panel-body">
+    <p class="text-lowercase">Lowercased text.</p>
+    <p class="text-uppercase">Uppercased text.</p>
+    <p class="text-capitalize">Capitalized text.</p>
+  </div>
+</div>
+
+
+# Helper classes
+
+## Contextual colors
+
+<div class="panel panel-default">
+  <div class="panel-body">
+    <p class="text-muted">Fusce dapibus, tellus ac cursus commodo, tortor mauris nibh.</p>
+    <p class="text-primary">Nullam id dolor id nibh ultricies vehicula ut id elit.</p>
+    <p class="text-success">Duis mollis, est non commodo luctus, nisi erat porttitor ligula.</p>
+    <p class="text-info">Maecenas sed diam eget risus varius blandit sit amet non magna.</p>
+    <p class="text-warning">Etiam porta sem malesuada magna mollis euismod.</p>
+    <p class="text-danger">Donec ullamcorper nulla non metus auctor fringilla.</p>
+  </div>
+</div>
+
+## Contextual backgrounds
+
+<div class="panel panel-default">
+  <div class="panel-body">
+    <p class="bg-primary">Nullam id dolor id nibh ultricies vehicula ut id elit.</p>
+    <p class="bg-success">Duis mollis, est non commodo luctus, nisi erat porttitor ligula.</p>
+    <p class="bg-info">Maecenas sed diam eget risus varius blandit sit amet non magna.</p>
+    <p class="bg-warning">Etiam porta sem malesuada magna mollis euismod.</p>
+    <p class="bg-danger">Donec ullamcorper nulla non metus auctor fringilla.</p>
+  </div>
+</div>

+ 2 - 2
resource/locales/ja/translation.json

@@ -257,9 +257,9 @@
       "Redirect": "リダイレクトする"
     },
     "help": {
-      "redirect": "<code>%s</code> にアクセスされた際に自動的に新しいページにジャンプします",
+      "redirect": "<code class='text-break'>%s</code> にアクセスされた際に自動的に新しいページにジャンプします",
       "metadata": "最終更新ユーザー、最終更新日を更新せず維持します",
-      "recursive": "<code>%s</code> 配下のページも移動/名前変更します"
+      "recursive": "<code class='text-break'>%s</code> 配下のページも移動/名前変更します"
     }
   },
   "Put Back": "元に戻す",

+ 2 - 0
src/client/js/app.jsx

@@ -35,6 +35,7 @@ import CommentContainer from './services/CommentContainer';
 import EditorContainer from './services/EditorContainer';
 import TagContainer from './services/TagContainer';
 import GrowiSubNavigation from './components/Navbar/GrowiSubNavigation';
+import GrowiSubNavigationForUserPage from './components/Navbar/GrowiSubNavigationForUserPage';
 import PersonalContainer from './services/PersonalContainer';
 
 import { appContainer, componentMappings } from './bootstrap';
@@ -101,6 +102,7 @@ if (pageContainer.state.path != null) {
     'revision-path': <RevisionPath behaviorType={appContainer.config.behaviorType} pageId={pageContainer.state.pageId} pagePath={pageContainer.state.path} />,
     'tag-label': <TagLabels />,
     'grw-subnav': <GrowiSubNavigation />,
+    'grw-subnav-for-user-page': <GrowiSubNavigationForUserPage />,
   });
 }
 

+ 1 - 1
src/client/js/components/Admin/ExportArchiveData/ArchiveFilesTableMenu.jsx

@@ -13,7 +13,7 @@ class ArchiveFilesTableMenu extends React.Component {
 
     return (
       <div className="btn-group admin-user-menu">
-        <button type="button" className="btn btn-sm btn-light dropdown-toggle" data-toggle="dropdown">
+        <button type="button" className="btn btn-sm btn-outline-secondary dropdown-toggle" data-toggle="dropdown">
           <i className="icon-settings"></i> <span className="caret"></span>
         </button>
         <ul className="dropdown-menu" role="menu">

+ 3 - 3
src/client/js/components/Admin/ExportArchiveData/SelectCollectionsModal.jsx

@@ -183,10 +183,10 @@ class SelectCollectionsModal extends React.Component {
           <ModalBody>
             <div className="row">
               <div className="col-sm-12">
-                <button type="button" className="btn btn-sm btn-light mr-2" onClick={this.checkAll}>
+                <button type="button" className="btn btn-sm btn-outline-secondary mr-2" onClick={this.checkAll}>
                   <i className="fa fa-check-square-o"></i> {t('admin:export_management.check_all')}
                 </button>
-                <button type="button" className="btn btn-sm btn-light mr-2" onClick={this.uncheckAll}>
+                <button type="button" className="btn btn-sm btn-outline-secondary mr-2" onClick={this.uncheckAll}>
                   <i className="fa fa-square-o"></i> {t('admin:export_management.uncheck_all')}
                 </button>
               </div>
@@ -219,7 +219,7 @@ class SelectCollectionsModal extends React.Component {
           </ModalBody>
 
           <ModalFooter>
-            <button type="button" className="btn btn-sm btn-light" onClick={this.props.onClose}>{t('export_management.cancel')}</button>
+            <button type="button" className="btn btn-sm btn-outline-secondary" onClick={this.props.onClose}>{t('export_management.cancel')}</button>
             <button type="submit" className="btn btn-sm btn-primary" disabled={!this.validateForm()}>{t('export_management.export')}</button>
           </ModalFooter>
         </form>

+ 1 - 1
src/client/js/components/Admin/ExportArchiveDataPage.jsx

@@ -213,7 +213,7 @@ class ExportArchiveDataPage extends React.Component {
       <Fragment>
         <h2>{t('Export Archive Data')}</h2>
 
-        <button type="button" className="btn btn-light" disabled={isExporting} onClick={this.openExportModal}>
+        <button type="button" className="btn btn-outline-secondary" disabled={isExporting} onClick={this.openExportModal}>
           {t('admin:export_management.create_new_archive_data')}
         </button>
 

+ 1 - 1
src/client/js/components/Admin/ImportData/GrowiArchive/ImportCollectionConfigurationModal.jsx

@@ -213,7 +213,7 @@ class ImportCollectionConfigurationModal extends React.Component {
         </ModalBody>
 
         <ModalFooter>
-          <button type="button" className="btn btn-sm btn-light" onClick={this.props.onClose}>{t('Cancel')}</button>
+          <button type="button" className="btn btn-sm btn-outline-secondary" onClick={this.props.onClose}>{t('Cancel')}</button>
           <button type="button" className="btn btn-sm btn-primary" onClick={this.updateOption}>{t('Update')}</button>
         </ModalFooter>
       </Modal>

+ 1 - 1
src/client/js/components/Admin/ImportData/GrowiArchive/ImportCollectionItem.jsx

@@ -149,7 +149,7 @@ export default class ImportCollectionItem extends React.Component {
     return (
       <button
         type="button"
-        className="btn btn-light btn-sm p-1 ml-2"
+        className="btn btn-outline-secondary btn-sm p-1 ml-2"
         disabled={isImporting || !isConfigButtonAvailable}
         onClick={isConfigButtonAvailable ? this.configButtonClickedHandler : null}
       >

+ 3 - 3
src/client/js/components/Admin/ImportData/GrowiArchive/ImportForm.jsx

@@ -455,12 +455,12 @@ class ImportForm extends React.Component {
       <>
         <form className="form-inline">
           <div className="form-group">
-            <button type="button" className="btn btn-sm btn-light mr-2" onClick={this.checkAll}>
+            <button type="button" className="btn btn-sm btn-outline-secondary mr-2" onClick={this.checkAll}>
               <i className="fa fa-check-square-o"></i> {t('admin:export_management.check_all')}
             </button>
           </div>
           <div className="form-group">
-            <button type="button" className="btn btn-sm btn-light mr-2" onClick={this.uncheckAll}>
+            <button type="button" className="btn btn-sm btn-outline-secondary mr-2" onClick={this.uncheckAll}>
               <i className="fa fa-square-o"></i> {t('admin:export_management.uncheck_all')}
             </button>
           </div>
@@ -472,7 +472,7 @@ class ImportForm extends React.Component {
         {this.renderOthers()}
 
         <div className="mt-4 text-center">
-          <button type="button" className="btn btn-light mx-1" onClick={this.props.onDiscard}>
+          <button type="button" className="btn btn-outline-secondary mx-1" onClick={this.props.onDiscard}>
             {t('admin:importer_management.growi_settings.discard')}
           </button>
           <button type="button" className="btn btn-primary mx-1" onClick={this.import} disabled={!canImport || isImporting}>

+ 2 - 2
src/client/js/components/Admin/ImportDataPage.jsx

@@ -216,7 +216,7 @@ class ImportDataPage extends React.Component {
                     name="Esa"
                     type="button"
                     id="importFromEsa"
-                    className="btn btn-light btn-esa"
+                    className="btn btn-outline-secondary btn-esa"
                     onClick={this.esaHandleSubmitTest}
                     value={t('admin:importer_management.esa_settings.test_connection')}
                   />
@@ -309,7 +309,7 @@ class ImportDataPage extends React.Component {
                     name="Qiita"
                     type="button"
                     id="importFromQiita"
-                    className="btn btn-light btn-qiita"
+                    className="btn btn-outline-secondary btn-qiita"
                     onClick={this.qiitaHandleSubmitTest}
                     value={t('admin:importer_management.qiita_settings.test_connection')}
                   />

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

@@ -48,7 +48,7 @@ class ManageExternalAccount extends React.Component {
     return (
       <Fragment>
         <p>
-          <a className="btn btn-default" href="/admin/users">
+          <a className="btn btn-outline-secondary" href="/admin/users">
             <i className="icon-fw ti-arrow-left" aria-hidden="true"></i>
             {t('admin:user_management.back_to_user_management')}
           </a>

+ 13 - 3
src/client/js/components/Admin/MarkdownSetting/PresentationForm.jsx

@@ -42,7 +42,7 @@ class PresentationForm extends React.Component {
     return (
       <fieldset className="form-group col-12 my-2">
 
-        <label className="col-12 control-label font-weight-bold text-left mt-3">
+        <label className="col-8 offset-4 control-label font-weight-bold text-left mt-3">
           {t('admin:markdown_setting.presentation_options.page_break_setting')}
         </label>
 
@@ -60,7 +60,12 @@ class PresentationForm extends React.Component {
                 <p className="font-weight-bold">{ t('admin:markdown_setting.presentation_options.preset_one_separator') }</p>
                 <div className="mt-3">
                   { t('admin:markdown_setting.presentation_options.preset_one_separator_desc') }
-                  <pre><code>{ t('admin:markdown_setting.presentation_options.preset_one_separator_value') }</code></pre>
+                  <input
+                    className="form-control"
+                    type="text"
+                    value={t('admin:markdown_setting.presentation_options.preset_one_separator_value')}
+                    readOnly
+                  />
                 </div>
               </label>
             </div>
@@ -79,7 +84,12 @@ class PresentationForm extends React.Component {
                 <p className="font-weight-bold">{ t('admin:markdown_setting.presentation_options.preset_two_separator') }</p>
                 <div className="mt-3">
                   { t('admin:markdown_setting.presentation_options.preset_two_separator_desc') }
-                  <pre><code>{ t('admin:markdown_setting.presentation_options.preset_two_separator_value') }</code></pre>
+                  <input
+                    className="form-control"
+                    type="text"
+                    value={t('admin:markdown_setting.presentation_options.preset_two_separator_value')}
+                    readOnly
+                  />
                 </div>
               </label>
             </div>

+ 1 - 1
src/client/js/components/Admin/Notification/GlobalNotification.jsx

@@ -99,7 +99,7 @@ class GlobalNotification extends React.Component {
 
         <h2 className="border-bottom mb-5">{t('notification_setting.notification_list')}
           <a href="/admin/global-notification/new">
-            <p className="btn btn-light pull-right">{t('notification_setting.add_notification')}</p>
+            <p className="btn btn-outline-secondary pull-right">{t('notification_setting.add_notification')}</p>
           </a>
         </h2>
 

+ 21 - 19
src/client/js/components/Admin/Notification/GlobalNotificationList.jsx

@@ -91,36 +91,38 @@ class GlobalNotificationList extends React.Component {
                 {notification.triggerPath}
               </td>
               <td>
-                {notification.triggerEvents.includes('pageCreate') && (
-                  <span className="badge badge-pill badge-success" data-toggle="tooltip" data-placement="top" title="Page Create">
+                <ul className="list-inline">
+                  {notification.triggerEvents.includes('pageCreate') && (
+                  <li className="list-inline-item badge badge-pill badge-success" data-toggle="tooltip" data-placement="top" title="Page Create">
                     <i className="icon-doc"></i> CREATE
-                  </span>
+                  </li>
                 )}
-                {notification.triggerEvents.includes('pageEdit') && (
-                  <span className="badge badge-pill badge-warning" data-toggle="tooltip" data-placement="top" title="Page Edit">
+                  {notification.triggerEvents.includes('pageEdit') && (
+                  <li className="list-inline-item badge badge-pill badge-warning" data-toggle="tooltip" data-placement="top" title="Page Edit">
                     <i className="icon-pencil"></i> EDIT
-                  </span>
+                  </li>
                 )}
-                {notification.triggerEvents.includes('pageMove') && (
-                  <span className="badge badge-pill badge-warning" data-toggle="tooltip" data-placement="top" title="Page Move">
+                  {notification.triggerEvents.includes('pageMove') && (
+                  <li className="list-inline-item badge badge-pill badge-warning" data-toggle="tooltip" data-placement="top" title="Page Move">
                     <i className="icon-action-redo"></i> MOVE
-                  </span>
+                  </li>
                 )}
-                {notification.triggerEvents.includes('pageDelete') && (
-                  <span className="badge badge-pill badge-danger" data-toggle="tooltip" data-placement="top" title="Page Delte">
+                  {notification.triggerEvents.includes('pageDelete') && (
+                  <li className="list-inline-item badge badge-pill badge-danger" data-toggle="tooltip" data-placement="top" title="Page Delte">
                     <i className="icon-fire"></i> DELETE
-                  </span>
+                  </li>
                 )}
-                {notification.triggerEvents.includes('pageLike') && (
-                  <span className="badge badge-pill badge-info" data-toggle="tooltip" data-placement="top" title="Page Like">
+                  {notification.triggerEvents.includes('pageLike') && (
+                  <li className="list-inline-item badge badge-pill badge-info" data-toggle="tooltip" data-placement="top" title="Page Like">
                     <i className="icon-like"></i> LIKE
-                  </span>
+                  </li>
                 )}
-                {notification.triggerEvents.includes('comment') && (
-                  <span className="badge badge-pill badge-light" data-toggle="tooltip" data-placement="top" title="New Comment">
+                  {notification.triggerEvents.includes('comment') && (
+                  <li className="list-inline-item badge badge-pill badge-light" data-toggle="tooltip" data-placement="top" title="New Comment">
                     <i className="icon-fw icon-bubble"></i> POST
-                  </span>
+                  </li>
                 )}
+                </ul>
               </td>
               <td>
                 {notification.__t === 'mail'
@@ -131,7 +133,7 @@ class GlobalNotificationList extends React.Component {
               <td className="td-abs-center">
                 <div className="dropdown">
                   <button
-                    className="btn btn-light dropdown-toggle"
+                    className="btn btn-outline-secondary dropdown-toggle"
                     type="button"
                     id="dropdownMenuButton"
                     data-toggle="dropdown"

+ 110 - 81
src/client/js/components/Admin/Notification/ManageGlobalNotification.jsx

@@ -165,100 +165,129 @@ class ManageGlobalNotification extends React.Component {
 
             {this.state.notifyToType === 'mail'
               ? (
-                <div className="form-group notify-to-option" id="mail-input">
-                  <input
-                    className="form-control"
-                    type="text"
-                    name="toEmail"
-                    placeholder="Email"
-                    value={this.state.emailToSend}
-                    onChange={(e) => { this.onChangeEmailToSend(e.target.value) }}
-                  />
-                  <p className="help">
+                <>
+                  <div className="input-group notify-to-option" id="mail-input">
+                    <div className="input-group-prepend">
+                      <span className="input-group-text" id="mail-addon"><i className="ti-email" /></span>
+                    </div>
+                    <input
+                      className="form-control"
+                      type="text"
+                      aria-describedby="mail-addon"
+                      name="toEmail"
+                      placeholder="Email"
+                      value={this.state.emailToSend}
+                      onChange={(e) => { this.onChangeEmailToSend(e.target.value) }}
+                    />
+
+                  </div>
+                  <p className="p-2">
                     <b>Hint: </b>
                     <a href="https://ifttt.com/create" target="blank">{t('notification_setting.email.ifttt_link')}
                       <i className="icon-share-alt" />
                     </a>
                   </p>
-                </div>
+                </>
               )
               : (
-                <div className="form-group notify-to-option" id="slack-input">
-                  <input
-                    className="form-control"
-                    type="text"
-                    name="notificationGlobal[slackChannels]"
-                    placeholder="Slack Channel"
-                    value={this.state.slackChannelToSend}
-                    onChange={(e) => { this.onChangeSlackChannelToSend(e.target.value) }}
-                  />
-                </div>
+                <>
+                  <div className="input-group notify-to-option" id="slack-input">
+                    <div className="input-group-prepend">
+                      <span className="input-group-text" id="slack-channel-addon"><i className="fa fa-slack" /></span>
+                    </div>
+                    <input
+                      className="form-control"
+                      type="text"
+                      aria-describedby="slack-channel-addon"
+                      name="notificationGlobal[slackChannels]"
+                      placeholder="Slack Channel"
+                      value={this.state.slackChannelToSend}
+                      onChange={(e) => { this.onChangeSlackChannelToSend(e.target.value) }}
+                    />
+                  </div>
+                  <p className="p-2">
+                    {/* eslint-disable-next-line react/no-danger */}
+                    <span dangerouslySetInnerHTML={{ __html: t('notification_setting.channel_desc') }} />
+                  </p>
+                </>
               )}
           </div>
 
           <div className="offset-1 col-sm-5">
             <div className="form-group">
               <h3>{t('notification_setting.trigger_events')}</h3>
-              <TriggerEventCheckBox
-                checkbox="success"
-                event="pageCreate"
-                checked={this.state.triggerEvents.has('pageCreate')}
-                onChange={() => this.onChangeTriggerEvents('pageCreate')}
-              >
-                <span className="badge badge-pill badge-success">
-                  <i className="icon-doc"></i> CREATE
-                </span>
-              </TriggerEventCheckBox>
-              <TriggerEventCheckBox
-                checkbox="warning"
-                event="pageEdit"
-                checked={this.state.triggerEvents.has('pageEdit')}
-                onChange={() => this.onChangeTriggerEvents('pageEdit')}
-              >
-                <span className="badge badge-pill badge-warning">
-                  <i className="icon-pencil"></i>EDIT
-                </span>
-              </TriggerEventCheckBox>
-              <TriggerEventCheckBox
-                checkbox="warning"
-                event="pageMove"
-                checked={this.state.triggerEvents.has('pageMove')}
-                onChange={() => this.onChangeTriggerEvents('pageMove')}
-              >
-                <span className="badge badge-pill badge-warning">
-                  <i className="icon-action-redo"></i>MOVE
-                </span>
-              </TriggerEventCheckBox>
-              <TriggerEventCheckBox
-                checkbox="danger"
-                event="pageDelete"
-                checked={this.state.triggerEvents.has('pageDelete')}
-                onChange={() => this.onChangeTriggerEvents('pageDelete')}
-              >
-                <span className="badge badge-pill badge-danger">
-                  <i className="icon-fire"></i>DELETE
-                </span>
-              </TriggerEventCheckBox>
-              <TriggerEventCheckBox
-                checkbox="info"
-                event="pageLike"
-                checked={this.state.triggerEvents.has('pageLike')}
-                onChange={() => this.onChangeTriggerEvents('pageLike')}
-              >
-                <span className="badge badge-pill badge-info">
-                  <i className="icon-like"></i>LIKE
-                </span>
-              </TriggerEventCheckBox>
-              <TriggerEventCheckBox
-                checkbox="secondary"
-                event="comment"
-                checked={this.state.triggerEvents.has('comment')}
-                onChange={() => this.onChangeTriggerEvents('comment')}
-              >
-                <span className="badge badge-pill badge-light">
-                  <i className="icon-bubble"></i>POST
-                </span>
-              </TriggerEventCheckBox>
+              <div className="my-1">
+                <TriggerEventCheckBox
+                  checkbox="success"
+                  event="pageCreate"
+                  checked={this.state.triggerEvents.has('pageCreate')}
+                  onChange={() => this.onChangeTriggerEvents('pageCreate')}
+                >
+                  <span className="badge badge-pill badge-success">
+                    <i className="icon-doc mr-1" /> CREATE
+                  </span>
+                </TriggerEventCheckBox>
+              </div>
+              <div className="my-1">
+                <TriggerEventCheckBox
+                  checkbox="warning"
+                  event="pageEdit"
+                  checked={this.state.triggerEvents.has('pageEdit')}
+                  onChange={() => this.onChangeTriggerEvents('pageEdit')}
+                >
+                  <span className="badge badge-pill badge-warning">
+                    <i className="icon-pencil mr-1" />EDIT
+                  </span>
+                </TriggerEventCheckBox>
+              </div>
+              <div className="my-1">
+                <TriggerEventCheckBox
+                  checkbox="warning"
+                  event="pageMove"
+                  checked={this.state.triggerEvents.has('pageMove')}
+                  onChange={() => this.onChangeTriggerEvents('pageMove')}
+                >
+                  <span className="badge badge-pill badge-warning">
+                    <i className="icon-action-redo mr-1" />MOVE
+                  </span>
+                </TriggerEventCheckBox>
+              </div>
+              <div className="my-1">
+                <TriggerEventCheckBox
+                  checkbox="danger"
+                  event="pageDelete"
+                  checked={this.state.triggerEvents.has('pageDelete')}
+                  onChange={() => this.onChangeTriggerEvents('pageDelete')}
+                >
+                  <span className="badge badge-pill badge-danger">
+                    <i className="icon-fire mr-1" />DELETE
+                  </span>
+                </TriggerEventCheckBox>
+              </div>
+              <div className="my-1">
+                <TriggerEventCheckBox
+                  checkbox="info"
+                  event="pageLike"
+                  checked={this.state.triggerEvents.has('pageLike')}
+                  onChange={() => this.onChangeTriggerEvents('pageLike')}
+                >
+                  <span className="badge badge-pill badge-info">
+                    <i className="icon-like mr-1" />LIKE
+                  </span>
+                </TriggerEventCheckBox>
+              </div>
+              <div className="my-1">
+                <TriggerEventCheckBox
+                  checkbox="secondary"
+                  event="comment"
+                  checked={this.state.triggerEvents.has('comment')}
+                  onChange={() => this.onChangeTriggerEvents('comment')}
+                >
+                  <span className="badge badge-pill badge-secondary">
+                    <i className="icon-bubble mr-1" />POST
+                  </span>
+                </TriggerEventCheckBox>
+              </div>
 
             </div>
           </div>

+ 4 - 4
src/client/js/components/Admin/Notification/UserNotificationRow.jsx

@@ -15,14 +15,14 @@ class UserNotificationRow extends React.PureComponent {
     return (
       <React.Fragment>
         <tr className="admin-notif-row" key={notification._id}>
-          <td>
+          <td className="px-4">
             {notification.pathPattern}
           </td>
-          <td>
-            {notification.channel}
+          <td className="px-4">
+            <span data-toggle="tooltip" data-placement="top" title="Slack"><i className="fa fa-slack"></i> {notification.channel}</span>
           </td>
           <td>
-            <button type="submit" className="btn btn-light" onClick={() => { this.props.onClickDeleteBtn(notification._id) }}>{t('Delete')}</button>
+            <button type="submit" className="btn btn-outline-danger" onClick={() => { this.props.onClickDeleteBtn(notification._id) }}>{t('Delete')}</button>
           </td>
         </tr>
       </React.Fragment>

+ 21 - 13
src/client/js/components/Admin/Notification/UserTriggerNotification.jsx

@@ -103,22 +103,30 @@ class UserTriggerNotification extends React.Component {
                   placeholder="e.g. /projects/xxx/MTG/*"
                   onChange={(e) => { this.changePathPattern(e.target.value) }}
                 />
-                {/* eslint-disable-next-line react/no-danger */}
-                <p className="help-block" dangerouslySetInnerHTML={{ __html: t('notification_setting.pattern_desc') }} />
+                <p className="p-2 mb-0">
+                  {/* eslint-disable-next-line react/no-danger */}
+                  <span dangerouslySetInnerHTML={{ __html: t('notification_setting.pattern_desc') }} />
+                </p>
               </td>
 
               <td>
-                <input
-                  className="form-control form-inline"
-                  type="text"
-                  name="channel"
-                  value={this.state.channel}
-                  placeholder="e.g. project-xxx"
-                  onChange={(e) => { this.changeChannel(e.target.value) }}
-                />
-                {/* eslint-disable-next-line react/no-danger */}
-                <p className="help-block" dangerouslySetInnerHTML={{ __html: t('notification_setting.channel_desc') }} />
-
+                <div className="input-group notify-to-option" id="slack-input">
+                  <div className="input-group-prepend">
+                    <span className="input-group-text"><i className="fa fa-slack" /></span>
+                  </div>
+                  <input
+                    className="form-control form-inline"
+                    type="text"
+                    name="channel"
+                    value={this.state.channel}
+                    placeholder="e.g. project-xxx"
+                    onChange={(e) => { this.changeChannel(e.target.value) }}
+                  />
+                </div>
+                <p className="p-2 mb-0">
+                  {/* eslint-disable-next-line react/no-danger */}
+                  <span dangerouslySetInnerHTML={{ __html: t('notification_setting.channel_desc') }} />
+                </p>
               </td>
               <td>
                 <button type="button" className="btn btn-primary" disabled={!this.validateForm()} onClick={this.onClickSubmit}>{t('add')}</button>

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

@@ -83,7 +83,7 @@ class GitHubSecurityManagement extends React.Component {
               </label>
             </div>
             {(!adminGeneralSecurityContainer.state.setupStrategies.includes('github') && isGitHubEnabled)
-              && <div className="badg badge-warning">{t('security_setting.setup_is_not_yet_complete')}</div>}
+              && <div className="badge badge-warning">{t('security_setting.setup_is_not_yet_complete')}</div>}
           </div>
         </div>
 

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

@@ -116,7 +116,7 @@ class LdapAuthTest extends React.Component {
           <textarea id="taLogs" className="col-xs-12" rows="4" value={this.state.logs} readOnly />
         </div>
 
-        <button type="button" className="btn btn-default mt-3 col-xs-offset-5 col-xs-2" onClick={this.testLdapCredentials}>Test</button>
+        <button type="button" className="btn btn-outline-secondary mt-3 col-xs-offset-5 col-xs-2" onClick={this.testLdapCredentials}>Test</button>
 
       </React.Fragment>
 

+ 7 - 2
src/client/js/components/Admin/Security/LdapSecuritySetting.jsx

@@ -128,7 +128,7 @@ class LdapSecuritySetting extends React.Component {
               <div className="col-6">
                 <div className="dropdown">
                   <button
-                    className="btn btn-light dropdown-toggle"
+                    className="btn btn-outline-secondary dropdown-toggle"
                     type="button"
                     id="dropdownMenuButton"
                     data-toggle="dropdown"
@@ -425,7 +425,12 @@ class LdapSecuritySetting extends React.Component {
                 >
                   {t('Update')}
                 </button>
-                <button type="button" className="btn btn-light ml-2" onClick={this.openLdapAuthTestModal}>{t('security_setting.ldap.test_config')}</button>
+                <button
+                  type="button"
+                  className="btn btn-outline-secondary ml-2"
+                  onClick={this.openLdapAuthTestModal}
+                >{t('security_setting.ldap.test_config')}
+                </button>
               </div>
             </div>
 

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

@@ -106,7 +106,7 @@ class LocalSecuritySetting extends React.Component {
               <div className="col-6">
                 <div className="dropdown">
                   <button
-                    className="btn btn-light dropdown-toggle"
+                    className="btn btn-outline-secondary dropdown-toggle"
                     type="button"
                     id="dropdownMenuButton"
                     data-toggle="dropdown"

+ 2 - 2
src/client/js/components/Admin/Security/SecuritySetting.jsx

@@ -64,7 +64,7 @@ class SecuritySetting extends React.Component {
           <div className="col-6">
             <div className="dropdown">
               <button
-                className={`btn btn-light dropdown-toggle ${adminGeneralSecurityContainer.isWikiModeForced && 'disabled'}`}
+                className={`btn btn-outline-secondary dropdown-toggle ${adminGeneralSecurityContainer.isWikiModeForced && 'disabled'}`}
                 type="button"
                 id="dropdownMenuButton"
                 data-toggle="dropdown"
@@ -144,7 +144,7 @@ class SecuritySetting extends React.Component {
           <div className="col-9">
             <div className="dropdown">
               <button
-                className="btn btn-light dropdown-toggle"
+                className="btn btn-outline-secondary dropdown-toggle"
                 type="button"
                 id="dropdownMenuButton"
                 data-toggle="dropdown"

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

@@ -70,7 +70,7 @@ class UserGroupCreateForm extends React.Component {
         <p>
           {this.props.isAclEnabled
             ? (
-              <button type="button" data-toggle="collapse" className="btn btn-light" href="#createGroupForm">
+              <button type="button" data-toggle="collapse" className="btn btn-outline-secondary" href="#createGroupForm">
                 {t('admin:user_group_management.create_group')}
               </button>
             )

+ 2 - 2
src/client/js/components/Admin/UserGroup/UserGroupTable.jsx

@@ -69,7 +69,7 @@ class UserGroupTable extends React.Component {
                   <td>
                     <ul className="list-inline">
                       {this.state.userGroupRelations[group._id].map((user) => {
-                        return <li key={user._id} className="list-inline-item badge badge-pill badge-primary">{this.xss.process(user.username)}</li>;
+                        return <li key={user._id} className="list-inline-item badge badge-pill badge-warning">{this.xss.process(user.username)}</li>;
                       })}
                     </ul>
                   </td>
@@ -81,7 +81,7 @@ class UserGroupTable extends React.Component {
                           <button
                             type="button"
                             id={`admin-group-menu-button-${group._id}`}
-                            className="btn btn-light btn-sm dropdown-toggle"
+                            className="btn btn-outline-secondary btn-sm dropdown-toggle"
                             data-toggle="dropdown"
                           >
                             <i className="icon-settings"></i>

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

@@ -16,7 +16,7 @@ class UserGroupDetailPage extends React.Component {
 
     return (
       <div>
-        <a href="/admin/user-groups" className="btn btn-light">
+        <a href="/admin/user-groups" className="btn btn-outline-secondary">
           <i className="icon-fw ti-arrow-left" aria-hidden="true"></i>
           {t('admin:user_group_management.back_to_list')}
         </a>

+ 2 - 2
src/client/js/components/Admin/UserGroupDetail/UserGroupUserTable.jsx

@@ -67,7 +67,7 @@ class UserGroupUserTable extends React.Component {
                     <button
                       type="button"
                       id={`admin-group-menu-button-${relatedUser._id}`}
-                      className="btn btn-light btn-sm dropdown-toggle"
+                      className="btn btn-outline-secondary btn-sm dropdown-toggle"
                       data-toggle="dropdown"
                     >
                       <i className="icon-settings"></i>
@@ -91,7 +91,7 @@ class UserGroupUserTable extends React.Component {
           <tr>
             <td></td>
             <td className="text-center">
-              <button className="btn btn-light" type="button" onClick={adminUserGroupDetailContainer.openUserGroupUserModal}>
+              <button className="btn btn-outline-secondary" type="button" onClick={adminUserGroupDetailContainer.openUserGroupUserModal}>
                 <i className="ti-plus"></i>
               </button>
             </td>

+ 3 - 3
src/client/js/components/Admin/UserManagement.jsx

@@ -95,7 +95,7 @@ class UserManagement extends React.Component {
     const { t, adminUsersContainer } = this.props;
 
     const pager = (
-      <div className="pull-right">
+      <div className="pull-right my-3">
         <PaginationWrapper
           activePage={adminUsersContainer.state.activePage}
           changePage={this.handlePage}
@@ -187,7 +187,7 @@ class UserManagement extends React.Component {
                 </label>
               </div>
 
-              <div className="custom-control custom-checkbox custom-checkbox-warning mr-2">
+              <div className="custom-control custom-checkbox custom-checkbox-secondary mr-2">
                 <input
                   className="custom-control-input"
                   type="checkbox"
@@ -196,7 +196,7 @@ class UserManagement extends React.Component {
                   onClick={() => { this.handleClick('suspended') }}
                 />
                 <label className="custom-control-label" htmlFor="c4">
-                  <span className="badge badge-warning d-inline-block vt mt-1">Suspended</span>
+                  <span className="badge badge-secondary d-inline-block vt mt-1">Suspended</span>
                 </label>
               </div>
 

+ 1 - 1
src/client/js/components/Admin/Users/ExternalAccountTable.jsx

@@ -94,7 +94,7 @@ class ExternalAccountTable extends React.Component {
                   <td>{dateFnsFormat(new Date(ea.createdAt), 'yyyy-MM-dd')}</td>
                   <td>
                     <div className="btn-group admin-user-menu">
-                      <button type="button" className="btn btn-default btn-sm dropdown-toggle" data-toggle="dropdown">
+                      <button type="button" className="btn btn-outline-secondary btn-sm dropdown-toggle" data-toggle="dropdown">
                         <i className="icon-settings"></i> <span className="caret"></span>
                       </button>
                       <ul className="dropdown-menu" role="menu">

+ 1 - 1
src/client/js/components/Admin/Users/InviteUserControl.jsx

@@ -14,7 +14,7 @@ class InviteUserControl extends React.Component {
 
     return (
       <Fragment>
-        <button type="button" className="btn btn-light" onClick={adminUsersContainer.toggleUserInviteModal}>
+        <button type="button" className="btn btn-outline-secondary" onClick={adminUsersContainer.toggleUserInviteModal}>
           {t('admin:user_management.invite_users')}
         </button>
         <UserInviteModal />

+ 2 - 2
src/client/js/components/Admin/Users/UserMenu.jsx

@@ -36,7 +36,7 @@ class UserMenu extends React.Component {
         <li className="dropdown-divider"></li>
         <li className="dropdown-header">{t('admin:user_management.user_table.edit_menu')}</li>
         <li>
-          <a className="dropdown-item" href="" onClick={this.onPasswordResetClicked}>
+          <a className="dropdown-item" href="#" onClick={this.onPasswordResetClicked}>
             <i className="icon-fw icon-key"></i>{ t('admin:user_management.reset_password') }
           </a>
         </li>
@@ -81,7 +81,7 @@ class UserMenu extends React.Component {
     return (
       <Fragment>
         <div className="btn-group admin-user-menu" role="group">
-          <button id="userMenu" type="button" className="btn btn-light btn-sm dropdown-toggle" data-toggle="dropdown">
+          <button id="userMenu" type="button" className="btn btn-outline-secondary btn-sm dropdown-toggle" data-toggle="dropdown">
             <i className="icon-settings"></i>
           </button>
           <div className="dropdown-menu" aria-labelledby="userMenu">

+ 1 - 1
src/client/js/components/Admin/Users/UserTable.jsx

@@ -42,7 +42,7 @@ class UserTable extends React.Component {
         text = 'Active';
         break;
       case 3:
-        additionalClassName = 'badge-warning';
+        additionalClassName = 'badge-secondary';
         text = 'Suspended';
         break;
       case 4:

+ 31 - 39
src/client/js/components/BookmarkButton.jsx

@@ -1,58 +1,51 @@
 import React from 'react';
 import PropTypes from 'prop-types';
 
-export default class BookmarkButton extends React.Component {
+import { toastError } from '../util/apiNotification';
+
+class BookmarkButton extends React.Component {
 
   constructor(props) {
     super(props);
 
     this.state = {
-      bookmarked: false,
+      isBookmarked: false,
     };
 
     this.handleClick = this.handleClick.bind(this);
   }
 
-  componentDidMount() {
+  async componentDidMount() {
+    const { pageId, crowi } = this.props;
     // if guest user
     if (!this.isUserLoggedIn()) {
       // do nothing
       return;
     }
 
-    this.props.crowi.apiGet('/bookmarks.get', { page_id: this.props.pageId })
-      .then((res) => {
-        if (res.bookmark) {
-          this.markBookmarked();
-        }
-      });
-  }
-
-  handleClick(event) {
-    event.preventDefault();
-
-    const pageId = this.props.pageId;
-
-    if (!this.state.bookmarked) {
-      this.props.crowi.apiPost('/bookmarks.add', { page_id: pageId })
-        .then((res) => {
-          this.markBookmarked();
-        });
+    try {
+      const response = await crowi.apiv3.get('/bookmarks', { pageId });
+      if (response.data.bookmark != null) {
+        this.setState({ isBookmarked: true });
+      }
     }
-    else {
-      this.props.crowi.apiPost('/bookmarks.remove', { page_id: pageId })
-        .then((res) => {
-          this.markUnBookmarked();
-        });
+    catch (err) {
+      toastError(err);
     }
-  }
 
-  markBookmarked() {
-    this.setState({ bookmarked: true });
   }
 
-  markUnBookmarked() {
-    this.setState({ bookmarked: false });
+  async handleClick() {
+    const { crowi, pageId } = this.props;
+    const bool = !this.state.isBookmarked;
+
+    try {
+      await crowi.apiv3.put('/bookmarks', { pageId, bool });
+      this.setState({ isBookmarked: bool });
+    }
+    catch (err) {
+      toastError(err);
+    }
   }
 
   isUserLoggedIn() {
@@ -65,20 +58,13 @@ export default class BookmarkButton extends React.Component {
       return <div></div>;
     }
 
-    const btnSizeClassName = this.props.size ? `btn-${this.props.size}` : 'btn-md';
-    const addedClassNames = [
-      this.state.bookmarked ? 'active' : '',
-      btnSizeClassName,
-    ];
-    const addedClassName = addedClassNames.join(' ');
-
     return (
       <button
         type="button"
         href="#"
         title="Bookmark"
         onClick={this.handleClick}
-        className={`btn btn-circle btn-outline-warning btn-bookmark border-0 ${addedClassName}`}
+        className={`btn btn-circle btn-outline-warning btn-bookmark border-0 ${`btn-${this.props.size}`} ${this.state.isBookmarked && 'active'}`}
       >
         <i className="icon-star"></i>
       </button>
@@ -92,3 +78,9 @@ BookmarkButton.propTypes = {
   crowi: PropTypes.object.isRequired,
   size: PropTypes.string,
 };
+
+BookmarkButton.defaultProps = {
+  size: 'md',
+};
+
+export default BookmarkButton;

+ 6 - 18
src/client/js/components/LikeButton.jsx

@@ -19,25 +19,13 @@ class LikeButton extends React.Component {
 
   async handleClick() {
     const { appContainer, pageId } = this.props;
-    const { isLiked } = this.state;
-
-    if (!isLiked) {
-      try {
-        await appContainer.apiPost('/likes.add', { page_id: pageId });
-        this.setState({ isLiked: true });
-      }
-      catch (err) {
-        toastError(err);
-      }
+    const bool = !this.state.isLiked;
+    try {
+      await appContainer.apiv3.put('/page/likes', { pageId, bool });
+      this.setState({ isLiked: bool });
     }
-    else {
-      try {
-        await appContainer.apiPost('/likes.remove', { page_id: pageId });
-        this.setState({ isLiked: false });
-      }
-      catch (err) {
-        toastError(err);
-      }
+    catch (err) {
+      toastError(err);
     }
   }
 

+ 1 - 1
src/client/js/components/Me/DisassociateModal.jsx

@@ -57,7 +57,7 @@ class DisassociateModal extends React.Component {
           <p dangerouslySetInnerHTML={{ __html: t('personal_settings.disassociate_external_account_desc', { providerType, accountId }) }} />
         </ModalBody>
         <ModalFooter>
-          <button type="button" className="btn btn-sm btn-default" onClick={this.props.onClose}>
+          <button type="button" className="btn btn-sm btn-outline-secondary" onClick={this.props.onClose}>
             { t('Cancel') }
           </button>
           <button type="button" className="btn btn-sm btn-danger" onClick={this.onClickDisassociateBtn}>

+ 1 - 1
src/client/js/components/Me/ExternalAccountLinkedMe.jsx

@@ -69,7 +69,7 @@ class ExternalAccountLinkedMe extends React.Component {
       <Fragment>
         <div className="container-fluid p-0 my-4">
           <h2 className="border-bottom">
-            <button type="button" className="btn btn-light btn-sm pull-right" onClick={this.openAssociateModal}>
+            <button type="button" className="btn btn-outline-secondary btn-sm pull-right" onClick={this.openAssociateModal}>
               <i className="icon-plus" aria-hidden="true" />
             Add
             </button>

+ 1 - 1
src/client/js/components/Me/ExternalAccountRow.jsx

@@ -18,7 +18,7 @@ const ExternalAccountRow = (props) => {
       <td className="text-center">
         <button
           type="button"
-          className="btn btn-default btn-sm btn-danger"
+          className="btn btn-outline-secondary btn-sm btn-danger"
           onClick={() => props.openDisassociateModal(account)}
         >
           <i className="ti-unlink"></i>

+ 8 - 12
src/client/js/components/Me/ImageCropModal.jsx

@@ -94,19 +94,15 @@ class ImageCropModal extends React.Component {
           <ReactCrop circularCrop src={this.props.src} crop={this.state.crop} onImageLoaded={this.onImageLoaded} onChange={this.onCropChange} />
         </ModalBody>
         <ModalFooter>
-          <div className="d-flex justify-content-between">
-            <button type="button" className="btn btn-sm bg-danger" onClick={this.reset}>
+          <button type="button" className="btn btn-outline-danger rounded-pill mr-auto" onClick={this.reset}>
               Reset
-            </button>
-            <div className="d-flex">
-              <button type="button" className="btn btn-sm bg-light" onClick={this.props.onModalClose}>
-                Cancel
-              </button>
-              <button type="button" className="btn btn-sm bg-primary" onClick={this.crop}>
-                Crop
-              </button>
-            </div>
-          </div>
+          </button>
+          <button type="button" className="btn btn-outline-secondary rounded-pill mr-2" onClick={this.props.onModalClose}>
+                  Cancel
+          </button>
+          <button type="button" className="btn btn-outline-primary rounded-pill" onClick={this.crop}>
+                  Crop
+          </button>
         </ModalFooter>
       </Modal>
     );

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

@@ -141,7 +141,7 @@ class MyDraftList extends React.Component {
             <div className="d-flex justify-content-between">
               <h4>Total: {totalCount} drafts</h4>
               <div className="align-self-center">
-                <button type="button" className="btn btn-sm btn-default" onClick={this.clearAllDrafts}>
+                <button type="button" className="btn btn-sm btn-outline-secondary" onClick={this.clearAllDrafts}>
                   <i className="icon-fw icon-fire text-danger"></i>
                   {t('Delete All')}
                 </button>

+ 3 - 3
src/client/js/components/Navbar/GrowiSubNavigation.jsx

@@ -20,7 +20,7 @@ const GrowiSubNavigation = (props) => {
   const {
     pageId, path, createdAt, creator, updatedAt, revisionAuthor, isCompactMode,
   } = pageContainer.state;
-  const compactClassName = isCompactMode ? 'fixed-top grw-compact-subnavbar px-3' : null;
+  const compactClassName = isCompactMode ? 'grw-compact-subnavbar' : null;
 
   // Display only the RevisionPath if the page is trash or forbidden
   if (isTrashPage(path) || isPageForbidden) {
@@ -36,7 +36,7 @@ const GrowiSubNavigation = (props) => {
   }
 
   return (
-    <div className={`d-flex align-items-center ${compactClassName}`}>
+    <div className={`row px-3 py-1 align-items-center ${compactClassName}`}>
 
       {/* Page Path */}
       <div className="title-container mr-auto">
@@ -48,7 +48,7 @@ const GrowiSubNavigation = (props) => {
 
       {/* Header Button */}
       <div className="mr-2">
-        <LikeButton pageId={pageId} />
+        <LikeButton pageId={pageId} isLiked={pageContainer.state.isLiked} />
       </div>
       <div>
         <BookmarkButton pageId={pageId} crowi={appContainer} />

+ 85 - 0
src/client/js/components/Navbar/GrowiSubNavigationForUserPage.jsx

@@ -0,0 +1,85 @@
+import React, { useState, useEffect } from 'react';
+import PropTypes from 'prop-types';
+
+import { withTranslation } from 'react-i18next';
+import { throttle } from 'throttle-debounce';
+
+import { createSubscribedElement } from '../UnstatedUtils';
+import AppContainer from '../../services/AppContainer';
+import RevisionPath from '../Page/RevisionPath';
+import PageContainer from '../../services/PageContainer';
+import BookmarkButton from '../BookmarkButton';
+import UserPicture from '../User/UserPicture';
+
+const GrowiSubNavigationForUserPage = (props) => {
+  const pageUser = JSON.parse(document.querySelector('#grw-subnav-for-user-page').getAttribute('data-page-user'));
+  const { appContainer, pageContainer } = props;
+  const { pageId } = pageContainer.state;
+  const [isCompactMode, setIsCompactMode] = useState(false);
+  const scrollAmountForFixed = 50;
+  const layoutType = appContainer.getConfig().layoutType;
+
+  useEffect(() => {
+    window.addEventListener('scroll', throttle(300, () => {
+      setIsCompactMode(window.pageYOffset > scrollAmountForFixed);
+    }));
+  }, []);
+
+  return (
+    <div className={`row px-3 py-1 ${(isCompactMode && layoutType === 'growi') && 'grw-compact-subnavbar'}`}>
+
+      <div className="col-12">
+        {/* Page Path */}
+        <h4>
+          <RevisionPath behaviorType={appContainer.config.behaviorType} pageId={pageId} pagePath={pageContainer.state.path} />
+        </h4>
+
+        <div className="d-flex">
+          <div className="users-info d-flex align-items-center mr-auto">
+            <UserPicture user={pageUser} />
+
+            <div className="users-meta">
+              <div className="d-flex align-items-center">
+                <h1>
+                  {pageUser.name}
+                </h1>
+              </div>
+              <div className="user-page-meta">
+                <ul>
+                  <li className="user-page-username"><i className="icon-user mr-1"></i>{pageUser.username}</li>
+                  <li className="user-page-email">
+                    <i className="icon-envelope mr-1"></i>
+                    {pageUser.isEmailPublished ? pageUser.email : '*****'}
+                  </li>
+                  {pageUser.introduction && <li className="user-page-introduction"><p>{pageUser.introduction}</p></li>}
+                </ul>
+              </div>
+            </div>
+          </div>
+
+          {/* Header Button */}
+          <BookmarkButton pageId={pageId} crowi={appContainer} size="lg" />
+        </div>
+      </div>
+
+
+    </div>
+  );
+
+};
+
+/**
+ * Wrapper component for using unstated
+ */
+const GrowiSubNavigationForUserPageWrapper = (props) => {
+  return createSubscribedElement(GrowiSubNavigationForUserPage, props, [AppContainer, PageContainer]);
+};
+
+
+GrowiSubNavigationForUserPage.propTypes = {
+  t: PropTypes.func.isRequired, //  i18next
+  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
+  pageContainer: PropTypes.instanceOf(PageContainer).isRequired,
+};
+
+export default withTranslation()(GrowiSubNavigationForUserPageWrapper);

+ 13 - 1
src/client/js/components/Page/RevisionBody.jsx

@@ -35,7 +35,19 @@ export default class RevisionBody extends React.PureComponent {
 
   renderMathJax() {
     const MathJax = window.MathJax;
-    MathJax.Hub.Queue(['Typeset', MathJax.Hub, this.element]);
+    // Workaround MathJax Rendering (Errors still occur, but MathJax can be rendered)
+    //
+    // Reason:
+    //   Addition of draw.io Integration causes initialization conflict between MathJax of draw.io and MathJax of GROWI.
+    //   So, before MathJax is initialized, execute renderMathJaxWithDebounce again.
+    //   Avoiding initialization of MathJax of draw.io solves the problem.
+    //   refs: https://github.com/jgraph/drawio/pull/831
+    if (MathJax != null && MathJax.Hub != null) {
+      MathJax.Hub.Queue(['Typeset', MathJax.Hub, this.element]);
+    }
+    else {
+      this.renderMathJaxWithDebounce();
+    }
   }
 
   generateInnerHtml(html) {

+ 1 - 1
src/client/js/components/Page/RevisionPath.jsx

@@ -159,7 +159,7 @@ class RevisionPath extends React.Component {
 
         <CopyDropdown t={this.props.t} pagePath={this.props.pagePath} pageId={this.props.pageId} buttonStyle={buttonStyle}></CopyDropdown>
 
-        <a href="#edit" className="d-block btn btn-default btn-edit text-muted" style={buttonStyle}>
+        <a href="#edit" className="d-block btn btn-outline-secondary btn-edit text-muted" style={buttonStyle}>
           <i className="icon-note" />
         </a>
       </span>

+ 1 - 1
src/client/js/components/PageComment/Comment.jsx

@@ -206,7 +206,7 @@ class Comment extends React.PureComponent {
               </div>
               <div className="page-comment-body">{commentBody}</div>
               <div className="page-comment-meta">
-                {commentedDate}
+                <span><a href={`#${commentId}`}>{commentedDate}</a></span>
                 <UncontrolledTooltip placement="bottom" fade={false} target={commentedDateId}>{commentedDateFormatted}</UncontrolledTooltip>
                 { isEdited && (
                   <>

+ 2 - 2
src/client/js/components/PageEditor/Editor.jsx

@@ -259,7 +259,7 @@ export default class Editor extends AbstractEditor {
     };
 
     return (
-      <Modal isOpen={this.state.isCheatsheetModalShown} toggle={hideCheatsheetModal} className="modal modal-gfm-cheatsheet">
+      <Modal isOpen={this.state.isCheatsheetModalShown} toggle={hideCheatsheetModal} className="modal-gfm-cheatsheet">
         <ModalHeader tag="h4" toggle={hideCheatsheetModal} className="bg-primary">
           <span className="text-white"><i className="icon-fw icon-question" />Markdown Help</span>
         </ModalHeader>
@@ -339,7 +339,7 @@ export default class Editor extends AbstractEditor {
           && (
           <button
             type="button"
-            className="btn btn-default btn-block btn-open-dropzone"
+            className="btn btn-outline-secondary btn-block btn-open-dropzone"
             onClick={() => { this.dropzone.open() }}
           >
             <i className="icon-paper-clip" aria-hidden="true"></i>&nbsp;

+ 2 - 2
src/client/js/components/PageEditor/OptionsSelector.jsx

@@ -112,7 +112,7 @@ class OptionsSelector extends React.Component {
       <div className="my-0 form-group">
         <label>Theme:</label>
         <div className="btn-group btn-group-sm dropup">
-          <button className="btn btn-light dropdown-toggle" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
+          <button className="btn btn-outline-secondary dropdown-toggle" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
             {selectedTheme}
           </button>
           <div className="dropdown-menu" aria-labelledby="dropdownMenuLink">
@@ -139,7 +139,7 @@ class OptionsSelector extends React.Component {
       <div className="my-0 form-group">
         <label>Keymap:</label>
         <div className="btn-group btn-group-sm dropup">
-          <button className="btn btn-light dropdown-toggle" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
+          <button className="btn btn-outline-secondary dropdown-toggle" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
             {selectedKeymapMode}
           </button>
           <div className="dropdown-menu" aria-labelledby="dropdownMenuLink">

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

@@ -294,7 +294,7 @@ class PageEditorByHackmd extends React.Component {
 
           <div className="text-center hackmd-discard-button-container mb-3">
             <button
-              className="btn btn-default btn-lg waves-effect waves-light"
+              className="btn btn-outline-secondary btn-lg waves-effect waves-light"
               type="button"
               onClick={() => { return this.discardChanges() }}
             >

+ 1 - 1
src/client/js/components/SearchPage/SearchPageForm.jsx

@@ -42,7 +42,7 @@ class SearchPageForm extends React.Component {
           />
         </div>
         <div className="input-group-append">
-          <button className="btn btn-light" type="button" id="button-addon2" onClick={this.search}>
+          <button className="btn btn-outline-secondary" type="button" id="button-addon2" onClick={this.search}>
             <i className="icon-magnifier"></i>
           </button>
         </div>

+ 2 - 2
src/client/js/components/SearchPage/SearchResult.jsx

@@ -202,7 +202,7 @@ class SearchResult extends React.Component {
     if (this.state.deletionMode) {
       deletionModeButtons = (
         <div className="btn-group">
-          <button type="button" className="btn btn-light btn-sm rounded-pill-weak" onClick={() => { return this.handleDeletionModeChange() }}>
+          <button type="button" className="btn btn-outline-secondary btn-sm rounded-pill-weak" onClick={() => { return this.handleDeletionModeChange() }}>
             <i className="icon-ban" /> Cancel
           </button>
           <button
@@ -231,7 +231,7 @@ class SearchResult extends React.Component {
     else {
       deletionModeButtons = (
         <div className="btn-group">
-          <button type="button" className="btn btn-light rounded-pill btn-sm" onClick={() => { return this.handleDeletionModeChange() }}>
+          <button type="button" className="btn btn-outline-secondary rounded-pill btn-sm" onClick={() => { return this.handleDeletionModeChange() }}>
             <i className="ti-check-box" /> DeletionMode
           </button>
         </div>

+ 1 - 1
src/client/js/components/SearchPage/SearchResultList.jsx

@@ -21,7 +21,7 @@ class SearchResultList extends React.Component {
         // Add prefix 'id_' in id attr, because scrollspy of bootstrap doesn't work when the first letter of id of target component is numeral.
         <div id={`id_${page._id}`} key={page._id} className="search-result-page mb-5">
           <h2>
-            <a href={page.path}>{page.path}</a>
+            <a href={page.path} className="text-break">{page.path}</a>
             { showTags && (
               <div className="mt-1 small"><i className="tag-icon icon-tag"></i> {page.tags.join(', ')}</div>
             )}

+ 71 - 59
src/client/js/components/Sidebar.jsx

@@ -1,74 +1,83 @@
 import React from 'react';
-// import PropTypes from 'prop-types';
+import PropTypes from 'prop-types';
 
 import { withTranslation } from 'react-i18next';
 
-import BacklogIcon from '@atlaskit/icon/glyph/backlog';
-import BoardIcon from '@atlaskit/icon/glyph/board';
-import GraphLineIcon from '@atlaskit/icon/glyph/graph-line';
-import ShortcutIcon from '@atlaskit/icon/glyph/shortcut';
-import { JiraWordmark } from '@atlaskit/logo';
-
 import {
-  GroupHeading,
-  HeaderSection,
-  Item,
+  withNavigationUIController,
   LayoutManager,
-  MenuSection,
   NavigationProvider,
-  Separator,
-  Wordmark,
   ThemeProvider, modeGenerator,
 } from '@atlaskit/navigation-next';
 
+import Drawer from '@atlaskit/drawer';
+
 import { createSubscribedElement } from './UnstatedUtils';
 import AppContainer from '../services/AppContainer';
 
+import GrowiLogo from './GrowiLogo';
 import SidebarNav from './Sidebar/SidebarNav';
+import History from './Sidebar/History';
+import CustomSidebar from './Sidebar/CustomSidebar';
 
 class Sidebar extends React.Component {
 
   static propTypes = {
+    navigationUIController: PropTypes.any.isRequired,
   };
 
   state = {
+    currentContentsId: 'custom',
+    isDrawerOpen: false,
   };
 
-  renderSidebarContents = () => (
+  openDrawer = () => this.setState({ isDrawerOpen: true });
+
+  closeDrawer = () => this.setState({ isDrawerOpen: false });
+
+  itemSelectedHandler = (contentsId) => {
+    const { navigationUIController } = this.props;
+    const { currentContentsId } = this.state;
+
+    // already selected
+    if (currentContentsId === contentsId) {
+      navigationUIController.toggleCollapse();
+    }
+    // switch and expand
+    else {
+      this.setState({ currentContentsId: contentsId });
+      navigationUIController.expand();
+    }
+
+    // if (contentsId === 'drawer') {
+    //   this.openDrawer();
+    // }
+  }
+
+  renderGlobalNavigation = () => (
     <>
-      <HeaderSection>
-        { () => (
-          <div className="grw-product-nav-header">
-            <Wordmark wordmark={JiraWordmark} />
-          </div>
-        ) }
-      </HeaderSection>
-      <MenuSection>
-        { () => (
-          <div className="grw-product-nav-menu">
-            <Item
-              before={BacklogIcon}
-              text="Backlog"
-              isSelected
-            />
-            <Item
-              before={BoardIcon}
-              text="Active sprints"
-            />
-            <Item
-              before={GraphLineIcon}
-              text="Reports"
-            />
-            <Separator />
-            <GroupHeading>Shortcuts</GroupHeading>
-            <Item before={ShortcutIcon} text="Project space" />
-            <Item before={ShortcutIcon} text="Looooooooooooooooooooooooooooooong Menu" />
-          </div>
-        ) }
-      </MenuSection>
+      <div className="grw-logo">
+        <a href="/"><GrowiLogo /></a>
+      </div>
+      <SidebarNav currentContentsId={this.state.currentContentsId} onItemSelected={this.itemSelectedHandler} />
+      <Drawer onClose={this.closeDrawer} isOpen={this.state.isDrawerOpen} width="wide">
+        <code>Drawer contents</code>
+      </Drawer>
     </>
   );
 
+  renderSidebarContents = () => {
+    let contents = <CustomSidebar></CustomSidebar>;
+
+    switch (this.state.currentContentsId) {
+      case 'history':
+        contents = <History></History>;
+        break;
+    }
+
+    return contents;
+  }
+
   render() {
     return (
       <ThemeProvider
@@ -80,31 +89,34 @@ class Sidebar extends React.Component {
           }),
         })}
       >
-        <NavigationProvider>
-          <LayoutManager
-            globalNavigation={SidebarNav}
-            productNavigation={() => null}
-            containerNavigation={this.renderSidebarContents}
-            experimental_flyoutOnHover
-            experimental_alternateFlyoutBehaviour
-            // experimental_fullWidthFlyout
-            shouldHideGlobalNavShadow
-            showContextualNavigation
-          >
-          </LayoutManager>
-        </NavigationProvider>
+        <LayoutManager
+          globalNavigation={this.renderGlobalNavigation}
+          productNavigation={() => null}
+          containerNavigation={this.renderSidebarContents}
+          experimental_hideNavVisuallyOnCollapse
+          experimental_flyoutOnHover
+          experimental_alternateFlyoutBehaviour
+          // experimental_fullWidthFlyout
+          shouldHideGlobalNavShadow
+          showContextualNavigation
+        >
+        </LayoutManager>
       </ThemeProvider>
     );
   }
 
 }
 
+const SidebarWithNavigationUI = withNavigationUIController(Sidebar);
+const SidebarWithNavigationUIAndTranslation = withTranslation()(SidebarWithNavigationUI);
 
 /**
  * Wrapper component for using unstated
  */
 const SidebarWrapper = (props) => {
-  return createSubscribedElement(Sidebar, props, [AppContainer]);
+  return createSubscribedElement(SidebarWithNavigationUIAndTranslation, props, [AppContainer]);
 };
 
-export default withTranslation()(SidebarWrapper);
+export default () => (
+  <NavigationProvider><SidebarWrapper /></NavigationProvider>
+);

+ 53 - 0
src/client/js/components/Sidebar/CustomSidebar.jsx

@@ -0,0 +1,53 @@
+import React from 'react';
+// import PropTypes from 'prop-types';
+
+import { withTranslation } from 'react-i18next';
+
+import { JiraWordmark } from '@atlaskit/logo';
+
+import {
+  HeaderSection,
+  MenuSection,
+  Wordmark,
+} from '@atlaskit/navigation-next';
+
+import { createSubscribedElement } from '../UnstatedUtils';
+import AppContainer from '../../services/AppContainer';
+
+class CustomSidebar extends React.Component {
+
+  static propTypes = {
+  };
+
+  state = {
+  };
+
+  render() {
+    return (
+      <>
+        <HeaderSection>
+          { () => (
+            <div className="grw-product-nav-header">
+              <Wordmark wordmark={JiraWordmark} />
+            </div>
+          ) }
+        </HeaderSection>
+        <MenuSection>
+          { () => (
+            <span>(TBD) CustomSidebar Contents</span>
+          ) }
+        </MenuSection>
+      </>
+    );
+  }
+
+}
+
+/**
+ * Wrapper component for using unstated
+ */
+const CustomSidebarWrapper = (props) => {
+  return createSubscribedElement(CustomSidebar, props, [AppContainer]);
+};
+
+export default withTranslation()(CustomSidebarWrapper);

+ 53 - 0
src/client/js/components/Sidebar/History.jsx

@@ -0,0 +1,53 @@
+import React from 'react';
+// import PropTypes from 'prop-types';
+
+import { withTranslation } from 'react-i18next';
+
+import { JiraWordmark } from '@atlaskit/logo';
+
+import {
+  HeaderSection,
+  MenuSection,
+  Wordmark,
+} from '@atlaskit/navigation-next';
+
+import { createSubscribedElement } from '../UnstatedUtils';
+import AppContainer from '../../services/AppContainer';
+
+class History extends React.Component {
+
+  static propTypes = {
+  };
+
+  state = {
+  };
+
+  render() {
+    return (
+      <>
+        <HeaderSection>
+          { () => (
+            <div className="grw-product-nav-header">
+              <Wordmark wordmark={JiraWordmark} />
+            </div>
+          ) }
+        </HeaderSection>
+        <MenuSection>
+          { () => (
+            <span>(TBD) History Contents</span>
+          ) }
+        </MenuSection>
+      </>
+    );
+  }
+
+}
+
+/**
+ * Wrapper component for using unstated
+ */
+const HistoryWrapper = (props) => {
+  return createSubscribedElement(History, props, [AppContainer]);
+};
+
+export default withTranslation()(HistoryWrapper);

+ 26 - 25
src/client/js/components/Sidebar/SidebarNav.jsx

@@ -1,5 +1,5 @@
 import React from 'react';
-// import PropTypes from 'prop-types';
+import PropTypes from 'prop-types';
 
 import { withTranslation } from 'react-i18next';
 
@@ -9,46 +9,47 @@ import TrayIcon from '@atlaskit/icon/glyph/tray';
 import {
   GlobalNav,
 } from '@atlaskit/navigation-next';
-import Drawer from '@atlaskit/drawer';
 
 import { createSubscribedElement } from '../UnstatedUtils';
 import AppContainer from '../../services/AppContainer';
 
-import GrowiLogo from '../GrowiLogo';
 
 class SidebarNav extends React.Component {
 
-  propTypes = {
+  static propTypes = {
+    currentContentsId: PropTypes.string,
+    onItemSelected: PropTypes.func,
   };
 
   state = {
-    isDrawerOpen: false,
   };
 
-  openDrawer = () => this.setState({ isDrawerOpen: true });
+  itemSelectedHandler = (contentsId) => {
+    const { onItemSelected } = this.props;
+    if (onItemSelected != null) {
+      onItemSelected(contentsId);
+    }
+  }
 
-  closeDrawer = () => this.setState({ isDrawerOpen: false });
+  generateSidebarItemObj(id, icon, label) {
+    return {
+      id,
+      icon,
+      label,
+      isSelected: this.props.currentContentsId === id,
+      onClick: () => this.itemSelectedHandler(id),
+    };
+  }
 
   render() {
-    const { isDrawerOpen } = this.state;
     return (
-      <>
-        <div className="grw-logo">
-          <GrowiLogo />
-        </div>
-        <GlobalNav
-          primaryItems={[
-            { id: 'create', icon: EditIcon, label: 'Create' },
-            {
-              id: 'drawer', icon: TrayIcon, label: 'Drawer', onClick: this.openDrawer,
-            },
-          ]}
-          secondaryItems={[]}
-        />
-        <Drawer onClose={this.closeDrawer} isOpen={isDrawerOpen} width="wide">
-          <code>Drawer contents</code>
-        </Drawer>
-      </>
+      <GlobalNav
+        primaryItems={[
+          this.generateSidebarItemObj('custom', EditIcon, 'Custom Sidebar'),
+          this.generateSidebarItemObj('history', TrayIcon, 'History'),
+        ]}
+        secondaryItems={[]}
+      />
     );
   }
 

+ 2 - 2
src/client/js/services/PageContainer.js

@@ -8,7 +8,7 @@ import * as toastr from 'toastr';
 import { throttle } from 'throttle-debounce';
 
 const logger = loggerFactory('growi:services:PageContainer');
-const scrollAmountForFixed = 122;
+const scrollAmountForFixed = 50;
 
 /**
  * Service container related to Page
@@ -41,7 +41,7 @@ export default class PageContainer extends Container {
       revisionAuthor: JSON.parse(mainContent.getAttribute('data-page-revision-author')),
       path: mainContent.getAttribute('data-path'),
       tocHtml: '',
-      isLiked: mainContent.getAttribute('data-page-is-liked'),
+      isLiked: JSON.parse(mainContent.getAttribute('data-page-is-liked')),
       seenUserIds: [],
       likerUserIds: [],
       createdAt: mainContent.getAttribute('data-page-created-at'),

+ 3 - 1
src/client/styles/scss/_comment_growi.scss

@@ -56,6 +56,8 @@
 
   .page-comment {
     position: relative;
+    padding-top: 70px;
+    margin-top: -70px;
 
     // ユーザー名
     .page-comment-creator {
@@ -72,7 +74,7 @@
     // コメントセクション
     .page-comment-main {
       @extend %comment-section;
-      @include media-breakpoint-up(sm){
+      @include media-breakpoint-up(sm) {
         margin-left: 4.5em !important;
       }
       @include media-breakpoint-down(xs) {

+ 1 - 0
src/client/styles/scss/_layout_kibela.scss

@@ -98,6 +98,7 @@ body.kibela {
     left: 0px;
     z-index: absolute;
     height: 11em;
+    max-width: 840px;
     margin: auto;
     border-top: solid 0.4em #5584e1;
     border-radius: 0.35em;

+ 1 - 1
src/client/styles/scss/_login.scss

@@ -122,7 +122,7 @@
   .collapse-external-auth {
     overflow: hidden;
 
-    &:not(.in) {
+    &:not(.show) {
       height: 0;
       padding: 0 !important;
     }

+ 1 - 1
src/client/styles/scss/_navbar.scss

@@ -7,7 +7,7 @@
     padding: 0 1rem;
   }
 
-  .personal-dropdown > .dropdown-toggle::after {
+  #personal-dropdown::after {
     // hide caret
     content: none;
   }

+ 1 - 0
src/client/styles/scss/_on-edit.scss

@@ -25,6 +25,7 @@ body.on-edit {
   }
 
   // hide unnecessary elements
+  header,
   .grw-subnav,
   .row.row-alerts,
   .row.page-list,

+ 8 - 8
src/client/styles/scss/_override-bootstrap-variables.scss

@@ -4,14 +4,14 @@
 
 //== Colors
 //
-$primary: #112744;
-$secondary: #6c757d;
-$info: #009fbb;
-$success: #00bb83;
-$warning: #ffa32b;
-$danger: #ff0a54;
-$light: #dee2e6;
-$dark: #343a40;
+$primary: $growi-blue !default;
+$secondary: #6c757d !default;
+$info: #009fbb !default;
+$success: #00bb83 !default;
+$warning: #ffa32b !default;
+$danger: #ff0a54 !default;
+$light: #e4e7ea !default;
+$dark: #3e4d6c !default;
 
 //== Typography
 //

+ 9 - 0
src/client/styles/scss/_override-bootstrap.scss

@@ -82,6 +82,11 @@ h6 {
   .dropdown-toggle.btn.disabled {
     cursor: not-allowed;
   }
+
+  // hide caret
+  .dropdown-toggle.dropdown-toggle-no-caret::after {
+    content: none;
+  }
 }
 
 // agile-admin style
@@ -175,4 +180,8 @@ fieldset[disabled] .btn {
 // badge
 .badge {
   letter-spacing: 0.05em;
+  &.badge-warning {
+    // badge-warning text is white color in bootstrap3
+    color: white;
+  }
 }

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

@@ -1,5 +1,7 @@
 .growi {
   header {
+    // Adjust to be on top of the growi subnavigation
+    z-index: $zindex-sticky - 100;
     ul.authors {
       padding-left: 1.5em;
       margin: 0;

+ 0 - 3
src/client/styles/scss/_vendor.scss

@@ -7,9 +7,6 @@
 // import toastr styles
 @import '~toastr/build/toastr';
 
-// import metismenu styles
-@import '~metismenu/dist/metisMenu';
-
 // import CodeMirror styles
 @import '~codemirror/lib/codemirror.css';
 @import '~codemirror/theme/elegant.css';

+ 4 - 3
src/server/models/page.js

@@ -2,6 +2,7 @@
 /* eslint-disable no-return-await */
 
 /* eslint-disable no-use-before-define */
+const logger = require('@alias/logger')('growi:models:page');
 
 const debug = require('debug')('growi:models:page');
 const nodePath = require('path');
@@ -367,12 +368,12 @@ module.exports = function(crowi) {
           if (err) {
             return reject(err);
           }
-          debug('liker updated!', added);
+          logger.debug('liker updated!', added);
           return resolve(data);
         });
       }
       else {
-        this.logger.warn('liker not updated');
+        logger.debug('liker not updated');
         return reject(self);
       }
     }));
@@ -393,7 +394,7 @@ module.exports = function(crowi) {
         });
       }
       else {
-        debug('liker not updated');
+        logger.debug('liker not updated');
         return reject(self);
       }
     }));

+ 157 - 0
src/server/routes/apiv3/bookmarks.js

@@ -0,0 +1,157 @@
+const loggerFactory = require('@alias/logger');
+
+const logger = loggerFactory('growi:routes:apiv3:bookmark'); // eslint-disable-line no-unused-vars
+
+const express = require('express');
+const { body } = require('express-validator');
+
+const router = express.Router();
+
+/**
+ * @swagger
+ *  tags:
+ *    name: Bookmarks
+ */
+
+/**
+ * @swagger
+ *
+ *  components:
+ *    schemas:
+ *      Bookmark:
+ *        description: Bookmark
+ *        type: object
+ *        properties:
+ *          _id:
+ *            type: string
+ *            description: page ID
+ *            example: 5e07345972560e001761fa63
+ *          __v:
+ *            type: number
+ *            description: DB record version
+ *            example: 0
+ *          createdAt:
+ *            type: string
+ *            description: date created at
+ *            example: 2010-01-01T00:00:00.000Z
+ *          page:
+ *            $ref: '#/components/schemas/Page/properties/_id'
+ *          user:
+ *            $ref: '#/components/schemas/User/properties/_id'
+ *
+ *      BookmarkParams:
+ *        description: BookmarkParams
+ *        type: object
+ *        properties:
+ *          pageId:
+ *            type: string
+ *            description: page ID
+ *            example: 5e07345972560e001761fa63
+ *          bool:
+ *            type: boolean
+ *            description: boolean for bookmark status
+ */
+
+module.exports = (crowi) => {
+  const accessTokenParser = require('../../middleware/access-token-parser')(crowi);
+  const loginRequired = require('../../middleware/login-required')(crowi);
+  const csrf = require('../../middleware/csrf')(crowi);
+
+  const { Page, Bookmark } = crowi.models;
+  const { ApiV3FormValidator } = crowi.middlewares;
+
+  const validator = {
+    bookmarks: [
+      body('pageId').isString(),
+      body('bool').isBoolean(),
+    ],
+  };
+
+  /**
+   * @swagger
+   *
+   *    /bookmarks:
+   *      get:
+   *        tags: [Bookmarks]
+   *        summary: /bookmarks
+   *        description: Get bookmarked status
+   *        operationId: getBookmarkedStatus
+   *        parameters:
+   *          - name: pageId
+   *            in: query
+   *            description: page id
+   *            schema:
+   *              type: string
+   *        responses:
+   *          200:
+   *            description: Succeeded to get bookmarked status.
+   *            content:
+   *              application/json:
+   *                schema:
+   *                  $ref: '#/components/schemas/Bookmark'
+   */
+  router.get('/', accessTokenParser, loginRequired, async(req, res) => {
+    const { pageId } = req.query;
+
+    try {
+      const bookmark = await Bookmark.findByPageIdAndUserId(pageId, req.user);
+      return res.apiv3({ bookmark });
+    }
+    catch (err) {
+      logger.error('get-bookmark-failed', err);
+      return res.apiv3Err(err, 500);
+    }
+  });
+
+
+  /**
+   * @swagger
+   *
+   *    /bookmarks:
+   *      put:
+   *        tags: [Bookmarks]
+   *        summary: /bookmarks
+   *        description: Update bookmarked status
+   *        operationId: updateBookmarkedStatus
+   *        requestBody:
+   *          content:
+   *            application/json:
+   *              schema:
+   *                $ref: '#/components/schemas/BookmarkParams'
+   *        responses:
+   *          200:
+   *            description: Succeeded to update bookmarked status.
+   *            content:
+   *              application/json:
+   *                schema:
+   *                  $ref: '#/components/schemas/Bookmark'
+   */
+  router.put('/', accessTokenParser, loginRequired, csrf, validator.bookmarks, ApiV3FormValidator, async(req, res) => {
+    const { pageId, bool } = req.body;
+
+    let bookmark;
+    try {
+      const page = await Page.findByIdAndViewer(pageId, req.user);
+      if (page == null) {
+        return res.apiv3Err(`Page '${pageId}' is not found or forbidden`);
+      }
+      if (bool) {
+        bookmark = await Bookmark.add(page, req.user);
+      }
+      else {
+        bookmark = await Bookmark.removeBookmark(page, req.user);
+      }
+    }
+    catch (err) {
+      logger.error('update-bookmark-failed', err);
+      return res.apiv3Err(err, 500);
+    }
+
+    bookmark.depopulate('page');
+    bookmark.depopulate('user');
+
+    return res.apiv3({ bookmark });
+  });
+
+  return router;
+};

+ 4 - 2
src/server/routes/apiv3/index.js

@@ -18,9 +18,7 @@ module.exports = (crowi) => {
   router.use('/markdown-setting', require('./markdown-setting')(crowi));
   router.use('/app-settings', require('./app-settings')(crowi));
   router.use('/customize-setting', require('./customize-setting')(crowi));
-
   router.use('/notification-setting', require('./notification-setting')(crowi));
-
   router.use('/users', require('./users')(crowi));
   router.use('/user-groups', require('./user-group')(crowi));
   router.use('/export', require('./export')(crowi));
@@ -39,5 +37,9 @@ module.exports = (crowi) => {
 
   router.use('/search', require('./search')(crowi));
 
+  router.use('/page', require('./page')(crowi));
+
+  router.use('/bookmarks', require('./bookmarks')(crowi));
+
   return router;
 };

+ 187 - 0
src/server/routes/apiv3/page.js

@@ -0,0 +1,187 @@
+const loggerFactory = require('@alias/logger');
+
+const logger = loggerFactory('growi:routes:apiv3:page'); // eslint-disable-line no-unused-vars
+
+const express = require('express');
+const { body } = require('express-validator');
+
+const router = express.Router();
+
+// const ErrorV3 = require('../../models/vo/error-apiv3');
+
+/**
+ * @swagger
+ *  tags:
+ *    name: Page
+ */
+
+/**
+ * @swagger
+ *
+ *  components:
+ *    schemas:
+ *      Page:
+ *        description: Page
+ *        type: object
+ *        properties:
+ *          _id:
+ *            type: string
+ *            description: page ID
+ *            example: 5e07345972560e001761fa63
+ *          __v:
+ *            type: number
+ *            description: DB record version
+ *            example: 0
+ *          commentCount:
+ *            type: number
+ *            description: count of comments
+ *            example: 3
+ *          createdAt:
+ *            type: string
+ *            description: date created at
+ *            example: 2010-01-01T00:00:00.000Z
+ *          creator:
+ *            $ref: '#/components/schemas/User'
+ *          extended:
+ *            type: object
+ *            description: extend data
+ *            example: {}
+ *          grant:
+ *            type: number
+ *            description: grant
+ *            example: 1
+ *          grantedUsers:
+ *            type: array
+ *            description: granted users
+ *            items:
+ *              type: string
+ *              description: user ID
+ *            example: ["5ae5fccfc5577b0004dbd8ab"]
+ *          lastUpdateUser:
+ *            $ref: '#/components/schemas/User'
+ *          liker:
+ *            type: array
+ *            description: granted users
+ *            items:
+ *              type: string
+ *              description: user ID
+ *            example: []
+ *          path:
+ *            type: string
+ *            description: page path
+ *            example: /
+ *          redirectTo:
+ *            type: string
+ *            description: redirect path
+ *            example: ""
+ *          revision:
+ *            type: string
+ *            description: page revision
+ *          seenUsers:
+ *            type: array
+ *            description: granted users
+ *            items:
+ *              type: string
+ *              description: user ID
+ *            example: ["5ae5fccfc5577b0004dbd8ab"]
+ *          status:
+ *            type: string
+ *            description: status
+ *            enum:
+ *              - 'wip'
+ *              - 'published'
+ *              - 'deleted'
+ *              - 'deprecated'
+ *            example: published
+ *          updatedAt:
+ *            type: string
+ *            description: date updated at
+ *            example: 2010-01-01T00:00:00.000Z
+ *
+ *      LikeParams:
+ *        description: LikeParams
+ *        type: object
+ *        properties:
+ *          pageId:
+ *            type: string
+ *            description: page ID
+ *            example: 5e07345972560e001761fa63
+ *          bool:
+ *            type: boolean
+ *            description: boolean for like status
+ */
+module.exports = (crowi) => {
+  const accessTokenParser = require('../../middleware/access-token-parser')(crowi);
+  const loginRequired = require('../../middleware/login-required')(crowi);
+  const csrf = require('../../middleware/csrf')(crowi);
+
+  const globalNotificationService = crowi.getGlobalNotificationService();
+  const { Page, GlobalNotificationSetting } = crowi.models;
+  const { ApiV3FormValidator } = crowi.middlewares;
+
+
+  const validator = {
+    likes: [
+      body('pageId').isString(),
+      body('bool').isBoolean(),
+    ],
+  };
+
+  /**
+   * @swagger
+   *
+   *    /page/likes:
+   *      put:
+   *        tags: [Page]
+   *        summary: /page/likes
+   *        description: Update liked status
+   *        operationId: updateLikedStatus
+   *        requestBody:
+   *          content:
+   *            application/json:
+   *              schema:
+   *                $ref: '#/components/schemas/LikeParams'
+   *        responses:
+   *          200:
+   *            description: Succeeded to update liked status.
+   *            content:
+   *              application/json:
+   *                schema:
+   *                  $ref: '#/components/schemas/Page'
+   */
+  router.put('/likes', accessTokenParser, loginRequired, csrf, validator.likes, ApiV3FormValidator, async(req, res) => {
+    const { pageId, bool } = req.body;
+
+    let page;
+    try {
+      page = await Page.findByIdAndViewer(pageId, req.user);
+      if (page == null) {
+        return res.apiv3Err(`Page '${pageId}' is not found or forbidden`);
+      }
+      if (bool) {
+        page = await page.like(req.user);
+      }
+      else {
+        page = await page.unlike(req.user);
+      }
+    }
+    catch (err) {
+      logger.error('update-like-failed', err);
+      return res.apiv3Err(err, 500);
+    }
+
+    try {
+      // global notification
+      await globalNotificationService.fire(GlobalNotificationSetting.EVENT.PAGE_LIKE, page, req.user);
+    }
+    catch (err) {
+      logger.error('Like notification failed', err);
+    }
+
+    const result = { page };
+    result.seenUser = page.seenUsers;
+    return res.apiv3({ result });
+  });
+
+  return router;
+};

+ 0 - 218
src/server/routes/bookmark.js

@@ -1,218 +0,0 @@
-/**
- * @swagger
- *  tags:
- *    name: Bookmarks
- */
-
-/**
- * @swagger
- *
- *  components:
- *    schemas:
- *      Bookmark:
- *        description: Bookmark
- *        type: object
- *        properties:
- *          _id:
- *            type: string
- *            description: page ID
- *            example: 5e07345972560e001761fa63
- *          __v:
- *            type: number
- *            description: DB record version
- *            example: 0
- *          createdAt:
- *            type: string
- *            description: date created at
- *            example: 2010-01-01T00:00:00.000Z
- *          page:
- *            $ref: '#/components/schemas/Page/properties/_id'
- *          user:
- *            $ref: '#/components/schemas/User/properties/_id'
- */
-
-module.exports = function(crowi, app) {
-  const debug = require('debug')('growi:routes:bookmark');
-  const Bookmark = crowi.model('Bookmark');
-  const Page = crowi.model('Page');
-  const ApiResponse = require('../util/apiResponse');
-  const ApiPaginate = require('../util/apiPaginate');
-  const actions = {};
-  actions.api = {};
-
-  /**
-   * @swagger
-   *
-   *    /bookmarks.get:
-   *      get:
-   *        tags: [Bookmarks, CrowiCompatibles]
-   *        operationId: getBookmark
-   *        summary: /bookmarks.get
-   *        description: Get bookmark of the page with the user
-   *        parameters:
-   *          - in: query
-   *            name: page_id
-   *            required: true
-   *            schema:
-   *              $ref: '#/components/schemas/Page/properties/_id'
-   *        responses:
-   *          200:
-   *            description: Succeeded to get bookmark of the page with the user.
-   *            content:
-   *              application/json:
-   *                schema:
-   *                  properties:
-   *                    ok:
-   *                      $ref: '#/components/schemas/V1Response/properties/ok'
-   *                    bookmark:
-   *                      $ref: '#/components/schemas/Bookmark'
-   *          403:
-   *            $ref: '#/components/responses/403'
-   *          500:
-   *            $ref: '#/components/responses/500'
-   */
-  /**
-   * @api {get} /bookmarks.get Get bookmark of the page with the user
-   * @apiName GetBookmarks
-   * @apiGroup Bookmark
-   *
-   * @apiParam {String} page_id Page Id.
-   */
-  actions.api.get = function(req, res) {
-    const pageId = req.query.page_id;
-
-    Bookmark.findByPageIdAndUserId(pageId, req.user)
-      .then((data) => {
-        debug('bookmark found', pageId, data);
-        const result = {};
-
-        result.bookmark = data;
-        return res.json(ApiResponse.success(result));
-      })
-      .catch((err) => {
-        return res.json(ApiResponse.error(err));
-      });
-  };
-
-  actions.api.list = function(req, res) {
-    const paginateOptions = ApiPaginate.parseOptions(req.query);
-
-    const options = Object.assign(paginateOptions, { populatePage: true });
-    Bookmark.findByUserId(req.user._id, options)
-      .then((result) => {
-        return res.json(ApiResponse.success(result));
-      })
-      .catch((err) => {
-        return res.json(ApiResponse.error(err));
-      });
-  };
-
-  /**
-   * @swagger
-   *
-   *    /bookmarks.add:
-   *      post:
-   *        tags: [Bookmarks, CrowiCompatibles]
-   *        operationId: addBookmark
-   *        summary: /bookmarks.add
-   *        description: Add bookmark of the page
-   *        parameters:
-   *          - in: query
-   *            name: page_id
-   *            schema:
-   *              $ref: '#/components/schemas/Page/properties/_id'
-   *            required: true
-   *        responses:
-   *          200:
-   *            description: Succeeded to add bookmark of the page.
-   *            content:
-   *              application/json:
-   *                schema:
-   *                  properties:
-   *                    ok:
-   *                      $ref: '#/components/schemas/V1Response/properties/ok'
-   *                    bookmark:
-   *                      $ref: '#/components/schemas/Bookmark'
-   *          403:
-   *            $ref: '#/components/responses/403'
-   *          500:
-   *            $ref: '#/components/responses/500'
-   */
-  /**
-   * @api {post} /bookmarks.add Add bookmark of the page
-   * @apiName AddBookmark
-   * @apiGroup Bookmark
-   *
-   * @apiParam {String} page_id Page Id.
-   */
-  actions.api.add = async function(req, res) {
-    const pageId = req.body.page_id;
-
-    const page = await Page.findByIdAndViewer(pageId, req.user);
-    if (page == null) {
-      return res.json(ApiResponse.success({ bookmark: null }));
-    }
-
-    const bookmark = await Bookmark.add(page, req.user);
-
-    bookmark.depopulate('page');
-    bookmark.depopulate('user');
-    const result = { bookmark };
-
-    return res.json(ApiResponse.success(result));
-  };
-
-  /**
-   * @swagger
-   *
-   *    /bookmarks.remove:
-   *      post:
-   *        tags: [Bookmarks, CrowiCompatibles]
-   *        operationId: removeBookmark
-   *        summary: /bookmarks.remove
-   *        description: Remove bookmark of the page
-   *        requestBody:
-   *          content:
-   *            application/json:
-   *              schema:
-   *                properties:
-   *                  page_id:
-   *                    $ref: '#/components/schemas/Page/properties/_id'
-   *                required:
-   *                  - page_id
-   *        responses:
-   *          200:
-   *            description: Succeeded to remove bookmark of the page.
-   *            content:
-   *              application/json:
-   *                schema:
-   *                  properties:
-   *                    ok:
-   *                      $ref: '#/components/schemas/V1Response/properties/ok'
-   *          403:
-   *            $ref: '#/components/responses/403'
-   *          500:
-   *            $ref: '#/components/responses/500'
-   */
-  /**
-   * @api {post} /bookmarks.remove Remove bookmark of the page
-   * @apiName RemoveBookmark
-   * @apiGroup Bookmark
-   *
-   * @apiParam {String} page_id Page Id.
-   */
-  actions.api.remove = function(req, res) {
-    const pageId = req.body.page_id;
-
-    Bookmark.removeBookmark(pageId, req.user)
-      .then((data) => {
-        debug('Bookmark removed.', data); // if the bookmark is not exists, this 'data' is null
-        return res.json(ApiResponse.success());
-      })
-      .catch((err) => {
-        return res.json(ApiResponse.error(err));
-      });
-  };
-
-  return actions;
-};

+ 0 - 6
src/server/routes/index.js

@@ -23,7 +23,6 @@ module.exports = function(crowi, app) {
   const user = require('./user')(crowi, app);
   const attachment = require('./attachment')(crowi, app);
   const comment = require('./comment')(crowi, app);
-  const bookmark = require('./bookmark')(crowi, app);
   const tag = require('./tag')(crowi, app);
   const revision = require('./revision')(crowi, app);
   const search = require('./search')(crowi, app);
@@ -160,11 +159,6 @@ module.exports = function(crowi, app) {
   app.post('/_api/comments.add'       , comment.api.validators.add(), accessTokenParser , loginRequiredStrictly , csrf, comment.api.add);
   app.post('/_api/comments.update'    , comment.api.validators.add(), accessTokenParser , loginRequiredStrictly , csrf, comment.api.update);
   app.post('/_api/comments.remove'    , accessTokenParser , loginRequiredStrictly , csrf, comment.api.remove);
-  app.get('/_api/bookmarks.get'       , accessTokenParser , loginRequired , bookmark.api.get);
-  app.post('/_api/bookmarks.add'      , accessTokenParser , loginRequiredStrictly , csrf, bookmark.api.add);
-  app.post('/_api/bookmarks.remove'   , accessTokenParser , loginRequiredStrictly , csrf, bookmark.api.remove);
-  app.post('/_api/likes.add'          , accessTokenParser , loginRequiredStrictly , csrf, page.api.like);
-  app.post('/_api/likes.remove'       , accessTokenParser , loginRequiredStrictly , csrf, page.api.unlike);
   app.get('/_api/attachments.list'    , accessTokenParser , loginRequired , attachment.api.list);
   app.post('/_api/attachments.add'                  , uploads.single('file'), autoReap, accessTokenParser, loginRequiredStrictly ,csrf, attachment.api.add);
   app.post('/_api/attachments.uploadProfileImage'   , uploads.single('file'), autoReap, accessTokenParser, loginRequiredStrictly ,csrf, attachment.api.uploadProfileImage);

+ 7 - 2
src/server/routes/installer.js

@@ -21,8 +21,13 @@ module.exports = function(crowi, app) {
   }
 
   async function createPage(filePath, pagePath, owner, lang) {
-    const markdown = fs.readFileSync(filePath);
-    return Page.create(pagePath, markdown, owner, {});
+    try {
+      const markdown = fs.readFileSync(filePath);
+      return Page.create(pagePath, markdown, owner, {});
+    }
+    catch (err) {
+      logger.error(`Failed to create ${pagePath}`, err);
+    }
   }
 
   async function createInitialPages(owner, lang) {

+ 0 - 144
src/server/routes/page.js

@@ -1230,150 +1230,6 @@ module.exports = function(crowi, app) {
     return res.json(ApiResponse.success(result));
   };
 
-  /**
-   * @swagger
-   *
-   *    /likes.add:
-   *      post:
-   *        tags: [Likes, CrowiCompatibles]
-   *        operationId: addLike
-   *        summary: /likes.add
-   *        description: Like page
-   *        requestBody:
-   *          content:
-   *            application/json:
-   *              schema:
-   *                properties:
-   *                  page_id:
-   *                    $ref: '#/components/schemas/Page/properties/_id'
-   *                required:
-   *                  - page_id
-   *        responses:
-   *          200:
-   *            description: Succeeded to be page liked.
-   *            content:
-   *              application/json:
-   *                schema:
-   *                  properties:
-   *                    ok:
-   *                      $ref: '#/components/schemas/V1Response/properties/ok'
-   *                    page:
-   *                      $ref: '#/components/schemas/Page'
-   *          403:
-   *            $ref: '#/components/responses/403'
-   *          500:
-   *            $ref: '#/components/responses/500'
-   */
-  /**
-   * @api {post} /likes.add Like page
-   * @apiName LikePage
-   * @apiGroup Page
-   *
-   * @apiParam {String} page_id Page Id.
-   */
-  api.like = async function(req, res) {
-    const pageId = req.body.page_id;
-    if (!pageId) {
-      return res.json(ApiResponse.error('page_id required'));
-    }
-    if (!req.user) {
-      return res.json(ApiResponse.error('user required'));
-    }
-
-    let page;
-    try {
-      page = await Page.findByIdAndViewer(pageId, req.user);
-      if (page == null) {
-        throw new Error(`Page '${pageId}' is not found or forbidden`);
-      }
-      page = await page.like(req.user);
-    }
-    catch (err) {
-      debug('Seen user update error', err);
-      return res.json(ApiResponse.error(err));
-    }
-
-    const result = { page };
-    result.seenUser = page.seenUsers;
-    res.json(ApiResponse.success(result));
-
-    try {
-      // global notification
-      await globalNotificationService.fire(GlobalNotificationSetting.EVENT.PAGE_LIKE, page, req.user);
-    }
-    catch (err) {
-      logger.error('Like notification failed', err);
-    }
-  };
-
-  /**
-   * @swagger
-   *
-   *    /likes.remove:
-   *      post:
-   *        tags: [Likes, CrowiCompatibles]
-   *        operationId: removeLike
-   *        summary: /likes.remove
-   *        description: Unlike page
-   *        requestBody:
-   *          content:
-   *            application/json:
-   *              schema:
-   *                properties:
-   *                  page_id:
-   *                    $ref: '#/components/schemas/Page/properties/_id'
-   *                required:
-   *                  - page_id
-   *        responses:
-   *          200:
-   *            description: Succeeded to not be page liked.
-   *            content:
-   *              application/json:
-   *                schema:
-   *                  properties:
-   *                    ok:
-   *                      $ref: '#/components/schemas/V1Response/properties/ok'
-   *                    page:
-   *                      $ref: '#/components/schemas/Page'
-   *          403:
-   *            $ref: '#/components/responses/403'
-   *          500:
-   *            $ref: '#/components/responses/500'
-   */
-  /**
-   * @api {post} /likes.remove Unlike page
-   * @apiName UnlikePage
-   * @apiGroup Page
-   *
-   * @apiParam {String} page_id Page Id.
-   */
-  api.unlike = async function(req, res) {
-    const pageId = req.body.page_id;
-    if (!pageId) {
-      return res.json(ApiResponse.error('page_id required'));
-    }
-    if (req.user == null) {
-      return res.json(ApiResponse.error('user required'));
-    }
-
-    let page;
-    try {
-      page = await Page.findByIdAndViewer(pageId, req.user);
-      if (page == null) {
-        throw new Error(`Page '${pageId}' is not found or forbidden`);
-      }
-      page = await page.unlike(req.user);
-    }
-    catch (err) {
-      debug('Seen user update error', err);
-      return res.json(ApiResponse.error(err));
-    }
-
-    const result = { page };
-    result.seenUser = page.seenUsers;
-    return res.json(ApiResponse.success(result));
-  };
-
   /**
    * @swagger
    *

+ 1 - 1
src/server/views/layout-crowi/page_list.html

@@ -46,7 +46,7 @@
     {% include '../widget/page_content.html' %}
   </div>
 
-  <div class="row page-list hidden-print {% if page.isPortal() %}mt-5{% endif %}">
+  <div class="row page-list d-print-none {% if page.isPortal() %}mt-5{% endif %}">
     <div class="col-md-12">
       {% include '../widget/page_list_and_timeline.html' %}
     </div>

+ 11 - 6
src/server/views/layout-growi/base/layout.html

@@ -9,12 +9,17 @@
 {% block layout_main %}
 <div class="container-fluid">
 
-  <div class="row grw-subnav">
-    <div class="col-12 grw-title-bar">
-      {% block content_header %}
-      {% endblock %}
-    </div><!-- /.grw-title-bar -->
-  </div><!-- /.row -->
+  <header class="sticky-top py-0" id="page-header">
+
+    <div id="grw-subnav" data-is-forbidden-page="{{ forbidden }}"></div>
+
+      {% if not page and not forbidden and ('/' === path or 'crowi' === getConfig('crowi', 'customize:behavior')) and not isUserPageList(path) and !isTrashPage() %}
+        {% if '/' === path.slice(-1) %}
+          {% include '../../widget/create_portal.html' %}
+        {% endif %}
+      {% endif %}
+
+  </header>
 
   <div class="row">
     <div id="main" class="main col-md-12 {% if page %}{{ css.grant(page) }}{% endif %} {% block main_css_class %}{% endblock %}">

+ 1 - 1
src/server/views/layout-growi/page.html

@@ -35,7 +35,7 @@
   </div>
 
   {% if 'growi' === getConfig('crowi', 'customize:behavior') || 'crowi-plus' === getConfig('crowi', 'customize:behavior') %}
-  <div class="row page-list hidden-print mt-5">
+  <div class="row page-list d-print-none mt-5">
     <div class="col-md-10">
       {% include '../widget/page_list_and_timeline.html' %}
     </div>

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

@@ -25,7 +25,7 @@
     </div>
 
     {# relocate #revision-toc #}
-    <div class="col-xl-2 col-lg-3 d-none d-lg-block revision-toc-container">
+    <div class="col-xl-2 col-lg-3 d-none d-lg-block revision-toc-container pr-2">
       {% include './widget/liker-and-seenusers.html' %}
       <div id="revision-toc" class="revision-toc mt-3 sps sps--abv" data-sps-offset="123">
         <div id="revision-toc-content" class="revision-toc-content"></div>
@@ -34,7 +34,7 @@
 
   </div>
 
-  <div class="row page-list hidden-print {% if page.isPortal() %}mt-5{% endif %}">
+  <div class="row page-list d-print-none {% if page.isPortal() %}mt-5{% endif %}">
     <div class="col-md-10">
       {% include '../widget/page_list_and_timeline.html' %}
     </div>

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

@@ -7,7 +7,7 @@
 
 {% block content_header %}
   {% if pageUser %}
-    {% include '../widget/user_page_header.html' %}
+    <header id="grw-subnav-for-user-page" class="user-page-header" data-page-user="{{ pageUser|json }}"></header>
   {% else %}
     {% parent %}
   {% endif %}
@@ -59,7 +59,7 @@
   </div>
 
   {% if 'growi' === getConfig('crowi', 'customize:behavior') || 'crowi-plus' === getConfig('crowi', 'customize:behavior') %}
-  <div class="row page-list hidden-print mt-5">
+  <div class="row page-list d-print-none mt-5">
     <div class="col-md-10">
       {% include '../widget/page_list_and_timeline.html' %}
     </div>

+ 1 - 1
src/server/views/layout-kibela/base/layout.html

@@ -11,7 +11,7 @@
 
   <div class="row body m-0 p-0">
 
-    <div id="main" class="main col-md-7 col-12 kibela-block bg-white round-corner {% if page %}{{ css.grant(page) }}{% endif %}{% block main_css_class %}{% endblock %}">
+    <div id="main" class="main col-12 kibela-block bg-white round-corner {% if page %}{{ css.grant(page) }}{% endif %}{% block main_css_class %}{% endblock %}">
       <div class="row grw-subnav">
         <div class="col-12 grw-title-bar">
           {% block content_header %} {% endblock %}

+ 1 - 1
src/server/views/layout-kibela/user_page.html

@@ -7,7 +7,7 @@
 
 {% block content_header %}
   {% if pageUser %}
-    {% include '../widget/user_page_header.html' %}
+    <header id="grw-subnav-for-user-page" class="user-page-header" data-page-user="{{ pageUser|json }}"></header>
   {% else %}
     {% parent %}
   {% endif %}

+ 1 - 1
src/server/views/layout/layout.html

@@ -114,7 +114,7 @@
             <i class="icon-question mr-2"></i><span class="d-none d-md-inline-block mr-2">{{ t('Help') }}</span><span class="text-muted small"><i class="icon-share-alt"></i></span>
           </a>
         </li>
-        <li id="personal-dropdown" class="nav-item dropdown"></li>
+        <li id="personal-dropdown" class="nav-item dropdown dropdown-toggle"></li>
         {% else %}
         <li id="login-user" class="nav-item"><a class="nav-link" href="/login">Login</a></li>
         {% endif %}

+ 69 - 67
src/server/views/login.html

@@ -30,74 +30,76 @@
         <div class="logo mb-3">{% include 'widget/logo.html' %}</div>
         <h1>{{ appService.getAppTitle() }}</h1>
 
-        <div class="login-form-errors">
-          {% if isLdapSetupFailed() %}
-          <div class="alert alert-warning small">
-            <strong><i class="icon-fw icon-info"></i>LDAP is enabled but the configuration has something wrong.</strong>
-            <br>
-            (Please set the environment variables <code>DEBUG=crowi:service:PassportService</code> to get the logs)
-          </div>
-          {% endif %}
-
-          {#
-          # The case that there already exists a user whose username matches ID of the newly created LDAP user
-          # https://github.com/weseek/growi/issues/193
-          #}
-          {% set failedProviderForDuplicatedUsernameException = req.flash('provider-DuplicatedUsernameException') %}
-          {% if failedProviderForDuplicatedUsernameException != null %}
-          <div class="alert alert-warning small">
-            <p><strong><i class="icon-fw icon-ban"></i>DuplicatedUsernameException occured</strong></p>
-            <p>
-              Your {{ failedProviderForDuplicatedUsernameException }} authentication was succeess, but a new user could not be created.
-              See the issue <a href="https://github.com/weseek/growi/issues/193">#193</a>.
-            </p>
-          </div>
-          {% endif %}
+        <div class="row">
+          <div class="login-form-errors col-12">
+            {% if isLdapSetupFailed() %}
+            <div class="alert alert-warning small">
+              <strong><i class="icon-fw icon-info"></i>LDAP is enabled but the configuration has something wrong.</strong>
+              <br>
+              (Please set the environment variables <code>DEBUG=crowi:service:PassportService</code> to get the logs)
+            </div>
+            {% endif %}
 
-          {% set success = req.flash('successMessage') %}
-          {% if success.length %}
-          <div class="alert alert-success">
-            {{ success }}
-          </div>
-          {% endif %}
+            {#
+            # The case that there already exists a user whose username matches ID of the newly created LDAP user
+            # https://github.com/weseek/growi/issues/193
+            #}
+            {% set failedProviderForDuplicatedUsernameException = req.flash('provider-DuplicatedUsernameException') %}
+            {% if failedProviderForDuplicatedUsernameException != null %}
+            <div class="alert alert-warning small">
+              <p><strong><i class="icon-fw icon-ban"></i>DuplicatedUsernameException occured</strong></p>
+              <p>
+                Your {{ failedProviderForDuplicatedUsernameException }} authentication was succeess, but a new user could not be created.
+                See the issue <a href="https://github.com/weseek/growi/issues/193">#193</a>.
+              </p>
+            </div>
+            {% endif %}
 
-          {% set warn = req.flash('warningMessage') %}
-          {% if warn.length %}
-          {% for w in warn %}
-          <div class="alert alert-warning">
-            {{ w }}
-          </div>
-          {% endfor %}
-          {% endif %}
-
-          {% set error = req.flash('errorMessage') %}
-          {% if error.length %}
-          {% for e in error %}
-          <div class="alert alert-danger">
-            {{ e }}
-          </div>
-          {% endfor %}
-          {% endif %}
-
-          {% if req.form.errors.length > 0 %}
-          <div class="alert alert-danger">
-            <ul>
-            {% for error in req.form.errors %}
-              <li>{{ error }}</li>
+            {% set success = req.flash('successMessage') %}
+            {% if success.length %}
+            <div class="alert alert-success">
+              {{ success }}
+            </div>
+            {% endif %}
+
+            {% set warn = req.flash('warningMessage') %}
+            {% if warn.length %}
+            {% for w in warn %}
+            <div class="alert alert-warning">
+              {{ w }}
+            </div>
             {% endfor %}
-            </ul>
+            {% endif %}
+
+            {% set error = req.flash('errorMessage') %}
+            {% if error.length %}
+            {% for e in error %}
+            <div class="alert alert-danger">
+              {{ e }}
+            </div>
+            {% endfor %}
+            {% endif %}
+
+            {% if req.form.errors.length > 0 %}
+            <div class="alert alert-danger">
+              <ul>
+              {% for error in req.form.errors %}
+                <li>{{ error }}</li>
+              {% endfor %}
+              </ul>
+            </div>
+            {% endif %}
           </div>
-          {% endif %}
-        </div>
-        <div id="register-form-errors">
-          {% set message = req.flash('registerWarningMessage') %}
-          {% if message.length %}
-          <div class="alert alert-danger">
-            {% for msg in message %}
-            {{ msg }}<br>
-            {% endfor  %}
+          <div id="register-form-errors">
+            {% set message = req.flash('registerWarningMessage') %}
+            {% if message.length %}
+            <div class="alert alert-danger">
+              {% for msg in message %}
+              {{ msg }}<br>
+              {% endfor  %}
+            </div>
+            {% endif %}
           </div>
-          {% endif %}
         </div>
       </div>
     </div>
@@ -118,17 +120,17 @@
             {% if isLocalOrLdapStrategiesEnabled %}
             <form role="form" action="/login" method="post">
 
-              <div class="input-group">
+              <div class="input-group mb-3">
                 <div class="input-group-prepend">
                   <span class="input-group-text"><i class="icon-user"></i></span>
                 </div>
                 <input type="text" class="form-control" placeholder="Username or E-mail" name="loginForm[username]">
                 {% if passportService.isLdapStrategySetup %}
-                <span class="input-group-append">
-                  <small class="text-success">
+                <div class="input-group-append">
+                  <small class="input-group-text text-success">
                     <i class="icon-fw icon-check"></i> LDAP
                   </small>
-                </span>
+                </div>
                 {% endif %}
               </div>
 

+ 2 - 2
src/server/views/modal/create_template.html

@@ -19,7 +19,7 @@
                   <p class="text-center"><code>_template</code></p>
                   <p class="form-text text-muted text-center"><small>{{ t('template.children.desc') }}</small></p>
                 </div>
-                <div class="card-footer text-center">
+                <div class="card-footer text-center bg-white">
                   <a href="{{templateParentPath}}_template#edit"
                       class="btn btn-sm btn-primary" id="template-button-children">
                       {{ t("Edit") }}
@@ -34,7 +34,7 @@
                   <p class="text-center"><code>__template</code></p>
                   <p class="form-text text-muted text-center"><small>{{ t('template.decendants.desc') }}</small></p>
                 </div>
-                <div class="card-footer text-center">
+                <div class="card-footer text-center bg-white">
                   <a href="{{templateParentPath}}__template#edit"
                       class="btn btn-sm btn-primary" id="template-button-decendants">
                       {{ t("Edit") }}

+ 1 - 1
src/server/views/modal/duplicate.html

@@ -20,7 +20,7 @@
                 <span class="input-group-text">{{ baseUrl }}</span>
               </div>
                 {% if isSearchServiceConfigured() %}
-                <div id="duplicate-page-name-input" class="page-name-input"></div>
+                <div id="duplicate-page-name-input" class="page-name-input flex-fill"></div>
                 {% else %}
                 <input type="text" class="form-control" name="new_path" id="duplicatePageName" value="{{ page.path }}">
                 {% endif %}

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

@@ -43,7 +43,7 @@
       <form role="form" id="unlink-page-form" onsubmit="return false;">
         <input type="hidden" name="_csrf" value="{{ csrf() }}">
         <input type="hidden" name="path" value="{{ path }}">
-        <button type="submit" class="btn btn-light btn-sm float-right">
+        <button type="submit" class="btn btn-outline-secondary btn-sm float-right">
           <i class="ti-unlink" aria-hidden="true"></i>
           Unlink
         </button>
@@ -90,7 +90,7 @@
       </div>
       {% if page.isDeleted() and user %}
       <div>
-        <button href="#" class="btn btn-light rounded-pill btn-sm mr-2" data-target="#putBackPage" data-toggle="modal"><i class="icon-action-undo" aria-hidden="true"></i> {{ t('Put Back') }}</button>
+        <button href="#" class="btn btn-outline-secondary rounded-pill btn-sm mr-2" data-target="#putBackPage" data-toggle="modal"><i class="icon-action-undo" aria-hidden="true"></i> {{ t('Put Back') }}</button>
         <button href="#" class="btn btn-danger rounded-pill btn-sm mr-2" {% if !user.canDeleteCompletely(page.creator._id) %} disabled="disabled" {% endif %} data-target="#deletePage" data-toggle="modal"><i class="icon-fire" aria-hidden="true"></i> {{ t('Delete Completely') }}</button>
       {# /.float-right #}
       </div>

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

@@ -10,7 +10,7 @@
 <li>
   <img src="{{ page.lastUpdateUser|picture }}" class="picture rounded-circle">
   <a href="{{ page.path }}"
-    class="page-list-link"
+    class="page-list-link text-break"
     data-path="{{ page.path }}">{{ decodeURIComponent(page.path) }}
   </a>
   <span class="page-list-meta">

+ 6 - 6
src/server/views/widget/page_tabs.html

@@ -68,11 +68,11 @@
   <!-- icon-options-vertical -->
   {% if !isTrashPage() %}
     {% if page.isPortal() %}
-    <li class="nav-item">
+    <li class="nav-item dropdown">
       <a
-        {% if user %} role="button" class="nav-link" data-toggle="dropdown" {% endif %}
+        {% if user %} role="button" class="nav-link dropdown-toggle dropdown-toggle-no-caret" data-toggle="dropdown" {% endif %}
         {% if not user %}
-          class="dropdown-toggle dropdown-toggle-disabled"
+          class="nav-link dropdown-toggle dropdown-toggle-disabled dropdown-toggle-no-caret"
           data-toggle="tooltip" data-placement="top" data-container="body" title="{{ t('Not available for guest') }}"
         {% endif %}
       >
@@ -90,11 +90,11 @@
       </div>
     </li>
     {% else %}
-    <li class="nav-item dropdown d-flex align-items-center">
+    <li class="nav-item dropdown">
       <a
-        {% if user %} role="button" data-toggle="dropdown" {% endif %}
+        {% if user %} role="button" class="nav-link dropdown-toggle dropdown-toggle-no-caret" data-toggle="dropdown" {% endif %}
         {% if not user %}
-          class="dropdown-toggle dropdown-toggle-disabled"
+          class="nav-link dropdown-toggle dropdown-toggle-disabled dropdown-toggle-no-caret"
           data-toggle="tooltip" data-placement="top" data-container="body" title="{{ t('Not available for guest') }}"
         {% endif %}
       >

+ 49 - 41
src/server/views/widget/page_tabs_kibela.html

@@ -1,21 +1,21 @@
 {% if page %}
-<ul class="nav nav-tabs hidden-print">
+<ul class="nav nav-tabs d-print-none">
 
   {#
     Left Tabs
   #}
-  <li class="nav-item grw-nav-main-left-tab active">
+  <li class="nav-item active">
     <a class="nav-link active" href="#revision-body" data-toggle="tab">
       <i class="icon-control-play"></i> View
     </a>
   </li>
 
   {% if !isTrashPage() %}
-  <li class="grw-nav-main-left-tab nav-tab-edit">
+  <li class="nav-item nav-tab-edit">
     <a
-      {% if user %} href="#edit" data-toggle="tab" class="edit-button" {% endif %}
+      {% if user %} href="#edit" data-toggle="tab" class="nav-link edit-button" {% endif %}
       {% if not user %}
-        class="edit-button edit-button-disabled"
+        class="nav-link edit-button edit-button-disabled"
         data-toggle="tooltip" data-placement="top" data-container="body" title="{{ t('Not available for guest') }}"
       {% endif %}
     >
@@ -23,11 +23,11 @@
     </a>
   </li>
   {% if isHackmdSetup() %}
-  <li class="grw-nav-main-left-tab nav-tab-hackmd">
+  <li class="nav-item nav-tab-hackmd">
     <a
-      {% if user %} href="#hackmd" data-toggle="tab" class="edit-button" {% endif %}
+      {% if user %} href="#hackmd" data-toggle="tab" class="nav-link edit-button" {% endif %}
       {% if not user %}
-        class="edit-button edit-button-disabled"
+        class="nav-link edit-button edit-button-disabled"
         data-toggle="tooltip" data-placement="top" data-container="body" title="{{ t('Not available for guest') }}"
       {% endif %}
     >
@@ -40,77 +40,85 @@
   {#
     Right Tabs
   #}
+  {# to place right side #}
+  <div class="mr-auto"></div>
+
+  {% if not page.isPortal() %}
+  <li class="nav-item">
+    <a href="?presentation=1" class="nav-link toggle-presentation">
+      <i class="icon-film"></i><span class="d-none d-sm-inline"> {{ t('Presentation Mode') }}</span>
+    </a>
+  </li>
+  {% endif %}
+
+  <li class="nav-item">
+    <a href="#revision-history" class="nav-link" data-toggle="tab">
+      <i class="icon-layers"></i><span class="d-none d-sm-inline"> {{ t('History') }}</span>
+    </a>
+  </li>
+  
   {% if !isTrashPage() %}
     {% if page.isPortal() %}
-    <li class="float-right dropdown">
+    <li class="nav-item dropdown">
       <a
-        {% if user %} role="button" class="dropdown-toggle" data-toggle="dropdown" {% endif %}
+        {% if user %} role="button" class="nav-link dropdown-toggle dropdown-toggle-no-caret" data-toggle="dropdown" {% endif %}
         {% if not user %}
-          class="dropdown-toggle dropdown-toggle-disabled"
+          class="nav-link dropdown-toggle dropdown-toggle-disabled dropdown-toggle-no-caret"
           data-toggle="tooltip" data-placement="top" data-container="body" title="{{ t('Not available for guest') }}"
         {% endif %}
       >
         <i class="icon-options-vertical"></i>
       </a>
-      <ul class="dropdown-menu">
-        <li><a href="#" data-target="#create-template" data-toggle="modal"><i class="icon-fw icon-magic-wand"></i> {{ t('template.option_label.create/edit') }}</a></li>
+      <ul class="dropdown-menu dropdown-menu-right">
+        <li class="dropdown-item">
+          <a href="#" data-target="#create-template" data-toggle="modal"><i class="icon-fw icon-magic-wand"></i> {{ t('template.option_label.create/edit') }}</a>
+        </li>
         {% if ('/' !== path) %}
-        <li class="divider"></li>
-        <li><a href="#" data-target="#unportalize" data-toggle="modal"><i class="fa fa-share"></i> {{ t('Unportalize') }}</a></li>
+        <li class="dropdown-divider"></li>
+        <li class="dropdown-item"><a href="#" data-target="#unportalize" data-toggle="modal"><i class="fa fa-share"></i> {{ t('Unportalize') }}</a></li>
         {% endif %}
       </ul>
     </li>
     {% else %}
-    <li class="dropdown float-right">
+    <li class="nav-item dropdown">
       <a
-        {% if user %} role="button" class="dropdown-toggle" data-toggle="dropdown" {% endif %}
+        {% if user %} role="button" class="nav-link dropdown-toggle dropdown-toggle-no-caret" data-toggle="dropdown" {% endif %}
         {% if not user %}
-          class="dropdown-toggle dropdown-toggle-disabled"
+          class="nav-link dropdown-toggle dropdown-toggle-disabled dropdown-toggle-no-caret"
           data-toggle="tooltip" data-placement="top" data-container="body" title="{{ t('Not available for guest') }}"
         {% endif %}
       >
         <i class="icon-options-vertical"></i>
       </a>
-      <ul class="dropdown-menu">
-        <li><a href="#" data-target="#renamePage" data-toggle="modal"><i class="icon-fw icon-action-redo"></i> {{ t('Move/Rename') }}</a></li>
-        <li><a href="#" data-target="#duplicatePage" data-toggle="modal"><i class="icon-fw icon-docs"></i> {{ t('Duplicate') }}</a></li>
-        <li class="divider"></li>
-        <li><a href="#" data-target="#create-template" data-toggle="modal"><i class="icon-fw icon-magic-wand"></i> {{ t('template.option_label.create/edit') }}</a></li>
+      <ul class="dropdown-menu dropdown-menu-right">
+        <li class="dropdown-item"><a href="#" data-target="#renamePage" data-toggle="modal"><i class="icon-fw icon-action-redo"></i> {{ t('Move/Rename') }}</a></li>
+        <li class="dropdown-item"><a href="#" data-target="#duplicatePage" data-toggle="modal"><i class="icon-fw icon-docs"></i> {{ t('Duplicate') }}</a></li>
+        <li class="dropdown-divider"></li>
+        <li class="dropdown-item">
+          <a href="#" data-target="#create-template" data-toggle="modal"><i class="icon-fw icon-magic-wand"></i> {{ t('template.option_label.create/edit') }}</a>
+        </li>
         {% if isDeletablePage() %}
-        <li class="divider"></li>
-        <li><a href="#" data-target="#deletePage" data-toggle="modal"><i class="icon-fw icon-fire text-danger"></i> {{ t('Delete') }}</a></li>
+        <li class="dropdown-divider"></li>
+        <li class="dropdown-item"><a href="#" data-target="#deletePage" data-toggle="modal"><i class="icon-fw icon-fire text-danger"></i> {{ t('Delete') }}</a></li>
         {% endif %}
       </ul>
     </li>
     {% endif %}
   {% endif %}
 
-  <li class="float-right">
-    <a href="#revision-history" data-toggle="tab">
-      <i class="icon-layers"></i><span class="hidden-xs"> {{ t('History') }}</span>
-    </a>
-  </li>
-  {% if not page.isPortal() %}
-    <li class="float-right">
-      <a href="?presentation=1" class="toggle-presentation">
-        <i class="icon-film"></i><span class="hidden-xs"> {{ t('Presentation Mode') }}</span>
-      </a>
-    </li>
-  {% endif %}
-
 </ul>
 
 {% else %} {# for creating portal #}
 
-<ul class="nav nav-tabs nav-tabs-create-portal hidden-print">
+<ul class="nav nav-tabs nav-tabs-create-portal d-print-none">
 
-  <li class="nav-item grw-nav-main-left-tab">
+  <li class="nav-item ">
     <a id="portal-form-close" class="nav-link" href="#" data-toggle="tab">
       <i class="icon-action-undo"></i> {{ t('Cancel') }}
     </a>
   </li>
 
-  <li class="nav-item grw-nav-main-left-tab active">
+  <li class="nav-item  active">
     <a class="nav-link">
       <i class="icon-note"></i> {{ t('Create') }}
     </a>

+ 0 - 34
src/server/views/widget/user_page_header.html

@@ -1,34 +0,0 @@
-<header id="page-header" class="user-page-header">
-
-  <h4 id="revision-path"></h4>
-
-  <div class="users-info d-flex align-items-center">
-    <img src="{{ pageUser|picture }}" class="picture rounded-circle">
-    <div class="users-meta" style="flex: 1;">
-      <div class="d-flex align-items-center">
-        <h1>
-          {{ pageUser.name }}
-        </h1>
-      </div>
-      <div class="user-page-meta">
-        <ul>
-          <li class="user-page-username"><i class="icon-user"></i> {{ pageUser.username }}</li>
-          <li class="user-page-email">
-            <i class="icon-envelope"></i>
-            {% if pageUser.isEmailPublished %}
-              {{ pageUser.email }}
-            {% else %}
-              *****
-            {% endif %}
-          </li>
-          {% if pageUser.introduction %}
-          <li class="user-page-introduction"><p>{{ pageUser.introduction|nl2br }}</p></li>
-          {% endif %}
-        </ul>
-      </div>
-    </div>
-    <div class="d-flex">
-      {% include 'header-buttons-lg.html' %}
-    </div>
-  </div>
-</header>

+ 7 - 11
yarn.lock

@@ -9112,10 +9112,6 @@ methods@~1.1.1, methods@~1.1.2:
   version "1.1.2"
   resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee"
 
-metismenu@^3.0.3:
-  version "3.0.3"
-  resolved "https://registry.yarnpkg.com/metismenu/-/metismenu-3.0.3.tgz#961e4c9469144d5078f6228b6e049e58f3137140"
-
 micromatch@2.3.11:
   version "2.3.11"
   resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-2.3.11.tgz#86677c97d1720b363431d04d0d15293bd38c1565"
@@ -11752,13 +11748,6 @@ react-i18next@^11.1.0:
     "@babel/runtime" "^7.3.1"
     html-parse-stringify2 "2.0.1"
 
-react-input-autosize@^2.2.2:
-  version "2.2.2"
-  resolved "https://registry.yarnpkg.com/react-input-autosize/-/react-input-autosize-2.2.2.tgz#fcaa7020568ec206bc04be36f4eb68e647c4d8c2"
-  integrity sha512-jQJgYCA3S0j+cuOwzuCd1OjmBmnZLdqQdiLKRYrsMMzbjUrVDS5RvJUDwJqA7sKuksDuzFtm6hZGKFu7Mjk5aw==
-  dependencies:
-    prop-types "^15.5.8"
-
 react-image-crop@^8.3.0:
   version "8.3.0"
   resolved "https://registry.yarnpkg.com/react-image-crop/-/react-image-crop-8.3.0.tgz#a0642dd3daafd77f142bac01887628cb967876b7"
@@ -11768,6 +11757,13 @@ react-image-crop@^8.3.0:
     core-js "^3.2.1"
     prop-types "^15.7.2"
 
+react-input-autosize@^2.2.2:
+  version "2.2.2"
+  resolved "https://registry.yarnpkg.com/react-input-autosize/-/react-input-autosize-2.2.2.tgz#fcaa7020568ec206bc04be36f4eb68e647c4d8c2"
+  integrity sha512-jQJgYCA3S0j+cuOwzuCd1OjmBmnZLdqQdiLKRYrsMMzbjUrVDS5RvJUDwJqA7sKuksDuzFtm6hZGKFu7Mjk5aw==
+  dependencies:
+    prop-types "^15.5.8"
+
 react-is@^16.12.0:
   version "16.12.0"
   resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.12.0.tgz#2cc0fe0fba742d97fd527c42a13bec4eeb06241c"