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

Merge branch 'master' into imprv/profile-image-cache

yusuketk 5 лет назад
Родитель
Сommit
ea9bf8a820
92 измененных файлов с 1324 добавлено и 1064 удалено
  1. 0 95
      .github/workflows/build.yml
  2. 0 0
      .github/workflows/release-rc.yml
  3. 88 2
      .github/workflows/release.yml
  4. 29 1
      CHANGES.md
  5. 2 0
      config/webpack.dev.dll.js
  6. 3 3
      package.json
  7. 5 0
      resource/locales/en-US/translation.json
  8. 5 0
      resource/locales/ja/translation.json
  9. 4 4
      src/client/js/components/Admin/ExportArchiveData/ArchiveFilesTableMenu.jsx
  10. 2 2
      src/client/js/components/Admin/ImportData/GrowiArchive/ImportCollectionItem.jsx
  11. 15 14
      src/client/js/components/Admin/Notification/GlobalNotificationList.jsx
  12. 43 0
      src/client/js/components/Admin/Notification/NotificationTypeIcon.jsx
  13. 3 3
      src/client/js/components/Admin/Notification/SlackAppConfiguration.jsx
  14. 5 2
      src/client/js/components/Admin/Notification/UserNotificationRow.jsx
  15. 4 4
      src/client/js/components/Admin/Security/LdapSecuritySetting.jsx
  16. 6 6
      src/client/js/components/Admin/Security/LocalSecuritySetting.jsx
  17. 14 10
      src/client/js/components/Admin/Security/SecuritySetting.jsx
  18. 3 3
      src/client/js/components/Admin/UserGroup/UserGroupTable.jsx
  19. 2 2
      src/client/js/components/Admin/UserGroupDetail/UserGroupUserTable.jsx
  20. 2 2
      src/client/js/components/Admin/Users/ExternalAccountTable.jsx
  21. 2 2
      src/client/js/components/Admin/Users/GiveAdminButton.jsx
  22. 2 2
      src/client/js/components/Admin/Users/RemoveAdminButton.jsx
  23. 2 2
      src/client/js/components/Admin/Users/StatusActivateButton.jsx
  24. 2 2
      src/client/js/components/Admin/Users/StatusSuspendedButton.jsx
  25. 2 2
      src/client/js/components/Admin/Users/UserMenu.jsx
  26. 2 2
      src/client/js/components/Admin/Users/UserRemoveButton.jsx
  27. 2 2
      src/client/js/components/LoginForm.jsx
  28. 5 1
      src/client/js/components/MyDraftList/MyDraftList.jsx
  29. 2 2
      src/client/js/components/Navbar/PageCreateButton.jsx
  30. 3 3
      src/client/js/components/Navbar/PersonalDropdown.jsx
  31. 8 8
      src/client/js/components/Page/PageManagement.jsx
  32. 9 16
      src/client/js/components/PageComment/Comment.jsx
  33. 174 116
      src/client/js/components/PageComment/CommentEditor.jsx
  34. 10 64
      src/client/js/components/PageComment/CommentEditorLazyRenderer.jsx
  35. 9 9
      src/client/js/components/PageComment/ReplayComments.jsx
  36. 16 4
      src/client/js/components/PageComments.jsx
  37. 44 31
      src/client/js/components/PageCreateModal.jsx
  38. 1 1
      src/client/js/components/PageDeleteModal.jsx
  39. 1 1
      src/client/js/components/PageDuplicateModal.jsx
  40. 2 2
      src/client/js/components/PageEditor.jsx
  41. 1 1
      src/client/js/components/PageEditor/HandsontableModal.jsx
  42. 8 4
      src/client/js/components/PageEditor/TextAreaEditor.jsx
  43. 11 4
      src/client/js/components/PageRenameModal.jsx
  44. 7 7
      src/client/js/components/SearchForm.jsx
  45. 19 5
      src/client/js/components/Sidebar.jsx
  46. 11 24
      src/client/js/components/Sidebar/CustomSidebar.jsx
  47. 17 30
      src/client/js/components/Sidebar/RecentChanges.jsx
  48. 33 55
      src/client/js/components/Sidebar/SidebarNav.jsx
  49. 10 2
      src/client/js/components/User/UserPicture.jsx
  50. 5 4
      src/client/js/services/AppContainer.js
  51. 8 4
      src/client/styles/scss/_admin.scss
  52. 2 9
      src/client/styles/scss/_layout.scss
  53. 4 0
      src/client/styles/scss/_layout_kibela.scss
  54. 10 55
      src/client/styles/scss/_login.scss
  55. 3 0
      src/client/styles/scss/_navbar.scss
  56. 6 10
      src/client/styles/scss/_on-edit.scss
  57. 1 0
      src/client/styles/scss/_override-bootstrap-variables.scss
  58. 11 0
      src/client/styles/scss/_override-bootstrap.scss
  59. 57 3
      src/client/styles/scss/_search.scss
  60. 45 62
      src/client/styles/scss/_sidebar.scss
  61. 1 1
      src/client/styles/scss/atoms/_buttons.scss
  62. 3 0
      src/client/styles/scss/atoms/_nav.scss
  63. 1 1
      src/client/styles/scss/molecules/copy-dropdown.scss
  64. 8 0
      src/client/styles/scss/style-app.scss
  65. 107 0
      src/client/styles/scss/theme/_apply-colors-dark.scss
  66. 82 6
      src/client/styles/scss/theme/_apply-colors-light.scss
  67. 45 31
      src/client/styles/scss/theme/_apply-colors.scss
  68. 16 8
      src/client/styles/scss/theme/antarctic.scss
  69. 27 34
      src/client/styles/scss/theme/christmas.scss
  70. 4 2
      src/client/styles/scss/theme/default.scss
  71. 18 5
      src/client/styles/scss/theme/future.scss
  72. 3 2
      src/client/styles/scss/theme/halloween.scss
  73. 5 4
      src/client/styles/scss/theme/island.scss
  74. 2 2
      src/client/styles/scss/theme/mixins/_list-group.scss
  75. 8 6
      src/client/styles/scss/theme/mono-blue.scss
  76. 17 7
      src/client/styles/scss/theme/spring.scss
  77. 26 12
      src/client/styles/scss/theme/wood.scss
  78. 1 1
      src/lib/components/PagePathHierarchicalLink.jsx
  79. 6 2
      src/server/models/page.js
  80. 1 1
      src/server/routes/apiv3/pages.js
  81. 7 7
      src/server/routes/page.js
  82. 8 12
      src/server/views/layout-growi/base/layout.html
  83. 6 8
      src/server/views/layout-growi/widget/liker-and-seenusers.html
  84. 7 5
      src/server/views/layout/layout.html
  85. 3 3
      src/server/views/login/error.html
  86. 2 2
      src/server/views/widget/not_found_tabs.html
  87. 7 6
      src/server/views/widget/page_alerts.html
  88. 1 1
      src/server/views/widget/page_content.html
  89. 8 8
      src/server/views/widget/page_tabs.html
  90. 1 1
      src/server/views/widget/page_tabs_kibela.html
  91. 12 4
      src/server/views/widget/user_page_content.html
  92. 79 178
      yarn.lock

+ 0 - 95
.github/workflows/build.yml

@@ -1,95 +0,0 @@
-name: Release Docker Images
-
-on:
-  push:
-    branches:
-      - tmp/release-**
-
-
-jobs:
-
-  build:
-
-    runs-on: ubuntu-latest
-
-    strategy:
-      matrix:
-        flavor: [default, nocdn]
-
-    steps:
-    - uses: actions/checkout@v2
-
-    - name: Determine suffix
-      run: |
-        [[ ${{ matrix.flavor }} = "nocdn" ]] && suffix="-nocdn" || suffix=""
-        echo ::set-env name=SUFFIX::$suffix
-
-    - name: Cache/Restore yarn cache
-      uses: actions/cache@v1
-      with:
-        path: /usr/local/share/.cache/yarn
-        key: ${{ runner.os }}-yarn-${{ matrix.node-version }}-${{ hashFiles('**/yarn.lock') }}
-        restore-keys: |
-          ${{ runner.os }}-yarn-${{ matrix.node-version }}-
-
-    - name: Set up Docker Buildx
-      uses: crazy-max/ghaction-docker-buildx@v1.0.4
-
-    - name: Login to docker.io registry
-      run: |
-        echo ${{ secrets. DOCKER_REGISTRY_PASSWORD }} | docker login --username wsmoogle --password-stdin
-
-    - name: Build Docker Image
-      run: |
-        CACHE_REF=weseek/growi-cache:3${{ env.SUFFIX }}
-        docker buildx build \
-          --tag growi${{ env.SUFFIX }} \
-          --build-arg flavor=${{ matrix.flavor }} \
-          --platform linux/amd64 \
-          --load \
-          --cache-from=type=registry,ref=$CACHE_REF \
-          --cache-to=type=registry,ref=$CACHE_REF,mode=max \
-          --file ./docker/Dockerfile .
-
-    - name: Get SemVer
-      run: |
-        semver=`npm run version --silent`
-        echo ::set-env name=SEMVER::$semver
-
-    - name: Docker Tags by SemVer
-      uses: weseek/ghaction-docker-tags-by-semver@v1.0.5
-      with:
-        source: growi${{ env.SUFFIX }}
-        target: weseek/growi
-        semver: ${{ env.SEMVER }}
-        suffix: ${{ env.SUFFIX }}
-        additional-tags: 'latest'
-        publish: true
-
-    - name: Slack Notification
-      uses: weseek/ghaction-release-slack-notification@master
-      with:
-        channel: '#general'
-        url: ${{ secrets.SLACK_WEBHOOK_URL }}
-        created_tag: 'v${{ env.SEMVER }}${{ env.SUFFIX }}'
-
-    - name: Check whether workspace is clean
-      run: |
-        STATUS=`git status --porcelain`
-        if [ -z "$STATUS" ]; then exit 0; else exit 1; fi
-
-  publish-desc:
-
-    runs-on: ubuntu-latest
-    needs: build
-
-    steps:
-    - uses: actions/checkout@v2
-
-    - name: Update Docker Hub Description
-      uses: peter-evans/dockerhub-description@v2.1.0
-      env:
-        DOCKERHUB_USERNAME: wsmoogle
-        DOCKERHUB_PASSWORD: ${{ secrets.DOCKER_REGISTRY_PASSWORD }}
-        DOCKERHUB_REPOSITORY: weseek/growi
-        README_FILEPATH: ./docker/README.md

+ 0 - 0
.github/workflows/build-rc.yml → .github/workflows/release-rc.yml


+ 88 - 2
.github/workflows/release.yml

@@ -1,4 +1,4 @@
-name: GitHub Release
+name: Release
 
 on:
   push:
@@ -6,10 +6,13 @@ on:
       - release/**
 
 jobs:
-  release:
+  github-release:
 
     runs-on: ubuntu-latest
 
+    outputs:
+      RELEASE_VERSION: ${{ steps.bump-version.outputs.RELEASE_VERSION }}
+
     steps:
     - uses: actions/checkout@v2
 
@@ -20,11 +23,13 @@ jobs:
         git remote set-url origin "https://$GITHUB_ACTOR:${{ secrets.GITHUB_TOKEN }}@github.com/$GITHUB_REPOSITORY"
 
     - name: Bump version
+      id: bump-version
       run: |
         npm --no-git-tag-version version patch
         export RELEASE_VERSION=`npm run version --silent`
         sh ./bin/github-actions/update-readme.sh
         echo ::set-env name=RELEASE_VERSION::$RELEASE_VERSION
+        echo ::set-output name=RELEASE_VERSION::$RELEASE_VERSION
 
     - name: Checkout, Commit, Tag and Push
       run: |
@@ -42,3 +47,84 @@ jobs:
         changelog_file: CHANGES.md
       env:
         GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+
+
+  build-image:
+    needs: github-release
+
+    runs-on: ubuntu-latest
+
+    strategy:
+      matrix:
+        flavor: [default, nocdn]
+
+    steps:
+    - uses: actions/checkout@v2
+
+    - name: Checkout released tag
+      run: |
+        git fetch --tags
+        git checkout refs/tags/v${{ needs.github-release.outputs.RELEASE_VERSION }}
+
+    - name: Determine suffix
+      run: |
+        [[ ${{ matrix.flavor }} = "nocdn" ]] && suffix="-nocdn" || suffix=""
+        echo ::set-env name=SUFFIX::$suffix
+
+    - name: Cache/Restore yarn cache
+      uses: actions/cache@v1
+      with:
+        path: /usr/local/share/.cache/yarn
+        key: ${{ runner.os }}-yarn-${{ matrix.node-version }}-${{ hashFiles('**/yarn.lock') }}
+        restore-keys: |
+          ${{ runner.os }}-yarn-${{ matrix.node-version }}-
+
+    - name: Set up Docker Buildx
+      uses: crazy-max/ghaction-docker-buildx@v1.0.4
+
+    - name: Login to docker.io registry
+      run: |
+        echo ${{ secrets. DOCKER_REGISTRY_PASSWORD }} | docker login --username wsmoogle --password-stdin
+
+    - name: Build Docker Image
+      run: |
+        CACHE_REF=weseek/growi-cache:3${{ env.SUFFIX }}
+        docker buildx build \
+          --tag growi${{ env.SUFFIX }} \
+          --build-arg flavor=${{ matrix.flavor }} \
+          --platform linux/amd64 \
+          --load \
+          --cache-from=type=registry,ref=$CACHE_REF \
+          --cache-to=type=registry,ref=$CACHE_REF,mode=max \
+          --file ./docker/Dockerfile .
+
+    - name: Docker Tags by SemVer
+      uses: weseek/ghaction-docker-tags-by-semver@v1.0.5
+      with:
+        source: growi${{ env.SUFFIX }}
+        target: weseek/growi
+        semver: ${{ needs.github-release.outputs.RELEASE_VERSION }}
+        suffix: ${{ env.SUFFIX }}
+        additional-tags: 'latest'
+        publish: true
+
+    - name: Slack Notification
+      uses: weseek/ghaction-release-slack-notification@master
+      with:
+        channel: '#general'
+        url: ${{ secrets.SLACK_WEBHOOK_URL }}
+        created_tag: 'v${{ needs.github-release.outputs.RELEASE_VERSION }}${{ env.SUFFIX }}'
+
+    - name: Update Docker Hub Description
+      uses: peter-evans/dockerhub-description@v2.1.0
+      env:
+        DOCKERHUB_USERNAME: wsmoogle
+        DOCKERHUB_PASSWORD: ${{ secrets.DOCKER_REGISTRY_PASSWORD }}
+        DOCKERHUB_REPOSITORY: weseek/growi
+        README_FILEPATH: ./docker/README.md
+
+    - name: Check whether workspace is clean
+      run: |
+        STATUS=`git status --porcelain`
+        if [ -z "$STATUS" ]; then exit 0; else exit 1; fi
+

+ 29 - 1
CHANGES.md

@@ -1,6 +1,32 @@
 # CHANGES
 
-## v4.0.0-RC
+## v4.0.3-RC
+
+* Fix: Editor doesn't work on mobile
+* Fix: navbar is broken on Safari
+
+## v4.0.2
+
+* Fix: Internal Server Error occurred when the guest user access to the pages that has likes
+* Fix: Some buttons are broken on Safari
+
+## v4.0.1
+
+* Improvement: Accessibility for Handsontable under dark mode
+* Improvement: Refactor '/pages.exist' API
+* Fix: Storing the state of sidebar
+* Fix: Comments order should be asc
+* Fix: Show/Hide replies button doesn't work
+* Fix: Tooltip doesn't work
+* Fix: Change the display of the scroll bar when modal is shown
+* Fix: Submit with enter key on Create/Rename modals
+* Fix: Show/Hide Unlink redirection button conditions
+* Fix: Link color in alerts
+* Support: Upgrade libs
+    * @atlaskit/drawer
+    * @atlaskit/navigation-next
+
+## v4.0.0
 
 ### BREAKING CHANGES
 
@@ -9,6 +35,8 @@
 * 'default-dark' theme is now merged as a dark mode variant of 'default' theme
 * 'blue-night' theme is now merged as a dark mode variant of 'mono-blue' theme
 
+Upgrading Guide: <https://docs.growi.org/en/admin-guide/upgrading/40x.html>
+
 ### Updates
 
 * Feature: Sidebar

+ 2 - 0
config/webpack.dev.dll.js

@@ -10,6 +10,8 @@ module.exports = {
   entry: {
     dlls: [
       // Libraries
+      '@atlaskit/drawer',
+      '@atlaskit/navigation-next',
       'axios',
       'browser-bunyan', 'bunyan-format',
       'codemirror', 'react-codemirror2',

+ 3 - 3
package.json

@@ -1,6 +1,6 @@
 {
   "name": "growi",
-  "version": "4.0.0-RC",
+  "version": "4.0.3-RC",
   "description": "Team collaboration software using markdown",
   "tags": [
     "wiki",
@@ -154,8 +154,8 @@
       "handsontable: v7.0.0 or above is no loger MIT lisence."
     ],
     "@alienfast/i18next-loader": "^1.0.16",
-    "@atlaskit/drawer": "^5.3.5",
-    "@atlaskit/navigation-next": "^8.0.2",
+    "@atlaskit/drawer": "^5.3.7",
+    "@atlaskit/navigation-next": "^8.0.5",
     "@babel/core": "^7.4.5",
     "@babel/plugin-proposal-class-properties": "^7.8.3",
     "@babel/plugin-proposal-optional-chaining": "^7.9.0",

+ 5 - 0
resource/locales/en-US/translation.json

@@ -678,5 +678,10 @@
     "export_menu": "Export Menu",
     "download": "Download",
     "delete": "Delete"
+  },
+  "login": {
+    "Sign in error": "Login error",
+    "Registration successful": "Registration successful",
+    "Setup": "Setup"
   }
 }

+ 5 - 0
resource/locales/ja/translation.json

@@ -667,5 +667,10 @@
     "export_menu": "エクスポートメニュー",
     "download": "ダウンロード",
     "delete": "削除"
+  },
+  "login": {
+    "Sign in error": "ログインエラー",
+    "Registration successful": "登録完了",
+    "Setup": "セットアップ"
   }
 }

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

@@ -18,12 +18,12 @@ class ArchiveFilesTableMenu extends React.Component {
         </button>
         <ul className="dropdown-menu" role="menu">
           <li className="dropdown-header">{t('admin:export_management.export_menu')}</li>
-          <a type="button" className="dropdown-item" href={`/admin/export/${this.props.fileName}`}>
+          <button type="button" className="dropdown-item" href={`/admin/export/${this.props.fileName}`}>
             <i className="icon-cloud-download" /> {t('admin:export_management.download')}
-          </a>
-          <a type="button" className="dropdown-item" role="button" onClick={() => this.props.onZipFileStatRemove(this.props.fileName)}>
+          </button>
+          <button type="button" className="dropdown-item" role="button" onClick={() => this.props.onZipFileStatRemove(this.props.fileName)}>
             <span className="text-danger"><i className="icon-trash" /> {t('admin:export_management.delete')}</span>
-          </a>
+          </button>
         </ul>
       </div>
     );

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

@@ -131,9 +131,9 @@ export default class ImportCollectionItem extends React.Component {
             { modes.map((mode) => {
               return (
                 <li key={`buttonMode_${mode}`}>
-                  <a type="button" className="dropdown-item" role="button" onClick={() => this.modeSelectedHandler(mode)}>
+                  <button type="button" className="dropdown-item" role="button" onClick={() => this.modeSelectedHandler(mode)}>
                     {this.renderModeLabel(mode, true)}
-                  </a>
+                  </button>
                 </li>
               );
             }) }

+ 15 - 14
src/client/js/components/Admin/Notification/GlobalNotificationList.jsx

@@ -9,7 +9,9 @@ import { toastSuccess, toastError } from '../../../util/apiNotification';
 
 import AppContainer from '../../../services/AppContainer';
 import AdminNotificationContainer from '../../../services/AdminNotificationContainer';
+
 import NotificationDeleteModal from './NotificationDeleteModal';
+import NotificationTypeIcon from './NotificationTypeIcon';
 
 const logger = loggerFactory('growi:GolobalNotificationList');
 
@@ -91,44 +93,43 @@ class GlobalNotificationList extends React.Component {
                 {notification.triggerPath}
               </td>
               <td>
-                <ul className="list-inline">
+                <ul className="list-inline mb-0">
                   {notification.triggerEvents.includes('pageCreate') && (
-                  <li className="list-inline-item badge badge-pill badge-success" data-toggle="tooltip" data-placement="top" title="Page Create">
+                  <li className="list-inline-item badge badge-pill badge-success">
                     <i className="icon-doc"></i> CREATE
                   </li>
                 )}
                   {notification.triggerEvents.includes('pageEdit') && (
-                  <li className="list-inline-item badge badge-pill badge-warning" data-toggle="tooltip" data-placement="top" title="Page Edit">
+                  <li className="list-inline-item badge badge-pill badge-warning">
                     <i className="icon-pencil"></i> EDIT
                   </li>
                 )}
                   {notification.triggerEvents.includes('pageMove') && (
-                  <li className="list-inline-item badge badge-pill badge-pink" data-toggle="tooltip" data-placement="top" title="Page Move">
+                  <li className="list-inline-item badge badge-pill badge-pink">
                     <i className="icon-action-redo"></i> MOVE
                   </li>
                 )}
                   {notification.triggerEvents.includes('pageDelete') && (
-                  <li className="list-inline-item badge badge-pill badge-danger" data-toggle="tooltip" data-placement="top" title="Page Delte">
+                  <li className="list-inline-item badge badge-pill badge-danger">
                     <i className="icon-fire"></i> DELETE
                   </li>
                 )}
                   {notification.triggerEvents.includes('pageLike') && (
-                  <li className="list-inline-item badge badge-pill badge-info" data-toggle="tooltip" data-placement="top" title="Page Like">
+                  <li className="list-inline-item badge badge-pill badge-info">
                     <i className="icon-like"></i> LIKE
                   </li>
                 )}
                   {notification.triggerEvents.includes('comment') && (
-                  <li className="list-inline-item badge badge-pill badge-secondary" data-toggle="tooltip" data-placement="top" title="New Comment">
+                  <li className="list-inline-item badge badge-pill badge-secondary">
                     <i className="icon-fw icon-bubble"></i> POST
                   </li>
                 )}
                 </ul>
               </td>
               <td>
-                {notification.__t === 'mail'
-                  && <span data-toggle="tooltip" data-placement="top" title="Email"><i className="ti-email"></i> {notification.toEmail}</span>}
-                {notification.__t === 'slack'
-                  && <span data-toggle="tooltip" data-placement="top" title="Slack"><i className="fa fa-hashtag"></i> {notification.slackChannels}</span>}
+                <NotificationTypeIcon notification={notification} />
+                { notification.__t === 'mail' && notification.toEmail }
+                { notification.__t === 'slack' && notification.slackChannels }
               </td>
               <td className="td-abs-center">
                 <div className="dropdown">
@@ -143,12 +144,12 @@ class GlobalNotificationList extends React.Component {
                     <i className="icon-settings"></i> <span className="caret"></span>
                   </button>
                   <div className="dropdown-menu dropdown-menu-right" aria-labelledby="dropdownMenuButton">
-                    <a className="dropdown-item" type="button" href={urljoin('/admin/global-notification/', notification._id)}>
+                    <a className="dropdown-item" href={urljoin('/admin/global-notification/', notification._id)}>
                       <i className="icon-fw icon-note"></i> {t('Edit')}
                     </a>
-                    <a className="dropdown-item" type="button" onClick={() => this.openConfirmationModal(notification)}>
+                    <button className="dropdown-item" type="button" onClick={() => this.openConfirmationModal(notification)}>
                       <i className="icon-fw icon-fire text-danger"></i> {t('Delete')}
-                    </a>
+                    </button>
                   </div>
                 </div>
               </td>

+ 43 - 0
src/client/js/components/Admin/Notification/NotificationTypeIcon.jsx

@@ -0,0 +1,43 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+
+import { UncontrolledTooltip } from 'reactstrap';
+
+const SlackIcon = (props) => {
+  const { __t, _id, provider } = props.notification;
+
+  let type = 'slack';
+
+  // User trigger notification
+  if (provider != null) {
+    // only slack type
+  }
+
+  // Global notification
+  if (__t != null) {
+    if (__t === 'mail') {
+      type = 'mail';
+    }
+  }
+
+  const elemId = `notification-${type}-${_id}`;
+  const className = type === 'mail'
+    ? 'icon-fw fa fa-envelope-o'
+    : 'icon-fw fa fa-hashtag';
+
+  return (
+    <>
+      <i id={elemId} className={className}></i>
+      <UncontrolledTooltip target={elemId}>Slack</UncontrolledTooltip>
+    </>
+  );
+};
+
+SlackIcon.propTypes = {
+  // supports 2 types:
+  //   User trigger notification -> has 'provider: slack'
+  //   Global notification -> has '__t: slack|mail'
+  notification: PropTypes.object.isRequired,
+};
+
+export default SlackIcon;

+ 3 - 3
src/client/js/components/Admin/Notification/SlackAppConfiguration.jsx

@@ -53,10 +53,10 @@ class SlackAppConfiguration extends React.Component {
                 {`Slack ${adminNotificationContainer.state.selectSlackOption}`}
               </button>
               <div className="dropdown-menu" aria-labelledby="dropdownMenuButton">
-                <a className="dropdown-item" type="button" onClick={() => adminNotificationContainer.switchSlackOption('Incoming Webhooks')}>
+                <button className="dropdown-item" type="button" onClick={() => adminNotificationContainer.switchSlackOption('Incoming Webhooks')}>
                   Slack Incoming Webhooks
-                </a>
-                <a className="dropdown-item" type="button" onClick={() => adminNotificationContainer.switchSlackOption('App')}>Slack App</a>
+                </button>
+                <button className="dropdown-item" type="button" onClick={() => adminNotificationContainer.switchSlackOption('App')}>Slack App</button>
               </div>
             </div>
           </div>

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

@@ -7,19 +7,22 @@ import { createSubscribedElement } from '../../UnstatedUtils';
 import AppContainer from '../../../services/AppContainer';
 import AdminNotificationContainer from '../../../services/AdminNotificationContainer';
 
+import NotificationTypeIcon from './NotificationTypeIcon';
 
 class UserNotificationRow extends React.PureComponent {
 
   render() {
     const { t, notification } = this.props;
+    const id = `user-notification-${notification._id}`;
+
     return (
       <React.Fragment>
-        <tr className="admin-notif-row" key={notification._id}>
+        <tr className="admin-notif-row" key={id}>
           <td className="px-4">
             {notification.pathPattern}
           </td>
           <td className="px-4">
-            <span data-toggle="tooltip" data-placement="top" title="Slack"><i className="fa fa-hashtag"></i> {notification.channel}</span>
+            <NotificationTypeIcon notification={notification} />{notification.channel}
           </td>
           <td>
             <button type="submit" className="btn btn-outline-danger" onClick={() => { this.props.onClickDeleteBtn(notification._id) }}>{t('Delete')}</button>

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

@@ -140,12 +140,12 @@ class LdapSecuritySetting extends React.Component {
                         : <span className="pull-left">{t('security_setting.ldap.bind_manager')}</span>}
                   </button>
                   <div className="dropdown-menu" aria-labelledby="dropdownMenuButton">
-                    <a className="dropdown-item" type="button" onClick={() => { adminLdapSecurityContainer.changeLdapBindMode(true) }}>
+                    <button className="dropdown-item" type="button" onClick={() => { adminLdapSecurityContainer.changeLdapBindMode(true) }}>
                       {t('security_setting.ldap.bind_user')}
-                    </a>
-                    <a className="dropdown-item" type="button" onClick={() => { adminLdapSecurityContainer.changeLdapBindMode(false) }}>
+                    </button>
+                    <button className="dropdown-item" type="button" onClick={() => { adminLdapSecurityContainer.changeLdapBindMode(false) }}>
                       {t('security_setting.ldap.bind_manager')}
-                    </a>
+                    </button>
                   </div>
                 </div>
               </div>

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

@@ -118,15 +118,15 @@ class LocalSecuritySetting extends React.Component {
                     {registrationMode === 'Closed' && t('security_setting.registration_mode.closed')}
                   </button>
                   <div className="dropdown-menu" aria-labelledby="dropdownMenuButton">
-                    <a className="dropdown-item" type="button" onClick={() => { adminLocalSecurityContainer.changeRegistrationMode('Open') }}>
+                    <button className="dropdown-item" type="button" onClick={() => { adminLocalSecurityContainer.changeRegistrationMode('Open') }}>
                       {t('security_setting.registration_mode.open')}
-                    </a>
-                    <a className="dropdown-item" type="button" onClick={() => { adminLocalSecurityContainer.changeRegistrationMode('Restricted') }}>
+                    </button>
+                    <button className="dropdown-item" type="button" onClick={() => { adminLocalSecurityContainer.changeRegistrationMode('Restricted') }}>
                       {t('security_setting.registration_mode.restricted')}
-                    </a>
-                    <a className="dropdown-item" type="button" onClick={() => { adminLocalSecurityContainer.changeRegistrationMode('Closed') }}>
+                    </button>
+                    <button className="dropdown-item" type="button" onClick={() => { adminLocalSecurityContainer.changeRegistrationMode('Closed') }}>
                       {t('security_setting.registration_mode.closed')}
-                    </a>
+                    </button>
                   </div>
                 </div>
 

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

@@ -135,12 +135,12 @@ class SecuritySetting extends React.Component {
                 </span>
               </button>
               <div className="dropdown-menu" aria-labelledby="dropdownMenuButton">
-                <a className="dropdown-item" type="button" onClick={() => { adminGeneralSecurityContainer.changeRestrictGuestMode('Deny') }}>
+                <button className="dropdown-item" type="button" onClick={() => { adminGeneralSecurityContainer.changeRestrictGuestMode('Deny') }}>
                   {t('security_setting.guest_mode.deny')}
-                </a>
-                <a className="dropdown-item" type="button" onClick={() => { adminGeneralSecurityContainer.changeRestrictGuestMode('Readonly') }}>
+                </button>
+                <button className="dropdown-item" type="button" onClick={() => { adminGeneralSecurityContainer.changeRestrictGuestMode('Readonly') }}>
                   {t('security_setting.guest_mode.readonly')}
-                </a>
+                </button>
               </div>
             </div>
             {adminGeneralSecurityContainer.isWikiModeForced && (
@@ -180,19 +180,23 @@ class SecuritySetting extends React.Component {
                 </span>
               </button>
               <div className="dropdown-menu" aria-labelledby="dropdownMenuButton">
-                <a className="dropdown-item" type="button" onClick={() => { adminGeneralSecurityContainer.changePageCompleteDeletionAuthority('anyOne') }}>
+                <button className="dropdown-item" type="button" onClick={() => { adminGeneralSecurityContainer.changePageCompleteDeletionAuthority('anyOne') }}>
                   {t('security_setting.anyone')}
-                </a>
-                <a className="dropdown-item" type="button" onClick={() => { adminGeneralSecurityContainer.changePageCompleteDeletionAuthority('adminOnly') }}>
+                </button>
+                <button
+                  className="dropdown-item"
+                  type="button"
+                  onClick={() => { adminGeneralSecurityContainer.changePageCompleteDeletionAuthority('adminOnly') }}
+                >
                   {t('security_setting.admin_only')}
-                </a>
-                <a
+                </button>
+                <button
                   className="dropdown-item"
                   type="button"
                   onClick={() => { adminGeneralSecurityContainer.changePageCompleteDeletionAuthority('adminAndAuthor') }}
                 >
                   {t('security_setting.admin_and_author')}
-                </a>
+                </button>
               </div>
               <p className="form-text text-muted small">
                 {t('security_setting.complete_deletion_explain')}

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

@@ -87,12 +87,12 @@ class UserGroupTable extends React.Component {
                             <i className="icon-settings"></i>
                           </button>
                           <div className="dropdown-menu" role="menu" aria-labelledby={`admin-group-menu-button-${group._id}`}>
-                            <a className="dropdown-item" type="button" href={`/admin/user-group-detail/${group._id}`}>
+                            <a className="dropdown-item" href={`/admin/user-group-detail/${group._id}`}>
                               <i className="icon-fw icon-note"></i> {t('Edit')}
                             </a>
-                            <a className="dropdown-item" type="button" role="button" onClick={this.onDelete} data-user-group-id={group._id}>
+                            <button className="dropdown-item" type="button" role="button" onClick={this.onDelete} data-user-group-id={group._id}>
                               <i className="icon-fw icon-fire text-danger"></i> {t('Delete')}
-                            </a>
+                            </button>
                           </div>
                         </div>
                       </td>

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

@@ -73,7 +73,7 @@ class UserGroupUserTable extends React.Component {
                       <i className="icon-settings"></i>
                     </button>
                     <div className="dropdown-menu" role="menu" aria-labelledby={`admin-group-menu-button-${relatedUser._id}`}>
-                      <a
+                      <button
                         className="dropdown-item"
                         type="button"
                         onClick={() => {
@@ -81,7 +81,7 @@ class UserGroupUserTable extends React.Component {
                         }}
                       >
                         <i className="icon-fw icon-user-unfollow"></i> {t('admin:user_group_management.remove_from_group')}
-                      </a>
+                      </button>
                     </div>
                   </div>
                 </td>

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

@@ -99,9 +99,9 @@ class ExternalAccountTable extends React.Component {
                       </button>
                       <ul className="dropdown-menu" role="menu">
                         <li className="dropdown-header">{t('admin:user_management.user_table.edit_menu')}</li>
-                        <a className="dropdown-item" type="button" role="button" onClick={() => { return this.removeExtenalAccount(ea._id) }}>
+                        <button className="dropdown-item" type="button" role="button" onClick={() => { return this.removeExtenalAccount(ea._id) }}>
                           <i className="icon-fw icon-fire text-danger"></i> {t('Delete')}
-                        </a>
+                        </button>
                       </ul>
                     </div>
                   </td>

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

@@ -31,9 +31,9 @@ class GiveAdminButton extends React.Component {
     const { t } = this.props;
 
     return (
-      <a className="dropdown-item" type="button" onClick={() => { this.onClickGiveAdminBtn() }}>
+      <button className="dropdown-item" type="button" onClick={() => { this.onClickGiveAdminBtn() }}>
         <i className="icon-fw icon-user-following"></i> {t('admin:user_management.user_table.give_admin_access')}
-      </a>
+      </button>
     );
   }
 

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

@@ -32,9 +32,9 @@ class RemoveAdminButton extends React.Component {
     const { t } = this.props;
 
     return (
-      <a className="dropdown-item" type="button" onClick={() => { this.onClickRemoveAdminBtn() }}>
+      <button className="dropdown-item" type="button" onClick={() => { this.onClickRemoveAdminBtn() }}>
         <i className="icon-fw icon-user-unfollow"></i> {t('admin:user_management.user_table.remove_admin_access')}
-      </a>
+      </button>
     );
   }
 

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

@@ -31,9 +31,9 @@ class StatusActivateButton extends React.Component {
     const { t } = this.props;
 
     return (
-      <a className="dropdown-item" type="button" onClick={() => { this.onClickAcceptBtn() }}>
+      <button className="dropdown-item" type="button" onClick={() => { this.onClickAcceptBtn() }}>
         <i className="icon-fw icon-user-following"></i> {t('admin:user_management.user_table.accept')}
-      </a>
+      </button>
     );
   }
 

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

@@ -31,9 +31,9 @@ class StatusSuspendedButton extends React.Component {
     const { t } = this.props;
 
     return (
-      <a className="dropdown-item" type="button" onClick={() => { this.onClickDeactiveBtn() }}>
+      <button className="dropdown-item" type="button" onClick={() => { this.onClickDeactiveBtn() }}>
         <i className="icon-fw icon-ban"></i> {t('admin:user_management.user_table.deactivate_account')}
-      </a>
+      </button>
     );
   }
 

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

@@ -36,9 +36,9 @@ class UserMenu extends React.Component {
         <li className="dropdown-divider"></li>
         <li className="dropdown-header">{t('admin:user_management.user_table.edit_menu')}</li>
         <li>
-          <a className="dropdown-item" type="button" onClick={this.onPasswordResetClicked}>
+          <button className="dropdown-item" type="button" onClick={this.onPasswordResetClicked}>
             <i className="icon-fw icon-key"></i>{ t('admin:user_management.reset_password') }
-          </a>
+          </button>
         </li>
       </Fragment>
     );

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

@@ -32,9 +32,9 @@ class UserRemoveButton extends React.Component {
     const { t } = this.props;
 
     return (
-      <a className="dropdown-item" type="button" onClick={() => { this.onClickDeleteBtn() }}>
+      <button className="dropdown-item" type="button" onClick={() => { this.onClickDeleteBtn() }}>
         <i className="icon-fw icon-fire text-danger"></i> {t('Delete')}
-      </a>
+      </button>
     );
   }
 

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

@@ -116,7 +116,7 @@ class LoginForm extends React.Component {
 
     return (
       <>
-        <div className="border-top border-bottom">
+        <div className="grw-external-auth-form border-top border-bottom">
           <div id="external-auth" className={`external-auth ${collapsibleClass}`}>
             <div className="row mt-2">
               {Object.keys(objOfIsExternalAuthEnableds).map((auth) => {
@@ -131,7 +131,7 @@ class LoginForm extends React.Component {
         <div className="text-center">
           <button
             type="button"
-            className="btn btn-secondary btn-sm rounded-0 mb-3"
+            className="btn btn-secondary btn-external-auth-tab btn-sm rounded-0 mb-3"
             data-toggle={isExternalAuthCollapsible ? 'collapse' : ''}
             data-target="#external-auth"
             aria-expanded="false"

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

@@ -45,8 +45,12 @@ class MyDraftList extends React.Component {
   async getDraftsFromLocalStorage() {
     const draftsAsObj = this.props.editorContainer.drafts;
 
+    if (draftsAsObj == null) {
+      return;
+    }
+
     const res = await this.props.appContainer.apiGet('/pages.exist', {
-      pages: draftsAsObj,
+      pagePaths: JSON.stringify(Object.keys(draftsAsObj)),
     });
 
     // {'/a': '#a', '/b': '#b'} => [{path: '/a', markdown: '#a'}, {path: '/b', markdown: '#b'}]

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

@@ -18,10 +18,10 @@ const PageCreateButton = (props) => {
   }
 
   return (
-    <a className="nav-link create-page" type="button" onClick={appContainer.openPageCreateModal}>
+    <button className="nav-link create-page border-0 bg-transparent" type="button" onClick={appContainer.openPageCreateModal}>
       <i className="icon-pencil mr-2"></i>
       <span>{ t('New') }</span>
-    </a>
+    </button>
   );
 };
 

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

@@ -65,8 +65,8 @@ const PersonalDropdown = (props) => {
       {/* Menu */}
       <div className="dropdown-menu dropdown-menu-right">
 
-        <a className="dropdown-item" type="button" href={`/user/${user.username}`}><i className="icon-fw icon-user"></i>{ t('User\'s Home') }</a>
-        <a className="dropdown-item" type="button" href="/me"><i className="icon-fw icon-wrench"></i>{ t('User Settings') }</a>
+        <a className="dropdown-item" href={`/user/${user.username}`}><i className="icon-fw icon-user"></i>{ t('User\'s Home') }</a>
+        <a className="dropdown-item" href="/me"><i className="icon-fw icon-wrench"></i>{ t('User Settings') }</a>
 
         <div className="dropdown-divider"></div>
 
@@ -107,7 +107,7 @@ const PersonalDropdown = (props) => {
 
         <div className="dropdown-divider"></div>
 
-        <a className="dropdown-item" type="button" onClick={logoutHandler}><i className="icon-fw icon-power"></i>{ t('Sign out') }</a>
+        <a className="dropdown-item" onClick={logoutHandler}><i className="icon-fw icon-power"></i>{ t('Sign out') }</a>
       </div>
 
     </>

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

@@ -60,12 +60,12 @@ const PageManagement = (props) => {
   function renderDropdownItemForNotTopPage() {
     return (
       <>
-        <a className="dropdown-item" type="button" onClick={openPageRenameModalHandler}>
+        <button className="dropdown-item" type="button" onClick={openPageRenameModalHandler}>
           <i className="icon-fw icon-action-redo"></i> { t('Move/Rename') }
-        </a>
-        <a className="dropdown-item" type="button" onClick={openPageDuplicateModalHandler}>
+        </button>
+        <button className="dropdown-item" type="button" onClick={openPageDuplicateModalHandler}>
           <i className="icon-fw icon-docs"></i> { t('Duplicate') }
-        </a>
+        </button>
         <div className="dropdown-divider"></div>
       </>
     );
@@ -75,9 +75,9 @@ const PageManagement = (props) => {
     return (
       <>
         <div className="dropdown-divider"></div>
-        <a className="dropdown-item" type="button" onClick={openPageDeleteModalHandler}>
+        <button className="dropdown-item" type="button" onClick={openPageDeleteModalHandler}>
           <i className="icon-fw icon-fire text-danger"></i> { t('Delete') }
-        </a>
+        </button>
       </>
     );
   }
@@ -123,9 +123,9 @@ const PageManagement = (props) => {
       </a>
       <div className="dropdown-menu dropdown-menu-right">
         {!isTopPagePath && renderDropdownItemForNotTopPage()}
-        <a className="dropdown-item" type="button" onClick={openPageTemplateModalHandler}>
+        <button className="dropdown-item" type="button" onClick={openPageTemplateModalHandler}>
           <i className="icon-fw icon-magic-wand"></i> { t('template.option_label.create/edit') }
-        </a>
+        </button>
         {(!isTopPagePath && isDeletable) && renderDropdownItemForDeletablePage()}
       </div>
       {renderModals()}

+ 9 - 16
src/client/js/components/PageComment/Comment.jsx

@@ -35,17 +35,13 @@ class Comment extends React.PureComponent {
       isReEdit: false,
     };
 
-    this.growiRenderer = this.props.appContainer.getRenderer('comment');
-
     this.isCurrentUserIsAuthor = this.isCurrentUserEqualsToAuthor.bind(this);
     this.isCurrentRevision = this.isCurrentRevision.bind(this);
     this.getRootClassName = this.getRootClassName.bind(this);
     this.getRevisionLabelClassName = this.getRevisionLabelClassName.bind(this);
-    this.editBtnClickedHandler = this.editBtnClickedHandler.bind(this);
     this.deleteBtnClickedHandler = this.deleteBtnClickedHandler.bind(this);
     this.renderText = this.renderText.bind(this);
     this.renderHtml = this.renderHtml.bind(this);
-    this.commentButtonClickedHandler = this.commentButtonClickedHandler.bind(this);
   }
 
 
@@ -114,14 +110,6 @@ class Comment extends React.PureComponent {
       this.isCurrentRevision() ? 'badge-primary' : 'badge-secondary'}`;
   }
 
-  editBtnClickedHandler() {
-    this.setState({ isReEdit: !this.state.isReEdit });
-  }
-
-  commentButtonClickedHandler() {
-    this.editBtnClickedHandler();
-  }
-
   deleteBtnClickedHandler() {
     this.props.deleteBtnClicked(this.props.comment);
   }
@@ -187,12 +175,13 @@ class Comment extends React.PureComponent {
 
         {this.state.isReEdit ? (
           <CommentEditor
-            growiRenderer={this.growiRenderer}
+            growiRenderer={this.props.growiRenderer}
             currentCommentId={commentId}
             commentBody={comment.comment}
             replyTo={undefined}
-            commentButtonClickedHandler={this.commentButtonClickedHandler}
             commentCreator={creator.username}
+            onCancelButtonClicked={() => this.setState({ isReEdit: false })}
+            onCommentButtonClicked={() => this.setState({ isReEdit: false })}
           />
         ) : (
           <div id={commentId} className={rootClassName}>
@@ -216,8 +205,12 @@ class Comment extends React.PureComponent {
                 ) }
                 <span className="ml-2"><a className={revisionLavelClassName} href={revHref}>{revFirst8Letters}</a></span>
               </div>
-              {this.checkPermissionToControlComment()
-                  && <CommentControl onClickDeleteBtn={this.deleteBtnClickedHandler} onClickEditBtn={this.editBtnClickedHandler} />}
+              { this.checkPermissionToControlComment() && (
+                <CommentControl
+                  onClickDeleteBtn={this.deleteBtnClickedHandler}
+                  onClickEditBtn={() => this.setState({ isReEdit: true })}
+                />
+              ) }
             </div>
           </div>
           )

+ 174 - 116
src/client/js/components/PageComment/CommentEditor.jsx

@@ -38,6 +38,7 @@ class CommentEditor extends React.Component {
     const isUploadableFile = config.upload.file;
 
     this.state = {
+      isReadyToUse: !this.props.isForNewComment,
       comment: this.props.commentBody || '',
       isMarkdown: true,
       html: '',
@@ -51,14 +52,16 @@ class CommentEditor extends React.Component {
     this.updateState = this.updateState.bind(this);
     this.updateStateCheckbox = this.updateStateCheckbox.bind(this);
 
-    this.postHandler = this.postHandler.bind(this);
+    this.cancelButtonClickedHandler = this.cancelButtonClickedHandler.bind(this);
+    this.commentButtonClickedHandler = this.commentButtonClickedHandler.bind(this);
+    this.ctrlEnterHandler = this.ctrlEnterHandler.bind(this);
+    this.postComment = this.postComment.bind(this);
     this.uploadHandler = this.uploadHandler.bind(this);
 
     this.renderHtml = this.renderHtml.bind(this);
     this.handleSelect = this.handleSelect.bind(this);
     this.onSlackEnabledFlagChange = this.onSlackEnabledFlagChange.bind(this);
     this.onSlackChannelsChange = this.onSlackChannelsChange.bind(this);
-    this.toggleEditor = this.toggleEditor.bind(this);
   }
 
   updateState(value) {
@@ -85,11 +88,6 @@ class CommentEditor extends React.Component {
     this.props.commentContainer.setState({ slackChannels });
   }
 
-  toggleEditor() {
-    const targetId = this.props.replyTo || this.props.currentCommentId;
-    this.props.commentButtonClickedHandler(targetId);
-  }
-
   initializeEditor() {
     this.setState({
       comment: '',
@@ -100,36 +98,65 @@ class CommentEditor extends React.Component {
     });
     // reset value
     this.editor.setValue('');
-    this.toggleEditor();
   }
 
-  /**
-   * Post comment with CommentContainer and update state
-   */
-  async postHandler(event) {
+  cancelButtonClickedHandler() {
+    const { isForNewComment, onCancelButtonClicked } = this.props;
+
+    // change state to not ready
+    // when this editor is for the new comment mode
+    if (isForNewComment) {
+      this.setState({ isReadyToUse: false });
+    }
+
+    if (onCancelButtonClicked != null) {
+      const { replyTo, currentCommentId } = this.props;
+      onCancelButtonClicked(replyTo || currentCommentId);
+    }
+  }
+
+  commentButtonClickedHandler() {
+    this.postComment();
+  }
+
+  ctrlEnterHandler(event) {
     if (event != null) {
       event.preventDefault();
     }
 
+    this.postComment();
+  }
+
+  /**
+   * Post comment with CommentContainer and update state
+   */
+  async postComment() {
+    const {
+      commentContainer, replyTo, currentCommentId, commentCreator, onCommentButtonClicked,
+    } = this.props;
     try {
-      if (this.props.currentCommentId != null) {
-        await this.props.commentContainer.putComment(
+      if (currentCommentId != null) {
+        await commentContainer.putComment(
           this.state.comment,
           this.state.isMarkdown,
-          this.props.currentCommentId,
-          this.props.commentCreator,
+          currentCommentId,
+          commentCreator,
         );
       }
       else {
         await this.props.commentContainer.postComment(
           this.state.comment,
           this.state.isMarkdown,
-          this.props.replyTo,
-          this.props.commentContainer.state.isSlackEnabled,
-          this.props.commentContainer.state.slackChannels,
+          replyTo,
+          commentContainer.state.isSlackEnabled,
+          commentContainer.state.slackChannels,
         );
       }
       this.initializeEditor();
+
+      if (onCommentButtonClicked != null) {
+        onCommentButtonClicked(replyTo || currentCommentId);
+      }
     }
     catch (err) {
       const errorMessage = err.message || 'An unknown error occured when posting comment';
@@ -212,7 +239,21 @@ class CommentEditor extends React.Component {
     return { __html: html };
   }
 
-  render() {
+  renderBeforeReady() {
+    return (
+      <div className="text-center">
+        <button
+          type="button"
+          className="btn btn-lg btn-link"
+          onClick={() => this.setState({ isReadyToUse: true })}
+        >
+          <i className="icon-bubble"></i> Add Comment
+        </button>
+      </div>
+    );
+  }
+
+  renderReady() {
     const { appContainer, commentContainer } = this.props;
     const { activeTab } = this.state;
 
@@ -221,7 +262,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="btn btn-outline-danger rounded-pill" onClick={this.toggleEditor}>
+      <Button outline color="danger" size="xs" className="btn btn-outline-danger rounded-pill" onClick={this.cancelButtonClickedHandler}>
         Cancel
       </Button>
     );
@@ -230,120 +271,128 @@ class CommentEditor extends React.Component {
         outline
         color="primary"
         className="btn btn-outline-primary rounded-pill"
-        onClick={this.postHandler}
+        onClick={this.commentButtonClickedHandler}
       >
         Comment
       </Button>
     );
 
     return (
-      <div className="form page-comment-form">
-        <div className="comment-form">
-          <div className="comment-form-user">
-            <UserPicture user={appContainer.currentUser} />
-          </div>
-          <div className="comment-form-main">
-            <div className="comment-write">
-              <Nav tabs>
-                <NavItem>
-                  <NavLink type="button" className={activeTab === 1 ? 'active' : ''} onClick={() => this.handleSelect(1)}>
+      <>
+        <div className="comment-write">
+          <Nav tabs>
+            <NavItem>
+              <NavLink type="button" className={activeTab === 1 ? 'active' : ''} onClick={() => this.handleSelect(1)}>
                     Write
-                  </NavLink>
-                </NavItem>
-                { this.state.isMarkdown && (
-                  <NavItem>
-                    <NavLink type="button" className={activeTab === 2 ? 'active' : ''} onClick={() => this.handleSelect(2)}>
+              </NavLink>
+            </NavItem>
+            { this.state.isMarkdown && (
+            <NavItem>
+              <NavLink type="button" className={activeTab === 2 ? 'active' : ''} onClick={() => this.handleSelect(2)}>
                       Preview
-                    </NavLink>
-                  </NavItem>
+              </NavLink>
+            </NavItem>
                 ) }
-              </Nav>
-              <TabContent activeTab={activeTab}>
-                <TabPane tabId={1}>
-                  <Editor
-                    ref={(c) => { this.editor = c }}
-                    value={this.state.comment}
-                    isGfmMode={this.state.isMarkdown}
-                    lineNumbers={false}
-                    isMobile={appContainer.isMobile}
-                    isUploadable={this.state.isUploadable}
-                    isUploadableFile={this.state.isUploadableFile}
-                    emojiStrategy={emojiStrategy}
-                    onChange={this.updateState}
-                    onUpload={this.uploadHandler}
-                    onCtrlEnter={this.postHandler}
-                  />
-                </TabPane>
-                <TabPane tabId={2}>
-                  <div className="comment-form-preview">
-                    {commentPreview}
-                  </div>
-                </TabPane>
-              </TabContent>
-            </div>
-            <div className="comment-submit">
-              <div className="d-flex">
-                <label className="mr-2">
-                  {activeTab === 1 && (
-                    <span className="custom-control custom-checkbox">
-                      <input
-                        type="checkbox"
-                        className="custom-control-input"
-                        id="comment-form-is-markdown"
-                        name="isMarkdown"
-                        checked={this.state.isMarkdown}
-                        value="1"
-                        onChange={this.updateStateCheckbox}
-                      />
-                      <label
-                        className="ml-2 custom-control-label"
-                        htmlFor="comment-form-is-markdown"
-                      >
-                        Markdown
-                      </label>
-                    </span>
-                  ) }
-                </label>
-                <span className="flex-grow-1" />
-                <span className="d-none d-sm-inline">{ this.state.errorMessage && errorMessage }</span>
-                { this.state.hasSlackConfig
-                  && (
-                  <div className="form-inline align-self-center mr-md-2">
-                    <SlackNotification
-                      isSlackEnabled={commentContainer.state.isSlackEnabled}
-                      slackChannels={commentContainer.state.slackChannels}
-                      onEnabledFlagChange={this.onSlackEnabledFlagChange}
-                      onChannelChange={this.onSlackChannelsChange}
-                    />
-                  </div>
-                  )
-                }
-                <div className="d-none d-sm-block">
-                  <span className="mr-2">{cancelButton}</span><span>{submitButton}</span>
-                </div>
+          </Nav>
+          <TabContent activeTab={activeTab}>
+            <TabPane tabId={1}>
+              <Editor
+                ref={(c) => { this.editor = c }}
+                value={this.state.comment}
+                isGfmMode={this.state.isMarkdown}
+                lineNumbers={false}
+                isMobile={appContainer.isMobile}
+                isUploadable={this.state.isUploadable}
+                isUploadableFile={this.state.isUploadableFile}
+                emojiStrategy={emojiStrategy}
+                onChange={this.updateState}
+                onUpload={this.uploadHandler}
+                onCtrlEnter={this.ctrlEnterHandler}
+              />
+            </TabPane>
+            <TabPane tabId={2}>
+              <div className="comment-form-preview">
+                {commentPreview}
               </div>
-              <div className="d-block d-sm-none mt-2">
-                <div className="d-flex justify-content-end">
-                  { this.state.errorMessage && errorMessage }
-                  <span className="mr-2">{cancelButton}</span><span>{submitButton}</span>
-                </div>
+            </TabPane>
+          </TabContent>
+        </div>
+
+        <div className="comment-submit">
+          <div className="d-flex">
+            <label className="mr-2">
+              {activeTab === 1 && (
+              <span className="custom-control custom-checkbox">
+                <input
+                  type="checkbox"
+                  className="custom-control-input"
+                  id="comment-form-is-markdown"
+                  name="isMarkdown"
+                  checked={this.state.isMarkdown}
+                  value="1"
+                  onChange={this.updateStateCheckbox}
+                />
+                <label
+                  className="ml-2 custom-control-label"
+                  htmlFor="comment-form-is-markdown"
+                >
+                  Markdown
+                </label>
+              </span>
+                  ) }
+            </label>
+            <span className="flex-grow-1" />
+            <span className="d-none d-sm-inline">{ this.state.errorMessage && errorMessage }</span>
+            { this.state.hasSlackConfig
+              && (
+              <div className="form-inline align-self-center mr-md-2">
+                <SlackNotification
+                  isSlackEnabled={commentContainer.state.isSlackEnabled}
+                  slackChannels={commentContainer.state.slackChannels}
+                  onEnabledFlagChange={this.onSlackEnabledFlagChange}
+                  onChannelChange={this.onSlackChannelsChange}
+                />
               </div>
+              )
+            }
+            <div className="d-none d-sm-block">
+              <span className="mr-2">{cancelButton}</span><span>{submitButton}</span>
+            </div>
+          </div>
+          <div className="d-block d-sm-none mt-2">
+            <div className="d-flex justify-content-end">
+              { this.state.errorMessage && errorMessage }
+              <span className="mr-2">{cancelButton}</span><span>{submitButton}</span>
             </div>
           </div>
         </div>
+      </>
+    );
+  }
+
+  render() {
+    const { appContainer } = this.props;
+    const { isReadyToUse } = this.state;
+
+    return (
+      <div className="form page-comment-form">
+        <div className="comment-form">
+          <div className="comment-form-user">
+            <UserPicture user={appContainer.currentUser} noLink noTooltip />
+          </div>
+          <div className="comment-form-main">
+            { !isReadyToUse
+              ? this.renderBeforeReady()
+              : this.renderReady()
+            }
+          </div>
+        </div>
       </div>
     );
   }
 
 }
 
-/**
- * Wrapper component for using unstated
- */
-const CommentEditorWrapper = (props) => {
-  return createSubscribedElement(CommentEditor, props, [AppContainer, PageContainer, EditorContainer, CommentContainer]);
-};
-
 CommentEditor.propTypes = {
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
   pageContainer: PropTypes.instanceOf(PageContainer).isRequired,
@@ -351,11 +400,20 @@ CommentEditor.propTypes = {
   commentContainer: PropTypes.instanceOf(CommentContainer).isRequired,
 
   growiRenderer: PropTypes.instanceOf(GrowiRenderer).isRequired,
+  isForNewComment: PropTypes.bool,
   replyTo: PropTypes.string,
   currentCommentId: PropTypes.string,
   commentBody: PropTypes.string,
   commentCreator: PropTypes.string,
-  commentButtonClickedHandler: PropTypes.func.isRequired,
+  onCancelButtonClicked: PropTypes.func,
+  onCommentButtonClicked: PropTypes.func,
+};
+
+/**
+ * Wrapper component for using unstated
+ */
+const CommentEditorWrapper = (props) => {
+  return createSubscribedElement(CommentEditor, props, [AppContainer, PageContainer, EditorContainer, CommentContainer]);
 };
 
 export default CommentEditorWrapper;

+ 10 - 64
src/client/js/components/PageComment/CommentEditorLazyRenderer.jsx

@@ -3,75 +3,21 @@ import PropTypes from 'prop-types';
 
 import { createSubscribedElement } from '../UnstatedUtils';
 import AppContainer from '../../services/AppContainer';
-import UserPicture from '../User/UserPicture';
 
 import CommentEditor from './CommentEditor';
 
-class CommentEditorLazyRenderer extends React.Component {
+const CommentEditorLazyRenderer = (props) => {
 
-  constructor(props) {
-    super(props);
+  const growiRenderer = props.appContainer.getRenderer('comment');
 
-    this.state = {
-      isEditorShown: false,
-    };
-
-    this.growiRenderer = this.props.appContainer.getRenderer('comment');
-
-    this.showCommentFormBtnClickHandler = this.showCommentFormBtnClickHandler.bind(this);
-  }
-
-  showCommentFormBtnClickHandler() {
-    this.setState({ isEditorShown: !this.state.isEditorShown });
-  }
-
-  render() {
-    const { appContainer } = this.props;
-    const user = appContainer.currentUser;
-    const isLoggedIn = user != null;
-
-    if (!isLoggedIn) {
-      return <React.Fragment></React.Fragment>;
-    }
-
-    return (
-      <React.Fragment>
-
-        { !this.state.isEditorShown && (
-          <div className="form page-comment-form">
-            <div className="comment-form">
-              <div className="comment-form-user">
-                <UserPicture user={user} />
-              </div>
-              <div className="comment-form-main">
-                { !this.state.isEditorShown && (
-                  <button
-                    type="button"
-                    className="btn btn-lg btn-link center-block"
-                    onClick={this.showCommentFormBtnClickHandler}
-                  >
-                    <i className="icon-bubble"></i> Add Comment
-                  </button>
-                ) }
-              </div>
-            </div>
-          </div>
-        ) }
-
-        { this.state.isEditorShown && (
-          <CommentEditor
-            growiRenderer={this.growiRenderer}
-            replyTo={undefined}
-            commentButtonClickedHandler={this.showCommentFormBtnClickHandler}
-          >
-          </CommentEditor>
-        ) }
-
-      </React.Fragment>
-    );
-  }
-
-}
+  return (
+    <CommentEditor
+      growiRenderer={growiRenderer}
+      replyTo={undefined}
+      isForNewComment
+    />
+  );
+};
 
 /**
  * Wrapper component for using unstated

+ 9 - 9
src/client/js/components/PageComment/ReplayComments.jsx

@@ -1,7 +1,7 @@
 import React from 'react';
 import PropTypes from 'prop-types';
 
-import { Button, Collapse } from 'reactstrap';
+import { Collapse } from 'reactstrap';
 
 import AppContainer from '../../services/AppContainer';
 import PageContainer from '../../services/PageContainer';
@@ -19,10 +19,10 @@ class ReplayComments extends React.PureComponent {
       isOlderRepliesShown: false,
     };
 
-    this.toggleIsOlderRepliesShown = this.toggleIsOlderRepliesShown.bind(this);
+    this.toggleOlderReplies = this.toggleOlderReplies.bind(this);
   }
 
-  toggleIsOlderRepliesShown() {
+  toggleOlderReplies() {
     this.setState({ isOlderRepliesShown: !this.state.isOlderRepliesShown });
   }
 
@@ -75,17 +75,17 @@ class ReplayComments extends React.PureComponent {
       <React.Fragment>
         {areThereHiddenReplies && (
           <div className="page-comments-hidden-replies">
-            <Collapse in={this.state.isOlderRepliesShown}>
+            <Collapse isOpen={this.state.isOlderRepliesShown}>
               <div>{hiddenElements}</div>
             </Collapse>
             <div className="text-center">
-              <Button
-                bsStyle="link"
-                className="page-comments-list-toggle-older"
-                onClick={this.toggleIsOlderRepliesShown}
+              <button
+                type="button"
+                className="btn btn-link"
+                onClick={this.toggleOlderReplies}
               >
                 {toggleButtonIcon} {toggleButtonLabel}
-              </Button>
+              </button>
             </div>
           </div>
         )}

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

@@ -50,7 +50,9 @@ class PageComments extends React.Component {
     this.showDeleteConfirmModal = this.showDeleteConfirmModal.bind(this);
     this.closeDeleteConfirmModal = this.closeDeleteConfirmModal.bind(this);
     this.replyButtonClickedHandler = this.replyButtonClickedHandler.bind(this);
-    this.commentButtonClickedHandler = this.commentButtonClickedHandler.bind(this);
+    this.editorCancelHandler = this.editorCancelHandler.bind(this);
+    this.editorCommentHandler = this.editorCommentHandler.bind(this);
+    this.resetEditor = this.resetEditor.bind(this);
   }
 
   componentWillMount() {
@@ -99,7 +101,15 @@ class PageComments extends React.Component {
     this.setState({ showEditorIds: ids });
   }
 
-  commentButtonClickedHandler(commentId) {
+  editorCancelHandler(commentId) {
+    this.resetEditor(commentId);
+  }
+
+  editorCommentHandler(commentId) {
+    this.resetEditor(commentId);
+  }
+
+  resetEditor(commentId) {
     this.setState((prevState) => {
       prevState.showEditorIds.delete(commentId);
       return {
@@ -169,7 +179,8 @@ class PageComments extends React.Component {
             <CommentEditor
               growiRenderer={this.growiRenderer}
               replyTo={commentId}
-              commentButtonClickedHandler={this.commentButtonClickedHandler}
+              onCancelButtonClicked={this.editorCancelHandler}
+              onCommentButtonClicked={this.editorCommentHandler}
             />
           </div>
         )}
@@ -180,7 +191,8 @@ class PageComments extends React.Component {
   render() {
     const topLevelComments = [];
     const allReplies = [];
-    const comments = this.props.commentContainer.state.comments;
+    const comments = this.props.commentContainer.state.comments
+      .slice().reverse(); // create shallow copy and reverse
 
     comments.forEach((comment) => {
       if (comment.replyTo === undefined) {

+ 44 - 31
src/client/js/components/PageCreateModal.jsx

@@ -20,7 +20,7 @@ const PageCreateModal = (props) => {
 
   const config = appContainer.getConfig();
   const isReachable = config.isSearchServiceReachable;
-  const { pathname } = window.location;
+  const pathname = decodeURI(window.location.pathname);
   const userPageRootPath = userPageRoot(appContainer.currentUser);
   const parentPath = pathUtils.addTrailingSlash(pathname);
   const now = format(new Date(), 'yyyy/MM/dd');
@@ -30,6 +30,12 @@ const PageCreateModal = (props) => {
   const [pageNameInput, setPageNameInput] = useState(parentPath);
   const [template, setTemplate] = useState(null);
 
+  function transitBySubmitEvent(e, transitHandler) {
+    // prevent page transition by submit
+    e.preventDefault();
+    transitHandler();
+  }
+
   /**
    * change todayInput1
    * @param {string} value
@@ -91,9 +97,9 @@ const PageCreateModal = (props) => {
   /**
    * access template page
    */
-  function createTemplatePage() {
+  function createTemplatePage(e) {
     const pageName = (template === 'children') ? '_template' : '__template';
-    window.location.href = encodeURI(urljoin(parentPath, pageName, '#edit'));
+    window.location.href = encodeURI(urljoin(pathname, pageName, '#edit'));
   }
 
   function renderCreateTodayForm() {
@@ -107,22 +113,26 @@ const PageCreateModal = (props) => {
             <div className="d-flex align-items-center flex-fill flex-wrap flex-lg-nowrap">
               <div className="d-flex align-items-center">
                 <span>{userPageRootPath}/</span>
+                <form onSubmit={e => transitBySubmitEvent(e, createTodayPage)}>
+                  <input
+                    type="text"
+                    className="page-today-input1 form-control text-center mx-2"
+                    value={todayInput1}
+                    onChange={e => onChangeTodayInput1Handler(e.target.value)}
+                  />
+                </form>
+                <span className="page-today-suffix">/{now}/</span>
+              </div>
+              <form className="mt-1 mt-lg-0 ml-lg-2 w-100" onSubmit={e => transitBySubmitEvent(e, createTodayPage)}>
                 <input
                   type="text"
-                  className="page-today-input1 form-control text-center mx-2"
-                  value={todayInput1}
-                  onChange={e => onChangeTodayInput1Handler(e.target.value)}
+                  className="page-today-input2 form-control w-100"
+                  id="page-today-input2"
+                  placeholder={t('Input page name (optional)')}
+                  value={todayInput2}
+                  onChange={e => onChangeTodayInput2Handler(e.target.value)}
                 />
-                <span className="page-today-suffix">/{now}/</span>
-              </div>
-              <input
-                type="text"
-                className="page-today-input2 form-control mt-1 mt-lg-0 mx-lg-2 flex-fill"
-                id="page-today-input2"
-                placeholder={t('Input page name (optional)')}
-                value={todayInput2}
-                onChange={e => onChangeTodayInput2Handler(e.target.value)}
-              />
+              </form>
             </div>
 
             <div className="d-flex justify-content-end mt-1 mt-sm-0">
@@ -151,21 +161,23 @@ const PageCreateModal = (props) => {
                 ? (
                   <PagePathAutoComplete
                     crowi={appContainer}
-                    initializedPath={decodeURI(pathname)}
+                    initializedPath={pathname}
                     addTrailingSlash
                     onSubmit={ppacSubmitHandler}
                     onInputChange={ppacInputChangeHandler}
                   />
                 )
                 : (
-                  <input
-                    type="text"
-                    value={pageNameInput}
-                    className="form-control flex-fill"
-                    placeholder={t('Input page name')}
-                    onChange={e => onChangePageNameInputHandler(e.target.value)}
-                    required
-                  />
+                  <form onSubmit={e => transitBySubmitEvent(e, createInputPage)}>
+                    <input
+                      type="text"
+                      value={pageNameInput}
+                      className="form-control flex-fill"
+                      placeholder={t('Input page name')}
+                      onChange={e => onChangePageNameInputHandler(e.target.value)}
+                      required
+                    />
+                  </form>
                 )}
             </div>
 
@@ -187,8 +199,9 @@ const PageCreateModal = (props) => {
       <div className="row">
         <fieldset className="col-12">
 
-          <h3 className="grw-modal-head pb-2">{ t('template.modal_label.Create template under')}<br />
-            <code>{decodeURI(pathname)}</code>
+          <h3 className="grw-modal-head pb-2">
+            { t('template.modal_label.Create template under')}<br />
+            <code className="h6">{pathname}</code>
           </h3>
 
           <div className="d-sm-flex align-items-center justify-items-between">
@@ -200,14 +213,14 @@ const PageCreateModal = (props) => {
                 {template === 'decendants' && t('template.decendants.label')}
               </button>
               <div className="dropdown-menu" aria-labelledby="userMenu">
-                <a className="dropdown-item" type="button" onClick={() => onChangeTemplateHandler('children')}>
+                <button className="dropdown-item" type="button" onClick={() => onChangeTemplateHandler('children')}>
                   { t('template.children.label') } (_template)<br className="d-block d-md-none" />
                   <small className="text-muted text-wrap">- { t('template.children.desc') }</small>
-                </a>
-                <a className="dropdown-item" type="button" onClick={() => onChangeTemplateHandler('decendants')}>
+                </button>
+                <button className="dropdown-item" type="button" onClick={() => onChangeTemplateHandler('decendants')}>
                   { t('template.decendants.label') } (__template) <br className="d-block d-md-none" />
                   <small className="text-muted">- { t('template.decendants.desc') }</small>
-                </a>
+                </button>
               </div>
             </div>
 

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

@@ -110,7 +110,7 @@ const PageDeleteModal = (props) => {
   }
 
   return (
-    <Modal isOpen={isOpen} toggle={onClose} className="grw-create-page">
+    <Modal size="lg" isOpen={isOpen} toggle={onClose} className="grw-create-page">
       <ModalHeader tag="h4" toggle={onClose} className={`bg-${deleteIconAndKey[deleteMode].color} text-light`}>
         <i className={`icon-fw icon-${deleteIconAndKey[deleteMode].icon}`}></i>
         { t(`modal_delete.delete_${deleteIconAndKey[deleteMode].translationKey}`) }

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

@@ -61,7 +61,7 @@ const PageDuplicateModal = (props) => {
   }
 
   return (
-    <Modal isOpen={props.isOpen} toggle={props.onClose} className="grw-duplicate-page">
+    <Modal size="lg" isOpen={props.isOpen} toggle={props.onClose} className="grw-duplicate-page">
       <ModalHeader tag="h4" toggle={props.onClose} className="bg-primary text-light">
         { t('modal_duplicate.label.Duplicate page') }
       </ModalHeader>

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

@@ -289,7 +289,7 @@ class PageEditor extends React.Component {
 
     return (
       <div className="d-flex flex-wrap">
-        <div className="page-editor-editor-container" style={{ flex: 1 }}>
+        <div className="page-editor-editor-container flex-grow-1 flex-basis-0 mw-0">
           <Editor
             ref={(c) => { this.editor = c }}
             value={this.state.markdown}
@@ -305,7 +305,7 @@ class PageEditor extends React.Component {
             onSave={this.onSaveWithShortcut}
           />
         </div>
-        <div className="d-none d-xl-block page-editor-preview-container" style={{ flex: 1 }}>
+        <div className="d-none d-xl-block page-editor-preview-container flex-grow-1 flex-basis-0 mw-0">
           <Preview
             markdown={this.state.markdown}
             // eslint-disable-next-line no-return-assign

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

@@ -437,7 +437,7 @@ export default class HandsontableModal extends React.PureComponent {
           Edit Table
         </ModalHeader>
         <ModalBody className="p-0 d-flex flex-column">
-          <div className="px-4 py-3 border-bottom bg-light">
+          <div className="grw-hot-modal-navbar px-4 py-3 border-bottom">
             <button
               type="button"
               className="mr-4 data-import-button btn btn-secondary"

+ 8 - 4
src/client/js/components/PageEditor/TextAreaEditor.jsx

@@ -3,6 +3,8 @@ import React from 'react';
 
 import InterceptorManager from '@commons/service/interceptor-manager';
 
+import { Input } from 'reactstrap';
+
 import AbstractEditor from './AbstractEditor';
 
 import pasteHelper from './PasteHelper';
@@ -21,6 +23,8 @@ export default class TextAreaEditor extends AbstractEditor {
       isGfmMode: this.props.isGfmMode,
     };
 
+    this.textarea = React.createRef();
+
     this.init();
 
     this.handleEnterKey = this.handleEnterKey.bind(this);
@@ -249,10 +253,10 @@ export default class TextAreaEditor extends AbstractEditor {
   render() {
     return (
       <React.Fragment>
-        <input
-          componentClass="textarea"
-          className="textarea-editor"
-          inputRef={(ref) => { this.textarea = ref }}
+        <Input
+          type="textarea"
+          className="textarea-editor shadow-none"
+          innerRef={(c) => { this.textarea = c }}
           defaultValue={this.state.value}
           onChange={(e) => {
           if (this.props.onChange != null) {

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

@@ -61,8 +61,15 @@ const PageRenameModal = (props) => {
         isRenameRedirect,
         isRenameMetadata,
       );
+
       const { page } = response;
-      window.location.href = encodeURI(`${page.path}?renamed=${path}`);
+      const url = new URL(page.path, 'https://dummy');
+      url.searchParams.append('renamedFrom', path);
+      if (isRenameRedirect) {
+        url.searchParams.append('withRedirect', true);
+      }
+
+      window.location.href = `${url.pathname}${url.search}`;
     }
     catch (err) {
       setErrorCode(err.code);
@@ -71,7 +78,7 @@ const PageRenameModal = (props) => {
   }
 
   return (
-    <Modal isOpen={props.isOpen} toggle={props.onClose} className="grw-create-page">
+    <Modal size="lg" isOpen={props.isOpen} toggle={props.onClose} className="grw-create-page">
       <ModalHeader tag="h4" toggle={props.onClose} className="bg-primary text-light">
         { t('modal_rename.label.Move/Rename page') }
       </ModalHeader>
@@ -86,7 +93,7 @@ const PageRenameModal = (props) => {
             <div className="input-group-prepend">
               <span className="input-group-text">{crowi.url}</span>
             </div>
-            <div className="flex-fill">
+            <form className="flex-fill" onSubmit={(e) => { e.preventDefault(); rename() }}>
               <input
                 type="text"
                 value={pageNameInput}
@@ -94,7 +101,7 @@ const PageRenameModal = (props) => {
                 onChange={e => inputChangeHandler(e.target.value)}
                 required
               />
-            </div>
+            </form>
           </div>
         </div>
         <div className="custom-control custom-checkbox custom-checkbox-warning">

+ 7 - 7
src/client/js/components/SearchForm.jsx

@@ -62,37 +62,37 @@ class SearchForm extends React.Component {
         </caption>
         <tbody>
           <tr>
-            <th className="text-right py-2">
+            <th className="py-2">
               <code>word1</code> <code>word2</code><br></br>
               <small>({ t('search_help.and.syntax help') })</small>
             </th>
             <td><h6 className="m-0">{ t('search_help.and.desc', { word1: 'word1', word2: 'word2' }) }</h6></td>
           </tr>
           <tr>
-            <th className="text-right py-2">
+            <th className="py-2">
               <code>&quot;This is GROWI&quot;</code><br></br>
               <small>({ t('search_help.phrase.syntax help') })</small>
             </th>
             <td><h6 className="m-0">{ t('search_help.phrase.desc', { phrase: 'This is GROWI' }) }</h6></td>
           </tr>
           <tr>
-            <th className="text-right py-2"><code>-keyword</code></th>
+            <th className="py-2"><code>-keyword</code></th>
             <td><h6 className="m-0">{ t('search_help.exclude.desc', { word: 'keyword' }) }</h6></td>
           </tr>
           <tr>
-            <th className="text-right py-2"><code>prefix:/user/</code></th>
+            <th className="py-2"><code>prefix:/user/</code></th>
             <td><h6 className="m-0">{ t('search_help.prefix.desc', { path: '/user/' }) }</h6></td>
           </tr>
           <tr>
-            <th className="text-right py-2"><code>-prefix:/user/</code></th>
+            <th className="py-2"><code>-prefix:/user/</code></th>
             <td><h6 className="m-0">{ t('search_help.exclude_prefix.desc', { path: '/user/' }) }</h6></td>
           </tr>
           <tr>
-            <th className="text-right py-2"><code>tag:wiki</code></th>
+            <th className="py-2"><code>tag:wiki</code></th>
             <td><h6 className="m-0">{ t('search_help.tag.desc', { tag: 'wiki' }) }</h6></td>
           </tr>
           <tr>
-            <th className="text-right py-2"><code>-tag:wiki</code></th>
+            <th className="py-2"><code>-tag:wiki</code></th>
             <td><h6 className="m-0">{ t('search_help.exclude_tag.desc', { tag: 'wiki' }) }</h6></td>
           </tr>
         </tbody>

+ 19 - 5
src/client/js/components/Sidebar.jsx

@@ -1,8 +1,6 @@
 import React from 'react';
 import PropTypes from 'prop-types';
 
-import { withTranslation } from 'react-i18next';
-
 import {
   withNavigationUIController,
   LayoutManager,
@@ -32,9 +30,26 @@ class Sidebar extends React.Component {
   };
 
   componentWillMount() {
+    this.hackUIController();
     this.initBreakpointEvents();
   }
 
+  /**
+   * hack and override UIController.storeState
+   *
+   * Since UIController is an unstated container, setState() in storeState method should be awaited before writing to cache.
+   */
+  hackUIController() {
+    const { navigationUIController } = this.props;
+
+    // see: @atlaskit/navigation-next/dist/esm/ui-controller/UIController.js
+    const orgStoreState = navigationUIController.storeState;
+    navigationUIController.storeState = async(state) => {
+      await navigationUIController.setState(state);
+      orgStoreState(state);
+    };
+  }
+
   initBreakpointEvents() {
     const { appContainer, navigationUIController } = this.props;
 
@@ -99,7 +114,7 @@ class Sidebar extends React.Component {
         break;
     }
 
-    return contents;
+    return <div className="grw-sidebar-content-container">{contents}</div>;
   }
 
   render() {
@@ -139,13 +154,12 @@ class Sidebar extends React.Component {
 }
 
 const SidebarWithNavigationUI = withNavigationUIController(Sidebar);
-const SidebarWithNavigationUIAndTranslation = withTranslation()(SidebarWithNavigationUI);
 
 /**
  * Wrapper component for using unstated
  */
 const SidebarWrapper = (props) => {
-  return createSubscribedElement(SidebarWithNavigationUIAndTranslation, props, [AppContainer]);
+  return createSubscribedElement(SidebarWithNavigationUI, props, [AppContainer]);
 };
 
 export default () => (

+ 11 - 24
src/client/js/components/Sidebar/CustomSidebar.jsx

@@ -3,11 +3,6 @@ import React from 'react';
 
 import { withTranslation } from 'react-i18next';
 
-import {
-  HeaderSection,
-  MenuSection,
-} from '@atlaskit/navigation-next';
-
 import { createSubscribedElement } from '../UnstatedUtils';
 import AppContainer from '../../services/AppContainer';
 
@@ -25,25 +20,17 @@ class CustomSidebar extends React.Component {
 
   render() {
     return (
-      <div className="grw-sidebar-custom">
-        <HeaderSection>
-          { () => (
-            <div className="grw-sidebar-header-container p-3 d-flex">
-              <h3>Custom Sidebar</h3>
-              <button type="button" className="btn btn-xs btn-outline-secondary ml-auto" onClick={this.reloadData}>
-                <i className="icon icon-reload"></i>
-              </button>
-            </div>
-          ) }
-        </HeaderSection>
-        <MenuSection>
-          { () => (
-            <div className="grw-sidebar-content-container p-3">
-              (TBD) Under implementation
-            </div>
-          ) }
-        </MenuSection>
-      </div>
+      <>
+        <div className="grw-sidebar-content-header p-3 d-flex">
+          <h3 className="mb-0">Custom Sidebar</h3>
+          <button type="button" className="btn btn-sm btn-outline-secondary ml-auto" onClick={this.reloadData}>
+            <i className="icon icon-reload"></i>
+          </button>
+        </div>
+        <div className="grw-sidebar-content-header p-3">
+          (TBD) Under implementation
+        </div>
+      </>
     );
 
   }

+ 17 - 30
src/client/js/components/Sidebar/RecentChanges.jsx

@@ -3,11 +3,6 @@ import PropTypes from 'prop-types';
 
 import { withTranslation } from 'react-i18next';
 
-import {
-  HeaderSection,
-  MenuSection,
-} from '@atlaskit/navigation-next';
-
 import loggerFactory from '@alias/logger';
 
 import DevidedPagePath from '@commons/models/devided-page-path';
@@ -62,14 +57,14 @@ class RecentChanges extends React.Component {
     );
 
     return (
-      <li className="list-group-item">
+      <li className="list-group-item p-2">
         <div className="d-flex w-100">
           <UserPicture user={page.lastUpdatedUser} size="md" />
           <div className="flex-grow-1 ml-2">
             { !dPagePath.isRoot && <FormerLink /> }
-            <h4 className="mb-1">
+            <h5 className="mb-1">
               <PagePathHierarchicalLink linkedPagePath={linkedPagePathLatter} basePath={dPagePath.isRoot ? undefined : dPagePath.former} />
-            </h4>
+            </h5>
             <div className="text-right small">
               <FormattedDistanceDate id={page.id} date={page.updatedAt} />
             </div>
@@ -85,28 +80,20 @@ class RecentChanges extends React.Component {
     const { recentlyUpdatedPages } = this.props.appContainer.state;
 
     return (
-      <div className="grw-sidebar-history">
-        <HeaderSection>
-          { () => (
-            <div className="grw-sidebar-header-container p-3 d-flex">
-              <h3>{t('Recent Changes')}</h3>
-              {/* <h3>{t('Recent Created')}</h3> */} {/* TODO: impl switching */}
-              <button type="button" className="btn btn-sm btn-outline-secondary ml-auto" onClick={this.reloadData}>
-                <i className="icon icon-reload"></i>
-              </button>
-            </div>
-          ) }
-        </HeaderSection>
-        <MenuSection>
-          { () => (
-            <div className="grw-sidebar-content-container p-3">
-              <ul className="list-group list-group-flush">
-                { recentlyUpdatedPages.map(page => <PageItem key={page.id} page={page} />) }
-              </ul>
-            </div>
-          ) }
-        </MenuSection>
-      </div>
+      <>
+        <div className="grw-sidebar-content-header p-3 d-flex">
+          <h3 className="mb-0">{t('Recent Changes')}</h3>
+          {/* <h3 className="mb-0">{t('Recent Created')}</h3> */} {/* TODO: impl switching */}
+          <button type="button" className="btn btn-sm btn-outline-secondary ml-auto" onClick={this.reloadData}>
+            <i className="icon icon-reload"></i>
+          </button>
+        </div>
+        <div className="grw-sidebar-content-body p-3">
+          <ul className="list-group list-group-flush">
+            { recentlyUpdatedPages.map(page => <PageItem key={page.id} page={page} />) }
+          </ul>
+        </div>
+      </>
     );
   }
 

+ 33 - 55
src/client/js/components/Sidebar/SidebarNav.jsx

@@ -3,10 +3,6 @@ import PropTypes from 'prop-types';
 
 import { withTranslation } from 'react-i18next';
 
-import {
-  GlobalNav,
-} from '@atlaskit/navigation-next';
-
 import { createSubscribedElement } from '../UnstatedUtils';
 import AppContainer from '../../services/AppContainer';
 
@@ -28,36 +24,28 @@ class SidebarNav extends React.Component {
     }
   }
 
-  generatePrimaryItemObj(id, label, iconName) {
+  PrimaryItem = ({ id, label, iconName }) => {
     const isSelected = this.props.currentContentsId === id;
 
-    return {
-      id,
-      component: ({ className }) => (
-        <div className={`${className} grw-global-item-container ${isSelected ? 'active' : ''}`}>
-          <button
-            type="button"
-            className={`btn btn-primary btn-lg ${isSelected ? 'active' : ''}`}
-            onClick={() => this.itemSelectedHandler(id)}
-          >
-            <i className="material-icons">{iconName}</i>
-          </button>
-        </div>
-      ),
-    };
+    return (
+      <button
+        type="button"
+        className={`d-block btn btn-primary ${isSelected ? 'active' : ''}`}
+        onClick={() => this.itemSelectedHandler(id)}
+      >
+        <i className="material-icons">{iconName}</i>
+      </button>
+    );
   }
 
-  generateSecondaryItemObj(id, label, iconName, href, isBlank) {
-    return {
-      id,
-      component: ({ className }) => (
-        <div className={`${className} grw-global-item-container`}>
-          <a href={href} className="btn btn-primary" target={`${isBlank ? '_blank' : ''}`}>
-            <i className="material-icons">{iconName}</i>
-          </a>
-        </div>
-      ),
-    };
+  SecondaryItem({
+    label, iconName, href, isBlank,
+  }) {
+    return (
+      <a href={href} className="d-block btn btn-primary" target={`${isBlank ? '_blank' : ''}`}>
+        <i className="material-icons">{iconName}</i>
+      </a>
+    );
   }
 
   generateIconFactory(classNames) {
@@ -68,33 +56,23 @@ class SidebarNav extends React.Component {
     const { isAdmin, currentUsername } = this.props.appContainer;
     const isLoggedIn = currentUsername != null;
 
-    const primaryItems = [
-      this.generatePrimaryItemObj('custom', 'Custom Sidebar', 'code'),
-      this.generatePrimaryItemObj('recent', 'Recent Changes', 'update'),
-      // this.generatePrimaryItemObj('tag', 'Tags', 'icon-tag'),
-      // this.generatePrimaryItemObj('favorite', 'Favorite', 'icon-star'),
-    ];
-
-    let secondaryItems = [
-      isAdmin && (
-        this.generateSecondaryItemObj('admin', 'Admin', 'settings', '/admin')
-      ),
-      isLoggedIn && (
-        this.generateSecondaryItemObj('draft', 'Draft', 'file_copy', `/user/${currentUsername}#user-draft-list`)
-      ),
-      this.generateSecondaryItemObj('help', 'Help', 'help', 'https://docs.growi.org', true),
-      isLoggedIn && (
-        this.generateSecondaryItemObj('trash', 'Trash', 'delete', '/trash')
-      ),
-    ];
-    // remove 'false' items
-    secondaryItems = secondaryItems.filter(item => item !== false);
+    const { PrimaryItem, SecondaryItem } = this;
 
     return (
-      <GlobalNav
-        primaryItems={primaryItems}
-        secondaryItems={secondaryItems}
-      />
+      <div className="grw-sidebar-nav d-flex flex-column justify-content-between pb-4">
+        <div className="grw-sidebar-nav-primary-container">
+          <PrimaryItem id="custom" label="Custom Sidebar" iconName="code" />
+          <PrimaryItem id="recent" label="Recent Changes" iconName="update" />
+          {/* <PrimaryItem id="tag" label="Tags" iconName="icon-tag" /> */}
+          {/* <PrimaryItem id="favorite" label="Favorite" iconName="icon-star" /> */}
+        </div>
+        <div className="grw-sidebar-nav-secondary-container">
+          {isAdmin && <SecondaryItem label="Admin" iconName="settings" href="/admin" />}
+          {isLoggedIn && <SecondaryItem label="Draft" iconName="file_copy" href={`/user/${currentUsername}#user-draft-list`} />}
+          <SecondaryItem label="Help" iconName="help" href="https://docs.growi.org" isBlank />
+          {isLoggedIn && <SecondaryItem label="Trash" iconName="delete" href="/trash" />}
+        </div>
+      </div>
     );
   }
 

+ 10 - 2
src/client/js/components/User/UserPicture.jsx

@@ -3,6 +3,8 @@ import PropTypes from 'prop-types';
 
 import { userPageRoot } from '@commons/util/path-utils';
 
+import { UncontrolledTooltip } from 'reactstrap';
+
 const DEFAULT_IMAGE = '/images/icons/user.svg';
 
 // TODO UserComponent?
@@ -41,10 +43,16 @@ export default class UserPicture extends React.Component {
 
   withTooltip = (RootElm) => {
     const { user } = this.props;
-    const title = `@${user.username}<br />${user.name}`;
+    const id = `user-picture-${Math.random().toString(32).substring(2)}`;
 
     return props => (
-      <RootElm data-toggle="tooltip" data-placement="bottom" data-html="true" title={title}>{props.children}</RootElm>
+      <>
+        <RootElm id={id}>{props.children}</RootElm>
+        <UncontrolledTooltip placement="bottom" target={id} delay={0} fade={false}>
+          @{user.username}<br />
+          {user.name}
+        </UncontrolledTooltip>
+      </>
     );
   }
 

+ 5 - 4
src/client/js/services/AppContainer.js

@@ -116,9 +116,9 @@ export default class AppContainer extends Container {
   }
 
   async initColorScheme() {
-    const switchStateByMediaQuery = (mql) => {
+    const switchStateByMediaQuery = async(mql) => {
       const preferDarkMode = mql.matches;
-      this.setState({ preferDarkModeByMediaQuery: preferDarkMode });
+      await this.setState({ preferDarkModeByMediaQuery: preferDarkMode });
 
       this.applyColorScheme();
     };
@@ -127,13 +127,13 @@ export default class AppContainer extends Container {
     // add event listener
     mqlForDarkMode.addListener(switchStateByMediaQuery);
 
-    // restore settings from localStorage
+    // initialize1: restore settings from localStorage
     const { localStorage } = window;
     if (localStorage.preferDarkModeByUser != null) {
       await this.setState({ preferDarkModeByUser: localStorage.preferDarkModeByUser === 'true' });
     }
 
-    // initialize
+    // initialize2: check media query
     switchStateByMediaQuery(mqlForDarkMode);
   }
 
@@ -410,6 +410,7 @@ export default class AppContainer extends Container {
    */
   applyColorScheme() {
     const { preferDarkModeByMediaQuery, preferDarkModeByUser } = this.state;
+
     let isDarkMode = preferDarkModeByMediaQuery;
     if (preferDarkModeByUser != null) {
       isDarkMode = preferDarkModeByUser;

+ 8 - 4
src/client/styles/scss/_admin.scss

@@ -47,10 +47,14 @@
   }
 
   .admin-notification {
-    .td-abs-center {
-      width: 1px; // to keep the cell small
-      text-align: center;
-      vertical-align: middle;
+    table .admin-notif-list {
+      td {
+        vertical-align: middle;
+      }
+      .td-abs-center {
+        width: 1px; // to keep the cell small
+        text-align: center;
+      }
     }
   }
 

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

@@ -1,11 +1,5 @@
-// FIXME: replace with mt-2 or mt-3
-.grw-mt-10px {
-  margin-top: 10px !important;
-}
-
-// FIXME: replace with pt-2 or pt-3
-.grw-pt-10px {
-  padding-top: 10px !important;
+body {
+  overflow-y: scroll !important;
 }
 
 .grw-logo {
@@ -21,7 +15,6 @@
 }
 
 #page-wrapper {
-  min-width: 0;
   margin-top: $grw-navbar-height + $grw-navbar-border-width;
 }
 

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

@@ -1,6 +1,10 @@
 $navbar-height-adjustment: 10px;
 
 body.kibela {
+  .grw-pt-10px {
+    padding-top: 10px !important;
+  }
+
   /* navbar for kibela */
   #page-wrapper {
     margin-top: $grw-navbar-height + $grw-navbar-border-width;

+ 10 - 55
src/client/styles/scss/_login.scss

@@ -1,12 +1,4 @@
 .nologin {
-  $gray-800-for-login: darken(white, 30%);
-  $color-gradient: #3e4d6c;
-
-  // background color
-  background: linear-gradient(45deg, darken($color-gradient, 30%) 0%, hsla(340, 100%, 55%, 0) 70%),
-    linear-gradient(135deg, $growi-green 10%, hsla(225, 95%, 50%, 0) 70%), linear-gradient(225deg, $growi-blue 10%, hsla(140, 90%, 50%, 0) 80%),
-    linear-gradient(315deg, darken($color-gradient, 25%) 100%, hsla(35, 95%, 55%, 0) 70%);
-
   #page-wrapper {
     background: none;
   }
@@ -62,43 +54,20 @@
 
   // styles
   .login-header {
-    background-color: rgba(white, 0.5);
-
-    .logo {
-      background-color: rgba(black, 0);
-      fill: rgba(black, 0.5);
-    }
-
     h1 {
       font-size: 22px;
       line-height: 1em;
-      color: rgba(black, 0.5);
     }
   }
 
-  .login-dialog {
-    background-color: rgba(white, 0.5);
-  }
-
   .input-group {
     margin-bottom: 10px;
 
     .input-group-text {
-      color: $gray-800-for-login;
       text-align: center;
-      background-color: rgba(black, 0.4);
       border: none;
       border-radius: 0;
     }
-
-    .form-control {
-      color: white;
-      background-color: rgba(lighten(black, 10%), 0.4);
-
-      &::placeholder {
-        color: $gray-800-for-login;
-      }
-    }
   }
 
   .input-group:not(.has-error) {
@@ -114,39 +83,39 @@
   $btn-fill-colors: (
     'login': (
       rgba($danger, 0.4),
-      rgba(#7e4153, 0.5),
+      rgba(#7e4153, 0.7),
     ),
     'register': (
       rgba($success, 0.4),
-      rgba(#3f7263, 0.5),
+      rgba(#3f7263, 0.7),
     ),
     'google': (
       rgba(#24292e, 0.4),
-      #555,
+      #444,
     ),
     'github': (
       rgba(lighten(black, 20%), 0.4),
-      #555,
+      #444,
     ),
     'facebook': (
       rgba(#29487d, 0.4),
-      #555,
+      #444,
     ),
     'twitter': (
       rgba(#1da1f2, 0.4),
-      #555,
+      #444,
     ),
     'oidc': (
       rgba(#24292e, 0.4),
-      #555,
+      #444,
     ),
     'saml': (
       rgba(#55a79a, 0.4),
-      #555,
+      #444,
     ),
     'basic': (
       rgba(#24292e, 0.4),
-      #555,
+      #444,
     ),
   );
 
@@ -165,26 +134,12 @@
   .link-growi-org {
     font-size: smaller;
     font-weight: bold;
-    color: rgba(black, 0.4);
 
     &,
     .growi,
     .org {
       transition: color 0.8s;
     }
-
-    &:hover,
-    &.focus {
-      color: black;
-
-      .growi {
-        color: darken($growi-green, 20%);
-      }
-
-      .org {
-        color: darken($growi-blue, 15%);
-      }
-    }
   }
 
   .link-switch {
@@ -210,7 +165,7 @@
   .link-growi-org {
     position: absolute;
     bottom: 9px;
-    z-index: 2;
+    z-index: 3;
   }
 
   // To adjust the behavior, this problem is not solved.

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

@@ -1,5 +1,8 @@
 .grw-navbar {
+  border-top: 0;
+  border-right: 0;
   border-bottom: $grw-navbar-border-width solid;
+  border-left: 0;
 
   .grw-navbar-toggler {
     padding: 0.5rem;

+ 6 - 10
src/client/styles/scss/_on-edit.scss

@@ -9,6 +9,8 @@ body:not(.on-edit) {
 }
 
 body.on-edit {
+  overflow-y: hidden !important;
+
   // calculate margin
   $editor-header-plus-footer: 42px // .nav-tabs height
     + 1px //                          .page-editor-footer border-top
@@ -24,8 +26,10 @@ body.on-edit {
   }
 
   // show
-  .d-edit-block {
-    display: block !important;
+  .d-edit-sm-block {
+    @include media-breakpoint-up(sm) {
+      display: block !important;
+    }
   }
 
   // hide unnecessary elements
@@ -132,14 +136,6 @@ body.on-edit {
     }
   }
 
-  // deal with word wrap problem
-  // see: https://qiita.com/mpyw/items/dfc63c1fed5dfc5eda26
-  .page-editor-editor-container,
-  .page-editor-preview-container {
-    min-width: 0;
-    overflow-wrap: break-word;
-  }
-
   /*********************
    * Navigation styles
    */

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

@@ -62,6 +62,7 @@ $modal-header-padding-x: 1rem;
 //== Alerts
 $alert-bg-level: -2;
 $alert-border-level: 0;
+$alert-color-level: -10;
 
 //== Progress bar
 $progress-height: 4px;

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

@@ -89,6 +89,12 @@
   }
 
   //Modals
+  .modal-open {
+    position: fixed;
+    width: 100%;
+    padding-right: 0 !important;
+  }
+
   .modal-content {
     box-shadow: 0 0.3rem 1rem rgba(0, 0, 0, 0.1);
   }
@@ -124,4 +130,9 @@
     margin-bottom: 18px;
     overflow: hidden;
   }
+
+  .text-break {
+    word-break: break-word;
+    overflow-wrap: break-word;
+  }
 }

+ 57 - 3
src/client/styles/scss/_search.scss

@@ -66,13 +66,22 @@
     border-bottom-left-radius: 40px;
   }
 
+  .search-typeahead {
+    // corner radius
+    border-top-right-radius: 40px;
+    border-bottom-right-radius: 40px;
+    .rbt-input-main {
+      padding-right: 58px;
+      // corner radius
+      border-top-right-radius: 40px;
+      border-bottom-right-radius: 40px;
+    }
+  }
+
   // using react-bootstrap-typeahead
   // see: https://github.com/ericgio/react-bootstrap-typeahead
   .rbt-input.form-control {
     height: 30px;
-    border-top-right-radius: 40px;
-    border-bottom-right-radius: 40px;
-
     .rbt-input-wrapper {
       margin-left: 8px;
     }
@@ -120,6 +129,22 @@
       @include media-breakpoint-up(md) {
         width: 300px;
       }
+      @include media-breakpoint-up(lg) {
+        // focus
+        &.focus {
+          width: 400px;
+        }
+      }
+      @include media-breakpoint-up(xl) {
+        width: 350px;
+        // focus
+        &.focus {
+          width: 450px;
+        }
+      }
+    }
+    .search-typeahead {
+      border-radius: 0 25px 25px 0;
     }
   }
 }
@@ -210,3 +235,32 @@
     display: table-header-group;
   }
 }
+
+@include media-breakpoint-down(sm) {
+  .grw-search-table {
+    th {
+      text-align: right;
+    }
+  
+    td {
+      overflow-wrap: anywhere;
+      white-space: normal !important;
+    }
+  
+    @include media-breakpoint-down(xs) {
+      th,
+      td {
+        display: block;
+      }
+      
+      th {
+        text-align: left;
+      }
+      
+      td {
+        border-top: none !important;
+        padding-top: 0 !important;
+      }
+    }
+  }
+}

+ 45 - 62
src/client/styles/scss/_sidebar.scss

@@ -12,6 +12,7 @@
       width: 0;
       content: '';
       border: 9px solid transparent;
+      border-right-color: white;
       border-left-width: 0;
       transform: translateY(-#{$sidebar-nav-button-height / 2});
     }
@@ -49,40 +50,33 @@
 
   // override @atlaskit/navigation-next styles
   $navbar-total-height: $grw-navbar-height + $grw-navbar-border-width;
-  div[class$='-LayoutContainer'] {
+  div[data-layout-container='true'] {
     height: calc(100vh - #{$navbar-total-height});
   }
-  div[class$='-NavigationContainer'] {
+  div[data-testid='Navigation'] {
     top: $navbar-total-height;
 
     // Adjust to be on top of the growi subnavigation
     z-index: $zindex-sticky + 5;
-  }
-  div[data-testid='GlobalNavigation'] {
-    > div {
-      height: calc(100vh - #{$navbar-total-height});
-      padding-top: 0;
+
+    // css-xxx-Outer
+    > div:nth-of-type(2) {
+      width: 0;
+
+      // css-xxx-Shadow
+      > div:first-child {
+        background: linear-gradient(to left, rgba(0, 0, 0, 0.1) 0px, rgba(0, 0, 0, 0.1) 1px, rgba(0, 0, 0, 0.1) 1px, rgba(0, 0, 0, 0) 100%);
+      }
     }
   }
-  div[class$='-Outer'] {
-    width: 0;
-  }
-  div[class$='-PrimaryItemsList'],
-  div[class$='-SecondaryItemsList'] {
-    > div {
-      padding: 0;
-    }
 
-    .grw-global-item-container {
-      width: unset;
-      height: unset;
-      background-color: transparent;
-      border-radius: 0;
+  .grw-sidebar-nav {
+    height: calc(100vh - #{$navbar-total-height});
 
-      .btn {
-        width: $grw-sidebar-nav-width;
-        border-radius: 0;
-      }
+    .btn {
+      width: $grw-sidebar-nav-width;
+      line-height: 1em;
+      border-radius: 0;
 
       // icon opacity
       &:not(.active) {
@@ -97,39 +91,29 @@
         }
       }
     }
-  }
-  div[class$='-PrimaryItemsList'] {
-    .grw-global-item-container {
-      .btn-lg {
-        padding: 0.8rem 1rem;
-        line-height: 1em;
+
+    .grw-sidebar-nav-primary-container {
+      .btn {
+        padding: 1em;
         i {
-          font-size: 1.7em;
+          font-size: 2.3em;
         }
-      }
 
-      &.active {
-        button {
+        &.active {
           @extend %fukidashi-for-active;
         }
       }
     }
-  }
-  div[class$='-ScrollableTransitionGroup'] {
-    // remove horizontal line
-    > div,
-    > div > div {
-      &:before,
-      &:after {
-        display: none;
+
+    .grw-sidebar-nav-secondary-container {
+      .btn {
+        padding: 0.9em;
+        i {
+          font-size: 1.5em;
+        }
       }
     }
   }
-  div[class$='-Outer'] {
-    div[class$='-Shadow'] {
-      background: linear-gradient(to left, rgba(0, 0, 0, 0.1) 0px, rgba(0, 0, 0, 0.1) 1px, rgba(0, 0, 0, 0.1) 1px, rgba(0, 0, 0, 0) 100%);
-    }
-  }
 }
 
 // Drawer Mode
@@ -139,23 +123,31 @@
     z-index: $zindex-fixed - 2;
 
     // override @atlaskit/navigation-next styles
-    div[class$='-Outer'],
-    div[class$='-teprsg'] {
-      display: none;
+    div[data-layout-container='true'] {
+      // css-teprsg
+      > div:nth-of-type(2) {
+        display: none;
+      }
+    }
+    div[data-testid='Navigation'] {
+      // css-xxx-Outer
+      > div:nth-of-type(2) {
+        display: none;
+      }
     }
 
     &:not(.open) {
-      div[class$='-NavigationContainer'] {
+      div[data-testid='Navigation'] {
         left: -#{$grw-sidebar-nav-width + $grw-sidebar-content-min-width};
       }
     }
     &.open {
-      div[class$='-NavigationContainer'] {
+      div[data-testid='Navigation'] {
         left: 0;
       }
     }
 
-    div[class$='-NavigationContainer'] {
+    div[data-testid='Navigation'] {
       transition: left 300ms cubic-bezier(0.25, 1, 0.5, 1);
     }
   }
@@ -164,12 +156,3 @@
     z-index: $zindex-fixed - 4;
   }
 }
-
-.grw-sidebar-header-container {
-  h3 {
-    margin-bottom: 0;
-  }
-}
-
-.grw-sidebar-content-container {
-}

+ 1 - 1
src/client/styles/scss/atoms/_buttons.scss

@@ -41,7 +41,7 @@
   color: white;
   text-align: center;
   cursor: pointer;
-  background-color: rgba(lighten(black, 20%), 0.4);
+  background-color: rgba(lighten(black, 15%), 0.5);
   border: none;
 
   .btn-label {

+ 3 - 0
src/client/styles/scss/atoms/_nav.scss

@@ -1,6 +1,9 @@
 .nav-tabs .grw-main-nav-item-left {
   width: $grw-nav-main-left-tab-width;
   text-align: center;
+  @include media-breakpoint-down(xs) {
+    width: 45px;
+  }
 
   .nav-link {
     padding-right: 0;

+ 1 - 1
src/client/styles/scss/molecules/copy-dropdown.scss

@@ -12,7 +12,7 @@
     }
 
     .well {
-      font-size: 0.6em;
+      font-size: 0.7em;
       word-break: break-all;
     }
   }

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

@@ -75,6 +75,14 @@
  * Helper Classes
  */
 
+.mw-0 {
+  min-width: 0;
+}
+
+.flex-basis-0 {
+  flex-basis: 0;
+}
+
 .picture {
   width: 24px;
   height: 24px;

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

@@ -1,10 +1,17 @@
 // determine optional variables
+$color-list: $color-global !default;
+$bgcolor-list: $bgcolor-global !default;
+$color-list-hover: $color-global !default;
+$bgcolor-list-hover: lighten($bgcolor-global, 3%) !default;
+$color-list-active: $color-reversal !default;
+$bgcolor-list-active: $primary !default;
 $bgcolor-subnabvar: lighten($bgcolor-global, 3%) !default;
 $color-table: white !default;
 $bgcolor-table: #343a40 !default;
 $border-color-table: lighten($bgcolor-table, 7.5%) !default;
 $color-table-hover: rgba(white, 0.075) !default;
 $bgcolor-table-hover: lighten($bgcolor-table, 7.5%) !default;
+$bgcolor-sidebar-list-group: $bgcolor-list !default;
 
 // override bootstrap variables
 $table-dark-color: $color-table;
@@ -15,10 +22,14 @@ $table-dark-hover-bg: $bgcolor-table-hover;
 
 @import 'reboot-bootstrap-tables';
 
+// List Group
+@include override-list-group-item($color-list, $bgcolor-list, $color-list-hover, $bgcolor-list-hover, $color-list-active, $bgcolor-list-active);
+
 /*
   * Form
   */
 input.form-control,
+select.form-control,
 textarea.form-control {
   color: lighten($color-global, 30%);
   background-color: darken($bgcolor-global, 5%);
@@ -83,6 +94,87 @@ ul.pagination {
   }
 }
 
+/*
+ * GROWI Login form
+ */
+.nologin {
+  // background color
+  $color-gradient: #3c465c;
+  background: linear-gradient(45deg, darken($color-gradient, 30%) 0%, hsla(340, 100%, 55%, 0) 70%),
+    linear-gradient(135deg, darken($growi-green, 30%) 10%, hsla(225, 95%, 50%, 0) 70%),
+    linear-gradient(225deg, darken($growi-blue, 20%) 10%, hsla(140, 90%, 50%, 0) 80%),
+    linear-gradient(315deg, darken($color-gradient, 25%) 100%, hsla(35, 95%, 55%, 0) 70%);
+
+  .login-header {
+    background-color: rgba(black, 0.5);
+
+    .logo {
+      background-color: rgba(white, 0);
+      fill: rgba(white, 0.5);
+    }
+
+    h1 {
+      color: rgba(white, 0.5);
+    }
+  }
+
+  .login-dialog {
+    background-color: rgba(black, 0.5);
+  }
+
+  .input-group {
+    .input-group-text {
+      color: darken(white, 30%);
+      background-color: rgba(#444, 0.7);
+    }
+
+    .form-control {
+      color: white;
+      background-color: rgba(#505050, 0.7);
+      box-shadow: unset;
+
+      &::placeholder {
+        color: darken(white, 30%);
+      }
+    }
+  }
+
+  .btn-fill {
+    .btn-label {
+      color: #ccc;
+    }
+    .btn-label-text {
+      color: #aaa;
+    }
+  }
+
+  .grw-external-auth-form {
+    border-color: gray !important;
+  }
+
+  .btn-external-auth-tab {
+    @extend .btn-dark;
+  }
+
+  // footer link text
+  .link-growi-org {
+    color: rgba(white, 0.4);
+
+    &:hover,
+    &.focus {
+      color: rgba(white, 0.7);
+
+      .growi {
+        color: darken($growi-green, 5%);
+      }
+
+      .org {
+        color: darken($growi-blue, 5%);
+      }
+    }
+  }
+}
+
 /*
  * GROWI page list
  */
@@ -111,6 +203,14 @@ ul.pagination {
   }
 }
 
+/*
+ * GROWI Sidebar
+ */
+.grw-sidebar {
+  // List
+  @include override-list-group-item($color-list, $bgcolor-sidebar-list-group, $color-list-hover, $bgcolor-list-hover, $color-list-active, $bgcolor-list-active);
+}
+
 /*
  * GROWI on-edit
  */
@@ -120,6 +220,13 @@ ul.pagination {
   }
 }
 
+/*
+ * GROWI HandsontableModal
+ */
+.grw-hot-modal-navbar {
+  background-color: $dark;
+}
+
 .wiki {
   h1 {
     border-color: lighten($border-color-theme, 10%);

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

@@ -1,10 +1,17 @@
 // determine optional variables
+$color-list: $color-global !default;
+$bgcolor-list: $bgcolor-global !default;
+$color-list-hover: $color-global !default;
+$bgcolor-list-hover: darken($bgcolor-global, 3%) !default;
+$color-list-active: $color-reversal !default;
+$bgcolor-list-active: $primary !default;
 $bgcolor-subnabvar: darken($bgcolor-global, 3%) !default;
 $color-table: $color-global !default;
 $bgcolor-table: null !default;
 $border-color-table: #dee2e6 !default;
 $color-table-hover: $color-table !default;
 $bgcolor-table-hover: rgba(black, 0.075) !default;
+$bgcolor-sidebar-list-group: $bgcolor-list !default;
 
 // override bootstrap variables
 $table-color: $color-table;
@@ -15,6 +22,9 @@ $table-hover-bg: $bgcolor-table-hover;
 
 @import 'reboot-bootstrap-tables';
 
+// List Group
+@include override-list-group-item($color-list, $bgcolor-list, $color-list-hover, $bgcolor-list-hover, $color-list-active, $bgcolor-list-active);
+
 /*
  * Form
  */
@@ -29,14 +39,65 @@ $table-hover-bg: $bgcolor-table-hover;
 }
 
 /*
- * GROWI search-top
+ * GROWI Login form
  */
-.search-top {
-  .btn-group-dropdown-scope .dropdown-toggle {
-    background-color: rgba($bgcolor-global, 0.8);
+.nologin {
+  // background color
+  $color-gradient: #3e4d6c;
+  background: linear-gradient(45deg, darken($color-gradient, 30%) 0%, hsla(340, 100%, 55%, 0) 70%),
+    linear-gradient(135deg, $growi-green 10%, hsla(225, 95%, 50%, 0) 70%), linear-gradient(225deg, $growi-blue 10%, hsla(140, 90%, 50%, 0) 80%),
+    linear-gradient(315deg, darken($color-gradient, 25%) 100%, hsla(35, 95%, 55%, 0) 70%);
+
+  .login-header {
+    background-color: rgba(white, 0.5);
+
+    .logo {
+      background-color: rgba(black, 0);
+      fill: rgba(black, 0.5);
+    }
+
+    h1 {
+      color: rgba(black, 0.5);
+    }
+  }
+
+  .login-dialog {
+    background-color: rgba(white, 0.5);
+  }
+
+  .input-group {
+    .input-group-text {
+      color: darken(white, 30%);
+      background-color: rgba(#444, 0.7);
+    }
+
+    .form-control {
+      color: white;
+      background-color: rgba(#505050, 0.7);
+      box-shadow: unset;
+
+      &::placeholder {
+        color: darken(white, 30%);
+      }
+    }
   }
-  .rbt-input.form-control {
-    background-color: rgba($bgcolor-global, 0.9);
+
+  // footer link text
+  .link-growi-org {
+    color: rgba(black, 0.4);
+
+    &:hover,
+    &.focus {
+      color: black;
+
+      .growi {
+        color: darken($growi-green, 20%);
+      }
+
+      .org {
+        color: darken($growi-blue, 15%);
+      }
+    }
   }
 }
 
@@ -47,6 +108,14 @@ $table-hover-bg: $bgcolor-table-hover;
   background-color: $bgcolor-subnabvar;
 }
 
+/*
+ * GROWI Sidebar
+ */
+.grw-sidebar {
+  // List
+  @include override-list-group-item($color-list, $bgcolor-sidebar-list-group, $color-list-hover, $bgcolor-list-hover, $color-list-active, $bgcolor-list-active);
+}
+
 /*
  * GROWI page list
  */
@@ -69,6 +138,13 @@ $table-hover-bg: $bgcolor-table-hover;
   }
 }
 
+/*
+ * GROWI HandsontableModal
+ */
+.grw-hot-modal-navbar {
+  background-color: $light;
+}
+
 .wiki {
   h1 {
     border-color: darken($border-color-theme, 10%);

+ 45 - 31
src/client/styles/scss/theme/_apply-colors.scss

@@ -3,16 +3,10 @@
 //
 
 // determine optional variables
-$color-list: $color-global !default;
-$bgcolor-list: $bgcolor-global !default;
-$color-list-hover: $color-reversal !default;
-$color-list-active: $color-reversal !default;
-$bgcolor-list-active: $primary !default;
 $border-image-navbar: linear-gradient(to right, #ccc 0%, #ccc 100%) !default;
 $bgcolor-search-top-dropdown: $secondary !default;
 $bgcolor-sidebar-nav-item-active: darken($bgcolor-sidebar, 10%) !default;
 $text-shadow-sidebar-nav-item-active: 1px 1px 2px $primary !default;
-$bgcolor-sidebar-list-group: $bgcolor-list !default;
 $bgcolor-inline-code: #f0f0f0 !default;
 $color-inline-code: #c7254e !default;
 $bordercolor-inline-code: #ccc8c8 !default;
@@ -61,6 +55,16 @@ pre:not(.hljs):not(.CodeMirror-line) {
 //== Apply to Bootstrap Elements
 //
 
+// Alert link
+@each $color, $value in $theme-colors {
+  .alert.alert-#{$color} {
+    a,
+    a:hover {
+      color: theme-color-level($color, $alert-color-level - 2);
+    }
+  }
+}
+
 // Link buttons
 .btn-link {
   color: $link-color;
@@ -86,9 +90,6 @@ pre:not(.hljs):not(.CodeMirror-line) {
   }
 }
 
-// List Group
-@include override-list-group-item($color-list, $bgcolor-list, $color-list-hover, $color-list-active, $bgcolor-list-active);
-
 // Form
 .form-control {
   @include form-control-focus();
@@ -105,6 +106,13 @@ pre:not(.hljs):not(.CodeMirror-line) {
   }
 }
 
+//
+//== Apply to Handsontable
+//
+.handsontable {
+  color: initial;
+}
+
 //
 //== Apply to GROWI Elements
 //
@@ -146,9 +154,15 @@ pre:not(.hljs):not(.CodeMirror-line) {
   .btn-secondary.dropdown-toggle {
     @include button-variant($bgcolor-search-top-dropdown, $bgcolor-search-top-dropdown);
   }
+
+  // for https://youtrack.weseek.co.jp/issue/GW-2603
+  .search-typeahead {
+    background-color: rgba($bgcolor-global, 0.9);
+  }
 }
 
 .grw-sidebar {
+  // override @atlaskit/navigation-next styles
   .ak-navigation-resize-button {
     $color-resize-button: $color-global !default;
     $bgcolor-resize-button: white !default;
@@ -161,8 +175,19 @@ pre:not(.hljs):not(.CodeMirror-line) {
       @include override-hexagon-color($color-resize-button-hover, $bgcolor-resize-button-hover);
     }
   }
+  div[data-testid='GlobalNavigation'] {
+    > div {
+      background-color: $bgcolor-sidebar;
+    }
+  }
+  div[data-testid='ContextualNavigation'] {
+    > div {
+      color: $color-sidebar-context;
+      background-color: $bgcolor-sidebar-context;
+    }
+  }
 
-  .grw-global-item-container {
+  .grw-sidebar-nav {
     .btn {
       @include button-variant(
         $bgcolor-sidebar,
@@ -174,29 +199,17 @@ pre:not(.hljs):not(.CodeMirror-line) {
       );
     }
   }
-  .grw-global-item-container.active {
-    .btn:after {
-      // fukidashi color
-      border-right-color: $bgcolor-sidebar-context;
-    }
-    i {
-      text-shadow: $text-shadow-sidebar-nav-item-active;
-    }
-  }
-  div[data-testid='GlobalNavigation'] {
-    > div {
-      background-color: $bgcolor-sidebar;
-    }
-  }
-  div[data-testid='ContextualNavigation'] {
-    > div {
-      color: $color-sidebar-context;
-      background-color: $bgcolor-sidebar-context;
+  .grw-sidebar-nav-primary-container {
+    .btn.active {
+      i {
+        text-shadow: $text-shadow-sidebar-nav-item-active;
+      }
+      // fukidashi
+      &:after {
+        border-right-color: $bgcolor-sidebar-context;
+      }
     }
   }
-
-  // List
-  @include override-list-group-item($color-list, $bgcolor-sidebar-list-group, $color-list-hover, $color-list-active, $bgcolor-list-active);
 }
 
 /*
@@ -238,6 +251,7 @@ pre:not(.hljs):not(.CodeMirror-line) {
  * cards
  */
 .card.well {
+  color: $color-global;
   background-color: $bgcolor-card;
   border-color: $light;
   box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05);

+ 16 - 8
src/client/styles/scss/theme/antarctic.scss

@@ -64,11 +64,12 @@ html[dark] {
   $color-inline-code: #c7254e; // optional
 
   // List Group colors
-  $color-list: $color-global;
-  $bgcolor-list: $bgcolor-global;
-  $color-list-active: $color-reversal;
-  $bgcolor-list-active: $primary;
-  $color-list-hover: $color-reversal;
+  // $color-list: $color-global;
+  // $bgcolor-list: $bgcolor-global;
+  // $color-list-hover: $color-reversal;
+  // $bgcolor-list-hover: ;
+  // $color-list-active: $color-reversal;
+  // $bgcolor-list-active: $primary;
 
   // Navbar
   $bgcolor-navbar: #35393f;
@@ -122,9 +123,16 @@ html[dark] {
     }
   }
 
-  a#login.link-switch,
-  a#register.link-switch {
-    color: rgba(black, 0.5);
+  // login and register
+  .nologin {
+    a#login.link-switch,
+    a#register.link-switch {
+      color: rgba(black, 0.5);
+    }
+
+    .grw-external-auth-form {
+      border-color: #aaa !important;
+    }
   }
 }
 

+ 27 - 34
src/client/styles/scss/theme/christmas.scss

@@ -56,18 +56,18 @@ html[dark] {
   $border-color-table: #aaa; // optional
 
   // List Group colors
-  $color-list: $color-global;
-  $bgcolor-list: $themelight;
-  $color-list-active: $color-reversal;
+  // $color-list: $color-global;
+  $bgcolor-list: transparent;
+  // $color-list-hover: $color-reversal;
+  $color-list-active: white;
   $bgcolor-list-active: $themecolor;
-  $color-list-hover: $color-reversal;
 
   // Navbar
   $bgcolor-navbar: $themecolor;
   $bgcolor-search-top-dropdown: $primary;
   $border-color-navbar-gradient-left: #545fff;
   $border-color-navbar-gradient-right: #00a6e5;
-  $border-image-navbar: linear-gradient(to right, #6458bc 33%, #5cb4ff 66%, #85d800 100%);
+  $border-image-navbar: linear-gradient(to right, $primary 0%, $subthemecolor 100%);
 
   // Logo colors
   $bgcolor-logo: $themecolor;
@@ -75,7 +75,7 @@ html[dark] {
   // Sidebar
   $bgcolor-sidebar: $subthemecolor;
   $bgcolor-sidebar-nav-item-active: rgba(#000000, 0.37); // optional
-  $text-shadow-sidebar-nav-item-active: 0px 0px 10px #0099ff; // optional
+  $text-shadow-sidebar-nav-item-active: 0px 0px 10px $primary; // optional
   // Sidebar resize button
   $color-resize-button: $color-reversal;
   $bgcolor-resize-button: $primary;
@@ -135,22 +135,26 @@ html[dark] {
   // login page
   .nologin {
     .input-group {
-      .input-group-addon {
-        background-color: rgba(lighten(black, 10%), 0.6);
+      .input-group-text {
+        color: #444;
+        background-color: rgba(darken(white, 20%), 0.6);
       }
       .form-control {
-        background-color: rgba(lighten(black, 10%), 0.6);
+        color: #444;
+        background-color: rgba(white, 0.6);
       }
     }
 
-    &.login-page {
-      .login-header,
-      .login-dialog {
-        background-color: rgba(#ccc, 0.5);
-      }
-      .link-switch {
-        color: #bd3425;
-      }
+    .login-header,
+    .login-dialog {
+      background-color: rgba(#ccc, 0.5);
+    }
+    .link-switch {
+      color: #bd3425;
+    }
+
+    .grw-external-auth-form {
+      border-color: #aaa !important;
     }
   }
 
@@ -163,26 +167,15 @@ html[dark] {
   }
 
   /*
-  * Panel
+  * Card
   */
-  .panel {
-    &.panel-white,
-    &.panel-default {
-      border-color: $subthemecolor;
-      .panel-heading {
-        color: $dark;
-        background-color: $subthemecolor;
-        border-bottom: 1px solid $subthemecolor;
-      }
+  .card {
+    &.border-primary {
+      border-color: $themecolor !important;
     }
-  }
-
-  .panel.panel-primary {
-    border-color: #bd3425;
-    .panel-heading {
+    .card-header.bg-primary {
       color: white;
-      background-color: $themecolor;
-      background-image: url('/images/themes/christmas/christmas-navbar.jpg');
+      background-image: url('/images/themes/christmas/christmas-navbar.jpg') !important;
     }
   }
 

+ 4 - 2
src/client/styles/scss/theme/default.scss

@@ -36,7 +36,8 @@ html[light] {
   // List Group colors
   // $color-list: $color-global; // optional
   // $bgcolor-list: $bgcolor-global; // optional
-  // $color-list-hover: $color-reversal; // optional
+  // $color-list-hover: $color-global; // optional
+  // $bgcolor-list-hover: darken($bgcolor-global, 3%); // optional
   // $color-list-active: $color-reversal; // optional
   // $bgcolor-list-active: $primary; // optional
 
@@ -123,7 +124,8 @@ html[dark] {
   // List Group colors
   // $color-list: $color-global; // optional
   // $bgcolor-list: $bgcolor-global; // optional
-  // $color-list-hover: $color-reversal; // optional
+  // $color-list-hover: $color-global; // optional
+  // $bgcolor-list-hover: lighten($bgcolor-global, 3%); // optional
   $color-list-active: white; // optional
   // $bgcolor-list-active: $primary; // optional
 

+ 18 - 5
src/client/styles/scss/theme/future.scss

@@ -14,7 +14,7 @@ html[dark] {
 
   // Font colors
   $color-global: #95abba;
-  $color-reversal: $accentcolor;
+  $color-reversal: #222;
   $color-header: #95abba;
   $color-link: $accentcolor;
   $color-link-hover: lighten($color-link, 20%);
@@ -25,11 +25,18 @@ html[dark] {
   $color-search: $primary;
 
   // List Group colors
-  $color-list: $color-global;
+  // $color-list: $color-global;
   $bgcolor-list: transparent;
-  $color-list-active: $color-reversal;
-  $bgcolor-list-active: $primary;
-  $color-list-hover: $color-reversal;
+  // $color-list-hover: $color-reversal;
+  $color-list-active: white;
+  // $bgcolor-list-active: $primary;
+
+  // Table colors
+  // $color-table: #; // optional
+  $bgcolor-table: darken($themecolor, 3%); // optional
+  // $border-color-table: #; // optional
+  // $color-table-hover: #; // optional
+  // $bgcolor-table-hover: #; // optional
 
   // Navbar
   $bgcolor-navbar: #01181a;
@@ -58,6 +65,12 @@ html[dark] {
   $color-resize-button-hover: #0e2329;
   $bgcolor-resize-button-hover: lighten($bgcolor-resize-button, 5%);
 
+  // Tabs
+  $bordercolor-nav-tabs: #4c9eb4; // optional
+  // $color-nav-tabs-link-active: #; //optional
+  $bordercolor-nav-tabs-hover: #295561 #295561 $bordercolor-nav-tabs; // optional
+  // $bordercolor-nav-tabs-active: # # $bgcolor-global; // optional
+
   // Icon colors
   $color-editor-icons: $color-global;
 

+ 3 - 2
src/client/styles/scss/theme/halloween.scss

@@ -54,9 +54,10 @@ html[dark] {
   // List Group colors
   $color-list: #979797;
   $bgcolor-list: transparent;
-  $color-list-active: $color-reversal;
-  $bgcolor-list-active: $primary;
   $color-list-hover: $themecolor;
+  // $bgcolor-list-hover: ;
+  // $color-list-active: $color-reversal;
+  // $bgcolor-list-active: $primary;
 
   // Search Top
   $color-search: $primary;

+ 5 - 4
src/client/styles/scss/theme/island.scss

@@ -24,11 +24,12 @@ html[dark] {
   $color-inline-code: #c7254e; // optional
 
   // List Group colors
-  $color-list: $color-global;
-  $bgcolor-list: lighten($color-themelight, 10%);
+  // $color-list: $color-global;
+  // $bgcolor-list: lighten($color-themelight, 10%);
+  // $color-list-hover: ;
+  // $bgcolor-list-hover: ;
   $color-list-active: $color-global;
-  $bgcolor-list-active: $primary;
-  $color-list-hover: $color-reversal;
+  // $bgcolor-list-active: $primary;
 
   // Table colors
   // $color-table: #; // optional

+ 2 - 2
src/client/styles/scss/theme/mixins/_list-group.scss

@@ -1,4 +1,4 @@
-@mixin override-list-group-item($color, $bgcolor, $color-hover: $color, $color-active: $color, $bgcolor-active: $bgcolor) {
+@mixin override-list-group-item($color, $bgcolor, $color-hover: $color, $bgcolor-hover: $bgcolor, $color-active: $color, $bgcolor-active: $bgcolor) {
   .list-group {
     .list-group-item {
       color: $color;
@@ -6,7 +6,7 @@
 
       &.list-group-item-action {
         &:hover {
-          background-color: $color-hover;
+          background-color: $bgcolor-hover;
         }
         &.active {
           color: $color-active;

+ 8 - 6
src/client/styles/scss/theme/mono-blue.scss

@@ -27,11 +27,12 @@ html[light] {
   $color-search: #c0d6df;
 
   // List Group colors
-  $color-list: $color-global;
+  // $color-list: $color-global;
   $bgcolor-list: transparent;
-  $color-list-active: $color-reversal;
-  $bgcolor-list-active: $primary;
   $color-list-hover: $color-search;
+  $bgcolor-list-hover: darken($bgcolor-global, 3%);
+  // $color-list-active: $color-reversal;
+  // $bgcolor-list-active: $primary;
 
   // Navbar
   $bgcolor-navbar: #2a2929;
@@ -118,11 +119,12 @@ html[dark] {
   $color-search: #000102;
 
   // List Group colors
-  $color-list: $color-global;
+  // $color-list: $color-global;
   $bgcolor-list: transparent;
-  $color-list-active: $color-reversal;
-  $bgcolor-list-active: $primary;
   $color-list-hover: $accentcolor;
+  // $bgcolor-list-hover: lighten($bgcolor-global, 3%);
+  // $color-list-active: $color-reversal;
+  // $bgcolor-list-active: $primary;
 
   // Navbar
   $bgcolor-navbar: #2a2929;

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

@@ -46,11 +46,12 @@ html[dark] {
   $color-inline-code: #c7254e; // optional
 
   // List Group colors
-  $color-list: $color-global;
+  // $color-list: $color-global;
   $bgcolor-list: $themelight;
+  $color-list-hover: lighten($accentcolor, 20%);
+  $bgcolor-list-hover: darken($bgcolor-list, 2%);
   $color-list-active: $bgcolor-global;
   $bgcolor-list-active: $accentcolor;
-  $color-list-hover: lighten($accentcolor, 20%);
 
   // Navbar
   $bgcolor-navbar: #d3687c;
@@ -103,7 +104,8 @@ html[dark] {
     }
   }
 
-  .growi.login-page {
+  // login and register
+  .nologin {
     #page-wrapper {
       background-color: $themelight;
       background-image: url('/images/themes/spring/spring.svg');
@@ -111,11 +113,19 @@ html[dark] {
       background-position: bottom;
       background-size: cover;
     }
-  }
 
-  a#login.link-switch,
-  a#register.link-switch {
-    color: $color-global;
+    .login-header,
+    .login-dialog {
+      background-color: rgba(black, 0.1);
+    }
+
+    .link-switch {
+      color: $color-global;
+    }
+
+    .grw-external-auth-form {
+      border-color: $accentcolor !important;
+    }
   }
 
   .table {

+ 26 - 12
src/client/styles/scss/theme/wood.scss

@@ -42,7 +42,6 @@ html[dark] {
 
   // Background colors
   $bgcolor-global: #ffffff;
-  $bgcolor-inline-code: #f0f0f0; //optional
   $bgcolor-card: #ece8de;
 
   // Font colors
@@ -50,15 +49,24 @@ html[dark] {
   $color-global: #433005;
   $color-reversal: #fffffc;
   $color-link: #9d7406;
-  $color-link-hover: lighten($color-link, 20%);
-  $color-link-wiki: lighten($themecolor, 5%);
-  $color-link-wiki-hover: lighten($color-link-wiki, 15%);
+  $color-link-hover: lighten($color-link, 10%);
+  $color-link-wiki: $color-link;
+  $color-link-wiki-hover: lighten($color-link-wiki, 10%);
   $color-link-nabvar: #a7a7a7;
-  $color-inline-code: #c7254e; // optional
   $color-search: white;
 
+  // Inline code
+  $bgcolor-inline-code: $themelight; //optional
+  // $color-inline-code: # !default;
+  $bordercolor-inline-code: $themecolor; //optional
+
   // List Group colors
+  // $color-list: $color-global;
+  $bgcolor-list: transparent;
   $color-list-hover: #eee;
+  $bgcolor-list-hover: darken($bgcolor-global, 3%);
+  // $color-list-active: $color-reversal;
+  // $bgcolor-list-active: $primary;
 
   // Table colors
   // $color-table: #; // optional
@@ -136,14 +144,20 @@ html[dark] {
   }
 
   // login and register
+  .nologin {
+    background: white;
 
-  .login-header,
-  .login-dialog {
-    background-color: rgba(black, 0.1);
-  }
+    .login-header,
+    .login-dialog {
+      background-color: rgba(black, 0.1);
+    }
 
-  a#login.link-switch,
-  a#register.link-switch {
-    color: rgba(black, 0.5);
+    .link-switch {
+      color: rgba(black, 0.5);
+    }
+
+    .grw-external-auth-form {
+      border-color: #aaa !important;
+    }
   }
 }

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

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

+ 6 - 2
src/server/models/page.js

@@ -335,9 +335,13 @@ module.exports = function(crowi) {
     return true;
   };
 
-  pageSchema.methods.isLiked = function(userData) {
+  pageSchema.methods.isLiked = function(user) {
+    if (user == null || user._id == null) {
+      return false;
+    }
+
     return this.liker.some((likedUserId) => {
-      return likedUserId.toString() === userData._id.toString();
+      return likedUserId.toString() === user._id.toString();
     });
   };
 

+ 1 - 1
src/server/routes/apiv3/pages.js

@@ -13,7 +13,7 @@ const router = express.Router();
  *    name: Pages
  */
 module.exports = (crowi) => {
-  const loginRequired = require('../../middleware/login-required')(crowi);
+  const loginRequired = require('../../middleware/login-required')(crowi, true);
   const adminRequired = require('../../middleware/admin-required')(crowi);
   const csrf = require('../../middleware/csrf')(crowi);
 

+ 7 - 7
src/server/routes/page.js

@@ -986,11 +986,11 @@ module.exports = function(crowi, app) {
    *        description: Get page existence
    *        parameters:
    *          - in: query
-   *            name: pages
+   *            name: pagePaths
    *            schema:
    *              type: string
-   *              description: Page paths specified by hash key in JSON format
-   *              example: '{"/": "unused value", "/user/unknown": "unused value"}'
+   *              description: Page path list in JSON Array format
+   *              example: '["/", "/user/unknown"]'
    *        responses:
    *          200:
    *            description: Succeeded to get page existence.
@@ -1017,17 +1017,17 @@ module.exports = function(crowi, app) {
    * @apiParam {String} pages (stringified JSON)
    */
   api.exist = async function(req, res) {
-    const pagesAsObj = JSON.parse(req.query.pages || '{}');
-    const pagePaths = Object.keys(pagesAsObj);
+    const pagePaths = JSON.parse(req.query.pagePaths || '[]');
 
+    const pages = {};
     await Promise.all(pagePaths.map(async(path) => {
       // check page existence
       const isExist = await Page.count({ path }) > 0;
-      pagesAsObj[path] = isExist;
+      pages[path] = isExist;
       return;
     }));
 
-    const result = { pages: pagesAsObj };
+    const result = { pages };
 
     return res.json(ApiResponse.success(result));
   };

+ 8 - 12
src/server/views/layout-growi/base/layout.html

@@ -15,20 +15,16 @@
 </header>
 {% endblock %}
 
-<div class="container-fluid">
-  <div class="row">
-    <div id="main" class="main col-md-12 {% if page %}{{ css.grant(page) }}{% endif %} {% block main_css_class %}{% endblock %}">
-      {% block content_main_before %}
-      {% endblock %}
+<div id="main" class="main container-fluid {% if page %}{{ css.grant(page) }}{% endif %} {% block main_css_class %}{% endblock %}">
+  {% block content_main_before %}
+  {% endblock %}
 
-      {% block content_main %}
-      {% endblock content_main %}
+  {% block content_main %}
+  {% endblock content_main %}
 
-      {% block content_main_after %}
-      {% endblock %}
-    </div><!-- /.main -->
-  </div><!-- /.row -->
-</div><!-- /.container-fluid -->
+  {% block content_main_after %}
+  {% endblock %}
+</div><!-- /.main -->
 
 <footer class="footer">
   {% include '../../widget/system-version.html' %}

+ 6 - 8
src/server/views/layout-growi/widget/liker-and-seenusers.html

@@ -1,16 +1,14 @@
 <div class="liker-and-seenusers">
-  <div class="text-right">
-    {% if page.liker.length > 10 %}<span class="text-muted">..</span>{% endif %}
-    <span id="liker-list" class="mr-3" data-user-ids="{{ page.liker|slice(-9)|default([])|join(',') }}"></span>
+  <div class="text-truncate text-muted text-right" style="direction: rtl;">
     <span class="text-info">
-      <i class="icon-fw icon-like"></i><span class="liker-user-count">{{ page.liker.length|default(0) }}</span>
+      <span class="liker-user-count">{{ page.liker.length|default(0) }}</span><i class="icon-fw icon-like"></i>
     </span>
+    <span id="liker-list" class="mr-1" data-user-ids="{{ page.liker|slice(-15)|default([])|reverse|join(',') }}"></span>
   </div>
-  <div class="text-right">
-    {% if page.seenUsers.length > 10 %}<span class="text-muted">..</span>{% endif %}
-    <span id="seen-user-list" class="mr-3" data-user-ids="{{ page.seenUsers|slice(-9)|default([])|join(',') }}"></span>
+  <div class="text-truncate text-muted text-right" style="direction: rtl;">
     <span class="text-danger">
-      <i class="icon-fw fa fa-paw"></i><span class="seen-user-count">{{ page.seenUsers.length|default(0) }}</span>
+      <span class="seen-user-count">{{ page.seenUsers.length|default(0) }}</span><i class="fa fa-fw fa-paw"></i>
     </span>
+    <span id="seen-user-list" class="mr-1" data-user-ids="{{ page.seenUsers|slice(-15)|default([])|reverse|join(',') }}"></span>
   </div>
 </div>

+ 7 - 5
src/server/views/layout/layout.html

@@ -118,7 +118,7 @@
     {# Sidebar #}
     <div id="grw-sidebar-wrapper"></div>
 
-    <div class="flex-grow-1">
+    <div class="flex-fill mw-0">
       {% block head_warn_alert_siteurl_undefined %}{% include '../widget/alert_siteurl_undefined.html' %}{% endblock %}
 
       {# Search #}
@@ -132,11 +132,13 @@
 
 </div><!-- /#wrapper -->
 
-<div class="grw-fixed-controls-container d-md-none d-edit-none animated fadeInUp faster">
-  <div class="grw-fixed-controls-button-container rounded-circle">
-    <div id='create-page-button-icon'></div>
+{% if user %}
+  <div class="grw-fixed-controls-container d-md-none d-edit-none animated fadeInUp faster">
+    <div class="grw-fixed-controls-button-container rounded-circle">
+      <div id='create-page-button-icon'></div>
+    </div>
   </div>
-</div>
+{% endif %}
 
 <!-- /#staff-credit -->
 <div id="staff-credit"></div>

+ 3 - 3
src/server/views/login/error.html

@@ -2,7 +2,7 @@
 
 {% block html_base_css %}error nologin{% endblock %}
 
-{% block html_title %}{{ customizeService.generateCustomTitleForFixedPageName('セットアップ') }}{% endblock %}
+{% block html_title %}{{ customizeService.generateCustomTitleForFixedPageName(t('login.Setup')) }}{% endblock %}
 
 
 
@@ -33,11 +33,11 @@
       <div class="mb-4 login-form-errors text-center">
         {% if reason === 'registered'%}
         <div class="alert alert-success">
-          <h2>登録完了</h2>
+          <h2>{{ t('login.Registration successful') }}</h2>
         </div>
         {% else %}
         <div class="alert alert-warning">
-            <h2>ログインエラー</h2>
+            <h2>{{ t('login.Sign in error') }}</h2>
         </div>
         {% endif %}
       </div>

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

@@ -9,12 +9,12 @@
   <li class="nav-item grw-nav-main-left-tab">
     <a
       {% if user %} href="#edit" role="tab" data-toggle="tab" class="nav-link edit-button" {% endif %}
-      {% if not user %} class="edit-button edit-button-disabled" data-toggle="tooltip" data-placement="top" data-container="body" title="{{ t('Not available for guest') }}" {% endif %}
+      {% if not user %} class="nav-link edit-button edit-button-disabled" data-toggle="tooltip" data-placement="top" data-container="body" title="{{ t('Not available for guest') }}" {% endif %}
     >
       <i class="icon-note"></i> {{ t('Create') }}
     </a>
   </li>
 
-  <div id="page-editor-path-nav" class="d-none d-edit-block ml-2"></div>
+  <div id="page-editor-path-nav" class="d-none d-edit-sm-block ml-2"></div>
   {% endif %}
 </ul>

+ 7 - 6
src/server/views/widget/page_alerts.html

@@ -28,21 +28,22 @@
       {% endif %}
     {% endif %}
 
-    {% if redirectFrom or req.query.renamed or req.query.redirectFrom %}
+    {% if redirectFrom or req.query.renamedFrom or req.query.redirectFrom %}
     <div class="alert alert-pink d-edit-none py-3 px-4 d-flex align-items-center justify-content-between">
       <span>
-        {% set fromPath = req.query.renamed or req.query.redirectFrom %}
+        {% set fromPath = req.query.renamedFrom or req.query.redirectFrom %}
         {% if redirectFrom or req.query.redirectFrom %}
           <strong>{{ t('Redirected') }}:</strong> {{ t('page_page.notice.redirected', fromPath | preventXss) }}
         {% endif %}
-        {% if req.query.renamed %}
+        {% if req.query.renamedFrom %}
           <strong>{{ t('Moved') }}:</strong> {{ t('page_page.notice.moved', fromPath | preventXss) }}
         {% endif %}
       </span>
-      {% if user and not page.isDeleted() %}
-        <button type="button" id="unlink-page-button" class="btn btn-secondary btn-sm float-right">
+      {% set hasRedirectLink = redirectFrom or req.query.redirectFrom or req.query.withRedirect %}
+      {% if user and not page.isDeleted() and hasRedirectLink %}
+        <button type="button" id="unlink-page-button" class="btn btn-outline-dark btn-sm float-right">
           <i class="ti-unlink" aria-hidden="true"></i>
-          Unlink
+          Unlink redirection
         </button>
       {% endif %}
     </div>

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

@@ -9,7 +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-liked="{% if user %}{{ page.isLiked(user) }}{% else %}false{% endif %}"
   data-page-is-seen="{% if page and page.isSeenUser(user) %}1{% else %}0{% endif %}"
   data-page-is-deleted="{% if page.isDeleted() %}true{% else %}false{% endif %}"
   data-page-is-deletable="{% if isDeletablePage() %}true{% else %}false{% endif %}"

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

@@ -6,7 +6,7 @@
   #}
   <li class="nav-item grw-main-nav-item-left">
     <a class="nav-link active" href="#revision-body" role="tab" data-toggle="tab">
-      <i class="icon-control-play"></i> View
+      <i class="icon-control-play icon-fw"></i><span class="d-none d-sm-inline">View</span>
     </a>
   </li>
 
@@ -15,11 +15,11 @@
       <a
         {% 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 %}
       >
-        <i class="icon-note"></i> {{ t('Edit') }}
+        <i class="icon-note icon-fw"></i><span class="d-none d-sm-inline">{{ t('Edit') }}</span>
       </a>
     </li>
 
@@ -28,16 +28,16 @@
       <a
         {% 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 %}
       >
-        <i class="fa fa-file-text-o"></i> {{ t('HackMD') }}
+        <i class="fa fa-fw fa-file-text-o"></i><span class="d-none d-sm-inline">{{ t('HackMD') }}</span>
       </a>
     </li>
     {% endif %}
 
-    <div id="page-editor-path-nav" class="d-none d-edit-block ml-2"></div>
+    <div id="page-editor-path-nav" class="d-none d-edit-sm-block ml-2"></div>
   {% endif %}
 
   {#
@@ -51,7 +51,7 @@
   {% if not page.isTopPage() %}
     <li class="nav-item">
       <a href="?presentation=1" class="nav-link toggle-presentation">
-        <i class="icon-film"></i><span class="d-none d-md-inline"> {{ t('Presentation Mode') }}</span>
+        <i class="icon-film icon-fw"></i><span class="d-none d-md-inline">{{ t('Presentation Mode') }}</span>
       </a>
     </li>
   {% endif %}
@@ -59,7 +59,7 @@
   <!-- revision-history -->
   <li class="nav-item">
     <a class="nav-link" href="#revision-history" role="tab" data-toggle="tab">
-      <i class="icon-layers"></i><span class="d-none d-md-inline"> {{ t('History') }}</span>
+      <i class="icon-layers icon-fw"></i><span class="d-none d-md-inline">{{ t('History') }}</span>
     </a>
   </li>
 

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

@@ -36,7 +36,7 @@
     </li>
     {% endif %}
 
-    <div id="page-editor-path-nav" class="d-none d-edit-block ml-2"></div>
+    <div id="page-editor-path-nav" class="d-none d-edit-sm-block ml-2"></div>
 
   {% endif %}
 

+ 12 - 4
src/server/views/widget/user_page_content.html

@@ -2,20 +2,28 @@
   <ul class="nav nav-tabs user-page-content-menu mb-4" role="tablist">
     <li class="nav-item">
       <a class="nav-link active" href="#user-bookmark-list" role="tab" data-toggle="tab">
-        <i class="icon-star"></i> Bookmarks
+        <i class="icon-star"></i>
+        <span class="d-none d-sm-inline">Bookmarks</span>
       </a>
     </li>
     <li class="nav-item">
       <a class="nav-link" href="#user-created-list" role="tab" data-toggle="tab">
-        <i class="icon-clock"></i> Recently Created
+        <i class="icon-clock"></i>
+        <span class="d-none d-sm-inline">Recently Created</span>
       </a>
     </li>
     <li class="nav-item">
-      <a class="nav-link" href="#user-draft-list" role="tab" data-toggle="tab"><i class="icon-docs"></i> My Drafts</a>
+      <a class="nav-link" href="#user-draft-list" role="tab" data-toggle="tab">
+        <i class="icon-docs"></i>
+        <span class="d-none d-sm-inline">My Drafts</span>
+      </a>
     </li>
     {% if user._id.toString() == pageUser._id.toString() %}
     <li class="nav-item">
-      <a class="nav-link" href="/me" role="tab"><i class="icon-wrench"></i> Settings</a>
+      <a class="nav-link" href="/me" role="tab">
+        <i class="icon-wrench"></i>
+        <span class="d-none d-sm-inline">Settings</span>
+      </a>
     </li>
     {% endif %}
   </ul>

+ 79 - 178
yarn.lock

@@ -29,34 +29,34 @@
     tslib "^1.9.3"
     use-memo-one "^1.1.1"
 
-"@atlaskit/avatar@^17.1.9":
-  version "17.1.9"
-  resolved "https://registry.yarnpkg.com/@atlaskit/avatar/-/avatar-17.1.9.tgz#cf452ce32040977a355246b2eb39f58572feac03"
-  integrity sha512-DZM9OVNn1XHPqx+1JaOr99dBPPMkXwlB1eyC6NkJnFuW3jdN7JoqWu9xT/eiyjcm9GV1hGUUmQ15nGfy7uhkSQ==
+"@atlaskit/avatar@^17.1.11":
+  version "17.1.11"
+  resolved "https://registry.yarnpkg.com/@atlaskit/avatar/-/avatar-17.1.11.tgz#3b34a216250dc65026a994b657bd8ea9cbb867ec"
+  integrity sha512-ETb66o66A5F8eph0U0H3mNuUd9m3OVKOdI388KAqKzhJSXa2VpdfaLA2V2mRT5tWyBpaUFF1scGE/LKDvb0/cg==
   dependencies:
     "@atlaskit/analytics-next" "^6.3.5"
     "@atlaskit/theme" "^9.5.1"
-    "@atlaskit/tooltip" "^15.2.5"
+    "@atlaskit/tooltip" "^15.2.7"
     tslib "^1.9.3"
 
-"@atlaskit/blanket@^10.0.17":
-  version "10.0.17"
-  resolved "https://registry.yarnpkg.com/@atlaskit/blanket/-/blanket-10.0.17.tgz#86cb68496b91ffa493e4dbb1f9c591986f52dcb0"
-  integrity sha512-bwpt15VxUnrCjb6ytFXENSw88RnPvdeOgbMhdp0qmQCCAhjXzo2YqIr5/PGRrPnZB/rK9s7Ce+RzN1pSlMqaFg==
+"@atlaskit/blanket@^10.0.18":
+  version "10.0.18"
+  resolved "https://registry.yarnpkg.com/@atlaskit/blanket/-/blanket-10.0.18.tgz#e7a008c8a5cc93a564083aab8cce3b4c2cec85e5"
+  integrity sha512-vwflq+p7cT0gLFABJNdV6y8Ln448qyh4VWflhP3opzyAUtnVa/LCJ5EkC1ZD0CA9uhBOeKsAQL0dwis5oIvChw==
   dependencies:
     "@atlaskit/analytics-next" "^6.3.5"
     "@atlaskit/theme" "^9.5.1"
     tslib "^1.9.3"
 
-"@atlaskit/drawer@^5.3.5":
-  version "5.3.5"
-  resolved "https://registry.yarnpkg.com/@atlaskit/drawer/-/drawer-5.3.5.tgz#eaf8d88337b62223b0c575efc3ed8e20520c7132"
-  integrity sha512-HQv7oPjXMU3XtcySOxwEiANaLZehZI2Mz8dtUu6MqshWK5c8J3yAw7BfzItTwA4sCSotLJpcDGztt1MQp9RGqw==
+"@atlaskit/drawer@^5.3.7":
+  version "5.3.7"
+  resolved "https://registry.yarnpkg.com/@atlaskit/drawer/-/drawer-5.3.7.tgz#cebf416145fd33e26d661a3dee5ecda010871c3b"
+  integrity sha512-QMdFr8yI3VvvWfeawrig+pu6S+ZQ3N0hJnoRcUzIU+9/T6w/v1a1fPR3toAMCWRmN+h3rn1sDzAdMEpt+ALqug==
   dependencies:
     "@atlaskit/analytics-next" "^6.3.5"
-    "@atlaskit/avatar" "^17.1.9"
-    "@atlaskit/blanket" "^10.0.17"
-    "@atlaskit/icon" "^20.1.0"
+    "@atlaskit/avatar" "^17.1.11"
+    "@atlaskit/blanket" "^10.0.18"
+    "@atlaskit/icon" "^20.1.1"
     "@atlaskit/item" "^11.0.2"
     "@atlaskit/portal" "^3.1.6"
     "@atlaskit/theme" "^9.5.1"
@@ -70,10 +70,10 @@
     tiny-invariant "^0.0.3"
     tslib "^1.9.3"
 
-"@atlaskit/icon@^20.1.0":
-  version "20.1.0"
-  resolved "https://registry.yarnpkg.com/@atlaskit/icon/-/icon-20.1.0.tgz#d89652672d298dd16276da237fcaf134e213f859"
-  integrity sha512-kIVSyrAo/0dZaU2BenQ7TACtafRQ7cjvQlUPD9xxuJBQuL8rx7NCB0NnK8JYfVGOxpujJ6SaeBGB1osMovukTw==
+"@atlaskit/icon@^20.1.1":
+  version "20.1.2"
+  resolved "https://registry.yarnpkg.com/@atlaskit/icon/-/icon-20.1.2.tgz#1054196d5442cb818faefe17a47c3e528bc15ae7"
+  integrity sha512-cDpE6kfiCxv4VNY4LKtRUPAdXTcx4t2eEU1K5Htm/5i6/rmJMHMITIvpZaRqF2R7XdBH5kE2MLxSfexBHC0DjQ==
   dependencies:
     "@atlaskit/theme" "^9.5.1"
     tslib "^1.9.3"
@@ -90,19 +90,19 @@
     react-addons-text-content "^0.0.4"
     uuid "^3.1.0"
 
-"@atlaskit/navigation-next@^8.0.2":
-  version "8.0.2"
-  resolved "https://registry.yarnpkg.com/@atlaskit/navigation-next/-/navigation-next-8.0.2.tgz#045839d292d21ab01655325f6942e587c00a311d"
-  integrity sha512-+YRctHYFmi9E/U/O2ehYrXTiE4IySFC3pyjHL8H5La0Lh3KWjDa8BGD+xBIhtBYIQgRW/E2hV4lgoeItq3a/UQ==
+"@atlaskit/navigation-next@^8.0.5":
+  version "8.0.5"
+  resolved "https://registry.yarnpkg.com/@atlaskit/navigation-next/-/navigation-next-8.0.5.tgz#0258dc7d7d41c7d7179e0d3c3705d64b6236641c"
+  integrity sha512-Eu8ybgNig6Yzwf4ElRfDJTqvNGr8fUVxtoIMMGJKrhiOZYQLrLTvccLcxNwrscOTQo5GfAVAkG5GGZpCZsuZ8g==
   dependencies:
     "@atlaskit/analytics-namespaced-context" "^4.1.11"
     "@atlaskit/analytics-next" "^6.3.5"
-    "@atlaskit/avatar" "^17.1.9"
-    "@atlaskit/icon" "^20.1.0"
-    "@atlaskit/select" "^11.0.9"
-    "@atlaskit/spinner" "^12.1.6"
+    "@atlaskit/avatar" "^17.1.11"
+    "@atlaskit/icon" "^20.1.1"
+    "@atlaskit/select" "^11.0.11"
+    "@atlaskit/spinner" "^12.1.7"
     "@atlaskit/theme" "^9.5.1"
-    "@atlaskit/tooltip" "^15.2.5"
+    "@atlaskit/tooltip" "^15.2.7"
     "@babel/runtime" "^7.0.0"
     "@emotion/core" "^10.0.9"
     chromatism "^2.6.0"
@@ -117,10 +117,10 @@
     shallow-equal "^1.0.0"
     unstated "^1.2.0"
 
-"@atlaskit/popper@^3.1.11":
-  version "3.1.11"
-  resolved "https://registry.yarnpkg.com/@atlaskit/popper/-/popper-3.1.11.tgz#e6dabbf6440ffaf0d8a42d9ddc7dd3bd16e88ab8"
-  integrity sha512-c6Kms/6I5bS4FWbvkarLE6jooNwfSpom0DyaDGqG8rLuFZld1YOVVDXwlLHCLq4oY0nhPi7Jl2aGz0VpGuJpLA==
+"@atlaskit/popper@^3.1.12":
+  version "3.1.12"
+  resolved "https://registry.yarnpkg.com/@atlaskit/popper/-/popper-3.1.12.tgz#9c8722d7787c847229c9cbd8232ba646eefa8353"
+  integrity sha512-KATLHu/SAAGMqjDoWX9li6x0IKYfx0Q7HvoCbGq+A5m4e8qYhZ51g6M9wWNW/eidhAlM3WAsUgEspNbLMer6AA==
   dependencies:
     memoize-one "^5.1.0"
     react-popper "1.3.6"
@@ -136,14 +136,14 @@
     tiny-invariant "^0.0.3"
     tslib "^1.9.3"
 
-"@atlaskit/select@^11.0.9":
-  version "11.0.9"
-  resolved "https://registry.yarnpkg.com/@atlaskit/select/-/select-11.0.9.tgz#9c512c2a74c97bf03a23d217d5df47e2a7f6a1f5"
-  integrity sha512-wSeVDdueOkgLYSG5CKtjTZ+6Z/9KAcXPb/VdM2HnWN8eHuZ04jUEacsKKX3z1oS63bUuPwlJdvAJhtB1GYJiww==
+"@atlaskit/select@^11.0.11":
+  version "11.0.11"
+  resolved "https://registry.yarnpkg.com/@atlaskit/select/-/select-11.0.11.tgz#56bac433d0574e446dbf2801c28aa6956d7f4896"
+  integrity sha512-97c4gocTLyxPghqkOqlwiwMf6ANaCyX/g3IMfbaS0kTPmwrfuCkQpnKMsSsuOwNU5HErOynpj0os30ED3vimlA==
   dependencies:
     "@atlaskit/analytics-next" "^6.3.5"
-    "@atlaskit/icon" "^20.1.0"
-    "@atlaskit/spinner" "^12.1.6"
+    "@atlaskit/icon" "^20.1.1"
+    "@atlaskit/spinner" "^12.1.7"
     "@atlaskit/theme" "^9.5.1"
     "@emotion/core" "^10.0.9"
     "@types/react-select" "^3.0.8"
@@ -156,10 +156,10 @@
     shallow-equal "^1.0.0"
     tslib "^1.9.3"
 
-"@atlaskit/spinner@^12.1.6":
-  version "12.1.6"
-  resolved "https://registry.yarnpkg.com/@atlaskit/spinner/-/spinner-12.1.6.tgz#bf8cfef92ebbdb87c492d087032e4096c4840078"
-  integrity sha512-pqwgpc8Gx0GMlWNqHmeRvJo1K0J7kiL59lEdtfv4drjWd2ny/wuW12AzdSrEs1gcGPjrdcwGx01bB7MCsEtRYA==
+"@atlaskit/spinner@^12.1.7":
+  version "12.1.7"
+  resolved "https://registry.yarnpkg.com/@atlaskit/spinner/-/spinner-12.1.7.tgz#ced8c614d48f2bebfea959891806ffa7466bf73f"
+  integrity sha512-fGnD6fcBW13RiS1DzGTvrm+M5Ld9Jhlw+Tx3PMs9naFpZvpTqoI5oVyTz+VDoyXhdQGKJAcfk0SntyONFZmDBg==
   dependencies:
     "@atlaskit/theme" "^9.5.1"
     react-transition-group "^2.2.1"
@@ -174,13 +174,13 @@
     prop-types "^15.5.10"
     tslib "^1.9.3"
 
-"@atlaskit/tooltip@^15.2.5":
-  version "15.2.5"
-  resolved "https://registry.yarnpkg.com/@atlaskit/tooltip/-/tooltip-15.2.5.tgz#a0bdc2e00f28cba11253c6f69df870d927dacd2e"
-  integrity sha512-qtLDqcZ58QxXkbVY9IdsiJBOdJbI3roqeehnLF7IHQ7JZkH8UcqHns+7IriKlFzN/54PKsa25o6QaWB2yKBvpQ==
+"@atlaskit/tooltip@^15.2.7":
+  version "15.2.7"
+  resolved "https://registry.yarnpkg.com/@atlaskit/tooltip/-/tooltip-15.2.7.tgz#f94dac24e98287dd49e38fce7619c6063b8ac455"
+  integrity sha512-1wS05MBcX2+1BZUt3RpqRQG/9OyHN3jFPAIgVkzpE2A3GUf68b3y482Eotk/4kW0rhDxN/NQd+U2QX21/ppQDg==
   dependencies:
     "@atlaskit/analytics-next" "^6.3.5"
-    "@atlaskit/popper" "^3.1.11"
+    "@atlaskit/popper" "^3.1.12"
     "@atlaskit/portal" "^3.1.6"
     "@atlaskit/theme" "^9.5.1"
     flushable "^1.0.0"
@@ -2974,6 +2974,13 @@ binary@~0.3.0:
     buffers "~0.1.1"
     chainsaw "~0.1.0"
 
+bindings@^1.5.0:
+  version "1.5.0"
+  resolved "https://registry.yarnpkg.com/bindings/-/bindings-1.5.0.tgz#10353c9e945334bc0511a6d90b38fbc7c9c504df"
+  integrity sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==
+  dependencies:
+    file-uri-to-path "1.0.0"
+
 bl@^3.0.0:
   version "3.0.0"
   resolved "https://registry.yarnpkg.com/bl/-/bl-3.0.0.tgz#3611ec00579fd18561754360b21e9f784500ff88"
@@ -3627,10 +3634,6 @@ chokidar@^2.0.4:
   optionalDependencies:
     fsevents "^1.2.7"
 
-chownr@^1.0.1:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.0.1.tgz#e2a75042a9551908bebd25b8523d5f9769d79181"
-
 chownr@^1.1.1:
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.1.tgz#54726b8b8fff4df053c42187e801fb4412df1494"
@@ -4603,7 +4606,7 @@ debounce@^1.0.0:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/debounce/-/debounce-1.1.0.tgz#6a1a4ee2a9dc4b7c24bb012558dbcdb05b37f408"
 
-debug@2, debug@2.6.9, debug@^2.0.0, debug@^2.1.2, debug@^2.2.0, debug@^2.3.3, debug@^2.6.8, debug@^2.6.9, debug@~2.6.4, debug@~2.6.6, debug@~2.6.9:
+debug@2, debug@2.6.9, debug@^2.0.0, debug@^2.2.0, debug@^2.3.3, debug@^2.6.8, debug@^2.6.9, debug@~2.6.4, debug@~2.6.6, debug@~2.6.9:
   version "2.6.9"
   resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
   dependencies:
@@ -4652,10 +4655,6 @@ decompress-response@^3.3.0:
   dependencies:
     mimic-response "^1.0.0"
 
-deep-extend@~0.4.0:
-  version "0.4.2"
-  resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.4.2.tgz#48b699c27e334bf89f10892be432f6e4c7d34a7f"
-
 deep-is@~0.1.3:
   version "0.1.3"
   resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34"
@@ -4737,10 +4736,6 @@ detect-indent@^5.0.0:
   resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-5.0.0.tgz#3871cc0a6a002e8c3e5b3cf7f336264675f06b9d"
   integrity sha1-OHHMCmoALow+Wzz38zYmRnXwa50=
 
-detect-libc@^1.0.2:
-  version "1.0.3"
-  resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b"
-
 detect-newline@^3.0.0:
   version "3.1.0"
   resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-3.1.0.tgz#576f5dfc63ae1a192ff192d8ad3af6308991b651"
@@ -5967,6 +5962,11 @@ file-selector@^0.1.11:
   dependencies:
     tslib "^1.9.0"
 
+file-uri-to-path@1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz#553a7b8446ff6f684359c445f1e37a05dacc33dd"
+  integrity sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==
+
 filename-regex@^2.0.0:
   version "2.0.1"
   resolved "https://registry.yarnpkg.com/filename-regex/-/filename-regex-2.0.1.tgz#c1c4b9bee3e09725ddb106b75c1e301fe2f18b26"
@@ -6255,12 +6255,6 @@ fs-extra@8.1.0:
     jsonfile "^4.0.0"
     universalify "^0.1.0"
 
-fs-minipass@^1.2.5:
-  version "1.2.5"
-  resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-1.2.5.tgz#06c277218454ec288df77ada54a03b8702aacb9d"
-  dependencies:
-    minipass "^2.2.1"
-
 fs-write-stream-atomic@^1.0.8:
   version "1.0.10"
   resolved "https://registry.yarnpkg.com/fs-write-stream-atomic/-/fs-write-stream-atomic-1.0.10.tgz#b47df53493ef911df75731e70a9ded0189db40c9"
@@ -6274,19 +6268,13 @@ fs.realpath@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
 
-fsevents@^1.2.2:
-  version "1.2.4"
-  resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.2.4.tgz#f41dcb1af2582af3692da36fc55cbd8e1041c426"
+fsevents@^1.2.2, fsevents@^1.2.7:
+  version "1.2.13"
+  resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.2.13.tgz#f325cb0455592428bcf11b383370ef70e3bfcc38"
+  integrity sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==
   dependencies:
-    nan "^2.9.2"
-    node-pre-gyp "^0.10.0"
-
-fsevents@^1.2.7:
-  version "1.2.7"
-  resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.2.7.tgz#4851b664a3783e52003b3c66eb0eee1074933aa4"
-  dependencies:
-    nan "^2.9.2"
-    node-pre-gyp "^0.10.0"
+    bindings "^1.5.0"
+    nan "^2.12.1"
 
 fsevents@^2.1.2:
   version "2.1.2"
@@ -7090,7 +7078,7 @@ iconv-lite@0.4.19, iconv-lite@~0.4.13:
   version "0.4.19"
   resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.19.tgz#f7468f60135f5e5dad3399c0a81be9a1603a082b"
 
-iconv-lite@0.4.23, iconv-lite@^0.4.4:
+iconv-lite@0.4.23:
   version "0.4.23"
   resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.23.tgz#297871f63be507adcfbfca715d0cd0eed84e9a63"
   dependencies:
@@ -7121,12 +7109,6 @@ iferr@^0.1.5:
   version "0.1.5"
   resolved "https://registry.yarnpkg.com/iferr/-/iferr-0.1.5.tgz#c60eed69e6d8fdb6b3104a1fcbca1c192dc5b501"
 
-ignore-walk@^3.0.1:
-  version "3.0.1"
-  resolved "https://registry.yarnpkg.com/ignore-walk/-/ignore-walk-3.0.1.tgz#a83e62e7d272ac0e3b551aaa82831a19b69f82f8"
-  dependencies:
-    minimatch "^3.0.4"
-
 ignore@^4.0.6:
   version "4.0.6"
   resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc"
@@ -7245,7 +7227,7 @@ inherits@2.0.1:
   version "2.0.1"
   resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.1.tgz#b17d08d326b4423e568eff719f91b0b1cbdf69f1"
 
-ini@^1.3.4, ini@^1.3.5, ini@~1.3.0:
+ini@^1.3.4, ini@^1.3.5:
   version "1.3.5"
   resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927"
 
@@ -9309,19 +9291,6 @@ minimist@~0.0.1:
   version "0.0.10"
   resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.10.tgz#de3f98543dbf96082be48ad1a0c7cda836301dcf"
 
-minipass@^2.2.1, minipass@^2.3.3:
-  version "2.3.3"
-  resolved "https://registry.yarnpkg.com/minipass/-/minipass-2.3.3.tgz#a7dcc8b7b833f5d368759cce544dccb55f50f233"
-  dependencies:
-    safe-buffer "^5.1.2"
-    yallist "^3.0.0"
-
-minizlib@^1.1.0:
-  version "1.1.0"
-  resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-1.1.0.tgz#11e13658ce46bc3a70a267aac58359d1e0c29ceb"
-  dependencies:
-    minipass "^2.2.1"
-
 mississippi@^3.0.0:
   version "3.0.0"
   resolved "https://registry.yarnpkg.com/mississippi/-/mississippi-3.0.0.tgz#ea0a3291f97e0b5e8776b363d5f0a12d94c67022"
@@ -9569,6 +9538,11 @@ mv@~2:
     ncp "~2.0.0"
     rimraf "~2.4.0"
 
+nan@^2.12.1:
+  version "2.14.1"
+  resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.1.tgz#d7be34dfa3105b91494c3147089315eff8874b01"
+  integrity sha512-isWHgVjnFjh2x2yuJ/tj3JbwoHu3UC2dX5G/88Cm24yB6YopVgxvBObDY7n5xW6ExmFhJpSEQqFPvq9zaXc8Jw==
+
 nan@^2.13.2:
   version "2.14.0"
   resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.0.tgz#7818f722027b2459a86f0295d434d1fc2336c52c"
@@ -9578,10 +9552,6 @@ nan@^2.3.3:
   version "2.8.0"
   resolved "https://registry.yarnpkg.com/nan/-/nan-2.8.0.tgz#ed715f3fe9de02b57a5e6252d90a96675e1f085a"
 
-nan@^2.9.2:
-  version "2.10.0"
-  resolved "https://registry.yarnpkg.com/nan/-/nan-2.10.0.tgz#96d0cd610ebd58d4b4de9cc0c6828cda99c7548f"
-
 nanomatch@^1.2.9:
   version "1.2.9"
   resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.9.tgz#879f7150cb2dab7a471259066c104eee6e0fa7c2"
@@ -9607,14 +9577,6 @@ ncp@~2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/ncp/-/ncp-2.0.0.tgz#195a21d6c46e361d2fb1281ba38b91e9df7bdbb3"
 
-needle@^2.2.0:
-  version "2.2.1"
-  resolved "https://registry.yarnpkg.com/needle/-/needle-2.2.1.tgz#b5e325bd3aae8c2678902fa296f729455d1d3a7d"
-  dependencies:
-    debug "^2.1.2"
-    iconv-lite "^0.4.4"
-    sax "^1.2.4"
-
 negotiator@0.6.1:
   version "0.6.1"
   resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.1.tgz#2b327184e8992101177b28563fb5e7102acd0ca9"
@@ -9777,21 +9739,6 @@ node-object-hash@^1.2.0:
   resolved "https://registry.yarnpkg.com/node-object-hash/-/node-object-hash-1.4.2.tgz#385833d85b229902b75826224f6077be969a9e94"
   integrity sha512-UdS4swXs85fCGWWf6t6DMGgpN/vnlKeSGEQ7hJcrs7PBFoxoKLmibc3QRb7fwiYsjdL7PX8iI/TMSlZ90dgHhQ==
 
-node-pre-gyp@^0.10.0:
-  version "0.10.0"
-  resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.10.0.tgz#6e4ef5bb5c5203c6552448828c852c40111aac46"
-  dependencies:
-    detect-libc "^1.0.2"
-    mkdirp "^0.5.1"
-    needle "^2.2.0"
-    nopt "^4.0.1"
-    npm-packlist "^1.1.6"
-    npmlog "^4.0.2"
-    rc "^1.1.7"
-    rimraf "^2.6.1"
-    semver "^5.3.0"
-    tar "^4"
-
 node-readfiles@^0.2.0:
   version "0.2.0"
   resolved "https://registry.yarnpkg.com/node-readfiles/-/node-readfiles-0.2.0.tgz#dbbd4af12134e2e635c245ef93ffcf6f60673a5d"
@@ -9864,13 +9811,6 @@ nopt@1.0.10:
   dependencies:
     abbrev "1"
 
-nopt@^4.0.1:
-  version "4.0.1"
-  resolved "https://registry.yarnpkg.com/nopt/-/nopt-4.0.1.tgz#d0d4685afd5415193c8c7505602d0d17cd64474d"
-  dependencies:
-    abbrev "1"
-    osenv "^0.1.4"
-
 normalize-package-data@^2.3.2, normalize-package-data@^2.3.4:
   version "2.4.0"
   resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.4.0.tgz#12f95a307d58352075a04907b84ac8be98ac012f"
@@ -9932,17 +9872,6 @@ normalize-url@^3.0.0:
   version "3.2.0"
   resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-3.2.0.tgz#98d0948afc82829f374320f405fe9ca55a5f8567"
 
-npm-bundled@^1.0.1:
-  version "1.0.3"
-  resolved "https://registry.yarnpkg.com/npm-bundled/-/npm-bundled-1.0.3.tgz#7e71703d973af3370a9591bafe3a63aca0be2308"
-
-npm-packlist@^1.1.6:
-  version "1.1.10"
-  resolved "https://registry.yarnpkg.com/npm-packlist/-/npm-packlist-1.1.10.tgz#1039db9e985727e464df066f4cf0ab6ef85c398a"
-  dependencies:
-    ignore-walk "^3.0.1"
-    npm-bundled "^1.0.1"
-
 npm-run-all@^4.1.2:
   version "4.1.2"
   resolved "https://registry.yarnpkg.com/npm-run-all/-/npm-run-all-4.1.2.tgz#90d62d078792d20669139e718621186656cea056"
@@ -9970,7 +9899,7 @@ npm-run-path@^4.0.0:
   dependencies:
     path-key "^3.0.0"
 
-"npmlog@0 || 1 || 2 || 3 || 4", npmlog@^4.0.0, npmlog@^4.0.2:
+"npmlog@0 || 1 || 2 || 3 || 4", npmlog@^4.0.0:
   version "4.1.2"
   resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.1.2.tgz#08a7f2a8bf734604779a9efa4ad5cc717abb954b"
   dependencies:
@@ -10354,13 +10283,6 @@ osenv@0:
     os-homedir "^1.0.0"
     os-tmpdir "^1.0.0"
 
-osenv@^0.1.4:
-  version "0.1.4"
-  resolved "https://registry.yarnpkg.com/osenv/-/osenv-0.1.4.tgz#42fe6d5953df06c8064be6f176c3d05aaaa34644"
-  dependencies:
-    os-homedir "^1.0.0"
-    os-tmpdir "^1.0.0"
-
 p-any@^1.1.0:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/p-any/-/p-any-1.1.0.tgz#1d03835c7eed1e34b8e539c47b7b60d0d015d4e1"
@@ -11634,15 +11556,6 @@ raw-body@^2.3.2:
     iconv-lite "0.4.23"
     unpipe "1.0.0"
 
-rc@^1.1.7:
-  version "1.2.3"
-  resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.3.tgz#51575a900f8dd68381c710b4712c2154c3e2035b"
-  dependencies:
-    deep-extend "~0.4.0"
-    ini "~1.3.0"
-    minimist "^1.2.0"
-    strip-json-comments "~2.0.1"
-
 react-addons-text-content@^0.0.4:
   version "0.0.4"
   resolved "https://registry.yarnpkg.com/react-addons-text-content/-/react-addons-text-content-0.0.4.tgz#d2e259fdc951d1d8906c08902002108dce8792e5"
@@ -12541,7 +12454,7 @@ rimraf@2, rimraf@2.6.3, rimraf@^2.6.3:
   dependencies:
     glob "^7.1.3"
 
-rimraf@^2.5.4, rimraf@^2.6.1:
+rimraf@^2.5.4:
   version "2.6.2"
   resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.2.tgz#2ed8150d24a16ea8651e6d6ef0f47c4158ce7a36"
   dependencies:
@@ -12692,7 +12605,7 @@ sax@1.2.1:
   version "1.2.1"
   resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.1.tgz#7b8e656190b228e81a66aea748480d828cd2d37a"
 
-sax@>=0.6.0, sax@^1.2.1, sax@^1.2.4, sax@~1.2.4:
+sax@>=0.6.0, sax@^1.2.1, sax@~1.2.4:
   version "1.2.4"
   resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9"
   integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==
@@ -13651,7 +13564,7 @@ strip-indent@^3.0.0:
   dependencies:
     min-indent "^1.0.0"
 
-strip-json-comments@^2.0.1, strip-json-comments@~2.0.1:
+strip-json-comments@^2.0.1:
   version "2.0.1"
   resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a"
 
@@ -13986,18 +13899,6 @@ tar@^2.0.0:
     fstream "^1.0.2"
     inherits "2"
 
-tar@^4:
-  version "4.4.4"
-  resolved "https://registry.yarnpkg.com/tar/-/tar-4.4.4.tgz#ec8409fae9f665a4355cc3b4087d0820232bb8cd"
-  dependencies:
-    chownr "^1.0.1"
-    fs-minipass "^1.2.5"
-    minipass "^2.3.3"
-    minizlib "^1.1.0"
-    mkdirp "^0.5.0"
-    safe-buffer "^5.1.2"
-    yallist "^3.0.2"
-
 teeny-request@^5.2.1:
   version "5.2.1"
   resolved "https://registry.yarnpkg.com/teeny-request/-/teeny-request-5.2.1.tgz#a6394db8359b87e64e47eeb2fbf34a65c9a751ff"
@@ -15309,7 +15210,7 @@ yallist@^2.1.2:
   version "2.1.2"
   resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52"
 
-yallist@^3.0.0, yallist@^3.0.2:
+yallist@^3.0.2:
   version "3.0.2"
   resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.0.2.tgz#8452b4bb7e83c7c188d8041c1a837c773d6d8bb9"