|
|
@@ -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()} (-> <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 && <> (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';
|