DEV: Remove OpenComposer mixin and refactor related logic (#23015)

The OpenComposer mixin comes from a time before we had a composer service. As well as being a general cleanup/refactor, this commit aims to removes interlinking between composer APIs and the discovery-related controllers which are being removed as part of #22622.

In summary, this commit:
- Removes OpenComposer mixin
- Adds and updates composer service APIs to support everything that `openComposer` did
- Updates consumers to call the composer service directly, instead of relying on the mixin (either directly, or via a route-action which bubbled up to some parent)
- Deprecates composer-related methods on `DiscourseRoute` and on the application route
This commit is contained in:
David Taylor 2023-08-11 09:53:44 +01:00 committed by GitHub
parent 31626ce85d
commit 6de4b3ac3f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 253 additions and 293 deletions

View File

@ -7,7 +7,6 @@
@showUser={{this.showUser}} @showUser={{this.showUser}}
@filterPosts={{this.filterPosts}} @filterPosts={{this.filterPosts}}
@composePrivateMessage={{route-action "composePrivateMessage"}} @composePrivateMessage={{route-action "composePrivateMessage"}}
@createNewMessageViaParams={{route-action "createNewMessageViaParams"}}
role="dialog" role="dialog"
/> />
@ -15,5 +14,4 @@
@topic={{this.topic.model}} @topic={{this.topic.model}}
@showUser={{this.showUser}} @showUser={{this.showUser}}
@showGroup={{this.showGroup}} @showGroup={{this.showGroup}}
@createNewMessageViaParams={{route-action "createNewMessageViaParams"}}
/> />

View File

@ -2,8 +2,6 @@
@composerModel={{this.model}} @composerModel={{this.model}}
@replyOptions={{this.model.replyOptions}} @replyOptions={{this.model.replyOptions}}
@canWhisper={{this.canWhisper}} @canWhisper={{this.canWhisper}}
@openComposer={{this.openComposer}}
@closeComposer={{this.closeComposer}}
@action={{this.model.action}} @action={{this.model.action}}
@tabindex={{this.tabindex}} @tabindex={{this.tabindex}}
@topic={{this.model.topic}} @topic={{this.model.topic}}

View File

@ -46,8 +46,6 @@
<div class="reply-details"> <div class="reply-details">
<ComposerActionTitle <ComposerActionTitle
@model={{this.composer.model}} @model={{this.composer.model}}
@openComposer={{this.composer.openComposer}}
@closeComposer={{this.composer.closeComposer}}
@canWhisper={{this.composer.canWhisper}} @canWhisper={{this.composer.canWhisper}}
/> />

View File

@ -8,10 +8,12 @@ import discourseComputed from "discourse-common/utils/decorators";
import { groupPath } from "discourse/lib/url"; import { groupPath } from "discourse/lib/url";
import { setting } from "discourse/lib/computed"; import { setting } from "discourse/lib/computed";
import { modKeysPressed } from "discourse/lib/utilities"; import { modKeysPressed } from "discourse/lib/utilities";
import { inject as service } from "@ember/service";
const maxMembersToDisplay = 10; const maxMembersToDisplay = 10;
export default Component.extend(CardContentsBase, CleansUp, { export default Component.extend(CardContentsBase, CleansUp, {
composer: service(),
elementId: "group-card", elementId: "group-card",
mentionSelector: "a.mention-group", mentionSelector: "a.mention-group",
classNames: ["no-bg", "group-card"], classNames: ["no-bg", "group-card"],
@ -105,7 +107,7 @@ export default Component.extend(CardContentsBase, CleansUp, {
}, },
messageGroup() { messageGroup() {
this.createNewMessageViaParams({ this.composer.openNewMessage({
recipients: this.get("group.name"), recipients: this.get("group.name"),
hasGroups: true, hasGroups: true,
}); });

View File

@ -18,10 +18,10 @@ export default class TopicsController extends DiscoveryController.extend(
DismissTopics DismissTopics
) { ) {
@service router; @service router;
@service composer;
@controller discovery; @controller discovery;
period = null; period = null;
canCreateTopicOnCategory = null;
selected = null; selected = null;
expandGloballyPinned = false; expandGloballyPinned = false;
expandAllPinned = false; expandAllPinned = false;

View File

@ -22,6 +22,7 @@ export default Controller.extend({
dialog: service(), dialog: service(),
currentUser: service(), currentUser: service(),
router: service(), router: service(),
composer: service(),
counts: null, counts: null,
showing: "members", showing: "members",
@ -130,7 +131,7 @@ export default Controller.extend({
@action @action
messageGroup() { messageGroup() {
this.send("createNewMessageViaParams", { this.composer.openNewMessage({
recipients: this.get("model.name"), recipients: this.get("model.name"),
hasGroups: true, hasGroups: true,
}); });

View File

@ -1,6 +1,8 @@
import NavigationDefaultController from "discourse/controllers/navigation/default"; import NavigationDefaultController from "discourse/controllers/navigation/default";
import { inject as controller } from "@ember/controller"; import { inject as controller } from "@ember/controller";
import { inject as service } from "@ember/service";
export default class NavigationCategoriesController extends NavigationDefaultController { export default class NavigationCategoriesController extends NavigationDefaultController {
@service composer;
@controller("discovery/categories") discoveryCategories; @controller("discovery/categories") discoveryCategories;
} }

View File

@ -2,8 +2,11 @@ import NavigationDefaultController from "discourse/controllers/navigation/defaul
import { calculateFilterMode } from "discourse/lib/filter-mode"; import { calculateFilterMode } from "discourse/lib/filter-mode";
import { dependentKeyCompat } from "@ember/object/compat"; import { dependentKeyCompat } from "@ember/object/compat";
import { tracked } from "@glimmer/tracking"; import { tracked } from "@glimmer/tracking";
import { inject as service } from "@ember/service";
export default class NavigationCategoryController extends NavigationDefaultController { export default class NavigationCategoryController extends NavigationDefaultController {
@service composer;
@tracked category; @tracked category;
@tracked filterType; @tracked filterType;
@tracked noSubcategories; @tracked noSubcategories;
@ -16,4 +19,22 @@ export default class NavigationCategoryController extends NavigationDefaultContr
noSubcategories: this.noSubcategories, noSubcategories: this.noSubcategories,
}); });
} }
get createTopicTargetCategory() {
if (this.category?.canCreateTopic) {
return this.category;
}
if (this.siteSettings.default_subcategory_on_read_only_category) {
return this.category?.subcategoryWithCreateTopicPermission;
}
}
get enableCreateTopicButton() {
return !!this.createTopicTargetCategory;
}
get canCreateTopic() {
return this.currentUser?.can_create_topic;
}
} }

View File

@ -7,6 +7,7 @@ import { tracked } from "@glimmer/tracking";
export default class NavigationDefaultController extends Controller { export default class NavigationDefaultController extends Controller {
@service router; @service router;
@service composer;
@controller discovery; @controller discovery;
@tracked category; @tracked category;

View File

@ -1,77 +0,0 @@
// This mixin allows a route to open the composer
import Composer from "discourse/models/composer";
import Mixin from "@ember/object/mixin";
import { getOwner } from "discourse-common/lib/get-owner";
export default Mixin.create({
openComposer(controller) {
let categoryId = controller.get("category.id");
if (
this.siteSettings.default_subcategory_on_read_only_category &&
!controller.canCreateTopicOnCategory
) {
if (controller.canCreateTopicOnSubCategory) {
categoryId = controller.get("defaultSubcategory.id");
} else {
categoryId = this.siteSettings.default_composer_category;
}
}
if (
categoryId &&
!this.siteSettings.default_subcategory_on_read_only_category &&
controller.category.isUncategorizedCategory &&
!this.siteSettings.allow_uncategorized_topics
) {
categoryId = null;
}
getOwner(this)
.lookup("service:composer")
.open({
prioritizedCategoryId: categoryId,
topicCategoryId: categoryId,
action: Composer.CREATE_TOPIC,
draftKey: controller.get("model.draft_key") || Composer.NEW_TOPIC_KEY,
draftSequence: controller.get("model.draft_sequence") || 0,
});
},
openComposerWithTopicParams(
controller,
topicTitle,
topicBody,
topicCategoryId,
topicTags
) {
getOwner(this)
.lookup("service:composer")
.open({
action: Composer.CREATE_TOPIC,
topicTitle,
topicBody,
topicCategoryId,
topicTags,
draftKey: controller.get("model.draft_key") || Composer.NEW_TOPIC_KEY,
draftSequence: controller.get("model.draft_sequence"),
});
},
openComposerWithMessageParams({
recipients = "",
topicTitle = "",
topicBody = "",
hasGroups = false,
} = {}) {
getOwner(this).lookup("service:composer").open({
action: Composer.PRIVATE_MESSAGE,
recipients,
topicTitle,
topicBody,
archetypeId: "private_message",
draftKey: Composer.NEW_PRIVATE_MESSAGE_KEY,
hasGroups,
});
},
});

View File

@ -345,6 +345,16 @@ const Category = RestModel.extend({
isUncategorizedCategory(id) { isUncategorizedCategory(id) {
return Category.isUncategorized(id); return Category.isUncategorized(id);
}, },
get canCreateTopic() {
return this.permission === PermissionType.FULL;
},
get subcategoryWithCreateTopicPermission() {
return this.subcategories?.find(
(subcategory) => subcategory.canCreateTopic
);
},
}); });
let _uncategorized; let _uncategorized;

View File

@ -3,7 +3,6 @@ import Category from "discourse/models/category";
import Composer from "discourse/models/composer"; import Composer from "discourse/models/composer";
import DiscourseRoute from "discourse/routes/discourse"; import DiscourseRoute from "discourse/routes/discourse";
import I18n from "I18n"; import I18n from "I18n";
import OpenComposer from "discourse/mixins/open-composer";
import { ajax } from "discourse/lib/ajax"; import { ajax } from "discourse/lib/ajax";
import { findAll } from "discourse/models/login-method"; import { findAll } from "discourse/models/login-method";
import { getOwner } from "discourse-common/lib/get-owner"; import { getOwner } from "discourse-common/lib/get-owner";
@ -17,6 +16,7 @@ import { action } from "@ember/object";
import KeyboardShortcutsHelp from "discourse/components/modal/keyboard-shortcuts-help"; import KeyboardShortcutsHelp from "discourse/components/modal/keyboard-shortcuts-help";
import NotActivatedModal from "../components/modal/not-activated"; import NotActivatedModal from "../components/modal/not-activated";
import ForgotPassword from "discourse/components/modal/forgot-password"; import ForgotPassword from "discourse/components/modal/forgot-password";
import deprecated from "discourse-common/lib/deprecated";
function unlessReadOnly(method, message) { function unlessReadOnly(method, message) {
return function () { return function () {
@ -38,7 +38,7 @@ function unlessStrictlyReadOnly(method, message) {
}; };
} }
const ApplicationRoute = DiscourseRoute.extend(OpenComposer, { const ApplicationRoute = DiscourseRoute.extend({
siteTitle: setting("title"), siteTitle: setting("title"),
shortSiteDescription: setting("short_site_description"), shortSiteDescription: setting("short_site_description"),
documentTitle: service(), documentTitle: service(),
@ -190,14 +190,17 @@ const ApplicationRoute = DiscourseRoute.extend(OpenComposer, {
user.checkEmail(); user.checkEmail();
}, },
createNewTopicViaParams(title, body, category_id, tags) { createNewTopicViaParams(title, body, categoryId, tags) {
this.openComposerWithTopicParams( deprecated(
this.controllerFor("discovery/topics"), "createNewTopicViaParam on the application route is deprecated. Use the composer service instead",
{ id: "discourse.createNewTopicViaParams" }
);
getOwner(this).lookup("service:composer").openNewTopic({
title, title,
body, body,
category_id, categoryId,
tags tags,
); });
}, },
createNewMessageViaParams({ createNewMessageViaParams({
@ -206,10 +209,14 @@ const ApplicationRoute = DiscourseRoute.extend(OpenComposer, {
topicBody = "", topicBody = "",
hasGroups = false, hasGroups = false,
} = {}) { } = {}) {
this.openComposerWithMessageParams({ deprecated(
"createNewMessageViaParams on the application route is deprecated. Use the composer service instead",
{ id: "discourse.createNewMessageViaParams" }
);
getOwner(this).lookup("service:composer").openNewMessage({
recipients, recipients,
topicTitle, title: topicTitle,
topicBody, body: topicBody,
hasGroups, hasGroups,
}); });
}, },

View File

@ -13,7 +13,6 @@ import Category from "discourse/models/category";
import CategoryList from "discourse/models/category-list"; import CategoryList from "discourse/models/category-list";
import DiscourseRoute from "discourse/routes/discourse"; import DiscourseRoute from "discourse/routes/discourse";
import I18n from "I18n"; import I18n from "I18n";
import PermissionType from "discourse/models/permission-type";
import TopicList from "discourse/models/topic-list"; import TopicList from "discourse/models/topic-list";
import { action } from "@ember/object"; import { action } from "@ember/object";
import PreloadStore from "discourse/lib/preload-store"; import PreloadStore from "discourse/lib/preload-store";
@ -152,33 +151,7 @@ class AbstractCategoryRoute extends DiscourseRoute {
setupController(controller, model) { setupController(controller, model) {
const topics = this.topics, const topics = this.topics,
category = model.category, category = model.category;
canCreateTopic = topics.get("can_create_topic");
let canCreateTopicOnCategory =
canCreateTopic && category.get("permission") === PermissionType.FULL;
let cannotCreateTopicOnCategory = !canCreateTopicOnCategory;
let defaultSubcategory;
let canCreateTopicOnSubCategory;
if (this.siteSettings.default_subcategory_on_read_only_category) {
cannotCreateTopicOnCategory = false;
if (!canCreateTopicOnCategory && category.subcategories) {
defaultSubcategory = category.subcategories.find((subcategory) => {
return subcategory.get("permission") === PermissionType.FULL;
});
canCreateTopicOnSubCategory = !!defaultSubcategory;
}
}
this.controllerFor("navigation/category").setProperties({
canCreateTopicOnCategory,
cannotCreateTopicOnCategory,
canCreateTopic,
canCreateTopicOnSubCategory,
defaultSubcategory,
});
let topicOpts = { let topicOpts = {
model: topics, model: topics,
@ -189,10 +162,6 @@ class AbstractCategoryRoute extends DiscourseRoute {
selected: [], selected: [],
noSubcategories: this.routeConfig && !!this.routeConfig.no_subcategories, noSubcategories: this.routeConfig && !!this.routeConfig.no_subcategories,
expandAllPinned: true, expandAllPinned: true,
canCreateTopic,
canCreateTopicOnCategory,
canCreateTopicOnSubCategory,
defaultSubcategory,
}; };
const p = category.get("params"); const p = category.get("params");

View File

@ -1,9 +1,8 @@
import Composer from "discourse/models/composer";
import Draft from "discourse/models/draft";
import Route from "@ember/routing/route"; import Route from "@ember/routing/route";
import { once } from "@ember/runloop"; import { once } from "@ember/runloop";
import { seenUser } from "discourse/lib/user-presence"; import { seenUser } from "discourse/lib/user-presence";
import { getOwner } from "discourse-common/lib/get-owner"; import { getOwner } from "discourse-common/lib/get-owner";
import deprecated from "discourse-common/lib/deprecated";
const DiscourseRoute = Route.extend({ const DiscourseRoute = Route.extend({
showFooter: false, showFooter: false,
@ -54,24 +53,14 @@ const DiscourseRoute = Route.extend({
}, },
openTopicDraft() { openTopicDraft() {
const composer = getOwner(this).lookup("service:composer"); deprecated(
"DiscourseRoute#openTopicDraft is deprecated. Inject the composer service and call openNewTopic instead",
if ( { id: "discourse.open-topic-draft" }
composer.get("model.action") === Composer.CREATE_TOPIC && );
composer.get("model.draftKey") === Composer.NEW_TOPIC_KEY if (this.currentUser?.has_topic_draft) {
) { return getOwner(this)
composer.set("model.composeState", Composer.OPEN); .lookup("service:composer")
} else { .openNewTopic({ preferDraft: true });
Draft.get(Composer.NEW_TOPIC_KEY).then((data) => {
if (data.draft) {
composer.open({
action: Composer.CREATE_TOPIC,
draft: data.draft,
draftKey: Composer.NEW_TOPIC_KEY,
draftSequence: data.draft_sequence,
});
}
});
} }
}, },

View File

@ -2,7 +2,6 @@ import CategoryList from "discourse/models/category-list";
import DiscourseRoute from "discourse/routes/discourse"; import DiscourseRoute from "discourse/routes/discourse";
import EmberObject, { action } from "@ember/object"; import EmberObject, { action } from "@ember/object";
import I18n from "I18n"; import I18n from "I18n";
import OpenComposer from "discourse/mixins/open-composer";
import PreloadStore from "discourse/lib/preload-store"; import PreloadStore from "discourse/lib/preload-store";
import TopicList from "discourse/models/topic-list"; import TopicList from "discourse/models/topic-list";
import { ajax } from "discourse/lib/ajax"; import { ajax } from "discourse/lib/ajax";
@ -13,9 +12,7 @@ import showModal from "discourse/lib/show-modal";
import Session from "discourse/models/session"; import Session from "discourse/models/session";
import { inject as service } from "@ember/service"; import { inject as service } from "@ember/service";
export default class DiscoveryCategoriesRoute extends DiscourseRoute.extend( export default class DiscoveryCategoriesRoute extends DiscourseRoute {
OpenComposer
) {
@service router; @service router;
renderTemplate() { renderTemplate() {
@ -157,15 +154,6 @@ export default class DiscoveryCategoriesRoute extends DiscourseRoute.extend(
showModal("reorder-categories"); showModal("reorder-categories");
} }
@action
createTopic() {
if (this.get("currentUser.has_topic_draft")) {
this.openTopicDraft();
} else {
this.openComposer(this.controllerFor("discovery/categories"));
}
}
@action @action
didTransition() { didTransition() {
next(() => this.controllerFor("application").set("showFooter", true)); next(() => this.controllerFor("application").set("showFooter", true));

View File

@ -1,5 +1,4 @@
import DiscourseRoute from "discourse/routes/discourse"; import DiscourseRoute from "discourse/routes/discourse";
import OpenComposer from "discourse/mixins/open-composer";
import User from "discourse/models/user"; import User from "discourse/models/user";
import { setTopicList } from "discourse/lib/topic-list-tracker"; import { setTopicList } from "discourse/lib/topic-list-tracker";
import { action } from "@ember/object"; import { action } from "@ember/object";
@ -9,9 +8,7 @@ import { resetCachedTopicList } from "discourse/lib/cached-topic-list";
The parent route for all discovery routes. The parent route for all discovery routes.
Handles the logic for showing the loading spinners. Handles the logic for showing the loading spinners.
**/ **/
export default class DiscoveryRoute extends DiscourseRoute.extend( export default class DiscoveryRoute extends DiscourseRoute {
OpenComposer
) {
queryParams = { queryParams = {
filter: { refreshModel: true }, filter: { refreshModel: true },
}; };
@ -74,15 +71,6 @@ export default class DiscoveryRoute extends DiscourseRoute.extend(
topic.clearPin(); topic.clearPin();
} }
@action
createTopic() {
if (this.get("currentUser.has_topic_draft")) {
this.openTopicDraft();
} else {
this.openComposer(this.controllerFor("discovery/topics"));
}
}
@action @action
dismissReadTopics(dismissTopics) { dismissReadTopics(dismissTopics) {
const operationType = dismissTopics ? "topics" : "posts"; const operationType = dismissTopics ? "topics" : "posts";

View File

@ -7,6 +7,7 @@ import { inject as service } from "@ember/service";
export default DiscourseRoute.extend({ export default DiscourseRoute.extend({
dialog: service(), dialog: service(),
composer: service(),
beforeModel(transition) { beforeModel(transition) {
const params = transition.to.queryParams; const params = transition.to.queryParams;
@ -14,12 +15,12 @@ export default DiscourseRoute.extend({
const groupName = params.groupname || params.group_name; const groupName = params.groupname || params.group_name;
if (this.currentUser) { if (this.currentUser) {
this.replaceWith("discovery.latest").then((e) => { this.replaceWith("discovery.latest").then(() => {
if (params.username) { if (params.username) {
e.send("createNewMessageViaParams", { this.composer.openNewMessage({
recipients: params.username, recipients: params.username,
topicTitle: params.title, title: params.title,
topicBody: params.body, body: params.body,
}); });
} else if (groupName) { } else if (groupName) {
// send a message to a group // send a message to a group
@ -27,10 +28,10 @@ export default DiscourseRoute.extend({
.then((result) => { .then((result) => {
if (result.messageable) { if (result.messageable) {
next(() => next(() =>
e.send("createNewMessageViaParams", { this.composer.openNewMessage({
recipients: groupName, recipients: groupName,
topicTitle: params.title, title: params.title,
topicBody: params.body, body: params.body,
}) })
); );
} else { } else {
@ -41,9 +42,9 @@ export default DiscourseRoute.extend({
}) })
.catch(() => this.dialog.alert(I18n.t("generic_error"))); .catch(() => this.dialog.alert(I18n.t("generic_error")));
} else { } else {
e.send("createNewMessageViaParams", { this.composer.openNewMessage({
topicTitle: params.title, title: params.title,
topicBody: params.body, body: params.body,
}); });
} }
}); });

View File

@ -2,14 +2,58 @@ import Category from "discourse/models/category";
import DiscourseRoute from "discourse/routes/discourse"; import DiscourseRoute from "discourse/routes/discourse";
import cookie from "discourse/lib/cookie"; import cookie from "discourse/lib/cookie";
import { next } from "@ember/runloop"; import { next } from "@ember/runloop";
import { inject as service } from "@ember/service";
export default class extends DiscourseRoute {
@service composer;
export default DiscourseRoute.extend({
beforeModel(transition) { beforeModel(transition) {
if (this.currentUser) { if (this.currentUser) {
let category, categoryId; const category = this.parseCategoryFromTransition(transition);
if (category) {
this.replaceWith("discovery.category", {
category,
id: category.id,
}).then(() => {
if (this.currentUser.can_create_topic) {
this.openComposer({ transition, category });
}
});
} else if (transition.from) {
// Navigation from another ember route
transition.abort();
this.openComposer({ transition });
} else {
this.replaceWith("discovery.latest").then(() => {
if (this.currentUser.can_create_topic) {
this.openComposer({ transition });
}
});
}
} else {
// User is not logged in
cookie("destination_url", window.location.href);
this.replaceWith("login");
}
}
openComposer({ transition, category }) {
next(() => {
this.composer.openNewTopic({
title: transition.to.queryParams.title,
body: transition.to.queryParams.body,
category,
tags: transition.to.queryParams.tags,
});
});
}
parseCategoryFromTransition(transition) {
let category;
if (transition.to.queryParams.category_id) { if (transition.to.queryParams.category_id) {
categoryId = transition.to.queryParams.category_id; const categoryId = transition.to.queryParams.category_id;
category = Category.findById(categoryId); category = Category.findById(categoryId);
} else if (transition.to.queryParams.category) { } else if (transition.to.queryParams.category) {
const splitCategory = transition.to.queryParams.category.split("/"); const splitCategory = transition.to.queryParams.category.split("/");
@ -27,52 +71,10 @@ export default DiscourseRoute.extend({
"slug" "slug"
); );
} }
if (category) {
categoryId = category.id;
} }
return category;
} }
if (category) {
let route = "discovery.category";
let params = { category, id: category.id };
this.replaceWith(route, params).then((e) => {
if (this.controllerFor("navigation/category").canCreateTopic) {
this._sendTransition(e, transition, categoryId);
}
});
} else {
if (transition.from) {
transition.abort();
this.send("createNewTopicViaParams");
} else {
this.replaceWith("discovery.latest").then((e) => {
if (this.controllerFor("navigation/default").canCreateTopic) {
this._sendTransition(e, transition);
}
});
}
}
} else {
// User is not logged in
cookie("destination_url", window.location.href);
this.replaceWith("login");
}
},
_sendTransition(event, transition, categoryId) {
next(() => {
event.send(
"createNewTopicViaParams",
transition.to.queryParams.title,
transition.to.queryParams.body,
categoryId,
transition.to.queryParams.tags
);
});
},
_getCategory(mainCategory, subCategory, type) { _getCategory(mainCategory, subCategory, type) {
let category; let category;
if (!subCategory) { if (!subCategory) {
@ -92,5 +94,5 @@ export default DiscourseRoute.extend({
} }
return category; return category;
}, }
}); }

View File

@ -1,4 +1,9 @@
import Composer, { SAVE_ICONS, SAVE_LABELS } from "discourse/models/composer"; import Composer, {
CREATE_TOPIC,
NEW_TOPIC_KEY,
SAVE_ICONS,
SAVE_LABELS,
} from "discourse/models/composer";
import EmberObject, { action, computed } from "@ember/object"; import EmberObject, { action, computed } from "@ember/object";
import { alias, and, or, reads } from "@ember/object/computed"; import { alias, and, or, reads } from "@ember/object/computed";
import { import {
@ -567,33 +572,6 @@ export default class ComposerService extends Service {
this.close(); this.close();
} }
@action
async openComposer(options, post, topic) {
await this.open(options);
let url = post?.url || topic?.url;
const topicTitle = topic?.title;
if (!url || !topicTitle) {
return;
}
url = `${location.protocol}//${location.host}${url}`;
const link = `[${escapeExpression(topicTitle)}](${url})`;
const continueDiscussion = I18n.t("post.continue_discussion", {
postLink: link,
});
const reply = this.get("model.reply");
if (reply?.includes(continueDiscussion)) {
return;
}
this.model.prependText(continueDiscussion, {
new_line: true,
});
}
@action @action
onPopupMenuAction(menuAction) { onPopupMenuAction(menuAction) {
return ( return (
@ -1339,6 +1317,62 @@ export default class ComposerService extends Service {
} }
} }
async #openNewTopicDraft() {
if (
this.model?.action === Composer.CREATE_TOPIC &&
this.model?.draftKey === Composer.NEW_TOPIC_KEY
) {
this.set("model.composeState", Composer.OPEN);
} else {
const data = await Draft.get(Composer.NEW_TOPIC_KEY);
if (data.draft) {
return this.open({
action: Composer.CREATE_TOPIC,
draft: data.draft,
draftKey: Composer.NEW_TOPIC_KEY,
draftSequence: data.draft_sequence,
});
}
}
}
@action
async openNewTopic({
title,
body,
category,
tags,
preferDraft = false,
} = {}) {
if (preferDraft && this.currentUser.has_topic_draft) {
return this.#openNewTopicDraft();
} else {
return this.open({
prioritizedCategoryId: category?.id,
topicCategoryId: category?.id,
topicTitle: title,
topicBody: body,
topicTags: tags,
action: CREATE_TOPIC,
draftKey: NEW_TOPIC_KEY,
draftSequence: 0,
});
}
}
@action
async openNewMessage({ title, body, recipients, hasGroups }) {
return this.open({
action: Composer.PRIVATE_MESSAGE,
recipients,
topicTitle: title,
topicBody: body,
archetypeId: "private_message",
draftKey: Composer.NEW_PRIVATE_MESSAGE_KEY,
hasGroups,
});
}
// Given a potential instance and options, set the model for this composer. // Given a potential instance and options, set the model for this composer.
async _setModel(optionalComposerModel, opts) { async _setModel(optionalComposerModel, opts) {
this.set("linkLookup", null); this.set("linkLookup", null);
@ -1406,6 +1440,12 @@ export default class ComposerService extends Service {
this.model.set("reply", opts.topicBody); this.model.set("reply", opts.topicBody);
} }
if (opts.prependText && !this.model.reply?.includes(opts.prependText)) {
this.model.prependText(opts.prependText, {
new_line: true,
});
}
const defaultComposerHeight = this._getDefaultComposerHeight(); const defaultComposerHeight = this._getDefaultComposerHeight();
this.set("model.composerHeight", defaultComposerHeight); this.set("model.composerHeight", defaultComposerHeight);

View File

@ -3,7 +3,7 @@
{{#unless this.viewingCategoriesList}} {{#unless this.viewingCategoriesList}}
<CategoryReadOnlyBanner <CategoryReadOnlyBanner
@category={{this.category}} @category={{this.category}}
@readOnly={{this.navigationCategory.cannotCreateTopicOnCategory}} @readOnly={{not this.navigationCategory.enableCreateTopicButton}}
/> />
{{/unless}} {{/unless}}
</div> </div>

View File

@ -119,9 +119,12 @@
@message={{this.footerMessage}} @message={{this.footerMessage}}
> >
{{#if this.latest}} {{#if this.latest}}
{{#if this.canCreateTopicOnCategory}} {{#if this.category.canCreateTopic}}
<DiscourseLinkedText <DiscourseLinkedText
@action={{route-action "createTopic"}} @action={{fn
this.composer.openNewTopic
(hash category=this.category preferDraft=true)
}}
@text="topic.suggest_create_topic" @text="topic.suggest_create_topic"
/> />
{{/if}} {{/if}}

View File

@ -76,9 +76,12 @@
@message={{this.footerMessage}} @message={{this.footerMessage}}
> >
{{#if this.latest}} {{#if this.latest}}
{{#if this.canCreateTopicOnCategory}} {{#if this.category.canCreateTopic}}
<DiscourseLinkedText <DiscourseLinkedText
@action={{route-action "createTopic"}} @action={{fn
this.composer.openNewTopic
(hash category=this.category preferDraft=true)
}}
@text="topic.suggest_create_topic" @text="topic.suggest_create_topic"
/> />
{{/if}} {{/if}}

View File

@ -6,6 +6,6 @@
@reorderCategories={{route-action "reorderCategories"}} @reorderCategories={{route-action "reorderCategories"}}
@canCreateTopic={{this.canCreateTopic}} @canCreateTopic={{this.canCreateTopic}}
@hasDraft={{this.currentUser.has_topic_draft}} @hasDraft={{this.currentUser.has_topic_draft}}
@createTopic={{route-action "createTopic"}} @createTopic={{fn this.composer.openNewTopic (hash preferDraft=true)}}
/> />
</DSection> </DSection>

View File

@ -23,8 +23,11 @@
@filterMode={{this.filterMode}} @filterMode={{this.filterMode}}
@noSubcategories={{this.noSubcategories}} @noSubcategories={{this.noSubcategories}}
@canCreateTopic={{this.canCreateTopic}} @canCreateTopic={{this.canCreateTopic}}
@createTopic={{route-action "createTopic"}} @createTopic={{fn
@createTopicDisabled={{this.cannotCreateTopicOnCategory}} this.composer.openNewTopic
(hash category=this.createTopicTargetCategory preferDraft=true)
}}
@createTopicDisabled={{not this.enableCreateTopicButton}}
@hasDraft={{this.currentUser.has_topic_draft}} @hasDraft={{this.currentUser.has_topic_draft}}
@editCategory={{route-action "editCategory" this.category}} @editCategory={{route-action "editCategory" this.category}}
/> />

View File

@ -3,7 +3,7 @@
@filterMode={{this.filterMode}} @filterMode={{this.filterMode}}
@canCreateTopic={{this.canCreateTopic}} @canCreateTopic={{this.canCreateTopic}}
@hasDraft={{this.currentUser.has_topic_draft}} @hasDraft={{this.currentUser.has_topic_draft}}
@createTopic={{route-action "createTopic"}} @createTopic={{fn this.composer.openNewTopic (hash preferDraft=true)}}
@skipCategoriesNavItem={{this.skipCategoriesNavItem}} @skipCategoriesNavItem={{this.skipCategoriesNavItem}}
/> />
</DSection> </DSection>

View File

@ -14,6 +14,7 @@ export default {
moderator: true, moderator: true,
staff: true, staff: true,
can_create_group: true, can_create_group: true,
can_create_topic: true,
title: "co-founder", title: "co-founder",
reply_count: 859, reply_count: 859,
topic_count: 36, topic_count: 36,

View File

@ -13,6 +13,7 @@ import { camelize } from "@ember/string";
import { equal, gt } from "@ember/object/computed"; import { equal, gt } from "@ember/object/computed";
import { isEmpty } from "@ember/utils"; import { isEmpty } from "@ember/utils";
import { inject as service } from "@ember/service"; import { inject as service } from "@ember/service";
import { escapeExpression } from "discourse/lib/utilities";
// Component can get destroyed and lose state // Component can get destroyed and lose state
let _topicSnapshot = null; let _topicSnapshot = null;
@ -27,6 +28,7 @@ export function _clearSnapshots() {
export default DropdownSelectBoxComponent.extend({ export default DropdownSelectBoxComponent.extend({
dialog: service(), dialog: service(),
composer: service(),
seq: 0, seq: 0,
pluginApiIdentifiers: ["composer-actions"], pluginApiIdentifiers: ["composer-actions"],
classNames: ["composer-actions"], classNames: ["composer-actions"],
@ -234,14 +236,32 @@ export default DropdownSelectBoxComponent.extend({
return items; return items;
}, },
_continuedFromText(post, topic) {
let url = post?.url || topic?.url;
const topicTitle = topic?.title;
if (!url || !topicTitle) {
return;
}
url = `${location.protocol}//${location.host}${url}`;
const link = `[${escapeExpression(topicTitle)}](${url})`;
return I18n.t("post.continue_discussion", {
postLink: link,
});
},
_replyFromExisting(options, post, topic) { _replyFromExisting(options, post, topic) {
this.closeComposer(); this.composer.closeComposer();
this.openComposer(options, post, topic); this.composer.open({
...options,
prependText: this._continuedFromText(post, topic),
});
}, },
_openComposer(options) { _openComposer(options) {
this.closeComposer(); this.composer.closeComposer();
this.openComposer(options); this.composer.open(options);
}, },
toggleWhisperSelected(options, model) { toggleWhisperSelected(options, model) {

View File

@ -41,13 +41,9 @@ describe "Default to Subcategory when parent Category doesn't allow posting", ty
end end
end end
describe "Category does not have subcategory" do describe "Category does not have subcategory" do
it "should have the 'New Topic' button enabled and default Subcategory set to latest default subcategory" do it "should have the 'New Topic' button disabled" do
category_page.visit(category_with_no_subcategory) category_page.visit(category_with_no_subcategory)
expect(category_page).to have_button("New Topic", disabled: false) expect(category_page).to have_button("New Topic", disabled: true)
category_page.new_topic_button.click
select_kit =
PageObjects::Components::SelectKit.new("#reply-control.open .category-chooser")
expect(select_kit).to have_selected_value(default_latest_category.id)
end end
end end
end end
@ -70,13 +66,9 @@ describe "Default to Subcategory when parent Category doesn't allow posting", ty
end end
describe "Can't post on parent category" do describe "Can't post on parent category" do
describe "Category does not have subcategory" do describe "Category does not have subcategory" do
it "should have the 'New Topic' button enabled and default Subcategory not set" do it "should have the 'New Topic' button disabled" do
category_page.visit(category_with_no_subcategory) category_page.visit(category_with_no_subcategory)
expect(category_page).to have_button("New Topic", disabled: false) expect(category_page).to have_button("New Topic", disabled: true)
category_page.new_topic_button.click
select_kit =
PageObjects::Components::SelectKit.new("#reply-control.open .category-chooser")
expect(select_kit).to have_selected_name("category&hellip;")
end end
end end
end end