Explorar o código

Merge pull request #166 from weseek/imprv/selective-batch-deletion-in-search-result-page

Imprv/selective batch deletion in search result page
Yuki Takei %!s(int64=8) %!d(string=hai) anos
pai
achega
9ae59cacd6

+ 2 - 0
lib/views/layout/layout.html

@@ -70,6 +70,8 @@
   data-me="{{ user._id.toString() }}"
   data-plugin-enabled="{{ isEnabledPlugins() }}"
  {% block html_base_attr %}{% endblock %}
+  data-csrftoken="{{ csrf() }}"
+  data-current-username="{% if user %}{{ user.username }}{% endif %}"
  >
 
 {% block layout_head_nav %}

+ 0 - 2
lib/views/not_found.html

@@ -38,12 +38,10 @@
   data-path-shortname="{{ path|path2name }}"
   data-page-id="{% if page %}{{ page._id.toString() }}{% endif %}"
   data-current-user="{% if user %}{{ user._id.toString() }}{% endif %}"
-  data-current-username="{% if user %}{{ user.username }}{% endif %}"
   data-page-revision-id="{% if revision %}{{ revision._id.toString() }}{% endif %}"
   data-page-revision-created="{% if revision %}{{ revision.createdAt|datetz('U') }}{% endif %}"
   data-page-is-seen="{% if page and page.isSeenUser(user) %}1{% else %}0{% endif %}"
   data-linebreaks-enabled="{{ isEnabledLinebreaks() }}"
-  data-csrftoken="{{ csrf() }}"
   >
 
   <ul class="nav nav-tabs hidden-print">

+ 0 - 2
lib/views/page.html

@@ -54,12 +54,10 @@
   data-path-shortname="{{ path|path2name }}"
   data-page-id="{% if page %}{{ page._id.toString() }}{% endif %}"
   data-current-user="{% if user %}{{ user._id.toString() }}{% endif %}"
-  data-current-username="{% if user %}{{ user.username }}{% endif %}"
   data-page-revision-id="{% if revision %}{{ revision._id.toString() }}{% endif %}"
   data-page-revision-created="{% if revision %}{{ revision.createdAt|datetz('U') }}{% endif %}"
   data-page-is-seen="{% if page and page.isSeenUser(user) %}1{% else %}0{% endif %}"
   data-linebreaks-enabled="{{ isEnabledLinebreaks() }}"
-  data-csrftoken="{{ csrf() }}"
   >
 
   {% if not page %}

+ 0 - 2
lib/views/page_list.html

@@ -77,12 +77,10 @@
   data-page-portal="{% if page and page.isPortal() %}1{% else %}0{% endif %}"
   data-page-id="{% if page %}{{ page._id.toString() }}{% endif %}"
   data-current-user="{% if user %}{{ user._id.toString() }}{% endif %}"
-  data-current-username="{% if user %}{{ user.username }}{% endif %}"
   data-page-revision-id="{% if revision %}{{ revision._id.toString() }}{% endif %}"
   data-page-revision-created="{% if revision %}{{ revision.createdAt|datetz('U') }}{% endif %}"
   data-page-is-seen="{% if page and page.isSeenUser(user) %}1{% else %}0{% endif %}"
   data-linebreaks-enabled="{{ isEnabledLinebreaks() }}"
-  data-csrftoken="{{ csrf() }}"
   >
 
 <div class="portal {% if not page or req.query.offset > 0 %}hide{% endif %}">

+ 2 - 2
resource/js/app.js

@@ -39,8 +39,8 @@ if (mainContent !== null) {
 
 // FIXME
 const crowi = new Crowi({
-  me: $('#content-main').data('current-username'),
-  csrfToken: $('#content-main').data('csrftoken'),
+  me: $('body').data('current-username'),
+  csrfToken: $('body').data('csrftoken'),
 }, window);
 window.crowi = crowi;
 crowi.setConfig(JSON.parse(document.getElementById('crowi-context-hydrate').textContent || '{}'));

+ 2 - 0
resource/js/components/PageListSearch.js

@@ -151,6 +151,7 @@ export default class PageListSearch extends React.Component {
           searchingKeyword={this.state.searchingKeyword}
           searchResultMeta={this.state.searchResultMeta}
           searchError={this.state.searchError}
+          crowi={this.props.crowi}
           />
       </div>
     );
@@ -159,6 +160,7 @@ export default class PageListSearch extends React.Component {
 
 PageListSearch.propTypes = {
   query: PropTypes.object,
+  crowi: PropTypes.object.isRequired,
 };
 PageListSearch.defaultProps = {
   //pollInterval: 1000,

+ 2 - 0
resource/js/components/SearchPage.js

@@ -104,6 +104,7 @@ export default class SearchPage extends React.Component {
           pages={this.state.searchedPages}
           searchingKeyword={this.state.searchingKeyword}
           searchResultMeta={this.state.searchResultMeta}
+          crowi={this.props.crowi}
           />
       </div>
     );
@@ -112,6 +113,7 @@ export default class SearchPage extends React.Component {
 
 SearchPage.propTypes = {
   query: PropTypes.object,
+  crowi: PropTypes.object.isRequired,
 };
 SearchPage.defaultProps = {
   //pollInterval: 1000,

+ 61 - 0
resource/js/components/SearchPage/DeletePageListModal.js

@@ -0,0 +1,61 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+
+import { Button, Modal } from 'react-bootstrap';
+import moment from 'moment';
+
+import ReactUtils from '../ReactUtils';
+
+export default class DeletePageListModal extends React.Component {
+
+  /*
+   * the threshold for omitting body
+   */
+  static get OMIT_BODY_THRES() { return 400 };
+
+  constructor(props) {
+    super(props);
+  }
+
+  componentWillMount() {
+  }
+
+  render() {
+    if (this.props.pages === undefined || this.props.pages.length == 0) {
+      return <div></div>
+    }
+
+    const listView = this.props.pages.map((page) => {
+      return (
+        <li key={page._id}>{page.path}</li>
+      );
+    });
+
+    return (
+      <Modal show={this.props.isShown} onHide={this.props.cancel} className="page-list-delete-modal">
+        <Modal.Header closeButton>
+          <Modal.Title>Deleting pages:</Modal.Title>
+        </Modal.Header>
+        <Modal.Body>
+          <ul>
+            {listView}
+          </ul>
+        </Modal.Body>
+        <Modal.Footer>
+          <span className="text-danger">{this.props.errorMessage}</span>&nbsp;
+          <Button onClick={this.props.cancel}>Cancel</Button>
+          <Button onClick={this.props.confirmedToDelete} className="btn-danger">Delete</Button>
+        </Modal.Footer>
+      </Modal>
+    );
+  }
+
+}
+
+DeletePageListModal.propTypes = {
+  isShown: PropTypes.bool.isRequired,
+  pages: PropTypes.array,
+  errorMessage: PropTypes.string,
+  cancel: PropTypes.func.isRequired,            // for cancel evnet handling
+  confirmedToDelete: PropTypes.func.isRequired, // for confirmed event handling
+};

+ 124 - 1
resource/js/components/SearchPage/SearchResult.js

@@ -3,9 +3,21 @@ import PropTypes from 'prop-types';
 
 import Page from '../PageList/Page';
 import SearchResultList from './SearchResultList';
+import DeletePageListModal from './DeletePageListModal';
 
 // Search.SearchResult
 export default class SearchResult extends React.Component {
+  constructor(props) {
+    super(props);
+    this.state = {
+      deletionMode : false,
+      selectedPages : new Set(),
+      isDeleteConfirmModalShown: false,
+      errorMessageForDeleting: undefined,
+    }
+    this.deleteSelectedPages = this.deleteSelectedPages.bind(this);
+    this.closeDeleteConfirmModal = this.closeDeleteConfirmModal.bind(this);
+  }
 
   isNotSearchedYet() {
     return !this.props.searchResultMeta.took;
@@ -22,6 +34,84 @@ export default class SearchResult extends React.Component {
     return false;
   }
 
+  /**
+   * toggle checkbox and add (or delete from) selected pages list
+   *
+   * @param {any} page
+   * @memberof SearchResult
+   */
+  toggleCheckbox(page) {
+    if (this.state.selectedPages.has(page)) {
+      this.state.selectedPages.delete(page);
+    } else {
+      this.state.selectedPages.add(page);
+    }
+    this.setState({isDeleteConfirmModalShown: false});
+    this.setState({selectedPages: this.state.selectedPages});
+  }
+
+  /**
+   * change deletion mode
+   *
+   * @memberof SearchResult
+   */
+  handleDeletionModeChange() {
+    this.state.selectedPages.clear();
+    this.setState({deletionMode: !this.state.deletionMode});
+  }
+
+  /**
+   * delete selected pages
+   *
+   * @memberof SearchResult
+   */
+  deleteSelectedPages() {
+    let isDeleteComplete = true;
+    Array.from(this.state.selectedPages).map((page) => {
+      const pageId = page._id;
+      const revisionId = page.revision._id;
+      this.props.crowi.apiPost('/pages.remove',
+        {page_id: pageId, revision_id: revisionId})
+      .then(res => {
+        if (res.ok) {
+          this.state.selectedPages.delete(page);
+        }
+        else {
+          isDeleteComplete = false;
+        }
+      }).catch(err => {
+        console.log(err.message);
+        isDeleteComplete = false;
+        this.setState({errorMessageForDeleting: err.message});
+      });
+    });
+
+    if ( isDeleteComplete ) {
+      window.location.reload();
+    }
+  }
+
+  /**
+   * open confirm modal for page selection delete
+   *
+   * @memberof SearchResult
+   */
+  showDeleteConfirmModal() {
+    this.setState({isDeleteConfirmModalShown: true});
+  }
+
+  /**
+   * close confirm modal for page selection delete
+   *
+   * @memberof SearchResult
+   */
+  closeDeleteConfirmModal() {
+    this.setState({
+      isDeleteConfirmModalShown: false,
+      errorMessageForDeleting: undefined,
+    });
+  }
+
   render() {
     const excludePathString = this.props.tree;
 
@@ -52,6 +142,22 @@ export default class SearchResult extends React.Component {
 
     }
 
+    let deletionModeButtons = '';
+
+    if (this.state.deletionMode) {
+      deletionModeButtons =
+      <div className="btn-group">
+        <button type="button" className="btn btn-danger" onClick={() => this.showDeleteConfirmModal()} disabled={this.state.selectedPages.size == 0}><i className="fa fa-trash-o"/> Delete</button>
+        <button type="button" className="btn btn-default" onClick={() => this.handleDeletionModeChange()}><i className="fa fa-undo"/> Cancel</button>
+      </div>
+    }
+    else {
+      deletionModeButtons =
+      <div className="btn-group">
+        <button type="button" className="btn btn-default" onClick={() => this.handleDeletionModeChange()}><i className="fa fa-trash-o"/> DeletionMode</button>
+      </div>
+    }
+
     const listView = this.props.pages.map((page) => {
       const pageId = "#" + page._id;
       return (
@@ -60,6 +166,13 @@ export default class SearchResult extends React.Component {
           key={page._id}
           excludePathString={excludePathString}
           >
+          { this.state.deletionMode &&
+            <input
+              type="checkbox"
+              value={pageId}
+              checked={this.state.selectedPages.has(page)}
+              onClick={() => this.toggleCheckbox(page)} />
+            }
           <div className="page-list-option">
             <a href={page.path}><i className="fa fa-arrow-circle-right" /></a>
           </div>
@@ -81,6 +194,7 @@ export default class SearchResult extends React.Component {
         <div className="search-result row" id="search-result">
           <div className="col-md-4 hidden-xs hidden-sm page-list search-result-list" id="search-result-list">
             <nav data-spy="affix" data-offset-top="120">
+              {deletionModeButtons}
               <ul className="page-list-ul page-list-ul-flat nav">
                 {listView}
               </ul>
@@ -94,7 +208,15 @@ export default class SearchResult extends React.Component {
               />
           </div>
         </div>
-      </div>
+        <DeletePageListModal
+          isShown={this.state.isDeleteConfirmModalShown}
+          pages={Array.from(this.state.selectedPages)}
+          errorMessage={this.state.errorMessageForDeleting}
+          cancel={this.closeDeleteConfirmModal}
+          confirmedToDelete={this.deleteSelectedPages}
+        />
+
+      </div>//content-main
     );
   }
 }
@@ -104,6 +226,7 @@ SearchResult.propTypes = {
   pages: PropTypes.array.isRequired,
   searchingKeyword: PropTypes.string.isRequired,
   searchResultMeta: PropTypes.object.isRequired,
+  crowi: PropTypes.object.isRequired,
 };
 SearchResult.defaultProps = {
   tree: '',