page.tsx 9.3 KB

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