Procházet zdrojové kódy

refactor dirty evaluation

Yuki Takei před 2 roky
rodič
revize
a710c77586

+ 23 - 19
apps/app/src/components/PageComment/CommentEditor.tsx

@@ -24,7 +24,7 @@ import {
   useSWRxSlackChannels, useIsSlackEnabled, useIsEnabledUnsavedWarning, useEditorSettings,
 } from '~/stores/editor';
 import { useCurrentPagePath } from '~/stores/page';
-import { useEditingCommentsNum } from '~/stores/ui';
+import { useCommentEditorDirtyMap } from '~/stores/ui';
 import { useNextThemes } from '~/stores/use-next-themes';
 import loggerFactory from '~/utils/logger';
 
@@ -85,16 +85,25 @@ export const CommentEditor = (props: CommentEditorProps): JSX.Element => {
   const { data: editorSettings } = useEditorSettings();
   const { mutate: mutateIsEnabledUnsavedWarning } = useIsEnabledUnsavedWarning();
   const {
-    increment: incrementEditingCommentsNum,
-    decrement: decrementEditingCommentsNum,
-  } = useEditingCommentsNum();
+    evaluate: evaluateEditorDirtyMap,
+    clean: cleanEditorDirtyMap,
+  } = useCommentEditorDirtyMap();
   const { mutate: mutateResolvedTheme } = useResolvedThemeForEditor();
   const { resolvedTheme } = useNextThemes();
   mutateResolvedTheme({ themeData: resolvedTheme });
 
-  const { data: codeMirrorEditor } = useCodeMirrorEditorIsolated(currentCommentId ?? GlobalCodeMirrorEditorKey.COMMENT_NEW);
+  const editorKey = useMemo(() => {
+    if (replyTo != null) {
+      return `comment_replyTo_${replyTo}`;
+    }
+    if (currentCommentId != null) {
+      return `comment_edit_${currentCommentId}`;
+    }
+    return GlobalCodeMirrorEditorKey.COMMENT_NEW;
+  }, [currentCommentId, replyTo]);
+
+  const { data: codeMirrorEditor } = useCodeMirrorEditorIsolated(editorKey);
 
-  const [isInitialized, setInitialized] = useState(true);
   const [showPreview, setShowPreview] = useState(false);
   const [error, setError] = useState();
   const [slackChannels, setSlackChannels] = useState<string>('');
@@ -125,20 +134,15 @@ export const CommentEditor = (props: CommentEditorProps): JSX.Element => {
   }, []);
 
   const initializeEditor = useCallback(async() => {
-    if (!isInitialized) {
-      const editingCommentsNum = await decrementEditingCommentsNum();
-      if (editingCommentsNum != null && editingCommentsNum === 0) {
-        mutateIsEnabledUnsavedWarning(false); // must be after clearing comment or else onChange will override bool
-      }
-    }
+    const dirtyNum = await cleanEditorDirtyMap(editorKey);
+    mutateIsEnabledUnsavedWarning(dirtyNum > 0);
 
-    setInitialized(true);
     setShowPreview(false);
     setError(undefined);
 
     initializeSlackEnabled();
 
-  }, [isInitialized, initializeSlackEnabled, decrementEditingCommentsNum, mutateIsEnabledUnsavedWarning]);
+  }, [editorKey, cleanEditorDirtyMap, mutateIsEnabledUnsavedWarning, initializeSlackEnabled]);
 
   const cancelButtonClickedHandler = useCallback(() => {
     initializeEditor();
@@ -204,10 +208,10 @@ export const CommentEditor = (props: CommentEditorProps): JSX.Element => {
     });
   }, [codeMirrorEditor, pageId]);
 
-  const onChangeHandler = useCallback(() => {
-    incrementEditingCommentsNum();
-    setInitialized(false);
-  }, [incrementEditingCommentsNum]);
+  const onChangeHandler = useCallback(async(value: string) => {
+    const dirtyNum = await evaluateEditorDirtyMap(editorKey, value);
+    mutateIsEnabledUnsavedWarning(dirtyNum > 0);
+  }, [editorKey, evaluateEditorDirtyMap, mutateIsEnabledUnsavedWarning]);
 
   // initialize CodeMirrorEditor
   useEffect(() => {
@@ -257,7 +261,7 @@ export const CommentEditor = (props: CommentEditorProps): JSX.Element => {
         <TabContent activeTab={showPreview ? 'comment_preview' : 'comment_editor'}>
           <TabPane tabId="comment_editor">
             <CodeMirrorEditorComment
-              commentId={currentCommentId}
+              editorKey={editorKey}
               acceptedUploadFileType={acceptedUploadFileType}
               onChange={onChangeHandler}
               onSave={postCommentHandler}

+ 42 - 13
apps/app/src/stores/ui.tsx

@@ -1,5 +1,6 @@
 import {
   type RefObject, useCallback, useEffect,
+  useLayoutEffect,
 } from 'react';
 
 import { PageGrant, type Nullable } from '@growi/core';
@@ -7,6 +8,7 @@ import { type SWRResponseWithUtils, useSWRStatic, withUtils } from '@growi/core/
 import { pagePathUtils, isClient, isServer } from '@growi/core/dist/utils';
 import { Breakpoint } from '@growi/ui/dist/interfaces';
 import { addBreakpointListener, cleanupBreakpointListener } from '@growi/ui/dist/utils';
+import { useRouter } from 'next/router';
 import type { HtmlElementNode } from 'rehype-toc';
 import type SimpleBar from 'simplebar-react';
 import type { MutatorOptions } from 'swr';
@@ -15,9 +17,8 @@ import {
 } from 'swr';
 import useSWRImmutable from 'swr/immutable';
 
-import type { IFocusable } from '~/client/interfaces/focusable';
 import { scheduleToPut } from '~/client/services/user-ui-settings';
-import type { IPageGrantData, IPageSelectedGrant } from '~/interfaces/page';
+import type { IPageSelectedGrant } from '~/interfaces/page';
 import { SidebarContentsType, SidebarMode } from '~/interfaces/ui';
 import type { UpdateDescCountData } from '~/interfaces/websocket';
 import {
@@ -385,27 +386,55 @@ export const usePageTreeDescCountMap = (initialData?: UpdateDescCountData): SWRR
 };
 
 
-type EditingCommentsNumOperation = {
-  increment(): Promise<number | undefined>,
-  decrement(): Promise<number | undefined>,
+type UseCommentEditorDirtyMapOperation = {
+  evaluate(key: string, commentBody: string): Promise<number>,
+  clean(key: string): Promise<number>,
 }
 
-export const useEditingCommentsNum = (): SWRResponse<number, Error> & EditingCommentsNumOperation => {
-  const swrResponse = useSWRStatic<number, Error>('editingCommentsNum', undefined, { fallbackData: 0 });
+export const useCommentEditorDirtyMap = (): SWRResponse<Map<string, boolean>, Error> & UseCommentEditorDirtyMapOperation => {
+  const router = useRouter();
+
+  const swrResponse = useSWRStatic<Map<string, boolean>, Error>('editingCommentsNum', undefined, { fallbackData: new Map() });
 
   const { mutate } = swrResponse;
 
-  const increment = useCallback(() => {
-    return mutate(prevData => (prevData ?? 0) + 1);
+  const evaluate = useCallback(async(key: string, commentBody: string) => {
+    const newMap = await mutate((map) => {
+      if (map == null) return new Map();
+
+      if (commentBody.length === 0) {
+        map.delete(key);
+      }
+      else {
+        map.set(key, true);
+      }
+
+      return map;
+    });
+    return newMap?.size ?? 0;
   }, [mutate]);
-  const decrement = useCallback(() => {
-    return mutate(prevData => Math.max(0, (prevData ?? 0) - 1));
+  const clean = useCallback(async(key: string) => {
+    const newMap = await mutate((map) => {
+      if (map == null) return new Map();
+      map.delete(key);
+      return map;
+    });
+    return newMap?.size ?? 0;
   }, [mutate]);
 
+  const reset = useCallback(() => mutate(new Map()), [mutate]);
+
+  useLayoutEffect(() => {
+    router.events.on('routeChangeComplete', reset);
+    return () => {
+      router.events.off('routeChangeComplete', reset);
+    };
+  }, [reset, router.events]);
+
   return {
     ...swrResponse,
-    increment,
-    decrement,
+    evaluate,
+    clean,
   };
 };
 

+ 1 - 1
packages/editor/src/components/CodeMirrorEditor/CodeMirrorEditor.tsx

@@ -30,6 +30,7 @@ const CodeMirrorEditorContainer = forwardRef<HTMLDivElement>((props, ref) => {
 });
 
 export type CodeMirrorEditorProps = {
+  editorKey: string | GlobalCodeMirrorEditorKey,
   acceptedUploadFileType?: AcceptedUploadFileType,
   indentSize?: number,
   editorSettings?: EditorSettings,
@@ -40,7 +41,6 @@ export type CodeMirrorEditorProps = {
 }
 
 type Props = CodeMirrorEditorProps & {
-  editorKey: string | GlobalCodeMirrorEditorKey,
   hideToolbar?: boolean,
 }
 

+ 6 - 12
packages/editor/src/components/CodeMirrorEditorComment.tsx

@@ -1,9 +1,8 @@
-import { useEffect } from 'react';
+import { memo, useEffect } from 'react';
 
 import type { Extension } from '@codemirror/state';
 import { keymap, scrollPastEnd } from '@codemirror/view';
 
-import { GlobalCodeMirrorEditorKey } from '../consts';
 import { useCodeMirrorEditorIsolated } from '../stores';
 
 import { CodeMirrorEditor, CodeMirrorEditorProps } from '.';
@@ -14,18 +13,13 @@ const additionalExtensions: Extension[] = [
 ];
 
 
-type Props = CodeMirrorEditorProps & {
-  commentId?: string,
-}
-
-export const CodeMirrorEditorComment = (props: Props): JSX.Element => {
+export const CodeMirrorEditorComment = memo((props: CodeMirrorEditorProps): JSX.Element => {
   const {
-    commentId,
+    editorKey,
     onSave, ...rest
   } = props;
 
-  const key = commentId ?? GlobalCodeMirrorEditorKey.COMMENT_NEW;
-  const { data: codeMirrorEditor } = useCodeMirrorEditorIsolated(key);
+  const { data: codeMirrorEditor } = useCodeMirrorEditorIsolated(editorKey);
 
   // setup additional extensions
   useEffect(() => {
@@ -59,9 +53,9 @@ export const CodeMirrorEditorComment = (props: Props): JSX.Element => {
 
   return (
     <CodeMirrorEditor
-      editorKey={key}
+      editorKey={editorKey}
       onSave={onSave}
       {...rest}
     />
   );
-};
+});