DEV: Convert `move-to-topic` modal to component-based API (#23293)

<img width="654" alt="Screenshot 2023-08-28 at 2 49 04 PM" src="https://github.com/discourse/discourse/assets/50783505/5fc4dfb5-a7da-4a2b-a524-e81b5039efe6">
<img width="745" alt="Screenshot 2023-08-28 at 2 48 47 PM" src="https://github.com/discourse/discourse/assets/50783505/4432338e-ba05-4206-8553-eb69f8b31b4f">
<img width="448" alt="Screenshot 2023-08-28 at 2 48 44 PM" src="https://github.com/discourse/discourse/assets/50783505/6d6c305d-396b-488a-9ee9-bc41773dd98b">
<img width="747" alt="Screenshot 2023-08-28 at 2 48 42 PM" src="https://github.com/discourse/discourse/assets/50783505/9279f591-ca4a-4152-81c5-71d79dbcbc10">
<img width="671" alt="Screenshot 2023-08-28 at 2 40 42 PM" src="https://github.com/discourse/discourse/assets/50783505/dea02b8b-cf87-45bb-8466-449367b5609a">
<img width="643" alt="Screenshot 2023-08-28 at 2 40 37 PM" src="https://github.com/discourse/discourse/assets/50783505/e422b9b0-89b5-4681-91db-89b8eee05872">
This commit is contained in:
Isaac Janzen 2023-08-29 13:50:13 -05:00 committed by GitHub
parent 480419565f
commit 6650aa18f5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 400 additions and 402 deletions

View File

@ -9,6 +9,7 @@
@value={{this.topicTitle}}
@placeholderKey="choose_topic.title.placeholder"
@id="choose-topic-title"
{{did-insert this.focusInput}}
/>
{{#if this.loading}}

View File

@ -140,6 +140,13 @@ export default Component.extend({
}
},
@action
focusInput(element) {
if (this.autoFocus) {
element.focus();
}
},
_handleEnter(event) {
if (event.key === "Enter") {
event.preventDefault();

View File

@ -0,0 +1,225 @@
<DModal
id="choosing-topic"
@title={{i18n "topic.move_to.title"}}
@closeModal={{@closeModal}}
class="choose-topic-modal"
>
<:body>
{{#if @model.topic.isPrivateMessage}}
<div class="radios">
{{#if this.canSplitToPM}}
<label class="radio-label" for="move-to-new-message">
<RadioButton
id="move-to-new-message"
@name="move-to-entity"
@value="new_message"
@selection={{this.selection}}
/>
<b>{{i18n "topic.move_to_new_message.radio_label"}}</b>
</label>
{{/if}}
<label class="radio-label" for="move-to-existing-message">
<RadioButton
id="move-to-existing-message"
@name="move-to-entity"
@value="existing_message"
@selection={{this.selection}}
/>
<b>{{i18n "topic.move_to_existing_message.radio_label"}}</b>
</label>
</div>
{{#if this.canSplitTopic}}
{{#if this.newMessage}}
<p>
{{html-safe
(i18n
"topic.move_to_new_message.instructions"
count=@model.selectedPostsCount
)
}}
</p>
<form>
<label>{{i18n "topic.move_to_new_message.message_title"}}</label>
<TextField
@value={{this.topicName}}
@placeholderKey="composer.title_placeholder"
id="split-topic-name"
/>
{{#if this.canTagMessages}}
<label>{{i18n "tagging.tags"}}</label>
<TagChooser @tags={{this.tags}} />
{{/if}}
</form>
{{/if}}
{{/if}}
{{#if this.existingMessage}}
<p>
{{html-safe
(i18n
"topic.move_to_existing_message.instructions"
count=@model.selectedPostsCount
)
}}
</p>
<form>
<ChooseMessage
@currentTopicId={{@model.topic.id}}
@selectedTopicId={{this.selectedTopicId}}
/>
<label>{{i18n "topic.move_to_new_message.participants"}}</label>
<EmailGroupUserChooser
class="participant-selector"
@value={{this.participants}}
@onChange={{action (mut this.participants)}}
/>
{{#if this.selectedTopicId}}
<hr />
<label for="chronological-order" class="checkbox-label">
<Input
id="chronological-order"
@type="checkbox"
@checked={{this.chronologicalOrder}}
/>
{{i18n "topic.merge_topic.chronological_order"}}
</label>
{{/if}}
</form>
{{/if}}
{{else}}
<div class="radios">
{{#if this.canSplitTopic}}
<label class="radio-label" for="move-to-new-topic">
<RadioButton
id="move-to-new-topic"
@name="move-to-entity"
@value="new_topic"
@selection={{this.selection}}
/>
<b>{{i18n "topic.split_topic.radio_label"}}</b>
</label>
{{/if}}
<label class="radio-label" for="move-to-existing-topic">
<RadioButton
id="move-to-existing-topic"
@name="move-to-entity"
@value="existing_topic"
@selection={{this.selection}}
/>
<b>{{i18n "topic.merge_topic.radio_label"}}</b>
</label>
{{#if this.canSplitToPM}}
<label class="radio-label" for="move-to-new-message">
<RadioButton
id="move-to-new-message"
@name="move-to-entity"
@value="new_message"
@selection={{this.selection}}
/>
<b>{{i18n "topic.move_to_new_message.radio_label"}}</b>
</label>
{{/if}}
</div>
{{#if this.existingTopic}}
<p>{{html-safe
(i18n
"topic.merge_topic.instructions" count=@model.selectedPostsCount
)
}}</p>
<form>
<ChooseTopic
@currentTopicId={{@model.topic.id}}
@selectedTopicId={{this.selectedTopicId}}
@autoFocus={{this.existingTopic}}
/>
{{#if this.selectedTopicId}}
<hr />
<label for="chronological-order" class="checkbox-label">
<Input
id="chronological-order"
@type="checkbox"
@checked={{this.chronologicalOrder}}
/>
{{i18n "topic.merge_topic.chronological_order"}}
</label>
{{/if}}
</form>
{{/if}}
{{#if this.canSplitTopic}}
{{#if this.newTopic}}
<p>{{html-safe
(i18n
"topic.split_topic.instructions" count=@model.selectedPostsCount
)
}}</p>
<form>
<label>{{i18n "topic.split_topic.topic_name"}}</label>
<TextField
@value={{this.topicName}}
@placeholderKey="composer.title_placeholder"
id="split-topic-name"
/>
<label>{{i18n "categories.category"}}</label>
<CategoryChooser
@value={{this.categoryId}}
class="small"
@onChange={{action (mut this.categoryId)}}
/>
{{#if this.canAddTags}}
<label>{{i18n "tagging.tags"}}</label>
<TagChooser @tags={{this.tags}} @categoryId={{this.categoryId}} />
{{/if}}
</form>
{{/if}}
{{/if}}
{{#if this.canSplitTopic}}
{{#if this.newMessage}}
<p>
{{html-safe
(i18n
"topic.move_to_new_message.instructions"
count=@model.selectedPostsCount
)
}}
</p>
<form>
<label>{{i18n "topic.move_to_new_message.message_title"}}</label>
<TextField
@value={{this.topicName}}
@placeholderKey="composer.title_placeholder"
id="split-topic-name"
/>
{{#if this.canTagMessages}}
<label>{{i18n "tagging.tags"}}</label>
<TagChooser @tags={{this.tags}} />
{{/if}}
</form>
{{/if}}
{{/if}}
{{/if}}
</:body>
<:footer>
<DButton
class="btn-primary"
@disabled={{this.buttonDisabled}}
@action={{this.performMove}}
@icon="sign-out-alt"
@label={{this.buttonTitle}}
/>
</:footer>
</DModal>

View File

@ -0,0 +1,156 @@
import Component from "@glimmer/component";
import { action } from "@ember/object";
import { tracked } from "@glimmer/tracking";
import { mergeTopic, movePosts } from "discourse/models/topic";
import DiscourseURL from "discourse/lib/url";
import I18n from "I18n";
import { isEmpty } from "@ember/utils";
import { inject as service } from "@ember/service";
export default class MoveToTopic extends Component {
@service currentUser;
@service site;
@tracked topicName;
@tracked saving = false;
@tracked categoryId;
@tracked tags;
@tracked participants = [];
@tracked chronologicalOrder = false;
@tracked selection = "new_topic";
@tracked selectedTopicId;
constructor() {
super(...arguments);
if (this.args.model.topic.isPrivateMessage) {
this.selection = this.canSplitToPM ? "new_message" : "existing_message";
} else if (!this.canSplitTopic) {
this.selection = "existing_topic";
}
}
get newTopic() {
return this.selection === "new_topic";
}
get existingTopic() {
return this.selection === "existing_topic";
}
get newMessage() {
return this.selection === "new_message";
}
get existingMessage() {
return this.selection === "existing_message";
}
get buttonDisabled() {
return (
this.saving || (isEmpty(this.selectedTopicId) && isEmpty(this.topicName))
);
}
get buttonTitle() {
if (this.newTopic) {
return "topic.split_topic.title";
} else if (this.existingTopic) {
return "topic.merge_topic.title";
} else if (this.newMessage) {
return "topic.move_to_new_message.title";
} else if (this.existingMessage) {
return "topic.move_to_existing_message.title";
} else {
return "saving";
}
}
get canSplitTopic() {
return (
!this.args.model.selectedAllPosts &&
this.args.model.selectedPosts.length > 0 &&
this.args.model.selectedPosts.sort(
(a, b) => a.post_number - b.post_number
)[0].post_type === this.site.get("post_types.regular")
);
}
get canSplitToPM() {
return this.canSplitTopic && this.currentUser?.admin;
}
@action
performMove() {
if (this.newTopic) {
this.movePostsTo("newTopic");
} else if (this.existingTopic) {
this.movePostsTo("existingTopic");
} else if (this.newMessage) {
this.movePostsTo("newMessage");
} else if (this.existingMessage) {
this.movePostsTo("existingMessage");
}
}
@action
async movePostsTo(type) {
this.saving = true;
this.flash = null;
let mergeOptions, moveOptions;
if (type === "existingTopic") {
mergeOptions = {
destination_topic_id: this.selectedTopicId,
chronological_order: this.chronologicalOrder,
};
moveOptions = {
post_ids: this.args.model.selectedPostIds,
...mergeOptions,
};
} else if (type === "existingMessage") {
mergeOptions = {
destination_topic_id: this.selectedTopicId,
participants: this.participants.join(","),
archetype: "private_message",
chronological_order: this.chronologicalOrder,
};
moveOptions = {
post_ids: this.args.model.selectedPostIds,
...mergeOptions,
};
} else if (type === "newTopic") {
mergeOptions = {};
moveOptions = {
title: this.topicName,
post_ids: this.args.model.selectedPostIds,
category_id: this.categoryId,
tags: this.tags,
};
} else {
mergeOptions = {};
moveOptions = {
title: this.topicName,
post_ids: this.args.model.selectedPostIds,
tags: this.tags,
archetype: "private_message",
};
}
try {
let result;
if (this.args.model.selectedAllPosts) {
result = await mergeTopic(this.args.model.topic.id, mergeOptions);
} else {
result = await movePosts(this.args.model.topic.id, moveOptions);
}
this.args.closeModal();
this.args.model.toggleMultiSelect();
DiscourseURL.routeTo(result.url);
} catch {
this.flash = I18n.t("topic.move_to.error");
} finally {
this.saving = false;
}
}
}

View File

@ -1,186 +0,0 @@
import Controller, { inject as controller } from "@ember/controller";
import { alias, equal } from "@ember/object/computed";
import { mergeTopic, movePosts } from "discourse/models/topic";
import DiscourseURL from "discourse/lib/url";
import I18n from "I18n";
import ModalFunctionality from "discourse/mixins/modal-functionality";
import discourseComputed from "discourse-common/utils/decorators";
import { flashAjaxError } from "discourse/lib/ajax-error";
import { isEmpty } from "@ember/utils";
import { next } from "@ember/runloop";
export default Controller.extend(ModalFunctionality, {
topicName: null,
saving: false,
categoryId: null,
tags: null,
canAddTags: alias("site.can_create_tag"),
canTagMessages: alias("site.can_tag_pms"),
selectedTopicId: null,
newTopic: equal("selection", "new_topic"),
existingTopic: equal("selection", "existing_topic"),
newMessage: equal("selection", "new_message"),
existingMessage: equal("selection", "existing_message"),
participants: null,
chronologicalOrder: false,
init() {
this._super(...arguments);
this.saveAttrNames = [
"newTopic",
"existingTopic",
"newMessage",
"existingMessage",
];
this.moveTypes = [
"newTopic",
"existingTopic",
"newMessage",
"existingMessage",
];
},
topicController: controller("topic"),
selectedPostsCount: alias("topicController.selectedPostsCount"),
selectedAllPosts: alias("topicController.selectedAllPosts"),
selectedPosts: alias("topicController.selectedPosts"),
@discourseComputed("saving", "selectedTopicId", "topicName")
buttonDisabled(saving, selectedTopicId, topicName) {
return saving || (isEmpty(selectedTopicId) && isEmpty(topicName));
},
@discourseComputed(
"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": "choose-topic-modal",
saving: false,
selection: "new_topic",
categoryId: null,
topicName: "",
tags: null,
participants: [],
selectedTopicId: null,
chronologicalOrder: false,
});
const isPrivateMessage = this.get("model.isPrivateMessage");
if (isPrivateMessage) {
this.set(
"selection",
this.canSplitToPM ? "new_message" : "existing_message"
);
} else if (!this.canSplitTopic) {
this.set("selection", "existing_topic");
next(() => $("#choose-topic-title").focus());
}
},
@discourseComputed("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")
);
},
@discourseComputed("canSplitTopic")
canSplitToPM(canSplitTopic) {
return canSplitTopic && this.currentUser && this.currentUser.admin;
},
actions: {
performMove() {
this.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.selectedTopicId,
chronological_order: this.chronologicalOrder,
};
moveOptions = Object.assign(
{ post_ids: this.get("topicController.selectedPostIds") },
mergeOptions
);
} else if (type === "existingMessage") {
mergeOptions = {
destination_topic_id: this.selectedTopicId,
participants: this.participants.join(","),
archetype: "private_message",
chronological_order: this.chronologicalOrder,
};
moveOptions = Object.assign(
{ post_ids: this.get("topicController.selectedPostIds") },
mergeOptions
);
} else if (type === "newTopic") {
mergeOptions = {};
moveOptions = {
title: this.topicName,
post_ids: this.get("topicController.selectedPostIds"),
category_id: this.categoryId,
tags: this.tags,
};
} else {
mergeOptions = {};
moveOptions = {
title: this.topicName,
post_ids: this.get("topicController.selectedPostIds"),
tags: this.tags,
archetype: "private_message",
};
}
const promise = this.get("topicController.selectedAllPosts")
? mergeTopic(topicId, mergeOptions)
: movePosts(topicId, moveOptions);
promise
.then((result) => {
this.send("closeModal");
this.topicController.send("toggleMultiSelect");
DiscourseURL.routeTo(result.url);
})
.catch(flashAjaxError(this, I18n.t("topic.move_to.error")))
.finally(() => {
this.set("saving", false);
});
return false;
},
},
});

View File

@ -17,6 +17,7 @@ import ChangeTimestampModal from "discourse/components/modal/change-timestamp";
import EditTopicTimerModal from "discourse/components/modal/edit-topic-timer";
import FeatureTopicModal from "discourse/components/modal/feature-topic";
import FlagModal from "discourse/components/modal/flag";
import MoveToTopicModal from "discourse/components/modal/move-to-topic";
const SCROLL_DELAY = 500;
@ -212,9 +213,16 @@ const TopicRoute = DiscourseRoute.extend({
@action
moveToTopic() {
showModal("move-to-topic", {
model: this.modelFor("topic"),
title: "topic.move_to.title",
const topicController = this.controllerFor("topic");
this.modal.show(MoveToTopicModal, {
model: {
topic: this.modelFor("topic"),
selectedPostsCount: topicController.selectedPostsCount,
selectedAllPosts: topicController.selectedAllPosts,
selectedPosts: topicController.selectedPosts,
selectedPostIds: topicController.selectedPostIds,
toggleMultiSelect: topicController.toggleMultiSelect,
},
});
},

View File

@ -21,7 +21,6 @@ const KNOWN_LEGACY_MODALS = [
"grant-badge",
"group-default-notifications",
"login",
"move-to-topic",
"raw-email",
"reject-reason-reviewable",
"reorder-categories",

View File

@ -1,212 +0,0 @@
<DModalBody @id="choosing-topic">
{{#if this.model.isPrivateMessage}}
<div class="radios">
{{#if this.canSplitToPM}}
<label class="radio-label" for="move-to-new-message">
<RadioButton
@id="move-to-new-message"
@name="move-to-entity"
@value="new_message"
@selection={{this.selection}}
/>
<b>{{i18n "topic.move_to_new_message.radio_label"}}</b>
</label>
{{/if}}
<label class="radio-label" for="move-to-existing-message">
<RadioButton
@id="move-to-existing-message"
@name="move-to-entity"
@value="existing_message"
@selection={{this.selection}}
/>
<b>{{i18n "topic.move_to_existing_message.radio_label"}}</b>
</label>
</div>
{{#if this.canSplitTopic}}
{{#if this.newMessage}}
<p>{{html-safe
(i18n
"topic.move_to_new_message.instructions"
count=this.selectedPostsCount
)
}}</p>
<form>
<label>{{i18n "topic.move_to_new_message.message_title"}}</label>
<TextField
@value={{this.topicName}}
@placeholderKey="composer.title_placeholder"
@id="split-topic-name"
/>
{{#if this.canTagMessages}}
<label>{{i18n "tagging.tags"}}</label>
<TagChooser @tags={{this.tags}} />
{{/if}}
</form>
{{/if}}
{{/if}}
{{#if this.existingMessage}}
<p>{{html-safe
(i18n
"topic.move_to_existing_message.instructions"
count=this.selectedPostsCount
)
}}</p>
<form>
<ChooseMessage
@currentTopicId={{this.model.id}}
@selectedTopicId={{this.selectedTopicId}}
/>
<label>{{i18n "topic.move_to_new_message.participants"}}</label>
<EmailGroupUserChooser
@class="participant-selector"
@value={{this.participants}}
@onChange={{action (mut this.participants)}}
/>
{{#if this.selectedTopicId}}
<hr />
<label for="chronological-order" class="checkbox-label">
<Input
id="chronological-order"
@type="checkbox"
@checked={{this.chronologicalOrder}}
/>
{{i18n "topic.merge_topic.chronological_order"}}
</label>
{{/if}}
</form>
{{/if}}
{{else}}
<div class="radios">
{{#if this.canSplitTopic}}
<label class="radio-label" for="move-to-new-topic">
<RadioButton
@id="move-to-new-topic"
@name="move-to-entity"
@value="new_topic"
@selection={{this.selection}}
/>
<b>{{i18n "topic.split_topic.radio_label"}}</b>
</label>
{{/if}}
<label class="radio-label" for="move-to-existing-topic">
<RadioButton
@id="move-to-existing-topic"
@name="move-to-entity"
@value="existing_topic"
@selection={{this.selection}}
/>
<b>{{i18n "topic.merge_topic.radio_label"}}</b>
</label>
{{#if this.canSplitToPM}}
<label class="radio-label" for="move-to-new-message">
<RadioButton
@id="move-to-new-message"
@name="move-to-entity"
@value="new_message"
@selection={{this.selection}}
/>
<b>{{i18n "topic.move_to_new_message.radio_label"}}</b>
</label>
{{/if}}
</div>
{{#if this.existingTopic}}
<p>{{html-safe
(i18n "topic.merge_topic.instructions" count=this.selectedPostsCount)
}}</p>
<form>
<ChooseTopic
@currentTopicId={{this.model.id}}
@selectedTopicId={{this.selectedTopicId}}
/>
{{#if this.selectedTopicId}}
<hr />
<label for="chronological-order" class="checkbox-label">
<Input
id="chronological-order"
@type="checkbox"
@checked={{this.chronologicalOrder}}
/>
{{i18n "topic.merge_topic.chronological_order"}}
</label>
{{/if}}
</form>
{{/if}}
{{#if this.canSplitTopic}}
{{#if this.newTopic}}
<p>{{html-safe
(i18n
"topic.split_topic.instructions" count=this.selectedPostsCount
)
}}</p>
<form>
<label>{{i18n "topic.split_topic.topic_name"}}</label>
<TextField
@value={{this.topicName}}
@placeholderKey="composer.title_placeholder"
@id="split-topic-name"
/>
<label>{{i18n "categories.category"}}</label>
<CategoryChooser
@value={{this.categoryId}}
@class="small"
@onChange={{action (mut this.categoryId)}}
/>
{{#if this.canAddTags}}
<label>{{i18n "tagging.tags"}}</label>
<TagChooser @tags={{this.tags}} @categoryId={{this.categoryId}} />
{{/if}}
</form>
{{/if}}
{{/if}}
{{#if this.canSplitTopic}}
{{#if this.newMessage}}
<p>{{html-safe
(i18n
"topic.move_to_new_message.instructions"
count=this.selectedPostsCount
)
}}</p>
<form>
<label>{{i18n "topic.move_to_new_message.message_title"}}</label>
<TextField
@value={{this.topicName}}
@placeholderKey="composer.title_placeholder"
@id="split-topic-name"
/>
{{#if this.canTagMessages}}
<label>{{i18n "tagging.tags"}}</label>
<TagChooser @tags={{this.tags}} />
{{/if}}
</form>
{{/if}}
{{/if}}
{{/if}}
</DModalBody>
<div class="modal-footer">
<DButton
@class="btn-primary"
@disabled={{this.buttonDisabled}}
@action={{action "performMove"}}
@icon="sign-out-alt"
@translatedLabel={{this.buttonTitle}}
/>
</div>