Compare commits

...
This repository has been archived on 2023-03-18. You can view files and clone it, but cannot push or open issues or pull requests.

4 Commits

Author SHA1 Message Date
David Taylor
0537e5086f
Update core references to controller:composer 2023-03-17 17:16:45 +00:00
David Taylor
e7764b5865
Convert composer controller to service with backwards-compatibility shims 2023-03-17 17:16:42 +00:00
David Taylor
32815293e5
Rename files
(for cleaner git history)
2023-03-17 17:05:49 +00:00
David Taylor
887af6d074
DEV: Convert composer controller to native class syntax
Actions are moved from `actions: {}` to top-level functions with `@action` decorator. Previously we had a `save()` action and a top-level function of the same name, so this commit centralizes the logic from both into one function.
2023-03-17 17:04:36 +00:00
26 changed files with 2169 additions and 2077 deletions

View File

@ -101,6 +101,12 @@ const DEPRECATED_MODULES = new Map(
dropFrom: "3.0.0", dropFrom: "3.0.0",
silent: true, silent: true,
}, },
"controller:composer": {
newName: "service:composer",
since: "3.1.0.beta3",
dropFrom: "3.2.0",
silent: true,
},
}) })
); );

View File

@ -0,0 +1,422 @@
<ComposerBody
@composer={{this.composer.model}}
@showPreview={{this.composer.showPreview}}
@openIfDraft={{this.composer.openIfDraft}}
@typed={{this.composer.typed}}
@cancelled={{this.composer.cancelled}}
@save={{this.composer.save}}
>
<div class="grippie"></div>
{{#if this.composer.visible}}
<ComposerMessages
@composer={{this.composer.model}}
@messageCount={{this.composer.messageCount}}
@addLinkLookup={{this.composer.addLinkLookup}}
/>
{{#if this.composer.showFullScreenPrompt}}
<ComposerFullscreenPrompt
@removeFullScreenExitPrompt={{this.composer.removeFullScreenExitPrompt}}
/>
{{/if}}
{{#if this.composer.model.viewOpenOrFullscreen}}
<div
role="form"
aria-label={{I18n this.composer.saveLabel}}
class="reply-area
{{if this.composer.canEditTags 'with-tags' 'without-tags'}}"
>
<span>
<PluginOutlet
@name="composer-open"
@connectorTagName="div"
@outletArgs={{hash model=this.composer.model}}
/>
</span>
<div class="reply-to">
{{#unless this.composer.model.viewFullscreen}}
<div class="reply-details">
<ComposerActionTitle
@model={{this.composer.model}}
@openComposer={{this.composer.openComposer}}
@closeComposer={{this.composer.closeComposer}}
@canWhisper={{this.composer.canWhisper}}
/>
<PluginOutlet
@name="composer-action-after"
@outletArgs={{hash model=this.composer.model}}
/>
{{#unless this.composer.site.mobileView}}
{{#if this.composer.model.unlistTopic}}
<span class="unlist">({{i18n "composer.unlist"}})</span>
{{/if}}
{{#if this.composer.isWhispering}}
{{#if this.composer.model.noBump}}
<span class="no-bump">{{d-icon "anchor"}}</span>
{{/if}}
{{/if}}
{{/unless}}
{{#if this.composer.canEdit}}
<LinkToInput
@onClick={{this.composer.displayEditReason}}
@showInput={{this.composer.showEditReason}}
@icon="info-circle"
@class="display-edit-reason"
>
<TextField
@value={{this.composer.editReason}}
@id="edit-reason"
@maxlength="255"
@placeholderKey="composer.edit_reason_placeholder"
/>
</LinkToInput>
{{/if}}
</div>
{{/unless}}
<PluginOutlet
@name="before-composer-controls"
@outletArgs={{hash model=this.composer.model}}
/>
<ComposerToggles
@composeState={{this.composer.model.composeState}}
@showToolbar={{this.composer.showToolbar}}
@toggleComposer={{this.composer.toggle}}
@toggleToolbar={{this.composer.toggleToolbar}}
@toggleFullscreen={{this.composer.fullscreenComposer}}
@disableTextarea={{this.composer.disableTextarea}}
/>
</div>
<ComposerEditor
@topic={{this.composer.topic}}
@composer={{this.composer.model}}
@lastValidatedAt={{this.composer.lastValidatedAt}}
@canWhisper={{this.composer.canWhisper}}
@storeToolbarState={{this.composer.storeToolbarState}}
@onPopupMenuAction={{this.composer.onPopupMenuAction}}
@showUploadModal={{route-action "showUploadSelector"}}
@popupMenuOptions={{this.composer.popupMenuOptions}}
@draftStatus={{this.composer.model.draftStatus}}
@isUploading={{this.composer.isUploading}}
@isProcessingUpload={{this.composer.isProcessingUpload}}
@allowUpload={{this.composer.allowUpload}}
@uploadIcon={{this.composer.uploadIcon}}
@isCancellable={{this.composer.isCancellable}}
@uploadProgress={{this.composer.uploadProgress}}
@groupsMentioned={{this.composer.groupsMentioned}}
@cannotSeeMention={{this.composer.cannotSeeMention}}
@hereMention={{this.composer.hereMention}}
@importQuote={{this.composer.importQuote}}
@togglePreview={{this.composer.togglePreview}}
@processPreview={{this.composer.showPreview}}
@showToolbar={{this.composer.showToolbar}}
@afterRefresh={{this.composer.afterRefresh}}
@focusTarget={{this.composer.focusTarget}}
@disableTextarea={{this.composer.disableTextarea}}
>
<div class="composer-fields">
<PluginOutlet
@name="before-composer-fields"
@outletArgs={{hash model=this.composer.model}}
/>
{{#unless this.composer.model.viewFullscreen}}
{{#if this.composer.model.canEditTitle}}
{{#if this.composer.model.creatingPrivateMessage}}
<div class="user-selector">
<ComposerUserSelector
@topicId={{this.composer.topicModel.id}}
@recipients={{this.composer.model.targetRecipients}}
@hasGroups={{this.composer.model.hasTargetGroups}}
@focusTarget={{this.composer.focusTarget}}
@class={{concat
"users-input"
(if this.composer.showWarning " can-warn")
}}
/>
{{#if this.composer.showWarning}}
<label class="add-warning">
<Input
@type="checkbox"
@checked={{this.composer.model.isWarning}}
/>
<span>{{i18n "composer.add_warning"}}</span>
</label>
{{/if}}
</div>
{{/if}}
<div
class="title-and-category
{{if this.composer.showPreview 'with-preview'}}"
>
<ComposerTitle
@composer={{this.composer.model}}
@lastValidatedAt={{this.composer.lastValidatedAt}}
@focusTarget={{this.composer.focusTarget}}
/>
{{#if this.composer.model.showCategoryChooser}}
<div class="category-input">
<CategoryChooser
@value={{this.composer.model.categoryId}}
@onChange={{action
(mut this.composer.model.categoryId)
}}
@options={{hash
disabled=this.composer.disableCategoryChooser
scopedCategoryId=this.composer.scopedCategoryId
prioritizedCategoryId=this.composer.prioritizedCategoryId
}}
/>
<PopupInputTip
@validation={{this.composer.categoryValidation}}
/>
</div>
{{/if}}
{{#if this.composer.canEditTags}}
<MiniTagChooser
@value={{this.composer.model.tags}}
@onChange={{action (mut this.composer.model.tags)}}
@options={{hash
disabled=this.composer.disableTagsChooser
categoryId=this.composer.model.categoryId
minimum=this.composer.model.minimumRequiredTags
}}
/>
<PopupInputTip
@validation={{this.composer.tagValidation}}
/>
{{/if}}
<PluginOutlet
@name="after-title-and-category"
@outletArgs={{hash
model=this.composer.model
tagValidation=this.composer.tagValidation
canEditTags=this.composer.canEditTags
disabled=this.composer.disableTagsChooser
}}
/>
</div>
{{/if}}
<span>
<PluginOutlet
@name="composer-fields"
@connectorTagName="div"
@outletArgs={{hash
model=this.composer.model
showPreview=this.composer.showPreview
}}
/>
</span>
{{/unless}}
</div>
</ComposerEditor>
<span>
<PluginOutlet
@name="composer-after-composer-editor"
@outletArgs={{hash model=this.composer.model}}
/>
</span>
<div class="submit-panel">
<span>
<PluginOutlet
@name="composer-fields-below"
@connectorTagName="div"
@outletArgs={{hash model=this.composer.model}}
/>
</span>
<div class="save-or-cancel">
<ComposerSaveButton
@action={{this.composer.save}}
@icon={{this.composer.saveIcon}}
@label={{this.composer.saveLabel}}
@forwardEvent={{true}}
@disableSubmit={{this.composer.disableSubmit}}
/>
{{#if this.composer.site.mobileView}}
<a
href
{{on "click" this.composer.cancel}}
title={{i18n "cancel"}}
class="cancel"
>
{{#if this.composer.canEdit}}
{{d-icon "times"}}
{{else}}
{{d-icon "far-trash-alt"}}
{{/if}}
</a>
{{else}}
<a href {{on "click" this.composer.cancel}} class="cancel">{{i18n
"close"
}}</a>
{{/if}}
{{#if this.composer.site.mobileView}}
{{#if this.composer.whisperOrUnlistTopic}}
<span class="whisper">
{{d-icon "far-eye-slash"}}
</span>
{{/if}}
{{#if this.composer.model.noBump}}
<span class="no-bump">{{d-icon "anchor"}}</span>
{{/if}}
{{/if}}
{{#if
(or this.composer.isUploading this.composer.isProcessingUpload)
}}
<div id="file-uploading">
{{#if this.composer.isProcessingUpload}}
{{loading-spinner size="small"}}<span>{{i18n
"upload_selector.processing"
}}</span>
{{else}}
{{loading-spinner size="small"}}<span>{{i18n
"upload_selector.uploading"
}}
{{this.composer.uploadProgress}}%</span>
{{/if}}
{{#if this.composer.isCancellable}}
<a
href
id="cancel-file-upload"
{{on "click" this.composer.cancelUpload}}
>{{d-icon "times"}}</a>
{{/if}}
</div>
{{/if}}
<div
class={{if this.composer.isUploading "hidden"}}
id="draft-status"
>
{{#if this.composer.model.draftStatus}}
<span
class="draft-error"
title={{this.composer.model.draftStatus}}
>
{{#if this.composer.model.draftConflictUser}}
{{avatar
this.composer.model.draftConflictUser
imageSize="small"
}}
{{d-icon "user-edit"}}
{{else}}
{{d-icon "exclamation-triangle"}}
{{/if}}
{{#unless this.composer.site.mobileView}}
{{this.composer.model.draftStatus}}
{{/unless}}
</span>
{{/if}}
</div>
<span>
<PluginOutlet
@name="composer-after-save-or-cancel"
@outletArgs={{hash model=this.composer.model}}
/>
</span>
</div>
{{#if this.composer.site.mobileView}}
<span>
<PluginOutlet
@name="composer-mobile-buttons-bottom"
@outletArgs={{hash model=this.composer.model}}
/>
</span>
{{#if this.composer.allowUpload}}
<a
id="mobile-file-upload"
class="btn btn-default no-text mobile-file-upload
{{if this.composer.isUploading 'hidden'}}"
aria-label={{i18n "composer.upload_title"}}
>
{{d-icon this.composer.uploadIcon}}
</a>
{{/if}}
<a
href
class="btn btn-default no-text mobile-preview"
title={{i18n "composer.show_preview"}}
{{on "click" this.composer.togglePreview}}
aria-label={{i18n "preview"}}
>
{{d-icon "desktop"}}
</a>
{{#if this.composer.showPreview}}
<DButton
@action={{this.composer.togglePreview}}
@class="hide-preview"
@ariaLabel="composer.hide_preview"
@icon="pencil-alt"
/>
{{/if}}
{{else}}
<DButton
@action={{this.composer.togglePreview}}
@translatedTitle={{this.composer.toggleText}}
@icon="angle-double-left"
@class={{concat
"btn-flat btn-mini-toggle toggle-preview "
(unless this.composer.showPreview "active")
}}
/>
{{/if}}
</div>
</div>
{{else}}
<div class="saving-text">
{{#if this.composer.model.createdPost}}
{{i18n "composer.saved"}}
<a
href={{this.composer.createdPost.url}}
{{on "click" this.composer.viewNewReply}}
class="permalink"
>{{i18n "composer.view_new_post"}}</a>
{{else}}
{{i18n "composer.saving"}}
{{loading-spinner size="small"}}
{{/if}}
</div>
<div class="draft-text">
{{#if this.composer.model.topic}}
{{d-icon "share"}}
{{html-safe this.composer.draftTitle}}
{{else}}
{{i18n "composer.saved_draft"}}
{{/if}}
</div>
<ComposerToggles
@composeState={{this.composer.model.composeState}}
@toggleFullscreen={{this.composer.openIfDraft}}
@toggleComposer={{this.composer.toggle}}
@toggleToolbar={{this.composer.toggleToolbar}}
/>
{{/if}}
{{/if}}
</ComposerBody>

View File

@ -0,0 +1,6 @@
import Component from "@glimmer/component";
import { inject as service } from "@ember/service";
export default class ComposerContainer extends Component {
@service composer;
}

View File

@ -28,7 +28,7 @@ export default Component.extend({
dismiss() { dismiss() {
this.set("shownAt", null); this.set("shownAt", null);
const composer = getOwner(this).lookup("controller:composer"); const composer = getOwner(this).lookup("service:composer");
composer.clearLastValidatedAt(); composer.clearLastValidatedAt();
this.element.previousElementSibling?.focus(); this.element.previousElementSibling?.focus();
}, },

View File

@ -68,7 +68,7 @@ export default class SidebarUserCommunitySection extends SidebarCommonCommunityS
} }
next(() => { next(() => {
getOwner(this).lookup("controller:composer").open(composerArgs); getOwner(this).lookup("service:composer").open(composerArgs);
}); });
} }
} }

View File

@ -100,7 +100,7 @@ export default Component.extend(LoadMore, {
}, },
resumeDraft(item) { resumeDraft(item) {
const composer = getOwner(this).lookup("controller:composer"); const composer = getOwner(this).lookup("service:composer");
if (composer.get("model.viewOpen")) { if (composer.get("model.viewOpen")) {
composer.close(); composer.close();
} }

File diff suppressed because it is too large Load Diff

View File

@ -22,6 +22,7 @@ import { Promise } from "rsvp";
import { search as searchCategoryTag } from "discourse/lib/category-tag-search"; import { search as searchCategoryTag } from "discourse/lib/category-tag-search";
import showModal from "discourse/lib/show-modal"; import showModal from "discourse/lib/show-modal";
import userSearch from "discourse/lib/user-search"; import userSearch from "discourse/lib/user-search";
import { inject as service } from "@ember/service";
const SortOrders = [ const SortOrders = [
{ name: I18n.t("search.relevance"), id: 0 }, { name: I18n.t("search.relevance"), id: 0 },
@ -39,7 +40,7 @@ const PAGE_LIMIT = 10;
export default Controller.extend({ export default Controller.extend({
application: controller(), application: controller(),
composer: controller(), composer: service(),
bulkSelectEnabled: null, bulkSelectEnabled: null,
loading: false, loading: false,

View File

@ -47,7 +47,7 @@ export function registerCustomPostMessageCallback(type, callback) {
} }
export default Controller.extend(bufferedProperty("model"), { export default Controller.extend(bufferedProperty("model"), {
composer: controller(), composer: service(),
application: controller(), application: controller(),
dialog: service(), dialog: service(),
documentTitle: service(), documentTitle: service(),

View File

@ -445,14 +445,14 @@ export default {
return; return;
} }
this.container.lookup("controller:composer").open({ this.container.lookup("service:composer").open({
action: Composer.CREATE_TOPIC, action: Composer.CREATE_TOPIC,
draftKey: Composer.NEW_TOPIC_KEY, draftKey: Composer.NEW_TOPIC_KEY,
}); });
}, },
focusComposer(event) { focusComposer(event) {
const composer = this.container.lookup("controller:composer"); const composer = this.container.lookup("service:composer");
if (event) { if (event) {
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
@ -461,7 +461,7 @@ export default {
}, },
fullscreenComposer() { fullscreenComposer() {
const composer = this.container.lookup("controller:composer"); const composer = this.container.lookup("service:composer");
if (composer.get("model")) { if (composer.get("model")) {
composer.toggleFullscreen(); composer.toggleFullscreen();
} }

View File

@ -58,7 +58,7 @@ import { addPluginReviewableParam } from "discourse/components/reviewable-item";
import { import {
addComposerSaveErrorCallback, addComposerSaveErrorCallback,
addPopupMenuOptionsCallback, addPopupMenuOptionsCallback,
} from "discourse/controllers/composer"; } from "discourse/services/composer";
import { addPostClassesCallback } from "discourse/widgets/post"; import { addPostClassesCallback } from "discourse/widgets/post";
import { import {
addGroupPostSmallActionCode, addGroupPostSmallActionCode,

View File

@ -410,7 +410,7 @@ const DiscourseURL = EmberObject.extend({
}, },
get isComposerOpen() { get isComposerOpen() {
return this.controllerFor("composer")?.visible; return this.container.lookup("service:composer")?.visible;
}, },
get router() { get router() {

View File

@ -1,6 +1,7 @@
// This mixin allows a route to open the composer // This mixin allows a route to open the composer
import Composer from "discourse/models/composer"; import Composer from "discourse/models/composer";
import Mixin from "@ember/object/mixin"; import Mixin from "@ember/object/mixin";
import { getOwner } from "discourse-common/lib/get-owner";
export default Mixin.create({ export default Mixin.create({
openComposer(controller) { openComposer(controller) {
@ -13,13 +14,15 @@ export default Mixin.create({
categoryId = null; categoryId = null;
} }
this.controllerFor("composer").open({ getOwner(this)
prioritizedCategoryId: categoryId, .lookup("service:composer")
topicCategoryId: categoryId, .open({
action: Composer.CREATE_TOPIC, prioritizedCategoryId: categoryId,
draftKey: controller.get("model.draft_key") || Composer.NEW_TOPIC_KEY, topicCategoryId: categoryId,
draftSequence: controller.get("model.draft_sequence") || 0, action: Composer.CREATE_TOPIC,
}); draftKey: controller.get("model.draft_key") || Composer.NEW_TOPIC_KEY,
draftSequence: controller.get("model.draft_sequence") || 0,
});
}, },
openComposerWithTopicParams( openComposerWithTopicParams(
@ -29,15 +32,17 @@ export default Mixin.create({
topicCategoryId, topicCategoryId,
topicTags topicTags
) { ) {
this.controllerFor("composer").open({ getOwner(this)
action: Composer.CREATE_TOPIC, .lookup("service:composer")
topicTitle, .open({
topicBody, action: Composer.CREATE_TOPIC,
topicCategoryId, topicTitle,
topicTags, topicBody,
draftKey: controller.get("model.draft_key") || Composer.NEW_TOPIC_KEY, topicCategoryId,
draftSequence: controller.get("model.draft_sequence"), topicTags,
}); draftKey: controller.get("model.draft_key") || Composer.NEW_TOPIC_KEY,
draftSequence: controller.get("model.draft_sequence"),
});
}, },
openComposerWithMessageParams({ openComposerWithMessageParams({
@ -46,7 +51,7 @@ export default Mixin.create({
topicBody = "", topicBody = "",
hasGroups = false, hasGroups = false,
} = {}) { } = {}) {
this.controllerFor("composer").open({ getOwner(this).lookup("service:composer").open({
action: Composer.PRIVATE_MESSAGE, action: Composer.PRIVATE_MESSAGE,
recipients, recipients,
topicTitle, topicTitle,

View File

@ -39,6 +39,7 @@ const ApplicationRoute = DiscourseRoute.extend(OpenComposer, {
shortSiteDescription: setting("short_site_description"), shortSiteDescription: setting("short_site_description"),
documentTitle: service(), documentTitle: service(),
dialog: service(), dialog: service(),
composer: service(),
actions: { actions: {
toggleAnonymous() { toggleAnonymous() {
@ -72,13 +73,6 @@ const ApplicationRoute = DiscourseRoute.extend(OpenComposer, {
this.documentTitle.setTitle(tokens.join(" - ")); this.documentTitle.setTitle(tokens.join(" - "));
}, },
postWasEnqueued(details) {
showModal("post-enqueued", {
model: details,
title: "review.approval.title",
});
},
composePrivateMessage(user, post) { composePrivateMessage(user, post) {
const recipients = user ? user.get("username") : ""; const recipients = user ? user.get("username") : "";
const reply = post const reply = post
@ -91,7 +85,7 @@ const ApplicationRoute = DiscourseRoute.extend(OpenComposer, {
: null; : null;
// used only once, one less dependency // used only once, one less dependency
return this.controllerFor("composer").open({ return this.composer.open({
action: Composer.PRIVATE_MESSAGE, action: Composer.PRIVATE_MESSAGE,
recipients, recipients,
archetypeId: "private_message", archetypeId: "private_message",
@ -258,7 +252,6 @@ const ApplicationRoute = DiscourseRoute.extend(OpenComposer, {
this.render("application"); this.render("application");
this.render("user-card", { into: "application", outlet: "user-card" }); this.render("user-card", { into: "application", outlet: "user-card" });
this.render("modal", { into: "application", outlet: "modal" }); this.render("modal", { into: "application", outlet: "modal" });
this.render("composer", { into: "application", outlet: "composer" });
}, },
handleShowLogin() { handleShowLogin() {

View File

@ -16,11 +16,13 @@ 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";
import { inject as service } from "@ember/service";
// A helper function to create a category route with parameters // A helper function to create a category route with parameters
export default (filterArg, params) => { export default (filterArg, params) => {
return DiscourseRoute.extend({ return DiscourseRoute.extend({
queryParams, queryParams,
composer: service(),
model(modelParams) { model(modelParams) {
const category = Category.findBySlugPathWithID( const category = Category.findBySlugPathWithID(
@ -209,7 +211,7 @@ export default (filterArg, params) => {
deactivate() { deactivate() {
this._super(...arguments); this._super(...arguments);
this.controllerFor("composer").set("prioritizedCategoryId", null); this.composer.set("prioritizedCategoryId", null);
this.searchService.set("searchContext", null); this.searchService.set("searchContext", null);
}, },

View File

@ -3,6 +3,7 @@ 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";
const DiscourseRoute = Route.extend({ const DiscourseRoute = Route.extend({
showFooter: false, showFooter: false,
@ -53,7 +54,7 @@ const DiscourseRoute = Route.extend({
}, },
openTopicDraft() { openTopicDraft() {
const composer = this.controllerFor("composer"); const composer = getOwner(this).lookup("service:composer");
if ( if (
composer.get("model.action") === Composer.CREATE_TOPIC && composer.get("model.action") === Composer.CREATE_TOPIC &&

View File

@ -18,11 +18,13 @@ import { setTopicList } from "discourse/lib/topic-list-tracker";
import showModal from "discourse/lib/show-modal"; import showModal from "discourse/lib/show-modal";
import { action } from "@ember/object"; import { action } from "@ember/object";
import PreloadStore from "discourse/lib/preload-store"; import PreloadStore from "discourse/lib/preload-store";
import { inject as service } from "@ember/service";
const NONE = "none"; const NONE = "none";
const ALL = "all"; const ALL = "all";
export default DiscourseRoute.extend(FilterModeMixin, { export default DiscourseRoute.extend(FilterModeMixin, {
composer: service(),
navMode: "latest", navMode: "latest",
queryParams, queryParams,
@ -214,8 +216,7 @@ export default DiscourseRoute.extend(FilterModeMixin, {
this.openTopicDraft(); this.openTopicDraft();
} else { } else {
const controller = this.controllerFor("tag.show"); const controller = this.controllerFor("tag.show");
const composerController = this.controllerFor("composer"); this.composer
composerController
.open({ .open({
categoryId: controller.category?.id, categoryId: controller.category?.id,
action: Composer.CREATE_TOPIC, action: Composer.CREATE_TOPIC,
@ -223,8 +224,8 @@ export default DiscourseRoute.extend(FilterModeMixin, {
}) })
.then(() => { .then(() => {
// Pre-fill the tags input field // Pre-fill the tags input field
if (composerController.canEditTags && controller.tag?.id) { if (this.composer.canEditTags && controller.tag?.id) {
const composerModel = this.controllerFor("composer").model; const composerModel = this.composer.model;
composerModel.set("tags", this._controllerTags(controller)); composerModel.set("tags", this._controllerTags(controller));
} }
}); });

View File

@ -5,9 +5,12 @@ import { isEmpty } from "@ember/utils";
import { isTesting } from "discourse-common/config/environment"; import { isTesting } from "discourse-common/config/environment";
import { schedule } from "@ember/runloop"; import { schedule } from "@ember/runloop";
import { action } from "@ember/object"; import { action } from "@ember/object";
import { inject as service } from "@ember/service";
// This route is used for retrieving a topic based on params // This route is used for retrieving a topic based on params
export default DiscourseRoute.extend({ export default DiscourseRoute.extend({
composer: service(),
// Avoid default model hook // Avoid default model hook
model(params) { model(params) {
params = params || {}; params = params || {};
@ -56,7 +59,6 @@ export default DiscourseRoute.extend({
} }
const topicController = this.controllerFor("topic"); const topicController = this.controllerFor("topic");
const composerController = this.controllerFor("composer");
const topic = this.modelFor("topic"); const topic = this.modelFor("topic");
const postStream = topic.postStream; const postStream = topic.postStream;
@ -105,7 +107,7 @@ export default DiscourseRoute.extend({
} }
if (!isEmpty(topic.draft)) { if (!isEmpty(topic.draft)) {
composerController.open({ this.composer.open({
draft: Draft.getLocal(topic.draft_key, topic.draft), draft: Draft.getLocal(topic.draft_key, topic.draft),
draftKey: topic.draft_key, draftKey: topic.draft_key,
draftSequence: topic.draft_sequence, draftSequence: topic.draft_sequence,

View File

@ -14,6 +14,7 @@ import PostFlag from "discourse/lib/flag-targets/post-flag";
const SCROLL_DELAY = 500; const SCROLL_DELAY = 500;
const TopicRoute = DiscourseRoute.extend({ const TopicRoute = DiscourseRoute.extend({
composer: service(),
screenTrack: service(), screenTrack: service(),
scheduledReplace: null, scheduledReplace: null,
@ -333,7 +334,7 @@ const TopicRoute = DiscourseRoute.extend({
postStream.cancelFilter(); postStream.cancelFilter();
topicController.set("multiSelect", false); topicController.set("multiSelect", false);
this.controllerFor("composer").set("topic", null); this.composer.set("topic", null);
this.screenTrack.stop(); this.screenTrack.stop();
this.appEvents.trigger("header:hide-topic"); this.appEvents.trigger("header:hide-topic");
@ -357,7 +358,7 @@ const TopicRoute = DiscourseRoute.extend({
controller.set("multiSelect", false); controller.set("multiSelect", false);
controller.get("quoteState").clear(); controller.get("quoteState").clear();
this.controllerFor("composer").set("topic", model); this.composer.set("topic", model);
this.topicTrackingState.trackIncoming("all"); this.topicTrackingState.trackIncoming("all");
// We reset screen tracking every time a topic is entered // We reset screen tracking every time a topic is entered

View File

@ -2,8 +2,11 @@ import Composer from "discourse/models/composer";
import DiscourseRoute from "discourse/routes/discourse"; import DiscourseRoute from "discourse/routes/discourse";
import Draft from "discourse/models/draft"; import Draft from "discourse/models/draft";
import { action } from "@ember/object"; import { action } from "@ember/object";
import { inject as service } from "@ember/service";
export default DiscourseRoute.extend({ export default DiscourseRoute.extend({
composer: service(),
renderTemplate() { renderTemplate() {
this.render("user/messages"); this.render("user/messages");
}, },
@ -20,11 +23,9 @@ export default DiscourseRoute.extend({
controller.set("model", model); controller.set("model", model);
if (this.currentUser) { if (this.currentUser) {
const composerController = this.controllerFor("composer");
Draft.get("new_private_message").then((data) => { Draft.get("new_private_message").then((data) => {
if (data.draft) { if (data.draft) {
composerController.open({ this.composer.open({
draft: data.draft, draft: data.draft,
draftKey: Composer.NEW_PRIVATE_MESSAGE_KEY, draftKey: Composer.NEW_PRIVATE_MESSAGE_KEY,
ignoreIfChanged: true, ignoreIfChanged: true,

File diff suppressed because it is too large Load Diff

View File

@ -86,7 +86,7 @@
{{outlet "modal"}} {{outlet "modal"}}
<DialogHolder /> <DialogHolder />
<TopicEntrance /> <TopicEntrance />
{{outlet "composer"}} <ComposerContainer />
{{#if this.showFooterNav}} {{#if this.showFooterNav}}
<FooterNav /> <FooterNav />

View File

@ -1,404 +0,0 @@
<ComposerBody
@composer={{this.model}}
@showPreview={{this.showPreview}}
@openIfDraft={{action "openIfDraft"}}
@typed={{action "typed"}}
@cancelled={{action "cancelled"}}
@save={{action "save"}}
>
<div class="grippie"></div>
{{#if this.visible}}
<ComposerMessages
@composer={{this.model}}
@messageCount={{this.messageCount}}
@addLinkLookup={{action "addLinkLookup"}}
/>
{{#if this.showFullScreenPrompt}}
<ComposerFullscreenPrompt
@removeFullScreenExitPrompt={{action "removeFullScreenExitPrompt"}}
/>
{{/if}}
{{#if this.model.viewOpenOrFullscreen}}
<div
role="form"
aria-label={{I18n this.saveLabel}}
class="reply-area {{if this.canEditTags 'with-tags' 'without-tags'}}"
>
<span>
<PluginOutlet
@name="composer-open"
@connectorTagName="div"
@outletArgs={{hash model=this.model}}
/>
</span>
<div class="reply-to">
{{#unless this.model.viewFullscreen}}
<div class="reply-details">
<ComposerActionTitle
@model={{this.model}}
@openComposer={{action "openComposer"}}
@closeComposer={{action "closeComposer"}}
@canWhisper={{this.canWhisper}}
/>
<PluginOutlet
@name="composer-action-after"
@outletArgs={{hash model=this.model}}
/>
{{#unless this.site.mobileView}}
{{#if this.model.unlistTopic}}
<span class="unlist">({{i18n "composer.unlist"}})</span>
{{/if}}
{{#if this.isWhispering}}
{{#if this.model.noBump}}
<span class="no-bump">{{d-icon "anchor"}}</span>
{{/if}}
{{/if}}
{{/unless}}
{{#if this.canEdit}}
<LinkToInput
@onClick={{action "displayEditReason"}}
@showInput={{this.showEditReason}}
@icon="info-circle"
@class="display-edit-reason"
>
<TextField
@value={{this.editReason}}
@id="edit-reason"
@maxlength="255"
@placeholderKey="composer.edit_reason_placeholder"
/>
</LinkToInput>
{{/if}}
</div>
{{/unless}}
<PluginOutlet
@name="before-composer-controls"
@outletArgs={{hash model=this.model}}
/>
<ComposerToggles
@composeState={{this.model.composeState}}
@showToolbar={{this.showToolbar}}
@toggleComposer={{action "toggle"}}
@toggleToolbar={{action "toggleToolbar"}}
@toggleFullscreen={{action "fullscreenComposer"}}
@disableTextarea={{this.disableTextarea}}
/>
</div>
<ComposerEditor
@topic={{this.topic}}
@composer={{this.model}}
@lastValidatedAt={{this.lastValidatedAt}}
@canWhisper={{this.canWhisper}}
@storeToolbarState={{action "storeToolbarState"}}
@onPopupMenuAction={{action "onPopupMenuAction"}}
@showUploadModal={{route-action "showUploadSelector"}}
@popupMenuOptions={{this.popupMenuOptions}}
@draftStatus={{this.model.draftStatus}}
@isUploading={{this.isUploading}}
@isProcessingUpload={{this.isProcessingUpload}}
@allowUpload={{this.allowUpload}}
@uploadIcon={{this.uploadIcon}}
@isCancellable={{this.isCancellable}}
@uploadProgress={{this.uploadProgress}}
@groupsMentioned={{action "groupsMentioned"}}
@cannotSeeMention={{action "cannotSeeMention"}}
@hereMention={{action "hereMention"}}
@importQuote={{action "importQuote"}}
@togglePreview={{action "togglePreview"}}
@processPreview={{this.showPreview}}
@showToolbar={{this.showToolbar}}
@afterRefresh={{action "afterRefresh"}}
@focusTarget={{this.focusTarget}}
@disableTextarea={{this.disableTextarea}}
>
<div class="composer-fields">
<PluginOutlet
@name="before-composer-fields"
@outletArgs={{hash model=this.model}}
/>
{{#unless this.model.viewFullscreen}}
{{#if this.model.canEditTitle}}
{{#if this.model.creatingPrivateMessage}}
<div class="user-selector">
<ComposerUserSelector
@topicId={{this.topicModel.id}}
@recipients={{this.model.targetRecipients}}
@hasGroups={{this.model.hasTargetGroups}}
@focusTarget={{this.focusTarget}}
@class={{concat
"users-input"
(if this.showWarning " can-warn")
}}
/>
{{#if this.showWarning}}
<label class="add-warning">
<Input
@type="checkbox"
@checked={{this.model.isWarning}}
/>
<span>{{i18n "composer.add_warning"}}</span>
</label>
{{/if}}
</div>
{{/if}}
<div
class="title-and-category
{{if this.showPreview 'with-preview'}}"
>
<ComposerTitle
@composer={{this.model}}
@lastValidatedAt={{this.lastValidatedAt}}
@focusTarget={{this.focusTarget}}
/>
{{#if this.model.showCategoryChooser}}
<div class="category-input">
<CategoryChooser
@value={{this.model.categoryId}}
@onChange={{action (mut this.model.categoryId)}}
@options={{hash
disabled=this.disableCategoryChooser
scopedCategoryId=this.scopedCategoryId
prioritizedCategoryId=this.prioritizedCategoryId
}}
/>
<PopupInputTip @validation={{this.categoryValidation}} />
</div>
{{/if}}
{{#if this.canEditTags}}
<MiniTagChooser
@value={{this.model.tags}}
@onChange={{action (mut this.model.tags)}}
@options={{hash
disabled=this.disableTagsChooser
categoryId=this.model.categoryId
minimum=this.model.minimumRequiredTags
}}
/>
<PopupInputTip @validation={{this.tagValidation}} />
{{/if}}
<PluginOutlet
@name="after-title-and-category"
@outletArgs={{hash
model=this.model
tagValidation=this.tagValidation
canEditTags=this.canEditTags
disabled=this.disableTagsChooser
}}
/>
</div>
{{/if}}
<span>
<PluginOutlet
@name="composer-fields"
@connectorTagName="div"
@outletArgs={{hash
model=this.model
showPreview=this.showPreview
}}
/>
</span>
{{/unless}}
</div>
</ComposerEditor>
<span>
<PluginOutlet
@name="composer-after-composer-editor"
@outletArgs={{hash model=this.model}}
/>
</span>
<div class="submit-panel">
<span>
<PluginOutlet
@name="composer-fields-below"
@connectorTagName="div"
@outletArgs={{hash model=this.model}}
/>
</span>
<div class="save-or-cancel">
<ComposerSaveButton
@action={{action "save"}}
@icon={{this.saveIcon}}
@label={{this.saveLabel}}
@forwardEvent={{true}}
@disableSubmit={{this.disableSubmit}}
/>
{{#if this.site.mobileView}}
<a
href
{{on "click" this.cancel}}
title={{i18n "cancel"}}
class="cancel"
>
{{#if this.canEdit}}
{{d-icon "times"}}
{{else}}
{{d-icon "far-trash-alt"}}
{{/if}}
</a>
{{else}}
<a href {{on "click" this.cancel}} class="cancel">{{i18n
"close"
}}</a>
{{/if}}
{{#if this.site.mobileView}}
{{#if this.whisperOrUnlistTopic}}
<span class="whisper">
{{d-icon "far-eye-slash"}}
</span>
{{/if}}
{{#if this.model.noBump}}
<span class="no-bump">{{d-icon "anchor"}}</span>
{{/if}}
{{/if}}
{{#if (or this.isUploading this.isProcessingUpload)}}
<div id="file-uploading">
{{#if this.isProcessingUpload}}
{{loading-spinner size="small"}}<span>{{i18n
"upload_selector.processing"
}}</span>
{{else}}
{{loading-spinner size="small"}}<span>{{i18n
"upload_selector.uploading"
}}
{{this.uploadProgress}}%</span>
{{/if}}
{{#if this.isCancellable}}
<a
href
id="cancel-file-upload"
{{on "click" this.cancelUpload}}
>{{d-icon "times"}}</a>
{{/if}}
</div>
{{/if}}
<div class={{if this.isUploading "hidden"}} id="draft-status">
{{#if this.model.draftStatus}}
<span class="draft-error" title={{this.model.draftStatus}}>
{{#if this.model.draftConflictUser}}
{{avatar this.model.draftConflictUser imageSize="small"}}
{{d-icon "user-edit"}}
{{else}}
{{d-icon "exclamation-triangle"}}
{{/if}}
{{#unless this.site.mobileView}}
{{this.model.draftStatus}}
{{/unless}}
</span>
{{/if}}
</div>
<span>
<PluginOutlet
@name="composer-after-save-or-cancel"
@outletArgs={{hash model=this.model}}
/>
</span>
</div>
{{#if this.site.mobileView}}
<span>
<PluginOutlet
@name="composer-mobile-buttons-bottom"
@outletArgs={{hash model=this.model}}
/>
</span>
{{#if this.allowUpload}}
<a
id="mobile-file-upload"
class="btn btn-default no-text mobile-file-upload
{{if this.isUploading 'hidden'}}"
aria-label={{i18n "composer.upload_title"}}
>
{{d-icon this.uploadIcon}}
</a>
{{/if}}
<a
href
class="btn btn-default no-text mobile-preview"
title={{i18n "composer.show_preview"}}
{{on "click" this.togglePreview}}
aria-label={{i18n "preview"}}
>
{{d-icon "desktop"}}
</a>
{{#if this.showPreview}}
<DButton
@action={{action "togglePreview"}}
@class="hide-preview"
@ariaLabel="composer.hide_preview"
@icon="pencil-alt"
/>
{{/if}}
{{else}}
<DButton
@action={{action "togglePreview"}}
@translatedTitle={{this.toggleText}}
@icon="angle-double-left"
@class={{concat
"btn-flat btn-mini-toggle toggle-preview "
(unless this.showPreview "active")
}}
/>
{{/if}}
</div>
</div>
{{else}}
<div class="saving-text">
{{#if this.model.createdPost}}
{{i18n "composer.saved"}}
<a
href={{this.createdPost.url}}
{{on "click" this.viewNewReply}}
class="permalink"
>{{i18n "composer.view_new_post"}}</a>
{{else}}
{{i18n "composer.saving"}}
{{loading-spinner size="small"}}
{{/if}}
</div>
<div class="draft-text">
{{#if this.model.topic}}
{{d-icon "share"}}
{{html-safe this.draftTitle}}
{{else}}
{{i18n "composer.saved_draft"}}
{{/if}}
</div>
<ComposerToggles
@composeState={{this.model.composeState}}
@toggleFullscreen={{action "openIfDraft"}}
@toggleComposer={{action "toggle"}}
@toggleToolbar={{action "toggleToolbar"}}
/>
{{/if}}
{{/if}}
</ComposerBody>

View File

@ -12,7 +12,7 @@ import I18n from "I18n";
import selectKit from "discourse/tests/helpers/select-kit-helper"; import selectKit from "discourse/tests/helpers/select-kit-helper";
import sinon from "sinon"; import sinon from "sinon";
import { test } from "qunit"; import { test } from "qunit";
import { toggleCheckDraftPopup } from "discourse/controllers/composer"; import { toggleCheckDraftPopup } from "discourse/services/composer";
import userFixtures from "discourse/tests/fixtures/user-fixtures"; import userFixtures from "discourse/tests/fixtures/user-fixtures";
import { cloneJSON } from "discourse-common/lib/object"; import { cloneJSON } from "discourse-common/lib/object";

View File

@ -7,7 +7,7 @@ import {
triggerKeyEvent, triggerKeyEvent,
visit, visit,
} from "@ember/test-helpers"; } from "@ember/test-helpers";
import { toggleCheckDraftPopup } from "discourse/controllers/composer"; import { toggleCheckDraftPopup } from "discourse/services/composer";
import { cloneJSON } from "discourse-common/lib/object"; import { cloneJSON } from "discourse-common/lib/object";
import TopicFixtures from "discourse/tests/fixtures/topic"; import TopicFixtures from "discourse/tests/fixtures/topic";
import LinkLookup from "discourse/lib/link-lookup"; import LinkLookup from "discourse/lib/link-lookup";
@ -996,7 +996,7 @@ acceptance("Composer", function (needs) {
await visit("/t/internationalization-localization/280"); await visit("/t/internationalization-localization/280");
await click("#topic-footer-buttons .create"); await click("#topic-footer-buttons .create");
this.container.lookup("controller:composer").set( this.container.lookup("service:composer").set(
"linkLookup", "linkLookup",
new LinkLookup({ new LinkLookup({
"github.com": { "github.com": {
@ -1165,7 +1165,7 @@ acceptance("Composer - Focus Open and Closed", function (needs) {
test("Focusing a composer which is not open with create topic", async function (assert) { test("Focusing a composer which is not open with create topic", async function (assert) {
await visit("/t/internationalization-localization/280"); await visit("/t/internationalization-localization/280");
const composer = this.container.lookup("controller:composer"); const composer = this.container.lookup("service:composer");
await composer.focusComposer({ fallbackToNewTopic: true }); await composer.focusComposer({ fallbackToNewTopic: true });
await settled(); await settled();
@ -1180,7 +1180,7 @@ acceptance("Composer - Focus Open and Closed", function (needs) {
test("Focusing a composer which is not open with create topic and append text", async function (assert) { test("Focusing a composer which is not open with create topic and append text", async function (assert) {
await visit("/t/internationalization-localization/280"); await visit("/t/internationalization-localization/280");
const composer = this.container.lookup("controller:composer"); const composer = this.container.lookup("service:composer");
await composer.focusComposer({ await composer.focusComposer({
fallbackToNewTopic: true, fallbackToNewTopic: true,
insertText: "this is appended", insertText: "this is appended",
@ -1202,7 +1202,7 @@ acceptance("Composer - Focus Open and Closed", function (needs) {
await visit("/"); await visit("/");
await click("#create-topic"); await click("#create-topic");
const composer = this.container.lookup("controller:composer"); const composer = this.container.lookup("service:composer");
await composer.focusComposer(); await composer.focusComposer();
await settled(); await settled();
@ -1217,7 +1217,7 @@ acceptance("Composer - Focus Open and Closed", function (needs) {
await visit("/"); await visit("/");
await click("#create-topic"); await click("#create-topic");
const composer = this.container.lookup("controller:composer"); const composer = this.container.lookup("service:composer");
await composer.focusComposer({ insertText: "this is some appended text" }); await composer.focusComposer({ insertText: "this is some appended text" });
await settled(); await settled();
@ -1239,7 +1239,7 @@ acceptance("Composer - Focus Open and Closed", function (needs) {
await fillIn(".d-editor-input", "This is a dirty reply"); await fillIn(".d-editor-input", "This is a dirty reply");
await click(".toggle-minimize"); await click(".toggle-minimize");
const composer = this.container.lookup("controller:composer"); const composer = this.container.lookup("service:composer");
await composer.focusComposer({ insertText: "this is some appended text" }); await composer.focusComposer({ insertText: "this is some appended text" });
await settled(); await settled();

View File

@ -542,7 +542,7 @@ acceptance("Tag info", function (needs) {
test("composer will not set tags if user cannot create them", async function (assert) { test("composer will not set tags if user cannot create them", async function (assert) {
await visit("/tag/planters"); await visit("/tag/planters");
await click("#create-topic"); await click("#create-topic");
let composer = this.owner.lookup("controller:composer"); let composer = this.owner.lookup("service:composer");
assert.strictEqual(composer.get("model").tags, undefined); assert.strictEqual(composer.get("model").tags, undefined);
}); });
}); });
@ -611,7 +611,7 @@ acceptance("Tag show - create topic", function (needs) {
}); });
test("composer will not set tags with all/none tags when creating topic", async function (assert) { test("composer will not set tags with all/none tags when creating topic", async function (assert) {
const composer = this.owner.lookup("controller:composer"); const composer = this.owner.lookup("service:composer");
await visit("/tag/none"); await visit("/tag/none");
await click("#create-topic"); await click("#create-topic");
@ -623,7 +623,7 @@ acceptance("Tag show - create topic", function (needs) {
}); });
test("composer will set tags from selected tag", async function (assert) { test("composer will set tags from selected tag", async function (assert) {
const composer = this.owner.lookup("controller:composer"); const composer = this.owner.lookup("service:composer");
await visit("/tag/planters"); await visit("/tag/planters");
await click("#create-topic"); await click("#create-topic");