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

Merge pull request #9092 from weseek/imprv/153641-recent-changes-update-on-create-update-delete

imprv: Update Recent Changes when a page is created, updated, or deleted
mergify[bot] 1 год назад
Родитель
Сommit
5358b2a17d

+ 3 - 2
apps/app/src/client/components/DescendantsPageList.tsx

@@ -14,7 +14,7 @@ import type { OnDeletedFunction, OnPutBackedFunction } from '~/interfaces/ui';
 import { useIsGuestUser, useIsReadOnlyUser, useIsSharedUser } from '~/stores-universal/context';
 import { useIsGuestUser, useIsReadOnlyUser, useIsSharedUser } from '~/stores-universal/context';
 import {
 import {
   mutatePageTree,
   mutatePageTree,
-  useSWRxPageInfoForList, useSWRxPageList,
+  useSWRxPageInfoForList, useSWRxPageList, mutateRecentlyUpdated,
 } from '~/stores/page-listing';
 } from '~/stores/page-listing';
 
 
 import type { ForceHideMenuItems } from './Common/Dropdown/PageItemControl';
 import type { ForceHideMenuItems } from './Common/Dropdown/PageItemControl';
@@ -67,7 +67,7 @@ const DescendantsPageListSubstance = (props: SubstanceProps): JSX.Element => {
     else {
     else {
       toastSuccess(t('deleted_pages_completely', { path }));
       toastSuccess(t('deleted_pages_completely', { path }));
     }
     }
-
+    mutateRecentlyUpdated();
     mutatePageTree();
     mutatePageTree();
     if (onPagesDeleted != null) {
     if (onPagesDeleted != null) {
       onPagesDeleted(...args);
       onPagesDeleted(...args);
@@ -77,6 +77,7 @@ const DescendantsPageListSubstance = (props: SubstanceProps): JSX.Element => {
   const pagePutBackedHandler: OnPutBackedFunction = useCallback((path) => {
   const pagePutBackedHandler: OnPutBackedFunction = useCallback((path) => {
     toastSuccess(t('page_has_been_reverted', { path }));
     toastSuccess(t('page_has_been_reverted', { path }));
 
 
+    mutateRecentlyUpdated();
     mutatePageTree();
     mutatePageTree();
     if (onPagePutBacked != null) {
     if (onPagePutBacked != null) {
       onPagePutBacked(path);
       onPagePutBacked(path);

+ 3 - 1
apps/app/src/client/components/Navbar/GrowiContextualSubNavigation.tsx

@@ -34,7 +34,7 @@ import {
 import {
 import {
   useSWRMUTxCurrentPage, useCurrentPageId, useSWRxPageInfo,
   useSWRMUTxCurrentPage, useCurrentPageId, useSWRxPageInfo,
 } from '~/stores/page';
 } from '~/stores/page';
-import { mutatePageTree } from '~/stores/page-listing';
+import { mutatePageTree, mutateRecentlyUpdated } from '~/stores/page-listing';
 import {
 import {
   useIsAbleToShowPageManagement,
   useIsAbleToShowPageManagement,
   useIsAbleToChangeEditorMode,
   useIsAbleToChangeEditorMode,
@@ -271,6 +271,7 @@ const GrowiContextualSubNavigation = (props: GrowiContextualSubNavigationProps):
       mutateCurrentPage();
       mutateCurrentPage();
       mutatePageInfo();
       mutatePageInfo();
       mutatePageTree();
       mutatePageTree();
+      mutateRecentlyUpdated();
     };
     };
     openRenameModal(page, { onRenamed: renamedHandler });
     openRenameModal(page, { onRenamed: renamedHandler });
   }, [mutateCurrentPage, mutatePageInfo, openRenameModal]);
   }, [mutateCurrentPage, mutatePageInfo, openRenameModal]);
@@ -294,6 +295,7 @@ const GrowiContextualSubNavigation = (props: GrowiContextualSubNavigationProps):
       mutateCurrentPage();
       mutateCurrentPage();
       mutatePageInfo();
       mutatePageInfo();
       mutatePageTree();
       mutatePageTree();
+      mutateRecentlyUpdated();
     };
     };
     openDeleteModal([pageWithMeta], { onDeleted: deletedHandler });
     openDeleteModal([pageWithMeta], { onDeleted: deletedHandler });
   }, [currentPathname, mutateCurrentPage, openDeleteModal, router, mutatePageInfo]);
   }, [currentPathname, mutateCurrentPage, openDeleteModal, router, mutatePageInfo]);

+ 3 - 1
apps/app/src/client/components/PageEditor/PageEditor.tsx

@@ -41,7 +41,7 @@ import {
 import {
 import {
   useCurrentPagePath, useSWRxCurrentPage, useCurrentPageId, useIsNotFound, useTemplateBodyData, useSWRxCurrentGrantData,
   useCurrentPagePath, useSWRxCurrentPage, useCurrentPageId, useIsNotFound, useTemplateBodyData, useSWRxCurrentGrantData,
 } from '~/stores/page';
 } from '~/stores/page';
-import { mutatePageTree } from '~/stores/page-listing';
+import { mutatePageTree, mutateRecentlyUpdated } from '~/stores/page-listing';
 import { usePreviewOptions } from '~/stores/renderer';
 import { usePreviewOptions } from '~/stores/renderer';
 import { useIsUntitledPage, useSelectedGrant } from '~/stores/ui';
 import { useIsUntitledPage, useSelectedGrant } from '~/stores/ui';
 import { useEditingUsers } from '~/stores/use-editing-users';
 import { useEditingUsers } from '~/stores/use-editing-users';
@@ -190,6 +190,8 @@ export const PageEditor = React.memo((props: Props): JSX.Element => {
 
 
       // to sync revision id with page tree: https://github.com/weseek/growi/pull/7227
       // to sync revision id with page tree: https://github.com/weseek/growi/pull/7227
       mutatePageTree();
       mutatePageTree();
+
+      mutateRecentlyUpdated();
       // sync current grant data after update
       // sync current grant data after update
       mutateIsGrantNormalized();
       mutateIsGrantNormalized();
 
 

+ 2 - 1
apps/app/src/client/components/PageEditor/page-path-rename-utils.ts

@@ -6,7 +6,7 @@ import { useTranslation } from 'next-i18next';
 import { apiv3Put } from '~/client/util/apiv3-client';
 import { apiv3Put } from '~/client/util/apiv3-client';
 import { toastSuccess, toastError } from '~/client/util/toastr';
 import { toastSuccess, toastError } from '~/client/util/toastr';
 import { useSWRMUTxCurrentPage } from '~/stores/page';
 import { useSWRMUTxCurrentPage } from '~/stores/page';
-import { mutatePageTree, mutatePageList } from '~/stores/page-listing';
+import { mutatePageTree, mutatePageList, mutateRecentlyUpdated } from '~/stores/page-listing';
 import { useIsUntitledPage } from '~/stores/ui';
 import { useIsUntitledPage } from '~/stores/ui';
 
 
 
 
@@ -33,6 +33,7 @@ export const usePagePathRenameHandler = (
 
 
     const onRenamed = (fromPath: string | undefined, toPath: string) => {
     const onRenamed = (fromPath: string | undefined, toPath: string) => {
       mutatePageTree();
       mutatePageTree();
+      mutateRecentlyUpdated();
       mutatePageList();
       mutatePageList();
       mutateIsUntitledPage(false);
       mutateIsUntitledPage(false);
 
 

+ 2 - 1
apps/app/src/client/components/SearchPage/SearchPageBase.tsx

@@ -15,7 +15,7 @@ import {
   useIsGuestUser, useIsReadOnlyUser, useIsSearchServiceConfigured, useIsSearchServiceReachable,
   useIsGuestUser, useIsReadOnlyUser, useIsSearchServiceConfigured, useIsSearchServiceReachable,
 } from '~/stores-universal/context';
 } from '~/stores-universal/context';
 import { usePageDeleteModal } from '~/stores/modal';
 import { usePageDeleteModal } from '~/stores/modal';
-import { mutatePageTree } from '~/stores/page-listing';
+import { mutatePageTree, mutateRecentlyUpdated } from '~/stores/page-listing';
 
 
 import type { ForceHideMenuItems } from '../Common/Dropdown/PageItemControl';
 import type { ForceHideMenuItems } from '../Common/Dropdown/PageItemControl';
 
 
@@ -275,6 +275,7 @@ export const usePageDeleteModalForBulkDeletion = (
           toastSuccess(t('deleted_pages_completely', { path }));
           toastSuccess(t('deleted_pages_completely', { path }));
         }
         }
         mutatePageTree();
         mutatePageTree();
+        mutateRecentlyUpdated();
 
 
         if (onDeleted != null) {
         if (onDeleted != null) {
           onDeleted(...args);
           onDeleted(...args);

+ 4 - 1
apps/app/src/client/components/SearchPage/SearchResultContent.tsx

@@ -21,7 +21,7 @@ import { useCurrentUser } from '~/stores-universal/context';
 import {
 import {
   usePageDuplicateModal, usePageRenameModal, usePageDeleteModal,
   usePageDuplicateModal, usePageRenameModal, usePageDeleteModal,
 } from '~/stores/modal';
 } from '~/stores/modal';
-import { mutatePageList, mutatePageTree } from '~/stores/page-listing';
+import { mutatePageList, mutatePageTree, mutateRecentlyUpdated } from '~/stores/page-listing';
 import { useSearchResultOptions } from '~/stores/renderer';
 import { useSearchResultOptions } from '~/stores/renderer';
 import { mutateSearching } from '~/stores/search';
 import { mutateSearching } from '~/stores/search';
 
 
@@ -135,6 +135,7 @@ export const SearchResultContent: FC<Props> = (props: Props) => {
       toastSuccess(t('duplicated_pages', { fromPath }));
       toastSuccess(t('duplicated_pages', { fromPath }));
 
 
       mutatePageTree();
       mutatePageTree();
+      mutateRecentlyUpdated();
       mutateSearching();
       mutateSearching();
       mutatePageList();
       mutatePageList();
     };
     };
@@ -146,6 +147,7 @@ export const SearchResultContent: FC<Props> = (props: Props) => {
       toastSuccess(t('renamed_pages', { path }));
       toastSuccess(t('renamed_pages', { path }));
 
 
       mutatePageTree();
       mutatePageTree();
+      mutateRecentlyUpdated();
       mutateSearching();
       mutateSearching();
       mutatePageList();
       mutatePageList();
     };
     };
@@ -165,6 +167,7 @@ export const SearchResultContent: FC<Props> = (props: Props) => {
       toastSuccess(t('deleted_pages', { path }));
       toastSuccess(t('deleted_pages', { path }));
     }
     }
     mutatePageTree();
     mutatePageTree();
+    mutateRecentlyUpdated();
     mutateSearching();
     mutateSearching();
     mutatePageList();
     mutatePageList();
   }, [t]);
   }, [t]);

+ 4 - 1
apps/app/src/client/components/SearchPage/SearchResultList.tsx

@@ -12,7 +12,7 @@ import type { ISelectable, ISelectableAll } from '~/client/interfaces/selectable
 import { toastSuccess } from '~/client/util/toastr';
 import { toastSuccess } from '~/client/util/toastr';
 import type { IPageSearchMeta, IPageWithSearchMeta } from '~/interfaces/search';
 import type { IPageSearchMeta, IPageWithSearchMeta } from '~/interfaces/search';
 import { useIsGuestUser, useIsReadOnlyUser } from '~/stores-universal/context';
 import { useIsGuestUser, useIsReadOnlyUser } from '~/stores-universal/context';
-import { mutatePageTree, useSWRxPageInfoForList } from '~/stores/page-listing';
+import { mutatePageTree, useSWRxPageInfoForList, mutateRecentlyUpdated } from '~/stores/page-listing';
 import { mutateSearching } from '~/stores/search';
 import { mutateSearching } from '~/stores/search';
 
 
 import type { ForceHideMenuItems } from '../Common/Dropdown/PageItemControl';
 import type { ForceHideMenuItems } from '../Common/Dropdown/PageItemControl';
@@ -94,6 +94,7 @@ const SearchResultListSubstance: ForwardRefRenderFunction<ISelectableAll, Props>
     toastSuccess(t('duplicated_pages', { fromPath }));
     toastSuccess(t('duplicated_pages', { fromPath }));
 
 
     mutatePageTree();
     mutatePageTree();
+    mutateRecentlyUpdated();
     mutateSearching();
     mutateSearching();
   }, [t]);
   }, [t]);
 
 
@@ -101,6 +102,7 @@ const SearchResultListSubstance: ForwardRefRenderFunction<ISelectableAll, Props>
     toastSuccess(t('renamed_pages', { path }));
     toastSuccess(t('renamed_pages', { path }));
 
 
     mutatePageTree();
     mutatePageTree();
+    mutateRecentlyUpdated();
     mutateSearching();
     mutateSearching();
   }, [t]);
   }, [t]);
 
 
@@ -118,6 +120,7 @@ const SearchResultListSubstance: ForwardRefRenderFunction<ISelectableAll, Props>
       toastSuccess(t('deleted_pages', { path }));
       toastSuccess(t('deleted_pages', { path }));
     }
     }
     mutatePageTree();
     mutatePageTree();
+    mutateRecentlyUpdated();
     mutateSearching();
     mutateSearching();
   }, [t]);
   }, [t]);
 
 

+ 2 - 1
apps/app/src/client/components/Sidebar/PageTree/PageTreeSubstance.tsx

@@ -8,7 +8,7 @@ import { debounce } from 'throttle-debounce';
 import { useTargetAndAncestors, useIsGuestUser, useIsReadOnlyUser } from '~/stores-universal/context';
 import { useTargetAndAncestors, useIsGuestUser, useIsReadOnlyUser } from '~/stores-universal/context';
 import { useCurrentPagePath, useCurrentPageId } from '~/stores/page';
 import { useCurrentPagePath, useCurrentPageId } from '~/stores/page';
 import {
 import {
-  mutatePageTree, useSWRxPageAncestorsChildren, useSWRxRootPage, useSWRxV5MigrationStatus,
+  mutatePageTree, mutateRecentlyUpdated, useSWRxPageAncestorsChildren, useSWRxRootPage, useSWRxV5MigrationStatus,
 } from '~/stores/page-listing';
 } from '~/stores/page-listing';
 import { useSidebarScrollerRef } from '~/stores/ui';
 import { useSidebarScrollerRef } from '~/stores/ui';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
@@ -35,6 +35,7 @@ export const PageTreeHeader = memo(({ isWipPageShown, onWipPageShownChange }: He
   const mutate = useCallback(() => {
   const mutate = useCallback(() => {
     mutateRootPage();
     mutateRootPage();
     mutatePageTree();
     mutatePageTree();
+    mutateRecentlyUpdated();
   }, [mutateRootPage]);
   }, [mutateRootPage]);
 
 
   return (
   return (

+ 4 - 6
apps/app/src/client/components/Sidebar/RecentChanges/RecentChangesSubstance.tsx

@@ -151,13 +151,12 @@ type HeaderProps = {
   onWipPageShownChange: () => void,
   onWipPageShownChange: () => void,
 }
 }
 
 
-const PER_PAGE = 20;
 export const RecentChangesHeader = ({
 export const RecentChangesHeader = ({
   isSmall, onSizeChange, isWipPageShown, onWipPageShownChange,
   isSmall, onSizeChange, isWipPageShown, onWipPageShownChange,
 }: HeaderProps): JSX.Element => {
 }: HeaderProps): JSX.Element => {
   const { t } = useTranslation();
   const { t } = useTranslation();
 
 
-  const { mutate } = useSWRINFxRecentlyUpdated(PER_PAGE, isWipPageShown, { suspense: true });
+  const { mutate } = useSWRINFxRecentlyUpdated(isWipPageShown, { suspense: true });
 
 
   const retrieveSizePreferenceFromLocalStorage = useCallback(() => {
   const retrieveSizePreferenceFromLocalStorage = useCallback(() => {
     if (window.localStorage.isRecentChangesSidebarSmall === 'true') {
     if (window.localStorage.isRecentChangesSidebarSmall === 'true') {
@@ -232,14 +231,13 @@ type ContentProps = {
 }
 }
 
 
 export const RecentChangesContent = ({ isSmall, isWipPageShown }: ContentProps): JSX.Element => {
 export const RecentChangesContent = ({ isSmall, isWipPageShown }: ContentProps): JSX.Element => {
-  const swrInifinitexRecentlyUpdated = useSWRINFxRecentlyUpdated(PER_PAGE, isWipPageShown, { suspense: true });
+  const swrInifinitexRecentlyUpdated = useSWRINFxRecentlyUpdated(isWipPageShown, { suspense: true });
   const { data } = swrInifinitexRecentlyUpdated;
   const { data } = swrInifinitexRecentlyUpdated;
 
 
   const { pushState } = useKeywordManager();
   const { pushState } = useKeywordManager();
-
   const isEmpty = data?.[0]?.pages.length === 0;
   const isEmpty = data?.[0]?.pages.length === 0;
-  const isReachingEnd = isEmpty || (data != null && data[data.length - 1]?.pages.length < PER_PAGE);
-
+  const lastPageIndex = data?.length ? data.length - 1 : 0;
+  const isReachingEnd = isEmpty || (data != null && lastPageIndex > 0 && data[lastPageIndex]?.pages.length < data[lastPageIndex - 1]?.pages.length);
   return (
   return (
     <div className="grw-recent-changes">
     <div className="grw-recent-changes">
       <ul className="list-group list-group-flush">
       <ul className="list-group list-group-flush">

+ 3 - 2
apps/app/src/client/components/TreeItem/NewPageInput/use-new-page-input.tsx

@@ -11,12 +11,12 @@ import { useRect } from '@growi/ui/dist/utils';
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 import { debounce } from 'throttle-debounce';
 import { debounce } from 'throttle-debounce';
 
 
+import { AutosizeSubmittableInput, getAdjustedMaxWidthForAutosizeInput } from '~/client/components/Common/SubmittableInput';
 import { useCreatePage } from '~/client/services/create-page';
 import { useCreatePage } from '~/client/services/create-page';
 import { toastWarning, toastError, toastSuccess } from '~/client/util/toastr';
 import { toastWarning, toastError, toastSuccess } from '~/client/util/toastr';
 import type { InputValidationResult } from '~/client/util/use-input-validator';
 import type { InputValidationResult } from '~/client/util/use-input-validator';
 import { ValidationTarget, useInputValidator } from '~/client/util/use-input-validator';
 import { ValidationTarget, useInputValidator } from '~/client/util/use-input-validator';
-import { AutosizeSubmittableInput, getAdjustedMaxWidthForAutosizeInput } from '~/client/components/Common/SubmittableInput';
-import { mutatePageTree } from '~/stores/page-listing';
+import { mutatePageTree, mutateRecentlyUpdated } from '~/stores/page-listing';
 import { usePageTreeDescCountMap } from '~/stores/ui';
 import { usePageTreeDescCountMap } from '~/stores/ui';
 
 
 import { shouldCreateWipPage } from '../../../../utils/should-create-wip-page';
 import { shouldCreateWipPage } from '../../../../utils/should-create-wip-page';
@@ -123,6 +123,7 @@ export const useNewPageInput = (): UseNewPageInput => {
             skipTransition: true,
             skipTransition: true,
             onCreated: () => {
             onCreated: () => {
               mutatePageTree();
               mutatePageTree();
+              mutateRecentlyUpdated();
 
 
               if (!hasDescendants) {
               if (!hasDescendants) {
                 stateHandlers?.setIsOpen(true);
                 stateHandlers?.setIsOpen(true);

+ 2 - 0
apps/app/src/components/PageView/PageAlerts/TrashPageAlert.tsx

@@ -9,6 +9,7 @@ import { usePageDeleteModal, usePutBackPageModal } from '~/stores/modal';
 import {
 import {
   useCurrentPagePath, useSWRxPageInfo, useSWRxCurrentPage, useIsTrashPage, useSWRMUTxCurrentPage,
   useCurrentPagePath, useSWRxPageInfo, useSWRxCurrentPage, useIsTrashPage, useSWRMUTxCurrentPage,
 } from '~/stores/page';
 } from '~/stores/page';
+import { mutateRecentlyUpdated } from '~/stores/page-listing';
 import { useIsAbleToShowTrashPageManagementButtons } from '~/stores/ui';
 import { useIsAbleToShowTrashPageManagementButtons } from '~/stores/ui';
 
 
 
 
@@ -57,6 +58,7 @@ export const TrashPageAlert = (): JSX.Element => {
 
 
         router.push(`/${pageId}`);
         router.push(`/${pageId}`);
         mutateCurrentPage();
         mutateCurrentPage();
+        mutateRecentlyUpdated();
       }
       }
       catch (err) {
       catch (err) {
         const toastError = (await import('~/client/util/toastr')).toastError;
         const toastError = (await import('~/client/util/toastr')).toastError;

+ 3 - 0
apps/app/src/components/PageView/PageAlerts/WipPageAlert.tsx

@@ -26,6 +26,9 @@ export const WipPageAlert = (): JSX.Element => {
       const mutatePageTree = (await import('~/stores/page-listing')).mutatePageTree;
       const mutatePageTree = (await import('~/stores/page-listing')).mutatePageTree;
       await mutatePageTree();
       await mutatePageTree();
 
 
+      const mutateRecentlyUpdated = (await import('~/stores/page-listing')).mutateRecentlyUpdated;
+      await mutateRecentlyUpdated();
+
       const toastSuccess = (await import('~/client/util/toastr')).toastSuccess;
       const toastSuccess = (await import('~/client/util/toastr')).toastSuccess;
       toastSuccess(t('wip_page.success_publish_page'));
       toastSuccess(t('wip_page.success_publish_page'));
     }
     }

+ 34 - 13
apps/app/src/stores/page-listing.tsx

@@ -7,9 +7,10 @@ import type {
 import useSWR, {
 import useSWR, {
   mutate, type SWRConfiguration, type SWRResponse, type Arguments,
   mutate, type SWRConfiguration, type SWRResponse, type Arguments,
 } from 'swr';
 } from 'swr';
+import { cache } from 'swr/_internal';
 import useSWRImmutable from 'swr/immutable';
 import useSWRImmutable from 'swr/immutable';
 import type { SWRInfiniteResponse } from 'swr/infinite';
 import type { SWRInfiniteResponse } from 'swr/infinite';
-import useSWRInfinite from 'swr/infinite';
+import useSWRInfinite, { unstable_serialize } from 'swr/infinite'; // eslint-disable-line camelcase
 
 
 import type { IPagingResult } from '~/interfaces/paging-result';
 import type { IPagingResult } from '~/interfaces/paging-result';
 
 
@@ -33,27 +34,47 @@ type RecentApiResult = {
   totalCount: number,
   totalCount: number,
   offset: number,
   offset: number,
 }
 }
-export const useSWRINFxRecentlyUpdated = (limit: number, includeWipPage?: boolean, config?: SWRConfiguration) : SWRInfiniteResponse<RecentApiResult, Error> => {
-  return useSWRInfinite(
-    (pageIndex, previousPageData) => {
-      if (previousPageData != null && previousPageData.pages.length === 0) return null;
 
 
-      if (pageIndex === 0 || previousPageData == null) {
-        return ['/pages/recent', undefined, limit, includeWipPage];
-      }
+export const getRecentlyUpdatedKey = (
+    pageIndex: number,
+    previousPageData: RecentApiResult | null,
+    includeWipPage?: boolean,
+): [string, number | undefined, boolean | undefined] | null => {
+  if (previousPageData != null && previousPageData.pages.length === 0) return null;
 
 
-      const offset = previousPageData.offset + limit;
-      return ['/pages/recent', offset, limit, includeWipPage];
-    },
-    ([endpoint, offset, limit, includeWipPage]) => apiv3Get<RecentApiResult>(endpoint, { offset, limit, includeWipPage }).then(response => response.data),
+  if (pageIndex === 0 || previousPageData == null) {
+    return ['/pages/recent', undefined, includeWipPage];
+  }
+  const offset = previousPageData.offset + previousPageData.pages.length;
+  return ['/pages/recent', offset, includeWipPage];
+
+};
+
+export const useSWRINFxRecentlyUpdated = (
+    includeWipPage?: boolean,
+    config?: SWRConfiguration,
+): SWRInfiniteResponse<RecentApiResult, Error> => {
+  const PER_PAGE = 20;
+  return useSWRInfinite(
+    (pageIndex, previousPageData) => getRecentlyUpdatedKey(pageIndex, previousPageData, includeWipPage),
+    ([endpoint, offset, includeWipPage]) => apiv3Get<RecentApiResult>(endpoint, { offset, limit: PER_PAGE, includeWipPage }).then(response => response.data),
     {
     {
       ...config,
       ...config,
       revalidateFirstPage: false,
       revalidateFirstPage: false,
-      revalidateAll: false,
+      revalidateAll: true,
     },
     },
   );
   );
 };
 };
 
 
+export const mutateRecentlyUpdated = async(): Promise<undefined> => {
+  [true, false].forEach(includeWipPage => mutate(
+    unstable_serialize(
+      (pageIndex, previousPageData) => getRecentlyUpdatedKey(pageIndex, previousPageData, includeWipPage),
+    ),
+  ));
+  return;
+};
+
 export const mutatePageList = async(): Promise<void[]> => {
 export const mutatePageList = async(): Promise<void[]> => {
   return mutate(
   return mutate(
     key => Array.isArray(key) && key[0] === '/pages/list',
     key => Array.isArray(key) && key[0] === '/pages/list',