page-operation.ts 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182
  1. import { useCallback } from 'react';
  2. import type { IPageHasId } from '@growi/core';
  3. import { SubscriptionStatusType } from '@growi/core';
  4. import urljoin from 'url-join';
  5. import type { SyncLatestRevisionBody } from '~/interfaces/yjs';
  6. import { useEditingMarkdown, usePageTagsForEditors } from '~/stores/editor';
  7. import {
  8. useCurrentPageId, useSWRMUTxCurrentPage, useSWRxApplicableGrant, useSWRxTagsInfo,
  9. useSWRxCurrentGrantData,
  10. } from '~/stores/page';
  11. import { useSetRemoteLatestPageData } from '~/stores/remote-latest-page';
  12. import loggerFactory from '~/utils/logger';
  13. import { apiPost } from '../util/apiv1-client';
  14. import { apiv3Get, apiv3Post, apiv3Put } from '../util/apiv3-client';
  15. import { toastError } from '../util/toastr';
  16. const logger = loggerFactory('growi:services:page-operation');
  17. export const toggleSubscribe = async(pageId: string, currentStatus: SubscriptionStatusType | undefined): Promise<void> => {
  18. try {
  19. const newStatus = currentStatus === SubscriptionStatusType.SUBSCRIBE
  20. ? SubscriptionStatusType.UNSUBSCRIBE
  21. : SubscriptionStatusType.SUBSCRIBE;
  22. await apiv3Put('/page/subscribe', { pageId, status: newStatus });
  23. }
  24. catch (err) {
  25. toastError(err);
  26. }
  27. };
  28. export const toggleLike = async(pageId: string, currentValue?: boolean): Promise<void> => {
  29. try {
  30. await apiv3Put('/page/likes', { pageId, bool: !currentValue });
  31. }
  32. catch (err) {
  33. toastError(err);
  34. }
  35. };
  36. export const toggleBookmark = async(pageId: string, currentValue?: boolean): Promise<void> => {
  37. try {
  38. await apiv3Put('/bookmarks', { pageId, bool: !currentValue });
  39. }
  40. catch (err) {
  41. toastError(err);
  42. }
  43. };
  44. export const updateContentWidth = async(pageId: string, newValue: boolean): Promise<void> => {
  45. try {
  46. await apiv3Put(`/page/${pageId}/content-width`, { expandContentWidth: newValue });
  47. }
  48. catch (err) {
  49. toastError(err);
  50. }
  51. };
  52. export const bookmark = async(pageId: string): Promise<void> => {
  53. try {
  54. await apiv3Put('/bookmarks', { pageId, bool: true });
  55. }
  56. catch (err) {
  57. toastError(err);
  58. }
  59. };
  60. export const unbookmark = async(pageId: string): Promise<void> => {
  61. try {
  62. await apiv3Put('/bookmarks', { pageId, bool: false });
  63. }
  64. catch (err) {
  65. toastError(err);
  66. }
  67. };
  68. export const exportAsMarkdown = (pageId: string, revisionId: string, format: string): void => {
  69. const url = new URL(urljoin(window.location.origin, '_api/v3/page/export', pageId));
  70. url.searchParams.append('format', format);
  71. url.searchParams.append('revisionId', revisionId);
  72. window.location.href = url.href;
  73. };
  74. /**
  75. * send request to fix broken paths caused by unexpected events such as server shutdown while renaming page paths
  76. */
  77. export const resumeRenameOperation = async(pageId: string): Promise<void> => {
  78. await apiv3Post('/pages/resume-rename', { pageId });
  79. };
  80. export type UpdateStateAfterSaveOption = {
  81. supressEditingMarkdownMutation: boolean,
  82. }
  83. export const useUpdateStateAfterSave = (pageId: string|undefined|null, opts?: UpdateStateAfterSaveOption): (() => Promise<void>) | undefined => {
  84. const { mutate: mutateCurrentPageId } = useCurrentPageId();
  85. const { trigger: mutateCurrentPage } = useSWRMUTxCurrentPage();
  86. const { setRemoteLatestPageData } = useSetRemoteLatestPageData();
  87. const { mutate: mutateTagsInfo } = useSWRxTagsInfo(pageId);
  88. const { sync: syncTagsInfoForEditor } = usePageTagsForEditors(pageId);
  89. const { mutate: mutateEditingMarkdown } = useEditingMarkdown();
  90. const { mutate: mutateCurrentGrantData } = useSWRxCurrentGrantData(pageId);
  91. const { mutate: mutateApplicableGrant } = useSWRxApplicableGrant(pageId);
  92. // update swr 'currentPageId', 'currentPage', remote states
  93. return useCallback(async() => {
  94. if (pageId == null) { return }
  95. // update tag before page: https://github.com/weseek/growi/pull/7158
  96. // !! DO NOT CHANGE THE ORDERS OF THE MUTATIONS !! -- 12.26 yuken-t
  97. await mutateTagsInfo(); // get from DB
  98. syncTagsInfoForEditor(); // sync global state for client
  99. await mutateCurrentPageId(pageId);
  100. const updatedPage = await mutateCurrentPage();
  101. if (updatedPage == null || updatedPage.revision == null) { return }
  102. // supress to mutate only when updated from built-in editor
  103. // and see: https://github.com/weseek/growi/pull/7118
  104. const supressEditingMarkdownMutation = opts?.supressEditingMarkdownMutation ?? false;
  105. if (!supressEditingMarkdownMutation) {
  106. mutateEditingMarkdown(updatedPage.revision.body);
  107. }
  108. mutateCurrentGrantData();
  109. mutateApplicableGrant();
  110. const remoterevisionData = {
  111. remoteRevisionId: updatedPage.revision._id,
  112. remoteRevisionBody: updatedPage.revision.body,
  113. remoteRevisionLastUpdateUser: updatedPage.lastUpdateUser,
  114. remoteRevisionLastUpdatedAt: updatedPage.updatedAt,
  115. };
  116. setRemoteLatestPageData(remoterevisionData);
  117. },
  118. // eslint-disable-next-line max-len
  119. [pageId, mutateTagsInfo, syncTagsInfoForEditor, mutateCurrentPageId, mutateCurrentPage, opts?.supressEditingMarkdownMutation, mutateCurrentGrantData, mutateApplicableGrant, setRemoteLatestPageData, mutateEditingMarkdown]);
  120. };
  121. export const unlink = async(path: string): Promise<void> => {
  122. await apiPost('/pages.unlink', { path });
  123. };
  124. interface PageExistResponse {
  125. isExist: boolean,
  126. }
  127. export const exist = async(path: string): Promise<PageExistResponse> => {
  128. const res = await apiv3Get<PageExistResponse>('/page/exist', { path });
  129. return res.data;
  130. };
  131. interface NonUserRelatedGroupsGrantedResponse {
  132. isNonUserRelatedGroupsGranted: boolean,
  133. }
  134. export const getIsNonUserRelatedGroupsGranted = async(path: string): Promise<NonUserRelatedGroupsGrantedResponse> => {
  135. const res = await apiv3Get<NonUserRelatedGroupsGrantedResponse>('/page/non-user-related-groups-granted', { path });
  136. return res.data;
  137. };
  138. export const publish = async(pageId: string): Promise<IPageHasId> => {
  139. const res = await apiv3Put(`/page/${pageId}/publish`);
  140. return res.data;
  141. };
  142. export const unpublish = async(pageId: string): Promise<IPageHasId> => {
  143. const res = await apiv3Put(`/page/${pageId}/unpublish`);
  144. return res.data;
  145. };
  146. export const syncLatestRevisionBody = async(pageId: string, editingMarkdownLength?: number): Promise<SyncLatestRevisionBody> => {
  147. const res = await apiv3Put(`/page/${pageId}/sync-latest-revision-body-to-yjs-draft`, { editingMarkdownLength });
  148. return res.data;
  149. };