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

Merge branch 'dev/4.0.x' into imprv/nature-and-wood

akira-s 5 лет назад
Родитель
Сommit
212e490c09
46 измененных файлов с 808 добавлено и 870 удалено
  1. 1 1
      config/webpack.common.js
  2. 1 0
      package.json
  3. 2 3
      src/client/js/bootstrap.jsx
  4. 15 19
      src/client/js/components/Admin/Customize/CustomizeThemeOptions.jsx
  5. 11 10
      src/client/js/components/Admin/Customize/ThemeColorBox.jsx
  6. 13 17
      src/client/js/components/Admin/Security/SecuritySetting.jsx
  7. 3 3
      src/client/js/components/InstallerForm.jsx
  8. 316 0
      src/client/js/components/LoginForm.jsx
  9. 3 5
      src/client/js/components/Me/BasicInfoSettings.jsx
  10. 3 13
      src/client/js/components/Navbar/GrowiSubNavigation.jsx
  11. 2 10
      src/client/js/components/Navbar/GrowiSubNavigationForUserPage.jsx
  12. 42 15
      src/client/js/components/Navbar/SearchTop.jsx
  13. 25 37
      src/client/js/components/Sidebar.jsx
  14. 0 24
      src/client/js/installer.jsx
  15. 81 0
      src/client/js/nologin.jsx
  16. 23 1
      src/client/js/services/AppContainer.js
  17. 23 0
      src/client/js/services/NoLoginContainer.js
  18. 1 5
      src/client/styles/scss/_admin.scss
  19. 57 189
      src/client/styles/scss/_login.scss
  20. 0 3
      src/client/styles/scss/_me.scss
  21. 18 9
      src/client/styles/scss/_search.scss
  22. 1 1
      src/client/styles/scss/_sidebar.scss
  23. 0 42
      src/client/styles/scss/_subnav.scss
  24. 0 4
      src/client/styles/scss/_tag.scss
  25. 0 35
      src/client/styles/scss/_user.scss
  26. 8 1
      src/client/styles/scss/atoms/_buttons.scss
  27. 4 17
      src/client/styles/scss/theme/_apply-colors-dark.scss
  28. 4 14
      src/client/styles/scss/theme/_apply-colors-light.scss
  29. 8 2
      src/client/styles/scss/theme/default.scss
  30. 48 40
      src/client/styles/scss/theme/mono-blue.scss
  31. 12 11
      src/server/routes/index.js
  32. 9 8
      src/server/routes/login-passport.js
  33. 1 1
      src/server/views/installer.html
  34. 3 3
      src/server/views/invited.html
  35. 1 1
      src/server/views/layout-growi/base/layout.html
  36. 2 2
      src/server/views/layout-growi/user_page.html
  37. 27 22
      src/server/views/layout/layout.html
  38. 26 293
      src/server/views/login.html
  39. 1 1
      src/server/views/login/error.html
  40. 1 1
      src/server/views/tags.html
  41. 1 1
      src/server/views/widget/forbidden_content.html
  42. 3 3
      src/server/views/widget/not_creatable_content.html
  43. 1 1
      src/server/views/widget/not_found_tabs.html
  44. 1 1
      src/server/views/widget/page_content.html
  45. 1 1
      src/server/views/widget/page_tabs.html
  46. 5 0
      yarn.lock

+ 1 - 1
config/webpack.common.js

@@ -22,7 +22,7 @@ module.exports = (options) => {
     entry: Object.assign({
       'js/app':                       './src/client/js/app',
       'js/admin':                     './src/client/js/admin',
-      'js/installer':                 './src/client/js/installer',
+      'js/nologin':                   './src/client/js/nologin',
       'js/legacy':                    './src/client/js/legacy/crowi',
       'js/legacy-presentation':       './src/client/js/legacy/crowi-presentation',
       'js/plugin':                    './src/client/js/plugin',

+ 1 - 0
package.json

@@ -134,6 +134,7 @@
     "passport-local": "^1.0.0",
     "passport-saml": "^1.0.0",
     "passport-twitter": "^1.0.4",
+    "react-card-flip": "^1.0.10",
     "react-image-crop": "^8.3.0",
     "rimraf": "^3.0.0",
     "slack-node": "^0.1.8",

+ 2 - 3
src/client/js/bootstrap.jsx

@@ -3,7 +3,7 @@ import React from 'react';
 import loggerFactory from '@alias/logger';
 import Xss from '@commons/service/xss';
 
-import HeaderSearchBox from './components/HeaderSearchBox';
+import SearchTop from './components/Navbar/SearchTop';
 import NavbarToggler from './components/Navbar/NavbarToggler';
 import PersonalDropdown from './components/Navbar/PersonalDropdown';
 import Sidebar from './components/Sidebar';
@@ -42,8 +42,7 @@ appContainer.injectToWindow();
 const componentMappings = {
   'grw-navbar-toggler': <NavbarToggler />,
 
-  'search-top': <HeaderSearchBox />,
-  'search-sidebar': <HeaderSearchBox crowi={appContainer} />,
+  'grw-search-top': <SearchTop />,
   'personal-dropdown': <PersonalDropdown />,
 
   'create-page-button': <PageCreateButton />,

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

@@ -15,29 +15,31 @@ class CustomizeThemeOptions extends React.Component {
     const { t, adminCustomizeContainer } = this.props;
     const { currentLayout, currentTheme } = adminCustomizeContainer.state;
 
+    /* eslint-disable no-multi-spaces */
     const lightNDarkTheme = [{
-      name: 'default', bg: '#ffffff', topbar: '#334455', theme: '#112744',
+      name: 'default',    bg: '#ffffff', topbar: '#2a2929', sidebar: '#122c55', theme: '#209fd8',
     }, {
-      name: 'mono-blue', bg: '#F7FBFD', topbar: '#00587A', theme: '#00587A',
+      name: 'mono-blue',  bg: '#F7FBFD', topbar: '#2a2929', sidebar: '#00587A', theme: '#00587A',
     }];
 
     const uniqueTheme = [{
-      name: 'nature', bg: '#f9fff3', topbar: '#118050', theme: '#460039',
+      name: 'nature',     bg: '#f9fff3', topbar: '#2a2929', sidebar: '#118050', theme: '#460039',
     }, {
-      name: 'wood', bg: '#fffefb', topbar: '#aaa45f', theme: '#dddebf',
+      name: 'wood',       bg: '#fffefb', topbar: '#2a2929', sidebar: '#aaa45f', theme: '#dddebf',
     }, {
-      name: 'island', bg: '#8ecac0', topbar: '#0c2a44', theme: '#cef2ef',
+      name: 'island',     bg: '#8ecac0', topbar: '#2a2929', sidebar: '#0c2a44', theme: '#cef2ef',
     }, {
-      name: 'christmas', bg: '#fffefb', topbar: '#b3000c', theme: '#017e20',
+      name: 'christmas',  bg: '#fffefb', topbar: '#2a2929', sidebar: '#b3000c', theme: '#017e20',
     }, {
-      name: 'antarctic', bg: '#ffffff', topbar: '#000080', theme: '#99cccc',
+      name: 'antarctic',  bg: '#ffffff', topbar: '#2a2929', sidebar: '#000080', theme: '#99cccc',
     }, {
-      name: 'spring', bg: '#fff5ee', topbar: '#ff69b4', theme: '#ffb6c1',
+      name: 'spring',     bg: '#fff5ee', topbar: '#2a2929', sidebar: '#ff69b4', theme: '#ffb6c1',
     }, {
-      name: 'future', bg: '#16282D', topbar: '#011414', theme: '#04B4AE',
+      name: 'future',     bg: '#16282D', topbar: '#2a2929', sidebar: '#011414', theme: '#04B4AE',
     }, {
-      name: 'halloween', bg: '#030003', topbar: '#cc5d1f', theme: '#e9af2b',
+      name: 'halloween',  bg: '#030003', topbar: '#2a2929', sidebar: '#cc5d1f', theme: '#e9af2b',
     }];
+    /* eslint-enable no-multi-spaces */
 
     return (
       <div id="themeOptions" className={`${currentLayout === 'kibela' && 'disabled'}`}>
@@ -51,17 +53,14 @@ class CustomizeThemeOptions extends React.Component {
                   key={theme.name}
                   isSelected={currentTheme === theme.name}
                   onSelected={() => adminCustomizeContainer.switchThemeType(theme.name)}
-                  name={theme.name}
-                  bg={theme.bg}
-                  topbar={theme.topbar}
-                  theme={theme.theme}
+                  {...theme}
                 />
               );
             })}
           </div>
         </div>
         {/* Unique Theme */}
-        <div>
+        <div className="mt-3">
           <h3>{t('admin:customize_setting.theme_desc.unique')}</h3>
           <div className="d-flex flex-wrap">
             {uniqueTheme.map((theme) => {
@@ -70,10 +69,7 @@ class CustomizeThemeOptions extends React.Component {
                   key={theme.name}
                   isSelected={currentTheme === theme.name}
                   onSelected={() => adminCustomizeContainer.switchThemeType(theme.name)}
-                  name={theme.name}
-                  bg={theme.bg}
-                  topbar={theme.topbar}
-                  theme={theme.theme}
+                  {...theme}
                 />
               );
             })}

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

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

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

@@ -118,7 +118,7 @@ class SecuritySetting extends React.Component {
           <div className="col-md-3 text-md-right py-2">
             <strong>{t('security_setting.Guest Users Access')}</strong>
           </div>
-          <div className="col-md-6 ml-md-5">
+          <div className="col-md-9">
             <div className="dropdown">
               <button
                 className={`btn btn-outline-secondary dropdown-toggle text-right col-12
@@ -143,30 +143,26 @@ class SecuritySetting extends React.Component {
                 </a>
               </div>
             </div>
-          </div>
-        </div>
-        {adminGeneralSecurityContainer.isWikiModeForced && (
-        <div className="row mb-4">
-          <div className="offset-3 col-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={{
+            {adminGeneralSecurityContainer.isWikiModeForced && (
+              <p className="alert alert-warning mt-2 text-left offset-3 col-6">
+                <i className="icon-exclamation icon-fw">
+                </i><b>FIXED</b><br />
+                <b
+                  dangerouslySetInnerHTML={{
                     __html: t('security_setting.Fixed by env var',
-                    { forcewikimode: 'FORCE_WIKI_MODE', wikimode: adminGeneralSecurityContainer.state.wikiMode }),
-                    }}
-              />
-            </p>
+                      { forcewikimode: 'FORCE_WIKI_MODE', wikimode: adminGeneralSecurityContainer.state.wikiMode }),
+                  }}
+                />
+              </p>
+            )}
           </div>
         </div>
-          )}
 
         <div className="row mb-4">
           <div className="col-md-3 text-md-right mb-2">
             <strong>{t('security_setting.complete_deletion')}</strong>
           </div>
-          <div className="col-md-6 ml-md-5">
+          <div className="col-md-6">
             <div className="dropdown">
               <button
                 className="btn btn-outline-secondary dropdown-toggle text-right col-12 col-md-auto"

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

@@ -150,10 +150,10 @@ class InstallerForm extends React.Component {
             <input type="hidden" name="_csrf" value={this.props.csrf} />
 
             <div className="input-group mt-4 mb-3 d-flex justify-content-center">
-              <button type="submit" className="btn-fill btn btn-register px-0 py-2" id="register">
+              <button type="submit" className="btn-fill btn btn-register" id="register">
                 <div className="eff"></div>
-                <span className="btn-label p-3"><i className="icon-user-follow" /></span>
-                <span className="btn-label-text p-3">{ this.props.t('Create') }</span>
+                <span className="btn-label"><i className="icon-user-follow" /></span>
+                <span className="btn-label-text">{ this.props.t('Create') }</span>
               </button>
             </div>
 

+ 316 - 0
src/client/js/components/LoginForm.jsx

@@ -0,0 +1,316 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import ReactCardFlip from 'react-card-flip';
+
+import { withTranslation } from 'react-i18next';
+
+import { createSubscribedElement } from './UnstatedUtils';
+import NoLoginContainer from '../services/NoLoginContainer';
+
+class LoginForm extends React.Component {
+
+  constructor(props) {
+    super(props);
+
+    this.state = {
+      isRegistering: false,
+    };
+
+    this.switchForm = this.switchForm.bind(this);
+    this.handleLoginWithExternalAuth = this.handleLoginWithExternalAuth.bind(this);
+    this.renderLocalOrLdapLoginForm = this.renderLocalOrLdapLoginForm.bind(this);
+    this.renderExternalAuthLoginForm = this.renderExternalAuthLoginForm.bind(this);
+    this.renderExternalAuthInput = this.renderExternalAuthInput.bind(this);
+    this.renderRegisterForm = this.renderRegisterForm.bind(this);
+
+    const { hash } = window.location;
+    if (hash === '#register') {
+      this.state.isRegistering = true;
+    }
+  }
+
+  switchForm() {
+    this.setState({ isRegistering: !this.state.isRegistering });
+  }
+
+  handleLoginWithExternalAuth(e) {
+    const auth = e.currentTarget.id;
+    const csrf = this.props.noLoginContainer.csrfToken;
+    window.location.href = `/passport/${auth}?_csrf=${csrf}`;
+  }
+
+  renderLocalOrLdapLoginForm() {
+    const { t, noLoginContainer, isLdapStrategySetup } = this.props;
+
+    return (
+      <form role="form" action="/login" method="post">
+        <div className="input-group mb-3">
+          <div className="input-group-prepend">
+            <span className="input-group-text">
+              <i className="icon-user"></i>
+            </span>
+          </div>
+          <input type="text" className="form-control" placeholder="Username or E-mail" name="loginForm[username]" />
+          {isLdapStrategySetup && (
+            <div className="input-group-append">
+              <small className="input-group-text text-success">
+                <i className="icon-fw icon-check"></i> LDAP
+              </small>
+            </div>
+          )}
+        </div>
+
+        <div className="input-group mb-3">
+          <div className="input-group-prepend">
+            <span className="input-group-text">
+              <i className="icon-lock"></i>
+            </span>
+          </div>
+          <input type="password" className="form-control" placeholder="Password" name="loginForm[password]" />
+        </div>
+
+        <div className="input-group mt-5">
+          <input type="hidden" name="_csrf" value={noLoginContainer.csrfToken} />
+          <button type="submit" id="login" className="btn btn-fill login mx-auto">
+            <div className="eff"></div>
+            <span className="btn-label">
+              <i className="icon-login"></i>
+            </span>
+            <span className="btn-label-text">{t('Sign in')}</span>
+          </button>
+        </div>
+      </form>
+    );
+  }
+
+  renderExternalAuthInput(auth) {
+    const { t } = this.props;
+    const authIconNames = {
+      google: 'google',
+      github: 'github',
+      facebook: 'facebook',
+      twitter: 'twitter',
+      oidc: 'openid',
+      saml: 'key',
+      basic: 'lock',
+    };
+
+    return (
+      <div key={auth} className="col-6 mb-2">
+        <button type="button" className="btn btn-fill" id={auth} onClick={this.handleLoginWithExternalAuth}>
+          <div className="eff"></div>
+          <span className="btn-label">
+            <i className={`fa fa-${authIconNames[auth]}`}></i>
+          </span>
+          <span className="btn-label-text">{t('Sign in')}</span>
+        </button>
+        <div className="small text-right">by {auth} Account</div>
+      </div>
+    );
+  }
+
+  renderExternalAuthLoginForm() {
+    const { isLocalStrategySetup, isLdapStrategySetup, objOfIsExternalAuthEnableds } = this.props;
+    const isExternalAuthCollapsible = isLocalStrategySetup || isLdapStrategySetup;
+    const collapsibleClass = isExternalAuthCollapsible ? 'collapse collapse-external-auth' : '';
+
+    return (
+      <>
+        <div className="border-top border-bottom">
+          <div id="external-auth" className={`external-auth ${collapsibleClass}`}>
+            <div className="row mt-2">
+              {Object.keys(objOfIsExternalAuthEnableds).map((auth) => {
+                if (!objOfIsExternalAuthEnableds[auth]) {
+                  return;
+                }
+                return this.renderExternalAuthInput(auth);
+              })}
+            </div>
+          </div>
+        </div>
+        <div className="text-center">
+          <button
+            type="button"
+            className="btn btn-secondary btn-sm mb-3"
+            data-toggle={isExternalAuthCollapsible ? 'collapse' : ''}
+            data-target="#external-auth"
+            aria-expanded="false"
+            aria-controls="external-auth"
+          >
+            External Auth
+          </button>
+        </div>
+      </>
+    );
+  }
+
+  renderRegisterForm() {
+    const {
+      t,
+      username,
+      name,
+      email,
+      noLoginContainer,
+      registrationMode,
+      registrationWhiteList,
+    } = this.props;
+
+    return (
+      <React.Fragment>
+        {registrationMode === 'Restricted' && (
+        <p className="alert alert-warning">
+          {t('page_register.notice.restricted')}
+          <br />
+          {t('page_register.notice.restricted_defail')}
+        </p>
+        )}
+        <form role="form" action="/register" method="post" id="register-form">
+          <div className="input-group" id="input-group-username">
+            <div className="input-group-prepend">
+              <span className="input-group-text">
+                <i className="icon-user"></i>
+              </span>
+            </div>
+            <input type="text" className="form-control" placeholder={t('User ID')} name="registerForm[username]" defaultValue={username} required />
+          </div>
+          <p className="form-text text-danger">
+            <span id="help-block-username"></span>
+          </p>
+
+          <div className="input-group">
+            <div className="input-group-prepend">
+              <span className="input-group-text">
+                <i className="icon-tag"></i>
+              </span>
+            </div>
+            <input type="text" className="form-control" placeholder={t('Name')} name="registerForm[name]" defaultValue={name} required />
+          </div>
+
+          <div className="input-group">
+            <div className="input-group-prepend">
+              <span className="input-group-text">
+                <i className="icon-envelope"></i>
+              </span>
+            </div>
+            <input type="email" className="form-control" placeholder={t('Email')} name="registerForm[email]" defaultValue={email} required />
+          </div>
+
+          {registrationWhiteList.length > 0 && (
+          <>
+            <p className="form-text">{t('page_register.form_help.email')}</p>
+            <ul>
+              {registrationWhiteList.map((elem) => {
+                  return (
+                    <li key={elem}>
+                      <code>{elem}</code>
+                    </li>
+                  );
+                })}
+            </ul>
+          </>
+          )}
+
+          <div className="input-group">
+            <div className="input-group-prepend">
+              <span className="input-group-text">
+                <i className="icon-lock"></i>
+              </span>
+            </div>
+            <input type="password" className="form-control" placeholder={t('Password')} name="registerForm[password]" required />
+          </div>
+
+          <div className="input-group justify-content-center mt-5">
+            <input type="hidden" name="_csrf" value={noLoginContainer.csrfToken} />
+            <button type="submit" className="btn btn-fill" id="register">
+              <div className="eff"></div>
+              <span className="btn-label">
+                <i className="icon-user-follow"></i>
+              </span>
+              <span className="btn-label-text">{t('Sign up')}</span>
+            </button>
+          </div>
+        </form>
+
+        <div className="border-bottom mb-3"></div>
+
+        <div className="row">
+          <div className="text-right col-12 py-1">
+            <a href="#login" id="login" className="link-switch" onClick={this.switchForm}>
+              <i className="icon-fw icon-login"></i>
+              {t('Sign in is here')}
+            </a>
+          </div>
+        </div>
+      </React.Fragment>
+    );
+  }
+
+  render() {
+    const {
+      t,
+      isLocalStrategySetup,
+      isLdapStrategySetup,
+      isRegistrationEnabled,
+      objOfIsExternalAuthEnableds,
+    } = this.props;
+
+    const isLocalOrLdapStrategiesEnabled = isLocalStrategySetup || isLdapStrategySetup;
+    const isSomeExternalAuthEnabled = Object.values(objOfIsExternalAuthEnableds).some(elem => elem);
+
+    return (
+      <div className="login-dialog mx-auto" id="login-dialog">
+        <div className="row mx-0">
+          <div className="col-12">
+            <ReactCardFlip isFlipped={this.state.isRegistering} flipDirection="horizontal" cardZIndex="3">
+              <div className="front">
+                {isLocalOrLdapStrategiesEnabled && this.renderLocalOrLdapLoginForm()}
+                {isSomeExternalAuthEnabled && this.renderExternalAuthLoginForm()}
+                {isRegistrationEnabled && (
+                  <div className="row">
+                    <div className="col-12 text-right py-2">
+                      <a href="#register" id="register" className="link-switch" onClick={this.switchForm}>
+                        <i className="ti-check-box"></i> {t('Sign up is here')}
+                      </a>
+                    </div>
+                  </div>
+              )}
+              </div>
+              <div className="back">
+                {isRegistrationEnabled && this.renderRegisterForm()}
+              </div>
+            </ReactCardFlip>
+          </div>
+        </div>
+        <a href="https://growi.org" className="link-growi-org pl-3">
+          <span className="growi">GROWI</span>.<span className="org">ORG</span>
+        </a>
+      </div>
+    );
+  }
+
+}
+
+/**
+ * Wrapper component for using unstated
+ */
+const LoginFormWrapper = (props) => {
+  return createSubscribedElement(LoginForm, props, [NoLoginContainer]);
+};
+
+LoginForm.propTypes = {
+  // i18next
+  t: PropTypes.func.isRequired,
+  noLoginContainer: PropTypes.instanceOf(NoLoginContainer).isRequired,
+  isRegistering: PropTypes.bool,
+  username: PropTypes.string,
+  name: PropTypes.string,
+  email: PropTypes.string,
+  isRegistrationEnabled: PropTypes.bool,
+  registrationMode: PropTypes.string,
+  registrationWhiteList: PropTypes.array,
+  isLocalStrategySetup: PropTypes.bool,
+  isLdapStrategySetup: PropTypes.bool,
+  objOfIsExternalAuthEnableds: PropTypes.object,
+};
+
+export default withTranslation()(LoginFormWrapper);

+ 3 - 5
src/client/js/components/Me/BasicInfoSettings.jsx

@@ -68,17 +68,15 @@ class BasicInfoSettings extends React.Component {
               defaultValue={personalContainer.state.email}
               onChange={(e) => { personalContainer.changeEmail(e.target.value) }}
             />
-          </div>
-          {registrationWhiteList.length !== 0 && (
-            <div className="col-sm-offset-2 col-sm-10">
+            {registrationWhiteList.length !== 0 && (
               <div className="form-text text-muted">
                 {t('page_register.form_help.email')}
                 <ul>
                   {registrationWhiteList.map(data => <li key={data}><code>{data}</code></li>)}
                 </ul>
               </div>
-            </div>
-          )}
+            )}
+          </div>
         </div>
 
         <div className="form-group row">

+ 3 - 13
src/client/js/components/Navbar/GrowiSubNavigation.jsx

@@ -61,7 +61,7 @@ const GrowiSubNavigation = (props) => {
   const isPageForbidden = document.querySelector('#grw-subnav').getAttribute('data-is-forbidden-page') === 'true';
   const { appContainer, pageContainer } = props;
   const {
-    pageId, path, createdAt, creator, updatedAt, revisionAuthor, isHeaderSticky, isSubnavCompact,
+    pageId, path, createdAt, creator, updatedAt, revisionAuthor,
   } = pageContainer.state;
 
   const isPageNotFound = pageId == null;
@@ -77,16 +77,6 @@ const GrowiSubNavigation = (props) => {
   }
 
   const additionalClassNames = ['grw-subnavbar'];
-  const layoutType = appContainer.getConfig().layoutType;
-
-  if (layoutType === 'growi') {
-    if (isHeaderSticky) {
-      additionalClassNames.push('grw-subnavbar-sticky');
-    }
-    if (isSubnavCompact) {
-      additionalClassNames.push('grw-subnavbar-compact');
-    }
-  }
 
   return (
     <div className={`d-flex align-items-center justify-content-between px-3 py-1 ${additionalClassNames.join(' ')}`}>
@@ -116,12 +106,12 @@ const GrowiSubNavigation = (props) => {
         <ul className="authors text-nowrap d-none d-lg-block d-edit-none">
           { creator != null && (
             <li>
-              <PageCreator creator={creator} createdAt={createdAt} isCompactMode={isSubnavCompact} />
+              <PageCreator creator={creator} createdAt={createdAt} />
             </li>
           ) }
           { revisionAuthor != null && (
             <li className="mt-1">
-              <RevisionAuthor revisionAuthor={revisionAuthor} updatedAt={updatedAt} isCompactMode={isSubnavCompact} />
+              <RevisionAuthor revisionAuthor={revisionAuthor} updatedAt={updatedAt} />
             </li>
           ) }
         </ul>

+ 2 - 10
src/client/js/components/Navbar/GrowiSubNavigationForUserPage.jsx

@@ -36,22 +36,14 @@ const GrowiSubNavigationForUserPage = (props) => {
   const pageUser = JSON.parse(document.querySelector('#grw-subnav-for-user-page').getAttribute('data-page-user'));
   const { appContainer, pageContainer } = props;
   const {
-    pageId, path, isHeaderSticky, isSubnavCompact,
+    pageId, path,
   } = pageContainer.state;
 
   const additionalClassNames = ['grw-subnavbar', 'grw-subnavbar-user-page'];
   const layoutType = appContainer.getConfig().layoutType;
 
   if (layoutType === 'growi') {
-    if (isHeaderSticky) {
-      additionalClassNames.push('grw-subnavbar-sticky');
-    }
-    if (isSubnavCompact) {
-      additionalClassNames.push('py-2 grw-subnavbar-compact');
-    }
-    else {
-      additionalClassNames.push('py-3');
-    }
+    additionalClassNames.push('py-3');
   }
 
   return (

+ 42 - 15
src/client/js/components/HeaderSearchBox.jsx → src/client/js/components/Navbar/SearchTop.jsx

@@ -2,13 +2,13 @@ import React from 'react';
 import PropTypes from 'prop-types';
 import { withTranslation } from 'react-i18next';
 
-import { createSubscribedElement } from './UnstatedUtils';
-import AppContainer from '../services/AppContainer';
+import { createSubscribedElement } from '../UnstatedUtils';
+import AppContainer from '../../services/AppContainer';
 
-import SearchForm from './SearchForm';
+import SearchForm from '../SearchForm';
 
 
-class HeaderSearchBox extends React.Component {
+class SearchTop extends React.Component {
 
   constructor(props) {
     super(props);
@@ -16,6 +16,7 @@ class HeaderSearchBox extends React.Component {
     this.state = {
       text: '',
       isScopeChildren: false,
+      isCollapsed: true,
     };
 
     this.onInputChange = this.onInputChange.bind(this);
@@ -24,10 +25,14 @@ class HeaderSearchBox extends React.Component {
     this.search = this.search.bind(this);
   }
 
-  componentDidMount() {
+  componentWillMount() {
+    this.initBreakpointEvents();
   }
 
-  componentWillUnmount() {
+  initBreakpointEvents() {
+    this.props.appContainer.addBreakpointListener('md', (mql) => {
+      this.setState({ isCollapsed: !mql.matches });
+    }, true);
   }
 
   onInputChange(text) {
@@ -56,7 +61,23 @@ class HeaderSearchBox extends React.Component {
     window.location.href = url.href;
   }
 
-  render() {
+  Root = ({ children }) => {
+    const { isCollapsed } = this.state;
+
+    return isCollapsed
+      ? (
+        <div id="grw-search-top-collapse" className="collapse bg-dark p-3">
+          {children}
+        </div>
+      )
+      : (
+        <div className="grw-search-top-fixed position-fixed">
+          {children}
+        </div>
+      );
+  };
+
+  SearchTopForm = () => {
     const { t, appContainer } = this.props;
     const scopeLabel = this.state.isScopeChildren
       ? t('header_search_box.label.This tree')
@@ -94,19 +115,25 @@ class HeaderSearchBox extends React.Component {
     );
   }
 
+  render() {
+    const { Root, SearchTopForm } = this;
+    return (
+      <Root><SearchTopForm /></Root>
+    );
+  }
+
 }
 
+SearchTop.propTypes = {
+  t: PropTypes.func.isRequired, // i18next
+  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
+};
 
 /**
  * Wrapper component for using unstated
  */
-const HeaderSearchBoxWrapper = (props) => {
-  return createSubscribedElement(HeaderSearchBox, props, [AppContainer]);
-};
-
-HeaderSearchBox.propTypes = {
-  t: PropTypes.func.isRequired, // i18next
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
+const SearchTopWrapper = (props) => {
+  return createSubscribedElement(SearchTop, props, [AppContainer]);
 };
 
-export default withTranslation()(HeaderSearchBoxWrapper);
+export default withTranslation()(SearchTopWrapper);

+ 25 - 37
src/client/js/components/Sidebar.jsx

@@ -38,44 +38,32 @@ class Sidebar extends React.Component {
   initBreakpointEvents() {
     const { appContainer, navigationUIController } = this.props;
 
-    document.addEventListener('DOMContentLoaded', () => {
-      // get the value of '--breakpoint-*'
-      // const breakpointSm = parseInt(window.getComputedStyle(document.documentElement).getPropertyValue('--breakpoint-sm'), 10);
-      const breakpointMd = parseInt(window.getComputedStyle(document.documentElement).getPropertyValue('--breakpoint-md'), 10);
-
-      const smHandler = (mql) => {
-        if (mql.matches) {
-          // cache width
-          this.sidebarWidthCached = navigationUIController.state.productNavWidth;
-
-          appContainer.setState({ isDrawerOpened: false });
-          navigationUIController.disableResize();
-          navigationUIController.expand();
-
-          // fix width
-          navigationUIController.setState({ productNavWidth: sidebarDefaultWidth });
+    const mdOrAvobeHandler = (mql) => {
+      // sm -> md
+      if (mql.matches) {
+        appContainer.setState({ isDrawerOpened: false });
+        navigationUIController.enableResize();
+
+        // restore width
+        if (this.sidebarWidthCached != null) {
+          navigationUIController.setState({ productNavWidth: this.sidebarWidthCached });
         }
-        else {
-          appContainer.setState({ isDrawerOpened: false });
-          navigationUIController.enableResize();
-
-          // restore width
-          if (this.sidebarWidthCached != null) {
-            navigationUIController.setState({ productNavWidth: this.sidebarWidthCached });
-          }
-        }
-      };
-
-      // const mediaQueryForXs = window.matchMedia(`(max-width: ${breakpointSm}px)`);
-      const mediaQueryForSm = window.matchMedia(`(max-width: ${breakpointMd}px)`);
-
-      // add event listener
-      // mediaQueryForXs.addListener(xsHandler);
-      mediaQueryForSm.addListener(smHandler);
-      // initialize
-      // xsHandler(mediaQueryForXs);
-      smHandler(mediaQueryForSm);
-    });
+      }
+      // md -> sm
+      else {
+        // cache width
+        this.sidebarWidthCached = navigationUIController.state.productNavWidth;
+
+        appContainer.setState({ isDrawerOpened: false });
+        navigationUIController.disableResize();
+        navigationUIController.expand();
+
+        // fix width
+        navigationUIController.setState({ productNavWidth: sidebarDefaultWidth });
+      }
+    };
+
+    appContainer.addBreakpointListener('md', mdOrAvobeHandler, true);
   }
 
   backdropClickedHandler = () => {

+ 0 - 24
src/client/js/installer.jsx

@@ -1,24 +0,0 @@
-import React from 'react';
-import ReactDOM from 'react-dom';
-import { I18nextProvider } from 'react-i18next';
-
-import i18nFactory from './util/i18n';
-
-import InstallerForm from './components/InstallerForm';
-
-const i18n = i18nFactory();
-
-// render InstallerForm
-const installerFormElem = document.getElementById('installer-form');
-if (installerFormElem) {
-  const userName = installerFormElem.dataset.userName;
-  const name = installerFormElem.dataset.name;
-  const email = installerFormElem.dataset.email;
-  const csrf = installerFormElem.dataset.csrf;
-  ReactDOM.render(
-    <I18nextProvider i18n={i18n}>
-      <InstallerForm userName={userName} name={name} email={email} csrf={csrf} />
-    </I18nextProvider>,
-    installerFormElem,
-  );
-}

+ 81 - 0
src/client/js/nologin.jsx

@@ -0,0 +1,81 @@
+import React from 'react';
+import ReactDOM from 'react-dom';
+import { Provider } from 'unstated';
+import { I18nextProvider } from 'react-i18next';
+
+import i18nFactory from './util/i18n';
+
+import NoLoginContainer from './services/NoLoginContainer';
+import AppContainer from './services/AppContainer';
+
+import InstallerForm from './components/InstallerForm';
+import LoginForm from './components/LoginForm';
+
+const i18n = i18nFactory();
+
+// render InstallerForm
+const installerFormElem = document.getElementById('installer-form');
+if (installerFormElem) {
+  const userName = installerFormElem.dataset.userName;
+  const name = installerFormElem.dataset.name;
+  const email = installerFormElem.dataset.email;
+  const csrf = installerFormElem.dataset.csrf;
+  ReactDOM.render(
+    <I18nextProvider i18n={i18n}>
+      <InstallerForm userName={userName} name={name} email={email} csrf={csrf} />
+    </I18nextProvider>,
+    installerFormElem,
+  );
+}
+
+// render loginForm
+const loginFormElem = document.getElementById('login-form');
+if (loginFormElem) {
+  const noLoginContainer = new NoLoginContainer();
+  const appContainer = new AppContainer();
+  appContainer.init();
+
+  const username = loginFormElem.dataset.username;
+  const name = loginFormElem.dataset.name;
+  const email = loginFormElem.dataset.email;
+  const isRegistrationEnabled = loginFormElem.dataset.isRegistrationEnabled === 'true';
+  const registrationMode = loginFormElem.dataset.registrationMode;
+
+
+  let registrationWhiteList = loginFormElem.dataset.registrationWhiteList;
+  registrationWhiteList = registrationWhiteList.length > 0
+    ? registrationWhiteList = loginFormElem.dataset.registrationWhiteList.split(',')
+    : registrationWhiteList = [];
+
+
+  const isLocalStrategySetup = loginFormElem.dataset.isLocalStrategySetup === 'true';
+  const isLdapStrategySetup = loginFormElem.dataset.isLdapStrategySetup === 'true';
+  const objOfIsExternalAuthEnableds = {
+    google: loginFormElem.dataset.isGoogleAuthEnabled === 'true',
+    github: loginFormElem.dataset.isGithubAuthEnabled === 'true',
+    facebook: loginFormElem.dataset.isFacebookAuthEnabled === 'true',
+    twitter: loginFormElem.dataset.isTwitterAuthEnabled === 'true',
+    saml: loginFormElem.dataset.isSamlAuthEnabled === 'true',
+    oidc: loginFormElem.dataset.isOidcAuthEnabled === 'true',
+    basic: loginFormElem.dataset.isBasicAuthEnabled === 'true',
+  };
+
+  ReactDOM.render(
+    <I18nextProvider i18n={i18n}>
+      <Provider inject={[noLoginContainer, appContainer]}>
+        <LoginForm
+          username={username}
+          name={name}
+          email={email}
+          isRegistrationEnabled={isRegistrationEnabled}
+          registrationMode={registrationMode}
+          registrationWhiteList={registrationWhiteList}
+          isLocalStrategySetup={isLocalStrategySetup}
+          isLdapStrategySetup={isLdapStrategySetup}
+          objOfIsExternalAuthEnableds={objOfIsExternalAuthEnableds}
+        />
+      </Provider>
+    </I18nextProvider>,
+    loginFormElem,
+  );
+}

+ 23 - 1
src/client/js/services/AppContainer.js

@@ -33,6 +33,7 @@ export default class AppContainer extends Container {
       editorMode: null,
       preferDarkModeByMediaQuery: false,
       preferDarkModeByUser: null,
+      breakpoint: 'xs',
       isDrawerOpened: false,
 
       isPageCreateModalShown: false,
@@ -108,7 +109,6 @@ export default class AppContainer extends Container {
   }
 
   init() {
-    // this.initBreakpointEvents();
     this.initColorScheme();
     this.initPlugins();
   }
@@ -233,6 +233,28 @@ export default class AppContainer extends Container {
     return this.componentInstances[id];
   }
 
+  /**
+   *
+   * @param {string} breakpoint id of breakpoint
+   * @param {function} handler event handler for media query
+   * @param {boolean} invokeOnInit invoke handler after the initialization if true
+   */
+  addBreakpointListener(breakpoint, handler, invokeOnInit = false) {
+    document.addEventListener('DOMContentLoaded', () => {
+      // get the value of '--breakpoint-*'
+      const breakpointPixel = parseInt(window.getComputedStyle(document.documentElement).getPropertyValue(`--breakpoint-${breakpoint}`), 10);
+
+      const mediaQuery = window.matchMedia(`(min-width: ${breakpointPixel}px)`);
+
+      // add event listener
+      mediaQuery.addListener(handler);
+      // initialize
+      if (invokeOnInit) {
+        handler(mediaQuery);
+      }
+    });
+  }
+
   getOriginRenderer() {
     return this.originRenderer;
   }

+ 23 - 0
src/client/js/services/NoLoginContainer.js

@@ -0,0 +1,23 @@
+import { Container } from 'unstated';
+
+/**
+ * Service container related to Nologin (installer, login)
+ * @extends {Container} unstated Container
+ */
+export default class NoLoginContainer extends Container {
+
+  constructor() {
+    super();
+
+    const body = document.querySelector('body');
+    this.csrfToken = body.dataset.csrftoken;
+  }
+
+  /**
+   * Workaround for the mangling in production build to break constructor.name
+   */
+  static getClassName() {
+    return 'NoLoginContainer';
+  }
+
+}

+ 1 - 5
src/client/styles/scss/_admin.scss

@@ -1,8 +1,4 @@
 .admin-page {
-  header.grw-header {
-    height: unset;
-  }
-
   .admin-user-menu {
     .dropdown-menu {
       right: 0;
@@ -168,4 +164,4 @@
 
 .admin-navigation > a + a {
   margin-top: 2px;
-}
+}

+ 57 - 189
src/client/styles/scss/_login.scss

@@ -19,6 +19,7 @@
       display: flex;
       align-items: center;
       height: 100vh;
+      margin-top: 0px;
 
       .main {
         width: 100vw;
@@ -106,139 +107,57 @@
     }
   }
 
-  .external-auth {
-    form {
-      flex: 1;
-
-      @media (min-width: 350px) {
-        flex: 0.49;
-      }
-    }
-
-    .spacer {
-      height: 10px;
-    }
-  }
-
   .collapse-external-auth {
     overflow: hidden;
-
-    &:not(.show) {
-      height: 0;
-      padding: 0 !important;
-    }
   }
 
-  // button style
-  .btn-fill.login {
-    .btn-label {
-      background-color: rgba($danger, 0.4);
-    }
-    .eff {
-      background-color: rgba(#7e4153, 0.5);
-    }
-  }
-
-  // google
-  .btn-fill#google {
-    .btn-label {
-      background-color: rgba(#24292e, 0.4);
-    }
-
-    .eff {
-      background-color: #555;
-    }
-  }
-
-  // github
-  .btn-fill#github {
-    .btn-label {
-      background-color: rgba(lighten(black, 20%), 0.4);
-    }
-
-    .eff {
-      background-color: #555;
-    }
-  }
-
-  // facebook
-  .btn-fill#facebook {
-    .btn-label {
-      background-color: rgba(#29487d, 0.4);
-    }
-
-    .eff {
-      background-color: #555;
-    }
-  }
-
-  // twitter
-  .btn-fill#twitter {
-    .btn-label {
-      background-color: rgba(#1da1f2, 0.4);
-    }
-
-    .eff {
-      background-color: #555;
-    }
-  }
-
-  // oidc
-  .btn-fill#oidc {
-    .btn-label {
-      background-color: rgba(#24292e, 0.4);
-    }
-
-    .eff {
-      background-color: #555;
-    }
-  }
-
-  // saml
-  .btn-fill#saml {
-    .btn-label {
-      background-color: rgba(#55a79a, 0.4);
-    }
-
-    .eff {
-      background-color: #555;
-    }
-  }
-
-  // basic
-  .btn-fill#basic {
-    .btn-label {
-      background-color: rgba(#24292e, 0.4);
-    }
-
-    .eff {
-      background-color: #555;
-    }
-  }
-  // register
-  .btn-fill#register {
-    .btn-label {
-      background-color: rgba($success, 0.4);
-    }
-
-    .eff {
-      background-color: rgba(#3f7263, 0.5);
-    }
-  }
-
-  // external-auth
-  .btn-collapse-external-auth {
-    color: white;
-    background-color: rgba(lighten(black, 20%), 0.4);
-    border: none;
-
-    .btn-label {
-      padding: 9px 15px;
-      margin: -8px 20px -8px -20px;
-    }
-
-    &:focus {
-      border: none;
+  $btn-fill-colors: (
+    'login': (
+      rgba($danger, 0.4),
+      rgba(#7e4153, 0.5),
+    ),
+    'register': (
+      rgba($success, 0.4),
+      rgba(#3f7263, 0.5),
+    ),
+    'google': (
+      rgba(#24292e, 0.4),
+      #555,
+    ),
+    'github': (
+      rgba(lighten(black, 20%), 0.4),
+      #555,
+    ),
+    'facebook': (
+      rgba(#29487d, 0.4),
+      #555,
+    ),
+    'twitter': (
+      rgba(#1da1f2, 0.4),
+      #555,
+    ),
+    'oidc': (
+      rgba(#24292e, 0.4),
+      #555,
+    ),
+    'saml': (
+      rgba(#55a79a, 0.4),
+      #555,
+    ),
+    'basic': (
+      rgba(#24292e, 0.4),
+      #555,
+    ),
+  );
+
+  @each $label, $colors in $btn-fill-colors {
+    .btn-fill##{$label} {
+      .btn-label {
+        background-color: nth($colors, 1);
+      }
+      .eff {
+        background-color: nth($colors, 2);
+      }
     }
   }
 
@@ -283,16 +202,9 @@
 
 .login-page {
   // layout
-  .main .row {
-    @media (min-width: 350px) {
-      .col-sm-offset-4 {
-        margin-left: calc(50% - 160px);
-      }
-
-      .col-sm-4 {
-        width: 320px;
-      }
-    }
+  .main .row .login-header,
+  .login-dialog {
+    width: 320px;
   }
 
   .link-growi-org {
@@ -301,55 +213,11 @@
     z-index: 2;
   }
 
-  // flip animation
-  .login-dialog.flipper {
-    transition: min-height 0.2s;
-
-    &.to-flip {
-      min-height: 295px;
-
-      // has-error
-      &.has-error {
-        min-height: #{295px + 32px};
-      }
-    }
-
-    .front,
-    .back {
-      transition: 0.4s;
-      backface-visibility: hidden;
-      transform-style: preserve-3d;
-      // fix https://github.com/weseek/growi/issues/330
-      -webkit-backface-visibility: hidden;
-      -webkit-transform-style: preserve-3d;
-    }
-
-    .front {
-      z-index: 2;
-    }
-
-    .back {
-      position: absolute;
-      top: 0;
-      right: 15px;
-      left: 15px;
-    }
-
-    .back,
-    &.to-flip .front {
-      transform: rotateY(180deg);
-
-      // fix https://github.com/weseek/growi/issues/330
-      // 'backface-visibility: hidden' and 'z-index: -1' breaks layout in iOS
-      ::after {
-        z-index: 0;
-        opacity: 0.3;
-      }
-    }
-
-    &.to-flip .back {
-      transform: rotateY(0deg);
-    }
+  // To adjust the behavior, this problem is not solved.
+  // See https://github.com/AaronCCWong/react-card-flip/issues/56
+  .react-card-front,
+  .react-card-back {
+    height: 0% !important;
   }
 }
 
@@ -357,7 +225,7 @@
 .nologin.error {
   .main .row {
     @media (min-width: 510px) {
-      .col-sm-offset-4 {
+      .offset-sm-4 {
         margin-left: calc(50% - 240px);
       }
 

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

@@ -1,5 +1,2 @@
 .user-settings-page {
-  header.grw-header {
-    height: unset;
-  }
 }

+ 18 - 9
src/client/styles/scss/_search.scss

@@ -18,6 +18,7 @@
 
 .search-typeahead {
   position: relative;
+  width: 100%;
 
   .search-clear {
     position: absolute;
@@ -102,17 +103,25 @@
 
 // layout
 .search-top {
-  .rbt-input.form-control {
-    width: 200px;
-    transition: 0.3s ease-out;
+  .grw-search-top-fixed {
+    // centering on navbar
+    top: $grw-navbar-height / 2;
+    left: 50vw;
+    z-index: $zindex-fixed + 1;
+    transform: translate(-50%, -50%);
 
-    // focus
-    &.focus {
-      width: 300px;
-    }
+    .rbt-input.form-control {
+      width: 200px;
+      transition: 0.3s ease-out;
 
-    @include media-breakpoint-up(md) {
-      width: 300px;
+      // focus
+      &.focus {
+        width: 300px;
+      }
+
+      @include media-breakpoint-up(md) {
+        width: 300px;
+      }
     }
   }
 }

+ 1 - 1
src/client/styles/scss/_sidebar.scss

@@ -51,7 +51,6 @@
   $navbar-total-height: $grw-navbar-height + $grw-navbar-border-width;
   div[class$='-LayoutContainer'] {
     height: calc(100vh - #{$navbar-total-height});
-    margin-top: $navbar-total-height;
   }
   div[class$='-NavigationContainer'] {
     top: $navbar-total-height;
@@ -118,6 +117,7 @@
   }
   div[class$='-ScrollableTransitionGroup'] {
     // remove horizontal line
+    > div,
     > div > div {
       &:before,
       &:after {

+ 0 - 42
src/client/styles/scss/_subnav.scss

@@ -5,48 +5,6 @@ $easeInOutCubic: cubic-bezier(0.65, 0, 0.35, 1);
   transition: all 300ms $easeInOutCubic;
 }
 
-/*
- * layout for sticky
- */
-header.grw-header {
-  top: $grw-navbar-height + $grw-navbar-border-width;
-
-  // Adjust to be on top of the growi subnavigation
-  z-index: $zindex-sticky;
-
-  height: 110px;
-  pointer-events: none; // disable pointer events for sticky
-
-  .grw-subnav {
-    overflow: unset;
-    pointer-events: all; // enable pointer events
-  }
-}
-
-/*
- * Compact Mode Switching
- */
-.grw-subnavbar {
-  &.grw-subnavbar-compact {
-    @extend %transitionForCompactMode;
-
-    h1 {
-      @include variable-font-size(18px);
-      @extend %transitionForCompactMode;
-    }
-  }
-}
-
-/*
- * Sticky Mode Switching
- */
-.grw-subnavbar {
-  &.grw-subnavbar-sticky {
-    // set transition-duration (init -> sticky)
-    transition: all 400ms linear !important;
-  }
-}
-
 /*
  * Styles
  */

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

@@ -1,8 +1,4 @@
 .tags-page {
-  header.grw-header {
-    height: unset;
-  }
-
   .list-tag-count {
     background: rgba(0, 0, 0, 0.08);
   }

+ 0 - 35
src/client/styles/scss/_user.scss

@@ -5,41 +5,6 @@ $easeInOutCubic: cubic-bezier(0.65, 0, 0.35, 1);
   transition: all 300ms $easeInOutCubic;
 }
 
-.grw-header.grw-header-user-page {
-  height: 150px;
-}
-
-/*
- * Compact Mode Switching
- */
-.grw-subnavbar.grw-subnavbar-user-page {
-  &.grw-subnavbar-compact {
-    .grw-user-page-path {
-      margin-bottom: 0;
-      font-size: 14px;
-
-      @extend %transitionForCompactMode;
-    }
-    .picture {
-      width: 62px;
-      height: 62px;
-
-      @extend %transitionForCompactMode;
-    }
-    h1 {
-      font-size: 1.5em;
-      line-height: 30px;
-
-      @extend %transitionForCompactMode;
-    }
-    .users-meta {
-      margin-left: 15px;
-
-      @extend %transitionForCompactMode;
-    }
-  }
-}
-
 /*
  * Styles
  */

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

@@ -31,8 +31,12 @@
 }
 
 // fill button style
-.btn-fill {
+:root .btn.btn-fill {
   position: relative;
+  display: flex;
+  justify-content: space-between;
+  min-width: 130px;
+  padding: 0px;
   overflow: hidden;
   color: white;
   text-align: center;
@@ -43,6 +47,7 @@
   .btn-label {
     position: relative;
     z-index: 1;
+    padding: 9px 15px;
     color: white;
     text-decoration: none;
   }
@@ -50,7 +55,9 @@
   .btn-label-text {
     position: relative;
     z-index: 1;
+    margin: auto;
     color: white;
+    text-align: center;
     text-decoration: none;
   }
 

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

@@ -1,3 +1,6 @@
+// determine optional variables
+$bgcolor-subnabvar: lighten($bgcolor-global, 3%) !default;
+
 /*
   * Form
   */
@@ -131,24 +134,8 @@ ul.pagination {
 /*
  * GROWI subnavigation
  */
-.admin-page,
-.user-settings-page,
-.tags-page {
-  .grw-header {
-    background-color: rgba(darken($bgcolor-global, 90%), 0.9);
-  }
-}
-
 .grw-subnavbar {
-  background-color: rgba(darken($bgcolor-global, 90%), 1);
-
-  &.grw-subnavbar-sticky {
-    background-color: rgba(darken($bgcolor-global, 90%), 0.9);
-    box-shadow: 0 3px 2px -2px darken($bgcolor-global, 5%);
-  }
-}
-
-.grw-subnavbar-sticky {
+  background-color: $bgcolor-subnabvar;
 }
 
 /*

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

@@ -1,3 +1,6 @@
+// determine optional variables
+$bgcolor-subnabvar: darken($bgcolor-global, 3%) !default;
+
 /*
  * Form
  */
@@ -26,21 +29,8 @@
 /*
  * GROWI subnavigation
  */
-.admin-page,
-.user-settings-page,
-.tags-page {
-  .grw-header {
-    background-color: rgba(darken($bgcolor-global, 6%), 0.9);
-  }
-}
-
 .grw-subnavbar {
-  background-color: rgba(darken($bgcolor-global, 5%), 1);
-
-  &.grw-subnavbar-sticky {
-    background-color: rgba(darken($bgcolor-global, 6%), 0.9);
-    box-shadow: 0 3px 2px -2px darken($bgcolor-global, 40%);
-  }
+  background-color: $bgcolor-subnabvar;
 }
 
 /*

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

@@ -64,6 +64,9 @@ html[light] {
   // Sidebar list group
   $bgcolor-sidebar-list-group: #fafbff; // optional
 
+  // Subnavigation
+  // $bgcolor-subnabvar: #fafafa; // optional
+
   // Icon colors
   $color-editor-icons: $color-global;
 
@@ -130,9 +133,9 @@ html[dark] {
   $bgcolor-sidebar-nav-item-active: rgba(#969494, 0.3); // optional
   $text-shadow-sidebar-nav-item-active: 0px 0px 10px #0099ff; // optional
   // Sidebar resize button
-  $color-resize-button: $color-global;
+  $color-resize-button: white;
   $bgcolor-resize-button: $primary;
-  $color-resize-button-hover: $color-global;
+  $color-resize-button-hover: white;
   $bgcolor-resize-button-hover: darken($bgcolor-resize-button, 5%);
   // Sidebar contents
   $bgcolor-sidebar-context: #111d2f;
@@ -140,6 +143,9 @@ html[dark] {
   // Sidebar list group
   $bgcolor-sidebar-list-group: #1c2a3e; // optional
 
+  // Subnavigation
+  $bgcolor-subnabvar: lighten($bgcolor-global, 4%); // optional
+
   // Icon colors
   $color-editor-icons: $color-global;
 

+ 48 - 40
src/client/styles/scss/theme/mono-blue.scss

@@ -12,7 +12,6 @@ html[light] {
 
   // Background colors
   $bgcolor-global: $themelight;
-  $bgcolor-navbar: $themecolor;
   $bgcolor-inline-code: lighten($subthemecolor, 70%);
   $bgcolor-card: darken($themelight, 5%);
 
@@ -32,11 +31,31 @@ html[light] {
   $bgcolor-list: transparent;
   $color-list-active: $color-reversal;
   $bgcolor-list-active: $primary;
-  $color-list-hover: $color-reversal;
+  $color-list-hover: $color-search;
+
+  // Navbar
+  $bgcolor-navbar: #2a2929;
+  $bgcolor-search-top-dropdown: $themecolor;
+  $border-image-navbar: linear-gradient(to right, #54bafd 0%, #3d98a3 50%, #708b0b 100%);
 
   // Logo colors
   $bgcolor-logo: $themecolor;
-  $fillcolor-logo-mark: lighten(desaturate($bgcolor-navbar, 30%), 20%);
+  $fillcolor-logo-mark: lighten(desaturate($bgcolor-navbar, 10%), 15%);
+
+  // Sidebar
+  $bgcolor-sidebar: $themecolor;
+  // $bgcolor-sidebar-nav-item-active: rgba(#, 0.37); // optional
+  $text-shadow-sidebar-nav-item-active: 0px 0px 10px #0099ff; // optional
+  // Sidebar resize button
+  $color-resize-button: $color-reversal;
+  $bgcolor-resize-button: #209fd8;
+  $color-resize-button-hover: $color-reversal;
+  $bgcolor-resize-button-hover: lighten($bgcolor-resize-button, 5%);
+  // Sidebar contents
+  $color-sidebar-context: $color-global;
+  $bgcolor-sidebar-context: #f1fcff;
+  // Sidebar list group
+  // $bgcolor-sidebar-list-group: #; // optional
 
   // Icon colors
   $color-editor-icons: $color-global;
@@ -57,11 +76,6 @@ html[light] {
   // badge
   $color-badge: $color-reversal;
 
-  // Sidebar
-  $bgcolor-sidebar: $bgcolor-navbar;
-  $color-sidebar-context: $color-reversal;
-  $bgcolor-sidebar-context: lighten($bgcolor-sidebar, 10%);
-
   @import 'apply-colors';
   @import 'apply-colors-light';
 
@@ -78,27 +92,16 @@ html[light] {
       }
     }
   }
-
-  // Search Top
-  .search-top {
-    .input-group-prepend .dropdown-toggle {
-      color: $themecolor;
-      background-color: $color-search;
-      &:hover {
-        background-color: darken($color-search, 10%);
-      }
-    }
-  }
 }
 
 html[dark] {
   // Theme colors
-  $themecolor: #0090c8;
+  $themecolor: #00587a;
   $themedark: #061f2f;
   $accentcolor: #16617d;
   $subthemecolor: #c1f1f0;
 
-  $primary: $themecolor;
+  $primary: #0090c8;
   $dark: #031018;
 
   // Background colors
@@ -123,13 +126,33 @@ html[dark] {
   $bgcolor-list: transparent;
   $color-list-active: $color-reversal;
   $bgcolor-list-active: $primary;
-  $color-list-hover: $color-reversal;
+  $color-list-hover: $accentcolor;
+
+  // Navbar
+  $bgcolor-navbar: #2a2929;
+  $bgcolor-search-top-dropdown: $themecolor;
+  $border-image-navbar: linear-gradient(to right, #54bafd 0%, #3d98a3 50%, #708b0b 100%);
 
   // Logo colors
   $bgcolor-logo: #13191c;
   $fillcolor-logo-mark: lighten(desaturate($bgcolor-navbar, 10%), 15%);
   // $fillcolor-logo-mark: #4e5a60;
 
+  // Sidebar
+  $bgcolor-sidebar: $accentcolor;
+  // $bgcolor-sidebar-nav-item-active: rgba(#, 0.3); // optional
+  $text-shadow-sidebar-nav-item-active: 0px 0px 10px #0099ff; // optional
+  // Sidebar resize button
+  $color-resize-button: $color-global;
+  $bgcolor-resize-button: $themecolor;
+  $color-resize-button-hover: $color-global;
+  $bgcolor-resize-button-hover: darken($bgcolor-resize-button, 5%);
+  // Sidebar contents
+  $bgcolor-sidebar-context: darken($bgcolor-sidebar, 13%);
+  $color-sidebar-context: $color-global;
+  // Sidebar list group
+  // $bgcolor-sidebar-list-group: #; // optional
+
   // Icon colors
   $color-editor-icons: $color-global;
 
@@ -138,7 +161,8 @@ html[dark] {
 
   // Dropdown colors
   $bgcolor-dropdown-link-active: $primary;
-  $color-dropdown-link-active: $color-reversal;
+  $color-dropdown-link-active: $color-global;
+  $color-dropdown-link-hover: $color-reversal;
 
   // admin theme box
   $color-theme-color-box: $primary;
@@ -149,15 +173,10 @@ html[dark] {
   // badge
   $color-badge: $color-reversal;
 
-  // Sidebar
-  $bgcolor-sidebar: $bgcolor-navbar;
-  $color-sidebar-context: $color-reversal;
-  $bgcolor-sidebar-context: lighten($bgcolor-sidebar, 10%);
-
   @import 'apply-colors';
   @import 'apply-colors-dark';
 
-  // Navs {
+  // Navs
   .nav-tabs {
     border-bottom: $border-color-theme 1px solid;
     .nav-link {
@@ -173,17 +192,6 @@ html[dark] {
     }
   }
 
-  // Search Top
-  .search-top {
-    .input-group-prepend .dropdown-toggle {
-      background-color: $color-search;
-      border-color: $color-search;
-      &:hover {
-        background-color: darken($color-search, 10%);
-      }
-    }
-  }
-
   // Table
   .table {
     color: white;

+ 12 - 11
src/server/routes/index.js

@@ -63,17 +63,17 @@ module.exports = function(crowi, app) {
   app.get('/admin/security'                     , loginRequiredStrictly , adminRequired , admin.security.index);
 
   // OAuth
-  app.get('/passport/google'                      , loginPassport.loginWithGoogle);
-  app.get('/passport/github'                      , loginPassport.loginWithGitHub);
-  app.get('/passport/twitter'                     , loginPassport.loginWithTwitter);
-  app.get('/passport/oidc'                        , loginPassport.loginWithOidc);
-  app.get('/passport/saml'                        , loginPassport.loginWithSaml);
-  app.get('/passport/basic'                       , loginPassport.loginWithBasic);
-  app.get('/passport/google/callback'             , loginPassport.loginPassportGoogleCallback);
-  app.get('/passport/github/callback'             , loginPassport.loginPassportGitHubCallback);
-  app.get('/passport/twitter/callback'            , loginPassport.loginPassportTwitterCallback);
-  app.get('/passport/oidc/callback'               , loginPassport.loginPassportOidcCallback);
-  app.post('/passport/saml/callback'              , loginPassport.loginPassportSamlCallback);
+  app.get('/passport/google'                      , loginPassport.loginWithGoogle, loginPassport.loginFailure);
+  app.get('/passport/github'                      , loginPassport.loginWithGitHub, loginPassport.loginFailure);
+  app.get('/passport/twitter'                     , loginPassport.loginWithTwitter, loginPassport.loginFailure);
+  app.get('/passport/oidc'                        , loginPassport.loginWithOidc, loginPassport.loginFailure);
+  app.get('/passport/saml'                        , loginPassport.loginWithSaml, loginPassport.loginFailure);
+  app.get('/passport/basic'                       , loginPassport.loginWithBasic, loginPassport.loginFailure);
+  app.get('/passport/google/callback'             , loginPassport.loginPassportGoogleCallback   , loginPassport.loginFailure);
+  app.get('/passport/github/callback'             , loginPassport.loginPassportGitHubCallback   , loginPassport.loginFailure);
+  app.get('/passport/twitter/callback'            , loginPassport.loginPassportTwitterCallback  , loginPassport.loginFailure);
+  app.get('/passport/oidc/callback'               , loginPassport.loginPassportOidcCallback     , loginPassport.loginFailure);
+  app.post('/passport/saml/callback'              , loginPassport.loginPassportSamlCallback     , loginPassport.loginFailure);
 
   // markdown admin
   app.get('/admin/markdown'                   , loginRequiredStrictly , adminRequired , admin.markdown.index);
@@ -182,4 +182,5 @@ module.exports = function(crowi, app) {
 
   app.get('/*/$'                   , loginRequired , page.showPageWithEndOfSlash, page.notFound);
   app.get('/*'                     , loginRequired , page.showPage, page.notFound);
+
 };

+ 9 - 8
src/server/routes/login-passport.js

@@ -89,7 +89,8 @@ module.exports = function(crowi, app) {
       ldapAccountInfo = await promisifiedPassportAuthentication(strategyName, req, res);
     }
     catch (err) {
-      return next(err);
+      debug(err.message);
+      return next();
     }
 
     // check groups for LDAP
@@ -125,7 +126,7 @@ module.exports = function(crowi, app) {
 
     // login
     await req.logIn(user, (err) => {
-      if (err) { return next(err) }
+      if (err) { debug(err.message); return next() }
       return loginSuccessHandler(req, res, user);
     });
   };
@@ -216,7 +217,7 @@ module.exports = function(crowi, app) {
       }
       if (!user) { return next() }
       req.logIn(user, (err) => {
-        if (err) { return next() }
+        if (err) { debug(err.message); return next() }
 
         return loginSuccessHandler(req, res, user);
       });
@@ -287,7 +288,7 @@ module.exports = function(crowi, app) {
 
     // login
     req.logIn(user, (err) => {
-      if (err) { return next(err) }
+      if (err) { debug(err.message); return next() }
       return loginSuccessHandler(req, res, user);
     });
   };
@@ -329,7 +330,7 @@ module.exports = function(crowi, app) {
 
     // login
     req.logIn(user, (err) => {
-      if (err) { return next(err) }
+      if (err) { debug(err.message); return next() }
       return loginSuccessHandler(req, res, user);
     });
   };
@@ -371,7 +372,7 @@ module.exports = function(crowi, app) {
 
     // login
     req.logIn(user, (err) => {
-      if (err) { return next(err) }
+      if (err) { debug(err.message); return next() }
       return loginSuccessHandler(req, res, user);
     });
   };
@@ -419,7 +420,7 @@ module.exports = function(crowi, app) {
     // login
     const user = await externalAccount.getPopulatedUser();
     req.logIn(user, (err) => {
-      if (err) { return next(err) }
+      if (err) { debug(err.message); return next() }
       return loginSuccessHandler(req, res, user);
     });
   };
@@ -523,7 +524,7 @@ module.exports = function(crowi, app) {
 
     const user = await externalAccount.getPopulatedUser();
     await req.logIn(user, (err) => {
-      if (err) { return next() }
+      if (err) { debug(err.message); return next() }
       return loginSuccessHandler(req, res, user);
     });
   };

+ 1 - 1
src/server/views/installer.html

@@ -30,7 +30,7 @@
 
   {{ cdnStyleTagsByGroup('basis') }}
 
-  <script src="{{ webpack_asset('js/installer.js') }}" defer></script>
+  <script src="{{ webpack_asset('js/nologin.js') }}" defer></script>
 
 </head>
 {% endblock %}

+ 3 - 3
src/server/views/invited.html

@@ -91,10 +91,10 @@
 
         <div class="input-group justify-content-center d-flex mt-5">
           <input type="hidden" name="_csrf" value="{{ csrf() }}">
-          <button type="submit" class="btn btn-fill login px-0 py-2" id="register">
+          <button type="submit" class="btn btn-fill" id="register">
             <div class="eff"></div>
-            <span class="btn-label p-3"><i class="icon-user-follow"></i></span>
-            <span class="btn-label-text p-3">{{ t('Create') }}</span>
+            <span class="btn-label"><i class="icon-user-follow"></i></span>
+            <span class="btn-label-text">{{ t('Create') }}</span>
           </button>
         </div>
 

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

@@ -9,7 +9,7 @@
 {% block layout_main %}
 
 {% block content_header_wrapper %}
-<header class="py-0 position-sticky grw-header">
+<header class="py-0 grw-header">
   {% block content_header %}
   {% endblock %}
 </header>

+ 2 - 2
src/server/views/layout-growi/user_page.html

@@ -6,7 +6,7 @@
 {% endblock %}
 
 {% block content_header_wrapper %}
-  <header class="py-0 position-sticky grw-header grw-header-user-page">
+  <header class="py-0 grw-header grw-header-user-page">
     {% if pageUser %}
       <div id="grw-subnav-for-user-page" class="grw-subnav" data-page-user="{{ pageUser|json }}"></div>
     {% else %}
@@ -27,7 +27,7 @@
         #   Because this block has content like 'Bookmarks' or 'Recent Created' whose height changes dynamically,
         #   setting of 'revision-toc' (affix) is hindered.
         #}
-      <div class="m-b-30 user-page-content-container d-edit-none hidden-print">
+      <div class="m-b-30 user-page-content-container d-edit-none d-print-none">
         {% include '../widget/user_page_content.html' %}
       </div>
 

+ 27 - 22
src/server/views/layout/layout.html

@@ -86,48 +86,53 @@
       </a>
     </div>
 
-    {# Search #}
-    <ul class="navbar-nav grw-navbar-search d-none d-md-block">
-      <li>
-        {% if isSearchServiceConfigured() %}
-        <div class="search-top" role="search" id="search-top"></div>
-        {% endif %}
-      </li>
-    </ul>
-
     {# Navbar Right #}
     <ul class="navbar-nav ml-auto">
       {% if user %}
-      <!-- TODO GW-79 enable after refactoring  <li id="create-page-button" class="nav-item d-none d-md-block"></li> -->
-      <li class="nav-item d-none d-md-block">
-        <a class="nav-link create-page px-4" href="#" data-target="#create-page" data-toggle="modal">
-          <i class="icon-pencil mr-2"></i>
-          <span>{{ t('New') }}</span>
-        </a>
-      </li>
-      <li id="personal-dropdown" class="nav-item dropdown dropdown-toggle"></li>
+        <!-- TODO GW-79 enable after refactoring  <li id="create-page-button" class="nav-item d-none d-md-block"></li> -->
+        <li class="nav-item d-none d-md-block">
+          <a class="nav-link create-page px-4" href="#" data-target="#create-page" data-toggle="modal">
+            <i class="icon-pencil mr-2"></i>
+            <span>{{ t('New') }}</span>
+          </a>
+        </li>
+        {% if isSearchServiceConfigured() %}
+          <li class="nav-item d-md-none">
+            <a type="button" class="nav-link px-4" data-target="#grw-search-top-collapse" data-toggle="collapse">
+              <i class="icon-magnifier mr-2"></i>
+            </a>
+          </li>
+        {% endif %}
+        <li id="personal-dropdown" class="nav-item dropdown dropdown-toggle"></li>
       {% else %}
-      <li id="login-user" class="nav-item"><a class="nav-link" href="/login">Login</a></li>
+        <li id="login-user" class="nav-item"><a class="nav-link" href="/login">Login</a></li>
       {% endif %}
+
       {% if getConfig('crowi', 'app:confidential') %}
       <li class="nav-item confidential text-light">{{ getConfig('crowi', 'app:confidential') }}</li>
       {% endif %}
     </ul>
 
   </nav>
+
   {% include '../modal/create_page.html' %}
   {% endblock  %} {# layout_head_nav #}
 
   {% block head_warn_breaking_changes %}{% include '../widget/alert_breaking_changes.html' %}{% endblock %}
 
-  <div class="d-flex">
+  <div id="page-wrapper" class="page-wrapper d-flex">
     {# Sidebar #}
     <div id="grw-sidebar-wrapper"></div>
 
-    <div id="page-wrapper" class="flex-grow-1">
+    <div class="flex-grow-1">
       {% block head_warn_alert_siteurl_undefined %}{% include '../widget/alert_siteurl_undefined.html' %}{% endblock %}
-      {% block layout_main %}
-      {% endblock %} {# layout_main #}
+
+      {# Search #}
+      {% if isSearchServiceConfigured() %}
+        <div id="grw-search-top" class="search-top" role="search"></div>
+      {% endif %}
+
+      {% block layout_main %}{% endblock %}
     </div>
   </div>
 

+ 26 - 293
src/server/views/login.html

@@ -18,7 +18,9 @@
 {% block sidebar %}
 {% endblock %}
 
-
+{% block html_additional_headers %}
+  <script src="{{ webpack_asset('js/nologin.js') }}" defer></script>
+{% endblock %}
 
 {% block layout_main %}
 
@@ -30,8 +32,7 @@
         <div class="logo mb-3">{% include 'widget/logo.html' %}</div>
         <h1>{{ appService.getAppTitle() }}</h1>
 
-        <div class="row">
-          <div class="login-form-errors col-12">
+          <div class="login-form-errors px-3">
             {% if isLdapSetupFailed() %}
             <div class="alert alert-warning small">
               <strong><i class="icon-fw icon-info"></i>LDAP is enabled but the configuration has something wrong.</strong>
@@ -100,288 +101,30 @@
             </div>
             {% endif %}
           </div>
-        </div>
       </div>
-    </div>
-  </div>
-
-  <div class="row mb-5">
-    <div class="col-md-12">
-
-    {% set isLocalOrLdapStrategiesEnabled = passportService.isLocalStrategySetup || passportService.isLdapStrategySetup %}
-    {% set isExternalAuthCollapsible = isLocalOrLdapStrategiesEnabled %}
-    {% set isRegistrationEnabled = passportService.isLocalStrategySetup && getConfig('crowi', 'security:registrationMode') != 'Closed' %}
-
-      <div class="login-dialog mx-auto flipper {% if req.query.register or req.body.registerForm or isRegistering %}to-flip{% endif %}" id="login-dialog">
-
-        <div class="col-12">
-          <div class="front">
-
-            {% if isLocalOrLdapStrategiesEnabled %}
-            <form role="form" action="/login" method="post">
-
-              <div class="input-group mb-3">
-                <div class="input-group-prepend">
-                  <span class="input-group-text"><i class="icon-user"></i></span>
-                </div>
-                <input type="text" class="form-control" placeholder="Username or E-mail" name="loginForm[username]">
-                {% if passportService.isLdapStrategySetup %}
-                <div class="input-group-append">
-                  <small class="input-group-text text-success">
-                    <i class="icon-fw icon-check"></i> LDAP
-                  </small>
-                </div>
-                {% endif %}
-              </div>
-
-              <div class="input-group">
-                <div class="input-group-prepend">
-                  <span class="input-group-text"><i class="icon-lock"></i></span>
-                </div>
-                <input type="password" class="form-control" placeholder="Password" name="loginForm[password]">
-              </div>
-
-              <div class="input-group justify-content-center d-flex mt-5">
-                <input type="hidden" name="_csrf" value="{{ csrf() }}">
-                <button type="submit" class="btn btn-fill login px-0 py-2">
-                  <div class="eff"></div>
-                  <span class="btn-label p-3"><i class="icon-login"></i></span>
-                  <span class="btn-label-text p-3">{{ t('Sign in') }}</span>
-                </button>
-              </div>
-
-            </form>
-            {% endif %}
-
-            {% if (
-              getConfig('crowi', 'security:passport-google:isEnabled') ||
-              getConfig('crowi', 'security:passport-github:isEnabled') ||
-              getConfig('crowi', 'security:passport-facebook:isEnabled') ||
-              getConfig('crowi', 'security:passport-twitter:isEnabled')||
-              getConfig('crowi', 'security:passport-oidc:isEnabled') ||
-              getConfig('crowi', 'security:passport-saml:isEnabled') ||
-              getConfig('crowi', 'security:passport-basic:isEnabled')
-            ) %}
-            <div class="border-bottom"></div>
-            <div id="external-auth" class="external-auth {% if isExternalAuthCollapsible %}collapse collapse-external-auth collapse-anchor{% endif %}">
-              <div class="spacer"></div>
-              <div class="d-flex flex-row justify-content-between flex-wrap">
-                {% if getConfig('crowi', 'security:passport-google:isEnabled') %}
-                <div class="input-group justify-content-center d-flex mt-5">
-                  <form role="form" action="/passport/google" class="d-inline-flex flex-column">
-                    <button type="submit" class="btn btn-fill px-0 py-2" id="google">
-                      <div class="eff"></div>
-                      <span class="btn-label p-3"><i class="fa fa-google"></i></span>
-                      <span class="btn-label-text p-3">{{ t('Sign in') }}</span>
-                    </button>
-                    <div class="small text-center">by Google Account</div>
-                  </form>
-                </div>
-                {% endif %}
-                {% if getConfig('crowi', 'security:passport-github:isEnabled') %}
-                <div class="input-group justify-content-center d-flex mt-5">
-                  <form role="form" action="/passport/github" class="d-inline-flex flex-column">
-                    <input type="hidden" name="_csrf" value="{{ csrf() }}">
-                    <button type="submit" class="btn btn-fill px-0 py-2" id="github">
-                      <div class="eff"></div>
-                      <span class="btn-label p-3"><i class="fa fa-github"></i></span>
-                      <span class="btn-label-text p-3">{{ t('Sign in') }}</span>
-                    </button>
-                    <div class="small text-center">by GitHub Account</div>
-                  </form>
-                </div>
-                {% endif %}
-                {% if getConfig('crowi', 'security:passport-facebook:isEnabled') %}
-                <div class="input-group justify-content-center d-flex mt-5">
-                  <form role="form" action="/passport/facebook" class="d-inline-flex flex-column">
-                    <input type="hidden" name="_csrf" value="{{ csrf() }}">
-                    <button type="submit" class="btn btn-fill px-0 py-2" id="facebook">
-                      <div class="eff"></div>
-                      <span class="btn-label p-3"><i class="fa fa-facebook"></i></span>
-                      <span class="btn-label-text p-3">{{ t('Sign in') }}</span>
-                    </button>
-                    <div class="small text-center">by Facebook Account</div>
-                  </form>
-                </div>
-                {% endif %}
-                {% if getConfig('crowi', 'security:passport-twitter:isEnabled') %}
-                <div class="input-group justify-content-center d-flex mt-5">
-                  <form role="form" action="/passport/twitter" class="d-inline-flex flex-column">
-                    <input type="hidden" name="_csrf" value="{{ csrf() }}">
-                    <button type="submit" class="btn btn-fill px-0 py-2" id="twitter">
-                      <div class="eff"></div>
-                      <span class="btn-label p-3"><i class="fa fa-twitter"></i></span>
-                      <span class="btn-label-text p-3">{{ t('Sign in') }}</span>
-                    </button>
-                    <div class="small text-center">by Twitter Account</div>
-                  </form>
-                </div>
-                {% endif %}
-                {% if getConfig('crowi', 'security:passport-oidc:isEnabled') %}
-                <div class="input-group justify-content-center d-flex mt-5">
-                  <form role="form" action="/passport/oidc" class="d-inline-flex flex-column">
-                    <input type="hidden" name="_csrf" value="{{ csrf() }}">
-                    <button type="submit" class="btn btn-fill px-0 py-2" id="oidc">
-                      <div class="eff"></div>
-                      <span class="btn-label p-3"><i class="fa fa-openid"></i></span>
-                      <span class="btn-label-text p-3">{{ t('Sign in') }}</span>
-                    </button>
-                    <div class="small text-center">{{ getConfig('crowi', 'security:passport-oidc:providerName') || "OpenID Connect" }}</div>
-                  </form>
-                </div>
-                {% endif %}
-                {% if getConfig('crowi', 'security:passport-saml:isEnabled') %}
-                <div class="input-group justify-content-center d-flex mt-5">
-                  <form role="form" action="/passport/saml" class="d-inline-flex flex-column">
-                    <input type="hidden" name="_csrf" value="{{ csrf() }}">
-                    <button type="submit" class="btn btn-fill px-0 py-2" id="saml">
-                      <div class="eff"></div>
-                      <span class="btn-label p-3"><i class="fa fa-key"></i></span>
-                      <span class="btn-label-text p-3">{{ t('Sign in') }}</span>
-                    </button>
-                    <div class="small text-center">with SAML</div>
-                  </form>
-                </div>
-                {% endif %}
-                {% if getConfig('crowi', 'security:passport-basic:isEnabled') %}
-                <div class="input-group justify-content-center d-flex mt-5">
-                  <form role="form" action="/passport/basic" class="d-inline-flex flex-column">
-                    <input type="hidden" name="_csrf" value="{{ csrf() }}">
-                    <button type="submit" class="btn btn-fill px-0 py-2" id="basic">
-                      <div class="eff"></div>
-                      <span class="btn-label p-3"><i class="fa fa-lock"></i></span>
-                      <span class="btn-label-text p-3">{{ t('Sign in') }}</span>
-                    </button>
-                    <div class="small text-center">with Basic Auth</div>
-                  </form>
-                </div>
-              {% endif %}
-              </div>{# ./d-flex flex-row flex-wrap #}
-              <div class="spacer"></div>
-            </div>
-            <div class="border-bottom"></div>
-            <div class="text-center">
-              <button class="collapse-anchor btn btn-xs btn-collapse-external-auth mb-3"
-                  data-toggle="{% if isExternalAuthCollapsible %}collapse{% endif %}" data-target="#external-auth" aria-expanded="false" aria-controls="external-auth">
-                External Auth
-              </button>
-            </div>
-            {% else %}
-            <div class="border-bottom mb-3"></div>
-            {% endif %}
-
-            {% if isExternalAuthCollapsible %}
-            <script>
-              const isMobile = /iphone|ipad|android/.test(window.navigator.userAgent.toLowerCase());
-
-              if (!isMobile) {
-                $(".collapse-anchor").hover(
-                  function() {
-                    $('.collapse-external-auth').collapse('show');
-                  },
-                  function() {
-                    $('.collapse-external-auth').collapse('hide');
-                  }
-                );
-              }
-            </script>
-            {% endif %}
-
-            <div class="row">
-              <div class="col-12 text-right py-2">
-                {% if isRegistrationEnabled %}
-                <a href="#register" id="register" class="link-switch">
-                  <i class="ti-check-box"></i> {{ t('Sign up is here') }}
-                </a>
-                {% else %}
-                &nbsp;
-                {% endif %}
-              </div>
-            </div>
 
-          </div>
-
-          {% if isRegistrationEnabled %}
-          <div class="back">
-            {% if getConfig('crowi', 'security:registrationMode') == 'Restricted' %}
-            <p class="alert alert-warning">
-              {{ t('page_register.notice.restricted') }}<br>
-              {{ t('page_register.notice.restricted_defail') }}
-            </p>
-            {% endif %}
-
-            <form role="form" action="/register" method="post" id="register-form">
-              <div class="input-group" id="input-group-username">
-                <div class="input-group-prepend">
-                  <span class="input-group-text"><i class="icon-user"></i></span>
-                </div>
-                <input type="text" class="form-control" placeholder="{{ t('User ID') }}" name="registerForm[username]" value="{{ req.body.registerForm.username }}" required>
-              </div>
-              <p class="form-text text-danger">
-                <span id="help-block-username"></span>
-              </p>
-
-              <div class="input-group">
-                <div class="input-group-prepend">
-                  <span class="input-group-text"><i class="icon-tag"></i></span>
-                </div>
-                <input type="text" class="form-control" placeholder="{{ t('Name') }}" name="registerForm[name]" value="{{ req.body.registerForm.name }}" required>
-              </div>
-
-              <div class="input-group">
-                <div class="input-group-prepend">
-                  <span class="input-group-text"><i class="icon-envelope"></i></span>
-                </div>
-                <input type="email" class="form-control" placeholder="{{ t('Email') }}" name="registerForm[email]" value="{{ req.body.registerForm.email }}" required>
-              </div>
-              {% if getConfig('crowi', 'security:registrationWhiteList') && getConfig('crowi', 'security:registrationWhiteList').length %}
-              <p class="form-text">
-                {{ t('page_register.form_help.email') }}
-              </p>
-              <ul>
-                {% for em in getConfig('crowi', 'security:registrationWhiteList') %}
-                <li><code>{{ em }}</code></li>
-                {% endfor %}
-              </ul>
-              {% endif %}
-
-              <div class="input-group">
-                <div class="input-group-prepend">
-                  <span class="input-group-text"><i class="icon-lock"></i></span>
-                </div>
-                <input type="password" class="form-control" placeholder="{{ t('Password') }}" name="registerForm[password]" required>
-              </div>
-
-              <div class="input-group justify-content-center mt-5">
-                <input type="hidden" name="_csrf" value="{{ csrf() }}">
-                <button type="submit" class="btn btn-fill px-0 py-2" id="register">
-                  <div class="eff"></div>
-                  <span class="btn-label p-3"><i class="icon-user-follow"></i></span>
-                  <span class="btn-label-text p-3">{{ t('Sign up') }}</span>
-                </button>
-              </div>
-
-            </form>
-
-            <div class="border-bottom mb-3"></div>
-
-            <div class="row">
-              <div class="text-right col-12 py-1">
-                <a href="#login" id="login" class="link-switch">
-                  <i class="icon-fw icon-login"></i>{{ t('Sign in is here') }}
-                </a>
-              </div>
-            </div>
-          </div>
-
-          {% endif %} {# if isRegistrationEnabled id false #}
-        </div>
-
-        <a href="https://growi.org" class="link-growi-org pl-3">
-          <span class="growi">GROWI</span>.<span class="org">ORG
-        </a>
-
-      </div>
+      {% set registrationMode = getConfig('crowi', 'security:registrationMode') %}
+      {% set isRegistrationEnabled = passportService.isLocalStrategySetup && registrationMode != 'Closed' %}
+
+      <div
+        id="login-form"
+        data-is-registering="{{ req.query.register or req.body.registerForm or isRegistering }}"
+        data-username ="{{ req.body.registerForm.username }}"
+        data-name ="{{ req.body.registerForm.name }}"
+        data-email ="{{ req.body.registerForm.email }}"
+        data-is-registration-enabled="{{ isRegistrationEnabled }}"
+        data-registration-mode = "{{ registrationMode }}"
+        data-registration-white-list = "{{ getConfig('crowi', 'security:registrationWhiteList') }}"
+        data-is-local-strategy-setup = "{{ passportService.isLocalStrategySetup }}"
+        data-is-ldap-strategy-setup = "{{ passportService.isLdapStrategySetup}}"
+        data-is-google-auth-enabled = "{{ getConfig('crowi', 'security:passport-google:isEnabled') }}"
+        data-is-github-auth-enabled = "{{ getConfig('crowi', 'security:passport-github:isEnabled') }}"
+        data-is-facebook-auth-enabled = "{{ getConfig('crowi', 'security:passport-facebook:isEnabled') }}"
+        data-is-twitter-auth-enabled = "{{ getConfig('crowi', 'security:passport-twitter:isEnabled') }}"
+        data-is-saml-auth-enabled = "{{ getConfig('crowi', 'security:passport-saml:isEnabled') }}"
+        data-is-oidc-auth-enabled = "{{ getConfig('crowi', 'security:passport-oidc:isEnabled') }}"
+        data-is-basic-auth-enabled = "{{ getConfig('crowi', 'security:passport-basic:isEnabled') }}"
+      ></div>
     </div>
   </div>
 </div>
@@ -391,16 +134,6 @@
 
 {% block body_end %}
 <script>
-  // login
-  $('#register').on('click', function() {
-    $('#login-dialog').addClass('to-flip');
-    return false;
-  });
-  $('#login').on('click', function() {
-    $('#login-dialog').removeClass('to-flip');
-    return false;
-  });
-
   $('#register-form input[name="registerForm[username]"]').change(function(e) {
     var username = $(this).val();
     $('#login-dialog').removeClass('has-error');

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

@@ -26,7 +26,7 @@
 
   <div class="row">
 
-    <div class="login-header col-sm-offset-4 col-sm-4">
+    <div class="login-header offset-sm-4 col-sm-4">
       <div class="logo">{% include '../widget/logo.html' %}</div>
       <h1>GROWI</h1>
 

+ 1 - 1
src/server/views/tags.html

@@ -5,7 +5,7 @@
 {% block html_base_css %}tags-page{% endblock %}
 
 {% block layout_main %}
-<header class="py-0 position-sticky grw-header">
+<header class="py-0 grw-header">
   <h1 class="title">{{ t('Tags') }}</h1>
 </header>
 

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

@@ -20,7 +20,7 @@
     </div>
   </div>
 
-  <ul class="nav nav-tabs hidden-print" role="tablist">
+  <ul class="nav nav-tabs d-print-none" role="tablist">
     <li class="nav-item grw-nav-main-left-tab">
       <a class="nav-link active" role="tab" href="#revision-body" data-toggle="tab">
         <i class="icon-notebook"></i> List

+ 3 - 3
src/server/views/widget/not_creatable_content.html

@@ -12,9 +12,9 @@
   data-current-user="{% if user %}{{ user._id.toString() }}{% endif %}"
   >
 
-  <ul class="nav nav-tabs hidden-print">
-    <li class="grw-nav-main-left-tab active">
-      <a href="#revision-body" data-toggle="tab">
+  <ul class="nav nav-tabs d-print-none" role="tablist">
+    <li class="nav-item grw-nav-main-left-tab active" role="presentation">
+      <a class="nav-link active" role="tab" href="#revision-body" data-toggle="tab">
         <i class="icon-notebook"></i> List
       </a>
     </li>

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

@@ -1,4 +1,4 @@
-<ul class="nav nav-tabs hidden-print" role="tablist">
+<ul class="nav nav-tabs d-print-none" role="tablist">
   <li class="nav-item grw-nav-main-left-tab">
     <a class="nav-link active" role="tab" href="#revision-body" data-toggle="tab">
       <i class="icon-notebook"></i> List

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

@@ -39,7 +39,7 @@
 
       {# formatted text #}
       <div class="tab-pane active" id="revision-body">
-        <div class="revision-toc hidden-print" id="revision-toc">
+        <div class="revision-toc d-print-none" id="revision-toc">
           <a data-toggle="collapse" data-parent="#revision-toc" href="#revision-toc-content" class="revision-toc-head">{{ t('Table of Contents') }}</a>
           <div id="revision-toc-content" class="revision-toc-content collapse in"></div>
         </div>

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

@@ -1,5 +1,5 @@
 {% if page %}
-<ul class="nav nav-tabs hidden-print" role="tablist">
+<ul class="nav nav-tabs d-print-none" role="tablist">
 
   {#
     Left Tabs

+ 5 - 0
yarn.lock

@@ -11677,6 +11677,11 @@ react-bootstrap-typeahead@^3.4.7:
     react-popper "^1.0.0"
     warning "^4.0.1"
 
+react-card-flip@^1.0.10:
+  version "1.0.10"
+  resolved "https://registry.yarnpkg.com/react-card-flip/-/react-card-flip-1.0.10.tgz#f3eab968f2cba6de6eccb84cf73bcaf6b53fb974"
+  integrity sha512-BqK6PmP+L/xmcH1AoMuirbxRuDIiaNy3r8734GJQqEyIWoW8L4j2c/di6mbNg+I2rGue3tLH1I9QbJLd7M89ww==
+
 react-clientside-effect@^1.2.0:
   version "1.2.2"
   resolved "https://registry.yarnpkg.com/react-clientside-effect/-/react-clientside-effect-1.2.2.tgz#6212fb0e07b204e714581dd51992603d1accc837"