| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265 |
- 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<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,
- 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<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,
- };
- 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 (
- <div className="text-warning">
- <i className="fa fa-exclamation-triangle fa-fw"></i>
- {lsxContext.toString()} (-> <small>{errorMessage}</small>)
- </div>
- );
- }
- const showListView = nodeTree != null && (!isLoading || nodeTree.length > 0);
- return (
- <>
- { isLoading && (
- <div className={`text-muted ${isLoading ? 'lsx-blink' : ''}`}>
- <small>
- <i className="fa fa-spinner fa-pulse mr-1"></i>
- {lsxContext.toString()}
- { isCacheExists && <> (Showing cache..)</> }
- </small>
- </div>
- ) }
- { showListView && (
- <LsxListView nodeTree={nodeTree} lsxContext={lsxContext} basisViewersCount={basisViewersCount} />
- ) }
- </>
- );
- };
- return <div className={`lsx ${styles.lsx}`}>{renderContents()}</div>;
- };
|