PageEditorByHackmd.jsx 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305
  1. import React from 'react';
  2. import PropTypes from 'prop-types';
  3. import SplitButton from 'react-bootstrap/es/SplitButton';
  4. import MenuItem from 'react-bootstrap/es/MenuItem';
  5. import * as toastr from 'toastr';
  6. import AppContainer from '../services/AppContainer';
  7. import PageContainer from '../services/PageContainer';
  8. import { createSubscribedElement } from './UnstatedUtils';
  9. import HackmdEditor from './PageEditorByHackmd/HackmdEditor';
  10. class PageEditorByHackmd extends React.Component {
  11. constructor(props) {
  12. super(props);
  13. this.state = {
  14. markdown: this.props.pageContainer.state.markdown,
  15. isInitialized: false,
  16. isInitializing: false,
  17. };
  18. this.getHackmdUri = this.getHackmdUri.bind(this);
  19. this.startToEdit = this.startToEdit.bind(this);
  20. this.resumeToEdit = this.resumeToEdit.bind(this);
  21. this.hackmdEditorChangeHandler = this.hackmdEditorChangeHandler.bind(this);
  22. this.apiErrorHandler = this.apiErrorHandler.bind(this);
  23. }
  24. componentWillMount() {
  25. this.props.appContainer.registerComponentInstance(this);
  26. }
  27. /**
  28. * return markdown document of HackMD
  29. * @return {Promise<string>}
  30. */
  31. getMarkdown() {
  32. if (!this.state.isInitialized) {
  33. return Promise.reject(new Error('HackmdEditor component has not initialized'));
  34. }
  35. return this.hackmdEditor.getValue()
  36. .then((document) => {
  37. this.setState({ markdown: document });
  38. return document;
  39. });
  40. }
  41. /**
  42. * reset initialized status
  43. */
  44. reset() {
  45. this.setState({ isInitialized: false });
  46. }
  47. getHackmdUri() {
  48. const envVars = this.props.appContainer.getConfig().env;
  49. return envVars.HACKMD_URI;
  50. }
  51. /**
  52. * Start integration with HackMD
  53. */
  54. startToEdit() {
  55. const { pageContainer } = this.props;
  56. const hackmdUri = this.getHackmdUri();
  57. if (hackmdUri == null) {
  58. // do nothing
  59. return;
  60. }
  61. this.setState({
  62. isInitialized: false,
  63. isInitializing: true,
  64. });
  65. const params = {
  66. pageId: pageContainer.state.pageId,
  67. };
  68. this.props.appContainer.apiPost('/hackmd.integrate', params)
  69. .then((res) => {
  70. if (!res.ok) {
  71. throw new Error(res.error);
  72. }
  73. this.setState({
  74. isInitialized: true,
  75. });
  76. pageContainer.setState({
  77. pageIdOnHackmd: res.pageIdOnHackmd,
  78. revisionIdHackmdSynced: res.revisionIdHackmdSynced,
  79. });
  80. })
  81. .catch(this.apiErrorHandler)
  82. .then(() => {
  83. this.setState({ isInitializing: false });
  84. });
  85. }
  86. /**
  87. * Start to edit w/o any api request
  88. */
  89. resumeToEdit() {
  90. this.setState({ isInitialized: true });
  91. }
  92. /**
  93. * Reset draft
  94. */
  95. discardChanges() {
  96. this.props.pageContainer.setState({ hasDraftOnHackmd: false });
  97. }
  98. /**
  99. * onChange event of HackmdEditor handler
  100. */
  101. hackmdEditorChangeHandler(body) {
  102. const hackmdUri = this.getHackmdUri();
  103. const { pageContainer } = this.props;
  104. if (hackmdUri == null) {
  105. // do nothing
  106. return;
  107. }
  108. // do nothing if contents are same
  109. if (pageContainer.state.markdown === body) {
  110. return;
  111. }
  112. const params = {
  113. pageId: pageContainer.state.pageId,
  114. };
  115. this.props.appContainer.apiPost('/hackmd.saveOnHackmd', params)
  116. .then((res) => {
  117. // do nothing
  118. })
  119. .catch((err) => {
  120. // do nothing
  121. });
  122. }
  123. apiErrorHandler(error) {
  124. toastr.error(error.message, 'Error occured', {
  125. closeButton: true,
  126. progressBar: true,
  127. newestOnTop: false,
  128. showDuration: '100',
  129. hideDuration: '100',
  130. timeOut: '3000',
  131. });
  132. }
  133. render() {
  134. const hackmdUri = this.getHackmdUri();
  135. const { pageContainer } = this.props;
  136. const {
  137. pageIdOnHackmd, revisionId, revisionIdHackmdSynced, remoteRevisionId, hasDraftOnHackmd,
  138. } = pageContainer.state;
  139. const isPageExistsOnHackmd = (pageIdOnHackmd != null);
  140. const isResume = isPageExistsOnHackmd && hasDraftOnHackmd;
  141. if (this.state.isInitialized) {
  142. return (
  143. <HackmdEditor
  144. ref={(c) => { this.hackmdEditor = c }}
  145. hackmdUri={hackmdUri}
  146. pageIdOnHackmd={pageIdOnHackmd}
  147. initializationMarkdown={isResume ? null : this.state.markdown}
  148. onChange={this.hackmdEditorChangeHandler}
  149. onSaveWithShortcut={(document) => {
  150. this.props.onSaveWithShortcut(document);
  151. }}
  152. >
  153. </HackmdEditor>
  154. );
  155. }
  156. const isRevisionOutdated = revisionId !== remoteRevisionId;
  157. const isHackmdDocumentOutdated = revisionIdHackmdSynced !== remoteRevisionId;
  158. let content;
  159. /*
  160. * HackMD is not setup
  161. */
  162. if (hackmdUri == null) {
  163. content = (
  164. <div>
  165. <p className="text-center hackmd-status-label"><i className="fa fa-file-text"></i> HackMD is not set up.</p>
  166. </div>
  167. );
  168. }
  169. /*
  170. * Resume to edit or discard changes
  171. */
  172. else if (isResume) {
  173. const title = (
  174. <React.Fragment>
  175. <span className="btn-label"><i className="icon-control-end"></i></span>
  176. Resume to edit with HackMD
  177. </React.Fragment>
  178. );
  179. content = (
  180. <div>
  181. <p className="text-center hackmd-status-label"><i className="fa fa-file-text"></i> HackMD is READY!</p>
  182. <div className="text-center hackmd-resume-button-container mb-3">
  183. <SplitButton
  184. id="split-button-resume-hackmd"
  185. title={title}
  186. bsStyle="success"
  187. bsSize="large"
  188. className="btn-resume waves-effect waves-light"
  189. onClick={() => { return this.resumeToEdit() }}
  190. >
  191. <MenuItem className="text-center" onClick={() => { return this.discardChanges() }}>
  192. <i className="icon-control-rewind"></i> Discard changes
  193. </MenuItem>
  194. </SplitButton>
  195. </div>
  196. <p className="text-center">
  197. Click to edit from the previous continuation<br />
  198. or
  199. <button
  200. type="button"
  201. className="btn btn-link text-danger p-0 hackmd-discard-button"
  202. onClick={() => { return this.discardChanges() }}
  203. >
  204. Discard changes
  205. </button>.
  206. </p>
  207. { isHackmdDocumentOutdated
  208. && (
  209. <div className="panel panel-warning mt-5">
  210. <div className="panel-heading"><i className="icon-fw icon-info"></i> DRAFT MAY BE OUTDATED</div>
  211. <div className="panel-body text-center">
  212. The current draft on HackMD is based on&nbsp;
  213. <a href={`?revision=${revisionIdHackmdSynced}`}><span className="label label-default">{revisionIdHackmdSynced.substr(-8)}</span></a>.<br />
  214. <button
  215. type="button"
  216. className="btn btn-link text-danger p-0 hackmd-discard-button"
  217. onClick={() => { return this.discardChanges() }}
  218. >
  219. Discard it
  220. </button> to start to edit with current revision.
  221. </div>
  222. </div>
  223. )
  224. }
  225. </div>
  226. );
  227. }
  228. /*
  229. * Start to edit
  230. */
  231. else {
  232. content = (
  233. <div>
  234. <p className="text-center hackmd-status-label"><i className="fa fa-file-text"></i> HackMD is READY!</p>
  235. <div className="text-center hackmd-start-button-container mb-3">
  236. <button
  237. className="btn btn-info btn-lg waves-effect waves-light"
  238. type="button"
  239. disabled={isRevisionOutdated || this.state.isInitializing}
  240. onClick={() => { return this.startToEdit() }}
  241. >
  242. <span className="btn-label"><i className="icon-paper-plane"></i></span>
  243. Start to edit with HackMD
  244. </button>
  245. </div>
  246. <p className="text-center">Click to clone page content and start to edit.</p>
  247. </div>
  248. );
  249. }
  250. return (
  251. <div className="hackmd-preinit d-flex justify-content-center align-items-center">
  252. {content}
  253. </div>
  254. );
  255. }
  256. }
  257. /**
  258. * Wrapper component for using unstated
  259. */
  260. const PageEditorByHackmdWrapper = (props) => {
  261. return createSubscribedElement(PageEditorByHackmd, props, [AppContainer, PageContainer]);
  262. };
  263. PageEditorByHackmd.propTypes = {
  264. appContainer: PropTypes.instanceOf(AppContainer).isRequired,
  265. pageContainer: PropTypes.instanceOf(PageContainer).isRequired,
  266. onSaveWithShortcut: PropTypes.func.isRequired,
  267. };
  268. export default PageEditorByHackmdWrapper;