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

Merge branch 'master' into imprv/gw-4349-me-setting-layout

# Conflicts:
#	src/client/js/components/Me/PersonalSettings.jsx
kaori 5 лет назад
Родитель
Сommit
4c29af6bd3
38 измененных файлов с 408 добавлено и 167 удалено
  1. 15 0
      .github/workflows/release-rc.yml
  2. 24 7
      .github/workflows/release.yml
  3. 5 0
      CHANGES.md
  4. 1 0
      src/client/js/app.jsx
  5. 29 6
      src/client/js/components/Admin/Notification/NotificationSetting.jsx
  6. 42 12
      src/client/js/components/Admin/Security/SecurityManagementContents.jsx
  7. 9 1
      src/client/js/components/CustomNavigation.jsx
  8. 18 0
      src/client/js/components/Drawio.jsx
  9. 12 4
      src/client/js/components/Fab.jsx
  10. 0 5
      src/client/js/components/Icons/LooockIcon.jsx
  11. 0 5
      src/client/js/components/Icons/PaperPlaneIcon.jsx
  12. 0 5
      src/client/js/components/Icons/ShareAltIcon.jsx
  13. 0 5
      src/client/js/components/Icons/UserIcon.jsx
  14. 24 29
      src/client/js/components/Me/PersonalSettings.jsx
  15. 18 15
      src/client/js/components/NotFoundPage.jsx
  16. 16 3
      src/client/js/components/Page/NotFoundAlert.jsx
  17. 1 1
      src/client/js/components/PageAccessoriesModal.jsx
  18. 1 1
      src/client/js/components/ShareLink/ShareLinkForm.jsx
  19. 11 9
      src/client/js/components/TrashPageList.jsx
  20. 3 1
      src/client/js/legacy/crowi.js
  21. 4 0
      src/client/js/services/NavigationContainer.js
  22. 13 0
      src/client/js/services/PageContainer.js
  23. 2 2
      src/client/styles/scss/_admin.scss
  24. 1 1
      src/client/styles/scss/_layout.scss
  25. 1 0
      src/client/styles/scss/_page-accessories-control.scss
  26. 9 0
      src/client/styles/scss/atoms/_buttons.scss
  27. 57 26
      src/client/styles/scss/theme/_apply-colors-dark.scss
  28. 55 14
      src/client/styles/scss/theme/_apply-colors-light.scss
  29. 2 6
      src/client/styles/scss/theme/_apply-colors.scss
  30. 1 1
      src/client/styles/scss/theme/default.scss
  31. 1 1
      src/client/styles/scss/theme/halloween.scss
  32. 1 0
      src/client/styles/scss/theme/mixins/_list-group.scss
  33. 4 0
      src/client/styles/scss/theme/spring.scss
  34. 16 0
      src/lib/util/locale-utils.js
  35. 5 1
      src/server/models/user.js
  36. 3 3
      src/server/views/layout-growi/base/layout.html
  37. 3 2
      src/server/views/widget/headers/drawio.html
  38. 1 1
      src/server/views/widget/not_creatable_content.html

+ 15 - 0
.github/workflows/release-rc.yml

@@ -45,6 +45,21 @@ jobs:
         semver: ${{ env.SEMVER }}
         publish: true
 
+    - name: Login to GitHub Container Registry
+      uses: docker/login-action@v1
+      with:
+        registry: ghcr.io
+        username: wsmoogle
+        password: ${{ secrets.DOCKER_REGISTRY_ON_GITHUB_PASSWORD }}
+
+    - name: Docker Tags by SemVer in Github Container Registry
+      uses: weseek/ghaction-docker-tags-by-semver@v1.0.3
+      with:
+        source: growi
+        target: ghcr.io/weseek/growi
+        semver: ${{ env.SEMVER }}
+        publish: true
+
     - name: Check whether workspace is clean
       run: |
         STATUS=`git status --porcelain`

+ 24 - 7
.github/workflows/release.yml

@@ -97,13 +97,6 @@ jobs:
         additional-tags: 'latest'
         publish: true
 
-    - name: Slack Notification
-      uses: weseek/ghaction-release-slack-notification@master
-      with:
-        channel: '#general'
-        url: ${{ secrets.SLACK_WEBHOOK_URL }}
-        created_tag: 'v${{ needs.github-release.outputs.RELEASE_VERSION }}${{ env.SUFFIX }}'
-
     - name: Update Docker Hub Description
       uses: peter-evans/dockerhub-description@v2
       with:
@@ -112,6 +105,30 @@ jobs:
         repository: weseek/growi
         readme-filepath: ./docker/README.md
 
+    - name: Login to GitHub Container Registry
+      uses: docker/login-action@v1
+      with:
+        registry: ghcr.io
+        username: wsmoogle
+        password: ${{ secrets.DOCKER_REGISTRY_ON_GITHUB_PASSWORD }}
+
+    - name: Docker Tags by SemVer in Github Container Registry
+      uses: weseek/ghaction-docker-tags-by-semver@v1.0.3
+      with:
+        source: growi${{ env.SUFFIX }}
+        target: ghcr.io/weseek/growi
+        semver: ${{ needs.github-release.outputs.RELEASE_VERSION }}
+        suffix: ${{ env.SUFFIX }}
+        additional-tags: 'latest'
+        publish: true
+
+    - name: Slack Notification
+      uses: weseek/ghaction-release-slack-notification@master
+      with:
+        channel: '#general'
+        url: ${{ secrets.SLACK_WEBHOOK_URL }}
+        created_tag: 'v${{ needs.github-release.outputs.RELEASE_VERSION }}${{ env.SUFFIX }}'
+
     - name: Check whether workspace is clean
       run: |
         STATUS=`git status --porcelain`

+ 5 - 0
CHANGES.md

@@ -15,6 +15,11 @@
     * migrate-mongo
     * mongoose
 
+## v4.1.11-RC
+
+* Improvement: Generating draft DOM id strategy
+* Fix: GROWI version downgrade causes a validation error for user.lang
+
 ## v4.1.10
 
 * Fix: Make listing users API secure

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

@@ -89,6 +89,7 @@ Object.assign(componentMappings, {
 
   'not-found-alert': <NotFoundAlert
     onPageCreateClicked={navigationContainer.setEditorMode}
+    isGuestUserMode={appContainer.currentUser == null}
     isHidden={pageContainer.state.isForbidden || pageContainer.state.isNotCreatable || pageContainer.state.isTrashPage}
   />,
 

+ 29 - 6
src/client/js/components/Admin/Notification/NotificationSetting.jsx

@@ -1,8 +1,9 @@
-import React, { useMemo } from 'react';
+import React, { useMemo, useState } from 'react';
 import PropTypes from 'prop-types';
 
 import loggerFactory from '@alias/logger';
 
+import { TabContent, TabPane } from 'reactstrap';
 import { withUnstatedContainers } from '../../UnstatedUtils';
 import { toastError } from '../../../util/apiNotification';
 import toArrayIfNot from '../../../../../lib/util/toArrayIfNot';
@@ -10,7 +11,7 @@ import { withLoadingSppiner } from '../../SuspenseUtils';
 
 import AdminNotificationContainer from '../../../services/AdminNotificationContainer';
 
-import CustomNavigation from '../../CustomNavigation';
+import { CustomNav } from '../../CustomNavigation';
 
 import SlackAppConfiguration from './SlackAppConfiguration';
 import UserTriggerNotification from './UserTriggerNotification';
@@ -21,6 +22,15 @@ const logger = loggerFactory('growi:NotificationSetting');
 let retrieveErrors = null;
 function NotificationSetting(props) {
   const { adminNotificationContainer } = props;
+
+  const [activeTab, setActiveTab] = useState('slack_configuration');
+  const [activeComponents, setActiveComponents] = useState(new Set(['slack_configuration']));
+
+  const switchActiveTab = (selectedTab) => {
+    setActiveTab(selectedTab);
+    setActiveComponents(activeComponents.add(selectedTab));
+  };
+
   if (adminNotificationContainer.state.webhookUrl === adminNotificationContainer.dummyWebhookUrl) {
     throw (async() => {
       try {
@@ -44,26 +54,39 @@ function NotificationSetting(props) {
     return {
       slack_configuration: {
         Icon: () => <i className="icon-settings" />,
-        Content: SlackAppConfiguration,
         i18n: 'Slack configuration',
         index: 0,
       },
       user_trigger_notification: {
         Icon: () => <i className="icon-settings" />,
-        Content: UserTriggerNotification,
         i18n: 'User trigger notification',
         index: 1,
       },
       global_notification: {
         Icon: () => <i className="icon-settings" />,
-        Content: GlobalNotification,
         i18n: 'Global notification',
         index: 2,
       },
     };
   }, []);
 
-  return <CustomNavigation navTabMapping={navTabMapping} />;
+  return (
+    <>
+      <CustomNav activeTab={activeTab} navTabMapping={navTabMapping} onNavSelected={switchActiveTab} hideBorderBottom />
+
+      <TabContent activeTab={activeTab} className="p-5">
+        <TabPane tabId="slack_configuration">
+          {activeComponents.has('slack_configuration') && <SlackAppConfiguration />}
+        </TabPane>
+        <TabPane tabId="user_trigger_notification">
+          {activeComponents.has('user_trigger_notification') && <UserTriggerNotification />}
+        </TabPane>
+        <TabPane tabId="global_notification">
+          {activeComponents.has('global_notification') && <GlobalNotification />}
+        </TabPane>
+      </TabContent>
+    </>
+  );
 }
 
 const NotificationSettingWithUnstatedContainer = withUnstatedContainers(withLoadingSppiner(NotificationSetting), [AdminNotificationContainer]);

+ 42 - 12
src/client/js/components/Admin/Security/SecurityManagementContents.jsx

@@ -1,7 +1,9 @@
-import React, { Fragment, useMemo } from 'react';
+import React, { Fragment, useMemo, useState } from 'react';
 import PropTypes from 'prop-types';
 import { withTranslation } from 'react-i18next';
 
+import { TabContent, TabPane } from 'reactstrap';
+
 import LdapSecuritySetting from './LdapSecuritySetting';
 import LocalSecuritySetting from './LocalSecuritySetting';
 import SamlSecuritySetting from './SamlSecuritySetting';
@@ -14,64 +16,63 @@ import TwitterSecuritySetting from './TwitterSecuritySetting';
 import FacebookSecuritySetting from './FacebookSecuritySetting';
 import ShareLinkSetting from './ShareLinkSetting';
 
-import CustomNavigation from '../../CustomNavigation';
+import { CustomNav } from '../../CustomNavigation';
 
 function SecurityManagementContents(props) {
   const { t } = props;
 
+  const [activeTab, setActiveTab] = useState('passport_local');
+  const [activeComponents, setActiveComponents] = useState(new Set(['passport_local']));
+
+  const switchActiveTab = (selectedTab) => {
+    setActiveTab(selectedTab);
+    setActiveComponents(activeComponents.add(selectedTab));
+  };
+
   const navTabMapping = useMemo(() => {
     return {
       passport_local: {
         Icon: () => <i className="fa fa-users" />,
-        Content: LocalSecuritySetting,
         i18n: 'ID/Pass',
         index: 0,
       },
       passport_ldap: {
         Icon: () => <i className="fa fa-sitemap" />,
-        Content: LdapSecuritySetting,
         i18n: 'LDAP',
         index: 1,
       },
       passport_saml: {
         Icon: () => <i className="fa fa-key" />,
-        Content: SamlSecuritySetting,
         i18n: 'SAML',
         index: 2,
       },
       passport_oidc: {
         Icon: () => <i className="fa fa-key" />,
-        Content: OidcSecuritySetting,
         i18n: 'OIDC',
         index: 3,
       },
       passport_basic: {
         Icon: () => <i className="fa fa-lock" />,
-        Content: BasicSecuritySetting,
         i18n: 'BASIC',
         index: 4,
       },
       passport_google: {
         Icon: () => <i className="fa fa-google" />,
-        Content: GoogleSecuritySetting,
         i18n: 'Google',
         index: 5,
       },
       passport_github: {
         Icon: () => <i className="fa fa-github" />,
-        Content: GitHubSecuritySetting,
         i18n: 'GitHub',
         index: 6,
       },
       passport_twitter: {
         Icon: () => <i className="fa fa-twitter" />,
-        Content: TwitterSecuritySetting,
         i18n: 'Twitter',
         index: 7,
       },
       passport_facebook: {
         Icon: () => <i className="fa fa-facebook" />,
-        Content: FacebookSecuritySetting,
         i18n: '(TBD) Facebook',
         index: 8,
       },
@@ -103,7 +104,36 @@ function SecurityManagementContents(props) {
 
       <div className="auth-mechanism-configurations">
         <h2 className="border-bottom">{t('security_setting.Authentication mechanism settings')}</h2>
-        <CustomNavigation navTabMapping={navTabMapping} />
+        <CustomNav activeTab={activeTab} navTabMapping={navTabMapping} onNavSelected={switchActiveTab} hideBorderBottom />
+        <TabContent activeTab={activeTab} className="p-5">
+          <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>
   );

+ 9 - 1
src/client/js/components/CustomNavigation.jsx

@@ -12,7 +12,9 @@ export const CustomNav = (props) => {
   const [sliderWidth, setSliderWidth] = useState(0);
   const [sliderMarginLeft, setSliderMarginLeft] = useState(0);
 
-  const { activeTab, navTabMapping, onNavSelected } = props;
+  const {
+    activeTab, navTabMapping, onNavSelected, hideBorderBottom,
+  } = props;
 
   const navTabRefs = useMemo(() => {
     const obj = {};
@@ -87,6 +89,7 @@ export const CustomNav = (props) => {
         </Nav>
       </div>
       <hr className="my-0 grw-nav-slide-hr border-none" style={{ width: `${sliderWidth}%`, marginLeft: `${sliderMarginLeft}%` }} />
+      { !hideBorderBottom && <hr className="my-0 border-top-0 border-bottom" /> }
     </div>
   );
 
@@ -96,6 +99,11 @@ CustomNav.propTypes = {
   activeTab: PropTypes.string.isRequired,
   navTabMapping: PropTypes.object.isRequired,
   onNavSelected: PropTypes.func,
+  hideBorderBottom: PropTypes.bool,
+};
+
+CustomNav.defaultProps = {
+  hideBorderBottom: false,
 };
 
 

+ 18 - 0
src/client/js/components/Drawio.jsx

@@ -1,6 +1,8 @@
 import React from 'react';
 import PropTypes from 'prop-types';
 
+import { debounce } from 'throttle-debounce';
+
 import { withTranslation } from 'react-i18next';
 
 import AppContainer from '../services/AppContainer';
@@ -26,6 +28,9 @@ class Drawio extends React.Component {
     this.drawioContent = this.props.drawioContent;
 
     this.onEdit = this.onEdit.bind(this);
+
+    // create debounced method for rendering Drawio
+    this.renderDrawioWithDebounce = debounce(200, this.renderDrawio);
   }
 
   onEdit() {
@@ -35,6 +40,16 @@ class Drawio extends React.Component {
   }
 
   componentDidMount() {
+    const DrawioViewer = window.GraphViewer;
+    if (DrawioViewer != null) {
+      this.renderDrawio();
+    }
+    else {
+      this.renderDrawioWithDebounce();
+    }
+  }
+
+  renderDrawio() {
     const DrawioViewer = window.GraphViewer;
     if (DrawioViewer != null) {
       const mxgraphs = this.drawioContainer.getElementsByClassName('mxgraph');
@@ -48,6 +63,9 @@ class Drawio extends React.Component {
         }
       }
     }
+    else {
+      this.renderDrawioWithDebounce();
+    }
   }
 
   renderContents() {

+ 12 - 4
src/client/js/components/Fab.jsx

@@ -17,13 +17,17 @@ const Fab = (props) => {
   const { currentUser } = appContainer;
 
   const [animateClasses, setAnimateClasses] = useState('invisible');
+  const [buttonClasses, setButtonClasses] = useState('');
 
 
   const stickyChangeHandler = useCallback((event) => {
     logger.debug('StickyEvents.CHANGE detected');
 
-    const classes = event.detail.isSticky ? 'animated fadeInUp faster' : 'animated fadeOut faster';
-    setAnimateClasses(classes);
+    const newAnimateClasses = event.detail.isSticky ? 'animated fadeInUp faster' : 'animated fadeOut faster';
+    const newButtonClasses = event.detail.isSticky ? '' : 'disabled grw-pointer-events-none';
+
+    setAnimateClasses(newAnimateClasses);
+    setButtonClasses(newButtonClasses);
   }, []);
 
   // setup effect by sticky event
@@ -47,7 +51,7 @@ const Fab = (props) => {
         <div className={`rounded-circle position-absolute ${animateClasses}`} style={{ bottom: '2.3rem', right: '4rem' }}>
           <button
             type="button"
-            className="btn btn-lg btn-create-page btn-primary rounded-circle p-0 waves-effect waves-light"
+            className={`btn btn-lg btn-create-page btn-primary rounded-circle p-0 waves-effect waves-light ${buttonClasses}`}
             onClick={navigationContainer.openPageCreateModal}
           >
             <CreatePageIcon />
@@ -61,7 +65,11 @@ const Fab = (props) => {
     <div className="grw-fab d-none d-md-block">
       {currentUser != null && renderPageCreateButton()}
       <div className={`rounded-circle position-absolute ${animateClasses}`} style={{ bottom: 0, right: 0 }}>
-        <button type="button" className="btn btn-light btn-scroll-to-top rounded-circle p-0" onClick={() => navigationContainer.smoothScrollIntoView()}>
+        <button
+          type="button"
+          className={`btn btn-light btn-scroll-to-top rounded-circle p-0 ${buttonClasses}`}
+          onClick={() => navigationContainer.smoothScrollIntoView()}
+        >
           <ReturnTopIcon />
         </button>
       </div>

+ 0 - 5
src/client/js/components/Icons/LooockIcon.jsx

@@ -1,5 +0,0 @@
-import React from 'react';
-
-const LockIcon = () => <i className="icon-fw icon-lock"></i>;
-
-export default LockIcon;

+ 0 - 5
src/client/js/components/Icons/PaperPlaneIcon.jsx

@@ -1,5 +0,0 @@
-import React from 'react';
-
-const PaperPlaneIcon = () => <i className="icon-fw icon-paper-plane"></i>;
-
-export default PaperPlaneIcon;

+ 0 - 5
src/client/js/components/Icons/ShareAltIcon.jsx

@@ -1,5 +0,0 @@
-import React from 'react';
-
-const ShareAltIcon = () => <i className="icon-fw icon-share-alt"></i>;
-
-export default ShareAltIcon;

+ 0 - 5
src/client/js/components/Icons/UserIcon.jsx

@@ -1,5 +0,0 @@
-import React from 'react';
-
-const UserIcon = () => <i className="icon-fw icon-user"></i>;
-
-export default UserIcon;

+ 24 - 29
src/client/js/components/Me/PersonalSettings.jsx

@@ -1,5 +1,5 @@
 
-import React from 'react';
+import React, { useMemo } from 'react';
 import PropTypes from 'prop-types';
 import { withTranslation } from 'react-i18next';
 import CustomNavigation from '../CustomNavigation';
@@ -8,61 +8,56 @@ import PasswordSettings from './PasswordSettings';
 import ExternalAccountLinkedMe from './ExternalAccountLinkedMe';
 import ApiSettings from './ApiSettings';
 
-import UserIcon from '../Icons/UserIcon';
-import ShareAltIcon from '../Icons/ShareAltIcon';
-import LockIcon from '../Icons/LooockIcon';
-import PaperPlaneIcon from '../Icons/PaperPlaneIcon';
+const PersonalSettings = (props) => {
 
-class PersonalSettings extends React.Component {
+  const { t } = props;
 
-  render() {
-    const { t } = this.props;
-
-    const navTabMapping = {
+  const navTabMapping = useMemo(() => {
+    return {
       user_infomation: {
-        Icon: UserIcon,
+        Icon: () => <i className="icon-fw icon-user"></i>,
         Content: UserSettings,
         i18n: t('User Information'),
         index: 0,
       },
       external_accounts: {
-        Icon: ShareAltIcon,
+        Icon: () => <i className="icon-fw icon-share-alt"></i>,
         Content: ExternalAccountLinkedMe,
         i18n: t('admin:user_management.external_accounts'),
         index: 1,
       },
       password_settings: {
-        Icon: LockIcon,
+        Icon: () => <i className="icon-fw icon-lock"></i>,
         Content: PasswordSettings,
         i18n: t('Password Settings'),
         index: 2,
       },
       api_settings: {
-        Icon: PaperPlaneIcon,
+        Icon: () => <i className="icon-fw icon-paper-plane"></i>,
         Content: ApiSettings,
         i18n: t('API Settings'),
         index: 3,
       },
     };
+  }, [t]);
 
 
-    return (
-      <>
-        <header className="py-3">
-          <div className="container-fluid">
-            <h1 className="title">{ t('User Settings') }</h1>
-          </div>
-        </header>
-        <div id="main" className="main">
-          <div id="content-main" className="content-main container-lg">
-            <CustomNavigation navTabMapping={navTabMapping} tabContentClasses={['px-0']} />
-          </div>
+  return (
+    <>
+      <header className="py-3">
+        <div className="container-fluid">
+          <h1 className="title">{ t('User Settings') }</h1>
+        </div>
+      </header>
+      <div id="main" className="main">
+        <div id="content-main" className="content-main container-lg">
+          <CustomNavigation navTabMapping={navTabMapping} tabContentClasses={['px-0']} />
         </div>
-      </>
-    );
-  }
+      </div>
+    </>
+  );
 
-}
+};
 
 PersonalSettings.propTypes = {
   t: PropTypes.func.isRequired, // i18next

+ 18 - 15
src/client/js/components/NotFoundPage.jsx

@@ -1,4 +1,4 @@
-import React from 'react';
+import React, { useMemo } from 'react';
 import PropTypes from 'prop-types';
 import { withTranslation } from 'react-i18next';
 import PageListIcon from './Icons/PageListIcon';
@@ -10,20 +10,23 @@ import PageTimeline from './PageTimeline';
 const NotFoundPage = (props) => {
   const { t } = props;
 
-  const navTabMapping = {
-    pagelist: {
-      Icon: PageListIcon,
-      Content: PageList,
-      i18n: t('page_list'),
-      index: 0,
-    },
-    timeLine: {
-      Icon: TimeLineIcon,
-      Content: PageTimeline,
-      i18n: t('Timeline View'),
-      index: 1,
-    },
-  };
+  const navTabMapping = useMemo(() => {
+    return {
+      pagelist: {
+        Icon: PageListIcon,
+        Content: PageList,
+        i18n: t('page_list'),
+        index: 0,
+      },
+      timeLine: {
+        Icon: TimeLineIcon,
+        Content: PageTimeline,
+        i18n: t('Timeline View'),
+        index: 1,
+      },
+    };
+  }, [t]);
+
 
   return (
     <div className="mt-5 d-edit-none">

+ 16 - 3
src/client/js/components/Page/NotFoundAlert.jsx

@@ -1,9 +1,11 @@
 import React from 'react';
 import PropTypes from 'prop-types';
 import { withTranslation } from 'react-i18next';
+import { UncontrolledTooltip } from 'reactstrap';
+
 
 const NotFoundAlert = (props) => {
-  const { t, isHidden } = props;
+  const { t, isHidden, isGuestUserMode } = props;
   function clickHandler(viewType) {
     if (props.onPageCreateClicked === null) {
       return;
@@ -15,21 +17,31 @@ const NotFoundAlert = (props) => {
     return null;
   }
 
+
   return (
     <div className="border border-info p-3">
-      <div className="col-md-12 p-0">
+      <div
+        className="col-md-12 p-0"
+      >
         <h2 className="text-info lead">
           <i className="icon-info pr-2 font-weight-bold" aria-hidden="true"></i>
           {t('not_found_page.page_not_exist_alert')}
         </h2>
         <button
+          id="create-page-btn-wrapper-for-tooltip"
           type="button"
-          className="m-1 pl-3 pr-3 btn bg-info text-white"
+          className={`m-1 pl-3 pr-3 btn bg-info text-white ${isGuestUserMode && 'disabled'}`}
           onClick={() => { clickHandler('edit') }}
         >
           <i className="icon-note icon-fw" />
           {t('not_found_page.Create Page')}
         </button>
+
+        {isGuestUserMode && (
+        <UncontrolledTooltip placement="bottom" target="create-page-btn-wrapper-for-tooltip" fade={false}>
+          {t('Not available for guest')}
+        </UncontrolledTooltip>
+      )}
       </div>
     </div>
   );
@@ -40,6 +52,7 @@ NotFoundAlert.propTypes = {
   t: PropTypes.func.isRequired, // i18next
   onPageCreateClicked: PropTypes.func,
   isHidden: PropTypes.bool.isRequired,
+  isGuestUserMode: PropTypes.bool.isRequired,
 };
 
 export default withTranslation()(NotFoundAlert);

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

@@ -94,7 +94,7 @@ const PageAccessoriesModal = (props) => {
     <React.Fragment>
       <Modal size="xl" isOpen={props.isOpen} toggle={closeModalHandler} className={`grw-page-accessories-modal ${isWindowExpanded && 'grw-modal-expanded'} `}>
         <ModalHeader className="p-0" toggle={closeModalHandler} close={buttons}>
-          <CustomNav activeTab={activeTab} navTabMapping={navTabMapping} onNavSelected={switchActiveTab} />
+          <CustomNav activeTab={activeTab} navTabMapping={navTabMapping} onNavSelected={switchActiveTab} hideBorderBottom />
         </ModalHeader>
         <ModalBody className="overflow-auto grw-modal-body-style p-0">
           {/* Do not use CustomTabContent because of performance problem:

+ 1 - 1
src/client/js/components/ShareLink/ShareLinkForm.jsx

@@ -22,7 +22,7 @@ class ShareLinkForm extends React.Component {
       numberOfDays: '7',
       description: '',
       customExpirationDate: dateFnsFormat(new Date(), 'yyyy-MM-dd'),
-      customExpirationTime: dateFnsFormat(new Date(), 'hh:mm'),
+      customExpirationTime: dateFnsFormat(new Date(), 'HH:mm'),
     };
 
     this.handleChangeExpirationType = this.handleChangeExpirationType.bind(this);

+ 11 - 9
src/client/js/components/TrashPageList.jsx

@@ -1,4 +1,4 @@
-import React from 'react';
+import React, { useMemo } from 'react';
 import PropTypes from 'prop-types';
 import { withTranslation } from 'react-i18next';
 import PageListIcon from './Icons/PageListIcon';
@@ -9,14 +9,16 @@ import PageList from './PageList';
 const TrashPageList = (props) => {
   const { t } = props;
 
-  const navTabMapping = {
-    pagelist: {
-      Icon: PageListIcon,
-      Content: PageList,
-      i18n: t('page_list'),
-      index: 0,
-    },
-  };
+  const navTabMapping = useMemo(() => {
+    return {
+      pagelist: {
+        Icon: PageListIcon,
+        Content: PageList,
+        i18n: t('page_list'),
+        index: 0,
+      },
+    };
+  }, [t]);
 
   return (
     <div className="mt-5 d-edit-none">

+ 3 - 1
src/client/js/legacy/crowi.js

@@ -156,12 +156,14 @@ Crowi.highlightSelectedSection = function(hash) {
 
 window.addEventListener('load', (e) => {
   const { appContainer } = window;
+  const pageContainer = appContainer.getContainer('PageContainer');
+  const { isEditable } = pageContainer;
 
   // hash on page
   if (window.location.hash) {
     const navigationContainer = appContainer.getContainer('NavigationContainer');
 
-    if (window.location.hash === '#edit') {
+    if (window.location.hash === '#edit' && isEditable) {
       navigationContainer.setEditorMode('edit');
 
       // focus

+ 4 - 0
src/client/js/services/NavigationContainer.js

@@ -176,6 +176,10 @@ export default class NavigationContainer extends Container {
   }
 
   openPageCreateModal() {
+    if (this.appContainer.currentUser == null) {
+      logger.warn('Please login or signup to create a new page.');
+      return;
+    }
     this.setState({ isPageCreateModalShown: true });
   }
 

+ 13 - 0
src/client/js/services/PageContainer.js

@@ -139,6 +139,19 @@ export default class PageContainer extends Container {
     return 'PageContainer';
   }
 
+
+  get isEditable() {
+    const { currentUser } = this.appContainer;
+    const {
+      isPageExist, isPageForbidden, isNotCreatable, isTrashPage,
+    } = this.state;
+
+    if (isPageExist && (currentUser != null) && !isPageForbidden && !isNotCreatable && !isTrashPage) {
+      return true;
+    }
+    return false;
+  }
+
   /**
    * initialize state for markdown data
    */

+ 2 - 2
src/client/styles/scss/_admin.scss

@@ -40,7 +40,7 @@
   }
 
   .admin-setting-header {
-    border-bottom: 1px solid #dee2e6;
+    border-bottom: 1px solid transparent;
   }
 
   .admin-security {
@@ -142,7 +142,7 @@
     // style
     .theme-option-container a {
       background-color: $gray-50;
-      border: 1px solid $gray-300;
+      border: 1px solid $border-color;
     }
     .theme-option-name {
       opacity: 0.3;

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

@@ -16,7 +16,7 @@ body {
 
 .grw-modal-head {
   font-size: 1em;
-  border-bottom: 1px solid $gray-500;
+  border-bottom: 1px solid transparent;
 }
 
 // padding settings for GrowiNavbarBottom

+ 1 - 0
src/client/styles/scss/_page-accessories-control.scss

@@ -14,6 +14,7 @@
 
   .grw-border-vr {
     height: 25px;
+    border-left: solid 1px transparent;
   }
 
   .seen-user-count {

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

@@ -91,3 +91,12 @@
   background-color: transparent;
   transition: 0.3s;
 }
+
+// define disabled button w/o pointer-events, see _override-bootstrap.scss
+.btn.disabled,
+.btn[disabled],
+fieldset[disabled] .btn {
+  &.grw-pointer-events-none {
+    pointer-events: none;
+  }
+}

+ 57 - 26
src/client/styles/scss/theme/_apply-colors-dark.scss

@@ -14,14 +14,16 @@ $bgcolor-table-hover: lighten($bgcolor-table, 7.5%) !default;
 $bgcolor-sidebar-list-group: $bgcolor-list !default;
 $color-tags: #949494 !default;
 $bgcolor-tags: $dark !default;
+$border-color-global: $gray-500 !default;
+$border-color-toc: $border-color-global !default;
 
 // override bootstrap variables
-$border-color: $gray-700;
 $table-dark-color: $color-table;
 $table-dark-bg: $bgcolor-table;
 $table-dark-border-color: $border-color-table;
 $table-dark-hover-color: $color-table-hover;
 $table-dark-hover-bg: $bgcolor-table-hover;
+$border-color: $border-color-global;
 
 @import 'reboot-bootstrap-border-colors';
 @import 'reboot-bootstrap-tables';
@@ -37,6 +39,7 @@ select.form-control,
 textarea.form-control {
   color: lighten($color-global, 30%);
   background-color: darken($bgcolor-global, 5%);
+  border-color: $border-color-global;
   &:focus {
     background-color: $bgcolor-global;
   }
@@ -64,7 +67,7 @@ textarea.form-control {
 }
 
 .input-group input {
-  border-color: $secondary;
+  border-color: $border-color-global;
 }
 
 /*
@@ -254,18 +257,6 @@ ul.pagination {
   @include override-list-group-item($color-list, $bgcolor-sidebar-list-group, $color-list-hover, $bgcolor-list-hover, $color-list-active, $bgcolor-list-active);
 }
 
-/*
- * GROWI on-edit
- */
-body.on-edit {
-  .grw-editor-navbar-bottom {
-    background-color: $bgcolor-global;
-    #slack-mark-black {
-      display: none;
-    }
-  }
-}
-
 /*
  * Popover
  */
@@ -282,6 +273,9 @@ body.on-edit {
   }
 }
 
+/*
+ * Slack
+ */
 .grw-slack-notification {
   background-color: transparent;
   $color-slack: #4b144c;
@@ -306,22 +300,21 @@ body.on-edit {
       background-image: url(/images/icons/slack/slack-logo-dark-on.svg);
     }
   }
-}
-
-.grw-slack-logo svg {
-  fill: #dd80de;
-}
+  .grw-slack-logo svg {
+    fill: #dd80de;
+  }
 
-.grw-btn-slack {
-  background-color: black;
-  &:focus,
-  &:hover {
+  .grw-btn-slack {
     background-color: black;
+    &:focus,
+    &:hover {
+      background-color: black;
+    }
   }
-}
 
-.grw-btn-slack-triangle {
-  color: $secondary;
+  .grw-btn-slack-triangle {
+    color: $secondary;
+  }
 }
 
 /*
@@ -362,3 +355,41 @@ body.on-edit {
     background-color: $bgcolor-tags;
   }
 }
+
+/*
+ * admin settings
+ */
+.admin-setting-header {
+  border-color: $border-color-global;
+}
+
+/*
+* grw-side-contents
+*/
+.grw-side-contents-sticky-container {
+  .grw-border-vr {
+    border-color: $border-color-toc;
+  }
+
+  .revision-toc {
+    border-color: $border-color-toc;
+  }
+}
+
+/*
+ * modal
+ */
+.grw-modal-head {
+  border-color: $border-color-global;
+}
+
+/*
+ * GROWI user page
+ */
+.grw-page-list-m {
+  .grw-page-list-title-m {
+    svg {
+      fill: $color-global;
+    }
+  }
+}

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

@@ -14,14 +14,16 @@ $bgcolor-table-hover: rgba(black, 0.075) !default;
 $bgcolor-sidebar-list-group: $bgcolor-list !default;
 $color-tags: $gray-500 !default;
 $bgcolor-tags: $gray-200 !default;
+$border-color-global: $gray-300 !default;
+$border-color-toc: $border-color-global !default;
 
 // override bootstrap variables
-$border-color: $gray-200;
 $table-color: $color-table;
 $table-bg: $bgcolor-table;
 $table-border-color: $border-color-table;
 $table-hover-color: $color-table-hover;
 $table-hover-bg: $bgcolor-table-hover;
+$border-color: $border-color-global;
 
 @import 'reboot-bootstrap-border-colors';
 @import 'reboot-bootstrap-tables';
@@ -213,6 +215,9 @@ $table-hover-bg: $bgcolor-table-hover;
   }
 }
 
+/*
+ * Slack
+ */
 .grw-slack-notification {
   background-color: white;
   $color-slack: #4b144c;
@@ -235,23 +240,22 @@ $table-hover-bg: $bgcolor-table-hover;
       background-image: url(/images/icons/slack/slack-logo-on.svg);
     }
   }
-}
-
-.grw-slack-logo svg {
-  fill: #af30b0;
-}
-
-.grw-btn-slack {
-  background-color: white;
+  .grw-slack-logo svg {
+    fill: #af30b0;
+  }
 
-  &:hover,
-  &:focus {
+  .grw-btn-slack {
     background-color: white;
+
+    &:hover,
+    &:focus {
+      background-color: white;
+    }
   }
-}
 
-.grw-btn-slack-triangle {
-  color: $secondary;
+  .grw-btn-slack-triangle {
+    color: $secondary;
+  }
 }
 
 /*
@@ -292,3 +296,40 @@ $table-hover-bg: $bgcolor-table-hover;
     background-color: $bgcolor-tags;
   }
 }
+
+/*
+* grw-side-contents
+*/
+.grw-side-contents-sticky-container {
+  .grw-border-vr {
+    border-color: $border-color-toc;
+  }
+  .revision-toc {
+    border-color: $border-color-toc;
+  }
+}
+
+/*
+ * admin settings
+ */
+.admin-setting-header {
+  border-color: $border-color;
+}
+
+/*
+ * modal
+ */
+.grw-modal-head {
+  border-color: $border-color-global;
+}
+
+/*
+ * GROWI user page
+ */
+.grw-page-list-m {
+  .grw-page-list-title-m {
+    svg {
+      fill: $color-global;
+    }
+  }
+}

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

@@ -14,7 +14,6 @@ $bordercolor-nav-tabs: $gray-300 !default;
 $bordercolor-nav-tabs-hover: $gray-200 $gray-200 $bordercolor-nav-tabs !default;
 $color-nav-tabs-link-active: $gray-600 !default;
 $bordercolor-nav-tabs-active: $bordercolor-nav-tabs $bordercolor-nav-tabs $bgcolor-global !default;
-$bordercolor-toc: $bordercolor-nav-tabs !default;
 $color-seen-user: #549c79 !default;
 
 // override bootstrap variables
@@ -138,7 +137,8 @@ ul.pagination {
     button.page-link {
       color: color-yiq($primary);
       background-color: $primary;
-      &:hover {
+      &:hover,
+      &:focus {
         color: color-yiq($primary);
         background-color: $primary;
       }
@@ -323,10 +323,6 @@ ul.pagination {
   }
 }
 
-.revision-toc {
-  border-color: $bordercolor-toc;
-}
-
 .grw-custom-nav {
   .nav-item {
     .nav-link {

+ 1 - 1
src/client/styles/scss/theme/default.scss

@@ -190,7 +190,7 @@ html[dark] {
   $color-editor-icons: $color-global;
 
   // Border colors
-  $border-color-theme: $gray-700;
+  $border-color-theme: $gray-400;
   $bordercolor-inline-code: $secondary; // optional
 
   // Dropdown colors

+ 1 - 1
src/client/styles/scss/theme/halloween.scss

@@ -38,7 +38,7 @@ html[dark] {
   // Background colors
   $bgcolor-global: #050000;
   $bgcolor-inline-code: #1f1f22; //optional
-  $bgcolor-card: $gray-50;
+  $bgcolor-card: $bgcolor-global;
 
   // Font colors
   $color-global: #e9af2b;

+ 1 - 0
src/client/styles/scss/theme/mixins/_list-group.scss

@@ -3,6 +3,7 @@
     .list-group-item {
       color: $color;
       background-color: $bgcolor;
+      border-color: $border-color-global;
 
       &.list-group-item-action {
         &:hover {

+ 4 - 0
src/client/styles/scss/theme/spring.scss

@@ -94,6 +94,10 @@ html[dark] {
   @import 'apply-colors-light';
 
   //Button
+  // Outline buttons are applyed the accent color to this spring theme cuz the primary is too light and it looks like unable to click them.
+  .btn.btn-outline-primary {
+    @include button-outline-variant($accentcolor, $accentcolor, lighten($accentcolor, 20%), $accentcolor);
+  }
   .btn-group.grw-three-stranded-button {
     .btn.btn-outline-primary {
       @include three-stranded-button(darken($primary, 50%), lighten($primary, 5%), lighten($primary, 10%));

+ 16 - 0
src/lib/util/locale-utils.js

@@ -2,6 +2,11 @@ const fs = require('fs');
 
 const helpers = require('./helpers');
 
+const MIGRATE_LOCALE_MAP = {
+  en: 'en_US',
+  ja: 'ja_JP',
+};
+
 /**
  * List locales dirents
  */
@@ -28,7 +33,18 @@ function listLocaleIds() {
     .map(meta => meta.id);
 }
 
+function migrateDeprecatedLocaleId(localeId) {
+  const toValue = MIGRATE_LOCALE_MAP[localeId];
+
+  if (toValue != null) {
+    return toValue;
+  }
+
+  return localeId;
+}
+
 module.exports = {
   listLocaleMetadatas,
   listLocaleIds,
+  migrateDeprecatedLocaleId,
 };

+ 5 - 1
src/server/models/user.js

@@ -11,7 +11,7 @@ const md5 = require('md5');
 const ObjectId = mongoose.Schema.Types.ObjectId;
 const crypto = require('crypto');
 
-const { listLocaleIds } = require('@commons/util/locale-utils');
+const { listLocaleIds, migrateDeprecatedLocaleId } = require('@commons/util/locale-utils');
 
 module.exports = function(crowi) {
   const STATUS_REGISTERED = 1;
@@ -75,6 +75,10 @@ module.exports = function(crowi) {
       },
     },
   });
+  // eslint-disable-next-line prefer-arrow-callback
+  userSchema.pre('validate', function() {
+    this.lang = migrateDeprecatedLocaleId(this.lang);
+  });
   userSchema.plugin(mongoosePaginate);
   userSchema.plugin(uniqueValidator);
 

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

@@ -14,10 +14,10 @@
       {% block content_header %}
         <div id="grw-subnav-container"></div>
       {% endblock %}
-      <div id="grw-subnav-switcher-container" class="d-edit-none"></div>
-      <div id="grw-subnav-sticky-trigger" class="sticky-top"></div>
-      <div id="grw-fav-sticky-trigger" class="sticky-top"></div>
     </header>
+    <div id="grw-subnav-switcher-container" class="d-edit-none"></div>
+    <div id="grw-subnav-sticky-trigger" class="sticky-top"></div>
+    <div id="grw-fav-sticky-trigger" class="sticky-top"></div>
   {% endblock %}
 
   <div class="flex-grow-1">

+ 3 - 2
src/server/views/widget/headers/drawio.html

@@ -24,8 +24,9 @@
       DrawioViewer.useResizeSensor = false;
       DrawioViewer.prototype.checkVisibleState = false;
 
-      // initialize
-      DrawioViewer.processElements();
+      // Set responsive option.
+      // refs: https://github.com/jgraph/drawio/blob/v13.9.1/src/main/webapp/js/diagramly/GraphViewer.js#L89-L95
+      DrawioViewer.prototype.responsive = true;
     }
   };
 </script>

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

@@ -1,4 +1,4 @@
-<div class="row not-found-message-row mb-4">
+<div class="row not-found-message-row">
   <div class="col-md-12">
     <h2 class="text-muted">
       <i class="icon-ban" aria-hidden="true"></i>