Jelajahi Sumber

Merge pull request #925 from weseek/feat/brush-up-tag-feature

Feat/brush up tag feature
yusuketk 7 tahun lalu
induk
melakukan
909b2f2cf5

+ 44 - 3
src/client/js/components/Page/TagLabel.jsx

@@ -1,6 +1,7 @@
 import React from 'react';
 import PropTypes from 'prop-types';
 import { withTranslation } from 'react-i18next';
+import * as toastr from 'toastr';
 import Button from 'react-bootstrap/es/Button';
 import Modal from 'react-bootstrap/es/Modal';
 import PageTagForm from '../PageTagForm';
@@ -14,12 +15,15 @@ class TagLabel extends React.Component {
       currentPageTags: [],
       newPageTags: [],
       isOpenModal: false,
+      isEditorMode: null,
     };
 
     this.addNewTag = this.addNewTag.bind(this);
     this.handleShowModal = this.handleShowModal.bind(this);
     this.handleCloseModal = this.handleCloseModal.bind(this);
     this.handleSubmit = this.handleSubmit.bind(this);
+    this.apiSuccessHandler = this.apiSuccessHandler.bind(this);
+    this.apiErrorHandler = this.apiErrorHandler.bind(this);
   }
 
   async componentWillMount() {
@@ -41,14 +45,51 @@ class TagLabel extends React.Component {
   }
 
   handleShowModal() {
-    this.setState({ isOpenModal: true });
+    const isEditorMode = this.props.crowi.getCrowiForJquery().getCurrentEditorMode();
+    this.setState({ isOpenModal: true, isEditorMode });
   }
 
-  handleSubmit() {
-    this.props.sendTagData(this.state.newPageTags);
+  async handleSubmit() {
+
+    if (this.state.isEditorMode) { // set tag on draft on edit
+      this.props.sendTagData(this.state.newPageTags);
+    }
+    else { // update tags without saving the page on view
+      try {
+        await this.props.crowi.apiPost('/tags.update', { pageId: this.props.pageId, tags: this.state.newPageTags });
+        this.apiSuccessHandler();
+      }
+      catch (err) {
+        this.apiErrorHandler(err);
+        return;
+      }
+    }
     this.setState({ currentPageTags: this.state.newPageTags, 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() {
     const tags = [];
     const { t } = this.props;

+ 39 - 15
src/client/js/components/PageTagForm.jsx

@@ -20,8 +20,42 @@ export default class PageTagForm extends React.Component {
       resultTags: [],
       isLoading: false,
       selected: this.props.currentPageTags,
+      defaultPageTags: this.props.currentPageTags,
     };
     this.crowi = this.props.crowi;
+
+    this.handleChange = this.handleChange.bind(this);
+    this.handleSearch = this.handleSearch.bind(this);
+    this.handleSelect = this.handleSelect.bind(this);
+  }
+
+  handleChange(selected) {
+    // list is a list of object about value. an element have customOption, id and label properties
+    this.setState({ selected }, () => {
+      this.props.addNewTag(this.state.selected);
+    });
+  }
+
+  async handleSearch(query) {
+    this.setState({ isLoading: true });
+    const res = await this.crowi.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
+      isLoading: false,
+    });
+  }
+
+  handleSelect(e) {
+    if (e.keyCode === 32) { // '32' means ASCII code of 'space'
+      e.preventDefault();
+      const instance = this.typeahead.getInstance();
+      const { initialItem } = instance.state;
+
+      if (initialItem) {
+        instance._handleMenuItemSelect(initialItem, e);
+      }
+    }
   }
 
   render() {
@@ -29,26 +63,16 @@ export default class PageTagForm extends React.Component {
       <div className="tag-typeahead">
         <AsyncTypeahead
           id="async-typeahead"
+          ref={(typeahead) => { this.typeahead = typeahead }}
           caseSensitive={false}
-          defaultSelected={this.props.currentPageTags}
+          defaultSelected={this.state.defaultPageTags}
           isLoading={this.state.isLoading}
           minLength={1}
           multiple
           newSelectionPrefix=""
-          onChange={(list) => { // list is a list of object about value. an element have customOption, id and label properties
-            this.setState({ selected: list.map((obj) => { return obj.label }) }, () => {
-              this.props.addNewTag(this.state.selected);
-            });
-          }}
-          onSearch={async(query) => {
-            this.setState({ isLoading: true });
-            const res = await this.crowi.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
-              isLoading: false,
-            });
-          }}
+          onChange={this.handleChange}
+          onSearch={this.handleSearch}
+          onKeyDown={this.handleSelect}
           options={this.state.resultTags} // Search result (Some tag names)
           placeholder="tag name"
           selectHintOnEnter

+ 6 - 0
src/server/models/page.js

@@ -353,6 +353,12 @@ module.exports = function(crowi) {
     return (this.latestRevision == this.revision._id.toString());
   };
 
+  pageSchema.methods.findRelatedTagsById = async function() {
+    const PageTagRelation = mongoose.model('PageTagRelation');
+    const relations = await PageTagRelation.find({ relatedPage: this._id }).populate('relatedTag');
+    return relations.map((relation) => { return relation.relatedTag.name });
+  };
+
   pageSchema.methods.isUpdatable = function(previousRevision) {
     const revision = this.latestRevision || this.revision;
     // comparing ObjectId with string

+ 1 - 0
src/server/routes/index.js

@@ -205,6 +205,7 @@ module.exports = function(crowi, app) {
   app.get('/tags'                     , loginRequired(crowi, app, false), tag.showPage);
   app.get('/_api/tags.list'           , accessTokenParser, loginRequired(crowi, app, false), tag.api.list);
   app.get('/_api/tags.search'         , accessTokenParser, loginRequired(crowi, app, false), tag.api.search);
+  app.post('/_api/tags.update'         , accessTokenParser, loginRequired(crowi, app, false), tag.api.update);
   app.get('/_api/comments.get'        , accessTokenParser , loginRequired(crowi, app, false) , comment.api.get);
   app.post('/_api/comments.add'       , form.comment, accessTokenParser , loginRequired(crowi, app) , csrf, comment.api.add);
   app.post('/_api/comments.remove'    , accessTokenParser , loginRequired(crowi, app) , csrf, comment.api.remove);

+ 2 - 0
src/server/routes/page.js

@@ -1074,10 +1074,12 @@ module.exports = function(crowi, app) {
     }
 
     await page.populateDataToShowRevision();
+    const originTags = await page.findRelatedTagsById();
 
     req.body.path = newPagePath;
     req.body.body = page.revision.body;
     req.body.grant = page.grant;
+    req.body.pageTags = originTags;
 
     return api.create(req, res);
   };

+ 20 - 0
src/server/routes/tag.js

@@ -25,6 +25,26 @@ module.exports = function(crowi, app) {
     return res.json(ApiResponse.success({ tags }));
   };
 
+  /**
+   * @api {post} /tags.update update tags
+   * @apiName UpdateTag
+   * @apiGroup Tag
+   *
+   * @apiParam {String} PageId
+   * @apiParam {array} tags
+   */
+  api.update = async function(req, res) {
+    const Page = crowi.model('Page');
+    try {
+      const page = await Page.findById(req.body.pageId);
+      await page.updateTags(req.body.tags);
+    }
+    catch (err) {
+      return res.json(ApiResponse.error(err));
+    }
+    return res.json(ApiResponse.success());
+  };
+
   /**
    * @api {get} /tags.list get tagnames and count pages relate each tag
    * @apiName tagList