PageEditorByHackmd.jsx 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264
  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. revisionId: this.props.revisionId,
  15. revisionIdHackmdSynced: this.props.revisionIdHackmdSynced,
  16. pageIdOnHackmd: this.props.pageIdOnHackmd,
  17. hasDraftOnHackmd: this.props.hasDraftOnHackmd,
  18. };
  19. this.getHackmdUri = this.getHackmdUri.bind(this);
  20. this.startToEdit = this.startToEdit.bind(this);
  21. this.resumeToEdit = this.resumeToEdit.bind(this);
  22. this.hackmdEditorChangeHandler = this.hackmdEditorChangeHandler.bind(this);
  23. this.apiErrorHandler = this.apiErrorHandler.bind(this);
  24. }
  25. componentWillMount() {
  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.refs.hackmdEditor.getValue()
  36. .then(document => {
  37. this.setState({ markdown: document });
  38. return document;
  39. });
  40. }
  41. setMarkdown(markdown, updateEditorValue = true) {
  42. this.setState({ markdown });
  43. if (this.state.isInitialized && updateEditorValue) {
  44. this.refs.hackmdEditor.setValue(markdown);
  45. }
  46. }
  47. /**
  48. * update revisionId of state
  49. * @param {string} revisionId
  50. */
  51. setRevisionId(revisionId) {
  52. this.setState({revisionId});
  53. }
  54. /**
  55. * update revisionIdHackmdSynced of state
  56. * @param {string} revisionIdHackmdSynced
  57. */
  58. setRevisionIdHackmdSynced(revisionIdHackmdSynced) {
  59. this.setState({revisionIdHackmdSynced});
  60. }
  61. /**
  62. * update hasDraftOnHackmd of state
  63. * @param {bool} hasDraftOnHackmd
  64. */
  65. setHasDraftOnHackmd(hasDraftOnHackmd) {
  66. this.setState({hasDraftOnHackmd});
  67. }
  68. getHackmdUri() {
  69. const envVars = this.props.crowi.config.env;
  70. return envVars.HACKMD_URI;
  71. }
  72. /**
  73. * Start integration with HackMD
  74. */
  75. startToEdit() {
  76. const hackmdUri = this.getHackmdUri();
  77. if (hackmdUri == null) {
  78. // do nothing
  79. return;
  80. }
  81. this.setState({
  82. isInitialized: false,
  83. isInitializing: true,
  84. });
  85. const params = {
  86. pageId: this.props.pageId,
  87. };
  88. this.props.crowi.apiPost('/hackmd.integrate', params)
  89. .then(res => {
  90. if (!res.ok) {
  91. throw new Error(res.error);
  92. }
  93. this.setState({
  94. isInitialized: true,
  95. pageIdOnHackmd: res.pageIdOnHackmd,
  96. revisionIdHackmdSynced: res.revisionIdHackmdSynced,
  97. });
  98. })
  99. .catch(this.apiErrorHandler)
  100. .then(() => {
  101. this.setState({isInitializing: false});
  102. });
  103. }
  104. /**
  105. * Start to edit w/o any api request
  106. */
  107. resumeToEdit() {
  108. this.setState({isInitialized: true});
  109. }
  110. /**
  111. * Reset draft
  112. */
  113. discardChanges() {
  114. this.setState({hasDraftOnHackmd: false});
  115. }
  116. /**
  117. * onChange event of HackmdEditor handler
  118. */
  119. hackmdEditorChangeHandler(body) {
  120. const hackmdUri = this.getHackmdUri();
  121. if (hackmdUri == null) {
  122. // do nothing
  123. return;
  124. }
  125. // do nothing if contents are same
  126. if (this.props.markdown === body) {
  127. return;
  128. }
  129. const params = {
  130. pageId: this.props.pageId,
  131. };
  132. this.props.crowi.apiPost('/hackmd.saveOnHackmd', params)
  133. .then(res => {
  134. // do nothing
  135. })
  136. .catch(err => {
  137. // do nothing
  138. });
  139. }
  140. apiErrorHandler(error) {
  141. toastr.error(error.message, 'Error occured', {
  142. closeButton: true,
  143. progressBar: true,
  144. newestOnTop: false,
  145. showDuration: '100',
  146. hideDuration: '100',
  147. timeOut: '3000',
  148. });
  149. }
  150. render() {
  151. const hackmdUri = this.getHackmdUri();
  152. const isPageExistsOnHackmd = (this.state.pageIdOnHackmd != null);
  153. const isResume = isPageExistsOnHackmd && this.state.hasDraftOnHackmd;
  154. const isRevisionMatch = (this.state.revisionId === this.state.revisionIdHackmdSynced);
  155. if (this.state.isInitialized) {
  156. return (
  157. <HackmdEditor
  158. ref='hackmdEditor'
  159. hackmdUri={hackmdUri}
  160. pageIdOnHackmd={this.state.pageIdOnHackmd}
  161. initializationMarkdown={isResume ? null : this.state.markdown}
  162. onChange={this.hackmdEditorChangeHandler}
  163. onSaveWithShortcut={(document) => {
  164. this.props.onSaveWithShortcut(document);
  165. }}
  166. >
  167. </HackmdEditor>
  168. );
  169. }
  170. let content = undefined;
  171. // HackMD is not setup
  172. if (hackmdUri == null) {
  173. content = (
  174. <div>
  175. <p className="text-center hackmd-status-label"><i className="fa fa-file-text"></i> HackMD is not set up.</p>
  176. </div>
  177. );
  178. }
  179. else if (isResume) {
  180. const title = (
  181. <React.Fragment>
  182. <span className="btn-label"><i className="icon-control-end"></i></span>
  183. Resume to edit with HackMD
  184. </React.Fragment>);
  185. content = (
  186. <div>
  187. <p className="text-center hackmd-status-label"><i className="fa fa-file-text"></i> HackMD is READY!</p>
  188. <div className="text-center hackmd-resume-button-container mb-3">
  189. <SplitButton id='split-button-resume-hackmd' title={title} bsStyle="success" bsSize="large" className="btn-resume waves-effect waves-light" onClick={() => this.resumeToEdit()}>
  190. <MenuItem className="text-center" onClick={() => this.discardChanges()}>
  191. <i className="icon-control-rewind"></i> Discard changes
  192. </MenuItem>
  193. </SplitButton>
  194. </div>
  195. <p className="text-center">Click to edit from the previous continuation.</p>
  196. </div>
  197. );
  198. }
  199. else {
  200. content = (
  201. <div>
  202. <p className="text-center hackmd-status-label"><i className="fa fa-file-text"></i> HackMD is READY!</p>
  203. <div className="text-center hackmd-start-button-container mb-3">
  204. <button className="btn btn-info btn-lg waves-effect waves-light" type="button"
  205. onClick={() => this.startToEdit()} disabled={this.state.isInitializing}>
  206. <span className="btn-label"><i className="icon-paper-plane"></i></span>
  207. Start to edit with HackMD
  208. </button>
  209. </div>
  210. <p className="text-center">Click to clone page content and start to edit.</p>
  211. </div>
  212. );
  213. }
  214. return (
  215. <div className="hackmd-preinit d-flex justify-content-center align-items-center">
  216. {content}
  217. </div>
  218. );
  219. }
  220. }
  221. PageEditorByHackmd.propTypes = {
  222. crowi: PropTypes.object.isRequired,
  223. markdown: PropTypes.string.isRequired,
  224. onSaveWithShortcut: PropTypes.func.isRequired,
  225. pageId: PropTypes.string,
  226. revisionId: PropTypes.string,
  227. pageIdOnHackmd: PropTypes.string,
  228. revisionIdHackmdSynced: PropTypes.string,
  229. hasDraftOnHackmd: PropTypes.bool,
  230. };