mirror of
https://github.com/discourse/discourse.git
synced 2025-09-06 10:50:21 +08:00
UX: List popular themes and components in admin panel (#6997)
Reorganizes theme create/upload flows into one install flow Adds quick list of popular themes/components with one-click installation
This commit is contained in:
parent
4955ab1689
commit
cafe637407
20 changed files with 556 additions and 285 deletions
|
@ -0,0 +1,3 @@
|
||||||
|
export default Ember.Component.extend({
|
||||||
|
classNames: ["install-theme-item"]
|
||||||
|
});
|
|
@ -12,5 +12,10 @@ export default Ember.Controller.extend({
|
||||||
@computed("model", "model.@each.component")
|
@computed("model", "model.@each.component")
|
||||||
childThemes(themes) {
|
childThemes(themes) {
|
||||||
return themes.filter(t => t.get("component"));
|
return themes.filter(t => t.get("component"));
|
||||||
|
},
|
||||||
|
|
||||||
|
@computed("model", "model.@each.component")
|
||||||
|
installedThemes(themes) {
|
||||||
|
return themes.map(t => t.name);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,65 +0,0 @@
|
||||||
import ModalFunctionality from "discourse/mixins/modal-functionality";
|
|
||||||
import { default as computed } from "ember-addons/ember-computed-decorators";
|
|
||||||
import { popupAjaxError } from "discourse/lib/ajax-error";
|
|
||||||
import { THEMES, COMPONENTS } from "admin/models/theme";
|
|
||||||
|
|
||||||
const MIN_NAME_LENGTH = 4;
|
|
||||||
|
|
||||||
export default Ember.Controller.extend(ModalFunctionality, {
|
|
||||||
saving: false,
|
|
||||||
triggerError: false,
|
|
||||||
themesController: Ember.inject.controller("adminCustomizeThemes"),
|
|
||||||
types: [
|
|
||||||
{ name: I18n.t("admin.customize.theme.theme"), value: THEMES },
|
|
||||||
{ name: I18n.t("admin.customize.theme.component"), value: COMPONENTS }
|
|
||||||
],
|
|
||||||
|
|
||||||
@computed("triggerError", "nameTooShort")
|
|
||||||
showError(trigger, tooShort) {
|
|
||||||
return trigger && tooShort;
|
|
||||||
},
|
|
||||||
|
|
||||||
@computed("name")
|
|
||||||
nameTooShort(name) {
|
|
||||||
return !name || name.length < MIN_NAME_LENGTH;
|
|
||||||
},
|
|
||||||
|
|
||||||
@computed("component")
|
|
||||||
placeholder(component) {
|
|
||||||
if (component) {
|
|
||||||
return I18n.t("admin.customize.theme.component_name");
|
|
||||||
} else {
|
|
||||||
return I18n.t("admin.customize.theme.theme_name");
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
@computed("themesController.currentTab")
|
|
||||||
selectedType(tab) {
|
|
||||||
return tab;
|
|
||||||
},
|
|
||||||
|
|
||||||
@computed("selectedType")
|
|
||||||
component(type) {
|
|
||||||
return type === COMPONENTS;
|
|
||||||
},
|
|
||||||
|
|
||||||
actions: {
|
|
||||||
createTheme() {
|
|
||||||
if (this.get("nameTooShort")) {
|
|
||||||
this.set("triggerError", true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.set("saving", true);
|
|
||||||
const theme = this.store.createRecord("theme");
|
|
||||||
theme
|
|
||||||
.save({ name: this.get("name"), component: this.get("component") })
|
|
||||||
.then(() => {
|
|
||||||
this.get("themesController").send("addTheme", theme);
|
|
||||||
this.send("closeModal");
|
|
||||||
})
|
|
||||||
.catch(popupAjaxError)
|
|
||||||
.finally(() => this.set("saving", false));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
|
@ -1,99 +0,0 @@
|
||||||
import ModalFunctionality from "discourse/mixins/modal-functionality";
|
|
||||||
import { ajax } from "discourse/lib/ajax";
|
|
||||||
import { popupAjaxError } from "discourse/lib/ajax-error";
|
|
||||||
import {
|
|
||||||
default as computed,
|
|
||||||
observes
|
|
||||||
} from "ember-addons/ember-computed-decorators";
|
|
||||||
|
|
||||||
export default Ember.Controller.extend(ModalFunctionality, {
|
|
||||||
local: Ember.computed.equal("selection", "local"),
|
|
||||||
remote: Ember.computed.equal("selection", "remote"),
|
|
||||||
selection: "local",
|
|
||||||
adminCustomizeThemes: Ember.inject.controller(),
|
|
||||||
loading: false,
|
|
||||||
keyGenUrl: "/admin/themes/generate_key_pair",
|
|
||||||
importUrl: "/admin/themes/import",
|
|
||||||
checkPrivate: Ember.computed.match("uploadUrl", /^git/),
|
|
||||||
localFile: null,
|
|
||||||
uploadUrl: null,
|
|
||||||
urlPlaceholder: "https://github.com/discourse/sample_theme",
|
|
||||||
advancedVisible: false,
|
|
||||||
|
|
||||||
@computed("loading", "remote", "uploadUrl", "local", "localFile")
|
|
||||||
importDisabled(isLoading, isRemote, uploadUrl, isLocal, localFile) {
|
|
||||||
return isLoading || (isRemote && !uploadUrl) || (isLocal && !localFile);
|
|
||||||
},
|
|
||||||
|
|
||||||
@observes("privateChecked")
|
|
||||||
privateWasChecked() {
|
|
||||||
this.get("privateChecked")
|
|
||||||
? this.set("urlPlaceholder", "git@github.com:discourse/sample_theme.git")
|
|
||||||
: this.set("urlPlaceholder", "https://github.com/discourse/sample_theme");
|
|
||||||
|
|
||||||
const checked = this.get("privateChecked");
|
|
||||||
if (checked && !this._keyLoading) {
|
|
||||||
this._keyLoading = true;
|
|
||||||
ajax(this.get("keyGenUrl"), { method: "POST" })
|
|
||||||
.then(pair => {
|
|
||||||
this.set("privateKey", pair.private_key);
|
|
||||||
this.set("publicKey", pair.public_key);
|
|
||||||
})
|
|
||||||
.catch(popupAjaxError)
|
|
||||||
.finally(() => {
|
|
||||||
this._keyLoading = false;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
actions: {
|
|
||||||
uploadLocaleFile() {
|
|
||||||
this.set("localFile", $("#file-input")[0].files[0]);
|
|
||||||
},
|
|
||||||
|
|
||||||
toggleAdvanced() {
|
|
||||||
this.set("advancedVisible", !this.get("advancedVisible"));
|
|
||||||
},
|
|
||||||
|
|
||||||
importTheme() {
|
|
||||||
let options = {
|
|
||||||
type: "POST"
|
|
||||||
};
|
|
||||||
|
|
||||||
if (this.get("local")) {
|
|
||||||
options.processData = false;
|
|
||||||
options.contentType = false;
|
|
||||||
options.data = new FormData();
|
|
||||||
options.data.append("theme", this.get("localFile"));
|
|
||||||
} else {
|
|
||||||
options.data = {
|
|
||||||
remote: this.get("uploadUrl"),
|
|
||||||
branch: this.get("branch")
|
|
||||||
};
|
|
||||||
|
|
||||||
if (this.get("privateChecked")) {
|
|
||||||
options.data.private_key = this.get("privateKey");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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.get("importUrl"), options)
|
|
||||||
.then(result => {
|
|
||||||
const theme = this.store.createRecord("theme", result.theme);
|
|
||||||
this.get("adminCustomizeThemes").send("addTheme", theme);
|
|
||||||
this.send("closeModal");
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
this.set("privateKey", null);
|
|
||||||
this.set("publicKey", null);
|
|
||||||
})
|
|
||||||
.catch(popupAjaxError)
|
|
||||||
.finally(() => this.set("loading", false));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
|
@ -0,0 +1,283 @@
|
||||||
|
import ModalFunctionality from "discourse/mixins/modal-functionality";
|
||||||
|
import { ajax } from "discourse/lib/ajax";
|
||||||
|
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||||
|
import {
|
||||||
|
default as computed,
|
||||||
|
observes
|
||||||
|
} from "ember-addons/ember-computed-decorators";
|
||||||
|
import { THEMES, COMPONENTS } from "admin/models/theme";
|
||||||
|
|
||||||
|
const MIN_NAME_LENGTH = 4;
|
||||||
|
|
||||||
|
// TODO: use a central repository for themes/components
|
||||||
|
const POPULAR_THEMES = [
|
||||||
|
{
|
||||||
|
name: "Graceful",
|
||||||
|
value: "https://github.com/discourse/graceful",
|
||||||
|
preview: "https://theme-creator.discourse.org/theme/awesomerobot/graceful",
|
||||||
|
description: "A light and graceful theme for Discourse.",
|
||||||
|
meta_url:
|
||||||
|
"https://meta.discourse.org/t/a-graceful-theme-for-discourse/93040"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Material Design Theme",
|
||||||
|
value: "https://github.com/discourse/material-design-stock-theme",
|
||||||
|
preview: "https://newmaterial.trydiscourse.com",
|
||||||
|
description:
|
||||||
|
"Inspired by Material Design, this theme comes with several color palettes (incl. a dark one).",
|
||||||
|
meta_url: "https://meta.discourse.org/t/material-design-stock-theme/47142"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Minima",
|
||||||
|
value: "https://github.com/discourse/minima",
|
||||||
|
preview: "https://theme-creator.discourse.org/theme/awesomerobot/minima",
|
||||||
|
description: "A minimal theme with reduced UI elements and focus on text.",
|
||||||
|
meta_url:
|
||||||
|
"https://meta.discourse.org/t/minima-a-minimal-theme-for-discourse/108178"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Sam's Simple Theme",
|
||||||
|
value: "https://github.com/discourse/discourse-simple-theme",
|
||||||
|
preview: "https://theme-creator.discourse.org/theme/sam/simple",
|
||||||
|
description:
|
||||||
|
"Simplified front page design with classic colors and typography.",
|
||||||
|
meta_url:
|
||||||
|
"https://meta.discourse.org/t/sams-personal-minimal-topic-list-design/23552"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Vincent",
|
||||||
|
value: "https://github.com/discourse/discourse-vincent-theme",
|
||||||
|
preview: "https://theme-creator.discourse.org/theme/awesomerobot/vincent",
|
||||||
|
description: "An elegant dark theme with a few color palettes.",
|
||||||
|
meta_url: "https://meta.discourse.org/t/discourse-vincent-theme/76662"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Alternative Logos",
|
||||||
|
value: "https://github.com/discourse/discourse-alt-logo",
|
||||||
|
description: "Add alternative logos for dark / light themes.",
|
||||||
|
meta_url:
|
||||||
|
"https://meta.discourse.org/t/alternative-logo-for-dark-themes/88502",
|
||||||
|
component: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Brand Header Theme Component",
|
||||||
|
value: "https://github.com/discourse/discourse-brand-header",
|
||||||
|
description:
|
||||||
|
"Add an extra top header with your logo, navigation links and social icons.",
|
||||||
|
meta_url: "https://meta.discourse.org/t/brand-header-theme-component/77977",
|
||||||
|
component: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Custom Header Links",
|
||||||
|
value: "https://github.com/discourse/discourse-custom-header-links",
|
||||||
|
preview:
|
||||||
|
"https://theme-creator.discourse.org/theme/Johani/custom-header-links",
|
||||||
|
description: "Easily add custom text-based links to the header.",
|
||||||
|
meta_url: "https://meta.discourse.org/t/custom-header-links/90588",
|
||||||
|
component: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Category Banners",
|
||||||
|
value: "https://github.com/discourse/discourse-category-banners",
|
||||||
|
preview:
|
||||||
|
"https://theme-creator.discourse.org/theme/awesomerobot/discourse-category-banners",
|
||||||
|
description:
|
||||||
|
"Show banners on category pages using your existing category details.",
|
||||||
|
meta_url: "https://meta.discourse.org/t/discourse-category-banners/86241",
|
||||||
|
component: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Hamburger Theme Selector",
|
||||||
|
value: "https://github.com/discourse/discourse-hamburger-theme-selector",
|
||||||
|
description:
|
||||||
|
"Displays a theme selector in the hamburger menu provided there is more than one user-selectable theme.",
|
||||||
|
meta_url: "https://meta.discourse.org/t/hamburger-theme-selector/61210",
|
||||||
|
component: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Header submenus",
|
||||||
|
value: "https://github.com/discourse/discourse-header-submenus",
|
||||||
|
preview: "https://theme-creator.discourse.org/theme/Johani/header-submenus",
|
||||||
|
description: "Lets you build a header menu with submenus (dropdowns).",
|
||||||
|
meta_url: "https://meta.discourse.org/t/header-submenus/94584",
|
||||||
|
component: true
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
export default Ember.Controller.extend(ModalFunctionality, {
|
||||||
|
popular: Ember.computed.equal("selection", "popular"),
|
||||||
|
local: Ember.computed.equal("selection", "local"),
|
||||||
|
remote: Ember.computed.equal("selection", "remote"),
|
||||||
|
create: Ember.computed.equal("selection", "create"),
|
||||||
|
selection: "popular",
|
||||||
|
adminCustomizeThemes: Ember.inject.controller(),
|
||||||
|
loading: false,
|
||||||
|
keyGenUrl: "/admin/themes/generate_key_pair",
|
||||||
|
importUrl: "/admin/themes/import",
|
||||||
|
checkPrivate: Ember.computed.match("uploadUrl", /^git/),
|
||||||
|
localFile: null,
|
||||||
|
uploadUrl: null,
|
||||||
|
urlPlaceholder: "https://github.com/discourse/sample_theme",
|
||||||
|
advancedVisible: false,
|
||||||
|
themesController: Ember.inject.controller("adminCustomizeThemes"),
|
||||||
|
createTypes: [
|
||||||
|
{ name: I18n.t("admin.customize.theme.theme"), value: THEMES },
|
||||||
|
{ name: I18n.t("admin.customize.theme.component"), value: COMPONENTS }
|
||||||
|
],
|
||||||
|
selectedType: Ember.computed.alias("themesController.currentTab"),
|
||||||
|
component: Ember.computed.equal("selectedType", COMPONENTS),
|
||||||
|
|
||||||
|
@computed("themesController.installedThemes")
|
||||||
|
themes(installedThemes) {
|
||||||
|
return POPULAR_THEMES.map(t => {
|
||||||
|
if (installedThemes.includes(t.name)) {
|
||||||
|
Ember.set(t, "installed", true);
|
||||||
|
}
|
||||||
|
return t;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
@computed(
|
||||||
|
"loading",
|
||||||
|
"remote",
|
||||||
|
"uploadUrl",
|
||||||
|
"local",
|
||||||
|
"localFile",
|
||||||
|
"create",
|
||||||
|
"nameTooShort"
|
||||||
|
)
|
||||||
|
installDisabled(
|
||||||
|
isLoading,
|
||||||
|
isRemote,
|
||||||
|
uploadUrl,
|
||||||
|
isLocal,
|
||||||
|
localFile,
|
||||||
|
isCreate,
|
||||||
|
nameTooShort
|
||||||
|
) {
|
||||||
|
return (
|
||||||
|
isLoading ||
|
||||||
|
(isRemote && !uploadUrl) ||
|
||||||
|
(isLocal && !localFile) ||
|
||||||
|
(isCreate && nameTooShort)
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
@observes("privateChecked")
|
||||||
|
privateWasChecked() {
|
||||||
|
this.get("privateChecked")
|
||||||
|
? this.set("urlPlaceholder", "git@github.com:discourse/sample_theme.git")
|
||||||
|
: this.set("urlPlaceholder", "https://github.com/discourse/sample_theme");
|
||||||
|
|
||||||
|
const checked = this.get("privateChecked");
|
||||||
|
if (checked && !this._keyLoading) {
|
||||||
|
this._keyLoading = true;
|
||||||
|
ajax(this.get("keyGenUrl"), { method: "POST" })
|
||||||
|
.then(pair => {
|
||||||
|
this.setProperties({ privateKey: pair.private_key, publicKey: pair.public_key });
|
||||||
|
})
|
||||||
|
.catch(popupAjaxError)
|
||||||
|
.finally(() => {
|
||||||
|
this._keyLoading = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
@computed("name")
|
||||||
|
nameTooShort(name) {
|
||||||
|
return !name || name.length < MIN_NAME_LENGTH;
|
||||||
|
},
|
||||||
|
|
||||||
|
@computed("component")
|
||||||
|
placeholder(component) {
|
||||||
|
if (component) {
|
||||||
|
return I18n.t("admin.customize.theme.component_name");
|
||||||
|
} else {
|
||||||
|
return I18n.t("admin.customize.theme.theme_name");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
@computed("selection")
|
||||||
|
submitLabel(selection) {
|
||||||
|
return `admin.customize.theme.${
|
||||||
|
selection === "create" ? "create" : "install"
|
||||||
|
}`;
|
||||||
|
},
|
||||||
|
|
||||||
|
@computed("privateChecked", "checkPrivate", "publicKey")
|
||||||
|
showPublicKey(privateChecked, checkPrivate, publicKey) {
|
||||||
|
return privateChecked && checkPrivate && publicKey;
|
||||||
|
},
|
||||||
|
|
||||||
|
actions: {
|
||||||
|
uploadLocaleFile() {
|
||||||
|
this.set("localFile", $("#file-input")[0].files[0]);
|
||||||
|
},
|
||||||
|
|
||||||
|
toggleAdvanced() {
|
||||||
|
this.toggleProperty("advancedVisible");
|
||||||
|
},
|
||||||
|
|
||||||
|
installThemeFromList(url) {
|
||||||
|
this.set("uploadUrl", url);
|
||||||
|
this.send("installTheme");
|
||||||
|
},
|
||||||
|
|
||||||
|
installTheme() {
|
||||||
|
if (this.get("create")) {
|
||||||
|
this.set("loading", true);
|
||||||
|
const theme = this.store.createRecord("theme");
|
||||||
|
theme
|
||||||
|
.save({ name: this.get("name"), component: this.get("component") })
|
||||||
|
.then(() => {
|
||||||
|
this.get("themesController").send("addTheme", theme);
|
||||||
|
this.send("closeModal");
|
||||||
|
})
|
||||||
|
.catch(popupAjaxError)
|
||||||
|
.finally(() => this.set("loading", false));
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let options = {
|
||||||
|
type: "POST"
|
||||||
|
};
|
||||||
|
|
||||||
|
if (this.get("local")) {
|
||||||
|
options.processData = false;
|
||||||
|
options.contentType = false;
|
||||||
|
options.data = new FormData();
|
||||||
|
options.data.append("theme", this.get("localFile"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.get("remote") || this.get("popular")) {
|
||||||
|
options.data = {
|
||||||
|
remote: this.get("uploadUrl"),
|
||||||
|
branch: this.get("branch")
|
||||||
|
};
|
||||||
|
|
||||||
|
if (this.get("privateChecked")) {
|
||||||
|
options.data.private_key = this.get("privateKey");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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.get("importUrl"), options)
|
||||||
|
.then(result => {
|
||||||
|
const theme = this.store.createRecord("theme", result.theme);
|
||||||
|
this.get("adminCustomizeThemes").send("addTheme", theme);
|
||||||
|
this.send("closeModal");
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
this.setProperties({ privateKey: null, publicKey: null });
|
||||||
|
})
|
||||||
|
.catch(popupAjaxError)
|
||||||
|
.finally(() => this.set("loading", false));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
|
@ -11,17 +11,13 @@ export default Ember.Route.extend({
|
||||||
},
|
},
|
||||||
|
|
||||||
actions: {
|
actions: {
|
||||||
importModal() {
|
installModal() {
|
||||||
showModal("admin-import-theme", { admin: true });
|
showModal("admin-install-theme", { admin: true });
|
||||||
},
|
},
|
||||||
|
|
||||||
addTheme(theme) {
|
addTheme(theme) {
|
||||||
this.refresh();
|
this.refresh();
|
||||||
this.transitionTo("adminCustomizeThemes.show", theme.get("id"));
|
this.transitionTo("adminCustomizeThemes.show", theme.get("id"));
|
||||||
},
|
|
||||||
|
|
||||||
showCreateModal() {
|
|
||||||
showModal("admin-create-theme", { admin: true });
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
{{radio-button name="install-items" id=value value=value selection=selection}}
|
||||||
|
<label class="radio" for="{{value}}">
|
||||||
|
{{#if showIcon}}
|
||||||
|
{{d-icon 'plus'}}
|
||||||
|
{{/if}}
|
||||||
|
{{i18n label}}
|
||||||
|
</label>
|
||||||
|
{{d-icon 'caret-right'}}
|
|
@ -1,8 +1,8 @@
|
||||||
<div class="themes-list-header">
|
<div class="themes-list-header">
|
||||||
<div {{action "changeView" THEMES}} class="themes-tab tab {{if themesTabActive 'active' ''}}">
|
<div {{action "changeView" THEMES}} class="themes-tab tab {{if themesTabActive 'active' ''}}">
|
||||||
{{d-icon "cube"}}
|
|
||||||
{{I18n "admin.customize.theme.title"}}
|
{{I18n "admin.customize.theme.title"}}
|
||||||
</div><div {{action "changeView" COMPONENTS}} class="components-tab tab {{if componentsTabActive 'active' ''}}">
|
</div><div {{action "changeView" COMPONENTS}} class="components-tab tab {{if componentsTabActive 'active' ''}}">
|
||||||
|
{{d-icon "puzzle-piece"}}
|
||||||
{{I18n "admin.customize.theme.components"}}
|
{{I18n "admin.customize.theme.components"}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -42,6 +42,5 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="create-actions">
|
<div class="create-actions">
|
||||||
{{d-button label="admin.customize.new" icon="plus" action=showCreateModal class="btn-primary"}}
|
{{d-button action=installModal icon="upload" label="admin.customize.install" class="btn-primary"}}
|
||||||
{{d-button action=importModal icon="upload" label="admin.customize.import" class="btn-default"}}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -3,8 +3,7 @@
|
||||||
<div class="content-wrapper">
|
<div class="content-wrapper">
|
||||||
<h1>{{I18n "admin.customize.theme.themes_intro"}}</h1>
|
<h1>{{I18n "admin.customize.theme.themes_intro"}}</h1>
|
||||||
<div class="create-actions">
|
<div class="create-actions">
|
||||||
{{d-button label="admin.customize.new" icon="plus" action=(route-action "showCreateModal") class="btn-primary"}}
|
{{d-button action=(route-action "installModal") icon="upload" label="admin.customize.install" class="btn-primary"}}
|
||||||
{{d-button action=(route-action "importModal") icon="upload" label="admin.customize.import" class="btn-default"}}
|
|
||||||
</div>
|
</div>
|
||||||
<div class="external-resources">
|
<div class="external-resources">
|
||||||
{{#each externalResources as |resource|}}
|
{{#each externalResources as |resource|}}
|
||||||
|
@ -12,7 +11,7 @@
|
||||||
{{d-icon resource.icon}}
|
{{d-icon resource.icon}}
|
||||||
{{I18n resource.key}}
|
{{I18n resource.key}}
|
||||||
</a>
|
</a>
|
||||||
{{/each}}
|
{{/each}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -193,7 +193,10 @@
|
||||||
|
|
||||||
{{#if availableChildThemes}}
|
{{#if availableChildThemes}}
|
||||||
<div class="control-unit">
|
<div class="control-unit">
|
||||||
<div class="mini-title">{{i18n "admin.customize.theme.theme_components"}}</div>
|
<div class="mini-title">
|
||||||
|
{{d-icon "puzzle-piece"}}
|
||||||
|
{{i18n "admin.customize.theme.theme_components"}}
|
||||||
|
</div>
|
||||||
{{#if model.childThemes.length}}
|
{{#if model.childThemes.length}}
|
||||||
<ul class='removable-list'>
|
<ul class='removable-list'>
|
||||||
{{#each model.childThemes as |child|}}
|
{{#each model.childThemes as |child|}}
|
||||||
|
|
|
@ -3,8 +3,7 @@
|
||||||
themes=fullThemes
|
themes=fullThemes
|
||||||
components=childThemes
|
components=childThemes
|
||||||
currentTab=currentTab
|
currentTab=currentTab
|
||||||
showCreateModal=(route-action "showCreateModal")
|
installModal=(route-action "installModal")}}
|
||||||
importModal=(route-action "importModal")}}
|
|
||||||
{{/unless}}
|
{{/unless}}
|
||||||
|
|
||||||
{{outlet}}
|
{{outlet}}
|
||||||
|
|
|
@ -1,30 +0,0 @@
|
||||||
{{#d-modal-body class="create-theme-modal" title="admin.customize.theme.create"}}
|
|
||||||
<div class="input">
|
|
||||||
<span class="label">
|
|
||||||
{{I18n "admin.customize.theme.create_name"}}
|
|
||||||
</span>
|
|
||||||
<span class="control">
|
|
||||||
{{input value=name placeholder=placeholder}}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="input">
|
|
||||||
<span class="label">
|
|
||||||
{{I18n "admin.customize.theme.create_type"}}
|
|
||||||
</span>
|
|
||||||
<span class="control">
|
|
||||||
{{combo-box valueAttribute="value" content=types value=selectedType}}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
{{#if showError}}
|
|
||||||
<div class="error">
|
|
||||||
{{d-icon "warning"}}
|
|
||||||
{{I18n "admin.customize.theme.name_too_short"}}
|
|
||||||
</div>
|
|
||||||
{{/if}}
|
|
||||||
{{/d-modal-body}}
|
|
||||||
|
|
||||||
<div class="modal-footer">
|
|
||||||
{{d-button class="btn btn-primary" label="admin.customize.theme.create" action=(action "createTheme") disabled=saving}}
|
|
||||||
{{d-modal-cancel close=(route-action "closeModal")}}
|
|
||||||
</div>
|
|
|
@ -1,58 +0,0 @@
|
||||||
{{#d-modal-body class='upload-selector import-theme' title="admin.customize.theme.import_theme"}}
|
|
||||||
<div class="radios">
|
|
||||||
{{radio-button name="upload" id="local" value="local" selection=selection}}
|
|
||||||
<label class="radio" for="local">{{i18n 'upload_selector.from_my_computer'}}</label>
|
|
||||||
{{#if local}}
|
|
||||||
<div class="inputs">
|
|
||||||
<input onchange={{action "uploadLocaleFile"}} type="file" id="file-input" accept='.dcstyle.json,application/json,.tar.gz,application/x-gzip'><br>
|
|
||||||
<span class="description">{{i18n 'admin.customize.theme.import_file_tip'}}</span>
|
|
||||||
</div>
|
|
||||||
{{/if}}
|
|
||||||
</div>
|
|
||||||
<div class="radios">
|
|
||||||
{{radio-button name="upload" id="remote" value="remote" selection=selection}}
|
|
||||||
<label class="radio" for="remote">{{i18n 'upload_selector.from_the_web'}}</label>
|
|
||||||
{{#if remote}}
|
|
||||||
<div class="inputs">
|
|
||||||
<div class='repo'>
|
|
||||||
<div class="label">{{i18n 'admin.customize.theme.import_web_tip'}}</div>
|
|
||||||
{{input value=uploadUrl placeholder=urlPlaceholder}}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{{d-button
|
|
||||||
class="btn-small advanced-repo"
|
|
||||||
action=(action "toggleAdvanced")
|
|
||||||
label='admin.customize.theme.import_web_advanced'}}
|
|
||||||
|
|
||||||
{{#if advancedVisible}}
|
|
||||||
<div class='branch'>
|
|
||||||
<div class="label">{{i18n 'admin.customize.theme.remote_branch'}}</div>
|
|
||||||
{{input value=branch placeholder="master"}}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class='check-private'>
|
|
||||||
<label>
|
|
||||||
{{input type="checkbox" checked=privateChecked}}
|
|
||||||
{{i18n 'admin.customize.theme.is_private'}}
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
{{#if checkPrivate}}
|
|
||||||
{{#if privateChecked}}
|
|
||||||
{{#if publicKey}}
|
|
||||||
<div class='public-key'>
|
|
||||||
<div class="label">{{i18n 'admin.customize.theme.public_key'}}</div>
|
|
||||||
{{textarea readonly=true value=publicKey}}
|
|
||||||
</div>
|
|
||||||
{{/if}}
|
|
||||||
{{/if}}
|
|
||||||
{{/if}}
|
|
||||||
{{/if}}
|
|
||||||
</div>
|
|
||||||
{{/if}}
|
|
||||||
</div>
|
|
||||||
{{/d-modal-body}}
|
|
||||||
|
|
||||||
<div class="modal-footer">
|
|
||||||
{{d-button action=(action "importTheme") disabled=importDisabled class='btn btn-primary' icon='upload' label='admin.customize.import'}}
|
|
||||||
{{d-modal-cancel close=(route-action "closeModal")}}
|
|
||||||
</div>
|
|
|
@ -0,0 +1,104 @@
|
||||||
|
{{#d-modal-body class='upload-selector install-theme' title="admin.customize.theme.install"}}
|
||||||
|
<div class="install-theme-items">
|
||||||
|
{{install-theme-item value="popular" selection=selection label="admin.customize.theme.install_popular"}}
|
||||||
|
{{install-theme-item value="local" selection=selection label="admin.customize.theme.install_upload"}}
|
||||||
|
{{install-theme-item value="remote" selection=selection label="admin.customize.theme.install_git_repo"}}
|
||||||
|
{{install-theme-item value="create" selection=selection label="admin.customize.theme.install_create" showIcon=true}}
|
||||||
|
</div>
|
||||||
|
<div class="install-theme-content">
|
||||||
|
{{#if popular}}
|
||||||
|
<div class="popular-theme-items">
|
||||||
|
{{#each themes as |theme|}}
|
||||||
|
<div class="popular-theme-item">
|
||||||
|
<div class="popular-theme-name">
|
||||||
|
<a href="{{theme.meta_url}}" 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}}
|
||||||
|
{{d-button class='btn-small'
|
||||||
|
label="admin.customize.theme.install"
|
||||||
|
disabled=installDisabled
|
||||||
|
icon="upload"
|
||||||
|
action=(action "installThemeFromList" theme.value)}}
|
||||||
|
|
||||||
|
{{#if theme.preview}}
|
||||||
|
<a href="{{theme.preview}}" target="_blank">{{d-icon "desktop"}} {{I18n "admin.customize.theme.preview"}}</a>
|
||||||
|
{{/if}}
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{/each}}
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
{{#if local}}
|
||||||
|
<div class="inputs">
|
||||||
|
<input onchange={{action "uploadLocaleFile"}} type="file" id="file-input" accept='.dcstyle.json,application/json,.tar.gz,application/x-gzip'><br>
|
||||||
|
<span class="description">{{i18n 'admin.customize.theme.import_file_tip'}}</span>
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
{{#if remote}}
|
||||||
|
<div class="inputs">
|
||||||
|
<div class='repo'>
|
||||||
|
<div class="label">{{i18n 'admin.customize.theme.import_web_tip'}}</div>
|
||||||
|
{{input value=uploadUrl placeholder=urlPlaceholder}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{d-button
|
||||||
|
class="btn-small advanced-repo"
|
||||||
|
action=(action "toggleAdvanced")
|
||||||
|
label='admin.customize.theme.import_web_advanced'}}
|
||||||
|
|
||||||
|
{{#if advancedVisible}}
|
||||||
|
<div class='branch'>
|
||||||
|
<div class="label">{{i18n 'admin.customize.theme.remote_branch'}}</div>
|
||||||
|
{{input value=branch placeholder="master"}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class='check-private'>
|
||||||
|
<label>
|
||||||
|
{{input type="checkbox" checked=privateChecked}}
|
||||||
|
{{i18n 'admin.customize.theme.is_private'}}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
{{#if showPublicKey}}
|
||||||
|
<div class='public-key'>
|
||||||
|
<div class="label">{{i18n 'admin.customize.theme.public_key'}}</div>
|
||||||
|
{{textarea readonly=true value=publicKey}}
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
{{#if create}}
|
||||||
|
<div class="inputs">
|
||||||
|
<div class="label">{{I18n "admin.customize.theme.create_name"}}</div>
|
||||||
|
{{input value=name placeholder=placeholder}}
|
||||||
|
|
||||||
|
<div class="label">{{I18n "admin.customize.theme.create_type"}}</div>
|
||||||
|
{{combo-box valueAttribute="value" content=createTypes value=selectedType}}
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{/d-modal-body}}
|
||||||
|
|
||||||
|
{{#unless popular}}
|
||||||
|
<div class="modal-footer">
|
||||||
|
{{d-button action=(action "installTheme") disabled=installDisabled class='btn btn-primary' label=submitLabel}}
|
||||||
|
{{d-modal-cancel close=(route-action "closeModal")}}
|
||||||
|
</div>
|
||||||
|
{{/unless}}
|
|
@ -0,0 +1,19 @@
|
||||||
|
<div class="popular-theme-item">
|
||||||
|
<div class="popular-theme-name">
|
||||||
|
{{theme.name}}
|
||||||
|
{{#if theme.preview}}
|
||||||
|
<a href="{{theme.preview}}" title="Preview" target="_blank">{{d-icon "eye"}}</a>
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
|
<div class="popular-theme-buttons">
|
||||||
|
{{#if theme.installed}}
|
||||||
|
<span>{{I18n "admin.customize.theme.installed"}}</span>
|
||||||
|
{{else}}
|
||||||
|
{{d-button class='btn-small'
|
||||||
|
label="admin.customize.theme.install"
|
||||||
|
disabled=installDisabled
|
||||||
|
icon="upload"
|
||||||
|
action=(action "installThemeFromList" theme.value)}}
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
|
</div>
|
|
@ -994,6 +994,7 @@ table#user-badges {
|
||||||
@import "common/admin/flagging";
|
@import "common/admin/flagging";
|
||||||
@import "common/admin/staff_logs";
|
@import "common/admin/staff_logs";
|
||||||
@import "common/admin/customize";
|
@import "common/admin/customize";
|
||||||
|
@import "common/admin/customize-install-theme";
|
||||||
@import "common/admin/api";
|
@import "common/admin/api";
|
||||||
@import "common/admin/backups";
|
@import "common/admin/backups";
|
||||||
@import "common/admin/plugins";
|
@import "common/admin/plugins";
|
||||||
|
|
108
app/assets/stylesheets/common/admin/customize-install-theme.scss
Normal file
108
app/assets/stylesheets/common/admin/customize-install-theme.scss
Normal file
|
@ -0,0 +1,108 @@
|
||||||
|
.install-theme {
|
||||||
|
min-width: 650px;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.install-theme-items {
|
||||||
|
width: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.install-theme-item {
|
||||||
|
border: 1px solid $primary-low;
|
||||||
|
border-bottom: none;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
border-bottom: 1px solid $primary-low;
|
||||||
|
}
|
||||||
|
|
||||||
|
input,
|
||||||
|
.d-icon-caret-right {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
display: block;
|
||||||
|
padding: 10px 30px 10px 10px;
|
||||||
|
margin-bottom: 0px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
input:checked + label {
|
||||||
|
color: $secondary;
|
||||||
|
background-color: $tertiary;
|
||||||
|
+ .d-icon {
|
||||||
|
display: block;
|
||||||
|
color: $secondary;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.d-icon-caret-right {
|
||||||
|
position: absolute;
|
||||||
|
right: 5px;
|
||||||
|
top: 50%;
|
||||||
|
margin-top: -7px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.install-theme-content {
|
||||||
|
padding: 0px 0px 10px 20px;
|
||||||
|
width: calc(100% - 200px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.repo {
|
||||||
|
input[type="text"] {
|
||||||
|
width: 90%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.popular-theme-items {
|
||||||
|
height: 65vh;
|
||||||
|
overflow: auto;
|
||||||
|
padding-right: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popular-theme-item {
|
||||||
|
border-bottom: 1px solid $primary-low;
|
||||||
|
padding: 8px 0px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
.popular-theme-name {
|
||||||
|
flex: 1;
|
||||||
|
.popular-theme-type {
|
||||||
|
font-weight: normal;
|
||||||
|
font-size: $font-down-2;
|
||||||
|
color: $primary-medium;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: $primary-very-high;
|
||||||
|
font-weight: bold;
|
||||||
|
&:hover,
|
||||||
|
&:visited,
|
||||||
|
&:active {
|
||||||
|
color: $primary-high;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.popular-theme-description {
|
||||||
|
font-size: $font-down-1;
|
||||||
|
padding-right: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.popular-theme-buttons {
|
||||||
|
> span {
|
||||||
|
font-style: italic;
|
||||||
|
font-size: $font-down-1;
|
||||||
|
color: $primary-medium;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
> a {
|
||||||
|
display: block;
|
||||||
|
font-size: $font-down-2;
|
||||||
|
margin-top: 5px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -52,11 +52,3 @@
|
||||||
padding: 7px 10px;
|
padding: 7px 10px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.import-theme {
|
|
||||||
min-width: 650px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.advanced-repo {
|
|
||||||
margin-top: 10px;
|
|
||||||
}
|
|
||||||
|
|
|
@ -3304,7 +3304,7 @@ en:
|
||||||
save: "Save"
|
save: "Save"
|
||||||
new: "New"
|
new: "New"
|
||||||
new_style: "New Style"
|
new_style: "New Style"
|
||||||
import: "Import"
|
install: "Install"
|
||||||
delete: "Delete"
|
delete: "Delete"
|
||||||
delete_confirm: "Delete this theme?"
|
delete_confirm: "Delete this theme?"
|
||||||
color: "Color"
|
color: "Color"
|
||||||
|
@ -3329,17 +3329,15 @@ en:
|
||||||
components: "Components"
|
components: "Components"
|
||||||
theme_name: "Theme name"
|
theme_name: "Theme name"
|
||||||
component_name: "Component name"
|
component_name: "Component name"
|
||||||
themes_intro: "Select an existing theme or create a new one to get started"
|
themes_intro: "Select an existing theme or install a new one to get started"
|
||||||
beginners_guide_title: "Beginner’s guide to using Discourse Themes"
|
beginners_guide_title: "Beginner’s guide to using Discourse Themes"
|
||||||
developers_guide_title: "Developer’s guide to Discourse Themes"
|
developers_guide_title: "Developer’s guide to Discourse Themes"
|
||||||
browse_themes: "Browse community themes"
|
browse_themes: "Browse community themes"
|
||||||
import_theme: "Import Theme"
|
|
||||||
customize_desc: "Customize:"
|
customize_desc: "Customize:"
|
||||||
title: "Themes"
|
title: "Themes"
|
||||||
create: "Create"
|
create: "Create"
|
||||||
create_type: "Type:"
|
create_type: "Type"
|
||||||
create_name: "Name:"
|
create_name: "Name"
|
||||||
name_too_short: "The name must be at least 4 characters long."
|
|
||||||
long_title: "Amend colors, CSS and HTML contents of your site"
|
long_title: "Amend colors, CSS and HTML contents of your site"
|
||||||
edit: "Edit"
|
edit: "Edit"
|
||||||
edit_confirm: "This is a remote theme, if you edit CSS/HTML your changes will be erased next time you update the theme."
|
edit_confirm: "This is a remote theme, if you edit CSS/HTML your changes will be erased next time you update the theme."
|
||||||
|
@ -3395,6 +3393,12 @@ en:
|
||||||
is_private: "Theme is in a private git repository"
|
is_private: "Theme is in a private git repository"
|
||||||
remote_branch: "Branch name (optional)"
|
remote_branch: "Branch name (optional)"
|
||||||
public_key: "Grant the following public key access to the repo:"
|
public_key: "Grant the following public key access to the repo:"
|
||||||
|
install: "Install"
|
||||||
|
installed: "Installed"
|
||||||
|
install_popular: "Popular"
|
||||||
|
install_upload: "From your device"
|
||||||
|
install_git_repo: "From a git repository"
|
||||||
|
install_create: "Create new"
|
||||||
about_theme: "About"
|
about_theme: "About"
|
||||||
license: "License"
|
license: "License"
|
||||||
version: "Version:"
|
version: "Version:"
|
||||||
|
@ -4131,10 +4135,9 @@ en:
|
||||||
with_post_time: <span class="username">%{username}</span> for post in %{link} at <span class="time">%{time}</span>
|
with_post_time: <span class="username">%{username}</span> for post in %{link} at <span class="time">%{time}</span>
|
||||||
with_time: <span class="username">%{username}</span> at <span class="time">%{time}</span>
|
with_time: <span class="username">%{username}</span> at <span class="time">%{time}</span>
|
||||||
badge_intro:
|
badge_intro:
|
||||||
title: 'Select an existing badge or create a new one to get started'
|
title: "Select an existing badge or create a new one to get started"
|
||||||
what_are_badges_title: 'What are badges?'
|
what_are_badges_title: "What are badges?"
|
||||||
badge_query_examples_title: 'Badge query examples'
|
badge_query_examples_title: "Badge query examples"
|
||||||
|
|
||||||
|
|
||||||
emoji:
|
emoji:
|
||||||
title: "Emoji"
|
title: "Emoji"
|
||||||
|
|
|
@ -144,6 +144,7 @@ module SvgSprite
|
||||||
"plus-circle",
|
"plus-circle",
|
||||||
"plus-square",
|
"plus-square",
|
||||||
"power-off",
|
"power-off",
|
||||||
|
"puzzle-piece",
|
||||||
"question",
|
"question",
|
||||||
"question-circle",
|
"question-circle",
|
||||||
"quote-left",
|
"quote-left",
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue