Lsx.jsx 5.5 KB

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