mirror of
https://github.com/discourse/discourse.git
synced 2025-06-07 10:58:48 +08:00
DEV: Convert install-theme
modal to component-based API (#22939)
This commit is contained in:
@ -0,0 +1,198 @@
|
|||||||
|
<DModal
|
||||||
|
@bodyClass="install-theme"
|
||||||
|
class="admin-install-theme-modal"
|
||||||
|
@title={{i18n "admin.customize.theme.install"}}
|
||||||
|
@closeModal={{@closeModal}}
|
||||||
|
>
|
||||||
|
<:body>
|
||||||
|
{{#unless this.directRepoInstall}}
|
||||||
|
<div class="install-theme-items">
|
||||||
|
<InstallThemeItem
|
||||||
|
@value="popular"
|
||||||
|
@selection={{this.selection}}
|
||||||
|
@label="admin.customize.theme.install_popular"
|
||||||
|
/>
|
||||||
|
<InstallThemeItem
|
||||||
|
@value="local"
|
||||||
|
@selection={{this.selection}}
|
||||||
|
@label="admin.customize.theme.install_upload"
|
||||||
|
/>
|
||||||
|
<InstallThemeItem
|
||||||
|
@value="remote"
|
||||||
|
@selection={{this.selection}}
|
||||||
|
@label="admin.customize.theme.install_git_repo"
|
||||||
|
/>
|
||||||
|
<InstallThemeItem
|
||||||
|
@value="create"
|
||||||
|
@selection={{this.selection}}
|
||||||
|
@label="admin.customize.theme.install_create"
|
||||||
|
@showIcon={{true}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{{/unless}}
|
||||||
|
<div class="install-theme-content">
|
||||||
|
{{#if this.popular}}
|
||||||
|
<div class="popular-theme-items">
|
||||||
|
{{#each this.themes as |theme|}}
|
||||||
|
<div class="popular-theme-item" data-name={{theme.name}}>
|
||||||
|
<div class="popular-theme-name">
|
||||||
|
<a
|
||||||
|
href={{theme.meta_url}}
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
{{#if theme.component}}
|
||||||
|
{{d-icon
|
||||||
|
"puzzle-piece"
|
||||||
|
title="admin.customize.theme.component"
|
||||||
|
}}
|
||||||
|
{{/if}}
|
||||||
|
{{theme.name}}
|
||||||
|
</a>
|
||||||
|
<div class="popular-theme-description">
|
||||||
|
{{theme.description}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="popular-theme-buttons">
|
||||||
|
{{#if theme.installed}}
|
||||||
|
<span>{{i18n "admin.customize.theme.installed"}}</span>
|
||||||
|
{{else}}
|
||||||
|
<DButton
|
||||||
|
class="btn-small"
|
||||||
|
@label="admin.customize.theme.install"
|
||||||
|
@disabled={{this.installDisabled}}
|
||||||
|
@icon="upload"
|
||||||
|
@action={{fn this.installThemeFromList theme.value}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{{#if theme.preview}}
|
||||||
|
<a
|
||||||
|
href={{theme.preview}}
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
{{d-icon "desktop"}}
|
||||||
|
{{i18n "admin.customize.theme.preview"}}
|
||||||
|
</a>
|
||||||
|
{{/if}}
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{/each}}
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
{{#if this.local}}
|
||||||
|
<div class="inputs">
|
||||||
|
<input
|
||||||
|
{{on "change" this.uploadLocaleFile}}
|
||||||
|
type="file"
|
||||||
|
id="file-input"
|
||||||
|
accept=".dcstyle.json,application/json,.tar.gz,application/x-gzip,.zip,application/zip"
|
||||||
|
/>
|
||||||
|
<br />
|
||||||
|
<span class="description">
|
||||||
|
{{i18n "admin.customize.theme.import_file_tip"}}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
{{#if this.remote}}
|
||||||
|
<div class="inputs">
|
||||||
|
<div class="repo">
|
||||||
|
<div class="label">
|
||||||
|
{{i18n "admin.customize.theme.import_web_tip"}}
|
||||||
|
</div>
|
||||||
|
<Input
|
||||||
|
@value={{this.uploadUrl}}
|
||||||
|
placeholder={{this.urlPlaceholder}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<DButton
|
||||||
|
class="btn-small advanced-repo"
|
||||||
|
@action={{this.toggleAdvanced}}
|
||||||
|
@label="admin.customize.theme.import_web_advanced"
|
||||||
|
/>
|
||||||
|
{{#if this.advancedVisible}}
|
||||||
|
<div class="branch">
|
||||||
|
<div class="label">
|
||||||
|
{{i18n "admin.customize.theme.remote_branch"}}
|
||||||
|
</div>
|
||||||
|
<Input @value={{this.branch}} placeholder="main" />
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
{{#if this.showPublicKey}}
|
||||||
|
<div class="public-key">
|
||||||
|
<div class="label">
|
||||||
|
{{i18n "admin.customize.theme.public_key"}}
|
||||||
|
</div>
|
||||||
|
<div class="public-key-text-wrapper">
|
||||||
|
<Textarea
|
||||||
|
class="public-key-value"
|
||||||
|
readonly={{true}}
|
||||||
|
@value={{this.publicKey}}
|
||||||
|
{{did-insert this.generatePublicKey}}
|
||||||
|
/>
|
||||||
|
<CopyButton @selector="textarea.public-key-value" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
{{#if this.create}}
|
||||||
|
<div class="inputs">
|
||||||
|
<div class="label">{{i18n "admin.customize.theme.create_name"}}</div>
|
||||||
|
<Input @value={{this.name}} placeholder={{this.placeholder}} />
|
||||||
|
<div class="label">{{i18n "admin.customize.theme.create_type"}}</div>
|
||||||
|
<ComboBox
|
||||||
|
@valueProperty="value"
|
||||||
|
@content={{this.createTypes}}
|
||||||
|
@value={{this.selectedType}}
|
||||||
|
@onChange={{this.updateSelectedType}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
{{#if this.directRepoInstall}}
|
||||||
|
<div class="repo">
|
||||||
|
<div class="label">
|
||||||
|
{{html-safe
|
||||||
|
(i18n
|
||||||
|
"admin.customize.theme.direct_install_tip" name=this.uploadName
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</div>
|
||||||
|
<pre><code>{{this.uploadUrl}}</code></pre>
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
|
</:body>
|
||||||
|
<:footer>
|
||||||
|
{{#unless this.popular}}
|
||||||
|
<div class="modal-footer">
|
||||||
|
{{#if this.duplicateRemoteThemeWarning}}
|
||||||
|
<div class="install-theme-warning">
|
||||||
|
⚠️
|
||||||
|
{{this.duplicateRemoteThemeWarning}}
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
{{#if this.themeCannotBeInstalled}}
|
||||||
|
<div class="install-theme-warning">
|
||||||
|
⚠️
|
||||||
|
{{this.themeCannotBeInstalled}}
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
<DButton
|
||||||
|
@action={{this.installTheme}}
|
||||||
|
@disabled={{this.installDisabled}}
|
||||||
|
class="btn
|
||||||
|
{{if this.themeCannotBeInstalled 'btn-danger' 'btn-primary'}}"
|
||||||
|
@label={{this.submitLabel}}
|
||||||
|
/>
|
||||||
|
<DButton
|
||||||
|
class="btn-flat d-modal-cancel"
|
||||||
|
@action={{@closeModal}}
|
||||||
|
@label="cancel"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{{/unless}}
|
||||||
|
</:footer>
|
||||||
|
</DModal>
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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));
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,13 +1,14 @@
|
|||||||
import { action } from "@ember/object";
|
import { action } from "@ember/object";
|
||||||
import { inject as service } from "@ember/service";
|
import { inject as service } from "@ember/service";
|
||||||
import Route from "@ember/routing/route";
|
import Route from "@ember/routing/route";
|
||||||
import showModal from "discourse/lib/show-modal";
|
|
||||||
import I18n from "I18n";
|
import I18n from "I18n";
|
||||||
|
import InstallThemeModal from "../components/modal/install-theme";
|
||||||
import { next } from "@ember/runloop";
|
import { next } from "@ember/runloop";
|
||||||
|
|
||||||
export default class AdminCustomizeThemesRoute extends Route {
|
export default class AdminCustomizeThemesRoute extends Route {
|
||||||
@service dialog;
|
@service dialog;
|
||||||
@service router;
|
@service router;
|
||||||
|
@service modal;
|
||||||
|
|
||||||
queryParams = {
|
queryParams = {
|
||||||
repoUrl: null,
|
repoUrl: null,
|
||||||
@ -18,44 +19,73 @@ export default class AdminCustomizeThemesRoute extends Route {
|
|||||||
return this.store.findAll("theme");
|
return this.store.findAll("theme");
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
|
||||||
routeRefreshModel() {
|
|
||||||
this.refresh();
|
|
||||||
}
|
|
||||||
|
|
||||||
setupController(controller, model) {
|
setupController(controller, model) {
|
||||||
super.setupController(controller, model);
|
super.setupController(controller, model);
|
||||||
controller.set("editingTheme", false);
|
controller.set("editingTheme", false);
|
||||||
|
|
||||||
if (controller.repoUrl) {
|
if (controller.repoUrl) {
|
||||||
next(() => {
|
next(() => {
|
||||||
showModal("admin-install-theme", {
|
this.modal.show(InstallThemeModal, {
|
||||||
admin: true,
|
model: {
|
||||||
}).setProperties({
|
uploadUrl: controller.repoUrl,
|
||||||
uploadUrl: controller.repoUrl,
|
uploadName: controller.repoName,
|
||||||
uploadName: controller.repoName,
|
selection: "directRepoInstall",
|
||||||
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
|
@action
|
||||||
installModal() {
|
installModal() {
|
||||||
const currentTheme = this.controllerFor("adminCustomizeThemes.show").model;
|
const currentTheme = this.modelFor("adminCustomizeThemes");
|
||||||
if (currentTheme?.warnUnassignedComponent) {
|
if (this.currentModel?.warnUnassignedComponent) {
|
||||||
this.dialog.yesNoConfirm({
|
this.dialog.yesNoConfirm({
|
||||||
message: I18n.t("admin.customize.theme.unsaved_parent_themes"),
|
message: I18n.t("admin.customize.theme.unsaved_parent_themes"),
|
||||||
didConfirm: () => {
|
didConfirm: () => {
|
||||||
currentTheme.set("recentlyInstalled", false);
|
currentTheme.set("recentlyInstalled", false);
|
||||||
showModal("admin-install-theme", { admin: true });
|
this.modal.show(InstallThemeModal, {
|
||||||
|
model: { ...this.installThemeOptions(currentTheme) },
|
||||||
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
} else {
|
} 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
|
@action
|
||||||
addTheme(theme) {
|
addTheme(theme) {
|
||||||
this.refresh();
|
this.refresh();
|
||||||
|
@ -1,186 +0,0 @@
|
|||||||
<DModalBody @class="install-theme" @title="admin.customize.theme.install">
|
|
||||||
{{#unless this.directRepoInstall}}
|
|
||||||
<div class="install-theme-items">
|
|
||||||
<InstallThemeItem
|
|
||||||
@value="popular"
|
|
||||||
@selection={{this.selection}}
|
|
||||||
@label="admin.customize.theme.install_popular"
|
|
||||||
/>
|
|
||||||
<InstallThemeItem
|
|
||||||
@value="local"
|
|
||||||
@selection={{this.selection}}
|
|
||||||
@label="admin.customize.theme.install_upload"
|
|
||||||
/>
|
|
||||||
<InstallThemeItem
|
|
||||||
@value="remote"
|
|
||||||
@selection={{this.selection}}
|
|
||||||
@label="admin.customize.theme.install_git_repo"
|
|
||||||
/>
|
|
||||||
<InstallThemeItem
|
|
||||||
@value="create"
|
|
||||||
@selection={{this.selection}}
|
|
||||||
@label="admin.customize.theme.install_create"
|
|
||||||
@showIcon={{true}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{{/unless}}
|
|
||||||
<div class="install-theme-content">
|
|
||||||
{{#if this.popular}}
|
|
||||||
<div class="popular-theme-items">
|
|
||||||
{{#each this.themes as |theme|}}
|
|
||||||
<div class="popular-theme-item" data-name={{theme.name}}>
|
|
||||||
<div class="popular-theme-name">
|
|
||||||
<a
|
|
||||||
href={{theme.meta_url}}
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
target="_blank"
|
|
||||||
>
|
|
||||||
{{#if theme.component}}
|
|
||||||
{{d-icon
|
|
||||||
"puzzle-piece"
|
|
||||||
title="admin.customize.theme.component"
|
|
||||||
}}
|
|
||||||
{{/if}}
|
|
||||||
{{theme.name}}
|
|
||||||
</a>
|
|
||||||
<div class="popular-theme-description">
|
|
||||||
{{theme.description}}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="popular-theme-buttons">
|
|
||||||
{{#if theme.installed}}
|
|
||||||
<span>{{i18n "admin.customize.theme.installed"}}</span>
|
|
||||||
{{else}}
|
|
||||||
<DButton
|
|
||||||
@class="btn-small"
|
|
||||||
@label="admin.customize.theme.install"
|
|
||||||
@disabled={{this.installDisabled}}
|
|
||||||
@icon="upload"
|
|
||||||
@action={{action "installThemeFromList" theme.value}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{{#if theme.preview}}
|
|
||||||
<a
|
|
||||||
href={{theme.preview}}
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
target="_blank"
|
|
||||||
>{{d-icon "desktop"}}
|
|
||||||
{{i18n "admin.customize.theme.preview"}}</a>
|
|
||||||
{{/if}}
|
|
||||||
{{/if}}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{{/each}}
|
|
||||||
</div>
|
|
||||||
{{/if}}
|
|
||||||
|
|
||||||
{{#if this.local}}
|
|
||||||
<div class="inputs">
|
|
||||||
<input
|
|
||||||
onchange={{action "uploadLocaleFile"}}
|
|
||||||
type="file"
|
|
||||||
id="file-input"
|
|
||||||
accept=".dcstyle.json,application/json,.tar.gz,application/x-gzip,.zip,application/zip"
|
|
||||||
/><br />
|
|
||||||
<span class="description">{{i18n
|
|
||||||
"admin.customize.theme.import_file_tip"
|
|
||||||
}}</span>
|
|
||||||
</div>
|
|
||||||
{{/if}}
|
|
||||||
|
|
||||||
{{#if this.remote}}
|
|
||||||
<div class="inputs">
|
|
||||||
<div class="repo">
|
|
||||||
<div class="label">{{i18n
|
|
||||||
"admin.customize.theme.import_web_tip"
|
|
||||||
}}</div>
|
|
||||||
<Input
|
|
||||||
@value={{this.uploadUrl}}
|
|
||||||
placeholder={{this.urlPlaceholder}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<DButton
|
|
||||||
@class="btn-small advanced-repo"
|
|
||||||
@action={{action "toggleAdvanced"}}
|
|
||||||
@label="admin.customize.theme.import_web_advanced"
|
|
||||||
/>
|
|
||||||
|
|
||||||
{{#if this.advancedVisible}}
|
|
||||||
<div class="branch">
|
|
||||||
<div class="label">{{i18n
|
|
||||||
"admin.customize.theme.remote_branch"
|
|
||||||
}}</div>
|
|
||||||
<Input @value={{this.branch}} placeholder="main" />
|
|
||||||
</div>
|
|
||||||
{{/if}}
|
|
||||||
|
|
||||||
{{#if this.showPublicKey}}
|
|
||||||
<div class="public-key">
|
|
||||||
<div class="label">{{i18n "admin.customize.theme.public_key"}}</div>
|
|
||||||
<div class="public-key-text-wrapper">
|
|
||||||
<Textarea
|
|
||||||
class="public-key-value"
|
|
||||||
readonly={{true}}
|
|
||||||
@value={{this.publicKey}}
|
|
||||||
/>
|
|
||||||
<CopyButton @selector="textarea.public-key-value" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{{/if}}
|
|
||||||
</div>
|
|
||||||
{{/if}}
|
|
||||||
|
|
||||||
{{#if this.create}}
|
|
||||||
<div class="inputs">
|
|
||||||
<div class="label">{{i18n "admin.customize.theme.create_name"}}</div>
|
|
||||||
<Input @value={{this.name}} placeholder={{this.placeholder}} />
|
|
||||||
|
|
||||||
<div class="label">{{i18n "admin.customize.theme.create_type"}}</div>
|
|
||||||
<ComboBox
|
|
||||||
@valueProperty="value"
|
|
||||||
@content={{this.createTypes}}
|
|
||||||
@value={{this.selectedType}}
|
|
||||||
@onChange={{action (mut this.selectedType)}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{{/if}}
|
|
||||||
|
|
||||||
{{#if this.directRepoInstall}}
|
|
||||||
<div class="repo">
|
|
||||||
<div class="label">{{html-safe
|
|
||||||
(i18n
|
|
||||||
"admin.customize.theme.direct_install_tip" name=this.uploadName
|
|
||||||
)
|
|
||||||
}}</div>
|
|
||||||
<pre><code>{{this.uploadUrl}}</code></pre>
|
|
||||||
</div>
|
|
||||||
{{/if}}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</DModalBody>
|
|
||||||
|
|
||||||
{{#unless this.popular}}
|
|
||||||
<div class="modal-footer">
|
|
||||||
{{#if this.duplicateRemoteThemeWarning}}
|
|
||||||
<div class="install-theme-warning">
|
|
||||||
⚠️
|
|
||||||
{{this.duplicateRemoteThemeWarning}}
|
|
||||||
</div>
|
|
||||||
{{/if}}
|
|
||||||
{{#if this.themeCannotBeInstalled}}
|
|
||||||
<div class="install-theme-warning">
|
|
||||||
⚠️
|
|
||||||
{{this.themeCannotBeInstalled}}
|
|
||||||
</div>
|
|
||||||
{{/if}}
|
|
||||||
<DButton
|
|
||||||
@action={{action "installTheme"}}
|
|
||||||
@disabled={{this.installDisabled}}
|
|
||||||
@class="btn {{if this.themeCannotBeInstalled 'btn-danger' 'btn-primary'}}"
|
|
||||||
@label={{this.submitLabel}}
|
|
||||||
/>
|
|
||||||
<DModalCancel @close={{route-action "closeModal"}} />
|
|
||||||
</div>
|
|
||||||
{{/unless}}
|
|
@ -48,7 +48,6 @@ const KNOWN_LEGACY_MODALS = [
|
|||||||
"tag-upload",
|
"tag-upload",
|
||||||
"topic-summary",
|
"topic-summary",
|
||||||
"user-status",
|
"user-status",
|
||||||
"admin-install-theme",
|
|
||||||
"admin-penalize-user",
|
"admin-penalize-user",
|
||||||
"admin-theme-change",
|
"admin-theme-change",
|
||||||
"site-setting-default-categories",
|
"site-setting-default-categories",
|
||||||
|
@ -30,6 +30,7 @@ acceptance("Admin - Themes - Install modal", function (needs) {
|
|||||||
|
|
||||||
await click(".create-actions .btn-primary");
|
await click(".create-actions .btn-primary");
|
||||||
await click("#remote");
|
await click("#remote");
|
||||||
|
await click(".install-theme-content .inputs .advanced-repo");
|
||||||
assert.strictEqual(query(urlInput).value, "", "url input is reset");
|
assert.strictEqual(query(urlInput).value, "", "url input is reset");
|
||||||
assert.strictEqual(query(branchInput).value, "", "branch input is reset");
|
assert.strictEqual(query(branchInput).value, "", "branch input is reset");
|
||||||
assert.notOk(query(publicKey), "hide public key");
|
assert.notOk(query(publicKey), "hide public key");
|
||||||
|
Reference in New Issue
Block a user