Przeglądaj źródła

impl CommentContainer with unstated

Yuki Takei 6 lat temu
rodzic
commit
fbf35241bb

+ 9 - 13
src/client/js/app.js

@@ -2,6 +2,7 @@
 
 import React from 'react';
 import ReactDOM from 'react-dom';
+import { Provider } from 'unstated';
 import { I18nextProvider } from 'react-i18next';
 import * as toastr from 'toastr';
 
@@ -28,7 +29,7 @@ import PageEditorByHackmd from './components/PageEditorByHackmd';
 import Page from './components/Page';
 import PageHistory from './components/PageHistory';
 import PageComments from './components/PageComments';
-import CommentForm from './components/PageComment/CommentForm';
+import CommentContainer from './components/PageComment/CommentContainer';
 import CommentEditor from './components/PageComment/CommentEditor';
 import PageAttachment from './components/PageAttachment';
 import PageStatusAlert from './components/PageStatusAlert';
@@ -514,17 +515,12 @@ const postCompleteHandler = (comment) => {
 // render comment form
 const writeCommentElem = document.getElementById('page-comment-write');
 if (writeCommentElem) {
+  // create unstated container instance
+  const commentContainer = new CommentContainer(crowi, pageId, pageRevisionId);
+
   ReactDOM.render(
-    <I18nextProvider i18n={i18n}>
-      <CommentForm
-        onPostComplete={postCompleteHandler}
-        replyTo={undefined}
-        pageId={pageId}
-        pagePath={pagePath}
-        slackChannels={slackChannels}
-        crowi={crowi}
-        revisionId={pageRevisionId}
-      >
+    <Provider inject={[commentContainer]}>
+      <I18nextProvider i18n={i18n}>
         <CommentEditor
           editorOptions={pageEditorOptions}
           crowi={crowi}
@@ -532,8 +528,8 @@ if (writeCommentElem) {
           showCommentEditor
         >
         </CommentEditor>
-      </CommentForm>
-    </I18nextProvider>,
+      </I18nextProvider>
+    </Provider>,
     writeCommentElem,
   );
 }

+ 76 - 0
src/client/js/components/PageComment/CommentContainer.jsx

@@ -0,0 +1,76 @@
+import { Container } from 'unstated';
+
+/**
+ *
+ * @author Yuki Takei <yuki@weseek.co.jp>
+ *
+ * @extends {Container} unstated Container
+ */
+export default class CommentContainer extends Container {
+
+  constructor(crowi, pageId, revisionId) {
+    super();
+
+    this.crowi = crowi;
+    this.pageId = pageId;
+    this.revisionId = revisionId;
+  }
+
+  init() {
+    if (!this.props.pageId) {
+      return;
+    }
+  }
+
+  /**
+   * Load data of comments and rerender <PageComments />
+   */
+  postComment(comment, isMarkdown, replyTo, isSlackEnabled, slackChannels) {
+    return this.crowi.apiPost('/comments.add', {
+      commentForm: {
+        comment,
+        _csrf: this.crowi.csrfToken,
+        page_id: this.pageId,
+        revision_id: this.revisionId,
+        is_markdown: isMarkdown,
+        replyTo,
+      },
+      slackNotificationForm: {
+        isSlackEnabled,
+        slackChannels,
+      },
+    });
+  }
+
+  onUpload(file) {
+    const endpoint = '/attachments.add';
+
+    // // create a FromData instance
+    // const formData = new FormData();
+    // formData.append('_csrf', this.props.data.crowi.csrfToken);
+    // formData.append('file', file);
+    // formData.append('path', this.props.data.pagePath);
+    // formData.append('page_id', this.props.data.pageId || 0);
+
+    // // post
+    // this.props.data.crowi.apiPost(endpoint, formData)
+    //   .then((res) => {
+    //     const attachment = res.attachment;
+    //     const fileName = attachment.originalName;
+
+    //     let insertText = `[${fileName}](${attachment.filePathProxied})`;
+    //     // when image
+    //     if (attachment.fileFormat.startsWith('image/')) {
+    //       // modify to "![fileName](url)" syntax
+    //       insertText = `!${insertText}`;
+    //     }
+    //     this.editor.insertText(insertText);
+    //   })
+    //   .catch(this.apiErrorHandler)
+    //   // finally
+    //   .then(() => {
+    //     this.editor.terminateUploadingState();
+    //   });
+  }
+
+}

+ 130 - 98
src/client/js/components/PageComment/CommentEditor.jsx

@@ -1,6 +1,8 @@
 import React from 'react';
 import PropTypes from 'prop-types';
 
+import { Subscribe } from 'unstated';
+
 import Button from 'react-bootstrap/es/Button';
 import Tab from 'react-bootstrap/es/Tab';
 import Tabs from 'react-bootstrap/es/Tabs';
@@ -10,6 +12,7 @@ import ReactUtils from '../ReactUtils';
 import GrowiRenderer from '../../util/GrowiRenderer';
 
 import Editor from '../PageEditor/Editor';
+import CommentContainer from './CommentContainer';
 import CommentPreview from './CommentPreview';
 import SlackNotification from '../SlackNotification';
 
@@ -103,10 +106,37 @@ export default class CommentEditor extends React.Component {
   /**
    * Load data of comments and rerender <PageComments />
    */
-  onSubmit(event) {
+  onSubmit(event, commentContainer) {
     if (event != null) {
       event.preventDefault();
     }
+
+    commentContainer.postComment(
+      this.state.comment,
+      this.state.isMarkdown,
+      null, // TODO set replyTo
+      this.state.isSlackEnabled,
+      this.state.slackChannels,
+    );
+    // .then((res) => {
+    //   if (this.props.onPostComplete != null) {
+    //     this.props.onPostComplete(res.comment);
+    //   }
+    //   this.setState({
+    //     comment: '',
+    //     isMarkdown: true,
+    //     html: '',
+    //     key: 1,
+    //     errorMessage: undefined,
+    //     isSlackEnabled: false,
+    //   });
+    //   // reset value
+    //   this.editor.setValue('');
+    // })
+    // .catch((err) => {
+    //   const errorMessage = err.message || 'An unknown error occured when posting comment';
+    //   this.setState({ errorMessage });
+    // });
   }
 
   getCommentHtml() {
@@ -224,49 +254,52 @@ export default class CommentEditor extends React.Component {
 
     const errorMessage = <span className="text-danger text-right mr-2">{this.state.errorMessage}</span>;
     const submitButton = (
-      <Button bsStyle="primary" className="fcbtn btn btn-sm btn-primary btn-outline btn-rounded btn-1b" onClick={this.onSubmit}>
-        Comment
-      </Button>
+      <Subscribe to={[CommentContainer]}>
+        { commentContainer => (
+          // eslint-disable-next-line arrow-body-style
+          <Button bsStyle="primary" className="fcbtn btn btn-sm btn-primary btn-outline btn-rounded btn-1b" onClick={(e) => { this.onSubmit(e, commentContainer) }}>
+            Comment
+          </Button>
+        )}
+      </Subscribe>
     );
 
     return (
       <div>
-        {
-          this.state.showCommentEditor
-          && (
-
-          <div>
 
-            { username
+        { username
+          && (
+          <div className="comment-form">
+            { isLayoutTypeGrowi
               && (
-              <div className="comment-form">
-                { isLayoutTypeGrowi
-                  && (
-                  <div className="comment-form-user">
-                    <UserPicture user={user} />
-                  </div>
-                  )
-                }
-                <div className="comment-form-main">
-                  {/* Add Comment Button */}
-                  { !this.state.isFormShown
-                    && (
-                    <button
-                      type="button"
-                      className={`btn btn-lg ${isLayoutTypeGrowi ? 'btn-link' : 'btn-primary'} center-block`}
-                      onClick={this.showCommentFormBtnClickHandler}
-                    >
-                      <i className="icon-bubble"></i> Add Comment
-                    </button>
-                    )
-                  }
-                  {/* Editor */}
-                  { this.state.isFormShown
-                    && (
-                    <React.Fragment>
-                      <div className="comment-write">
-                        <Tabs activeKey={this.state.key} id="comment-form-tabs" onSelect={this.handleSelect} animation={false}>
-                          <Tab eventKey={1} title="Write">
+              <div className="comment-form-user">
+                <UserPicture user={user} />
+              </div>
+              )
+            }
+            <div className="comment-form-main">
+              {/* Add Comment Button */}
+              { !this.state.isFormShown
+                && (
+                <button
+                  type="button"
+                  className={`btn btn-lg ${isLayoutTypeGrowi ? 'btn-link' : 'btn-primary'} center-block`}
+                  onClick={this.showCommentFormBtnClickHandler}
+                >
+                  <i className="icon-bubble"></i> Add Comment
+                </button>
+                )
+              }
+              {/* Editor */}
+              { this.state.isFormShown
+                && (
+                <React.Fragment>
+                  <div className="comment-write">
+                    <Tabs activeKey={this.state.key} id="comment-form-tabs" onSelect={this.handleSelect} animation={false}>
+                      <Tab eventKey={1} title="Write">
+                        <Subscribe to={[CommentContainer]}>
+                          { commentContainer => (
+                            // eslint-disable-next-line arrow-body-style
                             <Editor
                               ref={(c) => { this.editor = c }}
                               value={this.state.comment}
@@ -279,72 +312,72 @@ export default class CommentEditor extends React.Component {
                               emojiStrategy={emojiStrategy}
                               onChange={this.updateState}
                               onUpload={this.onUpload}
-                              onCtrlEnter={this.onSubmit}
+                              // onCtrlEnter={(e) => { this.onSubmit(e, commentContainer) }}
+                              onCtrlEnter={(e) => { this.onSubmit(e, commentContainer) }}
                             />
-                          </Tab>
-                          { this.state.isMarkdown
-                            && (
-                            <Tab eventKey={2} title="Preview">
-                              <div className="comment-form-preview">
-                                {commentPreview}
-                              </div>
-                            </Tab>
-                            )
-                          }
-                        </Tabs>
-                      </div>
-                      <div className="comment-submit">
-                        <div className="d-flex">
-                          <label style={{ flex: 1 }}>
-                            { isLayoutTypeGrowi && this.state.key === 1
-                              && (
-                              <span>
-                                <input
-                                  type="checkbox"
-                                  id="comment-form-is-markdown"
-                                  name="isMarkdown"
-                                  checked={this.state.isMarkdown}
-                                  value="1"
-                                  onChange={this.updateStateCheckbox}
-                                />
-                                <span className="ml-2">Markdown</span>
-                              </span>
-                              )
-                          }
-                          </label>
-                          <span className="hidden-xs">{ this.state.errorMessage && errorMessage }</span>
-                          { this.state.hasSlackConfig
-                            && (
-                            <div className="form-inline align-self-center mr-md-2">
-                              <SlackNotification
-                                isSlackEnabled={this.state.isSlackEnabled}
-                                slackChannels={this.state.slackChannels}
-                                onEnabledFlagChange={this.onSlackEnabledFlagChange}
-                                onChannelChange={this.onSlackChannelsChange}
-                              />
-                            </div>
-                            )
-                          }
-                          <div className="hidden-xs">{submitButton}</div>
-                        </div>
-                        <div className="visible-xs mt-2">
-                          <div className="d-flex justify-content-end">
-                            { this.state.errorMessage && errorMessage }
-                            <div>{submitButton}</div>
+                          )}
+                        </Subscribe>
+                      </Tab>
+                      { this.state.isMarkdown
+                        && (
+                        <Tab eventKey={2} title="Preview">
+                          <div className="comment-form-preview">
+                            {commentPreview}
                           </div>
+                        </Tab>
+                        )
+                      }
+                    </Tabs>
+                  </div>
+                  <div className="comment-submit">
+                    <div className="d-flex">
+                      <label style={{ flex: 1 }}>
+                        { isLayoutTypeGrowi && this.state.key === 1
+                          && (
+                          <span>
+                            <input
+                              type="checkbox"
+                              id="comment-form-is-markdown"
+                              name="isMarkdown"
+                              checked={this.state.isMarkdown}
+                              value="1"
+                              onChange={this.updateStateCheckbox}
+                            />
+                            <span className="ml-2">Markdown</span>
+                          </span>
+                          )
+                      }
+                      </label>
+                      <span className="hidden-xs">{ this.state.errorMessage && errorMessage }</span>
+                      { this.state.hasSlackConfig
+                        && (
+                        <div className="form-inline align-self-center mr-md-2">
+                          <SlackNotification
+                            isSlackEnabled={this.state.isSlackEnabled}
+                            slackChannels={this.state.slackChannels}
+                            onEnabledFlagChange={this.onSlackEnabledFlagChange}
+                            onChannelChange={this.onSlackChannelsChange}
+                          />
                         </div>
+                        )
+                      }
+                      <div className="hidden-xs">{submitButton}</div>
+                    </div>
+                    <div className="visible-xs mt-2">
+                      <div className="d-flex justify-content-end">
+                        { this.state.errorMessage && errorMessage }
+                        <div>{submitButton}</div>
                       </div>
-                    </React.Fragment>
-                    )
-                  }
-                </div>
-              </div>
-              )
-            }
-
+                    </div>
+                  </div>
+                </React.Fragment>
+                )
+              }
+            </div>
           </div>
           )
         }
+
       </div>
     );
   }
@@ -357,5 +390,4 @@ CommentEditor.propTypes = {
   slackChannels: PropTypes.string,
   crowi: PropTypes.object.isRequired,
   crowiOriginRenderer: PropTypes.object.isRequired,
-  showCommentEditor: PropTypes.bool,
 };

+ 0 - 164
src/client/js/components/PageComment/CommentForm.jsx

@@ -1,164 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-
-import Button from 'react-bootstrap/es/Button';
-import Tab from 'react-bootstrap/es/Tab';
-import Tabs from 'react-bootstrap/es/Tabs';
-import * as toastr from 'toastr';
-import UserPicture from '../User/UserPicture';
-import ReactUtils from '../ReactUtils';
-
-/**
- *
- * @author Yuki Takei <yuki@weseek.co.jp>
- *
- * @export
- * @class Comment
- * @extends {React.Component}
- */
-
-export default class CommentForm extends React.Component {
-
-  constructor(props) {
-    super(props);
-
-    this.state = {
-    };
-
-    // 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);
-    // this.onSlackEnabledFlagChange = this.onSlackEnabledFlagChange.bind(this);
-    // this.onSlackChannelsChange = this.onSlackChannelsChange.bind(this);
-    // this.showCommentFormBtnClickHandler = this.showCommentFormBtnClickHandler.bind(this);
-  }
-
-  componentWillMount() {
-    this.init();
-  }
-
-  init() {
-    if (!this.props.pageId) {
-      return;
-    }
-  }
-
-  /**
-   * Load data of comments and rerender <PageComments />
-   */
-  postComment(event) {
-    if (event != null) {
-      event.preventDefault();
-    }
-
-    // this.props.data.crowi.apiPost('/comments.add', {
-    //   commentForm: {
-    //     comment: this.state.comment,
-    //     _csrf: this.props.data.crowi.csrfToken,
-    //     page_id: this.props.data.pageId,
-    //     revision_id: this.props.data.revisionId,
-    //     is_markdown: this.state.isMarkdown,
-    //     replyTo: this.props.replyTo,
-    //   },
-    //   slackNotificationForm: {
-    //     isSlackEnabled: this.state.isSlackEnabled,
-    //     slackChannels: this.state.slackChannels,
-    //   },
-    // })
-    //   .then((res) => {
-    //     if (this.props.onPostComplete != null) {
-    //       this.props.onPostComplete(res.comment);
-    //     }
-    //     this.setState({
-    //       comment: '',
-    //       isMarkdown: true,
-    //       html: '',
-    //       key: 1,
-    //       errorMessage: undefined,
-    //       isSlackEnabled: false,
-    //     });
-    //     // reset value
-    //     this.editor.setValue('');
-    //   })
-    //   .catch((err) => {
-    //     const errorMessage = err.message || 'An unknown error occured when posting comment';
-    //     this.setState({ errorMessage });
-    //   });
-  }
-
-  onUpload(file) {
-    const endpoint = '/attachments.add';
-
-    // // create a FromData instance
-    // const formData = new FormData();
-    // formData.append('_csrf', this.props.data.crowi.csrfToken);
-    // formData.append('file', file);
-    // formData.append('path', this.props.data.pagePath);
-    // formData.append('page_id', this.props.data.pageId || 0);
-
-    // // post
-    // this.props.data.crowi.apiPost(endpoint, formData)
-    //   .then((res) => {
-    //     const attachment = res.attachment;
-    //     const fileName = attachment.originalName;
-
-    //     let insertText = `[${fileName}](${attachment.filePathProxied})`;
-    //     // when image
-    //     if (attachment.fileFormat.startsWith('image/')) {
-    //       // modify to "![fileName](url)" syntax
-    //       insertText = `!${insertText}`;
-    //     }
-    //     this.editor.insertText(insertText);
-    //   })
-    //   .catch(this.apiErrorHandler)
-    //   // finally
-    //   .then(() => {
-    //     this.editor.terminateUploadingState();
-    //   });
-  }
-
-  apiErrorHandler(error) {
-    toastr.error(error.message, 'Error occured', {
-      closeButton: true,
-      progressBar: true,
-      newestOnTop: false,
-      showDuration: '100',
-      hideDuration: '100',
-      timeOut: '3000',
-    });
-  }
-
-  render() {
-    const children = React.Children.map(this.props.children, (child) => {
-      return React.cloneElement(child, {
-        onPostComplete: this.props.onPostComplete,
-        replyTo: this.props.replyTo,
-        pageId: this.props.pageId,
-        pagePath: this.props.pagePath,
-        slackChannels: this.props.slackChannels,
-        revisionId: this.props.revisionId,
-      });
-    });
-    return (
-      <div>
-        {children}
-      </div>
-    );
-  }
-
-}
-
-CommentForm.propTypes = {
-  children: PropTypes.node.isRequired,
-  onPostComplete: PropTypes.func,
-  replyTo: PropTypes.string,
-  pageId: PropTypes.string,
-  pagePath: PropTypes.string,
-  slackChannels: PropTypes.string,
-  revisionId: PropTypes.string,
-  crowi: PropTypes.object.isRequired,
-};

+ 21 - 23
src/client/js/components/PageComments.js

@@ -2,10 +2,12 @@
 import React from 'react';
 import PropTypes from 'prop-types';
 
+import { Provider } from 'unstated';
+
 import { withTranslation } from 'react-i18next';
 import GrowiRenderer from '../util/GrowiRenderer';
 
-import CommentForm from './PageComment/CommentForm';
+import CommentContainer from './PageComment/CommentContainer';
 import CommentEditor from './PageComment/CommentEditor';
 
 import Comment from './PageComment/Comment';
@@ -154,19 +156,13 @@ class PageComments extends React.Component {
    * @memberOf PageComments
    */
   generateCommentElements(comments, replies) {
+    // create unstated container instance
+    const commentContainer = new CommentContainer(this.props.crowi, this.props.pageId, this.props.revisionId);
+
     const commentsWithReplies = this.reorderBasedOnReplies(comments, replies);
     return commentsWithReplies.map((comment) => {
       return (
-        <CommentForm
-          key={comment._id}
-          onPostComplete={this.retrieveData}
-          replyTo={comment.replyTo}
-          pageId={this.props.pageId}
-          pagePath={this.props.pagePath}
-          slackChannels={this.props.slackChannels}
-          crowi={this.props.crowi}
-          revisionId={this.props.revisionId}
-        >
+        <div key={comment._id}>
           <Comment
             comment={comment}
             deleteBtnClicked={this.confirmToDeleteComment}
@@ -174,14 +170,16 @@ class PageComments extends React.Component {
             onReplyButtonClicked={this.replyToComment}
             crowi={this.props.crowi}
           />
-          <CommentEditor
-            ref={(instance) => { this.state.children[comment._id] = instance }}
-            editorOptions={this.props.editorOptions}
-            crowiOriginRenderer={this.props.crowiOriginRenderer}
-            showCommentEditor={false}
-            crowi={this.props.crowi}
-          />
-        </CommentForm>
+          { true && (
+            <Provider key={comment._id} inject={[commentContainer]}>
+              <CommentEditor
+                crowi={this.props.crowi}
+                crowiOriginRenderer={this.props.crowiOriginRenderer}
+                editorOptions={this.props.editorOptions}
+              />
+            </Provider>
+          )}
+        </div>
       );
     });
   }
@@ -313,14 +311,14 @@ class PageComments extends React.Component {
 }
 
 PageComments.propTypes = {
+  crowi: PropTypes.object.isRequired,
+  crowiOriginRenderer: PropTypes.object.isRequired,
+  pageId: PropTypes.string.isRequired,
+  revisionId: PropTypes.string.isRequired,
   revisionCreatedAt: PropTypes.number,
-  pageId: PropTypes.string,
   pagePath: PropTypes.string,
   editorOptions: PropTypes.object,
   slackChannels: PropTypes.string,
-  crowi: PropTypes.object.isRequired,
-  crowiOriginRenderer: PropTypes.object.isRequired,
-  revisionId: PropTypes.string,
 };
 
 export default withTranslation(null, { withRef: true })(PageComments);