Просмотр исходного кода

Merge pull request #497 from weseek/feat/editor-component-comment-write

Feat/editor component comment write
Yuki Takei 7 лет назад
Родитель
Сommit
da65ee81e0

+ 15 - 14
resource/js/app.js

@@ -134,20 +134,6 @@ Object.keys(componentMappings).forEach((key) => {
   }
 });
 
-// render comment form
-const writeCommentElem = document.getElementById('page-comment-write');
-if (writeCommentElem) {
-  const pageCommentsElem = componentInstances['page-comments-list'];
-  const postCompleteHandler = (comment) => {
-    if (pageCommentsElem != null) {
-      pageCommentsElem.retrieveData();
-    }
-  };
-  ReactDOM.render(
-    <CommentForm crowi={crowi} pageId={pageId} revisionId={pageRevisionId} onPostComplete={postCompleteHandler} crowiRenderer={crowiRenderer}/>,
-    writeCommentElem);
-}
-
 /*
  * PageEditor
  */
@@ -178,6 +164,21 @@ if (pageEditorElem) {
   // set refs for pageEditor
   crowi.setPageEditor(pageEditor);
 }
+
+// render comment form
+const writeCommentElem = document.getElementById('page-comment-write');
+if (writeCommentElem) {
+  const pageCommentsElem = componentInstances['page-comments-list'];
+  const postCompleteHandler = (comment) => {
+    if (pageCommentsElem != null) {
+      pageCommentsElem.retrieveData();
+    }
+  };
+  ReactDOM.render(
+    <CommentForm crowi={crowi} crowiRenderer={crowiRenderer} pageId={pageId} revisionId={pageRevisionId} pagePath={pagePath} onPostComplete={postCompleteHandler} editorOptions={editorOptions}/>,
+    writeCommentElem);
+}
+
 // render OptionsSelector
 const pageEditorOptionsSelectorElem = document.getElementById('page-editor-options-selector');
 if (pageEditorOptionsSelectorElem) {

+ 93 - 21
resource/js/components/PageComment/CommentForm.js

@@ -2,6 +2,9 @@ import React from 'react';
 import PropTypes from 'prop-types';
 import ReactUtils from '../ReactUtils';
 
+import * as toastr from 'toastr';
+
+import Editor from '../PageEditor/Editor';
 import CommentPreview from '../PageComment/CommentPreview';
 
 import Button from 'react-bootstrap/es/Button';
@@ -23,27 +26,36 @@ export default class CommentForm extends React.Component {
   constructor(props) {
     super(props);
 
+    const config = this.props.crowi.getConfig();
+    const isUploadable = config.upload.image || config.upload.file;
+    const isUploadableFile = config.upload.file;
+
     this.state = {
       comment: '',
       isMarkdown: true,
       html: '',
       key: 1,
+      isUploadable,
+      isUploadableFile,
+      errorMessage: undefined,
     };
 
     this.updateState = this.updateState.bind(this);
+    this.updateStateCheckbox = this.updateStateCheckbox.bind(this);
     this.postComment = this.postComment.bind(this);
     this.renderHtml = this.renderHtml.bind(this);
     this.handleSelect = this.handleSelect.bind(this);
+    this.apiErrorHandler = this.apiErrorHandler.bind(this);
+    this.onUpload = this.onUpload.bind(this);
   }
 
-  updateState(event) {
-    const target = event.target;
-    const value = target.type === 'checkbox' ? target.checked : target.value;
-    const name = target.name;
+  updateState(value) {
+    this.setState({comment: value});
+  }
 
-    this.setState({
-      [name]: value
-    });
+  updateStateCheckbox(event) {
+    const value = event.target.checked;
+    this.setState({isMarkdown: value});
   }
 
   handleSelect(key) {
@@ -74,7 +86,14 @@ export default class CommentForm extends React.Component {
           isMarkdown: true,
           html: '',
           key: 1,
+          errorMessage: undefined,
         });
+        // reset value
+        this.refs.editor.setValue('');
+      })
+      .catch(err => {
+        const errorMessage = err.message || 'An unknown error occured when posting comment';
+        this.setState({ errorMessage });
       });
   }
 
@@ -121,6 +140,49 @@ export default class CommentForm extends React.Component {
     return {__html: html};
   }
 
+  onUpload(file) {
+    const endpoint = '/attachments.add';
+
+    // create a FromData instance
+    const formData = new FormData();
+    formData.append('_csrf', this.props.crowi.csrfToken);
+    formData.append('file', file);
+    formData.append('path', this.props.pagePath);
+    formData.append('page_id', this.props.pageId || 0);
+
+    // post
+    this.props.crowi.apiPost(endpoint, formData)
+      .then((res) => {
+        const url = res.url;
+        const attachment = res.attachment;
+        const fileName = attachment.originalName;
+
+        let insertText = `[${fileName}](${url})`;
+        // when image
+        if (attachment.fileFormat.startsWith('image/')) {
+          // modify to "![fileName](url)" syntax
+          insertText = '!' + insertText;
+        }
+        this.refs.editor.insertText(insertText);
+      })
+      .catch(this.apiErrorHandler)
+      // finally
+      .then(() => {
+        this.refs.editor.terminateUploadingState();
+      });
+  }
+
+  apiErrorHandler(error) {
+    console.error(error);
+    toastr.error(error.message, 'Error occured', {
+      closeButton: true,
+      progressBar: true,
+      newestOnTop: false,
+      showDuration: '100',
+      hideDuration: '100',
+      timeOut: '3000',
+    });
+  }
 
   render() {
     const crowi = this.props.crowi;
@@ -129,7 +191,9 @@ export default class CommentForm extends React.Component {
     const creatorsPage = `/user/${username}`;
     const comment = this.state.comment;
     const commentPreview = this.state.isMarkdown ? this.getCommentHtml(): ReactUtils.nl2br(comment);
+    const emojiStrategy = this.props.crowi.getEmojiStrategy();
 
+    const editorOptions = Object.assign(this.props.editorOptions || {}, { lineNumbers: false });
     return (
       <div>
         <form className="form page-comment-form" id="page-comment-form" onSubmit={this.postComment}>
@@ -144,8 +208,16 @@ export default class CommentForm extends React.Component {
                 <div className="comment-write">
                   <Tabs activeKey={this.state.key} id="comment-form-tabs" onSelect={this.handleSelect} animation={false}>
                     <Tab eventKey={1} title="Write">
-                      <textarea className="comment-form-comment form-control" id="comment-form-comment" name="comment" required placeholder="Write comments here..." value={this.state.comment} onChange={this.updateState} >
-                      </textarea>
+                       <Editor ref="editor"
+                       value={this.state.comment}
+                       editorOptions={editorOptions}
+                       isMobile={this.props.crowi.isMobile}
+                       isUploadable={this.state.isUploadable}
+                       isUploadableFile={this.state.isUploadableFile}
+                       emojiStrategy={emojiStrategy}
+                       onChange={this.updateState}
+                       onUpload={this.onUpload}
+                      />
                     </Tab>
                     { this.state.isMarkdown == true &&
                     <Tab eventKey={2} title="Preview">
@@ -156,21 +228,19 @@ export default class CommentForm extends React.Component {
                     }
                   </Tabs>
                 </div>
-                <div className="comment-submit">
-                  <div className="pull-left">
+                <div className="comment-submit d-flex">
                   { this.state.key == 1 &&
                     <label>
-                      <input type="checkbox" id="comment-form-is-markdown" name="isMarkdown" checked={this.state.isMarkdown} value="1" onChange={this.updateState} /> Markdown
+                      <input type="checkbox" id="comment-form-is-markdown" name="isMarkdown" checked={this.state.isMarkdown} value="1" onChange={this.updateStateCheckbox} /> Markdown
                     </label>
                   }
-                  </div>
-                  <div className="pull-right">
-                    <Button type="submit" value="Submit" bsStyle="primary" className="fcbtn btn btn-sm btn-primary btn-outline btn-rounded btn-1b">
-                        Comment
-                    </Button>
-                  </div>
-                  <div className="clearfix">
-                  </div>
+                  <div style={{flex: 1}}></div>{/* spacer */}
+                  { this.state.errorMessage &&
+                    <span className="text-danger text-right mr-2">{this.state.errorMessage}</span>
+                  }
+                  <Button type="submit" value="Submit" bsStyle="primary" className="fcbtn btn btn-sm btn-primary btn-outline btn-rounded btn-1b">
+                    Comment
+                  </Button>
                 </div>
               </div>
             </div>
@@ -183,8 +253,10 @@ export default class CommentForm extends React.Component {
 
 CommentForm.propTypes = {
   crowi: PropTypes.object.isRequired,
+  crowiRenderer:  PropTypes.object.isRequired,
   onPostComplete: PropTypes.func,
   pageId: PropTypes.string,
   revisionId: PropTypes.string,
-  crowiRenderer:  PropTypes.object.isRequired,
+  pagePath: PropTypes.string,
+  editorOptions: PropTypes.object,
 };

+ 6 - 0
resource/js/components/PageEditor/AbstractEditor.js

@@ -21,6 +21,12 @@ export default class AbstractEditor extends React.Component {
   forceToFocus() {
   }
 
+  /**
+   * set new value
+   */
+  setValue(newValue) {
+  }
+
   /**
    * set caret position of codemirror
    * @param {string} number

+ 19 - 7
resource/js/components/PageEditor/CodeMirrorEditor.js

@@ -129,6 +129,14 @@ export default class CodeMirrorEditor extends AbstractEditor {
     }, 100);
   }
 
+  /**
+   * @inheritDoc
+   */
+  setValue(newValue) {
+    this.setState({ value: newValue });
+    this.getCodeMirror().getDoc().setValue(newValue);
+  }
+
   /**
    * @inheritDoc
    */
@@ -396,8 +404,12 @@ export default class CodeMirrorEditor extends AbstractEditor {
   }
 
   render() {
-    const theme = this.props.editorOptions.theme || 'elegant';
-    const styleActiveLine = this.props.editorOptions.styleActiveLine || undefined;
+    const defaultEditorOptions = {
+      theme: 'elegant',
+      lineNumbers: true,
+    };
+    const editorOptions = Object.assign(defaultEditorOptions, this.props.editorOptions || {});
+
     return <React.Fragment>
       <ReactCodeMirror
         ref="cm"
@@ -410,9 +422,9 @@ export default class CodeMirrorEditor extends AbstractEditor {
         value={this.state.value}
         options={{
           mode: 'gfm',
-          theme: theme,
-          styleActiveLine: styleActiveLine,
-          lineNumbers: true,
+          theme: editorOptions.theme,
+          styleActiveLine: editorOptions.styleActiveLine,
+          lineNumbers: editorOptions.lineNumbers,
           tabSize: 4,
           indentUnit: 4,
           lineWrapping: true,
@@ -421,8 +433,8 @@ export default class CodeMirrorEditor extends AbstractEditor {
           matchBrackets: true,
           matchTags: {bothTags: true},
           // folding
-          foldGutter: true,
-          gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'],
+          foldGutter: (editorOptions.lineNumbers ? true : false),
+          gutters: (editorOptions.lineNumbers ? ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'] : []),
           // match-highlighter, matchesonscrollbar, annotatescrollbar options
           highlightSelectionMatches: {annotateScrollbar: true},
           // markdown mode options

+ 7 - 0
resource/js/components/PageEditor/Editor.js

@@ -50,6 +50,13 @@ export default class Editor extends AbstractEditor {
     this.getEditorSubstance().forceToFocus();
   }
 
+  /**
+   * @inheritDoc
+   */
+  setValue(newValue) {
+    this.getEditorSubstance().setValue(newValue);
+  }
+
   /**
    * @inheritDoc
    */

+ 8 - 0
resource/js/components/PageEditor/TextAreaEditor.js

@@ -57,6 +57,14 @@ export default class TextAreaEditor extends AbstractEditor {
     }, 150);
   }
 
+  /**
+   * @inheritDoc
+   */
+  setValue(newValue) {
+    this.setState({ value: newValue });
+    this.textarea.value = newValue;
+  }
+
   /**
    * @inheritDoc
    */