|
@@ -4,9 +4,10 @@ import React, {
|
|
|
|
|
|
|
|
import EventEmitter from 'events';
|
|
import EventEmitter from 'events';
|
|
|
|
|
|
|
|
-import { envUtils, PageGrant } from '@growi/core';
|
|
|
|
|
|
|
+import { envUtils, IPageHasId, PageGrant } from '@growi/core';
|
|
|
import detectIndent from 'detect-indent';
|
|
import detectIndent from 'detect-indent';
|
|
|
import { useTranslation } from 'next-i18next';
|
|
import { useTranslation } from 'next-i18next';
|
|
|
|
|
+import { useRouter } from 'next/router';
|
|
|
import { throttle, debounce } from 'throttle-debounce';
|
|
import { throttle, debounce } from 'throttle-debounce';
|
|
|
|
|
|
|
|
import { saveOrUpdate } from '~/client/services/page-operation';
|
|
import { saveOrUpdate } from '~/client/services/page-operation';
|
|
@@ -16,7 +17,7 @@ import { getOptionsToSave } from '~/client/util/editor';
|
|
|
import { IEditorMethods } from '~/interfaces/editor-methods';
|
|
import { IEditorMethods } from '~/interfaces/editor-methods';
|
|
|
import {
|
|
import {
|
|
|
useCurrentPathname, useCurrentPageId,
|
|
useCurrentPathname, useCurrentPageId,
|
|
|
- useIsEditable, useIsIndentSizeForced, useIsUploadableFile, useIsUploadableImage, useEditingMarkdown,
|
|
|
|
|
|
|
+ useIsEditable, useIsIndentSizeForced, useIsUploadableFile, useIsUploadableImage, useEditingMarkdown, useIsNotFound,
|
|
|
} from '~/stores/context';
|
|
} from '~/stores/context';
|
|
|
import {
|
|
import {
|
|
|
useCurrentIndentSize, useSWRxSlackChannels, useIsSlackEnabled, useIsTextlintEnabled, usePageTagsForEditors,
|
|
useCurrentIndentSize, useSWRxSlackChannels, useIsSlackEnabled, useIsTextlintEnabled, usePageTagsForEditors,
|
|
@@ -51,7 +52,10 @@ let isOriginOfScrollSyncPreview = false;
|
|
|
const PageEditor = React.memo((): JSX.Element => {
|
|
const PageEditor = React.memo((): JSX.Element => {
|
|
|
|
|
|
|
|
const { t } = useTranslation();
|
|
const { t } = useTranslation();
|
|
|
- const { data: pageId } = useCurrentPageId();
|
|
|
|
|
|
|
+ const router = useRouter();
|
|
|
|
|
+
|
|
|
|
|
+ const { data: isNotFound } = useIsNotFound();
|
|
|
|
|
+ const { data: pageId, mutate: mutateCurrentPageId } = useCurrentPageId();
|
|
|
const { data: currentPagePath } = useCurrentPagePath();
|
|
const { data: currentPagePath } = useCurrentPagePath();
|
|
|
const { data: currentPathname } = useCurrentPathname();
|
|
const { data: currentPathname } = useCurrentPathname();
|
|
|
const { data: currentPage, mutate: mutateCurrentPage } = useSWRxCurrentPage();
|
|
const { data: currentPage, mutate: mutateCurrentPage } = useSWRxCurrentPage();
|
|
@@ -66,7 +70,7 @@ const PageEditor = React.memo((): JSX.Element => {
|
|
|
const { data: isTextlintEnabled } = useIsTextlintEnabled();
|
|
const { data: isTextlintEnabled } = useIsTextlintEnabled();
|
|
|
const { data: isIndentSizeForced } = useIsIndentSizeForced();
|
|
const { data: isIndentSizeForced } = useIsIndentSizeForced();
|
|
|
const { data: indentSize, mutate: mutateCurrentIndentSize } = useCurrentIndentSize();
|
|
const { data: indentSize, mutate: mutateCurrentIndentSize } = useCurrentIndentSize();
|
|
|
- const { mutate: mutateIsEnabledUnsavedWarning } = useIsEnabledUnsavedWarning();
|
|
|
|
|
|
|
+ const { data: isEnabledUnsavedWarning, mutate: mutateIsEnabledUnsavedWarning } = useIsEnabledUnsavedWarning();
|
|
|
const { data: isUploadableFile } = useIsUploadableFile();
|
|
const { data: isUploadableFile } = useIsUploadableFile();
|
|
|
const { data: isUploadableImage } = useIsUploadableImage();
|
|
const { data: isUploadableImage } = useIsUploadableImage();
|
|
|
|
|
|
|
@@ -112,7 +116,7 @@ const PageEditor = React.memo((): JSX.Element => {
|
|
|
}, [setMarkdownWithDebounce]);
|
|
}, [setMarkdownWithDebounce]);
|
|
|
|
|
|
|
|
// return true if the save succeeds, otherwise false.
|
|
// return true if the save succeeds, otherwise false.
|
|
|
- const save = useCallback(async(opts?: {overwriteScopesOfDescendants: boolean}): Promise<boolean> => {
|
|
|
|
|
|
|
+ const save = useCallback(async(opts?: {overwriteScopesOfDescendants: boolean}): Promise<IPageHasId | null> => {
|
|
|
if (grantData == null || isSlackEnabled == null || currentPathname == null) {
|
|
if (grantData == null || isSlackEnabled == null || currentPathname == null) {
|
|
|
logger.error('Some materials to save are invalid', { grantData, isSlackEnabled, currentPathname });
|
|
logger.error('Some materials to save are invalid', { grantData, isSlackEnabled, currentPathname });
|
|
|
throw new Error('Some materials to save are invalid');
|
|
throw new Error('Some materials to save are invalid');
|
|
@@ -127,10 +131,13 @@ const PageEditor = React.memo((): JSX.Element => {
|
|
|
);
|
|
);
|
|
|
|
|
|
|
|
try {
|
|
try {
|
|
|
- await saveOrUpdate(optionsToSave, { pageId, path: currentPagePath || currentPathname, revisionId: currentRevisionId }, markdownToSave.current);
|
|
|
|
|
- await mutateCurrentPage();
|
|
|
|
|
- mutateIsEnabledUnsavedWarning(false);
|
|
|
|
|
- return true;
|
|
|
|
|
|
|
+ const { page } = await saveOrUpdate(
|
|
|
|
|
+ optionsToSave,
|
|
|
|
|
+ { pageId, path: currentPagePath || currentPathname, revisionId: currentRevisionId },
|
|
|
|
|
+ markdownToSave.current,
|
|
|
|
|
+ );
|
|
|
|
|
+
|
|
|
|
|
+ return page;
|
|
|
}
|
|
}
|
|
|
catch (error) {
|
|
catch (error) {
|
|
|
logger.error('failed to save', error);
|
|
logger.error('failed to save', error);
|
|
@@ -143,20 +150,34 @@ const PageEditor = React.memo((): JSX.Element => {
|
|
|
// lastUpdateUser: error.data.user,
|
|
// lastUpdateUser: error.data.user,
|
|
|
// });
|
|
// });
|
|
|
}
|
|
}
|
|
|
- return false;
|
|
|
|
|
|
|
+ return null;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// eslint-disable-next-line max-len
|
|
// eslint-disable-next-line max-len
|
|
|
- }, [grantData, isSlackEnabled, currentPathname, slackChannels, pageTags, pageId, currentPagePath, currentRevisionId, mutateCurrentPage, mutateIsEnabledUnsavedWarning]);
|
|
|
|
|
|
|
+ }, [grantData, isSlackEnabled, currentPathname, slackChannels, pageTags, pageId, currentPagePath, currentRevisionId]);
|
|
|
|
|
|
|
|
const saveAndReturnToViewHandler = useCallback(async(opts?: {overwriteScopesOfDescendants: boolean}) => {
|
|
const saveAndReturnToViewHandler = useCallback(async(opts?: {overwriteScopesOfDescendants: boolean}) => {
|
|
|
if (editorMode !== EditorMode.Editor) {
|
|
if (editorMode !== EditorMode.Editor) {
|
|
|
return;
|
|
return;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- await save(opts);
|
|
|
|
|
|
|
+ const page = await save(opts);
|
|
|
|
|
+ if (page == null) {
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ // The updateFn should be a promise or asynchronous function to handle the remote mutation
|
|
|
|
|
+ // it should return updated data. see: https://swr.vercel.app/docs/mutation#optimistic-updates
|
|
|
|
|
+ // Moreover, `async() => false` does not work since it's too fast to be calculated.
|
|
|
|
|
+ await mutateIsEnabledUnsavedWarning(new Promise(r => setTimeout(() => r(false), 10)), { optimisticData: () => false });
|
|
|
|
|
+ if (isNotFound) {
|
|
|
|
|
+ await router.push(`/${page._id}`);
|
|
|
|
|
+ }
|
|
|
|
|
+ else {
|
|
|
|
|
+ await mutateCurrentPageId(page._id);
|
|
|
|
|
+ await mutateCurrentPage();
|
|
|
|
|
+ }
|
|
|
mutateEditorMode(EditorMode.View);
|
|
mutateEditorMode(EditorMode.View);
|
|
|
- }, [editorMode, save, mutateEditorMode]);
|
|
|
|
|
|
|
+ }, [editorMode, save, mutateIsEnabledUnsavedWarning, isNotFound, mutateEditorMode, router, mutateCurrentPageId, mutateCurrentPage]);
|
|
|
|
|
|
|
|
const saveWithShortcut = useCallback(async() => {
|
|
const saveWithShortcut = useCallback(async() => {
|
|
|
if (editorMode !== EditorMode.Editor) {
|
|
if (editorMode !== EditorMode.Editor) {
|