Page.tsx 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220
  1. import React, {
  2. useCallback,
  3. useEffect, useRef,
  4. } from 'react';
  5. import EventEmitter from 'events';
  6. import { DrawioEditByViewerProps } from '@growi/remark-drawio-plugin';
  7. import { useTranslation } from 'next-i18next';
  8. import dynamic from 'next/dynamic';
  9. import { HtmlElementNode } from 'rehype-toc';
  10. import MarkdownTable from '~/client/models/MarkdownTable';
  11. import { useSaveOrUpdate } from '~/client/services/page-operation';
  12. import { toastSuccess, toastError } from '~/client/util/apiNotification';
  13. import { OptionsToSave } from '~/interfaces/page-operation';
  14. import {
  15. useIsGuestUser, useShareLinkId,
  16. } from '~/stores/context';
  17. import { useEditingMarkdown } from '~/stores/editor';
  18. import { useDrawioModal, useHandsontableModal } from '~/stores/modal';
  19. import { useSWRxCurrentPage, useSWRxTagsInfo } from '~/stores/page';
  20. import { useViewOptions } from '~/stores/renderer';
  21. import {
  22. useCurrentPageTocNode,
  23. useIsMobile,
  24. } from '~/stores/ui';
  25. import loggerFactory from '~/utils/logger';
  26. import RevisionRenderer from './Page/RevisionRenderer';
  27. import mdu from './PageEditor/MarkdownDrawioUtil';
  28. import mtu from './PageEditor/MarkdownTableUtil';
  29. declare global {
  30. // eslint-disable-next-line vars-on-top, no-var
  31. var globalEmitter: EventEmitter;
  32. }
  33. // const DrawioModal = dynamic(() => import('./PageEditor/DrawioModal'), { ssr: false });
  34. const GridEditModal = dynamic(() => import('./PageEditor/GridEditModal'), { ssr: false });
  35. const LinkEditModal = dynamic(() => import('./PageEditor/LinkEditModal'), { ssr: false });
  36. const logger = loggerFactory('growi:Page');
  37. export const Page = (props) => {
  38. const { t } = useTranslation();
  39. // Pass tocRef to generateViewOptions (=> rehypePlugin => customizeTOC) to call mutateCurrentPageTocNode when tocRef.current changes.
  40. // The toc node passed by customizeTOC is assigned to tocRef.current.
  41. const tocRef = useRef<HtmlElementNode>();
  42. const storeTocNodeHandler = useCallback((toc: HtmlElementNode) => {
  43. tocRef.current = toc;
  44. }, []);
  45. const { data: shareLinkId } = useShareLinkId();
  46. const { data: currentPage, mutate: mutateCurrentPage } = useSWRxCurrentPage(shareLinkId ?? undefined);
  47. const { mutate: mutateEditingMarkdown } = useEditingMarkdown();
  48. const { data: tagsInfo } = useSWRxTagsInfo(currentPage?._id);
  49. const { data: isGuestUser } = useIsGuestUser();
  50. const { data: isMobile } = useIsMobile();
  51. const { data: rendererOptions } = useViewOptions(storeTocNodeHandler);
  52. const { mutate: mutateCurrentPageTocNode } = useCurrentPageTocNode();
  53. const { open: openDrawioModal } = useDrawioModal();
  54. const { open: openHandsontableModal } = useHandsontableModal();
  55. const saveOrUpdate = useSaveOrUpdate();
  56. useEffect(() => {
  57. mutateCurrentPageTocNode(tocRef.current);
  58. // eslint-disable-next-line react-hooks/exhaustive-deps
  59. }, [mutateCurrentPageTocNode, tocRef.current]); // include tocRef.current to call mutateCurrentPageTocNode when tocRef.current changes
  60. const saveByDrawioModal = useCallback(async(drawioMxFile: string, bol: number, eol: number) => {
  61. if (currentPage == null || tagsInfo == null) {
  62. return;
  63. }
  64. // disable if share link
  65. if (shareLinkId != null) {
  66. return;
  67. }
  68. const currentMarkdown = currentPage.revision.body;
  69. const optionsToSave: OptionsToSave = {
  70. isSlackEnabled: false,
  71. slackChannels: '',
  72. grant: currentPage.grant,
  73. grantUserGroupId: currentPage.grantedGroup?._id,
  74. grantUserGroupName: currentPage.grantedGroup?.name,
  75. pageTags: tagsInfo.tags,
  76. };
  77. const newMarkdown = mdu.replaceDrawioInMarkdown(drawioMxFile, currentMarkdown, bol, eol);
  78. try {
  79. const currentRevisionId = currentPage.revision._id;
  80. await saveOrUpdate(
  81. newMarkdown,
  82. { pageId: currentPage._id, path: currentPage.path, revisionId: currentRevisionId },
  83. optionsToSave,
  84. );
  85. toastSuccess(t('toaster.save_succeeded'));
  86. // rerender
  87. mutateCurrentPage();
  88. mutateEditingMarkdown(newMarkdown);
  89. }
  90. catch (error) {
  91. logger.error('failed to save', error);
  92. toastError(error);
  93. }
  94. }, [currentPage, mutateCurrentPage, mutateEditingMarkdown, saveOrUpdate, shareLinkId, t, tagsInfo]);
  95. // set handler to open DrawioModal
  96. useEffect(() => {
  97. // disable if share link
  98. if (shareLinkId != null) {
  99. return;
  100. }
  101. const handler = (data: DrawioEditByViewerProps) => {
  102. openDrawioModal(data.drawioMxFile, drawioMxFile => saveByDrawioModal(drawioMxFile, data.bol, data.eol));
  103. };
  104. globalEmitter.on('launchDrawioModal', handler);
  105. return function cleanup() {
  106. globalEmitter.removeListener('launchDrawioModal', handler);
  107. };
  108. }, [openDrawioModal, saveByDrawioModal, shareLinkId]);
  109. const saveByHandsontableModal = useCallback(async(table: MarkdownTable, bol: number, eol: number) => {
  110. if (currentPage == null || tagsInfo == null) {
  111. return;
  112. }
  113. const currentMarkdown = currentPage.revision.body;
  114. const optionsToSave: OptionsToSave = {
  115. isSlackEnabled: false,
  116. slackChannels: '',
  117. grant: currentPage.grant,
  118. grantUserGroupId: currentPage.grantedGroup?._id,
  119. grantUserGroupName: currentPage.grantedGroup?.name,
  120. pageTags: tagsInfo.tags,
  121. };
  122. const newMarkdown = mtu.replaceMarkdownTableInMarkdown(table, currentMarkdown, bol, eol);
  123. try {
  124. const currentRevisionId = currentPage.revision._id;
  125. await saveOrUpdate(
  126. newMarkdown,
  127. { pageId: currentPage._id, path: currentPage.path, revisionId: currentRevisionId },
  128. optionsToSave,
  129. );
  130. toastSuccess(t('toaster.save_succeeded'));
  131. // rerender
  132. mutateCurrentPage();
  133. mutateEditingMarkdown(newMarkdown);
  134. }
  135. catch (error) {
  136. logger.error('failed to save', error);
  137. toastError(error);
  138. }
  139. }, [currentPage, mutateCurrentPage, mutateEditingMarkdown, saveOrUpdate, t, tagsInfo]);
  140. // set handler to open HandsonTableModal
  141. useEffect(() => {
  142. const handler = (bol: number, eol: number) => {
  143. if (currentPage == null) {
  144. return;
  145. }
  146. const markdown = currentPage.revision.body;
  147. const table = mtu.getMarkdownTableFromLine(markdown, bol, eol);
  148. openHandsontableModal(table, undefined, false, table => saveByHandsontableModal(table, bol, eol));
  149. };
  150. globalEmitter.on('launchHandsonTableModal', handler);
  151. return function cleanup() {
  152. globalEmitter.removeListener('launchHandsonTableModal', handler);
  153. };
  154. }, [currentPage, openHandsontableModal, saveByHandsontableModal]);
  155. if (currentPage == null || isGuestUser == null || rendererOptions == null) {
  156. const entries = Object.entries({
  157. currentPage, isGuestUser, rendererOptions,
  158. })
  159. .map(([key, value]) => [key, value == null ? 'null' : undefined])
  160. .filter(([, value]) => value != null);
  161. logger.warn('Some of materials are missing.', Object.fromEntries(entries));
  162. return null;
  163. }
  164. const { _id: revisionId, body: markdown } = currentPage.revision;
  165. return (
  166. <div className={`mb-5 ${isMobile ? 'page-mobile' : ''}`}>
  167. { revisionId != null && (
  168. <RevisionRenderer rendererOptions={rendererOptions} markdown={markdown} />
  169. )}
  170. { !isGuestUser && (
  171. <>
  172. <GridEditModal />
  173. <LinkEditModal />
  174. </>
  175. )}
  176. </div>
  177. );
  178. };