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

Merge branch 'master' into imprv/implement-skeltons-of-sidebar

kymn 3 лет назад
Родитель
Сommit
d89d35a9b9

+ 1 - 0
packages/app/package.json

@@ -158,6 +158,7 @@
     "react-bootstrap-typeahead": "^5.2.2",
     "react-bootstrap-typeahead": "^5.2.2",
     "react-card-flip": "^1.0.10",
     "react-card-flip": "^1.0.10",
     "react-datepicker": "^4.7.0",
     "react-datepicker": "^4.7.0",
+    "react-disable": "^0.1.1",
     "react-dnd": "^14.0.5",
     "react-dnd": "^14.0.5",
     "react-dnd-html5-backend": "^14.1.0",
     "react-dnd-html5-backend": "^14.1.0",
     "react-dom": "^18.2.0",
     "react-dom": "^18.2.0",

+ 28 - 0
packages/app/src/client/services/page-operation.ts

@@ -2,7 +2,10 @@ import { SubscriptionStatusType, Nullable } from '@growi/core';
 import urljoin from 'url-join';
 import urljoin from 'url-join';
 
 
 import { OptionsToSave } from '~/interfaces/page-operation';
 import { OptionsToSave } from '~/interfaces/page-operation';
+import { useCurrentPageId } from '~/stores/context';
 import { useIsEnabledUnsavedWarning } from '~/stores/editor';
 import { useIsEnabledUnsavedWarning } from '~/stores/editor';
+import { useSWRxCurrentPage } from '~/stores/page';
+import { useSetRemoteLatestPageData } from '~/stores/remote-latest-page';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
 import { toastError } from '../util/apiNotification';
 import { toastError } from '../util/apiNotification';
@@ -171,3 +174,28 @@ export const useSaveOrUpdate = (): SaveOrUpdateFunction => {
     return res;
     return res;
   };
   };
 };
 };
+
+export const useUpdateStateAfterSave = () => {
+  const { mutate: mutateCurrentPageId } = useCurrentPageId();
+  const { mutate: mutateCurrentPage } = useSWRxCurrentPage();
+  const { setRemoteLatestPageData } = useSetRemoteLatestPageData();
+
+  // update swr 'currentPageId', 'currentPage', remote states
+  return async(pageId: string) => {
+    await mutateCurrentPageId(pageId);
+    const updatedPage = await mutateCurrentPage();
+
+    if (updatedPage == null) { return }
+
+    const remoterevisionData = {
+      remoteRevisionId: updatedPage.revision._id,
+      remoteRevisionBody: updatedPage.revision.body,
+      remoteRevisionLastUpdateUser: updatedPage.lastUpdateUser,
+      remoteRevisionLastUpdatedAt: updatedPage.updatedAt,
+      revisionIdHackmdSynced: updatedPage.revisionHackmdSynced.toString(),
+      hasDraftOnHackmd: updatedPage.hasDraftOnHackmd,
+    };
+
+    setRemoteLatestPageData(remoterevisionData);
+  };
+};

+ 0 - 41
packages/app/src/components/NotAvailableForGuest.jsx

@@ -1,41 +0,0 @@
-import React from 'react';
-
-import { useTranslation } from 'next-i18next';
-import PropTypes from 'prop-types';
-import { UncontrolledTooltip } from 'reactstrap';
-
-import { useIsGuestUser } from '~/stores/context';
-
-const NotAvailableForGuest = (props) => {
-  const { children } = props;
-  const { t } = useTranslation();
-
-  const { data: isGuestUser } = useIsGuestUser();
-
-  if (!isGuestUser) {
-    return props.children;
-  }
-
-  const id = children.props.id || `grw-not-available-for-guest-${Math.random().toString(32).substring(2)}`;
-
-  // clone and add className
-  const clonedChild = React.cloneElement(children, {
-    id,
-    className: `${children.props.className} grw-not-available-for-guest`,
-    onClick: () => { /* do nothing */ },
-  });
-
-  return (
-    <>
-      { clonedChild }
-      <UncontrolledTooltip placement="top" target={id}>{t('Not available for guest')}</UncontrolledTooltip>
-    </>
-  );
-
-};
-
-NotAvailableForGuest.propTypes = {
-  children: PropTypes.node.isRequired,
-};
-
-export default NotAvailableForGuest;

+ 35 - 0
packages/app/src/components/NotAvailableForGuest.tsx

@@ -0,0 +1,35 @@
+import React from 'react';
+
+import { useTranslation } from 'next-i18next';
+import { Disable } from 'react-disable';
+import { UncontrolledTooltip } from 'reactstrap';
+
+import { useIsGuestUser } from '~/stores/context';
+
+type NotAvailableForGuestProps = {
+  children: JSX.Element
+}
+
+export const NotAvailableForGuest = ({ children }: NotAvailableForGuestProps): JSX.Element => {
+  const { t } = useTranslation();
+
+  const { data: isGuestUser } = useIsGuestUser();
+  const isDisabled = !!isGuestUser;
+
+  if (!isGuestUser) {
+    return children;
+  }
+
+  const id = `grw-not-available-for-guest-${Math.random().toString(32).substring(2)}`;
+
+  return (
+    <>
+      <div id={id}>
+        <Disable disabled={isDisabled}>
+          { children }
+        </Disable>
+      </div>
+      <UncontrolledTooltip placement="top" target={id}>{t('Not available for guest')}</UncontrolledTooltip>
+    </>
+  );
+};

+ 1 - 1
packages/app/src/components/Page/RenderTagLabels.tsx

@@ -2,7 +2,7 @@ import React from 'react';
 
 
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 
 
-import NotAvailableForGuest from '../NotAvailableForGuest';
+import { NotAvailableForGuest } from '../NotAvailableForGuest';
 
 
 type RenderTagLabelsProps = {
 type RenderTagLabelsProps = {
   tags: string[],
   tags: string[],

+ 1 - 1
packages/app/src/components/PageComment/CommentEditor.tsx

@@ -20,7 +20,7 @@ import { useSWRxSlackChannels, useIsSlackEnabled } from '~/stores/editor';
 import { useCurrentPagePath } from '~/stores/page';
 import { useCurrentPagePath } from '~/stores/page';
 
 
 import { CustomNavTab } from '../CustomNavigation/CustomNav';
 import { CustomNavTab } from '../CustomNavigation/CustomNav';
-import NotAvailableForGuest from '../NotAvailableForGuest';
+import { NotAvailableForGuest } from '../NotAvailableForGuest';
 import Editor from '../PageEditor/Editor';
 import Editor from '../PageEditor/Editor';
 
 
 
 

+ 8 - 7
packages/app/src/components/PageEditor.tsx

@@ -13,7 +13,7 @@ import { useTranslation } from 'next-i18next';
 import { useRouter } from 'next/router';
 import { useRouter } from 'next/router';
 import { throttle, debounce } from 'throttle-debounce';
 import { throttle, debounce } from 'throttle-debounce';
 
 
-import { useSaveOrUpdate } from '~/client/services/page-operation';
+import { useUpdateStateAfterSave, useSaveOrUpdate } from '~/client/services/page-operation';
 import { apiGet, apiPostForm } from '~/client/util/apiv1-client';
 import { apiGet, apiPostForm } from '~/client/util/apiv1-client';
 import { toastError, toastSuccess } from '~/client/util/toastr';
 import { toastError, toastSuccess } from '~/client/util/toastr';
 import { IEditorMethods } from '~/interfaces/editor-methods';
 import { IEditorMethods } from '~/interfaces/editor-methods';
@@ -31,6 +31,7 @@ import {
 } from '~/stores/editor';
 } from '~/stores/editor';
 import { useConflictDiffModal } from '~/stores/modal';
 import { useConflictDiffModal } from '~/stores/modal';
 import { useCurrentPagePath, useSWRxCurrentPage, useSWRxTagsInfo } from '~/stores/page';
 import { useCurrentPagePath, useSWRxCurrentPage, useSWRxTagsInfo } from '~/stores/page';
+import { useSetRemoteLatestPageData } from '~/stores/remote-latest-page';
 import { usePreviewOptions } from '~/stores/renderer';
 import { usePreviewOptions } from '~/stores/renderer';
 import {
 import {
   EditorMode,
   EditorMode,
@@ -93,6 +94,8 @@ const PageEditor = React.memo((): JSX.Element => {
   const { mutate: mutateIsEnabledUnsavedWarning } = useIsEnabledUnsavedWarning();
   const { mutate: mutateIsEnabledUnsavedWarning } = useIsEnabledUnsavedWarning();
   const saveOrUpdate = useSaveOrUpdate();
   const saveOrUpdate = useSaveOrUpdate();
 
 
+  const updateStateAfterSave = useUpdateStateAfterSave();
+
   const currentRevisionId = currentPage?.revision?._id;
   const currentRevisionId = currentPage?.revision?._id;
 
 
   const initialValue = useMemo(() => {
   const initialValue = useMemo(() => {
@@ -245,11 +248,10 @@ const PageEditor = React.memo((): JSX.Element => {
       await router.push(`/${page._id}`);
       await router.push(`/${page._id}`);
     }
     }
     else {
     else {
-      await mutateCurrentPageId(page._id);
-      await mutateCurrentPage();
+      updateStateAfterSave(page._id);
     }
     }
     mutateEditorMode(EditorMode.View);
     mutateEditorMode(EditorMode.View);
-  }, [editorMode, save, isNotFound, mutateEditorMode, router, mutateCurrentPageId, mutateCurrentPage]);
+  }, [editorMode, save, isNotFound, mutateEditorMode, router, useUpdateStateAfterSave]);
 
 
   const saveWithShortcut = useCallback(async() => {
   const saveWithShortcut = useCallback(async() => {
     if (editorMode !== EditorMode.Editor) {
     if (editorMode !== EditorMode.Editor) {
@@ -258,11 +260,10 @@ const PageEditor = React.memo((): JSX.Element => {
 
 
     const page = await save();
     const page = await save();
     if (page != null) {
     if (page != null) {
+      updateStateAfterSave(page._id);
       toastSuccess(t('toaster.save_succeeded'));
       toastSuccess(t('toaster.save_succeeded'));
-      await mutateCurrentPageId(page._id);
-      await mutateCurrentPage();
     }
     }
-  }, [editorMode, mutateCurrentPage, mutateCurrentPageId, save, t]);
+  }, [editorMode, save, t, useUpdateStateAfterSave]);
 
 
 
 
   /**
   /**

+ 11 - 11
packages/app/src/components/PageEditorByHackmd.tsx

@@ -10,7 +10,7 @@ import { useRouter } from 'next/router';
 import { useTranslation } from 'react-i18next';
 import { useTranslation } from 'react-i18next';
 import urljoin from 'url-join';
 import urljoin from 'url-join';
 
 
-import { useSaveOrUpdate } from '~/client/services/page-operation';
+import { useUpdateStateAfterSave, useSaveOrUpdate } from '~/client/services/page-operation';
 import { toastError, toastSuccess } from '~/client/util/apiNotification';
 import { toastError, toastSuccess } from '~/client/util/apiNotification';
 import { apiPost } from '~/client/util/apiv1-client';
 import { apiPost } from '~/client/util/apiv1-client';
 import { IResHackmdIntegrated, IResHackmdDiscard } from '~/interfaces/hackmd';
 import { IResHackmdIntegrated, IResHackmdDiscard } from '~/interfaces/hackmd';
@@ -25,7 +25,7 @@ import {
   usePageIdOnHackmd, useHasDraftOnHackmd, useRevisionIdHackmdSynced, useIsHackmdDraftUpdatingInRealtime,
   usePageIdOnHackmd, useHasDraftOnHackmd, useRevisionIdHackmdSynced, useIsHackmdDraftUpdatingInRealtime,
 } from '~/stores/hackmd';
 } from '~/stores/hackmd';
 import { useCurrentPagePath, useSWRxCurrentPage, useSWRxTagsInfo } from '~/stores/page';
 import { useCurrentPagePath, useSWRxCurrentPage, useSWRxTagsInfo } from '~/stores/page';
-import { useRemoteRevisionId } from '~/stores/remote-latest-page';
+import { useRemoteRevisionId, useSetRemoteLatestPageData } from '~/stores/remote-latest-page';
 import {
 import {
   EditorMode,
   EditorMode,
   useEditorMode, useSelectedGrant,
   useEditorMode, useSelectedGrant,
@@ -86,7 +86,9 @@ export const PageEditorByHackmd = (): JSX.Element => {
   const { data: revisionIdHackmdSynced, mutate: mutateRevisionIdHackmdSynced } = useRevisionIdHackmdSynced();
   const { data: revisionIdHackmdSynced, mutate: mutateRevisionIdHackmdSynced } = useRevisionIdHackmdSynced();
   const { mutate: mutateIsEnabledUnsavedWarning } = useIsEnabledUnsavedWarning();
   const { mutate: mutateIsEnabledUnsavedWarning } = useIsEnabledUnsavedWarning();
   const { data: isHackmdDraftUpdatingInRealtime, mutate: mutateIsHackmdDraftUpdatingInRealtime } = useIsHackmdDraftUpdatingInRealtime();
   const { data: isHackmdDraftUpdatingInRealtime, mutate: mutateIsHackmdDraftUpdatingInRealtime } = useIsHackmdDraftUpdatingInRealtime();
-  const { data: remoteRevisionId, mutate: mutateRemoteRevisionId } = useRemoteRevisionId(revision?._id);
+  const { data: remoteRevisionId, mutate: mutateRemoteRevisionId } = useRemoteRevisionId();
+
+  const updateStateAfterSave = useUpdateStateAfterSave();
 
 
   const hackmdEditorRef = useRef<HackEditorRef>(null);
   const hackmdEditorRef = useRef<HackEditorRef>(null);
 
 
@@ -123,8 +125,7 @@ export const PageEditorByHackmd = (): JSX.Element => {
         await router.push(`/${page._id}`);
         await router.push(`/${page._id}`);
       }
       }
       else {
       else {
-        await mutateCurrentPageId(page._id);
-        await mutatePageData();
+        updateStateAfterSave(page._id);
       }
       }
       setIsInitialized(false);
       setIsInitialized(false);
       mutateEditorMode(EditorMode.View);
       mutateEditorMode(EditorMode.View);
@@ -134,7 +135,7 @@ export const PageEditorByHackmd = (): JSX.Element => {
       toastError(error.message);
       toastError(error.message);
     }
     }
   // eslint-disable-next-line max-len
   // eslint-disable-next-line max-len
-  }, [editorMode, isSlackEnabled, currentPathname, slackChannels, grant, revision, revisionIdHackmdSynced, pageTags, saveOrUpdate, pageId, currentPagePath, mutatePageData, mutateTagsInfo, isNotFound, mutateEditorMode, router, mutateCurrentPageId]);
+  }, [editorMode, isSlackEnabled, currentPathname, slackChannels, grant, revision, revisionIdHackmdSynced, pageTags, saveOrUpdate, pageId, currentPagePath, mutatePageData, mutateTagsInfo, isNotFound, mutateEditorMode, router, useUpdateStateAfterSave]);
 
 
   // set handler to save and reload Page
   // set handler to save and reload Page
   useEffect(() => {
   useEffect(() => {
@@ -255,9 +256,7 @@ export const PageEditorByHackmd = (): JSX.Element => {
       mutatePageData(res);
       mutatePageData(res);
 
 
       // set updated data
       // set updated data
-      mutateRemoteRevisionId(res.revision._id);
-      mutateRevisionIdHackmdSynced(res.page.revisionHackmdSynced);
-      mutateHasDraftOnHackmd(res.page.hasDraftOnHackmd);
+      updateStateAfterSave(res._id);
       mutateTagsInfo();
       mutateTagsInfo();
 
 
       logger.debug('success to save');
       logger.debug('success to save');
@@ -269,8 +268,9 @@ export const PageEditorByHackmd = (): JSX.Element => {
       toastError(error.message);
       toastError(error.message);
     }
     }
   }, [
   }, [
-    currentPagePath, currentPathname, isSlackEnabled, grant, slackChannels, pageId, revisionIdHackmdSynced, pageTags,
-    saveOrUpdate, mutatePageData, mutateRemoteRevisionId, mutateRevisionIdHackmdSynced, mutateHasDraftOnHackmd, mutateTagsInfo, t]);
+    currentPagePath, currentPathname, isSlackEnabled, grant, slackChannels, pageId, revisionIdHackmdSynced,
+    pageTags, saveOrUpdate, mutatePageData, useUpdateStateAfterSave, mutateTagsInfo, t,
+  ]);
 
 
   /**
   /**
    * onChange event of HackmdEditor handler
    * onChange event of HackmdEditor handler

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

@@ -119,6 +119,9 @@ export const PageStatusAlert = (): JSX.Element => {
     const isRevisionOutdated = revision?._id !== remoteRevisionId;
     const isRevisionOutdated = revision?._id !== remoteRevisionId;
     const isHackmdDocumentOutdated = revisionIdHackmdSynced !== remoteRevisionId;
     const isHackmdDocumentOutdated = revisionIdHackmdSynced !== remoteRevisionId;
 
 
+    // 'revision?._id' and 'remoteRevisionId' are can not be undefined
+    if (revision?._id == null || remoteRevisionId == null) { return }
+
     // when remote revision is newer than both
     // when remote revision is newer than both
     if (isHackmdDocumentOutdated && isRevisionOutdated) {
     if (isHackmdDocumentOutdated && isRevisionOutdated) {
       return getContentsForUpdatedAlert();
       return getContentsForUpdatedAlert();

+ 21 - 21
packages/app/src/components/Sidebar/PageTree/Item.tsx

@@ -14,7 +14,7 @@ import { bookmark, unbookmark, resumeRenameOperation } from '~/client/services/p
 import { toastWarning, toastError, toastSuccess } from '~/client/util/apiNotification';
 import { toastWarning, toastError, toastSuccess } from '~/client/util/apiNotification';
 import { apiv3Put, apiv3Post } from '~/client/util/apiv3-client';
 import { apiv3Put, apiv3Post } from '~/client/util/apiv3-client';
 import TriangleIcon from '~/components/Icons/TriangleIcon';
 import TriangleIcon from '~/components/Icons/TriangleIcon';
-import NotAvailableForGuest from '~/components/NotAvailableForGuest';
+import { NotAvailableForGuest } from '~/components/NotAvailableForGuest';
 import {
 import {
   IPageHasId, IPageInfoAll, IPageToDeleteWithMeta,
   IPageHasId, IPageInfoAll, IPageToDeleteWithMeta,
 } from '~/interfaces/page';
 } from '~/interfaces/page';
@@ -482,27 +482,27 @@ const Item: FC<ItemProps> = (props: ItemProps) => {
             <CountBadge count={descendantCount} />
             <CountBadge count={descendantCount} />
           </div>
           </div>
         )}
         )}
-        <div className="grw-pagetree-control d-flex">
-          <PageItemControl
-            pageId={page._id}
-            isEnableActions={isEnableActions}
-            onClickBookmarkMenuItem={bookmarkMenuItemClickHandler}
-            onClickDuplicateMenuItem={duplicateMenuItemClickHandler}
-            onClickRenameMenuItem={renameMenuItemClickHandler}
-            onClickDeleteMenuItem={deleteMenuItemClickHandler}
-            onClickPathRecoveryMenuItem={pathRecoveryMenuItemClickHandler}
-            isInstantRename
-            // Todo: It is wanted to find a better way to pass operationProcessData to PageItemControl
-            operationProcessData={page.processData}
-          >
-            {/* pass the color property to reactstrap dropdownToggle props. https://6-4-0--reactstrap.netlify.app/components/dropdowns/  */}
-            <DropdownToggle color="transparent" className="border-0 rounded btn-page-item-control p-0 grw-visible-on-hover mr-1">
-              <NotAvailableForGuest>
+        <NotAvailableForGuest>
+          <div className="grw-pagetree-control d-flex">
+            <PageItemControl
+              pageId={page._id}
+              isEnableActions={isEnableActions}
+              onClickBookmarkMenuItem={bookmarkMenuItemClickHandler}
+              onClickDuplicateMenuItem={duplicateMenuItemClickHandler}
+              onClickRenameMenuItem={renameMenuItemClickHandler}
+              onClickDeleteMenuItem={deleteMenuItemClickHandler}
+              onClickPathRecoveryMenuItem={pathRecoveryMenuItemClickHandler}
+              isInstantRename
+              // Todo: It is wanted to find a better way to pass operationProcessData to PageItemControl
+              operationProcessData={page.processData}
+            >
+              {/* pass the color property to reactstrap dropdownToggle props. https://6-4-0--reactstrap.netlify.app/components/dropdowns/  */}
+              <DropdownToggle color="transparent" className="border-0 rounded btn-page-item-control p-0 grw-visible-on-hover mr-1">
                 <i id='option-button-in-page-tree' className="icon-options fa fa-rotate-90 p-1"></i>
                 <i id='option-button-in-page-tree' className="icon-options fa fa-rotate-90 p-1"></i>
-              </NotAvailableForGuest>
-            </DropdownToggle>
-          </PageItemControl>
-        </div>
+              </DropdownToggle>
+            </PageItemControl>
+          </div>
+        </NotAvailableForGuest>
 
 
         {!pagePathUtils.isUsersTopPage(page.path ?? '') && (
         {!pagePathUtils.isUsersTopPage(page.path ?? '') && (
           <NotAvailableForGuest>
           <NotAvailableForGuest>

+ 4 - 1
packages/app/test/cypress/integration/21-basic-features-for-guest/21-basic-features-for-guest--access-to-page.spec.ts

@@ -29,7 +29,10 @@ context('Access to page by guest', () => {
     cy.collapseSidebar(true, true);
     cy.collapseSidebar(true, true);
 
 
     cy.get('.math').should('be.visible');
     cy.get('.math').should('be.visible');
-    cy.screenshot(`${ssPrefix}-sandbox-math`);
+
+    cy.screenshot(`${ssPrefix}-sandbox-math`, {
+      blackout: ['.revision-toc', '[data-hide-in-vrt=true]']
+    });
   });
   });
 
 
   it('/Sandbox with edit is successfully loaded', () => {
   it('/Sandbox with edit is successfully loaded', () => {

+ 5 - 0
yarn.lock

@@ -18479,6 +18479,11 @@ react-datepicker@^4.7.0:
     react-onclickoutside "^6.12.0"
     react-onclickoutside "^6.12.0"
     react-popper "^2.2.5"
     react-popper "^2.2.5"
 
 
+react-disable@^0.1.1:
+  version "0.1.1"
+  resolved "https://registry.yarnpkg.com/react-disable/-/react-disable-0.1.1.tgz#86d2d0932259f626a70fa46f63d6d61cbe7dd066"
+  integrity sha512-KKEDYJUnF8hIPlmGYJu38HG8BlBB4EElCFY1zfA9W46/MF76DSGvgcduWl1eVT/CAw3ahb2sWTSfhon+kPSiKw==
+
 react-dnd-html5-backend@^14.1.0:
 react-dnd-html5-backend@^14.1.0:
   version "14.1.0"
   version "14.1.0"
   resolved "https://registry.yarnpkg.com/react-dnd-html5-backend/-/react-dnd-html5-backend-14.1.0.tgz#b35a3a0c16dd3a2bfb5eb7ec62cf0c2cace8b62f"
   resolved "https://registry.yarnpkg.com/react-dnd-html5-backend/-/react-dnd-html5-backend-14.1.0.tgz#b35a3a0c16dd3a2bfb5eb7ec62cf0c2cace8b62f"