Răsfoiți Sursa

Merge pull request #8042 from weseek/imprv/codemirror-custom-hooks-initialization

imprv: Codemirror custom hooks initialization
Yuki Takei 2 ani în urmă
părinte
comite
56e82b18cb

+ 1 - 1
apps/app/package.json

@@ -194,7 +194,7 @@
     "string-width": "=4.2.2",
     "superjson": "^1.9.1",
     "swagger-jsdoc": "^6.1.0",
-    "swr": "^2.0.3",
+    "swr": "^2.2.2",
     "throttle-debounce": "^5.0.0",
     "uglifycss": "^0.0.29",
     "universal-bunyan": "^0.9.2",

+ 1 - 4
apps/app/src/client/services/page-operation.ts

@@ -165,10 +165,7 @@ export const useSaveOrUpdate = (): SaveOrUpdateFunction => {
       res = await updatePage(pageId, revisionId, markdown, options);
     }
 
-    // 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 });
+    mutateIsEnabledUnsavedWarning(false);
 
     return res;
   }, [mutateIsEnabledUnsavedWarning]);

+ 10 - 3
apps/app/src/components/PageEditor/PageEditor.tsx

@@ -132,6 +132,7 @@ export const PageEditor = React.memo((props: Props): JSX.Element => {
   // for https://redmine.weseek.co.jp/issues/125923
   const currentRevisionId = currentPage?.revision?._id ?? createdPageRevisionIdWithAttachment;
 
+  const initialValueRef = useRef('');
   const initialValue = useMemo(() => {
     if (!isNotFound) {
       return editingMarkdown ?? '';
@@ -145,18 +146,25 @@ export const PageEditor = React.memo((props: Props): JSX.Element => {
     if (templateBodyData != null) {
       initialValue += `${templateBodyData}\n`;
     }
+
     return initialValue;
 
   }, [isNotFound, currentPathname, editingMarkdown, isEnabledAttachTitleHeader, templateBodyData]);
 
+  useEffect(() => {
+    // set to ref
+    initialValueRef.current = initialValue;
+  }, [initialValue]);
+
+
   const [markdownToPreview, setMarkdownToPreview] = useState<string>(initialValue);
   const setMarkdownPreviewWithDebounce = useMemo(() => debounce(100, throttle(150, (value: string) => {
     setMarkdownToPreview(value);
   })), []);
   const mutateIsEnabledUnsavedWarningWithDebounce = useMemo(() => debounce(600, throttle(900, (value: string) => {
     // Displays an unsaved warning alert
-    mutateIsEnabledUnsavedWarning(value !== initialValue);
-  })), [initialValue, mutateIsEnabledUnsavedWarning]);
+    mutateIsEnabledUnsavedWarning(value !== initialValueRef.current);
+  })), [mutateIsEnabledUnsavedWarning]);
 
   const useCodeMirrorEditorMainProps = useMemo<ReactCodeMirrorProps>(() => {
     return {
@@ -471,7 +479,6 @@ export const PageEditor = React.memo((props: Props): JSX.Element => {
     if (initialValue == null) {
       return;
     }
-    // markdownToSave.current = initialValue;
     codeMirrorEditor?.initDoc(initialValue);
     setMarkdownToPreview(initialValue);
     mutateIsEnabledUnsavedWarning(false);

+ 1 - 4
apps/app/src/stores/admin/customize.tsx

@@ -49,10 +49,7 @@ export const useSWRxGrowiThemeSetting = (): SWRResponse<IResGrowiTheme, Error> &
     }
 
     const newData = { ...swrResponse.data, currentTheme: theme };
-    // 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 swrResponse.mutate(new Promise(r => setTimeout(() => r(newData), 10)), { optimisticData: () => newData });
+    swrResponse.mutate(newData, { optimisticData: newData });
   };
 
   return Object.assign(

+ 2 - 2
apps/app/src/stores/ui.tsx

@@ -181,7 +181,7 @@ export const useIsDeviceSmallerThanMd = (): SWRResponse<boolean, Error> => {
   const { cache, mutate } = useSWRConfig();
 
   useEffect(() => {
-    if (isClient()) {
+    if (key != null) {
       const mdOrAvobeHandler = function(this: MediaQueryList): void {
         // sm -> md: matches will be true
         // md -> sm: matches will be false
@@ -209,7 +209,7 @@ export const useIsDeviceSmallerThanLg = (): SWRResponse<boolean, Error> => {
   const { cache, mutate } = useSWRConfig();
 
   useEffect(() => {
-    if (isClient()) {
+    if (key != null) {
       const lgOrAvobeHandler = function(this: MediaQueryList): void {
         // md -> lg: matches will be true
         // lg -> md: matches will be false

+ 1 - 1
packages/core/package.json

@@ -73,6 +73,6 @@
   },
   "devDependencies": {
     "eslint-plugin-regex": "^1.8.0",
-    "swr": "^2.0.3"
+    "swr": "^2.2.2"
   }
 }

+ 5 - 3
packages/core/src/swr/use-swr-static.ts

@@ -23,12 +23,14 @@ export function useSWRStatic<Data, Error>(
   const { cache } = useSWRConfig();
   const swrResponse = useSWRImmutable(key, null, {
     ...configuration,
-    fallbackData: configuration?.fallbackData ?? cache.get(key)?.data,
+    fallbackData: configuration?.fallbackData ?? (
+      key != null ? cache.get(key?.toString())?.data : undefined
+    ),
   });
 
   // write data to cache directly
-  if (data !== undefined) {
-    cache.set(key, { ...cache.get(key), data });
+  if (key != null && data !== undefined) {
+    cache.set(key.toString(), { ...cache.get(key.toString()), data });
   }
 
   return swrResponse;

+ 1 - 1
packages/editor/package.json

@@ -34,6 +34,6 @@
     "eslint-plugin-react-refresh": "^0.4.1",
     "react-hook-form": "^7.45.4",
     "react-toastify": "^9.1.3",
-    "swr": "^2.0.3"
+    "swr": "^2.2.2"
   }
 }

+ 4 - 0
packages/editor/src/components/playground/Playground.tsx

@@ -25,6 +25,10 @@ export const Playground = (): JSX.Element => {
   }, []);
   const { data: codeMirrorEditor } = useCodeMirrorEditorMain(containerRef.current, props);
 
+  useEffect(() => {
+    codeMirrorEditor?.initDoc('# header\n');
+  }, [codeMirrorEditor]);
+
   // set handler to save with shortcut key
   useEffect(() => {
     const extension = keymap.of([

+ 0 - 11
packages/editor/src/services/codemirror-editor/interfaces/react-codemirror.ts

@@ -1,11 +0,0 @@
-import type { EditorState } from '@codemirror/state';
-import type { EditorView } from '@codemirror/view';
-
-export type UseCodeMirrorEditorStates = {
-  state: EditorState | undefined;
-  setState: import('react').Dispatch<import('react').SetStateAction<EditorState | undefined>>;
-  view: EditorView | undefined;
-  setView: import('react').Dispatch<import('react').SetStateAction<EditorView | undefined>>;
-  container: HTMLDivElement | undefined;
-  setContainer: import('react').Dispatch<import('react').SetStateAction<HTMLDivElement | undefined>>;
-}

+ 10 - 9
packages/editor/src/services/codemirror-editor/use-codemirror-editor/use-codemirror-editor.ts

@@ -2,11 +2,10 @@ import { useMemo } from 'react';
 
 import { markdown, markdownLanguage } from '@codemirror/lang-markdown';
 import { languages } from '@codemirror/language-data';
-import { type Extension } from '@codemirror/state';
+import { EditorState, type Extension } from '@codemirror/state';
+import { EditorView } from '@codemirror/view';
 import { useCodeMirror, type UseCodeMirror } from '@uiw/react-codemirror';
 
-import type { UseCodeMirrorEditorStates } from '../interfaces/react-codemirror';
-
 import { AppendExtension, useAppendExtension } from './utils/append-extension';
 import { useFocus, type Focus } from './utils/focus';
 import { useGetDoc, type GetDoc } from './utils/get-doc';
@@ -20,7 +19,10 @@ type UseCodeMirrorEditorUtils = {
   focus: Focus,
   setCaretLine: SetCaretLine,
 }
-export type UseCodeMirrorEditor = UseCodeMirrorEditorStates & UseCodeMirrorEditorUtils;
+export type UseCodeMirrorEditor = {
+  state: EditorState | undefined;
+  view: EditorView | undefined;
+} & UseCodeMirrorEditorUtils;
 
 
 const defaultExtensions: Extension[] = [
@@ -33,15 +35,13 @@ export const useCodeMirrorEditor = (props?: UseCodeMirror): UseCodeMirrorEditor
     return {
       ...props,
       extensions: [
-        ...defaultExtensions,
         ...(props?.extensions ?? []),
+        ...defaultExtensions,
       ],
     };
   }, [props]);
 
-  const codemirror = useCodeMirror(mergedProps);
-
-  const { view } = codemirror;
+  const { state, view } = useCodeMirror(mergedProps);
 
   const initDoc = useInitDoc(view);
   const appendExtension = useAppendExtension(view);
@@ -50,7 +50,8 @@ export const useCodeMirrorEditor = (props?: UseCodeMirror): UseCodeMirrorEditor
   const setCaretLine = useSetCaretLine(view);
 
   return {
-    ...codemirror,
+    state,
+    view,
     initDoc,
     appendExtension,
     getDoc,

+ 26 - 3
packages/editor/src/stores/codemirror-editor.ts

@@ -1,4 +1,4 @@
-import { useMemo } from 'react';
+import { useMemo, useRef } from 'react';
 
 import { type Extension } from '@codemirror/state';
 import { scrollPastEnd } from '@codemirror/view';
@@ -9,11 +9,25 @@ import type { SWRResponse } from 'swr';
 import type { UseCodeMirrorEditor } from '../services';
 import { useCodeMirrorEditor } from '../services';
 
+
+const isValid = (u: UseCodeMirrorEditor) => {
+  return u.state != null && u.view != null;
+};
+
+const isDeepEquals = <T extends object>(obj1: T, obj2: T): boolean => {
+  const typedKeys = Object.keys(obj1) as (keyof typeof obj1)[];
+  return typedKeys.every(key => obj1[key] === obj2[key]);
+};
+
+
 const defaultExtensionsMain: Extension[] = [
   scrollPastEnd(),
 ];
 
 export const useCodeMirrorEditorMain = (container?: HTMLDivElement | null, props?: ReactCodeMirrorProps): SWRResponse<UseCodeMirrorEditor> => {
+  const ref = useRef<UseCodeMirrorEditor>();
+  const currentData = ref.current;
+
   const mergedProps = useMemo<UseCodeMirror>(() => {
     return {
       ...props,
@@ -25,7 +39,16 @@ export const useCodeMirrorEditorMain = (container?: HTMLDivElement | null, props
     };
   }, [container, props]);
 
-  const states = useCodeMirrorEditor(mergedProps);
+  const newData = useCodeMirrorEditor(mergedProps);
+
+  const shouldUpdate = props != null && (
+    currentData == null
+    || (isValid(newData) && !isDeepEquals(currentData, newData))
+  );
+
+  if (shouldUpdate) {
+    ref.current = newData;
+  }
 
-  return useSWRStatic('codeMirrorEditorMain', props != null ? states : undefined);
+  return useSWRStatic('codeMirrorEditorMain', shouldUpdate ? newData : undefined);
 };

+ 1 - 1
packages/remark-attachment-refs/package.json

@@ -48,7 +48,7 @@
     "axios": "^0.24.0",
     "bunyan": "^1.8.15",
     "hast-util-select": "^5.0.5",
-    "swr": "^2.0.3",
+    "swr": "^2.2.2",
     "universal-bunyan": "^0.9.2"
   },
   "devDependencies": {

+ 1 - 1
packages/remark-lsx/package.json

@@ -40,7 +40,7 @@
     "express": "^4.16.1",
     "http-errors": "^2.0.0",
     "mongoose": "^6.11.3",
-    "swr": "^2.0.3"
+    "swr": "^2.2.2"
   },
   "devDependencies": {
     "eslint-plugin-regex": "^1.8.0",

+ 8 - 7
yarn.lock

@@ -2950,7 +2950,7 @@
     axios "^0.24.0"
     bunyan "^1.8.15"
     hast-util-select "^5.0.5"
-    swr "^2.0.3"
+    swr "^2.2.2"
     universal-bunyan "^0.9.2"
 
 "@growi/remark-drawio@link:packages/remark-drawio":
@@ -2983,7 +2983,7 @@
     express "^4.16.1"
     http-errors "^2.0.0"
     mongoose "^6.11.3"
-    swr "^2.0.3"
+    swr "^2.2.2"
 
 "@growi/slack@link:packages/slack":
   version "7.0.0-RC.0"
@@ -6503,7 +6503,7 @@ cli-width@^3.0.0:
   resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-3.0.0.tgz#a2f48437a2caa9a22436e794bf071ec9e61cedf6"
   integrity sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==
 
-client-only@0.0.1:
+client-only@0.0.1, client-only@^0.0.1:
   version "0.0.1"
   resolved "https://registry.yarnpkg.com/client-only/-/client-only-0.0.1.tgz#38bba5d403c41ab150bff64a95c85013cf73bca1"
   integrity sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==
@@ -16338,11 +16338,12 @@ swagger2openapi@^7.0.8:
     yaml "^1.10.0"
     yargs "^17.0.1"
 
-swr@^2.0.3:
-  version "2.0.3"
-  resolved "https://registry.yarnpkg.com/swr/-/swr-2.0.3.tgz#9fe59a17f55b0fdddccd76b7b2f723f9f8e2263e"
-  integrity sha512-sGvQDok/AHEWTPfhUWXEHBVEXmgGnuahyhmRQbjl9XBYxT/MSlAzvXEKQpyM++bMPaI52vcWS2HiKNaW7+9OFw==
+swr@^2.2.2:
+  version "2.2.2"
+  resolved "https://registry.yarnpkg.com/swr/-/swr-2.2.2.tgz#abcb1f9c97e10527789884169d58b878472d4c98"
+  integrity sha512-CbR41AoMD4TQBQw9ic3GTXspgfM9Y8Mdhb5Ob4uIKXhWqnRLItwA5fpGvB7SmSw3+zEjb0PdhiEumtUvYoQ+bQ==
   dependencies:
+    client-only "^0.0.1"
     use-sync-external-store "^1.2.0"
 
 synckit@^0.7.2: