Page.tsx 8.1 KB

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