import React, { useCallback, useEffect, useMemo, useState, } from 'react'; import * as url from 'url'; import { IPage, pathUtils } from '@growi/core'; import axios from 'axios'; 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, 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 = {}; 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, prefix: string, num?: string, depth?: string, sort?: string, reverse?: string, filter?: string, forceToFetchData?: boolean, }; type StateCache = { isError: boolean, errorMessage: string, basisViewersCount?: number, nodeTree?: PageNode[], } 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(); const [basisViewersCount, setBasisViewersCount] = useState(); const [errorMessage, setErrorMessage] = useState(''); 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 = 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 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 renderContents = () => { if (isError) { return (
{lsxContext.toString()} (-> {errorMessage})
); } const showListView = nodeTree != null && (!isLoading || nodeTree.length > 0); return ( <> { isLoading && (
{lsxContext.toString()} { isCacheExists && <> (Showing cache..) }
) } { showListView && ( ) } ); }; return
{renderContents()}
; };