mizozobu пре 6 година
родитељ
комит
1dd59890a9
33 измењених фајлова са 692 додато и 201 уклоњено
  1. 8 0
      CHANGES.md
  2. 3 0
      config/logger/config.dev.js
  3. 10 8
      package.json
  4. 8 0
      resource/cdn-manifests.js
  5. 13 7
      src/client/js/app.js
  6. 15 3
      src/client/js/components/Admin/AdminRebuildSearch.jsx
  7. 8 3
      src/client/js/components/Page.jsx
  8. 1 1
      src/client/js/components/PageAttachment.jsx
  9. 11 21
      src/client/js/components/PageEditor.jsx
  10. 16 11
      src/client/js/components/PageEditorByHackmd.jsx
  11. 2 2
      src/client/js/components/PageStatusAlert.jsx
  12. 6 0
      src/client/js/components/SavePageControls.jsx
  13. 1 1
      src/client/js/components/SearchPage.js
  14. 106 0
      src/client/js/components/StaffCredit/Contributor.js
  15. 124 0
      src/client/js/components/StaffCredit/StaffCredit.jsx
  16. 1 1
      src/client/js/components/UnstatedUtils.jsx
  17. 16 18
      src/client/js/services/AppContainer.js
  18. 7 0
      src/client/js/services/CommentContainer.js
  19. 27 0
      src/client/js/services/EditorContainer.js
  20. 7 3
      src/client/js/services/PageContainer.js
  21. 7 0
      src/client/js/services/TagContainer.js
  22. 7 0
      src/client/js/services/WebsocketContainer.js
  23. 75 0
      src/client/styles/scss/_staff_credit.scss
  24. 2 0
      src/client/styles/scss/style-app.scss
  25. 17 2
      src/server/crowi/express-init.js
  26. 5 0
      src/server/routes/avoid-session-routes.js
  27. 1 1
      src/server/routes/index.js
  28. 1 1
      src/server/views/admin/users.html
  29. 1 1
      src/server/views/admin/widget/passport/oidc.html
  30. 3 0
      src/server/views/layout/layout.html
  31. 1 1
      src/server/views/modal/create_page.html
  32. 0 1
      src/server/views/modal/delete.html
  33. 182 115
      yarn.lock

+ 8 - 0
CHANGES.md

@@ -10,12 +10,20 @@
 
 * Feature: Comment Thread
 * Feature: OpenID Connect authentication
+* Feature: Staff Credits with [Konami Code](https://en.wikipedia.org/wiki/Konami_Code)
 * Improvement Draft list
 * Fix: Deleting page completely
+* Fix: Search with `prefix:` param with CJK pathname
 * I18n: User Management Details
 * I18n: Group Management Details
 * Support: Apply unstated
 * Support: Upgrade libs
+    * async
+    * axios
+    * file-loader
+    * googleapis
+    * i18next
+    * migrate-mongo
     * mini-css-extract-plugin
     * null-loader
 

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

@@ -1,6 +1,8 @@
 module.exports = {
   default: 'info',
 
+  // 'express-session': 'debug',
+
   /*
    * configure level for server
    */
@@ -29,4 +31,5 @@ module.exports = {
    */
   'growi:app': 'debug',
   'growi:services:*': 'debug',
+  'growi:StaffCredit': 'debug',
 };

+ 10 - 8
package.json

@@ -60,14 +60,15 @@
     "webpack": "webpack"
   },
   "dependencies": {
-    "async": "^2.3.0",
+    "async": "^3.0.1",
     "aws-sdk": "^2.88.0",
-    "axios": "^0.18.0",
+    "axios": "^0.19.0",
     "basic-auth-connect": "~1.0.0",
     "body-parser": "^1.18.2",
     "bunyan": "^1.8.12",
     "bunyan-format": "^0.2.1",
-    "check-node-version": "^3.1.1",
+    "//": "see https://github.com/parshap/check-node-version/issues/35",
+    "check-node-version": "=3.3.0",
     "connect-flash": "~0.1.1",
     "connect-mongo": "^2.0.1",
     "connect-redis": "^3.3.0",
@@ -87,17 +88,17 @@
     "express-session": "^1.16.1",
     "express-validator": "^5.3.1",
     "express-webpack-assets": "^0.1.0",
-    "googleapis": "^39.1.0",
+    "googleapis": "^40.0.0",
     "graceful-fs": "^4.1.11",
     "growi-commons": "^4.0.1",
     "helmet": "^3.13.0",
-    "i18next": "^15.0.9",
+    "i18next": "^17.0.3",
     "i18next-express-middleware": "^1.4.1",
     "i18next-node-fs-backend": "^2.1.0",
     "i18next-sprintf-postprocessor": "^0.2.2",
     "md5": "^2.2.1",
     "method-override": "^3.0.0",
-    "migrate-mongo": "^5.0.1",
+    "migrate-mongo": "^6.0.0",
     "mkdirp": "~0.5.1",
     "module-alias": "^2.0.6",
     "mongoose": "^5.4.4",
@@ -118,6 +119,7 @@
     "passport-saml": "^1.0.0",
     "passport-twitter": "^1.0.4",
     "react-dropzone": "^10.1.3",
+    "react-hotkeys": "^1.1.4",
     "rimraf": "^2.6.1",
     "slack-node": "^0.1.8",
     "socket.io": "^2.0.3",
@@ -161,7 +163,7 @@
     "eslint-plugin-chai-friendly": "^0.4.1",
     "eslint-plugin-import": "^2.16.0",
     "eslint-plugin-react": "^7.12.4",
-    "file-loader": "^3.0.1",
+    "file-loader": "^4.0.0",
     "handsontable": "^6.0.1",
     "i18next-browser-languagedetector": "^3.0.1",
     "imports-loader": "^0.8.0",
@@ -188,7 +190,7 @@
     "node-sass": "^4.11.0",
     "nodelist-foreach-polyfill": "^1.2.0",
     "normalize-path": "^3.0.0",
-    "null-loader": "^2.0.0",
+    "null-loader": "^3.0.0",
     "on-headers": "^1.0.1",
     "optimize-css-assets-webpack-plugin": "^5.0.0",
     "penpal": "^4.0.0",

+ 8 - 0
resource/cdn-manifests.js

@@ -87,6 +87,14 @@ module.exports = {
         integrity: '',
       },
     },
+    {
+      name: 'Press Start 2P',
+      url: 'https://fonts.googleapis.com/css?family=Press+Start+2P',
+      groups: ['basis'],
+      args: {
+        integrity: '',
+      },
+    },
     {
       name: 'font-awesome',
       url: 'https://cdn.jsdelivr.net/npm/font-awesome@4.7.0/css/font-awesome.min.css',

+ 13 - 7
src/client/js/app.js

@@ -30,6 +30,7 @@ import BookmarkButton from './components/BookmarkButton';
 import LikeButton from './components/LikeButton';
 import PagePathAutoComplete from './components/PagePathAutoComplete';
 import RecentCreated from './components/RecentCreated/RecentCreated';
+import StaffCredit from './components/StaffCredit/StaffCredit';
 import MyDraftList from './components/MyDraftList/MyDraftList';
 import UserPictureList from './components/User/UserPictureList';
 
@@ -112,6 +113,8 @@ if (pageContainer.state.pageId != null) {
     'bookmark-button-lg':  <BookmarkButton pageId={pageContainer.state.pageId} crowi={appContainer} size="lg" />,
     'rename-page-name-input':  <PagePathAutoComplete crowi={appContainer} initializedPath={pageContainer.state.path} />,
     'duplicate-page-name-input':  <PagePathAutoComplete crowi={appContainer} initializedPath={pageContainer.state.path} />,
+
+    'admin-rebuild-search': <AdminRebuildSearch crowi={appContainer} />,
   }, componentMappings);
 }
 if (pageContainer.state.path != null) {
@@ -168,13 +171,6 @@ if (customHeaderEditorElem != null) {
     customHeaderEditorElem,
   );
 }
-const adminRebuildSearchElem = document.getElementById('admin-rebuild-search');
-if (adminRebuildSearchElem != null) {
-  ReactDOM.render(
-    <AdminRebuildSearch crowi={appContainer} />,
-    adminRebuildSearchElem,
-  );
-}
 const adminGrantSelectorElem = document.getElementById('admin-delete-user-group-modal');
 if (adminGrantSelectorElem != null) {
   ReactDOM.render(
@@ -187,6 +183,16 @@ if (adminGrantSelectorElem != null) {
   );
 }
 
+// render for stuff credit
+const pageStuffCreditElem = document.getElementById('staff-credit');
+if (pageStuffCreditElem) {
+  ReactDOM.render(
+    <StaffCredit></StaffCredit>,
+    pageStuffCreditElem,
+  );
+
+}
+
 // うわーもうー (commented by Crowi team -- 2018.03.23 Yuki Takei)
 $('a[data-toggle="tab"][href="#revision-history"]').on('show.bs.tab', () => {
   ReactDOM.render(

+ 15 - 3
src/client/js/components/Admin/AdminRebuildSearch.jsx

@@ -1,7 +1,10 @@
 import React from 'react';
 import PropTypes from 'prop-types';
 
-export default class AdminRebuildSearch extends React.Component {
+import { createSubscribedElement } from '../UnstatedUtils';
+import WebsocketContainer from '../../services/AppContainer';
+
+class AdminRebuildSearch extends React.Component {
 
   constructor(props) {
     super(props);
@@ -15,7 +18,7 @@ export default class AdminRebuildSearch extends React.Component {
   }
 
   componentDidMount() {
-    const socket = this.props.crowi.getWebSocket();
+    const socket = this.props.webspcketContainer.getWebSocket();
 
     socket.on('admin:addPageProgress', (data) => {
       const newStates = Object.assign(data, { isCompleted: false });
@@ -65,6 +68,15 @@ export default class AdminRebuildSearch extends React.Component {
 
 }
 
+/**
+ * Wrapper component for using unstated
+ */
+const AdminRebuildSearchWrapper = (props) => {
+  return createSubscribedElement(AdminRebuildSearch, props, [WebsocketContainer]);
+};
+
 AdminRebuildSearch.propTypes = {
-  crowi: PropTypes.object.isRequired,
+  webspcketContainer: PropTypes.instanceOf(WebsocketContainer).isRequired,
 };
+
+export default AdminRebuildSearchWrapper;

+ 8 - 3
src/client/js/components/Page.jsx

@@ -5,6 +5,7 @@ import loggerFactory from '@alias/logger';
 import { createSubscribedElement } from './UnstatedUtils';
 import AppContainer from '../services/AppContainer';
 import PageContainer from '../services/PageContainer';
+import EditorContainer from '../services/EditorContainer';
 
 import MarkdownTable from '../models/MarkdownTable';
 
@@ -29,7 +30,7 @@ class Page extends React.Component {
   }
 
   componentWillMount() {
-    this.props.appContainer.registerComponentInstance(this);
+    this.props.appContainer.registerComponentInstance('Page', this);
   }
 
   /**
@@ -45,7 +46,7 @@ class Page extends React.Component {
   }
 
   async saveHandlerForHandsontableModal(markdownTable) {
-    const { pageContainer } = this.props;
+    const { pageContainer, editorContainer } = this.props;
 
     const newMarkdown = mtu.replaceMarkdownTableInMarkdown(
       markdownTable,
@@ -55,6 +56,9 @@ class Page extends React.Component {
     );
 
     try {
+      // disable unsaved warning
+      editorContainer.disableUnsavedWarning();
+
       // eslint-disable-next-line no-unused-vars
       const { page, tags } = await pageContainer.save(newMarkdown);
       logger.debug('success to save');
@@ -88,13 +92,14 @@ class Page extends React.Component {
  * Wrapper component for using unstated
  */
 const PageWrapper = (props) => {
-  return createSubscribedElement(Page, props, [AppContainer, PageContainer]);
+  return createSubscribedElement(Page, props, [AppContainer, PageContainer, EditorContainer]);
 };
 
 
 Page.propTypes = {
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
   pageContainer: PropTypes.instanceOf(PageContainer).isRequired,
+  editorContainer: PropTypes.instanceOf(EditorContainer).isRequired,
 };
 
 export default PageWrapper;

+ 1 - 1
src/client/js/components/PageAttachment.js → src/client/js/components/PageAttachment.jsx

@@ -49,7 +49,7 @@ class PageAttachment extends React.Component {
   }
 
   checkIfFileInUse(attachment) {
-    const { markdown } = this.pageContainer.state;
+    const { markdown } = this.props.pageContainer.state;
 
     if (markdown.match(attachment.filePathProxied)) {
       return true;

+ 11 - 21
src/client/js/components/PageEditor.jsx

@@ -42,7 +42,6 @@ class PageEditor extends React.Component {
     this.onPreviewScroll = this.onPreviewScroll.bind(this);
     this.saveDraft = this.saveDraft.bind(this);
     this.clearDraft = this.clearDraft.bind(this);
-    this.showUnsavedWarning = this.showUnsavedWarning.bind(this);
 
     // get renderer
     this.growiRenderer = this.props.appContainer.getRenderer('editor');
@@ -58,28 +57,13 @@ class PageEditor extends React.Component {
     this.scrollEditorByPreviewScrollWithThrottle = throttle(20, this.scrollEditorByPreviewScroll);
     this.renderPreviewWithDebounce = debounce(50, throttle(100, this.renderPreview));
     this.saveDraftWithDebounce = debounce(800, this.saveDraft);
-
   }
 
   componentWillMount() {
-    this.props.appContainer.registerComponentInstance(this);
+    this.props.appContainer.registerComponentInstance('PageEditor', this);
 
     // initial rendering
     this.renderPreview(this.state.markdown);
-
-    window.addEventListener('beforeunload', this.showUnsavedWarning);
-  }
-
-  componentWillUnmount() {
-    window.removeEventListener('beforeunload', this.showUnsavedWarning);
-  }
-
-  showUnsavedWarning(e) {
-    if (!this.props.appContainer.getIsDocSaved()) {
-      // display browser default message
-      e.returnValue = '';
-      return '';
-    }
   }
 
   getMarkdown() {
@@ -110,7 +94,6 @@ class PageEditor extends React.Component {
   onMarkdownChanged(value) {
     this.renderPreviewWithDebounce(value);
     this.saveDraftWithDebounce();
-    this.props.appContainer.setIsDocSaved(false);
   }
 
   /**
@@ -121,6 +104,9 @@ class PageEditor extends React.Component {
     const optionsToSave = editorContainer.getCurrentOptionsToSave();
 
     try {
+      // disable unsaved warning
+      editorContainer.disableUnsavedWarning();
+
       // eslint-disable-next-line no-unused-vars
       const { page, tags } = await pageContainer.save(this.state.markdown, optionsToSave);
       logger.debug('success to save');
@@ -153,10 +139,13 @@ class PageEditor extends React.Component {
       }
 
       const formData = new FormData();
+      const { pageId, path } = pageContainer.state;
       formData.append('_csrf', appContainer.csrfToken);
       formData.append('file', file);
-      formData.append('path', pageContainer.state.path);
-      formData.append('page_id', this.state.pageId || 0);
+      formData.append('path', path);
+      if (pageId != null) {
+        formData.append('page_id', pageContainer.state.pageId);
+      }
 
       res = await appContainer.apiPost('/attachments.add', formData);
       const attachment = res.attachment;
@@ -172,7 +161,7 @@ class PageEditor extends React.Component {
 
       // when if created newly
       if (res.pageCreated) {
-        // do nothing
+        logger.info('Page is created', res.pageCreated._id);
       }
     }
     catch (e) {
@@ -287,6 +276,7 @@ class PageEditor extends React.Component {
     if (!pageContainer.state.revisionId) {
       editorContainer.saveDraft(pageContainer.state.path, this.state.markdown);
     }
+    editorContainer.enableUnsavedWarning();
   }
 
   clearDraft() {

+ 16 - 11
src/client/js/components/PageEditorByHackmd.jsx

@@ -33,7 +33,7 @@ class PageEditorByHackmd extends React.Component {
   }
 
   componentWillMount() {
-    this.props.appContainer.registerComponentInstance(this);
+    this.props.appContainer.registerComponentInstance('PageEditorByHackmd', this);
   }
 
   /**
@@ -129,6 +129,9 @@ class PageEditorByHackmd extends React.Component {
     const optionsToSave = editorContainer.getCurrentOptionsToSave();
 
     try {
+      // disable unsaved warning
+      editorContainer.disableUnsavedWarning();
+
       // eslint-disable-next-line no-unused-vars
       const { page, tags } = await pageContainer.save(markdown, optionsToSave);
       logger.debug('success to save');
@@ -147,9 +150,9 @@ class PageEditorByHackmd extends React.Component {
   /**
    * onChange event of HackmdEditor handler
    */
-  hackmdEditorChangeHandler(body) {
+  async hackmdEditorChangeHandler(body) {
     const hackmdUri = this.getHackmdUri();
-    const { pageContainer } = this.props;
+    const { pageContainer, editorContainer } = this.props;
 
     if (hackmdUri == null) {
       // do nothing
@@ -157,20 +160,22 @@ class PageEditorByHackmd extends React.Component {
     }
 
     // do nothing if contents are same
-    if (pageContainer.state.markdown === body) {
+    if (this.state.markdown === body) {
       return;
     }
 
+    // enable unsaved warning
+    editorContainer.enableUnsavedWarning();
+
     const params = {
       pageId: pageContainer.state.pageId,
     };
-    this.props.appContainer.apiPost('/hackmd.saveOnHackmd', params)
-      .then((res) => {
-        // do nothing
-      })
-      .catch((err) => {
-        // do nothing
-      });
+    try {
+      await this.props.appContainer.apiPost('/hackmd.saveOnHackmd', params);
+    }
+    catch (err) {
+      logger.error(err);
+    }
   }
 
   render() {

+ 2 - 2
src/client/js/components/PageStatusAlert.jsx

@@ -31,7 +31,7 @@ class PageStatusAlert extends React.Component {
   }
 
   componentWillMount() {
-    this.props.appContainer.registerComponentInstance(this);
+    this.props.appContainer.registerComponentInstance('PageStatusAlert', this);
   }
 
   refreshPage() {
@@ -80,7 +80,7 @@ class PageStatusAlert extends React.Component {
         &nbsp;
         <i className="fa fa-angle-double-right"></i>
         &nbsp;
-        <a onClick={this.refreshPage}>
+        <a href="#" onClick={this.refreshPage}>
           {label2}
         </a>
       </div>

+ 6 - 0
src/client/js/components/SavePageControls.jsx

@@ -47,11 +47,17 @@ class SavePageControls extends React.Component {
 
   save() {
     const { pageContainer, editorContainer } = this.props;
+    // disable unsaved warning
+    editorContainer.disableUnsavedWarning();
+    // save
     pageContainer.saveAndReload(editorContainer.getCurrentOptionsToSave());
   }
 
   saveAndOverwriteScopesOfDescendants() {
     const { pageContainer, editorContainer } = this.props;
+    // disable unsaved warning
+    editorContainer.disableUnsavedWarning();
+    // save
     const optionsToSave = Object.assign(editorContainer.getCurrentOptionsToSave(), {
       overwriteScopesOfDescendants: true,
     });

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

@@ -16,7 +16,7 @@ class SearchPage extends React.Component {
     super(props);
 
     this.state = {
-      searchingKeyword: this.props.query.q || '',
+      searchingKeyword: decodeURI(this.props.query.q) || '',
       searchedKeyword: '',
       searchedPages: [],
       searchResultMeta: {},

+ 106 - 0
src/client/js/components/StaffCredit/Contributor.js

@@ -0,0 +1,106 @@
+const contributors = [
+  {
+    sectionName: 'GROWI VILLAGE',
+    additionalClass: '',
+    memberGroups: [
+      {
+        additionalClass: 'col-md-12 my-4',
+        members: [
+          { position: 'Founder', name: 'yuki-takei' },
+          { position: 'Soncho 1st', name: 'mizozobu' },
+          { position: 'Soncho 2nd', name: 'yusuketk' },
+        ],
+      },
+      {
+        additionalClass: 'col-md-6 my-4',
+        members: [
+          { name: 'utsushiiro' },
+          { name: 'mayumorita' },
+          { name: 'TatsuyaIse' },
+          { name: 'shinoka7' },
+          { name: 'SeiyaTashiro' },
+          { name: 'itizawa' },
+          { name: 'TsuyoshiSuzukief' },
+          { name: 'Yuchan4342' },
+          { name: 'ryu-sato' },
+          { name: 'haruhikonyan' },
+          { name: 'KazuyaNagase' },
+          { name: 'kaishuu0123' },
+          { name: 'kouki-o' },
+          { name: 'Angola' },
+        ],
+      },
+    ],
+  },
+  {
+    sectionName: 'CONTRIBUTER',
+    additionalClass: '',
+    memberGroups: [
+      {
+        additionalClass: 'col-md-4 my-4',
+        members: [
+          { name: 'shield-9' },
+          { name: 'yaodingyd' },
+          { name: 'hitochan777' },
+          { name: 'ttaka66' },
+          { name: 'watagashi' },
+          { name: 'nt-7' },
+          { name: 'hideo54' },
+          { name: 'wadahiro' },
+          { name: 'fumitti' },
+        ],
+      },
+      {
+        additionalClass: 'col-md-6 my-4',
+        members: [
+          { name: 'shaminmeerankutty' },
+          { name: 'rabitarochan' },
+        ],
+      },
+      {
+        additionalClass: 'col-md-4 my-4',
+        members: [
+          { name: 'fmy' },
+          { name: 'yaamai' },
+          { name: 'ta2yak' },
+          { name: 'ryo33' },
+          { name: 'r-tateshina' },
+          { name: 'nekoruri' },
+          { name: 'kmyk' },
+          { name: 'aximov' },
+        ],
+      },
+    ],
+  },
+  // {
+  //   sectionName: 'VALNERABILITY HUNTER',
+  //   additionalClass: '',
+  //   memberGroups: [
+  //     {
+  //       additionalClass: 'col-md-6 my-4',
+  //       members: [
+  //         { name: 'Yoshinori Hayashi' },
+  //         { name: 'Kanta Nishitani' },
+  //         { name: 'Takashi Yoneuchi' },
+  //         { position: 'DeCurret', name: 'Yusuke Tanomogi' },
+  //       ],
+  //     },
+  //   ],
+  // },
+  {
+    sectionName: 'SPECIAL THANKS',
+    additionalClass: '',
+    memberGroups: [
+      {
+        additionalClass: 'col-md-4 my-4',
+        members: [
+          { name: 'Crowi Team' },
+          { position: 'Ambassador', name: 'Tsuyoshi Suzuki' },
+          { name: 'JPCERT/CC' },
+        ],
+      },
+    ],
+  },
+];
+
+module.exports = contributors;

+ 124 - 0
src/client/js/components/StaffCredit/StaffCredit.jsx

@@ -0,0 +1,124 @@
+import React from 'react';
+import { HotKeys } from 'react-hotkeys';
+
+import loggerFactory from '@alias/logger';
+
+import contributors from './Contributor';
+
+/**
+ * Page staff credit component
+ *
+ * @export
+ * @class StaffCredit
+ * @extends {React.Component}
+ */
+
+export default class StaffCredit extends React.Component {
+
+  constructor(props) {
+    super(props);
+
+    this.logger = loggerFactory('growi:StaffCredit');
+
+    this.state = {
+      isShown: false,
+      userCommand: [],
+    };
+    this.konamiCommand = ['ArrowUp', 'ArrowUp', 'ArrowDown', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'ArrowLeft', 'ArrowRight', 'b', 'a'];
+    this.deleteCredit = this.deleteCredit.bind(this);
+  }
+
+  check(event) {
+    this.logger.debug(`'${event.key}' pressed`);
+
+    // compare keydown and next konamiCommand
+    if (this.konamiCommand[this.state.userCommand.length] === event.key) {
+      const nextValue = this.state.userCommand.concat(event.key);
+      if (nextValue.length === this.konamiCommand.length) {
+        this.setState({
+          isShown: true,
+          userCommand: [],
+        });
+      }
+      else {
+        // add UserCommand
+        this.setState({ userCommand: nextValue });
+
+        this.logger.debug('userCommand', this.state.userCommand);
+      }
+    }
+    else {
+      this.setState({ userCommand: [] });
+    }
+  }
+
+  deleteCredit() {
+    if (this.state.isShown) {
+      this.setState({ isShown: false });
+    }
+  }
+
+  renderMembers(memberGroup, keyPrefix) {
+    // construct members elements
+    const members = memberGroup.members.map((member) => {
+      return (
+        <div className={memberGroup.additionalClass} key={`${keyPrefix}-${member.name}-container`}>
+          <span className="dev-position" key={`${keyPrefix}-${member.name}-position`}>
+            {/* position or '&nbsp;' */}
+            { member.position || '\u00A0' }
+          </span>
+          <p className="dev-name" key={`${keyPrefix}-${member.name}`}>{member.name}</p>
+        </div>
+      );
+    });
+    return (
+      <React.Fragment key={`${keyPrefix}-fragment`}>
+        {members}
+      </React.Fragment>
+    );
+  }
+
+  renderContributors() {
+    if (this.state.isShown) {
+      const credit = contributors.map((contributor) => {
+        // construct members elements
+        const memberGroups = contributor.memberGroups.map((memberGroup, idx) => {
+          return this.renderMembers(memberGroup, `${contributor.sectionName}-group${idx}`);
+        });
+        return (
+          <React.Fragment key={`${contributor.sectionName}-fragment`}>
+            <div className={`row staff-credit-my-10 ${contributor.additionalClass}`} key={`${contributor.sectionName}-row`}>
+              <h2 className="col-md-12 dev-team mt-5 staff-credit-mb-10" key={contributor.sectionName}>{contributor.sectionName}</h2>
+              {memberGroups}
+            </div>
+            <div className="clearfix"></div>
+          </React.Fragment>
+        );
+      });
+      return (
+        <div className="text-center credit-curtain" onClick={this.deleteCredit}>
+          <div className="credit-body">
+            <h1 className="staff-credit-mb-10">GROWI Contributors</h1>
+            <div className="clearfix"></div>
+            {credit}
+          </div>
+        </div>
+      );
+    }
+    return null;
+  }
+
+  render() {
+    const keyMap = { check: ['up', 'down', 'right', 'left', 'b', 'a'] };
+    const handlers = { check: (event) => { return this.check(event) } };
+    return (
+      <HotKeys focused attach={window} keyMap={keyMap} handlers={handlers}>
+        {this.renderContributors()}
+      </HotKeys>
+    );
+  }
+
+}
+
+StaffCredit.propTypes = {
+};

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

@@ -19,7 +19,7 @@ function generateAutoNamedProps(instances) {
 
   instances.forEach((instance) => {
     // get class name
-    const className = instance.constructor.name;
+    const className = instance.constructor.getClassName();
     // convert initial charactor to lower case
     const propName = `${className.charAt(0).toLowerCase()}${className.slice(1)}`;
 

+ 16 - 18
src/client/js/services/AppContainer.js

@@ -70,6 +70,13 @@ export default class AppContainer extends Container {
     this.apiRequest = this.apiRequest.bind(this);
   }
 
+  /**
+   * Workaround for the mangling in production build to break constructor.name
+   */
+  static getClassName() {
+    return 'AppContainer';
+  }
+
   initPlugins() {
     if (this.isPluginEnabled) {
       const growiPlugin = window.growiPlugin;
@@ -109,7 +116,7 @@ export default class AppContainer extends Container {
       throw new Error('The specified instance must not be null');
     }
 
-    const className = instance.constructor.name;
+    const className = instance.constructor.getClassName();
 
     if (this.containerInstances[className] != null) {
       throw new Error('The specified instance couldn\'t register because the same type object has already been registered');
@@ -131,28 +138,27 @@ export default class AppContainer extends Container {
 
   /**
    * Register React component instance
+   * @param {string} id
    * @param {object} instance React component instance
    */
-  registerComponentInstance(instance) {
+  registerComponentInstance(id, instance) {
     if (instance == null) {
       throw new Error('The specified instance must not be null');
     }
 
-    const className = instance.constructor.name;
-
-    if (this.componentInstances[className] != null) {
-      throw new Error('The specified instance couldn\'t register because the same type object has already been registered');
+    if (this.componentInstances[id] != null) {
+      throw new Error('The specified instance couldn\'t register because the same id has already been registered');
     }
 
-    this.componentInstances[className] = instance;
+    this.componentInstances[id] = instance;
   }
 
   /**
    * Get registered React component instance
-   * @param {string} className
+   * @param {string} id
    */
-  getComponentInstance(className) {
-    return this.componentInstances[className];
+  getComponentInstance(id) {
+    return this.componentInstances[id];
   }
 
   getOriginRenderer() {
@@ -177,14 +183,6 @@ export default class AppContainer extends Container {
     return renderer;
   }
 
-  setIsDocSaved(isSaved) {
-    this.isDocSaved = isSaved;
-  }
-
-  getIsDocSaved() {
-    return this.isDocSaved;
-  }
-
   getEmojiStrategy() {
     return emojiStrategy;
   }

+ 7 - 0
src/client/js/services/CommentContainer.js

@@ -36,6 +36,13 @@ export default class CommentContainer extends Container {
     this.retrieveComments = this.retrieveComments.bind(this);
   }
 
+  /**
+   * Workaround for the mangling in production build to break constructor.name
+   */
+  static getClassName() {
+    return 'CommentContainer';
+  }
+
   getPageContainer() {
     return this.appContainer.getContainer('PageContainer');
   }

+ 27 - 0
src/client/js/services/EditorContainer.js

@@ -37,6 +37,8 @@ export default class EditorContainer extends Container {
       previewOptions: {},
     };
 
+    this.isSetBeforeunloadEventHandler = false;
+
     this.initStateGrant();
     this.initDrafts();
 
@@ -44,6 +46,13 @@ export default class EditorContainer extends Container {
     this.initEditorOptions('previewOptions', 'previewOptions', defaultPreviewOptions);
   }
 
+  /**
+   * Workaround for the mangling in production build to break constructor.name
+   */
+  static getClassName() {
+    return 'EditorContainer';
+  }
+
   /**
    * initialize state for page permission
    */
@@ -139,6 +148,24 @@ export default class EditorContainer extends Container {
     return opt;
   }
 
+  showUnsavedWarning(e) {
+    // display browser default message
+    e.returnValue = '';
+    return '';
+  }
+
+  disableUnsavedWarning() {
+    window.removeEventListener('beforeunload', this.showUnsavedWarning);
+    this.isSetBeforeunloadEventHandler = false;
+  }
+
+  enableUnsavedWarning() {
+    if (!this.isSetBeforeunloadEventHandler) {
+      window.addEventListener('beforeunload', this.showUnsavedWarning);
+      this.isSetBeforeunloadEventHandler = true;
+    }
+  }
+
   clearDraft(path) {
     delete this.drafts[path];
     window.localStorage.setItem('drafts', JSON.stringify(this.drafts));

+ 7 - 3
src/client/js/services/PageContainer.js

@@ -60,6 +60,13 @@ export default class PageContainer extends Container {
     this.addWebSocketEventHandlers();
   }
 
+  /**
+   * Workaround for the mangling in production build to break constructor.name
+   */
+  static getClassName() {
+    return 'PageContainer';
+  }
+
   /**
    * initialize state for markdown data
    */
@@ -110,9 +117,6 @@ export default class PageContainer extends Container {
    * @param {Array[Tag]} tags Array of Tag
    */
   updateStateAfterSave(page, tags) {
-    // mark that the document is not editing
-    this.appContainer.setIsDocSaved(true);
-
     const { editorMode } = this.appContainer.state;
 
     // update state of PageContainer

+ 7 - 0
src/client/js/services/TagContainer.js

@@ -19,6 +19,13 @@ export default class TagContainer extends Container {
     this.init();
   }
 
+  /**
+   * Workaround for the mangling in production build to break constructor.name
+   */
+  static getClassName() {
+    return 'TagContainer';
+  }
+
   /**
    * retrieve tags data
    * !! This method should be invoked after PageContainer and EditorContainer has been initialized !!

+ 7 - 0
src/client/js/services/WebsocketContainer.js

@@ -22,6 +22,13 @@ export default class WebsocketContainer extends Container {
 
   }
 
+  /**
+   * Workaround for the mangling in production build to break constructor.name
+   */
+  static getClassName() {
+    return 'WebsocketContainer';
+  }
+
   getWebSocket() {
     return this.socket;
   }

+ 75 - 0
src/client/styles/scss/_staff_credit.scss

@@ -0,0 +1,75 @@
+// Staff Credit
+#staff-credit {
+  font-family: 'Press Start 2P', $basefont1;
+  color: white;
+
+  h1,
+  h2,
+  h3,
+  h4,
+  h5,
+  h6 {
+    font-family: 'Press Start 2P', $basefont1;
+    color: white;
+  }
+
+  $credit-length: -200em;
+
+  .credit-curtain {
+    position: fixed;
+    top: 10vh;
+    left: 20vh;
+    width: 80vw;
+    height: 80vh;
+    overflow-y: hidden;
+    background-color: black;
+  }
+
+  .credit-body {
+    position: relative;
+    top: $credit-length;
+    animation-name: Credit;
+    // credit duration
+    animation-duration: 20s;
+    animation-timing-function: linear;
+  }
+
+  @keyframes Credit {
+    from {
+      top: 100%;
+    }
+    to {
+      // credit length
+      top: $credit-length;
+    }
+  }
+
+  h1 {
+    font-size: 3em;
+  }
+
+  h2 {
+    font-size: 2.2em;
+  }
+
+  .dev-position {
+    font-size: 1em;
+  }
+
+  .dev-name {
+    font-size: 1.8em;
+  }
+
+  .staff-credit-mt-10 {
+    margin-top: 6rem;
+  }
+
+  .staff-credit-mb-10 {
+    margin-bottom: 6rem;
+  }
+
+  .staff-credit-my-10 {
+    @extend .staff-credit-mt-10;
+    @extend .staff-credit-mb-10;
+  }
+}

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

@@ -39,7 +39,9 @@
 @import 'user_growi';
 @import 'handsontable';
 @import 'wiki';
+@import 'staff_credit';
 @import 'tag';
+@import 'staff_credit';
 @import 'draft';
 
 /*

+ 17 - 2
src/server/crowi/express-init.js

@@ -9,7 +9,7 @@ module.exports = function(crowi, app) {
   const cookieParser = require('cookie-parser');
   const methodOverride = require('method-override');
   const passport = require('passport');
-  const session = require('express-session');
+  const expressSession = require('express-session');
   const sanitizer = require('express-sanitizer');
   const basicAuth = require('basic-auth-connect');
   const flash = require('connect-flash');
@@ -19,7 +19,11 @@ module.exports = function(crowi, app) {
   const i18nFsBackend = require('i18next-node-fs-backend');
   const i18nSprintf = require('i18next-sprintf-postprocessor');
   const i18nMiddleware = require('i18next-express-middleware');
+
+  const avoidSessionRoutes = require('../routes/avoid-session-routes');
   const i18nUserSettingDetector = require('../util/i18nUserSettingDetector');
+  const middleware = require('../util/middlewares');
+
   const env = crowi.node_env;
 
   // Old type config API
@@ -101,7 +105,18 @@ module.exports = function(crowi, app) {
   app.use(bodyParser.json({ limit: '50mb' }));
   app.use(sanitizer());
   app.use(cookieParser());
-  app.use(session(crowi.sessionConfig));
+
+  // configure express-session
+  app.use((req, res, next) => {
+    // test whether the route is listed in avoidSessionTroutes
+    for (const regex of avoidSessionRoutes) {
+      if (regex.test(req.path)) {
+        return next();
+      }
+    }
+
+    expressSession(crowi.sessionConfig)(req, res, next);
+  });
 
   // Set basic auth middleware
   app.use((req, res, next) => {

+ 5 - 0
src/server/routes/avoid-session-routes.js

@@ -0,0 +1,5 @@
+module.exports = [
+  /^\/_api\/v3\/healthcheck/,
+  /^\/_hackmd\//,
+  /^\/api-docs\//,
+];

+ 1 - 1
src/server/routes/index.js

@@ -209,7 +209,7 @@ module.exports = function(crowi, app) {
   app.get('/_api/tags.search'         , accessTokenParser, loginRequired(false), tag.api.search);
   app.post('/_api/tags.update'        , accessTokenParser, loginRequired(false), tag.api.update);
   app.get('/_api/comments.get'        , accessTokenParser , loginRequired(false) , comment.api.get);
-  app.post('/_api/comments.add'       , form.comment, accessTokenParser , loginRequired() , csrf, comment.api.add);
+  app.post('/_api/comments.add'       , comment.api.validators.add(), accessTokenParser , loginRequired() , csrf, comment.api.add);
   app.post('/_api/comments.remove'    , accessTokenParser , loginRequired() , csrf, comment.api.remove);
   app.get('/_api/bookmarks.get'       , accessTokenParser , loginRequired(false) , bookmark.api.get);
   app.post('/_api/bookmarks.add'      , accessTokenParser , loginRequired() , csrf, bookmark.api.add);

+ 1 - 1
src/server/views/admin/users.html

@@ -77,7 +77,7 @@
             <div class="modal-body">
               <p>
                 {{ t('user_management.temporary_password') }}<br>
-                {{ t('user_management.password.never.seen') }}<span class="text-danger">{{ t('user_management.send_temporary_password') }}</span>
+                {{ t('user_management.password_never_seen') }}<span class="text-danger">{{ t('user_management.send_temporary_password') }}</span>
               </p>
 
               <pre>{% for cUser in createdUser %}{% if cUser.user %}{{ cUser.email }} {{ cUser.password }}<br>{% else %}{{ cUser.email }} 作成失敗<br>{% endif %}{% endfor %}</pre>

+ 1 - 1
src/server/views/admin/widget/passport/oidc.html

@@ -96,7 +96,7 @@
     <div class="form-group">
       <label for="settingForm[security:passport-oidc:attrMapName]" class="col-xs-3 control-label">Name</label>
       <div class="col-xs-6">
-        <input class="form-control" type="text" name="settingForm[security:passport-oidc:attrMapName]" value="{{ settingForm['security:passport-oidc:attrName'] || '' }}">
+        <input class="form-control" type="text" name="settingForm[security:passport-oidc:attrMapName]" value="{{ settingForm['security:passport-oidc:attrMapName'] || '' }}">
         <p class="help-block">
           <small>
             {{ t("security_setting.OAuth.OIDC.name_detail") }}

+ 3 - 0
src/server/views/layout/layout.html

@@ -239,6 +239,9 @@
 
 </div><!-- /#wrapper -->
 
+<!-- /#staff-credit -->
+<div id="staff-credit"></div>
+
 {% include '../modal/shortcuts.html' %}
 
 {% block body_end %}

+ 1 - 1
src/server/views/modal/create_page.html

@@ -72,7 +72,7 @@
             // modify href
             const value = $(this).val();
             const pageName = (value === 'children') ? '_template' : '__template';
-            const truePath = "{{ page.path || path | addTrailingSlash }}";
+            const truePath = "{% if page.path || path %}{{ page.path || path | addTrailingSlash }}{% else %}{{ page.path || path }}{% endif %}"
             const link = truePath + pageName + '#edit-form';
             $('#link-to-template').attr('href', link);
           });

+ 0 - 1
src/server/views/modal/delete.html

@@ -23,7 +23,6 @@
           <hr>
 
           {% if page.grant != 2 %}
-          {{ page.grant }}
           <div class="checkbox checkbox-warning">
             <input name="recursively" id="cbDeleteRecursively" value="1" type="checkbox" checked>
             <label for="cbDeleteRecursively">{{ t('modal_delete.label.Delete recursively') }}</label>

+ 182 - 115
yarn.lock

@@ -394,9 +394,10 @@ abbrev@1:
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8"
 
-abort-controller@^2.0.2:
-  version "2.0.2"
-  resolved "https://registry.yarnpkg.com/abort-controller/-/abort-controller-2.0.2.tgz#f0c059173ac7fdc4dba73e3833102def407a6a29"
+abort-controller@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/abort-controller/-/abort-controller-3.0.0.tgz#eaf54d53b62bae4138e809ca225c8439a6efb392"
+  integrity sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==
   dependencies:
     event-target-shim "^5.0.0"
 
@@ -689,6 +690,11 @@ arrify@^1.0.0, arrify@^1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d"
 
+arrify@^2.0.0:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/arrify/-/arrify-2.0.1.tgz#c9655e9331e0abcd588d2a7cad7e9956f66701fa"
+  integrity sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==
+
 asap@^2.0.0, asap@~2.0.3:
   version "2.0.6"
   resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46"
@@ -769,11 +775,10 @@ async@^0.9.0:
   version "0.9.2"
   resolved "https://registry.yarnpkg.com/async/-/async-0.9.2.tgz#aea74d5e61c1f899613bf64bda66d4c78f2fd17d"
 
-async@^2.3.0:
-  version "2.6.0"
-  resolved "https://registry.yarnpkg.com/async/-/async-2.6.0.tgz#61a29abb6fcc026fea77e56d1c6ec53a795951f4"
-  dependencies:
-    lodash "^4.14.0"
+async@^3.0.1:
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/async/-/async-3.0.1.tgz#dfeb34657d1e63c94c0eee424297bf8a2c9a8182"
+  integrity sha512-ZswD8vwPtmBZzbn9xyi8XBQWXH3AvOQ43Za1KWYq7JeycrZuUYzx01KvHcVbXltjqH4y0MWrQ33008uLTqXuDw==
 
 async@~0.2.6:
   version "0.2.10"
@@ -856,12 +861,13 @@ axios@0.17.1:
     follow-redirects "^1.2.5"
     is-buffer "^1.1.5"
 
-axios@^0.18.0:
-  version "0.18.0"
-  resolved "https://registry.yarnpkg.com/axios/-/axios-0.18.0.tgz#32d53e4851efdc0a11993b6cd000789d70c05102"
+axios@^0.19.0:
+  version "0.19.0"
+  resolved "https://registry.yarnpkg.com/axios/-/axios-0.19.0.tgz#8e09bff3d9122e133f7b8101c8fbdd00ed3d2ab8"
+  integrity sha512-1uvKqKQta3KBxIz14F2v06AEHZ/dIoeKfbTRkK1E5oqjDnuEerLmYTgJB5AiQZHJcljpg1TuRzdjDR06qNk0DQ==
   dependencies:
-    follow-redirects "^1.3.0"
-    is-buffer "^1.1.5"
+    follow-redirects "1.5.10"
+    is-buffer "^2.0.2"
 
 babel-code-frame@^6.26.0:
   version "6.26.0"
@@ -1828,6 +1834,11 @@ bson@^1.1.0, bson@~1.1.0:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/bson/-/bson-1.1.0.tgz#bee57d1fb6a87713471af4e32bcae36de814b5b0"
 
+bson@^1.1.1:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/bson/-/bson-1.1.1.tgz#4330f5e99104c4e751e7351859e2d408279f2f13"
+  integrity sha512-jCGVYLoYMHDkOsbwJZBCqwMHyH4c+wzgI9hG7Z6SZJRXWr+x58pdIbm2i9a/jFGCkRJqRUr8eoI7lDWa0hTkxg==
+
 bson@~1.0.4:
   version "1.0.4"
   resolved "https://registry.yarnpkg.com/bson/-/bson-1.0.4.tgz#93c10d39eaa5b58415cbc4052f3e53e562b0b72c"
@@ -2122,7 +2133,7 @@ check-error@^1.0.1:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/check-error/-/check-error-1.0.2.tgz#574d312edd88bb5dd8912e9286dd6c0aed4aac82"
 
-check-node-version@^3.1.1:
+check-node-version@=3.3.0:
   version "3.3.0"
   resolved "https://registry.yarnpkg.com/check-node-version/-/check-node-version-3.3.0.tgz#a53d0be9c24e7fd22e029de032863d020362fe32"
   integrity sha512-OAtp7prQf+8YYKn2UB/fK1Ppb9OT+apW56atoKYUvucYLPq69VozOY0B295okBwCKymk2cictrS3qsdcZwyfzw==
@@ -2392,9 +2403,10 @@ commander@2.17.1, commander@~2.17.1:
   version "2.17.1"
   resolved "https://registry.yarnpkg.com/commander/-/commander-2.17.1.tgz#bd77ab7de6de94205ceacc72f1716d29f20a77bf"
 
-commander@2.19.0:
-  version "2.19.0"
-  resolved "https://registry.yarnpkg.com/commander/-/commander-2.19.0.tgz#f6198aa84e5b83c46054b94ddedbfed5ee9ff12a"
+commander@2.20.0, commander@^2.20.0, commander@^2.7.1:
+  version "2.20.0"
+  resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.0.tgz#d58bb2b5c1ee8f87b0d340027e9e94e222c5a422"
+  integrity sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ==
 
 commander@^2.11.0, commander@^2.9.0:
   version "2.12.2"
@@ -2408,11 +2420,6 @@ commander@^2.2.0:
   version "2.15.1"
   resolved "https://registry.yarnpkg.com/commander/-/commander-2.15.1.tgz#df46e867d0fc2aec66a34662b406a9ccafff5b0f"
 
-commander@^2.20.0, commander@^2.7.1:
-  version "2.20.0"
-  resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.0.tgz#d58bb2b5c1ee8f87b0d340027e9e94e222c5a422"
-  integrity sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ==
-
 commondir@^1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b"
@@ -2966,9 +2973,10 @@ debug@2, debug@2.6.9, debug@^2.0.0, debug@^2.1.2, debug@^2.2.0, debug@^2.3.3, de
   dependencies:
     ms "2.0.0"
 
-debug@3.1.0, debug@^3.1.0, debug@~3.1.0:
+debug@3.1.0, debug@=3.1.0, debug@^3.1.0, debug@~3.1.0:
   version "3.1.0"
   resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261"
+  integrity sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==
   dependencies:
     ms "2.0.0"
 
@@ -4159,11 +4167,12 @@ file-entry-cache@^5.0.1:
   dependencies:
     flat-cache "^2.0.1"
 
-file-loader@^3.0.1:
-  version "3.0.1"
-  resolved "https://registry.yarnpkg.com/file-loader/-/file-loader-3.0.1.tgz#f8e0ba0b599918b51adfe45d66d1e771ad560faa"
+file-loader@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/file-loader/-/file-loader-4.0.0.tgz#c3570783fefb6e1bc0978a856f4bf5825b966c2a"
+  integrity sha512-roAbL6IdSGczwfXxhMi6Zq+jD4IfUpL0jWHD7fvmjdOVb7xBfdRUHe4LpBgO23VtVK5AW1OlWZo0p34Jvx3iWg==
   dependencies:
-    loader-utils "^1.0.2"
+    loader-utils "^1.2.2"
     schema-utils "^1.0.0"
 
 file-selector@^0.1.11:
@@ -4311,9 +4320,17 @@ flush-write-stream@^1.0.0:
     inherits "^2.0.1"
     readable-stream "^2.0.4"
 
-fn-args@3.0.0:
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/fn-args/-/fn-args-3.0.0.tgz#df5c3805ed41ec3b38a72aabe390cf9493ec084c"
+fn-args@5.0.0:
+  version "5.0.0"
+  resolved "https://registry.yarnpkg.com/fn-args/-/fn-args-5.0.0.tgz#7a18e105c8fb3bf0a51c30389bf16c9ebe740bb3"
+  integrity sha512-CtbfI3oFFc3nbdIoHycrfbrxiGgxXBXXuyOl49h47JawM1mYrqpiRqnH5CB2mBatdXvHHOUO6a+RiAuuvKt0lw==
+
+follow-redirects@1.5.10:
+  version "1.5.10"
+  resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.5.10.tgz#7b7a9f9aea2fdff36786a94ff643ed07f4ff5e2a"
+  integrity sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==
+  dependencies:
+    debug "=3.1.0"
 
 follow-redirects@^1.2.5:
   version "1.7.0"
@@ -4321,12 +4338,6 @@ follow-redirects@^1.2.5:
   dependencies:
     debug "^3.2.6"
 
-follow-redirects@^1.3.0:
-  version "1.4.1"
-  resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.4.1.tgz#d8120f4518190f55aac65bb6fc7b85fcd666d6aa"
-  dependencies:
-    debug "^3.1.0"
-
 for-in@^0.1.3:
   version "0.1.8"
   resolved "https://registry.yarnpkg.com/for-in/-/for-in-0.1.8.tgz#d8773908e31256109952b1fdb9b3fa867d2775e1"
@@ -4426,9 +4437,10 @@ fs-extra@3.0.1:
     jsonfile "^3.0.0"
     universalify "^0.1.0"
 
-fs-extra@7.0.1:
-  version "7.0.1"
-  resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-7.0.1.tgz#4f189c44aa123b895f722804f55ea23eadc348e9"
+fs-extra@8.0.1:
+  version "8.0.1"
+  resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-8.0.1.tgz#90294081f978b1f182f347a440a209154344285b"
+  integrity sha512-W+XLrggcDzlle47X/XnS7FXrXu9sDo+Ze9zpndeBxdgv88FHLm1HtmkhEwavruS6koanBjp098rUpHs65EmG7A==
   dependencies:
     graceful-fs "^4.1.2"
     jsonfile "^4.0.0"
@@ -4497,14 +4509,15 @@ gauge@~2.7.3:
     strip-ansi "^3.0.1"
     wide-align "^1.1.0"
 
-gaxios@^1.0.2, gaxios@^1.0.4, gaxios@^1.2.1, gaxios@^1.2.2:
-  version "1.7.0"
-  resolved "https://registry.yarnpkg.com/gaxios/-/gaxios-1.7.0.tgz#cf1638426411cb362403038e0787105f5bf08d22"
+gaxios@^2.0.0:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/gaxios/-/gaxios-2.0.1.tgz#2ca1c9eb64c525d852048721316c138dddf40708"
+  integrity sha512-c1NXovTxkgRJTIgB2FrFmOFg4YIV6N/bAa4f/FZ4jIw13Ql9ya/82x69CswvotJhbV3DiGnlTZwoq2NVXk2Irg==
   dependencies:
-    abort-controller "^2.0.2"
+    abort-controller "^3.0.0"
     extend "^3.0.2"
     https-proxy-agent "^2.2.1"
-    node-fetch "^2.2.0"
+    node-fetch "^2.3.0"
 
 gaze@^1.0.0:
   version "1.1.3"
@@ -4512,11 +4525,12 @@ gaze@^1.0.0:
   dependencies:
     globule "^1.0.0"
 
-gcp-metadata@^0.9.3:
-  version "0.9.3"
-  resolved "https://registry.yarnpkg.com/gcp-metadata/-/gcp-metadata-0.9.3.tgz#1f9d7495f7460a14526481f29e11596dd563dd26"
+gcp-metadata@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/gcp-metadata/-/gcp-metadata-2.0.0.tgz#afd6092bd68e64c508e1687dfb829f5800eaa12e"
+  integrity sha512-BN6KUUWo6WLkDRst+Y7bqpXq1PYMrKUecNLRdZESp7oYtMjWcZdAM0UYvcip8wb0GXNO/j8Z8HTccK4iYtMvyQ==
   dependencies:
-    gaxios "^1.0.2"
+    gaxios "^2.0.0"
     json-bigint "^0.3.0"
 
 get-caller-file@^1.0.1:
@@ -4696,19 +4710,20 @@ gonzales-pe@^4.0.3:
   dependencies:
     minimist "1.1.x"
 
-google-auth-library@^3.0.0:
-  version "3.1.0"
-  resolved "https://registry.yarnpkg.com/google-auth-library/-/google-auth-library-3.1.0.tgz#6378ea3e56067312209eee58223e5a00adaec639"
+google-auth-library@^4.0.0:
+  version "4.2.0"
+  resolved "https://registry.yarnpkg.com/google-auth-library/-/google-auth-library-4.2.0.tgz#fe1144ff07bea2852f3669b759f774fcc5171d02"
+  integrity sha512-AtIzv/MZpo8BQvt1J+ObetUVFQBAae2I3u6Wy4XqePYShHnYiRdXqWr2WWBkIllOGbWEwsq4PUfvafgw76XGLQ==
   dependencies:
+    arrify "^2.0.0"
     base64-js "^1.3.0"
     fast-text-encoding "^1.0.0"
-    gaxios "^1.2.1"
-    gcp-metadata "^0.9.3"
-    gtoken "^2.3.2"
-    https-proxy-agent "^2.2.1"
+    gaxios "^2.0.0"
+    gcp-metadata "^2.0.0"
+    gtoken "^3.0.0"
     jws "^3.1.5"
     lru-cache "^5.0.0"
-    semver "^5.5.0"
+    semver "^6.0.0"
 
 google-auth-library@~0.10.0:
   version "0.10.0"
@@ -4730,19 +4745,20 @@ google-p12-pem@^0.1.0:
   dependencies:
     node-forge "^0.7.1"
 
-google-p12-pem@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/google-p12-pem/-/google-p12-pem-1.0.0.tgz#375cc4e977a311908d365b47ed3519e7207c1f77"
+google-p12-pem@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/google-p12-pem/-/google-p12-pem-2.0.0.tgz#78f57cfeee46bd676e4a556009432712b687bad6"
+  integrity sha512-n8eGSKzWOb9/EmSBIh81sPvsQM939QlpHMXahTZDzuRIpCu09x3Oaqz+mXGjL4TeCvSbcnOC0YZRvjkJ9s9lnA==
   dependencies:
-    node-forge "^0.7.1"
-    pify "^3.0.0"
+    node-forge "^0.8.0"
 
-googleapis-common@^0.7.0:
-  version "0.7.2"
-  resolved "https://registry.yarnpkg.com/googleapis-common/-/googleapis-common-0.7.2.tgz#a694f55d979cb7c2eac21a0e0439af12f9b418ba"
+googleapis-common@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/googleapis-common/-/googleapis-common-2.0.0.tgz#66683d7036503fbd3ac7d2cb154efe1d767c928d"
+  integrity sha512-RyUkadTbrTWOCMnKYVYg1pxeH6oFKDr8WHOesbjsgPY1tS10q8Wdmf3VUKL3MMqNEM5ue2IxdfM2FzpYUGHaxA==
   dependencies:
-    gaxios "^1.2.2"
-    google-auth-library "^3.0.0"
+    gaxios "^2.0.0"
+    google-auth-library "^4.0.0"
     pify "^4.0.0"
     qs "^6.5.2"
     url-template "^2.0.8"
@@ -4756,12 +4772,13 @@ googleapis@^16.0.0:
     google-auth-library "~0.10.0"
     string-template "~1.0.0"
 
-googleapis@^39.1.0:
-  version "39.1.0"
-  resolved "https://registry.yarnpkg.com/googleapis/-/googleapis-39.1.0.tgz#7a89092e9cc64b25dde4503db606032e3514444d"
+googleapis@^40.0.0:
+  version "40.0.0"
+  resolved "https://registry.yarnpkg.com/googleapis/-/googleapis-40.0.0.tgz#c2b16f660fb7a8dcada97e9862b73955ba83865e"
+  integrity sha512-G4iUF6V141mbgbXmbXQDYP0pOYJAONvA8m+RzYfuVBcwfKm7Pn6Aes9LT0a6ddmW9CmydHmHdOgKZuWwkXueXg==
   dependencies:
-    google-auth-library "^3.0.0"
-    googleapis-common "^0.7.0"
+    google-auth-library "^4.0.0"
+    googleapis-common "^2.0.0"
 
 got@^6.7.1:
   version "6.7.1"
@@ -4833,12 +4850,13 @@ gtoken@^1.2.1:
     mime "^1.4.1"
     request "^2.72.0"
 
-gtoken@^2.3.2:
-  version "2.3.2"
-  resolved "https://registry.yarnpkg.com/gtoken/-/gtoken-2.3.2.tgz#49890a866c1f44e173099be95515db5872a92151"
+gtoken@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/gtoken/-/gtoken-3.0.0.tgz#7d37245b5109442d8ec28075d5de15505c7462ad"
+  integrity sha512-IY9HVi78D4ykVHn+ThI7rlcpdFtKyo9e9YLim9S9T3rp6fEnfeTexcrqzSpExVshPofsdauLKIa8dEnzX7ZLfQ==
   dependencies:
-    gaxios "^1.0.4"
-    google-p12-pem "^1.0.0"
+    gaxios "^2.0.0"
+    google-p12-pem "^2.0.0"
     jws "^3.1.5"
     mime "^2.2.0"
     pify "^4.0.0"
@@ -5196,9 +5214,10 @@ i18next-sprintf-postprocessor@^0.2.2:
   version "0.2.2"
   resolved "https://registry.yarnpkg.com/i18next-sprintf-postprocessor/-/i18next-sprintf-postprocessor-0.2.2.tgz#2e409f1043579382698b6a2da70cdaa551d67ea4"
 
-i18next@^15.0.9:
-  version "15.0.9"
-  resolved "https://registry.yarnpkg.com/i18next/-/i18next-15.0.9.tgz#42536407a921bb5a8535a4c090a26f16827a1884"
+i18next@^17.0.3:
+  version "17.0.3"
+  resolved "https://registry.yarnpkg.com/i18next/-/i18next-17.0.3.tgz#82d67826d13e8ca2cd2a3c87871533414e952d03"
+  integrity sha512-vQyW6a4ZLt3Dxnd6GXSnhbW5DwGYC4uLPKk1MFE5pfFbR9CEiNatdwwUZDQfrcNOh2x0eOGDFYeCEyLlkLvDQA==
   dependencies:
     "@babel/runtime" "^7.3.1"
 
@@ -5452,9 +5471,10 @@ is-buffer@^1.1.4, is-buffer@^1.1.5, is-buffer@~1.1.1:
   version "1.1.6"
   resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be"
 
-is-buffer@~2.0.3:
+is-buffer@^2.0.2, is-buffer@~2.0.3:
   version "2.0.3"
   resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-2.0.3.tgz#4ecf3fcf749cbd1e472689e109ac66261a25e725"
+  integrity sha512-U15Q7MXTuZlrbymiz95PJpZxu8IlipAp4dtS3wOdgPXx3mqBnslrWU14kxfHB+Py/+2PVKSr37dMAgM2A4uArw==
 
 is-builtin-module@^1.0.0:
   version "1.0.0"
@@ -6201,7 +6221,7 @@ loader-utils@^1.0.1, loader-utils@^1.0.2, loader-utils@^1.1.0:
     emojis-list "^2.0.0"
     json5 "^0.5.0"
 
-loader-utils@^1.2.3:
+loader-utils@^1.2.2, loader-utils@^1.2.3:
   version "1.2.3"
   resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.2.3.tgz#1ff5dc6911c9f0a062531a4c04b609406108c2c7"
   integrity sha512-fkpz8ejdnEMG3s37wGL07iSBDg99O9D5yflE9RGNH3hRdx9SOwYfnGYdZOUIZitN8E+E2vkq3MUMYMvPYl5ZZA==
@@ -6263,7 +6283,12 @@ lodash.has@^4.0, lodash.has@^4.5.2:
   version "4.5.2"
   resolved "https://registry.yarnpkg.com/lodash.has/-/lodash.has-4.5.2.tgz#d19f4dc1095058cccbe2b0cdf4ee0fe4aa37c862"
 
-lodash.isequal@^4.0.0:
+lodash.isboolean@^3.0.3:
+  version "3.0.3"
+  resolved "https://registry.yarnpkg.com/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz#6c2e171db2a257cd96802fd43b01b20d5f5870f6"
+  integrity sha1-bC4XHbKiV82WgC/UOwGyDV9YcPY=
+
+lodash.isequal@^4.0.0, lodash.isequal@^4.5.0:
   version "4.5.0"
   resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0"
   integrity sha1-QVxEePK8wwEgwizhDtMib30+GOA=
@@ -6272,6 +6297,11 @@ lodash.isfinite@^3.3.2:
   version "3.3.2"
   resolved "https://registry.yarnpkg.com/lodash.isfinite/-/lodash.isfinite-3.3.2.tgz#fb89b65a9a80281833f0b7478b3a5104f898ebb3"
 
+lodash.isobject@^3.0.2:
+  version "3.0.2"
+  resolved "https://registry.yarnpkg.com/lodash.isobject/-/lodash.isobject-3.0.2.tgz#3c8fb8d5b5bf4bf90ae06e14f2a530a4ed935e1d"
+  integrity sha1-PI+41bW/S/kK4G4U8qUwpO2TXh0=
+
 lodash.memoize@^4.1.2:
   version "4.1.2"
   resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe"
@@ -6640,18 +6670,19 @@ micromatch@^3.0.4, micromatch@^3.1.10, micromatch@^3.1.4, micromatch@^3.1.8:
     snapdragon "^0.8.1"
     to-regex "^3.0.2"
 
-migrate-mongo@^5.0.1:
-  version "5.0.1"
-  resolved "https://registry.yarnpkg.com/migrate-mongo/-/migrate-mongo-5.0.1.tgz#8829f64edb2df6ccef9d1048cf61759cb9fbfe5f"
+migrate-mongo@^6.0.0:
+  version "6.0.0"
+  resolved "https://registry.yarnpkg.com/migrate-mongo/-/migrate-mongo-6.0.0.tgz#78ca370cb7b22fa6b305da8e44afc5f9b8de6daa"
+  integrity sha512-shNyzAOzHd5mh3Xjs8KGOCWz+u7ea3x8b16oJhQUWazBCHFOMV7DEor+sfjl1a5dmkCq83iS36wHgx4tVp72Vw==
   dependencies:
     cli-table "0.3.1"
-    commander "2.19.0"
+    commander "2.20.0"
     date-fns "1.30.1"
-    fn-args "3.0.0"
-    fs-extra "7.0.1"
+    fn-args "5.0.0"
+    fs-extra "8.0.1"
     lodash "4.17.11"
-    mongodb "3.1.10"
-    p-each-series "1.0.0"
+    mongodb "3.2.7"
+    p-each-series "2.1.0"
 
 miller-rabin@^4.0.0:
   version "4.0.1"
@@ -6879,6 +6910,17 @@ mongodb-core@3.1.9:
   optionalDependencies:
     saslprep "^1.0.0"
 
+mongodb-core@3.2.7:
+  version "3.2.7"
+  resolved "https://registry.yarnpkg.com/mongodb-core/-/mongodb-core-3.2.7.tgz#a8ef1fe764a192c979252dacbc600dc88d77e28f"
+  integrity sha512-WypKdLxFNPOH/Jy6i9z47IjG2wIldA54iDZBmHMINcgKOUcWJh8og+Wix76oGd7EyYkHJKssQ2FAOw5Su/n4XQ==
+  dependencies:
+    bson "^1.1.1"
+    require_optional "^1.0.1"
+    safe-buffer "^5.1.2"
+  optionalDependencies:
+    saslprep "^1.0.0"
+
 mongodb@3.1.10:
   version "3.1.10"
   resolved "https://registry.yarnpkg.com/mongodb/-/mongodb-3.1.10.tgz#45ad9b74ea376f4122d0881b75e5489b9e504ed7"
@@ -6886,6 +6928,14 @@ mongodb@3.1.10:
     mongodb-core "3.1.9"
     safe-buffer "^5.1.2"
 
+mongodb@3.2.7:
+  version "3.2.7"
+  resolved "https://registry.yarnpkg.com/mongodb/-/mongodb-3.2.7.tgz#8ba149e4be708257cad0dea72aebb2bbb311a7ac"
+  integrity sha512-2YdWrdf1PJgxcCrT1tWoL6nHuk6hCxhddAAaEh8QJL231ci4+P9FLyqopbTm2Z2sAU6mhCri+wd9r1hOcHdoMw==
+  dependencies:
+    mongodb-core "3.2.7"
+    safe-buffer "^5.1.2"
+
 mongodb@^2.0.36:
   version "2.2.35"
   resolved "https://registry.yarnpkg.com/mongodb/-/mongodb-2.2.35.tgz#cd1b5af8a9463e3f9a787fa5b3d05565579730f9"
@@ -6964,6 +7014,11 @@ morgan@^1.9.0:
     on-finished "~2.3.0"
     on-headers "~1.0.1"
 
+mousetrap@^1.5.2:
+  version "1.6.3"
+  resolved "https://registry.yarnpkg.com/mousetrap/-/mousetrap-1.6.3.tgz#80fee49665fd478bccf072c9d46bdf1bfed3558a"
+  integrity sha512-bd+nzwhhs9ifsUrC2tWaSgm24/oo2c83zaRyZQF06hYA6sANfsXHtnZ19AbbbDXCDzeH5nZBSQ4NvCjgD62tJA==
+
 move-concurrently@^1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/move-concurrently/-/move-concurrently-1.0.1.tgz#be2c005fda32e0b29af1f05d7c4b33214c701f92"
@@ -7126,9 +7181,10 @@ node-fetch@^1.0.1:
     encoding "^0.1.11"
     is-stream "^1.0.1"
 
-node-fetch@^2.2.0:
-  version "2.3.0"
-  resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.3.0.tgz#1a1d940bbfb916a1d3e0219f037e89e71f8c5fa5"
+node-fetch@^2.3.0:
+  version "2.6.0"
+  resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.0.tgz#e633456386d4aa55863f676a7ab0daa8fdecb0fd"
+  integrity sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA==
 
 node-forge@^0.7.0:
   version "0.7.6"
@@ -7138,7 +7194,7 @@ node-forge@^0.7.1:
   version "0.7.1"
   resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.7.1.tgz#9da611ea08982f4b94206b3beb4cc9665f20c300"
 
-node-forge@^0.8.1:
+node-forge@^0.8.0, node-forge@^0.8.1:
   version "0.8.4"
   resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.8.4.tgz#d6738662b661be19e2711ef01aa3b18212f13030"
   integrity sha512-UOfdpxivIYY4g5tqp5FNRNgROVNxRACUxxJREntJLFaJr1E0UEqFtUIk0F/jYx/E+Y6sVXd0KDi/m5My0yGCVw==
@@ -7395,10 +7451,10 @@ nth-check@^1.0.1:
   dependencies:
     boolbase "~1.0.0"
 
-null-loader@^2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/null-loader/-/null-loader-2.0.0.tgz#1c99da3f0d2c0996b51e9eada3a898a5d57472aa"
-  integrity sha512-PhEeA3v/tAacxC5dNO1i2yXzGVWWrZ9jTx+TMEJ716amvnBXzvrxIwy9HW7MyJsHe8ACQzpiQgbrAjDRMA7gcg==
+null-loader@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/null-loader/-/null-loader-3.0.0.tgz#3e2b6c663c5bda8c73a54357d8fa0708dc61b245"
+  integrity sha512-hf5sNLl8xdRho4UPBOOeoIwT3WhjYcMUQm0zj44EhD6UscMAz72o2udpoDFBgykucdEDGIcd6SXbc/G6zssbzw==
   dependencies:
     loader-utils "^1.2.3"
     schema-utils "^1.0.0"
@@ -7699,11 +7755,10 @@ p-defer@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/p-defer/-/p-defer-1.0.0.tgz#9f6eb182f6c9aa8cd743004a7d4f96b196b0fb0c"
 
-p-each-series@1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/p-each-series/-/p-each-series-1.0.0.tgz#930f3d12dd1f50e7434457a22cd6f04ac6ad7f71"
-  dependencies:
-    p-reduce "^1.0.0"
+p-each-series@2.1.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/p-each-series/-/p-each-series-2.1.0.tgz#961c8dd3f195ea96c747e636b262b800a6b1af48"
+  integrity sha512-ZuRs1miPT4HrjFa+9fRfOFXxGJfORgelKV9f9nNOWw2gl6gVsRaVDOQP0+MI0G0wGKns1Yacsu0GjOFbTK0JFQ==
 
 p-finally@^1.0.0:
   version "1.0.0"
@@ -7742,10 +7797,6 @@ p-locate@^3.0.0:
   dependencies:
     p-limit "^2.0.0"
 
-p-reduce@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/p-reduce/-/p-reduce-1.0.0.tgz#18c2b0dd936a4690a529f8231f58a0fdb6a47dfa"
-
 p-some@^2.0.0:
   version "2.0.1"
   resolved "https://registry.yarnpkg.com/p-some/-/p-some-2.0.1.tgz#65d87c8b154edbcf5221d167778b6d2e150f6f06"
@@ -8598,6 +8649,14 @@ prop-types@^15.5.10, prop-types@^15.5.8:
     loose-envify "^1.3.1"
     object-assign "^4.1.1"
 
+prop-types@^15.6.0, prop-types@^15.7.2:
+  version "15.7.2"
+  resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5"
+  dependencies:
+    loose-envify "^1.4.0"
+    object-assign "^4.1.1"
+    react-is "^16.8.1"
+
 prop-types@^15.6.1:
   version "15.6.1"
   resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.6.1.tgz#36644453564255ddda391191fb3a125cbdf654ca"
@@ -8606,14 +8665,6 @@ prop-types@^15.6.1:
     loose-envify "^1.3.1"
     object-assign "^4.1.1"
 
-prop-types@^15.7.2:
-  version "15.7.2"
-  resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5"
-  dependencies:
-    loose-envify "^1.4.0"
-    object-assign "^4.1.1"
-    react-is "^16.8.1"
-
 proxy-addr@~2.0.2:
   version "2.0.2"
   resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.2.tgz#6571504f47bb988ec8180253f85dd7e14952bdec"
@@ -8869,6 +8920,17 @@ react-frame-component@^4.0.0:
   version "4.0.0"
   resolved "https://registry.yarnpkg.com/react-frame-component/-/react-frame-component-4.0.0.tgz#57d51cdb2da3b204cc34577349f9f5bb84a76aac"
 
+react-hotkeys@^1.1.4:
+  version "1.1.4"
+  resolved "https://registry.yarnpkg.com/react-hotkeys/-/react-hotkeys-1.1.4.tgz#a0712aa2e0c03a759fd7885808598497a4dace72"
+  integrity sha1-oHEqouDAOnWf14hYCFmEl6TaznI=
+  dependencies:
+    lodash.isboolean "^3.0.3"
+    lodash.isequal "^4.5.0"
+    lodash.isobject "^3.0.2"
+    mousetrap "^1.5.2"
+    prop-types "^15.6.0"
+
 react-i18next@^10.6.1:
   version "10.6.1"
   resolved "https://registry.yarnpkg.com/react-i18next/-/react-i18next-10.6.1.tgz#044c39fb463a8d96cc548509187a1bb316e660fa"
@@ -9661,6 +9723,11 @@ semver@^5.5.1:
   version "5.6.0"
   resolved "https://registry.yarnpkg.com/semver/-/semver-5.6.0.tgz#7e74256fbaa49c75aa7c7a205cc22799cac80004"
 
+semver@^6.0.0:
+  version "6.1.1"
+  resolved "https://registry.yarnpkg.com/semver/-/semver-6.1.1.tgz#53f53da9b30b2103cd4f15eab3a18ecbcb210c9b"
+  integrity sha512-rWYq2e5iYW+fFe/oPPtYJxYgjBm8sC4rmoGdUOgBB7VnwKt6HrL793l2voH1UlsyYZpJ4g0wfjnTEO1s1NP2eQ==
+
 semver@~5.3.0:
   version "5.3.0"
   resolved "https://registry.yarnpkg.com/semver/-/semver-5.3.0.tgz#9b2ce5d3de02d17c6012ad326aa6b4d0cf54f94f"