Page.tsx 7.4 KB

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