From a820be117cd508e6df0e14b75c31eeca607347fc Mon Sep 17 00:00:00 2001
From: Isaac Janzen <50783505+janzenisaac@users.noreply.github.com>
Date: Thu, 3 Aug 2023 10:52:04 -0500
Subject: [PATCH] DEV: Convert `install-theme` modal to component-based API
(#22939)
---
.../addon/components/modal/install-theme.hbs | 198 ++++++++++++++
.../addon/components/modal/install-theme.js | 229 ++++++++++++++++
.../controllers/modals/admin-install-theme.js | 249 ------------------
.../addon/routes/admin-customize-themes.js | 64 +++--
.../templates/modal/admin-install-theme.hbs | 186 -------------
.../discourse/app/services/modal.js | 1 -
.../admin-install-theme-modal-test.js | 1 +
7 files changed, 475 insertions(+), 453 deletions(-)
create mode 100644 app/assets/javascripts/admin/addon/components/modal/install-theme.hbs
create mode 100644 app/assets/javascripts/admin/addon/components/modal/install-theme.js
delete mode 100644 app/assets/javascripts/admin/addon/controllers/modals/admin-install-theme.js
delete mode 100644 app/assets/javascripts/admin/addon/templates/modal/admin-install-theme.hbs
diff --git a/app/assets/javascripts/admin/addon/components/modal/install-theme.hbs b/app/assets/javascripts/admin/addon/components/modal/install-theme.hbs
new file mode 100644
index 00000000000..b9d55e46bfb
--- /dev/null
+++ b/app/assets/javascripts/admin/addon/components/modal/install-theme.hbs
@@ -0,0 +1,198 @@
+
+ <:body>
+ {{#unless this.directRepoInstall}}
+
+
+
+
+
+
+ {{/unless}}
+
+ {{#if this.popular}}
+
+ {{#each this.themes as |theme|}}
+
+ {{/each}}
+
+ {{/if}}
+ {{#if this.local}}
+
+
+
+
+ {{i18n "admin.customize.theme.import_file_tip"}}
+
+
+ {{/if}}
+ {{#if this.remote}}
+
+ {{/if}}
+ {{#if this.create}}
+
+ {{/if}}
+ {{#if this.directRepoInstall}}
+
+
+ {{html-safe
+ (i18n
+ "admin.customize.theme.direct_install_tip" name=this.uploadName
+ )
+ }}
+
+
{{this.uploadUrl}}
+
+ {{/if}}
+
+
+ <:footer>
+ {{#unless this.popular}}
+
+ {{/unless}}
+
+
\ No newline at end of file
diff --git a/app/assets/javascripts/admin/addon/components/modal/install-theme.js b/app/assets/javascripts/admin/addon/components/modal/install-theme.js
new file mode 100644
index 00000000000..87750516e69
--- /dev/null
+++ b/app/assets/javascripts/admin/addon/components/modal/install-theme.js
@@ -0,0 +1,229 @@
+import Component from "@glimmer/component";
+import { tracked } from "@glimmer/tracking";
+import { action } from "@ember/object";
+import { COMPONENTS, THEMES } from "admin/models/theme";
+import { POPULAR_THEMES } from "discourse-common/lib/popular-themes";
+import { ajax } from "discourse/lib/ajax";
+import I18n from "I18n";
+import { inject as service } from "@ember/service";
+import { popupAjaxError } from "discourse/lib/ajax-error";
+
+const MIN_NAME_LENGTH = 4;
+
+export default class InstallTheme extends Component {
+ @service store;
+
+ @tracked selection = this.args.model.selection || "popular";
+ @tracked uploadUrl = this.args.model.uploadUrl;
+ @tracked uploadName = this.args.model.uploadName;
+ @tracked selectedType = this.args.model.selectedType;
+ @tracked advancedVisible = false;
+ @tracked loading = false;
+ @tracked localFile;
+ @tracked publicKey;
+ @tracked branch;
+ @tracked duplicateRemoteThemeWarning;
+ @tracked themeCannotBeInstalled;
+ @tracked name;
+
+ recordType = "theme";
+
+ get createTypes() {
+ return [
+ { name: I18n.t("admin.customize.theme.theme"), value: THEMES },
+ { name: I18n.t("admin.customize.theme.component"), value: COMPONENTS },
+ ];
+ }
+
+ get showPublicKey() {
+ return this.uploadUrl?.match?.(/^ssh:\/\/.+@.+$|.+@.+:.+$/);
+ }
+
+ get submitLabel() {
+ if (this.themeCannotBeInstalled) {
+ return "admin.customize.theme.create_placeholder";
+ }
+
+ return `admin.customize.theme.${this.create ? "create" : "install"}`;
+ }
+
+ get component() {
+ return this.selectedType === COMPONENTS;
+ }
+
+ get local() {
+ return this.selection === "local";
+ }
+
+ get remote() {
+ return this.selection === "remote";
+ }
+
+ get create() {
+ return this.selection === "create";
+ }
+
+ get directRepoInstall() {
+ return this.selection === "directRepoInstall";
+ }
+
+ get popular() {
+ return this.selection === "popular";
+ }
+
+ get nameTooShort() {
+ return !this.name || this.name.length < MIN_NAME_LENGTH;
+ }
+
+ get installDisabled() {
+ return (
+ this.loading ||
+ (this.remote && !this.uploadUrl) ||
+ (this.local && !this.localFile) ||
+ (this.create && this.nameTooShort)
+ );
+ }
+
+ get placeholder() {
+ if (this.component) {
+ return I18n.t("admin.customize.theme.component_name");
+ } else {
+ return I18n.t("admin.customize.theme.theme_name");
+ }
+ }
+
+ get themes() {
+ return POPULAR_THEMES.map((t) => {
+ if (
+ this.args.model.installedThemes.some((theme) =>
+ this.themeHasSameUrl(theme, t.value)
+ )
+ ) {
+ t.installed = true;
+ }
+ return t;
+ });
+ }
+
+ themeHasSameUrl(theme, url) {
+ const themeUrl = theme.remote_theme && theme.remote_theme.remote_url;
+ return (
+ themeUrl &&
+ url &&
+ url.replace(/\.git$/, "") === themeUrl.replace(/\.git$/, "")
+ );
+ }
+
+ willDestroy() {
+ this.args.model.clearParams?.();
+ }
+
+ @action
+ async generatePublicKey() {
+ try {
+ const pair = await ajax("/admin/themes/generate_key_pair", {
+ type: "POST",
+ });
+ this.publicKey = pair.public_key;
+ } catch (e) {
+ popupAjaxError(e);
+ }
+ }
+
+ @action
+ toggleAdvanced() {
+ this.advancedVisible = !this.advancedVisible;
+ }
+
+ @action
+ uploadLocaleFile(event) {
+ this.localFile = event.target.files[0];
+ }
+
+ @action
+ updateSelectedType(type) {
+ this.args.model.updateSelectedType(type);
+ this.selectedType = type;
+ }
+
+ @action
+ installThemeFromList(url) {
+ this.uploadUrl = url;
+ this.installTheme();
+ }
+
+ @action
+ async installTheme() {
+ if (this.create) {
+ this.loading = true;
+ const theme = this.store.createRecord(this.recordType);
+ try {
+ await theme.save({ name: this.name, component: this.component });
+ this.args.model.addTheme(theme);
+ this.args.closeModal();
+ } catch {
+ popupAjaxError;
+ } finally {
+ this.loading = false;
+ }
+ return;
+ }
+
+ let options = {
+ type: "POST",
+ };
+
+ if (this.local) {
+ options.processData = false;
+ options.contentType = false;
+ options.data = new FormData();
+ options.data.append("theme", this.localFile);
+ }
+
+ if (this.remote || this.popular || this.directRepoInstall) {
+ const duplicate = this.args.model.content.find((theme) =>
+ this.themeHasSameUrl(theme, this.uploadUrl)
+ );
+ if (duplicate && !this.duplicateRemoteThemeWarning) {
+ const warning = I18n.t("admin.customize.theme.duplicate_remote_theme", {
+ name: duplicate.name,
+ });
+ this.duplicateRemoteThemeWarning = warning;
+ return;
+ }
+ options.data = {
+ remote: this.uploadUrl,
+ branch: this.branch,
+ public_key: this.publicKey,
+ };
+ }
+
+ // User knows that theme cannot be installed, but they want to continue
+ // to force install it.
+ if (this.themeCannotBeInstalled) {
+ options.data["force"] = true;
+ }
+
+ // Used by theme-creator
+ if (this.args.model.userId) {
+ options.data["user_id"] = this.args.model.userId;
+ }
+
+ try {
+ this.loading = true;
+ const result = await ajax("/admin/themes/import", options);
+ const theme = this.store.createRecord(this.recordType, result.theme);
+ this.args.model.addTheme(theme);
+ this.args.closeModal();
+ } catch (e) {
+ if (!this.publicKey || this.themeCannotBeInstalled) {
+ return popupAjaxError(e);
+ }
+ this.themeCannotBeInstalled = I18n.t(
+ "admin.customize.theme.force_install"
+ );
+ } finally {
+ this.loading = false;
+ }
+ }
+}
diff --git a/app/assets/javascripts/admin/addon/controllers/modals/admin-install-theme.js b/app/assets/javascripts/admin/addon/controllers/modals/admin-install-theme.js
deleted file mode 100644
index 500654d2dcc..00000000000
--- a/app/assets/javascripts/admin/addon/controllers/modals/admin-install-theme.js
+++ /dev/null
@@ -1,249 +0,0 @@
-import { alias, equal, match } from "@ember/object/computed";
-import { COMPONENTS, THEMES } from "admin/models/theme";
-import Controller, { inject as controller } from "@ember/controller";
-import discourseComputed from "discourse-common/utils/decorators";
-import { observes } from "@ember-decorators/object";
-import I18n from "I18n";
-import ModalFunctionality from "discourse/mixins/modal-functionality";
-import { POPULAR_THEMES } from "discourse-common/lib/popular-themes";
-import { ajax } from "discourse/lib/ajax";
-import { popupAjaxError } from "discourse/lib/ajax-error";
-import { action, set } from "@ember/object";
-
-const MIN_NAME_LENGTH = 4;
-
-export default class AdminInstallThemeController extends Controller.extend(
- ModalFunctionality
-) {
- @controller adminCustomizeThemes;
- @controller("adminCustomizeThemes") themesController;
-
- @equal("selection", "popular") popular;
- @equal("selection", "local") local;
- @equal("selection", "remote") remote;
- @equal("selection", "create") create;
- @equal("selection", "directRepoInstall") directRepoInstall;
- selection = "popular";
- loading = false;
- keyGenUrl = "/admin/themes/generate_key_pair";
- importUrl = "/admin/themes/import";
- recordType = "theme";
- @match("uploadUrl", /^ssh:\/\/.+@.+$|.+@.+:.+$/) checkPrivate;
- localFile = null;
- uploadUrl = null;
- uploadName = null;
- advancedVisible = false;
- @alias("themesController.currentTab") selectedType;
- @equal("selectedType", COMPONENTS) component;
- urlPlaceholder = "https://github.com/discourse/sample_theme";
-
- createTypes = [
- { name: I18n.t("admin.customize.theme.theme"), value: THEMES },
- { name: I18n.t("admin.customize.theme.component"), value: COMPONENTS },
- ];
-
- @discourseComputed("themesController.installedThemes")
- themes(installedThemes) {
- return POPULAR_THEMES.map((t) => {
- if (
- installedThemes.some((theme) => this.themeHasSameUrl(theme, t.value))
- ) {
- set(t, "installed", true);
- }
- return t;
- });
- }
-
- @discourseComputed(
- "loading",
- "remote",
- "uploadUrl",
- "local",
- "localFile",
- "create",
- "nameTooShort"
- )
- installDisabled(
- isLoading,
- isRemote,
- uploadUrl,
- isLocal,
- localFile,
- isCreate,
- nameTooShort
- ) {
- return (
- isLoading ||
- (isRemote && !uploadUrl) ||
- (isLocal && !localFile) ||
- (isCreate && nameTooShort)
- );
- }
-
- @discourseComputed("name")
- nameTooShort(name) {
- return !name || name.length < MIN_NAME_LENGTH;
- }
-
- @discourseComputed("component")
- placeholder(component) {
- if (component) {
- return I18n.t("admin.customize.theme.component_name");
- } else {
- return I18n.t("admin.customize.theme.theme_name");
- }
- }
-
- @observes("checkPrivate")
- privateWasChecked() {
- const checked = this.checkPrivate;
- if (checked && !this._keyLoading && !this.publicKey) {
- this._keyLoading = true;
- ajax(this.keyGenUrl, { type: "POST" })
- .then((pair) => {
- this.set("publicKey", pair.public_key);
- })
- .catch(popupAjaxError)
- .finally(() => {
- this._keyLoading = false;
- });
- }
- }
-
- @discourseComputed("selection", "themeCannotBeInstalled")
- submitLabel(selection, themeCannotBeInstalled) {
- if (themeCannotBeInstalled) {
- return "admin.customize.theme.create_placeholder";
- }
-
- return `admin.customize.theme.${
- selection === "create" ? "create" : "install"
- }`;
- }
-
- @discourseComputed("checkPrivate", "publicKey")
- showPublicKey(checkPrivate, publicKey) {
- return checkPrivate && publicKey;
- }
-
- onClose() {
- this.setProperties({
- duplicateRemoteThemeWarning: null,
- localFile: null,
- uploadUrl: null,
- publicKey: null,
- branch: null,
- selection: "popular",
- });
-
- this.themesController.setProperties({
- repoName: null,
- repoUrl: null,
- });
- }
-
- themeHasSameUrl(theme, url) {
- const themeUrl = theme.remote_theme && theme.remote_theme.remote_url;
- return (
- themeUrl &&
- url &&
- url.replace(/\.git$/, "") === themeUrl.replace(/\.git$/, "")
- );
- }
-
- @action
- uploadLocaleFile() {
- this.set("localFile", $("#file-input")[0].files[0]);
- }
-
- @action
- toggleAdvanced() {
- this.toggleProperty("advancedVisible");
- }
-
- @action
- installThemeFromList(url) {
- this.set("uploadUrl", url);
- this.send("installTheme");
- }
-
- @action
- installTheme() {
- if (this.create) {
- this.set("loading", true);
- const theme = this.store.createRecord(this.recordType);
- theme
- .save({ name: this.name, component: this.component })
- .then(() => {
- this.themesController.send("addTheme", theme);
- this.send("closeModal");
- })
- .catch(popupAjaxError)
- .finally(() => this.set("loading", false));
-
- return;
- }
-
- let options = {
- type: "POST",
- };
-
- if (this.local) {
- options.processData = false;
- options.contentType = false;
- options.data = new FormData();
- options.data.append("theme", this.localFile);
- }
-
- if (this.remote || this.popular || this.directRepoInstall) {
- const duplicate = this.themesController.model.content.find((theme) =>
- this.themeHasSameUrl(theme, this.uploadUrl)
- );
- if (duplicate && !this.duplicateRemoteThemeWarning) {
- const warning = I18n.t("admin.customize.theme.duplicate_remote_theme", {
- name: duplicate.name,
- });
- this.set("duplicateRemoteThemeWarning", warning);
- return;
- }
- options.data = {
- remote: this.uploadUrl,
- branch: this.branch,
- public_key: this.publicKey,
- };
- }
-
- // User knows that theme cannot be installed, but they want to continue
- // to force install it.
- if (this.themeCannotBeInstalled) {
- options.data["force"] = true;
- }
-
- if (this.get("model.user_id")) {
- // Used by theme-creator
- options.data["user_id"] = this.get("model.user_id");
- }
-
- this.set("loading", true);
- ajax(this.importUrl, options)
- .then((result) => {
- const theme = this.store.createRecord(this.recordType, result.theme);
- this.adminCustomizeThemes.send("addTheme", theme);
- this.send("closeModal");
- })
- .then(() => {
- this.set("publicKey", null);
- })
- .catch((error) => {
- if (!this.publicKey || this.themeCannotBeInstalled) {
- return popupAjaxError(error);
- }
-
- this.set(
- "themeCannotBeInstalled",
- I18n.t("admin.customize.theme.force_install")
- );
- })
- .finally(() => this.set("loading", false));
- }
-}
diff --git a/app/assets/javascripts/admin/addon/routes/admin-customize-themes.js b/app/assets/javascripts/admin/addon/routes/admin-customize-themes.js
index 04f1df28105..c17412b588a 100644
--- a/app/assets/javascripts/admin/addon/routes/admin-customize-themes.js
+++ b/app/assets/javascripts/admin/addon/routes/admin-customize-themes.js
@@ -1,13 +1,14 @@
import { action } from "@ember/object";
import { inject as service } from "@ember/service";
import Route from "@ember/routing/route";
-import showModal from "discourse/lib/show-modal";
import I18n from "I18n";
+import InstallThemeModal from "../components/modal/install-theme";
import { next } from "@ember/runloop";
export default class AdminCustomizeThemesRoute extends Route {
@service dialog;
@service router;
+ @service modal;
queryParams = {
repoUrl: null,
@@ -18,44 +19,73 @@ export default class AdminCustomizeThemesRoute extends Route {
return this.store.findAll("theme");
}
- @action
- routeRefreshModel() {
- this.refresh();
- }
-
setupController(controller, model) {
super.setupController(controller, model);
controller.set("editingTheme", false);
-
if (controller.repoUrl) {
next(() => {
- showModal("admin-install-theme", {
- admin: true,
- }).setProperties({
- uploadUrl: controller.repoUrl,
- uploadName: controller.repoName,
- selection: "directRepoInstall",
+ this.modal.show(InstallThemeModal, {
+ model: {
+ uploadUrl: controller.repoUrl,
+ uploadName: controller.repoName,
+ selection: "directRepoInstall",
+ clearParams: this.clearParams,
+ ...this.installThemeOptions(model),
+ },
});
});
}
}
+ installThemeOptions(model) {
+ return {
+ selectedType: this.controller.currentTab,
+ userId: model.userId,
+ content: model.content,
+ installedThemes: this.controller.installedThemes,
+ addTheme: this.addTheme,
+ updateSelectedType: this.updateSelectedType,
+ };
+ }
+
+ @action
+ routeRefreshModel() {
+ this.refresh();
+ }
+
@action
installModal() {
- const currentTheme = this.controllerFor("adminCustomizeThemes.show").model;
- if (currentTheme?.warnUnassignedComponent) {
+ const currentTheme = this.modelFor("adminCustomizeThemes");
+ if (this.currentModel?.warnUnassignedComponent) {
this.dialog.yesNoConfirm({
message: I18n.t("admin.customize.theme.unsaved_parent_themes"),
didConfirm: () => {
currentTheme.set("recentlyInstalled", false);
- showModal("admin-install-theme", { admin: true });
+ this.modal.show(InstallThemeModal, {
+ model: { ...this.installThemeOptions(currentTheme) },
+ });
},
});
} else {
- showModal("admin-install-theme", { admin: true });
+ this.modal.show(InstallThemeModal, {
+ model: { ...this.installThemeOptions(currentTheme) },
+ });
}
}
+ @action
+ updateSelectedType(type) {
+ this.controller.set("currentTab", type);
+ }
+
+ @action
+ clearParams() {
+ this.controller.setProperties({
+ repoUrl: null,
+ repoName: null,
+ });
+ }
+
@action
addTheme(theme) {
this.refresh();
diff --git a/app/assets/javascripts/admin/addon/templates/modal/admin-install-theme.hbs b/app/assets/javascripts/admin/addon/templates/modal/admin-install-theme.hbs
deleted file mode 100644
index a6add46fc11..00000000000
--- a/app/assets/javascripts/admin/addon/templates/modal/admin-install-theme.hbs
+++ /dev/null
@@ -1,186 +0,0 @@
-
- {{#unless this.directRepoInstall}}
-
-
-
-
-
-
- {{/unless}}
-
- {{#if this.popular}}
-
- {{#each this.themes as |theme|}}
-
- {{/each}}
-
- {{/if}}
-
- {{#if this.local}}
-
-
- {{i18n
- "admin.customize.theme.import_file_tip"
- }}
-
- {{/if}}
-
- {{#if this.remote}}
-
- {{/if}}
-
- {{#if this.create}}
-
- {{/if}}
-
- {{#if this.directRepoInstall}}
-
-
{{html-safe
- (i18n
- "admin.customize.theme.direct_install_tip" name=this.uploadName
- )
- }}
-
{{this.uploadUrl}}
-
- {{/if}}
-
-
-
-
-{{#unless this.popular}}
-
-{{/unless}}
\ No newline at end of file
diff --git a/app/assets/javascripts/discourse/app/services/modal.js b/app/assets/javascripts/discourse/app/services/modal.js
index fc3aae5f13d..cb2b12bd0ac 100644
--- a/app/assets/javascripts/discourse/app/services/modal.js
+++ b/app/assets/javascripts/discourse/app/services/modal.js
@@ -48,7 +48,6 @@ const KNOWN_LEGACY_MODALS = [
"tag-upload",
"topic-summary",
"user-status",
- "admin-install-theme",
"admin-penalize-user",
"admin-theme-change",
"site-setting-default-categories",
diff --git a/app/assets/javascripts/discourse/tests/acceptance/admin-install-theme-modal-test.js b/app/assets/javascripts/discourse/tests/acceptance/admin-install-theme-modal-test.js
index 68b58decea7..a3b0ddb9642 100644
--- a/app/assets/javascripts/discourse/tests/acceptance/admin-install-theme-modal-test.js
+++ b/app/assets/javascripts/discourse/tests/acceptance/admin-install-theme-modal-test.js
@@ -30,6 +30,7 @@ acceptance("Admin - Themes - Install modal", function (needs) {
await click(".create-actions .btn-primary");
await click("#remote");
+ await click(".install-theme-content .inputs .advanced-repo");
assert.strictEqual(query(urlInput).value, "", "url input is reset");
assert.strictEqual(query(branchInput).value, "", "branch input is reset");
assert.notOk(query(publicKey), "hide public key");