FEATURE: Add filter box to the themes/components list (#13767)
This commit is contained in:
parent
6d999fb087
commit
1c82989f77
|
@ -1,8 +1,9 @@
|
||||||
import { COMPONENTS, THEMES } from "admin/models/theme";
|
import { COMPONENTS, THEMES } from "admin/models/theme";
|
||||||
import { equal, gt } from "@ember/object/computed";
|
import { equal, gt, gte } from "@ember/object/computed";
|
||||||
import Component from "@ember/component";
|
import Component from "@ember/component";
|
||||||
import discourseComputed from "discourse-common/utils/decorators";
|
import discourseComputed from "discourse-common/utils/decorators";
|
||||||
import { inject as service } from "@ember/service";
|
import { inject as service } from "@ember/service";
|
||||||
|
import { action } from "@ember/object";
|
||||||
|
|
||||||
export default Component.extend({
|
export default Component.extend({
|
||||||
router: service(),
|
router: service(),
|
||||||
|
@ -10,10 +11,12 @@ export default Component.extend({
|
||||||
COMPONENTS,
|
COMPONENTS,
|
||||||
|
|
||||||
classNames: ["themes-list"],
|
classNames: ["themes-list"],
|
||||||
|
filterTerm: null,
|
||||||
|
|
||||||
hasThemes: gt("themesList.length", 0),
|
hasThemes: gt("themesList.length", 0),
|
||||||
hasActiveThemes: gt("activeThemes.length", 0),
|
hasActiveThemes: gt("activeThemes.length", 0),
|
||||||
hasInactiveThemes: gt("inactiveThemes.length", 0),
|
hasInactiveThemes: gt("inactiveThemes.length", 0),
|
||||||
|
showFilter: gte("themesList.length", 10),
|
||||||
|
|
||||||
themesTabActive: equal("currentTab", THEMES),
|
themesTabActive: equal("currentTab", THEMES),
|
||||||
componentsTabActive: equal("currentTab", COMPONENTS),
|
componentsTabActive: equal("currentTab", COMPONENTS),
|
||||||
|
@ -31,28 +34,36 @@ export default Component.extend({
|
||||||
"themesList",
|
"themesList",
|
||||||
"currentTab",
|
"currentTab",
|
||||||
"themesList.@each.user_selectable",
|
"themesList.@each.user_selectable",
|
||||||
"themesList.@each.default"
|
"themesList.@each.default",
|
||||||
|
"filterTerm"
|
||||||
)
|
)
|
||||||
inactiveThemes(themes) {
|
inactiveThemes(themes) {
|
||||||
|
let results;
|
||||||
if (this.componentsTabActive) {
|
if (this.componentsTabActive) {
|
||||||
return themes.filter((theme) => theme.get("parent_themes.length") <= 0);
|
results = themes.filter(
|
||||||
|
(theme) => theme.get("parent_themes.length") <= 0
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
results = themes.filter(
|
||||||
|
(theme) => !theme.get("user_selectable") && !theme.get("default")
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return themes.filter(
|
return this._filterThemes(results, this.filterTerm);
|
||||||
(theme) => !theme.get("user_selectable") && !theme.get("default")
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
@discourseComputed(
|
@discourseComputed(
|
||||||
"themesList",
|
"themesList",
|
||||||
"currentTab",
|
"currentTab",
|
||||||
"themesList.@each.user_selectable",
|
"themesList.@each.user_selectable",
|
||||||
"themesList.@each.default"
|
"themesList.@each.default",
|
||||||
|
"filterTerm"
|
||||||
)
|
)
|
||||||
activeThemes(themes) {
|
activeThemes(themes) {
|
||||||
|
let results;
|
||||||
if (this.componentsTabActive) {
|
if (this.componentsTabActive) {
|
||||||
return themes.filter((theme) => theme.get("parent_themes.length") > 0);
|
results = themes.filter((theme) => theme.get("parent_themes.length") > 0);
|
||||||
} else {
|
} else {
|
||||||
return themes
|
results = themes
|
||||||
.filter((theme) => theme.get("user_selectable") || theme.get("default"))
|
.filter((theme) => theme.get("user_selectable") || theme.get("default"))
|
||||||
.sort((a, b) => {
|
.sort((a, b) => {
|
||||||
if (a.get("default") && !b.get("default")) {
|
if (a.get("default") && !b.get("default")) {
|
||||||
|
@ -66,16 +77,29 @@ export default Component.extend({
|
||||||
.localeCompare(b.get("name").toLowerCase());
|
.localeCompare(b.get("name").toLowerCase());
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
return this._filterThemes(results, this.filterTerm);
|
||||||
},
|
},
|
||||||
|
|
||||||
actions: {
|
_filterThemes(themes, term) {
|
||||||
changeView(newTab) {
|
term = term?.trim()?.toLowerCase();
|
||||||
if (newTab !== this.currentTab) {
|
if (!term) {
|
||||||
this.set("currentTab", newTab);
|
return themes;
|
||||||
|
}
|
||||||
|
return themes.filter(({ name }) => name.toLowerCase().includes(term));
|
||||||
|
},
|
||||||
|
|
||||||
|
@action
|
||||||
|
changeView(newTab) {
|
||||||
|
if (newTab !== this.currentTab) {
|
||||||
|
this.set("currentTab", newTab);
|
||||||
|
if (!this.showFilter) {
|
||||||
|
this.set("filterTerm", null);
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
navigateToTheme(theme) {
|
},
|
||||||
this.router.transitionTo("adminCustomizeThemes.show", theme);
|
|
||||||
},
|
@action
|
||||||
|
navigateToTheme(theme) {
|
||||||
|
this.router.transitionTo("adminCustomizeThemes.show", theme);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -15,6 +15,17 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="themes-list-container">
|
<div class="themes-list-container">
|
||||||
|
{{#if showFilter}}
|
||||||
|
<div class="themes-list-filter themes-list-item">
|
||||||
|
{{input
|
||||||
|
class="filter-input"
|
||||||
|
placeholder=(i18n "admin.customize.theme.filter_placeholder")
|
||||||
|
autocomplete="discourse"
|
||||||
|
value=(mut filterTerm)
|
||||||
|
}}
|
||||||
|
{{d-icon "search"}}
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
{{#if hasThemes}}
|
{{#if hasThemes}}
|
||||||
{{#if hasActiveThemes}}
|
{{#if hasActiveThemes}}
|
||||||
{{#each activeThemes as |theme|}}
|
{{#each activeThemes as |theme|}}
|
||||||
|
|
|
@ -6,26 +6,37 @@ import componentTest, {
|
||||||
import {
|
import {
|
||||||
count,
|
count,
|
||||||
discourseModule,
|
discourseModule,
|
||||||
|
exists,
|
||||||
|
query,
|
||||||
queryAll,
|
queryAll,
|
||||||
} from "discourse/tests/helpers/qunit-helpers";
|
} from "discourse/tests/helpers/qunit-helpers";
|
||||||
import hbs from "htmlbars-inline-precompile";
|
import hbs from "htmlbars-inline-precompile";
|
||||||
|
import { click, fillIn } from "@ember/test-helpers";
|
||||||
|
|
||||||
|
function createThemes(itemsCount, customAttributesCallback) {
|
||||||
|
return [...Array(itemsCount)].map((_, i) => {
|
||||||
|
const attrs = { name: `Theme ${i + 1}` };
|
||||||
|
if (customAttributesCallback) {
|
||||||
|
Object.assign(attrs, customAttributesCallback(i + 1));
|
||||||
|
}
|
||||||
|
return Theme.create(attrs);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
discourseModule("Integration | Component | themes-list", function (hooks) {
|
discourseModule("Integration | Component | themes-list", function (hooks) {
|
||||||
setupRenderingTest(hooks);
|
setupRenderingTest(hooks);
|
||||||
componentTest("current tab is themes", {
|
componentTest("current tab is themes", {
|
||||||
template: hbs`{{themes-list themes=themes components=components currentTab=currentTab}}`,
|
template: hbs`{{themes-list themes=themes components=components currentTab=currentTab}}`,
|
||||||
beforeEach() {
|
beforeEach() {
|
||||||
this.themes = [1, 2, 3, 4, 5].map((num) =>
|
this.themes = createThemes(5);
|
||||||
Theme.create({ name: `Theme ${num}` })
|
this.components = createThemes(5, (n) => {
|
||||||
);
|
return {
|
||||||
this.components = [1, 2, 3, 4, 5].map((num) =>
|
name: `Child ${n}`,
|
||||||
Theme.create({
|
|
||||||
name: `Child ${num}`,
|
|
||||||
component: true,
|
component: true,
|
||||||
parentThemes: [this.themes[num - 1]],
|
parentThemes: [this.themes[n - 1]],
|
||||||
parent_themes: [1, 2, 3, 4, 5],
|
parent_themes: [1, 2, 3, 4, 5],
|
||||||
})
|
};
|
||||||
);
|
});
|
||||||
this.setProperties({
|
this.setProperties({
|
||||||
themes: this.themes,
|
themes: this.themes,
|
||||||
components: this.components,
|
components: this.components,
|
||||||
|
@ -94,17 +105,15 @@ discourseModule("Integration | Component | themes-list", function (hooks) {
|
||||||
componentTest("current tab is components", {
|
componentTest("current tab is components", {
|
||||||
template: hbs`{{themes-list themes=themes components=components currentTab=currentTab}}`,
|
template: hbs`{{themes-list themes=themes components=components currentTab=currentTab}}`,
|
||||||
beforeEach() {
|
beforeEach() {
|
||||||
this.themes = [1, 2, 3, 4, 5].map((num) =>
|
this.themes = createThemes(5);
|
||||||
Theme.create({ name: `Theme ${num}` })
|
this.components = createThemes(5, (n) => {
|
||||||
);
|
return {
|
||||||
this.components = [1, 2, 3, 4, 5].map((num) =>
|
name: `Child ${n}`,
|
||||||
Theme.create({
|
|
||||||
name: `Child ${num}`,
|
|
||||||
component: true,
|
component: true,
|
||||||
parentThemes: [this.themes[num - 1]],
|
parentThemes: [this.themes[n - 1]],
|
||||||
parent_themes: [1, 2, 3, 4, 5],
|
parent_themes: [1, 2, 3, 4, 5],
|
||||||
})
|
};
|
||||||
);
|
});
|
||||||
this.setProperties({
|
this.setProperties({
|
||||||
themes: this.themes,
|
themes: this.themes,
|
||||||
components: this.components,
|
components: this.components,
|
||||||
|
@ -144,4 +153,139 @@ discourseModule("Integration | Component | themes-list", function (hooks) {
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
componentTest(
|
||||||
|
"themes filter is not visible when there are less than 10 themes",
|
||||||
|
{
|
||||||
|
template: hbs`{{themes-list themes=themes components=[] currentTab=currentTab}}`,
|
||||||
|
|
||||||
|
beforeEach() {
|
||||||
|
const themes = createThemes(9);
|
||||||
|
this.setProperties({
|
||||||
|
themes,
|
||||||
|
currentTab: THEMES,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
async test(assert) {
|
||||||
|
assert.ok(
|
||||||
|
!exists(".themes-list-filter"),
|
||||||
|
"filter input not shown when we have fewer than 10 themes"
|
||||||
|
);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
componentTest(
|
||||||
|
"themes filter keeps themes whose names include the filter term",
|
||||||
|
{
|
||||||
|
template: hbs`{{themes-list themes=themes components=[] currentTab=currentTab}}`,
|
||||||
|
|
||||||
|
beforeEach() {
|
||||||
|
const themes = ["osama", "OsAmaa", "osAMA 1234"]
|
||||||
|
.map((name) => Theme.create({ name: `Theme ${name}` }))
|
||||||
|
.concat(createThemes(7));
|
||||||
|
this.setProperties({
|
||||||
|
themes,
|
||||||
|
currentTab: THEMES,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
async test(assert) {
|
||||||
|
assert.ok(exists(".themes-list-filter"));
|
||||||
|
await fillIn(".themes-list-filter .filter-input", " oSAma ");
|
||||||
|
assert.deepEqual(
|
||||||
|
Array.from(queryAll(".themes-list-item .name")).map((node) =>
|
||||||
|
node.textContent.trim()
|
||||||
|
),
|
||||||
|
["Theme osama", "Theme OsAmaa", "Theme osAMA 1234"],
|
||||||
|
"only themes whose names include the filter term are shown"
|
||||||
|
);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
componentTest(
|
||||||
|
"switching between themes and components tabs keeps the filter visible only if both tabs have at least 10 items",
|
||||||
|
{
|
||||||
|
template: hbs`{{themes-list themes=themes components=components currentTab=currentTab}}`,
|
||||||
|
|
||||||
|
beforeEach() {
|
||||||
|
const themes = createThemes(10, (n) => {
|
||||||
|
return { name: `Theme ${n}${n}` };
|
||||||
|
});
|
||||||
|
const components = createThemes(5, (n) => {
|
||||||
|
return {
|
||||||
|
name: `Component ${n}${n}`,
|
||||||
|
component: true,
|
||||||
|
parent_themes: [1],
|
||||||
|
parentThemes: [1],
|
||||||
|
};
|
||||||
|
});
|
||||||
|
this.setProperties({
|
||||||
|
themes,
|
||||||
|
components,
|
||||||
|
currentTab: THEMES,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
async test(assert) {
|
||||||
|
await fillIn(".themes-list-filter .filter-input", "11");
|
||||||
|
assert.equal(
|
||||||
|
query(".themes-list-container").textContent.trim(),
|
||||||
|
"Theme 11",
|
||||||
|
"only 1 theme is shown"
|
||||||
|
);
|
||||||
|
await click(".themes-list-header .components-tab");
|
||||||
|
assert.ok(
|
||||||
|
!exists(".themes-list-filter"),
|
||||||
|
"filter input/term do not persist when we switch to the other" +
|
||||||
|
" tab because it has fewer than 10 items"
|
||||||
|
);
|
||||||
|
assert.deepEqual(
|
||||||
|
Array.from(queryAll(".themes-list-item .name")).map((node) =>
|
||||||
|
node.textContent.trim()
|
||||||
|
),
|
||||||
|
[
|
||||||
|
"Component 11",
|
||||||
|
"Component 22",
|
||||||
|
"Component 33",
|
||||||
|
"Component 44",
|
||||||
|
"Component 55",
|
||||||
|
],
|
||||||
|
"all components are shown"
|
||||||
|
);
|
||||||
|
|
||||||
|
this.set(
|
||||||
|
"components",
|
||||||
|
this.components.concat(
|
||||||
|
createThemes(5, (n) => {
|
||||||
|
n += 5;
|
||||||
|
return {
|
||||||
|
name: `Component ${n}${n}`,
|
||||||
|
component: true,
|
||||||
|
parent_themes: [1],
|
||||||
|
parentThemes: [1],
|
||||||
|
};
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
assert.ok(
|
||||||
|
exists(".themes-list-filter"),
|
||||||
|
"filter is now shown for the components tab"
|
||||||
|
);
|
||||||
|
|
||||||
|
await fillIn(".themes-list-filter .filter-input", "66");
|
||||||
|
assert.equal(
|
||||||
|
query(".themes-list-container").textContent.trim(),
|
||||||
|
"Component 66",
|
||||||
|
"only 1 component is shown"
|
||||||
|
);
|
||||||
|
|
||||||
|
await click(".themes-list-header .themes-tab");
|
||||||
|
assert.equal(
|
||||||
|
query(".themes-list-container").textContent.trim(),
|
||||||
|
"Theme 66",
|
||||||
|
"filter term persisted between tabs because both have more than 10 items"
|
||||||
|
);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
|
@ -295,6 +295,37 @@
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.themes-list-filter {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
background: var(--secondary);
|
||||||
|
z-index: z("base");
|
||||||
|
height: 3em;
|
||||||
|
|
||||||
|
.d-icon {
|
||||||
|
position: absolute;
|
||||||
|
padding-left: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-input {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
margin: 0;
|
||||||
|
border: 0;
|
||||||
|
padding-left: 2em;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
outline: 0;
|
||||||
|
|
||||||
|
~ .d-icon {
|
||||||
|
color: var(--tertiary-hover);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.theme.settings {
|
.theme.settings {
|
||||||
|
|
|
@ -4230,6 +4230,7 @@ en:
|
||||||
theme: "Theme"
|
theme: "Theme"
|
||||||
component: "Component"
|
component: "Component"
|
||||||
components: "Components"
|
components: "Components"
|
||||||
|
filter_placeholder: "type to filter…"
|
||||||
theme_name: "Theme name"
|
theme_name: "Theme name"
|
||||||
component_name: "Component name"
|
component_name: "Component name"
|
||||||
themes_intro: "Select an existing theme or install a new one to get started"
|
themes_intro: "Select an existing theme or install a new one to get started"
|
||||||
|
|
Loading…
Reference in New Issue