kaori 4 лет назад
Родитель
Сommit
57a8b57c28

+ 2 - 2
packages/app/package.json

@@ -175,7 +175,7 @@
     "css-loader": "^3.0.0",
     "css-loader": "^3.0.0",
     "csv-to-markdown-table": "^1.0.1",
     "csv-to-markdown-table": "^1.0.1",
     "diff2html": "^3.1.2",
     "diff2html": "^3.1.2",
-    "eazy-logger": "^3.0.2",
+    "eazy-logger": "^3.1.0",
     "file-loader": "^5.0.2",
     "file-loader": "^5.0.2",
     "handsontable": "=6.2.2",
     "handsontable": "=6.2.2",
     "hard-source-webpack-plugin": "^0.13.1",
     "hard-source-webpack-plugin": "^0.13.1",
@@ -243,7 +243,7 @@
     "unstated": "^2.1.1",
     "unstated": "^2.1.1",
     "webpack": "^4.39.3",
     "webpack": "^4.39.3",
     "webpack-assets-manifest": "^3.1.1",
     "webpack-assets-manifest": "^3.1.1",
-    "webpack-bundle-analyzer": "^3.0.2",
+    "webpack-bundle-analyzer": "^3.9.0",
     "webpack-cli": "^3.3.7",
     "webpack-cli": "^3.3.7",
     "webpack-merge": "^4.2.2"
     "webpack-merge": "^4.2.2"
   }
   }

+ 1 - 1
packages/app/src/client/app.jsx

@@ -98,7 +98,7 @@ Object.assign(componentMappings, {
   'not-found-page': <NotFoundPage />,
   'not-found-page': <NotFoundPage />,
   'not-found-alert': <NotFoundAlert
   'not-found-alert': <NotFoundAlert
     isGuestUserMode={appContainer.isGuestUser}
     isGuestUserMode={appContainer.isGuestUser}
-    isHidden={pageContainer.state.isNotCreatable || pageContainer.state.isTrashPage}
+    isHidden={pageContainer.state.pageId != null ? (pageContainer.state.isNotCreatable ?? pageContainer.state.isTrashPage) : false} // !!DO NOT MOVE THIS!! https://github.com/weseek/growi/pull/4899
   />,
   />,
 
 
   'forbidden-page': <ForbiddenPage />,
   'forbidden-page': <ForbiddenPage />,

+ 3 - 0
packages/app/src/client/interfaces/focusable.ts

@@ -0,0 +1,3 @@
+export interface IFocusable {
+  focus: () => void,
+}

+ 13 - 0
packages/app/src/client/interfaces/react-bootstrap-typeahead.ts

@@ -0,0 +1,13 @@
+// https://github.com/ericgio/react-bootstrap-typeahead/blob/3.x/docs/Props.md
+export type TypeaheadProps = {
+  dropup?: boolean,
+  emptyLabel?: string,
+  placeholder?: string,
+  autoFocus?: boolean,
+
+  onChange?: (data: unknown[]) => void,
+  onBlur?: () => void,
+  onFocus?: () => void,
+  onInputChange?: (text: string) => void,
+  onKeyDown?: (input: string) => void,
+};

+ 6 - 3
packages/app/src/client/services/ContextExtractor.tsx

@@ -2,7 +2,7 @@ import React, { FC, useEffect, useState } from 'react';
 import { pagePathUtils } from '@growi/core';
 import { pagePathUtils } from '@growi/core';
 
 
 import {
 import {
-  useCreatedAt, useDeleteUsername, useDeletedAt, useHasChildren, useHasDraftOnHackmd, useIsAbleToDeleteCompletely,
+  useCurrentCreatedAt, useDeleteUsername, useDeletedAt, useHasChildren, useHasDraftOnHackmd, useIsAbleToDeleteCompletely,
   useIsDeletable, useIsDeleted, useIsNotCreatable, useIsPageExist, useIsTrashPage, useIsUserPage, useLastUpdateUsername,
   useIsDeletable, useIsDeleted, useIsNotCreatable, useIsPageExist, useIsTrashPage, useIsUserPage, useLastUpdateUsername,
   usePageId, usePageIdOnHackmd, usePageUser, useCurrentPagePath, useRevisionCreatedAt, useRevisionId, useRevisionIdHackmdSynced,
   usePageId, usePageIdOnHackmd, usePageUser, useCurrentPagePath, useRevisionCreatedAt, useRevisionId, useRevisionIdHackmdSynced,
   useShareLinkId, useShareLinksNumber, useTemplateTagData, useCurrentUpdatedAt, useCreator, useRevisionAuthor, useCurrentUser,
   useShareLinkId, useShareLinksNumber, useTemplateTagData, useCurrentUpdatedAt, useCreator, useRevisionAuthor, useCurrentUser,
@@ -39,7 +39,10 @@ const ContextExtractorOnce: FC = () => {
   const path = decodeURI(mainContent?.getAttribute('data-path') || '');
   const path = decodeURI(mainContent?.getAttribute('data-path') || '');
   const pageId = mainContent?.getAttribute('data-page-id') || null;
   const pageId = mainContent?.getAttribute('data-page-id') || null;
   const revisionCreatedAt = +(mainContent?.getAttribute('data-page-revision-created') || '');
   const revisionCreatedAt = +(mainContent?.getAttribute('data-page-revision-created') || '');
-  const createdAt = mainContent?.getAttribute('data-page-created-at');
+
+  const createdAtAttribute = mainContent?.getAttribute('data-page-created-at');
+  const createdAt: Date | null = (createdAtAttribute != null) ? new Date(createdAtAttribute) : null;
+
   const updatedAt = mainContent?.getAttribute('data-page-updated-at');
   const updatedAt = mainContent?.getAttribute('data-page-updated-at');
   const deletedAt = mainContent?.getAttribute('data-page-deleted-at') || null;
   const deletedAt = mainContent?.getAttribute('data-page-deleted-at') || null;
   const isUserPage = JSON.parse(mainContent?.getAttribute('data-page-user') || jsonNull);
   const isUserPage = JSON.parse(mainContent?.getAttribute('data-page-user') || jsonNull);
@@ -76,7 +79,7 @@ const ContextExtractorOnce: FC = () => {
   useCurrentProductNavWidth(userUISettings?.currentProductNavWidth);
   useCurrentProductNavWidth(userUISettings?.currentProductNavWidth);
 
 
   // Page
   // Page
-  useCreatedAt(createdAt);
+  useCurrentCreatedAt(createdAt);
   useDeleteUsername(deleteUsername);
   useDeleteUsername(deleteUsername);
   useDeletedAt(deletedAt);
   useDeletedAt(deletedAt);
   useHasChildren(hasChildren);
   useHasChildren(hasChildren);

+ 21 - 2
packages/app/src/components/Navbar/AuthorInfo.jsx

@@ -24,9 +24,26 @@ const AuthorInfo = (props) => {
     : <i>Unknown</i>;
     : <i>Unknown</i>;
 
 
   if (locate === 'footer') {
   if (locate === 'footer') {
-    return <p>{infoLabelForFooter} {format(new Date(date), formatType)} by <UserPicture user={user} size="sm" /> {userLabel}</p>;
+    try {
+      return <p>{infoLabelForFooter} {format(new Date(date), formatType)} by <UserPicture user={user} size="sm" /> {userLabel}</p>;
+    }
+    catch (err) {
+      if (err instanceof RangeError) {
+        return <p>Created by <UserPicture user={user} size="sm" /> {userLabel}</p>;
+      }
+      return;
+    }
   }
   }
 
 
+  const renderParsedDate = () => {
+    try {
+      return format(new Date(date), formatType);
+    }
+    catch (err) {
+      return '';
+    }
+  };
+
   return (
   return (
     <div className="d-flex align-items-center">
     <div className="d-flex align-items-center">
       <div className="mr-2">
       <div className="mr-2">
@@ -34,7 +51,9 @@ const AuthorInfo = (props) => {
       </div>
       </div>
       <div>
       <div>
         <div>{infoLabelForSubNav} {userLabel}</div>
         <div>{infoLabelForSubNav} {userLabel}</div>
-        <div className="text-muted text-date">{format(new Date(date), formatType)}</div>
+        <div className="text-muted text-date">
+          {renderParsedDate()}
+        </div>
       </div>
       </div>
     </div>
     </div>
   );
   );

+ 0 - 108
packages/app/src/components/Navbar/GlobalSearch.jsx

@@ -1,108 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import { withTranslation } from 'react-i18next';
-
-import { withUnstatedContainers } from '../UnstatedUtils';
-import AppContainer from '~/client/services/AppContainer';
-
-import SearchForm from '../SearchForm';
-
-
-class GlobalSearch extends React.Component {
-
-  constructor(props) {
-    super(props);
-
-    const isSearchScopeChildrenAsDefault = this.props.appContainer.getConfig().isSearchScopeChildrenAsDefault;
-
-    this.state = {
-      text: '',
-      isScopeChildren: isSearchScopeChildrenAsDefault,
-    };
-
-    this.onInputChange = this.onInputChange.bind(this);
-    this.onClickAllPages = this.onClickAllPages.bind(this);
-    this.onClickChildren = this.onClickChildren.bind(this);
-    this.search = this.search.bind(this);
-  }
-
-  onInputChange(text) {
-    this.setState({ text });
-  }
-
-  onClickAllPages() {
-    this.setState({ isScopeChildren: false });
-  }
-
-  onClickChildren() {
-    this.setState({ isScopeChildren: true });
-  }
-
-  search() {
-    const url = new URL(window.location.href);
-    url.pathname = '/_search';
-
-    // construct search query
-    let q = this.state.text;
-    if (this.state.isScopeChildren) {
-      q += ` prefix:${window.location.pathname}`;
-    }
-    url.searchParams.append('q', q);
-
-    window.location.href = url.href;
-  }
-
-  render() {
-    const { t, appContainer, dropup } = this.props;
-    const scopeLabel = this.state.isScopeChildren
-      ? t('header_search_box.label.This tree')
-      : t('header_search_box.label.All pages');
-
-    const config = appContainer.getConfig();
-    const isReachable = config.isSearchServiceReachable;
-
-    return (
-      <div className={`form-group mb-0 d-print-none ${isReachable ? '' : 'has-error'}`}>
-        <div className="input-group flex-nowrap">
-          <div className={`input-group-prepend ${dropup ? 'dropup' : ''}`}>
-            <button className="btn btn-secondary dropdown-toggle py-0" type="button" data-toggle="dropdown" aria-haspopup="true">
-              {scopeLabel}
-            </button>
-            <div className="dropdown-menu">
-              <button className="dropdown-item" type="button" onClick={this.onClickAllPages}>{ t('header_search_box.item_label.All pages') }</button>
-              <button className="dropdown-item" type="button" onClick={this.onClickChildren}>{ t('header_search_box.item_label.This tree') }</button>
-            </div>
-          </div>
-          <SearchForm
-            t={this.props.t}
-            crowi={this.props.appContainer}
-            onInputChange={this.onInputChange}
-            onSubmit={this.search}
-            placeholder="Search ..."
-            dropup={dropup}
-          />
-          <div className="btn-group-submit-search">
-            <span className="btn-link text-decoration-none" onClick={this.search}>
-              <i className="icon-magnifier"></i>
-            </span>
-          </div>
-        </div>
-      </div>
-    );
-  }
-
-}
-
-GlobalSearch.propTypes = {
-  t: PropTypes.func.isRequired, // i18next
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
-
-  dropup: PropTypes.bool,
-};
-
-/**
- * Wrapper component for using unstated
- */
-const GlobalSearchWrapper = withUnstatedContainers(GlobalSearch, [AppContainer]);
-
-export default withTranslation()(GlobalSearchWrapper);

+ 94 - 0
packages/app/src/components/Navbar/GlobalSearch.tsx

@@ -0,0 +1,94 @@
+import React, {
+  FC, useState, useCallback,
+} from 'react';
+import { useTranslation } from 'react-i18next';
+
+import AppContainer from '~/client/services/AppContainer';
+import { IPage } from '~/interfaces/page';
+
+import { withUnstatedContainers } from '../UnstatedUtils';
+
+import SearchForm from '../SearchForm';
+
+
+type Props = {
+  appContainer: AppContainer,
+
+  dropup?: boolean,
+}
+
+const GlobalSearch: FC<Props> = (props: Props) => {
+  const { appContainer, dropup } = props;
+  const { t } = useTranslation();
+
+  const [text, setText] = useState('');
+  const [isScopeChildren, setScopeChildren] = useState<boolean>(appContainer.getConfig().isSearchScopeChildrenAsDefault);
+
+  const gotoPage = useCallback((data: unknown[]) => {
+    const page = data[0] as IPage; // should be single page selected
+
+    // navigate to page
+    if (page != null) {
+      window.location.href = page.path;
+    }
+  }, []);
+
+  const search = useCallback(() => {
+    const url = new URL(window.location.href);
+    url.pathname = '/_search';
+
+    // construct search query
+    let q = text;
+    if (isScopeChildren) {
+      q += ` prefix:${window.location.pathname}`;
+    }
+    url.searchParams.append('q', q);
+
+    window.location.href = url.href;
+  }, [isScopeChildren, text]);
+
+  const scopeLabel = isScopeChildren
+    ? t('header_search_box.label.This tree')
+    : t('header_search_box.label.All pages');
+
+  const isSearchServiceReachable = appContainer.getConfig().isSearchServiceReachable;
+
+  return (
+    <div className={`form-group mb-0 d-print-none ${isSearchServiceReachable ? '' : 'has-error'}`}>
+      <div className="input-group flex-nowrap">
+        <div className={`input-group-prepend ${dropup ? 'dropup' : ''}`}>
+          <button className="btn btn-secondary dropdown-toggle py-0" type="button" data-toggle="dropdown" aria-haspopup="true">
+            {scopeLabel}
+          </button>
+          <div className="dropdown-menu">
+            <button className="dropdown-item" type="button" onClick={() => setScopeChildren(false)}>
+              { t('header_search_box.item_label.All pages') }
+            </button>
+            <button className="dropdown-item" type="button" onClick={() => setScopeChildren(true)}>
+              { t('header_search_box.item_label.This tree') }
+            </button>
+          </div>
+        </div>
+        <SearchForm
+          isSearchServiceReachable={isSearchServiceReachable}
+          dropup={dropup}
+          onChange={gotoPage}
+          onInputChange={text => setText(text)}
+          onSubmit={search}
+        />
+        <div className="btn-group-submit-search">
+          <span className="btn-link text-decoration-none" onClick={search}>
+            <i className="icon-magnifier"></i>
+          </span>
+        </div>
+      </div>
+    </div>
+  );
+};
+
+/**
+ * Wrapper component for using unstated
+ */
+const GlobalSearchWrapper = withUnstatedContainers(GlobalSearch, [AppContainer]);
+
+export default GlobalSearchWrapper;

+ 3 - 2
packages/app/src/components/Navbar/GrowiSubNavigation.jsx

@@ -11,7 +11,7 @@ import PageContainer from '~/client/services/PageContainer';
 import {
 import {
   EditorMode, useDrawerMode, useEditorMode, useIsDeviceSmallerThanMd,
   EditorMode, useDrawerMode, useEditorMode, useIsDeviceSmallerThanMd,
 } from '~/stores/ui';
 } from '~/stores/ui';
-import { useCurrentUpdatedAt } from '~/stores/context';
+import { useCurrentCreatedAt, useCurrentUpdatedAt } from '~/stores/context';
 
 
 import CopyDropdown from '../Page/CopyDropdown';
 import CopyDropdown from '../Page/CopyDropdown';
 import TagLabels from '../Page/TagLabels';
 import TagLabels from '../Page/TagLabels';
@@ -71,13 +71,14 @@ const GrowiSubNavigation = (props) => {
   const { data: isDeviceSmallerThanMd } = useIsDeviceSmallerThanMd();
   const { data: isDeviceSmallerThanMd } = useIsDeviceSmallerThanMd();
   const { data: isDrawerMode } = useDrawerMode();
   const { data: isDrawerMode } = useDrawerMode();
   const { data: editorMode, mutate: mutateEditorMode } = useEditorMode();
   const { data: editorMode, mutate: mutateEditorMode } = useEditorMode();
+  const { data: createdAt } = useCurrentCreatedAt();
   const { data: updatedAt } = useCurrentUpdatedAt();
   const { data: updatedAt } = useCurrentUpdatedAt();
 
 
   const {
   const {
     appContainer, pageContainer, isCompactMode,
     appContainer, pageContainer, isCompactMode,
   } = props;
   } = props;
   const {
   const {
-    pageId, path, createdAt, creator, revisionAuthor, isPageExist,
+    pageId, path, creator, revisionAuthor, isPageExist,
   } = pageContainer.state;
   } = pageContainer.state;
 
 
   const { isGuestUser } = appContainer;
   const { isGuestUser } = appContainer;

+ 4 - 2
packages/app/src/components/PageContentFooter.jsx

@@ -6,12 +6,14 @@ import AuthorInfo from './Navbar/AuthorInfo';
 import AppContainer from '~/client/services/AppContainer';
 import AppContainer from '~/client/services/AppContainer';
 import PageContainer from '~/client/services/PageContainer';
 import PageContainer from '~/client/services/PageContainer';
 import { withUnstatedContainers } from './UnstatedUtils';
 import { withUnstatedContainers } from './UnstatedUtils';
-import { useCurrentUpdatedAt } from '~/stores/context';
+import { useCurrentCreatedAt, useCurrentUpdatedAt } from '~/stores/context';
 
 
 const PageContentFooter = (props) => {
 const PageContentFooter = (props) => {
   const { pageContainer } = props;
   const { pageContainer } = props;
+  const { data: createdAt } = useCurrentCreatedAt();
+
   const {
   const {
-    createdAt, creator, revisionAuthor,
+    creator, revisionAuthor,
   } = pageContainer.state;
   } = pageContainer.state;
 
 
   const { data: updatedAt } = useCurrentUpdatedAt();
   const { data: updatedAt } = useCurrentUpdatedAt();

+ 0 - 177
packages/app/src/components/SearchForm.jsx

@@ -1,177 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import { withUnstatedContainers } from './UnstatedUtils';
-import AppContainer from '~/client/services/AppContainer';
-
-import SearchTypeahead from './SearchTypeahead';
-
-// SearchTypeahead wrapper
-class SearchForm extends React.Component {
-
-  constructor(props) {
-    super(props);
-
-    this.state = {
-      searchError: null,
-      isShownHelp: false,
-    };
-
-    this.onSearchError = this.onSearchError.bind(this);
-    this.onChange = this.onChange.bind(this);
-    this.onBlur = this.onBlur.bind(this);
-    this.onFocus = this.onFocus.bind(this);
-  }
-
-  componentDidMount() {
-  }
-
-  componentWillUnmount() {
-  }
-
-  onSearchError(err) {
-    this.setState({
-      searchError: err,
-    });
-  }
-
-  onChange(selected) {
-    const page = selected[0]; // should be single page selected
-
-    // navigate to page
-    if (page != null) {
-      window.location = page.path;
-    }
-  }
-
-  onBlur() {
-    this.setState({
-      isShownHelp: false,
-    });
-
-    this.getHelpElement();
-  }
-
-  onFocus() {
-    this.setState({
-      isShownHelp: true,
-    });
-  }
-
-  getHelpElement() {
-    const { t, appContainer } = this.props;
-    const { isShownHelp } = this.state;
-
-    const config = appContainer.getConfig();
-    const isReachable = config.isSearchServiceReachable;
-
-
-    if (!isReachable) {
-      return (
-        <>
-          <h5 className="text-danger">Error occured on Search Service</h5>
-          Try to reconnect from management page.
-        </>
-      );
-    }
-
-    if (!isShownHelp) {
-      return <></>;
-    }
-
-    return (
-      <table className="table grw-search-table search-help m-0">
-        <caption className="text-left text-primary p-2">
-          <h5 className="h6"><i className="icon-magnifier pr-2 mb-2" />{ t('search_help.title') }</h5>
-        </caption>
-        <tbody>
-          <tr>
-            <th className="py-2">
-              <code>word1</code> <code>word2</code><br></br>
-              <small>({ t('search_help.and.syntax help') })</small>
-            </th>
-            <td><h6 className="m-0">{ t('search_help.and.desc', { word1: 'word1', word2: 'word2' }) }</h6></td>
-          </tr>
-          <tr>
-            <th className="py-2">
-              <code>&quot;This is GROWI&quot;</code><br></br>
-              <small>({ t('search_help.phrase.syntax help') })</small>
-            </th>
-            <td><h6 className="m-0">{ t('search_help.phrase.desc', { phrase: 'This is GROWI' }) }</h6></td>
-          </tr>
-          <tr>
-            <th className="py-2"><code>-keyword</code></th>
-            <td><h6 className="m-0">{ t('search_help.exclude.desc', { word: 'keyword' }) }</h6></td>
-          </tr>
-          <tr>
-            <th className="py-2"><code>prefix:/user/</code></th>
-            <td><h6 className="m-0">{ t('search_help.prefix.desc', { path: '/user/' }) }</h6></td>
-          </tr>
-          <tr>
-            <th className="py-2"><code>-prefix:/user/</code></th>
-            <td><h6 className="m-0">{ t('search_help.exclude_prefix.desc', { path: '/user/' }) }</h6></td>
-          </tr>
-          <tr>
-            <th className="py-2"><code>tag:wiki</code></th>
-            <td><h6 className="m-0">{ t('search_help.tag.desc', { tag: 'wiki' }) }</h6></td>
-          </tr>
-          <tr>
-            <th className="py-2"><code>-tag:wiki</code></th>
-            <td><h6 className="m-0">{ t('search_help.exclude_tag.desc', { tag: 'wiki' }) }</h6></td>
-          </tr>
-        </tbody>
-      </table>
-    );
-  }
-
-  render() {
-    const { t, appContainer, dropup } = this.props;
-
-    const config = appContainer.getConfig();
-    const isReachable = config.isSearchServiceReachable;
-
-    const placeholder = isReachable
-      ? 'Search ...'
-      : 'Error on Search Service';
-    const emptyLabel = (this.state.searchError !== null)
-      ? 'Error on searching.'
-      : t('search.search page bodies');
-
-    return (
-      <SearchTypeahead
-        dropup={dropup}
-        onChange={this.onChange}
-        onSubmit={this.props.onSubmit}
-        onInputChange={this.props.onInputChange}
-        onSearchError={this.onSearchError}
-        emptyLabel={emptyLabel}
-        placeholder={placeholder}
-        helpElement={this.getHelpElement()}
-        keywordOnInit={this.props.keyword}
-        onBlur={this.onBlur}
-        onFocus={this.onFocus}
-      />
-    );
-  }
-
-}
-
-/**
- * Wrapper component for using unstated
- */
-const SearchFormWrapper = withUnstatedContainers(SearchForm, [AppContainer]);
-
-SearchForm.propTypes = {
-  t: PropTypes.func.isRequired, // i18next
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
-
-  dropup: PropTypes.bool,
-  keyword: PropTypes.string,
-  onSubmit: PropTypes.func.isRequired,
-  onInputChange: PropTypes.func,
-};
-
-SearchForm.defaultProps = {
-  onInputChange: () => {},
-};
-
-export default SearchFormWrapper;

+ 140 - 0
packages/app/src/components/SearchForm.tsx

@@ -0,0 +1,140 @@
+import React, {
+  FC, forwardRef, ForwardRefRenderFunction, useImperativeHandle,
+  useRef, useState,
+} from 'react';
+import { useTranslation } from 'react-i18next';
+
+import { IFocusable } from '~/client/interfaces/focusable';
+
+import SearchTypeahead from './SearchTypeahead';
+
+
+type SearchFormHelpProps = {
+  isReachable: boolean,
+  isShownHelp: boolean,
+}
+
+const SearchFormHelp: FC<SearchFormHelpProps> = (props: SearchFormHelpProps) => {
+  const { t } = useTranslation();
+
+  const { isReachable, isShownHelp } = props;
+
+  if (!isReachable) {
+    return (
+      <>
+        <h5 className="text-danger">Error occured on Search Service</h5>
+        Try to reconnect from management page.
+      </>
+    );
+  }
+
+  if (!isShownHelp) {
+    return <></>;
+  }
+
+  return (
+    <table className="table grw-search-table search-help m-0">
+      <caption className="text-left text-primary p-2">
+        <h5 className="h6"><i className="icon-magnifier pr-2 mb-2" />{ t('search_help.title') }</h5>
+      </caption>
+      <tbody>
+        <tr>
+          <th className="py-2">
+            <code>word1</code> <code>word2</code><br></br>
+            <small>({ t('search_help.and.syntax help') })</small>
+          </th>
+          <td><h6 className="m-0">{ t('search_help.and.desc', { word1: 'word1', word2: 'word2' }) }</h6></td>
+        </tr>
+        <tr>
+          <th className="py-2">
+            <code>&quot;This is GROWI&quot;</code><br></br>
+            <small>({ t('search_help.phrase.syntax help') })</small>
+          </th>
+          <td><h6 className="m-0">{ t('search_help.phrase.desc', { phrase: 'This is GROWI' }) }</h6></td>
+        </tr>
+        <tr>
+          <th className="py-2"><code>-keyword</code></th>
+          <td><h6 className="m-0">{ t('search_help.exclude.desc', { word: 'keyword' }) }</h6></td>
+        </tr>
+        <tr>
+          <th className="py-2"><code>prefix:/user/</code></th>
+          <td><h6 className="m-0">{ t('search_help.prefix.desc', { path: '/user/' }) }</h6></td>
+        </tr>
+        <tr>
+          <th className="py-2"><code>-prefix:/user/</code></th>
+          <td><h6 className="m-0">{ t('search_help.exclude_prefix.desc', { path: '/user/' }) }</h6></td>
+        </tr>
+        <tr>
+          <th className="py-2"><code>tag:wiki</code></th>
+          <td><h6 className="m-0">{ t('search_help.tag.desc', { tag: 'wiki' }) }</h6></td>
+        </tr>
+        <tr>
+          <th className="py-2"><code>-tag:wiki</code></th>
+          <td><h6 className="m-0">{ t('search_help.exclude_tag.desc', { tag: 'wiki' }) }</h6></td>
+        </tr>
+      </tbody>
+    </table>
+  );
+};
+
+
+type Props = {
+  isSearchServiceReachable: boolean,
+
+  dropup?: boolean,
+  keyword?: string,
+  onChange?: (data: unknown[]) => void,
+  onSubmit?: (input: string) => void,
+  onInputChange?: (text: string) => void,
+};
+
+
+const SearchForm: ForwardRefRenderFunction<IFocusable, Props> = (props: Props, ref) => {
+  const { t } = useTranslation();
+  const {
+    isSearchServiceReachable, dropup,
+    onChange, onSubmit, onInputChange,
+  } = props;
+
+  const [searchError, setSearchError] = useState<Error | null>(null);
+  const [isShownHelp, setShownHelp] = useState(false);
+
+  const searchTyheaheadRef = useRef<IFocusable>(null);
+
+  // publish focus()
+  useImperativeHandle(ref, () => ({
+    focus() {
+      const instance = searchTyheaheadRef?.current;
+      if (instance != null) {
+        instance.focus();
+      }
+    },
+  }));
+
+  const placeholder = isSearchServiceReachable
+    ? 'Search ...'
+    : 'Error on Search Service';
+
+  const emptyLabel = (searchError != null)
+    ? 'Error on searching.'
+    : t('search.search page bodies');
+
+  return (
+    <SearchTypeahead
+      ref={searchTyheaheadRef}
+      dropup={dropup}
+      emptyLabel={emptyLabel}
+      placeholder={placeholder}
+      onChange={onChange}
+      onSubmit={onSubmit}
+      onInputChange={onInputChange}
+      onSearchError={err => setSearchError(err)}
+      onBlur={() => setShownHelp(false)}
+      onFocus={() => setShownHelp(true)}
+      helpElement={<SearchFormHelp isShownHelp={isShownHelp} isReachable={isSearchServiceReachable} />}
+      keywordOnInit={props.keyword}
+    />
+  );
+};
+
+export default forwardRef(SearchForm);

+ 4 - 1
packages/app/src/components/SearchPage/SearchPageForm.jsx

@@ -31,11 +31,14 @@ class SearchPageForm extends React.Component {
   }
   }
 
 
   render() {
   render() {
+    const { appContainer } = this.props;
+    const isSearchServiceReachable = appContainer.getConfig().isSearchServiceReachable;
+
     return (
     return (
       <div className="input-group mb-3 d-flex">
       <div className="input-group mb-3 d-flex">
         <div className="flex-fill">
         <div className="flex-fill">
           <SearchForm
           <SearchForm
-            t={this.props.t}
+            isSearchServiceReachable={isSearchServiceReachable}
             onSubmit={this.search}
             onSubmit={this.search}
             keyword={this.state.searchedKeyword}
             keyword={this.state.searchedKeyword}
             onInputChange={this.onInputChange}
             onInputChange={this.onInputChange}

+ 0 - 274
packages/app/src/components/SearchTypeahead.jsx

@@ -1,274 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-
-import { noop } from 'lodash/noop';
-import { AsyncTypeahead } from 'react-bootstrap-typeahead';
-
-import { UserPicture, PageListMeta, PagePathLabel } from '@growi/ui';
-
-import AppContainer from '~/client/services/AppContainer';
-import { withUnstatedContainers } from './UnstatedUtils';
-
-class SearchTypeahead extends React.Component {
-
-  constructor(props) {
-
-    super(props);
-
-    this.state = {
-      input: this.props.keywordOnInit,
-      pages: [],
-      isLoading: false,
-      searchError: null,
-    };
-
-    this.restoreInitialData = this.restoreInitialData.bind(this);
-    this.clearKeyword = this.clearKeyword.bind(this);
-    this.changeKeyword = this.changeKeyword.bind(this);
-    this.search = this.search.bind(this);
-    this.onInputChange = this.onInputChange.bind(this);
-    this.onKeyDown = this.onKeyDown.bind(this);
-    this.dispatchSubmit = this.dispatchSubmit.bind(this);
-    this.getEmptyLabel = this.getEmptyLabel.bind(this);
-    this.getResetFormButton = this.getResetFormButton.bind(this);
-    this.renderMenuItemChildren = this.renderMenuItemChildren.bind(this);
-    this.getTypeahead = this.getTypeahead.bind(this);
-  }
-
-  /**
-   * Get instance of AsyncTypeahead
-   */
-  getTypeahead() {
-    return this.typeahead ? this.typeahead.getInstance() : null;
-  }
-
-  componentDidMount() {
-  }
-
-  componentWillUnmount() {
-  }
-
-  /**
-   * Initialize keywordyword
-   */
-  restoreInitialData() {
-    this.changeKeyword(this.props.keywordOnInit);
-  }
-
-  /**
-   * clear keyword
-   */
-  clearKeyword(text) {
-    this.changeKeyword('');
-  }
-
-  /**
-   * change keyword
-   */
-  changeKeyword(text) {
-    // see https://github.com/ericgio/react-bootstrap-typeahead/issues/266#issuecomment-414987723
-    const instance = this.typeahead.getInstance();
-    instance.clear();
-    instance.setState({ text });
-  }
-
-  search(keyword) {
-
-    if (keyword === '') {
-      return;
-    }
-
-    this.setState({ isLoading: true });
-
-    this.props.appContainer.apiGet('/search', { q: keyword })
-      .then((res) => { this.onSearchSuccess(res) })
-      .catch((err) => { this.onSearchError(err) });
-  }
-
-  /**
-   * Callback function which is occured when search is exit successfully
-   * @param {*} pages
-   */
-  onSearchSuccess(res) {
-    this.setState({
-      isLoading: false,
-      pages: res.data,
-    });
-    if (this.props.onSearchSuccess != null) {
-      this.props.onSearchSuccess(res);
-    }
-  }
-
-  /**
-   * Callback function which is occured when search is exit abnormaly
-   * @param {*} err
-   */
-  onSearchError(err) {
-    this.setState({
-      isLoading: false,
-      searchError: err,
-    });
-    if (this.props.onSearchError != null) {
-      this.props.onSearchError(err);
-    }
-  }
-
-  onInputChange(text) {
-    this.setState({ input: text });
-    this.props.onInputChange(text);
-    if (text === '') {
-      this.setState({ pages: [] });
-    }
-  }
-
-  onKeyDown(event) {
-    if (event.keyCode === 13) {
-      this.dispatchSubmit();
-    }
-  }
-
-  dispatchSubmit() {
-    if (this.props.onSubmit != null) {
-      this.props.onSubmit(this.state.input);
-    }
-  }
-
-  getEmptyLabel() {
-    const { emptyLabel, helpElement } = this.props;
-    const { input } = this.state;
-
-    // show help element if empty
-    if (input.length === 0) {
-      return helpElement;
-    }
-
-    // use props.emptyLabel as is if defined
-    if (emptyLabel !== undefined) {
-      return this.props.emptyLabel;
-    }
-
-    let emptyLabelExceptError = 'No matches found on title...';
-    if (this.props.emptyLabelExceptError !== undefined) {
-      emptyLabelExceptError = this.props.emptyLabelExceptError;
-    }
-
-    return (this.state.searchError !== null)
-      ? 'Error on searching.'
-      : emptyLabelExceptError;
-  }
-
-  /**
-   * Get restore form button to initialize button
-   */
-  getResetFormButton() {
-    const isClearBtn = this.props.behaviorOfResetBtn === 'clear';
-    const initialKeyword = isClearBtn ? '' : this.props.keywordOnInit;
-    const isHidden = this.state.input === initialKeyword;
-    const resetForm = isClearBtn ? this.clearKeyword : this.restoreInitialData;
-
-    return isHidden ? (
-      <span />
-    ) : (
-      <button type="button" className="btn btn-link search-clear" onMouseDown={resetForm}>
-        <i className="icon-close" />
-      </button>
-    );
-  }
-
-  renderMenuItemChildren(option, props, index) {
-    const page = option;
-    return (
-      <span>
-        <UserPicture user={page.lastUpdateUser} size="sm" noLink />
-        <span className="ml-1 text-break text-wrap"><PagePathLabel page={page} /></span>
-        <PageListMeta page={page} />
-      </span>
-    );
-  }
-
-  render() {
-    const defaultSelected = (this.props.keywordOnInit !== '')
-      ? [{ path: this.props.keywordOnInit }]
-      : [];
-    const inputProps = { autoComplete: 'off' };
-    if (this.props.inputName != null) {
-      inputProps.name = this.props.inputName;
-    }
-
-    const resetFormButton = this.getResetFormButton();
-
-    return (
-      <div className="search-typeahead">
-        <AsyncTypeahead
-          {...this.props}
-          id="search-typeahead-asynctypeahead"
-          ref={(c) => { this.typeahead = c }}
-          inputProps={inputProps}
-          isLoading={this.state.isLoading}
-          labelKey="path"
-          minLength={0}
-          options={this.state.pages} // Search result (Some page names)
-          promptText={this.props.helpElement}
-          emptyLabel={this.getEmptyLabel()}
-          align="left"
-          submitFormOnEnter
-          onSearch={this.search}
-          onInputChange={this.onInputChange}
-          onKeyDown={this.onKeyDown}
-          renderMenuItemChildren={this.renderMenuItemChildren}
-          caseSensitive={false}
-          defaultSelected={defaultSelected}
-          autoFocus={this.props.autoFocus}
-          onBlur={this.props.onBlur}
-          onFocus={this.props.onFocus}
-        />
-        {resetFormButton}
-      </div>
-    );
-  }
-
-}
-
-/**
- * Wrapper component for using unstated
- */
-const SearchTypeaheadWrapper = withUnstatedContainers(SearchTypeahead, [AppContainer]);
-
-/**
- * Properties
- */
-SearchTypeahead.propTypes = {
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
-
-  onSearchSuccess: PropTypes.func,
-  onSearchError:   PropTypes.func,
-  onChange:        PropTypes.func,
-  onBlur:          PropTypes.func,
-  onFocus:         PropTypes.func,
-  onSubmit:        PropTypes.func,
-  onInputChange:   PropTypes.func,
-  inputName:       PropTypes.string,
-  emptyLabel:      PropTypes.string,
-  emptyLabelExceptError: PropTypes.string,
-  placeholder:     PropTypes.string,
-  keywordOnInit:   PropTypes.string,
-  helpElement:     PropTypes.object,
-  autoFocus:       PropTypes.bool,
-  behaviorOfResetBtn: PropTypes.oneOf(['restore', 'clear']),
-};
-
-/**
- * Properties
- */
-SearchTypeahead.defaultProps = {
-  onSearchSuccess: noop,
-  onSearchError:   noop,
-  onChange:        noop,
-  placeholder:     '',
-  keywordOnInit:   '',
-  behaviorOfResetBtn: 'restore',
-  autoFocus:       false,
-  onInputChange: () => {},
-};
-
-export default SearchTypeaheadWrapper;

+ 246 - 0
packages/app/src/components/SearchTypeahead.tsx

@@ -0,0 +1,246 @@
+import React, {
+  FC, ForwardRefRenderFunction, forwardRef, useImperativeHandle,
+  KeyboardEvent, useCallback, useRef, useState,
+} from 'react';
+// eslint-disable-next-line no-restricted-imports
+import { AxiosResponse } from 'axios';
+
+import { AsyncTypeahead } from 'react-bootstrap-typeahead';
+
+import { UserPicture, PageListMeta, PagePathLabel } from '@growi/ui';
+
+import { IFocusable } from '~/client/interfaces/focusable';
+import { TypeaheadProps } from '~/client/interfaces/react-bootstrap-typeahead';
+import { apiGet } from '~/client/util/apiv1-client';
+import { IPage } from '~/interfaces/page';
+
+
+type ResetFormButtonProps = {
+  keywordOnInit: string,
+  behaviorOfResetBtn: 'restore' | 'clear',
+  input: string,
+  onReset: () => void,
+}
+
+const ResetFormButton: FC<ResetFormButtonProps> = (props: ResetFormButtonProps) => {
+  const isClearBtn = props.behaviorOfResetBtn === 'clear';
+  const initialKeyword = isClearBtn ? '' : props.keywordOnInit;
+  const isHidden = props.input === initialKeyword;
+
+  return isHidden ? (
+    <span />
+  ) : (
+    <button type="button" className="btn btn-link search-clear" onMouseDown={props.onReset}>
+      <i className="icon-close" />
+    </button>
+  );
+};
+
+
+type Props = TypeaheadProps & {
+  onSearchSuccess?: (res: IPage[]) => void,
+  onSearchError?: (err: Error) => void,
+  onSubmit?: (input: string) => void,
+  inputName?: string,
+  keywordOnInit?: string,
+  // eslint-disable-next-line @typescript-eslint/no-explicit-any
+  helpElement?: any,
+  behaviorOfResetBtn?: 'restore' | 'clear',
+};
+
+// see https://github.com/ericgio/react-bootstrap-typeahead/issues/266#issuecomment-414987723
+type TypeaheadInstance = {
+  clear: () => void,
+  focus: () => void,
+  setState: ({ text: string }) => void,
+}
+type TypeaheadInstanceFactory = {
+  getInstance: () => TypeaheadInstance,
+}
+
+const SearchTypeahead: ForwardRefRenderFunction<IFocusable, Props> = (props: Props, ref) => {
+  const {
+    keywordOnInit,
+    onSearchSuccess, onSearchError, onInputChange, onSubmit,
+    emptyLabel, helpElement,
+  } = props;
+
+  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+  const [input, setInput] = useState(props.keywordOnInit!);
+  const [pages, setPages] = useState<IPage[]>();
+  // eslint-disable-next-line @typescript-eslint/no-unused-vars
+  const [searchError, setSearchError] = useState<Error | null>(null);
+  const [isLoading, setLoading] = useState(false);
+
+  const typeaheadRef = useRef<TypeaheadInstanceFactory>(null);
+
+
+  // publish focus()
+  useImperativeHandle(ref, () => ({
+    focus() {
+      const instance = typeaheadRef.current?.getInstance();
+      if (instance != null) {
+        instance.focus();
+      }
+    },
+  }));
+
+
+  const changeKeyword = (text: string | undefined) => {
+    const instance = typeaheadRef.current?.getInstance();
+    if (instance != null) {
+      instance.clear();
+      instance.setState({ text });
+    }
+  };
+
+  const restoreInitialData = () => {
+    changeKeyword(keywordOnInit);
+  };
+
+  const clearKeyword = () => {
+    changeKeyword('');
+  };
+
+  /**
+   * Callback function which is occured when search is exit successfully
+   */
+  const searchSuccessHandler = useCallback((res: AxiosResponse<IPage[]>) => {
+    setPages(res.data);
+
+    if (onSearchSuccess != null) {
+      onSearchSuccess(res.data);
+    }
+  }, [onSearchSuccess]);
+
+  /**
+   * Callback function which is occured when search is exit abnormaly
+   */
+  const searchErrorHandler = useCallback((err: Error) => {
+    setSearchError(err);
+
+    if (onSearchError != null) {
+      onSearchError(err);
+    }
+  }, [onSearchError]);
+
+  const search = useCallback(async(keyword: string) => {
+    if (keyword === '') {
+      return;
+    }
+
+    setLoading(true);
+
+    try {
+      const res = await apiGet('/search', { q: keyword }) as AxiosResponse<IPage[]>;
+      searchSuccessHandler(res);
+    }
+    catch (err) {
+      searchErrorHandler(err);
+    }
+    finally {
+      setLoading(false);
+    }
+
+  }, [searchErrorHandler, searchSuccessHandler]);
+
+  const inputChangeHandler = useCallback((text: string) => {
+    setInput(text);
+
+    if (onInputChange != null) {
+      onInputChange(text);
+    }
+
+    if (text === '') {
+      setPages([]);
+    }
+  }, [onInputChange]);
+
+  const keyDownHandler = useCallback((event: KeyboardEvent) => {
+    if (event.keyCode === 13) { // Enter key
+      if (onSubmit != null) {
+        onSubmit(input);
+      }
+    }
+  }, [input, onSubmit]);
+
+  const getEmptyLabel = () => {
+    // show help element if empty
+    if (input.length === 0) {
+      return helpElement;
+    }
+
+    // use props.emptyLabel as is if defined
+    if (emptyLabel !== undefined) {
+      return emptyLabel;
+    }
+
+    return false;
+  };
+
+  const defaultSelected = (props.keywordOnInit !== '')
+    ? [{ path: props.keywordOnInit }]
+    : [];
+  // eslint-disable-next-line @typescript-eslint/no-explicit-any
+  const inputProps: any = { autoComplete: 'off' };
+  if (props.inputName != null) {
+    inputProps.name = props.inputName;
+  }
+
+  const isClearBtn = props.behaviorOfResetBtn === 'clear';
+  const resetForm = isClearBtn ? clearKeyword : restoreInitialData;
+
+  const renderMenuItemChildren = (page: IPage) => (
+    <span>
+      <UserPicture user={page.lastUpdateUser} size="sm" noLink />
+      <span className="ml-1 text-break text-wrap"><PagePathLabel page={page} /></span>
+      <PageListMeta page={page} />
+    </span>
+  );
+
+  return (
+    <div className="search-typeahead">
+      <AsyncTypeahead
+        {...props}
+        id="search-typeahead-asynctypeahead"
+        ref={typeaheadRef}
+        inputProps={inputProps}
+        isLoading={isLoading}
+        labelKey="path"
+        minLength={0}
+        options={pages} // Search result (Some page names)
+        promptText={props.helpElement}
+        emptyLabel={getEmptyLabel()}
+        align="left"
+        onSearch={search}
+        onInputChange={inputChangeHandler}
+        onKeyDown={keyDownHandler}
+        renderMenuItemChildren={renderMenuItemChildren}
+        caseSensitive={false}
+        defaultSelected={defaultSelected}
+        autoFocus={props.autoFocus}
+        onBlur={props.onBlur}
+        onFocus={props.onFocus}
+      />
+      <ResetFormButton
+        // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+        keywordOnInit={props.keywordOnInit!}
+        // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+        behaviorOfResetBtn={props.behaviorOfResetBtn!}
+        input={input}
+        onReset={resetForm}
+      />
+    </div>
+  );
+};
+
+const ForwardedSearchTypeahead = forwardRef(SearchTypeahead);
+
+ForwardedSearchTypeahead.defaultProps = {
+  placeholder: '',
+  keywordOnInit: '',
+  behaviorOfResetBtn: 'restore',
+  autoFocus: false,
+};
+
+export default ForwardedSearchTypeahead;

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

@@ -17,7 +17,7 @@
   data-page-is-not-creatable="false"
   data-page-is-not-creatable="false"
   data-page-is-able-to-delete-completely="{% if user.canDeleteCompletely(page.creator._id) %}true{% else %}false{% endif %}"
   data-page-is-able-to-delete-completely="{% if user.canDeleteCompletely(page.creator._id) %}true{% else %}false{% endif %}"
   data-slack-channels="{% if page %}{{ page.slackChannels }}{% endif %}"
   data-slack-channels="{% if page %}{{ page.slackChannels }}{% endif %}"
-  data-page-created-at="{% if page %}{{ page.createdAt|datetz('Y/m/d H:i:s') }}{% endif %}"
+  data-page-created-at="{{ page.createdAt|datetz('Y/m/d H:i:s') }}"
   data-page-creator="{% if page && page.creator %}{{ page.creator|json }}{% endif %}"
   data-page-creator="{% if page && page.creator %}{{ page.creator|json }}{% endif %}"
   data-page-last-update-username="{% if page && page.lastUpdateUser %}{{ page.lastUpdateUser.name }}{% endif %}"
   data-page-last-update-username="{% if page && page.lastUpdateUser %}{{ page.lastUpdateUser.name }}{% endif %}"
   data-page-updated-at="{{ page.updatedAt|datetz('Y/m/d H:i:s') }}"
   data-page-updated-at="{{ page.updatedAt|datetz('Y/m/d H:i:s') }}"

+ 2 - 2
packages/app/src/stores/context.tsx

@@ -30,8 +30,8 @@ export const useRevisionCreatedAt = (initialData?: Nullable<any>): SWRResponse<N
   return useStaticSWR<Nullable<any>, Error>('revisionCreatedAt', initialData ?? null);
   return useStaticSWR<Nullable<any>, Error>('revisionCreatedAt', initialData ?? null);
 };
 };
 
 
-export const useCreatedAt = (initialData?: Nullable<any>): SWRResponse<Nullable<any>, Error> => {
-  return useStaticSWR<Nullable<any>, Error>('createdAt', initialData ?? null);
+export const useCurrentCreatedAt = (initialData?: Nullable<Date>): SWRResponse<Nullable<Date>, Error> => {
+  return useStaticSWR<Nullable<Date>, Error>('createdAt', initialData ?? null);
 };
 };
 
 
 export const useCurrentUpdatedAt = (initialData?: Nullable<any>): SWRResponse<Nullable<any>, Error> => {
 export const useCurrentUpdatedAt = (initialData?: Nullable<any>): SWRResponse<Nullable<any>, Error> => {

+ 2 - 1
packages/app/src/styles/_sidebar.scss

@@ -22,7 +22,8 @@
   position: sticky;
   position: sticky;
   top: $grw-navbar-border-width;
   top: $grw-navbar-border-width;
 
 
-  height: 100vh;
+  // set the max value that should be taken when sticky
+  height: calc(100vh - $grw-navbar-border-width);
 
 
   .grw-navigation-resize-button {
   .grw-navigation-resize-button {
     position: fixed;
     position: fixed;

+ 102 - 66
yarn.lock

@@ -3406,11 +3406,6 @@ acorn-walk@^7.1.1:
   resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-7.2.0.tgz#0de889a601203909b0fbe07b8938dc21d2e967bc"
   resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-7.2.0.tgz#0de889a601203909b0fbe07b8938dc21d2e967bc"
   integrity sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==
   integrity sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==
 
 
-acorn@^5.7.3:
-  version "5.7.3"
-  resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.7.3.tgz#67aa231bf8812974b85235a96771eb6bd07ea279"
-  integrity sha512-T/zvzYRfbVojPWahDsE5evJdHb3oJoQfFbsrKM7w5Zcs++Tr257tia3BmMP8XYVjp1S9RZXQMh7gao96BlqZOw==
-
 acorn@^6.4.1:
 acorn@^6.4.1:
   version "6.4.2"
   version "6.4.2"
   resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.4.2.tgz#35866fd710528e92de10cf06016498e47e39e1e6"
   resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.4.2.tgz#35866fd710528e92de10cf06016498e47e39e1e6"
@@ -3926,7 +3921,7 @@ asn1.js@^4.0.0:
     inherits "^2.0.1"
     inherits "^2.0.1"
     minimalistic-assert "^1.0.0"
     minimalistic-assert "^1.0.0"
 
 
-asn1.js@^5.4.1:
+asn1.js@^5.2.0, asn1.js@^5.4.1:
   version "5.4.1"
   version "5.4.1"
   resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-5.4.1.tgz#11a980b84ebb91781ce35b0fdc2ee294e3783f07"
   resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-5.4.1.tgz#11a980b84ebb91781ce35b0fdc2ee294e3783f07"
   integrity sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==
   integrity sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==
@@ -4090,20 +4085,13 @@ aws4@^1.8.0:
   resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.10.0.tgz#a17b3a8ea811060e74d47d306122400ad4497ae2"
   resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.10.0.tgz#a17b3a8ea811060e74d47d306122400ad4497ae2"
   integrity sha512-3YDiu347mtVtjpyV3u5kVqQLP242c06zwDOgpeRnybmXlYYsLbtTrUBUm8i8srONt+FWobl5aibnU1030PeeuA==
   integrity sha512-3YDiu347mtVtjpyV3u5kVqQLP242c06zwDOgpeRnybmXlYYsLbtTrUBUm8i8srONt+FWobl5aibnU1030PeeuA==
 
 
-axios@0.21.4:
+axios@0.21.4, axios@^0.21.1:
   version "0.21.4"
   version "0.21.4"
   resolved "https://registry.yarnpkg.com/axios/-/axios-0.21.4.tgz#c67b90dc0568e5c1cf2b0b858c43ba28e2eda575"
   resolved "https://registry.yarnpkg.com/axios/-/axios-0.21.4.tgz#c67b90dc0568e5c1cf2b0b858c43ba28e2eda575"
   integrity sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==
   integrity sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==
   dependencies:
   dependencies:
     follow-redirects "^1.14.0"
     follow-redirects "^1.14.0"
 
 
-axios@^0.21.1:
-  version "0.21.1"
-  resolved "https://registry.yarnpkg.com/axios/-/axios-0.21.1.tgz#22563481962f4d6bde9a76d516ef0e5d3c09b2b8"
-  integrity sha512-dKQiRHxGD9PPRIUNIWvZhPTPpl1rf/OxTYKsqKUDjBwYylTvV7SjSHJb9ratfyzM6wCdLCOYLzs73qpg5c4iGA==
-  dependencies:
-    follow-redirects "^1.10.0"
-
 axios@^0.24.0:
 axios@^0.24.0:
   version "0.24.0"
   version "0.24.0"
   resolved "https://registry.yarnpkg.com/axios/-/axios-0.24.0.tgz#804e6fa1e4b9c5288501dd9dff56a7a0940d20d6"
   resolved "https://registry.yarnpkg.com/axios/-/axios-0.24.0.tgz#804e6fa1e4b9c5288501dd9dff56a7a0940d20d6"
@@ -4406,10 +4394,20 @@ bluebird@~3.4.1:
   resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.4.7.tgz#f72d760be09b7f76d08ed8fae98b289a8d05fab3"
   resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.4.7.tgz#f72d760be09b7f76d08ed8fae98b289a8d05fab3"
   integrity sha1-9y12C+Cbf3bQjtj66Ysomo0F+rM=
   integrity sha1-9y12C+Cbf3bQjtj66Ysomo0F+rM=
 
 
-bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.1.1, bn.js@^4.4.0:
+bn.js@^4.0.0, bn.js@^4.1.0:
   version "4.11.8"
   version "4.11.8"
   resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.8.tgz#2cde09eb5ee341f484746bb0309b3253b1b1442f"
   resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.8.tgz#2cde09eb5ee341f484746bb0309b3253b1b1442f"
 
 
+bn.js@^4.11.9:
+  version "4.12.0"
+  resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.12.0.tgz#775b3f278efbb9718eec7361f483fb36fbbfea88"
+  integrity sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==
+
+bn.js@^5.0.0, bn.js@^5.1.1:
+  version "5.2.0"
+  resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-5.2.0.tgz#358860674396c6997771a9d051fcc1b57d4ae002"
+  integrity sha512-D7iWRBvnZE8ecXiLj/9wbxH7Tk79fAh8IHaTNq1RWRixsS02W+5qS+iE9yq6RYl0asXx5tw0bLhmT5pIfbSquw==
+
 body-parser@1.18.2, body-parser@^1.18.2:
 body-parser@1.18.2, body-parser@^1.18.2:
   version "1.18.2"
   version "1.18.2"
   resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.18.2.tgz#87678a19d84b47d859b83199bd59bce222b10454"
   resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.18.2.tgz#87678a19d84b47d859b83199bd59bce222b10454"
@@ -4516,7 +4514,7 @@ braces@^3.0.1, braces@~3.0.2:
   dependencies:
   dependencies:
     fill-range "^7.0.1"
     fill-range "^7.0.1"
 
 
-brorand@^1.0.1:
+brorand@^1.0.1, brorand@^1.1.0:
   version "1.1.0"
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f"
   resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f"
 
 
@@ -4632,17 +4630,28 @@ browserify-rsa@^4.0.0:
     bn.js "^4.1.0"
     bn.js "^4.1.0"
     randombytes "^2.0.1"
     randombytes "^2.0.1"
 
 
-browserify-sign@^4.0.0:
-  version "4.0.4"
-  resolved "https://registry.yarnpkg.com/browserify-sign/-/browserify-sign-4.0.4.tgz#aa4eb68e5d7b658baa6bf6a57e630cbd7a93d298"
+browserify-rsa@^4.0.1:
+  version "4.1.0"
+  resolved "https://registry.yarnpkg.com/browserify-rsa/-/browserify-rsa-4.1.0.tgz#b2fd06b5b75ae297f7ce2dc651f918f5be158c8d"
+  integrity sha512-AdEER0Hkspgno2aR97SAf6vi0y0k8NuOpGnVH3O99rcA5Q6sh8QxcngtHuJ6uXwnfAXNM4Gn1Gb7/MV1+Ymbog==
   dependencies:
   dependencies:
-    bn.js "^4.1.1"
-    browserify-rsa "^4.0.0"
-    create-hash "^1.1.0"
-    create-hmac "^1.1.2"
-    elliptic "^6.0.0"
-    inherits "^2.0.1"
-    parse-asn1 "^5.0.0"
+    bn.js "^5.0.0"
+    randombytes "^2.0.1"
+
+browserify-sign@^4.0.0:
+  version "4.2.1"
+  resolved "https://registry.yarnpkg.com/browserify-sign/-/browserify-sign-4.2.1.tgz#eaf4add46dd54be3bb3b36c0cf15abbeba7956c3"
+  integrity sha512-/vrA5fguVAKKAVTNJjgSm1tRQDHUU6DbwO9IROu/0WAzC8PKhucDSh18J0RMvVeHAn5puMd+QHC2erPRNf8lmg==
+  dependencies:
+    bn.js "^5.1.1"
+    browserify-rsa "^4.0.1"
+    create-hash "^1.2.0"
+    create-hmac "^1.1.7"
+    elliptic "^6.5.3"
+    inherits "^2.0.4"
+    parse-asn1 "^5.1.5"
+    readable-stream "^3.6.0"
+    safe-buffer "^5.2.0"
 
 
 browserify-zlib@^0.2.0:
 browserify-zlib@^0.2.0:
   version "0.2.0"
   version "0.2.0"
@@ -6211,11 +6220,12 @@ crc32-stream@^4.0.2:
     readable-stream "^3.4.0"
     readable-stream "^3.4.0"
 
 
 create-ecdh@^4.0.0:
 create-ecdh@^4.0.0:
-  version "4.0.0"
-  resolved "https://registry.yarnpkg.com/create-ecdh/-/create-ecdh-4.0.0.tgz#888c723596cdf7612f6498233eebd7a35301737d"
+  version "4.0.4"
+  resolved "https://registry.yarnpkg.com/create-ecdh/-/create-ecdh-4.0.4.tgz#d6e7f4bffa66736085a0762fd3a632684dabcc4e"
+  integrity sha512-mf+TCx8wWc9VpuxfP2ht0iSISLZnt0JgWlrOKZiNqyUZWnjIaCIVNQArMHnCZKfEYRg6IM7A+NeJoN8gf/Ws0A==
   dependencies:
   dependencies:
     bn.js "^4.1.0"
     bn.js "^4.1.0"
-    elliptic "^6.0.0"
+    elliptic "^6.5.3"
 
 
 create-error-class@^3.0.0:
 create-error-class@^3.0.0:
   version "3.0.2"
   version "3.0.2"
@@ -6233,7 +6243,18 @@ create-hash@^1.1.0, create-hash@^1.1.2:
     ripemd160 "^2.0.0"
     ripemd160 "^2.0.0"
     sha.js "^2.4.0"
     sha.js "^2.4.0"
 
 
-create-hmac@^1.1.0, create-hmac@^1.1.2, create-hmac@^1.1.4:
+create-hash@^1.2.0:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/create-hash/-/create-hash-1.2.0.tgz#889078af11a63756bcfb59bd221996be3a9ef196"
+  integrity sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==
+  dependencies:
+    cipher-base "^1.0.1"
+    inherits "^2.0.1"
+    md5.js "^1.3.4"
+    ripemd160 "^2.0.1"
+    sha.js "^2.4.0"
+
+create-hmac@^1.1.0, create-hmac@^1.1.4:
   version "1.1.6"
   version "1.1.6"
   resolved "https://registry.yarnpkg.com/create-hmac/-/create-hmac-1.1.6.tgz#acb9e221a4e17bdb076e90657c42b93e3726cf06"
   resolved "https://registry.yarnpkg.com/create-hmac/-/create-hmac-1.1.6.tgz#acb9e221a4e17bdb076e90657c42b93e3726cf06"
   dependencies:
   dependencies:
@@ -6244,6 +6265,18 @@ create-hmac@^1.1.0, create-hmac@^1.1.2, create-hmac@^1.1.4:
     safe-buffer "^5.0.1"
     safe-buffer "^5.0.1"
     sha.js "^2.4.8"
     sha.js "^2.4.8"
 
 
+create-hmac@^1.1.7:
+  version "1.1.7"
+  resolved "https://registry.yarnpkg.com/create-hmac/-/create-hmac-1.1.7.tgz#69170c78b3ab957147b2b8b04572e47ead2243ff"
+  integrity sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==
+  dependencies:
+    cipher-base "^1.0.3"
+    create-hash "^1.1.0"
+    inherits "^2.0.1"
+    ripemd160 "^2.0.0"
+    safe-buffer "^5.0.1"
+    sha.js "^2.4.8"
+
 create-react-class@^15.5.2:
 create-react-class@^15.5.2:
   version "15.7.0"
   version "15.7.0"
   resolved "https://registry.yarnpkg.com/create-react-class/-/create-react-class-15.7.0.tgz#7499d7ca2e69bb51d13faf59bd04f0c65a1d6c1e"
   resolved "https://registry.yarnpkg.com/create-react-class/-/create-react-class-15.7.0.tgz#7499d7ca2e69bb51d13faf59bd04f0c65a1d6c1e"
@@ -7149,19 +7182,13 @@ easy-extender@^2.3.4:
   dependencies:
   dependencies:
     lodash "^4.17.10"
     lodash "^4.17.10"
 
 
-eazy-logger@3.1.0:
+eazy-logger@3.1.0, eazy-logger@^3.1.0:
   version "3.1.0"
   version "3.1.0"
   resolved "https://registry.yarnpkg.com/eazy-logger/-/eazy-logger-3.1.0.tgz#b169eb56df714608fa114f164c8a2956bec9f0f3"
   resolved "https://registry.yarnpkg.com/eazy-logger/-/eazy-logger-3.1.0.tgz#b169eb56df714608fa114f164c8a2956bec9f0f3"
   integrity sha512-/snsn2JqBtUSSstEl4R0RKjkisGHAhvYj89i7r3ytNUKW12y178KDZwXLXIgwDqLW6E/VRMT9qfld7wvFae8bQ==
   integrity sha512-/snsn2JqBtUSSstEl4R0RKjkisGHAhvYj89i7r3ytNUKW12y178KDZwXLXIgwDqLW6E/VRMT9qfld7wvFae8bQ==
   dependencies:
   dependencies:
     tfunk "^4.0.0"
     tfunk "^4.0.0"
 
 
-eazy-logger@^3.0.2:
-  version "3.0.2"
-  resolved "https://registry.yarnpkg.com/eazy-logger/-/eazy-logger-3.0.2.tgz#a325aa5e53d13a2225889b2ac4113b2b9636f4fc"
-  dependencies:
-    tfunk "^3.0.1"
-
 ecc-jsbn@~0.1.1:
 ecc-jsbn@~0.1.1:
   version "0.1.2"
   version "0.1.2"
   resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9"
   resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9"
@@ -7211,17 +7238,18 @@ electron-to-chromium@^1.3.723:
   resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.792.tgz#791b0d8fcf7411885d086193fb49aaef0c1594ca"
   resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.792.tgz#791b0d8fcf7411885d086193fb49aaef0c1594ca"
   integrity sha512-RM2O2xrNarM7Cs+XF/OE2qX/aBROyOZqqgP+8FXMXSuWuUqCfUUzg7NytQrzZU3aSqk1Qq6zqnVkJsbfMkIatg==
   integrity sha512-RM2O2xrNarM7Cs+XF/OE2qX/aBROyOZqqgP+8FXMXSuWuUqCfUUzg7NytQrzZU3aSqk1Qq6zqnVkJsbfMkIatg==
 
 
-elliptic@^6.0.0:
-  version "6.4.0"
-  resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.4.0.tgz#cac9af8762c85836187003c8dfe193e5e2eae5df"
+elliptic@^6.5.3:
+  version "6.5.4"
+  resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.4.tgz#da37cebd31e79a1367e941b592ed1fbebd58abbb"
+  integrity sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ==
   dependencies:
   dependencies:
-    bn.js "^4.4.0"
-    brorand "^1.0.1"
+    bn.js "^4.11.9"
+    brorand "^1.1.0"
     hash.js "^1.0.0"
     hash.js "^1.0.0"
-    hmac-drbg "^1.0.0"
-    inherits "^2.0.1"
-    minimalistic-assert "^1.0.0"
-    minimalistic-crypto-utils "^1.0.0"
+    hmac-drbg "^1.0.1"
+    inherits "^2.0.4"
+    minimalistic-assert "^1.0.1"
+    minimalistic-crypto-utils "^1.0.1"
 
 
 emittery@^0.8.1:
 emittery@^0.8.1:
   version "0.8.1"
   version "0.8.1"
@@ -8748,7 +8776,7 @@ follow-redirects@^1.0.0, follow-redirects@^1.14.0:
   resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.6.tgz#8cfb281bbc035b3c067d6cd975b0f6ade6e855cd"
   resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.6.tgz#8cfb281bbc035b3c067d6cd975b0f6ade6e855cd"
   integrity sha512-fhUl5EwSJbbl8AR+uYL2KQDxLkdSjZGR36xy46AO7cOMTrCMON6Sa28FmAnC2tRTDbd/Uuzz3aJBv7EBN7JH8A==
   integrity sha512-fhUl5EwSJbbl8AR+uYL2KQDxLkdSjZGR36xy46AO7cOMTrCMON6Sa28FmAnC2tRTDbd/Uuzz3aJBv7EBN7JH8A==
 
 
-follow-redirects@^1.10.0, follow-redirects@^1.14.4:
+follow-redirects@^1.14.4:
   version "1.14.5"
   version "1.14.5"
   resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.5.tgz#f09a5848981d3c772b5392309778523f8d85c381"
   resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.5.tgz#f09a5848981d3c772b5392309778523f8d85c381"
   integrity sha512-wtphSXy7d4/OR+MvIFbCVBDzZ5520qV8XfPklSN5QtxuMUJZ+b0Wnst1e1lCDocfzuCkHqj8k0FpZqO+UIaKNA==
   integrity sha512-wtphSXy7d4/OR+MvIFbCVBDzZ5520qV8XfPklSN5QtxuMUJZ+b0Wnst1e1lCDocfzuCkHqj8k0FpZqO+UIaKNA==
@@ -9777,9 +9805,10 @@ highlight.js@^10.7.1:
   resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-10.7.2.tgz#89319b861edc66c48854ed1e6da21ea89f847360"
   resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-10.7.2.tgz#89319b861edc66c48854ed1e6da21ea89f847360"
   integrity sha512-oFLl873u4usRM9K63j4ME9u3etNF0PLiJhSQ8rdfuL51Wn3zkD6drf9ZW0dOzjnZI22YYG24z30JcmfCZjMgYg==
   integrity sha512-oFLl873u4usRM9K63j4ME9u3etNF0PLiJhSQ8rdfuL51Wn3zkD6drf9ZW0dOzjnZI22YYG24z30JcmfCZjMgYg==
 
 
-hmac-drbg@^1.0.0:
+hmac-drbg@^1.0.1:
   version "1.0.1"
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1"
   resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1"
+  integrity sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=
   dependencies:
   dependencies:
     hash.js "^1.0.3"
     hash.js "^1.0.3"
     minimalistic-assert "^1.0.0"
     minimalistic-assert "^1.0.0"
@@ -13235,7 +13264,12 @@ minimalistic-assert@^1.0.0:
   version "1.0.0"
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.0.tgz#702be2dda6b37f4836bcb3f5db56641b64a1d3d3"
   resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.0.tgz#702be2dda6b37f4836bcb3f5db56641b64a1d3d3"
 
 
-minimalistic-crypto-utils@^1.0.0, minimalistic-crypto-utils@^1.0.1:
+minimalistic-assert@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7"
+  integrity sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==
+
+minimalistic-crypto-utils@^1.0.1:
   version "1.0.1"
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a"
   resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a"
 
 
@@ -14418,10 +14452,6 @@ object-keys@~0.4.0:
   version "0.4.0"
   version "0.4.0"
   resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-0.4.0.tgz#28a6aae7428dd2c3a92f3d95f21335dd204e0336"
   resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-0.4.0.tgz#28a6aae7428dd2c3a92f3d95f21335dd204e0336"
 
 
-object-path@^0.9.0:
-  version "0.9.2"
-  resolved "https://registry.yarnpkg.com/object-path/-/object-path-0.9.2.tgz#0fd9a74fc5fad1ae3968b586bda5c632bd6c05a5"
-
 object-visit@^1.0.0:
 object-visit@^1.0.0:
   version "1.0.1"
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/object-visit/-/object-visit-1.0.1.tgz#f79c4493af0c5377b59fe39d395e41042dd045bb"
   resolved "https://registry.yarnpkg.com/object-visit/-/object-visit-1.0.1.tgz#f79c4493af0c5377b59fe39d395e41042dd045bb"
@@ -14956,6 +14986,17 @@ parse-asn1@^5.0.0:
     evp_bytestokey "^1.0.0"
     evp_bytestokey "^1.0.0"
     pbkdf2 "^3.0.3"
     pbkdf2 "^3.0.3"
 
 
+parse-asn1@^5.1.5:
+  version "5.1.6"
+  resolved "https://registry.yarnpkg.com/parse-asn1/-/parse-asn1-5.1.6.tgz#385080a3ec13cb62a62d39409cb3e88844cdaed4"
+  integrity sha512-RnZRo1EPU6JBnra2vGHj0yhp6ebyjBZpmUCLHWiFhxlzvBCCpAuZ7elsBp1PVAbQN0/04VD/19rfzlBSwLstMw==
+  dependencies:
+    asn1.js "^5.2.0"
+    browserify-aes "^1.0.0"
+    evp_bytestokey "^1.0.0"
+    pbkdf2 "^3.0.3"
+    safe-buffer "^5.1.1"
+
 parse-entities@^1.0.2, parse-entities@^1.1.0:
 parse-entities@^1.0.2, parse-entities@^1.1.0:
   version "1.2.2"
   version "1.2.2"
   resolved "https://registry.yarnpkg.com/parse-entities/-/parse-entities-1.2.2.tgz#c31bf0f653b6661354f8973559cb86dd1d5edf50"
   resolved "https://registry.yarnpkg.com/parse-entities/-/parse-entities-1.2.2.tgz#c31bf0f653b6661354f8973559cb86dd1d5edf50"
@@ -17472,7 +17513,7 @@ safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1:
   resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
   resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
   integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==
   integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==
 
 
-safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@^5.2.1:
+safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@^5.2.0, safe-buffer@^5.2.1:
   version "5.2.1"
   version "5.2.1"
   resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6"
   resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6"
   integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==
   integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==
@@ -19797,13 +19838,6 @@ textlint@^12.0.2:
     try-resolve "^1.0.1"
     try-resolve "^1.0.1"
     unique-concat "^0.2.2"
     unique-concat "^0.2.2"
 
 
-tfunk@^3.0.1:
-  version "3.1.0"
-  resolved "https://registry.yarnpkg.com/tfunk/-/tfunk-3.1.0.tgz#38e4414fc64977d87afdaa72facb6d29f82f7b5b"
-  dependencies:
-    chalk "^1.1.1"
-    object-path "^0.9.0"
-
 tfunk@^4.0.0:
 tfunk@^4.0.0:
   version "4.0.0"
   version "4.0.0"
   resolved "https://registry.yarnpkg.com/tfunk/-/tfunk-4.0.0.tgz#de9399feaf2060901d590b7faad80fcd5443077e"
   resolved "https://registry.yarnpkg.com/tfunk/-/tfunk-4.0.0.tgz#de9399feaf2060901d590b7faad80fcd5443077e"
@@ -21043,11 +21077,13 @@ webpack-assets-manifest@^3.1.1:
     tapable "^1.0.0"
     tapable "^1.0.0"
     webpack-sources "^1.0.0"
     webpack-sources "^1.0.0"
 
 
-webpack-bundle-analyzer@^3.0.2:
-  version "3.0.2"
-  resolved "https://registry.yarnpkg.com/webpack-bundle-analyzer/-/webpack-bundle-analyzer-3.0.2.tgz#22f19ea6d1b5a15fd7a90baae0bc0f39bd1e4d48"
+webpack-bundle-analyzer@^3.9.0:
+  version "3.9.0"
+  resolved "https://registry.yarnpkg.com/webpack-bundle-analyzer/-/webpack-bundle-analyzer-3.9.0.tgz#f6f94db108fb574e415ad313de41a2707d33ef3c"
+  integrity sha512-Ob8amZfCm3rMB1ScjQVlbYYUEJyEjdEtQ92jqiFUYt5VkEeO2v5UMbv49P/gnmCZm3A6yaFQzCBvpZqN4MUsdA==
   dependencies:
   dependencies:
-    acorn "^5.7.3"
+    acorn "^7.1.1"
+    acorn-walk "^7.1.1"
     bfj "^6.1.1"
     bfj "^6.1.1"
     chalk "^2.4.1"
     chalk "^2.4.1"
     commander "^2.18.0"
     commander "^2.18.0"
@@ -21055,7 +21091,7 @@ webpack-bundle-analyzer@^3.0.2:
     express "^4.16.3"
     express "^4.16.3"
     filesize "^3.6.1"
     filesize "^3.6.1"
     gzip-size "^5.0.0"
     gzip-size "^5.0.0"
-    lodash "^4.17.10"
+    lodash "^4.17.19"
     mkdirp "^0.5.1"
     mkdirp "^0.5.1"
     opener "^1.5.1"
     opener "^1.5.1"
     ws "^6.0.0"
     ws "^6.0.0"