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

Merge branch 'master' into fix/hackmd-load-styles

Haku Mizuki 3 лет назад
Родитель
Сommit
de69261b9f
43 измененных файлов с 284 добавлено и 193 удалено
  1. 1 1
      package.json
  2. 29 0
      packages/app/cypress.config.ts
  3. 0 18
      packages/app/cypress.json
  4. 1 0
      packages/app/package.json
  5. 6 52
      packages/app/src/client/util/apiNotification.js
  6. 91 0
      packages/app/src/client/util/toastr.ts
  7. 1 1
      packages/app/src/components/Admin/Common/AdminNavigation.jsx
  8. 4 1
      packages/app/src/components/Admin/ElasticsearchManagement/ElasticsearchManagement.tsx
  9. 1 1
      packages/app/src/components/Admin/PluginsExtension/PluginInstallerForm.tsx
  10. 1 1
      packages/app/src/components/Common/Dropdown/PageItemControl.tsx
  11. 2 2
      packages/app/src/components/InAppNotification/InAppNotificationList.tsx
  12. 3 6
      packages/app/src/components/InAppNotification/PageNotification/PageModelNotification.tsx
  13. 2 0
      packages/app/src/components/Layout/RawLayout.tsx
  14. 7 4
      packages/app/src/components/Navbar/GrowiContextualSubNavigation.tsx
  15. 1 1
      packages/app/src/components/Page/CopyDropdown.jsx
  16. 9 11
      packages/app/src/components/Page/RenderTagLabels.tsx
  17. 1 1
      packages/app/src/components/Page/TagLabels.tsx
  18. 13 11
      packages/app/src/components/PageAlert/TrashPageAlert.tsx
  19. 4 0
      packages/app/src/components/PageCreateModal.jsx
  20. 2 1
      packages/app/src/components/PageEditor.tsx
  21. 3 0
      packages/app/src/interfaces/in-app-notification.ts
  22. 2 2
      packages/app/src/models/serializers/in-app-notification-snapshot/page.ts
  23. 1 1
      packages/app/src/pages/admin/plugins.page.tsx
  24. 1 1
      packages/app/src/server/routes/apiv3/index.js
  25. 0 0
      packages/app/src/server/routes/apiv3/plugins-extension.ts
  26. 12 12
      packages/app/src/server/routes/login.js
  27. 21 2
      packages/app/src/stores/in-app-notification.ts
  28. 9 3
      packages/app/src/styles/_editor.scss
  29. 1 0
      packages/app/src/styles/molecules/toastr.scss
  30. 1 1
      packages/app/src/utils/array-utils.ts
  31. 0 0
      packages/app/test/cypress/integration/10-install/10-install--install.spec.ts
  32. 0 0
      packages/app/test/cypress/integration/20-basic-features/20-basic-features--access-to-page.spec.ts
  33. 0 0
      packages/app/test/cypress/integration/20-basic-features/20-basic-features--access-to-pagelist.spec.ts
  34. 0 0
      packages/app/test/cypress/integration/20-basic-features/20-basic-features--click-page-icons.spec.ts
  35. 0 0
      packages/app/test/cypress/integration/20-basic-features/20-basic-features--use-tools.spec.ts
  36. 0 0
      packages/app/test/cypress/integration/21-basic-features-for-guest/21-basic-features-for-guest--access-to-page.spec.ts
  37. 7 6
      packages/app/test/cypress/integration/30-search/30-search--search.spec.ts
  38. 0 0
      packages/app/test/cypress/integration/40-admin/40-admin--access-to-admin-page.spec.ts
  39. 0 0
      packages/app/test/cypress/integration/50-sidebar/50-sidebar--access-to-side-bar.spec.ts
  40. 0 0
      packages/app/test/cypress/integration/50-sidebar/50-sidebar--switching-sidebar-mode.spec.ts
  41. 0 0
      packages/app/test/cypress/integration/60-home/60-home--home.spec.ts
  42. 0 33
      packages/app/test/cypress/plugins/index.ts
  43. 47 20
      yarn.lock

+ 1 - 1
package.json

@@ -61,7 +61,7 @@
     "@types/rewire": "^2.5.28",
     "@types/rewire": "^2.5.28",
     "@typescript-eslint/eslint-plugin": "^5.0.0",
     "@typescript-eslint/eslint-plugin": "^5.0.0",
     "@typescript-eslint/parser": "^5.0.0",
     "@typescript-eslint/parser": "^5.0.0",
-    "cypress": "^9.2.0",
+    "cypress": "^12.0.1",
     "eslint": "^8.18.0",
     "eslint": "^8.18.0",
     "eslint-config-next": "^12.1.6",
     "eslint-config-next": "^12.1.6",
     "eslint-config-weseek": "^2.1.0",
     "eslint-config-weseek": "^2.1.0",

+ 29 - 0
packages/app/cypress.config.ts

@@ -0,0 +1,29 @@
+import { defineConfig } from 'cypress';
+
+export default defineConfig({
+  e2e: {
+    baseUrl: 'http://localhost:3000',
+    specPattern: 'test/cypress/integration/',
+    supportFile: 'test/cypress/support/index.ts',
+    setupNodeEvents: (on) => {
+      // change screen size
+      // see: https://docs.cypress.io/api/plugins/browser-launch-api#Set-screen-size-when-running-headless
+      on('before:browser:launch', (browser, launchOptions) => {
+        if (browser.name === 'chrome' && browser.isHeadless) {
+          launchOptions.args.push('--window-size=1400,1024');
+          launchOptions.args.push('--force-device-scale-factor=1');
+        }
+        return launchOptions;
+      });
+    },
+  },
+  fileServerFolder: 'test/cypress',
+  fixturesFolder: 'test/cypress/fixtures',
+  screenshotsFolder: 'test/cypress/screenshots',
+  videosFolder: 'test/cypress/videos',
+
+  viewportWidth: 1400,
+  viewportHeight: 1024,
+
+  defaultCommandTimeout: 30000,
+});

+ 0 - 18
packages/app/cypress.json

@@ -1,18 +0,0 @@
-{
-  "baseUrl": "http://localhost:3000",
-
-  "fileServerFolder": "test/cypress",
-  "fixturesFolder": "test/cypress/fixtures",
-  "integrationFolder": "test/cypress/integration",
-  "screenshotsFolder": "test/cypress/screenshots",
-  "videosFolder": "test/cypress/videos",
-  "supportFile": "test/cypress/support/index.ts",
-  "pluginsFile": "test/cypress/plugins/index.ts",
-  "testFiles": "**/*.spec.ts",
-
-  "viewportWidth": 1400,
-  "viewportHeight": 1024,
-
-  "experimentalSessionSupport": true,
-  "defaultCommandTimeout": 30000
-}

+ 1 - 0
packages/app/package.json

@@ -165,6 +165,7 @@
     "react-multiline-clamp": "^2.0.0",
     "react-multiline-clamp": "^2.0.0",
     "react-scroll": "^1.8.7",
     "react-scroll": "^1.8.7",
     "react-syntax-highlighter": "^15.5.0",
     "react-syntax-highlighter": "^15.5.0",
+    "react-toastify": "^9.1.1",
     "react-use-ripple": "^1.5.2",
     "react-use-ripple": "^1.5.2",
     "reactstrap": "^8.9.0",
     "reactstrap": "^8.9.0",
     "reconnecting-websocket": "^4.4.0",
     "reconnecting-websocket": "^4.4.0",

+ 6 - 52
packages/app/src/client/util/apiNotification.js

@@ -1,53 +1,7 @@
-// show API error/sucess toastr
+import { legacy } from './toastr';
 
 
-import * as toastr from 'toastr';
-import { toArrayIfNot } from '~/utils/array-utils';
-
-const toastrOption = {
-  error: {
-    closeButton: true,
-    progressBar: true,
-    newestOnTop: false,
-    showDuration: '100',
-    hideDuration: '100',
-    timeOut: '0',
-  },
-  success: {
-    closeButton: true,
-    progressBar: true,
-    newestOnTop: false,
-    showDuration: '100',
-    hideDuration: '100',
-    timeOut: '3000',
-  },
-  warning: {
-    closeButton: true,
-    progressBar: true,
-    newestOnTop: false,
-    showDuration: '100',
-    hideDuration: '100',
-    timeOut: '6000',
-  },
-};
-
-// accepts both a single error and an array of errors
-export const toastError = (err, header = 'Error', option = toastrOption.error) => {
-  const errs = toArrayIfNot(err);
-
-  if (err.length === 0) {
-    toastr.error('', header);
-  }
-
-  for (const err of errs) {
-    toastr.error(err.message || err, header, option);
-  }
-};
-
-// only accepts a single item
-export const toastSuccess = (body, header = 'Success', option = toastrOption.success) => {
-  toastr.success(body, header, option);
-};
-
-export const toastWarning = (body, header = 'Warning', option = toastrOption.warning) => {
-  toastr.warning(body, header, option);
-};
+// DEPRECATED -- 2022.12.07 Yuki Takei
+// Use methods from './toastr.ts' instead
+export const toastError = legacy.toastError;
+export const toastSuccess = legacy.toastSuccess;
+export const toastWarning = legacy.toastWarning;

+ 91 - 0
packages/app/src/client/util/toastr.ts

@@ -0,0 +1,91 @@
+import { toast, ToastContent, ToastOptions } from 'react-toastify';
+import * as toastrLegacy from 'toastr';
+
+import { toArrayIfNot } from '~/utils/array-utils';
+
+
+export const toastErrorOption: ToastOptions = {
+  autoClose: 0,
+  closeButton: true,
+};
+export const toastError = (err: string | Error | Error[], option: ToastOptions = toastErrorOption): void => {
+  const errs = toArrayIfNot(err);
+
+  if (errs.length === 0) {
+    return;
+  }
+
+  for (const err of errs) {
+    const message = (typeof err === 'string') ? err : err.message;
+    toast.error(message || err, option);
+  }
+};
+
+export const toastSuccessOption: ToastOptions = {
+  autoClose: 2000,
+  closeButton: true,
+};
+export const toastSuccess = (content: ToastContent, option: ToastOptions = toastSuccessOption): void => {
+  toast.success(content, option);
+};
+
+export const toastWarningOption: ToastOptions = {
+  autoClose: 5000,
+  closeButton: true,
+};
+export const toastWarning = (content: ToastContent, option: ToastOptions = toastWarningOption): void => {
+  toastrLegacy.warning(content, option);
+};
+
+
+const toastrLegacyOption = {
+  error: {
+    closeButton: true,
+    progressBar: true,
+    newestOnTop: false,
+    showDuration: '100',
+    hideDuration: '100',
+    timeOut: '0',
+  },
+  success: {
+    closeButton: true,
+    progressBar: true,
+    newestOnTop: false,
+    showDuration: '100',
+    hideDuration: '100',
+    timeOut: '3000',
+  },
+  warning: {
+    closeButton: true,
+    progressBar: true,
+    newestOnTop: false,
+    showDuration: '100',
+    hideDuration: '100',
+    timeOut: '6000',
+  },
+};
+
+export const legacy = {
+  // accepts both a single error and an array of errors
+  toastError: (err: string | Error | Error[], header = 'Error', option = toastrLegacyOption.error): void => {
+    const errs = toArrayIfNot(err);
+
+    if (errs.length === 0) {
+      toastrLegacy.error('', header);
+    }
+
+    for (const err of errs) {
+      const message = (typeof err === 'string') ? err : err.message;
+      toastrLegacy.error(message || err, header, option);
+    }
+  },
+
+  // only accepts a single item
+  toastSuccess: (body: string, header = 'Success', option = toastrLegacyOption.success): void => {
+    toastrLegacy.success(body, header, option);
+  },
+
+  toastWarning: (body: string, header = 'Warning', option = toastrLegacyOption.warning): void => {
+    toastrLegacy.warning(body, header, option);
+  },
+};

+ 1 - 1
packages/app/src/components/Admin/Common/AdminNavigation.jsx

@@ -36,7 +36,7 @@ const AdminNavigation = (props) => {
       case 'user-groups':              return <><i className="mr-1 icon-fw icon-people"></i>{          t('user_group_management.user_group_management') }</>;
       case 'user-groups':              return <><i className="mr-1 icon-fw icon-people"></i>{          t('user_group_management.user_group_management') }</>;
       case 'search':                   return <><i className="mr-1 icon-fw icon-magnifier"></i>{       t('full_text_search_management.full_text_search_management') }</>;
       case 'search':                   return <><i className="mr-1 icon-fw icon-magnifier"></i>{       t('full_text_search_management.full_text_search_management') }</>;
       case 'audit-log':                return <><i className="mr-1 icon-fw icon-feed"></i>{            t('audit_log_management.audit_log')}</>;
       case 'audit-log':                return <><i className="mr-1 icon-fw icon-feed"></i>{            t('audit_log_management.audit_log')}</>;
-      case 'plugins':                  return <><i className="mr-1 icon-fw icon-puzzle"></i>{           'Plugins Extention'}</>;
+      case 'plugins':                  return <><i className="mr-1 icon-fw icon-puzzle"></i>{           'Plugins Extension'}</>;
       case 'cloud':                    return <><i className="mr-1 icon-fw icon-share-alt"></i>{       t('cloud_setting_management.to_cloud_settings')} </>;
       case 'cloud':                    return <><i className="mr-1 icon-fw icon-share-alt"></i>{       t('cloud_setting_management.to_cloud_settings')} </>;
       default:                         return <><i className="mr-1 icon-fw icon-home"></i>{            t('wiki_management_home_page') }</>;
       default:                         return <><i className="mr-1 icon-fw icon-home"></i>{            t('wiki_management_home_page') }</>;
       /* eslint-enable no-multi-spaces, max-len */
       /* eslint-enable no-multi-spaces, max-len */

+ 4 - 1
packages/app/src/components/Admin/ElasticsearchManagement/ElasticsearchManagement.tsx

@@ -54,9 +54,12 @@ const ElasticsearchManagement = () => {
             setIsConfigured(false);
             setIsConfigured(false);
           }
           }
         }
         }
+        toastError(errors as Error[]);
+      }
+      else {
+        toastError(errors as Error);
       }
       }
 
 
-      toastError(errors);
     }
     }
     finally {
     finally {
       setIsInitialized(true);
       setIsInitialized(true);

+ 1 - 1
packages/app/src/components/Admin/PluginsExtension/PluginInstallerForm.tsx

@@ -30,7 +30,7 @@ export const PluginInstallerForm = (): JSX.Element => {
     };
     };
 
 
     try {
     try {
-      await apiv3Post('/plugins-extention', { pluginInstallerForm });
+      await apiv3Post('/plugins-extension', { pluginInstallerForm });
       toastSuccess('Plugin Install Successed!');
       toastSuccess('Plugin Install Successed!');
     }
     }
     catch (err) {
     catch (err) {

+ 1 - 1
packages/app/src/components/Common/Dropdown/PageItemControl.tsx

@@ -248,7 +248,7 @@ const PageItemControlDropdownMenu = React.memo((props: DropdownMenuProps): JSX.E
     <DropdownMenu
     <DropdownMenu
       data-testid="page-item-control-menu"
       data-testid="page-item-control-menu"
       positionFixed
       positionFixed
-      modifiers={{ preventOverflow: { boundariesElement: undefined } }}
+      modifiers={{ preventOverflow: { boundariesElement: 'viewport' } }}
       right={alignRight}
       right={alignRight}
     >
     >
       {contents}
       {contents}

+ 2 - 2
packages/app/src/components/InAppNotification/InAppNotificationList.tsx

@@ -2,7 +2,7 @@ import React, { FC } from 'react';
 
 
 import { HasObjectId } from '@growi/core';
 import { HasObjectId } from '@growi/core';
 
 
-import { IInAppNotification, PaginateResult } from '~/interfaces/in-app-notification';
+import type { IInAppNotification, PaginateResult } from '~/interfaces/in-app-notification';
 
 
 import InAppNotificationElm from './InAppNotificationElm';
 import InAppNotificationElm from './InAppNotificationElm';
 
 
@@ -26,7 +26,7 @@ const InAppNotificationList: FC<Props> = (props: Props) => {
     );
     );
   }
   }
 
 
-  const notifications = inAppNotificationData.docs;
+  const notifications = inAppNotificationData.docs.filter((notification) => { return notification.parsedSnapshot != null });
 
 
   return (
   return (
     <>
     <>

+ 3 - 6
packages/app/src/components/InAppNotification/PageNotification/PageModelNotification.tsx

@@ -6,10 +6,9 @@ import { HasObjectId } from '@growi/core';
 import { PagePathLabel } from '@growi/ui';
 import { PagePathLabel } from '@growi/ui';
 import { useRouter } from 'next/router';
 import { useRouter } from 'next/router';
 
 
-import { IInAppNotificationOpenable } from '~/client/interfaces/in-app-notification-openable';
-import { IInAppNotification } from '~/interfaces/in-app-notification';
+import type { IInAppNotificationOpenable } from '~/client/interfaces/in-app-notification-openable';
+import type { IInAppNotification } from '~/interfaces/in-app-notification';
 
 
-import { parseSnapshot } from '../../../models/serializers/in-app-notification-snapshot/page';
 import FormattedDistanceDate from '../../FormattedDistanceDate';
 import FormattedDistanceDate from '../../FormattedDistanceDate';
 
 
 interface Props {
 interface Props {
@@ -27,8 +26,6 @@ const PageModelNotification: ForwardRefRenderFunction<IInAppNotificationOpenable
 
 
   const router = useRouter();
   const router = useRouter();
 
 
-  const snapshot = parseSnapshot(notification.snapshot);
-
   // publish open()
   // publish open()
   useImperativeHandle(ref, () => ({
   useImperativeHandle(ref, () => ({
     open() {
     open() {
@@ -45,7 +42,7 @@ const PageModelNotification: ForwardRefRenderFunction<IInAppNotificationOpenable
   return (
   return (
     <div className="p-2 overflow-hidden">
     <div className="p-2 overflow-hidden">
       <div className="text-truncate">
       <div className="text-truncate">
-        <b>{actionUsers}</b> {actionMsg} <PagePathLabel path={snapshot.path} />
+        <b>{actionUsers}</b> {actionMsg} <PagePathLabel path={notification.parsedSnapshot?.path ?? ''} />
       </div>
       </div>
       <i className={`${actionIcon} mr-2`} />
       <i className={`${actionIcon} mr-2`} />
       <FormattedDistanceDate
       <FormattedDistanceDate

+ 2 - 0
packages/app/src/components/Layout/RawLayout.tsx

@@ -1,6 +1,7 @@
 import React, { ReactNode, useState } from 'react';
 import React, { ReactNode, useState } from 'react';
 
 
 import Head from 'next/head';
 import Head from 'next/head';
+import { ToastContainer } from 'react-toastify';
 import { useIsomorphicLayoutEffect } from 'usehooks-ts';
 import { useIsomorphicLayoutEffect } from 'usehooks-ts';
 
 
 import { useGrowiTheme } from '~/stores/context';
 import { useGrowiTheme } from '~/stores/context';
@@ -48,6 +49,7 @@ export const RawLayout = ({ children, title, className }: Props): JSX.Element =>
         <GrowiThemeProvider theme={growiTheme} colorScheme={colorScheme}>
         <GrowiThemeProvider theme={growiTheme} colorScheme={colorScheme}>
           <div className={classNames.join(' ')} data-color-scheme={colorScheme}>
           <div className={classNames.join(' ')} data-color-scheme={colorScheme}>
             {children}
             {children}
+            <ToastContainer theme={colorScheme} />
           </div>
           </div>
         </GrowiThemeProvider>
         </GrowiThemeProvider>
       </NextThemesProvider>
       </NextThemesProvider>

+ 7 - 4
packages/app/src/components/Navbar/GrowiContextualSubNavigation.tsx

@@ -1,6 +1,6 @@
 import React, { useState, useEffect, useCallback } from 'react';
 import React, { useState, useEffect, useCallback } from 'react';
 
 
-import { isPopulated, IUser } from '@growi/core';
+import { isPopulated, IUser, pagePathUtils } from '@growi/core';
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 import dynamic from 'next/dynamic';
 import dynamic from 'next/dynamic';
 import { useRouter } from 'next/router';
 import { useRouter } from 'next/router';
@@ -43,6 +43,7 @@ import type { SubNavButtonsProps } from './SubNavButtons';
 import AuthorInfoStyles from './AuthorInfo.module.scss';
 import AuthorInfoStyles from './AuthorInfo.module.scss';
 import PageEditorModeManagerStyles from './PageEditorModeManager.module.scss';
 import PageEditorModeManagerStyles from './PageEditorModeManager.module.scss';
 
 
+const { isUsersHomePage } = pagePathUtils;
 
 
 const AuthorInfoSkeleton = () => <Skeleton additionalClass={`${AuthorInfoStyles['grw-author-info-skeleton']} py-1`} />;
 const AuthorInfoSkeleton = () => <Skeleton additionalClass={`${AuthorInfoStyles['grw-author-info-skeleton']} py-1`} />;
 
 
@@ -304,11 +305,13 @@ const GrowiContextualSubNavigation = (props: GrowiContextualSubNavigationProps):
         router.push(path);
         router.push(path);
       }
       }
       else {
       else {
-        reload();
+        // Do not use "router.push(currentPathname)" to avoid `Error: Invariant: attempted to hard navigate to the same URL`
+        // See: https://github.com/weseek/growi/pull/7061
+        router.reload();
       }
       }
     };
     };
     openDeleteModal([pageWithMeta], { onDeleted: deletedHandler });
     openDeleteModal([pageWithMeta], { onDeleted: deletedHandler });
-  }, [openDeleteModal, reload, router]);
+  }, [openDeleteModal, router]);
 
 
   const switchContentWidthHandler = useCallback(async(pageId: string, value: boolean) => {
   const switchContentWidthHandler = useCallback(async(pageId: string, value: boolean) => {
     await updateContentWidth(pageId, value);
     await updateContentWidth(pageId, value);
@@ -378,7 +381,7 @@ const GrowiContextualSubNavigation = (props: GrowiContextualSubNavigationProps):
               />
               />
             )}
             )}
           </div>
           </div>
-          { (isAbleToShowPageAuthors && !isCompactMode) && (
+          { (isAbleToShowPageAuthors && !isCompactMode && !isUsersHomePage(path ?? '')) && (
             <ul className={`${AuthorInfoStyles['grw-author-info']} text-nowrap border-left d-none d-lg-block d-edit-none py-2 pl-4 mb-0 ml-3`}>
             <ul className={`${AuthorInfoStyles['grw-author-info']} text-nowrap border-left d-none d-lg-block d-edit-none py-2 pl-4 mb-0 ml-3`}>
               <li className="pb-1">
               <li className="pb-1">
                 { currentPage != null
                 { currentPage != null

+ 1 - 1
packages/app/src/components/Page/CopyDropdown.jsx

@@ -118,7 +118,7 @@ const CopyDropdown = (props) => {
           <span id={dropdownToggleId}>{children}</span>
           <span id={dropdownToggleId}>{children}</span>
         </DropdownToggle>
         </DropdownToggle>
 
 
-        <DropdownMenu positionFixed modifiers={{ preventOverflow: { boundariesElement: undefined } }}>
+        <DropdownMenu positionFixed modifiers={{ preventOverflow: { boundariesElement: 'viewport' } }}>
 
 
           <div className="d-flex align-items-center justify-content-between">
           <div className="d-flex align-items-center justify-content-between">
             <DropdownItem header className="px-3">
             <DropdownItem header className="px-3">

+ 9 - 11
packages/app/src/components/Page/RenderTagLabels.tsx

@@ -21,25 +21,23 @@ const RenderTagLabels = React.memo((props: RenderTagLabelsProps) => {
   }
   }
 
 
   const isTagsEmpty = tags.length === 0;
   const isTagsEmpty = tags.length === 0;
-  const tagElements = tags.map((tag) => {
-    return (
-      <a key={tag} href={`/_search?q=tag:${tag}`} className="grw-tag-label badge badge-secondary mr-2">
-        {tag}
-      </a>
-    );
-  });
 
 
   return (
   return (
     <>
     <>
-      {tagElements}
-
+      {tags.map((tag) => {
+        return (
+          <a key={tag} href={`/_search?q=tag:${tag}`} className="grw-tag-label badge badge-secondary mr-2">
+            {tag}
+          </a>
+        );
+      })}
       <div id="edit-tags-btn-wrapper-for-tooltip">
       <div id="edit-tags-btn-wrapper-for-tooltip">
         <a
         <a
-          className={`btn btn-link btn-edit-tags p-0 text-muted d-flex ${isTagsEmpty ? 'no-tags' : ''} ${isGuestUser ? 'disabled' : ''}`}
+          className={`btn btn-link btn-edit-tags text-muted p-0 d-flex align-items-center ${isTagsEmpty && 'no-tags'} ${isGuestUser && 'disabled'}`}
           onClick={openEditorHandler}
           onClick={openEditorHandler}
         >
         >
           { isTagsEmpty && <>{ t('Add tags for this page') }</>}
           { isTagsEmpty && <>{ t('Add tags for this page') }</>}
-          <i className="ml-1 icon-plus"></i>
+          <i className={`icon-plus ${isTagsEmpty && 'ml-1'}`}/>
         </a>
         </a>
       </div>
       </div>
       {isGuestUser && (
       {isGuestUser && (

+ 1 - 1
packages/app/src/components/Page/TagLabels.tsx

@@ -37,7 +37,7 @@ export const TagLabels:FC<Props> = (props: Props) => {
   return (
   return (
     <>
     <>
       <div className={`${styles['grw-tag-labels']} grw-tag-labels d-flex align-items-center`} data-testid="grw-tag-labels">
       <div className={`${styles['grw-tag-labels']} grw-tag-labels d-flex align-items-center`} data-testid="grw-tag-labels">
-        <i className="tag-icon icon-tag mr-2"></i>
+        <i className="tag-icon icon-tag mr-2"/>
         <RenderTagLabels
         <RenderTagLabels
           tags={tags}
           tags={tags}
           openEditorModal={openEditorModal}
           openEditorModal={openEditorModal}

+ 13 - 11
packages/app/src/components/PageAlert/TrashPageAlert.tsx

@@ -1,4 +1,4 @@
-import React from 'react';
+import React, { useCallback } from 'react';
 
 
 import { UserPicture } from '@growi/ui';
 import { UserPicture } from '@growi/ui';
 import { format } from 'date-fns';
 import { format } from 'date-fns';
@@ -32,27 +32,25 @@ export const TrashPageAlert = (): JSX.Element => {
   const { open: openDeleteModal } = usePageDeleteModal();
   const { open: openDeleteModal } = usePageDeleteModal();
   const { open: openPutBackPageModal } = usePutBackPageModal();
   const { open: openPutBackPageModal } = usePutBackPageModal();
 
 
-  if (!isTrashPage) {
-    return <></>;
-  }
-
 
 
   const deleteUser = pageData?.deleteUser;
   const deleteUser = pageData?.deleteUser;
   const deletedAt = pageData?.deletedAt ? format(new Date(pageData?.deletedAt), 'yyyy/MM/dd HH:mm') : '';
   const deletedAt = pageData?.deletedAt ? format(new Date(pageData?.deletedAt), 'yyyy/MM/dd HH:mm') : '';
   const revisionId = pageData?.revision?._id;
   const revisionId = pageData?.revision?._id;
 
 
 
 
-  function openPutbackPageModalHandler() {
+  const openPutbackPageModalHandler = useCallback(() => {
     if (pageId === undefined || pagePath === undefined) {
     if (pageId === undefined || pagePath === undefined) {
       return;
       return;
     }
     }
     const putBackedHandler = () => {
     const putBackedHandler = () => {
-      router.push(`/${pageId}`);
+      // Do not use "router.push(`/${pageId}`)" to avoid `Error: Invariant: attempted to hard navigate to the same URL`
+      // See: https://github.com/weseek/growi/pull/7054
+      router.reload();
     };
     };
     openPutBackPageModal({ pageId, path: pagePath }, { onPutBacked: putBackedHandler });
     openPutBackPageModal({ pageId, path: pagePath }, { onPutBacked: putBackedHandler });
-  }
+  }, [openPutBackPageModal, pageId, pagePath, router]);
 
 
-  function openPageDeleteModalHandler() {
+  const openPageDeleteModalHandler = useCallback(() => {
     if (pageId === undefined || revisionId === undefined || pagePath === undefined) {
     if (pageId === undefined || revisionId === undefined || pagePath === undefined) {
       return;
       return;
     }
     }
@@ -65,9 +63,9 @@ export const TrashPageAlert = (): JSX.Element => {
       meta: pageInfo,
       meta: pageInfo,
     };
     };
     openDeleteModal([pageToDelete], { onDeleted: onDeletedHandler });
     openDeleteModal([pageToDelete], { onDeleted: onDeletedHandler });
-  }
+  }, [openDeleteModal, pageId, pageInfo, pagePath, revisionId]);
 
 
-  function renderTrashPageManagementButtons() {
+  const renderTrashPageManagementButtons = useCallback(() => {
     return (
     return (
       <>
       <>
         <button
         <button
@@ -88,6 +86,10 @@ export const TrashPageAlert = (): JSX.Element => {
         </button>
         </button>
       </>
       </>
     );
     );
+  }, [openPageDeleteModalHandler, openPutbackPageModalHandler, pageInfo?.isAbleToDeleteCompletely, t]);
+
+  if (!isTrashPage) {
+    return <></>;
   }
   }
 
 
   return (
   return (

+ 4 - 0
packages/app/src/components/PageCreateModal.jsx

@@ -55,6 +55,10 @@ const PageCreateModal = () => {
     }
     }
   }, [isOpened, pathname, isCreatable]);
   }, [isOpened, pathname, isCreatable]);
 
 
+  useEffect(() => {
+    setTodayInput1(t('Memo'));
+  }, [t]);
+
   const checkIsUsersHomePageDebounce = useMemo(() => {
   const checkIsUsersHomePageDebounce = useMemo(() => {
     const checkIsUsersHomePage = () => {
     const checkIsUsersHomePage = () => {
       setIsMatchedWithUserHomePagePath(isUsersHomePage(pageNameInput));
       setIsMatchedWithUserHomePagePath(isUsersHomePage(pageNameInput));

+ 2 - 1
packages/app/src/components/PageEditor.tsx

@@ -2,6 +2,7 @@ import React, {
   useCallback, useEffect, useMemo, useRef, useState,
   useCallback, useEffect, useMemo, useRef, useState,
 } from 'react';
 } from 'react';
 
 
+
 import EventEmitter from 'events';
 import EventEmitter from 'events';
 
 
 import {
 import {
@@ -13,8 +14,8 @@ import { useRouter } from 'next/router';
 import { throttle, debounce } from 'throttle-debounce';
 import { throttle, debounce } from 'throttle-debounce';
 
 
 import { useSaveOrUpdate } from '~/client/services/page-operation';
 import { useSaveOrUpdate } from '~/client/services/page-operation';
-import { toastSuccess, toastError } from '~/client/util/apiNotification';
 import { apiGet, apiPostForm } from '~/client/util/apiv1-client';
 import { apiGet, apiPostForm } from '~/client/util/apiv1-client';
+import { toastError, toastSuccess } from '~/client/util/toastr';
 import { IEditorMethods } from '~/interfaces/editor-methods';
 import { IEditorMethods } from '~/interfaces/editor-methods';
 import { OptionsToSave } from '~/interfaces/page-operation';
 import { OptionsToSave } from '~/interfaces/page-operation';
 import { SocketEventName } from '~/interfaces/websocket';
 import { SocketEventName } from '~/interfaces/websocket';

+ 3 - 0
packages/app/src/interfaces/in-app-notification.ts

@@ -1,3 +1,5 @@
+import type { IPageSnapshot } from '~/models/serializers/in-app-notification-snapshot/page';
+
 import { IPage } from './page';
 import { IPage } from './page';
 import { IUser } from './user';
 import { IUser } from './user';
 
 
@@ -16,6 +18,7 @@ export interface IInAppNotification {
   actionUsers: IUser[]
   actionUsers: IUser[]
   createdAt: Date
   createdAt: Date
   snapshot: string
   snapshot: string
+  parsedSnapshot?: IPageSnapshot
 }
 }
 
 
 /*
 /*

+ 2 - 2
packages/app/src/models/serializers/in-app-notification-snapshot/page.ts

@@ -1,5 +1,5 @@
-import { IUser } from '~/interfaces/user';
-import { IPage } from '~/interfaces/page';
+import type { IPage } from '~/interfaces/page';
+import type { IUser } from '~/interfaces/user';
 
 
 export interface IPageSnapshot {
 export interface IPageSnapshot {
   path: string
   path: string

+ 1 - 1
packages/app/src/pages/admin/plugins.page.tsx

@@ -27,7 +27,7 @@ const AdminAppPage: NextPage<CommonProps> = (props) => {
   useIsMaintenanceMode(props.isMaintenanceMode);
   useIsMaintenanceMode(props.isMaintenanceMode);
   useCurrentUser(props.currentUser ?? null);
   useCurrentUser(props.currentUser ?? null);
 
 
-  const title = 'Plugins Extention';
+  const title = 'Plugins Extension';
   const injectableContainers: Container<any>[] = [];
   const injectableContainers: Container<any>[] = [];
 
 
   if (isClient()) {
   if (isClient()) {

+ 1 - 1
packages/app/src/server/routes/apiv3/index.js

@@ -104,7 +104,7 @@ module.exports = (crowi, app) => {
     userActivation.validateCompleteRegistration,
     userActivation.validateCompleteRegistration,
     userActivation.completeRegistrationAction(crowi));
     userActivation.completeRegistrationAction(crowi));
 
 
-  router.use('/plugins-extention', require('./plugins-extention')(crowi));
+  router.use('/plugins-extension', require('./plugins-extension')(crowi));
 
 
   router.use('/user-ui-settings', require('./user-ui-settings')(crowi));
   router.use('/user-ui-settings', require('./user-ui-settings')(crowi));
 
 

+ 0 - 0
packages/app/src/server/routes/apiv3/plugins-extention.ts → packages/app/src/server/routes/apiv3/plugins-extension.ts


+ 12 - 12
packages/app/src/server/routes/login.js

@@ -102,18 +102,18 @@ module.exports = function(crowi, app) {
 
 
   actions.preLogin = function(req, res, next) {
   actions.preLogin = function(req, res, next) {
     // user has already logged in
     // user has already logged in
-    // const { user } = req;
-    // if (user != null && user.status === User.STATUS_ACTIVE) {
-    //   const { redirectTo } = req.session;
-    //   // remove session.redirectTo
-    //   delete req.session.redirectTo;
-    //   return res.safeRedirect(redirectTo);
-    // }
-
-    // // set referer to 'redirectTo'
-    // if (req.session.redirectTo == null && req.headers.referer != null) {
-    //   req.session.redirectTo = req.headers.referer;
-    // }
+    const { user } = req;
+    if (user != null && user.status === User.STATUS_ACTIVE) {
+      const { redirectTo } = req.session;
+      // remove session.redirectTo
+      delete req.session.redirectTo;
+      return res.safeRedirect(redirectTo);
+    }
+
+    // set referer to 'redirectTo'
+    if (req.session.redirectTo == null && req.headers.referer != null) {
+      req.session.redirectTo = req.headers.referer;
+    }
 
 
     next();
     next();
   };
   };

+ 21 - 2
packages/app/src/stores/in-app-notification.ts

@@ -1,7 +1,15 @@
 import useSWR, { SWRResponse } from 'swr';
 import useSWR, { SWRResponse } from 'swr';
-import { InAppNotificationStatuses, IInAppNotification, PaginateResult } from '~/interfaces/in-app-notification';
+
+import type { InAppNotificationStatuses, IInAppNotification, PaginateResult } from '~/interfaces/in-app-notification';
+import { parseSnapshot } from '~/models/serializers/in-app-notification-snapshot/page';
+import loggerFactory from '~/utils/logger';
+
 import { apiv3Get } from '../client/util/apiv3-client';
 import { apiv3Get } from '../client/util/apiv3-client';
 
 
+const logger = loggerFactory('growi:cli:InAppNotification');
+
+type inAppNotificationPaginateResult = PaginateResult<IInAppNotification>
+
 // eslint-disable-next-line @typescript-eslint/no-unused-vars
 // eslint-disable-next-line @typescript-eslint/no-unused-vars
 export const useSWRxInAppNotifications = <Data, Error>(
 export const useSWRxInAppNotifications = <Data, Error>(
   limit: number,
   limit: number,
@@ -10,7 +18,18 @@ export const useSWRxInAppNotifications = <Data, Error>(
 ): SWRResponse<PaginateResult<IInAppNotification>, Error> => {
 ): SWRResponse<PaginateResult<IInAppNotification>, Error> => {
   return useSWR(
   return useSWR(
     ['/in-app-notification/list', limit, offset, status],
     ['/in-app-notification/list', limit, offset, status],
-    endpoint => apiv3Get(endpoint, { limit, offset, status }).then(response => response.data),
+    endpoint => apiv3Get(endpoint, { limit, offset, status }).then((response) => {
+      const inAppNotificationPaginateResult = response.data as inAppNotificationPaginateResult;
+      inAppNotificationPaginateResult.docs.forEach((doc) => {
+        try {
+          doc.parsedSnapshot = parseSnapshot(doc.snapshot as string);
+        }
+        catch (err) {
+          logger.warn('Failed to parse snapshot', err);
+        }
+      });
+      return inAppNotificationPaginateResult;
+    }),
   );
   );
 };
 };
 
 

+ 9 - 3
packages/app/src/styles/_editor.scss

@@ -69,9 +69,6 @@
     display: none;
     display: none;
   }
   }
 
 
-  .toast-top-right {
-    top: 64px;
-  }
 
 
   /*****************
   /*****************
    * Expand Editor
    * Expand Editor
@@ -276,6 +273,15 @@
       vertical-align: unset;
       vertical-align: unset;
     }
     }
   }
   }
+
+
+  /*****************
+   *     Toastr
+   *****************/
+  .Toastify .Toastify__toast-container {
+    top: 4.5em;
+  }
+
 }
 }
 
 
 .layout-root.editing {
 .layout-root.editing {

+ 1 - 0
packages/app/src/styles/molecules/toastr.scss

@@ -1,3 +1,4 @@
 :root {
 :root {
   @import '~toastr/build/toastr';
   @import '~toastr/build/toastr';
 }
 }
+@import '~react-toastify/scss/main';

+ 1 - 1
packages/app/src/utils/array-utils.ts

@@ -1,6 +1,6 @@
 // converts non-array item to array
 // converts non-array item to array
 
 
-export const toArrayIfNot = <T = unknown>(item?: T): T[] => {
+export const toArrayIfNot = <T = unknown>(item?: T | T[]): T[] => {
   if (item == null) {
   if (item == null) {
     return [];
     return [];
   }
   }

+ 0 - 0
packages/app/test/cypress/integration/10-install/install.spec.ts → packages/app/test/cypress/integration/10-install/10-install--install.spec.ts


+ 0 - 0
packages/app/test/cypress/integration/20-basic-features/access-to-page.spec.ts → packages/app/test/cypress/integration/20-basic-features/20-basic-features--access-to-page.spec.ts


+ 0 - 0
packages/app/test/cypress/integration/20-basic-features/access-to-pagelist.spec.ts → packages/app/test/cypress/integration/20-basic-features/20-basic-features--access-to-pagelist.spec.ts


+ 0 - 0
packages/app/test/cypress/integration/20-basic-features/click-page-icons.spec.ts → packages/app/test/cypress/integration/20-basic-features/20-basic-features--click-page-icons.spec.ts


+ 0 - 0
packages/app/test/cypress/integration/20-basic-features/use-tools.spec.ts → packages/app/test/cypress/integration/20-basic-features/20-basic-features--use-tools.spec.ts


+ 0 - 0
packages/app/test/cypress/integration/21-basic-features-for-guest/access-to-page.spec.ts → packages/app/test/cypress/integration/21-basic-features-for-guest/21-basic-features-for-guest--access-to-page.spec.ts


+ 7 - 6
packages/app/test/cypress/integration/30-search/search.spec.ts → packages/app/test/cypress/integration/30-search/30-search--search.spec.ts

@@ -202,6 +202,7 @@ context('Search all pages', () => {
 
 
   });
   });
 
 
+  // TODO: fix this VRT
   it('Successfully order page search results by tag', () => {
   it('Successfully order page search results by tag', () => {
     const tag = 'help';
     const tag = 'help';
 
 
@@ -225,8 +226,8 @@ context('Search all pages', () => {
     cy.getByTestid('search-result-base').should('be.visible');
     cy.getByTestid('search-result-base').should('be.visible');
     cy.getByTestid('search-result-list').should('be.visible');
     cy.getByTestid('search-result-list').should('be.visible');
     cy.getByTestid('search-result-content').should('be.visible');
     cy.getByTestid('search-result-content').should('be.visible');
-    cy.get('.wiki').should('be.visible');
-    cy.screenshot(`${ssPrefix}2-tag-order-by-relevance`);
+    // cy.get('.wiki').should('be.visible');
+    // cy.screenshot(`${ssPrefix}2-tag-order-by-relevance`);
 
 
     cy.get('.grw-search-page-nav').within(() => {
     cy.get('.grw-search-page-nav').within(() => {
       cy.get('button.dropdown-toggle').first().click({force: true});
       cy.get('button.dropdown-toggle').first().click({force: true});
@@ -236,8 +237,8 @@ context('Search all pages', () => {
     cy.getByTestid('search-result-base').should('be.visible');
     cy.getByTestid('search-result-base').should('be.visible');
     cy.getByTestid('search-result-list').should('be.visible');
     cy.getByTestid('search-result-list').should('be.visible');
     cy.getByTestid('search-result-content').should('be.visible');
     cy.getByTestid('search-result-content').should('be.visible');
-    cy.get('.wiki').should('be.visible');
-    cy.screenshot(`${ssPrefix}3-tag-order-by-creation-date`);
+    // cy.get('.wiki').should('be.visible');
+    // cy.screenshot(`${ssPrefix}3-tag-order-by-creation-date`);
 
 
     cy.get('.grw-search-page-nav').within(() => {
     cy.get('.grw-search-page-nav').within(() => {
       cy.get('button.dropdown-toggle').first().click({force: true});
       cy.get('button.dropdown-toggle').first().click({force: true});
@@ -247,10 +248,10 @@ context('Search all pages', () => {
     cy.getByTestid('search-result-base').should('be.visible');
     cy.getByTestid('search-result-base').should('be.visible');
     cy.getByTestid('search-result-list').should('be.visible');
     cy.getByTestid('search-result-list').should('be.visible');
     cy.getByTestid('search-result-content').should('be.visible');
     cy.getByTestid('search-result-content').should('be.visible');
-    cy.get('.wiki').should('be.visible');
+    // cy.get('.wiki').should('be.visible');
     cy.waitUntilSpinnerDisappear();
     cy.waitUntilSpinnerDisappear();
 
 
-    cy.screenshot(`${ssPrefix}4-tag-order-by-last-update-date`);
+    // cy.screenshot(`${ssPrefix}4-tag-order-by-last-update-date`);
   });
   });
 
 
 });
 });

+ 0 - 0
packages/app/test/cypress/integration/40-admin/access-to-admin-page.spec.ts → packages/app/test/cypress/integration/40-admin/40-admin--access-to-admin-page.spec.ts


+ 0 - 0
packages/app/test/cypress/integration/50-sidebar/access-to-side-bar.spec.ts → packages/app/test/cypress/integration/50-sidebar/50-sidebar--access-to-side-bar.spec.ts


+ 0 - 0
packages/app/test/cypress/integration/50-sidebar/switching-sidebar-mode.spec.ts → packages/app/test/cypress/integration/50-sidebar/50-sidebar--switching-sidebar-mode.spec.ts


+ 0 - 0
packages/app/test/cypress/integration/60-home/home.spec.ts → packages/app/test/cypress/integration/60-home/60-home--home.spec.ts


+ 0 - 33
packages/app/test/cypress/plugins/index.ts

@@ -1,33 +0,0 @@
-/// <reference types="cypress" />
-// ***********************************************************
-// This example plugins/index.js can be used to load plugins
-//
-// You can change the location of this file or turn off loading
-// the plugins file with the 'pluginsFile' configuration option.
-//
-// You can read more here:
-// https://on.cypress.io/plugins-guide
-// ***********************************************************
-
-// This function is called when a project is opened or re-opened (e.g. due to
-// the project's config changing)
-
-/**
- * @type {Cypress.PluginConfig}
- */
-// eslint-disable-next-line no-unused-vars
-module.exports = (on, config) => {
-  // `on` is used to hook into various events Cypress emits
-  // `config` is the resolved Cypress config
-
-  // change screen size
-  // see: https://docs.cypress.io/api/plugins/browser-launch-api#Set-screen-size-when-running-headless
-  on('before:browser:launch', (browser, launchOptions) => {
-    if (browser.name === 'chrome' && browser.isHeadless) {
-      launchOptions.args.push('--window-size=1400,1024')
-      launchOptions.args.push('--force-device-scale-factor=1')
-    }
-
-    return launchOptions
-  })
-}

+ 47 - 20
yarn.lock

@@ -1647,6 +1647,11 @@
   resolved "https://registry.yarnpkg.com/@browser-bunyan/levels/-/levels-1.8.0.tgz#1c0a98d04284e0620e8ee414d7ce43385080a5cf"
   resolved "https://registry.yarnpkg.com/@browser-bunyan/levels/-/levels-1.8.0.tgz#1c0a98d04284e0620e8ee414d7ce43385080a5cf"
   integrity sha512-f9oSDik8kAl+4rhVyHqIr012P1boHFUKc7D9nzA5+lDsFoP90UQnDwpseqBdF2mTaWYju10E7h+GdH8u+7MHOQ==
   integrity sha512-f9oSDik8kAl+4rhVyHqIr012P1boHFUKc7D9nzA5+lDsFoP90UQnDwpseqBdF2mTaWYju10E7h+GdH8u+7MHOQ==
 
 
+"@colors/colors@1.5.0":
+  version "1.5.0"
+  resolved "https://registry.yarnpkg.com/@colors/colors/-/colors-1.5.0.tgz#bb504579c1cae923e6576a4f5da43d25f97bdbd9"
+  integrity sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==
+
 "@cspotcode/source-map-support@^0.8.0":
 "@cspotcode/source-map-support@^0.8.0":
   version "0.8.1"
   version "0.8.1"
   resolved "https://registry.yarnpkg.com/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz#00629c35a688e05a88b1cda684fb9d5e73f000a1"
   resolved "https://registry.yarnpkg.com/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz#00629c35a688e05a88b1cda684fb9d5e73f000a1"
@@ -4532,10 +4537,10 @@
     "@types/mime" "^1"
     "@types/mime" "^1"
     "@types/node" "*"
     "@types/node" "*"
 
 
-"@types/sinonjs__fake-timers@^6.0.2":
-  version "6.0.4"
-  resolved "https://registry.yarnpkg.com/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-6.0.4.tgz#0ecc1b9259b76598ef01942f547904ce61a6a77d"
-  integrity sha512-IFQTJARgMUBF+xVd2b+hIgXWrZEjND3vJtRCvIelcFB5SIXfjV4bOHbHJ0eXKh+0COrBRc8MqteKAz/j88rE0A==
+"@types/sinonjs__fake-timers@8.1.1":
+  version "8.1.1"
+  resolved "https://registry.yarnpkg.com/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.1.tgz#b49c2c70150141a15e0fa7e79cf1f92a72934ce3"
+  integrity sha512-0kSuKjAS0TrGLJ0M/+8MaFkGsQhZpB6pxOmvS3K8FYI72K//YmdfoW9X2qPsAKh1mkwxGD5zib9s1FIFed6E8g==
 
 
 "@types/sizzle@*", "@types/sizzle@^2.3.2":
 "@types/sizzle@*", "@types/sizzle@^2.3.2":
   version "2.3.3"
   version "2.3.3"
@@ -6884,7 +6889,7 @@ cli-spinners@^2.5.0:
   resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-2.6.1.tgz#adc954ebe281c37a6319bfa401e6dd2488ffb70d"
   resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-2.6.1.tgz#adc954ebe281c37a6319bfa401e6dd2488ffb70d"
   integrity sha512-x/5fWmGMnbKQAaNwN+UZlV79qBLM9JFnJuJ03gIi5whrob0xV0ofNVHy9DhwGdsMJQc2OKv0oGmLzvaqvAVv+g==
   integrity sha512-x/5fWmGMnbKQAaNwN+UZlV79qBLM9JFnJuJ03gIi5whrob0xV0ofNVHy9DhwGdsMJQc2OKv0oGmLzvaqvAVv+g==
 
 
-cli-table3@^0.6.0, cli-table3@~0.6.0:
+cli-table3@^0.6.0:
   version "0.6.0"
   version "0.6.0"
   resolved "https://registry.yarnpkg.com/cli-table3/-/cli-table3-0.6.0.tgz#b7b1bc65ca8e7b5cef9124e13dc2b21e2ce4faee"
   resolved "https://registry.yarnpkg.com/cli-table3/-/cli-table3-0.6.0.tgz#b7b1bc65ca8e7b5cef9124e13dc2b21e2ce4faee"
   integrity sha512-gnB85c3MGC7Nm9I/FkiasNBOKjOiO1RNuXXarQms37q4QMpWdlbBgD/VnOStA2faG1dpXMv31RFApjX1/QdgWQ==
   integrity sha512-gnB85c3MGC7Nm9I/FkiasNBOKjOiO1RNuXXarQms37q4QMpWdlbBgD/VnOStA2faG1dpXMv31RFApjX1/QdgWQ==
@@ -6894,6 +6899,15 @@ cli-table3@^0.6.0, cli-table3@~0.6.0:
   optionalDependencies:
   optionalDependencies:
     colors "^1.1.2"
     colors "^1.1.2"
 
 
+cli-table3@~0.6.1:
+  version "0.6.3"
+  resolved "https://registry.yarnpkg.com/cli-table3/-/cli-table3-0.6.3.tgz#61ab765aac156b52f222954ffc607a6f01dbeeb2"
+  integrity sha512-w5Jac5SykAeZJKntOxJCrm63Eg5/4dhMWIcuTbo9rpE+brgaSZo0RuNJZeOyMgsUdhDeojvgyQLmjI+K50ZGyg==
+  dependencies:
+    string-width "^4.2.0"
+  optionalDependencies:
+    "@colors/colors" "1.5.0"
+
 cli-truncate@^2.1.0:
 cli-truncate@^2.1.0:
   version "2.1.0"
   version "2.1.0"
   resolved "https://registry.yarnpkg.com/cli-truncate/-/cli-truncate-2.1.0.tgz#c39e28bf05edcde5be3b98992a22deed5a2b93c7"
   resolved "https://registry.yarnpkg.com/cli-truncate/-/cli-truncate-2.1.0.tgz#c39e28bf05edcde5be3b98992a22deed5a2b93c7"
@@ -6987,6 +7001,11 @@ clsx@^1.0.4:
   resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.0.4.tgz#0c0171f6d5cb2fe83848463c15fcc26b4df8c2ec"
   resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.0.4.tgz#0c0171f6d5cb2fe83848463c15fcc26b4df8c2ec"
   integrity sha512-1mQ557MIZTrL/140j+JVdRM6e31/OA4vTYxXgqIIZlndyfjHpyawKZia1Im05Vp9BWmImkcNrNtFYQMyFcgJDg==
   integrity sha512-1mQ557MIZTrL/140j+JVdRM6e31/OA4vTYxXgqIIZlndyfjHpyawKZia1Im05Vp9BWmImkcNrNtFYQMyFcgJDg==
 
 
+clsx@^1.1.1:
+  version "1.2.1"
+  resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.2.1.tgz#0ddc4a20a549b59c93a4116bb26f5294ca17dc12"
+  integrity sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==
+
 cmd-shim@^4.1.0:
 cmd-shim@^4.1.0:
   version "4.1.0"
   version "4.1.0"
   resolved "https://registry.yarnpkg.com/cmd-shim/-/cmd-shim-4.1.0.tgz#b3a904a6743e9fede4148c6f3800bf2a08135bdd"
   resolved "https://registry.yarnpkg.com/cmd-shim/-/cmd-shim-4.1.0.tgz#b3a904a6743e9fede4148c6f3800bf2a08135bdd"
@@ -7781,30 +7800,31 @@ currently-unhandled@^0.4.1:
   dependencies:
   dependencies:
     array-find-index "^1.0.1"
     array-find-index "^1.0.1"
 
 
-cypress@^9.2.0:
-  version "9.2.0"
-  resolved "https://registry.yarnpkg.com/cypress/-/cypress-9.2.0.tgz#727c20b4662167890db81d5f6ba615231835b17d"
-  integrity sha512-Jn26Tprhfzh/a66Sdj9SoaYlnNX6Mjfmj5PHu2a7l3YHXhrgmavM368wjCmgrxC6KHTOv9SpMQGhAJn+upDViA==
+cypress@^12.0.1:
+  version "12.0.1"
+  resolved "https://registry.yarnpkg.com/cypress/-/cypress-12.0.1.tgz#3a51a38b2f162256c7226e68e902cfe1750e3d92"
+  integrity sha512-I1Ag5RsPEINfUlQtV6xwkd6ktJuu5QGiKZ3pFa/IXjcyCY6I7CH3gOz0juLOhg/LXOPrQtZH35ulcWDQohyyEA==
   dependencies:
   dependencies:
     "@cypress/request" "^2.88.10"
     "@cypress/request" "^2.88.10"
     "@cypress/xvfb" "^1.2.4"
     "@cypress/xvfb" "^1.2.4"
     "@types/node" "^14.14.31"
     "@types/node" "^14.14.31"
-    "@types/sinonjs__fake-timers" "^6.0.2"
+    "@types/sinonjs__fake-timers" "8.1.1"
     "@types/sizzle" "^2.3.2"
     "@types/sizzle" "^2.3.2"
     arch "^2.2.0"
     arch "^2.2.0"
     blob-util "^2.0.2"
     blob-util "^2.0.2"
-    bluebird "3.7.2"
+    bluebird "^3.7.2"
+    buffer "^5.6.0"
     cachedir "^2.3.0"
     cachedir "^2.3.0"
     chalk "^4.1.0"
     chalk "^4.1.0"
     check-more-types "^2.24.0"
     check-more-types "^2.24.0"
     cli-cursor "^3.1.0"
     cli-cursor "^3.1.0"
-    cli-table3 "~0.6.0"
+    cli-table3 "~0.6.1"
     commander "^5.1.0"
     commander "^5.1.0"
     common-tags "^1.8.0"
     common-tags "^1.8.0"
     dayjs "^1.10.4"
     dayjs "^1.10.4"
     debug "^4.3.2"
     debug "^4.3.2"
     enquirer "^2.3.6"
     enquirer "^2.3.6"
-    eventemitter2 "^6.4.3"
+    eventemitter2 "6.4.7"
     execa "4.1.0"
     execa "4.1.0"
     executable "^4.1.1"
     executable "^4.1.1"
     extract-zip "2.0.1"
     extract-zip "2.0.1"
@@ -7817,15 +7837,15 @@ cypress@^9.2.0:
     listr2 "^3.8.3"
     listr2 "^3.8.3"
     lodash "^4.17.21"
     lodash "^4.17.21"
     log-symbols "^4.0.0"
     log-symbols "^4.0.0"
-    minimist "^1.2.5"
+    minimist "^1.2.6"
     ospath "^1.2.2"
     ospath "^1.2.2"
     pretty-bytes "^5.6.0"
     pretty-bytes "^5.6.0"
     proxy-from-env "1.0.0"
     proxy-from-env "1.0.0"
     request-progress "^3.0.0"
     request-progress "^3.0.0"
+    semver "^7.3.2"
     supports-color "^8.1.1"
     supports-color "^8.1.1"
     tmp "~0.2.1"
     tmp "~0.2.1"
     untildify "^4.0.0"
     untildify "^4.0.0"
-    url "^0.11.0"
     yauzl "^2.10.0"
     yauzl "^2.10.0"
 
 
 damerau-levenshtein@^1.0.7:
 damerau-levenshtein@^1.0.7:
@@ -9782,10 +9802,10 @@ event-target-shim@^5.0.0:
   resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789"
   resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789"
   integrity sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==
   integrity sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==
 
 
-eventemitter2@^6.4.3:
-  version "6.4.5"
-  resolved "https://registry.yarnpkg.com/eventemitter2/-/eventemitter2-6.4.5.tgz#97380f758ae24ac15df8353e0cc27f8b95644655"
-  integrity sha512-bXE7Dyc1i6oQElDG0jMRZJrRAn9QR2xyyFGmBdZleNmyQX0FqGYmhZIrIrpPfm/w//LTo4tVQGOGQcGCb5q9uw==
+eventemitter2@6.4.7:
+  version "6.4.7"
+  resolved "https://registry.yarnpkg.com/eventemitter2/-/eventemitter2-6.4.7.tgz#a7f6c4d7abf28a14c1ef3442f21cb306a054271d"
+  integrity sha512-tYUSVOGeQPKt/eC1ABfhHy5Xd96N3oIijJvN3O9+TsC28T5V9yX9oEfEK5faP0EFSNVOG97qtAS68GBrQB2hDg==
 
 
 eventemitter3@^3.1.0:
 eventemitter3@^3.1.0:
   version "3.1.2"
   version "3.1.2"
@@ -18648,6 +18668,13 @@ react-syntax-highlighter@^15.5.0:
     prismjs "^1.27.0"
     prismjs "^1.27.0"
     refractor "^3.6.0"
     refractor "^3.6.0"
 
 
+react-toastify@^9.1.1:
+  version "9.1.1"
+  resolved "https://registry.yarnpkg.com/react-toastify/-/react-toastify-9.1.1.tgz#9280caea4a13dc1739c350d90660a630807bf10b"
+  integrity sha512-pkFCla1z3ve045qvjEmn2xOJOy4ZciwRXm1oMPULVkELi5aJdHCN/FHnuqXq8IwGDLB7PPk2/J6uP9D8ejuiRw==
+  dependencies:
+    clsx "^1.1.1"
+
 react-transition-group@^2.3.1:
 react-transition-group@^2.3.1:
   version "2.9.0"
   version "2.9.0"
   resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-2.9.0.tgz#df9cdb025796211151a436c69a8f3b97b5b07c8d"
   resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-2.9.0.tgz#df9cdb025796211151a436c69a8f3b97b5b07c8d"
@@ -24039,7 +24066,7 @@ url@0.10.3:
     punycode "1.3.2"
     punycode "1.3.2"
     querystring "0.2.0"
     querystring "0.2.0"
 
 
-url@0.11.0, url@^0.11.0:
+url@0.11.0:
   version "0.11.0"
   version "0.11.0"
   resolved "https://registry.yarnpkg.com/url/-/url-0.11.0.tgz#3838e97cfc60521eb73c525a8e55bfdd9e2e28f1"
   resolved "https://registry.yarnpkg.com/url/-/url-0.11.0.tgz#3838e97cfc60521eb73c525a8e55bfdd9e2e28f1"
   dependencies:
   dependencies: