DEV: refactor composer references on composer-container/-editor (#29629)

Most of it is removing the ComposerContainer > ComposerEditor indirect references to the composer service, so ComposerEditor now deals with the service directly.

Form template was moved from DEditor to ComposerEditor.
This commit is contained in:
Renato Atilio 2024-11-21 13:29:12 -03:00 committed by GitHub
parent 8fd2980685
commit 6e5d4ee492
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 214 additions and 220 deletions

View File

@ -102,36 +102,7 @@
/>
</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}}
@formTemplateIds={{this.composer.formTemplateIds}}
@formTemplateInitialValues={{this.composer.formTemplateInitialValues}}
@onSelectFormTemplate={{this.composer.onSelectFormTemplate}}
>
<ComposerEditor>
<div class="composer-fields">
<PluginOutlet
@name="before-composer-fields"

View File

@ -1,38 +1,56 @@
<DEditor
@value={{this.composer.reply}}
@placeholder={{this.replyPlaceholder}}
@previewUpdated={{action "previewUpdated"}}
@markdownOptions={{this.markdownOptions}}
@extraButtons={{action "extraButtons"}}
@importQuote={{this.importQuote}}
@showUploadModal={{this.showUploadModal}}
@togglePreview={{this.togglePreview}}
@processPreview={{this.processPreview}}
@validation={{this.validation}}
@loading={{this.composer.loading}}
@forcePreview={{this.forcePreview}}
@showLink={{this.showLink}}
@composerEvents={{true}}
@onExpandPopupMenuOptions={{action "onExpandPopupMenuOptions"}}
@onPopupMenuAction={{this.onPopupMenuAction}}
@popupMenuOptions={{this.popupMenuOptions}}
@formTemplateId={{this.composer.formTemplateId}}
@formTemplateIds={{this.formTemplateIds}}
@formTemplateInitialValues={{@formTemplateInitialValues}}
@onSelectFormTemplate={{@onSelectFormTemplate}}
@replyingToTopic={{this.composer.replyingToTopic}}
@editingPost={{this.composer.editingPost}}
@disabled={{this.disableTextarea}}
@outletArgs={{hash composer=this.composer editorType="composer"}}
@topicId={{this.composer.topic.id}}
@categoryId={{this.composer.category.id}}
>
{{yield}}
</DEditor>
{{#if this.showFormTemplateForm}}
<div class="d-editor">
<div class="d-editor-container">
<div class="d-editor-textarea-column">
{{yield}}
{{#if this.allowUpload}}
{{#if (gt this.composer.formTemplateIds.length 1)}}
<FormTemplateChooser
@filteredIds={{this.composer.formTemplateIds}}
@value={{this.selectedFormTemplateId}}
@onChange={{this.updateSelectedFormTemplateId}}
@options={{hash maximum=1}}
class="composer-select-form-template"
/>
{{/if}}
<form id="form-template-form">
<FormTemplateField::Wrapper
@id={{this.selectedFormTemplateId}}
@initialValues={{this.composer.formTemplateInitialValues}}
@onSelectFormTemplate={{this.composer.onSelectFormTemplate}}
/>
</form>
</div>
</div>
</div>
{{else}}
<DEditor
@value={{this.composer.model.reply}}
@placeholder={{this.replyPlaceholder}}
@previewUpdated={{action "previewUpdated"}}
@markdownOptions={{this.markdownOptions}}
@extraButtons={{action "extraButtons"}}
@importQuote={{this.composer.importQuote}}
@processPreview={{this.composer.showPreview}}
@validation={{this.validation}}
@loading={{this.composer.loading}}
@forcePreview={{this.forcePreview}}
@showLink={{this.showLink}}
@composerEvents={{true}}
@onPopupMenuAction={{this.composer.onPopupMenuAction}}
@popupMenuOptions={{this.composer.popupMenuOptions}}
@disabled={{this.composer.disableTextarea}}
@outletArgs={{hash composer=this.composer.model editorType="composer"}}
@topicId={{this.composer.model.topic.id}}
@categoryId={{this.composer.model.category.id}}
>
{{yield}}
</DEditor>
{{/if}}
{{#if this.composer.allowUpload}}
<PickFilesButton
@fileInputId="file-uploader"
@fileInputId={{this.fileUploadElementId}}
@allowMultiple={{true}}
name="file-uploader"
/>

View File

@ -1,8 +1,8 @@
import Component from "@ember/component";
import EmberObject, { action, computed } from "@ember/object";
import { alias } from "@ember/object/computed";
import { getOwner } from "@ember/owner";
import { next, schedule, throttle } from "@ember/runloop";
import { service } from "@ember/service";
import { classNameBindings } from "@ember-decorators/component";
import { observes, on } from "@ember-decorators/object";
import { BasePlugin } from "@uppy/core";
@ -87,14 +87,15 @@ export function addApiImageWrapperButtonClickEvent(fn) {
const DEBOUNCE_FETCH_MS = 450;
const DEBOUNCE_JIT_MS = 2000;
@classNameBindings("showToolbar:toolbar-visible", ":wmd-controls")
@classNameBindings("composer.showToolbar:toolbar-visible", ":wmd-controls")
export default class ComposerEditor extends Component {
@service composer;
composerEventPrefix = "composer";
shouldBuildScrollMap = true;
scrollMap = null;
processPreview = true;
@alias("composer") composerModel;
fileUploadElementId = "file-uploader";
init() {
super.init(...arguments);
@ -103,14 +104,19 @@ export default class ComposerEditor extends Component {
this.uppyComposerUpload = new UppyComposerUpload(getOwner(this), {
composerEventPrefix: this.composerEventPrefix,
composerModel: this.composerModel,
composerModel: this.composer.model,
uploadMarkdownResolvers,
uploadPreProcessors,
uploadHandlers,
fileUploadElementId: this.fileUploadElementId,
});
}
@discourseComputed("composer.requiredCategoryMissing")
get topic() {
return this.composer.get("model.topic");
}
@discourseComputed("composer.model.requiredCategoryMissing")
replyPlaceholder(requiredCategoryMissing) {
if (requiredCategoryMissing) {
return "composer.reply_placeholder_choose_category";
@ -130,9 +136,9 @@ export default class ComposerEditor extends Component {
return this.currentUser && this.currentUser.link_posting_access !== "none";
}
@observes("focusTarget")
@observes("composer.focusTarget")
setFocus() {
if (this.focusTarget === "editor") {
if (this.composer.focusTarget === "editor") {
putCursorAtEnd(this.element.querySelector("textarea"));
}
}
@ -193,11 +199,11 @@ export default class ComposerEditor extends Component {
this._registerImageAltTextButtonClick(preview);
// Focus on the body unless we have a title
if (!this.get("composer.canEditTitle")) {
if (!this.get("composer.model.canEditTitle")) {
putCursorAtEnd(input);
}
if (this.allowUpload) {
if (this.composer.allowUpload) {
this.uppyComposerUpload.setup(this.element);
}
@ -205,11 +211,11 @@ export default class ComposerEditor extends Component {
}
@discourseComputed(
"composer.reply",
"composer.replyLength",
"composer.missingReplyCharacters",
"composer.minimumPostLength",
"lastValidatedAt"
"composer.model.reply",
"composer.model.replyLength",
"composer.model.missingReplyCharacters",
"composer.model.minimumPostLength",
"composer.lastValidatedAt"
)
validation(
reply,
@ -254,9 +260,9 @@ export default class ComposerEditor extends Component {
@computed("composer.{creatingTopic,editingFirstPost,creatingSharedDraft}")
get _isNewTopic() {
return (
this.composer.creatingTopic ||
this.composer.editingFirstPost ||
this.composer.creatingSharedDraft
this.composer.model.creatingTopic ||
this.composer.model.editingFirstPost ||
this.composer.model.creatingSharedDraft
);
}
@ -442,8 +448,8 @@ export default class ComposerEditor extends Component {
_renderUnseenMentions(preview, unseen) {
fetchUnseenMentions({
names: unseen,
topicId: this.get("composer.topic.id"),
allowedNames: this.get("composer.targetRecipients")?.split(","),
topicId: this.get("composer.model.topic.id"),
allowedNames: this.get("composer.model.targetRecipients")?.split(","),
}).then((response) => {
linkSeenMentions(preview, this.siteSettings);
this._warnMentionedGroups(preview);
@ -510,7 +516,7 @@ export default class ComposerEditor extends Component {
}
this.warnedGroupMentions.push(name);
this.groupsMentioned({
this.composer.groupsMentioned({
name,
userCount: mention.dataset.mentionableUserCount,
maxMentions: mention.dataset.maxMentions,
@ -523,7 +529,7 @@ export default class ComposerEditor extends Component {
// previously we would warn after @bob even if you were about to mention @bob2
@debounce(DEBOUNCE_JIT_MS)
_warnCannotSeeMention(preview) {
if (this.composer.draftKey === Composer.NEW_PRIVATE_MESSAGE_KEY) {
if (this.composer.model?.draftKey === Composer.NEW_PRIVATE_MESSAGE_KEY) {
return;
}
@ -534,7 +540,7 @@ export default class ComposerEditor extends Component {
}
this.warnedCannotSeeMentions.push(name);
this.cannotSeeMention({
this.composer.cannotSeeMention({
name,
reason: mention.dataset.reason,
});
@ -549,7 +555,7 @@ export default class ComposerEditor extends Component {
}
this.warnedCannotSeeMentions.push(name);
this.cannotSeeMention({
this.composer.cannotSeeMention({
name,
reason: mention.dataset.reason,
notifiedCount: mention.dataset.notifiedUserCount,
@ -563,7 +569,7 @@ export default class ComposerEditor extends Component {
return;
}
this.hereMention(hereCount);
this.composer.hereMention(hereCount);
}
@bind
@ -578,8 +584,9 @@ export default class ComposerEditor extends Component {
);
const scale = event.target.dataset.scale;
const matchingPlaceholder =
this.get("composer.reply").match(IMAGE_MARKDOWN_REGEX);
const matchingPlaceholder = this.get("composer.model.reply").match(
IMAGE_MARKDOWN_REGEX
);
if (matchingPlaceholder) {
const match = matchingPlaceholder[index];
@ -624,8 +631,9 @@ export default class ComposerEditor extends Component {
commitAltText(buttonWrapper) {
const index = parseInt(buttonWrapper.getAttribute("data-image-index"), 10);
const matchingPlaceholder =
this.get("composer.reply").match(IMAGE_MARKDOWN_REGEX);
const matchingPlaceholder = this.get("composer.model.reply").match(
IMAGE_MARKDOWN_REGEX
);
const match = matchingPlaceholder[index];
const input = buttonWrapper.querySelector("input.alt-text-input");
const replacement = match.replace(
@ -717,8 +725,9 @@ export default class ComposerEditor extends Component {
event.target.closest(".button-wrapper").dataset.imageIndex,
10
);
const matchingPlaceholder =
this.get("composer.reply").match(IMAGE_MARKDOWN_REGEX);
const matchingPlaceholder = this.get("composer.model.reply").match(
IMAGE_MARKDOWN_REGEX
);
this.appEvents.trigger(
`${this.composerEventPrefix}:replace-text`,
matchingPlaceholder[index],
@ -737,7 +746,7 @@ export default class ComposerEditor extends Component {
event.target.closest(".button-wrapper").dataset.imageIndex,
10
);
const reply = this.get("composer.reply");
const reply = this.get("composer.model.reply");
const matches = reply.match(IMAGE_MARKDOWN_REGEX);
const closingIndex =
index + parseInt(event.target.dataset.imageCount, 10) - 1;
@ -757,6 +766,10 @@ export default class ComposerEditor extends Component {
}
_registerImageAltTextButtonClick(preview) {
if (!preview) {
return;
}
preview.addEventListener("click", this._handleAltTextCancelButtonClick);
preview.addEventListener("click", this._handleAltTextEditButtonClick);
preview.addEventListener("click", this._handleAltTextOkButtonClick);
@ -775,7 +788,7 @@ export default class ComposerEditor extends Component {
const input = this.element.querySelector(".d-editor-input");
const preview = this.element.querySelector(".d-editor-preview-wrapper");
if (this.allowUpload) {
if (this.composer.allowUpload) {
this.uppyComposerUpload.teardown();
}
@ -811,11 +824,11 @@ export default class ComposerEditor extends Component {
onExpandPopupMenuOptions(toolbarEvent) {
const selected = toolbarEvent.selected;
toolbarEvent.selectText(selected.start, selected.end - selected.start);
this.storeToolbarState(toolbarEvent);
this.composer.storeToolbarState(toolbarEvent);
}
showPreview() {
this.send("togglePreview");
this.composer.togglePreview();
}
_isInQuote(element) {
@ -848,16 +861,20 @@ export default class ComposerEditor extends Component {
id: "quote",
group: "fontStyles",
icon: "far-comment",
sendAction: this.importQuote,
sendAction: this.composer.importQuote,
title: "composer.quote_post_title",
unshift: true,
});
if (this.allowUpload && this.uploadIcon && this.site.desktopView) {
if (
this.composer.allowUpload &&
this.composer.uploadIcon &&
this.site.desktopView
) {
toolbar.addButton({
id: "upload",
group: "insertions",
icon: this.uploadIcon,
icon: this.composer.uploadIcon,
title: "upload",
sendAction: this.showUploadModal,
});
@ -884,6 +901,40 @@ export default class ComposerEditor extends Component {
this._decorateCookedElement(preview);
}
this.afterRefresh(preview);
this.composer.afterRefresh(preview);
}
@computed("composer.formTemplateIds")
get selectedFormTemplateId() {
if (this._selectedFormTemplateId) {
return this._selectedFormTemplateId;
}
return (
this.composer.model.formTemplateId || this.composer.formTemplateIds?.[0]
);
}
set selectedFormTemplateId(value) {
this._selectedFormTemplateId = value;
}
@action
updateSelectedFormTemplateId(formTemplateId) {
this.selectedFormTemplateId = formTemplateId;
}
@discourseComputed(
"composer.formTemplateIds",
"composer.model.replyingToTopic",
"composer.model.editingPost"
)
showFormTemplateForm(formTemplateIds, replyingToTopic, editingPost) {
return formTemplateIds?.length > 0 && !replyingToTopic && !editingPost;
}
@action
showUploadModal() {
document.getElementById(this.fileUploadElementId).click();
}
}

View File

@ -1,81 +1,63 @@
<div class="d-editor-container">
<div class="d-editor-textarea-column">
{{yield}}
{{#if this.showFormTemplateForm}}
{{#if (gt @formTemplateIds.length 1)}}
<FormTemplateChooser
@filteredIds={{@formTemplateIds}}
@value={{this.selectedFormTemplateId}}
@onChange={{this.updateSelectedFormTemplateId}}
@options={{hash maximum=1}}
class="composer-select-form-template"
/>
{{/if}}
<form id="form-template-form">
<FormTemplateField::Wrapper
@id={{this.selectedFormTemplateId}}
@initialValues={{@formTemplateInitialValues}}
@onSelectFormTemplate={{@onSelectFormTemplate}}
/>
</form>
{{else}}
<div
class="d-editor-textarea-wrapper
{{if this.disabled 'disabled'}}
{{if this.isEditorFocused 'in-focus'}}"
>
<div class="d-editor-button-bar" role="toolbar">
{{#each this.toolbar.groups as |group|}}
{{#each group.buttons as |b|}}
{{#if (b.condition this)}}
{{#if b.popupMenu}}
<ToolbarPopupMenuOptions
@content={{this.popupMenuOptions}}
@onChange={{this.onPopupMenuAction}}
@onOpen={{action b.action b}}
@tabindex={{-1}}
@onKeydown={{this.rovingButtonBar}}
@options={{hash icon=b.icon focusAfterOnChange=false}}
class={{b.className}}
/>
{{else}}
<DButton
@action={{fn (action b.action) b}}
@translatedTitle={{b.title}}
@label={{b.label}}
@icon={{b.icon}}
@preventFocus={{b.preventFocus}}
@onKeyDown={{this.rovingButtonBar}}
tabindex={{b.tabindex}}
class={{b.className}}
/>
{{/if}}
{{/if}}
{{/each}}
{{/each}}
</div>
<ConditionalLoadingSpinner @condition={{this.loading}} />
<this.editorComponent
@onSetup={{this.setupEditor}}
@markdownOptions={{this.markdownOptions}}
@keymap={{this.keymap}}
@value={{this.value}}
@placeholder={{this.placeholderTranslated}}
@disabled={{this.disabled}}
@change={{this.change}}
@focusIn={{this.handleFocusIn}}
@focusOut={{this.handleFocusOut}}
@id={{this.textAreaId}}
/>
<PopupInputTip @validation={{this.validation}} />
<PluginOutlet
@name="after-d-editor"
@connectorTagName="div"
@outletArgs={{this.outletArgs}}
/>
<div
class="d-editor-textarea-wrapper
{{if this.disabled 'disabled'}}
{{if this.isEditorFocused 'in-focus'}}"
>
<div class="d-editor-button-bar" role="toolbar">
{{#each this.toolbar.groups as |group|}}
{{#each group.buttons as |b|}}
{{#if (b.condition this)}}
{{#if b.popupMenu}}
<ToolbarPopupMenuOptions
@content={{this.popupMenuOptions}}
@onChange={{this.onPopupMenuAction}}
@onOpen={{action b.action b}}
@tabindex={{-1}}
@onKeydown={{this.rovingButtonBar}}
@options={{hash icon=b.icon focusAfterOnChange=false}}
class={{b.className}}
/>
{{else}}
<DButton
@action={{fn (action b.action) b}}
@translatedTitle={{b.title}}
@label={{b.label}}
@icon={{b.icon}}
@preventFocus={{b.preventFocus}}
@onKeyDown={{this.rovingButtonBar}}
tabindex={{b.tabindex}}
class={{b.className}}
/>
{{/if}}
{{/if}}
{{/each}}
{{/each}}
</div>
{{/if}}
<ConditionalLoadingSpinner @condition={{this.loading}} />
<this.editorComponent
@onSetup={{this.setupEditor}}
@markdownOptions={{this.markdownOptions}}
@keymap={{this.keymap}}
@value={{this.value}}
@placeholder={{this.placeholderTranslated}}
@disabled={{this.disabled}}
@change={{this.change}}
@focusIn={{this.handleFocusIn}}
@focusOut={{this.handleFocusOut}}
@id={{this.textAreaId}}
/>
<PopupInputTip @validation={{this.validation}} />
<PluginOutlet
@name="after-d-editor"
@connectorTagName="div"
@outletArgs={{this.outletArgs}}
/>
</div>
</div>
<div

View File

@ -1,5 +1,5 @@
import Component from "@ember/component";
import { action, computed } from "@ember/object";
import { action } from "@ember/object";
import { getOwner } from "@ember/owner";
import { schedule, scheduleOnce } from "@ember/runloop";
import { service } from "@ember/service";
@ -82,30 +82,6 @@ export default class DEditor extends Component {
this.register = getRegister(this);
}
@computed("formTemplateIds")
get selectedFormTemplateId() {
if (this._selectedFormTemplateId) {
return this._selectedFormTemplateId;
}
return this.formTemplateId || this.formTemplateIds?.[0];
}
set selectedFormTemplateId(value) {
this._selectedFormTemplateId = value;
}
@action
updateSelectedFormTemplateId(formTemplateId) {
this.selectedFormTemplateId = formTemplateId;
}
@discourseComputed("formTemplateIds", "replyingToTopic", "editingPost")
showFormTemplateForm(formTemplateIds, replyingToTopic, editingPost) {
// TODO(@keegan): Remove !editingPost once we add edit/draft support for form templates
return formTemplateIds?.length > 0 && !replyingToTopic && !editingPost;
}
@discourseComputed("placeholder")
placeholderTranslated(placeholder) {
if (placeholder) {

View File

@ -44,7 +44,7 @@ export default class UppyComposerUpload {
uploadType = "composer";
editorInputClass = ".d-editor-input";
mobileFileUploaderId = "mobile-file-upload";
fileUploadElementId = "file-uploader";
fileUploadElementId;
editorClass = ".d-editor";
composerEventPrefix;
@ -73,6 +73,7 @@ export default class UppyComposerUpload {
uploadMarkdownResolvers,
uploadPreProcessors,
uploadHandlers,
fileUploadElementId,
}
) {
setOwner(this, owner);
@ -82,6 +83,7 @@ export default class UppyComposerUpload {
this.uploadMarkdownResolvers = uploadMarkdownResolvers;
this.uploadPreProcessors = uploadPreProcessors;
this.uploadHandlers = uploadHandlers;
this.fileUploadElementId = fileUploadElementId;
}
@bind

View File

@ -8,8 +8,6 @@ module("Integration | Component | ComposerEditor", function (hooks) {
setupRenderingTest(hooks);
test("warns about users that will not see a mention", async function (assert) {
const model = {};
const noop = () => {};
const expectation = (warning) => {
if (warning.name === "user-no") {
assert.deepEqual(warning, { name: "user-no", reason: "a reason" });
@ -31,24 +29,24 @@ module("Integration | Component | ComposerEditor", function (hooks) {
});
});
await render(<template>
<ComposerEditor
@composer={{model}}
@afterRefresh={{noop}}
@cannotSeeMention={{expectation}}
/>
</template>);
const originalComposerService = this.owner.lookup("service:composer");
const composerMockClass = class ComposerMock extends originalComposerService.constructor {
cannotSeeMention() {
expectation(...arguments);
}
};
this.owner.unregister("service:composer");
this.owner.register("service:composer", new composerMockClass(this.owner), {
instantiate: false,
});
await render(<template><ComposerEditor /></template>);
await fillIn("textarea", "@user-no @user-ok @user-nope");
});
test("preview sanitizes HTML", async function (assert) {
const model = {};
const noop = () => {};
await render(<template>
<ComposerEditor @composer={{model}} @afterRefresh={{noop}} />
</template>);
await render(<template><ComposerEditor /></template>);
await fillIn(".d-editor-input", `"><svg onload="prompt(/xss/)"></svg>`);
assert.dom(".d-editor-preview").hasHtml('<p>"&gt;<svg></svg></p>');

View File

@ -421,10 +421,6 @@ html.composer-open {
}
}
#file-uploader {
display: none;
}
.composer-select-form-template {
margin-bottom: 8px;
width: 100%;