Yuki Takei 6 месяцев назад
Родитель
Сommit
0f132f44ab

+ 10 - 16
apps/app/src/client/components/PageComment/CommentEditor.tsx

@@ -22,13 +22,13 @@ import { toastError } from '~/client/util/toastr';
 import { useCurrentUser } from '~/states/global';
 import { useCurrentPagePath } from '~/states/page';
 import { isSlackConfiguredAtom } from '~/states/server-configurations';
+import { useCommentEditorsDirtyMap } from '~/states/ui/unsaved-warning';
 import { useAcceptedUploadFileType } from '~/stores-universal/context';
 import { useNextThemes } from '~/stores-universal/use-next-themes';
 import { useSWRxPageComment } from '~/stores/comment';
 import {
-  useSWRxSlackChannels, useIsSlackEnabled, useIsEnabledUnsavedWarning, useEditorSettings,
+  useSWRxSlackChannels, useIsSlackEnabled, useEditorSettings,
 } from '~/stores/editor';
-import { useCommentEditorDirtyMap } from '~/stores/ui';
 import loggerFactory from '~/utils/logger';
 
 import { NotAvailableForGuest } from '../NotAvailableForGuest';
@@ -86,11 +86,7 @@ export const CommentEditor = (props: CommentEditorProps): JSX.Element => {
   const { data: slackChannelsData } = useSWRxSlackChannels(currentPagePath);
   const isSlackConfigured = useAtomValue(isSlackConfiguredAtom);
   const { data: editorSettings } = useEditorSettings();
-  const { mutate: mutateIsEnabledUnsavedWarning } = useIsEnabledUnsavedWarning();
-  const {
-    evaluate: evaluateEditorDirtyMap,
-    clean: cleanEditorDirtyMap,
-  } = useCommentEditorDirtyMap();
+  const { markDirty, markClean } = useCommentEditorsDirtyMap();
   const { mutate: mutateResolvedTheme } = useResolvedThemeForEditor();
   const { resolvedTheme } = useNextThemes();
   mutateResolvedTheme({ themeData: resolvedTheme });
@@ -136,23 +132,22 @@ export const CommentEditor = (props: CommentEditorProps): JSX.Element => {
     setSlackChannels(slackChannels);
   }, []);
 
-  const initializeEditor = useCallback(async() => {
-    const dirtyNum = await cleanEditorDirtyMap(editorKey);
-    mutateIsEnabledUnsavedWarning(dirtyNum > 0);
+  const initializeEditor = useCallback(() => {
+    markClean(editorKey);
 
     setShowPreview(false);
     setError(undefined);
 
     initializeSlackEnabled();
 
-  }, [editorKey, cleanEditorDirtyMap, mutateIsEnabledUnsavedWarning, initializeSlackEnabled]);
+  }, [editorKey, markClean, initializeSlackEnabled]);
 
   const cancelButtonClickedHandler = useCallback(() => {
     initializeEditor();
     onCanceled?.();
   }, [onCanceled, initializeEditor]);
 
-  const postCommentHandler = useCallback(async() => {
+  const postCommentHandler = useCallback(async () => {
     const commentBodyToPost = codeMirrorEditor?.getDocString() ?? '';
 
     try {
@@ -210,11 +205,10 @@ export const CommentEditor = (props: CommentEditorProps): JSX.Element => {
   }, [codeMirrorEditor, pageId]);
 
   const cmProps = useMemo(() => ({
-    onChange: async(value: string) => {
-      const dirtyNum = await evaluateEditorDirtyMap(editorKey, value);
-      mutateIsEnabledUnsavedWarning(dirtyNum > 0);
+    onChange: (value: string) => {
+      markDirty(editorKey, value);
     },
-  }), [editorKey, evaluateEditorDirtyMap, mutateIsEnabledUnsavedWarning]);
+  }), [editorKey, markDirty]);
 
 
   // initialize CodeMirrorEditor

+ 5 - 5
apps/app/src/client/components/UnsavedAlertDialog.tsx

@@ -5,12 +5,12 @@ import React, {
 import { useTranslation } from 'next-i18next';
 import { useRouter } from 'next/router';
 
-import { useIsEnabledUnsavedWarning } from '~/stores/editor';
+import { useUnsavedWarning } from '~/states/ui/unsaved-warning';
 
 const UnsavedAlertDialog = (): JSX.Element => {
   const { t } = useTranslation();
   const router = useRouter();
-  const { data: isEnabledUnsavedWarning, mutate: mutateIsEnabledUnsavedWarning } = useIsEnabledUnsavedWarning();
+  const { isEnabled: isEnabledUnsavedWarning, reset } = useUnsavedWarning();
 
   const alertUnsavedWarningByBrowser = useCallback((e) => {
     if (isEnabledUnsavedWarning) {
@@ -29,15 +29,15 @@ const UnsavedAlertDialog = (): JSX.Element => {
       // eslint-disable-next-line no-alert
       const answer = window.confirm(t('page_edit.changes_not_saved'));
       if (!answer) {
-      // eslint-disable-next-line no-throw-literal
+        // eslint-disable-next-line no-throw-literal
         throw 'Abort route';
       }
     }
   }, [isEnabledUnsavedWarning, t]);
 
   const onRouterChangeComplete = useCallback(() => {
-    mutateIsEnabledUnsavedWarning(false);
-  }, [mutateIsEnabledUnsavedWarning]);
+    reset();
+  }, [reset]);
 
   /*
   * Route changes by Browser

+ 63 - 0
apps/app/src/states/ui/unsaved-warning.ts

@@ -0,0 +1,63 @@
+import { useCallback, useLayoutEffect } from 'react';
+import { useRouter } from 'next/router';
+
+import { atom, useAtomValue, useSetAtom } from 'jotai';
+
+// Type definitions
+type CommentEditorDirtyMapData = Map<string, boolean>;
+
+// Internal atoms
+const commentEditorDirtyMapAtom = atom<CommentEditorDirtyMapData>(new Map());
+
+// Derived atom for unsaved warning state
+const isUnsavedWarningEnabledAtom = atom((get) => {
+  const dirtyMap = get(commentEditorDirtyMapAtom);
+  return dirtyMap.size > 0;
+});
+
+// Hook 1: Read warning state + global control (for UnsavedAlertDialog)
+export const useUnsavedWarning = () => {
+  const router = useRouter();
+  const isEnabled = useAtomValue(isUnsavedWarningEnabledAtom);
+  const setDirtyMap = useSetAtom(commentEditorDirtyMapAtom);
+
+  const reset = useCallback(() => {
+    setDirtyMap(new Map());
+  }, [setDirtyMap]);
+
+  // Router event handling
+  useLayoutEffect(() => {
+    router.events.on('routeChangeComplete', reset);
+    return () => {
+      router.events.off('routeChangeComplete', reset);
+    };
+  }, [reset, router.events]);
+
+  return { isEnabled, reset };
+};
+// Hook 2: Action-only hook (for CommentEditor)
+export const useCommentEditorsDirtyMap = () => {
+  const setDirtyMap = useSetAtom(commentEditorDirtyMapAtom);
+
+  const markDirty = useCallback((editorKey: string, content: string) => {
+    setDirtyMap((current) => {
+      const newMap = new Map(current);
+      if (content.length === 0) {
+        newMap.delete(editorKey);
+      } else {
+        newMap.set(editorKey, true);
+      }
+      return newMap;
+    });
+  }, [setDirtyMap]);
+
+  const markClean = useCallback((editorKey: string) => {
+    setDirtyMap((current) => {
+      const newMap = new Map(current);
+      newMap.delete(editorKey);
+      return newMap;
+    });
+  }, [setDirtyMap]);
+
+  return { markDirty, markClean };
+};

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

@@ -46,7 +46,7 @@ export const useEditorSettings = (): SWRResponseWithUtils<EditorSettingsOperatio
   );
 
   return withUtils<EditorSettingsOperation, EditorSettings, Error>(swrResult, {
-    update: async(updateData) => {
+    update: async (updateData) => {
       const { data, mutate } = swrResult;
 
       if (data == null) {
@@ -111,10 +111,6 @@ export const usePageTagsForEditors = (pageId: Nullable<string>): SWRResponse<str
   };
 };
 
-export const useIsEnabledUnsavedWarning = (): SWRResponse<boolean, Error> => {
-  return useSWRStatic<boolean, Error>('isEnabledUnsavedWarning');
-};
-
 
 export const useReservedNextCaretLine = (initialData?: number): SWRResponse<number> => {
 

+ 0 - 68
apps/app/src/stores/ui.tsx

@@ -1,11 +1,4 @@
-import {
-  useCallback,
-  useLayoutEffect,
-} from 'react';
-
-import { useSWRStatic } from '@growi/core/dist/swr';
 import { pagePathUtils } from '@growi/core/dist/utils';
-import { useRouter } from 'next/router';
 import {
   type SWRResponse,
 } from 'swr';
@@ -22,72 +15,11 @@ import { useShareLinkId } from '~/states/page/hooks';
 import { EditorMode, useEditorMode } from '~/states/ui/editor';
 import loggerFactory from '~/utils/logger';
 
-import { useStaticSWR } from './use-static-swr';
-
 const { isTrashTopPage, isUsersTopPage } = pagePathUtils;
 
 const logger = loggerFactory('growi:stores:ui');
 
 
-/** **********************************************************
- *                          SWR Hooks
- *                      for switching UI
- *********************************************************** */
-
-
-type UseCommentEditorDirtyMapOperation = {
-  evaluate(key: string, commentBody: string): Promise<number>,
-  clean(key: string): Promise<number>,
-}
-
-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 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 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,
-    evaluate,
-    clean,
-  };
-};
-
-
 /** **********************************************************
  *                          SWR Hooks
  *                Determined value by context