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

Merge branch 'imprv/update-cache-bulk' into imprv/update-cache-bulk-with-comments

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

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

+ 31 - 5
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,8 +35,12 @@
 * '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
+* Feature: Recent changes on Sidebar
 * Feature: Switch Light/Dark Mode
 * Improvement: Migrate to Bootstrap 4
 * Improvement: Copy Page URL menu item to copy path dropdown
@@ -18,10 +48,6 @@
 * Support: Upgrade libs
     * bootstrap
 
-## v3.8.2-RC
-
-*
-
 ## v3.8.1
 
 ### BREAKING CHANGES

+ 2 - 2
bin/github-actions/update-readme.sh

@@ -2,5 +2,5 @@
 
 cd docker
 
-sed -i -e "s/^\([*] \[\`\)[^\`]\+\(\`, \`3\.8\`, .\+\]\)\(.\+\/blob\/v\).\+\(\/docker\/Dockerfile.\+\)$/\1${RELEASE_VERSION}\2\3${RELEASE_VERSION}\4/" README.md
-sed -i -e "s/^\([*] \[\`\)[^\`]\+\(\`, \`3\.8-nocdn\`, .\+\]\)\(.\+\/blob\/v\).\+\(\/docker\/Dockerfile.\+\)$/\1${RELEASE_VERSION}-nocdn\2\3${RELEASE_VERSION}\4/" README.md
+sed -i -e "s/^\([*] \[\`\)[^\`]\+\(\`, \`4\.0\`, .\+\]\)\(.\+\/blob\/v\).\+\(\/docker\/Dockerfile.\+\)$/\1${RELEASE_VERSION}\2\3${RELEASE_VERSION}\4/" README.md
+sed -i -e "s/^\([*] \[\`\)[^\`]\+\(\`, \`4\.0-nocdn\`, .\+\]\)\(.\+\/blob\/v\).\+\(\/docker\/Dockerfile.\+\)$/\1${RELEASE_VERSION}-nocdn\2\3${RELEASE_VERSION}\4/" README.md

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

+ 4 - 4
docker/README.md

@@ -10,10 +10,10 @@ GROWI Official docker image
 Supported tags and respective Dockerfile links
 ------------------------------------------------
 
-* [`3.8.0`, `3.8`, `3`, `latest` (Dockerfile)](https://github.com/weseek/growi/blob/v3.8.0/docker/Dockerfile)
-* [`3.8.0-nocdn`, `3.8-nocdn`, `3-nocdn`, `latest-nocdn` (Dockerfile)](https://github.com/weseek/growi/blob/v3.8.0/docker/Dockerfile)
-* [`3.7.6`, `3.7` (Dockerfile)](https://github.com/weseek/growi/blob/v3.7.6/docker/Dockerfile)
-* [`3.7.6-nocdn`, `3.7-nocdn` (Dockerfile)](https://github.com/weseek/growi/blob/v3.7.6/docker/Dockerfile)
+* [`4.0.0`, `4.0`, `4`, `latest` (Dockerfile)](https://github.com/weseek/growi/blob/v4.0.0/docker/Dockerfile)
+* [`4.0.0-nocdn`, `4.0-nocdn`, `4-nocdn`, `latest-nocdn` (Dockerfile)](https://github.com/weseek/growi/blob/v4.0.0/docker/Dockerfile)
+* [`3.8.0`, `3.8`, `3` (Dockerfile)](https://github.com/weseek/growi/blob/v3.8.0/docker/Dockerfile)
+* [`3.8.0-nocdn`, `3.8-nocdn`, `3-nocdn` (Dockerfile)](https://github.com/weseek/growi/blob/v3.8.0/docker/Dockerfile)
 
 
 What is GROWI?

+ 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": "セットアップ"
   }
 }

+ 8 - 8
src/client/js/components/Admin/Customize/CustomizeThemeOptions.jsx

@@ -23,21 +23,21 @@ class CustomizeThemeOptions extends React.Component {
     }];
 
     const uniqueTheme = [{
-      name: 'nature',     bg: '#f9fff3', topbar: '#2a2929', sidebar: '#118050', theme: '#460039',
+      name: 'nature',     bg: '#f9fff3', topbar: '#234136', sidebar: '#118050', theme: '#460039',
     }, {
-      name: 'wood',       bg: '#fffefb', topbar: '#2a2929', sidebar: '#aaa45f', theme: '#dddebf',
+      name: 'wood',       bg: '#fffefb', topbar: '#2a2929', sidebar: '#aaa45f', theme: '#aaa45f',
     }, {
-      name: 'island',     bg: '#8ecac0', topbar: '#2a2929', sidebar: '#0c2a44', theme: '#cef2ef',
+      name: 'island',     bg: '#cef2ef', topbar: '#2a2929', sidebar: '#0c2a44', theme: 'rgba(183, 226, 219, 1)',
     }, {
-      name: 'christmas',  bg: '#fffefb', topbar: '#2a2929', sidebar: '#b3000c', theme: '#017e20',
+      name: 'christmas',  bg: '#fffefb', topbar: '#b3000c', sidebar: '#30882c', theme: '#d3c665',
     }, {
-      name: 'antarctic',  bg: '#ffffff', topbar: '#2a2929', sidebar: '#000080', theme: '#99cccc',
+      name: 'antarctic',  bg: '#ffffff', topbar: '#2a2929', sidebar: '#000080', theme: '#fa9913',
     }, {
-      name: 'spring',     bg: '#fff5ee', topbar: '#2a2929', sidebar: '#ff69b4', theme: '#ffb6c1',
+      name: 'spring',     bg: '#ffffff', topbar: '#d3687c', sidebar: '#ffb8c6', theme: '#67a856',
     }, {
-      name: 'future',     bg: '#16282D', topbar: '#2a2929', sidebar: '#011414', theme: '#04B4AE',
+      name: 'future',     bg: '#16282d', topbar: '#2a2929', sidebar: '#00b5b7', theme: '#00b5b7',
     }, {
-      name: 'halloween',  bg: '#030003', topbar: '#2a2929', sidebar: '#cc5d1f', theme: '#e9af2b',
+      name: 'halloween',  bg: '#030003', topbar: '#aa4a04', sidebar: '#162b33', theme: '#e9af2b',
     }];
     /* eslint-enable no-multi-spaces */
 

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

+ 14 - 13
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">
@@ -146,9 +147,9 @@ class GlobalNotificationList extends React.Component {
                     <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" 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>

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

@@ -101,11 +101,9 @@ class ManageGlobalNotification extends React.Component {
       <React.Fragment>
 
         <div className="my-3">
-          <a href="/admin/notification#global-notification">
-            <button type="button" className="btn page-link text-dark d-inline-block">
-              <i className="icon-fw ti-arrow-left" aria-hidden="true"></i>
-              {t('notification_setting.back_to_list')}
-            </button>
+          <a href="/admin/notification#global-notification" className="btn btn-outline-secondary">
+            <i className="icon-fw ti-arrow-left" aria-hidden="true"></i>
+            {t('notification_setting.back_to_list')}
           </a>
         </div>
 

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

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

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

+ 18 - 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" 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" 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,15 +180,23 @@ class SecuritySetting extends React.Component {
                 </span>
               </button>
               <div className="dropdown-menu" aria-labelledby="dropdownMenuButton">
-                <a className="dropdown-item" onClick={() => { adminGeneralSecurityContainer.changePageCompleteDeletionAuthority('anyOne') }}>
+                <button className="dropdown-item" type="button" onClick={() => { adminGeneralSecurityContainer.changePageCompleteDeletionAuthority('anyOne') }}>
                   {t('security_setting.anyone')}
-                </a>
-                <a className="dropdown-item" onClick={() => { adminGeneralSecurityContainer.changePageCompleteDeletionAuthority('adminOnly') }}>
+                </button>
+                <button
+                  className="dropdown-item"
+                  type="button"
+                  onClick={() => { adminGeneralSecurityContainer.changePageCompleteDeletionAuthority('adminOnly') }}
+                >
                   {t('security_setting.admin_only')}
-                </a>
-                <a className="dropdown-item" onClick={() => { adminGeneralSecurityContainer.changePageCompleteDeletionAuthority('adminAndAuthor') }}>
+                </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')}

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

@@ -90,9 +90,9 @@ class UserGroupTable extends React.Component {
                             <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" 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>

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

@@ -73,14 +73,15 @@ 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={() => {
                           return this.removeUser(relatedUser.username);
                         }}
                       >
                         <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" 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" href="#" 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" 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" 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" 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" href="#" 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" 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>
     );
   }
 

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

@@ -46,7 +46,7 @@ const EmptyTrashModal = (props) => {
       <ModalFooter>
         <ApiErrorMessage errorCode={errorCode} errorMessage={errorMessage} />
         <button type="button" className="btn btn-danger" onClick={emptyButtonHandler}>
-          <i className="icon-trash mr-2" aria-hidden="true"></i>Empty
+          <i className="icon-trash mr-2" aria-hidden="true"></i> Empty
         </button>
       </ModalFooter>
     </Modal>
@@ -67,7 +67,6 @@ EmptyTrashModal.propTypes = {
 
   isOpen: PropTypes.bool.isRequired,
   onClose: PropTypes.func.isRequired,
-  onClickEmptyBtn: PropTypes.func.isRequired,
 };
 
 export default withTranslation()(EmptyTrashModalWrapper);

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

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

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

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

@@ -288,8 +288,8 @@ class PageEditor extends React.Component {
     const emojiStrategy = this.props.appContainer.getEmojiStrategy();
 
     return (
-      <div className="d-flex">
-        <div className="page-editor-editor-container" style={{ flex: 1 }}>
+      <div className="d-flex flex-wrap">
+        <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"

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

@@ -191,7 +191,7 @@ class OptionsSelector extends React.Component {
     return (
       <DropdownItem toggle={false} onClick={this.onClickStyleActiveLine}>
         <span className="icon-container"></span>
-        <span className="menuitem-label">{ t('page_edit.Show active line') }</span>
+        <span className="menuitem-label mr-2">{ t('page_edit.Show active line') }</span>
         <span className="icon-container"><i className={iconClassName}></i></span>
       </DropdownItem>
     );

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

+ 17 - 25
src/client/js/services/PageContainer.js

@@ -138,19 +138,7 @@ export default class PageContainer extends Container {
       const { users } = await this.appContainer.apiGet('/users.list', { user_ids: userIdsStr });
       this.setState({ seenUsers: users });
 
-      const noImageCacheUsers = users.filter((user) => { return !user.imageUrlCached });
-      if (noImageCacheUsers.length === 0) {
-        return;
-      }
-
-      const noImageCacheUserIds = noImageCacheUsers.map((user) => { return user.id });
-      try {
-        await this.appContainer.apiv3Put('/users/update.imageUrlCache', { userIds: noImageCacheUserIds });
-      }
-      catch (err) {
-        // Error alert doesn't apear, because user don't need to notice this error.
-        logger.error(err);
-      }
+      await this.updateImageUrlCached(users);
     }
 
 
@@ -164,19 +152,23 @@ export default class PageContainer extends Container {
       const { users } = await this.appContainer.apiGet('/users.list', { user_ids: userIdsStr });
       this.setState({ likerUsers: users });
 
-      const noImageCacheUsers = users.filter((user) => { return !user.imageUrlCached });
-      if (noImageCacheUsers.length === 0) {
-        return;
-      }
+      await this.updateImageUrlCached(users);
+    }
+  }
 
-      const noImageCacheUserIds = noImageCacheUsers.map((user) => { return user.id });
-      try {
-        await this.appContainer.apiv3Put('/users/update.imageUrlCache', { userIds: noImageCacheUserIds });
-      }
-      catch (err) {
-        // Error alert doesn't apear, because user don't need to notice this error.
-        logger.error(err);
-      }
+  async updateImageUrlCached(users) {
+    const noImageCacheUsers = users.filter((user) => { return user.imageUrlCached == null });
+    if (noImageCacheUsers.length === 0) {
+      return;
+    }
+
+    const noImageCacheUserIds = noImageCacheUsers.map((user) => { return user.id });
+    try {
+      await this.appContainer.apiv3Put('/users/update.imageUrlCache', { userIds: noImageCacheUserIds });
+    }
+    catch (err) {
+      // Error alert doesn't apear, because user don't need to notice this error.
+      logger.error(err);
     }
   }
 

+ 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 - 7
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
@@ -206,11 +210,6 @@ body.on-edit {
           display: flex;
           align-items: center;
           justify-content: space-between;
-
-          .menuitem-label {
-            flex: 1;
-            margin-right: 10px;
-          }
         }
       }
     }

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

@@ -59,6 +59,11 @@ $modal-content-border-width: 0;
 $modal-header-padding-y: 0.75rem;
 $modal-header-padding-x: 1rem;
 
+//== Alerts
+$alert-bg-level: -2;
+$alert-border-level: 0;
+$alert-color-level: -10;
+
 //== Progress bar
 $progress-height: 4px;
 $progress-border-radius: $border-radius-sm;

+ 9 - 7
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);
   }
@@ -125,12 +131,8 @@
     overflow: hidden;
   }
 
-  // badge
-  .badge {
-    letter-spacing: 0.05em;
-    &.badge-warning {
-      // badge-warning text is white color in bootstrap3
-      color: white;
-    }
+  .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;

+ 108 - 4
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,17 +94,95 @@ 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
  */
 .page-list {
   .page-list-ul {
     > li {
-      > a strong {
-        color: lighten($color-global, 25%);
-      }
       > span.page-list-meta {
-        color: $color-global;
+        color: darken($color-global, 10%);
       }
     }
   }
@@ -114,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
  */
@@ -123,6 +220,13 @@ ul.pagination {
   }
 }
 
+/*
+ * GROWI HandsontableModal
+ */
+.grw-hot-modal-navbar {
+  background-color: $dark;
+}
+
 .wiki {
   h1 {
     border-color: lighten($border-color-theme, 10%);

+ 142 - 140
src/client/styles/scss/theme/_apply-colors-kibela.scss

@@ -1,185 +1,187 @@
 body.kibela {
-  .icon-link,
-  .CodeMirror-hint-active,
-  .grw-nav-main-left-tab,
-  .tav-pane,
-  .active {
-    color: $subthemecolor;
-  }
+  .growi:not(.login-page) {
+    .icon-link,
+    .CodeMirror-hint-active,
+    .grw-nav-main-left-tab,
+    .tav-pane,
+    .active {
+      color: $subthemecolor;
+    }
 
-  .bg-white {
-    background: #fefffe !important;
-  }
+    .bg-white {
+      background: #fefffe !important;
+    }
 
-  .bg-primary {
-    background-color: $primary !important;
-  }
+    .bg-primary {
+      background-color: $primary !important;
+    }
 
-  .grw-subnavbar {
-    background-color: rgba(lighten($bgcolor-global, 50%), 1);
-  }
+    .grw-subnavbar {
+      background-color: rgba(lighten($bgcolor-global, 50%), 1);
+    }
 
-  /* kibela block */
-  .kibela-border-top {
-    border-top: solid 0.4em $thickborder;
-  }
+    /* kibela block */
+    .kibela-border-top {
+      border-top: solid 0.4em $thickborder;
+    }
 
-  /* page wrapper */
-  #page-wrapper {
-    background-color: $bgcolor-global;
-  }
+    /* page wrapper */
+    #page-wrapper {
+      background-color: $bgcolor-global;
+    }
 
-  .search-input-group,
-  .search-typeahead {
-    .btn {
-      background-color: transparent;
+    .search-input-group,
+    .search-typeahead {
+      .btn {
+        background-color: transparent;
+      }
     }
-  }
 
-  .btn-open-dropzone {
-    background: $themelight;
-  }
+    .btn-open-dropzone {
+      background: $themelight;
+    }
 
-  /* page list */
-  .page-list {
-    background: white;
-  }
+    /* page list */
+    .page-list {
+      background: white;
+    }
 
-  .page-attachments-row {
-    background-color: #e5ecf1;
-  }
+    .page-attachments-row {
+      background-color: #e5ecf1;
+    }
 
-  /* round */
-  .round-corner-top {
-    border-top: solid 0.4em $thickborder;
-  }
+    /* round */
+    .round-corner-top {
+      border-top: solid 0.4em $thickborder;
+    }
 
-  /* admin navigation */
-  .admin-navigation {
-    .list-group-item {
-      background-color: transparent;
+    /* admin navigation */
+    .admin-navigation {
+      .list-group-item {
+        background-color: transparent;
 
-      &:hover {
-        background: #eee;
+        &:hover {
+          background: #eee;
+        }
+      }
+
+      .list-group-item.active {
+        color: white;
+        background: $bgcolor-navbar-active;
       }
     }
 
-    .list-group-item.active {
-      color: white;
-      background: $bgcolor-navbar-active;
+    /* search page */
+    .search-result-list,
+    .page-list-li {
+      background: $themelight;
     }
-  }
 
-  /* search page */
-  .search-result-list,
-  .page-list-li {
-    background: $themelight;
-  }
+    /* Tabs */
+    .nav.nav-tabs {
+      > .nav-item {
+        color: $color-link;
+        background: transparent;
 
-  /* Tabs */
-  .nav.nav-tabs {
-    > .nav-item {
-      color: $color-link;
-      background: transparent;
+        &:hover,
+        &:focus {
+          > .nav-link {
+            color: $color-link-hover;
+          }
+        }
 
-      &:hover,
-      &:focus {
         > .nav-link {
-          color: $color-link-hover;
+          color: $color-link;
         }
-      }
-
-      > .nav-link {
-        color: $color-link;
-      }
 
-      > .nav-link.active {
-        background: transparent !important;
-        border-bottom: solid 2.7px $thickborder;
+        > .nav-link.active {
+          background: transparent !important;
+          border-bottom: solid 2.7px $thickborder;
+        }
       }
     }
-  }
-
-  /* wiki */
-  .wiki {
-    h1 {
-      border-bottom: solid 2px $thickborder !important;
-    }
 
-    h2 {
-      border-color: solid 1px $thickborder;
-    }
+    /* wiki */
+    .wiki {
+      h1 {
+        border-bottom: solid 2px $thickborder !important;
+      }
 
-    // change color of highlighted header in wiki (default: orange)
-    .code-line.revision-head.highlighted {
-      color: $themelight;
-      background-color: lighten($bgcolor-theme, 20%);
+      h2 {
+        border-color: solid 1px $thickborder;
+      }
 
-      .icon-note,
-      .icon-link {
+      // change color of highlighted header in wiki (default: orange)
+      .code-line.revision-head.highlighted {
         color: $themelight;
+        background-color: lighten($bgcolor-theme, 20%);
+
+        .icon-note,
+        .icon-link {
+          color: $themelight;
+        }
       }
     }
-  }
 
-  /* Modal */
-  .modal-title {
-    color: #ffffff; // override header colors
-  }
-  .modal-content {
-    background-color: $themelight;
-  }
-
-  /* Inline Code */
-  :not(.hljs) > code:not(.hljs) {
-    color: $color-inline-code;
-    background-color: $bgcolor-inline-code;
-    border: solid 1px $bordercolor-inline-code;
-    border-radius: 0.35em;
-  }
+    /* Modal */
+    .modal-title {
+      color: #ffffff; // override header colors
+    }
+    .modal-content {
+      background-color: $themelight;
+    }
 
-  /* button */
-  .btn-primary {
-    background: $primary;
-    border: 1px solid $primary;
-  }
+    /* Inline Code */
+    :not(.hljs) > code:not(.hljs) {
+      color: $color-inline-code;
+      background-color: $bgcolor-inline-code;
+      border: solid 1px $bordercolor-inline-code;
+      border-radius: 0.35em;
+    }
 
-  /* edit */
-  .CodeMirror {
-    border: solid 1.2px #d8d8d8;
-    border-top: solid 0.3em $thickborder !important;
-  }
+    /* button */
+    .btn-primary {
+      background: $primary;
+      border: 1px solid $primary;
+    }
 
-  &.on-edit {
-    .page-editor-preview-container {
-      background: white !important;
+    /* edit */
+    .CodeMirror {
+      border: solid 1.2px #d8d8d8;
+      border-top: solid 0.3em $thickborder !important;
     }
-  }
 
-  /* navbar */
-  .grw-navbar {
-    .nav-item > .nav-link {
-      &:hover {
-        color: $color-link-nabvar-hover;
-      }
-      &:focus {
-        color: $color-link-nabvar;
+    &.on-edit {
+      .page-editor-preview-container {
+        background: white !important;
       }
     }
-    #personal-dropdown {
-      a.nav-link {
-        color: $color-global;
+
+    /* navbar */
+    .grw-navbar {
+      .nav-item > .nav-link {
+        &:hover {
+          color: $color-link-nabvar-hover;
+        }
+        &:focus {
+          color: $color-link-nabvar;
+        }
+      }
+      #personal-dropdown {
+        a.nav-link {
+          color: $color-global;
+        }
       }
     }
-  }
 
-  /* h */
-  h1,
-  h2,
-  h3,
-  h4,
-  h5,
-  h6 {
-    color: $color-header;
+    /* h */
+    h1,
+    h2,
+    h3,
+    h4,
+    h5,
+    h6 {
+      color: $color-header;
+    }
   }
 }

+ 82 - 9
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);
   }
-  .rbt-input.form-control {
-    background-color: rgba($bgcolor-global, 0.9);
+
+  .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%);
+      }
+    }
+  }
+
+  // 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,15 +108,20 @@ $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
  */
 .page-list {
   .page-list-ul {
     > li {
-      > a strong {
-        color: darken($color-global, 20%);
-      }
       > span.page-list-meta {
         color: lighten($color-global, 10%);
       }
@@ -72,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);

+ 10 - 18
src/client/styles/scss/theme/_reboot-bootstrap-theme-colors.scss

@@ -52,26 +52,18 @@ $theme-colors: map-merge($theme-colors, $colors);
   }
 }
 
-@each $theme-color, $color in $theme-colors {
-  .alert.alert-#{$theme-color} {
-    color: $color-alert;
-    background: $color;
-    border: none;
-
-    a:not(.btn) {
-      color: $bgcolor-global;
-
-      &:hover,
-      &:focus {
-        color: lighten($color, 30%);
-      }
-    }
+@each $color, $value in $theme-colors {
+  .alert-#{$color} {
+    @include alert-variant(
+      theme-color-level($color, $alert-bg-level),
+      theme-color-level($color, $alert-border-level),
+      theme-color-level($color, $alert-color-level)
+    );
   }
 }
 
-@each $theme-color, $color in $theme-colors {
-  .badge.badge-#{$theme-color} {
-    color: $color-badge;
-    background: $color;
+@each $color, $value in $theme-colors {
+  .badge-#{$color} {
+    @include badge-variant($value);
   }
 }

+ 18 - 17
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;
@@ -108,12 +109,6 @@ html[dark] {
   // admin theme box
   $color-theme-color-box: lighten($themecolor, 20%);
 
-  // alert
-  $color-alert: $color-reversal;
-
-  // badge
-  $color-badge: $color-reversal;
-
   @import 'apply-colors';
   @import 'apply-colors-light';
 
@@ -127,6 +122,18 @@ html[dark] {
       background-color: transparent;
     }
   }
+
+  // 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;
+    }
+  }
 }
 
 //== Dark Mode
@@ -175,12 +182,6 @@ html[dark] {
 //   $color-dropdown-link-active: $color-global;
 //   $color-dropdown-link-hover: $color-reversal;
 
-//   // alert
-//   $color-alert: $color-global;
-
-//   // badge
-//   $color-badge: $color-global;
-
 //   // Sidebar
 //   $bgcolor-sidebar: $bgcolor-navbar;
 //   $color-sidebar-context: $color-global;

+ 30 - 40
src/client/styles/scss/theme/christmas.scss

@@ -52,19 +52,22 @@ html[dark] {
   $color-link-nabvar: $color-reversal;
   $color-inline-code: #c7254e; // optional
 
+  // Table colors
+  $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;
@@ -72,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;
@@ -98,12 +101,6 @@ html[dark] {
   // admin theme box
   $color-theme-color-box: lighten($themecolor, 20%);
 
-  // alert
-  $color-alert: $color-reversal;
-
-  // badge
-  $color-badge: $color-reversal;
-
   @import 'apply-colors';
   @import 'apply-colors-light';
 
@@ -138,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;
     }
   }
 
@@ -166,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 - 14
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
 
@@ -95,12 +96,6 @@ html[light] {
   // admin theme box
   $color-theme-color-box: lighten($primary, 20%);
 
-  // alert
-  $color-alert: $color-reversal;
-
-  // badge
-  $color-badge: $color-reversal;
-
   @import 'apply-colors';
   @import 'apply-colors-light';
 }
@@ -129,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
 
@@ -188,12 +184,6 @@ html[dark] {
   // admin theme box
   $color-theme-color-box: $primary;
 
-  // alert
-  $color-alert: $color-reversal;
-
-  // badge
-  $color-badge: $color-reversal;
-
   @import 'apply-colors';
   @import 'apply-colors-dark';
 }

+ 22 - 15
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;
@@ -53,11 +60,17 @@ html[dark] {
   $bgcolor-sidebar-list-group: #162126; // optional
 
   // Sidebar resize button
-  $color-resize-button: #0E2329;
-  $bgcolor-resize-button: #00C2C4;
-  $color-resize-button-hover: #0E2329;
+  $color-resize-button: #0e2329;
+  $bgcolor-resize-button: #00c2c4;
+  $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;
 
@@ -73,19 +86,13 @@ html[dark] {
   // admin theme box
   $color-theme-color-box: lighten($primary, 20%);
 
-  // alert
-  $color-alert: $color-reversal;
-
-  // badge
-  $color-badge: $color-reversal;
-
   @import 'apply-colors';
   @import 'apply-colors-dark';
 
   // headers
   @for $i from 1 through 6 {
     h#{$i} {
-      color: $color-header;
+      color: white;
     }
   }
 

+ 3 - 9
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;
@@ -103,12 +104,6 @@ html[dark] {
   // admin theme box
   $color-theme-color-box: lighten($primary, 20%);
 
-  // alert
-  $color-alert: $color-reversal;
-
-  // badge
-  $color-badge: $color-reversal;
-
   @import 'apply-colors';
   @import 'apply-colors-dark';
 
@@ -121,5 +116,4 @@ html[dark] {
     color: #edba4a;
     background: #000000;
   }
-
 }

+ 43 - 122
src/client/styles/scss/theme/island.scss

@@ -1,53 +1,70 @@
 @import '../variables';
 @import '../override-bootstrap-variables';
 
-$color-themelight: rgba(183, 226, 219, 1);
 $color-primary: #97cbc3;
-$color-navbar: #0c2a44;
-$color-global: #3c6d72;
-$color-link-global: $color-global;
-$color-inline-code: #8f5313;
-$color-active-bgnav-tabs: #dbf0ed;
-$color-link-wiki: $color-global;
-$color-link-wiki-hover: rgba($color-global, 0.8);
-$bgcolor-inline-code: darken($color-themelight, 3%);
-
-$dark: darken($color-global, 5%);
+$color-themelight: rgba(183, 226, 219, 1);
 
 html[light],
 html[dark] {
+  $primary: $color-primary;
   // Background colors
   $bgcolor-card: #f5f5f5;
   $bgcolor-global: lighten($color-themelight, 10%);
   $bgcolor-inline-code: #f0f0f0; //optional
 
   // Font colors
+  $color-global: #112744;
   $color-reversal: #eeeeee;
-  $color-link: lighten($color-global, 20%);
+  // $color-header: #2b2b2b;
+  $color-link: #3c6d72;
   $color-link-hover: lighten($color-link, 20%);
+  $color-link-wiki: $color-link;
+  $color-link-wiki-hover: lighten($color-link-wiki, 20%);
   $color-link-nabvar: $color-reversal;
   $color-inline-code: #c7254e; // optional
 
   // List Group colors
-  $color-list: $color-global;
-  $bgcolor-list: lighten($color-themelight, 10%);
-  $color-list-active: $color-reversal;
-  $bgcolor-list-active: $color-primary;
-  $color-list-hover: $color-reversal;
+  // $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;
+
+  // Table colors
+  // $color-table: #; // optional
+  // $bgcolor-table: #; // optional
+  $border-color-table: $primary; // optional
+  // $color-table-hover: #; // optional
+  // $bgcolor-table-hover: #; // optional
 
   // Navbar
-  $bgcolor-navbar: #0c2a44;
-  $border-color-navbar-gradient-left: #545fff;
-  $border-color-navbar-gradient-right: #00a6e5;
+  $bgcolor-navbar: #302e2e;
+  $bgcolor-search-top-dropdown: $color-primary;
+  $border-image-navbar: linear-gradient(to right, #5ce4ef 0%, #5953eb 100%);
 
   // Logo colors
-  $bgcolor-logo: $color-navbar;
+  $bgcolor-logo: #0d3955;
   $fillcolor-logo-mark: lighten(desaturate($bgcolor-inline-code, 10%), 15%);
 
   // Sidebar
-  $bgcolor-sidebar: $color-navbar;
-  $color-sidebar-context: $color-reversal;
-  $bgcolor-sidebar-context: lighten($color-navbar, 10%);
+  $bgcolor-sidebar: #0d3955;
+  $bgcolor-sidebar-nav-item-active: rgba(#000000, 0.37);
+  // $bgcolor-sidebar-nav-item-active: rgba(#969494, 0.3); // optional
+  $text-shadow-sidebar-nav-item-active: 0px 0px 10px #0099ff; // optional
+  // Sidebar resize button
+  $color-resize-button: white;
+  $bgcolor-resize-button: $primary;
+  $color-resize-button-hover: white;
+  $bgcolor-resize-button-hover: darken($bgcolor-resize-button, 5%);
+  // Sidebar contents
+  $bgcolor-sidebar-context: #e2f3f1;
+  $color-sidebar-context: $color-link;
+  // Sidebar list group
+  $bgcolor-sidebar-list-group: #eff8f7; // optional
+
+  // Tabs
+  $bordercolor-nav-tabs: #ccc; // optional
 
   // Icon colors
   $color-editor-icons: $color-global;
@@ -62,68 +79,14 @@ html[dark] {
   $color-dropdown-link-hover: $color-global;
 
   // admin theme box
-  $color-theme-color-box: lighten($primary, 20%);
-
-  // alert
-  $color-alert: $color-reversal;
-
-  // badge
-  $color-badge: $color-reversal;
+  $color-theme-color-box: darken($primary, 15%);
 
   @import 'apply-colors';
   @import 'apply-colors-light';
 
   .wiki {
     .highlighted {
-      background-color: lighten($color-primary, 20%);
-    }
-  }
-
-  .nav-tabs,
-  .nav-tabs .nav-link.active,
-  .nav-link {
-    background: none;
-    border-color: $color-primary;
-    border-bottom-color: $color-primary;
-  }
-
-  .card,
-  .card-header {
-    background: none;
-    border: none;
-  }
-
-  .panel {
-    &,
-    &.panel-white,
-    &.panel-default {
-      color: $color-primary;
-      background-color: lighten($color-primary, 30%);
-      border-color: white;
-
-      .panel-heading {
-        color: $color-primary;
-        background-color: white;
-      }
-
-      ul {
-        li {
-          a {
-            color: darken($color-primary, 15%);
-          }
-        }
-      }
-    }
-  }
-
-  /* GROWI page list */
-  .page-list {
-    .page-list-ul {
-      > li {
-        > a strong {
-          color: $color-link-global;
-        }
-      }
+      background-color: lighten($primary, 20%);
     }
   }
 
@@ -137,48 +100,6 @@ html[dark] {
     background-attachment: fixed;
   }
 
-  /*
-   * Tabs
-   */
-  body:not(.on-edit) .nav.nav-tabs {
-    > li.active > a {
-      background: linear-gradient(
-        rgba($color-active-bgnav-tabs, 0) 0%,
-        rgba($color-active-bgnav-tabs, 0) 90%,
-        $color-active-bgnav-tabs 100%
-      ); // overwrite only the bottom pixel
-    }
-  }
-
-  /* Table */
-  .table > thead > tr > th,
-  .table > tbody > tr > th,
-  .table > tfoot > tr > th,
-  .table > thead > tr > td,
-  .table > tbody > tr > td,
-  .table > tfoot > tr > td,
-  .table > thead > tr > th,
-  .table-bordered {
-    border-top: 1px solid $color-primary;
-  }
-
-  .table-bordered > thead > tr > th,
-  .table-bordered > tbody > tr > th,
-  .table-bordered > tfoot > tr > th,
-  .table-bordered > thead > tr > td,
-  .table-bordered > tbody > tr > td,
-  .table-bordered > tfoot > tr > td {
-    border: 1px solid $color-primary;
-  }
-
-  .table > thead > tr > th {
-    border-bottom: 1px solid $color-primary;
-  }
-
-  .table-bordered {
-    border: 1px solid $color-primary;
-  }
-
   // login page
   .nologin {
     &.login-page {

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

@@ -61,12 +61,6 @@ html[dark] {
   // admin theme box
   $color-theme-color-box: lighten($bgcolor-theme, 20%);
 
-  // alert
-  $color-alert: $color-reversal;
-
-  // badge
-  $color-badge: $color-reversal;
-
   // Sidebar
   $bgcolor-sidebar: $bgcolor-theme;
   $color-sidebar-context: $color-reversal;

+ 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 - 18
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;
@@ -71,12 +72,6 @@ html[light] {
   // admin theme box
   $color-theme-color-box: lighten($primary, 20%);
 
-  // alert
-  $color-alert: $color-reversal;
-
-  // badge
-  $color-badge: $color-reversal;
-
   @import 'apply-colors';
   @import 'apply-colors-light';
 
@@ -124,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;
@@ -170,12 +166,6 @@ html[dark] {
   // admin theme box
   $color-theme-color-box: $primary;
 
-  // alert
-  $color-alert: $color-reversal;
-
-  // badge
-  $color-badge: $color-reversal;
-
   @import 'apply-colors';
   @import 'apply-colors-dark';
 

+ 3 - 67
src/client/styles/scss/theme/nature.scss

@@ -87,15 +87,12 @@ html[dark] {
   $color-dropdown-link-active: $color-reversal;
   $color-dropdown-link-hover: $color-global;
 
+  // Table colors
+  $border-color-table: #aaa; // optional
+
   // admin theme box
   $color-theme-color-box: lighten($primary, 20%);
 
-  // alert
-  $color-alert: $color-reversal;
-
-  // badge
-  $color-badge: $color-reversal;
-
   @import 'apply-colors';
   @import 'apply-colors-light';
 
@@ -119,64 +116,3 @@ html[dark] {
     }
   }
 }
-
-//== Dark Mode
-//
-// html[dark] {
-//   $primary: #d65a31;
-
-//   $basecolor: #222831;
-
-//   // Background colors
-//   $bgcolor-global: $basecolor;
-//   $bgcolor-navbar: #151515;
-//   $bgcolor-inline-code: darken($basecolor, 5%);
-//   $bgcolor-card: darken($basecolor, 5%);
-
-//   // Font colors
-//   $color-global: #eeeeee;
-//   $color-reversal: #333333;
-//   // $color-header: desaturate($primary, 20%);
-//   $color-link: $primary;
-//   $color-link-hover: lighten($color-link, 10%);
-//   $color-link-wiki: lighten($basecolor, 50%);
-//   $color-link-wiki-hover: darken($color-link-wiki, 5%);
-//   $color-link-nabvar: $color-global;
-//   $color-inline-code: #c7254e;
-
-//   // 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;
-
-//   // Logo colors
-//   $bgcolor-logo: $bgcolor-navbar;
-//   $fillcolor-logo-mark: #444;
-
-//   // Icon colors
-//   $color-editor-icons: darken($accentcolor, 15%);
-
-//   // Border colors
-//   $border-color-theme: black; // former: `$navbar-border: #ccc;`
-
-//   // Dropdown colors
-//   $bgcolor-dropdown-link-active: $primary;
-//   $color-dropdown-link-active: $color-global;
-//   $color-dropdown-link-hover: $color-reversal;
-
-//   // alert
-//   $color-alert: $color-global;
-
-//   // badge
-//   $color-badge: $color-global;
-
-//   // Sidebar
-//   $bgcolor-sidebar: $bgcolor-navbar;
-//   $color-sidebar-context: $color-global;
-//   $bgcolor-sidebar-context: lighten($bgcolor-navbar, 5%);
-
-//   @import 'apply-colors';
-//   @import 'apply-colors-dark';
-// }

+ 38 - 89
src/client/styles/scss/theme/spring.scss

@@ -13,26 +13,6 @@
 // $light: #;
 // $dark: #;
 
-.growi:not(.login-page) {
-  // add background-image
-  #page-wrapper,
-  .page-editor-preview-container {
-    background-image: url('/images/themes/spring/spring02.svg');
-    background-attachment: fixed;
-    background-position: bottom;
-    background-size: cover;
-  }
-}
-
-.growi.login-page {
-  #page-wrapper {
-    background-image: url('/images/themes/spring/spring.svg');
-    background-attachment: fixed;
-    background-position: bottom;
-    background-size: cover;
-  }
-}
-
 $themecolor: #ffb8c6;
 $themelight: #fff0f5;
 $subthemecolor: #67a856;
@@ -66,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;
@@ -109,15 +90,44 @@ html[dark] {
   // admin theme box
   $color-theme-color-box: darken($primary, 20%);
 
-  // alert
-  $color-alert: $color-reversal;
-
-  // badge
-  $color-badge: $color-reversal;
-
   @import 'apply-colors';
   @import 'apply-colors-light';
 
+  .growi:not(.login-page) {
+    // add background-image
+    #page-wrapper,
+    .page-editor-preview-container {
+      background-image: url('/images/themes/spring/spring02.svg');
+      background-attachment: fixed;
+      background-position: bottom;
+      background-size: cover;
+    }
+  }
+
+  // login and register
+  .nologin {
+    #page-wrapper {
+      background-color: $themelight;
+      background-image: url('/images/themes/spring/spring.svg');
+      background-attachment: fixed;
+      background-position: bottom;
+      background-size: cover;
+    }
+
+    .login-header,
+    .login-dialog {
+      background-color: rgba(black, 0.1);
+    }
+
+    .link-switch {
+      color: $color-global;
+    }
+
+    .grw-external-auth-form {
+      border-color: $accentcolor !important;
+    }
+  }
+
   .table {
     background-color: $bgcolor-global;
   }
@@ -139,64 +149,3 @@ html[dark] {
     }
   }
 }
-
-//== Dark Mode
-//
-// html[dark] {
-//   $primary: #d65a31;
-
-//   $basecolor: #222831;
-
-//   // Background colors
-//   $bgcolor-global: $basecolor;
-//   $bgcolor-navbar: #151515;
-//   $bgcolor-inline-code: darken($basecolor, 5%);
-//   $bgcolor-card: darken($basecolor, 5%);
-
-//   // Font colors
-//   $color-global: #eeeeee;
-//   $color-reversal: #333333;
-//   // $color-header: desaturate($primary, 20%);
-//   $color-link: $primary;
-//   $color-link-hover: lighten($color-link, 10%);
-//   $color-link-wiki: lighten($basecolor, 50%);
-//   $color-link-wiki-hover: darken($color-link-wiki, 5%);
-//   $color-link-nabvar: $color-global;
-//   $color-inline-code: #c7254e;
-
-//   // 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;
-
-//   // Logo colors
-//   $bgcolor-logo: $bgcolor-navbar;
-//   $fillcolor-logo-mark: #444;
-
-//   // Icon colors
-//   $color-editor-icons: darken($accentcolor, 15%);
-
-//   // Border colors
-//   $border-color-theme: black; // former: `$navbar-border: #ccc;`
-
-//   // Dropdown colors
-//   $bgcolor-dropdown-link-active: $primary;
-//   $color-dropdown-link-active: $color-global;
-//   $color-dropdown-link-hover: $color-reversal;
-
-//   // alert
-//   $color-alert: $color-global;
-
-//   // badge
-//   $color-badge: $color-global;
-
-//   // Sidebar
-//   $bgcolor-sidebar: $bgcolor-navbar;
-//   $color-sidebar-context: $color-global;
-//   $bgcolor-sidebar-context: lighten($bgcolor-navbar, 5%);
-
-//   @import 'apply-colors';
-//   @import 'apply-colors-dark';
-// }

+ 57 - 92
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,20 +49,36 @@ 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
+  // $bgcolor-table: #; // optional
+  $border-color-table: #aaa; // optional
+  // $color-table-hover: #; // optional
+  // $bgcolor-table-hover: #; // optional
 
   // Navbar
   $bgcolor-navbar: #2a2929;
   $bgcolor-search-top-dropdown: $themecolor;
-  $border-image-navbar: linear-gradient(to right, #5c78ef 0%, #16bc42 50%, #5c78ef 100%);
+  $border-image-navbar: linear-gradient(to right, $themecolor 0%, darken($themecolor, 20%) 100%);
 
   // Logo colors
   $bgcolor-logo: darken($themecolor, 10%);
@@ -71,12 +86,12 @@ html[dark] {
   $color-editor-icons: $color-global;
 
   // Sidebar
-  $bgcolor-sidebar: $bgcolor-navbar;
+  $bgcolor-sidebar: transparent;
   // Sidebar contents
   $color-sidebar-context: #9d7406;
-  $bgcolor-sidebar-context: #f0efe7;
+  $bgcolor-sidebar-context: transparent;
   // Sidebar list group
-  $bgcolor-sidebar-list-group: #f7f5f1;
+  $bgcolor-sidebar-list-group: rgba(#f7f5f1, 0.5);
   // Sidebar resize button
   $color-resize-button: white;
   $bgcolor-resize-button: $themecolor;
@@ -96,103 +111,53 @@ html[dark] {
   // alert
   $color-alert: $color-reversal;
 
-  // badge
-  $color-badge: $color-reversal;
-
   // portal
   $info: lighten($themecolor, 10%);
 
   @import 'apply-colors';
   @import 'apply-colors-light';
 
-  // hljs
-  .hljs {
-    color: #433005;
-    background-color: $bgcolor-inline-code;
+  /*
+   * Modal
+   */
+  .modal-dialog .modal-header.bg-primary {
+    background-image: url('/images/themes/wood/wood-navbar.jpg');
   }
 
-  // List
-  .list-group .list-group-item {
-    background: transparent;
-  }
+  // Sidebar
+  .grw-sidebar {
+    div[data-testid='GlobalNavigation'] {
+      background-image: url('/images/themes/wood/wood-navbar.jpg');
 
-  // Search Top
-  .search-top {
-    .btn-secondary.dropdown-toggle {
-      color: $color-search;
+      button,
+      .btn {
+        border: none;
+      }
+    }
+    div[data-testid='ContextualNavigation'] {
+      > div {
+        background-color: rgba(white, 0.6);
+        background-image: url('/images/themes/wood/wood-navbar.jpg');
+        background-blend-mode: lighten;
+      }
     }
   }
 
-  // Sidebar
-  .grw-sidebar div[data-testid='GlobalNavigation'] {
-    * {
-      background-image: url('/images/themes/wood/wood-navbar.jpg');
+  // login and register
+  .nologin {
+    background: white;
+
+    .login-header,
+    .login-dialog {
+      background-color: rgba(black, 0.1);
     }
 
-    button,
-    .btn {
-      border: none;
+    .link-switch {
+      color: rgba(black, 0.5);
+    }
+
+    .grw-external-auth-form {
+      border-color: #aaa !important;
     }
   }
 }
-
-//== Dark Mode
-//
-// html[dark] {
-//   $primary: #d65a31;
-
-//   $basecolor: #222831;
-
-//   // Background colors
-//   $bgcolor-global: $basecolor;
-//   $bgcolor-navbar: #151515;
-//   $bgcolor-inline-code: darken($basecolor, 5%);
-//   $bgcolor-card: darken($basecolor, 5%);
-
-//   // Font colors
-//   $color-global: #eeeeee;
-//   $color-reversal: #333333;
-//   // $color-header: desaturate($primary, 20%);
-//   $color-link: $primary;
-//   $color-link-hover: lighten($color-link, 10%);
-//   $color-link-wiki: lighten($basecolor, 50%);
-//   $color-link-wiki-hover: darken($color-link-wiki, 5%);
-//   $color-link-nabvar: $color-global;
-//   $color-inline-code: #c7254e;
-
-//   // 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;
-
-//   // Logo colors
-//   $bgcolor-logo: $bgcolor-navbar;
-//   $fillcolor-logo-mark: #444;
-
-//   // Icon colors
-//   $color-editor-icons: darken($accentcolor, 15%);
-
-//   // Border colors
-//   $border-color-theme: black; // former: `$navbar-border: #ccc;`
-
-//   // Dropdown colors
-//   $bgcolor-dropdown-link-active: $primary;
-//   $color-dropdown-link-active: $color-global;
-//   $color-dropdown-link-hover: $color-reversal;
-
-//   // alert
-//   $color-alert: $color-global;
-
-//   // badge
-//   $color-badge: $color-global;
-
-//   // Sidebar
-//   $bgcolor-sidebar: $bgcolor-navbar;
-//   $color-sidebar-context: $color-global;
-//   $bgcolor-sidebar-context: lighten($bgcolor-navbar, 5%);
-
-//   @import 'apply-colors';
-//   @import 'apply-colors-dark';
-// }

+ 3 - 4
src/lib/components/PagePathHierarchicalLink.jsx

@@ -46,13 +46,13 @@ 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 (
     <RootElm>
       { isParentExists && (
-        <PagePathHierarchicalLink linkedPagePath={linkedPagePath.parent} basePath={basePath} isInnerElem />
+        <PagePathHierarchicalLink linkedPagePath={linkedPagePath.parent} basePath={basePath} isInTrash={isInTrash || linkedPagePath.isInTrash} isInnerElem />
       ) }
       { isSeparatorRequired && (
         <span className="separator">/</span>
@@ -66,11 +66,10 @@ const PagePathHierarchicalLink = (props) => {
 PagePathHierarchicalLink.propTypes = {
   linkedPagePath: PropTypes.instanceOf(LinkedPagePath).isRequired,
   basePath: PropTypes.string,
+  isInTrash: PropTypes.bool,
 
   // !!INTERNAL USE ONLY!!
   isInnerElem: PropTypes.bool,
-
-  isInTrash: PropTypes.bool, // TODO: omit
 };
 
 export default PagePathHierarchicalLink;

+ 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 - 0
src/server/routes/apiv3/index.js

@@ -37,6 +37,7 @@ module.exports = (crowi) => {
 
   router.use('/search', require('./search')(crowi));
 
+  router.use('/page', require('./page')(crowi));
   router.use('/pages', require('./pages')(crowi));
 
   router.use('/bookmarks', require('./bookmarks')(crowi));

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

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

@@ -13,7 +13,7 @@
 
     <div id="main" class="main col-12 kibela-block round-corner {% if page %}{{ css.grant(page) }}{% endif %}{% block main_css_class %}{% endblock %}">
       <div class="row grw-subnav d-edit-none">
-        <div class="col px-0 mx-0 bg-white kibela-border-top round-corner">
+        <div class="col-12 col-xl-9 col-lg-8 px-0 mx-0 bg-white kibela-border-top round-corner">
           {% block content_header %} {% endblock %}
         </div>
         <div class="col-xl-3 col-lg-4"></div>

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

@@ -13,7 +13,7 @@
 
 {% block content_main %}
   <div class="row">
-    <div class="col bg-white round-corner">
+    <div class="col-12 col-xl-9 col-lg-8 bg-white round-corner">
       {% include '../widget/forbidden_content.html' %}
     </div>
   </div>

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

@@ -13,7 +13,7 @@
 
 {% block content_main %}
   <div class="row">
-    <div class="col bg-white round-corner">
+    <div class="col-12 col-xl-9 col-lg-8 bg-white round-corner">
       {% include '../widget/not_creatable_content.html' %}
     </div>
   </div>

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

@@ -13,7 +13,7 @@
 
 {% block content_main %}
   <div class="row">
-    <div class="col bg-white round-corner">
+    <div class="col-12 col-xl-9 col-lg-8 bg-white round-corner">
       {% include '../widget/not_found_content.html' %}
     </div>
     <div class="col-xl-3 col-lg-4 d-none d-lg-block revision-toc-container"></div>

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

@@ -13,7 +13,7 @@
 {% block content_main %}
 <div class="row">
 
-  <div class="col bg-white round-corner">
+  <div class="col-12 col-xl-9 col-lg-8 bg-white round-corner">
 
     {% include '../widget/page_content.html' %}
     {# force remove #revision-toc from #content_main of parent #}

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