Explorar el Código

re-impl Lsx component

Yuki Takei hace 3 años
padre
commit
8e801f1648

+ 154 - 143
packages/app/src/components/ReactMarkdownComponents/Lsx/Lsx.tsx

@@ -1,9 +1,10 @@
-import React from 'react';
+import React, {
+  useCallback, useEffect, useMemo, useState,
+} from 'react';
 
 import * as url from 'url';
 
 import { pathUtils } from '@growi/core';
-import PropTypes from 'prop-types';
 
 import axios from '~/utils/axios';
 
@@ -16,80 +17,107 @@ import { getInstance as getTagCacheManager } from './tag-cache-manager';
 
 import styles from './Lsx.module.scss';
 
-export class Lsx extends React.Component {
 
-  constructor(props) {
-    super(props);
+const tagCacheManager = getTagCacheManager();
 
-    this.state = {
-      isLoading: false,
-      isError: false,
-      isCacheExists: false,
-      nodeTree: undefined,
-      basisViewersCount: undefined,
-      errorMessage: '',
-    };
 
-    this.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;
   }
 
-  async componentDidMount() {
-    const { lsxContext, forceToFetchData } = this.props;
+  // return when already registered
+  if (pathToNodeMap[pagePath] != null) {
+    return pathToNodeMap[pagePath];
+  }
 
-    // get state object cache
-    const stateCache = this.retrieveDataFromCache();
+  // generate node
+  const node = new PageNode(pagePath);
+  pathToNodeMap[pagePath] = node;
+
+  /*
+    * process recursively for ancestors
+    */
+  // get or create parent node
+  const parentPath = this.getParentPath(pagePath);
+  const parentNode = this.generatePageNode(pathToNodeMap, rootPagePath, parentPath);
+  // associate to patent
+  if (parentNode != null) {
+    parentNode.children.push(node);
+  }
 
-    if (stateCache != null) {
-      this.setState({
-        isCacheExists: true,
-        nodeTree: stateCache.nodeTree,
-        isError: stateCache.isError,
-        errorMessage: stateCache.errorMessage,
-      });
+  return node;
+}
 
-      // switch behavior by forceToFetchData
-      if (!forceToFetchData) {
-        return; // go to render()
-      }
-    }
 
-    lsxContext.parse();
-    this.setState({ isLoading: true });
+type Props = {
+  // lsxContext: PropTypes.instanceOf(LsxContext).isRequired,
 
-    // add slash ensure not to forward match to another page
-    // ex: '/Java/' not to match to '/JavaScript'
-    const pagePath = pathUtils.addTrailingSlash(lsxContext.pagePath);
+  children: React.ReactNode,
+  className?: string,
 
-    try {
-      const res = await axios.get('/_api/plugins/lsx', {
-        params: {
-          pagePath,
-          options: lsxContext.options,
-        },
-      });
+  prefix: string,
+  num?: string,
+  depth?: string,
+  sort?: string,
+  reverse?: string,
+  filter?: string,
 
-      if (res.data.ok) {
-        const basisViewersCount = res.data.toppageViewersCount;
-        const nodeTree = this.generatePageNodeTree(pagePath, res.data.pages);
-        this.setState({ nodeTree, basisViewersCount });
-      }
-    }
-    catch (error) {
-      this.setState({ isError: true, errorMessage: error.message });
-    }
-    finally {
-      this.setState({ isLoading: false });
+  forceToFetchData?: boolean,
+};
 
-      // store to sessionStorage
-      this.tagCacheManager.cacheState(lsxContext, this.state);
-    }
-  }
+export const Lsx = ({
+  prefix,
+  num, depth, sort, reverse, filter,
+  ...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('');
 
-  retrieveDataFromCache() {
-    const { lsxContext } = this.props;
+  const { forceToFetchData } = props;
 
+  const lsxContext = useMemo(() => {
+    const options = {
+      num, depth, sort, reverse, filter,
+    };
+    return new LsxContext(prefix, options);
+  }, [depth, filter, num, prefix, reverse, sort]);
+
+  const retrieveDataFromCache = useCallback(() => {
     // get state object cache
-    const stateCache = this.tagCacheManager.getStateCache(lsxContext);
+    const stateCache = tagCacheManager.getStateCache(lsxContext);
 
     // instanciate PageNode
     if (stateCache != null && stateCache.nodeTree != null) {
@@ -99,43 +127,36 @@ export class Lsx extends React.Component {
     }
 
     return stateCache;
-  }
+  }, []);
 
-  /**
-   * generate tree structure
-   *
-   * @param {string} rootPagePath
-   * @param {Page[]} pages Array of Page model
-   *
-   * @memberOf Lsx
-   */
-  generatePageNodeTree(rootPagePath, pages) {
-    const pathToNodeMap = {};
+  const generatePageNodeTree = useCallback((rootPagePath, pages) => {
+    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 (this.isEquals(pagePath, rootPagePath)) {
+      if (node == null) {
         return;
       }
 
-      const node = this.generatePageNode(pathToNodeMap, rootPagePath, pagePath); // this will not be null
       // set the Page substance
       node.page = page;
     });
 
     // return root objects
-    const rootNodes = [];
+    const rootNodes: PageNode[] = [];
     Object.keys(pathToNodeMap).forEach((pagePath) => {
       // exclude '/'
       if (pagePath === '/') {
         return;
       }
 
-      const parentPath = this.getParentPath(pagePath);
+      const parentPath = getParentPath(pagePath);
 
       // pick up what parent doesn't exist
       if ((parentPath === '/') || !(parentPath in pathToNodeMap)) {
@@ -143,74 +164,74 @@ export class Lsx extends React.Component {
       }
     });
     return rootNodes;
-  }
+  }, []);
 
-  /**
-   * generate PageNode instances for target page and the ancestors
-   *
-   * @param {any} pathToNodeMap
-   * @param {any} rootPagePath
-   * @param {any} pagePath
-   * @returns
-   * @memberof Lsx
-   */
-  generatePageNode(pathToNodeMap, rootPagePath, pagePath) {
-    // exclude rootPagePath itself
-    if (this.isEquals(pagePath, rootPagePath)) {
-      return null;
-    }
+  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 res = await axios.get('/_api/plugins/lsx', {
+        params: {
+          pagePath,
+          options: lsxContext.options,
+        },
+      });
 
-    // return when already registered
-    if (pathToNodeMap[pagePath] != null) {
-      return pathToNodeMap[pagePath];
+      if (res.data.ok) {
+        const basisViewersCount = res.data.toppageViewersCount;
+        newNodeTree = generatePageNodeTree(pagePath, res.data.pages);
+        setNodeTree(newNodeTree);
+        setBasisViewersCount(basisViewersCount);
+      }
+    }
+    catch (error) {
+      setError(true);
+      setErrorMessage(error.message);
     }
+    finally {
+      setLoading(false);
 
-    // generate node
-    const node = new PageNode(pagePath);
-    pathToNodeMap[pagePath] = node;
-
-    /*
-     * process recursively for ancestors
-     */
-    // get or create parent node
-    const parentPath = this.getParentPath(pagePath);
-    const parentNode = this.generatePageNode(pathToNodeMap, rootPagePath, parentPath);
-    // associate to patent
-    if (parentNode != null) {
-      parentNode.children.push(node);
+      // store to sessionStorage
+      tagCacheManager.cacheState(lsxContext, {
+        isError,
+        isCacheExists,
+        basisViewersCount,
+        errorMessage,
+        nodeTree: newNodeTree,
+      });
     }
+  }, [basisViewersCount, errorMessage, generatePageNodeTree, isCacheExists, isError, lsxContext]);
 
-    return node;
-  }
+  useEffect(() => {
+    // get state object cache
+    const stateCache = retrieveDataFromCache();
 
-  /**
-   * compare whether path1 and path2 is the same
-   *
-   * @param {string} path1
-   * @param {string} path2
-   * @returns
-   *
-   * @memberOf Lsx
-   */
-  isEquals(path1, path2) {
-    return pathUtils.removeTrailingSlash(path1) === pathUtils.removeTrailingSlash(path2);
-  }
+    if (stateCache != null) {
+      setCacheExists(true);
+      setNodeTree(stateCache.nodeTree);
+      setError(stateCache.isError);
+      setErrorMessage(stateCache.errorMessage);
 
-  getParentPath(path) {
-    return pathUtils.addTrailingSlash(decodeURIComponent(url.resolve(path, '../')));
-  }
+      // switch behavior by forceToFetchData
+      if (!forceToFetchData) {
+        return; // go to render()
+      }
+    }
 
-  renderContents() {
-    const lsxContext = this.props.lsxContext;
-    const {
-      isLoading, isError, isCacheExists, nodeTree,
-    } = this.state;
+    loadData();
+  }, [forceToFetchData, loadData, retrieveDataFromCache]);
 
+  const renderContents = () => {
     if (isError) {
       return (
         <div className="text-warning">
           <i className="fa fa-exclamation-triangle fa-fw"></i>
-          {lsxContext.tagExpression} (-&gt; <small>{this.state.errorMessage}</small>)
+          {/* {lsxContext.tagExpression} (-&gt; <small>{this.state.errorMessage}</small>) */}
         </div>
       );
     }
@@ -221,26 +242,16 @@ export class Lsx extends React.Component {
         { isLoading && (
           <div className="text-muted">
             <i className="fa fa-spinner fa-pulse mr-1"></i>
-            {lsxContext.tagExpression}
+            {/* {lsxContext.tagExpression} */}
             { isCacheExists && <small>&nbsp;(Showing cache..)</small> }
           </div>
         ) }
         { nodeTree && (
-          <LsxListView nodeTree={this.state.nodeTree} lsxContext={this.props.lsxContext} basisViewersCount={this.state.basisViewersCount} />
+          <LsxListView nodeTree={nodeTree} lsxContext={lsxContext} basisViewersCount={basisViewersCount} />
         ) }
       </div>
     );
+  };
 
-  }
-
-  render() {
-    return <div className={`lsx ${styles.lsx}`}>{this.renderContents()}</div>;
-  }
-
-}
-
-Lsx.propTypes = {
-  lsxContext: PropTypes.instanceOf(LsxContext).isRequired,
-
-  forceToFetchData: PropTypes.bool,
+  return <div className={`lsx ${styles.lsx}`}>{renderContents()}</div>;
 };

+ 2 - 0
packages/app/src/components/ReactMarkdownComponents/Lsx/PageNode.js

@@ -3,6 +3,8 @@ export class PageNode {
   constructor(pagePath) {
     this.pagePath = pagePath;
     this.children = [];
+
+    this.page = undefined;
   }
 
   /**

+ 9 - 42
packages/app/src/components/ReactMarkdownComponents/Lsx/lsx-context.ts

@@ -1,53 +1,20 @@
-import * as url from 'url';
+import { customTagUtils } from '@growi/core';
 
-import { customTagUtils, pathUtils } from '@growi/core';
+const { OptionParser } = customTagUtils;
 
-const { TagContext, ArgsParser, OptionParser } = customTagUtils;
+export class LsxContext {
 
-export class LsxContext extends TagContext {
+  pagePath: string;
 
-  fromPagePath: string;
+  options?: Record<string, string|undefined>;
 
-  isParsed?: boolean;
-
-  pagePath?: string;
-
-  options?: any;
-
-
-  parse() {
-    if (this.isParsed) {
-      return;
-    }
-
-    const parsedResult = ArgsParser.parse(this.args);
-    this.options = parsedResult.options;
-
-    // determine specifiedPath
-    // order:
-    //   1: lsx(prefix=..., ...)
-    //   2: lsx(firstArgs, ...)
-    //   3: fromPagePath
-    const specifiedPath = this.options.prefix
-        ?? ((parsedResult.firstArgsValue === true) ? parsedResult.firstArgsKey : undefined)
-        ?? this.fromPagePath;
-
-    // resolve pagePath
-    //   when `fromPagePath`=/hoge and `specifiedPath`=./fuga,
-    //        `pagePath` to be /hoge/fuga
-    //   when `fromPagePath`=/hoge and `specifiedPath`=/fuga,
-    //        `pagePath` to be /fuga
-    //   when `fromPagePath`=/hoge and `specifiedPath`=undefined,
-    //        `pagePath` to be /hoge
-    this.pagePath = (specifiedPath !== undefined)
-      ? decodeURIComponent(url.resolve(pathUtils.addTrailingSlash(this.fromPagePath), specifiedPath))
-      : this.fromPagePath;
-
-    this.isParsed = true;
+  constructor(pagePath: string, options: Record<string, string|undefined>) {
+    this.pagePath = pagePath;
+    this.options = options;
   }
 
   getOptDepth() {
-    if (this.options.depth === undefined) {
+    if (this.options?.depth == null) {
       return undefined;
     }
     return OptionParser.parseRange(this.options.depth);

+ 1 - 1
packages/app/src/services/renderer/renderer.ts

@@ -283,7 +283,6 @@ const generateCommonOptions = (pagePath: string|undefined, config: RendererConfi
     components: {
       a: NextLink,
       code: CodeBlock,
-      lsx: Lsx,
     },
   };
 };
@@ -352,6 +351,7 @@ export const generateViewOptions = (
     components.h1 = Header;
     components.h2 = Header;
     components.h3 = Header;
+    components.lsx = Lsx;
   }
 
   // // Add configurers for viewer