|
@@ -8,10 +8,7 @@ import { envUtils, PageGrant } from '@growi/core';
|
|
|
import detectIndent from 'detect-indent';
|
|
import detectIndent from 'detect-indent';
|
|
|
import { throttle, debounce } from 'throttle-debounce';
|
|
import { throttle, debounce } from 'throttle-debounce';
|
|
|
|
|
|
|
|
-import { saveAndReload } from '~/client/services/page-operation';
|
|
|
|
|
-
|
|
|
|
|
-// import EditorContainer from '~/client/services/EditorContainer';
|
|
|
|
|
-// import PageContainer from '~/client/services/PageContainer';
|
|
|
|
|
|
|
+import { saveOrUpdate } from '~/client/services/page-operation';
|
|
|
import { apiGet, apiPostForm } from '~/client/util/apiv1-client';
|
|
import { apiGet, apiPostForm } from '~/client/util/apiv1-client';
|
|
|
import { getOptionsToSave } from '~/client/util/editor';
|
|
import { getOptionsToSave } from '~/client/util/editor';
|
|
|
import {
|
|
import {
|
|
@@ -34,10 +31,7 @@ import loggerFactory from '~/utils/logger';
|
|
|
import Editor from './PageEditor/Editor';
|
|
import Editor from './PageEditor/Editor';
|
|
|
import Preview from './PageEditor/Preview';
|
|
import Preview from './PageEditor/Preview';
|
|
|
import scrollSyncHelper from './PageEditor/ScrollSyncHelper';
|
|
import scrollSyncHelper from './PageEditor/ScrollSyncHelper';
|
|
|
-// import { withUnstatedContainers } from './UnstatedUtils';
|
|
|
|
|
-
|
|
|
|
|
|
|
|
|
|
-// TODO: remove this when omitting unstated is completed
|
|
|
|
|
|
|
|
|
|
const logger = loggerFactory('growi:PageEditor');
|
|
const logger = loggerFactory('growi:PageEditor');
|
|
|
|
|
|
|
@@ -55,7 +49,6 @@ type EditorRef = {
|
|
|
|
|
|
|
|
type Props = {
|
|
type Props = {
|
|
|
// pageContainer: PageContainer,
|
|
// pageContainer: PageContainer,
|
|
|
- // editorContainer: EditorContainer,
|
|
|
|
|
|
|
|
|
|
// isEditable: boolean,
|
|
// isEditable: boolean,
|
|
|
|
|
|
|
@@ -82,37 +75,34 @@ let isOriginOfScrollSyncPreview = false;
|
|
|
|
|
|
|
|
const PageEditor = React.memo((props: Props): JSX.Element => {
|
|
const PageEditor = React.memo((props: Props): JSX.Element => {
|
|
|
// const {
|
|
// const {
|
|
|
- // pageContainer, editorContainer,
|
|
|
|
|
|
|
+ // pageContainer,
|
|
|
// } = props;
|
|
// } = props;
|
|
|
|
|
|
|
|
- const { data: isEditable } = useIsEditable();
|
|
|
|
|
- const { data: editorMode } = useEditorMode();
|
|
|
|
|
- const { data: isMobile } = useIsMobile();
|
|
|
|
|
- const { data: isSlackEnabled } = useIsSlackEnabled();
|
|
|
|
|
const { data: pageId } = useCurrentPageId();
|
|
const { data: pageId } = useCurrentPageId();
|
|
|
- const { data: pageTags } = usePageTagsForEditors(pageId);
|
|
|
|
|
const { data: currentPagePath } = useCurrentPagePath();
|
|
const { data: currentPagePath } = useCurrentPagePath();
|
|
|
const { data: currentPathname } = useCurrentPathname();
|
|
const { data: currentPathname } = useCurrentPathname();
|
|
|
- const { data: slackChannelsData } = useSWRxSlackChannels(currentPagePath);
|
|
|
|
|
|
|
+ const { data: currentPage, mutate: mutateCurrentPage } = useSWRxCurrentPage();
|
|
|
const { data: grantData, mutate: mutateGrant } = useSelectedGrant();
|
|
const { data: grantData, mutate: mutateGrant } = useSelectedGrant();
|
|
|
|
|
+ const { data: pageTags } = usePageTagsForEditors(pageId);
|
|
|
|
|
+
|
|
|
|
|
+ const { data: isEditable } = useIsEditable();
|
|
|
|
|
+ const { data: editorMode, mutate: mutateEditorMode } = useEditorMode();
|
|
|
|
|
+ const { data: isMobile } = useIsMobile();
|
|
|
|
|
+ const { data: isSlackEnabled } = useIsSlackEnabled();
|
|
|
|
|
+ const { data: slackChannelsData } = useSWRxSlackChannels(currentPagePath);
|
|
|
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();
|
|
|
- const { data: currentPage } = useSWRxCurrentPage();
|
|
|
|
|
|
|
|
|
|
const { data: rendererOptions } = usePreviewOptions();
|
|
const { data: rendererOptions } = usePreviewOptions();
|
|
|
|
|
|
|
|
- const [markdown, setMarkdown] = useState<string>('');
|
|
|
|
|
-
|
|
|
|
|
|
|
+ const currentRevisionId = currentPage?.revision?._id;
|
|
|
|
|
+ const initialValue = currentPage?.revision?.body;
|
|
|
|
|
|
|
|
- useEffect(() => {
|
|
|
|
|
- if (currentPage != null) {
|
|
|
|
|
- setMarkdown(currentPage.revision?.body);
|
|
|
|
|
- }
|
|
|
|
|
- }, [currentPage, currentPage?.revision?.body]);
|
|
|
|
|
|
|
+ const [markdown, setMarkdown] = useState<string>(initialValue ?? '');
|
|
|
|
|
|
|
|
const slackChannels = useMemo(() => (slackChannelsData ? slackChannelsData.toString() : ''), []);
|
|
const slackChannels = useMemo(() => (slackChannelsData ? slackChannelsData.toString() : ''), []);
|
|
|
|
|
|
|
@@ -121,49 +111,66 @@ const PageEditor = React.memo((props: Props): JSX.Element => {
|
|
|
const previewRef = useRef<HTMLDivElement>(null);
|
|
const previewRef = useRef<HTMLDivElement>(null);
|
|
|
|
|
|
|
|
const setMarkdownWithDebounce = useMemo(() => debounce(50, throttle(100, value => setMarkdown(value))), []);
|
|
const setMarkdownWithDebounce = useMemo(() => debounce(50, throttle(100, value => setMarkdown(value))), []);
|
|
|
- // const saveDraftWithDebounce = useMemo(() => debounce(800, () => {
|
|
|
|
|
- // editorContainer.saveDraft(pageContainer.state.path, markdown);
|
|
|
|
|
- // }), [editorContainer, markdown, pageContainer.state.path]);
|
|
|
|
|
|
|
+
|
|
|
|
|
|
|
|
const markdownChangedHandler = useCallback((value: string): void => {
|
|
const markdownChangedHandler = useCallback((value: string): void => {
|
|
|
setMarkdownWithDebounce(value);
|
|
setMarkdownWithDebounce(value);
|
|
|
- // only when the first time to edit
|
|
|
|
|
- // if (!pageContainer.state.revisionId) {
|
|
|
|
|
- // saveDraftWithDebounce();
|
|
|
|
|
- // }
|
|
|
|
|
- // }, [pageContainer.state.revisionId, saveDraftWithDebounce, setMarkdownWithDebounce]);
|
|
|
|
|
}, [setMarkdownWithDebounce]);
|
|
}, [setMarkdownWithDebounce]);
|
|
|
|
|
|
|
|
-
|
|
|
|
|
- const saveWithShortcut = useCallback(async() => {
|
|
|
|
|
- if (grantData == null) {
|
|
|
|
|
- return;
|
|
|
|
|
|
|
+ const save = useCallback(async(opts?: {overwriteScopesOfDescendants: boolean}) => {
|
|
|
|
|
+ if (grantData == null || isSlackEnabled == null || currentPathname == null) {
|
|
|
|
|
+ logger.error('Some materials to save are invalid', { grantData, isSlackEnabled, currentPathname });
|
|
|
|
|
+ throw new Error('Some materials to save are invalid');
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- const optionsToSave = getOptionsToSave(
|
|
|
|
|
- isSlackEnabled ?? false, slackChannels,
|
|
|
|
|
- grantData.grant, grantData.grantedGroup?.id, grantData.grantedGroup?.name,
|
|
|
|
|
- pageTags || [],
|
|
|
|
|
|
|
+ const grant = grantData.grant || PageGrant.GRANT_PUBLIC;
|
|
|
|
|
+ const grantedGroup = grantData?.grantedGroup;
|
|
|
|
|
+
|
|
|
|
|
+ const optionsToSave = Object.assign(
|
|
|
|
|
+ getOptionsToSave(isSlackEnabled, slackChannels, grant || 1, grantedGroup?.id, grantedGroup?.name, pageTags || []),
|
|
|
|
|
+ { ...opts },
|
|
|
);
|
|
);
|
|
|
|
|
|
|
|
try {
|
|
try {
|
|
|
- // disable unsaved warning
|
|
|
|
|
|
|
+ await saveOrUpdate(optionsToSave, { pageId, path: currentPagePath || currentPathname, revisionId: currentRevisionId }, markdown);
|
|
|
|
|
+ await mutateCurrentPage();
|
|
|
mutateIsEnabledUnsavedWarning(false);
|
|
mutateIsEnabledUnsavedWarning(false);
|
|
|
-
|
|
|
|
|
- // eslint-disable-next-line no-unused-vars
|
|
|
|
|
- // const { tags } = await pageContainer.save(markdown, editorMode, optionsToSave);
|
|
|
|
|
- logger.debug('success to save');
|
|
|
|
|
-
|
|
|
|
|
- // pageContainer.showSuccessToastr();
|
|
|
|
|
-
|
|
|
|
|
- // update state of EditorContainer
|
|
|
|
|
- // editorContainer.setState({ tags });
|
|
|
|
|
}
|
|
}
|
|
|
catch (error) {
|
|
catch (error) {
|
|
|
logger.error('failed to save', error);
|
|
logger.error('failed to save', error);
|
|
|
// pageContainer.showErrorToastr(error);
|
|
// pageContainer.showErrorToastr(error);
|
|
|
|
|
+ if (error.code === 'conflict') {
|
|
|
|
|
+ // pageContainer.setState({
|
|
|
|
|
+ // remoteRevisionId: error.data.revisionId,
|
|
|
|
|
+ // remoteRevisionBody: error.data.revisionBody,
|
|
|
|
|
+ // remoteRevisionUpdateAt: error.data.createdAt,
|
|
|
|
|
+ // lastUpdateUser: error.data.user,
|
|
|
|
|
+ // });
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
- }, [grantData, isSlackEnabled, slackChannels, pageTags, mutateIsEnabledUnsavedWarning]);
|
|
|
|
|
|
|
+
|
|
|
|
|
+ // eslint-disable-next-line max-len
|
|
|
|
|
+ }, [grantData, isSlackEnabled, currentPathname, slackChannels, pageTags, pageId, currentPagePath, currentRevisionId, markdown, mutateCurrentPage, mutateIsEnabledUnsavedWarning]);
|
|
|
|
|
+
|
|
|
|
|
+ const saveAndReturnToViewHandler = useCallback(async(opts?: {overwriteScopesOfDescendants: boolean}) => {
|
|
|
|
|
+ if (editorMode !== EditorMode.Editor) {
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ await save(opts);
|
|
|
|
|
+ mutateEditorMode(EditorMode.View);
|
|
|
|
|
+ }, [editorMode, save, mutateEditorMode]);
|
|
|
|
|
+
|
|
|
|
|
+ const saveWithShortcut = useCallback(async() => {
|
|
|
|
|
+ if (editorMode !== EditorMode.Editor) {
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ await save();
|
|
|
|
|
+
|
|
|
|
|
+ // TODO: show toastr
|
|
|
|
|
+ // pageContainer.showErrorToastr(error);
|
|
|
|
|
+ }, [editorMode, save]);
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
@@ -221,7 +228,6 @@ const PageEditor = React.memo((props: Props): JSX.Element => {
|
|
|
finally {
|
|
finally {
|
|
|
editorRef.current.terminateUploadingState();
|
|
editorRef.current.terminateUploadingState();
|
|
|
}
|
|
}
|
|
|
- // }, [editorMode, mutateGrant, pageContainer]);
|
|
|
|
|
}, [currentPagePath, mutateGrant, pageId]);
|
|
}, [currentPagePath, mutateGrant, pageId]);
|
|
|
|
|
|
|
|
|
|
|
|
@@ -342,68 +348,14 @@ const PageEditor = React.memo((props: Props): JSX.Element => {
|
|
|
};
|
|
};
|
|
|
}, []);
|
|
}, []);
|
|
|
|
|
|
|
|
-
|
|
|
|
|
- const saveAndReloadHandler = useCallback(async(opts?: {overwriteScopesOfDescendants: boolean}) => {
|
|
|
|
|
- if (editorMode !== EditorMode.Editor) {
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- const grant = grantData?.grant || PageGrant.GRANT_PUBLIC;
|
|
|
|
|
- const grantedGroup = grantData?.grantedGroup;
|
|
|
|
|
-
|
|
|
|
|
- if (isSlackEnabled == null || currentPathname == null) {
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- let optionsToSave;
|
|
|
|
|
-
|
|
|
|
|
- const currentOptionsToSave = getOptionsToSave(isSlackEnabled, slackChannels, grant || 1, grantedGroup?.id, grantedGroup?.name, pageTags || []);
|
|
|
|
|
-
|
|
|
|
|
- if (opts != null) {
|
|
|
|
|
- optionsToSave = Object.assign(currentOptionsToSave, {
|
|
|
|
|
- ...opts,
|
|
|
|
|
- });
|
|
|
|
|
- }
|
|
|
|
|
- else {
|
|
|
|
|
- optionsToSave = currentOptionsToSave;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- try {
|
|
|
|
|
- await saveAndReload(optionsToSave, { pageId, path: currentPagePath || currentPathname, revisionId: currentPage?.revision?._id }, markdown);
|
|
|
|
|
- }
|
|
|
|
|
- catch (error) {
|
|
|
|
|
- logger.error('failed to save', error);
|
|
|
|
|
- // pageContainer.showErrorToastr(error);
|
|
|
|
|
- if (error.code === 'conflict') {
|
|
|
|
|
- // pageContainer.setState({
|
|
|
|
|
- // remoteRevisionId: error.data.revisionId,
|
|
|
|
|
- // remoteRevisionBody: error.data.revisionBody,
|
|
|
|
|
- // remoteRevisionUpdateAt: error.data.createdAt,
|
|
|
|
|
- // lastUpdateUser: error.data.user,
|
|
|
|
|
- // });
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- }, [currentPage?.revision?._id,
|
|
|
|
|
- currentPagePath,
|
|
|
|
|
- currentPathname,
|
|
|
|
|
- editorMode,
|
|
|
|
|
- grantData?.grant,
|
|
|
|
|
- grantData?.grantedGroup,
|
|
|
|
|
- isSlackEnabled,
|
|
|
|
|
- markdown,
|
|
|
|
|
- pageId,
|
|
|
|
|
- pageTags,
|
|
|
|
|
- slackChannels,
|
|
|
|
|
- ]);
|
|
|
|
|
-
|
|
|
|
|
- // set handler to save and reload Page
|
|
|
|
|
|
|
+ // set handler to save and return to View
|
|
|
useEffect(() => {
|
|
useEffect(() => {
|
|
|
- globalEmitter.on('saveAndReload', saveAndReloadHandler);
|
|
|
|
|
|
|
+ globalEmitter.on('saveAndReturnToView', saveAndReturnToViewHandler);
|
|
|
|
|
|
|
|
return function cleanup() {
|
|
return function cleanup() {
|
|
|
- globalEmitter.removeListener('saveAndReload', saveAndReloadHandler);
|
|
|
|
|
|
|
+ globalEmitter.removeListener('saveAndReturnToView', saveAndReturnToViewHandler);
|
|
|
};
|
|
};
|
|
|
- }, [saveAndReloadHandler]);
|
|
|
|
|
|
|
+ }, [saveAndReturnToViewHandler]);
|
|
|
|
|
|
|
|
// set handler to focus
|
|
// set handler to focus
|
|
|
useEffect(() => {
|
|
useEffect(() => {
|
|
@@ -412,27 +364,31 @@ const PageEditor = React.memo((props: Props): JSX.Element => {
|
|
|
}
|
|
}
|
|
|
}, [editorMode]);
|
|
}, [editorMode]);
|
|
|
|
|
|
|
|
|
|
+ // Unnecessary code. Delete after PageEditor and PageEditorByHackmd implementation has completed. -- 2022.09.06 Yuki Takei
|
|
|
|
|
+ //
|
|
|
// set handler to update editor value
|
|
// set handler to update editor value
|
|
|
- useEffect(() => {
|
|
|
|
|
- const handler = (markdown) => {
|
|
|
|
|
- if (editorRef.current != null) {
|
|
|
|
|
- editorRef.current.setValue(markdown);
|
|
|
|
|
- }
|
|
|
|
|
- };
|
|
|
|
|
- globalEmitter.on('updateEditorValue', handler);
|
|
|
|
|
|
|
+ // useEffect(() => {
|
|
|
|
|
+ // const handler = (markdown) => {
|
|
|
|
|
+ // if (editorRef.current != null) {
|
|
|
|
|
+ // editorRef.current.setValue(markdown);
|
|
|
|
|
+ // }
|
|
|
|
|
+ // };
|
|
|
|
|
+ // globalEmitter.on('updateEditorValue', handler);
|
|
|
|
|
|
|
|
- return function cleanup() {
|
|
|
|
|
- globalEmitter.removeListener('updateEditorValue', handler);
|
|
|
|
|
- };
|
|
|
|
|
- }, []);
|
|
|
|
|
|
|
+ // return function cleanup() {
|
|
|
|
|
+ // globalEmitter.removeListener('updateEditorValue', handler);
|
|
|
|
|
+ // };
|
|
|
|
|
+ // }, []);
|
|
|
|
|
|
|
|
- // Displays an alert if there is a difference with pageContainer's markdown
|
|
|
|
|
- // useEffect(() => {
|
|
|
|
|
- // // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
|
|
|
- // if (pageContainer.state.markdown! !== markdown) {
|
|
|
|
|
- // mutateIsEnabledUnsavedWarning(true);
|
|
|
|
|
- // }
|
|
|
|
|
- // }, [editorContainer, markdown, mutateIsEnabledUnsavedWarning, pageContainer.state.markdown]);
|
|
|
|
|
|
|
+ // Displays an alert if there is a difference with original markdown body
|
|
|
|
|
+ useEffect(() => {
|
|
|
|
|
+ if (initialValue == null || isEnabledUnsavedWarning) {
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (initialValue !== markdown) {
|
|
|
|
|
+ mutateIsEnabledUnsavedWarning(true);
|
|
|
|
|
+ }
|
|
|
|
|
+ }, [initialValue, isEnabledUnsavedWarning, markdown, mutateIsEnabledUnsavedWarning]);
|
|
|
|
|
|
|
|
// Detect indent size from contents (only when users are allowed to change it)
|
|
// Detect indent size from contents (only when users are allowed to change it)
|
|
|
// useEffect(() => {
|
|
// useEffect(() => {
|
|
@@ -456,13 +412,12 @@ const PageEditor = React.memo((props: Props): JSX.Element => {
|
|
|
|
|
|
|
|
const isUploadable = isUploadableImage || isUploadableFile;
|
|
const isUploadable = isUploadableImage || isUploadableFile;
|
|
|
|
|
|
|
|
-
|
|
|
|
|
return (
|
|
return (
|
|
|
<div className="d-flex flex-wrap">
|
|
<div className="d-flex flex-wrap">
|
|
|
<div className="page-editor-editor-container flex-grow-1 flex-basis-0 mw-0">
|
|
<div className="page-editor-editor-container flex-grow-1 flex-basis-0 mw-0">
|
|
|
<Editor
|
|
<Editor
|
|
|
ref={editorRef}
|
|
ref={editorRef}
|
|
|
- value={markdown}
|
|
|
|
|
|
|
+ value={initialValue}
|
|
|
isUploadable={isUploadable}
|
|
isUploadable={isUploadable}
|
|
|
isUploadableFile={isUploadableFile}
|
|
isUploadableFile={isUploadableFile}
|
|
|
isTextlintEnabled={isTextlintEnabled}
|
|
isTextlintEnabled={isTextlintEnabled}
|
|
@@ -495,10 +450,4 @@ const PageEditor = React.memo((props: Props): JSX.Element => {
|
|
|
});
|
|
});
|
|
|
PageEditor.displayName = 'PageEditor';
|
|
PageEditor.displayName = 'PageEditor';
|
|
|
|
|
|
|
|
-/**
|
|
|
|
|
- * Wrapper component for using unstated
|
|
|
|
|
- */
|
|
|
|
|
-// const PageEditorWrapper = withUnstatedContainers(PageEditor, [PageContainer, EditorContainer]);
|
|
|
|
|
-
|
|
|
|
|
-// export default PageEditorWrapper;
|
|
|
|
|
export default PageEditor;
|
|
export default PageEditor;
|