Yuki Takei 6 лет назад
Родитель
Сommit
5c6eb52ce2

+ 3 - 12
src/client/js/app.js

@@ -63,7 +63,6 @@ const i18n = i18nFactory(userlang);
 const xss = new Xss();
 window.xss = xss;
 
-let pageTags = [];
 const isLoggedin = document.querySelector('.main-container.nologin') == null;
 
 // create unstated container instance
@@ -101,14 +100,6 @@ if (isEnabledPlugins) {
   crowiPlugin.installAll(crowi, crowiRenderer);
 }
 
-/**
- * receive tags from PageTagForm
- * @param {Array} tagData new tags
- */
-const setTagData = function(tagData) {
-  pageTags = tagData;
-};
-
 /**
  * save success handler when reloading is not needed
  * @param {object} page Page instance
@@ -179,7 +170,7 @@ const saveWithShortcut = function(markdown) {
   // get options
   const options = pageContainer.getCurrentOptionsToSave();
   options.socketClientId = websocketContainer.getCocketClientId();
-  options.pageTags = pageTags;
+  // options.pageTags = pageTags;
 
   if (editorMode === 'hackmd') {
     // set option to sync
@@ -218,7 +209,7 @@ const saveWithSubmitButton = function(submitOpts) {
   // get options
   const options = pageContainer.getCurrentOptionsToSave();
   options.socketClientId = websocketContainer.getCocketClientId();
-  options.pageTags = pageTags;
+  // options.pageTags = pageTags;
 
   // set 'submitOpts.overwriteScopesOfDescendants' to options
   options.overwriteScopesOfDescendants = submitOpts ? !!submitOpts.overwriteScopesOfDescendants : false;
@@ -300,7 +291,7 @@ if (pageContainer.state.path != null) {
     // eslint-disable-next-line quote-props
     'page': <Page crowiRenderer={crowiRenderer} onSaveWithShortcut={saveWithShortcut} />,
     'revision-path':  <RevisionPath pageId={pageContainer.state.pageId} pagePath={pageContainer.state.path} crowi={crowi} />,
-    'tag-label':  <TagLabels crowi={crowi} pageId={pageContainer.state.pageId} sendTagData={setTagData} templateTagData={pageContainer.state.templateTagData} />,
+    'tag-label':  <TagLabels />,
   }, componentMappings);
 }
 

+ 11 - 49
src/client/js/components/Page/TagEditor.jsx

@@ -1,11 +1,13 @@
 import React from 'react';
 import PropTypes from 'prop-types';
-import * as toastr from 'toastr';
 import Button from 'react-bootstrap/es/Button';
 import Modal from 'react-bootstrap/es/Modal';
+
+import AppContainer from '../../services/AppContainer';
+
 import TagsInput from './TagsInput';
 
-class TagEditor extends React.Component {
+export default class TagEditor extends React.Component {
 
   constructor(props) {
     super(props);
@@ -13,23 +15,20 @@ class TagEditor extends React.Component {
     this.state = {
       tags: [],
       isOpenModal: false,
-      isEditorMode: null,
     };
 
     this.show = this.show.bind(this);
-    this.onTagsUpdatedByFormHandler = this.onTagsUpdatedByFormHandler.bind(this);
+    this.onTagsUpdatedByTagsInput = this.onTagsUpdatedByTagsInput.bind(this);
     this.closeModalHandler = this.closeModalHandler.bind(this);
     this.handleSubmit = this.handleSubmit.bind(this);
-    this.apiSuccessHandler = this.apiSuccessHandler.bind(this);
-    this.apiErrorHandler = this.apiErrorHandler.bind(this);
   }
 
   show(tags) {
-    const isEditorMode = this.props.crowi.getCrowiForJquery().getCurrentEditorMode();
-    this.setState({ isOpenModal: true, isEditorMode, tags });
+    // const isEditorMode = this.props.crowi.getCrowiForJquery().getCurrentEditorMode();
+    this.setState({ tags, isOpenModal: true });
   }
 
-  onTagsUpdatedByFormHandler(tags) {
+  onTagsUpdatedByTagsInput(tags) {
     this.setState({ tags });
   }
 
@@ -38,47 +37,12 @@ class TagEditor extends React.Component {
   }
 
   async handleSubmit() {
-
-    if (!this.state.isEditorMode) {
-      try {
-        await this.props.crowi.apiPost('/tags.update', { pageId: this.props.pageId, tags: this.state.tags });
-        this.apiSuccessHandler();
-      }
-      catch (err) {
-        this.apiErrorHandler(err);
-        return;
-      }
-    }
-
     this.props.onTagsUpdated(this.state.tags);
 
     // close modal
     this.setState({ isOpenModal: false });
   }
 
-  apiSuccessHandler() {
-    toastr.success(undefined, 'updated tags successfully', {
-      closeButton: true,
-      progressBar: true,
-      newestOnTop: false,
-      showDuration: '100',
-      hideDuration: '100',
-      timeOut: '1200',
-      extendedTimeOut: '150',
-    });
-  }
-
-  apiErrorHandler(err) {
-    toastr.error(err.message, 'Error occured', {
-      closeButton: true,
-      progressBar: true,
-      newestOnTop: false,
-      showDuration: '100',
-      hideDuration: '100',
-      timeOut: '3000',
-    });
-  }
-
   render() {
     return (
       <Modal show={this.state.isOpenModal} onHide={this.closeModalHandler} id="editTagModal">
@@ -86,7 +50,7 @@ class TagEditor extends React.Component {
           <Modal.Title className="text-white">Edit Tags</Modal.Title>
         </Modal.Header>
         <Modal.Body>
-          <TagsInput crowi={this.props.crowi} tags={this.state.tags} onTagsUpdated={this.onTagsUpdatedByFormHandler} />
+          <TagsInput tags={this.state.tags} onTagsUpdated={this.onTagsUpdatedByTagsInput} />
         </Modal.Body>
         <Modal.Footer>
           <Button variant="primary" onClick={this.handleSubmit}>
@@ -100,9 +64,7 @@ class TagEditor extends React.Component {
 }
 
 TagEditor.propTypes = {
-  crowi: PropTypes.object.isRequired,
-  pageId: PropTypes.string,
+  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
+
   onTagsUpdated: PropTypes.func.isRequired,
 };
-
-export default TagEditor;

+ 73 - 36
src/client/js/components/Page/TagLabels.jsx

@@ -2,6 +2,12 @@ import React from 'react';
 import PropTypes from 'prop-types';
 import { withTranslation } from 'react-i18next';
 
+import * as toastr from 'toastr';
+
+import { createSubscribedElement } from '../UnstatedUtils';
+import AppContainer from '../../services/AppContainer';
+import PageContainer from '../../services/PageContainer';
+
 import TagEditor from './TagEditor';
 
 class TagLabels extends React.Component {
@@ -10,61 +16,86 @@ class TagLabels extends React.Component {
     super(props);
 
     this.state = {
-      tags: [],
+      showTagEditor: false,
     };
 
     this.showEditor = this.showEditor.bind(this);
     this.tagsUpdatedHandler = this.tagsUpdatedHandler.bind(this);
   }
 
-  async componentWillMount() {
-    // set pageTag on button
-    const pageId = this.props.pageId;
+  showEditor() {
+    const { tags } = this.props.pageContainer.state;
+    this.tagEditor.show(tags);
+  }
+
+  async tagsUpdatedHandler(tags) {
+    // TODO
+    // if (currentEditorMode == null) {
+    try {
+      const { pageContainer } = this.props;
+      const { pageId } = this.props.pageContainer.state;
+      await this.props.appContainer.apiPost('/tags.update', { pageId, tags });
+
+      // update pageContainer.state
+      pageContainer.setState({ tags });
 
-    if (pageId) {
-      const res = await this.props.crowi.apiGet('/pages.getPageTag', { pageId });
-      this.setState({ tags: res.tags });
-      this.props.sendTagData(res.tags);
+      this.apiSuccessHandler();
     }
-    else if (this.props.templateTagData) {
-      const templateTags = this.props.templateTagData.split(',');
-      this.setState({ tags: templateTags });
-      this.props.sendTagData(templateTags);
+    catch (err) {
+      this.apiErrorHandler(err);
+      return;
     }
+    // else {
+    // update editorContainer.tags
+    // }
   }
 
-  showEditor() {
-    this.tagEditor.show(this.state.tags);
+  apiSuccessHandler() {
+    toastr.success(undefined, 'updated tags successfully', {
+      closeButton: true,
+      progressBar: true,
+      newestOnTop: false,
+      showDuration: '100',
+      hideDuration: '100',
+      timeOut: '1200',
+      extendedTimeOut: '150',
+    });
   }
 
-  tagsUpdatedHandler(tags) {
-    this.setState({ tags });
-    this.props.sendTagData(tags);
+  apiErrorHandler(err) {
+    toastr.error(err.message, 'Error occured', {
+      closeButton: true,
+      progressBar: true,
+      newestOnTop: false,
+      showDuration: '100',
+      hideDuration: '100',
+      timeOut: '3000',
+    });
   }
 
   render() {
-    const tagElements = [];
-    const { t, pageId } = this.props;
+    const { t } = this.props;
+    const { pageId } = this.props.pageContainer.state;
+    const { tags } = this.props.pageContainer.state;
 
-    for (let i = 0; i < this.state.tags.length; i++) {
-      tagElements.push(
-        <span key={`${pageId}_${i}`} className="text-muted">
+    const tagElements = tags.map((tag) => {
+      return (
+        <span key={`${pageId}_${tag}`} className="text-muted">
           <i className="tag-icon icon-tag mr-1"></i>
-          <a className="tag-name mr-2" href={`/_search?q=tag:${this.state.tags[i]}`} key={i.toString()}>{this.state.tags[i]}</a>
-        </span>,
+          <a className="tag-name mr-2" href={`/_search?q=tag:${tag}`} key={`${pageId}_${tag}_link`}>{tag}</a>
+        </span>
       );
-
-    }
+    });
 
     return (
-      <div className={`tag-viewer ${this.props.pageId ? 'existed-page' : 'new-page'}`}>
-        {this.state.tags.length === 0 && (
+      <div className={`tag-viewer ${pageId ? 'existed-page' : 'new-page'}`}>
+        {tags.length === 0 && (
           <a className="btn btn-link btn-edit-tags no-tags p-0" onClick={this.showEditor}>
             { t('Add tags for this page') } <i className="manage-tags ml-2 icon-plus"></i>
           </a>
         )}
         {tagElements}
-        {this.state.tags.length > 0 && (
+        {tags.length > 0 && (
           <a className="btn btn-link btn-edit-tags p-0" onClick={this.showEditor}>
             <i className="manage-tags ml-2 icon-plus"></i> { t('Edit tags for this page') }
           </a>
@@ -72,8 +103,8 @@ class TagLabels extends React.Component {
 
         <TagEditor
           ref={(c) => { this.tagEditor = c }}
-          crowi={this.props.crowi}
-          pageId={this.props.pageId}
+          appContainer={this.props.appContainer}
+          show={this.state.showTagEditor}
           onTagsUpdated={this.tagsUpdatedHandler}
         >
         </TagEditor>
@@ -83,12 +114,18 @@ class TagLabels extends React.Component {
 
 }
 
+/**
+ * Wrapper component for using unstated
+ */
+const TagLabelsWrapper = (props) => {
+  return createSubscribedElement(TagLabels, props, [AppContainer, PageContainer]);
+};
+
+
 TagLabels.propTypes = {
   t: PropTypes.func.isRequired, // i18next
-  crowi: PropTypes.object.isRequired,
-  pageId: PropTypes.string,
-  sendTagData: PropTypes.func.isRequired,
-  templateTagData: PropTypes.string,
+  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
+  pageContainer: PropTypes.instanceOf(PageContainer).isRequired,
 };
 
-export default withTranslation()(TagLabels);
+export default withTranslation()(TagLabelsWrapper);

+ 16 - 4
src/client/js/components/Page/TagsInput.jsx

@@ -2,6 +2,9 @@ import React from 'react';
 import PropTypes from 'prop-types';
 import { AsyncTypeahead } from 'react-bootstrap-typeahead';
 
+import { createSubscribedElement } from '../UnstatedUtils';
+import AppContainer from '../../services/AppContainer';
+
 /**
  *
  * @author Yuki Takei <yuki@weseek.co.jp>
@@ -11,7 +14,7 @@ import { AsyncTypeahead } from 'react-bootstrap-typeahead';
  * @extends {React.Component}
  */
 
-export default class TagsInput extends React.Component {
+class TagsInput extends React.Component {
 
   constructor(props) {
     super(props);
@@ -22,7 +25,6 @@ export default class TagsInput extends React.Component {
       selected: this.props.tags,
       defaultPageTags: this.props.tags,
     };
-    this.crowi = this.props.crowi;
 
     this.handleChange = this.handleChange.bind(this);
     this.handleSearch = this.handleSearch.bind(this);
@@ -42,7 +44,7 @@ export default class TagsInput extends React.Component {
 
   async handleSearch(query) {
     this.setState({ isLoading: true });
-    const res = await this.crowi.apiGet('/tags.search', { q: query });
+    const res = await this.props.appContainer.apiGet('/tags.search', { q: query });
     res.tags.unshift(query); // selectable new tag whose name equals query
     this.setState({
       resultTags: Array.from(new Set(res.tags)), // use Set for de-duplication
@@ -87,11 +89,21 @@ export default class TagsInput extends React.Component {
 
 }
 
+/**
+ * Wrapper component for using unstated
+ */
+const TagsInputWrapper = (props) => {
+  return createSubscribedElement(TagsInput, props, [AppContainer]);
+};
+
 TagsInput.propTypes = {
-  crowi: PropTypes.object.isRequired,
+  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
+
   tags: PropTypes.array.isRequired,
   onTagsUpdated: PropTypes.func.isRequired,
 };
 
 TagsInput.defaultProps = {
 };
+
+export default TagsInputWrapper;

+ 19 - 0
src/client/js/services/PageContainer.js

@@ -35,6 +35,7 @@ export default class PageContainer extends Container {
       revisionCreatedAt: +mainContent.getAttribute('data-page-revision-created'),
       path: mainContent.getAttribute('data-path'),
 
+      tags: [],
       templateTagData: mainContent.getAttribute('data-template-tags') || '',
 
       isSlackEnabled: false,
@@ -56,6 +57,7 @@ export default class PageContainer extends Container {
     this.initStateMarkdown();
     this.initStateGrant();
     this.initDrafts();
+    this.retrieveTags();
 
     this.addWebSocketEventHandlers = this.addWebSocketEventHandlers.bind(this);
     this.addWebSocketEventHandlers();
@@ -93,6 +95,23 @@ export default class PageContainer extends Container {
     }
   }
 
+  /**
+   * retrieve tags data
+   */
+  async retrieveTags() {
+    const { pageId, templateTagData } = this.state;
+
+    // when the page exists
+    if (pageId != null) {
+      const res = await this.appContainer.apiGet('/pages.getPageTag', { pageId });
+      this.setState({ tags: res.tags });
+    }
+    // when the page not exist
+    else if (templateTagData != null) {
+      this.setState({ tags: templateTagData.split(',') });
+    }
+  }
+
   /**
    * initialize state for drafts
    */