Procházet zdrojové kódy

Merge remote-tracking branch 'origin/support/apply-bootstrap4' into dev/4.0.x

Yuki Takei před 6 roky
rodič
revize
3c09f17059
85 změnil soubory, kde provedl 941 přidání a 881 odebrání
  1. 2 2
      resource/locales/ja/translation.json
  2. 2 5
      src/client/js/app.jsx
  3. 2 2
      src/client/js/components/Admin/UserManagement.jsx
  4. 3 3
      src/client/js/components/Admin/Users/UserInviteModal.jsx
  5. 1 1
      src/client/js/components/Admin/Users/UserTable.jsx
  6. 1 1
      src/client/js/components/BookmarkButton.jsx
  7. 1 1
      src/client/js/components/HeaderSearchBox.jsx
  8. 20 11
      src/client/js/components/InstallerForm.jsx
  9. 11 27
      src/client/js/components/LikeButton.jsx
  10. 4 4
      src/client/js/components/Me/ApiSettings.jsx
  11. 40 14
      src/client/js/components/Me/AssociateModal.jsx
  12. 19 15
      src/client/js/components/Me/BasicInfoSettings.jsx
  13. 2 2
      src/client/js/components/Me/ExternalAccountLinkedMe.jsx
  14. 8 12
      src/client/js/components/Me/ImageCropModal.jsx
  15. 16 18
      src/client/js/components/Me/PasswordSettings.jsx
  16. 8 8
      src/client/js/components/Me/PersonalSettings.jsx
  17. 9 7
      src/client/js/components/Me/ProfileImageSettings.jsx
  18. 17 10
      src/client/js/components/Navbar/GrowiSubNavigation.jsx
  19. 83 0
      src/client/js/components/Navbar/GrowiSubNavigationForUserPage.jsx
  20. 12 6
      src/client/js/components/Navbar/PageCreator.jsx
  21. 3 1
      src/client/js/components/Navbar/PersonalDropdown.jsx
  22. 11 6
      src/client/js/components/Navbar/RevisionAuthor.jsx
  23. 1 1
      src/client/js/components/Page/RevisionPath.jsx
  24. 1 1
      src/client/js/components/Page/TagEditor.jsx
  25. 2 2
      src/client/js/components/PageAttachment/Attachment.jsx
  26. 1 1
      src/client/js/components/PageComment/Comment.jsx
  27. 2 2
      src/client/js/components/PageComment/CommentEditor.jsx
  28. 3 3
      src/client/js/components/PageComment/DeleteCommentModal.jsx
  29. 1 1
      src/client/js/components/PageEditor/Editor.jsx
  30. 3 3
      src/client/js/components/PageEditor/OptionsSelector.jsx
  31. 1 1
      src/client/js/components/PageEditorByHackmd.jsx
  32. 1 1
      src/client/js/components/SavePageControls/GrantSelector.jsx
  33. 16 6
      src/client/js/components/SearchPage/DeletePageListModal.jsx
  34. 3 2
      src/client/js/components/SearchPage/SearchResult.jsx
  35. 1 1
      src/client/js/components/SearchPage/SearchResultList.jsx
  36. 1 1
      src/client/js/components/SlackNotification.jsx
  37. 1 0
      src/client/js/services/AdminGeneralSecurityContainer.js
  38. 9 5
      src/client/js/services/PageContainer.js
  39. 54 0
      src/client/styles/scss/_layout_kibela.scss
  40. 19 63
      src/client/styles/scss/_login.scss
  41. 2 2
      src/client/styles/scss/_override-bootstrap-variables.scss
  42. 10 0
      src/client/styles/scss/_override-bootstrap.scss
  43. 15 0
      src/client/styles/scss/_page_growi.scss
  44. 58 13
      src/client/styles/scss/atoms/_buttons.scss
  45. 8 0
      src/client/styles/scss/theme/_apply-colors-dark.scss
  46. 8 0
      src/client/styles/scss/theme/_apply-colors-light.scss
  47. 6 2
      src/client/styles/scss/theme/_apply-colors.scss
  48. 39 0
      src/client/styles/scss/theme/_layout_kibela_variable.scss
  49. 9 5
      src/client/styles/scss/theme/_reboot-bootstrap-colors.scss
  50. 12 0
      src/client/styles/scss/theme/default.scss
  51. 1 0
      src/client/styles/scss/theme/kibela.scss
  52. 4 3
      src/server/models/page.js
  53. 2 2
      src/server/routes/apiv3/index.js
  54. 2 2
      src/server/routes/apiv3/notification-setting.js
  55. 187 0
      src/server/routes/apiv3/page.js
  56. 0 218
      src/server/routes/bookmark.js
  57. 0 6
      src/server/routes/index.js
  58. 0 144
      src/server/routes/page.js
  59. 20 12
      src/server/views/invited.html
  60. 0 1
      src/server/views/layout-crowi/page.html
  61. 1 2
      src/server/views/layout-crowi/page_list.html
  62. 1 1
      src/server/views/layout-growi/page.html
  63. 1 1
      src/server/views/layout-growi/page_list.html
  64. 2 2
      src/server/views/layout-growi/user_page.html
  65. 1 1
      src/server/views/layout-kibela/user_page.html
  66. 1 1
      src/server/views/layout-kibela/widget/header.html
  67. 78 76
      src/server/views/login.html
  68. 3 3
      src/server/views/modal/create_page.html
  69. 1 1
      src/server/views/modal/duplicate.html
  70. 7 6
      src/server/views/modal/put_back.html
  71. 3 1
      src/server/views/modal/rename.html
  72. 6 6
      src/server/views/widget/forbidden_content.html
  73. 0 6
      src/server/views/widget/header-button-bookmark.html
  74. 0 6
      src/server/views/widget/header-button-like.html
  75. 0 13
      src/server/views/widget/header-buttons-lg.html
  76. 0 12
      src/server/views/widget/header-buttons.html
  77. 1 1
      src/server/views/widget/modal/page-api-error-messages.html
  78. 1 1
      src/server/views/widget/not_creatable_content.html
  79. 1 1
      src/server/views/widget/not_found_content.html
  80. 3 3
      src/server/views/widget/not_found_tabs.html
  81. 1 1
      src/server/views/widget/page_attachments.html
  82. 1 0
      src/server/views/widget/page_content.html
  83. 1 1
      src/server/views/widget/page_list.html
  84. 49 41
      src/server/views/widget/page_tabs_kibela.html
  85. 0 34
      src/server/views/widget/user_page_header.html

+ 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 - 5
src/client/js/app.jsx

@@ -23,8 +23,6 @@ import PageAttachment from './components/PageAttachment';
 import PageStatusAlert from './components/PageStatusAlert';
 import RevisionPath from './components/Page/RevisionPath';
 import TagLabels from './components/Page/TagLabels';
-import BookmarkButton from './components/BookmarkButton';
-import LikeButton from './components/LikeButton';
 import PagePathAutoComplete from './components/PagePathAutoComplete';
 import RecentCreated from './components/RecentCreated/RecentCreated';
 import MyDraftList from './components/MyDraftList/MyDraftList';
@@ -37,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';
@@ -90,11 +89,8 @@ if (pageContainer.state.pageId != null) {
     'page-timeline': <PageTimeline />,
     'page-comment-write': <CommentEditorLazyRenderer />,
     'revision-toc': <TableOfContents />,
-    'like-button': <LikeButton pageId={pageContainer.state.pageId} isLiked={pageContainer.state.isLiked} />,
     'seen-user-list': <UserPictureList userIds={pageContainer.state.seenUserIds} />,
     'liker-list': <UserPictureList userIds={pageContainer.state.likerUserIds} />,
-    'bookmark-button': <BookmarkButton pageId={pageContainer.state.pageId} crowi={appContainer} />,
-    'bookmark-button-lg': <BookmarkButton pageId={pageContainer.state.pageId} crowi={appContainer} size="lg" />,
     'rename-page-name-input': <PagePathAutoComplete crowi={appContainer} initializedPath={pageContainer.state.path} />,
     'duplicate-page-name-input': <PagePathAutoComplete crowi={appContainer} initializedPath={pageContainer.state.path} />,
   });
@@ -106,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 />,
   });
 }
 

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

@@ -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>
 

+ 3 - 3
src/client/js/components/Admin/Users/UserInviteModal.jsx

@@ -87,7 +87,7 @@ class UserInviteModal extends React.Component {
         <div>
           <button
             type="button"
-            className="fcbtn btn btn-outline-light rounded-pill mr-2"
+            className="btn btn-outline-danger rounded-pill mr-2"
             onClick={this.onToggleModal}
           >
             Cancel
@@ -95,7 +95,7 @@ class UserInviteModal extends React.Component {
 
           <button
             type="button"
-            className="fcbtn btn btn-outline-primary rounded-pill btn-1b"
+            className="btn btn-outline-primary rounded-pill"
             onClick={this.handleSubmit}
             disabled={!this.validEmail()}
           >
@@ -116,7 +116,7 @@ class UserInviteModal extends React.Component {
         </label>
         <button
           type="button"
-          className="fcbtn btn btn-primary"
+          className="btn btn-outline-primary"
           onClick={this.onToggleModal}
         >
           Close

+ 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:

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

@@ -78,7 +78,7 @@ export default class BookmarkButton extends React.Component {
         href="#"
         title="Bookmark"
         onClick={this.handleClick}
-        className={`btn btn-circle btn-outline-warning border-0 ${addedClassName}`}
+        className={`btn btn-circle btn-outline-warning btn-bookmark border-0 ${addedClassName}`}
       >
         <i className="icon-star"></i>
       </button>

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

@@ -85,7 +85,7 @@ class HeaderSearchBox extends React.Component {
             placeholder="Search ..."
           />
           <div className="btn-group-submit-search">
-            <span className="btn-link" onClick={this.search}>
+            <span className="btn-link text-decoration-none" onClick={this.search}>
               <i className="icon-magnifier"></i>
             </span>
           </div>

+ 20 - 11
src/client/js/components/InstallerForm.jsx

@@ -90,8 +90,10 @@ class InstallerForm extends React.Component {
               </div>
             </div>
 
-            <div className={`input-group${hasErrorClass}`}>
-              <span className="input-group-addon"><i className="icon-user" /></span>
+            <div className={`input-group mb-3${hasErrorClass}`}>
+              <div className="input-group-prepend">
+                <span className="input-group-text"><i className="icon-user" /></span>
+              </div>
               <input
                 type="text"
                 className="form-control"
@@ -104,8 +106,10 @@ class InstallerForm extends React.Component {
             </div>
             <p className="form-text">{ unavailableUserId }</p>
 
-            <div className="input-group">
-              <span className="input-group-addon"><i className="icon-tag" /></span>
+            <div className="input-group mb-3">
+              <div className="input-group-prepend">
+                <span className="input-group-text"><i className="icon-tag" /></span>
+              </div>
               <input
                 type="text"
                 className="form-control"
@@ -116,8 +120,10 @@ class InstallerForm extends React.Component {
               />
             </div>
 
-            <div className="input-group">
-              <span className="input-group-addon"><i className="icon-envelope" /></span>
+            <div className="input-group mb-3">
+              <div className="input-group-prepend">
+                <span className="input-group-text"><i className="icon-envelope" /></span>
+              </div>
               <input
                 type="email"
                 className="form-control"
@@ -128,8 +134,10 @@ class InstallerForm extends React.Component {
               />
             </div>
 
-            <div className="input-group">
-              <span className="input-group-addon"><i className="icon-lock" /></span>
+            <div className="input-group mb-3">
+              <div className="input-group-prepend">
+                <span className="input-group-text"><i className="icon-lock" /></span>
+              </div>
               <input
                 type="password"
                 className="form-control"
@@ -142,9 +150,10 @@ class InstallerForm extends React.Component {
             <input type="hidden" name="_csrf" value={this.props.csrf} />
 
             <div className="input-group mt-4 mb-3 d-flex justify-content-center">
-              <button type="submit" className="fcbtn btn btn-success btn-1b btn-register">
-                <span className="btn-label"><i className="icon-user-follow" /></span>
-                <span className="btn-label-text">{ this.props.t('Create') }</span>
+              <button type="submit" className="btn-fill btn btn-register px-0 py-2" id="register">
+                <div className="eff"></div>
+                <span className="btn-label p-3"><i className="icon-user-follow" /></span>
+                <span className="btn-label-text p-3">{ this.props.t('Create') }</span>
               </button>
             </div>
 

+ 11 - 27
src/client/js/components/LikeButton.jsx

@@ -1,6 +1,7 @@
 import React from 'react';
 import PropTypes from 'prop-types';
 
+import { toastError } from '../util/apiNotification';
 import { createSubscribedElement } from './UnstatedUtils';
 import AppContainer from '../services/AppContainer';
 
@@ -10,29 +11,21 @@ class LikeButton extends React.Component {
     super(props);
 
     this.state = {
-      isLiked: !!props.isLiked,
+      isLiked: props.isLiked,
     };
 
     this.handleClick = this.handleClick.bind(this);
   }
 
-  handleClick(event) {
-    event.preventDefault();
-
-    const { appContainer } = this.props;
-    const pageId = this.props.pageId;
-
-    if (!this.state.isLiked) {
-      appContainer.apiPost('/likes.add', { page_id: pageId })
-        .then((res) => {
-          this.setState({ isLiked: true });
-        });
+  async handleClick() {
+    const { appContainer, pageId } = this.props;
+    const bool = !this.state.isLiked;
+    try {
+      await appContainer.apiv3.put('/page/likes', { pageId, bool });
+      this.setState({ isLiked: bool });
     }
-    else {
-      appContainer.apiPost('/likes.remove', { page_id: pageId })
-        .then((res) => {
-          this.setState({ isLiked: false });
-        });
+    catch (err) {
+      toastError(err);
     }
   }
 
@@ -46,20 +39,11 @@ class LikeButton extends React.Component {
       return <div></div>;
     }
 
-    const btnSizeClassName = this.props.size ? `btn-${this.props.size}` : 'btn-md';
-    const addedClassNames = [
-      this.state.isLiked ? 'active' : '',
-      btnSizeClassName,
-    ];
-    const addedClassName = addedClassNames.join(' ');
-
     return (
       <button
         type="button"
-        href="#"
-        title="Like"
         onClick={this.handleClick}
-        className={`btn btn-circle btn-outline-info border-0 ${addedClassName}`}
+        className={`btn btn-circle btn-outline-info btn-like border-0 ${this.state.isLiked ? 'active' : ''}`}
       >
         <i className="icon-like"></i>
       </button>

+ 4 - 4
src/client/js/components/Me/ApiSettings.jsx

@@ -43,8 +43,8 @@ class ApiSettings extends React.Component {
         </div>
 
         <div className="row mb-3">
-          <label htmlFor="apiToken" className="col-xs-3 text-right">{t('Current API Token')}</label>
-          <div className="col-xs-6">
+          <label htmlFor="apiToken" className="col-3 text-right">{t('Current API Token')}</label>
+          <div className="col-6">
             {personalContainer.state.apiToken != null
             ? (
               <input
@@ -65,7 +65,7 @@ class ApiSettings extends React.Component {
 
 
         <div className="row">
-          <div className="col-xs-offset-3 col-xs-6">
+          <div className="offset-3 col-6">
 
             <p className="alert alert-warning">
               { t('page_me_apitoken.notice.update_token1') }<br />
@@ -76,7 +76,7 @@ class ApiSettings extends React.Component {
         </div>
 
         <div className="row my-3">
-          <div className="col-xs-offset-4 col-xs-5">
+          <div className="offset-4 col-5">
             <button
               type="button"
               className="btn btn-primary"

+ 40 - 14
src/client/js/components/Me/AssociateModal.jsx

@@ -70,28 +70,54 @@ class AssociateModal extends React.Component {
     const { t } = this.props;
 
     return (
-      <Modal isOpen={this.props.isOpen} toggle={this.props.onClose}>
+      <Modal isOpen={this.props.isOpen} toggle={this.props.onClose} className="mw-100 m-4">
         <ModalHeader className="bg-info" toggle={this.props.onClose}>
           { t('Create External Account') }
         </ModalHeader>
         <ModalBody>
           <ul className="nav nav-tabs passport-settings mb-2" role="tablist">
-            <li className="active">
-              <a href="#passport-ldap" data-toggle="tab" role="tab"><i className="fa fa-sitemap"></i> LDAP</a>
+            <li className="nav-item active">
+              <a href="#passport-ldap" className="nav-link active" data-toggle="tab" role="tab">
+                <i className="fa fa-sitemap"></i> LDAP
+              </a>
+            </li>
+            <li className="nav-item">
+              <a href="#github-tbd" className="nav-link" data-toggle="tab" role="tab">
+                <i className="fa fa-github"></i> (TBD) GitHub
+              </a>
+            </li>
+            <li className="nav-item">
+              <a href="#google-tbd" className="nav-link" data-toggle="tab" role="tab">
+                <i className="fa fa-google"></i> (TBD) Google OAuth
+              </a>
+            </li>
+            <li className="nav-item">
+              <a href="#facebook-tbd" className="nav-link" data-toggle="tab" role="tab">
+                <i className="fa fa-facebook"></i> (TBD) Facebook
+              </a>
+            </li>
+            <li className="nav-item">
+              <a href="#twitter-tbd" className="nav-link" data-toggle="tab" role="tab">
+                <i className="fa fa-twitter"></i> (TBD) Twitter
+              </a>
             </li>
-            <li className="tbd disabled"><a><i className="fa fa-github"></i> (TBD) GitHub</a></li>
-            <li className="tbd disabled"><a><i className="fa fa-google"></i> (TBD) Google OAuth</a></li>
-            <li className="tbd disabled"><a><i className="fa fa-facebook"></i> (TBD) Facebook</a></li>
-            <li className="tbd disabled"><a><i className="fa fa-twitter"></i> (TBD) Twitter</a></li>
           </ul>
-          <LdapAuthTest
-            username={this.state.username}
-            password={this.state.password}
-            onChangeUsername={this.onChangeUsername}
-            onChangePassword={this.onChangePassword}
-          />
+          <div className="tab-content">
+            <div id="passport-ldap" className="tab-pane active">
+              <LdapAuthTest
+                username={this.state.username}
+                password={this.state.password}
+                onChangeUsername={this.onChangeUsername}
+                onChangePassword={this.onChangePassword}
+              />
+            </div>
+            <div id="github-tbd" className="tab-pane" role="tabpanel">TBD</div>
+            <div id="google-tbd" className="tab-pane" role="tabpanel">TBD</div>
+            <div id="facebook-tbd" className="tab-pane" role="tabpanel">TBD</div>
+            <div id="twitter-tbd" className="tab-pane" role="tabpanel">TBD</div>
+          </div>
         </ModalBody>
-        <ModalFooter>
+        <ModalFooter className="border-top-0">
           <button type="button" className="btn btn-info mt-3" onClick={this.onClickAddBtn}>
             <i className="fa fa-plus-circle" aria-hidden="true"></i>
             {t('add')}

+ 19 - 15
src/client/js/components/Me/BasicInfoSettings.jsx

@@ -45,7 +45,7 @@ class BasicInfoSettings extends React.Component {
     return (
       <Fragment>
 
-        <div className="row mb-3">
+        <div className="row form-group mb-3">
           <label htmlFor="userForm[name]" className="col-sm-2 text-right">{t('Name')}</label>
           <div className="col-sm-4 text-left">
             <input
@@ -58,7 +58,7 @@ class BasicInfoSettings extends React.Component {
           </div>
         </div>
 
-        <div className="row mb-3">
+        <div className="row form-group mb-3">
           <label htmlFor="userForm[email]" className="col-sm-2 text-right">{t('Email')}</label>
           <div className="col-sm-4 text-left">
             <input
@@ -82,59 +82,63 @@ class BasicInfoSettings extends React.Component {
         </div>
 
         <div className="row mb-3">
-          <label className="col-xs-2 text-right">{t('Disclose E-mail')}</label>
-          <div className="col-xs-6">
-            <div className="radio radio-primary radio-inline">
+          <label className="col-sm-2 text-right">{t('Disclose E-mail')}</label>
+          <div className="col-6">
+            <div className="custom-control custom-radio custom-control-inline">
               <input
                 type="radio"
                 id="radioEmailShow"
+                className="custom-control-input"
                 name="userForm[isEmailPublished]"
                 checked={personalContainer.state.isEmailPublished}
                 onChange={() => { personalContainer.changeIsEmailPublished(true) }}
               />
-              <label htmlFor="radioEmailShow">{t('Show')}</label>
+              <label className="custom-control-label" htmlFor="radioEmailShow">{t('Show')}</label>
             </div>
-            <div className="radio radio-primary radio-inline">
+            <div className="custom-control custom-radio custom-control-inline">
               <input
                 type="radio"
                 id="radioEmailHide"
+                className="custom-control-input"
                 name="userForm[isEmailPublished]"
                 checked={!personalContainer.state.isEmailPublished}
                 onChange={() => { personalContainer.changeIsEmailPublished(false) }}
               />
-              <label htmlFor="radioEmailHide">{t('Hide')}</label>
+              <label className="custom-control-label" htmlFor="radioEmailHide">{t('Hide')}</label>
             </div>
           </div>
         </div>
 
         <div className="row mb-3">
-          <label className="col-xs-2 text-right">{t('Language')}</label>
-          <div className="col-xs-6">
-            <div className="radio radio-primary radio-inline">
+          <label className="col-sm-2 col-form-label text-right">{t('Language')}</label>
+          <div className="col-6">
+            <div className="custom-control custom-radio custom-control-inline">
               <input
                 type="radio"
                 id="radioLangEn"
+                className="custom-control-input"
                 name="userForm[lang]"
                 checked={personalContainer.state.lang === 'en-US'}
                 onChange={() => { personalContainer.changeLang('en-US') }}
               />
-              <label htmlFor="radioLangEn">{t('English')}</label>
+              <label className="custom-control-label" htmlFor="radioLangEn">{t('English')}</label>
             </div>
-            <div className="radio radio-primary radio-inline">
+            <div className="custom-control custom-radio custom-control-inline">
               <input
                 type="radio"
                 id="radioLangJa"
+                className="custom-control-input"
                 name="userForm[lang]"
                 checked={personalContainer.state.lang === 'ja'}
                 onChange={() => { personalContainer.changeLang('ja') }}
               />
-              <label htmlFor="radioLangJa">{t('Japanese')}</label>
+              <label className="custom-control-label" htmlFor="radioLangJa">{t('Japanese')}</label>
             </div>
           </div>
         </div>
 
         <div className="row my-3">
-          <div className="col-xs-offset-4 col-xs-5">
+          <div className="offset-4 col-5">
             <button type="button" className="btn btn-primary" onClick={this.onClickSubmit} disabled={personalContainer.state.retrieveError != null}>
               {t('Update')}
             </button>

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

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

+ 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>
     );

+ 16 - 18
src/client/js/components/Me/PasswordSettings.jsx

@@ -76,8 +76,8 @@ class PasswordSettings extends React.Component {
         {(personalContainer.state.isPasswordSet)
         && (
           <div className="row mb-3">
-            <label htmlFor="oldPassword" className="col-xs-3 text-right">{ t('personal_settings.current_password') }</label>
-            <div className="col-xs-6">
+            <label htmlFor="oldPassword" className="col-3 text-right">{ t('personal_settings.current_password') }</label>
+            <div className="col-6">
               <input
                 className="form-control"
                 type="password"
@@ -89,8 +89,8 @@ class PasswordSettings extends React.Component {
           </div>
         )}
         <div className="row mb-3">
-          <label htmlFor="newPassword" className="col-xs-3 text-right">{t('personal_settings.new_password') }</label>
-          <div className="col-xs-6">
+          <label htmlFor="newPassword" className="col-3 text-right">{t('personal_settings.new_password') }</label>
+          <div className="col-6">
             <input
               className="form-control"
               type="password"
@@ -101,10 +101,10 @@ class PasswordSettings extends React.Component {
           </div>
         </div>
         <div className={`row mb-3 ${isIncorrectConfirmPassword && 'has-error'}`}>
-          <label htmlFor="newPasswordConfirm" className="col-xs-3 text-right">{t('personal_settings.new_password_confirm') }</label>
-          <div className="col-xs-6">
+          <label htmlFor="newPasswordConfirm" className="col-3 text-right">{t('personal_settings.new_password_confirm') }</label>
+          <div className="col-6">
             <input
-              className="form-control col-xs-4"
+              className="form-control"
               type="password"
               name="newPasswordConfirm"
               value={this.state.newPasswordConfirm}
@@ -115,17 +115,15 @@ class PasswordSettings extends React.Component {
           </div>
         </div>
 
-        <div className="row my-3">
-          <div className="col-xs-offset-4 col-xs-5">
-            <button
-              type="button"
-              className="btn btn-primary"
-              onClick={this.onClickSubmit}
-              disabled={this.state.retrieveError != null || isIncorrectConfirmPassword}
-            >
-              {t('Update')}
-            </button>
-          </div>
+        <div className="my-3 text-center">
+          <button
+            type="button"
+            className="btn btn-primary"
+            onClick={this.onClickSubmit}
+            disabled={this.state.retrieveError != null || isIncorrectConfirmPassword}
+          >
+            {t('Update')}
+          </button>
         </div>
       </React.Fragment>
     );

+ 8 - 8
src/client/js/components/Me/PersonalSettings.jsx

@@ -19,17 +19,17 @@ class PersonalSettings extends React.Component {
         <div className="m-t-10">
           <div className="personal-settings">
             <ul className="nav nav-tabs" role="tablist">
-              <li className="active">
-                <a href="#user-settings" data-toggle="tab" role="tab"><i className="icon-user"></i> { t('User Information') }</a>
+              <li className="nav-item">
+                <a className="nav-link active" href="#user-settings" data-toggle="tab" role="tab"><i className="icon-user"></i> { t('User Information') }</a>
               </li>
-              <li>
-                <a href="#external-accounts" data-toggle="tab" role="tab"><i className="icon-share-alt"></i> { t('External Accounts') }</a>
+              <li className="nav-item">
+                <a className="nav-link" href="#external-accounts" data-toggle="tab" role="tab"><i className="icon-share-alt"></i> { t('External Accounts') }</a>
               </li>
-              <li>
-                <a href="#password-settings" data-toggle="tab" role="tab"><i className="icon-lock"></i> { t('Password Settings') }</a>
+              <li className="nav-item">
+                <a className="nav-link" href="#password-settings" data-toggle="tab" role="tab"><i className="icon-lock"></i> { t('Password Settings') }</a>
               </li>
-              <li>
-                <a href="#apiToken" data-toggle="tab" role="tab"><i className="icon-paper-plane"></i> { t('API Settings') }</a>
+              <li className="nav-item">
+                <a className="nav-link" href="#apiToken" data-toggle="tab" role="tab"><i className="icon-paper-plane"></i> { t('API Settings') }</a>
               </li>
             </ul>
             <div className="tab-content p-t-10">

+ 9 - 7
src/client/js/components/Me/ProfileImageSettings.jsx

@@ -102,18 +102,19 @@ class ProfileImageSettings extends React.Component {
     return (
       <React.Fragment>
         <div className="row">
-          <div className="col-md-2 col-sm-offset-1 col-sm-4">
+          <div className="col-md-2 offset-1 col-sm-4">
             <h4>
-              <div className="radio radio-primary">
+              <div className="custom-control custom-radio radio-primary">
                 <input
                   type="radio"
                   id="radioGravatar"
+                  className="custom-control-input"
                   form="formImageType"
                   name="imagetypeForm[isGravatarEnabled]"
                   checked={isGravatarEnabled}
                   onChange={() => { personalContainer.changeIsGravatarEnabled(true) }}
                 />
-                <label htmlFor="radioGravatar">
+                <label className="custom-control-label" htmlFor="radioGravatar">
                   <img src="https://gravatar.com/avatar/00000000000000000000000000000000?s=24" /> Gravatar
                 </label>
                 <a href="https://gravatar.com/">
@@ -127,16 +128,17 @@ class ProfileImageSettings extends React.Component {
 
           <div className="col-md-4 col-sm-7">
             <h4>
-              <div className="radio radio-primary">
+              <div className="custom-control custom-radio radio-primary">
                 <input
                   type="radio"
                   id="radioUploadPicture"
+                  className="custom-control-input"
                   form="formImageType"
                   name="imagetypeForm[isGravatarEnabled]"
                   checked={!isGravatarEnabled}
                   onChange={() => { personalContainer.changeIsGravatarEnabled(false) }}
                 />
-                <label htmlFor="radioUploadPicture">
+                <label className="custom-control-label" htmlFor="radioUploadPicture">
                   { t('Upload Image') }
                 </label>
               </div>
@@ -146,7 +148,7 @@ class ProfileImageSettings extends React.Component {
                 { t('Current Image') }
               </label>
               <div className="col-sm-8">
-                {uploadedPictureSrc && (<p><img src={uploadedPictureSrc} className="picture picture-lg img-circle" id="settingUserPicture" /></p>)}
+                {uploadedPictureSrc && (<p><img src={uploadedPictureSrc} className="picture picture-lg rounded-circle" id="settingUserPicture" /></p>)}
                 {isUploadedPicture && <button type="button" className="btn btn-danger" onClick={this.onClickDeleteBtn}>{ t('Delete Image') }</button>}
               </div>
             </div>
@@ -169,7 +171,7 @@ class ProfileImageSettings extends React.Component {
         />
 
         <div className="row my-3">
-          <div className="col-xs-offset-4 col-xs-5">
+          <div className="offset-4 col-5">
             <button type="button" className="btn btn-primary" onClick={this.onClickSubmit} disabled={personalContainer.state.retrieveError != null}>
               {t('Update')}
             </button>

+ 17 - 10
src/client/js/components/Navbar/GrowiSubNavigation.jsx

@@ -18,8 +18,9 @@ const GrowiSubNavigation = (props) => {
   const isPageForbidden = document.querySelector('#grw-subnav').getAttribute('data-is-forbidden-page');
   const { appContainer, pageContainer } = props;
   const {
-    path, createdAt, creator, updatedAt, revisionAuthor,
+    pageId, path, createdAt, creator, updatedAt, revisionAuthor, isCompactMode,
   } = pageContainer.state;
+  const compactClassName = isCompactMode ? 'fixed-top grw-compact-subnavbar px-3' : null;
 
   // Display only the RevisionPath if the page is trash or forbidden
   if (isTrashPage(path) || isPageForbidden) {
@@ -27,7 +28,7 @@ const GrowiSubNavigation = (props) => {
       <div className="d-flex align-items-center">
         <div className="title-container mr-auto">
           <h1>
-            <RevisionPath behaviorType={appContainer.config.behaviorType} pageId={pageContainer.state.pageId} pagePath={pageContainer.state.path} />
+            <RevisionPath behaviorType={appContainer.config.behaviorType} pageId={pageId} pagePath={pageContainer.state.path} />
           </h1>
         </div>
       </div>
@@ -35,28 +36,34 @@ const GrowiSubNavigation = (props) => {
   }
 
   return (
-    <div className="d-flex align-items-center">
+    <div className={`d-flex align-items-center ${compactClassName}`}>
 
       {/* Page Path */}
       <div className="title-container mr-auto">
         <h1>
-          <RevisionPath behaviorType={appContainer.config.behaviorType} pageId={pageContainer.state.pageId} pagePath={pageContainer.state.path} />
+          <RevisionPath behaviorType={appContainer.config.behaviorType} pageId={pageId} pagePath={pageContainer.state.path} />
         </h1>
         <TagLabels />
       </div>
 
       {/* Header Button */}
-      <div className="ml-1">
-        <LikeButton pageId={pageContainer.state.pageId} isLiked={pageContainer.state.isLiked} />
+      <div className="mr-2">
+        <LikeButton pageId={pageId} isLiked={pageContainer.state.isLiked} />
       </div>
       <div>
-        <BookmarkButton pageId={pageContainer.state.pageId} crowi={appContainer} />
+        <BookmarkButton pageId={pageId} crowi={appContainer} />
       </div>
 
       {/* Page Authors */}
-      <ul className="authors hidden-sm hidden-xs text-nowrap">
-        {creator != null && <li><PageCreator creator={creator} createdAt={createdAt} /></li>}
-        {revisionAuthor != null && <li className="mt-1"><RevisionAuthor revisionAuthor={revisionAuthor} updatedAt={updatedAt} /></li>}
+      <ul className="authors text-nowrap d-none d-lg-block">
+        {creator != null && <li><PageCreator creator={creator} createdAt={createdAt} isCompactMode={isCompactMode} /></li>}
+        { revisionAuthor != null
+          && (
+            <li className="mt-1">
+              <RevisionAuthor revisionAuthor={revisionAuthor} updatedAt={updatedAt} isCompactMode={isCompactMode} />
+            </li>
+          )
+        }
       </ul>
 
     </div>

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

@@ -0,0 +1,83 @@
+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 = 175;
+  const layoutType = appContainer.getConfig().layoutType;
+
+  useEffect(() => {
+    window.addEventListener('scroll', throttle(300, () => {
+      setIsCompactMode(window.pageYOffset > scrollAmountForFixed);
+    }));
+  }, []);
+
+  return (
+    <div className={(isCompactMode && layoutType === 'growi') && 'fixed-top grw-compact-subnavbar px-3'}>
+
+      {/* 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} />
+      </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);

+ 12 - 6
src/client/js/components/Navbar/PageCreator.jsx

@@ -5,17 +5,18 @@ import UserPicture from '../User/UserPicture';
 import { userPageRoot } from '../../../../lib/util/path-utils';
 
 const PageCreator = (props) => {
-  const { creator, createdAt } = props;
+  const { creator, createdAt, isCompactMode } = props;
+  const creatInfo = isCompactMode
+    ? (<div>Created in <span className="text-muted">{createdAt}</span></div>)
+    : (<div><div>Created by <a href={userPageRoot(creator)}>{creator.name}</a></div><div className="text-muted">{createdAt}</div></div>);
+  const pictureSize = isCompactMode ? 'xs' : 'sm';
 
   return (
     <div className="d-flex align-items-center">
       <div className="mr-2" href={userPageRoot(creator)} data-toggle="tooltip" data-placement="bottom" title={creator.name}>
-        <UserPicture user={creator} size="sm" />
-      </div>
-      <div>
-        <div>Created by <a href={userPageRoot(creator)}>{creator.name}</a></div>
-        <div className="text-muted">{createdAt}</div>
+        <UserPicture user={creator} size={pictureSize} />
       </div>
+      {creatInfo}
     </div>
   );
 };
@@ -24,6 +25,11 @@ PageCreator.propTypes = {
 
   creator: PropTypes.object.isRequired,
   createdAt: PropTypes.string.isRequired,
+  isCompactMode: PropTypes.bool,
+};
+
+PageCreator.defaultProps = {
+  isCompactMode: false,
 };
 
 

+ 3 - 1
src/client/js/components/Navbar/PersonalDropdown.jsx

@@ -56,7 +56,9 @@ const PersonalDropdown = (props) => {
   return (
     <>
       {/* Button */}
-      <a className="nav-link dropdown-toggle waves-effect waves-light" data-toggle="dropdown">
+      {/* remove .dropdown-toggle for hide caret */}
+      {/* See https://stackoverflow.com/a/44577512/13183572 */}
+      <a className="nav-link waves-effect waves-light" data-toggle="dropdown">
         <UserPicture user={user} withoutLink />&nbsp;{user.name}
       </a>
 

+ 11 - 6
src/client/js/components/Navbar/RevisionAuthor.jsx

@@ -5,17 +5,18 @@ import UserPicture from '../User/UserPicture';
 import { userPageRoot } from '../../../../lib/util/path-utils';
 
 const RevisionAuthor = (props) => {
-  const { revisionAuthor, updatedAt } = props;
+  const { revisionAuthor, updatedAt, isCompactMode } = props;
+  const updateInfo = isCompactMode
+    ? (<div>Updated in <span className="text-muted">{updatedAt}</span></div>)
+    : (<div><div>Updated in  <a href={userPageRoot(revisionAuthor)}>{revisionAuthor.name}</a></div><div className="text-muted">{updatedAt}</div></div>);
+  const pictureSize = isCompactMode ? 'xs' : 'sm';
 
   return (
     <div className="d-flex align-items-center">
       <div className="mr-2" href={userPageRoot(revisionAuthor)} data-toggle="tooltip" data-placement="bottom" title={revisionAuthor.name}>
-        <UserPicture user={revisionAuthor} size="sm" />
-      </div>
-      <div>
-        <div>Updated by  <a href={userPageRoot(revisionAuthor)}>{revisionAuthor.name}</a></div>
-        <div className="text-muted">{updatedAt}</div>
+        <UserPicture user={revisionAuthor} size={pictureSize} />
       </div>
+      {updateInfo}
     </div>
   );
 };
@@ -24,7 +25,11 @@ RevisionAuthor.propTypes = {
 
   revisionAuthor: PropTypes.object.isRequired,
   updatedAt: PropTypes.string.isRequired,
+  isCompactMode: PropTypes.bool,
 };
 
+RevisionAuthor.defaultProps = {
+  isCompactMode: false,
+};
 
 export default RevisionAuthor;

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

@@ -152,7 +152,7 @@ class RevisionPath extends React.Component {
     });
 
     return (
-      <span className="d-flex align-items-center">
+      <span className="d-flex align-items-center flex-wrap">
 
         {rootElement}
         {afterElements}

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

@@ -54,7 +54,7 @@ export default class TagEditor extends React.Component {
           <TagsInput tags={this.state.tags} onTagsUpdated={this.onTagsUpdatedByTagsInput} />
         </ModalBody>
         <ModalFooter>
-          <Button variant="primary" onClick={this.handleSubmit}>
+          <Button color="primary" onClick={this.handleSubmit}>
             Done
           </Button>
         </ModalFooter>

+ 2 - 2
src/client/js/components/PageAttachment/Attachment.jsx

@@ -31,10 +31,10 @@ export default class Attachment extends React.Component {
 
     let fileInUse = '';
     if (this.props.inUse) {
-      fileInUse = <span className="attachment-in-use label label-info">In Use</span>;
+      fileInUse = <span className="attachment-in-use badge badge-pill badge-info">In Use</span>;
     }
 
-    const fileType = <span className="attachment-filetype label label-default">{attachment.fileFormat}</span>;
+    const fileType = <span className="attachment-filetype badge badge-pill badge-secondary">{attachment.fileFormat}</span>;
 
     const btnDownload = (this.props.isUserLoggedIn)
       ? (

+ 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/PageComment/CommentEditor.jsx

@@ -224,7 +224,7 @@ class CommentEditor extends React.Component {
 
     const errorMessage = <span className="text-danger text-right mr-2">{this.state.errorMessage}</span>;
     const cancelButton = (
-      <Button outline color="danger" size="xs" className="fcbtn rounded-pill" onClick={this.toggleEditor}>
+      <Button outline color="danger" size="xs" className="btn btn-outline-danger rounded-pill" onClick={this.toggleEditor}>
         Cancel
       </Button>
     );
@@ -232,7 +232,7 @@ class CommentEditor extends React.Component {
       <Button
         outline
         color="primary"
-        className="fcbtn rounded-pill btn-1b"
+        className="btn btn-outline-primary rounded-pill"
         onClick={this.postHandler}
       >
         Comment

+ 3 - 3
src/client/js/components/PageComment/DeleteCommentModal.jsx

@@ -45,12 +45,12 @@ export default class DeleteCommentModal extends React.Component {
         </ModalHeader>
         <ModalBody>
           <UserPicture user={comment.creator} size="xs" /> <strong><Username user={comment.creator}></Username></strong> wrote on {commentDate}:
-          <p className="well well-sm comment-body mt-2">{commentBody}</p>
+          <p className="card well comment-body mt-2 p-2">{commentBody}</p>
         </ModalBody>
         <ModalFooter>
           <span className="text-danger">{this.props.errorMessage}</span>&nbsp;
-          <Button onClick={this.props.cancel} bsClass="btn btn-sm">Cancel</Button>
-          <Button onClick={this.props.confirmedToDelete} bsClass="btn btn-sm btn-danger">
+          <Button onClick={this.props.cancel}>Cancel</Button>
+          <Button color="danger" onClick={this.props.confirmedToDelete}>
             <i className="icon icon-fire"></i>
             Delete
           </Button>

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

@@ -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-light btn-block btn-open-dropzone"
             onClick={() => { this.dropzone.open() }}
           >
             <i className="icon-paper-clip" aria-hidden="true"></i>&nbsp;

+ 3 - 3
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-white dropdown-toggle" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
+          <button className="btn btn-light 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-white dropdown-toggle" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
+          <button className="btn btn-light dropdown-toggle" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
             {selectedKeymapMode}
           </button>
           <div className="dropdown-menu" aria-labelledby="dropdownMenuLink">
@@ -162,7 +162,7 @@ class OptionsSelector extends React.Component {
           toggle={this.onToggleConfigurationDropdown}
         >
 
-          <DropdownToggle color="white" caret>
+          <DropdownToggle color="light" caret>
             <i className="icon-settings"></i>
           </DropdownToggle>
 

+ 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-light btn-lg waves-effect waves-light"
               type="button"
               onClick={() => { return this.discardChanges() }}
             >

+ 1 - 1
src/client/js/components/SavePageControls/GrantSelector.jsx

@@ -166,7 +166,7 @@ class GrantSelector extends React.Component {
     return (
       <div className="form-group grw-grant-selector mb-0">
         <UncontrolledDropdown direction="up" size="sm">
-          <DropdownToggle color="white" caret className="d-flex justify-content-between align-items-center" disabled={this.props.disabled}>
+          <DropdownToggle color="light" caret className="d-flex justify-content-between align-items-center" disabled={this.props.disabled}>
             {dropdownToggleLabelElm}
           </DropdownToggle>
           <DropdownMenu>

+ 16 - 6
src/client/js/components/SearchPage/DeletePageListModal.jsx

@@ -41,19 +41,24 @@ export default class DeletePageListModal extends React.Component {
           <div className="d-flex justify-content-between">
             <span className="text-danger">{this.props.errorMessage}</span>
             <span className="d-flex align-items-center">
-              <div className="custom-control custom-checkbox custom-checkbox-danger">
-                <input type="checkbox" className="custom-control-input" id="customCheck-delete-completely" />
+              <div className="custom-control custom-checkbox custom-checkbox-danger mr-2">
+                <input
+                  type="checkbox"
+                  className="custom-control-input"
+                  id="customCheck-delete-completely"
+                  checked={this.props.isDeleteCompletely}
+                  onChange={this.props.toggleDeleteCompletely}
+                />
                 <label
                   className="custom-control-label text-danger"
                   htmlFor="customCheck-delete-completely"
-                  onClick={this.props.toggleDeleteCompletely}
                 >
                   Delete completely
                 </label>
               </div>
-              <span className="ml-2">
-                <Button color="secondary" onClick={this.props.confirmedToDelete}><i className="icon-trash"></i>Delete</Button>
-              </span>
+              <Button color={this.props.isDeleteCompletely ? 'danger' : 'light'} onClick={this.props.confirmedToDelete}>
+                <i className="icon-trash"></i>Delete
+              </Button>
             </span>
           </div>
         </ModalFooter>
@@ -63,11 +68,16 @@ export default class DeletePageListModal extends React.Component {
 
 }
 
+DeletePageListModal.defaultProps = {
+  isDeleteCompletely: false, // for when undefined is passed
+};
+
 DeletePageListModal.propTypes = {
   isShown: PropTypes.bool.isRequired,
   pages: PropTypes.array,
   errorMessage: PropTypes.string,
   cancel: PropTypes.func.isRequired, //                 for cancel evnet handling
+  isDeleteCompletely: PropTypes.bool,
   confirmedToDelete: PropTypes.func.isRequired, //      for confirmed event handling
   toggleDeleteCompletely: PropTypes.func.isRequired, // for delete completely check event handling
 };

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

@@ -202,12 +202,12 @@ class SearchResult extends React.Component {
     if (this.state.deletionMode) {
       deletionModeButtons = (
         <div className="btn-group">
-          <button type="button" className="btn btn-rounded btn-light btn-sm" onClick={() => { return this.handleDeletionModeChange() }}>
+          <button type="button" className="btn btn-light btn-sm rounded-pill-weak" onClick={() => { return this.handleDeletionModeChange() }}>
             <i className="icon-ban" /> Cancel
           </button>
           <button
             type="button"
-            className="btn btn-rounded btn-danger btn-sm"
+            className="btn btn-danger btn-sm rounded-pill-weak"
             onClick={() => { return this.showDeleteConfirmModal() }}
             disabled={this.state.selectedPages.size === 0}
           >
@@ -305,6 +305,7 @@ class SearchResult extends React.Component {
           errorMessage={this.state.errorMessageForDeleting}
           cancel={this.closeDeleteConfirmModal}
           confirmedToDelete={this.deleteSelectedPages}
+          isDeleteCompletely={this.state.isDeleteCompletely}
           toggleDeleteCompletely={this.toggleDeleteCompletely}
         />
       </div> // content-main

+ 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>
             )}

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

@@ -36,7 +36,7 @@ export default class SlackNotification extends React.Component {
   render() {
     return (
       <div className="input-group input-group-sm input-group-slack extended-setting">
-        <label className="input-group-addon">
+        <label className="input-group-addon bg-light">
           <img id="slack-mark-white" alt="slack-mark" src="/images/icons/slack/mark-monochrome_white.svg" width="18" height="18" />
           <img id="slack-mark-black" alt="slack-mark" src="/images/icons/slack/mark-monochrome_black.svg" width="18" height="18" />
 

+ 1 - 0
src/client/js/services/AdminGeneralSecurityContainer.js

@@ -39,6 +39,7 @@ export default class AdminGeneralSecurityContainer extends Container {
     const response = await this.appContainer.apiv3.get('/security-setting/');
     const { generalSetting, generalAuth } = response.data.securityParams;
     this.setState({
+      currentRestrictGuestMode: generalSetting.restrictGuestMode,
       currentPageCompleteDeletionAuthority: generalSetting.pageCompleteDeletionAuthority,
       isShowRestrictedByOwner: !generalSetting.hideRestrictedByOwner,
       isShowRestrictedByGroup: !generalSetting.hideRestrictedByGroup,

+ 9 - 5
src/client/js/services/PageContainer.js

@@ -5,7 +5,10 @@ import loggerFactory from '@alias/logger';
 import * as entities from 'entities';
 import * as toastr from 'toastr';
 
+import { throttle } from 'throttle-debounce';
+
 const logger = loggerFactory('growi:services:PageContainer');
+const scrollAmountForFixed = 122;
 
 /**
  * Service container related to Page
@@ -38,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: false,
+      isLiked: JSON.parse(mainContent.getAttribute('data-page-is-liked')),
       seenUserIds: [],
       likerUserIds: [],
       createdAt: mainContent.getAttribute('data-page-created-at'),
@@ -55,6 +58,7 @@ export default class PageContainer extends Container {
       pageIdOnHackmd: mainContent.getAttribute('data-page-id-on-hackmd') || null,
       hasDraftOnHackmd: !!mainContent.getAttribute('data-page-has-draft-on-hackmd'),
       isHackmdDraftUpdatingInRealtime: false,
+      isCompactMode: false,
     };
 
     this.initStateMarkdown();
@@ -64,6 +68,10 @@ export default class PageContainer extends Container {
     this.save = this.save.bind(this);
     this.addWebSocketEventHandlers = this.addWebSocketEventHandlers.bind(this);
     this.addWebSocketEventHandlers();
+
+    window.addEventListener('scroll', throttle(300, () => {
+      this.setState({ isCompactMode: window.pageYOffset > scrollAmountForFixed });
+    }));
   }
 
   /**
@@ -89,10 +97,6 @@ export default class PageContainer extends Container {
   }
 
   initStateOthers() {
-    const likeButtonElem = document.getElementById('like-button');
-    if (likeButtonElem != null) {
-      this.state.isLiked = likeButtonElem.dataset.liked === 'true';
-    }
 
     const seenUserListElem = document.getElementById('seen-user-list');
     if (seenUserListElem != null) {

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

@@ -1,3 +1,5 @@
+@import '../scss/theme/layout_kibela_variable';
+
 body.kibela {
   .icon-link,
   .CodeMirror-hint-active,
@@ -11,6 +13,10 @@ body.kibela {
     background: #fefffe !important;
   }
 
+  .bg-primary {
+    background-color: $primary !important;
+  }
+
   .logo {
     background: transparent;
 
@@ -180,6 +186,54 @@ body.kibela {
     }
   }
 
+  /* Modal */
+  .modal-content {
+    background-color: $themelight;
+
+    .modal-header.bg-primary {
+      color: white;
+
+      .close {
+        color: white;
+      }
+    }
+  }
+
+  /* Inline Code */
+  :not(.hljs) > code:not(.hljs) {
+    background-color: $bgcolor-inline-code;
+    color: $color-inline-code;
+  }
+
+  /* Card */
+  .card {
+    border: 1px solid $border;
+
+    .card-header {
+      background-color: $lightthemecolor;
+      border-bottom: 1px solid $border;
+    }
+
+    .card-body {
+      background-color: $themelight;
+    }
+
+    .card-footer {
+      background: white;
+      border-top: 1px solid $border
+    }
+  }
+
+  /* button */
+  .btn {
+    border-radius: $radius;
+  }
+
+  .btn-primary {
+    background: $primary;
+    border: 1px solid $primary;
+  }
+
   /* edit */
   .CodeMirror {
     border: solid 1.2px #d8d8d8;

+ 19 - 63
src/client/styles/scss/_login.scss

@@ -122,57 +122,14 @@
   .collapse-external-auth {
     overflow: hidden;
 
-    &:not(.in) {
+    &:not(.show) {
       height: 0;
       padding: 0 !important;
     }
   }
 
   // button style
-
-  .fcbtn {
-    position: relative;
-    overflow: hidden;
-    color: white;
-    text-align: center;
-    cursor: pointer;
-    background-color: rgba(lighten(black, 20%), 0.4);
-    border: none;
-
-    .btn-label {
-      position: relative;
-      z-index: 1;
-      color: white;
-      text-decoration: none;
-    }
-
-    .btn-label-text {
-      position: relative;
-      z-index: 1;
-      color: white;
-      text-decoration: none;
-    }
-
-    // effect
-    .eff {
-      position: absolute;
-      top: -50px;
-      left: 0px;
-      z-index: 0;
-      width: 100%;
-      height: 100%;
-      transition: all 0.5s ease;
-    }
-
-    &:hover {
-      .eff {
-        top: 0;
-      }
-    }
-  }
-
-  // login
-  .fcbtn.login {
+  .btn-fill.login {
     .btn-label {
       background-color: rgba($danger, 0.4);
     }
@@ -182,7 +139,7 @@
   }
 
   // google
-  .fcbtn#google {
+  .btn-fill#google {
     .btn-label {
       background-color: rgba(#24292e, 0.4);
     }
@@ -193,7 +150,7 @@
   }
 
   // github
-  .fcbtn#github {
+  .btn-fill#github {
     .btn-label {
       background-color: rgba(lighten(black, 20%), 0.4);
     }
@@ -204,7 +161,7 @@
   }
 
   // facebook
-  .fcbtn#facebook {
+  .btn-fill#facebook {
     .btn-label {
       background-color: rgba(#29487d, 0.4);
     }
@@ -215,7 +172,7 @@
   }
 
   // twitter
-  .fcbtn#twitter {
+  .btn-fill#twitter {
     .btn-label {
       background-color: rgba(#1da1f2, 0.4);
     }
@@ -226,7 +183,7 @@
   }
 
   // oidc
-  .fcbtn#oidc {
+  .btn-fill#oidc {
     .btn-label {
       background-color: rgba(#24292e, 0.4);
     }
@@ -237,7 +194,7 @@
   }
 
   // saml
-  .fcbtn#saml {
+  .btn-fill#saml {
     .btn-label {
       background-color: rgba(#55a79a, 0.4);
     }
@@ -248,7 +205,7 @@
   }
 
   // basic
-  .fcbtn#basic {
+  .btn-fill#basic {
     .btn-label {
       background-color: rgba(#24292e, 0.4);
     }
@@ -257,6 +214,16 @@
       background-color: #555;
     }
   }
+  // register
+  .btn-fill#register {
+    .btn-label {
+      background-color: rgba($success, 0.4);
+    }
+
+    .eff {
+      background-color: rgba(#3f7263, 0.5);
+    }
+  }
 
   // external-auth
   .btn-collapse-external-auth {
@@ -274,17 +241,6 @@
     }
   }
 
-  // register
-  .fcbtn#register {
-    .btn-label {
-      background-color: rgba($success, 0.4);
-    }
-
-    .eff {
-      background-color: rgba(#3f7263, 0.5);
-    }
-  }
-
   // footer link text
   .link-growi-org {
     font-size: smaller;

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

@@ -6,9 +6,9 @@
 //
 $primary: #112744;
 $secondary: #6c757d;
-$info: #0d8ea5;
+$info: #009fbb;
 $success: #00bb83;
-$warning: #ee773b;
+$warning: #ffa32b;
 $danger: #ff0a54;
 $light: #dee2e6;
 $dark: #343a40;

+ 10 - 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
@@ -171,3 +176,8 @@ fieldset[disabled] .btn {
   margin-bottom: 18px;
   overflow: hidden;
 }
+
+// badge
+.badge {
+  letter-spacing: 0.05em;
+}

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

@@ -20,5 +20,20 @@
         }
       }
     }
+    .grw-compact-subnavbar {
+      h2 {
+        font-size: 20px;
+        line-height: 1.1em;
+        @include media-breakpoint-down(md) {
+          font-size: 18px;
+        }
+        @include media-breakpoint-down(sm) {
+          font-size: 14px;
+        }
+        @include media-breakpoint-down(xs) {
+          font-size: 12px;
+        }
+      }
+    }
   }
 }

+ 58 - 13
src/client/styles/scss/atoms/_buttons.scss

@@ -26,25 +26,24 @@
   border-radius: 35px;
 }
 
-#like-button,
-#bookmark-button {
-  & button {
-    font-size: 1.2em;
-    line-height: 0.8em;
-
-    &:not(:hover):not(.active) {
-      background-color: transparent;
-    }
+.btn-like,
+.btn-bookmark {
+  font-size: 1.2em;
+  line-height: 0.8em;
+
+  &.active {
+    // header buttons are always white for active
+    color: white !important;
+  }
+
+  &:not(:hover):not(.active) {
+    background-color: transparent;
   }
 }
 
 .btn-copy,
 .btn-edit {
   opacity: 0.3;
-
-  &:hover {
-    background-color: $light;
-  }
 }
 
 .btn-edit-tags {
@@ -54,3 +53,49 @@
     opacity: 0.7;
   }
 }
+
+.rounded-pill-weak {
+  border-radius: 60px;
+}
+
+// fill button style
+.btn-fill {
+  position: relative;
+  overflow: hidden;
+  color: white;
+  text-align: center;
+  cursor: pointer;
+  background-color: rgba(lighten(black, 20%), 0.4);
+  border: none;
+
+  .btn-label {
+    position: relative;
+    z-index: 1;
+    color: white;
+    text-decoration: none;
+  }
+
+  .btn-label-text {
+    position: relative;
+    z-index: 1;
+    color: white;
+    text-decoration: none;
+  }
+
+  // effect
+  .eff {
+    position: absolute;
+    top: -50px;
+    left: 0px;
+    z-index: 0;
+    width: 100%;
+    height: 100%;
+    transition: all 0.5s ease;
+  }
+
+  &:hover {
+    .eff {
+      top: 0;
+    }
+  }
+}

+ 8 - 0
src/client/styles/scss/theme/_apply-colors-dark.scss

@@ -136,6 +136,14 @@ header.affix {
   }
 }
 
+/*
+ * GROWI subnavigation
+ */
+.grw-compact-subnavbar {
+  background-color: rgba(darken($bgcolor-global, 90%), 0.9);
+  box-shadow: 0 0 2px darken($bgcolor-global, 5%);
+}
+
 /*
  * GROWI search page
  */

+ 8 - 0
src/client/styles/scss/theme/_apply-colors-light.scss

@@ -32,6 +32,14 @@ header.affix {
   }
 }
 
+/*
+ * GROWI subnavigation
+ */
+.grw-compact-subnavbar {
+  background-color: rgba(darken($bgcolor-global, 6%), 0.9);
+  box-shadow: 0 0 2px darken($bgcolor-global, 40%);
+}
+
 /*
  * GROWI page list
  */

+ 6 - 2
src/client/styles/scss/theme/_apply-colors.scss

@@ -25,7 +25,11 @@ $link-hover-color: $color-link-hover;
   color: $color-global;
   &.active,
   &:active {
-    @include gradient-bg($dropdown-link-active-bg);
+    color: $color-dropdown-link-active;
+    background-color: $bgcolor-dropdown-link-active;
+  }
+  &:hover:not(.active) {
+    color: $color-dropdown-link-hover;
   }
 }
 
@@ -313,4 +317,4 @@ body.on-edit {
       }
     }
   }
-}
+}

+ 39 - 0
src/client/styles/scss/theme/_layout_kibela_variable.scss

@@ -0,0 +1,39 @@
+$radius: .25em;
+
+$bgcolor-theme: rgb(18, 86, 163);
+$themelight: #f4f5f6;
+$subthemecolor: rgb(90, 149, 216);
+$lightthemecolor: rgba(181, 203, 247, 0.61);
+
+$bgcolor-navbar: $bgcolor-theme;
+$bgcolor-global: $themelight;
+$bgcolor-global: $themelight;
+
+$color-header: $bgcolor-theme;
+$color-global: #3c4a60;
+$linktext: rgb(74, 109, 204);
+$linktext-hover: lighten($linktext, 12%);
+$sidebar-text: $bgcolor-theme;
+
+$primary: $bgcolor-theme;
+$info: lighten($bgcolor-theme, 20%);
+
+$fillcolor-logo-mark: lighten($bgcolor-theme, 20%);
+$color-link-wiki: lighten($bgcolor-theme, 20%);
+$color-link-wiki-hover: lighten($color-link-wiki, 20%);
+$color-inline-code: $subthemecolor;
+$bgcolor-inline-code: lighten($subthemecolor, 70%);
+$border: $lightthemecolor;
+
+// change color of highlighted header in wiki (default: orange)
+.wiki {
+  .code-line.revision-head.highlighted {
+    color: $themelight;
+    background-color: lighten($bgcolor-theme, 20%);
+
+    .icon-note,
+    .icon-link {
+      color: $themelight;
+    }
+  }
+}

+ 9 - 5
src/client/styles/scss/theme/_reboot-bootstrap-colors.scss

@@ -190,13 +190,17 @@ body {
 //
 
 a {
-  color: $link-color;
-  // text-decoration: $link-decoration;
-  background-color: transparent; // Remove the gray background on active links in IE 10.
+  :not(.badge) {
+    color: $link-color;
+    // text-decoration: $link-decoration;
+    background-color: transparent; // Remove the gray background on active links in IE 10.
+  }
 
   @include hover() {
-    color: $link-hover-color;
-    // text-decoration: $link-hover-decoration;
+    &:not(.list-group-item) {
+      color: $link-hover-color;
+      // text-decoration: $link-hover-decoration;
+    }
   }
 }
 

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

@@ -26,6 +26,7 @@ html:not([dark]) {
 
   // Font colors
   $color-global: #333333;
+  $color-reversal: #eeeeee;
   // $color-header: #2b2b2b;
   $color-link: lighten($primary, 20%);
   $color-link-hover: lighten($color-link, 20%);
@@ -39,6 +40,11 @@ html:not([dark]) {
   // Border colors
   $border-color-theme: #ccc; // former: `$navbar-border: #ccc;`
 
+  // Dropdown colors
+  $bgcolor-dropdown-link-active: $growi-blue;
+  $color-dropdown-link-active: $color-reversal;
+  $color-dropdown-link-hover: $color-global;
+
   @import 'apply-colors';
   @import 'apply-colors-light';
 }
@@ -58,6 +64,7 @@ html[dark] {
 
   // Font colors
   $color-global: #eeeeee;
+  $color-reversal: #333333;
   // $color-header: desaturate($primary, 20%);
   $color-link: $primary;
   $color-link-hover: lighten($color-link, 10%);
@@ -71,6 +78,11 @@ html[dark] {
   // Border colors
   $border-color-theme: black; // former: `$navbar-border: #ccc;`
 
+  // Dropdown colors
+  $bgcolor-dropdown-link-active: $primary;
+  $color-dropdown-link-active: $color-global;
+  $color-dropdown-link-hover: $color-reversal;
+
   @import 'apply-colors';
   @import 'apply-colors-dark';
 }

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

@@ -1,5 +1,6 @@
 // import colors
 @import '../../agile-admin/inverse/colors/kibela';
+@import 'layout_kibela_valiable';
 
 // apply agile-admin theme
 @import '../../agile-admin/inverse/style';

+ 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);
       }
     }));

+ 2 - 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,7 @@ module.exports = (crowi) => {
 
   router.use('/search', require('./search')(crowi));
 
+  router.use('/page', require('./page')(crowi));
+
   return router;
 };

+ 2 - 2
src/server/routes/apiv3/notification-setting.js

@@ -14,9 +14,9 @@ const removeNullPropertyFromObject = require('../../../lib/util/removeNullProper
 
 const validator = {
   slackConfiguration: [
-    body('webhookUrl').isString().trim(),
+    body('webhookUrl').if(value => value != null).isString().trim(),
     body('isIncomingWebhookPrioritized').isBoolean(),
-    body('slackToken').isString().trim(),
+    body('slackToken').if(value => value != null).isString().trim(),
   ],
   userNotification: [
     body('pathPattern').isString().trim(),

+ 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);

+ 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
    *

+ 20 - 12
src/server/views/invited.html

@@ -26,7 +26,7 @@
 
   <div class="row">
 
-    <div class="login-header col-sm-offset-4 col-sm-4">
+    <div class="login-header offset-4 col-sm-4">
       <div class="logo">{% include 'widget/logo.html' %}</div>
       <h1>GROWI</h1>
 
@@ -50,7 +50,7 @@
       </div>
     </div>
 
-    <div class="login-dialog grw-pt-10px p-b-10 col-sm-offset-4 col-sm-4" id="login-dialog">
+    <div class="login-dialog grw-pt-10px p-b-10 offset-4 col-sm-4" id="login-dialog">
       <p class="alert alert-success">
         <strong>アカウントの作成</strong><br>
         <small>招待を受け取ったメールアドレスでアカウントを作成します</small>
@@ -59,12 +59,15 @@
       <form role="form" action="/login/activateInvited" method="post" id="invited-form">
 
         <div class="input-group">
-          <span class="input-group-addon"><i class="icon-envelope"></i></span>
+          <div class="input-group-prepend">
+            <span class="input-group-text"><i class="icon-envelope"></i></span>
+          </div>
           <input type="text" class="form-control" disabled value="{{ user.email }}">
         </div>
-
         <div class="input-group" id="input-group-username">
-          <span class="input-group-addon"><i class="icon-user"></i></span>
+          <div class="input-group-prepend">
+            <span class="input-group-text"><i class="icon-user"></i></span>
+          </div>
           <input type="text" class="form-control" placeholder="{{ t('User ID') }}" name="invitedForm[username]" value="{{ req.body.invitedForm.username }}" required>
         </div>
         <p class="help-block">
@@ -72,21 +75,26 @@
         </p>
 
         <div class="input-group">
-          <span class="input-group-addon"><i class="icon-tag"></i></span>
+          <div class="input-group-prepend">
+            <span class="input-group-text"><i class="icon-tag"></i></span>
+          </div>
           <input type="text" class="form-control" placeholder="{{ t('Name') }}" name="invitedForm[name]" value="{{ req.body.invitedForm.name }}" required>
         </div>
 
 
         <div class="input-group">
-          <span class="input-group-addon"><i class="icon-lock"></i></span>
+          <div class="input-group-prepend">
+            <span class="input-group-text"><i class="icon-lock"></i></span>
+          </div>
           <input type="password" class="form-control" placeholder="{{ t('Password') }}" name="invitedForm[password]" required>
         </div>
 
-        <input type="hidden" name="_csrf" value="{{ csrf() }}">
-        <div class="input-group mt-5 m-b-20 d-flex justify-content-center">
-          <button type="submit" class="fcbtn btn btn-success btn-1b btn-register">
-            <span class="btn-label"><i class="icon-user-follow"></i></span>
-            {{ t('Create') }}
+        <div class="input-group justify-content-center d-flex mt-5">
+          <input type="hidden" name="_csrf" value="{{ csrf() }}">
+          <button type="submit" class="btn btn-fill login px-0 py-2" id="register">
+            <div class="eff"></div>
+            <span class="btn-label p-3"><i class="icon-user-follow"></i></span>
+            <span class="btn-label-text p-3">{{ t('Create') }}</span>
           </button>
         </div>
 

+ 0 - 1
src/server/views/layout-crowi/page.html

@@ -14,7 +14,6 @@
           <div id="tag-label"></div>
         {% endif %}
       </div>
-      {% include '../widget/header-buttons.html' %}
     </div>
   </header>
 

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

@@ -20,7 +20,6 @@
         <div id="tag-label"></div>
       {% endif %}
     </div>
-    {% include '../widget/header-buttons.html' %}
   </div>
 
 </header>
@@ -47,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>

+ 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>

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

@@ -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/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-kibela/widget/header.html

@@ -11,7 +11,7 @@
         <div id="tag-label"></div>
       {% endif %}
     </div>
-    {% if page %} {% include '../../widget/header-buttons.html' %}
+    {% if page %}
 
     <ul class="authors hidden-sm hidden-xs text-nowrap grw-pt-10px">
       <li>

+ 78 - 76
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>
 
@@ -141,7 +143,7 @@
 
               <div class="input-group justify-content-center d-flex mt-5">
                 <input type="hidden" name="_csrf" value="{{ csrf() }}">
-                <button type="submit" class="btn fcbtn login px-0 py-2">
+                <button type="submit" class="btn btn-fill login px-0 py-2">
                   <div class="eff"></div>
                   <span class="btn-label p-3"><i class="icon-login"></i></span>
                   <span class="btn-label-text p-3">{{ t('Sign in') }}</span>
@@ -167,7 +169,7 @@
                 {% if getConfig('crowi', 'security:passport-google:isEnabled') %}
                 <div class="input-group justify-content-center d-flex mt-5">
                   <form role="form" action="/passport/google" class="d-inline-flex flex-column">
-                    <button type="submit" class="btn fcbtn px-0 py-2" id="google">
+                    <button type="submit" class="btn btn-fill px-0 py-2" id="google">
                       <div class="eff"></div>
                       <span class="btn-label p-3"><i class="fa fa-google"></i></span>
                       <span class="btn-label-text p-3">{{ t('Sign in') }}</span>
@@ -180,7 +182,7 @@
                 <div class="input-group justify-content-center d-flex mt-5">
                   <form role="form" action="/passport/github" class="d-inline-flex flex-column">
                     <input type="hidden" name="_csrf" value="{{ csrf() }}">
-                    <button type="submit" class="btn fcbtn px-0 py-2" id="github">
+                    <button type="submit" class="btn btn-fill px-0 py-2" id="github">
                       <div class="eff"></div>
                       <span class="btn-label p-3"><i class="fa fa-github"></i></span>
                       <span class="btn-label-text p-3">{{ t('Sign in') }}</span>
@@ -193,7 +195,7 @@
                 <div class="input-group justify-content-center d-flex mt-5">
                   <form role="form" action="/passport/facebook" class="d-inline-flex flex-column">
                     <input type="hidden" name="_csrf" value="{{ csrf() }}">
-                    <button type="submit" class="btn fcbtn px-0 py-2" id="facebook">
+                    <button type="submit" class="btn btn-fill px-0 py-2" id="facebook">
                       <div class="eff"></div>
                       <span class="btn-label p-3"><i class="fa fa-facebook"></i></span>
                       <span class="btn-label-text p-3">{{ t('Sign in') }}</span>
@@ -206,7 +208,7 @@
                 <div class="input-group justify-content-center d-flex mt-5">
                   <form role="form" action="/passport/twitter" class="d-inline-flex flex-column">
                     <input type="hidden" name="_csrf" value="{{ csrf() }}">
-                    <button type="submit" class="btn fcbtn px-0 py-2" id="twitter">
+                    <button type="submit" class="btn btn-fill px-0 py-2" id="twitter">
                       <div class="eff"></div>
                       <span class="btn-label p-3"><i class="fa fa-twitter"></i></span>
                       <span class="btn-label-text p-3">{{ t('Sign in') }}</span>
@@ -219,7 +221,7 @@
                 <div class="input-group justify-content-center d-flex mt-5">
                   <form role="form" action="/passport/oidc" class="d-inline-flex flex-column">
                     <input type="hidden" name="_csrf" value="{{ csrf() }}">
-                    <button type="submit" class="btn fcbtn px-0 py-2" id="oidc">
+                    <button type="submit" class="btn btn-fill px-0 py-2" id="oidc">
                       <div class="eff"></div>
                       <span class="btn-label p-3"><i class="fa fa-openid"></i></span>
                       <span class="btn-label-text p-3">{{ t('Sign in') }}</span>
@@ -232,7 +234,7 @@
                 <div class="input-group justify-content-center d-flex mt-5">
                   <form role="form" action="/passport/saml" class="d-inline-flex flex-column">
                     <input type="hidden" name="_csrf" value="{{ csrf() }}">
-                    <button type="submit" class="btn fcbtn px-0 py-2" id="saml">
+                    <button type="submit" class="btn btn-fill px-0 py-2" id="saml">
                       <div class="eff"></div>
                       <span class="btn-label p-3"><i class="fa fa-key"></i></span>
                       <span class="btn-label-text p-3">{{ t('Sign in') }}</span>
@@ -245,7 +247,7 @@
                 <div class="input-group justify-content-center d-flex mt-5">
                   <form role="form" action="/passport/basic" class="d-inline-flex flex-column">
                     <input type="hidden" name="_csrf" value="{{ csrf() }}">
-                    <button type="submit" class="btn fcbtn px-0 py-2" id="basic">
+                    <button type="submit" class="btn btn-fill px-0 py-2" id="basic">
                       <div class="eff"></div>
                       <span class="btn-label p-3"><i class="fa fa-lock"></i></span>
                       <span class="btn-label-text p-3">{{ t('Sign in') }}</span>
@@ -352,7 +354,7 @@
 
               <div class="input-group justify-content-center mt-5">
                 <input type="hidden" name="_csrf" value="{{ csrf() }}">
-                <button type="submit" class="btn fcbtn px-0 py-2" id="register">
+                <button type="submit" class="btn btn-fill px-0 py-2" id="register">
                   <div class="eff"></div>
                   <span class="btn-label p-3"><i class="icon-user-follow"></i></span>
                   <span class="btn-label-text p-3">{{ t('Sign up') }}</span>

+ 3 - 3
src/server/views/modal/create_page.html

@@ -21,7 +21,7 @@
                   <input type="text" data-prefix="/{{ now|datetz('Y/m/d') }}/" class="page-today-input2 form-control" id="page-today-input2" name="" placeholder="{{ t('Input page name (optional)') }}">
                 </div>
                 <div class="create-page-button-container">
-                  <button type="submit" class="fcbtn btn btn-outline-primary rounded-pill btn-1b"><i class="icon-fw icon-doc"></i>{{ t('Create') }}</button>
+                  <button type="submit" class="btn btn-outline-primary rounded-pill"><i class="icon-fw icon-doc"></i>{{ t('Create') }}</button>
                 </div>
               </div>
             </fieldset>
@@ -41,7 +41,7 @@
                   {% endif %}
                 </div>
                 <div class="create-page-button-container">
-                  <button type="submit" class="fcbtn btn btn-outline-primary rounded-pill btn-1b"><i class="icon-fw icon-doc"></i>{{ t('Create') }}</button>
+                  <button type="submit" class="btn btn-outline-primary rounded-pill"><i class="icon-fw icon-doc"></i>{{ t('Create') }}</button>
                 </div>
               </div>
             </fieldset>
@@ -72,7 +72,7 @@
 
               </div>
               <div class="create-page-button-container my-auto">
-                <a id="link-to-template" href="{{ page.path || path }}" class="fcbtn btn btn-outline-primary rounded-pill btn-1b disabled">
+                <a id="link-to-template" href="{{ page.path || path }}" class="btn btn-outline-primary rounded-pill disabled">
                   <i class="icon-fw icon-doc"></i>
                   <span id="create-template-button-link">{{ t('Edit') }}</span>
                 </a>

+ 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 %}

+ 7 - 6
src/server/views/modal/put_back.html

@@ -5,19 +5,20 @@
       <form role="form" id="revert-delete-page-form" onsubmit="return false;">
 
         <div class="modal-header bg-info">
-          <button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
           <div class="modal-title"><i class="icon-action-undo"></i> {{ t('modal_putback.label.Put Back Page') }}</div>
+          <button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
         </div>
         <div class="modal-body">
           <div class="form-group">
             <label for="">Put back page:</label><br>
             <code>{{ page.path }}</code>
           </div>
-          <div class="checkbox checkbox-warning">
-            <input name="recursively" id="cbPutbackRecursively" value="1" type="checkbox" checked>
-            <label for="cbPutbackRecursively">{{ t('modal_putback.label.recursively') }}</label>
-            <p class="help-block"> {{ t('modal_putback.help.recursively', page.path) }}
-            </p>
+          <div class="custom-control custom-checkbox custom-checkbox-warning">
+            <input class="custom-control-input" name="recursively" id="cbPutbackRecursively" value="1" type="checkbox" checked>
+            <label class="custom-control-label" for="cbPutbackRecursively">
+              {{ t('modal_putback.label.recursively') }}
+              <p class="help-block mt-0">{{ t('modal_putback.help.recursively', page.path) }}</p>
+            </label>
           </div>
         </div>
         <div class="modal-footer">

+ 3 - 1
src/server/views/modal/rename.html

@@ -62,9 +62,11 @@
               <input type="hidden" name="path" value="{{ page.path }}">
               <input type="hidden" name="page_id" value="{{ page._id.toString() }}">
               <input type="hidden" name="revision_id" value="{{ page.revision._id.toString() }}">
-              <button type="submit" class="btn btn-primary">Rename</button>
             </div>
           </div>
+          <div class="d-flex justify-content-end">
+            <button type="submit" class="btn btn-primary">Rename</button>
+          </div>
         </div>
 
       </form>

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

@@ -1,5 +1,5 @@
 <div class="row not-found-message-row mb-4">
-  <div class="col-md-12">
+  <div class="col-lg-12">
     <h2 class="text-muted">
       <i class="icon-ban" aria-hidden="true"></i>
       Forbidden
@@ -7,22 +7,22 @@
   </div>
 </div>
 
-<div id="content-main" class="content-main content-main-not-found page-list"
+<div id="content-main" class="content-main page-list"
   data-path="{{ path | preventXss }}"
   data-current-user="{% if user %}{{ user._id.toString() }}{% endif %}"
   >
 
   <div class="row row-alerts">
-    <div class="col-xs-12">
-        <p class="alert alert-inverse alert-grant"> <!-- TODO remove inverse and grant -->
+    <div class="col-sm-12">
+        <p class="alert alert-primary py-3 px-4">
           <i class="icon-fw icon-lock" aria-hidden="true"></i> Browsing of this page is restricted
         </p>
     </div>
   </div>
 
-  <ul class="nav nav-tabs hidden-print">
+  <ul class="nav nav-tabs hidden-print" role="tablist">
     <li class="nav-item grw-nav-main-left-tab">
-      <a class="nav-link active" href="#revision-body" data-toggle="tab">
+      <a class="nav-link active" role="tab" href="#revision-body" data-toggle="tab">
         <i class="icon-notebook"></i> List
       </a>
     </li>

+ 0 - 6
src/server/views/widget/header-button-bookmark.html

@@ -1,6 +0,0 @@
-{# This widget will be rendered by React #}
-{% if not size == null %}
-  <span id="bookmark-button-{{size}}"></span>
-{% else %}
-  <span id="bookmark-button"></span>
-{% endif %}

+ 0 - 6
src/server/views/widget/header-button-like.html

@@ -1,6 +0,0 @@
-{# This widget will be rendered by React #}
-{% if not size == null %}
-  <span id="like-button-{{size}}" data-liked="{% if page.isLiked(user) %}true{% else %}false{% endif %}"></span>
-{% else %}
-  <span id="like-button" data-liked="{% if page.isLiked(user) %}true{% else %}false{% endif %}"></span>
-{% endif %}

+ 0 - 13
src/server/views/widget/header-buttons-lg.html

@@ -1,13 +0,0 @@
-{% if page %}
-  {% set opts = { size: 'lg' }  %}
-  <div>
-    {% if user %}
-      {% include 'header-button-like.html' with opts %}
-    {% endif %}
-  </div>
-  <div class="ml-1">
-    {% if user %}
-      {% include 'header-button-bookmark.html' with opts %}
-    {% endif %}
-  </div>
-{% endif %}

+ 0 - 12
src/server/views/widget/header-buttons.html

@@ -1,12 +0,0 @@
-{% if page %}
-  <div>
-    {% if user %}
-      {% include 'header-button-like.html' %}
-    {% endif %}
-  </div>
-  <div class="ml-1">
-    {% if user %}
-      {% include 'header-button-bookmark.html' %}
-    {% endif %}
-  </div>
-{% endif %}

+ 1 - 1
src/server/views/widget/modal/page-api-error-messages.html

@@ -6,7 +6,7 @@
     <strong><i class="icon-fw icon-ban"></i>{{ t('page_api_error.user_not_admin') }}</strong>
   </span>
   <span class="text-danger msg msg-already_exists">
-    <strong><i class="icon-fw icon-ban"></i>{{ t('page_api_error.already_exists') }}</strong>
+    <strong><i class="icon-fw icon-ban"></i>{{ t('page_api_error.already_exists') }}</strong><br>
     <small id="linkToNewPage"></small>
   </span>
   <span class="text-warning msg msg-outdated">

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

@@ -7,7 +7,7 @@
   </div>
 </div>
 
-<div id="content-main" class="content-main content-main-not-found page-list"
+<div id="content-main" class="content-main page-list"
   data-path="{{ path | preventXss }}"
   data-current-user="{% if user %}{{ user._id.toString() }}{% endif %}"
   >

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

@@ -7,7 +7,7 @@
   </div>
 </div>
 
-<div id="content-main" class="content-main content-main-not-found page-list"
+<div id="content-main" class="content-main page-list"
   data-path="{{ path | preventXss }}"
   data-current-user="{% if user %}{{ user._id.toString() }}{% endif %}"
   {% if templateTags %}

+ 3 - 3
src/server/views/widget/not_found_tabs.html

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

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

@@ -1,5 +1,5 @@
 <div class="row page-attachments-row hidden-print">
-  <div class="col-xs-12">
+  <div class="col-12">
     <div class="mt-4 mb-4">
       <div class="page-attachments" id="page-attachment"></div>
 

+ 1 - 0
src/server/views/widget/page_content.html

@@ -9,6 +9,7 @@
   data-page-revision-id-hackmd-synced="{% if revisionHackmdSynced %}{{ revisionHackmdSynced.toString() }}{% endif %}"
   data-page-id-on-hackmd="{% if pageIdOnHackmd %}{{ pageIdOnHackmd.toString() }}{% endif %}"
   data-page-has-draft-on-hackmd="{% if hasDraftOnHackmd %}{{ hasDraftOnHackmd.toString() }}{% endif %}"
+  data-page-is-liked="{% if page.isLiked(user) %}true{% else %}false{% endif %}"
   data-page-is-seen="{% if page and page.isSeenUser(user) %}1{% else %}0{% endif %}"
   data-slack-channels="{{ slack|default('') }}"
   data-page-created-at="{% if page %}{{ page.createdAt|datetz('Y/m/d H:i:s') }}{% endif %}"

+ 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">

+ 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>