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

Merge branch 'support/apply-bootstrap4' into support/apply-bootstrap4-installer-form

yusuketk 6 лет назад
Родитель
Сommit
b1a2589cf0
100 измененных файлов с 1694 добавлено и 1181 удалено
  1. 1 0
      .eslintrc.js
  2. 6 0
      CHANGES.md
  3. 1 0
      config/logger/config.dev.js
  4. 0 1
      config/webpack.common.js
  5. 2 1
      package.json
  6. 10 9
      resource/cdn-manifests.js
  7. 1 1
      resource/locales/en-US/translation.json
  8. 1 1
      resource/locales/ja/translation.json
  9. 3 0
      src/client/js/app.jsx
  10. 1 1
      src/client/js/bootstrap.jsx
  11. 1 1
      src/client/js/components/Admin/App/PluginSetting.jsx
  12. 1 1
      src/client/js/components/Admin/Customize/CustomizeFunctionOption.jsx
  13. 0 1
      src/client/js/components/Admin/Customize/CustomizeFunctionSetting.jsx
  14. 1 1
      src/client/js/components/Admin/Customize/CustomizeHighlightSetting.jsx
  15. 0 2
      src/client/js/components/Admin/Customize/CustomizeThemeOptions.jsx
  16. 4 4
      src/client/js/components/Admin/ExportArchiveData/SelectCollectionsModal.jsx
  17. 1 1
      src/client/js/components/Admin/ImportData/GrowiArchive/ImportForm.jsx
  18. 2 2
      src/client/js/components/Admin/ImportDataPage.jsx
  19. 2 2
      src/client/js/components/Admin/MarkdownSetting/LineBreakForm.jsx
  20. 1 1
      src/client/js/components/Admin/MarkdownSetting/XssForm.jsx
  21. 1 1
      src/client/js/components/Admin/Notification/GlobalNotificationList.jsx
  22. 6 0
      src/client/js/components/Admin/Notification/ManageGlobalNotification.jsx
  23. 2 2
      src/client/js/components/Admin/Notification/SlackAppConfiguration.jsx
  24. 4 2
      src/client/js/components/Admin/Notification/TriggerEventCheckBox.jsx
  25. 10 10
      src/client/js/components/Admin/Security/BasicSecuritySetting.jsx
  26. 16 16
      src/client/js/components/Admin/Security/GitHubSecuritySetting.jsx
  27. 16 16
      src/client/js/components/Admin/Security/GoogleSecuritySetting.jsx
  28. 6 6
      src/client/js/components/Admin/Security/LdapAuthTestModal.jsx
  29. 76 53
      src/client/js/components/Admin/Security/LdapSecuritySetting.jsx
  30. 57 70
      src/client/js/components/Admin/Security/LocalSecuritySetting.jsx
  31. 34 32
      src/client/js/components/Admin/Security/OidcSecuritySetting.jsx
  32. 18 16
      src/client/js/components/Admin/Security/SamlSecuritySetting.jsx
  33. 121 62
      src/client/js/components/Admin/Security/SecurityManagement.jsx
  34. 114 143
      src/client/js/components/Admin/Security/SecuritySetting.jsx
  35. 17 17
      src/client/js/components/Admin/Security/TwitterSecuritySetting.jsx
  36. 2 2
      src/client/js/components/Admin/UserGroupDetail/UserGroupDetailPage.jsx
  37. 1 1
      src/client/js/components/Admin/UserGroupDetail/UserGroupEditForm.jsx
  38. 1 1
      src/client/js/components/Admin/Users/ExternalAccountTable.jsx
  39. 3 3
      src/client/js/components/Admin/Users/UserInviteModal.jsx
  40. 84 10
      src/client/js/components/Navbar/PersonalDropdown.jsx
  41. 1 1
      src/client/js/components/Page/CopyDropdown.jsx
  42. 1 1
      src/client/js/components/Page/RevisionPath.jsx
  43. 2 2
      src/client/js/components/Page/TagLabels.jsx
  44. 2 2
      src/client/js/components/PageComment/Comment.jsx
  45. 4 1
      src/client/js/components/PageEditor/MarkdownDrawioUtil.js
  46. 14 8
      src/client/js/components/PageEditorByHackmd.jsx
  47. 1 1
      src/client/js/components/PageHistory/PageRevisionList.jsx
  48. 1 1
      src/client/js/components/SearchPage.jsx
  49. 0 5
      src/client/js/components/SearchPage/SearchResult.jsx
  50. 41 16
      src/client/js/components/TableOfContents.jsx
  51. 0 23
      src/client/js/legacy/crowi.js
  52. 68 0
      src/client/js/services/AppContainer.js
  53. 41 38
      src/client/styles/agile-admin/inverse/colors/_apply-colors-dark.scss
  54. 0 29
      src/client/styles/agile-admin/inverse/colors/default-dark.scss
  55. 0 21
      src/client/styles/agile-admin/inverse/colors/default.scss
  56. 16 18
      src/client/styles/scss/_comment.scss
  57. 10 69
      src/client/styles/scss/_layout.scss
  58. 4 5
      src/client/styles/scss/_layout_growi.scss
  59. 5 12
      src/client/styles/scss/_layout_kibela.scss
  60. 0 22
      src/client/styles/scss/_override-bootstrap-variables.scss
  61. 0 3
      src/client/styles/scss/_page.scss
  62. 0 43
      src/client/styles/scss/_page_growi.scss
  63. 10 0
      src/client/styles/scss/_page_header.scss
  64. 1 3
      src/client/styles/scss/_search.scss
  65. 2 3
      src/client/styles/scss/_user_growi.scss
  66. 29 0
      src/client/styles/scss/atoms/_buttons.scss
  67. 1 0
      src/client/styles/scss/style-app.scss
  68. 18 17
      src/client/styles/scss/theme/_apply-colors-dark.scss
  69. 2 2
      src/client/styles/scss/theme/_apply-colors-light.scss
  70. 15 82
      src/client/styles/scss/theme/_apply-colors.scss
  71. 481 0
      src/client/styles/scss/theme/_reboot-bootstrap-colors.scss
  72. 58 0
      src/client/styles/scss/theme/_reboot-bootstrap-theme-colors.scss
  73. 0 8
      src/client/styles/scss/theme/default-dark.scss
  74. 58 45
      src/client/styles/scss/theme/default.scss
  75. 2 2
      src/server/routes/apiv3/customize-setting.js
  76. 48 48
      src/server/routes/apiv3/security-setting.js
  77. 33 24
      src/server/routes/login-passport.js
  78. 5 0
      src/server/service/passport.js
  79. 3 5
      src/server/views/admin/Users_reserve.html
  80. 3 5
      src/server/views/admin/app.html
  81. 3 5
      src/server/views/admin/customize.html
  82. 3 5
      src/server/views/admin/export.html
  83. 3 5
      src/server/views/admin/external-accounts.html
  84. 3 5
      src/server/views/admin/global-notification-detail.html
  85. 3 5
      src/server/views/admin/importer.html
  86. 3 5
      src/server/views/admin/index.html
  87. 3 5
      src/server/views/admin/markdown.html
  88. 3 5
      src/server/views/admin/notification.html
  89. 3 5
      src/server/views/admin/search.html
  90. 3 5
      src/server/views/admin/security.html
  91. 3 5
      src/server/views/admin/user-group-detail.html
  92. 3 5
      src/server/views/admin/user-groups.html
  93. 3 5
      src/server/views/admin/users.html
  94. 1 0
      src/server/views/layout-crowi/base/layout.html
  95. 8 11
      src/server/views/layout-crowi/forbidden.html
  96. 8 11
      src/server/views/layout-crowi/not_creatable.html
  97. 8 11
      src/server/views/layout-crowi/not_found.html
  98. 10 12
      src/server/views/layout-crowi/page.html
  99. 11 13
      src/server/views/layout-crowi/page_list.html
  100. 1 0
      src/server/views/layout-growi/base/layout.html

+ 1 - 0
.eslintrc.js

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

+ 6 - 0
CHANGES.md

@@ -1,10 +1,16 @@
 # CHANGES
 
+## v4.0.0-RC
+
+* Support: Upgrade libs
+    * bootstrap
+
 ## v3.7.0-RC
 
 * Feature: [Draw.io](https://www.draw.io/) Integration
 * Feature: SAML Attribute-based Login Control
 * Improvement: Reactify admin pages (Security)
+* Improvement: Behavior of pre-editing screen of HackMD when user needs to resume
 
 ## v3.6.10
 

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

+ 0 - 1
config/webpack.common.js

@@ -34,7 +34,6 @@ 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',

+ 2 - 1
package.json

@@ -162,7 +162,7 @@
     "babel-loader": "^8.0.6",
     "babel-plugin-lodash": "^3.3.4",
     "babel-plugin-transform-imports": "^2.0.0",
-    "bootstrap": "^4.3.1",
+    "bootstrap": "^4.4.1",
     "browser-bunyan": "^1.3.0",
     "browser-sync": "^2.26.3",
     "bunyan-debug": "^2.0.0",
@@ -231,6 +231,7 @@
     "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",
     "stylelint": "^13.2.0",
     "stylelint-config-recess-order": "^2.0.1",

+ 10 - 9
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/popper.js@1.15.0,npm/bootstrap@4.3.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.4.1/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: '',
@@ -44,6 +45,14 @@ module.exports = {
         integrity: '',
       },
     },
+    {
+      name: 'drawio-viewer',
+      url: 'https://jgraph.github.io/drawio/src/main/webapp/js/viewer.min.js',
+      args: {
+        async: true,
+        integrity: '',
+      },
+    },
     {
       name: 'codemirror-dialog',
       url: 'https://cdn.jsdelivr.net/npm/codemirror@5.48.4/addon/dialog/dialog.min.js',
@@ -79,14 +88,6 @@ module.exports = {
         integrity: '',
       },
     },
-    {
-      name: 'drawio-viewer',
-      url: 'https://jgraph.github.io/drawio/src/main/webapp/js/viewer.min.js',
-      groups: ['basis'],
-      args: {
-        integrity: '',
-      },
-    },
   ],
   style: [
     {

+ 1 - 1
resource/locales/en-US/translation.json

@@ -61,7 +61,7 @@
   "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",

+ 1 - 1
resource/locales/ja/translation.json

@@ -61,7 +61,7 @@
   "No diff": "差分なし",
   "Shrink versions that have no diffs": "差分のないバージョンをコンパクトに表示する",
   "User ID": "ユーザーID",
-  "Home": "ホーム",
+  "User's Home": "ユーザーホーム",
   "User Settings": "ユーザー設定",
   "User Information": "ユーザー情報",
   "Basic Info": "ユーザーの基本情報",

+ 3 - 0
src/client/js/app.jsx

@@ -125,3 +125,6 @@ $('a[data-toggle="tab"][href="#revision-history"]').on('show.bs.tab', () => {
     </I18nextProvider>, document.getElementById('revision-history'),
   );
 });
+
+// initialize scrollpos-styler
+ScrollPosStyler.init();

+ 1 - 1
src/client/js/bootstrap.jsx

@@ -27,7 +27,7 @@ const websocketContainer = new WebsocketContainer(appContainer);
 
 logger.info('unstated containers have been initialized');
 
-appContainer.initPlugins();
+appContainer.init();
 appContainer.injectToWindow();
 
 /**

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

@@ -43,7 +43,7 @@ class PluginSetting extends React.Component {
 
         <div className="row form-group mb-5">
           <div className="offset-3 col-6 text-left">
-            <div className="custom-control custom-switch checkbox-success">
+            <div className="custom-control custom-switch custom-checkbox-success">
               <input
                 id="isEnabledPlugins"
                 className="custom-control-input"

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

@@ -7,7 +7,7 @@ class CustomizeFunctionOption extends React.PureComponent {
   render() {
     return (
       <React.Fragment>
-        <div className="custom-control custom-switch checkbox-success">
+        <div className="custom-control custom-switch custom-checkbox-success">
           <input
             className="custom-control-input"
             type="checkbox"

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

@@ -116,7 +116,6 @@ class CustomizeBehaviorSetting extends React.Component {
                   <DropdownToggle className="text-right col-6" caret>
                     <span className="float-left">{adminCustomizeContainer.state.currentRecentCreatedLimit}</span>
                   </DropdownToggle>
-                  {/* TODO adjust dropdown after BS4 */}
                   <DropdownMenu className="dropdown-menu" role="menu">
                     <DropdownItem key={10} role="presentation" onClick={() => { adminCustomizeContainer.switchRecentCreatedLimit(10) }}>
                       <a role="menuitem">10</a>

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

@@ -114,7 +114,7 @@ class CustomizeHighlightSetting extends React.Component {
 
             <div className="form-group row">
               <div className="offset-3 col-6 text-left">
-                <div className="custom-control custom-switch checkbox-success">
+                <div className="custom-control custom-switch custom-checkbox-success">
                   <input
                     type="checkbox"
                     className="custom-control-input"

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

@@ -32,8 +32,6 @@ class CustomizeThemeOptions extends React.Component {
     }];
 
     const darkTheme = [{
-      name: 'default-dark', bg: '#212731', topbar: '#151515', theme: '#f75b36',
-    }, {
       name: 'future', bg: '#16282D', topbar: '#011414', theme: '#04B4AE',
     }, {
       name: 'blue-night', bg: '#061F2F', topbar: '#27343B', theme: '#0090C8',

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

@@ -192,26 +192,26 @@ class SelectCollectionsModal extends React.Component {
             </div>
             <div className="row mt-4">
               <div className="col-sm-12">
-                <legend>Page Collections</legend>
+                <h3 className="admin-setting-header">Page Collections</h3>
                 {this.renderGroups(GROUPS_PAGE)}
               </div>
             </div>
             <div className="row mt-4">
               <div className="col-sm-12">
-                <legend>User Collections</legend>
+                <h3 className="admin-setting-header">User Collections</h3>
                 {this.renderGroups(GROUPS_USER, 'danger')}
                 {this.renderWarnForUser()}
               </div>
             </div>
             <div className="row mt-4">
               <div className="col-sm-12">
-                <legend>Config Collections</legend>
+                <h3 className="admin-setting-header">Config Collections</h3>
                 {this.renderGroups(GROUPS_CONFIG)}
               </div>
             </div>
             <div className="row mt-4">
               <div className="col-sm-12">
-                <legend>Other Collections</legend>
+                <h3 className="admin-setting-header">Other Collections</h3>
                 {this.renderOthers()}
               </div>
             </div>

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

@@ -345,7 +345,7 @@ class ImportForm extends React.Component {
     return (
       <div className="mt-4 row">
         <div className="col-12">
-          <legend>{groupName} Collections</legend>
+          <h3 className="admin-setting-header">{groupName} Collections</h3>
           { wellContent != null && (
             <div className="card well small" role="alert">
               <ul>

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

@@ -143,7 +143,7 @@ class ImportDataPage extends React.Component {
           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>
@@ -233,7 +233,7 @@ class ImportDataPage extends React.Component {
           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>

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

@@ -46,7 +46,7 @@ class LineBreakForm extends React.Component {
     return (
       <div className="form-group text-left my-3">
         <div className="col-8 offset-4">
-          <div className="custom-control custom-switch checkbox-success">
+          <div className="custom-control custom-switch custom-checkbox-success">
             <input
               type="checkbox"
               className="custom-control-input"
@@ -73,7 +73,7 @@ class LineBreakForm extends React.Component {
     return (
       <div className="form-group text-left my-3">
         <div className="col-8 offset-4">
-          <div className="custom-control custom-switch checkbox-success">
+          <div className="custom-control custom-switch custom-checkbox-success">
             <input
               type="checkbox"
               className="custom-control-input"

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

@@ -133,7 +133,7 @@ class XssForm extends React.Component {
         <fieldset className="col-12">
           <div className="form-group">
             <div className="col-8 offset-4 my-3">
-              <div className="custom-control custom-switch checkbox-success">
+              <div className="custom-control custom-switch custom-checkbox-success">
                 <input
                   type="checkbox"
                   className="custom-control-input"

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

@@ -76,7 +76,7 @@ class GlobalNotificationList extends React.Component {
           return (
             <tr key={notification._id}>
               <td className="align-middle td-abs-center">
-                <div className="custom-control custom-switch checkbox-success">
+                <div className="custom-control custom-switch custom-checkbox-success">
                   <input
                     type="checkbox"
                     className="custom-control-input"

+ 6 - 0
src/client/js/components/Admin/Notification/ManageGlobalNotification.jsx

@@ -200,6 +200,7 @@ class ManageGlobalNotification extends React.Component {
             <div className="form-group">
               <h3>{t('notification_setting.trigger_events')}</h3>
               <TriggerEventCheckBox
+                checkbox="success"
                 event="pageCreate"
                 checked={this.state.triggerEvents.has('pageCreate')}
                 onChange={() => this.onChangeTriggerEvents('pageCreate')}
@@ -209,6 +210,7 @@ class ManageGlobalNotification extends React.Component {
                 </span>
               </TriggerEventCheckBox>
               <TriggerEventCheckBox
+                checkbox="warning"
                 event="pageEdit"
                 checked={this.state.triggerEvents.has('pageEdit')}
                 onChange={() => this.onChangeTriggerEvents('pageEdit')}
@@ -218,6 +220,7 @@ class ManageGlobalNotification extends React.Component {
                 </span>
               </TriggerEventCheckBox>
               <TriggerEventCheckBox
+                checkbox="warning"
                 event="pageMove"
                 checked={this.state.triggerEvents.has('pageMove')}
                 onChange={() => this.onChangeTriggerEvents('pageMove')}
@@ -227,6 +230,7 @@ class ManageGlobalNotification extends React.Component {
                 </span>
               </TriggerEventCheckBox>
               <TriggerEventCheckBox
+                checkbox="danger"
                 event="pageDelete"
                 checked={this.state.triggerEvents.has('pageDelete')}
                 onChange={() => this.onChangeTriggerEvents('pageDelete')}
@@ -236,6 +240,7 @@ class ManageGlobalNotification extends React.Component {
                 </span>
               </TriggerEventCheckBox>
               <TriggerEventCheckBox
+                checkbox="info"
                 event="pageLike"
                 checked={this.state.triggerEvents.has('pageLike')}
                 onChange={() => this.onChangeTriggerEvents('pageLike')}
@@ -245,6 +250,7 @@ class ManageGlobalNotification extends React.Component {
                 </span>
               </TriggerEventCheckBox>
               <TriggerEventCheckBox
+                checkbox="secondary"
                 event="comment"
                 checked={this.state.triggerEvents.has('comment')}
                 onChange={() => this.onChangeTriggerEvents('comment')}

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

@@ -50,7 +50,7 @@ class SlackAppConfiguration extends React.Component {
                 aria-haspopup="true"
                 aria-expanded="true"
               >
-                {`Slack ${adminNotificationContainer.state.selectSlackOption}`} <span className="caret"></span>
+                {`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>
@@ -77,7 +77,7 @@ class SlackAppConfiguration extends React.Component {
 
             <div className="row mb-3">
               <div className="offset-3 col-6 text-left">
-                <div className="custom-control custom-switch checkbox-success">
+                <div className="custom-control custom-switch custom-checkbox-success">
                   <input
                     type="checkbox"
                     className="custom-control-input"

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

@@ -6,14 +6,15 @@ const TriggerEventCheckBox = (props) => {
   const { t } = props;
 
   return (
-    <div className="checkbox">
+    <div className={`custom-control custom-checkbox custom-checkbox-${props.checkbox}`}>
       <input
+        className="custom-control-input"
         type="checkbox"
         id={`trigger-event-${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>
@@ -25,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,

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

@@ -68,18 +68,16 @@ class BasicSecurityManagement extends React.Component {
         )}
 
         <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="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>
@@ -90,22 +88,24 @@ class BasicSecurityManagement extends React.Component {
               </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-3 col-6">
+              <div className="custom-control custom-switch 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') }}
                 />
@@ -117,7 +117,7 @@ class BasicSecurityManagement extends React.Component {
           </div>
 
           <div className="row my-3">
-            <div className="col-xs-offset-4 col-xs-5">
+            <div className="offset-4 col-5">
               <button type="button" className="btn btn-primary" disabled={adminBasicSecurityContainer.state.retrieveError != null} onClick={this.onClickSubmit}>
                 {t('Update')}
               </button>

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

@@ -69,29 +69,27 @@ class GitHubSecurityManagement extends React.Component {
         )}
 
         <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="offset-3 col-6 text-left">
+            <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="badg 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-3 text-right py-2">{t('security_setting.callback_URL')}</label>
+          <div className="col-6">
             <input
               className="form-control"
               type="text"
@@ -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"
@@ -134,8 +132,8 @@ class GitHubSecurityManagement extends React.Component {
             </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"
@@ -150,15 +148,17 @@ class GitHubSecurityManagement extends React.Component {
             </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-switch 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') }}
                   />
@@ -170,7 +170,7 @@ class GitHubSecurityManagement extends React.Component {
             </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>

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

@@ -69,29 +69,27 @@ class GoogleSecurityManagement extends React.Component {
         )}
 
         <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="offset-3 col-6">
+            <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-3 text-right py-2">{t('security_setting.callback_URL')}</label>
+          <div className="col-6">
             <input
               className="form-control"
               type="text"
@@ -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"
@@ -134,8 +132,8 @@ class GoogleSecurityManagement extends React.Component {
             </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"
@@ -150,15 +148,17 @@ class GoogleSecurityManagement extends React.Component {
             </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-switch 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') }}
                   />
@@ -170,7 +170,7 @@ class GoogleSecurityManagement extends React.Component {
             </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"

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

@@ -118,8 +118,8 @@ class LdapAuthTestModal extends React.Component {
           {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">
+            <label htmlFor="username" className="col-3 text-right">{t('username')}</label>
+            <div className="col-6">
               <input
                 className="form-control"
                 name="username"
@@ -129,8 +129,8 @@ class LdapAuthTestModal 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">
+            <label htmlFor="password" className="col-3 text-right">{t('Password')}</label>
+            <div className="col-6">
               <input
                 className="form-control"
                 type="password"
@@ -142,11 +142,11 @@ class LdapAuthTestModal extends React.Component {
           </div>
           <div>
             <h5>Logs</h5>
-            <textarea id="taLogs" className="col-xs-12" rows="4" value={this.state.logs} readOnly />
+            <textarea id="taLogs" className="col-12" rows="4" value={this.state.logs} readOnly />
           </div>
         </ModalBody>
         <ModalFooter>
-          <button type="button" className="btn btn-default mt-3 col-xs-offset-5 col-xs-2" onClick={this.testLdapCredentials}>Test</button>
+          <button type="button" className="btn btn-light mt-3 offset-5" onClick={this.testLdapCredentials}>Test</button>
         </ModalFooter>
       </Modal>
     );

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

@@ -74,23 +74,21 @@ class LdapSecuritySetting extends React.Component {
         </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="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>
 
@@ -101,8 +99,10 @@ 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">
+              <label htmlFor="serverUrl" className="col-3 control-label text-right py-2">
+                Server URL
+              </label>
+              <div className="col-6">
                 <input
                   className="form-control"
                   type="text"
@@ -122,35 +122,40 @@ class LdapSecuritySetting extends React.Component {
             </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="col-3 text-right py-2">
+                <strong>{t('security_setting.ldap.bind_mode')}</strong>
+              </div>
+              <div className="col-6">
+                <div className="dropdown">
+                  <button
+                    className="btn btn-light 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="col-3 text-right py-2">
+                <strong>Bind DN</strong>
+              </div>
+              <div className="col-6">
                 <input
                   className="form-control"
                   type="text"
@@ -182,10 +187,12 @@ class LdapSecuritySetting extends React.Component {
             </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 htmlFor="bindDNPassword" className="col-3 text-right py-2">
+                <strong>{t('security_setting.ldap.bind_DN_password')}</strong>
+              </div>
+              <div className="col-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>
@@ -211,8 +218,10 @@ class LdapSecuritySetting extends React.Component {
             </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="col-3 text-right py-2">
+                <strong>{t('security_setting.ldap.search_filter')}</strong>
+              </div>
+              <div className="col-6">
                 <input
                   className="form-control"
                   type="text"
@@ -245,8 +254,10 @@ class LdapSecuritySetting extends React.Component {
             </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="col-3 text-right py-2">
+                <strong htmlFor="attrMapUsername">{t('username')}</strong>
+              </div>
+              <div className="col-6">
                 <input
                   className="form-control"
                   type="text"
@@ -263,15 +274,17 @@ class LdapSecuritySetting extends React.Component {
             </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-switch 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') }}
@@ -285,8 +298,10 @@ class LdapSecuritySetting extends React.Component {
             </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="col-3 text-right py-2">
+                <strong htmlFor="attrMapMail">{t('Email')}</strong>
+              </div>
+              <div className="col-6">
                 <input
                   className="form-control"
                   type="text"
@@ -304,8 +319,10 @@ class LdapSecuritySetting extends React.Component {
             </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="col-3 text-right py-2">
+                <strong htmlFor="attrMapName">{t('Name')}</strong>
+              </div>
+              <div className="col-6">
                 <input
                   className="form-control"
                   type="text"
@@ -327,8 +344,10 @@ class LdapSecuritySetting extends React.Component {
             </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="col-3 text-right py-2">
+                <strong htmlFor="groupSearchBase">{t('security_setting.ldap.group_search_base_DN')}</strong>
+              </div>
+              <div className="col-6">
                 <input
                   className="form-control"
                   type="text"
@@ -347,8 +366,10 @@ class LdapSecuritySetting extends React.Component {
             </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="col-3 text-right py-2">
+                <strong htmlFor="groupSearchFilter">{t('security_setting.ldap.group_search_filter')}</strong>
+              </div>
+              <div className="col-6">
                 <input
                   className="form-control"
                   type="text"
@@ -376,8 +397,10 @@ class LdapSecuritySetting extends React.Component {
             </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="col-3 text-right py-2">
+                <strong htmlFor="groupDnProperty">{t('security_setting.ldap.group_search_user_DN_property')}</strong>
+              </div>
+              <div className="col-6">
                 <input
                   className="form-control"
                   type="text"
@@ -393,7 +416,7 @@ class LdapSecuritySetting extends React.Component {
               </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,7 @@ 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-light 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-3 text-right py-2">
+                <strong>{t('Register limitation')}</strong>
+              </div>
+              <div className="col-6">
+                <div className="dropdown">
+                  <button
+                    className="btn btn-light 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="help-block 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_instance')}
-                    <code>@growi.org</code>{t('security_setting.only_those')}<br />
-                    {t('security_setting.insert_single')}
-                  </p>
-                </div>
+            <div className="row">
+              <div className="col-3 text-right">
+                <strong dangerouslySetInnerHTML={{ __html: t('The whitelist of registration permission E-mail address') }} />
+              </div>
+              <div className="col-6">
+                <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_instance')}
+                  <code>@growi.org</code>{t('security_setting.only_those')}<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"

+ 34 - 32
src/client/js/components/Admin/Security/OidcSecuritySetting.jsx

@@ -64,29 +64,27 @@ class OidcSecurityManagement extends React.Component {
         </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="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">
+          <label className="col-3 text-right py-2">{t('security_setting.callback_URL')}</label>
+          <div className="col-6">
             <input
               className="form-control"
               type="text"
@@ -112,8 +110,8 @@ 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">
+              <label htmlFor="oidcProviderName" className="col-3 text-right py-2">{t('security_setting.providerName')}</label>
+              <div className="col-6">
                 <input
                   className="form-control"
                   type="text"
@@ -125,8 +123,8 @@ class OidcSecurityManagement extends React.Component {
             </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">
+              <label htmlFor="oidcIssuerHost" className="col-3 text-right py-2">{t('security_setting.issuerHost')}</label>
+              <div className="col-6">
                 <input
                   className="form-control"
                   type="text"
@@ -141,8 +139,8 @@ class OidcSecurityManagement extends React.Component {
             </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">
+              <label htmlFor="oidcClientId" className="col-3 text-right py-2">{t('security_setting.clientID')}</label>
+              <div className="col-6">
                 <input
                   className="form-control"
                   type="text"
@@ -157,8 +155,8 @@ class OidcSecurityManagement extends React.Component {
             </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">
+              <label htmlFor="oidcClientSecret" className="col-3 text-right py-2">{t('security_setting.client_secret')}</label>
+              <div className="col-6">
                 <input
                   className="form-control"
                   type="text"
@@ -177,8 +175,8 @@ class OidcSecurityManagement extends React.Component {
             </h3>
 
             <div className="row mb-5">
-              <label htmlFor="oidcAttrMapId" className="col-xs-3 text-right">Identifier</label>
-              <div className="col-xs-6">
+              <label htmlFor="oidcAttrMapId" className="col-3 text-right py-2">Identifier</label>
+              <div className="col-6">
                 <input
                   className="form-control"
                   type="text"
@@ -193,8 +191,8 @@ class OidcSecurityManagement extends React.Component {
             </div>
 
             <div className="row mb-5">
-              <label htmlFor="oidcAttrMapUserName" className="col-xs-3 text-right">{t('username')}</label>
-              <div className="col-xs-6">
+              <label htmlFor="oidcAttrMapUserName" className="col-3 text-right py-2">{t('username')}</label>
+              <div className="col-6">
                 <input
                   className="form-control"
                   type="text"
@@ -209,8 +207,8 @@ class OidcSecurityManagement extends React.Component {
             </div>
 
             <div className="row mb-5">
-              <label htmlFor="oidcAttrMapName" className="col-xs-3 text-right">{t('Name')}</label>
-              <div className="col-xs-6">
+              <label htmlFor="oidcAttrMapName" className="col-3 text-right py-2">{t('Name')}</label>
+              <div className="col-6">
                 <input
                   className="form-control"
                   type="text"
@@ -225,8 +223,8 @@ class OidcSecurityManagement extends React.Component {
             </div>
 
             <div className="row mb-5">
-              <label htmlFor="oidcAttrMapEmail" className="col-xs-3 text-right">{t('Email')}</label>
-              <div className="col-xs-6">
+              <label htmlFor="oidcAttrMapEmail" className="col-3 text-right py-2">{t('Email')}</label>
+              <div className="col-6">
                 <input
                   className="form-control"
                   type="text"
@@ -241,8 +239,8 @@ class OidcSecurityManagement extends React.Component {
             </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-3 text-right py-2">{t('security_setting.callback_URL')}</label>
+              <div className="col-6">
                 <input
                   className="form-control"
                   type="text"
@@ -263,15 +261,17 @@ class OidcSecurityManagement extends React.Component {
             </div>
 
             <div className="row mb-3">
-              <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-switch 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') }}
                   />
@@ -283,15 +283,17 @@ class OidcSecurityManagement extends React.Component {
             </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-switch 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') }}
                   />
@@ -303,7 +305,7 @@ class OidcSecurityManagement extends React.Component {
             </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"

+ 18 - 16
src/client/js/components/Admin/Security/SamlSecuritySetting.jsx

@@ -90,30 +90,28 @@ 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="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">
+          <label className="col-3 text-right py-2">{t('security_setting.callback_URL')}</label>
+          <div className="col-6">
             <input
               className="form-control"
               type="text"
@@ -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
 ...
@@ -423,15 +421,17 @@ pWVdnzS1VCO8fKsJ7YYIr+JmHvseph3kFUOI5RqkCcMZlKUv83aUThsTHw==
             </h3>
 
             <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-switch 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') }}
                   />
@@ -443,15 +443,17 @@ pWVdnzS1VCO8fKsJ7YYIr+JmHvseph3kFUOI5RqkCcMZlKUv83aUThsTHw==
             </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-switch 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') }}
                   />
@@ -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"

+ 121 - 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,111 @@ 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') }}
+              >
+                <i className="fa fa-users" /> ID/Pass
+              </NavLink>
+            </NavItem>
+            <NavItem>
+              <NavLink
+                className={`${activeTab === 'passport-ldap' && 'active'} `}
+                onClick={() => { this.toggleActiveTab('passport-ldap') }}
+              >
+                <i className="fa fa-sitemap" /> LDAP
+              </NavLink>
+            </NavItem>
+            <NavItem>
+              <NavLink
+                className={`${activeTab === 'passport-saml' && 'active'} `}
+                onClick={() => { this.toggleActiveTab('passport-saml') }}
+              >
+                <i className="fa fa-key" /> SAML
+              </NavLink>
+            </NavItem>
+            <NavItem>
+              <NavLink
+                className={`${activeTab === 'passport-oidc' && 'active'} `}
+                onClick={() => { this.toggleActiveTab('passport-oidc') }}
+              >
+                <i className="fa fa-openid" /> OIDC
+              </NavLink>
+            </NavItem>
+            <NavItem>
+              <NavLink
+                className={`${activeTab === 'passport-basic' && 'active'} `}
+                onClick={() => { this.toggleActiveTab('passport-basic') }}
+              >
+                <i className="fa fa-lock" /> BASIC
+              </NavLink>
+            </NavItem>
+            <NavItem>
+              <NavLink
+                className={`${activeTab === 'passport-google' && 'active'} `}
+                onClick={() => { this.toggleActiveTab('passport-google') }}
+              >
+                <i className="fa fa-google" /> Google
+              </NavLink>
+            </NavItem>
+            <NavItem>
+              <NavLink
+                className={`${activeTab === 'passport-github' && 'active'} `}
+                onClick={() => { this.toggleActiveTab('passport-github') }}
+              >
+                <i className="fa fa-github" /> GitHub
+              </NavLink>
+            </NavItem>
+            <NavItem>
+              <NavLink
+                className={`${activeTab === 'passport-twitter' && 'active'} `}
+                onClick={() => { this.toggleActiveTab('passport-twitter') }}
+              >
+                <i className="fa fa-twitter" /> Twitter
+              </NavLink>
+            </NavItem>
+            <NavItem>
+              <NavLink
+                className={`${activeTab === 'passport-facebook' && 'active'} `}
+                onClick={() => { this.toggleActiveTab('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>
     );

+ 114 - 143
src/client/js/components/Admin/Security/SecuritySetting.jsx

@@ -49,166 +49,137 @@ 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>
-                </div>
+        <div className="row mb-5">
+          <div className="col-3 text-right py-2">
+            <strong>{t('security_setting.Guest Users Access')}</strong>
+          </div>
+          <div className="col-6">
+            <div className="dropdown">
+              <button
+                className={`btn btn-light dropdown-toggle ${adminGeneralSecurityContainer.isWikiModeForced && 'disabled'}`}
+                type="button"
+                id="dropdownMenuButton"
+                data-toggle="dropdown"
+                aria-haspopup="true"
+                aria-expanded="true"
+              >
+                {currentRestrictGuestMode === 'Deny' && t('security_setting.guest_mode.deny')}
+                {currentRestrictGuestMode === 'Readonly' && t('security_setting.guest_mode.readonly')}
+              </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>
-          {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={{
+        </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 }),
                     }}
-                  />
-                </p>
-              </div>
-            </div>
+              />
+            </p>
+          </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() }}
-                />
-                <label htmlFor="isShowRestrictedByOwner">
-                  {t('security_setting.page_listing_1_desc')}
-                </label>
-              </div>
+        <div className="row mb-5">
+          <strong className="col-3 text-right" dangerouslySetInnerHTML={{ __html: t('security_setting.page_listing_1') }} />
+          <div className="col-6">
+            <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('security_setting.page_listing_1_desc')}
+              </label>
             </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-5">
+          <strong className="col-3 text-right" dangerouslySetInnerHTML={{ __html: t('security_setting.page_listing_2') }} />
+          <div className="col-6">
+            <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() }}
+              />
+              <label className="custom-control-label" htmlFor="isShowRestrictedByGroup">
+                {t('security_setting.page_listing_2_desc')}
+              </label>
             </div>
           </div>
+        </div>
 
-          <div className="row mb-5">
-            <strong className="col-xs-3 text-right"> {t('security_setting.complete_deletion')} </strong>
-            <div className="col-xs-9 text-left">
-              <div className="my-0 btn-group">
-                <div className="dropdown">
-                  <button className="btn btn-default dropdown-toggle w-100" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
-                    <span className="pull-left">
-                      {currentPageCompleteDeletionAuthority === 'anyOne' && t('security_setting.anyone')}
-                      {currentPageCompleteDeletionAuthority === 'adminOnly' && t('security_setting.admin_only')}
-                      {(currentPageCompleteDeletionAuthority === 'adminAndAuthor' || currentPageCompleteDeletionAuthority == null)
-                        && t('security_setting.admin_and_author')}
-                    </span>
-                    <span className="bs-caret pull-right">
-                      <span className="caret" />
-                    </span>
-                  </button>
-                  {/* TODO adjust dropdown after BS4 */}
-                  <ul className="dropdown-menu" role="menu">
-                    <li
-                      key="anyone"
-                      role="presentation"
-                      type="button"
-                      onClick={() => { adminGeneralSecurityContainer.changePageCompleteDeletionAuthority('anyOne') }}
-                    >
-                      <a role="menuitem">{t('security_setting.anyone')}</a>
-                    </li>
-                    <li
-                      key="admin_only"
-                      role="presentation"
-                      type="button"
-                      onClick={() => { adminGeneralSecurityContainer.changePageCompleteDeletionAuthority('adminOnly') }}
-                    >
-                      <a role="menuitem">{t('security_setting.admin_only')}</a>
-                    </li>
-                    <li
-                      key="admin_and_author"
-                      role="presentation"
-                      type="button"
-                      onClick={() => { adminGeneralSecurityContainer.changePageCompleteDeletionAuthority('adminAndAuthor') }}
-                    >
-                      <a role="menuitem">{t('security_setting.admin_and_author')}</a>
-                    </li>
-                  </ul>
-                </div>
-                <p className="help-block small">
-                  {t('security_setting.complete_deletion_explain')}
-                </p>
-              </div>
-            </div>
+        <div className="row mb-5">
+          <div className="col-3 text-right">
+            <strong>{t('security_setting.complete_deletion')}</strong>
           </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')}
+          <div className="col-9">
+            <div className="dropdown">
+              <button
+                className="btn btn-light dropdown-toggle"
+                type="button"
+                id="dropdownMenuButton"
+                data-toggle="dropdown"
+                aria-haspopup="true"
+                aria-expanded="true"
+              >
+                {currentPageCompleteDeletionAuthority === 'anyOne' && t('security_setting.anyone')}
+                {currentPageCompleteDeletionAuthority === 'adminOnly' && t('security_setting.admin_only')}
+                {(currentPageCompleteDeletionAuthority === 'adminAndAuthor' || currentPageCompleteDeletionAuthority == null)
+                    && t('security_setting.admin_and_author')}
               </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="help-block small">
+                {t('security_setting.complete_deletion_explain')}
+              </p>
             </div>
           </div>
-        </fieldset>
+        </div>
+        <div className="row my-3">
+          <div className="offset-3 col-5">
+            <button type="button" className="btn btn-primary" disabled={this.state.retrieveError != null} onClick={this.putSecuritySetting}>
+              {t('Update')}
+            </button>
+          </div>
+        </div>
       </React.Fragment>
     );
   }

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

@@ -49,7 +49,7 @@ class TwitterSecurityManagement extends React.Component {
 
   render() {
     const { t, adminGeneralSecurityContainer, adminTwitterSecurityContainer } = this.props;
-    const { isTwitterEnabled } = adminTwitterSecurityContainer.state;
+    const { isTwitterEnabled } = adminGeneralSecurityContainer.state;
 
     if (this.state.isRetrieving) {
       return null;
@@ -69,29 +69,27 @@ class TwitterSecurityManagement extends React.Component {
         )}
 
         <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="offset-3 col-6">
+            <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-3 text-right py-2">{t('security_setting.callback_URL')}</label>
+          <div className="col-6">
             <input
               className="form-control"
               type="text"
@@ -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-3 text-right py-2">{t('security_setting.clientID')}</label>
+              <div className="col-6">
                 <input
                   className="form-control"
                   type="text"
@@ -134,8 +132,8 @@ class TwitterSecurityManagement extends React.Component {
             </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-3 text-right py-2">{t('security_setting.client_secret')}</label>
+              <div className="col-6">
                 <input
                   className="form-control"
                   type="text"
@@ -150,15 +148,17 @@ class TwitterSecurityManagement extends React.Component {
             </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-switch 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') }}
                   />
@@ -170,7 +170,7 @@ class TwitterSecurityManagement extends React.Component {
             </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"

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

@@ -23,10 +23,10 @@ class UserGroupDetailPage extends React.Component {
         <div className="m-t-20 form-box">
           <UserGroupEditForm />
         </div>
-        <legend className="m-t-20">{t('admin:user_group_management.user_list')}</legend>
+        <h2 className="admin-setting-header m-t-20">{t('admin:user_group_management.user_list')}</h2>
         <UserGroupUserTable />
         <UserGroupUserModal />
-        <legend className="m-t-20">{t('Page')}</legend>
+        <h2 className="admin-setting-header m-t-20">{t('Page')}</h2>
         <div className="page-list">
           <UserGroupPageList />
         </div>

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

@@ -63,7 +63,7 @@ class UserGroupEditForm extends React.Component {
     return (
       <form className="form-horizontal" onSubmit={this.handleSubmit}>
         <fieldset>
-          <legend>{t('admin:user_group_management.basic_info')}</legend>
+          <h2 className="admin-setting-header">{t('admin:user_group_management.basic_info')}</h2>
           <div className="form-group">
             <label htmlFor="name" className="col-sm-2 control-label">{t('Name')}</label>
             <div className="col-sm-4">

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

@@ -85,7 +85,7 @@ class ExternalAccountTable extends React.Component {
                         </span>
                       )
                       : (
-                        <span className="label label-warning">
+                        <span className="badge badge-warning">
                           {t('admin:user_management.unset')}
                         </span>
                       )

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

@@ -78,9 +78,9 @@ class UserInviteModal extends React.Component {
 
     return (
       <>
-        <div className="checkbox checkbox-success text-left" onChange={this.handleCheckBox} style={{ flex: 0.95 }}>
-          <input type="checkbox" id="sendEmail" className="form-check-input" name="sendEmail" defaultChecked={this.state.sendEmail} />
-          <label htmlFor="sendEmail">
+        <div className="ccustom-control custom-switch custom-checkbox-info text-left" onChange={this.handleCheckBox} style={{ flex: 0.95 }}>
+          <input type="checkbox" id="sendEmail" className="custom-control-input" name="sendEmail" defaultChecked={this.state.sendEmail} />
+          <label className="custom-control-label" htmlFor="sendEmail">
             {t('admin:user_management.invite_modal.invite_thru_email')}
           </label>
         </div>

+ 84 - 10
src/client/js/components/Navbar/PersonalDropdown.jsx

@@ -25,20 +25,94 @@ const PersonalDropdown = (props) => {
     window.location.href = '/logout';
   };
 
+  const followOsCheckboxModifiedHandler = (bool) => {
+    // reset user preference
+    if (bool) {
+      appContainer.setColorSchemePreference(null);
+    }
+    // set preferDarkModeByMediaQuery as users preference
+    else {
+      appContainer.setColorSchemePreference(appContainer.state.preferDarkModeByMediaQuery);
+    }
+  };
+
+  const userPreferenceSwitchModifiedHandler = (bool) => {
+    appContainer.setColorSchemePreference(bool);
+  };
+
+
+  /*
+   * render
+   */
+  const { preferDarkModeByMediaQuery, preferDarkModeByUser } = appContainer.state;
+  const isUserPreferenceExists = preferDarkModeByUser != null;
+  const isDarkMode = () => {
+    if (isUserPreferenceExists) {
+      return preferDarkModeByUser;
+    }
+    return preferDarkModeByMediaQuery;
+  };
+
   return (
     <>
-      <a className="dropdown-toggle waves-effect waves-light" data-toggle="dropdown">
+      {/* Button */}
+      <a className="nav-link dropdown-toggle waves-effect waves-light" data-toggle="dropdown">
         <UserPicture user={user} withoutLink />&nbsp;{user.name}
       </a>
-      <ul className="dropdown-menu dropdown-menu-right">
-        <li><a href={`/user/${user.username}`}><i className="icon-fw icon-home"></i>{ t('Home') }</a></li>
-        <li><a href="/me"><i className="icon-fw icon-wrench"></i>{ t('User Settings') }</a></li>
-        <li role="separator" className="divider"></li>
-        <li><a href={`/user/${user.username}#user-draft-list`}><i className="icon-fw icon-docs"></i>{ t('List Drafts') }</a></li>
-        <li><a href="/trash"><i className="icon-fw icon-trash"></i>{ t('Deleted Pages') }</a></li>
-        <li role="separator" className="divider"></li>
-        <li><a role="button" onClick={logoutHandler}><i className="icon-fw icon-power"></i>{ t('Sign out') }</a></li>
-      </ul>
+
+      {/* Menu */}
+      <div className="dropdown-menu dropdown-menu-right">
+
+        <a className="dropdown-item" href={`/user/${user.username}`}><i className="icon-fw icon-user"></i>{ t('User\'s Home') }</a>
+        <a className="dropdown-item" href="/me"><i className="icon-fw icon-wrench"></i>{ t('User Settings') }</a>
+
+        <div className="dropdown-divider"></div>
+
+        <a className="dropdown-item" href={`/user/${user.username}#user-draft-list`}><i className="icon-fw icon-docs"></i>{ t('List Drafts') }</a>
+        <a className="dropdown-item" href="/trash"><i className="icon-fw icon-trash"></i>{ t('Deleted Pages') }</a>
+
+        <div className="dropdown-divider"></div>
+
+        <h6 className="dropdown-header">Color Scheme</h6>
+        <form className="px-4">
+          <div className="form-row align-items-center">
+            <div className="col-auto">
+              <div className="custom-control custom-checkbox">
+                <input
+                  id="cbFollowOs"
+                  className="custom-control-input"
+                  type="checkbox"
+                  checked={!isUserPreferenceExists}
+                  onChange={e => followOsCheckboxModifiedHandler(e.target.checked)}
+                />
+                <label className="custom-control-label" htmlFor="cbFollowOs">Use OS Setting</label>
+              </div>
+            </div>
+          </div>
+          <div className="form-row align-items-center">
+            <div className="col-auto d-flex">
+              <span className={isUserPreferenceExists ? '' : 'text-muted'}>Light</span>
+              <div className="custom-control custom-switch custom-checkbox-secondary ml-2">
+                <input
+                  id="swUserPreference"
+                  className="custom-control-input"
+                  type="checkbox"
+                  checked={isDarkMode()}
+                  disabled={!isUserPreferenceExists}
+                  onChange={e => userPreferenceSwitchModifiedHandler(e.target.checked)}
+                />
+                <label className="custom-control-label" htmlFor="swUserPreference"></label>
+              </div>
+              <span className={isUserPreferenceExists ? '' : 'text-muted'}>Dark</span>
+            </div>
+          </div>
+        </form>
+
+        <div className="dropdown-divider"></div>
+
+        <a className="dropdown-item" type="button" onClick={logoutHandler}><i className="icon-fw icon-power"></i>{ t('Sign out') }</a>
+      </div>
+
     </>
   );
 

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

@@ -59,7 +59,7 @@ export default class CopyDropdown extends React.Component {
 
           <DropdownToggle
             caret
-            className="btn-copy"
+            className="d-block text-muted bg-transparent btn-copy"
             style={this.props.buttonStyle}
           >
             <i className="ti-clipboard"></i>

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

@@ -159,7 +159,7 @@ class RevisionPath extends React.Component {
 
         <CopyDropdown t={this.props.t} pagePath={this.props.pagePath} pageId={this.props.pageId} buttonStyle={buttonStyle}></CopyDropdown>
 
-        <a href="#edit" className="btn btn-default btn-edit" style={buttonStyle}>
+        <a href="#edit" className="d-block btn btn-default btn-edit text-muted" style={buttonStyle}>
           <i className="icon-note" />
         </a>
       </span>

+ 2 - 2
src/client/js/components/Page/TagLabels.jsx

@@ -110,13 +110,13 @@ class TagLabels extends React.Component {
     return (
       <div className={`tag-viewer ${pageId ? 'existed-page' : 'new-page'}`}>
         {tags.length === 0 && (
-          <a className="btn btn-link btn-edit-tags no-tags p-0" onClick={this.showEditor}>
+          <a className="btn btn-link btn-edit-tags no-tags p-0 text-muted" onClick={this.showEditor}>
             { t('Add tags for this page') } <i className="manage-tags ml-2 icon-plus"></i>
           </a>
         )}
         {tagElements}
         {tags.length > 0 && (
-          <a className="btn btn-link btn-edit-tags p-0" onClick={this.showEditor}>
+          <a className="btn btn-link btn-edit-tags p-0 text-muted" onClick={this.showEditor}>
             <i className="manage-tags ml-2 icon-plus"></i> { t('Edit tags for this page') }
           </a>
         )}

+ 2 - 2
src/client/js/components/PageComment/Comment.jsx

@@ -108,8 +108,8 @@ class Comment extends React.PureComponent {
   }
 
   getRevisionLabelClassName() {
-    return `page-comment-revision label ${
-      this.isCurrentRevision() ? 'label-primary' : 'label-default'}`;
+    return `page-comment-revision badge ${
+      this.isCurrentRevision() ? 'badge-primary' : 'badge-default'}`;
   }
 
   editBtnClickedHandler() {

+ 4 - 1
src/client/js/components/PageEditor/MarkdownDrawioUtil.js

@@ -83,7 +83,10 @@ class MarkdownDrawioUtil {
    * return boolean value whether the cursor position is in a drawio
    */
   isInDrawioBlock(editor) {
-    return (this.getBod(editor) !== this.getEod(editor));
+    const bod = this.getBod(editor);
+    const eod = this.getEod(editor);
+
+    return (JSON.stringify(bod) !== JSON.stringify(eod));
   }
 
   /**

+ 14 - 8
src/client/js/components/PageEditorByHackmd.jsx

@@ -61,6 +61,16 @@ class PageEditorByHackmd extends React.Component {
     return envVars.HACKMD_URI;
   }
 
+  get isResume() {
+    const { pageContainer } = this.props;
+    const {
+      pageIdOnHackmd, hasDraftOnHackmd, isHackmdDraftUpdatingInRealtime,
+    } = pageContainer.state;
+
+    const isPageExistsOnHackmd = (pageIdOnHackmd != null);
+    return (isPageExistsOnHackmd && hasDraftOnHackmd) || isHackmdDraftUpdatingInRealtime;
+  }
+
   /**
    * Start integration with HackMD
    */
@@ -217,11 +227,9 @@ class PageEditorByHackmd extends React.Component {
     const hackmdUri = this.getHackmdUri();
     const { pageContainer } = this.props;
     const {
-      pageIdOnHackmd, revisionId, revisionIdHackmdSynced, remoteRevisionId, hasDraftOnHackmd,
+      revisionId, revisionIdHackmdSynced, remoteRevisionId,
     } = pageContainer.state;
 
-    const isPageExistsOnHackmd = (pageIdOnHackmd != null);
-    const isResume = isPageExistsOnHackmd && hasDraftOnHackmd;
 
     let content;
 
@@ -238,7 +246,7 @@ class PageEditorByHackmd extends React.Component {
     /*
      * Resume to edit or discard changes
      */
-    else if (isResume) {
+    else if (this.isResume) {
       const isHackmdDocumentOutdated = revisionIdHackmdSynced !== remoteRevisionId;
 
       content = (
@@ -331,11 +339,9 @@ class PageEditorByHackmd extends React.Component {
     const hackmdUri = this.getHackmdUri();
     const { pageContainer } = this.props;
     const {
-      markdown, pageIdOnHackmd, hasDraftOnHackmd,
+      markdown, pageIdOnHackmd,
     } = pageContainer.state;
 
-    const isPageExistsOnHackmd = (pageIdOnHackmd != null);
-    const isResume = isPageExistsOnHackmd && hasDraftOnHackmd;
 
     let content;
 
@@ -345,7 +351,7 @@ class PageEditorByHackmd extends React.Component {
           ref={(c) => { this.hackmdEditor = c }}
           hackmdUri={hackmdUri}
           pageIdOnHackmd={pageIdOnHackmd}
-          initializationMarkdown={isResume ? null : markdown}
+          initializationMarkdown={this.isResume ? null : markdown}
           onChange={this.hackmdEditorChangeHandler}
           onSaveWithShortcut={(document) => {
             this.onSaveWithShortcut(document);

+ 1 - 1
src/client/js/components/PageHistory/PageRevisionList.jsx

@@ -95,7 +95,7 @@ export default class PageRevisionList extends React.Component {
 
     return (
       <React.Fragment>
-        <div className="custom-control custom-switch float-right">
+        <div className="custom-control custom-checkbox custom-checkbox-info float-right">
           <input
             type="checkbox"
             id="cbCompactize"

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

@@ -92,7 +92,7 @@ class SearchPage extends React.Component {
   render() {
     return (
       <div>
-        <div className="search-page-input">
+        <div className="search-page-input sps sps--abv">
           <SearchPageForm
             t={this.props.t}
             onSearchFormChanged={this.search}

+ 0 - 5
src/client/js/components/SearchPage/SearchResult.jsx

@@ -264,11 +264,6 @@ class SearchResult extends React.Component {
       );
     });
 
-    // TODO あとでなんとかする
-    setTimeout(() => {
-      $('#search-result-list > nav').affix({ offset: { top: 50 } });
-    }, 1200);
-
     /*
     UI あとで考える
     <span className="search-result-meta">Found: {this.props.searchResultMeta.total} pages with "{this.props.searchingKeyword}"</span>

+ 41 - 16
src/client/js/components/TableOfContents.jsx

@@ -1,15 +1,19 @@
 import React from 'react';
 import PropTypes from 'prop-types';
+import loggerFactory from '@alias/logger';
 
 import { withTranslation } from 'react-i18next';
 
 import { debounce } from 'throttle-debounce';
+import StickyEvents from 'sticky-events';
 
 import AppContainer from '../services/AppContainer';
 import PageContainer from '../services/PageContainer';
 
 import { createSubscribedElement } from './UnstatedUtils';
 
+const logger = loggerFactory('growi:TableOfContents');
+
 // get these value with
 //   document.querySelector('.revision-toc').getBoundingClientRect().top
 const DEFAULT_REVISION_TOC_TOP_FOR_GROWI_LAYOUT = 190;
@@ -27,37 +31,54 @@ class TableOfContents extends React.Component {
   constructor(props) {
     super(props);
 
+    this.init = this.init.bind(this);
     this.resetScrollbarDebounced = debounce(100, this.resetScrollbar);
+
+    const { layoutType } = this.props.appContainer.config;
+
+    this.defaultRevisionTocTop = DEFAULT_REVISION_TOC_TOP_FOR_GROWI_LAYOUT;
+    if (layoutType === 'kibela') {
+      this.defaultRevisionTocTop = DEFAULT_REVISION_TOC_TOP_FOR_KIBELA_LAYOUT;
+    }
+  }
+
+  componentDidMount() {
+    this.init();
+    this.resetScrollbar();
   }
 
   componentDidUpdate() {
+    this.resetScrollbar();
+  }
+
+  init() {
     const { layoutType } = this.props.appContainer.config;
     if (layoutType === 'crowi') {
       return;
     }
 
-    let defaultRevisionTocTop = DEFAULT_REVISION_TOC_TOP_FOR_GROWI_LAYOUT;
-    if (layoutType === 'kibela') {
-      defaultRevisionTocTop = DEFAULT_REVISION_TOC_TOP_FOR_KIBELA_LAYOUT;
-    }
-
-    // initialize
-    this.resetScrollbar(defaultRevisionTocTop);
-
     /*
      * set event listener
      */
     // resize
     window.addEventListener('resize', (event) => {
-      this.resetScrollbarDebounced(defaultRevisionTocTop);
+      this.resetScrollbarDebounced(this.defaultRevisionTocTop);
+    });
+
+    // sticky
+    // See: https://github.com/ryanwalters/sticky-events
+    const stickyEvents = new StickyEvents({
+      stickySelector: '#revision-toc',
     });
-    // affix on
-    $('#revision-toc').on('affixed.bs.affix', () => {
-      this.resetScrollbar(this.getCurrentRevisionTocTop());
+    const { stickySelector } = stickyEvents;
+    const elem = document.querySelector(stickySelector);
+    elem.addEventListener(StickyEvents.STUCK, (event) => {
+      logger.debug('StickyEvents.STUCK detected');
+      this.resetScrollbar();
     });
-    // affix off
-    $('#revision-toc').on('affixed-top.bs.affix', () => {
-      this.resetScrollbar(defaultRevisionTocTop);
+    elem.addEventListener(StickyEvents.UNSTUCK, (event) => {
+      logger.debug('StickyEvents.UNSTUCK detected');
+      this.resetScrollbar(this.defaultRevisionTocTop);
     });
   }
 
@@ -67,18 +88,22 @@ class TableOfContents extends React.Component {
     return revisionTocElem.getBoundingClientRect().top;
   }
 
-  resetScrollbar(revisionTocTop) {
+  resetScrollbar(defaultRevisionTocTop) {
     const tocContentElem = document.querySelector('.revision-toc .markdownIt-TOC');
 
     if (tocContentElem == null) {
       return;
     }
 
+    const revisionTocTop = defaultRevisionTocTop || this.getCurrentRevisionTocTop();
     // window height - revisionTocTop - .system-version height
     const viewHeight = window.innerHeight - revisionTocTop - 20;
 
     const tocContentHeight = tocContentElem.getBoundingClientRect().height + 15; // add margin
 
+    logger.debug('viewHeight', viewHeight);
+    logger.debug('tocContentHeight', tocContentHeight);
+
     if (viewHeight < tocContentHeight) {
       $('#revision-toc-content').slimScroll({
         railVisible: true,

+ 0 - 23
src/client/js/legacy/crowi.js

@@ -116,28 +116,6 @@ Crowi.handleKeyCtrlSlashHandler = (event) => {
   event.preventDefault();
 };
 
-Crowi.initAffix = () => {
-  const $affixContent = $('#page-header');
-  if ($affixContent.length > 0) {
-    const $affixContentContainer = $('.row.grw-subnav');
-    const containerHeight = $affixContentContainer.outerHeight(true);
-    $affixContent.affix({
-      offset: {
-        top() {
-          return $('.navbar').outerHeight(true) + containerHeight;
-        },
-      },
-    });
-    $('[data-affix-disable]').on('click', function(e) {
-      const $elm = $($(this).data('affix-disable'));
-      $(window).off('.affix');
-      $elm.removeData('affix').removeClass('affix affix-top affix-bottom');
-      return false;
-    });
-    $affixContentContainer.css({ 'min-height': containerHeight });
-  }
-};
-
 Crowi.initClassesByOS = function() {
   // add classes to cmd-key by OS
   const platform = navigator.platform.toLowerCase();
@@ -665,7 +643,6 @@ window.addEventListener('load', (e) => {
 
   Crowi.highlightSelectedSection(window.location.hash);
   Crowi.modifyScrollTop();
-  Crowi.initAffix();
   Crowi.initClassesByOS();
 });
 

+ 68 - 0
src/client/js/services/AppContainer.js

@@ -31,6 +31,8 @@ export default class AppContainer extends Container {
 
     this.state = {
       editorMode: null,
+      preferDarkModeByMediaQuery: false,
+      preferDarkModeByUser: null,
     };
 
     const body = document.querySelector('body');
@@ -97,6 +99,33 @@ export default class AppContainer extends Container {
     return 'AppContainer';
   }
 
+  init() {
+    this.initColorScheme();
+    this.initPlugins();
+  }
+
+  async initColorScheme() {
+    const switchStateByMediaQuery = (mql) => {
+      const preferDarkMode = mql.matches;
+      this.setState({ preferDarkModeByMediaQuery: preferDarkMode });
+
+      this.applyColorScheme();
+    };
+
+    const mqlForDarkMode = window.matchMedia('(prefers-color-scheme: dark)');
+    // add event listener
+    mqlForDarkMode.addListener(switchStateByMediaQuery);
+
+    // restore settings from localStorage
+    const { localStorage } = window;
+    if (localStorage.preferDarkModeByUser != null) {
+      await this.setState({ preferDarkModeByUser: localStorage.preferDarkModeByUser === 'true' });
+    }
+
+    // initialize
+    switchStateByMediaQuery(mqlForDarkMode);
+  }
+
   initPlugins() {
     if (this.isPluginEnabled) {
       const growiPlugin = window.growiPlugin;
@@ -314,6 +343,45 @@ export default class AppContainer extends Container {
     targetComponent.launchDrawioModal(beginLineNumber, endLineNumber);
   }
 
+  /**
+   * Set color scheme preference by user
+   * @param {boolean} isDarkMode
+   */
+  async setColorSchemePreference(isDarkMode) {
+    await this.setState({ preferDarkModeByUser: isDarkMode });
+
+    // store settings to localStorage
+    const { localStorage } = window;
+    if (isDarkMode == null) {
+      delete localStorage.removeItem('preferDarkModeByUser');
+    }
+    else {
+      localStorage.preferDarkModeByUser = isDarkMode;
+    }
+
+    this.applyColorScheme();
+  }
+
+  /**
+   * Apply color scheme as 'dark' attribute of <html></html>
+   */
+  applyColorScheme() {
+    const { preferDarkModeByMediaQuery, preferDarkModeByUser } = this.state;
+    let isDarkMode = preferDarkModeByMediaQuery;
+    if (preferDarkModeByUser != null) {
+      isDarkMode = preferDarkModeByUser;
+    }
+
+    // switch to dark mode
+    if (isDarkMode) {
+      document.documentElement.setAttribute('dark', 'true');
+    }
+    // switch to light mode
+    else {
+      document.documentElement.removeAttribute('dark');
+    }
+  }
+
   async apiGet(path, params) {
     return this.apiRequest('get', path, { params });
   }

+ 41 - 38
src/client/styles/agile-admin/inverse/colors/_apply-colors-dark.scss

@@ -1,16 +1,19 @@
 .top-left-part {
-  .logo-mark, .logo-text {
+  .logo-mark,
+  .logo-text {
     fill: white;
   }
 }
 
-
 /*
  * Button
  */
 .btn-default {
-  &:hover, &:focus,
-  &.active, &.active:hover, &.active:focus {
+  &:hover,
+  &:focus,
+  &.active,
+  &.active:hover,
+  &.active:focus {
     color: white;
     background-color: lighten($bodycolor, 5%);
   }
@@ -19,12 +22,14 @@
 /*
   * Form
   */
-input.form-control, textarea.form-control {
+input.form-control,
+textarea.form-control {
   color: lighten($bodytext, 30%);
   background-color: darken($bodycolor, 5%);
   border: 1px solid darken($border, 30%);
 }
-.form-control[disabled], .form-control[readonly] {
+.form-control[disabled],
+.form-control[readonly] {
   color: lighten($bodytext, 10%);
   background-color: lighten($bodycolor, 5%);
 }
@@ -61,8 +66,11 @@ input.form-control, textarea.form-control {
  * Panel
  */
 .panel {
-  &, &.panel-white, &.panel-default {
-    .panel-heading, .panel-body {
+  &,
+  &.panel-white,
+  &.panel-default {
+    .panel-heading,
+    .panel-body {
       color: $light;
     }
   }
@@ -71,36 +79,31 @@ input.form-control, textarea.form-control {
 /*
  * Table
  */
- .table > thead > tr > th, .table > tbody > tr > th, .table > tfoot > tr > th,
- .table > thead > tr > td, .table > tbody > tr > td, .table > tfoot > tr > td ,
- .table > thead > tr > th, .table-bordered{
-     border-top: 1px solid $border;
- }
-
- .table-bordered > thead > tr > th,
- .table-bordered > tbody > tr > th,
- .table-bordered > tfoot > tr > th,
- .table-bordered > thead > tr > td,
- .table-bordered > tbody > tr > td,
- .table-bordered > tfoot > tr > td {
-   border: 1px solid $border;
- }
- .table > thead > tr > th {
-     border-bottom: 1px solid $border;
- }
-
- .table-bordered {
-     border: 1px solid $border;
- }
+.table > thead > tr > th,
+.table > tbody > tr > th,
+.table > tfoot > tr > th,
+.table > thead > tr > td,
+.table > tbody > tr > td,
+.table > tfoot > tr > td,
+.table > thead > tr > th,
+.table-bordered {
+  border-top: 1px solid $border;
+}
 
+.table-bordered > thead > tr > th,
+.table-bordered > tbody > tr > th,
+.table-bordered > tfoot > tr > th,
+.table-bordered > thead > tr > td,
+.table-bordered > tbody > tr > td,
+.table-bordered > tfoot > tr > td {
+  border: 1px solid $border;
+}
+.table > thead > tr > th {
+  border-bottom: 1px solid $border;
+}
 
-/*
- * GROWI header
- */
-.main-container header.affix {
-  .logo-mark {
-    fill: white;
-  }
+.table-bordered {
+  border: 1px solid $border;
 }
 
 /*
@@ -160,8 +163,8 @@ legend {
 .admin-page {
   #themeOptions {
     .theme-option-container.active a {
-      background-color: darken($themecolor,15%);
-      border-color: darken($themecolor,15%);
+      background-color: darken($themecolor, 15%);
+      border-color: darken($themecolor, 15%);
     }
   }
 }

+ 0 - 29
src/client/styles/agile-admin/inverse/colors/default-dark.scss

@@ -1,29 +0,0 @@
-@import '../variables';
-
-$basecolor: #212731;
-$bgcolor-theme: #f75b36;
-
-$bgcolor-navbar: #151515;
-$bgcolor-global: #101010;
-$bgcolor-global: $basecolor;
-$color-header: lighten($basecolor, 45%);
-$color-global: lighten($basecolor, 35%);
-$linktext: lighten($basecolor, 45%);
-$linktext-hover: lighten($linktext, 10%);
-$sidebar-text: #a6acbc;
-$dark-themecolor: #4f5467;
-
-$primary: $bgcolor-theme;
-$fillcolor-logo-mark: #444;
-$color-link-wiki: saturate($color-global, 20%);
-$color-link-wiki-hover: darken($color-link-wiki, 5%);
-
-$dark: darken($color-global, 5%);
-$border: lighten($basecolor, 15%);
-$navbar-border: lighten($border, 10%);
-$active-navbar-border: darken($border, 3%);
-$btn-default-bgcolor: darken($basecolor, 10%);
-$bgcolor-inline-code: darken($bgcolor-global, 5%);
-
-@import 'apply-colors';
-@import 'apply-colors-dark';

+ 0 - 21
src/client/styles/agile-admin/inverse/colors/default.scss

@@ -1,21 +0,0 @@
-@import '../variables';
-
-$bgcolor-theme: #112744;
-
-$bgcolor-navbar: #334455;
-$bgcolor-global: #fff;
-$bgcolor-global: #fff;
-$color-header: #2b2b2b;
-$color-global: #333333;
-$linktext: lighten($bgcolor-theme, 20%);
-$linktext-hover: lighten($linktext, 20%);
-$sidebar-text: #38495a;
-
-$primary: $bgcolor-theme;
-
-$fillcolor-logo-mark: lighten(desaturate($bgcolor-navbar, 10%), 15%);
-$color-link-wiki: lighten($bgcolor-theme, 20%);
-$color-link-wiki-hover: lighten($color-link-wiki, 20%);
-
-@import 'apply-colors';
-@import 'apply-colors-light';

+ 16 - 18
src/client/styles/scss/_comment.scss

@@ -1,21 +1,3 @@
-.main-container {
-  .page-comment-main {
-    pointer-events: auto;
-
-    // delete button
-    .page-comment-control {
-      position: absolute;
-      top: 0;
-      right: 0;
-      visibility: hidden;
-    }
-
-    &:hover > .page-comment-control {
-      visibility: visible;
-    }
-  }
-}
-
 // modal
 .page-comment-delete-modal .modal-content {
   .modal-body {
@@ -59,4 +41,20 @@
       color: #999;
     }
   }
+
+  .page-comment-main {
+    pointer-events: auto;
+
+    // delete button
+    .page-comment-control {
+      position: absolute;
+      top: 0;
+      right: 0;
+      visibility: hidden;
+    }
+
+    &:hover > .page-comment-control {
+      visibility: visible;
+    }
+  }
 }

+ 10 - 69
src/client/styles/scss/_layout.scss

@@ -44,79 +44,21 @@
   border-bottom: 1px solid $grw-line-gray;
 }
 
-.header-wrap {
+header {
   padding-top: 0.5rem;
   padding-bottom: 0.5rem;
 
-  header {
-    line-height: 1em;
-    // the container of h1
-    div.title-container {
-      padding-right: 5px;
-      padding-left: 5px;
-      margin-right: auto;
-    }
-
-    .btn-copy,
-    .btn-copy-link,
-    .btn-edit {
-      display: block;
-      color: $text-muted;
-      border: none;
-      opacity: 0.3;
-
-      &:not(:hover) {
-        background-color: transparent;
-      }
-      // change button opacity
-      &:hover {
-        opacity: unset;
-      }
-    }
-
-    .btn-edit-tags {
-      color: $text-muted;
-      opacity: 0.5;
-
-      &.no-tags {
-        opacity: 0.7;
-      }
-      // change button opacity
-      &:hover {
-        opacity: unset;
-      }
-    }
-
-    h1 {
-      @include variable-font-size(28px);
-      line-height: 1.1em;
-    }
-
-    // affix
-    &.affix {
-      top: 0;
-      left: 0;
-      z-index: 15; // over the .sidebar
-      width: 100%;
-      padding: 2px 20px;
-      box-shadow: 0 0px 2px #999;
-
-      h1 {
-        @include variable-font-size(20px);
-      }
-    }
+  line-height: 1em;
+  // the container of h1
+  div.title-container {
+    padding-right: 5px;
+    padding-left: 5px;
+    margin-right: auto;
   }
 
-  #like-button,
-  #bookmark-button {
-    & button {
-      font-size: 1.2em;
-      line-height: 0.8em;
-
-      &:not(:hover):not(.active) {
-        background-color: transparent;
-      }
-    }
+  h1 {
+    @include variable-font-size(28px);
+    line-height: 1.1em;
   }
 }
 
@@ -145,7 +87,6 @@
 }
 
 .revision-toc {
-  max-width: 250px;
   overflow: hidden;
   font-size: 0.9em;
 

+ 4 - 5
src/client/styles/scss/_layout_growi.scss

@@ -19,11 +19,10 @@
   }
 
   .revision-toc {
-    &.affix {
-      top: calc(46px + 5px);
-      min-width: calc(#{100 / 12 * 2%} - #{$grid-gutter-width}); // width of 2column - padding
-      margin-top: 5px;
-    }
+    position: sticky;
+    top: calc(46px + 5px);
+    min-width: 100%;
+    margin-top: 5px;
 
     .revision-toc-content {
       padding: 0;

+ 5 - 12
src/client/styles/scss/_layout_kibela.scss

@@ -101,12 +101,11 @@ body.kibela {
   }
 
   .revision-toc {
-    &.affix {
-      top: calc(60px + 5px);
-      right: 10rem;
-      min-width: calc(#{100 / 12 * 2%} - #{$grid-gutter-width}); // width of 2column - padding
-      margin-top: 40px;
-    }
+    position: sticky;
+    top: calc(60px + 5px);
+    right: 10rem;
+    min-width: 100%;
+    margin-top: 40px;
 
     .revision-toc-content {
       padding: 0;
@@ -160,12 +159,6 @@ body.kibela {
     }
   }
 
-  /* user page */
-  .header-wrap {
-    padding: 0px;
-    margin-left: 2em;
-  }
-
   /* edit */
   .CodeMirror {
     border: solid 1.2px #d8d8d8;

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

@@ -12,20 +12,6 @@ $warning: #ee773b;
 $danger: #ff0a54;
 $light: #dee2e6;
 $dark: #343a40;
-//## Gray and brand colors for use across Bootstrap.
-$gray-base:              #000 !default;
-$gray-darker:            lighten($gray-base, 13.5%); // #222
-$gray-dark:              lighten($gray-base, 20%);   // #333
-$gray:                   lighten($gray-base, 33.5%); // #555
-$gray-light:             lighten($gray-base, 46.7%); // #777
-$gray-lighter:           lighten($gray-base, 93.5%); // #eee
-$gray-extralight:        #fafafa; // Growi original
-
-$brand-primary:         $primary;
-$brand-success:         $success;
-$brand-info:            $info;
-$brand-warning:         $warning;
-$brand-danger:          $danger;
 
 //== Typography
 //
@@ -50,7 +36,6 @@ $border-radius-small: 0;
 //
 //## For each of Bootstrap's buttons, define text, background and border color.
 
-$btn-default-bg: $btn-default-bgcolor;
 $btn-border-radius: 0;
 $btn-border-radius-lg: 0;
 $btn-border-radius-sm: 0;
@@ -90,13 +75,6 @@ $modal-header-padding-x: 1rem;
 
 //== Alerts
 $alert-border-radius: 0;
-//** `<input>` background color
-$input-bg:                       $bodycolor;
-//** `<input disabled>` background color
-$input-bg-disabled:              $btn-default-bgcolor;
-
-//** `<input>` border color
-$input-border:                   $border;
 
 //== Progress bar
 $progress-height: 4px;

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

@@ -2,8 +2,6 @@
 @import '~diff2html/bundles/css/diff2html.min.css';
 
 .main-container {
-  // padding controll of .header-wrap and .content-main are moved to _layout and _form
-
   .url-line {
     font-size: 1rem;
     color: #999;
@@ -48,7 +46,6 @@
 .main .content-main .revision-history {
   .revision-history-list {
     .revision-history-outer {
-
       // add border-top except of first element
       &:not(:first-of-type) {
         border-top: 1px solid $border;

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

@@ -1,23 +1,5 @@
 .growi {
   header {
-    div.title-logo-container {
-      display: none; // hide in default
-
-      a {
-        // centering
-        display: flex;
-        align-items: center;
-        justify-content: center;
-        width: 32px;
-        height: 32px;
-
-        img {
-          width: 16px;
-          height: 16px;
-        }
-      }
-    }
-
     ul.authors {
       padding-left: 1.5em;
       margin: 0;
@@ -39,29 +21,4 @@
       }
     }
   }
-
-  /*
-   * affix header
-   */
-  header:not(.affix) {
-    .only-affix {
-      display: none !important;
-    }
-  }
-  header.affix {
-    .not-affix {
-      display: none !important;
-    }
-
-    // show logo link
-    div.title-logo-container {
-      display: unset;
-      margin-right: 6px;
-      margin-left: -12px;
-    }
-    // hide authors in affix
-    .authors {
-      padding-left: 0.5em;
-    }
-  }
 }

+ 10 - 0
src/client/styles/scss/_page_header.scss

@@ -0,0 +1,10 @@
+#page-header {
+  &:hover {
+    .btn-copy,
+    .btn-edit,
+    .btn-edit-tags {
+      // change button opacity
+      opacity: unset;
+    }
+  }
+}

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

@@ -180,11 +180,9 @@
 
 .search-page-input {
   position: sticky;
-  top: 0;
+  top: 15px;
   z-index: 1;
 
-  // for sticky layout
-  padding-top: 15px;
   margin-bottom: 15px;
 
   .input-group-btn .btn {

+ 2 - 3
src/client/styles/scss/_user_growi.scss

@@ -7,8 +7,7 @@
   }
 
   .revision-toc {
-    &.affix {
-      top: 105px;
-    }
+    position: sticky;
+    top: 105px;
   }
 }

+ 29 - 0
src/client/styles/scss/atoms/_buttons.scss

@@ -25,3 +25,32 @@
   line-height: 1.33;
   border-radius: 35px;
 }
+
+#like-button,
+#bookmark-button {
+  & button {
+    font-size: 1.2em;
+    line-height: 0.8em;
+
+    &:not(:hover):not(.active) {
+      background-color: transparent;
+    }
+  }
+}
+
+.btn-copy,
+.btn-edit {
+  opacity: 0.3;
+
+  &:hover {
+    background-color: $light;
+  }
+}
+
+.btn-edit-tags {
+  opacity: 0.5;
+
+  &.no-tags {
+    opacity: 0.7;
+  }
+}

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

@@ -48,6 +48,7 @@
 @import 'on-edit';
 @import 'page_list';
 @import 'page';
+@import 'page_header';
 @import 'page_growi';
 @import 'search';
 @import 'shortcuts';

+ 18 - 17
src/client/styles/scss/theme/_apply-colors-dark.scss

@@ -26,7 +26,8 @@ input.form-control,
 textarea.form-control {
   color: lighten($color-global, 30%);
   background-color: darken($bgcolor-global, 5%);
-  border: 1px solid darken($border, 30%);
+  // FIXME: accent color
+  // border: 1px solid darken($border, 30%);
 }
 
 .form-control[disabled],
@@ -38,7 +39,8 @@ textarea.form-control {
 .input-group .input-group-addon {
   color: theme-color('dark');
   background-color: rgba($bgcolor-navbar, 0.4);
-  border: 1px solid darken($border, 30%);
+  // FIXME: accent color
+  // border: 1px solid darken($border, 30%);
   border-right: none;
 }
 
@@ -50,10 +52,6 @@ textarea.form-control {
   > li > a {
     color: $color-global;
   }
-
-  .divider {
-    background-color: $border;
-  }
 }
 
 .modal {
@@ -89,7 +87,8 @@ textarea.form-control {
 .table > tfoot > tr > td,
 .table > thead > tr > th,
 .table-bordered {
-  border-top: 1px solid $border;
+  // FIXME: use $table-dark-*
+  // border-top: 1px solid $border;
 }
 
 .table-bordered > thead > tr > th,
@@ -98,15 +97,18 @@ textarea.form-control {
 .table-bordered > thead > tr > td,
 .table-bordered > tbody > tr > td,
 .table-bordered > tfoot > tr > td {
-  border: 1px solid $border;
+  // FIXME: use $table-dark-*
+  // border: 1px solid $border;
 }
 
 .table > thead > tr > th {
-  border-bottom: 1px solid $border;
+  // FIXME: use $table-dark-*
+  // border-bottom: 1px solid $border;
 }
 
 .table-bordered {
-  border: 1px solid $border;
+  // FIXME: use $table-dark-*
+  // border: 1px solid $border;
 }
 
 /*
@@ -140,7 +142,8 @@ header.affix {
 .search-page {
   .input-group-btn {
     .btn-light {
-      border: 1px solid darken($border, 30%); // fit to input.form-control
+      // FIXME:
+      // border: 1px solid darken($border, 30%); // fit to input.form-control
     }
   }
 }
@@ -154,16 +157,14 @@ header.affix {
   }
 }
 
-legend {
-  border-color: lighten($border, 10%);
-}
-
 .wiki {
   h1 {
-    border-color: lighten($border, 10%);
+    // FIXME:
+    // border-color: lighten($border, 10%);
   }
   h2 {
-    border-color: $border;
+    // FIXME:
+    // border-color: $border;
   }
 }
 

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

@@ -59,10 +59,10 @@ header.affix {
 
 .wiki {
   h1 {
-    border-color: darken($border, 10%);
+    border-color: darken($border-color-theme, 10%);
   }
   h2 {
-    border-color: $border;
+    border-color: $border-color-theme;
   }
 }
 

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

@@ -1,71 +1,15 @@
 //
 //== Apply to Bootstrap
 //
-@import '~bootstrap/scss/bootstrap-reboot';
 
-@each $color, $value in $theme-colors {
-  @include bg-variant('.bg-#{$color}', $value);
-}
+// override bootstrap variables
+$body-bg: $bgcolor-global;
+$body-color: $color-global;
+$link-color: $color-link;
+$link-hover-color: $color-link-hover;
 
-@each $color, $value in $theme-colors {
-  .btn-#{$color} {
-    @include button-variant($value, $value);
-  }
-}
-
-@each $color, $value in $theme-colors {
-  .btn-outline-#{$color} {
-    @include button-outline-variant($value);
-  }
-}
-
-@each $theme-color, $color in $theme-colors {
-  .custom-checkbox-#{$theme-color} {
-    .custom-control-label::before {
-      border-color: #d7d7d7;
-      transition: 0.3s ease-in-out;
-    }
-    .custom-control-input:checked + .custom-control-label::before {
-      background-color: $color;
-      border-color: $color;
-    }
-    .custom-control-input:checked + .custom-control-label::after {
-      color: white;
-    }
-    .custom-control-input:not(:disabled):active ~ .custom-control-label::before {
-      color: white;
-      background-color: $color;
-      border-color: $color;
-    }
-    .custom-control-input:focus:not(:checked) ~ .custom-control-label::before {
-      color: white;
-      background-color: white;
-      border-color: #d7d7d7;
-    }
-  }
-}
-
-@each $theme-color, $color in $theme-colors {
-  .alert.alert-#{$theme-color} {
-    color: white;
-    background: $color;
-    border: none;
-
-    a:not(.btn) {
-      color: white;
-
-      &:hover,
-      &:focus {
-        color: lighten($color, 30%);
-      }
-    }
-  }
-}
-
-// Border light
-.border-light {
-  border-color: $light !important;
-}
+@import 'reboot-bootstrap-colors';
+@import 'reboot-bootstrap-theme-colors';
 
 // Link buttons
 .btn-link {
@@ -105,10 +49,6 @@
 //== Apply to GROWI Elements
 //
 
-body {
-  background: $bgcolor-global;
-}
-
 .logo {
   // set transition for fill
   svg * {
@@ -155,19 +95,12 @@ body {
   background-color: $bgcolor-inline-code;
 }
 
-/*
- * Legend
- */
-legend {
-  color: $color-header;
-}
-
 /*
  * Modal
  */
 .modal {
   .modal-header {
-    border-bottom-color: $border;
+    border-bottom-color: $border-color-theme;
 
     &.bg-primary,
     &.bg-info,
@@ -195,7 +128,7 @@ legend {
   }
 
   .modal-footer {
-    border-top-color: $border;
+    border-top-color: $border-color-theme;
   }
 }
 
@@ -238,7 +171,7 @@ legend {
  */
 .crowi-sidebar {
   background-color: darken($bgcolor-global, 4%);
-  border-left: solid 1px $border;
+  border-left: solid 1px $border-color;
 
   .system-version {
     background-color: darken($bgcolor-global, 4%);
@@ -284,11 +217,11 @@ body.on-edit {
     background-color: darken($bgcolor-global, 2%);
 
     .page-editor-editor-container {
-      border-right-color: $navbar-border;
+      border-right-color: $border-color-theme;
 
       .navbar-editor {
         background-color: $bgcolor-global; // same color with active tab
-        border-bottom-color: $border;
+        border-bottom-color: $border-color-theme;
       }
     }
 
@@ -297,7 +230,7 @@ body.on-edit {
     }
 
     .page-editor-footer {
-      border-top-color: $border;
+      border-top-color: $border-color-theme;
     }
   }
 }
@@ -307,7 +240,7 @@ body.on-edit {
  */
 .growi .main {
   .page-comments-row {
-    border-top-color: $border;
+    border-top-color: $border-color-theme;
   }
 
   .page-comment .page-comment-main,
@@ -357,7 +290,7 @@ body.on-edit {
  */
 .page-attachments-row {
   background-color: darken($bgcolor-global, 2%);
-  border-top-color: $border;
+  border-top-color: $border-color-theme;
 }
 
 /*

+ 481 - 0
src/client/styles/scss/theme/_reboot-bootstrap-colors.scss

@@ -0,0 +1,481 @@
+@import '~bootstrap/scss/functions';
+@import '~bootstrap/scss/variables';
+@import '~bootstrap/scss/mixins';
+//
+//
+// Apply partially
+//   https://github.com/twbs/bootstrap/blob/v4.4.1/scss/_reboot.scss
+//
+//
+
+// stylelint-disable at-rule-no-vendor-prefix, declaration-no-important, selector-no-qualifying-type, property-no-vendor-prefix
+
+// Reboot
+//
+// Normalization of HTML elements, manually forked from Normalize.css to remove
+// styles targeting irrelevant browsers while applying new styles.
+//
+// Normalize is licensed MIT. https://github.com/necolas/normalize.css
+
+// Document
+//
+// 1. Change from `box-sizing: content-box` so that `width` is not affected by `padding` or `border`.
+// 2. Change the default font family in all browsers.
+// 3. Correct the line height in all browsers.
+// 4. Prevent adjustments of font size after orientation changes in IE on Windows Phone and in iOS.
+// 5. Change the default tap highlight to be completely transparent in iOS.
+
+// *,
+// *::before,
+// *::after {
+//   box-sizing: border-box; // 1
+// }
+
+// html {
+//   font-family: sans-serif; // 2
+//   line-height: 1.15; // 3
+//   -webkit-text-size-adjust: 100%; // 4
+//   -webkit-tap-highlight-color: rgba($black, 0); // 5
+// }
+
+// Shim for "new" HTML5 structural elements to display correctly (IE10, older browsers)
+// TODO: remove in v5
+// stylelint-disable-next-line selector-list-comma-newline-after
+// article, aside, figcaption, figure, footer, header, hgroup, main, nav, section {
+//   display: block;
+// }
+
+// Body
+//
+// 1. Remove the margin in all browsers.
+// 2. As a best practice, apply a default `background-color`.
+// 3. Set an explicit initial text-align value so that we can later use
+//    the `inherit` value on things like `<th>` elements.
+
+body {
+  // margin: 0; // 1
+  // font-family: $font-family-base;
+  // @include font-size($font-size-base);
+  // font-weight: $font-weight-base;
+  // line-height: $line-height-base;
+  color: $body-color;
+  // text-align: left; // 3
+  background-color: $body-bg; // 2
+}
+
+// Future-proof rule: in browsers that support :focus-visible, suppress the focus outline
+// on elements that programmatically receive focus but wouldn't normally show a visible
+// focus outline. In general, this would mean that the outline is only applied if the
+// interaction that led to the element receiving programmatic focus was a keyboard interaction,
+// or the browser has somehow determined that the user is primarily a keyboard user and/or
+// wants focus outlines to always be presented.
+//
+// See https://developer.mozilla.org/en-US/docs/Web/CSS/:focus-visible
+// and https://developer.paciellogroup.com/blog/2018/03/focus-visible-and-backwards-compatibility/
+// [tabindex="-1"]:focus:not(:focus-visible) {
+//   outline: 0 !important;
+// }
+
+// Content grouping
+//
+// 1. Add the correct box sizing in Firefox.
+// 2. Show the overflow in Edge and IE.
+
+// hr {
+//   box-sizing: content-box; // 1
+//   height: 0; // 1
+//   overflow: visible; // 2
+// }
+
+//
+// Typography
+//
+
+// Remove top margins from headings
+//
+// By default, `<h1>`-`<h6>` all receive top and bottom margins. We nuke the top
+// margin for easier control within type scales as it avoids margin collapsing.
+// stylelint-disable-next-line selector-list-comma-newline-after
+// h1, h2, h3, h4, h5, h6 {
+//   margin-top: 0;
+//   margin-bottom: $headings-margin-bottom;
+// }
+
+// Reset margins on paragraphs
+//
+// Similarly, the top margin on `<p>`s get reset. However, we also reset the
+// bottom margin to use `rem` units instead of `em`.
+// p {
+//   margin-top: 0;
+//   margin-bottom: $paragraph-margin-bottom;
+// }
+
+// Abbreviations
+//
+// 1. Duplicate behavior to the data-* attribute for our tooltip plugin
+// 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.
+// 3. Add explicit cursor to indicate changed behavior.
+// 4. Remove the bottom border in Firefox 39-.
+// 5. Prevent the text-decoration to be skipped.
+
+// abbr[title],
+// abbr[data-original-title] { // 1
+//   text-decoration: underline; // 2
+//   text-decoration: underline dotted; // 2
+//   cursor: help; // 3
+//   border-bottom: 0; // 4
+//   text-decoration-skip-ink: none; // 5
+// }
+
+// address {
+//   margin-bottom: 1rem;
+//   font-style: normal;
+//   line-height: inherit;
+// }
+
+// ol,
+// ul,
+// dl {
+//   margin-top: 0;
+//   margin-bottom: 1rem;
+// }
+
+// ol ol,
+// ul ul,
+// ol ul,
+// ul ol {
+//   margin-bottom: 0;
+// }
+
+// dt {
+//   font-weight: $dt-font-weight;
+// }
+
+// dd {
+//   margin-bottom: .5rem;
+//   margin-left: 0; // Undo browser default
+// }
+
+// blockquote {
+//   margin: 0 0 1rem;
+// }
+
+// b,
+// strong {
+//   font-weight: $font-weight-bolder; // Add the correct font weight in Chrome, Edge, and Safari
+// }
+
+// small {
+//   @include font-size(80%); // Add the correct font size in all browsers
+// }
+
+//
+// Prevent `sub` and `sup` elements from affecting the line height in
+// all browsers.
+//
+
+// sub,
+// sup {
+//   position: relative;
+//   @include font-size(75%);
+//   line-height: 0;
+//   vertical-align: baseline;
+// }
+
+// sub { bottom: -.25em; }
+// sup { top: -.5em; }
+
+//
+// Links
+//
+
+a {
+  color: $link-color;
+  // text-decoration: $link-decoration;
+  background-color: transparent; // Remove the gray background on active links in IE 10.
+
+  @include hover() {
+    color: $link-hover-color;
+    // text-decoration: $link-hover-decoration;
+  }
+}
+
+// And undo these styles for placeholder links/named anchors (without href).
+// It would be more straightforward to just use a[href] in previous block, but that
+// causes specificity issues in many other styles that are too complex to fix.
+// See https://github.com/twbs/bootstrap/issues/19402
+
+// a:not([href]) {
+//   color: inherit;
+//   text-decoration: none;
+
+//   @include hover() {
+//     color: inherit;
+//     text-decoration: none;
+//   }
+// }
+
+//
+// Code
+//
+
+// pre,
+// code,
+// kbd,
+// samp {
+//   font-family: $font-family-monospace;
+//   @include font-size(1em); // Correct the odd `em` font sizing in all browsers.
+// }
+
+// pre {
+//   // Remove browser default top margin
+//   margin-top: 0;
+//   // Reset browser default of `1em` to use `rem`s
+//   margin-bottom: 1rem;
+//   // Don't allow content to break outside
+//   overflow: auto;
+// }
+
+//
+// Figures
+//
+
+// figure {
+//   // Apply a consistent margin strategy (matches our type styles).
+//   margin: 0 0 1rem;
+// }
+
+//
+// Images and content
+//
+
+// img {
+//   vertical-align: middle;
+//   border-style: none; // Remove the border on images inside links in IE 10-.
+// }
+
+// svg {
+//   // Workaround for the SVG overflow bug in IE10/11 is still required.
+//   // See https://github.com/twbs/bootstrap/issues/26878
+//   overflow: hidden;
+//   vertical-align: middle;
+// }
+
+//
+// Tables
+//
+
+// table {
+//   border-collapse: collapse; // Prevent double borders
+// }
+
+// caption {
+//   padding-top: $table-cell-padding;
+//   padding-bottom: $table-cell-padding;
+//   color: $table-caption-color;
+//   text-align: left;
+//   caption-side: bottom;
+// }
+
+// th {
+//   // Matches default `<td>` alignment by inheriting from the `<body>`, or the
+//   // closest parent with a set `text-align`.
+//   text-align: inherit;
+// }
+
+//
+// Forms
+//
+
+// label {
+//   // Allow labels to use `margin` for spacing.
+//   display: inline-block;
+//   margin-bottom: $label-margin-bottom;
+// }
+
+// Remove the default `border-radius` that macOS Chrome adds.
+//
+// Details at https://github.com/twbs/bootstrap/issues/24093
+// button {
+//   // stylelint-disable-next-line property-blacklist
+//   border-radius: 0;
+// }
+
+// Work around a Firefox/IE bug where the transparent `button` background
+// results in a loss of the default `button` focus styles.
+//
+// Credit: https://github.com/suitcss/base/
+// button:focus {
+//   outline: 1px dotted;
+//   outline: 5px auto -webkit-focus-ring-color;
+// }
+
+// input,
+// button,
+// select,
+// optgroup,
+// textarea {
+//   margin: 0; // Remove the margin in Firefox and Safari
+//   font-family: inherit;
+//   @include font-size(inherit);
+//   line-height: inherit;
+// }
+
+// button,
+// input {
+//   overflow: visible; // Show the overflow in Edge
+// }
+
+// button,
+// select {
+//   text-transform: none; // Remove the inheritance of text transform in Firefox
+// }
+
+// Remove the inheritance of word-wrap in Safari.
+//
+// Details at https://github.com/twbs/bootstrap/issues/24990
+// select {
+//   word-wrap: normal;
+// }
+
+// 1. Prevent a WebKit bug where (2) destroys native `audio` and `video`
+//    controls in Android 4.
+// 2. Correct the inability to style clickable types in iOS and Safari.
+// button,
+// [type="button"], // 1
+// [type="reset"],
+// [type="submit"] {
+//   -webkit-appearance: button; // 2
+// }
+
+// Opinionated: add "hand" cursor to non-disabled button elements.
+// @if $enable-pointer-cursor-for-buttons {
+//   button,
+//   [type="button"],
+//   [type="reset"],
+//   [type="submit"] {
+//     &:not(:disabled) {
+//       cursor: pointer;
+//     }
+//   }
+// }
+
+// Remove inner border and padding from Firefox, but don't restore the outline like Normalize.
+// button::-moz-focus-inner,
+// [type="button"]::-moz-focus-inner,
+// [type="reset"]::-moz-focus-inner,
+// [type="submit"]::-moz-focus-inner {
+//   padding: 0;
+//   border-style: none;
+// }
+
+// input[type="radio"],
+// input[type="checkbox"] {
+//   box-sizing: border-box; // 1. Add the correct box sizing in IE 10-
+//   padding: 0; // 2. Remove the padding in IE 10-
+// }
+
+// input[type="date"],
+// input[type="time"],
+// input[type="datetime-local"],
+// input[type="month"] {
+//   // Remove the default appearance of temporal inputs to avoid a Mobile Safari
+//   // bug where setting a custom line-height prevents text from being vertically
+//   // centered within the input.
+//   // See https://bugs.webkit.org/show_bug.cgi?id=139848
+//   // and https://github.com/twbs/bootstrap/issues/11266
+//   -webkit-appearance: listbox;
+// }
+
+// textarea {
+//   overflow: auto; // Remove the default vertical scrollbar in IE.
+//   // Textareas should really only resize vertically so they don't break their (horizontal) containers.
+//   resize: vertical;
+// }
+
+// fieldset {
+//   // Browsers set a default `min-width: min-content;` on fieldsets,
+//   // unlike e.g. `<div>`s, which have `min-width: 0;` by default.
+//   // So we reset that to ensure fieldsets behave more like a standard block element.
+//   // See https://github.com/twbs/bootstrap/issues/12359
+//   // and https://html.spec.whatwg.org/multipage/#the-fieldset-and-legend-elements
+//   min-width: 0;
+//   // Reset the default outline behavior of fieldsets so they don't affect page layout.
+//   padding: 0;
+//   margin: 0;
+//   border: 0;
+// }
+
+// 1. Correct the text wrapping in Edge and IE.
+// 2. Correct the color inheritance from `fieldset` elements in IE.
+// legend {
+//   display: block;
+//   width: 100%;
+//   max-width: 100%; // 1
+//   padding: 0;
+//   margin-bottom: .5rem;
+//   @include font-size(1.5rem);
+//   line-height: inherit;
+//   color: inherit; // 2
+//   white-space: normal; // 1
+// }
+
+// progress {
+//   vertical-align: baseline; // Add the correct vertical alignment in Chrome, Firefox, and Opera.
+// }
+
+// Correct the cursor style of increment and decrement buttons in Chrome.
+// [type="number"]::-webkit-inner-spin-button,
+// [type="number"]::-webkit-outer-spin-button {
+//   height: auto;
+// }
+
+// [type="search"] {
+//   // This overrides the extra rounded corners on search inputs in iOS so that our
+//   // `.form-control` class can properly style them. Note that this cannot simply
+//   // be added to `.form-control` as it's not specific enough. For details, see
+//   // https://github.com/twbs/bootstrap/issues/11586.
+//   outline-offset: -2px; // 2. Correct the outline style in Safari.
+//   -webkit-appearance: none;
+// }
+
+//
+// Remove the inner padding in Chrome and Safari on macOS.
+//
+
+// [type="search"]::-webkit-search-decoration {
+//   -webkit-appearance: none;
+// }
+
+//
+// 1. Correct the inability to style clickable types in iOS and Safari.
+// 2. Change font properties to `inherit` in Safari.
+//
+
+// ::-webkit-file-upload-button {
+//   font: inherit; // 2
+//   -webkit-appearance: button; // 1
+// }
+
+//
+// Correct element displays
+//
+
+// output {
+//   display: inline-block;
+// }
+
+// summary {
+//   display: list-item; // Add the correct display in all browsers
+//   cursor: pointer;
+// }
+
+// template {
+//   display: none; // Add the correct display in IE
+// }
+
+// Always hide an element with the `hidden` HTML attribute (from PureCSS).
+// Needed for proper display in IE 10-.
+// [hidden] {
+//   display: none !important;
+// }

+ 58 - 0
src/client/styles/scss/theme/_reboot-bootstrap-theme-colors.scss

@@ -0,0 +1,58 @@
+@each $color, $value in $theme-colors {
+  @include bg-variant('.bg-#{$color}', $value);
+}
+
+@each $color, $value in $theme-colors {
+  .btn-#{$color} {
+    @include button-variant($value, $value);
+  }
+}
+
+@each $color, $value in $theme-colors {
+  .btn-outline-#{$color} {
+    @include button-outline-variant($value);
+  }
+}
+
+@each $theme-color, $color in $theme-colors {
+  .custom-checkbox-#{$theme-color} {
+    .custom-control-label::before {
+      border-color: $input-border-color;
+      transition: 0.3s ease-in-out;
+    }
+    .custom-control-input:checked + .custom-control-label::before {
+      background-color: $color;
+      border-color: $color;
+    }
+    .custom-control-input:checked + .custom-control-label::after {
+      color: $bgcolor-global;
+    }
+    .custom-control-input:not(:disabled):active ~ .custom-control-label::before {
+      color: $bgcolor-global;
+      background-color: $color;
+      border-color: $color;
+    }
+    .custom-control-input:focus:not(:checked) ~ .custom-control-label::before {
+      color: $bgcolor-global;
+      background-color: $bgcolor-global;
+      border-color: $input-focus-border-color;
+    }
+  }
+}
+
+@each $theme-color, $color in $theme-colors {
+  .alert.alert-#{$theme-color} {
+    color: $bgcolor-global;
+    background: $color;
+    border: none;
+
+    a:not(.btn) {
+      color: $bgcolor-global;
+
+      &:hover,
+      &:focus {
+        color: lighten($color, 30%);
+      }
+    }
+  }
+}

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

@@ -1,8 +0,0 @@
-// import colors
-@import '../../agile-admin/inverse/colors/default-dark';
-
-// apply agile-admin theme
-@import '../../agile-admin/inverse/style';
-
-// override
-@import 'override-agileadmin';

+ 58 - 45
src/client/styles/scss/theme/default.scss

@@ -1,63 +1,76 @@
 @import '../variables';
+@import '../override-bootstrap-variables';
 
-// == Define colors
+// == Define Bootstrap theme colors
 //
 
 // colors for overriding bootstrap $theme-colors
-$primary: #112744;
-$secondary: #ced4da;
+// $secondary: #;
 // $info: #;
 // $success: #;
 // $warning: #;
 // $danger: #;
-$light: #f0f0f0;
+// $light: #;
 // $dark: #;
 
-// Background colors
-$bgcolor-global: white;
-$bgcolor-navbar: #334455;
-$bgcolor-inline-code: #f9f2f4;
-$bgcolor-card: #f5f5f5;
-
-// Font colors
-$color-global: #333333;
-$color-header: #2b2b2b;
-$color-link: lighten($primary, 20%);
-$color-link-hover: lighten($color-link, 20%);
-$color-link-wiki: lighten($primary, 20%);
-$color-link-wiki-hover: lighten($color-link-wiki, 20%);
-$color-inline-code: #c7254e;
-
-// Logo colors
-$fillcolor-logo-mark: lighten(desaturate($bgcolor-navbar, 10%), 15%);
-
-// Border colors
-$border: #f0f0f0;
-$navbar-border: #ccc;
-$active-navbar-border: lighten($navbar-border, 10%);
-
-// override bootstrap variables
-$link-color: $color-link;
-$link-hover-color: $color-link-hover;
-
-// light mode colors
-@media (prefers-color-scheme: no-preference),
-  (prefers-color-scheme: light) {}
-
-// dark mode colors
-@media (prefers-color-scheme: dark) {}
-
-//== Apply
+//== Light Mode
 //
-@import 'apply-colors';
+html:not([dark]) {
+  $primary: #112744;
 
-// apply for no-preference or light mode
-@media (prefers-color-scheme: no-preference),
-  (prefers-color-scheme: light) {
+  // Background colors
+  $bgcolor-global: white;
+  $bgcolor-navbar: #334455;
+  $bgcolor-inline-code: #f9f2f4;
+  $bgcolor-card: #f5f5f5;
+
+  // Font colors
+  $color-global: #333333;
+  // $color-header: #2b2b2b;
+  $color-link: lighten($primary, 20%);
+  $color-link-hover: lighten($color-link, 20%);
+  $color-link-wiki: lighten($primary, 20%);
+  $color-link-wiki-hover: lighten($color-link-wiki, 20%);
+  $color-inline-code: #c7254e;
+
+  // Logo colors
+  $fillcolor-logo-mark: lighten(desaturate($bgcolor-navbar, 10%), 15%);
+
+  // Border colors
+  $border-color-theme: #ccc; // former: `$navbar-border: #ccc;`
+
+  @import 'apply-colors';
   @import 'apply-colors-light';
 }
 
-// apply for dark mode
-@media (prefers-color-scheme: dark) {
+//== Dark Mode
+//
+html[dark] {
+  $primary: #d65a31;
+
+  $basecolor: #222831;
+
+  // Background colors
+  $bgcolor-global: $basecolor;
+  $bgcolor-navbar: #151515;
+  $bgcolor-inline-code: darken($basecolor, 5%);
+  $bgcolor-card: darken($basecolor, 5%);
+
+  // Font colors
+  $color-global: #eeeeee;
+  // $color-header: desaturate($primary, 20%);
+  $color-link: $primary;
+  $color-link-hover: lighten($color-link, 10%);
+  $color-link-wiki: lighten($basecolor, 50%);
+  $color-link-wiki-hover: darken($color-link-wiki, 5%);
+  $color-inline-code: #c7254e;
+
+  // Logo colors
+  $fillcolor-logo-mark: #444;
+
+  // Border colors
+  $border-color-theme: black; // former: `$navbar-border: #ccc;`
+
+  @import 'apply-colors';
   @import 'apply-colors-dark';
 }

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

@@ -96,13 +96,13 @@ module.exports = (crowi) => {
   const validator = {
     themeAssetPath: [
       query('themeName').isString().isIn([
-        'default', 'nature', 'mono-blue', 'wood', 'island', 'christmas', 'antarctic', 'default-dark', 'future', 'blue-night', 'halloween', 'spring',
+        'default', 'nature', 'mono-blue', 'wood', 'island', 'christmas', 'antarctic', 'future', 'blue-night', 'halloween', 'spring',
       ]),
     ],
     layoutTheme: [
       body('layoutType').isString().isIn(['growi', 'kibela', 'crowi']),
       body('themeType').isString().isIn([
-        'default', 'nature', 'mono-blue', 'wood', 'island', 'christmas', 'antarctic', 'default-dark', 'future', 'blue-night', 'halloween', 'spring',
+        'default', 'nature', 'mono-blue', 'wood', 'island', 'christmas', 'antarctic', 'future', 'blue-night', 'halloween', 'spring',
       ]),
     ],
     behavior: [

+ 48 - 48
src/server/routes/apiv3/security-setting.js

@@ -12,17 +12,17 @@ const removeNullPropertyFromObject = require('../../../lib/util/removeNullProper
 
 const validator = {
   generalSetting: [
-    body('restrictGuestMode').isString().isIn([
+    body('restrictGuestMode').if(value => value != null).isString().isIn([
       'Deny', 'Readonly',
     ]),
-    body('pageCompleteDeletionAuthority').isString().isIn([
+    body('pageCompleteDeletionAuthority').if(value => value != null).isString().isIn([
       'anyOne', 'adminOnly', 'adminAndAuthor',
     ]),
-    body('hideRestrictedByOwner').if((value, { req }) => req.body.hideRestrictedByOwner).isBoolean(),
-    body('hideRestrictedByGroup').if((value, { req }) => req.body.hideRestrictedByGroup).isBoolean(),
+    body('hideRestrictedByOwner').if(value => value != null).isBoolean(),
+    body('hideRestrictedByGroup').if(value => value != null).isBoolean(),
   ],
   authenticationSetting: [
-    body('isEnabled').if((value, { req }) => req.body.isEnabled).isBoolean(),
+    body('isEnabled').if(value => value != null).isBoolean(),
     body('authId').isString().isIn([
       'local', 'ldap', 'saml', 'oidc', 'basic', 'google', 'github', 'twitter',
     ]),
@@ -31,65 +31,65 @@ const validator = {
     body('registrationMode').isString().isIn([
       'Open', 'Restricted', 'Closed',
     ]),
-    body('registrationWhiteList').if((value, { req }) => req.body.registrationWhiteList).isArray().customSanitizer((value, { req }) => {
+    body('registrationWhiteList').if(value => value != null).isArray().customSanitizer((value, { req }) => {
       return value.filter(email => email !== '');
     }),
   ],
   ldapAuth: [
-    body('serverUrl').if((value, { req }) => req.body.serverUrl).isString(),
-    body('isUserBind').if((value, { req }) => req.body.isUserBind).isBoolean(),
-    body('ldapBindDN').if((value, { req }) => req.body.ldapBindDN).isString(),
-    body('ldapBindDNPassword').if((value, { req }) => req.body.ldapBindDNPassword).isString(),
-    body('ldapSearchFilter').if((value, { req }) => req.body.ldapSearchFilter).isString(),
-    body('ldapAttrMapUsername').if((value, { req }) => req.body.ldapAttrMapUsername).isString(),
-    body('isSameUsernameTreatedAsIdenticalUser').if((value, { req }) => req.body.isSameUsernameTreatedAsIdenticalUser).isBoolean(),
-    body('ldapAttrMapMail').if((value, { req }) => req.body.ldapAttrMapMail).isString(),
-    body('ldapAttrMapName').if((value, { req }) => req.body.ldapAttrMapName).isString(),
-    body('ldapGroupSearchBase').if((value, { req }) => req.body.ldapGroupSearchBase).isString(),
-    body('ldapGroupSearchFilter').if((value, { req }) => req.body.ldapGroupSearchFilter).isString(),
-    body('ldapGroupDnProperty').if((value, { req }) => req.body.ldapGroupDnProperty).isString(),
+    body('serverUrl').if(value => value != null).isString(),
+    body('isUserBind').if(value => value != null).isBoolean(),
+    body('ldapBindDN').if(value => value != null).isString(),
+    body('ldapBindDNPassword').if(value => value != null).isString(),
+    body('ldapSearchFilter').if(value => value != null).isString(),
+    body('ldapAttrMapUsername').if(value => value != null).isString(),
+    body('isSameUsernameTreatedAsIdenticalUser').if(value => value != null).isBoolean(),
+    body('ldapAttrMapMail').if(value => value != null).isString(),
+    body('ldapAttrMapName').if(value => value != null).isString(),
+    body('ldapGroupSearchBase').if(value => value != null).isString(),
+    body('ldapGroupSearchFilter').if(value => value != null).isString(),
+    body('ldapGroupDnProperty').if(value => value != null).isString(),
   ],
   samlAuth: [
-    body('entryPoint').if((value, { req }) => req.body.samlEntryPoint).isString(),
-    body('issuer').if((value, { req }) => req.body.samlIssuer).isString(),
-    body('cert').if((value, { req }) => req.body.samlCert).isString(),
-    body('attrMapId').if((value, { req }) => req.body.samlAttrMapId).isString(),
-    body('attrMapUsername').if((value, { req }) => req.body.samlAttrMapUsername).isString(),
-    body('attrMapMail').if((value, { req }) => req.body.samlAttrMapMail).isString(),
-    body('attrMapFirstName').if((value, { req }) => req.body.samlAttrMapFirstName).isString(),
-    body('attrMapLastName').if((value, { req }) => req.body.samlAttrMapLastName).isString(),
-    body('isSameUsernameTreatedAsIdenticalUser').if((value, { req }) => req.body.isSameUsernameTreatedAsIdenticalUser).isBoolean(),
-    body('isSameEmailTreatedAsIdenticalUser').if((value, { req }) => req.body.isSameEmailTreatedAsIdenticalUser).isBoolean(),
-    body('ABLCRule').if((value, { req }) => req.body.samlABLCRule).isString(),
+    body('entryPoint').if(value => value != null).isString(),
+    body('issuer').if(value => value != null).isString(),
+    body('cert').if(value => value != null).isString(),
+    body('attrMapId').if(value => value != null).isString(),
+    body('attrMapUsername').if(value => value != null).isString(),
+    body('attrMapMail').if(value => value != null).isString(),
+    body('attrMapFirstName').if(value => value != null).isString(),
+    body('attrMapLastName').if(value => value != null).isString(),
+    body('isSameUsernameTreatedAsIdenticalUser').if(value => value != null).isBoolean(),
+    body('isSameEmailTreatedAsIdenticalUser').if(value => value != null).isBoolean(),
+    body('ABLCRule').if(value => value != null).isString(),
   ],
   oidcAuth: [
-    body('oidcProviderName').if((value, { req }) => req.body.oidcProviderName).isString(),
-    body('oidcIssuerHost').if((value, { req }) => req.body.oidcIssuerHost).isString(),
-    body('oidcClientId').if((value, { req }) => req.body.oidcClientId).isString(),
-    body('oidcClientSecret').if((value, { req }) => req.body.oidcClientSecret).isString(),
-    body('oidcAttrMapId').if((value, { req }) => req.body.oidcAttrMapId).isString(),
-    body('oidcAttrMapUserName').if((value, { req }) => req.body.oidcAttrMapUserName).isString(),
-    body('oidcAttrMapEmail').if((value, { req }) => req.body.oidcAttrMapEmail).isString(),
-    body('isSameUsernameTreatedAsIdenticalUser').if((value, { req }) => req.body.isSameUsernameTreatedAsIdenticalUser).isBoolean(),
-    body('isSameEmailTreatedAsIdenticalUser').if((value, { req }) => req.body.isSameEmailTreatedAsIdenticalUser).isBoolean(),
+    body('oidcProviderName').if(value => value != null).isString(),
+    body('oidcIssuerHost').if(value => value != null).isString(),
+    body('oidcClientId').if(value => value != null).isString(),
+    body('oidcClientSecret').if(value => value != null).isString(),
+    body('oidcAttrMapId').if(value => value != null).isString(),
+    body('oidcAttrMapUserName').if(value => value != null).isString(),
+    body('oidcAttrMapEmail').if(value => value != null).isString(),
+    body('isSameUsernameTreatedAsIdenticalUser').if(value => value != null).isBoolean(),
+    body('isSameEmailTreatedAsIdenticalUser').if(value => value != null).isBoolean(),
   ],
   basicAuth: [
-    body('isSameUsernameTreatedAsIdenticalUser').if((value, { req }) => req.body.isSameUsernameTreatedAsIdenticalUser).isBoolean(),
+    body('isSameUsernameTreatedAsIdenticalUser').if(value => value != null).isBoolean(),
   ],
   googleOAuth: [
-    body('googleClientId').if((value, { req }) => req.body.googleClientId).isString(),
-    body('googleClientSecret').if((value, { req }) => req.body.googleClientSecret).isString(),
-    body('isSameUsernameTreatedAsIdenticalUser').if((value, { req }) => req.body.isSameUsernameTreatedAsIdenticalUser).isBoolean(),
+    body('googleClientId').if(value => value != null).isString(),
+    body('googleClientSecret').if(value => value != null).isString(),
+    body('isSameUsernameTreatedAsIdenticalUser').if(value => value != null).isBoolean(),
   ],
   githubOAuth: [
-    body('githubClientId').if((value, { req }) => req.body.githubClientId).isString(),
-    body('githubClientSecret').if((value, { req }) => req.body.githubClientSecret).isString(),
-    body('isSameUsernameTreatedAsIdenticalUser').if((value, { req }) => req.body.isSameUsernameTreatedAsIdenticalUser).isBoolean(),
+    body('githubClientId').if(value => value != null).isString(),
+    body('githubClientSecret').if(value => value != null).isString(),
+    body('isSameUsernameTreatedAsIdenticalUser').if(value => value != null).isBoolean(),
   ],
   twitterOAuth: [
-    body('twitterConsumerKey').if((value, { req }) => req.body.twitterConsumerKey).isString(),
-    body('twitterConsumerSecret').if((value, { req }) => req.body.twitterConsumerSecret).isString(),
-    body('isSameUsernameTreatedAsIdenticalUser').if((value, { req }) => req.body.isSameUsernameTreatedAsIdenticalUser).isBoolean(),
+    body('twitterConsumerKey').if(value => value != null).isString(),
+    body('twitterConsumerSecret').if(value => value != null).isString(),
+    body('isSameUsernameTreatedAsIdenticalUser').if(value => value != null).isBoolean(),
   ],
 };
 

+ 33 - 24
src/server/routes/login-passport.js

@@ -13,7 +13,7 @@ module.exports = function(crowi, app) {
    * @param {*} req
    * @param {*} res
    */
-  const loginSuccess = (req, res, user) => {
+  const loginSuccessHandler = (req, res, user) => {
     // update lastLoginAt
     user.updateLastLoginAt(new Date(), (err, userData) => {
       if (err) {
@@ -33,11 +33,20 @@ module.exports = function(crowi, app) {
    * @param {*} req
    * @param {*} res
    */
-  const loginFailure = (req, res, message) => {
+  const loginFailureHandler = (req, res, message) => {
     req.flash('errorMessage', message || 'Sign in failure.');
     return res.redirect('/login');
   };
 
+  /**
+   * middleware for login failure
+   * @param {*} req
+   * @param {*} res
+   */
+  const loginFailure = (req, res) => {
+    return loginFailureHandler(req, res, 'Sign in failure.');
+  };
+
   /**
    * return true(valid) or false(invalid)
    *
@@ -117,7 +126,7 @@ module.exports = function(crowi, app) {
     // login
     await req.logIn(user, (err) => {
       if (err) { return next(err) }
-      return loginSuccess(req, res, user);
+      return loginSuccessHandler(req, res, user);
     });
   };
 
@@ -209,7 +218,7 @@ module.exports = function(crowi, app) {
       req.logIn(user, (err) => {
         if (err) { return next() }
 
-        return loginSuccess(req, res, user);
+        return loginSuccessHandler(req, res, user);
       });
     })(req, res, next);
   };
@@ -235,7 +244,7 @@ module.exports = function(crowi, app) {
       response = await promisifiedPassportAuthentication(strategyName, req, res);
     }
     catch (err) {
-      return loginFailure(req, res);
+      return loginFailureHandler(req, res);
     }
 
     const userInfo = {
@@ -255,7 +264,7 @@ module.exports = function(crowi, app) {
 
     const externalAccount = await getOrCreateUser(req, res, userInfo, providerId);
     if (!externalAccount) {
-      return loginFailure(req, res);
+      return loginFailureHandler(req, res);
     }
 
     const user = await externalAccount.getPopulatedUser();
@@ -263,7 +272,7 @@ module.exports = function(crowi, app) {
     // login
     req.logIn(user, (err) => {
       if (err) { return next(err) }
-      return loginSuccess(req, res, user);
+      return loginSuccessHandler(req, res, user);
     });
   };
 
@@ -286,7 +295,7 @@ module.exports = function(crowi, app) {
       response = await promisifiedPassportAuthentication(strategyName, req, res);
     }
     catch (err) {
-      return loginFailure(req, res);
+      return loginFailureHandler(req, res);
     }
 
     const userInfo = {
@@ -297,7 +306,7 @@ module.exports = function(crowi, app) {
 
     const externalAccount = await getOrCreateUser(req, res, userInfo, providerId);
     if (!externalAccount) {
-      return loginFailure(req, res);
+      return loginFailureHandler(req, res);
     }
 
     const user = await externalAccount.getPopulatedUser();
@@ -305,7 +314,7 @@ module.exports = function(crowi, app) {
     // login
     req.logIn(user, (err) => {
       if (err) { return next(err) }
-      return loginSuccess(req, res, user);
+      return loginSuccessHandler(req, res, user);
     });
   };
 
@@ -328,7 +337,7 @@ module.exports = function(crowi, app) {
       response = await promisifiedPassportAuthentication(strategyName, req, res);
     }
     catch (err) {
-      return loginFailure(req, res);
+      return loginFailureHandler(req, res);
     }
 
     const userInfo = {
@@ -339,7 +348,7 @@ module.exports = function(crowi, app) {
 
     const externalAccount = await getOrCreateUser(req, res, userInfo, providerId);
     if (!externalAccount) {
-      return loginFailure(req, res);
+      return loginFailureHandler(req, res);
     }
 
     const user = await externalAccount.getPopulatedUser();
@@ -347,7 +356,7 @@ module.exports = function(crowi, app) {
     // login
     req.logIn(user, (err) => {
       if (err) { return next(err) }
-      return loginSuccess(req, res, user);
+      return loginSuccessHandler(req, res, user);
     });
   };
 
@@ -375,7 +384,7 @@ module.exports = function(crowi, app) {
     }
     catch (err) {
       debug(err);
-      return loginFailure(req, res);
+      return loginFailureHandler(req, res);
     }
 
     const userInfo = {
@@ -388,14 +397,14 @@ module.exports = function(crowi, app) {
 
     const externalAccount = await getOrCreateUser(req, res, userInfo, providerId);
     if (!externalAccount) {
-      return loginFailure(req, res);
+      return loginFailureHandler(req, res);
     }
 
     // login
     const user = await externalAccount.getPopulatedUser();
     req.logIn(user, (err) => {
       if (err) { return next(err) }
-      return loginSuccess(req, res, user);
+      return loginSuccessHandler(req, res, user);
     });
   };
 
@@ -423,7 +432,7 @@ module.exports = function(crowi, app) {
       response = await promisifiedPassportAuthentication(strategyName, req, res);
     }
     catch (err) {
-      return loginFailure(req, res);
+      return loginFailureHandler(req, res);
     }
 
     const userInfo = {
@@ -441,23 +450,23 @@ module.exports = function(crowi, app) {
 
     const externalAccount = await getOrCreateUser(req, res, userInfo, providerId);
     if (!externalAccount) {
-      return loginFailure(req, res);
+      return loginFailureHandler(req, res);
     }
 
     const user = await externalAccount.getPopulatedUser();
 
     // Attribute-based Login Control
     if (!crowi.passportService.verifySAMLResponseByABLCRule(response)) {
-      return loginFailure(req, res, 'Sign in failure due to insufficient privileges.');
+      return loginFailureHandler(req, res, 'Sign in failure due to insufficient privileges.');
     }
 
     // login
     req.logIn(user, (err) => {
       if (err != null) {
         logger.error(err);
-        return loginFailure(req, res);
+        return loginFailureHandler(req, res);
       }
-      return loginSuccess(req, res, user);
+      return loginSuccessHandler(req, res, user);
     });
   };
 
@@ -482,7 +491,7 @@ module.exports = function(crowi, app) {
       userId = await promisifiedPassportAuthentication(strategyName, req, res);
     }
     catch (err) {
-      return loginFailure(req, res);
+      return loginFailureHandler(req, res);
     }
 
     const userInfo = {
@@ -493,13 +502,13 @@ module.exports = function(crowi, app) {
 
     const externalAccount = await getOrCreateUser(req, res, userInfo, providerId);
     if (!externalAccount) {
-      return loginFailure(req, res);
+      return loginFailureHandler(req, res);
     }
 
     const user = await externalAccount.getPopulatedUser();
     await req.logIn(user, (err) => {
       if (err) { return next() }
-      return loginSuccess(req, res, user);
+      return loginSuccessHandler(req, res, user);
     });
   };
 

+ 5 - 0
src/server/service/passport.js

@@ -719,6 +719,11 @@ class PassportService {
     if (field === '<implicit>') {
       return attributes[term] != null;
     }
+
+    if (attributes[field] == null) {
+      return false;
+    }
+
     return attributes[field].includes(term);
   }
 

+ 3 - 5
src/server/views/admin/Users_reserve.html

@@ -3,11 +3,9 @@
 {% block html_title %}{{ customizeService.generateCustomTitle(t('User_Management')) }}{% endblock %}
 
 {% block content_header %}
-<div class="header-wrap">
-  <header id="page-header">
-    <h1 id="admin-title" class="title">{{ t('User_Management') }}</h1>
-  </header>
-</div>
+<header id="page-header">
+  <h1 id="admin-title" class="title">{{ t('User_Management') }}</h1>
+</header>
 {% endblock %}
 
 {% block content_main %}

+ 3 - 5
src/server/views/admin/app.html

@@ -6,11 +6,9 @@
 {% endblock %}
 
 {% block content_header %}
-<div class="header-wrap">
-  <header id="page-header">
-    <h1 id="admin-title" class="title">{{ t('App settings') }}</h1>
-  </header>
-</div>
+<header id="page-header">
+  <h1 id="admin-title" class="title">{{ t('App settings') }}</h1>
+</header>
 {% endblock %}
 
 {% block content_main %}

+ 3 - 5
src/server/views/admin/customize.html

@@ -13,11 +13,9 @@
 {% endblock %}
 
 {% block content_header %}
-<div class="header-wrap">
-  <header id="page-header">
-    <h1 id="admin-title" class="title">{{ t('Customize') }}</h1>
-  </header>
-</div>
+<header id="page-header">
+  <h1 id="admin-title" class="title">{{ t('Customize') }}</h1>
+</header>
 {% endblock %}
 
 {% block content_main %}

+ 3 - 5
src/server/views/admin/export.html

@@ -3,11 +3,9 @@
 {% block html_title %}{{ customizeService.generateCustomTitle(t('Export Archive Data')) }}{% endblock %}
 
 {% block content_header %}
-<div class="header-wrap">
-  <header id="page-header">
-    <h1 id="admin-title" class="title">{{ t('Export Archive Data') }}</h1>
-  </header>
-</div>
+<header id="page-header">
+  <h1 id="admin-title" class="title">{{ t('Export Archive Data') }}</h1>
+</header>
 {% endblock %}
 
 {% block content_main %}

+ 3 - 5
src/server/views/admin/external-accounts.html

@@ -3,11 +3,9 @@
 {% block html_title %}{{ customizeService.generateCustomTitle(t('external_account_management')) }}{% endblock %}
 
 {% block content_header %}
-<div class="header-wrap">
-  <header id="page-header">
-    <h1 id="admin-title" class="title">{{ t('User_Management') }} / {{ t('external_account_management') }}</h1>
-  </header>
-</div>
+<header id="page-header">
+  <h1 id="admin-title" class="title">{{ t('User_Management') }} / {{ t('external_account_management') }}</h1>
+</header>
 {% endblock %}
 
 {% block content_main %}

+ 3 - 5
src/server/views/admin/global-notification-detail.html

@@ -3,11 +3,9 @@
 {% block html_title %}{{ customizeService.generateCustomTitle(t('Notification settings')) }}{% endblock %}
 
 {% block content_header %}
-<div class="header-wrap">
-  <header id="page-header">
-    <h1 id="admin-title" class="title">{{ t('Notification settings') }}</h1>
-  </header>
-</div>
+<header id="page-header">
+  <h1 id="admin-title" class="title">{{ t('Notification settings') }}</h1>
+</header>
 {% endblock %}
 
 {% block content_main %}

+ 3 - 5
src/server/views/admin/importer.html

@@ -3,11 +3,9 @@
 {% block html_title %}{{ customizeService.generateCustomTitle(t('Import Data')) }}{% endblock %}
 
 {% block content_header %}
-<div class="header-wrap">
-  <header id="page-header">
-    <h1 id="admin-title" class="title">{{ t('Import Data') }}</h1>
-  </header>
-</div>
+<header id="page-header">
+  <h1 id="admin-title" class="title">{{ t('Import Data') }}</h1>
+</header>
 {% endblock %}
 
 {% block content_main %}

+ 3 - 5
src/server/views/admin/index.html

@@ -3,11 +3,9 @@
 {% block html_title %}{{ customizeService.generateCustomTitle(t('Management Wiki Home')) }}{% endblock %}
 
 {% block content_header %}
-<div class="header-wrap">
-  <header id="page-header">
-    <h1 id="admin-title" class="title"> {{ t('Management Wiki Home') }}</h1>
-  </header>
-</div>
+<header id="page-header">
+  <h1 id="admin-title" class="title"> {{ t('Management Wiki Home') }}</h1>
+</header>
 {% endblock %}
 
 {% block content_main %}

+ 3 - 5
src/server/views/admin/markdown.html

@@ -4,11 +4,9 @@
  · {{ path }}{% endblock %}
 
 {% block content_header %}
-<div class="header-wrap">
-  <header id="page-header">
-    <h1 id="admin-title" class="title">{{ t('Markdown Settings') }}</h1>
-  </header>
-</div>
+<header id="page-header">
+  <h1 id="admin-title" class="title">{{ t('Markdown Settings') }}</h1>
+</header>
 {% endblock %}
 
 {% block content_main %}

+ 3 - 5
src/server/views/admin/notification.html

@@ -3,11 +3,9 @@
 {% block html_title %}{{ customizeService.generateCustomTitle(t('Notification settings')) }}{% endblock %}
 
 {% block content_header %}
-<div class="header-wrap">
-  <header id="page-header">
-    <h1 id="admin-title" class="title">{{ t('Notification settings') }}</h1>
-  </header>
-</div>
+<header id="page-header">
+  <h1 id="admin-title" class="title">{{ t('Notification settings') }}</h1>
+</header>
 {% endblock %}
 
 {% block content_main %}

+ 3 - 5
src/server/views/admin/search.html

@@ -3,11 +3,9 @@
 {% block html_title %}{{ customizeService.generateCustomTitle(t('Full Text Search management')) }}{% endblock %}
 
 {% block content_header %}
-<div class="header-wrap">
-  <header id="page-header">
-    <h1 id="admin-title" class="title">{{ t('Full Text Search management') }}</h1>
-  </header>
-</div>
+<header id="page-header">
+  <h1 id="admin-title" class="title">{{ t('Full Text Search management') }}</h1>
+</header>
 {% endblock %}
 
 {% block content_main %}

+ 3 - 5
src/server/views/admin/security.html

@@ -3,11 +3,9 @@
 {% block html_title %}{{ customizeService.generateCustomTitle(t('security_settings')) }} · {% endblock %}
 
 {% block content_header %}
-<div class="header-wrap">
-  <header id="page-header">
-    <h1 id="admin-title" class="title">{{ t('security_settings') }}</h1>
-  </header>
-</div>
+<header id="page-header">
+  <h1 id="admin-title" class="title">{{ t('security_settings') }}</h1>
+</header>
 {% endblock %}
 
 {% block content_main %}

+ 3 - 5
src/server/views/admin/user-group-detail.html

@@ -3,11 +3,9 @@
 {% block html_title %}{{ customizeService.generateCustomTitle(t('UserGroup Management') + '/' + userGroup.name) | preventXss }}{% endblock %}
 
 {% block content_header %}
-<div class="header-wrap">
-  <header id="page-header">
-    <h1 id="admin-title" class="title">{{ t('UserGroup Management') + '/' + userGroup.name | preventXss }}</h1>
-  </header>
-</div>
+<header id="page-header">
+  <h1 id="admin-title" class="title">{{ t('UserGroup Management') + '/' + userGroup.name | preventXss }}</h1>
+</header>
 {% endblock %}
 
 {% block content_main %}

+ 3 - 5
src/server/views/admin/user-groups.html

@@ -3,11 +3,9 @@
 {% block html_title %}{{ customizeService.generateCustomTitle(t('UserGroup Management')) }}{% endblock %}
 
 {% block content_header %}
-<div class="header-wrap">
-  <header id="page-header">
-    <h1 id="admin-title" class="title">{{ t('UserGroup Management') }}</h1>
-  </header>
-</div>
+<header id="page-header">
+  <h1 id="admin-title" class="title">{{ t('UserGroup Management') }}</h1>
+</header>
 {% endblock %}
 
 {% block content_main %}

+ 3 - 5
src/server/views/admin/users.html

@@ -3,11 +3,9 @@
 {% block html_title %}{{ customizeService.generateCustomTitle(t('User_Management')) }}{% endblock %}
 
 {% block content_header %}
-<div class="header-wrap">
-  <header id="page-header">
-    <h1 id="admin-title" class="title">{{ t('User_Management') }}</h1>
-  </header>
-</div>
+<header id="page-header">
+  <h1 id="admin-title" class="title">{{ t('User_Management') }}</h1>
+</header>
 {% endblock %}
 
 {% block content_main %}

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

@@ -5,6 +5,7 @@
 {% block html_additional_headers %}
   {% parent %}
   {{ cdnScriptTag('highlight-addons') }}
+  {{ cdnScriptTag('drawio-viewer') }}
 {% endblock %}
 
 {% block layout_main %}

+ 8 - 11
src/server/views/layout-crowi/forbidden.html

@@ -5,19 +5,16 @@
   {% block content_header_before %}
   {% endblock %}
 
-  <div class="header-wrap">
-    <header id="page-header">
+  <header id="page-header">
+    <div>
       <div>
-        <div>
-          <h1 class="title" id="revision-path"></h1>
-          {% if page and not forbidden and not isTrashPage() %}
-            <div id="tag-label"></div>
-          {% endif %}
-        </div>
+        <h1 class="title" id="revision-path"></h1>
+        {% if page and not forbidden and not isTrashPage() %}
+          <div id="tag-label"></div>
+        {% endif %}
       </div>
-
-    </header>
-  </div>
+    </div>
+  </header>
 
   {% block content_header_after %}
   {% endblock %}

+ 8 - 11
src/server/views/layout-crowi/not_creatable.html

@@ -5,19 +5,16 @@
   {% block content_header_before %}
   {% endblock %}
 
-  <div class="header-wrap">
-    <header id="page-header">
+  <header id="page-header">
+    <div>
       <div>
-        <div>
-          <h1 class="title" id="revision-path"></h1>
-          {% if page and not forbidden and not isTrashPage() %}
-            <div id="tag-label"></div>
-          {% endif %}
-        </div>
+        <h1 class="title" id="revision-path"></h1>
+        {% if page and not forbidden and not isTrashPage() %}
+          <div id="tag-label"></div>
+        {% endif %}
       </div>
-
-    </header>
-  </div>
+    </div>
+  </header>
 
   {% block content_header_after %}
   {% endblock %}

+ 8 - 11
src/server/views/layout-crowi/not_found.html

@@ -5,19 +5,16 @@
   {% block content_header_before %}
   {% endblock %}
 
-  <div class="header-wrap">
-    <header id="page-header">
+  <header id="page-header">
+    <div>
       <div>
-        <div>
-          <h1 class="title" id="revision-path"></h1>
-          {% if not forbidden and not isTrashPage() %}
-            <div id="tag-label"></div>
-          {% endif %}
-        </div>
+        <h1 class="title" id="revision-path"></h1>
+        {% if not forbidden and not isTrashPage() %}
+          <div id="tag-label"></div>
+        {% endif %}
       </div>
-
-    </header>
-  </div>
+    </div>
+  </header>
 
   {% block content_header_after %}
   {% endblock %}

+ 10 - 12
src/server/views/layout-crowi/page.html

@@ -6,19 +6,17 @@
   {% block content_header_before %}
   {% endblock %}
 
-  <div class="header-wrap">
-    <header id="page-header">
-      <div class="d-flex align-items-center">
-        <div class="title-container">
-          <h1 class="title" id="revision-path"></h1>
-          {% if page and not forbidden and not isTrashPage() %}
-            <div id="tag-label"></div>
-          {% endif %}
-        </div>
-        {% include '../widget/header-buttons.html' %}
+  <header id="page-header">
+    <div class="d-flex align-items-center">
+      <div class="title-container">
+        <h1 class="title" id="revision-path"></h1>
+        {% if page and not forbidden and not isTrashPage() %}
+          <div id="tag-label"></div>
+        {% endif %}
       </div>
-    </header>
-  </div>
+      {% include '../widget/header-buttons.html' %}
+    </div>
+  </header>
 
   {% block content_header_after %}
   {% endblock %}

+ 11 - 13
src/server/views/layout-crowi/page_list.html

@@ -11,21 +11,19 @@
 {% block content_header_before %}
 {% endblock %}
 
-<div class="header-wrap">
-  <header id="page-header" class="{% if page %}has-page{% endif %}">
-
-    <div class="d-flex align-items-center">
-      <div class="title-container">
-        <h1 class="title" id="revision-path"></h1>
-        {% if page and not forbidden and not isTrashPage() %}
-          <div id="tag-label"></div>
-        {% endif %}
-      </div>
-      {% include '../widget/header-buttons.html' %}
+<header id="page-header" class="{% if page %}has-page{% endif %}">
+
+  <div class="d-flex align-items-center">
+    <div class="title-container">
+      <h1 class="title" id="revision-path"></h1>
+      {% if page and not forbidden and not isTrashPage() %}
+        <div id="tag-label"></div>
+      {% endif %}
     </div>
+    {% include '../widget/header-buttons.html' %}
+  </div>
 
-  </header>
-</div>
+</header>
 
 {% endblock %}
 

+ 1 - 0
src/server/views/layout-growi/base/layout.html

@@ -3,6 +3,7 @@
 {% block html_additional_headers %}
   {% parent %}
   {{ cdnScriptTag('highlight-addons') }}
+  {{ cdnScriptTag('drawio-viewer') }}
 {% endblock %}
 
 {% block layout_main %}

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