mirror of
https://github.com/discourse/discourse.git
synced 2024-11-29 16:15:14 +08:00
Support for "Select All / Deselect All" while selecting posts to merge / delete.
This commit is contained in:
parent
7daca77443
commit
a80ec535a3
|
@ -15,41 +15,48 @@ Discourse.TopicController = Discourse.ObjectController.extend(Discourse.Selected
|
|||
loadingBelow: false,
|
||||
loadingAbove: false,
|
||||
needs: ['header', 'modal', 'composer', 'quoteButton'],
|
||||
allPostsSelected: false,
|
||||
selectedPosts: new Em.Set(),
|
||||
|
||||
selectedPosts: function() {
|
||||
var posts = this.get('content.posts');
|
||||
if (!posts) return null;
|
||||
return posts.filterProperty('selected');
|
||||
}.property('content.posts.@each.selected'),
|
||||
|
||||
canMoveSelected: function() {
|
||||
if (!this.get('content.can_move_posts')) return false;
|
||||
canMergeTopic: function() {
|
||||
if (!this.get('can_move_posts')) return false;
|
||||
return (this.get('selectedPostsCount') > 0);
|
||||
}.property('canDeleteSelected'),
|
||||
}.property('selectedPostsCount'),
|
||||
|
||||
canSplitTopic: function() {
|
||||
if (!this.get('can_move_posts')) return false;
|
||||
if (this.get('allPostsSelected')) return false;
|
||||
return (this.get('selectedPostsCount') > 0);
|
||||
}.property('selectedPostsCount'),
|
||||
|
||||
|
||||
canSelectAll: Em.computed.not('allPostsSelected'),
|
||||
|
||||
canDeselectAll: function () {
|
||||
if (this.get('selectedPostsCount') > 0) return true;
|
||||
if (this.get('allPostsSelected')) return true;
|
||||
}.property('selectedPostsCount', 'allPostsSelected'),
|
||||
|
||||
canDeleteSelected: function() {
|
||||
var selectedPosts = this.get('selectedPosts');
|
||||
|
||||
if (this.get('allPostsSelected')) return true;
|
||||
if (this.get('selectedPostsCount') === 0) return false;
|
||||
|
||||
var canDelete = true;
|
||||
selectedPosts.each(function(p) {
|
||||
selectedPosts.forEach(function(p) {
|
||||
if (!p.get('can_delete')) {
|
||||
canDelete = false;
|
||||
return false;
|
||||
}
|
||||
});
|
||||
return canDelete;
|
||||
}.property('selectedPosts'),
|
||||
}.property('selectedPostsCount'),
|
||||
|
||||
multiSelectChanged: function() {
|
||||
// Deselect all posts when multi select is turned off
|
||||
if (!this.get('multiSelect')) {
|
||||
var posts = this.get('content.posts');
|
||||
if (posts) {
|
||||
posts.forEach(function(p) {
|
||||
p.set('selected', false);
|
||||
});
|
||||
}
|
||||
this.deselectAll();
|
||||
}
|
||||
}.observes('multiSelect'),
|
||||
|
||||
|
@ -61,7 +68,32 @@ Discourse.TopicController = Discourse.ObjectController.extend(Discourse.Selected
|
|||
}.property('content.loaded', 'currentPost', 'content.filtered_posts_count'),
|
||||
|
||||
selectPost: function(post) {
|
||||
post.toggleProperty('selected');
|
||||
var selectedPosts = this.get('selectedPosts');
|
||||
if (selectedPosts.contains(post)) {
|
||||
selectedPosts.removeObject(post);
|
||||
this.set('allPostsSelected', false);
|
||||
} else {
|
||||
selectedPosts.addObject(post);
|
||||
|
||||
// If the user manually selects all posts, all posts are selected
|
||||
if (selectedPosts.length === this.get('posts_count')) {
|
||||
this.set('allPostsSelected');
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
selectAll: function() {
|
||||
var posts = this.get('posts');
|
||||
var selectedPosts = this.get('selectedPosts');
|
||||
if (posts) {
|
||||
selectedPosts.addObjects(posts);
|
||||
}
|
||||
this.set('allPostsSelected', true);
|
||||
},
|
||||
|
||||
deselectAll: function() {
|
||||
this.get('selectedPosts').clear();
|
||||
this.set('allPostsSelected', false);
|
||||
},
|
||||
|
||||
toggleMultiSelect: function() {
|
||||
|
@ -90,14 +122,21 @@ Discourse.TopicController = Discourse.ObjectController.extend(Discourse.Selected
|
|||
modalController.show(Discourse.MergeTopicView.create({
|
||||
topicController: this,
|
||||
topic: this.get('content'),
|
||||
allPostsSelected: this.get('allPostsSelected'),
|
||||
selectedPosts: this.get('selectedPosts')
|
||||
}));
|
||||
},
|
||||
|
||||
deleteSelected: function() {
|
||||
var topicController = this;
|
||||
return bootbox.confirm(Em.String.i18n("post.delete.confirm", { count: this.get('selectedPostsCount')}), function(result) {
|
||||
bootbox.confirm(Em.String.i18n("post.delete.confirm", { count: this.get('selectedPostsCount')}), function(result) {
|
||||
if (result) {
|
||||
|
||||
// If all posts are selected, it's the same thing as deleting the topic
|
||||
if (topicController.get('allPostsSelected')) {
|
||||
return topicController.deleteTopic();
|
||||
}
|
||||
|
||||
var selectedPosts = topicController.get('selectedPosts');
|
||||
Discourse.Post.deleteMany(selectedPosts);
|
||||
topicController.get('content.posts').removeObjects(selectedPosts);
|
||||
|
@ -245,7 +284,7 @@ Discourse.TopicController = Discourse.ObjectController.extend(Discourse.Selected
|
|||
});
|
||||
}.observes('postFilters'),
|
||||
|
||||
deleteTopic: function(e) {
|
||||
deleteTopic: function() {
|
||||
var topicController = this;
|
||||
this.unsubscribe();
|
||||
this.get('content').destroy().then(function() {
|
||||
|
|
|
@ -10,8 +10,11 @@ Discourse.SelectedPostsCount = Em.Mixin.create({
|
|||
|
||||
selectedPostsCount: function() {
|
||||
if (!this.get('selectedPosts')) return 0;
|
||||
return this.get('selectedPosts').length;
|
||||
}.property('selectedPosts')
|
||||
|
||||
if (this.get('allPostsSelected')) return this.get('posts_count') || this.get('topic.posts_count');
|
||||
|
||||
return this.get('selectedPosts.length');
|
||||
}.property('selectedPosts.length', 'allPostsSelected')
|
||||
|
||||
});
|
||||
|
||||
|
|
|
@ -449,6 +449,17 @@ Discourse.Topic.reopenClass({
|
|||
});
|
||||
},
|
||||
|
||||
mergeTopic: function(topicId, destinationTopicId) {
|
||||
var promise = Discourse.ajax("/t/" + topicId + "/merge-topic", {
|
||||
type: 'POST',
|
||||
data: {destination_topic_id: destinationTopicId}
|
||||
}).then(function (result) {
|
||||
if (result.success) return result;
|
||||
promise.reject();
|
||||
});
|
||||
return promise;
|
||||
},
|
||||
|
||||
movePosts: function(topicId, opts) {
|
||||
var promise = Discourse.ajax("/t/" + topicId + "/move-posts", {
|
||||
type: 'POST',
|
||||
|
|
|
@ -1,11 +1,21 @@
|
|||
<p>{{countI18n topic.multi_select.description countBinding="controller.selectedPostsCount"}}</p>
|
||||
|
||||
{{#if canSelectAll}}
|
||||
<p><a href='#' {{action selectAll}}>select all</a></p>
|
||||
{{/if}}
|
||||
|
||||
{{#if canDeselectAll}}
|
||||
<p><a href='#' {{action deselectAll}}>deselect all</a></p>
|
||||
{{/if}}
|
||||
|
||||
{{#if canDeleteSelected}}
|
||||
<button class='btn' {{action deleteSelected}}><i class='icon icon-trash'></i> {{i18n topic.multi_select.delete}}</button>
|
||||
{{/if}}
|
||||
|
||||
{{#if canMoveSelected}}
|
||||
{{#if canSplitTopic}}
|
||||
<button class='btn' {{action splitTopic}}><i class='icon icon-move'></i> {{i18n topic.split_topic.action}}</button>
|
||||
{{/if}}
|
||||
{{#if canMergeTopic}}
|
||||
<button class='btn' {{action mergeTopic}}><i class='icon icon-move'></i> {{i18n topic.merge_topic.action}}}</button>
|
||||
{{/if}}
|
||||
|
||||
|
|
|
@ -23,13 +23,20 @@ Discourse.MergeTopicView = Discourse.ModalBodyView.extend(Discourse.SelectedPost
|
|||
movePostsToExistingTopic: function() {
|
||||
this.set('saving', true);
|
||||
|
||||
var postIds = this.get('selectedPosts').map(function(p) { return p.get('id'); });
|
||||
var moveSelectedView = this;
|
||||
|
||||
Discourse.Topic.movePosts(this.get('topic.id'), {
|
||||
destination_topic_id: this.get('selectedTopicId'),
|
||||
post_ids: postIds
|
||||
}).then(function(result) {
|
||||
var promise = null;
|
||||
if (this.get('allPostsSelected')) {
|
||||
promise = Discourse.Topic.mergeTopic(this.get('topic.id'), this.get('selectedTopicId'));
|
||||
} else {
|
||||
var postIds = this.get('selectedPosts').map(function(p) { return p.get('id'); });
|
||||
promise = Discourse.Topic.movePosts(this.get('topic.id'), {
|
||||
destination_topic_id: this.get('selectedTopicId'),
|
||||
post_ids: postIds
|
||||
});
|
||||
}
|
||||
|
||||
promise.then(function(result) {
|
||||
// Posts moved
|
||||
$('#discourse-modal').modal('hide');
|
||||
moveSelectedView.get('topicController').toggleMultiSelect();
|
||||
|
|
|
@ -11,14 +11,14 @@ Discourse.PostView = Discourse.View.extend({
|
|||
templateName: 'post',
|
||||
classNameBindings: ['post.lastPost',
|
||||
'postTypeClass',
|
||||
'post.selected',
|
||||
'selected',
|
||||
'post.hidden:hidden',
|
||||
'post.deleted_at:deleted',
|
||||
'parentPost:replies-above'],
|
||||
postBinding: 'content',
|
||||
|
||||
// TODO really we should do something cleaner here... this makes it work in debug but feels really messy
|
||||
screenTrack: (function() {
|
||||
screenTrack: function() {
|
||||
var parentView = this.get('parentView');
|
||||
var screenTrack = null;
|
||||
while (parentView && !screenTrack) {
|
||||
|
@ -26,17 +26,17 @@ Discourse.PostView = Discourse.View.extend({
|
|||
parentView = parentView.get('parentView');
|
||||
}
|
||||
return screenTrack;
|
||||
}).property('parentView'),
|
||||
}.property('parentView'),
|
||||
|
||||
postTypeClass: (function() {
|
||||
postTypeClass: function() {
|
||||
return this.get('post.post_type') === Discourse.get('site.post_types.moderator_action') ? 'moderator' : 'regular';
|
||||
}).property('post.post_type'),
|
||||
}.property('post.post_type'),
|
||||
|
||||
// If the cooked content changed, add the quote controls
|
||||
cookedChanged: (function() {
|
||||
cookedChanged: function() {
|
||||
var postView = this;
|
||||
Em.run.next(function() { postView.insertQuoteControls(); });
|
||||
}).observes('post.cooked'),
|
||||
}.observes('post.cooked'),
|
||||
|
||||
init: function() {
|
||||
this._super();
|
||||
|
@ -45,13 +45,19 @@ Discourse.PostView = Discourse.View.extend({
|
|||
|
||||
mouseUp: function(e) {
|
||||
if (this.get('controller.multiSelect') && (e.metaKey || e.ctrlKey)) {
|
||||
this.toggleProperty('post.selected');
|
||||
this.get('controller').selectPost(this.get('post'));
|
||||
}
|
||||
},
|
||||
|
||||
selected: function() {
|
||||
var selectedPosts = this.get('controller.selectedPosts');
|
||||
if (!selectedPosts) return false;
|
||||
return selectedPosts.contains(this.get('post'));
|
||||
}.property('controller.selectedPostsCount'),
|
||||
|
||||
selectText: function() {
|
||||
return this.get('post.selected') ? Em.String.i18n('topic.multi_select.selected', { count: this.get('controller.selectedPostsCount') }) : Em.String.i18n('topic.multi_select.select');
|
||||
}.property('post.selected', 'controller.selectedPostsCount'),
|
||||
return this.get('selected') ? Em.String.i18n('topic.multi_select.selected', { count: this.get('controller.selectedPostsCount') }) : Em.String.i18n('topic.multi_select.select');
|
||||
}.property('selected', 'controller.selectedPostsCount'),
|
||||
|
||||
repliesHidden: function() {
|
||||
return !this.get('repliesShown');
|
||||
|
|
|
@ -15,6 +15,7 @@ class TopicsController < ApplicationController
|
|||
:unmute,
|
||||
:set_notifications,
|
||||
:move_posts,
|
||||
:merge_topic,
|
||||
:clear_pin,
|
||||
:autoclose]
|
||||
|
||||
|
@ -137,6 +138,20 @@ class TopicsController < ApplicationController
|
|||
render json: success_json
|
||||
end
|
||||
|
||||
def merge_topic
|
||||
requires_parameters(:destination_topic_id)
|
||||
|
||||
topic = Topic.where(id: params[:topic_id]).first
|
||||
guardian.ensure_can_move_posts!(topic)
|
||||
|
||||
dest_topic = topic.move_posts(current_user, topic.posts.pluck(:id), destination_topic_id: params[:destination_topic_id].to_i)
|
||||
if dest_topic.present?
|
||||
render json: {success: true, url: dest_topic.relative_url}
|
||||
else
|
||||
render json: {success: false}
|
||||
end
|
||||
end
|
||||
|
||||
def move_posts
|
||||
requires_parameters(:post_ids)
|
||||
|
||||
|
@ -153,7 +168,6 @@ class TopicsController < ApplicationController
|
|||
else
|
||||
render json: {success: false}
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
def clear_pin
|
||||
|
|
|
@ -212,6 +212,7 @@ Discourse::Application.routes.draw do
|
|||
post 't/:topic_id/timings' => 'topics#timings', constraints: {topic_id: /\d+/}
|
||||
post 't/:topic_id/invite' => 'topics#invite', constraints: {topic_id: /\d+/}
|
||||
post 't/:topic_id/move-posts' => 'topics#move_posts', constraints: {topic_id: /\d+/}
|
||||
post 't/:topic_id/merge-topic' => 'topics#merge_topic', constraints: {topic_id: /\d+/}
|
||||
delete 't/:topic_id/timings' => 'topics#destroy_timings', constraints: {topic_id: /\d+/}
|
||||
|
||||
post 't/:topic_id/notifications' => 'topics#set_notifications' , constraints: {topic_id: /\d+/}
|
||||
|
|
|
@ -2,6 +2,8 @@ require 'spec_helper'
|
|||
|
||||
describe TopicsController do
|
||||
|
||||
|
||||
|
||||
context 'move_posts' do
|
||||
it 'needs you to be logged in' do
|
||||
lambda { xhr :post, :move_posts, topic_id: 111, title: 'blah', post_ids: [1,2,3] }.should raise_error(Discourse::NotLoggedIn)
|
||||
|
@ -95,6 +97,49 @@ describe TopicsController do
|
|||
end
|
||||
end
|
||||
|
||||
context "merge_topic" do
|
||||
it 'needs you to be logged in' do
|
||||
lambda { xhr :post, :merge_topic, topic_id: 111, destination_topic_id: 345 }.should raise_error(Discourse::NotLoggedIn)
|
||||
end
|
||||
|
||||
describe 'moving to a new topic' do
|
||||
let!(:user) { log_in(:moderator) }
|
||||
let(:p1) { Fabricate(:post, user: user) }
|
||||
let(:topic) { p1.topic }
|
||||
|
||||
it "raises an error without destination_topic_id" do
|
||||
lambda { xhr :post, :merge_topic, topic_id: topic.id }.should raise_error(Discourse::InvalidParameters)
|
||||
end
|
||||
|
||||
it "raises an error when the user doesn't have permission to merge" do
|
||||
Guardian.any_instance.expects(:can_move_posts?).returns(false)
|
||||
xhr :post, :merge_topic, topic_id: 111, destination_topic_id: 345
|
||||
response.should be_forbidden
|
||||
end
|
||||
|
||||
let(:dest_topic) { Fabricate(:topic) }
|
||||
|
||||
context 'moves all the posts to the destination topic' do
|
||||
let(:p2) { Fabricate(:post, user: user) }
|
||||
|
||||
before do
|
||||
Topic.any_instance.expects(:move_posts).with(user, [p1.id], destination_topic_id: dest_topic.id).returns(topic)
|
||||
xhr :post, :merge_topic, topic_id: topic.id, destination_topic_id: dest_topic.id
|
||||
end
|
||||
|
||||
it "returns success" do
|
||||
response.should be_success
|
||||
result = ::JSON.parse(response.body)
|
||||
result['success'].should be_true
|
||||
result['url'].should be_present
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
context 'similar_to' do
|
||||
|
||||
let(:title) { 'this title is long enough to search for' }
|
||||
|
|
Loading…
Reference in New Issue
Block a user