ui.tsx 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602
  1. import useSWR, {
  2. useSWRConfig, SWRResponse, Key, Fetcher,
  3. } from 'swr';
  4. import useSWRImmutable from 'swr/immutable';
  5. import { Breakpoint, addBreakpointListener } from '@growi/ui';
  6. import { pagePathUtils } from '@growi/core';
  7. import { RefObject } from 'react';
  8. import { SidebarContentsType } from '~/interfaces/ui';
  9. import loggerFactory from '~/utils/logger';
  10. import { useStaticSWR } from './use-static-swr';
  11. import {
  12. useCurrentPageId, useCurrentPagePath, useIsEditable, useIsTrashPage, useIsUserPage,
  13. useIsNotCreatable, useIsSharedUser, useNotFoundTargetPathOrId, useIsForbidden, useIsIdenticalPath,
  14. } from './context';
  15. import { IFocusable } from '~/client/interfaces/focusable';
  16. import { Nullable } from '~/interfaces/common';
  17. const { isSharedPage } = pagePathUtils;
  18. const logger = loggerFactory('growi:stores:ui');
  19. const isServer = typeof window === 'undefined';
  20. /** **********************************************************
  21. * Unions
  22. *********************************************************** */
  23. export const EditorMode = {
  24. View: 'view',
  25. Editor: 'editor',
  26. HackMD: 'hackmd',
  27. } as const;
  28. export type EditorMode = typeof EditorMode[keyof typeof EditorMode];
  29. /** **********************************************************
  30. * SWR Hooks
  31. * for switching UI
  32. *********************************************************** */
  33. export const useIsMobile = (): SWRResponse<boolean, Error> => {
  34. const key = isServer ? null : 'isMobile';
  35. let configuration;
  36. if (!isServer) {
  37. const userAgent = window.navigator.userAgent.toLowerCase();
  38. configuration = {
  39. fallbackData: /iphone|ipad|android/.test(userAgent),
  40. };
  41. }
  42. return useStaticSWR<boolean, Error>(key, undefined, configuration);
  43. };
  44. const updateBodyClassesByEditorMode = (newEditorMode: EditorMode) => {
  45. switch (newEditorMode) {
  46. case EditorMode.View:
  47. $('body').removeClass('on-edit');
  48. $('body').removeClass('builtin-editor');
  49. $('body').removeClass('hackmd');
  50. $('body').removeClass('pathname-sidebar');
  51. break;
  52. case EditorMode.Editor:
  53. $('body').addClass('on-edit');
  54. $('body').addClass('builtin-editor');
  55. $('body').removeClass('hackmd');
  56. // editing /Sidebar
  57. if (window.location.pathname === '/Sidebar') {
  58. $('body').addClass('pathname-sidebar');
  59. }
  60. break;
  61. case EditorMode.HackMD:
  62. $('body').addClass('on-edit');
  63. $('body').addClass('hackmd');
  64. $('body').removeClass('builtin-editor');
  65. $('body').removeClass('pathname-sidebar');
  66. break;
  67. }
  68. };
  69. const updateHashByEditorMode = (newEditorMode: EditorMode) => {
  70. const { pathname } = window.location;
  71. switch (newEditorMode) {
  72. case EditorMode.View:
  73. window.history.replaceState(null, '', pathname);
  74. break;
  75. case EditorMode.Editor:
  76. window.history.replaceState(null, '', `${pathname}#edit`);
  77. break;
  78. case EditorMode.HackMD:
  79. window.history.replaceState(null, '', `${pathname}#hackmd`);
  80. break;
  81. }
  82. };
  83. export const determineEditorModeByHash = (): EditorMode => {
  84. const { hash } = window.location;
  85. switch (hash) {
  86. case '#edit':
  87. return EditorMode.Editor;
  88. case '#hackmd':
  89. return EditorMode.HackMD;
  90. default:
  91. return EditorMode.View;
  92. }
  93. };
  94. let isEditorModeLoaded = false;
  95. export const useEditorMode = (): SWRResponse<EditorMode, Error> => {
  96. const { data: _isEditable } = useIsEditable();
  97. const editorModeByHash = determineEditorModeByHash();
  98. const isLoading = _isEditable === undefined;
  99. const isEditable = !isLoading && _isEditable;
  100. const initialData = isEditable ? editorModeByHash : EditorMode.View;
  101. const swrResponse = useSWRImmutable(
  102. isLoading ? null : ['editorMode', isEditable],
  103. null,
  104. { fallbackData: initialData },
  105. );
  106. // initial updating
  107. if (!isEditorModeLoaded && !isLoading && swrResponse.data != null) {
  108. if (isEditable) {
  109. updateBodyClassesByEditorMode(swrResponse.data);
  110. }
  111. isEditorModeLoaded = true;
  112. }
  113. return {
  114. ...swrResponse,
  115. // overwrite mutate
  116. mutate: (editorMode: EditorMode, shouldRevalidate?: boolean) => {
  117. if (!isEditable) {
  118. return Promise.resolve(EditorMode.View); // fixed if not editable
  119. }
  120. updateBodyClassesByEditorMode(editorMode);
  121. updateHashByEditorMode(editorMode);
  122. return swrResponse.mutate(editorMode, shouldRevalidate);
  123. },
  124. };
  125. };
  126. export const useIsDeviceSmallerThanMd = (): SWRResponse<boolean, Error> => {
  127. const key: Key = isServer ? null : 'isDeviceSmallerThanMd';
  128. const { cache, mutate } = useSWRConfig();
  129. if (!isServer) {
  130. const mdOrAvobeHandler = function(this: MediaQueryList): void {
  131. // sm -> md: matches will be true
  132. // md -> sm: matches will be false
  133. mutate(key, !this.matches);
  134. };
  135. const mql = addBreakpointListener(Breakpoint.MD, mdOrAvobeHandler);
  136. // initialize
  137. if (cache.get(key) == null) {
  138. document.addEventListener('DOMContentLoaded', () => {
  139. mutate(key, !mql.matches);
  140. });
  141. }
  142. }
  143. return useStaticSWR(key);
  144. };
  145. export const useIsDeviceSmallerThanLg = (): SWRResponse<boolean, Error> => {
  146. const key: Key = isServer ? null : 'isDeviceSmallerThanLg';
  147. const { cache, mutate } = useSWRConfig();
  148. if (!isServer) {
  149. const lgOrAvobeHandler = function(this: MediaQueryList): void {
  150. // md -> lg: matches will be true
  151. // lg -> md: matches will be false
  152. mutate(key, !this.matches);
  153. };
  154. const mql = addBreakpointListener(Breakpoint.LG, lgOrAvobeHandler);
  155. // initialize
  156. if (cache.get(key) == null) {
  157. document.addEventListener('DOMContentLoaded', () => {
  158. mutate(key, !mql.matches);
  159. });
  160. }
  161. }
  162. return useStaticSWR(key);
  163. };
  164. export const usePreferDrawerModeByUser = (initialData?: boolean): SWRResponse<boolean, Error> => {
  165. return useStaticSWR('preferDrawerModeByUser', initialData, { fallbackData: false });
  166. };
  167. export const usePreferDrawerModeOnEditByUser = (initialData?: boolean): SWRResponse<boolean, Error> => {
  168. return useStaticSWR('preferDrawerModeOnEditByUser', initialData, { fallbackData: true });
  169. };
  170. export const useSidebarCollapsed = (initialData?: boolean): SWRResponse<boolean, Error> => {
  171. return useStaticSWR('isSidebarCollapsed', initialData, { fallbackData: false });
  172. };
  173. export const useCurrentSidebarContents = (initialData?: SidebarContentsType): SWRResponse<SidebarContentsType, Error> => {
  174. return useStaticSWR('sidebarContents', initialData, { fallbackData: SidebarContentsType.RECENT });
  175. };
  176. export const useCurrentProductNavWidth = (initialData?: number): SWRResponse<number, Error> => {
  177. return useStaticSWR('productNavWidth', initialData, { fallbackData: 320 });
  178. };
  179. export const useDrawerMode = (): SWRResponse<boolean, Error> => {
  180. const { data: editorMode } = useEditorMode();
  181. const { data: preferDrawerModeByUser } = usePreferDrawerModeByUser();
  182. const { data: preferDrawerModeOnEditByUser } = usePreferDrawerModeOnEditByUser();
  183. const { data: isDeviceSmallerThanMd } = useIsDeviceSmallerThanMd();
  184. const condition = editorMode != null || preferDrawerModeByUser != null || preferDrawerModeOnEditByUser != null || isDeviceSmallerThanMd != null;
  185. const calcDrawerMode: Fetcher<boolean> = (
  186. key: Key, editorMode: EditorMode, preferDrawerModeByUser: boolean, preferDrawerModeOnEditByUser: boolean, isDeviceSmallerThanMd: boolean,
  187. ): boolean => {
  188. // get preference on view or edit
  189. const preferDrawerMode = editorMode !== EditorMode.View ? preferDrawerModeOnEditByUser : preferDrawerModeByUser;
  190. return isDeviceSmallerThanMd || preferDrawerMode;
  191. };
  192. return useSWRImmutable(
  193. condition ? ['isDrawerMode', editorMode, preferDrawerModeByUser, preferDrawerModeOnEditByUser, isDeviceSmallerThanMd] : null,
  194. calcDrawerMode,
  195. {
  196. fallback: calcDrawerMode,
  197. },
  198. );
  199. };
  200. export const useDrawerOpened = (isOpened?: boolean): SWRResponse<boolean, Error> => {
  201. return useStaticSWR('isDrawerOpened', isOpened, { fallbackData: false });
  202. };
  203. export const useSidebarResizeDisabled = (isDisabled?: boolean): SWRResponse<boolean, Error> => {
  204. return useStaticSWR('isSidebarResizeDisabled', isDisabled, { fallbackData: false });
  205. };
  206. // PageCreateModal
  207. type CreateModalStatus = {
  208. isOpened: boolean,
  209. path?: string,
  210. }
  211. type CreateModalStatusUtils = {
  212. open(path?: string): Promise<CreateModalStatus | undefined>
  213. close(): Promise<CreateModalStatus | undefined>
  214. }
  215. export const useCreateModalStatus = (status?: CreateModalStatus): SWRResponse<CreateModalStatus, Error> & CreateModalStatusUtils => {
  216. const initialData: CreateModalStatus = { isOpened: false };
  217. const swrResponse = useStaticSWR<CreateModalStatus, Error>('pageCreateModalStatus', status, { fallbackData: initialData });
  218. return {
  219. ...swrResponse,
  220. open: (path?: string) => swrResponse.mutate({ isOpened: true, path }),
  221. close: () => swrResponse.mutate({ isOpened: false }),
  222. };
  223. };
  224. export const useCreateModalOpened = (): SWRResponse<boolean, Error> => {
  225. const { data } = useCreateModalStatus();
  226. return useSWR(
  227. data != null ? ['isCreaateModalOpened', data] : null,
  228. () => {
  229. return data != null ? data.isOpened : false;
  230. },
  231. );
  232. };
  233. export const useCreateModalPath = (): SWRResponse<string | null | undefined, Error> => {
  234. const { data: currentPagePath } = useCurrentPagePath();
  235. const { data: status } = useCreateModalStatus();
  236. return useSWR(
  237. currentPagePath != null && status != null ? [currentPagePath, status] : null,
  238. (currentPagePath, status) => {
  239. return status?.path || currentPagePath;
  240. },
  241. );
  242. };
  243. // PageDeleteModal
  244. export type IPageForPageDeleteModal = {
  245. pageId: string,
  246. revisionId?: string,
  247. path: string
  248. }
  249. export type OnDeletedFunction = (pathOrPaths: string | string[], isRecursively: Nullable<true>, isCompletely: Nullable<true>) => void;
  250. type DeleteModalStatus = {
  251. isOpened: boolean,
  252. pages?: IPageForPageDeleteModal[],
  253. onDeleted?: OnDeletedFunction,
  254. }
  255. type DeleteModalOpened = {
  256. isOpend: boolean,
  257. onDeleted?: OnDeletedFunction,
  258. }
  259. type DeleteModalStatusUtils = {
  260. open(pages?: IPageForPageDeleteModal[], onDeleted?: OnDeletedFunction): Promise<DeleteModalStatus | undefined>,
  261. close(): Promise<DeleteModalStatus | undefined>,
  262. }
  263. export const usePageDeleteModal = (status?: DeleteModalStatus): SWRResponse<DeleteModalStatus, Error> & DeleteModalStatusUtils => {
  264. const initialData: DeleteModalStatus = { isOpened: false };
  265. const swrResponse = useStaticSWR<DeleteModalStatus, Error>('deleteModalStatus', status, { fallbackData: initialData });
  266. return {
  267. ...swrResponse,
  268. open: (pages?: IPageForPageDeleteModal[], onDeleted?: OnDeletedFunction) => swrResponse.mutate({ isOpened: true, pages, onDeleted }),
  269. close: () => swrResponse.mutate({ isOpened: false }),
  270. };
  271. };
  272. export const usePageDeleteModalOpened = (): SWRResponse<(DeleteModalOpened | null), Error> => {
  273. const { data } = usePageDeleteModal();
  274. return useSWRImmutable(
  275. data != null ? ['isDeleteModalOpened', data] : null,
  276. () => {
  277. return data != null ? { isOpend: data.isOpened, onDeleted: data?.onDeleted } : null;
  278. },
  279. );
  280. };
  281. // PageDuplicateModal
  282. export type IPageForPageDuplicateModal = {
  283. pageId: string,
  284. path: string
  285. }
  286. type DuplicateModalStatus = {
  287. isOpened: boolean,
  288. pageId?: string,
  289. path?: string,
  290. }
  291. type DuplicateModalStatusUtils = {
  292. open(pageId: string, path: string): Promise<DuplicateModalStatus | undefined>
  293. close(): Promise<DuplicateModalStatus | undefined>
  294. }
  295. export const usePageDuplicateModalStatus = (status?: DuplicateModalStatus): SWRResponse<DuplicateModalStatus, Error> & DuplicateModalStatusUtils => {
  296. const initialData: DuplicateModalStatus = { isOpened: false, pageId: '', path: '' };
  297. const swrResponse = useStaticSWR<DuplicateModalStatus, Error>('duplicateModalStatus', status, { fallbackData: initialData });
  298. return {
  299. ...swrResponse,
  300. open: (pageId: string, path: string) => swrResponse.mutate({ isOpened: true, pageId, path }),
  301. close: () => swrResponse.mutate({ isOpened: false }),
  302. };
  303. };
  304. export const usePageDuplicateModalOpened = (): SWRResponse<boolean, Error> => {
  305. const { data } = usePageDuplicateModalStatus();
  306. return useSWRImmutable(
  307. data != null ? ['isDuplicateModalOpened', data] : null,
  308. () => {
  309. return data != null ? data.isOpened : false;
  310. },
  311. );
  312. };
  313. // PageRenameModal
  314. export type IPageForPageRenameModal = {
  315. pageId: string,
  316. revisionId: string,
  317. path: string
  318. }
  319. type RenameModalStatus = {
  320. isOpened: boolean,
  321. pageId?: string,
  322. revisionId?: string
  323. path?: string,
  324. }
  325. type RenameModalStatusUtils = {
  326. open(pageId: string, revisionId: string, path: string): Promise<RenameModalStatus | undefined>
  327. close(): Promise<RenameModalStatus | undefined>
  328. }
  329. export const usePageRenameModalStatus = (status?: RenameModalStatus): SWRResponse<RenameModalStatus, Error> & RenameModalStatusUtils => {
  330. const initialData: RenameModalStatus = {
  331. isOpened: false, pageId: '', revisionId: '', path: '',
  332. };
  333. const swrResponse = useStaticSWR<RenameModalStatus, Error>('renameModalStatus', status, { fallbackData: initialData });
  334. return {
  335. ...swrResponse,
  336. open: (pageId: string, revisionId: string, path: string) => swrResponse.mutate({
  337. isOpened: true, pageId, revisionId, path,
  338. }),
  339. close: () => swrResponse.mutate({ isOpened: false }),
  340. };
  341. };
  342. export const usePageRenameModalOpened = (): SWRResponse<boolean, Error> => {
  343. const { data } = usePageRenameModalStatus();
  344. return useSWRImmutable(
  345. data != null ? ['isRenameModalOpened', data] : null,
  346. () => {
  347. return data != null ? data.isOpened : false;
  348. },
  349. );
  350. };
  351. // PagePresentationModal
  352. type PresentationModalStatus = {
  353. isOpened: boolean,
  354. href?: string
  355. }
  356. type PresentationModalStatusUtils = {
  357. open(href: string): Promise<PresentationModalStatus | undefined>
  358. close(): Promise<PresentationModalStatus | undefined>
  359. }
  360. export const usePagePresentationModal = (
  361. status?: PresentationModalStatus,
  362. ): SWRResponse<PresentationModalStatus, Error> & PresentationModalStatusUtils => {
  363. const initialData: PresentationModalStatus = {
  364. isOpened: false, href: '?presentation=1',
  365. };
  366. const swrResponse = useStaticSWR<PresentationModalStatus, Error>('presentationModalStatus', status, { fallbackData: initialData });
  367. return {
  368. ...swrResponse,
  369. open: (href: string) => swrResponse.mutate({ isOpened: true, href }),
  370. close: () => swrResponse.mutate({ isOpened: false }),
  371. };
  372. };
  373. type DescendantsPageListModalStatus = {
  374. isOpened: boolean,
  375. path?: string,
  376. }
  377. type DescendantsPageListUtils = {
  378. open(path: string): Promise<DescendantsPageListModalStatus | undefined>
  379. close(): Promise<DuplicateModalStatus | undefined>
  380. }
  381. export const useDescendantsPageListModal = (
  382. status?: DescendantsPageListModalStatus,
  383. ): SWRResponse<DescendantsPageListModalStatus, Error> & DescendantsPageListUtils => {
  384. const initialData: DescendantsPageListModalStatus = { isOpened: false };
  385. const swrResponse = useStaticSWR<DescendantsPageListModalStatus, Error>('descendantsPageListModalStatus', status, { fallbackData: initialData });
  386. return {
  387. ...swrResponse,
  388. open: (path: string) => swrResponse.mutate({ isOpened: true, path }),
  389. close: () => swrResponse.mutate({ isOpened: false }),
  390. };
  391. };
  392. export const PageAccessoriesModalContents = {
  393. PageHistory: 'PageHistory',
  394. Attachment: 'Attachment',
  395. ShareLink: 'ShareLink',
  396. } as const;
  397. export type PageAccessoriesModalContents = typeof PageAccessoriesModalContents[keyof typeof PageAccessoriesModalContents];
  398. type PageAccessoriesModalStatus = {
  399. isOpened: boolean,
  400. onOpened?: (initialActivatedContents: PageAccessoriesModalContents) => void,
  401. }
  402. type PageAccessoriesModalUtils = {
  403. open(activatedContents: PageAccessoriesModalContents): void
  404. close(): void
  405. }
  406. export const usePageAccessoriesModal = (): SWRResponse<PageAccessoriesModalStatus, Error> & PageAccessoriesModalUtils => {
  407. const initialStatus = { isOpened: false };
  408. const swrResponse = useStaticSWR<PageAccessoriesModalStatus, Error>('pageAccessoriesModalStatus', undefined, { fallbackData: initialStatus });
  409. return {
  410. ...swrResponse,
  411. open: (activatedContents: PageAccessoriesModalContents) => {
  412. if (swrResponse.data == null) {
  413. return;
  414. }
  415. swrResponse.mutate({ isOpened: true });
  416. if (swrResponse.data.onOpened != null) {
  417. swrResponse.data.onOpened(activatedContents);
  418. }
  419. },
  420. close: () => {
  421. if (swrResponse.data == null) {
  422. return;
  423. }
  424. swrResponse.mutate({ isOpened: false });
  425. },
  426. };
  427. };
  428. export const useSelectedGrant = (initialData?: Nullable<number>): SWRResponse<Nullable<number>, Error> => {
  429. return useStaticSWR<Nullable<number>, Error>('grant', initialData);
  430. };
  431. export const useSelectedGrantGroupId = (initialData?: Nullable<string>): SWRResponse<Nullable<string>, Error> => {
  432. return useStaticSWR<Nullable<string>, Error>('grantGroupId', initialData);
  433. };
  434. export const useSelectedGrantGroupName = (initialData?: Nullable<string>): SWRResponse<Nullable<string>, Error> => {
  435. return useStaticSWR<Nullable<string>, Error>('grantGroupName', initialData);
  436. };
  437. export const useGlobalSearchFormRef = (initialData?: RefObject<IFocusable>): SWRResponse<RefObject<IFocusable>, Error> => {
  438. return useStaticSWR('globalSearchTypeahead', initialData);
  439. };
  440. export const useIsAbleToShowPageManagement = (): SWRResponse<boolean, Error> => {
  441. const key = 'isAbleToShowPageManagement';
  442. const { data: currentPageId } = useCurrentPageId();
  443. const { data: isTrashPage } = useIsTrashPage();
  444. const { data: isSharedUser } = useIsSharedUser();
  445. const includesUndefined = [currentPageId, isTrashPage, isSharedUser].some(v => v === undefined);
  446. const isPageExist = currentPageId != null;
  447. return useSWRImmutable(
  448. includesUndefined ? null : key,
  449. () => isPageExist && !isTrashPage && !isSharedUser,
  450. );
  451. };
  452. export const useIsAbleToShowTagLabel = (): SWRResponse<boolean, Error> => {
  453. const key = 'isAbleToShowTagLabel';
  454. const { data: isUserPage } = useIsUserPage();
  455. const { data: currentPagePath } = useCurrentPagePath();
  456. const { data: isIdenticalPath } = useIsIdenticalPath();
  457. const { data: notFoundTargetPathOrId } = useNotFoundTargetPathOrId();
  458. const { data: editorMode } = useEditorMode();
  459. const includesUndefined = [isUserPage, currentPagePath, isIdenticalPath, notFoundTargetPathOrId, editorMode].some(v => v === undefined);
  460. const isViewMode = editorMode === EditorMode.View;
  461. const isNotFoundPage = notFoundTargetPathOrId != null;
  462. return useSWRImmutable(
  463. includesUndefined ? null : key,
  464. // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
  465. () => !isUserPage && !isSharedPage(currentPagePath!) && !isIdenticalPath && !(isViewMode && isNotFoundPage),
  466. );
  467. };
  468. export const useIsAbleToShowPageEditorModeManager = (): SWRResponse<boolean, Error> => {
  469. const key = 'isAbleToShowPageEditorModeManager';
  470. const { data: isNotCreatable } = useIsNotCreatable();
  471. const { data: isForbidden } = useIsForbidden();
  472. const { data: isTrashPage } = useIsTrashPage();
  473. const { data: isSharedUser } = useIsSharedUser();
  474. const includesUndefined = [isNotCreatable, isForbidden, isTrashPage, isSharedUser].some(v => v === undefined);
  475. return useSWRImmutable(
  476. includesUndefined ? null : key,
  477. () => !isNotCreatable && !isForbidden && !isTrashPage && !isSharedUser,
  478. );
  479. };
  480. export const useIsAbleToShowPageAuthors = (): SWRResponse<boolean, Error> => {
  481. const key = 'isAbleToShowPageAuthors';
  482. const { data: currentPageId } = useCurrentPageId();
  483. const { data: isUserPage } = useIsUserPage();
  484. const includesUndefined = [currentPageId, isUserPage].some(v => v === undefined);
  485. const isPageExist = currentPageId != null;
  486. return useSWRImmutable(
  487. includesUndefined ? null : key,
  488. () => isPageExist && !isUserPage,
  489. );
  490. };