Lsx.jsx 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240
  1. import React from 'react';
  2. import * as url from 'url';
  3. import { pathUtils } from '@growi/core';
  4. import PropTypes from 'prop-types';
  5. // eslint-disable-next-line no-unused-vars
  6. import styles from '../../css/index.css';
  7. import { LsxContext } from '../util/LsxContext';
  8. import { TagCacheManagerFactory } from '../util/TagCacheManagerFactory';
  9. import { LsxListView } from './LsxPageList/LsxListView';
  10. import { PageNode } from './PageNode';
  11. export class Lsx extends React.Component {
  12. constructor(props) {
  13. super(props);
  14. this.state = {
  15. isLoading: false,
  16. isError: false,
  17. isCacheExists: false,
  18. nodeTree: undefined,
  19. errorMessage: '',
  20. };
  21. this.tagCacheManager = TagCacheManagerFactory.getInstance();
  22. }
  23. async componentWillMount() {
  24. const { lsxContext, forceToFetchData } = this.props;
  25. // get state object cache
  26. const stateCache = this.retrieveDataFromCache();
  27. if (stateCache != null) {
  28. this.setState({
  29. isCacheExists: true,
  30. nodeTree: stateCache.nodeTree,
  31. isError: stateCache.isError,
  32. errorMessage: stateCache.errorMessage,
  33. });
  34. // switch behavior by forceToFetchData
  35. if (!forceToFetchData) {
  36. return; // go to render()
  37. }
  38. }
  39. lsxContext.parse();
  40. this.setState({ isLoading: true });
  41. // add slash ensure not to forward match to another page
  42. // ex: '/Java/' not to match to '/JavaScript'
  43. const pagePath = pathUtils.addTrailingSlash(lsxContext.pagePath);
  44. try {
  45. const res = await this.props.appContainer.apiGet('/plugins/lsx', { pagePath, options: lsxContext.options });
  46. lsxContext.activeUsersCount = res.activeUsersCount;
  47. if (res.ok) {
  48. const nodeTree = this.generatePageNodeTree(pagePath, res.pages);
  49. this.setState({ nodeTree });
  50. }
  51. }
  52. catch (error) {
  53. this.setState({ isError: true, errorMessage: error.message });
  54. }
  55. finally {
  56. this.setState({ isLoading: false });
  57. // store to sessionStorage
  58. this.tagCacheManager.cacheState(lsxContext, this.state);
  59. }
  60. }
  61. retrieveDataFromCache() {
  62. const { lsxContext } = this.props;
  63. // get state object cache
  64. const stateCache = this.tagCacheManager.getStateCache(lsxContext);
  65. // instanciate PageNode
  66. if (stateCache != null && stateCache.nodeTree != null) {
  67. stateCache.nodeTree = stateCache.nodeTree.map((obj) => {
  68. return PageNode.instanciateFrom(obj);
  69. });
  70. }
  71. return stateCache;
  72. }
  73. /**
  74. * generate tree structure
  75. *
  76. * @param {string} rootPagePath
  77. * @param {Page[]} pages Array of Page model
  78. *
  79. * @memberOf Lsx
  80. */
  81. generatePageNodeTree(rootPagePath, pages) {
  82. const pathToNodeMap = {};
  83. pages.forEach((page) => {
  84. // add slash ensure not to forward match to another page
  85. // e.g. '/Java/' not to match to '/JavaScript'
  86. const pagePath = pathUtils.addTrailingSlash(page.path);
  87. // exclude rootPagePath itself
  88. if (this.isEquals(pagePath, rootPagePath)) {
  89. return;
  90. }
  91. const node = this.generatePageNode(pathToNodeMap, rootPagePath, pagePath); // this will not be null
  92. // set the Page substance
  93. node.page = page;
  94. });
  95. // return root objects
  96. const rootNodes = [];
  97. Object.keys(pathToNodeMap).forEach((pagePath) => {
  98. // exclude '/'
  99. if (pagePath === '/') {
  100. return;
  101. }
  102. const parentPath = this.getParentPath(pagePath);
  103. // pick up what parent doesn't exist
  104. if ((parentPath === '/') || !(parentPath in pathToNodeMap)) {
  105. rootNodes.push(pathToNodeMap[pagePath]);
  106. }
  107. });
  108. return rootNodes;
  109. }
  110. /**
  111. * generate PageNode instances for target page and the ancestors
  112. *
  113. * @param {any} pathToNodeMap
  114. * @param {any} rootPagePath
  115. * @param {any} pagePath
  116. * @returns
  117. * @memberof Lsx
  118. */
  119. generatePageNode(pathToNodeMap, rootPagePath, pagePath) {
  120. // exclude rootPagePath itself
  121. if (this.isEquals(pagePath, rootPagePath)) {
  122. return null;
  123. }
  124. // return when already registered
  125. if (pathToNodeMap[pagePath] != null) {
  126. return pathToNodeMap[pagePath];
  127. }
  128. // generate node
  129. const node = new PageNode(pagePath);
  130. pathToNodeMap[pagePath] = node;
  131. /*
  132. * process recursively for ancestors
  133. */
  134. // get or create parent node
  135. const parentPath = this.getParentPath(pagePath);
  136. const parentNode = this.generatePageNode(pathToNodeMap, rootPagePath, parentPath);
  137. // associate to patent
  138. if (parentNode != null) {
  139. parentNode.children.push(node);
  140. }
  141. return node;
  142. }
  143. /**
  144. * compare whether path1 and path2 is the same
  145. *
  146. * @param {string} path1
  147. * @param {string} path2
  148. * @returns
  149. *
  150. * @memberOf Lsx
  151. */
  152. isEquals(path1, path2) {
  153. return pathUtils.removeTrailingSlash(path1) === pathUtils.removeTrailingSlash(path2);
  154. }
  155. getParentPath(path) {
  156. return pathUtils.addTrailingSlash(decodeURIComponent(url.resolve(path, '../')));
  157. }
  158. renderContents() {
  159. const lsxContext = this.props.lsxContext;
  160. const {
  161. isLoading, isError, isCacheExists, nodeTree,
  162. } = this.state;
  163. if (isError) {
  164. return (
  165. <div className="text-warning">
  166. <i className="fa fa-exclamation-triangle fa-fw"></i>
  167. {lsxContext.tagExpression} (-&gt; <small>{this.state.errorMessage}</small>)
  168. </div>
  169. );
  170. }
  171. return (
  172. <div className={isLoading ? 'lsx-blink' : ''}>
  173. { isLoading && (
  174. <div className="text-muted">
  175. <i className="fa fa-spinner fa-pulse mr-1"></i>
  176. {lsxContext.tagExpression}
  177. { isCacheExists && <small>&nbsp;(Showing cache..)</small> }
  178. </div>
  179. ) }
  180. { nodeTree && (
  181. <LsxListView nodeTree={this.state.nodeTree} lsxContext={this.props.lsxContext} />
  182. ) }
  183. </div>
  184. );
  185. }
  186. render() {
  187. return <div className="lsx">{this.renderContents()}</div>;
  188. }
  189. }
  190. Lsx.propTypes = {
  191. appContainer: PropTypes.object.isRequired,
  192. lsxContext: PropTypes.instanceOf(LsxContext).isRequired,
  193. forceToFetchData: PropTypes.bool,
  194. };