diff --git a/apps/app/src/components/PageEditor.tsx b/apps/app/src/components/PageEditor.tsx index 78399913144..2c9040fbb39 100644 --- a/apps/app/src/components/PageEditor.tsx +++ b/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) => { diff --git a/apps/app/src/components/PageEditorByHackmd.tsx b/apps/app/src/components/PageEditorByHackmd.tsx index 7c441aaa671..3141d64f8c6 100644 --- a/apps/app/src/components/PageEditorByHackmd.tsx +++ b/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 diff --git a/apps/app/src/components/SavePageControls.tsx b/apps/app/src/components/SavePageControls.tsx index f67ec16d816..0f34cf7c779 100644 --- a/apps/app/src/components/SavePageControls.tsx +++ b/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 } - - + {labelOverwriteScopes} diff --git a/apps/app/src/stores/editor.tsx b/apps/app/src/stores/editor.tsx index 8e2aca330a6..bb4fbdfef96 100644 --- a/apps/app/src/stores/editor.tsx +++ b/apps/app/src/stores/editor.tsx @@ -17,6 +17,11 @@ import { useSWRxTagsInfo } from './page'; import { useStaticSWR } from './use-static-swr'; +export const useWaitingSaveProcessing = (): SWRResponse => { + return useStaticSWR('waitingSaveProcessing', undefined, { fallbackData: false }); +}; + + export const useEditingMarkdown = (initialData?: string): SWRResponse => { return useStaticSWR('editingMarkdown', initialData); }; diff --git a/apps/app/test/cypress/integration/20-basic-features/20-basic-features--use-tools.spec.ts b/apps/app/test/cypress/integration/20-basic-features/20-basic-features--use-tools.spec.ts index 9c6c531c19b..5cd38a4d463 100644 --- a/apps/app/test/cypress/integration/20-basic-features/20-basic-features--use-tools.spec.ts +++ b/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');