UX: improvements to admin theme UI
This commit is contained in:
parent
2ef16d0719
commit
a4f057a589
|
@ -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;
|
||||
}
|
||||
});
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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"));
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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}`;
|
||||
},
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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}}
|
|
@ -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>
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
<p class="about">{{i18n 'admin.customize.about'}}</p>
|
|
@ -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>
|
||||
|
|
|
@ -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}}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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, "'")}'`;
|
||||
}
|
||||
if (params.label) {
|
||||
html += " aria-hidden='true'";
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
);
|
||||
}
|
||||
});
|
|
@ -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"
|
||||
);
|
||||
}
|
||||
});
|
|
@ -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(
|
||||
|
|
Loading…
Reference in New Issue