Page.tsx 7.8 KB

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