Explorar o código

impl with SWR

Yuki Takei %!s(int64=3) %!d(string=hai) anos
pai
achega
525cbcd5ec

+ 3 - 4
packages/app/src/services/renderer/renderer.tsx

@@ -1,7 +1,7 @@
 // allow only types to import from react
 import { ComponentType } from 'react';
 
-import { Lsx } from '@growi/plugin-lsx/components';
+import { Lsx, LsxImmutable } from '@growi/plugin-lsx/components';
 import * as lsxGrowiPlugin from '@growi/plugin-lsx/services/renderer';
 import * as drawioPlugin from '@growi/remark-drawio-plugin';
 import growiPlugin from '@growi/remark-growi-plugin';
@@ -351,7 +351,6 @@ export const generateViewOptions = (
     components.h1 = Header;
     components.h2 = Header;
     components.h3 = Header;
-    // components.lsx = props => <Lsx {...props} forceToFetchData />;
     components.lsx = Lsx;
     components.drawio = DrawioViewerWithEditButton;
   }
@@ -425,7 +424,7 @@ export const generateSimpleViewOptions = (config: RendererConfig, pagePath: stri
 
   // add components
   if (components != null) {
-    components.lsx = Lsx;
+    components.lsx = LsxImmutable;
     components.drawio = drawioPlugin.DrawioViewer;
   }
 
@@ -465,7 +464,7 @@ export const generatePreviewOptions = (config: RendererConfig, pagePath: string)
 
   // add components
   if (components != null) {
-    components.lsx = Lsx;
+    components.lsx = LsxImmutable;
     components.drawio = drawioPlugin.DrawioViewer;
   }
 

+ 2 - 1
packages/plugin-lsx/package.json

@@ -25,7 +25,8 @@
   "dependencies": {
     "@growi/core": "^6.0.0-RC.9",
     "@growi/remark-growi-plugin": "^6.0.0-RC.9",
-    "@growi/ui": "^6.0.0-RC.9"
+    "@growi/ui": "^6.0.0-RC.9",
+    "swr": "^1.3.0"
   },
   "devDependencies": {
     "eslint-plugin-regex": "^1.8.0",

+ 20 - 205
packages/plugin-lsx/src/components/Lsx.tsx

@@ -1,116 +1,14 @@
-import React, {
-  useCallback, useEffect, useMemo, useState,
-} from 'react';
+import React, { useCallback, useMemo } from 'react';
 
-import * as url from 'url';
 
-import { IPage, pathUtils } from '@growi/core';
-import axios from 'axios';
+import { useSWRxNodeTree } from '../stores/lsx';
 
 import { LsxListView } from './LsxPageList/LsxListView';
-import { PageNode } from './PageNode';
 import { LsxContext } from './lsx-context';
-import { getInstance as getTagCacheManager } from './tag-cache-manager';
 
 import styles from './Lsx.module.scss';
 
 
-const tagCacheManager = getTagCacheManager();
-
-
-/**
- * compare whether path1 and path2 is the same
- *
- * @param {string} path1
- * @param {string} path2
- * @returns
- *
- * @memberOf Lsx
- */
-function isEquals(path1: string, path2: string) {
-  return pathUtils.removeTrailingSlash(path1) === pathUtils.removeTrailingSlash(path2);
-}
-
-function getParentPath(path: string) {
-  return pathUtils.addTrailingSlash(decodeURIComponent(url.resolve(path, '../')));
-}
-
-/**
- * generate PageNode instances for target page and the ancestors
- *
- * @param {any} pathToNodeMap
- * @param {any} rootPagePath
- * @param {any} pagePath
- * @returns
- * @memberof Lsx
- */
-function generatePageNode(pathToNodeMap: Record<string, PageNode>, rootPagePath: string, pagePath: string): PageNode | null {
-  // exclude rootPagePath itself
-  if (isEquals(pagePath, rootPagePath)) {
-    return null;
-  }
-
-  // return when already registered
-  if (pathToNodeMap[pagePath] != null) {
-    return pathToNodeMap[pagePath];
-  }
-
-  // generate node
-  const node = new PageNode(pagePath);
-  pathToNodeMap[pagePath] = node;
-
-  /*
-    * process recursively for ancestors
-    */
-  // get or create parent node
-  const parentPath = getParentPath(pagePath);
-  const parentNode = generatePageNode(pathToNodeMap, rootPagePath, parentPath);
-  // associate to patent
-  if (parentNode != null) {
-    parentNode.children.push(node);
-  }
-
-  return node;
-}
-
-function generatePageNodeTree(rootPagePath: string, pages: IPage[]) {
-  const pathToNodeMap: Record<string, PageNode> = {};
-
-  pages.forEach((page) => {
-    // add slash ensure not to forward match to another page
-    // e.g. '/Java/' not to match to '/JavaScript'
-    const pagePath = pathUtils.addTrailingSlash(page.path);
-
-    const node = generatePageNode(pathToNodeMap, rootPagePath, pagePath); // this will not be null
-
-    // exclude rootPagePath itself
-    if (node == null) {
-      return;
-    }
-
-    // set the Page substance
-    node.page = page;
-  });
-
-  // return root objects
-  const rootNodes: PageNode[] = [];
-  Object.keys(pathToNodeMap).forEach((pagePath) => {
-    // exclude '/'
-    if (pagePath === '/') {
-      return;
-    }
-
-    const parentPath = getParentPath(pagePath);
-
-    // pick up what parent doesn't exist
-    if ((parentPath === '/') || !(parentPath in pathToNodeMap)) {
-      rootNodes.push(pathToNodeMap[pagePath]);
-    }
-  });
-  return rootNodes;
-}
-
-
 type Props = {
   children: React.ReactNode,
   className?: string,
@@ -122,31 +20,16 @@ type Props = {
   reverse?: string,
   filter?: string,
 
-  forceToFetchData?: boolean,
+  isImmutable?: boolean,
 };
 
-type StateCache = {
-  isError: boolean,
-  errorMessage: string,
-  basisViewersCount?: number,
-  nodeTree?: PageNode[],
-}
-
 export const Lsx = React.memo(({
   prefix,
   num, depth, sort, reverse, filter,
+  isImmutable,
   ...props
 }: Props): JSX.Element => {
 
-  const [isLoading, setLoading] = useState(false);
-  const [isError, setError] = useState(false);
-  const [isCacheExists, setCacheExists] = useState(false);
-  const [nodeTree, setNodeTree] = useState<PageNode[]|undefined>();
-  const [basisViewersCount, setBasisViewersCount] = useState<number|undefined>();
-  const [errorMessage, setErrorMessage] = useState('');
-
-  const { forceToFetchData } = props;
-
   const lsxContext = useMemo(() => {
     const options = {
       num, depth, sort, reverse, filter,
@@ -154,85 +37,14 @@ export const Lsx = React.memo(({
     return new LsxContext(prefix, options);
   }, [depth, filter, num, prefix, reverse, sort]);
 
-  const retrieveDataFromCache = useCallback(() => {
-    // get state object cache
-    const stateCache = tagCacheManager.getStateCache(lsxContext) as StateCache | null;
-
-    // instanciate PageNode
-    if (stateCache != null && stateCache.nodeTree != null) {
-      stateCache.nodeTree = stateCache.nodeTree.map((obj) => {
-        return PageNode.instanciateFrom(obj);
-      });
-    }
-
-    return stateCache;
-  }, [lsxContext]);
+  const { data, error } = useSWRxNodeTree(lsxContext, isImmutable);
 
-  const loadData = useCallback(async() => {
-    setLoading(true);
-
-    // add slash ensure not to forward match to another page
-    // ex: '/Java/' not to match to '/JavaScript'
-    const pagePath = pathUtils.addTrailingSlash(lsxContext.pagePath);
-
-    let newNodeTree: PageNode[] = [];
-    try {
-      const result = await axios.get('/_api/plugins/lsx', {
-        params: {
-          pagePath,
-          options: lsxContext.options,
-        },
-      });
-
-      newNodeTree = generatePageNodeTree(pagePath, result.data.pages);
-      setNodeTree(newNodeTree);
-      setBasisViewersCount(result.data.toppageViewersCount);
-      setError(false);
-
-      // store to sessionStorage
-      tagCacheManager.cacheState(lsxContext, {
-        isError: false,
-        errorMessage: '',
-        basisViewersCount,
-        nodeTree: newNodeTree,
-      });
-    }
-    catch (error) {
-      setError(true);
-      setErrorMessage(error.message);
-
-      // store to sessionStorage
-      tagCacheManager.cacheState(lsxContext, {
-        isError: true,
-        errorMessage: error.message,
-      });
-    }
-    finally {
-      setLoading(false);
-    }
-  }, [basisViewersCount, lsxContext]);
-
-  useEffect(() => {
-    // get state object cache
-    const stateCache = retrieveDataFromCache();
-
-    if (stateCache != null) {
-      setCacheExists(true);
-      setNodeTree(stateCache.nodeTree);
-      setError(stateCache.isError);
-      setErrorMessage(stateCache.errorMessage);
-
-      // switch behavior by forceToFetchData
-      if (!forceToFetchData) {
-        return; // go to render()
-      }
-    }
-
-    loadData();
-  }, [forceToFetchData, loadData, retrieveDataFromCache]);
+  const isLoading = data === undefined;
+  const hasError = error != null;
+  const errorMessage = error?.message;
 
   const Error = useCallback((): JSX.Element => {
-    if (!isError) {
+    if (!hasError) {
       return <></>;
     }
 
@@ -242,10 +54,10 @@ export const Lsx = React.memo(({
         {lsxContext.toString()} (-&gt; <small>{errorMessage}</small>)
       </div>
     );
-  }, [errorMessage, isError, lsxContext]);
+  }, [errorMessage, hasError, lsxContext]);
 
   const Loading = useCallback((): JSX.Element => {
-    if (isError) {
+    if (hasError) {
       return <></>;
     }
     if (!isLoading) {
@@ -257,20 +69,18 @@ export const Lsx = React.memo(({
         <small>
           <i className="fa fa-spinner fa-pulse mr-1"></i>
           {lsxContext.toString()}
-          { isCacheExists && <>&nbsp;(Showing cache..)</> }
         </small>
       </div>
     );
-  }, [isCacheExists, isError, isLoading, lsxContext]);
+  }, [hasError, isLoading, lsxContext]);
 
   const contents = useMemo(() => {
-    const showListView = nodeTree != null && (!isLoading || nodeTree.length > 0);
-    if (!showListView) {
+    if (isLoading) {
       return <></>;
     }
 
-    return <LsxListView nodeTree={nodeTree} lsxContext={lsxContext} basisViewersCount={basisViewersCount} />;
-  }, [basisViewersCount, isLoading, lsxContext, nodeTree]);
+    return <LsxListView nodeTree={data.nodeTree} lsxContext={lsxContext} basisViewersCount={data.toppageViewersCount} />;
+  }, [data?.nodeTree, data?.toppageViewersCount, isLoading, lsxContext]);
 
   return (
     <div className={`lsx ${styles.lsx}`}>
@@ -281,3 +91,8 @@ export const Lsx = React.memo(({
   );
 });
 Lsx.displayName = 'Lsx';
+
+export const LsxImmutable = React.memo((props: Omit<Props, 'isImmutable'>): JSX.Element => {
+  return <Lsx {...props} isImmutable />;
+});
+LsxImmutable.displayName = 'LsxImmutable';

+ 2 - 2
packages/plugin-lsx/src/components/LsxPageList/LsxListView.tsx

@@ -9,7 +9,7 @@ import styles from './LsxListView.module.scss';
 
 
 type Props = {
-  nodeTree: PageNode[],
+  nodeTree?: PageNode[],
   lsxContext: LsxContext,
   basisViewersCount?: number,
 };
@@ -19,7 +19,7 @@ export const LsxListView = React.memo((props: Props): JSX.Element => {
 
   const { nodeTree, lsxContext, basisViewersCount } = props;
 
-  const isEmpty = nodeTree.length === 0;
+  const isEmpty = nodeTree == null || nodeTree.length === 0;
 
   const contents = useMemo(() => {
     if (isEmpty) {

+ 1 - 1
packages/plugin-lsx/src/components/index.ts

@@ -1 +1 @@
-export { Lsx } from './Lsx';
+export { Lsx, LsxImmutable } from './Lsx';

+ 144 - 0
packages/plugin-lsx/src/stores/lsx.tsx

@@ -0,0 +1,144 @@
+import * as url from 'url';
+
+import { IPage, pathUtils } from '@growi/core';
+import axios from 'axios';
+import useSWR, { SWRResponse } from 'swr';
+
+import { PageNode } from '../components/PageNode';
+import { LsxContext } from '../components/lsx-context';
+
+function isEquals(path1: string, path2: string) {
+  return pathUtils.removeTrailingSlash(path1) === pathUtils.removeTrailingSlash(path2);
+}
+
+function getParentPath(path: string) {
+  return pathUtils.addTrailingSlash(decodeURIComponent(url.resolve(path, '../')));
+}
+
+/**
+ * generate PageNode instances for target page and the ancestors
+ *
+ * @param {any} pathToNodeMap
+ * @param {any} rootPagePath
+ * @param {any} pagePath
+ * @returns
+ * @memberof Lsx
+ */
+function generatePageNode(pathToNodeMap: Record<string, PageNode>, rootPagePath: string, pagePath: string): PageNode | null {
+  // exclude rootPagePath itself
+  if (isEquals(pagePath, rootPagePath)) {
+    return null;
+  }
+
+  // return when already registered
+  if (pathToNodeMap[pagePath] != null) {
+    return pathToNodeMap[pagePath];
+  }
+
+  // generate node
+  const node = new PageNode(pagePath);
+  pathToNodeMap[pagePath] = node;
+
+  /*
+    * process recursively for ancestors
+    */
+  // get or create parent node
+  const parentPath = getParentPath(pagePath);
+  const parentNode = generatePageNode(pathToNodeMap, rootPagePath, parentPath);
+  // associate to patent
+  if (parentNode != null) {
+    parentNode.children.push(node);
+  }
+
+  return node;
+}
+
+function generatePageNodeTree(rootPagePath: string, pages: IPage[]) {
+  const pathToNodeMap: Record<string, PageNode> = {};
+
+  pages.forEach((page) => {
+    // add slash ensure not to forward match to another page
+    // e.g. '/Java/' not to match to '/JavaScript'
+    const pagePath = pathUtils.addTrailingSlash(page.path);
+
+    const node = generatePageNode(pathToNodeMap, rootPagePath, pagePath); // this will not be null
+
+    // exclude rootPagePath itself
+    if (node == null) {
+      return;
+    }
+
+    // set the Page substance
+    node.page = page;
+  });
+
+  // return root objects
+  const rootNodes: PageNode[] = [];
+  Object.keys(pathToNodeMap).forEach((pagePath) => {
+    // exclude '/'
+    if (pagePath === '/') {
+      return;
+    }
+
+    const parentPath = getParentPath(pagePath);
+
+    // pick up what parent doesn't exist
+    if ((parentPath === '/') || !(parentPath in pathToNodeMap)) {
+      rootNodes.push(pathToNodeMap[pagePath]);
+    }
+  });
+  return rootNodes;
+}
+
+type LsxResponse = {
+  pages: IPage[],
+  toppageViewersCount: number,
+}
+
+const useSWRxLsxResponse = (
+    pagePath: string, options?: Record<string, string | undefined>, isImmutable?: boolean,
+): SWRResponse<LsxResponse, Error> => {
+  return useSWR(
+    ['/_api/plugins/lsx', pagePath, options, isImmutable],
+    (endpoint, pagePath, options) => {
+      return axios.get(endpoint, {
+        params: {
+          pagePath,
+          options,
+        },
+      }).then(result => result.data as LsxResponse);
+    },
+    {
+      revalidateIfStale: !isImmutable,
+      revalidateOnFocus: !isImmutable,
+      revalidateOnReconnect: !isImmutable,
+    },
+  );
+};
+
+type LsxNodeTree = {
+  nodeTree: PageNode[],
+  toppageViewersCount: number,
+}
+
+export const useSWRxNodeTree = (lsxContext: LsxContext, isImmutable?: boolean): SWRResponse<LsxNodeTree, Error> => {
+  const { data, error } = useSWRxLsxResponse(lsxContext.pagePath, lsxContext.options, isImmutable);
+
+  return useSWR(
+    data === undefined ? null : ['lsxNodeTree', lsxContext.pagePath, lsxContext.options, isImmutable, data],
+    (key, pagePath, options, isImmutable, data) => {
+      if (error != null) {
+        throw error;
+      }
+      return {
+        nodeTree: generatePageNodeTree(pagePath, data.pages),
+        toppageViewersCount: data.toppageViewersCount,
+      };
+    },
+    {
+      revalidateIfStale: !isImmutable,
+      revalidateOnFocus: !isImmutable,
+      revalidateOnReconnect: !isImmutable,
+    },
+  );
+};