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

Merge branch 'master' into support/112440-migration-v6

yuken 3 лет назад
Родитель
Сommit
44970c9352
50 измененных файлов с 255 добавлено и 259 удалено
  1. 1 1
      packages/app/package.json
  2. 1 1
      packages/app/public/static/locales/en_US/commons.json
  3. 1 1
      packages/app/public/static/locales/ja_JP/commons.json
  4. 1 1
      packages/app/public/static/locales/zh_CN/commons.json
  5. 1 1
      packages/app/src/client/util/toastr.ts
  6. 9 8
      packages/app/src/components/Admin/App/ConfirmModal.tsx
  7. 3 3
      packages/app/src/components/Admin/Common/AdminNavigation.jsx
  8. 1 2
      packages/app/src/components/Admin/UserGroup/UserGroupDeleteModal.tsx
  9. 1 2
      packages/app/src/components/Admin/UserGroup/UserGroupForm.tsx
  10. 1 2
      packages/app/src/components/Admin/UserGroup/UserGroupModal.tsx
  11. 1 2
      packages/app/src/components/Admin/UserGroup/UserGroupTable.tsx
  12. 1 1
      packages/app/src/components/Admin/UserGroupDetail/UserGroupUserTable.tsx
  13. 0 11
      packages/app/src/components/Layout/BasicLayout.tsx
  14. 1 1
      packages/app/src/components/Me/BasicInfoSettings.tsx
  15. 0 3
      packages/app/src/components/Navbar/GrowiNavbar.tsx
  16. 1 2
      packages/app/src/components/NotAvailable.tsx
  17. 1 6
      packages/app/src/components/Page/PageContents.tsx
  18. 1 3
      packages/app/src/components/Page/RevisionLoader.tsx
  19. 1 1
      packages/app/src/components/PageAlert/TrashPageAlert.tsx
  20. 5 3
      packages/app/src/components/PageEditor/EditorNavbarBottom.tsx
  21. 1 2
      packages/app/src/components/SearchPage.tsx
  22. 2 2
      packages/app/src/components/SearchPage/SearchControl.tsx
  23. 1 7
      packages/app/src/components/ShareLink/ShareLinkPageContents.tsx
  24. 8 2
      packages/app/src/components/Sidebar/SidebarNav.tsx
  25. 2 1
      packages/app/src/components/StickyStretchableScroller.tsx
  26. 21 8
      packages/app/src/pages/[[...path]].page.tsx
  27. 2 12
      packages/app/src/pages/_app.page.tsx
  28. 5 5
      packages/app/src/pages/trash.page.tsx
  29. 1 1
      packages/app/src/pages/utils/commons.ts
  30. 1 1
      packages/app/src/stores/activity.ts
  31. 1 1
      packages/app/src/stores/attachment.tsx
  32. 1 1
      packages/app/src/stores/comment.tsx
  33. 8 8
      packages/app/src/stores/context.tsx
  34. 3 3
      packages/app/src/stores/editor.tsx
  35. 2 2
      packages/app/src/stores/in-app-notification.ts
  36. 1 1
      packages/app/src/stores/maintenanceMode.tsx
  37. 10 10
      packages/app/src/stores/page-listing.tsx
  38. 27 24
      packages/app/src/stores/page.tsx
  39. 49 44
      packages/app/src/stores/renderer.tsx
  40. 1 2
      packages/app/src/stores/search.tsx
  41. 4 1
      packages/app/src/stores/share-link.tsx
  42. 2 2
      packages/app/src/stores/tag.tsx
  43. 1 1
      packages/app/src/stores/template.tsx
  44. 27 21
      packages/app/src/stores/ui.tsx
  45. 2 2
      packages/app/src/stores/use-context-swr.tsx
  46. 11 11
      packages/app/src/stores/user-group.tsx
  47. 2 2
      packages/app/src/stores/user.tsx
  48. 3 1
      packages/app/src/stores/xss.ts
  49. 12 8
      packages/app/src/utils/swr-utils.ts
  50. 12 18
      yarn.lock

+ 1 - 1
packages/app/package.json

@@ -185,7 +185,7 @@
     "string-width": "=4.2.2",
     "superjson": "^1.9.1",
     "swagger-jsdoc": "^6.1.0",
-    "swr": "^1.3.0",
+    "swr": "^2.0.2",
     "throttle-debounce": "^3.0.1",
     "toastr": "^2.1.2",
     "uglifycss": "^0.0.29",

+ 1 - 1
packages/app/public/static/locales/en_US/commons.json

@@ -100,7 +100,7 @@
 
   "g2g_data_transfer": {
     "tab": "Data transfer",
-    "data_transfer": "GROWI To GROWI Data Transfer",
+    "data_transfer": "Data Transfer",
     "transfer_data_to_this_growi": "Transfer data from another GROWI to this GROWI",
     "publish_transfer_key": "Publish transfer key",
     "transfer_key_limit": "Transfer keys are valid for 1 hour after issuance.",

+ 1 - 1
packages/app/public/static/locales/ja_JP/commons.json

@@ -100,7 +100,7 @@
 
   "g2g_data_transfer": {
     "tab": "データ移行",
-    "data_transfer": "別GROWIとのデータ移行",
+    "data_transfer": "データ移行",
     "transfer_data_to_this_growi": "別GROWIのデータをこのGROWIへ移行する",
     "publish_transfer_key": "移行キーを発行する",
     "transfer_key_limit": "※ 移行キーの有効期限は発行から1時間となります。",

+ 1 - 1
packages/app/public/static/locales/zh_CN/commons.json

@@ -100,7 +100,7 @@
 
   "g2g_data_transfer": {
     "tab": "数据迁移",
-    "data_transfer": "与另一个GROWI的数据转移",
+    "data_transfer": "数据迁移",
     "transfer_data_to_this_growi": "将数据从另一个GROWI迁移到这个GROWI上",
     "publish_transfer_key": "发布迁移密钥",
     "transfer_key_limit": "迁移密钥在签发后一小时内有效。",

+ 1 - 1
packages/app/src/client/util/toastr.ts

@@ -17,7 +17,7 @@ export const toastError = (err: string | Error | Error[], option: ToastOptions =
 
   for (const err of errs) {
     const message = (typeof err === 'string') ? err : err.message;
-    toast.error(message || err, option);
+    toast.error(message, option);
   }
 };
 

+ 9 - 8
packages/app/src/components/Admin/App/ConfirmModal.tsx

@@ -1,15 +1,15 @@
 import React, { FC } from 'react';
+
+import { useTranslation } from 'next-i18next';
 import {
   Modal, ModalHeader, ModalBody, ModalFooter,
 } from 'reactstrap';
-import { useTranslation } from 'next-i18next';
-import { TFunctionResult } from 'i18next';
 
 type ConfirmModalProps = {
   isModalOpen: boolean
-  warningMessage: TFunctionResult
-  supplymentaryMessage: TFunctionResult | null
-  confirmButtonTitle: TFunctionResult
+  warningMessage: string
+  supplymentaryMessage: string | null
+  confirmButtonTitle: string
   onConfirm?: () => Promise<void>
   onCancel?: () => void
 };
@@ -43,13 +43,14 @@ export const ConfirmModal: FC<ConfirmModalProps> = (props: ConfirmModalProps) =>
               <br />
               <br />
               <span className="text-warning">
-                <i className="icon-exclamation icon-fw"></i>
-                {props.supplymentaryMessage}
+                <>
+                  <i className="icon-exclamation icon-fw"></i>
+                  {props.supplymentaryMessage}
+                </>
               </span>
             </>
           )
         }
-
       </ModalBody>
       <ModalFooter>
         <button

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

@@ -29,15 +29,15 @@ const AdminNavigation = (props) => {
       case 'customize':                return <><i className="mr-1 icon-fw icon-wrench"></i>{          t('customize_settings.customize_settings') }</>;
       case 'importer':                 return <><i className="mr-1 icon-fw icon-cloud-upload"></i>{    t('importer_management.import_data') }</>;
       case 'export':                   return <><i className="mr-1 icon-fw icon-cloud-download"></i>{  t('export_management.export_archive_data') }</>;
+      case 'data-transfer':            return <><i className="mr-1 icon-fw icon-plane"></i>{           t('g2g_data_transfer.data_transfer', { ns: 'commons' })}</>;
       case 'notification':             return <><i className="mr-1 icon-fw icon-bell"></i>{            t('external_notification.external_notification')}</>;
       case 'slack-integration':        return <><i className="mr-1 icon-fw icon-shuffle"></i>{         t('slack_integration.slack_integration') }</>;
       case 'slack-integration-legacy': return <><i className="mr-1 icon-fw icon-shuffle"></i>{         t('slack_integration_legacy.slack_integration_legacy')}</>;
       case 'users':                    return <><i className="mr-1 icon-fw icon-user"></i>{            t('user_management.user_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 'audit-log':                return <><i className="mr-1 icon-fw icon-feed"></i>{            t('audit_log_management.audit_log')}</>;
-      case 'data-transfer':            return <><i className="mr-1 icon-fw icon-arrow-right"></i>{     t('g2g_data_transfer.data_transfer', { ns: 'commons' })}</>;
       case 'plugins':                  return <><i className="mr-1 icon-fw icon-puzzle"></i>{          t('plugins.plugins')}</>;
+      case 'search':                   return <><i className="mr-1 icon-fw icon-magnifier"></i>{       t('full_text_search_management.full_text_search_management') }</>;
       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') }</>;
       /* eslint-enable no-multi-spaces, max-len */
@@ -87,6 +87,7 @@ const AdminNavigation = (props) => {
         <MenuLink menu="customize"    isListGroupItems isActive={isActiveMenu('/customize')} />
         <MenuLink menu="importer"     isListGroupItems isActive={isActiveMenu('/importer')} />
         <MenuLink menu="export"       isListGroupItems isActive={isActiveMenu('/export')} />
+        <MenuLink menu="data-transfer" isListGroupItems isActive={isActiveMenu('/data-transfer')} />
         <MenuLink menu="notification" isListGroupItems isActive={isActiveMenu('/notification') || isActiveMenu('/global-notification')} />
         <MenuLink menu="slack-integration" isListGroupItems isActive={isActiveMenu('/slack-integration')} />
         <MenuLink menu="slack-integration-legacy" isListGroupItems isActive={isActiveMenu('/slack-integration-legacy')} />
@@ -94,7 +95,6 @@ const AdminNavigation = (props) => {
         <MenuLink menu="user-groups"  isListGroupItems isActive={isActiveMenu('/user-groups')} />
         <MenuLink menu="audit-log"    isListGroupItems isActive={isActiveMenu('/audit-log')} />
         <MenuLink menu="plugins"      isListGroupItems isActive={isActiveMenu('/plugins')} />
-        <MenuLink menu="data-transfer" isListGroupItems isActive={isActiveMenu('/data-transfer')} />
         <MenuLink menu="search"       isListGroupItems isActive={isActiveMenu('/search')} />
         {growiCloudUri != null && growiAppIdForGrowiCloud != null
           && (

+ 1 - 2
packages/app/src/components/Admin/UserGroup/UserGroupDeleteModal.tsx

@@ -2,7 +2,6 @@ import React, {
   FC, useCallback, useState, useMemo,
 } from 'react';
 
-import { TFunctionResult } from 'i18next';
 import { useTranslation } from 'next-i18next';
 import {
   Modal, ModalHeader, ModalBody, ModalFooter,
@@ -30,7 +29,7 @@ type AvailableOption = {
   actionForPages: string,
   iconClass: string,
   styleClass: string,
-  label: TFunctionResult,
+  label: string,
 };
 
 // actionName master constants

+ 1 - 2
packages/app/src/components/Admin/UserGroup/UserGroupForm.tsx

@@ -1,7 +1,6 @@
 import React, { FC, useCallback, useState } from 'react';
 
 import dateFnsFormat from 'date-fns/format';
-import { TFunctionResult } from 'i18next';
 import { useTranslation } from 'next-i18next';
 
 import { IUserGroupHasId } from '~/interfaces/user';
@@ -9,7 +8,7 @@ import { IUserGroupHasId } from '~/interfaces/user';
 type Props = {
   userGroup: IUserGroupHasId,
   selectableParentUserGroups?: IUserGroupHasId[],
-  submitButtonLabel: TFunctionResult;
+  submitButtonLabel: string;
   onSubmit?: (targetGroup: IUserGroupHasId, userGroupData: Partial<IUserGroupHasId>) => Promise<void> | void
 };
 

+ 1 - 2
packages/app/src/components/Admin/UserGroup/UserGroupModal.tsx

@@ -3,7 +3,6 @@ import React, {
 } from 'react';
 
 import { Ref } from '@growi/core';
-import { TFunctionResult } from 'i18next';
 import { useTranslation } from 'next-i18next';
 import {
   Modal, ModalHeader, ModalBody, ModalFooter,
@@ -13,7 +12,7 @@ import { IUserGroup, IUserGroupHasId } from '~/interfaces/user';
 
 type Props = {
   userGroup?: IUserGroupHasId,
-  buttonLabel?: TFunctionResult,
+  buttonLabel?: string,
   onClickSubmit?: (userGroupData: Partial<IUserGroupHasId>) => Promise<IUserGroupHasId | void>
   isShow?: boolean
   onHide?: () => Promise<void> | void

+ 1 - 2
packages/app/src/components/Admin/UserGroup/UserGroupTable.tsx

@@ -4,12 +4,11 @@ import React, {
 
 import type { IUserGroupHasId, IUserGroupRelation, IUserHasId } from '@growi/core';
 import dateFnsFormat from 'date-fns/format';
-import { TFunctionResult } from 'i18next';
 import { useTranslation } from 'next-i18next';
 
 
 type Props = {
-  headerLabel?: TFunctionResult,
+  headerLabel?: string,
   userGroups: IUserGroupHasId[],
   userGroupRelations: IUserGroupRelation[],
   childUserGroups: IUserGroupHasId[],

+ 1 - 1
packages/app/src/components/Admin/UserGroupDetail/UserGroupUserTable.tsx

@@ -13,7 +13,7 @@ type Props = {
 }
 
 export const UserGroupUserTable = (props: Props): JSX.Element => {
-  const { t } = useTranslation();
+  const { t } = useTranslation('admin');
 
   const {
     userGroupRelations, onClickRemoveUserBtn, onClickPlusBtn,

+ 0 - 11
packages/app/src/components/Layout/BasicLayout.tsx

@@ -4,7 +4,6 @@ import dynamic from 'next/dynamic';
 import { DndProvider } from 'react-dnd';
 import { HTML5Backend } from 'react-dnd-html5-backend';
 
-import { useEditorModeClassName } from '../../client/services/layout';
 import { GrowiNavbar } from '../Navbar/GrowiNavbar';
 import Sidebar from '../Sidebar';
 
@@ -68,13 +67,3 @@ export const BasicLayout = ({ children, className }: Props): JSX.Element => {
     </RawLayout>
   );
 };
-
-export const BasicLayoutWithEditorMode = ({ children }: Props): JSX.Element => {
-  const className = useEditorModeClassName();
-
-  return (
-    <BasicLayout className={className}>
-      {children}
-    </BasicLayout>
-  );
-};

+ 1 - 1
packages/app/src/components/Me/BasicInfoSettings.tsx

@@ -120,7 +120,7 @@ export const BasicInfoSettings = (): JSX.Element => {
                     checked={personalSettingsInfo?.lang === locale}
                     onChange={() => changePersonalSettingsHandler({ lang: locale })}
                   />
-                  <label className="custom-control-label" htmlFor={`radioLang${locale}`}>{fixedT('meta.display_name')}</label>
+                  <label className="custom-control-label" htmlFor={`radioLang${locale}`}>{fixedT('meta.display_name') as string}</label>
                 </div>
               );
             })

+ 0 - 3
packages/app/src/components/Navbar/GrowiNavbar.tsx

@@ -2,10 +2,8 @@ import React, {
   FC, memo, useMemo, useRef,
 } from 'react';
 
-import { isServer } from '@growi/core';
 import { useTranslation } from 'next-i18next';
 import dynamic from 'next/dynamic';
-import Image from 'next/image';
 import Link from 'next/link';
 import { useRipple } from 'react-use-ripple';
 import { UncontrolledTooltip } from 'reactstrap';
@@ -17,7 +15,6 @@ import { usePageCreateModal } from '~/stores/modal';
 import { useCurrentPagePath } from '~/stores/page';
 import { useIsDeviceSmallerThanMd } from '~/stores/ui';
 
-import { HasChildren } from '../../interfaces/common';
 import GrowiLogo from '../Icons/GrowiLogo';
 
 import { GlobalSearchProps } from './GlobalSearch';

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

@@ -1,13 +1,12 @@
 import React from 'react';
 
-import { TFunction } from 'next-i18next';
 import { Disable } from 'react-disable';
 import { UncontrolledTooltip, UncontrolledTooltipProps } from 'reactstrap';
 
 type NotAvailableProps = {
   children: JSX.Element
   isDisabled: boolean
-  title: ReturnType<TFunction>
+  title: string
   classNamePrefix?: string
   placement?: UncontrolledTooltipProps['placement']
 }

+ 1 - 6
packages/app/src/components/Page/PageContents.tsx

@@ -2,7 +2,6 @@ import React, { useEffect } from 'react';
 
 import { pagePathUtils } from '@growi/core';
 import { useTranslation } from 'next-i18next';
-import type { HtmlElementNode } from 'rehype-toc';
 
 import { useUpdateStateAfterSave } from '~/client/services/page-operation';
 import { useDrawioModalLauncherForView } from '~/client/services/side-effects/drawio-modal-launcher-for-view';
@@ -12,7 +11,6 @@ import { useCurrentPathname } from '~/stores/context';
 import { useEditingMarkdown } from '~/stores/editor';
 import { useSWRxCurrentPage } from '~/stores/page';
 import { useViewOptions } from '~/stores/renderer';
-import { useCurrentPageTocNode } from '~/stores/ui';
 import { registerGrowiFacade } from '~/utils/growi-facade';
 import loggerFactory from '~/utils/logger';
 
@@ -30,12 +28,9 @@ export const PageContents = (): JSX.Element => {
 
   const { data: currentPage, mutate: mutateCurrentPage } = useSWRxCurrentPage();
   const { mutate: mutateEditingMarkdown } = useEditingMarkdown();
-  const { mutate: mutateCurrentPageTocNode } = useCurrentPageTocNode();
   const updateStateAfterSave = useUpdateStateAfterSave(currentPage?._id);
 
-  const { data: rendererOptions, mutate: mutateRendererOptions } = useViewOptions((toc: HtmlElementNode) => {
-    mutateCurrentPageTocNode(toc);
-  });
+  const { data: rendererOptions, mutate: mutateRendererOptions } = useViewOptions();
 
   // register to facade
   useEffect(() => {

+ 1 - 3
packages/app/src/components/Page/RevisionLoader.tsx

@@ -84,9 +84,7 @@ export const RevisionLoader = (props: RevisionLoaderProps): JSX.Element => {
   /* ----- before load ----- */
   if (lazy && !isLoaded) {
     return (
-      <Waypoint onPositionChange={onWaypointChange} bottomOffset="-100px">
-        <div></div>
-      </Waypoint>
+      <Waypoint onPositionChange={onWaypointChange} bottomOffset="-100px" />
     );
   }
 

+ 1 - 1
packages/app/src/components/PageAlert/TrashPageAlert.tsx

@@ -114,7 +114,7 @@ export const TrashPageAlert = (): JSX.Element => {
           <br />
           <UserPicture user={deleteUser} />
           <span className="ml-2">
-            Deleted by { deleteUser?.name } at <span data-vrt-blackout-datetime>{deletedAt || pageData?.updatedAt}</span>
+            Deleted by { deleteUser?.name } at <span data-vrt-blackout-datetime>{deletedAt ?? pageData?.updatedAt}</span>
           </span>
         </div>
         <div className="pt-1 d-flex align-items-end align-items-lg-center">

+ 5 - 3
packages/app/src/components/PageEditor/EditorNavbarBottom.tsx

@@ -36,12 +36,14 @@ const EditorNavbarBottom = (): JSX.Element => {
 
   const [slackChannelsStr, setSlackChannelsStr] = useState<string>('');
 
+  // DO NOT dependent on slackChannelsData directly: https://github.com/weseek/growi/pull/7332
+  const slackChannelsDataString = slackChannelsData?.toString();
   useEffect(() => {
-    if (slackChannelsData != null) {
-      setSlackChannelsStr(slackChannelsData.toString());
+    if (editorMode === 'editor') {
+      setSlackChannelsStr(slackChannelsDataString ?? '');
       mutateIsSlackEnabled(false);
     }
-  }, [mutateIsSlackEnabled, slackChannelsData]);
+  }, [editorMode, mutateIsSlackEnabled, slackChannelsDataString]);
 
   const isSlackEnabledToggleHandler = (bool: boolean) => {
     mutateIsSlackEnabled(bool, false);

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

@@ -216,8 +216,7 @@ export const SearchPage = (): JSX.Element => {
         initialSearchConditions={initialSearchConditions}
         onSearchInvoked={searchInvokedHandler}
         allControl={allControl}
-      >
-      </SearchControl>
+      />
     );
   }, [allControl, initialSearchConditions, isSearchServiceReachable, searchInvokedHandler]);
 

+ 2 - 2
packages/app/src/components/SearchPage/SearchControl.tsx

@@ -1,5 +1,5 @@
 import React, {
-  FC, useCallback, useEffect, useState,
+  useCallback, useEffect, useState,
 } from 'react';
 
 import { useTranslation } from 'next-i18next';
@@ -23,7 +23,7 @@ type Props = {
   allControl: React.ReactNode,
 }
 
-const SearchControl: FC <Props> = React.memo((props: Props) => {
+const SearchControl = React.memo((props: Props): JSX.Element => {
 
   const {
     isSearchServiceReachable,

+ 1 - 7
packages/app/src/components/ShareLink/ShareLinkPageContents.tsx

@@ -1,10 +1,8 @@
 import React, { useEffect } from 'react';
 
 import type { IPagePopulatedToShowRevision } from '@growi/core';
-import type { HtmlElementNode } from 'rehype-toc';
 
 import { useViewOptions } from '~/stores/renderer';
-import { useCurrentPageTocNode } from '~/stores/ui';
 import { registerGrowiFacade } from '~/utils/growi-facade';
 import loggerFactory from '~/utils/logger';
 
@@ -21,11 +19,7 @@ export type ShareLinkPageContentsProps = {
 export const ShareLinkPageContents = (props: ShareLinkPageContentsProps): JSX.Element => {
   const { page } = props;
 
-  const { mutate: mutateCurrentPageTocNode } = useCurrentPageTocNode();
-
-  const { data: rendererOptions, mutate: mutateRendererOptions } = useViewOptions((toc: HtmlElementNode) => {
-    mutateCurrentPageTocNode(toc);
-  });
+  const { data: rendererOptions, mutate: mutateRendererOptions } = useViewOptions();
 
   // register to facade
   useEffect(() => {

+ 8 - 2
packages/app/src/components/Sidebar/SidebarNav.tsx

@@ -1,4 +1,6 @@
-import React, { FC, memo, useCallback } from 'react';
+import React, {
+  FC, memo, useCallback, useEffect, useState,
+} from 'react';
 
 import Link from 'next/link';
 
@@ -80,10 +82,14 @@ export const SidebarNav: FC<Props> = (props: Props) => {
 
   const { data: currentUser } = useCurrentUser();
 
-  const isAdmin = currentUser?.admin;
+  const [isAdmin, setAdmin] = useState(false);
 
   const { onItemSelected } = props;
 
+  useEffect(() => {
+    setAdmin(currentUser?.admin === true);
+  }, [currentUser?.admin]);
+
   return (
     <div className={`grw-sidebar-nav ${styles['grw-sidebar-nav']}`} data-vrt-blackout-sidebar-nav>
       <div className="grw-sidebar-nav-primary-container">

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

@@ -15,6 +15,7 @@ export type StickyStretchableScrollerProps = {
   stickyElemSelector: string,
   simplebarRef?: (ref: RefObject<SimpleBar>) => void,
   calcViewHeight?: (scrollElement: HTMLElement) => number,
+  children?: JSX.Element,
 }
 
 /**
@@ -39,7 +40,7 @@ export type StickyStretchableScrollerProps = {
     </StickyStretchableScroller>
   );
  */
-export const StickyStretchableScroller: FC<StickyStretchableScrollerProps> = (props) => {
+export const StickyStretchableScroller = (props: StickyStretchableScrollerProps): JSX.Element => {
 
   const {
     children, stickyElemSelector, calcViewHeight, simplebarRef: setSimplebarRef,

+ 21 - 8
packages/app/src/pages/[[...path]].page.tsx

@@ -1,4 +1,4 @@
-import React, { useEffect } from 'react';
+import React, { ReactNode, useEffect } from 'react';
 
 
 import EventEmitter from 'events';
@@ -7,7 +7,7 @@ import {
   isClient, isIPageInfoForEntity, pagePathUtils, pathUtils,
 } from '@growi/core';
 import type {
-  IDataWithMeta, IPageInfoForEntity, IPagePopulatedToShowRevision, IUser, IUserHasId,
+  IDataWithMeta, IPageInfoForEntity, IPagePopulatedToShowRevision, IUserHasId,
 } from '@growi/core';
 import ExtensibleCustomError from 'extensible-custom-error';
 import {
@@ -19,7 +19,7 @@ import Head from 'next/head';
 import { useRouter } from 'next/router';
 import superjson from 'superjson';
 
-import { useCurrentGrowiLayoutFluidClassName } from '~/client/services/layout';
+import { useCurrentGrowiLayoutFluidClassName, useEditorModeClassName } from '~/client/services/layout';
 import { PageView } from '~/components/Page/PageView';
 import RevisionRenderer from '~/components/Page/RevisionRenderer';
 import { DrawioViewerScript } from '~/components/Script/DrawioViewerScript';
@@ -46,7 +46,7 @@ import { useSetupGlobalSocket, useSetupGlobalSocketForPage } from '~/stores/webs
 import loggerFactory from '~/utils/logger';
 
 import { DescendantsPageListModal } from '../components/DescendantsPageListModal';
-import { BasicLayoutWithEditorMode } from '../components/Layout/BasicLayout';
+import { BasicLayout } from '../components/Layout/BasicLayout';
 import GrowiContextualSubNavigationSubstance from '../components/Navbar/GrowiContextualSubNavigation';
 import type { GrowiSubNavigationSwitcherProps } from '../components/Navbar/GrowiSubNavigationSwitcher';
 import { DisplaySwitcher } from '../components/Page/DisplaySwitcher';
@@ -137,8 +137,6 @@ const PutbackPageModal = (): JSX.Element => {
 };
 
 type Props = CommonProps & {
-  currentUser: IUser,
-
   pageWithMeta: IPageToShowRevisionWithMeta | null,
   // pageUser?: any,
   redirectFrom?: string;
@@ -342,14 +340,29 @@ const Page: NextPageWithLayout<Props> = (props: Props) => {
   );
 };
 
+type LayoutProps = {
+  children?: ReactNode
+  className?: string
+}
+
+const Layout = ({ children }: LayoutProps): JSX.Element => {
+  const className = useEditorModeClassName();
+
+  return (
+    <BasicLayout className={className}>
+      {children}
+    </BasicLayout>
+  );
+};
+
 Page.getLayout = function getLayout(page) {
   return (
     <>
       <DrawioViewerScript />
 
-      <BasicLayoutWithEditorMode>
+      <Layout>
         {page}
-      </BasicLayoutWithEditorMode>
+      </Layout>
       <UnsavedAlertDialog />
       <DescendantsPageListModal />
       <DrawioModal />

+ 2 - 12
packages/app/src/pages/_app.page.tsx

@@ -1,6 +1,5 @@
 import React, { ReactElement, ReactNode, useEffect } from 'react';
 
-import { isServer } from '@growi/core';
 import { NextPage } from 'next';
 import { appWithTranslation } from 'next-i18next';
 import { AppProps } from 'next/app';
@@ -13,8 +12,7 @@ import { useI18nextHMR } from '~/services/i18next-hmr';
 import {
   useAppTitle, useConfidential, useGrowiVersion, useSiteUrl, useIsDefaultLogo, useForcedColorScheme,
 } from '~/stores/context';
-import { SWRConfigValue, swrGlobalConfiguration } from '~/utils/swr-utils';
-
+import { swrGlobalConfiguration } from '~/utils/swr-utils';
 
 import { CommonProps } from './utils/commons';
 import { registerTransformerForObjectId } from './utils/objectid-transformer';
@@ -26,14 +24,6 @@ import '~/styles/theme/_apply-colors.scss';
 
 const isDev = process.env.NODE_ENV === 'development';
 
-const swrConfig: SWRConfigValue = {
-  ...swrGlobalConfiguration,
-  // set the request scoped cache provider in server
-  provider: isServer()
-    ? cache => new Map(cache)
-    : undefined,
-};
-
 
 // eslint-disable-next-line @typescript-eslint/ban-types
 export type NextPageWithLayout<P = {}, IP = P> = NextPage<P, IP> & {
@@ -72,7 +62,7 @@ function GrowiApp({ Component, pageProps }: GrowiAppProps): JSX.Element {
   const getLayout = Component.getLayout ?? (page => page);
 
   return (
-    <SWRConfig value={swrConfig}>
+    <SWRConfig value={swrGlobalConfiguration}>
       {getLayout(<Component {...pageProps} />)}
     </SWRConfig>
   );

+ 5 - 5
packages/app/src/pages/trash.page.tsx

@@ -1,7 +1,8 @@
 import React from 'react';
 
 import type { IUser, IUserHasId } from '@growi/core';
-import { NextPage, GetServerSideProps, GetServerSidePropsContext } from 'next';
+import { GetServerSideProps, GetServerSidePropsContext } from 'next';
+import { useTranslation } from 'next-i18next';
 import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
 import dynamic from 'next/dynamic';
 import Head from 'next/head';
@@ -17,7 +18,7 @@ import {
   useCurrentProductNavWidth, useCurrentSidebarContents, useDrawerMode, usePreferDrawerModeByUser, usePreferDrawerModeOnEditByUser, useSidebarCollapsed,
 } from '~/stores/ui';
 
-import { BasicLayoutWithEditorMode } from '../components/Layout/BasicLayout';
+import { BasicLayout } from '../components/Layout/BasicLayout';
 import {
   useCurrentUser, useCurrentPageId, useCurrentPathname,
   useIsSearchServiceConfigured, useIsSearchServiceReachable,
@@ -28,7 +29,6 @@ import { NextPageWithLayout } from './_app.page';
 import {
   CommonProps, getServerSideCommonProps, getNextI18NextConfig, generateCustomTitleForPage,
 } from './utils/commons';
-import { useTranslation } from 'next-i18next';
 
 const TrashPageList = dynamic(() => import('~/components/TrashPageList').then(mod => mod.TrashPageList), { ssr: false });
 const EmptyTrashModal = dynamic(() => import('~/components/EmptyTrashModal'), { ssr: false });
@@ -109,9 +109,9 @@ const TrashPage: NextPageWithLayout<CommonProps> = (props: Props) => {
 TrashPage.getLayout = function getLayout(page) {
   return (
     <>
-      <BasicLayoutWithEditorMode>
+      <BasicLayout>
         {page}
-      </BasicLayoutWithEditorMode>
+      </BasicLayout>
       <EmptyTrashModal />
       <PutbackPageModal />
     </>

+ 1 - 1
packages/app/src/pages/utils/commons.ts

@@ -22,7 +22,7 @@ export type CommonProps = {
   isMaintenanceMode: boolean,
   redirectDestination: string | null,
   isDefaultLogo: boolean,
-  currentUser?: IUser,
+  currentUser?: IUserHasId,
   forcedColorScheme?: ColorScheme,
 } & Partial<SSRConfig>;
 

+ 1 - 1
packages/app/src/stores/activity.ts

@@ -12,7 +12,7 @@ export const useSWRxActivity = (limit?: number, offset?: number, searchFilter?:
   const stringifiedSearchFilter = JSON.stringify(searchFilter);
   return useSWRImmutable(
     auditLogEnabled ? ['/activity', limit, offset, stringifiedSearchFilter] : null,
-    (endpoint, limit, offset, stringifiedSearchFilter) => apiv3Get(endpoint, { limit, offset, searchFilter: stringifiedSearchFilter })
+    ([endpoint, limit, offset, stringifiedSearchFilter]) => apiv3Get(endpoint, { limit, offset, searchFilter: stringifiedSearchFilter })
       .then(result => result.data.serializedPaginationResult),
   );
 };

+ 1 - 1
packages/app/src/stores/attachment.tsx

@@ -23,7 +23,7 @@ type IDataAttachmentList = {
 export const useSWRxAttachments = (pageId?: Nullable<string>, pageNumber?: number): SWRResponseWithUtils<Util, IDataAttachmentList, Error> => {
   const shouldFetch = pageId != null && pageNumber != null;
 
-  const fetcher = useCallback(async(endpoint, pageId, pageNumber) => {
+  const fetcher = useCallback(async([endpoint, pageId, pageNumber]) => {
     const res = await apiv3Get<IResAttachmentList>(endpoint, { pageId, pageNumber });
     const resAttachmentList = res.data;
     const { paginateResult } = resAttachmentList;

+ 1 - 1
packages/app/src/stores/comment.tsx

@@ -20,7 +20,7 @@ export const useSWRxPageComment = (pageId: Nullable<string>): SWRResponse<IComme
 
   const swrResponse = useSWR(
     shouldFetch ? ['/comments.get', pageId] : null,
-    (endpoint, pageId) => apiGet(endpoint, { page_id: pageId }).then((response:IResponseComment) => response.comments),
+    ([endpoint, pageId]) => apiGet(endpoint, { page_id: pageId }).then((response:IResponseComment) => response.comments),
   );
 
   const update = async(comment: string, revisionId: string, commentId: string) => {

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

@@ -1,4 +1,4 @@
-import type { ColorScheme, IUser } from '@growi/core';
+import type { ColorScheme, IUser, IUserHasId } from '@growi/core';
 import { Key, SWRResponse } from 'swr';
 import useSWRImmutable from 'swr/immutable';
 
@@ -36,8 +36,8 @@ export const useConfidential = (initialData?: string): SWRResponse<string, Error
   return useContextSWR('confidential', initialData);
 };
 
-export const useCurrentUser = (initialData?: Nullable<IUser>): SWRResponse<Nullable<IUser>, Error> => {
-  return useContextSWR<Nullable<IUser>, Error>('currentUser', initialData);
+export const useCurrentUser = (initialData?: Nullable<IUserHasId>): SWRResponse<Nullable<IUserHasId>, Error> => {
+  return useContextSWR('currentUser', initialData);
 };
 
 export const useCurrentPathname = (initialData?: string): SWRResponse<string, Error> => {
@@ -230,12 +230,12 @@ export const useIsContainerFluid = (initialData?: boolean): SWRResponse<boolean,
  *********************************************************** */
 
 export const useIsGuestUser = (): SWRResponse<boolean, Error> => {
-  const { data: currentUser } = useCurrentUser();
+  const { data: currentUser, isLoading } = useCurrentUser();
 
   return useSWRImmutable(
-    ['isGuestUser', currentUser],
-    (key: Key, currentUser: IUser) => currentUser == null,
-    { fallbackData: currentUser == null },
+    isLoading ? null : ['isGuestUser', currentUser?._id],
+    ([, currentUserId]) => currentUserId == null,
+    { fallbackData: currentUser?._id == null },
   );
 };
 
@@ -247,7 +247,7 @@ export const useIsEditable = (): SWRResponse<boolean, Error> => {
 
   return useSWRImmutable(
     ['isEditable', isGuestUser, isForbidden, isNotCreatable, isIdenticalPath],
-    (key: Key, isGuestUser: boolean, isForbidden: boolean, isNotCreatable: boolean, isIdenticalPath: boolean) => {
+    ([, isGuestUser, isForbidden, isNotCreatable, isIdenticalPath]) => {
       return (!isForbidden && !isIdenticalPath && !isNotCreatable && !isGuestUser);
     },
   );

+ 3 - 3
packages/app/src/stores/editor.tsx

@@ -39,9 +39,9 @@ export const useEditorSettings = (): SWRResponseWithUtils<EditorSettingsOperatio
   const { data: currentUser } = useCurrentUser();
   const { data: isGuestUser } = useIsGuestUser();
 
-  const swrResult = useSWRImmutable<IEditorSettings>(
+  const swrResult = useSWRImmutable(
     isGuestUser ? null : ['/personal-setting/editor-settings', currentUser?.username],
-    endpoint => apiv3Get(endpoint).then(result => result.data),
+    ([endpoint]) => apiv3Get(endpoint).then(result => result.data),
     {
       // use: [localStorageMiddleware], // store to localStorage for initialization fastly
       // fallbackData: undefined,
@@ -96,7 +96,7 @@ export const useSWRxSlackChannels = (currentPagePath: Nullable<string>): SWRResp
   const shouldFetch: boolean = currentPagePath != null;
   return useSWRImmutable(
     shouldFetch ? ['/pages.updatePost', currentPagePath] : null,
-    (endpoint, path) => apiGet(endpoint, { path }).then((response: SlackChannels) => response.updatePost),
+    ([endpoint, path]) => apiGet(endpoint, { path }).then((response: SlackChannels) => response.updatePost),
     { fallbackData: [''] },
   );
 };

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

@@ -19,7 +19,7 @@ export const useSWRxInAppNotifications = <Data, Error>(
 ): SWRResponse<PaginateResult<IInAppNotification>, Error> => {
   return useSWR(
     ['/in-app-notification/list', limit, offset, status],
-    endpoint => apiv3Get(endpoint, { limit, offset, status }).then((response) => {
+    ([endpoint]) => apiv3Get(endpoint, { limit, offset, status }).then((response) => {
       const inAppNotificationPaginateResult = response.data as inAppNotificationPaginateResult;
       inAppNotificationPaginateResult.docs.forEach((doc) => {
         try {
@@ -39,7 +39,7 @@ export const useSWRxInAppNotifications = <Data, Error>(
 export const useSWRxInAppNotificationStatus = <Data, Error>(
 ): SWRResponse<number, Error> => {
   return useSWR(
-    ['/in-app-notification/status'],
+    '/in-app-notification/status',
     endpoint => apiv3Get(endpoint).then(response => response.data.count),
   );
 };

+ 1 - 1
packages/app/src/stores/maintenanceMode.tsx

@@ -27,5 +27,5 @@ export const useIsMaintenanceMode = (initialData?: boolean): SWRResponseWithUtil
     },
   };
 
-  return withUtils(swrResult, utils);
+  return withUtils<maintenanceModeUtils, boolean>(swrResult, utils);
 };

+ 10 - 10
packages/app/src/stores/page-listing.tsx

@@ -18,9 +18,9 @@ import { ITermNumberManagerUtil, useTermNumberManager } from './use-static-swr';
 
 export const useSWRxPagesByPath = (path?: Nullable<string>): SWRResponse<IPageHasId[], Error> => {
   const findAll = true;
-  return useSWR<IPageHasId[], Error>(
+  return useSWR(
     path != null ? ['/page', path, findAll] : null,
-    (endpoint, path, findAll) => apiv3Get(endpoint, { path, findAll }).then(result => result.data.pages),
+    ([endpoint, path, findAll]) => apiv3Get(endpoint, { path, findAll }).then(result => result.data.pages),
   );
 };
 
@@ -31,12 +31,12 @@ export const useSWRxRecentlyUpdated = (): SWRResponse<(IPageHasId)[], Error> =>
   );
 };
 export const useSWRInifinitexRecentlyUpdated = () : SWRInfiniteResponse<(IPageHasId)[], Error> => {
-  const getKey = (page: number) => {
+  const getKey = (page: number): string => {
     return `/pages/recent?offset=${page + 1}`;
   };
   return useSWRInfinite(
     getKey,
-    (endpoint: string) => apiv3Get<{ pages:(IPageHasId)[] }>(endpoint).then(response => response.data?.pages),
+    endpoint => apiv3Get<{ pages:(IPageHasId)[] }>(endpoint).then(response => response.data?.pages),
     {
       revalidateFirstPage: false,
       revalidateAll: false,
@@ -48,7 +48,7 @@ export const useSWRxPageList = (
     path: string | null, pageNumber?: number, termNumber?: number, limit?: number,
 ): SWRResponse<IPagingResult<IPageHasId>, Error> => {
 
-  let key;
+  let key: [string, number|undefined] | null;
   // if path not exist then the key is null
   if (path == null) {
     key = null;
@@ -62,7 +62,7 @@ export const useSWRxPageList = (
 
   return useSWR(
     key,
-    (endpoint: string) => apiv3Get<{pages: IPageHasId[], totalCount: number, limit: number}>(endpoint).then((response) => {
+    ([endpoint]) => apiv3Get<{pages: IPageHasId[], totalCount: number, limit: number}>(endpoint).then((response) => {
       return {
         items: response.data.pages,
         totalCount: response.data.totalCount,
@@ -105,9 +105,9 @@ export const useSWRxPageInfoForList = (
 
   const shouldFetch = (pageIds != null && pageIds.length > 0) || path != null;
 
-  const swrResult = useSWRImmutable<Record<string, IPageInfoForListing>>(
+  const swrResult = useSWRImmutable(
     shouldFetch ? ['/page-listing/info', pageIds, path, attachBookmarkCount, attachShortBody] : null,
-    (endpoint, pageIds, path, attachBookmarkCount, attachShortBody) => {
+    ([endpoint, pageIds, path, attachBookmarkCount, attachShortBody]) => {
       return apiv3Get(endpoint, {
         pageIds, path, attachBookmarkCount, attachShortBody,
       }).then(response => response.data);
@@ -159,7 +159,7 @@ export const useSWRxPageAncestorsChildren = (
 
   return useSWRImmutable(
     path ? [`/page-listing/ancestors-children?path=${path}`, termNumber] : null,
-    endpoint => apiv3Get(endpoint).then((response) => {
+    ([endpoint]) => apiv3Get(endpoint).then((response) => {
       return {
         ancestorsChildren: response.data.ancestorsChildren,
       };
@@ -177,7 +177,7 @@ export const useSWRxPageChildren = (
 
   return useSWR(
     id ? [`/page-listing/children?id=${id}`, termNumber] : null,
-    endpoint => apiv3Get(endpoint).then((response) => {
+    ([endpoint]) => apiv3Get(endpoint).then((response) => {
       return {
         children: response.data.children,
       };

+ 27 - 24
packages/app/src/stores/page.tsx

@@ -31,22 +31,25 @@ export const useSWRxPage = (
     initialData?: IPagePopulatedToShowRevision|null,
     config?: SWRConfiguration,
 ): SWRResponse<IPagePopulatedToShowRevision|null, Error> => {
-  const swrResponse = useSWRImmutable<IPagePopulatedToShowRevision|null, Error>(
+
+  const swrResponse = useSWRImmutable(
     pageId != null ? ['/page', pageId, shareLinkId, revisionId] : null,
     // TODO: upgrade SWR to v2 and use useSWRMutation
     //        in order to avoid complicated fetcher settings
     Object.assign({
-      fetcher: (endpoint, pageId, shareLinkId, revisionId) => apiv3Get<{ page: IPagePopulatedToShowRevision }>(endpoint, { pageId, shareLinkId, revisionId })
-        .then(result => result.data.page)
-        .catch((errs) => {
-          if (!Array.isArray(errs)) { throw Error('error is not array') }
-          const statusCode = errs[0].status;
-          if (statusCode === 403 || statusCode === 404) {
-            // for NotFoundPage
-            return null;
-          }
-          throw Error('failed to get page');
-        }),
+      fetcher: ([endpoint, pageId, shareLinkId, revisionId]: [string, string, string|undefined, string|undefined]) => {
+        return apiv3Get<{ page: IPagePopulatedToShowRevision }>(endpoint, { pageId, shareLinkId, revisionId })
+          .then(result => result.data.page)
+          .catch((errs) => {
+            if (!Array.isArray(errs)) { throw Error('error is not array') }
+            const statusCode = errs[0].status;
+            if (statusCode === 403 || statusCode === 404) {
+              // for NotFoundPage
+              return null;
+            }
+            throw Error('failed to get page');
+          });
+      },
     }, config ?? {}),
   );
 
@@ -61,9 +64,9 @@ export const useSWRxPage = (
 };
 
 export const useSWRxPageByPath = (path?: string): SWRResponse<IPagePopulatedToShowRevision, Error> => {
-  return useSWR<IPagePopulatedToShowRevision, Error>(
+  return useSWR(
     path != null ? ['/page', path] : null,
-    (endpoint, path) => apiv3Get<{ page: IPagePopulatedToShowRevision }>(endpoint, { path }).then(result => result.data.page),
+    ([endpoint, path]) => apiv3Get<{ page: IPagePopulatedToShowRevision }>(endpoint, { path }).then(result => result.data.page),
   );
 };
 
@@ -99,9 +102,9 @@ export const useSWRxTagsInfo = (pageId: Nullable<string>): SWRResponse<IPageTags
 
   const endpoint = `/pages.getPageTag?pageId=${pageId}`;
 
-  return useSWRImmutable<IPageTagsInfo | undefined, Error>(
+  return useSWRImmutable(
     shareLinkId == null && pageId != null ? [endpoint, pageId] : null,
-    (endpoint, pageId) => apiGet<IPageTagsInfo>(endpoint, { pageId }).then(result => result),
+    ([endpoint, pageId]) => apiGet<IPageTagsInfo>(endpoint, { pageId }).then(result => result),
   );
 };
 
@@ -120,9 +123,9 @@ export const useSWRxPageInfo = (
   // assign null if shareLinkId is undefined in order to identify SWR key only by pageId
   const fixedShareLinkId = shareLinkId ?? null;
 
-  const swrResult = useSWRImmutable<IPageInfo | IPageInfoForOperation, Error>(
+  const swrResult = useSWRImmutable(
     pageId != null && termNumber != null ? ['/page/info', pageId, fixedShareLinkId, termNumber] : null,
-    (endpoint, pageId, shareLinkId) => apiv3Get(endpoint, { pageId, shareLinkId }).then(response => response.data),
+    ([endpoint, pageId, shareLinkId]) => apiv3Get(endpoint, { pageId, shareLinkId }).then(response => response.data),
     { fallbackData: initialData },
   );
 
@@ -135,9 +138,9 @@ export const useSWRxPageRevisions = (
     pageId: string | null | undefined,
 ): SWRResponse<IRevisionsForPagination, Error> => {
 
-  return useSWRImmutable<IRevisionsForPagination, Error>(
+  return useSWRImmutable(
     ['/revisions/list', pageId, page, limit],
-    (endpoint, pageId, page, limit) => {
+    ([endpoint, pageId, page, limit]) => {
       return apiv3Get(endpoint, { pageId, page, limit }).then((response) => {
         const revisions = {
           revisions: response.data.docs,
@@ -162,7 +165,7 @@ export const useSWRxIsGrantNormalized = (
 
   return useSWRImmutable(
     key,
-    (endpoint, pageId) => apiv3Get(endpoint, { pageId }).then(response => response.data),
+    ([endpoint, pageId]) => apiv3Get(endpoint, { pageId }).then(response => response.data),
   );
 };
 
@@ -172,7 +175,7 @@ export const useSWRxApplicableGrant = (
 
   return useSWRImmutable(
     pageId != null ? ['/page/applicable-grant', pageId] : null,
-    (endpoint, pageId) => apiv3Get(endpoint, { pageId }).then(response => response.data),
+    ([endpoint, pageId]) => apiv3Get(endpoint, { pageId }).then(response => response.data),
   );
 };
 
@@ -187,7 +190,7 @@ export const useCurrentPagePath = (): SWRResponse<string | undefined, Error> =>
 
   return useSWRImmutable(
     ['currentPagePath', currentPage?.path, currentPathname],
-    (key: Key, pagePath: string|undefined, pathname: string|undefined) => {
+    ([, , pathname]) => {
       if (currentPage?.path != null) {
         return currentPage.path;
       }
@@ -206,7 +209,7 @@ export const useIsTrashPage = (): SWRResponse<boolean, Error> => {
 
   return useSWRImmutable(
     pagePath == null ? null : ['isTrashPage', pagePath],
-    (key: Key, pagePath: string) => pagePathUtils.isTrashPage(pagePath),
+    ([, pagePath]) => pagePathUtils.isTrashPage(pagePath),
     // TODO: set fallbackData
     // { fallbackData:  }
   );

+ 49 - 44
packages/app/src/stores/renderer.tsx

@@ -1,3 +1,7 @@
+import {
+  useCallback, useEffect, useRef,
+} from 'react';
+
 import { HtmlElementNode } from 'rehype-toc';
 import useSWR, { SWRResponse } from 'swr';
 import useSWRImmutable from 'swr/immutable';
@@ -17,19 +21,30 @@ import { useCurrentPagePath } from './page';
 import { useCurrentPageTocNode } from './ui';
 
 
-export const useViewOptions = (storeTocNodeHandler: (toc: HtmlElementNode) => void): SWRResponse<RendererOptions, Error> => {
+export const useViewOptions = (): SWRResponse<RendererOptions, Error> => {
   const { data: currentPagePath } = useCurrentPagePath();
   const { data: rendererConfig } = useRendererConfig();
+  const { mutate: mutateCurrentPageTocNode } = useCurrentPageTocNode();
 
-  const isAllDataValid = currentPagePath != null && rendererConfig != null;
+  const tocRef = useRef<HtmlElementNode|undefined>();
 
-  const key = isAllDataValid
-    ? ['viewOptions', currentPagePath, rendererConfig]
-    : null;
+  const storeTocNodeHandler = useCallback((toc: HtmlElementNode) => {
+    tocRef.current = toc;
+  }, []);
+
+  useEffect(() => {
+    mutateCurrentPageTocNode(tocRef.current);
+  // using useRef not to re-render
+  // eslint-disable-next-line react-hooks/exhaustive-deps
+  }, [mutateCurrentPageTocNode, tocRef.current]);
+
+  const isAllDataValid = currentPagePath != null && rendererConfig != null;
 
-  return useSWR<RendererOptions, Error>(
-    key,
-    (rendererId, currentPagePath, rendererConfig) => {
+  return useSWR(
+    isAllDataValid
+      ? ['viewOptions', currentPagePath, rendererConfig]
+      : null,
+    ([, currentPagePath, rendererConfig]) => {
       // determine options generator
       const optionsGenerator = getGrowiFacade().markdownRenderer?.optionsGenerators?.customGenerateViewOptions ?? generateViewOptions;
       return optionsGenerator(currentPagePath, rendererConfig, storeTocNodeHandler);
@@ -47,13 +62,11 @@ export const useTocOptions = (): SWRResponse<RendererOptions, Error> => {
 
   const isAllDataValid = currentPagePath != null && rendererConfig != null && tocNode != null;
 
-  const key = isAllDataValid
-    ? ['tocOptions', currentPagePath, tocNode, rendererConfig]
-    : null;
-
-  return useSWRImmutable<RendererOptions, Error>(
-    key,
-    (rendererId, path, tocNode, rendererConfig) => generateTocOptions(rendererConfig, tocNode),
+  return useSWRImmutable(
+    isAllDataValid
+      ? ['tocOptions', currentPagePath, tocNode, rendererConfig]
+      : null,
+    ([, , tocNode, rendererConfig]) => generateTocOptions(rendererConfig, tocNode),
     {
       fallbackData: isAllDataValid ? generateTocOptions(rendererConfig, tocNode) : undefined,
     },
@@ -66,16 +79,14 @@ export const usePreviewOptions = (): SWRResponse<RendererOptions, Error> => {
 
   const isAllDataValid = currentPagePath != null && rendererConfig != null;
 
-  const key = isAllDataValid
-    ? ['previewOptions', rendererConfig, currentPagePath]
-    : null;
-
-  return useSWR<RendererOptions, Error>(
-    key,
-    (rendererId, rendererConfig, pagePath, highlightKeywords) => {
+  return useSWR(
+    isAllDataValid
+      ? ['previewOptions', rendererConfig, currentPagePath]
+      : null,
+    ([, rendererConfig, pagePath]) => {
       // determine options generator
       const optionsGenerator = getGrowiFacade().markdownRenderer?.optionsGenerators?.customGeneratePreviewOptions ?? generatePreviewOptions;
-      return optionsGenerator(rendererConfig, pagePath, highlightKeywords);
+      return optionsGenerator(rendererConfig, pagePath);
     },
     {
       fallbackData: isAllDataValid ? generatePreviewOptions(rendererConfig, currentPagePath) : undefined,
@@ -89,13 +100,11 @@ export const useCommentForCurrentPageOptions = (): SWRResponse<RendererOptions,
 
   const isAllDataValid = currentPagePath != null && rendererConfig != null;
 
-  const key = isAllDataValid
-    ? ['commentPreviewOptions', rendererConfig, currentPagePath]
-    : null;
-
-  return useSWRImmutable<RendererOptions, Error>(
-    key,
-    (rendererId, rendererConfig, currentPagePath) => generateSimpleViewOptions(
+  return useSWRImmutable(
+    isAllDataValid
+      ? ['commentPreviewOptions', rendererConfig, currentPagePath]
+      : null,
+    ([, rendererConfig, currentPagePath]) => generateSimpleViewOptions(
       rendererConfig,
       currentPagePath,
       undefined,
@@ -118,13 +127,11 @@ export const useSelectedPagePreviewOptions = (pagePath: string, highlightKeyword
 
   const isAllDataValid = rendererConfig != null;
 
-  const key = isAllDataValid
-    ? ['selectedPagePreviewOptions', rendererConfig, pagePath, highlightKeywords]
-    : null;
-
-  return useSWRImmutable<RendererOptions, Error>(
-    key,
-    (rendererId, rendererConfig, pagePath, highlightKeywords) => generateSimpleViewOptions(rendererConfig, pagePath, highlightKeywords),
+  return useSWRImmutable(
+    isAllDataValid
+      ? ['selectedPagePreviewOptions', rendererConfig, pagePath, highlightKeywords]
+      : null,
+    ([, rendererConfig, pagePath, highlightKeywords]) => generateSimpleViewOptions(rendererConfig, pagePath, highlightKeywords),
     {
       fallbackData: isAllDataValid ? generateSimpleViewOptions(rendererConfig, pagePath, highlightKeywords) : undefined,
     },
@@ -139,13 +146,11 @@ export const useCustomSidebarOptions = (): SWRResponse<RendererOptions, Error> =
 
   const isAllDataValid = rendererConfig != null;
 
-  const key = isAllDataValid
-    ? ['customSidebarOptions', rendererConfig]
-    : null;
-
-  return useSWRImmutable<RendererOptions, Error>(
-    key,
-    (rendererId, rendererConfig, pagePath, highlightKeywords) => generateSimpleViewOptions(rendererConfig, pagePath, highlightKeywords),
+  return useSWRImmutable(
+    isAllDataValid
+      ? ['customSidebarOptions', rendererConfig]
+      : null,
+    ([, rendererConfig]) => generateSimpleViewOptions(rendererConfig, '/'),
     {
       fallbackData: isAllDataValid ? generateSimpleViewOptions(rendererConfig, '/') : undefined,
     },

+ 1 - 2
packages/app/src/stores/search.tsx

@@ -2,7 +2,6 @@ import { SWRResponse } from 'swr';
 import useSWRImmutable from 'swr/immutable';
 
 import { apiGet } from '~/client/util/apiv1-client';
-
 import { IFormattedSearchResult, SORT_AXIS, SORT_ORDER } from '~/interfaces/search';
 
 import { ITermNumberManagerUtil, useTermNumberManager } from './use-static-swr';
@@ -73,7 +72,7 @@ export const useSWRxSearch = (
 
   const swrResult = useSWRImmutable(
     isKeywordValid ? ['/search', keyword, fixedConfigurations, termNumber] : null,
-    (endpoint, keyword, fixedConfigurations) => {
+    ([endpoint, , fixedConfigurations]) => {
       const {
         limit, offset, sort, order,
       } = fixedConfigurations;

+ 4 - 1
packages/app/src/stores/share-link.tsx

@@ -10,5 +10,8 @@ const fetchShareLinks = async(endpoint, pageId) => {
 };
 
 export const useSWRxSharelink = (currentPageId: Nullable<string>): SWRResponse<IResShareLinkList['shareLinksResult'], Error> => {
-  return useSWR(currentPageId == null ? null : ['/share-links/', currentPageId], (endpoint => fetchShareLinks(endpoint, currentPageId)));
+  return useSWR(
+    currentPageId == null ? null : ['/share-links/', currentPageId],
+    ([endpoint]) => fetchShareLinks(endpoint, currentPageId),
+  );
 };

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

@@ -7,13 +7,13 @@ import { IResTagsListApiv1, IResTagsSearchApiv1 } from '~/interfaces/tag';
 export const useSWRxTagsList = (limit?: number, offset?: number): SWRResponse<IResTagsListApiv1, Error> => {
   return useSWRImmutable(
     ['/tags.list', limit, offset],
-    (endpoint, limit, offset) => apiGet(endpoint, { limit, offset }).then((result: IResTagsListApiv1) => result),
+    ([endpoint, limit, offset]) => apiGet(endpoint, { limit, offset }).then((result: IResTagsListApiv1) => result),
   );
 };
 
 export const useSWRxTagsSearch = (query: string): SWRResponse<IResTagsSearchApiv1, Error> => {
   return useSWRImmutable(
     ['/tags.search', query],
-    (endpoint, query) => apiGet(endpoint, { q: query }).then((result: IResTagsSearchApiv1) => result),
+    ([endpoint, query]) => apiGet(endpoint, { q: query }).then((result: IResTagsSearchApiv1) => result),
   );
 };

+ 1 - 1
packages/app/src/stores/template.tsx

@@ -48,7 +48,7 @@ yyyy/mm/dd (予定、時間は追って連絡)`,
 ];
 
 export const useTemplates = (): SWRResponse<ITemplate[], Error> => {
-  return useSWR<ITemplate[], Error>(
+  return useSWR(
     'templates',
     () => [
       ...presetTemplates,

+ 27 - 21
packages/app/src/stores/ui.tsx

@@ -8,7 +8,7 @@ import { Breakpoint, addBreakpointListener, cleanupBreakpointListener } from '@g
 import { HtmlElementNode } from 'rehype-toc';
 import type SimpleBar from 'simplebar-react';
 import {
-  useSWRConfig, SWRResponse, Key, Fetcher,
+  useSWRConfig, SWRResponse, Key,
 } from 'swr';
 import useSWRImmutable from 'swr/immutable';
 
@@ -155,7 +155,7 @@ export const useEditorMode = (): SWRResponseWithUtils<EditorModeUtils, EditorMod
   const isEditable = !isLoading && _isEditable;
   const initialData = isEditable ? editorModeByHash : EditorMode.View;
 
-  const swrResponse = useSWRImmutable<EditorMode>(
+  const swrResponse = useSWRImmutable(
     isLoading ? null : ['editorMode', isEditable],
     null,
     { fallbackData: initialData },
@@ -199,8 +199,8 @@ export const useIsDeviceSmallerThanMd = (): SWRResponse<boolean, Error> => {
       const mql = addBreakpointListener(Breakpoint.MD, mdOrAvobeHandler);
 
       // initialize
-      if (cache.get(key) == null) {
-        mutate(key, !mql.matches);
+      if (cache.get(key)?.data == null) {
+        cache.set(key, { ...cache.get(key), data: !mql.matches });
       }
 
       return () => {
@@ -227,8 +227,8 @@ export const useIsDeviceSmallerThanLg = (): SWRResponse<boolean, Error> => {
       const mql = addBreakpointListener(Breakpoint.LG, lgOrAvobeHandler);
 
       // initialize
-      if (cache.get(key) == null) {
-        mutate(key, !mql.matches);
+      if (cache.get(key)?.data == null) {
+        cache.set(key, { ...cache.get(key), data: !mql.matches });
       }
 
       return () => {
@@ -260,7 +260,7 @@ export const usePreferDrawerModeByUser = (initialData?: boolean): SWRResponseWit
     },
   };
 
-  return withUtils(swrResponse, utils);
+  return withUtils<PreferDrawerModeByUserUtils>(swrResponse, utils);
 
 };
 
@@ -286,29 +286,36 @@ export const useDrawerMode = (): SWRResponse<boolean, Error> => {
   const { data: editorMode } = useEditorMode();
   const { data: isDeviceSmallerThanMd } = useIsDeviceSmallerThanMd();
 
-  const condition = editorMode != null || preferDrawerModeByUser != null || preferDrawerModeOnEditByUser != null || isDeviceSmallerThanMd != null;
+  const condition = editorMode != null && preferDrawerModeByUser != null && preferDrawerModeOnEditByUser != null && isDeviceSmallerThanMd != null;
 
-  const calcDrawerMode: Fetcher<boolean> = (
-      key: Key, editorMode: EditorMode, preferDrawerModeByUser: boolean, preferDrawerModeOnEditByUser: boolean, isDeviceSmallerThanMd: boolean,
+  const calcDrawerMode = (
+      endpoint: string,
+      editorMode: EditorMode,
+      preferDrawerModeByUser: boolean,
+      preferDrawerModeOnEditByUser: boolean,
+      isDeviceSmallerThanMd: boolean,
   ): boolean => {
-
     // get preference on view or edit
     const preferDrawerMode = editorMode !== EditorMode.View ? preferDrawerModeOnEditByUser : preferDrawerModeByUser;
 
-    return isDeviceSmallerThanMd || preferDrawerMode;
+    return isDeviceSmallerThanMd ?? preferDrawerMode ?? false;
   };
 
   const isViewModeWithPreferDrawerMode = editorMode === EditorMode.View && preferDrawerModeByUser;
   const isEditModeWithPreferDrawerMode = editorMode === EditorMode.Editor && preferDrawerModeOnEditByUser;
-  const useFallbackData = isViewModeWithPreferDrawerMode || isEditModeWithPreferDrawerMode;
-  const fallbackOption = useFallbackData
-    ? { fallbackData: true }
-    : { fallback: calcDrawerMode };
+  const isDrawerModeFixed = isViewModeWithPreferDrawerMode || isEditModeWithPreferDrawerMode;
 
   return useSWRImmutable(
     condition ? ['isDrawerMode', editorMode, preferDrawerModeByUser, preferDrawerModeOnEditByUser, isDeviceSmallerThanMd] : null,
-    calcDrawerMode,
-    fallbackOption,
+    // calcDrawerMode,
+    key => calcDrawerMode(...key),
+    condition
+      ? {
+        fallbackData: isDrawerModeFixed
+          ? true
+          : calcDrawerMode('isDrawerMode', editorMode, preferDrawerModeByUser, preferDrawerModeOnEditByUser, isDeviceSmallerThanMd),
+      }
+      : undefined,
   );
 };
 
@@ -321,7 +328,7 @@ type SidebarConfigOption = {
 }
 
 export const useSWRxSidebarConfig = (): SWRResponse<ISidebarConfig, Error> & SidebarConfigOption => {
-  const swrResponse = useSWRImmutable<ISidebarConfig>(
+  const swrResponse = useSWRImmutable(
     '/customize-setting/sidebar',
     endpoint => apiv3Get(endpoint).then(result => result.data),
   );
@@ -439,8 +446,7 @@ export const useIsAbleToShowPageManagement = (): SWRResponse<boolean, Error> =>
 
   return useSWRImmutable(
     includesUndefined ? null : [key, pageId, isPageExist, isEmptyPage, isTrashPage, isSharedUser],
-    // eslint-disable-next-line max-len
-    (key: string, pageId: string, isPageExist: boolean, isTrashPage: boolean, isSharedUser: boolean) => (isPageExist && !isTrashPage && !isSharedUser) || isEmptyPage,
+    ([, , isPageExist, isEmptyPage, isTrashPage, isSharedUser]) => (isPageExist && !isTrashPage && !isSharedUser) || isEmptyPage,
   );
 };
 

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

@@ -23,12 +23,12 @@ export function useContextSWR<Data, Error>(
   const { cache } = useSWRConfig();
   const swrResponse = useSWRImmutable(key, null, {
     ...configuration,
-    fallbackData: configuration?.fallbackData ?? cache.get(key),
+    fallbackData: configuration?.fallbackData ?? cache.get(key)?.data,
   });
 
   // write data to cache directly
   if (data !== undefined) {
-    cache.set(key, data);
+    cache.set(key, { ...cache.get(key), data });
   }
 
   const result = Object.assign(swrResponse, { mutate: () => { throw Error('mutate can not be used in context') } });

+ 11 - 11
packages/app/src/stores/user-group.tsx

@@ -25,13 +25,13 @@ export const useSWRxMyUserGroupRelations = (shouldFetch: boolean): SWRResponse<I
 
 export const useSWRxUserGroup = (groupId: string | undefined): SWRResponse<IUserGroupHasId, Error> => {
   return useSWRImmutable(
-    groupId != null ? [`/user-groups/${groupId}`] : null,
+    groupId != null ? `/user-groups/${groupId}` : null,
     endpoint => apiv3Get<UserGroupResult>(endpoint).then(result => result.data.userGroup),
   );
 };
 
 export const useSWRxUserGroupList = (initialData?: IUserGroupHasId[]): SWRResponse<IUserGroupHasId[], Error> => {
-  return useSWRImmutable<IUserGroupHasId[], Error>(
+  return useSWRImmutable(
     '/user-groups',
     endpoint => apiv3Get<UserGroupListResult>(endpoint, { pagination: false }).then(result => result.data.userGroups),
     {
@@ -44,9 +44,9 @@ export const useSWRxChildUserGroupList = (
     parentIds?: string[], includeGrandChildren?: boolean,
 ): SWRResponse<ChildUserGroupListResult, Error> => {
   const shouldFetch = parentIds != null && parentIds.length > 0;
-  return useSWRImmutable<ChildUserGroupListResult, Error>(
+  return useSWRImmutable(
     shouldFetch ? ['/user-groups/children', parentIds, includeGrandChildren] : null,
-    (endpoint, parentIds, includeGrandChildren) => apiv3Get<ChildUserGroupListResult>(
+    ([endpoint, parentIds, includeGrandChildren]) => apiv3Get<ChildUserGroupListResult>(
       endpoint, { parentIds, includeGrandChildren },
     ).then((result => result.data)),
   );
@@ -54,7 +54,7 @@ export const useSWRxChildUserGroupList = (
 
 export const useSWRxUserGroupRelations = (groupId: string): SWRResponse<IUserGroupRelationHasIdPopulatedUser[], Error> => {
   return useSWRImmutable(
-    groupId != null ? [`/user-groups/${groupId}/user-group-relations`] : null,
+    groupId != null ? `/user-groups/${groupId}/user-group-relations` : null,
     endpoint => apiv3Get<UserGroupRelationsResult>(endpoint).then(result => result.data.userGroupRelations),
   );
 };
@@ -62,9 +62,9 @@ export const useSWRxUserGroupRelations = (groupId: string): SWRResponse<IUserGro
 export const useSWRxUserGroupRelationList = (
     groupIds: string[] | undefined, childGroupIds?: string[], initialData?: IUserGroupRelationHasId[],
 ): SWRResponse<IUserGroupRelationHasId[], Error> => {
-  return useSWRImmutable<IUserGroupRelationHasId[], Error>(
+  return useSWRImmutable(
     groupIds != null ? ['/user-group-relations', groupIds, childGroupIds] : null,
-    (endpoint, groupIds, childGroupIds) => apiv3Get<UserGroupRelationListResult>(
+    ([endpoint, groupIds, childGroupIds]) => apiv3Get<UserGroupRelationListResult>(
       endpoint, { groupIds, childGroupIds },
     ).then(result => result.data.userGroupRelations),
     {
@@ -76,27 +76,27 @@ export const useSWRxUserGroupRelationList = (
 export const useSWRxUserGroupPages = (groupId: string | undefined, limit: number, offset: number): SWRResponse<IPageHasId[], Error> => {
   return useSWRImmutable(
     groupId != null ? [`/user-groups/${groupId}/pages`, limit, offset] : null,
-    endpoint => apiv3Get<UserGroupPagesResult>(endpoint, { limit, offset }).then(result => result.data.pages),
+    ([endpoint, limit, offset]) => apiv3Get<UserGroupPagesResult>(endpoint, { limit, offset }).then(result => result.data.pages),
   );
 };
 
 export const useSWRxSelectableParentUserGroups = (groupId: string | undefined): SWRResponse<IUserGroupHasId[], Error> => {
   return useSWRImmutable(
     groupId != null ? ['/user-groups/selectable-parent-groups', groupId] : null,
-    endpoint => apiv3Get<SelectableParentUserGroupsResult>(endpoint, { groupId }).then(result => result.data.selectableParentGroups),
+    ([endpoint, groupId]) => apiv3Get<SelectableParentUserGroupsResult>(endpoint, { groupId }).then(result => result.data.selectableParentGroups),
   );
 };
 
 export const useSWRxSelectableChildUserGroups = (groupId: string | undefined): SWRResponse<IUserGroupHasId[], Error> => {
   return useSWRImmutable(
     groupId != null ? ['/user-groups/selectable-child-groups', groupId] : null,
-    endpoint => apiv3Get<SelectableUserChildGroupsResult>(endpoint, { groupId }).then(result => result.data.selectableChildGroups),
+    ([endpoint, groupId]) => apiv3Get<SelectableUserChildGroupsResult>(endpoint, { groupId }).then(result => result.data.selectableChildGroups),
   );
 };
 
 export const useSWRxAncestorUserGroups = (groupId: string | undefined): SWRResponse<IUserGroupHasId[], Error> => {
   return useSWRImmutable(
     groupId != null ? ['/user-groups/ancestors', groupId] : null,
-    endpoint => apiv3Get<AncestorUserGroupsResult>(endpoint, { groupId }).then(result => result.data.ancestorUserGroups),
+    ([endpoint, groupId]) => apiv3Get<AncestorUserGroupsResult>(endpoint, { groupId }).then(result => result.data.ancestorUserGroups),
   );
 };

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

@@ -9,7 +9,7 @@ export const useSWRxUsersList = (userIds: string[]): SWRResponse<IUserHasId[], E
   const distinctUserIds = userIds.length > 0 ? Array.from(new Set(userIds)).sort() : [];
   return useSWR(
     distinctUserIds.length > 0 ? ['/users/list', distinctUserIds] : null,
-    (endpoint, userIds) => apiv3Get(endpoint, { userIds: userIds.join(',') }).then((response) => {
+    ([endpoint, userIds]) => apiv3Get(endpoint, { userIds: userIds.join(',') }).then((response) => {
       return response.data.users;
     }),
     {
@@ -43,7 +43,7 @@ type usernameResult = {
 export const useSWRxUsernames = (q: string, offset?: number, limit?: number, options?: usernameRequestOptions): SWRResponse<usernameResult, Error> => {
   return useSWRImmutable(
     (q != null && q.trim() !== '') ? ['/users/usernames', q, offset, limit, options] : null,
-    (endpoint, q, offset, limit, options) => apiv3Get(endpoint, {
+    ([endpoint, q, offset, limit, options]) => apiv3Get(endpoint, {
       q, offset, limit, options,
     }).then(result => result.data),
   );

+ 3 - 1
packages/app/src/stores/xss.ts

@@ -1,8 +1,10 @@
 
-import { useStaticSWR } from './use-static-swr';
 import { SWRResponse } from 'swr';
+
 import Xss from '~/services/xss';
 
+import { useStaticSWR } from './use-static-swr';
+
 export const useXss = (initialData?: Xss): SWRResponse<Xss, Error> => {
   return useStaticSWR<Xss, Error>('xss', initialData);
 };

+ 12 - 8
packages/app/src/utils/swr-utils.ts

@@ -1,9 +1,13 @@
-import { ProviderConfiguration, PublicConfiguration } from 'swr/dist/types';
+import { isServer } from '@growi/core';
 
-export type SWRConfigValue = Partial<PublicConfiguration> & Partial<ProviderConfiguration> & {
-  provider?: (cache) => any | undefined,
-};
-
-export const swrGlobalConfiguration: SWRConfigValue = {
-  errorRetryCount: 1,
-};
+export const swrGlobalConfiguration = Object.assign(
+  {
+    errorRetryCount: 1,
+  },
+  // set the request scoped cache provider in server
+  isServer()
+    ? {
+      provider: (cache: any) => new Map<string, any>(cache),
+    }
+    : {},
+);

+ 12 - 18
yarn.lock

@@ -4437,18 +4437,10 @@
   dependencies:
     "@types/react" "*"
 
-"@types/react@*":
-  version "16.9.23"
-  resolved "https://registry.yarnpkg.com/@types/react/-/react-16.9.23.tgz#1a66c6d468ba11a8943ad958a8cb3e737568271c"
-  integrity sha512-SsGVT4E7L2wLN3tPYLiF20hmZTPGuzaayVunfgXzUn1x4uHVsKH6QDJQ/TdpHqwsTLd4CwrmQ2vOgxN7gE24gw==
-  dependencies:
-    "@types/prop-types" "*"
-    csstype "^2.2.0"
-
-"@types/react@>=16.9.11":
-  version "17.0.40"
-  resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.40.tgz#dc010cee6254d5239a138083f3799a16638e6bad"
-  integrity sha512-UrXhD/JyLH+W70nNSufXqMZNuUD2cXHu6UjCllC6pmOQgBX4SGXOH8fjRka0O0Ee0HrFxapDD8Bwn81Kmiz6jQ==
+"@types/react@*", "@types/react@>=16.9.11":
+  version "18.0.27"
+  resolved "https://registry.yarnpkg.com/@types/react/-/react-18.0.27.tgz#d9425abe187a00f8a5ec182b010d4fd9da703b71"
+  integrity sha512-3vtRKHgVxu3Jp9t718R9BuzoD4NcQ8YJ5XRzsSKxNDiDonD2MXIT1TmSkenxuCycZJoQT5d2vE8LwWJxBC1gmA==
   dependencies:
     "@types/prop-types" "*"
     "@types/scheduler" "*"
@@ -7553,11 +7545,6 @@ cssfilter@0.0.10:
   version "0.0.10"
   resolved "https://registry.yarnpkg.com/cssfilter/-/cssfilter-0.0.10.tgz#c6d2672632a2e5c83e013e6864a42ce8defd20ae"
 
-csstype@^2.2.0:
-  version "2.6.9"
-  resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.6.9.tgz#05141d0cd557a56b8891394c1911c40c8a98d098"
-  integrity sha512-xz39Sb4+OaTsULgUERcCk+TJj8ylkL4aSVDQiX/ksxbELSqwkgt4d4RD7fovIdgJGSuNYqwZEiVjYY5l0ask+Q==
-
 csstype@^3.0.2:
   version "3.0.11"
   resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.0.11.tgz#d66700c5eacfac1940deb4e3ee5642792d85cd33"
@@ -21803,6 +21790,13 @@ swr@^1.3.0:
   resolved "https://registry.yarnpkg.com/swr/-/swr-1.3.0.tgz#c6531866a35b4db37b38b72c45a63171faf9f4e8"
   integrity sha512-dkghQrOl2ORX9HYrMDtPa7LTVHJjCTeZoB1dqTbnnEDlSvN8JEKpYIYurDfvbQFUUS8Cg8PceFVZNkW0KNNYPw==
 
+swr@^2.0.2:
+  version "2.0.2"
+  resolved "https://registry.yarnpkg.com/swr/-/swr-2.0.2.tgz#fd34f3aac354f6b70f9134eb4218c747cc899a8d"
+  integrity sha512-iHbQW17hsduonMEliZnr6/yaxb+yvLe2r0+AH+ZfeqKzwc2bb+QRYpZm5/b/H0Lxgy7VWow4o71JeSazSun+9A==
+  dependencies:
+    use-sync-external-store "^1.2.0"
+
 synckit@^0.7.2:
   version "0.7.2"
   resolved "https://registry.yarnpkg.com/synckit/-/synckit-0.7.2.tgz#43c07b5a8101ee45355aebf0216895309fd32a6f"
@@ -23432,7 +23426,7 @@ url@0.10.3:
     punycode "1.3.2"
     querystring "0.2.0"
 
-use-sync-external-store@1.2.0:
+use-sync-external-store@1.2.0, use-sync-external-store@^1.2.0:
   version "1.2.0"
   resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz#7dbefd6ef3fe4e767a0cf5d7287aacfb5846928a"
   integrity sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==