PageEditorByHackmd.jsx 8.5 KB

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