瀏覽代碼

Merge branch 'support/107699-next-PageStatusAlert' of https://github.com/weseek/growi into support/107699-next-PageStatusAlert

Yuken Tezuka 3 年之前
父節點
當前提交
297b6bb52a

+ 5 - 4
packages/app/src/components/PageEditorByHackmd.tsx

@@ -19,7 +19,7 @@ import {
   useSWRxSlackChannels, useIsSlackEnabled, usePageTagsForEditors, useIsEnabledUnsavedWarning,
 } from '~/stores/editor';
 import {
-  usePageIdOnHackmd, useHasDraftOnHackmd, useRevisionIdHackmdSynced, useRemoteRevisionId,
+  usePageIdOnHackmd, useHasDraftOnHackmd, useRevisionIdHackmdSynced, useRemoteRevisionId, useIsHackmdDraftUpdatingInRealtime,
 } from '~/stores/hackmd';
 import { useCurrentPagePath, useSWRxCurrentPage, useSWRxTagsInfo } from '~/stores/page';
 import {
@@ -70,7 +70,7 @@ export const PageEditorByHackmd = (): JSX.Element => {
   const { data: hasDraftOnHackmd, mutate: mutateHasDraftOnHackmd } = useHasDraftOnHackmd();
   const { data: revisionIdHackmdSynced, mutate: mutateRevisionIdHackmdSynced } = useRevisionIdHackmdSynced();
   const { mutate: mutateIsEnabledUnsavedWarning } = useIsEnabledUnsavedWarning();
-  const [isHackmdDraftUpdatingInRealtime, setIsHackmdDraftUpdatingInRealtime] = useState(false);
+  const { data: isHackmdDraftUpdatingInRealtime, mutate: mutateIsHackmdDraftUpdatingInRealtime } = useIsHackmdDraftUpdatingInRealtime(false);
   const { data: remoteRevisionId, mutate: mutateRemoteRevisionId } = useRemoteRevisionId(revision?._id);
 
   const hackmdEditorRef = useRef<HackEditorRef>(null);
@@ -204,7 +204,7 @@ export const PageEditorByHackmd = (): JSX.Element => {
         throw new Error(res.error);
       }
 
-      setIsHackmdDraftUpdatingInRealtime(false);
+      mutateIsHackmdDraftUpdatingInRealtime(false);
       mutateHasDraftOnHackmd(false);
       mutatePageIdOnHackmd(res.pageIdOnHackmd);
       mutateRemoteRevisionId(res.revisionIdHackmdSynced);
@@ -216,7 +216,7 @@ export const PageEditorByHackmd = (): JSX.Element => {
       logger.error(err);
       toastError(err.message);
     }
-  }, [setIsHackmdDraftUpdatingInRealtime, mutateHasDraftOnHackmd, mutatePageIdOnHackmd, mutateRevisionIdHackmdSynced, pageId]);
+  }, [mutateIsHackmdDraftUpdatingInRealtime, mutateHasDraftOnHackmd, mutatePageIdOnHackmd, mutateRevisionIdHackmdSynced, mutateRemoteRevisionId, pageId]);
 
   /**
    * save and update state of containers
@@ -265,6 +265,7 @@ export const PageEditorByHackmd = (): JSX.Element => {
       mutateHasDraftOnHackmd,
       mutateTagsInfo,
       mutateIsEnabledUnsavedWarning,
+      mutateRemoteRevisionId,
       t]);
 
   /**

+ 0 - 177
packages/app/src/components/PageStatusAlert.jsx

@@ -1,177 +0,0 @@
-import React from 'react';
-
-import { useTranslation } from 'next-i18next';
-import PropTypes from 'prop-types';
-
-// import AppContainer from '~/client/services/AppContainer';
-// import PageContainer from '~/client/services/PageContainer';
-// import Username from '~/components/User/Username';
-
-import { withUnstatedContainers } from './UnstatedUtils';
-
-/**
- *
- * @author Yuki Takei <yuki@weseek.co.jp>
- *
- * @export
- * @class PageStatusAlert
- * @extends {React.Component}
- */
-
-class PageStatusAlert extends React.Component {
-
-  constructor(props) {
-    super(props);
-
-    this.state = {
-    };
-
-    this.getContentsForSomeoneEditingAlert = this.getContentsForSomeoneEditingAlert.bind(this);
-    this.getContentsForDraftExistsAlert = this.getContentsForDraftExistsAlert.bind(this);
-    this.getContentsForUpdatedAlert = this.getContentsForUpdatedAlert.bind(this);
-    this.onClickResolveConflict = this.onClickResolveConflict.bind(this);
-  }
-
-  refreshPage() {
-    window.location.reload();
-  }
-
-  onClickResolveConflict() {
-    this.props.pageContainer.setState({
-      isConflictDiffModalOpen: true,
-    });
-  }
-
-  getContentsForSomeoneEditingAlert() {
-    const { t } = this.props;
-    return [
-      ['bg-success', 'd-hackmd-none'],
-      <>
-        <i className="icon-fw icon-people"></i>
-        {t('hackmd.someone_editing')}
-      </>,
-      <a href="#hackmd" key="btnOpenHackmdSomeoneEditing" className="btn btn-outline-white">
-        <i className="fa fa-fw fa-file-text-o mr-1"></i>
-        Open HackMD Editor
-      </a>,
-    ];
-  }
-
-  getContentsForDraftExistsAlert(isRealtime) {
-    const { t } = this.props;
-    return [
-      ['bg-success', 'd-hackmd-none'],
-      <>
-        <i className="icon-fw icon-pencil"></i>
-        {t('hackmd.this_page_has_draft')}
-      </>,
-      <a href="#hackmd" key="btnOpenHackmdPageHasDraft" className="btn btn-outline-white">
-        <i className="fa fa-fw fa-file-text-o mr-1"></i>
-        Open HackMD Editor
-      </a>,
-    ];
-  }
-
-  getContentsForUpdatedAlert() {
-    const { t } = this.props;
-    // const pageEditor = appContainer.getComponentInstance('PageEditor');
-
-    const isConflictOnEdit = false;
-
-    // if (pageEditor != null) {
-    //   const markdownOnEdit = pageEditor.getMarkdown();
-    //   isConflictOnEdit = markdownOnEdit !== pageContainer.state.markdown;
-    // }
-
-    // TODO: re-impl with Next.js way
-    // const usernameComponentToString = ReactDOMServer.renderToString(<Username user={pageContainer.state.lastUpdateUser} />);
-
-    // const label1 = isConflictOnEdit
-    //   ? t('modal_resolve_conflict.file_conflicting_with_newer_remote')
-    //   // eslint-disable-next-line react/no-danger
-    //   : <span dangerouslySetInnerHTML={{ __html: `${usernameComponentToString} ${t('edited this page')}` }} />;
-    const label1 = '(TBD -- 2022.09.13)';
-
-    return [
-      ['bg-warning'],
-      <>
-        <i className="icon-fw icon-bulb"></i>
-        {label1}
-      </>,
-      <>
-        <button type="button" onClick={() => this.refreshPage()} className="btn btn-outline-white mr-4">
-          <i className="icon-fw icon-reload mr-1"></i>
-          {t('Load latest')}
-        </button>
-        { isConflictOnEdit && (
-          <button
-            type="button"
-            onClick={this.onClickResolveConflict}
-            className="btn btn-outline-white"
-          >
-            <i className="fa fa-fw fa-file-text-o mr-1"></i>
-            {t('modal_resolve_conflict.resolve_conflict')}
-          </button>
-        )}
-      </>,
-    ];
-  }
-
-  render() {
-    const {
-      revisionId, revisionIdHackmdSynced, remoteRevisionId, hasDraftOnHackmd, isHackmdDraftUpdatingInRealtime,
-    } = this.props.pageContainer.state;
-
-    const isRevisionOutdated = revisionId !== remoteRevisionId;
-    const isHackmdDocumentOutdated = revisionIdHackmdSynced !== remoteRevisionId;
-
-    let getContentsFunc = null;
-
-    // when remote revision is newer than both
-    if (isHackmdDocumentOutdated && isRevisionOutdated) {
-      getContentsFunc = this.getContentsForUpdatedAlert;
-    }
-    // when someone editing with HackMD
-    else if (isHackmdDraftUpdatingInRealtime) {
-      getContentsFunc = this.getContentsForSomeoneEditingAlert;
-    }
-    // when the draft of HackMD is newest
-    else if (hasDraftOnHackmd) {
-      getContentsFunc = this.getContentsForDraftExistsAlert;
-    }
-    // do not render anything
-    else {
-      return null;
-    }
-
-    const [additionalClasses, label, btn] = getContentsFunc();
-
-    return (
-      <div className={`card grw-page-status-alert text-white fixed-bottom animated fadeInUp faster ${additionalClasses.join(' ')}`}>
-        <div className="card-body">
-          <p className="card-text grw-card-label-container">
-            {label}
-          </p>
-          <p className="card-text grw-card-btn-container">
-            {btn}
-          </p>
-        </div>
-      </div>
-    );
-  }
-
-}
-
-PageStatusAlert.propTypes = {
-  t: PropTypes.func.isRequired, // i18next
-
-  // appContainer: PropTypes.instanceOf(AppContainer).isRequired,
-  // pageContainer: PropTypes.instanceOf(PageContainer).isRequired,
-};
-
-const PageStatusAlertWrapperFC = (props) => {
-  const { t } = useTranslation();
-  return <PageStatusAlert t={t} {...props} />;
-};
-
-export default PageStatusAlertWrapperFC;

+ 162 - 0
packages/app/src/components/PageStatusAlert.tsx

@@ -0,0 +1,162 @@
+import React, { useCallback, useMemo } from 'react';
+
+import { useTranslation } from 'next-i18next';
+
+import {
+  useHasDraftOnHackmd, useIsHackmdDraftUpdatingInRealtime, useRemoteRevisionId, useRevisionIdHackmdSynced,
+} from '~/stores/hackmd';
+import { useSWRxCurrentPage } from '~/stores/page';
+
+type AlertComponentContents = {
+  additionalClasses: string[],
+  label: JSX.Element,
+  btn: JSX.Element
+}
+
+export const PageStatusAlert = (): JSX.Element => {
+
+  const { t } = useTranslation();
+  const { data: isHackmdDraftUpdatingInRealtime } = useIsHackmdDraftUpdatingInRealtime();
+  const { data: hasDraftOnHackmd } = useHasDraftOnHackmd();
+  const { data: revisionIdHackmdSynced } = useRevisionIdHackmdSynced();
+  const { data: remoteRevisionId } = useRemoteRevisionId();
+  const { data: pageData } = useSWRxCurrentPage();
+  const revision = pageData?.revision;
+
+  const refreshPage = useCallback(() => {
+    window.location.reload();
+  }, []);
+
+  const onClickResolveConflict = useCallback(() => {
+    // this.props.pageContainer.setState({
+    //   isConflictDiffModalOpen: true,
+    // });
+  }, []);
+
+  const getContentsForSomeoneEditingAlert = useCallback((): AlertComponentContents => {
+    return {
+      additionalClasses: ['bg-success', 'd-hackmd-none'],
+      label:
+        <>
+          <i className="icon-fw icon-people"></i>
+          {t('hackmd.someone_editing')}
+        </>,
+      btn:
+        <a href="#hackmd" key="btnOpenHackmdSomeoneEditing" className="btn btn-outline-white">
+          <i className="fa fa-fw fa-file-text-o mr-1"></i>
+          Open HackMD Editor
+        </a>,
+    };
+  }, [t]);
+
+  const getContentsForDraftExistsAlert = useCallback((): AlertComponentContents => {
+    return {
+      additionalClasses: ['bg-success', 'd-hackmd-none'],
+      label:
+        <>
+          <i className="icon-fw icon-pencil"></i>
+          {t('hackmd.this_page_has_draft')}
+        </>,
+      btn:
+        <a href="#hackmd" key="btnOpenHackmdPageHasDraft" className="btn btn-outline-white">
+          <i className="fa fa-fw fa-file-text-o mr-1"></i>
+          Open HackMD Editor
+        </a>,
+    };
+  }, [t]);
+
+  const getContentsForUpdatedAlert = useCallback((): AlertComponentContents => {
+    // const pageEditor = appContainer.getComponentInstance('PageEditor');
+
+    const isConflictOnEdit = false;
+
+    // if (pageEditor != null) {
+    //   const markdownOnEdit = pageEditor.getMarkdown();
+    //   isConflictOnEdit = markdownOnEdit !== pageContainer.state.markdown;
+    // }
+
+    // TODO: re-impl with Next.js way
+    // const usernameComponentToString = ReactDOMServer.renderToString(<Username user={pageContainer.state.lastUpdateUser} />);
+
+    // const label1 = isConflictOnEdit
+    //   ? t('modal_resolve_conflict.file_conflicting_with_newer_remote')
+    //   // eslint-disable-next-line react/no-danger
+    //   : <span dangerouslySetInnerHTML={{ __html: `${usernameComponentToString} ${t('edited this page')}` }} />;
+    const label1 = '(TBD -- 2022.09.13)';
+
+    return {
+      additionalClasses: ['bg-warning'],
+      label:
+        <>
+          <i className="icon-fw icon-bulb"></i>
+          {label1}
+        </>,
+      btn:
+        <>
+          <button type="button" onClick={() => refreshPage()} className="btn btn-outline-white mr-4">
+            <i className="icon-fw icon-reload mr-1"></i>
+            {t('Load latest')}
+          </button>
+          { isConflictOnEdit && (
+            <button
+              type="button"
+              onClick={onClickResolveConflict}
+              className="btn btn-outline-white"
+            >
+              <i className="fa fa-fw fa-file-text-o mr-1"></i>
+              {t('modal_resolve_conflict.resolve_conflict')}
+            </button>
+          )}
+        </>,
+    };
+  }, [t, onClickResolveConflict, refreshPage]);
+
+  const alertComponentContents = useMemo(() => {
+    const isRevisionOutdated = revision?._id !== remoteRevisionId;
+    const isHackmdDocumentOutdated = revisionIdHackmdSynced !== remoteRevisionId;
+
+    // when remote revision is newer than both
+    if (isHackmdDocumentOutdated && isRevisionOutdated) {
+      return getContentsForUpdatedAlert();
+    }
+
+    // when someone editing with HackMD
+    if (isHackmdDraftUpdatingInRealtime) {
+      return getContentsForSomeoneEditingAlert();
+    }
+
+    // when the draft of HackMD is newest
+    if (hasDraftOnHackmd) {
+      return getContentsForDraftExistsAlert();
+    }
+
+    return null;
+  }, [
+    revision?._id,
+    remoteRevisionId,
+    revisionIdHackmdSynced,
+    isHackmdDraftUpdatingInRealtime,
+    hasDraftOnHackmd,
+    getContentsForUpdatedAlert,
+    getContentsForSomeoneEditingAlert,
+    getContentsForDraftExistsAlert,
+  ]);
+
+  if (alertComponentContents == null) { return <></> }
+
+  const { additionalClasses, label, btn } = alertComponentContents;
+
+  return (
+    <div className={`card grw-page-status-alert text-white fixed-bottom animated fadeInUp faster ${additionalClasses.join(' ')}`}>
+      <div className="card-body">
+        <p className="card-text grw-card-label-container">
+          {label}
+        </p>
+        <p className="card-text grw-card-btn-container">
+          {btn}
+        </p>
+      </div>
+    </div>
+  );
+
+};

+ 5 - 0
packages/app/src/stores/hackmd.ts

@@ -1,4 +1,5 @@
 import { SWRResponse } from 'swr';
+
 import { useStaticSWR } from './use-static-swr';
 
 type Nullable<T> = T | null;
@@ -19,3 +20,7 @@ export const useRevisionIdHackmdSynced = (initialData?: Nullable<any>): SWRRespo
 export const useRemoteRevisionId = (initialData?: Nullable<any>): SWRResponse<Nullable<any>, Error> => {
   return useStaticSWR<Nullable<any>, Error>('remoteRevisionId', initialData);
 };
+
+export const useIsHackmdDraftUpdatingInRealtime = (initialData?: Nullable<boolean>): SWRResponse<Nullable<boolean>, Error> => {
+  return useStaticSWR<Nullable<boolean>, Error>('isHackmdDraftUpdatingInRealtime', initialData);
+};