page.tsx 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294
  1. import { useEffect, useMemo } from 'react';
  2. import type {
  3. Ref, Nullable,
  4. IPageInfoForEntity, IPagePopulatedToShowRevision,
  5. SWRInfinitePageRevisionsResponse,
  6. IPageInfo, IPageInfoForOperation,
  7. IRevision, IRevisionHasId,
  8. } from '@growi/core';
  9. import { useSWRStatic } from '@growi/core/dist/swr';
  10. import { isClient, pagePathUtils } from '@growi/core/dist/utils';
  11. import useSWR, {
  12. mutate, useSWRConfig, type SWRResponse, type SWRConfiguration,
  13. } from 'swr';
  14. import useSWRImmutable from 'swr/immutable';
  15. import useSWRInfinite, { type SWRInfiniteResponse } from 'swr/infinite';
  16. import useSWRMutation, { type SWRMutationResponse } from 'swr/mutation';
  17. import { apiGet } from '~/client/util/apiv1-client';
  18. import { apiv3Get } from '~/client/util/apiv3-client';
  19. import type { IRecordApplicableGrant, IResIsGrantNormalized } from '~/interfaces/page-grant';
  20. import type { AxiosResponse } from '~/utils/axios';
  21. import type { IPageTagsInfo } from '../interfaces/tag';
  22. import {
  23. useCurrentPathname, useShareLinkId, useIsGuestUser, useIsReadOnlyUser,
  24. } from './context';
  25. const { isPermalink: _isPermalink } = pagePathUtils;
  26. export const useCurrentPageId = (initialData?: Nullable<string>): SWRResponse<Nullable<string>, Error> => {
  27. return useSWRStatic<Nullable<string>, Error>('currentPageId', initialData);
  28. };
  29. export const useIsLatestRevision = (initialData?: boolean): SWRResponse<boolean, any> => {
  30. return useSWRStatic('isLatestRevision', initialData);
  31. };
  32. export const useIsNotFound = (initialData?: boolean): SWRResponse<boolean, Error> => {
  33. return useSWRStatic<boolean, Error>('isNotFound', initialData, { fallbackData: false });
  34. };
  35. export const useTemplateTagData = (initialData?: string[]): SWRResponse<string[], Error> => {
  36. return useSWRStatic<string[], Error>('templateTagData', initialData);
  37. };
  38. export const useTemplateBodyData = (initialData?: string): SWRResponse<string, Error> => {
  39. return useSWRStatic<string, Error>('templateBodyData', initialData);
  40. };
  41. /** "useSWRxCurrentPage" is intended for initial data retrieval only. Use "useSWRMUTxCurrentPage" for revalidation */
  42. export const useSWRxCurrentPage = (initialData?: IPagePopulatedToShowRevision|null): SWRResponse<IPagePopulatedToShowRevision|null> => {
  43. const key = 'currentPage';
  44. const { cache } = useSWRConfig();
  45. const shouldMutate = initialData?._id !== cache.get(key)?.data?._id && initialData !== undefined;
  46. useEffect(() => {
  47. if (shouldMutate) {
  48. mutate(key, initialData, {
  49. optimisticData: initialData,
  50. populateCache: true,
  51. revalidate: false,
  52. });
  53. }
  54. }, [initialData, key, shouldMutate]);
  55. return useSWR(key, null, {
  56. keepPreviousData: true,
  57. });
  58. };
  59. const getPageApiErrorHandler = (errs: AxiosResponse[]) => {
  60. if (!Array.isArray(errs)) { throw Error('error is not array') }
  61. const statusCode = errs[0].status;
  62. if (statusCode === 403 || statusCode === 404) {
  63. // for NotFoundPage
  64. return null;
  65. }
  66. throw Error('failed to get page');
  67. };
  68. export const useSWRMUTxCurrentPage = (): SWRMutationResponse<IPagePopulatedToShowRevision|null> => {
  69. const key = 'currentPage';
  70. const { data: currentPageId } = useCurrentPageId();
  71. const { data: shareLinkId } = useShareLinkId();
  72. // Get URL parameter for specific revisionId
  73. let revisionId: string|undefined;
  74. if (isClient()) {
  75. const urlParams = new URLSearchParams(window.location.search);
  76. const requestRevisionId = urlParams.get('revisionId');
  77. revisionId = requestRevisionId != null ? requestRevisionId : undefined;
  78. }
  79. return useSWRMutation(
  80. key,
  81. () => apiv3Get<{ page: IPagePopulatedToShowRevision }>('/page', { pageId: currentPageId, shareLinkId, revisionId })
  82. .then(result => result.data.page)
  83. .catch(getPageApiErrorHandler),
  84. {
  85. populateCache: true,
  86. revalidate: false,
  87. },
  88. );
  89. };
  90. export const useSWRxPageByPath = (path?: string, config?: SWRConfiguration): SWRResponse<IPagePopulatedToShowRevision|null, Error> => {
  91. return useSWR(
  92. path != null ? ['/page', path] : null,
  93. ([endpoint, path]) => apiv3Get<{ page: IPagePopulatedToShowRevision }>(endpoint, { path })
  94. .then(result => result.data.page)
  95. .catch(getPageApiErrorHandler),
  96. {
  97. ...config,
  98. keepPreviousData: true,
  99. revalidateOnFocus: false,
  100. revalidateOnReconnect: false,
  101. },
  102. );
  103. };
  104. export const useSWRxTagsInfo = (pageId: Nullable<string>): SWRResponse<IPageTagsInfo | undefined, Error> => {
  105. const { data: shareLinkId } = useShareLinkId();
  106. const endpoint = `/pages.getPageTag?pageId=${pageId}`;
  107. return useSWRImmutable(
  108. shareLinkId == null && pageId != null ? [endpoint, pageId] : null,
  109. ([endpoint, pageId]) => apiGet<IPageTagsInfo>(endpoint, { pageId }).then(result => result),
  110. );
  111. };
  112. export const mutateAllPageInfo = (): Promise<void[]> => {
  113. return mutate(
  114. key => Array.isArray(key) && key[0] === '/page/info',
  115. );
  116. };
  117. export const useSWRxPageInfo = (
  118. pageId: string | null | undefined,
  119. shareLinkId?: string | null,
  120. initialData?: IPageInfoForEntity,
  121. ): SWRResponse<IPageInfo | IPageInfoForOperation> => {
  122. // assign null if shareLinkId is undefined in order to identify SWR key only by pageId
  123. const fixedShareLinkId = shareLinkId ?? null;
  124. const key = useMemo(() => {
  125. return pageId != null ? ['/page/info', pageId, fixedShareLinkId] : null;
  126. }, [fixedShareLinkId, pageId]);
  127. const swrResult = useSWRImmutable(
  128. key,
  129. ([endpoint, pageId, shareLinkId]: [string, string, string|null]) => apiv3Get(endpoint, { pageId, shareLinkId }).then(response => response.data),
  130. { fallbackData: initialData },
  131. );
  132. useEffect(() => {
  133. if (initialData !== undefined) {
  134. mutate(key, initialData, {
  135. optimisticData: initialData,
  136. populateCache: true,
  137. revalidate: false,
  138. });
  139. }
  140. }, [initialData, key]);
  141. return swrResult;
  142. };
  143. export const useSWRMUTxPageInfo = (
  144. pageId: string | null | undefined,
  145. shareLinkId?: string | null,
  146. ): SWRMutationResponse<IPageInfo | IPageInfoForOperation> => {
  147. // assign null if shareLinkId is undefined in order to identify SWR key only by pageId
  148. const fixedShareLinkId = shareLinkId ?? null;
  149. const key = useMemo(() => {
  150. return pageId != null ? ['/page/info', pageId, fixedShareLinkId] : null;
  151. }, [fixedShareLinkId, pageId]);
  152. return useSWRMutation(
  153. key,
  154. ([endpoint, pageId, shareLinkId]: [string, string, string|null]) => apiv3Get(endpoint, { pageId, shareLinkId }).then(response => response.data),
  155. );
  156. };
  157. export const useSWRxPageRevision = (pageId: string, revisionId: Ref<IRevision>): SWRResponse<IRevisionHasId> => {
  158. const key = [`/revisions/${revisionId}`, pageId, revisionId];
  159. return useSWRImmutable(
  160. key,
  161. () => apiv3Get<{ revision: IRevisionHasId }>(`/revisions/${revisionId}`, { pageId }).then(response => response.data.revision),
  162. );
  163. };
  164. /*
  165. * SWR Infinite for page revision list
  166. */
  167. export const useSWRxInfinitePageRevisions = (
  168. pageId: string,
  169. limit: number,
  170. ): SWRInfiniteResponse<SWRInfinitePageRevisionsResponse, Error> => {
  171. return useSWRInfinite(
  172. (pageIndex, previousRevisionData) => {
  173. if (previousRevisionData != null && previousRevisionData.revisions.length === 0) return null;
  174. if (pageIndex === 0 || previousRevisionData == null) {
  175. return ['/revisions/list', pageId, undefined, limit];
  176. }
  177. const offset = previousRevisionData.offset + limit;
  178. return ['/revisions/list', pageId, offset, limit];
  179. },
  180. ([endpoint, pageId, offset, limit]) => apiv3Get<SWRInfinitePageRevisionsResponse>(endpoint, { pageId, offset, limit }).then(response => response.data),
  181. {
  182. revalidateFirstPage: true,
  183. revalidateAll: false,
  184. },
  185. );
  186. };
  187. /*
  188. * Grant normalization fetching hooks
  189. */
  190. export const useSWRxIsGrantNormalized = (
  191. pageId: string | null | undefined,
  192. ): SWRResponse<IResIsGrantNormalized, Error> => {
  193. const { data: isGuestUser } = useIsGuestUser();
  194. const { data: isReadOnlyUser } = useIsReadOnlyUser();
  195. const { data: isNotFound } = useIsNotFound();
  196. const key = !isGuestUser && !isReadOnlyUser && !isNotFound && pageId != null
  197. ? ['/page/is-grant-normalized', pageId]
  198. : null;
  199. return useSWRImmutable(
  200. key,
  201. ([endpoint, pageId]) => apiv3Get(endpoint, { pageId }).then(response => response.data),
  202. );
  203. };
  204. export const useSWRxApplicableGrant = (
  205. pageId: string | null | undefined,
  206. ): SWRResponse<IRecordApplicableGrant, Error> => {
  207. return useSWRImmutable(
  208. pageId != null ? ['/page/applicable-grant', pageId] : null,
  209. ([endpoint, pageId]) => apiv3Get(endpoint, { pageId }).then(response => response.data),
  210. );
  211. };
  212. /** **********************************************************
  213. * Computed states
  214. *********************************************************** */
  215. export const useCurrentPagePath = (): SWRResponse<string | undefined, Error> => {
  216. const { data: currentPage } = useSWRxCurrentPage();
  217. const { data: currentPathname } = useCurrentPathname();
  218. return useSWRImmutable(
  219. ['currentPagePath', currentPage?.path, currentPathname],
  220. ([, , pathname]) => {
  221. if (currentPage?.path != null) {
  222. return currentPage.path;
  223. }
  224. if (pathname != null && !_isPermalink(pathname)) {
  225. return pathname;
  226. }
  227. return undefined;
  228. },
  229. // TODO: set fallbackData
  230. // { fallbackData: }
  231. );
  232. };
  233. export const useIsTrashPage = (): SWRResponse<boolean, Error> => {
  234. const { data: pagePath } = useCurrentPagePath();
  235. return useSWRImmutable(
  236. pagePath == null ? null : ['isTrashPage', pagePath],
  237. ([, pagePath]) => pagePathUtils.isTrashPage(pagePath),
  238. // TODO: set fallbackData
  239. // { fallbackData: }
  240. );
  241. };