PageEditorByHackmd.jsx 6.7 KB

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