Просмотр исходного кода

Merge pull request #7626 from weseek/feat/121093-make-LinkEditModal-global-with-swr

feat: 121093 make link edit modal global with swr
Ryoji Shimizu 2 лет назад
Родитель
Сommit
693345fe08

+ 21 - 13
apps/app/src/components/PageEditor/CodeMirrorEditor.jsx

@@ -9,7 +9,9 @@ import { throttle, debounce } from 'throttle-debounce';
 import urljoin from 'url-join';
 
 import InterceptorManager from '~/services/interceptor-manager';
-import { useHandsontableModal, useDrawioModal, useTemplateModal } from '~/stores/modal';
+import {
+  useHandsontableModal, useDrawioModal, useTemplateModal, useLinkEditModal,
+} from '~/stores/modal';
 import loggerFactory from '~/utils/logger';
 
 import { UncontrolledCodeMirror } from '../UncontrolledCodeMirror';
@@ -22,7 +24,6 @@ import EmojiPickerHelper from './EmojiPickerHelper';
 import GridEditModal from './GridEditModal';
 // TODO: re-impl with https://redmine.weseek.co.jp/issues/107248
 // import geu from './GridEditorUtil';
-import { LinkEditModal } from './LinkEditModal';
 import mdu from './MarkdownDrawioUtil';
 import markdownLinkUtil from './MarkdownLinkUtil';
 import markdownListUtil from './MarkdownListUtil';
@@ -149,13 +150,13 @@ class CodeMirrorEditor extends AbstractEditor {
     this.makeHeaderHandler = this.makeHeaderHandler.bind(this);
     // TODO: re-impl with https://redmine.weseek.co.jp/issues/107248
     // this.showGridEditorHandler = this.showGridEditorHandler.bind(this);
-    this.showLinkEditHandler = this.showLinkEditHandler.bind(this);
 
     this.foldDrawioSection = this.foldDrawioSection.bind(this);
     this.clickDrawioIconHandler = this.clickDrawioIconHandler.bind(this);
     this.clickTableIconHandler = this.clickTableIconHandler.bind(this);
 
     this.showTemplateModal = this.showTemplateModal.bind(this);
+    this.showLinkEditModal = this.showLinkEditModal.bind(this);
 
   }
 
@@ -846,15 +847,21 @@ class CodeMirrorEditor extends AbstractEditor {
   //   this.gridEditModal.current.show(geu.getGridHtml(this.getCodeMirror()));
   // }
 
-  showLinkEditHandler() {
-    this.linkEditModal.current.show(markdownLinkUtil.getMarkdownLink(this.getCodeMirror()));
-  }
-
   showTemplateModal() {
     const onSubmit = templateText => this.setValue(templateText);
     this.props.onClickTemplateBtn(onSubmit);
   }
 
+  showLinkEditModal() {
+    const onSubmit = (linkText) => {
+      return markdownLinkUtil.replaceFocusedMarkdownLinkWithEditor(this.getCodeMirror(), linkText);
+    };
+
+    const defaultMarkdownLink = markdownLinkUtil.getMarkdownLink(this.getCodeMirror());
+
+    this.props.onClickLinkEditBtn(defaultMarkdownLink, onSubmit);
+  }
+
   // fold draw.io section (``` drawio ~ ```)
   foldDrawioSection() {
     const editor = this.getCodeMirror();
@@ -985,7 +992,7 @@ class CodeMirrorEditor extends AbstractEditor {
         color={null}
         size="sm"
         title="Link"
-        onClick={this.showLinkEditHandler}
+        onClick={this.showLinkEditModal}
       >
         <EditorIcon icon="Link" />
       </Button>,
@@ -1125,11 +1132,6 @@ class CodeMirrorEditor extends AbstractEditor {
           onSave={(grid) => { return geu.replaceGridWithHtmlWithEditor(this.getCodeMirror(), grid) }}
         />
          */}
-
-        <LinkEditModal
-          ref={this.linkEditModal}
-          onSave={(linkText) => { return markdownLinkUtil.replaceFocusedMarkdownLinkWithEditor(this.getCodeMirror(), linkText) }}
-        />
       </div>
     );
   }
@@ -1154,6 +1156,7 @@ const CodeMirrorEditorFc = React.forwardRef((props, ref) => {
   const { open: openDrawioModal } = useDrawioModal();
   const { open: openHandsontableModal } = useHandsontableModal();
   const { open: openTemplateModal } = useTemplateModal();
+  const { open: openLinkEditModal } = useLinkEditModal();
 
   const openDrawioModalHandler = useCallback((drawioMxFile, onSave) => {
     openDrawioModal(drawioMxFile, onSave);
@@ -1167,12 +1170,17 @@ const CodeMirrorEditorFc = React.forwardRef((props, ref) => {
     openTemplateModal(onSubmit);
   }, [openTemplateModal]);
 
+  const openLinkEditModalHandler = useCallback((defaultMarkdownLink, onSubmit) => {
+    openLinkEditModal(defaultMarkdownLink, onSubmit);
+  }, [openLinkEditModal]);
+
   return (
     <CodeMirrorEditorMemoized
       ref={ref}
       onClickDrawioBtn={openDrawioModalHandler}
       onClickTableBtn={openTableModalHandler}
       onClickTemplateBtn={openTemplateModalHandler}
+      onClickLinkEditBtn={openLinkEditModalHandler}
       {...props}
     />
   );

+ 23 - 35
apps/app/src/components/PageEditor/LinkEditModal.tsx

@@ -1,4 +1,4 @@
-import React, { forwardRef, useImperativeHandle, useState } from 'react';
+import React, { useEffect, useState, useCallback } from 'react';
 
 import path from 'path';
 
@@ -16,6 +16,7 @@ import validator from 'validator';
 
 import Linker from '~/client/models/Linker';
 import { apiv3Get } from '~/client/util/apiv3-client';
+import { useLinkEditModal } from '~/stores/modal';
 import { useCurrentPagePath } from '~/stores/page';
 import { usePreviewOptions } from '~/stores/renderer';
 import loggerFactory from '~/utils/logger';
@@ -31,23 +32,12 @@ import styles from './LinkEditPreview.module.scss';
 
 const logger = loggerFactory('growi:components:LinkEditModal');
 
-type Props = {
-  onSave: (linkText: string) => void
-}
-
-export const LinkEditModal = forwardRef((props: Props, ref): JSX.Element => {
+export const LinkEditModal = (): JSX.Element => {
   const { t } = useTranslation();
   const { data: currentPath } = useCurrentPagePath();
   const { data: rendererOptions } = usePreviewOptions();
+  const { data: linkEditModalStatus, close } = useLinkEditModal();
 
-  useImperativeHandle(ref, () => ({
-    show: (defaultMarkdownLink: Linker) => {
-      // eslint-disable-next-line @typescript-eslint/no-use-before-define
-      show(defaultMarkdownLink);
-    },
-  }));
-
-  const [isOpen, setIsOpen] = useState<boolean>(false);
   const [isUseRelativePath, setIsUseRelativePath] = useState<boolean>(false);
   const [isUsePermanentLink, setIsUsePermanentLink] = useState<boolean>(false);
   const [linkInputValue, setLinkInputValue] = useState<string>('');
@@ -59,11 +49,11 @@ export const LinkEditModal = forwardRef((props: Props, ref): JSX.Element => {
   const [permalink, setPermalink] = useState<string>('');
   const [isPreviewOpen, setIsPreviewOpen] = useState<boolean>(false);
 
-  const getRootPath = (type: string) => {
+  const getRootPath = useCallback((type: string) => {
     // rootPaths of md link and pukiwiki link are different
     if (currentPath == null) return '';
     return type === Linker.types.markdownLink ? path.dirname(currentPath) : currentPath;
-  };
+  }, [currentPath]);
 
   // parse link, link is ...
   // case-1. url of this growi's page (ex. 'http://localhost:3000/hoge/fuga')
@@ -71,7 +61,7 @@ export const LinkEditModal = forwardRef((props: Props, ref): JSX.Element => {
   // case-3. relative path of this growi's page (ex. '../fuga', 'hoge')
   // case-4. external link (ex. 'https://growi.org')
   // case-5. the others (ex. '')
-  const parseLinkAndSetState = (link: string, type: string) => {
+  const parseLinkAndSetState = useCallback((link: string, type: string) => {
     // create url from link, add dummy origin if link is not valid url.
     // ex-1. link = 'https://growi.org/' -> url = 'https://growi.org/' (case-1,4)
     // ex-2. link = 'hoge' -> url = 'http://example.com/hoge' (case-2,3,5)
@@ -100,25 +90,20 @@ export const LinkEditModal = forwardRef((props: Props, ref): JSX.Element => {
 
     setLinkInputValue(reshapedLink);
     setIsUseRelativePath(isUseRelativePath);
-  };
+  }, [getRootPath]);
 
-  const show = (defaultMarkdownLink: Linker) => {
-    // if defaultMarkdownLink is null, set default value in inputs.
-    const { label = '', link = '' } = defaultMarkdownLink;
-    const { type = Linker.types.markdownLink } = defaultMarkdownLink;
+  useEffect(() => {
+    if (linkEditModalStatus == null) { return }
+    const { label = '', link = '' } = linkEditModalStatus.defaultMarkdownLink ?? {};
+    const { type = Linker.types.markdownLink } = linkEditModalStatus.defaultMarkdownLink ?? {};
 
     parseLinkAndSetState(link, type);
-
-    setIsOpen(true);
     setLabelInputValue(label);
     setIsUsePermanentLink(false);
     setPermalink('');
     setLinkerType(type);
-  };
 
-  const hide = () => {
-    setIsOpen(false);
-  };
+  }, [linkEditModalStatus, parseLinkAndSetState]);
 
   const toggleIsUseRelativePath = () => {
     if (!linkInputValue.startsWith('/') || linkerType === Linker.types.growiLink) {
@@ -238,11 +223,11 @@ export const LinkEditModal = forwardRef((props: Props, ref): JSX.Element => {
   const save = () => {
     const linker = generateLink();
 
-    if (props.onSave != null) {
-      props.onSave(linker.generateMarkdownText() ?? '');
+    if (linkEditModalStatus?.onSave != null) {
+      linkEditModalStatus.onSave(linker.generateMarkdownText() ?? '');
     }
 
-    hide();
+    close();
   };
 
   const toggleIsPreviewOpen = async() => {
@@ -347,10 +332,13 @@ export const LinkEditModal = forwardRef((props: Props, ref): JSX.Element => {
     );
   };
 
+  if (linkEditModalStatus == null) {
+    return <></>;
+  }
 
   return (
-    <Modal className="link-edit-modal" isOpen={isOpen} toggle={hide} size="lg" autoFocus={false}>
-      <ModalHeader tag="h4" toggle={hide} className="bg-primary text-light">
+    <Modal className="link-edit-modal" isOpen={linkEditModalStatus.isOpened} toggle={close} size="lg" autoFocus={false}>
+      <ModalHeader tag="h4" toggle={close} className="bg-primary text-light">
         {t('link_edit.edit_link')}
       </ModalHeader>
 
@@ -370,7 +358,7 @@ export const LinkEditModal = forwardRef((props: Props, ref): JSX.Element => {
       </ModalBody>
       <ModalFooter>
         { previewError && <span className='text-danger'>{previewError}</span>}
-        <button type="button" className="btn btn-sm btn-outline-secondary mx-1" onClick={hide}>
+        <button type="button" className="btn btn-sm btn-outline-secondary mx-1" onClick={close}>
           {t('Cancel')}
         </button>
         <button type="submit" className="btn btn-sm btn-primary mx-1" onClick={save}>
@@ -379,6 +367,6 @@ export const LinkEditModal = forwardRef((props: Props, ref): JSX.Element => {
       </ModalFooter>
     </Modal>
   );
-});
+};
 
 LinkEditModal.displayName = 'LinkEditModal';

+ 2 - 0
apps/app/src/pages/[[...path]].page.tsx

@@ -76,6 +76,7 @@ const GrowiSubNavigationSwitcher = dynamic<GrowiSubNavigationSwitcherProps>(() =
 const DrawioModal = dynamic(() => import('../components/PageEditor/DrawioModal').then(mod => mod.DrawioModal), { ssr: false });
 const HandsontableModal = dynamic(() => import('../components/PageEditor/HandsontableModal').then(mod => mod.HandsontableModal), { ssr: false });
 const TemplateModal = dynamic(() => import('../components/TemplateModal').then(mod => mod.TemplateModal), { ssr: false });
+const LinkEditModal = dynamic(() => import('../components/PageEditor/LinkEditModal').then(mod => mod.LinkEditModal), { ssr: false });
 const PageStatusAlert = dynamic(() => import('../components/PageStatusAlert').then(mod => mod.PageStatusAlert), { ssr: false });
 
 const logger = loggerFactory('growi:pages:all');
@@ -379,6 +380,7 @@ Page.getLayout = function getLayout(page: React.ReactElement<Props>) {
       <DrawioModal />
       <HandsontableModal />
       <TemplateModal />
+      <LinkEditModal />
     </>
   );
 };

+ 30 - 0
apps/app/src/stores/modal.tsx

@@ -2,6 +2,7 @@ import { useCallback, useMemo } from 'react';
 
 import { SWRResponse } from 'swr';
 
+import Linker from '~/client/models/Linker';
 import MarkdownTable from '~/client/models/MarkdownTable';
 import { IPageToDeleteWithMeta, IPageToRenameWithMeta } from '~/interfaces/page';
 import {
@@ -607,3 +608,32 @@ export const useTemplateModal = (): SWRResponse<TemplateModalStatus, Error> & Te
     },
   });
 };
+
+/*
+ * LinkEditModal
+ */
+type LinkEditModalStatus = {
+  isOpened: boolean,
+  defaultMarkdownLink?: Linker,
+  onSave?: (linkText: string) => void
+}
+
+type LinkEditModalUtils = {
+  open(defaultMarkdownLink: Linker, onSave: (linkText: string) => void): void,
+  close(): void,
+}
+
+export const useLinkEditModal = (): SWRResponse<LinkEditModalStatus, Error> & LinkEditModalUtils => {
+
+  const initialStatus: LinkEditModalStatus = { isOpened: false };
+  const swrResponse = useStaticSWR<LinkEditModalStatus, Error>('linkEditModal', undefined, { fallbackData: initialStatus });
+
+  return Object.assign(swrResponse, {
+    open: (defaultMarkdownLink: Linker, onSave: (linkText: string) => void) => {
+      swrResponse.mutate({ isOpened: true, defaultMarkdownLink, onSave });
+    },
+    close: () => {
+      swrResponse.mutate({ isOpened: false });
+    },
+  });
+};