Lsx.jsx 5.3 KB

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