diff --git a/app/assets/javascripts/discourse/app/components/composer-action-title.js b/app/assets/javascripts/discourse/app/components/composer-action-title.js index 851738c1586..c74d7089516 100644 --- a/app/assets/javascripts/discourse/app/components/composer-action-title.js +++ b/app/assets/javascripts/discourse/app/components/composer-action-title.js @@ -24,8 +24,15 @@ export default Component.extend({ options: alias("model.replyOptions"), action: alias("model.action"), - @discourseComputed("options", "action") + // Note we update when some other attributes like tag/category change to allow + // text customizations to use those. + @discourseComputed("options", "action", "model.tags", "model.category") actionTitle(opts, action) { + let result = this.model.customizationFor("actionTitle"); + if (result) { + return result; + } + if (TITLES[action]) { return I18n.t(TITLES[action]); } diff --git a/app/assets/javascripts/discourse/app/controllers/composer.js b/app/assets/javascripts/discourse/app/controllers/composer.js index 39418656ce1..56110de30bf 100644 --- a/app/assets/javascripts/discourse/app/controllers/composer.js +++ b/app/assets/javascripts/discourse/app/controllers/composer.js @@ -240,13 +240,22 @@ export default Controller.extend({ return SAVE_ICONS[modelAction]; }, + // Note we update when some other attributes like tag/category change to allow + // text customizations to use those. @discourseComputed( "model.action", "isWhispering", "model.editConflict", - "model.privateMessage" + "model.privateMessage", + "model.tags", + "model.category" ) saveLabel(modelAction, isWhispering, editConflict, privateMessage) { + let result = this.model.customizationFor("saveLabel"); + if (result) { + return result; + } + if (editConflict) { return "composer.overwrite_edit"; } else if (isWhispering) { diff --git a/app/assets/javascripts/discourse/app/lib/plugin-api.js b/app/assets/javascripts/discourse/app/lib/plugin-api.js index 3ecca9ea862..1c8c48ccebe 100644 --- a/app/assets/javascripts/discourse/app/lib/plugin-api.js +++ b/app/assets/javascripts/discourse/app/lib/plugin-api.js @@ -37,7 +37,9 @@ import { registerIconRenderer, replaceIcon, } from "discourse-common/lib/icon-library"; -import Composer from "discourse/models/composer"; +import Composer, { + registerCustomizationCallback, +} from "discourse/models/composer"; import DiscourseBanner from "discourse/components/discourse-banner"; import KeyboardShortcuts from "discourse/lib/keyboard-shortcuts"; import Sharing from "discourse/lib/sharing"; @@ -87,7 +89,7 @@ import { addSearchSuggestion } from "discourse/widgets/search-menu-results"; import { CUSTOM_USER_SEARCH_OPTIONS } from "select-kit/components/user-chooser"; // If you add any methods to the API ensure you bump up this number -const PLUGIN_API_VERSION = "0.12.3"; +const PLUGIN_API_VERSION = "0.12.5"; // This helper prevents us from applying the same `modifyClass` over and over in test mode. function canModify(klass, type, resolverName, changes) { @@ -1471,6 +1473,28 @@ class PluginApi { { ignoreMissing: true } ); } + + /** + * Support for customizing the composer text. By providing a callback. Callbacks should + * return `null` or `undefined` if you don't need a customization based on the current state. + * + * ``` + * api.customizeComposerText({ + * actionTitle(model) { + * if (model.hello) { + * return "hello.world"; + * } + * }, + * + * saveLabel(model) { + * return "my.custom_save_label_key"; + * } + * }) + * + */ + customizeComposerText(callbacks) { + registerCustomizationCallback(callbacks); + } } // from http://stackoverflow.com/questions/6832596/how-to-compare-software-version-number-using-js-only-number diff --git a/app/assets/javascripts/discourse/app/models/composer.js b/app/assets/javascripts/discourse/app/models/composer.js index c74a640eac5..4db89cc6677 100644 --- a/app/assets/javascripts/discourse/app/models/composer.js +++ b/app/assets/javascripts/discourse/app/models/composer.js @@ -24,6 +24,15 @@ import { isEmpty } from "@ember/utils"; import { propertyNotEqual } from "discourse/lib/computed"; import { throwAjaxError } from "discourse/lib/ajax-error"; +let _customizations = []; +export function registerCustomizationCallback(cb) { + _customizations.push(cb); +} + +export function resetComposerCustomizations() { + _customizations = []; +} + // The actions the composer can take export const CREATE_TOPIC = "createTopic", CREATE_SHARED_DRAFT = "createSharedDraft", @@ -1305,6 +1314,18 @@ const Composer = RestModel.extend({ this.set("draftSaving", false); }); }, + + customizationFor(type) { + for (let i = 0; i < _customizations.length; i++) { + let cb = _customizations[i][type]; + if (cb) { + let result = cb(this); + if (result) { + return result; + } + } + } + }, }); Composer.reopenClass({ diff --git a/app/assets/javascripts/discourse/tests/acceptance/composer-test.js b/app/assets/javascripts/discourse/tests/acceptance/composer-test.js index 5f612ef6c5d..e7d3ec797ba 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/composer-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/composer-test.js @@ -12,7 +12,8 @@ import { click, currentURL, fillIn, visit } from "@ember/test-helpers"; import { skip, test } from "qunit"; import Draft from "discourse/models/draft"; import I18n from "I18n"; -import { NEW_TOPIC_KEY } from "discourse/models/composer"; +import { CREATE_TOPIC, NEW_TOPIC_KEY } from "discourse/models/composer"; +import { withPluginApi } from "discourse/lib/plugin-api"; import { Promise } from "rsvp"; import { run } from "@ember/runloop"; import selectKit from "discourse/tests/helpers/select-kit-helper"; @@ -1008,3 +1009,51 @@ acceptance("Composer", function (needs) { assert.notOk(exists(".discard-draft-modal .save-draft")); }); }); + +acceptance("Composer - Customizations", function (needs) { + needs.user(); + needs.site({ can_tag_topics: true }); + + function customComposerAction(composer) { + return ( + (composer.tags || []).indexOf("monkey") !== -1 && + composer.action === CREATE_TOPIC + ); + } + + needs.hooks.beforeEach(() => { + withPluginApi("0.8.14", (api) => { + api.customizeComposerText({ + actionTitle(model) { + if (customComposerAction(model)) { + return "custom text"; + } + }, + + saveLabel(model) { + if (customComposerAction(model)) { + return "composer.emoji"; + } + }, + }); + }); + }); + + test("Supports text customization", async function (assert) { + await visit("/"); + await click("#create-topic"); + assert.equal(query(".action-title").innerText, I18n.t("topic.create_long")); + assert.equal( + query(".save-or-cancel button").innerText, + I18n.t("composer.create_topic") + ); + const tags = selectKit(".mini-tag-chooser"); + await tags.expand(); + await tags.selectRowByValue("monkey"); + assert.equal(query(".action-title").innerText, "custom text"); + assert.equal( + query(".save-or-cancel button").innerText, + I18n.t("composer.emoji") + ); + }); +}); diff --git a/app/assets/javascripts/discourse/tests/helpers/qunit-helpers.js b/app/assets/javascripts/discourse/tests/helpers/qunit-helpers.js index 3b2a062e4d5..8841b86e6c1 100644 --- a/app/assets/javascripts/discourse/tests/helpers/qunit-helpers.js +++ b/app/assets/javascripts/discourse/tests/helpers/qunit-helpers.js @@ -37,6 +37,7 @@ import { resetUsernameDecorators } from "discourse/helpers/decorate-username-sel import { resetWidgetCleanCallbacks } from "discourse/components/mount-widget"; import { resetUserSearchCache } from "discourse/lib/user-search"; import { resetCardClickListenerSelector } from "discourse/mixins/card-contents-base"; +import { resetComposerCustomizations } from "discourse/models/composer"; import sessionFixtures from "discourse/tests/fixtures/session-fixtures"; import { setTopicList } from "discourse/lib/topic-list-tracker"; import sinon from "sinon"; @@ -280,6 +281,7 @@ export function acceptance(name, optionsOrCallback) { resetCustomPostMessageCallbacks(); resetUserSearchCache(); resetCardClickListenerSelector(); + resetComposerCustomizations(); resetPostMenuExtraButtons(); clearNavItems(); setTopicList(null);