PageEditorByHackmd.jsx 8.4 KB

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