FEATURE: move posts to new/existing PM (#6802)

This commit is contained in:
Arpit Jalan 2018-12-31 17:17:22 +05:30 committed by GitHub
parent b478984f60
commit 70fdc10365
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 1357 additions and 691 deletions

View File

@ -0,0 +1,64 @@
import debounce from "discourse/lib/debounce";
import { searchForTerm } from "discourse/lib/search";
import { observes } from "ember-addons/ember-computed-decorators";
export default Ember.Component.extend({
loading: null,
noResults: null,
messages: null,
@observes("messageTitle")
messageTitleChanged() {
this.setProperties({
loading: true,
noResults: true,
selectedTopicId: null
});
this.search(this.get("messageTitle"));
},
@observes("messages")
messagesChanged() {
const messages = this.get("messages");
if (messages) {
this.set("noResults", messages.length === 0);
}
this.set("loading", false);
},
search: debounce(function(title) {
const currentTopicId = this.get("currentTopicId");
if (Em.isEmpty(title)) {
this.setProperties({ messages: null, loading: false });
return;
}
searchForTerm(title, {
typeFilter: "private_messages",
searchForId: true
}).then(results => {
if (results && results.posts && results.posts.length > 0) {
this.set(
"messages",
results.posts
.mapBy("topic")
.filter(t => t.get("id") !== currentTopicId)
);
} else {
this.setProperties({ messages: null, loading: false });
}
});
}, 300),
actions: {
chooseMessage(message) {
const messageId = Em.get(message, "id");
this.set("selectedTopicId", messageId);
Ember.run.next(() =>
$(`#choose-message-${messageId}`).prop("checked", "true")
);
return false;
}
}
});

View File

@ -1,59 +0,0 @@
import ModalFunctionality from "discourse/mixins/modal-functionality";
import { movePosts, mergeTopic } from "discourse/models/topic";
import DiscourseURL from "discourse/lib/url";
import computed from "ember-addons/ember-computed-decorators";
export default Ember.Controller.extend(ModalFunctionality, {
topicController: Ember.inject.controller("topic"),
saving: false,
selectedTopicId: null,
selectedPostsCount: Ember.computed.alias(
"topicController.selectedPostsCount"
),
@computed("saving", "selectedTopicId")
buttonDisabled(saving, selectedTopicId) {
return saving || Ember.isEmpty(selectedTopicId);
},
@computed("saving")
buttonTitle(saving) {
return saving ? I18n.t("saving") : I18n.t("topic.merge_topic.title");
},
onShow() {
this.set("modal.modalClass", "split-modal");
},
actions: {
movePostsToExistingTopic() {
const topicId = this.get("model.id");
this.set("saving", true);
let promise = this.get("topicController.selectedAllPosts")
? mergeTopic(topicId, this.get("selectedTopicId"))
: movePosts(topicId, {
destination_topic_id: this.get("selectedTopicId"),
post_ids: this.get("topicController.selectedPostIds")
});
promise
.then(result => {
this.send("closeModal");
this.get("topicController").send("toggleMultiSelect");
Ember.run.next(() => DiscourseURL.routeTo(result.url));
})
.catch(() => {
this.flash(I18n.t("topic.merge_topic.error"));
})
.finally(() => {
this.set("saving", false);
});
return false;
}
}
});

View File

@ -0,0 +1,155 @@
import ModalFunctionality from "discourse/mixins/modal-functionality";
import { movePosts, mergeTopic } from "discourse/models/topic";
import DiscourseURL from "discourse/lib/url";
import { default as computed } from "ember-addons/ember-computed-decorators";
import { extractError } from "discourse/lib/ajax-error";
export default Ember.Controller.extend(ModalFunctionality, {
topicName: null,
saving: false,
categoryId: null,
tags: null,
canAddTags: Ember.computed.alias("site.can_create_tag"),
canTagMessages: Ember.computed.alias("site.can_tag_pms"),
selectedTopicId: null,
newTopic: Ember.computed.equal("selection", "new_topic"),
existingTopic: Ember.computed.equal("selection", "existing_topic"),
newMessage: Ember.computed.equal("selection", "new_message"),
existingMessage: Ember.computed.equal("selection", "existing_message"),
moveTypes: ["newTopic", "existingTopic", "newMessage", "existingMessage"],
participants: null,
topicController: Ember.inject.controller("topic"),
selectedPostsCount: Ember.computed.alias(
"topicController.selectedPostsCount"
),
selectedAllPosts: Ember.computed.alias("topicController.selectedAllPosts"),
selectedPosts: Ember.computed.alias("topicController.selectedPosts"),
@computed("saving", "selectedTopicId", "topicName")
buttonDisabled(saving, selectedTopicId, topicName) {
return (
saving || (Ember.isEmpty(selectedTopicId) && Ember.isEmpty(topicName))
);
},
@computed(
"saving",
"newTopic",
"existingTopic",
"newMessage",
"existingMessage"
)
buttonTitle(saving, newTopic, existingTopic, newMessage, existingMessage) {
if (newTopic) {
return I18n.t("topic.split_topic.title");
} else if (existingTopic) {
return I18n.t("topic.merge_topic.title");
} else if (newMessage) {
return I18n.t("topic.move_to_new_message.title");
} else if (existingMessage) {
return I18n.t("topic.move_to_existing_message.title");
} else {
return I18n.t("saving");
}
},
onShow() {
this.setProperties({
"modal.modalClass": "move-to-modal",
saving: false,
selection: "new_topic",
categoryId: null,
topicName: "",
tags: null,
participants: null
});
const isPrivateMessage = this.get("model.isPrivateMessage");
const canSplitTopic = this.get("canSplitTopic");
if (isPrivateMessage) {
this.set("selection", canSplitTopic ? "new_message" : "existing_message");
} else if (!canSplitTopic) {
this.set("selection", "existing_topic");
}
},
@computed("selectedAllPosts", "selectedPosts", "selectedPosts.[]")
canSplitTopic(selectedAllPosts, selectedPosts) {
return (
!selectedAllPosts &&
selectedPosts.length > 0 &&
selectedPosts.sort((a, b) => a.post_number - b.post_number)[0]
.post_type === this.site.get("post_types.regular")
);
},
actions: {
performMove() {
this.get("moveTypes").forEach(type => {
if (this.get(type)) {
this.send("movePostsTo", type);
}
});
},
movePostsTo(type) {
this.set("saving", true);
const topicId = this.get("model.id");
let mergeOptions, moveOptions;
if (type === "existingTopic") {
mergeOptions = { destination_topic_id: this.get("selectedTopicId") };
moveOptions = Object.assign(
{ post_ids: this.get("topicController.selectedPostIds") },
mergeOptions
);
} else if (type === "existingMessage") {
mergeOptions = {
destination_topic_id: this.get("selectedTopicId"),
participants: this.get("participants"),
archetype: "private_message"
};
moveOptions = Object.assign(
{ post_ids: this.get("topicController.selectedPostIds") },
mergeOptions
);
} else if (type === "newTopic") {
mergeOptions = {};
moveOptions = {
title: this.get("topicName"),
post_ids: this.get("topicController.selectedPostIds"),
category_id: this.get("categoryId"),
tags: this.get("tags")
};
} else {
mergeOptions = {};
moveOptions = {
title: this.get("topicName"),
post_ids: this.get("topicController.selectedPostIds"),
tags: this.get("tags"),
archetype: "private_message"
};
}
const promise = this.get("topicController.selectedAllPosts")
? mergeTopic(topicId, mergeOptions)
: movePosts(topicId, moveOptions);
promise
.then(result => {
this.send("closeModal");
this.get("topicController").send("toggleMultiSelect");
DiscourseURL.routeTo(result.url);
})
.catch(xhr => {
this.flash(extractError(xhr, I18n.t("topic.move_to.error")));
})
.finally(() => {
this.set("saving", false);
});
return false;
}
}
});

View File

@ -1,66 +0,0 @@
import ModalFunctionality from "discourse/mixins/modal-functionality";
import { extractError } from "discourse/lib/ajax-error";
import { movePosts } from "discourse/models/topic";
import DiscourseURL from "discourse/lib/url";
import { default as computed } from "ember-addons/ember-computed-decorators";
export default Ember.Controller.extend(ModalFunctionality, {
topicName: null,
saving: false,
categoryId: null,
tags: null,
canAddTags: Ember.computed.alias("site.can_create_tag"),
topicController: Ember.inject.controller("topic"),
selectedPostsCount: Ember.computed.alias(
"topicController.selectedPostsCount"
),
@computed("saving", "topicName")
buttonDisabled(saving, topicName) {
return saving || Ember.isEmpty(topicName);
},
@computed("saving")
buttonTitle(saving) {
return saving ? I18n.t("saving") : I18n.t("topic.split_topic.action");
},
onShow() {
this.setProperties({
"modal.modalClass": "split-modal",
saving: false,
categoryId: null,
topicName: "",
tags: null
});
},
actions: {
movePostsToNewTopic() {
this.set("saving", true);
const options = {
title: this.get("topicName"),
post_ids: this.get("topicController.selectedPostIds"),
category_id: this.get("categoryId"),
tags: this.get("tags")
};
movePosts(this.get("model.id"), options)
.then(result => {
this.send("closeModal");
this.get("topicController").send("toggleMultiSelect");
Ember.run.next(() => DiscourseURL.routeTo(result.url));
})
.catch(xhr => {
this.flash(extractError(xhr, I18n.t("topic.split_topic.error")));
})
.finally(() => {
this.set("saving", false);
});
return false;
}
}
});

View File

@ -1113,22 +1113,6 @@ export default Ember.Controller.extend(BufferedContent, {
);
},
@computed(
"canMergeTopic",
"selectedAllPosts",
"selectedPosts",
"selectedPosts.[]"
)
canSplitTopic(canMergeTopic, selectedAllPosts, selectedPosts) {
return (
canMergeTopic &&
!selectedAllPosts &&
selectedPosts.length > 0 &&
selectedPosts.sort((a, b) => a.post_number - b.post_number)[0]
.post_type === 1
);
},
@computed("model.details.can_move_posts", "selectedPostsCount")
canMergeTopic(canMovePosts, selectedPostsCount) {
return canMovePosts && selectedPostsCount > 0;

View File

@ -752,11 +752,10 @@ export function movePosts(topicId, data) {
);
}
export function mergeTopic(topicId, destinationTopicId) {
return ajax("/t/" + topicId + "/merge-topic", {
type: "POST",
data: { destination_topic_id: destinationTopicId }
}).then(moveResult);
export function mergeTopic(topicId, data) {
return ajax("/t/" + topicId + "/merge-topic", { type: "POST", data }).then(
moveResult
);
}
export default Topic;

View File

@ -114,17 +114,13 @@ const TopicRoute = Discourse.Route.extend({
this.controllerFor("raw_email").loadRawEmail(model.get("id"));
},
mergeTopic() {
showModal("merge-topic", {
moveToTopic() {
showModal("move-to-topic", {
model: this.modelFor("topic"),
title: "topic.merge_topic.title"
title: "topic.move_to.title"
});
},
splitTopic() {
showModal("split-topic", { model: this.modelFor("topic") });
},
changeOwner() {
showModal("change-owner", {
model: this.modelFor("topic"),

View File

@ -0,0 +1,22 @@
<label for='choose-message-title'>{{i18n 'choose_message.title.search'}}</label>
{{text-field value=messageTitle placeholderKey="choose_message.title.placeholder" id="choose-message-title"}}
{{#if loading}}
<p>{{i18n 'loading'}}</p>
{{else}}
{{#if noResults}}
<p>{{i18n 'choose_message.none_found'}}</p>
{{else}}
{{#each messages as |m|}}
<div class='controls existing-message'>
<label class='radio'>
<input type='radio' id="choose-message-{{unbound m.id}}" name='choose_message_id' {{action "chooseMessage" m}}>
<span class="message-title">
{{m.title}}
</span>
</label>
</div>
{{/each}}
{{/if}}
{{/if}}

View File

@ -1,13 +0,0 @@
{{#d-modal-body id='move-selected'}}
<p>{{{i18n 'topic.merge_topic.instructions' count=selectedPostsCount}}}</p>
<form>
{{choose-topic currentTopicId=model.id selectedTopicId=selectedTopicId}}
</form>
{{/d-modal-body}}
<div class="modal-footer">
{{#d-button class="btn-primary" disabled=buttonDisabled action="movePostsToExistingTopic"}}
{{d-icon 'sign-out'}} {{buttonTitle}}
{{/d-button}}
</div>

View File

@ -0,0 +1,112 @@
{{#d-modal-body id='move-selected'}}
{{#if model.isPrivateMessage}}
<div class="radios">
{{#if canSplitTopic}}
<label class="radio-label" for="move-to-new-message">
{{radio-button id='move-to-new-message' name="move-to-entity" value="new_message" selection=selection}}
<b>{{i18n 'topic.move_to_new_message.radio_label'}}</b>
</label>
{{/if}}
<label class="radio-label" for="move-to-existing-message">
{{radio-button id='move-to-existing-message' name="move-to-entity" value="existing_message" selection=selection}}
<b>{{i18n 'topic.move_to_existing_message.radio_label'}}</b>
</label>
</div>
{{#if canSplitTopic}}
{{#if newMessage}}
<p>{{{i18n 'topic.move_to_new_message.instructions' count=selectedPostsCount}}}</p>
<form>
<label>{{i18n 'topic.move_to_new_message.message_title'}}</label>
{{text-field value=topicName placeholderKey="composer.title_placeholder" elementId='split-topic-name'}}
{{#if canTagMessages}}
<label>{{i18n 'tagging.tags'}}</label>
{{tag-chooser tags=tags filterable=true}}
{{/if}}
</form>
{{/if}}
{{/if}}
{{#if existingMessage}}
<p>{{{i18n 'topic.move_to_existing_message.instructions' count=selectedPostsCount}}}</p>
<form>
{{choose-message currentTopicId=model.id selectedTopicId=selectedTopicId}}
<label>{{i18n 'topic.move_to_new_message.participants'}}</label>
{{user-selector usernames=participants class="participant-selector"}}
</form>
{{/if}}
{{else}}
<div class="radios">
{{#if canSplitTopic}}
<label class="radio-label" for="move-to-new-topic">
{{radio-button id='move-to-new-topic' name="move-to-entity" value="new_topic" selection=selection}}
<b>{{i18n 'topic.split_topic.radio_label'}}</b>
</label>
{{/if}}
<label class="radio-label" for="move-to-existing-topic">
{{radio-button id='move-to-existing-topic' name="move-to-entity" value="existing_topic" selection=selection}}
<b>{{i18n 'topic.merge_topic.radio_label'}}</b>
</label>
{{#if canSplitTopic}}
<label class="radio-label" for="move-to-new-message">
{{radio-button id='move-to-new-message' name="move-to-entity" value="new_message" selection=selection}}
<b>{{i18n 'topic.move_to_new_message.radio_label'}}</b>
</label>
{{/if}}
</div>
{{#if existingTopic}}
<p>{{{i18n 'topic.merge_topic.instructions' count=selectedPostsCount}}}</p>
<form>
{{choose-topic currentTopicId=model.id selectedTopicId=selectedTopicId}}
</form>
{{/if}}
{{#if canSplitTopic}}
{{#if newTopic}}
<p>{{{i18n 'topic.split_topic.instructions' count=selectedPostsCount}}}</p>
<form>
<label>{{i18n 'topic.split_topic.topic_name'}}</label>
{{text-field value=topicName placeholderKey="composer.title_placeholder" elementId='split-topic-name'}}
<label>{{i18n 'categories.category'}}</label>
{{category-chooser value=categoryId class="small"}}
{{#if canAddTags}}
<label>{{i18n 'tagging.tags'}}</label>
{{tag-chooser tags=tags filterable=true categoryId=categoryId}}
{{/if}}
</form>
{{/if}}
{{/if}}
{{#if canSplitTopic}}
{{#if newMessage}}
<p>{{{i18n 'topic.move_to_new_message.instructions' count=selectedPostsCount}}}</p>
<form>
<label>{{i18n 'topic.move_to_new_message.message_title'}}</label>
{{text-field value=topicName placeholderKey="composer.title_placeholder" elementId='split-topic-name'}}
{{#if canTagMessages}}
<label>{{i18n 'tagging.tags'}}</label>
{{tag-chooser tags=tags filterable=true}}
{{/if}}
</form>
{{/if}}
{{/if}}
{{/if}}
{{/d-modal-body}}
<div class="modal-footer">
{{#d-button class="btn-primary" disabled=buttonDisabled action=(action "performMove")}}
{{d-icon 'sign-out'}} {{buttonTitle}}
{{/d-button}}
</div>

View File

@ -1,21 +0,0 @@
{{#d-modal-body id="move-selected" title="topic.split_topic.title"}}
{{{i18n 'topic.split_topic.instructions' count=selectedPostsCount}}}
<form>
<label>{{i18n 'topic.split_topic.topic_name'}}</label>
{{text-field value=topicName placeholderKey="composer.title_placeholder" elementId='split-topic-name'}}
<label>{{i18n 'categories.category'}}</label>
{{category-chooser value=categoryId class="small"}}
{{#if canAddTags}}
<label>{{i18n 'tagging.tags'}}</label>
{{tag-chooser tags=tags filterable=true categoryId=categoryId}}
{{/if}}
</form>
{{/d-modal-body}}
<div class="modal-footer">
{{#d-button class="btn-primary" disabled=buttonDisabled action="movePostsToNewTopic"}}
{{d-icon 'sign-out'}} {{buttonTitle}}
{{/d-button}}
</div>

View File

@ -12,12 +12,8 @@
{{d-button action="deleteSelected" icon="trash-o" label="topic.multi_select.delete" class="btn-danger"}}
{{/if}}
{{#if canSplitTopic}}
{{d-button action="splitTopic" icon="sign-out" label="topic.split_topic.action"}}
{{/if}}
{{#if canMergeTopic}}
{{d-button action="mergeTopic" icon="sign-out" label="topic.merge_topic.action"}}
{{d-button action=(route-action "moveToTopic") icon="sign-out" label="topic.move_to.action" class="move-to-topic"}}
{{/if}}
{{#if canChangeOwner}}

View File

@ -113,19 +113,28 @@
}
}
.split-modal {
.move-to-modal {
.modal-body {
position: relative;
height: 350px;
}
#move-selected {
width: 475px;
p {
margin-top: 0;
}
input[type="radio"] {
margin-right: 10px;
.radios {
margin-bottom: 10px;
display: flex;
flex-direction: row;
.radio-label {
display: inline-block;
padding-right: 15px;
}
}
button {
@ -142,9 +151,19 @@
width: 95%;
margin-top: 20px;
#split-topic-name,
#choose-topic-title {
#choose-topic-title,
#choose-message-title {
width: 100%;
}
.participant-selector {
width: 100%;
}
div.ac-wrap {
width: 100%;
margin-bottom: 9px;
}
}
}
}

View File

@ -607,13 +607,26 @@ class TopicsController < ApplicationController
end
def merge_topic
params.require(:destination_topic_id)
topic_id = params.require(:topic_id)
destination_topic_id = params.require(:destination_topic_id)
params.permit(:participants)
params.permit(:archetype)
topic = Topic.find_by(id: params[:topic_id])
raise Discourse::InvalidAccess if params[:archetype] == "private_message" && !guardian.is_staff?
topic = Topic.find_by(id: topic_id)
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)
render_topic_changes(dest_topic)
args = {}
args[:destination_topic_id] = destination_topic_id.to_i
if params[:archetype].present?
args[:archetype] = params[:archetype]
args[:participants] = params[:participants] if params[:participants].present? && params[:archetype] == "private_message"
end
destination_topic = topic.move_posts(current_user, topic.posts.pluck(:id), args)
render_topic_changes(destination_topic)
end
def move_posts
@ -621,6 +634,10 @@ class TopicsController < ApplicationController
topic_id = params.require(:topic_id)
params.permit(:category_id)
params.permit(:tags)
params.permit(:participants)
params.permit(:archetype)
raise Discourse::InvalidAccess if params[:archetype] == "private_message" && !guardian.is_staff?
topic = Topic.with_deleted.find_by(id: topic_id)
guardian.ensure_can_move_posts!(topic)
@ -630,8 +647,8 @@ class TopicsController < ApplicationController
return render_json_error("When moving posts to a new topic, the first post must be a regular post.")
end
dest_topic = move_posts_to_destination(topic)
render_topic_changes(dest_topic)
destination_topic = move_posts_to_destination(topic)
render_topic_changes(destination_topic)
rescue ActiveRecord::RecordInvalid => ex
render_json_error(ex)
end
@ -893,9 +910,15 @@ class TopicsController < ApplicationController
args = {}
args[:title] = params[:title] if params[:title].present?
args[:destination_topic_id] = params[:destination_topic_id].to_i if params[:destination_topic_id].present?
args[:category_id] = params[:category_id].to_i if params[:category_id].present?
args[:tags] = params[:tags] if params[:tags].present?
if params[:archetype].present?
args[:archetype] = params[:archetype]
args[:participants] = params[:participants] if params[:participants].present? && params[:archetype] == "private_message"
else
args[:category_id] = params[:category_id].to_i if params[:category_id].present?
end
topic.move_posts(current_user, post_ids_including_replies, args)
end

View File

@ -5,18 +5,24 @@ class PostMover
@move_types ||= Enum.new(:new_topic, :existing_topic)
end
def initialize(original_topic, user, post_ids)
def initialize(original_topic, user, post_ids, move_to_pm: false)
@original_topic = original_topic
@user = user
@post_ids = post_ids
@move_to_pm = move_to_pm
end
def to_topic(id)
def to_topic(id, participants: nil)
@move_type = PostMover.move_types[:existing_topic]
topic = Topic.find_by_id(id)
raise Discourse::InvalidParameters unless topic.archetype == @original_topic.archetype
Topic.transaction do
move_posts_to Topic.find_by_id(id)
move_posts_to topic
end
add_allowed_users(participants) if participants.present? && @move_to_pm
topic
end
def to_new_topic(title, category_id = nil, tags = nil)
@ -24,13 +30,15 @@ class PostMover
post = Post.find_by(id: post_ids.first)
raise Discourse::InvalidParameters unless post
archetype = @move_to_pm ? Archetype.private_message : Archetype.default
Topic.transaction do
new_topic = Topic.create!(
user: post.user,
title: title,
category_id: category_id,
created_at: post.created_at
created_at: post.created_at,
archetype: archetype
)
DiscourseTagging.tag_topic_by_names(new_topic, Guardian.new(user), tags)
move_posts_to new_topic
@ -79,7 +87,11 @@ class PostMover
posts.each do |post|
post.is_first_post? ? create_first_post(post) : move(post)
if @move_to_pm
destination_topic.topic_allowed_users.build(user_id: post.user_id) unless destination_topic.topic_allowed_users.where(user_id: post.user_id).exists?
end
end
destination_topic.save! if @move_to_pm
PostReply.where("reply_id IN (:post_ids) OR post_id IN (:post_ids)", post_ids: post_ids).each do |post_reply|
if post_reply.post && post_reply.reply && post_reply.reply.topic_id != post_reply.post.topic_id
@ -189,6 +201,7 @@ class PostMover
I18n.t(
"move_posts.#{move_type_str}_moderator_post",
count: posts.length,
entity: @move_to_pm ? "message" : "topic",
topic_link: posts.first.is_first_post? ?
"[#{destination_topic.title}](#{destination_topic.relative_url})" :
"[#{destination_topic.title}](#{posts.first.url})"
@ -234,4 +247,14 @@ class PostMover
notifications_reason_id: TopicUser.notification_reasons[:created_topic]
)
end
def add_allowed_users(usernames)
return unless usernames.present?
names = usernames.split(',').flatten
User.where(username: names).find_each do |user|
destination_topic.topic_allowed_users.build(user_id: user.id) unless destination_topic.topic_allowed_users.where(user_id: user.id).exists?
end
destination_topic.save!
end
end

View File

@ -896,10 +896,10 @@ class Topic < ActiveRecord::Base
end
def move_posts(moved_by, post_ids, opts)
post_mover = PostMover.new(self, moved_by, post_ids)
post_mover = PostMover.new(self, moved_by, post_ids, move_to_pm: opts[:archetype].present? && opts[:archetype] == "private_message")
if opts[:destination_topic_id]
topic = post_mover.to_topic(opts[:destination_topic_id])
topic = post_mover.to_topic(opts[:destination_topic_id], participants: opts[:participants])
DiscourseEvent.trigger(:topic_merged,
post_mover.original_topic,

View File

@ -347,9 +347,15 @@ en:
choose_topic:
none_found: "No topics found."
title:
search: "Search for a Topic by name, url or id:"
search: "Search for a Topic by title, url or id:"
placeholder: "type the topic title here"
choose_message:
none_found: "No messages found."
title:
search: "Search for a Message by title:"
placeholder: "type the message title here"
queue:
topic: "Topic:"
approve: 'Approve'
@ -2004,10 +2010,16 @@ en:
other: "{{count}} posts"
cancel: "Remove filter"
move_to:
title: "Move to"
action: "move to"
error: "There was an error moving posts."
split_topic:
title: "Move to New Topic"
action: "move to new topic"
topic_name: "New Topic Name"
topic_name: "New Topic Title"
radio_label: "New Topic"
error: "There was an error moving posts to the new topic."
instructions:
one: "You are about to create a new topic and populate it with the post you've selected."
@ -2017,10 +2029,30 @@ en:
title: "Move to Existing Topic"
action: "move to existing topic"
error: "There was an error moving posts into that topic."
radio_label: "Existing Topic"
instructions:
one: "Please choose the topic you'd like to move that post to."
other: "Please choose the topic you'd like to move those <b>{{count}}</b> posts to."
move_to_new_message:
title: "Move to New Message"
action: "move to new message"
message_title: "New Message Title"
radio_label: "New Message"
participants: "Participants"
instructions:
one: "You are about to create a new message and populate it with the post you've selected."
other: "You are about to create a new message and populate it with the <b>{{count}}</b> posts you've selected."
move_to_existing_message:
title: "Move to Existing Message"
action: "move to existing message"
radio_label: "Existing Message"
participants: "Participants"
instructions:
one: "Please choose the message you'd like to move that post to."
other: "Please choose the message you'd like to move those <b>{{count}}</b> posts to."
merge_posts:
title: "Merge Selected Posts"
action: "merge selected posts"

View File

@ -1988,11 +1988,11 @@ en:
move_posts:
new_topic_moderator_post:
one: "A post was split to a new topic: %{topic_link}"
other: "%{count} posts were split to a new topic: %{topic_link}"
one: "A post was split to a new %{entity}: %{topic_link}"
other: "%{count} posts were split to a new %{entity}: %{topic_link}"
existing_topic_moderator_post:
one: "A post was merged into an existing topic: %{topic_link}"
other: "%{count} posts were merged into an existing topic: %{topic_link}"
one: "A post was merged into an existing %{entity}: %{topic_link}"
other: "%{count} posts were merged into an existing %{entity}: %{topic_link}"
change_owner:
post_revision_text: "Ownership transferred"

File diff suppressed because it is too large Load Diff

View File

@ -241,6 +241,140 @@ RSpec.describe TopicsController do
end
end
end
describe 'moving to a new message' do
let(:user) { Fabricate(:user) }
let(:trust_level_4) { Fabricate(:trust_level_4) }
let(:moderator) { Fabricate(:moderator) }
let!(:message) { Fabricate(:private_message_topic) }
let!(:p1) { Fabricate(:post, user: user, post_number: 1, topic: message) }
let!(:p2) { Fabricate(:post, user: user, post_number: 2, topic: message) }
it "raises an error without post_ids" do
sign_in(moderator)
post "/t/#{message.id}/move-posts.json", params: { title: 'blah', archetype: 'private_message' }
expect(response.status).to eq(400)
end
it "raises an error when the user doesn't have permission to move the posts" do
sign_in(trust_level_4)
post "/t/#{message.id}/move-posts.json", params: {
title: 'blah', post_ids: [p1.post_number, p2.post_number], archetype: 'private_message'
}
expect(response.status).to eq(403)
result = ::JSON.parse(response.body)
expect(result['errors']).to be_present
end
context 'success' do
before { sign_in(Fabricate(:admin)) }
it "returns success" do
SiteSetting.allow_staff_to_tag_pms = true
expect do
post "/t/#{message.id}/move-posts.json", params: {
title: 'Logan is a good movie',
post_ids: [p2.id],
archetype: 'private_message',
tags: ["tag1", "tag2"]
}
end.to change { Topic.count }.by(1)
expect(response.status).to eq(200)
result = ::JSON.parse(response.body)
expect(result['success']).to eq(true)
expect(result['url']).to eq(Topic.last.relative_url)
expect(Tag.all.pluck(:name)).to contain_exactly("tag1", "tag2")
end
describe 'when message has been deleted' do
it 'should still be able to move posts' do
PostDestroyer.new(Fabricate(:admin), message.first_post).destroy
expect(message.reload.deleted_at).to_not be_nil
expect do
post "/t/#{message.id}/move-posts.json", params: {
title: 'Logan is a good movie',
post_ids: [p2.id],
archetype: 'private_message'
}
end.to change { Topic.count }.by(1)
expect(response.status).to eq(200)
result = JSON.parse(response.body)
expect(result['success']).to eq(true)
expect(result['url']).to eq(Topic.last.relative_url)
end
end
end
context 'failure' do
it "returns JSON with a false success" do
sign_in(moderator)
post "/t/#{message.id}/move-posts.json", params: {
post_ids: [p2.id],
archetype: 'private_message'
}
expect(response.status).to eq(200)
result = ::JSON.parse(response.body)
expect(result['success']).to eq(false)
expect(result['url']).to be_blank
end
end
end
describe 'moving to an existing message' do
let!(:user) { sign_in(Fabricate(:admin)) }
let(:trust_level_4) { Fabricate(:trust_level_4) }
let(:evil_trout) { Fabricate(:evil_trout) }
let(:message) { Fabricate(:private_message_topic) }
let(:p1) { Fabricate(:post, user: user, post_number: 1, topic: message) }
let(:p2) { Fabricate(:post, user: evil_trout, post_number: 2, topic: message) }
let(:dest_message) do
Fabricate(:private_message_topic, user: trust_level_4, topic_allowed_users: [
Fabricate.build(:topic_allowed_user, user: evil_trout)
])
end
context 'success' do
it "returns success" do
user
post "/t/#{message.id}/move-posts.json", params: {
post_ids: [p2.id],
destination_topic_id: dest_message.id,
archetype: 'private_message'
}
expect(response.status).to eq(200)
result = ::JSON.parse(response.body)
expect(result['success']).to eq(true)
expect(result['url']).to be_present
end
end
context 'failure' do
it "returns JSON with a false success" do
post "/t/#{message.id}/move-posts.json", params: {
post_ids: [p2.id],
archetype: 'private_message'
}
expect(response.status).to eq(200)
result = ::JSON.parse(response.body)
expect(result['success']).to eq(false)
expect(result['url']).to be_blank
end
end
end
end
describe '#merge_topic' do
@ -251,7 +385,7 @@ RSpec.describe TopicsController do
expect(response.status).to eq(403)
end
describe 'moving to a new topic' do
describe 'merging into another topic' do
let(:moderator) { Fabricate(:moderator) }
let(:user) { Fabricate(:user) }
let(:p1) { Fabricate(:post, user: user) }
@ -285,6 +419,53 @@ RSpec.describe TopicsController do
end
end
end
describe 'merging into another message' do
let(:moderator) { Fabricate(:moderator) }
let(:user) { Fabricate(:user) }
let(:trust_level_4) { Fabricate(:trust_level_4) }
let(:message) { Fabricate(:private_message_topic, user: user) }
let!(:p1) { Fabricate(:post, topic: message, user: trust_level_4) }
let!(:p2) { Fabricate(:post, topic: message, reply_to_post_number: p1.post_number, user: user) }
it "raises an error without destination_topic_id" do
sign_in(moderator)
post "/t/#{message.id}/merge-topic.json", params: {
archetype: 'private_message'
}
expect(response.status).to eq(400)
end
it "raises an error when the user doesn't have permission to merge" do
sign_in(trust_level_4)
post "/t/#{message.id}/merge-topic.json", params: {
destination_topic_id: 345,
archetype: 'private_message'
}
expect(response).to be_forbidden
end
let(:dest_message) do
Fabricate(:private_message_topic, user: trust_level_4, topic_allowed_users: [
Fabricate.build(:topic_allowed_user, user: moderator)
])
end
context 'moves all the posts to the destination message' do
it "returns success" do
sign_in(moderator)
post "/t/#{message.id}/merge-topic.json", params: {
destination_topic_id: dest_message.id,
archetype: 'private_message'
}
expect(response.status).to eq(200)
result = ::JSON.parse(response.body)
expect(result['success']).to eq(true)
expect(result['url']).to be_present
end
end
end
end
describe '#change_post_owners' do

View File

@ -0,0 +1,121 @@
import { acceptance } from "helpers/qunit-helpers";
acceptance("Topic move posts", { loggedIn: true });
QUnit.test("default", async assert => {
await visit("/t/internationalization-localization");
await click(".toggle-admin-menu");
await click(".topic-admin-multi-select .btn");
await click("#post_11 .select-below");
assert.equal(
find(".selected-posts .move-to-topic")
.text()
.trim(),
I18n.t("topic.move_to.action"),
"it should show the move to button"
);
await click(".selected-posts .move-to-topic");
assert.ok(
find(".move-to-modal .title")
.html()
.includes(I18n.t("topic.move_to.title")),
"it opens move to modal"
);
assert.ok(
find(".move-to-modal .radios")
.html()
.includes(I18n.t("topic.split_topic.radio_label")),
"it shows an option to move to new topic"
);
assert.ok(
find(".move-to-modal .radios")
.html()
.includes(I18n.t("topic.merge_topic.radio_label")),
"it shows an option to move to existing topic"
);
assert.ok(
find(".move-to-modal .radios")
.html()
.includes(I18n.t("topic.move_to_new_message.radio_label")),
"it shows an option to move to new message"
);
});
QUnit.test("moving all posts", async assert => {
await visit("/t/internationalization-localization");
await click(".toggle-admin-menu");
await click(".topic-admin-multi-select .btn");
await click(".select-all");
await click(".selected-posts .move-to-topic");
assert.ok(
find(".move-to-modal .title")
.html()
.includes(I18n.t("topic.move_to.title")),
"it opens move to modal"
);
assert.not(
find(".move-to-modal .radios")
.html()
.includes(I18n.t("topic.split_topic.radio_label")),
"it does not show an option to move to new topic"
);
assert.ok(
find(".move-to-modal .radios")
.html()
.includes(I18n.t("topic.merge_topic.radio_label")),
"it shows an option to move to existing topic"
);
assert.not(
find(".move-to-modal .radios")
.html()
.includes(I18n.t("topic.move_to_new_message.radio_label")),
"it does not show an option to move to new message"
);
});
QUnit.test("moving posts from personal message", async assert => {
await visit("/t/pm-for-testing/12");
await click(".toggle-admin-menu");
await click(".topic-admin-multi-select .btn");
await click("#post_1 .select-post");
assert.equal(
find(".selected-posts .move-to-topic")
.text()
.trim(),
I18n.t("topic.move_to.action"),
"it should show the move to button"
);
await click(".selected-posts .move-to-topic");
assert.ok(
find(".move-to-modal .title")
.html()
.includes(I18n.t("topic.move_to.title")),
"it opens move to modal"
);
assert.ok(
find(".move-to-modal .radios")
.html()
.includes(I18n.t("topic.move_to_new_message.radio_label")),
"it shows an option to move to new message"
);
assert.ok(
find(".move-to-modal .radios")
.html()
.includes(I18n.t("topic.move_to_existing_message.radio_label")),
"it shows an option to move to existing message"
);
});

View File

@ -281,10 +281,6 @@ QUnit.test("Can split/merge topic", function(assert) {
const controller = this.subject({ model });
const selectedPostIds = controller.get("selectedPostIds");
assert.not(
controller.get("canSplitTopic"),
"can't split topic when no posts are selected"
);
assert.not(
controller.get("canMergeTopic"),
"can't merge topic when no posts are selected"
@ -292,10 +288,6 @@ QUnit.test("Can split/merge topic", function(assert) {
selectedPostIds.pushObject(1);
assert.not(
controller.get("canSplitTopic"),
"can't split topic when can't move posts"
);
assert.not(
controller.get("canMergeTopic"),
"can't merge topic when can't move posts"
@ -303,16 +295,11 @@ QUnit.test("Can split/merge topic", function(assert) {
model.set("details.can_move_posts", true);
assert.ok(controller.get("canSplitTopic"), "can split topic");
assert.ok(controller.get("canMergeTopic"), "can merge topic");
selectedPostIds.removeObject(1);
selectedPostIds.pushObject(2);
assert.not(
controller.get("canSplitTopic"),
"can't split topic when 1st post is not a regular post"
);
assert.ok(
controller.get("canMergeTopic"),
"can merge topic when 1st post is not a regular post"
@ -320,10 +307,6 @@ QUnit.test("Can split/merge topic", function(assert) {
selectedPostIds.pushObject(3);
assert.not(
controller.get("canSplitTopic"),
"can't split topic when all posts are selected"
);
assert.ok(
controller.get("canMergeTopic"),
"can merge topic when all posts are selected"