page-operation.ts 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208
  1. import { SubscriptionStatusType, Nullable } from '@growi/core';
  2. import urljoin from 'url-join';
  3. import { OptionsToSave } from '~/interfaces/page-operation';
  4. import { useCurrentPageId } from '~/stores/context';
  5. import { useIsEnabledUnsavedWarning, usePageTagsForEditors } from '~/stores/editor';
  6. import { useSWRxCurrentPage, useSWRxTagsInfo } from '~/stores/page';
  7. import { useSetRemoteLatestPageData } from '~/stores/remote-latest-page';
  8. import loggerFactory from '~/utils/logger';
  9. import { toastError } from '../util/apiNotification';
  10. import { apiPost } from '../util/apiv1-client';
  11. import { apiv3Post, apiv3Put } from '../util/apiv3-client';
  12. const logger = loggerFactory('growi:services:page-operation');
  13. export const toggleSubscribe = async(pageId: string, currentStatus: SubscriptionStatusType | undefined): Promise<void> => {
  14. try {
  15. const newStatus = currentStatus === SubscriptionStatusType.SUBSCRIBE
  16. ? SubscriptionStatusType.UNSUBSCRIBE
  17. : SubscriptionStatusType.SUBSCRIBE;
  18. await apiv3Put('/page/subscribe', { pageId, status: newStatus });
  19. }
  20. catch (err) {
  21. toastError(err);
  22. }
  23. };
  24. export const toggleLike = async(pageId: string, currentValue?: boolean): Promise<void> => {
  25. try {
  26. await apiv3Put('/page/likes', { pageId, bool: !currentValue });
  27. }
  28. catch (err) {
  29. toastError(err);
  30. }
  31. };
  32. export const toggleBookmark = async(pageId: string, currentValue?: boolean): Promise<void> => {
  33. try {
  34. await apiv3Put('/bookmarks', { pageId, bool: !currentValue });
  35. }
  36. catch (err) {
  37. toastError(err);
  38. }
  39. };
  40. export const updateContentWidth = async(pageId: string, newValue: boolean): Promise<void> => {
  41. try {
  42. await apiv3Put(`/page/${pageId}/content-width`, { expandContentWidth: newValue });
  43. }
  44. catch (err) {
  45. toastError(err);
  46. }
  47. };
  48. export const bookmark = async(pageId: string): Promise<void> => {
  49. try {
  50. await apiv3Put('/bookmarks', { pageId, bool: true });
  51. }
  52. catch (err) {
  53. toastError(err);
  54. }
  55. };
  56. export const unbookmark = async(pageId: string): Promise<void> => {
  57. try {
  58. await apiv3Put('/bookmarks', { pageId, bool: false });
  59. }
  60. catch (err) {
  61. toastError(err);
  62. }
  63. };
  64. export const exportAsMarkdown = (pageId: string, revisionId: string, format: string): void => {
  65. const url = new URL(urljoin(window.location.origin, '_api/v3/page/export', pageId));
  66. url.searchParams.append('format', format);
  67. url.searchParams.append('revisionId', revisionId);
  68. window.location.href = url.href;
  69. };
  70. /**
  71. * send request to fix broken paths caused by unexpected events such as server shutdown while renaming page paths
  72. */
  73. export const resumeRenameOperation = async(pageId: string): Promise<void> => {
  74. await apiv3Post('/pages/resume-rename', { pageId });
  75. };
  76. // TODO: define return type
  77. const createPage = async(pagePath: string, markdown: string, tmpParams: OptionsToSave) => {
  78. // clone
  79. const params = Object.assign(tmpParams, {
  80. path: pagePath,
  81. body: markdown,
  82. });
  83. const res = await apiv3Post('/pages/', params);
  84. const { page, tags, revision } = res.data;
  85. return { page, tags, revision };
  86. };
  87. // TODO: define return type
  88. const updatePage = async(pageId: string, revisionId: string, markdown: string, tmpParams: OptionsToSave) => {
  89. // clone
  90. const params = Object.assign(tmpParams, {
  91. page_id: pageId,
  92. revision_id: revisionId,
  93. body: markdown,
  94. });
  95. const res: any = await apiPost('/pages.update', params);
  96. if (!res.ok) {
  97. throw new Error(res.error);
  98. }
  99. return res;
  100. };
  101. type PageInfo= {
  102. path: string,
  103. pageId: Nullable<string>,
  104. revisionId: Nullable<string>,
  105. }
  106. type SaveOrUpdateFunction = (markdown: string, pageInfo: PageInfo, optionsToSave?: OptionsToSave) => any;
  107. // TODO: define return type
  108. export const useSaveOrUpdate = (): SaveOrUpdateFunction => {
  109. /* eslint-disable react-hooks/rules-of-hooks */
  110. const { mutate: mutateIsEnabledUnsavedWarning } = useIsEnabledUnsavedWarning();
  111. /* eslint-enable react-hooks/rules-of-hooks */
  112. return async function(markdown: string, pageInfo: PageInfo, optionsToSave?: OptionsToSave) {
  113. const { path, pageId, revisionId } = pageInfo;
  114. const options: OptionsToSave = Object.assign({}, optionsToSave);
  115. /*
  116. * Note: variable "markdown" will be received from params
  117. * please delete the following code after implemating HackMD editor function
  118. */
  119. // let markdown;
  120. // if (editorMode === EditorMode.HackMD) {
  121. // const pageEditorByHackmd = this.appContainer.getComponentInstance('PageEditorByHackmd');
  122. // markdown = await pageEditorByHackmd.getMarkdown();
  123. // // set option to sync
  124. // options.isSyncRevisionToHackmd = true;
  125. // revisionId = this.state.revisionIdHackmdSynced;
  126. // }
  127. // else {
  128. // const pageEditor = this.appContainer.getComponentInstance('PageEditor');
  129. // const pageEditor = getComponentInstance('PageEditor');
  130. // markdown = pageEditor.getMarkdown();
  131. // }
  132. const isNoRevisionPage = pageId != null && revisionId == null;
  133. let res;
  134. if (pageId == null || isNoRevisionPage) {
  135. res = await createPage(path, markdown, options);
  136. }
  137. else {
  138. if (revisionId == null) {
  139. const msg = '\'revisionId\' is required to update page';
  140. throw new Error(msg);
  141. }
  142. res = await updatePage(pageId, revisionId, markdown, options);
  143. }
  144. // The updateFn should be a promise or asynchronous function to handle the remote mutation
  145. // it should return updated data. see: https://swr.vercel.app/docs/mutation#optimistic-updates
  146. // Moreover, `async() => false` does not work since it's too fast to be calculated.
  147. await mutateIsEnabledUnsavedWarning(new Promise(r => setTimeout(() => r(false), 10)), { optimisticData: () => false });
  148. return res;
  149. };
  150. };
  151. export const useUpdateStateAfterSave = (pageId: string|undefined): (() => Promise<void>) | undefined => {
  152. const { mutate: mutateCurrentPageId } = useCurrentPageId();
  153. const { mutate: mutateCurrentPage } = useSWRxCurrentPage();
  154. const { setRemoteLatestPageData } = useSetRemoteLatestPageData();
  155. const { mutate: mutateTagsInfo } = useSWRxTagsInfo(pageId);
  156. const { sync: syncTagsInfoForEditor } = usePageTagsForEditors(pageId);
  157. if (pageId == null) { return }
  158. // update swr 'currentPageId', 'currentPage', remote states
  159. return async() => {
  160. await mutateCurrentPageId(pageId);
  161. const updatedPage = await mutateCurrentPage();
  162. await mutateTagsInfo(); // get from DB
  163. syncTagsInfoForEditor(); // sync global state for client
  164. if (updatedPage == null) { return }
  165. const remoterevisionData = {
  166. remoteRevisionId: updatedPage.revision._id,
  167. remoteRevisionBody: updatedPage.revision.body,
  168. remoteRevisionLastUpdateUser: updatedPage.lastUpdateUser,
  169. remoteRevisionLastUpdatedAt: updatedPage.updatedAt,
  170. revisionIdHackmdSynced: updatedPage.revisionHackmdSynced.toString(),
  171. hasDraftOnHackmd: updatedPage.hasDraftOnHackmd,
  172. };
  173. setRemoteLatestPageData(remoterevisionData);
  174. };
  175. };