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

Merge pull request #7579 from weseek/imprv/show-spinner-for-saving-process

imprv: Show a spinner into the save button while the saving processing
Yuki Takei 3 лет назад
Родитель
Сommit
0f52fe6cec

+ 11 - 3
apps/app/src/components/PageEditor.tsx

@@ -29,6 +29,7 @@ import {
   useIsEnabledUnsavedWarning,
   useIsConflict,
   useEditingMarkdown,
+  useWaitingSaveProcessing,
 } from '~/stores/editor';
 import { useConflictDiffModal } from '~/stores/modal';
 import {
@@ -76,7 +77,7 @@ const PageEditor = React.memo((): JSX.Element => {
   const { t } = useTranslation();
   const router = useRouter();
 
-  const { data: isNotFound, mutate: mutateIsNotFound } = useIsNotFound();
+  const { data: isNotFound } = useIsNotFound();
   const { data: pageId, mutate: mutateCurrentPageId } = useCurrentPageId();
   const { data: currentPagePath } = useCurrentPagePath();
   const { data: currentPathname } = useCurrentPathname();
@@ -89,6 +90,7 @@ const PageEditor = React.memo((): JSX.Element => {
   const { data: isEnabledAttachTitleHeader } = useIsEnabledAttachTitleHeader();
   const { data: templateBodyData } = useTemplateBodyData();
   const { data: isEditable } = useIsEditable();
+  const { mutate: mutateWaitingSaveProcessing } = useWaitingSaveProcessing();
   const { data: editorMode, mutate: mutateEditorMode } = useEditorMode();
   const { data: isSlackEnabled } = useIsSlackEnabled();
   const { data: isTextlintEnabled } = useIsTextlintEnabled();
@@ -201,6 +203,8 @@ const PageEditor = React.memo((): JSX.Element => {
     const options = Object.assign(optionsToSave, opts);
 
     try {
+      mutateWaitingSaveProcessing(true);
+
       const { page } = await saveOrUpdate(
         markdownToSave.current,
         { pageId, path: currentPagePath || currentPathname, revisionId: currentRevisionId },
@@ -223,10 +227,14 @@ const PageEditor = React.memo((): JSX.Element => {
       }
       return null;
     }
+    finally {
+      mutateWaitingSaveProcessing(false);
+    }
 
   }, [
     currentPathname, optionsToSave, grantData, isSlackEnabled, saveOrUpdate, pageId,
-    currentPagePath, currentRevisionId, mutateRemotePageId, mutateRemoteRevisionId, mutateRemoteRevisionLastUpdatedAt, mutateRemoteRevisionLastUpdateUser,
+    currentPagePath, currentRevisionId,
+    mutateWaitingSaveProcessing, mutateRemotePageId, mutateRemoteRevisionId, mutateRemoteRevisionLastUpdatedAt, mutateRemoteRevisionLastUpdateUser,
   ]);
 
   const saveAndReturnToViewHandler = useCallback(async(opts: {slackChannels: string, overwriteScopesOfDescendants?: boolean}) => {
@@ -331,7 +339,7 @@ const PageEditor = React.memo((): JSX.Element => {
     finally {
       editorRef.current.terminateUploadingState();
     }
-  }, [currentPagePath, mutateCurrentPage, mutateCurrentPageId, mutateGrant, mutateIsLatestRevision, mutateIsNotFound, pageId]);
+  }, [currentPagePath, mutateCurrentPage, mutateCurrentPageId, mutateGrant, mutateIsLatestRevision, pageId]);
 
 
   const scrollPreviewByEditorLine = useCallback((line: number) => {

+ 24 - 3
apps/app/src/components/PageEditorByHackmd.tsx

@@ -19,7 +19,7 @@ import {
   useCurrentPathname, useHackmdUri,
 } from '~/stores/context';
 import {
-  useIsSlackEnabled, usePageTagsForEditors, useIsEnabledUnsavedWarning,
+  useIsSlackEnabled, usePageTagsForEditors, useIsEnabledUnsavedWarning, useWaitingSaveProcessing,
 } from '~/stores/editor';
 import {
   usePageIdOnHackmd, useHasDraftOnHackmd, useRevisionIdHackmdSynced, useIsHackmdDraftUpdatingInRealtime,
@@ -56,6 +56,7 @@ export const PageEditorByHackmd = (): JSX.Element => {
   const router = useRouter();
 
   const { data: isNotFound } = useIsNotFound();
+  const { mutate: mutateWaitingSaveProcessing } = useWaitingSaveProcessing();
   const { data: editorMode, mutate: mutateEditorMode } = useEditorMode();
   const { data: currentPagePath } = useCurrentPagePath();
   const { data: currentPathname } = useCurrentPathname();
@@ -116,6 +117,8 @@ export const PageEditorByHackmd = (): JSX.Element => {
         throw new Error('Some materials to save are invalid');
       }
 
+      mutateWaitingSaveProcessing(true);
+
       const options = Object.assign(optionsToSave, opts, { isSyncRevisionToHackmd: true });
 
       const markdown = await hackmdEditorRef.current.getValue();
@@ -142,8 +145,16 @@ export const PageEditorByHackmd = (): JSX.Element => {
       logger.error('failed to save', error);
       toastError(error.message);
     }
+    finally {
+      mutateWaitingSaveProcessing(false);
+    }
+
   // eslint-disable-next-line max-len
-  }, [editorMode, currentPathname, revision, revisionIdHackmdSynced, optionsToSave, saveOrUpdate, pageId, currentPagePath, isNotFound, mutateEditorMode, router, updateStateAfterSave, mutateIsHackmdDraftUpdatingInRealtime]);
+  }, [
+    pageId, currentPagePath, isNotFound, router,
+    editorMode, currentPathname, revision, revisionIdHackmdSynced, optionsToSave,
+    saveOrUpdate, mutateEditorMode, updateStateAfterSave, mutateIsHackmdDraftUpdatingInRealtime, mutateWaitingSaveProcessing,
+  ]);
 
   // set handler to save and reload Page
   useEffect(() => {
@@ -249,6 +260,8 @@ export const PageEditorByHackmd = (): JSX.Element => {
    */
   const onSaveWithShortcut = useCallback(async(markdown) => {
     try {
+      mutateWaitingSaveProcessing(true);
+
       const currentPagePathOrPathname = currentPagePath || currentPathname;
       if (
         pageId == null || revisionIdHackmdSynced == null || currentPagePathOrPathname == null || optionsToSave == null
@@ -278,8 +291,16 @@ export const PageEditorByHackmd = (): JSX.Element => {
       logger.error('failed to save', error);
       toastError(error.message);
     }
+    finally {
+      mutateWaitingSaveProcessing(false);
+    }
+
   // eslint-disable-next-line max-len
-  }, [currentPagePath, currentPathname, pageId, revisionIdHackmdSynced, optionsToSave, saveOrUpdate, mutatePageData, updateStateAfterSave, mutateTagsInfo, mutateIsEnabledUnsavedWarning, t]);
+  }, [
+    currentPagePath, currentPathname, pageId, revisionIdHackmdSynced, optionsToSave,
+    saveOrUpdate,
+    mutateWaitingSaveProcessing, mutatePageData, updateStateAfterSave, mutateTagsInfo, mutateIsEnabledUnsavedWarning, t,
+  ]);
 
   /**
    * onChange event of HackmdEditor handler

+ 14 - 2
apps/app/src/components/SavePageControls.tsx

@@ -13,6 +13,7 @@ import { IPageGrantData } from '~/interfaces/page';
 import {
   useIsEditable, useIsAclEnabled,
 } from '~/stores/context';
+import { useWaitingSaveProcessing } from '~/stores/editor';
 import { useCurrentPagePath, useCurrentPageId } from '~/stores/page';
 import { useSelectedGrant } from '~/stores/ui';
 import loggerFactory from '~/utils/logger';
@@ -42,7 +43,9 @@ export const SavePageControls = (props: SavePageControlsProps): JSX.Element | nu
   const { data: isAclEnabled } = useIsAclEnabled();
   const { data: grantData, mutate: mutateGrant } = useSelectedGrant();
   const { data: pageId } = useCurrentPageId();
+  const { data: _isWaitingSaveProcessing } = useWaitingSaveProcessing();
 
+  const isWaitingSaveProcessing = _isWaitingSaveProcessing === true; // ignore undefined
 
   const updateGrantHandler = useCallback((grantData: IPageGrantData): void => {
     mutateGrant(grantData);
@@ -91,10 +94,19 @@ export const SavePageControls = (props: SavePageControlsProps): JSX.Element | nu
       }
 
       <UncontrolledButtonDropdown direction="up">
-        <Button data-testid="save-page-btn" id="caret" color="primary" className="btn-submit" onClick={save}>
+        <Button
+          id="caret" data-testid="save-page-btn"
+          color="primary"
+          className="btn-submit"
+          onClick={save}
+          disabled={isWaitingSaveProcessing}
+        >
+          { isWaitingSaveProcessing && (
+            <i className="fa fa-spinner fa-pulse mr-1"></i>
+          ) }
           {labelSubmitButton}
         </Button>
-        <DropdownToggle caret color="primary" />
+        <DropdownToggle caret color="primary" disabled={isWaitingSaveProcessing} />
         <DropdownMenu right>
           <DropdownItem onClick={saveAndOverwriteScopesOfDescendants}>
             {labelOverwriteScopes}

+ 5 - 0
apps/app/src/stores/editor.tsx

@@ -17,6 +17,11 @@ import { useSWRxTagsInfo } from './page';
 import { useStaticSWR } from './use-static-swr';
 
 
+export const useWaitingSaveProcessing = (): SWRResponse<boolean, Error> => {
+  return useStaticSWR('waitingSaveProcessing', undefined, { fallbackData: false });
+};
+
+
 export const useEditingMarkdown = (initialData?: string): SWRResponse<string, Error> => {
   return useStaticSWR('editingMarkdown', initialData);
 };

+ 13 - 6
apps/app/test/cypress/integration/20-basic-features/20-basic-features--use-tools.spec.ts

@@ -44,16 +44,16 @@ context('Modal for page operation', () => {
       cy.screenshot(`${ssPrefix}today-add-page-name`);
       cy.getByTestid('btn-create-memo').click();
     });
-    cy.getByTestid('page-editor').should('be.visible');
 
+    cy.getByTestid('page-editor').should('be.visible');
+    cy.getByTestid('save-page-btn').as('save-page-btn').should('be.visible');
     cy.waitUntil(() => {
       // do
-      cy.getByTestid('save-page-btn').should('be.visible').click();
+      cy.get('@save-page-btn').click();
       // wait until
-      return cy.get('.layout-root').then($elem => $elem.hasClass('editing'));
+      return cy.get('@save-page-btn').then($elem => $elem.is(':disabled'));
     });
-
-    cy.getByTestid('grw-contextual-sub-nav').should('be.visible');
+    cy.get('.layout-root').should('not.have.class', 'editing');
 
     cy.collapseSidebar(true);
     cy.waitUntilSkeletonDisappear();
@@ -80,8 +80,15 @@ context('Modal for page operation', () => {
       cy.screenshot(`${ssPrefix}under-path-add-page-name`);
       cy.getByTestid('btn-create-page-under-below').click();
     });
+
     cy.getByTestid('page-editor').should('be.visible');
-    cy.getByTestid('save-page-btn').click();
+    cy.getByTestid('save-page-btn').as('save-page-btn').should('be.visible');
+    cy.waitUntil(() => {
+      // do
+      cy.get('@save-page-btn').click();
+      // wait until
+      return cy.get('@save-page-btn').then($elem => $elem.is(':disabled'));
+    });
     cy.get('.layout-root').should('not.have.class', 'editing');
 
     cy.getByTestid('grw-contextual-sub-nav').should('be.visible');