UX: improvements to admin theme UI

This commit is contained in:
OsamaSayegh 2018-08-30 22:23:15 +03:00 committed by Sam
parent 2ef16d0719
commit a4f057a589
20 changed files with 834 additions and 217 deletions

View File

@ -0,0 +1,32 @@
import { default as computed } from "ember-addons/ember-computed-decorators";
const MAX_COMPONENTS = 4;
export default Ember.Component.extend({
classNames: ["themes-list-item"],
classNameBindings: ["theme.active:active"],
hasComponents: Em.computed.gt("children.length", 0),
hasMore: Em.computed.gt("moreCount", 0),
@computed(
"theme.component",
"theme.childThemes.@each.name",
"theme.childThemes.length"
)
children() {
const theme = this.get("theme");
const children = theme.get("childThemes");
if (theme.get("component") || !children) {
return [];
}
return children.slice(0, MAX_COMPONENTS).map(t => t.get("name"));
},
@computed("theme.childThemes.length", "theme.component", "children.length")
moreCount(childrenCount, component) {
if (component || !childrenCount) {
return 0;
}
return childrenCount - MAX_COMPONENTS;
}
});

View File

@ -1,4 +1,84 @@
import { THEMES, COMPONENTS } from "admin/models/theme";
import { default as computed } from "ember-addons/ember-computed-decorators";
const NUM_ENTRIES = 8;
export default Ember.Component.extend({
THEMES: THEMES,
COMPONENTS: COMPONENTS,
classNames: ["themes-list"],
hasThemes: Ember.computed.gt("themes.length", 0)
hasThemes: Em.computed.gt("themesList.length", 0),
hasUserThemes: Em.computed.gt("userThemes.length", 0),
hasInactiveThemes: Em.computed.gt("inactiveThemes.length", 0),
themesTabActive: Em.computed.equal("currentTab", THEMES),
componentsTabActive: Em.computed.equal("currentTab", COMPONENTS),
@computed("themes", "components", "currentTab")
themesList(themes, components) {
if (this.get("themesTabActive")) {
return themes;
} else {
return components;
}
},
@computed(
"themesList",
"currentTab",
"themesList.@each.user_selectable",
"themesList.@each.default"
)
inactiveThemes(themes) {
if (this.get("componentsTabActive")) {
return [];
}
return themes.filter(
theme => !theme.get("user_selectable") && !theme.get("default")
);
},
@computed(
"themesList",
"currentTab",
"themesList.@each.user_selectable",
"themesList.@each.default"
)
userThemes(themes) {
if (this.get("componentsTabActive")) {
return [];
}
themes = themes.filter(
theme => theme.get("user_selectable") || theme.get("default")
);
return _.sortBy(themes, t => {
return [
!t.get("default"),
!t.get("user_selectable"),
t.get("name").toLowerCase()
];
});
},
didRender() {
let height = -1;
this.$(".themes-list-item")
.slice(0, NUM_ENTRIES)
.each(function() {
height += $(this).outerHeight();
});
if (height >= 485 && height <= 800) {
this.$(".themes-list-container").css("max-height", `${height}px`);
}
},
actions: {
changeView(newTab) {
if (newTab !== this.get("currentTab")) {
this.set("currentTab", newTab);
}
}
}
});

View File

@ -6,6 +6,7 @@ import { url } from "discourse/lib/computed";
import { popupAjaxError } from "discourse/lib/ajax-error";
import showModal from "discourse/lib/show-modal";
import ThemeSettings from "admin/models/theme-settings";
import { THEMES, COMPONENTS } from "admin/models/theme";
const THEME_UPLOAD_VAR = 2;
const SETTINGS_TYPE_ID = 5;
@ -111,9 +112,20 @@ export default Ember.Controller.extend({
},
@computed("model.component")
switchKey(component) {
convertKey(component) {
const type = component ? "component" : "theme";
return `admin.customize.theme.switch_${type}`;
return `admin.customize.theme.convert_${type}`;
},
@computed("model.component")
convertIcon(component) {
return component ? "cube" : "";
},
@computed("model.component")
convertTooltip(component) {
const type = component ? "component" : "theme";
return `admin.customize.theme.convert_${type}_tooltip`;
},
@computed("model.settings")
@ -128,6 +140,48 @@ export default Ember.Controller.extend({
downloadUrl: url("model.id", "/admin/themes/%@"),
commitSwitchType() {
const model = this.get("model");
const newValue = !model.get("component");
model.set("component", newValue);
if (newValue) {
// component
this.set("parentController.currentTab", COMPONENTS);
} else {
this.set("parentController.currentTab", THEMES);
}
model
.saveChanges("component")
.then(() => {
this.set("colorSchemeId", null);
model.setProperties({
default: false,
color_scheme_id: null,
user_selectable: false,
child_themes: [],
childThemes: []
});
this.get("parentController.model.content").forEach(theme => {
const children = Array.from(theme.get("childThemes"));
const rawChildren = Array.from(theme.get("child_themes") || []);
const index = children ? children.indexOf(model) : -1;
if (index > -1) {
children.splice(index, 1);
rawChildren.splice(index, 1);
theme.setProperties({
childThemes: children,
child_themes: rawChildren
});
}
});
})
.catch(popupAjaxError);
},
actions: {
updateToLatest() {
this.set("updatingRemote", true);
@ -264,30 +318,26 @@ export default Ember.Controller.extend({
},
switchType() {
return bootbox.confirm(
I18n.t(`${this.get("switchKey")}_alert`),
const relatives = this.get("model.component")
? this.get("parentThemes")
: this.get("model.childThemes");
if (relatives && relatives.length > 0) {
const names = relatives.map(relative => relative.get("name"));
bootbox.confirm(
I18n.t(`${this.get("convertKey")}_alert`, {
relatives: names.join(", ")
}),
I18n.t("no_value"),
I18n.t("yes_value"),
result => {
if (result) {
const model = this.get("model");
model.set("component", !model.get("component"));
model
.saveChanges("component")
.then(() => {
this.set("colorSchemeId", null);
model.setProperties({
default: false,
color_scheme_id: null,
user_selectable: false,
child_themes: [],
childThemes: []
});
})
.catch(popupAjaxError);
this.commitSwitchType();
}
}
);
} else {
this.commitSwitchType();
}
}
}
});

View File

@ -1,18 +1,15 @@
import { default as computed } from "ember-addons/ember-computed-decorators";
import { THEMES } from "admin/models/theme";
export default Ember.Controller.extend({
@computed("model", "model.@each", "model.@each.component")
currentTab: THEMES,
@computed("model", "model.@each.component")
fullThemes(themes) {
return _.sortBy(themes.filter(t => !t.get("component")), t => {
return [
!t.get("default"),
!t.get("user_selectable"),
t.get("name").toLowerCase()
];
});
return themes.filter(t => !t.get("component"));
},
@computed("model", "model.@each", "model.@each.component")
@computed("model", "model.@each.component")
childThemes(themes) {
return themes.filter(t => t.get("component"));
}

View File

@ -1,27 +1,56 @@
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 COMPONENT = "component";
const MIN_NAME_LENGTH = 4;
export default Ember.Controller.extend(ModalFunctionality, {
types: [
{ name: I18n.t("admin.customize.theme.theme"), value: "theme" },
{ name: I18n.t("admin.customize.theme.component"), value: COMPONENT }
],
selectedType: "theme",
name: I18n.t("admin.customize.new_style"),
saving: false,
triggerError: false,
themesController: Ember.inject.controller("adminCustomizeThemes"),
loading: false,
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 === COMPONENT;
return type === COMPONENTS;
},
actions: {
createTheme() {
this.set("loading", true);
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") })
@ -30,7 +59,7 @@ export default Ember.Controller.extend(ModalFunctionality, {
this.send("closeModal");
})
.catch(popupAjaxError)
.finally(() => this.set("loading", false));
.finally(() => this.set("saving", false));
}
}
});

View File

@ -4,6 +4,9 @@ import { popupAjaxError } from "discourse/lib/ajax-error";
const THEME_UPLOAD_VAR = 2;
export const THEMES = "themes";
export const COMPONENTS = "components";
const Theme = RestModel.extend({
FIELDS_IDS: [0, 1],
@ -33,6 +36,18 @@ const Theme = RestModel.extend({
);
},
@computed("theme_fields", "theme_fields.@each.error")
isBroken(fields) {
return (
fields && fields.some(field => field.error && field.error.length > 0)
);
},
@computed("remote_theme", "remote_theme.commits_behind")
isPendingUpdates(remote, commitsBehind) {
return remote && commitsBehind && commitsBehind > 0;
},
getKey(field) {
return `${field.target} ${field.name}`;
},

View File

@ -1,4 +1,5 @@
import { scrollTop } from "discourse/mixins/scroll-top";
import { THEMES, COMPONENTS } from "admin/models/theme";
export default Ember.Route.extend({
serialize(model) {
@ -14,19 +15,21 @@ export default Ember.Route.extend({
setupController(controller, model) {
this._super(...arguments);
controller.set("model", model);
const parentController = this.controllerFor("adminCustomizeThemes");
parentController.set("editingTheme", false);
controller.set("allThemes", parentController.get("model"));
parentController.setProperties({
editingTheme: false,
currentTab: model.get("component") ? COMPONENTS : THEMES
});
controller.setProperties({
model: model,
parentController: parentController,
allThemes: parentController.get("model"),
colorSchemeId: model.get("color_scheme_id"),
colorSchemes: parentController.get("model.extras.color_schemes")
});
this.handleHighlight(model);
controller.set(
"colorSchemes",
parentController.get("model.extras.color_schemes")
);
controller.set("colorSchemeId", model.get("color_scheme_id"));
},
deactivate() {

View File

@ -0,0 +1,34 @@
{{#link-to 'adminCustomizeThemes.show' theme replace=true}}
{{plugin-outlet name="admin-customize-themes-list-item" connectorTagName='span' args=(hash theme=theme)}}
<div class="info">
<span class="name">
{{theme.name}}
</span>
<span class="icons">
{{#if theme.default}}
{{d-icon "check" class="default-indicator" title="admin.customize.theme.default_theme_tooltip"}}
{{/if}}
{{#if theme.isPendingUpdates}}
{{d-icon "refresh" title="admin.customize.theme.updates_available_tooltip" class="light-grey-icon"}}
{{/if}}
{{#if theme.isBroken}}
{{d-icon "exclamation-circle" class="broken-indicator" title="admin.customize.theme.broken_theme_tooltip"}}
{{/if}}
</span>
</div>
{{#if hasComponents}}
<div class="components-list">
{{#each children as |child|}}
<span class="component">
{{child}}
</span>
{{/each}}
{{#if hasMore}}
<span class="others-count">{{I18n "admin.customize.theme.and_x_more" count=moreCount}}</span>
{{/if}}
</div>
{{/if}}
{{/link-to}}

View File

@ -1,23 +1,37 @@
<div class="themes-list-header">
<b>{{I18n title}}</b>
<div {{action "changeView" THEMES}} class="themes-tab tab {{if themesTabActive 'active' ''}}">
{{d-icon "cube"}}
{{I18n "admin.customize.theme.title"}}
</div><div {{action "changeView" COMPONENTS}} class="components-tab tab {{if componentsTabActive 'active' ''}}">
{{I18n "admin.customize.theme.components"}}
</div>
</div>
<div class="themes-list-container">
<div class="themes-list-container" style="max-height: 485px;">
{{#if hasThemes}}
{{#each themes as |theme|}}
<div class="themes-list-item {{if theme.active 'active' ''}}">
{{#link-to 'adminCustomizeThemes.show' theme replace=true}}
{{plugin-outlet name="admin-customize-themes-list-item" connectorTagName='span' args=(hash theme=theme)}}
{{theme.name}}
{{#if theme.user_selectable}}
{{d-icon "user"}}
{{/if}}
{{#if theme.default}}
{{d-icon "asterisk"}}
{{/if}}
{{/link-to}}
</div>
{{#if componentsTabActive}}
{{#each themesList as |theme|}}
{{themes-list-item theme=theme}}
{{/each}}
{{else}}
{{#if hasUserThemes}}
{{#each userThemes as |theme|}}
{{themes-list-item theme=theme}}
{{/each}}
{{#if hasInactiveThemes}}
<div class="themes-list-item inactive-indicator">
<span class="empty">{{I18n "admin.customize.theme.inactive_themes"}}</span>
</div>
{{/if}}
{{/if}}
{{#if hasInactiveThemes}}
{{#each inactiveThemes as |theme|}}
{{themes-list-item theme=theme}}
{{/each}}
{{/if}}
{{/if}}
{{else}}
<div class="themes-list-item">
<span class="empty">{{I18n "admin.customize.theme.empty"}}</span>

View File

@ -1 +0,0 @@
<p class="about">{{i18n 'admin.customize.about'}}</p>

View File

@ -1,5 +1,5 @@
<div class="show-current-style">
<h1>
<div class="title">
{{#if editingName}}
{{text-field value=model.name autofocus="true"}}
{{d-button action="finishedEditingName" class="btn-primary btn-small submit-edit" icon="check"}}
@ -7,38 +7,36 @@
{{else}}
{{model.name}} <a {{action "startEditingName"}}>{{d-icon "pencil"}}</a>
{{/if}}
</h1>
</div>
{{#if model.remote_theme}}
<p>
<a href="{{model.remote_theme.about_url}}">{{i18n "admin.customize.theme.about_theme"}}</a>
</p>
<a class="url about-url" href="{{model.remote_theme.about_url}}">{{i18n "admin.customize.theme.about_theme"}}</a>
{{#if model.remote_theme.license_url}}
<p>
<a href="{{model.remote_theme.license_url}}">{{i18n "admin.customize.theme.license"}} {{d-icon "copyright"}}</a>
</p>
<a class="url license-url" href="{{model.remote_theme.license_url}}">{{i18n "admin.customize.theme.license"}} {{d-icon "copyright"}}</a>
{{/if}}
{{/if}}
{{#if parentThemes}}
<h3>{{i18n "admin.customize.theme.component_of"}}</h3>
<div class="control-unit">
<div class="mini-title">{{i18n "admin.customize.theme.component_of"}}</div>
<ul>
{{#each parentThemes as |theme|}}
<li>{{#link-to 'adminCustomizeThemes.show' theme replace=true}}{{theme.name}}{{/link-to}}</li>
{{/each}}
</ul>
</div>
{{/if}}
{{#unless model.component}}
<p>
<div class="control-unit">
{{inline-edit-checkbox action="applyDefault" labelKey="admin.customize.theme.is_default" checked=model.default}}
{{inline-edit-checkbox action="applyUserSelectable" labelKey="admin.customize.theme.user_selectable" checked=model.user_selectable}}
</p>
</div>
<h3>{{i18n "admin.customize.theme.color_scheme"}}</h3>
<p>{{i18n "admin.customize.theme.color_scheme_select"}}</p>
<p>{{combo-box content=colorSchemes
<div class="control-unit">
<div class="mini-title">{{i18n "admin.customize.theme.color_scheme"}}</div>
<div class="description">{{i18n "admin.customize.theme.color_scheme_select"}}</div>
<div class="control">{{combo-box content=colorSchemes
filterable=true
forceEscape=true
value=colorSchemeId
@ -47,25 +45,26 @@
{{d-button action="changeScheme" class="btn-primary btn-small submit-edit" icon="check"}}
{{d-button action="cancelChangeScheme" class="btn-small cancel-edit" icon="times"}}
{{/if}}
</p>
</div>
{{#link-to 'adminCustomize.colors' class="btn edit"}}{{i18n 'admin.customize.colors.edit'}}{{/link-to}}
</div>
{{/unless}}
<h3>{{i18n "admin.customize.theme.css_html"}}</h3>
<div class="control-unit">
<div class="mini-title">{{i18n "admin.customize.theme.css_html"}}</div>
{{#if hasEditedFields}}
<p>{{i18n "admin.customize.theme.custom_sections"}}</p>
<div class="description">{{i18n "admin.customize.theme.custom_sections"}}</div>
<ul>
{{#each editedDescriptions as |desc|}}
<li>{{desc}}</li>
{{/each}}
</ul>
{{else}}
<p>
<div class="description">
{{i18n "admin.customize.theme.edit_css_html_help"}}
</p>
</div>
{{/if}}
<p>
{{#if model.remote_theme}}
{{#if model.remote_theme.commits_behind}}
{{#d-button action="updateToLatest" icon="download" class='btn-primary'}}{{i18n "admin.customize.theme.update_to_latest"}}{{/d-button}}
@ -75,7 +74,6 @@
{{/if}}
{{#d-button action="editTheme" class="btn edit"}}{{i18n 'admin.customize.theme.edit_css_html'}}{{/d-button}}
{{#if model.remote_theme}}
<span class='status-message'>
{{#if updatingRemote}}
@ -94,10 +92,10 @@
{{/if}}
</span>
{{/if}}
</p>
</div>
<h3>{{i18n "admin.customize.theme.uploads"}}</h3>
<div class="control-unit">
<div class="mini-title">{{i18n "admin.customize.theme.uploads"}}</div>
{{#if model.uploads}}
<ul class='removable-list'>
{{#each model.uploads as |upload|}}
@ -110,30 +108,32 @@
{{/each}}
</ul>
{{else}}
<p>{{i18n "admin.customize.theme.no_uploads"}}</p>
<div class="description">{{i18n "admin.customize.theme.no_uploads"}}</div>
{{/if}}
<p>
{{#d-button action="addUploadModal" icon="plus"}}{{i18n "admin.customize.theme.add"}}{{/d-button}}
</p>
</div>
{{#if hasSettings}}
<h3>{{i18n "admin.customize.theme.theme_settings"}}</h3>
<div class="control-unit">
<div class="mini-title">{{i18n "admin.customize.theme.theme_settings"}}</div>
{{#d-section class="form-horizontal theme settings"}}
{{#each settings as |setting|}}
{{theme-setting setting=setting model=model class="theme-setting"}}
{{/each}}
{{/d-section}}
</div>
{{/if}}
{{#if availableChildThemes}}
<h3>{{i18n "admin.customize.theme.theme_components"}}</h3>
<div class="control-unit">
<div class="mini-title">{{i18n "admin.customize.theme.theme_components"}}</div>
{{#unless model.childThemes.length}}
<p>
<div class="description">
<label class='checkbox-label'>
{{input type="checkbox" checked=allowChildThemes}}
{{i18n "admin.customize.theme.child_themes_check"}}
</label>
</p>
</div>
{{else}}
<ul class='removable-list'>
{{#each model.childThemes as |child|}}
@ -142,16 +142,17 @@
</ul>
{{/unless}}
{{#if selectableChildThemes}}
<p>
<div class="description">
{{combo-box forceEscape=true filterable=true content=selectableChildThemes value=selectedChildThemeId}}
{{#d-button action="addChildTheme" icon="plus"}}{{i18n "admin.customize.theme.add"}}{{/d-button}}
</p>
</div>
{{/if}}
</div>
{{/if}}
<a href='{{previewUrl}}' title="{{i18n 'admin.customize.explain_preview'}}" target='_blank' class='btn'>{{d-icon 'desktop'}}{{i18n 'admin.customize.theme.preview'}}</a>
<a class="btn export" target="_blank" href={{downloadUrl}}>{{d-icon "download"}} {{i18n 'admin.export_json.button_text'}}</a>
{{d-button action="switchType" label=switchKey icon="arrows-h" class="btn-danger"}}
{{d-button action="switchType" label="admin.customize.theme.convert" icon=convertIcon class="btn-normal" title=convertTooltip}}
{{d-button action="destroy" label="admin.customize.delete" icon="trash" class="btn-danger"}}
</div>

View File

@ -1,15 +1,14 @@
{{#unless editingTheme}}
<div class='content-list'>
<div class='customize-themes-header'>
<div class="title">
<h3>{{i18n 'admin.customize.theme.long_title'}}</h3>
</div>
<div class="create-actions">
{{d-button label="admin.customize.new" icon="plus" action="showCreateModal" class="btn-primary"}}
{{d-button action="importModal" icon="upload" label="admin.customize.import"}}
</div>
{{themes-list themes=fullThemes title="admin.customize.theme.title"}}
{{themes-list themes=childThemes title="admin.customize.theme.components"}}
</div>
</div>
{{themes-list themes=fullThemes components=childThemes currentTab=currentTab}}
{{/unless}}
{{outlet}}

View File

@ -1,10 +1,10 @@
{{#d-modal-body class="create-theme-modal" title="admin.customize.theme.modal_title"}}
{{#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}}
{{input value=name placeholder=placeholder}}
</span>
</div>
@ -16,9 +16,15 @@
{{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="createTheme" disabled=loading}}
{{d-button class="btn btn-primary" label="admin.customize.theme.create" action="createTheme" disabled=saving}}
{{d-modal-cancel close=(action "closeModal")}}
</div>

View File

@ -93,7 +93,7 @@ registerIconRenderer({
let tagName = params.tagName || "i";
let html = `<${tagName} class='${faClasses(icon, params)}'`;
if (params.title) {
html += ` title='${I18n.t(params.title)}'`;
html += ` title='${I18n.t(params.title).replace(/'/g, "&#39;")}'`;
}
if (params.label) {
html += " aria-hidden='true'";

View File

@ -52,6 +52,30 @@
display: inline-block;
}
}
.error {
color: $danger;
}
}
.admin-customize.admin-customize-themes {
.customize-themes-header {
border-bottom: 1px solid $primary-low;
padding-bottom: 8px;
display: flex;
align-items: center;
.title {
color: $primary-medium;
flex-grow: 1;
h3 {
margin-bottom: 0;
}
}
}
.admin-container {
padding: 0;
}
}
.admin-customize {
@ -99,70 +123,179 @@
}
}
.show-current-style {
margin-left: 20px;
float: left;
width: 70%;
h2 {
margin-bottom: 15px;
}
h3 {
.url {
margin-bottom: 10px;
margin-top: 30px;
}
}
.create-actions {
.title {
font-size: $font-up-4;
font-weight: bold;
margin-bottom: 10px;
}
.about-url,
license-url {
display: block;
margin-bottom: 10px;
}
.mini-title {
font-size: $font-up-1;
font-weight: bold;
margin-bottom: 7px;
}
.control-unit {
margin-bottom: 25px;
margin-top: 15px;
}
.control {
margin-bottom: 10px;
}
.description {
margin-bottom: 12px;
}
padding-left: 15px;
padding-top: 10px;
float: left;
width: 70%;
}
.themes-list {
margin-bottom: 20px;
border-right: 1px solid $primary-low;
border-bottom: 1px solid $primary-low;
float: left;
width: 28%;
}
.themes-list-header {
font-size: $font-up-1;
padding: 10px;
background-color: $primary-low;
}
.themes-list-container {
max-height: 280px;
overflow-y: scroll;
&::-webkit-scrollbar-track {
border-radius: 10px;
background-color: $secondary;
}
&::-webkit-scrollbar {
width: 5px;
}
&::-webkit-scrollbar-thumb {
border-radius: 10px;
background-color: $primary-low-mid;
}
.themes-list-item {
color: $primary;
width: 100%;
border-bottom: 1px solid $primary-low;
display: flex;
.tab {
display: inline-block;
padding: 10px;
width: 50%;
box-sizing: border-box;
text-align: center;
border-left: 1px solid $primary-low;
&.active {
font-weight: bold;
color: $secondary;
background-color: $tertiary;
}
&:not(.active) {
cursor: pointer;
&:hover {
background-color: $tertiary-low;
}
}
}
}
.themes-list-container {
max-height: 485px;
overflow-y: auto;
&::-webkit-scrollbar-track {
background-color: $secondary;
}
&::-webkit-scrollbar {
width: 5px;
}
&::-webkit-scrollbar-thumb {
background-color: $primary-low;
}
.themes-list-item:last-child {
border-bottom: none;
}
.themes-list-item {
color: $primary;
border-bottom: 1px solid $primary-low;
display: flex;
border-left: 1px solid $primary-low;
&:not(.inactive-indicator):not(.active):hover {
background-color: $tertiary-low;
.component {
border-color: $primary-low-mid;
}
}
&.active {
color: $secondary;
font-weight: bold;
background-color: $tertiary;
.fa {
color: inherit;
}
}
.light-grey-icon {
color: $primary-medium;
}
.info {
overflow: hidden;
font-weight: bold;
font-size: $font-up-1;
.name {
float: left;
}
.icons {
float: right;
}
}
.components-list {
margin-top: 5px;
display: flex;
flex-wrap: wrap;
font-size: $font-down-1;
align-items: baseline;
.component {
display: flex;
padding: 3px 5px 3px 5px;
border-radius: 2px;
border: 1px solid $primary-low;
margin-right: 5px;
margin-bottom: 5px;
}
}
&:not(.active) {
.broken-indicator {
color: $danger;
}
.default-indicator {
color: $success;
}
}
a {
padding: 10px;
}
&.inactive-indicator {
border-right: 0;
border-left: 0;
font-weight: bold;
color: $primary-medium;
span.empty {
padding-left: 5px;
padding-top: 15px;
}
}
span.empty {
padding: 3px 10px 3px 10px;
}
a,
span.empty {
color: inherit;
width: 100%;
padding: 10px;
}
}
}
@ -170,7 +303,7 @@
.theme.settings {
.theme-setting {
padding-bottom: 0;
padding-top: 18px;
margin-top: 18px;
min-height: 35px;
}
.setting-label {

View File

@ -5,6 +5,7 @@
<%= stylesheet_link_tag "test_helper" %>
<%= javascript_include_tag "test_helper" %>
<%= csrf_meta_tags %>
<script src="<%= ExtraLocalesController.url('admin') %>"></script>
</head>
<body>
<div id="qunit"></div>

View File

@ -3212,13 +3212,15 @@ en:
theme: "Theme"
component: "Component"
components: "Components"
theme_name: "Theme name"
component_name: "Component name"
import_theme: "Import Theme"
customize_desc: "Customize:"
title: "Themes"
modal_title: "Create Theme"
create: "Create"
create_type: "Type:"
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"
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."
@ -3233,10 +3235,16 @@ en:
color_scheme_select: "Select colors to be used by theme"
custom_sections: "Custom sections:"
theme_components: "Theme Components"
switch_component: "Make theme"
switch_component_alert: "Are you sure you want to convert this component to theme? This will make it an independant theme and it will be removed as a child from all themes."
switch_theme: "Make component"
switch_theme_alert: "Are you sure you want to convert this theme to component? It will be removed as a parent from all components."
convert: "Convert"
convert_component_alert: "Are you sure you want to convert this component to theme? It will be removed as a component from %{relatives}."
convert_component_tooltip: "Convert this component to theme"
convert_theme_alert: "Are you sure you want to convert this theme to component? It will be removed as a parent from %{relatives}."
convert_theme_tooltip: "Convert this theme to component"
inactive_themes: "Inactive themes:"
broken_theme_tooltip: "This theme has errors in its CSS, HTML or YAML"
default_theme_tooltip: "This theme is the site's default theme"
updates_available_tooltip: "Updates are available for this theme"
and_x_more: "and {{count}} more."
uploads: "Uploads"
no_uploads: "You can upload assets associated with your theme such as fonts and images"
add_upload: "Add Upload"

View File

@ -0,0 +1,84 @@
import componentTest from "helpers/component-test";
import Theme from "admin/models/theme";
moduleForComponent("themes-list-item", { integration: true });
componentTest("default theme", {
template: "{{themes-list-item theme=theme}}",
beforeEach() {
this.set("theme", Theme.create({ name: "Test", default: true }));
},
test(assert) {
assert.expect(1);
assert.equal(this.$(".fa-check").length, 1, "shows default theme icon");
}
});
componentTest("pending updates", {
template: "{{themes-list-item theme=theme}}",
beforeEach() {
this.set(
"theme",
Theme.create({ name: "Test", remote_theme: { commits_behind: 6 } })
);
},
test(assert) {
assert.expect(1);
assert.equal(this.$(".fa-refresh").length, 1, "shows pending update icon");
}
});
componentTest("borken theme", {
template: "{{themes-list-item theme=theme}}",
beforeEach() {
this.set(
"theme",
Theme.create({
name: "Test",
theme_fields: [{ name: "scss", type_id: 1, error: "something" }]
})
);
},
test(assert) {
assert.expect(1);
assert.equal(
this.$(".fa-exclamation-circle").length,
1,
"shows broken theme icon"
);
}
});
const childrenList = [1, 2, 3, 4, 5].map(num =>
Theme.create({ name: `Child ${num}`, component: true })
);
componentTest("with children", {
template: "{{themes-list-item theme=theme}}",
beforeEach() {
this.set(
"theme",
Theme.create({ name: "Test", childThemes: childrenList })
);
},
test(assert) {
assert.expect(2);
assert.deepEqual(
Array.from(this.$(".component")).map(node => node.innerText.trim()),
childrenList.splice(0, 4).map(theme => theme.get("name")),
"lists the first 4 children"
);
assert.deepEqual(
this.$(".others-count")
.text()
.trim(),
I18n.t("admin.customize.theme.and_x_more", { count: 1 }),
"shows count of remaining children"
);
}
});

View File

@ -0,0 +1,132 @@
import componentTest from "helpers/component-test";
import { default as Theme, THEMES, COMPONENTS } from "admin/models/theme";
moduleForComponent("themes-list", { integration: true });
const themes = [1, 2, 3, 4, 5].map(num =>
Theme.create({ name: `Theme ${num}` })
);
const components = [1, 2, 3, 4, 5].map(num =>
Theme.create({ name: `Child ${num}`, component: true })
);
componentTest("current tab is themes", {
template:
"{{themes-list themes=themes components=components currentTab=currentTab}}",
beforeEach() {
this.setProperties({
themes,
components,
currentTab: THEMES
});
},
test(assert) {
assert.equal(
this.$(".themes-tab").hasClass("active"),
true,
"themes tab is active"
);
assert.equal(
this.$(".components-tab").hasClass("active"),
false,
"components tab is not active"
);
assert.equal(
this.$(".inactive-indicator").index(),
-1,
"there is no inactive themes separator when all themes are inactive"
);
assert.equal(this.$(".themes-list-item").length, 5, "displays all themes");
[2, 3].forEach(num => themes[num].set("user_selectable", true));
themes[4].set("default", true);
this.set("themes", themes);
const names = [4, 2, 3, 0, 1].map(num => themes[num].get("name")); // default theme always on top, followed by user-selectable ones and then the rest
assert.deepEqual(
Array.from(this.$(".themes-list-item").find(".name")).map(node =>
node.innerText.trim()
),
names,
"sorts themes correctly"
);
assert.equal(
this.$(".inactive-indicator").index(),
3,
"the separator is in the right location"
);
themes.forEach(theme => theme.set("user_selectable", true));
this.set("themes", themes);
assert.equal(
this.$(".inactive-indicator").index(),
-1,
"there is no inactive themes separator when all themes are user-selectable"
);
this.set("themes", []);
assert.equal(
this.$(".themes-list-item").length,
1,
"shows one entry with a message when there is nothing to display"
);
assert.equal(
this.$(".themes-list-item span.empty")
.text()
.trim(),
I18n.t("admin.customize.theme.empty"),
"displays the right message"
);
}
});
componentTest("current tab is components", {
template:
"{{themes-list themes=themes components=components currentTab=currentTab}}",
beforeEach() {
this.setProperties({
themes,
components,
currentTab: COMPONENTS
});
},
test(assert) {
assert.equal(
this.$(".components-tab").hasClass("active"),
true,
"components tab is active"
);
assert.equal(
this.$(".themes-tab").hasClass("active"),
false,
"themes tab is not active"
);
assert.equal(
this.$(".inactive-indicator").index(),
-1,
"there is no separator"
);
assert.equal(
this.$(".themes-list-item").length,
5,
"displays all components"
);
this.set("components", []);
assert.equal(
this.$(".themes-list-item").length,
1,
"shows one entry with a message when there is nothing to display"
);
assert.equal(
this.$(".themes-list-item span.empty")
.text()
.trim(),
I18n.t("admin.customize.theme.empty"),
"displays the right message"
);
}
});

View File

@ -29,8 +29,8 @@ QUnit.test("can list themes correctly", function(assert) {
assert.deepEqual(
controller.get("fullThemes").map(t => t.get("name")),
[defaultTheme, userTheme, strayTheme1, strayTheme2].map(t => t.get("name")),
"sorts themes correctly"
[strayTheme2, strayTheme1, userTheme, defaultTheme].map(t => t.get("name")),
"returns a list of themes without components"
);
assert.deepEqual(