mirror of
https://github.com/discourse/discourse.git
synced 2025-02-25 18:55:32 -06:00
FEATURE: move posts to new/existing PM (#6802)
This commit is contained in:
parent
b478984f60
commit
70fdc10365
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
@ -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")
|
@computed("model.details.can_move_posts", "selectedPostsCount")
|
||||||
canMergeTopic(canMovePosts, selectedPostsCount) {
|
canMergeTopic(canMovePosts, selectedPostsCount) {
|
||||||
return canMovePosts && selectedPostsCount > 0;
|
return canMovePosts && selectedPostsCount > 0;
|
||||||
|
@ -752,11 +752,10 @@ export function movePosts(topicId, data) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function mergeTopic(topicId, destinationTopicId) {
|
export function mergeTopic(topicId, data) {
|
||||||
return ajax("/t/" + topicId + "/merge-topic", {
|
return ajax("/t/" + topicId + "/merge-topic", { type: "POST", data }).then(
|
||||||
type: "POST",
|
moveResult
|
||||||
data: { destination_topic_id: destinationTopicId }
|
);
|
||||||
}).then(moveResult);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Topic;
|
export default Topic;
|
||||||
|
@ -114,17 +114,13 @@ const TopicRoute = Discourse.Route.extend({
|
|||||||
this.controllerFor("raw_email").loadRawEmail(model.get("id"));
|
this.controllerFor("raw_email").loadRawEmail(model.get("id"));
|
||||||
},
|
},
|
||||||
|
|
||||||
mergeTopic() {
|
moveToTopic() {
|
||||||
showModal("merge-topic", {
|
showModal("move-to-topic", {
|
||||||
model: this.modelFor("topic"),
|
model: this.modelFor("topic"),
|
||||||
title: "topic.merge_topic.title"
|
title: "topic.move_to.title"
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
splitTopic() {
|
|
||||||
showModal("split-topic", { model: this.modelFor("topic") });
|
|
||||||
},
|
|
||||||
|
|
||||||
changeOwner() {
|
changeOwner() {
|
||||||
showModal("change-owner", {
|
showModal("change-owner", {
|
||||||
model: this.modelFor("topic"),
|
model: this.modelFor("topic"),
|
||||||
|
@ -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}}
|
@ -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>
|
|
@ -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>
|
@ -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>
|
|
@ -12,12 +12,8 @@
|
|||||||
{{d-button action="deleteSelected" icon="trash-o" label="topic.multi_select.delete" class="btn-danger"}}
|
{{d-button action="deleteSelected" icon="trash-o" label="topic.multi_select.delete" class="btn-danger"}}
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
{{#if canSplitTopic}}
|
|
||||||
{{d-button action="splitTopic" icon="sign-out" label="topic.split_topic.action"}}
|
|
||||||
{{/if}}
|
|
||||||
|
|
||||||
{{#if canMergeTopic}}
|
{{#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}}
|
||||||
|
|
||||||
{{#if canChangeOwner}}
|
{{#if canChangeOwner}}
|
||||||
|
@ -113,19 +113,28 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.split-modal {
|
.move-to-modal {
|
||||||
.modal-body {
|
.modal-body {
|
||||||
position: relative;
|
position: relative;
|
||||||
height: 350px;
|
height: 350px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#move-selected {
|
#move-selected {
|
||||||
|
width: 475px;
|
||||||
|
|
||||||
p {
|
p {
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
input[type="radio"] {
|
.radios {
|
||||||
margin-right: 10px;
|
margin-bottom: 10px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
|
||||||
|
.radio-label {
|
||||||
|
display: inline-block;
|
||||||
|
padding-right: 15px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
button {
|
button {
|
||||||
@ -142,9 +151,19 @@
|
|||||||
width: 95%;
|
width: 95%;
|
||||||
margin-top: 20px;
|
margin-top: 20px;
|
||||||
#split-topic-name,
|
#split-topic-name,
|
||||||
#choose-topic-title {
|
#choose-topic-title,
|
||||||
|
#choose-message-title {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.participant-selector {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.ac-wrap {
|
||||||
|
width: 100%;
|
||||||
|
margin-bottom: 9px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -607,13 +607,26 @@ class TopicsController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def merge_topic
|
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)
|
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)
|
args = {}
|
||||||
render_topic_changes(dest_topic)
|
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
|
end
|
||||||
|
|
||||||
def move_posts
|
def move_posts
|
||||||
@ -621,6 +634,10 @@ class TopicsController < ApplicationController
|
|||||||
topic_id = params.require(:topic_id)
|
topic_id = params.require(:topic_id)
|
||||||
params.permit(:category_id)
|
params.permit(:category_id)
|
||||||
params.permit(:tags)
|
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)
|
topic = Topic.with_deleted.find_by(id: topic_id)
|
||||||
guardian.ensure_can_move_posts!(topic)
|
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.")
|
return render_json_error("When moving posts to a new topic, the first post must be a regular post.")
|
||||||
end
|
end
|
||||||
|
|
||||||
dest_topic = move_posts_to_destination(topic)
|
destination_topic = move_posts_to_destination(topic)
|
||||||
render_topic_changes(dest_topic)
|
render_topic_changes(destination_topic)
|
||||||
rescue ActiveRecord::RecordInvalid => ex
|
rescue ActiveRecord::RecordInvalid => ex
|
||||||
render_json_error(ex)
|
render_json_error(ex)
|
||||||
end
|
end
|
||||||
@ -893,9 +910,15 @@ class TopicsController < ApplicationController
|
|||||||
args = {}
|
args = {}
|
||||||
args[:title] = params[:title] if params[:title].present?
|
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[: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?
|
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)
|
topic.move_posts(current_user, post_ids_including_replies, args)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -5,18 +5,24 @@ class PostMover
|
|||||||
@move_types ||= Enum.new(:new_topic, :existing_topic)
|
@move_types ||= Enum.new(:new_topic, :existing_topic)
|
||||||
end
|
end
|
||||||
|
|
||||||
def initialize(original_topic, user, post_ids)
|
def initialize(original_topic, user, post_ids, move_to_pm: false)
|
||||||
@original_topic = original_topic
|
@original_topic = original_topic
|
||||||
@user = user
|
@user = user
|
||||||
@post_ids = post_ids
|
@post_ids = post_ids
|
||||||
|
@move_to_pm = move_to_pm
|
||||||
end
|
end
|
||||||
|
|
||||||
def to_topic(id)
|
def to_topic(id, participants: nil)
|
||||||
@move_type = PostMover.move_types[:existing_topic]
|
@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
|
Topic.transaction do
|
||||||
move_posts_to Topic.find_by_id(id)
|
move_posts_to topic
|
||||||
end
|
end
|
||||||
|
add_allowed_users(participants) if participants.present? && @move_to_pm
|
||||||
|
topic
|
||||||
end
|
end
|
||||||
|
|
||||||
def to_new_topic(title, category_id = nil, tags = nil)
|
def to_new_topic(title, category_id = nil, tags = nil)
|
||||||
@ -24,13 +30,15 @@ class PostMover
|
|||||||
|
|
||||||
post = Post.find_by(id: post_ids.first)
|
post = Post.find_by(id: post_ids.first)
|
||||||
raise Discourse::InvalidParameters unless post
|
raise Discourse::InvalidParameters unless post
|
||||||
|
archetype = @move_to_pm ? Archetype.private_message : Archetype.default
|
||||||
|
|
||||||
Topic.transaction do
|
Topic.transaction do
|
||||||
new_topic = Topic.create!(
|
new_topic = Topic.create!(
|
||||||
user: post.user,
|
user: post.user,
|
||||||
title: title,
|
title: title,
|
||||||
category_id: category_id,
|
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)
|
DiscourseTagging.tag_topic_by_names(new_topic, Guardian.new(user), tags)
|
||||||
move_posts_to new_topic
|
move_posts_to new_topic
|
||||||
@ -79,7 +87,11 @@ class PostMover
|
|||||||
|
|
||||||
posts.each do |post|
|
posts.each do |post|
|
||||||
post.is_first_post? ? create_first_post(post) : move(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
|
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|
|
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
|
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(
|
I18n.t(
|
||||||
"move_posts.#{move_type_str}_moderator_post",
|
"move_posts.#{move_type_str}_moderator_post",
|
||||||
count: posts.length,
|
count: posts.length,
|
||||||
|
entity: @move_to_pm ? "message" : "topic",
|
||||||
topic_link: posts.first.is_first_post? ?
|
topic_link: posts.first.is_first_post? ?
|
||||||
"[#{destination_topic.title}](#{destination_topic.relative_url})" :
|
"[#{destination_topic.title}](#{destination_topic.relative_url})" :
|
||||||
"[#{destination_topic.title}](#{posts.first.url})"
|
"[#{destination_topic.title}](#{posts.first.url})"
|
||||||
@ -234,4 +247,14 @@ class PostMover
|
|||||||
notifications_reason_id: TopicUser.notification_reasons[:created_topic]
|
notifications_reason_id: TopicUser.notification_reasons[:created_topic]
|
||||||
)
|
)
|
||||||
end
|
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
|
end
|
||||||
|
@ -896,10 +896,10 @@ class Topic < ActiveRecord::Base
|
|||||||
end
|
end
|
||||||
|
|
||||||
def move_posts(moved_by, post_ids, opts)
|
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]
|
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,
|
DiscourseEvent.trigger(:topic_merged,
|
||||||
post_mover.original_topic,
|
post_mover.original_topic,
|
||||||
|
@ -347,9 +347,15 @@ en:
|
|||||||
choose_topic:
|
choose_topic:
|
||||||
none_found: "No topics found."
|
none_found: "No topics found."
|
||||||
title:
|
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"
|
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:
|
queue:
|
||||||
topic: "Topic:"
|
topic: "Topic:"
|
||||||
approve: 'Approve'
|
approve: 'Approve'
|
||||||
@ -2004,10 +2010,16 @@ en:
|
|||||||
other: "{{count}} posts"
|
other: "{{count}} posts"
|
||||||
cancel: "Remove filter"
|
cancel: "Remove filter"
|
||||||
|
|
||||||
|
move_to:
|
||||||
|
title: "Move to"
|
||||||
|
action: "move to"
|
||||||
|
error: "There was an error moving posts."
|
||||||
|
|
||||||
split_topic:
|
split_topic:
|
||||||
title: "Move to New Topic"
|
title: "Move to New Topic"
|
||||||
action: "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."
|
error: "There was an error moving posts to the new topic."
|
||||||
instructions:
|
instructions:
|
||||||
one: "You are about to create a new topic and populate it with the post you've selected."
|
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"
|
title: "Move to Existing Topic"
|
||||||
action: "move to existing topic"
|
action: "move to existing topic"
|
||||||
error: "There was an error moving posts into that topic."
|
error: "There was an error moving posts into that topic."
|
||||||
|
radio_label: "Existing Topic"
|
||||||
instructions:
|
instructions:
|
||||||
one: "Please choose the topic you'd like to move that post to."
|
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."
|
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:
|
merge_posts:
|
||||||
title: "Merge Selected Posts"
|
title: "Merge Selected Posts"
|
||||||
action: "merge selected posts"
|
action: "merge selected posts"
|
||||||
|
@ -1988,11 +1988,11 @@ en:
|
|||||||
|
|
||||||
move_posts:
|
move_posts:
|
||||||
new_topic_moderator_post:
|
new_topic_moderator_post:
|
||||||
one: "A post was 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 topic: %{topic_link}"
|
other: "%{count} posts were split to a new %{entity}: %{topic_link}"
|
||||||
existing_topic_moderator_post:
|
existing_topic_moderator_post:
|
||||||
one: "A post was 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 topic: %{topic_link}"
|
other: "%{count} posts were merged into an existing %{entity}: %{topic_link}"
|
||||||
|
|
||||||
change_owner:
|
change_owner:
|
||||||
post_revision_text: "Ownership transferred"
|
post_revision_text: "Ownership transferred"
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -241,6 +241,140 @@ RSpec.describe TopicsController do
|
|||||||
end
|
end
|
||||||
end
|
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
|
end
|
||||||
|
|
||||||
describe '#merge_topic' do
|
describe '#merge_topic' do
|
||||||
@ -251,7 +385,7 @@ RSpec.describe TopicsController do
|
|||||||
expect(response.status).to eq(403)
|
expect(response.status).to eq(403)
|
||||||
end
|
end
|
||||||
|
|
||||||
describe 'moving to a new topic' do
|
describe 'merging into another topic' do
|
||||||
let(:moderator) { Fabricate(:moderator) }
|
let(:moderator) { Fabricate(:moderator) }
|
||||||
let(:user) { Fabricate(:user) }
|
let(:user) { Fabricate(:user) }
|
||||||
let(:p1) { Fabricate(:post, user: user) }
|
let(:p1) { Fabricate(:post, user: user) }
|
||||||
@ -285,6 +419,53 @@ RSpec.describe TopicsController do
|
|||||||
end
|
end
|
||||||
end
|
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
|
end
|
||||||
|
|
||||||
describe '#change_post_owners' do
|
describe '#change_post_owners' do
|
||||||
|
121
test/javascripts/acceptance/topic-move-posts-test.js.es6
Normal file
121
test/javascripts/acceptance/topic-move-posts-test.js.es6
Normal 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"
|
||||||
|
);
|
||||||
|
});
|
@ -281,10 +281,6 @@ QUnit.test("Can split/merge topic", function(assert) {
|
|||||||
const controller = this.subject({ model });
|
const controller = this.subject({ model });
|
||||||
const selectedPostIds = controller.get("selectedPostIds");
|
const selectedPostIds = controller.get("selectedPostIds");
|
||||||
|
|
||||||
assert.not(
|
|
||||||
controller.get("canSplitTopic"),
|
|
||||||
"can't split topic when no posts are selected"
|
|
||||||
);
|
|
||||||
assert.not(
|
assert.not(
|
||||||
controller.get("canMergeTopic"),
|
controller.get("canMergeTopic"),
|
||||||
"can't merge topic when no posts are selected"
|
"can't merge topic when no posts are selected"
|
||||||
@ -292,10 +288,6 @@ QUnit.test("Can split/merge topic", function(assert) {
|
|||||||
|
|
||||||
selectedPostIds.pushObject(1);
|
selectedPostIds.pushObject(1);
|
||||||
|
|
||||||
assert.not(
|
|
||||||
controller.get("canSplitTopic"),
|
|
||||||
"can't split topic when can't move posts"
|
|
||||||
);
|
|
||||||
assert.not(
|
assert.not(
|
||||||
controller.get("canMergeTopic"),
|
controller.get("canMergeTopic"),
|
||||||
"can't merge topic when can't move posts"
|
"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);
|
model.set("details.can_move_posts", true);
|
||||||
|
|
||||||
assert.ok(controller.get("canSplitTopic"), "can split topic");
|
|
||||||
assert.ok(controller.get("canMergeTopic"), "can merge topic");
|
assert.ok(controller.get("canMergeTopic"), "can merge topic");
|
||||||
|
|
||||||
selectedPostIds.removeObject(1);
|
selectedPostIds.removeObject(1);
|
||||||
selectedPostIds.pushObject(2);
|
selectedPostIds.pushObject(2);
|
||||||
|
|
||||||
assert.not(
|
|
||||||
controller.get("canSplitTopic"),
|
|
||||||
"can't split topic when 1st post is not a regular post"
|
|
||||||
);
|
|
||||||
assert.ok(
|
assert.ok(
|
||||||
controller.get("canMergeTopic"),
|
controller.get("canMergeTopic"),
|
||||||
"can merge topic when 1st post is not a regular post"
|
"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);
|
selectedPostIds.pushObject(3);
|
||||||
|
|
||||||
assert.not(
|
|
||||||
controller.get("canSplitTopic"),
|
|
||||||
"can't split topic when all posts are selected"
|
|
||||||
);
|
|
||||||
assert.ok(
|
assert.ok(
|
||||||
controller.get("canMergeTopic"),
|
controller.get("canMergeTopic"),
|
||||||
"can merge topic when all posts are selected"
|
"can merge topic when all posts are selected"
|
||||||
|
Loading…
Reference in New Issue
Block a user