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

Merge pull request #2284 from weseek/dev/4.0.x

Dev/4.0.x
Yuki Takei пре 5 година
родитељ
комит
5695fe0bb6
100 измењених фајлова са 2934 додато и 2422 уклоњено
  1. 1 0
      .eslintrc.js
  2. 18 2
      CHANGES.md
  3. 16 0
      babel.config.js
  4. 2 2
      bin/github-actions/update-readme.sh
  5. 1 0
      config/logger/config.dev.js
  6. 3 5
      config/webpack.common.js
  7. 49 0
      config/webpack.dev.dll.js
  8. 4 4
      docker/README.md
  9. 11 6
      package.json
  10. 18 1
      resource/cdn-manifests.js
  11. 8 16
      resource/locales/en-US/admin/admin.json
  12. 0 135
      resource/locales/en-US/sandbox-bootstrap3.md
  13. 253 0
      resource/locales/en-US/sandbox-bootstrap4.md
  14. 7 7
      resource/locales/en-US/sandbox.md
  15. 20 10
      resource/locales/en-US/translation.json
  16. 5 7
      resource/locales/en-US/welcome.md
  17. 8 16
      resource/locales/ja/admin/admin.json
  18. 0 135
      resource/locales/ja/sandbox-bootstrap3.md
  19. 253 0
      resource/locales/ja/sandbox-bootstrap4.md
  20. 8 8
      resource/locales/ja/sandbox.md
  21. 24 14
      resource/locales/ja/translation.json
  22. 6 8
      resource/locales/ja/welcome.md
  23. 14 8
      src/client/js/admin.jsx
  24. 24 16
      src/client/js/app.jsx
  25. 15 4
      src/client/js/bootstrap.jsx
  26. 2 2
      src/client/js/components/Admin/AdminHome/AdminHome.jsx
  27. 1 1
      src/client/js/components/Admin/AdminHome/SystemInfomationTable.jsx
  28. 24 23
      src/client/js/components/Admin/App/AppSetting.jsx
  29. 5 5
      src/client/js/components/Admin/App/AppSettingsPage.jsx
  30. 17 17
      src/client/js/components/Admin/App/AwsSetting.jsx
  31. 11 11
      src/client/js/components/Admin/App/MailSetting.jsx
  32. 6 5
      src/client/js/components/Admin/App/PluginSetting.jsx
  33. 40 42
      src/client/js/components/Admin/App/SiteUrlSetting.jsx
  34. 106 35
      src/client/js/components/Admin/Common/AdminNavigation.jsx
  35. 9 13
      src/client/js/components/Admin/Common/AdminUpdateButtonRow.jsx
  36. 5 5
      src/client/js/components/Admin/Common/ProgressBar.jsx
  37. 0 4
      src/client/js/components/Admin/Customize/Customize.jsx
  38. 0 38
      src/client/js/components/Admin/Customize/CustomizeBehaviorOption.jsx
  39. 0 95
      src/client/js/components/Admin/Customize/CustomizeBehaviorSetting.jsx
  40. 24 19
      src/client/js/components/Admin/Customize/CustomizeCssSetting.jsx
  41. 3 2
      src/client/js/components/Admin/Customize/CustomizeFunctionOption.jsx
  42. 127 109
      src/client/js/components/Admin/Customize/CustomizeFunctionSetting.jsx
  43. 35 29
      src/client/js/components/Admin/Customize/CustomizeHeaderSetting.jsx
  44. 63 44
      src/client/js/components/Admin/Customize/CustomizeHighlightSetting.jsx
  45. 9 3
      src/client/js/components/Admin/Customize/CustomizeLayoutOption.jsx
  46. 17 29
      src/client/js/components/Admin/Customize/CustomizeLayoutOptions.jsx
  47. 14 6
      src/client/js/components/Admin/Customize/CustomizeLayoutSetting.jsx
  48. 57 42
      src/client/js/components/Admin/Customize/CustomizeScriptSetting.jsx
  49. 51 50
      src/client/js/components/Admin/Customize/CustomizeThemeOptions.jsx
  50. 44 20
      src/client/js/components/Admin/Customize/CustomizeTitle.jsx
  51. 11 10
      src/client/js/components/Admin/Customize/ThemeColorBox.jsx
  52. 7 7
      src/client/js/components/Admin/ElasticsearchManagement/ElasticsearchManagement.jsx
  53. 2 2
      src/client/js/components/Admin/ElasticsearchManagement/NormalizeIndicesControls.jsx
  54. 2 2
      src/client/js/components/Admin/ElasticsearchManagement/RebuildIndexControls.jsx
  55. 2 2
      src/client/js/components/Admin/ElasticsearchManagement/ReconnectControls.jsx
  56. 24 31
      src/client/js/components/Admin/ElasticsearchManagement/StatusTable.jsx
  57. 31 29
      src/client/js/components/Admin/ExportArchiveData/ArchiveFilesTable.jsx
  58. 8 12
      src/client/js/components/Admin/ExportArchiveData/ArchiveFilesTableMenu.jsx
  59. 46 42
      src/client/js/components/Admin/ExportArchiveData/SelectCollectionsModal.jsx
  60. 1 1
      src/client/js/components/Admin/ExportArchiveDataPage.jsx
  61. 7 7
      src/client/js/components/Admin/ImportData/GrowiArchive/ErrorViewer.jsx
  62. 46 31
      src/client/js/components/Admin/ImportData/GrowiArchive/ImportCollectionConfigurationModal.jsx
  63. 19 21
      src/client/js/components/Admin/ImportData/GrowiArchive/ImportCollectionItem.jsx
  64. 6 5
      src/client/js/components/Admin/ImportData/GrowiArchive/ImportForm.jsx
  65. 8 6
      src/client/js/components/Admin/ImportData/GrowiArchive/UploadForm.jsx
  66. 34 34
      src/client/js/components/Admin/ImportDataPage.jsx
  67. 1 1
      src/client/js/components/Admin/ManageExternalAccount.jsx
  68. 32 42
      src/client/js/components/Admin/MarkdownSetting/LineBreakForm.jsx
  69. 16 15
      src/client/js/components/Admin/MarkdownSetting/MarkDownSetting.jsx
  70. 72 53
      src/client/js/components/Admin/MarkdownSetting/PresentationForm.jsx
  71. 0 86
      src/client/js/components/Admin/MarkdownSetting/PresentationLineBreakOptions.jsx
  72. 4 4
      src/client/js/components/Admin/MarkdownSetting/WhiteListInput.jsx
  73. 86 74
      src/client/js/components/Admin/MarkdownSetting/XssForm.jsx
  74. 12 11
      src/client/js/components/Admin/Notification/GlobalNotification.jsx
  75. 48 39
      src/client/js/components/Admin/Notification/GlobalNotificationList.jsx
  76. 143 106
      src/client/js/components/Admin/Notification/ManageGlobalNotification.jsx
  77. 13 15
      src/client/js/components/Admin/Notification/NotificationDeleteModal.jsx
  78. 62 24
      src/client/js/components/Admin/Notification/NotificationSetting.jsx
  79. 35 35
      src/client/js/components/Admin/Notification/SlackAppConfiguration.jsx
  80. 4 3
      src/client/js/components/Admin/Notification/TriggerEventCheckBox.jsx
  81. 4 4
      src/client/js/components/Admin/Notification/UserNotificationRow.jsx
  82. 22 14
      src/client/js/components/Admin/Notification/UserTriggerNotification.jsx
  83. 13 13
      src/client/js/components/Admin/Security/BasicSecuritySetting.jsx
  84. 22 22
      src/client/js/components/Admin/Security/GitHubSecuritySetting.jsx
  85. 22 22
      src/client/js/components/Admin/Security/GoogleSecuritySetting.jsx
  86. 13 11
      src/client/js/components/Admin/Security/LdapAuthTest.jsx
  87. 11 9
      src/client/js/components/Admin/Security/LdapAuthTestModal.jsx
  88. 107 79
      src/client/js/components/Admin/Security/LdapSecuritySetting.jsx
  89. 57 70
      src/client/js/components/Admin/Security/LocalSecuritySetting.jsx
  90. 60 58
      src/client/js/components/Admin/Security/OidcSecuritySetting.jsx
  91. 44 42
      src/client/js/components/Admin/Security/SamlSecuritySetting.jsx
  92. 131 63
      src/client/js/components/Admin/Security/SecurityManagement.jsx
  93. 145 147
      src/client/js/components/Admin/Security/SecuritySetting.jsx
  94. 22 22
      src/client/js/components/Admin/Security/TwitterSecuritySetting.jsx
  95. 1 1
      src/client/js/components/Admin/UserGroup/UserGroupCreateForm.jsx
  96. 14 15
      src/client/js/components/Admin/UserGroup/UserGroupDeleteModal.jsx
  97. 16 17
      src/client/js/components/Admin/UserGroup/UserGroupTable.jsx
  98. 3 3
      src/client/js/components/Admin/UserGroupDetail/CheckBoxForSerchUserOption.jsx
  99. 3 3
      src/client/js/components/Admin/UserGroupDetail/RadioButtonForSerchUserOption.jsx
  100. 4 4
      src/client/js/components/Admin/UserGroupDetail/UserGroupDetailPage.jsx

+ 1 - 0
.eslintrc.js

@@ -13,6 +13,7 @@ module.exports = {
     jquery: true,
     jquery: true,
     emojione: true,
     emojione: true,
     hljs: true,
     hljs: true,
+    ScrollPosStyler: true,
     window: true,
     window: true,
   },
   },
   plugins: [
   plugins: [

+ 18 - 2
CHANGES.md

@@ -1,8 +1,24 @@
 # CHANGES
 # CHANGES
 
 
-## v3.8.2-RC
+## v4.0.0-RC
 
 
-*
+### BREAKING CHANGES
+
+* Crowi Classic Behavior is removed
+* Crowi Classic Layout is removed
+* '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
+
+### 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
+* Improvement: Show contributors by Bootstrap Modal
+* Support: Upgrade libs
+    * bootstrap
 
 
 ## v3.8.1
 ## v3.8.1
 
 

+ 16 - 0
babel.config.js

@@ -14,6 +14,22 @@ module.exports = function(api) {
   ];
   ];
   const plugins = [
   const plugins = [
     'lodash',
     'lodash',
+    // transform
+    //  from `import { Button } from 'reactstrap';`
+    //  to   `import Row from 'reactstrap/Button';`
+    [
+      'transform-imports', {
+        reactstrap: {
+          // eslint-disable-next-line no-template-curly-in-string
+          transform: 'reactstrap/es/${member}',
+          preventFullImport: true,
+        },
+      },
+    ],
+    '@babel/plugin-proposal-optional-chaining',
+    [
+      '@babel/plugin-proposal-class-properties', { loose: true },
+    ],
   ];
   ];
 
 
   return {
   return {

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

@@ -2,5 +2,5 @@
 
 
 cd docker
 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

+ 1 - 0
config/logger/config.dev.js

@@ -33,4 +33,5 @@ module.exports = {
   'growi:app': 'debug',
   'growi:app': 'debug',
   'growi:services:*': 'debug',
   'growi:services:*': 'debug',
   // 'growi:StaffCredit': 'debug',
   // 'growi:StaffCredit': 'debug',
+  // 'growi:TableOfContents': 'debug',
 };
 };

+ 3 - 5
config/webpack.common.js

@@ -22,7 +22,7 @@ module.exports = (options) => {
     entry: Object.assign({
     entry: Object.assign({
       'js/app':                       './src/client/js/app',
       'js/app':                       './src/client/js/app',
       'js/admin':                     './src/client/js/admin',
       'js/admin':                     './src/client/js/admin',
-      'js/installer':                 './src/client/js/installer',
+      'js/nologin':                   './src/client/js/nologin',
       'js/legacy':                    './src/client/js/legacy/crowi',
       'js/legacy':                    './src/client/js/legacy/crowi',
       'js/legacy-presentation':       './src/client/js/legacy/crowi-presentation',
       'js/legacy-presentation':       './src/client/js/legacy/crowi-presentation',
       'js/plugin':                    './src/client/js/plugin',
       'js/plugin':                    './src/client/js/plugin',
@@ -34,18 +34,16 @@ module.exports = (options) => {
       'styles/style-presentation':    './src/client/styles/scss/style-presentation.scss',
       'styles/style-presentation':    './src/client/styles/scss/style-presentation.scss',
       // themes
       // themes
       'styles/theme-default':         './src/client/styles/scss/theme/default.scss',
       'styles/theme-default':         './src/client/styles/scss/theme/default.scss',
-      'styles/theme-default-dark':    './src/client/styles/scss/theme/default-dark.scss',
       'styles/theme-nature':          './src/client/styles/scss/theme/nature.scss',
       'styles/theme-nature':          './src/client/styles/scss/theme/nature.scss',
       'styles/theme-mono-blue':       './src/client/styles/scss/theme/mono-blue.scss',
       'styles/theme-mono-blue':       './src/client/styles/scss/theme/mono-blue.scss',
       'styles/theme-future':          './src/client/styles/scss/theme/future.scss',
       'styles/theme-future':          './src/client/styles/scss/theme/future.scss',
-      'styles/theme-blue-night':      './src/client/styles/scss/theme/blue-night.scss',
       'styles/theme-kibela':          './src/client/styles/scss/theme/kibela.scss',
       'styles/theme-kibela':          './src/client/styles/scss/theme/kibela.scss',
       'styles/theme-halloween':       './src/client/styles/scss/theme/halloween.scss',
       'styles/theme-halloween':       './src/client/styles/scss/theme/halloween.scss',
-      'styles/theme-wood':          './src/client/styles/scss/theme/wood.scss',
       'styles/theme-christmas':          './src/client/styles/scss/theme/christmas.scss',
       'styles/theme-christmas':          './src/client/styles/scss/theme/christmas.scss',
+      'styles/theme-wood':          './src/client/styles/scss/theme/wood.scss',
       'styles/theme-island':      './src/client/styles/scss/theme/island.scss',
       'styles/theme-island':      './src/client/styles/scss/theme/island.scss',
-      'styles/theme-spring':      './src/client/styles/scss/theme/spring.scss',
       'styles/theme-antarctic':      './src/client/styles/scss/theme/antarctic.scss',
       'styles/theme-antarctic':      './src/client/styles/scss/theme/antarctic.scss',
+      'styles/theme-spring':         './src/client/styles/scss/theme/spring.scss',
       // styles for external services
       // styles for external services
       'styles/style-hackmd':          './src/client/styles/hackmd/style.scss',
       'styles/style-hackmd':          './src/client/styles/hackmd/style.scss',
     }, options.entry || {}), // Merge with env dependent settings
     }, options.entry || {}), // Merge with env dependent settings

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

@@ -0,0 +1,49 @@
+/**
+ * @author: Yuki Takei <yuki@weseek.co.jp>
+ */
+const webpack = require('webpack');
+const helpers = require('../src/lib/util/helpers');
+
+
+module.exports = {
+  mode: 'development',
+  entry: {
+    dlls: [
+      // Libraries
+      'axios',
+      'browser-bunyan', 'bunyan-format',
+      'codemirror', 'react-codemirror2',
+      'date-fns',
+      'diff2html',
+      'debug',
+      'entities',
+      'growi-commons',
+      'i18next', 'i18next-browser-languagedetector',
+      'jquery-slimscroll',
+      'lodash', 'pako',
+      'markdown-it', 'csv-to-markdown-table',
+      'react', 'react-dom',
+      'reactstrap', 'react-bootstrap-typeahead',
+      'react-i18next', 'react-dropzone', 'react-hotkeys', 'react-copy-to-clipboard', 'react-waypoint',
+      'socket.io-client',
+      'toastr',
+      'unstated',
+      'xss',
+    ],
+  },
+  output: {
+    path: helpers.root('public/dll'),
+    filename: 'dll.js',
+    library: 'growi_dlls',
+  },
+  resolve: {
+    extensions: ['.js', '.json'],
+    modules: [helpers.root('src'), helpers.root('node_modules')],
+  },
+  plugins: [
+    new webpack.DllPlugin({
+      path: helpers.root('public/dll/manifest.json'),
+      name: 'growi_dlls',
+    }),
+  ],
+};

+ 4 - 4
docker/README.md

@@ -10,10 +10,10 @@ GROWI Official docker image
 Supported tags and respective Dockerfile links
 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?
 What is GROWI?

+ 11 - 6
package.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "growi",
   "name": "growi",
-  "version": "3.8.2-RC",
+  "version": "4.0.0-RC",
   "description": "Team collaboration software using markdown",
   "description": "Team collaboration software using markdown",
   "tags": [
   "tags": [
     "wiki",
     "wiki",
@@ -98,7 +98,6 @@
     "express": "^4.16.1",
     "express": "^4.16.1",
     "express-bunyan-logger": "^1.3.3",
     "express-bunyan-logger": "^1.3.3",
     "express-form": "~0.12.0",
     "express-form": "~0.12.0",
-    "express-sanitizer": "^1.0.4",
     "express-session": "^1.16.1",
     "express-session": "^1.16.1",
     "express-validator": "^6.1.1",
     "express-validator": "^6.1.1",
     "express-webpack-assets": "^0.1.0",
     "express-webpack-assets": "^0.1.0",
@@ -135,6 +134,7 @@
     "passport-local": "^1.0.0",
     "passport-local": "^1.0.0",
     "passport-saml": "^1.0.0",
     "passport-saml": "^1.0.0",
     "passport-twitter": "^1.0.4",
     "passport-twitter": "^1.0.4",
+    "react-card-flip": "^1.0.10",
     "react-image-crop": "^8.3.0",
     "react-image-crop": "^8.3.0",
     "rimraf": "^3.0.0",
     "rimraf": "^3.0.0",
     "slack-node": "^0.1.8",
     "slack-node": "^0.1.8",
@@ -154,7 +154,11 @@
       "handsontable: v7.0.0 or above is no loger MIT lisence."
       "handsontable: v7.0.0 or above is no loger MIT lisence."
     ],
     ],
     "@alienfast/i18next-loader": "^1.0.16",
     "@alienfast/i18next-loader": "^1.0.16",
+    "@atlaskit/drawer": "^5.3.5",
+    "@atlaskit/navigation-next": "^8.0.2",
     "@babel/core": "^7.4.5",
     "@babel/core": "^7.4.5",
+    "@babel/plugin-proposal-class-properties": "^7.8.3",
+    "@babel/plugin-proposal-optional-chaining": "^7.9.0",
     "@babel/polyfill": "^7.4.4",
     "@babel/polyfill": "^7.4.4",
     "@babel/preset-env": "^7.4.5",
     "@babel/preset-env": "^7.4.5",
     "@babel/preset-react": "^7.0.0",
     "@babel/preset-react": "^7.0.0",
@@ -163,8 +167,8 @@
     "babel-eslint": "^10.0.1",
     "babel-eslint": "^10.0.1",
     "babel-loader": "^8.0.6",
     "babel-loader": "^8.0.6",
     "babel-plugin-lodash": "^3.3.4",
     "babel-plugin-lodash": "^3.3.4",
-    "bootstrap-sass": "^3.4.1",
-    "bootstrap-select": "^1.12.4",
+    "babel-plugin-transform-imports": "^2.0.0",
+    "bootstrap": "^4.5.0",
     "browser-bunyan": "^1.3.0",
     "browser-bunyan": "^1.3.0",
     "browser-sync": "^2.26.3",
     "browser-sync": "^2.26.3",
     "bunyan-debug": "^2.0.0",
     "bunyan-debug": "^2.0.0",
@@ -204,7 +208,6 @@
     "markdown-it-task-checkbox": "^1.0.6",
     "markdown-it-task-checkbox": "^1.0.6",
     "markdown-it-toc-and-anchor-with-slugid": "^1.1.4",
     "markdown-it-toc-and-anchor-with-slugid": "^1.1.4",
     "markdown-table": "^1.1.1",
     "markdown-table": "^1.1.1",
-    "metismenu": "^3.0.3",
     "mini-css-extract-plugin": "^0.9.0",
     "mini-css-extract-plugin": "^0.9.0",
     "morgan": "^1.9.0",
     "morgan": "^1.9.0",
     "node-dev": "^4.0.0",
     "node-dev": "^4.0.0",
@@ -218,7 +221,6 @@
     "postcss-loader": "^3.0.0",
     "postcss-loader": "^3.0.0",
     "prettier": "^1.19.1",
     "prettier": "^1.19.1",
     "react": "^16.8.3",
     "react": "^16.8.3",
-    "react-bootstrap": "^0.32.1",
     "react-bootstrap-typeahead": "^3.4.7",
     "react-bootstrap-typeahead": "^3.4.7",
     "react-codemirror2": "^6.0.0",
     "react-codemirror2": "^6.0.0",
     "react-copy-to-clipboard": "^5.0.1",
     "react-copy-to-clipboard": "^5.0.1",
@@ -228,12 +230,15 @@
     "react-hotkeys": "^2.0.0",
     "react-hotkeys": "^2.0.0",
     "react-i18next": "^11.1.0",
     "react-i18next": "^11.1.0",
     "react-waypoint": "^9.0.0",
     "react-waypoint": "^9.0.0",
+    "reactstrap": "^8.0.1",
     "replacestream": "^4.0.3",
     "replacestream": "^4.0.3",
     "reveal.js": "^3.5.0",
     "reveal.js": "^3.5.0",
     "sass-loader": "^8.0.0",
     "sass-loader": "^8.0.0",
     "simple-load-script": "^1.0.2",
     "simple-load-script": "^1.0.2",
     "socket.io-client": "^2.0.3",
     "socket.io-client": "^2.0.3",
+    "sticky-events": "^3.1.3",
     "style-loader": "^1.0.0",
     "style-loader": "^1.0.0",
+    "styled-components": "^5.0.1",
     "stylelint": "^13.2.0",
     "stylelint": "^13.2.0",
     "stylelint-config-recess-order": "^2.0.1",
     "stylelint-config-recess-order": "^2.0.1",
     "swagger-jsdoc": "^3.4.0",
     "swagger-jsdoc": "^3.4.0",

+ 18 - 1
resource/cdn-manifests.js

@@ -2,7 +2,8 @@ module.exports = {
   js: [
   js: [
     {
     {
       name: 'basis',
       name: 'basis',
-      url: 'https://cdn.jsdelivr.net/combine/npm/emojione@3.1.2,npm/jquery@3.4.0,npm/bootstrap@3.4.1/dist/js/bootstrap.min.js',
+      // eslint-disable-next-line max-len
+      url: 'https://cdn.jsdelivr.net/combine/npm/emojione@3.1.2,npm/jquery@3.4.0,npm/popper.js@1.15.0,npm/bootstrap@4.5.0/dist/js/bootstrap.min.js,npm/scrollpos-styler@0.7.1,npm/jquery-slimscroll@1.3.8/jquery.slimscroll.min.js',
       groups: ['basis'],
       groups: ['basis'],
       args: {
       args: {
         integrity: '',
         integrity: '',
@@ -129,6 +130,14 @@ module.exports = {
         integrity: '',
         integrity: '',
       },
       },
     },
     },
+    {
+      name: 'material-icons',
+      url: 'https://cdn.jsdelivr.net/npm/material-icons@0.3.1/iconfont/material-icons.min.css',
+      groups: ['basis'],
+      args: {
+        integrity: '',
+      },
+    },
     {
     {
       name: 'emojione',
       name: 'emojione',
       url: 'https://cdn.jsdelivr.net/npm/emojione@3.1.2/extras/css/emojione.min.css',
       url: 'https://cdn.jsdelivr.net/npm/emojione@3.1.2/extras/css/emojione.min.css',
@@ -137,6 +146,14 @@ module.exports = {
         integrity: '',
         integrity: '',
       },
       },
     },
     },
+    {
+      name: 'animate.css',
+      url: 'https://cdn.jsdelivr.net/npm/animate.css@3.7.2/animate.min.css',
+      groups: ['basis'],
+      args: {
+        integrity: '',
+      },
+    },
     {
     {
       name: 'jquery-ui',
       name: 'jquery-ui',
       url: 'https://cdn.jsdelivr.net/jquery.ui/1.11.4/jquery-ui.min.css',
       url: 'https://cdn.jsdelivr.net/jquery.ui/1.11.4/jquery-ui.min.css',

+ 8 - 16
resource/locales/en-US/admin/admin.json

@@ -97,22 +97,11 @@
       "kibela_title": "Easy viewing structure",
       "kibela_title": "Easy viewing structure",
       "kibela_text1": "Center aligned contents",
       "kibela_text1": "Center aligned contents",
       "kibela_text2": "Show and post comments at the bottom of the page",
       "kibela_text2": "Show and post comments at the bottom of the page",
-      "kibela_text3": "Affix Table-of-contents",
-      "crowi_title": "Separated functions",
-      "crowi_text1": "Collapsible Sidebar",
-      "crowi_text2": "Show and post comments in Sidebar",
-      "crowi_text3": "Collapsible table-of-contents"
+      "kibela_text3": "Affix Table-of-contents"
     },
     },
-    "behavior": "Behavior",
-    "behavior_desc": {
-      "growi_text1": "Both of <code>/page</code> and <code>/page/</code> shows the same page.",
-      "growi_text2": "<code>/nonexistent_page</code> shows editing form",
-      "growi_text3": "All pages show the list of child pages <b>if using GROWI Enhanced Layout</b>",
-      "crowi_text1": "<code>/page</code> shows the page",
-      "crowi_text2": "<code>/page/</code> shows the list of child pages",
-      "crowi_text3": "If portal is applied to <code>/page/</code> , the portal and the list of child pages are shown",
-      "crowi_text4": "<code>/nonexistent_page</code> shows editing form<",
-      "crowi_text5": "<code>/nonexistent_page/</code> the list of child pages"
+    "theme_desc": {
+      "light_and_dark": "Light and dark modes",
+      "unique": "Only one mode"
     },
     },
     "function": "Function",
     "function": "Function",
     "function_desc": "You can choose Valid/Invalid of the function",
     "function_desc": "You can choose Valid/Invalid of the function",
@@ -136,7 +125,10 @@
     "code_highlight": "Code highlight",
     "code_highlight": "Code highlight",
     "nocdn_desc": "This function is disabled when the environment variable <code>NO_CDN=true</code>.<br>Github style has been forcibly applied.",
     "nocdn_desc": "This function is disabled when the environment variable <code>NO_CDN=true</code>.<br>Github style has been forcibly applied.",
     "custom_title": "Custom title",
     "custom_title": "Custom title",
-    "custom_title_detail": "You can customize <code>&lt;title&gt;</code> tag.<br><code>&#123;&#123;sitename&#125;&#125;</code> will be automatically replaced with the app name, and <code>&#123;&#123;page&#125;&#125;</code> will be replaced with the page name/path.",
+    "custom_title_detail": "You can customize <code>&lt;title&gt;</code> tag. Following placeholders will be automatically replaced:",
+    "custom_title_detail_placeholder1": "<code>&#123;&#123;sitename&#125;&#125;</code> - The site name of this wiki.",
+    "custom_title_detail_placeholder2": "<code>&#123;&#123;pagename&#125;&#125;</code> - The page name of the current page.",
+    "custom_title_detail_placeholder3": "<code>&#123;&#123;pagepath&#125;&#125;</code> - The page path of the current page.",
     "custom_header": "Custom HTML header",
     "custom_header": "Custom HTML header",
     "custom_header_detail": "You can customize HTML header that applies all pages. Your custom script will be inserted in <code>&lt;header&gt;</code> but above other <code>&lt;script&gt;</code> tags.<br>Relaod page to see changes.",
     "custom_header_detail": "You can customize HTML header that applies all pages. Your custom script will be inserted in <code>&lt;header&gt;</code> but above other <code>&lt;script&gt;</code> tags.<br>Relaod page to see changes.",
     "custom_css": "Custom CSS",
     "custom_css": "Custom CSS",

+ 0 - 135
resource/locales/en-US/sandbox-bootstrap3.md

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

+ 253 - 0
resource/locales/en-US/sandbox-bootstrap4.md

@@ -0,0 +1,253 @@
+# Labels
+
+<span class="badge badge-primary">Primary</span>
+<span class="badge badge-secondary">Secondary</span>
+<span class="badge badge-success">Success</span>
+<span class="badge badge-info">Info</span>
+<span class="badge badge-warning">Warning</span>
+<span class="badge badge-danger">Danger</span>
+<span class="badge badge-light text-dark">Light</span>
+<span class="badge badge-dark">Dark</span>
+
+<span class="badge badge-blue">Blue</span>
+<span class="badge badge-indigo">Indigo</span>
+<span class="badge badge-purple">Purple</span>
+<span class="badge badge-pink">Pink</span>
+<span class="badge badge-red">Red</span>
+<span class="badge badge-orange">Orange</span>
+<span class="badge badge-yellow">Yellow</span>
+<span class="badge badge-green">Green</span>
+<span class="badge badge-teal">Teal</span>
+<span class="badge badge-cyan">Cyan</span>
+
+
+# Alerts
+
+<div class="alert alert-primary" role="alert">
+  This is a primary alert with <a href="#" class="alert-link">an example link</a>. Give it a click if you like.
+</div>
+<div class="alert alert-secondary" role="alert">
+  This is a secondary alert with <a href="#" class="alert-link">an example link</a>. Give it a click if you like.
+</div>
+<div class="alert alert-success" role="alert">
+  This is a success alert with <a href="#" class="alert-link">an example link</a>. Give it a click if you like.
+</div>
+<div class="alert alert-danger" role="alert">
+  This is a danger alert with <a href="#" class="alert-link">an example link</a>. Give it a click if you like.
+</div>
+<div class="alert alert-warning" role="alert">
+  This is a warning alert with <a href="#" class="alert-link">an example link</a>. Give it a click if you like.
+</div>
+<div class="alert alert-info" role="alert">
+  This is a info alert with <a href="#" class="alert-link">an example link</a>. Give it a click if you like.
+</div>
+<div class="alert alert-light text-dark" role="alert">
+  This is a light alert with <a href="#" class="alert-link text-dark">an example link</a>. Give it a click if you like.
+</div>
+<div class="alert alert-dark" role="alert">
+  This is a dark alert with <a href="#" class="alert-link">an example link</a>. Give it a click if you like.
+</div>
+
+# Cards
+
+<div class="d-flex">
+
+<div class="mr-3">
+<div class="card text-white bg-primary mb-3" style="max-width: 18rem;">
+  <div class="card-header">Header</div>
+  <div class="card-body">
+    <h5 class="card-title">Primary card title</h5>
+    <p class="card-text">Some quick example text to build on the card title and make up the bulk of the card's content.</p>
+  </div>
+</div>
+<div class="card text-white bg-secondary mb-3" style="max-width: 18rem;">
+  <div class="card-header">Header</div>
+  <div class="card-body">
+    <h5 class="card-title">Secondary card title</h5>
+    <p class="card-text">Some quick example text to build on the card title and make up the bulk of the card's content.</p>
+  </div>
+</div>
+<div class="card text-white bg-success mb-3" style="max-width: 18rem;">
+  <div class="card-header">Header</div>
+  <div class="card-body">
+    <h5 class="card-title">Success card title</h5>
+    <p class="card-text">Some quick example text to build on the card title and make up the bulk of the card's content.</p>
+  </div>
+</div>
+<div class="card text-white bg-danger mb-3" style="max-width: 18rem;">
+  <div class="card-header">Header</div>
+  <div class="card-body">
+    <h5 class="card-title">Danger card title</h5>
+    <p class="card-text">Some quick example text to build on the card title and make up the bulk of the card's content.</p>
+  </div>
+</div>
+<div class="card text-white bg-warning mb-3" style="max-width: 18rem;">
+  <div class="card-header">Header</div>
+  <div class="card-body">
+    <h5 class="card-title">Warning card title</h5>
+    <p class="card-text">Some quick example text to build on the card title and make up the bulk of the card's content.</p>
+  </div>
+</div>
+<div class="card text-white bg-info mb-3" style="max-width: 18rem;">
+  <div class="card-header">Header</div>
+  <div class="card-body">
+    <h5 class="card-title">Info card title</h5>
+    <p class="card-text">Some quick example text to build on the card title and make up the bulk of the card's content.</p>
+  </div>
+</div>
+<div class="card bg-light mb-3" style="max-width: 18rem;">
+  <div class="card-header">Header</div>
+  <div class="card-body">
+    <h5 class="card-title">Light card title</h5>
+    <p class="card-text">Some quick example text to build on the card title and make up the bulk of the card's content.</p>
+  </div>
+</div>
+<div class="card text-white bg-dark mb-3" style="max-width: 18rem;">
+  <div class="card-header">Header</div>
+  <div class="card-body">
+    <h5 class="card-title">Dark card title</h5>
+    <p class="card-text">Some quick example text to build on the card title and make up the bulk of the card's content.</p>
+  </div>
+</div>
+</div>
+
+<div>
+<div class="card border-primary mb-3" style="max-width: 18rem;">
+  <div class="card-header">Header</div>
+  <div class="card-body text-primary">
+    <h5 class="card-title">Primary card title</h5>
+    <p class="card-text">Some quick example text to build on the card title and make up the bulk of the card's content.</p>
+  </div>
+</div>
+<div class="card border-secondary mb-3" style="max-width: 18rem;">
+  <div class="card-header">Header</div>
+  <div class="card-body text-secondary">
+    <h5 class="card-title">Secondary card title</h5>
+    <p class="card-text">Some quick example text to build on the card title and make up the bulk of the card's content.</p>
+  </div>
+</div>
+<div class="card border-success mb-3" style="max-width: 18rem;">
+  <div class="card-header">Header</div>
+  <div class="card-body text-success">
+    <h5 class="card-title">Success card title</h5>
+    <p class="card-text">Some quick example text to build on the card title and make up the bulk of the card's content.</p>
+  </div>
+</div>
+<div class="card border-danger mb-3" style="max-width: 18rem;">
+  <div class="card-header">Header</div>
+  <div class="card-body text-danger">
+    <h5 class="card-title">Danger card title</h5>
+    <p class="card-text">Some quick example text to build on the card title and make up the bulk of the card's content.</p>
+  </div>
+</div>
+<div class="card border-warning mb-3" style="max-width: 18rem;">
+  <div class="card-header">Header</div>
+  <div class="card-body text-warning">
+    <h5 class="card-title">Warning card title</h5>
+    <p class="card-text">Some quick example text to build on the card title and make up the bulk of the card's content.</p>
+  </div>
+</div>
+<div class="card border-info mb-3" style="max-width: 18rem;">
+  <div class="card-header">Header</div>
+  <div class="card-body text-info">
+    <h5 class="card-title">Info card title</h5>
+    <p class="card-text">Some quick example text to build on the card title and make up the bulk of the card's content.</p>
+  </div>
+</div>
+<div class="card border-light mb-3" style="max-width: 18rem;">
+  <div class="card-header">Header</div>
+  <div class="card-body">
+    <h5 class="card-title">Light card title</h5>
+    <p class="card-text">Some quick example text to build on the card title and make up the bulk of the card's content.</p>
+  </div>
+</div>
+<div class="card border-dark mb-3" style="max-width: 18rem;">
+  <div class="card-header">Header</div>
+  <div class="card-body text-dark">
+    <h5 class="card-title">Dark card title</h5>
+    <p class="card-text">Some quick example text to build on the card title and make up the bulk of the card's content.</p>
+  </div>
+</div>
+</div>
+
+</div>
+
+# Wells
+
+## Default well
+
+<div class="card card-body">Look, I'm in a well! </div>
+
+## Optional classes
+
+<div class="card card-body bg-primary text-light p-2">Look, I'm in a well! </div>
+
+# Typography
+
+## Lead body copy
+
+<p class="lead">Vivamus sagittis lacus vel augue laoreet rutrum faucibus dolor auctor. Duis mollis, est non commodo luctus.</p>
+
+## Marked text
+
+You can use the mark tag to <mark>highlight</mark> text.
+
+## Small text
+
+<small>This line of text is meant to be treated as fine print.</small>
+
+## Alignment classes
+
+<div class="card">
+  <div class="card-body">
+    <p class="text-left">Left aligned text.</p>
+    <p class="text-center">Center aligned text.</p>
+    <p class="text-right">Right aligned text.</p>
+    <p class="text-justify">Justified text.</p>
+    <p class="text-nowrap">No wrap text.</p>
+  </div>
+</div>
+
+## Transformation classes
+
+<div class="card">
+  <div class="card-body">
+    <p class="text-lowercase">Lowercased text.</p>
+    <p class="text-uppercase">Uppercased text.</p>
+    <p class="text-capitalize">Capitalized text.</p>
+  </div>
+</div>
+
+
+# Helper classes
+
+## Contextual colors
+
+<div class="card">
+  <div class="card-body">
+    <p class="text-muted">Fusce dapibus, tellus ac cursus commodo, tortor mauris nibh.</p>
+    <p class="text-light">Lorem ipsum dolor sit amet, consectetur adipiscing elit.</p>
+    <p class="text-secondary">Sed luctus venenatis tellus, in aliquam ligula scelerisque eget.</p>
+    <p class="text-dark">Ut vel lorem aliquet, rhoncus libero at, condimentum mi. Fusce pellentesque quam nec magna maximus porta.</p>
+    <p class="text-primary">Nullam id dolor id nibh ultricies vehicula ut id elit.</p>
+    <p class="text-success">Duis mollis, est non commodo luctus, nisi erat porttitor ligula.</p>
+    <p class="text-info">Maecenas sed diam eget risus varius blandit sit amet non magna.</p>
+    <p class="text-warning">Etiam porta sem malesuada magna mollis euismod.</p>
+    <p class="text-danger">Donec ullamcorper nulla non metus auctor fringilla.</p>
+  </div>
+</div>
+
+## Contextual backgrounds
+
+<div class="card">
+  <div class="card-body">
+    <p class="bg-light">Lorem ipsum dolor sit amet, consectetur adipiscing elit.</p>
+    <p class="bg-secondary text-white">Sed luctus venenatis tellus, in aliquam ligula scelerisque eget.</p>
+    <p class="bg-dark text-white">Ut vel lorem aliquet, rhoncus libero at, condimentum mi.</p>
+    <p class="bg-primary text-white">Nullam id dolor id nibh ultricies vehicula ut id elit.</p>
+    <p class="bg-success text-white">Duis mollis, est non commodo luctus, nisi erat porttitor ligula.</p>
+    <p class="bg-info text-white">Maecenas sed diam eget risus varius blandit sit amet non magna.</p>
+    <p class="bg-warning text-white">Etiam porta sem malesuada magna mollis euismod.</p>
+    <p class="bg-danger text-white">Donec ullamcorper nulla non metus auctor fringilla.</p>
+  </div>
+</div>

+ 7 - 7
resource/locales/en-US/sandbox.md

@@ -1,5 +1,5 @@
-<div class="panel panel-default">
-  <div class="panel-body">
+<div class="card">
+  <div class="card-body">
 
 
 # Table of Contents
 # Table of Contents
 
 
@@ -252,12 +252,12 @@ This is the most flexible linker.
 Both the page description and link address can be displayed on the page.
 Both the page description and link address can be displayed on the page.
 
 
 ```
 ```
-[[./Bootstrap3]]
-Example of Bootstrap3 is[[here>./Bootstrap3]]
+[[./Bootstrap4]]
+Example of Bootstrap4 is[[here>./Bootstrap4]]
 ```
 ```
 
 
 [[../user]]
 [[../user]]
-Example of Bootstrap3 is[[here>./Bootstrap3]]
+Example of Bootstrap4 is[[here>./Bootstrap4]]
 
 
 # :pencil: Lists
 # :pencil: Lists
 
 
@@ -449,8 +449,8 @@ See [emojione](https://www.emojione.com/)
 
 
 # :heavy_plus_sign: More..
 # :heavy_plus_sign: More..
 
 
-- Try to attach Bootstrap3 Tags?
-    - :arrow_right: [/Sandbox/Bootstrap3]
+- Try to attach Bootstrap4 Tags?
+    - :arrow_right: [/Sandbox/Bootstrap4]
 - Try to draw Diagrams?
 - Try to draw Diagrams?
     - :arrow_right: [/Sandbox/Diagrams]
     - :arrow_right: [/Sandbox/Diagrams]
 - Try to write Math Formulas?
 - Try to write Math Formulas?

+ 20 - 10
resource/locales/en-US/translation.json

@@ -55,13 +55,12 @@
   "Share Link": "Share Link",
   "Share Link": "Share Link",
   "Markdown Link": "Markdown Link",
   "Markdown Link": "Markdown Link",
   "Create/Edit Template": "Create/Edit template page",
   "Create/Edit Template": "Create/Edit template page",
-  "Unportalize": "Unportalize",
   "Go to this version": "View this version",
   "Go to this version": "View this version",
   "View diff": "View diff",
   "View diff": "View diff",
   "No diff": "No diff",
   "No diff": "No diff",
   "Shrink versions that have no diffs": "Shrink versions that have no diffs",
   "Shrink versions that have no diffs": "Shrink versions that have no diffs",
   "User ID": "User ID",
   "User ID": "User ID",
-  "Home": "Home",
+  "User's Home": "User's Home",
   "User Settings": "User Settings",
   "User Settings": "User Settings",
   "User Information": "User information",
   "User Information": "User information",
   "Basic Info": "Basic info",
   "Basic Info": "Basic info",
@@ -111,6 +110,13 @@
   "Specified users only": "Specified users only",
   "Specified users only": "Specified users only",
   "Only me": "Only me",
   "Only me": "Only me",
   "Only inside the group": "Only inside the group",
   "Only inside the group": "Only inside the group",
+  "page_list_and_search_results": "Page list / Search results",
+  "scope_of_page_disclosure": "Scope of page disclosure",
+  "set_point": "Set point",
+  "always_displayed": "Always displayed",
+  "always_hidden": "Always hidden",
+  "displayed_or_hidden": "Displayed / Hidden",
+  "page_access_and_delete_rights": "Page access / Delete rights",
   "Reselect the group": "Reselect the group",
   "Reselect the group": "Reselect the group",
   "Shareable link": "Shareable link",
   "Shareable link": "Shareable link",
   "The whitelist of registration permission E-mail address": "The whitelist of registration permission E-mail address",
   "The whitelist of registration permission E-mail address": "The whitelist of registration permission E-mail address",
@@ -124,6 +130,8 @@
   "Deleted Pages": "Deleted Pages",
   "Deleted Pages": "Deleted Pages",
   "Sign out": "Logout",
   "Sign out": "Logout",
   "Disassociate": "Disassociate",
   "Disassociate": "Disassociate",
+  "Recent Created": "Recent Created",
+  "Recent Changes": "Recent Changes",
   "form_validation": {
   "form_validation": {
     "error_message": "Some values ​​are incorrect",
     "error_message": "Some values ​​are incorrect",
     "required": "%s is required",
     "required": "%s is required",
@@ -193,6 +201,7 @@
   "copy_to_clipboard": {
   "copy_to_clipboard": {
     "Copy to clipboard": "Copy to clipboard",
     "Copy to clipboard": "Copy to clipboard",
     "Page path": "Page path",
     "Page path": "Page path",
+    "Page URL": "Page URL",
     "Parmanent link": "Parmanent link",
     "Parmanent link": "Parmanent link",
     "Page path and parmanent link": "Page path and parmanent link",
     "Page path and parmanent link": "Page path and parmanent link",
     "Markdown link": "Markdown link"
     "Markdown link": "Markdown link"
@@ -261,9 +270,9 @@
       "Redirect": "Redirect"
       "Redirect": "Redirect"
     },
     },
     "help": {
     "help": {
-      "redirect": "Redirect to new page if someone accesses <code>%s</code>",
+      "redirect": "Redirect to new page if someone accesses under this path",
       "metadata": "Remains last update user and updated date as is",
       "metadata": "Remains last update user and updated date as is",
-      "recursive": "Move/Rename children of under <code>%s</code> recursively"
+      "recursive": "Move/Rename children of under this path recursively"
     }
     }
   },
   },
   "Put Back": "Put back",
   "Put Back": "Put back",
@@ -274,11 +283,12 @@
     "delete_recursively": "Delete child pages recursively.",
     "delete_recursively": "Delete child pages recursively.",
     "delete_completely": "Delete completely",
     "delete_completely": "Delete completely",
     "delete_completely_restriction": "You don't have the authority to delete pages completely.",
     "delete_completely_restriction": "You don't have the authority to delete pages completely.",
-    "recursively": "Delete children of <code>%s</code> recursively.",
+    "recursively": "Delete pages under this path recursively.",
     "completely": "Delete completely instead of putting it into trash."
     "completely": "Delete completely instead of putting it into trash."
   },
   },
   "modal_empty":{
   "modal_empty":{
-    "empty_the_trash": "Empty The Trash"
+    "empty_the_trash": "Empty The Trash",
+    "notice": "The pages deleted completely are unrecoverable."
   },
   },
   "modal_duplicate": {
   "modal_duplicate": {
     "label": {
     "label": {
@@ -293,13 +303,13 @@
       "recursively": "Put back recursively"
       "recursively": "Put back recursively"
     },
     },
     "help": {
     "help": {
-      "recursively": "Put back children of under <code>%s</code> recursively"
+      "recursively": "Put back page under this path recursively"
     }
     }
   },
   },
   "modal_shortcuts": {
   "modal_shortcuts": {
     "global": {
     "global": {
       "title": "Global shortcuts",
       "title": "Global shortcuts",
-      "Open/Close shortcut help": "Open/Close shortcut help",
+      "Open/Close shortcut help": "Open/Close<br>shortcut help",
       "Edit Page": "Edit Page",
       "Edit Page": "Edit Page",
       "Create Page": "Create Page",
       "Create Page": "Create Page",
       "Show Contributors": "Show Contributors",
       "Show Contributors": "Show Contributors",
@@ -330,7 +340,7 @@
   "template": {
   "template": {
     "modal_label": {
     "modal_label": {
       "Create/Edit Template Page": "Create/Edit template page",
       "Create/Edit Template Page": "Create/Edit template page",
-      "Create template under": "Create template page under:<br /><code><small>%s</small></code>"
+      "Create template under": "Create template page under this page"
     },
     },
     "option_label": {
     "option_label": {
       "create/edit": "Create/Edit template page..",
       "create/edit": "Create/Edit template page..",
@@ -657,7 +667,7 @@
     "export_collections": "Export Collections",
     "export_collections": "Export Collections",
     "check_all": "Check All",
     "check_all": "Check All",
     "uncheck_all": "Uncheck All",
     "uncheck_all": "Uncheck All",
-    "desc_password_seed": "DO NOT FORGET to set current <code>PASSWORD_SEED</code> to your new GROWI system when restoring user data, or users will NOT be able to login with their password.<br><br><strong>HINT:</strong><br>The current <code>PASSWORD_SEED</code> will be stored in <code>meta.json</code> in exported ZIP.",
+    "desc_password_seed": "<p>DO NOT FORGET to set current <code>PASSWORD_SEED</code> to your new GROWI system when restoring user data, or users will NOT be able to login with their password.</p><strong>HINT:</strong><p>The current <code>PASSWORD_SEED</code> will be stored in <code>meta.json</code> in exported ZIP.</p>",
     "create_new_archive_data": "Create New Archive Data",
     "create_new_archive_data": "Create New Archive Data",
     "export": "Export",
     "export": "Export",
     "cancel": "Cancel",
     "cancel": "Cancel",

+ 5 - 7
resource/locales/en-US/welcome.md

@@ -3,16 +3,14 @@
 [![GitHub Releases](https://img.shields.io/github/release/weseek/growi.svg)](https://github.com/weseek/growi/releases/latest)
 [![GitHub Releases](https://img.shields.io/github/release/weseek/growi.svg)](https://github.com/weseek/growi/releases/latest)
 [![MIT License](https://img.shields.io/badge/license-MIT-blue.svg?style=flat)](LICENSE)
 [![MIT License](https://img.shields.io/badge/license-MIT-blue.svg?style=flat)](LICENSE)
 
 
-<div class="panel panel-primary">
-  <div class="panel-heading">Tips</div>
-  <div class="panel-body"><ul>
-    <li>Ctrl(⌘)-/ to show quick help</li>
-    <li>You can write HTML with <a href="https://getbootstrap.com/docs/3.3/css/">Bootstrap 3</a>.</li>
+<div class="card border-primary">
+  <div class="card-header bg-primary text-light">Tips</div>
+  <div class="card-body"><ul>
+    <li>Ctrl(⌘) + "/" to show quick help</li>
+    <li>You can write HTML with <a href="https://getbootstrap.com/docs/4.5/components/">Bootstrap 4</a>.</li>
   </ul></div>
   </ul></div>
 </div>
 </div>
 
 
-<div class="clearfix"></div>
-
 Contents
 Contents
 =========
 =========
 
 

+ 8 - 16
resource/locales/ja/admin/admin.json

@@ -97,22 +97,11 @@
       "kibela_title": "閲覧重視の構造",
       "kibela_title": "閲覧重視の構造",
       "kibela_text1": "コンテンツが中心に表示されます。",
       "kibela_text1": "コンテンツが中心に表示されます。",
       "kibela_text2": "コメントはページの下部に表示されます。",
       "kibela_text2": "コメントはページの下部に表示されます。",
-      "kibela_text3": "ページ情報は下部に表示されます。",
-      "crowi_title": "ビュー・コントロールの分離",
-      "crowi_text1": "サイドバーを開くと情報が表示されます。",
-      "crowi_text2": "コメントはサイドバーに表示されます。",
-      "crowi_text3": "ページ情報はサイドバーに表示されます。"
+      "kibela_text3": "ページ情報は下部に表示されます。"
     },
     },
-    "behavior": "動作",
-    "behavior_desc": {
-      "growi_text1": "<code>/page</code>と<code>/page/</code>どちらのパスも同じページを表示します。",
-      "growi_text2": "<code>/nonexistent_page</code> では編集フォームを表示します",
-      "growi_text3": "<b>GROWI Enhanced Layout</b>では全てのページが配下のページリストを表示します",
-      "crowi_text1": "<code>/page</code> ではページを表示します。",
-      "crowi_text2": "<code>/page/</code> では配下のページを表示します。",
-      "crowi_text3": "<code>/page/</code>がポータルに適応している場合、ポータルページと配下のページリストを表示します。",
-      "crowi_text4": "<code>/nonexistent_page</code> では編集フォームを表示します",
-      "crowi_text5": "<code>/nonexistent_page</code> では配下のページリストを表示します。"
+    "theme_desc" : {
+      "light_and_dark": "Light/Dark モード選択あり",
+      "unique": "モード選択なし"
     },
     },
     "function": "機能",
     "function": "機能",
     "function_desc": "機能の有効/無効を選択できます。",
     "function_desc": "機能の有効/無効を選択できます。",
@@ -136,7 +125,10 @@
     "code_highlight": "コードハイライト",
     "code_highlight": "コードハイライト",
     "nocdn_desc": "この機能は、環境変数 <code>NO_CDN=true</code> の時は無効化されます。<br>GitHub スタイルが適用されています。",
     "nocdn_desc": "この機能は、環境変数 <code>NO_CDN=true</code> の時は無効化されます。<br>GitHub スタイルが適用されています。",
     "custom_title": "カスタム Title",
     "custom_title": "カスタム Title",
-    "custom_title_detail": "<code>&lt;title&gt;</code>タグのコンテンツをカスタマイズできます。<br><code>&#123;&#123;sitename&#125;&#125;</code>がサイト名、<code>&#123;&#123;page&#125;&#125;</code>がページ名またはページパスに置換されます。",
+    "custom_title_detail": "<code>&lt;title&gt;</code>タグのコンテンツをカスタマイズできます。以下のプレースホルダーは自動的に置換されます:",
+    "custom_title_detail_placeholder1": "<code>&#123;&#123;sitename&#125;&#125;</code> - この Wiki のサイト名",
+    "custom_title_detail_placeholder2": "<code>&#123;&#123;pagename&#125;&#125;</code> - 現在表示中のページ名",
+    "custom_title_detail_placeholder3": "<code>&#123;&#123;pagepath&#125;&#125;</code> - 現在表示中のページパス",
     "custom_header": "カスタム HTML Header",
     "custom_header": "カスタム HTML Header",
     "custom_header_detail": "システム全体に適用される HTML を記述できます。<code>&lt;header&gt;</code> タグ内の他の <code>&lt;script&gt;</code> タグ読み込み前に展開されます。<br>変更の反映はページの更新が必要です。",
     "custom_header_detail": "システム全体に適用される HTML を記述できます。<code>&lt;header&gt;</code> タグ内の他の <code>&lt;script&gt;</code> タグ読み込み前に展開されます。<br>変更の反映はページの更新が必要です。",
     "custom_css": "カスタム CSS",
     "custom_css": "カスタム CSS",

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

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

+ 253 - 0
resource/locales/ja/sandbox-bootstrap4.md

@@ -0,0 +1,253 @@
+# Labels
+
+<span class="badge badge-primary">Primary</span>
+<span class="badge badge-secondary">Secondary</span>
+<span class="badge badge-success">Success</span>
+<span class="badge badge-info">Info</span>
+<span class="badge badge-warning">Warning</span>
+<span class="badge badge-danger">Danger</span>
+<span class="badge badge-light text-dark">Light</span>
+<span class="badge badge-dark">Dark</span>
+
+<span class="badge badge-blue">Blue</span>
+<span class="badge badge-indigo">Indigo</span>
+<span class="badge badge-purple">Purple</span>
+<span class="badge badge-pink">Pink</span>
+<span class="badge badge-red">Red</span>
+<span class="badge badge-orange">Orange</span>
+<span class="badge badge-yellow">Yellow</span>
+<span class="badge badge-green">Green</span>
+<span class="badge badge-teal">Teal</span>
+<span class="badge badge-cyan">Cyan</span>
+
+
+# Alerts
+
+<div class="alert alert-primary" role="alert">
+  This is a primary alert with <a href="#" class="alert-link">an example link</a>. Give it a click if you like.
+</div>
+<div class="alert alert-secondary" role="alert">
+  This is a secondary alert with <a href="#" class="alert-link">an example link</a>. Give it a click if you like.
+</div>
+<div class="alert alert-success" role="alert">
+  This is a success alert with <a href="#" class="alert-link">an example link</a>. Give it a click if you like.
+</div>
+<div class="alert alert-danger" role="alert">
+  This is a danger alert with <a href="#" class="alert-link">an example link</a>. Give it a click if you like.
+</div>
+<div class="alert alert-warning" role="alert">
+  This is a warning alert with <a href="#" class="alert-link">an example link</a>. Give it a click if you like.
+</div>
+<div class="alert alert-info" role="alert">
+  This is a info alert with <a href="#" class="alert-link">an example link</a>. Give it a click if you like.
+</div>
+<div class="alert alert-light text-dark" role="alert">
+  This is a light alert with <a href="#" class="alert-link text-dark">an example link</a>. Give it a click if you like.
+</div>
+<div class="alert alert-dark" role="alert">
+  This is a dark alert with <a href="#" class="alert-link">an example link</a>. Give it a click if you like.
+</div>
+
+# Cards
+
+<div class="d-flex">
+
+<div class="mr-3">
+<div class="card text-white bg-primary mb-3" style="max-width: 18rem;">
+  <div class="card-header">Header</div>
+  <div class="card-body">
+    <h5 class="card-title">Primary card title</h5>
+    <p class="card-text">Some quick example text to build on the card title and make up the bulk of the card's content.</p>
+  </div>
+</div>
+<div class="card text-white bg-secondary mb-3" style="max-width: 18rem;">
+  <div class="card-header">Header</div>
+  <div class="card-body">
+    <h5 class="card-title">Secondary card title</h5>
+    <p class="card-text">Some quick example text to build on the card title and make up the bulk of the card's content.</p>
+  </div>
+</div>
+<div class="card text-white bg-success mb-3" style="max-width: 18rem;">
+  <div class="card-header">Header</div>
+  <div class="card-body">
+    <h5 class="card-title">Success card title</h5>
+    <p class="card-text">Some quick example text to build on the card title and make up the bulk of the card's content.</p>
+  </div>
+</div>
+<div class="card text-white bg-danger mb-3" style="max-width: 18rem;">
+  <div class="card-header">Header</div>
+  <div class="card-body">
+    <h5 class="card-title">Danger card title</h5>
+    <p class="card-text">Some quick example text to build on the card title and make up the bulk of the card's content.</p>
+  </div>
+</div>
+<div class="card text-white bg-warning mb-3" style="max-width: 18rem;">
+  <div class="card-header">Header</div>
+  <div class="card-body">
+    <h5 class="card-title">Warning card title</h5>
+    <p class="card-text">Some quick example text to build on the card title and make up the bulk of the card's content.</p>
+  </div>
+</div>
+<div class="card text-white bg-info mb-3" style="max-width: 18rem;">
+  <div class="card-header">Header</div>
+  <div class="card-body">
+    <h5 class="card-title">Info card title</h5>
+    <p class="card-text">Some quick example text to build on the card title and make up the bulk of the card's content.</p>
+  </div>
+</div>
+<div class="card bg-light mb-3" style="max-width: 18rem;">
+  <div class="card-header">Header</div>
+  <div class="card-body">
+    <h5 class="card-title">Light card title</h5>
+    <p class="card-text">Some quick example text to build on the card title and make up the bulk of the card's content.</p>
+  </div>
+</div>
+<div class="card text-white bg-dark mb-3" style="max-width: 18rem;">
+  <div class="card-header">Header</div>
+  <div class="card-body">
+    <h5 class="card-title">Dark card title</h5>
+    <p class="card-text">Some quick example text to build on the card title and make up the bulk of the card's content.</p>
+  </div>
+</div>
+</div>
+
+<div>
+<div class="card border-primary mb-3" style="max-width: 18rem;">
+  <div class="card-header">Header</div>
+  <div class="card-body text-primary">
+    <h5 class="card-title">Primary card title</h5>
+    <p class="card-text">Some quick example text to build on the card title and make up the bulk of the card's content.</p>
+  </div>
+</div>
+<div class="card border-secondary mb-3" style="max-width: 18rem;">
+  <div class="card-header">Header</div>
+  <div class="card-body text-secondary">
+    <h5 class="card-title">Secondary card title</h5>
+    <p class="card-text">Some quick example text to build on the card title and make up the bulk of the card's content.</p>
+  </div>
+</div>
+<div class="card border-success mb-3" style="max-width: 18rem;">
+  <div class="card-header">Header</div>
+  <div class="card-body text-success">
+    <h5 class="card-title">Success card title</h5>
+    <p class="card-text">Some quick example text to build on the card title and make up the bulk of the card's content.</p>
+  </div>
+</div>
+<div class="card border-danger mb-3" style="max-width: 18rem;">
+  <div class="card-header">Header</div>
+  <div class="card-body text-danger">
+    <h5 class="card-title">Danger card title</h5>
+    <p class="card-text">Some quick example text to build on the card title and make up the bulk of the card's content.</p>
+  </div>
+</div>
+<div class="card border-warning mb-3" style="max-width: 18rem;">
+  <div class="card-header">Header</div>
+  <div class="card-body text-warning">
+    <h5 class="card-title">Warning card title</h5>
+    <p class="card-text">Some quick example text to build on the card title and make up the bulk of the card's content.</p>
+  </div>
+</div>
+<div class="card border-info mb-3" style="max-width: 18rem;">
+  <div class="card-header">Header</div>
+  <div class="card-body text-info">
+    <h5 class="card-title">Info card title</h5>
+    <p class="card-text">Some quick example text to build on the card title and make up the bulk of the card's content.</p>
+  </div>
+</div>
+<div class="card border-light mb-3" style="max-width: 18rem;">
+  <div class="card-header">Header</div>
+  <div class="card-body">
+    <h5 class="card-title">Light card title</h5>
+    <p class="card-text">Some quick example text to build on the card title and make up the bulk of the card's content.</p>
+  </div>
+</div>
+<div class="card border-dark mb-3" style="max-width: 18rem;">
+  <div class="card-header">Header</div>
+  <div class="card-body text-dark">
+    <h5 class="card-title">Dark card title</h5>
+    <p class="card-text">Some quick example text to build on the card title and make up the bulk of the card's content.</p>
+  </div>
+</div>
+</div>
+
+</div>
+
+# Wells
+
+## Default well
+
+<div class="card card-body">Look, I'm in a well! </div>
+
+## Optional classes
+
+<div class="card card-body bg-primary text-light p-2">Look, I'm in a well! </div>
+
+# Typography
+
+## Lead body copy
+
+<p class="lead">Vivamus sagittis lacus vel augue laoreet rutrum faucibus dolor auctor. Duis mollis, est non commodo luctus.</p>
+
+## Marked text
+
+You can use the mark tag to <mark>highlight</mark> text.
+
+## Small text
+
+<small>This line of text is meant to be treated as fine print.</small>
+
+## Alignment classes
+
+<div class="card">
+  <div class="card-body">
+    <p class="text-left">Left aligned text.</p>
+    <p class="text-center">Center aligned text.</p>
+    <p class="text-right">Right aligned text.</p>
+    <p class="text-justify">Justified text.</p>
+    <p class="text-nowrap">No wrap text.</p>
+  </div>
+</div>
+
+## Transformation classes
+
+<div class="card">
+  <div class="card-body">
+    <p class="text-lowercase">Lowercased text.</p>
+    <p class="text-uppercase">Uppercased text.</p>
+    <p class="text-capitalize">Capitalized text.</p>
+  </div>
+</div>
+
+
+# Helper classes
+
+## Contextual colors
+
+<div class="card">
+  <div class="card-body">
+    <p class="text-muted">Fusce dapibus, tellus ac cursus commodo, tortor mauris nibh.</p>
+    <p class="text-light">Lorem ipsum dolor sit amet, consectetur adipiscing elit.</p>
+    <p class="text-secondary">Sed luctus venenatis tellus, in aliquam ligula scelerisque eget.</p>
+    <p class="text-dark">Ut vel lorem aliquet, rhoncus libero at, condimentum mi. Fusce pellentesque quam nec magna maximus porta.</p>
+    <p class="text-primary">Nullam id dolor id nibh ultricies vehicula ut id elit.</p>
+    <p class="text-success">Duis mollis, est non commodo luctus, nisi erat porttitor ligula.</p>
+    <p class="text-info">Maecenas sed diam eget risus varius blandit sit amet non magna.</p>
+    <p class="text-warning">Etiam porta sem malesuada magna mollis euismod.</p>
+    <p class="text-danger">Donec ullamcorper nulla non metus auctor fringilla.</p>
+  </div>
+</div>
+
+## Contextual backgrounds
+
+<div class="card">
+  <div class="card-body">
+    <p class="bg-light">Lorem ipsum dolor sit amet, consectetur adipiscing elit.</p>
+    <p class="bg-secondary text-white">Sed luctus venenatis tellus, in aliquam ligula scelerisque eget.</p>
+    <p class="bg-dark text-white">Ut vel lorem aliquet, rhoncus libero at, condimentum mi.</p>
+    <p class="bg-primary text-white">Nullam id dolor id nibh ultricies vehicula ut id elit.</p>
+    <p class="bg-success text-white">Duis mollis, est non commodo luctus, nisi erat porttitor ligula.</p>
+    <p class="bg-info text-white">Maecenas sed diam eget risus varius blandit sit amet non magna.</p>
+    <p class="bg-warning text-white">Etiam porta sem malesuada magna mollis euismod.</p>
+    <p class="bg-danger text-white">Donec ullamcorper nulla non metus auctor fringilla.</p>
+  </div>
+</div>

+ 8 - 8
resource/locales/ja/sandbox.md

@@ -1,5 +1,5 @@
-<div class="panel panel-default">
-  <div class="panel-body">
+<div class="card">
+  <div class="card-body">
 
 
 # 目次
 # 目次
 
 
@@ -37,7 +37,7 @@
 
 
 ## Block 段落
 ## Block 段落
 
 
-空白行を挟むことで段落となります。aaaa
+空白行を挟むことで段落となります。
 
 
 ```
 ```
 段落1
 段落1
@@ -251,12 +251,12 @@ ___
 記述中のページを基点とした相対リンクと、表示テキストに対するリンクを同時に実現できます。
 記述中のページを基点とした相対リンクと、表示テキストに対するリンクを同時に実現できます。
 
 
 ```
 ```
-[[./Bootstrap3]]
-Bootstrap3のExampleは[[こちら>./Bootstrap3]]
+[[./Bootstrap4]]
+Bootstrap4のExampleは[[こちら>./Bootstrap4]]
 ```
 ```
 
 
 [[../user]]
 [[../user]]
-Bootstrap3のExampleは[[こちら>./Bootstrap3]]
+Bootstrap4のExampleは[[こちら>./Bootstrap4]]
 
 
 # :pencil: Lists
 # :pencil: Lists
 
 
@@ -449,8 +449,8 @@ See [emojione](https://www.emojione.com/)
 
 
 # :heavy_plus_sign: 更に…
 # :heavy_plus_sign: 更に…
 
 
-- Bootstrap3 のタグを使う
-    - :arrow_right: [/Sandbox/Bootstrap3]
+- Bootstrap4 のタグを使う
+    - :arrow_right: [/Sandbox/Bootstrap4]
 - 図表を書く
 - 図表を書く
     - :arrow_right: [/Sandbox/Diagrams]
     - :arrow_right: [/Sandbox/Diagrams]
 - 数式を書く
 - 数式を書く

+ 24 - 14
resource/locales/ja/translation.json

@@ -55,13 +55,12 @@
   "Share Link": "共有用リンク",
   "Share Link": "共有用リンク",
   "Markdown Link": "Markdown形式のリンク",
   "Markdown Link": "Markdown形式のリンク",
   "Create/Edit Template": "テンプレートページの作成/編集",
   "Create/Edit Template": "テンプレートページの作成/編集",
-  "Unportalize": "ポータル解除",
   "Go to this version": "このバージョンを見る",
   "Go to this version": "このバージョンを見る",
   "View diff": "差分を表示",
   "View diff": "差分を表示",
   "No diff": "差分なし",
   "No diff": "差分なし",
   "Shrink versions that have no diffs": "差分のないバージョンをコンパクトに表示する",
   "Shrink versions that have no diffs": "差分のないバージョンをコンパクトに表示する",
   "User ID": "ユーザーID",
   "User ID": "ユーザーID",
-  "Home": "ホーム",
+  "User's Home": "ユーザーホーム",
   "User Settings": "ユーザー設定",
   "User Settings": "ユーザー設定",
   "User Information": "ユーザー情報",
   "User Information": "ユーザー情報",
   "Basic Info": "ユーザーの基本情報",
   "Basic Info": "ユーザーの基本情報",
@@ -110,6 +109,13 @@
   "Specified users": "特定ユーザーのみ",
   "Specified users": "特定ユーザーのみ",
   "Only me": "自分のみ",
   "Only me": "自分のみ",
   "Only inside the group": "特定グループのみ",
   "Only inside the group": "特定グループのみ",
+  "page_list_and_search_results": "ページリスト・検索結果",
+  "scope_of_page_disclosure": "ページの公開範囲",
+  "set_point": "設定値",
+  "always_displayed": "表示 (固定)",
+  "always_hidden": "非表示 (固定)",
+  "displayed_or_hidden": "表示 / 非表示",
+  "page_access_and_delete_rights": "ページの閲覧・削除権限",
   "Reselect the group": "グループの再選択",
   "Reselect the group": "グループの再選択",
   "Shareable link": "このページの共有用URL",
   "Shareable link": "このページの共有用URL",
   "The whitelist of registration permission E-mail address": "登録許可メールアドレスの<br>ホワイトリスト",
   "The whitelist of registration permission E-mail address": "登録許可メールアドレスの<br>ホワイトリスト",
@@ -123,6 +129,8 @@
   "Deleted Pages": "削除済みページ",
   "Deleted Pages": "削除済みページ",
   "Sign out": "ログアウト",
   "Sign out": "ログアウト",
   "Disassociate": "連携解除",
   "Disassociate": "連携解除",
+  "Recent Created": "最新の作成",
+  "Recent Changes": "最新の変更",
   "form_validation": {
   "form_validation": {
     "error_message": "いくつかの値が設定されていません",
     "error_message": "いくつかの値が設定されていません",
     "required": "%sに値を入力してください",
     "required": "%sに値を入力してください",
@@ -192,6 +200,7 @@
   "copy_to_clipboard": {
   "copy_to_clipboard": {
     "Copy to clipboard": "クリップボードにコピー",
     "Copy to clipboard": "クリップボードにコピー",
     "Page path": "ページ名",
     "Page path": "ページ名",
+    "Page URL": "ページURL",
     "Parmanent link": "パーマリンク",
     "Parmanent link": "パーマリンク",
     "Page path and parmanent link": "ページ名とパーマリンク",
     "Page path and parmanent link": "ページ名とパーマリンク",
     "Markdown link": "マークダウン形式のリンク"
     "Markdown link": "マークダウン形式のリンク"
@@ -259,9 +268,9 @@
       "Redirect": "リダイレクトする"
       "Redirect": "リダイレクトする"
     },
     },
     "help": {
     "help": {
-      "redirect": "<code>%s</code> にアクセスされた際に自動的に新しいページにジャンプします",
+      "redirect": "アクセスされた際に自動的に新しいページにジャンプします",
       "metadata": "最終更新ユーザー、最終更新日を更新せず維持します",
       "metadata": "最終更新ユーザー、最終更新日を更新せず維持します",
-      "recursive": "<code>%s</code> 配下のページも移動/名前変更します"
+      "recursive": "配下のページも移動/名前変更します"
     }
     }
   },
   },
   "Put Back": "元に戻す",
   "Put Back": "元に戻す",
@@ -272,11 +281,12 @@
     "delete_recursively": "全ての子ページも削除",
     "delete_recursively": "全ての子ページも削除",
     "delete_completely": "完全削除",
     "delete_completely": "完全削除",
     "delete_completely_restriction": "完全削除をするための権限がありません。",
     "delete_completely_restriction": "完全削除をするための権限がありません。",
-    "recursively": "<code>%s</code> 配下のページも削除します",
+    "recursively": "配下のページも削除します",
     "completely": "ゴミ箱を経由せず、完全に削除します"
     "completely": "ゴミ箱を経由せず、完全に削除します"
   },
   },
   "modal_empty":{
   "modal_empty":{
-    "empty_the_trash": "ゴミ箱を空にする"
+    "empty_the_trash": "ゴミ箱を空にする",
+    "notice": "完全削除したページは元に戻すことができません"
   },
   },
   "modal_duplicate": {
   "modal_duplicate": {
     "label": {
     "label": {
@@ -291,21 +301,21 @@
       "recursively": "全ての子ページも元に戻す"
       "recursively": "全ての子ページも元に戻す"
     },
     },
     "help": {
     "help": {
-      "recursively": "<code>%s</code> 配下のページも元に戻します"
+      "recursively": "配下のページも元に戻します"
     }
     }
   },
   },
   "modal_shortcuts": {
   "modal_shortcuts": {
     "global": {
     "global": {
       "title": "グローバルショートカット",
       "title": "グローバルショートカット",
-      "Open/Close shortcut help": "ショートカットヘルプの表示/非表示",
+      "Open/Close shortcut help": "ショートカットヘルプ<br>の表示/非表示",
       "Edit Page": "ページ編集",
       "Edit Page": "ページ編集",
       "Create Page": "ページ作成",
       "Create Page": "ページ作成",
-      "Show Contributors": "コントリビューターを表示",
+      "Show Contributors": "コントリビューター<br>を表示",
       "Konami Code": "コナミコマンド",
       "Konami Code": "コナミコマンド",
       "konami_code_url": "https://ja.wikipedia.org/wiki/コナミコマンド"
       "konami_code_url": "https://ja.wikipedia.org/wiki/コナミコマンド"
     },
     },
     "editor": {
     "editor": {
-      "titile": "エディターショートカット",
+      "title": "エディターショートカット",
       "Indent": "インデント",
       "Indent": "インデント",
       "Outdent": "左インデント",
       "Outdent": "左インデント",
       "Save Page": "保存",
       "Save Page": "保存",
@@ -323,12 +333,12 @@
     "activate_user_success": "{{username}}を有効化しました",
     "activate_user_success": "{{username}}を有効化しました",
     "deactivate_user_success": "{{username}}を無効化しました",
     "deactivate_user_success": "{{username}}を無効化しました",
     "remove_user_success": "{{username}}を削除しました",
     "remove_user_success": "{{username}}を削除しました",
-    "remove_external_user_success": "{{accountId}}を削除しました "
+    "remove_external_user_success": "{{accountId}}を削除しました"
   },
   },
   "template": {
   "template": {
     "modal_label": {
     "modal_label": {
       "Create/Edit Template Page": "テンプレートページの作成/編集",
       "Create/Edit Template Page": "テンプレートページの作成/編集",
-      "Create template under": "<code><small>%s</small></code><br />にテンプレートページを作成"
+      "Create template under": "配下にテンプレートページを作成"
     },
     },
     "option_label": {
     "option_label": {
       "select": "テンプレートタイプを選択してください",
       "select": "テンプレートタイプを選択してください",
@@ -386,7 +396,7 @@
     "discard_changes": "HackMD の変更を破棄する",
     "discard_changes": "HackMD の変更を破棄する",
     "integration_failed": "HackMD の統合に失敗しました",
     "integration_failed": "HackMD の統合に失敗しました",
     "fail_to_connect": "GROWI クライアントが HackMD の GROWI agent に接続できませんでした。",
     "fail_to_connect": "GROWI クライアントが HackMD の GROWI agent に接続できませんでした。",
-    "check_configuration": "<a href='https://docs.growi.org/guide/admin-cookbook/integrate-with-hackmd.html'>こちらのマニュアル</a>から設定を確認してください",
+    "check_configuration": "<a href='https://docs.growi.org/ja/admin-guide/admin-cookbook/integrate-with-hackmd.html'>こちらのマニュアル</a>から設定を確認してください",
     "not_initialized": "HackMD コンポーネントは初期化されていません",
     "not_initialized": "HackMD コンポーネントは初期化されていません",
     "someone_editing": "このページは、HackMD で編集されています。",
     "someone_editing": "このページは、HackMD で編集されています。",
     "this_page_has_draft": "このページは、HackMD のドラフトがあります。"
     "this_page_has_draft": "このページは、HackMD のドラフトがあります。"
@@ -646,7 +656,7 @@
     "export_collections": "コレクションのエクスポート",
     "export_collections": "コレクションのエクスポート",
     "check_all": "全てにチェックを付ける",
     "check_all": "全てにチェックを付ける",
     "uncheck_all": "全てからチェックを外す",
     "uncheck_all": "全てからチェックを外す",
-    "desc_password_seed": "ユーザーデータをバックアップ/リストアする場合、現在の <code>PASSWORD_SEED</code> を新しい GROWI システムにセットすることを忘れないでください。さもなくば、ユーザーがパスワードでログインできなくなります。<br><br><strong>ヒント:</strong><br>現在の <code>PASSWORD_SEED</code> は、エクスポートされる ZIP 中の <code>meta.json</code> に保存されます。",
+    "desc_password_seed": "<p>ユーザーデータをバックアップ/リストアする場合、現在の <code>PASSWORD_SEED</code> を新しい GROWI システムにセットすることを忘れないでください。さもなくば、ユーザーがパスワードでログインできなくなります。</p><strong>ヒント:</strong><p>現在の <code>PASSWORD_SEED</code> は、エクスポートされる ZIP 中の <code>meta.json</code> に保存されます。</p>",
     "create_new_archive_data": "アーカイブデータの新規作成",
     "create_new_archive_data": "アーカイブデータの新規作成",
     "export": "エクスポート",
     "export": "エクスポート",
     "cancel": "キャンセル",
     "cancel": "キャンセル",

+ 6 - 8
resource/locales/ja/welcome.md

@@ -3,16 +3,14 @@
 [![GitHub Releases](https://img.shields.io/github/release/weseek/growi.svg)](https://github.com/weseek/growi/releases/latest)
 [![GitHub Releases](https://img.shields.io/github/release/weseek/growi.svg)](https://github.com/weseek/growi/releases/latest)
 [![MIT License](https://img.shields.io/badge/license-MIT-blue.svg?style=flat)](LICENSE)
 [![MIT License](https://img.shields.io/badge/license-MIT-blue.svg?style=flat)](LICENSE)
 
 
-<div class="panel panel-primary">
-  <div class="panel-heading">Tips</div>
-  <div class="panel-body"><ul>
-    <li>Ctrl(⌘)-/ でショートカットヘルプを表示します</li>
-      <li>HTML/CSS の記述時は、<a href="https://getbootstrap.com/docs/3.3/css/">Bootstrap 3</a> を利用できます</li>
+<div class="card border-primary">
+  <div class="card-header bg-primary text-light">Tips</div>
+  <div class="card-body"><ul>
+    <li>Ctrl(⌘) + "/" でショートカットヘルプを表示します</li>
+    <li>HTML/CSS の記述には、<a href="https://getbootstrap.com/docs/4.5/components/">Bootstrap 4</a> を利用できます</li>
   </ul></div>
   </ul></div>
 </div>
 </div>
 
 
-<div class="clearfix"></div>
-
 Contents
 Contents
 =========
 =========
 
 
@@ -26,4 +24,4 @@ Slack
 <a href="https://growi-slackin.weseek.co.jp/"><img src="https://growi-slackin.weseek.co.jp/badge.svg"></a>
 <a href="https://growi-slackin.weseek.co.jp/"><img src="https://growi-slackin.weseek.co.jp/badge.svg"></a>
 
 
 GROWI をより良いものにするために、是非 Slack に参加してください。  
 GROWI をより良いものにするために、是非 Slack に参加してください。  
-開発に関する議論を行っている他、導入時の質問等も受け付けています。
+開発に関する議論を行っている他、導入時の質問等も受け付けています。

+ 14 - 8
src/client/js/admin.jsx

@@ -5,6 +5,8 @@ import { I18nextProvider } from 'react-i18next';
 
 
 import loggerFactory from '@alias/logger';
 import loggerFactory from '@alias/logger';
 
 
+import ErrorBoundary from './components/ErrorBoudary';
+
 import AdminHome from './components/Admin/AdminHome/AdminHome';
 import AdminHome from './components/Admin/AdminHome/AdminHome';
 import UserGroupDetailPage from './components/Admin/UserGroupDetail/UserGroupDetailPage';
 import UserGroupDetailPage from './components/Admin/UserGroupDetail/UserGroupDetailPage';
 import NotificationSetting from './components/Admin/Notification/NotificationSetting';
 import NotificationSetting from './components/Admin/Notification/NotificationSetting';
@@ -99,9 +101,11 @@ Object.keys(componentMappings).forEach((key) => {
   if (elem) {
   if (elem) {
     ReactDOM.render(
     ReactDOM.render(
       <I18nextProvider i18n={i18n}>
       <I18nextProvider i18n={i18n}>
-        <Provider inject={injectableContainers}>
-          {componentMappings[key]}
-        </Provider>
+        <ErrorBoundary>
+          <Provider inject={injectableContainers}>
+            {componentMappings[key]}
+          </Provider>
+        </ErrorBoundary>
       </I18nextProvider>,
       </I18nextProvider>,
       elem,
       elem,
     );
     );
@@ -124,11 +128,13 @@ if (adminSecuritySettingElem != null) {
     adminOidcSecurityContainer, adminBasicSecurityContainer, adminGoogleSecurityContainer, adminGitHubSecurityContainer, adminTwitterSecurityContainer,
     adminOidcSecurityContainer, adminBasicSecurityContainer, adminGoogleSecurityContainer, adminGitHubSecurityContainer, adminTwitterSecurityContainer,
   ];
   ];
   ReactDOM.render(
   ReactDOM.render(
-    <Provider inject={[...injectableContainers, ...adminSecurityContainers]}>
-      <I18nextProvider i18n={i18n}>
-        <SecurityManagement />
-      </I18nextProvider>
-    </Provider>,
+    <I18nextProvider i18n={i18n}>
+      <ErrorBoundary>
+        <Provider inject={[...injectableContainers, ...adminSecurityContainers]}>
+          <SecurityManagement />
+        </Provider>
+      </ErrorBoundary>
+    </I18nextProvider>,
     adminSecuritySettingElem,
     adminSecuritySettingElem,
   );
   );
 }
 }

+ 24 - 16
src/client/js/app.jsx

@@ -5,9 +5,11 @@ import { I18nextProvider } from 'react-i18next';
 
 
 import loggerFactory from '@alias/logger';
 import loggerFactory from '@alias/logger';
 
 
+import ErrorBoundary from './components/ErrorBoudary';
 import SearchPage from './components/SearchPage';
 import SearchPage from './components/SearchPage';
 import TagsList from './components/TagsList';
 import TagsList from './components/TagsList';
 import PageEditor from './components/PageEditor';
 import PageEditor from './components/PageEditor';
+import PagePathNavForEditor from './components/PageEditor/PagePathNavForEditor';
 // eslint-disable-next-line import/no-duplicates
 // eslint-disable-next-line import/no-duplicates
 import OptionsSelector from './components/PageEditor/OptionsSelector';
 import OptionsSelector from './components/PageEditor/OptionsSelector';
 // eslint-disable-next-line import/no-duplicates
 // eslint-disable-next-line import/no-duplicates
@@ -19,12 +21,10 @@ import PageHistory from './components/PageHistory';
 import PageComments from './components/PageComments';
 import PageComments from './components/PageComments';
 import PageTimeline from './components/PageTimeline';
 import PageTimeline from './components/PageTimeline';
 import CommentEditorLazyRenderer from './components/PageComment/CommentEditorLazyRenderer';
 import CommentEditorLazyRenderer from './components/PageComment/CommentEditorLazyRenderer';
+import PageManagement from './components/Page/PageManagement';
+import TrashPageAlert from './components/Page/TrashPageAlert';
 import PageAttachment from './components/PageAttachment';
 import PageAttachment from './components/PageAttachment';
 import PageStatusAlert from './components/PageStatusAlert';
 import PageStatusAlert from './components/PageStatusAlert';
-import RevisionPath from './components/Page/RevisionPath';
-import TagLabels from './components/Page/TagLabels';
-import BookmarkButton from './components/BookmarkButton';
-import LikeButton from './components/LikeButton';
 import PagePathAutoComplete from './components/PagePathAutoComplete';
 import PagePathAutoComplete from './components/PagePathAutoComplete';
 import RecentCreated from './components/RecentCreated/RecentCreated';
 import RecentCreated from './components/RecentCreated/RecentCreated';
 import MyDraftList from './components/MyDraftList/MyDraftList';
 import MyDraftList from './components/MyDraftList/MyDraftList';
@@ -36,6 +36,8 @@ import PageContainer from './services/PageContainer';
 import CommentContainer from './services/CommentContainer';
 import CommentContainer from './services/CommentContainer';
 import EditorContainer from './services/EditorContainer';
 import EditorContainer from './services/EditorContainer';
 import TagContainer from './services/TagContainer';
 import TagContainer from './services/TagContainer';
+import GrowiSubNavigation from './components/Navbar/GrowiSubNavigation';
+import GrowiSubNavigationForUserPage from './components/Navbar/GrowiSubNavigationForUserPage';
 import PersonalContainer from './services/PersonalContainer';
 import PersonalContainer from './services/PersonalContainer';
 
 
 import { appContainer, componentMappings } from './bootstrap';
 import { appContainer, componentMappings } from './bootstrap';
@@ -68,13 +70,14 @@ Object.assign(componentMappings, {
   // 'revision-history': <PageHistory pageId={pageId} />,
   // 'revision-history': <PageHistory pageId={pageId} />,
   'tags-page': <TagsList crowi={appContainer} />,
   'tags-page': <TagsList crowi={appContainer} />,
 
 
-  'create-page-name-input': <PagePathAutoComplete crowi={appContainer} initializedPath={pageContainer.state.path} addTrailingSlash />,
-
   'page-editor': <PageEditor />,
   'page-editor': <PageEditor />,
+  'page-editor-path-nav': <PagePathNavForEditor />,
   'page-editor-options-selector': <OptionsSelector crowi={appContainer} />,
   'page-editor-options-selector': <OptionsSelector crowi={appContainer} />,
   'page-status-alert': <PageStatusAlert />,
   'page-status-alert': <PageStatusAlert />,
   'save-page-controls': <SavePageControls />,
   'save-page-controls': <SavePageControls />,
 
 
+  'trash-page-alert': <TrashPageAlert />,
+
   'page-timeline': <PageTimeline />,
   'page-timeline': <PageTimeline />,
 
 
   'personal-setting': <PersonalSettings crowi={personalContainer} />,
   'personal-setting': <PersonalSettings crowi={personalContainer} />,
@@ -87,14 +90,12 @@ if (pageContainer.state.pageId != null) {
     'page-comments-list': <PageComments />,
     'page-comments-list': <PageComments />,
     'page-attachment': <PageAttachment />,
     'page-attachment': <PageAttachment />,
     'page-comment-write': <CommentEditorLazyRenderer />,
     'page-comment-write': <CommentEditorLazyRenderer />,
+    'page-management': <PageManagement />,
+
     'revision-toc': <TableOfContents />,
     'revision-toc': <TableOfContents />,
-    'like-button': <LikeButton pageId={pageContainer.state.pageId} isLiked={pageContainer.state.isLiked} />,
     'seen-user-list': <UserPictureList userIds={pageContainer.state.seenUserIds} />,
     'seen-user-list': <UserPictureList userIds={pageContainer.state.seenUserIds} />,
     'liker-list': <UserPictureList userIds={pageContainer.state.likerUserIds} />,
     'liker-list': <UserPictureList userIds={pageContainer.state.likerUserIds} />,
-    'bookmark-button': <BookmarkButton pageId={pageContainer.state.pageId} crowi={appContainer} />,
-    'bookmark-button-lg': <BookmarkButton pageId={pageContainer.state.pageId} crowi={appContainer} size="lg" />,
     'rename-page-name-input': <PagePathAutoComplete crowi={appContainer} initializedPath={pageContainer.state.path} />,
     'rename-page-name-input': <PagePathAutoComplete crowi={appContainer} initializedPath={pageContainer.state.path} />,
-    'duplicate-page-name-input': <PagePathAutoComplete crowi={appContainer} initializedPath={pageContainer.state.path} />,
 
 
     'user-created-list': <RecentCreated />,
     'user-created-list': <RecentCreated />,
     'user-draft-list': <MyDraftList />,
     'user-draft-list': <MyDraftList />,
@@ -104,8 +105,8 @@ if (pageContainer.state.path != null) {
   Object.assign(componentMappings, {
   Object.assign(componentMappings, {
     // eslint-disable-next-line quote-props
     // eslint-disable-next-line quote-props
     'page': <Page />,
     'page': <Page />,
-    'revision-path': <RevisionPath behaviorType={appContainer.config.behaviorType} pageId={pageContainer.state.pageId} pagePath={pageContainer.state.path} />,
-    'tag-label': <TagLabels />,
+    'grw-subnav': <GrowiSubNavigation />,
+    'grw-subnav-for-user-page': <GrowiSubNavigationForUserPage />,
   });
   });
 }
 }
 
 
@@ -114,9 +115,11 @@ Object.keys(componentMappings).forEach((key) => {
   if (elem) {
   if (elem) {
     ReactDOM.render(
     ReactDOM.render(
       <I18nextProvider i18n={i18n}>
       <I18nextProvider i18n={i18n}>
-        <Provider inject={injectableContainers}>
-          {componentMappings[key]}
-        </Provider>
+        <ErrorBoundary>
+          <Provider inject={injectableContainers}>
+            {componentMappings[key]}
+          </Provider>
+        </ErrorBoundary>
       </I18nextProvider>,
       </I18nextProvider>,
       elem,
       elem,
     );
     );
@@ -127,7 +130,12 @@ Object.keys(componentMappings).forEach((key) => {
 $('a[data-toggle="tab"][href="#revision-history"]').on('show.bs.tab', () => {
 $('a[data-toggle="tab"][href="#revision-history"]').on('show.bs.tab', () => {
   ReactDOM.render(
   ReactDOM.render(
     <I18nextProvider i18n={i18n}>
     <I18nextProvider i18n={i18n}>
-      <PageHistory pageId={pageContainer.state.pageId} crowi={appContainer} />
+      <ErrorBoundary>
+        <PageHistory pageId={pageContainer.state.pageId} crowi={appContainer} />
+      </ErrorBoundary>
     </I18nextProvider>, document.getElementById('revision-history'),
     </I18nextProvider>, document.getElementById('revision-history'),
   );
   );
 });
 });
+
+// initialize scrollpos-styler
+ScrollPosStyler.init();

+ 15 - 4
src/client/js/bootstrap.jsx

@@ -3,12 +3,16 @@ import React from 'react';
 import loggerFactory from '@alias/logger';
 import loggerFactory from '@alias/logger';
 import Xss from '@commons/service/xss';
 import Xss from '@commons/service/xss';
 
 
-import HeaderSearchBox from './components/HeaderSearchBox';
+import SearchTop from './components/Navbar/SearchTop';
+import NavbarToggler from './components/Navbar/NavbarToggler';
 import PersonalDropdown from './components/Navbar/PersonalDropdown';
 import PersonalDropdown from './components/Navbar/PersonalDropdown';
+import Sidebar from './components/Sidebar';
 import StaffCredit from './components/StaffCredit/StaffCredit';
 import StaffCredit from './components/StaffCredit/StaffCredit';
 
 
 import AppContainer from './services/AppContainer';
 import AppContainer from './services/AppContainer';
 import WebsocketContainer from './services/WebsocketContainer';
 import WebsocketContainer from './services/WebsocketContainer';
+import PageCreateButton from './components/Navbar/PageCreateButton';
+import PageCreateModal from './components/PageCreateModal';
 
 
 const logger = loggerFactory('growi:app');
 const logger = loggerFactory('growi:app');
 
 
@@ -27,7 +31,7 @@ const websocketContainer = new WebsocketContainer(appContainer);
 
 
 logger.info('unstated containers have been initialized');
 logger.info('unstated containers have been initialized');
 
 
-appContainer.initPlugins();
+appContainer.init();
 appContainer.injectToWindow();
 appContainer.injectToWindow();
 
 
 /**
 /**
@@ -36,10 +40,17 @@ appContainer.injectToWindow();
  *  value: React Element
  *  value: React Element
  */
  */
 const componentMappings = {
 const componentMappings = {
-  'search-top': <HeaderSearchBox />,
-  'search-sidebar': <HeaderSearchBox crowi={appContainer} />,
+  'grw-navbar-toggler': <NavbarToggler />,
+
+  'grw-search-top': <SearchTop />,
   'personal-dropdown': <PersonalDropdown />,
   'personal-dropdown': <PersonalDropdown />,
 
 
+  'create-page-button': <PageCreateButton />,
+  'create-page-button-icon': <PageCreateButton isIcon />,
+  'page-create-modal': <PageCreateModal />,
+
+  'grw-sidebar-wrapper': <Sidebar />,
+
   'staff-credit': <StaffCredit />,
   'staff-credit': <StaffCredit />,
 };
 };
 
 

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

@@ -41,14 +41,14 @@ class AdminHome extends React.Component {
         </p>
         </p>
 
 
         <div className="row mb-5">
         <div className="row mb-5">
-          <div className="col-md-12">
+          <div className="col-lg-12">
             <h2 className="admin-setting-header">{t('admin:admin_top.system_information')}</h2>
             <h2 className="admin-setting-header">{t('admin:admin_top.system_information')}</h2>
             <SystemInfomationTable />
             <SystemInfomationTable />
           </div>
           </div>
         </div>
         </div>
 
 
         <div className="row mb-5">
         <div className="row mb-5">
-          <div className="col-md-12">
+          <div className="col-lg-12">
             <h2 className="admin-setting-header">{t('admin:admin_top.list_of_installed_plugins')}</h2>
             <h2 className="admin-setting-header">{t('admin:admin_top.list_of_installed_plugins')}</h2>
             <InstalledPluginTable />
             <InstalledPluginTable />
           </div>
           </div>

+ 1 - 1
src/client/js/components/Admin/AdminHome/SystemInfomationTable.jsx

@@ -15,7 +15,7 @@ class SystemInformationTable extends React.Component {
       <table className="table table-bordered">
       <table className="table table-bordered">
         <tbody>
         <tbody>
           <tr>
           <tr>
-            <th className="col-sm-4">GROWI</th>
+            <th>GROWI</th>
             <td>{ adminHomeContainer.state.growiVersion }</td>
             <td>{ adminHomeContainer.state.growiVersion }</td>
           </tr>
           </tr>
           <tr>
           <tr>

+ 24 - 23
src/client/js/components/Admin/App/AppSetting.jsx

@@ -38,9 +38,9 @@ class AppSetting extends React.Component {
 
 
     return (
     return (
       <React.Fragment>
       <React.Fragment>
-        <div className="row mb-5">
-          <label className="col-xs-3 control-label">{t('admin:app_setting.site_name')}</label>
-          <div className="col-xs-6">
+        <div className="form-group row">
+          <label className="text-left text-md-right col-md-3 col-form-label">{t('admin:app_setting.site_name')}</label>
+          <div className="col-md-6">
             <input
             <input
               className="form-control"
               className="form-control"
               type="text"
               type="text"
@@ -48,13 +48,13 @@ class AppSetting extends React.Component {
               onChange={(e) => { adminAppContainer.changeTitle(e.target.value) }}
               onChange={(e) => { adminAppContainer.changeTitle(e.target.value) }}
               placeholder="GROWI"
               placeholder="GROWI"
             />
             />
-            <p className="help-block">{t('admin:app_setting.sitename_change')}</p>
+            <p className="form-text text-muted">{t('admin:app_setting.sitename_change')}</p>
           </div>
           </div>
         </div>
         </div>
 
 
-        <div className="row mb-5">
-          <label className="col-xs-3 control-label">{t('admin:app_setting.confidential_name')}</label>
-          <div className="col-xs-6">
+        <div className="row form-group mb-5">
+          <label className="text-left text-md-right col-md-3 col-form-label">{t('admin:app_setting.confidential_name')}</label>
+          <div className="col-md-6">
             <input
             <input
               className="form-control"
               className="form-control"
               type="text"
               type="text"
@@ -62,55 +62,56 @@ class AppSetting extends React.Component {
               onChange={(e) => { adminAppContainer.changeConfidential(e.target.value) }}
               onChange={(e) => { adminAppContainer.changeConfidential(e.target.value) }}
               placeholder={t('admin:app_setting.confidential_example')}
               placeholder={t('admin:app_setting.confidential_example')}
             />
             />
-            <p className="help-block">{t('admin:app_setting.header_content')}</p>
+            <p className="form-text text-muted">{t('admin:app_setting.header_content')}</p>
           </div>
           </div>
         </div>
         </div>
 
 
-        <div className="row mb-5">
-          <label className="col-xs-3 control-label">{t('admin:app_setting.default_language')}</label>
-          <div className="col-xs-6">
-            <div className="radio radio-primary radio-inline">
+        <div className="row form-group mb-5">
+          <label className="text-left text-md-right col-md-3 col-form-label">{t('admin:app_setting.default_language')}</label>
+          <div className="col-md-6">
+            <div className="custom-control custom-radio custom-control-inline">
               <input
               <input
                 type="radio"
                 type="radio"
                 id="radioLangEn"
                 id="radioLangEn"
+                className="custom-control-input"
                 name="globalLang"
                 name="globalLang"
                 value="en-US"
                 value="en-US"
                 checked={adminAppContainer.state.globalLang === 'en-US'}
                 checked={adminAppContainer.state.globalLang === 'en-US'}
                 onChange={(e) => { adminAppContainer.changeGlobalLang(e.target.value) }}
                 onChange={(e) => { adminAppContainer.changeGlobalLang(e.target.value) }}
               />
               />
-              <label htmlFor="radioLangEn">{t('English')}</label>
+              <label className="custom-control-label" htmlFor="radioLangEn">{t('English')}</label>
             </div>
             </div>
-            <div className="radio radio-primary radio-inline">
+            <div className="custom-control custom-radio custom-control-inline">
               <input
               <input
                 type="radio"
                 type="radio"
                 id="radioLangJa"
                 id="radioLangJa"
+                className="custom-control-input"
                 name="globalLang"
                 name="globalLang"
                 value="ja"
                 value="ja"
                 checked={adminAppContainer.state.globalLang === 'ja'}
                 checked={adminAppContainer.state.globalLang === 'ja'}
                 onChange={(e) => { adminAppContainer.changeGlobalLang(e.target.value) }}
                 onChange={(e) => { adminAppContainer.changeGlobalLang(e.target.value) }}
               />
               />
-              <label htmlFor="radioLangJa">{t('Japanese')}</label>
+              <label className="custom-control-label" htmlFor="radioLangJa">{t('Japanese')}</label>
             </div>
             </div>
           </div>
           </div>
         </div>
         </div>
 
 
-        <div className="row mb-5">
-          <label className="col-xs-3 control-label">{t('admin:app_setting.file_uploading')}</label>
-          <div className="col-xs-6">
-            <div className="checkbox checkbox-info">
+        <div className="row form-group mb-5">
+          <label className="text-left text-md-right col-md-3 col-form-label">{t('admin:app_setting.file_uploading')}</label>
+          <div className="col-md-6">
+            <div className="custom-control custom-checkbox custom-checkbox-info">
               <input
               <input
                 type="checkbox"
                 type="checkbox"
                 id="cbFileUpload"
                 id="cbFileUpload"
+                className="custom-control-input"
                 name="fileUpload"
                 name="fileUpload"
                 checked={adminAppContainer.state.fileUpload}
                 checked={adminAppContainer.state.fileUpload}
                 onChange={(e) => { adminAppContainer.changeFileUpload(e.target.checked) }}
                 onChange={(e) => { adminAppContainer.changeFileUpload(e.target.checked) }}
               />
               />
-              <label htmlFor="cbFileUpload">{t('admin:app_setting.enable_files_except_image')}</label>
+              <label className="custom-control-label" htmlFor="cbFileUpload">{t('admin:app_setting.enable_files_except_image')}</label>
             </div>
             </div>
 
 
-            <p className="help-block">
-              {t('admin:app_setting.enable_files_except_image')}
-              <br />
+            <p className="form-text text-muted">
               {t('admin:app_setting.attach_enable')}
               {t('admin:app_setting.attach_enable')}
             </p>
             </p>
           </div>
           </div>

+ 5 - 5
src/client/js/components/Admin/App/AppSettingsPage.jsx

@@ -38,35 +38,35 @@ class AppSettingsPage extends React.Component {
     return (
     return (
       <Fragment>
       <Fragment>
         <div className="row">
         <div className="row">
-          <div className="col-md-12">
+          <div className="col-lg-12">
             <h2 className="admin-setting-header">{t('App Settings')}</h2>
             <h2 className="admin-setting-header">{t('App Settings')}</h2>
             <AppSetting />
             <AppSetting />
           </div>
           </div>
         </div>
         </div>
 
 
         <div className="row mt-5">
         <div className="row mt-5">
-          <div className="col-md-12">
+          <div className="col-lg-12">
             <h2 className="admin-setting-header">{t('Site URL settings')}</h2>
             <h2 className="admin-setting-header">{t('Site URL settings')}</h2>
             <SiteUrlSetting />
             <SiteUrlSetting />
           </div>
           </div>
         </div>
         </div>
 
 
         <div className="row mt-5">
         <div className="row mt-5">
-          <div className="col-md-12">
+          <div className="col-lg-12">
             <h2 className="admin-setting-header">{t('admin:app_setting.mail_settings')}</h2>
             <h2 className="admin-setting-header">{t('admin:app_setting.mail_settings')}</h2>
             <MailSetting />
             <MailSetting />
           </div>
           </div>
         </div>
         </div>
 
 
         <div className="row mt-5">
         <div className="row mt-5">
-          <div className="col-md-12">
+          <div className="col-lg-12">
             <h2 className="admin-setting-header">{t('admin:app_setting.aws_settings')}</h2>
             <h2 className="admin-setting-header">{t('admin:app_setting.aws_settings')}</h2>
             <AwsSetting />
             <AwsSetting />
           </div>
           </div>
         </div>
         </div>
 
 
         <div className="row mt-5">
         <div className="row mt-5">
-          <div className="col-md-12">
+          <div className="col-lg-12">
             <h2 className="admin-setting-header">{t('admin:app_setting.plugin_settings')}</h2>
             <h2 className="admin-setting-header">{t('admin:app_setting.plugin_settings')}</h2>
             <PluginSetting />
             <PluginSetting />
           </div>
           </div>

+ 17 - 17
src/client/js/components/Admin/App/AwsSetting.jsx

@@ -38,7 +38,7 @@ class AwsSetting extends React.Component {
 
 
     return (
     return (
       <React.Fragment>
       <React.Fragment>
-        <p className="well">
+        <p className="card well">
           {t('admin:app_setting.aws_access')}
           {t('admin:app_setting.aws_access')}
           <br />
           <br />
           {t('admin:app_setting.no_smtp_setting')}
           {t('admin:app_setting.no_smtp_setting')}
@@ -50,11 +50,11 @@ class AwsSetting extends React.Component {
           </span>
           </span>
         </p>
         </p>
 
 
-        <div className="row mb-5">
-          <label className="col-xs-3 control-label">
+        <div className="row form-group">
+          <label className="text-left text-md-right col-md-3 col-form-label">
             {t('admin:app_setting.region')}
             {t('admin:app_setting.region')}
           </label>
           </label>
-          <div className="col-xs-6">
+          <div className="col-md-6">
             <input
             <input
               className="form-control"
               className="form-control"
               placeholder={`${t('eg')} ap-northeast-1`}
               placeholder={`${t('eg')} ap-northeast-1`}
@@ -66,11 +66,11 @@ class AwsSetting extends React.Component {
           </div>
           </div>
         </div>
         </div>
 
 
-        <div className="row mb-5">
-          <label className="col-xs-3 control-label">
+        <div className="row form-group">
+          <label className="text-left text-md-right col-md-3 col-form-label">
             {t('admin:app_setting.custom_endpoint')}
             {t('admin:app_setting.custom_endpoint')}
           </label>
           </label>
-          <div className="col-xs-6">
+          <div className="col-md-6">
             <input
             <input
               className="form-control"
               className="form-control"
               type="text"
               type="text"
@@ -80,15 +80,15 @@ class AwsSetting extends React.Component {
                 adminAppContainer.changeCustomEndpoint(e.target.value);
                 adminAppContainer.changeCustomEndpoint(e.target.value);
               }}
               }}
             />
             />
-            <p className="help-block">{t('admin:app_setting.custom_endpoint_change')}</p>
+            <p className="form-text text-muted">{t('admin:app_setting.custom_endpoint_change')}</p>
           </div>
           </div>
         </div>
         </div>
 
 
-        <div className="row mb-5">
-          <label className="col-xs-3 control-label">
+        <div className="row form-group">
+          <label className="text-left text-md-right col-md-3 col-form-label">
             {t('admin:app_setting.bucket_name')}
             {t('admin:app_setting.bucket_name')}
           </label>
           </label>
-          <div className="col-xs-6">
+          <div className="col-md-6">
             <input
             <input
               className="form-control"
               className="form-control"
               type="text"
               type="text"
@@ -101,11 +101,11 @@ class AwsSetting extends React.Component {
           </div>
           </div>
         </div>
         </div>
 
 
-        <div className="row mb-5">
-          <label className="col-xs-3 control-label">
+        <div className="row form-group">
+          <label className="text-left text-md-right col-md-3 col-form-label">
             Access key ID
             Access key ID
           </label>
           </label>
-          <div className="col-xs-6">
+          <div className="col-md-6">
             <input
             <input
               className="form-control"
               className="form-control"
               type="text"
               type="text"
@@ -117,11 +117,11 @@ class AwsSetting extends React.Component {
           </div>
           </div>
         </div>
         </div>
 
 
-        <div className="row mb-5">
-          <label className="col-xs-3 control-label">
+        <div className="row form-group">
+          <label className="text-left text-md-right col-md-3 col-form-label">
             Secret access key
             Secret access key
           </label>
           </label>
-          <div className="col-xs-6">
+          <div className="col-md-6">
             <input
             <input
               className="form-control"
               className="form-control"
               type="text"
               type="text"

+ 11 - 11
src/client/js/components/Admin/App/MailSetting.jsx

@@ -38,10 +38,10 @@ class MailSetting extends React.Component {
 
 
     return (
     return (
       <React.Fragment>
       <React.Fragment>
-        <p className="well">{t('admin:app_setting.smtp_used')} {t('admin:app_setting.smtp_but_aws')}<br />{t('admin:app_setting.neihter_of')}</p>
-        <div className="row mb-5">
-          <label className="col-xs-3 control-label">{t('admin:app_setting.from_e-mail_address')}</label>
-          <div className="col-xs-6">
+        <p className="card well">{t('admin:app_setting.smtp_used')} {t('admin:app_setting.smtp_but_aws')}<br />{t('admin:app_setting.neihter_of')}</p>
+        <div className="row form-group mb-5">
+          <label className="col-md-3 col-form-label text-left">{t('admin:app_setting.from_e-mail_address')}</label>
+          <div className="col-md-6">
             <input
             <input
               className="form-control"
               className="form-control"
               type="text"
               type="text"
@@ -52,9 +52,9 @@ class MailSetting extends React.Component {
           </div>
           </div>
         </div>
         </div>
 
 
-        <div className="row mb-5">
-          <label className="col-xs-3 control-label">{t('admin:app_setting.smtp_settings')}</label>
-          <div className="col-xs-4">
+        <div className="row form-group mb-5">
+          <label className="col-md-3 col-form-label text-left">{t('admin:app_setting.smtp_settings')}</label>
+          <div className="col-md-4">
             <label>{t('admin:app_setting.host')}</label>
             <label>{t('admin:app_setting.host')}</label>
             <input
             <input
               className="form-control"
               className="form-control"
@@ -63,7 +63,7 @@ class MailSetting extends React.Component {
               onChange={(e) => { adminAppContainer.changeSmtpHost(e.target.value) }}
               onChange={(e) => { adminAppContainer.changeSmtpHost(e.target.value) }}
             />
             />
           </div>
           </div>
-          <div className="col-xs-2">
+          <div className="col-md-2">
             <label>{t('admin:app_setting.port')}</label>
             <label>{t('admin:app_setting.port')}</label>
             <input
             <input
               className="form-control"
               className="form-control"
@@ -73,8 +73,8 @@ class MailSetting extends React.Component {
           </div>
           </div>
         </div>
         </div>
 
 
-        <div className="row mb-5">
-          <div className="col-xs-3 col-xs-offset-3">
+        <div className="row form-group mb-5">
+          <div className="col-md-3 offset-md-3">
             <label>{t('admin:app_setting.user')}</label>
             <label>{t('admin:app_setting.user')}</label>
             <input
             <input
               className="form-control"
               className="form-control"
@@ -83,7 +83,7 @@ class MailSetting extends React.Component {
               onChange={(e) => { adminAppContainer.changeSmtpUser(e.target.value) }}
               onChange={(e) => { adminAppContainer.changeSmtpUser(e.target.value) }}
             />
             />
           </div>
           </div>
-          <div className="col-xs-3">
+          <div className="col-md-3">
             <label>{t('Password')}</label>
             <label>{t('Password')}</label>
             <input
             <input
               className="form-control"
               className="form-control"

+ 6 - 5
src/client/js/components/Admin/App/PluginSetting.jsx

@@ -39,20 +39,21 @@ class PluginSetting extends React.Component {
 
 
     return (
     return (
       <React.Fragment>
       <React.Fragment>
-        <p className="well">{t('admin:app_setting.enable_plugin_loading')}</p>
+        <p className="card well">{t('admin:app_setting.enable_plugin_loading')}</p>
 
 
-        <div className="row mb-5">
-          <div className="col-xs-offset-3 col-xs-6 text-left">
-            <div className="checkbox checkbox-success">
+        <div className="row form-group mb-5">
+          <div className="offset-3 col-6 text-left">
+            <div className="custom-control custom-checkbox custom-checkbox-success">
               <input
               <input
                 id="isEnabledPlugins"
                 id="isEnabledPlugins"
+                className="custom-control-input"
                 type="checkbox"
                 type="checkbox"
                 checked={adminAppContainer.state.isEnabledPlugins}
                 checked={adminAppContainer.state.isEnabledPlugins}
                 onChange={(e) => {
                 onChange={(e) => {
                   adminAppContainer.changeIsEnabledPlugins(e.target.checked);
                   adminAppContainer.changeIsEnabledPlugins(e.target.checked);
                 }}
                 }}
               />
               />
-              <label htmlFor="isEnabledPlugins">{t('admin:app_setting.load_plugins')}</label>
+              <label className="custom-control-label" htmlFor="isEnabledPlugins">{t('admin:app_setting.load_plugins')}</label>
             </div>
             </div>
           </div>
           </div>
         </div>
         </div>

+ 40 - 42
src/client/js/components/Admin/App/SiteUrlSetting.jsx

@@ -38,51 +38,49 @@ class SiteUrlSetting extends React.Component {
 
 
     return (
     return (
       <React.Fragment>
       <React.Fragment>
-        <p className="well">{t('admin:app_setting.site_url_desc')}</p>
+        <p className="card well">{t('admin:app_setting.site_url_desc')}</p>
         {!adminAppContainer.state.isSetSiteUrl
         {!adminAppContainer.state.isSetSiteUrl
           && (<p className="alert alert-danger"><i className="icon-exclamation"></i> {t('admin:app_setting.site_url_warn')}</p>)}
           && (<p className="alert alert-danger"><i className="icon-exclamation"></i> {t('admin:app_setting.site_url_warn')}</p>)}
 
 
-        <div className="row">
-          <div className="col-md-12">
-            <div className="col-xs-offset-3">
-              <table className="table settings-table">
-                <colgroup>
-                  <col className="from-db" />
-                  <col className="from-env-vars" />
-                </colgroup>
-                <thead>
-                  <tr>
-                    <th>Database</th>
-                    <th>Environment variables</th>
-                  </tr>
-                </thead>
-                <tbody>
-                  <tr>
-                    <td>
-                      <input
-                        className="form-control"
-                        type="text"
-                        name="settingForm[app:siteUrl]"
-                        defaultValue={adminAppContainer.state.siteUrl || ''}
-                        onChange={(e) => { adminAppContainer.changeSiteUrl(e.target.value) }}
-                        placeholder="e.g. https://my.growi.org"
-                      />
-                      <p className="help-block">
-                        {/* eslint-disable-next-line react/no-danger */}
-                        <span dangerouslySetInnerHTML={{ __html: t('admin:app_setting.siteurl_help') }} />
-                      </p>
-                    </td>
-                    <td>
-                      <input className="form-control" type="text" value={adminAppContainer.state.envSiteUrl || ''} readOnly />
-                      <p className="help-block">
-                        {/* eslint-disable-next-line react/no-danger */}
-                        <span dangerouslySetInnerHTML={{ __html: t('admin:app_setting.use_env_var_if_empty', { variable: 'APP_SITE_URL' }) }} />
-                      </p>
-                    </td>
-                  </tr>
-                </tbody>
-              </table>
-            </div>
+        <div className="row form-group">
+          <div className="col-md-9 offset-md-3">
+            <table className="table settings-table">
+              <colgroup>
+                <col className="from-db" />
+                <col className="from-env-vars" />
+              </colgroup>
+              <thead>
+                <tr>
+                  <th>Database</th>
+                  <th>Environment variables</th>
+                </tr>
+              </thead>
+              <tbody>
+                <tr>
+                  <td>
+                    <input
+                      className="form-control"
+                      type="text"
+                      name="settingForm[app:siteUrl]"
+                      defaultValue={adminAppContainer.state.siteUrl || ''}
+                      onChange={(e) => { adminAppContainer.changeSiteUrl(e.target.value) }}
+                      placeholder="e.g. https://my.growi.org"
+                    />
+                    <p className="form-text text-muted">
+                      {/* eslint-disable-next-line react/no-danger */}
+                      <span dangerouslySetInnerHTML={{ __html: t('admin:app_setting.siteurl_help') }} />
+                    </p>
+                  </td>
+                  <td>
+                    <input className="form-control" type="text" value={adminAppContainer.state.envSiteUrl || ''} readOnly />
+                    <p className="form-text text-muted">
+                      {/* eslint-disable-next-line react/no-danger */}
+                      <span dangerouslySetInnerHTML={{ __html: t('admin:app_setting.use_env_var_if_empty', { variable: 'APP_SITE_URL' }) }} />
+                    </p>
+                  </td>
+                </tr>
+              </tbody>
+            </table>
           </div>
           </div>
         </div>
         </div>
 
 

+ 106 - 35
src/client/js/components/Admin/Common/AdminNavigation.jsx

@@ -11,42 +11,113 @@ const AdminNavigation = (props) => {
     return (pathname.startsWith(urljoin('/admin', path)));
     return (pathname.startsWith(urljoin('/admin', path)));
   };
   };
 
 
+  const getListGroupItemOrDropdownItemList = (isListGroupItems) => {
+    const pageTransitionClassName = isListGroupItems ? 'list-group-item list-group-item-action border-0 round-corner' : 'dropdown-item';
+    return (
+      <>
+        <a
+          href="/admin"
+          className={`${pageTransitionClassName} ${pathname === '/admin' && 'active'}`}
+        >
+          <i className="icon-fw icon-home"></i> {t('Wiki Management Home Page')}
+        </a>
+        <a
+          href="/admin/app"
+          className={`${pageTransitionClassName} ${isActiveMenu('/app') && 'active'}`}
+        >
+          <i className="icon-fw icon-settings"></i> {t('App Settings')}
+        </a>
+        <a
+          href="/admin/security"
+          className={`${pageTransitionClassName} ${isActiveMenu('/security') && 'active'}`}
+        >
+          <i className="icon-fw icon-shield"></i> {t('security_settings')}
+        </a>
+        <a
+          href="/admin/markdown"
+          className={`${pageTransitionClassName} ${isActiveMenu('/markdown') && 'active'}`}
+        >
+          <i className="icon-fw icon-note"></i> {t('Markdown Settings')}
+        </a>
+        <a
+          href="/admin/customize"
+          className={`${pageTransitionClassName} ${isActiveMenu('/customize') && 'active'}`}
+        >
+          <i className="icon-fw icon-wrench"></i> {t('Customize')}
+        </a>
+        <a
+          href="/admin/importer"
+          className={`${pageTransitionClassName} ${isActiveMenu('/importer') && 'active'}`}
+        >
+          <i className="icon-fw icon-cloud-upload"></i> {t('Import Data')}
+        </a>
+        <a
+          href="/admin/export"
+          className={`${pageTransitionClassName} ${isActiveMenu('/export') && 'active'}`}
+        >
+          <i className="icon-fw icon-cloud-download"></i> {t('Export Archive Data')}
+        </a>
+        <a
+          href="/admin/notification"
+          className={`${pageTransitionClassName} ${(isActiveMenu('/notification') || isActiveMenu('/global-notification')) && 'active'}`}
+        >
+          <i className="icon-fw icon-bell"></i> {t('Notification Settings')}
+        </a>
+        <a
+          href="/admin/users"
+          className={`${pageTransitionClassName} ${(isActiveMenu('/users')) && 'active'}`}
+        >
+          <i className="icon-fw icon-user"></i> {t('User_Management')}
+        </a>
+        <a
+          href="/admin/user-groups"
+          className={`${pageTransitionClassName} ${isActiveMenu('/user-group') && 'active'}`}
+        >
+          <i className="icon-fw icon-people"></i> {t('UserGroup Management')}
+        </a>
+        <a
+          href="/admin/search"
+          className={`${pageTransitionClassName} ${isActiveMenu('/search') && 'active'}`}
+        >
+          <i className="icon-fw icon-magnifier"></i> {t('Full Text Search Management')}
+        </a>
+      </>
+    );
+  };
+
   return (
   return (
-    <ul className="nav nav-pills nav-stacked">
-      <li className={`${pathname === '/admin' && 'active'}`}>
-        <a href="/admin"><i className="icon-fw icon-home"></i> { t('Wiki Management Home Page') }</a>
-      </li>
-      <li className={`${isActiveMenu('/app') && 'active'}`}>
-        <a href="/admin/app"><i className="icon-fw icon-settings"></i> { t('App Settings') }</a>
-      </li>
-      <li className={`${isActiveMenu('/security') && 'active'}`}>
-        <a href="/admin/security"><i className="icon-fw icon-shield"></i> { t('security_settings') }</a>
-      </li>
-      <li className={`${isActiveMenu('/markdown') && 'active'}`}>
-        <a href="/admin/markdown"><i className="icon-fw icon-note"></i> { t('Markdown Settings') }</a>
-      </li>
-      <li className={`${isActiveMenu('/customize') && 'active'}`}>
-        <a href="/admin/customize"><i className="icon-fw icon-wrench"></i> { t('Customize') }</a>
-      </li>
-      <li className={`${isActiveMenu('/importer') && 'active'}`}>
-        <a href="/admin/importer"><i className="icon-fw icon-cloud-upload"></i> { t('Import Data') }</a>
-      </li>
-      <li className={`${isActiveMenu('/export') && 'active'}`}>
-        <a href="/admin/export"><i className="icon-fw icon-cloud-download"></i> { t('Export Archive Data') }</a>
-      </li>
-      <li className={`${(isActiveMenu('/notification') || isActiveMenu('/global-notification')) && 'active'}`}>
-        <a href="/admin/notification"><i className="icon-fw icon-bell"></i> { t('Notification Settings') }</a>
-      </li>
-      <li className={`${(isActiveMenu('/users')) && 'active'}`}>
-        <a href="/admin/users"><i className="icon-fw icon-user"></i> { t('User_Management') }</a>
-      </li>
-      <li className={`${isActiveMenu('/user-group') && 'active'}`}>
-        <a href="/admin/user-groups"><i className="icon-fw icon-people"></i> { t('UserGroup Management') }</a>
-      </li>
-      <li className={`${isActiveMenu('/search') && 'active'}`}>
-        <a href="/admin/search"><i className="icon-fw icon-magnifier"></i> { t('Full Text Search Management') }</a>
-      </li>
-    </ul>
+    <div>
+      <div className="list-group admin-navigation d-none d-md-block">
+        {getListGroupItemOrDropdownItemList(true)}
+      </div>
+      <div className="dropdown d-block d-md-none">
+        <button
+          className="btn btn-outline-secondary dropdown-toggle col-12 text-right"
+          type="button"
+          id="dropdown-admin-navigation"
+          data-toggle="dropdown"
+          aria-haspopup="true"
+          aria-expanded="false"
+        >
+          <span className="float-left"><i className="icon-fw icon-home"></i>
+            {pathname === '/admin' && t('Wiki Management Home Page')}
+            {pathname === '/admin/app' && t('App Settings')}
+            {pathname === '/admin/security' && t('security_settings')}
+            {pathname === '/admin/markdown' && t('Markdown Settings')}
+            {pathname === '/admin/customize' && t('Customize')}
+            {pathname === '/admin/importer' && t('Import Data')}
+            {pathname === '/admin/export' && t('Export Archive Data')}
+            {pathname === ('/admin/notification' || '/admin/global-notification') && t('Notification Settings')}
+            {pathname === '/admin/users' && t('User_Management')}
+            {pathname === '/admin/user-groups' && t('UserGroup Management')}
+            {pathname === '/admin/search' && t('Full Text Search Management')}
+          </span>
+        </button>
+        <div className="dropdown-menu" aria-labelledby="dropdown-admin-navigation">
+          {getListGroupItemOrDropdownItemList(false)}
+        </div>
+      </div>
+    </div>
   );
   );
 };
 };
 
 

+ 9 - 13
src/client/js/components/Admin/Common/AdminUpdateButtonRow.jsx

@@ -2,21 +2,17 @@ import React from 'react';
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
 import { withTranslation } from 'react-i18next';
 import { withTranslation } from 'react-i18next';
 
 
-class AdminUpdateButtonRow extends React.PureComponent {
+const AdminUpdateButtonRow = (props) => {
+  const { t } = props;
 
 
-  render() {
-    const { t } = this.props;
-
-    return (
-      <div className="row my-3">
-        <div className="col-xs-offset-4 col-xs-5">
-          <button type="button" className="btn btn-primary" onClick={this.props.onClick} disabled={this.props.disabled}>{ t('Update') }</button>
-        </div>
+  return (
+    <div className="row my-3">
+      <div className="mx-auto">
+        <button type="button" className="btn btn-primary" onClick={props.onClick} disabled={props.disabled}>{ t('Update') }</button>
       </div>
       </div>
-    );
-  }
-
-}
+    </div>
+  );
+};
 
 
 AdminUpdateButtonRow.propTypes = {
 AdminUpdateButtonRow.propTypes = {
   t: PropTypes.func.isRequired, // i18next
   t: PropTypes.func.isRequired, // i18next

+ 5 - 5
src/client/js/components/Admin/Common/ProgressBar.jsx

@@ -17,13 +17,13 @@ class ProgressBar extends React.Component {
 
 
     return (
     return (
       <>
       <>
-        <h5 className="my-1">
+        <h6 className="my-1">
           {header}
           {header}
-          <div className="pull-right">{currentCount} / {totalCount}</div>
-        </h5>
-        <div className="progress progress-sm">
+          <div className="float-right">{currentCount} / {totalCount}</div>
+        </h6>
+        <div className="progress">
           <div
           <div
-            className={`progress-bar ${isActive ? 'progress-bar-info progress-bar-striped active' : 'progress-bar-success'}`}
+            className={`progress-bar ${isActive ? 'bg-info progress-bar-striped active' : 'bg-success'}`}
             style={{ width: `${percentage}%` }}
             style={{ width: `${percentage}%` }}
           >
           >
             <span className="sr-only">{percentage.toFixed(0)}% Complete</span>
             <span className="sr-only">{percentage.toFixed(0)}% Complete</span>

+ 0 - 4
src/client/js/components/Admin/Customize/Customize.jsx

@@ -10,7 +10,6 @@ import { createSubscribedElement } from '../../UnstatedUtils';
 import { toastError } from '../../../util/apiNotification';
 import { toastError } from '../../../util/apiNotification';
 
 
 import CustomizeLayoutSetting from './CustomizeLayoutSetting';
 import CustomizeLayoutSetting from './CustomizeLayoutSetting';
-import CustomizeBehaviorSetting from './CustomizeBehaviorSetting';
 import CustomizeFunctionSetting from './CustomizeFunctionSetting';
 import CustomizeFunctionSetting from './CustomizeFunctionSetting';
 import CustomizeHighlightSetting from './CustomizeHighlightSetting';
 import CustomizeHighlightSetting from './CustomizeHighlightSetting';
 import CustomizeCssSetting from './CustomizeCssSetting';
 import CustomizeCssSetting from './CustomizeCssSetting';
@@ -52,9 +51,6 @@ class Customize extends React.Component {
         <div className="mb-5">
         <div className="mb-5">
           <CustomizeLayoutSetting />
           <CustomizeLayoutSetting />
         </div>
         </div>
-        <div className="mb-5">
-          <CustomizeBehaviorSetting />
-        </div>
         <div className="mb-5">
         <div className="mb-5">
           <CustomizeFunctionSetting />
           <CustomizeFunctionSetting />
         </div>
         </div>

+ 0 - 38
src/client/js/components/Admin/Customize/CustomizeBehaviorOption.jsx

@@ -1,38 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import { withTranslation } from 'react-i18next';
-
-class CustomizeBehaviorOption extends React.PureComponent {
-
-  render() {
-
-    return (
-      <React.Fragment>
-        <h4>
-          <div className="radio radio-primary">
-            <input type="radio" id={`radioBehavior${this.props.behaviorType}`} checked={this.props.isSelected} onChange={this.props.onSelected} />
-            <label htmlFor={`radioBehavior${this.props.behaviorType}`}>
-              {/* eslint-disable-next-line react/no-danger */}
-              <span dangerouslySetInnerHTML={{ __html: this.props.labelHtml }} />
-            </label>
-          </div>
-        </h4>
-        {/* render layout description */}
-        {this.props.children}
-      </React.Fragment>
-    );
-  }
-
-}
-
-CustomizeBehaviorOption.propTypes = {
-  t: PropTypes.func.isRequired, // i18next
-
-  behaviorType: PropTypes.string.isRequired,
-  labelHtml: PropTypes.string.isRequired,
-  isSelected: PropTypes.bool.isRequired,
-  onSelected: PropTypes.func.isRequired,
-  children: PropTypes.object.isRequired,
-};
-
-export default withTranslation()(CustomizeBehaviorOption);

+ 0 - 95
src/client/js/components/Admin/Customize/CustomizeBehaviorSetting.jsx

@@ -1,95 +0,0 @@
-/* eslint-disable react/no-danger */
-import React from 'react';
-import PropTypes from 'prop-types';
-import { withTranslation } from 'react-i18next';
-
-import { createSubscribedElement } from '../../UnstatedUtils';
-import { toastSuccess, toastError } from '../../../util/apiNotification';
-
-import AppContainer from '../../../services/AppContainer';
-
-import AdminCustomizeContainer from '../../../services/AdminCustomizeContainer';
-import CustomizeBehaviorOption from './CustomizeBehaviorOption';
-import AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow';
-
-
-class CustomizeBehaviorSetting extends React.Component {
-
-  constructor(props) {
-    super(props);
-
-    this.onClickSubmit = this.onClickSubmit.bind(this);
-  }
-
-  async onClickSubmit() {
-    const { t, adminCustomizeContainer } = this.props;
-
-    try {
-      await adminCustomizeContainer.updateCustomizeBehavior();
-      toastSuccess(t('toaster.update_successed', { target: t('admin:customize_setting.behavior') }));
-    }
-    catch (err) {
-      toastError(err);
-    }
-  }
-
-  render() {
-    const { t, adminCustomizeContainer } = this.props;
-
-    return (
-      <React.Fragment>
-        <h2 className="admin-setting-header">{t('admin:customize_setting.behavior')}</h2>
-        <div className="row">
-          <div className="col-xs-6">
-            <CustomizeBehaviorOption
-              behaviorType="growi"
-              isSelected={adminCustomizeContainer.state.currentBehavior === 'growi'}
-              onSelected={() => adminCustomizeContainer.switchBehaviorType('growi')}
-              labelHtml={`GROWI Simplified behavior <small class="text-success">${t('admin:customize_setting.recommended')}</small>`}
-            >
-              <ul>
-                <li><span dangerouslySetInnerHTML={{ __html: t('admin:customize_setting.behavior_desc.growi_text1') }} /></li>
-                <li><span dangerouslySetInnerHTML={{ __html: t('admin:customize_setting.behavior_desc.growi_text2') }} /></li>
-                <li><span dangerouslySetInnerHTML={{ __html: t('admin:customize_setting.behavior_desc.growi_text3') }} /></li>
-              </ul>
-            </CustomizeBehaviorOption>
-          </div>
-
-          <div className="col-xs-6">
-            <CustomizeBehaviorOption
-              behaviorType="crowi-plus"
-              isSelected={adminCustomizeContainer.state.currentBehavior === 'crowi-plus'}
-              onSelected={() => adminCustomizeContainer.switchBehaviorType('crowi-plus')}
-              labelHtml="Crowi classic behavior"
-            >
-              <ul>
-                <li><span dangerouslySetInnerHTML={{ __html: t('admin:customize_setting.behavior_desc.crowi_text1') }} /></li>
-                <li><span dangerouslySetInnerHTML={{ __html: t('admin:customize_setting.behavior_desc.crowi_text2') }} /></li>
-                <ul>
-                  <li><span dangerouslySetInnerHTML={{ __html: t('admin:customize_setting.behavior_desc.crowi_text3') }} /></li>
-                </ul>
-                <li><span dangerouslySetInnerHTML={{ __html: t('admin:customize_setting.behavior_desc.crowi_text4') }} /></li>
-                <li><span dangerouslySetInnerHTML={{ __html: t('admin:customize_setting.behavior_desc.crowi_text5') }} /></li>
-              </ul>
-            </CustomizeBehaviorOption>
-          </div>
-        </div>
-
-        <AdminUpdateButtonRow onClick={this.onClickSubmit} disabled={adminCustomizeContainer.state.retrieveError != null} />
-      </React.Fragment>
-    );
-  }
-
-}
-
-const CustomizeBehaviorSettingWrapper = (props) => {
-  return createSubscribedElement(CustomizeBehaviorSetting, props, [AppContainer, AdminCustomizeContainer]);
-};
-
-CustomizeBehaviorSetting.propTypes = {
-  t: PropTypes.func.isRequired, // i18next
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
-  adminCustomizeContainer: PropTypes.instanceOf(AdminCustomizeContainer).isRequired,
-};
-
-export default withTranslation()(CustomizeBehaviorSettingWrapper);

+ 24 - 19
src/client/js/components/Admin/Customize/CustomizeCssSetting.jsx

@@ -1,6 +1,7 @@
 import React from 'react';
 import React from 'react';
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
 import { withTranslation } from 'react-i18next';
 import { withTranslation } from 'react-i18next';
+import { Card, CardBody } from 'reactstrap';
 
 
 import { createSubscribedElement } from '../../UnstatedUtils';
 import { createSubscribedElement } from '../../UnstatedUtils';
 import { toastSuccess, toastError } from '../../../util/apiNotification';
 import { toastSuccess, toastError } from '../../../util/apiNotification';
@@ -36,27 +37,31 @@ class CustomizeCssSetting extends React.Component {
 
 
     return (
     return (
       <React.Fragment>
       <React.Fragment>
-        <h2 className="admin-setting-header">{t('admin:customize_setting.custom_css')}</h2>
-        <p className="well">
-          {t('admin:customize_setting.write_css')}<br />
-          {t('admin:customize_setting.reflect_change')}
-        </p>
-        <div className="form-group">
-          <div className="col-xs-12">
-            <CustomCssEditor
-              value={adminCustomizeContainer.state.currentCustomizeCss || ''}
-              onChange={(inputValue) => { adminCustomizeContainer.changeCustomizeCss(inputValue) }}
-            />
-          </div>
-          <div className="col-xs-12">
-            <p className="help-block text-right">
-              <i className="fa fa-fw fa-keyboard-o" aria-hidden="true" />
-              {t('admin:customize_setting.ctrl_space')}
-            </p>
+        <div className="row">
+          <div className="col-12">
+            <h2 className="admin-setting-header">{t('admin:customize_setting.custom_css')}</h2>
+
+            <Card className="card well my-3">
+              <CardBody className="px-0 py-2">
+                { t('admin:customize_setting.write_css') }<br />
+                { t('admin:customize_setting.reflect_change') }
+              </CardBody>
+            </Card>
+
+            <div className="form-group">
+              <CustomCssEditor
+                value={adminCustomizeContainer.state.currentCustomizeCss || ''}
+                onChange={(inputValue) => { adminCustomizeContainer.changeCustomizeCss(inputValue) }}
+              />
+              <p className="form-text text-muted text-right">
+                <i className="fa fa-fw fa-keyboard-o" aria-hidden="true" />
+                {t('admin:customize_setting.ctrl_space')}
+              </p>
+            </div>
+
+            <AdminUpdateButtonRow onClick={this.onClickSubmit} disabled={adminCustomizeContainer.state.retrieveError != null} />
           </div>
           </div>
         </div>
         </div>
-
-        <AdminUpdateButtonRow onClick={this.onClickSubmit} disabled={adminCustomizeContainer.state.retrieveError != null} />
       </React.Fragment>
       </React.Fragment>
     );
     );
   }
   }

+ 3 - 2
src/client/js/components/Admin/Customize/CustomizeFunctionOption.jsx

@@ -7,14 +7,15 @@ class CustomizeFunctionOption extends React.PureComponent {
   render() {
   render() {
     return (
     return (
       <React.Fragment>
       <React.Fragment>
-        <div className="checkbox checkbox-success">
+        <div className="custom-control custom-checkbox custom-checkbox-success">
           <input
           <input
+            className="custom-control-input"
             type="checkbox"
             type="checkbox"
             id={this.props.optionId}
             id={this.props.optionId}
             checked={this.props.isChecked}
             checked={this.props.isChecked}
             onChange={this.props.onChecked}
             onChange={this.props.onChecked}
           />
           />
-          <label htmlFor={this.props.optionId}>
+          <label className="custom-control-label" htmlFor={this.props.optionId}>
             <strong>{this.props.label}</strong>
             <strong>{this.props.label}</strong>
           </label>
           </label>
         </div>
         </div>

+ 127 - 109
src/client/js/components/Admin/Customize/CustomizeFunctionSetting.jsx

@@ -1,6 +1,10 @@
 import React from 'react';
 import React from 'react';
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
 import { withTranslation } from 'react-i18next';
 import { withTranslation } from 'react-i18next';
+import {
+  Card, CardBody,
+  Dropdown, DropdownToggle, DropdownMenu, DropdownItem,
+} from 'reactstrap';
 
 
 import { createSubscribedElement } from '../../UnstatedUtils';
 import { createSubscribedElement } from '../../UnstatedUtils';
 import { toastSuccess, toastError } from '../../../util/apiNotification';
 import { toastSuccess, toastError } from '../../../util/apiNotification';
@@ -11,14 +15,23 @@ import AdminCustomizeContainer from '../../../services/AdminCustomizeContainer';
 import AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow';
 import AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow';
 import CustomizeFunctionOption from './CustomizeFunctionOption';
 import CustomizeFunctionOption from './CustomizeFunctionOption';
 
 
-class CustomizeBehaviorSetting extends React.Component {
+class CustomizeFunctionSetting extends React.Component {
 
 
   constructor(props) {
   constructor(props) {
     super(props);
     super(props);
 
 
+    this.state = {
+      isDropdownOpen: false,
+    };
+
+    this.onToggleDropdown = this.onToggleDropdown.bind(this);
     this.onClickSubmit = this.onClickSubmit.bind(this);
     this.onClickSubmit = this.onClickSubmit.bind(this);
   }
   }
 
 
+  onToggleDropdown() {
+    this.setState({ isDropdownOpen: !this.state.isDropdownOpen });
+  }
+
   async onClickSubmit() {
   async onClickSubmit() {
     const { t, adminCustomizeContainer } = this.props;
     const { t, adminCustomizeContainer } = this.props;
 
 
@@ -36,133 +49,138 @@ class CustomizeBehaviorSetting extends React.Component {
 
 
     return (
     return (
       <React.Fragment>
       <React.Fragment>
-        <h2 className="admin-setting-header">{t('admin:customize_setting.function')}</h2>
-        <p className="well">{t('admin:customize_setting.function_desc')}</p>
-
-        <div className="form-group row">
-          <div className="col-xs-offset-3 col-xs-6 text-left">
-            <CustomizeFunctionOption
-              optionId="isEnabledTimeline"
-              label={t('admin:customize_setting.function_options.timeline')}
-              isChecked={adminCustomizeContainer.state.isEnabledTimeline}
-              onChecked={() => { adminCustomizeContainer.switchEnableTimeline() }}
-            >
-              <p className="help-block">
-                {t('admin:customize_setting.function_options.timeline_desc1')}<br />
-                {t('admin:customize_setting.function_options.timeline_desc2')}<br />
-                {t('admin:customize_setting.function_options.timeline_desc3')}
-              </p>
-            </CustomizeFunctionOption>
-          </div>
-        </div>
+        <div className="row">
+          <div className="col-12">
+            <h2 className="admin-setting-header">{t('admin:customize_setting.function')}</h2>
+            <Card className="card well my-3">
+              <CardBody className="px-0 py-2">
+                {t('admin:customize_setting.function_desc')}
+              </CardBody>
+            </Card>
+
+
+            <div className="form-group row">
+              <div className="offset-md-3 col-md-6 text-left">
+                <CustomizeFunctionOption
+                  optionId="isEnabledTimeline"
+                  label={t('admin:customize_setting.function_options.timeline')}
+                  isChecked={adminCustomizeContainer.state.isEnabledTimeline}
+                  onChecked={() => { adminCustomizeContainer.switchEnableTimeline() }}
+                >
+                  <p className="form-text text-muted">
+                    {t('admin:customize_setting.function_options.timeline_desc1')}<br />
+                    {t('admin:customize_setting.function_options.timeline_desc2')}<br />
+                    {t('admin:customize_setting.function_options.timeline_desc3')}
+                  </p>
+                </CustomizeFunctionOption>
+              </div>
+            </div>
 
 
-        <div className="form-group row">
-          <div className="col-xs-offset-3 col-xs-6 text-left">
-            <CustomizeFunctionOption
-              optionId="isSavedStatesOfTabChanges"
-              label={t('admin:customize_setting.function_options.tab_switch')}
-              isChecked={adminCustomizeContainer.state.isSavedStatesOfTabChanges}
-              onChecked={() => { adminCustomizeContainer.switchSavedStatesOfTabChanges() }}
-            >
-              <p className="help-block">
-                {t('admin:customize_setting.function_options.tab_switch_desc1')}<br />
-                {t('admin:customize_setting.function_options.tab_switch_desc2')}
-              </p>
-            </CustomizeFunctionOption>
-          </div>
-        </div>
+            <div className="form-group row">
+              <div className="offset-md-3 col-md-6 text-left">
+                <CustomizeFunctionOption
+                  optionId="isSavedStatesOfTabChanges"
+                  label={t('admin:customize_setting.function_options.tab_switch')}
+                  isChecked={adminCustomizeContainer.state.isSavedStatesOfTabChanges}
+                  onChecked={() => { adminCustomizeContainer.switchSavedStatesOfTabChanges() }}
+                >
+                  <p className="form-text text-muted">
+                    {t('admin:customize_setting.function_options.tab_switch_desc1')}<br />
+                    {t('admin:customize_setting.function_options.tab_switch_desc2')}
+                  </p>
+                </CustomizeFunctionOption>
+              </div>
+            </div>
 
 
-        <div className="form-group row">
-          <div className="col-xs-offset-3 col-xs-6 text-left">
-            <CustomizeFunctionOption
-              optionId="isEnabledAttachTitleHeader"
-              label={t('admin:customize_setting.function_options.attach_title_header')}
-              isChecked={adminCustomizeContainer.state.isEnabledAttachTitleHeader}
-              onChecked={() => { adminCustomizeContainer.switchEnabledAttachTitleHeader() }}
-            >
-              <p className="help-block">
-                {t('admin:customize_setting.function_options.attach_title_header_desc')}
-              </p>
-            </CustomizeFunctionOption>
-          </div>
-        </div>
+            <div className="form-group row">
+              <div className="offset-md-3 col-md-6 text-left">
+                <CustomizeFunctionOption
+                  optionId="isEnabledAttachTitleHeader"
+                  label={t('admin:customize_setting.function_options.attach_title_header')}
+                  isChecked={adminCustomizeContainer.state.isEnabledAttachTitleHeader}
+                  onChecked={() => { adminCustomizeContainer.switchEnabledAttachTitleHeader() }}
+                >
+                  <p className="form-text text-muted">
+                    {t('admin:customize_setting.function_options.attach_title_header_desc')}
+                  </p>
+                </CustomizeFunctionOption>
+              </div>
+            </div>
 
 
-        <div className="form-group row">
-          <div className="col-xs-offset-3 col-xs-6 text-left">
-            <div className="my-0 btn-group">
-              <label>{t('admin:customize_setting.function_options.recent_created__n_draft_num_desc')}</label>
-              <div className="dropdown">
-                <button className="btn btn-default dropdown-toggle w-100" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
-                  <span className="pull-left">{adminCustomizeContainer.state.currentRecentCreatedLimit}</span>
-                  <span className="bs-caret pull-right">
-                    <span className="caret" />
-                  </span>
-                </button>
-                {/* TODO adjust dropdown after BS4 */}
-                <ul className="dropdown-menu" role="menu">
-                  <li key={10} role="presentation" type="button" onClick={() => { adminCustomizeContainer.switchRecentCreatedLimit(10) }}>
-                    <a role="menuitem">10</a>
-                  </li>
-                  <li key={30} role="presentation" type="button" onClick={() => { adminCustomizeContainer.switchRecentCreatedLimit(30) }}>
-                    <a role="menuitem">30</a>
-                  </li>
-                  <li key={50} role="presentation" type="button" onClick={() => { adminCustomizeContainer.switchRecentCreatedLimit(50) }}>
-                    <a role="menuitem">50</a>
-                  </li>
-                </ul>
+            <div className="form-group row">
+              <div className="offset-md-3 col-md-6 text-left">
+                <div className="my-0 w-100">
+                  <label>{t('admin:customize_setting.function_options.recent_created__n_draft_num_desc')}</label>
+                </div>
+                <Dropdown isOpen={this.state.isDropdownOpen} toggle={this.onToggleDropdown}>
+                  <DropdownToggle className="text-right col-6" caret>
+                    <span className="float-left">{adminCustomizeContainer.state.currentRecentCreatedLimit}</span>
+                  </DropdownToggle>
+                  <DropdownMenu className="dropdown-menu" role="menu">
+                    <DropdownItem key={10} role="presentation" onClick={() => { adminCustomizeContainer.switchRecentCreatedLimit(10) }}>
+                      <a role="menuitem">10</a>
+                    </DropdownItem>
+                    <DropdownItem key={30} role="presentation" onClick={() => { adminCustomizeContainer.switchRecentCreatedLimit(30) }}>
+                      <a role="menuitem">30</a>
+                    </DropdownItem>
+                    <DropdownItem key={50} role="presentation" onClick={() => { adminCustomizeContainer.switchRecentCreatedLimit(50) }}>
+                      <a role="menuitem">50</a>
+                    </DropdownItem>
+                  </DropdownMenu>
+                </Dropdown>
+                <p className="form-text text-muted">
+                  {t('admin:customize_setting.function_options.recently_created_n_draft_num_desc')}
+                </p>
               </div>
               </div>
-              <p className="help-block">
-                {t('admin:customize_setting.function_options.recently_created_n_draft_num_desc')}
-              </p>
             </div>
             </div>
-          </div>
-        </div>
 
 
-        <div className="form-group row">
-          <div className="col-xs-offset-3 col-xs-6 text-left">
-            <CustomizeFunctionOption
-              optionId="isEnabledStaleNotification"
-              label={t('admin:customize_setting.function_options.stale_notification')}
-              isChecked={adminCustomizeContainer.state.isEnabledStaleNotification}
-              onChecked={() => { adminCustomizeContainer.switchEnableStaleNotification() }}
-            >
-              <p className="help-block">
-                {t('admin:customize_setting.function_options.stale_notification_desc')}
-              </p>
-            </CustomizeFunctionOption>
-          </div>
-        </div>
+            <div className="form-group row">
+              <div className="offset-md-3 col-md-6 text-left">
+                <CustomizeFunctionOption
+                  optionId="isEnabledStaleNotification"
+                  label={t('admin:customize_setting.function_options.stale_notification')}
+                  isChecked={adminCustomizeContainer.state.isEnabledStaleNotification}
+                  onChecked={() => { adminCustomizeContainer.switchEnableStaleNotification() }}
+                >
+                  <p className="form-text text-muted">
+                    {t('admin:customize_setting.function_options.stale_notification_desc')}
+                  </p>
+                </CustomizeFunctionOption>
+              </div>
+            </div>
+
+            <div className="form-group row">
+              <div className="offset-md-3 col-md-6 text-left">
+                <CustomizeFunctionOption
+                  optionId="isAllReplyShown"
+                  label={t('admin:customize_setting.function_options.show_all_reply_comments')}
+                  isChecked={adminCustomizeContainer.state.isAllReplyShown || false}
+                  onChecked={() => { adminCustomizeContainer.switchIsAllReplyShown() }}
+                >
+                  <p className="form-text text-muted">
+                    {t('admin:customize_setting.function_options.show_all_reply_comments_desc')}
+                  </p>
+                </CustomizeFunctionOption>
+              </div>
+            </div>
 
 
-        <div className="form-group row">
-          <div className="col-xs-offset-3 col-xs-6 text-left">
-            <CustomizeFunctionOption
-              optionId="isAllReplyShown"
-              label={t('admin:customize_setting.function_options.show_all_reply_comments')}
-              isChecked={adminCustomizeContainer.state.isAllReplyShown || false}
-              onChecked={() => { adminCustomizeContainer.switchIsAllReplyShown() }}
-            >
-              <p className="help-block">
-                {t('admin:customize_setting.function_options.show_all_reply_comments_desc')}
-              </p>
-            </CustomizeFunctionOption>
+            <AdminUpdateButtonRow onClick={this.onClickSubmit} disabled={adminCustomizeContainer.state.retrieveError != null} />
           </div>
           </div>
         </div>
         </div>
-
-        <AdminUpdateButtonRow onClick={this.onClickSubmit} disabled={adminCustomizeContainer.state.retrieveError != null} />
       </React.Fragment>
       </React.Fragment>
     );
     );
   }
   }
 
 
 }
 }
 
 
-const CustomizeBehaviorSettingWrapper = (props) => {
-  return createSubscribedElement(CustomizeBehaviorSetting, props, [AppContainer, AdminCustomizeContainer]);
+const CustomizeFunctionSettingWrapper = (props) => {
+  return createSubscribedElement(CustomizeFunctionSetting, props, [AppContainer, AdminCustomizeContainer]);
 };
 };
 
 
-CustomizeBehaviorSetting.propTypes = {
+CustomizeFunctionSetting.propTypes = {
   t: PropTypes.func.isRequired, // i18next
   t: PropTypes.func.isRequired, // i18next
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
   adminCustomizeContainer: PropTypes.instanceOf(AdminCustomizeContainer).isRequired,
   adminCustomizeContainer: PropTypes.instanceOf(AdminCustomizeContainer).isRequired,
 };
 };
 
 
-export default withTranslation()(CustomizeBehaviorSettingWrapper);
+export default withTranslation()(CustomizeFunctionSettingWrapper);

+ 35 - 29
src/client/js/components/Admin/Customize/CustomizeHeaderSetting.jsx

@@ -1,6 +1,7 @@
 import React from 'react';
 import React from 'react';
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
 import { withTranslation } from 'react-i18next';
 import { withTranslation } from 'react-i18next';
+import { Card, CardBody } from 'reactstrap';
 
 
 import { createSubscribedElement } from '../../UnstatedUtils';
 import { createSubscribedElement } from '../../UnstatedUtils';
 import { toastSuccess, toastError } from '../../../util/apiNotification';
 import { toastSuccess, toastError } from '../../../util/apiNotification';
@@ -36,36 +37,41 @@ class CustomizeHeaderSetting extends React.Component {
 
 
     return (
     return (
       <React.Fragment>
       <React.Fragment>
-        <h2 className="admin-setting-header">{t('admin:customize_setting.custom_header')}</h2>
-
-        <p
-          className="well"
-          // eslint-disable-next-line react/no-danger
-          dangerouslySetInnerHTML={{ __html: t('admin:customize_setting.custom_header_detail') }}
-        />
-
-        <div className="help-block">
-          {t('Example')}:
-          <pre className="hljs">
-            {/* eslint-disable-next-line react/no-unescaped-entities */}
-            <code>&lt;script src="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@9.13.0/build/languages/yaml.min.js" defer&gt;&lt;/script&gt;</code>
-          </pre>
+        <div className="row">
+          <div className="col-12">
+            <h2 className="admin-setting-header">{t('admin:customize_setting.custom_header')}</h2>
+
+            <Card className="card well my-3">
+              <CardBody className="px-0 py-2">
+                <span
+                  // eslint-disable-next-line react/no-danger
+                  dangerouslySetInnerHTML={{ __html: t('admin:customize_setting.custom_header_detail') }}
+                />
+              </CardBody>
+            </Card>
+            <div className="form-text text-muted">
+              { t('Example') }:
+              <pre className="hljs">
+                {/* eslint-disable-next-line react/no-unescaped-entities */}
+                <code className="text-wrap">&lt;script src="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@9.13.0/build/languages/yaml.min.js"
+                defer&gt;&lt;/script&gt;
+                </code>
+              </pre>
+            </div>
+
+            <div className="form-group">
+              <CustomHeaderEditor
+                value={adminCustomizeContainer.state.currentCustomizeHeader || ''}
+                onChange={(inputValue) => { adminCustomizeContainer.changeCustomizeHeader(inputValue) }}
+              />
+              <p className="form-text text-muted text-right">
+                <i className="fa fa-fw fa-keyboard-o" aria-hidden="true"></i>
+                {t('admin:customize_setting.ctrl_space')}
+              </p>
+            </div>
+            <AdminUpdateButtonRow onClick={this.onClickSubmit} disabled={adminCustomizeContainer.state.retrieveError != null} />
+          </div>
         </div>
         </div>
-
-        <div className="col-xs-12">
-          <CustomHeaderEditor
-            value={adminCustomizeContainer.state.currentCustomizeHeader || ''}
-            onChange={(inputValue) => { adminCustomizeContainer.changeCustomizeHeader(inputValue) }}
-          />
-        </div>
-        <div className="col-xs-12">
-          <p className="help-block text-right">
-            <i className="fa fa-fw fa-keyboard-o" aria-hidden="true"></i>
-            {t('admin:customize_setting.ctrl_space')}
-          </p>
-        </div>
-
-        <AdminUpdateButtonRow onClick={this.onClickSubmit} disabled={adminCustomizeContainer.state.retrieveError != null} />
       </React.Fragment>
       </React.Fragment>
     );
     );
   }
   }

+ 63 - 44
src/client/js/components/Admin/Customize/CustomizeHighlightSetting.jsx

@@ -2,6 +2,9 @@
 import React from 'react';
 import React from 'react';
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
 import { withTranslation } from 'react-i18next';
 import { withTranslation } from 'react-i18next';
+import {
+  Dropdown, DropdownToggle, DropdownMenu, DropdownItem,
+} from 'reactstrap';
 
 
 import { createSubscribedElement } from '../../UnstatedUtils';
 import { createSubscribedElement } from '../../UnstatedUtils';
 import { toastSuccess, toastError } from '../../../util/apiNotification';
 import { toastSuccess, toastError } from '../../../util/apiNotification';
@@ -16,9 +19,18 @@ class CustomizeHighlightSetting extends React.Component {
   constructor(props) {
   constructor(props) {
     super(props);
     super(props);
 
 
+    this.state = {
+      isDropdownOpen: false,
+    };
+
+    this.onToggleDropdown = this.onToggleDropdown.bind(this);
     this.onClickSubmit = this.onClickSubmit.bind(this);
     this.onClickSubmit = this.onClickSubmit.bind(this);
   }
   }
 
 
+  onToggleDropdown() {
+    this.setState({ isDropdownOpen: !this.state.isDropdownOpen });
+  }
+
   async onClickSubmit() {
   async onClickSubmit() {
     const { t, adminCustomizeContainer } = this.props;
     const { t, adminCustomizeContainer } = this.props;
 
 
@@ -64,62 +76,69 @@ class CustomizeHighlightSetting extends React.Component {
       const isBorderEnable = option[1].border;
       const isBorderEnable = option[1].border;
 
 
       menuItem.push(
       menuItem.push(
-        <li key={styleId} role="presentation" type="button" onClick={() => adminCustomizeContainer.switchHighlightJsStyle(styleId, styleName, isBorderEnable)}>
-          <a role="button">{styleName}</a>
-        </li>,
+        <DropdownItem
+          key={styleId}
+          role="presentation"
+          onClick={() => adminCustomizeContainer.switchHighlightJsStyle(styleId, styleName, isBorderEnable)}
+        >
+          <a role="menuitem">{styleName}</a>
+        </DropdownItem>,
       );
       );
     });
     });
 
 
     return (
     return (
       <React.Fragment>
       <React.Fragment>
-        <h2 className="admin-setting-header">{t('admin:customize_setting.code_highlight')}</h2>
-
-        <div className="form-group row">
-          <div className="col-xs-offset-3 col-xs-6 text-left">
-            <div className="my-0 btn-group">
-              <label>{t('admin:customize_setting.theme')}</label>
-              <div className="dropdown">
-                <button className="btn btn-default dropdown-toggle w-100" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
-                  <span className="pull-left">{adminCustomizeContainer.state.currentHighlightJsStyleName}</span>
-                  <span className="bs-caret pull-right">
-                    <span className="caret" />
-                  </span>
-                </button>
-                {/* TODO adjust dropdown after BS4 */}
-                <ul className="dropdown-menu" role="menu">
-                  {menuItem}
-                </ul>
+        <div className="row">
+          <div className="col-12">
+            <h2 className="admin-setting-header">{t('admin:customize_setting.code_highlight')}</h2>
+
+            <div className="form-group row">
+              <div className="offset-md-3 col-md-6 text-left">
+                <div className="my-0">
+                  <label>{t('admin:customize_setting.theme')}</label>
+                </div>
+                <Dropdown isOpen={this.state.isDropdownOpen} toggle={this.onToggleDropdown}>
+                  <DropdownToggle className="text-right col-6" caret>
+                    <span className="float-left">{adminCustomizeContainer.state.currentHighlightJsStyleName}</span>
+                  </DropdownToggle>
+                  <DropdownMenu className="dropdown-menu" role="menu">
+                    {menuItem}
+                  </DropdownMenu>
+                </Dropdown>
+                <p className="form-text text-warning">
+                  {/* eslint-disable-next-line react/no-danger */}
+                  <span dangerouslySetInnerHTML={{ __html: t('admin:customize_setting.nocdn_desc') }} />
+                </p>
+              </div>
+            </div>
+
+            <div className="form-group row">
+              <div className="offset-md-3 col-md-6 text-left">
+                <div className="custom-control custom-switch custom-checkbox-success">
+                  <input
+                    type="checkbox"
+                    className="custom-control-input"
+                    id="highlightBorder"
+                    checked={adminCustomizeContainer.state.isHighlightJsStyleBorderEnabled}
+                    onChange={() => { adminCustomizeContainer.switchHighlightJsStyleBorder() }}
+                  />
+                  <label className="custom-control-label" htmlFor="highlightBorder">
+                    <strong>Border</strong>
+                  </label>
+                </div>
               </div>
               </div>
-              {/* eslint-disable-next-line react/no-danger */}
-              <p className="help-block text-warning"><span dangerouslySetInnerHTML={{ __html: t('admin:customize_setting.nocdn_desc') }} /></p>
             </div>
             </div>
-          </div>
-        </div>
 
 
-        <div className="form-group row">
-          <div className="col-xs-offset-3 col-xs-6 text-left">
-            <div className="checkbox checkbox-success">
-              <input
-                type="checkbox"
-                id="highlightBorder"
-                checked={adminCustomizeContainer.state.isHighlightJsStyleBorderEnabled}
-                onChange={() => { adminCustomizeContainer.switchHighlightJsStyleBorder() }}
-              />
-              <label htmlFor="highlightBorder">
-                <strong>Border</strong>
-              </label>
+            <div className="form-text text-muted">
+              <label>Examples:</label>
+              <div className="wiki">
+                {this.renderHljsDemo()}
+              </div>
             </div>
             </div>
-          </div>
-        </div>
 
 
-        <div className="help-block">
-          <label>Examples:</label>
-          <div className="wiki">
-            {this.renderHljsDemo()}
+            <AdminUpdateButtonRow onClick={this.onClickSubmit} disabled={adminCustomizeContainer.state.retrieveError != null} />
           </div>
           </div>
         </div>
         </div>
-
-        <AdminUpdateButtonRow onClick={this.onClickSubmit} disabled={adminCustomizeContainer.state.retrieveError != null} />
       </React.Fragment>
       </React.Fragment>
     );
     );
   }
   }

+ 9 - 3
src/client/js/components/Admin/Customize/CustomizeLayoutOption.jsx

@@ -10,9 +10,15 @@ class CustomizeLayoutOption extends React.Component {
     return (
     return (
       <React.Fragment>
       <React.Fragment>
         <h4>
         <h4>
-          <div className="radio radio-primary">
-            <input type="radio" id={`radio-layout-${layoutType}`} checked={this.props.isSelected} onChange={this.props.onSelected} />
-            <label htmlFor={`radio-layout-${layoutType}`}>
+          <div className="custom-control custom-radio">
+            <input
+              type="radio"
+              className="custom-control-input"
+              id={`radio-layout-${layoutType}`}
+              checked={this.props.isSelected}
+              onChange={this.props.onSelected}
+            />
+            <label className="custom-control-label" htmlFor={`radio-layout-${layoutType}`}>
               {/* eslint-disable-next-line react/no-danger */}
               {/* eslint-disable-next-line react/no-danger */}
               <span dangerouslySetInnerHTML={{ __html: this.props.labelHtml }} />
               <span dangerouslySetInnerHTML={{ __html: this.props.labelHtml }} />
             </label>
             </label>

+ 17 - 29
src/client/js/components/Admin/Customize/CustomizeLayoutOptions.jsx

@@ -14,8 +14,8 @@ class CustomizeLayoutOptions extends React.Component {
     const { t, adminCustomizeContainer } = this.props;
     const { t, adminCustomizeContainer } = this.props;
 
 
     return (
     return (
-      <div className="row">
-        <div className="col-sm-4">
+      <div className="row row-cols-1 row-cols-md-2">
+        <div className="col text-center">
           <CustomizeLayoutOption
           <CustomizeLayoutOption
             layoutType="crowi-plus"
             layoutType="crowi-plus"
             isSelected={adminCustomizeContainer.state.currentLayout === 'growi'}
             isSelected={adminCustomizeContainer.state.currentLayout === 'growi'}
@@ -23,15 +23,17 @@ class CustomizeLayoutOptions extends React.Component {
             labelHtml={`GROWI enhanced layout <small class="text-success">${t('admin:customize_setting.recommended')}</small>`}
             labelHtml={`GROWI enhanced layout <small class="text-success">${t('admin:customize_setting.recommended')}</small>`}
           >
           >
             <h4>{t('admin:customize_setting.layout_desc.growi_title')}</h4>
             <h4>{t('admin:customize_setting.layout_desc.growi_title')}</h4>
-            <ul>
-              <li>{t('admin:customize_setting.layout_desc.growi_text1')}</li>
-              <li>{t('admin:customize_setting.layout_desc.growi_text2')}</li>
-              <li>{t('admin:customize_setting.layout_desc.growi_text3')}</li>
-            </ul>
+            <div className="text-justify d-inline-block">
+              <ul>
+                <li>{t('admin:customize_setting.layout_desc.growi_text1')}</li>
+                <li>{t('admin:customize_setting.layout_desc.growi_text2')}</li>
+                <li>{t('admin:customize_setting.layout_desc.growi_text3')}</li>
+              </ul>
+            </div>
           </CustomizeLayoutOption>
           </CustomizeLayoutOption>
         </div>
         </div>
 
 
-        <div className="col-sm-4">
+        <div className="col text-center">
           <CustomizeLayoutOption
           <CustomizeLayoutOption
             layoutType="kibela"
             layoutType="kibela"
             isSelected={adminCustomizeContainer.state.currentLayout === 'kibela'}
             isSelected={adminCustomizeContainer.state.currentLayout === 'kibela'}
@@ -39,27 +41,13 @@ class CustomizeLayoutOptions extends React.Component {
             labelHtml="Kibela like layout"
             labelHtml="Kibela like layout"
           >
           >
             <h4>{t('admin:customize_setting.layout_desc.kibela_title')}</h4>
             <h4>{t('admin:customize_setting.layout_desc.kibela_title')}</h4>
-            <ul>
-              <li>{t('admin:customize_setting.layout_desc.kibela_text1')}</li>
-              <li>{t('admin:customize_setting.layout_desc.kibela_text2')}</li>
-              <li>{t('admin:customize_setting.layout_desc.kibela_text3')}</li>
-            </ul>
-          </CustomizeLayoutOption>
-        </div>
-
-        <div className="col-sm-4">
-          <CustomizeLayoutOption
-            layoutType="classic"
-            isSelected={adminCustomizeContainer.state.currentLayout === 'crowi'}
-            onSelected={() => adminCustomizeContainer.switchLayoutType('crowi')}
-            labelHtml="Crowi Classic Layout"
-          >
-            <h4>{t('admin:customize_setting.layout_desc.crowi_title')}</h4>
-            <ul>
-              <li>{t('admin:customize_setting.layout_desc.crowi_text1')}</li>
-              <li>{t('admin:customize_setting.layout_desc.crowi_text2')}</li>
-              <li>{t('admin:customize_setting.layout_desc.crowi_text3')}</li>
-            </ul>
+            <div className="text-justify d-inline-block">
+              <ul>
+                <li>{t('admin:customize_setting.layout_desc.kibela_text1')}</li>
+                <li>{t('admin:customize_setting.layout_desc.kibela_text2')}</li>
+                <li>{t('admin:customize_setting.layout_desc.kibela_text3')}</li>
+              </ul>
+            </div>
           </CustomizeLayoutOption>
           </CustomizeLayoutOption>
         </div>
         </div>
       </div>
       </div>

+ 14 - 6
src/client/js/components/Admin/Customize/CustomizeLayoutSetting.jsx

@@ -48,12 +48,20 @@ class CustomizeLayoutSetting extends React.Component {
 
 
     return (
     return (
       <React.Fragment>
       <React.Fragment>
-        <h2 className="admin-setting-header">{t('admin:customize_setting.layout')}</h2>
-        <CustomizeLayoutOptions />
-        <h2 className="admin-setting-header">{t('admin:customize_setting.theme')}</h2>
-        {this.renderDevAlert()}
-        <CustomizeThemeOptions />
-        <AdminUpdateButtonRow onClick={this.onClickSubmit} disabled={adminCustomizeContainer.state.retrieveError != null} />
+        <div className="row">
+          <div className="col-12">
+            <h2 className="admin-setting-header">{t('admin:customize_setting.layout')}</h2>
+            <CustomizeLayoutOptions />
+          </div>
+        </div>
+        <div className="row">
+          <div className="col-12">
+            <h2 className="admin-setting-header">{t('admin:customize_setting.theme')}</h2>
+            {this.renderDevAlert()}
+            <CustomizeThemeOptions />
+            <AdminUpdateButtonRow onClick={this.onClickSubmit} disabled={adminCustomizeContainer.state.retrieveError != null} />
+          </div>
+        </div>
       </React.Fragment>
       </React.Fragment>
     );
     );
   }
   }

+ 57 - 42
src/client/js/components/Admin/Customize/CustomizeScriptSetting.jsx

@@ -1,6 +1,7 @@
 import React from 'react';
 import React from 'react';
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
 import { withTranslation } from 'react-i18next';
 import { withTranslation } from 'react-i18next';
+import { Card, CardBody } from 'reactstrap';
 
 
 import { createSubscribedElement } from '../../UnstatedUtils';
 import { createSubscribedElement } from '../../UnstatedUtils';
 import { toastSuccess, toastError } from '../../../util/apiNotification';
 import { toastSuccess, toastError } from '../../../util/apiNotification';
@@ -44,50 +45,64 @@ class CustomizeScriptSetting extends React.Component {
 
 
     return (
     return (
       <React.Fragment>
       <React.Fragment>
-        <h2 className="admin-setting-header">{t('admin:customize_setting.custom_script')}</h2>
-        <p className="well">
-          {t('admin:customize_setting.write_java')}<br />
-          {t('admin:customize_setting.reflect_change')}
-        </p>
-
-        <div className="help-block">
-          Placeholders:<br />
-          (Available after <code>load</code> event)
-          <dl className="dl-horizontal">
-            <dt><code>$</code></dt>
-            <dd>jQuery instance</dd>
-            <dt><code>appContainer</code></dt>
-            <dd>GROWI app <a href="https://github.com/jamiebuilds/unstated">unstated container</a></dd>
-            <dt><code>growiRenderer</code></dt>
-            <dd>GROWI renderer origin instance</dd>
-            <dt><code>growiPlugin</code></dt>
-            <dd>GROWI plugin Manager instance</dd>
-            <dt><code>Crowi</code></dt>
-            <dd>Crowi legacy instance (jQuery based)</dd>
-          </dl>
-        </div>
-
-        <div className="help-block">
-          Examples:
-          <pre className="hljs"><code>{this.getExampleCode()}</code></pre>
-        </div>
-
-        <div className="form-group">
-          <div className="col-xs-12">
-            <CustomScriptEditor
-              value={adminCustomizeContainer.state.currentCustomizeScript || ''}
-              onChange={(inputValue) => { adminCustomizeContainer.changeCustomizeScript(inputValue) }}
-            />
-          </div>
-          <div className="col-xs-12">
-            <p className="help-block text-right">
-              <i className="fa fa-fw fa-keyboard-o" aria-hidden="true" />
-              {t('admin:customize_setting.ctrl_space')}
-            </p>
+        <div className="row">
+          <div className="col-12">
+            <h2 className="admin-setting-header">{t('admin:customize_setting.custom_script')}</h2>
+            <Card className="card well">
+              <CardBody className="px-0 py-2">
+                {t('admin:customize_setting.write_java')}<br />
+                {t('admin:customize_setting.reflect_change')}
+              </CardBody>
+            </Card>
+
+            <div className="form-text text-muted">
+              Placeholders:<br />
+              (Available after <code>load</code> event)
+            </div>
+            <table className="table table-borderless table-sm form-text text-muted offset-1">
+              <tbody>
+                <tr>
+                  <th className="text-right"><code>$</code></th>
+                  <td>jQuery instance</td>
+                </tr>
+                <tr>
+                  <th className="text-right"><code>appContainer</code></th>
+                  <td>GROWI App <a href="https://github.com/jamiebuilds/unstated">unstated container</a></td>
+                </tr>
+                <tr>
+                  <th className="text-right"><code>growiRenderer</code></th>
+                  <td>GROWI Renderer origin instance</td>
+                </tr>
+                <tr>
+                  <th className="text-right"><code>growiPlugin</code></th>
+                  <td>GROWI Plugin Manager instance</td>
+                </tr>
+                <tr>
+                  <th className="text-right"><code>Crowi</code></th>
+                  <td>Crowi legacy instance (jQuery based)</td>
+                </tr>
+              </tbody>
+            </table>
+
+            <div className="form-text text-muted">
+              Examples:
+              <pre className="hljs"><code>{this.getExampleCode()}</code></pre>
+            </div>
+
+            <div className="form-group">
+              <CustomScriptEditor
+                value={adminCustomizeContainer.state.currentCustomizeScript || ''}
+                onChange={(inputValue) => { adminCustomizeContainer.changeCustomizeScript(inputValue) }}
+              />
+              <p className="form-text text-muted text-right">
+                <i className="fa fa-fw fa-keyboard-o" aria-hidden="true" />
+                {t('admin:customize_setting.ctrl_space')}
+              </p>
+            </div>
+
+            <AdminUpdateButtonRow onClick={this.onClickSubmit} disabled={adminCustomizeContainer.state.retrieveError != null} />
           </div>
           </div>
         </div>
         </div>
-
-        <AdminUpdateButtonRow onClick={this.onClickSubmit} disabled={adminCustomizeContainer.state.retrieveError != null} />
       </React.Fragment>
       </React.Fragment>
     );
     );
   }
   }

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

@@ -1,5 +1,7 @@
 import React from 'react';
 import React from 'react';
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
+import { withTranslation } from 'react-i18next';
+
 
 
 import { createSubscribedElement } from '../../UnstatedUtils';
 import { createSubscribedElement } from '../../UnstatedUtils';
 
 
@@ -10,70 +12,68 @@ import AdminCustomizeContainer from '../../../services/AdminCustomizeContainer';
 class CustomizeThemeOptions extends React.Component {
 class CustomizeThemeOptions extends React.Component {
 
 
   render() {
   render() {
-    const { adminCustomizeContainer } = this.props;
+    const { t, adminCustomizeContainer } = this.props;
     const { currentLayout, currentTheme } = adminCustomizeContainer.state;
     const { currentLayout, currentTheme } = adminCustomizeContainer.state;
 
 
-    const lightTheme = [{
-      name: 'default', bg: '#ffffff', topbar: '#334455', theme: '#112744',
-    }, {
-      name: 'nature', bg: '#f9fff3', topbar: '#118050', theme: '#460039',
+    /* eslint-disable no-multi-spaces */
+    const lightNDarkTheme = [{
+      name: 'default',    bg: '#ffffff', topbar: '#2a2929', sidebar: '#122c55', theme: '#209fd8',
     }, {
     }, {
-      name: 'mono-blue', bg: '#F7FBFD', topbar: '#00587A', theme: '#00587A',
-    }, {
-      name: 'wood', bg: '#fffefb', topbar: '#aaa45f', theme: '#dddebf',
+      name: 'mono-blue',  bg: '#F7FBFD', topbar: '#2a2929', sidebar: '#00587A', theme: '#00587A',
+    }];
+
+    const uniqueTheme = [{
+      name: 'nature',     bg: '#f9fff3', topbar: '#234136', sidebar: '#118050', theme: '#460039',
     }, {
     }, {
-      name: 'island', bg: '#8ecac0', topbar: '#0c2a44', theme: '#cef2ef',
+      name: 'wood',       bg: '#fffefb', topbar: '#2a2929', sidebar: '#aaa45f', theme: '#aaa45f',
     }, {
     }, {
-      name: 'christmas', bg: '#fffefb', topbar: '#b3000c', theme: '#017e20',
+      name: 'island',     bg: '#cef2ef', topbar: '#2a2929', sidebar: '#0c2a44', theme: 'rgba(183, 226, 219, 1)',
     }, {
     }, {
-      name: 'antarctic', bg: '#ffffff', topbar: '#000080', theme: '#99cccc',
+      name: 'christmas',  bg: '#fffefb', topbar: '#b3000c', sidebar: '#30882c', theme: '#d3c665',
     }, {
     }, {
-      name: 'spring', bg: '#fff5ee', topbar: '#ff69b4', theme: '#ffb6c1',
-    }];
-
-    const darkTheme = [{
-      name: 'default-dark', bg: '#212731', topbar: '#151515', theme: '#f75b36',
+      name: 'antarctic',  bg: '#ffffff', topbar: '#2a2929', sidebar: '#000080', theme: '#fa9913',
     }, {
     }, {
-      name: 'future', bg: '#16282D', topbar: '#011414', theme: '#04B4AE',
+      name: 'spring',     bg: '#ffffff', topbar: '#d3687c', sidebar: '#ffb8c6', theme: '#67a856',
     }, {
     }, {
-      name: 'blue-night', bg: '#061F2F', topbar: '#27343B', theme: '#0090C8',
+      name: 'future',     bg: '#16282d', topbar: '#2a2929', sidebar: '#00b5b7', theme: '#00b5b7',
     }, {
     }, {
-      name: 'halloween', bg: '#030003', topbar: '#cc5d1f', theme: '#e9af2b',
+      name: 'halloween',  bg: '#030003', topbar: '#aa4a04', sidebar: '#162b33', theme: '#e9af2b',
     }];
     }];
+    /* eslint-enable no-multi-spaces */
 
 
     return (
     return (
       <div id="themeOptions" className={`${currentLayout === 'kibela' && 'disabled'}`}>
       <div id="themeOptions" className={`${currentLayout === 'kibela' && 'disabled'}`}>
-        {/* Light Themes  */}
-        <div className="d-flex">
-          {lightTheme.map((theme) => {
-            return (
-              <ThemeColorBox
-                key={theme.name}
-                isSelected={currentTheme === theme.name}
-                onSelected={() => adminCustomizeContainer.switchThemeType(theme.name)}
-                name={theme.name}
-                bg={theme.bg}
-                topbar={theme.topbar}
-                theme={theme.theme}
-              />
-            );
-          })}
+        {/* Light and Dark Themes */}
+        <div>
+          <h3>{t('admin:customize_setting.theme_desc.light_and_dark')}</h3>
+          <div className="d-flex flex-wrap">
+            {lightNDarkTheme.map((theme) => {
+              return (
+                <ThemeColorBox
+                  key={theme.name}
+                  isSelected={currentTheme === theme.name}
+                  onSelected={() => adminCustomizeContainer.switchThemeType(theme.name)}
+                  {...theme}
+                />
+              );
+            })}
+          </div>
         </div>
         </div>
-        {/* Dark Themes  */}
-        <div className="d-flex mt-3">
-          {darkTheme.map((theme) => {
-            return (
-              <ThemeColorBox
-                key={theme.name}
-                isSelected={currentTheme === theme.name}
-                onSelected={() => adminCustomizeContainer.switchThemeType(theme.name)}
-                name={theme.name}
-                bg={theme.bg}
-                topbar={theme.topbar}
-                theme={theme.theme}
-              />
-            );
-          })}
+        {/* Unique Theme */}
+        <div className="mt-3">
+          <h3>{t('admin:customize_setting.theme_desc.unique')}</h3>
+          <div className="d-flex flex-wrap">
+            {uniqueTheme.map((theme) => {
+              return (
+                <ThemeColorBox
+                  key={theme.name}
+                  isSelected={currentTheme === theme.name}
+                  onSelected={() => adminCustomizeContainer.switchThemeType(theme.name)}
+                  {...theme}
+                />
+              );
+            })}
+          </div>
         </div>
         </div>
       </div>
       </div>
     );
     );
@@ -86,8 +86,9 @@ const CustomizeThemeOptionsWrapper = (props) => {
 };
 };
 
 
 CustomizeThemeOptions.propTypes = {
 CustomizeThemeOptions.propTypes = {
+  t: PropTypes.func.isRequired, // i18next
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
   adminCustomizeContainer: PropTypes.instanceOf(AdminCustomizeContainer).isRequired,
   adminCustomizeContainer: PropTypes.instanceOf(AdminCustomizeContainer).isRequired,
 };
 };
 
 
-export default CustomizeThemeOptionsWrapper;
+export default withTranslation()(CustomizeThemeOptionsWrapper);

+ 44 - 20
src/client/js/components/Admin/Customize/CustomizeTitle.jsx

@@ -1,6 +1,8 @@
+/* eslint-disable max-len */
 import React from 'react';
 import React from 'react';
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
 import { withTranslation } from 'react-i18next';
 import { withTranslation } from 'react-i18next';
+import { Card, CardBody } from 'reactstrap';
 
 
 import AppContainer from '../../../services/AppContainer';
 import AppContainer from '../../../services/AppContainer';
 import AdminCustomizeContainer from '../../../services/AdminCustomizeContainer';
 import AdminCustomizeContainer from '../../../services/AdminCustomizeContainer';
@@ -34,27 +36,49 @@ class CustomizeTitle extends React.Component {
 
 
     return (
     return (
       <React.Fragment>
       <React.Fragment>
-        <h2 className="admin-setting-header">{t('admin:customize_setting.custom_title')}</h2>
-        <p
-          className="well"
-          // eslint-disable-next-line react/no-danger
-          dangerouslySetInnerHTML={{ __html: t('admin:customize_setting.custom_title_detail') }}
-        />
-        {/* TODO i18n */}
-        <div className="help-block">
-          Default value: <code>&#123;&#123;page&#125;&#125; - &#123;&#123;sitename&#125;&#125;</code>
-          <br />
-          Default output: <pre><code className="xml">&lt;title&gt;/Sandbox - {'GROWI'}&lt;&#047;title&gt;</code></pre>
-        </div>
-        <div className="form-group">
-          <input
-            className="form-control"
-            defaultValue={currentCustomizeTitle}
-            onChange={(e) => { adminCustomizeContainer.changeCustomizeTitle(e.target.value) }}
-          />
-        </div>
+        <div className="row">
+          <div className="col-12">
+            <h2 className="admin-setting-header">{t('admin:customize_setting.custom_title')}</h2>
+          </div>
 
 
-        <AdminUpdateButtonRow onClick={this.onClickSubmit} disabled={adminCustomizeContainer.state.retrieveError != null} />
+          <div className="col-12">
+            <Card className="card well">
+              <CardBody className="px-0 py-2">
+                {/* eslint-disable react/no-danger */}
+                <p dangerouslySetInnerHTML={{ __html: t('admin:customize_setting.custom_title_detail') }} />
+                <ul>
+                  <li>
+                    <span dangerouslySetInnerHTML={{ __html: t('admin:customize_setting.custom_title_detail_placeholder1') }} />
+                  </li>
+                  <li>
+                    <span dangerouslySetInnerHTML={{ __html: t('admin:customize_setting.custom_title_detail_placeholder2') }} />
+                  </li>
+                  <li>
+                    <span dangerouslySetInnerHTML={{ __html: t('admin:customize_setting.custom_title_detail_placeholder3') }} />
+                  </li>
+                </ul>
+                {/* eslint-enable react/no-danger */}
+              </CardBody>
+            </Card>
+          </div>
+
+          {/* TODO i18n */}
+          <div className="form-text text-muted col-12">
+            Default Value: <code>&#123;&#123;pagename&#125;&#125; - &#123;&#123;sitename&#125;&#125;</code>
+            <br />
+            Default Output Example: <code className="xml">&lt;title&gt;Page name - My GROWI&lt;&#047;title&gt;</code>
+          </div>
+          <div className="form-group col-12">
+            <input
+              className="form-control"
+              defaultValue={currentCustomizeTitle}
+              onChange={(e) => { adminCustomizeContainer.changeCustomizeTitle(e.target.value) }}
+            />
+          </div>
+          <div className="col-12">
+            <AdminUpdateButtonRow onClick={this.onClickSubmit} disabled={adminCustomizeContainer.state.retrieveError != null} />
+          </div>
+        </div>
       </React.Fragment>
       </React.Fragment>
     );
     );
   }
   }

+ 11 - 10
src/client/js/components/Admin/Customize/ThemeColorBox.jsx

@@ -5,23 +5,23 @@ import PropTypes from 'prop-types';
 class ThemeColorBox extends React.PureComponent {
 class ThemeColorBox extends React.PureComponent {
 
 
   render() {
   render() {
-    const { name } = this.props;
+    const {
+      isSelected, onSelected, name, bg, topbar, sidebar, theme,
+    } = this.props;
 
 
     return (
     return (
       <div
       <div
         id={`theme-option-${name}`}
         id={`theme-option-${name}`}
-        className={`theme-option-container d-flex flex-column align-items-center ${this.props.isSelected && 'active'}`}
-        onClick={this.props.onSelected}
+        className={`theme-option-container d-flex flex-column align-items-center ${isSelected && 'active'}`}
+        onClick={onSelected}
       >
       >
-        <a
-          className={`m-0 ${name} theme-button`}
-          id={name}
-        >
+        <a id={name} type="button" className={`m-0 ${name} theme-button`}>
           <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64">
           <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64">
             <g>
             <g>
-              <path d="M -1 -1 L65 -1 L65 65 L-1 65 L-1 -1 Z" fill={this.props.bg}></path>
-              <path d="M -1 -1 L65 -1 L65 15 L-1 15 L-1 -1 Z" fill={this.props.topbar}></path>
-              <path d="M 44 15 L65 15 L65 65 L44 65 L44 15 Z" fill={this.props.theme}></path>
+              <path d="M -1 -1 L65 -1 L65 65 L-1 65 L-1 -1 Z" fill={bg}></path>
+              <path d="M -1 -1 L65 -1 L65 15 L-1 15 L-1 -1 Z" fill={topbar}></path>
+              <path d="M -1 15 L15 15 L15 65 L-1 65 L-1 15 Z" fill={sidebar}></path>
+              <path d="M 65 45 L65 65 L45 65 L65 45 Z" fill={theme}></path>
             </g>
             </g>
           </svg>
           </svg>
         </a>
         </a>
@@ -39,6 +39,7 @@ ThemeColorBox.propTypes = {
   name: PropTypes.string.isRequired,
   name: PropTypes.string.isRequired,
   bg: PropTypes.string.isRequired,
   bg: PropTypes.string.isRequired,
   topbar: PropTypes.string.isRequired,
   topbar: PropTypes.string.isRequired,
+  sidebar: PropTypes.string.isRequired,
   theme: PropTypes.string.isRequired,
   theme: PropTypes.string.isRequired,
 };
 };
 
 

+ 7 - 7
src/client/js/components/Admin/ElasticsearchManagement/ElasticsearchManagement.jsx

@@ -160,7 +160,7 @@ class ElasticsearchManagement extends React.Component {
     return (
     return (
       <>
       <>
         <div className="row">
         <div className="row">
-          <div className="col-xs-12">
+          <div className="col-md-12">
             <StatusTable
             <StatusTable
               isInitialized={isInitialized}
               isInitialized={isInitialized}
               isErrorOccuredOnSearchService={isErrorOccuredOnSearchService}
               isErrorOccuredOnSearchService={isErrorOccuredOnSearchService}
@@ -177,8 +177,8 @@ class ElasticsearchManagement extends React.Component {
 
 
         {/* Controls */}
         {/* Controls */}
         <div className="row">
         <div className="row">
-          <label className="col-xs-3 control-label">{ t('full_text_search_management.reconnect') }</label>
-          <div className="col-xs-6">
+          <label className="col-md-3 col-form-label text-left text-md-right">{ t('full_text_search_management.reconnect') }</label>
+          <div className="col-md-6">
             <ReconnectControls
             <ReconnectControls
               isEnabled={isReconnectBtnEnabled}
               isEnabled={isReconnectBtnEnabled}
               isProcessing={isReconnectingProcessing}
               isProcessing={isReconnectingProcessing}
@@ -190,8 +190,8 @@ class ElasticsearchManagement extends React.Component {
         <hr />
         <hr />
 
 
         <div className="row">
         <div className="row">
-          <label className="col-xs-3 control-label">{ t('full_text_search_management.normalize') }</label>
-          <div className="col-xs-6">
+          <label className="col-md-3 col-form-label text-left text-md-right">{ t('full_text_search_management.normalize') }</label>
+          <div className="col-md-6">
             <NormalizeIndicesControls
             <NormalizeIndicesControls
               isRebuildingProcessing={isRebuildingProcessing}
               isRebuildingProcessing={isRebuildingProcessing}
               isRebuildingCompleted={isRebuildingCompleted}
               isRebuildingCompleted={isRebuildingCompleted}
@@ -204,8 +204,8 @@ class ElasticsearchManagement extends React.Component {
         <hr />
         <hr />
 
 
         <div className="row">
         <div className="row">
-          <label className="col-xs-3 control-label">{ t('full_text_search_management.rebuild') }</label>
-          <div className="col-xs-6">
+          <label className="col-md-3 col-form-label text-left text-md-right">{ t('full_text_search_management.rebuild') }</label>
+          <div className="col-md-6">
             <RebuildIndexControls
             <RebuildIndexControls
               isRebuildingProcessing={isRebuildingProcessing}
               isRebuildingProcessing={isRebuildingProcessing}
               isRebuildingCompleted={isRebuildingCompleted}
               isRebuildingCompleted={isRebuildingCompleted}

+ 2 - 2
src/client/js/components/Admin/ElasticsearchManagement/NormalizeIndicesControls.jsx

@@ -15,14 +15,14 @@ class NormalizeIndicesControls extends React.PureComponent {
       <>
       <>
         <button
         <button
           type="submit"
           type="submit"
-          className={`btn btn-outline ${isEnabled ? 'btn-info' : 'btn-default'}`}
+          className={`btn ${isEnabled ? 'btn-outline-info' : 'btn-outline-secondary'}`}
           onClick={() => { this.props.onNormalizingRequested() }}
           onClick={() => { this.props.onNormalizingRequested() }}
           disabled={!isEnabled}
           disabled={!isEnabled}
         >
         >
           { t('full_text_search_management.normalize_button') }
           { t('full_text_search_management.normalize_button') }
         </button>
         </button>
 
 
-        <p className="help-block">
+        <p className="form-text text-muted">
           { t('full_text_search_management.normalize_description') }<br />
           { t('full_text_search_management.normalize_description') }<br />
         </p>
         </p>
       </>
       </>

+ 2 - 2
src/client/js/components/Admin/ElasticsearchManagement/RebuildIndexControls.jsx

@@ -76,14 +76,14 @@ class RebuildIndexControls extends React.Component {
 
 
         <button
         <button
           type="submit"
           type="submit"
-          className="btn btn-inverse"
+          className="btn btn-primary"
           onClick={() => { this.props.onRebuildingRequested() }}
           onClick={() => { this.props.onRebuildingRequested() }}
           disabled={!isEnabled}
           disabled={!isEnabled}
         >
         >
           { t('full_text_search_management.rebuild_button') }
           { t('full_text_search_management.rebuild_button') }
         </button>
         </button>
 
 
-        <p className="help-block">
+        <p className="form-text text-muted">
           { t('full_text_search_management.rebuild_description_1') }<br />
           { t('full_text_search_management.rebuild_description_1') }<br />
           { t('full_text_search_management.rebuild_description_2') }<br />
           { t('full_text_search_management.rebuild_description_2') }<br />
         </p>
         </p>

+ 2 - 2
src/client/js/components/Admin/ElasticsearchManagement/ReconnectControls.jsx

@@ -13,7 +13,7 @@ class ReconnectControls extends React.PureComponent {
       <>
       <>
         <button
         <button
           type="submit"
           type="submit"
-          className={`btn btn-outline ${isEnabled ? 'btn-success' : 'btn-default'}`}
+          className={`btn ${isEnabled ? 'btn-outline-success' : 'btn-outline-secondary'}`}
           onClick={() => { this.props.onReconnectingRequested() }}
           onClick={() => { this.props.onReconnectingRequested() }}
           disabled={!isEnabled}
           disabled={!isEnabled}
         >
         >
@@ -21,7 +21,7 @@ class ReconnectControls extends React.PureComponent {
           { t('full_text_search_management.reconnect_button') }
           { t('full_text_search_management.reconnect_button') }
         </button>
         </button>
 
 
-        <p className="help-block">
+        <p className="form-text text-muted">
           { t('full_text_search_management.reconnect_description') }<br />
           { t('full_text_search_management.reconnect_description') }<br />
         </p>
         </p>
       </>
       </>

+ 24 - 31
src/client/js/components/Admin/ElasticsearchManagement/StatusTable.jsx

@@ -7,7 +7,7 @@ import { createSubscribedElement } from '../../UnstatedUtils';
 class StatusTable extends React.PureComponent {
 class StatusTable extends React.PureComponent {
 
 
   renderPreInitializedLabel() {
   renderPreInitializedLabel() {
-    return <span className="label label-default">――</span>;
+    return <span className="badge badge-pill badge-default">――</span>;
   }
   }
 
 
   renderConnectionStatusLabels() {
   renderConnectionStatusLabels() {
@@ -18,17 +18,17 @@ class StatusTable extends React.PureComponent {
     } = this.props;
     } = this.props;
 
 
     const errorOccuredLabel = isErrorOccuredOnSearchService
     const errorOccuredLabel = isErrorOccuredOnSearchService
-      ? <span className="label label-danger ml-2">{ t('full_text_search_management.connection_status_label_erroroccured') }</span>
+      ? <span className="badge badge-pill badge-danger ml-2">{ t('full_text_search_management.connection_status_label_erroroccured') }</span>
       : null;
       : null;
 
 
     let connectionStatusLabel = null;
     let connectionStatusLabel = null;
     if (!isConfigured) {
     if (!isConfigured) {
-      connectionStatusLabel = <span className="label label-default">{ t('full_text_search_management.connection_status_label_unconfigured') }</span>;
+      connectionStatusLabel = <span className="badge badge-pill badge-default">{ t('full_text_search_management.connection_status_label_unconfigured') }</span>;
     }
     }
     else {
     else {
       connectionStatusLabel = isConnected
       connectionStatusLabel = isConnected
-        ? <span className="label label-success">{ t('full_text_search_management.connection_status_label_connected') }</span>
-        : <span className="label label-danger">{ t('full_text_search_management.connection_status_label_disconnected') }</span>;
+        ? <span className="badge badge-pill badge-success">{ t('full_text_search_management.connection_status_label_connected') }</span>
+        : <span className="badge badge-pill badge-danger">{ t('full_text_search_management.connection_status_label_disconnected') }</span>;
     }
     }
 
 
     return (
     return (
@@ -42,8 +42,8 @@ class StatusTable extends React.PureComponent {
     const { t, isNormalized } = this.props;
     const { t, isNormalized } = this.props;
 
 
     return isNormalized
     return isNormalized
-      ? <span className="label label-info">{ t('full_text_search_management.indices_status_label_normalized') }</span>
-      : <span className="label label-warning">{ t('full_text_search_management.indices_status_label_unnormalized') }</span>;
+      ? <span className="badge badge-pill badge-info">{ t('full_text_search_management.indices_status_label_normalized') }</span>
+      : <span className="badge badge-pill badge-warning">{ t('full_text_search_management.indices_status_label_unnormalized') }</span>;
   }
   }
 
 
   renderIndexInfoPanel(indexName, body = {}, aliases = []) {
   renderIndexInfoPanel(indexName, body = {}, aliases = []) {
@@ -51,24 +51,23 @@ class StatusTable extends React.PureComponent {
 
 
     const aliasLabels = aliases.map((aliasName) => {
     const aliasLabels = aliases.map((aliasName) => {
       return (
       return (
-        <span key={`label-${indexName}-${aliasName}`} className="label label-primary mr-2">
+        <span key={`badge-${indexName}-${aliasName}`} className="badge badge-pill badge-primary mr-2">
           <i className="icon-tag"></i> {aliasName}
           <i className="icon-tag"></i> {aliasName}
         </span>
         </span>
       );
       );
     });
     });
 
 
     return (
     return (
-      <div className="panel panel-default">
-        <div className="panel-heading" role="tab">
-          <h4 className="panel-title">
-            <a role="button" data-toggle="collapse" data-parent="#accordion" href={`#${collapseId}`} aria-expanded="true" aria-controls={collapseId}>
-              <i className="fa fa-fw fa-database"></i> {indexName}
-            </a>
-            <span className="ml-3">{aliasLabels}</span>
-          </h4>
+      <div className="card">
+        <div className="card-header">
+
+          <a role="button" className="text-nowrap mr-2" data-toggle="collapse" href={`#${collapseId}`} aria-expanded="true" aria-controls={collapseId}>
+            <i className="fa fa-fw fa-database"></i> {indexName}
+          </a>
+          <span className="ml-md-3">{aliasLabels}</span>
         </div>
         </div>
-        <div id={collapseId} className="panel-collapse collapse" role="tabpanel">
-          <div className="panel-body">
+        <div id={collapseId} className="collapse">
+          <div className="card-body">
             <pre>
             <pre>
               {JSON.stringify(body, null, 2)}
               {JSON.stringify(body, null, 2)}
             </pre>
             </pre>
@@ -124,7 +123,7 @@ class StatusTable extends React.PureComponent {
       <div className="row">
       <div className="row">
         { Object.keys(indexNameToDataMap).map((indexName) => {
         { Object.keys(indexNameToDataMap).map((indexName) => {
           return (
           return (
-            <div key={`col-${indexName}`} className="col-xs-6">
+            <div key={`col-${indexName}`} className="col-md-6">
               { this.renderIndexInfoPanel(indexName, indexNameToDataMap[indexName], indexNameToAliasMap[indexName]) }
               { this.renderIndexInfoPanel(indexName, indexNameToDataMap[indexName], indexNameToAliasMap[indexName]) }
             </div>
             </div>
           );
           );
@@ -143,22 +142,16 @@ class StatusTable extends React.PureComponent {
       <table className="table table-bordered">
       <table className="table table-bordered">
         <tbody>
         <tbody>
           <tr>
           <tr>
-            <th>{ t('full_text_search_management.connection_status') }</th>
-            <td>
-              { isInitialized ? this.renderConnectionStatusLabels() : this.renderPreInitializedLabel() }
-            </td>
+            <th className="w-25">{t('full_text_search_management.connection_status')}</th>
+            <td className="w-75">{ isInitialized ? this.renderConnectionStatusLabels() : this.renderPreInitializedLabel() }</td>
           </tr>
           </tr>
           <tr>
           <tr>
-            <th>{ t('full_text_search_management.indices_status') }</th>
-            <td>
-              { isInitialized ? this.renderIndicesStatusLabel() : this.renderPreInitializedLabel() }
-            </td>
+            <th className="w-25">{t('full_text_search_management.indices_status')}</th>
+            <td className="w-75">{ isInitialized ? this.renderIndicesStatusLabel() : this.renderPreInitializedLabel() }</td>
           </tr>
           </tr>
           <tr>
           <tr>
-            <th className="col-sm-4">{ t('full_text_search_management.indices_summary') }</th>
-            <td className="p-4">
-              { isInitialized && this.renderIndexInfoPanels() }
-            </td>
+            <th className="w-25">{t('full_text_search_management.indices_summary')}</th>
+            <td className="p-4 w-75">{ isInitialized && this.renderIndexInfoPanels() }</td>
           </tr>
           </tr>
         </tbody>
         </tbody>
       </table>
       </table>

+ 31 - 29
src/client/js/components/Admin/ExportArchiveData/ArchiveFilesTable.jsx

@@ -14,35 +14,37 @@ class ArchiveFilesTable extends React.Component {
     const { t } = this.props;
     const { t } = this.props;
 
 
     return (
     return (
-      <table className="table table-bordered">
-        <thead>
-          <tr>
-            <th>{t('admin:export_management.file')}</th>
-            <th>{t('admin:export_management.growi_version')}</th>
-            <th>{t('admin:export_management.collections')}</th>
-            <th>{t('admin:export_management.exported_at')}</th>
-            <th></th>
-          </tr>
-        </thead>
-        <tbody>
-          {this.props.zipFileStats.map(({ meta, fileName, innerFileStats }) => {
-            return (
-              <tr key={fileName}>
-                <th>{fileName}</th>
-                <td>{meta.version}</td>
-                <td className="text-capitalize">{innerFileStats.map(fileStat => fileStat.collectionName).join(', ')}</td>
-                <td>{meta.exportedAt ? format(new Date(meta.exportedAt), 'yyyy/MM/dd HH:mm:ss') : ''}</td>
-                <td>
-                  <ArchiveFilesTableMenu
-                    fileName={fileName}
-                    onZipFileStatRemove={this.props.onZipFileStatRemove}
-                  />
-                </td>
-              </tr>
-            );
-          })}
-        </tbody>
-      </table>
+      <div className="table-responsive">
+        <table className="table table-bordered">
+          <thead>
+            <tr>
+              <th>{t('admin:export_management.file')}</th>
+              <th>{t('admin:export_management.growi_version')}</th>
+              <th>{t('admin:export_management.collections')}</th>
+              <th>{t('admin:export_management.exported_at')}</th>
+              <th></th>
+            </tr>
+          </thead>
+          <tbody>
+            {this.props.zipFileStats.map(({ meta, fileName, innerFileStats }) => {
+              return (
+                <tr key={fileName}>
+                  <th>{fileName}</th>
+                  <td>{meta.version}</td>
+                  <td className="text-capitalize">{innerFileStats.map(fileStat => fileStat.collectionName).join(', ')}</td>
+                  <td>{meta.exportedAt ? format(new Date(meta.exportedAt), 'yyyy/MM/dd HH:mm:ss') : ''}</td>
+                  <td>
+                    <ArchiveFilesTableMenu
+                      fileName={fileName}
+                      onZipFileStatRemove={this.props.onZipFileStatRemove}
+                    />
+                  </td>
+                </tr>
+              );
+            })}
+          </tbody>
+        </table>
+      </div>
     );
     );
   }
   }
 
 

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

@@ -12,22 +12,18 @@ class ArchiveFilesTableMenu extends React.Component {
     const { t } = this.props;
     const { t } = this.props;
 
 
     return (
     return (
-      <div className="btn-group admin-user-menu">
-        <button type="button" className="btn btn-sm btn-default dropdown-toggle" data-toggle="dropdown">
+      <div className="btn-group admin-user-menu dropdown">
+        <button type="button" className="btn btn-sm btn-outline-secondary dropdown-toggle" data-toggle="dropdown">
           <i className="icon-settings"></i> <span className="caret"></span>
           <i className="icon-settings"></i> <span className="caret"></span>
         </button>
         </button>
         <ul className="dropdown-menu" role="menu">
         <ul className="dropdown-menu" role="menu">
           <li className="dropdown-header">{t('admin:export_management.export_menu')}</li>
           <li className="dropdown-header">{t('admin:export_management.export_menu')}</li>
-          <li>
-            <a type="button" href={`/admin/export/${this.props.fileName}`}>
-              <i className="icon-cloud-download" /> {t('admin:export_management.download')}
-            </a>
-          </li>
-          <li>
-            <a type="button" role="button" onClick={() => this.props.onZipFileStatRemove(this.props.fileName)}>
-              <span className="text-danger"><i className="icon-trash" /> {t('admin:export_management.delete')}</span>
-            </a>
-          </li>
+          <a 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)}>
+            <span className="text-danger"><i className="icon-trash" /> {t('admin:export_management.delete')}</span>
+          </a>
         </ul>
         </ul>
       </div>
       </div>
     );
     );

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

@@ -1,7 +1,9 @@
 import React from 'react';
 import React from 'react';
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
 import { withTranslation } from 'react-i18next';
 import { withTranslation } from 'react-i18next';
-import Modal from 'react-bootstrap/es/Modal';
+import {
+  Modal, ModalHeader, ModalBody, ModalFooter,
+} from 'reactstrap';
 import * as toastr from 'toastr';
 import * as toastr from 'toastr';
 
 
 import { createSubscribedElement } from '../../UnstatedUtils';
 import { createSubscribedElement } from '../../UnstatedUtils';
@@ -120,7 +122,7 @@ class SelectCollectionsModal extends React.Component {
     const html = this.props.t('admin:export_management.desc_password_seed');
     const html = this.props.t('admin:export_management.desc_password_seed');
 
 
     // eslint-disable-next-line react/no-danger
     // eslint-disable-next-line react/no-danger
-    return <div className="well well-sm" dangerouslySetInnerHTML={{ __html: html }}></div>;
+    return <div className="card well" dangerouslySetInnerHTML={{ __html: html }}></div>;
   }
   }
 
 
   renderGroups(groupList, color) {
   renderGroups(groupList, color) {
@@ -140,28 +142,30 @@ class SelectCollectionsModal extends React.Component {
   }
   }
 
 
   renderCheckboxes(collectionNames, color) {
   renderCheckboxes(collectionNames, color) {
-    const checkboxColor = color ? `checkbox-${color}` : 'checkbox-info';
+    const checkboxColor = color ? `custom-checkbox-${color}` : 'custom-checkbox-info';
 
 
     return (
     return (
-      <div className={`row checkbox ${checkboxColor}`}>
-        {collectionNames.map((collectionName) => {
-          return (
-            <div className="col-xs-6 my-1" key={collectionName}>
-              <input
-                type="checkbox"
-                id={collectionName}
-                name={collectionName}
-                className="form-check-input"
-                value={collectionName}
-                checked={this.state.selectedCollections.has(collectionName)}
-                onChange={this.toggleCheckbox}
-              />
-              <label className="text-capitalize form-check-label ml-3" htmlFor={collectionName}>
-                {collectionName}
-              </label>
-            </div>
-          );
-        })}
+      <div className={`custom-control custom-checkbox ${checkboxColor}`}>
+        <div className="row">
+          {collectionNames.map((collectionName) => {
+            return (
+              <div className="col-sm-6 my-1" key={collectionName}>
+                <input
+                  type="checkbox"
+                  className="custom-control-input"
+                  id={collectionName}
+                  name={collectionName}
+                  value={collectionName}
+                  checked={this.state.selectedCollections.has(collectionName)}
+                  onChange={this.toggleCheckbox}
+                />
+                <label className="text-capitalize custom-control-label ml-3" htmlFor={collectionName}>
+                  {collectionName}
+                </label>
+              </div>
+            );
+          })}
+        </div>
       </div>
       </div>
     );
     );
   }
   }
@@ -170,54 +174,54 @@ class SelectCollectionsModal extends React.Component {
     const { t } = this.props;
     const { t } = this.props;
 
 
     return (
     return (
-      <Modal show={this.props.isOpen} onHide={this.props.onClose}>
-        <Modal.Header closeButton>
-          <Modal.Title>{t('admin:export_management.export_collections')}</Modal.Title>
-        </Modal.Header>
+      <Modal isOpen={this.props.isOpen} toggle={this.props.onClose}>
+        <ModalHeader tag="h4" toggle={this.props.onClose} className="bg-info text-light">
+          {t('admin:export_management.export_collections')}
+        </ModalHeader>
 
 
         <form onSubmit={this.export}>
         <form onSubmit={this.export}>
-          <Modal.Body>
+          <ModalBody>
             <div className="row">
             <div className="row">
               <div className="col-sm-12">
               <div className="col-sm-12">
-                <button type="button" className="btn btn-sm btn-default mr-2" onClick={this.checkAll}>
+                <button type="button" className="btn btn-sm btn-outline-secondary mr-2" onClick={this.checkAll}>
                   <i className="fa fa-check-square-o"></i> {t('admin:export_management.check_all')}
                   <i className="fa fa-check-square-o"></i> {t('admin:export_management.check_all')}
                 </button>
                 </button>
-                <button type="button" className="btn btn-sm btn-default mr-2" onClick={this.uncheckAll}>
+                <button type="button" className="btn btn-sm btn-outline-secondary mr-2" onClick={this.uncheckAll}>
                   <i className="fa fa-square-o"></i> {t('admin:export_management.uncheck_all')}
                   <i className="fa fa-square-o"></i> {t('admin:export_management.uncheck_all')}
                 </button>
                 </button>
               </div>
               </div>
             </div>
             </div>
             <div className="row mt-4">
             <div className="row mt-4">
-              <div className="col-xs-12">
-                <legend>MongoDB Page Collections</legend>
+              <div className="col-sm-12">
+                <h3 className="admin-setting-header">MongoDB Page Collections</h3>
                 {this.renderGroups(GROUPS_PAGE)}
                 {this.renderGroups(GROUPS_PAGE)}
               </div>
               </div>
             </div>
             </div>
             <div className="row mt-4">
             <div className="row mt-4">
-              <div className="col-xs-12">
-                <legend>MongoDB User Collections</legend>
+              <div className="col-sm-12">
+                <h3 className="admin-setting-header">MongoDB User Collections</h3>
                 {this.renderGroups(GROUPS_USER, 'danger')}
                 {this.renderGroups(GROUPS_USER, 'danger')}
                 {this.renderWarnForUser()}
                 {this.renderWarnForUser()}
               </div>
               </div>
             </div>
             </div>
             <div className="row mt-4">
             <div className="row mt-4">
-              <div className="col-xs-12">
-                <legend>MongoDB Config Collections</legend>
+              <div className="col-sm-12">
+                <h3 className="admin-setting-header">MongoDB Config Collections</h3>
                 {this.renderGroups(GROUPS_CONFIG)}
                 {this.renderGroups(GROUPS_CONFIG)}
               </div>
               </div>
             </div>
             </div>
             <div className="row mt-4">
             <div className="row mt-4">
-              <div className="col-xs-12">
-                <legend>Other Collections</legend>
+              <div className="col-sm-12">
+                <h3 className="admin-setting-header">MongoDB Other Collections</h3>
                 {this.renderOthers()}
                 {this.renderOthers()}
               </div>
               </div>
             </div>
             </div>
-          </Modal.Body>
+          </ModalBody>
 
 
-          <Modal.Footer>
-            <button type="button" className="btn btn-sm btn-default" onClick={this.props.onClose}>{t('admin:export_management.cancel')}</button>
-            <button type="submit" className="btn btn-sm btn-primary" disabled={!this.validateForm()}>{t('admin:export_management.export')}</button>
-          </Modal.Footer>
+          <ModalFooter>
+            <button type="button" className="btn btn-sm btn-outline-secondary" onClick={this.props.onClose}>{t('export_management.cancel')}</button>
+            <button type="submit" className="btn btn-sm btn-primary" disabled={!this.validateForm()}>{t('export_management.export')}</button>
+          </ModalFooter>
         </form>
         </form>
       </Modal>
       </Modal>
     );
     );

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

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

+ 7 - 7
src/client/js/components/Admin/ImportData/GrowiArchive/ErrorViewer.jsx

@@ -1,7 +1,7 @@
 import React from 'react';
 import React from 'react';
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
 import { withTranslation } from 'react-i18next';
 import { withTranslation } from 'react-i18next';
-import Modal from 'react-bootstrap/es/Modal';
+import { Modal, ModalHeader, ModalBody } from 'reactstrap';
 
 
 import { createSubscribedElement } from '../../../UnstatedUtils';
 import { createSubscribedElement } from '../../../UnstatedUtils';
 
 
@@ -20,13 +20,13 @@ class ErrorViewer extends React.Component {
     }
     }
 
 
     return (
     return (
-      <Modal show={this.props.isOpen} onHide={this.props.onClose}>
-        <Modal.Header closeButton className="bg-danger">
-          <Modal.Title className="text-white">Errors</Modal.Title>
-        </Modal.Header>
-        <Modal.Body>
+      <Modal isOpen={this.props.isOpen} toggle={this.props.onClose}>
+        <ModalHeader tag="h4" toggle={this.props.onClose} className="bg-danger text-light">
+          Errors
+        </ModalHeader>
+        <ModalBody>
           <textarea className="form-control" rows="8" readOnly wrap="off" defaultValue={value}></textarea>
           <textarea className="form-control" rows="8" readOnly wrap="off" defaultValue={value}></textarea>
-        </Modal.Body>
+        </ModalBody>
       </Modal>
       </Modal>
     );
     );
   }
   }

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

@@ -3,7 +3,12 @@
 import React from 'react';
 import React from 'react';
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
 import { withTranslation } from 'react-i18next';
 import { withTranslation } from 'react-i18next';
-import Modal from 'react-bootstrap/es/Modal';
+import {
+  Modal,
+  ModalHeader,
+  ModalBody,
+  ModalFooter,
+} from 'reactstrap';
 
 
 import GrowiArchiveImportOption from '@commons/models/admin/growi-archive-import-option';
 import GrowiArchiveImportOption from '@commons/models/admin/growi-archive-import-option';
 
 
@@ -62,82 +67,91 @@ class ImportCollectionConfigurationModal extends React.Component {
     /* eslint-disable react/no-unescaped-entities */
     /* eslint-disable react/no-unescaped-entities */
     return (
     return (
       <>
       <>
-        <div className="checkbox checkbox-warning">
+        <div className="custom-control custom-checkbox custom-checkbox-warning">
           <input
           <input
             id="cbOpt4"
             id="cbOpt4"
             type="checkbox"
             type="checkbox"
+            className="custom-control-input"
             checked={option.isOverwriteAuthorWithCurrentUser || false} // add ' || false' to avoid uncontrolled input warning
             checked={option.isOverwriteAuthorWithCurrentUser || false} // add ' || false' to avoid uncontrolled input warning
             onChange={() => this.changeHandler({ isOverwriteAuthorWithCurrentUser: !option.isOverwriteAuthorWithCurrentUser })}
             onChange={() => this.changeHandler({ isOverwriteAuthorWithCurrentUser: !option.isOverwriteAuthorWithCurrentUser })}
           />
           />
-          <label htmlFor="cbOpt4">
+          <label htmlFor="cbOpt4" className="custom-control-label">
             {t(`${translationBase}.overwrite_author.label`)}
             {t(`${translationBase}.overwrite_author.label`)}
-            <p className="help-block mt-0" dangerouslySetInnerHTML={{ __html: t(`${translationBase}.overwrite_author.desc`) }} />
+            <p className="form-text text-muted mt-0" dangerouslySetInnerHTML={{ __html: t(`${translationBase}.overwrite_author.desc`) }} />
           </label>
           </label>
         </div>
         </div>
-        <div className="checkbox checkbox-warning">
+        <div className="custom-control custom-checkbox custom-checkbox-warning">
           <input
           <input
             id="cbOpt1"
             id="cbOpt1"
             type="checkbox"
             type="checkbox"
+            className="custom-control-input"
             checked={option.makePublicForGrant2 || false} // add ' || false' to avoid uncontrolled input warning
             checked={option.makePublicForGrant2 || false} // add ' || false' to avoid uncontrolled input warning
             onChange={() => this.changeHandler({ makePublicForGrant2: !option.makePublicForGrant2 })}
             onChange={() => this.changeHandler({ makePublicForGrant2: !option.makePublicForGrant2 })}
           />
           />
-          <label htmlFor="cbOpt1">
+          <label htmlFor="cbOpt1" className="custom-control-label">
             {t(`${translationBase}.set_public_to_page.label`, { from: t('Anyone with the link') })}
             {t(`${translationBase}.set_public_to_page.label`, { from: t('Anyone with the link') })}
             <p
             <p
-              className="help-block mt-0"
+              className="form-text text-muted mt-0"
               dangerouslySetInnerHTML={{ __html: t(`${translationBase}.set_public_to_page.desc`, { from: t('Anyone with the link') }) }}
               dangerouslySetInnerHTML={{ __html: t(`${translationBase}.set_public_to_page.desc`, { from: t('Anyone with the link') }) }}
             />
             />
           </label>
           </label>
         </div>
         </div>
-        <div className="checkbox checkbox-warning">
+        <div className="custom-control custom-checkbox custom-checkbox-warning">
           <input
           <input
             id="cbOpt2"
             id="cbOpt2"
             type="checkbox"
             type="checkbox"
+            className="custom-control-input"
             checked={option.makePublicForGrant4 || false} // add ' || false' to avoid uncontrolled input warning
             checked={option.makePublicForGrant4 || false} // add ' || false' to avoid uncontrolled input warning
             onChange={() => this.changeHandler({ makePublicForGrant4: !option.makePublicForGrant4 })}
             onChange={() => this.changeHandler({ makePublicForGrant4: !option.makePublicForGrant4 })}
           />
           />
-          <label htmlFor="cbOpt2">
+          <label htmlFor="cbOpt2" className="custom-control-label">
             {t(`${translationBase}.set_public_to_page.label`, { from: t('Only me') })}
             {t(`${translationBase}.set_public_to_page.label`, { from: t('Only me') })}
-            <p className="help-block mt-0" dangerouslySetInnerHTML={{ __html: t(`${translationBase}.set_public_to_page.desc`, { from: t('Only me') }) }} />
+            <p
+              className="form-text text-muted mt-0"
+              dangerouslySetInnerHTML={{ __html: t(`${translationBase}.set_public_to_page.desc`, { from: t('Only me') }) }}
+            />
           </label>
           </label>
         </div>
         </div>
-        <div className="checkbox checkbox-warning">
+        <div className="custom-control custom-checkbox custom-checkbox-warning">
           <input
           <input
             id="cbOpt3"
             id="cbOpt3"
             type="checkbox"
             type="checkbox"
+            className="custom-control-input"
             checked={option.makePublicForGrant5 || false} // add ' || false' to avoid uncontrolled input warning
             checked={option.makePublicForGrant5 || false} // add ' || false' to avoid uncontrolled input warning
             onChange={() => this.changeHandler({ makePublicForGrant5: !option.makePublicForGrant5 })}
             onChange={() => this.changeHandler({ makePublicForGrant5: !option.makePublicForGrant5 })}
           />
           />
-          <label htmlFor="cbOpt3">
+          <label htmlFor="cbOpt3" className="custom-control-label">
             {t(`${translationBase}.set_public_to_page.label`, { from: t('Only inside the group') })}
             {t(`${translationBase}.set_public_to_page.label`, { from: t('Only inside the group') })}
             <p
             <p
-              className="help-block mt-0"
+              className="form-text text-muted mt-0"
               dangerouslySetInnerHTML={{ __html: t(`${translationBase}.set_public_to_page.desc`, { from: t('Only inside the group') }) }}
               dangerouslySetInnerHTML={{ __html: t(`${translationBase}.set_public_to_page.desc`, { from: t('Only inside the group') }) }}
             />
             />
           </label>
           </label>
         </div>
         </div>
-        <div className="checkbox checkbox-default">
+        <div className="custom-control custom-checkbox custom-checkbox-warning">
           <input
           <input
             id="cbOpt5"
             id="cbOpt5"
             type="checkbox"
             type="checkbox"
+            className="custom-control-input"
             checked={option.initPageMetadatas || false} // add ' || false' to avoid uncontrolled input warning
             checked={option.initPageMetadatas || false} // add ' || false' to avoid uncontrolled input warning
             onChange={() => this.changeHandler({ initPageMetadatas: !option.initPageMetadatas })}
             onChange={() => this.changeHandler({ initPageMetadatas: !option.initPageMetadatas })}
           />
           />
-          <label htmlFor="cbOpt5">
+          <label htmlFor="cbOpt5" className="custom-control-label">
             {t(`${translationBase}.initialize_meta_datas.label`)}
             {t(`${translationBase}.initialize_meta_datas.label`)}
-            <p className="help-block mt-0" dangerouslySetInnerHTML={{ __html: t(`${translationBase}.initialize_meta_datas.desc`) }} />
+            <p className="form-text text-muted mt-0" dangerouslySetInnerHTML={{ __html: t(`${translationBase}.initialize_meta_datas.desc`) }} />
           </label>
           </label>
         </div>
         </div>
-        <div className="checkbox checkbox-default">
+        <div className="custom-control custom-checkbox custom-checkbox-warning">
           <input
           <input
             id="cbOpt6"
             id="cbOpt6"
             type="checkbox"
             type="checkbox"
+            className="custom-control-input"
             checked={option.initHackmdDatas || false} // add ' || false' to avoid uncontrolled input warning
             checked={option.initHackmdDatas || false} // add ' || false' to avoid uncontrolled input warning
             onChange={() => this.changeHandler({ initHackmdDatas: !option.initHackmdDatas })}
             onChange={() => this.changeHandler({ initHackmdDatas: !option.initHackmdDatas })}
           />
           />
-          <label htmlFor="cbOpt6">
+          <label htmlFor="cbOpt6" className="custom-control-label">
             {t(`${translationBase}.initialize_hackmd_related_datas.label`)}
             {t(`${translationBase}.initialize_hackmd_related_datas.label`)}
-            <p className="help-block mt-0" dangerouslySetInnerHTML={{ __html: t(`${translationBase}.initialize_hackmd_related_datas.desc`) }} />
+            <p className="form-text text-muted mt-0" dangerouslySetInnerHTML={{ __html: t(`${translationBase}.initialize_hackmd_related_datas.desc`) }} />
           </label>
           </label>
         </div>
         </div>
       </>
       </>
@@ -154,16 +168,17 @@ class ImportCollectionConfigurationModal extends React.Component {
     /* eslint-disable react/no-unescaped-entities */
     /* eslint-disable react/no-unescaped-entities */
     return (
     return (
       <>
       <>
-        <div className="checkbox checkbox-warning">
+        <div className="custom-control custom-checkbox custom-checkbox-warning">
           <input
           <input
             id="cbOpt1"
             id="cbOpt1"
             type="checkbox"
             type="checkbox"
+            className="custom-control-input"
             checked={option.isOverwriteAuthorWithCurrentUser || false} // add ' || false' to avoid uncontrolled input warning
             checked={option.isOverwriteAuthorWithCurrentUser || false} // add ' || false' to avoid uncontrolled input warning
             onChange={() => this.changeHandler({ isOverwriteAuthorWithCurrentUser: !option.isOverwriteAuthorWithCurrentUser })}
             onChange={() => this.changeHandler({ isOverwriteAuthorWithCurrentUser: !option.isOverwriteAuthorWithCurrentUser })}
           />
           />
-          <label htmlFor="cbOpt1">
+          <label htmlFor="cbOpt1" className="custom-control-label">
             {t(`${translationBase}.overwrite_author.label`)}
             {t(`${translationBase}.overwrite_author.label`)}
-            <p className="help-block mt-0" dangerouslySetInnerHTML={{ __html: t(`${translationBase}.overwrite_author.desc`) }} />
+            <p className="form-text text-muted mt-0" dangerouslySetInnerHTML={{ __html: t(`${translationBase}.overwrite_author.desc`) }} />
           </label>
           </label>
         </div>
         </div>
       </>
       </>
@@ -188,19 +203,19 @@ class ImportCollectionConfigurationModal extends React.Component {
     }
     }
 
 
     return (
     return (
-      <Modal show={this.props.isOpen} onHide={this.props.onClose} onEnter={this.initialize}>
-        <Modal.Header closeButton>
-          <Modal.Title>{`'${collectionName}'`} Configuration</Modal.Title>
-        </Modal.Header>
+      <Modal isOpen={this.props.isOpen} toggle={this.props.onClose} onEnter={this.initialize}>
+        <ModalHeader tag="h4" toggle={this.props.onClose} className="bg-info text-light">
+          {`'${collectionName}'`} Configuration
+        </ModalHeader>
 
 
-        <Modal.Body>
+        <ModalBody>
           {contents}
           {contents}
-        </Modal.Body>
+        </ModalBody>
 
 
-        <Modal.Footer>
-          <button type="button" className="btn btn-sm btn-default" onClick={this.props.onClose}>{t('Cancel')}</button>
+        <ModalFooter>
+          <button type="button" className="btn btn-sm btn-outline-secondary" onClick={this.props.onClose}>{t('Cancel')}</button>
           <button type="button" className="btn btn-sm btn-primary" onClick={this.updateOption}>{t('Update')}</button>
           <button type="button" className="btn btn-sm btn-primary" onClick={this.updateOption}>{t('Update')}</button>
-        </Modal.Footer>
+        </ModalFooter>
       </Modal>
       </Modal>
     );
     );
   }
   }

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

@@ -4,7 +4,7 @@ import PropTypes from 'prop-types';
 // eslint-disable-next-line no-unused-vars
 // eslint-disable-next-line no-unused-vars
 import { withTranslation } from 'react-i18next';
 import { withTranslation } from 'react-i18next';
 
 
-import ProgressBar from 'react-bootstrap/es/ProgressBar';
+import { Progress } from 'reactstrap';
 
 
 import GrowiArchiveImportOption from '@commons/models/admin/growi-archive-import-option';
 import GrowiArchiveImportOption from '@commons/models/admin/growi-archive-import-option';
 
 
@@ -74,7 +74,7 @@ export default class ImportCollectionItem extends React.Component {
   renderModeLabel(mode, isColorized = false) {
   renderModeLabel(mode, isColorized = false) {
     const attrMap = MODE_ATTR_MAP[mode];
     const attrMap = MODE_ATTR_MAP[mode];
     const className = isColorized ? `text-${attrMap.color}` : '';
     const className = isColorized ? `text-${attrMap.color}` : '';
-    return <span className={className}><i className={attrMap.icon}></i> {attrMap.label}</span>;
+    return <span className={`text-nowrap ${className}`}><i className={attrMap.icon}></i> {attrMap.label}</span>;
   }
   }
 
 
   renderCheckbox() {
   renderCheckbox() {
@@ -83,18 +83,18 @@ export default class ImportCollectionItem extends React.Component {
     } = this.props;
     } = this.props;
 
 
     return (
     return (
-      <div className="checkbox checkbox-info my-0">
+      <div className="custom-control custom-checkbox custom-checkbox-info my-0">
         <input
         <input
           type="checkbox"
           type="checkbox"
           id={collectionName}
           id={collectionName}
           name={collectionName}
           name={collectionName}
-          className="form-check-input"
+          className="custom-control-input"
           value={collectionName}
           value={collectionName}
           checked={isSelected}
           checked={isSelected}
           disabled={isImporting}
           disabled={isImporting}
           onChange={this.changeHandler}
           onChange={this.changeHandler}
         />
         />
-        <label className="text-capitalize form-check-label" htmlFor={collectionName}>
+        <label className="text-capitalize custom-control-label" htmlFor={collectionName}>
           {collectionName}
           {collectionName}
         </label>
         </label>
       </div>
       </div>
@@ -116,7 +116,7 @@ export default class ImportCollectionItem extends React.Component {
         Mode:&nbsp;
         Mode:&nbsp;
         <div className="dropdown d-inline-block">
         <div className="dropdown d-inline-block">
           <button
           <button
-            className={`btn ${btnColor} btn-xs dropdown-toggle`}
+            className={`btn ${btnColor} btn-sm dropdown-toggle`}
             type="button"
             type="button"
             id="ddmMode"
             id="ddmMode"
             disabled={isImporting}
             disabled={isImporting}
@@ -131,7 +131,7 @@ export default class ImportCollectionItem extends React.Component {
             { modes.map((mode) => {
             { modes.map((mode) => {
               return (
               return (
                 <li key={`buttonMode_${mode}`}>
                 <li key={`buttonMode_${mode}`}>
-                  <a type="button" role="button" onClick={() => this.modeSelectedHandler(mode)}>
+                  <a type="button" className="dropdown-item" role="button" onClick={() => this.modeSelectedHandler(mode)}>
                     {this.renderModeLabel(mode, true)}
                     {this.renderModeLabel(mode, true)}
                   </a>
                   </a>
                 </li>
                 </li>
@@ -149,7 +149,7 @@ export default class ImportCollectionItem extends React.Component {
     return (
     return (
       <button
       <button
         type="button"
         type="button"
-        className="btn btn-default btn-xs ml-2"
+        className="btn btn-outline-secondary btn-sm p-1 ml-2"
         disabled={isImporting || !isConfigButtonAvailable}
         disabled={isImporting || !isConfigButtonAvailable}
         onClick={isConfigButtonAvailable ? this.configButtonClickedHandler : null}
         onClick={isConfigButtonAvailable ? this.configButtonClickedHandler : null}
       >
       >
@@ -166,11 +166,11 @@ export default class ImportCollectionItem extends React.Component {
     const total = insertedCount + modifiedCount + errorsCount;
     const total = insertedCount + modifiedCount + errorsCount;
 
 
     return (
     return (
-      <ProgressBar className="mb-0">
-        <ProgressBar max={total} striped={isImporting} active={isImporting} now={insertedCount} bsStyle="info" />
-        <ProgressBar max={total} striped={isImporting} active={isImporting} now={modifiedCount} bsStyle="success" />
-        <ProgressBar max={total} striped={isImporting} active={isImporting} now={errorsCount} bsStyle="danger" />
-      </ProgressBar>
+      <Progress multi className="mb-0">
+        <Progress bar max={total} color="info" striped={isImporting} animated={isImporting} value={insertedCount} />
+        <Progress bar max={total} color="success" striped={isImporting} animated={isImporting} value={modifiedCount} />
+        <Progress bar max={total} color="danger" striped={isImporting} animated={isImporting} value={errorsCount} />
+      </Progress>
     );
     );
   }
   }
 
 
@@ -201,9 +201,9 @@ export default class ImportCollectionItem extends React.Component {
     } = this.props;
     } = this.props;
 
 
     return (
     return (
-      <div className="panel panel-default">
-        <div className="panel-heading">
-          <div className="d-flex justify-content-between align-items-center">
+      <div className="card border-light">
+        <div className="card-header bg-light">
+          <div className="d-flex justify-content-between align-items-center flex-wrap">
             {/* left */}
             {/* left */}
             {this.renderCheckbox()}
             {this.renderCheckbox()}
             {/* right */}
             {/* right */}
@@ -213,14 +213,12 @@ export default class ImportCollectionItem extends React.Component {
             </span>
             </span>
           </div>
           </div>
         </div>
         </div>
-        { isSelected && (
+        {isSelected && (
           <>
           <>
             {this.renderProgressBar()}
             {this.renderProgressBar()}
-            <div className="panel-body">
-              {this.renderBody()}
-            </div>
+            <div className="card-body">{this.renderBody()}</div>
           </>
           </>
-        ) }
+        )}
       </div>
       </div>
     );
     );
   }
   }

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

@@ -378,7 +378,7 @@ class ImportForm extends React.Component {
           const isConfigButtonAvailable = Object.keys(IMPORT_OPTION_CLASS_MAPPING).includes(collectionName);
           const isConfigButtonAvailable = Object.keys(IMPORT_OPTION_CLASS_MAPPING).includes(collectionName);
 
 
           return (
           return (
-            <div className="col-xs-6 my-1" key={collectionName}>
+            <div className="col-md-6 my-1" key={collectionName}>
               <ImportCollectionItem
               <ImportCollectionItem
                 isImporting={isImporting}
                 isImporting={isImporting}
                 isImported={collectionProgress ? isImported : false}
                 isImported={collectionProgress ? isImported : false}
@@ -446,17 +446,18 @@ class ImportForm extends React.Component {
       <>
       <>
         <form className="form-inline">
         <form className="form-inline">
           <div className="form-group">
           <div className="form-group">
-            <button type="button" className="btn btn-sm btn-default mr-2" onClick={this.checkAll}>
+            <button type="button" className="btn btn-sm btn-outline-secondary mr-2" onClick={this.checkAll}>
               <i className="fa fa-check-square-o"></i> {t('admin:export_management.check_all')}
               <i className="fa fa-check-square-o"></i> {t('admin:export_management.check_all')}
             </button>
             </button>
           </div>
           </div>
           <div className="form-group">
           <div className="form-group">
-            <button type="button" className="btn btn-sm btn-default mr-2" onClick={this.uncheckAll}>
+            <button type="button" className="btn btn-sm btn-outline-secondary mr-2" onClick={this.uncheckAll}>
               <i className="fa fa-square-o"></i> {t('admin:export_management.uncheck_all')}
               <i className="fa fa-square-o"></i> {t('admin:export_management.uncheck_all')}
             </button>
             </button>
           </div>
           </div>
         </form>
         </form>
-        <div className="well well-sm small my-4">
+
+        <div className="card well small my-4">
           <ul>
           <ul>
             <li>{t('admin:importer_management.growi_settings.description_of_import_mode.about')}</li>
             <li>{t('admin:importer_management.growi_settings.description_of_import_mode.about')}</li>
             <ul>
             <ul>
@@ -473,7 +474,7 @@ class ImportForm extends React.Component {
         {this.renderOthers()}
         {this.renderOthers()}
 
 
         <div className="mt-4 text-center">
         <div className="mt-4 text-center">
-          <button type="button" className="btn btn-default mx-1" onClick={this.props.onDiscard}>
+          <button type="button" className="btn btn-outline-secondary mx-1" onClick={this.props.onDiscard}>
             {t('admin:importer_management.growi_settings.discard')}
             {t('admin:importer_management.growi_settings.discard')}
           </button>
           </button>
           <button type="button" className="btn btn-primary mx-1" onClick={this.import} disabled={!canImport || isImporting}>
           <button type="button" className="btn btn-primary mx-1" onClick={this.import} disabled={!canImport || isImporting}>

+ 8 - 6
src/client/js/components/Admin/ImportData/GrowiArchive/UploadForm.jsx

@@ -48,11 +48,13 @@ class UploadForm extends React.Component {
     const { t } = this.props;
     const { t } = this.props;
 
 
     return (
     return (
-      <form className="form-horizontal" onSubmit={this.uploadZipFile}>
+      <form onSubmit={this.uploadZipFile}>
         <fieldset>
         <fieldset>
-          <div className="form-group">
-            <label htmlFor="file" className="col-xs-3 control-label">{t('admin:importer_management.growi_settings.growi_archive_file')}</label>
-            <div className="col-xs-6">
+          <div className="form-group row">
+            <label htmlFor="file" className="col-md-3 col-form-label col-form-label-sm">
+              {t('admin:importer_management.growi_settings.growi_archive_file')}
+            </label>
+            <div className="col-md-6">
               <input
               <input
                 type="file"
                 type="file"
                 name="file"
                 name="file"
@@ -63,8 +65,8 @@ class UploadForm extends React.Component {
               />
               />
             </div>
             </div>
           </div>
           </div>
-          <div className="form-group">
-            <div className="col-xs-offset-3 col-xs-6">
+          <div className="form-group row">
+            <div className="mx-auto">
               <button type="submit" className="btn btn-primary" disabled={!this.validateForm()}>
               <button type="submit" className="btn btn-primary" disabled={!this.validateForm()}>
                 {t('admin:importer_management.growi_settings.upload')}
                 {t('admin:importer_management.growi_settings.upload')}
               </button>
               </button>

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

@@ -138,12 +138,12 @@ class ImportDataPage extends React.Component {
         <GrowiArchiveSection />
         <GrowiArchiveSection />
 
 
         <form
         <form
-          className="form-horizontal mt-5"
+          className="mt-5"
           id="importerSettingFormEsa"
           id="importerSettingFormEsa"
           role="form"
           role="form"
         >
         >
           <fieldset>
           <fieldset>
-            <legend>{t('admin:importer_management.import_from', { from: 'esa.io' })}</legend>
+            <h2 className="admin-setting-header">{t('admin:importer_management.import_from', { from: 'esa.io' })}</h2>
             <table className="table table-bordered table-mapping">
             <table className="table table-bordered table-mapping">
               <thead>
               <thead>
                 <tr>
                 <tr>
@@ -171,37 +171,37 @@ class ImportDataPage extends React.Component {
               </tbody>
               </tbody>
             </table>
             </table>
 
 
-            <div className="well well-sm mb-0 small">
+            <div className="card well mb-0 small">
               <ul>
               <ul>
                 <li>{t('admin:importer_management.page_skip')}</li>
                 <li>{t('admin:importer_management.page_skip')}</li>
               </ul>
               </ul>
             </div>
             </div>
 
 
-            <div className="form-group">
+            <div className="form-group row">
               <input type="password" name="dummypass" style={{ display: 'none', top: '-100px', left: '-100px' }} />
               <input type="password" name="dummypass" style={{ display: 'none', top: '-100px', left: '-100px' }} />
             </div>
             </div>
 
 
-            <div className="form-group">
-              <label htmlFor="settingForm[importer:esa:team_name]" className="col-xs-3 control-label">
-                {t('admin:importer_management.esa_settings.team_name')}
+            <div className="form-group row">
+              <label htmlFor="settingForm[importer:esa:team_name]" className="text-left text-md-right col-md-3 col-form-label">
+                { t('admin:importer_management.esa_settings.team_name') }
               </label>
               </label>
-              <div className="col-xs-6">
+              <div className="col-md-6">
                 <input className="form-control" type="text" name="esaTeamName" value={esaTeamName} onChange={this.handleInputValue} />
                 <input className="form-control" type="text" name="esaTeamName" value={esaTeamName} onChange={this.handleInputValue} />
               </div>
               </div>
 
 
             </div>
             </div>
 
 
-            <div className="form-group">
-              <label htmlFor="settingForm[importer:esa:access_token]" className="col-xs-3 control-label">
-                {t('admin:importer_management.esa_settings.access_token')}
+            <div className="form-group row">
+              <label htmlFor="settingForm[importer:esa:access_token]" className="text-left text-md-right col-md-3 col-form-label">
+                { t('admin:importer_management.esa_settings.access_token') }
               </label>
               </label>
-              <div className="col-xs-6">
+              <div className="col-md-6">
                 <input className="form-control" type="password" name="esaAccessToken" value={esaAccessToken} onChange={this.handleInputValue} />
                 <input className="form-control" type="password" name="esaAccessToken" value={esaAccessToken} onChange={this.handleInputValue} />
               </div>
               </div>
             </div>
             </div>
 
 
-            <div className="form-group">
-              <div className="col-xs-offset-3 col-xs-6">
+            <div className="form-group row">
+              <div className="offset-md-3 col-md-6">
                 <input
                 <input
                   id="testConnectionToEsa"
                   id="testConnectionToEsa"
                   type="button"
                   type="button"
@@ -211,12 +211,12 @@ class ImportDataPage extends React.Component {
                   value={t('admin:importer_management.import')}
                   value={t('admin:importer_management.import')}
                 />
                 />
                 <input type="button" className="btn btn-secondary" onClick={this.esaHandleSubmitUpdate} value={t('Update')} />
                 <input type="button" className="btn btn-secondary" onClick={this.esaHandleSubmitUpdate} value={t('Update')} />
-                <span className="col-xs-offset-1">
+                <span className="offset-0 offset-sm-1">
                   <input
                   <input
-                    name="Esa"
-                    type="button"
                     id="importFromEsa"
                     id="importFromEsa"
-                    className="btn btn-default btn-esa"
+                    type="button"
+                    name="Esa"
+                    className="btn btn-secondary btn-esa"
                     onClick={this.esaHandleSubmitTest}
                     onClick={this.esaHandleSubmitTest}
                     value={t('admin:importer_management.esa_settings.test_connection')}
                     value={t('admin:importer_management.esa_settings.test_connection')}
                   />
                   />
@@ -228,12 +228,12 @@ class ImportDataPage extends React.Component {
         </form>
         </form>
 
 
         <form
         <form
-          className="form-horizontal mt-5"
+          className="mt-5"
           id="importerSettingFormQiita"
           id="importerSettingFormQiita"
           role="form"
           role="form"
         >
         >
           <fieldset>
           <fieldset>
-            <legend>{t('admin:importer_management.import_from', { from: 'Qiita:Team' })}</legend>
+            <h2 className="admin-setting-header">{t('admin:importer_management.import_from', { from: 'Qiita:Team' })}</h2>
             <table className="table table-bordered table-mapping">
             <table className="table table-bordered table-mapping">
               <thead>
               <thead>
                 <tr>
                 <tr>
@@ -265,36 +265,36 @@ class ImportDataPage extends React.Component {
                 </tr>
                 </tr>
               </tbody>
               </tbody>
             </table>
             </table>
-            <div className="well well-sm mb-0 small">
+            <div className="card well mb-0 small">
               <ul>
               <ul>
                 <li>{t('admin:importer_management.page_skip')}</li>
                 <li>{t('admin:importer_management.page_skip')}</li>
               </ul>
               </ul>
             </div>
             </div>
 
 
-            <div className="form-group">
+            <div className="form-group row">
               <input type="password" name="dummypass" style={{ display: 'none', top: '-100px', left: '-100px' }} />
               <input type="password" name="dummypass" style={{ display: 'none', top: '-100px', left: '-100px' }} />
             </div>
             </div>
-            <div className="form-group">
-              <label htmlFor="settingForm[importer:qiita:team_name]" className="col-xs-3 control-label">
-                {t('admin:importer_management.qiita_settings.team_name')}
+            <div className="form-group row">
+              <label htmlFor="settingForm[importer:qiita:team_name]" className="text-left text-md-right col-md-3 col-form-label">
+                { t('admin:importer_management.qiita_settings.team_name') }
               </label>
               </label>
-              <div className="col-xs-6">
+              <div className="col-md-6">
                 <input className="form-control" type="text" name="qiitaTeamName" value={qiitaTeamName} onChange={this.handleInputValue} />
                 <input className="form-control" type="text" name="qiitaTeamName" value={qiitaTeamName} onChange={this.handleInputValue} />
               </div>
               </div>
             </div>
             </div>
 
 
-            <div className="form-group">
-              <label htmlFor="settingForm[importer:qiita:access_token]" className="col-xs-3 control-label">
-                {t('admin:importer_management.qiita_settings.access_token')}
+            <div className="form-group row">
+              <label htmlFor="settingForm[importer:qiita:access_token]" className="text-left text-md-right col-md-3 col-form-label">
+                { t('admin:importer_management.qiita_settings.access_token') }
               </label>
               </label>
-              <div className="col-xs-6">
+              <div className="col-md-6">
                 <input className="form-control" type="password" name="qiitaAccessToken" value={qiitaAccessToken} onChange={this.handleInputValue} />
                 <input className="form-control" type="password" name="qiitaAccessToken" value={qiitaAccessToken} onChange={this.handleInputValue} />
               </div>
               </div>
             </div>
             </div>
 
 
 
 
-            <div className="form-group">
-              <div className="col-xs-offset-3 col-xs-6">
+            <div className="form-group row">
+              <div className="offset-md-3 col-md-6">
                 <input
                 <input
                   id="testConnectionToQiita"
                   id="testConnectionToQiita"
                   type="button"
                   type="button"
@@ -304,12 +304,12 @@ class ImportDataPage extends React.Component {
                   value={t('admin:importer_management.import')}
                   value={t('admin:importer_management.import')}
                 />
                 />
                 <input type="button" className="btn btn-secondary" onClick={this.qiitaHandleSubmitUpdate} value={t('Update')} />
                 <input type="button" className="btn btn-secondary" onClick={this.qiitaHandleSubmitUpdate} value={t('Update')} />
-                <span className="col-xs-offset-1">
+                <span className="offset-0 offset-sm-1">
                   <input
                   <input
                     name="Qiita"
                     name="Qiita"
                     type="button"
                     type="button"
                     id="importFromQiita"
                     id="importFromQiita"
-                    className="btn btn-default btn-qiita"
+                    className="btn btn-secondary btn-qiita"
                     onClick={this.qiitaHandleSubmitTest}
                     onClick={this.qiitaHandleSubmitTest}
                     value={t('admin:importer_management.qiita_settings.test_connection')}
                     value={t('admin:importer_management.qiita_settings.test_connection')}
                   />
                   />

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

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

+ 32 - 42
src/client/js/components/Admin/MarkdownSetting/LineBreakForm.jsx

@@ -1,5 +1,6 @@
 /* eslint-disable react/no-danger */
 /* eslint-disable react/no-danger */
 import React from 'react';
 import React from 'react';
+
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
 import { withTranslation } from 'react-i18next';
 import { withTranslation } from 'react-i18next';
 import loggerFactory from '@alias/logger';
 import loggerFactory from '@alias/logger';
@@ -7,8 +8,10 @@ import loggerFactory from '@alias/logger';
 import { createSubscribedElement } from '../../UnstatedUtils';
 import { createSubscribedElement } from '../../UnstatedUtils';
 import { toastSuccess, toastError } from '../../../util/apiNotification';
 import { toastSuccess, toastError } from '../../../util/apiNotification';
 
 
+
 import AppContainer from '../../../services/AppContainer';
 import AppContainer from '../../../services/AppContainer';
 import AdminMarkDownContainer from '../../../services/AdminMarkDownContainer';
 import AdminMarkDownContainer from '../../../services/AdminMarkDownContainer';
+import AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow';
 
 
 const logger = loggerFactory('growi:importer');
 const logger = loggerFactory('growi:importer');
 
 
@@ -41,21 +44,20 @@ class LineBreakForm extends React.Component {
     const helpLineBreak = { __html: t('admin:markdown_setting.lineBreak_options.enable_lineBreak_desc') };
     const helpLineBreak = { __html: t('admin:markdown_setting.lineBreak_options.enable_lineBreak_desc') };
 
 
     return (
     return (
-      <div className="form-group row">
-        <div className="col-xs-offset-4 col-xs-6 text-left">
-          <div className="checkbox checkbox-success">
-            <input
-              type="checkbox"
-              id="isEnabledLinebreaks"
-              checked={isEnabledLinebreaks}
-              onChange={() => { adminMarkDownContainer.setState({ isEnabledLinebreaks: !isEnabledLinebreaks }) }}
-            />
-            <label htmlFor="isEnabledLinebreaks">
-              {t('admin:markdown_setting.lineBreak_options.enable_lineBreak')}
-            </label>
-          </div>
-          <p className="help-block" dangerouslySetInnerHTML={helpLineBreak} />
+      <div className="col">
+        <div className="custom-control custom-checkbox custom-checkbox-success">
+          <input
+            type="checkbox"
+            className="custom-control-input"
+            id="isEnabledLinebreaks"
+            checked={isEnabledLinebreaks}
+            onChange={() => { adminMarkDownContainer.setState({ isEnabledLinebreaks: !isEnabledLinebreaks }) }}
+          />
+          <label className="custom-control-label" htmlFor="isEnabledLinebreaks">
+            {t('admin:markdown_setting.lineBreak_options.enable_lineBreak') }
+          </label>
         </div>
         </div>
+        <p className="form-text text-muted" dangerouslySetInnerHTML={helpLineBreak} />
       </div>
       </div>
     );
     );
   }
   }
@@ -67,46 +69,34 @@ class LineBreakForm extends React.Component {
     const helpLineBreakInComment = { __html: t('admin:markdown_setting.lineBreak_options.enable_lineBreak_for_comment_desc') };
     const helpLineBreakInComment = { __html: t('admin:markdown_setting.lineBreak_options.enable_lineBreak_for_comment_desc') };
 
 
     return (
     return (
-      <div className="form-group row">
-        <div className="col-xs-offset-4 col-xs-6 text-left">
-          <div className="checkbox checkbox-success">
-            <input
-              type="checkbox"
-              id="isEnabledLinebreaksInComments"
-              checked={isEnabledLinebreaksInComments}
-              onChange={() => { adminMarkDownContainer.setState({ isEnabledLinebreaksInComments: !isEnabledLinebreaksInComments }) }}
-            />
-            <label htmlFor="isEnabledLinebreaksInComments">
-              {t('admin:markdown_setting.lineBreak_options.enable_lineBreak')}
-            </label>
-          </div>
-          <p className="help-block" dangerouslySetInnerHTML={helpLineBreakInComment} />
+      <div className="col">
+        <div className="custom-control custom-checkbox custom-checkbox-success">
+          <input
+            type="checkbox"
+            className="custom-control-input"
+            id="isEnabledLinebreaksInComments"
+            checked={isEnabledLinebreaksInComments}
+            onChange={() => { adminMarkDownContainer.setState({ isEnabledLinebreaksInComments: !isEnabledLinebreaksInComments }) }}
+          />
+          <label className="custom-control-label" htmlFor="isEnabledLinebreaksInComments">
+            {t('admin:markdown_setting.lineBreak_options.enable_lineBreak') }
+          </label>
         </div>
         </div>
+        <p className="form-text text-muted" dangerouslySetInnerHTML={helpLineBreakInComment} />
       </div>
       </div>
     );
     );
   }
   }
 
 
   render() {
   render() {
-    const { t, adminMarkDownContainer } = this.props;
+    const { adminMarkDownContainer } = this.props;
 
 
     return (
     return (
       <React.Fragment>
       <React.Fragment>
-        <fieldset className="row">
+        <fieldset className="form-group row row-cols-1 row-cols-md-2 mx-3">
           {this.renderLineBreakOption()}
           {this.renderLineBreakOption()}
           {this.renderLineBreakInCommentOption()}
           {this.renderLineBreakInCommentOption()}
         </fieldset>
         </fieldset>
-        <div className="form-group my-3">
-          <div className="col-xs-offset-4 col-xs-5">
-            <button
-              type="submit"
-              className="btn btn-primary"
-              onClick={this.onClickSubmit}
-              disabled={adminMarkDownContainer.state.retrieveError != null}
-            >
-              {t('Update')}
-            </button>
-          </div>
-        </div>
+        <AdminUpdateButtonRow onClick={this.onClickSubmit} disabled={adminMarkDownContainer.state.retrieveError != null} />
       </React.Fragment>
       </React.Fragment>
     );
     );
   }
   }

+ 16 - 15
src/client/js/components/Admin/MarkdownSetting/MarkDownSetting.jsx

@@ -1,4 +1,5 @@
 import React from 'react';
 import React from 'react';
+import { Card, CardBody } from 'reactstrap';
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
 import { withTranslation } from 'react-i18next';
 import { withTranslation } from 'react-i18next';
 
 
@@ -37,25 +38,25 @@ class MarkdownSetting extends React.Component {
     return (
     return (
       <React.Fragment>
       <React.Fragment>
         {/* Line Break Setting */}
         {/* Line Break Setting */}
-        <div className="row mb-5">
-          <h2 className="border-bottom">{t('admin:markdown_setting.lineBreak_header')}</h2>
-          <p className="well">{t('admin:markdown_setting.lineBreak_desc')}</p>
-          <LineBreakForm />
-        </div>
+        <h2 className="admin-setting-header">{t('admin:markdown_setting.lineBreak_header')}</h2>
+        <Card className="card well my-3">
+          <CardBody className="px-0 py-2">{ t('admin:markdown_setting.lineBreak_desc') }</CardBody>
+        </Card>
+        <LineBreakForm />
 
 
         {/* Presentation Setting */}
         {/* Presentation Setting */}
-        <div className="row mb-5">
-          <h2 className="border-bottom">{t('admin:markdown_setting.presentation_header')}</h2>
-          <p className="well">{t('admin:markdown_setting.presentation_desc')}</p>
-          <PresentationForm />
-        </div>
+        <h2 className="admin-setting-header">{ t('admin:markdown_setting.presentation_header') }</h2>
+        <Card className="card well my-3">
+          <CardBody className="px-0 py-2">{ t('admin:markdown_setting.presentation_desc') }</CardBody>
+        </Card>
+        <PresentationForm />
 
 
         {/* XSS Setting */}
         {/* XSS Setting */}
-        <div className="row mb-5">
-          <h2 className="border-bottom">{t('admin:markdown_setting.xss_header')}</h2>
-          <p className="well">{t('admin:markdown_setting.xss_desc')}</p>
-          <XssForm />
-        </div>
+        <h2 className="admin-setting-header">{ t('admin:markdown_setting.xss_header') }</h2>
+        <Card className="card well my-3">
+          <CardBody className="px-0 py-2">{ t('admin:markdown_setting.xss_desc') }</CardBody>
+        </Card>
+        <XssForm />
       </React.Fragment>
       </React.Fragment>
     );
     );
   }
   }

+ 72 - 53
src/client/js/components/Admin/MarkdownSetting/PresentationForm.jsx

@@ -1,4 +1,5 @@
 import React from 'react';
 import React from 'react';
+
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
 import { withTranslation } from 'react-i18next';
 import { withTranslation } from 'react-i18next';
 import loggerFactory from '@alias/logger';
 import loggerFactory from '@alias/logger';
@@ -8,6 +9,7 @@ import { toastSuccess, toastError } from '../../../util/apiNotification';
 
 
 import AppContainer from '../../../services/AppContainer';
 import AppContainer from '../../../services/AppContainer';
 import AdminMarkDownContainer from '../../../services/AdminMarkDownContainer';
 import AdminMarkDownContainer from '../../../services/AdminMarkDownContainer';
+import AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow';
 
 
 const logger = loggerFactory('growi:markdown:presentation');
 const logger = loggerFactory('growi:markdown:presentation');
 
 
@@ -38,70 +40,87 @@ class PresentationForm extends React.Component {
     const { pageBreakSeparator, pageBreakCustomSeparator } = adminMarkDownContainer.state;
     const { pageBreakSeparator, pageBreakCustomSeparator } = adminMarkDownContainer.state;
 
 
     return (
     return (
-      <fieldset className="form-group row my-2">
+      <fieldset className="form-group col-12 my-2">
 
 
-        <label className="col-xs-3 control-label text-right">
+        <label className="col-8 offset-4 col-form-label font-weight-bold text-left mt-3">
           {t('admin:markdown_setting.presentation_options.page_break_setting')}
           {t('admin:markdown_setting.presentation_options.page_break_setting')}
         </label>
         </label>
 
 
-        <div className="col-xs-3 radio radio-primary">
-          <input
-            type="radio"
-            id="pageBreakOption1"
-            checked={pageBreakSeparator === 1}
-            onChange={() => adminMarkDownContainer.switchPageBreakSeparator(1)}
-          />
-          <label htmlFor="pageBreakOption1">
-            <p className="font-weight-bold">{t('admin:markdown_setting.presentation_options.preset_one_separator')}</p>
-            <div className="mt-3">
-              {t('admin:markdown_setting.presentation_options.preset_one_separator_desc')}
-              <pre><code>{t('admin:markdown_setting.presentation_options.preset_one_separator_value')}</code></pre>
+        <div className="form-group col-12 my-3">
+          <div className="row">
+            <div className="col-md-4 col-sm-12 align-self-start mb-4">
+              <div className="custom-control custom-radio">
+                <input
+                  type="radio"
+                  className="custom-control-input"
+                  id="pageBreakOption1"
+                  checked={pageBreakSeparator === 1}
+                  onChange={() => adminMarkDownContainer.switchPageBreakSeparator(1)}
+                />
+                <label className="custom-control-label w-100" htmlFor="pageBreakOption1">
+                  <p className="font-weight-bold">{ t('admin:markdown_setting.presentation_options.preset_one_separator') }</p>
+                  <div className="mt-3">
+                    { t('admin:markdown_setting.presentation_options.preset_one_separator_desc') }
+                    <input
+                      className="form-control"
+                      type="text"
+                      value={t('admin:markdown_setting.presentation_options.preset_one_separator_value')}
+                      readOnly
+                    />
+                  </div>
+                </label>
+              </div>
             </div>
             </div>
-          </label>
-        </div>
 
 
-        <div className="col-xs-3 radio radio-primary mt-3">
-          <input
-            type="radio"
-            id="pageBreakOption2"
-            checked={pageBreakSeparator === 2}
-            onChange={() => adminMarkDownContainer.switchPageBreakSeparator(2)}
-          />
-          <label htmlFor="pageBreakOption2">
-            <p className="font-weight-bold">{t('admin:markdown_setting.presentation_options.preset_two_separator')}</p>
-            <div className="mt-3">
-              {t('admin:markdown_setting.presentation_options.preset_two_separator_desc')}
-              <pre><code>{t('admin:markdown_setting.presentation_options.preset_two_separator_value')}</code></pre>
+            <div className="col-md-4 col-sm-12 align-self-start mb-4">
+              <div className="custom-control custom-radio">
+                <input
+                  type="radio"
+                  className="custom-control-input"
+                  id="pageBreakOption2"
+                  checked={pageBreakSeparator === 2}
+                  onChange={() => adminMarkDownContainer.switchPageBreakSeparator(2)}
+                />
+                <label className="custom-control-label w-100" htmlFor="pageBreakOption2">
+                  <p className="font-weight-bold">{ t('admin:markdown_setting.presentation_options.preset_two_separator') }</p>
+                  <div className="mt-3">
+                    { t('admin:markdown_setting.presentation_options.preset_two_separator_desc') }
+                    <input
+                      className="form-control"
+                      type="text"
+                      value={t('admin:markdown_setting.presentation_options.preset_two_separator_value')}
+                      readOnly
+                    />
+                  </div>
+                </label>
+              </div>
             </div>
             </div>
-          </label>
-        </div>
-
-        <div className="col-xs-3 radio radio-primary mt-3">
-          <input
-            type="radio"
-            id="pageBreakOption3"
-            checked={pageBreakSeparator === 3}
-            onChange={() => adminMarkDownContainer.switchPageBreakSeparator(3)}
-          />
-          <label htmlFor="pageBreakOption3">
-            <p className="font-weight-bold">{t('admin:markdown_setting.presentation_options.custom_separator')}</p>
-            <div className="mt-3">
-              {t('admin:markdown_setting.presentation_options.custom_separator_desc')}
-              <input
-                className="form-control"
-                defaultValue={pageBreakCustomSeparator}
-                onChange={(e) => { adminMarkDownContainer.setPageBreakCustomSeparator(e.target.value) }}
-              />
+            <div className="col-md-4 col-sm-12 align-self-start mb-4">
+              <div className="custom-control custom-radio">
+                <input
+                  type="radio"
+                  id="pageBreakOption3"
+                  className="custom-control-input"
+                  checked={pageBreakSeparator === 3}
+                  onChange={() => adminMarkDownContainer.switchPageBreakSeparator(3)}
+                />
+                <label className="custom-control-label w-100" htmlFor="pageBreakOption3">
+                  <p className="font-weight-bold">{ t('admin:markdown_setting.presentation_options.custom_separator') }</p>
+                  <div className="mt-3">
+                    { t('admin:markdown_setting.presentation_options.custom_separator_desc') }
+                    <input
+                      className="form-control"
+                      defaultValue={pageBreakCustomSeparator}
+                      onChange={(e) => { adminMarkDownContainer.setPageBreakCustomSeparator(e.target.value) }}
+                    />
+                  </div>
+                </label>
+              </div>
             </div>
             </div>
-          </label>
-        </div>
-
-        <div className="form-group my-3">
-          <div className="col-xs-offset-4 col-xs-5">
-            <div className="btn btn-primary" onClick={this.onClickSubmit} disabled={adminMarkDownContainer.state.retrieveError != null}>{t('Update')}</div>
           </div>
           </div>
         </div>
         </div>
 
 
+        <AdminUpdateButtonRow onClick={this.onClickSubmit} disabled={adminMarkDownContainer.state.retrieveError != null} />
       </fieldset>
       </fieldset>
     );
     );
   }
   }

+ 0 - 86
src/client/js/components/Admin/MarkdownSetting/PresentationLineBreakOptions.jsx

@@ -1,86 +0,0 @@
-import React, { Fragment } from 'react';
-import PropTypes from 'prop-types';
-import { withTranslation } from 'react-i18next';
-
-import { createSubscribedElement } from '../../UnstatedUtils';
-
-import AppContainer from '../../../services/AppContainer';
-import AdminMarkDownContainer from '../../../services/AdminMarkDownContainer';
-
-class PresentationLineBreakOptions extends React.Component {
-
-  render() {
-    const { t, adminMarkDownContainer } = this.props;
-    const { pageBreakOption, customRegularExpression } = adminMarkDownContainer.state;
-
-    return (
-      <Fragment>
-        <div className="col-xs-3 radio radio-primary">
-          <input
-            type="radio"
-            id="pageBreakOption1"
-            checked={pageBreakOption === 1}
-            onChange={() => { adminMarkDownContainer.setState({ pageBreakOption: 1 }) }}
-          />
-          <label htmlFor="pageBreakOption1">
-            <p className="font-weight-bold">{ t('markdown_setting.Preset one separator') }</p>
-            <div className="mt-3">
-              { t('markdown_setting.Preset one separator desc') }
-              <pre><code>{ t('markdown_setting.Preset one separator value') }</code></pre>
-            </div>
-          </label>
-        </div>
-
-        <div className="col-xs-3 radio radio-primary mt-3">
-          <input
-            type="radio"
-            id="pageBreakOption2"
-            checked={pageBreakOption === 2}
-            onChange={() => { adminMarkDownContainer.setState({ pageBreakOption: 2 }) }}
-          />
-          <label htmlFor="pageBreakOption2">
-            <p className="font-weight-bold">{ t('markdown_setting.Preset two separator') }</p>
-            <div className="mt-3">
-              { t('markdown_setting.Preset two separator desc') }
-              <pre><code>{ t('markdown_setting.Preset two separator value') }</code></pre>
-            </div>
-          </label>
-        </div>
-
-        <div className="col-xs-3 radio radio-primary mt-3">
-          <input
-            type="radio"
-            id="pageBreakOption3"
-            checked={pageBreakOption === 3}
-            onChange={() => { adminMarkDownContainer.setState({ pageBreakOption: 3 }) }}
-          />
-          <label htmlFor="pageBreakOption3">
-            <p className="font-weight-bold">{ t('markdown_setting.Custom separator') }</p>
-            <div className="mt-3">
-              { t('markdown_setting.Custom separator desc') }
-              <input
-                className="form-control"
-                defaultValue={customRegularExpression}
-                onChange={(e) => { adminMarkDownContainer.setState({ customRegularExpression: e.target.value }) }}
-              />
-            </div>
-          </label>
-        </div>
-      </Fragment>
-    );
-  }
-
-}
-
-const PresentationLineBreakOptionsWrapper = (props) => {
-  return createSubscribedElement(PresentationLineBreakOptions, props, [AppContainer, AdminMarkDownContainer]);
-};
-
-PresentationLineBreakOptions.propTypes = {
-  t: PropTypes.func.isRequired, // i18next
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
-  adminMarkDownContainer: PropTypes.instanceOf(AdminMarkDownContainer).isRequired,
-
-};
-
-export default withTranslation()(PresentationLineBreakOptionsWrapper);

+ 4 - 4
src/client/js/components/Admin/MarkdownSetting/WhiteListInput.jsx

@@ -35,10 +35,10 @@ class WhiteListInput extends React.Component {
 
 
     return (
     return (
       <>
       <>
-        <div className="m-t-15">
+        <div className="mt-4">
           <div className="d-flex justify-content-between">
           <div className="d-flex justify-content-between">
             {t('admin:markdown_setting.xss_options.tag_names')}
             {t('admin:markdown_setting.xss_options.tag_names')}
-            <p id="btn-import-tags" className="btn btn-xs btn-primary" onClick={this.onClickRecommendTagButton}>
+            <p id="btn-import-tags" className="btn btn-sm btn-primary mb-0" onClick={this.onClickRecommendTagButton}>
               {t('admin:markdown_setting.xss_options.import_recommended', { target: 'Tags' })}
               {t('admin:markdown_setting.xss_options.import_recommended', { target: 'Tags' })}
             </p>
             </p>
           </div>
           </div>
@@ -52,10 +52,10 @@ class WhiteListInput extends React.Component {
             onChange={(e) => { adminMarkDownContainer.setState({ tagWhiteList: e.target.value }) }}
             onChange={(e) => { adminMarkDownContainer.setState({ tagWhiteList: e.target.value }) }}
           />
           />
         </div>
         </div>
-        <div className="m-t-15">
+        <div className="mt-4">
           <div className="d-flex justify-content-between">
           <div className="d-flex justify-content-between">
             {t('admin:markdown_setting.xss_options.tag_attributes')}
             {t('admin:markdown_setting.xss_options.tag_attributes')}
-            <p id="btn-import-tags" className="btn btn-xs btn-primary" onClick={this.onClickRecommendAttrButton}>
+            <p id="btn-import-tags" className="btn btn-sm btn-primary mb-0" onClick={this.onClickRecommendAttrButton}>
               {t('admin:markdown_setting.xss_options.import_recommended', { target: 'Attrs' })}
               {t('admin:markdown_setting.xss_options.import_recommended', { target: 'Attrs' })}
             </p>
             </p>
           </div>
           </div>

+ 86 - 74
src/client/js/components/Admin/MarkdownSetting/XssForm.jsx

@@ -1,4 +1,5 @@
 import React from 'react';
 import React from 'react';
+
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
 import { withTranslation } from 'react-i18next';
 import { withTranslation } from 'react-i18next';
 import loggerFactory from '@alias/logger';
 import loggerFactory from '@alias/logger';
@@ -9,6 +10,7 @@ import { tags, attrs } from '../../../../../lib/service/xss/recommended-whitelis
 
 
 import AppContainer from '../../../services/AppContainer';
 import AppContainer from '../../../services/AppContainer';
 import AdminMarkDownContainer from '../../../services/AdminMarkDownContainer';
 import AdminMarkDownContainer from '../../../services/AdminMarkDownContainer';
+import AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow';
 
 
 import WhiteListInput from './WhiteListInput';
 import WhiteListInput from './WhiteListInput';
 
 
@@ -40,76 +42,87 @@ class XssForm extends React.Component {
     const { xssOption } = adminMarkDownContainer.state;
     const { xssOption } = adminMarkDownContainer.state;
 
 
     return (
     return (
-      <fieldset className="row col-xs-12 my-3">
-        <div className="col-xs-4 radio radio-primary">
-          <input
-            type="radio"
-            id="xssOption1"
-            name="XssOption"
-            checked={xssOption === 1}
-            onChange={() => { adminMarkDownContainer.setState({ xssOption: 1 }) }}
-          />
-          <label htmlFor="xssOption1">
-            <p className="font-weight-bold">{t('admin:markdown_setting.xss_options.remove_all_tags')}</p>
-            <div className="m-t-15">
-              {t('admin:markdown_setting.xss_options.remove_all_tags_desc')}
+      <div className="form-group col-12 my-3">
+        <div className="row">
+          <div className="col-md-4 col-sm-12 align-self-start mb-4">
+            <div className="custom-control custom-radio ">
+              <input
+                type="radio"
+                className="custom-control-input"
+                id="xssOption1"
+                name="XssOption"
+                checked={xssOption === 1}
+                onChange={() => { adminMarkDownContainer.setState({ xssOption: 1 }) }}
+              />
+              <label className="custom-control-label w-100" htmlFor="xssOption1">
+                <p className="font-weight-bold">{t('admin:markdown_setting.xss_options.remove_all_tags')}</p>
+                <div className="mt-4">
+                  {t('admin:markdown_setting.xss_options.remove_all_tags_desc')}
+                </div>
+              </label>
             </div>
             </div>
-          </label>
-        </div>
+          </div>
 
 
-        <div className="col-xs-4 radio radio-primary">
-          <input
-            type="radio"
-            id="xssOption2"
-            name="XssOption"
-            checked={xssOption === 2}
-            onChange={() => { adminMarkDownContainer.setState({ xssOption: 2 }) }}
-          />
-          <label htmlFor="xssOption2">
-            <p className="font-weight-bold">{t('admin:markdown_setting.xss_options.recommended_setting')}</p>
-            <div className="m-t-15">
-              <div className="d-flex justify-content-between">
-                {t('admin:markdown_setting.xss_options.tag_names')}
-              </div>
-              <textarea
-                className="form-control xss-list"
-                name="recommendedTags"
-                rows="6"
-                cols="40"
-                readOnly
-                defaultValue={tags}
+          <div className="col-md-4 col-sm-12 align-self-start mb-4">
+            <div className="custom-control custom-radio">
+              <input
+                type="radio"
+                className="custom-control-input"
+                id="xssOption2"
+                name="XssOption"
+                checked={xssOption === 2}
+                onChange={() => { adminMarkDownContainer.setState({ xssOption: 2 }) }}
               />
               />
+              <label className="custom-control-label w-100" htmlFor="xssOption2">
+                <p className="font-weight-bold">{t('admin:markdown_setting.xss_options.recommended_setting')}</p>
+                <div className="mt-4">
+                  <div className="d-flex justify-content-between">
+                    {t('admin:markdown_setting.xss_options.tag_names')}
+                  </div>
+                  <textarea
+                    className="form-control xss-list"
+                    name="recommendedTags"
+                    rows="6"
+                    cols="40"
+                    readOnly
+                    defaultValue={tags}
+                  />
+                </div>
+                <div className="mt-4">
+                  <div className="d-flex justify-content-between">
+                    {t('admin:markdown_setting.xss_options.tag_attributes')}
+                  </div>
+                  <textarea
+                    className="form-control xss-list"
+                    name="recommendedAttrs"
+                    rows="6"
+                    cols="40"
+                    readOnly
+                    defaultValue={attrs}
+                  />
+                </div>
+              </label>
             </div>
             </div>
-            <div className="m-t-15">
-              <div className="d-flex justify-content-between">
-                {t('admin:markdown_setting.xss_options.tag_attributes')}
-              </div>
-              <textarea
-                className="form-control xss-list"
-                name="recommendedAttrs"
-                rows="6"
-                cols="40"
-                readOnly
-                defaultValue={attrs}
+          </div>
+
+          <div className="col-md-4 col-sm-12 align-self-start mb-4">
+            <div className="custom-control custom-radio">
+              <input
+                type="radio"
+                className="custom-control-input"
+                id="xssOption3"
+                name="XssOption"
+                checked={xssOption === 3}
+                onChange={() => { adminMarkDownContainer.setState({ xssOption: 3 }) }}
               />
               />
+              <label className="custom-control-label w-100" htmlFor="xssOption3">
+                <p className="font-weight-bold">{t('admin:markdown_setting.xss_options.custom_whitelist')}</p>
+                <WhiteListInput customizable />
+              </label>
             </div>
             </div>
-          </label>
-        </div>
-
-        <div className="col-xs-4 radio radio-primary">
-          <input
-            type="radio"
-            id="xssOption3"
-            name="XssOption"
-            checked={xssOption === 3}
-            onChange={() => { adminMarkDownContainer.setState({ xssOption: 3 }) }}
-          />
-          <label htmlFor="xssOption3">
-            <p className="font-weight-bold">{t('admin:markdown_setting.xss_options.custom_whitelist')}</p>
-            <WhiteListInput />
-          </label>
+          </div>
         </div>
         </div>
-      </fieldset>
+      </div>
     );
     );
   }
   }
 
 
@@ -119,31 +132,30 @@ class XssForm extends React.Component {
 
 
     return (
     return (
       <React.Fragment>
       <React.Fragment>
-        <form className="row">
+        <fieldset className="col-12">
           <div className="form-group">
           <div className="form-group">
-            <div className="col-xs-offset-4 col-xs-4 text-left">
-              <div className="checkbox checkbox-success">
+            <div className="col-8 offset-4 my-3">
+              <div className="custom-control custom-switch custom-checkbox-success">
                 <input
                 <input
                   type="checkbox"
                   type="checkbox"
+                  className="custom-control-input"
                   id="XssEnable"
                   id="XssEnable"
-                  className="form-check-input"
                   name="isEnabledXss"
                   name="isEnabledXss"
                   checked={isEnabledXss}
                   checked={isEnabledXss}
                   onChange={adminMarkDownContainer.switchEnableXss}
                   onChange={adminMarkDownContainer.switchEnableXss}
                 />
                 />
-                <label htmlFor="XssEnable">
+                <label className="custom-control-label w-100" htmlFor="XssEnable">
                   {t('admin:markdown_setting.xss_options.enable_xss_prevention')}
                   {t('admin:markdown_setting.xss_options.enable_xss_prevention')}
                 </label>
                 </label>
               </div>
               </div>
             </div>
             </div>
-            {isEnabledXss && this.xssOptions()}
           </div>
           </div>
-          <div className="form-group my-3">
-            <div className="col-xs-offset-4 col-xs-5">
-              <div className="btn btn-primary" onClick={this.onClickSubmit} disabled={adminMarkDownContainer.state.retrieveError != null}> {t('Update')}</div>
-            </div>
+
+          <div className="col-12">
+            {isEnabledXss && this.xssOptions()}
           </div>
           </div>
-        </form>
+        </fieldset>
+        <AdminUpdateButtonRow onClick={this.onClickSubmit} disabled={adminMarkDownContainer.state.retrieveError != null} />
       </React.Fragment>
       </React.Fragment>
     );
     );
   }
   }

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

@@ -40,27 +40,27 @@ class GlobalNotification extends React.Component {
     return (
     return (
       <React.Fragment>
       <React.Fragment>
 
 
-        <h2 className="border-bottom">{t('notification_setting.valid_page')}</h2>
+        <h2 className="border-bottom my-4">{t('notification_setting.valid_page')}</h2>
 
 
-        <p className="well">
+        <p className="card well">
           {/* eslint-disable-next-line react/no-danger */}
           {/* eslint-disable-next-line react/no-danger */}
           <span dangerouslySetInnerHTML={{ __html: t('notification_setting.link_notification_help') }} />
           <span dangerouslySetInnerHTML={{ __html: t('notification_setting.link_notification_help') }} />
         </p>
         </p>
 
 
 
 
         <div className="row mb-4">
         <div className="row mb-4">
-          <div className="col-md-8 col-md-offset-2">
-            <div className="checkbox checkbox-success">
+          <div className="col-md-8 offset-md-2">
+            <div className="custom-control custom-checkbox custom-checkbox-success">
               <input
               <input
                 id="isNotificationForOwnerPageEnabled"
                 id="isNotificationForOwnerPageEnabled"
+                className="custom-control-input"
                 type="checkbox"
                 type="checkbox"
                 checked={adminNotificationContainer.state.isNotificationForOwnerPageEnabled || false}
                 checked={adminNotificationContainer.state.isNotificationForOwnerPageEnabled || false}
                 onChange={() => { adminNotificationContainer.switchIsNotificationForOwnerPageEnabled() }}
                 onChange={() => { adminNotificationContainer.switchIsNotificationForOwnerPageEnabled() }}
               />
               />
-              <label htmlFor="isNotificationForOwnerPageEnabled">
+              <label className="custom-control-label" htmlFor="isNotificationForOwnerPageEnabled">
                 {/* eslint-disable-next-line react/no-danger */}
                 {/* eslint-disable-next-line react/no-danger */}
                 <span dangerouslySetInnerHTML={{ __html: t('notification_setting.just_me_notification_help') }} />
                 <span dangerouslySetInnerHTML={{ __html: t('notification_setting.just_me_notification_help') }} />
-
               </label>
               </label>
             </div>
             </div>
           </div>
           </div>
@@ -68,15 +68,16 @@ class GlobalNotification extends React.Component {
 
 
 
 
         <div className="row mb-4">
         <div className="row mb-4">
-          <div className="col-md-8 col-md-offset-2">
-            <div className="checkbox checkbox-success">
+          <div className="col-md-8 offset-md-2">
+            <div className="custom-control custom-checkbox custom-checkbox-success">
               <input
               <input
                 id="isNotificationForGroupPageEnabled"
                 id="isNotificationForGroupPageEnabled"
+                className="custom-control-input"
                 type="checkbox"
                 type="checkbox"
                 checked={adminNotificationContainer.state.isNotificationForGroupPageEnabled || false}
                 checked={adminNotificationContainer.state.isNotificationForGroupPageEnabled || false}
                 onChange={() => { adminNotificationContainer.switchIsNotificationForGroupPageEnabled() }}
                 onChange={() => { adminNotificationContainer.switchIsNotificationForGroupPageEnabled() }}
               />
               />
-              <label htmlFor="isNotificationForGroupPageEnabled">
+              <label className="custom-control-label" htmlFor="isNotificationForGroupPageEnabled">
                 {/* eslint-disable-next-line react/no-danger */}
                 {/* eslint-disable-next-line react/no-danger */}
                 <span dangerouslySetInnerHTML={{ __html: t('notification_setting.group_notification_help') }} />
                 <span dangerouslySetInnerHTML={{ __html: t('notification_setting.group_notification_help') }} />
               </label>
               </label>
@@ -85,7 +86,7 @@ class GlobalNotification extends React.Component {
         </div>
         </div>
 
 
         <div className="row my-3">
         <div className="row my-3">
-          <div className="col-xs-offset-4 col-xs-5">
+          <div className="col-sm-5 offset-sm-4">
             <button
             <button
               type="button"
               type="button"
               className="btn btn-primary"
               className="btn btn-primary"
@@ -98,7 +99,7 @@ class GlobalNotification extends React.Component {
 
 
         <h2 className="border-bottom mb-5">{t('notification_setting.notification_list')}
         <h2 className="border-bottom mb-5">{t('notification_setting.notification_list')}
           <a href="/admin/global-notification/new">
           <a href="/admin/global-notification/new">
-            <p className="btn btn-default pull-right">{t('notification_setting.add_notification')}</p>
+            <p className="btn btn-outline-secondary pull-right">{t('notification_setting.add_notification')}</p>
           </a>
           </a>
         </h2>
         </h2>
 
 

+ 48 - 39
src/client/js/components/Admin/Notification/GlobalNotificationList.jsx

@@ -76,71 +76,80 @@ class GlobalNotificationList extends React.Component {
           return (
           return (
             <tr key={notification._id}>
             <tr key={notification._id}>
               <td className="align-middle td-abs-center">
               <td className="align-middle td-abs-center">
-                <input
-                  id="isNotificationEnabled"
-                  type="checkbox"
-                  defaultChecked={notification.isEnabled}
-                  onClick={e => this.toggleIsEnabled(notification)}
-                />
+                <div className="custom-control custom-switch custom-checkbox-success">
+                  <input
+                    type="checkbox"
+                    className="custom-control-input"
+                    id={notification._id}
+                    defaultChecked={notification.isEnabled}
+                    onClick={() => this.toggleIsEnabled(notification)}
+                  />
+                  <label className="custom-control-label" htmlFor={notification._id} />
+                </div>
               </td>
               </td>
               <td>
               <td>
                 {notification.triggerPath}
                 {notification.triggerPath}
               </td>
               </td>
               <td>
               <td>
-                {notification.triggerEvents.includes('pageCreate') && (
-                  <span className="label label-success" data-toggle="tooltip" data-placement="top" title="Page Create">
+                <ul className="list-inline">
+                  {notification.triggerEvents.includes('pageCreate') && (
+                  <li className="list-inline-item badge badge-pill badge-success" data-toggle="tooltip" data-placement="top" title="Page Create">
                     <i className="icon-doc"></i> CREATE
                     <i className="icon-doc"></i> CREATE
-                  </span>
+                  </li>
                 )}
                 )}
-                {notification.triggerEvents.includes('pageEdit') && (
-                  <span className="label label-warning" data-toggle="tooltip" data-placement="top" title="Page Edit">
+                  {notification.triggerEvents.includes('pageEdit') && (
+                  <li className="list-inline-item badge badge-pill badge-warning" data-toggle="tooltip" data-placement="top" title="Page Edit">
                     <i className="icon-pencil"></i> EDIT
                     <i className="icon-pencil"></i> EDIT
-                  </span>
+                  </li>
                 )}
                 )}
-                {notification.triggerEvents.includes('pageMove') && (
-                  <span className="label label-warning" data-toggle="tooltip" data-placement="top" title="Page Move">
+                  {notification.triggerEvents.includes('pageMove') && (
+                  <li className="list-inline-item badge badge-pill badge-pink" data-toggle="tooltip" data-placement="top" title="Page Move">
                     <i className="icon-action-redo"></i> MOVE
                     <i className="icon-action-redo"></i> MOVE
-                  </span>
+                  </li>
                 )}
                 )}
-                {notification.triggerEvents.includes('pageDelete') && (
-                  <span className="label label-danger" data-toggle="tooltip" data-placement="top" title="Page Delte">
+                  {notification.triggerEvents.includes('pageDelete') && (
+                  <li className="list-inline-item badge badge-pill badge-danger" data-toggle="tooltip" data-placement="top" title="Page Delte">
                     <i className="icon-fire"></i> DELETE
                     <i className="icon-fire"></i> DELETE
-                  </span>
+                  </li>
                 )}
                 )}
-                {notification.triggerEvents.includes('pageLike') && (
-                  <span className="label label-info" data-toggle="tooltip" data-placement="top" title="Page Like">
+                  {notification.triggerEvents.includes('pageLike') && (
+                  <li className="list-inline-item badge badge-pill badge-info" data-toggle="tooltip" data-placement="top" title="Page Like">
                     <i className="icon-like"></i> LIKE
                     <i className="icon-like"></i> LIKE
-                  </span>
+                  </li>
                 )}
                 )}
-                {notification.triggerEvents.includes('comment') && (
-                  <span className="label label-default" data-toggle="tooltip" data-placement="top" title="New Comment">
+                  {notification.triggerEvents.includes('comment') && (
+                  <li className="list-inline-item badge badge-pill badge-secondary" data-toggle="tooltip" data-placement="top" title="New Comment">
                     <i className="icon-fw icon-bubble"></i> POST
                     <i className="icon-fw icon-bubble"></i> POST
-                  </span>
+                  </li>
                 )}
                 )}
+                </ul>
               </td>
               </td>
               <td>
               <td>
                 {notification.__t === 'mail'
                 {notification.__t === 'mail'
                   && <span data-toggle="tooltip" data-placement="top" title="Email"><i className="ti-email"></i> {notification.toEmail}</span>}
                   && <span data-toggle="tooltip" data-placement="top" title="Email"><i className="ti-email"></i> {notification.toEmail}</span>}
                 {notification.__t === 'slack'
                 {notification.__t === 'slack'
-                  && <span data-toggle="tooltip" data-placement="top" title="Slack"><i className="fa fa-slack"></i> {notification.slackChannels}</span>}
+                  && <span data-toggle="tooltip" data-placement="top" title="Slack"><i className="fa fa-hashtag"></i> {notification.slackChannels}</span>}
               </td>
               </td>
               <td className="td-abs-center">
               <td className="td-abs-center">
-                <div className="btn-group admin-group-menu">
-                  <button type="button" className="btn btn-default btn-sm dropdown-toggle" data-toggle="dropdown">
+                <div className="dropdown">
+                  <button
+                    className="btn btn-outline-secondary dropdown-toggle"
+                    type="button"
+                    id="dropdownMenuButton"
+                    data-toggle="dropdown"
+                    aria-haspopup="true"
+                    aria-expanded="false"
+                  >
                     <i className="icon-settings"></i> <span className="caret"></span>
                     <i className="icon-settings"></i> <span className="caret"></span>
                   </button>
                   </button>
-                  <ul className="dropdown-menu" role="menu">
-                    <li>
-                      <a href={urljoin('/admin/global-notification/', notification._id)}>
-                        <i className="icon-fw icon-note"></i> {t('Edit')}
-                      </a>
-                    </li>
-                    <li onClick={() => this.openConfirmationModal(notification)}>
-                      <a>
-                        <i className="icon-fw icon-fire text-danger"></i> {t('Delete')}
-                      </a>
-                    </li>
-                  </ul>
+                  <div className="dropdown-menu dropdown-menu-right" aria-labelledby="dropdownMenuButton">
+                    <a className="dropdown-item" type="button" href={urljoin('/admin/global-notification/', notification._id)}>
+                      <i className="icon-fw icon-note"></i> {t('Edit')}
+                    </a>
+                    <a className="dropdown-item" type="button" onClick={() => this.openConfirmationModal(notification)}>
+                      <i className="icon-fw icon-fire text-danger"></i> {t('Delete')}
+                    </a>
+                  </div>
                 </div>
                 </div>
               </td>
               </td>
             </tr>
             </tr>

+ 143 - 106
src/client/js/components/Admin/Notification/ManageGlobalNotification.jsx

@@ -100,36 +100,40 @@ class ManageGlobalNotification extends React.Component {
     return (
     return (
       <React.Fragment>
       <React.Fragment>
 
 
-        <a href="/admin/notification#global-notification" className="btn btn-default">
-          <i className="icon-fw ti-arrow-left" aria-hidden="true"></i>
-          {t('notification_setting.back_to_list')}
-        </a>
+        <div className="my-3">
+          <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>
+
 
 
         <div className="row">
         <div className="row">
-          <div className="m-t-20 form-box col-md-12">
+          <div className="form-box col-md-12">
             <h2 className="border-bottom mb-5">{t('notification_setting.notification_detail')}</h2>
             <h2 className="border-bottom mb-5">{t('notification_setting.notification_detail')}</h2>
           </div>
           </div>
 
 
           <div className="col-sm-4">
           <div className="col-sm-4">
+            <h3 htmlFor="triggerPath">{t('notification_setting.trigger_path')}
+              {/* eslint-disable-next-line react/no-danger */}
+              <small dangerouslySetInnerHTML={{ __html: t('notification_setting.trigger_path_help', '<code>*</code>') }} />
+            </h3>
             <div className="form-group">
             <div className="form-group">
-              <h3 htmlFor="triggerPath">{t('notification_setting.trigger_path')}
-                {/* eslint-disable-next-line react/no-danger */}
-                <small dangerouslySetInnerHTML={{ __html: t('notification_setting.trigger_path_help', '<code>*</code>') }} />
-                <input
-                  className="form-control"
-                  type="text"
-                  name="triggerPath"
-                  value={this.state.triggerPath}
-                  onChange={(e) => { this.onChangeTriggerPath(e.target.value) }}
-                  required
-                />
-              </h3>
+              <input
+                className="form-control"
+                type="text"
+                name="triggerPath"
+                value={this.state.triggerPath}
+                onChange={(e) => { this.onChangeTriggerPath(e.target.value) }}
+                required
+              />
             </div>
             </div>
 
 
+            <h3>{t('notification_setting.notify_to')}</h3>
             <div className="form-group form-inline">
             <div className="form-group form-inline">
-              <h3>{t('notification_setting.notify_to')}</h3>
-              <div className="radio radio-primary">
+              <div className="custom-control custom-radio">
                 <input
                 <input
+                  className="custom-control-input"
                   type="radio"
                   type="radio"
                   id="mail"
                   id="mail"
                   name="notifyToType"
                   name="notifyToType"
@@ -137,12 +141,13 @@ class ManageGlobalNotification extends React.Component {
                   checked={this.state.notifyToType === 'mail'}
                   checked={this.state.notifyToType === 'mail'}
                   onChange={() => { this.onChangeNotifyToType('mail') }}
                   onChange={() => { this.onChangeNotifyToType('mail') }}
                 />
                 />
-                <label htmlFor="mail">
+                <label className="custom-control-label" htmlFor="mail">
                   <p className="font-weight-bold">Email</p>
                   <p className="font-weight-bold">Email</p>
                 </label>
                 </label>
               </div>
               </div>
-              <div className="radio radio-primary">
+              <div className="custom-control custom-radio ml-2">
                 <input
                 <input
+                  className="custom-control-input"
                   type="radio"
                   type="radio"
                   id="slack"
                   id="slack"
                   name="notifyToType"
                   name="notifyToType"
@@ -150,7 +155,7 @@ class ManageGlobalNotification extends React.Component {
                   checked={this.state.notifyToType === 'slack'}
                   checked={this.state.notifyToType === 'slack'}
                   onChange={() => { this.onChangeNotifyToType('slack') }}
                   onChange={() => { this.onChangeNotifyToType('slack') }}
                 />
                 />
-                <label htmlFor="slack">
+                <label className="custom-control-label" htmlFor="slack">
                   <p className="font-weight-bold">Slack</p>
                   <p className="font-weight-bold">Slack</p>
                 </label>
                 </label>
               </div>
               </div>
@@ -158,107 +163,139 @@ class ManageGlobalNotification extends React.Component {
 
 
             {this.state.notifyToType === 'mail'
             {this.state.notifyToType === 'mail'
               ? (
               ? (
-                <div className="form-group notify-to-option" id="mail-input">
-                  <input
-                    className="form-control"
-                    type="text"
-                    name="toEmail"
-                    placeholder="Email"
-                    value={this.state.emailToSend}
-                    onChange={(e) => { this.onChangeEmailToSend(e.target.value) }}
-                  />
-                  <p className="help">
+                <>
+                  <div className="input-group notify-to-option" id="mail-input">
+                    <div className="input-group-prepend">
+                      <span className="input-group-text" id="mail-addon"><i className="ti-email" /></span>
+                    </div>
+                    <input
+                      className="form-control"
+                      type="text"
+                      aria-describedby="mail-addon"
+                      name="toEmail"
+                      placeholder="Email"
+                      value={this.state.emailToSend}
+                      onChange={(e) => { this.onChangeEmailToSend(e.target.value) }}
+                    />
+
+                  </div>
+                  <p className="p-2">
                     <b>Hint: </b>
                     <b>Hint: </b>
                     <a href="https://ifttt.com/create" target="blank">{t('notification_setting.email.ifttt_link')}
                     <a href="https://ifttt.com/create" target="blank">{t('notification_setting.email.ifttt_link')}
                       <i className="icon-share-alt" />
                       <i className="icon-share-alt" />
                     </a>
                     </a>
                   </p>
                   </p>
-                </div>
+                </>
               )
               )
               : (
               : (
-                <div className="form-group notify-to-option" id="slack-input">
-                  <input
-                    className="form-control"
-                    type="text"
-                    name="notificationGlobal[slackChannels]"
-                    placeholder="Slack Channel"
-                    value={this.state.slackChannelToSend}
-                    onChange={(e) => { this.onChangeSlackChannelToSend(e.target.value) }}
-                  />
-                </div>
+                <>
+                  <div className="input-group notify-to-option" id="slack-input">
+                    <div className="input-group-prepend">
+                      <span className="input-group-text" id="slack-channel-addon"><i className="fa fa-hashtag" /></span>
+                    </div>
+                    <input
+                      className="form-control"
+                      type="text"
+                      aria-describedby="slack-channel-addon"
+                      name="notificationGlobal[slackChannels]"
+                      placeholder="Slack Channel"
+                      value={this.state.slackChannelToSend}
+                      onChange={(e) => { this.onChangeSlackChannelToSend(e.target.value) }}
+                    />
+                  </div>
+                  <p className="p-2">
+                    {/* eslint-disable-next-line react/no-danger */}
+                    <span dangerouslySetInnerHTML={{ __html: t('notification_setting.channel_desc') }} />
+                  </p>
+                </>
               )}
               )}
-
           </div>
           </div>
 
 
-
-          <div className="col-sm-offset-1 col-sm-5">
+          <div className="offset-1 col-sm-5">
             <div className="form-group">
             <div className="form-group">
               <h3>{t('notification_setting.trigger_events')}</h3>
               <h3>{t('notification_setting.trigger_events')}</h3>
-              <TriggerEventCheckBox
-                event="pageCreate"
-                checked={this.state.triggerEvents.has('pageCreate')}
-                onChange={() => this.onChangeTriggerEvents('pageCreate')}
-              >
-                <span className="label label-success">
-                  <i className="icon-doc"></i> CREATE
-                </span>
-              </TriggerEventCheckBox>
-              <TriggerEventCheckBox
-                event="pageEdit"
-                checked={this.state.triggerEvents.has('pageEdit')}
-                onChange={() => this.onChangeTriggerEvents('pageEdit')}
-              >
-                <span className="label label-warning">
-                  <i className="icon-pencil"></i>EDIT
-                </span>
-              </TriggerEventCheckBox>
-              <TriggerEventCheckBox
-                event="pageMove"
-                checked={this.state.triggerEvents.has('pageMove')}
-                onChange={() => this.onChangeTriggerEvents('pageMove')}
-              >
-                <span className="label label-warning">
-                  <i className="icon-action-redo"></i>MOVE
-                </span>
-              </TriggerEventCheckBox>
-              <TriggerEventCheckBox
-                event="pageDelete"
-                checked={this.state.triggerEvents.has('pageDelete')}
-                onChange={() => this.onChangeTriggerEvents('pageDelete')}
-              >
-                <span className="label label-danger">
-                  <i className="icon-fire"></i>DELETE
-                </span>
-              </TriggerEventCheckBox>
-              <TriggerEventCheckBox
-                event="pageLike"
-                checked={this.state.triggerEvents.has('pageLike')}
-                onChange={() => this.onChangeTriggerEvents('pageLike')}
-              >
-                <span className="label label-info">
-                  <i className="icon-like"></i>LIKE
-                </span>
-              </TriggerEventCheckBox>
-              <TriggerEventCheckBox
-                event="comment"
-                checked={this.state.triggerEvents.has('comment')}
-                onChange={() => this.onChangeTriggerEvents('comment')}
-              >
-                <span className="label label-default">
-                  <i className="icon-bubble"></i>POST
-                </span>
-              </TriggerEventCheckBox>
+              <div className="my-1">
+                <TriggerEventCheckBox
+                  checkbox="success"
+                  event="pageCreate"
+                  checked={this.state.triggerEvents.has('pageCreate')}
+                  onChange={() => this.onChangeTriggerEvents('pageCreate')}
+                >
+                  <span className="badge badge-pill badge-success">
+                    <i className="icon-doc mr-1" /> CREATE
+                  </span>
+                </TriggerEventCheckBox>
+              </div>
+              <div className="my-1">
+                <TriggerEventCheckBox
+                  checkbox="warning"
+                  event="pageEdit"
+                  checked={this.state.triggerEvents.has('pageEdit')}
+                  onChange={() => this.onChangeTriggerEvents('pageEdit')}
+                >
+                  <span className="badge badge-pill badge-warning">
+                    <i className="icon-pencil mr-1" />EDIT
+                  </span>
+                </TriggerEventCheckBox>
+              </div>
+              <div className="my-1">
+                <TriggerEventCheckBox
+                  checkbox="pink"
+                  event="pageMove"
+                  checked={this.state.triggerEvents.has('pageMove')}
+                  onChange={() => this.onChangeTriggerEvents('pageMove')}
+                >
+                  <span className="badge badge-pill badge-pink">
+                    <i className="icon-action-redo mr-1" />MOVE
+                  </span>
+                </TriggerEventCheckBox>
+              </div>
+              <div className="my-1">
+                <TriggerEventCheckBox
+                  checkbox="danger"
+                  event="pageDelete"
+                  checked={this.state.triggerEvents.has('pageDelete')}
+                  onChange={() => this.onChangeTriggerEvents('pageDelete')}
+                >
+                  <span className="badge badge-pill badge-danger">
+                    <i className="icon-fire mr-1" />DELETE
+                  </span>
+                </TriggerEventCheckBox>
+              </div>
+              <div className="my-1">
+                <TriggerEventCheckBox
+                  checkbox="info"
+                  event="pageLike"
+                  checked={this.state.triggerEvents.has('pageLike')}
+                  onChange={() => this.onChangeTriggerEvents('pageLike')}
+                >
+                  <span className="badge badge-pill badge-info">
+                    <i className="icon-like mr-1" />LIKE
+                  </span>
+                </TriggerEventCheckBox>
+              </div>
+              <div className="my-1">
+                <TriggerEventCheckBox
+                  checkbox="secondary"
+                  event="comment"
+                  checked={this.state.triggerEvents.has('comment')}
+                  onChange={() => this.onChangeTriggerEvents('comment')}
+                >
+                  <span className="badge badge-pill badge-secondary">
+                    <i className="icon-bubble mr-1" />POST
+                  </span>
+                </TriggerEventCheckBox>
+              </div>
 
 
             </div>
             </div>
           </div>
           </div>
-
-          <AdminUpdateButtonRow
-            onClick={this.submitHandler}
-            disabled={this.state.retrieveError != null}
-          />
-
         </div>
         </div>
 
 
+        <AdminUpdateButtonRow
+          onClick={this.submitHandler}
+          disabled={this.state.retrieveError != null}
+        />
+
       </React.Fragment>
       </React.Fragment>
 
 
     );
     );

+ 13 - 15
src/client/js/components/Admin/Notification/NotificationDeleteModal.jsx

@@ -2,34 +2,32 @@ import React from 'react';
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
 import { withTranslation } from 'react-i18next';
 import { withTranslation } from 'react-i18next';
 
 
-import Modal from 'react-bootstrap/es/Modal';
+import {
+  Modal, ModalHeader, ModalBody, ModalFooter,
+} from 'reactstrap';
 
 
 class NotificationDeleteModal extends React.PureComponent {
 class NotificationDeleteModal extends React.PureComponent {
 
 
   render() {
   render() {
     const { t, notificationForConfiguration } = this.props;
     const { t, notificationForConfiguration } = this.props;
     return (
     return (
-      <Modal show={this.props.isOpen} onHide={this.props.onClose}>
-        <Modal.Header className="modal-header" closeButton>
-          <Modal.Title>
-            <div className="modal-header bg-danger">
-              <i className="icon icon-fire"></i> Delete Global Notification Setting
-            </div>
-          </Modal.Title>
-        </Modal.Header>
-        <Modal.Body>
+      <Modal isOpen={this.props.isOpen} toggle={this.props.onClose}>
+        <ModalHeader tag="h4" toggle={this.props.onClose} className="bg-danger text-light">
+          <i className="icon icon-fire"></i> Delete Global Notification Setting
+        </ModalHeader>
+        <ModalBody>
           <p>
           <p>
             {t('notification_setting.delete_notification_pattern_desc1', { path: notificationForConfiguration.triggerPath })}
             {t('notification_setting.delete_notification_pattern_desc1', { path: notificationForConfiguration.triggerPath })}
           </p>
           </p>
-          <span className="text-danger">
+          <p className="text-danger">
             {t('notification_setting.delete_notification_pattern_desc2')}
             {t('notification_setting.delete_notification_pattern_desc2')}
-          </span>
-        </Modal.Body>
-        <Modal.Footer className="text-right">
+          </p>
+        </ModalBody>
+        <ModalFooter>
           <button type="button" className="btn btn-sm btn-danger" onClick={this.props.onClickSubmit}>
           <button type="button" className="btn btn-sm btn-danger" onClick={this.props.onClickSubmit}>
             <i className="icon icon-fire"></i> {t('Delete')}
             <i className="icon icon-fire"></i> {t('Delete')}
           </button>
           </button>
-        </Modal.Footer>
+        </ModalFooter>
       </Modal>
       </Modal>
     );
     );
   }
   }

+ 62 - 24
src/client/js/components/Admin/Notification/NotificationSetting.jsx

@@ -1,6 +1,9 @@
 import React from 'react';
 import React from 'react';
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
 import { withTranslation } from 'react-i18next';
 import { withTranslation } from 'react-i18next';
+import {
+  TabContent, TabPane, Nav, NavItem, NavLink,
+} from 'reactstrap';
 
 
 import loggerFactory from '@alias/logger';
 import loggerFactory from '@alias/logger';
 
 
@@ -18,6 +21,18 @@ const logger = loggerFactory('growi:NotificationSetting');
 
 
 class NotificationSetting extends React.Component {
 class NotificationSetting extends React.Component {
 
 
+  constructor() {
+    super();
+
+    this.state = {
+      activeTab: 'slack-configuration',
+      // Prevent unnecessary rendering
+      activeComponents: new Set(['slack-configuration']),
+    };
+
+    this.toggleActiveTab = this.toggleActiveTab.bind(this);
+  }
+
   async componentDidMount() {
   async componentDidMount() {
     const { adminNotificationContainer } = this.props;
     const { adminNotificationContainer } = this.props;
 
 
@@ -32,34 +47,57 @@ class NotificationSetting extends React.Component {
 
 
   }
   }
 
 
+  toggleActiveTab(activeTab) {
+    this.setState({
+      activeTab, activeComponents: this.state.activeComponents.add(activeTab),
+    });
+  }
+
   render() {
   render() {
+    const { activeTab, activeComponents } = this.state;
 
 
     return (
     return (
       <React.Fragment>
       <React.Fragment>
-        <div className="notification-settings">
-          <ul className="nav nav-tabs" role="tablist">
-            <li className="active">
-              <a href="#slack-configuration" data-toggle="tab" role="tab"><i className="icon-settings"></i> Slack configuration</a>
-            </li>
-            <li>
-              <a href="#user-trigger-notification" data-toggle="tab" role="tab"><i className="icon-settings"></i> User trigger notification</a>
-            </li>
-            <li>
-              <a href="#global-notification" data-toggle="tab" role="tab"><i className="icon-settings"></i> Global notification</a>
-            </li>
-          </ul>
-          <div className="tab-content m-t-15">
-            <div id="slack-configuration" className="tab-pane active" role="tabpanel">
-              <SlackAppConfiguration />
-            </div>
-            <div id="user-trigger-notification" className="tab-pane" role="tabpanel">
-              <UserTriggerNotification />
-            </div>
-            <div id="global-notification" className="tab-pane" role="tabpanel">
-              <GlobalNotification />
-            </div>
-          </div>
-        </div>
+        <Nav tabs>
+          <NavItem>
+            <NavLink
+              className={`${activeTab === 'slack-configuration' && 'active'} `}
+              onClick={() => { this.toggleActiveTab('slack-configuration') }}
+              href="#slack-configuration"
+            >
+              <i className="icon-settings"></i> Slack configuration
+            </NavLink>
+          </NavItem>
+          <NavItem>
+            <NavLink
+              className={`${activeTab === 'user-trigger-notification' && 'active'} `}
+              onClick={() => { this.toggleActiveTab('user-trigger-notification') }}
+              href="#user-trigger-notification"
+            >
+              <i className="icon-settings"></i> User trigger notification
+            </NavLink>
+          </NavItem>
+          <NavItem>
+            <NavLink
+              className={`${activeTab === 'global-notification' && 'active'} `}
+              onClick={() => { this.toggleActiveTab('global-notification') }}
+              href="#global-notification"
+            >
+              <i className="icon-settings"></i> Global notification
+            </NavLink>
+          </NavItem>
+        </Nav>
+        <TabContent activeTab={activeTab}>
+          <TabPane tabId="slack-configuration">
+            {activeComponents.has('slack-configuration') && <SlackAppConfiguration />}
+          </TabPane>
+          <TabPane tabId="user-trigger-notification">
+            {activeComponents.has('user-trigger-notification') && <UserTriggerNotification />}
+          </TabPane>
+          <TabPane tabId="global-notification">
+            {activeComponents.has('global-notification') && <GlobalNotification />}
+          </TabPane>
+        </TabContent>
       </React.Fragment>
       </React.Fragment>
     );
     );
   }
   }

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

@@ -39,25 +39,24 @@ class SlackAppConfiguration extends React.Component {
 
 
     return (
     return (
       <React.Fragment>
       <React.Fragment>
-        <div className="row mb-5">
-          <div className="col-xs-6 text-left">
-            <div className="my-0 btn-group">
-              <div className="dropdown">
-                <button className="btn btn-default dropdown-toggle" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
-                  <span className="pull-left">Slack {adminNotificationContainer.state.selectSlackOption} </span>
-                  <span className="bs-caret pull-right">
-                    <span className="caret" />
-                  </span>
-                </button>
-                {/* TODO adjust dropdown after BS4 */}
-                <ul className="dropdown-menu" role="menu">
-                  <li type="button" onClick={() => adminNotificationContainer.switchSlackOption('Incoming Webhooks')}>
-                    <a role="menuitem">Slack Incoming Webhooks</a>
-                  </li>
-                  <li type="button" onClick={() => adminNotificationContainer.switchSlackOption('App')}>
-                    <a role="menuitem">Slack App</a>
-                  </li>
-                </ul>
+        <div className="row my-3">
+          <div className="col-6 text-left">
+            <div className="dropdown">
+              <button
+                className="btn btn-secondary dropdown-toggle"
+                type="button"
+                id="dropdownMenuButton"
+                data-toggle="dropdown"
+                aria-haspopup="true"
+                aria-expanded="true"
+              >
+                {`Slack ${adminNotificationContainer.state.selectSlackOption}`}
+              </button>
+              <div className="dropdown-menu" aria-labelledby="dropdownMenuButton">
+                <a className="dropdown-item" type="button" onClick={() => adminNotificationContainer.switchSlackOption('Incoming Webhooks')}>
+                  Slack Incoming Webhooks
+                </a>
+                <a className="dropdown-item" type="button" onClick={() => adminNotificationContainer.switchSlackOption('App')}>Slack App</a>
               </div>
               </div>
             </div>
             </div>
           </div>
           </div>
@@ -66,9 +65,9 @@ class SlackAppConfiguration extends React.Component {
           <React.Fragment>
           <React.Fragment>
             <h2 className="border-bottom mb-5">{t('notification_setting.slack_incoming_configuration')}</h2>
             <h2 className="border-bottom mb-5">{t('notification_setting.slack_incoming_configuration')}</h2>
 
 
-            <div className="row mb-5">
-              <label className="col-xs-3 text-right">Webhook URL</label>
-              <div className="col-xs-6">
+            <div className="row mb-3">
+              <label className="col-md-3 text-left text-md-right">Webhook URL</label>
+              <div className="col-md-6">
                 <input
                 <input
                   className="form-control"
                   className="form-control"
                   type="text"
                   type="text"
@@ -78,20 +77,21 @@ class SlackAppConfiguration extends React.Component {
               </div>
               </div>
             </div>
             </div>
 
 
-            <div className="row mb-5">
-              <div className="col-xs-offset-3 col-xs-6 text-left">
-                <div className="checkbox checkbox-success">
+            <div className="row mb-3">
+              <div className="offset-md-3 col-md-6 text-left">
+                <div className="custom-control custom-checkbox custom-checkbox-success">
                   <input
                   <input
-                    id="cbPrioritizeIWH"
                     type="checkbox"
                     type="checkbox"
+                    className="custom-control-input"
+                    id="cbPrioritizeIWH"
                     checked={adminNotificationContainer.state.isIncomingWebhookPrioritized || false}
                     checked={adminNotificationContainer.state.isIncomingWebhookPrioritized || false}
                     onChange={() => { adminNotificationContainer.switchIsIncomingWebhookPrioritized() }}
                     onChange={() => { adminNotificationContainer.switchIsIncomingWebhookPrioritized() }}
                   />
                   />
-                  <label htmlFor="cbPrioritizeIWH">
+                  <label className="custom-control-label" htmlFor="cbPrioritizeIWH">
                     {t('notification_setting.prioritize_webhook')}
                     {t('notification_setting.prioritize_webhook')}
                   </label>
                   </label>
                 </div>
                 </div>
-                <p className="help-block">
+                <p className="form-text text-muted">
                   {t('notification_setting.prioritize_webhook_desc')}
                   {t('notification_setting.prioritize_webhook_desc')}
                 </p>
                 </p>
               </div>
               </div>
@@ -102,24 +102,24 @@ class SlackAppConfiguration extends React.Component {
             <React.Fragment>
             <React.Fragment>
               <h2 className="border-bottom mb-5">{t('notification_setting.slack_app_configuration')}</h2>
               <h2 className="border-bottom mb-5">{t('notification_setting.slack_app_configuration')}</h2>
 
 
-              <div className="well">
-                <i className="icon-fw icon-exclamation text-danger"></i><span className="text-danger">NOT RECOMMENDED</span>
-                <br /><br />
+              <div className="well card">
+                <span className="text-danger"><i className="icon-fw icon-exclamation"></i>NOT RECOMMENDED</span>
+                <br />
                 {/* eslint-disable-next-line react/no-danger */}
                 {/* eslint-disable-next-line react/no-danger */}
                 <span dangerouslySetInnerHTML={{ __html: t('notification_setting.slack_app_configuration_desc') }} />
                 <span dangerouslySetInnerHTML={{ __html: t('notification_setting.slack_app_configuration_desc') }} />
-                <br /><br />
+                <br />
                 <a
                 <a
                   href="#slack-incoming-webhooks"
                   href="#slack-incoming-webhooks"
                   data-toggle="tab"
                   data-toggle="tab"
                   onClick={() => adminNotificationContainer.switchSlackOption('Incoming Webhooks')}
                   onClick={() => adminNotificationContainer.switchSlackOption('Incoming Webhooks')}
                 >
                 >
                   {t('notification_setting.use_instead')}
                   {t('notification_setting.use_instead')}
-                </a>{' '}
+                </a>
               </div>
               </div>
 
 
               <div className="row mb-5">
               <div className="row mb-5">
-                <label className="col-xs-3 text-right">OAuth access token</label>
-                <div className="col-xs-6">
+                <label className="col-md-3 text-left text-md-right">OAuth access token</label>
+                <div className="col-md-6">
                   <input
                   <input
                     className="form-control"
                     className="form-control"
                     type="text"
                     type="text"

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

@@ -6,15 +6,15 @@ const TriggerEventCheckBox = (props) => {
   const { t } = props;
   const { t } = props;
 
 
   return (
   return (
-    <div className="checkbox checkbox-inverse">
+    <div className={`custom-control custom-checkbox custom-checkbox-${props.checkbox}`}>
       <input
       <input
+        className="custom-control-input"
         type="checkbox"
         type="checkbox"
         id={`trigger-event-${props.event}`}
         id={`trigger-event-${props.event}`}
-        value={props.event}
         checked={props.checked}
         checked={props.checked}
         onChange={props.onChange}
         onChange={props.onChange}
       />
       />
-      <label htmlFor={`trigger-event-${props.event}`}>
+      <label className="custom-control-label" htmlFor={`trigger-event-${props.event}`}>
         {props.children}{' '}
         {props.children}{' '}
         {t(`notification_setting.event_${props.event}`)}
         {t(`notification_setting.event_${props.event}`)}
       </label>
       </label>
@@ -26,6 +26,7 @@ const TriggerEventCheckBox = (props) => {
 TriggerEventCheckBox.propTypes = {
 TriggerEventCheckBox.propTypes = {
   t: PropTypes.func.isRequired, // i18next
   t: PropTypes.func.isRequired, // i18next
 
 
+  checkbox: PropTypes.string.isRequired,
   checked: PropTypes.bool.isRequired,
   checked: PropTypes.bool.isRequired,
   onChange: PropTypes.func.isRequired,
   onChange: PropTypes.func.isRequired,
   event: PropTypes.string.isRequired,
   event: PropTypes.string.isRequired,

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

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

+ 22 - 14
src/client/js/components/Admin/Notification/UserTriggerNotification.jsx

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

+ 13 - 13
src/client/js/components/Admin/Security/BasicSecuritySetting.jsx

@@ -67,57 +67,57 @@ class BasicSecurityManagement extends React.Component {
         </div>
         </div>
         )}
         )}
 
 
-        <div className="row mb-5">
-          <div className="col-xs-3 my-3 text-right">
-            <strong>{t('security_setting.Basic.name')}</strong>
-          </div>
-          <div className="col-xs-6 text-left">
-            <div className="checkbox checkbox-success">
+        <div className="form-group row">
+          <div className="col-6 offset-3">
+            <div className="custom-control custom-switch custom-checkbox-success">
               <input
               <input
                 id="isBasicEnabled"
                 id="isBasicEnabled"
+                className="custom-control-input"
                 type="checkbox"
                 type="checkbox"
                 checked={adminGeneralSecurityContainer.state.isBasicEnabled}
                 checked={adminGeneralSecurityContainer.state.isBasicEnabled}
                 onChange={() => { adminGeneralSecurityContainer.switchIsBasicEnabled() }}
                 onChange={() => { adminGeneralSecurityContainer.switchIsBasicEnabled() }}
               />
               />
-              <label htmlFor="isBasicEnabled">
+              <label className="custom-control-label" htmlFor="isBasicEnabled">
                 { t('security_setting.Basic.enable_basic') }
                 { t('security_setting.Basic.enable_basic') }
               </label>
               </label>
             </div>
             </div>
-            <p className="help-block">
+            <p className="form-text text-muted">
               <small>
               <small>
                 <span dangerouslySetInnerHTML={{ __html: t('security_setting.Basic.desc_1') }} /><br />
                 <span dangerouslySetInnerHTML={{ __html: t('security_setting.Basic.desc_1') }} /><br />
                 { t('security_setting.Basic.desc_2')}
                 { t('security_setting.Basic.desc_2')}
               </small>
               </small>
             </p>
             </p>
             {(!adminGeneralSecurityContainer.state.setupStrategies.includes('basic') && isBasicEnabled)
             {(!adminGeneralSecurityContainer.state.setupStrategies.includes('basic') && isBasicEnabled)
-            && <div className="label label-warning">{t('security_setting.setup_is_not_yet_complete')}</div>}
+            && <div className="badge badge-warning">{t('security_setting.setup_is_not_yet_complete')}</div>}
           </div>
           </div>
         </div>
         </div>
 
 
         {isBasicEnabled && (
         {isBasicEnabled && (
         <React.Fragment>
         <React.Fragment>
           <div className="row mb-5">
           <div className="row mb-5">
-            <div className="col-xs-offset-3 col-xs-6 text-left">
-              <div className="checkbox checkbox-success">
+            <div className="offset-md-3 col-md-6">
+              <div className="custom-control custom-checkbox custom-checkbox-success">
                 <input
                 <input
                   id="bindByEmail-basic"
                   id="bindByEmail-basic"
+                  className="custom-control-input"
                   type="checkbox"
                   type="checkbox"
                   checked={adminBasicSecurityContainer.state.isSameUsernameTreatedAsIdenticalUser || false}
                   checked={adminBasicSecurityContainer.state.isSameUsernameTreatedAsIdenticalUser || false}
                   onChange={() => { adminBasicSecurityContainer.switchIsSameUsernameTreatedAsIdenticalUser() }}
                   onChange={() => { adminBasicSecurityContainer.switchIsSameUsernameTreatedAsIdenticalUser() }}
                 />
                 />
                 <label
                 <label
+                  className="custom-control-label"
                   htmlFor="bindByEmail-basic"
                   htmlFor="bindByEmail-basic"
                   dangerouslySetInnerHTML={{ __html: t('security_setting.Treat username matching as identical', 'username') }}
                   dangerouslySetInnerHTML={{ __html: t('security_setting.Treat username matching as identical', 'username') }}
                 />
                 />
               </div>
               </div>
-              <p className="help-block">
+              <p className="form-text text-muted">
                 <small dangerouslySetInnerHTML={{ __html: t('security_setting.Treat username matching as identical_warn', 'username') }} />
                 <small dangerouslySetInnerHTML={{ __html: t('security_setting.Treat username matching as identical_warn', 'username') }} />
               </p>
               </p>
             </div>
             </div>
           </div>
           </div>
 
 
           <div className="row my-3">
           <div className="row my-3">
-            <div className="col-xs-offset-4 col-xs-5">
+            <div className="offset-4 col-5">
               <button type="button" className="btn btn-primary" disabled={adminBasicSecurityContainer.state.retrieveError != null} onClick={this.onClickSubmit}>
               <button type="button" className="btn btn-primary" disabled={adminBasicSecurityContainer.state.retrieveError != null} onClick={this.onClickSubmit}>
                 {t('Update')}
                 {t('Update')}
               </button>
               </button>

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

@@ -68,43 +68,41 @@ class GitHubSecurityManagement extends React.Component {
           </div>
           </div>
         )}
         )}
 
 
-        <div className="row mb-5">
-          <div className="col-xs-3 my-3 text-right">
-            <strong>{t('security_setting.OAuth.GitHub.name')}</strong>
-          </div>
-          <div className="col-xs-6 text-left">
-            <div className="checkbox checkbox-success">
+        <div className="form-group row">
+          <div className="col-6 offset-3">
+            <div className="custom-control custom-switch custom-checkbox-success">
               <input
               <input
                 id="isGitHubEnabled"
                 id="isGitHubEnabled"
+                className="custom-control-input"
                 type="checkbox"
                 type="checkbox"
                 checked={adminGeneralSecurityContainer.state.isGitHubEnabled || false}
                 checked={adminGeneralSecurityContainer.state.isGitHubEnabled || false}
                 onChange={() => { adminGeneralSecurityContainer.switchIsGitHubOAuthEnabled() }}
                 onChange={() => { adminGeneralSecurityContainer.switchIsGitHubOAuthEnabled() }}
               />
               />
-              <label htmlFor="isGitHubEnabled">
+              <label className="custom-control-label" htmlFor="isGitHubEnabled">
                 {t('security_setting.OAuth.GitHub.enable_github')}
                 {t('security_setting.OAuth.GitHub.enable_github')}
               </label>
               </label>
             </div>
             </div>
             {(!adminGeneralSecurityContainer.state.setupStrategies.includes('github') && isGitHubEnabled)
             {(!adminGeneralSecurityContainer.state.setupStrategies.includes('github') && isGitHubEnabled)
-              && <div className="label label-warning">{t('security_setting.setup_is_not_yet_complete')}</div>}
+              && <div className="badge badge-warning">{t('security_setting.setup_is_not_yet_complete')}</div>}
           </div>
           </div>
         </div>
         </div>
 
 
         <div className="row mb-5">
         <div className="row mb-5">
-          <label className="col-xs-3 text-right">{t('security_setting.callback_URL')}</label>
-          <div className="col-xs-6">
+          <label className="col-12 col-md-3 text-left text-md-right py-2">{t('security_setting.callback_URL')}</label>
+          <div className="col-12 col-md-6">
             <input
             <input
               className="form-control"
               className="form-control"
               type="text"
               type="text"
               value={adminGitHubSecurityContainer.state.appSiteUrl}
               value={adminGitHubSecurityContainer.state.appSiteUrl}
               readOnly
               readOnly
             />
             />
-            <p className="help-block small">{t('security_setting.desc_of_callback_URL', { AuthName: 'OAuth' })}</p>
+            <p className="form-text text-muted small">{t('security_setting.desc_of_callback_URL', { AuthName: 'OAuth' })}</p>
             {!adminGeneralSecurityContainer.state.appSiteUrl && (
             {!adminGeneralSecurityContainer.state.appSiteUrl && (
               <div className="alert alert-danger">
               <div className="alert alert-danger">
                 <i
                 <i
                   className="icon-exclamation"
                   className="icon-exclamation"
                   // eslint-disable-next-line max-len
                   // eslint-disable-next-line max-len
-                  dangerouslySetInnerHTML={{ __html: t('security_setting.alert_siteUrl_is_not_set', { link: `<a href="/admin/app">${t('App settings')}<i class="icon-login"></i></a>` }) }}
+                  dangerouslySetInnerHTML={{ __html: t('security_setting.alert_siteUrl_is_not_set', { link: `<a href="/admin/app">${t('App Settings')}<i class="icon-login"></i></a>` }) }}
                 />
                 />
               </div>
               </div>
             )}
             )}
@@ -118,8 +116,8 @@ class GitHubSecurityManagement extends React.Component {
             <h3 className="border-bottom">{t('security_setting.configuration')}</h3>
             <h3 className="border-bottom">{t('security_setting.configuration')}</h3>
 
 
             <div className="row mb-5">
             <div className="row mb-5">
-              <label htmlFor="githubClientId" className="col-xs-3 text-right">{t('security_setting.clientID')}</label>
-              <div className="col-xs-6">
+              <label htmlFor="githubClientId" className="col-3 text-right py-2">{t('security_setting.clientID')}</label>
+              <div className="col-6">
                 <input
                 <input
                   className="form-control"
                   className="form-control"
                   type="text"
                   type="text"
@@ -127,15 +125,15 @@ class GitHubSecurityManagement extends React.Component {
                   value={adminGitHubSecurityContainer.state.githubClientId || ''}
                   value={adminGitHubSecurityContainer.state.githubClientId || ''}
                   onChange={e => adminGitHubSecurityContainer.changeGitHubClientId(e.target.value)}
                   onChange={e => adminGitHubSecurityContainer.changeGitHubClientId(e.target.value)}
                 />
                 />
-                <p className="help-block">
+                <p className="form-text text-muted">
                   <small dangerouslySetInnerHTML={{ __html: t('security_setting.Use env var if empty', { env: 'OAUTH_GITHUB_CLIENT_ID' }) }} />
                   <small dangerouslySetInnerHTML={{ __html: t('security_setting.Use env var if empty', { env: 'OAUTH_GITHUB_CLIENT_ID' }) }} />
                 </p>
                 </p>
               </div>
               </div>
             </div>
             </div>
 
 
             <div className="row mb-5">
             <div className="row mb-5">
-              <label htmlFor="githubClientSecret" className="col-xs-3 text-right">{t('security_setting.client_secret')}</label>
-              <div className="col-xs-6">
+              <label htmlFor="githubClientSecret" className="col-3 text-right py-2">{t('security_setting.client_secret')}</label>
+              <div className="col-6">
                 <input
                 <input
                   className="form-control"
                   className="form-control"
                   type="text"
                   type="text"
@@ -143,34 +141,36 @@ class GitHubSecurityManagement extends React.Component {
                   defaultValue={adminGitHubSecurityContainer.state.githubClientSecret || ''}
                   defaultValue={adminGitHubSecurityContainer.state.githubClientSecret || ''}
                   onChange={e => adminGitHubSecurityContainer.changeGitHubClientSecret(e.target.value)}
                   onChange={e => adminGitHubSecurityContainer.changeGitHubClientSecret(e.target.value)}
                 />
                 />
-                <p className="help-block">
+                <p className="form-text text-muted">
                   <small dangerouslySetInnerHTML={{ __html: t('security_setting.Use env var if empty', { env: 'OAUTH_GITHUB_CLIENT_SECRET' }) }} />
                   <small dangerouslySetInnerHTML={{ __html: t('security_setting.Use env var if empty', { env: 'OAUTH_GITHUB_CLIENT_SECRET' }) }} />
                 </p>
                 </p>
               </div>
               </div>
             </div>
             </div>
 
 
             <div className="row mb-5">
             <div className="row mb-5">
-              <div className="col-xs-offset-3 col-xs-6 text-left">
-                <div className="checkbox checkbox-success">
+              <div className="offset-3 col-6 text-left">
+                <div className="custom-control custom-checkbox custom-checkbox-success">
                   <input
                   <input
                     id="bindByUserNameGitHub"
                     id="bindByUserNameGitHub"
+                    className="custom-control-input"
                     type="checkbox"
                     type="checkbox"
                     checked={adminGitHubSecurityContainer.state.isSameUsernameTreatedAsIdenticalUser || false}
                     checked={adminGitHubSecurityContainer.state.isSameUsernameTreatedAsIdenticalUser || false}
                     onChange={() => { adminGitHubSecurityContainer.switchIsSameUsernameTreatedAsIdenticalUser() }}
                     onChange={() => { adminGitHubSecurityContainer.switchIsSameUsernameTreatedAsIdenticalUser() }}
                   />
                   />
                   <label
                   <label
+                    className="custom-control-label"
                     htmlFor="bindByUserNameGitHub"
                     htmlFor="bindByUserNameGitHub"
                     dangerouslySetInnerHTML={{ __html: t('security_setting.Treat email matching as identical') }}
                     dangerouslySetInnerHTML={{ __html: t('security_setting.Treat email matching as identical') }}
                   />
                   />
                 </div>
                 </div>
-                <p className="help-block">
+                <p className="form-text text-muted">
                   <small dangerouslySetInnerHTML={{ __html: t('security_setting.Treat email matching as identical_warn') }} />
                   <small dangerouslySetInnerHTML={{ __html: t('security_setting.Treat email matching as identical_warn') }} />
                 </p>
                 </p>
               </div>
               </div>
             </div>
             </div>
 
 
             <div className="row my-3">
             <div className="row my-3">
-              <div className="col-xs-offset-3 col-xs-5">
+              <div className="offset-3 col-5">
                 <div className="btn btn-primary" disabled={adminGitHubSecurityContainer.state.retrieveError != null} onClick={this.onClickSubmit}>
                 <div className="btn btn-primary" disabled={adminGitHubSecurityContainer.state.retrieveError != null} onClick={this.onClickSubmit}>
                   {t('Update')}
                   {t('Update')}
                 </div>
                 </div>

+ 22 - 22
src/client/js/components/Admin/Security/GoogleSecuritySetting.jsx

@@ -68,43 +68,41 @@ class GoogleSecurityManagement extends React.Component {
           </div>
           </div>
         )}
         )}
 
 
-        <div className="row mb-5">
-          <div className="col-xs-3 my-3 text-right">
-            <strong>{t('security_setting.OAuth.Google.name')}</strong>
-          </div>
-          <div className="col-xs-6 text-left">
-            <div className="checkbox checkbox-success">
+        <div className="form-group row">
+          <div className="col-6 offset-3">
+            <div className="custom-control custom-switch custom-checkbox-success">
               <input
               <input
                 id="isGoogleEnabled"
                 id="isGoogleEnabled"
+                className="custom-control-input"
                 type="checkbox"
                 type="checkbox"
                 checked={adminGeneralSecurityContainer.state.isGoogleEnabled || false}
                 checked={adminGeneralSecurityContainer.state.isGoogleEnabled || false}
                 onChange={() => { adminGeneralSecurityContainer.switchIsGoogleOAuthEnabled() }}
                 onChange={() => { adminGeneralSecurityContainer.switchIsGoogleOAuthEnabled() }}
               />
               />
-              <label htmlFor="isGoogleEnabled">
+              <label className="custom-control-label" htmlFor="isGoogleEnabled">
                 {t('security_setting.OAuth.Google.enable_google')}
                 {t('security_setting.OAuth.Google.enable_google')}
               </label>
               </label>
             </div>
             </div>
             {(!adminGeneralSecurityContainer.state.setupStrategies.includes('google') && isGoogleEnabled)
             {(!adminGeneralSecurityContainer.state.setupStrategies.includes('google') && isGoogleEnabled)
-              && <div className="label label-warning">{t('security_setting.setup_is_not_yet_complete')}</div>}
+              && <div className="badge badge-warning">{t('security_setting.setup_is_not_yet_complete')}</div>}
           </div>
           </div>
         </div>
         </div>
 
 
         <div className="row mb-5">
         <div className="row mb-5">
-          <label className="col-xs-3 text-right">{t('security_setting.callback_URL')}</label>
-          <div className="col-xs-6">
+          <label className="col-12 col-md-3 text-left text-md-right py-2">{t('security_setting.callback_URL')}</label>
+          <div className="col-12 col-md-6">
             <input
             <input
               className="form-control"
               className="form-control"
               type="text"
               type="text"
               value={adminGoogleSecurityContainer.state.callbackUrl}
               value={adminGoogleSecurityContainer.state.callbackUrl}
               readOnly
               readOnly
             />
             />
-            <p className="help-block small">{t('security_setting.desc_of_callback_URL', { AuthName: 'OAuth' })}</p>
+            <p className="form-text text-muted small">{t('security_setting.desc_of_callback_URL', { AuthName: 'OAuth' })}</p>
             {!adminGeneralSecurityContainer.state.appSiteUrl && (
             {!adminGeneralSecurityContainer.state.appSiteUrl && (
               <div className="alert alert-danger">
               <div className="alert alert-danger">
                 <i
                 <i
                   className="icon-exclamation"
                   className="icon-exclamation"
                   // eslint-disable-next-line max-len
                   // eslint-disable-next-line max-len
-                  dangerouslySetInnerHTML={{ __html: t('security_setting.alert_siteUrl_is_not_set', { link: `<a href="/admin/app">${t('App settings')}<i class="icon-login"></i></a>` }) }}
+                  dangerouslySetInnerHTML={{ __html: t('security_setting.alert_siteUrl_is_not_set', { link: `<a href="/admin/app">${t('App Settings')}<i class="icon-login"></i></a>` }) }}
                 />
                 />
               </div>
               </div>
             )}
             )}
@@ -118,8 +116,8 @@ class GoogleSecurityManagement extends React.Component {
             <h3 className="border-bottom">{t('security_setting.configuration')}</h3>
             <h3 className="border-bottom">{t('security_setting.configuration')}</h3>
 
 
             <div className="row mb-5">
             <div className="row mb-5">
-              <label htmlFor="googleClientId" className="col-xs-3 text-right">{t('security_setting.clientID')}</label>
-              <div className="col-xs-6">
+              <label htmlFor="googleClientId" className="col-3 text-right py-2">{t('security_setting.clientID')}</label>
+              <div className="col-6">
                 <input
                 <input
                   className="form-control"
                   className="form-control"
                   type="text"
                   type="text"
@@ -127,15 +125,15 @@ class GoogleSecurityManagement extends React.Component {
                   defaultValue={adminGoogleSecurityContainer.state.googleClientId || ''}
                   defaultValue={adminGoogleSecurityContainer.state.googleClientId || ''}
                   onChange={e => adminGoogleSecurityContainer.changeGoogleClientId(e.target.value)}
                   onChange={e => adminGoogleSecurityContainer.changeGoogleClientId(e.target.value)}
                 />
                 />
-                <p className="help-block">
+                <p className="form-text text-muted">
                   <small dangerouslySetInnerHTML={{ __html: t('security_setting.Use env var if empty', { env: 'OAUTH_GOOGLE_CLIENT_ID' }) }} />
                   <small dangerouslySetInnerHTML={{ __html: t('security_setting.Use env var if empty', { env: 'OAUTH_GOOGLE_CLIENT_ID' }) }} />
                 </p>
                 </p>
               </div>
               </div>
             </div>
             </div>
 
 
             <div className="row mb-5">
             <div className="row mb-5">
-              <label htmlFor="googleClientSecret" className="col-xs-3 text-right">{t('security_setting.client_secret')}</label>
-              <div className="col-xs-6">
+              <label htmlFor="googleClientSecret" className="col-3 text-right py-2">{t('security_setting.client_secret')}</label>
+              <div className="col-6">
                 <input
                 <input
                   className="form-control"
                   className="form-control"
                   type="text"
                   type="text"
@@ -143,34 +141,36 @@ class GoogleSecurityManagement extends React.Component {
                   defaultValue={adminGoogleSecurityContainer.state.googleClientSecret || ''}
                   defaultValue={adminGoogleSecurityContainer.state.googleClientSecret || ''}
                   onChange={e => adminGoogleSecurityContainer.changeGoogleClientSecret(e.target.value)}
                   onChange={e => adminGoogleSecurityContainer.changeGoogleClientSecret(e.target.value)}
                 />
                 />
-                <p className="help-block">
+                <p className="form-text text-muted">
                   <small dangerouslySetInnerHTML={{ __html: t('security_setting.Use env var if empty', { env: 'OAUTH_GOOGLE_CLIENT_SECRET' }) }} />
                   <small dangerouslySetInnerHTML={{ __html: t('security_setting.Use env var if empty', { env: 'OAUTH_GOOGLE_CLIENT_SECRET' }) }} />
                 </p>
                 </p>
               </div>
               </div>
             </div>
             </div>
 
 
             <div className="row mb-5">
             <div className="row mb-5">
-              <div className="col-xs-offset-3 col-xs-6 text-left">
-                <div className="checkbox checkbox-success">
+              <div className="offset-3 col-6">
+                <div className="custom-control custom-checkbox custom-checkbox-success">
                   <input
                   <input
                     id="bindByUserNameGoogle"
                     id="bindByUserNameGoogle"
+                    className="custom-control-input"
                     type="checkbox"
                     type="checkbox"
                     checked={adminGoogleSecurityContainer.state.isSameUsernameTreatedAsIdenticalUser || false}
                     checked={adminGoogleSecurityContainer.state.isSameUsernameTreatedAsIdenticalUser || false}
                     onChange={() => { adminGoogleSecurityContainer.switchIsSameUsernameTreatedAsIdenticalUser() }}
                     onChange={() => { adminGoogleSecurityContainer.switchIsSameUsernameTreatedAsIdenticalUser() }}
                   />
                   />
                   <label
                   <label
+                    className="custom-control-label"
                     htmlFor="bindByUserNameGoogle"
                     htmlFor="bindByUserNameGoogle"
                     dangerouslySetInnerHTML={{ __html: t('security_setting.Treat email matching as identical') }}
                     dangerouslySetInnerHTML={{ __html: t('security_setting.Treat email matching as identical') }}
                   />
                   />
                 </div>
                 </div>
-                <p className="help-block">
+                <p className="form-text text-muted">
                   <small dangerouslySetInnerHTML={{ __html: t('security_setting.Treat email matching as identical_warn') }} />
                   <small dangerouslySetInnerHTML={{ __html: t('security_setting.Treat email matching as identical_warn') }} />
                 </p>
                 </p>
               </div>
               </div>
             </div>
             </div>
 
 
             <div className="row my-3">
             <div className="row my-3">
-              <div className="col-xs-offset-3 col-xs-5">
+              <div className="offset-3 col-5">
                 <button
                 <button
                   type="button"
                   type="button"
                   className="btn btn-primary"
                   className="btn btn-primary"

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

@@ -88,9 +88,9 @@ class LdapAuthTest extends React.Component {
       <React.Fragment>
       <React.Fragment>
         {this.state.successMessage != null && <div className="alert alert-success">{this.state.successMessage}</div>}
         {this.state.successMessage != null && <div className="alert alert-success">{this.state.successMessage}</div>}
         {this.state.errorMessage != null && <div className="alert alert-warning">{this.state.errorMessage}</div>}
         {this.state.errorMessage != null && <div className="alert alert-warning">{this.state.errorMessage}</div>}
-        <div className="row p-3">
-          <label htmlFor="username" className="col-xs-3 text-right">{t('username')}</label>
-          <div className="col-xs-6">
+        <div className="form-group row">
+          <label htmlFor="username" className="col-3 col-form-label">{t('username')}</label>
+          <div className="col-6">
             <input
             <input
               className="form-control"
               className="form-control"
               name="username"
               name="username"
@@ -99,9 +99,9 @@ class LdapAuthTest extends React.Component {
             />
             />
           </div>
           </div>
         </div>
         </div>
-        <div className="row p-3">
-          <label htmlFor="password" className="col-xs-3 text-right">{t('Password')}</label>
-          <div className="col-xs-6">
+        <div className="form-group row">
+          <label htmlFor="password" className="col-3 col-form-label">{t('Password')}</label>
+          <div className="col-6">
             <input
             <input
               className="form-control"
               className="form-control"
               type="password"
               type="password"
@@ -111,13 +111,15 @@ class LdapAuthTest extends React.Component {
             />
             />
           </div>
           </div>
         </div>
         </div>
-        <div>
-          <h5>Logs</h5>
-          <textarea id="taLogs" className="col-xs-12" rows="4" value={this.state.logs} readOnly />
-        </div>
 
 
-        <button type="button" className="btn btn-default mt-3 col-xs-offset-5 col-xs-2" onClick={this.testLdapCredentials}>Test</button>
+        <div className="form-group">
+          <label><h5>Logs</h5></label>
+          <textarea id="taLogs" className="col" rows="4" value={this.state.logs} readOnly />
+        </div>
 
 
+        <div>
+          <button type="button" className="btn btn-outline-secondary offset-5 col-2" onClick={this.testLdapCredentials}>Test</button>
+        </div>
       </React.Fragment>
       </React.Fragment>
 
 
     );
     );

+ 11 - 9
src/client/js/components/Admin/Security/LdapAuthTestModal.jsx

@@ -2,7 +2,11 @@ import React from 'react';
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
 import { withTranslation } from 'react-i18next';
 import { withTranslation } from 'react-i18next';
 
 
-import Modal from 'react-bootstrap/es/Modal';
+import {
+  Modal,
+  ModalHeader,
+  ModalBody,
+} from 'reactstrap';
 
 
 import { createSubscribedElement } from '../../UnstatedUtils';
 import { createSubscribedElement } from '../../UnstatedUtils';
 
 
@@ -42,20 +46,18 @@ class LdapAuthTestModal extends React.Component {
   render() {
   render() {
 
 
     return (
     return (
-      <Modal show={this.props.isOpen} onHide={this.props.onClose}>
-        <Modal.Header className="bg-info modal-header" closeButton>
-          <Modal.Title className="text-white">
-            Test LDAP Account
-          </Modal.Title>
-        </Modal.Header>
-        <Modal.Body>
+      <Modal isOpen={this.props.isOpen} toggle={this.props.onClose}>
+        <ModalHeader tag="h4" toggle={this.props.onClose} className="bg-info text-light">
+          Test LDAP Account
+        </ModalHeader>
+        <ModalBody>
           <LdapAuthTest
           <LdapAuthTest
             username={this.state.username}
             username={this.state.username}
             password={this.state.password}
             password={this.state.password}
             onChangeUsername={this.onChangeUsername}
             onChangeUsername={this.onChangeUsername}
             onChangePassword={this.onChangePassword}
             onChangePassword={this.onChangePassword}
           />
           />
-        </Modal.Body>
+        </ModalBody>
       </Modal>
       </Modal>
     );
     );
   }
   }

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

@@ -73,24 +73,22 @@ class LdapSecuritySetting extends React.Component {
           LDAP
           LDAP
         </h2>
         </h2>
 
 
-        <div className="row mb-5">
-          <div className="col-xs-3 my-3 text-right">
-            <strong>Use LDAP</strong>
-          </div>
-          <div className="col-xs-6 text-left">
-            <div className="checkbox checkbox-success">
+        <div className="form-group row">
+          <div className="col-6 offset-3">
+            <div className="custom-control custom-switch custom-checkbox-success">
               <input
               <input
                 id="isLdapEnabled"
                 id="isLdapEnabled"
+                className="custom-control-input"
                 type="checkbox"
                 type="checkbox"
                 checked={isLdapEnabled}
                 checked={isLdapEnabled}
                 onChange={() => { adminGeneralSecurityContainer.switchIsLdapEnabled() }}
                 onChange={() => { adminGeneralSecurityContainer.switchIsLdapEnabled() }}
               />
               />
-              <label htmlFor="isLdapEnabled">
+              <label className="custom-control-label" htmlFor="isLdapEnabled">
                 {t('security_setting.ldap.enable_ldap')}
                 {t('security_setting.ldap.enable_ldap')}
               </label>
               </label>
             </div>
             </div>
             {(!adminGeneralSecurityContainer.state.setupStrategies.includes('ldap') && isLdapEnabled)
             {(!adminGeneralSecurityContainer.state.setupStrategies.includes('ldap') && isLdapEnabled)
-              && <div className="label label-warning">{t('security_setting.setup_is_not_yet_complete')}</div>}
+              && <div className="badge badge-warning">{t('security_setting.setup_is_not_yet_complete')}</div>}
           </div>
           </div>
         </div>
         </div>
 
 
@@ -100,9 +98,11 @@ class LdapSecuritySetting extends React.Component {
 
 
             <h3 className="border-bottom">{t('security_setting.configuration')}</h3>
             <h3 className="border-bottom">{t('security_setting.configuration')}</h3>
 
 
-            <div className="row mb-5">
-              <label htmlFor="serverUrl" className="col-xs-3 control-label text-right">Server URL</label>
-              <div className="col-xs-6">
+            <div className="form-group row">
+              <label htmlFor="serverUrl" className="text-left text-md-right col-md-3 col-form-label">
+                Server URL
+              </label>
+              <div className="col-md-6">
                 <input
                 <input
                   className="form-control"
                   className="form-control"
                   type="text"
                   type="text"
@@ -112,7 +112,7 @@ class LdapSecuritySetting extends React.Component {
                 />
                 />
                 <small>
                 <small>
                   <p
                   <p
-                    className="help-block"
+                    className="form-text text-muted"
                     // eslint-disable-next-line react/no-danger
                     // eslint-disable-next-line react/no-danger
                     dangerouslySetInnerHTML={{ __html: t('security_setting.ldap.server_url_detail') }}
                     dangerouslySetInnerHTML={{ __html: t('security_setting.ldap.server_url_detail') }}
                   />
                   />
@@ -121,36 +121,41 @@ class LdapSecuritySetting extends React.Component {
               </div>
               </div>
             </div>
             </div>
 
 
-            <div className="row mb-5">
-              <strong className="col-xs-3 text-right">{t('security_setting.ldap.bind_mode')}</strong>
-              <div className="col-xs-6 text-left">
-                <div className="my-0 btn-group">
-                  <div className="dropdown">
-                    <button className="btn btn-default dropdown-toggle w-100" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
-                      {adminLdapSecurityContainer.state.isUserBind
+            <div className="form-group row">
+              <label className="text-left text-md-right col-md-3 col-form-label">
+                <strong>{t('security_setting.ldap.bind_mode')}</strong>
+              </label>
+              <div className="col-md-6">
+                <div className="dropdown">
+                  <button
+                    className="btn btn-outline-secondary dropdown-toggle"
+                    type="button"
+                    id="dropdownMenuButton"
+                    data-toggle="dropdown"
+                    aria-haspopup="true"
+                    aria-expanded="true"
+                  >
+                    {adminLdapSecurityContainer.state.isUserBind
                         ? <span className="pull-left">{t('security_setting.ldap.bind_user')}</span>
                         ? <span className="pull-left">{t('security_setting.ldap.bind_user')}</span>
                         : <span className="pull-left">{t('security_setting.ldap.bind_manager')}</span>}
                         : <span className="pull-left">{t('security_setting.ldap.bind_manager')}</span>}
-                      <span className="bs-caret pull-right">
-                        <span className="caret" />
-                      </span>
-                    </button>
-                    {/* TODO adjust dropdown after BS4 */}
-                    <ul className="dropdown-menu" role="menu">
-                      <li key="user" role="presentation" type="button" onClick={() => { adminLdapSecurityContainer.changeLdapBindMode(true) }}>
-                        <a role="menuitem">{t('security_setting.ldap.bind_user')}</a>
-                      </li>
-                      <li key="manager" role="presentation" type="button" onClick={() => { adminLdapSecurityContainer.changeLdapBindMode(false) }}>
-                        <a role="menuitem">{t('security_setting.ldap.bind_manager')}</a>
-                      </li>
-                    </ul>
+                  </button>
+                  <div className="dropdown-menu" aria-labelledby="dropdownMenuButton">
+                    <a className="dropdown-item" type="button" onClick={() => { adminLdapSecurityContainer.changeLdapBindMode(true) }}>
+                      {t('security_setting.ldap.bind_user')}
+                    </a>
+                    <a className="dropdown-item" type="button" onClick={() => { adminLdapSecurityContainer.changeLdapBindMode(false) }}>
+                      {t('security_setting.ldap.bind_manager')}
+                    </a>
                   </div>
                   </div>
                 </div>
                 </div>
               </div>
               </div>
             </div>
             </div>
 
 
-            <div className="row mb-5">
-              <strong className="col-xs-3 text-right">Bind DN</strong>
-              <div className="col-xs-6">
+            <div className="form-group row">
+              <label className="text-left text-md-right col-md-3 col-form-label">
+                <strong>Bind DN</strong>
+              </label>
+              <div className="col-md-6">
                 <input
                 <input
                   className="form-control"
                   className="form-control"
                   type="text"
                   type="text"
@@ -159,7 +164,7 @@ class LdapSecuritySetting extends React.Component {
                   onChange={e => adminLdapSecurityContainer.changeBindDN(e.target.value)}
                   onChange={e => adminLdapSecurityContainer.changeBindDN(e.target.value)}
                 />
                 />
                 {(adminLdapSecurityContainer.state.isUserBind === true) ? (
                 {(adminLdapSecurityContainer.state.isUserBind === true) ? (
-                  <p className="help-block passport-ldap-userbind">
+                  <p className="form-text text-muted passport-ldap-userbind">
                     <small>
                     <small>
                       {t('security_setting.ldap.bind_DN_user_detail1')}<br />
                       {t('security_setting.ldap.bind_DN_user_detail1')}<br />
                       {/* eslint-disable-next-line react/no-danger */}
                       {/* eslint-disable-next-line react/no-danger */}
@@ -170,7 +175,7 @@ class LdapSecuritySetting extends React.Component {
                   </p>
                   </p>
                 )
                 )
                   : (
                   : (
-                    <p className="help-block passport-ldap-managerbind">
+                    <p className="form-text text-muted passport-ldap-managerbind">
                       <small>
                       <small>
                         {t('security_setting.ldap.bind_DN_manager_detail')}<br />
                         {t('security_setting.ldap.bind_DN_manager_detail')}<br />
                         {t('security_setting.example')}1: <code>uid=admin,dc=domain,dc=com</code><br />
                         {t('security_setting.example')}1: <code>uid=admin,dc=domain,dc=com</code><br />
@@ -181,11 +186,13 @@ class LdapSecuritySetting extends React.Component {
               </div>
               </div>
             </div>
             </div>
 
 
-            <div className="row mb-5">
-              <label htmlFor="bindDNPassword" className="col-xs-3 text-right">{t('security_setting.ldap.bind_DN_password')}</label>
-              <div className="col-xs-6">
+            <div className="form-group row">
+              <div htmlFor="bindDNPassword" className="text-left text-md-right col-md-3 col-form-label">
+                <strong>{t('security_setting.ldap.bind_DN_password')}</strong>
+              </div>
+              <div className="col-md-6">
                 {(adminLdapSecurityContainer.state.isUserBind) ? (
                 {(adminLdapSecurityContainer.state.isUserBind) ? (
-                  <p className="help-block passport-ldap-userbind">
+                  <p className="well card passport-ldap-userbind">
                     <small>
                     <small>
                       {t('security_setting.ldap.bind_DN_password_user_detail')}
                       {t('security_setting.ldap.bind_DN_password_user_detail')}
                     </small>
                     </small>
@@ -193,7 +200,7 @@ class LdapSecuritySetting extends React.Component {
                 )
                 )
                   : (
                   : (
                     <>
                     <>
-                      <p className="help-block passport-ldap-managerbind">
+                      <p className="well card passport-ldap-managerbind">
                         <small>
                         <small>
                           {t('security_setting.ldap.bind_DN_password_manager_detail')}
                           {t('security_setting.ldap.bind_DN_password_manager_detail')}
                         </small>
                         </small>
@@ -210,9 +217,11 @@ class LdapSecuritySetting extends React.Component {
               </div>
               </div>
             </div>
             </div>
 
 
-            <div className="row mb-5">
-              <strong className="col-xs-3 text-right">{t('security_setting.ldap.search_filter')}</strong>
-              <div className="col-xs-6">
+            <div className="form-group row">
+              <label className="text-left text-md-right col-md-3 col-form-label">
+                <strong>{t('security_setting.ldap.search_filter')}</strong>
+              </label>
+              <div className="col-md-6">
                 <input
                 <input
                   className="form-control"
                   className="form-control"
                   type="text"
                   type="text"
@@ -220,7 +229,7 @@ class LdapSecuritySetting extends React.Component {
                   defaultValue={adminLdapSecurityContainer.state.ldapSearchFilter || ''}
                   defaultValue={adminLdapSecurityContainer.state.ldapSearchFilter || ''}
                   onChange={e => adminLdapSecurityContainer.changeSearchFilter(e.target.value)}
                   onChange={e => adminLdapSecurityContainer.changeSearchFilter(e.target.value)}
                 />
                 />
-                <p className="help-block">
+                <p className="form-text text-muted">
                   <small>
                   <small>
                     {t('security_setting.ldap.search_filter_detail1')}<br />
                     {t('security_setting.ldap.search_filter_detail1')}<br />
                     {/* eslint-disable-next-line react/no-danger */}
                     {/* eslint-disable-next-line react/no-danger */}
@@ -229,7 +238,7 @@ class LdapSecuritySetting extends React.Component {
                     <span dangerouslySetInnerHTML={{ __html: t('security_setting.ldap.search_filter_detail3') }} />
                     <span dangerouslySetInnerHTML={{ __html: t('security_setting.ldap.search_filter_detail3') }} />
                   </small>
                   </small>
                 </p>
                 </p>
-                <p className="help-block">
+                <p className="form-text text-muted">
                   <small>
                   <small>
                     {t('security_setting.example')}1 - {t('security_setting.ldap.search_filter_example1')}:
                     {t('security_setting.example')}1 - {t('security_setting.ldap.search_filter_example1')}:
                     <code>(|(uid={'{{ username }}'})(mail={'{{ username }}'}))</code><br />
                     <code>(|(uid={'{{ username }}'})(mail={'{{ username }}'}))</code><br />
@@ -244,9 +253,11 @@ class LdapSecuritySetting extends React.Component {
               Attribute Mapping ({t('security_setting.optional')})
               Attribute Mapping ({t('security_setting.optional')})
             </h3>
             </h3>
 
 
-            <div className="row mb-5">
-              <strong htmlFor="attrMapUsername" className="col-xs-3 text-right">{t('username')}</strong>
-              <div className="col-xs-6">
+            <div className="form-group row">
+              <label className="text-left text-md-right col-md-3 col-form-label">
+                <strong htmlFor="attrMapUsername">{t('username')}</strong>
+              </label>
+              <div className="col-md-6">
                 <input
                 <input
                   className="form-control"
                   className="form-control"
                   type="text"
                   type="text"
@@ -255,38 +266,42 @@ class LdapSecuritySetting extends React.Component {
                   defaultValue={adminLdapSecurityContainer.state.ldapAttrMapUsername || ''}
                   defaultValue={adminLdapSecurityContainer.state.ldapAttrMapUsername || ''}
                   onChange={e => adminLdapSecurityContainer.changeAttrMapUsername(e.target.value)}
                   onChange={e => adminLdapSecurityContainer.changeAttrMapUsername(e.target.value)}
                 />
                 />
-                <p className="help-block">
+                <p className="form-text text-muted">
                   {/* eslint-disable-next-line react/no-danger */}
                   {/* eslint-disable-next-line react/no-danger */}
                   <small dangerouslySetInnerHTML={{ __html: t('security_setting.ldap.username_detail') }} />
                   <small dangerouslySetInnerHTML={{ __html: t('security_setting.ldap.username_detail') }} />
                 </p>
                 </p>
               </div>
               </div>
             </div>
             </div>
 
 
-            <div className="row mb-5">
-              <div className="col-xs-offset-3 col-xs-6 text-left">
-                <div className="checkbox checkbox-success">
+            <div className="form-group row">
+              <div className="offset-md-3 col-md-6">
+                <div className="custom-control custom-checkbox custom-checkbox-success">
                   <input
                   <input
-                    id="isSameUsernameTreatedAsIdenticalUser"
                     type="checkbox"
                     type="checkbox"
+                    className="custom-control-input"
+                    id="isSameUsernameTreatedAsIdenticalUser"
                     checked={adminLdapSecurityContainer.state.isSameUsernameTreatedAsIdenticalUser}
                     checked={adminLdapSecurityContainer.state.isSameUsernameTreatedAsIdenticalUser}
                     onChange={() => { adminLdapSecurityContainer.switchIsSameUsernameTreatedAsIdenticalUser() }}
                     onChange={() => { adminLdapSecurityContainer.switchIsSameUsernameTreatedAsIdenticalUser() }}
                   />
                   />
                   <label
                   <label
+                    className="custom-control-label"
                     htmlFor="isSameUsernameTreatedAsIdenticalUser"
                     htmlFor="isSameUsernameTreatedAsIdenticalUser"
                     // eslint-disable-next-line react/no-danger
                     // eslint-disable-next-line react/no-danger
                     dangerouslySetInnerHTML={{ __html: t('security_setting.Treat username matching as identical') }}
                     dangerouslySetInnerHTML={{ __html: t('security_setting.Treat username matching as identical') }}
                   />
                   />
                 </div>
                 </div>
-                <p className="help-block">
+                <p className="form-text text-muted">
                   {/* eslint-disable-next-line react/no-danger */}
                   {/* eslint-disable-next-line react/no-danger */}
                   <small dangerouslySetInnerHTML={{ __html: t('security_setting.Treat username matching as identical_warn') }} />
                   <small dangerouslySetInnerHTML={{ __html: t('security_setting.Treat username matching as identical_warn') }} />
                 </p>
                 </p>
               </div>
               </div>
             </div>
             </div>
 
 
-            <div className="row mb-5">
-              <strong htmlFor="attrMapMail" className="col-xs-3 text-right">{t('Email')}</strong>
-              <div className="col-xs-6">
+            <div className="form-group row">
+              <label className="text-left text-md-right col-md-3 col-form-label">
+                <strong htmlFor="attrMapMail">{t('Email')}</strong>
+              </label>
+              <div className="col-md-6">
                 <input
                 <input
                   className="form-control"
                   className="form-control"
                   type="text"
                   type="text"
@@ -295,7 +310,7 @@ class LdapSecuritySetting extends React.Component {
                   defaultValue={adminLdapSecurityContainer.state.ldapAttrMapMail || ''}
                   defaultValue={adminLdapSecurityContainer.state.ldapAttrMapMail || ''}
                   onChange={e => adminLdapSecurityContainer.changeAttrMapMail(e.target.value)}
                   onChange={e => adminLdapSecurityContainer.changeAttrMapMail(e.target.value)}
                 />
                 />
-                <p className="help-block">
+                <p className="form-text text-muted">
                   <small>
                   <small>
                     {t('security_setting.ldap.mail_detail')}
                     {t('security_setting.ldap.mail_detail')}
                   </small>
                   </small>
@@ -303,9 +318,11 @@ class LdapSecuritySetting extends React.Component {
               </div>
               </div>
             </div>
             </div>
 
 
-            <div className="row mb-5">
-              <strong htmlFor="attrMapName" className="col-xs-3 text-right">{t('Name')}</strong>
-              <div className="col-xs-6">
+            <div className="form-group row">
+              <label className="text-left text-md-right col-md-3 col-form-label">
+                <strong htmlFor="attrMapName">{t('Name')}</strong>
+              </label>
+              <div className="col-md-6">
                 <input
                 <input
                   className="form-control"
                   className="form-control"
                   type="text"
                   type="text"
@@ -313,7 +330,7 @@ class LdapSecuritySetting extends React.Component {
                   defaultValue={adminLdapSecurityContainer.state.ldapAttrMapName || ''}
                   defaultValue={adminLdapSecurityContainer.state.ldapAttrMapName || ''}
                   onChange={e => adminLdapSecurityContainer.changeAttrMapName(e.target.value)}
                   onChange={e => adminLdapSecurityContainer.changeAttrMapName(e.target.value)}
                 />
                 />
-                <p className="help-block">
+                <p className="form-text text-muted">
                   <small>
                   <small>
                     {t('security_setting.ldap.name_detail')}
                     {t('security_setting.ldap.name_detail')}
                   </small>
                   </small>
@@ -326,9 +343,11 @@ class LdapSecuritySetting extends React.Component {
               {t('security_setting.ldap.group_search_filter')} ({t('security_setting.optional')})
               {t('security_setting.ldap.group_search_filter')} ({t('security_setting.optional')})
             </h3>
             </h3>
 
 
-            <div className="row mb-5">
-              <strong htmlFor="groupSearchBase" className="col-xs-3 text-right">{t('security_setting.ldap.group_search_base_DN')}</strong>
-              <div className="col-xs-6">
+            <div className="form-group row">
+              <label className="text-left text-md-right col-md-3 col-form-label">
+                <strong htmlFor="groupSearchBase">{t('security_setting.ldap.group_search_base_DN')}</strong>
+              </label>
+              <div className="col-md-6">
                 <input
                 <input
                   className="form-control"
                   className="form-control"
                   type="text"
                   type="text"
@@ -336,7 +355,7 @@ class LdapSecuritySetting extends React.Component {
                   defaultValue={adminLdapSecurityContainer.state.ldapGroupSearchBase || ''}
                   defaultValue={adminLdapSecurityContainer.state.ldapGroupSearchBase || ''}
                   onChange={e => adminLdapSecurityContainer.changeGroupSearchBase(e.target.value)}
                   onChange={e => adminLdapSecurityContainer.changeGroupSearchBase(e.target.value)}
                 />
                 />
-                <p className="help-block">
+                <p className="form-text text-muted">
                   <small>
                   <small>
                     {/* eslint-disable-next-line react/no-danger */}
                     {/* eslint-disable-next-line react/no-danger */}
                     <span dangerouslySetInnerHTML={{ __html: t('security_setting.ldap.group_search_base_DN_detail') }} /><br />
                     <span dangerouslySetInnerHTML={{ __html: t('security_setting.ldap.group_search_base_DN_detail') }} /><br />
@@ -346,9 +365,11 @@ class LdapSecuritySetting extends React.Component {
               </div>
               </div>
             </div>
             </div>
 
 
-            <div className="row mb-5">
-              <strong htmlFor="groupSearchFilter" className="col-xs-3 text-right">{t('security_setting.ldap.group_search_filter')}</strong>
-              <div className="col-xs-6">
+            <div className="form-group row">
+              <label className="text-left text-md-right col-md-3 col-form-label">
+                <strong htmlFor="groupSearchFilter">{t('security_setting.ldap.group_search_filter')}</strong>
+              </label>
+              <div className="col-md-6">
                 <input
                 <input
                   className="form-control"
                   className="form-control"
                   type="text"
                   type="text"
@@ -356,7 +377,7 @@ class LdapSecuritySetting extends React.Component {
                   defaultValue={adminLdapSecurityContainer.state.ldapGroupSearchFilter || ''}
                   defaultValue={adminLdapSecurityContainer.state.ldapGroupSearchFilter || ''}
                   onChange={e => adminLdapSecurityContainer.changeGroupSearchFilter(e.target.value)}
                   onChange={e => adminLdapSecurityContainer.changeGroupSearchFilter(e.target.value)}
                 />
                 />
-                <p className="help-block">
+                <p className="form-text text-muted">
                   <small>
                   <small>
                     {/* eslint-disable react/no-danger */}
                     {/* eslint-disable react/no-danger */}
                     <span dangerouslySetInnerHTML={{ __html: t('security_setting.ldap.group_search_filter_detail1') }} /><br />
                     <span dangerouslySetInnerHTML={{ __html: t('security_setting.ldap.group_search_filter_detail1') }} /><br />
@@ -365,7 +386,7 @@ class LdapSecuritySetting extends React.Component {
                     {/* eslint-enable react/no-danger */}
                     {/* eslint-enable react/no-danger */}
                   </small>
                   </small>
                 </p>
                 </p>
-                <p className="help-block">
+                <p className="form-text text-muted">
                   <small>
                   <small>
                     {t('security_setting.example')}:
                     {t('security_setting.example')}:
                     {/* eslint-disable-next-line react/no-danger */}
                     {/* eslint-disable-next-line react/no-danger */}
@@ -375,9 +396,11 @@ class LdapSecuritySetting extends React.Component {
               </div>
               </div>
             </div>
             </div>
 
 
-            <div className="row mb-5">
-              <label htmlFor="groupDnProperty" className="col-xs-3 text-right">{t('security_setting.ldap.group_search_user_DN_property')}</label>
-              <div className="col-xs-6">
+            <div className="form-group row">
+              <label className="text-left text-md-right col-md-3 col-form-label">
+                <strong htmlFor="groupDnProperty">{t('security_setting.ldap.group_search_user_DN_property')}</strong>
+              </label>
+              <div className="col-md-6">
                 <input
                 <input
                   className="form-control"
                   className="form-control"
                   type="text"
                   type="text"
@@ -386,14 +409,14 @@ class LdapSecuritySetting extends React.Component {
                   defaultValue={adminLdapSecurityContainer.state.ldapGroupDnProperty || ''}
                   defaultValue={adminLdapSecurityContainer.state.ldapGroupDnProperty || ''}
                   onChange={e => adminLdapSecurityContainer.changeGroupDnProperty(e.target.value)}
                   onChange={e => adminLdapSecurityContainer.changeGroupDnProperty(e.target.value)}
                 />
                 />
-                <p className="help-block">
+                <p className="form-text text-muted">
                   {/* eslint-disable-next-line react/no-danger */}
                   {/* eslint-disable-next-line react/no-danger */}
                   <small dangerouslySetInnerHTML={{ __html: t('security_setting.ldap.group_search_user_DN_property_detail') }} />
                   <small dangerouslySetInnerHTML={{ __html: t('security_setting.ldap.group_search_user_DN_property_detail') }} />
                 </p>
                 </p>
               </div>
               </div>
             </div>
             </div>
             <div className="row my-3">
             <div className="row my-3">
-              <div className="col-xs-offset-3 col-xs-5">
+              <div className="offset-3 col-5">
                 <button
                 <button
                   type="button"
                   type="button"
                   className="btn btn-primary"
                   className="btn btn-primary"
@@ -402,7 +425,12 @@ class LdapSecuritySetting extends React.Component {
                 >
                 >
                   {t('Update')}
                   {t('Update')}
                 </button>
                 </button>
-                <button type="button" className="btn btn-default ml-2" onClick={this.openLdapAuthTestModal}>{t('security_setting.ldap.test_config')}</button>
+                <button
+                  type="button"
+                  className="btn btn-outline-secondary ml-2"
+                  onClick={this.openLdapAuthTestModal}
+                >{t('security_setting.ldap.test_config')}
+                </button>
               </div>
               </div>
             </div>
             </div>
 
 

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

@@ -75,24 +75,22 @@ class LocalSecuritySetting extends React.Component {
         )}
         )}
 
 
         <div className="row mb-5">
         <div className="row mb-5">
-          <div className="col-xs-3 my-3 text-right">
-            <strong>{t('security_setting.Local.name')}</strong>
-          </div>
-          <div className="col-xs-6 text-left">
-            <div className="checkbox checkbox-success">
+          <div className="col-6 offset-3">
+            <div className="custom-control custom-switch custom-checkbox-success">
               <input
               <input
-                id="isLocalEnabled"
                 type="checkbox"
                 type="checkbox"
-                checked={adminGeneralSecurityContainer.state.isLocalEnabled}
-                onChange={() => { adminGeneralSecurityContainer.switchIsLocalEnabled() }}
+                className="custom-control-input"
+                id="isLocalEnabled"
+                checked={isLocalEnabled}
+                onChange={() => adminGeneralSecurityContainer.switchIsLocalEnabled()}
                 disabled={adminLocalSecurityContainer.state.useOnlyEnvVars}
                 disabled={adminLocalSecurityContainer.state.useOnlyEnvVars}
               />
               />
-              <label htmlFor="isLocalEnabled">
+              <label className="custom-control-label" htmlFor="isLocalEnabled">
                 {t('security_setting.Local.enable_local')}
                 {t('security_setting.Local.enable_local')}
               </label>
               </label>
             </div>
             </div>
             {(!adminGeneralSecurityContainer.state.setupStrategies.includes('local') && isLocalEnabled)
             {(!adminGeneralSecurityContainer.state.setupStrategies.includes('local') && isLocalEnabled)
-            && <div className="label label-warning">{t('security_setting.setup_is_not_yet_complete')}</div>}
+              && <div className="badge badge-warning">{t('security_setting.setup_is_not_yet_complete')}</div>}
           </div>
           </div>
         </div>
         </div>
 
 
@@ -101,74 +99,63 @@ class LocalSecuritySetting extends React.Component {
 
 
             <h3 className="border-bottom">{t('security_setting.configuration')}</h3>
             <h3 className="border-bottom">{t('security_setting.configuration')}</h3>
 
 
-            <div className="row mb-5">
-              <strong className="col-xs-3 text-right">{t('Register limitation')}</strong>
-              <div className="col-xs-9 text-left">
-                <div className="my-0 btn-group">
-                  <div className="dropdown">
-                    <button className="btn btn-default dropdown-toggle w-100" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
-                      {registrationMode === 'Open' && <span className="pull-left">{t('security_setting.registration_mode.open')}</span>}
-                      {registrationMode === 'Restricted' && <span className="pull-left">{t('security_setting.registration_mode.restricted')}</span>}
-                      {registrationMode === 'Closed' && <span className="pull-left">{t('security_setting.registration_mode.closed')}</span>}
-                      <span className="bs-caret pull-right">
-                        <span className="caret" />
-                      </span>
-                    </button>
-                    {/* TODO adjust dropdown after BS4 */}
-                    <ul className="dropdown-menu" role="menu">
-                      <li
-                        key="Open"
-                        role="presentation"
-                        type="button"
-                        onClick={() => { adminLocalSecurityContainer.changeRegistrationMode('Open') }}
-                      >
-                        <a role="menuitem">{t('security_setting.registration_mode.open')}</a>
-                      </li>
-                      <li
-                        key="Restricted"
-                        role="presentation"
-                        type="button"
-                        onClick={() => { adminLocalSecurityContainer.changeRegistrationMode('Restricted') }}
-                      >
-                        <a role="menuitem">{t('security_setting.registration_mode.restricted')}</a>
-                      </li>
-                      <li
-                        key="Closed"
-                        role="presentation"
-                        type="button"
-                        onClick={() => { adminLocalSecurityContainer.changeRegistrationMode('Closed') }}
-                      >
-                        <a role="menuitem">{t('security_setting.registration_mode.closed')}</a>
-                      </li>
-                    </ul>
+            <div className="row">
+              <div className="col-12 col-md-3 text-left text-md-right py-2">
+                <strong>{t('Register limitation')}</strong>
+              </div>
+              <div className="col-12 col-md-6">
+                <div className="dropdown">
+                  <button
+                    className="btn btn-outline-secondary dropdown-toggle"
+                    type="button"
+                    id="dropdownMenuButton"
+                    data-toggle="dropdown"
+                    aria-haspopup="true"
+                    aria-expanded="true"
+                  >
+                    {registrationMode === 'Open' && t('security_setting.registration_mode.open')}
+                    {registrationMode === 'Restricted' && t('security_setting.registration_mode.restricted')}
+                    {registrationMode === 'Closed' && t('security_setting.registration_mode.closed')}
+                  </button>
+                  <div className="dropdown-menu" aria-labelledby="dropdownMenuButton">
+                    <a className="dropdown-item" type="button" onClick={() => { adminLocalSecurityContainer.changeRegistrationMode('Open') }}>
+                      {t('security_setting.registration_mode.open')}
+                    </a>
+                    <a className="dropdown-item" type="button" onClick={() => { adminLocalSecurityContainer.changeRegistrationMode('Restricted') }}>
+                      {t('security_setting.registration_mode.restricted')}
+                    </a>
+                    <a className="dropdown-item" type="button" onClick={() => { adminLocalSecurityContainer.changeRegistrationMode('Closed') }}>
+                      {t('security_setting.registration_mode.closed')}
+                    </a>
                   </div>
                   </div>
-                  <p className="help-block">
-                    {t('security_setting.Register limitation desc')}
-                  </p>
                 </div>
                 </div>
+
+                <p className="form-text text-muted small">
+                  {t('security_setting.Register limitation desc')}
+                </p>
               </div>
               </div>
             </div>
             </div>
-            <div className="row mb-5">
-              <strong className="col-xs-3 text-right" dangerouslySetInnerHTML={{ __html: t('The whitelist of registration permission E-mail address') }} />
-              <div className="col-xs-6">
-                <div>
-                  <textarea
-                    className="form-control"
-                    type="textarea"
-                    name="registrationWhiteList"
-                    defaultValue={adminLocalSecurityContainer.state.registrationWhiteList.join('\n')}
-                    onChange={e => adminLocalSecurityContainer.changeRegistrationWhiteList(e.target.value)}
-                  />
-                  <p className="help-block small">{t('security_setting.restrict_emails')}<br />{t('security_setting.for_example')}
-                    <code>@growi.org</code>{t('security_setting.in_this_case')}<br />
-                    {t('security_setting.insert_single')}
-                  </p>
-                </div>
+            <div className="row">
+              <div className="col-12 col-md-3 text-left text-md-right">
+                <strong dangerouslySetInnerHTML={{ __html: t('The whitelist of registration permission E-mail address') }} />
+              </div>
+              <div className="col-12 col-md-6">
+                <textarea
+                  className="form-control"
+                  type="textarea"
+                  name="registrationWhiteList"
+                  defaultValue={adminLocalSecurityContainer.state.registrationWhiteList.join('\n')}
+                  onChange={e => adminLocalSecurityContainer.changeRegistrationWhiteList(e.target.value)}
+                />
+                <p className="form-text text-muted small">{t('security_setting.restrict_emails')}<br />{t('security_setting.for_example')}
+                  <code>@growi.org</code>{t('security_setting.in_this_case')}<br />
+                  {t('security_setting.insert_single')}
+                </p>
               </div>
               </div>
             </div>
             </div>
 
 
             <div className="row my-3">
             <div className="row my-3">
-              <div className="col-xs-offset-3 col-xs-5">
+              <div className="offset-3 col-6">
                 <button
                 <button
                   type="button"
                   type="button"
                   className="btn btn-primary"
                   className="btn btn-primary"

+ 60 - 58
src/client/js/components/Admin/Security/OidcSecuritySetting.jsx

@@ -63,43 +63,41 @@ class OidcSecurityManagement extends React.Component {
           {t('security_setting.OAuth.OIDC.name')}
           {t('security_setting.OAuth.OIDC.name')}
         </h2>
         </h2>
 
 
-        <div className="row mb-5">
-          <div className="col-xs-3 my-3 text-right">
-            <strong>{t('security_setting.OAuth.OIDC.name')}</strong>
-          </div>
-          <div className="col-xs-6 text-left">
-            <div className="checkbox checkbox-success">
+        <div className="row mb-5 form-group">
+          <div className="offset-3 col-6">
+            <div className="custom-control custom-switch custom-checkbox-success">
               <input
               <input
                 id="isOidcEnabled"
                 id="isOidcEnabled"
+                className="custom-control-input"
                 type="checkbox"
                 type="checkbox"
                 checked={adminGeneralSecurityContainer.state.isOidcEnabled}
                 checked={adminGeneralSecurityContainer.state.isOidcEnabled}
                 onChange={() => { adminGeneralSecurityContainer.switchIsOidcEnabled() }}
                 onChange={() => { adminGeneralSecurityContainer.switchIsOidcEnabled() }}
               />
               />
-              <label htmlFor="isOidcEnabled">
+              <label className="custom-control-label" htmlFor="isOidcEnabled">
                 {t('security_setting.OAuth.enable_oidc')}
                 {t('security_setting.OAuth.enable_oidc')}
               </label>
               </label>
             </div>
             </div>
             {(!adminGeneralSecurityContainer.state.setupStrategies.includes('oidc') && isOidcEnabled)
             {(!adminGeneralSecurityContainer.state.setupStrategies.includes('oidc') && isOidcEnabled)
-              && <div className="label label-warning">{t('security_setting.setup_is_not_yet_complete')}</div>}
+              && <div className="badge badge-warning">{t('security_setting.setup_is_not_yet_complete')}</div>}
           </div>
           </div>
         </div>
         </div>
 
 
-        <div className="row mb-5">
-          <label className="col-xs-3 text-right">{t('security_setting.callback_URL')}</label>
-          <div className="col-xs-6">
+        <div className="row mb-5 form-group">
+          <label className="text-left text-md-right col-md-3 col-form-label">{t('security_setting.callback_URL')}</label>
+          <div className="col-md-6">
             <input
             <input
               className="form-control"
               className="form-control"
               type="text"
               type="text"
               value={adminOidcSecurityContainer.state.callbackUrl}
               value={adminOidcSecurityContainer.state.callbackUrl}
               readOnly
               readOnly
             />
             />
-            <p className="help-block small">{t('security_setting.desc_of_callback_URL', { AuthName: 'OAuth' })}</p>
+            <p className="form-text text-muted small">{t('security_setting.desc_of_callback_URL', { AuthName: 'OAuth' })}</p>
             {!adminGeneralSecurityContainer.state.appSiteUrl && (
             {!adminGeneralSecurityContainer.state.appSiteUrl && (
               <div className="alert alert-danger">
               <div className="alert alert-danger">
                 <i
                 <i
                   className="icon-exclamation"
                   className="icon-exclamation"
                   // eslint-disable-next-line max-len
                   // eslint-disable-next-line max-len
-                  dangerouslySetInnerHTML={{ __html: t('security_setting.alert_siteUrl_is_not_set', { link: `<a href="/admin/app">${t('App settings')}<i class="icon-login"></i></a>` }) }}
+                  dangerouslySetInnerHTML={{ __html: t('security_setting.alert_siteUrl_is_not_set', { link: `<a href="/admin/app">${t('App Settings')}<i class="icon-login"></i></a>` }) }}
                 />
                 />
               </div>
               </div>
             )}
             )}
@@ -111,9 +109,9 @@ class OidcSecurityManagement extends React.Component {
 
 
             <h3 className="border-bottom">{t('security_setting.configuration')}</h3>
             <h3 className="border-bottom">{t('security_setting.configuration')}</h3>
 
 
-            <div className="row mb-5">
-              <label htmlFor="oidcProviderName" className="col-xs-3 text-right">{t('security_setting.providerName')}</label>
-              <div className="col-xs-6">
+            <div className="row mb-5 form-group">
+              <label htmlFor="oidcProviderName" className="text-left text-md-right col-md-3 col-form-label">{t('security_setting.providerName')}</label>
+              <div className="col-md-6">
                 <input
                 <input
                   className="form-control"
                   className="form-control"
                   type="text"
                   type="text"
@@ -124,9 +122,9 @@ class OidcSecurityManagement extends React.Component {
               </div>
               </div>
             </div>
             </div>
 
 
-            <div className="row mb-5">
-              <label htmlFor="oidcIssuerHost" className="col-xs-3 text-right">{t('security_setting.issuerHost')}</label>
-              <div className="col-xs-6">
+            <div className="row mb-5 form-group">
+              <label htmlFor="oidcIssuerHost" className="text-left text-md-right col-md-3 col-form-label">{t('security_setting.issuerHost')}</label>
+              <div className="col-md-6">
                 <input
                 <input
                   className="form-control"
                   className="form-control"
                   type="text"
                   type="text"
@@ -134,15 +132,15 @@ class OidcSecurityManagement extends React.Component {
                   defaultValue={adminOidcSecurityContainer.state.oidcIssuerHost || ''}
                   defaultValue={adminOidcSecurityContainer.state.oidcIssuerHost || ''}
                   onChange={e => adminOidcSecurityContainer.changeOidcIssuerHost(e.target.value)}
                   onChange={e => adminOidcSecurityContainer.changeOidcIssuerHost(e.target.value)}
                 />
                 />
-                <p className="help-block">
+                <p className="form-text text-muted">
                   <small dangerouslySetInnerHTML={{ __html: t('security_setting.Use env var if empty', { env: 'OAUTH_OIDC_ISSUER_HOST' }) }} />
                   <small dangerouslySetInnerHTML={{ __html: t('security_setting.Use env var if empty', { env: 'OAUTH_OIDC_ISSUER_HOST' }) }} />
                 </p>
                 </p>
               </div>
               </div>
             </div>
             </div>
 
 
-            <div className="row mb-5">
-              <label htmlFor="oidcClientId" className="col-xs-3 text-right">{t('security_setting.clientID')}</label>
-              <div className="col-xs-6">
+            <div className="row mb-5 form-group">
+              <label htmlFor="oidcClientId" className="text-left text-md-right col-md-3 col-form-label">{t('security_setting.clientID')}</label>
+              <div className="col-md-6">
                 <input
                 <input
                   className="form-control"
                   className="form-control"
                   type="text"
                   type="text"
@@ -150,15 +148,15 @@ class OidcSecurityManagement extends React.Component {
                   defaultValue={adminOidcSecurityContainer.state.oidcClientId || ''}
                   defaultValue={adminOidcSecurityContainer.state.oidcClientId || ''}
                   onChange={e => adminOidcSecurityContainer.changeOidcClientId(e.target.value)}
                   onChange={e => adminOidcSecurityContainer.changeOidcClientId(e.target.value)}
                 />
                 />
-                <p className="help-block">
+                <p className="form-text text-muted">
                   <small dangerouslySetInnerHTML={{ __html: t('security_setting.Use env var if empty', { env: 'OAUTH_OIDC_CLIENT_ID' }) }} />
                   <small dangerouslySetInnerHTML={{ __html: t('security_setting.Use env var if empty', { env: 'OAUTH_OIDC_CLIENT_ID' }) }} />
                 </p>
                 </p>
               </div>
               </div>
             </div>
             </div>
 
 
-            <div className="row mb-5">
-              <label htmlFor="oidcClientSecret" className="col-xs-3 text-right">{t('security_setting.client_secret')}</label>
-              <div className="col-xs-6">
+            <div className="row mb-5 form-group">
+              <label htmlFor="oidcClientSecret" className="text-left text-md-right col-md-3 col-form-label">{t('security_setting.client_secret')}</label>
+              <div className="col-md-6">
                 <input
                 <input
                   className="form-control"
                   className="form-control"
                   type="text"
                   type="text"
@@ -166,7 +164,7 @@ class OidcSecurityManagement extends React.Component {
                   defaultValue={adminOidcSecurityContainer.state.oidcClientSecret || ''}
                   defaultValue={adminOidcSecurityContainer.state.oidcClientSecret || ''}
                   onChange={e => adminOidcSecurityContainer.changeOidcClientSecret(e.target.value)}
                   onChange={e => adminOidcSecurityContainer.changeOidcClientSecret(e.target.value)}
                 />
                 />
-                <p className="help-block">
+                <p className="form-text text-muted">
                   <small dangerouslySetInnerHTML={{ __html: t('security_setting.Use env var if empty', { env: 'OAUTH_OIDC_CLIENT_SECRET' }) }} />
                   <small dangerouslySetInnerHTML={{ __html: t('security_setting.Use env var if empty', { env: 'OAUTH_OIDC_CLIENT_SECRET' }) }} />
                 </p>
                 </p>
               </div>
               </div>
@@ -176,9 +174,9 @@ class OidcSecurityManagement extends React.Component {
               Attribute Mapping ({t('security_setting.optional')})
               Attribute Mapping ({t('security_setting.optional')})
             </h3>
             </h3>
 
 
-            <div className="row mb-5">
-              <label htmlFor="oidcAttrMapId" className="col-xs-3 text-right">Identifier</label>
-              <div className="col-xs-6">
+            <div className="row mb-5 form-group">
+              <label htmlFor="oidcAttrMapId" className="text-left text-md-right col-md-3 col-form-label">Identifier</label>
+              <div className="col-md-6">
                 <input
                 <input
                   className="form-control"
                   className="form-control"
                   type="text"
                   type="text"
@@ -186,15 +184,15 @@ class OidcSecurityManagement extends React.Component {
                   defaultValue={adminOidcSecurityContainer.state.oidcAttrMapId || ''}
                   defaultValue={adminOidcSecurityContainer.state.oidcAttrMapId || ''}
                   onChange={e => adminOidcSecurityContainer.changeOidcAttrMapId(e.target.value)}
                   onChange={e => adminOidcSecurityContainer.changeOidcAttrMapId(e.target.value)}
                 />
                 />
-                <p className="help-block">
+                <p className="form-text text-muted">
                   <small dangerouslySetInnerHTML={{ __html: t('security_setting.OAuth.OIDC.id_detail') }} />
                   <small dangerouslySetInnerHTML={{ __html: t('security_setting.OAuth.OIDC.id_detail') }} />
                 </p>
                 </p>
               </div>
               </div>
             </div>
             </div>
 
 
-            <div className="row mb-5">
-              <label htmlFor="oidcAttrMapUserName" className="col-xs-3 text-right">{t('username')}</label>
-              <div className="col-xs-6">
+            <div className="row mb-5 form-group">
+              <label htmlFor="oidcAttrMapUserName" className="text-left text-md-right col-md-3 col-form-label">{t('username')}</label>
+              <div className="col-md-6">
                 <input
                 <input
                   className="form-control"
                   className="form-control"
                   type="text"
                   type="text"
@@ -202,15 +200,15 @@ class OidcSecurityManagement extends React.Component {
                   defaultValue={adminOidcSecurityContainer.state.oidcAttrMapUserName || ''}
                   defaultValue={adminOidcSecurityContainer.state.oidcAttrMapUserName || ''}
                   onChange={e => adminOidcSecurityContainer.changeOidcAttrMapUserName(e.target.value)}
                   onChange={e => adminOidcSecurityContainer.changeOidcAttrMapUserName(e.target.value)}
                 />
                 />
-                <p className="help-block">
+                <p className="form-text text-muted">
                   <small dangerouslySetInnerHTML={{ __html: t('security_setting.OAuth.OIDC.username_detail') }} />
                   <small dangerouslySetInnerHTML={{ __html: t('security_setting.OAuth.OIDC.username_detail') }} />
                 </p>
                 </p>
               </div>
               </div>
             </div>
             </div>
 
 
-            <div className="row mb-5">
-              <label htmlFor="oidcAttrMapName" className="col-xs-3 text-right">{t('Name')}</label>
-              <div className="col-xs-6">
+            <div className="row mb-5 form-group">
+              <label htmlFor="oidcAttrMapName" className="text-left text-md-right col-md-3 col-form-label">{t('Name')}</label>
+              <div className="col-md-6">
                 <input
                 <input
                   className="form-control"
                   className="form-control"
                   type="text"
                   type="text"
@@ -218,15 +216,15 @@ class OidcSecurityManagement extends React.Component {
                   defaultValue={adminOidcSecurityContainer.state.oidcAttrMapName || ''}
                   defaultValue={adminOidcSecurityContainer.state.oidcAttrMapName || ''}
                   onChange={e => adminOidcSecurityContainer.changeOidcAttrMapName(e.target.value)}
                   onChange={e => adminOidcSecurityContainer.changeOidcAttrMapName(e.target.value)}
                 />
                 />
-                <p className="help-block">
+                <p className="form-text text-muted">
                   <small dangerouslySetInnerHTML={{ __html: t('security_setting.OAuth.OIDC.name_detail') }} />
                   <small dangerouslySetInnerHTML={{ __html: t('security_setting.OAuth.OIDC.name_detail') }} />
                 </p>
                 </p>
               </div>
               </div>
             </div>
             </div>
 
 
-            <div className="row mb-5">
-              <label htmlFor="oidcAttrMapEmail" className="col-xs-3 text-right">{t('Email')}</label>
-              <div className="col-xs-6">
+            <div className="row mb-5 form-group">
+              <label htmlFor="oidcAttrMapEmail" className="text-left text-md-right col-md-3 col-form-label">{t('Email')}</label>
+              <div className="col-md-6">
                 <input
                 <input
                   className="form-control"
                   className="form-control"
                   type="text"
                   type="text"
@@ -234,76 +232,80 @@ class OidcSecurityManagement extends React.Component {
                   defaultValue={adminOidcSecurityContainer.state.oidcAttrMapEmail || ''}
                   defaultValue={adminOidcSecurityContainer.state.oidcAttrMapEmail || ''}
                   onChange={e => adminOidcSecurityContainer.changeOidcAttrMapEmail(e.target.value)}
                   onChange={e => adminOidcSecurityContainer.changeOidcAttrMapEmail(e.target.value)}
                 />
                 />
-                <p className="help-block">
+                <p className="form-text text-muted">
                   <small dangerouslySetInnerHTML={{ __html: t('security_setting.OAuth.OIDC.mapping_detail', { target: t('Email') }) }} />
                   <small dangerouslySetInnerHTML={{ __html: t('security_setting.OAuth.OIDC.mapping_detail', { target: t('Email') }) }} />
                 </p>
                 </p>
               </div>
               </div>
             </div>
             </div>
 
 
-            <div className="row mb-5">
-              <label className="col-xs-3 text-right">{t('security_setting.callback_URL')}</label>
-              <div className="col-xs-6">
+            <div className="row mb-5 form-group">
+              <label className="text-left text-md-right col-md-3 col-form-label">{t('security_setting.callback_URL')}</label>
+              <div className="col-md-6">
                 <input
                 <input
                   className="form-control"
                   className="form-control"
                   type="text"
                   type="text"
                   defaultValue={adminOidcSecurityContainer.state.callbackUrl || ''}
                   defaultValue={adminOidcSecurityContainer.state.callbackUrl || ''}
                   readOnly
                   readOnly
                 />
                 />
-                <p className="help-block small">{t('security_setting.desc_of_callback_URL', { AuthName: 'OAuth' })}</p>
+                <p className="form-text text-muted small">{t('security_setting.desc_of_callback_URL', { AuthName: 'OAuth' })}</p>
                 {!adminGeneralSecurityContainer.state.appSiteUrl && (
                 {!adminGeneralSecurityContainer.state.appSiteUrl && (
                   <div className="alert alert-danger">
                   <div className="alert alert-danger">
                     <i
                     <i
                       className="icon-exclamation"
                       className="icon-exclamation"
                       // eslint-disable-next-line max-len
                       // eslint-disable-next-line max-len
-                      dangerouslySetInnerHTML={{ __html: t('security_setting.alert_siteUrl_is_not_set', { link: `<a href="/admin/app">${t('App settings')}<i class="icon-login"></i></a>` }) }}
+                      dangerouslySetInnerHTML={{ __html: t('security_setting.alert_siteUrl_is_not_set', { link: `<a href="/admin/app">${t('App Settings')}<i class="icon-login"></i></a>` }) }}
                     />
                     />
                   </div>
                   </div>
                 )}
                 )}
               </div>
               </div>
             </div>
             </div>
 
 
-            <div className="row mb-3">
-              <div className="col-xs-offset-3 col-xs-6 text-left">
-                <div className="checkbox checkbox-success">
+            <div className="row mb-5 form-group">
+              <div className="offset-md-3 col-md-6">
+                <div className="custom-control custom-checkbox custom-checkbox-success">
                   <input
                   <input
                     id="bindByUserName-oidc"
                     id="bindByUserName-oidc"
+                    className="custom-control-input"
                     type="checkbox"
                     type="checkbox"
                     checked={adminOidcSecurityContainer.state.isSameUsernameTreatedAsIdenticalUser}
                     checked={adminOidcSecurityContainer.state.isSameUsernameTreatedAsIdenticalUser}
                     onChange={() => { adminOidcSecurityContainer.switchIsSameUsernameTreatedAsIdenticalUser() }}
                     onChange={() => { adminOidcSecurityContainer.switchIsSameUsernameTreatedAsIdenticalUser() }}
                   />
                   />
                   <label
                   <label
+                    className="custom-control-label"
                     htmlFor="bindByUserName-oidc"
                     htmlFor="bindByUserName-oidc"
                     dangerouslySetInnerHTML={{ __html: t('security_setting.Treat username matching as identical') }}
                     dangerouslySetInnerHTML={{ __html: t('security_setting.Treat username matching as identical') }}
                   />
                   />
                 </div>
                 </div>
-                <p className="help-block">
+                <p className="form-text text-muted">
                   <small dangerouslySetInnerHTML={{ __html: t('security_setting.Treat username matching as identical_warn') }} />
                   <small dangerouslySetInnerHTML={{ __html: t('security_setting.Treat username matching as identical_warn') }} />
                 </p>
                 </p>
               </div>
               </div>
             </div>
             </div>
 
 
-            <div className="row mb-5">
-              <div className="col-xs-offset-3 col-xs-6 text-left">
-                <div className="checkbox checkbox-success">
+            <div className="row mb-5 form-group">
+              <div className="offset-md-3 col-md-6">
+                <div className="custom-control custom-checkbox custom-checkbox-success">
                   <input
                   <input
                     id="bindByEmail-oidc"
                     id="bindByEmail-oidc"
+                    className="custom-control-input"
                     type="checkbox"
                     type="checkbox"
                     checked={adminOidcSecurityContainer.state.isSameEmailTreatedAsIdenticalUser || false}
                     checked={adminOidcSecurityContainer.state.isSameEmailTreatedAsIdenticalUser || false}
                     onChange={() => { adminOidcSecurityContainer.switchIsSameEmailTreatedAsIdenticalUser() }}
                     onChange={() => { adminOidcSecurityContainer.switchIsSameEmailTreatedAsIdenticalUser() }}
                   />
                   />
                   <label
                   <label
+                    className="custom-control-label"
                     htmlFor="bindByEmail-oidc"
                     htmlFor="bindByEmail-oidc"
                     dangerouslySetInnerHTML={{ __html: t('security_setting.Treat email matching as identical') }}
                     dangerouslySetInnerHTML={{ __html: t('security_setting.Treat email matching as identical') }}
                   />
                   />
                 </div>
                 </div>
-                <p className="help-block">
+                <p className="form-text text-muted">
                   <small dangerouslySetInnerHTML={{ __html: t('security_setting.Treat email matching as identical_warn') }} />
                   <small dangerouslySetInnerHTML={{ __html: t('security_setting.Treat email matching as identical_warn') }} />
                 </p>
                 </p>
               </div>
               </div>
             </div>
             </div>
 
 
             <div className="row my-3">
             <div className="row my-3">
-              <div className="col-xs-offset-3 col-xs-5">
+              <div className="offset-3 col-5">
                 <button
                 <button
                   type="button"
                   type="button"
                   className="btn btn-primary"
                   className="btn btn-primary"

+ 44 - 42
src/client/js/components/Admin/Security/SamlSecuritySetting.jsx

@@ -89,44 +89,42 @@ class SamlSecurityManagement extends React.Component {
           />
           />
         )}
         )}
 
 
-        <div className="row mb-5">
-          <div className="col-xs-3 my-3 text-right">
-            <strong>{t('security_setting.SAML.name')}</strong>
-          </div>
-          <div className="col-xs-6 text-left">
-            <div className="checkbox checkbox-success">
+        <div className="row form-group mb-5">
+          <div className="col-6 offset-3">
+            <div className="custom-control custom-switch custom-checkbox-success">
               <input
               <input
                 id="isSamlEnabled"
                 id="isSamlEnabled"
+                className="custom-control-input"
                 type="checkbox"
                 type="checkbox"
                 checked={adminGeneralSecurityContainer.state.isSamlEnabled}
                 checked={adminGeneralSecurityContainer.state.isSamlEnabled}
                 onChange={() => { adminGeneralSecurityContainer.switchIsSamlEnabled() }}
                 onChange={() => { adminGeneralSecurityContainer.switchIsSamlEnabled() }}
                 disabled={adminSamlSecurityContainer.state.useOnlyEnvVars}
                 disabled={adminSamlSecurityContainer.state.useOnlyEnvVars}
               />
               />
-              <label htmlFor="isSamlEnabled">
+              <label className="custom-control-label" htmlFor="isSamlEnabled">
                 {t('security_setting.SAML.enable_saml')}
                 {t('security_setting.SAML.enable_saml')}
               </label>
               </label>
             </div>
             </div>
             {(!adminGeneralSecurityContainer.state.setupStrategies.includes('ldap') && isSamlEnabled)
             {(!adminGeneralSecurityContainer.state.setupStrategies.includes('ldap') && isSamlEnabled)
-              && <div className="label label-warning">{t('security_setting.setup_is_not_yet_complete')}</div>}
+              && <div className="badge badge-warning">{t('security_setting.setup_is_not_yet_complete')}</div>}
           </div>
           </div>
         </div>
         </div>
 
 
-        <div className="row mb-5">
-          <label className="col-xs-3 text-right">{t('security_setting.callback_URL')}</label>
-          <div className="col-xs-6">
+        <div className="row form-group mb-5">
+          <label className="text-left text-md-right col-md-3 col-form-label">{t('security_setting.callback_URL')}</label>
+          <div className="col-md-6">
             <input
             <input
               className="form-control"
               className="form-control"
               type="text"
               type="text"
               defaultValue={adminSamlSecurityContainer.state.callbackUrl}
               defaultValue={adminSamlSecurityContainer.state.callbackUrl}
               readOnly
               readOnly
             />
             />
-            <p className="help-block small">{t('security_setting.desc_of_callback_URL', { AuthName: 'SAML Identity' })}</p>
+            <p className="form-text text-muted small">{t('security_setting.desc_of_callback_URL', { AuthName: 'SAML Identity' })}</p>
             {!adminGeneralSecurityContainer.state.appSiteUrl && (
             {!adminGeneralSecurityContainer.state.appSiteUrl && (
               <div className="alert alert-danger">
               <div className="alert alert-danger">
                 <i
                 <i
                   className="icon-exclamation"
                   className="icon-exclamation"
                   // eslint-disable-next-line max-len
                   // eslint-disable-next-line max-len
-                  dangerouslySetInnerHTML={{ __html: t('security_setting.alert_siteUrl_is_not_set', { link: `<a href="/admin/app">${t('App settings')}<i class="icon-login"></i></a>` }) }}
+                  dangerouslySetInnerHTML={{ __html: t('security_setting.alert_siteUrl_is_not_set', { link: `<a href="/admin/app">${t('App Settings')}<i class="icon-login"></i></a>` }) }}
                 />
                 />
               </div>
               </div>
             )}
             )}
@@ -182,7 +180,7 @@ class SamlSecurityManagement extends React.Component {
                       value={this.state.envEntryPoint || ''}
                       value={this.state.envEntryPoint || ''}
                       readOnly
                       readOnly
                     />
                     />
-                    <p className="help-block">
+                    <p className="form-text text-muted">
                       <small dangerouslySetInnerHTML={{ __html: t('security_setting.SAML.Use env var if empty', { env: 'SAML_ENTRY_POINT' }) }} />
                       <small dangerouslySetInnerHTML={{ __html: t('security_setting.SAML.Use env var if empty', { env: 'SAML_ENTRY_POINT' }) }} />
                     </p>
                     </p>
                   </td>
                   </td>
@@ -206,7 +204,7 @@ class SamlSecurityManagement extends React.Component {
                       value={this.state.envIssuer || ''}
                       value={this.state.envIssuer || ''}
                       readOnly
                       readOnly
                     />
                     />
-                    <p className="help-block">
+                    <p className="form-text text-muted">
                       <small dangerouslySetInnerHTML={{ __html: t('security_setting.SAML.Use env var if empty', { env: 'SAML_ISSUER' }) }} />
                       <small dangerouslySetInnerHTML={{ __html: t('security_setting.SAML.Use env var if empty', { env: 'SAML_ISSUER' }) }} />
                     </p>
                     </p>
                   </td>
                   </td>
@@ -215,7 +213,7 @@ class SamlSecurityManagement extends React.Component {
                   <th>{t('security_setting.form_item_name.cert')}</th>
                   <th>{t('security_setting.form_item_name.cert')}</th>
                   <td>
                   <td>
                     <textarea
                     <textarea
-                      className="form-control input-sm"
+                      className="form-control form-control-sm"
                       type="text"
                       type="text"
                       rows="5"
                       rows="5"
                       name="samlCert"
                       name="samlCert"
@@ -223,7 +221,7 @@ class SamlSecurityManagement extends React.Component {
                       defaultValue={adminSamlSecurityContainer.state.samlCert}
                       defaultValue={adminSamlSecurityContainer.state.samlCert}
                       onChange={e => adminSamlSecurityContainer.changeSamlCert(e.target.value)}
                       onChange={e => adminSamlSecurityContainer.changeSamlCert(e.target.value)}
                     />
                     />
-                    <p className="help-block">
+                    <p>
                       <small>
                       <small>
                         {t('security_setting.SAML.cert_detail')}
                         {t('security_setting.SAML.cert_detail')}
                       </small>
                       </small>
@@ -231,7 +229,7 @@ class SamlSecurityManagement extends React.Component {
                     <div>
                     <div>
                       <small>
                       <small>
                         e.g.
                         e.g.
-                        <pre>{`-----BEGIN CERTIFICATE-----
+                        <pre className="well card">{`-----BEGIN CERTIFICATE-----
 MIICBzCCAXACCQD4US7+0A/b/zANBgkqhkiG9w0BAQsFADBIMQswCQYDVQQGEwJK
 MIICBzCCAXACCQD4US7+0A/b/zANBgkqhkiG9w0BAQsFADBIMQswCQYDVQQGEwJK
 UDEOMAwGA1UECAwFVG9reW8xFTATBgNVBAoMDFdFU0VFSywgSW5jLjESMBAGA1UE
 UDEOMAwGA1UECAwFVG9reW8xFTATBgNVBAoMDFdFU0VFSywgSW5jLjESMBAGA1UE
 ...
 ...
@@ -245,13 +243,13 @@ pWVdnzS1VCO8fKsJ7YYIr+JmHvseph3kFUOI5RqkCcMZlKUv83aUThsTHw==
                   </td>
                   </td>
                   <td>
                   <td>
                     <textarea
                     <textarea
-                      className="form-control input-sm"
+                      className="form-control form-control-sm"
                       type="text"
                       type="text"
                       rows="5"
                       rows="5"
                       readOnly
                       readOnly
                       value={this.state.envCert || ''}
                       value={this.state.envCert || ''}
                     />
                     />
-                    <p className="help-block">
+                    <p className="form-text text-muted">
                       <small dangerouslySetInnerHTML={{ __html: t('security_setting.SAML.Use env var if empty', { env: 'SAML_CERT' }) }} />
                       <small dangerouslySetInnerHTML={{ __html: t('security_setting.SAML.Use env var if empty', { env: 'SAML_CERT' }) }} />
                     </p>
                     </p>
                   </td>
                   </td>
@@ -283,7 +281,7 @@ pWVdnzS1VCO8fKsJ7YYIr+JmHvseph3kFUOI5RqkCcMZlKUv83aUThsTHw==
                       defaultValue={adminSamlSecurityContainer.state.samlAttrMapId}
                       defaultValue={adminSamlSecurityContainer.state.samlAttrMapId}
                       onChange={e => adminSamlSecurityContainer.changeSamlAttrMapId(e.target.value)}
                       onChange={e => adminSamlSecurityContainer.changeSamlAttrMapId(e.target.value)}
                     />
                     />
-                    <p className="help-block">
+                    <p className="form-text text-muted">
                       <small>
                       <small>
                         {t('security_setting.SAML.id_detail')}
                         {t('security_setting.SAML.id_detail')}
                       </small>
                       </small>
@@ -296,7 +294,7 @@ pWVdnzS1VCO8fKsJ7YYIr+JmHvseph3kFUOI5RqkCcMZlKUv83aUThsTHw==
                       value={this.state.envAttrMapId || ''}
                       value={this.state.envAttrMapId || ''}
                       readOnly
                       readOnly
                     />
                     />
-                    <p className="help-block">
+                    <p className="form-text text-muted">
                       <small dangerouslySetInnerHTML={{ __html: t('security_setting.SAML.Use env var if empty', { env: 'SAML_ATTR_MAPPING_ID' }) }} />
                       <small dangerouslySetInnerHTML={{ __html: t('security_setting.SAML.Use env var if empty', { env: 'SAML_ATTR_MAPPING_ID' }) }} />
                     </p>
                     </p>
                   </td>
                   </td>
@@ -311,7 +309,7 @@ pWVdnzS1VCO8fKsJ7YYIr+JmHvseph3kFUOI5RqkCcMZlKUv83aUThsTHw==
                       defaultValue={adminSamlSecurityContainer.state.samlAttrMapUsername}
                       defaultValue={adminSamlSecurityContainer.state.samlAttrMapUsername}
                       onChange={e => adminSamlSecurityContainer.changeSamlAttrMapUserName(e.target.value)}
                       onChange={e => adminSamlSecurityContainer.changeSamlAttrMapUserName(e.target.value)}
                     />
                     />
-                    <p className="help-block">
+                    <p className="form-text text-muted">
                       <small dangerouslySetInnerHTML={{ __html: t('security_setting.SAML.username_detail') }} />
                       <small dangerouslySetInnerHTML={{ __html: t('security_setting.SAML.username_detail') }} />
                     </p>
                     </p>
                   </td>
                   </td>
@@ -322,7 +320,7 @@ pWVdnzS1VCO8fKsJ7YYIr+JmHvseph3kFUOI5RqkCcMZlKUv83aUThsTHw==
                       value={this.state.envAttrMapUsername || ''}
                       value={this.state.envAttrMapUsername || ''}
                       readOnly
                       readOnly
                     />
                     />
-                    <p className="help-block">
+                    <p className="form-text text-muted">
                       <small dangerouslySetInnerHTML={{ __html: t('security_setting.SAML.Use env var if empty', { env: 'SAML_ATTR_MAPPING_USERNAME' }) }} />
                       <small dangerouslySetInnerHTML={{ __html: t('security_setting.SAML.Use env var if empty', { env: 'SAML_ATTR_MAPPING_USERNAME' }) }} />
                     </p>
                     </p>
                   </td>
                   </td>
@@ -337,7 +335,7 @@ pWVdnzS1VCO8fKsJ7YYIr+JmHvseph3kFUOI5RqkCcMZlKUv83aUThsTHw==
                       defaultValue={adminSamlSecurityContainer.state.samlAttrMapMail}
                       defaultValue={adminSamlSecurityContainer.state.samlAttrMapMail}
                       onChange={e => adminSamlSecurityContainer.changeSamlAttrMapMail(e.target.value)}
                       onChange={e => adminSamlSecurityContainer.changeSamlAttrMapMail(e.target.value)}
                     />
                     />
-                    <p className="help-block">
+                    <p className="form-text text-muted">
                       <small dangerouslySetInnerHTML={{ __html: t('security_setting.SAML.mapping_detail', { target: 'Email' }) }} />
                       <small dangerouslySetInnerHTML={{ __html: t('security_setting.SAML.mapping_detail', { target: 'Email' }) }} />
                     </p>
                     </p>
                   </td>
                   </td>
@@ -348,7 +346,7 @@ pWVdnzS1VCO8fKsJ7YYIr+JmHvseph3kFUOI5RqkCcMZlKUv83aUThsTHw==
                       value={this.state.envAttrMapMail || ''}
                       value={this.state.envAttrMapMail || ''}
                       readOnly
                       readOnly
                     />
                     />
-                    <p className="help-block">
+                    <p className="form-text text-muted">
                       <small dangerouslySetInnerHTML={{ __html: t('security_setting.SAML.Use env var if empty', { env: 'SAML_ATTR_MAPPING_MAIL' }) }} />
                       <small dangerouslySetInnerHTML={{ __html: t('security_setting.SAML.Use env var if empty', { env: 'SAML_ATTR_MAPPING_MAIL' }) }} />
                     </p>
                     </p>
                   </td>
                   </td>
@@ -363,7 +361,7 @@ pWVdnzS1VCO8fKsJ7YYIr+JmHvseph3kFUOI5RqkCcMZlKUv83aUThsTHw==
                       defaultValue={adminSamlSecurityContainer.state.samlAttrMapFirstName}
                       defaultValue={adminSamlSecurityContainer.state.samlAttrMapFirstName}
                       onChange={e => adminSamlSecurityContainer.changeSamlAttrMapFirstName(e.target.value)}
                       onChange={e => adminSamlSecurityContainer.changeSamlAttrMapFirstName(e.target.value)}
                     />
                     />
-                    <p className="help-block">
+                    <p className="form-text text-muted">
                       {/* eslint-disable-next-line max-len */}
                       {/* eslint-disable-next-line max-len */}
                       <small dangerouslySetInnerHTML={{ __html: t('security_setting.SAML.mapping_detail', { target: t('security_setting.form_item_name.attrMapFirstName') }) }} />
                       <small dangerouslySetInnerHTML={{ __html: t('security_setting.SAML.mapping_detail', { target: t('security_setting.form_item_name.attrMapFirstName') }) }} />
                     </p>
                     </p>
@@ -375,7 +373,7 @@ pWVdnzS1VCO8fKsJ7YYIr+JmHvseph3kFUOI5RqkCcMZlKUv83aUThsTHw==
                       value={this.state.envAttrMapFirstName || ''}
                       value={this.state.envAttrMapFirstName || ''}
                       readOnly
                       readOnly
                     />
                     />
-                    <p className="help-block">
+                    <p className="form-text text-muted">
                       <small>
                       <small>
                         <span dangerouslySetInnerHTML={{ __html: t('security_setting.SAML.Use env var if empty', { env: 'SAML_ATTR_MAPPING_FIRST_NAME' }) }} />
                         <span dangerouslySetInnerHTML={{ __html: t('security_setting.SAML.Use env var if empty', { env: 'SAML_ATTR_MAPPING_FIRST_NAME' }) }} />
                         <br />
                         <br />
@@ -394,7 +392,7 @@ pWVdnzS1VCO8fKsJ7YYIr+JmHvseph3kFUOI5RqkCcMZlKUv83aUThsTHw==
                       defaultValue={adminSamlSecurityContainer.state.samlAttrMapLastName}
                       defaultValue={adminSamlSecurityContainer.state.samlAttrMapLastName}
                       onChange={e => adminSamlSecurityContainer.changeSamlAttrMapLastName(e.target.value)}
                       onChange={e => adminSamlSecurityContainer.changeSamlAttrMapLastName(e.target.value)}
                     />
                     />
-                    <p className="help-block">
+                    <p className="form-text text-muted">
                       {/* eslint-disable-next-line max-len */}
                       {/* eslint-disable-next-line max-len */}
                       <small dangerouslySetInnerHTML={{ __html: t('security_setting.SAML.mapping_detail', { target: t('security_setting.form_item_name.attrMapLastName') }) }} />
                       <small dangerouslySetInnerHTML={{ __html: t('security_setting.SAML.mapping_detail', { target: t('security_setting.form_item_name.attrMapLastName') }) }} />
                     </p>
                     </p>
@@ -406,7 +404,7 @@ pWVdnzS1VCO8fKsJ7YYIr+JmHvseph3kFUOI5RqkCcMZlKUv83aUThsTHw==
                       value={this.state.envAttrMapLastName || ''}
                       value={this.state.envAttrMapLastName || ''}
                       readOnly
                       readOnly
                     />
                     />
-                    <p className="help-block">
+                    <p className="form-text text-muted">
                       <small>
                       <small>
                         <span dangerouslySetInnerHTML={{ __html: t('security_setting.SAML.Use env var if empty', { env: 'SAML_ATTR_MAPPING_LAST_NAME' }) }} />
                         <span dangerouslySetInnerHTML={{ __html: t('security_setting.SAML.Use env var if empty', { env: 'SAML_ATTR_MAPPING_LAST_NAME' }) }} />
                         <br />
                         <br />
@@ -422,41 +420,45 @@ pWVdnzS1VCO8fKsJ7YYIr+JmHvseph3kFUOI5RqkCcMZlKUv83aUThsTHw==
               Attribute Mapping Options
               Attribute Mapping Options
             </h3>
             </h3>
 
 
-            <div className="row mb-5">
-              <div className="col-xs-offset-3 col-xs-6 text-left">
-                <div className="checkbox checkbox-success">
+            <div className="row form-group mb-5">
+              <div className="offset-md-3 col-md-6 text-left">
+                <div className="custom-control custom-checkbox custom-checkbox-success">
                   <input
                   <input
                     id="bindByUserName-SAML"
                     id="bindByUserName-SAML"
+                    className="custom-control-input"
                     type="checkbox"
                     type="checkbox"
                     checked={adminSamlSecurityContainer.state.isSameUsernameTreatedAsIdenticalUser || false}
                     checked={adminSamlSecurityContainer.state.isSameUsernameTreatedAsIdenticalUser || false}
                     onChange={() => { adminSamlSecurityContainer.switchIsSameUsernameTreatedAsIdenticalUser() }}
                     onChange={() => { adminSamlSecurityContainer.switchIsSameUsernameTreatedAsIdenticalUser() }}
                   />
                   />
                   <label
                   <label
+                    className="custom-control-label"
                     htmlFor="bindByUserName-SAML"
                     htmlFor="bindByUserName-SAML"
                     dangerouslySetInnerHTML={{ __html: t('security_setting.Treat username matching as identical') }}
                     dangerouslySetInnerHTML={{ __html: t('security_setting.Treat username matching as identical') }}
                   />
                   />
                 </div>
                 </div>
-                <p className="help-block">
+                <p className="form-text text-muted">
                   <small dangerouslySetInnerHTML={{ __html: t('security_setting.Treat username matching as identical_warn') }} />
                   <small dangerouslySetInnerHTML={{ __html: t('security_setting.Treat username matching as identical_warn') }} />
                 </p>
                 </p>
               </div>
               </div>
             </div>
             </div>
 
 
-            <div className="row mb-5">
-              <div className="col-xs-offset-3 col-xs-6 text-left">
-                <div className="checkbox checkbox-success">
+            <div className="row form-group mb-5">
+              <div className="offset-md-3 col-md-6 text-left">
+                <div className="custom-control custom-checkbox custom-checkbox-success">
                   <input
                   <input
                     id="bindByEmail-SAML"
                     id="bindByEmail-SAML"
+                    className="custom-control-input"
                     type="checkbox"
                     type="checkbox"
                     checked={adminSamlSecurityContainer.state.isSameEmailTreatedAsIdenticalUser || false}
                     checked={adminSamlSecurityContainer.state.isSameEmailTreatedAsIdenticalUser || false}
                     onChange={() => { adminSamlSecurityContainer.switchIsSameEmailTreatedAsIdenticalUser() }}
                     onChange={() => { adminSamlSecurityContainer.switchIsSameEmailTreatedAsIdenticalUser() }}
                   />
                   />
                   <label
                   <label
+                    className="custom-control-label"
                     htmlFor="bindByEmail-SAML"
                     htmlFor="bindByEmail-SAML"
                     dangerouslySetInnerHTML={{ __html: t('security_setting.Treat email matching as identical') }}
                     dangerouslySetInnerHTML={{ __html: t('security_setting.Treat email matching as identical') }}
                   />
                   />
                 </div>
                 </div>
-                <p className="help-block">
+                <p className="form-text text-muted">
                   <small dangerouslySetInnerHTML={{ __html: t('security_setting.Treat email matching as identical_warn') }} />
                   <small dangerouslySetInnerHTML={{ __html: t('security_setting.Treat email matching as identical_warn') }} />
                 </p>
                 </p>
               </div>
               </div>
@@ -466,7 +468,7 @@ pWVdnzS1VCO8fKsJ7YYIr+JmHvseph3kFUOI5RqkCcMZlKUv83aUThsTHw==
               Attribute-based Login Control
               Attribute-based Login Control
             </h3>
             </h3>
 
 
-            <p className="help-block">
+            <p className="form-text text-muted">
               <small dangerouslySetInnerHTML={{ __html: t('security_setting.SAML.attr_based_login_control_detail') }} />
               <small dangerouslySetInnerHTML={{ __html: t('security_setting.SAML.attr_based_login_control_detail') }} />
             </p>
             </p>
 
 
@@ -492,7 +494,7 @@ pWVdnzS1VCO8fKsJ7YYIr+JmHvseph3kFUOI5RqkCcMZlKUv83aUThsTHw==
                       onChange={(e) => { adminSamlSecurityContainer.changeSamlABLCRule(e.target.value) }}
                       onChange={(e) => { adminSamlSecurityContainer.changeSamlABLCRule(e.target.value) }}
                       readOnly={useOnlyEnvVars}
                       readOnly={useOnlyEnvVars}
                     />
                     />
-                    <p className="help-block">
+                    <p className="form-text text-muted">
                       <small>
                       <small>
                         <span dangerouslySetInnerHTML={{ __html: t('security_setting.SAML.attr_based_login_control_rule_detail') }} />
                         <span dangerouslySetInnerHTML={{ __html: t('security_setting.SAML.attr_based_login_control_rule_detail') }} />
                         <span dangerouslySetInnerHTML={{ __html: t('security_setting.SAML.attr_based_login_control_rule_example') }} />
                         <span dangerouslySetInnerHTML={{ __html: t('security_setting.SAML.attr_based_login_control_rule_example') }} />
@@ -506,7 +508,7 @@ pWVdnzS1VCO8fKsJ7YYIr+JmHvseph3kFUOI5RqkCcMZlKUv83aUThsTHw==
                       value={this.state.envABLCRule || ''}
                       value={this.state.envABLCRule || ''}
                       readOnly
                       readOnly
                     />
                     />
-                    <p className="help-block">
+                    <p className="form-text text-muted">
                       <small dangerouslySetInnerHTML={{ __html: t('security_setting.SAML.Use env var if empty', { env: 'SAML_ABLC_RULE' }) }} />
                       <small dangerouslySetInnerHTML={{ __html: t('security_setting.SAML.Use env var if empty', { env: 'SAML_ABLC_RULE' }) }} />
                     </p>
                     </p>
                   </td>
                   </td>
@@ -515,7 +517,7 @@ pWVdnzS1VCO8fKsJ7YYIr+JmHvseph3kFUOI5RqkCcMZlKUv83aUThsTHw==
             </table>
             </table>
 
 
             <div className="row my-3">
             <div className="row my-3">
-              <div className="col-xs-offset-3 col-xs-5">
+              <div className="offset-3 col-5">
                 <button
                 <button
                   type="button"
                   type="button"
                   className="btn btn-primary"
                   className="btn btn-primary"

+ 131 - 63
src/client/js/components/Admin/Security/SecurityManagement.jsx

@@ -1,6 +1,9 @@
 import React, { Fragment } from 'react';
 import React, { Fragment } from 'react';
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
 import { withTranslation } from 'react-i18next';
 import { withTranslation } from 'react-i18next';
+import {
+  TabContent, TabPane, Nav, NavItem, NavLink,
+} from 'reactstrap';
 
 
 import { createSubscribedElement } from '../../UnstatedUtils';
 import { createSubscribedElement } from '../../UnstatedUtils';
 
 
@@ -18,13 +21,27 @@ import FacebookSecuritySetting from './FacebookSecuritySetting';
 
 
 class SecurityManagement extends React.Component {
 class SecurityManagement extends React.Component {
 
 
-  constructor(props) {
+  constructor() {
     super();
     super();
 
 
+    this.state = {
+      activeTab: 'passport-local',
+      // Prevent unnecessary rendering
+      activeComponents: new Set(['passport-local']),
+    };
+
+    this.toggleActiveTab = this.toggleActiveTab.bind(this);
+  }
+
+  toggleActiveTab(activeTab) {
+    this.setState({
+      activeTab, activeComponents: this.state.activeComponents.add(activeTab),
+    });
   }
   }
 
 
   render() {
   render() {
     const { t } = this.props;
     const { t } = this.props;
+    const { activeTab, activeComponents } = this.state;
     return (
     return (
       <Fragment>
       <Fragment>
         <div>
         <div>
@@ -41,69 +58,120 @@ class SecurityManagement extends React.Component {
           </div>
           </div>
         </div>
         </div>
 
 
-        {/* TODO GW-226 adapt BS4 */}
-        <div className="auth-mechanism-configurations m-t-10">
+        <div className="auth-mechanism-configurations">
           <h2 className="border-bottom">{t('security_setting.Authentication mechanism settings')}</h2>
           <h2 className="border-bottom">{t('security_setting.Authentication mechanism settings')}</h2>
-          <div className="passport-settings">
-            <ul className="nav nav-tabs" role="tablist">
-              <li className="active">
-                <a href="#passport-local" data-toggle="tab" role="tab"><i className="fa fa-users"></i> ID/Pass</a>
-              </li>
-              <li>
-                <a href="#passport-ldap" data-toggle="tab" role="tab"><i className="fa fa-sitemap"></i> LDAP</a>
-              </li>
-              <li>
-                <a href="#passport-saml" data-toggle="tab" role="tab"><i className="fa fa-key"></i> SAML</a>
-              </li>
-              <li>
-                <a href="#passport-oidc" data-toggle="tab" role="tab"><i className="fa fa-openid"></i> OIDC</a>
-              </li>
-              <li>
-                <a href="#passport-basic" data-toggle="tab" role="tab"><i className="fa fa-lock"></i> Basic</a>
-              </li>
-              <li>
-                <a href="#passport-google-oauth" data-toggle="tab" role="tab"><i className="fa fa-google"></i> Google</a>
-              </li>
-              <li>
-                <a href="#passport-github" data-toggle="tab" role="tab"><i className="fa fa-github"></i> GitHub</a>
-              </li>
-              <li>
-                <a href="#passport-twitter" data-toggle="tab" role="tab"><i className="fa fa-twitter"></i> Twitter</a>
-              </li>
-              <li className="tbd">
-                <a href="#passport-facebook" data-toggle="tab" role="tab"><i className="fa fa-facebook"></i> (TBD) Facebook</a>
-              </li>
-            </ul>
-            <div className="tab-content p-t-10">
-              <div id="passport-local" className="tab-pane active" role="tabpanel">
-                <LocalSecuritySetting />
-              </div>
-              <div id="passport-ldap" className="tab-pane" role="tabpanel">
-                <LdapSecuritySetting />
-              </div>
-              <div id="passport-saml" className="tab-pane" role="tabpanel">
-                <SamlSecuritySetting />
-              </div>
-              <div id="passport-oidc" className="tab-pane" role="tabpanel">
-                <OidcSecuritySetting />
-              </div>
-              <div id="passport-basic" className="tab-pane" role="tabpanel">
-                <BasicSecuritySetting />
-              </div>
-              <div id="passport-google-oauth" className="tab-pane" role="tabpanel">
-                <GoogleSecuritySetting />
-              </div>
-              <div id="passport-github" className="tab-pane" role="tabpanel">
-                <GitHubSecuritySetting />
-              </div>
-              <div id="passport-twitter" className="tab-pane" role="tabpanel">
-                <TwitterSecuritySetting />
-              </div>
-              <div id="passport-facebook" className="tab-pane" role="tabpanel">
-                <FacebookSecuritySetting />
-              </div>
-            </div>
-          </div>
+          <Nav tabs>
+            <NavItem>
+              <NavLink
+                className={`${activeTab === 'passport-local' && 'active'} `}
+                onClick={() => { this.toggleActiveTab('passport-local') }}
+                href="#passport-local"
+              >
+                <i className="fa fa-users" /> ID/Pass
+              </NavLink>
+            </NavItem>
+            <NavItem>
+              <NavLink
+                className={`${activeTab === 'passport-ldap' && 'active'} `}
+                onClick={() => { this.toggleActiveTab('passport-ldap') }}
+                href="#passport-ldap"
+              >
+                <i className="fa fa-sitemap" /> LDAP
+              </NavLink>
+            </NavItem>
+            <NavItem>
+              <NavLink
+                className={`${activeTab === 'passport-saml' && 'active'} `}
+                onClick={() => { this.toggleActiveTab('passport-saml') }}
+                href="#passport-saml"
+              >
+                <i className="fa fa-key" /> SAML
+              </NavLink>
+            </NavItem>
+            <NavItem>
+              <NavLink
+                className={`${activeTab === 'passport-oidc' && 'active'} `}
+                onClick={() => { this.toggleActiveTab('passport-oidc') }}
+                href="#passport-oidc"
+              >
+                <i className="fa fa-openid" /> OIDC
+              </NavLink>
+            </NavItem>
+            <NavItem>
+              <NavLink
+                className={`${activeTab === 'passport-basic' && 'active'} `}
+                onClick={() => { this.toggleActiveTab('passport-basic') }}
+                href="#passport-basic"
+              >
+                <i className="fa fa-lock" /> BASIC
+              </NavLink>
+            </NavItem>
+            <NavItem>
+              <NavLink
+                className={`${activeTab === 'passport-google' && 'active'} `}
+                onClick={() => { this.toggleActiveTab('passport-google') }}
+                href="#passport-google"
+              >
+                <i className="fa fa-google" /> Google
+              </NavLink>
+            </NavItem>
+            <NavItem>
+              <NavLink
+                className={`${activeTab === 'passport-github' && 'active'} `}
+                onClick={() => { this.toggleActiveTab('passport-github') }}
+                href="#passport-github"
+              >
+                <i className="fa fa-github" /> GitHub
+              </NavLink>
+            </NavItem>
+            <NavItem>
+              <NavLink
+                className={`${activeTab === 'passport-twitter' && 'active'} `}
+                onClick={() => { this.toggleActiveTab('passport-twitter') }}
+                href="#passport-twitter"
+              >
+                <i className="fa fa-twitter" /> Twitter
+              </NavLink>
+            </NavItem>
+            <NavItem>
+              <NavLink
+                className={`${activeTab === 'passport-facebook' && 'active'} `}
+                onClick={() => { this.toggleActiveTab('passport-facebook') }}
+                href="#passport-facebook"
+              >
+                <i className="fa fa-facebook" /> (TBD) Facebook
+              </NavLink>
+            </NavItem>
+          </Nav>
+          <TabContent activeTab={activeTab} className="mt-2">
+            <TabPane tabId="passport-local">
+              {activeComponents.has('passport-local') && <LocalSecuritySetting />}
+            </TabPane>
+            <TabPane tabId="passport-ldap">
+              {activeComponents.has('passport-ldap') && <LdapSecuritySetting />}
+            </TabPane>
+            <TabPane tabId="passport-saml">
+              {activeComponents.has('passport-saml') && <SamlSecuritySetting />}
+            </TabPane>
+            <TabPane tabId="passport-oidc">
+              {activeComponents.has('passport-oidc') && <OidcSecuritySetting />}
+            </TabPane>
+            <TabPane tabId="passport-basic">
+              {activeComponents.has('passport-basic') && <BasicSecuritySetting />}
+            </TabPane>
+            <TabPane tabId="passport-google">
+              {activeComponents.has('passport-google') && <GoogleSecuritySetting />}
+            </TabPane>
+            <TabPane tabId="passport-github">
+              {activeComponents.has('passport-github') && <GitHubSecuritySetting />}
+            </TabPane>
+            <TabPane tabId="passport-twitter">
+              {activeComponents.has('passport-twitter') && <TwitterSecuritySetting />}
+            </TabPane>
+            <TabPane tabId="passport-facebook">
+              {activeComponents.has('passport-facebook') && <FacebookSecuritySetting />}
+            </TabPane>
+          </TabContent>
         </div>
         </div>
       </Fragment>
       </Fragment>
     );
     );

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

@@ -49,166 +49,164 @@ class SecuritySetting extends React.Component {
 
 
     return (
     return (
       <React.Fragment>
       <React.Fragment>
-        <fieldset>
-          <h2 className="alert-anchor border-bottom">
-            {t('security_settings')}
-          </h2>
-          {this.state.retrieveError != null && (
-            <div className="alert alert-danger">
-              <p>{t('Error occurred')} : {this.state.retrieveError}</p>
-            </div>
+        <h2 className="alert-anchor border-bottom">
+          {t('security_settings')}
+        </h2>
+        {this.state.retrieveError != null && (
+        <div className="alert alert-danger">
+          <p>{t('Error occurred')} : {this.state.retrieveError}</p>
+        </div>
           )}
           )}
-          <div className="row">
-            <strong className="col-xs-3 text-right"> {t('security_setting.Guest Users Access')} </strong>
-            <div className="col-xs-9 text-left">
-              <div className="my-0 btn-group">
-                <div className="dropdown">
-                  <button
-                    className={`btn btn-default dropdown-toggle w-100 ${adminGeneralSecurityContainer.isWikiModeForced && 'disabled'}`}
-                    type="button"
-                    data-toggle="dropdown"
-                    aria-haspopup="true"
-                    aria-expanded="false"
-                  >
-                    <span className="pull-left">
-                      {currentRestrictGuestMode === 'Deny' && t('security_setting.guest_mode.deny')}
-                      {currentRestrictGuestMode === 'Readonly' && t('security_setting.guest_mode.readonly')}
-                    </span>
-                    <span className="bs-caret pull-right">
-                      <span className="caret" />
-                    </span>
-                  </button>
-                  {/* TODO adjust dropdown after BS4 */}
-                  <ul className="dropdown-menu" role="menu">
-                    <li
-                      key="Deny"
-                      role="presentation"
-                      type="button"
-                      onClick={() => { adminGeneralSecurityContainer.changeRestrictGuestMode('Deny') }}
-                    >
-                      <a role="menuitem">{t('security_setting.guest_mode.deny')}</a>
-                    </li>
-                    <li
-                      key="Readonly"
-                      role="presentation"
-                      type="button"
-                      onClick={() => { adminGeneralSecurityContainer.changeRestrictGuestMode('Readonly') }}
-                    >
-                      <a role="menuitem">{t('security_setting.guest_mode.readonly')}</a>
-                    </li>
-                  </ul>
+
+        <h4 className="mt-4">
+          { t('page_list_and_search_results') }
+        </h4>
+        <table className="table table-bordered col-lg-9 mb-5">
+          <thead>
+            <tr>
+              <th scope="col">{ t('scope_of_page_disclosure') }</th>
+              <th scope="col">{ t('set_point') }</th>
+            </tr>
+          </thead>
+          <tbody>
+            <tr>
+              <th scope="row">{ t('Public') }</th>
+              <td>{ t('always_displayed') }</td>
+            </tr>
+            <tr>
+              <th scope="row">{ t('Anyone with the link') }</th>
+              <td>{ t('always_hidden') }</td>
+            </tr>
+            <tr>
+              <th scope="row">{ t('Just me') }</th>
+              <td>
+                <div className="custom-control custom-switch custom-checkbox-success">
+                  <input
+                    type="checkbox"
+                    className="custom-control-input"
+                    id="isShowRestrictedByOwner"
+                    checked={adminGeneralSecurityContainer.state.isShowRestrictedByOwner}
+                    onChange={() => { adminGeneralSecurityContainer.switchIsShowRestrictedByOwner() }}
+                  />
+                  <label className="custom-control-label" htmlFor="isShowRestrictedByOwner">
+                    {t('displayed_or_hidden')}
+                  </label>
                 </div>
                 </div>
-              </div>
-            </div>
-          </div>
-          {adminGeneralSecurityContainer.isWikiModeForced && (
-            <div className="row mb-5">
-              <div className="col-xs-offset-3 col-xs-6 text-left">
-                <p className="alert alert-warning mt-2 text-left">
-                  <i className="icon-exclamation icon-fw">
-                  </i><b>FIXED</b><br />
-                  <b
-                    dangerouslySetInnerHTML={{
-                    __html: t('security_setting.Fixed by env var',
-                    { forcewikimode: 'FORCE_WIKI_MODE', wikimode: adminGeneralSecurityContainer.state.wikiMode }),
-                    }}
+              </td>
+            </tr>
+            <tr>
+              <th scope="row">{ t('Only inside the group') }</th>
+              <td>
+                <div className="custom-control custom-switch custom-checkbox-success">
+                  <input
+                    type="checkbox"
+                    className="custom-control-input"
+                    id="isShowRestrictedByGroup"
+                    checked={adminGeneralSecurityContainer.state.isShowRestrictedByGroup}
+                    onChange={() => { adminGeneralSecurityContainer.switchIsShowRestrictedByGroup() }}
                   />
                   />
-                </p>
+                  <label className="custom-control-label" htmlFor="isShowRestrictedByGroup">
+                    {t('displayed_or_hidden')}
+                  </label>
+                </div>
+              </td>
+            </tr>
+          </tbody>
+        </table>
+        <h4>{t('page_access_and_delete_rights')}</h4>
+        <div className="row mb-4">
+          <div className="col-md-3 text-md-right py-2">
+            <strong>{t('security_setting.Guest Users Access')}</strong>
+          </div>
+          <div className="col-md-9">
+            <div className="dropdown">
+              <button
+                className={`btn btn-outline-secondary dropdown-toggle text-right col-12
+                            col-md-auto ${adminGeneralSecurityContainer.isWikiModeForced && 'disabled'}`}
+                type="button"
+                id="dropdownMenuButton"
+                data-toggle="dropdown"
+                aria-haspopup="true"
+                aria-expanded="true"
+              >
+                <span className="float-left">
+                  {currentRestrictGuestMode === 'Deny' && t('security_setting.guest_mode.deny')}
+                  {currentRestrictGuestMode === 'Readonly' && t('security_setting.guest_mode.readonly')}
+                </span>
+              </button>
+              <div className="dropdown-menu" aria-labelledby="dropdownMenuButton">
+                <a className="dropdown-item" type="button" onClick={() => { adminGeneralSecurityContainer.changeRestrictGuestMode('Deny') }}>
+                  {t('security_setting.guest_mode.deny')}
+                </a>
+                <a className="dropdown-item" type="button" onClick={() => { adminGeneralSecurityContainer.changeRestrictGuestMode('Readonly') }}>
+                  {t('security_setting.guest_mode.readonly')}
+                </a>
               </div>
               </div>
             </div>
             </div>
-          )}
-          <div className="row mb-5">
-            <strong className="col-xs-3 text-right" dangerouslySetInnerHTML={{ __html: t('security_setting.page_listing_1') }} />
-            <div className="col-xs-6 text-left">
-              <div className="checkbox checkbox-success">
-                <input
-                  id="isShowRestrictedByOwner"
-                  type="checkbox"
-                  checked={adminGeneralSecurityContainer.state.isShowRestrictedByOwner}
-                  onChange={() => { adminGeneralSecurityContainer.switchIsShowRestrictedByOwner() }}
+            {adminGeneralSecurityContainer.isWikiModeForced && (
+              <p className="alert alert-warning mt-2 text-left offset-3 col-6">
+                <i className="icon-exclamation icon-fw">
+                </i><b>FIXED</b><br />
+                <b
+                  dangerouslySetInnerHTML={{
+                    __html: t('security_setting.Fixed by env var',
+                      { forcewikimode: 'FORCE_WIKI_MODE', wikimode: adminGeneralSecurityContainer.state.wikiMode }),
+                  }}
                 />
                 />
-                <label htmlFor="isShowRestrictedByOwner">
-                  {t('security_setting.page_listing_1_desc')}
-                </label>
-              </div>
-            </div>
+              </p>
+            )}
           </div>
           </div>
+        </div>
 
 
-          <div className="row mb-5">
-            <strong className="col-xs-3 text-right" dangerouslySetInnerHTML={{ __html: t('security_setting.page_listing_2') }} />
-            <div className="col-xs-6 text-left">
-              <div className="checkbox checkbox-success">
-                <input
-                  id="isShowRestrictedByGroup"
-                  type="checkbox"
-                  checked={adminGeneralSecurityContainer.state.isShowRestrictedByGroup}
-                  onChange={() => { adminGeneralSecurityContainer.switchIsShowRestrictedByGroup() }}
-                />
-                <label htmlFor="isShowRestrictedByGroup">
-                  {t('security_setting.page_listing_2_desc')}
-                </label>
-              </div>
-            </div>
+        <div className="row mb-4">
+          <div className="col-md-3 text-md-right mb-2">
+            <strong>{t('security_setting.complete_deletion')}</strong>
           </div>
           </div>
-
-          <div className="row mb-5">
-            <strong className="col-xs-3 text-right"> {t('security_setting.complete_deletion')} </strong>
-            <div className="col-xs-9 text-left">
-              <div className="my-0 btn-group">
-                <div className="dropdown">
-                  <button className="btn btn-default dropdown-toggle w-100" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
-                    <span className="pull-left">
-                      {currentPageCompleteDeletionAuthority === 'anyOne' && t('security_setting.anyone')}
-                      {currentPageCompleteDeletionAuthority === 'adminOnly' && t('security_setting.admin_only')}
-                      {(currentPageCompleteDeletionAuthority === 'adminAndAuthor' || currentPageCompleteDeletionAuthority == null)
-                        && t('security_setting.admin_and_author')}
-                    </span>
-                    <span className="bs-caret pull-right">
-                      <span className="caret" />
-                    </span>
-                  </button>
-                  {/* TODO adjust dropdown after BS4 */}
-                  <ul className="dropdown-menu" role="menu">
-                    <li
-                      key="anyone"
-                      role="presentation"
-                      type="button"
-                      onClick={() => { adminGeneralSecurityContainer.changePageCompleteDeletionAuthority('anyOne') }}
-                    >
-                      <a role="menuitem">{t('security_setting.anyone')}</a>
-                    </li>
-                    <li
-                      key="admin_only"
-                      role="presentation"
-                      type="button"
-                      onClick={() => { adminGeneralSecurityContainer.changePageCompleteDeletionAuthority('adminOnly') }}
-                    >
-                      <a role="menuitem">{t('security_setting.admin_only')}</a>
-                    </li>
-                    <li
-                      key="admin_and_author"
-                      role="presentation"
-                      type="button"
-                      onClick={() => { adminGeneralSecurityContainer.changePageCompleteDeletionAuthority('adminAndAuthor') }}
-                    >
-                      <a role="menuitem">{t('security_setting.admin_and_author')}</a>
-                    </li>
-                  </ul>
-                </div>
-                <p className="help-block small">
-                  {t('security_setting.complete_deletion_explain')}
-                </p>
+          <div className="col-md-6">
+            <div className="dropdown">
+              <button
+                className="btn btn-outline-secondary dropdown-toggle text-right col-12 col-md-auto"
+                type="button"
+                id="dropdownMenuButton"
+                data-toggle="dropdown"
+                aria-haspopup="true"
+                aria-expanded="true"
+              >
+                <span className="float-left">
+                  {currentPageCompleteDeletionAuthority === 'anyOne' && t('security_setting.anyone')}
+                  {currentPageCompleteDeletionAuthority === 'adminOnly' && t('security_setting.admin_only')}
+                  {(currentPageCompleteDeletionAuthority === 'adminAndAuthor' || currentPageCompleteDeletionAuthority == null)
+                      && t('security_setting.admin_and_author')}
+                </span>
+              </button>
+              <div className="dropdown-menu" aria-labelledby="dropdownMenuButton">
+                <a className="dropdown-item" type="button" onClick={() => { adminGeneralSecurityContainer.changePageCompleteDeletionAuthority('anyOne') }}>
+                  {t('security_setting.anyone')}
+                </a>
+                <a className="dropdown-item" type="button" onClick={() => { adminGeneralSecurityContainer.changePageCompleteDeletionAuthority('adminOnly') }}>
+                  {t('security_setting.admin_only')}
+                </a>
+                <a
+                  className="dropdown-item"
+                  type="button"
+                  onClick={() => { adminGeneralSecurityContainer.changePageCompleteDeletionAuthority('adminAndAuthor') }}
+                >
+                  {t('security_setting.admin_and_author')}
+                </a>
               </div>
               </div>
+              <p className="form-text text-muted small">
+                {t('security_setting.complete_deletion_explain')}
+              </p>
             </div>
             </div>
           </div>
           </div>
-          <div className="row my-3">
-            <div className="col-xs-offset-3 col-xs-5">
-              <button type="submit" className="btn btn-primary" disabled={this.state.retrieveError != null} onClick={this.putSecuritySetting}>
-                {t('Update')}
-              </button>
-            </div>
+        </div>
+        <div className="row my-3">
+          <div className="text-center text-md-left offset-md-3 col-md-5">
+            <button type="button" className="btn btn-primary" disabled={this.state.retrieveError != null} onClick={this.putSecuritySetting}>
+              {t('Update')}
+            </button>
           </div>
           </div>
-        </fieldset>
+        </div>
       </React.Fragment>
       </React.Fragment>
     );
     );
   }
   }

+ 22 - 22
src/client/js/components/Admin/Security/TwitterSecuritySetting.jsx

@@ -68,43 +68,41 @@ class TwitterSecurityManagement extends React.Component {
           </div>
           </div>
         )}
         )}
 
 
-        <div className="row mb-5">
-          <div className="col-xs-3 my-3 text-right">
-            <strong>{t('security_setting.OAuth.Twitter.name')}</strong>
-          </div>
-          <div className="col-xs-6 text-left">
-            <div className="checkbox checkbox-success">
+        <div className="form-group row">
+          <div className="col-6 offset-3">
+            <div className="custom-control custom-switch custom-checkbox-success">
               <input
               <input
                 id="isTwitterEnabled"
                 id="isTwitterEnabled"
+                className="custom-control-input"
                 type="checkbox"
                 type="checkbox"
                 checked={adminGeneralSecurityContainer.state.isTwitterEnabled}
                 checked={adminGeneralSecurityContainer.state.isTwitterEnabled}
                 onChange={() => { adminGeneralSecurityContainer.switchIsTwitterOAuthEnabled() }}
                 onChange={() => { adminGeneralSecurityContainer.switchIsTwitterOAuthEnabled() }}
               />
               />
-              <label htmlFor="isTwitterEnabled">
+              <label className="custom-control-label" htmlFor="isTwitterEnabled">
                 {t('security_setting.OAuth.Twitter.enable_twitter')}
                 {t('security_setting.OAuth.Twitter.enable_twitter')}
               </label>
               </label>
             </div>
             </div>
             {(!adminGeneralSecurityContainer.state.setupStrategies.includes('twitter') && isTwitterEnabled)
             {(!adminGeneralSecurityContainer.state.setupStrategies.includes('twitter') && isTwitterEnabled)
-              && <div className="label label-warning">{t('security_setting.setup_is_not_yet_complete')}</div>}
+              && <div className="badge badge-warning">{t('security_setting.setup_is_not_yet_complete')}</div>}
           </div>
           </div>
         </div>
         </div>
 
 
         <div className="row mb-5">
         <div className="row mb-5">
-          <label className="col-xs-3 text-right">{t('security_setting.callback_URL')}</label>
-          <div className="col-xs-6">
+          <label className="col-md-3 text-md-right py-2">{t('security_setting.callback_URL')}</label>
+          <div className="col-md-6">
             <input
             <input
               className="form-control"
               className="form-control"
               type="text"
               type="text"
               value={adminTwitterSecurityContainer.state.callbackUrl}
               value={adminTwitterSecurityContainer.state.callbackUrl}
               readOnly
               readOnly
             />
             />
-            <p className="help-block small">{t('security_setting.desc_of_callback_URL', { AuthName: 'OAuth' })}</p>
+            <p className="form-text text-muted small">{t('security_setting.desc_of_callback_URL', { AuthName: 'OAuth' })}</p>
             {!adminGeneralSecurityContainer.state.appSiteUrl && (
             {!adminGeneralSecurityContainer.state.appSiteUrl && (
               <div className="alert alert-danger">
               <div className="alert alert-danger">
                 <i
                 <i
                   className="icon-exclamation"
                   className="icon-exclamation"
                   // eslint-disable-next-line max-len
                   // eslint-disable-next-line max-len
-                  dangerouslySetInnerHTML={{ __html: t('security_setting.alert_siteUrl_is_not_set', { link: `<a href="/admin/app">${t('App settings')}<i class="icon-login"></i></a>` }) }}
+                  dangerouslySetInnerHTML={{ __html: t('security_setting.alert_siteUrl_is_not_set', { link: `<a href="/admin/app">${t('App Settings')}<i class="icon-login"></i></a>` }) }}
                 />
                 />
               </div>
               </div>
             )}
             )}
@@ -118,8 +116,8 @@ class TwitterSecurityManagement extends React.Component {
             <h3 className="border-bottom">{t('security_setting.configuration')}</h3>
             <h3 className="border-bottom">{t('security_setting.configuration')}</h3>
 
 
             <div className="row mb-5">
             <div className="row mb-5">
-              <label htmlFor="TwitterConsumerId" className="col-xs-3 text-right">{t('security_setting.clientID')}</label>
-              <div className="col-xs-6">
+              <label htmlFor="TwitterConsumerId" className="col-md-3 text-md-right py-2">{t('security_setting.clientID')}</label>
+              <div className="col-md-6">
                 <input
                 <input
                   className="form-control"
                   className="form-control"
                   type="text"
                   type="text"
@@ -127,15 +125,15 @@ class TwitterSecurityManagement extends React.Component {
                   defaultValue={adminTwitterSecurityContainer.state.twitterConsumerKey || ''}
                   defaultValue={adminTwitterSecurityContainer.state.twitterConsumerKey || ''}
                   onChange={e => adminTwitterSecurityContainer.changeTwitterConsumerKey(e.target.value)}
                   onChange={e => adminTwitterSecurityContainer.changeTwitterConsumerKey(e.target.value)}
                 />
                 />
-                <p className="help-block">
+                <p className="form-text text-muted">
                   <small dangerouslySetInnerHTML={{ __html: t('security_setting.Use env var if empty', { env: 'OAUTH_TWITTER_CONSUMER_KEY' }) }} />
                   <small dangerouslySetInnerHTML={{ __html: t('security_setting.Use env var if empty', { env: 'OAUTH_TWITTER_CONSUMER_KEY' }) }} />
                 </p>
                 </p>
               </div>
               </div>
             </div>
             </div>
 
 
             <div className="row mb-5">
             <div className="row mb-5">
-              <label htmlFor="TwitterConsumerSecret" className="col-xs-3 text-right">{t('security_setting.client_secret')}</label>
-              <div className="col-xs-6">
+              <label htmlFor="TwitterConsumerSecret" className="col-md-3 text-md-right py-2">{t('security_setting.client_secret')}</label>
+              <div className="col-md-6">
                 <input
                 <input
                   className="form-control"
                   className="form-control"
                   type="text"
                   type="text"
@@ -143,34 +141,36 @@ class TwitterSecurityManagement extends React.Component {
                   defaultValue={adminTwitterSecurityContainer.state.twitterConsumerSecret || ''}
                   defaultValue={adminTwitterSecurityContainer.state.twitterConsumerSecret || ''}
                   onChange={e => adminTwitterSecurityContainer.changeTwitterConsumerSecret(e.target.value)}
                   onChange={e => adminTwitterSecurityContainer.changeTwitterConsumerSecret(e.target.value)}
                 />
                 />
-                <p className="help-block">
+                <p className="form-text text-muted">
                   <small dangerouslySetInnerHTML={{ __html: t('security_setting.Use env var if empty', { env: 'OAUTH_TWITTER_CONSUMER_SECRET' }) }} />
                   <small dangerouslySetInnerHTML={{ __html: t('security_setting.Use env var if empty', { env: 'OAUTH_TWITTER_CONSUMER_SECRET' }) }} />
                 </p>
                 </p>
               </div>
               </div>
             </div>
             </div>
 
 
             <div className="row mb-5">
             <div className="row mb-5">
-              <div className="col-xs-offset-3 col-xs-6 text-left">
-                <div className="checkbox checkbox-success">
+              <div className="offset-md-3 col-md-6">
+                <div className="custom-control custom-checkbox custom-checkbox-success">
                   <input
                   <input
                     id="bindByUserNameTwitter"
                     id="bindByUserNameTwitter"
+                    className="custom-control-input"
                     type="checkbox"
                     type="checkbox"
                     checked={adminTwitterSecurityContainer.state.isSameUsernameTreatedAsIdenticalUser || false}
                     checked={adminTwitterSecurityContainer.state.isSameUsernameTreatedAsIdenticalUser || false}
                     onChange={() => { adminTwitterSecurityContainer.switchIsSameUsernameTreatedAsIdenticalUser() }}
                     onChange={() => { adminTwitterSecurityContainer.switchIsSameUsernameTreatedAsIdenticalUser() }}
                   />
                   />
                   <label
                   <label
+                    className="custom-control-label"
                     htmlFor="bindByUserNameTwitter"
                     htmlFor="bindByUserNameTwitter"
                     dangerouslySetInnerHTML={{ __html: t('security_setting.Treat email matching as identical') }}
                     dangerouslySetInnerHTML={{ __html: t('security_setting.Treat email matching as identical') }}
                   />
                   />
                 </div>
                 </div>
-                <p className="help-block">
+                <p className="form-text text-muted">
                   <small dangerouslySetInnerHTML={{ __html: t('security_setting.Treat email matching as identical_warn') }} />
                   <small dangerouslySetInnerHTML={{ __html: t('security_setting.Treat email matching as identical_warn') }} />
                 </p>
                 </p>
               </div>
               </div>
             </div>
             </div>
 
 
             <div className="row my-3">
             <div className="row my-3">
-              <div className="col-xs-offset-3 col-xs-5">
+              <div className="offset-4 col-5">
                 <button
                 <button
                   type="button"
                   type="button"
                   className="btn btn-primary"
                   className="btn btn-primary"

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

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

+ 14 - 15
src/client/js/components/Admin/UserGroup/UserGroupDeleteModal.jsx

@@ -1,8 +1,9 @@
 import React from 'react';
 import React from 'react';
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
 import { withTranslation } from 'react-i18next';
 import { withTranslation } from 'react-i18next';
-
-import Modal from 'react-bootstrap/es/Modal';
+import {
+  Modal, ModalHeader, ModalBody, ModalFooter,
+} from 'reactstrap';
 
 
 import { createSubscribedElement } from '../../UnstatedUtils';
 import { createSubscribedElement } from '../../UnstatedUtils';
 import AppContainer from '../../../services/AppContainer';
 import AppContainer from '../../../services/AppContainer';
@@ -162,31 +163,29 @@ class UserGroupDeleteModal extends React.Component {
     const { t } = this.props;
     const { t } = this.props;
 
 
     return (
     return (
-      <Modal show={this.props.isShow} onHide={this.onHide}>
-        <Modal.Header className="modal-header bg-danger" closeButton>
-          <Modal.Title>
-            <i className="icon icon-fire"></i> {t('admin:user_group_management.delete_modal.header')}
-          </Modal.Title>
-        </Modal.Header>
-        <Modal.Body>
+      <Modal className="modal-md" isOpen={this.props.isShow} toggle={this.props.onHide}>
+        <ModalHeader tag="h4" toggle={this.props.onHide} className="bg-danger text-light">
+          <i className="icon icon-fire"></i> {t('admin:user_group_management.delete_modal.header')}
+        </ModalHeader>
+        <ModalBody>
           <div>
           <div>
             <span className="font-weight-bold">{t('admin:user_group_management.group_name')}</span> : &quot;{this.props.deleteUserGroup.name}&quot;
             <span className="font-weight-bold">{t('admin:user_group_management.group_name')}</span> : &quot;{this.props.deleteUserGroup.name}&quot;
           </div>
           </div>
           <div className="text-danger mt-5">
           <div className="text-danger mt-5">
             {t('admin:user_group_management.delete_modal.desc')}
             {t('admin:user_group_management.delete_modal.desc')}
           </div>
           </div>
-        </Modal.Body>
-        <Modal.Footer>
-          <form className="d-flex justify-content-between" onSubmit={this.handleSubmit}>
-            <div className="d-flex">
+        </ModalBody>
+        <ModalFooter>
+          <form className="d-flex justify-content-between w-100" onSubmit={this.handleSubmit}>
+            <div className="d-flex form-group mb-0">
               {this.renderPageActionSelector()}
               {this.renderPageActionSelector()}
               {this.renderGroupSelector()}
               {this.renderGroupSelector()}
             </div>
             </div>
-            <button type="submit" value="" className="btn btn-sm btn-danger" disabled={!this.validateForm()}>
+            <button type="submit" value="" className="btn btn-sm btn-danger text-nowrap" disabled={!this.validateForm()}>
               <i className="icon icon-fire"></i> {t('Delete')}
               <i className="icon icon-fire"></i> {t('Delete')}
             </button>
             </button>
           </form>
           </form>
-        </Modal.Footer>
+        </ModalFooter>
       </Modal>
       </Modal>
     );
     );
   }
   }

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

@@ -69,7 +69,7 @@ class UserGroupTable extends React.Component {
                   <td>
                   <td>
                     <ul className="list-inline">
                     <ul className="list-inline">
                       {this.state.userGroupRelations[group._id].map((user) => {
                       {this.state.userGroupRelations[group._id].map((user) => {
-                        return <li key={user._id} className="list-inline-item badge badge-primary">{this.xss.process(user.username)}</li>;
+                        return <li key={user._id} className="list-inline-item badge badge-pill badge-warning">{this.xss.process(user.username)}</li>;
                       })}
                       })}
                     </ul>
                     </ul>
                   </td>
                   </td>
@@ -78,23 +78,22 @@ class UserGroupTable extends React.Component {
                     ? (
                     ? (
                       <td>
                       <td>
                         <div className="btn-group admin-group-menu">
                         <div className="btn-group admin-group-menu">
-                          <button type="button" className="btn btn-default btn-sm dropdown-toggle" data-toggle="dropdown">
-                            <i className="icon-settings"></i> <span className="caret"></span>
+                          <button
+                            type="button"
+                            id={`admin-group-menu-button-${group._id}`}
+                            className="btn btn-outline-secondary btn-sm dropdown-toggle"
+                            data-toggle="dropdown"
+                          >
+                            <i className="icon-settings"></i>
                           </button>
                           </button>
-                          <ul className="dropdown-menu" role="menu">
-                            <li>
-                              <a href={`/admin/user-group-detail/${group._id}`}>
-                                <i className="icon-fw icon-note"></i> {t('Edit')}
-                              </a>
-                            </li>
-
-                            <li>
-                              <a role="button" onClick={this.onDelete} data-user-group-id={group._id}>
-                                <i className="icon-fw icon-fire text-danger"></i> {t('Delete')}
-                              </a>
-                            </li>
-
-                          </ul>
+                          <div className="dropdown-menu" role="menu" aria-labelledby={`admin-group-menu-button-${group._id}`}>
+                            <a className="dropdown-item" type="button" href={`/admin/user-group-detail/${group._id}`}>
+                              <i className="icon-fw icon-note"></i> {t('Edit')}
+                            </a>
+                            <a className="dropdown-item" type="button" role="button" onClick={this.onDelete} data-user-group-id={group._id}>
+                              <i className="icon-fw icon-fire text-danger"></i> {t('Delete')}
+                            </a>
+                          </div>
                         </div>
                         </div>
                       </td>
                       </td>
                     )
                     )

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

@@ -8,15 +8,15 @@ class CheckBoxForSerchUserOption extends React.Component {
   render() {
   render() {
     const { t, option } = this.props;
     const { t, option } = this.props;
     return (
     return (
-      <div className="checkbox checkbox-info" key={`isAlso${option}Searched`}>
+      <div className="custom-control custom-checkbox custom-checkbox-info" key={`isAlso${option}Searched`}>
         <input
         <input
           type="checkbox"
           type="checkbox"
           id={`isAlso${option}Searched`}
           id={`isAlso${option}Searched`}
-          className="form-check-input"
+          className="custom-control-input"
           checked={this.props.checked}
           checked={this.props.checked}
           onChange={this.props.onChange}
           onChange={this.props.onChange}
         />
         />
-        <label className="form-check-label ml-3" htmlFor={`isAlso${option}Searched`}>
+        <label className="text-capitalize custom-control-label ml-3" htmlFor={`isAlso${option}Searched`}>
           {t('admin:user_group_management.add_modal.enable_option', { option })}
           {t('admin:user_group_management.add_modal.enable_option', { option })}
         </label>
         </label>
       </div>
       </div>

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

@@ -8,15 +8,15 @@ class RadioButtonForSerchUserOption extends React.Component {
   render() {
   render() {
     const { t, searchType } = this.props;
     const { t, searchType } = this.props;
     return (
     return (
-      <div className="radio" key={`${searchType}Match`}>
+      <div className="custom-control custom-radio custom-control-inline" key={`${searchType}Match`}>
         <input
         <input
           type="radio"
           type="radio"
           id={`${searchType}Match`}
           id={`${searchType}Match`}
-          className="form-check-radio"
+          className="custom-control-input"
           checked={this.props.checked}
           checked={this.props.checked}
           onChange={this.props.onChange}
           onChange={this.props.onChange}
         />
         />
-        <label className="form-check-label ml-3" htmlFor={`${searchType}Match`}>
+        <label className="text-capitalize custom-control-label ml-3" htmlFor={`${searchType}Match`}>
           {t(`admin:user_group_management.add_modal.${searchType}_match`)}
           {t(`admin:user_group_management.add_modal.${searchType}_match`)}
         </label>
         </label>
       </div>
       </div>

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

@@ -16,17 +16,17 @@ class UserGroupDetailPage extends React.Component {
 
 
     return (
     return (
       <div>
       <div>
-        <a href="/admin/user-groups" className="btn btn-default">
+        <a href="/admin/user-groups" className="btn btn-outline-secondary">
           <i className="icon-fw ti-arrow-left" aria-hidden="true"></i>
           <i className="icon-fw ti-arrow-left" aria-hidden="true"></i>
           {t('admin:user_group_management.back_to_list')}
           {t('admin:user_group_management.back_to_list')}
         </a>
         </a>
-        <div className="m-t-20 form-box">
+        <div className="mt-4 form-box">
           <UserGroupEditForm />
           <UserGroupEditForm />
         </div>
         </div>
-        <legend className="m-t-20">{t('admin:user_group_management.user_list')}</legend>
+        <h2 className="admin-setting-header mt-4">{t('admin:user_group_management.user_list')}</h2>
         <UserGroupUserTable />
         <UserGroupUserTable />
         <UserGroupUserModal />
         <UserGroupUserModal />
-        <legend className="m-t-20">{t('Page')}</legend>
+        <h2 className="admin-setting-header mt-4">{t('Page')}</h2>
         <div className="page-list">
         <div className="page-list">
           <UserGroupPageList />
           <UserGroupPageList />
         </div>
         </div>

Неке датотеке нису приказане због велике количине промена