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

Merge branch 'dev/4.0.x' into imprv/profile-image-cache

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

+ 1 - 0
.eslintrc.js

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

+ 18 - 0
CHANGES.md

@@ -1,5 +1,23 @@
 # CHANGES
 
+## 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: 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.2-RC
 
 *

+ 16 - 0
babel.config.js

@@ -14,6 +14,22 @@ module.exports = function(api) {
   ];
   const plugins = [
     '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 {

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

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

+ 3 - 5
config/webpack.common.js

@@ -22,7 +22,7 @@ module.exports = (options) => {
     entry: Object.assign({
       'js/app':                       './src/client/js/app',
       '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-presentation':       './src/client/js/legacy/crowi-presentation',
       'js/plugin':                    './src/client/js/plugin',
@@ -34,18 +34,16 @@ module.exports = (options) => {
       'styles/style-presentation':    './src/client/styles/scss/style-presentation.scss',
       // themes
       '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-mono-blue':       './src/client/styles/scss/theme/mono-blue.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-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-wood':          './src/client/styles/scss/theme/wood.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-spring':         './src/client/styles/scss/theme/spring.scss',
       // styles for external services
       'styles/style-hackmd':          './src/client/styles/hackmd/style.scss',
     }, 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',
+    }),
+  ],
+};

+ 11 - 6
package.json

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

+ 18 - 1
resource/cdn-manifests.js

@@ -2,7 +2,8 @@ module.exports = {
   js: [
     {
       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'],
       args: {
         integrity: '',
@@ -129,6 +130,14 @@ module.exports = {
         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',
       url: 'https://cdn.jsdelivr.net/npm/emojione@3.1.2/extras/css/emojione.min.css',
@@ -137,6 +146,14 @@ module.exports = {
         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',
       url: 'https://cdn.jsdelivr.net/jquery.ui/1.11.4/jquery-ui.min.css',

+ 4 - 15
resource/locales/en-US/admin/admin.json

@@ -97,22 +97,11 @@
       "kibela_title": "Easy viewing structure",
       "kibela_text1": "Center aligned contents",
       "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_desc": "You can choose Valid/Invalid of the function",

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

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

@@ -0,0 +1,164 @@
+# 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">Light</span>
+<span class="badge badge-dark">Dark</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>
+
+# Cards
+
+<div class="card">
+  <div class="card-header">Card header without title</div>
+  <div class="card-body">
+    Card body content
+  </div>
+</div>
+
+<div class="card bg-light">
+  <div class="card-header">Card header without title</div>
+  <div class="card-body">
+    Card body content
+  </div>
+</div>
+
+<div class="card bg-secondary text-white">
+  <div class="card-header">Card header without title</div>
+  <div class="card-body">
+    Card body content
+  </div>
+</div>
+
+<div class="card bg-dark text-white">
+  <div class="card-header">Card header without title</div>
+  <div class="card-body">
+    Card body content
+  </div>
+</div>
+
+<div class="card bg-primary text-white">
+  <div class="card-header">Card header without title</div>
+  <div class="card-body">
+    Card body content
+  </div>
+</div>
+
+<div class="card bg-success text-white">
+  <div class="card-header">Card header without title</div>
+  <div class="card-body">
+    Card body content
+  </div>
+</div>
+
+<div class="card bg-info text-white">
+  <div class="card-header">Card header without title</div>
+  <div class="card-body">
+    Card body content
+  </div>
+</div>
+
+<div class="card bg-warning text-white">
+  <div class="card-header">Card header without title</div>
+  <div class="card-body">
+    Card body content
+  </div>
+</div>
+
+<div class="card bg-danger text-white">
+  <div class="card-header">Card header without title</div>
+  <div class="card-body">
+    Card body content
+  </div>
+</div>
+
+# Wells
+
+## Default well
+
+<div class="card card-body bg-light">Look, I'm in a well! </div>
+
+## Optional classes
+
+<div class="card card-body bg-light p-5">Look, I'm in a well! </div>
+
+<div class="card card-body bg-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>

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

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

@@ -55,13 +55,12 @@
   "Share Link": "Share Link",
   "Markdown Link": "Markdown Link",
   "Create/Edit Template": "Create/Edit template page",
-  "Unportalize": "Unportalize",
   "Go to this version": "View this version",
   "View diff": "View diff",
   "No diff": "No diff",
   "Shrink versions that have no diffs": "Shrink versions that have no diffs",
   "User ID": "User ID",
-  "Home": "Home",
+  "User's Home": "User's Home",
   "User Settings": "User Settings",
   "User Information": "User information",
   "Basic Info": "Basic info",
@@ -111,6 +110,13 @@
   "Specified users only": "Specified users only",
   "Only me": "Only me",
   "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",
   "Shareable link": "Shareable link",
   "The whitelist of registration permission E-mail address": "The whitelist of registration permission E-mail address",
@@ -124,6 +130,8 @@
   "Deleted Pages": "Deleted Pages",
   "Sign out": "Logout",
   "Disassociate": "Disassociate",
+  "Recent Created": "Recent Created",
+  "Recent Changes": "Recent Changes",
   "form_validation": {
     "error_message": "Some values ​​are incorrect",
     "required": "%s is required",
@@ -193,6 +201,7 @@
   "copy_to_clipboard": {
     "Copy to clipboard": "Copy to clipboard",
     "Page path": "Page path",
+    "Page URL": "Page URL",
     "Parmanent link": "Parmanent link",
     "Page path and parmanent link": "Page path and parmanent link",
     "Markdown link": "Markdown link"
@@ -261,9 +270,9 @@
       "Redirect": "Redirect"
     },
     "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",
-      "recursive": "Move/Rename children of under <code>%s</code> recursively"
+      "recursive": "Move/Rename children of under this path recursively"
     }
   },
   "Put Back": "Put back",
@@ -274,11 +283,12 @@
     "delete_recursively": "Delete child pages recursively.",
     "delete_completely": "Delete 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."
   },
   "modal_empty":{
-    "empty_the_trash": "Empty The Trash"
+    "empty_the_trash": "Empty The Trash",
+    "notice": "The pages deleted completely are unrecoverable."
   },
   "modal_duplicate": {
     "label": {
@@ -293,13 +303,13 @@
       "recursively": "Put back recursively"
     },
     "help": {
-      "recursively": "Put back children of under <code>%s</code> recursively"
+      "recursively": "Put back page under this path recursively"
     }
   },
   "modal_shortcuts": {
     "global": {
       "title": "Global shortcuts",
-      "Open/Close shortcut help": "Open/Close shortcut help",
+      "Open/Close shortcut help": "Open/Close<br>shortcut help",
       "Edit Page": "Edit Page",
       "Create Page": "Create Page",
       "Show Contributors": "Show Contributors",
@@ -330,7 +340,7 @@
   "template": {
     "modal_label": {
       "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": {
       "create/edit": "Create/Edit template page..",
@@ -657,7 +667,7 @@
     "export_collections": "Export Collections",
     "check_all": "Check 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",
     "export": "Export",
     "cancel": "Cancel",

+ 3 - 3
resource/locales/en-US/welcome.md

@@ -3,9 +3,9 @@
 [![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)
 
-<div class="panel panel-primary">
-  <div class="panel-heading">Tips</div>
-  <div class="panel-body"><ul>
+<div class="card">
+  <div class="card-header">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/3.3/css/">Bootstrap 3</a>.</li>
   </ul></div>

+ 4 - 15
resource/locales/ja/admin/admin.json

@@ -97,22 +97,11 @@
       "kibela_title": "閲覧重視の構造",
       "kibela_text1": "コンテンツが中心に表示されます。",
       "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_desc": "機能の有効/無効を選択できます。",

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

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

@@ -0,0 +1,164 @@
+# 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">Light</span>
+<span class="badge badge-dark">Dark</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>
+
+# Cards
+
+<div class="card">
+  <div class="card-header">Card header without title</div>
+  <div class="card-body">
+    Card body content
+  </div>
+</div>
+
+<div class="card bg-light">
+  <div class="card-header">Card header without title</div>
+  <div class="card-body">
+    Card body content
+  </div>
+</div>
+
+<div class="card bg-secondary text-white">
+  <div class="card-header">Card header without title</div>
+  <div class="card-body">
+    Card body content
+  </div>
+</div>
+
+<div class="card bg-dark text-white">
+  <div class="card-header">Card header without title</div>
+  <div class="card-body">
+    Card body content
+  </div>
+</div>
+
+<div class="card bg-primary text-white">
+  <div class="card-header">Card header without title</div>
+  <div class="card-body">
+    Card body content
+  </div>
+</div>
+
+<div class="card bg-success text-white">
+  <div class="card-header">Card header without title</div>
+  <div class="card-body">
+    Card body content
+  </div>
+</div>
+
+<div class="card bg-info text-white">
+  <div class="card-header">Card header without title</div>
+  <div class="card-body">
+    Card body content
+  </div>
+</div>
+
+<div class="card bg-warning text-white">
+  <div class="card-header">Card header without title</div>
+  <div class="card-body">
+    Card body content
+  </div>
+</div>
+
+<div class="card bg-danger text-white">
+  <div class="card-header">Card header without title</div>
+  <div class="card-body">
+    Card body content
+  </div>
+</div>
+
+# Wells
+
+## Default well
+
+<div class="card card-body bg-light">Look, I'm in a well! </div>
+
+## Optional classes
+
+<div class="card card-body bg-light p-5">Look, I'm in a well! </div>
+
+<div class="card card-body bg-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>

+ 3 - 3
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 段落
 
-空白行を挟むことで段落となります。aaaa
+空白行を挟むことで段落となります。
 
 ```
 段落1

+ 23 - 13
resource/locales/ja/translation.json

@@ -55,13 +55,12 @@
   "Share Link": "共有用リンク",
   "Markdown Link": "Markdown形式のリンク",
   "Create/Edit Template": "テンプレートページの作成/編集",
-  "Unportalize": "ポータル解除",
   "Go to this version": "このバージョンを見る",
   "View diff": "差分を表示",
   "No diff": "差分なし",
   "Shrink versions that have no diffs": "差分のないバージョンをコンパクトに表示する",
   "User ID": "ユーザーID",
-  "Home": "ホーム",
+  "User's Home": "ユーザーホーム",
   "User Settings": "ユーザー設定",
   "User Information": "ユーザー情報",
   "Basic Info": "ユーザーの基本情報",
@@ -110,6 +109,13 @@
   "Specified users": "特定ユーザーのみ",
   "Only me": "自分のみ",
   "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": "グループの再選択",
   "Shareable link": "このページの共有用URL",
   "The whitelist of registration permission E-mail address": "登録許可メールアドレスの<br>ホワイトリスト",
@@ -123,6 +129,8 @@
   "Deleted Pages": "削除済みページ",
   "Sign out": "ログアウト",
   "Disassociate": "連携解除",
+  "Recent Created": "最新の作成",
+  "Recent Changes": "最新の変更",
   "form_validation": {
     "error_message": "いくつかの値が設定されていません",
     "required": "%sに値を入力してください",
@@ -192,6 +200,7 @@
   "copy_to_clipboard": {
     "Copy to clipboard": "クリップボードにコピー",
     "Page path": "ページ名",
+    "Page URL": "ページURL",
     "Parmanent link": "パーマリンク",
     "Page path and parmanent link": "ページ名とパーマリンク",
     "Markdown link": "マークダウン形式のリンク"
@@ -259,9 +268,9 @@
       "Redirect": "リダイレクトする"
     },
     "help": {
-      "redirect": "<code>%s</code> にアクセスされた際に自動的に新しいページにジャンプします",
+      "redirect": "アクセスされた際に自動的に新しいページにジャンプします",
       "metadata": "最終更新ユーザー、最終更新日を更新せず維持します",
-      "recursive": "<code>%s</code> 配下のページも移動/名前変更します"
+      "recursive": "配下のページも移動/名前変更します"
     }
   },
   "Put Back": "元に戻す",
@@ -272,11 +281,12 @@
     "delete_recursively": "全ての子ページも削除",
     "delete_completely": "完全削除",
     "delete_completely_restriction": "完全削除をするための権限がありません。",
-    "recursively": "<code>%s</code> 配下のページも削除します",
+    "recursively": "配下のページも削除します",
     "completely": "ゴミ箱を経由せず、完全に削除します"
   },
   "modal_empty":{
-    "empty_the_trash": "ゴミ箱を空にする"
+    "empty_the_trash": "ゴミ箱を空にする",
+    "notice": "完全削除したページは元に戻すことができません"
   },
   "modal_duplicate": {
     "label": {
@@ -291,16 +301,16 @@
       "recursively": "全ての子ページも元に戻す"
     },
     "help": {
-      "recursively": "<code>%s</code> 配下のページも元に戻します"
+      "recursively": "配下のページも元に戻します"
     }
   },
   "modal_shortcuts": {
     "global": {
       "title": "グローバルショートカット",
-      "Open/Close shortcut help": "ショートカットヘルプの表示/非表示",
+      "Open/Close shortcut help": "ショートカットヘルプ<br>の表示/非表示",
       "Edit Page": "ページ編集",
       "Create Page": "ページ作成",
-      "Show Contributors": "コントリビューターを表示",
+      "Show Contributors": "コントリビューター<br>を表示",
       "Konami Code": "コナミコマンド",
       "konami_code_url": "https://ja.wikipedia.org/wiki/コナミコマンド"
     },
@@ -323,12 +333,12 @@
     "activate_user_success": "{{username}}を有効化しました",
     "deactivate_user_success": "{{username}}を無効化しました",
     "remove_user_success": "{{username}}を削除しました",
-    "remove_external_user_success": "{{accountId}}を削除しました "
+    "remove_external_user_success": "{{accountId}}を削除しました"
   },
   "template": {
     "modal_label": {
       "Create/Edit Template Page": "テンプレートページの作成/編集",
-      "Create template under": "<code><small>%s</small></code><br />にテンプレートページを作成"
+      "Create template under": "配下にテンプレートページを作成"
     },
     "option_label": {
       "select": "テンプレートタイプを選択してください",
@@ -386,7 +396,7 @@
     "discard_changes": "HackMD の変更を破棄する",
     "integration_failed": "HackMD の統合に失敗しました",
     "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 コンポーネントは初期化されていません",
     "someone_editing": "このページは、HackMD で編集されています。",
     "this_page_has_draft": "このページは、HackMD のドラフトがあります。"
@@ -646,7 +656,7 @@
     "export_collections": "コレクションのエクスポート",
     "check_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": "アーカイブデータの新規作成",
     "export": "エクスポート",
     "cancel": "キャンセル",

+ 3 - 3
resource/locales/ja/welcome.md

@@ -3,9 +3,9 @@
 [![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)
 
-<div class="panel panel-primary">
-  <div class="panel-heading">Tips</div>
-  <div class="panel-body"><ul>
+<div class="card">
+  <div class="card-header">Tips</div>
+  <div class="card-body"><ul>
     <li>Ctrl(⌘)-/ でショートカットヘルプを表示します</li>
       <li>HTML/CSS の記述時は、<a href="https://getbootstrap.com/docs/3.3/css/">Bootstrap 3</a> を利用できます</li>
   </ul></div>

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

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

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

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

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

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

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

@@ -41,14 +41,14 @@ class AdminHome extends React.Component {
         </p>
 
         <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>
             <SystemInfomationTable />
           </div>
         </div>
 
         <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>
             <InstalledPluginTable />
           </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">
         <tbody>
           <tr>
-            <th className="col-sm-4">GROWI</th>
+            <th>GROWI</th>
             <td>{ adminHomeContainer.state.growiVersion }</td>
           </tr>
           <tr>

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

@@ -38,9 +38,9 @@ class AppSetting extends React.Component {
 
     return (
       <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
               className="form-control"
               type="text"
@@ -48,13 +48,13 @@ class AppSetting extends React.Component {
               onChange={(e) => { adminAppContainer.changeTitle(e.target.value) }}
               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 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
               className="form-control"
               type="text"
@@ -62,55 +62,56 @@ class AppSetting extends React.Component {
               onChange={(e) => { adminAppContainer.changeConfidential(e.target.value) }}
               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 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
                 type="radio"
                 id="radioLangEn"
+                className="custom-control-input"
                 name="globalLang"
                 value="en-US"
                 checked={adminAppContainer.state.globalLang === 'en-US'}
                 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 className="radio radio-primary radio-inline">
+            <div className="custom-control custom-radio custom-control-inline">
               <input
                 type="radio"
                 id="radioLangJa"
+                className="custom-control-input"
                 name="globalLang"
                 value="ja"
                 checked={adminAppContainer.state.globalLang === 'ja'}
                 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 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
                 type="checkbox"
                 id="cbFileUpload"
+                className="custom-control-input"
                 name="fileUpload"
                 checked={adminAppContainer.state.fileUpload}
                 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>
 
-            <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')}
             </p>
           </div>

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

@@ -38,35 +38,35 @@ class AppSettingsPage extends React.Component {
     return (
       <Fragment>
         <div className="row">
-          <div className="col-md-12">
+          <div className="col-lg-12">
             <h2 className="admin-setting-header">{t('App Settings')}</h2>
             <AppSetting />
           </div>
         </div>
 
         <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>
             <SiteUrlSetting />
           </div>
         </div>
 
         <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>
             <MailSetting />
           </div>
         </div>
 
         <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>
             <AwsSetting />
           </div>
         </div>
 
         <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>
             <PluginSetting />
           </div>

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

@@ -38,7 +38,7 @@ class AwsSetting extends React.Component {
 
     return (
       <React.Fragment>
-        <p className="well">
+        <p className="card well">
           {t('admin:app_setting.aws_access')}
           <br />
           {t('admin:app_setting.no_smtp_setting')}
@@ -50,11 +50,11 @@ class AwsSetting extends React.Component {
           </span>
         </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')}
           </label>
-          <div className="col-xs-6">
+          <div className="col-md-6">
             <input
               className="form-control"
               placeholder={`${t('eg')} ap-northeast-1`}
@@ -66,11 +66,11 @@ class AwsSetting extends React.Component {
           </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')}
           </label>
-          <div className="col-xs-6">
+          <div className="col-md-6">
             <input
               className="form-control"
               type="text"
@@ -80,15 +80,15 @@ class AwsSetting extends React.Component {
                 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 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')}
           </label>
-          <div className="col-xs-6">
+          <div className="col-md-6">
             <input
               className="form-control"
               type="text"
@@ -101,11 +101,11 @@ class AwsSetting extends React.Component {
           </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
           </label>
-          <div className="col-xs-6">
+          <div className="col-md-6">
             <input
               className="form-control"
               type="text"
@@ -117,11 +117,11 @@ class AwsSetting extends React.Component {
           </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
           </label>
-          <div className="col-xs-6">
+          <div className="col-md-6">
             <input
               className="form-control"
               type="text"

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

@@ -38,10 +38,10 @@ class MailSetting extends React.Component {
 
     return (
       <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
               className="form-control"
               type="text"
@@ -52,9 +52,9 @@ class MailSetting extends React.Component {
           </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>
             <input
               className="form-control"
@@ -63,7 +63,7 @@ class MailSetting extends React.Component {
               onChange={(e) => { adminAppContainer.changeSmtpHost(e.target.value) }}
             />
           </div>
-          <div className="col-xs-2">
+          <div className="col-md-2">
             <label>{t('admin:app_setting.port')}</label>
             <input
               className="form-control"
@@ -73,8 +73,8 @@ class MailSetting extends React.Component {
           </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>
             <input
               className="form-control"
@@ -83,7 +83,7 @@ class MailSetting extends React.Component {
               onChange={(e) => { adminAppContainer.changeSmtpUser(e.target.value) }}
             />
           </div>
-          <div className="col-xs-3">
+          <div className="col-md-3">
             <label>{t('Password')}</label>
             <input
               className="form-control"

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

@@ -39,20 +39,21 @@ class PluginSetting extends React.Component {
 
     return (
       <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
                 id="isEnabledPlugins"
+                className="custom-control-input"
                 type="checkbox"
                 checked={adminAppContainer.state.isEnabledPlugins}
                 onChange={(e) => {
                   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>

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

@@ -38,51 +38,49 @@ class SiteUrlSetting extends React.Component {
 
     return (
       <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
           && (<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>
 

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

@@ -11,42 +11,113 @@ const AdminNavigation = (props) => {
     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 (
-    <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 { 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>
+  );
+};
 
 AdminUpdateButtonRow.propTypes = {
   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 (
       <>
-        <h5 className="my-1">
+        <h6 className="my-1">
           {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
-            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}%` }}
           >
             <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 CustomizeLayoutSetting from './CustomizeLayoutSetting';
-import CustomizeBehaviorSetting from './CustomizeBehaviorSetting';
 import CustomizeFunctionSetting from './CustomizeFunctionSetting';
 import CustomizeHighlightSetting from './CustomizeHighlightSetting';
 import CustomizeCssSetting from './CustomizeCssSetting';
@@ -52,9 +51,6 @@ class Customize extends React.Component {
         <div className="mb-5">
           <CustomizeLayoutSetting />
         </div>
-        <div className="mb-5">
-          <CustomizeBehaviorSetting />
-        </div>
         <div className="mb-5">
           <CustomizeFunctionSetting />
         </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 PropTypes from 'prop-types';
 import { withTranslation } from 'react-i18next';
+import { Card, CardBody } from 'reactstrap';
 
 import { createSubscribedElement } from '../../UnstatedUtils';
 import { toastSuccess, toastError } from '../../../util/apiNotification';
@@ -36,27 +37,31 @@ class CustomizeCssSetting extends React.Component {
 
     return (
       <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>
-
-        <AdminUpdateButtonRow onClick={this.onClickSubmit} disabled={adminCustomizeContainer.state.retrieveError != null} />
       </React.Fragment>
     );
   }

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

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

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

@@ -1,6 +1,10 @@
 import React from 'react';
 import PropTypes from 'prop-types';
 import { withTranslation } from 'react-i18next';
+import {
+  Card, CardBody,
+  Dropdown, DropdownToggle, DropdownMenu, DropdownItem,
+} from 'reactstrap';
 
 import { createSubscribedElement } from '../../UnstatedUtils';
 import { toastSuccess, toastError } from '../../../util/apiNotification';
@@ -11,14 +15,23 @@ import AdminCustomizeContainer from '../../../services/AdminCustomizeContainer';
 import AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow';
 import CustomizeFunctionOption from './CustomizeFunctionOption';
 
-class CustomizeBehaviorSetting extends React.Component {
+class CustomizeFunctionSetting extends React.Component {
 
   constructor(props) {
     super(props);
 
+    this.state = {
+      isDropdownOpen: false,
+    };
+
+    this.onToggleDropdown = this.onToggleDropdown.bind(this);
     this.onClickSubmit = this.onClickSubmit.bind(this);
   }
 
+  onToggleDropdown() {
+    this.setState({ isDropdownOpen: !this.state.isDropdownOpen });
+  }
+
   async onClickSubmit() {
     const { t, adminCustomizeContainer } = this.props;
 
@@ -36,133 +49,138 @@ class CustomizeBehaviorSetting extends React.Component {
 
     return (
       <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>
-              <p className="help-block">
-                {t('admin:customize_setting.function_options.recently_created_n_draft_num_desc')}
-              </p>
             </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>
-
-        <AdminUpdateButtonRow onClick={this.onClickSubmit} disabled={adminCustomizeContainer.state.retrieveError != null} />
       </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
   appContainer: PropTypes.instanceOf(AppContainer).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 PropTypes from 'prop-types';
 import { withTranslation } from 'react-i18next';
+import { Card, CardBody } from 'reactstrap';
 
 import { createSubscribedElement } from '../../UnstatedUtils';
 import { toastSuccess, toastError } from '../../../util/apiNotification';
@@ -36,36 +37,41 @@ class CustomizeHeaderSetting extends React.Component {
 
     return (
       <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 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>
     );
   }

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

@@ -2,6 +2,9 @@
 import React from 'react';
 import PropTypes from 'prop-types';
 import { withTranslation } from 'react-i18next';
+import {
+  Dropdown, DropdownToggle, DropdownMenu, DropdownItem,
+} from 'reactstrap';
 
 import { createSubscribedElement } from '../../UnstatedUtils';
 import { toastSuccess, toastError } from '../../../util/apiNotification';
@@ -16,9 +19,18 @@ class CustomizeHighlightSetting extends React.Component {
   constructor(props) {
     super(props);
 
+    this.state = {
+      isDropdownOpen: false,
+    };
+
+    this.onToggleDropdown = this.onToggleDropdown.bind(this);
     this.onClickSubmit = this.onClickSubmit.bind(this);
   }
 
+  onToggleDropdown() {
+    this.setState({ isDropdownOpen: !this.state.isDropdownOpen });
+  }
+
   async onClickSubmit() {
     const { t, adminCustomizeContainer } = this.props;
 
@@ -64,62 +76,69 @@ class CustomizeHighlightSetting extends React.Component {
       const isBorderEnable = option[1].border;
 
       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 (
       <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>
-              {/* 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 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 className="help-block">
-          <label>Examples:</label>
-          <div className="wiki">
-            {this.renderHljsDemo()}
+            <AdminUpdateButtonRow onClick={this.onClickSubmit} disabled={adminCustomizeContainer.state.retrieveError != null} />
           </div>
         </div>
-
-        <AdminUpdateButtonRow onClick={this.onClickSubmit} disabled={adminCustomizeContainer.state.retrieveError != null} />
       </React.Fragment>
     );
   }

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

@@ -10,9 +10,15 @@ class CustomizeLayoutOption extends React.Component {
     return (
       <React.Fragment>
         <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 */}
               <span dangerouslySetInnerHTML={{ __html: this.props.labelHtml }} />
             </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;
 
     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
             layoutType="crowi-plus"
             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>`}
           >
             <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>
         </div>
 
-        <div className="col-sm-4">
+        <div className="col text-center">
           <CustomizeLayoutOption
             layoutType="kibela"
             isSelected={adminCustomizeContainer.state.currentLayout === 'kibela'}
@@ -39,27 +41,13 @@ class CustomizeLayoutOptions extends React.Component {
             labelHtml="Kibela like layout"
           >
             <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>
         </div>
       </div>

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

@@ -48,12 +48,20 @@ class CustomizeLayoutSetting extends React.Component {
 
     return (
       <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>
     );
   }

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

@@ -1,6 +1,7 @@
 import React from 'react';
 import PropTypes from 'prop-types';
 import { withTranslation } from 'react-i18next';
+import { Card, CardBody } from 'reactstrap';
 
 import { createSubscribedElement } from '../../UnstatedUtils';
 import { toastSuccess, toastError } from '../../../util/apiNotification';
@@ -44,50 +45,64 @@ class CustomizeScriptSetting extends React.Component {
 
     return (
       <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>
-
-        <AdminUpdateButtonRow onClick={this.onClickSubmit} disabled={adminCustomizeContainer.state.retrieveError != null} />
       </React.Fragment>
     );
   }

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

@@ -1,5 +1,7 @@
 import React from 'react';
 import PropTypes from 'prop-types';
+import { withTranslation } from 'react-i18next';
+
 
 import { createSubscribedElement } from '../../UnstatedUtils';
 
@@ -10,70 +12,68 @@ import AdminCustomizeContainer from '../../../services/AdminCustomizeContainer';
 class CustomizeThemeOptions extends React.Component {
 
   render() {
-    const { adminCustomizeContainer } = this.props;
+    const { t, adminCustomizeContainer } = this.props;
     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: '#2a2929', sidebar: '#118050', theme: '#460039',
     }, {
-      name: 'island', bg: '#8ecac0', topbar: '#0c2a44', theme: '#cef2ef',
+      name: 'wood',       bg: '#fffefb', topbar: '#2a2929', sidebar: '#aaa45f', theme: '#dddebf',
     }, {
-      name: 'christmas', bg: '#fffefb', topbar: '#b3000c', theme: '#017e20',
+      name: 'island',     bg: '#8ecac0', topbar: '#2a2929', sidebar: '#0c2a44', theme: '#cef2ef',
     }, {
-      name: 'antarctic', bg: '#ffffff', topbar: '#000080', theme: '#99cccc',
+      name: 'christmas',  bg: '#fffefb', topbar: '#2a2929', sidebar: '#b3000c', theme: '#017e20',
     }, {
-      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: '#99cccc',
     }, {
-      name: 'future', bg: '#16282D', topbar: '#011414', theme: '#04B4AE',
+      name: 'spring',     bg: '#fff5ee', topbar: '#2a2929', sidebar: '#ff69b4', theme: '#ffb6c1',
     }, {
-      name: 'blue-night', bg: '#061F2F', topbar: '#27343B', theme: '#0090C8',
+      name: 'future',     bg: '#16282D', topbar: '#2a2929', sidebar: '#011414', theme: '#04B4AE',
     }, {
-      name: 'halloween', bg: '#030003', topbar: '#cc5d1f', theme: '#e9af2b',
+      name: 'halloween',  bg: '#030003', topbar: '#2a2929', sidebar: '#cc5d1f', theme: '#e9af2b',
     }];
+    /* eslint-enable no-multi-spaces */
 
     return (
       <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>
-        {/* 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>
     );
@@ -86,8 +86,9 @@ const CustomizeThemeOptionsWrapper = (props) => {
 };
 
 CustomizeThemeOptions.propTypes = {
+  t: PropTypes.func.isRequired, // i18next
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
   adminCustomizeContainer: PropTypes.instanceOf(AdminCustomizeContainer).isRequired,
 };
 
-export default CustomizeThemeOptionsWrapper;
+export default withTranslation()(CustomizeThemeOptionsWrapper);

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

@@ -1,6 +1,8 @@
+/* eslint-disable max-len */
 import React from 'react';
 import PropTypes from 'prop-types';
 import { withTranslation } from 'react-i18next';
+import { Card, CardBody } from 'reactstrap';
 
 import AppContainer from '../../../services/AppContainer';
 import AdminCustomizeContainer from '../../../services/AdminCustomizeContainer';
@@ -34,27 +36,39 @@ class CustomizeTitle extends React.Component {
 
     return (
       <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">
+                <span
+                  // eslint-disable-next-line react/no-danger
+                  dangerouslySetInnerHTML={{ __html: t('admin:customize_setting.custom_title_detail') }}
+                />
+              </CardBody>
+            </Card>
+          </div>
+
+          {/* TODO i18n */}
+          <div className="form-text text-muted col-12">
+            Default Value: <code>&#123;&#123;page&#125;&#125; - &#123;&#123;sitename&#125;&#125;</code>
+            <br />
+            Default Output: <code className="xml">&lt;title&gt;/Somewhere/Page - {'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>
     );
   }

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

@@ -5,23 +5,23 @@ import PropTypes from 'prop-types';
 class ThemeColorBox extends React.PureComponent {
 
   render() {
-    const { name } = this.props;
+    const {
+      isSelected, onSelected, name, bg, topbar, sidebar, theme,
+    } = this.props;
 
     return (
       <div
         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">
             <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>
           </svg>
         </a>
@@ -39,6 +39,7 @@ ThemeColorBox.propTypes = {
   name: PropTypes.string.isRequired,
   bg: PropTypes.string.isRequired,
   topbar: PropTypes.string.isRequired,
+  sidebar: 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 (
       <>
         <div className="row">
-          <div className="col-xs-12">
+          <div className="col-md-12">
             <StatusTable
               isInitialized={isInitialized}
               isErrorOccuredOnSearchService={isErrorOccuredOnSearchService}
@@ -177,8 +177,8 @@ class ElasticsearchManagement extends React.Component {
 
         {/* Controls */}
         <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
               isEnabled={isReconnectBtnEnabled}
               isProcessing={isReconnectingProcessing}
@@ -190,8 +190,8 @@ class ElasticsearchManagement extends React.Component {
         <hr />
 
         <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
               isRebuildingProcessing={isRebuildingProcessing}
               isRebuildingCompleted={isRebuildingCompleted}
@@ -204,8 +204,8 @@ class ElasticsearchManagement extends React.Component {
         <hr />
 
         <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
               isRebuildingProcessing={isRebuildingProcessing}
               isRebuildingCompleted={isRebuildingCompleted}

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

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

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

@@ -76,14 +76,14 @@ class RebuildIndexControls extends React.Component {
 
         <button
           type="submit"
-          className="btn btn-inverse"
+          className="btn btn-primary"
           onClick={() => { this.props.onRebuildingRequested() }}
           disabled={!isEnabled}
         >
           { t('full_text_search_management.rebuild_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_2') }<br />
         </p>

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

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

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

@@ -7,7 +7,7 @@ import { createSubscribedElement } from '../../UnstatedUtils';
 class StatusTable extends React.PureComponent {
 
   renderPreInitializedLabel() {
-    return <span className="label label-default">――</span>;
+    return <span className="badge badge-pill badge-default">――</span>;
   }
 
   renderConnectionStatusLabels() {
@@ -18,17 +18,17 @@ class StatusTable extends React.PureComponent {
     } = this.props;
 
     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;
 
     let connectionStatusLabel = null;
     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 {
       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 (
@@ -42,8 +42,8 @@ class StatusTable extends React.PureComponent {
     const { t, isNormalized } = this.props;
 
     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 = []) {
@@ -51,24 +51,23 @@ class StatusTable extends React.PureComponent {
 
     const aliasLabels = aliases.map((aliasName) => {
       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}
         </span>
       );
     });
 
     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 id={collapseId} className="panel-collapse collapse" role="tabpanel">
-          <div className="panel-body">
+        <div id={collapseId} className="collapse">
+          <div className="card-body">
             <pre>
               {JSON.stringify(body, null, 2)}
             </pre>
@@ -124,7 +123,7 @@ class StatusTable extends React.PureComponent {
       <div className="row">
         { Object.keys(indexNameToDataMap).map((indexName) => {
           return (
-            <div key={`col-${indexName}`} className="col-xs-6">
+            <div key={`col-${indexName}`} className="col-md-6">
               { this.renderIndexInfoPanel(indexName, indexNameToDataMap[indexName], indexNameToAliasMap[indexName]) }
             </div>
           );
@@ -143,22 +142,16 @@ class StatusTable extends React.PureComponent {
       <table className="table table-bordered">
         <tbody>
           <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>
-            <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>
-            <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>
         </tbody>
       </table>

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

@@ -14,35 +14,37 @@ class ArchiveFilesTable extends React.Component {
     const { t } = this.props;
 
     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 text-nowrap">
+        <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>
     );
   }
 

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

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

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

@@ -1,7 +1,9 @@
 import React from 'react';
 import PropTypes from 'prop-types';
 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 { createSubscribedElement } from '../../UnstatedUtils';
@@ -120,7 +122,7 @@ class SelectCollectionsModal extends React.Component {
     const html = this.props.t('admin:export_management.desc_password_seed');
 
     // 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) {
@@ -140,28 +142,30 @@ class SelectCollectionsModal extends React.Component {
   }
 
   renderCheckboxes(collectionNames, color) {
-    const checkboxColor = color ? `checkbox-${color}` : 'checkbox-info';
+    const checkboxColor = color ? `custom-checkbox-${color}` : 'custom-checkbox-info';
 
     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>
     );
   }
@@ -170,54 +174,54 @@ class SelectCollectionsModal extends React.Component {
     const { t } = this.props;
 
     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}>
-          <Modal.Body>
+          <ModalBody>
             <div className="row">
               <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')}
                 </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')}
                 </button>
               </div>
             </div>
             <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)}
               </div>
             </div>
             <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.renderWarnForUser()}
               </div>
             </div>
             <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)}
               </div>
             </div>
             <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()}
               </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>
       </Modal>
     );

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

@@ -213,7 +213,7 @@ class ExportArchiveDataPage extends React.Component {
       <Fragment>
         <h2>{t('Export Archive Data')}</h2>
 
-        <button type="button" className="btn btn-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')}
         </button>
 

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

@@ -1,7 +1,7 @@
 import React from 'react';
 import PropTypes from 'prop-types';
 import { withTranslation } from 'react-i18next';
-import Modal from 'react-bootstrap/es/Modal';
+import { Modal, ModalHeader, ModalBody } from 'reactstrap';
 
 import { createSubscribedElement } from '../../../UnstatedUtils';
 
@@ -20,13 +20,13 @@ class ErrorViewer extends React.Component {
     }
 
     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>
-        </Modal.Body>
+        </ModalBody>
       </Modal>
     );
   }

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

@@ -3,7 +3,12 @@
 import React from 'react';
 import PropTypes from 'prop-types';
 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';
 
@@ -62,82 +67,91 @@ class ImportCollectionConfigurationModal extends React.Component {
     /* eslint-disable react/no-unescaped-entities */
     return (
       <>
-        <div className="checkbox checkbox-warning">
+        <div className="custom-control custom-checkbox custom-checkbox-warning">
           <input
             id="cbOpt4"
             type="checkbox"
+            className="custom-control-input"
             checked={option.isOverwriteAuthorWithCurrentUser || false} // add ' || false' to avoid uncontrolled input warning
             onChange={() => this.changeHandler({ isOverwriteAuthorWithCurrentUser: !option.isOverwriteAuthorWithCurrentUser })}
           />
-          <label htmlFor="cbOpt4">
+          <label htmlFor="cbOpt4" className="custom-control-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>
         </div>
-        <div className="checkbox checkbox-warning">
+        <div className="custom-control custom-checkbox custom-checkbox-warning">
           <input
             id="cbOpt1"
             type="checkbox"
+            className="custom-control-input"
             checked={option.makePublicForGrant2 || false} // add ' || false' to avoid uncontrolled input warning
             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') })}
             <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') }) }}
             />
           </label>
         </div>
-        <div className="checkbox checkbox-warning">
+        <div className="custom-control custom-checkbox custom-checkbox-warning">
           <input
             id="cbOpt2"
             type="checkbox"
+            className="custom-control-input"
             checked={option.makePublicForGrant4 || false} // add ' || false' to avoid uncontrolled input warning
             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') })}
-            <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>
         </div>
-        <div className="checkbox checkbox-warning">
+        <div className="custom-control custom-checkbox custom-checkbox-warning">
           <input
             id="cbOpt3"
             type="checkbox"
+            className="custom-control-input"
             checked={option.makePublicForGrant5 || false} // add ' || false' to avoid uncontrolled input warning
             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') })}
             <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') }) }}
             />
           </label>
         </div>
-        <div className="checkbox checkbox-default">
+        <div className="custom-control custom-checkbox custom-checkbox-warning">
           <input
             id="cbOpt5"
             type="checkbox"
+            className="custom-control-input"
             checked={option.initPageMetadatas || false} // add ' || false' to avoid uncontrolled input warning
             onChange={() => this.changeHandler({ initPageMetadatas: !option.initPageMetadatas })}
           />
-          <label htmlFor="cbOpt5">
+          <label htmlFor="cbOpt5" className="custom-control-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>
         </div>
-        <div className="checkbox checkbox-default">
+        <div className="custom-control custom-checkbox custom-checkbox-warning">
           <input
             id="cbOpt6"
             type="checkbox"
+            className="custom-control-input"
             checked={option.initHackmdDatas || false} // add ' || false' to avoid uncontrolled input warning
             onChange={() => this.changeHandler({ initHackmdDatas: !option.initHackmdDatas })}
           />
-          <label htmlFor="cbOpt6">
+          <label htmlFor="cbOpt6" className="custom-control-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>
         </div>
       </>
@@ -154,16 +168,17 @@ class ImportCollectionConfigurationModal extends React.Component {
     /* eslint-disable react/no-unescaped-entities */
     return (
       <>
-        <div className="checkbox checkbox-warning">
+        <div className="custom-control custom-checkbox custom-checkbox-warning">
           <input
             id="cbOpt1"
             type="checkbox"
+            className="custom-control-input"
             checked={option.isOverwriteAuthorWithCurrentUser || false} // add ' || false' to avoid uncontrolled input warning
             onChange={() => this.changeHandler({ isOverwriteAuthorWithCurrentUser: !option.isOverwriteAuthorWithCurrentUser })}
           />
-          <label htmlFor="cbOpt1">
+          <label htmlFor="cbOpt1" className="custom-control-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>
         </div>
       </>
@@ -188,19 +203,19 @@ class ImportCollectionConfigurationModal extends React.Component {
     }
 
     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}
-        </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>
-        </Modal.Footer>
+        </ModalFooter>
       </Modal>
     );
   }

+ 18 - 20
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
 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';
 
@@ -74,7 +74,7 @@ export default class ImportCollectionItem extends React.Component {
   renderModeLabel(mode, isColorized = false) {
     const attrMap = MODE_ATTR_MAP[mode];
     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() {
@@ -83,18 +83,18 @@ export default class ImportCollectionItem extends React.Component {
     } = this.props;
 
     return (
-      <div className="checkbox checkbox-info my-0">
+      <div className="custom-control custom-checkbox custom-checkbox-info my-0">
         <input
           type="checkbox"
           id={collectionName}
           name={collectionName}
-          className="form-check-input"
+          className="custom-control-input"
           value={collectionName}
           checked={isSelected}
           disabled={isImporting}
           onChange={this.changeHandler}
         />
-        <label className="text-capitalize form-check-label" htmlFor={collectionName}>
+        <label className="text-capitalize custom-control-label" htmlFor={collectionName}>
           {collectionName}
         </label>
       </div>
@@ -116,7 +116,7 @@ export default class ImportCollectionItem extends React.Component {
         Mode:&nbsp;
         <div className="dropdown d-inline-block">
           <button
-            className={`btn ${btnColor} btn-xs dropdown-toggle`}
+            className={`btn ${btnColor} btn-sm dropdown-toggle`}
             type="button"
             id="ddmMode"
             disabled={isImporting}
@@ -149,7 +149,7 @@ export default class ImportCollectionItem extends React.Component {
     return (
       <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}
         onClick={isConfigButtonAvailable ? this.configButtonClickedHandler : null}
       >
@@ -166,11 +166,11 @@ export default class ImportCollectionItem extends React.Component {
     const total = insertedCount + modifiedCount + errorsCount;
 
     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;
 
     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 */}
             {this.renderCheckbox()}
             {/* right */}
@@ -213,14 +213,12 @@ export default class ImportCollectionItem extends React.Component {
             </span>
           </div>
         </div>
-        { isSelected && (
+        {isSelected && (
           <>
             {this.renderProgressBar()}
-            <div className="panel-body">
-              {this.renderBody()}
-            </div>
+            <div className="card-body">{this.renderBody()}</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);
 
           return (
-            <div className="col-xs-6 my-1" key={collectionName}>
+            <div className="col-md-6 my-1" key={collectionName}>
               <ImportCollectionItem
                 isImporting={isImporting}
                 isImported={collectionProgress ? isImported : false}
@@ -446,17 +446,18 @@ class ImportForm extends React.Component {
       <>
         <form className="form-inline">
           <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')}
             </button>
           </div>
           <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')}
             </button>
           </div>
         </form>
-        <div className="well well-sm small my-4">
+
+        <div className="card well small my-4">
           <ul>
             <li>{t('admin:importer_management.growi_settings.description_of_import_mode.about')}</li>
             <ul>
@@ -473,7 +474,7 @@ class ImportForm extends React.Component {
         {this.renderOthers()}
 
         <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')}
           </button>
           <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;
 
     return (
-      <form className="form-horizontal" onSubmit={this.uploadZipFile}>
+      <form onSubmit={this.uploadZipFile}>
         <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
                 type="file"
                 name="file"
@@ -63,8 +65,8 @@ class UploadForm extends React.Component {
               />
             </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()}>
                 {t('admin:importer_management.growi_settings.upload')}
               </button>

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

@@ -138,12 +138,12 @@ class ImportDataPage extends React.Component {
         <GrowiArchiveSection />
 
         <form
-          className="form-horizontal mt-5"
+          className="mt-5"
           id="importerSettingFormEsa"
           role="form"
         >
           <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">
               <thead>
                 <tr>
@@ -171,37 +171,37 @@ class ImportDataPage extends React.Component {
               </tbody>
             </table>
 
-            <div className="well well-sm mb-0 small">
+            <div className="card well mb-0 small">
               <ul>
                 <li>{t('admin:importer_management.page_skip')}</li>
               </ul>
             </div>
 
-            <div className="form-group">
+            <div className="form-group row">
               <input type="password" name="dummypass" style={{ display: 'none', top: '-100px', left: '-100px' }} />
             </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>
-              <div className="col-xs-6">
+              <div className="col-md-6">
                 <input className="form-control" type="text" name="esaTeamName" value={esaTeamName} onChange={this.handleInputValue} />
               </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>
-              <div className="col-xs-6">
+              <div className="col-md-6">
                 <input className="form-control" type="password" name="esaAccessToken" value={esaAccessToken} onChange={this.handleInputValue} />
               </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
                   id="testConnectionToEsa"
                   type="button"
@@ -211,12 +211,12 @@ class ImportDataPage extends React.Component {
                   value={t('admin:importer_management.import')}
                 />
                 <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
-                    name="Esa"
-                    type="button"
                     id="importFromEsa"
-                    className="btn btn-default btn-esa"
+                    type="button"
+                    name="Esa"
+                    className="btn btn-secondary btn-esa"
                     onClick={this.esaHandleSubmitTest}
                     value={t('admin:importer_management.esa_settings.test_connection')}
                   />
@@ -228,12 +228,12 @@ class ImportDataPage extends React.Component {
         </form>
 
         <form
-          className="form-horizontal mt-5"
+          className="mt-5"
           id="importerSettingFormQiita"
           role="form"
         >
           <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">
               <thead>
                 <tr>
@@ -265,36 +265,36 @@ class ImportDataPage extends React.Component {
                 </tr>
               </tbody>
             </table>
-            <div className="well well-sm mb-0 small">
+            <div className="card well mb-0 small">
               <ul>
                 <li>{t('admin:importer_management.page_skip')}</li>
               </ul>
             </div>
 
-            <div className="form-group">
+            <div className="form-group row">
               <input type="password" name="dummypass" style={{ display: 'none', top: '-100px', left: '-100px' }} />
             </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>
-              <div className="col-xs-6">
+              <div className="col-md-6">
                 <input className="form-control" type="text" name="qiitaTeamName" value={qiitaTeamName} onChange={this.handleInputValue} />
               </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>
-              <div className="col-xs-6">
+              <div className="col-md-6">
                 <input className="form-control" type="password" name="qiitaAccessToken" value={qiitaAccessToken} onChange={this.handleInputValue} />
               </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
                   id="testConnectionToQiita"
                   type="button"
@@ -304,12 +304,12 @@ class ImportDataPage extends React.Component {
                   value={t('admin:importer_management.import')}
                 />
                 <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
                     name="Qiita"
                     type="button"
                     id="importFromQiita"
-                    className="btn btn-default btn-qiita"
+                    className="btn btn-secondary btn-qiita"
                     onClick={this.qiitaHandleSubmitTest}
                     value={t('admin:importer_management.qiita_settings.test_connection')}
                   />

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

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

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

@@ -1,5 +1,6 @@
 /* eslint-disable react/no-danger */
 import React from 'react';
+
 import PropTypes from 'prop-types';
 import { withTranslation } from 'react-i18next';
 import loggerFactory from '@alias/logger';
@@ -7,8 +8,10 @@ import loggerFactory from '@alias/logger';
 import { createSubscribedElement } from '../../UnstatedUtils';
 import { toastSuccess, toastError } from '../../../util/apiNotification';
 
+
 import AppContainer from '../../../services/AppContainer';
 import AdminMarkDownContainer from '../../../services/AdminMarkDownContainer';
+import AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow';
 
 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') };
 
     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>
+        <p className="form-text text-muted" dangerouslySetInnerHTML={helpLineBreak} />
       </div>
     );
   }
@@ -67,46 +69,34 @@ class LineBreakForm extends React.Component {
     const helpLineBreakInComment = { __html: t('admin:markdown_setting.lineBreak_options.enable_lineBreak_for_comment_desc') };
 
     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>
+        <p className="form-text text-muted" dangerouslySetInnerHTML={helpLineBreakInComment} />
       </div>
     );
   }
 
   render() {
-    const { t, adminMarkDownContainer } = this.props;
+    const { adminMarkDownContainer } = this.props;
 
     return (
       <React.Fragment>
-        <fieldset className="row">
+        <fieldset className="form-group row row-cols-1 row-cols-md-2 mx-3">
           {this.renderLineBreakOption()}
           {this.renderLineBreakInCommentOption()}
         </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>
     );
   }

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

@@ -1,4 +1,5 @@
 import React from 'react';
+import { Card, CardBody } from 'reactstrap';
 import PropTypes from 'prop-types';
 import { withTranslation } from 'react-i18next';
 
@@ -37,25 +38,25 @@ class MarkdownSetting extends React.Component {
     return (
       <React.Fragment>
         {/* 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 */}
-        <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 */}
-        <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>
     );
   }

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

@@ -1,4 +1,5 @@
 import React from 'react';
+
 import PropTypes from 'prop-types';
 import { withTranslation } from 'react-i18next';
 import loggerFactory from '@alias/logger';
@@ -8,6 +9,7 @@ import { toastSuccess, toastError } from '../../../util/apiNotification';
 
 import AppContainer from '../../../services/AppContainer';
 import AdminMarkDownContainer from '../../../services/AdminMarkDownContainer';
+import AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow';
 
 const logger = loggerFactory('growi:markdown:presentation');
 
@@ -38,70 +40,87 @@ class PresentationForm extends React.Component {
     const { pageBreakSeparator, pageBreakCustomSeparator } = adminMarkDownContainer.state;
 
     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')}
         </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>
-          </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>
-          </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>
-          </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>
 
+        <AdminUpdateButtonRow onClick={this.onClickSubmit} disabled={adminMarkDownContainer.state.retrieveError != null} />
       </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 (
       <>
-        <div className="m-t-15">
+        <div className="mt-4">
           <div className="d-flex justify-content-between">
             {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' })}
             </p>
           </div>
@@ -52,10 +52,10 @@ class WhiteListInput extends React.Component {
             onChange={(e) => { adminMarkDownContainer.setState({ tagWhiteList: e.target.value }) }}
           />
         </div>
-        <div className="m-t-15">
+        <div className="mt-4">
           <div className="d-flex justify-content-between">
             {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' })}
             </p>
           </div>

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

@@ -1,4 +1,5 @@
 import React from 'react';
+
 import PropTypes from 'prop-types';
 import { withTranslation } from 'react-i18next';
 import loggerFactory from '@alias/logger';
@@ -9,6 +10,7 @@ import { tags, attrs } from '../../../../../lib/service/xss/recommended-whitelis
 
 import AppContainer from '../../../services/AppContainer';
 import AdminMarkDownContainer from '../../../services/AdminMarkDownContainer';
+import AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow';
 
 import WhiteListInput from './WhiteListInput';
 
@@ -40,76 +42,87 @@ class XssForm extends React.Component {
     const { xssOption } = adminMarkDownContainer.state;
 
     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>
-          </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="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>
+                <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>
+              </label>
             </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>
-          </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>
-      </fieldset>
+      </div>
     );
   }
 
@@ -119,31 +132,30 @@ class XssForm extends React.Component {
 
     return (
       <React.Fragment>
-        <form className="row">
+        <fieldset className="col-12">
           <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
                   type="checkbox"
+                  className="custom-control-input"
                   id="XssEnable"
-                  className="form-check-input"
                   name="isEnabledXss"
                   checked={isEnabledXss}
                   onChange={adminMarkDownContainer.switchEnableXss}
                 />
-                <label htmlFor="XssEnable">
+                <label className="custom-control-label w-100" htmlFor="XssEnable">
                   {t('admin:markdown_setting.xss_options.enable_xss_prevention')}
                 </label>
               </div>
             </div>
-            {isEnabledXss && this.xssOptions()}
           </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>
-        </form>
+        </fieldset>
+        <AdminUpdateButtonRow onClick={this.onClickSubmit} disabled={adminMarkDownContainer.state.retrieveError != null} />
       </React.Fragment>
     );
   }

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

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

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

@@ -76,71 +76,80 @@ class GlobalNotificationList extends React.Component {
           return (
             <tr key={notification._id}>
               <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>
                 {notification.triggerPath}
               </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
-                  </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
-                  </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-warning" data-toggle="tooltip" data-placement="top" title="Page Move">
                     <i className="icon-action-redo"></i> MOVE
-                  </span>
+                  </li>
                 )}
-                {notification.triggerEvents.includes('pageDelete') && (
-                  <span className="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
-                  </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
-                  </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-light" data-toggle="tooltip" data-placement="top" title="New Comment">
                     <i className="icon-fw icon-bubble"></i> POST
-                  </span>
+                  </li>
                 )}
+                </ul>
               </td>
               <td>
                 {notification.__t === 'mail'
                   && <span data-toggle="tooltip" data-placement="top" title="Email"><i className="ti-email"></i> {notification.toEmail}</span>}
                 {notification.__t === 'slack'
-                  && <span data-toggle="tooltip" data-placement="top" title="Slack"><i className="fa fa-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 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>
                   </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" href={urljoin('/admin/global-notification/', notification._id)}>
+                      <i className="icon-fw icon-note"></i> {t('Edit')}
+                    </a>
+                    <a className="dropdown-item" onClick={() => this.openConfirmationModal(notification)}>
+                      <i className="icon-fw icon-fire text-danger"></i> {t('Delete')}
+                    </a>
+                  </div>
                 </div>
               </td>
             </tr>

+ 144 - 105
src/client/js/components/Admin/Notification/ManageGlobalNotification.jsx

@@ -100,10 +100,15 @@ class ManageGlobalNotification extends React.Component {
     return (
       <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">
+            <button type="button" className="btn page-link text-dark d-inline-block">
+              <i className="icon-fw ti-arrow-left" aria-hidden="true"></i>
+              {t('notification_setting.back_to_list')}
+            </button>
+          </a>
+        </div>
+
 
         <div className="row">
           <div className="m-t-20 form-box col-md-12">
@@ -111,25 +116,26 @@ class ManageGlobalNotification extends React.Component {
           </div>
 
           <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">
-              <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>
 
+            <h3>{t('notification_setting.notify_to')}</h3>
             <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
+                  className="custom-control-input"
                   type="radio"
                   id="mail"
                   name="notifyToType"
@@ -137,12 +143,13 @@ class ManageGlobalNotification extends React.Component {
                   checked={this.state.notifyToType === 'mail'}
                   onChange={() => { this.onChangeNotifyToType('mail') }}
                 />
-                <label htmlFor="mail">
+                <label className="custom-control-label" htmlFor="mail">
                   <p className="font-weight-bold">Email</p>
                 </label>
               </div>
-              <div className="radio radio-primary">
+              <div className="custom-control custom-radio ml-2">
                 <input
+                  className="custom-control-input"
                   type="radio"
                   id="slack"
                   name="notifyToType"
@@ -150,7 +157,7 @@ class ManageGlobalNotification extends React.Component {
                   checked={this.state.notifyToType === 'slack'}
                   onChange={() => { this.onChangeNotifyToType('slack') }}
                 />
-                <label htmlFor="slack">
+                <label className="custom-control-label" htmlFor="slack">
                   <p className="font-weight-bold">Slack</p>
                 </label>
               </div>
@@ -158,107 +165,139 @@ class ManageGlobalNotification extends React.Component {
 
             {this.state.notifyToType === 'mail'
               ? (
-                <div className="form-group notify-to-option" id="mail-input">
-                  <input
-                    className="form-control"
-                    type="text"
-                    name="toEmail"
-                    placeholder="Email"
-                    value={this.state.emailToSend}
-                    onChange={(e) => { this.onChangeEmailToSend(e.target.value) }}
-                  />
-                  <p className="help">
+                <>
+                  <div className="input-group notify-to-option" id="mail-input">
+                    <div className="input-group-prepend">
+                      <span className="input-group-text" id="mail-addon"><i className="ti-email" /></span>
+                    </div>
+                    <input
+                      className="form-control"
+                      type="text"
+                      aria-describedby="mail-addon"
+                      name="toEmail"
+                      placeholder="Email"
+                      value={this.state.emailToSend}
+                      onChange={(e) => { this.onChangeEmailToSend(e.target.value) }}
+                    />
+
+                  </div>
+                  <p className="p-2">
                     <b>Hint: </b>
                     <a href="https://ifttt.com/create" target="blank">{t('notification_setting.email.ifttt_link')}
                       <i className="icon-share-alt" />
                     </a>
                   </p>
-                </div>
+                </>
               )
               : (
-                <div className="form-group notify-to-option" id="slack-input">
-                  <input
-                    className="form-control"
-                    type="text"
-                    name="notificationGlobal[slackChannels]"
-                    placeholder="Slack Channel"
-                    value={this.state.slackChannelToSend}
-                    onChange={(e) => { this.onChangeSlackChannelToSend(e.target.value) }}
-                  />
-                </div>
+                <>
+                  <div className="input-group notify-to-option" id="slack-input">
+                    <div className="input-group-prepend">
+                      <span className="input-group-text" id="slack-channel-addon"><i className="fa fa-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 className="col-sm-offset-1 col-sm-5">
+          <div className="offset-1 col-sm-5">
             <div className="form-group">
               <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="warning"
+                  event="pageMove"
+                  checked={this.state.triggerEvents.has('pageMove')}
+                  onChange={() => this.onChangeTriggerEvents('pageMove')}
+                >
+                  <span className="badge badge-pill badge-warning">
+                    <i className="icon-action-redo mr-1" />MOVE
+                  </span>
+                </TriggerEventCheckBox>
+              </div>
+              <div className="my-1">
+                <TriggerEventCheckBox
+                  checkbox="danger"
+                  event="pageDelete"
+                  checked={this.state.triggerEvents.has('pageDelete')}
+                  onChange={() => this.onChangeTriggerEvents('pageDelete')}
+                >
+                  <span className="badge badge-pill badge-danger">
+                    <i className="icon-fire mr-1" />DELETE
+                  </span>
+                </TriggerEventCheckBox>
+              </div>
+              <div className="my-1">
+                <TriggerEventCheckBox
+                  checkbox="info"
+                  event="pageLike"
+                  checked={this.state.triggerEvents.has('pageLike')}
+                  onChange={() => this.onChangeTriggerEvents('pageLike')}
+                >
+                  <span className="badge badge-pill badge-info">
+                    <i className="icon-like mr-1" />LIKE
+                  </span>
+                </TriggerEventCheckBox>
+              </div>
+              <div className="my-1">
+                <TriggerEventCheckBox
+                  checkbox="secondary"
+                  event="comment"
+                  checked={this.state.triggerEvents.has('comment')}
+                  onChange={() => this.onChangeTriggerEvents('comment')}
+                >
+                  <span className="badge badge-pill badge-secondary">
+                    <i className="icon-bubble mr-1" />POST
+                  </span>
+                </TriggerEventCheckBox>
+              </div>
 
             </div>
           </div>
-
-          <AdminUpdateButtonRow
-            onClick={this.submitHandler}
-            disabled={this.state.retrieveError != null}
-          />
-
         </div>
 
+        <AdminUpdateButtonRow
+          onClick={this.submitHandler}
+          disabled={this.state.retrieveError != null}
+        />
+
       </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 { withTranslation } from 'react-i18next';
 
-import Modal from 'react-bootstrap/es/Modal';
+import {
+  Modal, ModalHeader, ModalBody, ModalFooter,
+} from 'reactstrap';
 
 class NotificationDeleteModal extends React.PureComponent {
 
   render() {
     const { t, notificationForConfiguration } = this.props;
     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>
             {t('notification_setting.delete_notification_pattern_desc1', { path: notificationForConfiguration.triggerPath })}
           </p>
-          <span className="text-danger">
+          <p className="text-danger">
             {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}>
             <i className="icon icon-fire"></i> {t('Delete')}
           </button>
-        </Modal.Footer>
+        </ModalFooter>
       </Modal>
     );
   }

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

@@ -1,6 +1,9 @@
 import React from 'react';
 import PropTypes from 'prop-types';
 import { withTranslation } from 'react-i18next';
+import {
+  TabContent, TabPane, Nav, NavItem, NavLink,
+} from 'reactstrap';
 
 import loggerFactory from '@alias/logger';
 
@@ -18,6 +21,18 @@ const logger = loggerFactory('growi:NotificationSetting');
 
 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() {
     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() {
+    const { activeTab, activeComponents } = this.state;
 
     return (
       <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>
     );
   }

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

@@ -39,25 +39,22 @@ class SlackAppConfiguration extends React.Component {
 
     return (
       <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" onClick={() => adminNotificationContainer.switchSlackOption('Incoming Webhooks')}>Slack Incoming Webhooks</a>
+                <a className="dropdown-item" onClick={() => adminNotificationContainer.switchSlackOption('App')}>Slack App</a>
               </div>
             </div>
           </div>
@@ -66,9 +63,9 @@ class SlackAppConfiguration extends React.Component {
           <React.Fragment>
             <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
                   className="form-control"
                   type="text"
@@ -78,20 +75,21 @@ class SlackAppConfiguration extends React.Component {
               </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
-                    id="cbPrioritizeIWH"
                     type="checkbox"
+                    className="custom-control-input"
+                    id="cbPrioritizeIWH"
                     checked={adminNotificationContainer.state.isIncomingWebhookPrioritized || false}
                     onChange={() => { adminNotificationContainer.switchIsIncomingWebhookPrioritized() }}
                   />
-                  <label htmlFor="cbPrioritizeIWH">
+                  <label className="custom-control-label" htmlFor="cbPrioritizeIWH">
                     {t('notification_setting.prioritize_webhook')}
                   </label>
                 </div>
-                <p className="help-block">
+                <p className="form-text text-muted">
                   {t('notification_setting.prioritize_webhook_desc')}
                 </p>
               </div>
@@ -102,24 +100,24 @@ class SlackAppConfiguration extends React.Component {
             <React.Fragment>
               <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 */}
                 <span dangerouslySetInnerHTML={{ __html: t('notification_setting.slack_app_configuration_desc') }} />
-                <br /><br />
+                <br />
                 <a
                   href="#slack-incoming-webhooks"
                   data-toggle="tab"
                   onClick={() => adminNotificationContainer.switchSlackOption('Incoming Webhooks')}
                 >
                   {t('notification_setting.use_instead')}
-                </a>{' '}
+                </a>
               </div>
 
               <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
                     className="form-control"
                     type="text"

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

@@ -6,15 +6,15 @@ const TriggerEventCheckBox = (props) => {
   const { t } = props;
 
   return (
-    <div className="checkbox checkbox-inverse">
+    <div className={`custom-control custom-checkbox custom-checkbox-${props.checkbox}`}>
       <input
+        className="custom-control-input"
         type="checkbox"
         id={`trigger-event-${props.event}`}
-        value={props.event}
         checked={props.checked}
         onChange={props.onChange}
       />
-      <label htmlFor={`trigger-event-${props.event}`}>
+      <label className="custom-control-label" htmlFor={`trigger-event-${props.event}`}>
         {props.children}{' '}
         {t(`notification_setting.event_${props.event}`)}
       </label>
@@ -26,6 +26,7 @@ const TriggerEventCheckBox = (props) => {
 TriggerEventCheckBox.propTypes = {
   t: PropTypes.func.isRequired, // i18next
 
+  checkbox: PropTypes.string.isRequired,
   checked: PropTypes.bool.isRequired,
   onChange: PropTypes.func.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 (
       <React.Fragment>
         <tr className="admin-notif-row" key={notification._id}>
-          <td>
+          <td className="px-4">
             {notification.pathPattern}
           </td>
-          <td>
-            {notification.channel}
+          <td className="px-4">
+            <span data-toggle="tooltip" data-placement="top" title="Slack"><i className="fa fa-hashtag"></i> {notification.channel}</span>
           </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>
         </tr>
       </React.Fragment>

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

@@ -82,7 +82,7 @@ class UserTriggerNotification extends React.Component {
 
     return (
       <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">
           <thead>
@@ -103,22 +103,30 @@ class UserTriggerNotification extends React.Component {
                   placeholder="e.g. /projects/xxx/MTG/*"
                   onChange={(e) => { this.changePathPattern(e.target.value) }}
                 />
-                {/* eslint-disable-next-line react/no-danger */}
-                <p className="help-block" dangerouslySetInnerHTML={{ __html: t('notification_setting.pattern_desc') }} />
+                <p className="p-2 mb-0">
+                  {/* eslint-disable-next-line react/no-danger */}
+                  <span dangerouslySetInnerHTML={{ __html: t('notification_setting.pattern_desc') }} />
+                </p>
               </td>
 
               <td>
-                <input
-                  className="form-control form-inline"
-                  type="text"
-                  name="channel"
-                  value={this.state.channel}
-                  placeholder="e.g. project-xxx"
-                  onChange={(e) => { this.changeChannel(e.target.value) }}
-                />
-                {/* eslint-disable-next-line react/no-danger */}
-                <p className="help-block" dangerouslySetInnerHTML={{ __html: t('notification_setting.channel_desc') }} />
-
+                <div className="input-group notify-to-option" id="slack-input">
+                  <div className="input-group-prepend">
+                    <span className="input-group-text"><i className="fa fa-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>
                 <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 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
                 id="isBasicEnabled"
+                className="custom-control-input"
                 type="checkbox"
                 checked={adminGeneralSecurityContainer.state.isBasicEnabled}
                 onChange={() => { adminGeneralSecurityContainer.switchIsBasicEnabled() }}
               />
-              <label htmlFor="isBasicEnabled">
+              <label className="custom-control-label" htmlFor="isBasicEnabled">
                 { t('security_setting.Basic.enable_basic') }
               </label>
             </div>
-            <p className="help-block">
+            <p className="form-text text-muted">
               <small>
                 <span dangerouslySetInnerHTML={{ __html: t('security_setting.Basic.desc_1') }} /><br />
                 { t('security_setting.Basic.desc_2')}
               </small>
             </p>
             {(!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>
 
         {isBasicEnabled && (
         <React.Fragment>
           <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
                   id="bindByEmail-basic"
+                  className="custom-control-input"
                   type="checkbox"
                   checked={adminBasicSecurityContainer.state.isSameUsernameTreatedAsIdenticalUser || false}
                   onChange={() => { adminBasicSecurityContainer.switchIsSameUsernameTreatedAsIdenticalUser() }}
                 />
                 <label
+                  className="custom-control-label"
                   htmlFor="bindByEmail-basic"
                   dangerouslySetInnerHTML={{ __html: t('security_setting.Treat username matching as identical', 'username') }}
                 />
               </div>
-              <p className="help-block">
+              <p className="form-text text-muted">
                 <small dangerouslySetInnerHTML={{ __html: t('security_setting.Treat username matching as identical_warn', 'username') }} />
               </p>
             </div>
           </div>
 
           <div className="row my-3">
-            <div className="col-xs-offset-4 col-xs-5">
+            <div className="offset-4 col-5">
               <button type="button" className="btn btn-primary" disabled={adminBasicSecurityContainer.state.retrieveError != null} onClick={this.onClickSubmit}>
                 {t('Update')}
               </button>

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

@@ -68,43 +68,41 @@ class GitHubSecurityManagement extends React.Component {
           </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
                 id="isGitHubEnabled"
+                className="custom-control-input"
                 type="checkbox"
                 checked={adminGeneralSecurityContainer.state.isGitHubEnabled || false}
                 onChange={() => { adminGeneralSecurityContainer.switchIsGitHubOAuthEnabled() }}
               />
-              <label htmlFor="isGitHubEnabled">
+              <label className="custom-control-label" htmlFor="isGitHubEnabled">
                 {t('security_setting.OAuth.GitHub.enable_github')}
               </label>
             </div>
             {(!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 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
               className="form-control"
               type="text"
               value={adminGitHubSecurityContainer.state.appSiteUrl}
               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 && (
               <div className="alert alert-danger">
                 <i
                   className="icon-exclamation"
                   // 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>
             )}
@@ -118,8 +116,8 @@ class GitHubSecurityManagement extends React.Component {
             <h3 className="border-bottom">{t('security_setting.configuration')}</h3>
 
             <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
                   className="form-control"
                   type="text"
@@ -127,15 +125,15 @@ class GitHubSecurityManagement extends React.Component {
                   value={adminGitHubSecurityContainer.state.githubClientId || ''}
                   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' }) }} />
                 </p>
               </div>
             </div>
 
             <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
                   className="form-control"
                   type="text"
@@ -143,34 +141,36 @@ class GitHubSecurityManagement extends React.Component {
                   defaultValue={adminGitHubSecurityContainer.state.githubClientSecret || ''}
                   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' }) }} />
                 </p>
               </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="offset-3 col-6 text-left">
+                <div className="custom-control custom-checkbox custom-checkbox-success">
                   <input
                     id="bindByUserNameGitHub"
+                    className="custom-control-input"
                     type="checkbox"
                     checked={adminGitHubSecurityContainer.state.isSameUsernameTreatedAsIdenticalUser || false}
                     onChange={() => { adminGitHubSecurityContainer.switchIsSameUsernameTreatedAsIdenticalUser() }}
                   />
                   <label
+                    className="custom-control-label"
                     htmlFor="bindByUserNameGitHub"
                     dangerouslySetInnerHTML={{ __html: t('security_setting.Treat email matching as identical') }}
                   />
                 </div>
-                <p className="help-block">
+                <p className="form-text text-muted">
                   <small dangerouslySetInnerHTML={{ __html: t('security_setting.Treat email matching as identical_warn') }} />
                 </p>
               </div>
             </div>
 
             <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}>
                   {t('Update')}
                 </div>

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

@@ -68,43 +68,41 @@ class GoogleSecurityManagement extends React.Component {
           </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
                 id="isGoogleEnabled"
+                className="custom-control-input"
                 type="checkbox"
                 checked={adminGeneralSecurityContainer.state.isGoogleEnabled || false}
                 onChange={() => { adminGeneralSecurityContainer.switchIsGoogleOAuthEnabled() }}
               />
-              <label htmlFor="isGoogleEnabled">
+              <label className="custom-control-label" htmlFor="isGoogleEnabled">
                 {t('security_setting.OAuth.Google.enable_google')}
               </label>
             </div>
             {(!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 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
               className="form-control"
               type="text"
               value={adminGoogleSecurityContainer.state.callbackUrl}
               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 && (
               <div className="alert alert-danger">
                 <i
                   className="icon-exclamation"
                   // 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>
             )}
@@ -118,8 +116,8 @@ class GoogleSecurityManagement extends React.Component {
             <h3 className="border-bottom">{t('security_setting.configuration')}</h3>
 
             <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
                   className="form-control"
                   type="text"
@@ -127,15 +125,15 @@ class GoogleSecurityManagement extends React.Component {
                   defaultValue={adminGoogleSecurityContainer.state.googleClientId || ''}
                   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' }) }} />
                 </p>
               </div>
             </div>
 
             <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
                   className="form-control"
                   type="text"
@@ -143,34 +141,36 @@ class GoogleSecurityManagement extends React.Component {
                   defaultValue={adminGoogleSecurityContainer.state.googleClientSecret || ''}
                   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' }) }} />
                 </p>
               </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="offset-3 col-6">
+                <div className="custom-control custom-checkbox custom-checkbox-success">
                   <input
                     id="bindByUserNameGoogle"
+                    className="custom-control-input"
                     type="checkbox"
                     checked={adminGoogleSecurityContainer.state.isSameUsernameTreatedAsIdenticalUser || false}
                     onChange={() => { adminGoogleSecurityContainer.switchIsSameUsernameTreatedAsIdenticalUser() }}
                   />
                   <label
+                    className="custom-control-label"
                     htmlFor="bindByUserNameGoogle"
                     dangerouslySetInnerHTML={{ __html: t('security_setting.Treat email matching as identical') }}
                   />
                 </div>
-                <p className="help-block">
+                <p className="form-text text-muted">
                   <small dangerouslySetInnerHTML={{ __html: t('security_setting.Treat email matching as identical_warn') }} />
                 </p>
               </div>
             </div>
 
             <div className="row my-3">
-              <div className="col-xs-offset-3 col-xs-5">
+              <div className="offset-3 col-5">
                 <button
                   type="button"
                   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>
         {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>}
-        <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
               className="form-control"
               name="username"
@@ -99,9 +99,9 @@ class LdapAuthTest extends React.Component {
             />
           </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
               className="form-control"
               type="password"
@@ -111,13 +111,15 @@ class LdapAuthTest extends React.Component {
             />
           </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>
 
     );

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

@@ -2,7 +2,11 @@ import React from 'react';
 import PropTypes from 'prop-types';
 import { withTranslation } from 'react-i18next';
 
-import Modal from 'react-bootstrap/es/Modal';
+import {
+  Modal,
+  ModalHeader,
+  ModalBody,
+} from 'reactstrap';
 
 import { createSubscribedElement } from '../../UnstatedUtils';
 
@@ -42,20 +46,18 @@ class LdapAuthTestModal extends React.Component {
   render() {
 
     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
             username={this.state.username}
             password={this.state.password}
             onChangeUsername={this.onChangeUsername}
             onChangePassword={this.onChangePassword}
           />
-        </Modal.Body>
+        </ModalBody>
       </Modal>
     );
   }

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

@@ -73,24 +73,22 @@ class LdapSecuritySetting extends React.Component {
           LDAP
         </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
                 id="isLdapEnabled"
+                className="custom-control-input"
                 type="checkbox"
                 checked={isLdapEnabled}
                 onChange={() => { adminGeneralSecurityContainer.switchIsLdapEnabled() }}
               />
-              <label htmlFor="isLdapEnabled">
+              <label className="custom-control-label" htmlFor="isLdapEnabled">
                 {t('security_setting.ldap.enable_ldap')}
               </label>
             </div>
             {(!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>
 
@@ -100,9 +98,11 @@ class LdapSecuritySetting extends React.Component {
 
             <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
                   className="form-control"
                   type="text"
@@ -112,7 +112,7 @@ class LdapSecuritySetting extends React.Component {
                 />
                 <small>
                   <p
-                    className="help-block"
+                    className="form-text text-muted"
                     // eslint-disable-next-line react/no-danger
                     dangerouslySetInnerHTML={{ __html: t('security_setting.ldap.server_url_detail') }}
                   />
@@ -121,36 +121,41 @@ class LdapSecuritySetting extends React.Component {
               </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_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" onClick={() => { adminLdapSecurityContainer.changeLdapBindMode(true) }}>
+                      {t('security_setting.ldap.bind_user')}
+                    </a>
+                    <a className="dropdown-item" onClick={() => { adminLdapSecurityContainer.changeLdapBindMode(false) }}>
+                      {t('security_setting.ldap.bind_manager')}
+                    </a>
                   </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
                   className="form-control"
                   type="text"
@@ -159,7 +164,7 @@ class LdapSecuritySetting extends React.Component {
                   onChange={e => adminLdapSecurityContainer.changeBindDN(e.target.value)}
                 />
                 {(adminLdapSecurityContainer.state.isUserBind === true) ? (
-                  <p className="help-block passport-ldap-userbind">
+                  <p className="form-text text-muted passport-ldap-userbind">
                     <small>
                       {t('security_setting.ldap.bind_DN_user_detail1')}<br />
                       {/* eslint-disable-next-line react/no-danger */}
@@ -170,7 +175,7 @@ class LdapSecuritySetting extends React.Component {
                   </p>
                 )
                   : (
-                    <p className="help-block passport-ldap-managerbind">
+                    <p className="form-text text-muted passport-ldap-managerbind">
                       <small>
                         {t('security_setting.ldap.bind_DN_manager_detail')}<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 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) ? (
-                  <p className="help-block passport-ldap-userbind">
+                  <p className="well card passport-ldap-userbind">
                     <small>
                       {t('security_setting.ldap.bind_DN_password_user_detail')}
                     </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>
                           {t('security_setting.ldap.bind_DN_password_manager_detail')}
                         </small>
@@ -210,9 +217,11 @@ class LdapSecuritySetting extends React.Component {
               </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
                   className="form-control"
                   type="text"
@@ -220,7 +229,7 @@ class LdapSecuritySetting extends React.Component {
                   defaultValue={adminLdapSecurityContainer.state.ldapSearchFilter || ''}
                   onChange={e => adminLdapSecurityContainer.changeSearchFilter(e.target.value)}
                 />
-                <p className="help-block">
+                <p className="form-text text-muted">
                   <small>
                     {t('security_setting.ldap.search_filter_detail1')}<br />
                     {/* 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') }} />
                   </small>
                 </p>
-                <p className="help-block">
+                <p className="form-text text-muted">
                   <small>
                     {t('security_setting.example')}1 - {t('security_setting.ldap.search_filter_example1')}:
                     <code>(|(uid={'{{ username }}'})(mail={'{{ username }}'}))</code><br />
@@ -244,9 +253,11 @@ class LdapSecuritySetting extends React.Component {
               Attribute Mapping ({t('security_setting.optional')})
             </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
                   className="form-control"
                   type="text"
@@ -255,38 +266,42 @@ class LdapSecuritySetting extends React.Component {
                   defaultValue={adminLdapSecurityContainer.state.ldapAttrMapUsername || ''}
                   onChange={e => adminLdapSecurityContainer.changeAttrMapUsername(e.target.value)}
                 />
-                <p className="help-block">
+                <p className="form-text text-muted">
                   {/* eslint-disable-next-line react/no-danger */}
                   <small dangerouslySetInnerHTML={{ __html: t('security_setting.ldap.username_detail') }} />
                 </p>
               </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
-                    id="isSameUsernameTreatedAsIdenticalUser"
                     type="checkbox"
+                    className="custom-control-input"
+                    id="isSameUsernameTreatedAsIdenticalUser"
                     checked={adminLdapSecurityContainer.state.isSameUsernameTreatedAsIdenticalUser}
                     onChange={() => { adminLdapSecurityContainer.switchIsSameUsernameTreatedAsIdenticalUser() }}
                   />
                   <label
+                    className="custom-control-label"
                     htmlFor="isSameUsernameTreatedAsIdenticalUser"
                     // eslint-disable-next-line react/no-danger
                     dangerouslySetInnerHTML={{ __html: t('security_setting.Treat username matching as identical') }}
                   />
                 </div>
-                <p className="help-block">
+                <p className="form-text text-muted">
                   {/* eslint-disable-next-line react/no-danger */}
                   <small dangerouslySetInnerHTML={{ __html: t('security_setting.Treat username matching as identical_warn') }} />
                 </p>
               </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
                   className="form-control"
                   type="text"
@@ -295,7 +310,7 @@ class LdapSecuritySetting extends React.Component {
                   defaultValue={adminLdapSecurityContainer.state.ldapAttrMapMail || ''}
                   onChange={e => adminLdapSecurityContainer.changeAttrMapMail(e.target.value)}
                 />
-                <p className="help-block">
+                <p className="form-text text-muted">
                   <small>
                     {t('security_setting.ldap.mail_detail')}
                   </small>
@@ -303,9 +318,11 @@ class LdapSecuritySetting extends React.Component {
               </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
                   className="form-control"
                   type="text"
@@ -313,7 +330,7 @@ class LdapSecuritySetting extends React.Component {
                   defaultValue={adminLdapSecurityContainer.state.ldapAttrMapName || ''}
                   onChange={e => adminLdapSecurityContainer.changeAttrMapName(e.target.value)}
                 />
-                <p className="help-block">
+                <p className="form-text text-muted">
                   <small>
                     {t('security_setting.ldap.name_detail')}
                   </small>
@@ -326,9 +343,11 @@ class LdapSecuritySetting extends React.Component {
               {t('security_setting.ldap.group_search_filter')} ({t('security_setting.optional')})
             </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
                   className="form-control"
                   type="text"
@@ -336,7 +355,7 @@ class LdapSecuritySetting extends React.Component {
                   defaultValue={adminLdapSecurityContainer.state.ldapGroupSearchBase || ''}
                   onChange={e => adminLdapSecurityContainer.changeGroupSearchBase(e.target.value)}
                 />
-                <p className="help-block">
+                <p className="form-text text-muted">
                   <small>
                     {/* eslint-disable-next-line react/no-danger */}
                     <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 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
                   className="form-control"
                   type="text"
@@ -356,7 +377,7 @@ class LdapSecuritySetting extends React.Component {
                   defaultValue={adminLdapSecurityContainer.state.ldapGroupSearchFilter || ''}
                   onChange={e => adminLdapSecurityContainer.changeGroupSearchFilter(e.target.value)}
                 />
-                <p className="help-block">
+                <p className="form-text text-muted">
                   <small>
                     {/* eslint-disable react/no-danger */}
                     <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 */}
                   </small>
                 </p>
-                <p className="help-block">
+                <p className="form-text text-muted">
                   <small>
                     {t('security_setting.example')}:
                     {/* eslint-disable-next-line react/no-danger */}
@@ -375,9 +396,11 @@ class LdapSecuritySetting extends React.Component {
               </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
                   className="form-control"
                   type="text"
@@ -386,14 +409,14 @@ class LdapSecuritySetting extends React.Component {
                   defaultValue={adminLdapSecurityContainer.state.ldapGroupDnProperty || ''}
                   onChange={e => adminLdapSecurityContainer.changeGroupDnProperty(e.target.value)}
                 />
-                <p className="help-block">
+                <p className="form-text text-muted">
                   {/* eslint-disable-next-line react/no-danger */}
                   <small dangerouslySetInnerHTML={{ __html: t('security_setting.ldap.group_search_user_DN_property_detail') }} />
                 </p>
               </div>
             </div>
             <div className="row my-3">
-              <div className="col-xs-offset-3 col-xs-5">
+              <div className="offset-3 col-5">
                 <button
                   type="button"
                   className="btn btn-primary"
@@ -402,7 +425,12 @@ class LdapSecuritySetting extends React.Component {
                 >
                   {t('Update')}
                 </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>
 

+ 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="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
-                id="isLocalEnabled"
                 type="checkbox"
-                checked={adminGeneralSecurityContainer.state.isLocalEnabled}
-                onChange={() => { adminGeneralSecurityContainer.switchIsLocalEnabled() }}
+                className="custom-control-input"
+                id="isLocalEnabled"
+                checked={isLocalEnabled}
+                onChange={() => adminGeneralSecurityContainer.switchIsLocalEnabled()}
                 disabled={adminLocalSecurityContainer.state.useOnlyEnvVars}
               />
-              <label htmlFor="isLocalEnabled">
+              <label className="custom-control-label" htmlFor="isLocalEnabled">
                 {t('security_setting.Local.enable_local')}
               </label>
             </div>
             {(!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>
 
@@ -101,74 +99,63 @@ class LocalSecuritySetting extends React.Component {
 
             <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" onClick={() => { adminLocalSecurityContainer.changeRegistrationMode('Open') }}>
+                      {t('security_setting.registration_mode.open')}
+                    </a>
+                    <a className="dropdown-item" onClick={() => { adminLocalSecurityContainer.changeRegistrationMode('Restricted') }}>
+                      {t('security_setting.registration_mode.restricted')}
+                    </a>
+                    <a className="dropdown-item" onClick={() => { adminLocalSecurityContainer.changeRegistrationMode('Closed') }}>
+                      {t('security_setting.registration_mode.closed')}
+                    </a>
                   </div>
-                  <p className="help-block">
-                    {t('security_setting.Register limitation desc')}
-                  </p>
                 </div>
+
+                <p className="form-text text-muted small">
+                  {t('security_setting.Register limitation desc')}
+                </p>
               </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 className="row my-3">
-              <div className="col-xs-offset-3 col-xs-5">
+              <div className="offset-3 col-6">
                 <button
                   type="button"
                   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')}
         </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
                 id="isOidcEnabled"
+                className="custom-control-input"
                 type="checkbox"
                 checked={adminGeneralSecurityContainer.state.isOidcEnabled}
                 onChange={() => { adminGeneralSecurityContainer.switchIsOidcEnabled() }}
               />
-              <label htmlFor="isOidcEnabled">
+              <label className="custom-control-label" htmlFor="isOidcEnabled">
                 {t('security_setting.OAuth.enable_oidc')}
               </label>
             </div>
             {(!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 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
               className="form-control"
               type="text"
               value={adminOidcSecurityContainer.state.callbackUrl}
               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 && (
               <div className="alert alert-danger">
                 <i
                   className="icon-exclamation"
                   // 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>
             )}
@@ -111,9 +109,9 @@ class OidcSecurityManagement extends React.Component {
 
             <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
                   className="form-control"
                   type="text"
@@ -124,9 +122,9 @@ class OidcSecurityManagement extends React.Component {
               </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
                   className="form-control"
                   type="text"
@@ -134,15 +132,15 @@ class OidcSecurityManagement extends React.Component {
                   defaultValue={adminOidcSecurityContainer.state.oidcIssuerHost || ''}
                   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' }) }} />
                 </p>
               </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
                   className="form-control"
                   type="text"
@@ -150,15 +148,15 @@ class OidcSecurityManagement extends React.Component {
                   defaultValue={adminOidcSecurityContainer.state.oidcClientId || ''}
                   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' }) }} />
                 </p>
               </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
                   className="form-control"
                   type="text"
@@ -166,7 +164,7 @@ class OidcSecurityManagement extends React.Component {
                   defaultValue={adminOidcSecurityContainer.state.oidcClientSecret || ''}
                   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' }) }} />
                 </p>
               </div>
@@ -176,9 +174,9 @@ class OidcSecurityManagement extends React.Component {
               Attribute Mapping ({t('security_setting.optional')})
             </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
                   className="form-control"
                   type="text"
@@ -186,15 +184,15 @@ class OidcSecurityManagement extends React.Component {
                   defaultValue={adminOidcSecurityContainer.state.oidcAttrMapId || ''}
                   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') }} />
                 </p>
               </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
                   className="form-control"
                   type="text"
@@ -202,15 +200,15 @@ class OidcSecurityManagement extends React.Component {
                   defaultValue={adminOidcSecurityContainer.state.oidcAttrMapUserName || ''}
                   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') }} />
                 </p>
               </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
                   className="form-control"
                   type="text"
@@ -218,15 +216,15 @@ class OidcSecurityManagement extends React.Component {
                   defaultValue={adminOidcSecurityContainer.state.oidcAttrMapName || ''}
                   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') }} />
                 </p>
               </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
                   className="form-control"
                   type="text"
@@ -234,76 +232,80 @@ class OidcSecurityManagement extends React.Component {
                   defaultValue={adminOidcSecurityContainer.state.oidcAttrMapEmail || ''}
                   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') }) }} />
                 </p>
               </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
                   className="form-control"
                   type="text"
                   defaultValue={adminOidcSecurityContainer.state.callbackUrl || ''}
                   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 && (
                   <div className="alert alert-danger">
                     <i
                       className="icon-exclamation"
                       // 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 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
                     id="bindByUserName-oidc"
+                    className="custom-control-input"
                     type="checkbox"
                     checked={adminOidcSecurityContainer.state.isSameUsernameTreatedAsIdenticalUser}
                     onChange={() => { adminOidcSecurityContainer.switchIsSameUsernameTreatedAsIdenticalUser() }}
                   />
                   <label
+                    className="custom-control-label"
                     htmlFor="bindByUserName-oidc"
                     dangerouslySetInnerHTML={{ __html: t('security_setting.Treat username matching as identical') }}
                   />
                 </div>
-                <p className="help-block">
+                <p className="form-text text-muted">
                   <small dangerouslySetInnerHTML={{ __html: t('security_setting.Treat username matching as identical_warn') }} />
                 </p>
               </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
                     id="bindByEmail-oidc"
+                    className="custom-control-input"
                     type="checkbox"
                     checked={adminOidcSecurityContainer.state.isSameEmailTreatedAsIdenticalUser || false}
                     onChange={() => { adminOidcSecurityContainer.switchIsSameEmailTreatedAsIdenticalUser() }}
                   />
                   <label
+                    className="custom-control-label"
                     htmlFor="bindByEmail-oidc"
                     dangerouslySetInnerHTML={{ __html: t('security_setting.Treat email matching as identical') }}
                   />
                 </div>
-                <p className="help-block">
+                <p className="form-text text-muted">
                   <small dangerouslySetInnerHTML={{ __html: t('security_setting.Treat email matching as identical_warn') }} />
                 </p>
               </div>
             </div>
 
             <div className="row my-3">
-              <div className="col-xs-offset-3 col-xs-5">
+              <div className="offset-3 col-5">
                 <button
                   type="button"
                   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
                 id="isSamlEnabled"
+                className="custom-control-input"
                 type="checkbox"
                 checked={adminGeneralSecurityContainer.state.isSamlEnabled}
                 onChange={() => { adminGeneralSecurityContainer.switchIsSamlEnabled() }}
                 disabled={adminSamlSecurityContainer.state.useOnlyEnvVars}
               />
-              <label htmlFor="isSamlEnabled">
+              <label className="custom-control-label" htmlFor="isSamlEnabled">
                 {t('security_setting.SAML.enable_saml')}
               </label>
             </div>
             {(!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 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
               className="form-control"
               type="text"
               defaultValue={adminSamlSecurityContainer.state.callbackUrl}
               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 && (
               <div className="alert alert-danger">
                 <i
                   className="icon-exclamation"
                   // 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>
             )}
@@ -182,7 +180,7 @@ class SamlSecurityManagement extends React.Component {
                       value={this.state.envEntryPoint || ''}
                       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' }) }} />
                     </p>
                   </td>
@@ -206,7 +204,7 @@ class SamlSecurityManagement extends React.Component {
                       value={this.state.envIssuer || ''}
                       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' }) }} />
                     </p>
                   </td>
@@ -215,7 +213,7 @@ class SamlSecurityManagement extends React.Component {
                   <th>{t('security_setting.form_item_name.cert')}</th>
                   <td>
                     <textarea
-                      className="form-control input-sm"
+                      className="form-control form-control-sm"
                       type="text"
                       rows="5"
                       name="samlCert"
@@ -223,7 +221,7 @@ class SamlSecurityManagement extends React.Component {
                       defaultValue={adminSamlSecurityContainer.state.samlCert}
                       onChange={e => adminSamlSecurityContainer.changeSamlCert(e.target.value)}
                     />
-                    <p className="help-block">
+                    <p>
                       <small>
                         {t('security_setting.SAML.cert_detail')}
                       </small>
@@ -231,7 +229,7 @@ class SamlSecurityManagement extends React.Component {
                     <div>
                       <small>
                         e.g.
-                        <pre>{`-----BEGIN CERTIFICATE-----
+                        <pre className="well card">{`-----BEGIN CERTIFICATE-----
 MIICBzCCAXACCQD4US7+0A/b/zANBgkqhkiG9w0BAQsFADBIMQswCQYDVQQGEwJK
 UDEOMAwGA1UECAwFVG9reW8xFTATBgNVBAoMDFdFU0VFSywgSW5jLjESMBAGA1UE
 ...
@@ -245,13 +243,13 @@ pWVdnzS1VCO8fKsJ7YYIr+JmHvseph3kFUOI5RqkCcMZlKUv83aUThsTHw==
                   </td>
                   <td>
                     <textarea
-                      className="form-control input-sm"
+                      className="form-control form-control-sm"
                       type="text"
                       rows="5"
                       readOnly
                       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' }) }} />
                     </p>
                   </td>
@@ -283,7 +281,7 @@ pWVdnzS1VCO8fKsJ7YYIr+JmHvseph3kFUOI5RqkCcMZlKUv83aUThsTHw==
                       defaultValue={adminSamlSecurityContainer.state.samlAttrMapId}
                       onChange={e => adminSamlSecurityContainer.changeSamlAttrMapId(e.target.value)}
                     />
-                    <p className="help-block">
+                    <p className="form-text text-muted">
                       <small>
                         {t('security_setting.SAML.id_detail')}
                       </small>
@@ -296,7 +294,7 @@ pWVdnzS1VCO8fKsJ7YYIr+JmHvseph3kFUOI5RqkCcMZlKUv83aUThsTHw==
                       value={this.state.envAttrMapId || ''}
                       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' }) }} />
                     </p>
                   </td>
@@ -311,7 +309,7 @@ pWVdnzS1VCO8fKsJ7YYIr+JmHvseph3kFUOI5RqkCcMZlKUv83aUThsTHw==
                       defaultValue={adminSamlSecurityContainer.state.samlAttrMapUsername}
                       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') }} />
                     </p>
                   </td>
@@ -322,7 +320,7 @@ pWVdnzS1VCO8fKsJ7YYIr+JmHvseph3kFUOI5RqkCcMZlKUv83aUThsTHw==
                       value={this.state.envAttrMapUsername || ''}
                       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' }) }} />
                     </p>
                   </td>
@@ -337,7 +335,7 @@ pWVdnzS1VCO8fKsJ7YYIr+JmHvseph3kFUOI5RqkCcMZlKUv83aUThsTHw==
                       defaultValue={adminSamlSecurityContainer.state.samlAttrMapMail}
                       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' }) }} />
                     </p>
                   </td>
@@ -348,7 +346,7 @@ pWVdnzS1VCO8fKsJ7YYIr+JmHvseph3kFUOI5RqkCcMZlKUv83aUThsTHw==
                       value={this.state.envAttrMapMail || ''}
                       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' }) }} />
                     </p>
                   </td>
@@ -363,7 +361,7 @@ pWVdnzS1VCO8fKsJ7YYIr+JmHvseph3kFUOI5RqkCcMZlKUv83aUThsTHw==
                       defaultValue={adminSamlSecurityContainer.state.samlAttrMapFirstName}
                       onChange={e => adminSamlSecurityContainer.changeSamlAttrMapFirstName(e.target.value)}
                     />
-                    <p className="help-block">
+                    <p className="form-text text-muted">
                       {/* eslint-disable-next-line max-len */}
                       <small dangerouslySetInnerHTML={{ __html: t('security_setting.SAML.mapping_detail', { target: t('security_setting.form_item_name.attrMapFirstName') }) }} />
                     </p>
@@ -375,7 +373,7 @@ pWVdnzS1VCO8fKsJ7YYIr+JmHvseph3kFUOI5RqkCcMZlKUv83aUThsTHw==
                       value={this.state.envAttrMapFirstName || ''}
                       readOnly
                     />
-                    <p className="help-block">
+                    <p className="form-text text-muted">
                       <small>
                         <span dangerouslySetInnerHTML={{ __html: t('security_setting.SAML.Use env var if empty', { env: 'SAML_ATTR_MAPPING_FIRST_NAME' }) }} />
                         <br />
@@ -394,7 +392,7 @@ pWVdnzS1VCO8fKsJ7YYIr+JmHvseph3kFUOI5RqkCcMZlKUv83aUThsTHw==
                       defaultValue={adminSamlSecurityContainer.state.samlAttrMapLastName}
                       onChange={e => adminSamlSecurityContainer.changeSamlAttrMapLastName(e.target.value)}
                     />
-                    <p className="help-block">
+                    <p className="form-text text-muted">
                       {/* eslint-disable-next-line max-len */}
                       <small dangerouslySetInnerHTML={{ __html: t('security_setting.SAML.mapping_detail', { target: t('security_setting.form_item_name.attrMapLastName') }) }} />
                     </p>
@@ -406,7 +404,7 @@ pWVdnzS1VCO8fKsJ7YYIr+JmHvseph3kFUOI5RqkCcMZlKUv83aUThsTHw==
                       value={this.state.envAttrMapLastName || ''}
                       readOnly
                     />
-                    <p className="help-block">
+                    <p className="form-text text-muted">
                       <small>
                         <span dangerouslySetInnerHTML={{ __html: t('security_setting.SAML.Use env var if empty', { env: 'SAML_ATTR_MAPPING_LAST_NAME' }) }} />
                         <br />
@@ -422,41 +420,45 @@ pWVdnzS1VCO8fKsJ7YYIr+JmHvseph3kFUOI5RqkCcMZlKUv83aUThsTHw==
               Attribute Mapping Options
             </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
                     id="bindByUserName-SAML"
+                    className="custom-control-input"
                     type="checkbox"
                     checked={adminSamlSecurityContainer.state.isSameUsernameTreatedAsIdenticalUser || false}
                     onChange={() => { adminSamlSecurityContainer.switchIsSameUsernameTreatedAsIdenticalUser() }}
                   />
                   <label
+                    className="custom-control-label"
                     htmlFor="bindByUserName-SAML"
                     dangerouslySetInnerHTML={{ __html: t('security_setting.Treat username matching as identical') }}
                   />
                 </div>
-                <p className="help-block">
+                <p className="form-text text-muted">
                   <small dangerouslySetInnerHTML={{ __html: t('security_setting.Treat username matching as identical_warn') }} />
                 </p>
               </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
                     id="bindByEmail-SAML"
+                    className="custom-control-input"
                     type="checkbox"
                     checked={adminSamlSecurityContainer.state.isSameEmailTreatedAsIdenticalUser || false}
                     onChange={() => { adminSamlSecurityContainer.switchIsSameEmailTreatedAsIdenticalUser() }}
                   />
                   <label
+                    className="custom-control-label"
                     htmlFor="bindByEmail-SAML"
                     dangerouslySetInnerHTML={{ __html: t('security_setting.Treat email matching as identical') }}
                   />
                 </div>
-                <p className="help-block">
+                <p className="form-text text-muted">
                   <small dangerouslySetInnerHTML={{ __html: t('security_setting.Treat email matching as identical_warn') }} />
                 </p>
               </div>
@@ -466,7 +468,7 @@ pWVdnzS1VCO8fKsJ7YYIr+JmHvseph3kFUOI5RqkCcMZlKUv83aUThsTHw==
               Attribute-based Login Control
             </h3>
 
-            <p className="help-block">
+            <p className="form-text text-muted">
               <small dangerouslySetInnerHTML={{ __html: t('security_setting.SAML.attr_based_login_control_detail') }} />
             </p>
 
@@ -492,7 +494,7 @@ pWVdnzS1VCO8fKsJ7YYIr+JmHvseph3kFUOI5RqkCcMZlKUv83aUThsTHw==
                       onChange={(e) => { adminSamlSecurityContainer.changeSamlABLCRule(e.target.value) }}
                       readOnly={useOnlyEnvVars}
                     />
-                    <p className="help-block">
+                    <p className="form-text text-muted">
                       <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_example') }} />
@@ -506,7 +508,7 @@ pWVdnzS1VCO8fKsJ7YYIr+JmHvseph3kFUOI5RqkCcMZlKUv83aUThsTHw==
                       value={this.state.envABLCRule || ''}
                       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' }) }} />
                     </p>
                   </td>
@@ -515,7 +517,7 @@ pWVdnzS1VCO8fKsJ7YYIr+JmHvseph3kFUOI5RqkCcMZlKUv83aUThsTHw==
             </table>
 
             <div className="row my-3">
-              <div className="col-xs-offset-3 col-xs-5">
+              <div className="offset-3 col-5">
                 <button
                   type="button"
                   className="btn btn-primary"

+ 130 - 62
src/client/js/components/Admin/Security/SecurityManagement.jsx

@@ -1,6 +1,9 @@
 import React, { Fragment } from 'react';
 import PropTypes from 'prop-types';
 import { withTranslation } from 'react-i18next';
+import {
+  TabContent, TabPane, Nav, NavItem, NavLink,
+} from 'reactstrap';
 
 import { createSubscribedElement } from '../../UnstatedUtils';
 
@@ -18,13 +21,27 @@ import FacebookSecuritySetting from './FacebookSecuritySetting';
 
 class SecurityManagement extends React.Component {
 
-  constructor(props) {
+  constructor() {
     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() {
     const { t } = this.props;
+    const { activeTab, activeComponents } = this.state;
     return (
       <Fragment>
         <div>
@@ -41,69 +58,120 @@ class SecurityManagement extends React.Component {
           </div>
         </div>
 
-        {/* TODO GW-226 adapt BS4 */}
         <div className="auth-mechanism-configurations m-t-10">
           <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>
       </Fragment>
     );

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

@@ -49,166 +49,160 @@ class SecuritySetting extends React.Component {
 
     return (
       <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>
-          {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" onClick={() => { adminGeneralSecurityContainer.changeRestrictGuestMode('Deny') }}>
+                  {t('security_setting.guest_mode.deny')}
+                </a>
+                <a className="dropdown-item" onClick={() => { adminGeneralSecurityContainer.changeRestrictGuestMode('Readonly') }}>
+                  {t('security_setting.guest_mode.readonly')}
+                </a>
               </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 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 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" onClick={() => { adminGeneralSecurityContainer.changePageCompleteDeletionAuthority('anyOne') }}>
+                  {t('security_setting.anyone')}
+                </a>
+                <a className="dropdown-item" onClick={() => { adminGeneralSecurityContainer.changePageCompleteDeletionAuthority('adminOnly') }}>
+                  {t('security_setting.admin_only')}
+                </a>
+                <a className="dropdown-item" onClick={() => { adminGeneralSecurityContainer.changePageCompleteDeletionAuthority('adminAndAuthor') }}>
+                  {t('security_setting.admin_and_author')}
+                </a>
               </div>
+              <p className="form-text text-muted small">
+                {t('security_setting.complete_deletion_explain')}
+              </p>
             </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>
-        </fieldset>
+        </div>
       </React.Fragment>
     );
   }

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

@@ -68,43 +68,41 @@ class TwitterSecurityManagement extends React.Component {
           </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
                 id="isTwitterEnabled"
+                className="custom-control-input"
                 type="checkbox"
                 checked={adminGeneralSecurityContainer.state.isTwitterEnabled}
                 onChange={() => { adminGeneralSecurityContainer.switchIsTwitterOAuthEnabled() }}
               />
-              <label htmlFor="isTwitterEnabled">
+              <label className="custom-control-label" htmlFor="isTwitterEnabled">
                 {t('security_setting.OAuth.Twitter.enable_twitter')}
               </label>
             </div>
             {(!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 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
               className="form-control"
               type="text"
               value={adminTwitterSecurityContainer.state.callbackUrl}
               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 && (
               <div className="alert alert-danger">
                 <i
                   className="icon-exclamation"
                   // 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>
             )}
@@ -118,8 +116,8 @@ class TwitterSecurityManagement extends React.Component {
             <h3 className="border-bottom">{t('security_setting.configuration')}</h3>
 
             <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
                   className="form-control"
                   type="text"
@@ -127,15 +125,15 @@ class TwitterSecurityManagement extends React.Component {
                   defaultValue={adminTwitterSecurityContainer.state.twitterConsumerKey || ''}
                   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' }) }} />
                 </p>
               </div>
             </div>
 
             <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
                   className="form-control"
                   type="text"
@@ -143,34 +141,36 @@ class TwitterSecurityManagement extends React.Component {
                   defaultValue={adminTwitterSecurityContainer.state.twitterConsumerSecret || ''}
                   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' }) }} />
                 </p>
               </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="offset-md-3 col-md-6">
+                <div className="custom-control custom-checkbox custom-checkbox-success">
                   <input
                     id="bindByUserNameTwitter"
+                    className="custom-control-input"
                     type="checkbox"
                     checked={adminTwitterSecurityContainer.state.isSameUsernameTreatedAsIdenticalUser || false}
                     onChange={() => { adminTwitterSecurityContainer.switchIsSameUsernameTreatedAsIdenticalUser() }}
                   />
                   <label
+                    className="custom-control-label"
                     htmlFor="bindByUserNameTwitter"
                     dangerouslySetInnerHTML={{ __html: t('security_setting.Treat email matching as identical') }}
                   />
                 </div>
-                <p className="help-block">
+                <p className="form-text text-muted">
                   <small dangerouslySetInnerHTML={{ __html: t('security_setting.Treat email matching as identical_warn') }} />
                 </p>
               </div>
             </div>
 
             <div className="row my-3">
-              <div className="col-xs-offset-3 col-xs-5">
+              <div className="offset-4 col-5">
                 <button
                   type="button"
                   className="btn btn-primary"

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

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

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

@@ -1,8 +1,9 @@
 import React from 'react';
 import PropTypes from 'prop-types';
 import { withTranslation } from 'react-i18next';
-
-import Modal from 'react-bootstrap/es/Modal';
+import {
+  Modal, ModalHeader, ModalBody, ModalFooter,
+} from 'reactstrap';
 
 import { createSubscribedElement } from '../../UnstatedUtils';
 import AppContainer from '../../../services/AppContainer';
@@ -162,31 +163,29 @@ class UserGroupDeleteModal extends React.Component {
     const { t } = this.props;
 
     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>
             <span className="font-weight-bold">{t('admin:user_group_management.group_name')}</span> : &quot;{this.props.deleteUserGroup.name}&quot;
           </div>
           <div className="text-danger mt-5">
             {t('admin:user_group_management.delete_modal.desc')}
           </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.renderGroupSelector()}
             </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')}
             </button>
           </form>
-        </Modal.Footer>
+        </ModalFooter>
       </Modal>
     );
   }

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

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

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

@@ -8,15 +8,15 @@ class CheckBoxForSerchUserOption extends React.Component {
   render() {
     const { t, option } = this.props;
     return (
-      <div className="checkbox checkbox-info" key={`isAlso${option}Searched`}>
+      <div className="custom-control custom-checkbox custom-checkbox-info" key={`isAlso${option}Searched`}>
         <input
           type="checkbox"
           id={`isAlso${option}Searched`}
-          className="form-check-input"
+          className="custom-control-input"
           checked={this.props.checked}
           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 })}
         </label>
       </div>

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

@@ -8,15 +8,15 @@ class RadioButtonForSerchUserOption extends React.Component {
   render() {
     const { t, searchType } = this.props;
     return (
-      <div className="radio" key={`${searchType}Match`}>
+      <div className="custom-control custom-radio custom-control-inline" key={`${searchType}Match`}>
         <input
           type="radio"
           id={`${searchType}Match`}
-          className="form-check-radio"
+          className="custom-control-input"
           checked={this.props.checked}
           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`)}
         </label>
       </div>

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

@@ -16,17 +16,17 @@ class UserGroupDetailPage extends React.Component {
 
     return (
       <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>
           {t('admin:user_group_management.back_to_list')}
         </a>
-        <div className="m-t-20 form-box">
+        <div className="mt-4 form-box">
           <UserGroupEditForm />
         </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 />
         <UserGroupUserModal />
-        <legend className="m-t-20">{t('Page')}</legend>
+        <h2 className="admin-setting-header mt-4">{t('Page')}</h2>
         <div className="page-list">
           <UserGroupPageList />
         </div>

+ 15 - 11
src/client/js/components/Admin/UserGroupDetail/UserGroupEditForm.jsx

@@ -61,18 +61,20 @@ class UserGroupEditForm extends React.Component {
     const { t, adminUserGroupDetailContainer } = this.props;
 
     return (
-      <form className="form-horizontal" onSubmit={this.handleSubmit}>
+      <form onSubmit={this.handleSubmit}>
         <fieldset>
-          <legend>{t('admin:user_group_management.basic_info')}</legend>
-          <div className="form-group">
-            <label htmlFor="name" className="col-sm-2 control-label">{t('Name')}</label>
-            <div className="col-sm-4">
+          <h2 className="admin-setting-header">{t('admin:user_group_management.basic_info')}</h2>
+          <div className="form-group row">
+            <label htmlFor="name" className="col-md-2 col-form-label">
+              {t('Name')}
+            </label>
+            <div className="col-md-4">
               <input className="form-control" type="text" name="name" value={this.state.name} onChange={this.changeUserGroupName} />
             </div>
           </div>
-          <div className="form-group">
-            <label className="col-sm-2 control-label">{t('Created')}</label>
-            <div className="col-sm-4">
+          <div className="form-group row">
+            <label className="col-md-2 col-form-label">{t('Created')}</label>
+            <div className="col-md-4">
               <input
                 type="text"
                 className="form-control"
@@ -81,9 +83,11 @@ class UserGroupEditForm extends React.Component {
               />
             </div>
           </div>
-          <div className="form-group">
-            <div className="col-sm-offset-2 col-sm-10">
-              <button type="submit" className="btn btn-primary" disabled={!this.validateForm()}>{t('Update')}</button>
+          <div className="form-group row">
+            <div className="offset-md-2 col-md-10">
+              <button type="submit" className="btn btn-primary" disabled={!this.validateForm()}>
+                {t('Update')}
+              </button>
             </div>
           </div>
         </fieldset>

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

@@ -55,8 +55,8 @@ class UserGroupPageList extends React.Component {
 
     return (
       <Fragment>
-        <ul className="page-list-ul page-list-ul-flat">
-          {this.state.currentPages.map((page) => { return <Page key={page._id} page={page} /> })}
+        <ul className="page-list-ul page-list-ul-flat mb-3">
+          {this.state.currentPages.map(page => <li key={page._id}><Page page={page} /></li>)}
         </ul>
         {adminUserGroupDetailContainer.state.relatedPages.length === 0 ? <p>{t('admin:user_group_management.no_pages')}</p> : null}
         <PaginationWrapper

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