Explorar o código

WIP: use containers

Yuki Takei %!s(int64=6) %!d(string=hai) anos
pai
achega
7902a4b95f

+ 20 - 22
src/client/js/app.js

@@ -158,29 +158,27 @@ const saveWithShortcutSuccessHandler = function(page) {
     extendedTimeOut: '150',
   });
 
-  pageId = page._id;
-  pageRevisionId = page.revision._id;
-  pageRevisionIdHackmdSynced = page.revisionHackmdSynced;
+  // update state of PageContainer
+  const newState = {
+    pageId: page._id,
+    revisionHackmdSynced: page.revisionHackmdSynced,
+    revisionId: page.revision._id,
+    markdown: page.revision.body,
+  };
+  pageContainer.setState(newState);
 
-  // set page id to SavePageControls
-  pageContainer.setState({ pageId });
-
-  // Page component
-  if (componentInstances.page != null) {
-    componentInstances.page.setMarkdown(page.revision.body);
-  }
   // PageEditor component
   if (componentInstances.pageEditor != null) {
-    const updateEditorValue = (editorMode !== 'builtin');
-    componentInstances.pageEditor.setMarkdown(page.revision.body, updateEditorValue);
+    if (editorMode !== 'builtin') {
+      componentInstances.pageEditor.updateEditorValue(newState.markdown);
+    }
   }
   // PageEditorByHackmd component
   if (componentInstances.pageEditorByHackmd != null) {
     // clear state of PageEditorByHackmd
-    componentInstances.pageEditorByHackmd.clearRevisionStatus(pageRevisionId, pageRevisionIdHackmdSynced);
+    componentInstances.pageEditorByHackmd.clearRevisionStatus(newState.revisionId);
     // reset
     if (editorMode !== 'hackmd') {
-      componentInstances.pageEditorByHackmd.setMarkdown(page.revision.body, false);
       componentInstances.pageEditorByHackmd.reset();
     }
   }
@@ -343,7 +341,7 @@ if (pageId) {
   componentMappings['page-attachment'] = <PageAttachment pageId={pageId} markdown={markdown} crowi={crowi} />;
 }
 if (pagePath) {
-  componentMappings.page = <Page crowi={crowi} crowiRenderer={crowiRenderer} markdown={markdown} pagePath={pagePath} onSaveWithShortcut={saveWithShortcut} />;
+  componentMappings.page = <Page crowiRenderer={crowiRenderer} onSaveWithShortcut={saveWithShortcut} />;
   componentMappings['revision-path'] = <I18nextProvider i18n={i18n}><RevisionPath pageId={pageId} pagePath={pagePath} crowi={crowi} /></I18nextProvider>;
   componentMappings['tag-label'] = <I18nextProvider i18n={i18n}><TagLabels crowi={crowi} pageId={pageId} sendTagData={setTagData} templateTagData={templateTagData} /></I18nextProvider>;
 }
@@ -351,7 +349,12 @@ if (pagePath) {
 Object.keys(componentMappings).forEach((key) => {
   const elem = document.getElementById(key);
   if (elem) {
-    componentInstances[key] = ReactDOM.render(componentMappings[key], elem);
+    componentInstances[key] = ReactDOM.render(
+      <Provider inject={[appContainer, pageContainer]}>
+        {componentMappings[key]}
+      </Provider>,
+      elem,
+    );
   }
 });
 
@@ -478,19 +481,14 @@ const pageEditorElem = document.getElementById('page-editor');
 if (pageEditorElem) {
   ReactDOM.render(
     <I18nextProvider i18n={i18n}>
-      <Provider inject={[editorContainer]}>
+      <Provider inject={[appContainer, pageContainer, editorContainer]}>
         <PageEditor
           ref={(elem) => {
             if (pageEditor == null) {
               pageEditor = elem;
             }
           }}
-          crowi={crowi}
           crowiRenderer={crowiRenderer}
-          pageId={pageId}
-          revisionId={pageRevisionId}
-          pagePath={pagePath}
-          markdown={markdown}
           onSaveWithShortcut={saveWithShortcut}
         />
       </Provider>

+ 37 - 15
src/client/js/components/Page.jsx

@@ -1,35 +1,36 @@
+/* eslint-disable react/no-multi-comp */
 import React from 'react';
 import PropTypes from 'prop-types';
+import { Subscribe } from 'unstated';
+
+import AppContainer from '../services/AppContainer';
+import PageContainer from '../services/PageContainer';
 
 import RevisionRenderer from './Page/RevisionRenderer';
 import HandsontableModal from './PageEditor/HandsontableModal';
 import MarkdownTable from '../models/MarkdownTable';
 import mtu from './PageEditor/MarkdownTableUtil';
 
-export default class Page extends React.Component {
+class Page extends React.Component {
 
   constructor(props) {
     super(props);
 
     this.state = {
-      markdown: this.props.markdown,
       currentTargetTableArea: null,
     };
 
     this.saveHandlerForHandsontableModal = this.saveHandlerForHandsontableModal.bind(this);
   }
 
-  setMarkdown(markdown) {
-    this.setState({ markdown });
-  }
-
   /**
    * launch HandsontableModal with data specified by arguments
    * @param beginLineNumber
    * @param endLineNumber
    */
   launchHandsontableModal(beginLineNumber, endLineNumber) {
-    const tableLines = this.state.markdown.split(/\r\n|\r|\n/).slice(beginLineNumber - 1, endLineNumber).join('\n');
+    const markdown = this.props.pageContainer.state.markdown;
+    const tableLines = markdown.split(/\r\n|\r|\n/).slice(beginLineNumber - 1, endLineNumber).join('\n');
     this.setState({ currentTargetTableArea: { beginLineNumber, endLineNumber } });
     this.handsontableModal.show(MarkdownTable.fromMarkdownString(tableLines));
   }
@@ -37,7 +38,7 @@ export default class Page extends React.Component {
   saveHandlerForHandsontableModal(markdownTable) {
     const newMarkdown = mtu.replaceMarkdownTableInMarkdown(
       markdownTable,
-      this.state.markdown,
+      this.props.pageContainer.state.markdown,
       this.state.currentTargetTableArea.beginLineNumber,
       this.state.currentTargetTableArea.endLineNumber,
     );
@@ -46,15 +47,12 @@ export default class Page extends React.Component {
   }
 
   render() {
-    const isMobile = this.props.crowi.isMobile;
+    const isMobile = this.props.appContainer.isMobile;
 
     return (
       <div className={isMobile ? 'page-mobile' : ''}>
         <RevisionRenderer
-          crowi={this.props.crowi}
           crowiRenderer={this.props.crowiRenderer}
-          markdown={this.state.markdown}
-          pagePath={this.props.pagePath}
         />
         <HandsontableModal ref={(c) => { this.handsontableModal = c }} onSave={this.saveHandlerForHandsontableModal} />
       </div>
@@ -63,10 +61,34 @@ export default class Page extends React.Component {
 
 }
 
+/**
+ * Wrapper component for using unstated
+ */
+class PageWrapper extends React.PureComponent {
+
+  render() {
+    return (
+      <Subscribe to={[AppContainer, PageContainer]}>
+        { (appContainer, pageContainer) => (
+          // eslint-disable-next-line arrow-body-style
+          <Page appContainer={appContainer} pageContainer={pageContainer} {...this.props} />
+        )}
+      </Subscribe>
+    );
+  }
+
+}
+
 Page.propTypes = {
-  crowi: PropTypes.object.isRequired,
+  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
+  pageContainer: PropTypes.instanceOf(PageContainer).isRequired,
+  crowiRenderer: PropTypes.object.isRequired,
+  onSaveWithShortcut: PropTypes.func.isRequired,
+};
+
+PageWrapper.propTypes = {
   crowiRenderer: PropTypes.object.isRequired,
   onSaveWithShortcut: PropTypes.func.isRequired,
-  markdown: PropTypes.string.isRequired,
-  pagePath: PropTypes.string.isRequired,
 };
+
+export default PageWrapper;

+ 47 - 16
src/client/js/components/Page/RevisionRenderer.jsx

@@ -1,9 +1,14 @@
+/* eslint-disable react/no-multi-comp */
 import React from 'react';
 import PropTypes from 'prop-types';
+import { Subscribe } from 'unstated';
+
+import AppContainer from '../../services/AppContainer';
+import PageContainer from '../../services/PageContainer';
 
 import RevisionBody from './RevisionBody';
 
-export default class RevisionRenderer extends React.Component {
+class RevisionRenderer extends React.Component {
 
   constructor(props) {
     super(props);
@@ -14,16 +19,16 @@ export default class RevisionRenderer extends React.Component {
 
     this.renderHtml = this.renderHtml.bind(this);
     this.getHighlightedBody = this.getHighlightedBody.bind(this);
-
-    this.setMarkdown(this.props.markdown);
   }
 
-  componentWillReceiveProps(nextProps) {
-    this.renderHtml(nextProps.markdown, this.props.highlightKeywords);
+  componentWillMount() {
+    const { pageContainer } = this.props;
+    this.renderHtml(pageContainer.state.markdown, this.props.highlightKeywords);
   }
 
-  setMarkdown(markdown) {
-    this.renderHtml(markdown, this.props.highlightKeywords);
+  componentWillReceiveProps(nextProps) {
+    const { pageContainer } = nextProps;
+    this.renderHtml(pageContainer.state.markdown, this.props.highlightKeywords);
   }
 
   /**
@@ -48,14 +53,16 @@ export default class RevisionRenderer extends React.Component {
     return returnBody;
   }
 
-  renderHtml(markdown, highlightKeywords) {
+  renderHtml(markdown) {
+    const { pageContainer } = this.props;
+
     const context = {
       markdown,
-      currentPagePath: this.props.pagePath,
+      currentPagePath: pageContainer.state.path,
     };
 
     const crowiRenderer = this.props.crowiRenderer;
-    const interceptorManager = this.props.crowi.interceptorManager;
+    const interceptorManager = this.props.appContainer.interceptorManager;
     interceptorManager.process('preRender', context)
       .then(() => { return interceptorManager.process('prePreProcess', context) })
       .then(() => {
@@ -70,8 +77,8 @@ export default class RevisionRenderer extends React.Component {
         context.parsedHTML = crowiRenderer.postProcess(context.parsedHTML);
 
         // highlight
-        if (highlightKeywords != null) {
-          context.parsedHTML = this.getHighlightedBody(context.parsedHTML, highlightKeywords);
+        if (this.props.highlightKeywords != null) {
+          context.parsedHTML = this.getHighlightedBody(context.parsedHTML, this.props.highlightKeywords);
         }
       })
       .then(() => { return interceptorManager.process('postPostProcess', context) })
@@ -85,7 +92,7 @@ export default class RevisionRenderer extends React.Component {
   }
 
   render() {
-    const config = this.props.crowi.getConfig();
+    const config = this.props.appContainer.getConfig();
     const isMathJaxEnabled = !!config.env.MATHJAX;
 
     return (
@@ -99,10 +106,34 @@ export default class RevisionRenderer extends React.Component {
 
 }
 
+/**
+ * Wrapper component for using unstated
+ */
+class RevisionRendererWrapper extends React.PureComponent {
+
+  render() {
+    return (
+      <Subscribe to={[AppContainer, PageContainer]}>
+        { (appContainer, pageContainer) => (
+          // eslint-disable-next-line arrow-body-style
+          <RevisionRenderer appContainer={appContainer} pageContainer={pageContainer} {...this.props} />
+        )}
+      </Subscribe>
+    );
+  }
+
+}
+
 RevisionRenderer.propTypes = {
-  crowi: PropTypes.object.isRequired,
+  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
+  pageContainer: PropTypes.instanceOf(PageContainer).isRequired,
   crowiRenderer: PropTypes.object.isRequired,
-  markdown: PropTypes.string.isRequired,
-  pagePath: PropTypes.string.isRequired,
   highlightKeywords: PropTypes.string,
 };
+
+RevisionRendererWrapper.propTypes = {
+  crowiRenderer: PropTypes.object.isRequired,
+  highlightKeywords: PropTypes.string,
+};
+
+export default RevisionRendererWrapper;

+ 60 - 29
src/client/js/components/PageEditor.js

@@ -1,36 +1,40 @@
+/* eslint-disable react/no-multi-comp */
 import React from 'react';
 import PropTypes from 'prop-types';
+import { Subscribe } from 'unstated';
 
 import { throttle, debounce } from 'throttle-debounce';
 
 import * as toastr from 'toastr';
 import GrowiRenderer from '../util/GrowiRenderer';
 
+import AppContainer from '../services/AppContainer';
+import PageContainer from '../services/PageContainer';
+import EditorContainer from '../services/EditorContainer';
+
 import Editor from './PageEditor/Editor';
 import Preview from './PageEditor/Preview';
 import scrollSyncHelper from './PageEditor/ScrollSyncHelper';
 
 
-export default class PageEditor extends React.Component {
+class PageEditor extends React.Component {
 
   constructor(props) {
     super(props);
 
-    const config = this.props.crowi.getConfig();
+    const config = this.props.appContainer.getConfig();
     const isUploadable = config.upload.image || config.upload.file;
     const isUploadableFile = config.upload.file;
     const isMathJaxEnabled = !!config.env.MATHJAX;
 
     this.state = {
-      pageId: this.props.pageId,
-      revisionId: this.props.revisionId,
-      markdown: this.props.markdown,
+      markdown: this.props.pageContainer.state.markdown,
       isUploadable,
       isUploadableFile,
       isMathJaxEnabled,
     };
 
-    this.growiRenderer = new GrowiRenderer(this.props.crowi, this.props.crowiRenderer, { mode: 'editor' });
+    this.growiRenderer = new GrowiRenderer(window.crowi, this.props.crowiRenderer, { mode: 'editor' });
 
     this.setCaretLine = this.setCaretLine.bind(this);
     this.focusToEditor = this.focusToEditor.bind(this);
@@ -70,7 +74,7 @@ export default class PageEditor extends React.Component {
   }
 
   showUnsavedWarning(e) {
-    if (!this.props.crowi.getIsDocSaved()) {
+    if (!this.props.appContainer.getIsDocSaved()) {
       // display browser default message
       e.returnValue = '';
       return '';
@@ -81,11 +85,8 @@ export default class PageEditor extends React.Component {
     return this.state.markdown;
   }
 
-  setMarkdown(markdown, updateEditorValue = true) {
-    this.setState({ markdown });
-    if (updateEditorValue) {
-      this.editor.setValue(markdown);
-    }
+  updateEditorValue(markdown) {
+    this.editor.setValue(markdown);
   }
 
   focusToEditor() {
@@ -108,12 +109,12 @@ export default class PageEditor extends React.Component {
   onMarkdownChanged(value) {
     this.renderPreviewWithDebounce(value);
     this.saveDraftWithDebounce();
-    this.props.crowi.setIsDocSaved(false);
+    this.props.appContainer.setIsDocSaved(false);
   }
 
   onSave() {
     this.props.onSaveWithShortcut(this.state.markdown);
-    this.props.crowi.setIsDocSaved(true);
+    this.props.appContainer.setIsDocSaved(true);
   }
 
   /**
@@ -122,18 +123,22 @@ export default class PageEditor extends React.Component {
    */
   async onUpload(file) {
     try {
-      let res = await this.props.crowi.apiGet('/attachments.limit', { _csrf: this.props.crowi.csrfToken, fileSize: file.size });
+      let res = await this.props.appContainer.apiGet('/attachments.limit', {
+        _csrf: this.props.appContainer.csrfToken,
+        fileSize: file.size,
+      });
+
       if (!res.isUploadable) {
         throw new Error(res.errorMessage);
       }
 
       const formData = new FormData();
-      formData.append('_csrf', this.props.crowi.csrfToken);
+      formData.append('_csrf', this.props.appContainer.csrfToken);
       formData.append('file', file);
-      formData.append('path', this.props.pagePath);
+      formData.append('path', this.props.pageContainer.state.pagePath);
       formData.append('page_id', this.state.pageId || 0);
 
-      res = await this.props.crowi.apiPost('/attachments.add', formData);
+      res = await this.props.appContainer.apiPost('/attachments.add', formData);
       const attachment = res.attachment;
       const fileName = attachment.originalName;
 
@@ -256,14 +261,15 @@ export default class PageEditor extends React.Component {
   }
 
   saveDraft() {
+    const { pageContainer } = this.props;
     // only when the first time to edit
     if (!this.state.revisionId) {
-      this.props.crowi.saveDraft(this.props.pagePath, this.state.markdown);
+      this.props.appContainer.saveDraft(pageContainer.state.pagePath, this.state.markdown);
     }
   }
 
   clearDraft() {
-    this.props.crowi.clearDraft(this.props.pagePath);
+    this.props.appContainer.clearDraft(this.props.pageContainer.state.pagePath);
   }
 
   renderPreview(value) {
@@ -276,7 +282,7 @@ export default class PageEditor extends React.Component {
     };
 
     const growiRenderer = this.growiRenderer;
-    const interceptorManager = this.props.crowi.interceptorManager;
+    const interceptorManager = this.props.appContainer.interceptorManager;
     interceptorManager.process('preRenderPreview', context)
       .then(() => { return interceptorManager.process('prePreProcess', context) })
       .then(() => {
@@ -313,9 +319,11 @@ export default class PageEditor extends React.Component {
   }
 
   render() {
-    const config = this.props.crowi.getConfig();
+    const { pageContainer } = this.props;
+
+    const config = this.props.appContainer.getConfig();
     const noCdn = !!config.env.NO_CDN;
-    const emojiStrategy = this.props.crowi.getEmojiStrategy();
+    const emojiStrategy = this.props.appContainer.getEmojiStrategy();
 
     return (
       <div className="row">
@@ -324,7 +332,7 @@ export default class PageEditor extends React.Component {
             ref={(c) => { this.editor = c }}
             value={this.state.markdown}
             noCdn={noCdn}
-            isMobile={this.props.crowi.isMobile}
+            isMobile={this.props.appContainer.isMobile}
             isUploadable={this.state.isUploadable}
             isUploadableFile={this.state.isUploadableFile}
             emojiStrategy={emojiStrategy}
@@ -351,12 +359,35 @@ export default class PageEditor extends React.Component {
 
 }
 
+/**
+ * Wrapper component for using unstated
+ */
+class PageEditorWrapper extends React.Component {
+
+  render() {
+    return (
+      <Subscribe to={[AppContainer, PageContainer, EditorContainer]}>
+        { (appContainer, pageContainer, editorContainer) => (
+          // eslint-disable-next-line arrow-body-style
+          <PageEditor appContainer={appContainer} pageContainer={pageContainer} editorContainer={editorContainer} {...this.props} />
+        )}
+      </Subscribe>
+    );
+  }
+
+}
+
 PageEditor.propTypes = {
-  crowi: PropTypes.object.isRequired,
+  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
+  pageContainer: PropTypes.instanceOf(PageContainer).isRequired,
+  editorContainer: PropTypes.instanceOf(EditorContainer).isRequired,
+  crowiRenderer: PropTypes.object.isRequired,
+  onSaveWithShortcut: PropTypes.func.isRequired,
+};
+
+PageEditorWrapper.propTypes = {
   crowiRenderer: PropTypes.object.isRequired,
   onSaveWithShortcut: PropTypes.func.isRequired,
-  markdown: PropTypes.string.isRequired,
-  pageId: PropTypes.string,
-  revisionId: PropTypes.string,
-  pagePath: PropTypes.string,
 };
+
+export default PageEditorWrapper;

+ 1 - 10
src/client/js/components/PageEditorByHackmd.jsx

@@ -51,13 +51,6 @@ export default class PageEditorByHackmd extends React.PureComponent {
       });
   }
 
-  setMarkdown(markdown, updateEditorValue = true) {
-    this.setState({ markdown });
-    if (this.state.isInitialized && updateEditorValue) {
-      this.hackmdEditor.setValue(markdown);
-    }
-  }
-
   /**
    * reset initialized status
    */
@@ -68,11 +61,9 @@ export default class PageEditorByHackmd extends React.PureComponent {
   /**
    * clear revision status (invoked when page is updated by myself)
    */
-  clearRevisionStatus(updatedRevisionId, updatedRevisionIdHackmdSynced) {
+  clearRevisionStatus(updatedRevisionId) {
     this.setState({
       initialRevisionId: updatedRevisionId,
-      revisionId: updatedRevisionId,
-      revisionIdHackmdSynced: updatedRevisionIdHackmdSynced,
     });
   }
 

+ 1 - 3
src/client/js/components/PageStatusAlert.jsx

@@ -33,11 +33,9 @@ class PageStatusAlert extends React.Component {
   /**
    * clear status (invoked when page is updated by myself)
    */
-  clearRevisionStatus(updatedRevisionId, updatedRevisionIdHackmdSynced) {
+  clearRevisionStatus(updatedRevisionId) {
     this.setState({
       initialRevisionId: updatedRevisionId,
-      revisionId: updatedRevisionId,
-      revisionIdHackmdSynced: updatedRevisionIdHackmdSynced,
       hasDraftOnHackmd: false,
       isDraftUpdatingInRealtime: false,
     });